experimental-a2 0.3.0 → 0.4.0
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/CHANGELOG.md +156 -0
- package/dist/ai-B4YhEnfw.d.ts +333 -0
- package/dist/ai-server.d.ts +48 -9
- package/dist/ai-server.js +121 -49
- package/dist/ai.d.ts +2 -303
- package/dist/ai.js +231 -86
- package/dist/cli-B3VuxoDe.js +597 -0
- package/dist/cli-bin.d.ts +1 -0
- package/dist/cli-bin.js +5 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +2 -0
- package/dist/client-BrfDXQ8A.d.ts +155 -0
- package/dist/client-Bt4tAKi9.js +798 -0
- package/dist/client.d.ts +2 -90
- package/dist/client.js +1 -409
- package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +10 -2
- package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +62 -8
- package/dist/devtools-J_jZ2vQf.d.ts +151 -0
- package/dist/devtools-kJJaORn-.js +338 -0
- package/dist/devtools-server.browser.js +1 -1
- package/dist/devtools-server.d.ts +2 -2
- package/dist/devtools-server.js +224 -43
- package/dist/devtools.d.ts +2 -0
- package/dist/devtools.js +2 -0
- package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +4 -4
- package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +1 -1
- package/dist/http.d.ts +71 -13
- package/dist/http.js +302 -41
- package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +2 -1
- package/dist/internal-aEotMzu_.js +209 -0
- package/dist/otel.d.ts +1 -1
- package/dist/platform-B4TnJtWu.js +32 -0
- package/dist/react.d.ts +37 -14
- package/dist/react.js +26 -15
- package/dist/scheduler-qstash.d.ts +78 -0
- package/dist/scheduler-qstash.js +499 -0
- package/dist/scheduler-task-BpzhPnRS.js +54 -0
- package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +17 -24
- package/dist/scheduler-vercel.js +226 -0
- package/dist/server-CcNnFnoW.js +1405 -0
- package/dist/server-YtPq7hjw.d.ts +260 -0
- package/dist/server.d.ts +4 -155
- package/dist/server.js +2 -2
- package/dist/{log-ldf5g8Cx.d.ts → store-C3sNAaBT.d.ts} +111 -35
- package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +1 -1
- package/dist/store-codec-DTG0Ftek.js +8 -0
- package/dist/store-memory.d.ts +11 -0
- package/dist/{log-memory.js → store-memory.js} +127 -24
- package/dist/{log-polling-DZ1MiKLg.js → store-polling-DgrrAE3d.js} +5 -5
- package/dist/{log-postgres.d.ts → store-postgres.d.ts} +6 -6
- package/dist/{log-postgres.js → store-postgres.js} +158 -24
- package/dist/{log-redis-core-CyJ5L8yR.js → store-redis-core-DWqx3F47.js} +269 -66
- package/dist/{log-redis-http.d.ts → store-redis-http.d.ts} +7 -7
- package/dist/{log-redis-http.js → store-redis-http.js} +20 -12
- package/dist/{log-redis.d.ts → store-redis.d.ts} +6 -6
- package/dist/{log-redis.js → store-redis.js} +99 -39
- package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +6 -6
- package/dist/{log-sqlite.js → store-sqlite.js} +116 -22
- package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +1 -1
- package/dist/testing.browser.d.ts +1 -0
- package/dist/testing.browser.js +4 -0
- package/dist/testing.d.ts +31 -0
- package/dist/testing.js +101 -0
- package/dist/wire-DCUZBUlT.js +222 -0
- package/docs/01-quickstart.mdx +3 -3
- package/docs/concepts/01-contracts.mdx +18 -13
- package/docs/concepts/02-handlers.mdx +7 -7
- package/docs/concepts/03-durability.mdx +21 -21
- package/docs/concepts/04-state.mdx +18 -21
- package/docs/guides/01-timers.mdx +154 -49
- package/docs/guides/02-cancellation.mdx +30 -4
- package/docs/guides/03-react.mdx +20 -21
- package/docs/guides/04-local-first.mdx +1 -1
- package/docs/guides/05-production.mdx +310 -61
- package/docs/guides/06-ai-agents.mdx +249 -49
- package/docs/guides/07-devtools.mdx +137 -12
- package/docs/guides/08-application-data.mdx +1 -1
- package/docs/guides/09-presence.mdx +284 -0
- package/docs/guides/10-transports.mdx +131 -0
- package/docs/index.mdx +18 -14
- package/docs/reference/01-api.mdx +751 -103
- package/docs/reference/02-errors.mdx +10 -5
- package/package.json +23 -7
- package/dist/internal-gCd5qMry.js +0 -44
- package/dist/log-memory.d.ts +0 -11
- package/dist/recovery-vercel.js +0 -119
- package/dist/server-BcLa4RFL.js +0 -877
- package/dist/wire-BVsgR8o9.js +0 -62
|
@@ -1,31 +1,17 @@
|
|
|
1
|
-
import { t as A2Error } from "./errors-
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { t as A2Error } from "./errors-BQuJpe82.js";
|
|
2
|
+
import { g as nullProtoRecord } from "./internal-aEotMzu_.js";
|
|
3
|
+
import { t as idempotentReplay } from "./idempotent-replay-DuqEkYA7.js";
|
|
4
|
+
import { t as decodeReturnedEventIds } from "./store-codec-DTG0Ftek.js";
|
|
5
|
+
//#region src/store-redis-core.ts
|
|
6
|
+
const READ_PAGE_SIZE = 256;
|
|
4
7
|
/**
|
|
5
|
-
*
|
|
6
|
-
* `close`, expressed over one injected `call` function so both
|
|
7
|
-
* transports reuse it: `log-redis` (Redis protocol via `ioredis`) and
|
|
8
|
-
* `log-redis-http` (provider REST APIs).
|
|
9
|
-
*
|
|
10
|
-
* The stream IS the log: each event is a stream entry whose ID is its
|
|
11
|
-
* dense a2 index (`${index}-0`), so `XRANGE` is the catch-up read.
|
|
12
|
-
* Mutable drain/failure bookkeeping lives in a side hash (stream
|
|
13
|
-
* entries are immutable); every write path is one atomic Lua script.
|
|
14
|
-
* The conformance suite in test/conformance is the executable
|
|
15
|
-
* contract, run against a real spawned `redis-server`.
|
|
16
|
-
*
|
|
17
|
-
* Timestamps and claim expiry come from the injected clock so tests
|
|
18
|
-
* can time-travel against a real server. Cluster mode is out: the
|
|
19
|
-
* atomic scripts span keys.
|
|
20
|
-
*/
|
|
21
|
-
/**
|
|
22
|
-
* KEYS: log stream, counter, global id hash, session index, metadata,
|
|
8
|
+
* KEYS: store stream, counter, global id hash, session index, metadata,
|
|
23
9
|
* pending, ready, watched.
|
|
24
10
|
* ARGV: sessionId, nowMs, n, then per event:
|
|
25
11
|
* id, type, payloadJson, causeJson, lane, settled.
|
|
26
12
|
* Returns ['empty', hasPending] | ['ok', firstIndex, hasPending, watched] |
|
|
27
13
|
* ['dup', hasPending, ...indexes] | ['partial', count] |
|
|
28
|
-
* ['foreign', eventId].
|
|
14
|
+
* ['foreign', eventId] | ['no_parent', idx] | ['superseded', idx].
|
|
29
15
|
*/
|
|
30
16
|
const APPEND_LUA = `
|
|
31
17
|
local function lane_field(lane, suffix)
|
|
@@ -67,6 +53,22 @@ if #dups == n then
|
|
|
67
53
|
return res
|
|
68
54
|
end
|
|
69
55
|
if #dups > 0 then return { 'partial', tostring(#dups) } end
|
|
56
|
+
for i = 1, n do
|
|
57
|
+
local cause = ARGV[3 + (i - 1) * 6 + 4]
|
|
58
|
+
if cause ~= '' then
|
|
59
|
+
local parsed = cjson.decode(cause)
|
|
60
|
+
local pidx = tonumber(parsed.index)
|
|
61
|
+
if pidx < 1 or pidx > base then
|
|
62
|
+
return { 'no_parent', tostring(parsed.index) }
|
|
63
|
+
end
|
|
64
|
+
local field = tostring(pidx)
|
|
65
|
+
local dispatches = tonumber(redis.call('HGET', KEYS[5], field .. ':d') or '0')
|
|
66
|
+
if dispatches ~= tonumber(parsed.attempt) or
|
|
67
|
+
redis.call('HEXISTS', KEYS[5], field .. ':f') == 1 then
|
|
68
|
+
return { 'superseded', field }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
70
72
|
for i = 1, n do
|
|
71
73
|
local id = ARGV[3 + (i - 1) * 6 + 1]
|
|
72
74
|
local typ = ARGV[3 + (i - 1) * 6 + 2]
|
|
@@ -152,7 +154,7 @@ end
|
|
|
152
154
|
return { 'failed', tostring(failures) }
|
|
153
155
|
`;
|
|
154
156
|
/**
|
|
155
|
-
* KEYS:
|
|
157
|
+
* KEYS: store, counter, meta, pending, ready.
|
|
156
158
|
* ARGV: holder, nowMs, expiresAtMs, excludedCount, excluded indexes.
|
|
157
159
|
*/
|
|
158
160
|
const CLAIM_AVAILABLE_LUA = `
|
|
@@ -202,29 +204,42 @@ if retry_at then return { 'busy', tostring(retry_at) } end
|
|
|
202
204
|
return { 'settled' }
|
|
203
205
|
`;
|
|
204
206
|
/**
|
|
205
|
-
* KEYS: counter, meta.
|
|
207
|
+
* KEYS: counter, meta.
|
|
208
|
+
* ARGV: holder, nowMs, expiresAtMs, n, then per claim: index, attempt.
|
|
209
|
+
* Returns [renewedCount, ...renewed, ...superseded].
|
|
206
210
|
*/
|
|
207
211
|
const RENEW_CLAIMS_LUA = `
|
|
208
212
|
local total = tonumber(redis.call('GET', KEYS[1]) or '0')
|
|
209
213
|
local renewed = {}
|
|
214
|
+
local superseded = {}
|
|
210
215
|
for i = 1, tonumber(ARGV[4]) do
|
|
211
|
-
local field = ARGV[4 + i]
|
|
216
|
+
local field = ARGV[4 + (i - 1) * 2 + 1]
|
|
217
|
+
local attempt = tonumber(ARGV[4 + (i - 1) * 2 + 2])
|
|
212
218
|
local idx = tonumber(field)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
if idx >= 1 and idx <= total then
|
|
220
|
+
local dispatches = tonumber(redis.call('HGET', KEYS[2], field .. ':d') or '0')
|
|
221
|
+
if dispatches > attempt then
|
|
222
|
+
superseded[#superseded + 1] = field
|
|
223
|
+
else
|
|
224
|
+
local expires = tonumber(redis.call('HGET', KEYS[2], field .. ':ce') or '0')
|
|
225
|
+
if tonumber(ARGV[3]) > tonumber(ARGV[2]) and
|
|
226
|
+
redis.call('HEXISTS', KEYS[2], field .. ':p') == 0 and
|
|
227
|
+
redis.call('HEXISTS', KEYS[2], field .. ':f') == 0 and
|
|
228
|
+
redis.call('HGET', KEYS[2], field .. ':ch') == ARGV[1] and
|
|
229
|
+
expires > tonumber(ARGV[2]) then
|
|
230
|
+
redis.call('HSET', KEYS[2], field .. ':ce', ARGV[3])
|
|
231
|
+
renewed[#renewed + 1] = field
|
|
232
|
+
end
|
|
233
|
+
end
|
|
222
234
|
end
|
|
223
235
|
end
|
|
224
|
-
|
|
236
|
+
local result = { tostring(#renewed) }
|
|
237
|
+
for i = 1, #renewed do result[#result + 1] = renewed[i] end
|
|
238
|
+
for i = 1, #superseded do result[#result + 1] = superseded[i] end
|
|
239
|
+
return result
|
|
225
240
|
`;
|
|
226
241
|
/**
|
|
227
|
-
* KEYS:
|
|
242
|
+
* KEYS: store, counter, ids, session index, meta, pending, ready, watched.
|
|
228
243
|
* ARGV: sessionId, nowMs, parent index, attempt, n, then per child:
|
|
229
244
|
* id, type, payloadJson, lane, settled.
|
|
230
245
|
*/
|
|
@@ -360,21 +375,83 @@ end
|
|
|
360
375
|
redis.call('SET', KEYS[1], ARGV[2])
|
|
361
376
|
return 1
|
|
362
377
|
`;
|
|
363
|
-
/**
|
|
378
|
+
/**
|
|
379
|
+
* KEYS: presence hash. ARGV: atMs, ttlMs, n, then per entry:
|
|
380
|
+
* hashField, valueJson ('' deletes). Field-wise LWW by the sender's
|
|
381
|
+
* `at`: a stored entry with a strictly newer stamp survives; ties go
|
|
382
|
+
* to the incoming write (memory-tier parity). Returns the applied
|
|
383
|
+
* 1-based entry positions. The whole-key PEXPIRE is the coarse bound,
|
|
384
|
+
* sound because core sends one uniform ttlMs; the per-entry `e` stamp
|
|
385
|
+
* is the fine bound (the injected storage clock at the applied set
|
|
386
|
+
* plus ttlMs — never the sender stamp), pruned on read against the
|
|
387
|
+
* same clock.
|
|
388
|
+
*/
|
|
389
|
+
const PRESENCE_SET_LUA = `
|
|
390
|
+
local at = tonumber(ARGV[1])
|
|
391
|
+
local applied = {}
|
|
392
|
+
for i = 1, tonumber(ARGV[3]) do
|
|
393
|
+
local field = ARGV[3 + (i - 1) * 2 + 1]
|
|
394
|
+
local value = ARGV[3 + (i - 1) * 2 + 2]
|
|
395
|
+
local existing = redis.call('HGET', KEYS[1], field)
|
|
396
|
+
if not existing or tonumber(cjson.decode(existing)['a']) <= at then
|
|
397
|
+
if value == '' then
|
|
398
|
+
redis.call('HDEL', KEYS[1], field)
|
|
399
|
+
else
|
|
400
|
+
redis.call('HSET', KEYS[1], field, value)
|
|
401
|
+
end
|
|
402
|
+
applied[#applied + 1] = tostring(i)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
if #applied > 0 then redis.call('PEXPIRE', KEYS[1], ARGV[2]) end
|
|
406
|
+
return applied
|
|
407
|
+
`;
|
|
408
|
+
/**
|
|
409
|
+
* KEYS: presence hash. ARGV: nowMs (the injected clock, so tests can
|
|
410
|
+
* time-travel expiry against a real server). Returns the live
|
|
411
|
+
* [field, value, ...] pairs; entries at or past their fine-bound
|
|
412
|
+
* expiry are HDELed in the same atomic step, so a stale reader can
|
|
413
|
+
* never sweep a concurrently refreshed field.
|
|
414
|
+
*/
|
|
415
|
+
const PRESENCE_READ_LUA = `
|
|
416
|
+
local now = tonumber(ARGV[1])
|
|
417
|
+
local rows = redis.call('HGETALL', KEYS[1])
|
|
418
|
+
local live = {}
|
|
419
|
+
for i = 1, #rows, 2 do
|
|
420
|
+
if tonumber(cjson.decode(rows[i + 1])['e']) <= now then
|
|
421
|
+
redis.call('HDEL', KEYS[1], rows[i])
|
|
422
|
+
else
|
|
423
|
+
live[#live + 1] = rows[i]
|
|
424
|
+
live[#live + 1] = rows[i + 1]
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
return live
|
|
428
|
+
`;
|
|
429
|
+
/** KEYS: snapshot, event counter. Returns one atomic cache-plus-frontier read. */
|
|
364
430
|
const READ_STATE_LUA = `
|
|
365
431
|
local snapshot = redis.call('GET', KEYS[1])
|
|
366
|
-
local
|
|
367
|
-
|
|
368
|
-
after = tonumber(cjson.decode(snapshot)['index'])
|
|
369
|
-
end
|
|
370
|
-
local events = redis.call('XRANGE', KEYS[2], tostring(after + 1) .. '-0', '+')
|
|
371
|
-
return { snapshot or '', events }
|
|
432
|
+
local frontier = redis.call('GET', KEYS[2]) or '0'
|
|
433
|
+
return { snapshot or '', frontier }
|
|
372
434
|
`;
|
|
373
435
|
const fieldMap = (fields) => {
|
|
374
436
|
const map = {};
|
|
375
437
|
for (let i = 0; i + 1 < fields.length; i += 2) map[fields[i]] = fields[i + 1];
|
|
376
438
|
return map;
|
|
377
439
|
};
|
|
440
|
+
const STORED_EVENT_META_SUFFIXES = [
|
|
441
|
+
"p",
|
|
442
|
+
"pa",
|
|
443
|
+
"ri",
|
|
444
|
+
"ch",
|
|
445
|
+
"ce",
|
|
446
|
+
"f",
|
|
447
|
+
"fc",
|
|
448
|
+
"lc",
|
|
449
|
+
"lf",
|
|
450
|
+
"lfa",
|
|
451
|
+
"d",
|
|
452
|
+
"a",
|
|
453
|
+
"e"
|
|
454
|
+
];
|
|
378
455
|
const entryIndex = (entryId) => Number(entryId.slice(0, entryId.indexOf("-")));
|
|
379
456
|
const toEvent = (sessionId, entry) => {
|
|
380
457
|
const fields = fieldMap(entry[1]);
|
|
@@ -445,28 +522,43 @@ const wrap = async (fn) => {
|
|
|
445
522
|
return await fn();
|
|
446
523
|
} catch (err) {
|
|
447
524
|
if (err instanceof A2Error || err instanceof TypeError) throw err;
|
|
448
|
-
throw new A2Error("
|
|
525
|
+
throw new A2Error("STORE_UNAVAILABLE", "redis store operation failed", { cause: err });
|
|
449
526
|
}
|
|
450
527
|
};
|
|
451
|
-
function
|
|
528
|
+
function createRedisStoreCore(options) {
|
|
452
529
|
const { call, clock, notify } = options;
|
|
453
530
|
const generateId = options.ids;
|
|
454
531
|
const prefix = options.keyPrefix;
|
|
455
|
-
const
|
|
532
|
+
const storeKey = (s) => `${prefix}:${s}:log`;
|
|
456
533
|
const countKey = (s) => `${prefix}:${s}:count`;
|
|
457
534
|
const metaKey = (s) => `${prefix}:${s}:meta`;
|
|
458
535
|
const pendingKey = (s) => `${prefix}:${s}:pending`;
|
|
459
536
|
const readyKey = (s) => `${prefix}:${s}:ready`;
|
|
460
537
|
const snapKey = (s, r) => `${prefix}:${s}:snap:${r}`;
|
|
461
538
|
const watchedKey = (s) => `${prefix}:${s}:watched`;
|
|
539
|
+
const presenceKey = (s) => `${prefix}:${s}:presence`;
|
|
462
540
|
const idsKey = `${prefix}:ids`;
|
|
463
541
|
const sessionsKey = `${prefix}:sessions`;
|
|
464
542
|
const evalScript = (script, keys, args) => call("EVAL", script, keys.length, ...keys, ...args);
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
543
|
+
const readFrontier = async (sessionId) => {
|
|
544
|
+
const frontier = Number(await call("GET", countKey(sessionId)) ?? 0);
|
|
545
|
+
if (!Number.isSafeInteger(frontier) || frontier < 0) throw new A2Error("STORE_UNAVAILABLE", `redis event count for session '${sessionId}' is invalid`);
|
|
546
|
+
return frontier;
|
|
547
|
+
};
|
|
548
|
+
const readRangePage = async (sessionId, afterIndex, throughIndex, limit) => {
|
|
549
|
+
if (throughIndex <= afterIndex) return [];
|
|
550
|
+
const entries = await call("XRANGE", storeKey(sessionId), `${afterIndex + 1}-0`, `${throughIndex}-0`, "COUNT", limit);
|
|
468
551
|
if (entries.length === 0) return [];
|
|
469
|
-
const
|
|
552
|
+
const metaFields = entries.flatMap((entry) => {
|
|
553
|
+
const index = entryIndex(entry[0]);
|
|
554
|
+
return STORED_EVENT_META_SUFFIXES.map((suffix) => `${index}:${suffix}`);
|
|
555
|
+
});
|
|
556
|
+
const metaValues = await call("HMGET", metaKey(sessionId), ...metaFields) ?? [];
|
|
557
|
+
const meta = {};
|
|
558
|
+
for (let i = 0; i < metaFields.length; i += 1) {
|
|
559
|
+
const value = metaValues[i];
|
|
560
|
+
if (value !== null && value !== void 0) meta[metaFields[i]] = value;
|
|
561
|
+
}
|
|
470
562
|
return entries.map((entry) => {
|
|
471
563
|
const event = toEvent(sessionId, entry);
|
|
472
564
|
const idx = event.index;
|
|
@@ -486,7 +578,7 @@ function createRedisLogCore(options) {
|
|
|
486
578
|
lane: fields["lane"] === "" ? null : fields["lane"],
|
|
487
579
|
processedAt: processedAt === void 0 ? null : new Date(Number(processedAt)),
|
|
488
580
|
processedByAttempt: processedByAttempt === void 0 ? null : Number(processedByAttempt),
|
|
489
|
-
returnedEventIds: returnedEventIds === void 0 ? null :
|
|
581
|
+
returnedEventIds: returnedEventIds === void 0 ? null : decodeReturnedEventIds(returnedEventIds),
|
|
490
582
|
firstClaimedAt: firstClaimedAt === void 0 ? null : new Date(Number(firstClaimedAt)),
|
|
491
583
|
lastClaimedAt: lastClaimedAt === void 0 ? null : new Date(Number(lastClaimedAt)),
|
|
492
584
|
attemptCount: Number(meta[`${idx}:d`] ?? 0),
|
|
@@ -500,6 +592,41 @@ function createRedisLogCore(options) {
|
|
|
500
592
|
});
|
|
501
593
|
});
|
|
502
594
|
};
|
|
595
|
+
const readEventRange = async (sessionId, afterIndex, throughIndex) => {
|
|
596
|
+
if (throughIndex <= afterIndex) return [];
|
|
597
|
+
const events = [];
|
|
598
|
+
let nextIndex = afterIndex + 1;
|
|
599
|
+
while (nextIndex <= throughIndex) {
|
|
600
|
+
const entries = await call("XRANGE", storeKey(sessionId), `${nextIndex}-0`, `${throughIndex}-0`, "COUNT", READ_PAGE_SIZE);
|
|
601
|
+
if (entries.length === 0) throw new A2Error("STORE_UNAVAILABLE", `redis event range for session '${sessionId}' ended before index ${throughIndex}`);
|
|
602
|
+
for (let position = 0; position < entries.length; position += 1) {
|
|
603
|
+
const index = entryIndex(entries[position][0]);
|
|
604
|
+
if (index !== nextIndex + position || index > throughIndex) throw new A2Error("STORE_UNAVAILABLE", `redis event range for session '${sessionId}' is not contiguous`);
|
|
605
|
+
}
|
|
606
|
+
events.push(...entries.map((entry) => toEvent(sessionId, entry)));
|
|
607
|
+
nextIndex = events.at(-1).index + 1;
|
|
608
|
+
}
|
|
609
|
+
return events;
|
|
610
|
+
};
|
|
611
|
+
/** Bounded XRANGE pages restore the full-read contract on capped providers. */
|
|
612
|
+
const readRange = async (sessionId, afterIndex, throughIndex) => {
|
|
613
|
+
if (throughIndex !== void 0 && throughIndex <= afterIndex) return [];
|
|
614
|
+
const currentFrontier = await readFrontier(sessionId);
|
|
615
|
+
const frontier = Math.min(throughIndex ?? currentFrontier, currentFrontier);
|
|
616
|
+
if (frontier <= afterIndex) return [];
|
|
617
|
+
const rows = [];
|
|
618
|
+
let nextIndex = afterIndex + 1;
|
|
619
|
+
while (nextIndex <= frontier) {
|
|
620
|
+
const page = await readRangePage(sessionId, nextIndex - 1, frontier, READ_PAGE_SIZE);
|
|
621
|
+
if (page.length === 0) throw new A2Error("STORE_UNAVAILABLE", `redis event range for session '${sessionId}' ended before index ${frontier}`);
|
|
622
|
+
for (let position = 0; position < page.length; position += 1) if (page[position].index !== nextIndex + position) throw new A2Error("STORE_UNAVAILABLE", `redis event range for session '${sessionId}' is not contiguous`);
|
|
623
|
+
const lastIndex = page.at(-1).index;
|
|
624
|
+
if (lastIndex < nextIndex || lastIndex > frontier) throw new A2Error("STORE_UNAVAILABLE", `redis event range for session '${sessionId}' did not advance`);
|
|
625
|
+
rows.push(...page);
|
|
626
|
+
nextIndex = lastIndex + 1;
|
|
627
|
+
}
|
|
628
|
+
return rows;
|
|
629
|
+
};
|
|
503
630
|
return {
|
|
504
631
|
async append(sessionId, events) {
|
|
505
632
|
return wrap(async () => {
|
|
@@ -528,7 +655,7 @@ function createRedisLogCore(options) {
|
|
|
528
655
|
])
|
|
529
656
|
];
|
|
530
657
|
const reply = await evalScript(APPEND_LUA, [
|
|
531
|
-
|
|
658
|
+
storeKey(sessionId),
|
|
532
659
|
countKey(sessionId),
|
|
533
660
|
idsKey,
|
|
534
661
|
sessionsKey,
|
|
@@ -543,6 +670,8 @@ function createRedisLogCore(options) {
|
|
|
543
670
|
hasPending: reply[1] === "1"
|
|
544
671
|
};
|
|
545
672
|
if (verdict === "foreign") throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${reply[1]}' already exists in another session`);
|
|
673
|
+
if (verdict === "no_parent") throw new TypeError(`no event at index ${reply[1]} in session '${sessionId}'`);
|
|
674
|
+
if (verdict === "superseded") throw new A2Error("SUPERSEDED_ATTEMPT", `a newer attempt owns event ${reply[1]} in session '${sessionId}'`);
|
|
546
675
|
if (verdict === "partial") throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${reply[1]} already-appended and ${events.length - Number(reply[1])} fresh events`);
|
|
547
676
|
if (verdict === "dup") {
|
|
548
677
|
const hasPending = reply[1] === "1";
|
|
@@ -586,7 +715,7 @@ function createRedisLogCore(options) {
|
|
|
586
715
|
},
|
|
587
716
|
async read(sessionId, opts) {
|
|
588
717
|
return wrap(async () => {
|
|
589
|
-
return await readRange(sessionId, opts?.afterIndex ?? 0);
|
|
718
|
+
return await readRange(sessionId, opts?.afterIndex ?? 0, opts?.throughIndex);
|
|
590
719
|
});
|
|
591
720
|
},
|
|
592
721
|
async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs, excludeIndexes }) {
|
|
@@ -595,7 +724,7 @@ function createRedisLogCore(options) {
|
|
|
595
724
|
const expiresAt = expiresAtMs ?? now + ttlMs;
|
|
596
725
|
const excluded = excludeIndexes ?? [];
|
|
597
726
|
const reply = await evalScript(CLAIM_AVAILABLE_LUA, [
|
|
598
|
-
|
|
727
|
+
storeKey(sessionId),
|
|
599
728
|
countKey(sessionId),
|
|
600
729
|
metaKey(sessionId),
|
|
601
730
|
pendingKey(sessionId),
|
|
@@ -610,17 +739,23 @@ function createRedisLogCore(options) {
|
|
|
610
739
|
return toClaimAvailableResult(sessionId, reply);
|
|
611
740
|
});
|
|
612
741
|
},
|
|
613
|
-
async renewClaims({ sessionId, holder,
|
|
742
|
+
async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {
|
|
614
743
|
return wrap(async () => {
|
|
615
744
|
const now = clock.now().getTime();
|
|
616
745
|
const expiresAt = expiresAtMs ?? now + ttlMs;
|
|
617
|
-
|
|
746
|
+
const reply = await evalScript(RENEW_CLAIMS_LUA, [countKey(sessionId), metaKey(sessionId)], [
|
|
618
747
|
holder,
|
|
619
748
|
now,
|
|
620
749
|
expiresAt,
|
|
621
|
-
|
|
622
|
-
...
|
|
623
|
-
])
|
|
750
|
+
claims.length,
|
|
751
|
+
...claims.flatMap((claim) => [claim.index, claim.attempt])
|
|
752
|
+
]);
|
|
753
|
+
const renewedCount = Number(reply[0]);
|
|
754
|
+
const values = reply.slice(1).map(Number);
|
|
755
|
+
return {
|
|
756
|
+
renewed: values.slice(0, renewedCount).toSorted((a, b) => a - b),
|
|
757
|
+
superseded: values.slice(renewedCount).toSorted((a, b) => a - b)
|
|
758
|
+
};
|
|
624
759
|
});
|
|
625
760
|
},
|
|
626
761
|
async completeAttempt({ sessionId, index, attempt, events }) {
|
|
@@ -629,7 +764,7 @@ function createRedisLogCore(options) {
|
|
|
629
764
|
if (new Set(ids).size !== ids.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "batch contains the same event id more than once");
|
|
630
765
|
const now = clock.now().getTime();
|
|
631
766
|
const reply = await evalScript(COMPLETE_ATTEMPT_LUA, [
|
|
632
|
-
|
|
767
|
+
storeKey(sessionId),
|
|
633
768
|
countKey(sessionId),
|
|
634
769
|
idsKey,
|
|
635
770
|
sessionsKey,
|
|
@@ -731,15 +866,18 @@ function createRedisLogCore(options) {
|
|
|
731
866
|
},
|
|
732
867
|
async readState(sessionId, reducerName) {
|
|
733
868
|
return wrap(async () => {
|
|
734
|
-
const reply = await evalScript(READ_STATE_LUA, [snapKey(sessionId, reducerName),
|
|
869
|
+
const reply = await evalScript(READ_STATE_LUA, [snapKey(sessionId, reducerName), countKey(sessionId)], []);
|
|
735
870
|
const raw = reply[0];
|
|
736
871
|
const parsed = raw === "" ? null : JSON.parse(raw);
|
|
872
|
+
const frontier = Number(reply[1]);
|
|
873
|
+
if (!Number.isSafeInteger(frontier) || frontier < 0 || parsed !== null && (!Number.isSafeInteger(parsed.index) || parsed.index < 0)) throw new A2Error("STORE_UNAVAILABLE", `redis state frontier for session '${sessionId}' is invalid`);
|
|
874
|
+
const events = await readEventRange(sessionId, parsed?.index ?? 0, frontier);
|
|
737
875
|
return {
|
|
738
876
|
snapshot: parsed ? {
|
|
739
877
|
index: parsed.index,
|
|
740
878
|
state: parsed.state
|
|
741
879
|
} : null,
|
|
742
|
-
events
|
|
880
|
+
events
|
|
743
881
|
};
|
|
744
882
|
});
|
|
745
883
|
},
|
|
@@ -752,6 +890,58 @@ function createRedisLogCore(options) {
|
|
|
752
890
|
})]);
|
|
753
891
|
});
|
|
754
892
|
},
|
|
893
|
+
presence: {
|
|
894
|
+
async set(ns, participant, values, meta) {
|
|
895
|
+
return wrap(async () => {
|
|
896
|
+
const entries = Object.entries(values);
|
|
897
|
+
if (entries.length === 0) return null;
|
|
898
|
+
const atMs = meta.at.getTime();
|
|
899
|
+
const expiresAtMs = clock.now().getTime() + meta.ttlMs;
|
|
900
|
+
const applied = await evalScript(PRESENCE_SET_LUA, [presenceKey(ns)], [
|
|
901
|
+
atMs,
|
|
902
|
+
meta.ttlMs,
|
|
903
|
+
entries.length,
|
|
904
|
+
...entries.flatMap(([field, value]) => [JSON.stringify([participant, field]), value === null ? "" : json({
|
|
905
|
+
v: value,
|
|
906
|
+
s: meta.seen,
|
|
907
|
+
a: atMs,
|
|
908
|
+
e: expiresAtMs
|
|
909
|
+
})])
|
|
910
|
+
]);
|
|
911
|
+
if (applied.length === 0) return null;
|
|
912
|
+
const appliedValues = nullProtoRecord();
|
|
913
|
+
for (const position of applied) {
|
|
914
|
+
const [field, value] = entries[Number(position) - 1];
|
|
915
|
+
appliedValues[field] = structuredClone(value);
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
participant,
|
|
919
|
+
values: appliedValues,
|
|
920
|
+
seen: meta.seen,
|
|
921
|
+
at: new Date(atMs)
|
|
922
|
+
};
|
|
923
|
+
});
|
|
924
|
+
},
|
|
925
|
+
async read(ns) {
|
|
926
|
+
return wrap(async () => {
|
|
927
|
+
const reply = await evalScript(PRESENCE_READ_LUA, [presenceKey(ns)], [clock.now().getTime()]);
|
|
928
|
+
const rows = [];
|
|
929
|
+
for (let i = 0; i + 1 < reply.length; i += 2) {
|
|
930
|
+
const [participant, field] = JSON.parse(reply[i]);
|
|
931
|
+
const entry = JSON.parse(reply[i + 1]);
|
|
932
|
+
rows.push({
|
|
933
|
+
participant,
|
|
934
|
+
field,
|
|
935
|
+
value: entry.v,
|
|
936
|
+
seen: entry.s,
|
|
937
|
+
at: new Date(entry.a),
|
|
938
|
+
expiresAt: new Date(entry.e)
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
return rows;
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
},
|
|
755
945
|
inspect: {
|
|
756
946
|
async listSessions(inspectionOptions) {
|
|
757
947
|
return wrap(async () => {
|
|
@@ -796,6 +986,19 @@ function createRedisLogCore(options) {
|
|
|
796
986
|
};
|
|
797
987
|
});
|
|
798
988
|
},
|
|
989
|
+
async readEvents(sessionId, pageOptions) {
|
|
990
|
+
return wrap(async () => {
|
|
991
|
+
const currentFrontier = await readFrontier(sessionId);
|
|
992
|
+
const throughIndex = Math.min(pageOptions.throughIndex ?? currentFrontier, currentFrontier);
|
|
993
|
+
const events = await readRangePage(sessionId, pageOptions.afterIndex, throughIndex, pageOptions.limit);
|
|
994
|
+
if (throughIndex > pageOptions.afterIndex && events.length === 0) throw new A2Error("STORE_UNAVAILABLE", `redis event range for session '${sessionId}' ended before index ${throughIndex}`);
|
|
995
|
+
for (let position = 0; position < events.length; position += 1) if (events[position].index !== pageOptions.afterIndex + position + 1) throw new A2Error("STORE_UNAVAILABLE", `redis event range for session '${sessionId}' is not contiguous`);
|
|
996
|
+
return {
|
|
997
|
+
events,
|
|
998
|
+
throughIndex
|
|
999
|
+
};
|
|
1000
|
+
});
|
|
1001
|
+
},
|
|
799
1002
|
async listSnapshots(sessionId) {
|
|
800
1003
|
return wrap(async () => {
|
|
801
1004
|
const start = `${prefix}:${sessionId}:snap:`;
|
|
@@ -819,12 +1022,12 @@ function createRedisLogCore(options) {
|
|
|
819
1022
|
});
|
|
820
1023
|
}
|
|
821
1024
|
},
|
|
822
|
-
async readRange(sessionId, afterIndex) {
|
|
823
|
-
return wrap(() => readRange(sessionId, afterIndex));
|
|
1025
|
+
async readRange(sessionId, afterIndex, throughIndex) {
|
|
1026
|
+
return wrap(() => readRange(sessionId, afterIndex, throughIndex));
|
|
824
1027
|
},
|
|
825
1028
|
async readEvents(sessionId, afterIndex) {
|
|
826
1029
|
return wrap(async () => {
|
|
827
|
-
return (await call("XRANGE",
|
|
1030
|
+
return (await call("XRANGE", storeKey(sessionId), `${afterIndex + 1}-0`, "+")).map((entry) => toEvent(sessionId, entry));
|
|
828
1031
|
});
|
|
829
1032
|
},
|
|
830
1033
|
async markWatched(sessionId, ttlMs) {
|
|
@@ -833,4 +1036,4 @@ function createRedisLogCore(options) {
|
|
|
833
1036
|
};
|
|
834
1037
|
}
|
|
835
1038
|
//#endregion
|
|
836
|
-
export {
|
|
1039
|
+
export { createRedisStoreCore as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as IdSource, i as Clock, t as
|
|
2
|
-
//#region src/
|
|
3
|
-
type
|
|
1
|
+
import { c as IdSource, i as Clock, t as A2Store } from "./store-C3sNAaBT.js";
|
|
2
|
+
//#region src/store-redis-http.d.ts
|
|
3
|
+
type RedisHttpStoreOptions = {
|
|
4
4
|
/** REST endpoint, e.g. `process.env.UPSTASH_REDIS_REST_URL`. */
|
|
5
5
|
url: string;
|
|
6
6
|
/** Bearer token, e.g. `process.env.UPSTASH_REDIS_REST_TOKEN`. */
|
|
@@ -12,10 +12,10 @@ type RedisHttpLogOptions = {
|
|
|
12
12
|
/** Injectable id source for generated event ids. */
|
|
13
13
|
ids?: IdSource;
|
|
14
14
|
};
|
|
15
|
-
type
|
|
16
|
-
/** Nothing to disconnect; present so callers can treat
|
|
15
|
+
type RedisHttpStore = A2Store & {
|
|
16
|
+
/** Nothing to disconnect; present so callers can treat stores uniformly. */
|
|
17
17
|
close(): Promise<void>;
|
|
18
18
|
};
|
|
19
|
-
declare function redisHttp(options:
|
|
19
|
+
declare function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore;
|
|
20
20
|
//#endregion
|
|
21
|
-
export {
|
|
21
|
+
export { RedisHttpStore, RedisHttpStoreOptions, redisHttp };
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import { t as A2Error } from "./errors-
|
|
2
|
-
import { n as
|
|
3
|
-
import { n as
|
|
4
|
-
import { t as
|
|
5
|
-
//#region src/
|
|
1
|
+
import { t as A2Error } from "./errors-BQuJpe82.js";
|
|
2
|
+
import { n as pollingStream } from "./store-polling-DgrrAE3d.js";
|
|
3
|
+
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
|
|
4
|
+
import { t as createRedisStoreCore } from "./store-redis-core-DWqx3F47.js";
|
|
5
|
+
//#region src/store-redis-http.ts
|
|
6
6
|
/**
|
|
7
|
-
* experimental-a2/
|
|
7
|
+
* experimental-a2/store-redis-http — the same Redis Streams store over a
|
|
8
8
|
* provider REST API (Upstash-shaped: POST one command as a JSON array,
|
|
9
9
|
* receive `{ result }` or `{ error }`). The storage semantics live in
|
|
10
|
-
*
|
|
10
|
+
* store-redis-core.ts, shared with experimental-a2/store-redis.
|
|
11
11
|
*
|
|
12
12
|
* Fully connectionless: every command is one `fetch`, so it works
|
|
13
13
|
* where a Redis-protocol connection cannot exist or cannot be spared.
|
|
14
14
|
* Without a connection there is no push and no pub/sub, so `stream()`
|
|
15
15
|
* is the shared activity-adaptive poll loop (25ms while events flow,
|
|
16
|
-
* backing off to a 250ms idle ceiling)
|
|
16
|
+
* backing off to a 250ms idle ceiling), and presence is the degraded
|
|
17
|
+
* tier: `set`/`read` from the shared core, no `subscribe`. No peer
|
|
18
|
+
* dependencies.
|
|
17
19
|
*/
|
|
18
20
|
function redisHttp(options) {
|
|
19
21
|
if (!options.url || !options.token) throw new TypeError("redisHttp() needs a url and a token");
|
|
@@ -30,13 +32,13 @@ function redisHttp(options) {
|
|
|
30
32
|
body: JSON.stringify([command, ...args.map(String)])
|
|
31
33
|
});
|
|
32
34
|
} catch (err) {
|
|
33
|
-
throw new A2Error("
|
|
35
|
+
throw new A2Error("STORE_UNAVAILABLE", "redis rest request failed", { cause: err });
|
|
34
36
|
}
|
|
35
37
|
const body = await response.json().catch(() => null);
|
|
36
|
-
if (body === null || !response.ok || body.error !== void 0) throw new A2Error("
|
|
38
|
+
if (body === null || !response.ok || body.error !== void 0) throw new A2Error("STORE_UNAVAILABLE", `redis rest command failed: ${body?.error ?? `status ${response.status}`}`);
|
|
37
39
|
return body.result;
|
|
38
40
|
};
|
|
39
|
-
const core =
|
|
41
|
+
const core = createRedisStoreCore({
|
|
40
42
|
call,
|
|
41
43
|
clock: options.clock ?? SYSTEM_CLOCK,
|
|
42
44
|
ids: options.ids ?? RANDOM_IDS,
|
|
@@ -51,9 +53,15 @@ function redisHttp(options) {
|
|
|
51
53
|
failAttempt: core.failAttempt,
|
|
52
54
|
readState: core.readState,
|
|
53
55
|
putSnapshot: core.putSnapshot,
|
|
56
|
+
presence: {
|
|
57
|
+
async set(ns, participant, values, meta) {
|
|
58
|
+
await core.presence.set(ns, participant, values, meta);
|
|
59
|
+
},
|
|
60
|
+
read: core.presence.read
|
|
61
|
+
},
|
|
54
62
|
inspect: core.inspect,
|
|
55
63
|
stream(sessionId, opts) {
|
|
56
|
-
return pollingStream((afterIndex) => core.readRange(sessionId, afterIndex), opts?.
|
|
64
|
+
return pollingStream((afterIndex) => core.readRange(sessionId, afterIndex), opts?.startAfter !== void 0 ? { startAfter: opts.startAfter } : {});
|
|
57
65
|
},
|
|
58
66
|
async close() {}
|
|
59
67
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { c as IdSource, i as Clock, t as
|
|
2
|
-
//#region src/
|
|
1
|
+
import { c as IdSource, i as Clock, t as A2Store } from "./store-C3sNAaBT.js";
|
|
2
|
+
//#region src/store-redis.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* The minimal client this backend needs — `ioredis` matches it
|
|
5
5
|
* structurally. `call` issues any command; `duplicate` opens the one
|
|
@@ -16,7 +16,7 @@ type RedisConnection = {
|
|
|
16
16
|
on(event: "message", listener: (channel: string, message: string) => void): unknown;
|
|
17
17
|
disconnect(): void;
|
|
18
18
|
};
|
|
19
|
-
type
|
|
19
|
+
type RedisStoreOptions = {
|
|
20
20
|
/** Creates an `ioredis` client lazily (optional peer dep `ioredis`). */
|
|
21
21
|
url?: string | undefined;
|
|
22
22
|
/** Bring your own client — anything `call`/`duplicate`/`on`/`disconnect`. */
|
|
@@ -28,10 +28,10 @@ type RedisLogOptions = {
|
|
|
28
28
|
/** Injectable id source for generated event ids. */
|
|
29
29
|
ids?: IdSource;
|
|
30
30
|
};
|
|
31
|
-
type
|
|
31
|
+
type RedisStore = A2Store & {
|
|
32
32
|
/** Disconnect the command client and the shared subscriber. */
|
|
33
33
|
close(): Promise<void>;
|
|
34
34
|
};
|
|
35
|
-
declare function redis(options?:
|
|
35
|
+
declare function redis(options?: RedisStoreOptions): RedisStore;
|
|
36
36
|
//#endregion
|
|
37
|
-
export { RedisConnection,
|
|
37
|
+
export { RedisConnection, RedisStore, RedisStoreOptions, redis };
|