@shadow-js/realtime 1.0.2 → 1.0.4

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.
@@ -50,6 +50,10 @@ function createHubCore(options = {}) {
50
50
  const rules = [];
51
51
  const conns = /* @__PURE__ */ new Map();
52
52
  const chans = /* @__PURE__ */ new Map();
53
+ /** Subscribes whose rules are being awaited: a revoke issued meanwhile takes them out, 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();
53
57
  let heartbeat;
54
58
  let closing = false;
55
59
  let beating = false;
@@ -90,7 +94,7 @@ function createHubCore(options = {}) {
90
94
  where: `rule.${op}`,
91
95
  channel
92
96
  });
93
- throw new RealtimeError("unavailable", "the channel rule could not be evaluated");
97
+ throw new RealtimeError("unavailable", ...["the channel rule could not be evaluated"]);
94
98
  }
95
99
  }
96
100
  function send(conn, text) {
@@ -152,11 +156,7 @@ function createHubCore(options = {}) {
152
156
  chan = {
153
157
  name,
154
158
  subs: /* @__PURE__ */ new Set(),
155
- bucket: new TokenBucket(limits.channelPublishesPerSecond, limits.channelPublishesPerSecond, now()),
156
- adapterSub: void 0,
157
- presenceSub: void 0,
158
- closing: false,
159
- frame: void 0
159
+ bucket: new TokenBucket(limits.channelPublishesPerSecond, limits.channelPublishesPerSecond, now())
160
160
  };
161
161
  chans.set(name, chan);
162
162
  }
@@ -181,32 +181,31 @@ function createHubCore(options = {}) {
181
181
  else sub.buffer.push(message);
182
182
  continue;
183
183
  }
184
- if (message.seq === sub.lastSeq + 1) deliver(sub, message, false);
185
- else if (message.seq > sub.lastSeq + 1) {
184
+ if (message.seq === sub.seq + 1) deliver(sub, message, false);
185
+ else if (message.seq > sub.seq + 1) {
186
186
  sub.buffer.push(message);
187
187
  pump(sub, void 0);
188
- } else if (message.at > sub.lastAt) removeSub(sub, "reset");
188
+ } else if (message.at > sub.at) removeSub(sub, "reset");
189
189
  }
190
190
  }
191
191
  function deliver(sub, message, replayed) {
192
- sub.lastSeq = message.seq;
193
- sub.lastAt = Math.max(sub.lastAt, message.at);
192
+ sub.seq = message.seq;
193
+ sub.at = Math.max(sub.at, message.at);
194
194
  if (!sub.echo && message.publisher === sub.conn.id) return;
195
195
  counters.delivered += 1;
196
196
  if (replayed) {
197
197
  send(sub.conn, messageFrame(sub.channel.name, message, true));
198
198
  return;
199
199
  }
200
- const cached = sub.channel.frame;
201
- const text = cached !== void 0 && cached.seq === message.seq ? cached.text : messageFrame(sub.channel.name, message, false);
202
- if (cached === void 0 || cached.seq !== message.seq) sub.channel.frame = {
200
+ let cached = sub.channel.frame;
201
+ if (cached === void 0 || cached.seq !== message.seq) cached = sub.channel.frame = {
203
202
  seq: message.seq,
204
- text
203
+ text: messageFrame(sub.channel.name, message, false)
205
204
  };
206
- send(sub.conn, text);
205
+ send(sub.conn, cached.text);
207
206
  }
208
207
  /**
209
- * Delivers, in order and without a gap, the messages after `sub.lastSeq` up to `upTo` and whatever is buffered. Fetches the
208
+ * Delivers, in order and without a gap, the messages after `sub.seq` up to `upTo` and whatever is buffered. Fetches the
210
209
  * missing ones from history. When history no longer has them: a subscription that reported a gap skips ahead; any other one
211
210
  * is removed with `unsubbed why:"gap"`.
212
211
  */
@@ -215,24 +214,24 @@ function createHubCore(options = {}) {
215
214
  sub.pumping = true;
216
215
  try {
217
216
  while (sub.active) {
218
- sub.buffer = sub.buffer.filter((message) => message.seq > sub.lastSeq);
217
+ sub.buffer = sub.buffer.filter((message) => message.seq > sub.seq);
219
218
  const next = sub.buffer[0];
220
- if (next !== void 0 && next.seq === sub.lastSeq + 1) {
219
+ if (next !== void 0 && next.seq === sub.seq + 1) {
221
220
  deliver(sub, next, false);
222
221
  sub.buffer.shift();
223
222
  continue;
224
223
  }
225
224
  const target = next === void 0 ? upTo ?? 0 : next.seq - 1;
226
- if (sub.lastSeq >= target) break;
227
- const page = await adapter.history(sub.channel.name, sub.lastSeq, Math.min(REPLAY_PAGE, target - sub.lastSeq));
225
+ if (sub.seq >= target) break;
226
+ const page = await adapter.history(sub.channel.name, sub.seq, Math.min(REPLAY_PAGE, target - sub.seq));
228
227
  if (!sub.active) return;
229
228
  const first = page.messages[0];
230
- if (first === void 0 || first.seq !== sub.lastSeq + 1) {
229
+ if (first === void 0 || first.seq !== sub.seq + 1) {
231
230
  if (!sub.gapOk) {
232
231
  removeSub(sub, "gap");
233
232
  return;
234
233
  }
235
- sub.lastSeq = (first?.seq ?? target + 1) - 1;
234
+ sub.seq = (first?.seq ?? target + 1) - 1;
236
235
  continue;
237
236
  }
238
237
  for (const message of page.messages) {
@@ -259,7 +258,7 @@ function createHubCore(options = {}) {
259
258
  sub.state = event.member.s;
260
259
  }
261
260
  if (!sub.active || !sub.joined && !sub.watch) continue;
262
- if (sub.presenceQueue !== void 0) sub.presenceQueue.push(event);
261
+ if (sub.queued !== void 0) sub.queued.push(event);
263
262
  else sendPresence(sub, event);
264
263
  }
265
264
  }
@@ -310,7 +309,7 @@ function createHubCore(options = {}) {
310
309
  if (chan.subs.size > 0 || chan.closing) return;
311
310
  chan.closing = true;
312
311
  if (chans.get(chan.name) === chan) chans.delete(chan.name);
313
- const stops = [chan.adapterSub, chan.presenceSub].filter((item) => item !== void 0);
312
+ const stops = [chan.feed, chan.pfeed].filter((item) => item !== void 0);
314
313
  await Promise.all(stops.map(async (stop) => {
315
314
  try {
316
315
  await (await stop)();
@@ -327,41 +326,46 @@ function createHubCore(options = {}) {
327
326
  }
328
327
  async function subscribe(conn, frame) {
329
328
  const { c: name, n } = frame;
330
- if (conn.subs.has(name)) return fail(conn, n, "conflict", name, "already subscribed");
331
- if (conn.subs.size >= limits.maxChannelsPerConnection) return fail(conn, n, "limit", name, "too many channels on this connection");
329
+ if (conn.subs.has(name)) return fail(conn, n, "conflict", name, ...["already subscribed"]);
330
+ if (conn.subs.size >= limits.maxChannelsPerConnection) return fail(conn, n, "limit", name, ...["too many channels on this connection"]);
332
331
  const found = match(name);
333
- if (found === void 0 || !await allowed(found.rule, "subscribe", name, found.params, conn.identity)) return fail(conn, n, identityDenied(conn), name);
332
+ if (found === void 0) return fail(conn, n, identityDenied(conn), name);
334
333
  const wantsPresence = frame.hasPresence || frame.watch === true;
335
- if (wantsPresence && !await allowed(found.rule, "presence", name, found.params, conn.identity)) return fail(conn, n, identityDenied(conn), name);
336
- if (frame.hasPresence && byteLength(JSON.stringify(frame.presence ?? null)) > limits.maxStateBytes) return fail(conn, n, "too_large", name, "the presence state is too large");
334
+ const pending = {
335
+ conn,
336
+ channel: name
337
+ };
338
+ judging.add(pending);
339
+ try {
340
+ if (!await allowed(found.rule, "subscribe", name, found.params, conn.identity) || wantsPresence && !await allowed(found.rule, "presence", name, found.params, conn.identity) || !judging.has(pending)) return fail(conn, n, identityDenied(conn), name);
341
+ } finally {
342
+ judging.delete(pending);
343
+ }
344
+ if (frame.hasPresence && byteLength(JSON.stringify(frame.presence ?? null)) > limits.maxStateBytes) return fail(conn, n, "too_large", name, ...["the presence state is too large"]);
337
345
  if (conn.closed) return;
338
346
  const chan = channelState(name);
339
- if (chan.subs.size >= limits.maxSubscribersPerChannel) return fail(conn, n, "limit", name, "the channel is full");
347
+ if (chan.subs.size >= limits.maxSubscribersPerChannel) return fail(conn, n, "limit", name, ...["the channel is full"]);
340
348
  const sub = {
341
349
  conn,
342
350
  channel: chan,
343
- rule: found.rule,
344
- params: found.params,
351
+ ...found,
345
352
  echo: frame.echo !== false,
346
353
  watch: frame.watch === true,
347
- joined: false,
348
354
  version: 0,
349
355
  state: null,
350
- lastSeq: 0,
351
- lastAt: now(),
356
+ seq: 0,
357
+ at: now(),
352
358
  pumping: true,
353
- gapOk: false,
354
359
  buffer: [],
355
- presenceQueue: wantsPresence ? [] : void 0,
356
- joining: void 0,
360
+ queued: wantsPresence ? [] : void 0,
357
361
  active: true
358
362
  };
359
363
  conn.subs.set(name, sub);
360
364
  chan.subs.add(sub);
361
365
  try {
362
- await ensureFeed(chan, "adapterSub", () => adapter.subscribe(chan.name, (message) => onMessage(chan, message)));
366
+ await ensureFeed(chan, "feed", () => adapter.subscribe(chan.name, (message) => onMessage(chan, message)));
363
367
  if (wantsPresence) {
364
- await ensureFeed(chan, "presenceSub", () => adapter.presenceSubscribe(chan.name, (event) => onPresence(chan, event)));
368
+ await ensureFeed(chan, "pfeed", () => adapter.presenceSubscribe(chan.name, (event) => onPresence(chan, event)));
365
369
  startHeartbeat();
366
370
  }
367
371
  const probe = await adapter.history(name, Number.MAX_SAFE_INTEGER, 0);
@@ -370,12 +374,12 @@ function createHubCore(options = {}) {
370
374
  const after = frame.after;
371
375
  let gap = false;
372
376
  let reset = false;
373
- if (after === void 0) sub.lastSeq = head;
377
+ if (after === void 0) sub.seq = head;
374
378
  else if (after > head) {
375
379
  reset = true;
376
- sub.lastSeq = head;
380
+ sub.seq = head;
377
381
  } else {
378
- sub.lastSeq = after;
382
+ sub.seq = after;
379
383
  gap = probe.oldest === 0 ? head > after : probe.oldest > after + 1;
380
384
  sub.gapOk = gap;
381
385
  }
@@ -407,19 +411,34 @@ function createHubCore(options = {}) {
407
411
  }
408
412
  }
409
413
  }
410
- async function joinPresence(sub, state) {
411
- if ((await adapter.presenceList(sub.channel.name)).length >= limits.maxPresenceMembers) throw new RealtimeError("limit", "the presence set is full");
412
- sub.version = 1;
413
- sub.state = state;
414
- sub.joined = true;
415
- startHeartbeat();
416
- const member = {
414
+ /** Joins one at a time per channel: the capacity check reads the set, so two joins in flight would both see room. */
415
+ function joinPresence(sub, state) {
416
+ const name = sub.channel.name;
417
+ const run = (joinChains.get(name) ?? Promise.resolve()).then(async () => {
418
+ if (!sub.active) return;
419
+ if ((await adapter.presenceList(name)).length >= limits.maxPresenceMembers) throw new RealtimeError("limit", ...["the presence set is full"]);
420
+ sub.version = 1;
421
+ sub.state = state;
422
+ sub.joined = true;
423
+ startHeartbeat();
424
+ await adapter.presenceSet(name, memberOf(sub), limits.presenceTtlMs);
425
+ });
426
+ const settle = () => {
427
+ if (joinChains.get(name) === tail) joinChains.delete(name);
428
+ };
429
+ const tail = run.then(settle, settle);
430
+ joinChains.set(name, tail);
431
+ return run;
432
+ }
433
+ /** The presence member a joined subscription stands for. */
434
+ function memberOf(sub) {
435
+ const { user } = sub.conn.identity;
436
+ return {
417
437
  id: sub.conn.id,
418
- ...sub.conn.identity.user === void 0 ? {} : { u: sub.conn.identity.user },
419
- s: state,
420
- v: 1
438
+ ...user === void 0 ? {} : { u: user },
439
+ s: sub.state,
440
+ v: sub.version
421
441
  };
422
- await adapter.presenceSet(sub.channel.name, member, limits.presenceTtlMs);
423
442
  }
424
443
  async function syncPresence(sub) {
425
444
  const members = await adapter.presenceList(sub.channel.name);
@@ -430,8 +449,8 @@ function createHubCore(options = {}) {
430
449
  op: "sync",
431
450
  m: members
432
451
  });
433
- const queued = sub.presenceQueue ?? [];
434
- sub.presenceQueue = void 0;
452
+ const queued = sub.queued ?? [];
453
+ sub.queued = void 0;
435
454
  const known = new Map(members.map((member) => [member.id, member.v]));
436
455
  for (const event of queued) {
437
456
  const version = known.get(event.member.id);
@@ -442,18 +461,12 @@ function createHubCore(options = {}) {
442
461
  async function setPresence(conn, frame) {
443
462
  const sub = conn.subs.get(frame.c);
444
463
  if (sub === void 0 || !sub.joined) return fail(conn, void 0, "not_subscribed", frame.c);
445
- if (byteLength(JSON.stringify(frame.s)) > limits.maxStateBytes) return fail(conn, void 0, "too_large", frame.c, "the presence state is too large");
464
+ if (byteLength(JSON.stringify(frame.s)) > limits.maxStateBytes) return fail(conn, void 0, "too_large", frame.c, ...["the presence state is too large"]);
446
465
  if (!conn.publishBucket.take(now())) return limit(conn, void 0, frame.c);
447
466
  sub.version += 1;
448
467
  sub.state = frame.s;
449
- const member = {
450
- id: conn.id,
451
- ...conn.identity.user === void 0 ? {} : { u: conn.identity.user },
452
- s: frame.s,
453
- v: sub.version
454
- };
455
468
  try {
456
- await adapter.presenceSet(frame.c, member, limits.presenceTtlMs);
469
+ await adapter.presenceSet(frame.c, memberOf(sub), limits.presenceTtlMs);
457
470
  } catch (error) {
458
471
  report(error, {
459
472
  where: "presenceSet",
@@ -471,7 +484,7 @@ function createHubCore(options = {}) {
471
484
  const { c: name, n } = frame;
472
485
  const found = match(name);
473
486
  if (found === void 0 || !await allowed(found.rule, "publish", name, found.params, conn.identity)) return fail(conn, n, identityDenied(conn), name);
474
- if (frame.d.length > limits.maxMessageBytes) return fail(conn, n, "too_large", name, "the message is too large");
487
+ if (frame.d.length > limits.maxMessageBytes) return fail(conn, n, "too_large", name, ...["the message is too large"]);
475
488
  if (!conn.publishBucket.take(now()) || chans.get(name)?.bucket.take(now()) === false) return limit(conn, n, name);
476
489
  try {
477
490
  const result = await adapter.publish(name, {
@@ -483,7 +496,7 @@ function createHubCore(options = {}) {
483
496
  history: found.rule.history ?? limits.history,
484
497
  historyTtlMs: limits.historyTtlMs,
485
498
  idempotencyTtlMs: limits.idempotencyTtlMs,
486
- ...frame.k === void 0 ? {} : { key: frame.k }
499
+ ...frame.k === void 0 ? {} : { key: `${frame.k}/${conn.identity.user === void 0 ? "a" : `u:${conn.identity.user}`}` }
487
500
  });
488
501
  counters.published += result.duplicate ? 0 : 1;
489
502
  if (n !== void 0) sendFrame(conn, {
@@ -512,7 +525,7 @@ function createHubCore(options = {}) {
512
525
  const sub = target;
513
526
  const handler = sub.rule.handlers?.[frame.e];
514
527
  if (handler === void 0) return fail(conn, n, "no_handler", name);
515
- if (conn.pending.size >= limits.maxPendingRequests) return fail(conn, n, "limit", name, "too many requests in flight");
528
+ if (conn.pending.size >= limits.maxPendingRequests) return fail(conn, n, "limit", name, ...["too many requests in flight"]);
516
529
  if (!conn.publishBucket.take(now())) return limit(conn, n, name);
517
530
  const controller = new AbortController();
518
531
  conn.pending.add(controller);
@@ -533,7 +546,7 @@ function createHubCore(options = {}) {
533
546
  });
534
547
  const reply = await Promise.race([run, aborted]) ?? null;
535
548
  const text = JSON.stringify(reply);
536
- if (text === void 0 || byteLength(text) > limits.maxMessageBytes) return fail(conn, n, "too_large", name, "the reply is too large");
549
+ if (text === void 0 || byteLength(text) > limits.maxMessageBytes) return fail(conn, n, "too_large", name, ...["the reply is too large"]);
537
550
  send(conn, `{"t":"rep","n":${n},"d":${text}}`);
538
551
  } catch (error) {
539
552
  if (error instanceof RealtimeError) fail(conn, n, error.code, name, error.code === "timeout" || error.code === "closed" ? void 0 : error.message);
@@ -565,7 +578,7 @@ function createHubCore(options = {}) {
565
578
  }
566
579
  const frame = decoded.frame;
567
580
  const n = "n" in frame ? frame.n : void 0;
568
- if ((frame.t === "sub" || frame.t === "unsub") && !conn.controlBucket.take(now()) && frame.t === "sub") return limit(conn, n, frame.c);
581
+ if ((frame.t === "sub" || frame.t === "unsub") && !conn.ctl.take(now()) && frame.t === "sub") return limit(conn, n, frame.c);
569
582
  try {
570
583
  switch (frame.t) {
571
584
  case "hello": return;
@@ -655,43 +668,35 @@ function createHubCore(options = {}) {
655
668
  if (beating) return;
656
669
  beating = true;
657
670
  try {
658
- await beatOnce();
671
+ const refs = [];
672
+ const watched = /* @__PURE__ */ new Set();
673
+ for (const conn of conns.values()) for (const sub of conn.subs.values()) {
674
+ if (sub.joined) refs.push({
675
+ channel: sub.channel.name,
676
+ id: conn.id
677
+ });
678
+ if (sub.joined || sub.watch) watched.add(sub.channel.name);
679
+ }
680
+ if (refs.length === 0 && watched.size === 0) {
681
+ clearInterval(heartbeat);
682
+ heartbeat = void 0;
683
+ return;
684
+ }
685
+ try {
686
+ const missing = await adapter.presenceTouch(refs, limits.presenceTtlMs);
687
+ for (const ref of missing) {
688
+ const sub = conns.get(ref.id)?.subs.get(ref.channel);
689
+ if (sub === void 0 || !sub.joined) continue;
690
+ await adapter.presenceSet(ref.channel, memberOf(sub), limits.presenceTtlMs);
691
+ }
692
+ for (const channel of watched) await adapter.presenceReap(channel);
693
+ } catch (error) {
694
+ report(error, { where: "presenceHeartbeat" });
695
+ }
659
696
  } finally {
660
697
  beating = false;
661
698
  }
662
699
  }
663
- async function beatOnce() {
664
- const refs = [];
665
- const watched = /* @__PURE__ */ new Set();
666
- for (const conn of conns.values()) for (const sub of conn.subs.values()) {
667
- if (sub.joined) refs.push({
668
- channel: sub.channel.name,
669
- id: conn.id
670
- });
671
- if (sub.joined || sub.watch) watched.add(sub.channel.name);
672
- }
673
- if (refs.length === 0 && watched.size === 0) {
674
- clearInterval(heartbeat);
675
- heartbeat = void 0;
676
- return;
677
- }
678
- try {
679
- const missing = await adapter.presenceTouch(refs, limits.presenceTtlMs);
680
- for (const ref of missing) {
681
- const sub = conns.get(ref.id)?.subs.get(ref.channel);
682
- if (sub === void 0 || !sub.joined) continue;
683
- await adapter.presenceSet(ref.channel, {
684
- id: ref.id,
685
- ...sub.conn.identity.user === void 0 ? {} : { u: sub.conn.identity.user },
686
- s: sub.state,
687
- v: sub.version
688
- }, limits.presenceTtlMs);
689
- }
690
- for (const channel of watched) await adapter.presenceReap(channel);
691
- } catch (error) {
692
- report(error, { where: "presenceHeartbeat" });
693
- }
694
- }
695
700
  for (const [pattern, rule] of Object.entries(options.channels ?? {})) rules.push(compileRule(pattern, rule));
696
701
  const core = {
697
702
  limits,
@@ -704,8 +709,8 @@ function createHubCore(options = {}) {
704
709
  return core;
705
710
  },
706
711
  connect(transport, identity = {}, connectOptions = {}) {
707
- if (closing) throw new RealtimeError("closed", "the hub is closed");
708
- let release = () => void 0;
712
+ if (closing) throw new RealtimeError("closed", ...["the hub is closed"]);
713
+ let release;
709
714
  const done = new Promise((resolve) => {
710
715
  release = resolve;
711
716
  });
@@ -718,14 +723,12 @@ function createHubCore(options = {}) {
718
723
  subs: /* @__PURE__ */ new Map(),
719
724
  queue: [],
720
725
  publishBucket: connectOptions.publishBucket ?? new TokenBucket(limits.publishesPerSecond, limits.publishBurst, now()),
721
- controlBucket: new TokenBucket(limits.publishesPerSecond, Math.max(limits.publishBurst, limits.maxChannelsPerConnection), now()),
726
+ ctl: new TokenBucket(limits.publishesPerSecond, Math.max(limits.publishBurst, limits.maxChannelsPerConnection), now()),
722
727
  stateless: connectOptions.stateless === true,
723
728
  waiters: [],
724
729
  invalid: new WindowCounter(10, 6e4),
725
730
  limited: new WindowCounter(50, 1e4),
726
731
  pending: /* @__PURE__ */ new Set(),
727
- running: false,
728
- closed: false,
729
732
  release,
730
733
  done
731
734
  };
@@ -766,9 +769,9 @@ function createHubCore(options = {}) {
766
769
  };
767
770
  },
768
771
  async publish(channel, event, data, publishOptions = {}) {
769
- if (!isChannelName(channel)) throw new RealtimeError("bad_channel", `"${channel}" is not a valid channel name`);
770
- if (!isEventName(event)) throw new RealtimeError("bad_frame", `"${event}" is not a valid event name`);
771
- if (publishOptions.key !== void 0 && !isIdempotencyKey(publishOptions.key)) throw new RealtimeError("bad_frame", "the idempotency key is not valid");
772
+ if (!isChannelName(channel)) throw new RealtimeError("bad_channel", ...[`"${channel}" is not a valid channel name`]);
773
+ if (!isEventName(event)) throw new RealtimeError("bad_frame", ...[`"${event}" is not a valid event name`]);
774
+ if (publishOptions.key !== void 0 && !isIdempotencyKey(publishOptions.key)) throw new RealtimeError("bad_frame", ...["the idempotency key is not valid"]);
772
775
  let text;
773
776
  try {
774
777
  text = JSON.stringify(data === void 0 ? null : data);
@@ -776,7 +779,7 @@ function createHubCore(options = {}) {
776
779
  throw new TypeError("the data is not JSON", { cause: error });
777
780
  }
778
781
  if (text === void 0) throw new TypeError("the data is not JSON");
779
- if (byteLength(text) > limits.maxMessageBytes) throw new RealtimeError("too_large", "the message is too large");
782
+ if (byteLength(text) > limits.maxMessageBytes) throw new RealtimeError("too_large", ...["the message is too large"]);
780
783
  const found = match(channel);
781
784
  const result = await adapter.publish(channel, {
782
785
  event,
@@ -785,18 +788,18 @@ function createHubCore(options = {}) {
785
788
  history: found?.rule.history ?? limits.history,
786
789
  historyTtlMs: limits.historyTtlMs,
787
790
  idempotencyTtlMs: limits.idempotencyTtlMs,
788
- ...publishOptions.key === void 0 ? {} : { key: publishOptions.key }
791
+ ...publishOptions.key === void 0 ? {} : { key: `${publishOptions.key}/s` }
789
792
  });
790
793
  counters.published += result.duplicate ? 0 : 1;
791
794
  return result;
792
795
  },
793
796
  async setMemberState(identity, channel, connectionId, state) {
794
- if (identity.user === void 0) throw new RealtimeError("unauthorized", "presence changes over HTTP need a signed-in user");
797
+ if (identity.user === void 0) throw new RealtimeError("unauthorized", ...["presence changes over HTTP need a signed-in user"]);
795
798
  const found = match(channel);
796
799
  if (found === void 0 || !await allowed(found.rule, "presence", channel, found.params, identity)) throw new RealtimeError("forbidden");
797
- if (byteLength(JSON.stringify(state)) > limits.maxStateBytes) throw new RealtimeError("too_large", "the presence state is too large");
800
+ if (byteLength(JSON.stringify(state)) > limits.maxStateBytes) throw new RealtimeError("too_large", ...["the presence state is too large"]);
798
801
  const member = (await adapter.presenceList(channel)).find((item) => item.id === connectionId);
799
- if (member === void 0 || member.u !== identity.user) throw new RealtimeError("forbidden", "that connection is not yours");
802
+ if (member === void 0 || member.u !== identity.user) throw new RealtimeError("forbidden", ...["that connection is not yours"]);
800
803
  const local = conns.get(connectionId)?.subs.get(channel);
801
804
  const version = Math.max(member.v, local?.version ?? 0) + 1;
802
805
  if (local?.joined === true) {
@@ -812,15 +815,16 @@ function createHubCore(options = {}) {
812
815
  },
813
816
  revoke(target) {
814
817
  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.");
818
+ 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);
815
819
  let count = 0;
816
- for (const conn of [...conns.values()]) {
817
- if (target.connection !== void 0 && conn.id !== target.connection) continue;
818
- if (target.user !== void 0 && conn.identity.user !== target.user) continue;
819
- for (const sub of [...conn.subs.values()]) {
820
- if (target.channel !== void 0 && sub.channel.name !== target.channel) continue;
821
- removeSub(sub, "revoked");
822
- count += 1;
823
- }
820
+ for (const item of judging) if (hit(item.conn, item.channel)) {
821
+ judging.delete(item);
822
+ count += 1;
823
+ }
824
+ for (const conn of [...conns.values()]) for (const sub of [...conn.subs.values()]) {
825
+ if (!hit(conn, sub.channel.name)) continue;
826
+ removeSub(sub, "revoked");
827
+ count += 1;
824
828
  }
825
829
  return count;
826
830
  },