experimental-a2 0.6.0 → 0.8.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.
Files changed (148) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/ai-server.d.ts +4 -5
  3. package/dist/ai-server.d.ts.map +1 -1
  4. package/dist/ai-server.js +20 -17
  5. package/dist/ai-server.js.map +1 -1
  6. package/dist/ai.d.ts +334 -2
  7. package/dist/ai.d.ts.map +1 -0
  8. package/dist/ai.js +1 -1
  9. package/dist/client.d.ts +202 -2
  10. package/dist/client.d.ts.map +1 -0
  11. package/dist/client.js +1025 -1
  12. package/dist/client.js.map +1 -0
  13. package/dist/errors-BQuJpe82.js.map +1 -1
  14. package/dist/index.d.ts +22 -3
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
  17. package/dist/internal-DRXJ56EI.js.map +1 -0
  18. package/dist/react.d.ts +1 -1
  19. package/dist/react.js +1 -1
  20. package/dist/scheduler-qstash.d.ts +3 -3
  21. package/dist/scheduler-qstash.js +4 -5
  22. package/dist/scheduler-qstash.js.map +1 -1
  23. package/dist/scheduler-vercel.d.ts +2 -2
  24. package/dist/scheduler-vercel.js +4 -4
  25. package/dist/scheduler-vercel.js.map +1 -1
  26. package/dist/{server-Duw6MVlB.js → server-B2XNevQA.js} +830 -131
  27. package/dist/server-B2XNevQA.js.map +1 -0
  28. package/dist/{server-DpvjhdoE.d.ts → server-DjPhHnbI.d.ts} +71 -50
  29. package/dist/server-DjPhHnbI.d.ts.map +1 -0
  30. package/dist/server.d.ts +3 -3
  31. package/dist/server.js +1 -1
  32. package/dist/store-N8PXxDAS.js.map +1 -1
  33. package/dist/{store-DysUkTH3.d.ts → store-RJO35BMj.d.ts} +24 -62
  34. package/dist/store-RJO35BMj.d.ts.map +1 -0
  35. package/dist/store-memory.d.ts +1 -1
  36. package/dist/store-memory.d.ts.map +1 -1
  37. package/dist/store-memory.js +80 -78
  38. package/dist/store-memory.js.map +1 -1
  39. package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
  40. package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
  41. package/dist/store-postgres.d.ts +1 -1
  42. package/dist/store-postgres.d.ts.map +1 -1
  43. package/dist/store-postgres.js +231 -182
  44. package/dist/store-postgres.js.map +1 -1
  45. package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DT01r4GZ.js} +213 -161
  46. package/dist/store-redis-core-DT01r4GZ.js.map +1 -0
  47. package/dist/store-redis-http.d.ts +1 -1
  48. package/dist/store-redis-http.js +3 -4
  49. package/dist/store-redis-http.js.map +1 -1
  50. package/dist/store-redis.d.ts +1 -1
  51. package/dist/store-redis.js +4 -5
  52. package/dist/store-redis.js.map +1 -1
  53. package/dist/store-sqlite.d.ts +1 -1
  54. package/dist/store-sqlite.d.ts.map +1 -1
  55. package/dist/store-sqlite.js +104 -91
  56. package/dist/store-sqlite.js.map +1 -1
  57. package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
  58. package/dist/wire-B6te_wns.js.map +1 -0
  59. package/docs/concepts/02-handlers.mdx +4 -0
  60. package/docs/concepts/04-state.mdx +57 -9
  61. package/docs/guides/03-react.mdx +20 -28
  62. package/docs/guides/05-production.mdx +9 -11
  63. package/docs/guides/06-ai-agents.mdx +10 -15
  64. package/docs/guides/09-presence.mdx +14 -19
  65. package/docs/guides/10-transports.mdx +104 -86
  66. package/docs/reference/01-api.mdx +182 -293
  67. package/docs/reference/02-errors.mdx +5 -7
  68. package/package.json +1 -14
  69. package/src/ai-server.ts +36 -15
  70. package/src/client.ts +2 -2
  71. package/src/errors.ts +1 -0
  72. package/src/internal.ts +3 -62
  73. package/src/push-envelope.ts +24 -21
  74. package/src/scheduler-qstash.ts +3 -3
  75. package/src/scheduler-vercel.ts +2 -2
  76. package/src/server-fetch.ts +344 -0
  77. package/src/server.ts +315 -312
  78. package/src/session-socket.ts +36 -20
  79. package/src/sse.ts +2 -2
  80. package/src/store-memory.ts +138 -101
  81. package/src/store-postgres.ts +355 -238
  82. package/src/store-redis-core.ts +247 -237
  83. package/src/store-redis-http.ts +1 -2
  84. package/src/store-redis.ts +1 -2
  85. package/src/store-sqlite.ts +191 -153
  86. package/src/store.ts +24 -66
  87. package/src/wire.ts +2 -1
  88. package/dist/ai-D_PGS-JR.d.ts +0 -334
  89. package/dist/ai-D_PGS-JR.d.ts.map +0 -1
  90. package/dist/cli-B3VuxoDe.js +0 -599
  91. package/dist/cli-B3VuxoDe.js.map +0 -1
  92. package/dist/cli-bin.d.ts +0 -1
  93. package/dist/cli-bin.js +0 -7
  94. package/dist/cli-bin.js.map +0 -1
  95. package/dist/cli.d.ts +0 -20
  96. package/dist/cli.d.ts.map +0 -1
  97. package/dist/cli.js +0 -2
  98. package/dist/client-CdMqi7mC.d.ts +0 -202
  99. package/dist/client-CdMqi7mC.d.ts.map +0 -1
  100. package/dist/client-Dj5d3SP_.js +0 -1026
  101. package/dist/client-Dj5d3SP_.js.map +0 -1
  102. package/dist/devtools-J_jZ2vQf.d.ts +0 -152
  103. package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
  104. package/dist/devtools-kJJaORn-.js +0 -340
  105. package/dist/devtools-kJJaORn-.js.map +0 -1
  106. package/dist/devtools-server.browser.d.ts +0 -1
  107. package/dist/devtools-server.browser.js +0 -6
  108. package/dist/devtools-server.browser.js.map +0 -1
  109. package/dist/devtools-server.d.ts +0 -23
  110. package/dist/devtools-server.d.ts.map +0 -1
  111. package/dist/devtools-server.js +0 -1270
  112. package/dist/devtools-server.js.map +0 -1
  113. package/dist/devtools.d.ts +0 -2
  114. package/dist/devtools.js +0 -2
  115. package/dist/errors-W6nwJ-fm.d.ts +0 -21
  116. package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
  117. package/dist/http.d.ts +0 -151
  118. package/dist/http.d.ts.map +0 -1
  119. package/dist/http.js +0 -706
  120. package/dist/http.js.map +0 -1
  121. package/dist/inspection-DaxB5jM2.js +0 -13
  122. package/dist/inspection-DaxB5jM2.js.map +0 -1
  123. package/dist/internal-DstsI6Re.js.map +0 -1
  124. package/dist/platform-B4TnJtWu.js +0 -34
  125. package/dist/platform-B4TnJtWu.js.map +0 -1
  126. package/dist/server-DpvjhdoE.d.ts.map +0 -1
  127. package/dist/server-Duw6MVlB.js.map +0 -1
  128. package/dist/store-DysUkTH3.d.ts.map +0 -1
  129. package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
  130. package/dist/testing.browser.d.ts +0 -1
  131. package/dist/testing.browser.js +0 -6
  132. package/dist/testing.browser.js.map +0 -1
  133. package/dist/testing.d.ts +0 -32
  134. package/dist/testing.d.ts.map +0 -1
  135. package/dist/testing.js +0 -103
  136. package/dist/testing.js.map +0 -1
  137. package/dist/wire-BFQmSJ-9.js.map +0 -1
  138. package/docs/guides/07-devtools.mdx +0 -229
  139. package/src/cli-bin.ts +0 -5
  140. package/src/cli.ts +0 -1046
  141. package/src/devtools-app.ts +0 -989
  142. package/src/devtools-server.browser.ts +0 -5
  143. package/src/devtools-server.ts +0 -604
  144. package/src/devtools.ts +0 -716
  145. package/src/http.ts +0 -394
  146. package/src/inspection.ts +0 -39
  147. package/src/testing.browser.ts +0 -5
  148. package/src/testing.ts +0 -185
@@ -1,12 +1,12 @@
1
1
  import { t as A2Error } from "./errors-BQuJpe82.js";
2
- import { g as nullProtoRecord } from "./internal-DstsI6Re.js";
2
+ import { h as nullProtoRecord } from "./internal-DRXJ56EI.js";
3
3
  import { t as idempotentReplay } from "./idempotent-replay-DuqEkYA7.js";
4
4
  import { t as decodeReturnedEventIds } from "./store-codec-DTG0Ftek.js";
5
5
  //#region src/store-redis-core.ts
6
6
  const READ_PAGE_SIZE = 256;
7
7
  /**
8
- * KEYS: store stream, counter, global id hash, session index, metadata,
9
- * pending, ready, watched.
8
+ * KEYS: store stream, counter, global id hash, metadata, pending, ready,
9
+ * watched.
10
10
  * ARGV: sessionId, nowMs, n, then per event:
11
11
  * id, type, payloadJson, causeJson, lane, settled.
12
12
  * Returns ['empty', hasPending] | ['ok', firstIndex, hasPending, watched] |
@@ -18,24 +18,24 @@ local function lane_field(lane, suffix)
18
18
  return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix
19
19
  end
20
20
  local function enqueue(field, idx, lane)
21
- redis.call('ZADD', KEYS[6], idx, field)
21
+ redis.call('ZADD', KEYS[5], idx, field)
22
22
  if lane == '' then
23
- redis.call('ZADD', KEYS[7], idx, field)
23
+ redis.call('ZADD', KEYS[6], idx, field)
24
24
  return
25
25
  end
26
26
  local tail_key = lane_field(lane, 'tail')
27
- local tail = redis.call('HGET', KEYS[5], tail_key)
27
+ local tail = redis.call('HGET', KEYS[4], tail_key)
28
28
  if tail then
29
- redis.call('HSET', KEYS[5], tail .. ':next', field)
29
+ redis.call('HSET', KEYS[4], tail .. ':next', field)
30
30
  else
31
- redis.call('HSET', KEYS[5], lane_field(lane, 'head'), field)
32
- redis.call('ZADD', KEYS[7], idx, field)
31
+ redis.call('HSET', KEYS[4], lane_field(lane, 'head'), field)
32
+ redis.call('ZADD', KEYS[6], idx, field)
33
33
  end
34
- redis.call('HSET', KEYS[5], tail_key, field)
34
+ redis.call('HSET', KEYS[4], tail_key, field)
35
35
  end
36
36
  local n = tonumber(ARGV[3])
37
37
  local base = tonumber(redis.call('GET', KEYS[2]) or '0')
38
- local has_pending = redis.call('ZCARD', KEYS[6]) > 0 and '1' or '0'
38
+ local has_pending = redis.call('ZCARD', KEYS[5]) > 0 and '1' or '0'
39
39
  if n == 0 then return { 'empty', has_pending } end
40
40
  local dups = {}
41
41
  for i = 1, n do
@@ -62,9 +62,9 @@ for i = 1, n do
62
62
  return { 'no_parent', tostring(parsed.index) }
63
63
  end
64
64
  local field = tostring(pidx)
65
- local dispatches = tonumber(redis.call('HGET', KEYS[5], field .. ':d') or '0')
65
+ local dispatches = tonumber(redis.call('HGET', KEYS[4], field .. ':d') or '0')
66
66
  if dispatches ~= tonumber(parsed.attempt) or
67
- redis.call('HEXISTS', KEYS[5], field .. ':f') == 1 then
67
+ redis.call('HEXISTS', KEYS[4], field .. ':f') == 1 then
68
68
  return { 'superseded', field }
69
69
  end
70
70
  end
@@ -81,21 +81,36 @@ for i = 1, n do
81
81
  'id', id, 'type', typ, 'payload', payload, 'created_at', ARGV[2],
82
82
  'cause', cause, 'lane', lane)
83
83
  redis.call('HSET', KEYS[3], id, cjson.encode({ ARGV[1], idx }))
84
- redis.call('HSET', KEYS[5], tostring(idx) .. ':l', lane)
84
+ redis.call('HSET', KEYS[4], tostring(idx) .. ':l', lane)
85
85
  if settled == '1' then
86
- redis.call('HSET', KEYS[5], tostring(idx) .. ':p', ARGV[2])
86
+ redis.call('HSET', KEYS[4], tostring(idx) .. ':p', ARGV[2])
87
87
  else
88
88
  enqueue(tostring(idx), idx, lane)
89
89
  end
90
90
  end
91
91
  redis.call('SET', KEYS[2], tostring(base + n))
92
- redis.call('ZADD', KEYS[4], 0, ARGV[1])
93
- has_pending = redis.call('ZCARD', KEYS[6]) > 0 and '1' or '0'
92
+ has_pending = redis.call('ZCARD', KEYS[5]) > 0 and '1' or '0'
94
93
  return { 'ok', tostring(base + 1), has_pending,
95
- tostring(redis.call('EXISTS', KEYS[8])) }
94
+ tostring(redis.call('EXISTS', KEYS[7])) }
96
95
  `;
97
- /** KEYS: counter, meta, pending, ready. ARGV: idx, attempt, error, maxFailures, nowMs. */
96
+ /**
97
+ * KEYS: counter, meta, pending, ready, event snapshot pins, snapshot pin counts.
98
+ * ARGV: idx, attempt, error, maxFailures, nowMs.
99
+ */
98
100
  const FAIL_ATTEMPT_LUA = `
101
+ local function release_snapshot_pins()
102
+ local refs = redis.call('SMEMBERS', KEYS[5])
103
+ for _, ref in ipairs(refs) do
104
+ local count = redis.call('HINCRBY', KEYS[6], ref, -1)
105
+ if count <= 0 then
106
+ redis.call('HDEL', KEYS[6], ref)
107
+ local checkpoint = cjson.decode(ref)
108
+ redis.call('ZREMRANGEBYSCORE', checkpoint[1], checkpoint[2], checkpoint[2])
109
+ end
110
+ end
111
+ redis.call('DEL', KEYS[5])
112
+ if redis.call('HLEN', KEYS[6]) == 0 then redis.call('DEL', KEYS[6]) end
113
+ end
99
114
  local function lane_field(lane, suffix)
100
115
  return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix
101
116
  end
@@ -149,6 +164,7 @@ redis.call('HDEL', KEYS[2], field .. ':ch', field .. ':ce')
149
164
  if failures >= tonumber(ARGV[4]) then
150
165
  redis.call('HSET', KEYS[2], field .. ':f', ARGV[5])
151
166
  redis.call('ZREM', KEYS[4], field)
167
+ release_snapshot_pins()
152
168
  return { 'dead_lettered', tostring(failures) }
153
169
  end
154
170
  return { 'failed', tostring(failures) }
@@ -239,61 +255,75 @@ for i = 1, #superseded do result[#result + 1] = superseded[i] end
239
255
  return result
240
256
  `;
241
257
  /**
242
- * KEYS: store, counter, ids, session index, meta, pending, ready, watched.
258
+ * KEYS: store, counter, ids, meta, pending, ready, watched,
259
+ * event snapshot pins, snapshot pin counts.
243
260
  * ARGV: sessionId, nowMs, parent index, attempt, n, then per child:
244
261
  * id, type, payloadJson, lane, settled.
245
262
  */
246
263
  const COMPLETE_ATTEMPT_LUA = `
264
+ local function release_snapshot_pins()
265
+ local refs = redis.call('SMEMBERS', KEYS[8])
266
+ for _, ref in ipairs(refs) do
267
+ local count = redis.call('HINCRBY', KEYS[9], ref, -1)
268
+ if count <= 0 then
269
+ redis.call('HDEL', KEYS[9], ref)
270
+ local checkpoint = cjson.decode(ref)
271
+ redis.call('ZREMRANGEBYSCORE', checkpoint[1], checkpoint[2], checkpoint[2])
272
+ end
273
+ end
274
+ redis.call('DEL', KEYS[8])
275
+ if redis.call('HLEN', KEYS[9]) == 0 then redis.call('DEL', KEYS[9]) end
276
+ end
247
277
  local function lane_field(lane, suffix)
248
278
  return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix
249
279
  end
250
280
  local function settle(field)
281
+ redis.call('ZREM', KEYS[5], field)
251
282
  redis.call('ZREM', KEYS[6], field)
252
- redis.call('ZREM', KEYS[7], field)
253
- local lane = redis.call('HGET', KEYS[5], field .. ':l') or ''
283
+ local lane = redis.call('HGET', KEYS[4], field .. ':l') or ''
254
284
  if lane == '' then return end
255
285
  local head_key = lane_field(lane, 'head')
256
- if redis.call('HGET', KEYS[5], head_key) ~= field then return end
257
- local next_field = redis.call('HGET', KEYS[5], field .. ':next')
258
- while next_field and redis.call('HEXISTS', KEYS[5], next_field .. ':p') == 1 do
259
- next_field = redis.call('HGET', KEYS[5], next_field .. ':next')
286
+ if redis.call('HGET', KEYS[4], head_key) ~= field then return end
287
+ local next_field = redis.call('HGET', KEYS[4], field .. ':next')
288
+ while next_field and redis.call('HEXISTS', KEYS[4], next_field .. ':p') == 1 do
289
+ next_field = redis.call('HGET', KEYS[4], next_field .. ':next')
260
290
  end
261
291
  if next_field then
262
- redis.call('HSET', KEYS[5], head_key, next_field)
263
- if redis.call('HEXISTS', KEYS[5], next_field .. ':f') == 0 then
264
- redis.call('ZADD', KEYS[7], tonumber(next_field), next_field)
292
+ redis.call('HSET', KEYS[4], head_key, next_field)
293
+ if redis.call('HEXISTS', KEYS[4], next_field .. ':f') == 0 then
294
+ redis.call('ZADD', KEYS[6], tonumber(next_field), next_field)
265
295
  end
266
296
  else
267
- redis.call('HDEL', KEYS[5], head_key, lane_field(lane, 'tail'))
297
+ redis.call('HDEL', KEYS[4], head_key, lane_field(lane, 'tail'))
268
298
  end
269
299
  end
270
300
  local function enqueue(field, idx, lane)
271
- redis.call('ZADD', KEYS[6], idx, field)
301
+ redis.call('ZADD', KEYS[5], idx, field)
272
302
  if lane == '' then
273
- redis.call('ZADD', KEYS[7], idx, field)
303
+ redis.call('ZADD', KEYS[6], idx, field)
274
304
  return
275
305
  end
276
306
  local tail_key = lane_field(lane, 'tail')
277
- local tail = redis.call('HGET', KEYS[5], tail_key)
307
+ local tail = redis.call('HGET', KEYS[4], tail_key)
278
308
  if tail then
279
- redis.call('HSET', KEYS[5], tail .. ':next', field)
309
+ redis.call('HSET', KEYS[4], tail .. ':next', field)
280
310
  else
281
- redis.call('HSET', KEYS[5], lane_field(lane, 'head'), field)
282
- redis.call('ZADD', KEYS[7], idx, field)
311
+ redis.call('HSET', KEYS[4], lane_field(lane, 'head'), field)
312
+ redis.call('ZADD', KEYS[6], idx, field)
283
313
  end
284
- redis.call('HSET', KEYS[5], tail_key, field)
314
+ redis.call('HSET', KEYS[4], tail_key, field)
285
315
  end
286
316
  local parent = ARGV[3]
287
317
  local attempt = tonumber(ARGV[4])
288
318
  local n = tonumber(ARGV[5])
289
319
  local total = tonumber(redis.call('GET', KEYS[2]) or '0')
290
320
  if tonumber(parent) < 1 or tonumber(parent) > total then return { 'missing' } end
291
- local dispatches = tonumber(redis.call('HGET', KEYS[5], parent .. ':d') or '0')
292
- local processed = redis.call('HGET', KEYS[5], parent .. ':p')
293
- local processed_attempt = tonumber(redis.call('HGET', KEYS[5], parent .. ':pa') or '0')
321
+ local dispatches = tonumber(redis.call('HGET', KEYS[4], parent .. ':d') or '0')
322
+ local processed = redis.call('HGET', KEYS[4], parent .. ':p')
323
+ local processed_attempt = tonumber(redis.call('HGET', KEYS[4], parent .. ':pa') or '0')
294
324
  if processed then
295
325
  if processed_attempt ~= attempt then return { 'superseded' } end
296
- local raw_recorded_ids = redis.call('HGET', KEYS[5], parent .. ':ri')
326
+ local raw_recorded_ids = redis.call('HGET', KEYS[4], parent .. ':ri')
297
327
  if not raw_recorded_ids then return { 'partial' } end
298
328
  local recorded_ids = cjson.decode(raw_recorded_ids)
299
329
  if #recorded_ids ~= n then return { 'partial' } end
@@ -323,9 +353,9 @@ if processed then
323
353
  return result
324
354
  end
325
355
  if dispatches ~= attempt then return { 'superseded' } end
326
- if redis.call('HEXISTS', KEYS[5], parent .. ':ch') == 0 or
327
- redis.call('HEXISTS', KEYS[5], parent .. ':f') == 1 or
328
- tonumber(redis.call('HGET', KEYS[5], parent .. ':lfa') or '0') == attempt then
356
+ if redis.call('HEXISTS', KEYS[4], parent .. ':ch') == 0 or
357
+ redis.call('HEXISTS', KEYS[4], parent .. ':f') == 1 or
358
+ tonumber(redis.call('HGET', KEYS[4], parent .. ':lfa') or '0') == attempt then
329
359
  return { 'superseded' }
330
360
  end
331
361
  for i = 1, n do
@@ -336,12 +366,13 @@ local returned_ids = {}
336
366
  for i = 1, n do returned_ids[i] = ARGV[5 + (i - 1) * 5 + 1] end
337
367
  local returned_json = '[]'
338
368
  if n > 0 then returned_json = cjson.encode(returned_ids) end
339
- redis.call('HSET', KEYS[5],
369
+ redis.call('HSET', KEYS[4],
340
370
  parent .. ':p', ARGV[2],
341
371
  parent .. ':pa', ARGV[4],
342
372
  parent .. ':ri', returned_json)
343
- redis.call('HDEL', KEYS[5], parent .. ':ch', parent .. ':ce')
373
+ redis.call('HDEL', KEYS[4], parent .. ':ch', parent .. ':ce')
344
374
  settle(parent)
375
+ release_snapshot_pins()
345
376
  local cause = cjson.encode({ index = tonumber(parent), attempt = attempt })
346
377
  for i = 1, n do
347
378
  local id = ARGV[5 + (i - 1) * 5 + 1]
@@ -354,25 +385,72 @@ for i = 1, n do
354
385
  'id', id, 'type', typ, 'payload', payload, 'created_at', ARGV[2],
355
386
  'cause', cause, 'lane', lane)
356
387
  redis.call('HSET', KEYS[3], id, cjson.encode({ ARGV[1], idx }))
357
- redis.call('HSET', KEYS[5], tostring(idx) .. ':l', lane)
388
+ redis.call('HSET', KEYS[4], tostring(idx) .. ':l', lane)
358
389
  if settled == '1' then
359
- redis.call('HSET', KEYS[5], tostring(idx) .. ':p', ARGV[2])
390
+ redis.call('HSET', KEYS[4], tostring(idx) .. ':p', ARGV[2])
360
391
  else
361
392
  enqueue(tostring(idx), idx, lane)
362
393
  end
363
394
  end
364
395
  redis.call('SET', KEYS[2], tostring(total + n))
365
- redis.call('ZADD', KEYS[4], 0, ARGV[1])
366
396
  return { 'ok', tostring(total + 1),
367
- tostring(redis.call('EXISTS', KEYS[8])) }
397
+ tostring(redis.call('EXISTS', KEYS[7])) }
368
398
  `;
369
- /** KEYS: snapshot. ARGV: upToIndex, json. Guarded: never move backward. */
370
- const PUT_SNAPSHOT_LUA = `
371
- local cur = redis.call('GET', KEYS[1])
372
- if cur and tonumber(cjson.decode(cur)['index']) >= tonumber(ARGV[1]) then
373
- return 0
399
+ /**
400
+ * KEYS: head, history, event counter, event meta, pin counts.
401
+ * ARGV: nowMs, event pin key prefix, write count, then per write:
402
+ * index, snapshot JSON, pin count, pin event indexes.
403
+ */
404
+ const PUT_SNAPSHOTS_LUA = `
405
+ local total = tonumber(redis.call('GET', KEYS[3]) or '0')
406
+ local writes = {}
407
+ local offset = 4
408
+ for i = 1, tonumber(ARGV[3]) do
409
+ local index = tonumber(ARGV[offset])
410
+ local snapshot = ARGV[offset + 1]
411
+ local pin_count = tonumber(ARGV[offset + 2])
412
+ local ref = cjson.encode({ KEYS[2], tostring(index) })
413
+ for p = 1, pin_count do
414
+ local event_index = tonumber(ARGV[offset + 2 + p])
415
+ if event_index >= 1 and event_index <= total and
416
+ redis.call('HEXISTS', KEYS[4], tostring(event_index) .. ':p') == 0 and
417
+ redis.call('HEXISTS', KEYS[4], tostring(event_index) .. ':f') == 0 then
418
+ local pin_key = ARGV[2] .. tostring(event_index)
419
+ if redis.call('SADD', pin_key, ref) == 1 then
420
+ redis.call('HINCRBY', KEYS[5], ref, 1)
421
+ end
422
+ end
423
+ end
424
+ writes[#writes + 1] = { index = index, snapshot = snapshot, ref = ref }
425
+ offset = offset + 3 + pin_count
426
+ end
427
+
428
+ local head = redis.call('GET', KEYS[1])
429
+ local head_index = nil
430
+ if head then head_index = tonumber(cjson.decode(head)['index']) end
431
+ local latest = writes[#writes]
432
+ local writes_head = not head_index or latest.index >= head_index
433
+ local advances = not head_index or latest.index > head_index
434
+ local final_head_index = advances and latest.index or head_index
435
+
436
+ if advances and head and tonumber(redis.call('HGET', KEYS[5], cjson.encode({ KEYS[2], tostring(head_index) })) or '0') > 0 then
437
+ redis.call('ZREMRANGEBYSCORE', KEYS[2], head_index, head_index)
438
+ redis.call('ZADD', KEYS[2], head_index, head)
439
+ end
440
+
441
+ for _, write in ipairs(writes) do
442
+ if write.index < final_head_index then
443
+ redis.call('ZREMRANGEBYSCORE', KEYS[2], write.index, write.index)
444
+ if tonumber(redis.call('HGET', KEYS[5], write.ref) or '0') > 0 then
445
+ redis.call('ZADD', KEYS[2], write.index, write.snapshot)
446
+ end
447
+ end
448
+ end
449
+
450
+ if writes_head then
451
+ redis.call('SET', KEYS[1], latest.snapshot)
452
+ redis.call('ZREMRANGEBYSCORE', KEYS[2], latest.index, latest.index)
374
453
  end
375
- redis.call('SET', KEYS[1], ARGV[2])
376
454
  return 1
377
455
  `;
378
456
  /**
@@ -426,11 +504,26 @@ for i = 1, #rows, 2 do
426
504
  end
427
505
  return live
428
506
  `;
429
- /** KEYS: snapshot, event counter. Returns one atomic cache-plus-frontier read. */
507
+ /**
508
+ * KEYS: head, history, event counter. ARGV: snapshot frontier or ''.
509
+ * Returns one atomic multiversion-cache-plus-log-frontier read.
510
+ */
430
511
  const READ_STATE_LUA = `
431
- local snapshot = redis.call('GET', KEYS[1])
432
- local frontier = redis.call('GET', KEYS[2]) or '0'
433
- return { snapshot or '', frontier }
512
+ local head = redis.call('GET', KEYS[1])
513
+ local cap = ARGV[1]
514
+ local snapshot = nil
515
+ if head and (cap == '' or tonumber(cjson.decode(head)['index']) <= tonumber(cap)) then
516
+ snapshot = head
517
+ end
518
+ local upper = cap == '' and '+inf' or cap
519
+ local historical = redis.call('ZREVRANGEBYSCORE', KEYS[2], upper, '-inf', 'LIMIT', 0, 1)
520
+ if #historical > 0 then
521
+ if not snapshot or tonumber(cjson.decode(historical[1])['index']) > tonumber(cjson.decode(snapshot)['index']) then
522
+ snapshot = historical[1]
523
+ end
524
+ end
525
+ local frontier = redis.call('GET', KEYS[3]) or '0'
526
+ return { head or '', snapshot or '', frontier }
434
527
  `;
435
528
  const fieldMap = (fields) => {
436
529
  const map = {};
@@ -465,7 +558,6 @@ const toEvent = (sessionId, entry) => {
465
558
  };
466
559
  };
467
560
  const json = (value) => JSON.stringify(value) ?? "null";
468
- const escapeGlob = (value) => value.replaceAll("\\", "\\\\").replaceAll("*", "\\*").replaceAll("?", "\\?").replaceAll("[", "\\[").replaceAll("]", "\\]");
469
561
  const toCause = (raw) => {
470
562
  if (raw === "" || raw === void 0) return null;
471
563
  const value = JSON.parse(raw);
@@ -535,10 +627,13 @@ function createRedisStoreCore(options) {
535
627
  const pendingKey = (s) => `${prefix}:${s}:pending`;
536
628
  const readyKey = (s) => `${prefix}:${s}:ready`;
537
629
  const snapKey = (s, r) => `${prefix}:${s}:snap:${r}`;
630
+ const snapHistoryKey = (s, r) => `${prefix}:${s}:snap-history:${r}`;
631
+ const snapshotEventPinsKey = (s, index) => `${prefix}:${s}:snapshot-pins:${index}`;
632
+ const snapshotEventPinsPrefix = (s) => `${prefix}:${s}:snapshot-pins:`;
633
+ const snapshotPinCountsKey = (s) => `${prefix}:${s}:snapshot-pin-counts`;
538
634
  const watchedKey = (s) => `${prefix}:${s}:watched`;
539
635
  const presenceKey = (s) => `${prefix}:${s}:presence`;
540
636
  const idsKey = `${prefix}:ids`;
541
- const sessionsKey = `${prefix}:sessions`;
542
637
  const evalScript = (script, keys, args) => call("EVAL", script, keys.length, ...keys, ...args);
543
638
  const readFrontier = async (sessionId) => {
544
639
  const frontier = Number(await call("GET", countKey(sessionId)) ?? 0);
@@ -658,7 +753,6 @@ function createRedisStoreCore(options) {
658
753
  storeKey(sessionId),
659
754
  countKey(sessionId),
660
755
  idsKey,
661
- sessionsKey,
662
756
  metaKey(sessionId),
663
757
  pendingKey(sessionId),
664
758
  readyKey(sessionId),
@@ -767,11 +861,12 @@ function createRedisStoreCore(options) {
767
861
  storeKey(sessionId),
768
862
  countKey(sessionId),
769
863
  idsKey,
770
- sessionsKey,
771
864
  metaKey(sessionId),
772
865
  pendingKey(sessionId),
773
866
  readyKey(sessionId),
774
- watchedKey(sessionId)
867
+ watchedKey(sessionId),
868
+ snapshotEventPinsKey(sessionId, index),
869
+ snapshotPinCountsKey(sessionId)
775
870
  ], [
776
871
  sessionId,
777
872
  now,
@@ -848,7 +943,9 @@ function createRedisStoreCore(options) {
848
943
  countKey(sessionId),
849
944
  metaKey(sessionId),
850
945
  pendingKey(sessionId),
851
- readyKey(sessionId)
946
+ readyKey(sessionId),
947
+ snapshotEventPinsKey(sessionId, index),
948
+ snapshotPinCountsKey(sessionId)
852
949
  ], [
853
950
  index,
854
951
  attempt,
@@ -864,30 +961,65 @@ function createRedisStoreCore(options) {
864
961
  };
865
962
  });
866
963
  },
867
- async readState(sessionId, reducerName) {
964
+ async readState(sessionId, reducerName, stateOptions) {
868
965
  return wrap(async () => {
869
- const reply = await evalScript(READ_STATE_LUA, [snapKey(sessionId, reducerName), countKey(sessionId)], []);
870
- const raw = reply[0];
966
+ const reply = await evalScript(READ_STATE_LUA, [
967
+ snapKey(sessionId, reducerName),
968
+ snapHistoryKey(sessionId, reducerName),
969
+ countKey(sessionId)
970
+ ], [stateOptions?.snapshotThroughIndex ?? stateOptions?.throughIndex ?? ""]);
971
+ const rawHead = reply[0];
972
+ const raw = reply[1];
973
+ const head = rawHead === "" ? null : JSON.parse(rawHead);
871
974
  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);
975
+ const currentFrontier = Number(reply[2]);
976
+ if (!Number.isSafeInteger(currentFrontier) || currentFrontier < 0 || head !== null && (!Number.isSafeInteger(head.index) || head.index < 0) || parsed !== null && (!Number.isSafeInteger(parsed.index) || parsed.index < 0)) throw new A2Error("STORE_UNAVAILABLE", `redis state frontier for session '${sessionId}' is invalid`);
977
+ const frontier = Math.min(currentFrontier, stateOptions?.throughIndex ?? currentFrontier);
978
+ const snapshot = parsed;
979
+ const events = await readEventRange(sessionId, snapshot?.index ?? 0, frontier);
875
980
  return {
876
- snapshot: parsed ? {
877
- index: parsed.index,
878
- state: parsed.state
981
+ headIndex: head?.index ?? null,
982
+ snapshot: snapshot ? {
983
+ index: snapshot.index,
984
+ state: snapshot.state
879
985
  } : null,
880
986
  events
881
987
  };
882
988
  });
883
989
  },
884
- async putSnapshot(sessionId, reducerName, index, state) {
990
+ async putSnapshots(sessionId, reducerName, writes) {
991
+ const byIndex = /* @__PURE__ */ new Map();
992
+ for (const write of writes) {
993
+ const existing = byIndex.get(write.index);
994
+ if (existing) {
995
+ existing.state = write.state;
996
+ for (const eventIndex of write.pinEventIndexes ?? []) existing.pinEventIndexes.add(eventIndex);
997
+ } else byIndex.set(write.index, {
998
+ index: write.index,
999
+ state: write.state,
1000
+ pinEventIndexes: new Set(write.pinEventIndexes ?? [])
1001
+ });
1002
+ }
1003
+ const normalized = [...byIndex.values()].toSorted((a, b) => a.index - b.index);
1004
+ if (normalized.length === 0) return;
885
1005
  await wrap(async () => {
886
- await evalScript(PUT_SNAPSHOT_LUA, [snapKey(sessionId, reducerName)], [index, json({
887
- index,
888
- state,
1006
+ const args = [
1007
+ clock.now().getTime(),
1008
+ snapshotEventPinsPrefix(sessionId),
1009
+ normalized.length
1010
+ ];
1011
+ for (const write of normalized) args.push(write.index, json({
1012
+ index: write.index,
1013
+ state: write.state,
889
1014
  updatedAt: clock.now().getTime()
890
- })]);
1015
+ }), write.pinEventIndexes.size, ...write.pinEventIndexes);
1016
+ await evalScript(PUT_SNAPSHOTS_LUA, [
1017
+ snapKey(sessionId, reducerName),
1018
+ snapHistoryKey(sessionId, reducerName),
1019
+ countKey(sessionId),
1020
+ metaKey(sessionId),
1021
+ snapshotPinCountsKey(sessionId)
1022
+ ], args);
891
1023
  });
892
1024
  },
893
1025
  presence: {
@@ -942,86 +1074,6 @@ function createRedisStoreCore(options) {
942
1074
  });
943
1075
  }
944
1076
  },
945
- inspect: {
946
- async listSessions(inspectionOptions) {
947
- return wrap(async () => {
948
- const minimum = inspectionOptions.cursor === void 0 ? `[${inspectionOptions.prefix}` : `(${inspectionOptions.cursor}`;
949
- const candidates = await call("ZRANGEBYLEX", sessionsKey, minimum, "+", "LIMIT", 0, inspectionOptions.limit + 1);
950
- const sessionIds = candidates.filter((id) => id.startsWith(inspectionOptions.prefix)).slice(0, inspectionOptions.limit);
951
- return {
952
- sessions: (await Promise.all(sessionIds.map(async (sessionId) => {
953
- const rows = await readRange(sessionId, 0);
954
- const first = rows[0];
955
- let updatedAt = first.createdAt;
956
- let pendingCount = 0;
957
- let failedCount = 0;
958
- let attemptCount = 0;
959
- let failureCount = 0;
960
- for (const row of rows) {
961
- attemptCount += row.attemptCount;
962
- failureCount += row.failureCount;
963
- if (row.processedAt === null && row.failedAt === null) pendingCount += 1;
964
- if (row.failedAt !== null) failedCount += 1;
965
- for (const timestamp of [
966
- row.createdAt,
967
- row.firstClaimedAt,
968
- row.lastClaimedAt,
969
- row.lastFailedAt,
970
- row.processedAt,
971
- row.failedAt
972
- ]) if (timestamp && timestamp > updatedAt) updatedAt = timestamp;
973
- }
974
- return {
975
- sessionId,
976
- eventCount: rows.length,
977
- pendingCount,
978
- failedCount,
979
- attemptCount,
980
- failureCount,
981
- firstEventAt: first.createdAt,
982
- updatedAt
983
- };
984
- }))).toSorted((a, b) => a.sessionId.localeCompare(b.sessionId)),
985
- cursor: candidates.length > inspectionOptions.limit && candidates[inspectionOptions.limit]?.startsWith(inspectionOptions.prefix) ? sessionIds.at(-1) ?? null : null
986
- };
987
- });
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
- },
1002
- async listSnapshots(sessionId) {
1003
- return wrap(async () => {
1004
- const start = `${prefix}:${sessionId}:snap:`;
1005
- let cursor = "0";
1006
- const keys = [];
1007
- do {
1008
- const reply = await call("SCAN", cursor, "MATCH", `${escapeGlob(start)}*`, "COUNT", 100);
1009
- cursor = reply[0];
1010
- keys.push(...reply[1].filter((key) => key.startsWith(start)));
1011
- } while (cursor !== "0");
1012
- return (await Promise.all(keys.map(async (key) => {
1013
- const raw = await call("GET", key);
1014
- if (raw === null) return null;
1015
- const parsed = JSON.parse(raw);
1016
- return {
1017
- reducerName: key.slice(start.length),
1018
- index: parsed.index,
1019
- updatedAt: new Date(parsed.updatedAt ?? 0)
1020
- };
1021
- }))).filter((snapshot) => snapshot !== null).toSorted((a, b) => a.reducerName.localeCompare(b.reducerName));
1022
- });
1023
- }
1024
- },
1025
1077
  async readRange(sessionId, afterIndex, throughIndex) {
1026
1078
  return wrap(() => readRange(sessionId, afterIndex, throughIndex));
1027
1079
  },
@@ -1038,4 +1090,4 @@ function createRedisStoreCore(options) {
1038
1090
  //#endregion
1039
1091
  export { createRedisStoreCore as t };
1040
1092
 
1041
- //# sourceMappingURL=store-redis-core-BFLwz0Wj.js.map
1093
+ //# sourceMappingURL=store-redis-core-DT01r4GZ.js.map