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,7 +1,13 @@
1
+ import {
2
+ managedSyncPath,
3
+ type ManagedClientMessage,
4
+ type ManagedServerMessage,
5
+ } from "@ignotum/contracts/managed-sync";
6
+ import { runManagedSession, type ManagedSyncOptions } from "./managed-sync.js";
7
+ import { ManagedWebSocket } from "./managed-websocket.js";
1
8
  import { LocalQueryCache } from "./query-cache.js";
2
9
  import {
3
10
  SessionRequest,
4
- idViewKey,
5
11
  sessionPath,
6
12
  sessionRequestsPath,
7
13
  type SessionState,
@@ -13,31 +19,19 @@ import type { IncomingMessage, ServerResponse } from "node:http";
13
19
  import { NodeHttpServer, NodeServices, NodeSocket } from "@effect/platform-node";
14
20
  import { SqliteClient } from "@effect/sql-sqlite-node";
15
21
  import {
16
- AppId,
17
22
  AppStateRevision,
18
- DeploymentGeneration,
19
- DeploymentId,
20
23
  InvocationId,
21
24
  RuntimeMutationResult,
22
25
  canonicalInvocationInput,
23
26
  canonicalQueryKey,
24
27
  } from "@ignotum/contracts/runtime/hosted";
25
- import {
26
- ClientMessageJson,
27
- ServerMessageJson,
28
- type ClientMessage,
29
- type FunctionAddress,
30
- type Operation,
31
- type ErrorCode,
32
- type ServerMessage,
33
- type SubscriptionId,
34
- } from "@ignotum/contracts/runtime/sync";
28
+ import { SubscriptionId, type FunctionAddress } from "@ignotum/contracts/runtime/sync";
29
+ import type { ExecutionSelection } from "@ignotum/contracts/platform";
35
30
  import {
36
31
  Effect,
37
32
  Clock,
38
33
  Context,
39
34
  FileSystem,
40
- Function,
41
35
  FiberSet,
42
36
  HashMap,
43
37
  Layer,
@@ -70,7 +64,7 @@ import {
70
64
  } from "@ignotum/runtime/sync";
71
65
 
72
66
  import { generate } from "../cli/codegen.js";
73
- import { isIgnotumPath, syncPath } from "../internal/http-paths.js";
67
+ import { isIgnotumPath } from "../internal/http-paths.js";
74
68
  import { LocalDatabase } from "./database.js";
75
69
  import { IdGenerator } from "@ignotum/shared/id";
76
70
  import { DevelopmentDatabase } from "./migrations.js";
@@ -85,16 +79,11 @@ import { LocalApplicationFiles, localApplicationFilesLayer } from "./files.js";
85
79
  interface QuerySubscription {
86
80
  readonly args: Schema.Json;
87
81
  readonly function: FunctionAddress;
82
+ readonly grantId: SubscriptionId;
88
83
  readonly queryKey: ReturnType<typeof canonicalQueryKey>;
89
84
  refreshGeneration: number;
90
85
  }
91
86
 
92
- export const localSyncIdentity = {
93
- appId: AppId.make("app_000000000000000000000001"),
94
- deploymentId: DeploymentId.make("dep_000000000000000000000001"),
95
- generation: DeploymentGeneration.make(0),
96
- } as const;
97
-
98
87
  export { QueryInvalidation };
99
88
 
100
89
  const reloadEnvironmentAndInvalidate = <Error, Requirements>(
@@ -134,9 +123,12 @@ class InvocationIdConflict extends Schema.TaggedError<InvocationIdConflict>()(
134
123
 
135
124
  const MutationReplayRow = Schema.Struct({ input: Schema.String, result: Schema.String });
136
125
 
137
- class MutationReplay extends Context.Service<
126
+ export class MutationReplay extends Context.Service<
138
127
  MutationReplay,
139
128
  {
129
+ readonly lookup: (
130
+ id: InvocationId,
131
+ ) => Effect.Effect<RuntimeMutationResult | undefined, SqlError.SqlError>;
140
132
  readonly execute: (
141
133
  id: InvocationId,
142
134
  input: string,
@@ -161,6 +153,16 @@ class MutationReplay extends Context.Service<
161
153
  });
162
154
 
163
155
  return MutationReplay.of({
156
+ lookup: Effect.fn("MutationReplay.lookup")(function* (id) {
157
+ const replay = yield* findReplay({ id }).pipe(
158
+ Effect.catchTags({ SchemaError: Effect.die }),
159
+ );
160
+ return Option.isNone(replay)
161
+ ? undefined
162
+ : yield* Schema.decodeEffect(Schema.fromJsonString(RuntimeMutationResult))(
163
+ replay.value.result,
164
+ ).pipe(Effect.orDie);
165
+ }),
164
166
  execute: Effect.fn("MutationReplay.execute")(
165
167
  function* (
166
168
  id,
@@ -198,63 +200,57 @@ class MutationReplay extends Context.Service<
198
200
 
199
201
  export const mutationReplayLayer = MutationReplay.layer;
200
202
 
201
- const operationForSubscription = (subscriptionId: SubscriptionId): Operation => ({
202
- type: "Subscription",
203
- id: subscriptionId,
204
- });
205
-
206
- const syncError = (code: ErrorCode, message: string, operation?: Operation): ServerMessage => {
207
- if (operation === undefined) {
208
- return { type: "Error", code, message };
203
+ export type LocalSessionMessage = Extract<
204
+ ManagedClientMessage,
205
+ {
206
+ readonly type: "Subscribe" | "Unsubscribe" | "Prepare" | "Invoke";
209
207
  }
210
- return { type: "Error", code, message, operation };
211
- };
208
+ >;
209
+ type LocalErrorCode = Extract<ManagedServerMessage, { readonly type: "Error" }>["code"];
210
+ const syncError = (code: LocalErrorCode, message: string, id: string): ManagedServerMessage => ({
211
+ type: "Error",
212
+ code,
213
+ message,
214
+ id,
215
+ });
212
216
 
213
- export const runSession = Effect.fn("SyncServer.runSession")(function* (
214
- socket: Socket.Socket,
215
- initialIdentity?: SessionState,
216
- refreshIdentity?: Effect.Effect<SessionState>,
217
+ /** Executes admitted session commands directly; the transport owns framing and selection checks. */
218
+ export const createLocalSession = Effect.fn("LocalSession.create")(function* (
219
+ selection: ExecutionSelection,
220
+ identity: SessionState,
221
+ send: (message: ManagedServerMessage) => Effect.Effect<void, Socket.SocketError>,
217
222
  ) {
218
- let identity = initialIdentity ?? {
219
- sessionEpoch: "dev:anonymous",
220
- user: null,
221
- viewRevision: 0,
222
- validUntil: Number.MAX_SAFE_INTEGER,
223
- };
224
223
  const runtime = yield* FunctionRuntime;
224
+ const ids = yield* IdGenerator;
225
225
  const invalidation = yield* QueryInvalidation;
226
226
  const mutationReplay = yield* MutationReplay;
227
227
  const queryCache = yield* LocalQueryCache;
228
228
  const files = yield* LocalApplicationFiles;
229
229
  const subscriptions = yield* Ref.make(HashMap.empty<SubscriptionId, QuerySubscription>());
230
230
  const dependencyIndex = makeDependencyIndex<SubscriptionId>();
231
+ yield* Effect.addFinalizer(() =>
232
+ Ref.get(subscriptions).pipe(
233
+ Effect.flatMap((active) =>
234
+ Effect.forEach(HashMap.values(active), (subscription) =>
235
+ files.releaseGrants(subscription.grantId),
236
+ ),
237
+ ),
238
+ Effect.asVoid,
239
+ ),
240
+ );
231
241
  const invocationFibers = yield* FiberSet.make();
232
- const messageSemaphore = yield* Semaphore.make(1);
233
- const writeSemaphore = yield* Semaphore.make(1);
234
- const write = yield* socket.writer;
235
-
236
- const send = Effect.fn("SyncServer.send")(function* (message: ServerMessage) {
237
- const bound =
238
- message.type === "Result" || message.type === "Preparation" || message.type === "Error"
239
- ? { ...message, sessionEpoch: identity.sessionEpoch }
240
- : message;
241
- yield* writeSemaphore.withPermits(1)(
242
- Schema.encodeEffect(ServerMessageJson)(bound).pipe(Effect.flatMap(write)),
243
- );
244
- });
245
242
 
246
243
  const sendResolutionError = (
247
- operation: Operation,
244
+ operation: string,
248
245
  error:
249
246
  | { readonly _tag: "UnknownFunction"; readonly message: string }
250
247
  | { readonly _tag: "WrongFunctionKind"; readonly message: string }
251
248
  | { readonly _tag: "InvalidArguments"; readonly message: string }
252
249
  | { readonly _tag: "FunctionUnavailable"; readonly message: string },
253
- deliver: (
254
- message: ServerMessage,
255
- ) => Effect.Effect<void, Schema.SchemaError | Socket.SocketError> = send,
250
+ deliver: (message: ManagedServerMessage) => Effect.Effect<void, Socket.SocketError> = send,
256
251
  ) => {
257
- const code: ErrorCode = error._tag;
252
+ const code: LocalErrorCode =
253
+ error._tag === "InvalidArguments" ? "InvalidArguments" : "Unavailable";
258
254
  return deliver(syncError(code, error.message, operation));
259
255
  };
260
256
 
@@ -268,7 +264,7 @@ export const runSession = Effect.fn("SyncServer.runSession")(function* (
268
264
  const sendIfActive = Effect.fn("SyncServer.sendIfActive")(function* (
269
265
  subscriptionId: SubscriptionId,
270
266
  subscription: QuerySubscription,
271
- message: ServerMessage,
267
+ message: ManagedServerMessage,
272
268
  ) {
273
269
  if (yield* isActive(subscriptionId, subscription)) {
274
270
  yield* send(message);
@@ -281,8 +277,9 @@ export const runSession = Effect.fn("SyncServer.runSession")(function* (
281
277
  refreshGeneration: number,
282
278
  previouslyPrepared?: PreparedFunction,
283
279
  ) {
284
- const operation = operationForSubscription(subscriptionId);
285
- const deliver = (message: ServerMessage) => sendIfActive(subscriptionId, subscription, message);
280
+ const operation = subscriptionId;
281
+ const deliver = (message: ManagedServerMessage) =>
282
+ sendIfActive(subscriptionId, subscription, message);
286
283
  let prepared = previouslyPrepared;
287
284
 
288
285
  while (yield* isActive(subscriptionId, subscription)) {
@@ -317,17 +314,12 @@ export const runSession = Effect.fn("SyncServer.runSession")(function* (
317
314
  }
318
315
  dependencyIndex.record(subscriptionId, result.dependencies);
319
316
  if ((yield* invalidation.latestRevision) > result.observedRevision) continue;
320
- const granted = yield* files.grants(subscriptionId, result.files ?? []);
317
+ const granted = yield* files.grants(subscription.grantId, result.files ?? []);
321
318
  const snapshot = {
322
- type: "Snapshot",
319
+ type: "QuerySnapshot",
323
320
  id: subscriptionId,
324
- result: result.result,
325
- revision: result.observedRevision,
326
- identity: {
327
- sessionEpoch: identity.sessionEpoch,
328
- viewRevision: identity.viewRevision,
329
- dependsOnId: result.dependencies.some((dependency) => dependency.type === "Id"),
330
- },
321
+ selection,
322
+ result,
331
323
  } as const;
332
324
  yield* deliver(granted.length === 0 ? snapshot : { ...snapshot, files: granted });
333
325
  return;
@@ -352,19 +344,6 @@ export const runSession = Effect.fn("SyncServer.runSession")(function* (
352
344
  };
353
345
 
354
346
  const refreshAll = Effect.fn("SyncServer.refreshAll")(function* () {
355
- if (refreshIdentity !== undefined) {
356
- const next = yield* refreshIdentity;
357
- const changed =
358
- idViewKey(identity) !== idViewKey(next) || identity.validUntil !== next.validUntil;
359
- identity = next;
360
- if (changed)
361
- yield* send({
362
- type: "Session",
363
- event: "Updated",
364
- session: identity,
365
- serverTime: yield* Clock.currentTimeMillis,
366
- });
367
- }
368
347
  const current = yield* Ref.get(subscriptions);
369
348
  yield* Effect.forEach(HashMap.toEntries(current), ([subscriptionId, subscription]) =>
370
349
  scheduleQuery(subscriptionId, subscription),
@@ -391,9 +370,9 @@ export const runSession = Effect.fn("SyncServer.runSession")(function* (
391
370
  );
392
371
 
393
372
  const handleSubscribe = Effect.fn("SyncServer.handleSubscribe")(function* (
394
- message: Extract<ClientMessage, { readonly type: "Subscribe" }>,
373
+ message: Extract<LocalSessionMessage, { readonly type: "Subscribe" }>,
395
374
  ) {
396
- const operation = operationForSubscription(message.id);
375
+ const operation = message.id;
397
376
  if (encodedFileOccurrencesOf(message.args).length > 0) {
398
377
  yield* send(
399
378
  syncError("InvalidArguments", "Queries cannot take files as arguments.", operation),
@@ -404,7 +383,7 @@ export const runSession = Effect.fn("SyncServer.runSession")(function* (
404
383
 
405
384
  if (HashMap.has(current, message.id)) {
406
385
  yield* send(
407
- syncError("DuplicateOperationId", `Subscription ${message.id} already exists.`, operation),
386
+ syncError("InvalidMessage", `Subscription ${message.id} already exists.`, operation),
408
387
  );
409
388
  return;
410
389
  }
@@ -425,7 +404,8 @@ export const runSession = Effect.fn("SyncServer.runSession")(function* (
425
404
  const subscription = {
426
405
  args: message.args,
427
406
  function: message.function,
428
- queryKey: canonicalQueryKey(localSyncIdentity.deploymentId, message.function, message.args),
407
+ grantId: yield* ids.generate(SubscriptionId),
408
+ queryKey: canonicalQueryKey(selection.versionId, message.function, message.args),
429
409
  refreshGeneration: 0,
430
410
  };
431
411
  yield* Ref.update(subscriptions, HashMap.set(message.id, subscription));
@@ -433,10 +413,10 @@ export const runSession = Effect.fn("SyncServer.runSession")(function* (
433
413
  });
434
414
 
435
415
  const handleInvoke = Effect.fn("SyncServer.handleInvoke")(function* (
436
- message: Extract<ClientMessage, { readonly type: "Invoke" }>,
416
+ message: Extract<LocalSessionMessage, { readonly type: "Invoke" }>,
437
417
  ) {
438
- const operation: Operation = { type: "Invocation", id: message.id };
439
- const admitted = yield* files.admit(message.id, message.function, message.args).pipe(
418
+ const operation = message.invocationId;
419
+ const admitted = yield* files.admit(message.invocationId, message.function, message.args).pipe(
440
420
  Effect.as(true),
441
421
  Effect.catchCause((cause) =>
442
422
  send(
@@ -449,7 +429,7 @@ export const runSession = Effect.fn("SyncServer.runSession")(function* (
449
429
  ),
450
430
  );
451
431
  if (!admitted) {
452
- yield* files.releasePreparation(message.id);
432
+ yield* files.releasePreparation(message.invocationId);
453
433
  return;
454
434
  }
455
435
  const prepared = yield* runtime
@@ -464,12 +444,12 @@ export const runSession = Effect.fn("SyncServer.runSession")(function* (
464
444
  );
465
445
 
466
446
  if (prepared === undefined) {
467
- yield* files.releasePreparation(message.id);
447
+ yield* files.releasePreparation(message.invocationId);
468
448
  return;
469
449
  }
470
450
 
471
451
  const input = canonicalInvocationInput(
472
- localSyncIdentity.deploymentId,
452
+ selection.versionId,
473
453
  "Mutation",
474
454
  message.function,
475
455
  message.args,
@@ -477,7 +457,7 @@ export const runSession = Effect.fn("SyncServer.runSession")(function* (
477
457
  );
478
458
  yield* mutationReplay
479
459
  .execute(
480
- message.id,
460
+ message.invocationId,
481
461
  input,
482
462
  prepared.execute.pipe(
483
463
  Effect.flatMap((result) =>
@@ -487,39 +467,42 @@ export const runSession = Effect.fn("SyncServer.runSession")(function* (
487
467
  )
488
468
  .pipe(
489
469
  Effect.flatMap((result) => {
490
- if (!("committedRevision" in result)) {
491
- return send({ type: "Result", id: message.id, result: result.result });
492
- }
493
470
  const response = send({
494
- type: "Result",
495
- id: message.id,
496
- result: result.result,
497
- committedRevision: result.committedRevision,
471
+ type: "MutationOutcome",
472
+ id: message.invocationId,
473
+ identity: { invocationId: message.invocationId, originalSelection: selection },
474
+ outcome: { type: "Settled", result },
498
475
  });
499
- return response.pipe(
500
- Effect.ensuring(
501
- invalidation.publish({
502
- type: "Dependencies",
503
- invalidations: result.invalidations,
504
- committedRevision: result.committedRevision,
505
- }),
506
- ),
507
- );
476
+ return "committedRevision" in result
477
+ ? response.pipe(
478
+ Effect.ensuring(
479
+ invalidation.publish({
480
+ type: "Dependencies",
481
+ invalidations: result.invalidations,
482
+ committedRevision: result.committedRevision,
483
+ }),
484
+ ),
485
+ )
486
+ : response;
508
487
  }),
509
488
  Effect.catchTags({
510
489
  InvocationIdConflict: () =>
511
490
  send(
512
491
  syncError(
513
492
  "InvocationIdConflict",
514
- `Invocation ${message.id} was already used with different inputs.`,
493
+ `Invocation ${message.invocationId} was already used with different inputs.`,
515
494
  operation,
516
495
  ),
517
496
  ),
518
497
  }),
519
- Effect.ensuring(files.releasePreparation(message.id)),
498
+ Effect.ensuring(files.releasePreparation(message.invocationId)),
520
499
  Effect.catchCause((cause) =>
521
500
  Effect.logError("A mutation invocation fiber failed.").pipe(
522
- Effect.annotateLogs({ cause, function: message.function, requestId: message.id }),
501
+ Effect.annotateLogs({
502
+ cause,
503
+ function: message.function,
504
+ requestId: message.invocationId,
505
+ }),
523
506
  ),
524
507
  ),
525
508
  FiberSet.run(invocationFibers),
@@ -528,12 +511,12 @@ export const runSession = Effect.fn("SyncServer.runSession")(function* (
528
511
  });
529
512
 
530
513
  const handlePrepare = Effect.fn("SyncServer.handlePrepare")(function* (
531
- message: Extract<ClientMessage, { readonly type: "Prepare" }>,
514
+ message: Extract<LocalSessionMessage, { readonly type: "Prepare" }>,
532
515
  ) {
533
- const operation: Operation = { type: "Invocation", id: message.id };
534
- yield* files.prepare(message.id, message.function, message.args, message.files).pipe(
516
+ const operation = message.invocationId;
517
+ yield* files.prepare(message.invocationId, message.function, message.args, message.files).pipe(
535
518
  Effect.flatMap((uploads) =>
536
- send({ type: "Preparation", id: message.id, kind: message.kind, uploads }),
519
+ send({ type: "Preparation", invocationId: message.invocationId, uploads }),
537
520
  ),
538
521
  Effect.catchCause((cause) =>
539
522
  send(
@@ -547,26 +530,19 @@ export const runSession = Effect.fn("SyncServer.runSession")(function* (
547
530
  );
548
531
  });
549
532
 
550
- const handleMessage = Effect.fn("SyncServer.handleMessage")(function* (text: string) {
551
- const message = yield* Schema.decodeEffect(ClientMessageJson)(text).pipe(
552
- Effect.catch(() =>
553
- send(syncError("InvalidMessage", "The WebSocket frame is not valid Ignotum JSON.")),
554
- ),
555
- );
556
-
557
- if (message === undefined) {
558
- return;
559
- }
560
-
533
+ const handle = Effect.fn("LocalSession.handle")(function* (message: LocalSessionMessage) {
561
534
  switch (message.type) {
562
535
  case "Subscribe":
563
536
  yield* handleSubscribe(message);
564
537
  return;
565
- case "Unsubscribe":
538
+ case "Unsubscribe": {
539
+ const current = yield* Ref.get(subscriptions);
540
+ const subscription = Option.getOrUndefined(HashMap.get(current, message.id));
566
541
  dependencyIndex.remove(message.id);
567
542
  yield* Ref.update(subscriptions, HashMap.remove(message.id));
568
- yield* files.releaseGrants(message.id);
543
+ if (subscription !== undefined) yield* files.releaseGrants(subscription.grantId);
569
544
  return;
545
+ }
570
546
  case "Prepare":
571
547
  yield* handlePrepare(message);
572
548
  return;
@@ -575,31 +551,11 @@ export const runSession = Effect.fn("SyncServer.runSession")(function* (
575
551
  return;
576
552
  }
577
553
  });
578
-
579
- yield* socket
580
- .runString((text) => messageSemaphore.withPermits(1)(handleMessage(text)), {
581
- onOpen: send({
582
- type: "Handshake",
583
- ...localSyncIdentity,
584
- session: identity,
585
- serverTime: yield* Clock.currentTimeMillis,
586
- }).pipe(Effect.orDie),
587
- })
588
- .pipe(
589
- Effect.ensuring(
590
- Ref.get(subscriptions).pipe(
591
- Effect.flatMap((active) =>
592
- Effect.forEach(HashMap.keys(active), (subscriptionId) =>
593
- files.releaseGrants(subscriptionId),
594
- ),
595
- ),
596
- Effect.asVoid,
597
- ),
598
- ),
599
- );
554
+ return { handle };
600
555
  });
601
556
 
602
557
  interface SyncHandlersService {
558
+ readonly invalidate: Effect.Effect<void>;
603
559
  readonly http: (request: IncomingMessage, response: ServerResponse) => void;
604
560
  readonly upgrade: NodeJS.EventEmitter extends infer _EventEmitter
605
561
  ? (request: IncomingMessage, socket: import("node:stream").Duplex, head: Buffer) => void
@@ -611,7 +567,17 @@ class SyncHandlers extends Context.Service<SyncHandlers, SyncHandlersService>()(
611
567
  "ignotum/dev-runtime/sync/SyncHandlers",
612
568
  ) {}
613
569
 
614
- const makeHandlersLayer = (server: ViteDevServer, appDirectory: string, databasePath: string) => {
570
+ type LocalDevSyncOptions = ManagedSyncOptions & {
571
+ readonly loadModule: ViteDevServer["ssrLoadModule"];
572
+ };
573
+
574
+ const makeHandlersLayer = (
575
+ appDirectory: string,
576
+ databasePath: string,
577
+ managed: LocalDevSyncOptions,
578
+ ) => {
579
+ const transportPath = managedSyncPath;
580
+ const loader = { ssrLoadModule: managed.loadModule };
615
581
  const sqliteLayer = SqliteClient.layer({ filename: databasePath });
616
582
  const persistenceLayer = Layer.merge(LocalDatabase.layer, mutationReplayLayer).pipe(
617
583
  Layer.provideMerge(DevelopmentDatabase.layer),
@@ -621,14 +587,14 @@ const makeHandlersLayer = (server: ViteDevServer, appDirectory: string, database
621
587
  Layer.provide(persistenceLayer),
622
588
  Layer.provide(IdGenerator.layer),
623
589
  );
624
- const environmentLayer = DevelopmentEnvironment.layer(server, appDirectory).pipe(
590
+ const environmentLayer = DevelopmentEnvironment.layer(loader, appDirectory).pipe(
625
591
  Layer.provide(NodeServices.layer),
626
592
  Layer.orDie,
627
593
  );
628
594
  const devFunctionRuntimeLayer = functionRuntimeLayer.pipe(
629
595
  Layer.provide(
630
596
  Layer.merge(
631
- FunctionRegistry.layer(server, appDirectory).pipe(
597
+ FunctionRegistry.layer(loader, appDirectory).pipe(
632
598
  Layer.provide(Layer.merge(NodeServices.layer, environmentLayer)),
633
599
  ),
634
600
  executorLayer,
@@ -640,6 +606,8 @@ const makeHandlersLayer = (server: ViteDevServer, appDirectory: string, database
640
606
  environmentLayer,
641
607
  queryInvalidationLayer,
642
608
  persistenceLayer,
609
+ sqliteLayer,
610
+ IdGenerator.layer,
643
611
  NodeServices.layer,
644
612
  LocalId.layer(appDirectory).pipe(Layer.provide(NodeServices.layer)),
645
613
  localApplicationFilesLayer(appDirectory).pipe(
@@ -778,31 +746,32 @@ const makeHandlersLayer = (server: ViteDevServer, appDirectory: string, database
778
746
  : HttpServerResponse.uint8Array(granted.value.bytes, { headers });
779
747
  }
780
748
  return yield* HttpServerResponse.json(
781
- pathname === syncPath
782
- ? { code: "UpgradeRequired", message: `Connect to ${syncPath} with WebSocket.` }
749
+ pathname === transportPath
750
+ ? { code: "UpgradeRequired", message: `Connect to ${transportPath} with WebSocket.` }
783
751
  : { code: "NotFound", message: "Unknown Ignotum API route." },
784
- { status: pathname === syncPath ? 426 : 404 },
785
- );
786
- });
787
- const socketApp = Effect.gen(function* () {
788
- const request = yield* HttpServerRequest.HttpServerRequest;
789
- const url = new URL(request.url, `http://${request.headers.host ?? "ignotum.local"}`);
790
- if (request.headers.origin !== url.origin) return HttpServerResponse.empty({ status: 403 });
791
- const refresh = localId.session(
792
- url.searchParams.get("username"),
793
- url.searchParams.get("epoch") ?? "dev:anonymous",
752
+ { status: pathname === transportPath ? 426 : 404 },
794
753
  );
795
- const identity = yield* refresh;
796
- const socket = yield* request.upgrade;
797
- yield* runSession(socket, identity, refresh);
798
- return HttpServerResponse.empty();
799
754
  });
800
755
  const http = yield* NodeHttpServer.makeHandler(httpApp, { scope });
801
- const upgrade = yield* NodeHttpServer.makeUpgradeHandler(
802
- Effect.succeed(webSocketServer),
803
- socketApp,
804
- { scope },
805
- );
756
+ const upgrade = yield* Layer.build(
757
+ ManagedWebSocket.layer(
758
+ webSocketServer,
759
+ Effect.fn("ManagedSyncServer.session")(function* (
760
+ socket: Socket.Socket,
761
+ request: IncomingMessage,
762
+ ) {
763
+ const url = new URL(
764
+ request.url ?? "/",
765
+ `http://${request.headers.host ?? "ignotum.local"}`,
766
+ );
767
+ const refresh = localId.session(
768
+ url.searchParams.get("username"),
769
+ url.searchParams.get("epoch") ?? "dev:anonymous",
770
+ );
771
+ yield* runManagedSession(socket, managed, yield* refresh, refresh);
772
+ }),
773
+ ),
774
+ ).pipe(Effect.map((context) => Context.get(context, ManagedWebSocket).upgrade));
806
775
  const serverDirectory = path.join(appDirectory, "server");
807
776
  const environmentPath = path.join(appDirectory, ".env.ignotum");
808
777
  const reloadUnsafe = Effect.fn("SyncServer.reload")(function* (
@@ -813,10 +782,19 @@ const makeHandlersLayer = (server: ViteDevServer, appDirectory: string, database
813
782
  const relative = path.relative(serverDirectory, file);
814
783
  const isServerTypeScript =
815
784
  !relative.startsWith("..") && !path.isAbsolute(relative) && file.endsWith(".ts");
816
- if (!isEnvironmentValues && !isServerTypeScript) {
785
+ if (
786
+ !isEnvironmentValues &&
787
+ !isServerTypeScript &&
788
+ managed.isServerDependency?.(file) !== true
789
+ ) {
817
790
  return;
818
791
  }
819
792
 
793
+ if (managed.beforeReload !== undefined)
794
+ yield* Effect.tryPromise(() => managed.beforeReload!(file, event)).pipe(
795
+ Effect.onError(() => invalidation.publish({ type: "All" })),
796
+ );
797
+
820
798
  if (isServerTypeScript && event !== "change") {
821
799
  yield* generate(appDirectory).pipe(
822
800
  Effect.provideService(FileSystem.FileSystem, fileSystem),
@@ -843,7 +821,12 @@ const makeHandlersLayer = (server: ViteDevServer, appDirectory: string, database
843
821
  ),
844
822
  );
845
823
 
846
- return SyncHandlers.of({ http, reload, upgrade });
824
+ return SyncHandlers.of({
825
+ http,
826
+ reload,
827
+ upgrade,
828
+ invalidate: invalidation.publish({ type: "All" }),
829
+ });
847
830
  }),
848
831
  ).pipe(Layer.provide(dependencies));
849
832
  };
@@ -870,60 +853,77 @@ const rejectUpgrade = (socket: import("node:stream").Duplex): void => {
870
853
 
871
854
  export const syncPluginInternals = { isAppOrigin, reloadEnvironmentAndInvalidate };
872
855
 
873
- export const ignotumSyncPlugin: {
874
- (databasePath: string): (appDirectory: string) => Plugin;
875
- (appDirectory: string, databasePath: string): Plugin;
876
- } = Function.dual(2, (appDirectory: string, databasePath: string): Plugin => ({
877
- name: "ignotum:sync",
878
- configureServer: (server) => {
879
- if (server.httpServer === null) {
880
- throw new Error("Ignotum sync requires Vite's Node HTTP server.");
881
- }
856
+ const createSyncPlugin = (
857
+ appDirectory: string,
858
+ databasePath: string,
859
+ managed: LocalDevSyncOptions,
860
+ ): Plugin & { api: { invalidate: () => Promise<void>; dispose: () => Promise<void> } } => {
861
+ const api = {
862
+ invalidate: (): Promise<void> => Promise.resolve(),
863
+ dispose: (): Promise<void> => Promise.resolve(),
864
+ };
865
+ return {
866
+ api,
867
+ name: "ignotum:sync",
868
+ configureServer: (server) => {
869
+ if (server.httpServer === null) {
870
+ throw new Error("Ignotum sync requires Vite's Node HTTP server.");
871
+ }
882
872
 
883
- const runtime = ManagedRuntime.make(makeHandlersLayer(server, appDirectory, databasePath));
884
- const httpServer = server.httpServer;
885
- return runtime.runPromise(SyncHandlers).then(
886
- (handlers) => {
887
- const onUpgrade = (
888
- request: IncomingMessage,
889
- socket: import("node:stream").Duplex,
890
- head: Buffer,
891
- ) => {
892
- if (new URL(request.url ?? "/", "http://ignotum.local").pathname !== syncPath) return;
893
- if (!isAppOrigin(request.headers.host, request.headers.origin)) {
894
- rejectUpgrade(socket);
895
- return;
896
- }
897
- handlers.upgrade(request, socket, head);
898
- };
899
- const onAdd = (file: string) => runtime.runFork(handlers.reload(file, "add"));
900
- const onChange = (file: string) => runtime.runFork(handlers.reload(file, "change"));
901
- const onUnlink = (file: string) => runtime.runFork(handlers.reload(file, "unlink"));
902
- const cleanup = () => {
903
- httpServer.off("upgrade", onUpgrade);
904
- server.watcher.off("add", onAdd);
905
- server.watcher.off("change", onChange);
906
- server.watcher.off("unlink", onUnlink);
873
+ const runtime = ManagedRuntime.make(makeHandlersLayer(appDirectory, databasePath, managed));
874
+ api.dispose = () => runtime.dispose();
875
+ const httpServer = server.httpServer;
876
+ return runtime.runPromise(SyncHandlers).then(
877
+ (handlers) => {
878
+ api.invalidate = () => runtime.runPromise(handlers.invalidate);
879
+ const onUpgrade = (
880
+ request: IncomingMessage,
881
+ socket: import("node:stream").Duplex,
882
+ head: Buffer,
883
+ ) => {
884
+ if (new URL(request.url ?? "/", "http://ignotum.local").pathname !== managedSyncPath)
885
+ return;
886
+ if (!isAppOrigin(request.headers.host, request.headers.origin)) {
887
+ rejectUpgrade(socket);
888
+ return;
889
+ }
890
+ handlers.upgrade(request, socket, head);
891
+ };
892
+ const onAdd = (file: string) => runtime.runFork(handlers.reload(file, "add"));
893
+ const onChange = (file: string) => runtime.runFork(handlers.reload(file, "change"));
894
+ const onUnlink = (file: string) => runtime.runFork(handlers.reload(file, "unlink"));
895
+ const cleanup = () => {
896
+ httpServer.off("upgrade", onUpgrade);
897
+ server.watcher.off("add", onAdd);
898
+ server.watcher.off("change", onChange);
899
+ server.watcher.off("unlink", onUnlink);
900
+ void runtime.dispose();
901
+ };
902
+
903
+ httpServer.on("upgrade", onUpgrade);
904
+ httpServer.once("close", cleanup);
905
+ server.watcher.on("add", onAdd);
906
+ server.watcher.on("change", onChange);
907
+ server.watcher.on("unlink", onUnlink);
908
+ server.middlewares.use((request, response, next) => {
909
+ if (!isApiPath(request)) {
910
+ next();
911
+ return;
912
+ }
913
+ handlers.http(request, response);
914
+ });
915
+ },
916
+ (error) => {
907
917
  void runtime.dispose();
908
- };
909
-
910
- httpServer.on("upgrade", onUpgrade);
911
- httpServer.once("close", cleanup);
912
- server.watcher.on("add", onAdd);
913
- server.watcher.on("change", onChange);
914
- server.watcher.on("unlink", onUnlink);
915
- server.middlewares.use((request, response, next) => {
916
- if (!isApiPath(request)) {
917
- next();
918
- return;
919
- }
920
- handlers.http(request, response);
921
- });
922
- },
923
- (error) => {
924
- void runtime.dispose();
925
- throw error;
926
- },
927
- );
928
- },
929
- }));
918
+ throw error;
919
+ },
920
+ );
921
+ },
922
+ };
923
+ };
924
+
925
+ export const ignotumManagedSyncPlugin = (
926
+ appDirectory: string,
927
+ databasePath: string,
928
+ options: LocalDevSyncOptions,
929
+ ) => createSyncPlugin(appDirectory, databasePath, options);