@volter/world-core 3.0.155 → 3.0.157

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 (150) hide show
  1. package/attach.cjs +2 -0
  2. package/capability-decision.cjs +6 -4
  3. package/dist/attach.cjs +2 -0
  4. package/dist/capability-decision.cjs +6 -4
  5. package/dist/generated/pack-facts.json +204 -4
  6. package/dist/inject.cjs +150 -86
  7. package/dist/network-policy.cjs +11 -1
  8. package/dist/network-policy.d.cts +1 -0
  9. package/dist/pack-facts.d.cts +3 -0
  10. package/dist/required-credential-transport.cjs +29 -0
  11. package/dist/required-credential-transport.d.cts +3 -0
  12. package/dist/src/actions.d.ts +15 -0
  13. package/dist/src/actions.js +82 -21
  14. package/dist/src/ancestry.d.ts +5 -4
  15. package/dist/src/ancestry.js +53 -28
  16. package/dist/src/blob-store.d.ts +16 -0
  17. package/dist/src/blob-store.js +76 -1
  18. package/dist/src/changeset.js +1 -1
  19. package/dist/src/clickhouse/compression.d.ts +28 -0
  20. package/dist/src/clickhouse/compression.js +281 -0
  21. package/dist/src/clickhouse/index.d.ts +1 -0
  22. package/dist/src/clickhouse/index.js +1 -0
  23. package/dist/src/derived-core.d.ts +84 -7
  24. package/dist/src/derived-core.js +336 -61
  25. package/dist/src/derived-real.js +97 -15
  26. package/dist/src/derived.d.ts +5 -0
  27. package/dist/src/derived.js +3 -0
  28. package/dist/src/executor.d.ts +7 -0
  29. package/dist/src/executor.js +15 -1
  30. package/dist/src/fork.js +1 -1
  31. package/dist/src/git/lfs.d.ts +13 -3
  32. package/dist/src/git/lfs.js +41 -9
  33. package/dist/src/grpc-wire.js +2 -1
  34. package/dist/src/h2.js +63 -10
  35. package/dist/src/head.d.ts +5 -0
  36. package/dist/src/head.js +15 -0
  37. package/dist/src/history.d.ts +1 -1
  38. package/dist/src/history.js +7 -7
  39. package/dist/src/index.d.ts +5 -1
  40. package/dist/src/index.js +6 -1
  41. package/dist/src/live/ingest.js +9 -13
  42. package/dist/src/live/rtmp.js +7 -5
  43. package/dist/src/live/segmenter.js +7 -1
  44. package/dist/src/log.d.ts +23 -3
  45. package/dist/src/log.js +160 -37
  46. package/dist/src/machines.js +5 -1
  47. package/dist/src/managed-go-programs.d.ts +55 -0
  48. package/dist/src/managed-go-programs.js +133 -0
  49. package/dist/src/managed-original-inputs.d.ts +101 -0
  50. package/dist/src/managed-original-inputs.js +125 -0
  51. package/dist/src/mongodb/client.d.ts +8 -0
  52. package/dist/src/mongodb/client.js +27 -0
  53. package/dist/src/mongodb/index.d.ts +2 -0
  54. package/dist/src/mongodb/index.js +5 -0
  55. package/dist/src/mongodb/stream.d.ts +97 -0
  56. package/dist/src/mongodb/stream.js +206 -0
  57. package/dist/src/observe.js +1 -1
  58. package/dist/src/pack-fetch.d.ts +6 -1
  59. package/dist/src/pack-fetch.js +69 -14
  60. package/dist/src/pack-paths.d.ts +2 -1
  61. package/dist/src/pack-paths.js +6 -1
  62. package/dist/src/packRegistry.d.ts +10 -14
  63. package/dist/src/packRegistry.js +2 -13
  64. package/dist/src/protocol.d.ts +13 -0
  65. package/dist/src/protocol.js +13 -0
  66. package/dist/src/redis/engine.d.ts +10 -4
  67. package/dist/src/redis/engine.js +103 -32
  68. package/dist/src/redis/member-storage.js +20 -8
  69. package/dist/src/redis/service-times.d.ts +25 -0
  70. package/dist/src/redis/service-times.js +87 -0
  71. package/dist/src/redis/stream.d.ts +2 -1
  72. package/dist/src/redis/stream.js +109 -20
  73. package/dist/src/resource-blob.d.ts +4 -0
  74. package/dist/src/resource-blob.js +8 -1
  75. package/dist/src/runtime-state.d.ts +11 -0
  76. package/dist/src/runtime-state.js +13 -0
  77. package/dist/src/runtime.d.ts +7 -5
  78. package/dist/src/runtime.js +7 -4
  79. package/dist/src/serve-http.d.ts +2 -0
  80. package/dist/src/serve-http.js +73 -6
  81. package/dist/src/serve.d.ts +10 -0
  82. package/dist/src/serve.js +43 -3
  83. package/dist/src/sockets.d.ts +4 -1
  84. package/dist/src/sockets.js +59 -6
  85. package/dist/src/storage.d.ts +2 -1
  86. package/dist/src/storage.js +6 -3
  87. package/dist/src/vendor-call.js +58 -0
  88. package/dist/src/world-boot.d.ts +1 -0
  89. package/dist/src/world-boot.js +6 -0
  90. package/dist/src/world-clock.js +6 -2
  91. package/dist/src/world-store.d.ts +122 -7
  92. package/dist/src/world-store.js +959 -103
  93. package/dist/src/xml.d.ts +30 -0
  94. package/dist/src/xml.js +74 -0
  95. package/generated/pack-facts.json +204 -4
  96. package/inject.cjs +150 -86
  97. package/network-policy.cjs +11 -1
  98. package/network-policy.d.cts +1 -0
  99. package/pack-facts.d.cts +3 -0
  100. package/package.json +27 -1
  101. package/required-credential-transport.cjs +29 -0
  102. package/required-credential-transport.d.cts +3 -0
  103. package/src/actions.ts +78 -21
  104. package/src/ancestry.ts +46 -24
  105. package/src/blob-store.ts +59 -1
  106. package/src/changeset.ts +1 -1
  107. package/src/clickhouse/compression.ts +172 -0
  108. package/src/clickhouse/index.ts +2 -0
  109. package/src/derived-core.ts +356 -61
  110. package/src/derived-real.ts +80 -14
  111. package/src/derived.ts +9 -1
  112. package/src/executor.ts +17 -3
  113. package/src/fork.ts +1 -1
  114. package/src/git/lfs.ts +40 -9
  115. package/src/grpc-wire.ts +2 -1
  116. package/src/h2.ts +46 -5
  117. package/src/head.ts +15 -0
  118. package/src/history.ts +7 -7
  119. package/src/index.ts +7 -1
  120. package/src/live/ingest.ts +7 -3
  121. package/src/live/rtmp.ts +7 -5
  122. package/src/live/segmenter.ts +7 -1
  123. package/src/log.ts +147 -32
  124. package/src/machines.ts +5 -1
  125. package/src/managed-go-programs.ts +146 -0
  126. package/src/managed-original-inputs.ts +163 -0
  127. package/src/mongodb/client.ts +27 -0
  128. package/src/mongodb/index.ts +5 -0
  129. package/src/mongodb/stream.ts +223 -0
  130. package/src/observe.ts +1 -1
  131. package/src/pack-fetch.ts +66 -16
  132. package/src/pack-paths.ts +5 -1
  133. package/src/packRegistry.ts +3 -15
  134. package/src/protocol.ts +13 -0
  135. package/src/redis/engine.ts +100 -57
  136. package/src/redis/member-storage.ts +17 -9
  137. package/src/redis/service-times.ts +81 -0
  138. package/src/redis/stream.ts +66 -14
  139. package/src/resource-blob.ts +9 -1
  140. package/src/runtime-state.ts +14 -0
  141. package/src/runtime.ts +10 -3
  142. package/src/serve-http.ts +65 -6
  143. package/src/serve.ts +45 -3
  144. package/src/sockets.ts +39 -8
  145. package/src/storage.ts +5 -3
  146. package/src/vendor-call.ts +54 -0
  147. package/src/world-boot.ts +6 -0
  148. package/src/world-clock.ts +6 -2
  149. package/src/world-store.ts +768 -91
  150. package/src/xml.ts +71 -0
package/src/runtime.ts CHANGED
@@ -157,10 +157,13 @@ export {
157
157
  setActiveBlobStore,
158
158
  withBlobStore,
159
159
  readBlobRange,
160
+ streamChunks,
161
+ writeBlobStream,
160
162
  } from './blob-store.ts';
161
- export type { BlobStore } from './blob-store.ts';
163
+ export type { BlobStore, BlobWritten } from './blob-store.ts';
162
164
  export {
163
165
  applyTwinWrite,
166
+ applyTwinWrites,
164
167
  applyTwinWriteAtomic,
165
168
  createTwinServer,
166
169
  CALLER_HEADER,
@@ -297,6 +300,8 @@ export { installedPackageDir, installedVendorPacks, packVendor, packRepository,
297
300
  export * as git from './git/index.ts';
298
301
  // S3's wire (contract "S3's wire is a kernel library too"): its XML, CORS, lifecycle, delete batches, ETags, body bytes.
299
302
  export * as s3 from './s3/wire.ts';
303
+ // Generic strict XML grammar; vendor interpretation remains in its consumer.
304
+ export * as xml from './xml.ts';
300
305
 
301
306
  // The push arm: the kernel's transaction over a pack's one `perform<Name>Action` (contract section of that name).
302
307
 
@@ -323,7 +328,7 @@ export { readTreeMap, readParentTreeMap, treeStamp, treeChangesSince, originEntr
323
328
 
324
329
  export { referenceField, registerReferences, packReferences, resolveReferences, type ReferenceDeclaration } from './references.ts';
325
330
  // the HTTP server seam: the one place the serve path meets the runtime (Bun or Node)
326
- export { serveHttp, serveStream, connectWebSocket, nodeBuiltin, WORLD_BOOT_PATH, type HttpServer, type ServeHttpOptions, type HttpHandler } from './serve-http.ts';
331
+ export { serveHttp, serveStream, connectWebSocket, nodeBuiltin, servedHandlerOf, WORLD_BOOT_PATH, type HttpServer, type ServeHttpOptions, type HttpHandler } from './serve-http.ts';
327
332
  // two runtime-neutral helpers for a serve path: a file as a Response, a page's client bundle
328
333
  export { fileResponse, contentTypeOf } from './file-response.ts';
329
334
  export { bundleClient, filePathOf } from './client-bundle.ts';
@@ -335,7 +340,7 @@ export { corsHeaders, withCors, type CorsDecl } from './cors.ts';
335
340
  export { setInProcessTwins, vendorFetch, VendorUnreachableError, type InProcessTwin } from './vendor-call.ts';
336
341
  export { closeManagedDatabases, databaseSession, EngineDatabaseError, EngineUnavailableError, managedDatabase, quoteIdent, useDatabaseDriver, type DatabaseDriver, type DatabaseSession } from './managed-database.ts';
337
342
  export type { Batch as EngineBatch, ManagedDatabase, Statement as EngineStatement, StatementResult as EngineStatementResult } from './managed-database.ts';
338
- export { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobFile, resourceBlobKey, resourceBlobSize, resourceChain, writeResourceBlob } from './resource-blob.ts';
343
+ export { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobFile, resourceBlobKey, resourceBlobSize, resourceChain, writeResourceBlob, writeResourceBlobStream } from './resource-blob.ts';
339
344
 
340
345
  export { assertStateRemovable, checkParent, stateGeneration, withAncestryLock, withHistoryLock, withHistoryLocks, withStateLock, withStateRemoval } from './ancestry.ts';
341
346
 
@@ -347,3 +352,5 @@ export type { MachinePool, MachineSpec, MachineProcess, MachineStatus, MachineFi
347
352
 
348
353
 
349
354
  export { boardFrameKey } from './board-keys.ts';
355
+
356
+ export * as managedOriginalInputs from './managed-original-inputs.ts';
package/src/serve-http.ts CHANGED
@@ -4,11 +4,12 @@
4
4
  // as a standard Request and the handler's Response written back. Async, because binding a port is
5
5
  // asynchronous on Node — Bun's synchronous bind is the special case.
6
6
  //
7
- // Nothing here runs at module scope, and `node:http` is reached through `process.getBuiltinModule`
7
+ // Nothing here runs at module scope (the served-handler registry is made on first use), and `node:http` is reached through `process.getBuiltinModule`
8
8
  // rather than a static import: this module rides into browser client bundles through the kernel's
9
9
  // entrypoint, where a static `node:` import is the 2026-09-06 class of break.
10
10
 
11
11
  import type { TwinStream } from './twin-fetch.ts';
12
+ import { worldBootAnswer } from './world-boot.ts';
12
13
 
13
14
  /** One socket library at the host seam, shared by inbound upgrades and authenticated outbound relays. */
14
15
  const socketLibrary = () => import('ws');
@@ -64,6 +65,24 @@ const bunRuntime = (): BunLike | undefined => { const b = (globalThis as { Bun?:
64
65
  /** The path a World asks a service it booted, when that service's port answers, which boot it belongs to. */
65
66
  export const WORLD_BOOT_PATH = '/__volter/world-boot';
66
67
 
68
+ /** Each listening server's own handler, by its port, as the seam serves it (the boot door answered, nothing journaled):
69
+ * the World's co-located host warms a twin in-process through it (world-runtime host.ts warmInProcess), so its warm-up is
70
+ * never a request on the wire nor in any journal. A server leaves it when it stops. Kept on globalThis under a registered
71
+ * symbol, as the reference registry is (references.ts), so a pack that resolved its own copy of this module shares it. */
72
+ const servedHandlers = (): Map<number, HttpHandler> =>
73
+ ((globalThis as Record<symbol, unknown>)[Symbol.for('volter.servedHandlers')] ??= new Map<number, HttpHandler>()) as Map<number, HttpHandler>;
74
+
75
+ /** The handler serving `port` in this process, when a server made through the seam listens there. */
76
+ export function servedHandlerOf(port: number): HttpHandler | undefined { return servedHandlers().get(port); }
77
+
78
+ /** A server's handler kept by its port until the server stops. */
79
+ function keepHandler(server: HttpServer, handler: HttpHandler): HttpServer {
80
+ const port = server.port;
81
+ if (!port) return server;
82
+ servedHandlers().set(port, handler);
83
+ return { ...server, stop: async (closeActive?: boolean) => { if (servedHandlers().get(port) === handler) servedHandlers().delete(port); await server.stop(closeActive); } };
84
+ }
85
+
67
86
  export async function serveHttp(options: ServeHttpOptions): Promise<HttpServer> {
68
87
  // Inside a World every server made through the seam answers which boot it belongs to, so the World can tell its own
69
88
  // twin from a leftover World's that holds the same port. Outside a World nothing changes.
@@ -76,7 +95,7 @@ export async function serveHttp(options: ServeHttpOptions): Promise<HttpServer>
76
95
  const worldBoot = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.VOLTER_WORLD_BOOT_ID;
77
96
  if (worldBoot) {
78
97
  const served = options.fetch;
79
- options = { ...options, fetch: (request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ? Response.json({ boot: worldBoot }) : served(request)) };
98
+ options = { ...options, fetch: (request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ? worldBootAnswer(worldBoot) : served(request)) };
80
99
  }
81
100
  // loopback by default: an unnamed interface is never every interface
82
101
  const hostname = options.hostname ?? '127.0.0.1';
@@ -86,6 +105,28 @@ export async function serveHttp(options: ServeHttpOptions): Promise<HttpServer>
86
105
  // the global `Bun.serve` — the request journal's wrap of it (serve.ts) applies to packs and to
87
106
  // this seam alike, so the seam never decorates twice on Bun
88
107
  const upgrade = options.upgrade;
108
+ // the sockets open now, by the seam's own count: Bun keeps a socket its server closed (a refused key's 1008, from
109
+ // `open` or later) in `pendingWebSockets` after its close handler ran, and `stop()`'s promise then never settles
110
+ // (measured on Bun 1.3.11, macOS arm64, alone: a Bun.serve whose open() calls ws.close(1008) leaves
111
+ // pendingWebSockets at 1 with its close handler called once, and an awaited stop(true) had not settled after 3 s;
112
+ // unawaited, the listener closed and the process used 4 ms of CPU over the next 2 s; a client-initiated close
113
+ // stops in 0 ms). Conform's life hung on it after OpenAI's keyless Realtime step.
114
+ // and the requests whose handlers have not answered: a stop that drops connections (closeActive) aborts each one's
115
+ // signal, and Bun's stop still waits for the handler to settle, which a handler that never answers never does
116
+ // (measured on Bun 1.3.11, macOS arm64, alone: a Bun.serve whose fetch never resolves, its request's signal aborted
117
+ // by stop(true), pendingRequests 1, the stop unsettled after 3 s). Conform's client check hung on a pack served with
118
+ // a scenario that answered nothing. Node's path below reports such a handler as cleanup and resolves; so does this one.
119
+ const live = new Set<unknown>();
120
+ const inflight = new Set<Request>();
121
+ const served = options.fetch;
122
+ const tracked = (request: Request, ...rest: unknown[]): Response | Promise<Response> => {
123
+ inflight.add(request);
124
+ let answer: Response | Promise<Response>;
125
+ try { answer = (served as (r: Request, ...x: unknown[]) => Response | Promise<Response>)(request, ...rest); } catch (error) { inflight.delete(request); throw error; }
126
+ if (!(answer instanceof Promise)) { inflight.delete(request); return answer; }
127
+ return answer.finally(() => { inflight.delete(request); });
128
+ };
129
+ options = { ...options, fetch: tracked };
89
130
  const server = bun.serve({ ...options, hostname, port: options.port ?? 0,
90
131
  ...(upgrade ? {
91
132
  fetch: (request: Request, server: { upgrade: (request: Request, options: { data: { request: Request } }) => boolean }) => {
@@ -95,16 +136,34 @@ export async function serveHttp(options: ServeHttpOptions): Promise<HttpServer>
95
136
  return options.fetch(request);
96
137
  },
97
138
  websocket: {
98
- open: (peer: WebSocketPeer & { data: { request: Request } }) => upgrade.open(peer, peer.data.request),
139
+ open: (peer: WebSocketPeer & { data: { request: Request } }) => { live.add(peer); return upgrade.open(peer, peer.data.request); },
99
140
  message: (peer: WebSocketPeer, data: string | Uint8Array) => upgrade.message(peer, data),
100
- close: upgrade.close,
141
+ close: (peer: WebSocketPeer, ...rest: unknown[]) => { live.delete(peer); return (upgrade.close as (p: WebSocketPeer, ...r: unknown[]) => unknown)(peer, ...rest); },
101
142
  },
102
143
  } : {}),
103
144
  });
104
145
  const port = server.port ?? 0;
105
- return { hostname, port, url: server.url instanceof URL ? server.url : new URL(`http://${urlHost}:${port}/`), stop: async (closeActive = true) => { await server.stop(closeActive); } };
146
+ return keepHandler({ hostname, port, url: server.url instanceof URL ? server.url : new URL(`http://${urlHost}:${port}/`), stop: async (closeActive = true) => {
147
+ const stopping = Promise.resolve(server.stop(closeActive));
148
+ // stop closes the sockets still open (closeActive), each a close Bun may then keep counting, and drops the
149
+ // connections of handlers still running: once every socket the seam opened has closed and every handler left is
150
+ // one whose request was dropped, Bun's stop would never settle on them (above), and the seam has stopped
151
+ let settled = false;
152
+ stopping.then(() => { settled = true; }, () => { settled = true; });
153
+ while (!settled) {
154
+ const orphaned = [...inflight].every((request) => request.signal.aborted);
155
+ const counted = ((server as { pendingWebSockets?: number }).pendingWebSockets ?? 0) > 0;
156
+ if (live.size === 0 && orphaned && (counted || (closeActive && inflight.size > 0))) {
157
+ stopping.catch(() => {});
158
+ if (inflight.size) console.error(`serveHttp: forced stop left pending handler cleanup: ${inflight.size} request(s)`);
159
+ return;
160
+ }
161
+ await new Promise((resolve) => setTimeout(resolve, 10));
162
+ }
163
+ await stopping;
164
+ } }, options.fetch);
106
165
  }
107
- return serveOnNode({ ...options, hostname, urlHost, fetch: decorate ? decorate(options) : options.fetch });
166
+ return keepHandler(await serveOnNode({ ...options, hostname, urlHost, fetch: decorate ? decorate(options) : options.fetch }), options.fetch);
108
167
  }
109
168
 
110
169
  type NodeHttp = typeof import('node:http');
package/src/serve.ts CHANGED
@@ -12,13 +12,14 @@ import { parseExactJson } from './exact-json.ts';
12
12
  // reality). Forking is a separate data operation (fork.ts), never a serve mode.
13
13
  import { AsyncLocalStorage } from 'node:async_hooks';
14
14
  import { serveHttp, setServeDecorator, WORLD_BOOT_PATH, type HttpHandler } from './serve-http.ts';
15
- import { performAtHead } from './head.ts';
15
+ import { performAtHead, performsAtHeadNow } from './head.ts';
16
16
  import { withHeaders } from './with-headers.ts';
17
17
  import { createHash } from 'node:crypto';
18
18
  import { dirname, join } from 'node:path';
19
19
  import type { SubjectFields } from './hash.ts';
20
20
  import { hashFieldValue } from './hash.ts';
21
- import { appendActionIfAbsent, appendActionOccurrence, decideAndAppendAction, projectResources } from './actions.ts';
21
+ import { appendActionIfAbsent, appendActionOccurrence, appendActionOccurrences, decideAndAppendAction, projectResources } from './actions.ts';
22
+ import { readSubject } from './log.ts';
22
23
  import type { ActionProjection, TwinAction, TwinActionPrecondition } from './actions.ts';
23
24
  import { observeWorldPaths, worldPaths } from './storage.ts';
24
25
  import { getActiveWorldStore } from './world-store.ts';
@@ -699,12 +700,53 @@ export async function applyTwinWrite(
699
700
  return {
700
701
  result,
701
702
  get resource(): TwinResource {
702
- return resolved ??= projectResources(service, root).find((r) => r.type === write.subjectType && r.id === answerId)
703
+ return resolved ??= readSubject(service, write.subjectType, answerId, root)
703
704
  ?? ({ id: answerId, type: write.subjectType, updatedAt: landed.occurredAt, ...write.fields } as TwinResource);
704
705
  },
705
706
  };
706
707
  }
707
708
 
709
+ /**
710
+ * Several local writes of one service, landed as `applyTwinWrite` lands each called in turn — the same actions, ids and
711
+ * order — under one lock and one append (actions.ts `appendActionOccurrences`). Writes that need the one-at-a-time
712
+ * path take it: a caller's identity (`idempotencyKey`/`actionId`), preconditions, or a head that performs each entry as
713
+ * it lands. An input is offloaded once per distinct value, as each write's own offload would name it.
714
+ */
715
+ export async function applyTwinWrites(
716
+ service: string,
717
+ writes: readonly TwinWriteInput[],
718
+ root?: string,
719
+ ): Promise<Array<{ result: TwinWriteResult; resource: TwinResource }>> {
720
+ if (writes.length <= 1 || performsAtHeadNow(service, root) || writes.some((write) => write.idempotencyKey || write.actionId || write.preconditions?.length)) {
721
+ const out: Array<{ result: TwinWriteResult; resource: TwinResource }> = [];
722
+ for (const write of writes) out.push(await applyTwinWrite(service, write, root));
723
+ return out;
724
+ }
725
+ // Each distinct input offloaded once (S2 R17): `offloadInput` is content-addressed (a string's blob is named by its
726
+ // SHA-256), so the result is the one each write's own offload would give, and a command's writes, which share one
727
+ // recorded request, hash and look up its blob once instead of once per write.
728
+ const offloaded = new Map<unknown, unknown>();
729
+ for (const write of writes) if (write.input !== undefined && !offloaded.has(write.input)) offloaded.set(write.input, await offloadInput(service, write.input, root));
730
+ const committed = appendActionOccurrences(writes.map((write) => actionForTwinWrite(service,
731
+ write.input === undefined ? write : { ...write, input: offloaded.get(write.input) as TwinWriteInput['input'] })), root);
732
+ const out: Array<{ result: TwinWriteResult; resource: TwinResource }> = [];
733
+ for (const [index, { appended, action: landed, placeholder }] of committed.entries()) {
734
+ const write = writes[index]!;
735
+ // the head does not perform here (`performsAtHeadNow` was false): this marks the request's deferred group as written
736
+ if (appended && !placeholder) await performAtHead(service, landed, root);
737
+ const answerId = write.subjectId;
738
+ let resolved: TwinResource | undefined;
739
+ out.push({
740
+ result: { status: appended ? 'performed' : 'replayed', actionId: landed.id },
741
+ get resource(): TwinResource {
742
+ return resolved ??= readSubject(service, write.subjectType, answerId, root)
743
+ ?? ({ id: answerId, type: write.subjectType, updatedAt: landed.occurredAt, ...write.fields } as TwinResource);
744
+ },
745
+ });
746
+ }
747
+ return out;
748
+ }
749
+
708
750
  export async function createTwinServer(options: { service: string; root?: string; port?: number; readOnly?: boolean }): Promise<{ port: number; stop: () => void }> {
709
751
  const readOnly = options.readOnly ?? false;
710
752
  const serveOptions = {
package/src/sockets.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  // upgrade through the serve seam, and offers every write to every open session of the vendor, after the write's
7
7
  // webhooks. A session's own counters (a sequence number, a heartbeat) are the connection's, never the World's state.
8
8
  // Nothing here runs at module scope beyond the empty registry.
9
+ import { worldNow } from './world-clock.ts';
9
10
  import type { EventWrite } from './events.ts';
10
11
  import type { ScenarioEngine } from './scenario.ts';
11
12
  import type { WebSocketPeer, WebSocketUpgrade } from './serve-http.ts';
@@ -48,7 +49,7 @@ export type SocketEngine<C> = {
48
49
  close?(session: SocketSession, ctx: C): void | Promise<void>;
49
50
  };
50
51
 
51
- type Open = { session: SocketSession; engine: SocketEngine<unknown>; context: () => Promise<unknown>; queue: Promise<void>; pending: Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }> };
52
+ type Open = { session: SocketSession; engine: SocketEngine<unknown>; context: () => Promise<unknown>; queue: Promise<void>; pending: Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout>; due: number }> };
52
53
  const sessions = new Map<string, Set<Open>>();
53
54
  /** A World's next session number, per World (its service and root): a session's id is the World's, never the
54
55
  * process's, so a life replayed on a fresh World numbers its sessions as it did the first time. */
@@ -60,6 +61,15 @@ export function openSessions(service: string, root: string | undefined, socket:
60
61
  return [...(sessions.get(keyOf(service, root)) ?? [])].filter((o) => o.session.socket === socket).map((o) => o.session);
61
62
  }
62
63
 
64
+ /** What a session awaits that the World's clock has run out (a life's `wait` past a relay's timeout), rejected as the
65
+ * machine's clock would reject it; called as the vendor serves a request ("A request answered by a session"). */
66
+ export function expireSocketWaits(service: string, root: string | undefined): void {
67
+ const open = sessions.get(keyOf(service, root));
68
+ if (!open?.size) return;
69
+ const now = Date.parse(worldNow());
70
+ for (const o of open) for (const [k, p] of o.pending) if (p.due <= now) { clearTimeout(p.timer); o.pending.delete(k); p.reject(new Error('no reply in time')); }
71
+ }
72
+
63
73
  /** Offer a write to every open session of the vendor, each in its turn after what it is already doing. Queued, never
64
74
  * awaited: the write may be one a session's own frame made, which that session finishes before it is told. */
65
75
  export function socketWrite(service: string, root: string | undefined, write: EventWrite): void {
@@ -72,21 +82,42 @@ export function socketWrite(service: string, root: string | undefined, write: Ev
72
82
  }
73
83
  }
74
84
 
85
+ /** A declared socket path's parameters for a request's path, or undefined when it does not match: a `{name}` segment
86
+ * matches one segment, as an operation's path parameter does (architecture, sockets). */
87
+ function socketParams(declared: string, path: string): Record<string, string> | undefined {
88
+ const names: string[] = [];
89
+ const pattern = (declared.replace(/\/+$/, '') || '/').split(/(\{[^}/]+\})/).map((part) => {
90
+ const name = /^\{([^}/]+)\}$/.exec(part)?.[1];
91
+ if (name === undefined) return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
+ names.push(name);
93
+ return '([^/]+)';
94
+ }).join('');
95
+ const hit = new RegExp(`^${pattern}$`).exec(path);
96
+ if (!hit) return undefined;
97
+ try { return Object.fromEntries(names.map((name, i) => [name, decodeURIComponent(hit[i + 1]!)])); } catch { return undefined; }
98
+ }
99
+
75
100
  /** The upgrade the serve seam takes: a request to a declared socket's path (and host) opens a session of its engine. */
76
101
  export function socketUpgrade<C>(
77
102
  service: string,
78
103
  root: string | undefined,
79
104
  decls: ReadonlyArray<SocketDecl>,
80
105
  engines: Record<string, SocketEngine<C>>,
81
- context: (request: Request, decl: SocketDecl) => Promise<C>,
106
+ context: (request: Request, decl: SocketDecl, params: Record<string, string>) => Promise<C>,
82
107
  hostOf: (request: Request) => string,
83
108
  pathOf: (request: Request) => string = (request) => new URL(request.url).pathname,
84
109
  decide: (asked: unknown) => Promise<ScenarioServed | undefined> = async () => undefined,
85
110
  ): WebSocketUpgrade {
86
- const declFor = (request: Request): SocketDecl | undefined => decls.find((d) => {
111
+ const matchFor = (request: Request): { decl: SocketDecl; params: Record<string, string> } | undefined => {
87
112
  const path = pathOf(request).replace(/\/+$/, '') || '/';
88
- return (d.path.replace(/\/+$/, '') || '/') === path && (!d.host || d.host.toLowerCase() === hostOf(request)) && engines[d.id] !== undefined;
89
- });
113
+ for (const d of decls) {
114
+ if ((d.host && d.host.toLowerCase() !== hostOf(request)) || engines[d.id] === undefined) continue;
115
+ const params = socketParams(d.path, path);
116
+ if (params) return { decl: d, params };
117
+ }
118
+ return undefined;
119
+ };
120
+ const declFor = (request: Request): SocketDecl | undefined => matchFor(request)?.decl;
90
121
  const byPeer = new WeakMap<WebSocketPeer, Open>();
91
122
  const key = keyOf(service, root);
92
123
  return {
@@ -99,7 +130,7 @@ export function socketUpgrade<C>(
99
130
  return declared ? offered.find((p) => declared.includes(p)) : offered[0];
100
131
  },
101
132
  open: (peer, request) => {
102
- const decl = declFor(request)!;
133
+ const { decl, params } = matchFor(request)!;
103
134
  const engine = engines[decl.id]! as SocketEngine<unknown>;
104
135
  const pending: Open['pending'] = new Map();
105
136
  const session: SocketSession = {
@@ -108,7 +139,7 @@ export function socketUpgrade<C>(
108
139
  close: (code, reason) => { peer.close(code, reason); },
109
140
  expect: (key, ms) => new Promise((resolve, reject) => {
110
141
  const timer = setTimeout(() => { pending.delete(key); reject(new Error('no reply in time')); }, ms);
111
- pending.set(key, { resolve, reject, timer });
142
+ pending.set(key, { resolve, reject, timer, due: Date.parse(worldNow()) + ms });
112
143
  }),
113
144
  fulfil: (key, value) => {
114
145
  const p = pending.get(key);
@@ -118,7 +149,7 @@ export function socketUpgrade<C>(
118
149
  },
119
150
  decide,
120
151
  };
121
- const o: Open = { session, engine, context: () => context(request.clone(), decl), queue: Promise.resolve(), pending };
152
+ const o: Open = { session, engine, context: () => context(request.clone(), decl, params), queue: Promise.resolve(), pending };
122
153
  byPeer.set(peer, o);
123
154
  (sessions.get(key) ?? sessions.set(key, new Set()).get(key)!).add(o);
124
155
  if (engine.open) o.queue = o.queue.then(async () => engine.open!(session, await o.context())).catch((error) => session.close(1011, String((error as Error).message ?? error).slice(0, 120)));
package/src/storage.ts CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  GenericWorldStateSchema,
7
7
  WorldServiceEventSchema,
8
8
  } from './schemas.ts';
9
- import { getActiveWorldStore } from './world-store.ts';
9
+ import { getActiveWorldStore, storageLockScope, TREE_GENERATION, type WorldLockOptions } from './world-store.ts';
10
10
  import { refuseReadOnlyWrite } from './request-scope.ts';
11
11
  import { parentEntries, toEvent } from './log.ts';
12
12
  import type {
@@ -198,8 +198,8 @@ function appendJsonl(path: string, value: unknown): void {
198
198
  * single-instance store it is a synchronous pass-through. Used to make read-then-append
199
199
  * critical sections atomic. Public so world-runtime can guard concurrent `upWorld`
200
200
  * claims of one instance dir with the SAME lock semantics the event log uses (TWIN-36). */
201
- export function withFileLock<T>(lockPath: string, fn: () => T): T {
202
- return withStateLock(lockPath, fn);
201
+ export function withFileLock<T>(lockPath: string, fn: () => T, options: WorldLockOptions = {}): T {
202
+ return withStateLock(lockPath, fn, storageLockScope(options));
203
203
  }
204
204
 
205
205
  function writeJsonAtomic(path: string, value: unknown): void {
@@ -385,6 +385,8 @@ const KNOWN_SERVICE_ENTRIES = new Set([
385
385
  function isKnownServiceEntry(name: string): boolean {
386
386
  // log.ts: the branch pointer and the checkpoint
387
387
  if (name === 'branch.json' || name === 'checkpoints') return true;
388
+ // world-store.ts: the generation a file store keeps beside a tree's files
389
+ if (name === TREE_GENERATION) return true;
388
390
  return KNOWN_SERVICE_ENTRIES.has(name) || name.endsWith('.tmp') || /\.stale\.\d+\.\d+$/.test(name);
389
391
  }
390
392
 
@@ -41,6 +41,50 @@ async function worldTwins(): Promise<Record<string, string>> {
41
41
  return { ...twinOrigins(env), ...twinOrigins(instanceEnv) };
42
42
  }
43
43
 
44
+ /** The hosts each claiming twin last listed at its `hostsClaimed` door, by the door's address, and when; read again
45
+ * after CLAIMS_TTL_MS or for a host it lacks, as the injector polls the doors every five seconds (inject.cjs, "HOSTS A TWIN CLAIMS WHILE IT
46
+ * RUNS"). */
47
+ const claimsRead = new Map<string, { hosts: Set<string>; at: number }>();
48
+ const CLAIMS_TTL_MS = 5_000;
49
+
50
+ /** The hosts a twin claims at its door: in process (a journey walk's companion) or at its address, within the injector's
51
+ * first-read deadline (inject.cjs CLAIMS_WAIT_MS, 1500 ms, measured beside it); a door that does not answer claims
52
+ * nothing. */
53
+ async function claimedHosts(at: string, ask: () => Promise<Response>, host: string): Promise<Set<string>> {
54
+ const held = claimsRead.get(at);
55
+ // a host the held list lacks is asked for again once, as the injector refreshes before it refuses: a domain claimed a
56
+ // moment ago is found
57
+ if (held && Date.now() - held.at < CLAIMS_TTL_MS && held.hosts.has(host)) return held.hosts;
58
+ let hosts = new Set<string>();
59
+ try {
60
+ const r = await ask();
61
+ const body = r.ok ? await r.json() as { hosts?: unknown[] } : null;
62
+ hosts = new Set((body?.hosts ?? []).map((h) => String(h).toLowerCase()));
63
+ } catch { /* an unanswered door claims nothing */ }
64
+ claimsRead.set(at, { hosts, at: Date.now() });
65
+ return hosts;
66
+ }
67
+
68
+ /** The twin that claims a host at its `hostsClaimed` door (an R2 bucket's custom domain, a Worker's), as the injector
69
+ * routes it: a twin this process answers first, the World's twins after. */
70
+ async function claimingTwin(target: URL, twins: Record<string, string>): Promise<{ vendor: string; local: boolean } | undefined> {
71
+ const { packFacts } = await import('../pack-facts.cjs') as { packFacts: () => { packs: Record<string, { hostsClaimed?: { door?: string } }> } };
72
+ const host = target.hostname.toLowerCase();
73
+ const key = process.env.VOLTER_TWINS_KEY;
74
+ const doors = Object.entries(packFacts().packs).flatMap(([vendor, facts]) => (facts.hostsClaimed?.door ? [[vendor, facts.hostsClaimed.door] as const] : []));
75
+ for (const [vendor, door] of doors) {
76
+ const twin = inProcessTwins[vendor];
77
+ if (twin && (await claimedHosts(`in-process:${vendor}${door}`, async () => twin(new Request(`http://${vendor}.twin${door}`)), host)).has(host)) return { vendor, local: true };
78
+ }
79
+ for (const [vendor, door] of doors) {
80
+ const origin = twins[vendor];
81
+ if (origin === undefined) continue;
82
+ const at = `${origin.replace(/\/$/, '')}${door}`;
83
+ if ((await claimedHosts(at, () => fetch(at, { headers: key ? { 'x-twins-key': key } : {}, signal: AbortSignal.timeout(1500) }), host)).has(host)) return { vendor, local: false };
84
+ }
85
+ return undefined;
86
+ }
87
+
44
88
  /** One URL answered only by a World-held application or vendor twin: the fetch's application stand-in first (its answer
45
89
  * is the application's; `'unreachable'` leaves no application to reach, the World's twins still reachable). */
46
90
  async function worldResponse(target: URL, init: RequestInit, standIn?: ApplicationStandIn): Promise<Response> {
@@ -67,8 +111,18 @@ async function worldResponse(target: URL, init: RequestInit, standIn?: Applicati
67
111
  if (key && !headers.has('x-twins-key')) headers.set('x-twins-key', key);
68
112
  return fetch(`${twins[vendor]!.replace(/\/$/, '')}${target.pathname}${target.search}`, { ...init, headers, redirect: 'manual' });
69
113
  }
114
+ // the application's own hosts before any twin's claimed ones: a call to the application asks no door
70
115
  const { appDestination, appFetch } = await import('../app-route.cjs') as { appDestination: (url: URL) => unknown; appFetch: (url: URL, init: RequestInit) => Promise<Response> };
71
116
  if (scripted !== 'unreachable' && appDestination(target)) return appFetch(target, { ...init, redirect: 'manual' });
117
+ const claimer = await claimingTwin(target, twins);
118
+ if (claimer !== undefined) {
119
+ const headers = new Headers(init.headers);
120
+ headers.set('x-volter-twin-original-host', target.host);
121
+ if (claimer.local) return inProcessTwins[claimer.vendor]!(new Request(target, { ...init, headers, redirect: 'manual' }));
122
+ const key = process.env.VOLTER_TWINS_KEY;
123
+ if (key && !headers.has('x-twins-key')) headers.set('x-twins-key', key);
124
+ return fetch(`${twins[claimer.vendor]!.replace(/\/$/, '')}${target.pathname}${target.search}`, { ...init, headers, redirect: 'manual' });
125
+ }
72
126
  throw new VendorUnreachableError(`${target.host}${target.pathname}: this World holds no application or vendor twin at this URL`);
73
127
  }
74
128
 
@@ -0,0 +1,6 @@
1
+ // One private boot answer for the kernel's HTTP and gRPC serve seams.
2
+ // Reading process identity happens on request; importing this helper has no side effects.
3
+ export function worldBootAnswer(boot: string): Response {
4
+ const pid = (globalThis as { process?: { pid?: number } }).process?.pid;
5
+ return Response.json({ boot, pid });
6
+ }
@@ -26,10 +26,14 @@ const machineNow = (): number => {
26
26
  * now); real wall-clock time otherwise. */
27
27
  export function worldNow(): string {
28
28
  const file = process.env[WORLD_CLOCK_ENV];
29
- if (file && getActiveWorldStore().exists(file)) {
29
+ // One read answers both whether the clock file is there and what it holds.
30
+ // Kept while the clock file's own facts stand (world-store.ts readKept): every operation asks the time.
31
+ const store = getActiveWorldStore();
32
+ const held = file ? (store.readKept ? store.readKept(file) : store.read(file)) : null;
33
+ if (held !== null) {
30
34
  // A configured-but-corrupt clock is a WORLD defect — parseClock fails loudly, never silently drifting back to
31
35
  // wall-clock (two services disagreeing about now breaks the world).
32
- const clock = clockForm.parseClock(getActiveWorldStore().read(file) ?? '');
36
+ const clock = clockForm.parseClock(held);
33
37
  return new Date(clockForm.clockNowMs(clock, machineNow())).toISOString();
34
38
  }
35
39
  return new Date(machineNow()).toISOString();