@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
package/dist/src/pack-fetch.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseExactJson } from "./exact-json.js";
|
|
2
|
+
import { redisServiceTimes } from "./redis/service-times.js";
|
|
2
3
|
import { readHostPort } from "./host-port.js";
|
|
3
4
|
// A Protocol 3 pack's whole HTTP surface, served by the kernel from the pack's declarations (docs/contributing/
|
|
4
5
|
// architecture.md, "The pack contract: the kernel's root"): the discovery door (`GET /twin`, the manifest's
|
|
@@ -160,7 +161,7 @@ export function createPackFetch(parts, options = {}) {
|
|
|
160
161
|
// the GraphQL wire over the same state, at the paths the manifest names (graphql-wire.ts)
|
|
161
162
|
const gql = m.graphql && parts.graphql ? m.graphql : undefined;
|
|
162
163
|
const graphql = gql && parts.graphql ? graphqlEndpoint({
|
|
163
|
-
sdl: parts.graphql.sdl, part: parts.graphql,
|
|
164
|
+
sdl: parts.graphql.sdl, part: parts.graphql, loadLibrary: parts.graphql.loadLibrary,
|
|
164
165
|
context: async (request) => contextFor(m, { request: request.clone(), operation: { id: 'graphql', method: 'POST', path: gql.paths[0], class: 'action' }, params: {} }, scope),
|
|
165
166
|
unmodeled: ({ type, field }) => Response.json({ errors: [{ ...(gql.unmodeled.type ? { type: gql.unmodeled.type } : {}), message: gql.unmodeled.message.replace('{field}', field).replace('{type}', type) }] }, { status: gql.unmodeled.status ?? 200 }),
|
|
166
167
|
mutationRefused: (request) => ((options.readOnly ?? false) || isReadOnlyRequest(request) ? vendorError(m, m.readOnly) : undefined),
|
|
@@ -393,6 +394,7 @@ export function createPackFetch(parts, options = {}) {
|
|
|
393
394
|
return readable instanceof Response ? readable : readable ? atContentHost(readable) : undecodable(arrived);
|
|
394
395
|
};
|
|
395
396
|
const fetch = async (arrived) => {
|
|
397
|
+
redisServiceTimes.lap('1 request made by the stream');
|
|
396
398
|
// the discovery door is the pack's own, whichever lane would take its path
|
|
397
399
|
const asked = new URL(arrived.url).pathname.replace(/\/+$/, '') || '/';
|
|
398
400
|
const discovery = arrived.method === 'GET' && m.discovery !== undefined && (asked === '/twin' || asked === '/twin/scenario');
|
|
@@ -427,6 +429,7 @@ export function createPackFetch(parts, options = {}) {
|
|
|
427
429
|
const url = new URL(request.url);
|
|
428
430
|
const path = url.pathname.replace(/\/+$/, '') || '/';
|
|
429
431
|
const readOnly = (options.readOnly ?? false) || isReadOnlyRequest(request);
|
|
432
|
+
redisServiceTimes.lap('2 content and lane routes, decoding, routing');
|
|
430
433
|
if (request.method === 'GET' && path === '/twin' && m.discovery) {
|
|
431
434
|
const d = m.discovery;
|
|
432
435
|
const discoveryDoors = [...(m.doors ?? []), ...(m.machines ? [{ id: 'machine-pool', method: 'POST', path: '/_twin/machine-pool', note: 'Enroll the World execution pool: { kind: light | docker | none | fake, images?: { image: { context, command?, runtime? } } }.' }] : [])];
|
|
@@ -549,12 +552,19 @@ export function createPackFetch(parts, options = {}) {
|
|
|
549
552
|
return encoded(m, request, await decided(request));
|
|
550
553
|
// The front reads a context before forwarding (possibly with new headers). Materialize a transport body before
|
|
551
554
|
// those independent reads, rather than chaining tees of the incoming stream through the front and the scenario.
|
|
555
|
+
redisServiceTimes.lap('3 doors, screens and machine routes passed');
|
|
552
556
|
const bytes = request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.arrayBuffer();
|
|
553
557
|
const buffered = new Request(request.url, { method: request.method, headers: request.headers, signal: request.signal, ...(bytes ? { body: bytes } : {}) });
|
|
554
558
|
const matched = matchOperation(routes, buffered.method, url.pathname, url.searchParams, buffered.headers);
|
|
555
559
|
const operation = matched?.operation ?? { id: 'unmatched', method: buffered.method, path: url.pathname, class: buffered.method === 'GET' ? 'retrieve' : 'action' };
|
|
560
|
+
redisServiceTimes.lap('4 body buffered, operation matched');
|
|
556
561
|
const ctx = await contextFor(m, { request: buffered.clone(), operation, params: matched?.params ?? pathParams(routes, url.pathname) }, scope);
|
|
557
|
-
|
|
562
|
+
redisServiceTimes.lap('5 the around context opened');
|
|
563
|
+
const answered = await parts.around(ctx, (rewritten) => decided(rewritten ?? buffered));
|
|
564
|
+
redisServiceTimes.lap('6d the handler, and the around after it');
|
|
565
|
+
const sent = await encoded(m, buffered, answered);
|
|
566
|
+
redisServiceTimes.lap('7 answer encoded');
|
|
567
|
+
return sent;
|
|
558
568
|
};
|
|
559
569
|
// the request's id is the correlation of every entry it appends (runtime contract D3), minted here, always: it is the
|
|
560
570
|
// group a perform sends as one call (the real-system adapters, "Perform"). Under `auto` the group is performed once
|
|
@@ -739,9 +749,17 @@ export function createVendorFetch(manifest, lanes) {
|
|
|
739
749
|
const ingest = Object.values(lanes).find((l) => l.fetch.ingest)?.fetch.ingest;
|
|
740
750
|
const lane = laneRouter(manifest.vendor, manifest.lanes, lanes);
|
|
741
751
|
return Object.assign(async (request) => {
|
|
752
|
+
const path = new URL(request.url).pathname.replace(/\/+$/, '') || '/';
|
|
753
|
+
// the discovery door is answered before any lane's clock moves, as a pack's own is (createPackFetch): reading what
|
|
754
|
+
// the twin is moves no vendor clock
|
|
755
|
+
if (request.method === 'GET' && path === '/twin' && manifest.discovery) {
|
|
756
|
+
// the vendor's screens are its lanes' (a dashboard drawn by one lane is the vendor's)
|
|
757
|
+
const screens = lanesWorkspaces(lanes);
|
|
758
|
+
const laneDiscovery = await laneDoors(lanes, request, '/twin');
|
|
759
|
+
return Response.json({ ...statefulTwinManifest({ vendor: manifest.vendor, ...manifest.discovery }), ...(screens.length ? { screens } : {}), ...(Object.keys(laneDiscovery).length ? { lanes: laneDiscovery } : {}) });
|
|
760
|
+
}
|
|
742
761
|
for (const part of Object.values(lanes))
|
|
743
762
|
await part.fetch.advanceClock?.(request);
|
|
744
|
-
const path = new URL(request.url).pathname.replace(/\/+$/, '') || '/';
|
|
745
763
|
// the vendor's board is the frames of the lanes that lay one out, each drawn when any lane's screen takes its url
|
|
746
764
|
if (request.method === 'GET' && path === '/twin/board') {
|
|
747
765
|
const frames = [];
|
|
@@ -758,12 +776,6 @@ export function createVendorFetch(manifest, lanes) {
|
|
|
758
776
|
const draws = (url) => Object.values(lanes).some((l) => l.fetch.draws?.(url) ?? false);
|
|
759
777
|
return Response.json({ frames: frames.map((f) => ({ ...f, drawn: draws(f.url) })) });
|
|
760
778
|
}
|
|
761
|
-
if (request.method === 'GET' && path === '/twin' && manifest.discovery) {
|
|
762
|
-
// the vendor's screens are its lanes' (a dashboard drawn by one lane is the vendor's)
|
|
763
|
-
const screens = lanesWorkspaces(lanes);
|
|
764
|
-
const laneDiscovery = await laneDoors(lanes, request, '/twin');
|
|
765
|
-
return Response.json({ ...statefulTwinManifest({ vendor: manifest.vendor, ...manifest.discovery }), ...(screens.length ? { screens } : {}), ...(Object.keys(laneDiscovery).length ? { lanes: laneDiscovery } : {}) });
|
|
766
|
-
}
|
|
767
779
|
if (request.method === 'GET' && path === '/twin/scenario') {
|
|
768
780
|
const laneScenarios = await laneDoors(lanes, request, '/twin/scenario');
|
|
769
781
|
if (Object.keys(laneScenarios).length)
|
|
@@ -5,6 +5,7 @@ import type { DerivedManifest } from './derived-core.js';
|
|
|
5
5
|
import type { DerivedSurface } from './derived.js';
|
|
6
6
|
import type { TwinEmitter } from './emit.js';
|
|
7
7
|
import { type StateSystemAdapters } from './state-system.js';
|
|
8
|
+
export { PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding } from './protocol.js';
|
|
8
9
|
/**
|
|
9
10
|
* How a pack's clients ADDRESS it. The first three are HTTP API styles; `raw-tcp` is the
|
|
10
11
|
* RAW-PROTOCOL class — a pack whose clients speak their protocol directly over a TCP socket
|
|
@@ -37,19 +38,6 @@ export type PackTransport = 'rest' | 'graphql' | 'web-api' | 'raw-tcp';
|
|
|
37
38
|
* seam (fly's runtime, github's git plane, supabase's external stack).
|
|
38
39
|
* 'proxy' — the vendor itself is a forwarding plane (tunnel).
|
|
39
40
|
*/
|
|
40
|
-
/** THE PLATFORM PROTOCOL VERSION (runtime contract R16): `major.minor`. The major names the
|
|
41
|
-
* descriptor shape, the factory signature, the doors and the adapter contract a package is
|
|
42
|
-
* written against, and the build form it is made in (the derived pack); a change that removes or
|
|
43
|
-
* alters a required shape bumps it, an additive change bumps minor. A package declares the major
|
|
44
|
-
* it targets (`protocol`), and the kernel serves its own major and no other: a package that
|
|
45
|
-
* declares another, or none, is refused. */
|
|
46
|
-
export declare const PROTOCOL_VERSION = "3.0";
|
|
47
|
-
export declare const PROTOCOL_MAJOR: number;
|
|
48
|
-
/** Where a package's declared protocol stands against this kernel: `current`, or `refused`. */
|
|
49
|
-
export declare function protocolStanding(declared: string | undefined): {
|
|
50
|
-
major: number;
|
|
51
|
-
standing: 'current' | 'refused';
|
|
52
|
-
};
|
|
53
41
|
export declare const PACK_ARCHETYPES: readonly ["crud", "generative", "signed-protocol", "engine-control", "proxy"];
|
|
54
42
|
export type PackArchetype = (typeof PACK_ARCHETYPES)[number];
|
|
55
43
|
/**
|
|
@@ -223,9 +211,17 @@ export type TwinPack = {
|
|
|
223
211
|
urlEnv: string;
|
|
224
212
|
};
|
|
225
213
|
managedService?: {
|
|
226
|
-
kind:
|
|
214
|
+
kind: string;
|
|
227
215
|
scheme: string;
|
|
228
216
|
note: string;
|
|
217
|
+
configurationArg?: '--configuration';
|
|
218
|
+
originalInputs?: {
|
|
219
|
+
version: 1;
|
|
220
|
+
manifestArg: '--original-inputs';
|
|
221
|
+
rootArg: '--original-root';
|
|
222
|
+
digestArg: '--original-inputs-sha256';
|
|
223
|
+
paths: string[];
|
|
224
|
+
};
|
|
229
225
|
};
|
|
230
226
|
managedDatabase?: {
|
|
231
227
|
kind: 'postgres';
|
package/dist/src/packRegistry.js
CHANGED
|
@@ -11,6 +11,8 @@ import { declareRateBudget } from "./rateBudget.js";
|
|
|
11
11
|
import { referenceField, registerReferences } from "./references.js";
|
|
12
12
|
import { deriveStateSystem, deriveVendorStateSystem } from "./derived-real.js";
|
|
13
13
|
import { registerAuthStrategy, registerStateSystem } from "./state-system.js";
|
|
14
|
+
import { PROTOCOL_VERSION, protocolStanding } from "./protocol.js";
|
|
15
|
+
export { PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding } from "./protocol.js";
|
|
14
16
|
/**
|
|
15
17
|
* The pack's SERVE-FAMILY — the axis the invariant matrix keys strictness off, DECLARED
|
|
16
18
|
* because it is the pack's own claim about what kind of thing it is (a heuristic over file
|
|
@@ -26,19 +28,6 @@ import { registerAuthStrategy, registerStateSystem } from "./state-system.js";
|
|
|
26
28
|
* seam (fly's runtime, github's git plane, supabase's external stack).
|
|
27
29
|
* 'proxy' — the vendor itself is a forwarding plane (tunnel).
|
|
28
30
|
*/
|
|
29
|
-
/** THE PLATFORM PROTOCOL VERSION (runtime contract R16): `major.minor`. The major names the
|
|
30
|
-
* descriptor shape, the factory signature, the doors and the adapter contract a package is
|
|
31
|
-
* written against, and the build form it is made in (the derived pack); a change that removes or
|
|
32
|
-
* alters a required shape bumps it, an additive change bumps minor. A package declares the major
|
|
33
|
-
* it targets (`protocol`), and the kernel serves its own major and no other: a package that
|
|
34
|
-
* declares another, or none, is refused. */
|
|
35
|
-
export const PROTOCOL_VERSION = '3.0';
|
|
36
|
-
export const PROTOCOL_MAJOR = Number(PROTOCOL_VERSION.split('.')[0]);
|
|
37
|
-
/** Where a package's declared protocol stands against this kernel: `current`, or `refused`. */
|
|
38
|
-
export function protocolStanding(declared) {
|
|
39
|
-
const major = declared === undefined ? Number.NaN : Number(String(declared).split('.')[0]);
|
|
40
|
-
return major === PROTOCOL_MAJOR ? { major, standing: 'current' } : { major, standing: 'refused' };
|
|
41
|
-
}
|
|
42
31
|
export const PACK_ARCHETYPES = ['crud', 'generative', 'signed-protocol', 'engine-control', 'proxy'];
|
|
43
32
|
/** The posture to assume for a pack that doesn't declare one. Conservative on purpose. */
|
|
44
33
|
export const DEFAULT_PULL_POSTURE = 'on-demand';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** THE PLATFORM PROTOCOL VERSION (runtime contract R16): `major.minor`. The major names the
|
|
2
|
+
* descriptor shape, the factory signature, the doors and the adapter contract a package is
|
|
3
|
+
* written against, and the build form it is made in (the derived pack); a change that removes or
|
|
4
|
+
* alters a required shape bumps it, an additive change bumps minor. A package declares the major
|
|
5
|
+
* it targets (`protocol`), and the kernel serves its own major and no other: a package that
|
|
6
|
+
* declares another, or none, is refused. */
|
|
7
|
+
export declare const PROTOCOL_VERSION = "3.0";
|
|
8
|
+
export declare const PROTOCOL_MAJOR: number;
|
|
9
|
+
/** Where a package's declared protocol stands against this kernel: `current`, or `refused`. */
|
|
10
|
+
export declare function protocolStanding(declared: string | undefined): {
|
|
11
|
+
major: number;
|
|
12
|
+
standing: 'current' | 'refused';
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** THE PLATFORM PROTOCOL VERSION (runtime contract R16): `major.minor`. The major names the
|
|
2
|
+
* descriptor shape, the factory signature, the doors and the adapter contract a package is
|
|
3
|
+
* written against, and the build form it is made in (the derived pack); a change that removes or
|
|
4
|
+
* alters a required shape bumps it, an additive change bumps minor. A package declares the major
|
|
5
|
+
* it targets (`protocol`), and the kernel serves its own major and no other: a package that
|
|
6
|
+
* declares another, or none, is refused. */
|
|
7
|
+
export const PROTOCOL_VERSION = '3.0';
|
|
8
|
+
export const PROTOCOL_MAJOR = Number(PROTOCOL_VERSION.split('.')[0]);
|
|
9
|
+
/** Where a package's declared protocol stands against this kernel: `current`, or `refused`. */
|
|
10
|
+
export function protocolStanding(declared) {
|
|
11
|
+
const major = declared === undefined ? Number.NaN : Number(String(declared).split('.')[0]);
|
|
12
|
+
return major === PROTOCOL_MAJOR ? { major, standing: 'current' } : { major, standing: 'refused' };
|
|
13
|
+
}
|
|
@@ -45,9 +45,9 @@ type StoredZSet = Array<[string, string]>;
|
|
|
45
45
|
export type StreamEntries = Array<[string, HashPairs]>;
|
|
46
46
|
/** A point-in-time copy of a run's whole mutable state, for script rollback. */
|
|
47
47
|
type Snapshot = {
|
|
48
|
-
rows:
|
|
48
|
+
rows: OverlayState<KeyRow>;
|
|
49
49
|
touched: Map<string, string>;
|
|
50
|
-
scripts:
|
|
50
|
+
scripts: OverlayState<string>;
|
|
51
51
|
touchedScripts: Map<string, string>;
|
|
52
52
|
};
|
|
53
53
|
export type KeyRow = {
|
|
@@ -168,12 +168,17 @@ export type KeyspaceImage = {
|
|
|
168
168
|
scripts: Map<string, string>;
|
|
169
169
|
revs: Map<string, number>;
|
|
170
170
|
};
|
|
171
|
+
declare const GONE: unique symbol;
|
|
172
|
+
type OverlayState<V> = {
|
|
173
|
+
own: Map<string, V | typeof GONE>;
|
|
174
|
+
moved: Set<string>;
|
|
175
|
+
};
|
|
171
176
|
/**
|
|
172
177
|
* A synchronous, in-memory image of the keyspace for the duration of ONE request.
|
|
173
178
|
*
|
|
174
179
|
* Seeded from the keyspace's image (the tree folded by `projectResources`, memoized per World state — the kernel IS
|
|
175
180
|
* the source of truth); every mutation records the key name in `touched`, and the flush writes exactly those keys
|
|
176
|
-
* back. This run
|
|
181
|
+
* back. This run owns its overlays: nothing a run changes reaches the image or a later request except
|
|
177
182
|
* through the tree.
|
|
178
183
|
*/
|
|
179
184
|
export declare class KeySpace {
|
|
@@ -185,7 +190,7 @@ export declare class KeySpace {
|
|
|
185
190
|
private readonly touchedScripts;
|
|
186
191
|
/** name → the last write ordinal seen, INCLUDING for keys currently deleted or expired. */
|
|
187
192
|
private readonly revs;
|
|
188
|
-
/** The
|
|
193
|
+
/** The shared base maps are patched by the next image(); a KeySpace must finish reading its base before it yields. */
|
|
189
194
|
readonly image: KeyspaceImage;
|
|
190
195
|
constructor(ctx: RedisContext, nowMs: number);
|
|
191
196
|
/**
|
|
@@ -323,5 +328,6 @@ export declare function execRedisRun(commands: string[][], ctx: RedisContext): P
|
|
|
323
328
|
items: RunItem[];
|
|
324
329
|
wrote: boolean;
|
|
325
330
|
instant: number;
|
|
331
|
+
touched: string[];
|
|
326
332
|
}>;
|
|
327
333
|
export {};
|
package/dist/src/redis/engine.js
CHANGED
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
import { createHash } from 'node:crypto';
|
|
47
47
|
import { projectResources } from "../actions.js";
|
|
48
48
|
import { treeChangesSince, treeStamp } from "../log.js";
|
|
49
|
-
import {
|
|
49
|
+
import { redisServiceTimes as times } from "./service-times.js";
|
|
50
|
+
import { applyTwinWrites } from "../serve.js";
|
|
50
51
|
import { getActiveWorldStore } from "../world-store.js";
|
|
51
52
|
import { LUA_NULL, luaScriptSha1, LuaError, LuaTable, runLua } from "./lua.js";
|
|
52
53
|
import { inlineCommand } from "./frames.js";
|
|
@@ -311,12 +312,68 @@ function patchImage(image, changed, removed, scope) {
|
|
|
311
312
|
}
|
|
312
313
|
return true;
|
|
313
314
|
}
|
|
315
|
+
const GONE = Symbol('gone');
|
|
316
|
+
/**
|
|
317
|
+
* A run's map over the shared image: what the run changed is its own, the rest is read through. Copying the whole image
|
|
318
|
+
* per run (and again per script, for its rollback) cost every command the size of the keyspace (S2 R19: Twenty's queue
|
|
319
|
+
* twin, its queue scripts waiting behind each other's copies). Iteration is a `new Map(base)` changed in place's: the
|
|
320
|
+
* base's order with its changed values, then the keys added, a base key deleted and set again among them at its set.
|
|
321
|
+
*/
|
|
322
|
+
class Overlay {
|
|
323
|
+
base;
|
|
324
|
+
own = new Map();
|
|
325
|
+
moved = new Set();
|
|
326
|
+
constructor(base) {
|
|
327
|
+
this.base = base;
|
|
328
|
+
}
|
|
329
|
+
get(key) {
|
|
330
|
+
const mine = this.own.get(key);
|
|
331
|
+
return mine === GONE ? undefined : mine !== undefined || this.own.has(key) ? mine : this.base.get(key);
|
|
332
|
+
}
|
|
333
|
+
has(key) { return this.own.has(key) ? this.own.get(key) !== GONE : this.base.has(key); }
|
|
334
|
+
set(key, value) {
|
|
335
|
+
if (this.own.get(key) === GONE) {
|
|
336
|
+
this.own.delete(key);
|
|
337
|
+
this.moved.add(key);
|
|
338
|
+
}
|
|
339
|
+
this.own.set(key, value);
|
|
340
|
+
}
|
|
341
|
+
delete(key) {
|
|
342
|
+
this.own.delete(key);
|
|
343
|
+
this.moved.delete(key);
|
|
344
|
+
if (this.base.has(key))
|
|
345
|
+
this.own.set(key, GONE);
|
|
346
|
+
}
|
|
347
|
+
*entries() {
|
|
348
|
+
for (const [key, value] of this.base) {
|
|
349
|
+
if (this.moved.has(key))
|
|
350
|
+
continue;
|
|
351
|
+
if (!this.own.has(key)) {
|
|
352
|
+
yield [key, value];
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const mine = this.own.get(key);
|
|
356
|
+
if (mine !== GONE)
|
|
357
|
+
yield [key, mine];
|
|
358
|
+
}
|
|
359
|
+
for (const [key, mine] of this.own)
|
|
360
|
+
if (mine !== GONE && (!this.base.has(key) || this.moved.has(key)))
|
|
361
|
+
yield [key, mine];
|
|
362
|
+
}
|
|
363
|
+
*keys() { for (const [key] of this.entries())
|
|
364
|
+
yield key; }
|
|
365
|
+
*values() { for (const [, value] of this.entries())
|
|
366
|
+
yield value; }
|
|
367
|
+
/** What the run changed, copied: a rollback point the size of the changes, not of the keyspace. */
|
|
368
|
+
save() { return { own: new Map(this.own), moved: new Set(this.moved) }; }
|
|
369
|
+
load(state) { this.own = new Map(state.own); this.moved = new Set(state.moved); }
|
|
370
|
+
}
|
|
314
371
|
/**
|
|
315
372
|
* A synchronous, in-memory image of the keyspace for the duration of ONE request.
|
|
316
373
|
*
|
|
317
374
|
* Seeded from the keyspace's image (the tree folded by `projectResources`, memoized per World state — the kernel IS
|
|
318
375
|
* the source of truth); every mutation records the key name in `touched`, and the flush writes exactly those keys
|
|
319
|
-
* back. This run
|
|
376
|
+
* back. This run owns its overlays: nothing a run changes reaches the image or a later request except
|
|
320
377
|
* through the tree.
|
|
321
378
|
*/
|
|
322
379
|
export class KeySpace {
|
|
@@ -328,16 +385,16 @@ export class KeySpace {
|
|
|
328
385
|
touchedScripts = new Map();
|
|
329
386
|
/** name → the last write ordinal seen, INCLUDING for keys currently deleted or expired. */
|
|
330
387
|
revs;
|
|
331
|
-
/** The
|
|
388
|
+
/** The shared base maps are patched by the next image(); a KeySpace must finish reading its base before it yields. */
|
|
332
389
|
image;
|
|
333
390
|
constructor(ctx, nowMs) {
|
|
334
391
|
this.ctx = ctx;
|
|
335
392
|
this.nowMs = nowMs;
|
|
336
|
-
//
|
|
393
|
+
// The base maps are patched by the next image(); finish reading this run's base before yielding.
|
|
337
394
|
const image = this.image = ctx.dialect.storage?.image(ctx) ?? keyspaceImageOf(ctx);
|
|
338
|
-
this.rows = new
|
|
339
|
-
this.scripts = new
|
|
340
|
-
this.revs = new
|
|
395
|
+
this.rows = new Overlay(image.rows);
|
|
396
|
+
this.scripts = new Overlay(image.scripts);
|
|
397
|
+
this.revs = new Overlay(image.revs);
|
|
341
398
|
}
|
|
342
399
|
/**
|
|
343
400
|
* Is this row expired AS OF NOW? Checked on every read, not merely when the snapshot was built.
|
|
@@ -394,18 +451,14 @@ export class KeySpace {
|
|
|
394
451
|
}
|
|
395
452
|
/** A point-in-time copy, so a failed script can be rolled back to exactly where it started. */
|
|
396
453
|
snapshot() {
|
|
397
|
-
return { rows:
|
|
454
|
+
return { rows: this.rows.save(), touched: new Map(this.touched), scripts: this.scripts.save(), touchedScripts: new Map(this.touchedScripts) };
|
|
398
455
|
}
|
|
399
456
|
restore(s) {
|
|
400
|
-
this.rows.
|
|
401
|
-
for (const [k, v] of s.rows)
|
|
402
|
-
this.rows.set(k, v);
|
|
457
|
+
this.rows.load(s.rows);
|
|
403
458
|
this.touched.clear();
|
|
404
459
|
for (const [k, v] of s.touched)
|
|
405
460
|
this.touched.set(k, v);
|
|
406
|
-
this.scripts.
|
|
407
|
-
for (const [k, v] of s.scripts)
|
|
408
|
-
this.scripts.set(k, v);
|
|
461
|
+
this.scripts.load(s.scripts);
|
|
409
462
|
this.touchedScripts.clear();
|
|
410
463
|
for (const [k, v] of s.touchedScripts)
|
|
411
464
|
this.touchedScripts.set(k, v);
|
|
@@ -447,8 +500,10 @@ export async function restoreKeyspace(image, ctx) {
|
|
|
447
500
|
await flushKeySpace(space);
|
|
448
501
|
}
|
|
449
502
|
async function flushKeySpace(space) {
|
|
503
|
+
// a run's writes land together, in this order, as they landed one at a time (serve.ts applyTwinWrites)
|
|
504
|
+
const writes = [];
|
|
450
505
|
for (const { name, operation, row } of space.pendingWrites()) {
|
|
451
|
-
|
|
506
|
+
writes.push({
|
|
452
507
|
operation,
|
|
453
508
|
subjectType: 'key',
|
|
454
509
|
subjectId: `${scopeOf(space.ctx)}key:${name}`,
|
|
@@ -460,18 +515,19 @@ async function flushKeySpace(space) {
|
|
|
460
515
|
: { name, kind: row.kind, v: row.v, pexpire_at: row.pexpireAt, last_id: row.lastId ?? '', gone: false, _rev: space.nextRev(name) },
|
|
461
516
|
occurredAt: space.ctx.occurredAt,
|
|
462
517
|
actor: { kind: 'agent' },
|
|
463
|
-
}
|
|
518
|
+
});
|
|
464
519
|
}
|
|
465
520
|
for (const { sha, operation, body } of space.pendingScriptWrites()) {
|
|
466
|
-
|
|
521
|
+
writes.push({
|
|
467
522
|
operation,
|
|
468
523
|
subjectType: 'script',
|
|
469
524
|
subjectId: `${scopeOf(space.ctx)}script:${sha}`,
|
|
470
525
|
fields: body === null ? { sha, body: '', gone: true } : { sha, body, gone: false },
|
|
471
526
|
occurredAt: space.ctx.occurredAt,
|
|
472
527
|
actor: { kind: 'agent' },
|
|
473
|
-
}
|
|
528
|
+
});
|
|
474
529
|
}
|
|
530
|
+
await applyTwinWrites(space.ctx.service, writes, space.ctx.root);
|
|
475
531
|
}
|
|
476
532
|
// ── typed value accessors (each asserts the key's type first) ─────────────────────────────────
|
|
477
533
|
export function expectType(row, kind) {
|
|
@@ -1905,23 +1961,38 @@ export async function execRedisRun(commands, ctx) {
|
|
|
1905
1961
|
const nowMs = Date.parse(ctx.occurredAt);
|
|
1906
1962
|
if (!Number.isFinite(nowMs))
|
|
1907
1963
|
throw new RedisCommandError(`ERR twin: unparseable occurredAt '${ctx.occurredAt}'`);
|
|
1908
|
-
|
|
1964
|
+
// The phases a served command's time is counted in (service-times.ts): the image, the commands, their writes.
|
|
1965
|
+
const space = times.timed('image', () => new KeySpace(ctx, nowMs));
|
|
1909
1966
|
const items = [];
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1967
|
+
const began = times.now();
|
|
1968
|
+
try {
|
|
1969
|
+
for (const argv of commands) {
|
|
1970
|
+
try {
|
|
1971
|
+
items.push({ result: execOne(space, argv) });
|
|
1972
|
+
}
|
|
1973
|
+
catch (e) {
|
|
1974
|
+
if (e instanceof ReadOnlyError)
|
|
1975
|
+
throw e; // surfaces as HTTP 405 for the whole request
|
|
1976
|
+
if (e instanceof RedisCommandError) {
|
|
1977
|
+
items.push({ error: e.message });
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
throw e;
|
|
1920
1981
|
}
|
|
1921
|
-
throw e;
|
|
1922
1982
|
}
|
|
1923
1983
|
}
|
|
1984
|
+
finally {
|
|
1985
|
+
times.add('execute', times.now() - began);
|
|
1986
|
+
}
|
|
1924
1987
|
const wrote = space.dirty;
|
|
1925
|
-
|
|
1926
|
-
|
|
1988
|
+
// The keys this run wrote, by name, read before the flush clears them: a stream wakes only the clients blocked on one.
|
|
1989
|
+
const touched = space.touchedNames();
|
|
1990
|
+
const flushing = times.now();
|
|
1991
|
+
try {
|
|
1992
|
+
await (space.ctx.dialect.storage?.flush(space) ?? flushKeySpace(space));
|
|
1993
|
+
}
|
|
1994
|
+
finally {
|
|
1995
|
+
times.add('flush', times.now() - flushing);
|
|
1996
|
+
}
|
|
1997
|
+
return { items, wrote, instant: nowMs, touched };
|
|
1927
1998
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// Redis's collection layout over the World tree. First need: the managed Redis queue's growing events stream.
|
|
2
2
|
// Key metadata and each hash field/set member/sorted-set member/stream entry are separate subjects.
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import {
|
|
4
|
+
import { applyTwinWrites } from "../serve.js";
|
|
5
5
|
import { projectResources } from "../actions.js";
|
|
6
6
|
import { treeStamp, treeChangesSince } from "../log.js";
|
|
7
7
|
import { getActiveWorldStore } from "../world-store.js";
|
|
8
8
|
const scope = (c) => c.database === undefined ? '' : `db:${c.database}:`;
|
|
9
9
|
const collections = new Set(['hash', 'set', 'zset', 'stream', 'list']);
|
|
10
|
+
// `byKey` holds each key's member subjects in `subjects`' own order, so rebuilding one key reads its members, not every
|
|
11
|
+
// subject of the service (S2 R19: the image phase was up to 560 ms of a 2 s window of Twenty's queue twin).
|
|
10
12
|
const held = new WeakMap();
|
|
11
13
|
const members = (r) => !r || !collections.has(r.kind) ? new Map() : new Map(r.v.map((x, i) => r.kind === 'list' ? [String(i), x] : r.kind === 'set' ? [x, x] : [x[0], x[1]]));
|
|
12
14
|
const memberId = (c, name, field) => `${scope(c)}member:${createHash('sha256').update(JSON.stringify([name, field])).digest('hex')}`;
|
|
@@ -21,9 +23,16 @@ export const memberStorage = {
|
|
|
21
23
|
return h.image;
|
|
22
24
|
const delta = h && treeChangesSince(ctx.service, ctx.root, h.stamp);
|
|
23
25
|
if (!h || !delta || delta.removed.length)
|
|
24
|
-
h = { stamp, subjects: new Map(), image: { rows: new Map(), scripts: new Map(), revs: new Map() } };
|
|
25
|
-
for (const r of delta && !delta.removed.length ? delta.changed : projectResources(ctx.service, ctx.root))
|
|
26
|
+
h = { stamp, subjects: new Map(), byKey: new Map(), image: { rows: new Map(), scripts: new Map(), revs: new Map() } };
|
|
27
|
+
for (const r of delta && !delta.removed.length ? delta.changed : projectResources(ctx.service, ctx.root)) {
|
|
26
28
|
h.subjects.set(r.id, r);
|
|
29
|
+
if (r.type === 'member') {
|
|
30
|
+
let of = h.byKey.get(r.key);
|
|
31
|
+
if (!of)
|
|
32
|
+
h.byKey.set(r.key, of = new Map());
|
|
33
|
+
of.set(r.id, r);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
27
36
|
// Reconstruct only keys affected by the tree delta, not every member on every read.
|
|
28
37
|
const affected = new Set();
|
|
29
38
|
const changed = delta && !delta.removed.length ? delta.changed : [...h.subjects.values()];
|
|
@@ -50,7 +59,7 @@ export const memberStorage = {
|
|
|
50
59
|
h.image.rows.delete(name);
|
|
51
60
|
continue;
|
|
52
61
|
}
|
|
53
|
-
const v = collections.has(meta.kind) ? [...h.
|
|
62
|
+
const v = collections.has(meta.kind) ? [...(h.byKey.get(name)?.values() ?? [])].filter((r) => r.id.startsWith(scope(ctx)) && r.gone !== true).sort((a, b) => meta.kind === 'list' ? Number(a.field) - Number(b.field) : 0).map((r) => meta.kind === 'list' ? r.value : meta.kind === 'set' ? r.field : [r.field, r.value]) : meta.v;
|
|
54
63
|
h.image.rows.set(name, { name, kind: meta.kind, v, pexpireAt: meta.pexpire_at == null ? null : String(meta.pexpire_at), lastId: meta.last_id, _rev: meta._rev });
|
|
55
64
|
}
|
|
56
65
|
h.stamp = stamp;
|
|
@@ -59,17 +68,20 @@ export const memberStorage = {
|
|
|
59
68
|
},
|
|
60
69
|
async flush(space) {
|
|
61
70
|
const ctx = space.ctx;
|
|
62
|
-
|
|
71
|
+
// a run's writes land together, in this order, as they landed one at a time (serve.ts applyTwinWrites)
|
|
72
|
+
const writes = [];
|
|
73
|
+
const write = (type, id, operation, fields) => { writes.push({ operation, subjectType: type, subjectId: id, fields, occurredAt: ctx.occurredAt, actor: { kind: 'agent' } }); };
|
|
63
74
|
for (const { name, row, operation } of space.pendingWrites()) {
|
|
64
75
|
const before = members(space.image.rows.get(name) ?? null), after = members(row);
|
|
65
76
|
for (const field of new Set([...before.keys(), ...after.keys()])) {
|
|
66
77
|
if (before.has(field) && after.has(field) && JSON.stringify(before.get(field)) === JSON.stringify(after.get(field)))
|
|
67
78
|
continue;
|
|
68
|
-
|
|
79
|
+
write('member', memberId(ctx, name, field), operation, { key: name, field, value: after.get(field) ?? null, gone: !after.has(field) });
|
|
69
80
|
}
|
|
70
|
-
|
|
81
|
+
write('key', `${scope(ctx)}key:${name}`, operation, { name, kind: row?.kind ?? 'string', v: row && !collections.has(row.kind) ? row.v : null, pexpire_at: row?.pexpireAt ?? null, last_id: row?.lastId ?? '', gone: row === null, _rev: space.nextRev(name) });
|
|
71
82
|
}
|
|
72
83
|
for (const { sha, body, operation } of space.pendingScriptWrites())
|
|
73
|
-
|
|
84
|
+
write('script', `${scope(ctx)}script:${sha}`, operation, { sha, body: body ?? '', gone: body === null });
|
|
85
|
+
await applyTwinWrites(ctx.service, writes, ctx.root);
|
|
74
86
|
},
|
|
75
87
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type Phase = 'image' | 'execute' | 'flush' | 'reply' | 'run';
|
|
2
|
+
type Command = {
|
|
3
|
+
session: number;
|
|
4
|
+
command: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const redisServiceTimes: {
|
|
7
|
+
now: () => number;
|
|
8
|
+
/** A command starts: `received` is when its bytes were read, `waiting` the commands read and not yet started. */
|
|
9
|
+
begin(received: number, waiting: number, command: Command): void;
|
|
10
|
+
/** Time of the open command in one phase. */
|
|
11
|
+
add(phase: Phase, ms: number): void;
|
|
12
|
+
wrote(): void;
|
|
13
|
+
/** A run begins: its first stretch is counted from here. */
|
|
14
|
+
lapFrom(): void;
|
|
15
|
+
/** The stretch of the open command's run that ends here, by name. Outside a served command it is nothing. */
|
|
16
|
+
lap(name: string): void;
|
|
17
|
+
/** A run has ended: nothing after it is a stretch of it. */
|
|
18
|
+
lapEnd(name: string): void;
|
|
19
|
+
/** The open command has answered (or blocked, or failed with its error answered). */
|
|
20
|
+
end(threw?: boolean): void;
|
|
21
|
+
/** `work`, its time counted in `phase` of the open command. */
|
|
22
|
+
timed<T>(phase: Phase, work: () => T): T;
|
|
23
|
+
phases: readonly Phase[];
|
|
24
|
+
};
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const PHASES = ['image', 'execute', 'flush', 'reply', 'run'];
|
|
2
|
+
const SAY_EVERY_MS = 2000;
|
|
3
|
+
const now = () => performance.now();
|
|
4
|
+
const fresh = () => ({ service: [], waited: 0, waitedMax: 0, own: 0, phases: { image: 0, execute: 0, flush: 0, reply: 0, run: 0 }, runs: 0, queueMax: 0, writes: 0, laps: {} });
|
|
5
|
+
// THE PARTS OF ONE RUN, in the order it passes them: a run is the pack's own request to itself, carried through the
|
|
6
|
+
// whole of a pack's fetch (pack-fetch.ts) to the command and back. Each `lap` names the stretch that ended there,
|
|
7
|
+
// from the lap before it; the stretches of a window's runs are summed by name as `runPartsMs`. A stretch holds
|
|
8
|
+
// whatever else this realm did between its two ends: under traffic that is other connections' bytes arriving.
|
|
9
|
+
let lapAt = 0;
|
|
10
|
+
let window = fresh();
|
|
11
|
+
let open;
|
|
12
|
+
let timer;
|
|
13
|
+
let served = 0;
|
|
14
|
+
function say() {
|
|
15
|
+
timer = undefined;
|
|
16
|
+
const w = window;
|
|
17
|
+
window = fresh();
|
|
18
|
+
if (w.service.length === 0)
|
|
19
|
+
return;
|
|
20
|
+
const sorted = [...w.service].sort((a, b) => a - b);
|
|
21
|
+
const at = (share) => sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * share))];
|
|
22
|
+
const ms = (value) => Math.round(value * 10) / 10;
|
|
23
|
+
const inRun = w.phases.image + w.phases.execute + w.phases.flush;
|
|
24
|
+
console.error('[redis-twin]', JSON.stringify({
|
|
25
|
+
commands: w.service.length, servedSoFar: served, runs: w.runs, wrote: w.writes,
|
|
26
|
+
// from a command's bytes read to its reply written, its wait behind the commands before it included
|
|
27
|
+
serviceMs: { total: ms(w.service.reduce((sum, each) => sum + each, 0)), p50: ms(at(0.5)), p95: ms(at(0.95)), max: ms(sorted[sorted.length - 1]) },
|
|
28
|
+
waitedMs: { total: ms(w.waited), max: ms(w.waitedMax) },
|
|
29
|
+
phasesMs: { context: ms(w.phases.run - inRun), image: ms(w.phases.image), execute: ms(w.phases.execute), flush: ms(w.phases.flush), reply: ms(w.phases.reply), other: ms(w.own - w.phases.run - w.phases.reply) },
|
|
30
|
+
runPartsMs: Object.fromEntries(Object.entries(w.laps).map(([name, value]) => [name, ms(value)])),
|
|
31
|
+
queueMax: w.queueMax,
|
|
32
|
+
slowest: w.slowest ? { ...w.slowest, serviceMs: ms(w.slowest.serviceMs), waitedMs: ms(w.slowest.waitedMs), ownMs: ms(w.slowest.ownMs) } : null,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
export const redisServiceTimes = {
|
|
36
|
+
now,
|
|
37
|
+
/** A command starts: `received` is when its bytes were read, `waiting` the commands read and not yet started. */
|
|
38
|
+
begin(received, waiting, command) {
|
|
39
|
+
open = { received, started: now(), command };
|
|
40
|
+
if (waiting > window.queueMax)
|
|
41
|
+
window.queueMax = waiting;
|
|
42
|
+
if (!timer) {
|
|
43
|
+
timer = setTimeout(say, SAY_EVERY_MS);
|
|
44
|
+
timer.unref?.();
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
/** Time of the open command in one phase. */
|
|
48
|
+
add(phase, ms) { if (open) {
|
|
49
|
+
window.phases[phase] += ms;
|
|
50
|
+
if (phase === 'run')
|
|
51
|
+
window.runs += 1;
|
|
52
|
+
} },
|
|
53
|
+
wrote() { if (open)
|
|
54
|
+
window.writes += 1; },
|
|
55
|
+
/** A run begins: its first stretch is counted from here. */
|
|
56
|
+
lapFrom() { lapAt = now(); },
|
|
57
|
+
/** The stretch of the open command's run that ends here, by name. Outside a served command it is nothing. */
|
|
58
|
+
lap(name) { if (!open || lapAt === 0)
|
|
59
|
+
return; const at = now(); window.laps[name] = (window.laps[name] ?? 0) + (at - lapAt); lapAt = at; },
|
|
60
|
+
/** A run has ended: nothing after it is a stretch of it. */
|
|
61
|
+
lapEnd(name) { this.lap(name); lapAt = 0; },
|
|
62
|
+
/** The open command has answered (or blocked, or failed with its error answered). */
|
|
63
|
+
end(threw = false) {
|
|
64
|
+
if (!open)
|
|
65
|
+
return;
|
|
66
|
+
const ended = now(), waited = open.started - open.received;
|
|
67
|
+
const serviceMs = ended - open.received;
|
|
68
|
+
if (!window.slowest || serviceMs > window.slowest.serviceMs) {
|
|
69
|
+
window.slowest = { ...open.command, threw, serviceMs, waitedMs: waited, ownMs: ended - open.started };
|
|
70
|
+
}
|
|
71
|
+
window.service.push(ended - open.received);
|
|
72
|
+
window.waited += waited;
|
|
73
|
+
if (waited > window.waitedMax)
|
|
74
|
+
window.waitedMax = waited;
|
|
75
|
+
window.own += ended - open.started;
|
|
76
|
+
served += 1;
|
|
77
|
+
open = undefined;
|
|
78
|
+
},
|
|
79
|
+
/** `work`, its time counted in `phase` of the open command. */
|
|
80
|
+
timed(phase, work) { const began = now(); try {
|
|
81
|
+
return work();
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
this.add(phase, now() - began);
|
|
85
|
+
} },
|
|
86
|
+
phases: PHASES,
|
|
87
|
+
};
|
|
@@ -9,6 +9,7 @@ export type RedisStreamOptions = {
|
|
|
9
9
|
items: RunItem[];
|
|
10
10
|
wrote: boolean;
|
|
11
11
|
instant?: number;
|
|
12
|
+
touched?: string[];
|
|
12
13
|
}>;
|
|
13
14
|
};
|
|
14
|
-
export declare function createRedisStream(
|
|
15
|
+
export declare function createRedisStream(given: RedisStreamOptions): TwinStream;
|