Unified · Split
| @@ -9,6 +9,7 @@ import { · 8 unmodified lines | ||
| 9 | 9 | type OrchestrationEvent, |
| 10 | 10 | type OrchestrationThread, |
| 11 | 11 | } from "@t3tools/contracts"; |
| 12 | +import * as Duration from "effect/Duration"; | |
| 12 | 13 | import * as Effect from "effect/Effect"; |
| 13 | 14 | import * as Exit from "effect/Exit"; |
| 14 | 15 | import * as FileSystem from "effect/FileSystem"; |
| @@ -120,39 +121,45 @@ class WaitForTimeoutError extends Schema.TaggedErrorClass<WaitForTimeoutError>() · 105 unmodified lines | ||
| 120 | 121 | }, |
| 121 | 122 | ) {} |
| 122 | 123 | |
| 124 | +class WaitForRetrySignal extends Schema.TaggedErrorClass<WaitForRetrySignal>()( | |
| 125 | + "WaitForRetrySignal", | |
| 126 | + { | |
| 127 | + description: Schema.String, | |
| 128 | + }, | |
| 129 | +) {} | |
| 130 | + | |
| 131 | +const isWaitForRetrySignal = Schema.is(WaitForRetrySignal); | |
| 132 | +const WAIT_FOR_RETRY_INTERVAL = Duration.millis(10); | |
| 133 | +const WAIT_FOR_DEFAULT_TIMEOUT = Duration.seconds(40); | |
| 134 | + | |
| 123 | 135 | function waitFor<A, E>( |
| 124 | 136 | read: Effect.Effect<A, E>, |
| 125 | 137 | predicate: (value: A) => boolean, |
| 126 | 138 | description: string, |
| 127 | − timeoutMs?: number, | |
| 139 | + timeout?: Duration.Input, | |
| 128 | 140 | ): Effect.Effect<A, never>; |
| 129 | 141 | function waitFor<A, B extends A, E>( |
| 130 | 142 | read: Effect.Effect<A, E>, |
| 131 | 143 | predicate: (value: A) => value is B, |
| 132 | 144 | description: string, |
| 133 | − timeoutMs?: number, | |
| 145 | + timeout?: Duration.Input, | |
| 134 | 146 | ): Effect.Effect<B, never>; |
| 135 | 147 | function waitFor<A, E>( |
| 136 | 148 | read: Effect.Effect<A, E>, |
| 137 | 149 | predicate: (value: A) => boolean, |
| 138 | 150 | description: string, |
| 139 | − timeoutMs = 40_000, | |
| 151 | + timeout: Duration.Input = WAIT_FOR_DEFAULT_TIMEOUT, | |
| 140 | 152 | ): Effect.Effect<A, never> { |
| 141 | − const RETRY_SIGNAL = "wait_for_retry"; | |
| 142 | − const retryIntervalMs = 10; | |
| 143 | − const maxRetries = Math.max(0, Math.floor(timeoutMs / retryIntervalMs)); | |
| 144 | − const retrySchedule = Schedule.spaced(`${retryIntervalMs} millis`); | |
| 145 | − | |
| 146 | 153 | return read.pipe( |
| 147 | − Effect.filterOrFail(predicate, () => RETRY_SIGNAL), | |
| 154 | + Effect.filterOrFail(predicate, () => new WaitForRetrySignal({ description })), | |
| 148 | 155 | Effect.retry({ |
| 149 | − schedule: retrySchedule, | |
| 150 | − times: maxRetries, | |
| 151 | − while: (error) => error === RETRY_SIGNAL, | |
| 156 | + schedule: Schedule.spaced(WAIT_FOR_RETRY_INTERVAL), | |
| 157 | + while: isWaitForRetrySignal, | |
| 158 | + }), | |
| 159 | + Effect.timeoutOrElse({ | |
| 160 | + duration: timeout, | |
| 161 | + orElse: () => Effect.fail(new WaitForTimeoutError({ description })), | |
| 152 | 162 | }), |
| 153 | − Effect.mapError((error) => | |
| 154 | − error === RETRY_SIGNAL ? new WaitForTimeoutError({ description }) : error, | |
| 155 | − ), | |
| 156 | 163 | Effect.orDie, |
| 157 | 164 | ); |
| 158 | 165 | } |
| @@ -185,11 +192,11 @@ export interface OrchestrationIntegrationHarness { · 26 unmodified lines | ||
| 185 | 192 | readonly waitForThread: ( |
| 186 | 193 | threadId: string, |
| 187 | 194 | predicate: (thread: OrchestrationThread) => boolean, |
| 188 | − timeoutMs?: number, | |
| 195 | + timeout?: Duration.Input, | |
| 189 | 196 | ) => Effect.Effect<OrchestrationThread, never>; |
| 190 | 197 | readonly waitForDomainEvent: ( |
| 191 | 198 | predicate: (event: OrchestrationEvent) => boolean, |
| 192 | − timeoutMs?: number, | |
| 199 | + timeout?: Duration.Input, | |
| 193 | 200 | ) => Effect.Effect<ReadonlyArray<OrchestrationEvent>, never>; |
| 194 | 201 | readonly waitForPendingApproval: ( |
| 195 | 202 | requestId: string, |
| @@ -198,7 +205,7 @@ export interface OrchestrationIntegrationHarness { · 2 unmodified lines | ||
| 198 | 205 | readonly decision: "accept" | "acceptForSession" | "decline" | "cancel" | null; |
| 199 | 206 | readonly resolvedAt: string | null; |
| 200 | 207 | }) => boolean, |
| 201 | − timeoutMs?: number, | |
| 208 | + timeout?: Duration.Input, | |
| 202 | 209 | ) => Effect.Effect< |
| 203 | 210 | { |
| 204 | 211 | readonly status: "pending" | "resolved"; |
| @@ -210,11 +217,11 @@ export interface OrchestrationIntegrationHarness { · 5 unmodified lines | ||
| 210 | 217 | readonly waitForReceipt: { |
| 211 | 218 | ( |
| 212 | 219 | predicate: (receipt: OrchestrationRuntimeReceipt) => boolean, |
| 213 | − timeoutMs?: number, | |
| 220 | + timeout?: Duration.Input, | |
| 214 | 221 | ): Effect.Effect<OrchestrationRuntimeReceipt, never>; |
| 215 | 222 | <Receipt extends OrchestrationRuntimeReceipt>( |
| 216 | 223 | predicate: (receipt: OrchestrationRuntimeReceipt) => receipt is Receipt, |
| 217 | − timeoutMs?: number, | |
| 224 | + timeout?: Duration.Input, | |
| 218 | 225 | ): Effect.Effect<Receipt, never>; |
| 219 | 226 | }; |
| 220 | 227 | readonly dispose: Effect.Effect<void, never>; |
| @@ -420,29 +427,29 @@ export const makeOrchestrationIntegrationHarness = ( · 199 unmodified lines | ||
| 420 | 427 | yield* Stream.runForEach(runtimeReceiptBus.streamEventsForTest, (receipt) => |
| 421 | 428 | Ref.update(receiptHistory, (history) => [...history, receipt]).pipe(Effect.asVoid), |
| 422 | 429 | ).pipe(Effect.forkIn(scope)); |
| 423 | − yield* Effect.sleep(10); | |
| 430 | + yield* Effect.sleep(Duration.millis(10)); | |
| 424 | 431 | |
| 425 | 432 | const waitForThread: OrchestrationIntegrationHarness["waitForThread"] = ( |
| 426 | 433 | threadId, |
| 427 | 434 | predicate, |
| 428 | − timeoutMs, | |
| 435 | + timeout, | |
| 429 | 436 | ) => |
| 430 | 437 | waitFor( |
| 431 | 438 | snapshotQuery |
| 432 | 439 | .getSnapshot() |
| 433 | 440 | .pipe( |
| 434 | − Effect.map( | |
| 435 | − (snapshot) => snapshot.threads.find((thread) => thread.id === threadId) ?? null, | |
| 441 | + Effect.map((snapshot) => | |
| 442 | + Option.fromUndefinedOr(snapshot.threads.find((thread) => thread.id === threadId)), | |
| 436 | 443 | ), |
| 437 | 444 | ), |
| 438 | − (thread): thread is OrchestrationThread => thread !== null && predicate(thread), | |
| 445 | + (thread) => Option.isSome(thread) && predicate(thread.value), | |
| 439 | 446 | `projected thread '${threadId}'`, |
| 440 | − timeoutMs, | |
| 441 | − ) as Effect.Effect<OrchestrationThread, never>; | |
| 447 | + timeout, | |
| 448 | + ).pipe(Effect.map(Option.getOrThrow)); | |
| 442 | 449 | |
| 443 | 450 | const waitForDomainEvent: OrchestrationIntegrationHarness["waitForDomainEvent"] = ( |
| 444 | 451 | predicate, |
| 445 | − timeoutMs, | |
| 452 | + timeout, | |
| 446 | 453 | ) => |
| 447 | 454 | waitFor( |
| 448 | 455 | Stream.runCollect(engine.readEvents(0)).pipe( |
| @@ -450,68 +457,49 @@ export const makeOrchestrationIntegrationHarness = ( · 1 unmodified line | ||
| 450 | 457 | ), |
| 451 | 458 | (events) => events.some(predicate), |
| 452 | 459 | "domain event", |
| 453 | − timeoutMs, | |
| 460 | + timeout, | |
| 454 | 461 | ); |
| 455 | 462 | |
| 456 | 463 | const waitForPendingApproval: OrchestrationIntegrationHarness["waitForPendingApproval"] = ( |
| 457 | 464 | requestId, |
| 458 | 465 | predicate, |
| 459 | − timeoutMs, | |
| 466 | + timeout, | |
| 460 | 467 | ) => |
| 461 | 468 | waitFor( |
| 462 | 469 | pendingApprovalRepository |
| 463 | 470 | .getByRequestId({ requestId: ApprovalRequestId.make(requestId) }) |
| 464 | 471 | .pipe( |
| 465 | − Effect.map((row) => | |
| 466 | − Option.match(row, { | |
| 467 | − onNone: () => null, | |
| 468 | − onSome: (value) => ({ | |
| 469 | − status: value.status, | |
| 470 | − decision: value.decision, | |
| 471 | − resolvedAt: value.resolvedAt, | |
| 472 | − }), | |
| 473 | − }), | |
| 472 | + Effect.map( | |
| 473 | + Option.map((value) => ({ | |
| 474 | + status: value.status, | |
| 475 | + decision: value.decision, | |
| 476 | + resolvedAt: value.resolvedAt, | |
| 477 | + })), | |
| 474 | 478 | ), |
| 475 | 479 | ), |
| 476 | − ( | |
| 477 | − row, | |
| 478 | − ): row is { | |
| 479 | − readonly status: "pending" | "resolved"; | |
| 480 | − readonly decision: "accept" | "acceptForSession" | "decline" | "cancel" | null; | |
| 481 | − readonly resolvedAt: string | null; | |
| 482 | − } => row !== null && predicate(row), | |
| 480 | + (row) => Option.isSome(row) && predicate(row.value), | |
| 483 | 481 | `pending approval '${requestId}'`, |
| 484 | − timeoutMs, | |
| 485 | − ) as Effect.Effect< | |
| 486 | − { | |
| 487 | − readonly status: "pending" | "resolved"; | |
| 488 | − readonly decision: "accept" | "acceptForSession" | "decline" | "cancel" | null; | |
| 489 | − readonly resolvedAt: string | null; | |
| 490 | − }, | |
| 491 | − never | |
| 492 | − >; | |
| 482 | + timeout, | |
| 483 | + ).pipe(Effect.map(Option.getOrThrow)); | |
| 493 | 484 | |
| 494 | 485 | function waitForReceipt( |
| 495 | 486 | predicate: (receipt: OrchestrationRuntimeReceipt) => boolean, |
| 496 | − timeoutMs?: number, | |
| 487 | + timeout?: Duration.Input, | |
| 497 | 488 | ): Effect.Effect<OrchestrationRuntimeReceipt, never>; |
| 498 | 489 | function waitForReceipt<Receipt extends OrchestrationRuntimeReceipt>( |
| 499 | 490 | predicate: (receipt: OrchestrationRuntimeReceipt) => receipt is Receipt, |
| 500 | − timeoutMs?: number, | |
| 491 | + timeout?: Duration.Input, | |
| 501 | 492 | ): Effect.Effect<Receipt, never>; |
| 502 | 493 | function waitForReceipt( |
| 503 | 494 | predicate: (receipt: OrchestrationRuntimeReceipt) => boolean, |
| 504 | − timeoutMs?: number, | |
| 495 | + timeout?: Duration.Input, | |
| 505 | 496 | ) { |
| 506 | 497 | const readMatchingReceipt = Ref.get(receiptHistory).pipe( |
| 507 | − Effect.map((history) => history.find(predicate)), | |
| 498 | + Effect.map((history) => Option.fromUndefinedOr(history.find(predicate))), | |
| 508 | 499 | ); |
| 509 | 500 | |
| 510 | − return waitFor( | |
| 511 | − readMatchingReceipt, | |
| 512 | − (receipt): receipt is OrchestrationRuntimeReceipt => receipt !== undefined, | |
| 513 | − "runtime receipt", | |
| 514 | − timeoutMs, | |
| 501 | + return waitFor(readMatchingReceipt, Option.isSome, "runtime receipt", timeout).pipe( | |
| 502 | + Effect.map(Option.getOrThrow), | |
| 515 | 503 | ); |
| 516 | 504 | } |
| 517 | 505 | |
| @@ -1,4 +1,4 @@ | ||
| 1 | −import { describe, expect, it } from "@effect/vitest"; | |
| 1 | +import { assert, describe, it } from "@effect/vitest"; | |
| 2 | 2 | import { vi } from "vite-plus/test"; |
| 3 | 3 | import * as Deferred from "effect/Deferred"; |
| 4 | 4 | import * as Effect from "effect/Effect"; |
| @@ -81,21 +81,24 @@ function makeHandle(input: { · 76 unmodified lines | ||
| 81 | 81 | |
| 82 | 82 | describe("CloudManagedEndpointRuntime", () => { |
| 83 | 83 | it("classifies Cloudflare connection and warning output", () => { |
| 84 | − expect( | |
| 84 | + assert.equal( | |
| 85 | 85 | ManagedEndpointRuntime.classifyRelayClientOutput( |
| 86 | 86 | "2026-06-17T02:00:00Z INF Registered tunnel connection connIndex=0", |
| 87 | 87 | ), |
| 88 | − ).toBe("connected"); | |
| 89 | − expect( | |
| 88 | + "connected", | |
| 89 | + ); | |
| 90 | + assert.equal( | |
| 90 | 91 | ManagedEndpointRuntime.classifyRelayClientOutput( |
| 91 | 92 | "2026-06-17T02:00:00Z ERR Failed to serve tunnel connection", |
| 92 | 93 | ), |
| 93 | − ).toBe("warning"); | |
| 94 | − expect( | |
| 94 | + "warning", | |
| 95 | + ); | |
| 96 | + assert.equal( | |
| 95 | 97 | ManagedEndpointRuntime.classifyRelayClientOutput( |
| 96 | 98 | "2026-06-17T02:00:00Z INF Starting metrics server", |
| 97 | 99 | ), |
| 98 | − ).toBe("debug"); | |
| 100 | + "debug", | |
| 101 | + ); | |
| 99 | 102 | }); |
| 100 | 103 | |
| 101 | 104 | it.effect("starts, deduplicates, rotates, and stops the Cloudflare connector", () => |
| @@ -123,41 +126,65 @@ describe("CloudManagedEndpointRuntime", () => { · 21 unmodified lines | ||
| 123 | 126 | ); |
| 124 | 127 | const runtime = yield* buildCloudManagedEndpointRuntime(spawner); |
| 125 | 128 | |
| 126 | − yield* runtime.applyConfig({ | |
| 127 | − providerKind: "cloudflare_tunnel", | |
| 128 | − connectorToken: "token-1", | |
| 129 | − tunnelId: "tunnel-1", | |
| 130 | − tunnelName: "t3-code-env-1", | |
| 131 | − }); | |
| 132 | − yield* runtime.applyConfig({ | |
| 133 | − providerKind: "cloudflare_tunnel", | |
| 134 | − connectorToken: "token-1", | |
| 135 | − tunnelId: "tunnel-1", | |
| 136 | − tunnelName: "t3-code-env-1", | |
| 137 | − }); | |
| 138 | − yield* runtime.applyConfig({ | |
| 139 | − providerKind: "cloudflare_tunnel", | |
| 140 | − connectorToken: "token-2", | |
| 141 | − tunnelId: "tunnel-1", | |
| 142 | − tunnelName: "t3-code-env-1", | |
| 143 | − }); | |
| 144 | − const stopped = yield* runtime.applyConfig(null); | |
| 129 | + yield* runtime.applyConfig( | |
| 130 | + Option.some({ | |
| 131 | + providerKind: "cloudflare_tunnel", | |
| 132 | + connectorToken: "token-1", | |
| 133 | + tunnelId: "tunnel-1", | |
| 134 | + tunnelName: "t3-code-env-1", | |
| 135 | + }), | |
| 136 | + ); | |
| 137 | + yield* runtime.applyConfig( | |
| 138 | + Option.some({ | |
| 139 | + providerKind: "cloudflare_tunnel", | |
| 140 | + connectorToken: "token-1", | |
| 141 | + tunnelId: "tunnel-1", | |
| 142 | + tunnelName: "t3-code-env-1", | |
| 143 | + }), | |
| 144 | + ); | |
| 145 | + yield* runtime.applyConfig( | |
| 146 | + Option.some({ | |
| 147 | + providerKind: "cloudflare_tunnel", | |
| 148 | + connectorToken: "token-2", | |
| 149 | + tunnelId: "tunnel-1", | |
| 150 | + tunnelName: "t3-code-env-1", | |
| 151 | + }), | |
| 152 | + ); | |
| 153 | + const stopped = yield* runtime.applyConfig(Option.none()); | |
| 145 | 154 | |
| 146 | − expect(spawned.map((command) => command.command)).toEqual(["cloudflared", "cloudflared"]); | |
| 147 | − expect(spawned.map((command) => command.args)).toEqual([ | |
| 148 | − ["tunnel", "run"], | |
| 149 | − ["tunnel", "run"], | |
| 150 | − ]); | |
| 151 | − expect(spawned.map((command) => command.options.env?.TUNNEL_TOKEN)).toEqual([ | |
| 152 | − "token-1", | |
| 153 | − "token-2", | |
| 154 | − ]); | |
| 155 | − expect(spawned.map((command) => command.options.stdout)).toEqual(["pipe", "pipe"]); | |
| 156 | − expect(spawned.map((command) => command.options.stderr)).toEqual(["pipe", "pipe"]); | |
| 157 | − expect(spawned.map((command) => command.options.detached)).toEqual([false, false]); | |
| 158 | − expect(spawned.map((command) => command.options.shell)).toEqual([false, false]); | |
| 159 | − expect(killed).toEqual([100, 101]); | |
| 160 | − expect(stopped).toEqual({ status: "disabled" }); | |
| 155 | + assert.deepEqual( | |
| 156 | + spawned.map((command) => command.command), | |
| 157 | + ["cloudflared", "cloudflared"], | |
| 158 | + ); | |
| 159 | + assert.deepEqual( | |
| 160 | + spawned.map((command) => command.args), | |
| 161 | + [ | |
| 162 | + ["tunnel", "run"], | |
| 163 | + ["tunnel", "run"], | |
| 164 | + ], | |
| 165 | + ); | |
| 166 | + assert.deepEqual( | |
| 167 | + spawned.map((command) => command.options.env?.TUNNEL_TOKEN), | |
| 168 | + ["token-1", "token-2"], | |
| 169 | + ); | |
| 170 | + assert.deepEqual( | |
| 171 | + spawned.map((command) => command.options.stdout), | |
| 172 | + ["pipe", "pipe"], | |
| 173 | + ); | |
| 174 | + assert.deepEqual( | |
| 175 | + spawned.map((command) => command.options.stderr), | |
| 176 | + ["pipe", "pipe"], | |
| 177 | + ); | |
| 178 | + assert.deepEqual( | |
| 179 | + spawned.map((command) => command.options.detached), | |
| 180 | + [false, false], | |
| 181 | + ); | |
| 182 | + assert.deepEqual( | |
| 183 | + spawned.map((command) => command.options.shell), | |
| 184 | + [false, false], | |
| 185 | + ); | |
| 186 | + assert.deepEqual(killed, [100, 101]); | |
| 187 | + assert.deepEqual(stopped, { status: "disabled" }); | |
| 161 | 188 | }), |
| 162 | 189 | ); |
| 163 | 190 | |
| @@ -178,18 +205,22 @@ describe("CloudManagedEndpointRuntime", () => { · 14 unmodified lines | ||
| 178 | 205 | ); |
| 179 | 206 | const runtime = yield* buildCloudManagedEndpointRuntime(spawner); |
| 180 | 207 | |
| 181 | − const started = yield* runtime.applyConfig({ | |
| 182 | − providerKind: "cloudflare_tunnel", | |
| 183 | − connectorToken: "token", | |
| 184 | − }); | |
| 185 | − const unsupported = yield* runtime.applyConfig({ | |
| 186 | − providerKind: "manual", | |
| 187 | − connectorToken: "manual-token", | |
| 188 | − }); | |
| 208 | + const started = yield* runtime.applyConfig( | |
| 209 | + Option.some({ | |
| 210 | + providerKind: "cloudflare_tunnel", | |
| 211 | + connectorToken: "token", | |
| 212 | + }), | |
| 213 | + ); | |
| 214 | + const unsupported = yield* runtime.applyConfig( | |
| 215 | + Option.some({ | |
| 216 | + providerKind: "manual", | |
| 217 | + connectorToken: "manual-token", | |
| 218 | + }), | |
| 219 | + ); | |
| 189 | 220 | |
| 190 | − expect(started.status).toBe("running"); | |
| 191 | − expect(unsupported).toEqual({ status: "unsupported", providerKind: "manual" }); | |
| 192 | − expect(killed).toEqual([200]); | |
| 221 | + assert.equal(started.status, "running"); | |
| 222 | + assert.deepEqual(unsupported, { status: "unsupported", providerKind: "manual" }); | |
| 223 | + assert.deepEqual(killed, [200]); | |
| 193 | 224 | }), |
| 194 | 225 | ); |
| 195 | 226 | |
| @@ -220,14 +251,20 @@ describe("CloudManagedEndpointRuntime", () => { · 24 unmodified lines | ||
| 220 | 251 | tunnelId: "tunnel-1", |
| 221 | 252 | }; |
| 222 | 253 | |
| 223 | − const first = yield* runtime.applyConfig(config); | |
| 254 | + const first = yield* runtime.applyConfig(Option.some(config)); | |
| 224 | 255 | firstRunning = false; |
| 225 | − const second = yield* runtime.applyConfig(config); | |
| 256 | + const second = yield* runtime.applyConfig(Option.some(config)); | |
| 226 | 257 | |
| 227 | − expect(first).toMatchObject({ status: "running", pid: 300 }); | |
| 228 | − expect(second).toMatchObject({ status: "running", pid: 301 }); | |
| 229 | − expect(spawned).toEqual([300, 301]); | |
| 230 | − expect(killed).toEqual([300]); | |
| 258 | + if (first.status !== "running") { | |
| 259 | + assert.fail(`Expected first connector to be running, got ${first.status}`); | |
| 260 | + } | |
| 261 | + assert.equal(first.pid, 300); | |
| 262 | + if (second.status !== "running") { | |
| 263 | + assert.fail(`Expected second connector to be running, got ${second.status}`); | |
| 264 | + } | |
| 265 | + assert.equal(second.pid, 301); | |
| 266 | + assert.deepEqual(spawned, [300, 301]); | |
| 267 | + assert.deepEqual(killed, [300]); | |
| 231 | 268 | }), |
| 232 | 269 | ); |
| 233 | 270 | |
| @@ -260,17 +297,22 @@ describe("CloudManagedEndpointRuntime", () => { · 26 unmodified lines | ||
| 260 | 297 | ); |
| 261 | 298 | const runtime = yield* buildCloudManagedEndpointRuntime(spawner); |
| 262 | 299 | |
| 263 | − const started = yield* runtime.applyConfig({ | |
| 264 | − providerKind: "cloudflare_tunnel", | |
| 265 | − connectorToken: "token", | |
| 266 | − tunnelId: "tunnel-1", | |
| 267 | − }); | |
| 300 | + const started = yield* runtime.applyConfig( | |
| 301 | + Option.some({ | |
| 302 | + providerKind: "cloudflare_tunnel", | |
| 303 | + connectorToken: "token", | |
| 304 | + tunnelId: "tunnel-1", | |
| 305 | + }), | |
| 306 | + ); | |
| 268 | 307 | yield* Deferred.succeed(firstExit, ChildProcessSpawner.ExitCode(1)); |
| 269 | 308 | yield* Deferred.await(secondSpawned); |
| 270 | 309 | |
| 271 | − expect(started).toMatchObject({ status: "running", pid: 400 }); | |
| 272 | − expect(spawned).toEqual([400, 401]); | |
| 273 | − expect(killed).toEqual([400]); | |
| 310 | + if (started.status !== "running") { | |
| 311 | + assert.fail(`Expected connector to be running, got ${started.status}`); | |
| 312 | + } | |
| 313 | + assert.equal(started.pid, 400); | |
| 314 | + assert.deepEqual(spawned, [400, 401]); | |
| 315 | + assert.deepEqual(killed, [400]); | |
| 274 | 316 | }), |
| 275 | 317 | ); |
| 276 | 318 | |
| @@ -301,26 +343,33 @@ describe("CloudManagedEndpointRuntime", () => { · 24 unmodified lines | ||
| 301 | 343 | const runtime = yield* buildCloudManagedEndpointRuntime(spawner); |
| 302 | 344 | |
| 303 | 345 | const first = yield* runtime |
| 304 | − .applyConfig({ | |
| 305 | − providerKind: "cloudflare_tunnel", | |
| 306 | − connectorToken: "token-1", | |
| 307 | − }) | |
| 346 | + .applyConfig( | |
| 347 | + Option.some({ | |
| 348 | + providerKind: "cloudflare_tunnel", | |
| 349 | + connectorToken: "token-1", | |
| 350 | + }), | |
| 351 | + ) | |
| 308 | 352 | .pipe(Effect.forkChild); |
| 309 | 353 | yield* Deferred.await(firstSpawnEntered); |
| 310 | 354 | const second = yield* runtime |
| 311 | − .applyConfig({ | |
| 312 | − providerKind: "cloudflare_tunnel", | |
| 313 | − connectorToken: "token-2", | |
| 314 | − }) | |
| 355 | + .applyConfig( | |
| 356 | + Option.some({ | |
| 357 | + providerKind: "cloudflare_tunnel", | |
| 358 | + connectorToken: "token-2", | |
| 359 | + }), | |
| 360 | + ) | |
| 315 | 361 | .pipe(Effect.forkChild); |
| 316 | 362 | yield* Deferred.succeed(releaseFirstSpawn, undefined); |
| 317 | 363 | |
| 318 | 364 | yield* Fiber.join(first); |
| 319 | 365 | const status = yield* Fiber.join(second); |
| 320 | 366 | |
| 321 | − expect(status).toMatchObject({ status: "running", pid: 501 }); | |
| 322 | − expect(spawned).toEqual([500, 501]); | |
| 323 | − expect(killed).toEqual([500]); | |
| 367 | + if (status.status !== "running") { | |
| 368 | + assert.fail(`Expected connector to be running, got ${status.status}`); | |
| 369 | + } | |
| 370 | + assert.equal(status.pid, 501); | |
| 371 | + assert.deepEqual(spawned, [500, 501]); | |
| 372 | + assert.deepEqual(killed, [500]); | |
| 324 | 373 | }), |
| 325 | 374 | ); |
| 326 | 375 | |
| @@ -338,17 +387,19 @@ describe("CloudManagedEndpointRuntime", () => { · 11 unmodified lines | ||
| 338 | 387 | ); |
| 339 | 388 | const runtime = yield* buildCloudManagedEndpointRuntime(spawner); |
| 340 | 389 | |
| 341 | − const status = yield* runtime.applyConfig({ | |
| 342 | − providerKind: "cloudflare_tunnel", | |
| 343 | − connectorToken: "token", | |
| 344 | − tunnelId: "tunnel-1", | |
| 345 | − }); | |
| 390 | + const status = yield* runtime.applyConfig( | |
| 391 | + Option.some({ | |
| 392 | + providerKind: "cloudflare_tunnel", | |
| 393 | + connectorToken: "token", | |
| 394 | + tunnelId: "tunnel-1", | |
| 395 | + }), | |
| 396 | + ); | |
| 346 | 397 | |
| 347 | − expect(status).toMatchObject({ | |
| 348 | − status: "failed", | |
| 349 | − providerKind: "cloudflare_tunnel", | |
| 350 | − tunnelId: "tunnel-1", | |
| 351 | − }); | |
| 398 | + if (status.status !== "failed") { | |
| 399 | + assert.fail(`Expected connector spawn to fail, got ${status.status}`); | |
| 400 | + } | |
| 401 | + assert.equal(status.providerKind, "cloudflare_tunnel"); | |
| 402 | + assert.equal(status.tunnelId, "tunnel-1"); | |
| 352 | 403 | }), |
| 353 | 404 | ); |
| 354 | 405 | |
| @@ -371,17 +422,19 @@ describe("CloudManagedEndpointRuntime", () => { · 16 unmodified lines | ||
| 371 | 422 | ), |
| 372 | 423 | ); |
| 373 | 424 | |
| 374 | − const status = yield* runtime.applyConfig({ | |
| 375 | − providerKind: "cloudflare_tunnel", | |
| 376 | − connectorToken: "token", | |
| 377 | − }); | |
| 425 | + const status = yield* runtime.applyConfig( | |
| 426 | + Option.some({ | |
| 427 | + providerKind: "cloudflare_tunnel", | |
| 428 | + connectorToken: "token", | |
| 429 | + }), | |
| 430 | + ); | |
| 378 | 431 | |
| 379 | − expect(status).toEqual({ | |
| 432 | + assert.deepEqual(status, { | |
| 380 | 433 | status: "failed", |
| 381 | 434 | providerKind: "cloudflare_tunnel", |
| 382 | 435 | reason: "The relay client is not installed.", |
| 383 | 436 | }); |
| 384 | − expect(spawn).not.toHaveBeenCalled(); | |
| 437 | + assert.equal(spawn.mock.calls.length, 0); | |
| 385 | 438 | }), |
| 386 | 439 | ); |
| 387 | 440 | }); |
| @@ -24,9 +24,9 @@ const readRuntimeConfig = Effect.gen(function* () { · 23 unmodified lines | ||
| 24 | 24 | const secrets = yield* ServerSecretStore.ServerSecretStore; |
| 25 | 25 | const bytes = yield* secrets.get(CLOUD_ENDPOINT_RUNTIME_CONFIG); |
| 26 | 26 | if (Option.isNone(bytes)) { |
| 27 | − return null; | |
| 27 | + return Option.none<RelayManagedEndpointRuntimeConfig>(); | |
| 28 | 28 | } |
| 29 | − return Option.getOrNull(decodeRuntimeConfig(bytesToString(bytes.value))); | |
| 29 | + return decodeRuntimeConfig(bytesToString(bytes.value)); | |
| 30 | 30 | }); |
| 31 | 31 | |
| 32 | 32 | export type CloudManagedEndpointRuntimeStatus = |
| @@ -56,7 +56,7 @@ export class CloudManagedEndpointRuntime extends Context.Service< · 23 unmodified lines | ||
| 56 | 56 | CloudManagedEndpointRuntime, |
| 57 | 57 | { |
| 58 | 58 | readonly applyConfig: ( |
| 59 | − config: RelayManagedEndpointRuntimeConfig | null, | |
| 59 | + config: Option.Option<RelayManagedEndpointRuntimeConfig>, | |
| 60 | 60 | ) => Effect.Effect<CloudManagedEndpointRuntimeStatus>; |
| 61 | 61 | } |
| 62 | 62 | >()("t3/cloud/ManagedEndpointRuntime/CloudManagedEndpointRuntime") {} |
| @@ -84,28 +84,32 @@ function runtimeConfigKey(config: RelayManagedEndpointRuntimeConfig): string { · 21 unmodified lines | ||
| 84 | 84 | }); |
| 85 | 85 | } |
| 86 | 86 | |
| 87 | −const stopConnector = (connector: ActiveConnector | null) => | |
| 88 | − connector | |
| 89 | − ? Scope.close(connector.scope, Exit.void).pipe( | |
| 87 | +const stopConnector = (connector: Option.Option<ActiveConnector>) => | |
| 88 | + Option.match(connector, { | |
| 89 | + onNone: () => Effect.void, | |
| 90 | + onSome: (active) => | |
| 91 | + Scope.close(active.scope, Exit.void).pipe( | |
| 90 | 92 | Effect.tap(() => |
| 91 | 93 | Effect.logInfo("Relay client stopped", { |
| 92 | − pid: Number(connector.child.pid), | |
| 94 | + pid: Number(active.child.pid), | |
| 93 | 95 | }), |
| 94 | 96 | ), |
| 95 | 97 | Effect.ignore, |
| 96 | − ) | |
| 97 | − : Effect.void; | |
| 98 | + ), | |
| 99 | + }); | |
| 98 | 100 | |
| 99 | 101 | export const make = Effect.gen(function* () { |
| 100 | 102 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 101 | 103 | const relayClient = yield* RelayClient.RelayClient; |
| 102 | − const activeRef = yield* Ref.make<ActiveConnector | null>(null); | |
| 103 | − const desiredConfigRef = yield* Ref.make<RelayManagedEndpointRuntimeConfig | null>(null); | |
| 104 | + const activeRef = yield* Ref.make<Option.Option<ActiveConnector>>(Option.none()); | |
| 105 | + const desiredConfigRef = yield* Ref.make<Option.Option<RelayManagedEndpointRuntimeConfig>>( | |
| 106 | + Option.none(), | |
| 107 | + ); | |
| 104 | 108 | const reconcileSemaphore = yield* Semaphore.make(1); |
| 105 | 109 | let reconcileConfig: CloudManagedEndpointRuntime["Service"]["applyConfig"]; |
| 106 | 110 | |
| 107 | 111 | const stopActive = Effect.gen(function* () { |
| 108 | − const active = yield* Ref.getAndSet(activeRef, null); | |
| 112 | + const active = yield* Ref.getAndSet(activeRef, Option.none()); | |
| 109 | 113 | yield* stopConnector(active); |
| 110 | 114 | }); |
| 111 | 115 | |
| @@ -116,19 +120,20 @@ export const make = Effect.gen(function* () { · 4 unmodified lines | ||
| 116 | 120 | Effect.gen(function* () { |
| 117 | 121 | const active = yield* Ref.get(activeRef); |
| 118 | 122 | if ( |
| 119 | − active?.child.pid !== connector.child.pid || | |
| 120 | − active.configKey !== connector.configKey | |
| 123 | + Option.isNone(active) || | |
| 124 | + active.value.child.pid !== connector.child.pid || | |
| 125 | + active.value.configKey !== connector.configKey | |
| 121 | 126 | ) { |
| 122 | 127 | return; |
| 123 | 128 | } |
| 124 | − yield* Ref.set(activeRef, null); | |
| 125 | − yield* stopConnector(connector); | |
| 129 | + yield* Ref.set(activeRef, Option.none()); | |
| 130 | + yield* stopConnector(Option.some(connector)); | |
| 126 | 131 | |
| 127 | 132 | const desiredConfig = yield* Ref.get(desiredConfigRef); |
| 128 | 133 | if ( |
| 129 | − !desiredConfig || | |
| 130 | − desiredConfig.providerKind !== "cloudflare_tunnel" || | |
| 131 | − runtimeConfigKey(desiredConfig) !== connector.configKey | |
| 134 | + Option.isNone(desiredConfig) || | |
| 135 | + desiredConfig.value.providerKind !== "cloudflare_tunnel" || | |
| 136 | + runtimeConfigKey(desiredConfig.value) !== connector.configKey | |
| 132 | 137 | ) { |
| 133 | 138 | return; |
| 134 | 139 | } |
| @@ -181,120 +186,130 @@ export const make = Effect.gen(function* () { · 46 unmodified lines | ||
| 181 | 186 | ), |
| 182 | 187 | ); |
| 183 | 188 | |
| 184 | − reconcileConfig = Effect.fn("CloudManagedEndpointRuntime.reconcileConfig")(function* (config) { | |
| 185 | − if (!config || config.providerKind !== "cloudflare_tunnel") { | |
| 189 | + reconcileConfig = Effect.fn("CloudManagedEndpointRuntime.reconcileConfig")( | |
| 190 | + function* (configOption) { | |
| 191 | + if (Option.isNone(configOption)) { | |
| 192 | + yield* stopActive; | |
| 193 | + return { status: "disabled" }; | |
| 194 | + } | |
| 195 | + | |
| 196 | + const config = configOption.value; | |
| 197 | + if (config.providerKind !== "cloudflare_tunnel") { | |
| 198 | + yield* stopActive; | |
| 199 | + return { status: "unsupported", providerKind: config.providerKind }; | |
| 200 | + } | |
| 201 | + | |
| 202 | + const nextConfigKey = runtimeConfigKey(config); | |
| 203 | + const active = yield* Ref.get(activeRef); | |
| 204 | + if (Option.isSome(active) && active.value.configKey === nextConfigKey) { | |
| 205 | + const isRunning = yield* active.value.child.isRunning.pipe( | |
| 206 | + Effect.orElseSucceed(() => false), | |
| 207 | + ); | |
| 208 | + if (isRunning) { | |
| 209 | + return { | |
| 210 | + status: "running", | |
| 211 | + providerKind: "cloudflare_tunnel", | |
| 212 | + pid: Number(active.value.child.pid), | |
| 213 | + ...(active.value.config.tunnelId ? { tunnelId: active.value.config.tunnelId } : {}), | |
| 214 | + ...(active.value.config.tunnelName | |
| 215 | + ? { tunnelName: active.value.config.tunnelName } | |
| 216 | + : {}), | |
| 217 | + } satisfies CloudManagedEndpointRuntimeStatus; | |
| 218 | + } | |
| 219 | + } | |
| 220 | + | |
| 186 | 221 | yield* stopActive; |
| 187 | − return config | |
| 188 | − ? { status: "unsupported", providerKind: config.providerKind } | |
| 189 | − : { status: "disabled" }; | |
| 190 | − } | |
| 191 | 222 | |
| 192 | − const nextConfigKey = runtimeConfigKey(config); | |
| 193 | − const active = yield* Ref.get(activeRef); | |
| 194 | − if (active?.configKey === nextConfigKey) { | |
| 195 | − const isRunning = yield* active.child.isRunning.pipe(Effect.orElseSucceed(() => false)); | |
| 196 | − if (isRunning) { | |
| 223 | + const executable = yield* relayClient.resolve; | |
| 224 | + if (executable.status !== "available") { | |
| 197 | 225 | return { |
| 198 | − status: "running", | |
| 226 | + status: "failed", | |
| 199 | 227 | providerKind: "cloudflare_tunnel", |
| 200 | − pid: Number(active.child.pid), | |
| 201 | − ...(active.config.tunnelId ? { tunnelId: active.config.tunnelId } : {}), | |
| 202 | − ...(active.config.tunnelName ? { tunnelName: active.config.tunnelName } : {}), | |
| 228 | + reason: | |
| 229 | + executable.status === "unsupported" | |
| 230 | + ? `Relay client is unsupported on ${executable.platform}-${executable.arch}.` | |
| 231 | + : "The relay client is not installed.", | |
| 232 | + ...(config.tunnelId ? { tunnelId: config.tunnelId } : {}), | |
| 233 | + ...(config.tunnelName ? { tunnelName: config.tunnelName } : {}), | |
| 203 | 234 | } satisfies CloudManagedEndpointRuntimeStatus; |
| 204 | 235 | } |
| 205 | − } | |
| 206 | 236 | |
| 207 | − yield* stopActive; | |
| 208 | − | |
| 209 | − const executable = yield* relayClient.resolve; | |
| 210 | − if (executable.status !== "available") { | |
| 211 | − return { | |
| 212 | − status: "failed", | |
| 213 | − providerKind: "cloudflare_tunnel", | |
| 214 | − reason: | |
| 215 | − executable.status === "unsupported" | |
| 216 | − ? `Relay client is unsupported on ${executable.platform}-${executable.arch}.` | |
| 217 | − : "The relay client is not installed.", | |
| 218 | − ...(config.tunnelId ? { tunnelId: config.tunnelId } : {}), | |
| 219 | − ...(config.tunnelName ? { tunnelName: config.tunnelName } : {}), | |
| 220 | − } satisfies CloudManagedEndpointRuntimeStatus; | |
| 221 | − } | |
| 222 | − | |
| 223 | − const connectorScope = yield* Scope.make("sequential"); | |
| 224 | − const child = yield* spawner | |
| 225 | − .spawn( | |
| 226 | − ChildProcess.make(executable.executablePath, ["tunnel", "run"], { | |
| 227 | − detached: false, | |
| 228 | − env: { | |
| 229 | − ...process.env, | |
| 230 | − TUNNEL_TOKEN: config.connectorToken, | |
| 231 | − }, | |
| 232 | − shell: false, | |
| 233 | − stderr: "pipe", | |
| 234 | − stdout: "pipe", | |
| 235 | − }), | |
| 236 | − ) | |
| 237 | − .pipe( | |
| 238 | − Effect.provideService(Scope.Scope, connectorScope), | |
| 239 | − Effect.tap((child) => | |
| 240 | − Effect.logInfo("Relay client process started; waiting for tunnel connection", { | |
| 241 | − pid: Number(child.pid), | |
| 242 | − tunnelId: config.tunnelId, | |
| 243 | − tunnelName: config.tunnelName, | |
| 237 | + const connectorScope = yield* Scope.make("sequential"); | |
| 238 | + const child = yield* spawner | |
| 239 | + .spawn( | |
| 240 | + ChildProcess.make(executable.executablePath, ["tunnel", "run"], { | |
| 241 | + detached: false, | |
| 242 | + env: { | |
| 243 | + ...process.env, | |
| 244 | + TUNNEL_TOKEN: config.connectorToken, | |
| 245 | + }, | |
| 246 | + shell: false, | |
| 247 | + stderr: "pipe", | |
| 248 | + stdout: "pipe", | |
| 244 | 249 | }), |
| 245 | − ), | |
| 246 | − Effect.catch((cause) => | |
| 247 | − Effect.logWarning("Failed to start relay client", { | |
| 248 | − cause, | |
| 249 | − tunnelId: config.tunnelId, | |
| 250 | − tunnelName: config.tunnelName, | |
| 251 | − }).pipe( | |
| 252 | − Effect.andThen(Scope.close(connectorScope, Exit.void).pipe(Effect.ignore)), | |
| 253 | − Effect.as({ | |
| 254 | − status: "failed", | |
| 255 | − providerKind: "cloudflare_tunnel", | |
| 256 | − reason: String(cause), | |
| 257 | − ...(config.tunnelId ? { tunnelId: config.tunnelId } : {}), | |
| 258 | − ...(config.tunnelName ? { tunnelName: config.tunnelName } : {}), | |
| 259 | − } satisfies CloudManagedEndpointRuntimeStatus), | |
| 250 | + ) | |
| 251 | + .pipe( | |
| 252 | + Effect.provideService(Scope.Scope, connectorScope), | |
| 253 | + Effect.tap((child) => | |
| 254 | + Effect.logInfo("Relay client process started; waiting for tunnel connection", { | |
| 255 | + pid: Number(child.pid), | |
| 256 | + tunnelId: config.tunnelId, | |
| 257 | + tunnelName: config.tunnelName, | |
| 258 | + }), | |
| 260 | 259 | ), |
| 261 | − ), | |
| 262 | − ); | |
| 260 | + Effect.catch((cause) => | |
| 261 | + Effect.logWarning("Failed to start relay client", { | |
| 262 | + cause, | |
| 263 | + tunnelId: config.tunnelId, | |
| 264 | + tunnelName: config.tunnelName, | |
| 265 | + }).pipe( | |
| 266 | + Effect.andThen(Scope.close(connectorScope, Exit.void).pipe(Effect.ignore)), | |
| 267 | + Effect.as({ | |
| 268 | + status: "failed", | |
| 269 | + providerKind: "cloudflare_tunnel", | |
| 270 | + reason: String(cause), | |
| 271 | + ...(config.tunnelId ? { tunnelId: config.tunnelId } : {}), | |
| 272 | + ...(config.tunnelName ? { tunnelName: config.tunnelName } : {}), | |
| 273 | + } satisfies CloudManagedEndpointRuntimeStatus), | |
| 274 | + ), | |
| 275 | + ), | |
| 276 | + ); | |
| 263 | 277 | |
| 264 | − if ("status" in child && child.status === "failed") { | |
| 265 | − return child; | |
| 266 | − } | |
| 278 | + if ("status" in child && child.status === "failed") { | |
| 279 | + return child; | |
| 280 | + } | |
| 281 | + | |
| 282 | + if (!("status" in child)) { | |
| 283 | + const connector = { | |
| 284 | + child, | |
| 285 | + scope: connectorScope, | |
| 286 | + configKey: nextConfigKey, | |
| 287 | + config, | |
| 288 | + } satisfies ActiveConnector; | |
| 289 | + yield* Ref.set(activeRef, Option.some(connector)); | |
| 290 | + yield* Effect.forkIn(observeConnectorOutput(connector), connectorScope); | |
| 291 | + yield* Effect.forkIn(superviseConnector(connector), connectorScope); | |
| 292 | + return { | |
| 293 | + status: "running", | |
| 294 | + providerKind: "cloudflare_tunnel", | |
| 295 | + pid: Number(child.pid), | |
| 296 | + ...(config.tunnelId ? { tunnelId: config.tunnelId } : {}), | |
| 297 | + ...(config.tunnelName ? { tunnelName: config.tunnelName } : {}), | |
| 298 | + } satisfies CloudManagedEndpointRuntimeStatus; | |
| 299 | + } | |
| 267 | 300 | |
| 268 | − if (!("status" in child)) { | |
| 269 | − const connector = { | |
| 270 | − child, | |
| 271 | − scope: connectorScope, | |
| 272 | − configKey: nextConfigKey, | |
| 273 | − config, | |
| 274 | − } satisfies ActiveConnector; | |
| 275 | − yield* Ref.set(activeRef, connector); | |
| 276 | − yield* Effect.forkIn(observeConnectorOutput(connector), connectorScope); | |
| 277 | − yield* Effect.forkIn(superviseConnector(connector), connectorScope); | |
| 278 | 301 | return { |
| 279 | − status: "running", | |
| 302 | + status: "failed", | |
| 280 | 303 | providerKind: "cloudflare_tunnel", |
| 281 | − pid: Number(child.pid), | |
| 304 | + reason: "Relay client did not start.", | |
| 282 | 305 | ...(config.tunnelId ? { tunnelId: config.tunnelId } : {}), |
| 283 | 306 | ...(config.tunnelName ? { tunnelName: config.tunnelName } : {}), |
| 284 | 307 | } satisfies CloudManagedEndpointRuntimeStatus; |
| 285 | − } | |
| 286 | − | |
| 287 | − return { | |
| 288 | − status: "failed", | |
| 289 | − providerKind: "cloudflare_tunnel", | |
| 290 | − reason: "Relay client did not start.", | |
| 291 | − ...(config.tunnelId ? { tunnelId: config.tunnelId } : {}), | |
| 292 | − ...(config.tunnelName ? { tunnelName: config.tunnelName } : {}), | |
| 293 | − } satisfies CloudManagedEndpointRuntimeStatus; | |
| 294 | − }); | |
| 308 | + }, | |
| 309 | + ); | |
| 295 | 310 | |
| 296 | 311 | const applyConfig = Effect.fn("CloudManagedEndpointRuntime.applyConfig")( |
| 297 | − (config: RelayManagedEndpointRuntimeConfig | null) => | |
| 312 | + (config: Option.Option<RelayManagedEndpointRuntimeConfig>) => | |
| 298 | 313 | reconcileSemaphore.withPermits(1)( |
| 299 | 314 | Ref.set(desiredConfigRef, config).pipe(Effect.andThen(reconcileConfig(config))), |
| 300 | 315 | ), |
| @@ -307,12 +322,12 @@ export const make = Effect.gen(function* () { · 6 unmodified lines | ||
| 307 | 322 | const initialConfig = yield* readRuntimeConfig.pipe( |
| 308 | 323 | Effect.catch((cause) => |
| 309 | 324 | Effect.logWarning("Failed to read managed endpoint runtime config", { cause }).pipe( |
| 310 | − Effect.as(null), | |
| 325 | + Effect.as(Option.none<RelayManagedEndpointRuntimeConfig>()), | |
| 311 | 326 | ), |
| 312 | 327 | ), |
| 313 | 328 | ); |
| 314 | 329 | yield* runtime.applyConfig(initialConfig); |
| 315 | − yield* Effect.addFinalizer(() => runtime.applyConfig(null)); | |
| 330 | + yield* Effect.addFinalizer(() => runtime.applyConfig(Option.none())); | |
| 316 | 331 | return runtime; |
| 317 | 332 | }); |
| 318 | 333 | |
| @@ -433,7 +433,7 @@ const applyCloudRelayConfig = Effect.fn("environment.cloud.applyRelayConfig")(fu · 432 unmodified lines | ||
| 433 | 433 | }); |
| 434 | 434 | yield* validateCloudMintPublicKey(payload.cloudMintPublicKey); |
| 435 | 435 | const endpointRuntimeStatus = yield* dependencies.endpointRuntime.applyConfig( |
| 436 | − payload.endpointRuntime, | |
| 436 | + Option.fromNullOr(payload.endpointRuntime), | |
| 437 | 437 | ); |
| 438 | 438 | const ok = |
| 439 | 439 | endpointRuntimeStatus.status === "disabled" || endpointRuntimeStatus.status === "running"; |
| @@ -642,7 +642,7 @@ const cloudLinkStateHandler = Effect.fn("environment.cloud.linkState")( · 202 unmodified lines | ||
| 642 | 642 | const cloudUnlinkHandler = Effect.fn("environment.cloud.unlink")( |
| 643 | 643 | function* (dependencies: CloudHttpDependencies) { |
| 644 | 644 | yield* requireEnvironmentScope(AuthRelayWriteScope); |
| 645 | − const endpointRuntimeStatus = yield* dependencies.endpointRuntime.applyConfig(null); | |
| 645 | + const endpointRuntimeStatus = yield* dependencies.endpointRuntime.applyConfig(Option.none()); | |
| 646 | 646 | yield* Effect.all( |
| 647 | 647 | [ |
| 648 | 648 | dependencies.secrets.remove(CLOUD_LINKED_USER_ID), |
| @@ -2253,16 +2253,17 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 2252 unmodified lines | ||
| 2253 | 2253 | layers: { |
| 2254 | 2254 | cloudManagedEndpointRuntime: { |
| 2255 | 2255 | applyConfig: (config) => { |
| 2256 | − appliedRuntimeConfigs.push(config); | |
| 2257 | − if (!config) { | |
| 2256 | + appliedRuntimeConfigs.push(Option.getOrNull(config)); | |
| 2257 | + if (Option.isNone(config)) { | |
| 2258 | 2258 | return Effect.succeed({ status: "disabled" }); |
| 2259 | 2259 | } |
| 2260 | + const runtimeConfig = config.value; | |
| 2260 | 2261 | return Effect.succeed({ |
| 2261 | 2262 | status: "running", |
| 2262 | 2263 | providerKind: "cloudflare_tunnel", |
| 2263 | 2264 | pid: 123, |
| 2264 | − ...(config.tunnelId ? { tunnelId: config.tunnelId } : {}), | |
| 2265 | − ...(config.tunnelName ? { tunnelName: config.tunnelName } : {}), | |
| 2265 | + ...(runtimeConfig.tunnelId ? { tunnelId: runtimeConfig.tunnelId } : {}), | |
| 2266 | + ...(runtimeConfig.tunnelName ? { tunnelName: runtimeConfig.tunnelName } : {}), | |
| 2266 | 2267 | }); |
| 2267 | 2268 | }, |
| 2268 | 2269 | }, |