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.
- package/CHANGELOG.md +45 -0
- package/dist/actor-DJi3RsNu.d.ts +310 -0
- package/dist/actor-DJi3RsNu.d.ts.map +1 -0
- package/dist/actor-client.d.ts +13 -4
- package/dist/actor-client.d.ts.map +1 -1
- package/dist/actor-client.js +63 -7
- package/dist/actor-client.js.map +1 -1
- package/dist/actor-react.d.ts +5 -4
- package/dist/actor-react.d.ts.map +1 -1
- package/dist/actor-react.js +16 -2
- package/dist/actor-react.js.map +1 -1
- package/dist/actor.d.ts +2 -176
- package/dist/actor.js +13 -2
- package/dist/actor.js.map +1 -1
- package/dist/ai-server.d.ts +1 -1
- package/dist/ai-server.js +2 -2
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/client-Bf6uSEAk.js +1342 -0
- package/dist/client-Bf6uSEAk.js.map +1 -0
- package/dist/client-P_NNNRM-.d.ts +243 -0
- package/dist/client-P_NNNRM-.d.ts.map +1 -0
- package/dist/client.d.ts +2 -208
- package/dist/client.js +2 -1206
- package/dist/errors-DCk6ch5n.js.map +1 -1
- package/dist/errors-DvhSXnxk.d.ts +28 -0
- package/dist/errors-DvhSXnxk.d.ts.map +1 -0
- package/dist/index.d.ts +3 -35
- package/dist/{internal-DRXJ56EI.js → internal-Dq2qYxou.js} +2 -2
- package/dist/{internal-DRXJ56EI.js.map → internal-Dq2qYxou.js.map} +1 -1
- package/dist/platform-B4TnJtWu.js +34 -0
- package/dist/platform-B4TnJtWu.js.map +1 -0
- package/dist/react.d.ts +3 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +2 -1
- package/dist/react.js.map +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -2
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +2 -2
- package/dist/{server-DlLyvaSH.js → server-BeNADlCI.js} +171 -65
- package/dist/server-BeNADlCI.js.map +1 -0
- package/dist/{server-DgCrSuhB.d.ts → server-DjZZa1wr.d.ts} +9 -3
- package/dist/{server-DgCrSuhB.d.ts.map → server-DjZZa1wr.d.ts.map} +1 -1
- package/dist/server.d.ts +3 -3
- package/dist/server.js +2 -2
- package/dist/{store-DGHeBtIQ.d.ts → store-DtDOWLSn.d.ts} +4 -5
- package/dist/{store-DGHeBtIQ.d.ts.map → store-DtDOWLSn.d.ts.map} +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.js +1 -1
- package/dist/{store-polling-6DW7F1DT.js → store-polling-CmxUbV93.js} +56 -7
- package/dist/store-polling-CmxUbV93.js.map +1 -0
- package/dist/store-postgres.d.ts +6 -4
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +701 -53
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-presence-polling-C7-XZyW9.js +94 -0
- package/dist/store-presence-polling-C7-XZyW9.js.map +1 -0
- package/dist/store-redis-http.d.ts +2 -2
- package/dist/store-redis-http.d.ts.map +1 -1
- package/dist/store-redis-http.js +184 -38
- package/dist/store-redis-http.js.map +1 -1
- package/dist/{store-redis-core-z-ykbyMg.js → store-redis-notify-BUCyXOn0.js} +491 -27
- package/dist/store-redis-notify-BUCyXOn0.js.map +1 -0
- package/dist/store-redis.d.ts +5 -13
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +27 -271
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.js +1 -1
- package/dist/{wire--yji6mO3.js → wire-BO5wWCb1.js} +18 -16
- package/dist/wire-BO5wWCb1.js.map +1 -0
- package/docs/actors/04-routes.mdx +40 -8
- package/docs/guides/03-react.mdx +52 -10
- package/docs/guides/05-production.mdx +54 -5
- package/docs/guides/09-presence.mdx +12 -7
- package/docs/guides/10-transports.mdx +15 -2
- package/docs/reference/01-api.mdx +124 -17
- package/docs/reference/02-errors.mdx +45 -11
- package/examples/playground/package.json +1 -1
- package/package.json +1 -1
- package/src/actor-client.ts +114 -14
- package/src/actor-react.ts +18 -3
- package/src/actor.ts +16 -1
- package/src/client-errors.ts +51 -0
- package/src/client.ts +253 -96
- package/src/errors.ts +4 -9
- package/src/index.ts +1 -1
- package/src/internal.ts +1 -1
- package/src/postgres-notification-scope.ts +134 -0
- package/src/postgres-notifications.ts +244 -0
- package/src/postgres-pool.ts +65 -0
- package/src/postgres-resources.ts +185 -0
- package/src/presence-recovery.ts +120 -0
- package/src/react.ts +3 -0
- package/src/redis-http-subscriptions.ts +199 -0
- package/src/server.ts +160 -22
- package/src/session-socket.ts +25 -7
- package/src/sse.ts +17 -0
- package/src/store-polling.ts +32 -12
- package/src/store-postgres.ts +292 -75
- package/src/store-presence-polling.ts +125 -0
- package/src/store-redis-core.ts +37 -30
- package/src/store-redis-http.ts +51 -45
- package/src/store-redis-notify.ts +464 -0
- package/src/store-redis.ts +9 -364
- package/src/store.ts +3 -4
- package/src/stream-activity.ts +32 -0
- package/src/wire.ts +39 -15
- package/dist/actor-shared-USo5MyuF.d.ts +0 -136
- package/dist/actor-shared-USo5MyuF.d.ts.map +0 -1
- package/dist/actor.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/server-DlLyvaSH.js.map +0 -1
- package/dist/store-polling-6DW7F1DT.js.map +0 -1
- package/dist/store-redis-core-z-ykbyMg.js.map +0 -1
- 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-
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
397
|
-
|
|
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)
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
|
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
|
|
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]) => [
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
-
|
|
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-
|
|
1557
|
+
//# sourceMappingURL=store-redis-notify-BUCyXOn0.js.map
|