@volter/world-core 3.0.154 → 3.0.156

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/generated/pack-facts.json +140 -0
  2. package/dist/inject.cjs +150 -86
  3. package/dist/src/actions.d.ts +15 -0
  4. package/dist/src/actions.js +82 -21
  5. package/dist/src/ancestry.d.ts +5 -4
  6. package/dist/src/ancestry.js +53 -28
  7. package/dist/src/changeset.js +1 -1
  8. package/dist/src/clickhouse/compression.d.ts +28 -0
  9. package/dist/src/clickhouse/compression.js +281 -0
  10. package/dist/src/clickhouse/index.d.ts +1 -0
  11. package/dist/src/clickhouse/index.js +1 -0
  12. package/dist/src/client-bundle.js +15 -15
  13. package/dist/src/derived-core.d.ts +12 -1
  14. package/dist/src/derived-core.js +145 -11
  15. package/dist/src/derived.js +3 -0
  16. package/dist/src/fork.js +1 -1
  17. package/dist/src/graphql-wire.d.ts +5 -0
  18. package/dist/src/graphql-wire.js +6 -6
  19. package/dist/src/grpc-wire.js +2 -1
  20. package/dist/src/h2.js +63 -10
  21. package/dist/src/head.d.ts +3 -0
  22. package/dist/src/head.js +9 -0
  23. package/dist/src/history.d.ts +1 -1
  24. package/dist/src/history.js +7 -7
  25. package/dist/src/index.d.ts +4 -1
  26. package/dist/src/index.js +5 -1
  27. package/dist/src/log.d.ts +23 -3
  28. package/dist/src/log.js +160 -37
  29. package/dist/src/managed-original-inputs.d.ts +101 -0
  30. package/dist/src/managed-original-inputs.js +125 -0
  31. package/dist/src/mongodb/client.d.ts +8 -0
  32. package/dist/src/mongodb/client.js +27 -0
  33. package/dist/src/mongodb/index.d.ts +2 -0
  34. package/dist/src/mongodb/index.js +5 -0
  35. package/dist/src/mongodb/stream.d.ts +74 -0
  36. package/dist/src/mongodb/stream.js +206 -0
  37. package/dist/src/observe.js +1 -1
  38. package/dist/src/pack-fetch.d.ts +2 -1
  39. package/dist/src/pack-fetch.js +21 -9
  40. package/dist/src/packRegistry.d.ts +10 -14
  41. package/dist/src/packRegistry.js +2 -13
  42. package/dist/src/protocol.d.ts +13 -0
  43. package/dist/src/protocol.js +13 -0
  44. package/dist/src/redis/engine.d.ts +10 -4
  45. package/dist/src/redis/engine.js +103 -32
  46. package/dist/src/redis/member-storage.js +20 -8
  47. package/dist/src/redis/service-times.d.ts +25 -0
  48. package/dist/src/redis/service-times.js +87 -0
  49. package/dist/src/redis/stream.d.ts +2 -1
  50. package/dist/src/redis/stream.js +109 -20
  51. package/dist/src/runtime-state.d.ts +11 -0
  52. package/dist/src/runtime-state.js +13 -0
  53. package/dist/src/runtime.d.ts +4 -2
  54. package/dist/src/runtime.js +5 -2
  55. package/dist/src/serve-http.d.ts +2 -0
  56. package/dist/src/serve-http.js +21 -4
  57. package/dist/src/serve.d.ts +10 -0
  58. package/dist/src/serve.js +43 -3
  59. package/dist/src/storage.d.ts +2 -1
  60. package/dist/src/storage.js +6 -3
  61. package/dist/src/world-boot.d.ts +1 -0
  62. package/dist/src/world-boot.js +6 -0
  63. package/dist/src/world-clock.js +6 -2
  64. package/dist/src/world-store.d.ts +108 -7
  65. package/dist/src/world-store.js +883 -103
  66. package/dist/src/xml.d.ts +30 -0
  67. package/dist/src/xml.js +74 -0
  68. package/generated/pack-facts.json +140 -0
  69. package/inject.cjs +150 -86
  70. package/package.json +20 -1
  71. package/src/actions.ts +78 -21
  72. package/src/ancestry.ts +46 -24
  73. package/src/changeset.ts +1 -1
  74. package/src/clickhouse/compression.ts +172 -0
  75. package/src/clickhouse/index.ts +2 -0
  76. package/src/client-bundle.ts +13 -14
  77. package/src/derived-core.ts +142 -11
  78. package/src/derived.ts +3 -0
  79. package/src/fork.ts +1 -1
  80. package/src/graphql-wire.ts +9 -6
  81. package/src/grpc-wire.ts +2 -1
  82. package/src/h2.ts +46 -5
  83. package/src/head.ts +9 -0
  84. package/src/history.ts +7 -7
  85. package/src/index.ts +6 -1
  86. package/src/log.ts +147 -32
  87. package/src/managed-original-inputs.ts +163 -0
  88. package/src/mongodb/client.ts +27 -0
  89. package/src/mongodb/index.ts +5 -0
  90. package/src/mongodb/stream.ts +213 -0
  91. package/src/observe.ts +1 -1
  92. package/src/pack-fetch.ts +23 -11
  93. package/src/packRegistry.ts +3 -15
  94. package/src/protocol.ts +13 -0
  95. package/src/redis/engine.ts +100 -57
  96. package/src/redis/member-storage.ts +17 -9
  97. package/src/redis/service-times.ts +81 -0
  98. package/src/redis/stream.ts +66 -14
  99. package/src/runtime-state.ts +14 -0
  100. package/src/runtime.ts +6 -1
  101. package/src/serve-http.ts +23 -4
  102. package/src/serve.ts +45 -3
  103. package/src/storage.ts +5 -3
  104. package/src/world-boot.ts +6 -0
  105. package/src/world-clock.ts +6 -2
  106. package/src/world-store.ts +704 -91
  107. package/src/xml.ts +71 -0
@@ -1,4 +1,5 @@
1
1
  import { parseExactJson } from "./exact-json.js";
2
+ import { redisServiceTimes } from "./redis/service-times.js";
2
3
  import { readHostPort } from "./host-port.js";
3
4
  // A Protocol 3 pack's whole HTTP surface, served by the kernel from the pack's declarations (docs/contributing/
4
5
  // architecture.md, "The pack contract: the kernel's root"): the discovery door (`GET /twin`, the manifest's
@@ -160,7 +161,7 @@ export function createPackFetch(parts, options = {}) {
160
161
  // the GraphQL wire over the same state, at the paths the manifest names (graphql-wire.ts)
161
162
  const gql = m.graphql && parts.graphql ? m.graphql : undefined;
162
163
  const graphql = gql && parts.graphql ? graphqlEndpoint({
163
- sdl: parts.graphql.sdl, part: parts.graphql,
164
+ sdl: parts.graphql.sdl, part: parts.graphql, loadLibrary: parts.graphql.loadLibrary,
164
165
  context: async (request) => contextFor(m, { request: request.clone(), operation: { id: 'graphql', method: 'POST', path: gql.paths[0], class: 'action' }, params: {} }, scope),
165
166
  unmodeled: ({ type, field }) => Response.json({ errors: [{ ...(gql.unmodeled.type ? { type: gql.unmodeled.type } : {}), message: gql.unmodeled.message.replace('{field}', field).replace('{type}', type) }] }, { status: gql.unmodeled.status ?? 200 }),
166
167
  mutationRefused: (request) => ((options.readOnly ?? false) || isReadOnlyRequest(request) ? vendorError(m, m.readOnly) : undefined),
@@ -393,6 +394,7 @@ export function createPackFetch(parts, options = {}) {
393
394
  return readable instanceof Response ? readable : readable ? atContentHost(readable) : undecodable(arrived);
394
395
  };
395
396
  const fetch = async (arrived) => {
397
+ redisServiceTimes.lap('1 request made by the stream');
396
398
  // the discovery door is the pack's own, whichever lane would take its path
397
399
  const asked = new URL(arrived.url).pathname.replace(/\/+$/, '') || '/';
398
400
  const discovery = arrived.method === 'GET' && m.discovery !== undefined && (asked === '/twin' || asked === '/twin/scenario');
@@ -427,6 +429,7 @@ export function createPackFetch(parts, options = {}) {
427
429
  const url = new URL(request.url);
428
430
  const path = url.pathname.replace(/\/+$/, '') || '/';
429
431
  const readOnly = (options.readOnly ?? false) || isReadOnlyRequest(request);
432
+ redisServiceTimes.lap('2 content and lane routes, decoding, routing');
430
433
  if (request.method === 'GET' && path === '/twin' && m.discovery) {
431
434
  const d = m.discovery;
432
435
  const discoveryDoors = [...(m.doors ?? []), ...(m.machines ? [{ id: 'machine-pool', method: 'POST', path: '/_twin/machine-pool', note: 'Enroll the World execution pool: { kind: light | docker | none | fake, images?: { image: { context, command?, runtime? } } }.' }] : [])];
@@ -549,12 +552,19 @@ export function createPackFetch(parts, options = {}) {
549
552
  return encoded(m, request, await decided(request));
550
553
  // The front reads a context before forwarding (possibly with new headers). Materialize a transport body before
551
554
  // those independent reads, rather than chaining tees of the incoming stream through the front and the scenario.
555
+ redisServiceTimes.lap('3 doors, screens and machine routes passed');
552
556
  const bytes = request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.arrayBuffer();
553
557
  const buffered = new Request(request.url, { method: request.method, headers: request.headers, signal: request.signal, ...(bytes ? { body: bytes } : {}) });
554
558
  const matched = matchOperation(routes, buffered.method, url.pathname, url.searchParams, buffered.headers);
555
559
  const operation = matched?.operation ?? { id: 'unmatched', method: buffered.method, path: url.pathname, class: buffered.method === 'GET' ? 'retrieve' : 'action' };
560
+ redisServiceTimes.lap('4 body buffered, operation matched');
556
561
  const ctx = await contextFor(m, { request: buffered.clone(), operation, params: matched?.params ?? pathParams(routes, url.pathname) }, scope);
557
- return encoded(m, buffered, await parts.around(ctx, (rewritten) => decided(rewritten ?? buffered)));
562
+ redisServiceTimes.lap('5 the around context opened');
563
+ const answered = await parts.around(ctx, (rewritten) => decided(rewritten ?? buffered));
564
+ redisServiceTimes.lap('6d the handler, and the around after it');
565
+ const sent = await encoded(m, buffered, answered);
566
+ redisServiceTimes.lap('7 answer encoded');
567
+ return sent;
558
568
  };
559
569
  // the request's id is the correlation of every entry it appends (runtime contract D3), minted here, always: it is the
560
570
  // group a perform sends as one call (the real-system adapters, "Perform"). Under `auto` the group is performed once
@@ -739,9 +749,17 @@ export function createVendorFetch(manifest, lanes) {
739
749
  const ingest = Object.values(lanes).find((l) => l.fetch.ingest)?.fetch.ingest;
740
750
  const lane = laneRouter(manifest.vendor, manifest.lanes, lanes);
741
751
  return Object.assign(async (request) => {
752
+ const path = new URL(request.url).pathname.replace(/\/+$/, '') || '/';
753
+ // the discovery door is answered before any lane's clock moves, as a pack's own is (createPackFetch): reading what
754
+ // the twin is moves no vendor clock
755
+ if (request.method === 'GET' && path === '/twin' && manifest.discovery) {
756
+ // the vendor's screens are its lanes' (a dashboard drawn by one lane is the vendor's)
757
+ const screens = lanesWorkspaces(lanes);
758
+ const laneDiscovery = await laneDoors(lanes, request, '/twin');
759
+ return Response.json({ ...statefulTwinManifest({ vendor: manifest.vendor, ...manifest.discovery }), ...(screens.length ? { screens } : {}), ...(Object.keys(laneDiscovery).length ? { lanes: laneDiscovery } : {}) });
760
+ }
742
761
  for (const part of Object.values(lanes))
743
762
  await part.fetch.advanceClock?.(request);
744
- const path = new URL(request.url).pathname.replace(/\/+$/, '') || '/';
745
763
  // the vendor's board is the frames of the lanes that lay one out, each drawn when any lane's screen takes its url
746
764
  if (request.method === 'GET' && path === '/twin/board') {
747
765
  const frames = [];
@@ -758,12 +776,6 @@ export function createVendorFetch(manifest, lanes) {
758
776
  const draws = (url) => Object.values(lanes).some((l) => l.fetch.draws?.(url) ?? false);
759
777
  return Response.json({ frames: frames.map((f) => ({ ...f, drawn: draws(f.url) })) });
760
778
  }
761
- if (request.method === 'GET' && path === '/twin' && manifest.discovery) {
762
- // the vendor's screens are its lanes' (a dashboard drawn by one lane is the vendor's)
763
- const screens = lanesWorkspaces(lanes);
764
- const laneDiscovery = await laneDoors(lanes, request, '/twin');
765
- return Response.json({ ...statefulTwinManifest({ vendor: manifest.vendor, ...manifest.discovery }), ...(screens.length ? { screens } : {}), ...(Object.keys(laneDiscovery).length ? { lanes: laneDiscovery } : {}) });
766
- }
767
779
  if (request.method === 'GET' && path === '/twin/scenario') {
768
780
  const laneScenarios = await laneDoors(lanes, request, '/twin/scenario');
769
781
  if (Object.keys(laneScenarios).length)
@@ -5,6 +5,7 @@ import type { DerivedManifest } from './derived-core.js';
5
5
  import type { DerivedSurface } from './derived.js';
6
6
  import type { TwinEmitter } from './emit.js';
7
7
  import { type StateSystemAdapters } from './state-system.js';
8
+ export { PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding } from './protocol.js';
8
9
  /**
9
10
  * How a pack's clients ADDRESS it. The first three are HTTP API styles; `raw-tcp` is the
10
11
  * RAW-PROTOCOL class — a pack whose clients speak their protocol directly over a TCP socket
@@ -37,19 +38,6 @@ export type PackTransport = 'rest' | 'graphql' | 'web-api' | 'raw-tcp';
37
38
  * seam (fly's runtime, github's git plane, supabase's external stack).
38
39
  * 'proxy' — the vendor itself is a forwarding plane (tunnel).
39
40
  */
40
- /** THE PLATFORM PROTOCOL VERSION (runtime contract R16): `major.minor`. The major names the
41
- * descriptor shape, the factory signature, the doors and the adapter contract a package is
42
- * written against, and the build form it is made in (the derived pack); a change that removes or
43
- * alters a required shape bumps it, an additive change bumps minor. A package declares the major
44
- * it targets (`protocol`), and the kernel serves its own major and no other: a package that
45
- * declares another, or none, is refused. */
46
- export declare const PROTOCOL_VERSION = "3.0";
47
- export declare const PROTOCOL_MAJOR: number;
48
- /** Where a package's declared protocol stands against this kernel: `current`, or `refused`. */
49
- export declare function protocolStanding(declared: string | undefined): {
50
- major: number;
51
- standing: 'current' | 'refused';
52
- };
53
41
  export declare const PACK_ARCHETYPES: readonly ["crud", "generative", "signed-protocol", "engine-control", "proxy"];
54
42
  export type PackArchetype = (typeof PACK_ARCHETYPES)[number];
55
43
  /**
@@ -223,9 +211,17 @@ export type TwinPack = {
223
211
  urlEnv: string;
224
212
  };
225
213
  managedService?: {
226
- kind: 'redis' | 'mongodb';
214
+ kind: string;
227
215
  scheme: string;
228
216
  note: string;
217
+ configurationArg?: '--configuration';
218
+ originalInputs?: {
219
+ version: 1;
220
+ manifestArg: '--original-inputs';
221
+ rootArg: '--original-root';
222
+ digestArg: '--original-inputs-sha256';
223
+ paths: string[];
224
+ };
229
225
  };
230
226
  managedDatabase?: {
231
227
  kind: 'postgres';
@@ -11,6 +11,8 @@ import { declareRateBudget } from "./rateBudget.js";
11
11
  import { referenceField, registerReferences } from "./references.js";
12
12
  import { deriveStateSystem, deriveVendorStateSystem } from "./derived-real.js";
13
13
  import { registerAuthStrategy, registerStateSystem } from "./state-system.js";
14
+ import { PROTOCOL_VERSION, protocolStanding } from "./protocol.js";
15
+ export { PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding } from "./protocol.js";
14
16
  /**
15
17
  * The pack's SERVE-FAMILY — the axis the invariant matrix keys strictness off, DECLARED
16
18
  * because it is the pack's own claim about what kind of thing it is (a heuristic over file
@@ -26,19 +28,6 @@ import { registerAuthStrategy, registerStateSystem } from "./state-system.js";
26
28
  * seam (fly's runtime, github's git plane, supabase's external stack).
27
29
  * 'proxy' — the vendor itself is a forwarding plane (tunnel).
28
30
  */
29
- /** THE PLATFORM PROTOCOL VERSION (runtime contract R16): `major.minor`. The major names the
30
- * descriptor shape, the factory signature, the doors and the adapter contract a package is
31
- * written against, and the build form it is made in (the derived pack); a change that removes or
32
- * alters a required shape bumps it, an additive change bumps minor. A package declares the major
33
- * it targets (`protocol`), and the kernel serves its own major and no other: a package that
34
- * declares another, or none, is refused. */
35
- export const PROTOCOL_VERSION = '3.0';
36
- export const PROTOCOL_MAJOR = Number(PROTOCOL_VERSION.split('.')[0]);
37
- /** Where a package's declared protocol stands against this kernel: `current`, or `refused`. */
38
- export function protocolStanding(declared) {
39
- const major = declared === undefined ? Number.NaN : Number(String(declared).split('.')[0]);
40
- return major === PROTOCOL_MAJOR ? { major, standing: 'current' } : { major, standing: 'refused' };
41
- }
42
31
  export const PACK_ARCHETYPES = ['crud', 'generative', 'signed-protocol', 'engine-control', 'proxy'];
43
32
  /** The posture to assume for a pack that doesn't declare one. Conservative on purpose. */
44
33
  export const DEFAULT_PULL_POSTURE = 'on-demand';
@@ -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 declare const PROTOCOL_VERSION = "3.0";
8
+ export declare const PROTOCOL_MAJOR: number;
9
+ /** Where a package's declared protocol stands against this kernel: `current`, or `refused`. */
10
+ export declare function protocolStanding(declared: string | undefined): {
11
+ major: number;
12
+ standing: 'current' | 'refused';
13
+ };
@@ -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) {
11
+ const major = declared === undefined ? Number.NaN : Number(String(declared).split('.')[0]);
12
+ return major === PROTOCOL_MAJOR ? { major, standing: 'current' } : { major, standing: 'refused' };
13
+ }
@@ -45,9 +45,9 @@ type StoredZSet = Array<[string, string]>;
45
45
  export type StreamEntries = Array<[string, HashPairs]>;
46
46
  /** A point-in-time copy of a run's whole mutable state, for script rollback. */
47
47
  type Snapshot = {
48
- rows: Map<string, KeyRow>;
48
+ rows: OverlayState<KeyRow>;
49
49
  touched: Map<string, string>;
50
- scripts: Map<string, string>;
50
+ scripts: OverlayState<string>;
51
51
  touchedScripts: Map<string, string>;
52
52
  };
53
53
  export type KeyRow = {
@@ -168,12 +168,17 @@ export type KeyspaceImage = {
168
168
  scripts: Map<string, string>;
169
169
  revs: Map<string, number>;
170
170
  };
171
+ declare const GONE: unique symbol;
172
+ type OverlayState<V> = {
173
+ own: Map<string, V | typeof GONE>;
174
+ moved: Set<string>;
175
+ };
171
176
  /**
172
177
  * A synchronous, in-memory image of the keyspace for the duration of ONE request.
173
178
  *
174
179
  * Seeded from the keyspace's image (the tree folded by `projectResources`, memoized per World state — the kernel IS
175
180
  * the source of truth); every mutation records the key name in `touched`, and the flush writes exactly those keys
176
- * back. This run's maps are its own copies: nothing a run changes reaches the image or a later request except
181
+ * back. This run owns its overlays: nothing a run changes reaches the image or a later request except
177
182
  * through the tree.
178
183
  */
179
184
  export declare class KeySpace {
@@ -185,7 +190,7 @@ export declare class KeySpace {
185
190
  private readonly touchedScripts;
186
191
  /** name → the last write ordinal seen, INCLUDING for keys currently deleted or expired. */
187
192
  private readonly revs;
188
- /** The keyspace as the tree held it when this run began: shared, never changed in place. */
193
+ /** The shared base maps are patched by the next image(); a KeySpace must finish reading its base before it yields. */
189
194
  readonly image: KeyspaceImage;
190
195
  constructor(ctx: RedisContext, nowMs: number);
191
196
  /**
@@ -323,5 +328,6 @@ export declare function execRedisRun(commands: string[][], ctx: RedisContext): P
323
328
  items: RunItem[];
324
329
  wrote: boolean;
325
330
  instant: number;
331
+ touched: string[];
326
332
  }>;
327
333
  export {};
@@ -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;