experimental-a2 0.10.0 → 0.12.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 (121) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/actor-DJi3RsNu.d.ts +310 -0
  3. package/dist/actor-DJi3RsNu.d.ts.map +1 -0
  4. package/dist/actor-client.d.ts +13 -4
  5. package/dist/actor-client.d.ts.map +1 -1
  6. package/dist/actor-client.js +63 -7
  7. package/dist/actor-client.js.map +1 -1
  8. package/dist/actor-react.d.ts +5 -4
  9. package/dist/actor-react.d.ts.map +1 -1
  10. package/dist/actor-react.js +16 -2
  11. package/dist/actor-react.js.map +1 -1
  12. package/dist/actor.d.ts +2 -176
  13. package/dist/actor.js +13 -2
  14. package/dist/actor.js.map +1 -1
  15. package/dist/ai-server.d.ts +1 -1
  16. package/dist/ai-server.js +2 -2
  17. package/dist/ai.d.ts +1 -1
  18. package/dist/ai.js +1 -1
  19. package/dist/client-Bf6uSEAk.js +1342 -0
  20. package/dist/client-Bf6uSEAk.js.map +1 -0
  21. package/dist/client-P_NNNRM-.d.ts +243 -0
  22. package/dist/client-P_NNNRM-.d.ts.map +1 -0
  23. package/dist/client.d.ts +2 -208
  24. package/dist/client.js +2 -1206
  25. package/dist/errors-DCk6ch5n.js.map +1 -1
  26. package/dist/errors-DvhSXnxk.d.ts +28 -0
  27. package/dist/errors-DvhSXnxk.d.ts.map +1 -0
  28. package/dist/index.d.ts +3 -35
  29. package/dist/{internal-DRXJ56EI.js → internal-Dq2qYxou.js} +2 -2
  30. package/dist/{internal-DRXJ56EI.js.map → internal-Dq2qYxou.js.map} +1 -1
  31. package/dist/platform-B4TnJtWu.js +34 -0
  32. package/dist/platform-B4TnJtWu.js.map +1 -0
  33. package/dist/react.d.ts +3 -1
  34. package/dist/react.d.ts.map +1 -1
  35. package/dist/react.js +2 -1
  36. package/dist/react.js.map +1 -1
  37. package/dist/scheduler-qstash.d.ts +2 -2
  38. package/dist/scheduler-qstash.js +3 -2
  39. package/dist/scheduler-qstash.js.map +1 -1
  40. package/dist/scheduler-vercel.d.ts +2 -2
  41. package/dist/scheduler-vercel.js +2 -2
  42. package/dist/{server-DlLyvaSH.js → server-BeNADlCI.js} +171 -65
  43. package/dist/server-BeNADlCI.js.map +1 -0
  44. package/dist/{server-DgCrSuhB.d.ts → server-DjZZa1wr.d.ts} +9 -3
  45. package/dist/{server-DgCrSuhB.d.ts.map → server-DjZZa1wr.d.ts.map} +1 -1
  46. package/dist/server.d.ts +3 -3
  47. package/dist/server.js +2 -2
  48. package/dist/{store-DGHeBtIQ.d.ts → store-DtDOWLSn.d.ts} +4 -5
  49. package/dist/{store-DGHeBtIQ.d.ts.map → store-DtDOWLSn.d.ts.map} +1 -1
  50. package/dist/store-N8PXxDAS.js.map +1 -1
  51. package/dist/store-memory.d.ts +1 -1
  52. package/dist/store-memory.js +1 -1
  53. package/dist/{store-polling-6DW7F1DT.js → store-polling-CmxUbV93.js} +56 -7
  54. package/dist/store-polling-CmxUbV93.js.map +1 -0
  55. package/dist/store-postgres.d.ts +6 -4
  56. package/dist/store-postgres.d.ts.map +1 -1
  57. package/dist/store-postgres.js +701 -53
  58. package/dist/store-postgres.js.map +1 -1
  59. package/dist/store-presence-polling-C7-XZyW9.js +94 -0
  60. package/dist/store-presence-polling-C7-XZyW9.js.map +1 -0
  61. package/dist/store-redis-http.d.ts +2 -2
  62. package/dist/store-redis-http.d.ts.map +1 -1
  63. package/dist/store-redis-http.js +184 -38
  64. package/dist/store-redis-http.js.map +1 -1
  65. package/dist/{store-redis-core-z-ykbyMg.js → store-redis-notify-BUCyXOn0.js} +491 -27
  66. package/dist/store-redis-notify-BUCyXOn0.js.map +1 -0
  67. package/dist/store-redis.d.ts +5 -13
  68. package/dist/store-redis.d.ts.map +1 -1
  69. package/dist/store-redis.js +27 -271
  70. package/dist/store-redis.js.map +1 -1
  71. package/dist/store-sqlite.d.ts +1 -1
  72. package/dist/store-sqlite.js +1 -1
  73. package/dist/{wire--yji6mO3.js → wire-BO5wWCb1.js} +18 -16
  74. package/dist/wire-BO5wWCb1.js.map +1 -0
  75. package/docs/actors/04-routes.mdx +40 -8
  76. package/docs/guides/03-react.mdx +52 -10
  77. package/docs/guides/05-production.mdx +54 -5
  78. package/docs/guides/09-presence.mdx +12 -7
  79. package/docs/guides/10-transports.mdx +15 -2
  80. package/docs/reference/01-api.mdx +124 -17
  81. package/docs/reference/02-errors.mdx +45 -11
  82. package/examples/playground/package.json +1 -1
  83. package/package.json +1 -1
  84. package/src/actor-client.ts +114 -14
  85. package/src/actor-react.ts +18 -3
  86. package/src/actor.ts +16 -1
  87. package/src/client-errors.ts +51 -0
  88. package/src/client.ts +253 -96
  89. package/src/errors.ts +4 -9
  90. package/src/index.ts +1 -1
  91. package/src/internal.ts +1 -1
  92. package/src/postgres-notification-scope.ts +134 -0
  93. package/src/postgres-notifications.ts +244 -0
  94. package/src/postgres-pool.ts +65 -0
  95. package/src/postgres-resources.ts +185 -0
  96. package/src/presence-recovery.ts +120 -0
  97. package/src/react.ts +3 -0
  98. package/src/redis-http-subscriptions.ts +199 -0
  99. package/src/server.ts +160 -22
  100. package/src/session-socket.ts +25 -7
  101. package/src/sse.ts +17 -0
  102. package/src/store-polling.ts +32 -12
  103. package/src/store-postgres.ts +292 -75
  104. package/src/store-presence-polling.ts +125 -0
  105. package/src/store-redis-core.ts +37 -30
  106. package/src/store-redis-http.ts +51 -45
  107. package/src/store-redis-notify.ts +464 -0
  108. package/src/store-redis.ts +9 -364
  109. package/src/store.ts +3 -4
  110. package/src/stream-activity.ts +32 -0
  111. package/src/wire.ts +39 -15
  112. package/dist/actor-shared-USo5MyuF.d.ts +0 -136
  113. package/dist/actor-shared-USo5MyuF.d.ts.map +0 -1
  114. package/dist/actor.d.ts.map +0 -1
  115. package/dist/client.d.ts.map +0 -1
  116. package/dist/client.js.map +0 -1
  117. package/dist/index.d.ts.map +0 -1
  118. package/dist/server-DlLyvaSH.js.map +0 -1
  119. package/dist/store-polling-6DW7F1DT.js.map +0 -1
  120. package/dist/store-redis-core-z-ykbyMg.js.map +0 -1
  121. package/dist/wire--yji6mO3.js.map +0 -1
@@ -1,15 +1,112 @@
1
1
  import { t as A2Error } from "./errors-DCk6ch5n.js";
2
- import { h as nullProtoRecord } from "./internal-DRXJ56EI.js";
2
+ import { h as nullProtoRecord, r as NOTIFY_TIMINGS } from "./internal-Dq2qYxou.js";
3
+ import { i as streamActivities, n as pollingStream, r as createStreamActivity, t as defaultSleep } from "./store-polling-CmxUbV93.js";
3
4
  import { t as idempotentReplay } from "./idempotent-replay-DVOlyYbx.js";
4
5
  import { t as decodeReturnedEventIds } from "./store-codec-DTG0Ftek.js";
6
+ import { t as pollPresence } from "./store-presence-polling-C7-XZyW9.js";
7
+ //#region src/presence-recovery.ts
8
+ const key = ({ participant, field }) => JSON.stringify([participant, field]);
9
+ function presenceRecovery({ read, deliver, isActive }) {
10
+ const fields = /* @__PURE__ */ new Map();
11
+ const reading = /* @__PURE__ */ new Set();
12
+ let generation = 0;
13
+ let latest;
14
+ const remember = ({ participant, field, seen, at }) => {
15
+ const id = key({
16
+ participant,
17
+ field
18
+ });
19
+ const previous = fields.get(id);
20
+ if (!previous || previous.at.getTime() < at.getTime() || previous.at.getTime() === at.getTime() && previous.seen !== seen) fields.set(id, {
21
+ participant,
22
+ field,
23
+ seen,
24
+ at
25
+ });
26
+ };
27
+ const snapshot = async ({ reconcile }) => {
28
+ const run = generation;
29
+ const before = new Map(fields);
30
+ const changed = /* @__PURE__ */ new Set();
31
+ reading.add(changed);
32
+ try {
33
+ const rows = await read();
34
+ if (!isActive()) return rows;
35
+ if (run !== generation) return latest;
36
+ const live = /* @__PURE__ */ new Set();
37
+ for (const row of rows) {
38
+ const id = key(row);
39
+ live.add(id);
40
+ if (changed.has(id)) continue;
41
+ remember(row);
42
+ if (reconcile) deliver({
43
+ participant: row.participant,
44
+ values: { [row.field]: row.value },
45
+ seen: row.seen,
46
+ at: row.at
47
+ });
48
+ }
49
+ if (reconcile) for (const [id, prior] of before) {
50
+ if (live.has(id) || changed.has(id) || fields.get(id) !== prior) continue;
51
+ fields.delete(id);
52
+ deliver({
53
+ participant: prior.participant,
54
+ values: { [prior.field]: null },
55
+ seen: prior.seen,
56
+ at: prior.at
57
+ });
58
+ }
59
+ return rows;
60
+ } finally {
61
+ reading.delete(changed);
62
+ if (reading.size === 0) latest = void 0;
63
+ }
64
+ };
65
+ return {
66
+ async read() {
67
+ let pending = latest;
68
+ while (pending) {
69
+ await pending;
70
+ if (latest === pending) break;
71
+ pending = latest;
72
+ }
73
+ return snapshot({ reconcile: false });
74
+ },
75
+ async reconnect() {
76
+ generation += 1;
77
+ latest = snapshot({ reconcile: true });
78
+ await latest;
79
+ },
80
+ update(patch) {
81
+ for (const [field, value] of Object.entries(patch.values)) {
82
+ if (value === void 0) continue;
83
+ const id = key({
84
+ participant: patch.participant,
85
+ field
86
+ });
87
+ const previous = fields.get(id);
88
+ if (previous && previous.at.getTime() > patch.at.getTime()) continue;
89
+ for (const changed of reading) changed.add(id);
90
+ if (value === null) fields.delete(id);
91
+ else remember({
92
+ participant: patch.participant,
93
+ field,
94
+ seen: patch.seen,
95
+ at: patch.at
96
+ });
97
+ }
98
+ }
99
+ };
100
+ }
101
+ //#endregion
5
102
  //#region src/store-redis-core.ts
6
103
  const READ_PAGE_SIZE = 256;
7
104
  /**
8
105
  * KEYS: store stream, counter, global id hash, metadata, pending, ready,
9
106
  * watched.
10
107
  * ARGV: sessionId, nowMs, n, then per event:
11
- * id, type, payloadJson, causeJson, lane, settled.
12
- * Returns ['empty', hasPending] | ['ok', firstIndex, hasPending, watched] |
108
+ * id, type, payloadJson, causeJson, lane, settled; finally notify channel.
109
+ * Returns ['empty', hasPending] | ['ok', firstIndex, hasPending] |
13
110
  * ['dup', hasPending, ...indexes] | ['partial', count] |
14
111
  * ['foreign', eventId] | ['no_parent', idx] | ['superseded', idx].
15
112
  */
@@ -90,8 +187,10 @@ for i = 1, n do
90
187
  end
91
188
  redis.call('SET', KEYS[2], tostring(base + n))
92
189
  has_pending = redis.call('ZCARD', KEYS[5]) > 0 and '1' or '0'
93
- return { 'ok', tostring(base + 1), has_pending,
94
- tostring(redis.call('EXISTS', KEYS[7])) }
190
+ if redis.call('EXISTS', KEYS[7]) == 1 then
191
+ redis.pcall('PUBLISH', ARGV[4 + n * 6], tostring(base + n))
192
+ end
193
+ return { 'ok', tostring(base + 1), has_pending }
95
194
  `;
96
195
  /**
97
196
  * KEYS: counter, meta, pending, ready, event snapshot pins, snapshot pin counts.
@@ -258,7 +357,7 @@ return result
258
357
  * KEYS: store, counter, ids, meta, pending, ready, watched,
259
358
  * event snapshot pins, snapshot pin counts.
260
359
  * ARGV: sessionId, nowMs, parent index, attempt, n, then per child:
261
- * id, type, payloadJson, lane, settled.
360
+ * id, type, payloadJson, lane, settled; finally notify channel.
262
361
  */
263
362
  const COMPLETE_ATTEMPT_LUA = `
264
363
  local function release_snapshot_pins()
@@ -393,8 +492,10 @@ for i = 1, n do
393
492
  end
394
493
  end
395
494
  redis.call('SET', KEYS[2], tostring(total + n))
396
- return { 'ok', tostring(total + 1),
397
- tostring(redis.call('EXISTS', KEYS[7])) }
495
+ if n > 0 and redis.call('EXISTS', KEYS[7]) == 1 then
496
+ redis.pcall('PUBLISH', ARGV[6 + n * 5], tostring(total + n))
497
+ end
498
+ return { 'ok', tostring(total + 1) }
398
499
  `;
399
500
  /**
400
501
  * KEYS: head, history, event counter, event meta, pin counts.
@@ -455,7 +556,9 @@ return 1
455
556
  `;
456
557
  /**
457
558
  * KEYS: presence hash. ARGV: atMs, ttlMs, n, then per entry:
458
- * hashField, valueJson ('' deletes). Field-wise LWW by the sender's
559
+ * hashField, valueJson ('' deletes), patchFieldJson; finally channel and
560
+ * message prefix. Wire fragments stay JSON text to preserve arrays
561
+ * and numeric precision through Lua. Field-wise LWW by the sender's
459
562
  * `at`: a stored entry with a strictly newer stamp survives; ties go
460
563
  * to the incoming write (memory-tier parity). Returns the applied
461
564
  * 1-based entry positions. The whole-key PEXPIRE is the coarse bound,
@@ -467,9 +570,11 @@ return 1
467
570
  const PRESENCE_SET_LUA = `
468
571
  local at = tonumber(ARGV[1])
469
572
  local applied = {}
470
- for i = 1, tonumber(ARGV[3]) do
471
- local field = ARGV[3 + (i - 1) * 2 + 1]
472
- local value = ARGV[3 + (i - 1) * 2 + 2]
573
+ local published = {}
574
+ local n = tonumber(ARGV[3])
575
+ for i = 1, n do
576
+ local field = ARGV[3 + (i - 1) * 3 + 1]
577
+ local value = ARGV[3 + (i - 1) * 3 + 2]
473
578
  local existing = redis.call('HGET', KEYS[1], field)
474
579
  if not existing or tonumber(cjson.decode(existing)['a']) <= at then
475
580
  if value == '' then
@@ -478,9 +583,13 @@ for i = 1, tonumber(ARGV[3]) do
478
583
  redis.call('HSET', KEYS[1], field, value)
479
584
  end
480
585
  applied[#applied + 1] = tostring(i)
586
+ published[#published + 1] = ARGV[3 + (i - 1) * 3 + 3]
481
587
  end
482
588
  end
483
- if #applied > 0 then redis.call('PEXPIRE', KEYS[1], ARGV[2]) end
589
+ if #applied > 0 then
590
+ redis.call('PEXPIRE', KEYS[1], ARGV[2])
591
+ redis.pcall('PUBLISH', ARGV[4 + n * 3], ARGV[5 + n * 3] .. table.concat(published, ',') .. '}}')
592
+ end
484
593
  return applied
485
594
  `;
486
595
  /**
@@ -618,7 +727,7 @@ const wrap = async (fn) => {
618
727
  }
619
728
  };
620
729
  function createRedisStoreCore(options) {
621
- const { call, clock, notify } = options;
730
+ const { call, clock } = options;
622
731
  const generateId = options.ids;
623
732
  const prefix = options.keyPrefix;
624
733
  const storeKey = (s) => `${prefix}:${s}:log`;
@@ -747,7 +856,8 @@ function createRedisStoreCore(options) {
747
856
  e.cause ? json(e.cause) : "",
748
857
  e.lane ?? "",
749
858
  e.settled ? "1" : "0"
750
- ])
859
+ ]),
860
+ `${prefix}:${sessionId}:notify`
751
861
  ];
752
862
  const reply = await evalScript(APPEND_LUA, [
753
863
  storeKey(sessionId),
@@ -778,7 +888,6 @@ function createRedisStoreCore(options) {
778
888
  };
779
889
  }
780
890
  const base = Number(reply[1]) - 1;
781
- if (reply[3] === "1") notify?.(sessionId, base + withIds.length);
782
891
  return {
783
892
  events: withIds.map((e, i) => ({
784
893
  id: e.id,
@@ -879,7 +988,8 @@ function createRedisStoreCore(options) {
879
988
  json(event.payload),
880
989
  event.lane ?? "",
881
990
  event.settled === true ? "1" : "0"
882
- ])
991
+ ]),
992
+ `${prefix}:${sessionId}:notify`
883
993
  ]);
884
994
  const outcome = reply[0];
885
995
  if (outcome === "missing") throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
@@ -891,7 +1001,6 @@ function createRedisStoreCore(options) {
891
1001
  };
892
1002
  if (outcome === "ok") {
893
1003
  const base = Number(reply[1]) - 1;
894
- if (events.length > 0 && reply[2] === "1") notify?.(sessionId, base + events.length);
895
1004
  return {
896
1005
  outcome: "completed",
897
1006
  events: events.map((event, i) => ({
@@ -1033,12 +1142,23 @@ function createRedisStoreCore(options) {
1033
1142
  atMs,
1034
1143
  meta.ttlMs,
1035
1144
  entries.length,
1036
- ...entries.flatMap(([field, value]) => [JSON.stringify([participant, field]), value === null ? "" : json({
1037
- v: value,
1038
- s: meta.seen,
1039
- a: atMs,
1040
- e: expiresAtMs
1041
- })])
1145
+ ...entries.flatMap(([field, value]) => [
1146
+ JSON.stringify([participant, field]),
1147
+ value === null ? "" : json({
1148
+ v: value,
1149
+ s: meta.seen,
1150
+ a: atMs,
1151
+ e: expiresAtMs
1152
+ }),
1153
+ `${json(field)}:${json(value)}`
1154
+ ]),
1155
+ presenceKey(ns),
1156
+ `${json({
1157
+ src: options.presenceSource,
1158
+ participant,
1159
+ seen: meta.seen,
1160
+ at: atMs
1161
+ }).slice(0, -1)},"values":{`
1042
1162
  ]);
1043
1163
  if (applied.length === 0) return null;
1044
1164
  const appliedValues = nullProtoRecord();
@@ -1079,7 +1199,7 @@ function createRedisStoreCore(options) {
1079
1199
  },
1080
1200
  async readEvents(sessionId, afterIndex) {
1081
1201
  return wrap(async () => {
1082
- return (await call("XRANGE", storeKey(sessionId), `${afterIndex + 1}-0`, "+")).map((entry) => toEvent(sessionId, entry));
1202
+ return (await call("XRANGE", storeKey(sessionId), `${afterIndex + 1}-0`, "+", "COUNT", READ_PAGE_SIZE)).map((entry) => toEvent(sessionId, entry));
1083
1203
  });
1084
1204
  },
1085
1205
  async markWatched(sessionId, ttlMs) {
@@ -1088,6 +1208,350 @@ function createRedisStoreCore(options) {
1088
1208
  };
1089
1209
  }
1090
1210
  //#endregion
1091
- export { createRedisStoreCore as t };
1211
+ //#region src/store-redis-notify.ts
1212
+ function createRedisNotifyStore(options) {
1213
+ const { client, clock, ids, keyPrefix: prefix } = options;
1214
+ const notifyChannel = (sessionId) => `${prefix}:${sessionId}:notify`;
1215
+ const presenceChannel = (ns) => `${prefix}:${ns}:presence`;
1216
+ const instanceId = crypto.randomUUID();
1217
+ const core = createRedisStoreCore({
1218
+ call: async (command, ...args) => {
1219
+ return (await client()).call(command, ...args);
1220
+ },
1221
+ clock,
1222
+ ids,
1223
+ keyPrefix: prefix,
1224
+ presenceSource: instanceId
1225
+ });
1226
+ let severed = false;
1227
+ let subscriber = null;
1228
+ /** channel → parked-feed wake-ups, armed before each catch-up read. */
1229
+ const wakers = /* @__PURE__ */ new Map();
1230
+ /** channel → refcounted subscription held for each live iterator. */
1231
+ const subscriptions = /* @__PURE__ */ new Map();
1232
+ /**
1233
+ * channel → last presence-marker refresh, throttling the SET.
1234
+ * Deliberately real time (`Date.now()`), not the injected clock:
1235
+ * the marker's PX expiry runs on server real time, and the throttle
1236
+ * must tick with the TTL it refreshes or time-traveling tests would
1237
+ * desync the two. Process-local and never stored, so the injected
1238
+ * clock's every-stored-timestamp pledge is untouched.
1239
+ */
1240
+ const watched = /* @__PURE__ */ new Map();
1241
+ const presenceSubscriptions = /* @__PURE__ */ new Map();
1242
+ const pollingStreams = /* @__PURE__ */ new Set();
1243
+ options.onReconnect?.((channel) => {
1244
+ for (const wake of wakers.get(channel) ?? []) wake();
1245
+ presenceSubscriptions.get(channel)?.recovery?.reconnect().catch(() => {});
1246
+ });
1247
+ const deliverRemotePresence = ({ subscription, message }) => {
1248
+ let parsed;
1249
+ try {
1250
+ parsed = JSON.parse(message);
1251
+ } catch {
1252
+ return;
1253
+ }
1254
+ if (parsed.src === instanceId) return;
1255
+ const patch = {
1256
+ participant: parsed.participant,
1257
+ values: parsed.values,
1258
+ seen: parsed.seen,
1259
+ at: new Date(parsed.at)
1260
+ };
1261
+ subscription.recovery?.update(patch);
1262
+ for (const listener of subscription.listeners) listener(patch);
1263
+ };
1264
+ const getSubscriber = () => {
1265
+ subscriber ??= (async () => {
1266
+ const conn = (await client()).duplicate();
1267
+ conn.on("message", (channel, message) => {
1268
+ const set = wakers.get(channel);
1269
+ if (set) for (const wake of set) wake();
1270
+ const subscription = presenceSubscriptions.get(channel);
1271
+ if (subscription) deliverRemotePresence({
1272
+ subscription,
1273
+ message
1274
+ });
1275
+ });
1276
+ return conn;
1277
+ })();
1278
+ return subscriber;
1279
+ };
1280
+ /**
1281
+ * The subscriber, gated on connection readiness — for channel
1282
+ * commands only (`close` uses the ungated promise, so it can never
1283
+ * hang on an unreachable server). SUBSCRIBE carries redis's
1284
+ * ok-loading flag, so ioredis writes it mid-handshake, ahead of the
1285
+ * ready check's INFO — which then finds the connection already in
1286
+ * subscriber mode and fails the check (a spurious error event plus
1287
+ * a reconnect). ECHO has no such flag: it resolves only once the
1288
+ * connection is ready, so every channel command chained behind it
1289
+ * lands after the check.
1290
+ */
1291
+ let subscriberGate = null;
1292
+ const gatedSubscriber = () => {
1293
+ subscriberGate ??= getSubscriber().then(async (conn) => {
1294
+ await conn.call("echo", "ready");
1295
+ return conn;
1296
+ });
1297
+ return subscriberGate;
1298
+ };
1299
+ const acquireChannel = async (channel) => {
1300
+ const existing = subscriptions.get(channel);
1301
+ if (existing) {
1302
+ existing.refs += 1;
1303
+ try {
1304
+ await existing.ready;
1305
+ } catch (err) {
1306
+ existing.refs -= 1;
1307
+ throw err;
1308
+ }
1309
+ return;
1310
+ }
1311
+ const lease = {
1312
+ refs: 1,
1313
+ ready: gatedSubscriber().then(async (conn) => {
1314
+ await conn.call("subscribe", channel);
1315
+ if (subscriptions.get(channel) !== lease || severed) return;
1316
+ const presence = presenceSubscriptions.get(channel);
1317
+ if (options.polling?.() && presence) {
1318
+ presence.recovery = void 0;
1319
+ const ns = channel.slice(prefix.length + 1, -9);
1320
+ presence.poll = pollPresence({
1321
+ read: () => core.presence.read(ns),
1322
+ deliver: (patch) => {
1323
+ for (const listener of presence.listeners) listener(patch);
1324
+ }
1325
+ });
1326
+ await presence.poll.ready;
1327
+ }
1328
+ })
1329
+ };
1330
+ subscriptions.set(channel, lease);
1331
+ try {
1332
+ await lease.ready;
1333
+ } catch (err) {
1334
+ if (subscriptions.get(channel) === lease) {
1335
+ subscriptions.delete(channel);
1336
+ const presence = presenceSubscriptions.get(channel);
1337
+ presence?.poll?.stop();
1338
+ if (presence) presence.poll = void 0;
1339
+ }
1340
+ throw err;
1341
+ }
1342
+ };
1343
+ const releaseChannel = (channel) => {
1344
+ const lease = subscriptions.get(channel);
1345
+ if (!lease) return;
1346
+ lease.refs -= 1;
1347
+ if (lease.refs > 0) return;
1348
+ subscriptions.delete(channel);
1349
+ watched.delete(channel);
1350
+ gatedSubscriber().then((conn) => conn.call("unsubscribe", channel)).catch(() => {});
1351
+ };
1352
+ const armWaker = (channel) => {
1353
+ let wake;
1354
+ const wakeup = new Promise((resolve) => {
1355
+ wake = resolve;
1356
+ });
1357
+ let set = wakers.get(channel);
1358
+ if (!set) {
1359
+ set = /* @__PURE__ */ new Set();
1360
+ wakers.set(channel, set);
1361
+ }
1362
+ set.add(wake);
1363
+ return {
1364
+ wakeup,
1365
+ wake,
1366
+ disarm: () => {
1367
+ set.delete(wake);
1368
+ if (set.size === 0) wakers.delete(channel);
1369
+ }
1370
+ };
1371
+ };
1372
+ return {
1373
+ append: core.append,
1374
+ read: core.read,
1375
+ claimAvailable: core.claimAvailable,
1376
+ renewClaims: core.renewClaims,
1377
+ completeAttempt: core.completeAttempt,
1378
+ failAttempt: core.failAttempt,
1379
+ readState: core.readState,
1380
+ putSnapshots: core.putSnapshots,
1381
+ presence: {
1382
+ async set(ns, participant, values, meta) {
1383
+ const patch = await core.presence.set(ns, participant, values, meta);
1384
+ if (!patch) return;
1385
+ const channel = presenceChannel(ns);
1386
+ const subscription = presenceSubscriptions.get(channel);
1387
+ if (subscription) {
1388
+ subscription.poll?.update(patch);
1389
+ subscription.recovery?.update(patch);
1390
+ for (const listener of subscription.listeners) listener(patch);
1391
+ }
1392
+ },
1393
+ async read(ns) {
1394
+ const channel = presenceChannel(ns);
1395
+ await subscriptions.get(channel)?.ready;
1396
+ return presenceSubscriptions.get(channel)?.recovery?.read() ?? core.presence.read(ns);
1397
+ },
1398
+ subscribe(ns, onPatch) {
1399
+ const channel = presenceChannel(ns);
1400
+ let subscription = presenceSubscriptions.get(channel);
1401
+ if (!subscription) {
1402
+ subscription = { listeners: /* @__PURE__ */ new Set() };
1403
+ presenceSubscriptions.set(channel, subscription);
1404
+ if (options.onReconnect && !options.polling?.()) {
1405
+ const current = subscription;
1406
+ subscription.recovery = presenceRecovery({
1407
+ read: () => core.presence.read(ns),
1408
+ deliver: (patch) => {
1409
+ for (const listener of current.listeners) listener(patch);
1410
+ },
1411
+ isActive: () => !severed && presenceSubscriptions.get(channel) === current
1412
+ });
1413
+ }
1414
+ }
1415
+ subscription.listeners.add(onPatch);
1416
+ acquireChannel(channel).catch(() => {});
1417
+ let stopped = false;
1418
+ return () => {
1419
+ if (stopped) return;
1420
+ stopped = true;
1421
+ subscription.listeners.delete(onPatch);
1422
+ if (subscription.listeners.size === 0) {
1423
+ subscription.poll?.stop();
1424
+ presenceSubscriptions.delete(channel);
1425
+ }
1426
+ releaseChannel(channel);
1427
+ };
1428
+ }
1429
+ },
1430
+ stream(sessionId, opts) {
1431
+ const startAfter = opts?.startAfter ?? 0;
1432
+ const channel = notifyChannel(sessionId);
1433
+ return { [Symbol.asyncIterator]() {
1434
+ let last = startAfter;
1435
+ let buffer = [];
1436
+ let closed = false;
1437
+ let subscribed = false;
1438
+ let pending = null;
1439
+ let interrupt = null;
1440
+ let poller;
1441
+ let stopActivity;
1442
+ const activity = createStreamActivity(() => {
1443
+ if (poller) streamActivities.get(poller)?.activate();
1444
+ });
1445
+ const release = () => {
1446
+ stopActivity?.();
1447
+ if (poller) {
1448
+ poller.return?.();
1449
+ pollingStreams.delete(poller);
1450
+ }
1451
+ if (!subscribed) return;
1452
+ subscribed = false;
1453
+ releaseChannel(channel);
1454
+ };
1455
+ const iterator = {
1456
+ async next() {
1457
+ for (;;) {
1458
+ if (closed) {
1459
+ release();
1460
+ return {
1461
+ value: void 0,
1462
+ done: true
1463
+ };
1464
+ }
1465
+ const row = buffer.shift();
1466
+ if (row) {
1467
+ last = row.index;
1468
+ return {
1469
+ value: row,
1470
+ done: false
1471
+ };
1472
+ }
1473
+ if (severed) {
1474
+ release();
1475
+ throw new A2Error("STORE_UNAVAILABLE", "the store was closed");
1476
+ }
1477
+ const waker = armWaker(channel);
1478
+ interrupt = waker.wake;
1479
+ try {
1480
+ if (!subscribed) {
1481
+ subscribed = true;
1482
+ await acquireChannel(channel);
1483
+ if (closed || severed) continue;
1484
+ }
1485
+ if (options.polling?.()) {
1486
+ if (!poller) {
1487
+ poller = pollingStream((after) => core.readEvents(sessionId, after), { startAfter: last })[Symbol.asyncIterator]();
1488
+ pollingStreams.add(poller);
1489
+ stopActivity = streamActivities.get(poller)?.subscribe(activity.setIdle);
1490
+ }
1491
+ const result = await poller.next();
1492
+ if (!result.done) presenceSubscriptions.get(presenceChannel(sessionId))?.poll?.wake();
1493
+ if (severed) throw new A2Error("STORE_UNAVAILABLE", "the store was closed");
1494
+ return result;
1495
+ }
1496
+ let marker = watched.get(channel);
1497
+ if (!marker || Date.now() - marker.at >= NOTIFY_TIMINGS.safetyReadMs) {
1498
+ marker = {
1499
+ at: Date.now(),
1500
+ ready: core.markWatched(sessionId, NOTIFY_TIMINGS.safetyReadMs * 3)
1501
+ };
1502
+ watched.set(channel, marker);
1503
+ }
1504
+ await marker.ready;
1505
+ if (closed || severed) continue;
1506
+ buffer = await core.readEvents(sessionId, last);
1507
+ if (buffer.length === 0 && !closed && !severed) {
1508
+ pending = defaultSleep(NOTIFY_TIMINGS.safetyReadMs);
1509
+ await Promise.race([waker.wakeup, pending.promise]);
1510
+ pending.cancel();
1511
+ pending = null;
1512
+ }
1513
+ } catch (err) {
1514
+ release();
1515
+ if (closed) return {
1516
+ value: void 0,
1517
+ done: true
1518
+ };
1519
+ if (err instanceof A2Error) throw err;
1520
+ throw new A2Error("STORE_UNAVAILABLE", "redis store operation failed", { cause: err });
1521
+ } finally {
1522
+ waker.disarm();
1523
+ interrupt = null;
1524
+ }
1525
+ }
1526
+ },
1527
+ async return() {
1528
+ closed = true;
1529
+ pending?.cancel();
1530
+ interrupt?.();
1531
+ release();
1532
+ return {
1533
+ value: void 0,
1534
+ done: true
1535
+ };
1536
+ }
1537
+ };
1538
+ streamActivities.set(iterator, activity);
1539
+ return iterator;
1540
+ } };
1541
+ },
1542
+ async close() {
1543
+ severed = true;
1544
+ for (const subscription of presenceSubscriptions.values()) subscription.poll?.stop();
1545
+ presenceSubscriptions.clear();
1546
+ for (const poller of pollingStreams) poller.return?.();
1547
+ pollingStreams.clear();
1548
+ for (const set of wakers.values()) for (const wake of set) wake();
1549
+ if (subscriber) (await subscriber.catch(() => null))?.disconnect();
1550
+ await options.closeClient();
1551
+ }
1552
+ };
1553
+ }
1554
+ //#endregion
1555
+ export { createRedisNotifyStore as t };
1092
1556
 
1093
- //# sourceMappingURL=store-redis-core-z-ykbyMg.js.map
1557
+ //# sourceMappingURL=store-redis-notify-BUCyXOn0.js.map