@volter/world-core 3.0.155 → 3.0.157
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/attach.cjs +2 -0
- package/capability-decision.cjs +6 -4
- package/dist/attach.cjs +2 -0
- package/dist/capability-decision.cjs +6 -4
- package/dist/generated/pack-facts.json +204 -4
- package/dist/inject.cjs +150 -86
- package/dist/network-policy.cjs +11 -1
- package/dist/network-policy.d.cts +1 -0
- package/dist/pack-facts.d.cts +3 -0
- package/dist/required-credential-transport.cjs +29 -0
- package/dist/required-credential-transport.d.cts +3 -0
- 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/blob-store.d.ts +16 -0
- package/dist/src/blob-store.js +76 -1
- 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/derived-core.d.ts +84 -7
- package/dist/src/derived-core.js +336 -61
- package/dist/src/derived-real.js +97 -15
- package/dist/src/derived.d.ts +5 -0
- package/dist/src/derived.js +3 -0
- package/dist/src/executor.d.ts +7 -0
- package/dist/src/executor.js +15 -1
- package/dist/src/fork.js +1 -1
- package/dist/src/git/lfs.d.ts +13 -3
- package/dist/src/git/lfs.js +41 -9
- package/dist/src/grpc-wire.js +2 -1
- package/dist/src/h2.js +63 -10
- package/dist/src/head.d.ts +5 -0
- package/dist/src/head.js +15 -0
- package/dist/src/history.d.ts +1 -1
- package/dist/src/history.js +7 -7
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.js +6 -1
- package/dist/src/live/ingest.js +9 -13
- package/dist/src/live/rtmp.js +7 -5
- package/dist/src/live/segmenter.js +7 -1
- package/dist/src/log.d.ts +23 -3
- package/dist/src/log.js +160 -37
- package/dist/src/machines.js +5 -1
- package/dist/src/managed-go-programs.d.ts +55 -0
- package/dist/src/managed-go-programs.js +133 -0
- 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 +97 -0
- package/dist/src/mongodb/stream.js +206 -0
- package/dist/src/observe.js +1 -1
- package/dist/src/pack-fetch.d.ts +6 -1
- package/dist/src/pack-fetch.js +69 -14
- package/dist/src/pack-paths.d.ts +2 -1
- package/dist/src/pack-paths.js +6 -1
- 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/resource-blob.d.ts +4 -0
- package/dist/src/resource-blob.js +8 -1
- package/dist/src/runtime-state.d.ts +11 -0
- package/dist/src/runtime-state.js +13 -0
- package/dist/src/runtime.d.ts +7 -5
- package/dist/src/runtime.js +7 -4
- package/dist/src/serve-http.d.ts +2 -0
- package/dist/src/serve-http.js +73 -6
- package/dist/src/serve.d.ts +10 -0
- package/dist/src/serve.js +43 -3
- package/dist/src/sockets.d.ts +4 -1
- package/dist/src/sockets.js +59 -6
- package/dist/src/storage.d.ts +2 -1
- package/dist/src/storage.js +6 -3
- package/dist/src/vendor-call.js +58 -0
- 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 +122 -7
- package/dist/src/world-store.js +959 -103
- package/dist/src/xml.d.ts +30 -0
- package/dist/src/xml.js +74 -0
- package/generated/pack-facts.json +204 -4
- package/inject.cjs +150 -86
- package/network-policy.cjs +11 -1
- package/network-policy.d.cts +1 -0
- package/pack-facts.d.cts +3 -0
- package/package.json +27 -1
- package/required-credential-transport.cjs +29 -0
- package/required-credential-transport.d.cts +3 -0
- package/src/actions.ts +78 -21
- package/src/ancestry.ts +46 -24
- package/src/blob-store.ts +59 -1
- package/src/changeset.ts +1 -1
- package/src/clickhouse/compression.ts +172 -0
- package/src/clickhouse/index.ts +2 -0
- package/src/derived-core.ts +356 -61
- package/src/derived-real.ts +80 -14
- package/src/derived.ts +9 -1
- package/src/executor.ts +17 -3
- package/src/fork.ts +1 -1
- package/src/git/lfs.ts +40 -9
- package/src/grpc-wire.ts +2 -1
- package/src/h2.ts +46 -5
- package/src/head.ts +15 -0
- package/src/history.ts +7 -7
- package/src/index.ts +7 -1
- package/src/live/ingest.ts +7 -3
- package/src/live/rtmp.ts +7 -5
- package/src/live/segmenter.ts +7 -1
- package/src/log.ts +147 -32
- package/src/machines.ts +5 -1
- package/src/managed-go-programs.ts +146 -0
- 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 +223 -0
- package/src/observe.ts +1 -1
- package/src/pack-fetch.ts +66 -16
- package/src/pack-paths.ts +5 -1
- 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/resource-blob.ts +9 -1
- package/src/runtime-state.ts +14 -0
- package/src/runtime.ts +10 -3
- package/src/serve-http.ts +65 -6
- package/src/serve.ts +45 -3
- package/src/sockets.ts +39 -8
- package/src/storage.ts +5 -3
- package/src/vendor-call.ts +54 -0
- package/src/world-boot.ts +6 -0
- package/src/world-clock.ts +6 -2
- package/src/world-store.ts +768 -91
- package/src/xml.ts +71 -0
package/src/pack-paths.ts
CHANGED
|
@@ -102,13 +102,17 @@ export function installedPackageDir(from: string, name: string): string | undefi
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/** A pack repository itself, or
|
|
105
|
+
/** A pack repository itself, or a platform checkout's P3 repository: the one `VOLTER_PACKS` names (a worktree's own
|
|
106
|
+
* pinned packs, as the repository's scripts read it), else the one beside it. Applications do not acquire a
|
|
106
107
|
* sibling checkout merely because one happens to be beside them. */
|
|
107
108
|
export function packRepository(from: string): string | undefined {
|
|
108
109
|
let dir = resolve(from);
|
|
109
110
|
for (;;) {
|
|
110
111
|
if (packageName(dir) === 'twin-packs-p3') return dir;
|
|
111
112
|
if (packageName(join(dir, 'packages', 'world-core')) === '@volter/world-core') {
|
|
113
|
+
const env = globalThis.process?.env?.VOLTER_PACKS;
|
|
114
|
+
const named = env ? resolve(env) : undefined;
|
|
115
|
+
if (named && packageName(named) === 'twin-packs-p3') return named;
|
|
112
116
|
const sibling = join(dirname(dir), 'twin-packs-p3');
|
|
113
117
|
if (packageName(sibling) === 'twin-packs-p3') return sibling;
|
|
114
118
|
}
|
package/src/packRegistry.ts
CHANGED
|
@@ -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: '
|
|
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;
|
package/src/protocol.ts
ADDED
|
@@ -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
|
+
}
|
package/src/redis/engine.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
417
|
-
/** The
|
|
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
|
-
//
|
|
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
|
|
423
|
-
this.scripts = new
|
|
424
|
-
this.revs = new
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
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
|
-
|
|
1627
|
-
|
|
1666
|
+
// The keys this run wrote, by name, read before the flush clears them: a stream wakes only the clients blocked on one.
|
|
1667
|
+
const touched = space.touchedNames();
|
|
1668
|
+
const flushing = times.now();
|
|
1669
|
+
try { await (space.ctx.dialect.storage?.flush(space) ?? flushKeySpace(space)); } finally { times.add('flush', times.now() - flushing); }
|
|
1670
|
+
return { items, wrote, instant: nowMs, touched };
|
|
1628
1671
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Redis's collection layout over the World tree. First need: the managed Redis queue's growing events stream.
|
|
2
2
|
// Key metadata and each hash field/set member/sorted-set member/stream entry are separate subjects.
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import {
|
|
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
|
}
|
package/src/resource-blob.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isAbsolute, join, relative } from 'node:path';
|
|
2
|
-
import { getActiveBlobStore, readBlobRange } from './blob-store.ts';
|
|
2
|
+
import { type BlobWritten, getActiveBlobStore, readBlobRange, streamChunks, writeBlobStream } from './blob-store.ts';
|
|
3
3
|
import { readBranchMeta } from './log.ts';
|
|
4
4
|
import { worldPaths } from './storage.ts';
|
|
5
5
|
|
|
@@ -19,6 +19,14 @@ export async function writeResourceBlob(service: string, key: string, bytes: Uin
|
|
|
19
19
|
await getActiveBlobStore().put(resourceBlobFile(worldPaths(service, root).resources, key), bytes);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** Write a resource payload on this branch from a body as it arrives (a model's weights, an upload's bytes), kept only
|
|
23
|
+
* when `accept` holds for its size and SHA-256 (the blob seam's `writeBlobStream`). True when kept. */
|
|
24
|
+
export async function writeResourceBlobStream(service: string, key: string, body: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>, accept: (written: BlobWritten) => boolean, root?: string): Promise<boolean> {
|
|
25
|
+
assertResourceKey(key);
|
|
26
|
+
const chunks = body instanceof ReadableStream ? streamChunks(body) : body;
|
|
27
|
+
return writeBlobStream(resourceBlobFile(worldPaths(service, root).resources, key), chunks, accept);
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
/** Remove this branch's copy of a resource payload; an ancestor's stays its own (the row that named it is what a
|
|
23
31
|
* deletion ends). */
|
|
24
32
|
export async function removeResourceBlob(service: string, key: string, root?: string): Promise<void> {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The lifecycle's state/identity primitives. Re-export their canonical modules
|
|
2
|
+
// so the active store, locks and scopes are shared with the full runtime and packs.
|
|
3
|
+
// Booting an empty World does not require pack registration or derived engines.
|
|
4
|
+
export { getActiveWorldStore, removeCarriedLocks } from './world-store.ts';
|
|
5
|
+
export type { WorldStore } from './world-store.ts';
|
|
6
|
+
export { stateDirName, withFileLock, worldPaths, worldStateRoot, WORLD_DATA_ENV } from './storage.ts';
|
|
7
|
+
export { assertStateRemovable, withAncestryLock, withStateRemoval } from './ancestry.ts';
|
|
8
|
+
export { readBranchMeta } from './log.ts';
|
|
9
|
+
export { volterHome } from './volter-home.ts';
|
|
10
|
+
export { installedPackageDir, installedVendorPacks, isDerivedPack, packRepository, vendorOfPackModule } from './pack-paths.ts';
|
|
11
|
+
export { PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding } from './protocol.ts';
|
|
12
|
+
export { WORLD_BOOT_PATH } from './serve-http.ts';
|
|
13
|
+
export { WORLD_ENV_NAMES_ENV } from './world-env.ts';
|
|
14
|
+
export { beginPlaceholderPull, endPlaceholderPull, placeholderPullActive } from './placeholder-remote.ts';
|