| author | |
| committer | |
| log | 0ad1e9d9e4bf691ad1dcfb5af69de11b0b48047f |
| tree | bbb8a32a0b3913f32caba44b02b511340d8a429e |
| parent | 2f8d3baa88cd137b11083942a4961ca79a9f99be |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>44 files changed, 1281 insertions(+), 1186 deletions(-)
apps/mobile/src/connection/catalog-store.ts+1-1| ... | @@ -18,7 +18,7 @@ export const LEGACY_CONNECTIONS_KEY = "t3code.connections"; | ... | @@ -18,7 +18,7 @@ export const LEGACY_CONNECTIONS_KEY = "t3code.connections"; |
| 18 | function catalogError(operation: string, cause: unknown) { | 18 | function catalogError(operation: string, cause: unknown) { |
| 19 | return new ConnectionTransientError({ | 19 | return new ConnectionTransientError({ |
| 20 | reason: "remote-unavailable", | 20 | reason: "remote-unavailable", |
| 21 | message: `Could not ${operation} the local connection catalog: ${String(cause)}`, | 21 | detail: `Could not ${operation} the local connection catalog: ${String(cause)}`, |
| 22 | }); | 22 | }); |
| 23 | } | 23 | } |
| 24 | 24 |
apps/mobile/src/connection/platform.ts+53-47| ... | @@ -10,8 +10,8 @@ import { | ... | @@ -10,8 +10,8 @@ import { |
| 10 | import { | 10 | import { |
| 11 | ConnectionBlockedError, | 11 | ConnectionBlockedError, |
| 12 | ConnectionTransientError, | 12 | ConnectionTransientError, |
| 13 | ConnectionWakeups, | ||
| 14 | Connectivity, | 13 | Connectivity, |
| 14 | Wakeups, | ||
| 15 | } from "@t3tools/client-runtime/connection"; | 15 | } from "@t3tools/client-runtime/connection"; |
| 16 | import { managedRelayAccountChanges, managedRelaySessionAtom } from "@t3tools/client-runtime/relay"; | 16 | import { managedRelayAccountChanges, managedRelaySessionAtom } from "@t3tools/client-runtime/relay"; |
| 17 | import { AuthStandardClientScopes } from "@t3tools/contracts"; | 17 | import { AuthStandardClientScopes } from "@t3tools/contracts"; |
| ... | @@ -41,53 +41,47 @@ function networkStatus(state: Network.NetworkState): "unknown" | "offline" | "on | ... | @@ -41,53 +41,47 @@ function networkStatus(state: Network.NetworkState): "unknown" | "offline" | "on |
| 41 | return "unknown"; | 41 | return "unknown"; |
| 42 | } | 42 | } |
| 43 | 43 | ||
| 44 | const connectivityLayer = Layer.succeed( | 44 | const connectivityLayer = Connectivity.layer({ |
| 45 | Connectivity, | 45 | status: Effect.tryPromise({ |
| 46 | Connectivity.of({ | 46 | try: () => Network.getNetworkStateAsync(), |
| 47 | status: Effect.tryPromise({ | 47 | catch: () => undefined, |
| 48 | try: () => Network.getNetworkStateAsync(), | 48 | }).pipe( |
| 49 | catch: () => undefined, | 49 | Effect.match({ |
| 50 | }).pipe( | 50 | onFailure: () => "unknown" as const, |
| 51 | Effect.match({ | 51 | onSuccess: networkStatus, |
| 52 | onFailure: () => "unknown" as const, | 52 | }), |
| 53 | onSuccess: networkStatus, | 53 | ), |
| 54 | }), | 54 | changes: Stream.callback((queue) => |
| 55 | ), | 55 | Effect.acquireRelease( |
| 56 | changes: Stream.callback((queue) => | 56 | Effect.sync(() => |
| 57 | Network.addNetworkStateListener((state) => { | ||
| 58 | Queue.offerUnsafe(queue, networkStatus(state)); | ||
| 59 | }), | ||
| 60 | ), | ||
| 61 | (subscription) => Effect.sync(() => subscription.remove()), | ||
| 62 | ).pipe(Effect.asVoid), | ||
| 63 | ), | ||
| 64 | }); | ||
| 65 | |||
| 66 | const wakeupsLayer = Wakeups.layer({ | ||
| 67 | changes: Stream.merge( | ||
| 68 | Stream.callback<"application-active">((queue) => | ||
| 57 | Effect.acquireRelease( | 69 | Effect.acquireRelease( |
| 58 | Effect.sync(() => | 70 | Effect.sync(() => |
| 59 | Network.addNetworkStateListener((state) => { | 71 | AppState.addEventListener("change", (state) => { |
| 60 | Queue.offerUnsafe(queue, networkStatus(state)); | 72 | if (state === "active") { |
| 73 | Queue.offerUnsafe(queue, "application-active"); | ||
| 74 | } | ||
| 61 | }), | 75 | }), |
| 62 | ), | 76 | ), |
| 63 | (subscription) => Effect.sync(() => subscription.remove()), | 77 | (subscription) => Effect.sync(() => subscription.remove()), |
| 64 | ).pipe(Effect.asVoid), | 78 | ).pipe(Effect.asVoid), |
| 65 | ), | 79 | ), |
| 66 | }), | 80 | managedRelayAccountChanges(appAtomRegistry).pipe( |
| 67 | ); | 81 | Stream.map(() => "credentials-changed" as const), |
| 68 | |||
| 69 | const wakeupsLayer = Layer.succeed( | ||
| 70 | ConnectionWakeups, | ||
| 71 | ConnectionWakeups.of({ | ||
| 72 | changes: Stream.merge( | ||
| 73 | Stream.callback<"application-active">((queue) => | ||
| 74 | Effect.acquireRelease( | ||
| 75 | Effect.sync(() => | ||
| 76 | AppState.addEventListener("change", (state) => { | ||
| 77 | if (state === "active") { | ||
| 78 | Queue.offerUnsafe(queue, "application-active"); | ||
| 79 | } | ||
| 80 | }), | ||
| 81 | ), | ||
| 82 | (subscription) => Effect.sync(() => subscription.remove()), | ||
| 83 | ).pipe(Effect.asVoid), | ||
| 84 | ), | ||
| 85 | managedRelayAccountChanges(appAtomRegistry).pipe( | ||
| 86 | Stream.map(() => "credentials-changed" as const), | ||
| 87 | ), | ||
| 88 | ), | 82 | ), |
| 89 | }), | 83 | ), |
| 90 | ); | 84 | }); |
| 91 | 85 | ||
| 92 | const capabilitiesLayer = Layer.succeedContext( | 86 | const capabilitiesLayer = Layer.succeedContext( |
| 93 | Context.make( | 87 | Context.make( |
| ... | @@ -98,7 +92,7 @@ const capabilitiesLayer = Layer.succeedContext( | ... | @@ -98,7 +92,7 @@ const capabilitiesLayer = Layer.succeedContext( |
| 98 | if (session === null) { | 92 | if (session === null) { |
| 99 | return yield* new ConnectionBlockedError({ | 93 | return yield* new ConnectionBlockedError({ |
| 100 | reason: "authentication", | 94 | reason: "authentication", |
| 101 | message: "Sign in to T3 Cloud to connect this environment.", | 95 | detail: "Sign in to T3 Cloud to connect this environment.", |
| 102 | }); | 96 | }); |
| 103 | } | 97 | } |
| 104 | const token = yield* session.readClerkToken().pipe( | 98 | const token = yield* session.readClerkToken().pipe( |
| ... | @@ -106,14 +100,14 @@ const capabilitiesLayer = Layer.succeedContext( | ... | @@ -106,14 +100,14 @@ const capabilitiesLayer = Layer.succeedContext( |
| 106 | (error) => | 100 | (error) => |
| 107 | new ConnectionTransientError({ | 101 | new ConnectionTransientError({ |
| 108 | reason: "network", | 102 | reason: "network", |
| 109 | message: error.message, | 103 | detail: error.message, |
| 110 | }), | 104 | }), |
| 111 | ), | 105 | ), |
| 112 | ); | 106 | ); |
| 113 | if (token === null) { | 107 | if (token === null) { |
| 114 | return yield* new ConnectionBlockedError({ | 108 | return yield* new ConnectionBlockedError({ |
| 115 | reason: "authentication", | 109 | reason: "authentication", |
| 116 | message: "The T3 Cloud session is unavailable.", | 110 | detail: "The T3 Cloud session is unavailable.", |
| 117 | }); | 111 | }); |
| 118 | } | 112 | } |
| 119 | return token; | 113 | return token; |
| ... | @@ -132,7 +126,7 @@ const capabilitiesLayer = Layer.succeedContext( | ... | @@ -132,7 +126,7 @@ const capabilitiesLayer = Layer.succeedContext( |
| 132 | catch: (cause) => | 126 | catch: (cause) => |
| 133 | new ConnectionTransientError({ | 127 | new ConnectionTransientError({ |
| 134 | reason: "remote-unavailable", | 128 | reason: "remote-unavailable", |
| 135 | message: `Could not load the mobile device identity: ${String(cause)}`, | 129 | detail: `Could not load the mobile device identity: ${String(cause)}`, |
| 136 | }), | 130 | }), |
| 137 | }).pipe(Effect.map(Option.some)), | 131 | }).pipe(Effect.map(Option.some)), |
| 138 | }), | 132 | }), |
| ... | @@ -151,14 +145,14 @@ const capabilitiesLayer = Layer.succeedContext( | ... | @@ -151,14 +145,14 @@ const capabilitiesLayer = Layer.succeedContext( |
| 151 | Effect.fail( | 145 | Effect.fail( |
| 152 | new ConnectionBlockedError({ | 146 | new ConnectionBlockedError({ |
| 153 | reason: "unsupported", | 147 | reason: "unsupported", |
| 154 | message: "SSH environments are only available in the desktop app.", | 148 | detail: "SSH environments are only available in the desktop app.", |
| 155 | }), | 149 | }), |
| 156 | ), | 150 | ), |
| 157 | prepare: () => | 151 | prepare: () => |
| 158 | Effect.fail( | 152 | Effect.fail( |
| 159 | new ConnectionBlockedError({ | 153 | new ConnectionBlockedError({ |
| 160 | reason: "unsupported", | 154 | reason: "unsupported", |
| 161 | message: "SSH environments are only available in the desktop app.", | 155 | detail: "SSH environments are only available in the desktop app.", |
| 162 | }), | 156 | }), |
| 163 | ), | 157 | ), |
| 164 | disconnect: () => Effect.void, | 158 | disconnect: () => Effect.void, |
| ... | @@ -195,7 +189,19 @@ const environmentOwnedDataCleanupLayer = Layer.succeed( | ... | @@ -195,7 +189,19 @@ const environmentOwnedDataCleanupLayer = Layer.succeed( |
| 195 | }), | 189 | }), |
| 196 | ); | 190 | ); |
| 197 | 191 | ||
| 198 | export const connectionPlatformLayer = Layer.mergeAll( | 192 | type ConnectionPlatformLayerSource = |
| 193 | | typeof connectionStorageLayer | ||
| 194 | | typeof connectivityLayer | ||
| 195 | | typeof wakeupsLayer | ||
| 196 | | typeof capabilitiesLayer | ||
| 197 | | typeof platformConnectionSourceLayer | ||
| 198 | | typeof environmentOwnedDataCleanupLayer; | ||
| 199 | |||
| 200 | export const connectionPlatformLayer: Layer.Layer< | ||
| 201 | Layer.Success<ConnectionPlatformLayerSource>, | ||
| 202 | Layer.Error<ConnectionPlatformLayerSource>, | ||
| 203 | Layer.Services<ConnectionPlatformLayerSource> | ||
| 204 | > = Layer.mergeAll( | ||
| 199 | connectionStorageLayer, | 205 | connectionStorageLayer, |
| 200 | connectivityLayer, | 206 | connectivityLayer, |
| 201 | wakeupsLayer, | 207 | wakeupsLayer, |
apps/mobile/src/connection/runtime.ts+18-6| ... | @@ -1,16 +1,28 @@ | ... | @@ -1,16 +1,28 @@ |
| 1 | import { connectionLayer as clientConnectionLayer } from "@t3tools/client-runtime/connection"; | 1 | import { Connection } from "@t3tools/client-runtime/connection"; |
| 2 | import * as Layer from "effect/Layer"; | 2 | import * as Layer from "effect/Layer"; |
| 3 | import { Atom } from "effect/unstable/reactivity"; | 3 | import { Atom } from "effect/unstable/reactivity"; |
| 4 | 4 | ||
| 5 | import { runtimeContextLayer } from "../lib/runtime"; | 5 | import { runtimeContextLayer } from "../lib/runtime"; |
| 6 | import { connectionPlatformLayer } from "./platform"; | 6 | import { connectionPlatformLayer } from "./platform"; |
| 7 | 7 | ||
| 8 | const providedConnectionPlatformLayer = connectionPlatformLayer.pipe( | 8 | const providedConnectionPlatformLayer: Layer.Layer< |
| 9 | Layer.provide(runtimeContextLayer), | 9 | Layer.Success<typeof connectionPlatformLayer>, |
| 10 | ); | 10 | Layer.Error<typeof connectionPlatformLayer> |
| 11 | > = connectionPlatformLayer.pipe(Layer.provide(runtimeContextLayer)); | ||
| 12 | |||
| 13 | type ConnectionLayerSource = | ||
| 14 | | typeof Connection.layer | ||
| 15 | | typeof runtimeContextLayer | ||
| 16 | | typeof providedConnectionPlatformLayer; | ||
| 11 | 17 | ||
| 12 | export const connectionLayer = clientConnectionLayer.pipe( | 18 | export const connectionLayer: Layer.Layer< |
| 19 | Layer.Success<ConnectionLayerSource>, | ||
| 20 | Layer.Error<ConnectionLayerSource> | ||
| 21 | > = Connection.layer.pipe( | ||
| 13 | Layer.provideMerge(Layer.mergeAll(runtimeContextLayer, providedConnectionPlatformLayer)), | 22 | Layer.provideMerge(Layer.mergeAll(runtimeContextLayer, providedConnectionPlatformLayer)), |
| 14 | ); | 23 | ); |
| 15 | 24 | ||
| 16 | export const connectionAtomRuntime = Atom.runtime(connectionLayer); | 25 | export const connectionAtomRuntime: Atom.AtomRuntime< |
| 26 | Layer.Success<typeof connectionLayer>, | ||
| 27 | Layer.Error<typeof connectionLayer> | ||
| 28 | > = Atom.runtime(connectionLayer); |
apps/mobile/src/connection/storage.ts+10-10| ... | @@ -8,11 +8,11 @@ import { | ... | @@ -8,11 +8,11 @@ import { |
| 8 | removeCatalogValue, | 8 | removeCatalogValue, |
| 9 | replaceCatalogValue, | 9 | replaceCatalogValue, |
| 10 | } from "@t3tools/client-runtime/platform"; | 10 | } from "@t3tools/client-runtime/platform"; |
| 11 | import { RemoteDpopAccessTokenStore } from "@t3tools/client-runtime/authorization"; | 11 | import { TokenStore } from "@t3tools/client-runtime/authorization"; |
| 12 | import { | 12 | import { |
| 13 | ConnectionCredentialStore, | ||
| 14 | ConnectionProfileStore, | ||
| 15 | ConnectionTransientError, | 13 | ConnectionTransientError, |
| 14 | CredentialStore, | ||
| 15 | ProfileStore, | ||
| 16 | } from "@t3tools/client-runtime/connection"; | 16 | } from "@t3tools/client-runtime/connection"; |
| 17 | import { | 17 | import { |
| 18 | EnvironmentId, | 18 | EnvironmentId, |
| ... | @@ -58,7 +58,7 @@ const LegacyStoredShellSnapshot = Schema.Struct({ | ... | @@ -58,7 +58,7 @@ const LegacyStoredShellSnapshot = Schema.Struct({ |
| 58 | function catalogError(operation: string, cause: unknown) { | 58 | function catalogError(operation: string, cause: unknown) { |
| 59 | return new ConnectionTransientError({ | 59 | return new ConnectionTransientError({ |
| 60 | reason: "remote-unavailable", | 60 | reason: "remote-unavailable", |
| 61 | message: `Could not ${operation} the local connection catalog: ${String(cause)}`, | 61 | detail: `Could not ${operation} the local connection catalog: ${String(cause)}`, |
| 62 | }); | 62 | }); |
| 63 | } | 63 | } |
| 64 | 64 | ||
| ... | @@ -197,7 +197,7 @@ export const connectionStorageLayer = Layer.effectContext( | ... | @@ -197,7 +197,7 @@ export const connectionStorageLayer = Layer.effectContext( |
| 197 | .update((document) => removeConnectionFromCatalog(document, target)) | 197 | .update((document) => removeConnectionFromCatalog(document, target)) |
| 198 | .pipe(Effect.mapError((error) => targetPersistenceError("remove-connection", error))), | 198 | .pipe(Effect.mapError((error) => targetPersistenceError("remove-connection", error))), |
| 199 | }); | 199 | }); |
| 200 | const profileStore = ConnectionProfileStore.of({ | 200 | const profileStore = ProfileStore.make({ |
| 201 | get: (connectionId) => | 201 | get: (connectionId) => |
| 202 | catalog.read.pipe( | 202 | catalog.read.pipe( |
| 203 | Effect.map((document) => | 203 | Effect.map((document) => |
| ... | @@ -221,7 +221,7 @@ export const connectionStorageLayer = Layer.effectContext( | ... | @@ -221,7 +221,7 @@ export const connectionStorageLayer = Layer.effectContext( |
| 221 | ), | 221 | ), |
| 222 | })), | 222 | })), |
| 223 | }); | 223 | }); |
| 224 | const credentialStore = ConnectionCredentialStore.of({ | 224 | const credentialStore = CredentialStore.make({ |
| 225 | get: (connectionId) => | 225 | get: (connectionId) => |
| 226 | catalog.read.pipe( | 226 | catalog.read.pipe( |
| 227 | Effect.map((document) => | 227 | Effect.map((document) => |
| ... | @@ -248,7 +248,7 @@ export const connectionStorageLayer = Layer.effectContext( | ... | @@ -248,7 +248,7 @@ export const connectionStorageLayer = Layer.effectContext( |
| 248 | ), | 248 | ), |
| 249 | })), | 249 | })), |
| 250 | }); | 250 | }); |
| 251 | const remoteTokenStore = RemoteDpopAccessTokenStore.of({ | 251 | const remoteTokenStore = TokenStore.make({ |
| 252 | get: (environmentId) => | 252 | get: (environmentId) => |
| 253 | catalog.read.pipe( | 253 | catalog.read.pipe( |
| 254 | Effect.map((document) => | 254 | Effect.map((document) => |
| ... | @@ -423,9 +423,9 @@ export const connectionStorageLayer = Layer.effectContext( | ... | @@ -423,9 +423,9 @@ export const connectionStorageLayer = Layer.effectContext( |
| 423 | 423 | ||
| 424 | return Context.make(ConnectionTargetStore, targetStore).pipe( | 424 | return Context.make(ConnectionTargetStore, targetStore).pipe( |
| 425 | Context.add(ConnectionRegistrationStore, registrationStore), | 425 | Context.add(ConnectionRegistrationStore, registrationStore), |
| 426 | Context.add(ConnectionProfileStore, profileStore), | 426 | Context.add(ProfileStore.ConnectionProfileStore, profileStore), |
| 427 | Context.add(ConnectionCredentialStore, credentialStore), | 427 | Context.add(CredentialStore.ConnectionCredentialStore, credentialStore), |
| 428 | Context.add(RemoteDpopAccessTokenStore, remoteTokenStore), | 428 | Context.add(TokenStore.RemoteDpopAccessTokenStore, remoteTokenStore), |
| 429 | Context.add(EnvironmentCacheStore, cacheStore), | 429 | Context.add(EnvironmentCacheStore, cacheStore), |
| 430 | ); | 430 | ); |
| 431 | }), | 431 | }), |
apps/web/src/cloud/linkEnvironment.test.ts+2-4| ... | @@ -15,9 +15,7 @@ import { HttpClient } from "effect/unstable/http"; | ... | @@ -15,9 +15,7 @@ import { HttpClient } from "effect/unstable/http"; |
| 15 | import { afterEach, beforeEach, vi } from "vite-plus/test"; | 15 | import { afterEach, beforeEach, vi } from "vite-plus/test"; |
| 16 | import { | 16 | import { |
| 17 | AVAILABLE_CONNECTION_STATE, | 17 | AVAILABLE_CONNECTION_STATE, |
| 18 | type EnvironmentRegistryService, | ||
| 19 | EnvironmentSupervisor, | 18 | EnvironmentSupervisor, |
| 20 | type EnvironmentSupervisorService, | ||
| 21 | type PreparedConnection, | 19 | type PreparedConnection, |
| 22 | PrimaryConnectionTarget, | 20 | PrimaryConnectionTarget, |
| 23 | } from "@t3tools/client-runtime/connection"; | 21 | } from "@t3tools/client-runtime/connection"; |
| ... | @@ -114,13 +112,13 @@ function registryLayer(options?: { | ... | @@ -114,13 +112,13 @@ function registryLayer(options?: { |
| 114 | connect: Effect.void, | 112 | connect: Effect.void, |
| 115 | disconnect: Effect.void, | 113 | disconnect: Effect.void, |
| 116 | retryNow: Effect.void, | 114 | retryNow: Effect.void, |
| 117 | } satisfies EnvironmentSupervisorService); | 115 | } satisfies EnvironmentSupervisor["Service"]); |
| 118 | const registry = { | 116 | const registry = { |
| 119 | run: <A, E, R>(_environmentId: EnvironmentId, effect: Effect.Effect<A, E, R>) => | 117 | run: <A, E, R>(_environmentId: EnvironmentId, effect: Effect.Effect<A, E, R>) => |
| 120 | Effect.provideService(effect, EnvironmentSupervisor, supervisor), | 118 | Effect.provideService(effect, EnvironmentSupervisor, supervisor), |
| 121 | runStream: <A, E, R>(_environmentId: EnvironmentId, stream: Stream.Stream<A, E, R>) => | 119 | runStream: <A, E, R>(_environmentId: EnvironmentId, stream: Stream.Stream<A, E, R>) => |
| 122 | Stream.provideService(stream, EnvironmentSupervisor, supervisor), | 120 | Stream.provideService(stream, EnvironmentSupervisor, supervisor), |
| 123 | } as unknown as EnvironmentRegistryService; | 121 | } as unknown as EnvironmentRegistry["Service"]; |
| 124 | return EnvironmentRegistry.of(registry); | 122 | return EnvironmentRegistry.of(registry); |
| 125 | }), | 123 | }), |
| 126 | ); | 124 | ); |
apps/web/src/connection/platform.ts+63-56| ... | @@ -10,11 +10,11 @@ import { | ... | @@ -10,11 +10,11 @@ import { |
| 10 | import { | 10 | import { |
| 11 | ConnectionBlockedError, | 11 | ConnectionBlockedError, |
| 12 | ConnectionTransientError, | 12 | ConnectionTransientError, |
| 13 | ConnectionWakeups, | ||
| 14 | Connectivity, | 13 | Connectivity, |
| 15 | mapRemoteEnvironmentError, | 14 | mapRemoteEnvironmentError, |
| 16 | PrimaryConnectionRegistration, | 15 | PrimaryConnectionRegistration, |
| 17 | PrimaryConnectionTarget, | 16 | PrimaryConnectionTarget, |
| 17 | Wakeups, | ||
| 18 | } from "@t3tools/client-runtime/connection"; | 18 | } from "@t3tools/client-runtime/connection"; |
| 19 | import { fetchRemoteEnvironmentDescriptor } from "@t3tools/client-runtime/environment"; | 19 | import { fetchRemoteEnvironmentDescriptor } from "@t3tools/client-runtime/environment"; |
| 20 | import { managedRelayAccountChanges, managedRelaySessionAtom } from "@t3tools/client-runtime/relay"; | 20 | import { managedRelayAccountChanges, managedRelaySessionAtom } from "@t3tools/client-runtime/relay"; |
| ... | @@ -50,56 +50,50 @@ function currentNetworkStatus(): "unknown" | "offline" | "online" { | ... | @@ -50,56 +50,50 @@ function currentNetworkStatus(): "unknown" | "offline" | "online" { |
| 50 | return navigator.onLine ? "online" : "offline"; | 50 | return navigator.onLine ? "online" : "offline"; |
| 51 | } | 51 | } |
| 52 | 52 | ||
| 53 | const connectivityLayer = Layer.succeed( | 53 | const connectivityLayer = Connectivity.layer({ |
| 54 | Connectivity, | 54 | status: Effect.sync(currentNetworkStatus), |
| 55 | Connectivity.of({ | 55 | changes: Stream.callback((queue) => |
| 56 | status: Effect.sync(currentNetworkStatus), | 56 | Effect.acquireRelease( |
| 57 | changes: Stream.callback((queue) => | 57 | Effect.sync(() => { |
| 58 | const online = () => Queue.offerUnsafe(queue, "online"); | ||
| 59 | const offline = () => Queue.offerUnsafe(queue, "offline"); | ||
| 60 | window.addEventListener("online", online); | ||
| 61 | window.addEventListener("offline", offline); | ||
| 62 | return { online, offline }; | ||
| 63 | }), | ||
| 64 | ({ online, offline }) => | ||
| 65 | Effect.sync(() => { | ||
| 66 | window.removeEventListener("online", online); | ||
| 67 | window.removeEventListener("offline", offline); | ||
| 68 | }), | ||
| 69 | ).pipe(Effect.asVoid), | ||
| 70 | ), | ||
| 71 | }); | ||
| 72 | |||
| 73 | const wakeupsLayer = Wakeups.layer({ | ||
| 74 | changes: Stream.merge( | ||
| 75 | Stream.callback<"application-active">((queue) => | ||
| 58 | Effect.acquireRelease( | 76 | Effect.acquireRelease( |
| 59 | Effect.sync(() => { | 77 | Effect.sync(() => { |
| 60 | const online = () => Queue.offerUnsafe(queue, "online"); | 78 | const listener = () => { |
| 61 | const offline = () => Queue.offerUnsafe(queue, "offline"); | 79 | if (document.visibilityState === "visible") { |
| 62 | window.addEventListener("online", online); | 80 | Queue.offerUnsafe(queue, "application-active"); |
| 63 | window.addEventListener("offline", offline); | 81 | } |
| 64 | return { online, offline }; | 82 | }; |
| 83 | document.addEventListener("visibilitychange", listener); | ||
| 84 | return listener; | ||
| 65 | }), | 85 | }), |
| 66 | ({ online, offline }) => | 86 | (listener) => |
| 67 | Effect.sync(() => { | 87 | Effect.sync(() => { |
| 68 | window.removeEventListener("online", online); | 88 | document.removeEventListener("visibilitychange", listener); |
| 69 | window.removeEventListener("offline", offline); | ||
| 70 | }), | 89 | }), |
| 71 | ).pipe(Effect.asVoid), | 90 | ).pipe(Effect.asVoid), |
| 72 | ), | 91 | ), |
| 73 | }), | 92 | managedRelayAccountChanges(appAtomRegistry).pipe( |
| 74 | ); | 93 | Stream.map(() => "credentials-changed" as const), |
| 75 | |||
| 76 | const wakeupsLayer = Layer.succeed( | ||
| 77 | ConnectionWakeups, | ||
| 78 | ConnectionWakeups.of({ | ||
| 79 | changes: Stream.merge( | ||
| 80 | Stream.callback<"application-active">((queue) => | ||
| 81 | Effect.acquireRelease( | ||
| 82 | Effect.sync(() => { | ||
| 83 | const listener = () => { | ||
| 84 | if (document.visibilityState === "visible") { | ||
| 85 | Queue.offerUnsafe(queue, "application-active"); | ||
| 86 | } | ||
| 87 | }; | ||
| 88 | document.addEventListener("visibilitychange", listener); | ||
| 89 | return listener; | ||
| 90 | }), | ||
| 91 | (listener) => | ||
| 92 | Effect.sync(() => { | ||
| 93 | document.removeEventListener("visibilitychange", listener); | ||
| 94 | }), | ||
| 95 | ).pipe(Effect.asVoid), | ||
| 96 | ), | ||
| 97 | managedRelayAccountChanges(appAtomRegistry).pipe( | ||
| 98 | Stream.map(() => "credentials-changed" as const), | ||
| 99 | ), | ||
| 100 | ), | 94 | ), |
| 101 | }), | 95 | ), |
| 102 | ); | 96 | }); |
| 103 | 97 | ||
| 104 | function clientMetadata() { | 98 | function clientMetadata() { |
| 105 | const desktop = window.desktopBridge !== undefined; | 99 | const desktop = window.desktopBridge !== undefined; |
| ... | @@ -116,12 +110,12 @@ function sshPreparationError(cause: unknown) { | ... | @@ -116,12 +110,12 @@ function sshPreparationError(cause: unknown) { |
| 116 | if (message.toLowerCase().includes("cancel")) { | 110 | if (message.toLowerCase().includes("cancel")) { |
| 117 | return new ConnectionBlockedError({ | 111 | return new ConnectionBlockedError({ |
| 118 | reason: "authentication", | 112 | reason: "authentication", |
| 119 | message, | 113 | detail: message, |
| 120 | }); | 114 | }); |
| 121 | } | 115 | } |
| 122 | return new ConnectionTransientError({ | 116 | return new ConnectionTransientError({ |
| 123 | reason: "remote-unavailable", | 117 | reason: "remote-unavailable", |
| 124 | message: `Could not prepare the SSH environment: ${message}`, | 118 | detail: `Could not prepare the SSH environment: ${message}`, |
| 125 | }); | 119 | }); |
| 126 | } | 120 | } |
| 127 | 121 | ||
| ... | @@ -139,7 +133,7 @@ export const provisionDesktopSshEnvironment = Effect.fn( | ... | @@ -139,7 +133,7 @@ export const provisionDesktopSshEnvironment = Effect.fn( |
| 139 | if (pairingToken === null) { | 133 | if (pairingToken === null) { |
| 140 | return yield* new ConnectionBlockedError({ | 134 | return yield* new ConnectionBlockedError({ |
| 141 | reason: "authentication", | 135 | reason: "authentication", |
| 142 | message: "The SSH environment did not issue a pairing credential.", | 136 | detail: "The SSH environment did not issue a pairing credential.", |
| 143 | }); | 137 | }); |
| 144 | } | 138 | } |
| 145 | const descriptor = yield* Effect.tryPromise({ | 139 | const descriptor = yield* Effect.tryPromise({ |
| ... | @@ -170,7 +164,7 @@ const capabilitiesLayer = Layer.effectContext( | ... | @@ -170,7 +164,7 @@ const capabilitiesLayer = Layer.effectContext( |
| 170 | if (session === null) { | 164 | if (session === null) { |
| 171 | return yield* new ConnectionBlockedError({ | 165 | return yield* new ConnectionBlockedError({ |
| 172 | reason: "authentication", | 166 | reason: "authentication", |
| 173 | message: "Sign in to T3 Cloud to connect this environment.", | 167 | detail: "Sign in to T3 Cloud to connect this environment.", |
| 174 | }); | 168 | }); |
| 175 | } | 169 | } |
| 176 | const token = yield* session.readClerkToken().pipe( | 170 | const token = yield* session.readClerkToken().pipe( |
| ... | @@ -178,14 +172,14 @@ const capabilitiesLayer = Layer.effectContext( | ... | @@ -178,14 +172,14 @@ const capabilitiesLayer = Layer.effectContext( |
| 178 | (error) => | 172 | (error) => |
| 179 | new ConnectionTransientError({ | 173 | new ConnectionTransientError({ |
| 180 | reason: "network", | 174 | reason: "network", |
| 181 | message: error.message, | 175 | detail: error.message, |
| 182 | }), | 176 | }), |
| 183 | ), | 177 | ), |
| 184 | ); | 178 | ); |
| 185 | if (token === null) { | 179 | if (token === null) { |
| 186 | return yield* new ConnectionBlockedError({ | 180 | return yield* new ConnectionBlockedError({ |
| 187 | reason: "authentication", | 181 | reason: "authentication", |
| 188 | message: "The T3 Cloud session is unavailable.", | 182 | detail: "The T3 Cloud session is unavailable.", |
| 189 | }); | 183 | }); |
| 190 | } | 184 | } |
| 191 | return token; | 185 | return token; |
| ... | @@ -200,7 +194,7 @@ const capabilitiesLayer = Layer.effectContext( | ... | @@ -200,7 +194,7 @@ const capabilitiesLayer = Layer.effectContext( |
| 200 | catch: (cause) => | 194 | catch: (cause) => |
| 201 | new ConnectionTransientError({ | 195 | new ConnectionTransientError({ |
| 202 | reason: "remote-unavailable", | 196 | reason: "remote-unavailable", |
| 203 | message: `Could not load the desktop primary credential: ${String(cause)}`, | 197 | detail: `Could not load the desktop primary credential: ${String(cause)}`, |
| 204 | }), | 198 | }), |
| 205 | }).pipe(Effect.map(Option.fromNullishOr)), | 199 | }).pipe(Effect.map(Option.fromNullishOr)), |
| 206 | }); | 200 | }); |
| ... | @@ -210,7 +204,7 @@ const capabilitiesLayer = Layer.effectContext( | ... | @@ -210,7 +204,7 @@ const capabilitiesLayer = Layer.effectContext( |
| 210 | if (bridge === undefined) { | 204 | if (bridge === undefined) { |
| 211 | return yield* new ConnectionBlockedError({ | 205 | return yield* new ConnectionBlockedError({ |
| 212 | reason: "unsupported", | 206 | reason: "unsupported", |
| 213 | message: "SSH environments are only available in the desktop app.", | 207 | detail: "SSH environments are only available in the desktop app.", |
| 214 | }); | 208 | }); |
| 215 | } | 209 | } |
| 216 | return yield* provisionDesktopSshEnvironment(bridge, target); | 210 | return yield* provisionDesktopSshEnvironment(bridge, target); |
| ... | @@ -220,7 +214,7 @@ const capabilitiesLayer = Layer.effectContext( | ... | @@ -220,7 +214,7 @@ const capabilitiesLayer = Layer.effectContext( |
| 220 | if (bridge === undefined) { | 214 | if (bridge === undefined) { |
| 221 | return yield* new ConnectionBlockedError({ | 215 | return yield* new ConnectionBlockedError({ |
| 222 | reason: "unsupported", | 216 | reason: "unsupported", |
| 223 | message: "SSH environments are only available in the desktop app.", | 217 | detail: "SSH environments are only available in the desktop app.", |
| 224 | }); | 218 | }); |
| 225 | } | 219 | } |
| 226 | const bootstrap = yield* Effect.tryPromise({ | 220 | const bootstrap = yield* Effect.tryPromise({ |
| ... | @@ -233,7 +227,7 @@ const capabilitiesLayer = Layer.effectContext( | ... | @@ -233,7 +227,7 @@ const capabilitiesLayer = Layer.effectContext( |
| 233 | if (bootstrap.pairingToken === null) { | 227 | if (bootstrap.pairingToken === null) { |
| 234 | return yield* new ConnectionBlockedError({ | 228 | return yield* new ConnectionBlockedError({ |
| 235 | reason: "authentication", | 229 | reason: "authentication", |
| 236 | message: "The SSH environment did not issue a pairing credential.", | 230 | detail: "The SSH environment did not issue a pairing credential.", |
| 237 | }); | 231 | }); |
| 238 | } | 232 | } |
| 239 | const access = yield* Effect.tryPromise({ | 233 | const access = yield* Effect.tryPromise({ |
| ... | @@ -256,7 +250,7 @@ const capabilitiesLayer = Layer.effectContext( | ... | @@ -256,7 +250,7 @@ const capabilitiesLayer = Layer.effectContext( |
| 256 | catch: (cause) => | 250 | catch: (cause) => |
| 257 | new ConnectionTransientError({ | 251 | new ConnectionTransientError({ |
| 258 | reason: "remote-unavailable", | 252 | reason: "remote-unavailable", |
| 259 | message: `Could not disconnect the SSH environment: ${String(cause)}`, | 253 | detail: `Could not disconnect the SSH environment: ${String(cause)}`, |
| 260 | }), | 254 | }), |
| 261 | }); | 255 | }); |
| 262 | }), | 256 | }), |
| ... | @@ -278,7 +272,7 @@ const loadPrimaryConnectionRegistration = Effect.fn( | ... | @@ -278,7 +272,7 @@ const loadPrimaryConnectionRegistration = Effect.fn( |
| 278 | if (resolved === null) { | 272 | if (resolved === null) { |
| 279 | return yield* new ConnectionBlockedError({ | 273 | return yield* new ConnectionBlockedError({ |
| 280 | reason: "configuration", | 274 | reason: "configuration", |
| 281 | message: "Unable to resolve the primary environment endpoint.", | 275 | detail: "Unable to resolve the primary environment endpoint.", |
| 282 | }); | 276 | }); |
| 283 | } | 277 | } |
| 284 | const descriptor = yield* fetchRemoteEnvironmentDescriptor({ | 278 | const descriptor = yield* fetchRemoteEnvironmentDescriptor({ |
| ... | @@ -342,7 +336,20 @@ const rpcRequestObserverLayer = Layer.succeed( | ... | @@ -342,7 +336,20 @@ const rpcRequestObserverLayer = Layer.succeed( |
| 342 | }), | 336 | }), |
| 343 | ); | 337 | ); |
| 344 | 338 | ||
| 345 | export const connectionPlatformLayer = Layer.mergeAll( | 339 | type ConnectionPlatformLayerSource = |
| 340 | | typeof connectionStorageLayer | ||
| 341 | | typeof connectivityLayer | ||
| 342 | | typeof wakeupsLayer | ||
| 343 | | typeof capabilitiesLayer | ||
| 344 | | typeof platformConnectionSourceLayer | ||
| 345 | | typeof environmentOwnedDataCleanupLayer | ||
| 346 | | typeof rpcRequestObserverLayer; | ||
| 347 | |||
| 348 | export const connectionPlatformLayer: Layer.Layer< | ||
| 349 | Layer.Success<ConnectionPlatformLayerSource>, | ||
| 350 | Layer.Error<ConnectionPlatformLayerSource>, | ||
| 351 | Layer.Services<ConnectionPlatformLayerSource> | ||
| 352 | > = Layer.mergeAll( | ||
| 346 | connectionStorageLayer, | 353 | connectionStorageLayer, |
| 347 | connectivityLayer, | 354 | connectivityLayer, |
| 348 | wakeupsLayer, | 355 | wakeupsLayer, |
apps/web/src/connection/runtime.ts+18-6| ... | @@ -1,16 +1,28 @@ | ... | @@ -1,16 +1,28 @@ |
| 1 | import { connectionLayer as clientConnectionLayer } from "@t3tools/client-runtime/connection"; | 1 | import { Connection } from "@t3tools/client-runtime/connection"; |
| 2 | import * as Layer from "effect/Layer"; | 2 | import * as Layer from "effect/Layer"; |
| 3 | import { Atom } from "effect/unstable/reactivity"; | 3 | import { Atom } from "effect/unstable/reactivity"; |
| 4 | 4 | ||
| 5 | import { runtimeContextLayer } from "../lib/runtime"; | 5 | import { runtimeContextLayer } from "../lib/runtime"; |
| 6 | import { connectionPlatformLayer } from "./platform"; | 6 | import { connectionPlatformLayer } from "./platform"; |
| 7 | 7 | ||
| 8 | const providedConnectionPlatformLayer = connectionPlatformLayer.pipe( | 8 | const providedConnectionPlatformLayer: Layer.Layer< |
| 9 | Layer.provide(runtimeContextLayer), | 9 | Layer.Success<typeof connectionPlatformLayer>, |
| 10 | ); | 10 | Layer.Error<typeof connectionPlatformLayer> |
| 11 | > = connectionPlatformLayer.pipe(Layer.provide(runtimeContextLayer)); | ||
| 12 | |||
| 13 | type ConnectionLayerSource = | ||
| 14 | | typeof Connection.layer | ||
| 15 | | typeof runtimeContextLayer | ||
| 16 | | typeof providedConnectionPlatformLayer; | ||
| 11 | 17 | ||
| 12 | export const connectionLayer = clientConnectionLayer.pipe( | 18 | export const connectionLayer: Layer.Layer< |
| 19 | Layer.Success<ConnectionLayerSource>, | ||
| 20 | Layer.Error<ConnectionLayerSource> | ||
| 21 | > = Connection.layer.pipe( | ||
| 13 | Layer.provideMerge(Layer.mergeAll(runtimeContextLayer, providedConnectionPlatformLayer)), | 22 | Layer.provideMerge(Layer.mergeAll(runtimeContextLayer, providedConnectionPlatformLayer)), |
| 14 | ); | 23 | ); |
| 15 | 24 | ||
| 16 | export const connectionAtomRuntime = Atom.runtime(connectionLayer); | 25 | export const connectionAtomRuntime: Atom.AtomRuntime< |
| 26 | Layer.Success<typeof connectionLayer>, | ||
| 27 | Layer.Error<typeof connectionLayer> | ||
| 28 | > = Atom.runtime(connectionLayer); |
apps/web/src/connection/storage.test.ts+1-1| ... | @@ -43,7 +43,7 @@ describe("makeCatalogStore", () => { | ... | @@ -43,7 +43,7 @@ describe("makeCatalogStore", () => { |
| 43 | Effect.gen(function* () { | 43 | Effect.gen(function* () { |
| 44 | const failure = new ConnectionTransientError({ | 44 | const failure = new ConnectionTransientError({ |
| 45 | reason: "remote-unavailable", | 45 | reason: "remote-unavailable", |
| 46 | message: "permission denied", | 46 | detail: "permission denied", |
| 47 | }); | 47 | }); |
| 48 | const store = yield* makeCatalogStore({ | 48 | const store = yield* makeCatalogStore({ |
| 49 | read: Effect.fail(failure), | 49 | read: Effect.fail(failure), |
apps/web/src/connection/storage.ts+10-10| ... | @@ -11,11 +11,11 @@ import { | ... | @@ -11,11 +11,11 @@ import { |
| 11 | removeConnectionFromCatalog, | 11 | removeConnectionFromCatalog, |
| 12 | replaceCatalogValue, | 12 | replaceCatalogValue, |
| 13 | } from "@t3tools/client-runtime/platform"; | 13 | } from "@t3tools/client-runtime/platform"; |
| 14 | import { RemoteDpopAccessTokenStore } from "@t3tools/client-runtime/authorization"; | 14 | import { TokenStore } from "@t3tools/client-runtime/authorization"; |
| 15 | import { | 15 | import { |
| 16 | ConnectionCredentialStore, | ||
| 17 | ConnectionProfileStore, | ||
| 18 | ConnectionTransientError, | 16 | ConnectionTransientError, |
| 17 | CredentialStore, | ||
| 18 | ProfileStore, | ||
| 19 | } from "@t3tools/client-runtime/connection"; | 19 | } from "@t3tools/client-runtime/connection"; |
| 20 | import { | 20 | import { |
| 21 | EnvironmentId, | 21 | EnvironmentId, |
| ... | @@ -63,7 +63,7 @@ const encodeStoredThreadSnapshot = Schema.encodeEffect(StoredThreadSnapshotJson) | ... | @@ -63,7 +63,7 @@ const encodeStoredThreadSnapshot = Schema.encodeEffect(StoredThreadSnapshotJson) |
| 63 | function catalogError(operation: string, cause: unknown) { | 63 | function catalogError(operation: string, cause: unknown) { |
| 64 | return new ConnectionTransientError({ | 64 | return new ConnectionTransientError({ |
| 65 | reason: "remote-unavailable", | 65 | reason: "remote-unavailable", |
| 66 | message: `Could not ${operation} the local connection catalog: ${String(cause)}`, | 66 | detail: `Could not ${operation} the local connection catalog: ${String(cause)}`, |
| 67 | }); | 67 | }); |
| 68 | } | 68 | } |
| 69 | 69 | ||
| ... | @@ -343,7 +343,7 @@ export const connectionStorageLayer = Layer.effectContext( | ... | @@ -343,7 +343,7 @@ export const connectionStorageLayer = Layer.effectContext( |
| 343 | .update((document) => removeConnectionFromCatalog(document, target)) | 343 | .update((document) => removeConnectionFromCatalog(document, target)) |
| 344 | .pipe(Effect.mapError((cause) => persistenceError("remove-connection", cause))), | 344 | .pipe(Effect.mapError((cause) => persistenceError("remove-connection", cause))), |
| 345 | }); | 345 | }); |
| 346 | const profileStore = ConnectionProfileStore.of({ | 346 | const profileStore = ProfileStore.make({ |
| 347 | get: (connectionId) => | 347 | get: (connectionId) => |
| 348 | catalog.read.pipe( | 348 | catalog.read.pipe( |
| 349 | Effect.map((document) => | 349 | Effect.map((document) => |
| ... | @@ -367,7 +367,7 @@ export const connectionStorageLayer = Layer.effectContext( | ... | @@ -367,7 +367,7 @@ export const connectionStorageLayer = Layer.effectContext( |
| 367 | ), | 367 | ), |
| 368 | })), | 368 | })), |
| 369 | }); | 369 | }); |
| 370 | const credentialStore = ConnectionCredentialStore.of({ | 370 | const credentialStore = CredentialStore.make({ |
| 371 | get: (connectionId) => | 371 | get: (connectionId) => |
| 372 | catalog.read.pipe( | 372 | catalog.read.pipe( |
| 373 | Effect.map((document) => | 373 | Effect.map((document) => |
| ... | @@ -394,7 +394,7 @@ export const connectionStorageLayer = Layer.effectContext( | ... | @@ -394,7 +394,7 @@ export const connectionStorageLayer = Layer.effectContext( |
| 394 | ), | 394 | ), |
| 395 | })), | 395 | })), |
| 396 | }); | 396 | }); |
| 397 | const remoteTokenStore = RemoteDpopAccessTokenStore.of({ | 397 | const remoteTokenStore = TokenStore.make({ |
| 398 | get: (environmentId) => | 398 | get: (environmentId) => |
| 399 | catalog.read.pipe( | 399 | catalog.read.pipe( |
| 400 | Effect.map((document) => | 400 | Effect.map((document) => |
| ... | @@ -527,9 +527,9 @@ export const connectionStorageLayer = Layer.effectContext( | ... | @@ -527,9 +527,9 @@ export const connectionStorageLayer = Layer.effectContext( |
| 527 | 527 | ||
| 528 | return Context.make(ConnectionTargetStore, targetStore).pipe( | 528 | return Context.make(ConnectionTargetStore, targetStore).pipe( |
| 529 | Context.add(ConnectionRegistrationStore, registrationStore), | 529 | Context.add(ConnectionRegistrationStore, registrationStore), |
| 530 | Context.add(ConnectionProfileStore, profileStore), | 530 | Context.add(ProfileStore.ConnectionProfileStore, profileStore), |
| 531 | Context.add(ConnectionCredentialStore, credentialStore), | 531 | Context.add(CredentialStore.ConnectionCredentialStore, credentialStore), |
| 532 | Context.add(RemoteDpopAccessTokenStore, remoteTokenStore), | 532 | Context.add(TokenStore.RemoteDpopAccessTokenStore, remoteTokenStore), |
| 533 | Context.add(EnvironmentCacheStore, cacheStore), | 533 | Context.add(EnvironmentCacheStore, cacheStore), |
| 534 | ); | 534 | ); |
| 535 | }), | 535 | }), |
packages/client-runtime/src/authorization/index.ts+6-3| ... | @@ -1,4 +1,7 @@ | ... | @@ -1,4 +1,7 @@ |
| 1 | export * from "./layer.ts"; | ||
| 2 | export * from "./remote.ts"; | 1 | export * from "./remote.ts"; |
| 3 | export * from "./service.ts"; | 2 | export { |
| 4 | export * from "./tokenStore.ts"; | 3 | type AuthorizedRemoteEnvironment, |
| 4 | type RelayEnvironmentAuthorization, | ||
| 5 | RemoteEnvironmentAuthorization, | ||
| 6 | } from "./service.ts"; | ||
| 7 | export * as TokenStore from "./tokenStore.ts"; |
packages/client-runtime/src/authorization/layer.test.ts+23-24| ... | @@ -5,12 +5,11 @@ import * as Layer from "effect/Layer"; | ... | @@ -5,12 +5,11 @@ import * as Layer from "effect/Layer"; |
| 5 | import * as Option from "effect/Option"; | 5 | import * as Option from "effect/Option"; |
| 6 | import * as Ref from "effect/Ref"; | 6 | import * as Ref from "effect/Ref"; |
| 7 | 7 | ||
| 8 | import { ManagedRelayDpopSigner, ManagedRelayDpopSignerError } from "../relay/managedRelay.ts"; | 8 | import * as ManagedRelay from "../relay/managedRelay.ts"; |
| 9 | import { remoteHttpClientLayer } from "../rpc/http.ts"; | 9 | import { remoteHttpClientLayer } from "../rpc/http.ts"; |
| 10 | import { ClientPresentation } from "../platform/capabilities.ts"; | 10 | import * as ClientCapabilities from "../platform/capabilities.ts"; |
| 11 | import { RemoteEnvironmentAuthorization, type RelayEnvironmentAuthorization } from "./service.ts"; | 11 | import * as RemoteEnvironmentAuthorization from "./service.ts"; |
| 12 | import { RemoteDpopAccessToken, RemoteDpopAccessTokenStore } from "./tokenStore.ts"; | 12 | import * as TokenStore from "./tokenStore.ts"; |
| 13 | import { remoteEnvironmentAuthorizationLayer } from "./layer.ts"; | ||
| 14 | 13 | ||
| 15 | const ENVIRONMENT_ID = EnvironmentId.make("environment-1"); | 14 | const ENVIRONMENT_ID = EnvironmentId.make("environment-1"); |
| 16 | const ENDPOINT = { | 15 | const ENDPOINT = { |
| ... | @@ -30,7 +29,7 @@ const DESCRIPTOR = { | ... | @@ -30,7 +29,7 @@ const DESCRIPTOR = { |
| 30 | repositoryIdentity: true, | 29 | repositoryIdentity: true, |
| 31 | }, | 30 | }, |
| 32 | }; | 31 | }; |
| 33 | const BOOTSTRAP: RelayEnvironmentAuthorization = { | 32 | const BOOTSTRAP: RemoteEnvironmentAuthorization.RelayEnvironmentAuthorization = { |
| 34 | environmentId: ENVIRONMENT_ID, | 33 | environmentId: ENVIRONMENT_ID, |
| 35 | endpoint: ENDPOINT, | 34 | endpoint: ENDPOINT, |
| 36 | credential: "relay-bootstrap", | 35 | credential: "relay-bootstrap", |
| ... | @@ -76,7 +75,7 @@ const authInvalid = () => | ... | @@ -76,7 +75,7 @@ const authInvalid = () => |
| 76 | ); | 75 | ); |
| 77 | 76 | ||
| 78 | const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* (input: { | 77 | const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* (input: { |
| 79 | readonly initialToken?: RemoteDpopAccessToken; | 78 | readonly initialToken?: TokenStore.RemoteDpopAccessToken; |
| 80 | readonly responses: ReadonlyArray<Response>; | 79 | readonly responses: ReadonlyArray<Response>; |
| 81 | }) { | 80 | }) { |
| 82 | const tokens = yield* Ref.make( | 81 | const tokens = yield* Ref.make( |
| ... | @@ -96,7 +95,7 @@ const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* ( | ... | @@ -96,7 +95,7 @@ const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* ( |
| 96 | >([]); | 95 | >([]); |
| 97 | const fetch = recordedFetch(input.responses); | 96 | const fetch = recordedFetch(input.responses); |
| 98 | 97 | ||
| 99 | const tokenStore = RemoteDpopAccessTokenStore.of({ | 98 | const tokenStore = TokenStore.RemoteDpopAccessTokenStore.of({ |
| 100 | get: (environmentId) => | 99 | get: (environmentId) => |
| 101 | Ref.get(tokens).pipe( | 100 | Ref.get(tokens).pipe( |
| 102 | Effect.map((current) => Option.fromUndefinedOr(current.get(environmentId))), | 101 | Effect.map((current) => Option.fromUndefinedOr(current.get(environmentId))), |
| ... | @@ -114,23 +113,23 @@ const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* ( | ... | @@ -114,23 +113,23 @@ const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* ( |
| 114 | return next; | 113 | return next; |
| 115 | }), | 114 | }), |
| 116 | }); | 115 | }); |
| 117 | const signer = ManagedRelayDpopSigner.of({ | 116 | const signer = ManagedRelay.ManagedRelayDpopSigner.of({ |
| 118 | thumbprint: Effect.succeed("thumbprint-1"), | 117 | thumbprint: Effect.succeed("thumbprint-1"), |
| 119 | createProof: (proofInput) => | 118 | createProof: (proofInput) => |
| 120 | Ref.update(proofInputs, (current) => [...current, proofInput]).pipe( | 119 | Ref.update(proofInputs, (current) => [...current, proofInput]).pipe( |
| 121 | Effect.as(`proof:${proofInput.url}`), | 120 | Effect.as(`proof:${proofInput.url}`), |
| 122 | Effect.mapError((cause) => new ManagedRelayDpopSignerError({ cause })), | 121 | Effect.mapError((cause) => new ManagedRelay.ManagedRelayDpopSignerError({ cause })), |
| 123 | ), | 122 | ), |
| 124 | }); | 123 | }); |
| 125 | const layer = remoteEnvironmentAuthorizationLayer.pipe( | 124 | const layer = RemoteEnvironmentAuthorization.layer.pipe( |
| 126 | Layer.provide( | 125 | Layer.provide( |
| 127 | Layer.mergeAll( | 126 | Layer.mergeAll( |
| 128 | remoteHttpClientLayer(fetch.fetchFn), | 127 | remoteHttpClientLayer(fetch.fetchFn), |
| 129 | Layer.succeed(ManagedRelayDpopSigner, signer), | 128 | Layer.succeed(ManagedRelay.ManagedRelayDpopSigner, signer), |
| 130 | Layer.succeed(RemoteDpopAccessTokenStore, tokenStore), | 129 | Layer.succeed(TokenStore.RemoteDpopAccessTokenStore, tokenStore), |
| 131 | Layer.succeed( | 130 | Layer.succeed( |
| 132 | ClientPresentation, | 131 | ClientCapabilities.ClientPresentation, |
| 133 | ClientPresentation.of({ | 132 | ClientCapabilities.ClientPresentation.of({ |
| 134 | metadata: { | 133 | metadata: { |
| 135 | label: "T3 Code Test", | 134 | label: "T3 Code Test", |
| 136 | deviceType: "mobile", | 135 | deviceType: "mobile", |
| ... | @@ -159,7 +158,7 @@ const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* ( | ... | @@ -159,7 +158,7 @@ const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* ( |
| 159 | describe("RemoteEnvironmentAuthorization", () => { | 158 | describe("RemoteEnvironmentAuthorization", () => { |
| 160 | it.effect("reuses a valid persisted environment token without contacting the relay", () => | 159 | it.effect("reuses a valid persisted environment token without contacting the relay", () => |
| 161 | Effect.gen(function* () { | 160 | Effect.gen(function* () { |
| 162 | const cached = new RemoteDpopAccessToken({ | 161 | const cached = new TokenStore.RemoteDpopAccessToken({ |
| 163 | environmentId: ENVIRONMENT_ID, | 162 | environmentId: ENVIRONMENT_ID, |
| 164 | label: DESCRIPTOR.label, | 163 | label: DESCRIPTOR.label, |
| 165 | endpoint: ENDPOINT, | 164 | endpoint: ENDPOINT, |
| ... | @@ -173,7 +172,7 @@ describe("RemoteEnvironmentAuthorization", () => { | ... | @@ -173,7 +172,7 @@ describe("RemoteEnvironmentAuthorization", () => { |
| 173 | }); | 172 | }); |
| 174 | 173 | ||
| 175 | const authorized = yield* Effect.gen(function* () { | 174 | const authorized = yield* Effect.gen(function* () { |
| 176 | const remote = yield* RemoteEnvironmentAuthorization; | 175 | const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; |
| 177 | return yield* remote.authorizeDpop({ | 176 | return yield* remote.authorizeDpop({ |
| 178 | expectedEnvironmentId: ENVIRONMENT_ID, | 177 | expectedEnvironmentId: ENVIRONMENT_ID, |
| 179 | obtainBootstrap: harness.obtainBootstrap, | 178 | obtainBootstrap: harness.obtainBootstrap, |
| ... | @@ -191,7 +190,7 @@ describe("RemoteEnvironmentAuthorization", () => { | ... | @@ -191,7 +190,7 @@ describe("RemoteEnvironmentAuthorization", () => { |
| 191 | 190 | ||
| 192 | it.effect("refreshes and persists an expired environment token", () => | 191 | it.effect("refreshes and persists an expired environment token", () => |
| 193 | Effect.gen(function* () { | 192 | Effect.gen(function* () { |
| 194 | const expired = new RemoteDpopAccessToken({ | 193 | const expired = new TokenStore.RemoteDpopAccessToken({ |
| 195 | environmentId: ENVIRONMENT_ID, | 194 | environmentId: ENVIRONMENT_ID, |
| 196 | label: DESCRIPTOR.label, | 195 | label: DESCRIPTOR.label, |
| 197 | endpoint: ENDPOINT, | 196 | endpoint: ENDPOINT, |
| ... | @@ -209,7 +208,7 @@ describe("RemoteEnvironmentAuthorization", () => { | ... | @@ -209,7 +208,7 @@ describe("RemoteEnvironmentAuthorization", () => { |
| 209 | }); | 208 | }); |
| 210 | 209 | ||
| 211 | const authorized = yield* Effect.gen(function* () { | 210 | const authorized = yield* Effect.gen(function* () { |
| 212 | const remote = yield* RemoteEnvironmentAuthorization; | 211 | const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; |
| 213 | return yield* remote.authorizeDpop({ | 212 | return yield* remote.authorizeDpop({ |
| 214 | expectedEnvironmentId: ENVIRONMENT_ID, | 213 | expectedEnvironmentId: ENVIRONMENT_ID, |
| 215 | obtainBootstrap: harness.obtainBootstrap, | 214 | obtainBootstrap: harness.obtainBootstrap, |
| ... | @@ -230,7 +229,7 @@ describe("RemoteEnvironmentAuthorization", () => { | ... | @@ -230,7 +229,7 @@ describe("RemoteEnvironmentAuthorization", () => { |
| 230 | 229 | ||
| 231 | it.effect("evicts an auth-invalid cached token and obtains a fresh bootstrap", () => | 230 | it.effect("evicts an auth-invalid cached token and obtains a fresh bootstrap", () => |
| 232 | Effect.gen(function* () { | 231 | Effect.gen(function* () { |
| 233 | const cached = new RemoteDpopAccessToken({ | 232 | const cached = new TokenStore.RemoteDpopAccessToken({ |
| 234 | environmentId: ENVIRONMENT_ID, | 233 | environmentId: ENVIRONMENT_ID, |
| 235 | label: DESCRIPTOR.label, | 234 | label: DESCRIPTOR.label, |
| 236 | endpoint: ENDPOINT, | 235 | endpoint: ENDPOINT, |
| ... | @@ -249,7 +248,7 @@ describe("RemoteEnvironmentAuthorization", () => { | ... | @@ -249,7 +248,7 @@ describe("RemoteEnvironmentAuthorization", () => { |
| 249 | }); | 248 | }); |
| 250 | 249 | ||
| 251 | const authorized = yield* Effect.gen(function* () { | 250 | const authorized = yield* Effect.gen(function* () { |
| 252 | const remote = yield* RemoteEnvironmentAuthorization; | 251 | const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; |
| 253 | return yield* remote.authorizeDpop({ | 252 | return yield* remote.authorizeDpop({ |
| 254 | expectedEnvironmentId: ENVIRONMENT_ID, | 253 | expectedEnvironmentId: ENVIRONMENT_ID, |
| 255 | obtainBootstrap: harness.obtainBootstrap, | 254 | obtainBootstrap: harness.obtainBootstrap, |
| ... | @@ -269,7 +268,7 @@ describe("RemoteEnvironmentAuthorization", () => { | ... | @@ -269,7 +268,7 @@ describe("RemoteEnvironmentAuthorization", () => { |
| 269 | 268 | ||
| 270 | it.effect("refreshes a cached endpoint after consecutive transient failures", () => | 269 | it.effect("refreshes a cached endpoint after consecutive transient failures", () => |
| 271 | Effect.gen(function* () { | 270 | Effect.gen(function* () { |
| 272 | const cached = new RemoteDpopAccessToken({ | 271 | const cached = new TokenStore.RemoteDpopAccessToken({ |
| 273 | environmentId: ENVIRONMENT_ID, | 272 | environmentId: ENVIRONMENT_ID, |
| 274 | label: DESCRIPTOR.label, | 273 | label: DESCRIPTOR.label, |
| 275 | endpoint: ENDPOINT, | 274 | endpoint: ENDPOINT, |
| ... | @@ -289,7 +288,7 @@ describe("RemoteEnvironmentAuthorization", () => { | ... | @@ -289,7 +288,7 @@ describe("RemoteEnvironmentAuthorization", () => { |
| 289 | }); | 288 | }); |
| 290 | 289 | ||
| 291 | const authorized = yield* Effect.gen(function* () { | 290 | const authorized = yield* Effect.gen(function* () { |
| 292 | const remote = yield* RemoteEnvironmentAuthorization; | 291 | const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; |
| 293 | const firstFailure = yield* remote | 292 | const firstFailure = yield* remote |
| 294 | .authorizeDpop({ | 293 | .authorizeDpop({ |
| 295 | expectedEnvironmentId: ENVIRONMENT_ID, | 294 | expectedEnvironmentId: ENVIRONMENT_ID, |
| ... | @@ -329,7 +328,7 @@ describe("RemoteEnvironmentAuthorization", () => { | ... | @@ -329,7 +328,7 @@ describe("RemoteEnvironmentAuthorization", () => { |
| 329 | }); | 328 | }); |
| 330 | 329 | ||
| 331 | yield* Effect.gen(function* () { | 330 | yield* Effect.gen(function* () { |
| 332 | const remote = yield* RemoteEnvironmentAuthorization; | 331 | const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; |
| 333 | return yield* remote.authorizeDpop({ | 332 | return yield* remote.authorizeDpop({ |
| 334 | expectedEnvironmentId: ENVIRONMENT_ID, | 333 | expectedEnvironmentId: ENVIRONMENT_ID, |
| 335 | obtainBootstrap: harness.obtainBootstrap, | 334 | obtainBootstrap: harness.obtainBootstrap, |
packages/client-runtime/src/authorization/layer.ts deleted-268| ... | @@ -1,268 +0,0 @@ | ||
| 1 | import { | ||
| 2 | exchangeRemoteDpopAccessToken, | ||
| 3 | type RemoteEnvironmentAuthError, | ||
| 4 | resolveRemoteDpopWebSocketConnectionUrl, | ||
| 5 | resolveRemoteWebSocketConnectionUrl, | ||
| 6 | } from "./remote.ts"; | ||
| 7 | import { environmentMismatchError, mapRemoteEnvironmentError } from "../connection/errors.ts"; | ||
| 8 | import { ConnectionBlockedError, type ConnectionAttemptError } from "../connection/model.ts"; | ||
| 9 | import { fetchRemoteEnvironmentDescriptor } from "../environment/descriptor.ts"; | ||
| 10 | import { environmentEndpointUrl } from "../environment/endpoint.ts"; | ||
| 11 | import { ClientPresentation } from "../platform/capabilities.ts"; | ||
| 12 | import { ManagedRelayDpopSigner } from "../relay/managedRelay.ts"; | ||
| 13 | import { RemoteEnvironmentAuthorization } from "./service.ts"; | ||
| 14 | import { RemoteDpopAccessToken, RemoteDpopAccessTokenStore } from "./tokenStore.ts"; | ||
| 15 | import * as Clock from "effect/Clock"; | ||
| 16 | import * as Effect from "effect/Effect"; | ||
| 17 | import * as Layer from "effect/Layer"; | ||
| 18 | import * as Option from "effect/Option"; | ||
| 19 | import * as Ref from "effect/Ref"; | ||
| 20 | import * as Result from "effect/Result"; | ||
| 21 | import { HttpClient } from "effect/unstable/http"; | ||
| 22 | |||
| 23 | const TOKEN_EXPIRY_SAFETY_MARGIN_MS = 60_000; | ||
| 24 | const CACHED_ENDPOINT_FAILURE_THRESHOLD = 2; | ||
| 25 | |||
| 26 | function mapDpopSocketError(error: RemoteEnvironmentAuthError | ConnectionAttemptError) { | ||
| 27 | return error._tag === "ConnectionTransientError" || error._tag === "ConnectionBlockedError" | ||
| 28 | ? error | ||
| 29 | : mapRemoteEnvironmentError(error); | ||
| 30 | } | ||
| 31 | |||
| 32 | const fetchDescriptor = Effect.fn("clientRuntime.connection.remote.fetchDescriptor")(function* ( | ||
| 33 | httpBaseUrl: string, | ||
| 34 | ) { | ||
| 35 | return yield* fetchRemoteEnvironmentDescriptor({ httpBaseUrl }).pipe( | ||
| 36 | Effect.mapError(mapRemoteEnvironmentError), | ||
| 37 | ); | ||
| 38 | }); | ||
| 39 | |||
| 40 | export const remoteEnvironmentAuthorizationLayer = Layer.effect( | ||
| 41 | RemoteEnvironmentAuthorization, | ||
| 42 | Effect.gen(function* () { | ||
| 43 | const signer = yield* ManagedRelayDpopSigner; | ||
| 44 | const presentation = yield* ClientPresentation; | ||
| 45 | const tokenStore = yield* RemoteDpopAccessTokenStore; | ||
| 46 | const httpClient = yield* HttpClient.HttpClient; | ||
| 47 | const cachedEndpointFailures = yield* Ref.make<ReadonlyMap<string, number>>(new Map()); | ||
| 48 | |||
| 49 | const resetCachedEndpointFailures = (environmentId: string) => | ||
| 50 | Ref.update(cachedEndpointFailures, (current) => { | ||
| 51 | if (!current.has(environmentId)) { | ||
| 52 | return current; | ||
| 53 | } | ||
| 54 | const next = new Map(current); | ||
| 55 | next.delete(environmentId); | ||
| 56 | return next; | ||
| 57 | }); | ||
| 58 | |||
| 59 | const recordCachedEndpointFailure = (environmentId: string) => | ||
| 60 | Ref.modify(cachedEndpointFailures, (current) => { | ||
| 61 | const failureCount = (current.get(environmentId) ?? 0) + 1; | ||
| 62 | const next = new Map(current); | ||
| 63 | next.set(environmentId, failureCount); | ||
| 64 | return [failureCount, next] as const; | ||
| 65 | }); | ||
| 66 | |||
| 67 | const authorizeBearer = Effect.fn("clientRuntime.connection.remote.authorizeBearer")( | ||
| 68 | function* (input: { | ||
| 69 | readonly expectedEnvironmentId: Parameters< | ||
| 70 | RemoteEnvironmentAuthorization["Service"]["authorizeBearer"] | ||
| 71 | >[0]["expectedEnvironmentId"]; | ||
| 72 | readonly httpBaseUrl: string; | ||
| 73 | readonly wsBaseUrl: string; | ||
| 74 | readonly bearerToken: string; | ||
| 75 | }) { | ||
| 76 | const descriptor = yield* fetchDescriptor(input.httpBaseUrl).pipe( | ||
| 77 | Effect.provideService(HttpClient.HttpClient, httpClient), | ||
| 78 | ); | ||
| 79 | if (descriptor.environmentId !== input.expectedEnvironmentId) { | ||
| 80 | return yield* environmentMismatchError({ | ||
| 81 | expected: input.expectedEnvironmentId, | ||
| 82 | actual: descriptor.environmentId, | ||
| 83 | }); | ||
| 84 | } | ||
| 85 | const socketUrl = yield* resolveRemoteWebSocketConnectionUrl({ | ||
| 86 | wsBaseUrl: input.wsBaseUrl, | ||
| 87 | httpBaseUrl: input.httpBaseUrl, | ||
| 88 | bearerToken: input.bearerToken, | ||
| 89 | }).pipe( | ||
| 90 | Effect.mapError(mapRemoteEnvironmentError), | ||
| 91 | Effect.provideService(HttpClient.HttpClient, httpClient), | ||
| 92 | ); | ||
| 93 | return { | ||
| 94 | environmentId: descriptor.environmentId, | ||
| 95 | label: descriptor.label, | ||
| 96 | httpBaseUrl: input.httpBaseUrl, | ||
| 97 | socketUrl, | ||
| 98 | httpAuthorization: { | ||
| 99 | _tag: "Bearer" as const, | ||
| 100 | token: input.bearerToken, | ||
| 101 | }, | ||
| 102 | }; | ||
| 103 | }, | ||
| 104 | ); | ||
| 105 | |||
| 106 | const createDpopSocketUrl = Effect.fn("clientRuntime.connection.remote.createDpopSocketUrl")( | ||
| 107 | function* (token: RemoteDpopAccessToken) { | ||
| 108 | const ticketProof = yield* signer | ||
| 109 | .createProof({ | ||
| 110 | method: "POST", | ||
| 111 | url: environmentEndpointUrl(token.endpoint.httpBaseUrl, "/api/auth/websocket-ticket"), | ||
| 112 | accessToken: token.accessToken, | ||
| 113 | }) | ||
| 114 | .pipe( | ||
| 115 | Effect.mapError( | ||
| 116 | () => | ||
| 117 | new ConnectionBlockedError({ | ||
| 118 | reason: "configuration", | ||
| 119 | message: "Could not create the websocket authorization proof.", | ||
| 120 | }), | ||
| 121 | ), | ||
| 122 | ); | ||
| 123 | return yield* resolveRemoteDpopWebSocketConnectionUrl({ | ||
| 124 | wsBaseUrl: token.endpoint.wsBaseUrl, | ||
| 125 | httpBaseUrl: token.endpoint.httpBaseUrl, | ||
| 126 | accessToken: token.accessToken, | ||
| 127 | dpopProof: ticketProof, | ||
| 128 | }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient)); | ||
| 129 | }, | ||
| 130 | ); | ||
| 131 | |||
| 132 | const authorizeDpop = Effect.fn("clientRuntime.connection.remote.authorizeDpop")( | ||
| 133 | function* (input: { | ||
| 134 | readonly expectedEnvironmentId: Parameters< | ||
| 135 | RemoteEnvironmentAuthorization["Service"]["authorizeDpop"] | ||
| 136 | >[0]["expectedEnvironmentId"]; | ||
| 137 | readonly obtainBootstrap: Parameters< | ||
| 138 | RemoteEnvironmentAuthorization["Service"]["authorizeDpop"] | ||
| 139 | >[0]["obtainBootstrap"]; | ||
| 140 | }) { | ||
| 141 | const thumbprint = yield* signer.thumbprint.pipe( | ||
| 142 | Effect.mapError( | ||
| 143 | () => | ||
| 144 | new ConnectionBlockedError({ | ||
| 145 | reason: "configuration", | ||
| 146 | message: "Could not load the environment authorization key.", | ||
| 147 | }), | ||
| 148 | ), | ||
| 149 | Effect.withSpan("environment.authorization.dpopKey.resolve"), | ||
| 150 | ); | ||
| 151 | const now = yield* Clock.currentTimeMillis; | ||
| 152 | const cached = yield* tokenStore | ||
| 153 | .get(input.expectedEnvironmentId) | ||
| 154 | .pipe(Effect.withSpan("environment.authorization.accessToken.cache")); | ||
| 155 | if ( | ||
| 156 | Option.isSome(cached) && | ||
| 157 | cached.value.environmentId === input.expectedEnvironmentId && | ||
| 158 | cached.value.dpopThumbprint === thumbprint && | ||
| 159 | cached.value.expiresAtEpochMs > now + TOKEN_EXPIRY_SAFETY_MARGIN_MS | ||
| 160 | ) { | ||
| 161 | yield* Effect.annotateCurrentSpan({ | ||
| 162 | "connection.remote_token_cache": "hit", | ||
| 163 | }); | ||
| 164 | const cachedSocket = yield* createDpopSocketUrl(cached.value).pipe(Effect.result); | ||
| 165 | if (Result.isSuccess(cachedSocket)) { | ||
| 166 | yield* resetCachedEndpointFailures(input.expectedEnvironmentId); | ||
| 167 | return { | ||
| 168 | environmentId: cached.value.environmentId, | ||
| 169 | label: cached.value.label, | ||
| 170 | httpBaseUrl: cached.value.endpoint.httpBaseUrl, | ||
| 171 | socketUrl: cachedSocket.success, | ||
| 172 | httpAuthorization: { | ||
| 173 | _tag: "Dpop" as const, | ||
| 174 | accessToken: cached.value.accessToken, | ||
| 175 | }, | ||
| 176 | }; | ||
| 177 | } | ||
| 178 | if (cachedSocket.failure._tag === "ConnectionBlockedError") { | ||
| 179 | return yield* mapDpopSocketError(cachedSocket.failure); | ||
| 180 | } | ||
| 181 | const mappedFailure = mapDpopSocketError(cachedSocket.failure); | ||
| 182 | if (mappedFailure._tag === "ConnectionTransientError") { | ||
| 183 | const failureCount = yield* recordCachedEndpointFailure(input.expectedEnvironmentId); | ||
| 184 | if (failureCount < CACHED_ENDPOINT_FAILURE_THRESHOLD) { | ||
| 185 | return yield* mappedFailure; | ||
| 186 | } | ||
| 187 | } | ||
| 188 | yield* tokenStore | ||
| 189 | .remove(input.expectedEnvironmentId) | ||
| 190 | .pipe(Effect.withSpan("environment.authorization.accessToken.remove")); | ||
| 191 | yield* resetCachedEndpointFailures(input.expectedEnvironmentId); | ||
| 192 | } | ||
| 193 | |||
| 194 | yield* resetCachedEndpointFailures(input.expectedEnvironmentId); | ||
| 195 | yield* Effect.annotateCurrentSpan({ | ||
| 196 | "connection.remote_token_cache": "miss", | ||
| 197 | }); | ||
| 198 | const bootstrap = yield* input.obtainBootstrap; | ||
| 199 | const descriptor = yield* fetchDescriptor(bootstrap.endpoint.httpBaseUrl).pipe( | ||
| 200 | Effect.provideService(HttpClient.HttpClient, httpClient), | ||
| 201 | Effect.withSpan("environment.authorization.descriptor"), | ||
| 202 | ); | ||
| 203 | if (descriptor.environmentId !== input.expectedEnvironmentId) { | ||
| 204 | return yield* environmentMismatchError({ | ||
| 205 | expected: input.expectedEnvironmentId, | ||
| 206 | actual: descriptor.environmentId, | ||
| 207 | }); | ||
| 208 | } | ||
| 209 | const bootstrapProof = yield* signer | ||
| 210 | .createProof({ | ||
| 211 | method: "POST", | ||
| 212 | url: environmentEndpointUrl(bootstrap.endpoint.httpBaseUrl, "/oauth/token"), | ||
| 213 | }) | ||
| 214 | .pipe( | ||
| 215 | Effect.mapError( | ||
| 216 | () => | ||
| 217 | new ConnectionBlockedError({ | ||
| 218 | reason: "configuration", | ||
| 219 | message: "Could not create the environment authorization proof.", | ||
| 220 | }), | ||
| 221 | ), | ||
| 222 | ); | ||
| 223 | const access = yield* exchangeRemoteDpopAccessToken({ | ||
| 224 | httpBaseUrl: bootstrap.endpoint.httpBaseUrl, | ||
| 225 | credential: bootstrap.credential, | ||
| 226 | dpopProof: bootstrapProof, | ||
| 227 | scopes: presentation.scopes, | ||
| 228 | clientMetadata: presentation.metadata, | ||
| 229 | }).pipe( | ||
| 230 | Effect.mapError(mapRemoteEnvironmentError), | ||
| 231 | Effect.provideService(HttpClient.HttpClient, httpClient), | ||
| 232 | Effect.withSpan("environment.authorization.accessToken.exchange"), | ||
| 233 | ); | ||
| 234 | const issuedAt = yield* Clock.currentTimeMillis; | ||
| 235 | const token = new RemoteDpopAccessToken({ | ||
| 236 | environmentId: descriptor.environmentId, | ||
| 237 | label: descriptor.label, | ||
| 238 | endpoint: bootstrap.endpoint, | ||
| 239 | accessToken: access.access_token, | ||
| 240 | expiresAtEpochMs: issuedAt + access.expires_in * 1_000, | ||
| 241 | dpopThumbprint: thumbprint, | ||
| 242 | }); | ||
| 243 | const socketUrl = yield* createDpopSocketUrl(token).pipe( | ||
| 244 | Effect.mapError(mapDpopSocketError), | ||
| 245 | ); | ||
| 246 | yield* tokenStore | ||
| 247 | .put(token) | ||
| 248 | .pipe(Effect.withSpan("environment.authorization.accessToken.persist")); | ||
| 249 | return { | ||
| 250 | environmentId: descriptor.environmentId, | ||
| 251 | label: descriptor.label, | ||
| 252 | httpBaseUrl: bootstrap.endpoint.httpBaseUrl, | ||
| 253 | socketUrl, | ||
| 254 | httpAuthorization: { | ||
| 255 | _tag: "Dpop" as const, | ||
| 256 | accessToken: token.accessToken, | ||
| 257 | }, | ||
| 258 | }; | ||
| 259 | }, | ||
| 260 | ); | ||
| 261 | |||
| 262 | return RemoteEnvironmentAuthorization.of({ | ||
| 263 | authorizeBearer, | ||
| 264 | authorizeDpop: (input) => | ||
| 265 | authorizeDpop(input).pipe(Effect.withSpan("environment.authorization")), | ||
| 266 | }); | ||
| 267 | }), | ||
| 268 | ); | ||
packages/client-runtime/src/authorization/service.ts+265-2| ... | @@ -1,9 +1,28 @@ | ... | @@ -1,9 +1,28 @@ |
| 1 | import { EnvironmentId } from "@t3tools/contracts"; | 1 | import { EnvironmentId } from "@t3tools/contracts"; |
| 2 | import type { RelayManagedEndpoint } from "@t3tools/contracts/relay"; | 2 | import type { RelayManagedEndpoint } from "@t3tools/contracts/relay"; |
| 3 | import { | ||
| 4 | exchangeRemoteDpopAccessToken, | ||
| 5 | type RemoteEnvironmentAuthError, | ||
| 6 | resolveRemoteDpopWebSocketConnectionUrl, | ||
| 7 | resolveRemoteWebSocketConnectionUrl, | ||
| 8 | } from "./remote.ts"; | ||
| 9 | import { environmentMismatchError, mapRemoteEnvironmentError } from "../connection/errors.ts"; | ||
| 10 | import { ConnectionBlockedError, type ConnectionAttemptError } from "../connection/model.ts"; | ||
| 11 | import { fetchRemoteEnvironmentDescriptor } from "../environment/descriptor.ts"; | ||
| 12 | import { environmentEndpointUrl } from "../environment/endpoint.ts"; | ||
| 13 | import * as ClientCapabilities from "../platform/capabilities.ts"; | ||
| 14 | import * as ManagedRelay from "../relay/managedRelay.ts"; | ||
| 15 | import * as TokenStore from "./tokenStore.ts"; | ||
| 16 | import * as Clock from "effect/Clock"; | ||
| 3 | import * as Context from "effect/Context"; | 17 | import * as Context from "effect/Context"; |
| 4 | import type * as Effect from "effect/Effect"; | 18 | import * as Effect from "effect/Effect"; |
| 19 | import * as Layer from "effect/Layer"; | ||
| 20 | import * as Option from "effect/Option"; | ||
| 21 | import * as Ref from "effect/Ref"; | ||
| 22 | import * as Result from "effect/Result"; | ||
| 23 | import * as HttpClient from "effect/unstable/http/HttpClient"; | ||
| 5 | 24 | ||
| 6 | import type { ConnectionAttemptError, PreparedHttpAuthorization } from "../connection/model.ts"; | 25 | import type { PreparedHttpAuthorization } from "../connection/model.ts"; |
| 7 | 26 | ||
| 8 | export interface RelayEnvironmentAuthorization { | 27 | export interface RelayEnvironmentAuthorization { |
| 9 | readonly environmentId: EnvironmentId; | 28 | readonly environmentId: EnvironmentId; |
| ... | @@ -37,3 +56,247 @@ export class RemoteEnvironmentAuthorization extends Context.Service< | ... | @@ -37,3 +56,247 @@ export class RemoteEnvironmentAuthorization extends Context.Service< |
| 37 | }) => Effect.Effect<AuthorizedRemoteEnvironment, ConnectionAttemptError>; | 56 | }) => Effect.Effect<AuthorizedRemoteEnvironment, ConnectionAttemptError>; |
| 38 | } | 57 | } |
| 39 | >()("@t3tools/client-runtime/authorization/service/RemoteEnvironmentAuthorization") {} | 58 | >()("@t3tools/client-runtime/authorization/service/RemoteEnvironmentAuthorization") {} |
| 59 | |||
| 60 | const TOKEN_EXPIRY_SAFETY_MARGIN_MS = 60_000; | ||
| 61 | const CACHED_ENDPOINT_FAILURE_THRESHOLD = 2; | ||
| 62 | |||
| 63 | function mapDpopSocketError(error: RemoteEnvironmentAuthError | ConnectionAttemptError) { | ||
| 64 | return error._tag === "ConnectionTransientError" || error._tag === "ConnectionBlockedError" | ||
| 65 | ? error | ||
| 66 | : mapRemoteEnvironmentError(error); | ||
| 67 | } | ||
| 68 | |||
| 69 | const fetchDescriptor = Effect.fn("clientRuntime.connection.remote.fetchDescriptor")(function* ( | ||
| 70 | httpBaseUrl: string, | ||
| 71 | ) { | ||
| 72 | return yield* fetchRemoteEnvironmentDescriptor({ httpBaseUrl }).pipe( | ||
| 73 | Effect.mapError(mapRemoteEnvironmentError), | ||
| 74 | ); | ||
| 75 | }); | ||
| 76 | |||
| 77 | export const make = Effect.gen(function* () { | ||
| 78 | const signer = yield* ManagedRelay.ManagedRelayDpopSigner; | ||
| 79 | const presentation = yield* ClientCapabilities.ClientPresentation; | ||
| 80 | const tokenStore = yield* TokenStore.RemoteDpopAccessTokenStore; | ||
| 81 | const httpClient = yield* HttpClient.HttpClient; | ||
| 82 | const cachedEndpointFailures = yield* Ref.make<ReadonlyMap<string, number>>(new Map()); | ||
| 83 | |||
| 84 | const resetCachedEndpointFailures = (environmentId: string) => | ||
| 85 | Ref.update(cachedEndpointFailures, (current) => { | ||
| 86 | if (!current.has(environmentId)) { | ||
| 87 | return current; | ||
| 88 | } | ||
| 89 | const next = new Map(current); | ||
| 90 | next.delete(environmentId); | ||
| 91 | return next; | ||
| 92 | }); | ||
| 93 | |||
| 94 | const recordCachedEndpointFailure = (environmentId: string) => | ||
| 95 | Ref.modify(cachedEndpointFailures, (current) => { | ||
| 96 | const failureCount = (current.get(environmentId) ?? 0) + 1; | ||
| 97 | const next = new Map(current); | ||
| 98 | next.set(environmentId, failureCount); | ||
| 99 | return [failureCount, next] as const; | ||
| 100 | }); | ||
| 101 | |||
| 102 | const authorizeBearer = Effect.fn("clientRuntime.connection.remote.authorizeBearer")( | ||
| 103 | function* (input: { | ||
| 104 | readonly expectedEnvironmentId: Parameters< | ||
| 105 | RemoteEnvironmentAuthorization["Service"]["authorizeBearer"] | ||
| 106 | >[0]["expectedEnvironmentId"]; | ||
| 107 | readonly httpBaseUrl: string; | ||
| 108 | readonly wsBaseUrl: string; | ||
| 109 | readonly bearerToken: string; | ||
| 110 | }) { | ||
| 111 | const descriptor = yield* fetchDescriptor(input.httpBaseUrl).pipe( | ||
| 112 | Effect.provideService(HttpClient.HttpClient, httpClient), | ||
| 113 | ); | ||
| 114 | if (descriptor.environmentId !== input.expectedEnvironmentId) { | ||
| 115 | return yield* environmentMismatchError({ | ||
| 116 | expected: input.expectedEnvironmentId, | ||
| 117 | actual: descriptor.environmentId, | ||
| 118 | }); | ||
| 119 | } | ||
| 120 | const socketUrl = yield* resolveRemoteWebSocketConnectionUrl({ | ||
| 121 | wsBaseUrl: input.wsBaseUrl, | ||
| 122 | httpBaseUrl: input.httpBaseUrl, | ||
| 123 | bearerToken: input.bearerToken, | ||
| 124 | }).pipe( | ||
| 125 | Effect.mapError(mapRemoteEnvironmentError), | ||
| 126 | Effect.provideService(HttpClient.HttpClient, httpClient), | ||
| 127 | ); | ||
| 128 | return { | ||
| 129 | environmentId: descriptor.environmentId, | ||
| 130 | label: descriptor.label, | ||
| 131 | httpBaseUrl: input.httpBaseUrl, | ||
| 132 | socketUrl, | ||
| 133 | httpAuthorization: { | ||
| 134 | _tag: "Bearer" as const, | ||
| 135 | token: input.bearerToken, | ||
| 136 | }, | ||
| 137 | }; | ||
| 138 | }, | ||
| 139 | ); | ||
| 140 | |||
| 141 | const createDpopSocketUrl = Effect.fn("clientRuntime.connection.remote.createDpopSocketUrl")( | ||
| 142 | function* (token: TokenStore.RemoteDpopAccessToken) { | ||
| 143 | const ticketProof = yield* signer | ||
| 144 | .createProof({ | ||
| 145 | method: "POST", | ||
| 146 | url: environmentEndpointUrl(token.endpoint.httpBaseUrl, "/api/auth/websocket-ticket"), | ||
| 147 | accessToken: token.accessToken, | ||
| 148 | }) | ||
| 149 | .pipe( | ||
| 150 | Effect.mapError( | ||
| 151 | () => | ||
| 152 | new ConnectionBlockedError({ | ||
| 153 | reason: "configuration", | ||
| 154 | detail: "Could not create the websocket authorization proof.", | ||
| 155 | }), | ||
| 156 | ), | ||
| 157 | ); | ||
| 158 | return yield* resolveRemoteDpopWebSocketConnectionUrl({ | ||
| 159 | wsBaseUrl: token.endpoint.wsBaseUrl, | ||
| 160 | httpBaseUrl: token.endpoint.httpBaseUrl, | ||
| 161 | accessToken: token.accessToken, | ||
| 162 | dpopProof: ticketProof, | ||
| 163 | }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient)); | ||
| 164 | }, | ||
| 165 | ); | ||
| 166 | |||
| 167 | const authorizeDpop = Effect.fn("clientRuntime.connection.remote.authorizeDpop")( | ||
| 168 | function* (input: { | ||
| 169 | readonly expectedEnvironmentId: Parameters< | ||
| 170 | RemoteEnvironmentAuthorization["Service"]["authorizeDpop"] | ||
| 171 | >[0]["expectedEnvironmentId"]; | ||
| 172 | readonly obtainBootstrap: Parameters< | ||
| 173 | RemoteEnvironmentAuthorization["Service"]["authorizeDpop"] | ||
| 174 | >[0]["obtainBootstrap"]; | ||
| 175 | }) { | ||
| 176 | const thumbprint = yield* signer.thumbprint.pipe( | ||
| 177 | Effect.mapError( | ||
| 178 | () => | ||
| 179 | new ConnectionBlockedError({ | ||
| 180 | reason: "configuration", | ||
| 181 | detail: "Could not load the environment authorization key.", | ||
| 182 | }), | ||
| 183 | ), | ||
| 184 | Effect.withSpan("environment.authorization.dpopKey.resolve"), | ||
| 185 | ); | ||
| 186 | const now = yield* Clock.currentTimeMillis; | ||
| 187 | const cached = yield* tokenStore | ||
| 188 | .get(input.expectedEnvironmentId) | ||
| 189 | .pipe(Effect.withSpan("environment.authorization.accessToken.cache")); | ||
| 190 | if ( | ||
| 191 | Option.isSome(cached) && | ||
| 192 | cached.value.environmentId === input.expectedEnvironmentId && | ||
| 193 | cached.value.dpopThumbprint === thumbprint && | ||
| 194 | cached.value.expiresAtEpochMs > now + TOKEN_EXPIRY_SAFETY_MARGIN_MS | ||
| 195 | ) { | ||
| 196 | yield* Effect.annotateCurrentSpan({ | ||
| 197 | "connection.remote_token_cache": "hit", | ||
| 198 | }); | ||
| 199 | const cachedSocket = yield* createDpopSocketUrl(cached.value).pipe(Effect.result); | ||
| 200 | if (Result.isSuccess(cachedSocket)) { | ||
| 201 | yield* resetCachedEndpointFailures(input.expectedEnvironmentId); | ||
| 202 | return { | ||
| 203 | environmentId: cached.value.environmentId, | ||
| 204 | label: cached.value.label, | ||
| 205 | httpBaseUrl: cached.value.endpoint.httpBaseUrl, | ||
| 206 | socketUrl: cachedSocket.success, | ||
| 207 | httpAuthorization: { | ||
| 208 | _tag: "Dpop" as const, | ||
| 209 | accessToken: cached.value.accessToken, | ||
| 210 | }, | ||
| 211 | }; | ||
| 212 | } | ||
| 213 | if (cachedSocket.failure._tag === "ConnectionBlockedError") { | ||
| 214 | return yield* mapDpopSocketError(cachedSocket.failure); | ||
| 215 | } | ||
| 216 | const mappedFailure = mapDpopSocketError(cachedSocket.failure); | ||
| 217 | if (mappedFailure._tag === "ConnectionTransientError") { | ||
| 218 | const failureCount = yield* recordCachedEndpointFailure(input.expectedEnvironmentId); | ||
| 219 | if (failureCount < CACHED_ENDPOINT_FAILURE_THRESHOLD) { | ||
| 220 | return yield* mappedFailure; | ||
| 221 | } | ||
| 222 | } | ||
| 223 | yield* tokenStore | ||
| 224 | .remove(input.expectedEnvironmentId) | ||
| 225 | .pipe(Effect.withSpan("environment.authorization.accessToken.remove")); | ||
| 226 | yield* resetCachedEndpointFailures(input.expectedEnvironmentId); | ||
| 227 | } | ||
| 228 | |||
| 229 | yield* resetCachedEndpointFailures(input.expectedEnvironmentId); | ||
| 230 | yield* Effect.annotateCurrentSpan({ | ||
| 231 | "connection.remote_token_cache": "miss", | ||
| 232 | }); | ||
| 233 | const bootstrap = yield* input.obtainBootstrap; | ||
| 234 | const descriptor = yield* fetchDescriptor(bootstrap.endpoint.httpBaseUrl).pipe( | ||
| 235 | Effect.provideService(HttpClient.HttpClient, httpClient), | ||
| 236 | Effect.withSpan("environment.authorization.descriptor"), | ||
| 237 | ); | ||
| 238 | if (descriptor.environmentId !== input.expectedEnvironmentId) { | ||
| 239 | return yield* environmentMismatchError({ | ||
| 240 | expected: input.expectedEnvironmentId, | ||
| 241 | actual: descriptor.environmentId, | ||
| 242 | }); | ||
| 243 | } | ||
| 244 | const bootstrapProof = yield* signer | ||
| 245 | .createProof({ | ||
| 246 | method: "POST", | ||
| 247 | url: environmentEndpointUrl(bootstrap.endpoint.httpBaseUrl, "/oauth/token"), | ||
| 248 | }) | ||
| 249 | .pipe( | ||
| 250 | Effect.mapError( | ||
| 251 | () => | ||
| 252 | new ConnectionBlockedError({ | ||
| 253 | reason: "configuration", | ||
| 254 | detail: "Could not create the environment authorization proof.", | ||
| 255 | }), | ||
| 256 | ), | ||
| 257 | ); | ||
| 258 | const access = yield* exchangeRemoteDpopAccessToken({ | ||
| 259 | httpBaseUrl: bootstrap.endpoint.httpBaseUrl, | ||
| 260 | credential: bootstrap.credential, | ||
| 261 | dpopProof: bootstrapProof, | ||
| 262 | scopes: presentation.scopes, | ||
| 263 | clientMetadata: presentation.metadata, | ||
| 264 | }).pipe( | ||
| 265 | Effect.mapError(mapRemoteEnvironmentError), | ||
| 266 | Effect.provideService(HttpClient.HttpClient, httpClient), | ||
| 267 | Effect.withSpan("environment.authorization.accessToken.exchange"), | ||
| 268 | ); | ||
| 269 | const issuedAt = yield* Clock.currentTimeMillis; | ||
| 270 | const token = new TokenStore.RemoteDpopAccessToken({ | ||
| 271 | environmentId: descriptor.environmentId, | ||
| 272 | label: descriptor.label, | ||
| 273 | endpoint: bootstrap.endpoint, | ||
| 274 | accessToken: access.access_token, | ||
| 275 | expiresAtEpochMs: issuedAt + access.expires_in * 1_000, | ||
| 276 | dpopThumbprint: thumbprint, | ||
| 277 | }); | ||
| 278 | const socketUrl = yield* createDpopSocketUrl(token).pipe(Effect.mapError(mapDpopSocketError)); | ||
| 279 | yield* tokenStore | ||
| 280 | .put(token) | ||
| 281 | .pipe(Effect.withSpan("environment.authorization.accessToken.persist")); | ||
| 282 | return { | ||
| 283 | environmentId: descriptor.environmentId, | ||
| 284 | label: descriptor.label, | ||
| 285 | httpBaseUrl: bootstrap.endpoint.httpBaseUrl, | ||
| 286 | socketUrl, | ||
| 287 | httpAuthorization: { | ||
| 288 | _tag: "Dpop" as const, | ||
| 289 | accessToken: token.accessToken, | ||
| 290 | }, | ||
| 291 | }; | ||
| 292 | }, | ||
| 293 | ); | ||
| 294 | |||
| 295 | return RemoteEnvironmentAuthorization.of({ | ||
| 296 | authorizeBearer, | ||
| 297 | authorizeDpop: (input) => | ||
| 298 | authorizeDpop(input).pipe(Effect.withSpan("environment.authorization")), | ||
| 299 | }); | ||
| 300 | }); | ||
| 301 | |||
| 302 | export const layer = Layer.effect(RemoteEnvironmentAuthorization, make); |
packages/client-runtime/src/authorization/tokenStore.ts+7| ... | @@ -2,6 +2,7 @@ import { EnvironmentId } from "@t3tools/contracts"; | ... | @@ -2,6 +2,7 @@ import { EnvironmentId } from "@t3tools/contracts"; |
| 2 | import { RelayManagedEndpoint } from "@t3tools/contracts/relay"; | 2 | import { RelayManagedEndpoint } from "@t3tools/contracts/relay"; |
| 3 | import * as Context from "effect/Context"; | 3 | import * as Context from "effect/Context"; |
| 4 | import type * as Effect from "effect/Effect"; | 4 | import type * as Effect from "effect/Effect"; |
| 5 | import * as Layer from "effect/Layer"; | ||
| 5 | import type * as Option from "effect/Option"; | 6 | import type * as Option from "effect/Option"; |
| 6 | import * as Schema from "effect/Schema"; | 7 | import * as Schema from "effect/Schema"; |
| 7 | 8 | ||
| ... | @@ -28,3 +29,9 @@ export class RemoteDpopAccessTokenStore extends Context.Service< | ... | @@ -28,3 +29,9 @@ export class RemoteDpopAccessTokenStore extends Context.Service< |
| 28 | readonly remove: (environmentId: EnvironmentId) => Effect.Effect<void, ConnectionAttemptError>; | 29 | readonly remove: (environmentId: EnvironmentId) => Effect.Effect<void, ConnectionAttemptError>; |
| 29 | } | 30 | } |
| 30 | >()("@t3tools/client-runtime/authorization/tokenStore/RemoteDpopAccessTokenStore") {} | 31 | >()("@t3tools/client-runtime/authorization/tokenStore/RemoteDpopAccessTokenStore") {} |
| 32 | |||
| 33 | export const make = (service: RemoteDpopAccessTokenStore["Service"]) => | ||
| 34 | RemoteDpopAccessTokenStore.of(service); | ||
| 35 | |||
| 36 | export const layer = (service: RemoteDpopAccessTokenStore["Service"]) => | ||
| 37 | Layer.succeed(RemoteDpopAccessTokenStore, make(service)); |
packages/client-runtime/src/connection/catalog.ts-28| ... | @@ -1,10 +1,7 @@ | ... | @@ -1,10 +1,7 @@ |
| 1 | import { DesktopSshEnvironmentTargetSchema, EnvironmentId } from "@t3tools/contracts"; | 1 | import { DesktopSshEnvironmentTargetSchema, EnvironmentId } from "@t3tools/contracts"; |
| 2 | import * as Context from "effect/Context"; | ||
| 3 | import type * as Effect from "effect/Effect"; | ||
| 4 | import * as Option from "effect/Option"; | 2 | import * as Option from "effect/Option"; |
| 5 | import * as Schema from "effect/Schema"; | 3 | import * as Schema from "effect/Schema"; |
| 6 | 4 | ||
| 7 | import type { ConnectionAttemptError } from "./model.ts"; | ||
| 8 | import { | 5 | import { |
| 9 | BearerConnectionTarget, | 6 | BearerConnectionTarget, |
| 10 | PrimaryConnectionTarget, | 7 | PrimaryConnectionTarget, |
| ... | @@ -116,28 +113,3 @@ export function connectionRegistrationCatalogEntry( | ... | @@ -116,28 +113,3 @@ export function connectionRegistrationCatalogEntry( |
| 116 | }; | 113 | }; |
| 117 | } | 114 | } |
| 118 | } | 115 | } |
| 119 | |||
| 120 | export class ConnectionProfileStore extends Context.Service< | ||
| 121 | ConnectionProfileStore, | ||
| 122 | { | ||
| 123 | readonly get: ( | ||
| 124 | connectionId: string, | ||
| 125 | ) => Effect.Effect<Option.Option<ConnectionProfile>, ConnectionAttemptError>; | ||
| 126 | readonly put: (profile: ConnectionProfile) => Effect.Effect<void, ConnectionAttemptError>; | ||
| 127 | readonly remove: (connectionId: string) => Effect.Effect<void, ConnectionAttemptError>; | ||
| 128 | } | ||
| 129 | >()("@t3tools/client-runtime/connection/catalog/ConnectionProfileStore") {} | ||
| 130 | |||
| 131 | export class ConnectionCredentialStore extends Context.Service< | ||
| 132 | ConnectionCredentialStore, | ||
| 133 | { | ||
| 134 | readonly get: ( | ||
| 135 | connectionId: string, | ||
| 136 | ) => Effect.Effect<Option.Option<ConnectionCredential>, ConnectionAttemptError>; | ||
| 137 | readonly put: ( | ||
| 138 | connectionId: string, | ||
| 139 | credential: ConnectionCredential, | ||
| 140 | ) => Effect.Effect<void, ConnectionAttemptError>; | ||
| 141 | readonly remove: (connectionId: string) => Effect.Effect<void, ConnectionAttemptError>; | ||
| 142 | } | ||
| 143 | >()("@t3tools/client-runtime/connection/catalog/ConnectionCredentialStore") {} |
packages/client-runtime/src/connection/connectivity.ts+6| ... | @@ -1,5 +1,6 @@ | ... | @@ -1,5 +1,6 @@ |
| 1 | import * as Context from "effect/Context"; | 1 | import * as Context from "effect/Context"; |
| 2 | import type * as Effect from "effect/Effect"; | 2 | import type * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; | ||
| 3 | import type * as Stream from "effect/Stream"; | 4 | import type * as Stream from "effect/Stream"; |
| 4 | 5 | ||
| 5 | import type { NetworkStatus } from "./model.ts"; | 6 | import type { NetworkStatus } from "./model.ts"; |
| ... | @@ -11,3 +12,8 @@ export class Connectivity extends Context.Service< | ... | @@ -11,3 +12,8 @@ export class Connectivity extends Context.Service< |
| 11 | readonly changes: Stream.Stream<NetworkStatus>; | 12 | readonly changes: Stream.Stream<NetworkStatus>; |
| 12 | } | 13 | } |
| 13 | >()("@t3tools/client-runtime/connection/connectivity") {} | 14 | >()("@t3tools/client-runtime/connection/connectivity") {} |
| 15 | |||
| 16 | export const make = (service: Connectivity["Service"]) => Connectivity.of(service); | ||
| 17 | |||
| 18 | export const layer = (service: Connectivity["Service"]) => | ||
| 19 | Layer.succeed(Connectivity, make(service)); |
packages/client-runtime/src/connection/credentialStore.ts created+27| ... | @@ -0,0 +1,27 @@ | ||
| 1 | import * as Context from "effect/Context"; | ||
| 2 | import type * as Effect from "effect/Effect"; | ||
| 3 | import * as Layer from "effect/Layer"; | ||
| 4 | import type * as Option from "effect/Option"; | ||
| 5 | |||
| 6 | import type { ConnectionCredential } from "./catalog.ts"; | ||
| 7 | import type { ConnectionAttemptError } from "./model.ts"; | ||
| 8 | |||
| 9 | export class ConnectionCredentialStore extends Context.Service< | ||
| 10 | ConnectionCredentialStore, | ||
| 11 | { | ||
| 12 | readonly get: ( | ||
| 13 | connectionId: string, | ||
| 14 | ) => Effect.Effect<Option.Option<ConnectionCredential>, ConnectionAttemptError>; | ||
| 15 | readonly put: ( | ||
| 16 | connectionId: string, | ||
| 17 | credential: ConnectionCredential, | ||
| 18 | ) => Effect.Effect<void, ConnectionAttemptError>; | ||
| 19 | readonly remove: (connectionId: string) => Effect.Effect<void, ConnectionAttemptError>; | ||
| 20 | } | ||
| 21 | >()("@t3tools/client-runtime/connection/credentialStore/ConnectionCredentialStore") {} | ||
| 22 | |||
| 23 | export const make = (service: ConnectionCredentialStore["Service"]) => | ||
| 24 | ConnectionCredentialStore.of(service); | ||
| 25 | |||
| 26 | export const layer = (service: ConnectionCredentialStore["Service"]) => | ||
| 27 | Layer.succeed(ConnectionCredentialStore, make(service)); | ||
packages/client-runtime/src/connection/driver.ts+34-36| ... | @@ -9,8 +9,8 @@ import type { | ... | @@ -9,8 +9,8 @@ import type { |
| 9 | ConnectionAttemptStage, | 9 | ConnectionAttemptStage, |
| 10 | PreparedConnection, | 10 | PreparedConnection, |
| 11 | } from "./model.ts"; | 11 | } from "./model.ts"; |
| 12 | import { ConnectionResolver } from "./resolver.ts"; | 12 | import * as ConnectionResolver from "./resolver.ts"; |
| 13 | import { RpcSessionFactory, type RpcSession } from "../rpc/session.ts"; | 13 | import * as RpcSession from "../rpc/session.ts"; |
| 14 | 14 | ||
| 15 | export type ConnectionDriverProgress = | 15 | export type ConnectionDriverProgress = |
| 16 | | { | 16 | | { |
| ... | @@ -23,44 +23,42 @@ export type ConnectionDriverProgress = | ... | @@ -23,44 +23,42 @@ export type ConnectionDriverProgress = |
| 23 | 23 | ||
| 24 | export interface EnvironmentConnectionLease { | 24 | export interface EnvironmentConnectionLease { |
| 25 | readonly prepared: PreparedConnection; | 25 | readonly prepared: PreparedConnection; |
| 26 | readonly session: RpcSession; | 26 | readonly session: RpcSession.RpcSession; |
| 27 | } | 27 | } |
| 28 | 28 | ||
| 29 | export interface ConnectionDriverService { | 29 | export class ConnectionDriver extends Context.Service< |
| 30 | readonly connect: ( | ||
| 31 | entry: ConnectionCatalogEntry, | ||
| 32 | reportProgress: (progress: ConnectionDriverProgress) => Effect.Effect<void>, | ||
| 33 | ) => Effect.Effect<EnvironmentConnectionLease, ConnectionAttemptError, Scope.Scope>; | ||
| 34 | } | ||
| 35 | |||
| 36 | export class ConnectionDriver extends Context.Service<ConnectionDriver, ConnectionDriverService>()( | ||
| 37 | "@t3tools/client-runtime/connection/driver/ConnectionDriver", | ||
| 38 | ) {} | ||
| 39 | |||
| 40 | export const connectionDriverLayer = Layer.effect( | ||
| 41 | ConnectionDriver, | 30 | ConnectionDriver, |
| 42 | Effect.gen(function* () { | 31 | { |
| 43 | const resolver = yield* ConnectionResolver; | 32 | readonly connect: ( |
| 44 | const sessions = yield* RpcSessionFactory; | ||
| 45 | |||
| 46 | const connect = Effect.fn("ConnectionDriver.connect")(function* ( | ||
| 47 | entry: ConnectionCatalogEntry, | 33 | entry: ConnectionCatalogEntry, |
| 48 | reportProgress: (progress: ConnectionDriverProgress) => Effect.Effect<void>, | 34 | reportProgress: (progress: ConnectionDriverProgress) => Effect.Effect<void>, |
| 49 | ) { | 35 | ) => Effect.Effect<EnvironmentConnectionLease, ConnectionAttemptError, Scope.Scope>; |
| 50 | const target = entry.target; | 36 | } |
| 51 | yield* Effect.annotateCurrentSpan({ | 37 | >()("@t3tools/client-runtime/connection/driver/ConnectionDriver") {} |
| 52 | "connection.environment.id": target.environmentId, | 38 | |
| 53 | "connection.target.kind": target._tag, | 39 | export const make = Effect.gen(function* () { |
| 54 | }); | 40 | const resolver = yield* ConnectionResolver.ConnectionResolver; |
| 55 | yield* reportProgress({ stage: "preparing" }); | 41 | const sessions = yield* RpcSession.RpcSessionFactory; |
| 56 | const prepared = yield* resolver.prepare(entry); | 42 | |
| 57 | yield* reportProgress({ stage: "opening", prepared }); | 43 | const connect = Effect.fn("ConnectionDriver.connect")(function* ( |
| 58 | const session = yield* sessions.connect(prepared); | 44 | entry: ConnectionCatalogEntry, |
| 59 | yield* reportProgress({ stage: "synchronizing", prepared }); | 45 | reportProgress: (progress: ConnectionDriverProgress) => Effect.Effect<void>, |
| 60 | yield* session.ready; | 46 | ) { |
| 61 | return { prepared, session } satisfies EnvironmentConnectionLease; | 47 | const target = entry.target; |
| 48 | yield* Effect.annotateCurrentSpan({ | ||
| 49 | "connection.environment.id": target.environmentId, | ||
| 50 | "connection.target.kind": target._tag, | ||
| 62 | }); | 51 | }); |
| 52 | yield* reportProgress({ stage: "preparing" }); | ||
| 53 | const prepared = yield* resolver.prepare(entry); | ||
| 54 | yield* reportProgress({ stage: "opening", prepared }); | ||
| 55 | const session = yield* sessions.connect(prepared); | ||
| 56 | yield* reportProgress({ stage: "synchronizing", prepared }); | ||
| 57 | yield* session.ready; | ||
| 58 | return { prepared, session } satisfies EnvironmentConnectionLease; | ||
| 59 | }); | ||
| 60 | |||
| 61 | return ConnectionDriver.of({ connect }); | ||
| 62 | }); | ||
| 63 | 63 | ||
| 64 | return ConnectionDriver.of({ connect }); | 64 | export const layer = Layer.effect(ConnectionDriver, make); |
| 65 | }), | ||
| 66 | ); |
packages/client-runtime/src/connection/errors.ts+17-17| ... | @@ -11,14 +11,14 @@ import { | ... | @@ -11,14 +11,14 @@ import { |
| 11 | export function profileMissingError(connectionId: string): ConnectionBlockedError { | 11 | export function profileMissingError(connectionId: string): ConnectionBlockedError { |
| 12 | return new ConnectionBlockedError({ | 12 | return new ConnectionBlockedError({ |
| 13 | reason: "configuration", | 13 | reason: "configuration", |
| 14 | message: `Connection profile ${connectionId} is unavailable.`, | 14 | detail: `Connection profile ${connectionId} is unavailable.`, |
| 15 | }); | 15 | }); |
| 16 | } | 16 | } |
| 17 | 17 | ||
| 18 | export function credentialMissingError(connectionId: string): ConnectionBlockedError { | 18 | export function credentialMissingError(connectionId: string): ConnectionBlockedError { |
| 19 | return new ConnectionBlockedError({ | 19 | return new ConnectionBlockedError({ |
| 20 | reason: "authentication", | 20 | reason: "authentication", |
| 21 | message: `Connection credential ${connectionId} is unavailable.`, | 21 | detail: `Connection credential ${connectionId} is unavailable.`, |
| 22 | }); | 22 | }); |
| 23 | } | 23 | } |
| 24 | 24 | ||
| ... | @@ -28,7 +28,7 @@ export function environmentMismatchError(input: { | ... | @@ -28,7 +28,7 @@ export function environmentMismatchError(input: { |
| 28 | }): ConnectionBlockedError { | 28 | }): ConnectionBlockedError { |
| 29 | return new ConnectionBlockedError({ | 29 | return new ConnectionBlockedError({ |
| 30 | reason: "configuration", | 30 | reason: "configuration", |
| 31 | message: `Connected environment ${input.actual} does not match ${input.expected}.`, | 31 | detail: `Connected environment ${input.actual} does not match ${input.expected}.`, |
| 32 | }); | 32 | }); |
| 33 | } | 33 | } |
| 34 | 34 | ||
| ... | @@ -40,34 +40,34 @@ function relayProtectedError(error: RelayProtectedError): ConnectionAttemptError | ... | @@ -40,34 +40,34 @@ function relayProtectedError(error: RelayProtectedError): ConnectionAttemptError |
| 40 | case "RelayAgentActivityPublishProofInvalidError": | 40 | case "RelayAgentActivityPublishProofInvalidError": |
| 41 | return new ConnectionBlockedError({ | 41 | return new ConnectionBlockedError({ |
| 42 | reason: "authentication", | 42 | reason: "authentication", |
| 43 | message: error.message, | 43 | detail: error.message, |
| 44 | traceId: error.traceId, | 44 | traceId: error.traceId, |
| 45 | }); | 45 | }); |
| 46 | case "RelayEnvironmentConnectNotAuthorizedError": | 46 | case "RelayEnvironmentConnectNotAuthorizedError": |
| 47 | case "RelayEnvironmentLinkProofInvalidError": | 47 | case "RelayEnvironmentLinkProofInvalidError": |
| 48 | return new ConnectionBlockedError({ | 48 | return new ConnectionBlockedError({ |
| 49 | reason: "permission", | 49 | reason: "permission", |
| 50 | message: error.message, | 50 | detail: error.message, |
| 51 | traceId: error.traceId, | 51 | traceId: error.traceId, |
| 52 | }); | 52 | }); |
| 53 | case "RelayEnvironmentEndpointTimedOutError": | 53 | case "RelayEnvironmentEndpointTimedOutError": |
| 54 | return new ConnectionTransientError({ | 54 | return new ConnectionTransientError({ |
| 55 | reason: "timeout", | 55 | reason: "timeout", |
| 56 | message: error.message, | 56 | detail: error.message, |
| 57 | traceId: error.traceId, | 57 | traceId: error.traceId, |
| 58 | }); | 58 | }); |
| 59 | case "RelayEnvironmentEndpointUnavailableError": | 59 | case "RelayEnvironmentEndpointUnavailableError": |
| 60 | case "RelayEnvironmentLinkUnavailableError": | 60 | case "RelayEnvironmentLinkUnavailableError": |
| 61 | return new ConnectionTransientError({ | 61 | return new ConnectionTransientError({ |
| 62 | reason: "endpoint-unavailable", | 62 | reason: "endpoint-unavailable", |
| 63 | message: error.message, | 63 | detail: error.message, |
| 64 | traceId: error.traceId, | 64 | traceId: error.traceId, |
| 65 | }); | 65 | }); |
| 66 | case "RelayEnvironmentLinkFailedError": | 66 | case "RelayEnvironmentLinkFailedError": |
| 67 | case "RelayInternalError": | 67 | case "RelayInternalError": |
| 68 | return new ConnectionTransientError({ | 68 | return new ConnectionTransientError({ |
| 69 | reason: "relay-unavailable", | 69 | reason: "relay-unavailable", |
| 70 | message: error.message, | 70 | detail: error.message, |
| 71 | traceId: error.traceId, | 71 | traceId: error.traceId, |
| 72 | }); | 72 | }); |
| 73 | } | 73 | } |
| ... | @@ -80,13 +80,13 @@ export function mapManagedRelayError(error: ManagedRelayClientError): Connection | ... | @@ -80,13 +80,13 @@ export function mapManagedRelayError(error: ManagedRelayClientError): Connection |
| 80 | if (error.cause?._tag === "ManagedRelayRequestTimeoutError") { | 80 | if (error.cause?._tag === "ManagedRelayRequestTimeoutError") { |
| 81 | return new ConnectionTransientError({ | 81 | return new ConnectionTransientError({ |
| 82 | reason: "timeout", | 82 | reason: "timeout", |
| 83 | message: error.message, | 83 | detail: error.message, |
| 84 | ...(error.traceId ? { traceId: error.traceId } : {}), | 84 | ...(error.traceId ? { traceId: error.traceId } : {}), |
| 85 | }); | 85 | }); |
| 86 | } | 86 | } |
| 87 | return new ConnectionTransientError({ | 87 | return new ConnectionTransientError({ |
| 88 | reason: "relay-unavailable", | 88 | reason: "relay-unavailable", |
| 89 | message: error.message, | 89 | detail: error.message, |
| 90 | ...(error.traceId ? { traceId: error.traceId } : {}), | 90 | ...(error.traceId ? { traceId: error.traceId } : {}), |
| 91 | }); | 91 | }); |
| 92 | } | 92 | } |
| ... | @@ -98,43 +98,43 @@ export function mapRemoteEnvironmentError( | ... | @@ -98,43 +98,43 @@ export function mapRemoteEnvironmentError( |
| 98 | case "EnvironmentAuthInvalidError": | 98 | case "EnvironmentAuthInvalidError": |
| 99 | return new ConnectionBlockedError({ | 99 | return new ConnectionBlockedError({ |
| 100 | reason: "authentication", | 100 | reason: "authentication", |
| 101 | message: "The environment credential is invalid.", | 101 | detail: "The environment credential is invalid.", |
| 102 | traceId: error.traceId, | 102 | traceId: error.traceId, |
| 103 | }); | 103 | }); |
| 104 | case "EnvironmentScopeRequiredError": | 104 | case "EnvironmentScopeRequiredError": |
| 105 | case "EnvironmentOperationForbiddenError": | 105 | case "EnvironmentOperationForbiddenError": |
| 106 | return new ConnectionBlockedError({ | 106 | return new ConnectionBlockedError({ |
| 107 | reason: "permission", | 107 | reason: "permission", |
| 108 | message: "The environment credential does not grant the required access.", | 108 | detail: "The environment credential does not grant the required access.", |
| 109 | traceId: error.traceId, | 109 | traceId: error.traceId, |
| 110 | }); | 110 | }); |
| 111 | case "EnvironmentRequestInvalidError": | 111 | case "EnvironmentRequestInvalidError": |
| 112 | return new ConnectionBlockedError({ | 112 | return new ConnectionBlockedError({ |
| 113 | reason: "configuration", | 113 | reason: "configuration", |
| 114 | message: "The environment rejected the authentication request.", | 114 | detail: "The environment rejected the authentication request.", |
| 115 | traceId: error.traceId, | 115 | traceId: error.traceId, |
| 116 | }); | 116 | }); |
| 117 | case "RemoteEnvironmentAuthTimeoutError": | 117 | case "RemoteEnvironmentAuthTimeoutError": |
| 118 | return new ConnectionTransientError({ | 118 | return new ConnectionTransientError({ |
| 119 | reason: "timeout", | 119 | reason: "timeout", |
| 120 | message: error.message, | 120 | detail: error.message, |
| 121 | }); | 121 | }); |
| 122 | case "RemoteEnvironmentAuthFetchError": | 122 | case "RemoteEnvironmentAuthFetchError": |
| 123 | return new ConnectionTransientError({ | 123 | return new ConnectionTransientError({ |
| 124 | reason: "network", | 124 | reason: "network", |
| 125 | message: error.message, | 125 | detail: error.message, |
| 126 | }); | 126 | }); |
| 127 | case "EnvironmentInternalError": | 127 | case "EnvironmentInternalError": |
| 128 | return new ConnectionTransientError({ | 128 | return new ConnectionTransientError({ |
| 129 | reason: "remote-unavailable", | 129 | reason: "remote-unavailable", |
| 130 | message: "The environment could not authorize the connection.", | 130 | detail: "The environment could not authorize the connection.", |
| 131 | traceId: error.traceId, | 131 | traceId: error.traceId, |
| 132 | }); | 132 | }); |
| 133 | case "RemoteEnvironmentAuthInvalidJsonError": | 133 | case "RemoteEnvironmentAuthInvalidJsonError": |
| 134 | case "RemoteEnvironmentAuthUndeclaredStatusError": | 134 | case "RemoteEnvironmentAuthUndeclaredStatusError": |
| 135 | return new ConnectionTransientError({ | 135 | return new ConnectionTransientError({ |
| 136 | reason: "remote-unavailable", | 136 | reason: "remote-unavailable", |
| 137 | message: error.message, | 137 | detail: error.message, |
| 138 | }); | 138 | }); |
| 139 | } | 139 | } |
| 140 | } | 140 | } |
packages/client-runtime/src/connection/index.ts+29-8| ... | @@ -1,12 +1,33 @@ | ... | @@ -1,12 +1,33 @@ |
| 1 | export * from "./catalog.ts"; | 1 | export * from "./catalog.ts"; |
| 2 | export * from "./connectivity.ts"; | 2 | export * as Connectivity from "./connectivity.ts"; |
| 3 | export * from "./driver.ts"; | 3 | export * as CredentialStore from "./credentialStore.ts"; |
| 4 | export { | ||
| 5 | ConnectionDriver, | ||
| 6 | type ConnectionDriverProgress, | ||
| 7 | type EnvironmentConnectionLease, | ||
| 8 | } from "./driver.ts"; | ||
| 4 | export * from "./errors.ts"; | 9 | export * from "./errors.ts"; |
| 5 | export * from "./layer.ts"; | 10 | export * as Connection from "./layer.ts"; |
| 6 | export * from "./model.ts"; | 11 | export * from "./model.ts"; |
| 7 | export * from "./onboarding.ts"; | 12 | export { |
| 13 | type BearerConnectionUpdateInput, | ||
| 14 | ConnectionOnboarding, | ||
| 15 | type PairingConnectionInput, | ||
| 16 | type SshConnectionInput, | ||
| 17 | prepareBearerConnectionUpdate, | ||
| 18 | preparePairingRegistration, | ||
| 19 | prepareSshRegistration, | ||
| 20 | registerPairingConnection, | ||
| 21 | registerSshConnection, | ||
| 22 | updateBearerConnection, | ||
| 23 | } from "./onboarding.ts"; | ||
| 8 | export * from "./presentation.ts"; | 24 | export * from "./presentation.ts"; |
| 9 | export * from "./registry.ts"; | 25 | export * as ProfileStore from "./profileStore.ts"; |
| 10 | export * from "./resolver.ts"; | 26 | export { |
| 11 | export * from "./supervisor.ts"; | 27 | EnvironmentNotRegisteredError, |
| 12 | export * from "./wakeups.ts"; | 28 | EnvironmentRegistry, |
| 29 | PlatformEnvironmentRemovalError, | ||
| 30 | } from "./registry.ts"; | ||
| 31 | export { ConnectionResolver } from "./resolver.ts"; | ||
| 32 | export { EnvironmentSupervisor, type EnvironmentSupervisorOptions } from "./supervisor.ts"; | ||
| 33 | export * as Wakeups from "./wakeups.ts"; |
packages/client-runtime/src/connection/layer.ts+19-21| ... | @@ -2,37 +2,37 @@ import * as Effect from "effect/Effect"; | ... | @@ -2,37 +2,37 @@ import * as Effect from "effect/Effect"; |
| 2 | import * as Layer from "effect/Layer"; | 2 | import * as Layer from "effect/Layer"; |
| 3 | import * as Stream from "effect/Stream"; | 3 | import * as Stream from "effect/Stream"; |
| 4 | 4 | ||
| 5 | import { connectionResolverLayer } from "./resolver.ts"; | 5 | import * as ConnectionResolver from "./resolver.ts"; |
| 6 | import { connectionDriverLayer } from "./driver.ts"; | 6 | import * as ConnectionDriver from "./driver.ts"; |
| 7 | import { environmentRegistryLayer, EnvironmentRegistry } from "./registry.ts"; | 7 | import * as EnvironmentRegistry from "./registry.ts"; |
| 8 | import { connectionOnboardingLayer } from "./onboarding.ts"; | 8 | import * as ConnectionOnboarding from "./onboarding.ts"; |
| 9 | import { PlatformConnectionSource } from "../platform/source.ts"; | 9 | import * as PlatformConnectionSource from "../platform/source.ts"; |
| 10 | import { relayEnvironmentDiscoveryLayer } from "../relay/discovery.ts"; | 10 | import * as RelayEnvironmentDiscovery from "../relay/discovery.ts"; |
| 11 | import { remoteEnvironmentAuthorizationLayer } from "../authorization/layer.ts"; | 11 | import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; |
| 12 | import { rpcSessionFactoryLayer } from "../rpc/session.ts"; | 12 | import * as RpcSession from "../rpc/session.ts"; |
| 13 | 13 | ||
| 14 | const resolverLayer = connectionResolverLayer.pipe( | 14 | const resolverLayer = ConnectionResolver.layer.pipe( |
| 15 | Layer.provide(remoteEnvironmentAuthorizationLayer), | 15 | Layer.provide(RemoteEnvironmentAuthorization.layer), |
| 16 | ); | 16 | ); |
| 17 | 17 | ||
| 18 | const driverLayer = connectionDriverLayer.pipe( | 18 | const driverLayer = ConnectionDriver.layer.pipe( |
| 19 | Layer.provide(Layer.mergeAll(resolverLayer, rpcSessionFactoryLayer)), | 19 | Layer.provide(Layer.mergeAll(resolverLayer, RpcSession.layer)), |
| 20 | ); | 20 | ); |
| 21 | 21 | ||
| 22 | const registryLayer = environmentRegistryLayer.pipe(Layer.provide(driverLayer)); | 22 | const registryLayer = EnvironmentRegistry.layer.pipe(Layer.provide(driverLayer)); |
| 23 | 23 | ||
| 24 | const onboardingLayer = connectionOnboardingLayer.pipe(Layer.provide(registryLayer)); | 24 | const onboardingLayer = ConnectionOnboarding.layer.pipe(Layer.provide(registryLayer)); |
| 25 | 25 | ||
| 26 | const connectionServicesLayer = Layer.mergeAll( | 26 | const connectionServicesLayer = Layer.mergeAll( |
| 27 | registryLayer, | 27 | registryLayer, |
| 28 | relayEnvironmentDiscoveryLayer, | 28 | RelayEnvironmentDiscovery.layer, |
| 29 | onboardingLayer, | 29 | onboardingLayer, |
| 30 | ); | 30 | ); |
| 31 | 31 | ||
| 32 | const connectionStartupLayer = Layer.effectDiscard( | 32 | const connectionStartupLayer = Layer.effectDiscard( |
| 33 | Effect.gen(function* () { | 33 | Effect.gen(function* () { |
| 34 | const registry = yield* EnvironmentRegistry; | 34 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 35 | const platformSource = yield* PlatformConnectionSource; | 35 | const platformSource = yield* PlatformConnectionSource.PlatformConnectionSource; |
| 36 | yield* registry.start; | 36 | yield* registry.start; |
| 37 | yield* platformSource.registrations.pipe( | 37 | yield* platformSource.registrations.pipe( |
| 38 | Stream.runForEach(registry.registerPlatform), | 38 | Stream.runForEach(registry.registerPlatform), |
| ... | @@ -41,6 +41,4 @@ const connectionStartupLayer = Layer.effectDiscard( | ... | @@ -41,6 +41,4 @@ const connectionStartupLayer = Layer.effectDiscard( |
| 41 | }).pipe(Effect.withSpan("clientRuntime.connection.application.start")), | 41 | }).pipe(Effect.withSpan("clientRuntime.connection.application.start")), |
| 42 | ); | 42 | ); |
| 43 | 43 | ||
| 44 | export const connectionLayer = connectionStartupLayer.pipe( | 44 | export const layer = connectionStartupLayer.pipe(Layer.provideMerge(connectionServicesLayer)); |
| 45 | Layer.provideMerge(connectionServicesLayer), | ||
| 46 | ); |
packages/client-runtime/src/connection/model.ts+31-26| ... | @@ -57,44 +57,49 @@ export type ConnectionTargetKind = ConnectionTarget["_tag"]; | ... | @@ -57,44 +57,49 @@ export type ConnectionTargetKind = ConnectionTarget["_tag"]; |
| 57 | 57 | ||
| 58 | export type NetworkStatus = "unknown" | "offline" | "online"; | 58 | export type NetworkStatus = "unknown" | "offline" | "online"; |
| 59 | 59 | ||
| 60 | export type ConnectionTransientReason = | 60 | export const ConnectionTransientReason = Schema.Literals([ |
| 61 | | "network" | 61 | "network", |
| 62 | | "timeout" | 62 | "timeout", |
| 63 | | "transport" | 63 | "transport", |
| 64 | | "endpoint-unavailable" | 64 | "endpoint-unavailable", |
| 65 | | "relay-unavailable" | 65 | "relay-unavailable", |
| 66 | | "remote-unavailable"; | 66 | "remote-unavailable", |
| 67 | 67 | ]); | |
| 68 | export type ConnectionBlockedReason = | 68 | export type ConnectionTransientReason = typeof ConnectionTransientReason.Type; |
| 69 | | "authentication" | 69 | |
| 70 | | "configuration" | 70 | export const ConnectionBlockedReason = Schema.Literals([ |
| 71 | | "permission" | 71 | "authentication", |
| 72 | | "unsupported"; | 72 | "configuration", |
| 73 | "permission", | ||
| 74 | "unsupported", | ||
| 75 | ]); | ||
| 76 | export type ConnectionBlockedReason = typeof ConnectionBlockedReason.Type; | ||
| 73 | 77 | ||
| 74 | export class ConnectionTransientError extends Schema.TaggedErrorClass<ConnectionTransientError>()( | 78 | export class ConnectionTransientError extends Schema.TaggedErrorClass<ConnectionTransientError>()( |
| 75 | "ConnectionTransientError", | 79 | "ConnectionTransientError", |
| 76 | { | 80 | { |
| 77 | reason: Schema.Literals([ | 81 | reason: ConnectionTransientReason, |
| 78 | "network", | 82 | detail: Schema.String, |
| 79 | "timeout", | ||
| 80 | "transport", | ||
| 81 | "endpoint-unavailable", | ||
| 82 | "relay-unavailable", | ||
| 83 | "remote-unavailable", | ||
| 84 | ]), | ||
| 85 | message: Schema.String, | ||
| 86 | traceId: Schema.optionalKey(Schema.String), | 83 | traceId: Schema.optionalKey(Schema.String), |
| 87 | }, | 84 | }, |
| 88 | ) {} | 85 | ) { |
| 86 | override get message(): string { | ||
| 87 | return this.detail; | ||
| 88 | } | ||
| 89 | } | ||
| 89 | 90 | ||
| 90 | export class ConnectionBlockedError extends Schema.TaggedErrorClass<ConnectionBlockedError>()( | 91 | export class ConnectionBlockedError extends Schema.TaggedErrorClass<ConnectionBlockedError>()( |
| 91 | "ConnectionBlockedError", | 92 | "ConnectionBlockedError", |
| 92 | { | 93 | { |
| 93 | reason: Schema.Literals(["authentication", "configuration", "permission", "unsupported"]), | 94 | reason: ConnectionBlockedReason, |
| 94 | message: Schema.String, | 95 | detail: Schema.String, |
| 95 | traceId: Schema.optionalKey(Schema.String), | 96 | traceId: Schema.optionalKey(Schema.String), |
| 96 | }, | 97 | }, |
| 97 | ) {} | 98 | ) { |
| 99 | override get message(): string { | ||
| 100 | return this.detail; | ||
| 101 | } | ||
| 102 | } | ||
| 98 | 103 | ||
| 99 | export type ConnectionAttemptError = ConnectionTransientError | ConnectionBlockedError; | 104 | export type ConnectionAttemptError = ConnectionTransientError | ConnectionBlockedError; |
| 100 | 105 |
packages/client-runtime/src/connection/onboarding.ts+52-47| ... | @@ -6,22 +6,22 @@ import * as Layer from "effect/Layer"; | ... | @@ -6,22 +6,22 @@ import * as Layer from "effect/Layer"; |
| 6 | import * as Option from "effect/Option"; | 6 | import * as Option from "effect/Option"; |
| 7 | import * as Schema from "effect/Schema"; | 7 | import * as Schema from "effect/Schema"; |
| 8 | import * as SubscriptionRef from "effect/SubscriptionRef"; | 8 | import * as SubscriptionRef from "effect/SubscriptionRef"; |
| 9 | import { HttpClient } from "effect/unstable/http"; | 9 | import * as HttpClient from "effect/unstable/http/HttpClient"; |
| 10 | 10 | ||
| 11 | import { bootstrapRemoteBearerSession } from "../authorization/remote.ts"; | 11 | import { bootstrapRemoteBearerSession } from "../authorization/remote.ts"; |
| 12 | import { deriveWsBaseUrl, normalizeHttpBaseUrl } from "../environment/endpoint.ts"; | 12 | import { deriveWsBaseUrl, normalizeHttpBaseUrl } from "../environment/endpoint.ts"; |
| 13 | import { fetchRemoteEnvironmentDescriptor } from "../environment/descriptor.ts"; | 13 | import { fetchRemoteEnvironmentDescriptor } from "../environment/descriptor.ts"; |
| 14 | import { ClientPresentation, SshEnvironmentGateway } from "../platform/capabilities.ts"; | 14 | import * as ClientCapabilities from "../platform/capabilities.ts"; |
| 15 | import { | 15 | import { |
| 16 | BearerConnectionCredential, | 16 | BearerConnectionCredential, |
| 17 | BearerConnectionProfile, | 17 | BearerConnectionProfile, |
| 18 | BearerConnectionRegistration, | 18 | BearerConnectionRegistration, |
| 19 | type ConnectionCatalogEntry, | 19 | type ConnectionCatalogEntry, |
| 20 | type ConnectionCredential, | 20 | type ConnectionCredential, |
| 21 | ConnectionCredentialStore, | ||
| 22 | SshConnectionProfile, | 21 | SshConnectionProfile, |
| 23 | SshConnectionRegistration, | 22 | SshConnectionRegistration, |
| 24 | } from "./catalog.ts"; | 23 | } from "./catalog.ts"; |
| 24 | import * as ConnectionCredentialStore from "./credentialStore.ts"; | ||
| 25 | import { mapRemoteEnvironmentError } from "./errors.ts"; | 25 | import { mapRemoteEnvironmentError } from "./errors.ts"; |
| 26 | import { | 26 | import { |
| 27 | BearerConnectionTarget, | 27 | BearerConnectionTarget, |
| ... | @@ -29,8 +29,8 @@ import { | ... | @@ -29,8 +29,8 @@ import { |
| 29 | SshConnectionTarget, | 29 | SshConnectionTarget, |
| 30 | type ConnectionAttemptError, | 30 | type ConnectionAttemptError, |
| 31 | } from "./model.ts"; | 31 | } from "./model.ts"; |
| 32 | import type { ConnectionPersistenceError } from "../platform/persistence.ts"; | 32 | import * as Persistence from "../platform/persistence.ts"; |
| 33 | import { EnvironmentRegistry } from "./registry.ts"; | 33 | import * as EnvironmentRegistry from "./registry.ts"; |
| 34 | 34 | ||
| 35 | export interface PairingConnectionInput { | 35 | export interface PairingConnectionInput { |
| 36 | readonly pairingUrl?: string; | 36 | readonly pairingUrl?: string; |
| ... | @@ -54,13 +54,19 @@ export class ConnectionOnboarding extends Context.Service< | ... | @@ -54,13 +54,19 @@ export class ConnectionOnboarding extends Context.Service< |
| 54 | { | 54 | { |
| 55 | readonly registerPairing: ( | 55 | readonly registerPairing: ( |
| 56 | input: PairingConnectionInput, | 56 | input: PairingConnectionInput, |
| 57 | ) => Effect.Effect<EnvironmentId, ConnectionAttemptError | ConnectionPersistenceError>; | 57 | ) => Effect.Effect< |
| 58 | EnvironmentId, | ||
| 59 | ConnectionAttemptError | Persistence.ConnectionPersistenceError | ||
| 60 | >; | ||
| 58 | readonly registerSsh: ( | 61 | readonly registerSsh: ( |
| 59 | input: SshConnectionInput, | 62 | input: SshConnectionInput, |
| 60 | ) => Effect.Effect<EnvironmentId, ConnectionAttemptError | ConnectionPersistenceError>; | 63 | ) => Effect.Effect< |
| 64 | EnvironmentId, | ||
| 65 | ConnectionAttemptError | Persistence.ConnectionPersistenceError | ||
| 66 | >; | ||
| 61 | readonly updateBearer: ( | 67 | readonly updateBearer: ( |
| 62 | input: BearerConnectionUpdateInput, | 68 | input: BearerConnectionUpdateInput, |
| 63 | ) => Effect.Effect<void, ConnectionAttemptError | ConnectionPersistenceError>; | 69 | ) => Effect.Effect<void, ConnectionAttemptError | Persistence.ConnectionPersistenceError>; |
| 64 | } | 70 | } |
| 65 | >()("@t3tools/client-runtime/connection/onboarding/ConnectionOnboarding") {} | 71 | >()("@t3tools/client-runtime/connection/onboarding/ConnectionOnboarding") {} |
| 66 | 72 | ||
| ... | @@ -71,7 +77,7 @@ const resolvePairingTarget = Effect.fn("clientRuntime.connection.onboarding.reso | ... | @@ -71,7 +77,7 @@ const resolvePairingTarget = Effect.fn("clientRuntime.connection.onboarding.reso |
| 71 | catch: (cause) => | 77 | catch: (cause) => |
| 72 | new ConnectionBlockedError({ | 78 | new ConnectionBlockedError({ |
| 73 | reason: "configuration", | 79 | reason: "configuration", |
| 74 | message: cause instanceof Error ? cause.message : "The pairing details are invalid.", | 80 | detail: cause instanceof Error ? cause.message : "The pairing details are invalid.", |
| 75 | }), | 81 | }), |
| 76 | }); | 82 | }); |
| 77 | }, | 83 | }, |
| ... | @@ -81,7 +87,7 @@ export const preparePairingRegistration = Effect.fn( | ... | @@ -81,7 +87,7 @@ export const preparePairingRegistration = Effect.fn( |
| 81 | "clientRuntime.connection.onboarding.preparePairingRegistration", | 87 | "clientRuntime.connection.onboarding.preparePairingRegistration", |
| 82 | )(function* (input: PairingConnectionInput) { | 88 | )(function* (input: PairingConnectionInput) { |
| 83 | const target = yield* resolvePairingTarget(input); | 89 | const target = yield* resolvePairingTarget(input); |
| 84 | const presentation = yield* ClientPresentation; | 90 | const presentation = yield* ClientCapabilities.ClientPresentation; |
| 85 | const descriptor = yield* fetchRemoteEnvironmentDescriptor({ | 91 | const descriptor = yield* fetchRemoteEnvironmentDescriptor({ |
| 86 | httpBaseUrl: target.httpBaseUrl, | 92 | httpBaseUrl: target.httpBaseUrl, |
| 87 | }).pipe(Effect.mapError(mapRemoteEnvironmentError)); | 93 | }).pipe(Effect.mapError(mapRemoteEnvironmentError)); |
| ... | @@ -116,7 +122,7 @@ export const registerPairingConnection = Effect.fn( | ... | @@ -116,7 +122,7 @@ export const registerPairingConnection = Effect.fn( |
| 116 | "clientRuntime.connection.onboarding.registerPairingConnection", | 122 | "clientRuntime.connection.onboarding.registerPairingConnection", |
| 117 | )(function* (input: PairingConnectionInput) { | 123 | )(function* (input: PairingConnectionInput) { |
| 118 | const registration = yield* preparePairingRegistration(input); | 124 | const registration = yield* preparePairingRegistration(input); |
| 119 | const registry = yield* EnvironmentRegistry; | 125 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 120 | yield* registry.register(registration); | 126 | yield* registry.register(registration); |
| 121 | return registration.target.environmentId; | 127 | return registration.target.environmentId; |
| 122 | }); | 128 | }); |
| ... | @@ -127,8 +133,8 @@ const isBearerProfile = Schema.is(BearerConnectionProfile); | ... | @@ -127,8 +133,8 @@ const isBearerProfile = Schema.is(BearerConnectionProfile); |
| 127 | export const updateBearerConnection = Effect.fn( | 133 | export const updateBearerConnection = Effect.fn( |
| 128 | "clientRuntime.connection.onboarding.updateBearerConnection", | 134 | "clientRuntime.connection.onboarding.updateBearerConnection", |
| 129 | )(function* (input: BearerConnectionUpdateInput) { | 135 | )(function* (input: BearerConnectionUpdateInput) { |
| 130 | const registry = yield* EnvironmentRegistry; | 136 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 131 | const credentials = yield* ConnectionCredentialStore; | 137 | const credentials = yield* ConnectionCredentialStore.ConnectionCredentialStore; |
| 132 | const entry = (yield* SubscriptionRef.get(registry.entries)).get(input.environmentId); | 138 | const entry = (yield* SubscriptionRef.get(registry.entries)).get(input.environmentId); |
| 133 | const credential = | 139 | const credential = |
| 134 | entry?.target._tag === "BearerConnectionTarget" | 140 | entry?.target._tag === "BearerConnectionTarget" |
| ... | @@ -159,7 +165,7 @@ export const prepareBearerConnectionUpdate = Effect.fn( | ... | @@ -159,7 +165,7 @@ export const prepareBearerConnectionUpdate = Effect.fn( |
| 159 | ) { | 165 | ) { |
| 160 | return yield* new ConnectionBlockedError({ | 166 | return yield* new ConnectionBlockedError({ |
| 161 | reason: "configuration", | 167 | reason: "configuration", |
| 162 | message: "Only saved bearer environments can be edited.", | 168 | detail: "Only saved bearer environments can be edited.", |
| 163 | }); | 169 | }); |
| 164 | } | 170 | } |
| 165 | 171 | ||
| ... | @@ -167,7 +173,7 @@ export const prepareBearerConnectionUpdate = Effect.fn( | ... | @@ -167,7 +173,7 @@ export const prepareBearerConnectionUpdate = Effect.fn( |
| 167 | if (Option.isNone(credential) || !isBearerCredential(credential.value)) { | 173 | if (Option.isNone(credential) || !isBearerCredential(credential.value)) { |
| 168 | return yield* new ConnectionBlockedError({ | 174 | return yield* new ConnectionBlockedError({ |
| 169 | reason: "authentication", | 175 | reason: "authentication", |
| 170 | message: "The saved bearer credential is unavailable.", | 176 | detail: "The saved bearer credential is unavailable.", |
| 171 | }); | 177 | }); |
| 172 | } | 178 | } |
| 173 | 179 | ||
| ... | @@ -175,7 +181,7 @@ export const prepareBearerConnectionUpdate = Effect.fn( | ... | @@ -175,7 +181,7 @@ export const prepareBearerConnectionUpdate = Effect.fn( |
| 175 | if (label === "") { | 181 | if (label === "") { |
| 176 | return yield* new ConnectionBlockedError({ | 182 | return yield* new ConnectionBlockedError({ |
| 177 | reason: "configuration", | 183 | reason: "configuration", |
| 178 | message: "Environment label cannot be empty.", | 184 | detail: "Environment label cannot be empty.", |
| 179 | }); | 185 | }); |
| 180 | } | 186 | } |
| 181 | const httpBaseUrl = yield* Effect.try({ | 187 | const httpBaseUrl = yield* Effect.try({ |
| ... | @@ -183,7 +189,7 @@ export const prepareBearerConnectionUpdate = Effect.fn( | ... | @@ -183,7 +189,7 @@ export const prepareBearerConnectionUpdate = Effect.fn( |
| 183 | catch: (cause) => | 189 | catch: (cause) => |
| 184 | new ConnectionBlockedError({ | 190 | new ConnectionBlockedError({ |
| 185 | reason: "configuration", | 191 | reason: "configuration", |
| 186 | message: cause instanceof Error ? cause.message : "The environment URL is invalid.", | 192 | detail: cause instanceof Error ? cause.message : "The environment URL is invalid.", |
| 187 | }), | 193 | }), |
| 188 | }); | 194 | }); |
| 189 | const connectionId = entry.target.connectionId; | 195 | const connectionId = entry.target.connectionId; |
| ... | @@ -207,7 +213,7 @@ export const prepareBearerConnectionUpdate = Effect.fn( | ... | @@ -207,7 +213,7 @@ export const prepareBearerConnectionUpdate = Effect.fn( |
| 207 | export const prepareSshRegistration = Effect.fn( | 213 | export const prepareSshRegistration = Effect.fn( |
| 208 | "clientRuntime.connection.onboarding.prepareSshRegistration", | 214 | "clientRuntime.connection.onboarding.prepareSshRegistration", |
| 209 | )(function* (input: SshConnectionInput) { | 215 | )(function* (input: SshConnectionInput) { |
| 210 | const gateway = yield* SshEnvironmentGateway; | 216 | const gateway = yield* ClientCapabilities.SshEnvironmentGateway; |
| 211 | const provisioned = yield* gateway.provision(input.target); | 217 | const provisioned = yield* gateway.provision(input.target); |
| 212 | const connectionId = `ssh:${provisioned.environmentId}`; | 218 | const connectionId = `ssh:${provisioned.environmentId}`; |
| 213 | const label = input.label?.trim() || provisioned.label || provisioned.bootstrap.target.alias; | 219 | const label = input.label?.trim() || provisioned.label || provisioned.bootstrap.target.alias; |
| ... | @@ -231,37 +237,36 @@ export const registerSshConnection = Effect.fn( | ... | @@ -231,37 +237,36 @@ export const registerSshConnection = Effect.fn( |
| 231 | "clientRuntime.connection.onboarding.registerSshConnection", | 237 | "clientRuntime.connection.onboarding.registerSshConnection", |
| 232 | )(function* (input: SshConnectionInput) { | 238 | )(function* (input: SshConnectionInput) { |
| 233 | const registration = yield* prepareSshRegistration(input); | 239 | const registration = yield* prepareSshRegistration(input); |
| 234 | const registry = yield* EnvironmentRegistry; | 240 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 235 | yield* registry.register(registration); | 241 | yield* registry.register(registration); |
| 236 | return registration.target.environmentId; | 242 | return registration.target.environmentId; |
| 237 | }); | 243 | }); |
| 238 | 244 | ||
| 239 | export const connectionOnboardingLayer = Layer.effect( | 245 | export const make = Effect.gen(function* () { |
| 240 | ConnectionOnboarding, | 246 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 241 | Effect.gen(function* () { | 247 | const presentation = yield* ClientCapabilities.ClientPresentation; |
| 242 | const registry = yield* EnvironmentRegistry; | 248 | const httpClient = yield* HttpClient.HttpClient; |
| 243 | const presentation = yield* ClientPresentation; | 249 | const ssh = yield* ClientCapabilities.SshEnvironmentGateway; |
| 244 | const httpClient = yield* HttpClient.HttpClient; | 250 | const credentials = yield* ConnectionCredentialStore.ConnectionCredentialStore; |
| 245 | const ssh = yield* SshEnvironmentGateway; | ||
| 246 | const credentials = yield* ConnectionCredentialStore; | ||
| 247 | 251 | ||
| 248 | return ConnectionOnboarding.of({ | 252 | return ConnectionOnboarding.of({ |
| 249 | registerPairing: (input) => | 253 | registerPairing: (input) => |
| 250 | registerPairingConnection(input).pipe( | 254 | registerPairingConnection(input).pipe( |
| 251 | Effect.provideService(EnvironmentRegistry, registry), | 255 | Effect.provideService(EnvironmentRegistry.EnvironmentRegistry, registry), |
| 252 | Effect.provideService(ClientPresentation, presentation), | 256 | Effect.provideService(ClientCapabilities.ClientPresentation, presentation), |
| 253 | Effect.provideService(HttpClient.HttpClient, httpClient), | 257 | Effect.provideService(HttpClient.HttpClient, httpClient), |
| 254 | ), | 258 | ), |
| 255 | registerSsh: (input) => | 259 | registerSsh: (input) => |
| 256 | registerSshConnection(input).pipe( | 260 | registerSshConnection(input).pipe( |
| 257 | Effect.provideService(EnvironmentRegistry, registry), | 261 | Effect.provideService(EnvironmentRegistry.EnvironmentRegistry, registry), |
| 258 | Effect.provideService(SshEnvironmentGateway, ssh), | 262 | Effect.provideService(ClientCapabilities.SshEnvironmentGateway, ssh), |
| 259 | ), | 263 | ), |
| 260 | updateBearer: (input) => | 264 | updateBearer: (input) => |
| 261 | updateBearerConnection(input).pipe( | 265 | updateBearerConnection(input).pipe( |
| 262 | Effect.provideService(EnvironmentRegistry, registry), | 266 | Effect.provideService(EnvironmentRegistry.EnvironmentRegistry, registry), |
| 263 | Effect.provideService(ConnectionCredentialStore, credentials), | 267 | Effect.provideService(ConnectionCredentialStore.ConnectionCredentialStore, credentials), |
| 264 | ), | 268 | ), |
| 265 | }); | 269 | }); |
| 266 | }), | 270 | }); |
| 267 | ); | 271 | |
| 272 | export const layer = Layer.effect(ConnectionOnboarding, make); |
packages/client-runtime/src/connection/presentation.test.ts+3-3| ... | @@ -67,7 +67,7 @@ describe("connection presentation", () => { | ... | @@ -67,7 +67,7 @@ describe("connection presentation", () => { |
| 67 | attempt: 2, | 67 | attempt: 2, |
| 68 | lastFailure: new ConnectionTransientError({ | 68 | lastFailure: new ConnectionTransientError({ |
| 69 | reason: "transport", | 69 | reason: "transport", |
| 70 | message: "Socket closed.", | 70 | detail: "Socket closed.", |
| 71 | traceId: "trace-previous", | 71 | traceId: "trace-previous", |
| 72 | }), | 72 | }), |
| 73 | }), | 73 | }), |
| ... | @@ -85,7 +85,7 @@ describe("connection presentation", () => { | ... | @@ -85,7 +85,7 @@ describe("connection presentation", () => { |
| 85 | retryAt: 1, | 85 | retryAt: 1, |
| 86 | lastFailure: new ConnectionTransientError({ | 86 | lastFailure: new ConnectionTransientError({ |
| 87 | reason: "transport", | 87 | reason: "transport", |
| 88 | message: "Disconnected.", | 88 | detail: "Disconnected.", |
| 89 | traceId: "trace-1", | 89 | traceId: "trace-1", |
| 90 | }), | 90 | }), |
| 91 | }), | 91 | }), |
| ... | @@ -106,7 +106,7 @@ describe("connection presentation", () => { | ... | @@ -106,7 +106,7 @@ describe("connection presentation", () => { |
| 106 | attempt: 2, | 106 | attempt: 2, |
| 107 | lastFailure: new ConnectionTransientError({ | 107 | lastFailure: new ConnectionTransientError({ |
| 108 | reason: "transport", | 108 | reason: "transport", |
| 109 | message: "Relay connection timed out.", | 109 | detail: "Relay connection timed out.", |
| 110 | traceId: "trace-retry", | 110 | traceId: "trace-retry", |
| 111 | }), | 111 | }), |
| 112 | }), | 112 | }), |
packages/client-runtime/src/connection/profileStore.ts created+24| ... | @@ -0,0 +1,24 @@ | ||
| 1 | import * as Context from "effect/Context"; | ||
| 2 | import type * as Effect from "effect/Effect"; | ||
| 3 | import * as Layer from "effect/Layer"; | ||
| 4 | import type * as Option from "effect/Option"; | ||
| 5 | |||
| 6 | import type { ConnectionProfile } from "./catalog.ts"; | ||
| 7 | import type { ConnectionAttemptError } from "./model.ts"; | ||
| 8 | |||
| 9 | export class ConnectionProfileStore extends Context.Service< | ||
| 10 | ConnectionProfileStore, | ||
| 11 | { | ||
| 12 | readonly get: ( | ||
| 13 | connectionId: string, | ||
| 14 | ) => Effect.Effect<Option.Option<ConnectionProfile>, ConnectionAttemptError>; | ||
| 15 | readonly put: (profile: ConnectionProfile) => Effect.Effect<void, ConnectionAttemptError>; | ||
| 16 | readonly remove: (connectionId: string) => Effect.Effect<void, ConnectionAttemptError>; | ||
| 17 | } | ||
| 18 | >()("@t3tools/client-runtime/connection/profileStore/ConnectionProfileStore") {} | ||
| 19 | |||
| 20 | export const make = (service: ConnectionProfileStore["Service"]) => | ||
| 21 | ConnectionProfileStore.of(service); | ||
| 22 | |||
| 23 | export const layer = (service: ConnectionProfileStore["Service"]) => | ||
| 24 | Layer.succeed(ConnectionProfileStore, make(service)); | ||
packages/client-runtime/src/connection/registry.test.ts+63-66| ... | @@ -14,23 +14,22 @@ import * as Result from "effect/Result"; | ... | @@ -14,23 +14,22 @@ import * as Result from "effect/Result"; |
| 14 | import * as Stream from "effect/Stream"; | 14 | import * as Stream from "effect/Stream"; |
| 15 | import * as SubscriptionRef from "effect/SubscriptionRef"; | 15 | import * as SubscriptionRef from "effect/SubscriptionRef"; |
| 16 | 16 | ||
| 17 | import { SshEnvironmentGateway } from "../platform/capabilities.ts"; | 17 | import * as ClientCapabilities from "../platform/capabilities.ts"; |
| 18 | import { RemoteDpopAccessToken, RemoteDpopAccessTokenStore } from "../authorization/tokenStore.ts"; | 18 | import * as TokenStore from "../authorization/tokenStore.ts"; |
| 19 | import { | 19 | import { |
| 20 | BearerConnectionCredential, | 20 | BearerConnectionCredential, |
| 21 | BearerConnectionProfile, | 21 | BearerConnectionProfile, |
| 22 | BearerConnectionRegistration, | 22 | BearerConnectionRegistration, |
| 23 | type ConnectionRegistration, | 23 | type ConnectionRegistration, |
| 24 | ConnectionCredentialStore, | ||
| 25 | ConnectionProfileStore, | ||
| 26 | PrimaryConnectionRegistration, | 24 | PrimaryConnectionRegistration, |
| 27 | RelayConnectionRegistration, | 25 | RelayConnectionRegistration, |
| 28 | SshConnectionProfile, | 26 | SshConnectionProfile, |
| 29 | type ConnectionCredential, | 27 | type ConnectionCredential, |
| 30 | type ConnectionProfile, | 28 | type ConnectionProfile, |
| 31 | } from "./catalog.ts"; | 29 | } from "./catalog.ts"; |
| 32 | import { Connectivity } from "./connectivity.ts"; | 30 | import * as Connectivity from "./connectivity.ts"; |
| 33 | import { ConnectionDriver } from "./driver.ts"; | 31 | import * as ConnectionCredentialStore from "./credentialStore.ts"; |
| 32 | import * as ConnectionDriver from "./driver.ts"; | ||
| 34 | import { | 33 | import { |
| 35 | ConnectionTransientError, | 34 | ConnectionTransientError, |
| 36 | BearerConnectionTarget, | 35 | BearerConnectionTarget, |
| ... | @@ -41,17 +40,12 @@ import { | ... | @@ -41,17 +40,12 @@ import { |
| 41 | type PreparedConnection, | 40 | type PreparedConnection, |
| 42 | type SupervisorConnectionState, | 41 | type SupervisorConnectionState, |
| 43 | } from "./model.ts"; | 42 | } from "./model.ts"; |
| 44 | import { | 43 | import * as Persistence from "../platform/persistence.ts"; |
| 45 | ConnectionPersistenceError, | 44 | import * as ConnectionProfileStore from "./profileStore.ts"; |
| 46 | ConnectionRegistrationStore, | 45 | import * as EnvironmentRegistry from "./registry.ts"; |
| 47 | ConnectionTargetStore, | 46 | import * as RpcSession from "../rpc/session.ts"; |
| 48 | EnvironmentCacheStore, | 47 | import * as EnvironmentSupervisor from "./supervisor.ts"; |
| 49 | EnvironmentOwnedDataCleanup, | 48 | import * as ConnectionWakeups from "./wakeups.ts"; |
| 50 | } from "../platform/persistence.ts"; | ||
| 51 | import { EnvironmentRegistry, environmentRegistryLayer } from "./registry.ts"; | ||
| 52 | import type { RpcSession } from "../rpc/session.ts"; | ||
| 53 | import { EnvironmentSupervisor } from "./supervisor.ts"; | ||
| 54 | import { ConnectionWakeups } from "./wakeups.ts"; | ||
| 55 | 49 | ||
| 56 | const TARGET = new PrimaryConnectionTarget({ | 50 | const TARGET = new PrimaryConnectionTarget({ |
| 57 | environmentId: EnvironmentId.make("environment-1"), | 51 | environmentId: EnvironmentId.make("environment-1"), |
| ... | @@ -137,10 +131,10 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( | ... | @@ -137,10 +131,10 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( |
| 137 | readonly beforeSessionConnect?: (environmentId: EnvironmentId) => Effect.Effect<void>; | 131 | readonly beforeSessionConnect?: (environmentId: EnvironmentId) => Effect.Effect<void>; |
| 138 | readonly beforeRegistrationRegister?: ( | 132 | readonly beforeRegistrationRegister?: ( |
| 139 | registration: ConnectionRegistration, | 133 | registration: ConnectionRegistration, |
| 140 | ) => Effect.Effect<void, ConnectionPersistenceError>; | 134 | ) => Effect.Effect<void, Persistence.ConnectionPersistenceError>; |
| 141 | readonly beforeRegistrationRemove?: ( | 135 | readonly beforeRegistrationRemove?: ( |
| 142 | target: ConnectionTarget, | 136 | target: ConnectionTarget, |
| 143 | ) => Effect.Effect<void, ConnectionPersistenceError>; | 137 | ) => Effect.Effect<void, Persistence.ConnectionPersistenceError>; |
| 144 | }, | 138 | }, |
| 145 | ) { | 139 | ) { |
| 146 | const storedTargets = yield* Ref.make( | 140 | const storedTargets = yield* Ref.make( |
| ... | @@ -160,7 +154,7 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( | ... | @@ -160,7 +154,7 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( |
| 160 | new Map([ | 154 | new Map([ |
| 161 | [ | 155 | [ |
| 162 | SSH_CONNECTION.environmentId, | 156 | SSH_CONNECTION.environmentId, |
| 163 | new RemoteDpopAccessToken({ | 157 | new TokenStore.RemoteDpopAccessToken({ |
| 164 | environmentId: SSH_CONNECTION.environmentId, | 158 | environmentId: SSH_CONNECTION.environmentId, |
| 165 | label: SSH_CONNECTION.label, | 159 | label: SSH_CONNECTION.label, |
| 166 | endpoint: { | 160 | endpoint: { |
| ... | @@ -177,10 +171,10 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( | ... | @@ -177,10 +171,10 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( |
| 177 | ); | 171 | ); |
| 178 | const disconnectedSshTargets = yield* Ref.make<ReadonlyArray<DesktopSshEnvironmentTarget>>([]); | 172 | const disconnectedSshTargets = yield* Ref.make<ReadonlyArray<DesktopSshEnvironmentTarget>>([]); |
| 179 | 173 | ||
| 180 | const targetStore = ConnectionTargetStore.of({ | 174 | const targetStore = Persistence.ConnectionTargetStore.of({ |
| 181 | list: Ref.get(storedTargets).pipe(Effect.map((targets) => [...targets.values()])), | 175 | list: Ref.get(storedTargets).pipe(Effect.map((targets) => [...targets.values()])), |
| 182 | }); | 176 | }); |
| 183 | const registrationStore = ConnectionRegistrationStore.of({ | 177 | const registrationStore = Persistence.ConnectionRegistrationStore.of({ |
| 184 | register: (registration) => | 178 | register: (registration) => |
| 185 | Effect.gen(function* () { | 179 | Effect.gen(function* () { |
| 186 | yield* options?.beforeRegistrationRegister?.(registration) ?? Effect.void; | 180 | yield* options?.beforeRegistrationRegister?.(registration) ?? Effect.void; |
| ... | @@ -239,7 +233,7 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( | ... | @@ -239,7 +233,7 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( |
| 239 | }); | 233 | }); |
| 240 | }), | 234 | }), |
| 241 | }); | 235 | }); |
| 242 | const cacheStore = EnvironmentCacheStore.of({ | 236 | const cacheStore = Persistence.EnvironmentCacheStore.of({ |
| 243 | loadShell: (environmentId) => | 237 | loadShell: (environmentId) => |
| 244 | Ref.get(shellCache).pipe( | 238 | Ref.get(shellCache).pipe( |
| 245 | Effect.map((cache) => Option.fromUndefinedOr(cache.get(environmentId))), | 239 | Effect.map((cache) => Option.fromUndefinedOr(cache.get(environmentId))), |
| ... | @@ -264,16 +258,16 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( | ... | @@ -264,16 +258,16 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( |
| 264 | ), | 258 | ), |
| 265 | ), | 259 | ), |
| 266 | }); | 260 | }); |
| 267 | const ownedDataCleanup = EnvironmentOwnedDataCleanup.of({ | 261 | const ownedDataCleanup = Persistence.EnvironmentOwnedDataCleanup.of({ |
| 268 | clear: (environmentId) => | 262 | clear: (environmentId) => |
| 269 | Ref.update(ownedDataClears, (environmentIds) => [...environmentIds, environmentId]), | 263 | Ref.update(ownedDataClears, (environmentIds) => [...environmentIds, environmentId]), |
| 270 | }); | 264 | }); |
| 271 | const networkStatus = yield* SubscriptionRef.make<"unknown" | "offline" | "online">("online"); | 265 | const networkStatus = yield* SubscriptionRef.make<"unknown" | "offline" | "online">("online"); |
| 272 | const connectivity = Connectivity.of({ | 266 | const connectivity = Connectivity.Connectivity.of({ |
| 273 | status: SubscriptionRef.get(networkStatus), | 267 | status: SubscriptionRef.get(networkStatus), |
| 274 | changes: SubscriptionRef.changes(networkStatus), | 268 | changes: SubscriptionRef.changes(networkStatus), |
| 275 | }); | 269 | }); |
| 276 | const profileStore = ConnectionProfileStore.of({ | 270 | const profileStore = ConnectionProfileStore.ConnectionProfileStore.of({ |
| 277 | get: (connectionId) => | 271 | get: (connectionId) => |
| 278 | Ref.update(profileReadCount, (count) => count + 1).pipe( | 272 | Ref.update(profileReadCount, (count) => count + 1).pipe( |
| 279 | Effect.andThen(Ref.get(storedProfiles)), | 273 | Effect.andThen(Ref.get(storedProfiles)), |
| ... | @@ -292,7 +286,7 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( | ... | @@ -292,7 +286,7 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( |
| 292 | return next; | 286 | return next; |
| 293 | }), | 287 | }), |
| 294 | }); | 288 | }); |
| 295 | const credentialStore = ConnectionCredentialStore.of({ | 289 | const credentialStore = ConnectionCredentialStore.ConnectionCredentialStore.of({ |
| 296 | get: (connectionId) => | 290 | get: (connectionId) => |
| 297 | Ref.get(storedCredentials).pipe( | 291 | Ref.get(storedCredentials).pipe( |
| 298 | Effect.map((current) => Option.fromUndefinedOr(current.get(connectionId))), | 292 | Effect.map((current) => Option.fromUndefinedOr(current.get(connectionId))), |
| ... | @@ -310,7 +304,7 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( | ... | @@ -310,7 +304,7 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( |
| 310 | return next; | 304 | return next; |
| 311 | }), | 305 | }), |
| 312 | }); | 306 | }); |
| 313 | const tokenStore = RemoteDpopAccessTokenStore.of({ | 307 | const tokenStore = TokenStore.RemoteDpopAccessTokenStore.of({ |
| 314 | get: (environmentId) => | 308 | get: (environmentId) => |
| 315 | Ref.get(storedRemoteTokens).pipe( | 309 | Ref.get(storedRemoteTokens).pipe( |
| 316 | Effect.map((current) => Option.fromUndefinedOr(current.get(environmentId))), | 310 | Effect.map((current) => Option.fromUndefinedOr(current.get(environmentId))), |
| ... | @@ -328,12 +322,12 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( | ... | @@ -328,12 +322,12 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( |
| 328 | return next; | 322 | return next; |
| 329 | }), | 323 | }), |
| 330 | }); | 324 | }); |
| 331 | const sshGateway = SshEnvironmentGateway.of({ | 325 | const sshGateway = ClientCapabilities.SshEnvironmentGateway.of({ |
| 332 | provision: () => Effect.die(new Error("SSH provisioning is not used.")), | 326 | provision: () => Effect.die(new Error("SSH provisioning is not used.")), |
| 333 | prepare: () => Effect.die(new Error("SSH preparation is not used.")), | 327 | prepare: () => Effect.die(new Error("SSH preparation is not used.")), |
| 334 | disconnect: (target) => Ref.update(disconnectedSshTargets, (current) => [...current, target]), | 328 | disconnect: (target) => Ref.update(disconnectedSshTargets, (current) => [...current, target]), |
| 335 | }); | 329 | }); |
| 336 | const driver = ConnectionDriver.of({ | 330 | const driver = ConnectionDriver.ConnectionDriver.of({ |
| 337 | connect: (entry, reportProgress) => | 331 | connect: (entry, reportProgress) => |
| 338 | Effect.gen(function* () { | 332 | Effect.gen(function* () { |
| 339 | const target = entry.target; | 333 | const target = entry.target; |
| ... | @@ -350,12 +344,12 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( | ... | @@ -350,12 +344,12 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( |
| 350 | yield* Ref.update(sessions, (current) => [...current, { closed }]); | 344 | yield* Ref.update(sessions, (current) => [...current, { closed }]); |
| 351 | const session = yield* Effect.acquireRelease( | 345 | const session = yield* Effect.acquireRelease( |
| 352 | Effect.succeed({ | 346 | Effect.succeed({ |
| 353 | client: {} as RpcSession["client"], | 347 | client: {} as RpcSession.RpcSession["client"], |
| 354 | initialConfig: Effect.die(new Error("Config is not used by registry tests.")), | 348 | initialConfig: Effect.die(new Error("Config is not used by registry tests.")), |
| 355 | ready: Effect.void, | 349 | ready: Effect.void, |
| 356 | probe: Effect.void, | 350 | probe: Effect.void, |
| 357 | closed: Deferred.await(closed), | 351 | closed: Deferred.await(closed), |
| 358 | } satisfies RpcSession), | 352 | } satisfies RpcSession.RpcSession), |
| 359 | () => Ref.update(releasedSessions, (count) => count + 1), | 353 | () => Ref.update(releasedSessions, (count) => count + 1), |
| 360 | ); | 354 | ); |
| 361 | yield* reportProgress({ stage: "synchronizing", prepared }); | 355 | yield* reportProgress({ stage: "synchronizing", prepared }); |
| ... | @@ -364,21 +358,24 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( | ... | @@ -364,21 +358,24 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( |
| 364 | }), | 358 | }), |
| 365 | }); | 359 | }); |
| 366 | 360 | ||
| 367 | const cacheLayer = Layer.succeed(EnvironmentCacheStore, cacheStore); | 361 | const cacheLayer = Layer.succeed(Persistence.EnvironmentCacheStore, cacheStore); |
| 368 | const layer = environmentRegistryLayer.pipe( | 362 | const layer = EnvironmentRegistry.layer.pipe( |
| 369 | Layer.provide( | 363 | Layer.provide( |
| 370 | Layer.mergeAll( | 364 | Layer.mergeAll( |
| 371 | Layer.succeed(ConnectionTargetStore, targetStore), | 365 | Layer.succeed(Persistence.ConnectionTargetStore, targetStore), |
| 372 | Layer.succeed(ConnectionRegistrationStore, registrationStore), | 366 | Layer.succeed(Persistence.ConnectionRegistrationStore, registrationStore), |
| 373 | Layer.succeed(ConnectionProfileStore, profileStore), | 367 | Layer.succeed(ConnectionProfileStore.ConnectionProfileStore, profileStore), |
| 374 | Layer.succeed(ConnectionCredentialStore, credentialStore), | 368 | Layer.succeed(ConnectionCredentialStore.ConnectionCredentialStore, credentialStore), |
| 375 | Layer.succeed(RemoteDpopAccessTokenStore, tokenStore), | 369 | Layer.succeed(TokenStore.RemoteDpopAccessTokenStore, tokenStore), |
| 376 | Layer.succeed(SshEnvironmentGateway, sshGateway), | 370 | Layer.succeed(ClientCapabilities.SshEnvironmentGateway, sshGateway), |
| 377 | Layer.succeed(Connectivity, connectivity), | 371 | Layer.succeed(Connectivity.Connectivity, connectivity), |
| 378 | Layer.succeed(ConnectionWakeups, ConnectionWakeups.of({ changes: Stream.never })), | 372 | Layer.succeed( |
| 379 | Layer.succeed(ConnectionDriver, driver), | 373 | ConnectionWakeups.ConnectionWakeups, |
| 374 | ConnectionWakeups.ConnectionWakeups.of({ changes: Stream.never }), | ||
| 375 | ), | ||
| 376 | Layer.succeed(ConnectionDriver.ConnectionDriver, driver), | ||
| 380 | cacheLayer, | 377 | cacheLayer, |
| 381 | Layer.succeed(EnvironmentOwnedDataCleanup, ownedDataCleanup), | 378 | Layer.succeed(Persistence.EnvironmentOwnedDataCleanup, ownedDataCleanup), |
| 382 | ), | 379 | ), |
| 383 | ), | 380 | ), |
| 384 | ); | 381 | ); |
| ... | @@ -401,7 +398,7 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( | ... | @@ -401,7 +398,7 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( |
| 401 | }); | 398 | }); |
| 402 | 399 | ||
| 403 | function awaitConnectionState( | 400 | function awaitConnectionState( |
| 404 | registry: EnvironmentRegistry["Service"], | 401 | registry: EnvironmentRegistry.EnvironmentRegistry["Service"], |
| 405 | environmentId: EnvironmentId, | 402 | environmentId: EnvironmentId, |
| 406 | predicate: (state: SupervisorConnectionState) => boolean, | 403 | predicate: (state: SupervisorConnectionState) => boolean, |
| 407 | ) { | 404 | ) { |
| ... | @@ -422,7 +419,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -422,7 +419,7 @@ describe("EnvironmentRegistry", () => { |
| 422 | const harness = yield* makeHarness([SSH_CONNECTION], [SSH_PROFILE]); | 419 | const harness = yield* makeHarness([SSH_CONNECTION], [SSH_PROFILE]); |
| 423 | 420 | ||
| 424 | yield* Effect.gen(function* () { | 421 | yield* Effect.gen(function* () { |
| 425 | const registry = yield* EnvironmentRegistry; | 422 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 426 | const entry = (yield* SubscriptionRef.get(registry.entries)).get( | 423 | const entry = (yield* SubscriptionRef.get(registry.entries)).get( |
| 427 | SSH_CONNECTION.environmentId, | 424 | SSH_CONNECTION.environmentId, |
| 428 | ); | 425 | ); |
| ... | @@ -438,7 +435,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -438,7 +435,7 @@ describe("EnvironmentRegistry", () => { |
| 438 | const harness = yield* makeHarness([]); | 435 | const harness = yield* makeHarness([]); |
| 439 | 436 | ||
| 440 | yield* Effect.gen(function* () { | 437 | yield* Effect.gen(function* () { |
| 441 | const registry = yield* EnvironmentRegistry; | 438 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 442 | const offline = yield* Effect.forkChild( | 439 | const offline = yield* Effect.forkChild( |
| 443 | SubscriptionRef.changes(registry.networkStatus).pipe( | 440 | SubscriptionRef.changes(registry.networkStatus).pipe( |
| 444 | Stream.filter((status) => status === "offline"), | 441 | Stream.filter((status) => status === "offline"), |
| ... | @@ -471,7 +468,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -471,7 +468,7 @@ describe("EnvironmentRegistry", () => { |
| 471 | }); | 468 | }); |
| 472 | 469 | ||
| 473 | yield* Effect.gen(function* () { | 470 | yield* Effect.gen(function* () { |
| 474 | const registry = yield* EnvironmentRegistry; | 471 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 475 | const start = yield* Effect.forkChild(registry.start); | 472 | const start = yield* Effect.forkChild(registry.start); |
| 476 | 473 | ||
| 477 | yield* Deferred.await(bothLoadsStarted).pipe(Effect.timeout("1 second")); | 474 | yield* Deferred.await(bothLoadsStarted).pipe(Effect.timeout("1 second")); |
| ... | @@ -487,7 +484,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -487,7 +484,7 @@ describe("EnvironmentRegistry", () => { |
| 487 | Effect.gen(function* () { | 484 | Effect.gen(function* () { |
| 488 | const harness = yield* makeHarness([TARGET]); | 485 | const harness = yield* makeHarness([TARGET]); |
| 489 | yield* Effect.gen(function* () { | 486 | yield* Effect.gen(function* () { |
| 490 | const registry = yield* EnvironmentRegistry; | 487 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 491 | yield* registry.start; | 488 | yield* registry.start; |
| 492 | yield* awaitConnectionState( | 489 | yield* awaitConnectionState( |
| 493 | registry, | 490 | registry, |
| ... | @@ -499,7 +496,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -499,7 +496,7 @@ describe("EnvironmentRegistry", () => { |
| 499 | .runStream( | 496 | .runStream( |
| 500 | TARGET.environmentId, | 497 | TARGET.environmentId, |
| 501 | Stream.unwrap( | 498 | Stream.unwrap( |
| 502 | EnvironmentSupervisor.pipe( | 499 | EnvironmentSupervisor.EnvironmentSupervisor.pipe( |
| 503 | Effect.map((supervisor) => | 500 | Effect.map((supervisor) => |
| 504 | Stream.concat( | 501 | Stream.concat( |
| 505 | Stream.fromEffect(SubscriptionRef.get(supervisor.state)), | 502 | Stream.fromEffect(SubscriptionRef.get(supervisor.state)), |
| ... | @@ -527,7 +524,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -527,7 +524,7 @@ describe("EnvironmentRegistry", () => { |
| 527 | Effect.gen(function* () { | 524 | Effect.gen(function* () { |
| 528 | const harness = yield* makeHarness([TARGET]); | 525 | const harness = yield* makeHarness([TARGET]); |
| 529 | yield* Effect.gen(function* () { | 526 | yield* Effect.gen(function* () { |
| 530 | const registry = yield* EnvironmentRegistry; | 527 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 531 | yield* registry.start; | 528 | yield* registry.start; |
| 532 | yield* awaitConnectionState( | 529 | yield* awaitConnectionState( |
| 533 | registry, | 530 | registry, |
| ... | @@ -554,7 +551,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -554,7 +551,7 @@ describe("EnvironmentRegistry", () => { |
| 554 | active!.closed, | 551 | active!.closed, |
| 555 | new ConnectionTransientError({ | 552 | new ConnectionTransientError({ |
| 556 | reason: "transport", | 553 | reason: "transport", |
| 557 | message: "Disconnected.", | 554 | detail: "Disconnected.", |
| 558 | }), | 555 | }), |
| 559 | ); | 556 | ); |
| 560 | yield* Fiber.join(retryFiber); | 557 | yield* Fiber.join(retryFiber); |
| ... | @@ -578,7 +575,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -578,7 +575,7 @@ describe("EnvironmentRegistry", () => { |
| 578 | const harness = yield* makeHarness([]); | 575 | const harness = yield* makeHarness([]); |
| 579 | 576 | ||
| 580 | yield* Effect.gen(function* () { | 577 | yield* Effect.gen(function* () { |
| 581 | const registry = yield* EnvironmentRegistry; | 578 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 582 | yield* registry.register(new RelayConnectionRegistration({ target: RELAY_TARGET })); | 579 | yield* registry.register(new RelayConnectionRegistration({ target: RELAY_TARGET })); |
| 583 | yield* awaitConnectionState( | 580 | yield* awaitConnectionState( |
| 584 | registry, | 581 | registry, |
| ... | @@ -603,7 +600,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -603,7 +600,7 @@ describe("EnvironmentRegistry", () => { |
| 603 | const harness = yield* makeHarness([RELAY_TARGET]); | 600 | const harness = yield* makeHarness([RELAY_TARGET]); |
| 604 | 601 | ||
| 605 | yield* Effect.gen(function* () { | 602 | yield* Effect.gen(function* () { |
| 606 | const registry = yield* EnvironmentRegistry; | 603 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 607 | const firstObserved = yield* Deferred.make<void>(); | 604 | const firstObserved = yield* Deferred.make<void>(); |
| 608 | const secondObserved = yield* Deferred.make<void>(); | 605 | const secondObserved = yield* Deferred.make<void>(); |
| 609 | const labels = yield* Ref.make<ReadonlyArray<string>>([]); | 606 | const labels = yield* Ref.make<ReadonlyArray<string>>([]); |
| ... | @@ -619,7 +616,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -619,7 +616,7 @@ describe("EnvironmentRegistry", () => { |
| 619 | .followStream( | 616 | .followStream( |
| 620 | RELAY_TARGET.environmentId, | 617 | RELAY_TARGET.environmentId, |
| 621 | Stream.unwrap( | 618 | Stream.unwrap( |
| 622 | EnvironmentSupervisor.pipe( | 619 | EnvironmentSupervisor.EnvironmentSupervisor.pipe( |
| 623 | Effect.map((supervisor) => | 620 | Effect.map((supervisor) => |
| 624 | Stream.concat(Stream.succeed(supervisor.target.label), Stream.never), | 621 | Stream.concat(Stream.succeed(supervisor.target.label), Stream.never), |
| 625 | ), | 622 | ), |
| ... | @@ -655,7 +652,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -655,7 +652,7 @@ describe("EnvironmentRegistry", () => { |
| 655 | const harness = yield* makeHarness([]); | 652 | const harness = yield* makeHarness([]); |
| 656 | 653 | ||
| 657 | yield* Effect.gen(function* () { | 654 | yield* Effect.gen(function* () { |
| 658 | const registry = yield* EnvironmentRegistry; | 655 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 659 | yield* registry.retryNow(EnvironmentId.make("removed-environment")); | 656 | yield* registry.retryNow(EnvironmentId.make("removed-environment")); |
| 660 | }).pipe(Effect.provide(harness.layer), Effect.scoped); | 657 | }).pipe(Effect.provide(harness.layer), Effect.scoped); |
| 661 | }), | 658 | }), |
| ... | @@ -670,7 +667,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -670,7 +667,7 @@ describe("EnvironmentRegistry", () => { |
| 670 | ); | 667 | ); |
| 671 | 668 | ||
| 672 | yield* Effect.gen(function* () { | 669 | yield* Effect.gen(function* () { |
| 673 | const registry = yield* EnvironmentRegistry; | 670 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 674 | yield* registry.removeRelayEnvironments(); | 671 | yield* registry.removeRelayEnvironments(); |
| 675 | 672 | ||
| 676 | const targets = yield* Ref.get(harness.storedTargets); | 673 | const targets = yield* Ref.get(harness.storedTargets); |
| ... | @@ -695,7 +692,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -695,7 +692,7 @@ describe("EnvironmentRegistry", () => { |
| 695 | const harness = yield* makeHarness([RELAY_TARGET], [], [], { | 692 | const harness = yield* makeHarness([RELAY_TARGET], [], [], { |
| 696 | beforeRegistrationRemove: () => | 693 | beforeRegistrationRemove: () => |
| 697 | Effect.fail( | 694 | Effect.fail( |
| 698 | new ConnectionPersistenceError({ | 695 | new Persistence.ConnectionPersistenceError({ |
| 699 | operation: "remove-connection", | 696 | operation: "remove-connection", |
| 700 | message: "Storage is unavailable.", | 697 | message: "Storage is unavailable.", |
| 701 | }), | 698 | }), |
| ... | @@ -703,7 +700,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -703,7 +700,7 @@ describe("EnvironmentRegistry", () => { |
| 703 | }); | 700 | }); |
| 704 | 701 | ||
| 705 | yield* Effect.gen(function* () { | 702 | yield* Effect.gen(function* () { |
| 706 | const registry = yield* EnvironmentRegistry; | 703 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 707 | yield* registry.start; | 704 | yield* registry.start; |
| 708 | yield* awaitConnectionState( | 705 | yield* awaitConnectionState( |
| 709 | registry, | 706 | registry, |
| ... | @@ -730,7 +727,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -730,7 +727,7 @@ describe("EnvironmentRegistry", () => { |
| 730 | const harness = yield* makeHarness([]); | 727 | const harness = yield* makeHarness([]); |
| 731 | 728 | ||
| 732 | yield* Effect.gen(function* () { | 729 | yield* Effect.gen(function* () { |
| 733 | const registry = yield* EnvironmentRegistry; | 730 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 734 | yield* registry.register( | 731 | yield* registry.register( |
| 735 | new BearerConnectionRegistration({ | 732 | new BearerConnectionRegistration({ |
| 736 | target: BEARER_TARGET, | 733 | target: BEARER_TARGET, |
| ... | @@ -760,7 +757,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -760,7 +757,7 @@ describe("EnvironmentRegistry", () => { |
| 760 | const harness = yield* makeHarness([]); | 757 | const harness = yield* makeHarness([]); |
| 761 | 758 | ||
| 762 | yield* Effect.gen(function* () { | 759 | yield* Effect.gen(function* () { |
| 763 | const registry = yield* EnvironmentRegistry; | 760 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 764 | yield* registry.registerPlatform(new PrimaryConnectionRegistration({ target: TARGET })); | 761 | yield* registry.registerPlatform(new PrimaryConnectionRegistration({ target: TARGET })); |
| 765 | yield* awaitConnectionState( | 762 | yield* awaitConnectionState( |
| 766 | registry, | 763 | registry, |
| ... | @@ -791,7 +788,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -791,7 +788,7 @@ describe("EnvironmentRegistry", () => { |
| 791 | const harness = yield* makeHarness([shadowedTarget]); | 788 | const harness = yield* makeHarness([shadowedTarget]); |
| 792 | 789 | ||
| 793 | yield* Effect.gen(function* () { | 790 | yield* Effect.gen(function* () { |
| 794 | const registry = yield* EnvironmentRegistry; | 791 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 795 | yield* registry.registerPlatform(new PrimaryConnectionRegistration({ target: TARGET })); | 792 | yield* registry.registerPlatform(new PrimaryConnectionRegistration({ target: TARGET })); |
| 796 | 793 | ||
| 797 | expect( | 794 | expect( |
| ... | @@ -825,7 +822,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -825,7 +822,7 @@ describe("EnvironmentRegistry", () => { |
| 825 | }); | 822 | }); |
| 826 | 823 | ||
| 827 | yield* Effect.gen(function* () { | 824 | yield* Effect.gen(function* () { |
| 828 | const registry = yield* EnvironmentRegistry; | 825 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 829 | const persistedRegistration = yield* registry | 826 | const persistedRegistration = yield* registry |
| 830 | .register(new RelayConnectionRegistration({ target: shadowedTarget })) | 827 | .register(new RelayConnectionRegistration({ target: shadowedTarget })) |
| 831 | .pipe(Effect.forkChild({ startImmediately: true })); | 828 | .pipe(Effect.forkChild({ startImmediately: true })); |
| ... | @@ -864,7 +861,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -864,7 +861,7 @@ describe("EnvironmentRegistry", () => { |
| 864 | }); | 861 | }); |
| 865 | 862 | ||
| 866 | yield* Effect.gen(function* () { | 863 | yield* Effect.gen(function* () { |
| 867 | const registry = yield* EnvironmentRegistry; | 864 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 868 | yield* registry.start; | 865 | yield* registry.start; |
| 869 | yield* awaitConnectionState( | 866 | yield* awaitConnectionState( |
| 870 | registry, | 867 | registry, |
| ... | @@ -894,7 +891,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -894,7 +891,7 @@ describe("EnvironmentRegistry", () => { |
| 894 | const harness = yield* makeHarness([]); | 891 | const harness = yield* makeHarness([]); |
| 895 | 892 | ||
| 896 | yield* Effect.gen(function* () { | 893 | yield* Effect.gen(function* () { |
| 897 | const registry = yield* EnvironmentRegistry; | 894 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 898 | const registration = new PrimaryConnectionRegistration({ target: TARGET }); | 895 | const registration = new PrimaryConnectionRegistration({ target: TARGET }); |
| 899 | yield* registry.registerPlatform(registration); | 896 | yield* registry.registerPlatform(registration); |
| 900 | yield* awaitConnectionState( | 897 | yield* awaitConnectionState( |
| ... | @@ -924,7 +921,7 @@ describe("EnvironmentRegistry", () => { | ... | @@ -924,7 +921,7 @@ describe("EnvironmentRegistry", () => { |
| 924 | ); | 921 | ); |
| 925 | 922 | ||
| 926 | yield* Effect.gen(function* () { | 923 | yield* Effect.gen(function* () { |
| 927 | const registry = yield* EnvironmentRegistry; | 924 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; |
| 928 | yield* registry.start; | 925 | yield* registry.start; |
| 929 | yield* registry.remove(SSH_CONNECTION.environmentId); | 926 | yield* registry.remove(SSH_CONNECTION.environmentId); |
| 930 | 927 |
packages/client-runtime/src/connection/registry.ts+107-102| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | import type { EnvironmentId } from "@t3tools/contracts"; | 1 | import { EnvironmentId } from "@t3tools/contracts"; |
| 2 | import * as Context from "effect/Context"; | 2 | import * as Context from "effect/Context"; |
| 3 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Equal from "effect/Equal"; | 4 | import * as Equal from "effect/Equal"; |
| ... | @@ -12,120 +12,124 @@ import * as Semaphore from "effect/Semaphore"; | ... | @@ -12,120 +12,124 @@ import * as Semaphore from "effect/Semaphore"; |
| 12 | import * as Stream from "effect/Stream"; | 12 | import * as Stream from "effect/Stream"; |
| 13 | import * as SubscriptionRef from "effect/SubscriptionRef"; | 13 | import * as SubscriptionRef from "effect/SubscriptionRef"; |
| 14 | 14 | ||
| 15 | import { SshEnvironmentGateway } from "../platform/capabilities.ts"; | 15 | import * as ClientCapabilities from "../platform/capabilities.ts"; |
| 16 | import { | 16 | import { |
| 17 | type ConnectionCatalogEntry, | 17 | type ConnectionCatalogEntry, |
| 18 | type ConnectionRegistration, | 18 | type ConnectionRegistration, |
| 19 | ConnectionProfileStore, | ||
| 20 | type PrimaryConnectionRegistration, | 19 | type PrimaryConnectionRegistration, |
| 21 | SshConnectionProfile, | 20 | SshConnectionProfile, |
| 22 | connectionRegistrationCatalogEntry, | 21 | connectionRegistrationCatalogEntry, |
| 23 | } from "./catalog.ts"; | 22 | } from "./catalog.ts"; |
| 24 | import { Connectivity } from "./connectivity.ts"; | 23 | import * as ConnectionProfileStore from "./profileStore.ts"; |
| 24 | import * as Connectivity from "./connectivity.ts"; | ||
| 25 | import type { | 25 | import type { |
| 26 | ConnectionAttemptError, | 26 | ConnectionAttemptError, |
| 27 | ConnectionTarget, | 27 | ConnectionTarget, |
| 28 | NetworkStatus, | 28 | NetworkStatus, |
| 29 | SupervisorConnectionState, | 29 | SupervisorConnectionState, |
| 30 | } from "./model.ts"; | 30 | } from "./model.ts"; |
| 31 | import { | 31 | import * as Persistence from "../platform/persistence.ts"; |
| 32 | type ConnectionPersistenceError, | 32 | import * as EnvironmentSupervisor from "./supervisor.ts"; |
| 33 | ConnectionRegistrationStore, | 33 | import * as ConnectionDriver from "./driver.ts"; |
| 34 | ConnectionTargetStore, | 34 | import * as ConnectionWakeups from "./wakeups.ts"; |
| 35 | EnvironmentCacheStore, | ||
| 36 | EnvironmentOwnedDataCleanup, | ||
| 37 | } from "../platform/persistence.ts"; | ||
| 38 | import { | ||
| 39 | EnvironmentSupervisor, | ||
| 40 | type EnvironmentSupervisorService, | ||
| 41 | makeEnvironmentSupervisor, | ||
| 42 | } from "./supervisor.ts"; | ||
| 43 | import { ConnectionDriver } from "./driver.ts"; | ||
| 44 | import { ConnectionWakeups } from "./wakeups.ts"; | ||
| 45 | 35 | ||
| 46 | const isSshConnectionProfile = Schema.is(SshConnectionProfile); | 36 | const isSshConnectionProfile = Schema.is(SshConnectionProfile); |
| 47 | 37 | ||
| 48 | export class EnvironmentNotRegisteredError extends Schema.TaggedErrorClass<EnvironmentNotRegisteredError>()( | 38 | export class EnvironmentNotRegisteredError extends Schema.TaggedErrorClass<EnvironmentNotRegisteredError>()( |
| 49 | "EnvironmentNotRegisteredError", | 39 | "EnvironmentNotRegisteredError", |
| 50 | { | 40 | { |
| 51 | environmentId: Schema.String, | 41 | environmentId: EnvironmentId, |
| 52 | message: Schema.String, | ||
| 53 | }, | 42 | }, |
| 54 | ) {} | 43 | ) { |
| 44 | override get message(): string { | ||
| 45 | return `Environment ${this.environmentId} is not registered.`; | ||
| 46 | } | ||
| 47 | } | ||
| 55 | 48 | ||
| 56 | export class PlatformEnvironmentRemovalError extends Schema.TaggedErrorClass<PlatformEnvironmentRemovalError>()( | 49 | export class PlatformEnvironmentRemovalError extends Schema.TaggedErrorClass<PlatformEnvironmentRemovalError>()( |
| 57 | "PlatformEnvironmentRemovalError", | 50 | "PlatformEnvironmentRemovalError", |
| 58 | { | 51 | { |
| 59 | environmentId: Schema.String, | ||
| 60 | message: Schema.String, | ||
| 61 | }, | ||
| 62 | ) {} | ||
| 63 | |||
| 64 | export interface EnvironmentRegistryService { | ||
| 65 | readonly entries: SubscriptionRef.SubscriptionRef< | ||
| 66 | ReadonlyMap<EnvironmentId, ConnectionCatalogEntry> | ||
| 67 | >; | ||
| 68 | readonly networkStatus: SubscriptionRef.SubscriptionRef<NetworkStatus>; | ||
| 69 | readonly start: Effect.Effect<void>; | ||
| 70 | readonly register: ( | ||
| 71 | registration: ConnectionRegistration, | ||
| 72 | ) => Effect.Effect<void, ConnectionPersistenceError>; | ||
| 73 | readonly registerPlatform: (registration: PrimaryConnectionRegistration) => Effect.Effect<void>; | ||
| 74 | readonly remove: ( | ||
| 75 | environmentId: EnvironmentId, | 52 | environmentId: EnvironmentId, |
| 76 | ) => Effect.Effect< | 53 | }, |
| 77 | void, | 54 | ) { |
| 78 | | ConnectionPersistenceError | 55 | override get message(): string { |
| 79 | | ConnectionAttemptError | 56 | return `Platform-managed environment ${this.environmentId} cannot be removed.`; |
| 80 | | EnvironmentNotRegisteredError | 57 | } |
| 81 | | PlatformEnvironmentRemovalError | ||
| 82 | >; | ||
| 83 | readonly removeRelayEnvironments: () => Effect.Effect< | ||
| 84 | void, | ||
| 85 | ConnectionPersistenceError | ConnectionAttemptError | PlatformEnvironmentRemovalError | ||
| 86 | >; | ||
| 87 | readonly retryNow: (environmentId: EnvironmentId) => Effect.Effect<void>; | ||
| 88 | readonly state: ( | ||
| 89 | environmentId: EnvironmentId, | ||
| 90 | ) => Effect.Effect<SupervisorConnectionState, EnvironmentNotRegisteredError>; | ||
| 91 | readonly stateChanges: ( | ||
| 92 | environmentId: EnvironmentId, | ||
| 93 | ) => Stream.Stream<SupervisorConnectionState, EnvironmentNotRegisteredError>; | ||
| 94 | readonly run: <A, E, R>( | ||
| 95 | environmentId: EnvironmentId, | ||
| 96 | effect: Effect.Effect<A, E, R>, | ||
| 97 | ) => Effect.Effect<A, E | EnvironmentNotRegisteredError, Exclude<R, EnvironmentSupervisor>>; | ||
| 98 | readonly runStream: <A, E, R>( | ||
| 99 | environmentId: EnvironmentId, | ||
| 100 | stream: Stream.Stream<A, E, R>, | ||
| 101 | ) => Stream.Stream<A, E | EnvironmentNotRegisteredError, Exclude<R, EnvironmentSupervisor>>; | ||
| 102 | readonly followStream: <A, E, R>( | ||
| 103 | environmentId: EnvironmentId, | ||
| 104 | stream: Stream.Stream<A, E, R>, | ||
| 105 | ) => Stream.Stream<A, E, Exclude<R, EnvironmentSupervisor>>; | ||
| 106 | } | 58 | } |
| 107 | 59 | ||
| 108 | export class EnvironmentRegistry extends Context.Service< | 60 | export class EnvironmentRegistry extends Context.Service< |
| 109 | EnvironmentRegistry, | 61 | EnvironmentRegistry, |
| 110 | EnvironmentRegistryService | 62 | { |
| 63 | readonly entries: SubscriptionRef.SubscriptionRef< | ||
| 64 | ReadonlyMap<EnvironmentId, ConnectionCatalogEntry> | ||
| 65 | >; | ||
| 66 | readonly networkStatus: SubscriptionRef.SubscriptionRef<NetworkStatus>; | ||
| 67 | readonly start: Effect.Effect<void>; | ||
| 68 | readonly register: ( | ||
| 69 | registration: ConnectionRegistration, | ||
| 70 | ) => Effect.Effect<void, Persistence.ConnectionPersistenceError>; | ||
| 71 | readonly registerPlatform: (registration: PrimaryConnectionRegistration) => Effect.Effect<void>; | ||
| 72 | readonly remove: ( | ||
| 73 | environmentId: EnvironmentId, | ||
| 74 | ) => Effect.Effect< | ||
| 75 | void, | ||
| 76 | | Persistence.ConnectionPersistenceError | ||
| 77 | | ConnectionAttemptError | ||
| 78 | | EnvironmentNotRegisteredError | ||
| 79 | | PlatformEnvironmentRemovalError | ||
| 80 | >; | ||
| 81 | readonly removeRelayEnvironments: () => Effect.Effect< | ||
| 82 | void, | ||
| 83 | | Persistence.ConnectionPersistenceError | ||
| 84 | | ConnectionAttemptError | ||
| 85 | | PlatformEnvironmentRemovalError | ||
| 86 | >; | ||
| 87 | readonly retryNow: (environmentId: EnvironmentId) => Effect.Effect<void>; | ||
| 88 | readonly state: ( | ||
| 89 | environmentId: EnvironmentId, | ||
| 90 | ) => Effect.Effect<SupervisorConnectionState, EnvironmentNotRegisteredError>; | ||
| 91 | readonly stateChanges: ( | ||
| 92 | environmentId: EnvironmentId, | ||
| 93 | ) => Stream.Stream<SupervisorConnectionState, EnvironmentNotRegisteredError>; | ||
| 94 | readonly run: <A, E, R>( | ||
| 95 | environmentId: EnvironmentId, | ||
| 96 | effect: Effect.Effect<A, E, R>, | ||
| 97 | ) => Effect.Effect< | ||
| 98 | A, | ||
| 99 | E | EnvironmentNotRegisteredError, | ||
| 100 | Exclude<R, EnvironmentSupervisor.EnvironmentSupervisor> | ||
| 101 | >; | ||
| 102 | readonly runStream: <A, E, R>( | ||
| 103 | environmentId: EnvironmentId, | ||
| 104 | stream: Stream.Stream<A, E, R>, | ||
| 105 | ) => Stream.Stream< | ||
| 106 | A, | ||
| 107 | E | EnvironmentNotRegisteredError, | ||
| 108 | Exclude<R, EnvironmentSupervisor.EnvironmentSupervisor> | ||
| 109 | >; | ||
| 110 | readonly followStream: <A, E, R>( | ||
| 111 | environmentId: EnvironmentId, | ||
| 112 | stream: Stream.Stream<A, E, R>, | ||
| 113 | ) => Stream.Stream<A, E, Exclude<R, EnvironmentSupervisor.EnvironmentSupervisor>>; | ||
| 114 | } | ||
| 111 | >()("@t3tools/client-runtime/connection/registry/EnvironmentRegistry") {} | 115 | >()("@t3tools/client-runtime/connection/registry/EnvironmentRegistry") {} |
| 112 | 116 | ||
| 113 | interface EnvironmentServiceScope { | 117 | interface EnvironmentServiceScope { |
| 114 | readonly entry: ConnectionCatalogEntry; | 118 | readonly entry: ConnectionCatalogEntry; |
| 115 | readonly supervisor: EnvironmentSupervisorService; | 119 | readonly supervisor: EnvironmentSupervisor.EnvironmentSupervisor["Service"]; |
| 116 | readonly scope: Scope.Closeable; | 120 | readonly scope: Scope.Closeable; |
| 117 | } | 121 | } |
| 118 | 122 | ||
| 119 | const makeEnvironmentRegistry = Effect.fn("EnvironmentRegistry.make")(function* () { | 123 | export const make = Effect.gen(function* () { |
| 120 | const storage = yield* ConnectionTargetStore; | 124 | const storage = yield* Persistence.ConnectionTargetStore; |
| 121 | const registrations = yield* ConnectionRegistrationStore; | 125 | const registrations = yield* Persistence.ConnectionRegistrationStore; |
| 122 | const cache = yield* EnvironmentCacheStore; | 126 | const cache = yield* Persistence.EnvironmentCacheStore; |
| 123 | const ownedDataCleanup = yield* EnvironmentOwnedDataCleanup; | 127 | const ownedDataCleanup = yield* Persistence.EnvironmentOwnedDataCleanup; |
| 124 | const profiles = yield* ConnectionProfileStore; | 128 | const profiles = yield* ConnectionProfileStore.ConnectionProfileStore; |
| 125 | const connectivity = yield* Connectivity; | 129 | const connectivity = yield* Connectivity.Connectivity; |
| 126 | const driver = yield* ConnectionDriver; | 130 | const driver = yield* ConnectionDriver.ConnectionDriver; |
| 127 | const wakeups = yield* ConnectionWakeups; | 131 | const wakeups = yield* ConnectionWakeups.ConnectionWakeups; |
| 128 | const ssh = yield* SshEnvironmentGateway; | 132 | const ssh = yield* ClientCapabilities.SshEnvironmentGateway; |
| 129 | const persistedTargets = yield* storage.list; | 133 | const persistedTargets = yield* storage.list; |
| 130 | const initialEntries = new Map( | 134 | const initialEntries = new Map( |
| 131 | yield* Effect.forEach( | 135 | yield* Effect.forEach( |
| ... | @@ -215,7 +219,6 @@ const makeEnvironmentRegistry = Effect.fn("EnvironmentRegistry.make")(function* | ... | @@ -215,7 +219,6 @@ const makeEnvironmentRegistry = Effect.fn("EnvironmentRegistry.make")(function* |
| 215 | if (entry === undefined) { | 219 | if (entry === undefined) { |
| 216 | return yield* new EnvironmentNotRegisteredError({ | 220 | return yield* new EnvironmentNotRegisteredError({ |
| 217 | environmentId, | 221 | environmentId, |
| 218 | message: `Environment ${environmentId} is not registered.`, | ||
| 219 | }); | 222 | }); |
| 220 | } | 223 | } |
| 221 | return entry; | 224 | return entry; |
| ... | @@ -241,12 +244,12 @@ const makeEnvironmentRegistry = Effect.fn("EnvironmentRegistry.make")(function* | ... | @@ -241,12 +244,12 @@ const makeEnvironmentRegistry = Effect.fn("EnvironmentRegistry.make")(function* |
| 241 | Effect.gen(function* () { | 244 | Effect.gen(function* () { |
| 242 | const environmentId = entry.target.environmentId; | 245 | const environmentId = entry.target.environmentId; |
| 243 | const scope = yield* Scope.make(); | 246 | const scope = yield* Scope.make(); |
| 244 | const supervisor = yield* makeEnvironmentSupervisor(entry, { | 247 | const supervisor = yield* EnvironmentSupervisor.make(entry, { |
| 245 | initiallyDesired: false, | 248 | initiallyDesired: false, |
| 246 | }).pipe( | 249 | }).pipe( |
| 247 | Effect.provideService(Connectivity, connectivity), | 250 | Effect.provideService(Connectivity.Connectivity, connectivity), |
| 248 | Effect.provideService(ConnectionDriver, driver), | 251 | Effect.provideService(ConnectionDriver.ConnectionDriver, driver), |
| 249 | Effect.provideService(ConnectionWakeups, wakeups), | 252 | Effect.provideService(ConnectionWakeups.ConnectionWakeups, wakeups), |
| 250 | Scope.provide(scope), | 253 | Scope.provide(scope), |
| 251 | Effect.onError(() => Scope.close(scope, Exit.void)), | 254 | Effect.onError(() => Scope.close(scope, Exit.void)), |
| 252 | ); | 255 | ); |
| ... | @@ -280,28 +283,30 @@ const makeEnvironmentRegistry = Effect.fn("EnvironmentRegistry.make")(function* | ... | @@ -280,28 +283,30 @@ const makeEnvironmentRegistry = Effect.fn("EnvironmentRegistry.make")(function* |
| 280 | ); | 283 | ); |
| 281 | }); | 284 | }); |
| 282 | 285 | ||
| 283 | const run: EnvironmentRegistryService["run"] = Effect.fn("EnvironmentRegistry.run")(function* < | 286 | const run: EnvironmentRegistry["Service"]["run"] = Effect.fn("EnvironmentRegistry.run")( |
| 284 | A, | 287 | function* <A, E, R>(environmentId: EnvironmentId, effect: Effect.Effect<A, E, R>) { |
| 285 | E, | 288 | const supervisor = yield* acquireSupervisor(environmentId); |
| 286 | R, | 289 | return yield* Effect.provideService( |
| 287 | >(environmentId: EnvironmentId, effect: Effect.Effect<A, E, R>) { | 290 | effect, |
| 288 | const supervisor = yield* acquireSupervisor(environmentId); | 291 | EnvironmentSupervisor.EnvironmentSupervisor, |
| 289 | return yield* Effect.provideService(effect, EnvironmentSupervisor, supervisor); | 292 | supervisor, |
| 290 | }); | 293 | ); |
| 294 | }, | ||
| 295 | ); | ||
| 291 | 296 | ||
| 292 | const runStream: EnvironmentRegistryService["runStream"] = <A, E, R>( | 297 | const runStream: EnvironmentRegistry["Service"]["runStream"] = <A, E, R>( |
| 293 | environmentId: EnvironmentId, | 298 | environmentId: EnvironmentId, |
| 294 | stream: Stream.Stream<A, E, R>, | 299 | stream: Stream.Stream<A, E, R>, |
| 295 | ) => | 300 | ) => |
| 296 | Stream.unwrap( | 301 | Stream.unwrap( |
| 297 | acquireSupervisor(environmentId).pipe( | 302 | acquireSupervisor(environmentId).pipe( |
| 298 | Effect.map((supervisor) => | 303 | Effect.map((supervisor) => |
| 299 | Stream.provideService(stream, EnvironmentSupervisor, supervisor), | 304 | Stream.provideService(stream, EnvironmentSupervisor.EnvironmentSupervisor, supervisor), |
| 300 | ), | 305 | ), |
| 301 | ), | 306 | ), |
| 302 | ); | 307 | ); |
| 303 | 308 | ||
| 304 | const followStream: EnvironmentRegistryService["followStream"] = <A, E, R>( | 309 | const followStream: EnvironmentRegistry["Service"]["followStream"] = <A, E, R>( |
| 305 | environmentId: EnvironmentId, | 310 | environmentId: EnvironmentId, |
| 306 | stream: Stream.Stream<A, E, R>, | 311 | stream: Stream.Stream<A, E, R>, |
| 307 | ) => | 312 | ) => |
| ... | @@ -320,7 +325,11 @@ const makeEnvironmentRegistry = Effect.fn("EnvironmentRegistry.make")(function* | ... | @@ -320,7 +325,11 @@ const makeEnvironmentRegistry = Effect.fn("EnvironmentRegistry.make")(function* |
| 320 | Effect.match({ | 325 | Effect.match({ |
| 321 | onFailure: () => Stream.empty, | 326 | onFailure: () => Stream.empty, |
| 322 | onSuccess: (supervisor) => | 327 | onSuccess: (supervisor) => |
| 323 | Stream.provideService(stream, EnvironmentSupervisor, supervisor), | 328 | Stream.provideService( |
| 329 | stream, | ||
| 330 | EnvironmentSupervisor.EnvironmentSupervisor, | ||
| 331 | supervisor, | ||
| 332 | ), | ||
| 324 | }), | 333 | }), |
| 325 | ), | 334 | ), |
| 326 | ), | 335 | ), |
| ... | @@ -443,7 +452,6 @@ const makeEnvironmentRegistry = Effect.fn("EnvironmentRegistry.make")(function* | ... | @@ -443,7 +452,6 @@ const makeEnvironmentRegistry = Effect.fn("EnvironmentRegistry.make")(function* |
| 443 | if ((yield* Ref.get(platformEnvironmentIds)).has(environmentId)) { | 452 | if ((yield* Ref.get(platformEnvironmentIds)).has(environmentId)) { |
| 444 | return yield* new PlatformEnvironmentRemovalError({ | 453 | return yield* new PlatformEnvironmentRemovalError({ |
| 445 | environmentId, | 454 | environmentId, |
| 446 | message: "Platform-managed environments cannot be removed.", | ||
| 447 | }); | 455 | }); |
| 448 | } | 456 | } |
| 449 | const target = (yield* getEntry(environmentId)).target; | 457 | const target = (yield* getEntry(environmentId)).target; |
| ... | @@ -532,7 +540,7 @@ const makeEnvironmentRegistry = Effect.fn("EnvironmentRegistry.make")(function* | ... | @@ -532,7 +540,7 @@ const makeEnvironmentRegistry = Effect.fn("EnvironmentRegistry.make")(function* |
| 532 | followStream( | 540 | followStream( |
| 533 | environmentId, | 541 | environmentId, |
| 534 | Stream.unwrap( | 542 | Stream.unwrap( |
| 535 | EnvironmentSupervisor.pipe( | 543 | EnvironmentSupervisor.EnvironmentSupervisor.pipe( |
| 536 | Effect.map((supervisor) => SubscriptionRef.changes(supervisor.state)), | 544 | Effect.map((supervisor) => SubscriptionRef.changes(supervisor.state)), |
| 537 | ), | 545 | ), |
| 538 | ), | 546 | ), |
| ... | @@ -570,7 +578,4 @@ const makeEnvironmentRegistry = Effect.fn("EnvironmentRegistry.make")(function* | ... | @@ -570,7 +578,4 @@ const makeEnvironmentRegistry = Effect.fn("EnvironmentRegistry.make")(function* |
| 570 | }); | 578 | }); |
| 571 | }); | 579 | }); |
| 572 | 580 | ||
| 573 | export const environmentRegistryLayer = Layer.effect( | 581 | export const layer = Layer.effect(EnvironmentRegistry, make); |
| 574 | EnvironmentRegistry, | ||
| 575 | makeEnvironmentRegistry(), | ||
| 576 | ); |
packages/client-runtime/src/connection/resolver.test.ts+43-46| ... | @@ -8,30 +8,19 @@ import * as Option from "effect/Option"; | ... | @@ -8,30 +8,19 @@ import * as Option from "effect/Option"; |
| 8 | import * as Ref from "effect/Ref"; | 8 | import * as Ref from "effect/Ref"; |
| 9 | import * as Tracer from "effect/Tracer"; | 9 | import * as Tracer from "effect/Tracer"; |
| 10 | 10 | ||
| 11 | import { | 11 | import * as ManagedRelay from "../relay/managedRelay.ts"; |
| 12 | ManagedRelayClient, | 12 | import * as ConnectionResolver from "./resolver.ts"; |
| 13 | ManagedRelayClientError, | 13 | import * as ClientCapabilities from "../platform/capabilities.ts"; |
| 14 | ManagedRelayRequestTimeoutError, | 14 | import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; |
| 15 | } from "../relay/managedRelay.ts"; | ||
| 16 | import { ConnectionResolver } from "./resolver.ts"; | ||
| 17 | import { connectionResolverLayer } from "./resolver.ts"; | ||
| 18 | import { | ||
| 19 | CloudSession, | ||
| 20 | PrimaryEnvironmentAuth, | ||
| 21 | RelayDeviceIdentity, | ||
| 22 | SshEnvironmentGateway, | ||
| 23 | } from "../platform/capabilities.ts"; | ||
| 24 | import { RemoteEnvironmentAuthorization } from "../authorization/service.ts"; | ||
| 25 | import { | 15 | import { |
| 26 | BearerConnectionCredential, | 16 | BearerConnectionCredential, |
| 27 | BearerConnectionProfile, | 17 | BearerConnectionProfile, |
| 28 | type ConnectionCatalogEntry, | 18 | type ConnectionCatalogEntry, |
| 29 | ConnectionCredentialStore, | ||
| 30 | ConnectionProfileStore, | ||
| 31 | SshConnectionProfile, | 19 | SshConnectionProfile, |
| 32 | type ConnectionCredential, | 20 | type ConnectionCredential, |
| 33 | type ConnectionProfile, | 21 | type ConnectionProfile, |
| 34 | } from "./catalog.ts"; | 22 | } from "./catalog.ts"; |
| 23 | import * as ConnectionCredentialStore from "./credentialStore.ts"; | ||
| 35 | import { | 24 | import { |
| 36 | BearerConnectionTarget, | 25 | BearerConnectionTarget, |
| 37 | ConnectionTransientError, | 26 | ConnectionTransientError, |
| ... | @@ -40,6 +29,7 @@ import { | ... | @@ -40,6 +29,7 @@ import { |
| 40 | SshConnectionTarget, | 29 | SshConnectionTarget, |
| 41 | type ConnectionTarget, | 30 | type ConnectionTarget, |
| 42 | } from "./model.ts"; | 31 | } from "./model.ts"; |
| 32 | import * as ConnectionProfileStore from "./profileStore.ts"; | ||
| 43 | 33 | ||
| 44 | const ENVIRONMENT_ID = EnvironmentId.make("environment-1"); | 34 | const ENVIRONMENT_ID = EnvironmentId.make("environment-1"); |
| 45 | const ENDPOINT = { | 35 | const ENDPOINT = { |
| ... | @@ -79,8 +69,10 @@ function collectingTracer(spans: Array<string>): Tracer.Tracer { | ... | @@ -79,8 +69,10 @@ function collectingTracer(spans: Array<string>): Tracer.Tracer { |
| 79 | }); | 69 | }); |
| 80 | } | 70 | } |
| 81 | 71 | ||
| 82 | function relayClient(connectEnvironment: ManagedRelayClient["Service"]["connectEnvironment"]) { | 72 | function relayClient( |
| 83 | return ManagedRelayClient.of({ | 73 | connectEnvironment: ManagedRelay.ManagedRelayClient["Service"]["connectEnvironment"], |
| 74 | ) { | ||
| 75 | return ManagedRelay.ManagedRelayClient.of({ | ||
| 84 | relayUrl: "https://relay.example.test", | 76 | relayUrl: "https://relay.example.test", |
| 85 | listEnvironments: () => unsupported("listEnvironments"), | 77 | listEnvironments: () => unsupported("listEnvironments"), |
| 86 | listDevices: () => unsupported("listDevices"), | 78 | listDevices: () => unsupported("listDevices"), |
| ... | @@ -99,29 +91,29 @@ function relayClient(connectEnvironment: ManagedRelayClient["Service"]["connectE | ... | @@ -99,29 +91,29 @@ function relayClient(connectEnvironment: ManagedRelayClient["Service"]["connectE |
| 99 | const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((options?: { | 91 | const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((options?: { |
| 100 | readonly profiles?: ReadonlyArray<ConnectionProfile>; | 92 | readonly profiles?: ReadonlyArray<ConnectionProfile>; |
| 101 | readonly credentials?: ReadonlyArray<readonly [string, ConnectionCredential]>; | 93 | readonly credentials?: ReadonlyArray<readonly [string, ConnectionCredential]>; |
| 102 | readonly connectEnvironment?: ManagedRelayClient["Service"]["connectEnvironment"]; | 94 | readonly connectEnvironment?: ManagedRelay.ManagedRelayClient["Service"]["connectEnvironment"]; |
| 103 | readonly authorizeBearer?: RemoteEnvironmentAuthorization["Service"]["authorizeBearer"]; | 95 | readonly authorizeBearer?: RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization["Service"]["authorizeBearer"]; |
| 104 | readonly authorizeDpop?: RemoteEnvironmentAuthorization["Service"]["authorizeDpop"]; | 96 | readonly authorizeDpop?: RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization["Service"]["authorizeDpop"]; |
| 105 | readonly primaryBearerToken?: string; | 97 | readonly primaryBearerToken?: string; |
| 106 | readonly prepareSsh?: SshEnvironmentGateway["Service"]["prepare"]; | 98 | readonly prepareSsh?: ClientCapabilities.SshEnvironmentGateway["Service"]["prepare"]; |
| 107 | }) => { | 99 | }) => { |
| 108 | const profiles = new Map( | 100 | const profiles = new Map( |
| 109 | (options?.profiles ?? []).map((profile) => [profile.connectionId, profile]), | 101 | (options?.profiles ?? []).map((profile) => [profile.connectionId, profile]), |
| 110 | ); | 102 | ); |
| 111 | const credentials = new Map(options?.credentials ?? []); | 103 | const credentials = new Map(options?.credentials ?? []); |
| 112 | 104 | ||
| 113 | const profileStore = ConnectionProfileStore.of({ | 105 | const profileStore = ConnectionProfileStore.ConnectionProfileStore.of({ |
| 114 | get: (connectionId) => Effect.succeed(Option.fromNullishOr(profiles.get(connectionId))), | 106 | get: (connectionId) => Effect.succeed(Option.fromNullishOr(profiles.get(connectionId))), |
| 115 | put: (profile) => Effect.sync(() => void profiles.set(profile.connectionId, profile)), | 107 | put: (profile) => Effect.sync(() => void profiles.set(profile.connectionId, profile)), |
| 116 | remove: (connectionId) => Effect.sync(() => void profiles.delete(connectionId)), | 108 | remove: (connectionId) => Effect.sync(() => void profiles.delete(connectionId)), |
| 117 | }); | 109 | }); |
| 118 | const credentialStore = ConnectionCredentialStore.of({ | 110 | const credentialStore = ConnectionCredentialStore.ConnectionCredentialStore.of({ |
| 119 | get: (connectionId) => Effect.succeed(Option.fromNullishOr(credentials.get(connectionId))), | 111 | get: (connectionId) => Effect.succeed(Option.fromNullishOr(credentials.get(connectionId))), |
| 120 | put: (connectionId, credential) => | 112 | put: (connectionId, credential) => |
| 121 | Effect.sync(() => void credentials.set(connectionId, credential)), | 113 | Effect.sync(() => void credentials.set(connectionId, credential)), |
| 122 | remove: (connectionId) => Effect.sync(() => void credentials.delete(connectionId)), | 114 | remove: (connectionId) => Effect.sync(() => void credentials.delete(connectionId)), |
| 123 | }); | 115 | }); |
| 124 | const remote = RemoteEnvironmentAuthorization.of({ | 116 | const remote = RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization.of({ |
| 125 | authorizeBearer: | 117 | authorizeBearer: |
| 126 | options?.authorizeBearer ?? | 118 | options?.authorizeBearer ?? |
| 127 | ((input) => | 119 | ((input) => |
| ... | @@ -151,7 +143,7 @@ const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((o | ... | @@ -151,7 +143,7 @@ const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((o |
| 151 | }), | 143 | }), |
| 152 | )), | 144 | )), |
| 153 | }); | 145 | }); |
| 154 | const ssh = SshEnvironmentGateway.of({ | 146 | const ssh = ClientCapabilities.SshEnvironmentGateway.of({ |
| 155 | provision: () => Effect.die("unused"), | 147 | provision: () => Effect.die("unused"), |
| 156 | prepare: | 148 | prepare: |
| 157 | options?.prepareSsh ?? | 149 | options?.prepareSsh ?? |
| ... | @@ -169,23 +161,28 @@ const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((o | ... | @@ -169,23 +161,28 @@ const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((o |
| 169 | }); | 161 | }); |
| 170 | 162 | ||
| 171 | const dependencies = Layer.mergeAll( | 163 | const dependencies = Layer.mergeAll( |
| 172 | Layer.succeed(ConnectionProfileStore, profileStore), | 164 | Layer.succeed(ConnectionProfileStore.ConnectionProfileStore, profileStore), |
| 173 | Layer.succeed(ConnectionCredentialStore, credentialStore), | 165 | Layer.succeed(ConnectionCredentialStore.ConnectionCredentialStore, credentialStore), |
| 174 | Layer.succeed(CloudSession, CloudSession.of({ clerkToken: Effect.succeed("clerk-session") })), | ||
| 175 | Layer.succeed( | 166 | Layer.succeed( |
| 176 | PrimaryEnvironmentAuth, | 167 | ClientCapabilities.CloudSession, |
| 177 | PrimaryEnvironmentAuth.of({ | 168 | ClientCapabilities.CloudSession.of({ clerkToken: Effect.succeed("clerk-session") }), |
| 169 | ), | ||
| 170 | Layer.succeed( | ||
| 171 | ClientCapabilities.PrimaryEnvironmentAuth, | ||
| 172 | ClientCapabilities.PrimaryEnvironmentAuth.of({ | ||
| 178 | bearerToken: Effect.succeed(Option.fromNullishOr(options?.primaryBearerToken)), | 173 | bearerToken: Effect.succeed(Option.fromNullishOr(options?.primaryBearerToken)), |
| 179 | }), | 174 | }), |
| 180 | ), | 175 | ), |
| 181 | Layer.succeed( | 176 | Layer.succeed( |
| 182 | RelayDeviceIdentity, | 177 | ClientCapabilities.RelayDeviceIdentity, |
| 183 | RelayDeviceIdentity.of({ deviceId: Effect.succeed(Option.some("device-1")) }), | 178 | ClientCapabilities.RelayDeviceIdentity.of({ |
| 179 | deviceId: Effect.succeed(Option.some("device-1")), | ||
| 180 | }), | ||
| 184 | ), | 181 | ), |
| 185 | Layer.succeed(RemoteEnvironmentAuthorization, remote), | 182 | Layer.succeed(RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization, remote), |
| 186 | Layer.succeed(SshEnvironmentGateway, ssh), | 183 | Layer.succeed(ClientCapabilities.SshEnvironmentGateway, ssh), |
| 187 | Layer.succeed( | 184 | Layer.succeed( |
| 188 | ManagedRelayClient, | 185 | ManagedRelay.ManagedRelayClient, |
| 189 | relayClient( | 186 | relayClient( |
| 190 | options?.connectEnvironment ?? | 187 | options?.connectEnvironment ?? |
| 191 | ((input) => | 188 | ((input) => |
| ... | @@ -199,14 +196,14 @@ const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((o | ... | @@ -199,14 +196,14 @@ const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((o |
| 199 | ), | 196 | ), |
| 200 | ); | 197 | ); |
| 201 | 198 | ||
| 202 | return Effect.succeed(connectionResolverLayer.pipe(Layer.provide(dependencies))); | 199 | return Effect.succeed(ConnectionResolver.layer.pipe(Layer.provide(dependencies))); |
| 203 | }); | 200 | }); |
| 204 | 201 | ||
| 205 | describe("ConnectionResolver", () => { | 202 | describe("ConnectionResolver", () => { |
| 206 | it.effect("prepares a primary environment without remote capabilities", () => | 203 | it.effect("prepares a primary environment without remote capabilities", () => |
| 207 | Effect.gen(function* () { | 204 | Effect.gen(function* () { |
| 208 | const brokerLayer = yield* makeDependencies(); | 205 | const brokerLayer = yield* makeDependencies(); |
| 209 | const broker = yield* ConnectionResolver.pipe(Effect.provide(brokerLayer)); | 206 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer)); |
| 210 | const target = new PrimaryConnectionTarget({ | 207 | const target = new PrimaryConnectionTarget({ |
| 211 | environmentId: ENVIRONMENT_ID, | 208 | environmentId: ENVIRONMENT_ID, |
| 212 | label: "Primary", | 209 | label: "Primary", |
| ... | @@ -244,7 +241,7 @@ describe("ConnectionResolver", () => { | ... | @@ -244,7 +241,7 @@ describe("ConnectionResolver", () => { |
| 244 | }), | 241 | }), |
| 245 | ), | 242 | ), |
| 246 | }); | 243 | }); |
| 247 | const broker = yield* ConnectionResolver.pipe(Effect.provide(brokerLayer)); | 244 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer)); |
| 248 | const target = new PrimaryConnectionTarget({ | 245 | const target = new PrimaryConnectionTarget({ |
| 249 | environmentId: ENVIRONMENT_ID, | 246 | environmentId: ENVIRONMENT_ID, |
| 250 | label: "Primary", | 247 | label: "Primary", |
| ... | @@ -292,7 +289,7 @@ describe("ConnectionResolver", () => { | ... | @@ -292,7 +289,7 @@ describe("ConnectionResolver", () => { |
| 292 | }), | 289 | }), |
| 293 | ), | 290 | ), |
| 294 | }); | 291 | }); |
| 295 | const broker = yield* ConnectionResolver.pipe(Effect.provide(brokerLayer)); | 292 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer)); |
| 296 | 293 | ||
| 297 | expect( | 294 | expect( |
| 298 | (yield* broker.prepare(catalogEntry(target, Option.some(profile)))).socketUrl, | 295 | (yield* broker.prepare(catalogEntry(target, Option.some(profile)))).socketUrl, |
| ... | @@ -349,7 +346,7 @@ describe("ConnectionResolver", () => { | ... | @@ -349,7 +346,7 @@ describe("ConnectionResolver", () => { |
| 349 | }), | 346 | }), |
| 350 | ), | 347 | ), |
| 351 | }); | 348 | }); |
| 352 | const broker = yield* ConnectionResolver.pipe(Effect.provide(brokerLayer)); | 349 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer)); |
| 353 | 350 | ||
| 354 | expect((yield* broker.prepare(catalogEntry(target))).socketUrl).toContain("wsTicket=dpop"); | 351 | expect((yield* broker.prepare(catalogEntry(target))).socketUrl).toContain("wsTicket=dpop"); |
| 355 | expect(yield* Ref.get(relayInputs)).toEqual([ | 352 | expect(yield* Ref.get(relayInputs)).toEqual([ |
| ... | @@ -387,7 +384,7 @@ describe("ConnectionResolver", () => { | ... | @@ -387,7 +384,7 @@ describe("ConnectionResolver", () => { |
| 387 | Effect.withSpan("test.remote.authorizeDpop"), | 384 | Effect.withSpan("test.remote.authorizeDpop"), |
| 388 | ), | 385 | ), |
| 389 | }); | 386 | }); |
| 390 | const broker = yield* ConnectionResolver.pipe(Effect.provide(brokerLayer)); | 387 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer)); |
| 391 | 388 | ||
| 392 | yield* broker | 389 | yield* broker |
| 393 | .prepare(catalogEntry(target)) | 390 | .prepare(catalogEntry(target)) |
| ... | @@ -431,7 +428,7 @@ describe("ConnectionResolver", () => { | ... | @@ -431,7 +428,7 @@ describe("ConnectionResolver", () => { |
| 431 | }), | 428 | }), |
| 432 | ), | 429 | ), |
| 433 | }); | 430 | }); |
| 434 | const broker = yield* ConnectionResolver.pipe(Effect.provide(brokerLayer)); | 431 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer)); |
| 435 | 432 | ||
| 436 | expect( | 433 | expect( |
| 437 | (yield* broker.prepare(catalogEntry(target, Option.some(profile)))).socketUrl, | 434 | (yield* broker.prepare(catalogEntry(target, Option.some(profile)))).socketUrl, |
| ... | @@ -449,15 +446,15 @@ describe("ConnectionResolver", () => { | ... | @@ -449,15 +446,15 @@ describe("ConnectionResolver", () => { |
| 449 | const brokerLayer = yield* makeDependencies({ | 446 | const brokerLayer = yield* makeDependencies({ |
| 450 | connectEnvironment: () => | 447 | connectEnvironment: () => |
| 451 | Effect.fail( | 448 | Effect.fail( |
| 452 | new ManagedRelayClientError({ | 449 | new ManagedRelay.ManagedRelayClientError({ |
| 453 | message: "Relay timed out.", | 450 | message: "Relay timed out.", |
| 454 | cause: new ManagedRelayRequestTimeoutError({ | 451 | cause: new ManagedRelay.ManagedRelayRequestTimeoutError({ |
| 455 | message: "Relay timed out.", | 452 | message: "Relay timed out.", |
| 456 | }), | 453 | }), |
| 457 | }), | 454 | }), |
| 458 | ), | 455 | ), |
| 459 | }); | 456 | }); |
| 460 | const broker = yield* ConnectionResolver.pipe(Effect.provide(brokerLayer)); | 457 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer)); |
| 461 | const error = yield* Effect.flip(broker.prepare(catalogEntry(target))); | 458 | const error = yield* Effect.flip(broker.prepare(catalogEntry(target))); |
| 462 | 459 | ||
| 463 | expect(error).toBeInstanceOf(ConnectionTransientError); | 460 | expect(error).toBeInstanceOf(ConnectionTransientError); |
packages/client-runtime/src/connection/resolver.ts+45-51| ... | @@ -6,22 +6,16 @@ import * as Layer from "effect/Layer"; | ... | @@ -6,22 +6,16 @@ import * as Layer from "effect/Layer"; |
| 6 | import * as Option from "effect/Option"; | 6 | import * as Option from "effect/Option"; |
| 7 | import * as Schema from "effect/Schema"; | 7 | import * as Schema from "effect/Schema"; |
| 8 | 8 | ||
| 9 | import { RemoteEnvironmentAuthorization } from "../authorization/service.ts"; | 9 | import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; |
| 10 | import { ManagedRelayClient } from "../relay/managedRelay.ts"; | 10 | import * as ManagedRelay from "../relay/managedRelay.ts"; |
| 11 | import { | 11 | import * as ClientCapabilities from "../platform/capabilities.ts"; |
| 12 | CloudSession, | ||
| 13 | PrimaryEnvironmentAuth, | ||
| 14 | RelayDeviceIdentity, | ||
| 15 | SshEnvironmentGateway, | ||
| 16 | } from "../platform/capabilities.ts"; | ||
| 17 | import { | 12 | import { |
| 18 | BearerConnectionCredential, | 13 | BearerConnectionCredential, |
| 19 | BearerConnectionProfile, | 14 | BearerConnectionProfile, |
| 20 | type ConnectionCatalogEntry, | 15 | type ConnectionCatalogEntry, |
| 21 | ConnectionCredentialStore, | ||
| 22 | ConnectionProfileStore, | ||
| 23 | SshConnectionProfile, | 16 | SshConnectionProfile, |
| 24 | } from "./catalog.ts"; | 17 | } from "./catalog.ts"; |
| 18 | import * as ConnectionCredentialStore from "./credentialStore.ts"; | ||
| 25 | import { | 19 | import { |
| 26 | credentialMissingError, | 20 | credentialMissingError, |
| 27 | environmentMismatchError, | 21 | environmentMismatchError, |
| ... | @@ -37,6 +31,7 @@ import type { | ... | @@ -37,6 +31,7 @@ import type { |
| 37 | SshConnectionTarget, | 31 | SshConnectionTarget, |
| 38 | } from "./model.ts"; | 32 | } from "./model.ts"; |
| 39 | import { ConnectionBlockedError, type ConnectionAttemptError } from "./model.ts"; | 33 | import { ConnectionBlockedError, type ConnectionAttemptError } from "./model.ts"; |
| 34 | import * as ConnectionProfileStore from "./profileStore.ts"; | ||
| 40 | 35 | ||
| 41 | export class ConnectionResolver extends Context.Service< | 36 | export class ConnectionResolver extends Context.Service< |
| 42 | ConnectionResolver, | 37 | ConnectionResolver, |
| ... | @@ -60,8 +55,8 @@ function primarySocketUrl(target: PrimaryConnectionTarget): string { | ... | @@ -60,8 +55,8 @@ function primarySocketUrl(target: PrimaryConnectionTarget): string { |
| 60 | } | 55 | } |
| 61 | 56 | ||
| 62 | const makePrimaryBroker = Effect.fn("clientRuntime.connection.broker.makePrimary")(function* () { | 57 | const makePrimaryBroker = Effect.fn("clientRuntime.connection.broker.makePrimary")(function* () { |
| 63 | const auth = yield* PrimaryEnvironmentAuth; | 58 | const auth = yield* ClientCapabilities.PrimaryEnvironmentAuth; |
| 64 | const remote = yield* RemoteEnvironmentAuthorization; | 59 | const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; |
| 65 | 60 | ||
| 66 | return Effect.fn("clientRuntime.connection.broker.primary")(function* ( | 61 | return Effect.fn("clientRuntime.connection.broker.primary")(function* ( |
| 67 | target: PrimaryConnectionTarget, | 62 | target: PrimaryConnectionTarget, |
| ... | @@ -92,8 +87,8 @@ const makePrimaryBroker = Effect.fn("clientRuntime.connection.broker.makePrimary | ... | @@ -92,8 +87,8 @@ const makePrimaryBroker = Effect.fn("clientRuntime.connection.broker.makePrimary |
| 92 | }); | 87 | }); |
| 93 | 88 | ||
| 94 | const makeBearerBroker = Effect.fn("clientRuntime.connection.broker.makeBearer")(function* () { | 89 | const makeBearerBroker = Effect.fn("clientRuntime.connection.broker.makeBearer")(function* () { |
| 95 | const credentials = yield* ConnectionCredentialStore; | 90 | const credentials = yield* ConnectionCredentialStore.ConnectionCredentialStore; |
| 96 | const remote = yield* RemoteEnvironmentAuthorization; | 91 | const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; |
| 97 | 92 | ||
| 98 | return Effect.fn("clientRuntime.connection.broker.bearer")(function* ( | 93 | return Effect.fn("clientRuntime.connection.broker.bearer")(function* ( |
| 99 | entry: ConnectionCatalogEntry & { readonly target: BearerConnectionTarget }, | 94 | entry: ConnectionCatalogEntry & { readonly target: BearerConnectionTarget }, |
| ... | @@ -106,7 +101,7 @@ const makeBearerBroker = Effect.fn("clientRuntime.connection.broker.makeBearer") | ... | @@ -106,7 +101,7 @@ const makeBearerBroker = Effect.fn("clientRuntime.connection.broker.makeBearer") |
| 106 | if (!isBearerProfile(profile)) { | 101 | if (!isBearerProfile(profile)) { |
| 107 | return yield* new ConnectionBlockedError({ | 102 | return yield* new ConnectionBlockedError({ |
| 108 | reason: "configuration", | 103 | reason: "configuration", |
| 109 | message: `Connection profile ${target.connectionId} is not a bearer connection.`, | 104 | detail: `Connection profile ${target.connectionId} is not a bearer connection.`, |
| 110 | }); | 105 | }); |
| 111 | } | 106 | } |
| 112 | if (profile.environmentId !== target.environmentId) { | 107 | if (profile.environmentId !== target.environmentId) { |
| ... | @@ -144,10 +139,10 @@ const makeBearerBroker = Effect.fn("clientRuntime.connection.broker.makeBearer") | ... | @@ -144,10 +139,10 @@ const makeBearerBroker = Effect.fn("clientRuntime.connection.broker.makeBearer") |
| 144 | }); | 139 | }); |
| 145 | 140 | ||
| 146 | const makeRelayBroker = Effect.fn("clientRuntime.connection.broker.makeRelay")(function* () { | 141 | const makeRelayBroker = Effect.fn("clientRuntime.connection.broker.makeRelay")(function* () { |
| 147 | const relay = yield* ManagedRelayClient; | 142 | const relay = yield* ManagedRelay.ManagedRelayClient; |
| 148 | const session = yield* CloudSession; | 143 | const session = yield* ClientCapabilities.CloudSession; |
| 149 | const identity = yield* RelayDeviceIdentity; | 144 | const identity = yield* ClientCapabilities.RelayDeviceIdentity; |
| 150 | const remote = yield* RemoteEnvironmentAuthorization; | 145 | const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; |
| 151 | 146 | ||
| 152 | return Effect.fnUntraced( | 147 | return Effect.fnUntraced( |
| 153 | function* (target: RelayConnectionTarget) { | 148 | function* (target: RelayConnectionTarget) { |
| ... | @@ -192,9 +187,9 @@ const makeRelayBroker = Effect.fn("clientRuntime.connection.broker.makeRelay")(f | ... | @@ -192,9 +187,9 @@ const makeRelayBroker = Effect.fn("clientRuntime.connection.broker.makeRelay")(f |
| 192 | }); | 187 | }); |
| 193 | 188 | ||
| 194 | const makeSshBroker = Effect.fn("clientRuntime.connection.broker.makeSsh")(function* () { | 189 | const makeSshBroker = Effect.fn("clientRuntime.connection.broker.makeSsh")(function* () { |
| 195 | const profiles = yield* ConnectionProfileStore; | 190 | const profiles = yield* ConnectionProfileStore.ConnectionProfileStore; |
| 196 | const ssh = yield* SshEnvironmentGateway; | 191 | const ssh = yield* ClientCapabilities.SshEnvironmentGateway; |
| 197 | const remote = yield* RemoteEnvironmentAuthorization; | 192 | const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; |
| 198 | 193 | ||
| 199 | return Effect.fn("clientRuntime.connection.broker.ssh")(function* ( | 194 | return Effect.fn("clientRuntime.connection.broker.ssh")(function* ( |
| 200 | entry: ConnectionCatalogEntry & { readonly target: SshConnectionTarget }, | 195 | entry: ConnectionCatalogEntry & { readonly target: SshConnectionTarget }, |
| ... | @@ -207,7 +202,7 @@ const makeSshBroker = Effect.fn("clientRuntime.connection.broker.makeSsh")(funct | ... | @@ -207,7 +202,7 @@ const makeSshBroker = Effect.fn("clientRuntime.connection.broker.makeSsh")(funct |
| 207 | if (!isSshProfile(profile)) { | 202 | if (!isSshProfile(profile)) { |
| 208 | return yield* new ConnectionBlockedError({ | 203 | return yield* new ConnectionBlockedError({ |
| 209 | reason: "configuration", | 204 | reason: "configuration", |
| 210 | message: `Connection profile ${target.connectionId} is not an SSH connection.`, | 205 | detail: `Connection profile ${target.connectionId} is not an SSH connection.`, |
| 211 | }); | 206 | }); |
| 212 | } | 207 | } |
| 213 | if (profile.environmentId !== target.environmentId) { | 208 | if (profile.environmentId !== target.environmentId) { |
| ... | @@ -246,34 +241,33 @@ const makeSshBroker = Effect.fn("clientRuntime.connection.broker.makeSsh")(funct | ... | @@ -246,34 +241,33 @@ const makeSshBroker = Effect.fn("clientRuntime.connection.broker.makeSsh")(funct |
| 246 | }); | 241 | }); |
| 247 | }); | 242 | }); |
| 248 | 243 | ||
| 249 | export const connectionResolverLayer = Layer.effect( | 244 | export const make = Effect.gen(function* () { |
| 250 | ConnectionResolver, | 245 | const primary = yield* makePrimaryBroker(); |
| 251 | Effect.gen(function* () { | 246 | const bearer = yield* makeBearerBroker(); |
| 252 | const primary = yield* makePrimaryBroker(); | 247 | const relay = yield* makeRelayBroker(); |
| 253 | const bearer = yield* makeBearerBroker(); | 248 | const ssh = yield* makeSshBroker(); |
| 254 | const relay = yield* makeRelayBroker(); | ||
| 255 | const ssh = yield* makeSshBroker(); | ||
| 256 | 249 | ||
| 257 | const prepare = Effect.fn("clientRuntime.connection.broker.prepare")(function* ( | 250 | const prepare = Effect.fn("clientRuntime.connection.broker.prepare")(function* ( |
| 258 | entry: ConnectionCatalogEntry, | 251 | entry: ConnectionCatalogEntry, |
| 259 | ) { | 252 | ) { |
| 260 | const target: ConnectionTarget = entry.target; | 253 | const target: ConnectionTarget = entry.target; |
| 261 | yield* Effect.annotateCurrentSpan({ | 254 | yield* Effect.annotateCurrentSpan({ |
| 262 | "connection.environment.id": target.environmentId, | 255 | "connection.environment.id": target.environmentId, |
| 263 | "connection.target.kind": target._tag, | 256 | "connection.target.kind": target._tag, |
| 264 | }); | ||
| 265 | switch (target._tag) { | ||
| 266 | case "PrimaryConnectionTarget": | ||
| 267 | return yield* primary(target); | ||
| 268 | case "BearerConnectionTarget": | ||
| 269 | return yield* bearer({ ...entry, target }); | ||
| 270 | case "RelayConnectionTarget": | ||
| 271 | return yield* relay(target); | ||
| 272 | case "SshConnectionTarget": | ||
| 273 | return yield* ssh({ ...entry, target }); | ||
| 274 | } | ||
| 275 | }); | 257 | }); |
| 258 | switch (target._tag) { | ||
| 259 | case "PrimaryConnectionTarget": | ||
| 260 | return yield* primary(target); | ||
| 261 | case "BearerConnectionTarget": | ||
| 262 | return yield* bearer({ ...entry, target }); | ||
| 263 | case "RelayConnectionTarget": | ||
| 264 | return yield* relay(target); | ||
| 265 | case "SshConnectionTarget": | ||
| 266 | return yield* ssh({ ...entry, target }); | ||
| 267 | } | ||
| 268 | }); | ||
| 269 | |||
| 270 | return ConnectionResolver.of({ prepare }); | ||
| 271 | }); | ||
| 276 | 272 | ||
| 277 | return ConnectionResolver.of({ prepare }); | 273 | export const layer = Layer.effect(ConnectionResolver, make); |
| 278 | }), | ||
| 279 | ); |
packages/client-runtime/src/connection/supervisor.test.ts+43-44| ... | @@ -13,12 +13,8 @@ import * as Tracer from "effect/Tracer"; | ... | @@ -13,12 +13,8 @@ import * as Tracer from "effect/Tracer"; |
| 13 | 13 | ||
| 14 | import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; | 14 | import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; |
| 15 | import type { ConnectionCatalogEntry } from "./catalog.ts"; | 15 | import type { ConnectionCatalogEntry } from "./catalog.ts"; |
| 16 | import { Connectivity } from "./connectivity.ts"; | 16 | import * as Connectivity from "./connectivity.ts"; |
| 17 | import { | 17 | import * as ConnectionDriver from "./driver.ts"; |
| 18 | ConnectionDriver, | ||
| 19 | type ConnectionDriverProgress, | ||
| 20 | type EnvironmentConnectionLease, | ||
| 21 | } from "./driver.ts"; | ||
| 22 | import { | 18 | import { |
| 23 | ConnectionBlockedError, | 19 | ConnectionBlockedError, |
| 24 | ConnectionTransientError, | 20 | ConnectionTransientError, |
| ... | @@ -30,9 +26,9 @@ import { | ... | @@ -30,9 +26,9 @@ import { |
| 30 | type PreparedConnection, | 26 | type PreparedConnection, |
| 31 | type SupervisorConnectionState, | 27 | type SupervisorConnectionState, |
| 32 | } from "./model.ts"; | 28 | } from "./model.ts"; |
| 33 | import type { RpcSession } from "../rpc/session.ts"; | 29 | import * as RpcSession from "../rpc/session.ts"; |
| 34 | import { makeEnvironmentSupervisor } from "./supervisor.ts"; | 30 | import * as EnvironmentSupervisor from "./supervisor.ts"; |
| 35 | import { ConnectionWakeups } from "./wakeups.ts"; | 31 | import * as ConnectionWakeups from "./wakeups.ts"; |
| 36 | 32 | ||
| 37 | const TARGET = new PrimaryConnectionTarget({ | 33 | const TARGET = new PrimaryConnectionTarget({ |
| 38 | environmentId: EnvironmentId.make("environment-1"), | 34 | environmentId: EnvironmentId.make("environment-1"), |
| ... | @@ -70,14 +66,14 @@ const TEST_RPC_CLIENT = {} as WsRpcProtocolClient; | ... | @@ -70,14 +66,14 @@ const TEST_RPC_CLIENT = {} as WsRpcProtocolClient; |
| 70 | function transient(message = "Connection failed.") { | 66 | function transient(message = "Connection failed.") { |
| 71 | return new ConnectionTransientError({ | 67 | return new ConnectionTransientError({ |
| 72 | reason: "transport", | 68 | reason: "transport", |
| 73 | message, | 69 | detail: message, |
| 74 | }); | 70 | }); |
| 75 | } | 71 | } |
| 76 | 72 | ||
| 77 | function blocked(message = "Authentication required.") { | 73 | function blocked(message = "Authentication required.") { |
| 78 | return new ConnectionBlockedError({ | 74 | return new ConnectionBlockedError({ |
| 79 | reason: "authentication", | 75 | reason: "authentication", |
| 80 | message, | 76 | detail: message, |
| 81 | }); | 77 | }); |
| 82 | } | 78 | } |
| 83 | 79 | ||
| ... | @@ -137,7 +133,7 @@ const makeHarness = Effect.fn("TestConnectionHarness.make")(function* (options?: | ... | @@ -137,7 +133,7 @@ const makeHarness = Effect.fn("TestConnectionHarness.make")(function* (options?: |
| 137 | ReadonlyArray<Deferred.Deferred<never, ConnectionTransientError>> | 133 | ReadonlyArray<Deferred.Deferred<never, ConnectionTransientError>> |
| 138 | >([]); | 134 | >([]); |
| 139 | 135 | ||
| 140 | const connectivity = Connectivity.of({ | 136 | const connectivity = Connectivity.Connectivity.of({ |
| 141 | status: SubscriptionRef.get(networkStatus), | 137 | status: SubscriptionRef.get(networkStatus), |
| 142 | changes: SubscriptionRef.changes(networkStatus), | 138 | changes: SubscriptionRef.changes(networkStatus), |
| 143 | }); | 139 | }); |
| ... | @@ -152,7 +148,7 @@ const makeHarness = Effect.fn("TestConnectionHarness.make")(function* (options?: | ... | @@ -152,7 +148,7 @@ const makeHarness = Effect.fn("TestConnectionHarness.make")(function* (options?: |
| 152 | 148 | ||
| 153 | const connect = Effect.fn("TestConnectionDriver.connect")(function* ( | 149 | const connect = Effect.fn("TestConnectionDriver.connect")(function* ( |
| 154 | entry: ConnectionCatalogEntry, | 150 | entry: ConnectionCatalogEntry, |
| 155 | reportProgress: (progress: ConnectionDriverProgress) => Effect.Effect<void>, | 151 | reportProgress: (progress: ConnectionDriver.ConnectionDriverProgress) => Effect.Effect<void>, |
| 156 | ) { | 152 | ) { |
| 157 | const target = entry.target; | 153 | const target = entry.target; |
| 158 | yield* reportProgress({ stage: "preparing" }); | 154 | yield* reportProgress({ stage: "preparing" }); |
| ... | @@ -170,27 +166,30 @@ const makeHarness = Effect.fn("TestConnectionHarness.make")(function* (options?: | ... | @@ -170,27 +166,30 @@ const makeHarness = Effect.fn("TestConnectionHarness.make")(function* (options?: |
| 170 | ready: options?.ready?.(attempt) ?? Effect.void, | 166 | ready: options?.ready?.(attempt) ?? Effect.void, |
| 171 | probe: options?.probe?.(attempt) ?? Effect.void, | 167 | probe: options?.probe?.(attempt) ?? Effect.void, |
| 172 | closed: Deferred.await(closed), | 168 | closed: Deferred.await(closed), |
| 173 | } satisfies RpcSession), | 169 | } satisfies RpcSession.RpcSession), |
| 174 | () => Ref.update(releaseCount, (count) => count + 1), | 170 | () => Ref.update(releaseCount, (count) => count + 1), |
| 175 | ); | 171 | ); |
| 176 | 172 | ||
| 177 | yield* reportProgress({ stage: "synchronizing", prepared }); | 173 | yield* reportProgress({ stage: "synchronizing", prepared }); |
| 178 | yield* session.ready; | 174 | yield* session.ready; |
| 179 | return { prepared, session } satisfies EnvironmentConnectionLease; | 175 | return { prepared, session } satisfies ConnectionDriver.EnvironmentConnectionLease; |
| 180 | }); | 176 | }); |
| 181 | 177 | ||
| 182 | const dependencies = Layer.mergeAll( | 178 | const dependencies = Layer.mergeAll( |
| 183 | Layer.succeed(Connectivity, connectivity), | 179 | Layer.succeed(Connectivity.Connectivity, connectivity), |
| 184 | Layer.succeed( | 180 | Layer.succeed( |
| 185 | ConnectionWakeups, | 181 | ConnectionWakeups.ConnectionWakeups, |
| 186 | ConnectionWakeups.of({ | 182 | ConnectionWakeups.ConnectionWakeups.of({ |
| 187 | changes: SubscriptionRef.changes(wakeups).pipe( | 183 | changes: SubscriptionRef.changes(wakeups).pipe( |
| 188 | Stream.drop(1), | 184 | Stream.drop(1), |
| 189 | Stream.map((event) => event.reason), | 185 | Stream.map((event) => event.reason), |
| 190 | ), | 186 | ), |
| 191 | }), | 187 | }), |
| 192 | ), | 188 | ), |
| 193 | Layer.succeed(ConnectionDriver, ConnectionDriver.of({ connect })), | 189 | Layer.succeed( |
| 190 | ConnectionDriver.ConnectionDriver, | ||
| 191 | ConnectionDriver.ConnectionDriver.of({ connect }), | ||
| 192 | ), | ||
| 194 | ); | 193 | ); |
| 195 | 194 | ||
| 196 | return { | 195 | return { |
| ... | @@ -235,7 +234,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -235,7 +234,7 @@ describe("EnvironmentSupervisor", () => { |
| 235 | prepare: (attempt) => | 234 | prepare: (attempt) => |
| 236 | attempt === 1 ? Effect.fail(transient()) : Effect.succeed(PREPARED_CONNECTION), | 235 | attempt === 1 ? Effect.fail(transient()) : Effect.succeed(PREPARED_CONNECTION), |
| 237 | }); | 236 | }); |
| 238 | const supervisor = yield* makeEnvironmentSupervisor(RELAY_ENTRY, { | 237 | const supervisor = yield* EnvironmentSupervisor.make(RELAY_ENTRY, { |
| 239 | initiallyDesired: true, | 238 | initiallyDesired: true, |
| 240 | }).pipe( | 239 | }).pipe( |
| 241 | Effect.provide(harness.dependencies), | 240 | Effect.provide(harness.dependencies), |
| ... | @@ -263,7 +262,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -263,7 +262,7 @@ describe("EnvironmentSupervisor", () => { |
| 263 | it.effect("does not attempt a connection until it is desired", () => | 262 | it.effect("does not attempt a connection until it is desired", () => |
| 264 | Effect.gen(function* () { | 263 | Effect.gen(function* () { |
| 265 | const harness = yield* makeHarness(); | 264 | const harness = yield* makeHarness(); |
| 266 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY).pipe( | 265 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY).pipe( |
| 267 | Effect.provide(harness.dependencies), | 266 | Effect.provide(harness.dependencies), |
| 268 | ); | 267 | ); |
| 269 | 268 | ||
| ... | @@ -275,7 +274,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -275,7 +274,7 @@ describe("EnvironmentSupervisor", () => { |
| 275 | it.effect("does not let the initial connect signal cancel the first attempt", () => | 274 | it.effect("does not let the initial connect signal cancel the first attempt", () => |
| 276 | Effect.gen(function* () { | 275 | Effect.gen(function* () { |
| 277 | const harness = yield* makeHarness(); | 276 | const harness = yield* makeHarness(); |
| 278 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY).pipe( | 277 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY).pipe( |
| 279 | Effect.provide(harness.dependencies), | 278 | Effect.provide(harness.dependencies), |
| 280 | ); | 279 | ); |
| 281 | 280 | ||
| ... | @@ -290,7 +289,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -290,7 +289,7 @@ describe("EnvironmentSupervisor", () => { |
| 290 | it.effect("waits while offline and connects immediately when the network returns", () => | 289 | it.effect("waits while offline and connects immediately when the network returns", () => |
| 291 | Effect.gen(function* () { | 290 | Effect.gen(function* () { |
| 292 | const harness = yield* makeHarness({ networkStatus: "offline" }); | 291 | const harness = yield* makeHarness({ networkStatus: "offline" }); |
| 293 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 292 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 294 | initiallyDesired: true, | 293 | initiallyDesired: true, |
| 295 | }).pipe(Effect.provide(harness.dependencies)); | 294 | }).pipe(Effect.provide(harness.dependencies)); |
| 296 | 295 | ||
| ... | @@ -317,7 +316,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -317,7 +316,7 @@ describe("EnvironmentSupervisor", () => { |
| 317 | const harness = yield* makeHarness({ | 316 | const harness = yield* makeHarness({ |
| 318 | prepare: () => Effect.fail(transient()), | 317 | prepare: () => Effect.fail(transient()), |
| 319 | }); | 318 | }); |
| 320 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 319 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 321 | initiallyDesired: true, | 320 | initiallyDesired: true, |
| 322 | }).pipe(Effect.provide(harness.dependencies)); | 321 | }).pipe(Effect.provide(harness.dependencies)); |
| 323 | 322 | ||
| ... | @@ -345,7 +344,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -345,7 +344,7 @@ describe("EnvironmentSupervisor", () => { |
| 345 | prepare: (attempt) => | 344 | prepare: (attempt) => |
| 346 | attempt === 1 ? Effect.fail(transient("Relay connection timed out.")) : Effect.never, | 345 | attempt === 1 ? Effect.fail(transient("Relay connection timed out.")) : Effect.never, |
| 347 | }); | 346 | }); |
| 348 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 347 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 349 | initiallyDesired: true, | 348 | initiallyDesired: true, |
| 350 | }).pipe(Effect.provide(harness.dependencies)); | 349 | }).pipe(Effect.provide(harness.dependencies)); |
| 351 | 350 | ||
| ... | @@ -378,7 +377,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -378,7 +377,7 @@ describe("EnvironmentSupervisor", () => { |
| 378 | const harness = yield* makeHarness({ | 377 | const harness = yield* makeHarness({ |
| 379 | ready: () => Effect.never, | 378 | ready: () => Effect.never, |
| 380 | }); | 379 | }); |
| 381 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 380 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 382 | initiallyDesired: true, | 381 | initiallyDesired: true, |
| 383 | }).pipe(Effect.provide(harness.dependencies)); | 382 | }).pipe(Effect.provide(harness.dependencies)); |
| 384 | 383 | ||
| ... | @@ -410,7 +409,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -410,7 +409,7 @@ describe("EnvironmentSupervisor", () => { |
| 410 | const harness = yield* makeHarness({ | 409 | const harness = yield* makeHarness({ |
| 411 | prepare: () => Effect.never, | 410 | prepare: () => Effect.never, |
| 412 | }); | 411 | }); |
| 413 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 412 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 414 | initiallyDesired: true, | 413 | initiallyDesired: true, |
| 415 | }).pipe(Effect.provide(harness.dependencies)); | 414 | }).pipe(Effect.provide(harness.dependencies)); |
| 416 | 415 | ||
| ... | @@ -442,7 +441,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -442,7 +441,7 @@ describe("EnvironmentSupervisor", () => { |
| 442 | ? Effect.die(new Error("Native transport defect.")) | 441 | ? Effect.die(new Error("Native transport defect.")) |
| 443 | : Effect.succeed(PREPARED_CONNECTION), | 442 | : Effect.succeed(PREPARED_CONNECTION), |
| 444 | }); | 443 | }); |
| 445 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 444 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 446 | initiallyDesired: true, | 445 | initiallyDesired: true, |
| 447 | }).pipe(Effect.provide(harness.dependencies)); | 446 | }).pipe(Effect.provide(harness.dependencies)); |
| 448 | 447 | ||
| ... | @@ -470,7 +469,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -470,7 +469,7 @@ describe("EnvironmentSupervisor", () => { |
| 470 | prepare: (attempt) => | 469 | prepare: (attempt) => |
| 471 | attempt === 1 ? Effect.fail(transient()) : Effect.succeed(PREPARED_CONNECTION), | 470 | attempt === 1 ? Effect.fail(transient()) : Effect.succeed(PREPARED_CONNECTION), |
| 472 | }); | 471 | }); |
| 473 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 472 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 474 | initiallyDesired: true, | 473 | initiallyDesired: true, |
| 475 | }).pipe(Effect.provide(harness.dependencies)); | 474 | }).pipe(Effect.provide(harness.dependencies)); |
| 476 | 475 | ||
| ... | @@ -488,7 +487,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -488,7 +487,7 @@ describe("EnvironmentSupervisor", () => { |
| 488 | prepare: (attempt) => | 487 | prepare: (attempt) => |
| 489 | attempt === 1 ? Effect.fail(blocked()) : Effect.succeed(PREPARED_CONNECTION), | 488 | attempt === 1 ? Effect.fail(blocked()) : Effect.succeed(PREPARED_CONNECTION), |
| 490 | }); | 489 | }); |
| 491 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 490 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 492 | initiallyDesired: true, | 491 | initiallyDesired: true, |
| 493 | }).pipe(Effect.provide(harness.dependencies)); | 492 | }).pipe(Effect.provide(harness.dependencies)); |
| 494 | 493 | ||
| ... | @@ -505,7 +504,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -505,7 +504,7 @@ describe("EnvironmentSupervisor", () => { |
| 505 | it.effect("releases a live session while offline and starts a new generation when online", () => | 504 | it.effect("releases a live session while offline and starts a new generation when online", () => |
| 506 | Effect.gen(function* () { | 505 | Effect.gen(function* () { |
| 507 | const harness = yield* makeHarness(); | 506 | const harness = yield* makeHarness(); |
| 508 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 507 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 509 | initiallyDesired: true, | 508 | initiallyDesired: true, |
| 510 | }).pipe(Effect.provide(harness.dependencies)); | 509 | }).pipe(Effect.provide(harness.dependencies)); |
| 511 | 510 | ||
| ... | @@ -534,7 +533,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -534,7 +533,7 @@ describe("EnvironmentSupervisor", () => { |
| 534 | prepare: (attempt) => | 533 | prepare: (attempt) => |
| 535 | attempt === 1 ? Effect.fail(blocked()) : Effect.succeed(PREPARED_CONNECTION), | 534 | attempt === 1 ? Effect.fail(blocked()) : Effect.succeed(PREPARED_CONNECTION), |
| 536 | }); | 535 | }); |
| 537 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 536 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 538 | initiallyDesired: true, | 537 | initiallyDesired: true, |
| 539 | }).pipe(Effect.provide(harness.dependencies)); | 538 | }).pipe(Effect.provide(harness.dependencies)); |
| 540 | 539 | ||
| ... | @@ -553,7 +552,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -553,7 +552,7 @@ describe("EnvironmentSupervisor", () => { |
| 553 | prepare: () => | 552 | prepare: () => |
| 554 | Deferred.succeed(firstAttemptStarted, undefined).pipe(Effect.andThen(Effect.never)), | 553 | Deferred.succeed(firstAttemptStarted, undefined).pipe(Effect.andThen(Effect.never)), |
| 555 | }); | 554 | }); |
| 556 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 555 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 557 | initiallyDesired: true, | 556 | initiallyDesired: true, |
| 558 | }).pipe(Effect.provide(harness.dependencies)); | 557 | }).pipe(Effect.provide(harness.dependencies)); |
| 559 | 558 | ||
| ... | @@ -591,7 +590,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -591,7 +590,7 @@ describe("EnvironmentSupervisor", () => { |
| 591 | it.effect("treats an involuntary session close as transient and reconnects", () => | 590 | it.effect("treats an involuntary session close as transient and reconnects", () => |
| 592 | Effect.gen(function* () { | 591 | Effect.gen(function* () { |
| 593 | const harness = yield* makeHarness(); | 592 | const harness = yield* makeHarness(); |
| 594 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 593 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 595 | initiallyDesired: true, | 594 | initiallyDesired: true, |
| 596 | }).pipe(Effect.provide(harness.dependencies)); | 595 | }).pipe(Effect.provide(harness.dependencies)); |
| 597 | 596 | ||
| ... | @@ -617,7 +616,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -617,7 +616,7 @@ describe("EnvironmentSupervisor", () => { |
| 617 | it.effect("keeps escalating backoff when a newly opened session flaps", () => | 616 | it.effect("keeps escalating backoff when a newly opened session flaps", () => |
| 618 | Effect.gen(function* () { | 617 | Effect.gen(function* () { |
| 619 | const harness = yield* makeHarness(); | 618 | const harness = yield* makeHarness(); |
| 620 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 619 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 621 | initiallyDesired: true, | 620 | initiallyDesired: true, |
| 622 | }).pipe(Effect.provide(harness.dependencies)); | 621 | }).pipe(Effect.provide(harness.dependencies)); |
| 623 | 622 | ||
| ... | @@ -663,7 +662,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -663,7 +662,7 @@ describe("EnvironmentSupervisor", () => { |
| 663 | Effect.andThen(Deferred.succeed(probeCalled, undefined)), | 662 | Effect.andThen(Deferred.succeed(probeCalled, undefined)), |
| 664 | ), | 663 | ), |
| 665 | }); | 664 | }); |
| 666 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 665 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 667 | initiallyDesired: true, | 666 | initiallyDesired: true, |
| 668 | }).pipe(Effect.provide(harness.dependencies)); | 667 | }).pipe(Effect.provide(harness.dependencies)); |
| 669 | 668 | ||
| ... | @@ -684,7 +683,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -684,7 +683,7 @@ describe("EnvironmentSupervisor", () => { |
| 684 | probe: (attempt) => | 683 | probe: (attempt) => |
| 685 | attempt === 1 ? Effect.fail(transient("The live session is stale.")) : Effect.void, | 684 | attempt === 1 ? Effect.fail(transient("The live session is stale.")) : Effect.void, |
| 686 | }); | 685 | }); |
| 687 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 686 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 688 | initiallyDesired: true, | 687 | initiallyDesired: true, |
| 689 | }).pipe(Effect.provide(harness.dependencies)); | 688 | }).pipe(Effect.provide(harness.dependencies)); |
| 690 | 689 | ||
| ... | @@ -707,7 +706,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -707,7 +706,7 @@ describe("EnvironmentSupervisor", () => { |
| 707 | const harness = yield* makeHarness({ | 706 | const harness = yield* makeHarness({ |
| 708 | probe: (attempt) => (attempt === 1 ? Effect.never : Effect.void), | 707 | probe: (attempt) => (attempt === 1 ? Effect.never : Effect.void), |
| 709 | }); | 708 | }); |
| 710 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 709 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 711 | initiallyDesired: true, | 710 | initiallyDesired: true, |
| 712 | }).pipe(Effect.provide(harness.dependencies)); | 711 | }).pipe(Effect.provide(harness.dependencies)); |
| 713 | 712 | ||
| ... | @@ -732,7 +731,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -732,7 +731,7 @@ describe("EnvironmentSupervisor", () => { |
| 732 | const harness = yield* makeHarness({ | 731 | const harness = yield* makeHarness({ |
| 733 | probe: () => Deferred.succeed(probeStarted, undefined).pipe(Effect.andThen(Effect.never)), | 732 | probe: () => Deferred.succeed(probeStarted, undefined).pipe(Effect.andThen(Effect.never)), |
| 734 | }); | 733 | }); |
| 735 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 734 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 736 | initiallyDesired: true, | 735 | initiallyDesired: true, |
| 737 | }).pipe(Effect.provide(harness.dependencies)); | 736 | }).pipe(Effect.provide(harness.dependencies)); |
| 738 | 737 | ||
| ... | @@ -749,7 +748,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -749,7 +748,7 @@ describe("EnvironmentSupervisor", () => { |
| 749 | it.effect("does not churn a healthy session when credentials change", () => | 748 | it.effect("does not churn a healthy session when credentials change", () => |
| 750 | Effect.gen(function* () { | 749 | Effect.gen(function* () { |
| 751 | const harness = yield* makeHarness(); | 750 | const harness = yield* makeHarness(); |
| 752 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 751 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 753 | initiallyDesired: true, | 752 | initiallyDesired: true, |
| 754 | }).pipe(Effect.provide(harness.dependencies)); | 753 | }).pipe(Effect.provide(harness.dependencies)); |
| 755 | 754 | ||
| ... | @@ -766,7 +765,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -766,7 +765,7 @@ describe("EnvironmentSupervisor", () => { |
| 766 | it.effect("releases and reconnects a relay session when credentials change", () => | 765 | it.effect("releases and reconnects a relay session when credentials change", () => |
| 767 | Effect.gen(function* () { | 766 | Effect.gen(function* () { |
| 768 | const harness = yield* makeHarness(); | 767 | const harness = yield* makeHarness(); |
| 769 | const supervisor = yield* makeEnvironmentSupervisor(RELAY_ENTRY, { | 768 | const supervisor = yield* EnvironmentSupervisor.make(RELAY_ENTRY, { |
| 770 | initiallyDesired: true, | 769 | initiallyDesired: true, |
| 771 | }).pipe(Effect.provide(harness.dependencies)); | 770 | }).pipe(Effect.provide(harness.dependencies)); |
| 772 | 771 | ||
| ... | @@ -791,7 +790,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -791,7 +790,7 @@ describe("EnvironmentSupervisor", () => { |
| 791 | ? Deferred.succeed(firstAttemptStarted, undefined).pipe(Effect.andThen(Effect.never)) | 790 | ? Deferred.succeed(firstAttemptStarted, undefined).pipe(Effect.andThen(Effect.never)) |
| 792 | : Effect.succeed(PREPARED_CONNECTION), | 791 | : Effect.succeed(PREPARED_CONNECTION), |
| 793 | }); | 792 | }); |
| 794 | const supervisor = yield* makeEnvironmentSupervisor(RELAY_ENTRY, { | 793 | const supervisor = yield* EnvironmentSupervisor.make(RELAY_ENTRY, { |
| 795 | initiallyDesired: true, | 794 | initiallyDesired: true, |
| 796 | }).pipe(Effect.provide(harness.dependencies)); | 795 | }).pipe(Effect.provide(harness.dependencies)); |
| 797 | 796 | ||
| ... | @@ -807,7 +806,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -807,7 +806,7 @@ describe("EnvironmentSupervisor", () => { |
| 807 | it.effect("explicit disconnect releases the session and returns to available", () => | 806 | it.effect("explicit disconnect releases the session and returns to available", () => |
| 808 | Effect.gen(function* () { | 807 | Effect.gen(function* () { |
| 809 | const harness = yield* makeHarness(); | 808 | const harness = yield* makeHarness(); |
| 810 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 809 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 811 | initiallyDesired: true, | 810 | initiallyDesired: true, |
| 812 | }).pipe(Effect.provide(harness.dependencies)); | 811 | }).pipe(Effect.provide(harness.dependencies)); |
| 813 | 812 | ||
| ... | @@ -824,7 +823,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -824,7 +823,7 @@ describe("EnvironmentSupervisor", () => { |
| 824 | it.effect("does not lose an explicit disconnect among concurrent wakeup signals", () => | 823 | it.effect("does not lose an explicit disconnect among concurrent wakeup signals", () => |
| 825 | Effect.gen(function* () { | 824 | Effect.gen(function* () { |
| 826 | const harness = yield* makeHarness(); | 825 | const harness = yield* makeHarness(); |
| 827 | const supervisor = yield* makeEnvironmentSupervisor(TARGET_ENTRY, { | 826 | const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { |
| 828 | initiallyDesired: true, | 827 | initiallyDesired: true, |
| 829 | }).pipe(Effect.provide(harness.dependencies)); | 828 | }).pipe(Effect.provide(harness.dependencies)); |
| 830 | 829 |
packages/client-runtime/src/connection/supervisor.ts+48-45| ... | @@ -15,12 +15,8 @@ import * as SubscriptionRef from "effect/SubscriptionRef"; | ... | @@ -15,12 +15,8 @@ import * as SubscriptionRef from "effect/SubscriptionRef"; |
| 15 | import * as Tracer from "effect/Tracer"; | 15 | import * as Tracer from "effect/Tracer"; |
| 16 | 16 | ||
| 17 | import type { ConnectionCatalogEntry } from "./catalog.ts"; | 17 | import type { ConnectionCatalogEntry } from "./catalog.ts"; |
| 18 | import { Connectivity } from "./connectivity.ts"; | 18 | import * as Connectivity from "./connectivity.ts"; |
| 19 | import { | 19 | import * as ConnectionDriver from "./driver.ts"; |
| 20 | ConnectionDriver, | ||
| 21 | type ConnectionDriverProgress, | ||
| 22 | type EnvironmentConnectionLease, | ||
| 23 | } from "./driver.ts"; | ||
| 24 | import { | 20 | import { |
| 25 | type ConnectionAttemptError, | 21 | type ConnectionAttemptError, |
| 26 | type ConnectionTarget, | 22 | type ConnectionTarget, |
| ... | @@ -29,8 +25,8 @@ import { | ... | @@ -29,8 +25,8 @@ import { |
| 29 | type PreparedConnection, | 25 | type PreparedConnection, |
| 30 | type SupervisorConnectionState, | 26 | type SupervisorConnectionState, |
| 31 | } from "./model.ts"; | 27 | } from "./model.ts"; |
| 32 | import type { RpcSession } from "../rpc/session.ts"; | 28 | import * as RpcSession from "../rpc/session.ts"; |
| 33 | import { type ConnectionWakeup, ConnectionWakeups } from "./wakeups.ts"; | 29 | import * as ConnectionWakeups from "./wakeups.ts"; |
| 34 | 30 | ||
| 35 | const RETRY_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 16_000] as const; | 31 | const RETRY_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 16_000] as const; |
| 36 | const CONNECTION_ESTABLISHMENT_TIMEOUT = "15 seconds"; | 32 | const CONNECTION_ESTABLISHMENT_TIMEOUT = "15 seconds"; |
| ... | @@ -47,7 +43,7 @@ type SupervisorSignal = | ... | @@ -47,7 +43,7 @@ type SupervisorSignal = |
| 47 | | { readonly _tag: "DisconnectRequested" } | 43 | | { readonly _tag: "DisconnectRequested" } |
| 48 | | { readonly _tag: "RetryRequested" } | 44 | | { readonly _tag: "RetryRequested" } |
| 49 | | { readonly _tag: "NetworkChanged"; readonly network: NetworkStatus } | 45 | | { readonly _tag: "NetworkChanged"; readonly network: NetworkStatus } |
| 50 | | { readonly _tag: "Wakeup"; readonly reason: ConnectionWakeup }; | 46 | | { readonly _tag: "Wakeup"; readonly reason: ConnectionWakeups.ConnectionWakeup }; |
| 51 | 47 | ||
| 52 | interface PendingRetryTrace { | 48 | interface PendingRetryTrace { |
| 53 | readonly previousAttempt: Tracer.Span; | 49 | readonly previousAttempt: Tracer.Span; |
| ... | @@ -80,7 +76,7 @@ type EstablishmentEvent = | ... | @@ -80,7 +76,7 @@ type EstablishmentEvent = |
| 80 | readonly exit: Exit.Exit< | 76 | readonly exit: Exit.Exit< |
| 81 | { | 77 | { |
| 82 | readonly attemptSpan: Option.Option<Tracer.Span>; | 78 | readonly attemptSpan: Option.Option<Tracer.Span>; |
| 83 | readonly lease: EnvironmentConnectionLease; | 79 | readonly lease: ConnectionDriver.EnvironmentConnectionLease; |
| 84 | }, | 80 | }, |
| 85 | TracedAttemptFailure | 81 | TracedAttemptFailure |
| 86 | >; | 82 | >; |
| ... | @@ -102,16 +98,6 @@ export interface EnvironmentSupervisorOptions { | ... | @@ -102,16 +98,6 @@ export interface EnvironmentSupervisorOptions { |
| 102 | readonly initiallyDesired?: boolean; | 98 | readonly initiallyDesired?: boolean; |
| 103 | } | 99 | } |
| 104 | 100 | ||
| 105 | export interface EnvironmentSupervisorService { | ||
| 106 | readonly target: ConnectionTarget; | ||
| 107 | readonly state: SubscriptionRef.SubscriptionRef<SupervisorConnectionState>; | ||
| 108 | readonly session: SubscriptionRef.SubscriptionRef<Option.Option<RpcSession>>; | ||
| 109 | readonly prepared: SubscriptionRef.SubscriptionRef<Option.Option<PreparedConnection>>; | ||
| 110 | readonly connect: Effect.Effect<void>; | ||
| 111 | readonly disconnect: Effect.Effect<void>; | ||
| 112 | readonly retryNow: Effect.Effect<void>; | ||
| 113 | } | ||
| 114 | |||
| 115 | function retryDelayMs(failureCount: number): number { | 101 | function retryDelayMs(failureCount: number): number { |
| 116 | return RETRY_DELAYS_MS[Math.min(failureCount, RETRY_DELAYS_MS.length - 1)] ?? 16_000; | 102 | return RETRY_DELAYS_MS[Math.min(failureCount, RETRY_DELAYS_MS.length - 1)] ?? 16_000; |
| 117 | } | 103 | } |
| ... | @@ -199,7 +185,7 @@ function failureFromExit<A>( | ... | @@ -199,7 +185,7 @@ function failureFromExit<A>( |
| 199 | failure: { | 185 | failure: { |
| 200 | error: new ConnectionTransientError({ | 186 | error: new ConnectionTransientError({ |
| 201 | reason: "transport", | 187 | reason: "transport", |
| 202 | message: `${target.label} connection failed unexpectedly.`, | 188 | detail: `${target.label} connection failed unexpectedly.`, |
| 203 | }), | 189 | }), |
| 204 | attemptSpan: Option.none(), | 190 | attemptSpan: Option.none(), |
| 205 | }, | 191 | }, |
| ... | @@ -208,34 +194,34 @@ function failureFromExit<A>( | ... | @@ -208,34 +194,34 @@ function failureFromExit<A>( |
| 208 | 194 | ||
| 209 | export class EnvironmentSupervisor extends Context.Service< | 195 | export class EnvironmentSupervisor extends Context.Service< |
| 210 | EnvironmentSupervisor, | 196 | EnvironmentSupervisor, |
| 211 | EnvironmentSupervisorService | 197 | { |
| 212 | >()("@t3tools/client-runtime/connection/supervisor/EnvironmentSupervisor") { | 198 | readonly target: ConnectionTarget; |
| 213 | static layer( | 199 | readonly state: SubscriptionRef.SubscriptionRef<SupervisorConnectionState>; |
| 214 | entry: ConnectionCatalogEntry, | 200 | readonly session: SubscriptionRef.SubscriptionRef<Option.Option<RpcSession.RpcSession>>; |
| 215 | options?: EnvironmentSupervisorOptions, | 201 | readonly prepared: SubscriptionRef.SubscriptionRef<Option.Option<PreparedConnection>>; |
| 216 | ): Layer.Layer< | 202 | readonly connect: Effect.Effect<void>; |
| 217 | EnvironmentSupervisor, | 203 | readonly disconnect: Effect.Effect<void>; |
| 218 | never, | 204 | readonly retryNow: Effect.Effect<void>; |
| 219 | Connectivity | ConnectionDriver | ConnectionWakeups | ||
| 220 | > { | ||
| 221 | return Layer.effect(EnvironmentSupervisor, makeEnvironmentSupervisor(entry, options)); | ||
| 222 | } | 205 | } |
| 223 | } | 206 | >()("@t3tools/client-runtime/connection/supervisor/EnvironmentSupervisor") {} |
| 224 | 207 | ||
| 225 | export const makeEnvironmentSupervisor = Effect.fn("EnvironmentSupervisor.make")(function* ( | 208 | export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( |
| 226 | entry: ConnectionCatalogEntry, | 209 | entry: ConnectionCatalogEntry, |
| 227 | options?: EnvironmentSupervisorOptions, | 210 | options?: EnvironmentSupervisorOptions, |
| 228 | ): Effect.fn.Return< | 211 | ): Effect.fn.Return< |
| 229 | EnvironmentSupervisorService, | 212 | EnvironmentSupervisor["Service"], |
| 230 | never, | 213 | never, |
| 231 | Connectivity | ConnectionDriver | Scope.Scope | ConnectionWakeups | 214 | | Connectivity.Connectivity |
| 215 | | ConnectionDriver.ConnectionDriver | ||
| 216 | | Scope.Scope | ||
| 217 | | ConnectionWakeups.ConnectionWakeups | ||
| 232 | > { | 218 | > { |
| 233 | const target = entry.target; | 219 | const target = entry.target; |
| 234 | yield* annotateTarget(target); | 220 | yield* annotateTarget(target); |
| 235 | 221 | ||
| 236 | const connectivity = yield* Connectivity; | 222 | const connectivity = yield* Connectivity.Connectivity; |
| 237 | const driver = yield* ConnectionDriver; | 223 | const driver = yield* ConnectionDriver.ConnectionDriver; |
| 238 | const wakeups = yield* ConnectionWakeups; | 224 | const wakeups = yield* ConnectionWakeups.ConnectionWakeups; |
| 239 | const initialIntent: SupervisorIntent = { | 225 | const initialIntent: SupervisorIntent = { |
| 240 | desired: options?.initiallyDesired ?? false, | 226 | desired: options?.initiallyDesired ?? false, |
| 241 | network: yield* connectivity.status, | 227 | network: yield* connectivity.status, |
| ... | @@ -249,7 +235,7 @@ export const makeEnvironmentSupervisor = Effect.fn("EnvironmentSupervisor.make") | ... | @@ -249,7 +235,7 @@ export const makeEnvironmentSupervisor = Effect.fn("EnvironmentSupervisor.make") |
| 249 | ? offlineState(initialIntent, 0, 0, null) | 235 | ? offlineState(initialIntent, 0, 0, null) |
| 250 | : connectingState(initialIntent, 0, 1, null), | 236 | : connectingState(initialIntent, 0, 1, null), |
| 251 | ); | 237 | ); |
| 252 | const session = yield* SubscriptionRef.make<Option.Option<RpcSession>>(Option.none()); | 238 | const session = yield* SubscriptionRef.make<Option.Option<RpcSession.RpcSession>>(Option.none()); |
| 253 | const prepared = yield* SubscriptionRef.make<Option.Option<PreparedConnection>>(Option.none()); | 239 | const prepared = yield* SubscriptionRef.make<Option.Option<PreparedConnection>>(Option.none()); |
| 254 | 240 | ||
| 255 | const clearLease = Effect.all( | 241 | const clearLease = Effect.all( |
| ... | @@ -280,7 +266,7 @@ export const makeEnvironmentSupervisor = Effect.fn("EnvironmentSupervisor.make") | ... | @@ -280,7 +266,7 @@ export const makeEnvironmentSupervisor = Effect.fn("EnvironmentSupervisor.make") |
| 280 | attempt: number, | 266 | attempt: number, |
| 281 | generation: number, | 267 | generation: number, |
| 282 | lastFailure: ConnectionAttemptError | null, | 268 | lastFailure: ConnectionAttemptError | null, |
| 283 | progress: ConnectionDriverProgress, | 269 | progress: ConnectionDriver.ConnectionDriverProgress, |
| 284 | ) { | 270 | ) { |
| 285 | if ("prepared" in progress) { | 271 | if ("prepared" in progress) { |
| 286 | yield* SubscriptionRef.set(prepared, Option.some(progress.prepared)); | 272 | yield* SubscriptionRef.set(prepared, Option.some(progress.prepared)); |
| ... | @@ -301,7 +287,11 @@ export const makeEnvironmentSupervisor = Effect.fn("EnvironmentSupervisor.make") | ... | @@ -301,7 +287,11 @@ export const makeEnvironmentSupervisor = Effect.fn("EnvironmentSupervisor.make") |
| 301 | }); | 287 | }); |
| 302 | 288 | ||
| 303 | const traceRelayEstablishment = ( | 289 | const traceRelayEstablishment = ( |
| 304 | effect: Effect.Effect<EnvironmentConnectionLease, ConnectionAttemptError, Scope.Scope>, | 290 | effect: Effect.Effect< |
| 291 | ConnectionDriver.EnvironmentConnectionLease, | ||
| 292 | ConnectionAttemptError, | ||
| 293 | Scope.Scope | ||
| 294 | >, | ||
| 305 | attempt: number, | 295 | attempt: number, |
| 306 | generation: number, | 296 | generation: number, |
| 307 | pendingRetry: Option.Option<PendingRetryTrace>, | 297 | pendingRetry: Option.Option<PendingRetryTrace>, |
| ... | @@ -392,7 +382,9 @@ export const makeEnvironmentSupervisor = Effect.fn("EnvironmentSupervisor.make") | ... | @@ -392,7 +382,9 @@ export const makeEnvironmentSupervisor = Effect.fn("EnvironmentSupervisor.make") |
| 392 | } | 382 | } |
| 393 | }); | 383 | }); |
| 394 | 384 | ||
| 395 | const monitorConnectedLease = Effect.fnUntraced(function* (lease: EnvironmentConnectionLease) { | 385 | const monitorConnectedLease = Effect.fnUntraced(function* ( |
| 386 | lease: ConnectionDriver.EnvironmentConnectionLease, | ||
| 387 | ) { | ||
| 396 | for (;;) { | 388 | for (;;) { |
| 397 | const next = yield* Queue.take(signals); | 389 | const next = yield* Queue.take(signals); |
| 398 | switch (next._tag) { | 390 | switch (next._tag) { |
| ... | @@ -417,7 +409,7 @@ export const makeEnvironmentSupervisor = Effect.fn("EnvironmentSupervisor.make") | ... | @@ -417,7 +409,7 @@ export const makeEnvironmentSupervisor = Effect.fn("EnvironmentSupervisor.make") |
| 417 | Effect.fail( | 409 | Effect.fail( |
| 418 | new ConnectionTransientError({ | 410 | new ConnectionTransientError({ |
| 419 | reason: "timeout", | 411 | reason: "timeout", |
| 420 | message: `${target.label} did not respond to a connection health check.`, | 412 | detail: `${target.label} did not respond to a connection health check.`, |
| 421 | }), | 413 | }), |
| 422 | ), | 414 | ), |
| 423 | }), | 415 | }), |
| ... | @@ -499,7 +491,7 @@ export const makeEnvironmentSupervisor = Effect.fn("EnvironmentSupervisor.make") | ... | @@ -499,7 +491,7 @@ export const makeEnvironmentSupervisor = Effect.fn("EnvironmentSupervisor.make") |
| 499 | failure: { | 491 | failure: { |
| 500 | error: new ConnectionTransientError({ | 492 | error: new ConnectionTransientError({ |
| 501 | reason: "timeout", | 493 | reason: "timeout", |
| 502 | message: `${target.label} did not respond during connection setup.`, | 494 | detail: `${target.label} did not respond during connection setup.`, |
| 503 | }), | 495 | }), |
| 504 | attemptSpan: Option.none(), | 496 | attemptSpan: Option.none(), |
| 505 | }, | 497 | }, |
| ... | @@ -722,3 +714,14 @@ export const makeEnvironmentSupervisor = Effect.fn("EnvironmentSupervisor.make") | ... | @@ -722,3 +714,14 @@ export const makeEnvironmentSupervisor = Effect.fn("EnvironmentSupervisor.make") |
| 722 | retryNow, | 714 | retryNow, |
| 723 | }); | 715 | }); |
| 724 | }); | 716 | }); |
| 717 | |||
| 718 | export const layer = ( | ||
| 719 | entry: ConnectionCatalogEntry, | ||
| 720 | options?: EnvironmentSupervisorOptions, | ||
| 721 | ): Layer.Layer< | ||
| 722 | EnvironmentSupervisor, | ||
| 723 | never, | ||
| 724 | | Connectivity.Connectivity | ||
| 725 | | ConnectionDriver.ConnectionDriver | ||
| 726 | | ConnectionWakeups.ConnectionWakeups | ||
| 727 | > => Layer.effect(EnvironmentSupervisor, make(entry, options)); |
packages/client-runtime/src/connection/wakeups.ts+6| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import * as Context from "effect/Context"; | 1 | import * as Context from "effect/Context"; |
| 2 | import * as Layer from "effect/Layer"; | ||
| 2 | import type * as Stream from "effect/Stream"; | 3 | import type * as Stream from "effect/Stream"; |
| 3 | 4 | ||
| 4 | export type ConnectionWakeup = "application-active" | "credentials-changed"; | 5 | export type ConnectionWakeup = "application-active" | "credentials-changed"; |
| ... | @@ -9,3 +10,8 @@ export class ConnectionWakeups extends Context.Service< | ... | @@ -9,3 +10,8 @@ export class ConnectionWakeups extends Context.Service< |
| 9 | readonly changes: Stream.Stream<ConnectionWakeup>; | 10 | readonly changes: Stream.Stream<ConnectionWakeup>; |
| 10 | } | 11 | } |
| 11 | >()("@t3tools/client-runtime/connection/wakeups/ConnectionWakeups") {} | 12 | >()("@t3tools/client-runtime/connection/wakeups/ConnectionWakeups") {} |
| 13 | |||
| 14 | export const make = (service: ConnectionWakeups["Service"]) => ConnectionWakeups.of(service); | ||
| 15 | |||
| 16 | export const layer = (service: ConnectionWakeups["Service"]) => | ||
| 17 | Layer.succeed(ConnectionWakeups, make(service)); |
packages/client-runtime/src/operations/commands.test.ts+8-11| ... | @@ -18,11 +18,8 @@ import { | ... | @@ -18,11 +18,8 @@ import { |
| 18 | PrimaryConnectionTarget, | 18 | PrimaryConnectionTarget, |
| 19 | type PreparedConnection, | 19 | type PreparedConnection, |
| 20 | } from "../connection/model.ts"; | 20 | } from "../connection/model.ts"; |
| 21 | import { | 21 | import * as EnvironmentSupervisor from "../connection/supervisor.ts"; |
| 22 | EnvironmentSupervisor, | 22 | import * as RpcSession from "../rpc/session.ts"; |
| 23 | type EnvironmentSupervisorService, | ||
| 24 | } from "../connection/supervisor.ts"; | ||
| 25 | import type { RpcSession } from "../rpc/session.ts"; | ||
| 26 | import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; | 23 | import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; |
| 27 | import { archiveThread, createProject, stopThreadSession } from "./commands.ts"; | 24 | import { archiveThread, createProject, stopThreadSession } from "./commands.ts"; |
| 28 | 25 | ||
| ... | @@ -51,14 +48,14 @@ const makeSupervisor = Effect.fn("TestEnvironmentCommands.makeSupervisor")(funct | ... | @@ -51,14 +48,14 @@ const makeSupervisor = Effect.fn("TestEnvironmentCommands.makeSupervisor")(funct |
| 51 | return { sequence: dispatched.length }; | 48 | return { sequence: dispatched.length }; |
| 52 | }), | 49 | }), |
| 53 | } as unknown as WsRpcProtocolClient; | 50 | } as unknown as WsRpcProtocolClient; |
| 54 | const session: RpcSession = { | 51 | const session: RpcSession.RpcSession = { |
| 55 | client, | 52 | client, |
| 56 | initialConfig: Effect.never, | 53 | initialConfig: Effect.never, |
| 57 | ready: Effect.void, | 54 | ready: Effect.void, |
| 58 | probe: Effect.void, | 55 | probe: Effect.void, |
| 59 | closed: Effect.never, | 56 | closed: Effect.never, |
| 60 | }; | 57 | }; |
| 61 | return EnvironmentSupervisor.of({ | 58 | return EnvironmentSupervisor.EnvironmentSupervisor.of({ |
| 62 | target: TARGET, | 59 | target: TARGET, |
| 63 | state: yield* SubscriptionRef.make(AVAILABLE_CONNECTION_STATE), | 60 | state: yield* SubscriptionRef.make(AVAILABLE_CONNECTION_STATE), |
| 64 | session: yield* SubscriptionRef.make(Option.some(session)), | 61 | session: yield* SubscriptionRef.make(Option.some(session)), |
| ... | @@ -66,7 +63,7 @@ const makeSupervisor = Effect.fn("TestEnvironmentCommands.makeSupervisor")(funct | ... | @@ -66,7 +63,7 @@ const makeSupervisor = Effect.fn("TestEnvironmentCommands.makeSupervisor")(funct |
| 66 | connect: Effect.void, | 63 | connect: Effect.void, |
| 67 | disconnect: Effect.void, | 64 | disconnect: Effect.void, |
| 68 | retryNow: Effect.void, | 65 | retryNow: Effect.void, |
| 69 | } satisfies EnvironmentSupervisorService); | 66 | } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]); |
| 70 | }); | 67 | }); |
| 71 | 68 | ||
| 72 | describe("environment commands", () => { | 69 | describe("environment commands", () => { |
| ... | @@ -80,7 +77,7 @@ describe("environment commands", () => { | ... | @@ -80,7 +77,7 @@ describe("environment commands", () => { |
| 80 | title: "Project", | 77 | title: "Project", |
| 81 | workspaceRoot: "/workspace/project", | 78 | workspaceRoot: "/workspace/project", |
| 82 | createdAt: "2026-06-06T00:00:00.000Z", | 79 | createdAt: "2026-06-06T00:00:00.000Z", |
| 83 | }).pipe(Effect.provideService(EnvironmentSupervisor, supervisor)); | 80 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 84 | 81 | ||
| 85 | expect(result).toEqual({ sequence: 1 }); | 82 | expect(result).toEqual({ sequence: 1 }); |
| 86 | expect(dispatched).toEqual([ | 83 | expect(dispatched).toEqual([ |
| ... | @@ -105,7 +102,7 @@ describe("environment commands", () => { | ... | @@ -105,7 +102,7 @@ describe("environment commands", () => { |
| 105 | commandId: CommandId.make("queued-command"), | 102 | commandId: CommandId.make("queued-command"), |
| 106 | threadId: ThreadId.make("thread-1"), | 103 | threadId: ThreadId.make("thread-1"), |
| 107 | createdAt: "2026-06-06T00:01:00.000Z", | 104 | createdAt: "2026-06-06T00:01:00.000Z", |
| 108 | }).pipe(Effect.provideService(EnvironmentSupervisor, supervisor)); | 105 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 109 | 106 | ||
| 110 | expect(dispatched).toEqual([ | 107 | expect(dispatched).toEqual([ |
| 111 | { | 108 | { |
| ... | @@ -126,7 +123,7 @@ describe("environment commands", () => { | ... | @@ -126,7 +123,7 @@ describe("environment commands", () => { |
| 126 | yield* archiveThread({ | 123 | yield* archiveThread({ |
| 127 | commandId: CommandId.make("archive-command"), | 124 | commandId: CommandId.make("archive-command"), |
| 128 | threadId: ThreadId.make("thread-1"), | 125 | threadId: ThreadId.make("thread-1"), |
| 129 | }).pipe(Effect.provideService(EnvironmentSupervisor, supervisor)); | 126 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 130 | 127 | ||
| 131 | expect(dispatched).toEqual([ | 128 | expect(dispatched).toEqual([ |
| 132 | { | 129 | { |
packages/client-runtime/src/platform/storageDocument.test.ts+2-2| ... | @@ -1,7 +1,7 @@ | ... | @@ -1,7 +1,7 @@ |
| 1 | import { EnvironmentId } from "@t3tools/contracts"; | 1 | import { EnvironmentId } from "@t3tools/contracts"; |
| 2 | import { describe, expect, it } from "@effect/vitest"; | 2 | import { describe, expect, it } from "@effect/vitest"; |
| 3 | 3 | ||
| 4 | import { RemoteDpopAccessToken } from "../authorization/tokenStore.ts"; | 4 | import * as TokenStore from "../authorization/tokenStore.ts"; |
| 5 | import { | 5 | import { |
| 6 | BearerConnectionCredential, | 6 | BearerConnectionCredential, |
| 7 | BearerConnectionProfile, | 7 | BearerConnectionProfile, |
| ... | @@ -38,7 +38,7 @@ const BEARER_PROFILE = new BearerConnectionProfile({ | ... | @@ -38,7 +38,7 @@ const BEARER_PROFILE = new BearerConnectionProfile({ |
| 38 | const BEARER_CREDENTIAL = new BearerConnectionCredential({ | 38 | const BEARER_CREDENTIAL = new BearerConnectionCredential({ |
| 39 | token: "bearer-token", | 39 | token: "bearer-token", |
| 40 | }); | 40 | }); |
| 41 | const REMOTE_TOKEN = new RemoteDpopAccessToken({ | 41 | const REMOTE_TOKEN = new TokenStore.RemoteDpopAccessToken({ |
| 42 | environmentId: ENVIRONMENT_ID, | 42 | environmentId: ENVIRONMENT_ID, |
| 43 | label: "Remote", | 43 | label: "Remote", |
| 44 | endpoint: { | 44 | endpoint: { |
packages/client-runtime/src/platform/storageDocument.ts+2-2| ... | @@ -6,7 +6,7 @@ import { | ... | @@ -6,7 +6,7 @@ import { |
| 6 | ConnectionProfile, | 6 | ConnectionProfile, |
| 7 | } from "../connection/catalog.ts"; | 7 | } from "../connection/catalog.ts"; |
| 8 | import { type ConnectionTarget, PersistedConnectionTarget } from "../connection/model.ts"; | 8 | import { type ConnectionTarget, PersistedConnectionTarget } from "../connection/model.ts"; |
| 9 | import { RemoteDpopAccessToken } from "../authorization/tokenStore.ts"; | 9 | import * as TokenStore from "../authorization/tokenStore.ts"; |
| 10 | 10 | ||
| 11 | export const StoredConnectionCredential = Schema.Struct({ | 11 | export const StoredConnectionCredential = Schema.Struct({ |
| 12 | connectionId: Schema.String, | 12 | connectionId: Schema.String, |
| ... | @@ -19,7 +19,7 @@ export const ConnectionCatalogDocument = Schema.Struct({ | ... | @@ -19,7 +19,7 @@ export const ConnectionCatalogDocument = Schema.Struct({ |
| 19 | targets: Schema.Array(PersistedConnectionTarget), | 19 | targets: Schema.Array(PersistedConnectionTarget), |
| 20 | profiles: Schema.Array(ConnectionProfile), | 20 | profiles: Schema.Array(ConnectionProfile), |
| 21 | credentials: Schema.Array(StoredConnectionCredential), | 21 | credentials: Schema.Array(StoredConnectionCredential), |
| 22 | remoteDpopTokens: Schema.Array(RemoteDpopAccessToken), | 22 | remoteDpopTokens: Schema.Array(TokenStore.RemoteDpopAccessToken), |
| 23 | }); | 23 | }); |
| 24 | export type ConnectionCatalogDocument = typeof ConnectionCatalogDocument.Type; | 24 | export type ConnectionCatalogDocument = typeof ConnectionCatalogDocument.Type; |
| 25 | 25 |
packages/client-runtime/src/relay/discovery.test.ts+43-39| ... | @@ -13,17 +13,12 @@ import * as Ref from "effect/Ref"; | ... | @@ -13,17 +13,12 @@ import * as Ref from "effect/Ref"; |
| 13 | import * as Stream from "effect/Stream"; | 13 | import * as Stream from "effect/Stream"; |
| 14 | import * as SubscriptionRef from "effect/SubscriptionRef"; | 14 | import * as SubscriptionRef from "effect/SubscriptionRef"; |
| 15 | 15 | ||
| 16 | import { | 16 | import * as ManagedRelay from "./managedRelay.ts"; |
| 17 | ManagedRelayClient, | 17 | import * as ClientCapabilities from "../platform/capabilities.ts"; |
| 18 | ManagedRelayClientError, | 18 | import * as Connectivity from "../connection/connectivity.ts"; |
| 19 | ManagedRelayRequestTimeoutError, | ||
| 20 | type ManagedRelayClientShape, | ||
| 21 | } from "./managedRelay.ts"; | ||
| 22 | import { CloudSession } from "../platform/capabilities.ts"; | ||
| 23 | import { Connectivity } from "../connection/connectivity.ts"; | ||
| 24 | import { ConnectionBlockedError, type NetworkStatus } from "../connection/model.ts"; | 19 | import { ConnectionBlockedError, type NetworkStatus } from "../connection/model.ts"; |
| 25 | import { ConnectionWakeups } from "../connection/wakeups.ts"; | 20 | import * as ConnectionWakeups from "../connection/wakeups.ts"; |
| 26 | import { RelayEnvironmentDiscovery, relayEnvironmentDiscoveryLayer } from "./discovery.ts"; | 21 | import * as RelayEnvironmentDiscovery from "./discovery.ts"; |
| 27 | 22 | ||
| 28 | const environments = [ | 23 | const environments = [ |
| 29 | { | 24 | { |
| ... | @@ -63,7 +58,7 @@ function status( | ... | @@ -63,7 +58,7 @@ function status( |
| 63 | const makeHarness = Effect.fn("RelayDiscoveryTest.makeHarness")(function* () { | 58 | const makeHarness = Effect.fn("RelayDiscoveryTest.makeHarness")(function* () { |
| 64 | const networkStatus = yield* SubscriptionRef.make<NetworkStatus>("online"); | 59 | const networkStatus = yield* SubscriptionRef.make<NetworkStatus>("online"); |
| 65 | const listCalls = yield* Ref.make(0); | 60 | const listCalls = yield* Ref.make(0); |
| 66 | const listFailure = yield* Ref.make<ManagedRelayClientError | null>(null); | 61 | const listFailure = yield* Ref.make<ManagedRelay.ManagedRelayClientError | null>(null); |
| 67 | const secondListCall = yield* Deferred.make<void>(); | 62 | const secondListCall = yield* Deferred.make<void>(); |
| 68 | const clerkToken = yield* Ref.make<string | null>("clerk-token"); | 63 | const clerkToken = yield* Ref.make<string | null>("clerk-token"); |
| 69 | const wakeups = yield* SubscriptionRef.make<{ | 64 | const wakeups = yield* SubscriptionRef.make<{ |
| ... | @@ -74,10 +69,16 @@ const makeHarness = Effect.fn("RelayDiscoveryTest.makeHarness")(function* () { | ... | @@ -74,10 +69,16 @@ const makeHarness = Effect.fn("RelayDiscoveryTest.makeHarness")(function* () { |
| 74 | reason: "application-active", | 69 | reason: "application-active", |
| 75 | }); | 70 | }); |
| 76 | const statusRequests = yield* Ref.make( | 71 | const statusRequests = yield* Ref.make( |
| 77 | new Map<string, Deferred.Deferred<RelayEnvironmentStatusResponse, ManagedRelayClientError>>(), | 72 | new Map< |
| 73 | string, | ||
| 74 | Deferred.Deferred<RelayEnvironmentStatusResponse, ManagedRelay.ManagedRelayClientError> | ||
| 75 | >(), | ||
| 78 | ); | 76 | ); |
| 79 | for (const environment of environments) { | 77 | for (const environment of environments) { |
| 80 | const request = yield* Deferred.make<RelayEnvironmentStatusResponse, ManagedRelayClientError>(); | 78 | const request = yield* Deferred.make< |
| 79 | RelayEnvironmentStatusResponse, | ||
| 80 | ManagedRelay.ManagedRelayClientError | ||
| 81 | >(); | ||
| 81 | yield* Ref.update(statusRequests, (current) => { | 82 | yield* Ref.update(statusRequests, (current) => { |
| 82 | const next = new Map(current); | 83 | const next = new Map(current); |
| 83 | next.set(environment.environmentId, request); | 84 | next.set(environment.environmentId, request); |
| ... | @@ -85,7 +86,7 @@ const makeHarness = Effect.fn("RelayDiscoveryTest.makeHarness")(function* () { | ... | @@ -85,7 +86,7 @@ const makeHarness = Effect.fn("RelayDiscoveryTest.makeHarness")(function* () { |
| 85 | }); | 86 | }); |
| 86 | } | 87 | } |
| 87 | 88 | ||
| 88 | const client = ManagedRelayClient.of({ | 89 | const client = ManagedRelay.ManagedRelayClient.of({ |
| 89 | relayUrl: "https://relay.example.test", | 90 | relayUrl: "https://relay.example.test", |
| 90 | listEnvironments: () => | 91 | listEnvironments: () => |
| 91 | Effect.gen(function* () { | 92 | Effect.gen(function* () { |
| ... | @@ -112,25 +113,25 @@ const makeHarness = Effect.fn("RelayDiscoveryTest.makeHarness")(function* () { | ... | @@ -112,25 +113,25 @@ const makeHarness = Effect.fn("RelayDiscoveryTest.makeHarness")(function* () { |
| 112 | unregisterDevice: () => Effect.die("unused"), | 113 | unregisterDevice: () => Effect.die("unused"), |
| 113 | registerLiveActivity: () => Effect.die("unused"), | 114 | registerLiveActivity: () => Effect.die("unused"), |
| 114 | resetTokenCache: Effect.void, | 115 | resetTokenCache: Effect.void, |
| 115 | } satisfies ManagedRelayClientShape); | 116 | } satisfies ManagedRelay.ManagedRelayClient["Service"]); |
| 116 | const connectivity = Connectivity.of({ | 117 | const connectivity = Connectivity.Connectivity.of({ |
| 117 | status: SubscriptionRef.get(networkStatus), | 118 | status: SubscriptionRef.get(networkStatus), |
| 118 | changes: SubscriptionRef.changes(networkStatus), | 119 | changes: SubscriptionRef.changes(networkStatus), |
| 119 | }); | 120 | }); |
| 120 | const layer = relayEnvironmentDiscoveryLayer.pipe( | 121 | const layer = RelayEnvironmentDiscovery.layer.pipe( |
| 121 | Layer.provide( | 122 | Layer.provide( |
| 122 | Layer.mergeAll( | 123 | Layer.mergeAll( |
| 123 | Layer.succeed(ManagedRelayClient, client), | 124 | Layer.succeed(ManagedRelay.ManagedRelayClient, client), |
| 124 | Layer.succeed( | 125 | Layer.succeed( |
| 125 | CloudSession, | 126 | ClientCapabilities.CloudSession, |
| 126 | CloudSession.of({ | 127 | ClientCapabilities.CloudSession.of({ |
| 127 | clerkToken: Ref.get(clerkToken).pipe( | 128 | clerkToken: Ref.get(clerkToken).pipe( |
| 128 | Effect.flatMap((token) => | 129 | Effect.flatMap((token) => |
| 129 | token === null | 130 | token === null |
| 130 | ? Effect.fail( | 131 | ? Effect.fail( |
| 131 | new ConnectionBlockedError({ | 132 | new ConnectionBlockedError({ |
| 132 | reason: "authentication", | 133 | reason: "authentication", |
| 133 | message: "Signed out.", | 134 | detail: "Signed out.", |
| 134 | }), | 135 | }), |
| 135 | ) | 136 | ) |
| 136 | : Effect.succeed(token), | 137 | : Effect.succeed(token), |
| ... | @@ -138,10 +139,10 @@ const makeHarness = Effect.fn("RelayDiscoveryTest.makeHarness")(function* () { | ... | @@ -138,10 +139,10 @@ const makeHarness = Effect.fn("RelayDiscoveryTest.makeHarness")(function* () { |
| 138 | ), | 139 | ), |
| 139 | }), | 140 | }), |
| 140 | ), | 141 | ), |
| 141 | Layer.succeed(Connectivity, connectivity), | 142 | Layer.succeed(Connectivity.Connectivity, connectivity), |
| 142 | Layer.succeed( | 143 | Layer.succeed( |
| 143 | ConnectionWakeups, | 144 | ConnectionWakeups.ConnectionWakeups, |
| 144 | ConnectionWakeups.of({ | 145 | ConnectionWakeups.ConnectionWakeups.of({ |
| 145 | changes: SubscriptionRef.changes(wakeups).pipe( | 146 | changes: SubscriptionRef.changes(wakeups).pipe( |
| 146 | Stream.drop(1), | 147 | Stream.drop(1), |
| 147 | Stream.map((event) => event.reason), | 148 | Stream.map((event) => event.reason), |
| ... | @@ -173,7 +174,7 @@ describe("RelayEnvironmentDiscovery", () => { | ... | @@ -173,7 +174,7 @@ describe("RelayEnvironmentDiscovery", () => { |
| 173 | Effect.gen(function* () { | 174 | Effect.gen(function* () { |
| 174 | const harness = yield* makeHarness(); | 175 | const harness = yield* makeHarness(); |
| 175 | yield* Effect.gen(function* () { | 176 | yield* Effect.gen(function* () { |
| 176 | const discovery = yield* RelayEnvironmentDiscovery; | 177 | const discovery = yield* RelayEnvironmentDiscovery.RelayEnvironmentDiscovery; |
| 177 | const refreshFiber = yield* Effect.forkChild(discovery.refresh); | 178 | const refreshFiber = yield* Effect.forkChild(discovery.refresh); |
| 178 | 179 | ||
| 179 | const checking = yield* SubscriptionRef.changes(discovery.state).pipe( | 180 | const checking = yield* SubscriptionRef.changes(discovery.state).pipe( |
| ... | @@ -224,7 +225,7 @@ describe("RelayEnvironmentDiscovery", () => { | ... | @@ -224,7 +225,7 @@ describe("RelayEnvironmentDiscovery", () => { |
| 224 | Effect.gen(function* () { | 225 | Effect.gen(function* () { |
| 225 | const harness = yield* makeHarness(); | 226 | const harness = yield* makeHarness(); |
| 226 | yield* Effect.gen(function* () { | 227 | yield* Effect.gen(function* () { |
| 227 | const discovery = yield* RelayEnvironmentDiscovery; | 228 | const discovery = yield* RelayEnvironmentDiscovery.RelayEnvironmentDiscovery; |
| 228 | const requests = yield* Ref.get(harness.statusRequests); | 229 | const requests = yield* Ref.get(harness.statusRequests); |
| 229 | for (const environment of environments) { | 230 | for (const environment of environments) { |
| 230 | yield* Deferred.succeed( | 231 | yield* Deferred.succeed( |
| ... | @@ -253,13 +254,13 @@ describe("RelayEnvironmentDiscovery", () => { | ... | @@ -253,13 +254,13 @@ describe("RelayEnvironmentDiscovery", () => { |
| 253 | it.effect("publishes listing failures without rejecting the refresh command", () => | 254 | it.effect("publishes listing failures without rejecting the refresh command", () => |
| 254 | Effect.gen(function* () { | 255 | Effect.gen(function* () { |
| 255 | const networkStatus = yield* SubscriptionRef.make<NetworkStatus>("online"); | 256 | const networkStatus = yield* SubscriptionRef.make<NetworkStatus>("online"); |
| 256 | const client = ManagedRelayClient.of({ | 257 | const client = ManagedRelay.ManagedRelayClient.of({ |
| 257 | relayUrl: "https://relay.example.test", | 258 | relayUrl: "https://relay.example.test", |
| 258 | listEnvironments: () => | 259 | listEnvironments: () => |
| 259 | Effect.fail( | 260 | Effect.fail( |
| 260 | new ManagedRelayClientError({ | 261 | new ManagedRelay.ManagedRelayClientError({ |
| 261 | message: "Relay environment listing timed out.", | 262 | message: "Relay environment listing timed out.", |
| 262 | cause: new ManagedRelayRequestTimeoutError({ | 263 | cause: new ManagedRelay.ManagedRelayRequestTimeoutError({ |
| 263 | message: "Relay environment listing timed out.", | 264 | message: "Relay environment listing timed out.", |
| 264 | }), | 265 | }), |
| 265 | }), | 266 | }), |
| ... | @@ -274,25 +275,28 @@ describe("RelayEnvironmentDiscovery", () => { | ... | @@ -274,25 +275,28 @@ describe("RelayEnvironmentDiscovery", () => { |
| 274 | unregisterDevice: () => Effect.die("unused"), | 275 | unregisterDevice: () => Effect.die("unused"), |
| 275 | registerLiveActivity: () => Effect.die("unused"), | 276 | registerLiveActivity: () => Effect.die("unused"), |
| 276 | resetTokenCache: Effect.void, | 277 | resetTokenCache: Effect.void, |
| 277 | } satisfies ManagedRelayClientShape); | 278 | } satisfies ManagedRelay.ManagedRelayClient["Service"]); |
| 278 | const layer = relayEnvironmentDiscoveryLayer.pipe( | 279 | const layer = RelayEnvironmentDiscovery.layer.pipe( |
| 279 | Layer.provide( | 280 | Layer.provide( |
| 280 | Layer.mergeAll( | 281 | Layer.mergeAll( |
| 281 | Layer.succeed(ManagedRelayClient, client), | 282 | Layer.succeed(ManagedRelay.ManagedRelayClient, client), |
| 282 | Layer.succeed(CloudSession, { | 283 | Layer.succeed(ClientCapabilities.CloudSession, { |
| 283 | clerkToken: Effect.succeed("clerk-token"), | 284 | clerkToken: Effect.succeed("clerk-token"), |
| 284 | }), | 285 | }), |
| 285 | Layer.succeed(Connectivity, { | 286 | Layer.succeed(Connectivity.Connectivity, { |
| 286 | status: SubscriptionRef.get(networkStatus), | 287 | status: SubscriptionRef.get(networkStatus), |
| 287 | changes: SubscriptionRef.changes(networkStatus), | 288 | changes: SubscriptionRef.changes(networkStatus), |
| 288 | }), | 289 | }), |
| 289 | Layer.succeed(ConnectionWakeups, ConnectionWakeups.of({ changes: Stream.never })), | 290 | Layer.succeed( |
| 291 | ConnectionWakeups.ConnectionWakeups, | ||
| 292 | ConnectionWakeups.ConnectionWakeups.of({ changes: Stream.never }), | ||
| 293 | ), | ||
| 290 | ), | 294 | ), |
| 291 | ), | 295 | ), |
| 292 | ); | 296 | ); |
| 293 | 297 | ||
| 294 | yield* Effect.gen(function* () { | 298 | yield* Effect.gen(function* () { |
| 295 | const discovery = yield* RelayEnvironmentDiscovery; | 299 | const discovery = yield* RelayEnvironmentDiscovery.RelayEnvironmentDiscovery; |
| 296 | yield* discovery.refresh; | 300 | yield* discovery.refresh; |
| 297 | 301 | ||
| 298 | const state = yield* SubscriptionRef.get(discovery.state); | 302 | const state = yield* SubscriptionRef.get(discovery.state); |
| ... | @@ -310,7 +314,7 @@ describe("RelayEnvironmentDiscovery", () => { | ... | @@ -310,7 +314,7 @@ describe("RelayEnvironmentDiscovery", () => { |
| 310 | Effect.gen(function* () { | 314 | Effect.gen(function* () { |
| 311 | const harness = yield* makeHarness(); | 315 | const harness = yield* makeHarness(); |
| 312 | yield* Effect.gen(function* () { | 316 | yield* Effect.gen(function* () { |
| 313 | const discovery = yield* RelayEnvironmentDiscovery; | 317 | const discovery = yield* RelayEnvironmentDiscovery.RelayEnvironmentDiscovery; |
| 314 | const requests = yield* Ref.get(harness.statusRequests); | 318 | const requests = yield* Ref.get(harness.statusRequests); |
| 315 | for (const environment of environments) { | 319 | for (const environment of environments) { |
| 316 | yield* Deferred.succeed( | 320 | yield* Deferred.succeed( |
| ... | @@ -323,7 +327,7 @@ describe("RelayEnvironmentDiscovery", () => { | ... | @@ -323,7 +327,7 @@ describe("RelayEnvironmentDiscovery", () => { |
| 323 | 327 | ||
| 324 | yield* Ref.set( | 328 | yield* Ref.set( |
| 325 | harness.listFailure, | 329 | harness.listFailure, |
| 326 | new ManagedRelayClientError({ | 330 | new ManagedRelay.ManagedRelayClientError({ |
| 327 | message: "Relay environment listing failed.", | 331 | message: "Relay environment listing failed.", |
| 328 | }), | 332 | }), |
| 329 | ); | 333 | ); |
| ... | @@ -340,7 +344,7 @@ describe("RelayEnvironmentDiscovery", () => { | ... | @@ -340,7 +344,7 @@ describe("RelayEnvironmentDiscovery", () => { |
| 340 | Effect.gen(function* () { | 344 | Effect.gen(function* () { |
| 341 | const harness = yield* makeHarness(); | 345 | const harness = yield* makeHarness(); |
| 342 | yield* Effect.gen(function* () { | 346 | yield* Effect.gen(function* () { |
| 343 | const discovery = yield* RelayEnvironmentDiscovery; | 347 | const discovery = yield* RelayEnvironmentDiscovery.RelayEnvironmentDiscovery; |
| 344 | const refreshFiber = yield* Effect.forkChild(discovery.refresh); | 348 | const refreshFiber = yield* Effect.forkChild(discovery.refresh); |
| 345 | yield* SubscriptionRef.changes(discovery.state).pipe( | 349 | yield* SubscriptionRef.changes(discovery.state).pipe( |
| 346 | Stream.filter((state) => state.environments.size === environments.length), | 350 | Stream.filter((state) => state.environments.size === environments.length), |
packages/client-runtime/src/relay/discovery.ts+17-22| ... | @@ -16,12 +16,12 @@ import * as Semaphore from "effect/Semaphore"; | ... | @@ -16,12 +16,12 @@ import * as Semaphore from "effect/Semaphore"; |
| 16 | import * as Stream from "effect/Stream"; | 16 | import * as Stream from "effect/Stream"; |
| 17 | import * as SubscriptionRef from "effect/SubscriptionRef"; | 17 | import * as SubscriptionRef from "effect/SubscriptionRef"; |
| 18 | 18 | ||
| 19 | import { ManagedRelayClient } from "./managedRelay.ts"; | 19 | import * as ManagedRelay from "./managedRelay.ts"; |
| 20 | import { CloudSession } from "../platform/capabilities.ts"; | 20 | import * as ClientCapabilities from "../platform/capabilities.ts"; |
| 21 | import { Connectivity } from "../connection/connectivity.ts"; | 21 | import * as Connectivity from "../connection/connectivity.ts"; |
| 22 | import { mapManagedRelayError } from "../connection/errors.ts"; | 22 | import { mapManagedRelayError } from "../connection/errors.ts"; |
| 23 | import { ConnectionBlockedError, type ConnectionAttemptError } from "../connection/model.ts"; | 23 | import { ConnectionBlockedError, type ConnectionAttemptError } from "../connection/model.ts"; |
| 24 | import { ConnectionWakeups } from "../connection/wakeups.ts"; | 24 | import * as ConnectionWakeups from "../connection/wakeups.ts"; |
| 25 | 25 | ||
| 26 | export type RelayEnvironmentAvailability = "checking" | "online" | "offline" | "error"; | 26 | export type RelayEnvironmentAvailability = "checking" | "online" | "offline" | "error"; |
| 27 | 27 | ||
| ... | @@ -39,14 +39,12 @@ export interface RelayEnvironmentDiscoveryState { | ... | @@ -39,14 +39,12 @@ export interface RelayEnvironmentDiscoveryState { |
| 39 | readonly error: Option.Option<ConnectionAttemptError>; | 39 | readonly error: Option.Option<ConnectionAttemptError>; |
| 40 | } | 40 | } |
| 41 | 41 | ||
| 42 | export interface RelayEnvironmentDiscoveryService { | ||
| 43 | readonly state: SubscriptionRef.SubscriptionRef<RelayEnvironmentDiscoveryState>; | ||
| 44 | readonly refresh: Effect.Effect<void>; | ||
| 45 | } | ||
| 46 | |||
| 47 | export class RelayEnvironmentDiscovery extends Context.Service< | 42 | export class RelayEnvironmentDiscovery extends Context.Service< |
| 48 | RelayEnvironmentDiscovery, | 43 | RelayEnvironmentDiscovery, |
| 49 | RelayEnvironmentDiscoveryService | 44 | { |
| 45 | readonly state: SubscriptionRef.SubscriptionRef<RelayEnvironmentDiscoveryState>; | ||
| 46 | readonly refresh: Effect.Effect<void>; | ||
| 47 | } | ||
| 50 | >()("@t3tools/client-runtime/relay/discovery/RelayEnvironmentDiscovery") {} | 48 | >()("@t3tools/client-runtime/relay/discovery/RelayEnvironmentDiscovery") {} |
| 51 | 49 | ||
| 52 | export const EMPTY_RELAY_ENVIRONMENT_DISCOVERY_STATE: RelayEnvironmentDiscoveryState = { | 50 | export const EMPTY_RELAY_ENVIRONMENT_DISCOVERY_STATE: RelayEnvironmentDiscoveryState = { |
| ... | @@ -64,7 +62,7 @@ function validateStatus( | ... | @@ -64,7 +62,7 @@ function validateStatus( |
| 64 | return Effect.fail( | 62 | return Effect.fail( |
| 65 | new ConnectionBlockedError({ | 63 | new ConnectionBlockedError({ |
| 66 | reason: "configuration", | 64 | reason: "configuration", |
| 67 | message: "Relay returned status for a different environment.", | 65 | detail: "Relay returned status for a different environment.", |
| 68 | }), | 66 | }), |
| 69 | ); | 67 | ); |
| 70 | } | 68 | } |
| ... | @@ -76,7 +74,7 @@ function validateStatus( | ... | @@ -76,7 +74,7 @@ function validateStatus( |
| 76 | return Effect.fail( | 74 | return Effect.fail( |
| 77 | new ConnectionBlockedError({ | 75 | new ConnectionBlockedError({ |
| 78 | reason: "configuration", | 76 | reason: "configuration", |
| 79 | message: "Relay returned status for a different environment endpoint.", | 77 | detail: "Relay returned status for a different environment endpoint.", |
| 80 | }), | 78 | }), |
| 81 | ); | 79 | ); |
| 82 | } | 80 | } |
| ... | @@ -87,7 +85,7 @@ function validateStatus( | ... | @@ -87,7 +85,7 @@ function validateStatus( |
| 87 | return Effect.fail( | 85 | return Effect.fail( |
| 88 | new ConnectionBlockedError({ | 86 | new ConnectionBlockedError({ |
| 89 | reason: "configuration", | 87 | reason: "configuration", |
| 90 | message: "Relay returned a descriptor for a different environment.", | 88 | detail: "Relay returned a descriptor for a different environment.", |
| 91 | }), | 89 | }), |
| 92 | ); | 90 | ); |
| 93 | } | 91 | } |
| ... | @@ -104,11 +102,11 @@ function relayAccountId(clerkToken: string): Option.Option<string> { | ... | @@ -104,11 +102,11 @@ function relayAccountId(clerkToken: string): Option.Option<string> { |
| 104 | } | 102 | } |
| 105 | } | 103 | } |
| 106 | 104 | ||
| 107 | const makeRelayEnvironmentDiscovery = Effect.fn("RelayEnvironmentDiscovery.make")(function* () { | 105 | export const make = Effect.fn("RelayEnvironmentDiscovery.make")(function* () { |
| 108 | const relay = yield* ManagedRelayClient; | 106 | const relay = yield* ManagedRelay.ManagedRelayClient; |
| 109 | const session = yield* CloudSession; | 107 | const session = yield* ClientCapabilities.CloudSession; |
| 110 | const connectivity = yield* Connectivity; | 108 | const connectivity = yield* Connectivity.Connectivity; |
| 111 | const wakeups = yield* ConnectionWakeups; | 109 | const wakeups = yield* ConnectionWakeups.ConnectionWakeups; |
| 112 | const state = yield* SubscriptionRef.make(EMPTY_RELAY_ENVIRONMENT_DISCOVERY_STATE); | 110 | const state = yield* SubscriptionRef.make(EMPTY_RELAY_ENVIRONMENT_DISCOVERY_STATE); |
| 113 | const refreshLock = yield* Semaphore.make(1); | 111 | const refreshLock = yield* Semaphore.make(1); |
| 114 | const hasRefreshed = yield* Ref.make(false); | 112 | const hasRefreshed = yield* Ref.make(false); |
| ... | @@ -327,7 +325,4 @@ const makeRelayEnvironmentDiscovery = Effect.fn("RelayEnvironmentDiscovery.make" | ... | @@ -327,7 +325,4 @@ const makeRelayEnvironmentDiscovery = Effect.fn("RelayEnvironmentDiscovery.make" |
| 327 | return RelayEnvironmentDiscovery.of({ state, refresh }); | 325 | return RelayEnvironmentDiscovery.of({ state, refresh }); |
| 328 | }); | 326 | }); |
| 329 | 327 | ||
| 330 | export const relayEnvironmentDiscoveryLayer = Layer.effect( | 328 | export const layer = Layer.effect(RelayEnvironmentDiscovery, make()); |
| 331 | RelayEnvironmentDiscovery, | ||
| 332 | makeRelayEnvironmentDiscovery(), | ||
| 333 | ); |
packages/client-runtime/src/rpc/client.test.ts+16-17| ... | @@ -22,11 +22,8 @@ import { | ... | @@ -22,11 +22,8 @@ import { |
| 22 | type PreparedConnection, | 22 | type PreparedConnection, |
| 23 | type SupervisorConnectionState, | 23 | type SupervisorConnectionState, |
| 24 | } from "../connection/model.ts"; | 24 | } from "../connection/model.ts"; |
| 25 | import { | 25 | import * as EnvironmentSupervisor from "../connection/supervisor.ts"; |
| 26 | EnvironmentSupervisor, | 26 | import * as RpcSession from "../rpc/session.ts"; |
| 27 | type EnvironmentSupervisorService, | ||
| 28 | } from "../connection/supervisor.ts"; | ||
| 29 | import type { RpcSession } from "../rpc/session.ts"; | ||
| 30 | import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; | 27 | import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; |
| 31 | import { EnvironmentRpcRequestObserver, request, runStream, subscribe } from "./client.ts"; | 28 | import { EnvironmentRpcRequestObserver, request, runStream, subscribe } from "./client.ts"; |
| 32 | 29 | ||
| ... | @@ -46,7 +43,7 @@ const INSTALL_DOWNLOADING: RelayClientInstallProgressEvent = { | ... | @@ -46,7 +43,7 @@ const INSTALL_DOWNLOADING: RelayClientInstallProgressEvent = { |
| 46 | stage: "downloading", | 43 | stage: "downloading", |
| 47 | }; | 44 | }; |
| 48 | 45 | ||
| 49 | function session(client: WsRpcProtocolClient): RpcSession { | 46 | function session(client: WsRpcProtocolClient): RpcSession.RpcSession { |
| 50 | return { | 47 | return { |
| 51 | client, | 48 | client, |
| 52 | initialConfig: Effect.never, | 49 | initialConfig: Effect.never, |
| ... | @@ -58,10 +55,12 @@ function session(client: WsRpcProtocolClient): RpcSession { | ... | @@ -58,10 +55,12 @@ function session(client: WsRpcProtocolClient): RpcSession { |
| 58 | 55 | ||
| 59 | const makeHarness = Effect.fn("TestEnvironmentRpc.makeHarness")(function* () { | 56 | const makeHarness = Effect.fn("TestEnvironmentRpc.makeHarness")(function* () { |
| 60 | const state = yield* SubscriptionRef.make<SupervisorConnectionState>(AVAILABLE_CONNECTION_STATE); | 57 | const state = yield* SubscriptionRef.make<SupervisorConnectionState>(AVAILABLE_CONNECTION_STATE); |
| 61 | const activeSession = yield* SubscriptionRef.make<Option.Option<RpcSession>>(Option.none()); | 58 | const activeSession = yield* SubscriptionRef.make<Option.Option<RpcSession.RpcSession>>( |
| 59 | Option.none(), | ||
| 60 | ); | ||
| 62 | const prepared = yield* SubscriptionRef.make<Option.Option<PreparedConnection>>(Option.none()); | 61 | const prepared = yield* SubscriptionRef.make<Option.Option<PreparedConnection>>(Option.none()); |
| 63 | const retryCount = yield* Ref.make(0); | 62 | const retryCount = yield* Ref.make(0); |
| 64 | const supervisor = EnvironmentSupervisor.of({ | 63 | const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({ |
| 65 | target: TARGET, | 64 | target: TARGET, |
| 66 | state, | 65 | state, |
| 67 | session: activeSession, | 66 | session: activeSession, |
| ... | @@ -69,7 +68,7 @@ const makeHarness = Effect.fn("TestEnvironmentRpc.makeHarness")(function* () { | ... | @@ -69,7 +68,7 @@ const makeHarness = Effect.fn("TestEnvironmentRpc.makeHarness")(function* () { |
| 69 | connect: Effect.void, | 68 | connect: Effect.void, |
| 70 | disconnect: Effect.void, | 69 | disconnect: Effect.void, |
| 71 | retryNow: Ref.update(retryCount, (count) => count + 1), | 70 | retryNow: Ref.update(retryCount, (count) => count + 1), |
| 72 | } satisfies EnvironmentSupervisorService); | 71 | } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]); |
| 73 | return { | 72 | return { |
| 74 | activeSession, | 73 | activeSession, |
| 75 | retryCount, | 74 | retryCount, |
| ... | @@ -89,7 +88,7 @@ describe("environment RPC", () => { | ... | @@ -89,7 +88,7 @@ describe("environment RPC", () => { |
| 89 | yield* SubscriptionRef.set(activeSession, Option.some(session(client))); | 88 | yield* SubscriptionRef.set(activeSession, Option.some(session(client))); |
| 90 | 89 | ||
| 91 | const result = yield* request(WS_METHODS.cloudGetRelayClientStatus, {}).pipe( | 90 | const result = yield* request(WS_METHODS.cloudGetRelayClientStatus, {}).pipe( |
| 92 | Effect.provideService(EnvironmentSupervisor, supervisor), | 91 | Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), |
| 93 | Effect.provideService( | 92 | Effect.provideService( |
| 94 | EnvironmentRpcRequestObserver, | 93 | EnvironmentRpcRequestObserver, |
| 95 | EnvironmentRpcRequestObserver.of({ | 94 | EnvironmentRpcRequestObserver.of({ |
| ... | @@ -128,7 +127,7 @@ describe("environment RPC", () => { | ... | @@ -128,7 +127,7 @@ describe("environment RPC", () => { |
| 128 | const resultFiber = yield* runStream(WS_METHODS.cloudInstallRelayClient, {}).pipe( | 127 | const resultFiber = yield* runStream(WS_METHODS.cloudInstallRelayClient, {}).pipe( |
| 129 | Stream.take(2), | 128 | Stream.take(2), |
| 130 | Stream.runCollect, | 129 | Stream.runCollect, |
| 131 | Effect.provideService(EnvironmentSupervisor, supervisor), | 130 | Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), |
| 132 | Effect.forkChild, | 131 | Effect.forkChild, |
| 133 | ); | 132 | ); |
| 134 | yield* Effect.yieldNow; | 133 | yield* Effect.yieldNow; |
| ... | @@ -172,7 +171,7 @@ describe("environment RPC", () => { | ... | @@ -172,7 +171,7 @@ describe("environment RPC", () => { |
| 172 | 171 | ||
| 173 | const subscriptionFiber = yield* subscribe(WS_METHODS.subscribeTerminalEvents, {}).pipe( | 172 | const subscriptionFiber = yield* subscribe(WS_METHODS.subscribeTerminalEvents, {}).pipe( |
| 174 | Stream.runDrain, | 173 | Stream.runDrain, |
| 175 | Effect.provideService(EnvironmentSupervisor, supervisor), | 174 | Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), |
| 176 | Effect.forkChild, | 175 | Effect.forkChild, |
| 177 | ); | 176 | ); |
| 178 | yield* SubscriptionRef.set(activeSession, Option.some(session(firstClient))); | 177 | yield* SubscriptionRef.set(activeSession, Option.some(session(firstClient))); |
| ... | @@ -212,7 +211,7 @@ describe("environment RPC", () => { | ... | @@ -212,7 +211,7 @@ describe("environment RPC", () => { |
| 212 | 211 | ||
| 213 | const subscriptionFiber = yield* subscribe(WS_METHODS.subscribeTerminalEvents, {}).pipe( | 212 | const subscriptionFiber = yield* subscribe(WS_METHODS.subscribeTerminalEvents, {}).pipe( |
| 214 | Stream.runDrain, | 213 | Stream.runDrain, |
| 215 | Effect.provideService(EnvironmentSupervisor, supervisor), | 214 | Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), |
| 216 | Effect.forkChild, | 215 | Effect.forkChild, |
| 217 | ); | 216 | ); |
| 218 | yield* SubscriptionRef.set(activeSession, Option.some(session(firstClient))); | 217 | yield* SubscriptionRef.set(activeSession, Option.some(session(firstClient))); |
| ... | @@ -243,7 +242,7 @@ describe("environment RPC", () => { | ... | @@ -243,7 +242,7 @@ describe("environment RPC", () => { |
| 243 | yield* SubscriptionRef.set(activeSession, Option.some(session(client))); | 242 | yield* SubscriptionRef.set(activeSession, Option.some(session(client))); |
| 244 | const error = yield* subscribe(WS_METHODS.subscribeTerminalEvents, {}).pipe( | 243 | const error = yield* subscribe(WS_METHODS.subscribeTerminalEvents, {}).pipe( |
| 245 | Stream.runDrain, | 244 | Stream.runDrain, |
| 246 | Effect.provideService(EnvironmentSupervisor, supervisor), | 245 | Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), |
| 247 | Effect.flip, | 246 | Effect.flip, |
| 248 | ); | 247 | ); |
| 249 | 248 | ||
| ... | @@ -283,7 +282,7 @@ describe("environment RPC", () => { | ... | @@ -283,7 +282,7 @@ describe("environment RPC", () => { |
| 283 | }, | 282 | }, |
| 284 | ).pipe( | 283 | ).pipe( |
| 285 | Stream.runDrain, | 284 | Stream.runDrain, |
| 286 | Effect.provideService(EnvironmentSupervisor, supervisor), | 285 | Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), |
| 287 | Effect.forkChild, | 286 | Effect.forkChild, |
| 288 | ); | 287 | ); |
| 289 | for (let attempt = 0; attempt < 100 && observedFailures.length < 1; attempt += 1) { | 288 | for (let attempt = 0; attempt < 100 && observedFailures.length < 1; attempt += 1) { |
| ... | @@ -329,7 +328,7 @@ describe("environment RPC", () => { | ... | @@ -329,7 +328,7 @@ describe("environment RPC", () => { |
| 329 | }, | 328 | }, |
| 330 | ).pipe( | 329 | ).pipe( |
| 331 | Stream.runDrain, | 330 | Stream.runDrain, |
| 332 | Effect.provideService(EnvironmentSupervisor, supervisor), | 331 | Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), |
| 333 | Effect.forkChild, | 332 | Effect.forkChild, |
| 334 | ); | 333 | ); |
| 335 | for (let attempt = 0; attempt < 100; attempt += 1) { | 334 | for (let attempt = 0; attempt < 100; attempt += 1) { |
| ... | @@ -377,7 +376,7 @@ describe("environment RPC", () => { | ... | @@ -377,7 +376,7 @@ describe("environment RPC", () => { |
| 377 | }, | 376 | }, |
| 378 | ).pipe( | 377 | ).pipe( |
| 379 | Stream.runDrain, | 378 | Stream.runDrain, |
| 380 | Effect.provideService(EnvironmentSupervisor, supervisor), | 379 | Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), |
| 381 | Effect.exit, | 380 | Effect.exit, |
| 382 | ); | 381 | ); |
| 383 | 382 |
packages/client-runtime/src/rpc/index.ts+1-1| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | export * from "./client.ts"; | 1 | export * from "./client.ts"; |
| 2 | export * from "./http.ts"; | 2 | export * from "./http.ts"; |
| 3 | export * from "./protocol.ts"; | 3 | export * from "./protocol.ts"; |
| 4 | export * from "./session.ts"; | 4 | export { type RpcSession, RpcSessionFactory } from "./session.ts"; |
packages/client-runtime/src/rpc/session.test.ts+3-3| ... | @@ -18,7 +18,7 @@ import { | ... | @@ -18,7 +18,7 @@ import { |
| 18 | PrimaryConnectionTarget, | 18 | PrimaryConnectionTarget, |
| 19 | type PreparedConnection, | 19 | type PreparedConnection, |
| 20 | } from "../connection/model.ts"; | 20 | } from "../connection/model.ts"; |
| 21 | import { RpcSessionFactory, rpcSessionFactoryLayer } from "./session.ts"; | 21 | import * as RpcSession from "./session.ts"; |
| 22 | 22 | ||
| 23 | type SocketEventType = "open" | "message" | "close" | "error"; | 23 | type SocketEventType = "open" | "message" | "close" | "error"; |
| 24 | type SocketEvent = { | 24 | type SocketEvent = { |
| ... | @@ -149,8 +149,8 @@ const makeFactory = Effect.fn("TestRpcSessionFactory.make")(function* () { | ... | @@ -149,8 +149,8 @@ const makeFactory = Effect.fn("TestRpcSessionFactory.make")(function* () { |
| 149 | sockets.push(socket); | 149 | sockets.push(socket); |
| 150 | return socket as unknown as globalThis.WebSocket; | 150 | return socket as unknown as globalThis.WebSocket; |
| 151 | }); | 151 | }); |
| 152 | const layer = rpcSessionFactoryLayer.pipe(Layer.provide(constructorLayer)); | 152 | const layer = RpcSession.layer.pipe(Layer.provide(constructorLayer)); |
| 153 | const factory = yield* RpcSessionFactory.pipe(Effect.provide(layer)); | 153 | const factory = yield* RpcSession.RpcSessionFactory.pipe(Effect.provide(layer)); |
| 154 | return { factory, sockets }; | 154 | return { factory, sockets }; |
| 155 | }); | 155 | }); |
| 156 | 156 |
packages/client-runtime/src/rpc/session.ts+74-74| ... | @@ -5,7 +5,8 @@ import * as Effect from "effect/Effect"; | ... | @@ -5,7 +5,8 @@ import * as Effect from "effect/Effect"; |
| 5 | import * as Layer from "effect/Layer"; | 5 | import * as Layer from "effect/Layer"; |
| 6 | import * as Schedule from "effect/Schedule"; | 6 | import * as Schedule from "effect/Schedule"; |
| 7 | import type * as Scope from "effect/Scope"; | 7 | import type * as Scope from "effect/Scope"; |
| 8 | import { RpcClient, RpcSerialization } from "effect/unstable/rpc"; | 8 | import * as RpcClient from "effect/unstable/rpc/RpcClient"; |
| 9 | import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; | ||
| 9 | import * as Socket from "effect/unstable/socket/Socket"; | 10 | import * as Socket from "effect/unstable/socket/Socket"; |
| 10 | 11 | ||
| 11 | import { makeWsRpcProtocolClient, type WsRpcProtocolClient } from "./protocol.ts"; | 12 | import { makeWsRpcProtocolClient, type WsRpcProtocolClient } from "./protocol.ts"; |
| ... | @@ -47,98 +48,97 @@ function mapInitialConfigError(error: InitialConfigError): ConnectionAttemptErro | ... | @@ -47,98 +48,97 @@ function mapInitialConfigError(error: InitialConfigError): ConnectionAttemptErro |
| 47 | case "EnvironmentAuthorizationError": | 48 | case "EnvironmentAuthorizationError": |
| 48 | return new ConnectionBlockedError({ | 49 | return new ConnectionBlockedError({ |
| 49 | reason: "permission", | 50 | reason: "permission", |
| 50 | message: error.message, | 51 | detail: error.message, |
| 51 | }); | 52 | }); |
| 52 | case "KeybindingsConfigParseError": | 53 | case "KeybindingsConfigParseError": |
| 53 | case "ServerSettingsError": | 54 | case "ServerSettingsError": |
| 54 | return new ConnectionTransientErrorClass({ | 55 | return new ConnectionTransientErrorClass({ |
| 55 | reason: "remote-unavailable", | 56 | reason: "remote-unavailable", |
| 56 | message: error.message, | 57 | detail: error.message, |
| 57 | }); | 58 | }); |
| 58 | case "RpcClientError": | 59 | case "RpcClientError": |
| 59 | return new ConnectionTransientErrorClass({ | 60 | return new ConnectionTransientErrorClass({ |
| 60 | reason: "transport", | 61 | reason: "transport", |
| 61 | message: error.message, | 62 | detail: error.message, |
| 62 | }); | 63 | }); |
| 63 | } | 64 | } |
| 64 | } | 65 | } |
| 65 | 66 | ||
| 66 | export const rpcSessionFactoryLayer = Layer.effect( | 67 | export const make = Effect.gen(function* () { |
| 67 | RpcSessionFactory, | 68 | const webSocketConstructor = yield* Socket.WebSocketConstructor; |
| 68 | Effect.gen(function* () { | ||
| 69 | const webSocketConstructor = yield* Socket.WebSocketConstructor; | ||
| 70 | 69 | ||
| 71 | const connect = Effect.fnUntraced(function* (connection: PreparedConnection) { | 70 | const connect = Effect.fnUntraced(function* (connection: PreparedConnection) { |
| 72 | yield* Effect.annotateCurrentSpan({ | 71 | yield* Effect.annotateCurrentSpan({ |
| 73 | "connection.environment.id": connection.environmentId, | 72 | "connection.environment.id": connection.environmentId, |
| 74 | }); | 73 | }); |
| 75 | 74 | ||
| 76 | const connected = yield* Deferred.make<void>(); | 75 | const connected = yield* Deferred.make<void>(); |
| 77 | const disconnected = yield* Deferred.make<never, ConnectionTransientError>(); | 76 | const disconnected = yield* Deferred.make<never, ConnectionTransientError>(); |
| 78 | const hooks = RpcClient.ConnectionHooks.of({ | 77 | const hooks = RpcClient.ConnectionHooks.of({ |
| 79 | onConnect: Deferred.succeed(connected, undefined).pipe(Effect.asVoid), | 78 | onConnect: Deferred.succeed(connected, undefined).pipe(Effect.asVoid), |
| 80 | onDisconnect: Deferred.isDone(connected).pipe( | 79 | onDisconnect: Deferred.isDone(connected).pipe( |
| 81 | Effect.flatMap((wasConnected) => | 80 | Effect.flatMap((wasConnected) => |
| 82 | Deferred.fail( | 81 | Deferred.fail( |
| 83 | disconnected, | 82 | disconnected, |
| 84 | new ConnectionTransientErrorClass({ | 83 | new ConnectionTransientErrorClass({ |
| 85 | reason: "transport", | 84 | reason: "transport", |
| 86 | message: wasConnected | 85 | detail: wasConnected |
| 87 | ? `${connection.label} disconnected.` | 86 | ? `${connection.label} disconnected.` |
| 88 | : `${connection.label} could not establish a WebSocket connection.`, | 87 | : `${connection.label} could not establish a WebSocket connection.`, |
| 89 | }), | 88 | }), |
| 90 | ), | ||
| 91 | ), | ||
| 92 | Effect.asVoid, | ||
| 93 | ), | ||
| 94 | }); | ||
| 95 | const socketLayer = Socket.layerWebSocket(connection.socketUrl, { | ||
| 96 | openTimeout: SOCKET_OPEN_TIMEOUT, | ||
| 97 | }).pipe(Layer.provide(Layer.succeed(Socket.WebSocketConstructor, webSocketConstructor))); | ||
| 98 | const protocolLayer = Layer.effect( | ||
| 99 | RpcClient.Protocol, | ||
| 100 | RpcClient.makeProtocolSocket({ | ||
| 101 | retryTransientErrors: false, | ||
| 102 | retryPolicy: Schedule.recurs(0), | ||
| 103 | }), | ||
| 104 | ).pipe( | ||
| 105 | Layer.provide( | ||
| 106 | Layer.mergeAll( | ||
| 107 | socketLayer, | ||
| 108 | RpcSerialization.layerJson, | ||
| 109 | Layer.succeed(RpcClient.ConnectionHooks, hooks), | ||
| 110 | ), | 89 | ), |
| 111 | ), | 90 | ), |
| 112 | ); | 91 | Effect.asVoid, |
| 113 | const protocolContext = yield* Layer.build(protocolLayer).pipe( | 92 | ), |
| 114 | Effect.withSpan("environment.websocket.connect"), | 93 | }); |
| 115 | ); | 94 | const socketLayer = Socket.layerWebSocket(connection.socketUrl, { |
| 116 | const client = yield* makeWsRpcProtocolClient.pipe(Effect.provide(protocolContext)); | 95 | openTimeout: SOCKET_OPEN_TIMEOUT, |
| 117 | const initialConfig = yield* Effect.cached( | 96 | }).pipe(Layer.provide(Layer.succeed(Socket.WebSocketConstructor, webSocketConstructor))); |
| 118 | client[WS_METHODS.serverGetConfig]({}).pipe( | 97 | const protocolLayer = Layer.effect( |
| 119 | Effect.mapError(mapInitialConfigError), | 98 | RpcClient.Protocol, |
| 120 | Effect.withSpan("environment.initialSync"), | 99 | RpcClient.makeProtocolSocket({ |
| 100 | retryTransientErrors: false, | ||
| 101 | retryPolicy: Schedule.recurs(0), | ||
| 102 | }), | ||
| 103 | ).pipe( | ||
| 104 | Layer.provide( | ||
| 105 | Layer.mergeAll( | ||
| 106 | socketLayer, | ||
| 107 | RpcSerialization.layerJson, | ||
| 108 | Layer.succeed(RpcClient.ConnectionHooks, hooks), | ||
| 121 | ), | 109 | ), |
| 122 | ); | 110 | ), |
| 123 | const probe = client[WS_METHODS.serverGetConfig]({}).pipe( | 111 | ); |
| 112 | const protocolContext = yield* Layer.build(protocolLayer).pipe( | ||
| 113 | Effect.withSpan("environment.websocket.connect"), | ||
| 114 | ); | ||
| 115 | const client = yield* makeWsRpcProtocolClient.pipe(Effect.provide(protocolContext)); | ||
| 116 | const initialConfig = yield* Effect.cached( | ||
| 117 | client[WS_METHODS.serverGetConfig]({}).pipe( | ||
| 124 | Effect.mapError(mapInitialConfigError), | 118 | Effect.mapError(mapInitialConfigError), |
| 119 | Effect.withSpan("environment.initialSync"), | ||
| 120 | ), | ||
| 121 | ); | ||
| 122 | const probe = client[WS_METHODS.serverGetConfig]({}).pipe( | ||
| 123 | Effect.mapError(mapInitialConfigError), | ||
| 124 | Effect.asVoid, | ||
| 125 | Effect.withSpan("clientRuntime.connection.rpcSession.probe"), | ||
| 126 | ); | ||
| 127 | |||
| 128 | return { | ||
| 129 | client, | ||
| 130 | initialConfig, | ||
| 131 | ready: Deferred.await(connected).pipe( | ||
| 132 | Effect.andThen(initialConfig), | ||
| 125 | Effect.asVoid, | 133 | Effect.asVoid, |
| 126 | Effect.withSpan("clientRuntime.connection.rpcSession.probe"), | 134 | Effect.raceFirst(Deferred.await(disconnected)), |
| 127 | ); | 135 | ), |
| 136 | probe, | ||
| 137 | closed: Deferred.await(disconnected), | ||
| 138 | } satisfies RpcSession; | ||
| 139 | }); | ||
| 128 | 140 | ||
| 129 | return { | 141 | return RpcSessionFactory.of({ connect }); |
| 130 | client, | 142 | }); |
| 131 | initialConfig, | ||
| 132 | ready: Deferred.await(connected).pipe( | ||
| 133 | Effect.andThen(initialConfig), | ||
| 134 | Effect.asVoid, | ||
| 135 | Effect.raceFirst(Deferred.await(disconnected)), | ||
| 136 | ), | ||
| 137 | probe, | ||
| 138 | closed: Deferred.await(disconnected), | ||
| 139 | } satisfies RpcSession; | ||
| 140 | }); | ||
| 141 | 143 | ||
| 142 | return RpcSessionFactory.of({ connect }); | 144 | export const layer = Layer.effect(RpcSessionFactory, make); |
| 143 | }), | ||
| 144 | ); |
packages/client-runtime/src/state/connections.ts+21-11| ... | @@ -5,10 +5,10 @@ import * as Stream from "effect/Stream"; | ... | @@ -5,10 +5,10 @@ import * as Stream from "effect/Stream"; |
| 5 | import * as SubscriptionRef from "effect/SubscriptionRef"; | 5 | import * as SubscriptionRef from "effect/SubscriptionRef"; |
| 6 | import { AsyncResult, Atom } from "effect/unstable/reactivity"; | 6 | import { AsyncResult, Atom } from "effect/unstable/reactivity"; |
| 7 | 7 | ||
| 8 | import { EnvironmentRegistry, type EnvironmentRegistryService } from "../connection/registry.ts"; | 8 | import * as EnvironmentRegistry from "../connection/registry.ts"; |
| 9 | import type { ConnectionCatalogEntry } from "../connection/catalog.ts"; | 9 | import type { ConnectionCatalogEntry } from "../connection/catalog.ts"; |
| 10 | import { AVAILABLE_CONNECTION_STATE } from "../connection/model.ts"; | 10 | import { AVAILABLE_CONNECTION_STATE } from "../connection/model.ts"; |
| 11 | import { EnvironmentSupervisor } from "../connection/supervisor.ts"; | 11 | import * as EnvironmentSupervisor from "../connection/supervisor.ts"; |
| 12 | import { | 12 | import { |
| 13 | createAtomCommandScheduler, | 13 | createAtomCommandScheduler, |
| 14 | createRuntimeCommand, | 14 | createRuntimeCommand, |
| ... | @@ -26,13 +26,13 @@ export const EMPTY_ENVIRONMENT_CATALOG_STATE: EnvironmentCatalogState = Object.f | ... | @@ -26,13 +26,13 @@ export const EMPTY_ENVIRONMENT_CATALOG_STATE: EnvironmentCatalogState = Object.f |
| 26 | }); | 26 | }); |
| 27 | 27 | ||
| 28 | export function createEnvironmentCatalogAtoms<R, E>( | 28 | export function createEnvironmentCatalogAtoms<R, E>( |
| 29 | runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>, | 29 | runtime: Atom.AtomRuntime<EnvironmentRegistry.EnvironmentRegistry | R, E>, |
| 30 | ) { | 30 | ) { |
| 31 | const commandScheduler = createAtomCommandScheduler(); | 31 | const commandScheduler = createAtomCommandScheduler(); |
| 32 | const serial = { mode: "serial" as const, key: () => "environment-catalog" }; | 32 | const serial = { mode: "serial" as const, key: () => "environment-catalog" }; |
| 33 | const catalogAtom = runtime.atom( | 33 | const catalogAtom = runtime.atom( |
| 34 | Stream.unwrap( | 34 | Stream.unwrap( |
| 35 | EnvironmentRegistry.pipe( | 35 | EnvironmentRegistry.EnvironmentRegistry.pipe( |
| 36 | Effect.map((registry) => | 36 | Effect.map((registry) => |
| 37 | SubscriptionRef.changes(registry.entries).pipe( | 37 | SubscriptionRef.changes(registry.entries).pipe( |
| 38 | Stream.map((entries) => ({ | 38 | Stream.map((entries) => ({ |
| ... | @@ -52,7 +52,7 @@ export function createEnvironmentCatalogAtoms<R, E>( | ... | @@ -52,7 +52,7 @@ export function createEnvironmentCatalogAtoms<R, E>( |
| 52 | 52 | ||
| 53 | const networkStatusAtom = runtime.atom( | 53 | const networkStatusAtom = runtime.atom( |
| 54 | Stream.unwrap( | 54 | Stream.unwrap( |
| 55 | EnvironmentRegistry.pipe( | 55 | EnvironmentRegistry.EnvironmentRegistry.pipe( |
| 56 | Effect.map((registry) => SubscriptionRef.changes(registry.networkStatus)), | 56 | Effect.map((registry) => SubscriptionRef.changes(registry.networkStatus)), |
| 57 | ), | 57 | ), |
| 58 | ), | 58 | ), |
| ... | @@ -68,7 +68,7 @@ export function createEnvironmentCatalogAtoms<R, E>( | ... | @@ -68,7 +68,7 @@ export function createEnvironmentCatalogAtoms<R, E>( |
| 68 | followStreamInEnvironment( | 68 | followStreamInEnvironment( |
| 69 | environmentId, | 69 | environmentId, |
| 70 | Stream.unwrap( | 70 | Stream.unwrap( |
| 71 | EnvironmentSupervisor.pipe( | 71 | EnvironmentSupervisor.EnvironmentSupervisor.pipe( |
| 72 | Effect.map((supervisor) => SubscriptionRef.changes(supervisor.state)), | 72 | Effect.map((supervisor) => SubscriptionRef.changes(supervisor.state)), |
| 73 | ), | 73 | ), |
| 74 | ), | 74 | ), |
| ... | @@ -81,29 +81,39 @@ export function createEnvironmentCatalogAtoms<R, E>( | ... | @@ -81,29 +81,39 @@ export function createEnvironmentCatalogAtoms<R, E>( |
| 81 | label: "environment-catalog:register", | 81 | label: "environment-catalog:register", |
| 82 | scheduler: commandScheduler, | 82 | scheduler: commandScheduler, |
| 83 | concurrency: serial, | 83 | concurrency: serial, |
| 84 | execute: (target: Parameters<EnvironmentRegistryService["register"]>[0]) => | 84 | execute: ( |
| 85 | EnvironmentRegistry.pipe(Effect.flatMap((registry) => registry.register(target))), | 85 | target: Parameters<EnvironmentRegistry.EnvironmentRegistry["Service"]["register"]>[0], |
| 86 | ) => | ||
| 87 | EnvironmentRegistry.EnvironmentRegistry.pipe( | ||
| 88 | Effect.flatMap((registry) => registry.register(target)), | ||
| 89 | ), | ||
| 86 | }); | 90 | }); |
| 87 | const remove = createRuntimeCommand(runtime, { | 91 | const remove = createRuntimeCommand(runtime, { |
| 88 | label: "environment-catalog:remove", | 92 | label: "environment-catalog:remove", |
| 89 | scheduler: commandScheduler, | 93 | scheduler: commandScheduler, |
| 90 | concurrency: serial, | 94 | concurrency: serial, |
| 91 | execute: (environmentId: EnvironmentIdType) => | 95 | execute: (environmentId: EnvironmentIdType) => |
| 92 | EnvironmentRegistry.pipe(Effect.flatMap((registry) => registry.remove(environmentId))), | 96 | EnvironmentRegistry.EnvironmentRegistry.pipe( |
| 97 | Effect.flatMap((registry) => registry.remove(environmentId)), | ||
| 98 | ), | ||
| 93 | }); | 99 | }); |
| 94 | const removeRelayEnvironments = createRuntimeCommand(runtime, { | 100 | const removeRelayEnvironments = createRuntimeCommand(runtime, { |
| 95 | label: "environment-catalog:remove-relay-environments", | 101 | label: "environment-catalog:remove-relay-environments", |
| 96 | scheduler: commandScheduler, | 102 | scheduler: commandScheduler, |
| 97 | concurrency: serial, | 103 | concurrency: serial, |
| 98 | execute: (_input: void) => | 104 | execute: (_input: void) => |
| 99 | EnvironmentRegistry.pipe(Effect.flatMap((registry) => registry.removeRelayEnvironments())), | 105 | EnvironmentRegistry.EnvironmentRegistry.pipe( |
| 106 | Effect.flatMap((registry) => registry.removeRelayEnvironments()), | ||
| 107 | ), | ||
| 100 | }); | 108 | }); |
| 101 | const retryNow = createRuntimeCommand(runtime, { | 109 | const retryNow = createRuntimeCommand(runtime, { |
| 102 | label: "environment-catalog:retry-now", | 110 | label: "environment-catalog:retry-now", |
| 103 | scheduler: commandScheduler, | 111 | scheduler: commandScheduler, |
| 104 | concurrency: serial, | 112 | concurrency: serial, |
| 105 | execute: (environmentId: EnvironmentIdType) => | 113 | execute: (environmentId: EnvironmentIdType) => |
| 106 | EnvironmentRegistry.pipe(Effect.flatMap((registry) => registry.retryNow(environmentId))), | 114 | EnvironmentRegistry.EnvironmentRegistry.pipe( |
| 115 | Effect.flatMap((registry) => registry.retryNow(environmentId)), | ||
| 116 | ), | ||
| 107 | }); | 117 | }); |
| 108 | 118 | ||
| 109 | return { | 119 | return { |
packages/client-runtime/src/state/shell-sync.test.ts+10-13| ... | @@ -16,12 +16,9 @@ import { | ... | @@ -16,12 +16,9 @@ import { |
| 16 | PrimaryConnectionTarget, | 16 | PrimaryConnectionTarget, |
| 17 | type PreparedConnection, | 17 | type PreparedConnection, |
| 18 | } from "../connection/model.ts"; | 18 | } from "../connection/model.ts"; |
| 19 | import { | 19 | import * as EnvironmentSupervisor from "../connection/supervisor.ts"; |
| 20 | EnvironmentSupervisor, | 20 | import * as Persistence from "../platform/persistence.ts"; |
| 21 | type EnvironmentSupervisorService, | 21 | import * as RpcSession from "../rpc/session.ts"; |
| 22 | } from "../connection/supervisor.ts"; | ||
| 23 | import { EnvironmentCacheStore } from "../platform/persistence.ts"; | ||
| 24 | import type { RpcSession } from "../rpc/session.ts"; | ||
| 25 | import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; | 22 | import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; |
| 26 | import { makeEnvironmentShellState } from "./shell.ts"; | 23 | import { makeEnvironmentShellState } from "./shell.ts"; |
| 27 | 24 | ||
| ... | @@ -39,7 +36,7 @@ const LIVE_SHELL_SNAPSHOT: OrchestrationShellSnapshot = { | ... | @@ -39,7 +36,7 @@ const LIVE_SHELL_SNAPSHOT: OrchestrationShellSnapshot = { |
| 39 | updatedAt: "2026-06-06T00:00:00.000Z", | 36 | updatedAt: "2026-06-06T00:00:00.000Z", |
| 40 | }; | 37 | }; |
| 41 | 38 | ||
| 42 | function session(client: WsRpcProtocolClient): RpcSession { | 39 | function session(client: WsRpcProtocolClient): RpcSession.RpcSession { |
| 43 | return { | 40 | return { |
| 44 | client, | 41 | client, |
| 45 | initialConfig: Effect.never, | 42 | initialConfig: Effect.never, |
| ... | @@ -57,10 +54,10 @@ describe("environment shell synchronization", () => { | ... | @@ -57,10 +54,10 @@ describe("environment shell synchronization", () => { |
| 57 | [ORCHESTRATION_WS_METHODS.subscribeShell]: () => Stream.fromQueue(events), | 54 | [ORCHESTRATION_WS_METHODS.subscribeShell]: () => Stream.fromQueue(events), |
| 58 | } as unknown as WsRpcProtocolClient; | 55 | } as unknown as WsRpcProtocolClient; |
| 59 | const supervisorState = yield* SubscriptionRef.make(AVAILABLE_CONNECTION_STATE); | 56 | const supervisorState = yield* SubscriptionRef.make(AVAILABLE_CONNECTION_STATE); |
| 60 | const activeSession = yield* SubscriptionRef.make<Option.Option<RpcSession>>( | 57 | const activeSession = yield* SubscriptionRef.make<Option.Option<RpcSession.RpcSession>>( |
| 61 | Option.some(session(client)), | 58 | Option.some(session(client)), |
| 62 | ); | 59 | ); |
| 63 | const supervisor = EnvironmentSupervisor.of({ | 60 | const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({ |
| 64 | target: TARGET, | 61 | target: TARGET, |
| 65 | state: supervisorState, | 62 | state: supervisorState, |
| 66 | session: activeSession, | 63 | session: activeSession, |
| ... | @@ -68,8 +65,8 @@ describe("environment shell synchronization", () => { | ... | @@ -68,8 +65,8 @@ describe("environment shell synchronization", () => { |
| 68 | connect: Effect.void, | 65 | connect: Effect.void, |
| 69 | disconnect: Effect.void, | 66 | disconnect: Effect.void, |
| 70 | retryNow: Effect.void, | 67 | retryNow: Effect.void, |
| 71 | } satisfies EnvironmentSupervisorService); | 68 | } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]); |
| 72 | const cache = EnvironmentCacheStore.of({ | 69 | const cache = Persistence.EnvironmentCacheStore.of({ |
| 73 | loadShell: () => Effect.succeed(Option.none()), | 70 | loadShell: () => Effect.succeed(Option.none()), |
| 74 | saveShell: () => Effect.never, | 71 | saveShell: () => Effect.never, |
| 75 | loadThread: () => Effect.succeed(Option.none()), | 72 | loadThread: () => Effect.succeed(Option.none()), |
| ... | @@ -78,8 +75,8 @@ describe("environment shell synchronization", () => { | ... | @@ -78,8 +75,8 @@ describe("environment shell synchronization", () => { |
| 78 | clear: () => Effect.void, | 75 | clear: () => Effect.void, |
| 79 | }); | 76 | }); |
| 80 | const shellState = yield* makeEnvironmentShellState().pipe( | 77 | const shellState = yield* makeEnvironmentShellState().pipe( |
| 81 | Effect.provideService(EnvironmentSupervisor, supervisor), | 78 | Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), |
| 82 | Effect.provideService(EnvironmentCacheStore, cache), | 79 | Effect.provideService(Persistence.EnvironmentCacheStore, cache), |
| 83 | ); | 80 | ); |
| 84 | 81 | ||
| 85 | yield* SubscriptionRef.set(supervisorState, { | 82 | yield* SubscriptionRef.set(supervisorState, { |
packages/client-runtime/src/state/threads-sync.test.ts+10-13| ... | @@ -24,12 +24,9 @@ import { | ... | @@ -24,12 +24,9 @@ import { |
| 24 | type PreparedConnection, | 24 | type PreparedConnection, |
| 25 | type SupervisorConnectionState, | 25 | type SupervisorConnectionState, |
| 26 | } from "../connection/model.ts"; | 26 | } from "../connection/model.ts"; |
| 27 | import { | 27 | import * as EnvironmentSupervisor from "../connection/supervisor.ts"; |
| 28 | EnvironmentSupervisor, | 28 | import * as Persistence from "../platform/persistence.ts"; |
| 29 | type EnvironmentSupervisorService, | 29 | import * as RpcSession from "../rpc/session.ts"; |
| 30 | } from "../connection/supervisor.ts"; | ||
| 31 | import { EnvironmentCacheStore } from "../platform/persistence.ts"; | ||
| 32 | import type { RpcSession } from "../rpc/session.ts"; | ||
| 33 | import { | 30 | import { |
| 34 | EMPTY_ENVIRONMENT_THREAD_STATE, | 31 | EMPTY_ENVIRONMENT_THREAD_STATE, |
| 35 | makeEnvironmentThreadState, | 32 | makeEnvironmentThreadState, |
| ... | @@ -69,7 +66,7 @@ const BASE_THREAD: OrchestrationThread = { | ... | @@ -69,7 +66,7 @@ const BASE_THREAD: OrchestrationThread = { |
| 69 | 66 | ||
| 70 | type TestThreadInput = OrchestrationThreadStreamItem | Error; | 67 | type TestThreadInput = OrchestrationThreadStreamItem | Error; |
| 71 | 68 | ||
| 72 | function testSession(client: WsRpcProtocolClient): RpcSession { | 69 | function testSession(client: WsRpcProtocolClient): RpcSession.RpcSession { |
| 73 | return { | 70 | return { |
| 74 | client, | 71 | client, |
| 75 | initialConfig: Effect.never, | 72 | initialConfig: Effect.never, |
| ... | @@ -117,11 +114,11 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o | ... | @@ -117,11 +114,11 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o |
| 117 | ), | 114 | ), |
| 118 | ), | 115 | ), |
| 119 | } as unknown as WsRpcProtocolClient; | 116 | } as unknown as WsRpcProtocolClient; |
| 120 | const supervisorSession = yield* SubscriptionRef.make<Option.Option<RpcSession>>( | 117 | const supervisorSession = yield* SubscriptionRef.make<Option.Option<RpcSession.RpcSession>>( |
| 121 | Option.some(testSession(client)), | 118 | Option.some(testSession(client)), |
| 122 | ); | 119 | ); |
| 123 | const prepared = yield* SubscriptionRef.make<Option.Option<PreparedConnection>>(Option.none()); | 120 | const prepared = yield* SubscriptionRef.make<Option.Option<PreparedConnection>>(Option.none()); |
| 124 | const supervisor = EnvironmentSupervisor.of({ | 121 | const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({ |
| 125 | target: TARGET, | 122 | target: TARGET, |
| 126 | state: supervisorState, | 123 | state: supervisorState, |
| 127 | session: supervisorSession, | 124 | session: supervisorSession, |
| ... | @@ -129,8 +126,8 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o | ... | @@ -129,8 +126,8 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o |
| 129 | connect: Effect.void, | 126 | connect: Effect.void, |
| 130 | disconnect: Effect.void, | 127 | disconnect: Effect.void, |
| 131 | retryNow: Ref.update(retryCount, (count) => count + 1), | 128 | retryNow: Ref.update(retryCount, (count) => count + 1), |
| 132 | } satisfies EnvironmentSupervisorService); | 129 | } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]); |
| 133 | const cache = EnvironmentCacheStore.of({ | 130 | const cache = Persistence.EnvironmentCacheStore.of({ |
| 134 | loadShell: () => Effect.succeed(Option.none()), | 131 | loadShell: () => Effect.succeed(Option.none()), |
| 135 | saveShell: () => Effect.void, | 132 | saveShell: () => Effect.void, |
| 136 | loadThread: (_environmentId, threadId) => | 133 | loadThread: (_environmentId, threadId) => |
| ... | @@ -146,8 +143,8 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o | ... | @@ -146,8 +143,8 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o |
| 146 | clear: () => Effect.void, | 143 | clear: () => Effect.void, |
| 147 | }); | 144 | }); |
| 148 | const threadState = yield* makeEnvironmentThreadState(THREAD_ID).pipe( | 145 | const threadState = yield* makeEnvironmentThreadState(THREAD_ID).pipe( |
| 149 | Effect.provideService(EnvironmentSupervisor, supervisor), | 146 | Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), |
| 150 | Effect.provideService(EnvironmentCacheStore, cache), | 147 | Effect.provideService(Persistence.EnvironmentCacheStore, cache), |
| 151 | ); | 148 | ); |
| 152 | yield* SubscriptionRef.changes(threadState).pipe( | 149 | yield* SubscriptionRef.changes(threadState).pipe( |
| 153 | Stream.runForEach((state) => | 150 | Stream.runForEach((state) => |