@volter/world-core 3.0.154 → 3.0.156

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 (107) hide show
  1. package/dist/generated/pack-facts.json +140 -0
  2. package/dist/inject.cjs +150 -86
  3. package/dist/src/actions.d.ts +15 -0
  4. package/dist/src/actions.js +82 -21
  5. package/dist/src/ancestry.d.ts +5 -4
  6. package/dist/src/ancestry.js +53 -28
  7. package/dist/src/changeset.js +1 -1
  8. package/dist/src/clickhouse/compression.d.ts +28 -0
  9. package/dist/src/clickhouse/compression.js +281 -0
  10. package/dist/src/clickhouse/index.d.ts +1 -0
  11. package/dist/src/clickhouse/index.js +1 -0
  12. package/dist/src/client-bundle.js +15 -15
  13. package/dist/src/derived-core.d.ts +12 -1
  14. package/dist/src/derived-core.js +145 -11
  15. package/dist/src/derived.js +3 -0
  16. package/dist/src/fork.js +1 -1
  17. package/dist/src/graphql-wire.d.ts +5 -0
  18. package/dist/src/graphql-wire.js +6 -6
  19. package/dist/src/grpc-wire.js +2 -1
  20. package/dist/src/h2.js +63 -10
  21. package/dist/src/head.d.ts +3 -0
  22. package/dist/src/head.js +9 -0
  23. package/dist/src/history.d.ts +1 -1
  24. package/dist/src/history.js +7 -7
  25. package/dist/src/index.d.ts +4 -1
  26. package/dist/src/index.js +5 -1
  27. package/dist/src/log.d.ts +23 -3
  28. package/dist/src/log.js +160 -37
  29. package/dist/src/managed-original-inputs.d.ts +101 -0
  30. package/dist/src/managed-original-inputs.js +125 -0
  31. package/dist/src/mongodb/client.d.ts +8 -0
  32. package/dist/src/mongodb/client.js +27 -0
  33. package/dist/src/mongodb/index.d.ts +2 -0
  34. package/dist/src/mongodb/index.js +5 -0
  35. package/dist/src/mongodb/stream.d.ts +74 -0
  36. package/dist/src/mongodb/stream.js +206 -0
  37. package/dist/src/observe.js +1 -1
  38. package/dist/src/pack-fetch.d.ts +2 -1
  39. package/dist/src/pack-fetch.js +21 -9
  40. package/dist/src/packRegistry.d.ts +10 -14
  41. package/dist/src/packRegistry.js +2 -13
  42. package/dist/src/protocol.d.ts +13 -0
  43. package/dist/src/protocol.js +13 -0
  44. package/dist/src/redis/engine.d.ts +10 -4
  45. package/dist/src/redis/engine.js +103 -32
  46. package/dist/src/redis/member-storage.js +20 -8
  47. package/dist/src/redis/service-times.d.ts +25 -0
  48. package/dist/src/redis/service-times.js +87 -0
  49. package/dist/src/redis/stream.d.ts +2 -1
  50. package/dist/src/redis/stream.js +109 -20
  51. package/dist/src/runtime-state.d.ts +11 -0
  52. package/dist/src/runtime-state.js +13 -0
  53. package/dist/src/runtime.d.ts +4 -2
  54. package/dist/src/runtime.js +5 -2
  55. package/dist/src/serve-http.d.ts +2 -0
  56. package/dist/src/serve-http.js +21 -4
  57. package/dist/src/serve.d.ts +10 -0
  58. package/dist/src/serve.js +43 -3
  59. package/dist/src/storage.d.ts +2 -1
  60. package/dist/src/storage.js +6 -3
  61. package/dist/src/world-boot.d.ts +1 -0
  62. package/dist/src/world-boot.js +6 -0
  63. package/dist/src/world-clock.js +6 -2
  64. package/dist/src/world-store.d.ts +108 -7
  65. package/dist/src/world-store.js +883 -103
  66. package/dist/src/xml.d.ts +30 -0
  67. package/dist/src/xml.js +74 -0
  68. package/generated/pack-facts.json +140 -0
  69. package/inject.cjs +150 -86
  70. package/package.json +20 -1
  71. package/src/actions.ts +78 -21
  72. package/src/ancestry.ts +46 -24
  73. package/src/changeset.ts +1 -1
  74. package/src/clickhouse/compression.ts +172 -0
  75. package/src/clickhouse/index.ts +2 -0
  76. package/src/client-bundle.ts +13 -14
  77. package/src/derived-core.ts +142 -11
  78. package/src/derived.ts +3 -0
  79. package/src/fork.ts +1 -1
  80. package/src/graphql-wire.ts +9 -6
  81. package/src/grpc-wire.ts +2 -1
  82. package/src/h2.ts +46 -5
  83. package/src/head.ts +9 -0
  84. package/src/history.ts +7 -7
  85. package/src/index.ts +6 -1
  86. package/src/log.ts +147 -32
  87. package/src/managed-original-inputs.ts +163 -0
  88. package/src/mongodb/client.ts +27 -0
  89. package/src/mongodb/index.ts +5 -0
  90. package/src/mongodb/stream.ts +213 -0
  91. package/src/observe.ts +1 -1
  92. package/src/pack-fetch.ts +23 -11
  93. package/src/packRegistry.ts +3 -15
  94. package/src/protocol.ts +13 -0
  95. package/src/redis/engine.ts +100 -57
  96. package/src/redis/member-storage.ts +17 -9
  97. package/src/redis/service-times.ts +81 -0
  98. package/src/redis/stream.ts +66 -14
  99. package/src/runtime-state.ts +14 -0
  100. package/src/runtime.ts +6 -1
  101. package/src/serve-http.ts +23 -4
  102. package/src/serve.ts +45 -3
  103. package/src/storage.ts +5 -3
  104. package/src/world-boot.ts +6 -0
  105. package/src/world-clock.ts +6 -2
  106. package/src/world-store.ts +704 -91
  107. package/src/xml.ts +71 -0
@@ -7,6 +7,7 @@ import { parseExactJson } from "./exact-json.js";
7
7
  // and what they answer is the pack's. Importing it does nothing.
8
8
  import { acceptH2Connection } from "./h2.js";
9
9
  import { nodeBuiltin, WORLD_BOOT_PATH } from "./serve-http.js";
10
+ import { worldBootAnswer } from "./world-boot.js";
10
11
  import { decodeMessage, encodeMessage } from "./protobuf.js";
11
12
  /** gRPC's status codes (https://grpc.github.io/grpc/core/md_doc_statuscodes.html). */
12
13
  export const GrpcStatus = {
@@ -162,7 +163,7 @@ export function grpcOverFetch(fetch) {
162
163
  export async function serveGrpc(options) {
163
164
  const net = nodeBuiltin('node:net');
164
165
  const worldBoot = globalThis.process?.env?.VOLTER_WORLD_BOOT_ID;
165
- const fetch = worldBoot ? (request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ? Promise.resolve(Response.json({ boot: worldBoot })) : options.fetch(request)) : options.fetch;
166
+ const fetch = worldBoot ? (request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ? Promise.resolve(worldBootAnswer(worldBoot)) : options.fetch(request)) : options.fetch;
166
167
  const handler = grpcStreamHandler(options.schema, grpcOverFetch(fetch));
167
168
  const server = net.createServer((socket) => { acceptH2Connection(socket, handler, fetch); });
168
169
  await new Promise((resolve, reject) => { server.once('error', reject); server.listen(options.port ?? 0, '127.0.0.1', () => resolve()); });
package/dist/src/h2.js CHANGED
@@ -10,6 +10,43 @@ const MAX_CONCURRENT_STREAMS = 1000;
10
10
  /** a request body larger than this resets its stream: a memory bound, not Temporal's message limit (the
11
11
  * SDKs' gRPC default is 4 MiB; this server does not enforce that one) */
12
12
  const MAX_REQUEST_BYTES = 64 * 1024 * 1024;
13
+ /** A chunked body (RFC 9112 §7.1) from `from`: its bytes once the last chunk and the trailer section have arrived,
14
+ * 'incomplete' before, 'bad' for a malformed size line or one past MAX_REQUEST_BYTES. Extensions and trailers are
15
+ * read past and not kept. */
16
+ function chunkedBody(buffer, from) {
17
+ const parts = [];
18
+ let at = from, total = 0;
19
+ for (;;) {
20
+ const line = buffer.indexOf('\r\n', at);
21
+ if (line < 0)
22
+ return buffer.length - at > 4096 ? 'bad' : 'incomplete';
23
+ const size = /^([0-9A-Fa-f]{1,8})[ \t]*(?:;.*)?$/su.exec(buffer.subarray(at, line).toString('latin1'));
24
+ if (!size)
25
+ return 'bad';
26
+ const length = parseInt(size[1], 16);
27
+ at = line + 2;
28
+ if (length === 0)
29
+ break;
30
+ total += length;
31
+ if (total > MAX_REQUEST_BYTES)
32
+ return 'bad';
33
+ if (buffer.length < at + length + 2)
34
+ return 'incomplete';
35
+ if (buffer[at + length] !== 0x0d || buffer[at + length + 1] !== 0x0a)
36
+ return 'bad';
37
+ parts.push(buffer.subarray(at, at + length));
38
+ at += length + 2;
39
+ }
40
+ // The trailer section: field lines up to an empty line.
41
+ for (;;) {
42
+ const line = buffer.indexOf('\r\n', at);
43
+ if (line < 0)
44
+ return buffer.length - at > 64 * 1024 ? 'bad' : 'incomplete';
45
+ if (line === at)
46
+ return Buffer.concat(parts);
47
+ at = line + 2;
48
+ }
49
+ }
13
50
  /** a header block (HEADERS + CONTINUATION) larger than this closes the connection */
14
51
  const MAX_HEADER_BLOCK = 256 * 1024;
15
52
  // ---- HPACK ------------------------------------------------------------------------------------
@@ -248,18 +285,34 @@ class H2Connection {
248
285
  this.respondHttp1(new Response('bad header', { status: 400 }));
249
286
  return;
250
287
  }
251
- if (headers.get('transfer-encoding')) {
252
- this.respondHttp1(new Response('chunked request bodies are not accepted', { status: 411 }));
253
- return;
288
+ // A request with a Transfer-Encoding has a body framed by it, and its Content-Length is not read (RFC 9112 §6.3):
289
+ // undici sends a streamed body (a World's fetch forwarding a request's own body, duplex 'half') chunked.
290
+ const coding = headers.get('transfer-encoding');
291
+ let body;
292
+ if (coding !== null) {
293
+ if (coding.split(',').map((each) => each.trim().toLowerCase()).at(-1) !== 'chunked') {
294
+ this.respondHttp1(new Response('a transfer coding other than chunked is not read', { status: 400 }));
295
+ return;
296
+ }
297
+ const read = chunkedBody(this.buffer, end + 4);
298
+ if (read === 'bad') {
299
+ this.respondHttp1(new Response('bad chunked body', { status: 400 }));
300
+ return;
301
+ }
302
+ if (read === 'incomplete')
303
+ return;
304
+ body = read;
254
305
  }
255
- const length = Number(headers.get('content-length') ?? 0);
256
- if (!Number.isFinite(length) || length < 0 || length > MAX_REQUEST_BYTES) {
257
- this.respondHttp1(new Response('bad content-length', { status: 400 }));
258
- return;
306
+ else {
307
+ const length = Number(headers.get('content-length') ?? 0);
308
+ if (!Number.isFinite(length) || length < 0 || length > MAX_REQUEST_BYTES) {
309
+ this.respondHttp1(new Response('bad content-length', { status: 400 }));
310
+ return;
311
+ }
312
+ if (this.buffer.length < end + 4 + length)
313
+ return;
314
+ body = this.buffer.subarray(end + 4, end + 4 + length);
259
315
  }
260
- if (this.buffer.length < end + 4 + length)
261
- return;
262
- const body = this.buffer.subarray(end + 4, end + 4 + length);
263
316
  this.buffer = Buffer.alloc(0);
264
317
  this.socket.removeAllListeners('data');
265
318
  let request;
@@ -174,6 +174,9 @@ export declare function performEntries(opts: {
174
174
  * `auto` is performed now. Returns the vendor's id when it minted one, so the answer carries it.
175
175
  * Raises `RefusedWriteError` / `VendorWriteError` for the pack to answer in the vendor's shape.
176
176
  */
177
+ /** Whether `performAtHead` would perform an entry now (a twin whose root says `auto`, outside a pack fetch's deferred
178
+ * group): a caller appending several writes at once appends them one at a time then, each performed before the next. */
179
+ export declare function performsAtHeadNow(service: string, root?: string): boolean;
177
180
  export declare function performAtHead(service: string, action: Pick<TwinAction, 'id'>, root?: string): Promise<{
178
181
  performed: boolean;
179
182
  externalId?: string;
package/dist/src/head.js CHANGED
@@ -464,6 +464,15 @@ async function performNow(opts) {
464
464
  * `auto` is performed now. Returns the vendor's id when it minted one, so the answer carries it.
465
465
  * Raises `RefusedWriteError` / `VendorWriteError` for the pack to answer in the vendor's shape.
466
466
  */
467
+ /** Whether `performAtHead` would perform an entry now (a twin whose root says `auto`, outside a pack fetch's deferred
468
+ * group): a caller appending several writes at once appends them one at a time then, each performed before the next. */
469
+ export function performsAtHeadNow(service, root) {
470
+ const request = currentCorrelationId();
471
+ if (request !== undefined && deferred.has(request))
472
+ return false;
473
+ const head = headOf(service, root);
474
+ return head.kind === 'real' && head.deploy === 'auto';
475
+ }
467
476
  export async function performAtHead(service, action, root) {
468
477
  // a pack fetch performs its request's entries as one group after the handler (performRequestGroup)
469
478
  const request = currentCorrelationId();
@@ -51,7 +51,7 @@ export declare function saveHistoryView(directory: string, view: HistoryView): s
51
51
  * (and the view document itself) and each named directory's ancestry record (its generation). A read cache keyed by this sees an
52
52
  * ancestor's retained history truncated, replaced or its directory re-created, which the full read
53
53
  * refuses (`historyEntries`), instead of serving a tree folded from the history it replaced. */
54
- export declare function inheritedHistoryKey(service: string, root?: string): string;
54
+ export declare function inheritedHistoryKey(service: string, root?: string, meta?: string | null): string;
55
55
  export declare function inheritedHistory(service: string, root?: string): HistoryView | undefined;
56
56
  /** Capture under the directory's history lock, as appends and complete observation batches are made. */
57
57
  export declare function captureHistory(service: string, root?: string): {
@@ -108,14 +108,14 @@ export function saveHistoryView(directory, view) {
108
108
  for (const dependency of dependencies)
109
109
  commitParentPin(dependency.directory, file);
110
110
  return id;
111
- });
111
+ }, { operation: 'view pin publication' });
112
112
  }
113
113
  /** What an inherited history's content stands on, as file facts: every segment file the view names
114
114
  * (and the view document itself) and each named directory's ancestry record (its generation). A read cache keyed by this sees an
115
115
  * ancestor's retained history truncated, replaced or its directory re-created, which the full read
116
116
  * refuses (`historyEntries`), instead of serving a tree folded from the history it replaced. */
117
- export function inheritedHistoryKey(service, root) {
118
- const parent = readBranchMeta(service, root)?.parent;
117
+ export function inheritedHistoryKey(service, root, meta) {
118
+ const parent = readBranchMeta(service, root, meta)?.parent;
119
119
  if (!parent?.view)
120
120
  return '';
121
121
  const store = getActiveWorldStore();
@@ -170,7 +170,7 @@ export function captureHistory(service, root) {
170
170
  descriptor.segments.push({ directory, generation, file, start: 0, count: entries.length, digest: historyDigest(entries) });
171
171
  }
172
172
  return { view: saveHistoryView(directory, descriptor), position: historyLength(descriptor.layout), descriptor };
173
- });
173
+ }, { operation: 'capture' });
174
174
  }
175
175
  export function originHead(service, root) {
176
176
  const raw = getActiveWorldStore().read(join(worldPaths(service, root).dir, 'origin-head.json'));
@@ -213,7 +213,7 @@ export function publishOriginHistory(service, root, remoteView, layout, entries)
213
213
  const view = saveHistoryView(directory, { version: 1, owner: { directory, generation }, layout, segments });
214
214
  store.writeAtomic(join(directory, 'origin-head.json'), JSON.stringify({ view, remoteView }));
215
215
  return { view, appended };
216
- });
216
+ }, { operation: 'origin fetch' });
217
217
  }
218
218
  /** A changeset binds the complete inherited view, including this root's observations. */
219
219
  export function captureParentHistory(service, root) {
@@ -221,7 +221,7 @@ export function captureParentHistory(service, root) {
221
221
  const head = captureHistory(service, root);
222
222
  const position = head.position - head.descriptor.layout.branch;
223
223
  return { view: saveHistoryView(worldPaths(service, root).dir, historyPrefix(head.descriptor, position)), position };
224
- });
224
+ }, { operation: 'capture' });
225
225
  }
226
226
  /** Compare evaluated views, so removals, reordered rows and layout changes count as drift. */
227
227
  export function historyChanges(service, before, after) {
@@ -295,7 +295,7 @@ export function historyAtInstant(service, instant, root, selectedView) {
295
295
  }
296
296
  const view = saveHistoryView(directory, { version: 1, owner: source.owner, layout, segments, ...(source.origin ? { origin: { ...source.origin, ...(keep.slice(0, source.origin.position).some(v => !v) ? { incomplete: true } : {}) } } : {}) });
297
297
  return { view, position: historyLength(layout) };
298
- });
298
+ }, { operation: 'capture' });
299
299
  }
300
300
  /** Replace only the tracked origin layer; inherited local layers keep their exact projection order. */
301
301
  export function replaceHistoryOrigin(previous, origin, next) {
@@ -40,7 +40,7 @@ export { answerVendorErrors, RefusedWriteError } from './head.js';
40
40
  export { GraphqlError, type GraphqlPart, type GraphqlResolver } from './graphql-wire.js';
41
41
  export { VendorUnreachableError } from './vendor-call.js';
42
42
  export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER } from './request-scope.js';
43
- export { serveHttp, serveStream, StreamListenerUnavailable, type HttpHandler, type HttpServer, type ServeHttpOptions } from './serve-http.js';
43
+ export { serveHttp, serveStream, nodeBuiltin, StreamListenerUnavailable, type HttpHandler, type HttpServer, type ServeHttpOptions } from './serve-http.js';
44
44
  export type { TwinStream, TwinStreamConnection, TwinStreamSink } from './twin-fetch.js';
45
45
  export { privateTransportKey } from './private-transport.js';
46
46
  export { siteLabel, siteUrlOf, twinSiteUrl, twinVendorUrl } from './twin-fetch.js';
@@ -58,6 +58,7 @@ export { EngineDatabaseError, EngineUnavailableError, quoteIdent } from './manag
58
58
  export type { Batch as EngineBatch, ManagedDatabase, Statement as EngineStatement, StatementResult as EngineStatementResult } from './managed-database.js';
59
59
  export type { Mail, SmtpRoute } from './smtp.js';
60
60
  export * as s3 from './s3/wire.js';
61
+ export * as xml from './xml.js';
61
62
  export * as clickhouse from './clickhouse/index.js';
62
63
  export { sampleFromSchema } from './schema-sample.js';
63
64
  export * as openaiWire from './openai-wire.js';
@@ -70,6 +71,7 @@ export { presignSigV4, signSigV4, verifySigV4, type SigV4Verdict } from './sigv4
70
71
  export * as redis from './redis/index.js';
71
72
  export * as mysql from './mysql/index.js';
72
73
  export * as live from './live/index.js';
74
+ export * as mongodb from './mongodb/index.js';
73
75
  export * as git from './git/index.js';
74
76
  export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError } from './scenario.js';
75
77
  export type { PackScenarioAdapter, ScenarioDecision, ScenarioDocument, ScenarioExtractorSpec, ScenarioFault, ScenarioFaultResult, ScenarioFeatures, ScenarioHandler, ScenarioMatcher, ScenarioMissRecord, ScenarioStatus } from './scenario.js';
@@ -77,3 +79,4 @@ export { verifyAttestationBundle } from './attestation.js';
77
79
  export { browserSessionToken, startBrowserSession, endBrowserSession } from './browser-session.js';
78
80
  export { readHostPort } from './host-port.js';
79
81
  export { assertBrowserName, browserOrigin, validateBrowserState, validateBrowserStorageState, validateBrowserContext, browserAcceptLanguage, cookieMatchesHost, type BrowserCookie, type BrowserState, type BrowserStorageState, type BrowserContext, type BrowserIndexedDB } from './browser-state.js';
82
+ export * as managedOriginalInputs from './managed-original-inputs.js';
package/dist/src/index.js CHANGED
@@ -29,7 +29,7 @@ export { GraphqlError } from "./graphql-wire.js";
29
29
  export { VendorUnreachableError } from "./vendor-call.js";
30
30
  export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER } from "./request-scope.js";
31
31
  // ── the serve factory ──────────────────────────────────────────────────────────────────────────
32
- export { serveHttp, serveStream, StreamListenerUnavailable } from "./serve-http.js";
32
+ export { serveHttp, serveStream, nodeBuiltin, StreamListenerUnavailable } from "./serve-http.js";
33
33
  export { privateTransportKey } from "./private-transport.js";
34
34
  // where a page links a hostname its twin answers, inside the World that shows it
35
35
  export { siteLabel, siteUrlOf, twinSiteUrl, twinVendorUrl } from "./twin-fetch.js";
@@ -49,6 +49,8 @@ export { passwordHash, personOf, personWith, recordPerson } from "./people.js";
49
49
  export { EngineDatabaseError, EngineUnavailableError, quoteIdent } from "./managed-database.js";
50
50
  // S3's wire (contract "S3's wire is a kernel library too") and the git plane (contract "The git plane is a kernel library")
51
51
  export * as s3 from "./s3/wire.js";
52
+ // Generic strict XML grammar; vendor interpretation remains in its consumer.
53
+ export * as xml from "./xml.js";
52
54
  export * as clickhouse from "./clickhouse/index.js";
53
55
  export { sampleFromSchema } from "./schema-sample.js";
54
56
  export * as openaiWire from "./openai-wire.js";
@@ -61,6 +63,7 @@ export { presignSigV4, signSigV4, verifySigV4 } from "./sigv4.js";
61
63
  export * as redis from "./redis/index.js";
62
64
  export * as mysql from "./mysql/index.js";
63
65
  export * as live from "./live/index.js";
66
+ export * as mongodb from "./mongodb/index.js";
64
67
  export * as git from "./git/index.js";
65
68
  // ── the scenario grammar (architecture, "Behaviour") ───────────────────────────────────────────
66
69
  export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError } from "./scenario.js";
@@ -68,3 +71,4 @@ export { verifyAttestationBundle } from "./attestation.js";
68
71
  export { browserSessionToken, startBrowserSession, endBrowserSession } from "./browser-session.js";
69
72
  export { readHostPort } from "./host-port.js";
70
73
  export { assertBrowserName, browserOrigin, validateBrowserState, validateBrowserStorageState, validateBrowserContext, browserAcceptLanguage, cookieMatchesHost } from "./browser-state.js";
74
+ export * as managedOriginalInputs from "./managed-original-inputs.js";
package/dist/src/log.d.ts CHANGED
@@ -102,7 +102,7 @@ export declare function originLogPath(service: string, root?: string): string;
102
102
  /** A parent is at a URL when it is not a path on this machine. */
103
103
  export declare function isUrlParent(at: string): boolean;
104
104
  export declare function checkpointPath(service: string, root?: string): string;
105
- export declare function readBranchMeta(service: string, root?: string): BranchMeta | null;
105
+ export declare function readBranchMeta(service: string, root?: string, read?: string | null): BranchMeta | null;
106
106
  export declare function writeBranchMeta(service: string, meta: BranchMeta, root?: string): void;
107
107
  /** A v1 observed row as an entry: a delta event folds its `changed.*.after`; anything else folds
108
108
  * nothing and rides along for `listEvents`. An entry row is itself. */
@@ -130,13 +130,21 @@ export declare function landedIds(parent: Entry[]): Set<string>;
130
130
  * before it had observed the vendor's own subject under that id (its tree was behind) must not fold
131
131
  * the vendor's subject onto the landed one, nor send a caller who addresses the vendor's id there. */
132
132
  export declare function aliasesFrom(parent: Entry[]): Map<string, string>;
133
- type Subject = {
133
+ export type Subject = {
134
134
  type: string;
135
135
  id: string;
136
136
  updatedAt: string;
137
137
  fields: SubjectFields;
138
138
  };
139
139
  export type Tree = Map<string, Subject>;
140
+ /** One subject read by the same guarded key as a write's answer; restore the vendor's own metadata fields. */
141
+ export declare function heldSubject(service: string, type: string, id: string, root?: string): Subject | undefined;
142
+ /**
143
+ * One local write folded onto its subject as the tree folds its entry (`overlay`): the subject as it was (undefined
144
+ * for one new to the tree) is copied, never changed. A command's writes staged in `ctx.batch` are read back by
145
+ * folding them this way, so a read inside the batch sees what the tree will hold once they land (derived-core.ts).
146
+ */
147
+ export declare function foldLocalWrite(previous: Subject | undefined, type: string, id: string, fields: SubjectFields, at: string): Subject;
140
148
  /** Fold entries onto a tree, in order: `set` overlays its fields and applies its projection; a
141
149
  * reverted entry folds nothing; a `confirm` row (v1) folds nothing. `skip` names entries the
142
150
  * parent already holds. */
@@ -152,6 +160,15 @@ export declare function ownFields(r: TwinResource): Record<string, unknown>;
152
160
  export declare function dropCheckpoint(service: string, root?: string): void;
153
161
  /** Preserve each ancestor's observed-then-local overlay, including landed IDs and aliases. */
154
162
  export declare function foldHistory(entries: Entry[], layout: HistoryLayout, service?: string): Tree;
163
+ /**
164
+ * One subject of the current tree as a resource, by its type and id: the resource `projectResources(...).find` gives
165
+ * for the same type and id, read by the subject's key without cloning or projecting the rest of the tree. Every subject is
166
+ * held under `${type}:${id}` (overlay is the only setter, and an alias re-keys through it), so the key is the
167
+ * subject. A write's answer read back the whole tree this way (serve.ts, applyTwinWrite's `resource`), once per
168
+ * written subject: a derived pack writing each document, index and collection of a database's startup paid a
169
+ * projection of every subject the World holds for each (S2 R17).
170
+ */
171
+ export declare function readSubject(service: string, type: string, id: string, root?: string): TwinResource | undefined;
155
172
  export declare function readTree(service: string, root?: string, opts?: {
156
173
  until?: string;
157
174
  at?: number;
@@ -171,6 +188,10 @@ export declare function readTreeMap(service: string, root?: string, opts?: {
171
188
  at?: number;
172
189
  view?: string;
173
190
  }): Tree;
191
+ /** Append `data` (whole JSONL lines of a service's branch log, `append` writing them) and, when the reads' memo held
192
+ * the tree as it stood, extend it by them. Called holding the directory's history lock. Answers the log's facts
193
+ * after the append (`size:mtimeMs`) when it read them, for a caller keyed on the same. */
194
+ export declare function appendToBranchLog(service: string, root: string | undefined, data: string, append: () => void): string | undefined;
174
195
  /** What changed in a service's tree since `since` (a `treeStamp` an earlier read was made at): the subjects
175
196
  * written since, as resources; the keys (`type:id`) of those no longer in the tree; and, of the changed, the keys
176
197
  * now at the tree's end in the order they stand there (new to the tree, or deleted and made again), which a
@@ -257,4 +278,3 @@ export declare function subjectHistory(service: string, subject: {
257
278
  type: string;
258
279
  id?: string;
259
280
  }, root?: string): Entry[];
260
- export {};
package/dist/src/log.js CHANGED
@@ -29,14 +29,14 @@ import { canonicalStatePath, checkParent, commitParentPin, pinParent, withAncest
29
29
  import { resolveReferences } from "./references.js";
30
30
  import { hashFieldValue } from "./hash.js";
31
31
  import { worldPaths } from "./storage.js";
32
- import { getActiveWorldStore } from "./world-store.js";
32
+ import { getActiveWorldStore, TREE_FILES } from "./world-store.js";
33
33
  export const CHECKPOINT_EVERY = 100;
34
34
  // Retained checkpoints and process memos are disposable projections, never history.
35
35
  const TREE_PROJECTION_VERSION = 2;
36
36
  export const DELTA_SUFFIX = '.delta';
37
37
  export function parentLogPath(service, root) { return worldPaths(service, root).events; }
38
- export function branchLogPath(service, root) { return join(dirname(worldPaths(service, root).events), 'actions.jsonl'); }
39
- export function branchMetaPath(service, root) { return join(dirname(worldPaths(service, root).events), 'branch.json'); }
38
+ export function branchLogPath(service, root) { return join(dirname(worldPaths(service, root).events), TREE_FILES.branch); }
39
+ export function branchMetaPath(service, root) { return join(dirname(worldPaths(service, root).events), TREE_FILES.pointer); }
40
40
  /** The fetched cache of a URL parent's whole log — what `fetch` extends and a read folds up to the position. */
41
41
  export function originLogPath(service, root) { return join(dirname(worldPaths(service, root).events), 'origin.jsonl'); }
42
42
  /** A parent is at a URL when it is not a path on this machine. */
@@ -114,8 +114,9 @@ function readRows(path) {
114
114
  cache.set(path, tail !== undefined ? { rows, lines: lines.length - 1, bytes: Buffer.byteLength(content), tail } : { content, rows, lines: lines.length - 1 });
115
115
  return rows.slice();
116
116
  }
117
- export function readBranchMeta(service, root) {
118
- const raw = getActiveWorldStore().read(branchMetaPath(service, root));
117
+ export function readBranchMeta(service, root, read) {
118
+ // `read`: branch.json's text where the caller has just read it (a tree's stamp reads it once for both uses).
119
+ const raw = read !== undefined ? read : getActiveWorldStore().read(branchMetaPath(service, root));
119
120
  if (raw === null)
120
121
  return null;
121
122
  const meta = parseExactJson(raw);
@@ -179,8 +180,8 @@ export function writeBranchMeta(service, meta, root) {
179
180
  getActiveWorldStore().writeAtomic(path, `${JSON.stringify(meta, null, 2)}\n`);
180
181
  if (meta.parent?.directory && !isUrlParent(meta.parent.at))
181
182
  commitParentPin(meta.parent.directory, path);
182
- });
183
- });
183
+ }, { operation: 'pointer publication' });
184
+ }, { operation: 'pointer publication' });
184
185
  }
185
186
  /** A v1 observed row as an entry: a delta event folds its `changed.*.after`; anything else folds
186
187
  * nothing and rides along for `listEvents`. An entry row is itself. */
@@ -251,7 +252,7 @@ export function appendOriginEntry(entry, root) {
251
252
  return { appended: false };
252
253
  store.append(path, `${JSON.stringify(entry)}\n`);
253
254
  return { appended: true };
254
- }));
255
+ }, { operation: 'origin append' }), { operation: 'origin append' });
255
256
  }
256
257
  /** Where the parent stands now: the length of its whole log (live at a path, the cache for a URL). */
257
258
  export function parentPosition(service, root) {
@@ -300,6 +301,22 @@ function overlay(tree, type, id, fields, at) {
300
301
  delete merged.deleted;
301
302
  tree.set(key, { ...existing, updatedAt: at, fields: merged });
302
303
  }
304
+ /** One subject read by the same guarded key as a write's answer; restore the vendor's own metadata fields. */
305
+ export function heldSubject(service, type, id, root) {
306
+ const row = readSubject(service, type, id, root);
307
+ return row ? { type: row.type, id: row.id, updatedAt: row.updatedAt, fields: ownFields(row) } : undefined;
308
+ }
309
+ /**
310
+ * One local write folded onto its subject as the tree folds its entry (`overlay`): the subject as it was (undefined
311
+ * for one new to the tree) is copied, never changed. A command's writes staged in `ctx.batch` are read back by
312
+ * folding them this way, so a read inside the batch sees what the tree will hold once they land (derived-core.ts).
313
+ */
314
+ export function foldLocalWrite(previous, type, id, fields, at) {
315
+ const key = `${type}:${id}`;
316
+ const tree = previous ? new Map([[key, { ...previous, fields: { ...previous.fields } }]]) : new Map();
317
+ overlay(tree, type, id, fields, at);
318
+ return tree.get(key);
319
+ }
303
320
  /** Fold entries onto a tree, in order: `set` overlays its fields and applies its projection; a
304
321
  * reverted entry folds nothing; a `confirm` row (v1) folds nothing. `skip` names entries the
305
322
  * parent already holds. */
@@ -402,6 +419,22 @@ export function foldHistory(entries, layout, service) {
402
419
  foldAliases(tree, aliasesFrom(parent), service);
403
420
  return tree;
404
421
  }
422
+ /**
423
+ * One subject of the current tree as a resource, by its type and id: the resource `projectResources(...).find` gives
424
+ * for the same type and id, read by the subject's key without cloning or projecting the rest of the tree. Every subject is
425
+ * held under `${type}:${id}` (overlay is the only setter, and an alias re-keys through it), so the key is the
426
+ * subject. A write's answer read back the whole tree this way (serve.ts, applyTwinWrite's `resource`), once per
427
+ * written subject: a derived pack writing each document, index and collection of a database's startup paid a
428
+ * projection of every subject the World holds for each (S2 R17).
429
+ */
430
+ export function readSubject(service, type, id, root) {
431
+ const key = `${type}:${id}`;
432
+ const held = currentTree(service, root).tree.get(key);
433
+ // a type or id holding ':' can share a key with another (type, id): only the subject itself is it
434
+ if (!held || held.type !== type || held.id !== id)
435
+ return undefined;
436
+ return treeResources(new Map([[key, { ...held, fields: { ...held.fields } }]]))[0];
437
+ }
405
438
  export function readTree(service, root, opts = {}) {
406
439
  return treeResources(readTreeMap(service, root, opts));
407
440
  }
@@ -414,7 +447,7 @@ export function readParentTreeMap(service, root) {
414
447
  return foldHistory([...(inherited ? historyEntries(inherited) : []), ...own], {
415
448
  ...(inherited ? { base: inherited.layout } : {}), parent: own.length, branch: 0,
416
449
  }, service);
417
- });
450
+ }, { operation: 'tree fold' });
418
451
  }
419
452
  const treeMemos = new WeakMap();
420
453
  /** How many extensions a memo remembers the touched subjects of; a projection further behind refolds. */
@@ -422,11 +455,44 @@ const TREE_STEPS_KEPT = 64;
422
455
  function statKey(path) { const s = getActiveWorldStore().stat(path); return s ? `${s.size}:${s.mtimeMs}` : '-'; }
423
456
  /** What a service's PARENT side at `root` is folded from (its parent log, branch.json and inherited history), as one
424
457
  * string: while it stands, its aliases and landed ids are the same, whatever the branch's own log does. */
425
- export function parentStamp(service, root) { return parentKeyOf(service, root); }
426
- function parentKeyOf(service, root) { return `${TREE_PROJECTION_VERSION}|${statKey(parentLogPath(service, root))}|${getActiveWorldStore().read(branchMetaPath(service, root)) ?? ''}|${inheritedHistoryKey(service, root)}`; }
458
+ export function parentStamp(service, root) { return treeFacts(service, root).parentKey; }
459
+ function parentKeyOf(service, root) { return parentFacts(service, root).key; }
460
+ /** The parent side's key, and whether it names a file outside the tree's own directory (an inherited view). */
461
+ function parentFacts(service, root) {
462
+ // branch.json is small and every stamp needs it: kept while its own facts stand (world-store.ts readKept), so an
463
+ // unchanged pointer costs one check. Its text is still in the key, so a changed pointer still moves the stamp.
464
+ const store = getActiveWorldStore(), pointer = branchMetaPath(service, root);
465
+ const meta = store.readKept ? store.readKept(pointer) : store.read(pointer);
466
+ const inherited = inheritedHistoryKey(service, root, meta);
467
+ return { key: `${TREE_PROJECTION_VERSION}|${statKey(parentLogPath(service, root))}|${meta ?? ''}|${inherited}`, inherits: inherited !== '' };
468
+ }
469
+ const keptFacts = new WeakMap();
470
+ function treeFacts(service, root) {
471
+ const store = getActiveWorldStore();
472
+ const branchLog = branchLogPath(service, root);
473
+ if (!store.generation)
474
+ return { parentKey: parentKeyOf(service, root), branchKey: statKey(branchLog) };
475
+ const directory = dirname(branchLog);
476
+ const generation = store.generation(directory);
477
+ let kept = keptFacts.get(store);
478
+ if (!kept) {
479
+ kept = new Map();
480
+ keptFacts.set(store, kept);
481
+ }
482
+ const held = kept.get(directory);
483
+ if (generation !== null && held?.generation === generation)
484
+ return held;
485
+ const parent = parentFacts(service, root);
486
+ const facts = { parentKey: parent.key, branchKey: statKey(branchLog) };
487
+ if (generation !== null && !parent.inherits)
488
+ kept.set(directory, { ...facts, generation });
489
+ else
490
+ kept.delete(directory);
491
+ return facts;
492
+ }
427
493
  /** What a service's tree (and every subject's history) at `root` is folded from, as one string: while it
428
494
  * stands, nothing was written. A pack that projects the tree into its own state memoizes on it. */
429
- export function treeStamp(service, root) { return `${parentKeyOf(service, root)}#${statKey(branchLogPath(service, root))}`; }
495
+ export function treeStamp(service, root) { const facts = treeFacts(service, root); return `${facts.parentKey}#${facts.branchKey}`; }
430
496
  const cloneTree = (tree) => new Map([...tree].map(([k, s]) => [k, { ...s, fields: { ...s.fields } }]));
431
497
  const memoStamp = (m) => `${m.parentKey}#${m.branchKey}`;
432
498
  /** The subject keys entries write (as foldEntries folds them), and the vendor rows an alias moves a local one onto. */
@@ -484,8 +550,11 @@ function currentTree(service, root) {
484
550
  // A hit needs no lock: a memo is only ever stored from a fold made under the history lock, and while the
485
551
  // facts still match that fold, nothing was written since. A writer mid-batch has moved them.
486
552
  const unlocked = memos.get(slot);
487
- if (unlocked && unlocked.parentKey === parentKeyOf(service, root) && unlocked.branchKey === statKey(branchLogPath(service, root)))
488
- return { tree: unlocked.tree, memo: unlocked };
553
+ if (unlocked) {
554
+ const facts = treeFacts(service, root);
555
+ if (unlocked.parentKey === facts.parentKey && unlocked.branchKey === facts.branchKey)
556
+ return { tree: unlocked.tree, memo: unlocked };
557
+ }
489
558
  return withHistoryLock(worldPaths(service, root).dir, () => {
490
559
  const parentKey = parentKeyOf(service, root);
491
560
  const branchKey = statKey(branchLogPath(service, root));
@@ -497,24 +566,7 @@ function currentTree(service, root) {
497
566
  if (held && held.parentKey === parentKey && branch.length >= held.branchCount
498
567
  && (held.branchCount === 0 ? held.lastBranchId === null : branch[held.branchCount - 1]?.id === held.lastBranchId)
499
568
  && !branch.slice(held.branchCount).some((e) => e.op === 'revert')) {
500
- const fresh = branch.slice(held.branchCount);
501
- const keys = touchedKeys(fresh, held.aliases);
502
- const from = memoStamp(held);
503
- let appended;
504
- try {
505
- appended = insertionsDuring(held.tree, () => {
506
- foldEntries(held.tree, fresh, held.landed);
507
- foldAliases(held.tree, held.aliases, service, keys);
508
- });
509
- }
510
- catch (error) {
511
- memos.delete(slot);
512
- throw error;
513
- } // a half-folded memo is never read again
514
- Object.assign(held, { branchKey, branchCount: branch.length, lastBranchId: last(branch) });
515
- held.steps.push({ from, to: memoStamp(held), keys, appended });
516
- if (held.steps.length > TREE_STEPS_KEPT)
517
- held.steps.splice(0, held.steps.length - TREE_STEPS_KEPT);
569
+ extendMemo(memos, slot, held, branch.slice(held.branchCount), branchKey, service);
518
570
  return { tree: held.tree, memo: held };
519
571
  }
520
572
  const tree = readTreeMapFull(service, root);
@@ -526,7 +578,78 @@ function currentTree(service, root) {
526
578
  return { tree, memo };
527
579
  }
528
580
  return { tree };
529
- });
581
+ }, { operation: 'tree fold' });
582
+ }
583
+ /** The memo extended in place by `fresh`, the branch log's entries after its own, which leave the log at `branchKey`,
584
+ * with the step that records the subjects they touched. */
585
+ function extendMemo(memos, slot, held, fresh, branchKey, service) {
586
+ const keys = touchedKeys(fresh, held.aliases);
587
+ const from = memoStamp(held);
588
+ let appended;
589
+ try {
590
+ appended = insertionsDuring(held.tree, () => {
591
+ foldEntries(held.tree, fresh, held.landed);
592
+ foldAliases(held.tree, held.aliases, service, keys);
593
+ });
594
+ }
595
+ catch (error) {
596
+ memos.delete(slot);
597
+ throw error;
598
+ } // a half-folded memo is never read again
599
+ Object.assign(held, { branchKey, branchCount: held.branchCount + fresh.length, lastBranchId: fresh.length ? fresh[fresh.length - 1].id : held.lastBranchId });
600
+ held.steps.push({ from, to: memoStamp(held), keys, appended });
601
+ if (held.steps.length > TREE_STEPS_KEPT)
602
+ held.steps.splice(0, held.steps.length - TREE_STEPS_KEPT);
603
+ }
604
+ // AN OWN APPEND EXTENDS THE READS' MEMO (S2 R17c). A write appended its entries and the next read found the facts
605
+ // moved, so it took the history lock, read the facts again and the log's new bytes, and extended the memo with the
606
+ // entries this process had just written: in a tab, where each of those is a call into the kernel, about twenty calls
607
+ // per command that writes, taken again by the command after it. The writer, still holding the history lock, extends
608
+ // the memo with its own entries and keeps the tree's facts under the generation its own write left, so the next read
609
+ // asks one question. It does so only where the store can say its write was the one write since the generation it
610
+ // read just before (`generationAfterOwnWrite`), the facts were kept under that generation and the memo stood at them,
611
+ // the parent side's facts read after the append are still the kept ones, and the log grew by exactly its bytes;
612
+ // otherwise it extends nothing, and the next read checks as before.
613
+ /** Append `data` (whole JSONL lines of a service's branch log, `append` writing them) and, when the reads' memo held
614
+ * the tree as it stood, extend it by them. Called holding the directory's history lock. Answers the log's facts
615
+ * after the append (`size:mtimeMs`) when it read them, for a caller keyed on the same. */
616
+ export function appendToBranchLog(service, root, data, append) {
617
+ const store = getActiveWorldStore();
618
+ const memos = treeMemos.get(store);
619
+ const slot = `${service}\u0000${root ?? ''}`;
620
+ const held = memos?.get(slot);
621
+ const branchLog = branchLogPath(service, root);
622
+ const directory = dirname(branchLog);
623
+ const kept = keptFacts.get(store)?.get(directory);
624
+ const standing = held !== undefined && kept !== undefined && kept.parentKey === held.parentKey && kept.branchKey === held.branchKey;
625
+ const before = standing && store.generation && store.generationAfterOwnWrite ? store.generation(directory) : null;
626
+ append();
627
+ if (!memos || !held || !kept || before === null || kept.generation !== before)
628
+ return undefined;
629
+ const after = store.generationAfterOwnWrite(directory, before);
630
+ if (after === null)
631
+ return undefined;
632
+ // read after the generation, as treeFacts reads its facts. The parent side is read again too: a writer the generation
633
+ // does not see (an older version's, or one that died before its second byte) may have moved it under the kept
634
+ // generation, and carried forward its old key would outlive every append this process makes (S2 R17c review).
635
+ const branchKey = statKey(branchLog);
636
+ if (parentKeyOf(service, root) !== kept.parentKey)
637
+ return branchKey;
638
+ const grown = (key) => (key === '-' ? 0 : Number(key.slice(0, key.indexOf(':'))));
639
+ const fresh = [];
640
+ // line numbers in a refusal count from this append's first line
641
+ parseLines(`${branchLog} (appended)`, data.split('\n'), 0, fresh);
642
+ if (grown(branchKey) !== grown(held.branchKey) + Buffer.byteLength(data) || fresh.some((e) => e.op === 'revert'))
643
+ return branchKey;
644
+ // the write has landed: a fold that fails here drops the memo (extendMemo) and the next read folds in full, never failing the write
645
+ try {
646
+ extendMemo(memos, slot, held, fresh, branchKey, service);
647
+ }
648
+ catch {
649
+ return branchKey;
650
+ }
651
+ keptFacts.get(store).set(directory, { parentKey: kept.parentKey, branchKey, generation: after });
652
+ return branchKey;
530
653
  }
531
654
  /** What changed in a service's tree since `since` (a `treeStamp` an earlier read was made at): the subjects
532
655
  * written since, as resources; the keys (`type:id`) of those no longer in the tree; and, of the changed, the keys
@@ -637,7 +760,7 @@ function readTreeMapFull(service, root, opts = {}) {
637
760
  getActiveWorldStore().writeAtomic(checkpointPath(service, root), JSON.stringify({ parentDigest, branchDigest, branchCount: branch.length, subjects: [...tree.values()], ...(now === facts ? { facts } : {}) }));
638
761
  }
639
762
  return tree;
640
- });
763
+ }, { operation: 'tree fold' });
641
764
  }
642
765
  export function cutCheckpoint(service, root) {
643
766
  return withHistoryLock(worldPaths(service, root).dir, () => {
@@ -646,7 +769,7 @@ export function cutCheckpoint(service, root) {
646
769
  const inherited = inheritedHistory(service, root);
647
770
  const own = ownParentEntries(service, root);
648
771
  getActiveWorldStore().writeAtomic(checkpointPath(service, root), JSON.stringify({ parentDigest: hashFieldValue({ projectionVersion: TREE_PROJECTION_VERSION, inherited, own }), branchDigest: hashFieldValue(branchEntries(service, root)), branchCount: branchEntries(service, root).length, subjects }));
649
- });
772
+ }, { operation: 'checkpoint' });
650
773
  }
651
774
  /** The branch's entries the parent does not hold: `set` rows, not reverted, not landed. */
652
775
  export function unpushedEntries(service, root) {
@@ -745,7 +868,7 @@ export function appendParentEntry(entry, root) {
745
868
  index.ids.add(entry.id);
746
869
  index.key = statKey(path);
747
870
  return { appended: true };
748
- }));
871
+ }, { operation: 'parent append' }), { operation: 'parent append' });
749
872
  }
750
873
  /** The whole log of a twin as a served world hands it out: the parent view, then the branch's own
751
874
  * entries, one sequence with a position. A clone's parent is exactly this. */
@@ -823,7 +946,7 @@ export function rebaseBranch(service, root, opts = {}) {
823
946
  dropCheckpoint(service, root);
824
947
  }
825
948
  return { moved, from, to, conflicts };
826
- });
949
+ }, { operation: 'branch-pointer adjustment' });
827
950
  }
828
951
  /**
829
952
  * HISTORY through the kernel: every entry that touched a subject (or every subject of a type), parent