@volter/world-core 2.0.0 → 2.0.1
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/app-route.cjs +95 -6
- package/app-route.d.cts +2 -1
- package/dist/app-route.cjs +95 -6
- package/dist/app-route.d.cts +2 -1
- package/dist/generated/pack-facts.json +127 -8
- package/dist/inject.cjs +46 -1
- package/dist/src/actions.d.ts +24 -0
- package/dist/src/actions.js +49 -2
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +4 -2
- package/dist/src/packRegistry.d.ts +8 -6
- package/dist/src/redis/engine.d.ts +308 -0
- package/dist/src/redis/engine.js +1663 -0
- package/dist/src/redis/index.d.ts +2 -0
- package/dist/src/redis/index.js +6 -0
- package/dist/src/redis/lua.d.ts +153 -0
- package/dist/src/redis/lua.js +1373 -0
- package/dist/src/storage.d.ts +22 -1
- package/dist/src/storage.js +55 -1
- package/dist/src/world-clock.d.ts +2 -2
- package/dist/src/world-clock.js +18 -17
- package/dist/vendor-hosts.cjs +2 -2
- package/dist/world-clock.cjs +40 -0
- package/dist/world-clock.d.cts +4 -0
- package/generated/pack-facts.json +127 -8
- package/inject.cjs +46 -1
- package/package.json +11 -1
- package/src/actions.ts +53 -2
- package/src/index.ts +5 -0
- package/src/packRegistry.ts +8 -6
- package/src/redis/engine.ts +1468 -0
- package/src/redis/index.ts +6 -0
- package/src/redis/lua.ts +1250 -0
- package/src/storage.ts +52 -2
- package/src/world-clock.ts +19 -16
- package/vendor-hosts.cjs +2 -2
- package/world-clock.cjs +40 -0
- package/world-clock.d.cts +4 -0
package/dist/src/actions.js
CHANGED
|
@@ -16,9 +16,9 @@ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
16
16
|
import { randomUUID } from 'node:crypto';
|
|
17
17
|
import { dirname, join } from 'node:path';
|
|
18
18
|
import { canonicalJson, subjectKey } from "./hash.js";
|
|
19
|
-
import { appendDurable, eventsLockPath, projectionLockPath, twinLog, withFileLock, worldPaths } from "./storage.js";
|
|
19
|
+
import { appendDurable, eventsLockPath, ownerStoreRoots, projectionLockPath, twinLog, withFileLock, withoutIdentityClaim, worldPaths } from "./storage.js";
|
|
20
20
|
import { landAsPlaceholder, placeholderPullActive } from "./placeholder-remote.js";
|
|
21
|
-
import { aliasesFrom, dropCheckpoint, landedCopy, landedIds, parentEntries, readTree } from "./log.js";
|
|
21
|
+
import { aliasesFrom, dropCheckpoint, landedCopy, landedIds, parentEntries, readTree, treeStamp } from "./log.js";
|
|
22
22
|
import { getActiveWorldStore } from "./world-store.js";
|
|
23
23
|
export class TwinActionPreconditionError extends Error {
|
|
24
24
|
actionId;
|
|
@@ -334,6 +334,53 @@ function assertPreconditions(action, root) {
|
|
|
334
334
|
export function projectResources(service, root, opts = {}) {
|
|
335
335
|
return readTree(service, root, opts);
|
|
336
336
|
}
|
|
337
|
+
/** A read of another pack's store that cannot choose: the subject it looks for (or, with none named, the store itself)
|
|
338
|
+
* is held by more than one service of the World. A reader answers it as the vendor answers a credential it cannot
|
|
339
|
+
* resolve; it is never a server error. */
|
|
340
|
+
export class OwnerStoreAmbiguousError extends Error {
|
|
341
|
+
constructor(owner, roots, subject) {
|
|
342
|
+
super(`${subject ? `${subject.type} ${subject.id} of ` : ''}${owner}'s store is held by more than one service of this World (${roots.join(', ')}); a cross-pack read cannot choose between them`);
|
|
343
|
+
this.name = 'OwnerStoreAmbiguousError';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const ownerIndexes = new Map();
|
|
347
|
+
function ownerIndex(owner, root) {
|
|
348
|
+
const key = `${owner}\0${root}`;
|
|
349
|
+
const stamp = treeStamp(owner, root);
|
|
350
|
+
const held = ownerIndexes.get(key);
|
|
351
|
+
if (held && held.stamp === stamp)
|
|
352
|
+
return held;
|
|
353
|
+
const rows = readTree(owner, root).map((r) => Object.freeze(r));
|
|
354
|
+
const index = { stamp, rows: Object.freeze(rows), byKey: new Map(rows.map((r) => [`${r.type}:${r.id}`, r])) };
|
|
355
|
+
ownerIndexes.set(key, index);
|
|
356
|
+
return index;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* ANOTHER pack's rows, read by contract (architecture A3: one vendor's store split across two packs, xidentity's tokens
|
|
360
|
+
* read by x, googleoauth's by googlecalendar; scripts/architecture.test.ts holds the declared reader-owner pairs): the
|
|
361
|
+
* owner's tree, from wherever the World keeps the owner's store (`ownerStoreRoots`). Read only: the rows are frozen and
|
|
362
|
+
* shared. It claims no journal identity (the twin answering is the reader).
|
|
363
|
+
*
|
|
364
|
+
* With `subject`, the store that holds that subject (the token a request presents): `[]` when none does, and
|
|
365
|
+
* `OwnerStoreAmbiguousError` only when more than one does, so a second service running the owning pack never breaks a
|
|
366
|
+
* lookup of what only one of them issued. Without it, the one store there is, and `OwnerStoreAmbiguousError` when the
|
|
367
|
+
* World holds two.
|
|
368
|
+
*/
|
|
369
|
+
export function projectOwnerResources(owner, root, subject) {
|
|
370
|
+
return withoutIdentityClaim(() => {
|
|
371
|
+
const roots = ownerStoreRoots(owner, root);
|
|
372
|
+
if (!subject) {
|
|
373
|
+
if (roots.length > 1)
|
|
374
|
+
throw new OwnerStoreAmbiguousError(owner, roots);
|
|
375
|
+
return roots[0] ? ownerIndex(owner, roots[0]).rows : [];
|
|
376
|
+
}
|
|
377
|
+
const key = `${subject.type}:${subject.id}`;
|
|
378
|
+
const holding = roots.filter((r) => ownerIndex(owner, r).byKey.has(key));
|
|
379
|
+
if (holding.length > 1)
|
|
380
|
+
throw new OwnerStoreAmbiguousError(owner, holding, subject);
|
|
381
|
+
return holding[0] ? ownerIndex(owner, holding[0]).rows : [];
|
|
382
|
+
});
|
|
383
|
+
}
|
|
337
384
|
/** The local → vendor id aliases the landed copies carry: a read by the id a caller was handed before
|
|
338
385
|
* its write was performed resolves to the row the vendor now owns. */
|
|
339
386
|
export function subjectAliases(service, root) {
|
package/dist/src/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export { loadWorldConfig, } from './worldConfig.js';
|
|
|
20
20
|
export type { WorldConfig, WorldServiceConfig, } from './worldConfig.js';
|
|
21
21
|
export { DELTA_TYPE_SUFFIX, diffSubjectFields, hashFieldValue, subjectKey, deltaAfterFields } from './hash.js';
|
|
22
22
|
export type { FieldChange, SubjectFields, } from './hash.js';
|
|
23
|
-
export { appendDurable, appendEvent, createEvent, emptyGenericState, genericWorldReducer, listEvents, loadState, projectionLockPath, readJsonFile, rebuildGenericState, rebuildState, scrubService, scrubWorld, stateDirName, twinLog, withFileLock, worldPaths, worldStateRoot, } from './storage.js';
|
|
23
|
+
export { appendDurable, appendEvent, createEvent, emptyGenericState, genericWorldReducer, listEvents, loadState, projectionLockPath, readJsonFile, rebuildGenericState, rebuildState, scrubService, scrubWorld, stateDirName, twinLog, withFileLock, worldPaths, worldStateRoot, ownerStoreRoots, WORLD_DATA_ENV, } from './storage.js';
|
|
24
24
|
export type { AppendEventResult, CommitQueuedEventsResult, GenericWorldState, EnqueueEventResult, QueuedWorldServiceEvent, WorldPaths, WorldReducer, WorldServiceEvent, } from './types.js';
|
|
25
25
|
export type { ScrubResult } from './storage.js';
|
|
26
26
|
export { FsWorldStore, MemoryWorldStore, getActiveWorldStore, setActiveWorldStore, withWorldStore, hydrateInto, flushFrom, } from './world-store.js';
|
|
@@ -36,7 +36,7 @@ export { createTwinProxy } from './proxy.js';
|
|
|
36
36
|
export type { TwinProxy, TwinProxyOptions, VendorRoute } from './proxy.js';
|
|
37
37
|
export { forkTwin, isFork, readForkMeta, } from './fork.js';
|
|
38
38
|
export type { ForkMeta } from './fork.js';
|
|
39
|
-
export { appendAction, appendTransactionCommit, checkPrecondition, confirmAction, listActions, listTransactionCommits, pendingActions, pushablePendingActions, isTwinBookkeeping, pendingTransactionCommits, projectResources, revertAction, TwinActionPreconditionError, subjectAliases, resolveSubjectId, runWithCorrelationId, currentCorrelationId } from './actions.js';
|
|
39
|
+
export { appendAction, appendTransactionCommit, checkPrecondition, confirmAction, listActions, listTransactionCommits, pendingActions, pushablePendingActions, isTwinBookkeeping, pendingTransactionCommits, projectResources, projectOwnerResources, OwnerStoreAmbiguousError, revertAction, TwinActionPreconditionError, subjectAliases, resolveSubjectId, runWithCorrelationId, currentCorrelationId } from './actions.js';
|
|
40
40
|
export type { ActionProjection, ProjectedDelivery, ProjectedResource, ProjectedResourcePatch, ProjectedResourceRef, TwinAction, TwinActionOp, TwinActionPrecondition, TwinActionPreconditionOp, TwinActionRevertSpec, TwinTransactionCommit, TwinTransactionCommitOp, TwinTransactionPrecondition, TwinTransactionRevertSpec, } from './actions.js';
|
|
41
41
|
export { approveChangeset, assertMarkerBelongsTo, assertSafeChangesetName, assertValidVerifiers, buildChangeset, captureMarker, changesetContentHash, changesetHashMatches, changesetReadiness, CHANGESET_KIND, diffLedgers, formatApplication, formatChangeset, formatRebaseReport, narrateActions, narrationDrift, rebaseChangeset, formatChangesetStatus, formatLedgerDelta, formatReplayReport, formatVerification, MARKER_KIND, normalizeChangeset, parseVerifierExpression, replayChangeset, runChangesetVerifiers, summarizeByVendor, twinWriteShape, withApplication, withVerification, worldBootMarker, WORLD_BOOT_MARKER_ID, } from './changeset.js';
|
|
42
42
|
export type { ApplyReceipt, RebaseActionResult, RebaseReport, RebaseTarget, Changeset, ChangesetAction, ChangesetApplication, CompensationEntry, ChangesetApproval, ChangesetReadiness, ChangesetVerification, ChangesetVerifier, ChangesetVerifierResult, LedgerDelta, LedgerPosition, LedgerRef, ReplayActionResult, ReplayReport, ReplayTarget, VendorSummary, WorldMarker, } from './changeset.js';
|
package/dist/src/index.js
CHANGED
|
@@ -42,7 +42,9 @@ twinLog,
|
|
|
42
42
|
// The kernel's cross-process mutual-exclusion primitive (exclusive-create lockfile with
|
|
43
43
|
// stale-holder reclaim). Public so world-runtime can guard concurrent `upWorld` claims of
|
|
44
44
|
// one instance dir with the SAME lock semantics the event log uses (TWIN-36).
|
|
45
|
-
withFileLock, worldPaths, worldStateRoot,
|
|
45
|
+
withFileLock, worldPaths, worldStateRoot,
|
|
46
|
+
// Where another pack's store is in a World (architecture A3), and the variable the runtime names its data directory in.
|
|
47
|
+
ownerStoreRoots, WORLD_DATA_ENV, } from "./storage.js";
|
|
46
48
|
// The pluggable persistence seam: the sync WorldStore interface, its fs (default) and
|
|
47
49
|
// in-memory implementations, the active-store injection point, and the async
|
|
48
50
|
// hydrate/flush boundary a serverless (Durable Object / KV / redis) entry uses.
|
|
@@ -54,7 +56,7 @@ export { FsBlobStore, MemoryBlobStore, blobDigest, getActiveBlobStore, setActive
|
|
|
54
56
|
export { applyTwinWrite, applyTwinWriteAtomic, createTwinServer, journalTwinRequest, twinRequestCredentials, readTwinRequestJournal, resolveTwinRead, twinRequestJournalEnabled, twinRequestJournalPath, twinResources, } from "./serve.js";
|
|
55
57
|
export { createTwinProxy } from "./proxy.js";
|
|
56
58
|
export { forkTwin, isFork, readForkMeta, } from "./fork.js";
|
|
57
|
-
export { appendAction, appendTransactionCommit, checkPrecondition, confirmAction, listActions, listTransactionCommits, pendingActions, pushablePendingActions, isTwinBookkeeping, pendingTransactionCommits, projectResources, revertAction, TwinActionPreconditionError, subjectAliases, resolveSubjectId, runWithCorrelationId, currentCorrelationId } from "./actions.js";
|
|
59
|
+
export { appendAction, appendTransactionCommit, checkPrecondition, confirmAction, listActions, listTransactionCommits, pendingActions, pushablePendingActions, isTwinBookkeeping, pendingTransactionCommits, projectResources, projectOwnerResources, OwnerStoreAmbiguousError, revertAction, TwinActionPreconditionError, subjectAliases, resolveSubjectId, runWithCorrelationId, currentCorrelationId } from "./actions.js";
|
|
58
60
|
// ── Operator control plane (R19/R20): remote refs, queue lifecycle, push ledger,
|
|
59
61
|
// apply leases, plan, status. (the twins architecture notes)
|
|
60
62
|
// The CHANGESET primitive + the ledger DIFF (docs/concepts/the-model.md v0) — "commit" and
|
|
@@ -5,12 +5,14 @@ import type { TwinEmitter } from './emit.js';
|
|
|
5
5
|
import { type StateSystemAdapters } from './state-system.js';
|
|
6
6
|
/**
|
|
7
7
|
* How a pack's clients ADDRESS it. The first three are HTTP API styles; `raw-tcp` is the
|
|
8
|
-
* RAW-PROTOCOL class — a pack whose clients speak
|
|
9
|
-
* rather than HTTP, so none of the HTTP machinery applies: no
|
|
10
|
-
* the injector's `VENDOR_HOSTS` host map is possible (the
|
|
11
|
-
* sees the traffic). Such a pack is wired into a world
|
|
12
|
-
* and this value is what tells a reader that "no injector
|
|
13
|
-
* missing wiring point. `packages/twin/smtp`
|
|
8
|
+
* RAW-PROTOCOL class — a pack whose clients speak their protocol directly over a TCP socket
|
|
9
|
+
* rather than through the HTTP/1 fetch machinery, so none of the HTTP machinery applies: no
|
|
10
|
+
* `browserRouting`, and no entry in the injector's `VENDOR_HOSTS` host map is possible (the
|
|
11
|
+
* injector patches http/fetch and never sees the traffic). Such a pack is wired into a world
|
|
12
|
+
* through app-read host/port env instead, and this value is what tells a reader that "no injector
|
|
13
|
+
* entry" is STRUCTURAL rather than a missing wiring point. `packages/twin/smtp` (a line protocol)
|
|
14
|
+
* is the first; `packages/twin/temporal` (gRPC: HTTP/2 framing its clients drive themselves) the
|
|
15
|
+
* second.
|
|
14
16
|
*
|
|
15
17
|
* Deliberately the transport CLASS, not the protocol name: naming the wire protocol would put a
|
|
16
18
|
* vendor id in the kernel the moment a pack is named after its protocol, which is exactly what
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { type LuaHost } from './lua.js';
|
|
2
|
+
/**
|
|
3
|
+
* A Redis SIMPLE STATUS reply, kept distinct from a bulk string.
|
|
4
|
+
*
|
|
5
|
+
* This is not pedantry — it is required for `Upstash-Encoding: base64` fidelity. LIVE-PROBED rule:
|
|
6
|
+
* with that header on, Upstash base64-encodes every string in `result` at any array depth EXCEPT
|
|
7
|
+
* the simple-status reply `OK`, which is passed through raw. `PING`'s status `PONG` IS encoded
|
|
8
|
+
* (`UE9ORw==`), and so is a BULK STRING whose value happens to be `OK` (`GET okkey` → `T0s=`). So
|
|
9
|
+
* the exemption is scoped to the status reply `OK` specifically, and a twin that exempted any
|
|
10
|
+
* string equal to `"OK"` would send raw bytes for `GET okkey`. Hence this wrapper.
|
|
11
|
+
*/
|
|
12
|
+
export declare class RedisStatus {
|
|
13
|
+
readonly value: string;
|
|
14
|
+
constructor(value: string);
|
|
15
|
+
}
|
|
16
|
+
export declare const OK: RedisStatus;
|
|
17
|
+
/** The JSON projection of a RESP reply — what Upstash puts in `{"result": …}`. */
|
|
18
|
+
export type RedisValue = null | number | string | RedisStatus | RedisValue[];
|
|
19
|
+
/** A command-level failure. `message` is the vendor's literal error string. Maps to HTTP 400. */
|
|
20
|
+
export declare class RedisCommandError extends Error {
|
|
21
|
+
constructor(message: string);
|
|
22
|
+
}
|
|
23
|
+
/** Thrown when a write is attempted against a read-only twin. Mapped to HTTP 405 by the handler. */
|
|
24
|
+
export declare class ReadOnlyError extends Error {
|
|
25
|
+
constructor();
|
|
26
|
+
}
|
|
27
|
+
export declare const KEY_TYPES: readonly ["string", "list", "set", "hash", "zset", "stream"];
|
|
28
|
+
export type KeyType = typeof KEY_TYPES[number];
|
|
29
|
+
export type HashPairs = Array<[string, string]>;
|
|
30
|
+
export type ZSetPairs = Array<[string, number]>;
|
|
31
|
+
/**
|
|
32
|
+
* How a zset is STORED. Scores persist as the STRING Redis itself would print, never as raw JSON
|
|
33
|
+
* numbers — because `JSON.stringify(Infinity)` is `null`, and `ZADD key +inf member` is ordinary
|
|
34
|
+
* Redis (leaderboard sentinels, "pin this to the top" idioms). §9 round 1 caught the bug this
|
|
35
|
+
* prevents: an in-memory `+inf` survived within one request and came back as the literal bulk
|
|
36
|
+
* string "null" on the next, whereupon `ZRANGEBYSCORE key 0 100` cheerfully returned the member —
|
|
37
|
+
* a plausible-looking WRONG answer over persisted state, which is the exact failure mode this pack
|
|
38
|
+
* forbids. `fmtScore`/`toFloat` round-trip inf, -inf and every finite double faithfully.
|
|
39
|
+
*/
|
|
40
|
+
type StoredZSet = Array<[string, string]>;
|
|
41
|
+
export type StreamEntries = Array<[string, HashPairs]>;
|
|
42
|
+
/** A point-in-time copy of a run's whole mutable state, for script rollback. */
|
|
43
|
+
type Snapshot = {
|
|
44
|
+
rows: Map<string, KeyRow>;
|
|
45
|
+
touched: Map<string, string>;
|
|
46
|
+
scripts: Map<string, string>;
|
|
47
|
+
touchedScripts: Map<string, string>;
|
|
48
|
+
};
|
|
49
|
+
export type KeyRow = {
|
|
50
|
+
name: string;
|
|
51
|
+
kind: KeyType;
|
|
52
|
+
/** string → string; list/set → string[]; hash → [f,v][]; zset → [member,score][]; stream → [id,[f,v][]][] */
|
|
53
|
+
v: string | string[] | HashPairs | ZSetPairs | StreamEntries;
|
|
54
|
+
/** Absolute expiry deadline in epoch ms, or null for "no TTL". */
|
|
55
|
+
pexpireAt: number | null;
|
|
56
|
+
/** Stream only: the last id this stream minted, so `*` stays monotonic. */
|
|
57
|
+
lastId?: string;
|
|
58
|
+
/** Stream only: its consumer groups and counters, as the dialect that serves them keeps them (the redis pack's XGROUP). */
|
|
59
|
+
meta?: unknown;
|
|
60
|
+
/**
|
|
61
|
+
* A per-key WRITE ORDINAL, incremented on every flush. No command ever reads it and it never
|
|
62
|
+
* leaves this module — it exists solely to defeat the kernel's action dedupe.
|
|
63
|
+
*
|
|
64
|
+
* THE BUG IT FIXES (found by self-attack after §9 round 1): `applyTwinWrite` dedupes by
|
|
65
|
+
* (content + occurredAt millisecond), so writing a key back to a value it ALREADY HELD at the
|
|
66
|
+
* same instant produced an actionId identical to the earlier action and was silently dropped as
|
|
67
|
+
* `replayed`. The projection then folded to the LATER surviving action, so the key kept the
|
|
68
|
+
* WRONG value while the command's reply reported the right one — reply and state disagreeing,
|
|
69
|
+
* which is the worst shape a wrong answer can take. Concretely, with a pinned clock:
|
|
70
|
+
* ZADD e 10 m ; ZADD e INCR 5 m → 15 ; ZADD e XX INCR 5 m → 20 ; ZADD e GT INCR 5 m → 25 ;
|
|
71
|
+
* ZADD e LT INCR -5 m → answered 20, but ZSCORE still said 25
|
|
72
|
+
* because a `[["m","20"]]` action already existed at that instant. The same hazard reaches any
|
|
73
|
+
* `SET k a ; SET k b ; SET k a` landing inside one millisecond. Bumping an ordinal makes every
|
|
74
|
+
* write's content unique, so nothing can be mistaken for a replay.
|
|
75
|
+
*/
|
|
76
|
+
_rev?: number;
|
|
77
|
+
};
|
|
78
|
+
/** Everything a run needs. `root` scopes the kernel state; `occurredAt` IS the clock. */
|
|
79
|
+
export type RedisContext = {
|
|
80
|
+
root?: string;
|
|
81
|
+
occurredAt: string;
|
|
82
|
+
/** When true, any write command raises `ReadOnlyError` (the wire answers its read-only refusal). */
|
|
83
|
+
readOnly?: boolean;
|
|
84
|
+
/** A database other than the service's first (Redis's SELECT n, a vendor's database id), whose keys and scripts
|
|
85
|
+
* are its own: subjects `db:<id>:key:<name>` and `db:<id>:script:<sha1>`. Absent: `key:<name>`, `script:<sha1>`. */
|
|
86
|
+
database?: string;
|
|
87
|
+
/** The World service whose tree holds the keys: the pack's own. */
|
|
88
|
+
service: string;
|
|
89
|
+
/** The wire the core answers for: its refusals, its commands, its Lua, its layout. */
|
|
90
|
+
dialect: RedisDialect;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* WHAT A WIRE ANSWERS DIFFERENTLY. Stock Redis and Upstash's REST envelope serve one set of Redis semantics and
|
|
94
|
+
* differ in what they refuse and what they add; a dialect carries exactly those differences.
|
|
95
|
+
*/
|
|
96
|
+
export type RedisDialect = {
|
|
97
|
+
/** The refusal for a command neither the core nor the dialect serves. */
|
|
98
|
+
unknownCommand: (argv: string[]) => string;
|
|
99
|
+
/** The refusal for an option of a served command the core does not model (`ZRANGE BYLEX`, `XADD MAXLEN trimming`):
|
|
100
|
+
* a loud refusal, never a success, in the wire's words. */
|
|
101
|
+
unmodeled: (what: string) => string;
|
|
102
|
+
/** The whole queue-time check, when the wire has its own (Upstash's REST-restricted commands, its table's arity);
|
|
103
|
+
* absent, a command is checked against the dialect's commands, then the core's, by arity. */
|
|
104
|
+
shapeError?: (argv: string[]) => string | null;
|
|
105
|
+
/** The dialect's own commands, `[min, max]` arguments after the name as `SERVED_COMMANDS`, run before the core. */
|
|
106
|
+
commands: Record<string, [number, number]>;
|
|
107
|
+
exec: (space: KeySpace, argv: string[]) => RedisValue;
|
|
108
|
+
/** Which of the dialect's commands write. */
|
|
109
|
+
writes: ReadonlySet<string>;
|
|
110
|
+
/** The script environment (`extend`: the command API's name, runtime libraries) and how a Lua number becomes a
|
|
111
|
+
* command argument (Redis 7: the shortest round-tripping form; Upstash: truncated). */
|
|
112
|
+
lua: Required<Pick<LuaHost, 'extend' | 'numberArg'>>;
|
|
113
|
+
/** Whether a script that fails part-way keeps what it wrote (Redis) or leaves nothing (Upstash). */
|
|
114
|
+
scriptErrorsKeepWrites: boolean;
|
|
115
|
+
/** How the dialect's keys are laid out in the tree, when not one subject per key (`keyspaceImageOf`, `flushKeySpace`). */
|
|
116
|
+
storage?: {
|
|
117
|
+
image: (ctx: RedisContext) => KeyspaceImage;
|
|
118
|
+
flush: (space: KeySpace) => Promise<void>;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
export declare const WRONGTYPE = "WRONGTYPE Operation against a key holding the wrong kind of value";
|
|
122
|
+
export declare const NOT_INT = "ERR value is not an integer or out of range";
|
|
123
|
+
export declare const NOT_FLOAT = "ERR value is not a valid float";
|
|
124
|
+
export declare const SYNTAX = "ERR syntax error";
|
|
125
|
+
export declare const NO_SUCH_KEY = "ERR no such key";
|
|
126
|
+
export declare function wrongArity(name: string): string;
|
|
127
|
+
/**
|
|
128
|
+
* The commands the core serves, each with the most arguments (after the name) its own parsing takes,
|
|
129
|
+
* `-1` for no bound: `[min, max]`, where `min` is Redis's table arity and `max` is what the command itself
|
|
130
|
+
* refuses past, with the same "wrong number of arguments". One table for both callers, `execOne` and
|
|
131
|
+
* `commandShapeError` (which lets a transaction reject a malformed batch at QUEUE time, as Redis's `EXECABORT`
|
|
132
|
+
* does). SCRIPT is a container: its subcommands are LOAD, EXISTS and FLUSH. SELECT is the dialect's: a database is
|
|
133
|
+
* a connection's (Redis) or a URL's (Upstash).
|
|
134
|
+
*/
|
|
135
|
+
export declare const SERVED_COMMANDS: Record<string, [number, number]>;
|
|
136
|
+
/**
|
|
137
|
+
* Does this command MUTATE? Takes the whole argv, not just the name, because two commands are
|
|
138
|
+
* only writes for some of their subcommands. A read-only twin and `EVAL_RO` both key off this,
|
|
139
|
+
* and getting it wrong in either direction is a bug: too broad refuses legitimate reads (§9 round
|
|
140
|
+
* 1), too narrow lets a write through a read-only twin.
|
|
141
|
+
*/
|
|
142
|
+
export declare function isWriteCommand(name: string, args: readonly string[] | undefined, dialect: RedisDialect): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* QUEUE-TIME validation: is this command well-formed enough to accept into a transaction? Returns the wire's
|
|
145
|
+
* error string, or `null` when the command is fine. Only structural faults live here (unknown command, wrong
|
|
146
|
+
* arity) — a WRONGTYPE or a bad integer is a RUNTIME error, which does NOT abort a transaction.
|
|
147
|
+
*/
|
|
148
|
+
export declare function commandShapeError(argv: unknown[], dialect: RedisDialect): string | null;
|
|
149
|
+
/** A database's keys, scripts and write ordinals as the tree holds them. Rows past their deadline are kept: a read
|
|
150
|
+
* asks `live` at its own instant (Redis's `keyIsExpired` is `now > when`, so a key is still ALIVE at exactly its
|
|
151
|
+
* deadline — §9 round 1 found the twin one millisecond early). */
|
|
152
|
+
export type KeyspaceImage = {
|
|
153
|
+
rows: Map<string, KeyRow>;
|
|
154
|
+
scripts: Map<string, string>;
|
|
155
|
+
revs: Map<string, number>;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* A synchronous, in-memory image of the keyspace for the duration of ONE request.
|
|
159
|
+
*
|
|
160
|
+
* Seeded from the keyspace's image (the tree folded by `projectResources`, memoized per World state — the kernel IS
|
|
161
|
+
* the source of truth); every mutation records the key name in `touched`, and the flush writes exactly those keys
|
|
162
|
+
* back. This run's maps are its own copies: nothing a run changes reaches the image or a later request except
|
|
163
|
+
* through the tree.
|
|
164
|
+
*/
|
|
165
|
+
export declare class KeySpace {
|
|
166
|
+
readonly ctx: RedisContext;
|
|
167
|
+
readonly nowMs: number;
|
|
168
|
+
private readonly rows;
|
|
169
|
+
private readonly touched;
|
|
170
|
+
private readonly scripts;
|
|
171
|
+
private readonly touchedScripts;
|
|
172
|
+
/** name → the last write ordinal seen, INCLUDING for keys currently deleted or expired. */
|
|
173
|
+
private readonly revs;
|
|
174
|
+
/** The keyspace as the tree held it when this run began: shared, never changed in place. */
|
|
175
|
+
readonly image: KeyspaceImage;
|
|
176
|
+
constructor(ctx: RedisContext, nowMs: number);
|
|
177
|
+
/**
|
|
178
|
+
* Is this row expired AS OF NOW? Checked on every read, not merely when the snapshot was built.
|
|
179
|
+
*
|
|
180
|
+
* §9 round 2: a deadline set INTO THE PAST inside a batch (`SET k v PXAT 1`, `GETEX k EXAT 1`)
|
|
181
|
+
* stayed visible for the rest of that batch, and `TTL` answered a huge negative number that real
|
|
182
|
+
* Redis can never return. Filtering only at construction time made the twin disagree with itself
|
|
183
|
+
* within one request; re-checking here makes every read path agree at every instant.
|
|
184
|
+
*/
|
|
185
|
+
private live;
|
|
186
|
+
all(): KeyRow[];
|
|
187
|
+
scriptFor(sha: string): string | undefined;
|
|
188
|
+
hasScript(sha: string): boolean;
|
|
189
|
+
putScript(sha: string, body: string): void;
|
|
190
|
+
flushScripts(): void;
|
|
191
|
+
pendingScriptWrites(): Array<{
|
|
192
|
+
sha: string;
|
|
193
|
+
operation: string;
|
|
194
|
+
body: string | null;
|
|
195
|
+
}>;
|
|
196
|
+
get(name: string): KeyRow | undefined;
|
|
197
|
+
put(row: KeyRow, operation: string): void;
|
|
198
|
+
remove(name: string, operation: string): void;
|
|
199
|
+
/** A point-in-time copy, so a failed script can be rolled back to exactly where it started. */
|
|
200
|
+
snapshot(): Snapshot;
|
|
201
|
+
restore(s: Snapshot): void;
|
|
202
|
+
pendingWrites(): Array<{
|
|
203
|
+
name: string;
|
|
204
|
+
operation: string;
|
|
205
|
+
row: KeyRow | null;
|
|
206
|
+
}>;
|
|
207
|
+
/** The next write ordinal for a key — see `KeyRow._rev`. Survives deletes and expiry. */
|
|
208
|
+
nextRev(name: string): number;
|
|
209
|
+
/** A key's write ordinal as the tree holds it, 0 for a key never written: what a WATCH compares. */
|
|
210
|
+
revOf(name: string): number;
|
|
211
|
+
/** The keys this run has written so far. */
|
|
212
|
+
touchedNames(): string[];
|
|
213
|
+
/** Did this run write anything? Used to decide whether the sync token advances. */
|
|
214
|
+
get dirty(): boolean;
|
|
215
|
+
}
|
|
216
|
+
/** One key as a backup holds it: its name, type, value, deadline and a stream's last id. */
|
|
217
|
+
export type KeyImage = {
|
|
218
|
+
name: string;
|
|
219
|
+
kind: string;
|
|
220
|
+
v: unknown;
|
|
221
|
+
pexpireAt: number | null;
|
|
222
|
+
lastId?: string;
|
|
223
|
+
};
|
|
224
|
+
/** A database's live keys as they stand at `ctx.occurredAt`: what a backup of it holds (the Developer API lane's
|
|
225
|
+
* backups, ../api/src/semantics/backups.ts). */
|
|
226
|
+
export declare function keyspaceImage(ctx: RedisContext): KeyImage[];
|
|
227
|
+
/** Read a database as it stands at `ctx.occurredAt`, writing nothing: what the redis pack's WATCH records and compares. */
|
|
228
|
+
export declare function readKeySpace<T>(ctx: RedisContext, read: (space: KeySpace) => T): T;
|
|
229
|
+
/** Replace a database's keys with a backup's: every live key is deleted, then the image's keys are written ("All
|
|
230
|
+
* existing data in the target database will be deleted before the restore operation begins",
|
|
231
|
+
* https://upstash.com/docs/redis/features/backup). */
|
|
232
|
+
export declare function restoreKeyspace(image: readonly KeyImage[], ctx: RedisContext): Promise<void>;
|
|
233
|
+
export declare function expectType(row: KeyRow | undefined, kind: KeyType): void;
|
|
234
|
+
export declare const asString: (row: KeyRow | undefined) => string | undefined;
|
|
235
|
+
export declare const asList: (row: KeyRow | undefined) => string[];
|
|
236
|
+
export declare const asSet: (row: KeyRow | undefined) => string[];
|
|
237
|
+
export declare const asHash: (row: KeyRow | undefined) => HashPairs;
|
|
238
|
+
export declare const asZSet: (row: KeyRow | undefined) => ZSetPairs;
|
|
239
|
+
/** Serialize for the kernel. The inverse of `asZSet` — see `StoredZSet`. */
|
|
240
|
+
export declare const storeZSet: (pairs: ZSetPairs) => StoredZSet;
|
|
241
|
+
export declare const asStream: (row: KeyRow | undefined) => StreamEntries;
|
|
242
|
+
export declare function toInt(raw: string): number;
|
|
243
|
+
export declare function toFloat(raw: string): number;
|
|
244
|
+
/**
|
|
245
|
+
* Redis's float-increment guard. `t_string.c` and `t_hash.c` both do
|
|
246
|
+
* `if (isnan(value) || isinf(value)) addReplyError(c,"increment would produce NaN or Infinity")`.
|
|
247
|
+
*
|
|
248
|
+
* §9 round 2 found this missing on the string and hash paths (it had only been added to the sorted
|
|
249
|
+
* set): `INCRBYFLOAT f inf` answered 200 with "inf", `INCRBYFLOAT f -inf` then persisted the
|
|
250
|
+
* literal "NaN", and the very next `INCRBYFLOAT f 1` answered "not a valid float" — the twin had
|
|
251
|
+
* written a value it could no longer read. A fake success that poisons its own key.
|
|
252
|
+
*/
|
|
253
|
+
export declare function guardFloatResult(next: number): number;
|
|
254
|
+
/** Redis renders a score as a bulk string; infinities spell out. */
|
|
255
|
+
export declare function fmtScore(n: number): string;
|
|
256
|
+
/** Redis sorts a zset by (score, then member lexicographically). */
|
|
257
|
+
export declare function sortZSet(pairs: ZSetPairs): ZSetPairs;
|
|
258
|
+
/** Redis's start/stop index normalisation (negatives count from the end, ends clamp). */
|
|
259
|
+
export declare function normalizeRange(startRaw: number, stopRaw: number, length: number): [number, number];
|
|
260
|
+
/** Redis glob-style key matching (`*`, `?`, `[abc]`, `[a-c]`, `[^a]`, `\` escape). */
|
|
261
|
+
export declare function globMatch(pattern: string, subject: string): boolean;
|
|
262
|
+
export declare function execOne(space: KeySpace, argv: string[]): RedisValue;
|
|
263
|
+
/**
|
|
264
|
+
* A key's fixed position in SCAN's iteration order: the low 31 bits of its own SHA-1.
|
|
265
|
+
*
|
|
266
|
+
* Deriving it from the KEY NAME is the whole point — the position is a property of the key, so
|
|
267
|
+
* adding or deleting any OTHER key cannot move it, and a key present for the whole scan is
|
|
268
|
+
* therefore returned exactly once. `+1` keeps every value strictly positive so that cursor 0
|
|
269
|
+
* unambiguously means "start"/"done" and can never also mean "resume at the first key".
|
|
270
|
+
*/
|
|
271
|
+
export declare function scanCursorFor(key: string): number;
|
|
272
|
+
/** Live key names, with the internal script-cache keys filtered out of the keyspace entirely. */
|
|
273
|
+
/** Every live key name. No filtering: the script cache lives in a different subject type entirely,
|
|
274
|
+
* so there is no internal name for a caller's key to collide with or be hidden by. */
|
|
275
|
+
export declare function visibleKeys(space: KeySpace): string[];
|
|
276
|
+
export type ScoreBound = {
|
|
277
|
+
value: number;
|
|
278
|
+
exclusive: boolean;
|
|
279
|
+
};
|
|
280
|
+
export declare function parseScoreBound(raw: string): ScoreBound;
|
|
281
|
+
export declare function inScoreRange(score: number, lo: ScoreBound, hi: ScoreBound): boolean;
|
|
282
|
+
export declare function zrange(space: KeySpace, name: string, a: string[]): RedisValue;
|
|
283
|
+
export declare function compareStreamIds(left: string, right: string): number;
|
|
284
|
+
export declare function parseStreamBound(raw: string, side: 'min' | 'max'): string;
|
|
285
|
+
export type RunItem = {
|
|
286
|
+
result: RedisValue;
|
|
287
|
+
} | {
|
|
288
|
+
error: string;
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Execute a whole request's worth of commands against one root, then flush.
|
|
292
|
+
*
|
|
293
|
+
* Each element comes back as `{result}` or `{error}` — the exact per-command envelope Upstash's
|
|
294
|
+
* `/pipeline` and `/multi-exec` endpoints return and the SDK's `Pipeline.exec` destructures.
|
|
295
|
+
*
|
|
296
|
+
* RUNTIME ERRORS DO NOT ABORT — not in a pipeline and (LIVE-PROBED, contrary to the intuition that
|
|
297
|
+
* a "transaction" rolls back) NOT in `/multi-exec` either: Upstash's own docs say "all commands
|
|
298
|
+
* will be executed. Upstash Redis will not stop the processing of commands. This is to provide same
|
|
299
|
+
* semantics with Redis when there are errors inside a transaction." A probe of the real service
|
|
300
|
+
* confirmed a `SET` after a failing `INCR` in a `/multi-exec` batch is applied. Structural faults —
|
|
301
|
+
* an unavailable command or a bad arity — are QUEUE-time and DO discard the whole batch, but the
|
|
302
|
+
* caller (`upstash-twin.ts`) rejects those before this function is ever reached.
|
|
303
|
+
*/
|
|
304
|
+
export declare function execRedisRun(commands: string[][], ctx: RedisContext): Promise<{
|
|
305
|
+
items: RunItem[];
|
|
306
|
+
wrote: boolean;
|
|
307
|
+
}>;
|
|
308
|
+
export {};
|