From c368199d2b6e5a7f0cfc4cde8bddd220bd69a5f6 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:33:43 -0700 Subject: [PATCH 1/7] refactor(client-runtime): import Effect services as namespaces Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/connection/onboarding.test.ts | 10 +- .../src/connection/supervisor.test.ts | 16 ++-- packages/client-runtime/src/rpc/client.ts | 22 +++-- .../client-runtime/src/state/assets.test.ts | 33 ++++--- .../state/boundedThreadSnapshotHttp.test.ts | 10 +- .../src/state/boundedThreadSnapshotHttp.ts | 50 +++++----- .../src/state/deviceHubAccess.ts | 10 +- .../src/state/environmentHttpAuth.test.ts | 93 +++++++++++-------- .../src/state/pullRequestDiffHttp.ts | 16 ++-- .../src/state/pullRequestRouting.ts | 38 +++++--- .../src/state/pullRequests.test.ts | 10 +- .../client-runtime/src/state/pullRequests.ts | 13 ++- packages/client-runtime/src/state/runtime.ts | 77 +++++++++------ packages/client-runtime/src/state/server.ts | 21 +++-- .../src/state/serverUsage.test.ts | 22 ++--- packages/client-runtime/src/state/session.ts | 26 +++--- .../src/state/shell-sync.test.ts | 39 ++++---- packages/client-runtime/src/state/shell.ts | 19 ++-- .../src/state/shellSnapshotHttp.ts | 16 ++-- .../client-runtime/src/state/sourceControl.ts | 4 +- .../src/state/threadCommands.test.ts | 14 +-- .../src/state/threadCommands.ts | 17 ++-- .../src/state/threadHistoryController.test.ts | 21 ++--- .../src/state/threadHistoryHttp.ts | 4 +- .../src/state/threadSnapshotHttp.ts | 10 +- .../src/state/threads-atoms.test.ts | 46 ++++----- .../src/state/threads-sync.test.ts | 17 ++-- packages/client-runtime/src/state/threads.ts | 66 +++++++------ packages/client-runtime/src/state/vcs.ts | 14 +-- .../client-runtime/src/state/vcsAction.ts | 6 +- .../src/state/vcsRefInvalidation.ts | 4 +- 31 files changed, 429 insertions(+), 335 deletions(-) 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/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/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..87fe17c90df8 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.pullRequestDiffLoaderLayer.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..955ada131106 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({ @@ -87,8 +89,10 @@ export const pullRequestDiffLoaderLayer: Layer.Layer< 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..282117b4f235 100644 --- a/packages/client-runtime/src/state/pullRequests.ts +++ b/packages/client-runtime/src/state/pullRequests.ts @@ -22,9 +22,9 @@ 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, @@ -155,7 +155,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 +264,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..83a97210f291 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 >, ) { diff --git a/packages/client-runtime/src/state/shellSnapshotHttp.ts b/packages/client-runtime/src/state/shellSnapshotHttp.ts index f5945e503c11..f3b725afcc48 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({ @@ -73,8 +75,10 @@ export const shellSnapshotLoaderLayer: Layer.Layer< 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..80de7577f0b0 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.threadHistoryControllerLayer)), ); 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.threadHistoryControllerLayer))), ); }); 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..0077af88a44a 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.threadHistoryControllerLayer)); 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..6a7893acda91 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.threadHistoryControllerLayer), ); 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..4e9d4bea7f03 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 >, ) { 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* () { From 56363c3c86f034dc0a3c7e96014cefb412619485 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:33:44 -0700 Subject: [PATCH 2/7] refactor(packages): import Effect services as namespaces in shared, ssh and codex app server Co-Authored-By: Claude Opus 5.5 (1M context) --- .../effect-codex-app-server/src/replay.ts | 9 +- packages/shared/src/relayClient.test.ts | 28 +++--- packages/ssh/src/tunnel.test.ts | 96 +++++++++---------- packages/ssh/src/tunnel.ts | 37 ++++--- 4 files changed, 81 insertions(+), 89 deletions(-) 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 ? { From 3b303010a14331277de8a962ee094240b6bad296 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:33:46 -0700 Subject: [PATCH 3/7] refactor(relay): import Effect services as namespaces Co-Authored-By: Claude Opus 5.5 (1M context) --- .../AgentActivityPublisher.test.ts | 8 +- infra/relay/src/agentActivity/ApnsClient.ts | 4 +- .../relay/src/agentActivity/FcmClient.test.ts | 22 ++-- .../src/agentActivity/FcmDeliveries.test.ts | 108 +++++++++--------- .../agentActivity/MobileRegistrations.test.ts | 4 +- 5 files changed, 70 insertions(+), 76 deletions(-) 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, }), From 07f32750b443901a29230452ca71f41e6ef942fa Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:33:48 -0700 Subject: [PATCH 4/7] refactor(mobile,desktop): import Effect services as namespaces Co-Authored-By: Claude Opus 5.5 (1M context) --- .../BrowserImport/LinuxBrowserSecret.ts | 4 +- .../environment-cache-store.test.ts | 34 +++++++++---- apps/mobile/src/connection/storage.test.ts | 10 ++-- .../liveActivityPreferences.test.ts | 6 +-- .../features/cloud/linkEnvironment.test.ts | 8 +-- .../src/persistence/mobile-database.test.ts | 12 ++--- apps/mobile/src/state/client-cache-state.ts | 10 ++-- apps/mobile/src/state/preferences.test.ts | 49 ++++++++++-------- apps/mobile/src/state/preferences.ts | 50 ++++++++++--------- 9 files changed, 106 insertions(+), 77 deletions(-) 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/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/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/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/linkEnvironment.test.ts b/apps/mobile/src/features/cloud/linkEnvironment.test.ts index 8fa346c5be45..5472f0dfb0c7 100644 --- a/apps/mobile/src/features/cloud/linkEnvironment.test.ts +++ b/apps/mobile/src/features/cloud/linkEnvironment.test.ts @@ -8,7 +8,7 @@ import { ManagedRelay } from "@t3tools/client-runtime/relay"; import { remoteHttpClientLayer } from "@t3tools/client-runtime/rpc"; import { HttpClient } from "effect/unstable/http"; -import { MobileStorage } from "../../persistence/mobile-storage"; +import * as MobileStorage from "../../persistence/mobile-storage"; import { linkEnvironmentToCloudWithPreference } from "./linkEnvironment"; @@ -76,8 +76,8 @@ function cloudClientLayer() { return Layer.mergeAll( httpClientLayer, Layer.succeed( - MobileStorage, - MobileStorage.of({ + MobileStorage.MobileStorage, + MobileStorage.MobileStorage.of({ loadSavedConnections: Effect.succeed([]), saveConnection: () => 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 + >, }); }), ), From c38b496bbcba9aa038b72931a07e7eaae53ed814 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Wed, 30 Sep 2026 02:04:17 -0700 Subject: [PATCH 5/7] refactor(client-runtime): expose connection and platform services as barrel namespaces The connection and platform barrels now re-export each service module as a namespace (EnvironmentRegistry, EnvironmentSupervisor, ConnectionOnboarding, ClientCapabilities, Persistence, PlatformConnectionSource), and web and mobile read tags through them. EnvironmentNotRegisteredError, PlatformEnvironmentRemovalError and ConnectionPersistenceError stay flat too, because web's inferred atom types must be able to name them. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/connection/background-activity.ts | 2 +- .../src/connection/environment-cache-store.ts | 13 +++---- apps/mobile/src/connection/onboarding.ts | 7 +++- apps/mobile/src/connection/platform.ts | 38 +++++++++---------- apps/mobile/src/connection/storage.ts | 14 +++---- .../mobile/src/features/cloud/cloud-drafts.ts | 2 +- apps/web/src/cloud/linkEnvironment.test.ts | 20 +++++----- apps/web/src/cloud/linkEnvironment.ts | 6 +-- apps/web/src/connection/onboarding.ts | 8 +++- apps/web/src/connection/platform.ts | 38 +++++++++---------- apps/web/src/connection/storage.ts | 19 ++++------ .../web/src/lib/backgroundActivityReporter.ts | 2 +- .../client-runtime/src/connection/index.ts | 17 +++------ packages/client-runtime/src/platform/index.ts | 8 ++-- 14 files changed, 93 insertions(+), 101 deletions(-) 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.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/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/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; }) { 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/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/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/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"; From ea3a7e47601edec92814a2ef87f9911357880536 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Wed, 30 Sep 2026 02:04:18 -0700 Subject: [PATCH 6/7] refactor(desktop,relay): stop mixing namespace and named imports of one service module Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/desktop/src/ipc/methods/window.test.ts | 6 ++++-- infra/relay/src/environments/ManagedEndpointReaper.ts | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) 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/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, From d7fb75475f547f399623110a1ac13236b5538ad2 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Wed, 30 Sep 2026 02:08:25 -0700 Subject: [PATCH 7/7] refactor(client-runtime): name single-implementation layers `layer` ThreadHistoryController, ShellSnapshotLoader, PullRequestDiffLoader and RpcSessionFactory each have one implementation. Their layers were exported as threadHistoryControllerLayer, shellSnapshotLoaderLayer, pullRequestDiffLoaderLayer and layerWithOptions. Each is now `layer`, and the state barrels re-export these modules as namespaces. Web and mobile runtimes compose ShellSnapshotLoader.layer, ThreadHistoryController.layer and PullRequestDiffLoader.layer. Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/mobile/src/connection/runtime.ts | 8 ++++---- apps/web/src/connection/runtime.ts | 12 ++++++------ packages/client-runtime/src/connection/layer.ts | 2 +- packages/client-runtime/src/rpc/session.test.ts | 2 +- packages/client-runtime/src/rpc/session.ts | 3 +-- .../src/state/environmentHttpAuth.test.ts | 2 +- .../client-runtime/src/state/pullRequestDiffHttp.ts | 6 +----- packages/client-runtime/src/state/pullRequests.ts | 3 +-- packages/client-runtime/src/state/shell.ts | 2 +- .../client-runtime/src/state/shellSnapshotHttp.ts | 6 +----- .../src/state/threadHistoryController.test.ts | 4 ++-- .../src/state/threadHistoryController.ts | 2 +- .../client-runtime/src/state/threads-atoms.test.ts | 2 +- .../client-runtime/src/state/threads-sync.test.ts | 2 +- packages/client-runtime/src/state/threads.ts | 4 +++- 15 files changed, 26 insertions(+), 34 deletions(-) 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/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/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/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/environmentHttpAuth.test.ts b/packages/client-runtime/src/state/environmentHttpAuth.test.ts index 87fe17c90df8..6be3c9af0282 100644 --- a/packages/client-runtime/src/state/environmentHttpAuth.test.ts +++ b/packages/client-runtime/src/state/environmentHttpAuth.test.ts @@ -398,7 +398,7 @@ 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 = PullRequestDiffLoader.pullRequestDiffLoaderLayer.pipe( + const loaderLayer = PullRequestDiffLoader.layer.pipe( Layer.provide( Layer.mergeAll( harness.httpLayer, diff --git a/packages/client-runtime/src/state/pullRequestDiffHttp.ts b/packages/client-runtime/src/state/pullRequestDiffHttp.ts index 955ada131106..519582a35b30 100644 --- a/packages/client-runtime/src/state/pullRequestDiffHttp.ts +++ b/packages/client-runtime/src/state/pullRequestDiffHttp.ts @@ -81,11 +81,7 @@ 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; diff --git a/packages/client-runtime/src/state/pullRequests.ts b/packages/client-runtime/src/state/pullRequests.ts index 282117b4f235..ad6f4f9e1209 100644 --- a/packages/client-runtime/src/state/pullRequests.ts +++ b/packages/client-runtime/src/state/pullRequests.ts @@ -29,9 +29,8 @@ 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( diff --git a/packages/client-runtime/src/state/shell.ts b/packages/client-runtime/src/state/shell.ts index 83a97210f291..698d4c2f4d49 100644 --- a/packages/client-runtime/src/state/shell.ts +++ b/packages/client-runtime/src/state/shell.ts @@ -449,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 f3b725afcc48..fb98c871ecb2 100644 --- a/packages/client-runtime/src/state/shellSnapshotHttp.ts +++ b/packages/client-runtime/src/state/shellSnapshotHttp.ts @@ -65,11 +65,7 @@ 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; diff --git a/packages/client-runtime/src/state/threadHistoryController.test.ts b/packages/client-runtime/src/state/threadHistoryController.test.ts index 80de7577f0b0..f449f1120a23 100644 --- a/packages/client-runtime/src/state/threadHistoryController.test.ts +++ b/packages/client-runtime/src/state/threadHistoryController.test.ts @@ -48,7 +48,7 @@ describe("ThreadHistoryController", () => { yield* controller.unregister(newerRegistration); expect(yield* controller.loadEarlier(ENV, THREAD)).toEqual({ _tag: "noop" }); - }).pipe(Effect.provide(ThreadHistoryController.threadHistoryControllerLayer)), + }).pipe(Effect.provide(ThreadHistoryController.layer)), ); it.effect("unregister removes only its own matching registration", () => @@ -58,6 +58,6 @@ describe("ThreadHistoryController", () => { 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(ThreadHistoryController.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/threads-atoms.test.ts b/packages/client-runtime/src/state/threads-atoms.test.ts index 0077af88a44a..074c21f5fab1 100644 --- a/packages/client-runtime/src/state/threads-atoms.test.ts +++ b/packages/client-runtime/src/state/threads-atoms.test.ts @@ -147,7 +147,7 @@ const makeHarness = Effect.fn("TestThreadAtoms.makeHarness")(function* (options? }); const historyController = yield* Effect.service( ThreadHistoryController.ThreadHistoryController, - ).pipe(Effect.provide(ThreadHistoryController.threadHistoryControllerLayer)); + ).pipe(Effect.provide(ThreadHistoryController.layer)); const historyHttpClient = HttpClient.make((request, url) => Effect.gen(function* () { const response = yield* Deferred.make(); diff --git a/packages/client-runtime/src/state/threads-sync.test.ts b/packages/client-runtime/src/state/threads-sync.test.ts index 6a7893acda91..0c1619a52b19 100644 --- a/packages/client-runtime/src/state/threads-sync.test.ts +++ b/packages/client-runtime/src/state/threads-sync.test.ts @@ -234,7 +234,7 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o ); } const historyController = yield* ThreadHistoryController.ThreadHistoryController.pipe( - Effect.provide(ThreadHistoryController.threadHistoryControllerLayer), + Effect.provide(ThreadHistoryController.layer), ); if (options?.historyPaging !== "no-controller") { makeThreadState = makeThreadState.pipe( diff --git a/packages/client-runtime/src/state/threads.ts b/packages/client-runtime/src/state/threads.ts index 4e9d4bea7f03..5ae532cbbed1 100644 --- a/packages/client-runtime/src/state/threads.ts +++ b/packages/client-runtime/src/state/threads.ts @@ -996,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";