@shadow-js/realtime 1.0.1 → 1.0.3
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 +29 -0
- package/README.md +35 -0
- package/dist/dev/client/core.js +107 -107
- package/dist/dev/client/core.js.map +1 -1
- package/dist/dev/client/sse.js +33 -24
- package/dist/dev/client/sse.js.map +1 -1
- package/dist/dev/client/types.d.ts +6 -0
- package/dist/dev/client/util.js +2 -2
- package/dist/dev/client/util.js.map +1 -1
- package/dist/dev/cloudflare/link.js +69 -8
- package/dist/dev/cloudflare/link.js.map +1 -1
- package/dist/dev/memory.d.ts +11 -0
- package/dist/dev/memory.js +38 -10
- package/dist/dev/memory.js.map +1 -1
- package/dist/dev/protocol.js +13 -12
- package/dist/dev/protocol.js.map +1 -1
- package/dist/dev/redis/topics.d.ts +6 -1
- package/dist/dev/redis/topics.js +4 -1
- package/dist/dev/redis/topics.js.map +1 -1
- package/dist/dev/server/http.js +37 -24
- package/dist/dev/server/http.js.map +1 -1
- package/dist/dev/server/hub.d.ts +1 -1
- package/dist/dev/server/hub.js +117 -69
- package/dist/dev/server/hub.js.map +1 -1
- package/dist/dev/server/index.d.ts +2 -2
- package/dist/dev/server/index.js.map +1 -1
- package/dist/dev/server/limits.d.ts +4 -1
- package/dist/dev/server/limits.js.map +1 -1
- package/dist/dev/server/rules.js.map +1 -1
- package/dist/prod/client/core.js +1 -1
- package/dist/prod/client/core.js.map +1 -1
- package/dist/prod/client/sse.js +1 -4
- package/dist/prod/client/sse.js.map +1 -1
- package/dist/prod/client/util.js +1 -1
- package/dist/prod/client/util.js.map +1 -1
- package/dist/prod/cloudflare/link.js +1 -1
- package/dist/prod/cloudflare/link.js.map +1 -1
- package/dist/prod/memory.js +1 -1
- package/dist/prod/memory.js.map +1 -1
- package/dist/prod/protocol.js +1 -1
- package/dist/prod/protocol.js.map +1 -1
- package/dist/prod/redis/topics.js +1 -1
- package/dist/prod/redis/topics.js.map +1 -1
- package/dist/prod/server/http.js +1 -1
- package/dist/prod/server/http.js.map +1 -1
- package/dist/prod/server/hub.js +1 -1
- package/dist/prod/server/hub.js.map +1 -1
- package/dist/prod/server/index.js.map +1 -1
- package/dist/prod/server/limits.js +1 -1
- package/dist/prod/server/limits.js.map +1 -1
- package/dist/prod/server/rules.js +1 -1
- package/dist/prod/server/rules.js.map +1 -1
- package/package.json +6 -5
package/dist/dev/server/hub.js
CHANGED
|
@@ -9,11 +9,8 @@ const MAX_PUMP_BUFFER = 1e4;
|
|
|
9
9
|
const ID_BYTES = 16;
|
|
10
10
|
/** A random id: 128 bits, base64url. */
|
|
11
11
|
function randomId() {
|
|
12
|
-
const bytes = new Uint8Array(ID_BYTES);
|
|
13
|
-
|
|
14
|
-
let text = "";
|
|
15
|
-
for (const byte of bytes) text += String.fromCharCode(byte);
|
|
16
|
-
return btoa(text).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
12
|
+
const bytes = crypto.getRandomValues(new Uint8Array(ID_BYTES));
|
|
13
|
+
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
17
14
|
}
|
|
18
15
|
function encodeFrame(frame) {
|
|
19
16
|
return JSON.stringify(frame);
|
|
@@ -43,10 +40,20 @@ function createHubCore(options = {}) {
|
|
|
43
40
|
const limits = resolveLimits(options.limits);
|
|
44
41
|
const now = options.now ?? Date.now;
|
|
45
42
|
const ownsAdapter = options.adapter === void 0;
|
|
46
|
-
const adapter = options.adapter ?? memoryAdapter({
|
|
43
|
+
const adapter = options.adapter ?? memoryAdapter({
|
|
44
|
+
now,
|
|
45
|
+
onError: (error, context) => report(error, {
|
|
46
|
+
where: `memory.${context.where}`,
|
|
47
|
+
channel: context.channel
|
|
48
|
+
})
|
|
49
|
+
});
|
|
47
50
|
const rules = [];
|
|
48
51
|
const conns = /* @__PURE__ */ new Map();
|
|
49
52
|
const chans = /* @__PURE__ */ new Map();
|
|
53
|
+
/** Subscribes whose rules are being awaited: a revoke issued meanwhile marks them, so the grant cannot outlive it. */
|
|
54
|
+
const judging = /* @__PURE__ */ new Set();
|
|
55
|
+
/** The tail of each channel's presence joins that are queued or running; an entry leaves when its chain is idle. */
|
|
56
|
+
const joinChains = /* @__PURE__ */ new Map();
|
|
50
57
|
let heartbeat;
|
|
51
58
|
let closing = false;
|
|
52
59
|
let beating = false;
|
|
@@ -104,9 +111,21 @@ function createHubCore(options = {}) {
|
|
|
104
111
|
where: "transport.send",
|
|
105
112
|
connection: conn.id
|
|
106
113
|
});
|
|
114
|
+
onTransport(conn, "transport.close", () => conn.transport.close(1011, "write failed"));
|
|
107
115
|
}
|
|
108
116
|
closeConnection(conn);
|
|
109
117
|
}
|
|
118
|
+
/** Runs a call on the transport; a transport that is already gone throws, which is only reported. */
|
|
119
|
+
function onTransport(conn, where, call) {
|
|
120
|
+
try {
|
|
121
|
+
call();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
report(error, {
|
|
124
|
+
where,
|
|
125
|
+
connection: conn.id
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
110
129
|
function sendFrame(conn, frame) {
|
|
111
130
|
send(conn, encodeFrame(frame));
|
|
112
131
|
}
|
|
@@ -117,22 +136,18 @@ function createHubCore(options = {}) {
|
|
|
117
136
|
...n === void 0 ? {} : { n },
|
|
118
137
|
...channel === void 0 ? {} : { c: channel },
|
|
119
138
|
code,
|
|
120
|
-
...message === void 0 ? {} : { msg: message }
|
|
139
|
+
...message === void 0 || message === "" ? {} : { msg: message }
|
|
121
140
|
});
|
|
122
141
|
}
|
|
123
142
|
/** Sends a final frame, closes the transport and releases the connection. */
|
|
124
143
|
function drop(conn, code, reason) {
|
|
125
144
|
if (conn.closed) return;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
} catch {}
|
|
133
|
-
try {
|
|
134
|
-
conn.transport.close(code, reason);
|
|
135
|
-
} catch {}
|
|
145
|
+
onTransport(conn, "transport.send", () => conn.transport.send(encodeFrame({
|
|
146
|
+
t: "bye",
|
|
147
|
+
code,
|
|
148
|
+
why: reason
|
|
149
|
+
})));
|
|
150
|
+
onTransport(conn, "transport.close", () => conn.transport.close(code, reason));
|
|
136
151
|
closeConnection(conn);
|
|
137
152
|
}
|
|
138
153
|
function channelState(name) {
|
|
@@ -151,23 +166,16 @@ function createHubCore(options = {}) {
|
|
|
151
166
|
}
|
|
152
167
|
return chan;
|
|
153
168
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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;
|
|
169
|
+
/** Opens one of the channel's two adapter feeds once; a failed attempt is forgotten so the next caller retries. */
|
|
170
|
+
async function ensureFeed(chan, key, open) {
|
|
171
|
+
if (chan[key] === void 0) {
|
|
172
|
+
const opened = open();
|
|
173
|
+
chan[key] = opened;
|
|
174
|
+
opened.catch(() => {
|
|
175
|
+
chan[key] = void 0;
|
|
168
176
|
});
|
|
169
177
|
}
|
|
170
|
-
await chan
|
|
178
|
+
await chan[key];
|
|
171
179
|
}
|
|
172
180
|
function onMessage(chan, message) {
|
|
173
181
|
for (const sub of chan.subs) {
|
|
@@ -250,6 +258,10 @@ function createHubCore(options = {}) {
|
|
|
250
258
|
}
|
|
251
259
|
function onPresence(chan, event) {
|
|
252
260
|
for (const sub of chan.subs) {
|
|
261
|
+
if (sub.joined && event.op !== "leave" && event.member.id === sub.conn.id && event.member.v > sub.version) {
|
|
262
|
+
sub.version = event.member.v;
|
|
263
|
+
sub.state = event.member.s;
|
|
264
|
+
}
|
|
253
265
|
if (!sub.active || !sub.joined && !sub.watch) continue;
|
|
254
266
|
if (sub.presenceQueue !== void 0) sub.presenceQueue.push(event);
|
|
255
267
|
else sendPresence(sub, event);
|
|
@@ -322,9 +334,19 @@ function createHubCore(options = {}) {
|
|
|
322
334
|
if (conn.subs.has(name)) return fail(conn, n, "conflict", name, "already subscribed");
|
|
323
335
|
if (conn.subs.size >= limits.maxChannelsPerConnection) return fail(conn, n, "limit", name, "too many channels on this connection");
|
|
324
336
|
const found = match(name);
|
|
325
|
-
if (found === void 0
|
|
337
|
+
if (found === void 0) return fail(conn, n, identityDenied(conn), name);
|
|
326
338
|
const wantsPresence = frame.hasPresence || frame.watch === true;
|
|
327
|
-
|
|
339
|
+
const pending = {
|
|
340
|
+
conn,
|
|
341
|
+
channel: name,
|
|
342
|
+
revoked: false
|
|
343
|
+
};
|
|
344
|
+
judging.add(pending);
|
|
345
|
+
try {
|
|
346
|
+
if (!await allowed(found.rule, "subscribe", name, found.params, conn.identity) || wantsPresence && !await allowed(found.rule, "presence", name, found.params, conn.identity) || pending.revoked) return fail(conn, n, identityDenied(conn), name);
|
|
347
|
+
} finally {
|
|
348
|
+
judging.delete(pending);
|
|
349
|
+
}
|
|
328
350
|
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
351
|
if (conn.closed) return;
|
|
330
352
|
const chan = channelState(name);
|
|
@@ -338,6 +360,7 @@ function createHubCore(options = {}) {
|
|
|
338
360
|
watch: frame.watch === true,
|
|
339
361
|
joined: false,
|
|
340
362
|
version: 0,
|
|
363
|
+
state: null,
|
|
341
364
|
lastSeq: 0,
|
|
342
365
|
lastAt: now(),
|
|
343
366
|
pumping: true,
|
|
@@ -350,9 +373,9 @@ function createHubCore(options = {}) {
|
|
|
350
373
|
conn.subs.set(name, sub);
|
|
351
374
|
chan.subs.add(sub);
|
|
352
375
|
try {
|
|
353
|
-
await
|
|
376
|
+
await ensureFeed(chan, "adapterSub", () => adapter.subscribe(chan.name, (message) => onMessage(chan, message)));
|
|
354
377
|
if (wantsPresence) {
|
|
355
|
-
await
|
|
378
|
+
await ensureFeed(chan, "presenceSub", () => adapter.presenceSubscribe(chan.name, (event) => onPresence(chan, event)));
|
|
356
379
|
startHeartbeat();
|
|
357
380
|
}
|
|
358
381
|
const probe = await adapter.history(name, Number.MAX_SAFE_INTEGER, 0);
|
|
@@ -398,18 +421,35 @@ function createHubCore(options = {}) {
|
|
|
398
421
|
}
|
|
399
422
|
}
|
|
400
423
|
}
|
|
401
|
-
|
|
424
|
+
/** Joins one at a time per channel: the capacity check reads the set, so two joins in flight would both see room. */
|
|
425
|
+
function joinPresence(sub, state) {
|
|
426
|
+
const name = sub.channel.name;
|
|
427
|
+
const run = (joinChains.get(name) ?? Promise.resolve()).then(() => joinOne(sub, state));
|
|
428
|
+
const settle = () => {
|
|
429
|
+
if (joinChains.get(name) === tail) joinChains.delete(name);
|
|
430
|
+
};
|
|
431
|
+
const tail = run.then(settle, settle);
|
|
432
|
+
joinChains.set(name, tail);
|
|
433
|
+
return run;
|
|
434
|
+
}
|
|
435
|
+
async function joinOne(sub, state) {
|
|
436
|
+
if (!sub.active) return;
|
|
402
437
|
if ((await adapter.presenceList(sub.channel.name)).length >= limits.maxPresenceMembers) throw new RealtimeError("limit", "the presence set is full");
|
|
403
438
|
sub.version = 1;
|
|
439
|
+
sub.state = state;
|
|
404
440
|
sub.joined = true;
|
|
405
441
|
startHeartbeat();
|
|
406
|
-
|
|
442
|
+
await adapter.presenceSet(sub.channel.name, memberOf(sub), limits.presenceTtlMs);
|
|
443
|
+
}
|
|
444
|
+
/** The presence member a joined subscription stands for. */
|
|
445
|
+
function memberOf(sub) {
|
|
446
|
+
const { user } = sub.conn.identity;
|
|
447
|
+
return {
|
|
407
448
|
id: sub.conn.id,
|
|
408
|
-
...
|
|
409
|
-
s: state,
|
|
410
|
-
v:
|
|
449
|
+
...user === void 0 ? {} : { u: user },
|
|
450
|
+
s: sub.state,
|
|
451
|
+
v: sub.version
|
|
411
452
|
};
|
|
412
|
-
await adapter.presenceSet(sub.channel.name, member, limits.presenceTtlMs);
|
|
413
453
|
}
|
|
414
454
|
async function syncPresence(sub) {
|
|
415
455
|
const members = await adapter.presenceList(sub.channel.name);
|
|
@@ -435,14 +475,9 @@ function createHubCore(options = {}) {
|
|
|
435
475
|
if (byteLength(JSON.stringify(frame.s)) > limits.maxStateBytes) return fail(conn, void 0, "too_large", frame.c, "the presence state is too large");
|
|
436
476
|
if (!conn.publishBucket.take(now())) return limit(conn, void 0, frame.c);
|
|
437
477
|
sub.version += 1;
|
|
438
|
-
|
|
439
|
-
id: conn.id,
|
|
440
|
-
...conn.identity.user === void 0 ? {} : { u: conn.identity.user },
|
|
441
|
-
s: frame.s,
|
|
442
|
-
v: sub.version
|
|
443
|
-
};
|
|
478
|
+
sub.state = frame.s;
|
|
444
479
|
try {
|
|
445
|
-
await adapter.presenceSet(frame.c,
|
|
480
|
+
await adapter.presenceSet(frame.c, memberOf(sub), limits.presenceTtlMs);
|
|
446
481
|
} catch (error) {
|
|
447
482
|
report(error, {
|
|
448
483
|
where: "presenceSet",
|
|
@@ -472,7 +507,7 @@ function createHubCore(options = {}) {
|
|
|
472
507
|
history: found.rule.history ?? limits.history,
|
|
473
508
|
historyTtlMs: limits.historyTtlMs,
|
|
474
509
|
idempotencyTtlMs: limits.idempotencyTtlMs,
|
|
475
|
-
...frame.k === void 0 ? {} : { key: frame.k }
|
|
510
|
+
...frame.k === void 0 ? {} : { key: `${frame.k}/${conn.identity.user === void 0 ? "a" : `u:${conn.identity.user}`}` }
|
|
476
511
|
});
|
|
477
512
|
counters.published += result.duplicate ? 0 : 1;
|
|
478
513
|
if (n !== void 0) sendFrame(conn, {
|
|
@@ -541,14 +576,20 @@ function createHubCore(options = {}) {
|
|
|
541
576
|
}
|
|
542
577
|
})();
|
|
543
578
|
}
|
|
579
|
+
/** Answers a frame that cannot be handled; a peer that keeps sending them is dropped. */
|
|
580
|
+
function refuseFrame(conn, n, code, why) {
|
|
581
|
+
fail(conn, n, code, void 0, why);
|
|
582
|
+
if (conn.invalid.hit(now())) drop(conn, 1008, "too many invalid frames");
|
|
583
|
+
}
|
|
544
584
|
async function handle(conn, text) {
|
|
545
585
|
const decoded = decodeClientFrame(text, limits.maxMessageBytes);
|
|
546
586
|
if (!decoded.ok) {
|
|
547
|
-
|
|
548
|
-
if (conn.invalid.hit(now())) drop(conn, 1008, "too many invalid frames");
|
|
587
|
+
refuseFrame(conn, decoded.n, decoded.code, decoded.why);
|
|
549
588
|
return;
|
|
550
589
|
}
|
|
551
590
|
const frame = decoded.frame;
|
|
591
|
+
const n = "n" in frame ? frame.n : void 0;
|
|
592
|
+
if ((frame.t === "sub" || frame.t === "unsub") && !conn.controlBucket.take(now()) && frame.t === "sub") return limit(conn, n, frame.c);
|
|
552
593
|
try {
|
|
553
594
|
switch (frame.t) {
|
|
554
595
|
case "hello": return;
|
|
@@ -570,7 +611,6 @@ function createHubCore(options = {}) {
|
|
|
570
611
|
case "pres": return await setPresence(conn, frame);
|
|
571
612
|
}
|
|
572
613
|
} catch (error) {
|
|
573
|
-
const n = "n" in frame ? frame.n : void 0;
|
|
574
614
|
const channel = "c" in frame ? frame.c : void 0;
|
|
575
615
|
if (error instanceof RealtimeError) fail(conn, n, error.code, channel);
|
|
576
616
|
else {
|
|
@@ -664,12 +704,7 @@ function createHubCore(options = {}) {
|
|
|
664
704
|
for (const ref of missing) {
|
|
665
705
|
const sub = conns.get(ref.id)?.subs.get(ref.channel);
|
|
666
706
|
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);
|
|
707
|
+
await adapter.presenceSet(ref.channel, memberOf(sub), limits.presenceTtlMs);
|
|
673
708
|
}
|
|
674
709
|
for (const channel of watched) await adapter.presenceReap(channel);
|
|
675
710
|
} catch (error) {
|
|
@@ -702,6 +737,7 @@ function createHubCore(options = {}) {
|
|
|
702
737
|
subs: /* @__PURE__ */ new Map(),
|
|
703
738
|
queue: [],
|
|
704
739
|
publishBucket: connectOptions.publishBucket ?? new TokenBucket(limits.publishesPerSecond, limits.publishBurst, now()),
|
|
740
|
+
controlBucket: new TokenBucket(limits.publishesPerSecond, Math.max(limits.publishBurst, limits.maxChannelsPerConnection), now()),
|
|
705
741
|
stateless: connectOptions.stateless === true,
|
|
706
742
|
waiters: [],
|
|
707
743
|
invalid: new WindowCounter(10, 6e4),
|
|
@@ -726,6 +762,10 @@ function createHubCore(options = {}) {
|
|
|
726
762
|
done,
|
|
727
763
|
receive(text) {
|
|
728
764
|
if (conn.closed) return;
|
|
765
|
+
if (byteLength(text) > limits.maxMessageBytes) {
|
|
766
|
+
refuseFrame(conn, void 0, "too_large");
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
729
769
|
if (conn.queue.length >= limits.maxQueuedFrames) {
|
|
730
770
|
drop(conn, 1013, "too many frames waiting");
|
|
731
771
|
return;
|
|
@@ -764,7 +804,7 @@ function createHubCore(options = {}) {
|
|
|
764
804
|
history: found?.rule.history ?? limits.history,
|
|
765
805
|
historyTtlMs: limits.historyTtlMs,
|
|
766
806
|
idempotencyTtlMs: limits.idempotencyTtlMs,
|
|
767
|
-
...publishOptions.key === void 0 ? {} : { key: publishOptions.key }
|
|
807
|
+
...publishOptions.key === void 0 ? {} : { key: `${publishOptions.key}/s` }
|
|
768
808
|
});
|
|
769
809
|
counters.published += result.duplicate ? 0 : 1;
|
|
770
810
|
return result;
|
|
@@ -776,24 +816,32 @@ function createHubCore(options = {}) {
|
|
|
776
816
|
if (byteLength(JSON.stringify(state)) > limits.maxStateBytes) throw new RealtimeError("too_large", "the presence state is too large");
|
|
777
817
|
const member = (await adapter.presenceList(channel)).find((item) => item.id === connectionId);
|
|
778
818
|
if (member === void 0 || member.u !== identity.user) throw new RealtimeError("forbidden", "that connection is not yours");
|
|
819
|
+
const local = conns.get(connectionId)?.subs.get(channel);
|
|
820
|
+
const version = Math.max(member.v, local?.version ?? 0) + 1;
|
|
821
|
+
if (local?.joined === true) {
|
|
822
|
+
local.version = version;
|
|
823
|
+
local.state = state;
|
|
824
|
+
}
|
|
779
825
|
await adapter.presenceSet(channel, {
|
|
780
826
|
id: member.id,
|
|
781
827
|
u: member.u,
|
|
782
828
|
s: state,
|
|
783
|
-
v:
|
|
829
|
+
v: version
|
|
784
830
|
}, limits.presenceTtlMs);
|
|
785
831
|
},
|
|
786
832
|
revoke(target) {
|
|
787
833
|
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.");
|
|
834
|
+
const hit = (conn, channel) => (target.connection === void 0 || conn.id === target.connection) && (target.user === void 0 || conn.identity.user === target.user) && (target.channel === void 0 || channel === target.channel);
|
|
788
835
|
let count = 0;
|
|
789
|
-
for (const
|
|
790
|
-
if (
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
836
|
+
for (const item of judging) {
|
|
837
|
+
if (item.revoked || !hit(item.conn, item.channel)) continue;
|
|
838
|
+
item.revoked = true;
|
|
839
|
+
count += 1;
|
|
840
|
+
}
|
|
841
|
+
for (const conn of [...conns.values()]) for (const sub of [...conn.subs.values()]) {
|
|
842
|
+
if (!hit(conn, sub.channel.name)) continue;
|
|
843
|
+
removeSub(sub, "revoked");
|
|
844
|
+
count += 1;
|
|
797
845
|
}
|
|
798
846
|
return count;
|
|
799
847
|
},
|