@shadow-js/realtime 0.0.0-stage → 1.0.1
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 +19 -0
- package/LICENSE +21 -0
- package/README.md +114 -2
- package/dist/dev/adapter.d.ts +73 -0
- package/dist/dev/client/core.d.ts +22 -0
- package/dist/dev/client/core.js +844 -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 +22 -0
- package/dist/dev/client/presence.js.map +1 -0
- package/dist/dev/client/sse.js +75 -0
- package/dist/dev/client/sse.js.map +1 -0
- package/dist/dev/client/types.d.ts +254 -0
- package/dist/dev/client/util.js +29 -0
- package/dist/dev/client/util.js.map +1 -0
- package/dist/dev/client/ws.js +38 -0
- package/dist/dev/client/ws.js.map +1 -0
- package/dist/dev/cloudflare/adapter.d.ts +78 -0
- package/dist/dev/cloudflare/adapter.js +346 -0
- package/dist/dev/cloudflare/adapter.js.map +1 -0
- package/dist/dev/cloudflare/core.js +700 -0
- package/dist/dev/cloudflare/core.js.map +1 -0
- package/dist/dev/cloudflare/link.js +396 -0
- package/dist/dev/cloudflare/link.js.map +1 -0
- package/dist/dev/cloudflare/object.d.ts +80 -0
- package/dist/dev/cloudflare/object.js +213 -0
- package/dist/dev/cloudflare/object.js.map +1 -0
- package/dist/dev/cloudflare/types.d.ts +98 -0
- package/dist/dev/cloudflare/wire.js +283 -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 +23 -0
- package/dist/dev/errors.js +23 -0
- package/dist/dev/errors.js.map +1 -0
- package/dist/dev/hooks.d.ts +108 -0
- package/dist/dev/hooks.js +161 -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 +31 -0
- package/dist/dev/memory.js +254 -0
- package/dist/dev/memory.js.map +1 -0
- package/dist/dev/protocol.d.ts +209 -0
- package/dist/dev/protocol.js +399 -0
- package/dist/dev/protocol.js.map +1 -0
- package/dist/dev/redis/client.d.ts +137 -0
- package/dist/dev/redis/client.js +145 -0
- package/dist/dev/redis/client.js.map +1 -0
- package/dist/dev/redis/scripts.js +274 -0
- package/dist/dev/redis/scripts.js.map +1 -0
- package/dist/dev/redis/topics.d.ts +6 -0
- package/dist/dev/redis/topics.js +376 -0
- package/dist/dev/redis/topics.js.map +1 -0
- package/dist/dev/redis.d.ts +66 -0
- package/dist/dev/redis.js +421 -0
- package/dist/dev/redis.js.map +1 -0
- package/dist/dev/server/http.d.ts +46 -0
- package/dist/dev/server/http.js +285 -0
- package/dist/dev/server/http.js.map +1 -0
- package/dist/dev/server/hub.d.ts +112 -0
- package/dist/dev/server/hub.js +826 -0
- package/dist/dev/server/hub.js.map +1 -0
- package/dist/dev/server/index.d.ts +45 -0
- package/dist/dev/server/index.js +40 -0
- package/dist/dev/server/index.js.map +1 -0
- package/dist/dev/server/limits.d.ts +52 -0
- package/dist/dev/server/limits.js +84 -0
- package/dist/dev/server/limits.js.map +1 -0
- package/dist/dev/server/rules.d.ts +41 -0
- package/dist/dev/server/rules.js +71 -0
- package/dist/dev/server/rules.js.map +1 -0
- package/dist/dev/testing.d.ts +113 -0
- package/dist/dev/testing.js +426 -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,826 @@
|
|
|
1
|
+
import { RealtimeError } from "../errors.js";
|
|
2
|
+
import { byteLength, decodeClientFrame, isChannelName, isEventName, isIdempotencyKey } from "../protocol.js";
|
|
3
|
+
import { TokenBucket, WindowCounter, resolveLimits } from "./limits.js";
|
|
4
|
+
import { memoryAdapter } from "../memory.js";
|
|
5
|
+
import { compileRule, matchRule } from "./rules.js";
|
|
6
|
+
// src/server/hub.ts
|
|
7
|
+
const REPLAY_PAGE = 500;
|
|
8
|
+
const MAX_PUMP_BUFFER = 1e4;
|
|
9
|
+
const ID_BYTES = 16;
|
|
10
|
+
/** A random id: 128 bits, base64url. */
|
|
11
|
+
function randomId() {
|
|
12
|
+
const bytes = new Uint8Array(ID_BYTES);
|
|
13
|
+
crypto.getRandomValues(bytes);
|
|
14
|
+
let text = "";
|
|
15
|
+
for (const byte of bytes) text += String.fromCharCode(byte);
|
|
16
|
+
return btoa(text).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
17
|
+
}
|
|
18
|
+
function encodeFrame(frame) {
|
|
19
|
+
return JSON.stringify(frame);
|
|
20
|
+
}
|
|
21
|
+
function messageFrame(channel, message, replayed) {
|
|
22
|
+
return `{"t":"msg","c":${JSON.stringify(channel)},"s":${message.seq},"e":${JSON.stringify(message.event)},"d":${message.data}` + (message.publisher === void 0 ? "" : `,"p":${JSON.stringify(message.publisher)}`) + (message.user === void 0 ? "" : `,"u":${JSON.stringify(message.user)}`) + (replayed ? ",\"r\":1}" : "}");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Creates the hub: the server side of `@shadow-js/realtime`. It owns connections and channel rules, enforces limits and talks to
|
|
26
|
+
* one adapter. Wire it to a transport with `hub.websocket()`, `hub.sse()` and `hub.post()` (Dusk routes) or with `hub.connect`.
|
|
27
|
+
*
|
|
28
|
+
* @param options The adapter, the channel rules and the limits.
|
|
29
|
+
* @returns The hub.
|
|
30
|
+
* @throws RangeError for a limit that is not valid; TypeError for an invalid channel pattern.
|
|
31
|
+
* @example
|
|
32
|
+
* import { createHub } from "@shadow-js/realtime/server";
|
|
33
|
+
*
|
|
34
|
+
* const hub = createHub({
|
|
35
|
+
* channels: { "chat:{room}": { subscribe: true, publish: ({ identity }) => identity.user !== undefined } },
|
|
36
|
+
* });
|
|
37
|
+
* const sent: string[] = [];
|
|
38
|
+
* const connection = hub.connect({ send: (frame) => (sent.push(frame), true), close: () => {} });
|
|
39
|
+
* connection.receive('{"t":"sub","n":1,"c":"chat:lobby"}');
|
|
40
|
+
* @since 1.0.0
|
|
41
|
+
*/
|
|
42
|
+
function createHubCore(options = {}) {
|
|
43
|
+
const limits = resolveLimits(options.limits);
|
|
44
|
+
const now = options.now ?? Date.now;
|
|
45
|
+
const ownsAdapter = options.adapter === void 0;
|
|
46
|
+
const adapter = options.adapter ?? memoryAdapter({ now });
|
|
47
|
+
const rules = [];
|
|
48
|
+
const conns = /* @__PURE__ */ new Map();
|
|
49
|
+
const chans = /* @__PURE__ */ new Map();
|
|
50
|
+
let heartbeat;
|
|
51
|
+
let closing = false;
|
|
52
|
+
let beating = false;
|
|
53
|
+
const counters = {
|
|
54
|
+
published: 0,
|
|
55
|
+
delivered: 0,
|
|
56
|
+
refused: 0,
|
|
57
|
+
slowConsumers: 0
|
|
58
|
+
};
|
|
59
|
+
const report = (error, context) => {
|
|
60
|
+
if (options.onError !== void 0) try {
|
|
61
|
+
options.onError(error, context);
|
|
62
|
+
} catch {}
|
|
63
|
+
else console.error(`[realtime] ${context.where}`, error);
|
|
64
|
+
};
|
|
65
|
+
function match(channel) {
|
|
66
|
+
for (const compiled of rules) {
|
|
67
|
+
const params = matchRule(compiled, channel);
|
|
68
|
+
if (params !== void 0) return {
|
|
69
|
+
rule: compiled.rule,
|
|
70
|
+
params
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function allowed(rule, op, channel, params, identity) {
|
|
75
|
+
const permission = rule[op];
|
|
76
|
+
if (permission === void 0 || permission === false) return false;
|
|
77
|
+
if (permission === true) return true;
|
|
78
|
+
try {
|
|
79
|
+
return await permission({
|
|
80
|
+
channel,
|
|
81
|
+
params,
|
|
82
|
+
identity,
|
|
83
|
+
op
|
|
84
|
+
}) === true;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
report(error, {
|
|
87
|
+
where: `rule.${op}`,
|
|
88
|
+
channel
|
|
89
|
+
});
|
|
90
|
+
throw new RealtimeError("unavailable", "the channel rule could not be evaluated");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function send(conn, text) {
|
|
94
|
+
if (conn.closed) return;
|
|
95
|
+
try {
|
|
96
|
+
if ((conn.transport.buffered?.() ?? 0) > limits.maxBufferedBytes) {
|
|
97
|
+
counters.slowConsumers += 1;
|
|
98
|
+
drop(conn, 1013, "slow consumer");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (conn.transport.send(text)) return;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
report(error, {
|
|
104
|
+
where: "transport.send",
|
|
105
|
+
connection: conn.id
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
closeConnection(conn);
|
|
109
|
+
}
|
|
110
|
+
function sendFrame(conn, frame) {
|
|
111
|
+
send(conn, encodeFrame(frame));
|
|
112
|
+
}
|
|
113
|
+
function fail(conn, n, code, channel, message) {
|
|
114
|
+
counters.refused += 1;
|
|
115
|
+
sendFrame(conn, {
|
|
116
|
+
t: "err",
|
|
117
|
+
...n === void 0 ? {} : { n },
|
|
118
|
+
...channel === void 0 ? {} : { c: channel },
|
|
119
|
+
code,
|
|
120
|
+
...message === void 0 ? {} : { msg: message }
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** Sends a final frame, closes the transport and releases the connection. */
|
|
124
|
+
function drop(conn, code, reason) {
|
|
125
|
+
if (conn.closed) return;
|
|
126
|
+
try {
|
|
127
|
+
conn.transport.send(encodeFrame({
|
|
128
|
+
t: "bye",
|
|
129
|
+
code,
|
|
130
|
+
why: reason
|
|
131
|
+
}));
|
|
132
|
+
} catch {}
|
|
133
|
+
try {
|
|
134
|
+
conn.transport.close(code, reason);
|
|
135
|
+
} catch {}
|
|
136
|
+
closeConnection(conn);
|
|
137
|
+
}
|
|
138
|
+
function channelState(name) {
|
|
139
|
+
let chan = chans.get(name);
|
|
140
|
+
if (chan === void 0 || chan.closing) {
|
|
141
|
+
chan = {
|
|
142
|
+
name,
|
|
143
|
+
subs: /* @__PURE__ */ new Set(),
|
|
144
|
+
bucket: new TokenBucket(limits.channelPublishesPerSecond, limits.channelPublishesPerSecond, now()),
|
|
145
|
+
adapterSub: void 0,
|
|
146
|
+
presenceSub: void 0,
|
|
147
|
+
closing: false,
|
|
148
|
+
frame: void 0
|
|
149
|
+
};
|
|
150
|
+
chans.set(name, chan);
|
|
151
|
+
}
|
|
152
|
+
return chan;
|
|
153
|
+
}
|
|
154
|
+
async function ensureAdapterSub(chan) {
|
|
155
|
+
if (chan.adapterSub === void 0) {
|
|
156
|
+
chan.adapterSub = adapter.subscribe(chan.name, (message) => onMessage(chan, message));
|
|
157
|
+
chan.adapterSub.catch(() => {
|
|
158
|
+
chan.adapterSub = void 0;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
await chan.adapterSub;
|
|
162
|
+
}
|
|
163
|
+
async function ensurePresenceSub(chan) {
|
|
164
|
+
if (chan.presenceSub === void 0) {
|
|
165
|
+
chan.presenceSub = adapter.presenceSubscribe(chan.name, (event) => onPresence(chan, event));
|
|
166
|
+
chan.presenceSub.catch(() => {
|
|
167
|
+
chan.presenceSub = void 0;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
await chan.presenceSub;
|
|
171
|
+
}
|
|
172
|
+
function onMessage(chan, message) {
|
|
173
|
+
for (const sub of chan.subs) {
|
|
174
|
+
if (!sub.active) continue;
|
|
175
|
+
if (sub.pumping) {
|
|
176
|
+
if (sub.buffer.length >= MAX_PUMP_BUFFER) removeSub(sub, "gap");
|
|
177
|
+
else sub.buffer.push(message);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (message.seq === sub.lastSeq + 1) deliver(sub, message, false);
|
|
181
|
+
else if (message.seq > sub.lastSeq + 1) {
|
|
182
|
+
sub.buffer.push(message);
|
|
183
|
+
pump(sub, void 0);
|
|
184
|
+
} else if (message.at > sub.lastAt) removeSub(sub, "reset");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function deliver(sub, message, replayed) {
|
|
188
|
+
sub.lastSeq = message.seq;
|
|
189
|
+
sub.lastAt = Math.max(sub.lastAt, message.at);
|
|
190
|
+
if (!sub.echo && message.publisher === sub.conn.id) return;
|
|
191
|
+
counters.delivered += 1;
|
|
192
|
+
if (replayed) {
|
|
193
|
+
send(sub.conn, messageFrame(sub.channel.name, message, true));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const cached = sub.channel.frame;
|
|
197
|
+
const text = cached !== void 0 && cached.seq === message.seq ? cached.text : messageFrame(sub.channel.name, message, false);
|
|
198
|
+
if (cached === void 0 || cached.seq !== message.seq) sub.channel.frame = {
|
|
199
|
+
seq: message.seq,
|
|
200
|
+
text
|
|
201
|
+
};
|
|
202
|
+
send(sub.conn, text);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Delivers, in order and without a gap, the messages after `sub.lastSeq` up to `upTo` and whatever is buffered. Fetches the
|
|
206
|
+
* missing ones from history. When history no longer has them: a subscription that reported a gap skips ahead; any other one
|
|
207
|
+
* is removed with `unsubbed why:"gap"`.
|
|
208
|
+
*/
|
|
209
|
+
async function pump(sub, upTo) {
|
|
210
|
+
if (sub.pumping && upTo === void 0) return;
|
|
211
|
+
sub.pumping = true;
|
|
212
|
+
try {
|
|
213
|
+
while (sub.active) {
|
|
214
|
+
sub.buffer = sub.buffer.filter((message) => message.seq > sub.lastSeq);
|
|
215
|
+
const next = sub.buffer[0];
|
|
216
|
+
if (next !== void 0 && next.seq === sub.lastSeq + 1) {
|
|
217
|
+
deliver(sub, next, false);
|
|
218
|
+
sub.buffer.shift();
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const target = next === void 0 ? upTo ?? 0 : next.seq - 1;
|
|
222
|
+
if (sub.lastSeq >= target) break;
|
|
223
|
+
const page = await adapter.history(sub.channel.name, sub.lastSeq, Math.min(REPLAY_PAGE, target - sub.lastSeq));
|
|
224
|
+
if (!sub.active) return;
|
|
225
|
+
const first = page.messages[0];
|
|
226
|
+
if (first === void 0 || first.seq !== sub.lastSeq + 1) {
|
|
227
|
+
if (!sub.gapOk) {
|
|
228
|
+
removeSub(sub, "gap");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
sub.lastSeq = (first?.seq ?? target + 1) - 1;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
for (const message of page.messages) {
|
|
235
|
+
if (message.seq > target) break;
|
|
236
|
+
deliver(sub, message, true);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
report(error, {
|
|
241
|
+
where: "pump",
|
|
242
|
+
channel: sub.channel.name,
|
|
243
|
+
connection: sub.conn.id
|
|
244
|
+
});
|
|
245
|
+
if (sub.active) removeSub(sub, "unavailable");
|
|
246
|
+
} finally {
|
|
247
|
+
sub.pumping = false;
|
|
248
|
+
}
|
|
249
|
+
if (sub.active && sub.buffer.length > 0) pump(sub, void 0);
|
|
250
|
+
}
|
|
251
|
+
function onPresence(chan, event) {
|
|
252
|
+
for (const sub of chan.subs) {
|
|
253
|
+
if (!sub.active || !sub.joined && !sub.watch) continue;
|
|
254
|
+
if (sub.presenceQueue !== void 0) sub.presenceQueue.push(event);
|
|
255
|
+
else sendPresence(sub, event);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function sendPresence(sub, event) {
|
|
259
|
+
sendFrame(sub.conn, {
|
|
260
|
+
t: "pres",
|
|
261
|
+
c: event.channel,
|
|
262
|
+
op: event.op,
|
|
263
|
+
m: event.member
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/** Removes a subscription from its connection and channel; tells the peer when `why` is given. */
|
|
267
|
+
function removeSub(sub, why) {
|
|
268
|
+
if (!sub.active) return;
|
|
269
|
+
sub.active = false;
|
|
270
|
+
sub.conn.subs.delete(sub.channel.name);
|
|
271
|
+
sub.channel.subs.delete(sub);
|
|
272
|
+
if (why !== void 0 && !sub.conn.closed) sendFrame(sub.conn, {
|
|
273
|
+
t: "unsubbed",
|
|
274
|
+
c: sub.channel.name,
|
|
275
|
+
...why === "" ? {} : { why }
|
|
276
|
+
});
|
|
277
|
+
releaseSub(sub);
|
|
278
|
+
}
|
|
279
|
+
/** The releases still running, so that `close` finishes them before it closes the adapter. */
|
|
280
|
+
const releasing = /* @__PURE__ */ new Set();
|
|
281
|
+
function releaseSub(sub) {
|
|
282
|
+
const running = release(sub);
|
|
283
|
+
releasing.add(running);
|
|
284
|
+
running.finally(() => releasing.delete(running));
|
|
285
|
+
return running;
|
|
286
|
+
}
|
|
287
|
+
async function release(sub) {
|
|
288
|
+
const chan = sub.channel;
|
|
289
|
+
try {
|
|
290
|
+
await sub.joining?.catch(() => void 0);
|
|
291
|
+
if (sub.joined) {
|
|
292
|
+
sub.joined = false;
|
|
293
|
+
await adapter.presenceRemove(chan.name, sub.conn.id);
|
|
294
|
+
}
|
|
295
|
+
} catch (error) {
|
|
296
|
+
report(error, {
|
|
297
|
+
where: "presenceRemove",
|
|
298
|
+
channel: chan.name,
|
|
299
|
+
connection: sub.conn.id
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
if (chan.subs.size > 0 || chan.closing) return;
|
|
303
|
+
chan.closing = true;
|
|
304
|
+
if (chans.get(chan.name) === chan) chans.delete(chan.name);
|
|
305
|
+
const stops = [chan.adapterSub, chan.presenceSub].filter((item) => item !== void 0);
|
|
306
|
+
await Promise.all(stops.map(async (stop) => {
|
|
307
|
+
try {
|
|
308
|
+
await (await stop)();
|
|
309
|
+
} catch (error) {
|
|
310
|
+
report(error, {
|
|
311
|
+
where: "unsubscribe",
|
|
312
|
+
channel: chan.name
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
function identityDenied(conn) {
|
|
318
|
+
return conn.identity.user === void 0 ? "unauthorized" : "forbidden";
|
|
319
|
+
}
|
|
320
|
+
async function subscribe(conn, frame) {
|
|
321
|
+
const { c: name, n } = frame;
|
|
322
|
+
if (conn.subs.has(name)) return fail(conn, n, "conflict", name, "already subscribed");
|
|
323
|
+
if (conn.subs.size >= limits.maxChannelsPerConnection) return fail(conn, n, "limit", name, "too many channels on this connection");
|
|
324
|
+
const found = match(name);
|
|
325
|
+
if (found === void 0 || !await allowed(found.rule, "subscribe", name, found.params, conn.identity)) return fail(conn, n, identityDenied(conn), name);
|
|
326
|
+
const wantsPresence = frame.hasPresence || frame.watch === true;
|
|
327
|
+
if (wantsPresence && !await allowed(found.rule, "presence", name, found.params, conn.identity)) return fail(conn, n, identityDenied(conn), name);
|
|
328
|
+
if (frame.hasPresence && byteLength(JSON.stringify(frame.presence ?? null)) > limits.maxStateBytes) return fail(conn, n, "too_large", name, "the presence state is too large");
|
|
329
|
+
if (conn.closed) return;
|
|
330
|
+
const chan = channelState(name);
|
|
331
|
+
if (chan.subs.size >= limits.maxSubscribersPerChannel) return fail(conn, n, "limit", name, "the channel is full");
|
|
332
|
+
const sub = {
|
|
333
|
+
conn,
|
|
334
|
+
channel: chan,
|
|
335
|
+
rule: found.rule,
|
|
336
|
+
params: found.params,
|
|
337
|
+
echo: frame.echo !== false,
|
|
338
|
+
watch: frame.watch === true,
|
|
339
|
+
joined: false,
|
|
340
|
+
version: 0,
|
|
341
|
+
lastSeq: 0,
|
|
342
|
+
lastAt: now(),
|
|
343
|
+
pumping: true,
|
|
344
|
+
gapOk: false,
|
|
345
|
+
buffer: [],
|
|
346
|
+
presenceQueue: wantsPresence ? [] : void 0,
|
|
347
|
+
joining: void 0,
|
|
348
|
+
active: true
|
|
349
|
+
};
|
|
350
|
+
conn.subs.set(name, sub);
|
|
351
|
+
chan.subs.add(sub);
|
|
352
|
+
try {
|
|
353
|
+
await ensureAdapterSub(chan);
|
|
354
|
+
if (wantsPresence) {
|
|
355
|
+
await ensurePresenceSub(chan);
|
|
356
|
+
startHeartbeat();
|
|
357
|
+
}
|
|
358
|
+
const probe = await adapter.history(name, Number.MAX_SAFE_INTEGER, 0);
|
|
359
|
+
if (!sub.active) return;
|
|
360
|
+
const head = probe.head;
|
|
361
|
+
const after = frame.after;
|
|
362
|
+
let gap = false;
|
|
363
|
+
let reset = false;
|
|
364
|
+
if (after === void 0) sub.lastSeq = head;
|
|
365
|
+
else if (after > head) {
|
|
366
|
+
reset = true;
|
|
367
|
+
sub.lastSeq = head;
|
|
368
|
+
} else {
|
|
369
|
+
sub.lastSeq = after;
|
|
370
|
+
gap = probe.oldest === 0 ? head > after : probe.oldest > after + 1;
|
|
371
|
+
sub.gapOk = gap;
|
|
372
|
+
}
|
|
373
|
+
sendFrame(conn, {
|
|
374
|
+
t: "subbed",
|
|
375
|
+
n,
|
|
376
|
+
c: name,
|
|
377
|
+
head,
|
|
378
|
+
...gap ? { gap: true } : {},
|
|
379
|
+
...reset ? { reset: true } : {}
|
|
380
|
+
});
|
|
381
|
+
if (frame.hasPresence) {
|
|
382
|
+
sub.joining = joinPresence(sub, frame.presence ?? null);
|
|
383
|
+
await sub.joining;
|
|
384
|
+
}
|
|
385
|
+
if (wantsPresence) await syncPresence(sub);
|
|
386
|
+
if (!sub.active) return;
|
|
387
|
+
await pump(sub, head);
|
|
388
|
+
sub.gapOk = false;
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (sub.active) {
|
|
391
|
+
removeSub(sub);
|
|
392
|
+
report(error, {
|
|
393
|
+
where: "subscribe",
|
|
394
|
+
channel: name,
|
|
395
|
+
connection: conn.id
|
|
396
|
+
});
|
|
397
|
+
fail(conn, n, error instanceof RealtimeError ? error.code : "unavailable", name);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function joinPresence(sub, state) {
|
|
402
|
+
if ((await adapter.presenceList(sub.channel.name)).length >= limits.maxPresenceMembers) throw new RealtimeError("limit", "the presence set is full");
|
|
403
|
+
sub.version = 1;
|
|
404
|
+
sub.joined = true;
|
|
405
|
+
startHeartbeat();
|
|
406
|
+
const member = {
|
|
407
|
+
id: sub.conn.id,
|
|
408
|
+
...sub.conn.identity.user === void 0 ? {} : { u: sub.conn.identity.user },
|
|
409
|
+
s: state,
|
|
410
|
+
v: 1
|
|
411
|
+
};
|
|
412
|
+
await adapter.presenceSet(sub.channel.name, member, limits.presenceTtlMs);
|
|
413
|
+
}
|
|
414
|
+
async function syncPresence(sub) {
|
|
415
|
+
const members = await adapter.presenceList(sub.channel.name);
|
|
416
|
+
if (!sub.active) return;
|
|
417
|
+
sendFrame(sub.conn, {
|
|
418
|
+
t: "pres",
|
|
419
|
+
c: sub.channel.name,
|
|
420
|
+
op: "sync",
|
|
421
|
+
m: members
|
|
422
|
+
});
|
|
423
|
+
const queued = sub.presenceQueue ?? [];
|
|
424
|
+
sub.presenceQueue = void 0;
|
|
425
|
+
const known = new Map(members.map((member) => [member.id, member.v]));
|
|
426
|
+
for (const event of queued) {
|
|
427
|
+
const version = known.get(event.member.id);
|
|
428
|
+
if (event.op === "leave" ? version === void 0 : version !== void 0 && version >= event.member.v) continue;
|
|
429
|
+
sendPresence(sub, event);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async function setPresence(conn, frame) {
|
|
433
|
+
const sub = conn.subs.get(frame.c);
|
|
434
|
+
if (sub === void 0 || !sub.joined) return fail(conn, void 0, "not_subscribed", frame.c);
|
|
435
|
+
if (byteLength(JSON.stringify(frame.s)) > limits.maxStateBytes) return fail(conn, void 0, "too_large", frame.c, "the presence state is too large");
|
|
436
|
+
if (!conn.publishBucket.take(now())) return limit(conn, void 0, frame.c);
|
|
437
|
+
sub.version += 1;
|
|
438
|
+
const member = {
|
|
439
|
+
id: conn.id,
|
|
440
|
+
...conn.identity.user === void 0 ? {} : { u: conn.identity.user },
|
|
441
|
+
s: frame.s,
|
|
442
|
+
v: sub.version
|
|
443
|
+
};
|
|
444
|
+
try {
|
|
445
|
+
await adapter.presenceSet(frame.c, member, limits.presenceTtlMs);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
report(error, {
|
|
448
|
+
where: "presenceSet",
|
|
449
|
+
channel: frame.c,
|
|
450
|
+
connection: conn.id
|
|
451
|
+
});
|
|
452
|
+
fail(conn, void 0, "unavailable", frame.c);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function limit(conn, n, channel) {
|
|
456
|
+
fail(conn, n, "rate_limited", channel);
|
|
457
|
+
if (conn.limited.hit(now())) drop(conn, 1008, "rate limit exceeded");
|
|
458
|
+
}
|
|
459
|
+
async function publishFrame(conn, frame) {
|
|
460
|
+
const { c: name, n } = frame;
|
|
461
|
+
const found = match(name);
|
|
462
|
+
if (found === void 0 || !await allowed(found.rule, "publish", name, found.params, conn.identity)) return fail(conn, n, identityDenied(conn), name);
|
|
463
|
+
if (frame.d.length > limits.maxMessageBytes) return fail(conn, n, "too_large", name, "the message is too large");
|
|
464
|
+
if (!conn.publishBucket.take(now()) || chans.get(name)?.bucket.take(now()) === false) return limit(conn, n, name);
|
|
465
|
+
try {
|
|
466
|
+
const result = await adapter.publish(name, {
|
|
467
|
+
event: frame.e,
|
|
468
|
+
data: frame.d,
|
|
469
|
+
publisher: conn.id,
|
|
470
|
+
...conn.identity.user === void 0 ? {} : { user: conn.identity.user }
|
|
471
|
+
}, {
|
|
472
|
+
history: found.rule.history ?? limits.history,
|
|
473
|
+
historyTtlMs: limits.historyTtlMs,
|
|
474
|
+
idempotencyTtlMs: limits.idempotencyTtlMs,
|
|
475
|
+
...frame.k === void 0 ? {} : { key: frame.k }
|
|
476
|
+
});
|
|
477
|
+
counters.published += result.duplicate ? 0 : 1;
|
|
478
|
+
if (n !== void 0) sendFrame(conn, {
|
|
479
|
+
t: "ack",
|
|
480
|
+
n,
|
|
481
|
+
s: result.seq,
|
|
482
|
+
...result.duplicate ? { dup: true } : {}
|
|
483
|
+
});
|
|
484
|
+
} catch (error) {
|
|
485
|
+
report(error, {
|
|
486
|
+
where: "publish",
|
|
487
|
+
channel: name,
|
|
488
|
+
connection: conn.id
|
|
489
|
+
});
|
|
490
|
+
fail(conn, n, "unavailable", name);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
async function request(conn, frame) {
|
|
494
|
+
const { c: name, n } = frame;
|
|
495
|
+
let target = conn.subs.get(name);
|
|
496
|
+
if (target === void 0 && conn.stateless) {
|
|
497
|
+
const found = match(name);
|
|
498
|
+
if (found !== void 0 && await allowed(found.rule, "subscribe", name, found.params, conn.identity)) target = found;
|
|
499
|
+
}
|
|
500
|
+
if (target === void 0) return fail(conn, n, identityDenied(conn) === "unauthorized" && conn.stateless ? "unauthorized" : "not_subscribed", name);
|
|
501
|
+
const sub = target;
|
|
502
|
+
const handler = sub.rule.handlers?.[frame.e];
|
|
503
|
+
if (handler === void 0) return fail(conn, n, "no_handler", name);
|
|
504
|
+
if (conn.pending.size >= limits.maxPendingRequests) return fail(conn, n, "limit", name, "too many requests in flight");
|
|
505
|
+
if (!conn.publishBucket.take(now())) return limit(conn, n, name);
|
|
506
|
+
const controller = new AbortController();
|
|
507
|
+
conn.pending.add(controller);
|
|
508
|
+
const timeout = Math.min(frame.ms ?? limits.requestTimeoutMs, limits.requestTimeoutMs);
|
|
509
|
+
const timer = setTimeout(() => controller.abort(new RealtimeError("timeout")), timeout);
|
|
510
|
+
(async () => {
|
|
511
|
+
try {
|
|
512
|
+
const run = Promise.resolve(handler({
|
|
513
|
+
channel: name,
|
|
514
|
+
params: sub.params,
|
|
515
|
+
identity: conn.identity,
|
|
516
|
+
event: frame.e,
|
|
517
|
+
data: frame.d,
|
|
518
|
+
signal: controller.signal
|
|
519
|
+
}));
|
|
520
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
521
|
+
controller.signal.addEventListener("abort", () => reject(controller.signal.reason), { once: true });
|
|
522
|
+
});
|
|
523
|
+
const reply = await Promise.race([run, aborted]) ?? null;
|
|
524
|
+
const text = JSON.stringify(reply);
|
|
525
|
+
if (text === void 0 || byteLength(text) > limits.maxMessageBytes) return fail(conn, n, "too_large", name, "the reply is too large");
|
|
526
|
+
send(conn, `{"t":"rep","n":${n},"d":${text}}`);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
if (error instanceof RealtimeError) fail(conn, n, error.code, name, error.code === "timeout" || error.code === "closed" ? void 0 : error.message);
|
|
529
|
+
else {
|
|
530
|
+
report(error, {
|
|
531
|
+
where: "request",
|
|
532
|
+
channel: name,
|
|
533
|
+
connection: conn.id
|
|
534
|
+
});
|
|
535
|
+
fail(conn, n, "handler_error", name);
|
|
536
|
+
}
|
|
537
|
+
} finally {
|
|
538
|
+
clearTimeout(timer);
|
|
539
|
+
conn.pending.delete(controller);
|
|
540
|
+
settle(conn);
|
|
541
|
+
}
|
|
542
|
+
})();
|
|
543
|
+
}
|
|
544
|
+
async function handle(conn, text) {
|
|
545
|
+
const decoded = decodeClientFrame(text, limits.maxMessageBytes);
|
|
546
|
+
if (!decoded.ok) {
|
|
547
|
+
fail(conn, decoded.n, decoded.code, void 0, decoded.why);
|
|
548
|
+
if (conn.invalid.hit(now())) drop(conn, 1008, "too many invalid frames");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const frame = decoded.frame;
|
|
552
|
+
try {
|
|
553
|
+
switch (frame.t) {
|
|
554
|
+
case "hello": return;
|
|
555
|
+
case "ping":
|
|
556
|
+
sendFrame(conn, {
|
|
557
|
+
t: "pong",
|
|
558
|
+
...frame.n === void 0 ? {} : { n: frame.n }
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
case "sub": return await subscribe(conn, frame);
|
|
562
|
+
case "unsub": {
|
|
563
|
+
const sub = conn.subs.get(frame.c);
|
|
564
|
+
if (sub === void 0) return fail(conn, frame.n, "not_subscribed", frame.c);
|
|
565
|
+
removeSub(sub);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
case "pub": return await publishFrame(conn, frame);
|
|
569
|
+
case "req": return await request(conn, frame);
|
|
570
|
+
case "pres": return await setPresence(conn, frame);
|
|
571
|
+
}
|
|
572
|
+
} catch (error) {
|
|
573
|
+
const n = "n" in frame ? frame.n : void 0;
|
|
574
|
+
const channel = "c" in frame ? frame.c : void 0;
|
|
575
|
+
if (error instanceof RealtimeError) fail(conn, n, error.code, channel);
|
|
576
|
+
else {
|
|
577
|
+
report(error, {
|
|
578
|
+
where: "frame",
|
|
579
|
+
connection: conn.id,
|
|
580
|
+
...channel === void 0 ? {} : { channel }
|
|
581
|
+
});
|
|
582
|
+
fail(conn, n, "unavailable", channel);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async function run(conn) {
|
|
587
|
+
if (conn.running) return;
|
|
588
|
+
conn.running = true;
|
|
589
|
+
try {
|
|
590
|
+
for (let text = conn.queue.shift(); text !== void 0; text = conn.queue.shift()) {
|
|
591
|
+
if (conn.closed) break;
|
|
592
|
+
try {
|
|
593
|
+
await handle(conn, text);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
if (error instanceof RealtimeError) fail(conn, void 0, error.code);
|
|
596
|
+
else {
|
|
597
|
+
report(error, {
|
|
598
|
+
where: "frame",
|
|
599
|
+
connection: conn.id
|
|
600
|
+
});
|
|
601
|
+
fail(conn, void 0, "unavailable");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
} finally {
|
|
606
|
+
conn.running = false;
|
|
607
|
+
settle(conn);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/** Wakes the callers of `settled` when the connection has nothing left to do. */
|
|
611
|
+
function settle(conn) {
|
|
612
|
+
if (conn.running || conn.queue.length > 0 || conn.pending.size > 0) return;
|
|
613
|
+
for (const wake of conn.waiters.splice(0)) wake();
|
|
614
|
+
}
|
|
615
|
+
async function closeConnection(conn) {
|
|
616
|
+
if (conn.closed) return conn.done;
|
|
617
|
+
conn.closed = true;
|
|
618
|
+
if (conns.get(conn.id) === conn) conns.delete(conn.id);
|
|
619
|
+
for (const controller of conn.pending) controller.abort(new RealtimeError("closed"));
|
|
620
|
+
conn.pending.clear();
|
|
621
|
+
conn.queue.length = 0;
|
|
622
|
+
for (const wake of conn.waiters.splice(0)) wake();
|
|
623
|
+
const subs = [...conn.subs.values()];
|
|
624
|
+
for (const sub of subs) {
|
|
625
|
+
sub.active = false;
|
|
626
|
+
sub.channel.subs.delete(sub);
|
|
627
|
+
}
|
|
628
|
+
conn.subs.clear();
|
|
629
|
+
await Promise.allSettled(subs.map((sub) => releaseSub(sub)));
|
|
630
|
+
conn.release?.();
|
|
631
|
+
}
|
|
632
|
+
function startHeartbeat() {
|
|
633
|
+
if (heartbeat !== void 0 || closing) return;
|
|
634
|
+
const timer = setInterval(() => void beat(), limits.presenceHeartbeatMs);
|
|
635
|
+
timer.unref?.();
|
|
636
|
+
heartbeat = timer;
|
|
637
|
+
}
|
|
638
|
+
async function beat() {
|
|
639
|
+
if (beating) return;
|
|
640
|
+
beating = true;
|
|
641
|
+
try {
|
|
642
|
+
await beatOnce();
|
|
643
|
+
} finally {
|
|
644
|
+
beating = false;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async function beatOnce() {
|
|
648
|
+
const refs = [];
|
|
649
|
+
const watched = /* @__PURE__ */ new Set();
|
|
650
|
+
for (const conn of conns.values()) for (const sub of conn.subs.values()) {
|
|
651
|
+
if (sub.joined) refs.push({
|
|
652
|
+
channel: sub.channel.name,
|
|
653
|
+
id: conn.id
|
|
654
|
+
});
|
|
655
|
+
if (sub.joined || sub.watch) watched.add(sub.channel.name);
|
|
656
|
+
}
|
|
657
|
+
if (refs.length === 0 && watched.size === 0) {
|
|
658
|
+
clearInterval(heartbeat);
|
|
659
|
+
heartbeat = void 0;
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
const missing = await adapter.presenceTouch(refs, limits.presenceTtlMs);
|
|
664
|
+
for (const ref of missing) {
|
|
665
|
+
const sub = conns.get(ref.id)?.subs.get(ref.channel);
|
|
666
|
+
if (sub === void 0 || !sub.joined) continue;
|
|
667
|
+
await adapter.presenceSet(ref.channel, {
|
|
668
|
+
id: ref.id,
|
|
669
|
+
...sub.conn.identity.user === void 0 ? {} : { u: sub.conn.identity.user },
|
|
670
|
+
s: null,
|
|
671
|
+
v: sub.version
|
|
672
|
+
}, limits.presenceTtlMs);
|
|
673
|
+
}
|
|
674
|
+
for (const channel of watched) await adapter.presenceReap(channel);
|
|
675
|
+
} catch (error) {
|
|
676
|
+
report(error, { where: "presenceHeartbeat" });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
for (const [pattern, rule] of Object.entries(options.channels ?? {})) rules.push(compileRule(pattern, rule));
|
|
680
|
+
const core = {
|
|
681
|
+
limits,
|
|
682
|
+
adapter,
|
|
683
|
+
report,
|
|
684
|
+
match,
|
|
685
|
+
allowed,
|
|
686
|
+
channel(pattern, rule) {
|
|
687
|
+
rules.push(compileRule(pattern, rule));
|
|
688
|
+
return core;
|
|
689
|
+
},
|
|
690
|
+
connect(transport, identity = {}, connectOptions = {}) {
|
|
691
|
+
if (closing) throw new RealtimeError("closed", "the hub is closed");
|
|
692
|
+
let release = () => void 0;
|
|
693
|
+
const done = new Promise((resolve) => {
|
|
694
|
+
release = resolve;
|
|
695
|
+
});
|
|
696
|
+
const owner = connectOptions.actingFor === void 0 ? void 0 : conns.get(connectOptions.actingFor);
|
|
697
|
+
const acting = owner !== void 0 && identity.user !== void 0 && owner.identity.user === identity.user;
|
|
698
|
+
const conn = {
|
|
699
|
+
id: acting ? connectOptions.actingFor : randomId(),
|
|
700
|
+
identity,
|
|
701
|
+
transport,
|
|
702
|
+
subs: /* @__PURE__ */ new Map(),
|
|
703
|
+
queue: [],
|
|
704
|
+
publishBucket: connectOptions.publishBucket ?? new TokenBucket(limits.publishesPerSecond, limits.publishBurst, now()),
|
|
705
|
+
stateless: connectOptions.stateless === true,
|
|
706
|
+
waiters: [],
|
|
707
|
+
invalid: new WindowCounter(10, 6e4),
|
|
708
|
+
limited: new WindowCounter(50, 1e4),
|
|
709
|
+
pending: /* @__PURE__ */ new Set(),
|
|
710
|
+
running: false,
|
|
711
|
+
closed: false,
|
|
712
|
+
release,
|
|
713
|
+
done
|
|
714
|
+
};
|
|
715
|
+
if (!acting) conns.set(conn.id, conn);
|
|
716
|
+
sendFrame(conn, {
|
|
717
|
+
t: "ready",
|
|
718
|
+
v: 1,
|
|
719
|
+
id: conn.id,
|
|
720
|
+
hb: limits.heartbeatMs,
|
|
721
|
+
max: limits.maxMessageBytes
|
|
722
|
+
});
|
|
723
|
+
return {
|
|
724
|
+
id: conn.id,
|
|
725
|
+
identity,
|
|
726
|
+
done,
|
|
727
|
+
receive(text) {
|
|
728
|
+
if (conn.closed) return;
|
|
729
|
+
if (conn.queue.length >= limits.maxQueuedFrames) {
|
|
730
|
+
drop(conn, 1013, "too many frames waiting");
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
conn.queue.push(text);
|
|
734
|
+
run(conn);
|
|
735
|
+
},
|
|
736
|
+
closed() {
|
|
737
|
+
closeConnection(conn);
|
|
738
|
+
},
|
|
739
|
+
settled() {
|
|
740
|
+
return new Promise((resolve) => {
|
|
741
|
+
conn.waiters.push(resolve);
|
|
742
|
+
settle(conn);
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
},
|
|
747
|
+
async publish(channel, event, data, publishOptions = {}) {
|
|
748
|
+
if (!isChannelName(channel)) throw new RealtimeError("bad_channel", `"${channel}" is not a valid channel name`);
|
|
749
|
+
if (!isEventName(event)) throw new RealtimeError("bad_frame", `"${event}" is not a valid event name`);
|
|
750
|
+
if (publishOptions.key !== void 0 && !isIdempotencyKey(publishOptions.key)) throw new RealtimeError("bad_frame", "the idempotency key is not valid");
|
|
751
|
+
let text;
|
|
752
|
+
try {
|
|
753
|
+
text = JSON.stringify(data === void 0 ? null : data);
|
|
754
|
+
} catch (error) {
|
|
755
|
+
throw new TypeError("the data is not JSON", { cause: error });
|
|
756
|
+
}
|
|
757
|
+
if (text === void 0) throw new TypeError("the data is not JSON");
|
|
758
|
+
if (byteLength(text) > limits.maxMessageBytes) throw new RealtimeError("too_large", "the message is too large");
|
|
759
|
+
const found = match(channel);
|
|
760
|
+
const result = await adapter.publish(channel, {
|
|
761
|
+
event,
|
|
762
|
+
data: text
|
|
763
|
+
}, {
|
|
764
|
+
history: found?.rule.history ?? limits.history,
|
|
765
|
+
historyTtlMs: limits.historyTtlMs,
|
|
766
|
+
idempotencyTtlMs: limits.idempotencyTtlMs,
|
|
767
|
+
...publishOptions.key === void 0 ? {} : { key: publishOptions.key }
|
|
768
|
+
});
|
|
769
|
+
counters.published += result.duplicate ? 0 : 1;
|
|
770
|
+
return result;
|
|
771
|
+
},
|
|
772
|
+
async setMemberState(identity, channel, connectionId, state) {
|
|
773
|
+
if (identity.user === void 0) throw new RealtimeError("unauthorized", "presence changes over HTTP need a signed-in user");
|
|
774
|
+
const found = match(channel);
|
|
775
|
+
if (found === void 0 || !await allowed(found.rule, "presence", channel, found.params, identity)) throw new RealtimeError("forbidden");
|
|
776
|
+
if (byteLength(JSON.stringify(state)) > limits.maxStateBytes) throw new RealtimeError("too_large", "the presence state is too large");
|
|
777
|
+
const member = (await adapter.presenceList(channel)).find((item) => item.id === connectionId);
|
|
778
|
+
if (member === void 0 || member.u !== identity.user) throw new RealtimeError("forbidden", "that connection is not yours");
|
|
779
|
+
await adapter.presenceSet(channel, {
|
|
780
|
+
id: member.id,
|
|
781
|
+
u: member.u,
|
|
782
|
+
s: state,
|
|
783
|
+
v: member.v + 1
|
|
784
|
+
}, limits.presenceTtlMs);
|
|
785
|
+
},
|
|
786
|
+
revoke(target) {
|
|
787
|
+
if (target.user === void 0 && target.connection === void 0 && target.channel === void 0) throw new TypeError("revoke needs a user, a connection or a channel.");
|
|
788
|
+
let count = 0;
|
|
789
|
+
for (const conn of [...conns.values()]) {
|
|
790
|
+
if (target.connection !== void 0 && conn.id !== target.connection) continue;
|
|
791
|
+
if (target.user !== void 0 && conn.identity.user !== target.user) continue;
|
|
792
|
+
for (const sub of [...conn.subs.values()]) {
|
|
793
|
+
if (target.channel !== void 0 && sub.channel.name !== target.channel) continue;
|
|
794
|
+
removeSub(sub, "revoked");
|
|
795
|
+
count += 1;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return count;
|
|
799
|
+
},
|
|
800
|
+
stats() {
|
|
801
|
+
let subscriptions = 0;
|
|
802
|
+
for (const conn of conns.values()) subscriptions += conn.subs.size;
|
|
803
|
+
return {
|
|
804
|
+
connections: conns.size,
|
|
805
|
+
channels: chans.size,
|
|
806
|
+
subscriptions,
|
|
807
|
+
...counters
|
|
808
|
+
};
|
|
809
|
+
},
|
|
810
|
+
async close() {
|
|
811
|
+
if (closing) return;
|
|
812
|
+
closing = true;
|
|
813
|
+
if (heartbeat !== void 0) clearInterval(heartbeat);
|
|
814
|
+
heartbeat = void 0;
|
|
815
|
+
const all = [...conns.values()];
|
|
816
|
+
for (const conn of all) drop(conn, 1001, "going away");
|
|
817
|
+
await Promise.all(all.map((conn) => conn.done));
|
|
818
|
+
await Promise.allSettled([...releasing]);
|
|
819
|
+
if (ownsAdapter) await adapter.close();
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
return core;
|
|
823
|
+
}
|
|
824
|
+
export { createHubCore };
|
|
825
|
+
|
|
826
|
+
//# sourceMappingURL=hub.js.map
|