| author | |
| committer | |
| log | 2640e6dcfcb6a5ba2f01f20295ce8bacd2fdb05e |
| tree | 210a8fa0ba371e6190a231c6826852289a801bbc |
| parent | e34250df469dcf50b444777f903b5f3482b4ce3c |
| signature | Signed by PGP key B5690EEEBB952194 |
8 files changed, 105 insertions(+), 7 deletions(-)
apps/server/src/environment/ServerEnvironment.test.ts+1| ... | ... | @@ -67,6 +67,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => { |
| 67 | 67 | |
| 68 | 68 | expect(first.environmentId).toBe(second.environmentId); |
| 69 | 69 | expect(second.capabilities.repositoryIdentity).toBe(true); |
| 70 | expect(second.capabilities.connectionProbe).toBe(true); | |
| 70 | 71 | }), |
| 71 | 72 | ); |
| 72 | 73 |
apps/server/src/environment/ServerEnvironment.ts+1| ... | ... | @@ -135,6 +135,7 @@ export const make = Effect.gen(function* () { |
| 135 | 135 | serverVersion: packageJson.version, |
| 136 | 136 | capabilities: { |
| 137 | 137 | repositoryIdentity: true, |
| 138 | connectionProbe: true, | |
| 138 | 139 | }, |
| 139 | 140 | }; |
| 140 | 141 |
apps/server/src/ws.ts+5| ... | ... | @@ -284,6 +284,7 @@ const RPC_REQUIRED_SCOPE = new Map<string, AuthEnvironmentScope>([ |
| 284 | 284 | [ORCHESTRATION_WS_METHODS.subscribeShell, AuthOrchestrationReadScope], |
| 285 | 285 | [ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, AuthOrchestrationReadScope], |
| 286 | 286 | [ORCHESTRATION_WS_METHODS.subscribeThread, AuthOrchestrationReadScope], |
| 287 | [WS_METHODS.serverProbe, AuthOrchestrationReadScope], | |
| 287 | 288 | [WS_METHODS.serverGetConfig, AuthOrchestrationReadScope], |
| 288 | 289 | [WS_METHODS.serverRefreshProviders, AuthOrchestrationOperateScope], |
| 289 | 290 | [WS_METHODS.serverUpdateProvider, AuthOrchestrationOperateScope], |
| ... | ... | @@ -1238,6 +1239,10 @@ const makeWsRpcLayer = ( |
| 1238 | 1239 | }), |
| 1239 | 1240 | { "rpc.aggregate": "orchestration" }, |
| 1240 | 1241 | ), |
| 1242 | [WS_METHODS.serverProbe]: (_input) => | |
| 1243 | observeRpcEffect(WS_METHODS.serverProbe, Effect.succeed({}), { | |
| 1244 | "rpc.aggregate": "server", | |
| 1245 | }), | |
| 1241 | 1246 | [WS_METHODS.serverGetConfig]: (_input) => |
| 1242 | 1247 | observeRpcEffect(WS_METHODS.serverGetConfig, loadServerConfig, { |
| 1243 | 1248 | "rpc.aggregate": "server", |
packages/client-runtime/src/connection/supervisor.test.ts+1-1| ... | ... | @@ -652,7 +652,7 @@ describe("EnvironmentSupervisor", () => { |
| 652 | 652 | }).pipe(Effect.provide(TestClock.layer())), |
| 653 | 653 | ); |
| 654 | 654 | |
| 655 | it.effect("keeps a healthy session when the application becomes active", () => | |
| 655 | it.effect("probes the active session without reconnecting on application activation", () => | |
| 656 | 656 | Effect.gen(function* () { |
| 657 | 657 | const probeCount = yield* Ref.make(0); |
| 658 | 658 | const probeCalled = yield* Deferred.make<void>(); |
packages/client-runtime/src/rpc/session.test.ts+78-2| ... | ... | @@ -109,6 +109,7 @@ const SERVER_CONFIG: ServerConfigType = { |
| 109 | 109 | serverVersion: "0.0.0-test", |
| 110 | 110 | capabilities: { |
| 111 | 111 | repositoryIdentity: true, |
| 112 | connectionProbe: true, | |
| 112 | 113 | }, |
| 113 | 114 | }, |
| 114 | 115 | auth: { |
| ... | ... | @@ -141,6 +142,16 @@ const decodeJson = Schema.decodeUnknownSync(Schema.UnknownFromJsonString); |
| 141 | 142 | const decodeRpcRequest = Schema.decodeUnknownSync(RpcRequest); |
| 142 | 143 | const encodeJson = Schema.encodeUnknownSync(Schema.UnknownFromJsonString); |
| 143 | 144 | const encodeServerConfig = Schema.encodeSync(ServerConfig); |
| 145 | const ENCODED_SERVER_CONFIG = encodeServerConfig(SERVER_CONFIG); | |
| 146 | const LEGACY_SERVER_CONFIG = { | |
| 147 | ...ENCODED_SERVER_CONFIG, | |
| 148 | environment: { | |
| 149 | ...ENCODED_SERVER_CONFIG.environment, | |
| 150 | capabilities: { | |
| 151 | repositoryIdentity: true, | |
| 152 | }, | |
| 153 | }, | |
| 154 | }; | |
| 144 | 155 | |
| 145 | 156 | const makeFactory = Effect.fn("TestRpcSessionFactory.make")(function* () { |
| 146 | 157 | const sockets: TestWebSocket[] = []; |
| ... | ... | @@ -169,9 +180,10 @@ const awaitSocket = Effect.fn("TestRpcSessionFactory.awaitSocket")(function* ( |
| 169 | 180 | |
| 170 | 181 | const awaitRequest = Effect.fn("TestRpcSessionFactory.awaitRequest")(function* ( |
| 171 | 182 | socket: TestWebSocket, |
| 183 | index = 0, | |
| 172 | 184 | ) { |
| 173 | 185 | for (let attempt = 0; attempt < 100; attempt += 1) { |
| 174 | const request = socket.sent[0]; | |
| 186 | const request = socket.sent[index]; | |
| 175 | 187 | if (request) { |
| 176 | 188 | return decodeRpcRequest(decodeJson(request)); |
| 177 | 189 | } |
| ... | ... | @@ -182,6 +194,7 @@ const awaitRequest = Effect.fn("TestRpcSessionFactory.awaitRequest")(function* ( |
| 182 | 194 | |
| 183 | 195 | const completeInitialConfig = Effect.fn("TestRpcSessionFactory.completeInitialConfig")(function* ( |
| 184 | 196 | socket: TestWebSocket, |
| 197 | config: unknown = ENCODED_SERVER_CONFIG, | |
| 185 | 198 | ) { |
| 186 | 199 | const request = yield* awaitRequest(socket); |
| 187 | 200 | expect(request).toMatchObject({ |
| ... | ... | @@ -195,7 +208,7 @@ const completeInitialConfig = Effect.fn("TestRpcSessionFactory.completeInitialCo |
| 195 | 208 | requestId: request.id, |
| 196 | 209 | exit: { |
| 197 | 210 | _tag: "Success", |
| 198 | value: encodeServerConfig(SERVER_CONFIG), | |
| 211 | value: config, | |
| 199 | 212 | }, |
| 200 | 213 | }), |
| 201 | 214 | ); |
| ... | ... | @@ -218,6 +231,30 @@ describe("RpcSessionFactory", () => { |
| 218 | 231 | expect(config).toEqual(SERVER_CONFIG); |
| 219 | 232 | expect(socket.sent).toHaveLength(1); |
| 220 | 233 | |
| 234 | const probeFiber = yield* Effect.forkChild(session.probe); | |
| 235 | const probeRequest = yield* awaitRequest(socket, 1); | |
| 236 | expect(probeRequest).toMatchObject({ | |
| 237 | _tag: "Request", | |
| 238 | tag: WS_METHODS.serverProbe, | |
| 239 | payload: {}, | |
| 240 | }); | |
| 241 | socket.serverMessage( | |
| 242 | encodeJson({ | |
| 243 | _tag: "Exit", | |
| 244 | requestId: probeRequest.id, | |
| 245 | exit: { | |
| 246 | _tag: "Success", | |
| 247 | value: {}, | |
| 248 | }, | |
| 249 | }), | |
| 250 | ); | |
| 251 | yield* Fiber.join(probeFiber); | |
| 252 | ||
| 253 | expect(socket.sent.map((request) => decodeRpcRequest(decodeJson(request)).tag)).toEqual([ | |
| 254 | WS_METHODS.serverGetConfig, | |
| 255 | WS_METHODS.serverProbe, | |
| 256 | ]); | |
| 257 | ||
| 221 | 258 | socket.close(1012, "service restart"); |
| 222 | 259 | const error = yield* Effect.flip(session.closed); |
| 223 | 260 | |
| ... | ... | @@ -250,6 +287,45 @@ describe("RpcSessionFactory", () => { |
| 250 | 287 | }), |
| 251 | 288 | ); |
| 252 | 289 | |
| 290 | it.effect("uses the legacy config RPC for probes when the server lacks the capability", () => | |
| 291 | Effect.scoped( | |
| 292 | Effect.gen(function* () { | |
| 293 | const { factory, sockets } = yield* makeFactory(); | |
| 294 | const session = yield* factory.connect(PREPARED); | |
| 295 | const readyFiber = yield* Effect.forkChild(session.ready); | |
| 296 | const socket = yield* awaitSocket(sockets); | |
| 297 | ||
| 298 | socket.open(); | |
| 299 | yield* completeInitialConfig(socket, LEGACY_SERVER_CONFIG); | |
| 300 | yield* Fiber.join(readyFiber); | |
| 301 | ||
| 302 | const probeFiber = yield* Effect.forkChild(session.probe); | |
| 303 | const probeRequest = yield* awaitRequest(socket, 1); | |
| 304 | expect(probeRequest).toMatchObject({ | |
| 305 | _tag: "Request", | |
| 306 | tag: WS_METHODS.serverGetConfig, | |
| 307 | payload: {}, | |
| 308 | }); | |
| 309 | socket.serverMessage( | |
| 310 | encodeJson({ | |
| 311 | _tag: "Exit", | |
| 312 | requestId: probeRequest.id, | |
| 313 | exit: { | |
| 314 | _tag: "Success", | |
| 315 | value: LEGACY_SERVER_CONFIG, | |
| 316 | }, | |
| 317 | }), | |
| 318 | ); | |
| 319 | yield* Fiber.join(probeFiber); | |
| 320 | ||
| 321 | expect(socket.sent.map((request) => decodeRpcRequest(decodeJson(request)).tag)).toEqual([ | |
| 322 | WS_METHODS.serverGetConfig, | |
| 323 | WS_METHODS.serverGetConfig, | |
| 324 | ]); | |
| 325 | }), | |
| 326 | ), | |
| 327 | ); | |
| 328 | ||
| 253 | 329 | it.effect("fails readiness when the websocket never opens", () => |
| 254 | 330 | Effect.gen(function* () { |
| 255 | 331 | const { factory, sockets } = yield* makeFactory(); |
packages/client-runtime/src/rpc/session.ts+10-4| ... | ... | @@ -42,8 +42,9 @@ export class RpcSessionFactory extends Context.Service< |
| 42 | 42 | type InitialConfigError = Effect.Error< |
| 43 | 43 | ReturnType<WsRpcProtocolClient[typeof WS_METHODS.serverGetConfig]> |
| 44 | 44 | >; |
| 45 | type ProbeError = Effect.Error<ReturnType<WsRpcProtocolClient[typeof WS_METHODS.serverProbe]>>; | |
| 45 | 46 | |
| 46 | function mapInitialConfigError(error: InitialConfigError): ConnectionAttemptError { | |
| 47 | function mapSessionRpcError(error: InitialConfigError | ProbeError): ConnectionAttemptError { | |
| 47 | 48 | switch (error._tag) { |
| 48 | 49 | case "EnvironmentAuthorizationError": |
| 49 | 50 | return new ConnectionBlockedError({ |
| ... | ... | @@ -115,12 +116,17 @@ export const make = Effect.gen(function* () { |
| 115 | 116 | const client = yield* makeWsRpcProtocolClient.pipe(Effect.provide(protocolContext)); |
| 116 | 117 | const initialConfig = yield* Effect.cached( |
| 117 | 118 | client[WS_METHODS.serverGetConfig]({}).pipe( |
| 118 | Effect.mapError(mapInitialConfigError), | |
| 119 | Effect.mapError(mapSessionRpcError), | |
| 119 | 120 | Effect.withSpan("environment.initialSync"), |
| 120 | 121 | ), |
| 121 | 122 | ); |
| 122 | const probe = client[WS_METHODS.serverGetConfig]({}).pipe( | |
| 123 | Effect.mapError(mapInitialConfigError), | |
| 123 | const probe = initialConfig.pipe( | |
| 124 | Effect.flatMap((config) => | |
| 125 | (config.environment.capabilities.connectionProbe === true | |
| 126 | ? client[WS_METHODS.serverProbe]({}) | |
| 127 | : client[WS_METHODS.serverGetConfig]({}) | |
| 128 | ).pipe(Effect.mapError(mapSessionRpcError)), | |
| 129 | ), | |
| 124 | 130 | Effect.asVoid, |
| 125 | 131 | Effect.withSpan("clientRuntime.connection.rpcSession.probe"), |
| 126 | 132 | ); |
packages/contracts/src/environment.ts+1| ... | ... | @@ -22,6 +22,7 @@ export type ExecutionEnvironmentPlatform = typeof ExecutionEnvironmentPlatform.T |
| 22 | 22 | |
| 23 | 23 | export const ExecutionEnvironmentCapabilities = Schema.Struct({ |
| 24 | 24 | repositoryIdentity: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), |
| 25 | connectionProbe: Schema.optionalKey(Schema.Boolean), | |
| 25 | 26 | }); |
| 26 | 27 | export type ExecutionEnvironmentCapabilities = typeof ExecutionEnvironmentCapabilities.Type; |
| 27 | 28 |
packages/contracts/src/rpc.ts+8| ... | ... | @@ -201,6 +201,7 @@ export const WS_METHODS = { |
| 201 | 201 | previewAutomationFocusHost: "previewAutomation.focusHost", |
| 202 | 202 | |
| 203 | 203 | // Server meta |
| 204 | serverProbe: "server.probe", | |
| 204 | 205 | serverGetConfig: "server.getConfig", |
| 205 | 206 | serverRefreshProviders: "server.refreshProviders", |
| 206 | 207 | serverUpdateProvider: "server.updateProvider", |
| ... | ... | @@ -246,6 +247,12 @@ export const WsServerRemoveKeybindingRpc = Rpc.make(WS_METHODS.serverRemoveKeybi |
| 246 | 247 | error: Schema.Union([KeybindingsConfigError, EnvironmentAuthorizationError]), |
| 247 | 248 | }); |
| 248 | 249 | |
| 250 | export const WsServerProbeRpc = Rpc.make(WS_METHODS.serverProbe, { | |
| 251 | payload: Schema.Struct({}), | |
| 252 | success: Schema.Struct({}), | |
| 253 | error: EnvironmentAuthorizationError, | |
| 254 | }); | |
| 255 | ||
| 249 | 256 | export const WsServerGetConfigRpc = Rpc.make(WS_METHODS.serverGetConfig, { |
| 250 | 257 | payload: Schema.Struct({}), |
| 251 | 258 | success: ServerConfig, |
| ... | ... | @@ -682,6 +689,7 @@ export const WsSubscribeAuthAccessRpc = Rpc.make(WS_METHODS.subscribeAuthAccess, |
| 682 | 689 | }); |
| 683 | 690 | |
| 684 | 691 | export const WsRpcGroup = RpcGroup.make( |
| 692 | WsServerProbeRpc, | |
| 685 | 693 | WsServerGetConfigRpc, |
| 686 | 694 | WsServerRefreshProvidersRpc, |
| 687 | 695 | WsServerUpdateProviderRpc, |