@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/pack-paths.ts CHANGED
@@ -102,13 +102,17 @@ export function installedPackageDir(from: string, name: string): string | undefi
102
102
  }
103
103
  }
104
104
 
105
- /** A pack repository itself, or the P3 repository beside a platform checkout. Applications do not acquire a
105
+ /** A pack repository itself, or a platform checkout's P3 repository: the one `VOLTER_PACKS` names (a worktree's own
106
+ * pinned packs, as the repository's scripts read it), else the one beside it. Applications do not acquire a
106
107
  * sibling checkout merely because one happens to be beside them. */
107
108
  export function packRepository(from: string): string | undefined {
108
109
  let dir = resolve(from);
109
110
  for (;;) {
110
111
  if (packageName(dir) === 'twin-packs-p3') return dir;
111
112
  if (packageName(join(dir, 'packages', 'world-core')) === '@volter/world-core') {
113
+ const env = globalThis.process?.env?.VOLTER_PACKS;
114
+ const named = env ? resolve(env) : undefined;
115
+ if (named && packageName(named) === 'twin-packs-p3') return named;
112
116
  const sibling = join(dirname(dir), 'twin-packs-p3');
113
117
  if (packageName(sibling) === 'twin-packs-p3') return sibling;
114
118
  }
@@ -15,6 +15,8 @@ import type { DerivedManifest, VendorManifest } from './derived-core.ts';
15
15
  import type { DerivedSurface } from './derived.ts';
16
16
  import type { TwinEmitter } from './emit.ts';
17
17
  import { registerAuthStrategy, registerStateSystem, type StateSystemAdapters } from './state-system.ts';
18
+ import { PROTOCOL_VERSION, protocolStanding } from './protocol.ts';
19
+ export { PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding } from './protocol.ts';
18
20
 
19
21
  /**
20
22
  * How a pack's clients ADDRESS it. The first three are HTTP API styles; `raw-tcp` is the
@@ -49,20 +51,6 @@ export type PackTransport = 'rest' | 'graphql' | 'web-api' | 'raw-tcp';
49
51
  * seam (fly's runtime, github's git plane, supabase's external stack).
50
52
  * 'proxy' — the vendor itself is a forwarding plane (tunnel).
51
53
  */
52
- /** THE PLATFORM PROTOCOL VERSION (runtime contract R16): `major.minor`. The major names the
53
- * descriptor shape, the factory signature, the doors and the adapter contract a package is
54
- * written against, and the build form it is made in (the derived pack); a change that removes or
55
- * alters a required shape bumps it, an additive change bumps minor. A package declares the major
56
- * it targets (`protocol`), and the kernel serves its own major and no other: a package that
57
- * declares another, or none, is refused. */
58
- export const PROTOCOL_VERSION = '3.0';
59
- export const PROTOCOL_MAJOR = Number(PROTOCOL_VERSION.split('.')[0]);
60
- /** Where a package's declared protocol stands against this kernel: `current`, or `refused`. */
61
- export function protocolStanding(declared: string | undefined): { major: number; standing: 'current' | 'refused' } {
62
- const major = declared === undefined ? Number.NaN : Number(String(declared).split('.')[0]);
63
- return major === PROTOCOL_MAJOR ? { major, standing: 'current' } : { major, standing: 'refused' };
64
- }
65
-
66
54
  export const PACK_ARCHETYPES = ['crud', 'generative', 'signed-protocol', 'engine-control', 'proxy'] as const;
67
55
  export type PackArchetype = (typeof PACK_ARCHETYPES)[number];
68
56
 
@@ -237,7 +225,7 @@ export type TwinPack = {
237
225
  /** Another pack of this vendor issues the API's credentials; architecture, descriptor World wiring. */
238
226
  credentialIssuer?: string;
239
227
  prismaAdapter?: { adapter: string; export: string; urlEnv: string };
240
- managedService?: { kind: 'redis' | 'mongodb'; scheme: string; note: string };
228
+ managedService?: { kind: string; scheme: string; note: string; configurationArg?: '--configuration'; originalInputs?: { version: 1; manifestArg: '--original-inputs'; rootArg: '--original-root'; digestArg: '--original-inputs-sha256'; paths: string[] } };
241
229
  managedDatabase?: { kind: 'postgres'; arg: string; note: string };
242
230
  sidePorts?: Array<{ name: string; option: string; flag: string; path?: string; scheme?: string; note: string }>;
243
231
  stateSystem?: StateSystemAdapters;
@@ -0,0 +1,13 @@
1
+ /** THE PLATFORM PROTOCOL VERSION (runtime contract R16): `major.minor`. The major names the
2
+ * descriptor shape, the factory signature, the doors and the adapter contract a package is
3
+ * written against, and the build form it is made in (the derived pack); a change that removes or
4
+ * alters a required shape bumps it, an additive change bumps minor. A package declares the major
5
+ * it targets (`protocol`), and the kernel serves its own major and no other: a package that
6
+ * declares another, or none, is refused. */
7
+ export const PROTOCOL_VERSION = '3.0';
8
+ export const PROTOCOL_MAJOR = Number(PROTOCOL_VERSION.split('.')[0]);
9
+ /** Where a package's declared protocol stands against this kernel: `current`, or `refused`. */
10
+ export function protocolStanding(declared: string | undefined): { major: number; standing: 'current' | 'refused' } {
11
+ const major = declared === undefined ? Number.NaN : Number(String(declared).split('.')[0]);
12
+ return major === PROTOCOL_MAJOR ? { major, standing: 'current' } : { major, standing: 'refused' };
13
+ }
@@ -46,7 +46,8 @@
46
46
  import { createHash } from 'node:crypto';
47
47
  import { projectResources } from '../actions.ts';
48
48
  import { treeChangesSince, treeStamp } from '../log.ts';
49
- import { applyTwinWrite, type TwinResource } from '../serve.ts';
49
+ import { redisServiceTimes as times } from './service-times.ts';
50
+ import { applyTwinWrites, type TwinResource, type TwinWriteInput } from '../serve.ts';
50
51
  import { getActiveWorldStore } from '../world-store.ts';
51
52
  import { LUA_NULL, luaScriptSha1, LuaError, LuaTable, runLua, type LuaHost, type LuaValue } from './lua.ts';
52
53
  import { inlineCommand } from './frames.ts';
@@ -110,7 +111,7 @@ type StoredZSet = Array<[string, string]>;
110
111
  export type StreamEntries = Array<[string, HashPairs]>;
111
112
 
112
113
  /** A point-in-time copy of a run's whole mutable state, for script rollback. */
113
- type Snapshot = { rows: Map<string, KeyRow>; touched: Map<string, string>; scripts: Map<string, string>; touchedScripts: Map<string, string> };
114
+ type Snapshot = { rows: OverlayState<KeyRow>; touched: Map<string, string>; scripts: OverlayState<string>; touchedScripts: Map<string, string> };
114
115
 
115
116
  export type KeyRow = {
116
117
  name: string;
@@ -399,29 +400,71 @@ function patchImage(image: KeyspaceImage, changed: readonly TwinResource[], remo
399
400
  return true;
400
401
  }
401
402
 
403
+ const GONE: unique symbol = Symbol('gone');
404
+ type OverlayState<V> = { own: Map<string, V | typeof GONE>; moved: Set<string> };
405
+ /**
406
+ * A run's map over the shared image: what the run changed is its own, the rest is read through. Copying the whole image
407
+ * per run (and again per script, for its rollback) cost every command the size of the keyspace (S2 R19: Twenty's queue
408
+ * twin, its queue scripts waiting behind each other's copies). Iteration is a `new Map(base)` changed in place's: the
409
+ * base's order with its changed values, then the keys added, a base key deleted and set again among them at its set.
410
+ */
411
+ class Overlay<V> {
412
+ private own = new Map<string, V | typeof GONE>();
413
+ private moved = new Set<string>();
414
+ constructor(private readonly base: ReadonlyMap<string, V>) {}
415
+ get(key: string): V | undefined {
416
+ const mine = this.own.get(key);
417
+ return mine === GONE ? undefined : mine !== undefined || this.own.has(key) ? mine as V : this.base.get(key);
418
+ }
419
+ has(key: string): boolean { return this.own.has(key) ? this.own.get(key) !== GONE : this.base.has(key); }
420
+ set(key: string, value: V): void {
421
+ if (this.own.get(key) === GONE) { this.own.delete(key); this.moved.add(key); }
422
+ this.own.set(key, value);
423
+ }
424
+ delete(key: string): void {
425
+ this.own.delete(key);
426
+ this.moved.delete(key);
427
+ if (this.base.has(key)) this.own.set(key, GONE);
428
+ }
429
+ *entries(): IterableIterator<[string, V]> {
430
+ for (const [key, value] of this.base) {
431
+ if (this.moved.has(key)) continue;
432
+ if (!this.own.has(key)) { yield [key, value]; continue; }
433
+ const mine = this.own.get(key)!;
434
+ if (mine !== GONE) yield [key, mine];
435
+ }
436
+ for (const [key, mine] of this.own) if (mine !== GONE && (!this.base.has(key) || this.moved.has(key))) yield [key, mine];
437
+ }
438
+ *keys(): IterableIterator<string> { for (const [key] of this.entries()) yield key; }
439
+ *values(): IterableIterator<V> { for (const [, value] of this.entries()) yield value; }
440
+ /** What the run changed, copied: a rollback point the size of the changes, not of the keyspace. */
441
+ save(): OverlayState<V> { return { own: new Map(this.own), moved: new Set(this.moved) }; }
442
+ load(state: OverlayState<V>): void { this.own = new Map(state.own); this.moved = new Set(state.moved); }
443
+ }
444
+
402
445
  /**
403
446
  * A synchronous, in-memory image of the keyspace for the duration of ONE request.
404
447
  *
405
448
  * Seeded from the keyspace's image (the tree folded by `projectResources`, memoized per World state — the kernel IS
406
449
  * the source of truth); every mutation records the key name in `touched`, and the flush writes exactly those keys
407
- * back. This run's maps are its own copies: nothing a run changes reaches the image or a later request except
450
+ * back. This run owns its overlays: nothing a run changes reaches the image or a later request except
408
451
  * through the tree.
409
452
  */
410
453
  export class KeySpace {
411
- private readonly rows: Map<string, KeyRow>;
454
+ private readonly rows: Overlay<KeyRow>;
412
455
  private readonly touched = new Map<string, string>(); // key → the operation label to record
413
- private readonly scripts: Map<string, string>; // sha1 → script source (a SEPARATE namespace)
456
+ private readonly scripts: Overlay<string>; // sha1 → script source (a SEPARATE namespace)
414
457
  private readonly touchedScripts = new Map<string, string>();
415
458
  /** name → the last write ordinal seen, INCLUDING for keys currently deleted or expired. */
416
- private readonly revs: Map<string, number>;
417
- /** The keyspace as the tree held it when this run began: shared, never changed in place. */
459
+ private readonly revs: Overlay<number>;
460
+ /** The shared base maps are patched by the next image(); a KeySpace must finish reading its base before it yields. */
418
461
  readonly image: KeyspaceImage;
419
462
  constructor(readonly ctx: RedisContext, readonly nowMs: number) {
420
- // the image is shared and never changed in place; this run's maps are its own
463
+ // The base maps are patched by the next image(); finish reading this run's base before yielding.
421
464
  const image = this.image = ctx.dialect.storage?.image(ctx) ?? keyspaceImageOf(ctx);
422
- this.rows = new Map(image.rows);
423
- this.scripts = new Map(image.scripts);
424
- this.revs = new Map(image.revs);
465
+ this.rows = new Overlay(image.rows);
466
+ this.scripts = new Overlay(image.scripts);
467
+ this.revs = new Overlay(image.revs);
425
468
  }
426
469
  /**
427
470
  * Is this row expired AS OF NOW? Checked on every read, not merely when the snapshot was built.
@@ -470,15 +513,13 @@ export class KeySpace {
470
513
  }
471
514
  /** A point-in-time copy, so a failed script can be rolled back to exactly where it started. */
472
515
  snapshot(): Snapshot {
473
- return { rows: new Map(this.rows), touched: new Map(this.touched), scripts: new Map(this.scripts), touchedScripts: new Map(this.touchedScripts) };
516
+ return { rows: this.rows.save(), touched: new Map(this.touched), scripts: this.scripts.save(), touchedScripts: new Map(this.touchedScripts) };
474
517
  }
475
518
  restore(s: Snapshot): void {
476
- this.rows.clear();
477
- for (const [k, v] of s.rows) this.rows.set(k, v);
519
+ this.rows.load(s.rows);
478
520
  this.touched.clear();
479
521
  for (const [k, v] of s.touched) this.touched.set(k, v);
480
- this.scripts.clear();
481
- for (const [k, v] of s.scripts) this.scripts.set(k, v);
522
+ this.scripts.load(s.scripts);
482
523
  this.touchedScripts.clear();
483
524
  for (const [k, v] of s.touchedScripts) this.touchedScripts.set(k, v);
484
525
  }
@@ -524,39 +565,34 @@ export async function restoreKeyspace(image: readonly KeyImage[], ctx: RedisCont
524
565
  }
525
566
 
526
567
  async function flushKeySpace(space: KeySpace): Promise<void> {
568
+ // a run's writes land together, in this order, as they landed one at a time (serve.ts applyTwinWrites)
569
+ const writes: TwinWriteInput[] = [];
527
570
  for (const { name, operation, row } of space.pendingWrites()) {
528
- await applyTwinWrite(
529
- space.ctx.service,
530
- {
531
- operation,
532
- subjectType: 'key',
533
- subjectId: `${scopeOf(space.ctx)}key:${name}`,
534
- // The kernel MERGES fields, so a delete must write EVERY field back to its "nothing here"
535
- // value — leaving `v`/`pexpire_at` behind would let a later recreate inherit a dead value
536
- // or a dead TTL.
537
- fields: row === null
538
- ? { name, kind: 'string', v: '', pexpire_at: null, last_id: '', gone: true, _rev: space.nextRev(name) }
539
- : { name, kind: row.kind, v: row.v as never, pexpire_at: row.pexpireAt, last_id: row.lastId ?? '', gone: false, _rev: space.nextRev(name) },
540
- occurredAt: space.ctx.occurredAt,
541
- actor: { kind: 'agent' },
542
- },
543
- space.ctx.root,
544
- );
571
+ writes.push({
572
+ operation,
573
+ subjectType: 'key',
574
+ subjectId: `${scopeOf(space.ctx)}key:${name}`,
575
+ // The kernel MERGES fields, so a delete must write EVERY field back to its "nothing here"
576
+ // value — leaving `v`/`pexpire_at` behind would let a later recreate inherit a dead value
577
+ // or a dead TTL.
578
+ fields: row === null
579
+ ? { name, kind: 'string', v: '', pexpire_at: null, last_id: '', gone: true, _rev: space.nextRev(name) }
580
+ : { name, kind: row.kind, v: row.v as never, pexpire_at: row.pexpireAt, last_id: row.lastId ?? '', gone: false, _rev: space.nextRev(name) },
581
+ occurredAt: space.ctx.occurredAt,
582
+ actor: { kind: 'agent' },
583
+ });
545
584
  }
546
585
  for (const { sha, operation, body } of space.pendingScriptWrites()) {
547
- await applyTwinWrite(
548
- space.ctx.service,
549
- {
550
- operation,
551
- subjectType: 'script',
552
- subjectId: `${scopeOf(space.ctx)}script:${sha}`,
553
- fields: body === null ? { sha, body: '', gone: true } : { sha, body, gone: false },
554
- occurredAt: space.ctx.occurredAt,
555
- actor: { kind: 'agent' },
556
- },
557
- space.ctx.root,
558
- );
586
+ writes.push({
587
+ operation,
588
+ subjectType: 'script',
589
+ subjectId: `${scopeOf(space.ctx)}script:${sha}`,
590
+ fields: body === null ? { sha, body: '', gone: true } : { sha, body, gone: false },
591
+ occurredAt: space.ctx.occurredAt,
592
+ actor: { kind: 'agent' },
593
+ });
559
594
  }
595
+ await applyTwinWrites(space.ctx.service, writes, space.ctx.root);
560
596
  }
561
597
 
562
598
  // ── typed value accessors (each asserts the key's type first) ─────────────────────────────────
@@ -1608,21 +1644,28 @@ export type RunItem = { result: RedisValue } | { error: string };
1608
1644
  * an unavailable command or a bad arity — are QUEUE-time and DO discard the whole batch, but the
1609
1645
  * caller (`upstash-twin.ts`) rejects those before this function is ever reached.
1610
1646
  */
1611
- export async function execRedisRun(commands: string[][], ctx: RedisContext): Promise<{ items: RunItem[]; wrote: boolean; instant: number }> {
1647
+ export async function execRedisRun(commands: string[][], ctx: RedisContext): Promise<{ items: RunItem[]; wrote: boolean; instant: number; touched: string[] }> {
1612
1648
  const nowMs = Date.parse(ctx.occurredAt);
1613
1649
  if (!Number.isFinite(nowMs)) throw new RedisCommandError(`ERR twin: unparseable occurredAt '${ctx.occurredAt}'`);
1614
- const space = new KeySpace(ctx, nowMs);
1650
+ // The phases a served command's time is counted in (service-times.ts): the image, the commands, their writes.
1651
+ const space = times.timed('image', () => new KeySpace(ctx, nowMs));
1615
1652
  const items: RunItem[] = [];
1616
- for (const argv of commands) {
1617
- try {
1618
- items.push({ result: execOne(space, argv) });
1619
- } catch (e) {
1620
- if (e instanceof ReadOnlyError) throw e; // surfaces as HTTP 405 for the whole request
1621
- if (e instanceof RedisCommandError) { items.push({ error: e.message }); continue; }
1622
- throw e;
1653
+ const began = times.now();
1654
+ try {
1655
+ for (const argv of commands) {
1656
+ try {
1657
+ items.push({ result: execOne(space, argv) });
1658
+ } catch (e) {
1659
+ if (e instanceof ReadOnlyError) throw e; // surfaces as HTTP 405 for the whole request
1660
+ if (e instanceof RedisCommandError) { items.push({ error: e.message }); continue; }
1661
+ throw e;
1662
+ }
1623
1663
  }
1624
- }
1664
+ } finally { times.add('execute', times.now() - began); }
1625
1665
  const wrote = space.dirty;
1626
- await (space.ctx.dialect.storage?.flush(space) ?? flushKeySpace(space));
1627
- return { items, wrote, instant: nowMs };
1666
+ // The keys this run wrote, by name, read before the flush clears them: a stream wakes only the clients blocked on one.
1667
+ const touched = space.touchedNames();
1668
+ const flushing = times.now();
1669
+ try { await (space.ctx.dialect.storage?.flush(space) ?? flushKeySpace(space)); } finally { times.add('flush', times.now() - flushing); }
1670
+ return { items, wrote, instant: nowMs, touched };
1628
1671
  }
@@ -1,7 +1,7 @@
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.ts';
4
+ import { applyTwinWrites, type TwinWriteInput } from '../serve.ts';
5
5
  import { projectResources } from '../actions.ts';
6
6
  import { treeStamp, treeChangesSince } from '../log.ts';
7
7
  import { getActiveWorldStore } from '../world-store.ts';
@@ -9,7 +9,9 @@ import type { RedisContext, KeyRow, KeyspaceImage, KeySpace } from './engine.ts'
9
9
 
10
10
  const scope = (c: RedisContext) => c.database === undefined ? '' : `db:${c.database}:`;
11
11
  const collections = new Set(['hash', 'set', 'zset', 'stream', 'list']);
12
- const held = new WeakMap<object, Map<string, { stamp: string; subjects: Map<string, any>; image: KeyspaceImage }>>();
12
+ // `byKey` holds each key's member subjects in `subjects`' own order, so rebuilding one key reads its members, not every
13
+ // subject of the service (S2 R19: the image phase was up to 560 ms of a 2 s window of Twenty's queue twin).
14
+ const held = new WeakMap<object, Map<string, { stamp: string; subjects: Map<string, any>; byKey: Map<string, Map<string, any>>; image: KeyspaceImage }>>();
13
15
  const members = (r: KeyRow | null): Map<string, unknown> => !r || !collections.has(r.kind) ? new Map() : new Map((r.v as any[]).map((x, i) => r.kind === 'list' ? [String(i), x] : r.kind === 'set' ? [x, x] : [x[0], x[1]]));
14
16
  const memberId = (c: RedisContext, name: string, field: string) => `${scope(c)}member:${createHash('sha256').update(JSON.stringify([name, field])).digest('hex')}`;
15
17
 
@@ -21,8 +23,11 @@ export const memberStorage = {
21
23
  const stamp = treeStamp(ctx.service, ctx.root);
22
24
  if (h?.stamp === stamp) return h.image;
23
25
  const delta = h && treeChangesSince(ctx.service, ctx.root, h.stamp);
24
- if (!h || !delta || delta.removed.length) 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)) h.subjects.set(r.id, r);
26
+ if (!h || !delta || delta.removed.length) 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)) {
28
+ h.subjects.set(r.id, r);
29
+ if (r.type === 'member') { let of = h.byKey.get(r.key); if (!of) h.byKey.set(r.key, of = new Map()); of.set(r.id, r); }
30
+ }
26
31
  // Reconstruct only keys affected by the tree delta, not every member on every read.
27
32
  const affected = new Set<string>();
28
33
  const changed = delta && !delta.removed.length ? delta.changed : [...h.subjects.values()];
@@ -37,22 +42,25 @@ export const memberStorage = {
37
42
  if (!meta) continue;
38
43
  h.image.revs.set(name, meta._rev ?? 0);
39
44
  if (meta.gone) { h.image.rows.delete(name); continue; }
40
- 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;
45
+ 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;
41
46
  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 });
42
47
  }
43
48
  h.stamp = stamp; cache.set(key, h); return h.image;
44
49
  },
45
50
  async flush(space: KeySpace): Promise<void> {
46
51
  const ctx = space.ctx;
47
- const write = async (type: string, id: string, operation: string, fields: any) => applyTwinWrite(ctx.service, { operation, subjectType: type, subjectId: id, fields, occurredAt: ctx.occurredAt, actor: { kind: 'agent' } }, ctx.root);
52
+ // a run's writes land together, in this order, as they landed one at a time (serve.ts applyTwinWrites)
53
+ const writes: TwinWriteInput[] = [];
54
+ const write = (type: string, id: string, operation: string, fields: any) => { writes.push({ operation, subjectType: type, subjectId: id, fields, occurredAt: ctx.occurredAt, actor: { kind: 'agent' } }); };
48
55
  for (const { name, row, operation } of space.pendingWrites()) {
49
56
  const before = members(space.image.rows.get(name) ?? null), after = members(row);
50
57
  for (const field of new Set([...before.keys(), ...after.keys()])) {
51
58
  if (before.has(field) && after.has(field) && JSON.stringify(before.get(field)) === JSON.stringify(after.get(field))) continue;
52
- await write('member', memberId(ctx, name, field), operation, { key: name, field, value: after.get(field) ?? null, gone: !after.has(field) });
59
+ write('member', memberId(ctx, name, field), operation, { key: name, field, value: after.get(field) ?? null, gone: !after.has(field) });
53
60
  }
54
- 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) });
61
+ 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) });
55
62
  }
56
- for (const { sha, body, operation } of space.pendingScriptWrites()) await write('script', `${scope(ctx)}script:${sha}`, operation, { sha, body: body ?? '', gone: body === null });
63
+ for (const { sha, body, operation } of space.pendingScriptWrites()) write('script', `${scope(ctx)}script:${sha}`, operation, { sha, body: body ?? '', gone: body === null });
64
+ await applyTwinWrites(ctx.service, writes, ctx.root);
57
65
  },
58
66
  };
@@ -0,0 +1,81 @@
1
+ // A Redis twin's own account of its service, from its own clock: what a command costs from its bytes' arrival to its
2
+ // reply, and which part of the twin holds that time. One line every two seconds while commands are served, none
3
+ // while there are none. It is read to name where a slow request's time is, which a CPU profile's seconds are not
4
+ // (a profile credits the gap between two samples to the later one).
5
+ //
6
+ // A stream serves one command at a time (stream.ts `ordered`), so one command is open at a time and a phase belongs
7
+ // to it. The phases: `image` (the keyspace's image, with the tree's stamp), `execute` (the command itself), `flush`
8
+ // (its writes: the append and its locks), `reply` (the write of the answer to the connection), and `context`, which
9
+ // is the rest of the pack's run around those three (its fetch, the World's clock, the request's context). `other`
10
+ // is what is left of the command's own time: the stream's own work and its waits between those.
11
+ type Phase = 'image' | 'execute' | 'flush' | 'reply' | 'run';
12
+ const PHASES: readonly Phase[] = ['image', 'execute', 'flush', 'reply', 'run'];
13
+ const SAY_EVERY_MS = 2000;
14
+ const now = (): number => performance.now();
15
+
16
+ type Command = { session: number; command: string };
17
+ type Slowest = Command & { threw: boolean; serviceMs: number; waitedMs: number; ownMs: number };
18
+ type Window = { service: number[]; waited: number; waitedMax: number; own: number; phases: Record<Phase, number>; runs: number; queueMax: number; writes: number; laps: Record<string, number>; slowest?: Slowest };
19
+ const fresh = (): Window => ({ 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: {} });
20
+ // THE PARTS OF ONE RUN, in the order it passes them: a run is the pack's own request to itself, carried through the
21
+ // whole of a pack's fetch (pack-fetch.ts) to the command and back. Each `lap` names the stretch that ended there,
22
+ // from the lap before it; the stretches of a window's runs are summed by name as `runPartsMs`. A stretch holds
23
+ // whatever else this realm did between its two ends: under traffic that is other connections' bytes arriving.
24
+ let lapAt = 0;
25
+ let window = fresh();
26
+ let open: { received: number; started: number; command: Command } | undefined;
27
+ let timer: ReturnType<typeof setTimeout> | undefined;
28
+ let served = 0;
29
+
30
+ function say(): void {
31
+ timer = undefined;
32
+ const w = window; window = fresh();
33
+ if (w.service.length === 0) return;
34
+ const sorted = [...w.service].sort((a, b) => a - b);
35
+ const at = (share: number): number => sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * share))]!;
36
+ const ms = (value: number): number => Math.round(value * 10) / 10;
37
+ const inRun = w.phases.image + w.phases.execute + w.phases.flush;
38
+ console.error('[redis-twin]', JSON.stringify({
39
+ commands: w.service.length, servedSoFar: served, runs: w.runs, wrote: w.writes,
40
+ // from a command's bytes read to its reply written, its wait behind the commands before it included
41
+ 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]!) },
42
+ waitedMs: { total: ms(w.waited), max: ms(w.waitedMax) },
43
+ 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) },
44
+ runPartsMs: Object.fromEntries(Object.entries(w.laps).map(([name, value]) => [name, ms(value)])),
45
+ queueMax: w.queueMax,
46
+ slowest: w.slowest ? { ...w.slowest, serviceMs: ms(w.slowest.serviceMs), waitedMs: ms(w.slowest.waitedMs), ownMs: ms(w.slowest.ownMs) } : null,
47
+ }));
48
+ }
49
+
50
+ export const redisServiceTimes = {
51
+ now,
52
+ /** A command starts: `received` is when its bytes were read, `waiting` the commands read and not yet started. */
53
+ begin(received: number, waiting: number, command: Command): void {
54
+ open = { received, started: now(), command };
55
+ if (waiting > window.queueMax) window.queueMax = waiting;
56
+ if (!timer) { timer = setTimeout(say, SAY_EVERY_MS); (timer as { unref?: () => void }).unref?.(); }
57
+ },
58
+ /** Time of the open command in one phase. */
59
+ add(phase: Phase, ms: number): void { if (open) { window.phases[phase] += ms; if (phase === 'run') window.runs += 1; } },
60
+ wrote(): void { if (open) window.writes += 1; },
61
+ /** A run begins: its first stretch is counted from here. */
62
+ lapFrom(): void { lapAt = now(); },
63
+ /** The stretch of the open command's run that ends here, by name. Outside a served command it is nothing. */
64
+ lap(name: string): void { if (!open || lapAt === 0) return; const at = now(); window.laps[name] = (window.laps[name] ?? 0) + (at - lapAt); lapAt = at; },
65
+ /** A run has ended: nothing after it is a stretch of it. */
66
+ lapEnd(name: string): void { this.lap(name); lapAt = 0; },
67
+ /** The open command has answered (or blocked, or failed with its error answered). */
68
+ end(threw = false): void {
69
+ if (!open) return;
70
+ const ended = now(), waited = open.started - open.received;
71
+ const serviceMs = ended - open.received;
72
+ if (!window.slowest || serviceMs > window.slowest.serviceMs) {
73
+ window.slowest = { ...open.command, threw, serviceMs, waitedMs: waited, ownMs: ended - open.started };
74
+ }
75
+ window.service.push(ended - open.received); window.waited += waited; if (waited > window.waitedMax) window.waitedMax = waited;
76
+ window.own += ended - open.started; served += 1; open = undefined;
77
+ },
78
+ /** `work`, its time counted in `phase` of the open command. */
79
+ timed<T>(phase: Phase, work: () => T): T { const began = now(); try { return work(); } finally { this.add(phase, now() - began); } },
80
+ phases: PHASES,
81
+ };
@@ -3,17 +3,26 @@
3
3
  import type { TwinStream, TwinStreamSink } from '../twin-fetch.ts';
4
4
  import { RedisStatus, commandInteger, commandShapeError, globMatch, type RedisDialect, type RunItem } from './engine.ts';
5
5
  import { binaryBytes, binaryText, inlineCommand, readResp, respReply } from './frames.ts';
6
+ import { worldNow } from '../world-clock.ts';
7
+ import { redisServiceTimes as times } from './service-times.ts';
6
8
  export type RedisStreamOptions = {
7
9
  dialect: RedisDialect; version: string; password?: string; databases: number;
8
- run(commands: string[][], database: string): Promise<{ items: RunItem[]; wrote: boolean; instant?: number }>;
10
+ run(commands: string[][], database: string): Promise<{ items: RunItem[]; wrote: boolean; instant?: number; touched?: string[] }>;
9
11
  };
10
- export function createRedisStream(options: RedisStreamOptions): TwinStream {
11
- type Session = { id: number; name: string; lib: Record<string, string>; database: string; authenticated: boolean; queue: string[][] | null; dirty: boolean; subscriptions: Set<string>; patterns: Set<string>; sink: TwinStreamSink; closed: boolean; pump?: () => Promise<void>; blocked?: { command: string[]; deadline: number; timer?: ReturnType<typeof setTimeout> } };
12
- const sessions = new Set<Session>(); let nextId = 0, commandCount = 0; let serial: Promise<any> = Promise.resolve();
12
+ export function createRedisStream(given: RedisStreamOptions): TwinStream {
13
+ // The pack's run, its time counted for the command it serves (service-times.ts).
14
+ const options: RedisStreamOptions = { ...given, run: async (commands, database) => { const began = times.now(); times.lapFrom(); try { const ran = await given.run(commands, database); if (ran.wrote) times.wrote(); return ran; } finally { times.lapEnd('8 answer read by the stream'); times.add('run', times.now() - began); } } };
15
+ type Session = { id: number; name: string; lib: Record<string, string>; database: string; authenticated: boolean; queue: string[][] | null; dirty: boolean; subscriptions: Set<string>; patterns: Set<string>; sink: TwinStreamSink; closed: boolean; received: number; answered: number; pump?: () => Promise<void>; blocked?: { command: string[]; deadline: number; timer?: ReturnType<typeof setTimeout> } };
16
+ const sessions = new Set<Session>(); let nextId = 0, commandCount = 0, waiting = 0; let serial: Promise<any> = Promise.resolve();
13
17
  const ordered = <T>(f: () => Promise<T>): Promise<T> => { const p = serial.then(f); serial = p.catch(() => {}); return p; };
14
18
  const error = (message: string) => ({ error: message }); const ok = new RedisStatus('OK');
15
- const answer = (s: Session, v: any) => { if (!s.closed) s.sink.write(binaryBytes(respReply(v))); };
16
- const close = (s: Session) => { s.closed = true; if (s.blocked?.timer) clearTimeout(s.blocked.timer); sessions.delete(s); };
19
+ const connectionTrace = (s: Session, stage: string, command?: string) => {
20
+ try { console.error('[redis-twin-connection]', JSON.stringify({ at: performance.timeOrigin + times.now(), pid: (globalThis as { process?: { pid?: number } }).process?.pid ?? 0,
21
+ session: s.id, stage, received: s.received, answered: s.answered, ...(command ? { command } : {}) })); }
22
+ catch { /* diagnostic output cannot change a connection */ }
23
+ };
24
+ const answer = (s: Session, v: any) => { if (!s.closed) { times.timed('reply', () => s.sink.write(binaryBytes(respReply(v)))); s.answered += 1; } };
25
+ const close = (s: Session) => { if (!s.closed) connectionTrace(s, 'closed'); s.closed = true; if (s.blocked?.timer) clearTimeout(s.blocked.timer); sessions.delete(s); };
17
26
  const unblock = (s: Session, v: any) => { if (s.blocked?.timer) clearTimeout(s.blocked.timer); s.blocked = undefined; answer(s, v); queueMicrotask(() => { void s.pump?.(); }); };
18
27
  const blockRun = async (s: Session, argv: string[]): Promise<any> => {
19
28
  if (argv[0]!.toUpperCase() === 'BZPOPMIN') {
@@ -21,7 +30,22 @@ export function createRedisStream(options: RedisStreamOptions): TwinStream {
21
30
  }
22
31
  throw new Error('the blocking command has no executor');
23
32
  };
24
- const wake = async () => { for (const s of sessions) if (s.blocked) { const v = await blockRun(s, s.blocked.command); if (v !== null) unblock(s, v); } };
33
+ // A CLIENT BLOCKED ON KEYS IS ASKED AGAIN ONLY WHEN ONE OF ITS KEYS WAS WRITTEN. Every write asked every blocked
34
+ // client again, each ask a whole run for each of its keys: Twenty's queue workers hold a blocked client apiece, and
35
+ // its twin made 59 to 89 runs for 31 to 41 commands in a two-second window, each run about 20 ms, with every
36
+ // other command waiting behind them (Twenty 51: commands waited 0.6 to 5.9 s). `touched` is the keys the run
37
+ // wrote, by name, whatever its database (a name written in another database asks a client that then finds
38
+ // nothing, never the reverse). Without it (an EXEC's queue, a run that does not say, a command that writes
39
+ // another database's keys) every blocked client is asked, as before.
40
+ const wake = async (touched?: readonly string[]) => {
41
+ for (const s of sessions) {
42
+ if (!s.blocked) continue;
43
+ if (touched && !s.blocked.command.slice(1, -1).some((key) => touched.includes(key))) continue;
44
+ const v = await blockRun(s, s.blocked.command); if (v !== null) unblock(s, v);
45
+ }
46
+ };
47
+ /** Commands that write keys of a database other than their session's: their run's `touched` does not name them all. */
48
+ const CROSS_DATABASE = new Set(['MOVE', 'COPY', 'SWAPDB']);
25
49
  const command = async (s: Session, argv: string[], executing = false): Promise<void> => {
26
50
  if (!argv.length || s.closed) return;
27
51
  commandCount++;
@@ -85,20 +109,48 @@ export function createRedisStream(options: RedisStreamOptions): TwinStream {
85
109
  // source: https://raw.githubusercontent.com/redis/redis/c9d29f6a918c335bc1778d9f68e521c1bbb36a0f/src/multi.c "we do not want to allow blocking commands inside multi"
86
110
  if (executing) { answer(s, null); return; }
87
111
  const initial = await options.run([], s.database); s.blocked = { command: argv, deadline: timeout === 0 ? Infinity : (initial.instant ?? 0) + timeout };
88
- const poll = () => { if (!s.blocked || s.closed) return; s.blocked.timer = setTimeout(() => { void ordered(async () => { if (!s.blocked) return; const now = await options.run([], s.database); if ((now.instant ?? 0) >= s.blocked.deadline) unblock(s, null); else poll(); }); }, 50); }; poll(); return;
112
+ // A BLOCKED CLIENT WAITS FOR A WRITE OR ITS DEADLINE, AND ASKS THE TREE NOTHING MEANWHILE. A write through this
113
+ // twin wakes it (wake, after every command that wrote). Its deadline is in the World's time, so the timer
114
+ // reads the World's clock (world-clock.ts worldNow: one check of the clock file, kept while it stands) and
115
+ // nothing else. It ran a whole empty request every 50 ms to read that time: the pack's fetch, its context
116
+ // and the keyspace's image with its tree stamp, for every blocked client, the whole time it was blocked.
117
+ // Twenty's queue workers each hold one blocked client: its Redis twin made about 18,700 stamps in a run, 56,145
118
+ // stat requests, and spent 55 s of 114 busy in them, with no command to answer. No timeout is no timer; a
119
+ // deadline far off is checked less often, never more than a second apart, and to 50 ms as it nears.
120
+ const poll = (): void => {
121
+ if (!s.blocked || s.closed || s.blocked.deadline === Infinity) return;
122
+ let left: number;
123
+ try { left = s.blocked.deadline - Date.parse(worldNow()); }
124
+ catch (cause) { unblock(s, error(`ERR twin: ${cause instanceof Error ? cause.message : String(cause)}`.replace(/[\r\n]+/g, ' '))); return; }
125
+ if (left <= 0) { void ordered(async () => { if (s.blocked) unblock(s, null); }); return; }
126
+ s.blocked.timer = setTimeout(poll, Math.max(50, Math.min(1000, left / 2)));
127
+ };
128
+ poll(); return;
89
129
  }
90
- default: { const res = await options.run([argv], s.database); const item = res.items[0]!; answer(s, 'error' in item ? item : item.result); if (res.wrote && !executing) await wake(); }
130
+ default: { const res = await options.run([argv], s.database); const item = res.items[0]!; answer(s, 'error' in item ? item : item.result); if (res.wrote && !executing) await wake(CROSS_DATABASE.has(cmd) ? undefined : res.touched); }
91
131
  }
92
132
  };
93
133
  return (sink) => {
94
- const s: Session = { id: ++nextId, name: '', lib: {}, database: '0', authenticated: !options.password, queue: null, dirty: false, subscriptions: new Set(), patterns: new Set(), sink, closed: false }; sessions.add(s);
95
- let buffer = '', chain: Promise<any> = Promise.resolve(); const pending: string[][] = [];
96
- const pump = async () => { while (pending.length && !s.blocked && !s.closed) await ordered(() => command(s, pending.shift()!)); };
134
+ const s: Session = { 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 }; sessions.add(s);
135
+ connectionTrace(s, 'opened');
136
+ let buffer = '', chain: Promise<any> = Promise.resolve(); const pending: Array<{ argv: string[]; received: number }> = [];
137
+ // A command whose storage fails (a World storage lock that timed out, a write that could not be made) answers its
138
+ // client with an error and the session goes on, as Redis answers one failed command: it was an unhandled
139
+ // rejection, and the twin's process exited with every client's connection (Twenty 46).
140
+ const pump = async () => {
141
+ while (pending.length && !s.blocked && !s.closed) {
142
+ const { argv, received } = pending.shift()!;
143
+ try { await ordered(async () => { waiting -= 1; times.begin(received, waiting, { session: s.id, command: argv[0]?.toUpperCase().slice(0, 32) ?? '' });
144
+ let threw = false;
145
+ try { await command(s, argv); } catch (cause) { threw = true; throw cause; } finally { times.end(threw); } }); }
146
+ catch (cause) { if (!s.closed) answer(s, error(`ERR twin: ${cause instanceof Error ? cause.message : String(cause)}`.replace(/[\r\n]+/g, ' '))); }
147
+ }
148
+ };
97
149
  s.pump = pump;
98
150
  return {
99
- data(chunk) { if (s.closed) return; buffer += binaryText(chunk); try { while (buffer.length) { let argv: string[]; let end: number; if (buffer[0] === '*') { const f = readResp(buffer); if (!f) break; if (!Array.isArray(f.value) || f.value.some((x: unknown) => typeof x !== 'string')) throw new Error('Protocol error: expected bulk string'); argv = f.value; end = f.end; } else { const at = buffer.indexOf('\r\n'); if (at < 0) break; argv = inlineCommand(buffer.slice(0, at)); end = at + 2; } buffer = buffer.slice(end); pending.push(argv); } chain = chain.then(pump); } catch (e) { answer(s, error(`ERR ${(e as Error).message}`)); close(s); sink.end(); } },
151
+ data(chunk) { if (s.closed) return; buffer += binaryText(chunk); try { while (buffer.length) { let argv: string[]; let end: number; if (buffer[0] === '*') { const f = readResp(buffer); if (!f) break; if (!Array.isArray(f.value) || f.value.some((x: unknown) => typeof x !== 'string')) throw new Error('Protocol error: expected bulk string'); argv = f.value; end = f.end; } else { const at = buffer.indexOf('\r\n'); if (at < 0) break; argv = inlineCommand(buffer.slice(0, at)); end = at + 2; } buffer = buffer.slice(end); pending.push({ argv, received: times.now() }); waiting += 1; s.received += 1; if (s.received === 1) connectionTrace(s, 'first-command', argv[0]?.toUpperCase().slice(0, 32)); } chain = chain.then(pump); } catch (e) { answer(s, error(`ERR ${(e as Error).message}`)); close(s); sink.end(); } },
100
152
  settled: () => chain,
101
- close: () => close(s),
153
+ close: () => { waiting -= pending.length; pending.length = 0; close(s); },
102
154
  };
103
155
  };
104
156
  }
@@ -1,5 +1,5 @@
1
1
  import { isAbsolute, join, relative } from 'node:path';
2
- import { getActiveBlobStore, readBlobRange } from './blob-store.ts';
2
+ import { type BlobWritten, getActiveBlobStore, readBlobRange, streamChunks, writeBlobStream } from './blob-store.ts';
3
3
  import { readBranchMeta } from './log.ts';
4
4
  import { worldPaths } from './storage.ts';
5
5
 
@@ -19,6 +19,14 @@ export async function writeResourceBlob(service: string, key: string, bytes: Uin
19
19
  await getActiveBlobStore().put(resourceBlobFile(worldPaths(service, root).resources, key), bytes);
20
20
  }
21
21
 
22
+ /** Write a resource payload on this branch from a body as it arrives (a model's weights, an upload's bytes), kept only
23
+ * when `accept` holds for its size and SHA-256 (the blob seam's `writeBlobStream`). True when kept. */
24
+ export async function writeResourceBlobStream(service: string, key: string, body: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>, accept: (written: BlobWritten) => boolean, root?: string): Promise<boolean> {
25
+ assertResourceKey(key);
26
+ const chunks = body instanceof ReadableStream ? streamChunks(body) : body;
27
+ return writeBlobStream(resourceBlobFile(worldPaths(service, root).resources, key), chunks, accept);
28
+ }
29
+
22
30
  /** Remove this branch's copy of a resource payload; an ancestor's stays its own (the row that named it is what a
23
31
  * deletion ends). */
24
32
  export async function removeResourceBlob(service: string, key: string, root?: string): Promise<void> {
@@ -0,0 +1,14 @@
1
+ // The lifecycle's state/identity primitives. Re-export their canonical modules
2
+ // so the active store, locks and scopes are shared with the full runtime and packs.
3
+ // Booting an empty World does not require pack registration or derived engines.
4
+ export { getActiveWorldStore, removeCarriedLocks } from './world-store.ts';
5
+ export type { WorldStore } from './world-store.ts';
6
+ export { stateDirName, withFileLock, worldPaths, worldStateRoot, WORLD_DATA_ENV } from './storage.ts';
7
+ export { assertStateRemovable, withAncestryLock, withStateRemoval } from './ancestry.ts';
8
+ export { readBranchMeta } from './log.ts';
9
+ export { volterHome } from './volter-home.ts';
10
+ export { installedPackageDir, installedVendorPacks, isDerivedPack, packRepository, vendorOfPackModule } from './pack-paths.ts';
11
+ export { PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding } from './protocol.ts';
12
+ export { WORLD_BOOT_PATH } from './serve-http.ts';
13
+ export { WORLD_ENV_NAMES_ENV } from './world-env.ts';
14
+ export { beginPlaceholderPull, endPlaceholderPull, placeholderPullActive } from './placeholder-remote.ts';