@shadow-js/realtime 0.0.0-stage → 1.0.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 +13 -0
- package/LICENSE +21 -0
- package/README.md +114 -2
- package/dist/dev/adapter.d.ts +75 -0
- package/dist/dev/client/core.d.ts +24 -0
- package/dist/dev/client/core.js +845 -0
- package/dist/dev/client/core.js.map +1 -0
- package/dist/dev/client/index.d.ts +5 -0
- package/dist/dev/client/index.js +3 -0
- package/dist/dev/client/presence.js +23 -0
- package/dist/dev/client/presence.js.map +1 -0
- package/dist/dev/client/sse.js +76 -0
- package/dist/dev/client/sse.js.map +1 -0
- package/dist/dev/client/types.d.ts +256 -0
- package/dist/dev/client/util.js +30 -0
- package/dist/dev/client/util.js.map +1 -0
- package/dist/dev/client/ws.js +39 -0
- package/dist/dev/client/ws.js.map +1 -0
- package/dist/dev/cloudflare/adapter.d.ts +80 -0
- package/dist/dev/cloudflare/adapter.js +347 -0
- package/dist/dev/cloudflare/adapter.js.map +1 -0
- package/dist/dev/cloudflare/core.js +701 -0
- package/dist/dev/cloudflare/core.js.map +1 -0
- package/dist/dev/cloudflare/link.js +397 -0
- package/dist/dev/cloudflare/link.js.map +1 -0
- package/dist/dev/cloudflare/object.d.ts +82 -0
- package/dist/dev/cloudflare/object.js +214 -0
- package/dist/dev/cloudflare/object.js.map +1 -0
- package/dist/dev/cloudflare/types.d.ts +100 -0
- package/dist/dev/cloudflare/wire.js +284 -0
- package/dist/dev/cloudflare/wire.js.map +1 -0
- package/dist/dev/cloudflare.d.ts +4 -0
- package/dist/dev/cloudflare.js +3 -0
- package/dist/dev/errors.d.ts +25 -0
- package/dist/dev/errors.js +24 -0
- package/dist/dev/errors.js.map +1 -0
- package/dist/dev/hooks.d.ts +110 -0
- package/dist/dev/hooks.js +162 -0
- package/dist/dev/hooks.js.map +1 -0
- package/dist/dev/index.d.ts +4 -0
- package/dist/dev/index.js +3 -0
- package/dist/dev/memory.d.ts +33 -0
- package/dist/dev/memory.js +255 -0
- package/dist/dev/memory.js.map +1 -0
- package/dist/dev/protocol.d.ts +211 -0
- package/dist/dev/protocol.js +400 -0
- package/dist/dev/protocol.js.map +1 -0
- package/dist/dev/redis/client.d.ts +139 -0
- package/dist/dev/redis/client.js +146 -0
- package/dist/dev/redis/client.js.map +1 -0
- package/dist/dev/redis/scripts.js +275 -0
- package/dist/dev/redis/scripts.js.map +1 -0
- package/dist/dev/redis/topics.d.ts +8 -0
- package/dist/dev/redis/topics.js +377 -0
- package/dist/dev/redis/topics.js.map +1 -0
- package/dist/dev/redis.d.ts +68 -0
- package/dist/dev/redis.js +422 -0
- package/dist/dev/redis.js.map +1 -0
- package/dist/dev/server/http.d.ts +48 -0
- package/dist/dev/server/http.js +286 -0
- package/dist/dev/server/http.js.map +1 -0
- package/dist/dev/server/hub.d.ts +114 -0
- package/dist/dev/server/hub.js +827 -0
- package/dist/dev/server/hub.js.map +1 -0
- package/dist/dev/server/index.d.ts +47 -0
- package/dist/dev/server/index.js +41 -0
- package/dist/dev/server/index.js.map +1 -0
- package/dist/dev/server/limits.d.ts +54 -0
- package/dist/dev/server/limits.js +85 -0
- package/dist/dev/server/limits.js.map +1 -0
- package/dist/dev/server/rules.d.ts +43 -0
- package/dist/dev/server/rules.js +72 -0
- package/dist/dev/server/rules.js.map +1 -0
- package/dist/dev/testing.d.ts +115 -0
- package/dist/dev/testing.js +427 -0
- package/dist/dev/testing.js.map +1 -0
- package/dist/prod/client/core.js +2 -0
- package/dist/prod/client/core.js.map +1 -0
- package/dist/prod/client/index.js +1 -0
- package/dist/prod/client/presence.js +2 -0
- package/dist/prod/client/presence.js.map +1 -0
- package/dist/prod/client/sse.js +5 -0
- package/dist/prod/client/sse.js.map +1 -0
- package/dist/prod/client/util.js +2 -0
- package/dist/prod/client/util.js.map +1 -0
- package/dist/prod/client/ws.js +2 -0
- package/dist/prod/client/ws.js.map +1 -0
- package/dist/prod/cloudflare/adapter.js +3 -0
- package/dist/prod/cloudflare/adapter.js.map +1 -0
- package/dist/prod/cloudflare/core.js +2 -0
- package/dist/prod/cloudflare/core.js.map +1 -0
- package/dist/prod/cloudflare/link.js +2 -0
- package/dist/prod/cloudflare/link.js.map +1 -0
- package/dist/prod/cloudflare/object.js +2 -0
- package/dist/prod/cloudflare/object.js.map +1 -0
- package/dist/prod/cloudflare/wire.js +2 -0
- package/dist/prod/cloudflare/wire.js.map +1 -0
- package/dist/prod/cloudflare.js +1 -0
- package/dist/prod/errors.js +2 -0
- package/dist/prod/errors.js.map +1 -0
- package/dist/prod/hooks.js +2 -0
- package/dist/prod/hooks.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/memory.js +2 -0
- package/dist/prod/memory.js.map +1 -0
- package/dist/prod/protocol.js +2 -0
- package/dist/prod/protocol.js.map +1 -0
- package/dist/prod/redis/client.js +2 -0
- package/dist/prod/redis/client.js.map +1 -0
- package/dist/prod/redis/scripts.js +157 -0
- package/dist/prod/redis/scripts.js.map +1 -0
- package/dist/prod/redis/topics.js +2 -0
- package/dist/prod/redis/topics.js.map +1 -0
- package/dist/prod/redis.js +2 -0
- package/dist/prod/redis.js.map +1 -0
- package/dist/prod/server/http.js +2 -0
- package/dist/prod/server/http.js.map +1 -0
- package/dist/prod/server/hub.js +2 -0
- package/dist/prod/server/hub.js.map +1 -0
- package/dist/prod/server/index.js +2 -0
- package/dist/prod/server/index.js.map +1 -0
- package/dist/prod/server/limits.js +2 -0
- package/dist/prod/server/limits.js.map +1 -0
- package/dist/prod/server/rules.js +2 -0
- package/dist/prod/server/rules.js.map +1 -0
- package/dist/prod/testing.js +2 -0
- package/dist/prod/testing.js.map +1 -0
- package/package.json +105 -4
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { RealtimeError } from "./errors.js";
|
|
2
|
+
import { HISTORY, KEEPALIVE, PRESENCE_LIST, PRESENCE_REAP, PRESENCE_REMOVE, PRESENCE_SET, PRESENCE_TOUCH, PUBLISH } from "./redis/scripts.js";
|
|
3
|
+
import { createTopics } from "./redis/topics.js";
|
|
4
|
+
import { fromIoredis, fromNodeRedis } from "./redis/client.js";
|
|
5
|
+
//#region src/redis.ts
|
|
6
|
+
const DAY_MS = 864e5;
|
|
7
|
+
const MAX_LIMIT = 1e4;
|
|
8
|
+
const MAX_KEY_BYTES = 1024;
|
|
9
|
+
const REAP_PAGE = 500;
|
|
10
|
+
const TOUCH_PAGE = 500;
|
|
11
|
+
const PIPELINE = 64;
|
|
12
|
+
function positive(name, value) {
|
|
13
|
+
if (!Number.isFinite(value) || value < 1) throw new RangeError(`${name} must be a positive number of milliseconds.`);
|
|
14
|
+
return Math.ceil(value);
|
|
15
|
+
}
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function text(reply) {
|
|
20
|
+
if (typeof reply === "string") return reply;
|
|
21
|
+
if (reply instanceof Uint8Array) return new TextDecoder().decode(reply);
|
|
22
|
+
if (typeof reply === "number") return String(reply);
|
|
23
|
+
throw new TypeError("Redis answered with an unexpected reply.");
|
|
24
|
+
}
|
|
25
|
+
function integer(reply) {
|
|
26
|
+
const value = typeof reply === "number" ? reply : Number(text(reply));
|
|
27
|
+
if (!Number.isSafeInteger(value)) throw new TypeError("Redis answered with an unexpected number.");
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function list(reply) {
|
|
31
|
+
if (!Array.isArray(reply)) throw new TypeError("Redis answered with an unexpected reply.");
|
|
32
|
+
return reply;
|
|
33
|
+
}
|
|
34
|
+
function isNoScript(error) {
|
|
35
|
+
return (error instanceof Error ? error.message : String(error)).includes("NOSCRIPT");
|
|
36
|
+
}
|
|
37
|
+
/** Walks pages of `size` items and runs `work` on up to `PIPELINE` of them at once. */
|
|
38
|
+
async function inBatches(items, work) {
|
|
39
|
+
for (let from = 0; from < items.length; from += PIPELINE) await Promise.all(items.slice(from, from + PIPELINE).map(work));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The adapter that makes several servers one: messages, history, idempotency and presence live in Redis (6.2 or later, or
|
|
43
|
+
* Valkey), and fan-out between servers goes through pub/sub. A hub created with it behaves exactly like one over the in-memory
|
|
44
|
+
* adapter, on as many servers as you run.
|
|
45
|
+
*
|
|
46
|
+
* What is stored, per channel `c` (`p` is the prefix; the braces make every key of one channel hash to one Redis Cluster slot):
|
|
47
|
+
* `p:{c}:m` (hash: head seq, history lifetime), `p:{c}:l` and `p:{c}:t` (sorted sets: the retained messages by seq, and when
|
|
48
|
+
* each was accepted), `p:{c}:k:<key>` (idempotency keys), `p:{c}:p` and `p:{c}:x` (presence members, and when each expires),
|
|
49
|
+
* and the topics `p:{c}:c` (messages) and `p:{c}:e` (presence events). A publish is one Lua script: it checks the key,
|
|
50
|
+
* numbers, stores, trims by count and age, and publishes, atomically, so seq order is delivery order. Every method rejects with
|
|
51
|
+
* `RealtimeError("unavailable")` when Redis cannot be reached, and leaves no half-applied state.
|
|
52
|
+
*
|
|
53
|
+
* @param options The connections and the key prefix.
|
|
54
|
+
* @returns The adapter. Close it with the hub (`createHub` closes an adapter it created; close one you passed yourself).
|
|
55
|
+
* @throws RangeError for an option that is not valid; TypeError when no subscriber is given and the client cannot create one.
|
|
56
|
+
* @example
|
|
57
|
+
* import { Redis } from "ioredis";
|
|
58
|
+
* import { createHub } from "@shadow-js/realtime/server";
|
|
59
|
+
* import { fromIoredis, redisAdapter } from "@shadow-js/realtime/redis";
|
|
60
|
+
*
|
|
61
|
+
* const adapter = redisAdapter({ client: fromIoredis(new Redis(process.env.REDIS_URL ?? "")), prefix: "myapp" });
|
|
62
|
+
* const hub = createHub({ adapter, channels: { "chat:{room}": { subscribe: true, publish: true } } });
|
|
63
|
+
* await hub.close();
|
|
64
|
+
* await adapter.close();
|
|
65
|
+
* @since 1.0.0
|
|
66
|
+
*/
|
|
67
|
+
function redisAdapter(options) {
|
|
68
|
+
const { client } = options;
|
|
69
|
+
if (typeof client?.sendCommand !== "function") throw new TypeError("redisAdapter needs a client with sendCommand (see fromIoredis, fromNodeRedis).");
|
|
70
|
+
if (options.subscriber === void 0 && client.createSubscriber === void 0) throw new TypeError("redisAdapter needs a subscriber: pass one, or a client that can create one (fromIoredis, fromNodeRedis).");
|
|
71
|
+
const prefix = options.prefix ?? "rt";
|
|
72
|
+
if (!/^[^\s{}]{1,128}$/.test(prefix)) throw new RangeError("prefix must be 1 to 128 characters without braces or whitespace.");
|
|
73
|
+
const channelTtlMs = positive("channelTtlMs", options.channelTtlMs ?? DAY_MS);
|
|
74
|
+
const graceMs = positive("presenceGraceMs", options.presenceGraceMs ?? 6e4);
|
|
75
|
+
const onError = options.onError;
|
|
76
|
+
let closed = false;
|
|
77
|
+
let ownedSubscriber;
|
|
78
|
+
function report(error, context) {
|
|
79
|
+
try {
|
|
80
|
+
onError?.(error, context);
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
function fail(error) {
|
|
84
|
+
if (error instanceof RealtimeError) return error;
|
|
85
|
+
return new RealtimeError("unavailable", closed ? "the Redis adapter is closed." : "the Redis adapter could not complete the command.", { cause: error });
|
|
86
|
+
}
|
|
87
|
+
function open() {
|
|
88
|
+
if (closed) throw new RealtimeError("unavailable", "the Redis adapter is closed.");
|
|
89
|
+
}
|
|
90
|
+
function name(channel) {
|
|
91
|
+
if (channel.length === 0 || channel.includes("{") || channel.includes("}")) throw new RealtimeError("bad_channel", "a channel name is empty or contains a brace.");
|
|
92
|
+
return `${prefix}:{${channel}}`;
|
|
93
|
+
}
|
|
94
|
+
const keysOf = (channel) => {
|
|
95
|
+
const base = name(channel);
|
|
96
|
+
return {
|
|
97
|
+
meta: `${base}:m`,
|
|
98
|
+
log: `${base}:l`,
|
|
99
|
+
time: `${base}:t`,
|
|
100
|
+
idem: (key) => `${base}:k:${key}`,
|
|
101
|
+
members: `${base}:p`,
|
|
102
|
+
expiry: `${base}:x`,
|
|
103
|
+
messages: `${base}:c`,
|
|
104
|
+
events: `${base}:e`
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
/** Runs a script by hash and, when the server does not know it (first use, a flush, a failover), by source. */
|
|
108
|
+
async function run(script, keys, args) {
|
|
109
|
+
open();
|
|
110
|
+
const tail = [
|
|
111
|
+
String(keys.length),
|
|
112
|
+
...keys,
|
|
113
|
+
...args
|
|
114
|
+
];
|
|
115
|
+
try {
|
|
116
|
+
try {
|
|
117
|
+
return await client.sendCommand([
|
|
118
|
+
"EVALSHA",
|
|
119
|
+
script.sha,
|
|
120
|
+
...tail
|
|
121
|
+
]);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (!isNoScript(error)) throw error;
|
|
124
|
+
return await client.sendCommand([
|
|
125
|
+
"EVAL",
|
|
126
|
+
script.source,
|
|
127
|
+
...tail
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw fail(error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function parseMessage(source, channel) {
|
|
135
|
+
try {
|
|
136
|
+
const value = JSON.parse(source);
|
|
137
|
+
if (isRecord(value)) {
|
|
138
|
+
const seq = value["s"];
|
|
139
|
+
const at = value["a"];
|
|
140
|
+
const event = value["e"];
|
|
141
|
+
const data = value["d"];
|
|
142
|
+
const publisher = value["p"];
|
|
143
|
+
const user = value["u"];
|
|
144
|
+
if (typeof seq === "number" && Number.isSafeInteger(seq) && seq >= 1 && typeof at === "number" && typeof event === "string" && typeof data === "string" && (publisher === void 0 || typeof publisher === "string") && (user === void 0 || typeof user === "string")) return {
|
|
145
|
+
seq,
|
|
146
|
+
at,
|
|
147
|
+
event,
|
|
148
|
+
data,
|
|
149
|
+
...publisher === void 0 ? {} : { publisher },
|
|
150
|
+
...user === void 0 ? {} : { user }
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
report(error, {
|
|
155
|
+
where: "parse",
|
|
156
|
+
channel
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
report(/* @__PURE__ */ new TypeError("a stored message has an unexpected shape."), {
|
|
161
|
+
where: "parse",
|
|
162
|
+
channel
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function memberOf(value) {
|
|
166
|
+
if (!isRecord(value)) return void 0;
|
|
167
|
+
const id = value["id"];
|
|
168
|
+
const user = value["u"];
|
|
169
|
+
const version = value["v"];
|
|
170
|
+
if (typeof id !== "string" || typeof version !== "number" || !Number.isSafeInteger(version) || !("s" in value) || user !== void 0 && typeof user !== "string") return void 0;
|
|
171
|
+
return {
|
|
172
|
+
id,
|
|
173
|
+
...user === void 0 ? {} : { u: user },
|
|
174
|
+
s: value["s"],
|
|
175
|
+
v: version
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function parseStoredMember(source, channel) {
|
|
179
|
+
try {
|
|
180
|
+
const value = JSON.parse(text(source));
|
|
181
|
+
const member = isRecord(value) ? memberOf(value["m"]) : void 0;
|
|
182
|
+
if (member !== void 0) return member;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
report(error, {
|
|
185
|
+
where: "parse",
|
|
186
|
+
channel
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
report(/* @__PURE__ */ new TypeError("a stored presence member has an unexpected shape."), {
|
|
191
|
+
where: "parse",
|
|
192
|
+
channel
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function parsePresence(source, channel) {
|
|
196
|
+
try {
|
|
197
|
+
const value = JSON.parse(source);
|
|
198
|
+
if (isRecord(value)) {
|
|
199
|
+
const op = value["o"];
|
|
200
|
+
const member = memberOf(value["m"]);
|
|
201
|
+
if ((op === "join" || op === "set" || op === "leave") && member !== void 0) return {
|
|
202
|
+
channel,
|
|
203
|
+
op,
|
|
204
|
+
member
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
} catch (error) {
|
|
208
|
+
report(error, {
|
|
209
|
+
where: "parse",
|
|
210
|
+
channel
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
report(/* @__PURE__ */ new TypeError("a presence event has an unexpected shape."), {
|
|
215
|
+
where: "parse",
|
|
216
|
+
channel
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
async function history(channel, after, limit) {
|
|
220
|
+
const keys = keysOf(channel);
|
|
221
|
+
const from = Number.isFinite(after) ? Math.min(Math.max(0, Math.floor(after)), Number.MAX_SAFE_INTEGER) : 0;
|
|
222
|
+
const count = Number.isFinite(limit) ? Math.min(Math.max(0, Math.floor(limit)), MAX_LIMIT) : 0;
|
|
223
|
+
const reply = list(await run(HISTORY, [
|
|
224
|
+
keys.meta,
|
|
225
|
+
keys.log,
|
|
226
|
+
keys.time
|
|
227
|
+
], [String(from), String(count)]));
|
|
228
|
+
const messages = [];
|
|
229
|
+
try {
|
|
230
|
+
for (const row of reply.slice(2)) {
|
|
231
|
+
const message = parseMessage(text(row), channel);
|
|
232
|
+
if (message !== void 0) messages.push(message);
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
messages,
|
|
236
|
+
head: integer(reply[0]),
|
|
237
|
+
oldest: integer(reply[1])
|
|
238
|
+
};
|
|
239
|
+
} catch (error) {
|
|
240
|
+
throw fail(error);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function members(channel) {
|
|
244
|
+
const keys = keysOf(channel);
|
|
245
|
+
const reply = list(await run(PRESENCE_LIST, [keys.members, keys.expiry], []));
|
|
246
|
+
const out = [];
|
|
247
|
+
for (const row of reply) {
|
|
248
|
+
const member = parseStoredMember(row, channel);
|
|
249
|
+
if (member !== void 0) out.push(member);
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
function subscriber() {
|
|
254
|
+
open();
|
|
255
|
+
if (options.subscriber !== void 0) return Promise.resolve(options.subscriber);
|
|
256
|
+
if (ownedSubscriber === void 0) {
|
|
257
|
+
const create = client.createSubscriber;
|
|
258
|
+
if (create === void 0) return Promise.reject(fail(/* @__PURE__ */ new TypeError("no subscriber.")));
|
|
259
|
+
ownedSubscriber = (async () => create.call(client))();
|
|
260
|
+
ownedSubscriber.catch(() => {
|
|
261
|
+
ownedSubscriber = void 0;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return ownedSubscriber.catch((error) => {
|
|
265
|
+
throw fail(error);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const topics = createTopics({
|
|
269
|
+
subscriber,
|
|
270
|
+
history: async (channel, after, limit) => (await history(channel, after, limit)).messages,
|
|
271
|
+
members,
|
|
272
|
+
keepAlive: async (channel) => {
|
|
273
|
+
await run(KEEPALIVE, [keysOf(channel).meta], [String(channelTtlMs)]);
|
|
274
|
+
},
|
|
275
|
+
parseMessage,
|
|
276
|
+
parsePresence,
|
|
277
|
+
messageTopic: (channel) => keysOf(channel).messages,
|
|
278
|
+
presenceTopic: (channel) => keysOf(channel).events,
|
|
279
|
+
report,
|
|
280
|
+
fail,
|
|
281
|
+
keepAliveMs: Math.max(100, Math.floor(channelTtlMs / 3))
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
async publish(channel, entry, publish) {
|
|
285
|
+
const keys = keysOf(channel);
|
|
286
|
+
if (typeof entry.data !== "string") throw new TypeError("a stored entry's data is the JSON text of the value.");
|
|
287
|
+
const history = Math.max(0, Math.floor(publish.history));
|
|
288
|
+
const historyTtl = positive("historyTtlMs", publish.historyTtlMs);
|
|
289
|
+
const idempotencyTtl = positive("idempotencyTtlMs", publish.idempotencyTtlMs);
|
|
290
|
+
const key = publish.key;
|
|
291
|
+
if (key !== void 0 && (key.length === 0 || key.length > MAX_KEY_BYTES)) throw new RealtimeError("bad_frame", "an idempotency key is empty or too long.");
|
|
292
|
+
const body = JSON.stringify({
|
|
293
|
+
e: entry.event,
|
|
294
|
+
d: entry.data,
|
|
295
|
+
...entry.publisher === void 0 ? {} : { p: entry.publisher },
|
|
296
|
+
...entry.user === void 0 ? {} : { u: entry.user }
|
|
297
|
+
}).slice(1);
|
|
298
|
+
const reply = list(await run(PUBLISH, [
|
|
299
|
+
keys.meta,
|
|
300
|
+
keys.log,
|
|
301
|
+
keys.time,
|
|
302
|
+
...key === void 0 ? [] : [keys.idem(key)]
|
|
303
|
+
], [
|
|
304
|
+
String(history),
|
|
305
|
+
String(historyTtl),
|
|
306
|
+
String(idempotencyTtl),
|
|
307
|
+
String(channelTtlMs),
|
|
308
|
+
keys.messages,
|
|
309
|
+
body
|
|
310
|
+
]));
|
|
311
|
+
try {
|
|
312
|
+
return {
|
|
313
|
+
seq: integer(reply[0]),
|
|
314
|
+
duplicate: integer(reply[1]) === 1
|
|
315
|
+
};
|
|
316
|
+
} catch (error) {
|
|
317
|
+
throw fail(error);
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
subscribe(channel, listener) {
|
|
321
|
+
try {
|
|
322
|
+
open();
|
|
323
|
+
name(channel);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return Promise.reject(fail(error));
|
|
326
|
+
}
|
|
327
|
+
return topics.messages(channel, listener);
|
|
328
|
+
},
|
|
329
|
+
history,
|
|
330
|
+
async presenceSet(channel, member, ttlMs) {
|
|
331
|
+
const keys = keysOf(channel);
|
|
332
|
+
if (!Number.isSafeInteger(member.v) || member.v < 0) throw new RangeError("a member version must be a non-negative integer.");
|
|
333
|
+
const json = JSON.stringify({
|
|
334
|
+
id: member.id,
|
|
335
|
+
...member.u === void 0 ? {} : { u: member.u },
|
|
336
|
+
s: member.s === void 0 ? null : member.s,
|
|
337
|
+
v: member.v
|
|
338
|
+
});
|
|
339
|
+
await run(PRESENCE_SET, [keys.members, keys.expiry], [
|
|
340
|
+
member.id,
|
|
341
|
+
String(member.v),
|
|
342
|
+
String(positive("ttlMs", ttlMs)),
|
|
343
|
+
keys.events,
|
|
344
|
+
`{"v":${member.v},"m":${json}}`,
|
|
345
|
+
json,
|
|
346
|
+
String(graceMs)
|
|
347
|
+
]);
|
|
348
|
+
},
|
|
349
|
+
async presenceRemove(channel, id) {
|
|
350
|
+
const keys = keysOf(channel);
|
|
351
|
+
return integer(await run(PRESENCE_REMOVE, [keys.members, keys.expiry], [id, keys.events])) === 1;
|
|
352
|
+
},
|
|
353
|
+
async presenceList(channel) {
|
|
354
|
+
return members(channel);
|
|
355
|
+
},
|
|
356
|
+
async presenceTouch(entries, ttlMs) {
|
|
357
|
+
const ttl = String(positive("ttlMs", ttlMs));
|
|
358
|
+
const byChannel = /* @__PURE__ */ new Map();
|
|
359
|
+
for (const ref of entries) {
|
|
360
|
+
const ids = byChannel.get(ref.channel);
|
|
361
|
+
if (ids === void 0) byChannel.set(ref.channel, [ref.id]);
|
|
362
|
+
else ids.push(ref.id);
|
|
363
|
+
}
|
|
364
|
+
const jobs = [];
|
|
365
|
+
for (const [channel, ids] of byChannel) for (let from = 0; from < ids.length; from += TOUCH_PAGE) jobs.push({
|
|
366
|
+
channel,
|
|
367
|
+
ids: ids.slice(from, from + TOUCH_PAGE)
|
|
368
|
+
});
|
|
369
|
+
const missing = [];
|
|
370
|
+
await inBatches(jobs, async ({ channel, ids }) => {
|
|
371
|
+
const keys = keysOf(channel);
|
|
372
|
+
const reply = list(await run(PRESENCE_TOUCH, [keys.members, keys.expiry], [
|
|
373
|
+
ttl,
|
|
374
|
+
String(graceMs),
|
|
375
|
+
...ids
|
|
376
|
+
]));
|
|
377
|
+
for (const id of reply) missing.push({
|
|
378
|
+
channel,
|
|
379
|
+
id: text(id)
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
return missing;
|
|
383
|
+
},
|
|
384
|
+
async presenceReap(channel) {
|
|
385
|
+
const keys = keysOf(channel);
|
|
386
|
+
const removed = [];
|
|
387
|
+
for (;;) {
|
|
388
|
+
const reply = list(await run(PRESENCE_REAP, [keys.members, keys.expiry], [keys.events, String(REAP_PAGE)]));
|
|
389
|
+
for (const row of reply) {
|
|
390
|
+
const member = parseStoredMember(row, channel);
|
|
391
|
+
if (member !== void 0) removed.push(member);
|
|
392
|
+
}
|
|
393
|
+
if (reply.length < REAP_PAGE) return removed;
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
presenceSubscribe(channel, listener) {
|
|
397
|
+
try {
|
|
398
|
+
open();
|
|
399
|
+
name(channel);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
return Promise.reject(fail(error));
|
|
402
|
+
}
|
|
403
|
+
return topics.presence(channel, listener);
|
|
404
|
+
},
|
|
405
|
+
async close() {
|
|
406
|
+
if (closed) return;
|
|
407
|
+
closed = true;
|
|
408
|
+
await topics.close();
|
|
409
|
+
const owned = ownedSubscriber;
|
|
410
|
+
ownedSubscriber = void 0;
|
|
411
|
+
if (owned !== void 0) try {
|
|
412
|
+
await (await owned).close?.();
|
|
413
|
+
} catch (error) {
|
|
414
|
+
report(error, { where: "close" });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
//#endregion
|
|
420
|
+
export { fromIoredis, fromNodeRedis, redisAdapter };
|
|
421
|
+
|
|
422
|
+
//# sourceMappingURL=redis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.js","names":[],"sources":["../../src/redis.ts"],"sourcesContent":["import type {\n Adapter,\n HistoryResult,\n PresenceRef,\n PublishOptions,\n PublishResult,\n Unsubscribe,\n} from \"./adapter\";\nimport { RealtimeError } from \"./errors\";\nimport type { Member, PresenceEvent, StoredEntry, StoredMessage } from \"./protocol\";\nimport type { RedisClient, RedisSubscriber } from \"./redis/client\";\nimport {\n HISTORY,\n KEEPALIVE,\n PRESENCE_LIST,\n PRESENCE_REAP,\n PRESENCE_REMOVE,\n PRESENCE_SET,\n PRESENCE_TOUCH,\n PUBLISH,\n type Script,\n} from \"./redis/scripts\";\nimport { createTopics, type RedisErrorContext } from \"./redis/topics\";\n\nexport {\n fromIoredis,\n fromNodeRedis,\n type IoredisLike,\n type IoredisSubscriberLike,\n type NodeRedisLike,\n type NodeRedisSubscriberLike,\n type RedisClient,\n type RedisSubscriber,\n} from \"./redis/client\";\nexport type { RedisErrorContext } from \"./redis/topics\";\n\n/**\n * Options of {@link redisAdapter}.\n * @since 1.0.0\n */\nexport interface RedisAdapterOptions {\n /** The connection that runs commands (see {@link fromIoredis} and {@link fromNodeRedis}). The adapter never closes it. */\n readonly client: RedisClient;\n /**\n * A connection in subscriber mode, shared by every subscription of this adapter. Without it the adapter asks the client for one\n * (`client.createSubscriber`) when the first subscription is made, and closes that one with the adapter. A subscriber you pass\n * is yours to close; give it to one adapter only (a topic has one listener on a subscriber).\n */\n readonly subscriber?: RedisSubscriber;\n /**\n * Prefix of every key and topic. Servers that share channels use the same one; apps (or environments) that must not share\n * use different ones. No braces or whitespace. Default `\"rt\"`.\n */\n readonly prefix?: string;\n /**\n * Milliseconds a channel's numbering outlives its last publish (and its last listener, which keeps it alive). After an idle\n * channel expired, its seq restarts at 1, which a client that resumes sees as `reset`. Default 24 hours.\n */\n readonly channelTtlMs?: number;\n /**\n * Milliseconds the presence keys of a channel outlive the last refresh of any member, so a channel nobody reaps anymore does\n * not stay in Redis forever. Default 60 seconds.\n */\n readonly presenceGraceMs?: number;\n /**\n * Receives every failure the adapter handled without surfacing it: a malformed stored entry that was skipped, a listener that\n * threw, a subscriber connection error, a recovery that gave up. Default: ignored.\n */\n readonly onError?: (error: unknown, context: RedisErrorContext) => void;\n}\n\nconst DAY_MS = 86_400_000;\nconst MAX_LIMIT = 10_000;\nconst MAX_KEY_BYTES = 1024;\nconst REAP_PAGE = 500;\nconst TOUCH_PAGE = 500;\nconst PIPELINE = 64;\n\nfunction positive(name: string, value: number): number {\n if (!Number.isFinite(value) || value < 1)\n throw new RangeError(`${name} must be a positive number of milliseconds.`);\n return Math.ceil(value);\n}\n\nfunction isRecord(value: unknown): value is Readonly<Record<string, unknown>> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction text(reply: unknown): string {\n if (typeof reply === \"string\") return reply;\n if (reply instanceof Uint8Array) return new TextDecoder().decode(reply);\n if (typeof reply === \"number\") return String(reply);\n throw new TypeError(\"Redis answered with an unexpected reply.\");\n}\n\nfunction integer(reply: unknown): number {\n const value = typeof reply === \"number\" ? reply : Number(text(reply));\n if (!Number.isSafeInteger(value))\n throw new TypeError(\"Redis answered with an unexpected number.\");\n return value;\n}\n\nfunction list(reply: unknown): readonly unknown[] {\n if (!Array.isArray(reply)) throw new TypeError(\"Redis answered with an unexpected reply.\");\n return reply;\n}\n\nfunction isNoScript(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error);\n return message.includes(\"NOSCRIPT\");\n}\n\n/** Walks pages of `size` items and runs `work` on up to `PIPELINE` of them at once. */\nasync function inBatches<T>(items: readonly T[], work: (item: T) => Promise<void>): Promise<void> {\n for (let from = 0; from < items.length; from += PIPELINE)\n await Promise.all(items.slice(from, from + PIPELINE).map(work));\n}\n\n/**\n * The adapter that makes several servers one: messages, history, idempotency and presence live in Redis (6.2 or later, or\n * Valkey), and fan-out between servers goes through pub/sub. A hub created with it behaves exactly like one over the in-memory\n * adapter, on as many servers as you run.\n *\n * What is stored, per channel `c` (`p` is the prefix; the braces make every key of one channel hash to one Redis Cluster slot):\n * `p:{c}:m` (hash: head seq, history lifetime), `p:{c}:l` and `p:{c}:t` (sorted sets: the retained messages by seq, and when\n * each was accepted), `p:{c}:k:<key>` (idempotency keys), `p:{c}:p` and `p:{c}:x` (presence members, and when each expires),\n * and the topics `p:{c}:c` (messages) and `p:{c}:e` (presence events). A publish is one Lua script: it checks the key,\n * numbers, stores, trims by count and age, and publishes, atomically, so seq order is delivery order. Every method rejects with\n * `RealtimeError(\"unavailable\")` when Redis cannot be reached, and leaves no half-applied state.\n *\n * @param options The connections and the key prefix.\n * @returns The adapter. Close it with the hub (`createHub` closes an adapter it created; close one you passed yourself).\n * @throws RangeError for an option that is not valid; TypeError when no subscriber is given and the client cannot create one.\n * @example\n * import { Redis } from \"ioredis\";\n * import { createHub } from \"@shadow-js/realtime/server\";\n * import { fromIoredis, redisAdapter } from \"@shadow-js/realtime/redis\";\n *\n * const adapter = redisAdapter({ client: fromIoredis(new Redis(process.env.REDIS_URL ?? \"\")), prefix: \"myapp\" });\n * const hub = createHub({ adapter, channels: { \"chat:{room}\": { subscribe: true, publish: true } } });\n * await hub.close();\n * await adapter.close();\n * @since 1.0.0\n */\nexport function redisAdapter(options: RedisAdapterOptions): Adapter {\n const { client } = options;\n if (typeof client?.sendCommand !== \"function\")\n throw new TypeError(\n \"redisAdapter needs a client with sendCommand (see fromIoredis, fromNodeRedis).\",\n );\n if (options.subscriber === undefined && client.createSubscriber === undefined)\n throw new TypeError(\n \"redisAdapter needs a subscriber: pass one, or a client that can create one (fromIoredis, fromNodeRedis).\",\n );\n const prefix = options.prefix ?? \"rt\";\n if (!/^[^\\s{}]{1,128}$/.test(prefix))\n throw new RangeError(\"prefix must be 1 to 128 characters without braces or whitespace.\");\n const channelTtlMs = positive(\"channelTtlMs\", options.channelTtlMs ?? DAY_MS);\n const graceMs = positive(\"presenceGraceMs\", options.presenceGraceMs ?? 60_000);\n const onError = options.onError;\n let closed = false;\n let ownedSubscriber: Promise<RedisSubscriber> | undefined;\n\n function report(error: unknown, context: RedisErrorContext): void {\n try {\n onError?.(error, context);\n } catch {\n // The reporter's failure must not become the adapter's.\n }\n }\n\n function fail(error: unknown): RealtimeError {\n if (error instanceof RealtimeError) return error;\n return new RealtimeError(\n \"unavailable\",\n closed ? \"the Redis adapter is closed.\" : \"the Redis adapter could not complete the command.\",\n { cause: error },\n );\n }\n\n function open(): void {\n if (closed) throw new RealtimeError(\"unavailable\", \"the Redis adapter is closed.\");\n }\n\n function name(channel: string): string {\n if (channel.length === 0 || channel.includes(\"{\") || channel.includes(\"}\"))\n throw new RealtimeError(\"bad_channel\", \"a channel name is empty or contains a brace.\");\n return `${prefix}:{${channel}}`;\n }\n\n const keysOf = (channel: string) => {\n const base = name(channel);\n return {\n meta: `${base}:m`,\n log: `${base}:l`,\n time: `${base}:t`,\n idem: (key: string): string => `${base}:k:${key}`,\n members: `${base}:p`,\n expiry: `${base}:x`,\n messages: `${base}:c`,\n events: `${base}:e`,\n };\n };\n\n /** Runs a script by hash and, when the server does not know it (first use, a flush, a failover), by source. */\n async function run(\n script: Script,\n keys: readonly string[],\n args: readonly string[],\n ): Promise<unknown> {\n open();\n const tail = [String(keys.length), ...keys, ...args];\n try {\n try {\n return await client.sendCommand([\"EVALSHA\", script.sha, ...tail]);\n } catch (error) {\n if (!isNoScript(error)) throw error;\n return await client.sendCommand([\"EVAL\", script.source, ...tail]);\n }\n } catch (error) {\n throw fail(error);\n }\n }\n\n // --- parsing what Redis holds --------------------------------------------------------------------------------------\n\n function parseMessage(source: string, channel: string): StoredMessage | undefined {\n try {\n const value: unknown = JSON.parse(source);\n if (isRecord(value)) {\n const seq = value[\"s\"];\n const at = value[\"a\"];\n const event = value[\"e\"];\n const data = value[\"d\"];\n const publisher = value[\"p\"];\n const user = value[\"u\"];\n if (\n typeof seq === \"number\" &&\n Number.isSafeInteger(seq) &&\n seq >= 1 &&\n typeof at === \"number\" &&\n typeof event === \"string\" &&\n typeof data === \"string\" &&\n (publisher === undefined || typeof publisher === \"string\") &&\n (user === undefined || typeof user === \"string\")\n )\n return {\n seq,\n at,\n event,\n data,\n ...(publisher === undefined ? {} : { publisher }),\n ...(user === undefined ? {} : { user }),\n };\n }\n } catch (error) {\n report(error, { where: \"parse\", channel });\n return undefined;\n }\n report(new TypeError(\"a stored message has an unexpected shape.\"), { where: \"parse\", channel });\n return undefined;\n }\n\n function memberOf(value: unknown): Member | undefined {\n if (!isRecord(value)) return undefined;\n const id = value[\"id\"];\n const user = value[\"u\"];\n const version = value[\"v\"];\n if (\n typeof id !== \"string\" ||\n typeof version !== \"number\" ||\n !Number.isSafeInteger(version) ||\n !(\"s\" in value) ||\n (user !== undefined && typeof user !== \"string\")\n )\n return undefined;\n return {\n id,\n ...(user === undefined ? {} : { u: user }),\n s: value[\"s\"] as Member[\"s\"],\n v: version,\n };\n }\n\n function parseStoredMember(source: unknown, channel: string): Member | undefined {\n try {\n const value: unknown = JSON.parse(text(source));\n const member = isRecord(value) ? memberOf(value[\"m\"]) : undefined;\n if (member !== undefined) return member;\n } catch (error) {\n report(error, { where: \"parse\", channel });\n return undefined;\n }\n report(new TypeError(\"a stored presence member has an unexpected shape.\"), {\n where: \"parse\",\n channel,\n });\n return undefined;\n }\n\n function parsePresence(source: string, channel: string): PresenceEvent | undefined {\n try {\n const value: unknown = JSON.parse(source);\n if (isRecord(value)) {\n const op = value[\"o\"];\n const member = memberOf(value[\"m\"]);\n if ((op === \"join\" || op === \"set\" || op === \"leave\") && member !== undefined)\n return { channel, op, member };\n }\n } catch (error) {\n report(error, { where: \"parse\", channel });\n return undefined;\n }\n report(new TypeError(\"a presence event has an unexpected shape.\"), { where: \"parse\", channel });\n return undefined;\n }\n\n // --- reads shared with the subscription recovery -------------------------------------------------------------------\n\n async function history(channel: string, after: number, limit: number): Promise<HistoryResult> {\n const keys = keysOf(channel);\n const from = Number.isFinite(after)\n ? Math.min(Math.max(0, Math.floor(after)), Number.MAX_SAFE_INTEGER)\n : 0;\n const count = Number.isFinite(limit) ? Math.min(Math.max(0, Math.floor(limit)), MAX_LIMIT) : 0;\n const reply = list(\n await run(HISTORY, [keys.meta, keys.log, keys.time], [String(from), String(count)]),\n );\n const messages: StoredMessage[] = [];\n try {\n for (const row of reply.slice(2)) {\n const message = parseMessage(text(row), channel);\n if (message !== undefined) messages.push(message);\n }\n return { messages, head: integer(reply[0]), oldest: integer(reply[1]) };\n } catch (error) {\n throw fail(error);\n }\n }\n\n async function members(channel: string): Promise<readonly Member[]> {\n const keys = keysOf(channel);\n const reply = list(await run(PRESENCE_LIST, [keys.members, keys.expiry], []));\n const out: Member[] = [];\n for (const row of reply) {\n const member = parseStoredMember(row, channel);\n if (member !== undefined) out.push(member);\n }\n return out;\n }\n\n // --- the shared subscriber -----------------------------------------------------------------------------------------\n\n function subscriber(): Promise<RedisSubscriber> {\n open();\n if (options.subscriber !== undefined) return Promise.resolve(options.subscriber);\n if (ownedSubscriber === undefined) {\n const create = client.createSubscriber;\n if (create === undefined) return Promise.reject(fail(new TypeError(\"no subscriber.\")));\n ownedSubscriber = (async () => create.call(client))();\n ownedSubscriber.catch(() => {\n ownedSubscriber = undefined;\n });\n }\n return ownedSubscriber.catch((error: unknown) => {\n throw fail(error);\n });\n }\n\n const topics = createTopics({\n subscriber,\n history: async (channel, after, limit) => (await history(channel, after, limit)).messages,\n members,\n keepAlive: async (channel) => {\n await run(KEEPALIVE, [keysOf(channel).meta], [String(channelTtlMs)]);\n },\n parseMessage,\n parsePresence,\n messageTopic: (channel) => keysOf(channel).messages,\n presenceTopic: (channel) => keysOf(channel).events,\n report,\n fail,\n keepAliveMs: Math.max(100, Math.floor(channelTtlMs / 3)),\n });\n\n // --- the adapter ---------------------------------------------------------------------------------------------------\n\n return {\n async publish(\n channel: string,\n entry: StoredEntry,\n publish: PublishOptions,\n ): Promise<PublishResult> {\n const keys = keysOf(channel);\n if (typeof entry.data !== \"string\")\n throw new TypeError(\"a stored entry's data is the JSON text of the value.\");\n const history = Math.max(0, Math.floor(publish.history));\n const historyTtl = positive(\"historyTtlMs\", publish.historyTtlMs);\n const idempotencyTtl = positive(\"idempotencyTtlMs\", publish.idempotencyTtlMs);\n const key = publish.key;\n if (key !== undefined && (key.length === 0 || key.length > MAX_KEY_BYTES))\n throw new RealtimeError(\"bad_frame\", \"an idempotency key is empty or too long.\");\n const body = JSON.stringify({\n e: entry.event,\n d: entry.data,\n ...(entry.publisher === undefined ? {} : { p: entry.publisher }),\n ...(entry.user === undefined ? {} : { u: entry.user }),\n }).slice(1);\n const reply = list(\n await run(\n PUBLISH,\n [keys.meta, keys.log, keys.time, ...(key === undefined ? [] : [keys.idem(key)])],\n [\n String(history),\n String(historyTtl),\n String(idempotencyTtl),\n String(channelTtlMs),\n keys.messages,\n body,\n ],\n ),\n );\n try {\n return { seq: integer(reply[0]), duplicate: integer(reply[1]) === 1 };\n } catch (error) {\n throw fail(error);\n }\n },\n\n subscribe(channel: string, listener: (message: StoredMessage) => void): Promise<Unsubscribe> {\n try {\n open();\n name(channel);\n } catch (error) {\n return Promise.reject(fail(error));\n }\n return topics.messages(channel, listener);\n },\n\n history,\n\n async presenceSet(channel: string, member: Member, ttlMs: number): Promise<void> {\n const keys = keysOf(channel);\n if (!Number.isSafeInteger(member.v) || member.v < 0)\n throw new RangeError(\"a member version must be a non-negative integer.\");\n const json = JSON.stringify({\n id: member.id,\n ...(member.u === undefined ? {} : { u: member.u }),\n s: member.s === undefined ? null : member.s,\n v: member.v,\n });\n await run(\n PRESENCE_SET,\n [keys.members, keys.expiry],\n [\n member.id,\n String(member.v),\n String(positive(\"ttlMs\", ttlMs)),\n keys.events,\n `{\"v\":${member.v},\"m\":${json}}`,\n json,\n String(graceMs),\n ],\n );\n },\n\n async presenceRemove(channel: string, id: string): Promise<boolean> {\n const keys = keysOf(channel);\n return (\n integer(await run(PRESENCE_REMOVE, [keys.members, keys.expiry], [id, keys.events])) === 1\n );\n },\n\n async presenceList(channel: string): Promise<readonly Member[]> {\n return members(channel);\n },\n\n async presenceTouch(\n entries: readonly PresenceRef[],\n ttlMs: number,\n ): Promise<readonly PresenceRef[]> {\n const ttl = String(positive(\"ttlMs\", ttlMs));\n const byChannel = new Map<string, string[]>();\n for (const ref of entries) {\n const ids = byChannel.get(ref.channel);\n if (ids === undefined) byChannel.set(ref.channel, [ref.id]);\n else ids.push(ref.id);\n }\n const jobs: { readonly channel: string; readonly ids: readonly string[] }[] = [];\n for (const [channel, ids] of byChannel)\n for (let from = 0; from < ids.length; from += TOUCH_PAGE)\n jobs.push({ channel, ids: ids.slice(from, from + TOUCH_PAGE) });\n const missing: PresenceRef[] = [];\n await inBatches(jobs, async ({ channel, ids }) => {\n const keys = keysOf(channel);\n const reply = list(\n await run(PRESENCE_TOUCH, [keys.members, keys.expiry], [ttl, String(graceMs), ...ids]),\n );\n for (const id of reply) missing.push({ channel, id: text(id) });\n });\n return missing;\n },\n\n async presenceReap(channel: string): Promise<readonly Member[]> {\n const keys = keysOf(channel);\n const removed: Member[] = [];\n for (;;) {\n const reply = list(\n await run(PRESENCE_REAP, [keys.members, keys.expiry], [keys.events, String(REAP_PAGE)]),\n );\n for (const row of reply) {\n const member = parseStoredMember(row, channel);\n if (member !== undefined) removed.push(member);\n }\n if (reply.length < REAP_PAGE) return removed;\n }\n },\n\n presenceSubscribe(\n channel: string,\n listener: (event: PresenceEvent) => void,\n ): Promise<Unsubscribe> {\n try {\n open();\n name(channel);\n } catch (error) {\n return Promise.reject(fail(error));\n }\n return topics.presence(channel, listener);\n },\n\n async close(): Promise<void> {\n if (closed) return;\n closed = true;\n await topics.close();\n const owned = ownedSubscriber;\n ownedSubscriber = undefined;\n if (owned !== undefined) {\n try {\n await (await owned).close?.();\n } catch (error) {\n report(error, { where: \"close\" });\n }\n }\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Identity } from "./rules.js";
|
|
2
|
+
//#region src/server/http.d.ts
|
|
3
|
+
/** What a hub needs from the framework's request event. @since 1.0.0 */
|
|
4
|
+
export interface RequestLike {
|
|
5
|
+
readonly request: Request;
|
|
6
|
+
}
|
|
7
|
+
/** How a connection is identified: an identity, or a response that refuses it. @since 1.0.0 */
|
|
8
|
+
export type AuthenticateResult = Identity | undefined | Response;
|
|
9
|
+
/** Options shared by the transport routes. @since 1.0.0 */
|
|
10
|
+
export interface HubRouteOptions<E extends RequestLike> {
|
|
11
|
+
/**
|
|
12
|
+
* Reads the identity from the request (the session cookie, a bearer token). Return a `Response` to refuse (a 401). Return
|
|
13
|
+
* nothing for an anonymous connection. Default: everyone is anonymous, and channel rules decide.
|
|
14
|
+
*/
|
|
15
|
+
readonly authenticate?: (event: E) => AuthenticateResult | Promise<AuthenticateResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Origins allowed to open a stream or post (full origins such as `https://app.example.com`). Default: the request's own
|
|
18
|
+
* origin only. A request with no `Origin` header (same-origin GET, a non-browser client) is allowed.
|
|
19
|
+
*/
|
|
20
|
+
readonly allowedOrigins?: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
/** The WebSocket connection the framework hands to handlers (the part this package uses). @since 1.0.0 */
|
|
23
|
+
export interface WebSocketLike {
|
|
24
|
+
readonly data: Record<string, unknown>;
|
|
25
|
+
readonly signal: AbortSignal;
|
|
26
|
+
send(message: string | Uint8Array): boolean;
|
|
27
|
+
close(code?: number, reason?: string): void;
|
|
28
|
+
}
|
|
29
|
+
/** The handlers of a WebSocket route, shaped like Dusk's `WS` export. @since 1.0.0 */
|
|
30
|
+
export interface WebSocketRoute<E extends RequestLike> {
|
|
31
|
+
upgrade(event: E): Promise<Response | undefined>;
|
|
32
|
+
open(ws: WebSocketLike, event: E): Promise<void>;
|
|
33
|
+
message(ws: WebSocketLike, message: string | Uint8Array, event: E): void;
|
|
34
|
+
close(ws: WebSocketLike): void;
|
|
35
|
+
error(ws: WebSocketLike, error: unknown, event: E): void;
|
|
36
|
+
}
|
|
37
|
+
/** The stream a server-sent events route writes to (the part this package uses). @since 1.0.0 */
|
|
38
|
+
export interface SseLike {
|
|
39
|
+
send(message: {
|
|
40
|
+
readonly event: string;
|
|
41
|
+
readonly data: string;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
close(): void;
|
|
44
|
+
readonly signal: AbortSignal;
|
|
45
|
+
readonly closed: boolean;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//# sourceMappingURL=http.d.ts.map
|