experimental-a2 0.10.0 → 0.11.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 +39 -0
- package/dist/actor-D_54lz_1.d.ts +310 -0
- package/dist/actor-D_54lz_1.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-Dkz2a84E.js} +165 -62
- package/dist/server-Dkz2a84E.js.map +1 -0
- package/dist/{server-DgCrSuhB.d.ts → server-DwPrMqHB.d.ts} +2 -2
- package/dist/{server-DgCrSuhB.d.ts.map → server-DwPrMqHB.d.ts.map} +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- 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 +60 -16
- 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 +146 -19
- 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
package/dist/store-redis-http.js
CHANGED
|
@@ -1,25 +1,160 @@
|
|
|
1
1
|
import { t as A2Error } from "./errors-DCk6ch5n.js";
|
|
2
|
-
import {
|
|
2
|
+
import { r as NOTIFY_TIMINGS } from "./internal-Dq2qYxou.js";
|
|
3
|
+
import { t as defaultSleep } from "./store-polling-CmxUbV93.js";
|
|
3
4
|
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
|
|
4
|
-
import { t as
|
|
5
|
+
import { t as createRedisNotifyStore } from "./store-redis-notify-BUCyXOn0.js";
|
|
6
|
+
//#region src/redis-http-subscriptions.ts
|
|
7
|
+
function redisHttpSubscriptions({ url, token, onReconnect, fallback }) {
|
|
8
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
9
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
10
|
+
let closed = false;
|
|
11
|
+
let supported = false;
|
|
12
|
+
const subscribe = (channel) => {
|
|
13
|
+
if (fallback.enabled()) return Promise.resolve();
|
|
14
|
+
const existing = subscriptions.get(channel);
|
|
15
|
+
if (existing) return existing.ready;
|
|
16
|
+
let resolve;
|
|
17
|
+
let reject;
|
|
18
|
+
const ready = new Promise((yes, no) => {
|
|
19
|
+
resolve = yes;
|
|
20
|
+
reject = no;
|
|
21
|
+
});
|
|
22
|
+
let stopped = false;
|
|
23
|
+
let connected = false;
|
|
24
|
+
let request;
|
|
25
|
+
let retry;
|
|
26
|
+
const subscription = {
|
|
27
|
+
ready,
|
|
28
|
+
stop() {
|
|
29
|
+
stopped = true;
|
|
30
|
+
request?.abort();
|
|
31
|
+
retry?.cancel();
|
|
32
|
+
resolve();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
subscriptions.set(channel, subscription);
|
|
36
|
+
const run = async () => {
|
|
37
|
+
let delay = 100;
|
|
38
|
+
for (;;) {
|
|
39
|
+
if (stopped) return;
|
|
40
|
+
request = new AbortController();
|
|
41
|
+
const timeout = setTimeout(() => request?.abort(), NOTIFY_TIMINGS.safetyReadMs);
|
|
42
|
+
let reader;
|
|
43
|
+
let connectedAt;
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(`${url.replace(/\/$/, "")}/subscribe/${encodeURIComponent(channel)}`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: {
|
|
48
|
+
authorization: `Bearer ${token}`,
|
|
49
|
+
accept: "text/event-stream"
|
|
50
|
+
},
|
|
51
|
+
signal: request.signal
|
|
52
|
+
});
|
|
53
|
+
if (!supported && [
|
|
54
|
+
404,
|
|
55
|
+
405,
|
|
56
|
+
501
|
|
57
|
+
].includes(response.status)) {
|
|
58
|
+
await response.body?.cancel();
|
|
59
|
+
await fallback.enable();
|
|
60
|
+
for (const active of subscriptions.values()) active.stop();
|
|
61
|
+
subscriptions.clear();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!response.ok || !response.headers.get("content-type")?.startsWith("text/event-stream") || !response.body) {
|
|
65
|
+
await response.body?.cancel();
|
|
66
|
+
throw new A2Error("STORE_UNAVAILABLE", `redis rest subscription failed: status ${response.status}`);
|
|
67
|
+
}
|
|
68
|
+
reader = response.body.getReader();
|
|
69
|
+
const decoder = new TextDecoder();
|
|
70
|
+
let buffer = "";
|
|
71
|
+
let data = [];
|
|
72
|
+
let acknowledged = false;
|
|
73
|
+
for (;;) {
|
|
74
|
+
if (stopped) break;
|
|
75
|
+
const chunk = await reader.read();
|
|
76
|
+
if (chunk.done) break;
|
|
77
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
78
|
+
let newline;
|
|
79
|
+
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
80
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
81
|
+
buffer = buffer.slice(newline + 1);
|
|
82
|
+
if (line.startsWith("data:")) data.push(line.slice(5).replace(/^ /, ""));
|
|
83
|
+
else if (line === "" && data.length > 0) {
|
|
84
|
+
const message = data.join("\n");
|
|
85
|
+
data = [];
|
|
86
|
+
if (message.startsWith(`subscribe,${channel},`)) {
|
|
87
|
+
if (acknowledged) continue;
|
|
88
|
+
acknowledged = true;
|
|
89
|
+
supported = true;
|
|
90
|
+
clearTimeout(timeout);
|
|
91
|
+
connectedAt = Date.now();
|
|
92
|
+
resolve();
|
|
93
|
+
if (connected) onReconnect(channel);
|
|
94
|
+
connected = true;
|
|
95
|
+
} else if (message.startsWith(`message,${channel},`)) {
|
|
96
|
+
const payload = message.slice(`message,${channel},`.length);
|
|
97
|
+
for (const listener of listeners) listener(channel, payload);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!connected && !stopped) throw new A2Error("STORE_UNAVAILABLE", "redis rest subscription ended before acknowledgement");
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (!connected && !stopped) {
|
|
105
|
+
reject(error);
|
|
106
|
+
if (subscriptions.get(channel) === subscription) subscriptions.delete(channel);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
clearTimeout(timeout);
|
|
111
|
+
request.abort();
|
|
112
|
+
await reader?.cancel().catch(() => {});
|
|
113
|
+
reader?.releaseLock();
|
|
114
|
+
}
|
|
115
|
+
if (stopped) return;
|
|
116
|
+
if (connectedAt !== void 0 && Date.now() - connectedAt >= NOTIFY_TIMINGS.safetyReadMs) delay = 100;
|
|
117
|
+
retry = defaultSleep(delay);
|
|
118
|
+
await retry.promise;
|
|
119
|
+
delay = Math.min(delay * 2, NOTIFY_TIMINGS.safetyReadMs);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
run();
|
|
123
|
+
return ready;
|
|
124
|
+
};
|
|
125
|
+
const connection = {
|
|
126
|
+
call(command, ...args) {
|
|
127
|
+
if (closed) return Promise.reject(new A2Error("STORE_UNAVAILABLE", "the store was closed"));
|
|
128
|
+
const channel = String(args[0]);
|
|
129
|
+
if (command === "echo") return Promise.resolve(args[0]);
|
|
130
|
+
if (command === "subscribe") return subscribe(channel);
|
|
131
|
+
if (command === "unsubscribe") {
|
|
132
|
+
subscriptions.get(channel)?.stop();
|
|
133
|
+
subscriptions.delete(channel);
|
|
134
|
+
return Promise.resolve(null);
|
|
135
|
+
}
|
|
136
|
+
return Promise.reject(/* @__PURE__ */ new Error(`unsupported subscription command: ${command}`));
|
|
137
|
+
},
|
|
138
|
+
duplicate: () => connection,
|
|
139
|
+
on(_event, listener) {
|
|
140
|
+
listeners.add(listener);
|
|
141
|
+
},
|
|
142
|
+
disconnect() {
|
|
143
|
+
closed = true;
|
|
144
|
+
for (const subscription of subscriptions.values()) subscription.stop();
|
|
145
|
+
subscriptions.clear();
|
|
146
|
+
listeners.clear();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
return connection;
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
5
152
|
//#region src/store-redis-http.ts
|
|
6
|
-
/**
|
|
7
|
-
* experimental-a2/store-redis-http — the same Redis Streams store over a
|
|
8
|
-
* provider REST API (Upstash-shaped: POST one command as a JSON array,
|
|
9
|
-
* receive `{ result }` or `{ error }`). The storage semantics live in
|
|
10
|
-
* store-redis-core.ts, shared with experimental-a2/store-redis.
|
|
11
|
-
*
|
|
12
|
-
* Fully connectionless: every command is one `fetch`, so it works
|
|
13
|
-
* where a Redis-protocol connection cannot exist or cannot be spared.
|
|
14
|
-
* Without a connection there is no push and no pub/sub, so `stream()`
|
|
15
|
-
* is the shared activity-adaptive poll loop (25ms while events flow,
|
|
16
|
-
* backing off to a 250ms idle ceiling), and presence is the degraded
|
|
17
|
-
* tier: `set`/`read` from the shared core, no `subscribe`. No peer
|
|
18
|
-
* dependencies.
|
|
19
|
-
*/
|
|
20
153
|
function redisHttp(options) {
|
|
21
154
|
if (!options.url || !options.token) throw new TypeError("redisHttp() needs a url and a token");
|
|
22
155
|
const { url, token } = options;
|
|
156
|
+
let polling = false;
|
|
157
|
+
let fallbackCheck;
|
|
23
158
|
const call = async (command, ...args) => {
|
|
24
159
|
let response;
|
|
25
160
|
try {
|
|
@@ -29,7 +164,8 @@ function redisHttp(options) {
|
|
|
29
164
|
authorization: `Bearer ${token}`,
|
|
30
165
|
"content-type": "application/json"
|
|
31
166
|
},
|
|
32
|
-
body: JSON.stringify([command, ...args.map(String)])
|
|
167
|
+
body: JSON.stringify([command, ...args.map(String)]),
|
|
168
|
+
...command === "PING" ? { signal: AbortSignal.timeout(NOTIFY_TIMINGS.safetyReadMs) } : {}
|
|
33
169
|
});
|
|
34
170
|
} catch (err) {
|
|
35
171
|
throw new A2Error("STORE_UNAVAILABLE", "redis rest request failed", { cause: err });
|
|
@@ -38,32 +174,42 @@ function redisHttp(options) {
|
|
|
38
174
|
if (body === null || !response.ok || body.error !== void 0) throw new A2Error("STORE_UNAVAILABLE", `redis rest command failed: ${body?.error ?? `status ${response.status}`}`);
|
|
39
175
|
return body.result;
|
|
40
176
|
};
|
|
41
|
-
|
|
177
|
+
let subscriber;
|
|
178
|
+
let onReconnect;
|
|
179
|
+
const client = {
|
|
42
180
|
call,
|
|
181
|
+
duplicate: () => subscriber ??= redisHttpSubscriptions({
|
|
182
|
+
url,
|
|
183
|
+
token,
|
|
184
|
+
onReconnect: (channel) => onReconnect?.(channel),
|
|
185
|
+
fallback: {
|
|
186
|
+
enabled: () => polling,
|
|
187
|
+
enable() {
|
|
188
|
+
fallbackCheck ??= call("PING").then((result) => {
|
|
189
|
+
if (result !== "PONG") throw new A2Error("STORE_UNAVAILABLE", "redis rest capability check failed");
|
|
190
|
+
polling = true;
|
|
191
|
+
}).catch((error) => {
|
|
192
|
+
fallbackCheck = void 0;
|
|
193
|
+
throw error;
|
|
194
|
+
});
|
|
195
|
+
return fallbackCheck;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}),
|
|
199
|
+
on() {},
|
|
200
|
+
disconnect: () => subscriber?.disconnect()
|
|
201
|
+
};
|
|
202
|
+
return createRedisNotifyStore({
|
|
203
|
+
client: () => Promise.resolve(client),
|
|
204
|
+
closeClient: async () => client.disconnect(),
|
|
205
|
+
polling: () => polling,
|
|
206
|
+
onReconnect: (listener) => {
|
|
207
|
+
onReconnect = listener;
|
|
208
|
+
},
|
|
43
209
|
clock: options.clock ?? SYSTEM_CLOCK,
|
|
44
210
|
ids: options.ids ?? RANDOM_IDS,
|
|
45
211
|
keyPrefix: options.keyPrefix ?? "a2"
|
|
46
212
|
});
|
|
47
|
-
return {
|
|
48
|
-
append: core.append,
|
|
49
|
-
read: core.read,
|
|
50
|
-
claimAvailable: core.claimAvailable,
|
|
51
|
-
renewClaims: core.renewClaims,
|
|
52
|
-
completeAttempt: core.completeAttempt,
|
|
53
|
-
failAttempt: core.failAttempt,
|
|
54
|
-
readState: core.readState,
|
|
55
|
-
putSnapshots: core.putSnapshots,
|
|
56
|
-
presence: {
|
|
57
|
-
async set(ns, participant, values, meta) {
|
|
58
|
-
await core.presence.set(ns, participant, values, meta);
|
|
59
|
-
},
|
|
60
|
-
read: core.presence.read
|
|
61
|
-
},
|
|
62
|
-
stream(sessionId, opts) {
|
|
63
|
-
return pollingStream((afterIndex) => core.readRange(sessionId, afterIndex), opts?.startAfter !== void 0 ? { startAfter: opts.startAfter } : {});
|
|
64
|
-
},
|
|
65
|
-
async close() {}
|
|
66
|
-
};
|
|
67
213
|
}
|
|
68
214
|
//#endregion
|
|
69
215
|
export { redisHttp };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store-redis-http.js","names":[],"sources":["../src/store-redis-http.ts"],"sourcesContent":["/**\n * experimental-a2/store-redis-http — the same Redis Streams store over a\n * provider REST API (Upstash-shaped: POST one command as a JSON array,\n * receive `{ result }` or `{ error }`). The storage semantics live in\n * store-redis-core.ts, shared with experimental-a2/store-redis.\n *\n * Fully connectionless: every command is one `fetch`, so it works\n * where a Redis-protocol connection cannot exist or cannot be spared.\n * Without a connection there is no push and no pub/sub, so `stream()`\n * is the shared activity-adaptive poll loop (25ms while events flow,\n * backing off to a 250ms idle ceiling), and presence is the degraded\n * tier: `set`/`read` from the shared core, no `subscribe`. No peer\n * dependencies.\n */\n\nimport { A2Error } from './errors.ts'\nimport { pollingStream } from './store-polling.ts'\nimport { createRedisStoreCore } from './store-redis-core.ts'\nimport {\n RANDOM_IDS,\n SYSTEM_CLOCK,\n type A2Store,\n type Clock,\n type IdSource,\n} from './store.ts'\n\nexport type RedisHttpStoreOptions = {\n /** REST endpoint, e.g. `process.env.UPSTASH_REDIS_REST_URL`. */\n url: string\n /** Bearer token, e.g. `process.env.UPSTASH_REDIS_REST_TOKEN`. */\n token: string\n /** Key prefix — isolates multiple apps on one Redis. Default `'a2'`. */\n keyPrefix?: string\n /** Injectable clock — every stored timestamp comes from here. */\n clock?: Clock\n /** Injectable id source for generated event ids. */\n ids?: IdSource\n}\n\nexport type RedisHttpStore = A2Store & {\n /** Nothing to disconnect; present so callers can treat stores uniformly. */\n close(): Promise<void>\n}\n\nexport function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore {\n if (!options.url || !options.token) {\n throw new TypeError('redisHttp() needs a url and a token')\n }\n const { url, token } = options\n\n const call = async (\n command: string,\n ...args: Array<string | number>\n ): Promise<unknown> => {\n let response: Response\n try {\n response = await fetch(url, {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify([command, ...args.map(String)]),\n })\n } catch (err) {\n throw new A2Error('STORE_UNAVAILABLE', 'redis rest request failed', {\n cause: err,\n })\n }\n const body = (await response.json().catch(() => null)) as {\n result?: unknown\n error?: string\n } | null\n if (body === null || !response.ok || body.error !== undefined) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis rest command failed: ${body?.error ?? `status ${response.status}`}`,\n )\n }\n return body.result\n }\n\n const core = createRedisStoreCore({\n call,\n clock: options.clock ?? SYSTEM_CLOCK,\n ids: options.ids ?? RANDOM_IDS,\n keyPrefix: options.keyPrefix ?? 'a2',\n })\n\n return {\n append: core.append,\n read: core.read,\n claimAvailable: core.claimAvailable,\n renewClaims: core.renewClaims,\n completeAttempt: core.completeAttempt,\n failAttempt: core.failAttempt,\n readState: core.readState,\n putSnapshots: core.putSnapshots,\n presence: {\n async set(ns, participant, values, meta) {\n await core.presence.set(ns, participant, values, meta)\n },\n read: core.presence.read,\n },\n\n stream(sessionId, opts) {\n return pollingStream(\n (afterIndex) => core.readRange(sessionId, afterIndex),\n opts?.startAfter !== undefined ? { startAfter: opts.startAfter } : {},\n )\n },\n\n async close() {},\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA4CA,SAAgB,UAAU,SAAgD;CACxE,IAAI,CAAC,QAAQ,OAAO,CAAC,QAAQ,OAC3B,MAAM,IAAI,UAAU,qCAAqC;CAE3D,MAAM,EAAE,KAAK,UAAU;CAEvB,MAAM,OAAO,OACX,SACA,GAAG,SACkB;EACrB,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,MAAM,KAAK;IAC1B,QAAQ;IACR,SAAS;KACP,eAAe,UAAU;KACzB,gBAAgB;IAClB;IACA,MAAM,KAAK,UAAU,CAAC,SAAS,GAAG,KAAK,IAAI,MAAM,CAAC,CAAC;GACrD,CAAC;EACH,SAAS,KAAK;GACZ,MAAM,IAAI,QAAQ,qBAAqB,6BAA6B,EAClE,OAAO,IACT,CAAC;EACH;EACA,MAAM,OAAQ,MAAM,SAAS,KAAK,CAAC,CAAC,YAAY,IAAI;EAIpD,IAAI,SAAS,QAAQ,CAAC,SAAS,MAAM,KAAK,UAAU,KAAA,GAClD,MAAM,IAAI,QACR,qBACA,8BAA8B,MAAM,SAAS,UAAU,SAAS,UAClE;EAEF,OAAO,KAAK;CACd;CAEA,MAAM,OAAO,qBAAqB;EAChC;EACA,OAAO,QAAQ,SAAS;EACxB,KAAK,QAAQ,OAAO;EACpB,WAAW,QAAQ,aAAa;CAClC,CAAC;CAED,OAAO;EACL,QAAQ,KAAK;EACb,MAAM,KAAK;EACX,gBAAgB,KAAK;EACrB,aAAa,KAAK;EAClB,iBAAiB,KAAK;EACtB,aAAa,KAAK;EAClB,WAAW,KAAK;EAChB,cAAc,KAAK;EACnB,UAAU;GACR,MAAM,IAAI,IAAI,aAAa,QAAQ,MAAM;IACvC,MAAM,KAAK,SAAS,IAAI,IAAI,aAAa,QAAQ,IAAI;GACvD;GACA,MAAM,KAAK,SAAS;EACtB;EAEA,OAAO,WAAW,MAAM;GACtB,OAAO,eACJ,eAAe,KAAK,UAAU,WAAW,UAAU,GACpD,MAAM,eAAe,KAAA,IAAY,EAAE,YAAY,KAAK,WAAW,IAAI,CAAC,CACtE;EACF;EAEA,MAAM,QAAQ,CAAC;CACjB;AACF"}
|
|
1
|
+
{"version":3,"file":"store-redis-http.js","names":[],"sources":["../src/redis-http-subscriptions.ts","../src/store-redis-http.ts"],"sourcesContent":["import { A2Error } from './errors.ts'\nimport { NOTIFY_TIMINGS } from './internal.ts'\nimport { defaultSleep, type Sleeper } from './store-polling.ts'\nimport type { RedisConnection } from './store-redis-notify.ts'\n\nexport function redisHttpSubscriptions({\n url,\n token,\n onReconnect,\n fallback,\n}: {\n url: string\n token: string\n onReconnect: (channel: string) => void\n fallback: { enabled(): boolean; enable(): Promise<void> }\n}): RedisConnection {\n const subscriptions = new Map<\n string,\n { ready: Promise<void>; stop(): void }\n >()\n const listeners = new Set<(channel: string, message: string) => void>()\n let closed = false\n let supported = false\n\n const subscribe = (channel: string): Promise<void> => {\n if (fallback.enabled()) return Promise.resolve()\n const existing = subscriptions.get(channel)\n if (existing) return existing.ready\n let resolve!: () => void\n let reject!: (error: unknown) => void\n const ready = new Promise<void>((yes, no) => {\n resolve = yes\n reject = no\n })\n let stopped = false\n let connected = false\n let request: AbortController | undefined\n let retry: Sleeper | undefined\n const subscription = {\n ready,\n stop() {\n stopped = true\n request?.abort()\n retry?.cancel()\n resolve()\n },\n }\n subscriptions.set(channel, subscription)\n\n const run = async (): Promise<void> => {\n let delay = 100\n for (;;) {\n if (stopped) return\n request = new AbortController()\n const timeout = setTimeout(\n () => request?.abort(),\n NOTIFY_TIMINGS.safetyReadMs,\n )\n let reader: ReadableStreamDefaultReader<Uint8Array> | undefined\n let connectedAt: number | undefined\n try {\n // oxlint-disable-next-line no-await-in-loop -- reconnect loop\n const response = await fetch(\n `${url.replace(/\\/$/, '')}/subscribe/${encodeURIComponent(channel)}`,\n {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n accept: 'text/event-stream',\n },\n signal: request.signal,\n },\n )\n if (!supported && [404, 405, 501].includes(response.status)) {\n // oxlint-disable-next-line no-await-in-loop -- finish capability detection before falling back\n await response.body?.cancel()\n // oxlint-disable-next-line no-await-in-loop -- confirm that ordinary commands work\n await fallback.enable()\n for (const active of subscriptions.values()) active.stop()\n subscriptions.clear()\n return\n }\n if (\n !response.ok ||\n !response.headers\n .get('content-type')\n ?.startsWith('text/event-stream') ||\n !response.body\n ) {\n // oxlint-disable-next-line no-await-in-loop -- release a failed subscription attempt\n await response.body?.cancel()\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis rest subscription failed: status ${response.status}`,\n )\n }\n reader = response.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n let data: string[] = []\n let acknowledged = false\n for (;;) {\n if (stopped) break\n // oxlint-disable-next-line no-await-in-loop -- consume SSE chunks\n const chunk = await reader.read()\n if (chunk.done) break\n buffer += decoder.decode(chunk.value, { stream: true })\n let newline: number\n while ((newline = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, newline).replace(/\\r$/, '')\n buffer = buffer.slice(newline + 1)\n if (line.startsWith('data:')) {\n data.push(line.slice(5).replace(/^ /, ''))\n } else if (line === '' && data.length > 0) {\n const message = data.join('\\n')\n data = []\n if (message.startsWith(`subscribe,${channel},`)) {\n if (acknowledged) continue\n acknowledged = true\n supported = true\n clearTimeout(timeout)\n connectedAt = Date.now()\n resolve()\n if (connected) onReconnect(channel)\n connected = true\n } else if (message.startsWith(`message,${channel},`)) {\n const payload = message.slice(`message,${channel},`.length)\n for (const listener of listeners) listener(channel, payload)\n }\n }\n }\n }\n if (!connected && !stopped) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'redis rest subscription ended before acknowledgement',\n )\n }\n } catch (error) {\n if (!connected && !stopped) {\n reject(error)\n if (subscriptions.get(channel) === subscription)\n subscriptions.delete(channel)\n return\n }\n } finally {\n clearTimeout(timeout)\n request.abort()\n // oxlint-disable-next-line no-await-in-loop -- close this attempt before reconnecting\n await reader?.cancel().catch(() => {})\n reader?.releaseLock()\n }\n if (stopped) return\n if (\n connectedAt !== undefined &&\n Date.now() - connectedAt >= NOTIFY_TIMINGS.safetyReadMs\n )\n delay = 100\n retry = defaultSleep(delay)\n // oxlint-disable-next-line no-await-in-loop -- bounded reconnect backoff\n await retry.promise\n delay = Math.min(delay * 2, NOTIFY_TIMINGS.safetyReadMs)\n }\n }\n void run()\n return ready\n }\n\n const connection: RedisConnection = {\n call(command, ...args) {\n if (closed)\n return Promise.reject(\n new A2Error('STORE_UNAVAILABLE', 'the store was closed'),\n )\n const channel = String(args[0])\n if (command === 'echo') return Promise.resolve(args[0])\n if (command === 'subscribe') return subscribe(channel)\n if (command === 'unsubscribe') {\n subscriptions.get(channel)?.stop()\n subscriptions.delete(channel)\n return Promise.resolve(null)\n }\n return Promise.reject(\n new Error(`unsupported subscription command: ${command}`),\n )\n },\n duplicate: () => connection,\n on(_event, listener) {\n listeners.add(listener)\n },\n disconnect() {\n closed = true\n for (const subscription of subscriptions.values()) subscription.stop()\n subscriptions.clear()\n listeners.clear()\n },\n }\n return connection\n}\n","import { A2Error } from './errors.ts'\nimport { NOTIFY_TIMINGS } from './internal.ts'\nimport { redisHttpSubscriptions } from './redis-http-subscriptions.ts'\nimport {\n createRedisNotifyStore,\n type RedisConnection,\n} from './store-redis-notify.ts'\nimport {\n RANDOM_IDS,\n SYSTEM_CLOCK,\n type A2Store,\n type Clock,\n type IdSource,\n} from './store.ts'\n\nexport type RedisHttpStoreOptions = {\n /** REST endpoint, e.g. `process.env.UPSTASH_REDIS_REST_URL`. */\n url: string\n /** Bearer token, e.g. `process.env.UPSTASH_REDIS_REST_TOKEN`. */\n token: string\n /** Key prefix — isolates multiple apps on one Redis. Default `'a2'`. */\n keyPrefix?: string\n /** Injectable clock — every stored timestamp comes from here. */\n clock?: Clock\n /** Injectable id source for generated event ids. */\n ids?: IdSource\n}\n\nexport type RedisHttpStore = A2Store & {\n /** Close live subscriptions and pending reconnects. */\n close(): Promise<void>\n}\n\nexport function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore {\n if (!options.url || !options.token) {\n throw new TypeError('redisHttp() needs a url and a token')\n }\n const { url, token } = options\n let polling = false\n let fallbackCheck: Promise<void> | undefined\n\n const call = async (\n command: string,\n ...args: Array<string | number>\n ): Promise<unknown> => {\n let response: Response\n try {\n response = await fetch(url, {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify([command, ...args.map(String)]),\n ...(command === 'PING'\n ? { signal: AbortSignal.timeout(NOTIFY_TIMINGS.safetyReadMs) }\n : {}),\n })\n } catch (err) {\n throw new A2Error('STORE_UNAVAILABLE', 'redis rest request failed', {\n cause: err,\n })\n }\n const body = (await response.json().catch(() => null)) as {\n result?: unknown\n error?: string\n } | null\n if (body === null || !response.ok || body.error !== undefined) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis rest command failed: ${body?.error ?? `status ${response.status}`}`,\n )\n }\n return body.result\n }\n\n let subscriber: RedisConnection | undefined\n let onReconnect: ((channel: string) => void) | undefined\n const client: RedisConnection = {\n call,\n duplicate: () =>\n (subscriber ??= redisHttpSubscriptions({\n url,\n token,\n onReconnect: (channel) => onReconnect?.(channel),\n fallback: {\n enabled: () => polling,\n enable() {\n fallbackCheck ??= call('PING')\n .then((result) => {\n if (result !== 'PONG')\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'redis rest capability check failed',\n )\n polling = true\n return undefined\n })\n .catch((error: unknown) => {\n fallbackCheck = undefined\n throw error\n })\n return fallbackCheck\n },\n },\n })),\n on() {},\n disconnect: () => subscriber?.disconnect(),\n }\n return createRedisNotifyStore({\n client: () => Promise.resolve(client),\n closeClient: async () => client.disconnect(),\n polling: () => polling,\n onReconnect: (listener) => {\n onReconnect = listener\n },\n clock: options.clock ?? SYSTEM_CLOCK,\n ids: options.ids ?? RANDOM_IDS,\n keyPrefix: options.keyPrefix ?? 'a2',\n })\n}\n"],"mappings":"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"}
|