| 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,6 +67,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => { |
| 67 | 67 | ||
| 68 | expect(first.environmentId).toBe(second.environmentId); | 68 | expect(first.environmentId).toBe(second.environmentId); |
| 69 | expect(second.capabilities.repositoryIdentity).toBe(true); | 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,6 +135,7 @@ export const make = Effect.gen(function* () { |
| 135 | serverVersion: packageJson.version, | 135 | serverVersion: packageJson.version, |
| 136 | capabilities: { | 136 | capabilities: { |
| 137 | repositoryIdentity: true, | 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,6 +284,7 @@ const RPC_REQUIRED_SCOPE = new Map<string, AuthEnvironmentScope>([ |
| 284 | [ORCHESTRATION_WS_METHODS.subscribeShell, AuthOrchestrationReadScope], | 284 | [ORCHESTRATION_WS_METHODS.subscribeShell, AuthOrchestrationReadScope], |
| 285 | [ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, AuthOrchestrationReadScope], | 285 | [ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, AuthOrchestrationReadScope], |
| 286 | [ORCHESTRATION_WS_METHODS.subscribeThread, AuthOrchestrationReadScope], | 286 | [ORCHESTRATION_WS_METHODS.subscribeThread, AuthOrchestrationReadScope], |
| 287 | [WS_METHODS.serverProbe, AuthOrchestrationReadScope], | ||
| 287 | [WS_METHODS.serverGetConfig, AuthOrchestrationReadScope], | 288 | [WS_METHODS.serverGetConfig, AuthOrchestrationReadScope], |
| 288 | [WS_METHODS.serverRefreshProviders, AuthOrchestrationOperateScope], | 289 | [WS_METHODS.serverRefreshProviders, AuthOrchestrationOperateScope], |
| 289 | [WS_METHODS.serverUpdateProvider, AuthOrchestrationOperateScope], | 290 | [WS_METHODS.serverUpdateProvider, AuthOrchestrationOperateScope], |
| ... | @@ -1238,6 +1239,10 @@ const makeWsRpcLayer = ( | ... | @@ -1238,6 +1239,10 @@ const makeWsRpcLayer = ( |
| 1238 | }), | 1239 | }), |
| 1239 | { "rpc.aggregate": "orchestration" }, | 1240 | { "rpc.aggregate": "orchestration" }, |
| 1240 | ), | 1241 | ), |
| 1242 | [WS_METHODS.serverProbe]: (_input) => | ||
| 1243 | observeRpcEffect(WS_METHODS.serverProbe, Effect.succeed({}), { | ||
| 1244 | "rpc.aggregate": "server", | ||
| 1245 | }), | ||
| 1241 | [WS_METHODS.serverGetConfig]: (_input) => | 1246 | [WS_METHODS.serverGetConfig]: (_input) => |
| 1242 | observeRpcEffect(WS_METHODS.serverGetConfig, loadServerConfig, { | 1247 | observeRpcEffect(WS_METHODS.serverGetConfig, loadServerConfig, { |
| 1243 | "rpc.aggregate": "server", | 1248 | "rpc.aggregate": "server", |
packages/client-runtime/src/connection/supervisor.test.ts+1-1| ... | @@ -652,7 +652,7 @@ describe("EnvironmentSupervisor", () => { | ... | @@ -652,7 +652,7 @@ describe("EnvironmentSupervisor", () => { |
| 652 | }).pipe(Effect.provide(TestClock.layer())), | 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 | Effect.gen(function* () { | 656 | Effect.gen(function* () { |
| 657 | const probeCount = yield* Ref.make(0); | 657 | const probeCount = yield* Ref.make(0); |
| 658 | const probeCalled = yield* Deferred.make<void>(); | 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,6 +109,7 @@ const SERVER_CONFIG: ServerConfigType = { |
| 109 | serverVersion: "0.0.0-test", | 109 | serverVersion: "0.0.0-test", |
| 110 | capabilities: { | 110 | capabilities: { |
| 111 | repositoryIdentity: true, | 111 | repositoryIdentity: true, |
| 112 | connectionProbe: true, | ||
| 112 | }, | 113 | }, |
| 113 | }, | 114 | }, |
| 114 | auth: { | 115 | auth: { |
| ... | @@ -141,6 +142,16 @@ const decodeJson = Schema.decodeUnknownSync(Schema.UnknownFromJsonString); | ... | @@ -141,6 +142,16 @@ const decodeJson = Schema.decodeUnknownSync(Schema.UnknownFromJsonString); |
| 141 | const decodeRpcRequest = Schema.decodeUnknownSync(RpcRequest); | 142 | const decodeRpcRequest = Schema.decodeUnknownSync(RpcRequest); |
| 142 | const encodeJson = Schema.encodeUnknownSync(Schema.UnknownFromJsonString); | 143 | const encodeJson = Schema.encodeUnknownSync(Schema.UnknownFromJsonString); |
| 143 | const encodeServerConfig = Schema.encodeSync(ServerConfig); | 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 | const makeFactory = Effect.fn("TestRpcSessionFactory.make")(function* () { | 156 | const makeFactory = Effect.fn("TestRpcSessionFactory.make")(function* () { |
| 146 | const sockets: TestWebSocket[] = []; | 157 | const sockets: TestWebSocket[] = []; |
| ... | @@ -169,9 +180,10 @@ const awaitSocket = Effect.fn("TestRpcSessionFactory.awaitSocket")(function* ( | ... | @@ -169,9 +180,10 @@ const awaitSocket = Effect.fn("TestRpcSessionFactory.awaitSocket")(function* ( |
| 169 | 180 | ||
| 170 | const awaitRequest = Effect.fn("TestRpcSessionFactory.awaitRequest")(function* ( | 181 | const awaitRequest = Effect.fn("TestRpcSessionFactory.awaitRequest")(function* ( |
| 171 | socket: TestWebSocket, | 182 | socket: TestWebSocket, |
| 183 | index = 0, | ||
| 172 | ) { | 184 | ) { |
| 173 | for (let attempt = 0; attempt < 100; attempt += 1) { | 185 | for (let attempt = 0; attempt < 100; attempt += 1) { |
| 174 | const request = socket.sent[0]; | 186 | const request = socket.sent[index]; |
| 175 | if (request) { | 187 | if (request) { |
| 176 | return decodeRpcRequest(decodeJson(request)); | 188 | return decodeRpcRequest(decodeJson(request)); |
| 177 | } | 189 | } |
| ... | @@ -182,6 +194,7 @@ const awaitRequest = Effect.fn("TestRpcSessionFactory.awaitRequest")(function* ( | ... | @@ -182,6 +194,7 @@ const awaitRequest = Effect.fn("TestRpcSessionFactory.awaitRequest")(function* ( |
| 182 | 194 | ||
| 183 | const completeInitialConfig = Effect.fn("TestRpcSessionFactory.completeInitialConfig")(function* ( | 195 | const completeInitialConfig = Effect.fn("TestRpcSessionFactory.completeInitialConfig")(function* ( |
| 184 | socket: TestWebSocket, | 196 | socket: TestWebSocket, |
| 197 | config: unknown = ENCODED_SERVER_CONFIG, | ||
| 185 | ) { | 198 | ) { |
| 186 | const request = yield* awaitRequest(socket); | 199 | const request = yield* awaitRequest(socket); |
| 187 | expect(request).toMatchObject({ | 200 | expect(request).toMatchObject({ |
| ... | @@ -195,7 +208,7 @@ const completeInitialConfig = Effect.fn("TestRpcSessionFactory.completeInitialCo | ... | @@ -195,7 +208,7 @@ const completeInitialConfig = Effect.fn("TestRpcSessionFactory.completeInitialCo |
| 195 | requestId: request.id, | 208 | requestId: request.id, |
| 196 | exit: { | 209 | exit: { |
| 197 | _tag: "Success", | 210 | _tag: "Success", |
| 198 | value: encodeServerConfig(SERVER_CONFIG), | 211 | value: config, |
| 199 | }, | 212 | }, |
| 200 | }), | 213 | }), |
| 201 | ); | 214 | ); |
| ... | @@ -218,6 +231,30 @@ describe("RpcSessionFactory", () => { | ... | @@ -218,6 +231,30 @@ describe("RpcSessionFactory", () => { |
| 218 | expect(config).toEqual(SERVER_CONFIG); | 231 | expect(config).toEqual(SERVER_CONFIG); |
| 219 | expect(socket.sent).toHaveLength(1); | 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 | socket.close(1012, "service restart"); | 258 | socket.close(1012, "service restart"); |
| 222 | const error = yield* Effect.flip(session.closed); | 259 | const error = yield* Effect.flip(session.closed); |
| 223 | 260 | ||
| ... | @@ -250,6 +287,45 @@ describe("RpcSessionFactory", () => { | ... | @@ -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 | it.effect("fails readiness when the websocket never opens", () => | 329 | it.effect("fails readiness when the websocket never opens", () => |
| 254 | Effect.gen(function* () { | 330 | Effect.gen(function* () { |
| 255 | const { factory, sockets } = yield* makeFactory(); | 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,8 +42,9 @@ export class RpcSessionFactory extends Context.Service< |
| 42 | type InitialConfigError = Effect.Error< | 42 | type InitialConfigError = Effect.Error< |
| 43 | ReturnType<WsRpcProtocolClient[typeof WS_METHODS.serverGetConfig]> | 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 | switch (error._tag) { | 48 | switch (error._tag) { |
| 48 | case "EnvironmentAuthorizationError": | 49 | case "EnvironmentAuthorizationError": |
| 49 | return new ConnectionBlockedError({ | 50 | return new ConnectionBlockedError({ |
| ... | @@ -115,12 +116,17 @@ export const make = Effect.gen(function* () { | ... | @@ -115,12 +116,17 @@ export const make = Effect.gen(function* () { |
| 115 | const client = yield* makeWsRpcProtocolClient.pipe(Effect.provide(protocolContext)); | 116 | const client = yield* makeWsRpcProtocolClient.pipe(Effect.provide(protocolContext)); |
| 116 | const initialConfig = yield* Effect.cached( | 117 | const initialConfig = yield* Effect.cached( |
| 117 | client[WS_METHODS.serverGetConfig]({}).pipe( | 118 | client[WS_METHODS.serverGetConfig]({}).pipe( |
| 118 | Effect.mapError(mapInitialConfigError), | 119 | Effect.mapError(mapSessionRpcError), |
| 119 | Effect.withSpan("environment.initialSync"), | 120 | Effect.withSpan("environment.initialSync"), |
| 120 | ), | 121 | ), |
| 121 | ); | 122 | ); |
| 122 | const probe = client[WS_METHODS.serverGetConfig]({}).pipe( | 123 | const probe = initialConfig.pipe( |
| 123 | Effect.mapError(mapInitialConfigError), | 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 | Effect.asVoid, | 130 | Effect.asVoid, |
| 125 | Effect.withSpan("clientRuntime.connection.rpcSession.probe"), | 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,6 +22,7 @@ export type ExecutionEnvironmentPlatform = typeof ExecutionEnvironmentPlatform.T |
| 22 | 22 | ||
| 23 | export const ExecutionEnvironmentCapabilities = Schema.Struct({ | 23 | export const ExecutionEnvironmentCapabilities = Schema.Struct({ |
| 24 | repositoryIdentity: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), | 24 | repositoryIdentity: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), |
| 25 | connectionProbe: Schema.optionalKey(Schema.Boolean), | ||
| 25 | }); | 26 | }); |
| 26 | export type ExecutionEnvironmentCapabilities = typeof ExecutionEnvironmentCapabilities.Type; | 27 | export type ExecutionEnvironmentCapabilities = typeof ExecutionEnvironmentCapabilities.Type; |
| 27 | 28 |
packages/contracts/src/rpc.ts+8| ... | @@ -201,6 +201,7 @@ export const WS_METHODS = { | ... | @@ -201,6 +201,7 @@ export const WS_METHODS = { |
| 201 | previewAutomationFocusHost: "previewAutomation.focusHost", | 201 | previewAutomationFocusHost: "previewAutomation.focusHost", |
| 202 | 202 | ||
| 203 | // Server meta | 203 | // Server meta |
| 204 | serverProbe: "server.probe", | ||
| 204 | serverGetConfig: "server.getConfig", | 205 | serverGetConfig: "server.getConfig", |
| 205 | serverRefreshProviders: "server.refreshProviders", | 206 | serverRefreshProviders: "server.refreshProviders", |
| 206 | serverUpdateProvider: "server.updateProvider", | 207 | serverUpdateProvider: "server.updateProvider", |
| ... | @@ -246,6 +247,12 @@ export const WsServerRemoveKeybindingRpc = Rpc.make(WS_METHODS.serverRemoveKeybi | ... | @@ -246,6 +247,12 @@ export const WsServerRemoveKeybindingRpc = Rpc.make(WS_METHODS.serverRemoveKeybi |
| 246 | error: Schema.Union([KeybindingsConfigError, EnvironmentAuthorizationError]), | 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 | export const WsServerGetConfigRpc = Rpc.make(WS_METHODS.serverGetConfig, { | 256 | export const WsServerGetConfigRpc = Rpc.make(WS_METHODS.serverGetConfig, { |
| 250 | payload: Schema.Struct({}), | 257 | payload: Schema.Struct({}), |
| 251 | success: ServerConfig, | 258 | success: ServerConfig, |
| ... | @@ -682,6 +689,7 @@ export const WsSubscribeAuthAccessRpc = Rpc.make(WS_METHODS.subscribeAuthAccess, | ... | @@ -682,6 +689,7 @@ export const WsSubscribeAuthAccessRpc = Rpc.make(WS_METHODS.subscribeAuthAccess, |
| 682 | }); | 689 | }); |
| 683 | 690 | ||
| 684 | export const WsRpcGroup = RpcGroup.make( | 691 | export const WsRpcGroup = RpcGroup.make( |
| 692 | WsServerProbeRpc, | ||
| 685 | WsServerGetConfigRpc, | 693 | WsServerGetConfigRpc, |
| 686 | WsServerRefreshProvidersRpc, | 694 | WsServerRefreshProvidersRpc, |
| 687 | WsServerUpdateProviderRpc, | 695 | WsServerUpdateProviderRpc, |