@volter/world-core 3.0.155 → 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 (101) hide show
  1. package/dist/generated/pack-facts.json +143 -12
  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/derived-core.d.ts +12 -1
  13. package/dist/src/derived-core.js +145 -11
  14. package/dist/src/derived.js +3 -0
  15. package/dist/src/fork.js +1 -1
  16. package/dist/src/grpc-wire.js +2 -1
  17. package/dist/src/h2.js +63 -10
  18. package/dist/src/head.d.ts +3 -0
  19. package/dist/src/head.js +9 -0
  20. package/dist/src/history.d.ts +1 -1
  21. package/dist/src/history.js +7 -7
  22. package/dist/src/index.d.ts +4 -1
  23. package/dist/src/index.js +5 -1
  24. package/dist/src/log.d.ts +23 -3
  25. package/dist/src/log.js +160 -37
  26. package/dist/src/managed-original-inputs.d.ts +101 -0
  27. package/dist/src/managed-original-inputs.js +125 -0
  28. package/dist/src/mongodb/client.d.ts +8 -0
  29. package/dist/src/mongodb/client.js +27 -0
  30. package/dist/src/mongodb/index.d.ts +2 -0
  31. package/dist/src/mongodb/index.js +5 -0
  32. package/dist/src/mongodb/stream.d.ts +74 -0
  33. package/dist/src/mongodb/stream.js +206 -0
  34. package/dist/src/observe.js +1 -1
  35. package/dist/src/pack-fetch.js +20 -8
  36. package/dist/src/packRegistry.d.ts +10 -14
  37. package/dist/src/packRegistry.js +2 -13
  38. package/dist/src/protocol.d.ts +13 -0
  39. package/dist/src/protocol.js +13 -0
  40. package/dist/src/redis/engine.d.ts +10 -4
  41. package/dist/src/redis/engine.js +103 -32
  42. package/dist/src/redis/member-storage.js +20 -8
  43. package/dist/src/redis/service-times.d.ts +25 -0
  44. package/dist/src/redis/service-times.js +87 -0
  45. package/dist/src/redis/stream.d.ts +2 -1
  46. package/dist/src/redis/stream.js +109 -20
  47. package/dist/src/runtime-state.d.ts +11 -0
  48. package/dist/src/runtime-state.js +13 -0
  49. package/dist/src/runtime.d.ts +4 -2
  50. package/dist/src/runtime.js +5 -2
  51. package/dist/src/serve-http.d.ts +2 -0
  52. package/dist/src/serve-http.js +21 -4
  53. package/dist/src/serve.d.ts +10 -0
  54. package/dist/src/serve.js +43 -3
  55. package/dist/src/storage.d.ts +2 -1
  56. package/dist/src/storage.js +6 -3
  57. package/dist/src/world-boot.d.ts +1 -0
  58. package/dist/src/world-boot.js +6 -0
  59. package/dist/src/world-clock.js +6 -2
  60. package/dist/src/world-store.d.ts +108 -7
  61. package/dist/src/world-store.js +883 -103
  62. package/dist/src/xml.d.ts +30 -0
  63. package/dist/src/xml.js +74 -0
  64. package/generated/pack-facts.json +143 -12
  65. package/inject.cjs +150 -86
  66. package/package.json +20 -1
  67. package/src/actions.ts +78 -21
  68. package/src/ancestry.ts +46 -24
  69. package/src/changeset.ts +1 -1
  70. package/src/clickhouse/compression.ts +172 -0
  71. package/src/clickhouse/index.ts +2 -0
  72. package/src/derived-core.ts +142 -11
  73. package/src/derived.ts +3 -0
  74. package/src/fork.ts +1 -1
  75. package/src/grpc-wire.ts +2 -1
  76. package/src/h2.ts +46 -5
  77. package/src/head.ts +9 -0
  78. package/src/history.ts +7 -7
  79. package/src/index.ts +6 -1
  80. package/src/log.ts +147 -32
  81. package/src/managed-original-inputs.ts +163 -0
  82. package/src/mongodb/client.ts +27 -0
  83. package/src/mongodb/index.ts +5 -0
  84. package/src/mongodb/stream.ts +213 -0
  85. package/src/observe.ts +1 -1
  86. package/src/pack-fetch.ts +20 -8
  87. package/src/packRegistry.ts +3 -15
  88. package/src/protocol.ts +13 -0
  89. package/src/redis/engine.ts +100 -57
  90. package/src/redis/member-storage.ts +17 -9
  91. package/src/redis/service-times.ts +81 -0
  92. package/src/redis/stream.ts +66 -14
  93. package/src/runtime-state.ts +14 -0
  94. package/src/runtime.ts +6 -1
  95. package/src/serve-http.ts +23 -4
  96. package/src/serve.ts +45 -3
  97. package/src/storage.ts +5 -3
  98. package/src/world-boot.ts +6 -0
  99. package/src/world-clock.ts +6 -2
  100. package/src/world-store.ts +704 -91
  101. package/src/xml.ts +71 -0
@@ -0,0 +1,213 @@
1
+ // THE KERNEL'S MONGODB WIRE (`@volter/world-core/mongodb`): MongoDB's client/server message framing as a TwinStream,
2
+ // so an unmodified MongoDB driver connects to a twin as to a server. The kernel holds the framing, which every
3
+ // MongoDB-speaking vendor shares; the pack holds what a command means (it answers each command document from its own
4
+ // engine over the tree) and the document codec (BSON, a library the pack installs). The messages served are what the
5
+ // vendor's own driver sends, by citation (docs/contributing/architecture.md, "Other wires", the MongoDB wire): OP_MSG
6
+ // with a body section and document-sequence sections, and the legacy OP_QUERY a driver's first hello may use, answered
7
+ // with OP_REPLY. Any other message ends the connection.
8
+ // source: archive:https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz#sha256=a12bd599cb4f39cdf222b32380ea3efd1f60c6d88615e0700632a303a9006b02!/package/src/cmap/commands.ts "moreToCome is an OP_MSG only concept"
9
+ import type { TwinStream, TwinStreamSink } from '../twin-fetch.ts';
10
+
11
+ /** A command or reply document as the pack's codec gives it: the pack decides how BSON's types appear in it. */
12
+ export type MongoDocument = Record<string, unknown>;
13
+ /** BSON, as the pack supplies it (the `bson` library, installed by the pack's package.json). The kernel frames
14
+ * documents and never reads inside one but for a command's `$db`. */
15
+ export type MongoCodec = { serialize(document: MongoDocument): Uint8Array; deserialize(bytes: Uint8Array): MongoDocument };
16
+
17
+ /** What the sender's message said beside its command: whether it will take more than one reply to it (OP_MSG's exhaustAllowed). */
18
+ export type MongoAsked = { exhaustAllowed: boolean };
19
+ const TIMED = Symbol('mongodb timed reply');
20
+ /**
21
+ * An answer the wire sends LATER, or goes on sending: the vendor's protocol does this in real time (an awaitable
22
+ * `hello` is held for the sender's maxAwaitTimeMS, and one sent with exhaustAllowed is answered again every such
23
+ * interval, each reply marked moreToCome). The pack says WHAT and WHEN as data and holds no clock: the same request
24
+ * gets the same answer every time. The wire does the holding and the repeating on the wall clock, and the delay is
25
+ * never read back into state (architecture, "The MongoDB wire").
26
+ */
27
+ export type MongoTimedReply = { readonly [TIMED]: true; reply: MongoDocument; afterMs: number; everyMs?: number };
28
+ /**
29
+ * `reply` after `afterMs` (0: at once); with `everyMs`, again each `everyMs` until the peer closes, where the sender
30
+ * allowed more than one reply. The two differ in the vendor's own answers: a hello naming the server's current
31
+ * topology is first answered after the wait, one naming another process at once, and both then repeat at the wait.
32
+ */
33
+ export const timedReply = (reply: MongoDocument, afterMs: number, everyMs?: number): MongoTimedReply => ({ [TIMED]: true, reply, afterMs, ...(everyMs !== undefined ? { everyMs } : {}) });
34
+ const isTimed = (answer: MongoDocument | MongoTimedReply): answer is MongoTimedReply => (answer as MongoTimedReply)[TIMED] === true;
35
+
36
+ export type MongoStreamOptions<Session> = {
37
+ codec: MongoCodec;
38
+ /** A connection opened: what the pack keeps for it (its sessions, an open transaction); never the tree. */
39
+ open(peer: string): Session;
40
+ /** Answer ONE command document sent to `database`. A document sequence (an insert's `documents`, an update's
41
+ * `updates`) arrives as that field holding the array. The answer is the reply document, `ok: 1` or the vendor's
42
+ * error document; a command the pack does not serve is the pack's to answer as the vendor answers an unknown one. */
43
+ command(session: Session, database: string, command: MongoDocument, asked: MongoAsked): Promise<MongoDocument | MongoTimedReply>;
44
+ /** The connection went away: abort what it left open. */
45
+ close?(session: Session): void;
46
+ /** Told why a connection is ended by this stream and not by its peer: the pack threw answering a command, or a
47
+ * message was one this wire does not serve. Ending the connection is all the wire can say to the client, so the
48
+ * reason is said here; unset, it is written to the process's error output. A stream that ends connections in
49
+ * silence leaves its operator with a client that hangs and nothing to read. */
50
+ ended?(reason: string, peer: string): void;
51
+ /** The largest message accepted; the vendor's server names 48,000,000 in its hello (`maxMessageSizeBytes`). */
52
+ maxMessageSizeBytes?: number;
53
+ };
54
+
55
+ export const MongoOp = { REPLY: 1, QUERY: 2004, MSG: 2013 } as const;
56
+ /** OP_MSG's flag bits. */
57
+ export const MongoMsgFlag = { CHECKSUM_PRESENT: 1 << 0, MORE_TO_COME: 1 << 1, EXHAUST_ALLOWED: 1 << 16 } as const;
58
+ const HEADER = 16;
59
+ const dec = new TextDecoder();
60
+ const enc = new TextEncoder();
61
+
62
+ const view = (bytes: Uint8Array): DataView => new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
63
+ function concat(parts: Uint8Array[]): Uint8Array {
64
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); let at = 0;
65
+ for (const p of parts) { out.set(p, at); at += p.length; }
66
+ return out;
67
+ }
68
+ function header(length: number, requestId: number, responseTo: number, opcode: number): Uint8Array {
69
+ const out = new Uint8Array(HEADER), v = view(out);
70
+ v.setInt32(0, length, true); v.setInt32(4, requestId, true); v.setInt32(8, responseTo, true); v.setInt32(12, opcode, true);
71
+ return out;
72
+ }
73
+
74
+ /** An OP_MSG message carrying one document as its body section. */
75
+ export function encodeMsg(codec: MongoCodec, document: MongoDocument, requestId: number, responseTo: number, flags = 0): Uint8Array {
76
+ const body = codec.serialize(document), bits = new Uint8Array(5);
77
+ view(bits).setUint32(0, flags, true);
78
+ return concat([header(HEADER + 4 + 1 + body.length, requestId, responseTo, MongoOp.MSG), bits, body]);
79
+ }
80
+ /** The command of an OP_MSG message: its body section, with each document sequence as the field it names. */
81
+ export function decodeMsg(codec: MongoCodec, message: Uint8Array): { flags: number; document: MongoDocument } {
82
+ const v = view(message), flags = v.getUint32(HEADER, true), end = message.length - (flags & MongoMsgFlag.CHECKSUM_PRESENT ? 4 : 0);
83
+ let document: MongoDocument | undefined; const sequences: Record<string, MongoDocument[]> = {};
84
+ for (let at = HEADER + 4; at < end;) {
85
+ const kind = message[at]!; at += 1;
86
+ if (kind === 0) {
87
+ const size = v.getInt32(at, true);
88
+ if (size < 5 || at + size > end) throw new Error('OP_MSG body section runs past the message');
89
+ document = codec.deserialize(message.subarray(at, at + size)); at += size;
90
+ } else if (kind === 1) {
91
+ const size = v.getInt32(at, true), stop = at + size;
92
+ if (size < 4 || stop > end) throw new Error('OP_MSG document sequence runs past the message');
93
+ const nameEnd = message.indexOf(0, at + 4);
94
+ if (nameEnd < 0 || nameEnd >= stop) throw new Error('OP_MSG document sequence names no field');
95
+ const name = dec.decode(message.subarray(at + 4, nameEnd)), documents: MongoDocument[] = [];
96
+ for (let cursor = nameEnd + 1; cursor < stop;) {
97
+ const length = v.getInt32(cursor, true);
98
+ if (length < 5 || cursor + length > stop) throw new Error('OP_MSG document sequence holds a document that runs past it');
99
+ documents.push(codec.deserialize(message.subarray(cursor, cursor + length))); cursor += length;
100
+ }
101
+ sequences[name] = documents; at = stop;
102
+ } else throw new Error(`OP_MSG section kind ${kind} is unknown`);
103
+ }
104
+ if (!document) throw new Error('OP_MSG carries no body section');
105
+ return { flags, document: { ...document, ...sequences } };
106
+ }
107
+
108
+ /** MongoDB's wire, served: each command document answered by the pack, in arrival order, on one connection. */
109
+ export function createMongoStream<Session>(options: MongoStreamOptions<Session>): TwinStream {
110
+ const limit = options.maxMessageSizeBytes ?? 48_000_000;
111
+ let nextResponse = 0;
112
+ return (sink: TwinStreamSink, peer: string) => {
113
+ const session = options.open(peer);
114
+ let buffer: Uint8Array = new Uint8Array(0);
115
+ let closed = false;
116
+ let chain: Promise<void> = Promise.resolve();
117
+ // A reply being held or repeated (a timed reply): its timer, and the messages that arrived meanwhile. A
118
+ // connection serves one command at a time, so nothing else is answered until a held reply is sent, and nothing
119
+ // at all while a repeating one runs (recording:hello-awaitable-exhaust-then-command.json: a ping sent into the
120
+ // stream has no reply). The timer is not work: `settled` does not wait for it, it does not keep the process
121
+ // alive (unref), and a close from either side clears it at once.
122
+ let held: ReturnType<typeof setTimeout> | undefined;
123
+ const parked: Uint8Array[] = [];
124
+ const release = (): void => { if (held !== undefined) { clearTimeout(held); held = undefined; } parked.length = 0; };
125
+ const hold = (ms: number, then: () => void): void => {
126
+ held = setTimeout(() => { held = undefined; if (!closed) then(); }, Math.max(0, ms));
127
+ (held as { unref?: () => void }).unref?.();
128
+ };
129
+ const end = (reason: string): void => {
130
+ if (closed) return;
131
+ closed = true; release();
132
+ (options.ended ?? ((why, from) => console.error(`mongodb wire: ended the connection from ${from}: ${why}`)))(reason, peer);
133
+ options.close?.(session); sink.end();
134
+ };
135
+
136
+ const answer = async (message: Uint8Array): Promise<void> => {
137
+ const v = view(message), requestId = v.getInt32(4, true), opcode = v.getInt32(12, true);
138
+ if (opcode === MongoOp.MSG) {
139
+ const { flags, document } = decodeMsg(options.codec, message);
140
+ const database = typeof document.$db === 'string' ? document.$db : 'admin';
141
+ const exhaustAllowed = (flags & MongoMsgFlag.EXHAUST_ALLOWED) !== 0;
142
+ const answered = await options.command(session, database, document, { exhaustAllowed });
143
+ // the sender waits for no reply to a message it marked moreToCome
144
+ if (flags & MongoMsgFlag.MORE_TO_COME) return;
145
+ if (!isTimed(answered)) { sink.write(encodeMsg(options.codec, answered, ++nextResponse, requestId)); return; }
146
+ if (answered.everyMs === undefined || !exhaustAllowed) {
147
+ // held once: one reply after the wait, with no flag; then what arrived meanwhile is answered in order
148
+ // source: recording:hello-awaitable-no-exhaust.json "received"
149
+ hold(answered.afterMs, () => {
150
+ sink.write(encodeMsg(options.codec, answered.reply, ++nextResponse, requestId));
151
+ for (const message of parked.splice(0)) take(message);
152
+ });
153
+ return;
154
+ }
155
+ // repeated: each reply marked moreToCome; the first answers the request, each later one answers this
156
+ // wire's own previous reply, until the peer closes
157
+ // source: recording:hello-awaitable-exhaust.json "received"
158
+ let answers = requestId;
159
+ const again = (): void => {
160
+ const id = ++nextResponse;
161
+ sink.write(encodeMsg(options.codec, answered.reply, id, answers, MongoMsgFlag.MORE_TO_COME));
162
+ answers = id;
163
+ hold(answered.everyMs!, again);
164
+ };
165
+ // source: recording:hello-stale-process-exhaust.json "received" (the first at once, the rest at the wait)
166
+ hold(answered.afterMs, again);
167
+ return;
168
+ }
169
+ if (opcode === MongoOp.QUERY) {
170
+ // the legacy form a driver's first hello may take: a query on `<database>.$cmd` whose document is the command
171
+ const nameEnd = message.indexOf(0, HEADER + 4);
172
+ if (nameEnd < 0) throw new Error('OP_QUERY names no collection');
173
+ const namespace = dec.decode(message.subarray(HEADER + 4, nameEnd));
174
+ if (!namespace.endsWith('.$cmd')) throw new Error(`OP_QUERY on ${namespace}: only a command on <database>.$cmd is served`);
175
+ const command = options.codec.deserialize(message.subarray(nameEnd + 1 + 8));
176
+ // the legacy form has no flags of OP_MSG's: nothing is held or repeated for it
177
+ const legacy = await options.command(session, namespace.slice(0, -'.$cmd'.length), command, { exhaustAllowed: false });
178
+ const reply = options.codec.serialize(isTimed(legacy) ? legacy.reply : legacy);
179
+ // OP_REPLY: responseFlags (AwaitCapable), cursorID 0, startingFrom 0, numberReturned 1, then the document
180
+ const fixed = new Uint8Array(20), f = view(fixed);
181
+ f.setInt32(0, 8, true); f.setInt32(16, 1, true);
182
+ sink.write(concat([header(HEADER + 20 + reply.length, ++nextResponse, requestId, MongoOp.REPLY), fixed, reply]));
183
+ return;
184
+ }
185
+ throw new Error(`opcode ${opcode} is not served`);
186
+ };
187
+
188
+ // One message, in arrival order: parked while a reply is held, answered otherwise.
189
+ const take = (message: Uint8Array): void => {
190
+ chain = chain.then(() => (closed ? undefined : held !== undefined ? void parked.push(message) : answer(message))).catch((error) => end(error instanceof Error ? error.message : String(error)));
191
+ };
192
+ return {
193
+ data(chunk: Uint8Array): void {
194
+ if (closed) return;
195
+ buffer = buffer.length ? concat([buffer, chunk]) : chunk;
196
+ while (buffer.length >= 4) {
197
+ const length = view(buffer).getInt32(0, true);
198
+ // where the citation stops: what the vendor's server does with a malformed or oversized message is not
199
+ // read; this ends the connection, which no driver's well-formed message reaches
200
+ if (length < HEADER || length > limit) { end(`a message of ${length} bytes (accepted: ${HEADER} to ${limit})`); return; }
201
+ if (buffer.length < length) break;
202
+ const message = buffer.subarray(0, length); buffer = buffer.subarray(length);
203
+ take(message.slice());
204
+ }
205
+ },
206
+ settled: () => chain,
207
+ close: () => { if (!closed) { closed = true; release(); options.close?.(session); } },
208
+ };
209
+ };
210
+ }
211
+
212
+ /** The bytes of a C string, for a client's OP_QUERY. */
213
+ export const cstring = (text: string): Uint8Array => concat([enc.encode(text), new Uint8Array(1)]);
package/src/observe.ts CHANGED
@@ -145,5 +145,5 @@ function foldResources(service: string, resources: ObservedResource[], opts: { r
145
145
  }
146
146
  }
147
147
  return report;
148
- });
148
+ }, { operation: 'observation batch' });
149
149
  }
package/src/pack-fetch.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { parseExactJson } from './exact-json.ts';
2
+ import { redisServiceTimes } from './redis/service-times.ts';
2
3
  import { readHostPort } from './host-port.ts';
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
@@ -431,6 +432,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
431
432
  return readable instanceof Response ? readable : readable ? atContentHost(readable) : undecodable(arrived);
432
433
  };
433
434
  const fetch = async (arrived: Request): Promise<Response> => {
435
+ redisServiceTimes.lap('1 request made by the stream');
434
436
  // the discovery door is the pack's own, whichever lane would take its path
435
437
  const asked = new URL(arrived.url).pathname.replace(/\/+$/, '') || '/';
436
438
  const discovery = arrived.method === 'GET' && m.discovery !== undefined && (asked === '/twin' || asked === '/twin/scenario');
@@ -460,6 +462,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
460
462
  const url = new URL(request.url);
461
463
  const path = url.pathname.replace(/\/+$/, '') || '/';
462
464
  const readOnly = (options.readOnly ?? false) || isReadOnlyRequest(request);
465
+ redisServiceTimes.lap('2 content and lane routes, decoding, routing');
463
466
  if (request.method === 'GET' && path === '/twin' && m.discovery) {
464
467
  const d = m.discovery;
465
468
  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? } } }.' }] : [])];
@@ -562,12 +565,19 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
562
565
  if (!parts.around) return encoded(m, request, await decided(request));
563
566
  // The front reads a context before forwarding (possibly with new headers). Materialize a transport body before
564
567
  // those independent reads, rather than chaining tees of the incoming stream through the front and the scenario.
568
+ redisServiceTimes.lap('3 doors, screens and machine routes passed');
565
569
  const bytes = request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.arrayBuffer();
566
570
  const buffered = new Request(request.url, { method: request.method, headers: request.headers, signal: request.signal, ...(bytes ? { body: bytes } : {}) });
567
571
  const matched = matchOperation(routes, buffered.method, url.pathname, url.searchParams, buffered.headers);
568
572
  const operation = matched?.operation ?? ({ id: 'unmatched', method: buffered.method, path: url.pathname, class: buffered.method === 'GET' ? 'retrieve' : 'action' } as DerivedOperation);
573
+ redisServiceTimes.lap('4 body buffered, operation matched');
569
574
  const ctx = await contextFor(m, { request: buffered.clone(), operation, params: matched?.params ?? pathParams(routes, url.pathname) }, scope);
570
- return encoded(m, buffered, await parts.around(ctx, (rewritten) => decided(rewritten ?? buffered)));
575
+ redisServiceTimes.lap('5 the around context opened');
576
+ const answered = await parts.around(ctx, (rewritten) => decided(rewritten ?? buffered));
577
+ redisServiceTimes.lap('6d the handler, and the around after it');
578
+ const sent = await encoded(m, buffered, answered);
579
+ redisServiceTimes.lap('7 answer encoded');
580
+ return sent;
571
581
  };
572
582
  // the request's id is the correlation of every entry it appends (runtime contract D3), minted here, always: it is the
573
583
  // group a perform sends as one call (the real-system adapters, "Perform"). Under `auto` the group is performed once
@@ -743,8 +753,16 @@ export function createVendorFetch(manifest: VendorManifest, lanes: Record<string
743
753
  const ingest = Object.values(lanes).find((l) => l.fetch.ingest)?.fetch.ingest;
744
754
  const lane = laneRouter(manifest.vendor, manifest.lanes, lanes);
745
755
  return Object.assign(async (request: Request): Promise<Response> => {
746
- for (const part of Object.values(lanes)) await (part.fetch as typeof part.fetch & { advanceClock?: (request: Request) => Promise<void> }).advanceClock?.(request);
747
756
  const path = new URL(request.url).pathname.replace(/\/+$/, '') || '/';
757
+ // the discovery door is answered before any lane's clock moves, as a pack's own is (createPackFetch): reading what
758
+ // the twin is moves no vendor clock
759
+ if (request.method === 'GET' && path === '/twin' && manifest.discovery) {
760
+ // the vendor's screens are its lanes' (a dashboard drawn by one lane is the vendor's)
761
+ const screens = lanesWorkspaces(lanes);
762
+ const laneDiscovery = await laneDoors(lanes, request, '/twin');
763
+ return Response.json({ ...statefulTwinManifest({ vendor: manifest.vendor, ...manifest.discovery }), ...(screens.length ? { screens } : {}), ...(Object.keys(laneDiscovery).length ? { lanes: laneDiscovery } : {}) });
764
+ }
765
+ for (const part of Object.values(lanes)) await (part.fetch as typeof part.fetch & { advanceClock?: (request: Request) => Promise<void> }).advanceClock?.(request);
748
766
  // the vendor's board is the frames of the lanes that lay one out, each drawn when any lane's screen takes its url
749
767
  if (request.method === 'GET' && path === '/twin/board') {
750
768
  const frames: BoardFrame[] = [];
@@ -758,12 +776,6 @@ export function createVendorFetch(manifest: VendorManifest, lanes: Record<string
758
776
  const draws = (url: string): boolean => 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) return Response.json({ vendor: manifest.vendor, lanes: laneScenarios });
@@ -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
  }