@volter/world-core 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/app-route.cjs +95 -6
  2. package/app-route.d.cts +2 -1
  3. package/dist/app-route.cjs +95 -6
  4. package/dist/app-route.d.cts +2 -1
  5. package/dist/generated/pack-facts.json +127 -8
  6. package/dist/inject.cjs +46 -1
  7. package/dist/src/actions.d.ts +27 -0
  8. package/dist/src/actions.js +62 -4
  9. package/dist/src/blob-store.d.ts +3 -0
  10. package/dist/src/blob-store.js +15 -1
  11. package/dist/src/client-bundle.d.ts +4 -0
  12. package/dist/src/client-bundle.js +13 -2
  13. package/dist/src/derived-core.d.ts +6 -0
  14. package/dist/src/derived-core.js +17 -3
  15. package/dist/src/derived.js +5 -2
  16. package/dist/src/git/refs.js +5 -3
  17. package/dist/src/head.js +12 -2
  18. package/dist/src/index.d.ts +8 -5
  19. package/dist/src/index.js +9 -5
  20. package/dist/src/log.d.ts +2 -0
  21. package/dist/src/packRegistry.d.ts +8 -6
  22. package/dist/src/redis/engine.d.ts +308 -0
  23. package/dist/src/redis/engine.js +1663 -0
  24. package/dist/src/redis/index.d.ts +2 -0
  25. package/dist/src/redis/index.js +6 -0
  26. package/dist/src/redis/lua.d.ts +153 -0
  27. package/dist/src/redis/lua.js +1373 -0
  28. package/dist/src/request-scope.d.ts +28 -0
  29. package/dist/src/request-scope.js +70 -0
  30. package/dist/src/storage.d.ts +22 -1
  31. package/dist/src/storage.js +57 -1
  32. package/dist/src/trace-context.d.ts +31 -0
  33. package/dist/src/trace-context.js +78 -0
  34. package/dist/src/twin-fetch.d.ts +16 -0
  35. package/dist/src/twin-fetch.js +66 -3
  36. package/dist/src/world-clock.d.ts +2 -2
  37. package/dist/src/world-clock.js +18 -17
  38. package/dist/src/world-store.js +9 -0
  39. package/dist/vendor-hosts.cjs +2 -2
  40. package/dist/world-clock.cjs +40 -0
  41. package/dist/world-clock.d.cts +4 -0
  42. package/generated/pack-facts.json +127 -8
  43. package/inject.cjs +46 -1
  44. package/package.json +11 -1
  45. package/src/actions.ts +69 -4
  46. package/src/blob-store.ts +15 -1
  47. package/src/client-bundle.ts +15 -2
  48. package/src/derived-core.ts +17 -3
  49. package/src/derived.ts +5 -2
  50. package/src/git/refs.ts +5 -3
  51. package/src/head.ts +11 -2
  52. package/src/index.ts +11 -3
  53. package/src/log.ts +2 -0
  54. package/src/packRegistry.ts +8 -6
  55. package/src/redis/engine.ts +1468 -0
  56. package/src/redis/index.ts +6 -0
  57. package/src/redis/lua.ts +1250 -0
  58. package/src/request-scope.ts +74 -0
  59. package/src/storage.ts +54 -2
  60. package/src/trace-context.ts +87 -0
  61. package/src/twin-fetch.ts +69 -3
  62. package/src/world-clock.ts +19 -16
  63. package/src/world-store.ts +9 -0
  64. package/vendor-hosts.cjs +2 -2
  65. package/world-clock.cjs +40 -0
  66. package/world-clock.d.cts +4 -0
@@ -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 {};