@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
@@ -46,7 +46,8 @@
46
46
  import { createHash } from 'node:crypto';
47
47
  import { projectResources } from "../actions.js";
48
48
  import { treeChangesSince, treeStamp } from "../log.js";
49
- import { applyTwinWrite } from "../serve.js";
49
+ import { redisServiceTimes as times } from "./service-times.js";
50
+ import { applyTwinWrites } from "../serve.js";
50
51
  import { getActiveWorldStore } from "../world-store.js";
51
52
  import { LUA_NULL, luaScriptSha1, LuaError, LuaTable, runLua } from "./lua.js";
52
53
  import { inlineCommand } from "./frames.js";
@@ -311,12 +312,68 @@ function patchImage(image, changed, removed, scope) {
311
312
  }
312
313
  return true;
313
314
  }
315
+ const GONE = Symbol('gone');
316
+ /**
317
+ * A run's map over the shared image: what the run changed is its own, the rest is read through. Copying the whole image
318
+ * per run (and again per script, for its rollback) cost every command the size of the keyspace (S2 R19: Twenty's queue
319
+ * twin, its queue scripts waiting behind each other's copies). Iteration is a `new Map(base)` changed in place's: the
320
+ * base's order with its changed values, then the keys added, a base key deleted and set again among them at its set.
321
+ */
322
+ class Overlay {
323
+ base;
324
+ own = new Map();
325
+ moved = new Set();
326
+ constructor(base) {
327
+ this.base = base;
328
+ }
329
+ get(key) {
330
+ const mine = this.own.get(key);
331
+ return mine === GONE ? undefined : mine !== undefined || this.own.has(key) ? mine : this.base.get(key);
332
+ }
333
+ has(key) { return this.own.has(key) ? this.own.get(key) !== GONE : this.base.has(key); }
334
+ set(key, value) {
335
+ if (this.own.get(key) === GONE) {
336
+ this.own.delete(key);
337
+ this.moved.add(key);
338
+ }
339
+ this.own.set(key, value);
340
+ }
341
+ delete(key) {
342
+ this.own.delete(key);
343
+ this.moved.delete(key);
344
+ if (this.base.has(key))
345
+ this.own.set(key, GONE);
346
+ }
347
+ *entries() {
348
+ for (const [key, value] of this.base) {
349
+ if (this.moved.has(key))
350
+ continue;
351
+ if (!this.own.has(key)) {
352
+ yield [key, value];
353
+ continue;
354
+ }
355
+ const mine = this.own.get(key);
356
+ if (mine !== GONE)
357
+ yield [key, mine];
358
+ }
359
+ for (const [key, mine] of this.own)
360
+ if (mine !== GONE && (!this.base.has(key) || this.moved.has(key)))
361
+ yield [key, mine];
362
+ }
363
+ *keys() { for (const [key] of this.entries())
364
+ yield key; }
365
+ *values() { for (const [, value] of this.entries())
366
+ yield value; }
367
+ /** What the run changed, copied: a rollback point the size of the changes, not of the keyspace. */
368
+ save() { return { own: new Map(this.own), moved: new Set(this.moved) }; }
369
+ load(state) { this.own = new Map(state.own); this.moved = new Set(state.moved); }
370
+ }
314
371
  /**
315
372
  * A synchronous, in-memory image of the keyspace for the duration of ONE request.
316
373
  *
317
374
  * Seeded from the keyspace's image (the tree folded by `projectResources`, memoized per World state — the kernel IS
318
375
  * the source of truth); every mutation records the key name in `touched`, and the flush writes exactly those keys
319
- * back. This run's maps are its own copies: nothing a run changes reaches the image or a later request except
376
+ * back. This run owns its overlays: nothing a run changes reaches the image or a later request except
320
377
  * through the tree.
321
378
  */
322
379
  export class KeySpace {
@@ -328,16 +385,16 @@ export class KeySpace {
328
385
  touchedScripts = new Map();
329
386
  /** name → the last write ordinal seen, INCLUDING for keys currently deleted or expired. */
330
387
  revs;
331
- /** The keyspace as the tree held it when this run began: shared, never changed in place. */
388
+ /** The shared base maps are patched by the next image(); a KeySpace must finish reading its base before it yields. */
332
389
  image;
333
390
  constructor(ctx, nowMs) {
334
391
  this.ctx = ctx;
335
392
  this.nowMs = nowMs;
336
- // the image is shared and never changed in place; this run's maps are its own
393
+ // The base maps are patched by the next image(); finish reading this run's base before yielding.
337
394
  const image = this.image = ctx.dialect.storage?.image(ctx) ?? keyspaceImageOf(ctx);
338
- this.rows = new Map(image.rows);
339
- this.scripts = new Map(image.scripts);
340
- this.revs = new Map(image.revs);
395
+ this.rows = new Overlay(image.rows);
396
+ this.scripts = new Overlay(image.scripts);
397
+ this.revs = new Overlay(image.revs);
341
398
  }
342
399
  /**
343
400
  * Is this row expired AS OF NOW? Checked on every read, not merely when the snapshot was built.
@@ -394,18 +451,14 @@ export class KeySpace {
394
451
  }
395
452
  /** A point-in-time copy, so a failed script can be rolled back to exactly where it started. */
396
453
  snapshot() {
397
- return { rows: new Map(this.rows), touched: new Map(this.touched), scripts: new Map(this.scripts), touchedScripts: new Map(this.touchedScripts) };
454
+ return { rows: this.rows.save(), touched: new Map(this.touched), scripts: this.scripts.save(), touchedScripts: new Map(this.touchedScripts) };
398
455
  }
399
456
  restore(s) {
400
- this.rows.clear();
401
- for (const [k, v] of s.rows)
402
- this.rows.set(k, v);
457
+ this.rows.load(s.rows);
403
458
  this.touched.clear();
404
459
  for (const [k, v] of s.touched)
405
460
  this.touched.set(k, v);
406
- this.scripts.clear();
407
- for (const [k, v] of s.scripts)
408
- this.scripts.set(k, v);
461
+ this.scripts.load(s.scripts);
409
462
  this.touchedScripts.clear();
410
463
  for (const [k, v] of s.touchedScripts)
411
464
  this.touchedScripts.set(k, v);
@@ -447,8 +500,10 @@ export async function restoreKeyspace(image, ctx) {
447
500
  await flushKeySpace(space);
448
501
  }
449
502
  async function flushKeySpace(space) {
503
+ // a run's writes land together, in this order, as they landed one at a time (serve.ts applyTwinWrites)
504
+ const writes = [];
450
505
  for (const { name, operation, row } of space.pendingWrites()) {
451
- await applyTwinWrite(space.ctx.service, {
506
+ writes.push({
452
507
  operation,
453
508
  subjectType: 'key',
454
509
  subjectId: `${scopeOf(space.ctx)}key:${name}`,
@@ -460,18 +515,19 @@ async function flushKeySpace(space) {
460
515
  : { name, kind: row.kind, v: row.v, pexpire_at: row.pexpireAt, last_id: row.lastId ?? '', gone: false, _rev: space.nextRev(name) },
461
516
  occurredAt: space.ctx.occurredAt,
462
517
  actor: { kind: 'agent' },
463
- }, space.ctx.root);
518
+ });
464
519
  }
465
520
  for (const { sha, operation, body } of space.pendingScriptWrites()) {
466
- await applyTwinWrite(space.ctx.service, {
521
+ writes.push({
467
522
  operation,
468
523
  subjectType: 'script',
469
524
  subjectId: `${scopeOf(space.ctx)}script:${sha}`,
470
525
  fields: body === null ? { sha, body: '', gone: true } : { sha, body, gone: false },
471
526
  occurredAt: space.ctx.occurredAt,
472
527
  actor: { kind: 'agent' },
473
- }, space.ctx.root);
528
+ });
474
529
  }
530
+ await applyTwinWrites(space.ctx.service, writes, space.ctx.root);
475
531
  }
476
532
  // ── typed value accessors (each asserts the key's type first) ─────────────────────────────────
477
533
  export function expectType(row, kind) {
@@ -1905,23 +1961,38 @@ export async function execRedisRun(commands, ctx) {
1905
1961
  const nowMs = Date.parse(ctx.occurredAt);
1906
1962
  if (!Number.isFinite(nowMs))
1907
1963
  throw new RedisCommandError(`ERR twin: unparseable occurredAt '${ctx.occurredAt}'`);
1908
- const space = new KeySpace(ctx, nowMs);
1964
+ // The phases a served command's time is counted in (service-times.ts): the image, the commands, their writes.
1965
+ const space = times.timed('image', () => new KeySpace(ctx, nowMs));
1909
1966
  const items = [];
1910
- for (const argv of commands) {
1911
- try {
1912
- items.push({ result: execOne(space, argv) });
1913
- }
1914
- catch (e) {
1915
- if (e instanceof ReadOnlyError)
1916
- throw e; // surfaces as HTTP 405 for the whole request
1917
- if (e instanceof RedisCommandError) {
1918
- items.push({ error: e.message });
1919
- continue;
1967
+ const began = times.now();
1968
+ try {
1969
+ for (const argv of commands) {
1970
+ try {
1971
+ items.push({ result: execOne(space, argv) });
1972
+ }
1973
+ catch (e) {
1974
+ if (e instanceof ReadOnlyError)
1975
+ throw e; // surfaces as HTTP 405 for the whole request
1976
+ if (e instanceof RedisCommandError) {
1977
+ items.push({ error: e.message });
1978
+ continue;
1979
+ }
1980
+ throw e;
1920
1981
  }
1921
- throw e;
1922
1982
  }
1923
1983
  }
1984
+ finally {
1985
+ times.add('execute', times.now() - began);
1986
+ }
1924
1987
  const wrote = space.dirty;
1925
- await (space.ctx.dialect.storage?.flush(space) ?? flushKeySpace(space));
1926
- return { items, wrote, instant: nowMs };
1988
+ // The keys this run wrote, by name, read before the flush clears them: a stream wakes only the clients blocked on one.
1989
+ const touched = space.touchedNames();
1990
+ const flushing = times.now();
1991
+ try {
1992
+ await (space.ctx.dialect.storage?.flush(space) ?? flushKeySpace(space));
1993
+ }
1994
+ finally {
1995
+ times.add('flush', times.now() - flushing);
1996
+ }
1997
+ return { items, wrote, instant: nowMs, touched };
1927
1998
  }
@@ -1,12 +1,14 @@
1
1
  // Redis's collection layout over the World tree. First need: the managed Redis queue's growing events stream.
2
2
  // Key metadata and each hash field/set member/sorted-set member/stream entry are separate subjects.
3
3
  import { createHash } from 'node:crypto';
4
- import { applyTwinWrite } from "../serve.js";
4
+ import { applyTwinWrites } from "../serve.js";
5
5
  import { projectResources } from "../actions.js";
6
6
  import { treeStamp, treeChangesSince } from "../log.js";
7
7
  import { getActiveWorldStore } from "../world-store.js";
8
8
  const scope = (c) => c.database === undefined ? '' : `db:${c.database}:`;
9
9
  const collections = new Set(['hash', 'set', 'zset', 'stream', 'list']);
10
+ // `byKey` holds each key's member subjects in `subjects`' own order, so rebuilding one key reads its members, not every
11
+ // subject of the service (S2 R19: the image phase was up to 560 ms of a 2 s window of Twenty's queue twin).
10
12
  const held = new WeakMap();
11
13
  const members = (r) => !r || !collections.has(r.kind) ? new Map() : new Map(r.v.map((x, i) => r.kind === 'list' ? [String(i), x] : r.kind === 'set' ? [x, x] : [x[0], x[1]]));
12
14
  const memberId = (c, name, field) => `${scope(c)}member:${createHash('sha256').update(JSON.stringify([name, field])).digest('hex')}`;
@@ -21,9 +23,16 @@ export const memberStorage = {
21
23
  return h.image;
22
24
  const delta = h && treeChangesSince(ctx.service, ctx.root, h.stamp);
23
25
  if (!h || !delta || delta.removed.length)
24
- h = { stamp, subjects: new Map(), image: { rows: new Map(), scripts: new Map(), revs: new Map() } };
25
- for (const r of delta && !delta.removed.length ? delta.changed : projectResources(ctx.service, ctx.root))
26
+ h = { stamp, subjects: new Map(), byKey: new Map(), image: { rows: new Map(), scripts: new Map(), revs: new Map() } };
27
+ for (const r of delta && !delta.removed.length ? delta.changed : projectResources(ctx.service, ctx.root)) {
26
28
  h.subjects.set(r.id, r);
29
+ if (r.type === 'member') {
30
+ let of = h.byKey.get(r.key);
31
+ if (!of)
32
+ h.byKey.set(r.key, of = new Map());
33
+ of.set(r.id, r);
34
+ }
35
+ }
27
36
  // Reconstruct only keys affected by the tree delta, not every member on every read.
28
37
  const affected = new Set();
29
38
  const changed = delta && !delta.removed.length ? delta.changed : [...h.subjects.values()];
@@ -50,7 +59,7 @@ export const memberStorage = {
50
59
  h.image.rows.delete(name);
51
60
  continue;
52
61
  }
53
- const v = collections.has(meta.kind) ? [...h.subjects.values()].filter((r) => r.type === 'member' && r.id.startsWith(scope(ctx)) && r.key === name && r.gone !== true).sort((a, b) => meta.kind === 'list' ? Number(a.field) - Number(b.field) : 0).map((r) => meta.kind === 'list' ? r.value : meta.kind === 'set' ? r.field : [r.field, r.value]) : meta.v;
62
+ const v = collections.has(meta.kind) ? [...(h.byKey.get(name)?.values() ?? [])].filter((r) => r.id.startsWith(scope(ctx)) && r.gone !== true).sort((a, b) => meta.kind === 'list' ? Number(a.field) - Number(b.field) : 0).map((r) => meta.kind === 'list' ? r.value : meta.kind === 'set' ? r.field : [r.field, r.value]) : meta.v;
54
63
  h.image.rows.set(name, { name, kind: meta.kind, v, pexpireAt: meta.pexpire_at == null ? null : String(meta.pexpire_at), lastId: meta.last_id, _rev: meta._rev });
55
64
  }
56
65
  h.stamp = stamp;
@@ -59,17 +68,20 @@ export const memberStorage = {
59
68
  },
60
69
  async flush(space) {
61
70
  const ctx = space.ctx;
62
- const write = async (type, id, operation, fields) => applyTwinWrite(ctx.service, { operation, subjectType: type, subjectId: id, fields, occurredAt: ctx.occurredAt, actor: { kind: 'agent' } }, ctx.root);
71
+ // a run's writes land together, in this order, as they landed one at a time (serve.ts applyTwinWrites)
72
+ const writes = [];
73
+ const write = (type, id, operation, fields) => { writes.push({ operation, subjectType: type, subjectId: id, fields, occurredAt: ctx.occurredAt, actor: { kind: 'agent' } }); };
63
74
  for (const { name, row, operation } of space.pendingWrites()) {
64
75
  const before = members(space.image.rows.get(name) ?? null), after = members(row);
65
76
  for (const field of new Set([...before.keys(), ...after.keys()])) {
66
77
  if (before.has(field) && after.has(field) && JSON.stringify(before.get(field)) === JSON.stringify(after.get(field)))
67
78
  continue;
68
- await write('member', memberId(ctx, name, field), operation, { key: name, field, value: after.get(field) ?? null, gone: !after.has(field) });
79
+ write('member', memberId(ctx, name, field), operation, { key: name, field, value: after.get(field) ?? null, gone: !after.has(field) });
69
80
  }
70
- await write('key', `${scope(ctx)}key:${name}`, operation, { name, kind: row?.kind ?? 'string', v: row && !collections.has(row.kind) ? row.v : null, pexpire_at: row?.pexpireAt ?? null, last_id: row?.lastId ?? '', gone: row === null, _rev: space.nextRev(name) });
81
+ write('key', `${scope(ctx)}key:${name}`, operation, { name, kind: row?.kind ?? 'string', v: row && !collections.has(row.kind) ? row.v : null, pexpire_at: row?.pexpireAt ?? null, last_id: row?.lastId ?? '', gone: row === null, _rev: space.nextRev(name) });
71
82
  }
72
83
  for (const { sha, body, operation } of space.pendingScriptWrites())
73
- await write('script', `${scope(ctx)}script:${sha}`, operation, { sha, body: body ?? '', gone: body === null });
84
+ write('script', `${scope(ctx)}script:${sha}`, operation, { sha, body: body ?? '', gone: body === null });
85
+ await applyTwinWrites(ctx.service, writes, ctx.root);
74
86
  },
75
87
  };
@@ -0,0 +1,25 @@
1
+ type Phase = 'image' | 'execute' | 'flush' | 'reply' | 'run';
2
+ type Command = {
3
+ session: number;
4
+ command: string;
5
+ };
6
+ export declare const redisServiceTimes: {
7
+ now: () => number;
8
+ /** A command starts: `received` is when its bytes were read, `waiting` the commands read and not yet started. */
9
+ begin(received: number, waiting: number, command: Command): void;
10
+ /** Time of the open command in one phase. */
11
+ add(phase: Phase, ms: number): void;
12
+ wrote(): void;
13
+ /** A run begins: its first stretch is counted from here. */
14
+ lapFrom(): void;
15
+ /** The stretch of the open command's run that ends here, by name. Outside a served command it is nothing. */
16
+ lap(name: string): void;
17
+ /** A run has ended: nothing after it is a stretch of it. */
18
+ lapEnd(name: string): void;
19
+ /** The open command has answered (or blocked, or failed with its error answered). */
20
+ end(threw?: boolean): void;
21
+ /** `work`, its time counted in `phase` of the open command. */
22
+ timed<T>(phase: Phase, work: () => T): T;
23
+ phases: readonly Phase[];
24
+ };
25
+ export {};
@@ -0,0 +1,87 @@
1
+ const PHASES = ['image', 'execute', 'flush', 'reply', 'run'];
2
+ const SAY_EVERY_MS = 2000;
3
+ const now = () => performance.now();
4
+ const fresh = () => ({ service: [], waited: 0, waitedMax: 0, own: 0, phases: { image: 0, execute: 0, flush: 0, reply: 0, run: 0 }, runs: 0, queueMax: 0, writes: 0, laps: {} });
5
+ // THE PARTS OF ONE RUN, in the order it passes them: a run is the pack's own request to itself, carried through the
6
+ // whole of a pack's fetch (pack-fetch.ts) to the command and back. Each `lap` names the stretch that ended there,
7
+ // from the lap before it; the stretches of a window's runs are summed by name as `runPartsMs`. A stretch holds
8
+ // whatever else this realm did between its two ends: under traffic that is other connections' bytes arriving.
9
+ let lapAt = 0;
10
+ let window = fresh();
11
+ let open;
12
+ let timer;
13
+ let served = 0;
14
+ function say() {
15
+ timer = undefined;
16
+ const w = window;
17
+ window = fresh();
18
+ if (w.service.length === 0)
19
+ return;
20
+ const sorted = [...w.service].sort((a, b) => a - b);
21
+ const at = (share) => sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * share))];
22
+ const ms = (value) => Math.round(value * 10) / 10;
23
+ const inRun = w.phases.image + w.phases.execute + w.phases.flush;
24
+ console.error('[redis-twin]', JSON.stringify({
25
+ commands: w.service.length, servedSoFar: served, runs: w.runs, wrote: w.writes,
26
+ // from a command's bytes read to its reply written, its wait behind the commands before it included
27
+ serviceMs: { total: ms(w.service.reduce((sum, each) => sum + each, 0)), p50: ms(at(0.5)), p95: ms(at(0.95)), max: ms(sorted[sorted.length - 1]) },
28
+ waitedMs: { total: ms(w.waited), max: ms(w.waitedMax) },
29
+ phasesMs: { context: ms(w.phases.run - inRun), image: ms(w.phases.image), execute: ms(w.phases.execute), flush: ms(w.phases.flush), reply: ms(w.phases.reply), other: ms(w.own - w.phases.run - w.phases.reply) },
30
+ runPartsMs: Object.fromEntries(Object.entries(w.laps).map(([name, value]) => [name, ms(value)])),
31
+ queueMax: w.queueMax,
32
+ slowest: w.slowest ? { ...w.slowest, serviceMs: ms(w.slowest.serviceMs), waitedMs: ms(w.slowest.waitedMs), ownMs: ms(w.slowest.ownMs) } : null,
33
+ }));
34
+ }
35
+ export const redisServiceTimes = {
36
+ now,
37
+ /** A command starts: `received` is when its bytes were read, `waiting` the commands read and not yet started. */
38
+ begin(received, waiting, command) {
39
+ open = { received, started: now(), command };
40
+ if (waiting > window.queueMax)
41
+ window.queueMax = waiting;
42
+ if (!timer) {
43
+ timer = setTimeout(say, SAY_EVERY_MS);
44
+ timer.unref?.();
45
+ }
46
+ },
47
+ /** Time of the open command in one phase. */
48
+ add(phase, ms) { if (open) {
49
+ window.phases[phase] += ms;
50
+ if (phase === 'run')
51
+ window.runs += 1;
52
+ } },
53
+ wrote() { if (open)
54
+ window.writes += 1; },
55
+ /** A run begins: its first stretch is counted from here. */
56
+ lapFrom() { lapAt = now(); },
57
+ /** The stretch of the open command's run that ends here, by name. Outside a served command it is nothing. */
58
+ lap(name) { if (!open || lapAt === 0)
59
+ return; const at = now(); window.laps[name] = (window.laps[name] ?? 0) + (at - lapAt); lapAt = at; },
60
+ /** A run has ended: nothing after it is a stretch of it. */
61
+ lapEnd(name) { this.lap(name); lapAt = 0; },
62
+ /** The open command has answered (or blocked, or failed with its error answered). */
63
+ end(threw = false) {
64
+ if (!open)
65
+ return;
66
+ const ended = now(), waited = open.started - open.received;
67
+ const serviceMs = ended - open.received;
68
+ if (!window.slowest || serviceMs > window.slowest.serviceMs) {
69
+ window.slowest = { ...open.command, threw, serviceMs, waitedMs: waited, ownMs: ended - open.started };
70
+ }
71
+ window.service.push(ended - open.received);
72
+ window.waited += waited;
73
+ if (waited > window.waitedMax)
74
+ window.waitedMax = waited;
75
+ window.own += ended - open.started;
76
+ served += 1;
77
+ open = undefined;
78
+ },
79
+ /** `work`, its time counted in `phase` of the open command. */
80
+ timed(phase, work) { const began = now(); try {
81
+ return work();
82
+ }
83
+ finally {
84
+ this.add(phase, now() - began);
85
+ } },
86
+ phases: PHASES,
87
+ };
@@ -9,6 +9,7 @@ export type RedisStreamOptions = {
9
9
  items: RunItem[];
10
10
  wrote: boolean;
11
11
  instant?: number;
12
+ touched?: string[];
12
13
  }>;
13
14
  };
14
- export declare function createRedisStream(options: RedisStreamOptions): TwinStream;
15
+ export declare function createRedisStream(given: RedisStreamOptions): TwinStream;
@@ -1,15 +1,38 @@
1
1
  import { RedisStatus, commandInteger, commandShapeError, globMatch } from "./engine.js";
2
2
  import { binaryBytes, binaryText, inlineCommand, readResp, respReply } from "./frames.js";
3
- export function createRedisStream(options) {
3
+ import { worldNow } from "../world-clock.js";
4
+ import { redisServiceTimes as times } from "./service-times.js";
5
+ export function createRedisStream(given) {
6
+ // The pack's run, its time counted for the command it serves (service-times.ts).
7
+ const options = { ...given, run: async (commands, database) => { const began = times.now(); times.lapFrom(); try {
8
+ const ran = await given.run(commands, database);
9
+ if (ran.wrote)
10
+ times.wrote();
11
+ return ran;
12
+ }
13
+ finally {
14
+ times.lapEnd('8 answer read by the stream');
15
+ times.add('run', times.now() - began);
16
+ } } };
4
17
  const sessions = new Set();
5
- let nextId = 0, commandCount = 0;
18
+ let nextId = 0, commandCount = 0, waiting = 0;
6
19
  let serial = Promise.resolve();
7
20
  const ordered = (f) => { const p = serial.then(f); serial = p.catch(() => { }); return p; };
8
21
  const error = (message) => ({ error: message });
9
22
  const ok = new RedisStatus('OK');
10
- const answer = (s, v) => { if (!s.closed)
11
- s.sink.write(binaryBytes(respReply(v))); };
12
- const close = (s) => { s.closed = true; if (s.blocked?.timer)
23
+ const connectionTrace = (s, stage, command) => {
24
+ try {
25
+ console.error('[redis-twin-connection]', JSON.stringify({ at: performance.timeOrigin + times.now(), pid: globalThis.process?.pid ?? 0,
26
+ session: s.id, stage, received: s.received, answered: s.answered, ...(command ? { command } : {}) }));
27
+ }
28
+ catch { /* diagnostic output cannot change a connection */ }
29
+ };
30
+ const answer = (s, v) => { if (!s.closed) {
31
+ times.timed('reply', () => s.sink.write(binaryBytes(respReply(v))));
32
+ s.answered += 1;
33
+ } };
34
+ const close = (s) => { if (!s.closed)
35
+ connectionTrace(s, 'closed'); s.closed = true; if (s.blocked?.timer)
13
36
  clearTimeout(s.blocked.timer); sessions.delete(s); };
14
37
  const unblock = (s, v) => { if (s.blocked?.timer)
15
38
  clearTimeout(s.blocked.timer); s.blocked = undefined; answer(s, v); queueMicrotask(() => { void s.pump?.(); }); };
@@ -26,12 +49,26 @@ export function createRedisStream(options) {
26
49
  }
27
50
  throw new Error('the blocking command has no executor');
28
51
  };
29
- const wake = async () => { for (const s of sessions)
30
- if (s.blocked) {
52
+ // A CLIENT BLOCKED ON KEYS IS ASKED AGAIN ONLY WHEN ONE OF ITS KEYS WAS WRITTEN. Every write asked every blocked
53
+ // client again, each ask a whole run for each of its keys: Twenty's queue workers hold a blocked client apiece, and
54
+ // its twin made 59 to 89 runs for 31 to 41 commands in a two-second window, each run about 20 ms, with every
55
+ // other command waiting behind them (Twenty 51: commands waited 0.6 to 5.9 s). `touched` is the keys the run
56
+ // wrote, by name, whatever its database (a name written in another database asks a client that then finds
57
+ // nothing, never the reverse). Without it (an EXEC's queue, a run that does not say, a command that writes
58
+ // another database's keys) every blocked client is asked, as before.
59
+ const wake = async (touched) => {
60
+ for (const s of sessions) {
61
+ if (!s.blocked)
62
+ continue;
63
+ if (touched && !s.blocked.command.slice(1, -1).some((key) => touched.includes(key)))
64
+ continue;
31
65
  const v = await blockRun(s, s.blocked.command);
32
66
  if (v !== null)
33
67
  unblock(s, v);
34
- } };
68
+ }
69
+ };
70
+ /** Commands that write keys of a database other than their session's: their run's `touched` does not name them all. */
71
+ const CROSS_DATABASE = new Set(['MOVE', 'COPY', 'SWAPDB']);
35
72
  const command = async (s, argv, executing = false) => {
36
73
  if (!argv.length || s.closed)
37
74
  return;
@@ -272,12 +309,32 @@ export function createRedisStream(options) {
272
309
  }
273
310
  const initial = await options.run([], s.database);
274
311
  s.blocked = { command: argv, deadline: timeout === 0 ? Infinity : (initial.instant ?? 0) + timeout };
275
- const poll = () => { if (!s.blocked || s.closed)
276
- return; s.blocked.timer = setTimeout(() => { void ordered(async () => { if (!s.blocked)
277
- return; const now = await options.run([], s.database); if ((now.instant ?? 0) >= s.blocked.deadline)
278
- unblock(s, null);
279
- else
280
- poll(); }); }, 50); };
312
+ // A BLOCKED CLIENT WAITS FOR A WRITE OR ITS DEADLINE, AND ASKS THE TREE NOTHING MEANWHILE. A write through this
313
+ // twin wakes it (wake, after every command that wrote). Its deadline is in the World's time, so the timer
314
+ // reads the World's clock (world-clock.ts worldNow: one check of the clock file, kept while it stands) and
315
+ // nothing else. It ran a whole empty request every 50 ms to read that time: the pack's fetch, its context
316
+ // and the keyspace's image with its tree stamp, for every blocked client, the whole time it was blocked.
317
+ // Twenty's queue workers each hold one blocked client: its Redis twin made about 18,700 stamps in a run, 56,145
318
+ // stat requests, and spent 55 s of 114 busy in them, with no command to answer. No timeout is no timer; a
319
+ // deadline far off is checked less often, never more than a second apart, and to 50 ms as it nears.
320
+ const poll = () => {
321
+ if (!s.blocked || s.closed || s.blocked.deadline === Infinity)
322
+ return;
323
+ let left;
324
+ try {
325
+ left = s.blocked.deadline - Date.parse(worldNow());
326
+ }
327
+ catch (cause) {
328
+ unblock(s, error(`ERR twin: ${cause instanceof Error ? cause.message : String(cause)}`.replace(/[\r\n]+/g, ' ')));
329
+ return;
330
+ }
331
+ if (left <= 0) {
332
+ void ordered(async () => { if (s.blocked)
333
+ unblock(s, null); });
334
+ return;
335
+ }
336
+ s.blocked.timer = setTimeout(poll, Math.max(50, Math.min(1000, left / 2)));
337
+ };
281
338
  poll();
282
339
  return;
283
340
  }
@@ -286,17 +343,45 @@ export function createRedisStream(options) {
286
343
  const item = res.items[0];
287
344
  answer(s, 'error' in item ? item : item.result);
288
345
  if (res.wrote && !executing)
289
- await wake();
346
+ await wake(CROSS_DATABASE.has(cmd) ? undefined : res.touched);
290
347
  }
291
348
  }
292
349
  };
293
350
  return (sink) => {
294
- const s = { id: ++nextId, name: '', lib: {}, database: '0', authenticated: !options.password, queue: null, dirty: false, subscriptions: new Set(), patterns: new Set(), sink, closed: false };
351
+ const s = { id: ++nextId, name: '', lib: {}, database: '0', authenticated: !options.password, queue: null, dirty: false, subscriptions: new Set(), patterns: new Set(), sink, closed: false, received: 0, answered: 0 };
295
352
  sessions.add(s);
353
+ connectionTrace(s, 'opened');
296
354
  let buffer = '', chain = Promise.resolve();
297
355
  const pending = [];
298
- const pump = async () => { while (pending.length && !s.blocked && !s.closed)
299
- await ordered(() => command(s, pending.shift())); };
356
+ // A command whose storage fails (a World storage lock that timed out, a write that could not be made) answers its
357
+ // client with an error and the session goes on, as Redis answers one failed command: it was an unhandled
358
+ // rejection, and the twin's process exited with every client's connection (Twenty 46).
359
+ const pump = async () => {
360
+ while (pending.length && !s.blocked && !s.closed) {
361
+ const { argv, received } = pending.shift();
362
+ try {
363
+ await ordered(async () => {
364
+ waiting -= 1;
365
+ times.begin(received, waiting, { session: s.id, command: argv[0]?.toUpperCase().slice(0, 32) ?? '' });
366
+ let threw = false;
367
+ try {
368
+ await command(s, argv);
369
+ }
370
+ catch (cause) {
371
+ threw = true;
372
+ throw cause;
373
+ }
374
+ finally {
375
+ times.end(threw);
376
+ }
377
+ });
378
+ }
379
+ catch (cause) {
380
+ if (!s.closed)
381
+ answer(s, error(`ERR twin: ${cause instanceof Error ? cause.message : String(cause)}`.replace(/[\r\n]+/g, ' ')));
382
+ }
383
+ }
384
+ };
300
385
  s.pump = pump;
301
386
  return {
302
387
  data(chunk) { if (s.closed)
@@ -321,7 +406,11 @@ export function createRedisStream(options) {
321
406
  end = at + 2;
322
407
  }
323
408
  buffer = buffer.slice(end);
324
- pending.push(argv);
409
+ pending.push({ argv, received: times.now() });
410
+ waiting += 1;
411
+ s.received += 1;
412
+ if (s.received === 1)
413
+ connectionTrace(s, 'first-command', argv[0]?.toUpperCase().slice(0, 32));
325
414
  }
326
415
  chain = chain.then(pump);
327
416
  }
@@ -331,7 +420,7 @@ export function createRedisStream(options) {
331
420
  sink.end();
332
421
  } },
333
422
  settled: () => chain,
334
- close: () => close(s),
423
+ close: () => { waiting -= pending.length; pending.length = 0; close(s); },
335
424
  };
336
425
  };
337
426
  }
@@ -1,7 +1,11 @@
1
+ import { type BlobWritten } from './blob-store.js';
1
2
  /** Read a resource payload at this branch, then its retained ancestors. */
2
3
  export declare function readResourceBlob(service: string, key: string, root?: string): Promise<Uint8Array | null>;
3
4
  /** Write a resource payload on this branch (a stored object's bytes), where a read finds it first. */
4
5
  export declare function writeResourceBlob(service: string, key: string, bytes: Uint8Array, root?: string): Promise<void>;
6
+ /** Write a resource payload on this branch from a body as it arrives (a model's weights, an upload's bytes), kept only
7
+ * when `accept` holds for its size and SHA-256 (the blob seam's `writeBlobStream`). True when kept. */
8
+ export declare function writeResourceBlobStream(service: string, key: string, body: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>, accept: (written: BlobWritten) => boolean, root?: string): Promise<boolean>;
5
9
  /** Remove this branch's copy of a resource payload; an ancestor's stays its own (the row that named it is what a
6
10
  * deletion ends). */
7
11
  export declare function removeResourceBlob(service: string, key: string, root?: string): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { isAbsolute, join, relative } from 'node:path';
2
- import { getActiveBlobStore, readBlobRange } from "./blob-store.js";
2
+ import { getActiveBlobStore, readBlobRange, streamChunks, writeBlobStream } from "./blob-store.js";
3
3
  import { readBranchMeta } from "./log.js";
4
4
  import { worldPaths } from "./storage.js";
5
5
  /** Read a resource payload at this branch, then its retained ancestors. */
@@ -17,6 +17,13 @@ export async function writeResourceBlob(service, key, bytes, root) {
17
17
  assertResourceKey(key);
18
18
  await getActiveBlobStore().put(resourceBlobFile(worldPaths(service, root).resources, key), bytes);
19
19
  }
20
+ /** Write a resource payload on this branch from a body as it arrives (a model's weights, an upload's bytes), kept only
21
+ * when `accept` holds for its size and SHA-256 (the blob seam's `writeBlobStream`). True when kept. */
22
+ export async function writeResourceBlobStream(service, key, body, accept, root) {
23
+ assertResourceKey(key);
24
+ const chunks = body instanceof ReadableStream ? streamChunks(body) : body;
25
+ return writeBlobStream(resourceBlobFile(worldPaths(service, root).resources, key), chunks, accept);
26
+ }
20
27
  /** Remove this branch's copy of a resource payload; an ancestor's stays its own (the row that named it is what a
21
28
  * deletion ends). */
22
29
  export async function removeResourceBlob(service, key, root) {
@@ -0,0 +1,11 @@
1
+ export { getActiveWorldStore, removeCarriedLocks } from './world-store.js';
2
+ export type { WorldStore } from './world-store.js';
3
+ export { stateDirName, withFileLock, worldPaths, worldStateRoot, WORLD_DATA_ENV } from './storage.js';
4
+ export { assertStateRemovable, withAncestryLock, withStateRemoval } from './ancestry.js';
5
+ export { readBranchMeta } from './log.js';
6
+ export { volterHome } from './volter-home.js';
7
+ export { installedPackageDir, installedVendorPacks, isDerivedPack, packRepository, vendorOfPackModule } from './pack-paths.js';
8
+ export { PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding } from './protocol.js';
9
+ export { WORLD_BOOT_PATH } from './serve-http.js';
10
+ export { WORLD_ENV_NAMES_ENV } from './world-env.js';
11
+ export { beginPlaceholderPull, endPlaceholderPull, placeholderPullActive } from './placeholder-remote.js';