diff --git a/apps/desktop/src/ipc/methods/window.test.ts b/apps/desktop/src/ipc/methods/window.test.ts index 0b43fa9e6ef9..1dd1ca8d0efb 100644 --- a/apps/desktop/src/ipc/methods/window.test.ts +++ b/apps/desktop/src/ipc/methods/window.test.ts @@ -24,7 +24,6 @@ import * as DesktopBackendPool from "../../backend/DesktopBackendPool.ts"; import * as ElectronDialog from "../../electron/ElectronDialog.ts"; import * as ElectronWindow from "../../electron/ElectronWindow.ts"; import * as DesktopAppSettings from "../../settings/DesktopAppSettings.ts"; -import type { DesktopSettings } from "../../settings/DesktopAppSettings.ts"; import { getLocalEnvironmentBootstraps, getWindowFullscreenState, @@ -213,7 +212,10 @@ describe("pasteAsText", () => { }); describe("pickProjectFavicon", () => { - const pickerLayer = (pickFiles: () => Effect.Effect>, settings?: DesktopSettings) => + const pickerLayer = ( + pickFiles: () => Effect.Effect>, + settings?: DesktopAppSettings.DesktopSettings, + ) => Layer.mergeAll( Layer.mock(ElectronDialog.ElectronDialog)({ pickFiles }), Layer.mock(ElectronWindow.ElectronWindow)({ diff --git a/apps/desktop/src/preview/BrowserImport/LinuxBrowserSecret.ts b/apps/desktop/src/preview/BrowserImport/LinuxBrowserSecret.ts index 1f5fe02bc454..b2c2d3a47e6b 100644 --- a/apps/desktop/src/preview/BrowserImport/LinuxBrowserSecret.ts +++ b/apps/desktop/src/preview/BrowserImport/LinuxBrowserSecret.ts @@ -3,7 +3,7 @@ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; -import { DesktopEnvironment } from "../../app/DesktopEnvironment.ts"; +import * as DesktopEnvironment from "../../app/DesktopEnvironment.ts"; /** Absolute path to the helper shipped with this desktop instance. */ export const LinuxBrowserSecretPath = Context.Reference( @@ -14,7 +14,7 @@ export const LinuxBrowserSecretPath = Context.Reference( export const layer = Layer.effect( LinuxBrowserSecretPath, Effect.gen(function* () { - const environment = yield* DesktopEnvironment; + const environment = yield* DesktopEnvironment.DesktopEnvironment; if (environment.platform !== "linux") return undefined; const fileSystem = yield* FileSystem.FileSystem; const relative = environment.path.join("browser-secret", "t3-browser-secret"); diff --git a/apps/mobile/src/connection/background-activity.ts b/apps/mobile/src/connection/background-activity.ts index 3364eed3c2d1..8b61b0053ea3 100644 --- a/apps/mobile/src/connection/background-activity.ts +++ b/apps/mobile/src/connection/background-activity.ts @@ -43,7 +43,7 @@ export const mobileBackgroundActivityObserverLayer = Layer.succeed( export const mobileBackgroundActivityReporterLayer = Layer.effectDiscard( Effect.gen(function* () { - const registry = yield* EnvironmentRegistry; + const registry = yield* EnvironmentRegistry.EnvironmentRegistry; const storage = yield* MobileStorage.MobileStorage; const clientId = yield* storage.loadOrCreateAgentAwarenessDeviceId.pipe( Effect.map((deviceId) => `mobile-${deviceId}`), diff --git a/apps/mobile/src/connection/environment-cache-store.test.ts b/apps/mobile/src/connection/environment-cache-store.test.ts index 3f8386e912f7..6ca84b2a5126 100644 --- a/apps/mobile/src/connection/environment-cache-store.test.ts +++ b/apps/mobile/src/connection/environment-cache-store.test.ts @@ -21,7 +21,7 @@ import { vi } from "vite-plus/test"; import * as Deferred from "effect/Deferred"; import * as Option from "effect/Option"; -import { type ClientCacheKind, MobileDatabase } from "../persistence/mobile-database"; +import * as MobileDatabase from "../persistence/mobile-database"; import { make } from "./environment-cache-store"; import { encodeStoredShellSnapshot } from "./shell-cache-encoding"; @@ -162,7 +162,11 @@ const REFS: VcsListRefsResult = { totalCount: 1, }; -function cacheId(environmentId: EnvironmentId, kind: ClientCacheKind, cacheKey: string) { +function cacheId( + environmentId: EnvironmentId, + kind: MobileDatabase.ClientCacheKind, + cacheKey: string, +) { return `${environmentId}:${kind}:${cacheKey}`; } @@ -170,7 +174,7 @@ function makeDatabase() { const values = new Map(); const schemaVersions = new Map(); const removed: Array = []; - const database = MobileDatabase.of({ + const database = MobileDatabase.MobileDatabase.of({ loadCache: (environmentId, kind, cacheKey) => Effect.succeed(Option.fromUndefinedOr(values.get(cacheId(environmentId, kind, cacheKey)))), listCache: (kind) => @@ -215,7 +219,9 @@ describe("mobile SQLite environment cache store", () => { it.effect("round-trips V2 shell and thread DateTime fields with the shared cache schema", () => Effect.gen(function* () { const memory = makeDatabase(); - const store = yield* make().pipe(Effect.provideService(MobileDatabase, memory.database)); + const store = yield* make().pipe( + Effect.provideService(MobileDatabase.MobileDatabase, memory.database), + ); yield* store.saveShell(ENVIRONMENT_ID, SHELL_SNAPSHOT); yield* store.saveThread(ENVIRONMENT_ID, THREAD_SNAPSHOT); @@ -251,7 +257,9 @@ describe("mobile SQLite environment cache store", () => { it.effect("round-trips schema-validated VCS refs", () => Effect.gen(function* () { const memory = makeDatabase(); - const store = yield* make().pipe(Effect.provideService(MobileDatabase, memory.database)); + const store = yield* make().pipe( + Effect.provideService(MobileDatabase.MobileDatabase, memory.database), + ); yield* store.saveVcsRefs(ENVIRONMENT_ID, "/repo", REFS); @@ -262,7 +270,9 @@ describe("mobile SQLite environment cache store", () => { it.effect("deletes a corrupt cache record and treats it as a miss", () => Effect.gen(function* () { const memory = makeDatabase(); - const store = yield* make().pipe(Effect.provideService(MobileDatabase, memory.database)); + const store = yield* make().pipe( + Effect.provideService(MobileDatabase.MobileDatabase, memory.database), + ); const id = cacheId(ENVIRONMENT_ID, "vcs-refs", "/repo"); memory.values.set(id, "{not-json"); @@ -274,7 +284,9 @@ describe("mobile SQLite environment cache store", () => { it.effect("removes one persisted VCS ref snapshot", () => Effect.gen(function* () { const memory = makeDatabase(); - const store = yield* make().pipe(Effect.provideService(MobileDatabase, memory.database)); + const store = yield* make().pipe( + Effect.provideService(MobileDatabase.MobileDatabase, memory.database), + ); yield* store.saveVcsRefs(ENVIRONMENT_ID, "/repo", REFS); yield* store.removeVcsRefs(ENVIRONMENT_ID, "/repo"); @@ -287,7 +299,9 @@ describe("mobile SQLite environment cache store", () => { it.effect("clears every persisted VCS ref snapshot in one environment", () => Effect.gen(function* () { const memory = makeDatabase(); - const store = yield* make().pipe(Effect.provideService(MobileDatabase, memory.database)); + const store = yield* make().pipe( + Effect.provideService(MobileDatabase.MobileDatabase, memory.database), + ); const otherEnvironmentId = EnvironmentId.make("environment-2"); yield* store.saveVcsRefs(ENVIRONMENT_ID, "/repo", REFS); yield* store.saveVcsRefs(ENVIRONMENT_ID, "/repo-worktree", REFS); @@ -304,7 +318,9 @@ describe("mobile SQLite environment cache store", () => { it.effect("clears one environment without touching another", () => Effect.gen(function* () { const memory = makeDatabase(); - const store = yield* make().pipe(Effect.provideService(MobileDatabase, memory.database)); + const store = yield* make().pipe( + Effect.provideService(MobileDatabase.MobileDatabase, memory.database), + ); const otherEnvironmentId = EnvironmentId.make("environment-2"); yield* store.saveVcsRefs(ENVIRONMENT_ID, "/repo", REFS); yield* store.saveVcsRefs(otherEnvironmentId, "/repo", REFS); diff --git a/apps/mobile/src/connection/environment-cache-store.ts b/apps/mobile/src/connection/environment-cache-store.ts index cd2cda0f50d7..70c2a91f67af 100644 --- a/apps/mobile/src/connection/environment-cache-store.ts +++ b/apps/mobile/src/connection/environment-cache-store.ts @@ -1,9 +1,8 @@ import { - ConnectionPersistenceError, - EnvironmentCacheStore, ORCHESTRATION_CACHE_SCHEMA_VERSION, StoredOrchestrationShellSnapshot, StoredOrchestrationThreadSnapshot, + Persistence, } from "@t3tools/client-runtime/platform"; import { type EnvironmentId, ServerConfig, VcsListRefsResult } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; @@ -49,10 +48,10 @@ const encodeStoredServerConfig = Schema.encodeEffect(Schema.fromJsonString(Store const decodeStoredVcsRefs = Schema.decodeUnknownEffect(Schema.fromJsonString(StoredVcsRefs)); const encodeStoredVcsRefs = Schema.encodeEffect(Schema.fromJsonString(StoredVcsRefs)); -type CacheOperation = ConnectionPersistenceError["operation"]; +type CacheOperation = Persistence.ConnectionPersistenceError["operation"]; function persistenceError(operation: CacheOperation, cause: unknown) { - return new ConnectionPersistenceError({ + return new Persistence.ConnectionPersistenceError({ operation, message: `Could not ${operation.replaceAll("-", " ")}: ${String(cause)}`, }); @@ -70,7 +69,7 @@ function loadDecodedCache(input: { readonly operation: CacheOperation; readonly decode: (raw: string) => Effect.Effect; readonly select: (value: A) => Option.Option; -}): Effect.Effect, ConnectionPersistenceError> { +}): Effect.Effect, Persistence.ConnectionPersistenceError> { return input.database.loadCache(input.environmentId, input.kind, input.cacheKey).pipe( Effect.mapError(mapDatabaseError(input.operation)), Effect.flatMap( @@ -103,7 +102,7 @@ function loadDecodedCache(input: { export const make = Effect.fn("MobileEnvironmentCacheStore.make")(function* () { const database = yield* MobileDatabase.MobileDatabase; attachProjectFaviconDatabase(database); - return EnvironmentCacheStore.of({ + return Persistence.EnvironmentCacheStore.of({ loadShell: Effect.fn("MobileEnvironmentCache.loadShell")((environmentId) => loadDecodedCache({ database, @@ -233,4 +232,4 @@ export const make = Effect.fn("MobileEnvironmentCacheStore.make")(function* () { }); }); -export const layer = Layer.effect(EnvironmentCacheStore, make()); +export const layer = Layer.effect(Persistence.EnvironmentCacheStore, make()); diff --git a/apps/mobile/src/connection/onboarding.ts b/apps/mobile/src/connection/onboarding.ts index 60a660cb4b8f..fdf1f527feb7 100644 --- a/apps/mobile/src/connection/onboarding.ts +++ b/apps/mobile/src/connection/onboarding.ts @@ -15,7 +15,7 @@ export const connectPairingUrl = createRuntimeCommand(connectionAtomRuntime, { scheduler: onboardingScheduler, concurrency: { mode: "singleFlight", key: (pairingUrl: string) => pairingUrl }, execute: (pairingUrl: string) => - ConnectionOnboarding.pipe( + ConnectionOnboarding.ConnectionOnboarding.pipe( Effect.flatMap((onboarding) => onboarding.registerPairing({ pairingUrl })), ), }); @@ -31,5 +31,8 @@ export const updateBearerConnection = createRuntimeCommand(connectionAtomRuntime readonly environmentId: EnvironmentId; readonly label: string; readonly httpBaseUrl: string; - }) => ConnectionOnboarding.pipe(Effect.flatMap((onboarding) => onboarding.updateBearer(input))), + }) => + ConnectionOnboarding.ConnectionOnboarding.pipe( + Effect.flatMap((onboarding) => onboarding.updateBearer(input)), + ), }); diff --git a/apps/mobile/src/connection/platform.ts b/apps/mobile/src/connection/platform.ts index f7fa30c8f230..251264c9c9ea 100644 --- a/apps/mobile/src/connection/platform.ts +++ b/apps/mobile/src/connection/platform.ts @@ -1,11 +1,7 @@ import { - ClientPresentation, - CloudSession, - EnvironmentOwnedDataCleanup, + ClientCapabilities, PlatformConnectionSource, - PrimaryEnvironmentAuth, - RelayDeviceIdentity, - SshEnvironmentGateway, + Persistence, } from "@t3tools/client-runtime/platform"; import { ConnectionBlockedError, @@ -116,8 +112,8 @@ const capabilitiesLayer = Layer.effectContext( Effect.gen(function* () { const storage = yield* MobileStorage.MobileStorage; return Context.make( - CloudSession, - CloudSession.of({ + ClientCapabilities.CloudSession, + ClientCapabilities.CloudSession.of({ identity: Effect.sync(() => Option.fromNullishOr(appAtomRegistry.get(managedRelaySessionAtom)), ), @@ -149,12 +145,14 @@ const capabilitiesLayer = Layer.effectContext( }), ).pipe( Context.add( - PrimaryEnvironmentAuth, - PrimaryEnvironmentAuth.of({ bearerToken: Effect.succeed(Option.none()) }), + ClientCapabilities.PrimaryEnvironmentAuth, + ClientCapabilities.PrimaryEnvironmentAuth.of({ + bearerToken: Effect.succeed(Option.none()), + }), ), Context.add( - RelayDeviceIdentity, - RelayDeviceIdentity.of({ + ClientCapabilities.RelayDeviceIdentity, + ClientCapabilities.RelayDeviceIdentity.of({ deviceId: storage.loadOrCreateAgentAwarenessDeviceId.pipe( Effect.mapError( (cause) => @@ -168,15 +166,15 @@ const capabilitiesLayer = Layer.effectContext( }), ), Context.add( - ClientPresentation, - ClientPresentation.of({ + ClientCapabilities.ClientPresentation, + ClientCapabilities.ClientPresentation.of({ metadata: authClientMetadata(Constants.expoConfig?.version), scopes: AuthStandardClientScopes, }), ), Context.add( - SshEnvironmentGateway, - SshEnvironmentGateway.of({ + ClientCapabilities.SshEnvironmentGateway, + ClientCapabilities.SshEnvironmentGateway.of({ provision: () => Effect.fail( new ConnectionBlockedError({ @@ -199,8 +197,8 @@ const capabilitiesLayer = Layer.effectContext( ); const platformConnectionSourceLayer = Layer.succeed( - PlatformConnectionSource, - PlatformConnectionSource.of({ + PlatformConnectionSource.PlatformConnectionSource, + PlatformConnectionSource.PlatformConnectionSource.of({ registrations: Stream.empty, }), ); @@ -213,8 +211,8 @@ const providedCapabilitiesLayer = capabilitiesLayer.pipe( ); const environmentOwnedDataCleanupLayer = Layer.succeed( - EnvironmentOwnedDataCleanup, - EnvironmentOwnedDataCleanup.of({ + Persistence.EnvironmentOwnedDataCleanup, + Persistence.EnvironmentOwnedDataCleanup.of({ clear: (environmentId) => Effect.all( [ diff --git a/apps/mobile/src/connection/runtime.ts b/apps/mobile/src/connection/runtime.ts index 2df3f728035e..4bf506ae0ba1 100644 --- a/apps/mobile/src/connection/runtime.ts +++ b/apps/mobile/src/connection/runtime.ts @@ -1,8 +1,8 @@ import { Connection } from "@t3tools/client-runtime/connection"; -import { shellSnapshotLoaderLayer } from "@t3tools/client-runtime/state/shell"; +import { ShellSnapshotLoader } from "@t3tools/client-runtime/state/shell"; import { boundedThreadSnapshotLoaderLayer, - threadHistoryControllerLayer, + ThreadHistoryController, } from "@t3tools/client-runtime/state/threads"; import * as Layer from "effect/Layer"; import { Atom } from "effect/unstable/reactivity"; @@ -25,8 +25,8 @@ const providedConnectionPlatformLayer = connectionPlatformLayer.pipe( const snapshotLoaderLayer = Layer.mergeAll( boundedThreadSnapshotLoaderLayer, - shellSnapshotLoaderLayer, - threadHistoryControllerLayer, + ShellSnapshotLoader.layer, + ThreadHistoryController.layer, ); type ConnectionLayerSource = diff --git a/apps/mobile/src/connection/storage.test.ts b/apps/mobile/src/connection/storage.test.ts index 34ba6ed850a9..7de8ded26639 100644 --- a/apps/mobile/src/connection/storage.test.ts +++ b/apps/mobile/src/connection/storage.test.ts @@ -13,12 +13,12 @@ vi.mock("expo-secure-store", () => ({ })); import { CONNECTION_CATALOG_KEY, LEGACY_CONNECTIONS_KEY, make } from "./catalog-store"; -import { MobileSecureStorage } from "../persistence/mobile-secure-storage"; +import * as MobileSecureStorage from "../persistence/mobile-secure-storage"; function makeStorage(initial: Readonly>) { const values = new Map(Object.entries(initial)); const deleted: Array = []; - const storage = MobileSecureStorage.of({ + const storage = MobileSecureStorage.MobileSecureStorage.of({ getItem: (key) => Effect.sync(() => values.get(key) ?? null), setItem: (key, value) => Effect.sync(() => { @@ -40,7 +40,7 @@ describe("mobile connection catalog storage", () => { [CONNECTION_CATALOG_KEY]: "{not-json", }); const catalog = yield* make().pipe( - Effect.provideService(MobileSecureStorage, memory.storage), + Effect.provideService(MobileSecureStorage.MobileSecureStorage, memory.storage), ); expect((yield* catalog.read).targets).toEqual([]); @@ -54,7 +54,7 @@ describe("mobile connection catalog storage", () => { [LEGACY_CONNECTIONS_KEY]: JSON.stringify({ connections: [{ invalid: true }] }), }); const catalog = yield* make().pipe( - Effect.provideService(MobileSecureStorage, memory.storage), + Effect.provideService(MobileSecureStorage.MobileSecureStorage, memory.storage), ); expect((yield* catalog.read).targets).toEqual([]); @@ -83,7 +83,7 @@ describe("mobile connection catalog storage", () => { }), }); const catalog = yield* make().pipe( - Effect.provideService(MobileSecureStorage, memory.storage), + Effect.provideService(MobileSecureStorage.MobileSecureStorage, memory.storage), ); expect((yield* catalog.read).targets).toHaveLength(1); diff --git a/apps/mobile/src/connection/storage.ts b/apps/mobile/src/connection/storage.ts index fa37dfe01a9e..d2fbdd8cd9a9 100644 --- a/apps/mobile/src/connection/storage.ts +++ b/apps/mobile/src/connection/storage.ts @@ -1,13 +1,11 @@ import { - ConnectionPersistenceError, - ConnectionRegistrationStore, - ConnectionTargetStore, putRemoteDpopTokenInCatalog, registerConnectionInCatalog, removeConnectionFromCatalog, setConnectionEnabledInCatalog, removeCatalogValue, replaceCatalogValue, + Persistence, } from "@t3tools/client-runtime/platform"; import { TokenStore } from "@t3tools/client-runtime/authorization"; import { @@ -33,7 +31,7 @@ function targetPersistenceError( | "set-connection-enabled", error: ConnectionTransientError, ) { - return new ConnectionPersistenceError({ + return new Persistence.ConnectionPersistenceError({ operation, message: error.message, }); @@ -48,7 +46,7 @@ export const connectionStorageLayer = Layer.effectContext( catalog.update((document) => ({ ...document, githubRoutingPermissions })), }); - const targetStore = ConnectionTargetStore.of({ + const targetStore = Persistence.ConnectionTargetStore.of({ list: catalog.read.pipe( Effect.map((document) => document.targets), Effect.mapError((error) => targetPersistenceError("list-targets", error)), @@ -58,7 +56,7 @@ export const connectionStorageLayer = Layer.effectContext( Effect.mapError((error) => targetPersistenceError("list-disabled-targets", error)), ), }); - const registrationStore = ConnectionRegistrationStore.of({ + const registrationStore = Persistence.ConnectionRegistrationStore.of({ register: (registration) => catalog .update((document) => registerConnectionInCatalog(document, registration)) @@ -145,9 +143,9 @@ export const connectionStorageLayer = Layer.effectContext( ), })), }); - return Context.make(ConnectionTargetStore, targetStore).pipe( + return Context.make(Persistence.ConnectionTargetStore, targetStore).pipe( Context.add(GitHubRoutingPermissions, githubRoutingPermissions), - Context.add(ConnectionRegistrationStore, registrationStore), + Context.add(Persistence.ConnectionRegistrationStore, registrationStore), Context.add(ProfileStore.ConnectionProfileStore, profileStore), Context.add(CredentialStore.ConnectionCredentialStore, credentialStore), Context.add(TokenStore.RemoteDpopAccessTokenStore, remoteTokenStore), diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts index 0ee3e59b9828..501904204867 100644 --- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts +++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts @@ -7,7 +7,7 @@ import * as Layer from "effect/Layer"; import { HttpClient } from "effect/unstable/http"; import type { SavedRemoteConnection } from "../../lib/connection"; -import { MobileStorage } from "../../persistence/mobile-storage"; +import * as MobileStorage from "../../persistence/mobile-storage"; import { CloudEnvironmentLinkError, linkEnvironmentToCloudWithPreference, @@ -50,8 +50,8 @@ const testLayer = Layer.mergeAll( HttpClient.make(() => Effect.die("unexpected HTTP request")), ), Layer.succeed( - MobileStorage, - MobileStorage.of({ + MobileStorage.MobileStorage, + MobileStorage.MobileStorage.of({ loadSavedConnections: Effect.succeed([]), saveConnection: () => Effect.void, clearSavedConnection: () => Effect.void, diff --git a/apps/mobile/src/features/cloud/cloud-drafts.ts b/apps/mobile/src/features/cloud/cloud-drafts.ts index 032df4752266..1cc94da52bb3 100644 --- a/apps/mobile/src/features/cloud/cloud-drafts.ts +++ b/apps/mobile/src/features/cloud/cloud-drafts.ts @@ -23,7 +23,7 @@ export class CloudDraftArchiveError extends Schema.TaggedError Effect.void, clearSavedConnection: () => Effect.void, @@ -104,7 +104,7 @@ const withCloudServices = ( | HttpClient.HttpClient | ManagedRelay.ManagedRelayClient | ManagedRelay.ManagedRelayDpopSigner - | MobileStorage + | MobileStorage.MobileStorage >, ) => effect.pipe(Effect.provide(cloudClientLayer())); diff --git a/apps/mobile/src/persistence/mobile-database.test.ts b/apps/mobile/src/persistence/mobile-database.test.ts index 56c3e31657ec..244331471fd0 100644 --- a/apps/mobile/src/persistence/mobile-database.test.ts +++ b/apps/mobile/src/persistence/mobile-database.test.ts @@ -6,7 +6,7 @@ const openDatabaseAsync = vi.hoisted(() => vi.fn()); vi.mock("expo-sqlite", () => ({ openDatabaseAsync })); -import { decodeLegacyCacheRecord, make } from "./mobile-database"; +import * as MobileDatabase from "./mobile-database"; describe("mobile database legacy cache migration", () => { it.effect("keeps acquisition failures typed on database operations", () => @@ -14,7 +14,7 @@ describe("mobile database legacy cache migration", () => { Effect.gen(function* () { openDatabaseAsync.mockRejectedValueOnce(new Error("SQLite unavailable")); - const database = yield* make; + const database = yield* MobileDatabase.make; const result = yield* Effect.result(database.loadPreferencesJson); expect(result).toMatchObject({ @@ -33,7 +33,7 @@ describe("mobile database legacy cache migration", () => { snapshot: {}, }); - expect(decodeLegacyCacheRecord("connection-thread-snapshots", payload)).toEqual({ + expect(MobileDatabase.decodeLegacyCacheRecord("connection-thread-snapshots", payload)).toEqual({ environmentId: "environment-1", kind: "thread", cacheKey: "thread-1", @@ -50,7 +50,7 @@ describe("mobile database legacy cache migration", () => { snapshot: {}, }); - expect(decodeLegacyCacheRecord("shell-snapshots", payload)).toEqual({ + expect(MobileDatabase.decodeLegacyCacheRecord("shell-snapshots", payload)).toEqual({ environmentId: "environment-1", kind: "shell", cacheKey: "snapshot", @@ -60,9 +60,9 @@ describe("mobile database legacy cache migration", () => { }); it("skips malformed legacy records", () => { - expect(decodeLegacyCacheRecord("connection-vcs-refs", "{not-json")).toBeNull(); + expect(MobileDatabase.decodeLegacyCacheRecord("connection-vcs-refs", "{not-json")).toBeNull(); expect( - decodeLegacyCacheRecord( + MobileDatabase.decodeLegacyCacheRecord( "connection-vcs-refs", JSON.stringify({ schemaVersion: 1, environmentId: "environment-1" }), ), diff --git a/apps/mobile/src/state/client-cache-state.ts b/apps/mobile/src/state/client-cache-state.ts index 0060e6ab0789..b8717facf2e6 100644 --- a/apps/mobile/src/state/client-cache-state.ts +++ b/apps/mobile/src/state/client-cache-state.ts @@ -2,7 +2,7 @@ import type { EnvironmentId } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import { Atom } from "effect/unstable/reactivity"; -import { type ClientCacheKind, MobileDatabase } from "../persistence/mobile-database"; +import * as MobileDatabase from "../persistence/mobile-database"; import { projectFaviconDatabaseCache } from "../lib/projectFaviconDatabaseCache"; import * as Runtime from "../lib/runtime"; @@ -10,7 +10,7 @@ export interface EnvironmentClientCacheSummary { readonly environmentId: EnvironmentId; readonly recordCount: number; readonly payloadBytes: number; - readonly kinds: Readonly>>; + readonly kinds: Readonly>>; } export interface ClientCacheSummary { @@ -26,7 +26,7 @@ export type ClientCacheClearScope = function aggregateCacheSummary( rows: ReadonlyArray<{ readonly environmentId: EnvironmentId; - readonly kind: ClientCacheKind; + readonly kind: MobileDatabase.ClientCacheKind; readonly recordCount: number; readonly payloadBytes: number; }>, @@ -63,7 +63,7 @@ const clientCacheRuntime = Atom.runtime(Runtime.runtimeContextLayer); export const clientCacheSummaryAtom = clientCacheRuntime .atom( - MobileDatabase.pipe( + MobileDatabase.MobileDatabase.pipe( Effect.flatMap((database) => database.inspectCaches), Effect.map(aggregateCacheSummary), ), @@ -77,7 +77,7 @@ export const clearClientCacheAtom = clientCacheRuntime ? projectFaviconDatabaseCache.clearAll() : projectFaviconDatabaseCache.clearEnvironment(scope.environmentId), ).pipe( - Effect.andThen(MobileDatabase), + Effect.andThen(MobileDatabase.MobileDatabase), Effect.flatMap((database) => scope.type === "all" ? database.clearAllCaches diff --git a/apps/mobile/src/state/preferences.test.ts b/apps/mobile/src/state/preferences.test.ts index 9a84d70cd61a..75fb23bdf173 100644 --- a/apps/mobile/src/state/preferences.test.ts +++ b/apps/mobile/src/state/preferences.test.ts @@ -26,13 +26,8 @@ vi.mock("../lib/runtime", async () => { }; }); -import type { Preferences } from "../persistence/mobile-preferences"; -import { - createMobilePreferencesState, - MobilePreferencesLoadError, - MobilePreferencesSaveError, - MobilePreferencesStore, -} from "./preferences"; +import { createMobilePreferencesState } from "./preferences"; +import * as MobilePreferences from "../persistence/mobile-preferences"; function deferred() { let resolve!: (value: A) => void; @@ -43,10 +38,10 @@ function deferred() { } function makePreferencesState( - service: Omit & - Partial>, + service: Omit & + Partial>, ) { - const completeService = MobilePreferencesStore.of({ + const completeService = MobilePreferences.MobilePreferencesStore.of({ ...service, update: service.update ?? @@ -56,19 +51,21 @@ function makePreferencesState( Effect.mapError((cause) => cause._tag === "MobilePreferencesSaveError" ? cause - : new MobilePreferencesSaveError({ cause }), + : new MobilePreferences.MobilePreferencesSaveError({ cause }), ), )), }); return createMobilePreferencesState( - Atom.runtime(Layer.succeed(MobilePreferencesStore, completeService)), + Atom.runtime(Layer.succeed(MobilePreferences.MobilePreferencesStore, completeService)), ); } describe("mobile preferences state", () => { it.effect("shares one preference load across consumers", () => Effect.gen(function* () { - const load = vi.fn(() => Promise.resolve({ baseFontSize: 17 })); + const load = vi.fn(() => + Promise.resolve({ baseFontSize: 17 }), + ); const state = makePreferencesState({ load: Effect.promise(load), savePatch: (patch) => Effect.succeed(patch), @@ -92,8 +89,10 @@ describe("mobile preferences state", () => { it.effect("preserves an optimistic patch when the initial load finishes later", () => Effect.gen(function* () { - const pendingLoad = deferred(); - const savePatch = vi.fn((patch: Partial) => Effect.succeed(patch)); + const pendingLoad = deferred(); + const savePatch = vi.fn((patch: Partial) => + Effect.succeed(patch), + ); const state = makePreferencesState({ load: Effect.promise(() => pendingLoad.promise), savePatch, @@ -130,7 +129,7 @@ describe("mobile preferences state", () => { it.effect("keeps both favorites when the React setter sends updates before a render", () => Effect.gen(function* () { - let persisted: Preferences = { modelFavorites: [] }; + let persisted: MobilePreferences.Preferences = { modelFavorites: [] }; const state = makePreferencesState({ load: Effect.succeed(persisted), savePatch: (patch) => @@ -205,7 +204,7 @@ describe("mobile preferences state", () => { Effect.gen(function* () { const state = makePreferencesState({ load: Effect.fail( - new MobilePreferencesLoadError({ + new MobilePreferences.MobilePreferencesLoadError({ cause: new Error("secure storage unavailable"), }), ), @@ -233,7 +232,11 @@ describe("mobile preferences state", () => { savePatch: (patch) => { saveCount += 1; return saveCount === 1 - ? Effect.fail(new MobilePreferencesSaveError({ cause: new Error("write failed") })) + ? Effect.fail( + new MobilePreferences.MobilePreferencesSaveError({ + cause: new Error("write failed"), + }), + ) : Effect.succeed(patch); }, }); @@ -270,7 +273,9 @@ describe("mobile preferences state", () => { const state = makePreferencesState({ load: Effect.succeed({ baseFontSize: 16 }), savePatch: () => - Effect.fail(new MobilePreferencesSaveError({ cause: new Error("write failed") })), + Effect.fail( + new MobilePreferences.MobilePreferencesSaveError({ cause: new Error("write failed") }), + ), }); const registry = AtomRegistry.make(); const unmountPreferences = registry.mount(state.preferencesAtom); @@ -306,7 +311,11 @@ describe("mobile preferences state", () => { saveCount += 1; return saveCount === 1 ? Effect.succeed({ baseFontSize: 14 }) - : Effect.fail(new MobilePreferencesSaveError({ cause: new Error("write failed") })); + : Effect.fail( + new MobilePreferences.MobilePreferencesSaveError({ + cause: new Error("write failed"), + }), + ); }, }); const registry = AtomRegistry.make(); diff --git a/apps/mobile/src/state/preferences.ts b/apps/mobile/src/state/preferences.ts index 7baff0251c15..b91bb20891bd 100644 --- a/apps/mobile/src/state/preferences.ts +++ b/apps/mobile/src/state/preferences.ts @@ -1,39 +1,39 @@ import * as Effect from "effect/Effect"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; -import { MobilePreferencesStore, type Preferences } from "../persistence/mobile-preferences"; +import * as MobilePreferences from "../persistence/mobile-preferences"; import * as Runtime from "../lib/runtime"; -export { - MobilePreferencesLoadError, - MobilePreferencesSaveError, - MobilePreferencesStore, -} from "../persistence/mobile-preferences"; - interface OptimisticPreferences { - readonly values: Partial; - readonly versions: Partial>; + readonly values: Partial; + readonly versions: Partial>; } // A bare function is interpreted by useAtomSet as an update to the command's // AsyncResult, not as a preference transform. Keep transforms inside a payload. type PreferencesUpdate = - | Partial - | { readonly transform: (current: Preferences) => Partial }; + | Partial + | { + readonly transform: ( + current: MobilePreferences.Preferences, + ) => Partial; + }; /** * Owns the device preference blob for the lifetime of the app registry. * Optimistic patches are kept separately so writes made while persistence is * still loading cannot be replaced by the eventual read result. */ -export function createMobilePreferencesState(runtime: Atom.AtomRuntime) { +export function createMobilePreferencesState( + runtime: Atom.AtomRuntime, +) { const storedPreferencesAtom = runtime .atom( - MobilePreferencesStore.pipe( + MobilePreferences.MobilePreferencesStore.pipe( Effect.flatMap((store) => store.load), Effect.catch((error) => Effect.logWarning("Could not load mobile preferences.", error).pipe( - Effect.as({}), + Effect.as({}), ), ), ), @@ -44,7 +44,7 @@ export function createMobilePreferencesState(runtime: Atom.AtomRuntime({}).pipe( + const confirmedPreferencesAtom = Atom.make({}).pipe( Atom.keepAlive, Atom.withLabel("mobile:preferences:confirmed"), ); @@ -74,14 +74,14 @@ export function createMobilePreferencesState(runtime: Atom.AtomRuntime) { + for (const key of Object.keys(patch) as Array) { versions[key] = version; } get.set(optimisticPatchAtom, { values: { ...current.values, ...patch }, versions, }); - return MobilePreferencesStore.pipe( + return MobilePreferences.MobilePreferencesStore.pipe( Effect.flatMap((store) => "transform" in update ? store.update(update.transform) : store.savePatch(patch), ), @@ -91,15 +91,17 @@ export function createMobilePreferencesState(runtime: Atom.AtomRuntime; const currentVersions = { ...optimistic.versions } as Record; - for (const key of Object.keys(patch) as Array) { + for (const key of Object.keys(patch) as Array) { if (optimistic.versions[key] === version) { delete values[key]; delete currentVersions[key]; } } get.set(optimisticPatchAtom, { - values: values as Partial, - versions: currentVersions as Partial>, + values: values as Partial, + versions: currentVersions as Partial< + Record + >, }); }), ), @@ -108,15 +110,17 @@ export function createMobilePreferencesState(runtime: Atom.AtomRuntime; const currentVersions = { ...optimistic.versions } as Record; - for (const key of Object.keys(patch) as Array) { + for (const key of Object.keys(patch) as Array) { if (optimistic.versions[key] === version) { delete values[key]; delete currentVersions[key]; } } get.set(optimisticPatchAtom, { - values: values as Partial, - versions: currentVersions as Partial>, + values: values as Partial, + versions: currentVersions as Partial< + Record + >, }); }), ), diff --git a/apps/web/src/cloud/linkEnvironment.test.ts b/apps/web/src/cloud/linkEnvironment.test.ts index 3ae0dbd74289..cb0da7b07d7f 100644 --- a/apps/web/src/cloud/linkEnvironment.test.ts +++ b/apps/web/src/cloud/linkEnvironment.test.ts @@ -15,9 +15,9 @@ import { HttpClient } from "effect/unstable/http"; import { afterEach, beforeEach, vi } from "vite-plus/test"; import { AVAILABLE_CONNECTION_STATE, - EnvironmentSupervisor, type PreparedConnection, PrimaryConnectionTarget, + EnvironmentSupervisor, } from "@t3tools/client-runtime/connection"; import { type RpcSession } from "@t3tools/client-runtime/rpc"; import { EnvironmentRegistry } from "@t3tools/client-runtime/connection"; @@ -77,7 +77,7 @@ function registryLayer(options?: { readonly installEvents?: ReadonlyArray; }) { return Layer.effect( - EnvironmentRegistry, + EnvironmentRegistry.EnvironmentRegistry, Effect.gen(function* () { const client = { [WS_METHODS.cloudGetRelayClientStatus]: () => @@ -99,7 +99,7 @@ function registryLayer(options?: { httpBaseUrl: TARGET.httpBaseUrl, wsBaseUrl: TARGET.wsBaseUrl, }); - const supervisor = EnvironmentSupervisor.of({ + const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({ target, state: yield* SubscriptionRef.make(AVAILABLE_CONNECTION_STATE), session: yield* SubscriptionRef.make(Option.some(session)), @@ -107,14 +107,14 @@ function registryLayer(options?: { connect: Effect.void, disconnect: Effect.void, retryNow: Effect.void, - } satisfies EnvironmentSupervisor["Service"]); + } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]); const registry = { run: (_environmentId: EnvironmentId, effect: Effect.Effect) => - Effect.provideService(effect, EnvironmentSupervisor, supervisor), + Effect.provideService(effect, EnvironmentSupervisor.EnvironmentSupervisor, supervisor), runStream: (_environmentId: EnvironmentId, stream: Stream.Stream) => - Stream.provideService(stream, EnvironmentSupervisor, supervisor), - } as unknown as EnvironmentRegistry["Service"]; - return EnvironmentRegistry.of(registry); + Stream.provideService(stream, EnvironmentSupervisor.EnvironmentSupervisor, supervisor), + } as unknown as EnvironmentRegistry.EnvironmentRegistry["Service"]; + return EnvironmentRegistry.EnvironmentRegistry.of(registry); }), ); } @@ -127,7 +127,9 @@ function withServices( effect: Effect.Effect< A, E, - HttpClient.HttpClient | ManagedRelay.ManagedRelayClient | EnvironmentRegistry + | HttpClient.HttpClient + | ManagedRelay.ManagedRelayClient + | EnvironmentRegistry.EnvironmentRegistry >, options?: Parameters[0], ) { diff --git a/apps/web/src/cloud/linkEnvironment.ts b/apps/web/src/cloud/linkEnvironment.ts index f88e7863969d..c12541a59a92 100644 --- a/apps/web/src/cloud/linkEnvironment.ts +++ b/apps/web/src/cloud/linkEnvironment.ts @@ -50,9 +50,9 @@ const relayClientRpcError = (message: string) => (cause: unknown) => function ensureRelayClientAvailable( environmentId: EnvironmentId, -): Effect.Effect { +): Effect.Effect { return Effect.gen(function* () { - const registry = yield* EnvironmentRegistry; + const registry = yield* EnvironmentRegistry.EnvironmentRegistry; const status = yield* registry .run(environmentId, request(WS_METHODS.cloudGetRelayClientStatus, {})) .pipe(Effect.mapError(relayClientRpcError("Could not check relay client availability."))); @@ -260,7 +260,7 @@ export function linkPrimaryEnvironmentToCloud(input: { }): Effect.Effect< void, CloudEnvironmentLinkError, - EnvironmentRegistry | HttpClient.HttpClient | ManagedRelay.ManagedRelayClient + EnvironmentRegistry.EnvironmentRegistry | HttpClient.HttpClient | ManagedRelay.ManagedRelayClient > { return Effect.gen(function* () { const configuredRelayUrl = relayUrl(); diff --git a/apps/web/src/connection/onboarding.ts b/apps/web/src/connection/onboarding.ts index 17b9912652b7..93a2373d820e 100644 --- a/apps/web/src/connection/onboarding.ts +++ b/apps/web/src/connection/onboarding.ts @@ -23,7 +23,9 @@ export const connectPairing = createRuntimeCommand(connectionAtomRuntime, { readonly host?: string; readonly pairingCode?: string; }) => - ConnectionOnboarding.pipe(Effect.flatMap((onboarding) => onboarding.registerPairing(input))), + ConnectionOnboarding.ConnectionOnboarding.pipe( + Effect.flatMap((onboarding) => onboarding.registerPairing(input)), + ), }); export const connectSshEnvironment = createRuntimeCommand(connectionAtomRuntime, { @@ -34,5 +36,7 @@ export const connectSshEnvironment = createRuntimeCommand(connectionAtomRuntime, key: (input: { readonly target: DesktopSshEnvironmentTarget }) => JSON.stringify(input.target), }, execute: (input: { readonly target: DesktopSshEnvironmentTarget; readonly label?: string }) => - ConnectionOnboarding.pipe(Effect.flatMap((onboarding) => onboarding.registerSsh(input))), + ConnectionOnboarding.ConnectionOnboarding.pipe( + Effect.flatMap((onboarding) => onboarding.registerSsh(input)), + ), }); diff --git a/apps/web/src/connection/platform.ts b/apps/web/src/connection/platform.ts index edd15c8df3eb..9560b390d159 100644 --- a/apps/web/src/connection/platform.ts +++ b/apps/web/src/connection/platform.ts @@ -1,11 +1,7 @@ import { - ClientPresentation, - CloudSession, - EnvironmentOwnedDataCleanup, + ClientCapabilities, PlatformConnectionSource, - PrimaryEnvironmentAuth, - RelayDeviceIdentity, - SshEnvironmentGateway, + Persistence, } from "@t3tools/client-runtime/platform"; import { BearerConnectionCredential, @@ -178,11 +174,11 @@ export const provisionDesktopSshEnvironment = Effect.fn( const capabilitiesLayer = Layer.effectContext( Effect.sync(() => { - const presentation = ClientPresentation.of({ + const presentation = ClientCapabilities.ClientPresentation.of({ metadata: clientMetadata(), scopes: AuthStandardClientScopes, }); - const cloudSession = CloudSession.of({ + const cloudSession = ClientCapabilities.CloudSession.of({ identity: Effect.sync(() => Option.fromNullishOr(appAtomRegistry.get(managedRelaySessionAtom)), ), @@ -212,10 +208,10 @@ const capabilitiesLayer = Layer.effectContext( return token; }), }); - const identity = RelayDeviceIdentity.of({ + const identity = ClientCapabilities.RelayDeviceIdentity.of({ deviceId: Effect.succeedNone, }); - const primaryAuth = PrimaryEnvironmentAuth.of({ + const primaryAuth = ClientCapabilities.PrimaryEnvironmentAuth.of({ bearerToken: Effect.tryPromise({ try: readDesktopPrimaryBearerToken, catch: (cause) => @@ -225,7 +221,7 @@ const capabilitiesLayer = Layer.effectContext( }), }).pipe(Effect.map(Option.fromNullishOr)), }); - const ssh = SshEnvironmentGateway.of({ + const ssh = ClientCapabilities.SshEnvironmentGateway.of({ provision: Effect.fn("web.connectionPlatform.ssh.provision")(function* (target) { const bridge = window.desktopBridge; if (bridge === undefined) { @@ -283,11 +279,11 @@ const capabilitiesLayer = Layer.effectContext( }), }); - return Context.make(CloudSession, cloudSession).pipe( - Context.add(PrimaryEnvironmentAuth, primaryAuth), - Context.add(RelayDeviceIdentity, identity), - Context.add(ClientPresentation, presentation), - Context.add(SshEnvironmentGateway, ssh), + return Context.make(ClientCapabilities.CloudSession, cloudSession).pipe( + Context.add(ClientCapabilities.PrimaryEnvironmentAuth, primaryAuth), + Context.add(ClientCapabilities.RelayDeviceIdentity, identity), + Context.add(ClientCapabilities.ClientPresentation, presentation), + Context.add(ClientCapabilities.SshEnvironmentGateway, ssh), ); }), ); @@ -463,10 +459,10 @@ export function secondaryRegistrationsToRetainAfterTopologyRead( } const platformConnectionSourceLayer = Layer.effect( - PlatformConnectionSource, + PlatformConnectionSource.PlatformConnectionSource, Effect.gen(function* () { if (isHostedStaticApp() || isLocalEnvironmentDisabled()) { - return PlatformConnectionSource.of({ + return PlatformConnectionSource.PlatformConnectionSource.of({ registrations: Stream.empty, }); } @@ -574,7 +570,7 @@ const platformConnectionSourceLayer = Layer.effect( return registrations as ReadonlyArray; }).pipe(Effect.provide(FetchHttpClient.layer)); - return PlatformConnectionSource.of({ + return PlatformConnectionSource.PlatformConnectionSource.of({ registrations: Stream.tick(PLATFORM_POLL_INTERVAL).pipe( Stream.mapEffect(() => buildPlatformRegistrations), ), @@ -583,8 +579,8 @@ const platformConnectionSourceLayer = Layer.effect( ); const environmentOwnedDataCleanupLayer = Layer.succeed( - EnvironmentOwnedDataCleanup, - EnvironmentOwnedDataCleanup.of({ + Persistence.EnvironmentOwnedDataCleanup, + Persistence.EnvironmentOwnedDataCleanup.of({ clear: (environmentId) => Effect.sync(() => { clearComposerDraftsEnvironment(environmentId); diff --git a/apps/web/src/connection/runtime.ts b/apps/web/src/connection/runtime.ts index c064b73aab24..65d86365d26f 100644 --- a/apps/web/src/connection/runtime.ts +++ b/apps/web/src/connection/runtime.ts @@ -1,10 +1,10 @@ import { Connection } from "@t3tools/client-runtime/connection"; -import { shellSnapshotLoaderLayer } from "@t3tools/client-runtime/state/shell"; +import { ShellSnapshotLoader } from "@t3tools/client-runtime/state/shell"; import { boundedThreadSnapshotLoaderLayer, - threadHistoryControllerLayer, + ThreadHistoryController, } from "@t3tools/client-runtime/state/threads"; -import { pullRequestDiffLoaderLayer } from "@t3tools/client-runtime/state/pull-requests"; +import { PullRequestDiffLoader } from "@t3tools/client-runtime/state/pull-requests"; import * as Layer from "effect/Layer"; import { Atom } from "effect/unstable/reactivity"; @@ -21,9 +21,9 @@ const providedConnectionPlatformLayer = connectionPlatformLayer.pipe( const snapshotLoaderLayer = Layer.mergeAll( boundedThreadSnapshotLoaderLayer, - shellSnapshotLoaderLayer, - threadHistoryControllerLayer, - pullRequestDiffLoaderLayer, + ShellSnapshotLoader.layer, + ThreadHistoryController.layer, + PullRequestDiffLoader.layer, ); type ConnectionLayerSource = diff --git a/apps/web/src/connection/storage.ts b/apps/web/src/connection/storage.ts index 55e8b3ee6264..7e909d02c7d6 100644 --- a/apps/web/src/connection/storage.ts +++ b/apps/web/src/connection/storage.ts @@ -1,11 +1,7 @@ import { ConnectionCatalogDocument, type ConnectionCatalogDocument as ConnectionCatalogDocumentType, - ConnectionPersistenceError, - ConnectionRegistrationStore, - ConnectionTargetStore, EMPTY_CONNECTION_CATALOG_DOCUMENT, - EnvironmentCacheStore, ORCHESTRATION_CACHE_SCHEMA_VERSION, StoredOrchestrationShellSnapshot, StoredOrchestrationThreadSnapshot, @@ -16,6 +12,7 @@ import { removeConnectionFromCatalog, setConnectionEnabledInCatalog, replaceCatalogValue, + Persistence, } from "@t3tools/client-runtime/platform"; import { TokenStore } from "@t3tools/client-runtime/authorization"; import { @@ -105,7 +102,7 @@ function persistenceError( | "clear-environment", cause: unknown, ) { - return new ConnectionPersistenceError({ + return new Persistence.ConnectionPersistenceError({ operation, message: `Could not ${operation.replaceAll("-", " ")}: ${String(cause)}`, }); @@ -466,7 +463,7 @@ export const connectionStorageLayer = Layer.effectContext( const catalog = yield* makeCatalogStore(makeCatalogBackend(database)); const githubRoutingPermissions = makeBrowserGitHubRoutingPermissions(); - const targetStore = ConnectionTargetStore.of({ + const targetStore = Persistence.ConnectionTargetStore.of({ list: catalog.read.pipe( Effect.map((document) => document.targets), Effect.mapError((cause) => persistenceError("list-targets", cause)), @@ -476,7 +473,7 @@ export const connectionStorageLayer = Layer.effectContext( Effect.mapError((cause) => persistenceError("list-disabled-targets", cause)), ), }); - const registrationStore = ConnectionRegistrationStore.of({ + const registrationStore = Persistence.ConnectionRegistrationStore.of({ register: (registration) => catalog .update((document) => registerConnectionInCatalog(document, registration)) @@ -561,7 +558,7 @@ export const connectionStorageLayer = Layer.effectContext( ), })), }); - const cacheStore = EnvironmentCacheStore.of({ + const cacheStore = Persistence.EnvironmentCacheStore.of({ loadShell: (environmentId) => readDatabaseValue(database, SHELL_STORE_NAME, environmentId).pipe( Effect.tap(() => Effect.promise(() => projectFaviconCache.hydrate())), @@ -762,13 +759,13 @@ export const connectionStorageLayer = Layer.effectContext( ).pipe(Effect.mapError((cause) => persistenceError("clear-environment", cause))), }); - return Context.make(ConnectionTargetStore, targetStore).pipe( + return Context.make(Persistence.ConnectionTargetStore, targetStore).pipe( Context.add(GitHubRoutingPermissions, githubRoutingPermissions), - Context.add(ConnectionRegistrationStore, registrationStore), + Context.add(Persistence.ConnectionRegistrationStore, registrationStore), Context.add(ProfileStore.ConnectionProfileStore, profileStore), Context.add(CredentialStore.ConnectionCredentialStore, credentialStore), Context.add(TokenStore.RemoteDpopAccessTokenStore, remoteTokenStore), - Context.add(EnvironmentCacheStore, cacheStore), + Context.add(Persistence.EnvironmentCacheStore, cacheStore), ); }), ); diff --git a/apps/web/src/lib/backgroundActivityReporter.ts b/apps/web/src/lib/backgroundActivityReporter.ts index 8af048b47f36..c646ac156d49 100644 --- a/apps/web/src/lib/backgroundActivityReporter.ts +++ b/apps/web/src/lib/backgroundActivityReporter.ts @@ -177,7 +177,7 @@ export const backgroundActivityReporterLayer = Layer.effectDiscard( return; } - const registry = yield* EnvironmentRegistry; + const registry = yield* EnvironmentRegistry.EnvironmentRegistry; const clock = yield* Clock.Clock; const reportRequests = yield* Queue.sliding(1); const requestReport = () => Queue.offerUnsafe(reportRequests, undefined); diff --git a/infra/relay/src/agentActivity/AgentActivityPublisher.test.ts b/infra/relay/src/agentActivity/AgentActivityPublisher.test.ts index 366e387f1e6f..7d76ec513a5a 100644 --- a/infra/relay/src/agentActivity/AgentActivityPublisher.test.ts +++ b/infra/relay/src/agentActivity/AgentActivityPublisher.test.ts @@ -7,12 +7,12 @@ import * as AgentActivityRows from "./AgentActivityRows.ts"; import * as EnvironmentLinks from "../environments/EnvironmentLinks.ts"; import * as LiveActivities from "./LiveActivities.ts"; import * as AgentActivityPublisher from "./AgentActivityPublisher.ts"; -import { FcmDeliveries } from "./FcmDeliveries.ts"; +import * as FcmDeliveries from "./FcmDeliveries.ts"; import * as ApnsDeliveries from "./ApnsDeliveries.ts"; const publisherLayer = AgentActivityPublisher.layer.pipe( Layer.provide( - Layer.succeed(FcmDeliveries, { + Layer.succeed(FcmDeliveries.FcmDeliveries, { enqueue: () => Effect.succeed(null), process: () => Effect.void, }), @@ -140,7 +140,7 @@ describe("AgentActivityPublisher", () => { it.effect("routes Android publication and registration replay to FCM alongside iOS", () => { const android = { ...target("android"), platform: "android" as const, ios_major_version: null }; const ios = target("ios"); - const fcmCalls: Array[0]> = []; + const fcmCalls: Array[0]> = []; const appleDevices: string[] = []; return Effect.gen(function* () { const publisher = yield* AgentActivityPublisher.AgentActivityPublisher; @@ -180,7 +180,7 @@ describe("AgentActivityPublisher", () => { }), }), ), - Layer.succeed(FcmDeliveries, { + Layer.succeed(FcmDeliveries.FcmDeliveries, { enqueue: (input) => Effect.sync(() => { fcmCalls.push(input); diff --git a/infra/relay/src/agentActivity/ApnsClient.ts b/infra/relay/src/agentActivity/ApnsClient.ts index 93c47af02e36..ca2c885dc4ea 100644 --- a/infra/relay/src/agentActivity/ApnsClient.ts +++ b/infra/relay/src/agentActivity/ApnsClient.ts @@ -7,7 +7,7 @@ import * as Schema from "effect/Schema"; import * as Headers from "effect/unstable/http/Headers"; import * as HttpClient from "effect/unstable/http/HttpClient"; import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; -import { ApnsEnvironment as ApnsEnvironmentSchema, type ApnsCredentials } from "../Config.ts"; +import { ApnsEnvironment, type ApnsCredentials } from "../Config.ts"; import type { ApnsLiveActivityAlert, ApnsNotificationPayload } from "./apnsDeliveryJobs.ts"; import type { ApnsJwtEncodingError, ApnsJwtSigningError } from "./apnsJwt.ts"; import * as ApnsProviderTokens from "./ApnsProviderTokens.ts"; @@ -58,7 +58,7 @@ export class ApnsHttpRequestError extends Schema.TaggedError { return Effect.succeed(response); }); yield* Effect.gen(function* () { - const client = yield* FcmClient; + const client = yield* FcmClient.FcmClient; const delivery = yield* client.send(input).pipe(Effect.flip, Effect.forkChild); yield* Deferred.await(started); yield* TestClock.adjust("10 seconds"); @@ -119,7 +119,7 @@ describe("FCM delivery", () => { expect(yield* client.send(input)).toEqual({ unregistered: false }); }).pipe( Effect.provide( - layer.pipe( + FcmClient.layer.pipe( Layer.provide( FcmAssertionSigner.layer.pipe( Layer.provide( @@ -127,7 +127,7 @@ describe("FCM delivery", () => { ), ), ), - Layer.provide(Layer.succeed(RelayConfiguration, config)), + Layer.provide(Layer.succeed(RelayConfiguration.RelayConfiguration, config)), Layer.provide(Layer.succeed(HttpClient.HttpClient, http)), ), ), @@ -173,7 +173,7 @@ describe("FCM delivery", () => { () => { const requests: HttpClientRequest.HttpClientRequest[] = []; return Effect.gen(function* () { - const client = yield* FcmClient; + const client = yield* FcmClient.FcmClient; yield* client.send(input); yield* client.send({ ...input, alert: true }); expect(requests.map((request) => request.url)).toEqual([ @@ -211,7 +211,7 @@ describe("FCM delivery", () => { it.effect("invalidates authorization after 401 and recognizes unregistered device tokens", () => { const requests: HttpClientRequest.HttpClientRequest[] = []; return Effect.gen(function* () { - const client = yield* FcmClient; + const client = yield* FcmClient.FcmClient; const first = yield* client.send(input).pipe(Effect.flip); expect(first.status).toBe(401); expect(yield* client.send(input)).toEqual({ unregistered: true }); @@ -243,7 +243,7 @@ describe("FCM delivery", () => { it.effect("rejects oversized data before contacting Firebase", () => { const requests: HttpClientRequest.HttpClientRequest[] = []; return Effect.gen(function* () { - const client = yield* FcmClient; + const client = yield* FcmClient.FcmClient; const error = yield* client .send({ ...input, data: { body: "漢".repeat(1500) } }) .pipe(Effect.flip); diff --git a/infra/relay/src/agentActivity/FcmDeliveries.test.ts b/infra/relay/src/agentActivity/FcmDeliveries.test.ts index 8f061fca16ff..9b0c0d13fab0 100644 --- a/infra/relay/src/agentActivity/FcmDeliveries.test.ts +++ b/infra/relay/src/agentActivity/FcmDeliveries.test.ts @@ -11,20 +11,14 @@ import * as Stream from "effect/Stream"; import type * as Cloudflare from "alchemy/Cloudflare"; import * as FcmDeliveryQueueConsumer from "./FcmDeliveryQueueConsumer.ts"; -import { RelayConfiguration } from "../Config.ts"; -import { RelayDb } from "../db.ts"; -import { EnvironmentLinks } from "../environments/EnvironmentLinks.ts"; -import { AgentActivityRows } from "./AgentActivityRows.ts"; -import { LiveActivities, type TargetRow } from "./LiveActivities.ts"; +import * as RelayConfiguration from "../Config.ts"; +import * as RelayDb from "../db.ts"; +import * as EnvironmentLinks from "../environments/EnvironmentLinks.ts"; +import * as AgentActivityRows from "./AgentActivityRows.ts"; +import * as LiveActivities from "./LiveActivities.ts"; import * as FcmDeliveryQueueSender from "./FcmDeliveryQueueSender.ts"; -import { FcmClient, FcmClientError } from "./FcmClient.ts"; -import { - FcmDeliveries, - androidAlertForState, - androidAlertForAggregate, - layer, - type FcmDeliveryJob, -} from "./FcmDeliveries.ts"; +import * as FcmClient from "./FcmClient.ts"; +import * as FcmDeliveries from "./FcmDeliveries.ts"; import { TestClock } from "effect/testing"; import { androidActivityData, fitFcmData } from "./fcmPayloads.ts"; import { makeAggregateState } from "./agentActivityAggregate.ts"; @@ -52,7 +46,7 @@ const preferences = { notifyOnCompletion: true, notifyOnFailure: true, }; -const target: TargetRow = { +const target: LiveActivities.TargetRow = { user_id: "user", device_id: "phone", platform: "android", @@ -88,12 +82,12 @@ const config = { cloudMintPublicKey: "unused", managedEndpointBaseDomain: undefined, managedEndpointNamespace: undefined, -} satisfies RelayConfiguration["Service"]; +} satisfies RelayConfiguration.RelayConfiguration["Service"]; function harness() { - const sent: Array[0]> = []; - const queued: FcmDeliveryJob[] = []; - const marked: Array[0]> = []; + const sent: Array[0]> = []; + const queued: FcmDeliveries.FcmDeliveryJob[] = []; + const marked: Array[0]> = []; const current = { target: { ...target }, state: { ...state } as RelayAgentActivityState | null, @@ -102,18 +96,18 @@ function harness() { notificationOnlyEnvironments: [] as string[], revokedEnvironments: [] as string[], linked: true, - deliveryFailure: null as FcmClientError | null, + deliveryFailure: null as FcmClient.FcmClientError | null, }; const services = Layer.mergeAll( NodeCryptoLayer.layer, - Layer.succeed(RelayConfiguration, config), + Layer.succeed(RelayConfiguration.RelayConfiguration, config), Layer.succeed(FcmDeliveryQueueSender.FcmDeliveryQueueSender, { send: (job) => Effect.sync(() => { queued.push(job); }), }), - Layer.succeed(FcmClient, { + Layer.succeed(FcmClient.FcmClient, { send: (input) => Effect.suspend(() => current.deliveryFailure @@ -124,7 +118,7 @@ function harness() { }), ), }), - Layer.succeed(LiveActivities, { + Layer.succeed(LiveActivities.LiveActivities, { register: () => Effect.void, listTargets: () => Effect.sync(() => [current.target]), markDelivery: (input) => @@ -136,7 +130,7 @@ function harness() { clearStartQueued: () => Effect.void, invalidateDeliveryToken: () => Effect.void, }), - Layer.succeed(AgentActivityRows, { + Layer.succeed(AgentActivityRows.AgentActivityRows, { upsert: () => Effect.void, remove: () => Effect.void, pruneTerminal: () => Effect.void, @@ -160,7 +154,7 @@ function harness() { : null, ), }), - Layer.succeed(EnvironmentLinks, { + Layer.succeed(EnvironmentLinks.EnvironmentLinks, { upsert: () => Effect.void, listDeliveryUsersForEnvironment: (input) => Effect.sync(() => @@ -195,21 +189,21 @@ function harness() { : null, ), }), - Layer.succeed(RelayDb, {} as RelayDb["Service"]), + Layer.succeed(RelayDb.RelayDb, {} as RelayDb.RelayDb["Service"]), ); return { sent, queued, marked, current, - layer: layer.pipe(Layer.provide(services)), + layer: FcmDeliveries.layer.pipe(Layer.provide(services)), job: { userId: "user", deviceId: "phone", token: "fcm-token", state, queuedAt: 0, - } satisfies FcmDeliveryJob, + } satisfies FcmDeliveries.FcmDeliveryJob, }; } @@ -229,7 +223,7 @@ describe("Android delivery routing", () => { const h = harness(); h.current.otherStates = [secondState]; return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.process(h.job); h.current.state = { ...state, phase: firstPhase }; h.current.otherStates = [{ ...secondState, phase: secondPhase }]; @@ -256,7 +250,7 @@ describe("Android delivery routing", () => { h.current.state = { ...state, phase: "waiting_for_approval" }; h.current.otherStates = [{ ...secondState, phase: "waiting_for_input" }]; return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.process({ ...h.job, state: h.current.state }); expect(h.sent[0]?.data).toMatchObject({ alert_title: "Second thread", @@ -274,7 +268,7 @@ describe("Android delivery routing", () => { { ...secondState, phase: "completed", updatedAt: "1969-12-31T23:57:00.000Z" }, ]; return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.process({ ...h.job, state: h.current.state }); expect(h.sent.every((message) => !message.alert)).toBe(true); }).pipe(Effect.provide(h.layer)); @@ -286,7 +280,7 @@ describe("Android delivery routing", () => { h.current.state = { ...state, phase: "completed" }; h.current.otherStates = [{ ...secondState, phase: "waiting_for_input" }]; return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.process({ ...h.job, state: h.current.state }); expect(h.sent[0]?.data).toMatchObject({ alert_title: "Second thread", @@ -307,7 +301,7 @@ describe("Android delivery routing", () => { const h = harness(); h.current.otherStates = [secondState]; return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.process(h.job); h.current.otherStates = []; h.current.state = { ...state, phase }; @@ -327,7 +321,7 @@ describe("Android delivery routing", () => { h.current.state = { ...state, phase: "waiting_for_approval" }; h.current.otherStates = [{ ...secondState, phase: "waiting_for_input" }]; return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.enqueue({ target, state: null, replay: true }); yield* delivery.process(h.queued[0]); yield* delivery.process({ ...h.job, state: h.current.state }); @@ -345,7 +339,7 @@ describe("Android delivery routing", () => { h.current.otherStates = [{ ...other, phase: "waiting_for_input" }]; h.current[restriction] = [other.environmentId]; return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.process({ ...h.job, state: h.current.state }); expect(h.sent[0]?.data).toMatchObject({ alert_title: "Fix notifications", @@ -370,10 +364,10 @@ describe("Android delivery routing", () => { preferences, nowMs: 0, }; - const alert = androidAlertForAggregate(input); + const alert = FcmDeliveries.androidAlertForAggregate(input); expect(alert?.alert_title).toBe("2 agents finished"); expect( - androidAlertForAggregate({ + FcmDeliveries.androidAlertForAggregate({ ...input, nextAggregate: { ...input.nextAggregate, @@ -382,7 +376,7 @@ describe("Android delivery routing", () => { })?.alert_id, ).toBe(alert?.alert_id); expect( - androidAlertForAggregate({ + FcmDeliveries.androidAlertForAggregate({ ...input, nextAggregate: { ...input.nextAggregate, @@ -403,7 +397,7 @@ describe("Android delivery routing", () => { }; const alreadyWaiting = { ...state, phase: "waiting_for_approval" as const }; expect( - androidAlertForAggregate({ + FcmDeliveries.androidAlertForAggregate({ previousAggregate: aggregateFor([alreadyWaiting, other]), nextAggregate: aggregateFor([alreadyWaiting, { ...other, phase: "waiting_for_input" }]), preferences, @@ -422,7 +416,7 @@ describe("Android delivery routing", () => { const h = harness(); h.current.state = { ...state, phase }; return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.process({ ...h.job, state: h.current.state }); expect(h.sent[0]?.data).toMatchObject({ alert_title: "Fix notifications", @@ -441,7 +435,7 @@ describe("Android delivery routing", () => { it("trims and truncates alert text like iOS", () => { expect( - androidAlertForState( + FcmDeliveries.androidAlertForState( { ...state, phase: "completed", @@ -462,7 +456,7 @@ describe("Android delivery routing", () => { () => { const h = harness(); return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.enqueue({ target, state }); h.current.state = { ...state, phase: "completed" }; yield* delivery.process(h.queued[0]); @@ -486,7 +480,7 @@ describe("Android delivery routing", () => { liveActivitiesEnabled: false, }); return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.process({ ...h.job, state: h.current.state }); expect(h.sent[0]?.data).toMatchObject({ active: "false", @@ -500,7 +494,7 @@ describe("Android delivery routing", () => { it.effect("drops jobs for rotated tokens, expired jobs, and revoked links", () => { const h = harness(); return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.process({ ...h.job, token: "old-token" }); yield* delivery.process({ ...h.job, queuedAt: -400_000 }); h.current.linked = false; @@ -514,7 +508,7 @@ describe("Android delivery routing", () => { h.current.target.last_aggregate_json = encodeJson(aggregateFor([state])); h.current.state = null; return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.process({ ...h.job, state: null }); expect(h.sent[0]?.data).toMatchObject({ active: "false" }); expect(h.sent[0]?.alert).toBe(false); @@ -525,7 +519,7 @@ describe("Android delivery routing", () => { it.effect("leaves iOS devices on their existing delivery path", () => { const h = harness(); return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; expect(yield* delivery.enqueue({ target: { ...target, platform: "ios" }, state })).toBeNull(); expect(h.queued).toHaveLength(0); }).pipe(Effect.provide(h.layer)); @@ -536,7 +530,7 @@ describe("Android delivery routing", () => { h.current.state = { ...state, phase: "waiting_for_approval" }; h.current.target.preferences_json = encodeJson({ ...preferences, notifyOnApproval: false }); return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.process({ ...h.job, state: h.current.state }); expect(h.sent[0]?.data.active).toBe("true"); expect(h.sent[0]?.data.alert_id).toBeUndefined(); @@ -560,7 +554,7 @@ describe("Android delivery routing", () => { liveActivitiesEnabled: ongoing, }); return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.process({ ...h.job, state: h.current.state }); expect(h.sent.every((message) => !message.alert)).toBe(true); }).pipe(Effect.provide(h.layer)); @@ -574,7 +568,7 @@ describe("Android delivery routing", () => { const h = harness(); h.current.state = { ...state, phase: "failed" }; return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.process({ ...h.job, state: h.current.state }); expect(h.sent[0]?.data).toMatchObject({ active: "false", @@ -597,7 +591,7 @@ describe("Android delivery routing", () => { const h = harness(); h.current.state = null; return Effect.gen(function* () { - const delivery = yield* FcmDeliveries; + const delivery = yield* FcmDeliveries.FcmDeliveries; yield* delivery.process({ ...h.job, state: null }); expect(h.sent[0]?.data.activity_expires_at).toBe("0"); expect(h.sent[0]?.alert).toBe(false); @@ -682,7 +676,7 @@ describe("delivery policy regressions", () => { h.current.otherStates = [old]; h.current.state = { ...state, phase: "completed" }; return Effect.gen(function* () { - const d = yield* FcmDeliveries; + const d = yield* FcmDeliveries.FcmDeliveries; yield* d.process(h.job); yield* d.process({ ...h.job, state: h.current.state }); expect(h.sent.filter((x) => x.alert)).toHaveLength(1); @@ -699,7 +693,7 @@ describe("delivery policy regressions", () => { h.current.otherStates = [{ ...other, phase: "waiting_for_input" }]; h.current.mutedEnvironments = [state.environmentId]; return Effect.gen(function* () { - const d = yield* FcmDeliveries; + const d = yield* FcmDeliveries.FcmDeliveries; yield* d.process(h.job); yield* d.process({ ...h.job, state: h.current.otherStates[0]! }); expect(h.sent.filter((x) => x.alert)).toHaveLength(1); @@ -717,7 +711,7 @@ describe("delivery policy regressions", () => { }; const next = aggregateFor([...running, waiting]); expect( - androidAlertForAggregate({ + FcmDeliveries.androidAlertForAggregate({ previousAggregate: aggregateFor(running), nextAggregate: next, preferences, @@ -740,7 +734,7 @@ describe("notification-only environments", () => { h.current.state = { ...state, phase: "waiting_for_input" }; h.current.notificationOnlyEnvironments = [state.environmentId]; return Effect.gen(function* () { - const deliveries = yield* FcmDeliveries; + const deliveries = yield* FcmDeliveries.FcmDeliveries; yield* deliveries.process({ ...h.job, state: h.current.state }); expect(h.sent).toHaveLength(1); expect(h.sent[0]?.alert).toBe(true); @@ -771,7 +765,7 @@ it.effect("notification-only jobs do not consume another environment's card aler h.current.state = { ...state, phase: "waiting_for_input" }; h.current.notificationOnlyEnvironments = [state.environmentId]; return Effect.gen(function* () { - const deliveries = yield* FcmDeliveries; + const deliveries = yield* FcmDeliveries.FcmDeliveries; yield* deliveries.process({ ...h.job, state: h.current.state }); yield* deliveries.process({ ...h.job, state: h.current.otherStates[0]! }); expect(h.sent.filter((delivery) => delivery.alert)).toHaveLength(2); @@ -780,10 +774,10 @@ it.effect("notification-only jobs do not consume another environment's card aler it.effect("preserves a structured Firebase failure through the queue consumer", () => { const h = harness(); - const failure = new FcmClientError({ operation: "send", status: 503 }); + const failure = new FcmClient.FcmClientError({ operation: "send", status: 503 }); h.current.deliveryFailure = failure; return Effect.gen(function* () { - const deliveries = yield* FcmDeliveries; + const deliveries = yield* FcmDeliveries.FcmDeliveries; expect(yield* deliveries.process(h.job).pipe(Effect.flip)).toBe(failure); }).pipe(Effect.provide(h.layer)); }); @@ -825,7 +819,7 @@ describe("FCM queue message isolation", () => { Effect.sync(() => { h.current.deliveryFailure = item.id === "failed" && failure === "fcm-rejection" - ? new FcmClientError({ operation: "send", status: 400 }) + ? new FcmClient.FcmClientError({ operation: "send", status: 400 }) : null; }), ), diff --git a/infra/relay/src/agentActivity/MobileRegistrations.test.ts b/infra/relay/src/agentActivity/MobileRegistrations.test.ts index 457a545f74b0..2f23660e1b63 100644 --- a/infra/relay/src/agentActivity/MobileRegistrations.test.ts +++ b/infra/relay/src/agentActivity/MobileRegistrations.test.ts @@ -18,11 +18,11 @@ import * as EnvironmentLinks from "../environments/EnvironmentLinks.ts"; import * as LiveActivities from "./LiveActivities.ts"; import * as RelayConfiguration from "../Config.ts"; import * as AgentActivityPublisher from "./AgentActivityPublisher.ts"; -import { FcmDeliveries } from "./FcmDeliveries.ts"; +import * as FcmDeliveries from "./FcmDeliveries.ts"; const publisherLayer = AgentActivityPublisher.layer.pipe( Layer.provide( - Layer.succeed(FcmDeliveries, { + Layer.succeed(FcmDeliveries.FcmDeliveries, { enqueue: () => Effect.succeed(null), process: () => Effect.void, }), diff --git a/infra/relay/src/environments/ManagedEndpointReaper.ts b/infra/relay/src/environments/ManagedEndpointReaper.ts index 33524dbf0c5d..4a3d408b49be 100644 --- a/infra/relay/src/environments/ManagedEndpointReaper.ts +++ b/infra/relay/src/environments/ManagedEndpointReaper.ts @@ -4,7 +4,6 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; -import type { ManagedEndpointCleanupMode } from "../Config.ts"; import * as RelayConfiguration from "../Config.ts"; import { managedEndpointTunnelNamePrefix } from "../deploymentConfig.ts"; import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts"; @@ -19,7 +18,7 @@ export const MANAGED_ENDPOINT_SWEEP_ATTEMPT_LIMIT = 100; export const MANAGED_ENDPOINT_SWEEP_LIST_REQUEST_LIMIT = 10; export interface ManagedEndpointSweepResult { - readonly mode: ManagedEndpointCleanupMode; + readonly mode: RelayConfiguration.ManagedEndpointCleanupMode; readonly listRequests: number; readonly scanned: number; readonly attempted: number; @@ -103,7 +102,9 @@ function rotatedPages(input: { return Array.from({ length: count }, (_, index) => 2 + ((start + index) % laterPageCount)); } -const emptyResult = (mode: ManagedEndpointCleanupMode): ManagedEndpointSweepResult => ({ +const emptyResult = ( + mode: RelayConfiguration.ManagedEndpointCleanupMode, +): ManagedEndpointSweepResult => ({ mode, listRequests: 0, scanned: 0, diff --git a/packages/client-runtime/src/connection/index.ts b/packages/client-runtime/src/connection/index.ts index cf8385aa132b..cd6312163681 100644 --- a/packages/client-runtime/src/connection/index.ts +++ b/packages/client-runtime/src/connection/index.ts @@ -6,20 +6,13 @@ export * from "./errors.ts"; export * from "./githubRoutingPermissions.ts"; export * as Connection from "./layer.ts"; export * from "./model.ts"; -export { - type BearerConnectionUpdateInput, - ConnectionOnboarding, - type PairingConnectionInput, - type SshConnectionInput, -} from "./onboarding.ts"; +export * as ConnectionOnboarding from "./onboarding.ts"; export * from "./presentation.ts"; export * as ProfileStore from "./profileStore.ts"; -export { - EnvironmentNotRegisteredError, - EnvironmentRegistry, - PlatformEnvironmentRemovalError, -} from "./registry.ts"; -export { EnvironmentSupervisor, type EnvironmentSupervisorOptions } from "./supervisor.ts"; +export * as EnvironmentRegistry from "./registry.ts"; +// Flat so consumers' inferred types can name them. +export { EnvironmentNotRegisteredError, PlatformEnvironmentRemovalError } from "./registry.ts"; +export * as EnvironmentSupervisor from "./supervisor.ts"; export * as Wakeups from "./wakeups.ts"; export { orchestrationProtocolCompatibilityError } from "./compatibility.ts"; diff --git a/packages/client-runtime/src/connection/layer.ts b/packages/client-runtime/src/connection/layer.ts index b64e58c4fef1..8b1a4eaa5dde 100644 --- a/packages/client-runtime/src/connection/layer.ts +++ b/packages/client-runtime/src/connection/layer.ts @@ -67,7 +67,7 @@ export const watchDiscoveredCompatibility = Effect.fn("connection.watchDiscovere export function layerWithOptions(options: RpcSession.RpcSessionOptions) { const driverLayer = ConnectionDriver.layer.pipe( - Layer.provide(Layer.mergeAll(ConnectionResolver.layer, RpcSession.layerWithOptions(options))), + Layer.provide(Layer.mergeAll(ConnectionResolver.layer, RpcSession.layer(options))), ); const registryLayer = EnvironmentRegistry.layer.pipe(Layer.provide(driverLayer)); const onboardingLayer = ConnectionOnboarding.layer.pipe(Layer.provide(registryLayer)); diff --git a/packages/client-runtime/src/connection/onboarding.test.ts b/packages/client-runtime/src/connection/onboarding.test.ts index 5e252df51884..5e8257a8cb82 100644 --- a/packages/client-runtime/src/connection/onboarding.test.ts +++ b/packages/client-runtime/src/connection/onboarding.test.ts @@ -9,7 +9,7 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import { remoteHttpClientLayer } from "../rpc/http.ts"; -import { ClientPresentation, SshEnvironmentGateway } from "../platform/capabilities.ts"; +import * as ClientCapabilities from "../platform/capabilities.ts"; import { BearerConnectionCredential, BearerConnectionProfile } from "./catalog.ts"; import { BearerConnectionTarget } from "./model.ts"; import { @@ -19,8 +19,8 @@ import { } from "./onboarding.ts"; const CLIENT_PRESENTATION_LAYER = Layer.succeed( - ClientPresentation, - ClientPresentation.of({ + ClientCapabilities.ClientPresentation, + ClientCapabilities.ClientPresentation.of({ metadata: { label: "T3 Code Test", deviceType: "desktop", @@ -246,8 +246,8 @@ describe("connection onboarding", () => { target, }).pipe( Effect.provideService( - SshEnvironmentGateway, - SshEnvironmentGateway.of({ + ClientCapabilities.SshEnvironmentGateway, + ClientCapabilities.SshEnvironmentGateway.of({ provision: () => Effect.succeed({ environmentId: EnvironmentId.make("environment-ssh"), diff --git a/packages/client-runtime/src/connection/supervisor.test.ts b/packages/client-runtime/src/connection/supervisor.test.ts index 7ad709c3a54b..d64863b9ffcf 100644 --- a/packages/client-runtime/src/connection/supervisor.test.ts +++ b/packages/client-runtime/src/connection/supervisor.test.ts @@ -14,11 +14,7 @@ import * as Tracer from "effect/Tracer"; import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; import * as TokenStore from "../authorization/tokenStore.ts"; import * as ClientCapabilities from "../platform/capabilities.ts"; -import { - ManagedRelayClient, - ManagedRelayDpopSigner, - ManagedRelayRequestTimeoutError, -} from "../relay/managedRelay.ts"; +import * as ManagedRelay from "../relay/managedRelay.ts"; import { remoteHttpClientLayer } from "../rpc/http.ts"; import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; import { fetchEnvironmentSessionState } from "../state/session.ts"; @@ -1247,12 +1243,12 @@ describe("EnvironmentSupervisor", () => { return Promise.reject(new Error(`Unexpected HTTP request to ${request.url}`)); } }) satisfies typeof fetch; - const signer = ManagedRelayDpopSigner.of({ + const signer = ManagedRelay.ManagedRelayDpopSigner.of({ thumbprint: Effect.succeed("test-thumbprint"), createProof: () => Effect.succeed("test-proof"), }); const unused = () => Effect.die("Unexpected relay operation."); - const relay = ManagedRelayClient.of({ + const relay = ManagedRelay.ManagedRelayClient.of({ relayUrl: "https://relay.example.test", listEnvironments: unused, listDevices: unused, @@ -1263,7 +1259,7 @@ describe("EnvironmentSupervisor", () => { connectEnvironment: Effect.fn("TestConnectionHttp.connectEnvironment")(function* () { yield* Ref.update(bootstrapCalls, (count) => count + 1); if (yield* Ref.get(bootstrapFails)) { - return yield* new ManagedRelayRequestTimeoutError({ + return yield* new ManagedRelay.ManagedRelayRequestTimeoutError({ activity: "Relay environment connection", timeoutMs: 6_000, traceId: null, @@ -1287,8 +1283,8 @@ describe("EnvironmentSupervisor", () => { Effect.provide( Layer.mergeAll( httpLayer, - Layer.succeed(ManagedRelayDpopSigner, signer), - Layer.succeed(ManagedRelayClient, relay), + Layer.succeed(ManagedRelay.ManagedRelayDpopSigner, signer), + Layer.succeed(ManagedRelay.ManagedRelayClient, relay), Layer.succeed(ClientCapabilities.CloudSession, { identity: Effect.succeedSome({ accountId: "test-account" }), clerkToken: Effect.succeed("clerk-token"), diff --git a/packages/client-runtime/src/platform/index.ts b/packages/client-runtime/src/platform/index.ts index 1006aa347a9b..8480b2b142f1 100644 --- a/packages/client-runtime/src/platform/index.ts +++ b/packages/client-runtime/src/platform/index.ts @@ -1,5 +1,7 @@ -export * from "./capabilities.ts"; -export * from "./persistence.ts"; +export * as ClientCapabilities from "./capabilities.ts"; +export * as Persistence from "./persistence.ts"; +// Flat so consumers' inferred types can name it. +export { ConnectionPersistenceError } from "./persistence.ts"; export * from "./orchestrationCache.ts"; -export * from "./source.ts"; +export * as PlatformConnectionSource from "./source.ts"; export * from "./storageDocument.ts"; diff --git a/packages/client-runtime/src/rpc/client.ts b/packages/client-runtime/src/rpc/client.ts index 7c5e4b6a23de..426ea63482ea 100644 --- a/packages/client-runtime/src/rpc/client.ts +++ b/packages/client-runtime/src/rpc/client.ts @@ -10,7 +10,7 @@ import * as Stream from "effect/Stream"; import * as SubscriptionRef from "effect/SubscriptionRef"; import { RpcClientError } from "effect/unstable/rpc"; -import { EnvironmentSupervisor } from "../connection/supervisor.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; import type { RpcSession } from "../rpc/session.ts"; @@ -116,7 +116,7 @@ export type EnvironmentRpcStreamFailure = : never; const currentSession = Effect.fn("EnvironmentRpc.currentSession")(function* () { - const supervisor = yield* EnvironmentSupervisor; + const supervisor = yield* EnvironmentSupervisor.EnvironmentSupervisor; return yield* SubscriptionRef.get(supervisor.session).pipe( Effect.flatMap( Option.match({ @@ -143,7 +143,7 @@ export const getInitialServerConfig = Effect.fn("EnvironmentRpc.getInitialServer export const request = Effect.fn("EnvironmentRpc.request")(function* < TTag extends EnvironmentUnaryRpcTag, >(tag: TTag, input: EnvironmentRpcInput) { - const supervisor = yield* EnvironmentSupervisor; + const supervisor = yield* EnvironmentSupervisor.EnvironmentSupervisor; yield* Effect.annotateCurrentSpan({ "environment.id": supervisor.target.environmentId, "rpc.method": tag, @@ -166,7 +166,7 @@ export function runStream( ): Stream.Stream< EnvironmentRpcStreamValue, EnvironmentRpcStreamFailure | EnvironmentRpcUnavailableError, - EnvironmentSupervisor + EnvironmentSupervisor.EnvironmentSupervisor > { return Stream.unwrap( currentSession().pipe( @@ -204,10 +204,14 @@ function subscribeDynamicMapped( stream: Stream.Stream, EnvironmentRpcStreamFailure>, ) => Stream.Stream>, options?: SubscriptionOptions, -): Stream.Stream, EnvironmentSupervisor> { +): Stream.Stream< + A, + EnvironmentRpcStreamFailure, + EnvironmentSupervisor.EnvironmentSupervisor +> { return Stream.unwrap( Effect.gen(function* () { - const supervisor = yield* EnvironmentSupervisor; + const supervisor = yield* EnvironmentSupervisor.EnvironmentSupervisor; const observer = yield* EnvironmentRpcSubscriptionObserver; const sessionChanges = SubscriptionRef.changes(supervisor.session); const sessions = @@ -328,7 +332,7 @@ export function subscribeDynamic( ): Stream.Stream< EnvironmentRpcStreamValue, EnvironmentRpcStreamFailure, - EnvironmentSupervisor + EnvironmentSupervisor.EnvironmentSupervisor > { return subscribeDynamicMapped(tag, makeInput, (_session, stream) => stream, options); } @@ -341,7 +345,7 @@ export function subscribeDynamicWithSession], EnvironmentRpcStreamFailure, - EnvironmentSupervisor + EnvironmentSupervisor.EnvironmentSupervisor > { return subscribeDynamicMapped( tag, @@ -358,7 +362,7 @@ export function subscribe( ): Stream.Stream< EnvironmentRpcStreamValue, EnvironmentRpcStreamFailure, - EnvironmentSupervisor + EnvironmentSupervisor.EnvironmentSupervisor > { return subscribeDynamic(tag, () => Effect.succeed(input), options); } diff --git a/packages/client-runtime/src/rpc/session.test.ts b/packages/client-runtime/src/rpc/session.test.ts index 9d49b8e6240a..d2baa5927475 100644 --- a/packages/client-runtime/src/rpc/session.test.ts +++ b/packages/client-runtime/src/rpc/session.test.ts @@ -220,7 +220,7 @@ const makeFactory = Effect.fn("TestRpcSessionFactory.make")(function* ( sockets.push(socket); return socket as unknown as globalThis.WebSocket; }); - const layer = RpcSession.layerWithOptions(options).pipe(Layer.provide(constructorLayer)); + const layer = RpcSession.layer(options).pipe(Layer.provide(constructorLayer)); const factory = yield* RpcSession.RpcSessionFactory.pipe(Effect.provide(layer)); return { factory, sockets }; }); diff --git a/packages/client-runtime/src/rpc/session.ts b/packages/client-runtime/src/rpc/session.ts index 93bb2ff2f7d3..ce0296819e7c 100644 --- a/packages/client-runtime/src/rpc/session.ts +++ b/packages/client-runtime/src/rpc/session.ts @@ -395,5 +395,4 @@ export const make = Effect.fn("RpcSessionFactory.make")(function* ( return RpcSessionFactory.of({ connect }); }); -export const layerWithOptions = (options: RpcSessionOptions) => - Layer.effect(RpcSessionFactory, make(options)); +export const layer = (options: RpcSessionOptions) => Layer.effect(RpcSessionFactory, make(options)); diff --git a/packages/client-runtime/src/state/assets.test.ts b/packages/client-runtime/src/state/assets.test.ts index bd266f261675..97a4c1d15296 100644 --- a/packages/client-runtime/src/state/assets.test.ts +++ b/packages/client-runtime/src/state/assets.test.ts @@ -17,14 +17,14 @@ import * as Option from "effect/Option"; import * as Layer from "effect/Layer"; import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity"; -import { EnvironmentRegistry } from "../connection/registry.ts"; +import * as EnvironmentRegistry from "../connection/registry.ts"; import { AVAILABLE_CONNECTION_STATE, PrimaryConnectionTarget, type PreparedConnection, type SupervisorConnectionState, } from "../connection/model.ts"; -import { EnvironmentSupervisor } from "../connection/supervisor.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; import type { RpcSession } from "../rpc/session.ts"; import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; import { createProjectFaviconCache } from "../projectFaviconCache.ts"; @@ -93,7 +93,10 @@ describe("createAssetEnvironmentAtoms", () => { ? new AssetWorkspaceContextNotFoundError({ resource }) : new AssetWorkspaceAssetNotFoundError({ resource }); const calls: EnvironmentId[] = []; - const supervisors = new Map(); + const supervisors = new Map< + EnvironmentId, + EnvironmentSupervisor.EnvironmentSupervisor["Service"] + >(); for (const environmentId of [remoteId, localId]) { const client = { [WS_METHODS.assetsCreateUrl]: () => { @@ -109,7 +112,7 @@ describe("createAssetEnvironmentAtoms", () => { const session = { client } as RpcSession; supervisors.set( environmentId, - EnvironmentSupervisor.of({ + EnvironmentSupervisor.EnvironmentSupervisor.of({ target: new PrimaryConnectionTarget({ environmentId, label: environmentId, @@ -128,12 +131,20 @@ describe("createAssetEnvironmentAtoms", () => { }), ); } - const environments = EnvironmentRegistry.of({ + const environments = EnvironmentRegistry.EnvironmentRegistry.of({ run: (id, effect) => - Effect.provideService(effect, EnvironmentSupervisor, supervisors.get(id)!), + Effect.provideService( + effect, + EnvironmentSupervisor.EnvironmentSupervisor, + supervisors.get(id)!, + ), followStream: (id, stream) => - Stream.provideService(stream, EnvironmentSupervisor, supervisors.get(id)!), - } as EnvironmentRegistry["Service"]); + Stream.provideService( + stream, + EnvironmentSupervisor.EnvironmentSupervisor, + supervisors.get(id)!, + ), + } as EnvironmentRegistry.EnvironmentRegistry["Service"]); const registry = AtomRegistry.make(); yield* Effect.addFinalizer(() => Effect.sync(() => registry.dispose())); const localTarget = { @@ -144,7 +155,7 @@ describe("createAssetEnvironmentAtoms", () => { scenario.primary === "none" || scenario.primary === "reconnecting" ? null : localTarget, ); const assets = createAssetEnvironmentAtoms( - Atom.runtime(Layer.succeed(EnvironmentRegistry, environments)), + Atom.runtime(Layer.succeed(EnvironmentRegistry.EnvironmentRegistry, environments)), localEnvironment, ); const query = assets.createUrl({ environmentId: remoteId, input: { resource } }); @@ -170,7 +181,7 @@ describe("createAssetEnvironmentAtoms", () => { it("keys asset URL queries by environment and resource", () => { const runtime = Atom.runtime(Layer.empty) as unknown as Atom.AtomRuntime< - EnvironmentRegistry, + EnvironmentRegistry.EnvironmentRegistry, never >; const assets = createAssetEnvironmentAtoms(runtime); @@ -229,7 +240,7 @@ describe("createAssetEnvironmentAtoms", () => { it("keys collections while preserving independent resource queries", () => { const runtime = Atom.runtime(Layer.empty) as unknown as Atom.AtomRuntime< - EnvironmentRegistry, + EnvironmentRegistry.EnvironmentRegistry, never >; const assets = createAssetEnvironmentAtoms(runtime); diff --git a/packages/client-runtime/src/state/boundedThreadSnapshotHttp.test.ts b/packages/client-runtime/src/state/boundedThreadSnapshotHttp.test.ts index 1ac5b7e6b529..15c7d79a296f 100644 --- a/packages/client-runtime/src/state/boundedThreadSnapshotHttp.test.ts +++ b/packages/client-runtime/src/state/boundedThreadSnapshotHttp.test.ts @@ -6,7 +6,7 @@ import * as Layer from "effect/Layer"; import { PrimaryConnectionTarget, type PreparedConnection } from "../connection/model.ts"; import { remoteHttpClientLayer } from "../rpc/http.ts"; import { boundedThreadSnapshotLoaderLayer } from "./boundedThreadSnapshotHttp.ts"; -import { ThreadSnapshotLoader } from "./threadSnapshotHttp.ts"; +import * as ThreadSnapshotLoader from "./threadSnapshotHttp.ts"; const TARGET = new PrimaryConnectionTarget({ environmentId: "environment-bounded" as never, @@ -100,7 +100,7 @@ describe("boundedThreadSnapshotLoader", () => { }) satisfies typeof fetch; return Effect.gen(function* () { - const loader = yield* ThreadSnapshotLoader; + const loader = yield* ThreadSnapshotLoader.ThreadSnapshotLoader; const result = yield* loader.load(PREPARED, THREAD_ID); expect(result._tag).toBe("present"); if (result._tag === "present") { @@ -133,7 +133,7 @@ describe("boundedThreadSnapshotLoader", () => { }) satisfies typeof fetch; return Effect.gen(function* () { - const loader = yield* ThreadSnapshotLoader; + const loader = yield* ThreadSnapshotLoader.ThreadSnapshotLoader; const result = yield* loader.load(PREPARED, THREAD_ID); expect(result).toEqual({ _tag: "missing" }); }).pipe( @@ -170,7 +170,7 @@ describe("boundedThreadSnapshotLoader", () => { }) satisfies typeof fetch; return Effect.gen(function* () { - const loader = yield* ThreadSnapshotLoader; + const loader = yield* ThreadSnapshotLoader.ThreadSnapshotLoader; const result = yield* loader.load(PREPARED, THREAD_ID); expect(result).toEqual({ _tag: "missing" }); expect(fullCalls).toBe(1); @@ -200,7 +200,7 @@ describe("boundedThreadSnapshotLoader", () => { }) satisfies typeof fetch; return Effect.gen(function* () { - const loader = yield* ThreadSnapshotLoader; + const loader = yield* ThreadSnapshotLoader.ThreadSnapshotLoader; const result = yield* loader.load(PREPARED, THREAD_ID); expect(result).toEqual({ _tag: "unavailable" }); expect(fullCalls).toBe(0); diff --git a/packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts b/packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts index e1624e94964c..1da5841db975 100644 --- a/packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts +++ b/packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts @@ -5,19 +5,15 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import { HttpClient } from "effect/unstable/http"; -import { RemoteEnvironmentAuthorization } from "../authorization/service.ts"; +import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; import type { PreparedConnection } from "../connection/model.ts"; import { environmentEndpointUrl } from "../environment/endpoint.ts"; -import { ManagedRelayDpopSigner } from "../relay/managedRelay.ts"; +import * as ManagedRelay from "../relay/managedRelay.ts"; import { executeAuthenticatedEnvironmentHttpRequest, withOrchestrationProtocolHeader, } from "./environmentHttpAuth.ts"; -import { - fetchEnvironmentThreadSnapshot, - ThreadSnapshotLoader, - type ThreadSnapshotLoadResult, -} from "./threadSnapshotHttp.ts"; +import * as ThreadSnapshotLoader from "./threadSnapshotHttp.ts"; // Same cold-open budget as the full snapshot path; bounded payloads should fit. const DEFAULT_BOUNDED_THREAD_SNAPSHOT_TIMEOUT_MS = 6_000; @@ -28,8 +24,10 @@ export const fetchEnvironmentBoundedThreadSnapshot = Effect.fn( )(function* (input: { readonly prepared: PreparedConnection; readonly threadId: ThreadId; - readonly signer: Option.Option; - readonly remoteAuthorization?: Option.Option; + readonly signer: Option.Option; + readonly remoteAuthorization?: Option.Option< + RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization["Service"] + >; readonly timeoutMs?: number; }) { return yield* executeAuthenticatedEnvironmentHttpRequest({ @@ -56,24 +54,26 @@ export const fetchEnvironmentBoundedThreadSnapshot = Effect.fn( * socket path remains a last resort for connectivity issues. */ export const boundedThreadSnapshotLoaderLayer: Layer.Layer< - ThreadSnapshotLoader, + ThreadSnapshotLoader.ThreadSnapshotLoader, never, HttpClient.HttpClient > = Layer.effect( - ThreadSnapshotLoader, + ThreadSnapshotLoader.ThreadSnapshotLoader, Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; - const signer = yield* Effect.serviceOption(ManagedRelayDpopSigner); - const remoteAuthorization = yield* Effect.serviceOption(RemoteEnvironmentAuthorization); - return ThreadSnapshotLoader.of({ + const signer = yield* Effect.serviceOption(ManagedRelay.ManagedRelayDpopSigner); + const remoteAuthorization = yield* Effect.serviceOption( + RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization, + ); + return ThreadSnapshotLoader.ThreadSnapshotLoader.of({ load: (prepared: PreparedConnection, threadId: ThreadId) => { - const loadFullFallback = fetchEnvironmentThreadSnapshot({ + const loadFullFallback = ThreadSnapshotLoader.fetchEnvironmentThreadSnapshot({ prepared, threadId, signer, remoteAuthorization, }).pipe( - Effect.map((snapshot): ThreadSnapshotLoadResult => ({ + Effect.map((snapshot): ThreadSnapshotLoader.ThreadSnapshotLoadResult => ({ _tag: "present", snapshot, })), @@ -84,7 +84,9 @@ export const boundedThreadSnapshotLoaderLayer: Layer.Layer< "Full thread snapshot not found over HTTP after bounded fallback; treating the thread as deleted.", ).pipe( Effect.annotateLogs({ threadId }), - Effect.as({ _tag: "missing" } satisfies ThreadSnapshotLoadResult), + Effect.as({ + _tag: "missing", + } satisfies ThreadSnapshotLoader.ThreadSnapshotLoadResult), ), }), Effect.catchCause((cause) => @@ -92,7 +94,9 @@ export const boundedThreadSnapshotLoaderLayer: Layer.Layer< "Could not load the full thread snapshot over HTTP after bounded fallback; using the socket snapshot instead.", ).pipe( Effect.annotateLogs({ threadId, cause: Cause.pretty(cause) }), - Effect.as({ _tag: "unavailable" } satisfies ThreadSnapshotLoadResult), + Effect.as({ + _tag: "unavailable", + } satisfies ThreadSnapshotLoader.ThreadSnapshotLoadResult), ), ), ); @@ -103,7 +107,7 @@ export const boundedThreadSnapshotLoaderLayer: Layer.Layer< signer, remoteAuthorization, }).pipe( - Effect.map((bounded): ThreadSnapshotLoadResult => ({ + Effect.map((bounded): ThreadSnapshotLoader.ThreadSnapshotLoadResult => ({ _tag: "present", snapshot: { snapshotSequence: bounded.snapshotSequence, @@ -123,7 +127,9 @@ export const boundedThreadSnapshotLoaderLayer: Layer.Layer< "Bounded thread snapshot not found over HTTP; treating the thread as deleted.", ).pipe( Effect.annotateLogs({ threadId }), - Effect.as({ _tag: "missing" } satisfies ThreadSnapshotLoadResult), + Effect.as({ + _tag: "missing", + } satisfies ThreadSnapshotLoader.ThreadSnapshotLoadResult), ), RemoteEnvironmentAuthInvalidJsonError: (error) => Effect.logDebug( @@ -144,7 +150,9 @@ export const boundedThreadSnapshotLoaderLayer: Layer.Layer< "Could not load the bounded thread snapshot over HTTP; using the socket snapshot instead.", ).pipe( Effect.annotateLogs({ threadId, cause: Cause.pretty(cause) }), - Effect.as({ _tag: "unavailable" } satisfies ThreadSnapshotLoadResult), + Effect.as({ + _tag: "unavailable", + } satisfies ThreadSnapshotLoader.ThreadSnapshotLoadResult), ), ), ); diff --git a/packages/client-runtime/src/state/deviceHubAccess.ts b/packages/client-runtime/src/state/deviceHubAccess.ts index 9ab65b25e634..92711db53dd8 100644 --- a/packages/client-runtime/src/state/deviceHubAccess.ts +++ b/packages/client-runtime/src/state/deviceHubAccess.ts @@ -14,10 +14,10 @@ import * as Effect from "effect/Effect"; import type { HttpClient } from "effect/unstable/http"; -import { RemoteEnvironmentAuthorization } from "../authorization/service.ts"; +import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; import type { PreparedConnection } from "../connection/model.ts"; import { environmentEndpointUrl } from "../environment/endpoint.ts"; -import { ManagedRelayDpopSigner } from "../relay/managedRelay.ts"; +import * as ManagedRelay from "../relay/managedRelay.ts"; import type { RemoteEnvironmentRequestError } from "../rpc/http.ts"; import { executeAuthenticatedEnvironmentHttpRequest } from "./environmentHttpAuth.ts"; import type { DeviceHubAccess } from "../device/hubAccess.ts"; @@ -36,8 +36,10 @@ export const resolveDeviceHubAccess = Effect.fn("clientRuntime.state.resolveDevi if (input.prepared.httpAuthorization === null) { return { httpBase, wsBase, query: {}, credentials: true }; } - const signer = yield* Effect.serviceOption(ManagedRelayDpopSigner); - const remoteAuthorization = yield* Effect.serviceOption(RemoteEnvironmentAuthorization); + const signer = yield* Effect.serviceOption(ManagedRelay.ManagedRelayDpopSigner); + const remoteAuthorization = yield* Effect.serviceOption( + RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization, + ); const ticket = yield* executeAuthenticatedEnvironmentHttpRequest({ prepared: input.prepared, signer, diff --git a/packages/client-runtime/src/state/environmentHttpAuth.test.ts b/packages/client-runtime/src/state/environmentHttpAuth.test.ts index 6ef2abcd38f0..6be3c9af0282 100644 --- a/packages/client-runtime/src/state/environmentHttpAuth.test.ts +++ b/packages/client-runtime/src/state/environmentHttpAuth.test.ts @@ -19,30 +19,24 @@ import * as Schema from "effect/Schema"; import { TestClock } from "effect/testing"; import type { HttpClient } from "effect/unstable/http"; -import { RemoteEnvironmentAuthorization } from "../authorization/service.ts"; +import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; import { ConnectionTransientError, RelayConnectionTarget, type PreparedConnection, type PreparedHttpAuthorization, } from "../connection/model.ts"; -import { ManagedRelayDpopSigner, type ManagedRelayDpopProofInput } from "../relay/managedRelay.ts"; +import * as ManagedRelay from "../relay/managedRelay.ts"; import { remoteHttpClientLayer, type RemoteEnvironmentRequestError } from "../rpc/http.ts"; -import { - fetchEnvironmentPullRequestDiff, - type PullRequestDiffCredentialRejectedError, - PullRequestDiffLoader, - pullRequestDiffLoaderLayer, -} from "./pullRequestDiffHttp.ts"; +import * as PullRequestDiffLoader from "./pullRequestDiffHttp.ts"; import { withOrchestrationProtocolHeader } from "./environmentHttpAuth.ts"; import { fetchEnvironmentSessionState } from "./session.ts"; import { fetchEnvironmentShellSnapshot } from "./shellSnapshotHttp.ts"; -import { fetchEnvironmentThreadSnapshot } from "./threadSnapshotHttp.ts"; +import * as ThreadSnapshotLoader from "./threadSnapshotHttp.ts"; import { boundedThreadSnapshotLoaderLayer, fetchEnvironmentBoundedThreadSnapshot, } from "./boundedThreadSnapshotHttp.ts"; -import { ThreadSnapshotLoader } from "./threadSnapshotHttp.ts"; import { fetchEnvironmentThreadHistoryPage } from "./threadHistoryHttp.ts"; import { v2Projection } from "./orchestrationV2TestFixtures.ts"; @@ -121,10 +115,12 @@ function credentialRejectedResponse(reason = "invalid_credential") { function makeHarness(reply: (requestNumber: number) => Response | Promise) { const calls: Array<{ readonly url: string; readonly init: RequestInit }> = []; const authorizations: Array< - Parameters[0] + Parameters< + RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization["Service"]["authorizeDpopHttp"] + >[0] > = []; - const proofs: Array = []; - const remoteAuthorization = RemoteEnvironmentAuthorization.of({ + const proofs: Array = []; + const remoteAuthorization = RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization.of({ authorizeBearer: () => Effect.die("Unexpected bearer connection preparation."), authorizeDpop: () => Effect.die("HTTP requests must not prepare a WebSocket connection."), authorizeDpopHttp: (input) => @@ -143,7 +139,7 @@ function makeHarness(reply: (requestNumber: number) => Response | Promise Effect.sync(() => { @@ -180,7 +176,7 @@ const LOADERS: ReadonlyArray<{ input: HttpInput, ) => Effect.Effect< unknown, - RemoteEnvironmentRequestError | PullRequestDiffCredentialRejectedError, + RemoteEnvironmentRequestError | PullRequestDiffLoader.PullRequestDiffCredentialRejectedError, HttpClient.HttpClient >; }> = [ @@ -190,7 +186,8 @@ const LOADERS: ReadonlyArray<{ path: "/api/pull-requests/diff", response: DIFF_RESULT, expected: DIFF_RESULT, - load: (input: HttpInput) => fetchEnvironmentPullRequestDiff({ ...input, diff: DIFF }), + load: (input: HttpInput) => + PullRequestDiffLoader.fetchEnvironmentPullRequestDiff({ ...input, diff: DIFF }), }, { name: "session permissions", @@ -215,7 +212,10 @@ const LOADERS: ReadonlyArray<{ response: encodeThreadSnapshot(THREAD), expected: THREAD, load: (input: HttpInput) => - fetchEnvironmentThreadSnapshot({ ...input, threadId: THREAD.projection.thread.id }), + ThreadSnapshotLoader.fetchEnvironmentThreadSnapshot({ + ...input, + threadId: THREAD.projection.thread.id, + }), }, { name: "bounded thread snapshot", @@ -293,9 +293,10 @@ describe("authenticated environment HTTP requests", () => { const harness = makeHarness((requestNumber) => requestNumber === 1 ? credentialRejectedResponse() : Response.json(DIFF_RESULT), ); - const result = yield* fetchEnvironmentPullRequestDiff({ ...harness.input, diff: DIFF }).pipe( - Effect.provide(harness.httpLayer), - ); + const result = yield* PullRequestDiffLoader.fetchEnvironmentPullRequestDiff({ + ...harness.input, + diff: DIFF, + }).pipe(Effect.provide(harness.httpLayer)); expect(result).toEqual(DIFF_RESULT); expect(harness.authorizations).toEqual([ @@ -363,12 +364,20 @@ describe("authenticated environment HTTP requests", () => { Layer.provide( Layer.mergeAll( harness.httpLayer, - Layer.succeed(ManagedRelayDpopSigner, Option.getOrThrow(harness.input.signer)), - Layer.succeed(RemoteEnvironmentAuthorization, harness.remoteAuthorization), + Layer.succeed( + ManagedRelay.ManagedRelayDpopSigner, + Option.getOrThrow(harness.input.signer), + ), + Layer.succeed( + RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization, + harness.remoteAuthorization, + ), ), ), ); - const loader = yield* ThreadSnapshotLoader.pipe(Effect.provide(loaderLayer)); + const loader = yield* ThreadSnapshotLoader.ThreadSnapshotLoader.pipe( + Effect.provide(loaderLayer), + ); const result = yield* loader.load(PREPARED, THREAD.projection.thread.id); expect(result).toEqual({ _tag: "present", @@ -389,16 +398,24 @@ describe("authenticated environment HTTP requests", () => { it.effect("uses the authorization service captured by the diff loader layer", () => Effect.gen(function* () { const harness = makeHarness(() => Response.json(DIFF_RESULT)); - const loaderLayer = pullRequestDiffLoaderLayer.pipe( + const loaderLayer = PullRequestDiffLoader.layer.pipe( Layer.provide( Layer.mergeAll( harness.httpLayer, - Layer.succeed(ManagedRelayDpopSigner, Option.getOrThrow(harness.input.signer)), - Layer.succeed(RemoteEnvironmentAuthorization, harness.remoteAuthorization), + Layer.succeed( + ManagedRelay.ManagedRelayDpopSigner, + Option.getOrThrow(harness.input.signer), + ), + Layer.succeed( + RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization, + harness.remoteAuthorization, + ), ), ), ); - const loader = yield* PullRequestDiffLoader.pipe(Effect.provide(loaderLayer)); + const loader = yield* PullRequestDiffLoader.PullRequestDiffLoader.pipe( + Effect.provide(loaderLayer), + ); const result = yield* loader.load(PREPARED, DIFF); expect(result).toEqual(DIFF_RESULT); @@ -411,10 +428,10 @@ describe("authenticated environment HTTP requests", () => { it.effect("preserves the credential rejection after the one recovery attempt fails", () => Effect.gen(function* () { const harness = makeHarness(() => credentialRejectedResponse()); - const error = yield* fetchEnvironmentPullRequestDiff({ ...harness.input, diff: DIFF }).pipe( - Effect.provide(harness.httpLayer), - Effect.flip, - ); + const error = yield* PullRequestDiffLoader.fetchEnvironmentPullRequestDiff({ + ...harness.input, + diff: DIFF, + }).pipe(Effect.provide(harness.httpLayer), Effect.flip); expect(error).toMatchObject({ _tag: "PullRequestDiffCredentialRejectedError", @@ -458,10 +475,10 @@ describe("authenticated environment HTTP requests", () => { ])("does not renew or retry on $name", ({ reply, errorTag }) => Effect.gen(function* () { const harness = makeHarness(reply); - const error = yield* fetchEnvironmentPullRequestDiff({ ...harness.input, diff: DIFF }).pipe( - Effect.provide(harness.httpLayer), - Effect.flip, - ); + const error = yield* PullRequestDiffLoader.fetchEnvironmentPullRequestDiff({ + ...harness.input, + diff: DIFF, + }).pipe(Effect.provide(harness.httpLayer), Effect.flip); expect(error._tag).toBe(errorTag); expect(harness.calls).toHaveLength(1); @@ -532,7 +549,7 @@ describe("authenticated environment HTTP requests", () => { Effect.gen(function* () { const harness = makeHarness(() => Response.json(SESSION)); const authorizing = yield* Deferred.make(); - const remoteAuthorization = RemoteEnvironmentAuthorization.of({ + const remoteAuthorization = RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization.of({ ...harness.remoteAuthorization, authorizeDpopHttp: () => Deferred.succeed(authorizing, undefined).pipe(Effect.andThen(Effect.never)), @@ -564,7 +581,7 @@ describe("authenticated environment HTTP requests", () => { }); const authorizing = yield* Deferred.make(); const authorize = yield* Deferred.make(); - const remoteAuthorization = RemoteEnvironmentAuthorization.of({ + const remoteAuthorization = RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization.of({ ...harness.remoteAuthorization, authorizeDpopHttp: (input) => Deferred.succeed(authorizing, undefined).pipe( @@ -601,7 +618,7 @@ describe("authenticated environment HTTP requests", () => { reason: "transport", detail: "Relay unavailable", }); - const remoteAuthorization = RemoteEnvironmentAuthorization.of({ + const remoteAuthorization = RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization.of({ ...harness.remoteAuthorization, authorizeDpopHttp: () => Effect.fail(failure), }); diff --git a/packages/client-runtime/src/state/pullRequestDiffHttp.ts b/packages/client-runtime/src/state/pullRequestDiffHttp.ts index dd514259d983..519582a35b30 100644 --- a/packages/client-runtime/src/state/pullRequestDiffHttp.ts +++ b/packages/client-runtime/src/state/pullRequestDiffHttp.ts @@ -10,9 +10,9 @@ import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import { HttpClient } from "effect/unstable/http"; -import { RemoteEnvironmentAuthorization } from "../authorization/service.ts"; +import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; import type { PreparedConnection } from "../connection/model.ts"; -import { ManagedRelayDpopSigner } from "../relay/managedRelay.ts"; +import * as ManagedRelay from "../relay/managedRelay.ts"; import { makeEnvironmentHttpApiUrlBuilder, type RemoteEnvironmentRequestError, @@ -44,8 +44,10 @@ export const fetchEnvironmentPullRequestDiff = Effect.fn( )(function* (input: { readonly prepared: PreparedConnection; readonly diff: PullRequestDiffInput; - readonly signer: Option.Option; - readonly remoteAuthorization?: Option.Option; + readonly signer: Option.Option; + readonly remoteAuthorization?: Option.Option< + RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization["Service"] + >; readonly timeoutMs?: number; }) { return yield* executeAuthenticatedEnvironmentHttpRequest({ @@ -79,16 +81,14 @@ export class PullRequestDiffLoader extends Context.Service< } >()("@t3tools/client-runtime/state/pullRequestDiffHttp/PullRequestDiffLoader") {} -export const pullRequestDiffLoaderLayer: Layer.Layer< - PullRequestDiffLoader, - never, - HttpClient.HttpClient -> = Layer.effect( +export const layer: Layer.Layer = Layer.effect( PullRequestDiffLoader, Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; - const signer = yield* Effect.serviceOption(ManagedRelayDpopSigner); - const remoteAuthorization = yield* Effect.serviceOption(RemoteEnvironmentAuthorization); + const signer = yield* Effect.serviceOption(ManagedRelay.ManagedRelayDpopSigner); + const remoteAuthorization = yield* Effect.serviceOption( + RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization, + ); return PullRequestDiffLoader.of({ load: (prepared, input) => fetchEnvironmentPullRequestDiff({ diff --git a/packages/client-runtime/src/state/pullRequestRouting.ts b/packages/client-runtime/src/state/pullRequestRouting.ts index 7ea3e18b09bd..643c6f8735ea 100644 --- a/packages/client-runtime/src/state/pullRequestRouting.ts +++ b/packages/client-runtime/src/state/pullRequestRouting.ts @@ -10,14 +10,14 @@ import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as SubscriptionRef from "effect/SubscriptionRef"; -import { EnvironmentRegistry, EnvironmentNotRegisteredError } from "../connection/registry.ts"; -import { EnvironmentSupervisor } from "../connection/supervisor.ts"; +import * as EnvironmentRegistry from "../connection/registry.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; import { GitHubRoutingPermissions, gitHubRoutingConnectionKey, } from "../connection/githubRoutingPermissions.ts"; import type { ConnectionCatalogEntry } from "../connection/catalog.ts"; -import { ConnectionProfileStore } from "../connection/profileStore.ts"; +import * as ConnectionProfileStore from "../connection/profileStore.ts"; import { request, EnvironmentRpcUnavailableError, @@ -69,8 +69,11 @@ interface RoutedRead { reference: PullRequestRef; targets: Set; } -const routedReads = new WeakMap>(); -const isUnregistered = Schema.is(EnvironmentNotRegisteredError); +const routedReads = new WeakMap< + EnvironmentRegistry.EnvironmentRegistry["Service"], + Map +>(); +const isUnregistered = Schema.is(EnvironmentRegistry.EnvironmentNotRegisteredError); const encodeKey = Schema.encodeSync( Schema.fromJsonString(Schema.Array(Schema.NullOr(Schema.String))), ); @@ -105,7 +108,7 @@ function rejectedBeforeDispatch(error: unknown): boolean { } const routingAllowed = Effect.fn("PullRequestRouting.allowed")(function* ( - registry: EnvironmentRegistry["Service"], + registry: EnvironmentRegistry.EnvironmentRegistry["Service"], originId: EnvironmentId, destinationId: EnvironmentId, write: boolean, @@ -123,7 +126,7 @@ const routingAllowed = Effect.fn("PullRequestRouting.allowed")(function* ( if (!allowed) return false; for (const entry of [origin, destination]) { if (entry.target._tag !== "SshConnectionTarget") continue; - const profiles = yield* Effect.serviceOption(ConnectionProfileStore); + const profiles = yield* Effect.serviceOption(ConnectionProfileStore.ConnectionProfileStore); if (Option.isNone(profiles)) return false; const profile = yield* profiles.value .get(entry.target.connectionId) @@ -144,7 +147,7 @@ function matchesReference(reference: PullRequestRef, filter: PullRequestRef): bo } const invalidateTarget = Effect.fn("PullRequestRouting.invalidateTarget")(function* ( - registry: EnvironmentRegistry["Service"], + registry: EnvironmentRegistry.EnvironmentRegistry["Service"], origin: EnvironmentId, target: EnvironmentId, inputs: ReadonlyArray, @@ -170,8 +173,8 @@ export function createPullRequestRouter() { const source = sourceRead ?? request(tag, input); if (tag === WS_METHODS.pullRequestsInvalidate && isInvalidation(input)) { const result = yield* request(tag, input); - const origin = yield* EnvironmentSupervisor; - const registry = yield* EnvironmentRegistry; + const origin = yield* EnvironmentSupervisor.EnvironmentSupervisor; + const registry = yield* EnvironmentRegistry.EnvironmentRegistry; const used = routedReads.get(registry); const targets = new Map(); for (const entry of used?.values() ?? []) { @@ -196,8 +199,8 @@ export function createPullRequestRouter() { return yield* request(tag, input); } const ref = input; - const origin = yield* EnvironmentSupervisor; - const registry = yield* EnvironmentRegistry; + const origin = yield* EnvironmentSupervisor.EnvironmentSupervisor; + const registry = yield* EnvironmentRegistry.EnvironmentRegistry; const entries = yield* SubscriptionRef.get(registry.entries); const sourceEntry = entries.get(origin.target.environmentId); const used = routedReads.get(registry) ?? new Map(); @@ -251,7 +254,12 @@ export function createPullRequestRouter() { if (!(yield* routingAllowed(registry, origin.target.environmentId, id, writes.has(tag)))) continue; const connected = yield* registry - .run(id, EnvironmentSupervisor.pipe(Effect.flatMap((s) => SubscriptionRef.get(s.session)))) + .run( + id, + EnvironmentSupervisor.EnvironmentSupervisor.pipe( + Effect.flatMap((s) => SubscriptionRef.get(s.session)), + ), + ) .pipe(Effect.orElseSucceed(() => Option.none())); if (Option.isNone(connected)) continue; if (tag === WS_METHODS.pullRequestsChecks) { @@ -378,10 +386,10 @@ export function createPullRequestRouter() { input: EnvironmentRpcInput, ) { if (!reads.has(tag) || !isRef(input)) return yield* routedRequest(tag, input); - const registry = yield* EnvironmentRegistry; + const registry = yield* EnvironmentRegistry.EnvironmentRegistry; const entries = yield* SubscriptionRef.get(registry.entries); if (entries.size < 2) return yield* request(tag, input); - const origin = yield* EnvironmentSupervisor; + const origin = yield* EnvironmentSupervisor.EnvironmentSupervisor; let allowed = false; for (const id of entries.keys()) { if ( diff --git a/packages/client-runtime/src/state/pullRequests.test.ts b/packages/client-runtime/src/state/pullRequests.test.ts index 53c46edb251b..86e4ad51196f 100644 --- a/packages/client-runtime/src/state/pullRequests.test.ts +++ b/packages/client-runtime/src/state/pullRequests.test.ts @@ -29,7 +29,7 @@ import { } from "../connection/model.ts"; import * as EnvironmentRegistry from "../connection/registry.ts"; import { SshConnectionProfile, type ConnectionCatalogEntry } from "../connection/catalog.ts"; -import { ConnectionProfileStore } from "../connection/profileStore.ts"; +import * as ConnectionProfileStore from "../connection/profileStore.ts"; import * as EnvironmentSupervisor from "../connection/supervisor.ts"; import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; import type { RpcSession } from "../rpc/session.ts"; @@ -38,7 +38,7 @@ import { createPullRequestEnvironmentAtoms, createPullRequestStackAtomFamily, } from "./pullRequests.ts"; -import { PullRequestDiffLoader } from "./pullRequestDiffHttp.ts"; +import * as PullRequestDiffLoader from "./pullRequestDiffHttp.ts"; import { executeAtomQuery } from "./runtime.ts"; import { createPullRequestRouter } from "./pullRequestRouting.ts"; import { GitHubRoutingPermissions } from "../connection/githubRoutingPermissions.ts"; @@ -408,8 +408,8 @@ const makeTestRuntime = Effect.fn("makeTestRuntime")(function* ( Layer.merge( Layer.succeed(EnvironmentRegistry.EnvironmentRegistry, environmentRegistry), Layer.succeed( - PullRequestDiffLoader, - PullRequestDiffLoader.of({ load: () => Effect.die("unused") }), + PullRequestDiffLoader.PullRequestDiffLoader, + PullRequestDiffLoader.PullRequestDiffLoader.of({ load: () => Effect.die("unused") }), ), ), ); @@ -580,7 +580,7 @@ for (const side of ["origin", "destination"] as const) { yield* stored === "unavailable" ? route : route.pipe( - Effect.provideService(ConnectionProfileStore, { + Effect.provideService(ConnectionProfileStore.ConnectionProfileStore, { get: () => read, put: () => Effect.die("unused"), remove: () => Effect.die("unused"), diff --git a/packages/client-runtime/src/state/pullRequests.ts b/packages/client-runtime/src/state/pullRequests.ts index 0d325af7ecf4..ad6f4f9e1209 100644 --- a/packages/client-runtime/src/state/pullRequests.ts +++ b/packages/client-runtime/src/state/pullRequests.ts @@ -22,16 +22,15 @@ import { createEnvironmentQueryAtomFamily, } from "./runtime.ts"; import { createPullRequestRouter } from "./pullRequestRouting.ts"; -import { PullRequestDiffLoader } from "./pullRequestDiffHttp.ts"; +import * as PullRequestDiffLoader from "./pullRequestDiffHttp.ts"; import type { EnvironmentRegistry } from "../connection/registry.ts"; -import { EnvironmentSupervisor } from "../connection/supervisor.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; export { type PullRequestDiffLoadError, PullRequestDiffCredentialRejectedError, - PullRequestDiffLoader, - pullRequestDiffLoaderLayer, } from "./pullRequestDiffHttp.ts"; +export * as PullRequestDiffLoader from "./pullRequestDiffHttp.ts"; /** @public Required to name the error in consumers' inferred pull request results. */ export class EnvironmentHttpConnectionNotReadyError extends Data.TaggedError( @@ -155,7 +154,10 @@ export function pullRequestDetailToVcsStatus( * pull request are order-sensitive. Confirmed label and reviewer edits update cached state. */ export function createPullRequestEnvironmentAtoms( - runtime: Atom.AtomRuntime, + runtime: Atom.AtomRuntime< + EnvironmentRegistry | PullRequestDiffLoader.PullRequestDiffLoader | R, + E + >, ) { const refreshes = createPullRequestRefreshAtomFamily(runtime); const commandScheduler = createAtomCommandScheduler(); @@ -261,8 +263,8 @@ export function createPullRequestEnvironmentAtoms( staleTimeMs: 60_000, execute: (input: PullRequestDiffInput) => Effect.gen(function* () { - const supervisor = yield* EnvironmentSupervisor; - const loader = yield* PullRequestDiffLoader; + const supervisor = yield* EnvironmentSupervisor.EnvironmentSupervisor; + const loader = yield* PullRequestDiffLoader.PullRequestDiffLoader; const prepared = yield* SubscriptionRef.get(supervisor.prepared); if (Option.isNone(prepared)) { return yield* new EnvironmentHttpConnectionNotReadyError({ diff --git a/packages/client-runtime/src/state/runtime.ts b/packages/client-runtime/src/state/runtime.ts index 0b9b0006e9fe..ef72946f5be2 100644 --- a/packages/client-runtime/src/state/runtime.ts +++ b/packages/client-runtime/src/state/runtime.ts @@ -8,7 +8,7 @@ import * as SubscriptionRef from "effect/SubscriptionRef"; import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity"; import type { ConnectionAttemptError } from "../connection/model.ts"; -import { EnvironmentNotRegisteredError, EnvironmentRegistry } from "../connection/registry.ts"; +import * as EnvironmentRegistry from "../connection/registry.ts"; import { type EnvironmentRpcInput, type EnvironmentRpcSuccess, @@ -21,7 +21,7 @@ import { request, subscribe, } from "../rpc/client.ts"; -import { EnvironmentSupervisor } from "../connection/supervisor.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; interface EnvironmentAtomOptions { readonly label: string; @@ -451,10 +451,10 @@ function runInEnvironment( effect: Effect.Effect, ): Effect.Effect< A, - E | EnvironmentNotRegisteredError, - EnvironmentRegistry | Exclude + E | EnvironmentRegistry.EnvironmentNotRegisteredError, + EnvironmentRegistry.EnvironmentRegistry | Exclude > { - return EnvironmentRegistry.pipe( + return EnvironmentRegistry.EnvironmentRegistry.pipe( Effect.flatMap((registry) => registry.run(environmentId, effect)), ); } @@ -464,32 +464,41 @@ export function runStreamInEnvironment( stream: Stream.Stream, ): Stream.Stream< A, - E | EnvironmentNotRegisteredError, - EnvironmentRegistry | Exclude + E | EnvironmentRegistry.EnvironmentNotRegisteredError, + EnvironmentRegistry.EnvironmentRegistry | Exclude > { return Stream.unwrap( - EnvironmentRegistry.pipe(Effect.map((registry) => registry.runStream(environmentId, stream))), + EnvironmentRegistry.EnvironmentRegistry.pipe( + Effect.map((registry) => registry.runStream(environmentId, stream)), + ), ); } export function followStreamInEnvironment( environmentId: EnvironmentIdType, stream: Stream.Stream, -): Stream.Stream> { +): Stream.Stream< + A, + E, + EnvironmentRegistry.EnvironmentRegistry | Exclude +> { return Stream.unwrap( - EnvironmentRegistry.pipe( + EnvironmentRegistry.EnvironmentRegistry.pipe( Effect.map((registry) => registry.followStream(environmentId, stream)), ), ); } export function createEnvironmentQueryAtomFamily( - runtime: Atom.AtomRuntime, + runtime: Atom.AtomRuntime, options: EnvironmentQueryAtomOptions< Input, A, E, - EnvironmentSupervisor | EnvironmentRegistry | AtomRegistry.AtomRegistry | R + | EnvironmentSupervisor.EnvironmentSupervisor + | EnvironmentRegistry.EnvironmentRegistry + | AtomRegistry.AtomRegistry + | R >, ): (target: { readonly environmentId: EnvironmentIdType; @@ -500,7 +509,7 @@ export function createEnvironmentQueryAtomFamily( followStreamInEnvironment( environmentId, Stream.unwrap( - EnvironmentSupervisor.pipe( + EnvironmentSupervisor.EnvironmentSupervisor.pipe( Effect.map((supervisor) => SubscriptionRef.changes(supervisor.state).pipe( Stream.zipLatest(SubscriptionRef.changes(supervisor.session)), @@ -518,7 +527,10 @@ export function createEnvironmentQueryAtomFamily( const queryAtom = runtime .atom< A, - E | ConnectionAttemptError | EnvironmentNotRegisteredError | EnvironmentRpcUnavailableError + | E + | ConnectionAttemptError + | EnvironmentRegistry.EnvironmentNotRegisteredError + | EnvironmentRpcUnavailableError >((get) => { const connection = Option.getOrNull( AsyncResult.value(get(connectionAtom(target.environmentId))), @@ -573,8 +585,13 @@ export function createEnvironmentQueryAtomFamily( } export function createEnvironmentSubscriptionAtomFamily( - runtime: Atom.AtomRuntime, - options: EnvironmentSubscriptionAtomOptions, + runtime: Atom.AtomRuntime, + options: EnvironmentSubscriptionAtomOptions< + Input, + A, + E, + EnvironmentSupervisor.EnvironmentSupervisor | R + >, ) { const family = Atom.family((key: string) => { const target = parseEnvironmentRpcKey(key); @@ -594,12 +611,12 @@ export function createEnvironmentSubscriptionAtomFamily( } export function createEnvironmentCommand( - runtime: Atom.AtomRuntime, + runtime: Atom.AtomRuntime, options: EnvironmentCommandAtomOptions< Input, A, E, - EnvironmentSupervisor | EnvironmentRegistry | R + EnvironmentSupervisor.EnvironmentSupervisor | EnvironmentRegistry.EnvironmentRegistry | R >, ) { return createRuntimeCommand(runtime, { @@ -615,7 +632,7 @@ export function createEnvironmentCommand( } export function createEnvironmentRpcQueryAtomFamily( - runtime: Atom.AtomRuntime, + runtime: Atom.AtomRuntime, options: { readonly label: string; readonly tag: TTag; @@ -624,7 +641,7 @@ export function createEnvironmentRpcQueryAtomFamily Effect.Effect< EnvironmentRpcSuccess, EnvironmentRpcFailure | EnvironmentRpcUnavailableError, - EnvironmentSupervisor | EnvironmentRegistry + EnvironmentSupervisor.EnvironmentSupervisor | EnvironmentRegistry.EnvironmentRegistry >; readonly staleTimeMs?: number; readonly idleTtlMs?: number; @@ -654,7 +671,7 @@ export function createEnvironmentRpcSubscriptionAtomFamily< TTag extends EnvironmentSubscriptionRpcTag, B = EnvironmentRpcStreamValue, >( - runtime: Atom.AtomRuntime, + runtime: Atom.AtomRuntime, options: { readonly label: string; readonly tag: TTag; @@ -663,9 +680,13 @@ export function createEnvironmentRpcSubscriptionAtomFamily< stream: Stream.Stream< EnvironmentRpcStreamValue, EnvironmentRpcStreamFailure, - EnvironmentSupervisor | R + EnvironmentSupervisor.EnvironmentSupervisor | R >, - ) => Stream.Stream, EnvironmentSupervisor | R>; + ) => Stream.Stream< + B, + EnvironmentRpcStreamFailure, + EnvironmentSupervisor.EnvironmentSupervisor | R + >; }, ) { return createEnvironmentSubscriptionAtomFamily(runtime, { @@ -674,14 +695,18 @@ export function createEnvironmentRpcSubscriptionAtomFamily< subscribe: (input: EnvironmentRpcInput) => { const stream = subscribe(options.tag, input); return options.transform === undefined - ? (stream as Stream.Stream, EnvironmentSupervisor | R>) + ? (stream as Stream.Stream< + B, + EnvironmentRpcStreamFailure, + EnvironmentSupervisor.EnvironmentSupervisor | R + >) : options.transform(stream); }, }); } export function createEnvironmentRpcCommand( - runtime: Atom.AtomRuntime, + runtime: Atom.AtomRuntime, options: { readonly label: string; readonly tag: TTag; @@ -690,7 +715,7 @@ export function createEnvironmentRpcCommand Effect.Effect< EnvironmentRpcSuccess, EnvironmentRpcFailure | EnvironmentRpcUnavailableError, - EnvironmentSupervisor | EnvironmentRegistry + EnvironmentSupervisor.EnvironmentSupervisor | EnvironmentRegistry.EnvironmentRegistry >; readonly scheduler?: AtomCommandScheduler; readonly concurrency?: AtomCommandConcurrency<{ diff --git a/packages/client-runtime/src/state/server.ts b/packages/client-runtime/src/state/server.ts index fb294b9aae74..ed5bc89469b1 100644 --- a/packages/client-runtime/src/state/server.ts +++ b/packages/client-runtime/src/state/server.ts @@ -33,10 +33,10 @@ import { createRuntimeCommand, scheduleAtomCommandEffect, } from "./runtime.ts"; -import { EnvironmentRegistry } from "../connection/registry.ts"; -import { EnvironmentSupervisor } from "../connection/supervisor.ts"; +import * as EnvironmentRegistry from "../connection/registry.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; import { safeErrorLogAttributes } from "../errors/safeLog.ts"; -import { EnvironmentCacheStore } from "../platform/persistence.ts"; +import * as Persistence from "../platform/persistence.ts"; import { runCachePersistence } from "./cachePersistence.ts"; import { isRpcClientError, @@ -366,8 +366,8 @@ export interface ServerConfigSubscriptionOptions { export const makeEnvironmentServerConfigState = Effect.fn("EnvironmentServerConfigState.make")( function* (subscription: ServerConfigSubscriptionOptions) { - const supervisor = yield* EnvironmentSupervisor; - const cache = yield* EnvironmentCacheStore; + const supervisor = yield* EnvironmentSupervisor.EnvironmentSupervisor; + const cache = yield* Persistence.EnvironmentCacheStore; const environmentId = supervisor.target.environmentId; const cachedConfig = yield* cache.loadServerConfig(environmentId).pipe( Effect.catch((error) => @@ -510,7 +510,7 @@ export function resolveServerWelcomeState( export const makeEnvironmentServerWelcomeState = Effect.fn("EnvironmentServerWelcomeState.make")( function* () { - const supervisor = yield* EnvironmentSupervisor; + const supervisor = yield* EnvironmentSupervisor.EnvironmentSupervisor; const initialSession = Option.getOrNull(yield* SubscriptionRef.get(supervisor.session)); const state = yield* SubscriptionRef.make({ currentSession: initialSession, @@ -609,7 +609,10 @@ export function resolveServerConfigValue( } export function createServerEnvironmentAtoms( - runtime: Atom.AtomRuntime, + runtime: Atom.AtomRuntime< + EnvironmentRegistry.EnvironmentRegistry | Persistence.EnvironmentCacheStore | R, + E + >, options: { readonly initialConfigValueAtom: ( environmentId: EnvironmentId, @@ -678,7 +681,7 @@ export function createServerEnvironmentAtoms( const updateStateAtom = (environmentId: EnvironmentId | null) => environmentId === null ? EMPTY_SERVER_UPDATE_STATE_ATOM : updateStateValueAtom(environmentId); const updateServer = createRuntimeCommand< - EnvironmentRegistry | EnvironmentCacheStore | R, + EnvironmentRegistry.EnvironmentRegistry | Persistence.EnvironmentCacheStore | R, E, ServerUpdateTarget, ServerSelfUpdateResult, @@ -703,7 +706,7 @@ export function createServerEnvironmentAtoms( }); return Effect.gen(function* () { - const environmentRegistry = yield* EnvironmentRegistry; + const environmentRegistry = yield* EnvironmentRegistry.EnvironmentRegistry; const desktopCommitStarting = yield* Deferred.make(); const desktopReconnectObserverArmed = yield* Deferred.make(); const desktopReconnected = yield* Deferred.make(); diff --git a/packages/client-runtime/src/state/serverUsage.test.ts b/packages/client-runtime/src/state/serverUsage.test.ts index 2df10f7cf860..53111e8e21e1 100644 --- a/packages/client-runtime/src/state/serverUsage.test.ts +++ b/packages/client-runtime/src/state/serverUsage.test.ts @@ -26,9 +26,9 @@ import { type PreparedConnection, type SupervisorConnectionState, } from "../connection/model.ts"; -import { EnvironmentRegistry } from "../connection/registry.ts"; -import { EnvironmentSupervisor } from "../connection/supervisor.ts"; -import { EnvironmentCacheStore } from "../platform/persistence.ts"; +import * as EnvironmentRegistry from "../connection/registry.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; +import * as Persistence from "../platform/persistence.ts"; import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; import type { RpcSession } from "../rpc/session.ts"; import { createServerEnvironmentAtoms } from "./server.ts"; @@ -106,7 +106,7 @@ const makeHarness = Effect.fn("ServerUsageTest.makeHarness")(function* ( probe: Effect.void, closed: Effect.never, }; - const supervisor = EnvironmentSupervisor.of({ + const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({ target: TARGET, state: yield* SubscriptionRef.make({ ...AVAILABLE_CONNECTION_STATE, @@ -118,13 +118,13 @@ const makeHarness = Effect.fn("ServerUsageTest.makeHarness")(function* ( disconnect: Effect.void, retryNow: Effect.void, }); - const environments = EnvironmentRegistry.of({ + const environments = EnvironmentRegistry.EnvironmentRegistry.of({ run: (_environmentId, effect) => - Effect.provideService(effect, EnvironmentSupervisor, supervisor), + Effect.provideService(effect, EnvironmentSupervisor.EnvironmentSupervisor, supervisor), followStream: (_environmentId, stream) => - Stream.provideService(stream, EnvironmentSupervisor, supervisor), - } as EnvironmentRegistry["Service"]); - const cache = EnvironmentCacheStore.of({ + Stream.provideService(stream, EnvironmentSupervisor.EnvironmentSupervisor, supervisor), + } as EnvironmentRegistry.EnvironmentRegistry["Service"]); + const cache = Persistence.EnvironmentCacheStore.of({ loadShell: () => Effect.succeedNone, saveShell: () => Effect.void, loadThread: () => Effect.succeedNone, @@ -140,8 +140,8 @@ const makeHarness = Effect.fn("ServerUsageTest.makeHarness")(function* ( }); const runtime = Atom.runtime( Layer.merge( - Layer.succeed(EnvironmentRegistry, environments), - Layer.succeed(EnvironmentCacheStore, cache), + Layer.succeed(EnvironmentRegistry.EnvironmentRegistry, environments), + Layer.succeed(Persistence.EnvironmentCacheStore, cache), ), ); const initialConfigValueAtom = Atom.make(CONFIG); diff --git a/packages/client-runtime/src/state/session.ts b/packages/client-runtime/src/state/session.ts index 15848d95d965..ab8036afacbe 100644 --- a/packages/client-runtime/src/state/session.ts +++ b/packages/client-runtime/src/state/session.ts @@ -6,12 +6,12 @@ import * as SubscriptionRef from "effect/SubscriptionRef"; import type { HttpClient } from "effect/unstable/http"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; -import { RemoteEnvironmentAuthorization } from "../authorization/service.ts"; -import { EnvironmentRegistry } from "../connection/registry.ts"; +import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; +import * as EnvironmentRegistry from "../connection/registry.ts"; import type { PreparedConnection } from "../connection/model.ts"; -import { EnvironmentSupervisor } from "../connection/supervisor.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; import { environmentEndpointUrl } from "../environment/endpoint.ts"; -import { ManagedRelayDpopSigner } from "../relay/managedRelay.ts"; +import * as ManagedRelay from "../relay/managedRelay.ts"; import { safeErrorLogAttributes } from "../errors/safeLog.ts"; import { executeAuthenticatedEnvironmentHttpRequest } from "./environmentHttpAuth.ts"; import { followStreamInEnvironment } from "./runtime.ts"; @@ -43,8 +43,10 @@ export const fetchEnvironmentSessionState = Effect.fn( "clientRuntime.state.fetchEnvironmentSessionState", )(function* (input: { readonly prepared: PreparedConnection; - readonly signer: Option.Option; - readonly remoteAuthorization?: Option.Option; + readonly signer: Option.Option; + readonly remoteAuthorization?: Option.Option< + RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization["Service"] + >; readonly timeoutMs?: number; }) { return yield* executeAuthenticatedEnvironmentHttpRequest({ @@ -60,14 +62,14 @@ export const fetchEnvironmentSessionState = Effect.fn( }); export function createEnvironmentSessionAtoms( - runtime: Atom.AtomRuntime, + runtime: Atom.AtomRuntime, ) { const initialConfigAtom = Atom.family((environmentId: EnvironmentId) => runtime.atom( followStreamInEnvironment( environmentId, Stream.unwrap( - EnvironmentSupervisor.pipe( + EnvironmentSupervisor.EnvironmentSupervisor.pipe( Effect.map((supervisor) => SubscriptionRef.changes(supervisor.session).pipe( Stream.mapEffect( @@ -103,7 +105,7 @@ export function createEnvironmentSessionAtoms( followStreamInEnvironment( environmentId, Stream.unwrap( - EnvironmentSupervisor.pipe( + EnvironmentSupervisor.EnvironmentSupervisor.pipe( Effect.map((supervisor) => SubscriptionRef.changes(supervisor.prepared)), ), ), @@ -131,8 +133,10 @@ export function createEnvironmentSessionAtoms( return Effect.never; } return Effect.gen(function* () { - const signer = yield* Effect.serviceOption(ManagedRelayDpopSigner); - const remoteAuthorization = yield* Effect.serviceOption(RemoteEnvironmentAuthorization); + const signer = yield* Effect.serviceOption(ManagedRelay.ManagedRelayDpopSigner); + const remoteAuthorization = yield* Effect.serviceOption( + RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization, + ); return yield* fetchEnvironmentSessionState({ prepared, signer, remoteAuthorization }); }); }) diff --git a/packages/client-runtime/src/state/shell-sync.test.ts b/packages/client-runtime/src/state/shell-sync.test.ts index 7103c3b7f33a..69d728316498 100644 --- a/packages/client-runtime/src/state/shell-sync.test.ts +++ b/packages/client-runtime/src/state/shell-sync.test.ts @@ -27,7 +27,8 @@ import * as ConnectionWakeups from "../connection/wakeups.ts"; import * as Persistence from "../platform/persistence.ts"; import * as RpcSession from "../rpc/session.ts"; import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; -import { makeEnvironmentShellState, ShellSnapshotLoader } from "./shell.ts"; +import { makeEnvironmentShellState } from "./shell.ts"; +import * as ShellSnapshotLoader from "./shellSnapshotHttp.ts"; import { v2Project, v2ShellSnapshot } from "./orchestrationV2TestFixtures.ts"; const TARGET = new PrimaryConnectionTarget({ @@ -98,13 +99,13 @@ describe("environment shell synchronization", () => { }); // Cold cache with no HTTP snapshot available → falls back to the // socket-embedded snapshot. - const snapshotLoader = ShellSnapshotLoader.of({ + const snapshotLoader = ShellSnapshotLoader.ShellSnapshotLoader.of({ load: () => Effect.succeedNone, }); const shellState = yield* makeEnvironmentShellState().pipe( Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), Effect.provideService(Persistence.EnvironmentCacheStore, cache), - Effect.provideService(ShellSnapshotLoader, snapshotLoader), + Effect.provideService(ShellSnapshotLoader.ShellSnapshotLoader, snapshotLoader), ); yield* SubscriptionRef.set(supervisorState, { @@ -197,8 +198,8 @@ describe("environment shell synchronization", () => { Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), Effect.provideService(Persistence.EnvironmentCacheStore, cache), Effect.provideService( - ShellSnapshotLoader, - ShellSnapshotLoader.of({ load: () => Effect.succeed(Option.none()) }), + ShellSnapshotLoader.ShellSnapshotLoader, + ShellSnapshotLoader.ShellSnapshotLoader.of({ load: () => Effect.succeed(Option.none()) }), ), ); yield* SubscriptionRef.set(supervisorState, { @@ -298,7 +299,7 @@ describe("environment shell synchronization", () => { clearVcsRefs: () => Effect.void, clear: () => Effect.void, }); - const snapshotLoader = ShellSnapshotLoader.of({ + const snapshotLoader = ShellSnapshotLoader.ShellSnapshotLoader.of({ load: () => SubscriptionRef.update(loaderCalls, (count) => count + 1).pipe( Effect.as(Option.some(httpSnapshot)), @@ -307,7 +308,7 @@ describe("environment shell synchronization", () => { const shellState = yield* makeEnvironmentShellState().pipe( Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), Effect.provideService(Persistence.EnvironmentCacheStore, cache), - Effect.provideService(ShellSnapshotLoader, snapshotLoader), + Effect.provideService(ShellSnapshotLoader.ShellSnapshotLoader, snapshotLoader), ); // Wait until the subscription is established from the warm cache. @@ -375,7 +376,7 @@ describe("environment shell synchronization", () => { clearVcsRefs: () => Effect.void, clear: () => Effect.void, }); - const snapshotLoader = ShellSnapshotLoader.of({ + const snapshotLoader = ShellSnapshotLoader.ShellSnapshotLoader.of({ load: () => Ref.updateAndGet(loaderCalls, (count) => count + 1).pipe( Effect.map((count) => @@ -386,7 +387,7 @@ describe("environment shell synchronization", () => { const shellState = yield* makeEnvironmentShellState().pipe( Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), Effect.provideService(Persistence.EnvironmentCacheStore, cache), - Effect.provideService(ShellSnapshotLoader, snapshotLoader), + Effect.provideService(ShellSnapshotLoader.ShellSnapshotLoader, snapshotLoader), Effect.provideService( ConnectionWakeups.ConnectionWakeups, ConnectionWakeups.ConnectionWakeups.of({ changes: Stream.fromQueue(wakeups) }), @@ -485,14 +486,14 @@ describe("environment shell synchronization", () => { clearVcsRefs: () => Effect.void, clear: () => Effect.void, }); - const snapshotLoader = ShellSnapshotLoader.of({ + const snapshotLoader = ShellSnapshotLoader.ShellSnapshotLoader.of({ load: () => Effect.succeed(Option.none()), }); const shellState = yield* makeEnvironmentShellState().pipe( Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), Effect.provideService(Persistence.EnvironmentCacheStore, cache), - Effect.provideService(ShellSnapshotLoader, snapshotLoader), + Effect.provideService(ShellSnapshotLoader.ShellSnapshotLoader, snapshotLoader), ); const sendSnapshot = Effect.fn(function* (sequence: number) { @@ -574,14 +575,14 @@ describe("environment shell synchronization", () => { clearVcsRefs: () => Effect.void, clear: () => Effect.void, }); - const snapshotLoader = ShellSnapshotLoader.of({ + const snapshotLoader = ShellSnapshotLoader.ShellSnapshotLoader.of({ load: () => Effect.succeed(Option.none()), }); const shellState = yield* makeEnvironmentShellState().pipe( Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), Effect.provideService(Persistence.EnvironmentCacheStore, cache), - Effect.provideService(ShellSnapshotLoader, snapshotLoader), + Effect.provideService(ShellSnapshotLoader.ShellSnapshotLoader, snapshotLoader), ); yield* SubscriptionRef.set(supervisorState, { @@ -716,14 +717,14 @@ describe("environment shell synchronization", () => { clearVcsRefs: () => Effect.void, clear: () => Effect.void, }); - const snapshotLoader = ShellSnapshotLoader.of({ + const snapshotLoader = ShellSnapshotLoader.ShellSnapshotLoader.of({ load: () => Effect.succeed(Option.none()), }); const shellState = yield* makeEnvironmentShellState().pipe( Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), Effect.provideService(Persistence.EnvironmentCacheStore, cache), - Effect.provideService(ShellSnapshotLoader, snapshotLoader), + Effect.provideService(ShellSnapshotLoader.ShellSnapshotLoader, snapshotLoader), ); yield* SubscriptionRef.set(supervisorState, { @@ -836,7 +837,7 @@ describe("environment shell synchronization", () => { clearVcsRefs: () => Effect.void, clear: () => Effect.void, }); - const snapshotLoader = ShellSnapshotLoader.of({ + const snapshotLoader = ShellSnapshotLoader.ShellSnapshotLoader.of({ load: () => Effect.succeed(Option.none()), }); @@ -844,7 +845,7 @@ describe("environment shell synchronization", () => { const shellState = yield* makeEnvironmentShellState().pipe( Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), Effect.provideService(Persistence.EnvironmentCacheStore, cache), - Effect.provideService(ShellSnapshotLoader, snapshotLoader), + Effect.provideService(ShellSnapshotLoader.ShellSnapshotLoader, snapshotLoader), Scope.provide(scope), ); @@ -931,13 +932,13 @@ describe("environment shell synchronization", () => { clearVcsRefs: () => Effect.void, clear: () => Effect.void, }); - const snapshotLoader = ShellSnapshotLoader.of({ + const snapshotLoader = ShellSnapshotLoader.ShellSnapshotLoader.of({ load: () => Effect.succeed(Option.some(httpSnapshot)), }); const shellState = yield* makeEnvironmentShellState().pipe( Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), Effect.provideService(Persistence.EnvironmentCacheStore, cache), - Effect.provideService(ShellSnapshotLoader, snapshotLoader), + Effect.provideService(ShellSnapshotLoader.ShellSnapshotLoader, snapshotLoader), ); yield* SubscriptionRef.changes(shellState).pipe( diff --git a/packages/client-runtime/src/state/shell.ts b/packages/client-runtime/src/state/shell.ts index e21a36226ce4..698d4c2f4d49 100644 --- a/packages/client-runtime/src/state/shell.ts +++ b/packages/client-runtime/src/state/shell.ts @@ -15,16 +15,16 @@ import * as Stream from "effect/Stream"; import * as SubscriptionRef from "effect/SubscriptionRef"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; -import { EnvironmentRegistry } from "../connection/registry.ts"; +import * as EnvironmentRegistry from "../connection/registry.ts"; import { connectionProjectionPhase } from "../connection/model.ts"; -import { EnvironmentSupervisor } from "../connection/supervisor.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; import * as ConnectionWakeups from "../connection/wakeups.ts"; import { safeErrorLogAttributes } from "../errors/safeLog.ts"; -import { EnvironmentCacheStore } from "../platform/persistence.ts"; +import * as Persistence from "../platform/persistence.ts"; import { runCachePersistence } from "./cachePersistence.ts"; import { subscribeDynamic } from "../rpc/client.ts"; import type { RpcSession } from "../rpc/session.ts"; -import { ShellSnapshotLoader } from "./shellSnapshotHttp.ts"; +import * as ShellSnapshotLoader from "./shellSnapshotHttp.ts"; import { applyShellStreamEvent, mergeShellSnapshotProjects } from "./shellReducer.ts"; import { type EnvironmentCatalogState, enabledEnvironmentIds } from "./connections.ts"; import { followStreamInEnvironment } from "./runtime.ts"; @@ -52,9 +52,9 @@ function shellStatusForSnapshot( const SHELL_SYNCHRONIZATION_ERROR_MESSAGE = "Could not synchronize environment data."; export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make")(function* () { - const supervisor = yield* EnvironmentSupervisor; - const cache = yield* EnvironmentCacheStore; - const snapshotLoader = yield* ShellSnapshotLoader; + const supervisor = yield* EnvironmentSupervisor.EnvironmentSupervisor; + const cache = yield* Persistence.EnvironmentCacheStore; + const snapshotLoader = yield* ShellSnapshotLoader.ShellSnapshotLoader; const wakeups = yield* Effect.serviceOption(ConnectionWakeups.ConnectionWakeups); const environmentId = supervisor.target.environmentId; const cachedSnapshot = yield* cache.loadShell(environmentId).pipe( @@ -421,7 +421,10 @@ export function createEnvironmentServerConfigsAtom(input: { export function createEnvironmentShellAtoms( runtime: Atom.AtomRuntime< - EnvironmentRegistry | EnvironmentCacheStore | ShellSnapshotLoader | R, + | EnvironmentRegistry.EnvironmentRegistry + | Persistence.EnvironmentCacheStore + | ShellSnapshotLoader.ShellSnapshotLoader + | R, E >, ) { @@ -446,5 +449,5 @@ export function createEnvironmentShellAtoms( export * from "./models.ts"; export * from "./shellCommands.ts"; export * from "./shellReducer.ts"; -export * from "./shellSnapshotHttp.ts"; +export * as ShellSnapshotLoader from "./shellSnapshotHttp.ts"; export * from "./snapshots.ts"; diff --git a/packages/client-runtime/src/state/shellSnapshotHttp.ts b/packages/client-runtime/src/state/shellSnapshotHttp.ts index f5945e503c11..fb98c871ecb2 100644 --- a/packages/client-runtime/src/state/shellSnapshotHttp.ts +++ b/packages/client-runtime/src/state/shellSnapshotHttp.ts @@ -6,10 +6,10 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import { HttpClient } from "effect/unstable/http"; -import { RemoteEnvironmentAuthorization } from "../authorization/service.ts"; +import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; import type { PreparedConnection } from "../connection/model.ts"; import { environmentEndpointUrl } from "../environment/endpoint.ts"; -import { ManagedRelayDpopSigner } from "../relay/managedRelay.ts"; +import * as ManagedRelay from "../relay/managedRelay.ts"; import { executeAuthenticatedEnvironmentHttpRequest, withOrchestrationProtocolHeader, @@ -33,8 +33,10 @@ export const fetchEnvironmentShellSnapshot = Effect.fn( "clientRuntime.state.fetchEnvironmentShellSnapshot", )(function* (input: { readonly prepared: PreparedConnection; - readonly signer: Option.Option; - readonly remoteAuthorization?: Option.Option; + readonly signer: Option.Option; + readonly remoteAuthorization?: Option.Option< + RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization["Service"] + >; readonly timeoutMs?: number; }) { return yield* executeAuthenticatedEnvironmentHttpRequest({ @@ -63,18 +65,16 @@ export class ShellSnapshotLoader extends Context.Service< } >()("@t3tools/client-runtime/state/shellSnapshotHttp/ShellSnapshotLoader") {} -export const shellSnapshotLoaderLayer: Layer.Layer< - ShellSnapshotLoader, - never, - HttpClient.HttpClient -> = Layer.effect( +export const layer: Layer.Layer = Layer.effect( ShellSnapshotLoader, Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; // Resolve the DPoP signer optionally: it is only needed for relay/DPoP // connections, so the loader must not hard-require it. - const signer = yield* Effect.serviceOption(ManagedRelayDpopSigner); - const remoteAuthorization = yield* Effect.serviceOption(RemoteEnvironmentAuthorization); + const signer = yield* Effect.serviceOption(ManagedRelay.ManagedRelayDpopSigner); + const remoteAuthorization = yield* Effect.serviceOption( + RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization, + ); return ShellSnapshotLoader.of({ load: (prepared: PreparedConnection) => fetchEnvironmentShellSnapshot({ prepared, signer, remoteAuthorization }).pipe( diff --git a/packages/client-runtime/src/state/sourceControl.ts b/packages/client-runtime/src/state/sourceControl.ts index 39c7a9544549..1e6b75b53440 100644 --- a/packages/client-runtime/src/state/sourceControl.ts +++ b/packages/client-runtime/src/state/sourceControl.ts @@ -8,12 +8,12 @@ import { createEnvironmentRpcSubscriptionAtomFamily, } from "./runtime.ts"; import type { EnvironmentRegistry } from "../connection/registry.ts"; -import { EnvironmentCacheStore } from "../platform/persistence.ts"; +import * as Persistence from "../platform/persistence.ts"; import { vcsCommandConcurrency, vcsCommandScheduler } from "./vcsCommandScheduler.ts"; import { invalidateCachedVcsRefs } from "./vcsRefInvalidation.ts"; export function createSourceControlEnvironmentAtoms( - runtime: Atom.AtomRuntime, + runtime: Atom.AtomRuntime, ) { const commandScheduler = createAtomCommandScheduler(); return { diff --git a/packages/client-runtime/src/state/threadCommands.test.ts b/packages/client-runtime/src/state/threadCommands.test.ts index 365d120c7f8c..14ff5efbeabe 100644 --- a/packages/client-runtime/src/state/threadCommands.test.ts +++ b/packages/client-runtime/src/state/threadCommands.test.ts @@ -20,8 +20,8 @@ import * as Queue from "effect/Queue"; import * as SubscriptionRef from "effect/SubscriptionRef"; import { Atom, AtomRegistry } from "effect/unstable/reactivity"; -import { EnvironmentRegistry } from "../connection/registry.ts"; -import { EnvironmentSupervisor } from "../connection/supervisor.ts"; +import * as EnvironmentRegistry from "../connection/registry.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; import type { RpcSession } from "../rpc/session.ts"; import { createThreadEnvironmentAtoms } from "./threadCommands.ts"; @@ -85,7 +85,7 @@ const makeHarness = Effect.fn("TestThreadCommands.makeHarness")(function* () { command: OrchestrationV2Command; reply: Deferred.Deferred<{ sequence: number }, Error>; }>(); - const supervisor = EnvironmentSupervisor.of({ + const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({ target: { environmentId: ENVIRONMENT_ID }, session: yield* SubscriptionRef.make( Option.some({ @@ -99,13 +99,13 @@ const makeHarness = Effect.fn("TestThreadCommands.makeHarness")(function* () { }, } as unknown as RpcSession), ), - } as EnvironmentSupervisor["Service"]); + } as EnvironmentSupervisor.EnvironmentSupervisor["Service"]); const runtime = Atom.runtime( Layer.mergeAll( - Layer.succeed(EnvironmentRegistry, { + Layer.succeed(EnvironmentRegistry.EnvironmentRegistry, { run: (_environmentId, effect) => - Effect.provideService(effect, EnvironmentSupervisor, supervisor), - } as EnvironmentRegistry["Service"]), + Effect.provideService(effect, EnvironmentSupervisor.EnvironmentSupervisor, supervisor), + } as EnvironmentRegistry.EnvironmentRegistry["Service"]), Layer.succeed( Crypto.Crypto, Crypto.make({ diff --git a/packages/client-runtime/src/state/threadCommands.ts b/packages/client-runtime/src/state/threadCommands.ts index ae9fb37cbbc6..685ccc454a08 100644 --- a/packages/client-runtime/src/state/threadCommands.ts +++ b/packages/client-runtime/src/state/threadCommands.ts @@ -88,11 +88,8 @@ import { visitThread, } from "../operations/commands.ts"; import type { EnvironmentRegistry } from "../connection/registry.ts"; -import { EnvironmentSupervisor } from "../connection/supervisor.ts"; -import { - ThreadHistoryController, - type ThreadHistoryLoadEarlierResult, -} from "./threadHistoryController.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; +import * as ThreadHistoryController from "./threadHistoryController.ts"; export type LoadEarlierThreadHistoryInput = { readonly threadId: ThreadId; @@ -361,10 +358,14 @@ export function createThreadEnvironmentAtoms( label: "environment-data:commands:thread:load-earlier-history", execute: (input: LoadEarlierThreadHistoryInput) => Effect.gen(function* () { - const supervisor = yield* EnvironmentSupervisor; - const controller = yield* Effect.serviceOption(ThreadHistoryController); + const supervisor = yield* EnvironmentSupervisor.EnvironmentSupervisor; + const controller = yield* Effect.serviceOption( + ThreadHistoryController.ThreadHistoryController, + ); if (Option.isNone(controller)) { - return { _tag: "noop" } satisfies ThreadHistoryLoadEarlierResult; + return { + _tag: "noop", + } satisfies ThreadHistoryController.ThreadHistoryLoadEarlierResult; } return yield* controller.value.loadEarlier( supervisor.target.environmentId, diff --git a/packages/client-runtime/src/state/threadHistoryController.test.ts b/packages/client-runtime/src/state/threadHistoryController.test.ts index 642dc25e8d10..f449f1120a23 100644 --- a/packages/client-runtime/src/state/threadHistoryController.test.ts +++ b/packages/client-runtime/src/state/threadHistoryController.test.ts @@ -3,19 +3,14 @@ import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -import { - ThreadHistoryController, - threadHistoryControllerLayer, - type ThreadHistoryHandler, - type ThreadHistoryLoadEarlierResult, -} from "./threadHistoryController.ts"; +import * as ThreadHistoryController from "./threadHistoryController.ts"; const ENV = "env-history" as EnvironmentId; const THREAD = "thread-history" as ThreadId; function handler( tag: string, -): ThreadHistoryHandler & { readonly tag: string; readonly calls: number } { +): ThreadHistoryController.ThreadHistoryHandler & { readonly tag: string; readonly calls: number } { const state = { tag, calls: 0 }; return { get tag() { @@ -26,7 +21,9 @@ function handler( }, loadEarlier: () => { state.calls += 1; - return Effect.succeed({ _tag: "loaded" } satisfies ThreadHistoryLoadEarlierResult); + return Effect.succeed({ + _tag: "loaded", + } satisfies ThreadHistoryController.ThreadHistoryLoadEarlierResult); }, }; } @@ -34,7 +31,7 @@ function handler( describe("ThreadHistoryController", () => { it.effect("does not let an older finalizer delete a newer registration", () => Effect.gen(function* () { - const controller = yield* ThreadHistoryController; + const controller = yield* ThreadHistoryController.ThreadHistoryController; const older = handler("older"); const newer = handler("newer"); @@ -51,16 +48,16 @@ describe("ThreadHistoryController", () => { yield* controller.unregister(newerRegistration); expect(yield* controller.loadEarlier(ENV, THREAD)).toEqual({ _tag: "noop" }); - }).pipe(Effect.provide(threadHistoryControllerLayer)), + }).pipe(Effect.provide(ThreadHistoryController.layer)), ); it.effect("unregister removes only its own matching registration", () => Effect.gen(function* () { - const controller = yield* ThreadHistoryController; + const controller = yield* ThreadHistoryController.ThreadHistoryController; const first = handler("first"); const registration = yield* controller.register(ENV, THREAD, first); yield* controller.unregister(registration); expect(yield* controller.loadEarlier(ENV, THREAD)).toEqual({ _tag: "noop" }); - }).pipe(Effect.provide(Layer.fresh(threadHistoryControllerLayer))), + }).pipe(Effect.provide(Layer.fresh(ThreadHistoryController.layer))), ); }); diff --git a/packages/client-runtime/src/state/threadHistoryController.ts b/packages/client-runtime/src/state/threadHistoryController.ts index 4fde44b108bf..81cd93b1b54e 100644 --- a/packages/client-runtime/src/state/threadHistoryController.ts +++ b/packages/client-runtime/src/state/threadHistoryController.ts @@ -46,7 +46,7 @@ export class ThreadHistoryController extends Context.Service< } >()("@t3tools/client-runtime/state/threadHistoryController") {} -export const threadHistoryControllerLayer: Layer.Layer = Layer.effect( +export const layer: Layer.Layer = Layer.effect( ThreadHistoryController, Effect.gen(function* () { const handlers = yield* Ref.make(new Map()); diff --git a/packages/client-runtime/src/state/threadHistoryHttp.ts b/packages/client-runtime/src/state/threadHistoryHttp.ts index a98c4798e0cc..a2416bda26f9 100644 --- a/packages/client-runtime/src/state/threadHistoryHttp.ts +++ b/packages/client-runtime/src/state/threadHistoryHttp.ts @@ -5,7 +5,7 @@ import * as Option from "effect/Option"; import type { RemoteEnvironmentAuthorization } from "../authorization/service.ts"; import type { PreparedConnection } from "../connection/model.ts"; import { environmentEndpointUrl } from "../environment/endpoint.ts"; -import { ManagedRelayDpopSigner } from "../relay/managedRelay.ts"; +import * as ManagedRelay from "../relay/managedRelay.ts"; import { executeAuthenticatedEnvironmentHttpRequest, withOrchestrationProtocolHeader, @@ -19,7 +19,7 @@ export const fetchEnvironmentThreadHistoryPage = Effect.fn( readonly prepared: PreparedConnection; readonly threadId: ThreadId; readonly cursor: string; - readonly signer: Option.Option; + readonly signer: Option.Option; readonly remoteAuthorization?: Option.Option; readonly timeoutMs?: number; }) { diff --git a/packages/client-runtime/src/state/threadSnapshotHttp.ts b/packages/client-runtime/src/state/threadSnapshotHttp.ts index 56150235ea0a..8b31e4c60687 100644 --- a/packages/client-runtime/src/state/threadSnapshotHttp.ts +++ b/packages/client-runtime/src/state/threadSnapshotHttp.ts @@ -3,10 +3,10 @@ import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; -import { RemoteEnvironmentAuthorization } from "../authorization/service.ts"; +import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; import type { PreparedConnection } from "../connection/model.ts"; import { environmentEndpointUrl } from "../environment/endpoint.ts"; -import { ManagedRelayDpopSigner } from "../relay/managedRelay.ts"; +import * as ManagedRelay from "../relay/managedRelay.ts"; import type { RemoteEnvironmentRequestError } from "../rpc/http.ts"; import { executeAuthenticatedEnvironmentHttpRequest, @@ -55,8 +55,10 @@ export const fetchEnvironmentThreadSnapshot = Effect.fn( )(function* (input: { readonly prepared: PreparedConnection; readonly threadId: ThreadId; - readonly signer: Option.Option; - readonly remoteAuthorization?: Option.Option; + readonly signer: Option.Option; + readonly remoteAuthorization?: Option.Option< + RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization["Service"] + >; readonly timeoutMs?: number; }) { return yield* executeAuthenticatedEnvironmentHttpRequest({ diff --git a/packages/client-runtime/src/state/threads-atoms.test.ts b/packages/client-runtime/src/state/threads-atoms.test.ts index 584b2665101f..074c21f5fab1 100644 --- a/packages/client-runtime/src/state/threads-atoms.test.ts +++ b/packages/client-runtime/src/state/threads-atoms.test.ts @@ -22,29 +22,23 @@ import * as SubscriptionRef from "effect/SubscriptionRef"; import { Atom, AtomRegistry } from "effect/unstable/reactivity"; import type { ConnectionCatalogEntry } from "../connection/catalog.ts"; -import { EnvironmentRegistry } from "../connection/registry.ts"; +import * as EnvironmentRegistry from "../connection/registry.ts"; import { AVAILABLE_CONNECTION_STATE, PrimaryConnectionTarget, type NetworkStatus, type PreparedConnection, } from "../connection/model.ts"; -import { EnvironmentSupervisor } from "../connection/supervisor.ts"; -import { EnvironmentCacheStore } from "../platform/persistence.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; +import * as Persistence from "../platform/persistence.ts"; import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; import type { RpcSession } from "../rpc/session.ts"; import { createEnvironmentThreadDetailAtoms } from "./threadDetail.ts"; import { THREAD_SNAPSHOT_IDLE_TTL_MS } from "./threadRetention.ts"; import { v2Projection, v2ThreadId } from "./orchestrationV2TestFixtures.ts"; -import { - ThreadHistoryController, - threadHistoryControllerLayer, -} from "./threadHistoryController.ts"; -import { - createEnvironmentThreadStateAtoms, - ThreadSnapshotLoader, - type EnvironmentThreadState, -} from "./threads.ts"; +import * as ThreadHistoryController from "./threadHistoryController.ts"; +import { createEnvironmentThreadStateAtoms, type EnvironmentThreadState } from "./threads.ts"; +import * as ThreadSnapshotLoader from "./threadSnapshotHttp.ts"; const TARGET = new PrimaryConnectionTarget({ environmentId: EnvironmentId.make("environment-1"), @@ -110,7 +104,7 @@ const makeHarness = Effect.fn("TestThreadAtoms.makeHarness")(function* (options? probe: Effect.void, closed: Effect.never, }; - const supervisor = EnvironmentSupervisor.of({ + const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({ target: TARGET, state: yield* SubscriptionRef.make(AVAILABLE_CONNECTION_STATE), session: yield* SubscriptionRef.make(Option.some(session)), @@ -128,7 +122,7 @@ const makeHarness = Effect.fn("TestThreadAtoms.makeHarness")(function* (options? disconnect: Effect.void, retryNow: Effect.void, }); - const environmentRegistry = EnvironmentRegistry.of({ + const environmentRegistry = EnvironmentRegistry.EnvironmentRegistry.of({ entries: yield* SubscriptionRef.make>( new Map(), ), @@ -145,15 +139,15 @@ const makeHarness = Effect.fn("TestThreadAtoms.makeHarness")(function* (options? state: () => SubscriptionRef.get(supervisor.state), stateChanges: () => SubscriptionRef.changes(supervisor.state), run: (_environmentId, effect) => - Effect.provideService(effect, EnvironmentSupervisor, supervisor), + Effect.provideService(effect, EnvironmentSupervisor.EnvironmentSupervisor, supervisor), runStream: (_environmentId, stream) => - Stream.provideService(stream, EnvironmentSupervisor, supervisor), + Stream.provideService(stream, EnvironmentSupervisor.EnvironmentSupervisor, supervisor), followStream: (_environmentId, stream) => - Stream.provideService(stream, EnvironmentSupervisor, supervisor), + Stream.provideService(stream, EnvironmentSupervisor.EnvironmentSupervisor, supervisor), }); - const historyController = yield* Effect.service(ThreadHistoryController).pipe( - Effect.provide(threadHistoryControllerLayer), - ); + const historyController = yield* Effect.service( + ThreadHistoryController.ThreadHistoryController, + ).pipe(Effect.provide(ThreadHistoryController.layer)); const historyHttpClient = HttpClient.make((request, url) => Effect.gen(function* () { const response = yield* Deferred.make(); @@ -166,12 +160,12 @@ const makeHarness = Effect.fn("TestThreadAtoms.makeHarness")(function* (options? ); const runtime = Atom.runtime( Layer.mergeAll( - Layer.succeed(ThreadHistoryController, historyController), + Layer.succeed(ThreadHistoryController.ThreadHistoryController, historyController), Layer.succeed(HttpClient.HttpClient, historyHttpClient), - Layer.succeed(EnvironmentRegistry, environmentRegistry), + Layer.succeed(EnvironmentRegistry.EnvironmentRegistry, environmentRegistry), Layer.succeed( - EnvironmentCacheStore, - EnvironmentCacheStore.of({ + Persistence.EnvironmentCacheStore, + Persistence.EnvironmentCacheStore.of({ loadShell: () => Effect.succeedNone, saveShell: () => Effect.void, loadThread: () => @@ -191,8 +185,8 @@ const makeHarness = Effect.fn("TestThreadAtoms.makeHarness")(function* (options? }), ), Layer.succeed( - ThreadSnapshotLoader, - ThreadSnapshotLoader.of({ + ThreadSnapshotLoader.ThreadSnapshotLoader, + ThreadSnapshotLoader.ThreadSnapshotLoader.of({ load: () => Effect.sync(() => { httpLoads += 1; diff --git a/packages/client-runtime/src/state/threads-sync.test.ts b/packages/client-runtime/src/state/threads-sync.test.ts index 8e6cef3339e5..0c1619a52b19 100644 --- a/packages/client-runtime/src/state/threads-sync.test.ts +++ b/packages/client-runtime/src/state/threads-sync.test.ts @@ -37,17 +37,14 @@ import * as EnvironmentSupervisor from "../connection/supervisor.ts"; import * as Persistence from "../platform/persistence.ts"; import * as RpcSession from "../rpc/session.ts"; import { v2Projection, v2ThreadId } from "./orchestrationV2TestFixtures.ts"; -import { - ThreadHistoryController, - threadHistoryControllerLayer, -} from "./threadHistoryController.ts"; +import * as ThreadHistoryController from "./threadHistoryController.ts"; import { EMPTY_ENVIRONMENT_THREAD_STATE, makeEnvironmentThreadState, - ThreadSnapshotLoader, type EnvironmentThreadState, type ThreadSnapshotLoadResult, } from "./threads.ts"; +import * as ThreadSnapshotLoader from "./threadSnapshotHttp.ts"; const TARGET = new PrimaryConnectionTarget({ environmentId: EnvironmentId.make("environment-1"), @@ -160,7 +157,7 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o const prepared = yield* SubscriptionRef.make>( Option.some(PREPARED), ); - const snapshotLoader = ThreadSnapshotLoader.of({ + const snapshotLoader = ThreadSnapshotLoader.ThreadSnapshotLoader.of({ load: (_prepared, threadId) => Ref.update(loaderCalls, (count) => count + 1).pipe( Effect.as( @@ -221,7 +218,7 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o let makeThreadState = makeEnvironmentThreadState(THREAD_ID, options?.resumeCache).pipe( Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), Effect.provideService(Persistence.EnvironmentCacheStore, cache), - Effect.provideService(ThreadSnapshotLoader, snapshotLoader), + Effect.provideService(ThreadSnapshotLoader.ThreadSnapshotLoader, snapshotLoader), Effect.provideService( ConnectionWakeups.ConnectionWakeups, ConnectionWakeups.ConnectionWakeups.of({ changes: Stream.fromQueue(wakeups) }), @@ -236,12 +233,12 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o ), ); } - const historyController = yield* ThreadHistoryController.pipe( - Effect.provide(threadHistoryControllerLayer), + const historyController = yield* ThreadHistoryController.ThreadHistoryController.pipe( + Effect.provide(ThreadHistoryController.layer), ); if (options?.historyPaging !== "no-controller") { makeThreadState = makeThreadState.pipe( - Effect.provideService(ThreadHistoryController, historyController), + Effect.provideService(ThreadHistoryController.ThreadHistoryController, historyController), ); } const threadState = yield* makeThreadState; diff --git a/packages/client-runtime/src/state/threads.ts b/packages/client-runtime/src/state/threads.ts index 5231958b346e..5ae532cbbed1 100644 --- a/packages/client-runtime/src/state/threads.ts +++ b/packages/client-runtime/src/state/threads.ts @@ -20,23 +20,20 @@ import * as SubscriptionRef from "effect/SubscriptionRef"; import { HttpClient } from "effect/unstable/http"; import { Atom } from "effect/unstable/reactivity"; -import { RemoteEnvironmentAuthorization } from "../authorization/service.ts"; -import { EnvironmentRegistry } from "../connection/registry.ts"; +import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; +import * as EnvironmentRegistry from "../connection/registry.ts"; import { connectionProjectionPhase } from "../connection/model.ts"; -import { EnvironmentSupervisor } from "../connection/supervisor.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; import * as ConnectionWakeups from "../connection/wakeups.ts"; -import { EnvironmentCacheStore } from "../platform/persistence.ts"; +import * as Persistence from "../platform/persistence.ts"; import { runCachePersistence } from "./cachePersistence.ts"; -import { ManagedRelayDpopSigner } from "../relay/managedRelay.ts"; +import * as ManagedRelay from "../relay/managedRelay.ts"; import { subscribeDynamic } from "../rpc/client.ts"; import { parseThreadKey, threadKey } from "./entities.ts"; import { applyOrchestrationV2ProjectionEvent } from "./orchestrationV2Projection.ts"; import { THREAD_SNAPSHOT_IDLE_TTL_MS } from "./threadRetention.ts"; import { followStreamInEnvironment } from "./runtime.ts"; -import { - ThreadHistoryController, - type ThreadHistoryLoadEarlierResult, -} from "./threadHistoryController.ts"; +import * as ThreadHistoryController from "./threadHistoryController.ts"; import { fetchEnvironmentThreadHistoryPage } from "./threadHistoryHttp.ts"; import { applyHistoryPageMeta, @@ -46,7 +43,7 @@ import { mergeOlderHistoryIntoProjection, type ThreadHistoryMeta, } from "./threadHistoryMerge.ts"; -import { ThreadSnapshotLoader, type ThreadSnapshotLoadResult } from "./threadSnapshotHttp.ts"; +import * as ThreadSnapshotLoader from "./threadSnapshotHttp.ts"; import { EMPTY_ENVIRONMENT_THREAD_STATE, type EnvironmentThreadState, @@ -173,13 +170,17 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make threadId: ThreadIdType, resumeCache?: ThreadResumeCache, ) { - const supervisor = yield* EnvironmentSupervisor; - const cache = yield* EnvironmentCacheStore; - const snapshotLoader = yield* ThreadSnapshotLoader; - const historyController = yield* Effect.serviceOption(ThreadHistoryController); + const supervisor = yield* EnvironmentSupervisor.EnvironmentSupervisor; + const cache = yield* Persistence.EnvironmentCacheStore; + const snapshotLoader = yield* ThreadSnapshotLoader.ThreadSnapshotLoader; + const historyController = yield* Effect.serviceOption( + ThreadHistoryController.ThreadHistoryController, + ); const httpClient = yield* Effect.serviceOption(HttpClient.HttpClient); - const dpopSigner = yield* Effect.serviceOption(ManagedRelayDpopSigner); - const remoteAuthorization = yield* Effect.serviceOption(RemoteEnvironmentAuthorization); + const dpopSigner = yield* Effect.serviceOption(ManagedRelay.ManagedRelayDpopSigner); + const remoteAuthorization = yield* Effect.serviceOption( + RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization, + ); const wakeups = yield* Effect.serviceOption(ConnectionWakeups.ConnectionWakeups); const environmentId = supervisor.target.environmentId; const retained = resumeCache?.snapshot; @@ -640,10 +641,10 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make !current.history.hasMoreHistory || current.history.historyCursor === null ) { - return { _tag: "noop" } satisfies ThreadHistoryLoadEarlierResult; + return { _tag: "noop" } satisfies ThreadHistoryController.ThreadHistoryLoadEarlierResult; } if (current.history.loading) { - return { _tag: "busy" } satisfies ThreadHistoryLoadEarlierResult; + return { _tag: "busy" } satisfies ThreadHistoryController.ThreadHistoryLoadEarlierResult; } // Capture the cursor that initiated this request. Completions/failures must @@ -675,12 +676,12 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make }, ); if (!stillCurrent) { - return { _tag: "noop" } satisfies ThreadHistoryLoadEarlierResult; + return { _tag: "noop" } satisfies ThreadHistoryController.ThreadHistoryLoadEarlierResult; } return { _tag: "error", message, - } satisfies ThreadHistoryLoadEarlierResult; + } satisfies ThreadHistoryController.ThreadHistoryLoadEarlierResult; } const pageResult = yield* fetchEnvironmentThreadHistoryPage({ @@ -711,9 +712,12 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make }, ); if (!stillCurrent) { - return { _tag: "noop" } satisfies ThreadHistoryLoadEarlierResult; + return { _tag: "noop" } satisfies ThreadHistoryController.ThreadHistoryLoadEarlierResult; } - return { _tag: "error", message } satisfies ThreadHistoryLoadEarlierResult; + return { + _tag: "error", + message, + } satisfies ThreadHistoryController.ThreadHistoryLoadEarlierResult; } const page = pageResult.success; @@ -723,7 +727,12 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make return yield* applyLock.withPermits(1)( SubscriptionRef.modify( state, - (latest): readonly [ThreadHistoryLoadEarlierResult, EnvironmentThreadState] => { + ( + latest, + ): readonly [ + ThreadHistoryController.ThreadHistoryLoadEarlierResult, + EnvironmentThreadState, + ] => { // Stale page: socket/full snapshot or newer bounded install changed the // progressive cursor while this request was in flight. Never mutate the // replacement meta (including deleted/empty installs). @@ -851,10 +860,8 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make }), ), ); - const httpResult: ThreadSnapshotLoadResult = yield* snapshotLoader.load( - prepared, - threadId, - ); + const httpResult: ThreadSnapshotLoader.ThreadSnapshotLoadResult = + yield* snapshotLoader.load(prepared, threadId); switch (httpResult._tag) { case "present": { if (canLoadHistory && httpResult.history !== undefined) { @@ -942,7 +949,10 @@ function threadStateChanges( export function createEnvironmentThreadStateAtoms( runtime: Atom.AtomRuntime< - EnvironmentRegistry | EnvironmentCacheStore | ThreadSnapshotLoader | R, + | EnvironmentRegistry.EnvironmentRegistry + | Persistence.EnvironmentCacheStore + | ThreadSnapshotLoader.ThreadSnapshotLoader + | R, E >, ) { @@ -986,7 +996,9 @@ export function createEnvironmentThreadStateAtoms( export * from "./archivedThreads.ts"; export * from "./checkpointDiff.ts"; export * from "./boundedThreadSnapshotHttp.ts"; -export * from "./threadHistoryController.ts"; +export * as ThreadHistoryController from "./threadHistoryController.ts"; +// Flat so consumers' inferred types can name it. +export type { ThreadHistoryLoadEarlierResult } from "./threadHistoryController.ts"; export * from "./threadHistoryMerge.ts"; export * from "./threadSnapshotHttp.ts"; export * from "./composerPathSearch.ts"; diff --git a/packages/client-runtime/src/state/vcs.ts b/packages/client-runtime/src/state/vcs.ts index bedcb751216f..c5dee0f75280 100644 --- a/packages/client-runtime/src/state/vcs.ts +++ b/packages/client-runtime/src/state/vcs.ts @@ -21,9 +21,9 @@ import { createEnvironmentSubscriptionAtomFamily, } from "./runtime.ts"; import type { EnvironmentRegistry } from "../connection/registry.ts"; -import { EnvironmentSupervisor } from "../connection/supervisor.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; import { safeErrorLogAttributes } from "../errors/safeLog.ts"; -import { EnvironmentCacheStore } from "../platform/persistence.ts"; +import * as Persistence from "../platform/persistence.ts"; import { request, subscribe, type EnvironmentRpcInput } from "../rpc/client.ts"; import { followStreamInEnvironment } from "./runtime.ts"; import { vcsCommandConcurrency, vcsCommandScheduler } from "./vcsCommandScheduler.ts"; @@ -56,7 +56,7 @@ function canUseVcsRefsCache(input: VcsListRefsInput): boolean { export const commitVcsRefsRefresh = Effect.fn("CachedVcsRefsState.commitRefresh")(function* ( registry: AtomRegistry.AtomRegistry, - cache: EnvironmentCacheStore["Service"], + cache: Persistence.EnvironmentCacheStore["Service"], input: { readonly environmentId: EnvironmentId; readonly cwd: string; @@ -126,8 +126,8 @@ export const makeCachedVcsRefsChanges = Effect.fn("CachedVcsRefsState.makeChange registry?: AtomRegistry.AtomRegistry, persistedCacheReadable = true, ) { - const supervisor = yield* EnvironmentSupervisor; - const cache = yield* EnvironmentCacheStore; + const supervisor = yield* EnvironmentSupervisor.EnvironmentSupervisor; + const cache = yield* Persistence.EnvironmentCacheStore; const environmentId = supervisor.target.environmentId; const useCache = canUseVcsRefsCache(input); const cached = @@ -147,7 +147,7 @@ export const makeCachedVcsRefsChanges = Effect.fn("CachedVcsRefsState.makeChange : Option.none(); const refresh = Effect.fn("CachedVcsRefsState.refresh")(function* () { const refs = yield* request(WS_METHODS.vcsListRefs, input).pipe( - Effect.provideService(EnvironmentSupervisor, supervisor), + Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), ); const persist = cache.saveVcsRefs(environmentId, input.cwd, refs).pipe( Effect.catch((error) => @@ -241,7 +241,7 @@ function cachedVcsRefsChanges( } export function createVcsEnvironmentAtoms( - runtime: Atom.AtomRuntime, + runtime: Atom.AtomRuntime, ) { /** * One flat family on purpose: families hold entries via WeakRef, so a nested diff --git a/packages/client-runtime/src/state/vcsAction.ts b/packages/client-runtime/src/state/vcsAction.ts index 2f871953cbab..1e40e8cc3ef4 100644 --- a/packages/client-runtime/src/state/vcsAction.ts +++ b/packages/client-runtime/src/state/vcsAction.ts @@ -18,7 +18,7 @@ import * as Stream from "effect/Stream"; import { AsyncResult, Atom, type AtomRegistry } from "effect/unstable/reactivity"; import type { EnvironmentRegistry } from "../connection/registry.ts"; -import { EnvironmentCacheStore } from "../platform/persistence.ts"; +import * as Persistence from "../platform/persistence.ts"; import { runStream } from "../rpc/client.ts"; import { createRuntimeCommand, @@ -413,7 +413,7 @@ export function applyVcsActionProgressEvent( } export function createVcsActionManager( - runtime: Atom.AtomRuntime, + runtime: Atom.AtomRuntime, ) { const runStackedActionCommands = new Map< string, @@ -435,7 +435,7 @@ export function createVcsActionManager( const target = targetKey === null ? null : parseVcsActionTargetKey(targetKey); const stateAtom = targetKey === null ? EMPTY_VCS_ACTION_ATOM : vcsActionStateAtom(targetKey); const command = createRuntimeCommand< - EnvironmentRegistry | EnvironmentCacheStore | R, + EnvironmentRegistry | Persistence.EnvironmentCacheStore | R, E, RunVcsStackedActionInput, GitRunStackedActionResult, diff --git a/packages/client-runtime/src/state/vcsRefInvalidation.ts b/packages/client-runtime/src/state/vcsRefInvalidation.ts index ff9de7bd1cc6..3d5c20f5662f 100644 --- a/packages/client-runtime/src/state/vcsRefInvalidation.ts +++ b/packages/client-runtime/src/state/vcsRefInvalidation.ts @@ -4,7 +4,7 @@ import * as PartitionedSemaphore from "effect/PartitionedSemaphore"; import { Atom, type AtomRegistry } from "effect/unstable/reactivity"; import { safeErrorLogAttributes } from "../errors/safeLog.ts"; -import { EnvironmentCacheStore } from "../platform/persistence.ts"; +import * as Persistence from "../platform/persistence.ts"; export interface VcsRefsInvalidationTarget { readonly environmentId: EnvironmentId; @@ -60,7 +60,7 @@ export const invalidateCachedVcsRefs = Effect.fn("VcsRefsState.invalidateCached" registry: AtomRegistry.AtomRegistry, target: CachedVcsRefsInvalidationTarget, ) { - const cache = yield* EnvironmentCacheStore; + const cache = yield* Persistence.EnvironmentCacheStore; yield* withVcsRefsPersistenceLock( target.environmentId, Effect.gen(function* () { diff --git a/packages/effect-codex-app-server/src/replay.ts b/packages/effect-codex-app-server/src/replay.ts index ebb3d007cfd0..1c98c7bc3b8a 100644 --- a/packages/effect-codex-app-server/src/replay.ts +++ b/packages/effect-codex-app-server/src/replay.ts @@ -9,7 +9,6 @@ import * as Sink from "effect/Sink"; import * as Stdio from "effect/Stdio"; import * as Stream from "effect/Stream"; -import { CodexAppServerClient } from "./client.ts"; import * as CodexClient from "./client.ts"; import * as CodexError from "./errors.ts"; @@ -364,8 +363,8 @@ function replayTransportError( export function layerReplay( transcript: CodexAppServerReplayTranscript, -): Layer.Layer { - return Layer.effect(CodexAppServerClient, makeReplayClient(transcript)); +): Layer.Layer { + return Layer.effect(CodexClient.CodexAppServerClient, makeReplayClient(transcript)); } export const makeReplayDriver = Effect.fn("effect-codex-app-server/replay.makeReplayDriver")( @@ -383,8 +382,8 @@ export const makeReplayDriver = Effect.fn("effect-codex-app-server/replay.makeRe export function layerReplayWithDriver( driver: CodexAppServerReplayDriver, -): Layer.Layer { - return Layer.effect(CodexAppServerClient, makeReplayClientWithState(driver)); +): Layer.Layer { + return Layer.effect(CodexClient.CodexAppServerClient, makeReplayClientWithState(driver)); } const makeReplayClient = Effect.fn("effect-codex-app-server/replay.makeReplayClient")(function* ( diff --git a/packages/shared/src/relayClient.test.ts b/packages/shared/src/relayClient.test.ts index 404d765ba74c..97dd90f277a7 100644 --- a/packages/shared/src/relayClient.test.ts +++ b/packages/shared/src/relayClient.test.ts @@ -12,11 +12,7 @@ import { HttpClient, HttpClientResponse } from "effect/unstable/http"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { HostProcessArchitecture, HostProcessPlatform } from "./hostProcess.ts"; -import { - RelayClientInstallError, - CLOUDFLARED_VERSION, - makeCloudflaredRelayClient, -} from "./relayClient.ts"; +import * as RelayClient from "./relayClient.ts"; // The suite runs the linux code path against the real filesystem, checking // POSIX exec bits that NTFS never reports; the win32 branch skips that check. @@ -81,7 +77,7 @@ describe("RelayClient", () => { const overridePath = `${baseDir}/override-cloudflared`; yield* fileSystem.writeFileString(overridePath, "override"); yield* fileSystem.chmod(overridePath, 0o755); - const manager = yield* makeCloudflaredRelayClient({ + const manager = yield* RelayClient.makeCloudflaredRelayClient({ baseDir, }); @@ -98,7 +94,7 @@ describe("RelayClient", () => { status: "available", executablePath: overridePath, source: "override", - version: CLOUDFLARED_VERSION, + version: RelayClient.CLOUDFLARED_VERSION, }); }).pipe( Effect.scoped, @@ -122,7 +118,7 @@ describe("RelayClient", () => { prefix: "t3-cloudflared-test-", }); const bytes = new TextEncoder().encode("test-cloudflared-binary"); - const manager = yield* makeCloudflaredRelayClient({ + const manager = yield* RelayClient.makeCloudflaredRelayClient({ baseDir, releaseAsset: { url: "https://example.test/cloudflared", @@ -139,12 +135,12 @@ describe("RelayClient", () => { } }), ); - const managedPath = `${baseDir}/tools/cloudflared/${CLOUDFLARED_VERSION}/linux-x64/cloudflared`; + const managedPath = `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/linux-x64/cloudflared`; expect(installed).toEqual({ status: "available", executablePath: managedPath, source: "managed", - version: CLOUDFLARED_VERSION, + version: RelayClient.CLOUDFLARED_VERSION, }); expect(new TextDecoder().decode(yield* fileSystem.readFile(managedPath))).toBe( "test-cloudflared-binary", @@ -178,7 +174,7 @@ describe("RelayClient", () => { const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-cloudflared-test-", }); - const manager = yield* makeCloudflaredRelayClient({ + const manager = yield* RelayClient.makeCloudflaredRelayClient({ baseDir, releaseAsset: { url: "https://example.test/cloudflared", @@ -188,7 +184,7 @@ describe("RelayClient", () => { }); const error = yield* manager.install.pipe(Effect.flip); - expect(error).toBeInstanceOf(RelayClientInstallError); + expect(error).toBeInstanceOf(RelayClient.RelayClientInstallError); expect(error.reason).toBe("invalid_checksum"); }).pipe( Effect.scoped, @@ -211,7 +207,7 @@ describe("RelayClient", () => { const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-cloudflared-test-", }); - const manager = yield* makeCloudflaredRelayClient({ + const manager = yield* RelayClient.makeCloudflaredRelayClient({ baseDir, releaseAsset: { url: "https://example.test/cloudflared", @@ -249,13 +245,13 @@ describe("RelayClient", () => { }); const binDir = `${baseDir}/bin`; const executablePath = `${binDir}/cloudflared`; - const manager = yield* makeCloudflaredRelayClient({ + const manager = yield* RelayClient.makeCloudflaredRelayClient({ baseDir, }); expect(yield* manager.resolve).toEqual({ status: "missing", - version: CLOUDFLARED_VERSION, + version: RelayClient.CLOUDFLARED_VERSION, }); yield* fileSystem.makeDirectory(binDir); @@ -267,7 +263,7 @@ describe("RelayClient", () => { status: "available", executablePath, source: "path", - version: CLOUDFLARED_VERSION, + version: RelayClient.CLOUDFLARED_VERSION, }); }).pipe( Effect.scoped, diff --git a/packages/ssh/src/tunnel.test.ts b/packages/ssh/src/tunnel.test.ts index a3c049323bce..1f093eee6d82 100644 --- a/packages/ssh/src/tunnel.test.ts +++ b/packages/ssh/src/tunnel.test.ts @@ -15,22 +15,9 @@ import { TestClock } from "effect/testing"; import { HttpClient, HttpClientResponse } from "effect/unstable/http"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; -import { SshPasswordPrompt } from "./auth.ts"; +import * as SshAuth from "./auth.ts"; import { SshCommandError } from "./errors.ts"; -import { - buildRemoteLaunchScript, - buildRemotePairingScript, - buildRemoteStopScript, - buildRemoteT3RunnerScript, - SshInvalidArchiveVersionError, - SshMissingRunnerError, - describeReadinessCause, - issueRemotePairingToken, - launchOrReuseRemoteServer, - REMOTE_PICK_PORT_SCRIPT, - SshEnvironmentManager, - waitForHttpReady, -} from "./tunnel.ts"; +import * as SshTunnel from "./tunnel.ts"; const TEST_NODE_ENGINE_RANGE = "^22.16 || ^23.11 || >=24.10"; @@ -112,7 +99,7 @@ const NODE_SCRIPT = { describe("ssh tunnel scripts", () => { it("installs and runs the release archive without Node, npm, or npx", () => { - const script = buildRemoteT3RunnerScript(ARCHIVE); + const script = SshTunnel.buildRemoteT3RunnerScript(ARCHIVE); assert.include(script, "T3_ARCHIVE_VERSION='1.2.3-preview.20260911.4'"); assert.include(script, "T3_NODE_SCRIPT_PATH=''"); @@ -162,7 +149,7 @@ describe("ssh tunnel scripts", () => { script.indexOf("T3_ARCHIVE_VERSION="), ); - const launch = buildRemoteLaunchScript({ + const launch = SshTunnel.buildRemoteLaunchScript({ ...ARCHIVE, releaseBaseUrl: "https://mirror.example/t3/", }); @@ -171,7 +158,7 @@ describe("ssh tunnel scripts", () => { assert.include(launch, '"$RUNNER_FILE" __ssh-helper pick-port "$PORT_FILE"'); assert.include(launch, '"$RUNNER_FILE" __ssh-helper wait-ready "$REMOTE_PORT"'); assert.include(launch, '"$RUNNER_FILE" __ssh-helper runtime-port "$DEFAULT_RUNTIME_FILE"'); - assert.include(buildRemoteLaunchScript(NODE_SCRIPT), "T3_ARCHIVE_MODE=0"); + assert.include(SshTunnel.buildRemoteLaunchScript(NODE_SCRIPT), "T3_ARCHIVE_MODE=0"); }); it("rejects archive versions that are not a single exact version segment", () => { @@ -184,34 +171,37 @@ describe("ssh tunnel scripts", () => { "v1.2.3", ]) { assert.throws( - () => buildRemoteT3RunnerScript({ archiveVersion }), - SshInvalidArchiveVersionError, + () => SshTunnel.buildRemoteT3RunnerScript({ archiveVersion }), + SshTunnel.SshInvalidArchiveVersionError, undefined, archiveVersion, ); } assert.include( - buildRemoteT3RunnerScript(ARCHIVE), + SshTunnel.buildRemoteT3RunnerScript(ARCHIVE), "T3_ARCHIVE_VERSION='1.2.3-preview.20260911.4'", ); }); it("refuses to build a runner with neither an archive version nor a node script", () => { for (const input of [undefined, {}, { archiveVersion: " " }, { nodeScriptPath: null }]) { - assert.throws(() => buildRemoteT3RunnerScript(input), SshMissingRunnerError); + assert.throws( + () => SshTunnel.buildRemoteT3RunnerScript(input), + SshTunnel.SshMissingRunnerError, + ); } - assert.throws(() => buildRemoteLaunchScript(), SshMissingRunnerError); + assert.throws(() => SshTunnel.buildRemoteLaunchScript(), SshTunnel.SshMissingRunnerError); }); it("does not hard-code a remote node engine range", () => { - const script = buildRemoteT3RunnerScript(NODE_SCRIPT); + const script = SshTunnel.buildRemoteT3RunnerScript(NODE_SCRIPT); assert.include(script, "T3_NODE_ENGINE_RANGE=''"); assert.notInclude(script, TEST_NODE_ENGINE_RANGE); }); it("builds the remote t3 runner with a node script override", () => { - const script = buildRemoteT3RunnerScript({ + const script = SshTunnel.buildRemoteT3RunnerScript({ ...NODE_SCRIPT, nodeEngineRange: TEST_NODE_ENGINE_RANGE, }); @@ -249,8 +239,8 @@ describe("ssh tunnel scripts", () => { username: "julius", port: 2222, } as const; - const launch = buildRemoteLaunchScript(ARCHIVE); - const devLaunch = buildRemoteLaunchScript({ + const launch = SshTunnel.buildRemoteLaunchScript(ARCHIVE); + const devLaunch = SshTunnel.buildRemoteLaunchScript({ ...NODE_SCRIPT, nodeEngineRange: TEST_NODE_ENGINE_RANGE, }); @@ -275,24 +265,30 @@ describe("ssh tunnel scripts", () => { assert.include(launch, "It wrote nothing to %s"); assert.include(launch, "T3_ARCHIVE_VERSION='1.2.3-preview.20260911.4'"); assert.include( - buildRemotePairingScript(target, ARCHIVE), + SshTunnel.buildRemotePairingScript(target, ARCHIVE), '"$RUNNER_FILE" auth pairing create --base-dir "$PAIRING_BASE_DIR" --json', ); assert.include( - buildRemotePairingScript(target, ARCHIVE), + SshTunnel.buildRemotePairingScript(target, ARCHIVE), 'PAIRING_BASE_DIR="$DEFAULT_SERVER_HOME"', ); - assert.notInclude(buildRemotePairingScript(target, ARCHIVE), "server-home"); + assert.notInclude(SshTunnel.buildRemotePairingScript(target, ARCHIVE), "server-home"); assert.include( - buildRemotePairingScript(target, ARCHIVE), + SshTunnel.buildRemotePairingScript(target, ARCHIVE), "T3_ARCHIVE_VERSION='1.2.3-preview.20260911.4'", ); assert.include( - buildRemoteStopScript(target), + SshTunnel.buildRemoteStopScript(target), 'if [ "$REMOTE_MANAGED" != "external" ] && [ -n "$REMOTE_PID" ]', ); - assert.include(buildRemoteStopScript(target), 'kill "$REMOTE_PID" 2>/dev/null || true'); - assert.include(buildRemoteStopScript(target), 'rm -f "$PID_FILE" "$PORT_FILE" "$MANAGED_FILE"'); + assert.include( + SshTunnel.buildRemoteStopScript(target), + 'kill "$REMOTE_PID" 2>/dev/null || true', + ); + assert.include( + SshTunnel.buildRemoteStopScript(target), + 'rm -f "$PID_FILE" "$PORT_FILE" "$MANAGED_FILE"', + ); assert.include( launch, 'DEFAULT_RUNTIME_FILE="$DEFAULT_SERVER_HOME/userdata/server-runtime.json"', @@ -333,7 +329,7 @@ describe("ssh tunnel scripts", () => { const processLayer = Layer.merge(NodeServices.layer, spawnerLayer); return Effect.gen(function* () { - const result = yield* launchOrReuseRemoteServer(target, undefined, ARCHIVE); + const result = yield* SshTunnel.launchOrReuseRemoteServer(target, undefined, ARCHIVE); assert.equal(result.remotePort, 3774); assert.deepEqual(spawnedCommands[0]?.slice(-5, -1), ["sh", "-l", "-s", "--"]); }).pipe(Effect.provide(processLayer)); @@ -354,7 +350,7 @@ describe("ssh tunnel scripts", () => { return Effect.gen(function* () { const fiber = yield* Effect.forkChild( - launchOrReuseRemoteServer(target, undefined, NODE_SCRIPT), + SshTunnel.launchOrReuseRemoteServer(target, undefined, NODE_SCRIPT), ); yield* Effect.yieldNow; yield* TestClock.adjust(Duration.seconds(75)); @@ -378,7 +374,9 @@ describe("ssh tunnel scripts", () => { const processLayer = Layer.mergeAll(NodeServices.layer, spawnerLayer, TestClock.layer()); return Effect.gen(function* () { - const fiber = yield* Effect.forkChild(launchOrReuseRemoteServer(target, undefined, ARCHIVE)); + const fiber = yield* Effect.forkChild( + SshTunnel.launchOrReuseRemoteServer(target, undefined, ARCHIVE), + ); yield* Effect.yieldNow; yield* TestClock.adjust(Duration.seconds(800)); @@ -388,14 +386,14 @@ describe("ssh tunnel scripts", () => { }); it("allows the remote port picker to run without a state file path", () => { - assert.include(REMOTE_PICK_PORT_SCRIPT, 'const filePath = process.argv[2] ?? "";'); + assert.include(SshTunnel.REMOTE_PICK_PORT_SCRIPT, 'const filePath = process.argv[2] ?? "";'); }); it.effect("bounds each HTTP readiness probe so retries cannot hang on one request", () => Effect.gen(function* () { const fiber = yield* Effect.forkChild( Effect.result( - waitForHttpReady({ + SshTunnel.waitForHttpReady({ baseUrl: "http://127.0.0.1:41773/", timeoutMs: 1_000, intervalMs: 100, @@ -421,7 +419,7 @@ describe("ssh tunnel scripts", () => { it("preserves primitive readiness reason values in diagnostic output", () => { assert.deepEqual( - describeReadinessCause({ + SshTunnel.describeReadinessCause({ _tag: "HttpClientError", message: "Backend readiness probe failed.", reason: "authentication failed", @@ -458,7 +456,7 @@ describe("ssh tunnel scripts", () => { const spawnerLayer = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner); const processLayer = Layer.merge(NodeServices.layer, spawnerLayer); return Effect.gen(function* () { - const result = yield* issueRemotePairingToken(target, undefined, ARCHIVE); + const result = yield* SshTunnel.issueRemotePairingToken(target, undefined, ARCHIVE); assert.equal(result.credential, "LCL4R2TPHDKQ"); }).pipe(Effect.provide(processLayer)); }); @@ -486,7 +484,7 @@ describe("ssh tunnel scripts", () => { const spawnerLayer = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner); const processLayer = Layer.merge(NodeServices.layer, spawnerLayer); return Effect.gen(function* () { - const result = yield* issueRemotePairingToken(target, undefined, ARCHIVE); + const result = yield* SshTunnel.issueRemotePairingToken(target, undefined, ARCHIVE); assert.equal(result.credential, "LCL4R2TPHDKQ"); }).pipe(Effect.provide(processLayer)); }); @@ -530,8 +528,8 @@ describe("ssh tunnel scripts", () => { Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner), Layer.succeed(HttpClient.HttpClient, testHttpClient), Layer.succeed(NetService.NetService, testNetService), - SshPasswordPrompt.disabledLayer, - SshEnvironmentManager.layer({ resolveCliRunner: Effect.succeed(ARCHIVE) }), + SshAuth.SshPasswordPrompt.disabledLayer, + SshTunnel.SshEnvironmentManager.layer({ resolveCliRunner: Effect.succeed(ARCHIVE) }), ); const target = { alias: "devbox", @@ -541,7 +539,7 @@ describe("ssh tunnel scripts", () => { } as const; return Effect.gen(function* () { - const manager = yield* SshEnvironmentManager; + const manager = yield* SshTunnel.SshEnvironmentManager; const first = yield* manager.ensureEnvironment(target); assert.equal(first.httpBaseUrl, "http://127.0.0.1:41773/"); @@ -655,11 +653,11 @@ describe("ssh tunnel scripts", () => { Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner), Layer.succeed(HttpClient.HttpClient, testHttpClient), Layer.succeed(NetService.NetService, testNetService), - SshPasswordPrompt.disabledLayer, - SshEnvironmentManager.layer({ resolveCliRunner: Effect.succeed(ARCHIVE) }), + SshAuth.SshPasswordPrompt.disabledLayer, + SshTunnel.SshEnvironmentManager.layer({ resolveCliRunner: Effect.succeed(ARCHIVE) }), ); yield* Effect.gen(function* () { - const manager = yield* SshEnvironmentManager; + const manager = yield* SshTunnel.SshEnvironmentManager; yield* manager.ensureEnvironment(target); const disconnect = yield* Effect.forkChild(manager.disconnectEnvironment(target)); yield* Deferred.await(shutdownStarted); @@ -768,7 +766,7 @@ describe("archive runner script", () => { const runner = `${root}/run-t3.sh`; yield* fs.writeFileString( runner, - buildRemoteT3RunnerScript({ archiveVersion, releaseBaseUrl }), + SshTunnel.buildRemoteT3RunnerScript({ archiveVersion, releaseBaseUrl }), ); const home = `${root}/home`; yield* fs.makeDirectory(home, { recursive: true }); diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 0473ffeceae2..6c58cf7174eb 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -23,12 +23,7 @@ import * as Stream from "effect/Stream"; import { HttpClient } from "effect/unstable/http"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; -import { - buildSshChildEnvironment, - type SshAuthOptions, - SshPasswordPrompt, - isSshAuthFailure, -} from "./auth.ts"; +import * as SshAuth from "./auth.ts"; import { baseSshArgs, buildSshHostSpecEffect, @@ -107,7 +102,7 @@ type SshEnvironmentEffectContext = | Path.Path | HttpClient.HttpClient | NetService.NetService - | SshPasswordPrompt; + | SshAuth.SshPasswordPrompt; type SshEnvironmentEffectError = | SshCommandError @@ -145,7 +140,7 @@ interface SshAuthOperationInput { readonly key: string; readonly target: DesktopSshEnvironmentTarget; readonly operation: ( - authOptions: SshAuthOptions, + authOptions: SshAuth.SshAuthOptions, ) => Effect.Effect; } @@ -867,7 +862,7 @@ function buildRemoteLogTailScript(target: DesktopSshEnvironmentTarget): string { export const launchOrReuseRemoteServer = Effect.fn("ssh/tunnel.launchOrReuseRemoteServer")( function* ( target: DesktopSshEnvironmentTarget, - input?: SshAuthOptions, + input?: SshAuth.SshAuthOptions, runner?: RemoteT3RunnerOptions, ): Effect.fn.Return< { readonly remotePort: number; readonly remoteServerKind: "external" | "managed" | null }, @@ -926,7 +921,7 @@ export const launchOrReuseRemoteServer = Effect.fn("ssh/tunnel.launchOrReuseRemo export const issueRemotePairingToken = Effect.fn("ssh/tunnel.issueRemotePairingToken")(function* ( target: DesktopSshEnvironmentTarget, - input?: SshAuthOptions, + input?: SshAuth.SshAuthOptions, runner?: RemoteT3RunnerOptions, ): Effect.fn.Return< { @@ -982,7 +977,7 @@ export const issueRemotePairingToken = Effect.fn("ssh/tunnel.issueRemotePairingT const stopRemoteServer = Effect.fn("ssh/tunnel.stopRemoteServer")(function* ( target: DesktopSshEnvironmentTarget, - input?: SshAuthOptions, + input?: SshAuth.SshAuthOptions, ): Effect.fn.Return< void, SshCommandError | SshInvalidTargetError, @@ -1007,7 +1002,7 @@ const stopRemoteServer = Effect.fn("ssh/tunnel.stopRemoteServer")(function* ( const readRemoteServerLogTail = Effect.fn("ssh/tunnel.readRemoteServerLogTail")(function* ( target: DesktopSshEnvironmentTarget, - input?: SshAuthOptions, + input?: SshAuth.SshAuthOptions, ): Effect.fn.Return< string, SshCommandError | SshInvalidTargetError, @@ -1107,7 +1102,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: readonly localPort: number; readonly httpBaseUrl: string; readonly wsBaseUrl: string; - readonly authOptions: SshAuthOptions; + readonly authOptions: SshAuth.SshAuthOptions; readonly remoteServerKind: "external" | "managed" | null; }): Effect.fn.Return< SshTunnelEntry, @@ -1120,7 +1115,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: | Scope.Scope > { const hostSpec = yield* buildSshHostSpecEffect(input.resolvedTarget); - const childEnvironment = yield* buildSshChildEnvironment({ + const childEnvironment = yield* SshAuth.buildSshChildEnvironment({ ...(input.authOptions.authSecret === undefined ? {} : { authSecret: input.authOptions.authSecret }), @@ -1380,8 +1375,12 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma const promptForPassword = Effect.fn("ssh/tunnel.promptForPassword")(function* ( target: DesktopSshEnvironmentTarget, attempt: number, - ): Effect.fn.Return { - const promptService = yield* SshPasswordPrompt; + ): Effect.fn.Return< + string, + SshInvalidTargetError | SshPasswordPromptError, + SshAuth.SshPasswordPrompt + > { + const promptService = yield* SshAuth.SshPasswordPrompt; const hostSpec = yield* buildSshHostSpecEffect(target); if (!promptService.isAvailable) { yield* Effect.logWarning("ssh.auth.passwordPrompt.unavailable", { @@ -1425,7 +1424,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma readonly error: SshEnvironmentEffectError; }, ): Effect.fn.Return { - if (!isSshAuthFailure(input.error)) { + if (!SshAuth.isSshAuthFailure(input.error)) { return yield* input.error; } @@ -1435,7 +1434,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma promptCount: input.promptCount, cause: input.error, }); - const promptService = yield* SshPasswordPrompt; + const promptService = yield* SshAuth.SshPasswordPrompt; if (!promptService.isAvailable) { return yield* input.error; } @@ -1460,7 +1459,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma const runWithSshAuthAttempt = Effect.fn("ssh/tunnel.runWithSshAuthAttempt")(function* ( input: SshAuthAttemptInput, ): Effect.fn.Return { - const promptService = yield* SshPasswordPrompt; + const promptService = yield* SshAuth.SshPasswordPrompt; const authOptions = input.authSecret === null ? {