@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.
- package/dist/generated/pack-facts.json +140 -0
- package/dist/inject.cjs +150 -86
- package/dist/src/actions.d.ts +15 -0
- package/dist/src/actions.js +82 -21
- package/dist/src/ancestry.d.ts +5 -4
- package/dist/src/ancestry.js +53 -28
- package/dist/src/changeset.js +1 -1
- package/dist/src/clickhouse/compression.d.ts +28 -0
- package/dist/src/clickhouse/compression.js +281 -0
- package/dist/src/clickhouse/index.d.ts +1 -0
- package/dist/src/clickhouse/index.js +1 -0
- package/dist/src/client-bundle.js +15 -15
- package/dist/src/derived-core.d.ts +12 -1
- package/dist/src/derived-core.js +145 -11
- package/dist/src/derived.js +3 -0
- package/dist/src/fork.js +1 -1
- package/dist/src/graphql-wire.d.ts +5 -0
- package/dist/src/graphql-wire.js +6 -6
- package/dist/src/grpc-wire.js +2 -1
- package/dist/src/h2.js +63 -10
- package/dist/src/head.d.ts +3 -0
- package/dist/src/head.js +9 -0
- package/dist/src/history.d.ts +1 -1
- package/dist/src/history.js +7 -7
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +5 -1
- package/dist/src/log.d.ts +23 -3
- package/dist/src/log.js +160 -37
- package/dist/src/managed-original-inputs.d.ts +101 -0
- package/dist/src/managed-original-inputs.js +125 -0
- package/dist/src/mongodb/client.d.ts +8 -0
- package/dist/src/mongodb/client.js +27 -0
- package/dist/src/mongodb/index.d.ts +2 -0
- package/dist/src/mongodb/index.js +5 -0
- package/dist/src/mongodb/stream.d.ts +74 -0
- package/dist/src/mongodb/stream.js +206 -0
- package/dist/src/observe.js +1 -1
- package/dist/src/pack-fetch.d.ts +2 -1
- package/dist/src/pack-fetch.js +21 -9
- package/dist/src/packRegistry.d.ts +10 -14
- package/dist/src/packRegistry.js +2 -13
- package/dist/src/protocol.d.ts +13 -0
- package/dist/src/protocol.js +13 -0
- package/dist/src/redis/engine.d.ts +10 -4
- package/dist/src/redis/engine.js +103 -32
- package/dist/src/redis/member-storage.js +20 -8
- package/dist/src/redis/service-times.d.ts +25 -0
- package/dist/src/redis/service-times.js +87 -0
- package/dist/src/redis/stream.d.ts +2 -1
- package/dist/src/redis/stream.js +109 -20
- package/dist/src/runtime-state.d.ts +11 -0
- package/dist/src/runtime-state.js +13 -0
- package/dist/src/runtime.d.ts +4 -2
- package/dist/src/runtime.js +5 -2
- package/dist/src/serve-http.d.ts +2 -0
- package/dist/src/serve-http.js +21 -4
- package/dist/src/serve.d.ts +10 -0
- package/dist/src/serve.js +43 -3
- package/dist/src/storage.d.ts +2 -1
- package/dist/src/storage.js +6 -3
- package/dist/src/world-boot.d.ts +1 -0
- package/dist/src/world-boot.js +6 -0
- package/dist/src/world-clock.js +6 -2
- package/dist/src/world-store.d.ts +108 -7
- package/dist/src/world-store.js +883 -103
- package/dist/src/xml.d.ts +30 -0
- package/dist/src/xml.js +74 -0
- package/generated/pack-facts.json +140 -0
- package/inject.cjs +150 -86
- package/package.json +20 -1
- package/src/actions.ts +78 -21
- package/src/ancestry.ts +46 -24
- package/src/changeset.ts +1 -1
- package/src/clickhouse/compression.ts +172 -0
- package/src/clickhouse/index.ts +2 -0
- package/src/client-bundle.ts +13 -14
- package/src/derived-core.ts +142 -11
- package/src/derived.ts +3 -0
- package/src/fork.ts +1 -1
- package/src/graphql-wire.ts +9 -6
- package/src/grpc-wire.ts +2 -1
- package/src/h2.ts +46 -5
- package/src/head.ts +9 -0
- package/src/history.ts +7 -7
- package/src/index.ts +6 -1
- package/src/log.ts +147 -32
- package/src/managed-original-inputs.ts +163 -0
- package/src/mongodb/client.ts +27 -0
- package/src/mongodb/index.ts +5 -0
- package/src/mongodb/stream.ts +213 -0
- package/src/observe.ts +1 -1
- package/src/pack-fetch.ts +23 -11
- package/src/packRegistry.ts +3 -15
- package/src/protocol.ts +13 -0
- package/src/redis/engine.ts +100 -57
- package/src/redis/member-storage.ts +17 -9
- package/src/redis/service-times.ts +81 -0
- package/src/redis/stream.ts +66 -14
- package/src/runtime-state.ts +14 -0
- package/src/runtime.ts +6 -1
- package/src/serve-http.ts +23 -4
- package/src/serve.ts +45 -3
- package/src/storage.ts +5 -3
- package/src/world-boot.ts +6 -0
- package/src/world-clock.ts +6 -2
- package/src/world-store.ts +704 -91
- package/src/xml.ts +71 -0
|
@@ -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 {
|
|
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
|
-
|
|
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))
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
write('member', memberId(ctx, name, field), operation, { key: name, field, value: after.get(field) ?? null, gone: !after.has(field) });
|
|
53
60
|
}
|
|
54
|
-
|
|
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())
|
|
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
|
+
};
|
package/src/redis/stream.ts
CHANGED
|
@@ -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(
|
|
11
|
-
|
|
12
|
-
const
|
|
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
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
}
|
|
@@ -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';
|
package/src/runtime.ts
CHANGED
|
@@ -161,6 +161,7 @@ export {
|
|
|
161
161
|
export type { BlobStore } from './blob-store.ts';
|
|
162
162
|
export {
|
|
163
163
|
applyTwinWrite,
|
|
164
|
+
applyTwinWrites,
|
|
164
165
|
applyTwinWriteAtomic,
|
|
165
166
|
createTwinServer,
|
|
166
167
|
CALLER_HEADER,
|
|
@@ -297,6 +298,8 @@ export { installedPackageDir, installedVendorPacks, packVendor, packRepository,
|
|
|
297
298
|
export * as git from './git/index.ts';
|
|
298
299
|
// S3's wire (contract "S3's wire is a kernel library too"): its XML, CORS, lifecycle, delete batches, ETags, body bytes.
|
|
299
300
|
export * as s3 from './s3/wire.ts';
|
|
301
|
+
// Generic strict XML grammar; vendor interpretation remains in its consumer.
|
|
302
|
+
export * as xml from './xml.ts';
|
|
300
303
|
|
|
301
304
|
// The push arm: the kernel's transaction over a pack's one `perform<Name>Action` (contract section of that name).
|
|
302
305
|
|
|
@@ -323,7 +326,7 @@ export { readTreeMap, readParentTreeMap, treeStamp, treeChangesSince, originEntr
|
|
|
323
326
|
|
|
324
327
|
export { referenceField, registerReferences, packReferences, resolveReferences, type ReferenceDeclaration } from './references.ts';
|
|
325
328
|
// the HTTP server seam: the one place the serve path meets the runtime (Bun or Node)
|
|
326
|
-
export { serveHttp, serveStream, connectWebSocket, nodeBuiltin, WORLD_BOOT_PATH, type HttpServer, type ServeHttpOptions, type HttpHandler } from './serve-http.ts';
|
|
329
|
+
export { serveHttp, serveStream, connectWebSocket, nodeBuiltin, servedHandlerOf, WORLD_BOOT_PATH, type HttpServer, type ServeHttpOptions, type HttpHandler } from './serve-http.ts';
|
|
327
330
|
// two runtime-neutral helpers for a serve path: a file as a Response, a page's client bundle
|
|
328
331
|
export { fileResponse, contentTypeOf } from './file-response.ts';
|
|
329
332
|
export { bundleClient, filePathOf } from './client-bundle.ts';
|
|
@@ -347,3 +350,5 @@ export type { MachinePool, MachineSpec, MachineProcess, MachineStatus, MachineFi
|
|
|
347
350
|
|
|
348
351
|
|
|
349
352
|
export { boardFrameKey } from './board-keys.ts';
|
|
353
|
+
|
|
354
|
+
export * as managedOriginalInputs from './managed-original-inputs.ts';
|
package/src/serve-http.ts
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
// as a standard Request and the handler's Response written back. Async, because binding a port is
|
|
5
5
|
// asynchronous on Node — Bun's synchronous bind is the special case.
|
|
6
6
|
//
|
|
7
|
-
// Nothing here runs at module scope, and `node:http` is reached through `process.getBuiltinModule`
|
|
7
|
+
// Nothing here runs at module scope (the served-handler registry is made on first use), and `node:http` is reached through `process.getBuiltinModule`
|
|
8
8
|
// rather than a static import: this module rides into browser client bundles through the kernel's
|
|
9
9
|
// entrypoint, where a static `node:` import is the 2026-09-06 class of break.
|
|
10
10
|
|
|
11
11
|
import type { TwinStream } from './twin-fetch.ts';
|
|
12
|
+
import { worldBootAnswer } from './world-boot.ts';
|
|
12
13
|
|
|
13
14
|
/** One socket library at the host seam, shared by inbound upgrades and authenticated outbound relays. */
|
|
14
15
|
const socketLibrary = () => import('ws');
|
|
@@ -64,6 +65,24 @@ const bunRuntime = (): BunLike | undefined => { const b = (globalThis as { Bun?:
|
|
|
64
65
|
/** The path a World asks a service it booted, when that service's port answers, which boot it belongs to. */
|
|
65
66
|
export const WORLD_BOOT_PATH = '/__volter/world-boot';
|
|
66
67
|
|
|
68
|
+
/** Each listening server's own handler, by its port, as the seam serves it (the boot door answered, nothing journaled):
|
|
69
|
+
* the World's co-located host warms a twin in-process through it (world-runtime host.ts warmInProcess), so its warm-up is
|
|
70
|
+
* never a request on the wire nor in any journal. A server leaves it when it stops. Kept on globalThis under a registered
|
|
71
|
+
* symbol, as the reference registry is (references.ts), so a pack that resolved its own copy of this module shares it. */
|
|
72
|
+
const servedHandlers = (): Map<number, HttpHandler> =>
|
|
73
|
+
((globalThis as Record<symbol, unknown>)[Symbol.for('volter.servedHandlers')] ??= new Map<number, HttpHandler>()) as Map<number, HttpHandler>;
|
|
74
|
+
|
|
75
|
+
/** The handler serving `port` in this process, when a server made through the seam listens there. */
|
|
76
|
+
export function servedHandlerOf(port: number): HttpHandler | undefined { return servedHandlers().get(port); }
|
|
77
|
+
|
|
78
|
+
/** A server's handler kept by its port until the server stops. */
|
|
79
|
+
function keepHandler(server: HttpServer, handler: HttpHandler): HttpServer {
|
|
80
|
+
const port = server.port;
|
|
81
|
+
if (!port) return server;
|
|
82
|
+
servedHandlers().set(port, handler);
|
|
83
|
+
return { ...server, stop: async (closeActive?: boolean) => { if (servedHandlers().get(port) === handler) servedHandlers().delete(port); await server.stop(closeActive); } };
|
|
84
|
+
}
|
|
85
|
+
|
|
67
86
|
export async function serveHttp(options: ServeHttpOptions): Promise<HttpServer> {
|
|
68
87
|
// Inside a World every server made through the seam answers which boot it belongs to, so the World can tell its own
|
|
69
88
|
// twin from a leftover World's that holds the same port. Outside a World nothing changes.
|
|
@@ -76,7 +95,7 @@ export async function serveHttp(options: ServeHttpOptions): Promise<HttpServer>
|
|
|
76
95
|
const worldBoot = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.VOLTER_WORLD_BOOT_ID;
|
|
77
96
|
if (worldBoot) {
|
|
78
97
|
const served = options.fetch;
|
|
79
|
-
options = { ...options, fetch: (request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ?
|
|
98
|
+
options = { ...options, fetch: (request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ? worldBootAnswer(worldBoot) : served(request)) };
|
|
80
99
|
}
|
|
81
100
|
// loopback by default: an unnamed interface is never every interface
|
|
82
101
|
const hostname = options.hostname ?? '127.0.0.1';
|
|
@@ -102,9 +121,9 @@ export async function serveHttp(options: ServeHttpOptions): Promise<HttpServer>
|
|
|
102
121
|
} : {}),
|
|
103
122
|
});
|
|
104
123
|
const port = server.port ?? 0;
|
|
105
|
-
return { hostname, port, url: server.url instanceof URL ? server.url : new URL(`http://${urlHost}:${port}/`), stop: async (closeActive = true) => { await server.stop(closeActive); } };
|
|
124
|
+
return keepHandler({ hostname, port, url: server.url instanceof URL ? server.url : new URL(`http://${urlHost}:${port}/`), stop: async (closeActive = true) => { await server.stop(closeActive); } }, options.fetch);
|
|
106
125
|
}
|
|
107
|
-
return serveOnNode({ ...options, hostname, urlHost, fetch: decorate ? decorate(options) : options.fetch });
|
|
126
|
+
return keepHandler(await serveOnNode({ ...options, hostname, urlHost, fetch: decorate ? decorate(options) : options.fetch }), options.fetch);
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
type NodeHttp = typeof import('node:http');
|
package/src/serve.ts
CHANGED
|
@@ -12,13 +12,14 @@ import { parseExactJson } from './exact-json.ts';
|
|
|
12
12
|
// reality). Forking is a separate data operation (fork.ts), never a serve mode.
|
|
13
13
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
14
14
|
import { serveHttp, setServeDecorator, WORLD_BOOT_PATH, type HttpHandler } from './serve-http.ts';
|
|
15
|
-
import { performAtHead } from './head.ts';
|
|
15
|
+
import { performAtHead, performsAtHeadNow } from './head.ts';
|
|
16
16
|
import { withHeaders } from './with-headers.ts';
|
|
17
17
|
import { createHash } from 'node:crypto';
|
|
18
18
|
import { dirname, join } from 'node:path';
|
|
19
19
|
import type { SubjectFields } from './hash.ts';
|
|
20
20
|
import { hashFieldValue } from './hash.ts';
|
|
21
|
-
import { appendActionIfAbsent, appendActionOccurrence, decideAndAppendAction, projectResources } from './actions.ts';
|
|
21
|
+
import { appendActionIfAbsent, appendActionOccurrence, appendActionOccurrences, decideAndAppendAction, projectResources } from './actions.ts';
|
|
22
|
+
import { readSubject } from './log.ts';
|
|
22
23
|
import type { ActionProjection, TwinAction, TwinActionPrecondition } from './actions.ts';
|
|
23
24
|
import { observeWorldPaths, worldPaths } from './storage.ts';
|
|
24
25
|
import { getActiveWorldStore } from './world-store.ts';
|
|
@@ -699,12 +700,53 @@ export async function applyTwinWrite(
|
|
|
699
700
|
return {
|
|
700
701
|
result,
|
|
701
702
|
get resource(): TwinResource {
|
|
702
|
-
return resolved ??=
|
|
703
|
+
return resolved ??= readSubject(service, write.subjectType, answerId, root)
|
|
703
704
|
?? ({ id: answerId, type: write.subjectType, updatedAt: landed.occurredAt, ...write.fields } as TwinResource);
|
|
704
705
|
},
|
|
705
706
|
};
|
|
706
707
|
}
|
|
707
708
|
|
|
709
|
+
/**
|
|
710
|
+
* Several local writes of one service, landed as `applyTwinWrite` lands each called in turn — the same actions, ids and
|
|
711
|
+
* order — under one lock and one append (actions.ts `appendActionOccurrences`). Writes that need the one-at-a-time
|
|
712
|
+
* path take it: a caller's identity (`idempotencyKey`/`actionId`), preconditions, or a head that performs each entry as
|
|
713
|
+
* it lands. An input is offloaded once per distinct value, as each write's own offload would name it.
|
|
714
|
+
*/
|
|
715
|
+
export async function applyTwinWrites(
|
|
716
|
+
service: string,
|
|
717
|
+
writes: readonly TwinWriteInput[],
|
|
718
|
+
root?: string,
|
|
719
|
+
): Promise<Array<{ result: TwinWriteResult; resource: TwinResource }>> {
|
|
720
|
+
if (writes.length <= 1 || performsAtHeadNow(service, root) || writes.some((write) => write.idempotencyKey || write.actionId || write.preconditions?.length)) {
|
|
721
|
+
const out: Array<{ result: TwinWriteResult; resource: TwinResource }> = [];
|
|
722
|
+
for (const write of writes) out.push(await applyTwinWrite(service, write, root));
|
|
723
|
+
return out;
|
|
724
|
+
}
|
|
725
|
+
// Each distinct input offloaded once (S2 R17): `offloadInput` is content-addressed (a string's blob is named by its
|
|
726
|
+
// SHA-256), so the result is the one each write's own offload would give, and a command's writes, which share one
|
|
727
|
+
// recorded request, hash and look up its blob once instead of once per write.
|
|
728
|
+
const offloaded = new Map<unknown, unknown>();
|
|
729
|
+
for (const write of writes) if (write.input !== undefined && !offloaded.has(write.input)) offloaded.set(write.input, await offloadInput(service, write.input, root));
|
|
730
|
+
const committed = appendActionOccurrences(writes.map((write) => actionForTwinWrite(service,
|
|
731
|
+
write.input === undefined ? write : { ...write, input: offloaded.get(write.input) as TwinWriteInput['input'] })), root);
|
|
732
|
+
const out: Array<{ result: TwinWriteResult; resource: TwinResource }> = [];
|
|
733
|
+
for (const [index, { appended, action: landed, placeholder }] of committed.entries()) {
|
|
734
|
+
const write = writes[index]!;
|
|
735
|
+
// the head does not perform here (`performsAtHeadNow` was false): this marks the request's deferred group as written
|
|
736
|
+
if (appended && !placeholder) await performAtHead(service, landed, root);
|
|
737
|
+
const answerId = write.subjectId;
|
|
738
|
+
let resolved: TwinResource | undefined;
|
|
739
|
+
out.push({
|
|
740
|
+
result: { status: appended ? 'performed' : 'replayed', actionId: landed.id },
|
|
741
|
+
get resource(): TwinResource {
|
|
742
|
+
return resolved ??= readSubject(service, write.subjectType, answerId, root)
|
|
743
|
+
?? ({ id: answerId, type: write.subjectType, updatedAt: landed.occurredAt, ...write.fields } as TwinResource);
|
|
744
|
+
},
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
return out;
|
|
748
|
+
}
|
|
749
|
+
|
|
708
750
|
export async function createTwinServer(options: { service: string; root?: string; port?: number; readOnly?: boolean }): Promise<{ port: number; stop: () => void }> {
|
|
709
751
|
const readOnly = options.readOnly ?? false;
|
|
710
752
|
const serveOptions = {
|
package/src/storage.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
GenericWorldStateSchema,
|
|
7
7
|
WorldServiceEventSchema,
|
|
8
8
|
} from './schemas.ts';
|
|
9
|
-
import { getActiveWorldStore } from './world-store.ts';
|
|
9
|
+
import { getActiveWorldStore, storageLockScope, TREE_GENERATION, type WorldLockOptions } from './world-store.ts';
|
|
10
10
|
import { refuseReadOnlyWrite } from './request-scope.ts';
|
|
11
11
|
import { parentEntries, toEvent } from './log.ts';
|
|
12
12
|
import type {
|
|
@@ -198,8 +198,8 @@ function appendJsonl(path: string, value: unknown): void {
|
|
|
198
198
|
* single-instance store it is a synchronous pass-through. Used to make read-then-append
|
|
199
199
|
* critical sections atomic. Public so world-runtime can guard concurrent `upWorld`
|
|
200
200
|
* claims of one instance dir with the SAME lock semantics the event log uses (TWIN-36). */
|
|
201
|
-
export function withFileLock<T>(lockPath: string, fn: () => T): T {
|
|
202
|
-
return withStateLock(lockPath, fn);
|
|
201
|
+
export function withFileLock<T>(lockPath: string, fn: () => T, options: WorldLockOptions = {}): T {
|
|
202
|
+
return withStateLock(lockPath, fn, storageLockScope(options));
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
function writeJsonAtomic(path: string, value: unknown): void {
|
|
@@ -385,6 +385,8 @@ const KNOWN_SERVICE_ENTRIES = new Set([
|
|
|
385
385
|
function isKnownServiceEntry(name: string): boolean {
|
|
386
386
|
// log.ts: the branch pointer and the checkpoint
|
|
387
387
|
if (name === 'branch.json' || name === 'checkpoints') return true;
|
|
388
|
+
// world-store.ts: the generation a file store keeps beside a tree's files
|
|
389
|
+
if (name === TREE_GENERATION) return true;
|
|
388
390
|
return KNOWN_SERVICE_ENTRIES.has(name) || name.endsWith('.tmp') || /\.stale\.\d+\.\d+$/.test(name);
|
|
389
391
|
}
|
|
390
392
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// One private boot answer for the kernel's HTTP and gRPC serve seams.
|
|
2
|
+
// Reading process identity happens on request; importing this helper has no side effects.
|
|
3
|
+
export function worldBootAnswer(boot: string): Response {
|
|
4
|
+
const pid = (globalThis as { process?: { pid?: number } }).process?.pid;
|
|
5
|
+
return Response.json({ boot, pid });
|
|
6
|
+
}
|
package/src/world-clock.ts
CHANGED
|
@@ -26,10 +26,14 @@ const machineNow = (): number => {
|
|
|
26
26
|
* now); real wall-clock time otherwise. */
|
|
27
27
|
export function worldNow(): string {
|
|
28
28
|
const file = process.env[WORLD_CLOCK_ENV];
|
|
29
|
-
|
|
29
|
+
// One read answers both whether the clock file is there and what it holds.
|
|
30
|
+
// Kept while the clock file's own facts stand (world-store.ts readKept): every operation asks the time.
|
|
31
|
+
const store = getActiveWorldStore();
|
|
32
|
+
const held = file ? (store.readKept ? store.readKept(file) : store.read(file)) : null;
|
|
33
|
+
if (held !== null) {
|
|
30
34
|
// A configured-but-corrupt clock is a WORLD defect — parseClock fails loudly, never silently drifting back to
|
|
31
35
|
// wall-clock (two services disagreeing about now breaks the world).
|
|
32
|
-
const clock = clockForm.parseClock(
|
|
36
|
+
const clock = clockForm.parseClock(held);
|
|
33
37
|
return new Date(clockForm.clockNowMs(clock, machineNow())).toISOString();
|
|
34
38
|
}
|
|
35
39
|
return new Date(machineNow()).toISOString();
|