ignotum 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +6 -6
  2. package/dist/cli/bin.mjs +6111 -4448
  3. package/dist/cli/bin.mjs.map +1 -1
  4. package/dist/cli/emscripten-module-D5GzfUNv.mjs +533 -0
  5. package/dist/cli/emscripten-module-D5GzfUNv.mjs.map +1 -0
  6. package/dist/cli/ffi-7IElU4Co.mjs +148 -0
  7. package/dist/cli/ffi-7IElU4Co.mjs.map +1 -0
  8. package/dist/cli/managed-release-WHYLZwaS.mjs +10902 -0
  9. package/dist/cli/managed-release-WHYLZwaS.mjs.map +1 -0
  10. package/dist/cli/managed-release.d.mts +1 -0
  11. package/dist/cli/managed-release.mjs +14 -0
  12. package/dist/cli/managed-release.mjs.map +1 -0
  13. package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs +1289 -0
  14. package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs.map +1 -0
  15. package/dist/runtime/{api-Cv3hMbzo.d.ts → api-B4fwp5a2.d.ts} +4 -4
  16. package/dist/runtime/bootstrap-BISQSn_O.js +916 -0
  17. package/dist/runtime/bootstrap-BISQSn_O.js.map +1 -0
  18. package/dist/runtime/bootstrap-BTFScKAc.d.ts +545 -0
  19. package/dist/runtime/client.d.ts +50 -15
  20. package/dist/runtime/client.js +7 -1138
  21. package/dist/runtime/client.js.map +1 -1
  22. package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js +269 -0
  23. package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js.map +1 -0
  24. package/dist/runtime/{id-Bt9XWRGL.js → id--1m0NuUL.js} +4 -4
  25. package/dist/runtime/id--1m0NuUL.js.map +1 -0
  26. package/dist/runtime/{id-BzFHf3Wo-DGPCjgrf.d.ts → id-7an3nxTu-DZJSM5xh.d.ts} +2 -2
  27. package/dist/runtime/id-CBOt2kDo.d.ts +1 -0
  28. package/dist/runtime/identity-QjtJRxBD.d.ts +2 -0
  29. package/dist/runtime/image-Djtjy4Z8.js +19 -0
  30. package/dist/runtime/image-Djtjy4Z8.js.map +1 -0
  31. package/dist/runtime/index-D4seNHlX.d.ts +184 -0
  32. package/dist/runtime/internal/api.d.ts +1 -1
  33. package/dist/runtime/internal/api.js +30 -1
  34. package/dist/runtime/internal/api.js.map +1 -0
  35. package/dist/runtime/internal/bootstrap.d.ts +2 -0
  36. package/dist/runtime/internal/bootstrap.js +2 -0
  37. package/dist/runtime/internal/client.d.ts +2 -0
  38. package/dist/runtime/internal/client.js +75 -0
  39. package/dist/runtime/internal/client.js.map +1 -0
  40. package/dist/runtime/internal/host.d.ts +25 -8
  41. package/dist/runtime/internal/host.js +27 -6
  42. package/dist/runtime/internal/host.js.map +1 -1
  43. package/dist/runtime/internal/routes.d.ts +2 -0
  44. package/dist/runtime/internal/routes.js +2 -0
  45. package/dist/runtime/internal/server.d.ts +1 -1
  46. package/dist/runtime/internal/server.js +1 -1
  47. package/dist/runtime/internal/types.d.ts +2 -1
  48. package/dist/runtime/internal/types.js +1 -1
  49. package/dist/runtime/pagination-CBOt2kDo.d.ts +1 -0
  50. package/dist/runtime/pagination-D3qd6s9N.js +33 -0
  51. package/dist/runtime/pagination-D3qd6s9N.js.map +1 -0
  52. package/dist/runtime/result-BkziOG1L.d.ts +1 -0
  53. package/dist/runtime/router-BSBI1oN1.js +2377 -0
  54. package/dist/runtime/router-BSBI1oN1.js.map +1 -0
  55. package/dist/runtime/routes-Ce8TVP-T.js +690 -0
  56. package/dist/runtime/routes-Ce8TVP-T.js.map +1 -0
  57. package/dist/runtime/{schema-1Zs03-iS.js → schema-D1wOqiNH.js} +40 -12
  58. package/dist/runtime/schema-D1wOqiNH.js.map +1 -0
  59. package/dist/runtime/server.d.ts +5 -5
  60. package/dist/runtime/server.js +2 -2
  61. package/dist/runtime/{sync-Bs8J3fIr.d.ts → sync-D-GK2sv9.d.ts} +3 -2
  62. package/dist/runtime/{api-CAgKDij7.js → sync-DzUwA8W9.js} +22 -41
  63. package/dist/runtime/sync-DzUwA8W9.js.map +1 -0
  64. package/dist/runtime/types-DeCCyqLU-BIm7rU-0.d.ts +341 -0
  65. package/package.json +29 -4
  66. package/src/cli/agent-files.ts +14 -8
  67. package/src/cli/build/managed-client.ts +411 -0
  68. package/src/cli/build/managed-server.ts +294 -0
  69. package/src/cli/build/managed.ts +67 -0
  70. package/src/cli/build/output.ts +47 -0
  71. package/src/cli/build/server.ts +51 -220
  72. package/src/cli/client-config.ts +1 -140
  73. package/src/cli/client-styles.ts +1 -0
  74. package/src/cli/codegen.ts +9 -4
  75. package/src/cli/command.ts +28 -5
  76. package/src/cli/control-client.ts +84 -101
  77. package/src/cli/deploy.ts +133 -177
  78. package/src/cli/dev.ts +506 -107
  79. package/src/cli/image-assets.ts +499 -0
  80. package/src/cli/managed-client-boundaries.ts +103 -0
  81. package/src/cli/managed-dev-platform.ts +118 -0
  82. package/src/cli/managed-exports.ts +219 -0
  83. package/src/cli/managed-release-bin.ts +13 -0
  84. package/src/cli/managed-release.ts +260 -0
  85. package/src/cli/module-boundaries.ts +11 -1
  86. package/src/cli/new-app.ts +111 -87
  87. package/src/cli/route-codegen.ts +311 -0
  88. package/src/cli/route-static.ts +710 -0
  89. package/src/client/bootstrap.ts +110 -0
  90. package/src/client/errors.ts +13 -7
  91. package/src/client/files.ts +64 -0
  92. package/src/client/id.ts +9 -5
  93. package/src/client/image.ts +28 -0
  94. package/src/client/index.ts +25 -2
  95. package/src/client/managed-client.ts +729 -0
  96. package/src/client/managed-upgrade.ts +283 -0
  97. package/src/client/recovery-journal.ts +195 -0
  98. package/src/client/route-boundaries.ts +184 -0
  99. package/src/client/router-history.ts +152 -0
  100. package/src/client/router-store.ts +644 -0
  101. package/src/client/router.ts +324 -0
  102. package/src/client/routes.ts +28 -0
  103. package/src/client/sync.ts +66 -592
  104. package/src/dev-runtime/database.ts +77 -2
  105. package/src/dev-runtime/functions.ts +3 -3
  106. package/src/dev-runtime/id.ts +19 -4
  107. package/src/dev-runtime/managed-functions.ts +255 -0
  108. package/src/dev-runtime/managed-sync.ts +263 -0
  109. package/src/dev-runtime/managed-websocket.ts +71 -0
  110. package/src/dev-runtime/migrations.ts +20 -0
  111. package/src/dev-runtime/sync.ts +236 -236
  112. package/src/image-assets.d.ts +36 -0
  113. package/src/internal/client.ts +186 -0
  114. package/src/internal/host.ts +1 -1
  115. package/src/internal/http-paths.ts +0 -3
  116. package/src/internal/routes.ts +45 -0
  117. package/dist/runtime/api-CAgKDij7.js.map +0 -1
  118. package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js +0 -154
  119. package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js.map +0 -1
  120. package/dist/runtime/id-Bt9XWRGL.js.map +0 -1
  121. package/dist/runtime/id-Dz0apuB3.d.ts +0 -1
  122. package/dist/runtime/index-D54flWtH.d.ts +0 -402
  123. package/dist/runtime/pagination-DnKg3dkI-r5ZUxBBx.d.ts +0 -112
  124. package/dist/runtime/pagination-Dz0apuB3.d.ts +0 -1
  125. package/dist/runtime/result-DKAA4gpS.d.ts +0 -1
  126. package/dist/runtime/schema-1Zs03-iS.js.map +0 -1
  127. package/src/cli/build/client.ts +0 -119
  128. package/src/cli/build/public.ts +0 -186
  129. package/src/cli/client-entry.ts +0 -152
  130. package/src/cli/client-plugin.ts +0 -136
  131. package/src/client/app.ts +0 -15
@@ -1,24 +1,12 @@
1
1
  import { BrowserSession } from "./id.js";
2
- import {
3
- Context,
4
- Deferred,
5
- Duration,
6
- Effect,
7
- Fiber,
8
- HashMap,
9
- Layer,
10
- ManagedRuntime,
11
- Option,
12
- Schedule,
13
- Schema,
14
- } from "effect";
15
- import * as Socket from "effect/unstable/socket/Socket";
16
- import * as BrowserCrypto from "@effect/platform-browser/BrowserCrypto";
2
+ import { ManagedBrowserClient } from "./managed-client.js";
3
+ import type { ExecutionSelection } from "@ignotum/contracts/platform";
4
+ import { Context, Effect, Layer, ManagedRuntime, Schema } from "effect";
17
5
 
18
6
  import { encodeCanonicalJson } from "@ignotum/contracts/json";
19
7
  import { type AppStateRevision } from "@ignotum/contracts/runtime/hosted";
20
8
  import { IdGenerator } from "@ignotum/shared/id";
21
- import { encodeTransportObject } from "@ignotum/contracts/runtime/sync";
9
+ import type { ManagedClientMessage, ManagedServerMessage } from "@ignotum/contracts/managed-sync";
22
10
 
23
11
  import type {
24
12
  ErrorValue,
@@ -27,33 +15,20 @@ import type {
27
15
  } from "@ignotum/contracts/runtime/result";
28
16
  import { failureFromWire, pending, Result } from "@ignotum/contracts/runtime/result";
29
17
  import {
30
- ClientMessageJson,
31
- DeploymentCloseCode,
32
- InvocationId,
33
- ServerMessageJson,
34
18
  SubscriptionId,
35
19
  decodeTransportValue,
36
- type ClientMessage,
37
20
  type FunctionAddress,
38
- type ServerMessage,
39
- type SyncHandshake,
40
21
  type WireResult,
41
22
  } from "@ignotum/contracts/runtime/sync";
42
23
  import {
43
24
  ClientInfrastructureError,
44
25
  ConnectionUnavailable,
45
- InvalidClientMessage,
46
26
  InvalidServerMessage,
47
- ServerError,
48
- InvalidMutationArguments,
49
27
  clientErrorReporterLayer,
50
- reportClientError,
51
28
  } from "./errors.js";
52
- import { syncPath } from "../internal/http-paths.js";
53
- import { applyFileGrants, prepareMutationArguments } from "./files.js";
29
+ import { applyFileGrants } from "./files.js";
54
30
 
55
31
  type Listener = () => void;
56
- type SocketWriter = (chunk: string) => Effect.Effect<void, Socket.SocketError>;
57
32
 
58
33
  interface QueryEntry {
59
34
  readonly args: Schema.Json;
@@ -67,13 +42,16 @@ interface QueryEntry {
67
42
  result: QueryResult<unknown, ErrorValue> | ClientInfrastructureError;
68
43
  }
69
44
 
70
- interface QueryObserver {
45
+ export interface QueryObserver {
71
46
  readonly getSnapshot: () => QueryResult<unknown, ErrorValue> | ClientInfrastructureError;
72
47
  readonly release: Effect.Effect<void>;
73
48
  readonly subscribe: (listener: Listener) => () => void;
74
49
  }
75
50
 
76
- type QueryLifecycleMessage = Extract<ClientMessage, { readonly type: "Subscribe" | "Unsubscribe" }>;
51
+ type QueryLifecycleMessage = Extract<
52
+ ManagedClientMessage,
53
+ { readonly type: "Subscribe" | "Unsubscribe" }
54
+ >;
77
55
 
78
56
  interface QueryCache {
79
57
  readonly invalidate: (ids?: ReadonlyArray<SubscriptionId>) => void;
@@ -201,7 +179,9 @@ const makeQueryCache = (
201
179
  } satisfies QueryCache;
202
180
  };
203
181
 
204
- type SnapshotFiles = NonNullable<Extract<ServerMessage, { readonly type: "Snapshot" }>["files"]>;
182
+ type SnapshotFiles = NonNullable<
183
+ Extract<ManagedServerMessage, { readonly type: "QuerySnapshot" }>["files"]
184
+ >;
205
185
 
206
186
  const resultFromWire = Effect.fn("SyncClient.resultFromWire")(
207
187
  (wire: WireResult, files: SnapshotFiles = []) =>
@@ -222,40 +202,13 @@ const resultFromWire = Effect.fn("SyncClient.resultFromWire")(
222
202
  }),
223
203
  );
224
204
 
225
- export const shouldReloadForCloseCode = (code: number): boolean => code === DeploymentCloseCode;
226
-
227
- const handshakesMatch = (first: SyncHandshake, next: SyncHandshake): boolean =>
228
- first.appId === next.appId &&
229
- first.deploymentId === next.deploymentId &&
230
- first.generation === next.generation;
231
-
232
- type HandshakeDecision = "Accept" | "Ignore" | "Reload";
233
-
234
- const handshakeDecision = (
235
- remembered: SyncHandshake | undefined,
236
- acceptedOnConnection: boolean,
237
- next: SyncHandshake,
238
- ): HandshakeDecision => {
239
- if (remembered !== undefined && !handshakesMatch(remembered, next)) return "Reload";
240
- return acceptedOnConnection ? "Ignore" : "Accept";
241
- };
242
-
243
205
  export const syncClientInternals = {
244
206
  makeQueryCache,
245
- handshakeDecision,
246
207
  queryIdentity: queryKey,
247
208
  resultFromWire,
248
- handshakesMatch,
249
- shouldReloadForCloseCode,
250
209
  };
251
210
 
252
- const socketUrl = (): string => {
253
- const url = new URL(syncPath, globalThis.location.href);
254
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
255
- return url.href;
256
- };
257
-
258
- interface SyncClientService {
211
+ export interface SyncClientService {
259
212
  readonly mutate: (
260
213
  functionAddress: FunctionAddress,
261
214
  // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Generated reference argument types are deliberately erased at the client transport boundary.
@@ -267,539 +220,60 @@ interface SyncClientService {
267
220
  ) => Effect.Effect<QueryObserver, ClientInfrastructureError>;
268
221
  }
269
222
 
270
- interface PendingInvocation {
271
- readonly sessionEpoch: string;
272
- readonly deferred: Deferred.Deferred<
273
- IgnotumResult<unknown, ErrorValue>,
274
- ClientInfrastructureError
275
- >;
276
- readonly function: FunctionAddress;
277
- readonly message: Extract<ClientMessage, { readonly type: "Invoke" | "Prepare" }>;
278
- readonly prepared: Deferred.Deferred<
279
- Extract<ServerMessage, { readonly type: "Preparation" }>["uploads"],
280
- ClientInfrastructureError
281
- >;
282
- }
283
-
284
223
  export class SyncClient extends Context.Service<SyncClient, SyncClientService>()(
285
224
  "ignotum/client/sync/SyncClient",
286
- ) {
287
- static readonly layer = Layer.effect(
288
- SyncClient,
289
- Effect.gen(function* () {
290
- const ids = yield* IdGenerator;
291
- const session = yield* BrowserSession;
292
- let invocations = HashMap.empty<InvocationId, PendingInvocation>();
293
- let rememberedHandshake: SyncHandshake | undefined;
294
- let reloadRequested = false;
295
- let writer: SocketWriter | undefined;
296
- let sessionSuspended: Deferred.Deferred<void> | undefined;
297
-
298
- const encode = (message: ClientMessage) =>
299
- Schema.encodeEffect(ClientMessageJson)(message).pipe(
300
- Effect.mapError((cause) =>
301
- InvalidClientMessage.make({
302
- cause,
303
- message: "Could not encode an Ignotum sync message.",
304
- }),
305
- ),
306
- );
307
-
308
- const sendWith = (write: SocketWriter, message: ClientMessage) =>
309
- encode(message).pipe(
310
- Effect.flatMap(write),
311
- Effect.mapError((cause) =>
312
- Schema.is(InvalidClientMessage)(cause)
313
- ? cause
314
- : ConnectionUnavailable.make({
315
- cause,
316
- message: "The sync connection was lost.",
317
- }),
318
- ),
319
- );
320
-
321
- const send = (message: ClientMessage) =>
322
- writer === undefined
323
- ? Effect.fail(
324
- ConnectionUnavailable.make({ message: "The sync connection is not ready." }),
325
- )
326
- : sendWith(writer, message);
327
-
328
- const queryCache = makeQueryCache(ids.generate(SubscriptionId), (message) =>
329
- writer === undefined
330
- ? Effect.void
331
- : send(message).pipe(
332
- Effect.catchTags({
333
- ConnectionUnavailable: reportClientError,
334
- InvalidClientMessage: reportClientError,
335
- }),
336
- ),
337
- );
338
-
339
- const unsubscribeSession = session.subscribe(() => {
340
- if (session.current() === undefined) {
341
- queryCache.invalidatePrivate();
342
- writer = undefined;
343
- if (sessionSuspended !== undefined) Deferred.doneUnsafe(sessionSuspended, Effect.void);
344
- }
345
- });
346
- yield* Effect.addFinalizer(() => Effect.sync(unsubscribeSession));
347
-
348
- const rejectInvocation = (id: InvocationId, error: ClientInfrastructureError) => {
349
- const pendingInvocation = Option.getOrUndefined(HashMap.get(invocations, id));
350
- if (pendingInvocation === undefined) {
351
- return Effect.void;
352
- }
353
- invocations = HashMap.remove(invocations, id);
354
- return reportClientError(error).pipe(
355
- Effect.andThen(Deferred.fail(pendingInvocation.prepared, error)),
356
- Effect.andThen(Deferred.fail(pendingInvocation.deferred, error)),
357
- Effect.asVoid,
358
- );
359
- };
360
-
361
- const handleMessage = Effect.fn("SyncClient.handleMessage")(function* (
362
- message: Exclude<ServerMessage, SyncHandshake | { readonly type: "Deployment" }>,
363
- ) {
364
- switch (message.type) {
365
- case "Session": {
366
- if (message.event === "Updated") {
367
- const previous = session.current();
368
- if (previous !== undefined && previous.sessionEpoch !== message.session.sessionEpoch)
369
- return;
370
- if (previous?.viewRevision !== message.session.viewRevision)
371
- queryCache.invalidatePrivate();
372
- session.accept(message.session, message.serverTime);
373
- } else {
374
- if (rememberedHandshake?.session?.sessionEpoch !== message.sessionEpoch) return;
375
- session.suspend(
376
- message.event === "Unavailable"
377
- ? "Your ID session is temporarily unavailable."
378
- : undefined,
379
- );
380
- if (message.event !== "Unavailable") {
381
- yield* Effect.forEach(HashMap.keys(invocations), (id) =>
382
- rejectInvocation(
383
- id,
384
- ServerError.make({
385
- code: "SessionChanged",
386
- message: "The session changed. This operation may already have completed.",
387
- }),
388
- ),
389
- );
390
- }
391
- }
392
- return;
393
- }
394
- case "Invalidate": {
395
- if (message.sessionEpoch === session.current()?.sessionEpoch)
396
- queryCache.invalidate(message.ids);
397
- return;
398
- }
399
- case "Snapshot": {
400
- const current = session.current();
401
- if (current === undefined || message.identity?.sessionEpoch !== current.sessionEpoch)
402
- return;
403
- if (
404
- message.identity.dependsOnId &&
405
- message.identity.viewRevision !== current.viewRevision
406
- )
407
- return;
408
- const entry = queryCache.getById(message.id);
409
- if (entry === undefined) return;
410
- entry.dependsOnId = message.identity.dependsOnId;
411
- if (entry.revision !== undefined && message.revision < entry.revision) return;
412
- yield* resultFromWire(message.result, message.files ?? []).pipe(
413
- Effect.tap((result) =>
414
- Effect.sync(() => queryCache.setResult(message.id, result, message.revision)),
415
- ),
416
- Effect.catchTags({
417
- InvalidServerMessage: (error) => {
418
- queryCache.setResult(message.id, error, message.revision);
419
- return reportClientError(error);
420
- },
421
- }),
422
- );
423
- return;
424
- }
425
- case "Preparation": {
426
- if (message.sessionEpoch !== session.current()?.sessionEpoch) return;
427
- const pendingInvocation = Option.getOrUndefined(HashMap.get(invocations, message.id));
428
- if (pendingInvocation !== undefined) {
429
- yield* Deferred.succeed(pendingInvocation.prepared, message.uploads);
430
- }
431
- return;
432
- }
433
- case "Result": {
434
- if (message.sessionEpoch !== session.current()?.sessionEpoch) return;
435
- const pendingInvocation = Option.getOrUndefined(HashMap.get(invocations, message.id));
436
- if (pendingInvocation === undefined) return;
437
- yield* resultFromWire(message.result).pipe(
438
- Effect.flatMap((result) => {
439
- invocations = HashMap.remove(invocations, message.id);
440
- return Deferred.succeed(pendingInvocation.deferred, result);
441
- }),
442
- Effect.catchTags({
443
- InvalidServerMessage: (error) => rejectInvocation(message.id, error),
444
- }),
445
- );
446
- return;
447
- }
448
- case "Error": {
449
- if (message.sessionEpoch !== session.current()?.sessionEpoch) return;
450
- const error =
451
- message.operation === undefined
452
- ? ServerError.make({
453
- code: message.code,
454
- message: message.message,
455
- })
456
- : ServerError.make({
457
- code: message.code,
458
- message: message.message,
459
- operation: message.operation,
460
- });
461
- if (message.operation?.type === "Invocation") {
462
- yield* rejectInvocation(message.operation.id, error);
463
- } else if (message.operation?.type === "Subscription") {
464
- queryCache.setResult(message.operation.id, error);
465
- yield* reportClientError(error);
466
- } else {
467
- yield* reportClientError(error);
468
- }
469
- return;
470
- }
471
- }
472
- });
473
-
474
- const requestReload = () => {
475
- if (reloadRequested) return;
476
- reloadRequested = true;
477
- writer = undefined;
478
- globalThis.location.reload();
479
- };
480
-
481
- const disconnect = Effect.sync(() => {
482
- writer = undefined;
483
- });
484
-
485
- const connect = Effect.scoped(
486
- Effect.gen(function* () {
487
- if (reloadRequested) return yield* Effect.never;
488
- yield* session.bootstrap.pipe(
489
- Effect.mapError((error) => ConnectionUnavailable.make({ message: error.message })),
490
- );
491
- const socket = yield* Socket.makeWebSocket(
492
- `${socketUrl()}${session.socketParameters()}`,
493
- {
494
- closeCodeIsError: (code) => {
495
- if (shouldReloadForCloseCode(code)) requestReload();
496
- return false;
497
- },
498
- },
499
- );
500
- const write = yield* socket.writer;
501
- const handshakeReceived = yield* Deferred.make<void>();
502
- let handshakeAccepted = false;
503
- const acceptHandshake = Effect.fn("SyncClient.acceptHandshake")(function* (
504
- handshake: SyncHandshake,
505
- ) {
506
- const decision = handshakeDecision(rememberedHandshake, handshakeAccepted, handshake);
507
- if (decision === "Reload") {
508
- requestReload();
509
- return;
510
- }
511
- if (decision === "Ignore") return;
512
- if (handshake.session === undefined || handshake.serverTime === undefined) {
513
- requestReload();
514
- return;
515
- }
516
- if (
517
- rememberedHandshake?.session !== undefined &&
518
- rememberedHandshake.session.sessionEpoch !== handshake.session.sessionEpoch
519
- ) {
520
- queryCache.invalidatePrivate();
521
- yield* Effect.forEach(HashMap.keys(invocations), (id) =>
522
- rejectInvocation(
523
- id,
524
- ServerError.make({
525
- code: "SessionChanged",
526
- message: "The session changed. This operation may already have completed.",
527
- }),
528
- ),
529
- );
530
- } else if (
531
- rememberedHandshake?.session?.viewRevision !== handshake.session.viewRevision
532
- ) {
533
- queryCache.invalidatePrivate();
534
- }
535
- yield* Effect.forEach(HashMap.entries(invocations), ([id, pending]) =>
536
- pending.sessionEpoch === handshake.session!.sessionEpoch
537
- ? Effect.void
538
- : rejectInvocation(
539
- id,
540
- ServerError.make({
541
- code: "SessionChanged",
542
- message: "The session changed. This operation may already have completed.",
543
- }),
544
- ),
545
- );
546
- session.accept(handshake.session, handshake.serverTime);
547
- rememberedHandshake = handshake;
548
- handshakeAccepted = true;
549
- writer = write;
550
- yield* Deferred.succeed(handshakeReceived, undefined);
551
- yield* Effect.gen(function* () {
552
- yield* Effect.forEach(queryCache.entries(), (entry) =>
553
- sendWith(write, subscribeMessage(entry)),
554
- );
555
- yield* Effect.forEach(HashMap.values(invocations), (pendingInvocation) =>
556
- sendWith(write, pendingInvocation.message),
557
- );
558
- }).pipe(Effect.tapError(reportClientError), Effect.ignore);
559
- });
560
-
561
- const handleText = Effect.fn("SyncClient.handleText")(function* (text: string) {
562
- const message = yield* Schema.decodeEffect(ServerMessageJson)(text).pipe(
563
- Effect.mapError((cause) =>
564
- InvalidServerMessage.make({
565
- cause,
566
- message: "The server returned an invalid sync message.",
567
- }),
568
- ),
569
- );
570
- if (message.type === "Handshake") return yield* acceptHandshake(message);
571
- if (message.type === "Deployment") {
572
- requestReload();
573
- return;
574
- }
575
- if (!handshakeAccepted) {
576
- return yield* InvalidServerMessage.make({
577
- cause: new Error("The server sent a sync message before the handshake."),
578
- message: "The server returned a sync message before the handshake.",
579
- });
580
- }
581
- return yield* handleMessage(message);
582
- });
583
-
584
- const suspended = yield* Deferred.make<void>();
585
- sessionSuspended = suspended;
586
- yield* Effect.addFinalizer(() =>
587
- Effect.sync(() => {
588
- if (sessionSuspended === suspended) sessionSuspended = undefined;
589
- }),
590
- );
591
- const run = yield* socket.runString(handleText).pipe(Effect.forkChild);
592
- yield* Effect.raceFirst(
593
- Deferred.await(handshakeReceived).pipe(Effect.timeout("10 seconds")),
594
- Fiber.join(run),
595
- );
596
- yield* Effect.raceFirst(Fiber.join(run), Deferred.await(suspended));
597
- }),
598
- ).pipe(
599
- Effect.mapError((cause) =>
600
- Schema.is(ClientInfrastructureError)(cause)
601
- ? cause
602
- : ConnectionUnavailable.make({
603
- cause,
604
- message: "The sync connection was lost.",
605
- }),
606
- ),
607
- Effect.ensuring(disconnect),
608
- );
609
-
610
- const reconnectSchedule = Schedule.exponential("250 millis").pipe(
611
- Schedule.modifyDelay(({ duration }) =>
612
- Effect.succeed(Duration.min(duration, Duration.seconds(5))),
613
- ),
614
- Schedule.jittered,
615
- );
616
- yield* connect.pipe(
617
- Effect.andThen(
618
- Effect.fail(
619
- ConnectionUnavailable.make({
620
- message: "The sync connection closed; reconnecting.",
621
- }),
622
- ),
623
- ),
624
- Effect.tapError(reportClientError),
625
- Effect.retry(reconnectSchedule),
626
- Effect.forkScoped,
627
- );
628
-
629
- const observe: SyncClientService["observe"] = Effect.fn("SyncClient.observe")(
630
- function* (functionAddress, args) {
631
- return yield* queryCache.observe(functionAddress, args);
632
- },
633
- );
634
-
635
- const mutate: SyncClientService["mutate"] = Effect.fn("SyncClient.mutate")(
636
- function* (functionAddress, args) {
637
- const admittedSession =
638
- session.current() ??
639
- rememberedHandshake?.session ??
640
- (yield* session.bootstrap.pipe(
641
- Effect.mapError((error) => ConnectionUnavailable.make({ message: error.message })),
642
- ));
643
- const invocationId = yield* ids.generate(InvocationId);
644
- const preparedArguments = yield* prepareMutationArguments(args).pipe(
645
- Effect.provideService(IdGenerator, ids),
646
- Effect.mapError((cause) =>
647
- InvalidMutationArguments.make({
648
- cause,
649
- function: functionAddress,
650
- message: `Mutation arguments for ${functionAddress} contain an unsupported file or value.`,
651
- }),
652
- ),
653
- Effect.tapError(reportClientError),
654
- );
655
- const jsonArgs = yield* Effect.try({
656
- try: () => encodeTransportObject(preparedArguments.value),
657
- catch: (cause) =>
658
- InvalidMutationArguments.make({
659
- cause,
660
- function: functionAddress,
661
- message: `Mutation arguments for ${functionAddress} contain an unsupported value.`,
662
- }),
663
- }).pipe(Effect.tapError(reportClientError));
664
- const deferred = yield* Deferred.make<
665
- IgnotumResult<unknown, ErrorValue>,
666
- ClientInfrastructureError
667
- >();
668
- const prepared = yield* Deferred.make<
669
- Extract<ServerMessage, { readonly type: "Preparation" }>["uploads"],
670
- ClientInfrastructureError
671
- >();
672
- const invokeMessage: Extract<ClientMessage, { readonly type: "Invoke" }> = {
673
- type: "Invoke",
674
- id: invocationId,
675
- kind: "Mutation",
676
- function: functionAddress,
677
- args: jsonArgs,
678
- };
679
- const message: PendingInvocation["message"] =
680
- preparedArguments.fileIds.length === 0
681
- ? invokeMessage
682
- : {
683
- type: "Prepare",
684
- id: invocationId,
685
- kind: "Mutation",
686
- function: functionAddress,
687
- args: jsonArgs,
688
- files: preparedArguments.fileIds,
689
- };
690
- invocations = HashMap.set(invocations, invocationId, {
691
- sessionEpoch: admittedSession.sessionEpoch,
692
- deferred,
693
- function: functionAddress,
694
- message,
695
- prepared,
696
- });
697
- return yield* Effect.gen(function* () {
698
- if (writer !== undefined) {
699
- yield* sendWith(writer, message).pipe(
700
- Effect.catchTags({
701
- ConnectionUnavailable: reportClientError,
702
- InvalidClientMessage: (error) => rejectInvocation(invocationId, error),
703
- }),
704
- );
705
- }
706
- if (message.type === "Prepare") {
707
- const uploads = yield* Deferred.await(prepared);
708
- const expected = new Set(message.files);
709
- const received = new Set<string>();
710
- const transfers: Array<{ readonly file: File; readonly url: string }> = [];
711
- for (const upload of uploads) {
712
- if (!expected.has(upload.id) || received.has(upload.id)) {
713
- return yield* InvalidServerMessage.make({
714
- cause: new Error(`Unexpected prepared file '${upload.id}'.`),
715
- message: "The server returned invalid application file preparation data.",
716
- });
717
- }
718
- received.add(upload.id);
719
- const file = preparedArguments.nativeFiles.get(upload.id);
720
- if (file === undefined) {
721
- return yield* InvalidServerMessage.make({
722
- cause: new Error(`Missing native file '${upload.id}'.`),
723
- message: "The server prepared an unknown application file.",
724
- });
725
- }
726
- if (upload.url === undefined) continue;
727
- transfers.push({ file, url: upload.url });
728
- }
729
- if (received.size !== expected.size) {
730
- return yield* InvalidServerMessage.make({
731
- cause: new Error("The server omitted a prepared file."),
732
- message: "The server returned incomplete application file preparation data.",
733
- });
734
- }
735
- yield* Effect.forEach(
736
- transfers,
737
- Effect.fn("SyncClient.uploadFile")(function* ({ file, url }) {
738
- const response = yield* Effect.tryPromise({
739
- try: (signal) =>
740
- fetch(url, {
741
- method: "PUT",
742
- body: file,
743
- headers: { "content-type": file.type },
744
- signal,
745
- }),
746
- catch: (cause) =>
747
- ConnectionUnavailable.make({
748
- cause,
749
- message: `File '${file.name}' could not be uploaded.`,
750
- }),
751
- });
752
- if (!response.ok) {
753
- const detail = yield* Effect.promise(() => response.text());
754
- return yield* InvalidMutationArguments.make({
755
- cause: new Error(detail),
756
- function: functionAddress,
757
- message: `File '${file.name}' was rejected by application storage.`,
758
- });
759
- }
760
- }),
761
- { concurrency: 3, discard: true },
762
- );
763
- const pendingInvocation = Option.getOrUndefined(
764
- HashMap.get(invocations, invocationId),
765
- );
766
- if (pendingInvocation === undefined) return yield* Deferred.await(deferred);
767
- invocations = HashMap.set(invocations, invocationId, {
768
- ...pendingInvocation,
769
- message: invokeMessage,
770
- });
771
- if (writer !== undefined) {
772
- yield* sendWith(writer, invokeMessage).pipe(
773
- Effect.catchTags({
774
- ConnectionUnavailable: reportClientError,
775
- InvalidClientMessage: (error) => rejectInvocation(invocationId, error),
776
- }),
777
- );
778
- }
779
- }
780
- return yield* Deferred.await(deferred);
781
- }).pipe(
782
- Effect.tapError(() =>
783
- Effect.sync(() => {
784
- invocations = HashMap.remove(invocations, invocationId);
785
- }),
786
- ),
787
- );
788
- },
789
- );
790
-
791
- return SyncClient.of({ mutate, observe });
792
- }),
793
- ).pipe(Layer.provide(Layer.merge(Socket.layerWebSocketConstructorGlobal, BrowserCrypto.layer)));
794
- }
795
-
796
- export const syncRuntime = ManagedRuntime.make(
797
- SyncClient.layer.pipe(
798
- Layer.provideMerge(BrowserSession.layer),
799
- Layer.provideMerge(clientErrorReporterLayer),
800
- Layer.provideMerge(IdGenerator.layer),
801
- ),
802
- );
225
+ ) {}
226
+
227
+ const makeUnmountedSyncRuntime = () =>
228
+ ManagedRuntime.make(
229
+ Layer.succeed(SyncClient)({
230
+ observe: () =>
231
+ Effect.fail(ConnectionUnavailable.make({ message: "The application has not mounted." })),
232
+ mutate: () =>
233
+ Effect.fail(ConnectionUnavailable.make({ message: "The application has not mounted." })),
234
+ }).pipe(
235
+ Layer.provideMerge(BrowserSession.layer),
236
+ Layer.provideMerge(clientErrorReporterLayer),
237
+ Layer.provideMerge(IdGenerator.layer),
238
+ ),
239
+ );
240
+ export let syncRuntime = makeUnmountedSyncRuntime();
241
+
242
+ export const mountManagedSync = (
243
+ selection: ExecutionSelection,
244
+ browser: Window,
245
+ callbacks: { readonly stopAdmission: () => void; readonly reload: () => void },
246
+ ) => {
247
+ const client = new ManagedBrowserClient(selection, browser, syncClientInternals, callbacks);
248
+ const previous = syncRuntime;
249
+ void previous.dispose();
250
+ syncRuntime = ManagedRuntime.make(
251
+ Layer.effect(
252
+ SyncClient,
253
+ Effect.gen(function* () {
254
+ client.attachSession(yield* BrowserSession, yield* IdGenerator);
255
+ return SyncClient.of({ observe: client.observe, mutate: client.mutate });
256
+ }),
257
+ ).pipe(
258
+ Layer.provideMerge(BrowserSession.layer),
259
+ Layer.provideMerge(clientErrorReporterLayer),
260
+ Layer.provideMerge(IdGenerator.layer),
261
+ ),
262
+ );
263
+ const runtime = syncRuntime;
264
+ void runtime.runPromise(SyncClient).then(
265
+ () => client.start(),
266
+ () => client.dispose(),
267
+ );
268
+ return {
269
+ client,
270
+ dispose: () => {
271
+ client.dispose();
272
+ void runtime.dispose();
273
+ if (syncRuntime === runtime) syncRuntime = makeUnmountedSyncRuntime();
274
+ },
275
+ };
276
+ };
803
277
 
804
278
  // SAFETY: Vite adds this optional property to browser development modules.
805
279
  const hot = (