@shadow-js/realtime 1.0.0 → 1.0.2

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +35 -0
  3. package/dist/dev/adapter.d.ts +0 -2
  4. package/dist/dev/client/core.d.ts +0 -2
  5. package/dist/dev/client/core.js +105 -108
  6. package/dist/dev/client/core.js.map +1 -1
  7. package/dist/dev/client/presence.js +1 -2
  8. package/dist/dev/client/sse.js +34 -26
  9. package/dist/dev/client/sse.js.map +1 -1
  10. package/dist/dev/client/types.d.ts +6 -2
  11. package/dist/dev/client/util.js +3 -4
  12. package/dist/dev/client/util.js.map +1 -1
  13. package/dist/dev/client/ws.js +1 -2
  14. package/dist/dev/cloudflare/adapter.d.ts +0 -2
  15. package/dist/dev/cloudflare/adapter.js +1 -2
  16. package/dist/dev/cloudflare/core.js +1 -2
  17. package/dist/dev/cloudflare/link.js +70 -10
  18. package/dist/dev/cloudflare/link.js.map +1 -1
  19. package/dist/dev/cloudflare/object.d.ts +0 -2
  20. package/dist/dev/cloudflare/object.js +1 -2
  21. package/dist/dev/cloudflare/types.d.ts +0 -2
  22. package/dist/dev/cloudflare/wire.js +1 -2
  23. package/dist/dev/errors.d.ts +0 -2
  24. package/dist/dev/errors.js +1 -2
  25. package/dist/dev/hooks.d.ts +0 -2
  26. package/dist/dev/hooks.js +1 -2
  27. package/dist/dev/memory.d.ts +11 -2
  28. package/dist/dev/memory.js +39 -12
  29. package/dist/dev/memory.js.map +1 -1
  30. package/dist/dev/protocol.d.ts +0 -2
  31. package/dist/dev/protocol.js +14 -14
  32. package/dist/dev/protocol.js.map +1 -1
  33. package/dist/dev/redis/client.d.ts +0 -2
  34. package/dist/dev/redis/client.js +1 -2
  35. package/dist/dev/redis/scripts.js +1 -2
  36. package/dist/dev/redis/topics.d.ts +6 -3
  37. package/dist/dev/redis/topics.js +5 -3
  38. package/dist/dev/redis/topics.js.map +1 -1
  39. package/dist/dev/redis.d.ts +0 -2
  40. package/dist/dev/redis.js +1 -2
  41. package/dist/dev/server/http.d.ts +0 -2
  42. package/dist/dev/server/http.js +38 -26
  43. package/dist/dev/server/http.js.map +1 -1
  44. package/dist/dev/server/hub.d.ts +0 -2
  45. package/dist/dev/server/hub.js +67 -41
  46. package/dist/dev/server/hub.js.map +1 -1
  47. package/dist/dev/server/index.d.ts +2 -4
  48. package/dist/dev/server/index.js +1 -2
  49. package/dist/dev/server/index.js.map +1 -1
  50. package/dist/dev/server/limits.d.ts +4 -3
  51. package/dist/dev/server/limits.js +1 -2
  52. package/dist/dev/server/limits.js.map +1 -1
  53. package/dist/dev/server/rules.d.ts +0 -2
  54. package/dist/dev/server/rules.js +1 -2
  55. package/dist/dev/server/rules.js.map +1 -1
  56. package/dist/dev/testing.d.ts +0 -2
  57. package/dist/dev/testing.js +1 -2
  58. package/dist/prod/client/core.js +1 -1
  59. package/dist/prod/client/core.js.map +1 -1
  60. package/dist/prod/client/sse.js +1 -4
  61. package/dist/prod/client/sse.js.map +1 -1
  62. package/dist/prod/client/util.js +1 -1
  63. package/dist/prod/client/util.js.map +1 -1
  64. package/dist/prod/cloudflare/link.js +1 -1
  65. package/dist/prod/cloudflare/link.js.map +1 -1
  66. package/dist/prod/memory.js +1 -1
  67. package/dist/prod/memory.js.map +1 -1
  68. package/dist/prod/protocol.js +1 -1
  69. package/dist/prod/protocol.js.map +1 -1
  70. package/dist/prod/redis/topics.js +1 -1
  71. package/dist/prod/redis/topics.js.map +1 -1
  72. package/dist/prod/server/http.js +1 -1
  73. package/dist/prod/server/http.js.map +1 -1
  74. package/dist/prod/server/hub.js +1 -1
  75. package/dist/prod/server/hub.js.map +1 -1
  76. package/dist/prod/server/index.js.map +1 -1
  77. package/dist/prod/server/limits.js +1 -1
  78. package/dist/prod/server/limits.js.map +1 -1
  79. package/dist/prod/server/rules.js +1 -1
  80. package/dist/prod/server/rules.js.map +1 -1
  81. package/package.json +6 -5
@@ -1,6 +1,6 @@
1
1
  import { RealtimeError } from "../errors.js";
2
2
  import { decodeLinkFrame } from "./wire.js";
3
- //#region src/cloudflare/link.ts
3
+ // src/cloudflare/link.ts
4
4
  /**
5
5
  * The Worker side of a channel's subscribe socket: one socket per channel and adapter, shared by every local listener, kept
6
6
  * open with backoff, resumed with `after`, and feeding each listener through its own queue.
@@ -28,13 +28,24 @@ function deferred() {
28
28
  var Pump = class {
29
29
  #queue = [];
30
30
  #max;
31
+ #onDrop;
31
32
  #wake;
32
33
  #stopped = false;
33
- constructor(max) {
34
+ /**
35
+ * @param max The most items waiting for the loop.
36
+ * @param onDrop Called for every item refused because the queue is full, so the owner can report it and repair what the
37
+ * listener missed. It runs inside `push`, so it must not call back into this pump.
38
+ */
39
+ constructor(max, onDrop) {
34
40
  this.#max = max;
41
+ this.#onDrop = onDrop;
35
42
  }
36
43
  push(item) {
37
- if (this.#stopped || this.#queue.length >= this.#max) return;
44
+ if (this.#stopped) return;
45
+ if (this.#queue.length >= this.#max) {
46
+ this.#onDrop();
47
+ return;
48
+ }
38
49
  this.#queue.push(item);
39
50
  const wake = this.#wake;
40
51
  this.#wake = void 0;
@@ -47,12 +58,14 @@ var Pump = class {
47
58
  this.#wake = void 0;
48
59
  wake?.();
49
60
  }
50
- async run(deliver, report, every) {
61
+ async run(deliver, report, every, idle) {
51
62
  const cancel = every?.();
52
63
  try {
53
64
  while (!this.#stopped) {
54
65
  const item = this.#queue.shift();
55
66
  if (item === void 0) {
67
+ idle?.();
68
+ if (this.#queue.length > 0) continue;
56
69
  await new Promise((resolve) => {
57
70
  this.#wake = resolve;
58
71
  });
@@ -105,11 +118,14 @@ var ChannelLink = class {
105
118
  addMessages(listener) {
106
119
  const sub = {
107
120
  listener,
108
- pump: new Pump(this.#host.maxQueued),
109
- last: this.#state === "open" ? this.#last : -1
121
+ pump: new Pump(this.#host.maxQueued, () => this.#dropped(sub)),
122
+ last: this.#state === "open" ? this.#last : -1,
123
+ overflowed: false
110
124
  };
111
125
  this.#messages.add(sub);
112
- sub.pump.run(listener, (error) => this.#host.report(error, "listener", this.channel), this.#nudger());
126
+ sub.pump.run(listener, (error) => this.#host.report(error, "listener", this.channel), this.#nudger(), () => {
127
+ sub.overflowed = false;
128
+ });
113
129
  this.#start();
114
130
  return sub;
115
131
  }
@@ -117,13 +133,58 @@ var ChannelLink = class {
117
133
  addPresence(listener) {
118
134
  const sub = {
119
135
  listener,
120
- pump: new Pump(this.#host.maxQueued)
136
+ pump: new Pump(this.#host.maxQueued, () => this.#dropped(sub)),
137
+ view: /* @__PURE__ */ new Map(),
138
+ stale: false
121
139
  };
122
140
  this.#presence.add(sub);
123
- sub.pump.run(listener, (error) => this.#host.report(error, "listener", this.channel), this.#nudger());
141
+ sub.pump.run((event) => {
142
+ if (event.op === "leave") sub.view.delete(event.member.id);
143
+ else sub.view.set(event.member.id, event.member);
144
+ listener(event);
145
+ }, (error) => this.#host.report(error, "listener", this.channel), this.#nudger(), () => this.#resync(sub));
124
146
  this.#start();
125
147
  return sub;
126
148
  }
149
+ /** A listener's queue refused an item: reported once per overflow, and a presence listener is marked for a resync. */
150
+ #dropped(sub) {
151
+ if ("view" in sub) {
152
+ if (sub.stale) return;
153
+ sub.stale = true;
154
+ } else {
155
+ if (sub.overflowed) return;
156
+ sub.overflowed = true;
157
+ }
158
+ this.#host.report(new RealtimeError("limit", "a listener fell behind: its queue is full and events were dropped"), "queue", this.channel);
159
+ }
160
+ /**
161
+ * Called when a presence listener has caught up: after a drop it is sent the difference between what it was given and the
162
+ * member set the link holds (leaves, joins, changes), so one overflow cannot leave its picture of the room wrong for good.
163
+ * When the difference is larger than the queue, the rest follows at the next catch-up.
164
+ */
165
+ #resync(sub) {
166
+ if (!sub.stale) return;
167
+ sub.stale = false;
168
+ const current = this.#members ?? /* @__PURE__ */ new Map();
169
+ for (const [id, member] of sub.view) if (!current.has(id)) sub.pump.push({
170
+ channel: this.channel,
171
+ op: "leave",
172
+ member
173
+ });
174
+ for (const [id, member] of current) {
175
+ const seen = sub.view.get(id);
176
+ if (seen === void 0) sub.pump.push({
177
+ channel: this.channel,
178
+ op: "join",
179
+ member
180
+ });
181
+ else if (member.v > seen.v) sub.pump.push({
182
+ channel: this.channel,
183
+ op: "set",
184
+ member
185
+ });
186
+ }
187
+ }
127
188
  /** Removes a listener; the last one closes the link. */
128
189
  remove(sub) {
129
190
  sub.pump.stop();
@@ -391,7 +452,6 @@ var ChannelLink = class {
391
452
  this.#host.forget(this);
392
453
  }
393
454
  };
394
- //#endregion
395
455
  export { ChannelLink };
396
456
 
397
457
  //# sourceMappingURL=link.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"link.js","names":[],"sources":["../../../src/cloudflare/link.ts"],"sourcesContent":["/**\n * The Worker side of a channel's subscribe socket: one socket per channel and adapter, shared by every local listener, kept\n * open with backoff, resumed with `after`, and feeding each listener through its own queue.\n *\n * A Worker may use an I/O object (a socket, a stream, a timer) only inside the request that created it. A listener runs in the\n * request that subscribed, so it is called by a loop that was started there and woken by a plain promise: the socket's\n * handler only moves data into queues and never calls a listener itself.\n * @packageDocumentation\n */\nimport { RealtimeError } from \"../errors\";\nimport type { Member, PresenceEvent, StoredMessage } from \"../protocol\";\nimport type { ClientSocket } from \"./types\";\nimport { decodeLinkFrame } from \"./wire\";\n\n/** What a link needs from the adapter that owns it. @internal */\nexport interface LinkHost {\n readonly reconnectMinMs: number;\n readonly reconnectMaxMs: number;\n /** Ping period; 0 turns pings off. */\n readonly keepAliveMs: number;\n readonly connectTimeoutMs: number;\n /** The most events queued for one listener. */\n readonly maxQueued: number;\n now(): number;\n random(): number;\n /** Opens the subscribe socket (not yet accepted). */\n open(channel: string, after: number | undefined): Promise<ClientSocket>;\n /** Runs `task` once after `ms`; returns the way to cancel. Everything scheduled is canceled when the adapter closes. */\n after(ms: number, task: () => void): () => void;\n /** Runs `task` every `ms`; returns the way to cancel. */\n every(ms: number, task: () => void): () => void;\n report(error: unknown, where: string, channel: string): void;\n /** Called once when the link has nothing left to do. */\n forget(link: ChannelLink): void;\n}\n\ninterface Deferred {\n readonly promise: Promise<void>;\n resolve(): void;\n reject(error: unknown): void;\n}\n\nfunction deferred(): Deferred {\n let resolve: () => void = () => undefined;\n let reject: (error: unknown) => void = () => undefined;\n const promise = new Promise<void>((onResolve, onReject) => {\n resolve = onResolve;\n reject = onReject;\n });\n // A waiter may give up first; the rejection must not become an unhandled one.\n promise.catch(() => undefined);\n return { promise, resolve, reject };\n}\n\n/** A queue and the loop that empties it, in the request that created it. */\nclass Pump<T> {\n readonly #queue: T[] = [];\n readonly #max: number;\n #wake: (() => void) | undefined;\n #stopped = false;\n\n constructor(max: number) {\n this.#max = max;\n }\n\n push(item: T): void {\n if (this.#stopped || this.#queue.length >= this.#max) return;\n this.#queue.push(item);\n const wake = this.#wake;\n this.#wake = undefined;\n wake?.();\n }\n\n stop(): void {\n this.#stopped = true;\n this.#queue.length = 0;\n const wake = this.#wake;\n this.#wake = undefined;\n wake?.();\n }\n\n async run(\n deliver: (item: T) => void,\n report: (error: unknown) => void,\n every: (() => () => void) | undefined,\n ): Promise<void> {\n const cancel = every?.();\n try {\n while (!this.#stopped) {\n const item = this.#queue.shift();\n if (item === undefined) {\n await new Promise<void>((resolve) => {\n this.#wake = resolve;\n });\n continue;\n }\n try {\n deliver(item);\n } catch (error) {\n report(error);\n }\n }\n } finally {\n cancel?.();\n }\n }\n}\n\ninterface MessageSub {\n readonly listener: (message: StoredMessage) => void;\n readonly pump: Pump<StoredMessage>;\n /** The highest seq queued for this listener; -1 until the link tells the head. */\n last: number;\n}\n\ninterface PresenceSub {\n readonly listener: (event: PresenceEvent) => void;\n readonly pump: Pump<PresenceEvent>;\n}\n\n/** A way to stop one listener. @internal */\nexport type LinkSubscription = MessageSub | PresenceSub;\n\ntype LinkState = \"idle\" | \"connecting\" | \"open\" | \"backoff\" | \"disposed\";\n\n/** @internal */\nexport class ChannelLink {\n readonly channel: string;\n readonly #host: LinkHost;\n readonly #messages = new Set<MessageSub>();\n readonly #presence = new Set<PresenceSub>();\n #state: LinkState = \"idle\";\n #socket: ClientSocket | undefined;\n #closeOwner: Deferred | undefined;\n #attempt = 0;\n /** The highest seq received on this link: where a reconnect resumes. */\n #last = 0;\n #known = false;\n #members: Map<string, Member> | undefined;\n #lastReceived = 0;\n #failures = 0;\n #retryAt = 0;\n #connectedAt = 0;\n #ready: Deferred = deferred();\n #cancelRetry: (() => void) | undefined;\n #cancelConnect: (() => void) | undefined;\n #cancelKeep: (() => void) | undefined;\n\n constructor(channel: string, host: LinkHost) {\n this.channel = channel;\n this.#host = host;\n }\n\n get state(): LinkState {\n return this.#state;\n }\n\n get size(): number {\n return this.#messages.size + this.#presence.size;\n }\n\n // --- listeners -----------------------------------------------------------------------------------------------------\n\n /** Adds a message listener (call it in the request that will receive messages). */\n addMessages(listener: (message: StoredMessage) => void): LinkSubscription {\n const sub: MessageSub = {\n listener,\n pump: new Pump(this.#host.maxQueued),\n last: this.#state === \"open\" ? this.#last : -1,\n };\n this.#messages.add(sub);\n void sub.pump.run(\n listener,\n (error) => this.#host.report(error, \"listener\", this.channel),\n this.#nudger(),\n );\n this.#start();\n return sub;\n }\n\n /** Adds a presence listener (call it in the request that will receive events). */\n addPresence(listener: (event: PresenceEvent) => void): LinkSubscription {\n const sub: PresenceSub = { listener, pump: new Pump(this.#host.maxQueued) };\n this.#presence.add(sub);\n void sub.pump.run(\n listener,\n (error) => this.#host.report(error, \"listener\", this.channel),\n this.#nudger(),\n );\n this.#start();\n return sub;\n }\n\n /** Removes a listener; the last one closes the link. */\n remove(sub: LinkSubscription): void {\n sub.pump.stop();\n this.#messages.delete(sub as MessageSub);\n this.#presence.delete(sub as PresenceSub);\n if (this.size === 0) this.dispose(new RealtimeError(\"closed\", \"the subscription ended\"));\n }\n\n /** Resolves when the socket is open and has told the channel's head; rejects after `ms`. */\n whenReady(ms: number): Promise<void> {\n if (this.#state === \"open\") return Promise.resolve();\n return new Promise<void>((resolve, reject) => {\n const cancel = this.#host.after(ms, () =>\n reject(new RealtimeError(\"unavailable\", \"timed out connecting to the channel object\")),\n );\n this.#ready.promise.then(\n () => {\n cancel();\n resolve();\n },\n (error: unknown) => {\n cancel();\n reject(error);\n },\n );\n });\n }\n\n // --- connecting ----------------------------------------------------------------------------------------------------\n\n /** The pump's periodic check, if pings are on. */\n #nudger(): (() => () => void) | undefined {\n const every = this.#host.keepAliveMs;\n if (every <= 0) return undefined;\n return () => this.#host.every(every, () => this.#check());\n }\n\n #start(): void {\n if (this.#state === \"idle\") this.#connect();\n }\n\n #connect(): void {\n if (this.#state === \"disposed\") return;\n this.#cancelRetry?.();\n this.#cancelRetry = undefined;\n this.#cancelConnect?.();\n this.#state = \"connecting\";\n const attempt = ++this.#attempt;\n this.#connectedAt = this.#host.now();\n this.#cancelConnect = this.#host.after(this.#host.connectTimeoutMs, () => {\n if (attempt === this.#attempt && this.#state === \"connecting\")\n this.#down(attempt, new RealtimeError(\"unavailable\", \"the channel object did not answer\"));\n });\n this.#host.open(this.channel, this.#known ? this.#last : undefined).then(\n (socket) => this.#opened(attempt, socket),\n (error: unknown) => {\n if (attempt === this.#attempt) this.#down(attempt, error);\n },\n );\n }\n\n #opened(attempt: number, socket: ClientSocket): void {\n if (attempt !== this.#attempt || this.#state !== \"connecting\") {\n this.#closeSocket(socket, undefined);\n return;\n }\n const owner = deferred();\n this.#closeOwner = owner;\n this.#socket = socket;\n // Waits in the request that opened the socket, which is the one place guaranteed to be allowed to close it.\n void owner.promise.then(\n () => this.#tryClose(socket),\n () => undefined,\n );\n socket.addEventListener(\"message\", (event) => {\n if (attempt !== this.#attempt || typeof event.data !== \"string\") return;\n this.#received(attempt, event.data);\n });\n socket.addEventListener(\"close\", () => {\n if (attempt === this.#attempt)\n this.#down(attempt, new RealtimeError(\"unavailable\", \"closed\"));\n });\n socket.addEventListener(\"error\", () => {\n if (attempt === this.#attempt) this.#down(attempt, new RealtimeError(\"unavailable\", \"error\"));\n });\n try {\n socket.accept();\n } catch (error) {\n this.#down(attempt, error);\n }\n }\n\n #tryClose(socket: ClientSocket): boolean {\n try {\n socket.close(1000, \"done\");\n return true;\n } catch {\n return false;\n }\n }\n\n /** Closes a socket from wherever this runs; when the runtime refuses, the request that opened it does it. */\n #closeSocket(socket: ClientSocket | undefined, owner: Deferred | undefined): void {\n if (socket === undefined) return;\n if (!this.#tryClose(socket)) owner?.resolve();\n }\n\n /** The socket failed or closed: schedule a new attempt, or end the link when nobody listens. */\n #down(attempt: number, error: unknown): void {\n if (attempt !== this.#attempt || this.#state === \"disposed\") return;\n this.#attempt += 1;\n this.#cancelConnect?.();\n this.#cancelConnect = undefined;\n this.#cancelKeep?.();\n this.#cancelKeep = undefined;\n const socket = this.#socket;\n const owner = this.#closeOwner;\n this.#socket = undefined;\n this.#closeOwner = undefined;\n this.#closeSocket(socket, owner);\n // Waiters keep the promise they hold until the link is open; only an opened link needs a new one.\n if (this.#state === \"open\") this.#ready = deferred();\n if (this.size === 0) {\n this.dispose(error);\n return;\n }\n this.#state = \"backoff\";\n this.#failures += 1;\n const ceiling = Math.min(\n this.#host.reconnectMaxMs,\n this.#host.reconnectMinMs * 2 ** Math.min(this.#failures - 1, 20),\n );\n const delay = Math.floor(this.#host.random() * ceiling);\n this.#retryAt = this.#host.now() + delay;\n this.#cancelRetry?.();\n this.#cancelRetry = this.#host.after(delay, () => this.#connect());\n }\n\n /** Reconnects now, without waiting: the object asked for it (a long replay is continued with a new socket). */\n #resume(attempt: number): void {\n if (attempt !== this.#attempt) return;\n this.#attempt += 1;\n const socket = this.#socket;\n const owner = this.#closeOwner;\n this.#socket = undefined;\n this.#closeOwner = undefined;\n this.#cancelKeep?.();\n this.#cancelKeep = undefined;\n this.#closeSocket(socket, owner);\n this.#state = \"idle\";\n this.#connect();\n }\n\n /**\n * A health check run by every listener's loop (each in its own request) and by the ping timer: whichever request is alive\n * can repair a link whose own request is gone, because a timer or a socket handler of a finished request never runs again.\n */\n #check(): void {\n const now = this.#host.now();\n const grace = Math.max(1000, this.#host.keepAliveMs);\n switch (this.#state) {\n case \"backoff\":\n if (now > this.#retryAt + grace) this.#connect();\n return;\n case \"connecting\":\n if (now > this.#connectedAt + this.#host.connectTimeoutMs * 2) this.#connect();\n return;\n case \"open\": {\n const keep = this.#host.keepAliveMs;\n if (keep > 0 && now - this.#lastReceived > keep * 3) {\n this.#down(\n this.#attempt,\n new RealtimeError(\"unavailable\", \"the channel object went silent\"),\n );\n return;\n }\n try {\n this.#socket?.send(\"ping\");\n } catch {\n // Sending from another request is refused; the owner's own ping covers it.\n }\n return;\n }\n default:\n return;\n }\n }\n\n // --- frames --------------------------------------------------------------------------------------------------------\n\n #received(attempt: number, text: string): void {\n this.#lastReceived = this.#host.now();\n if (text === \"pong\") return;\n const frame = decodeLinkFrame(text);\n if (frame === undefined) return;\n switch (frame.t) {\n case \"ready\": {\n if (this.#known && frame.head < this.#last) {\n // The channel's numbering restarted (it was forgotten): follow it.\n this.#last = frame.head;\n for (const sub of this.#messages) sub.last = Math.min(sub.last, frame.head);\n }\n if (!this.#known) {\n this.#last = frame.head;\n this.#known = true;\n }\n for (const sub of this.#messages) if (sub.last < 0) sub.last = frame.head;\n this.#state = \"open\";\n this.#failures = 0;\n this.#cancelConnect?.();\n this.#cancelConnect = undefined;\n this.#lastReceived = this.#host.now();\n if (this.#host.keepAliveMs > 0) {\n this.#cancelKeep?.();\n this.#cancelKeep = this.#host.every(this.#host.keepAliveMs, () => this.#check());\n }\n this.#ready.resolve();\n return;\n }\n case \"snap\": {\n const next = new Map<string, Member>();\n for (const member of frame.members) next.set(member.id, member);\n const before = this.#members;\n this.#members = next;\n if (before === undefined) return;\n for (const [id, member] of before)\n if (!next.has(id)) this.#fanPresence({ channel: this.channel, op: \"leave\", member });\n for (const [id, member] of next) {\n const old = before.get(id);\n if (old === undefined) this.#fanPresence({ channel: this.channel, op: \"join\", member });\n else if (member.v > old.v)\n this.#fanPresence({ channel: this.channel, op: \"set\", member });\n }\n return;\n }\n case \"msg\": {\n const message = frame.m;\n if (message.seq > this.#last) this.#last = message.seq;\n for (const sub of this.#messages)\n if (message.seq > sub.last) {\n sub.last = message.seq;\n sub.pump.push(message);\n }\n return;\n }\n case \"pres\": {\n const members = this.#members ?? new Map<string, Member>();\n this.#members = members;\n const known = members.get(frame.member.id);\n if (frame.op === \"leave\") {\n if (known === undefined) return;\n members.delete(frame.member.id);\n } else {\n if (known !== undefined && known.v >= frame.member.v && frame.op === \"join\") return;\n if (known !== undefined && known.v > frame.member.v) return;\n members.set(frame.member.id, frame.member);\n }\n this.#fanPresence({ channel: this.channel, op: frame.op, member: frame.member });\n return;\n }\n case \"more\":\n this.#resume(attempt);\n return;\n case \"bye\":\n this.#down(attempt, new RealtimeError(\"unavailable\", frame.why));\n return;\n }\n }\n\n #fanPresence(event: PresenceEvent): void {\n for (const sub of this.#presence) sub.pump.push(event);\n }\n\n // --- ending --------------------------------------------------------------------------------------------------------\n\n /** Ends the link: stops every listener, cancels timers and closes the socket. */\n dispose(reason: unknown): void {\n if (this.#state === \"disposed\") return;\n this.#state = \"disposed\";\n this.#attempt += 1;\n this.#cancelRetry?.();\n this.#cancelConnect?.();\n this.#cancelKeep?.();\n this.#cancelRetry = undefined;\n this.#cancelConnect = undefined;\n this.#cancelKeep = undefined;\n const socket = this.#socket;\n const owner = this.#closeOwner;\n this.#socket = undefined;\n this.#closeOwner = undefined;\n this.#closeSocket(socket, owner);\n this.#ready.reject(reason);\n for (const sub of this.#messages) sub.pump.stop();\n for (const sub of this.#presence) sub.pump.stop();\n this.#messages.clear();\n this.#presence.clear();\n this.#host.forget(this);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"link.js","names":[],"sources":["../../../src/cloudflare/link.ts"],"sourcesContent":["/**\n * The Worker side of a channel's subscribe socket: one socket per channel and adapter, shared by every local listener, kept\n * open with backoff, resumed with `after`, and feeding each listener through its own queue.\n *\n * A Worker may use an I/O object (a socket, a stream, a timer) only inside the request that created it. A listener runs in the\n * request that subscribed, so it is called by a loop that was started there and woken by a plain promise: the socket's\n * handler only moves data into queues and never calls a listener itself.\n * @packageDocumentation\n */\nimport { RealtimeError } from \"../errors\";\nimport type { Member, PresenceEvent, StoredMessage } from \"../protocol\";\nimport type { ClientSocket } from \"./types\";\nimport { decodeLinkFrame } from \"./wire\";\n\n/** What a link needs from the adapter that owns it. @internal */\nexport interface LinkHost {\n readonly reconnectMinMs: number;\n readonly reconnectMaxMs: number;\n /** Ping period; 0 turns pings off. */\n readonly keepAliveMs: number;\n readonly connectTimeoutMs: number;\n /** The most events queued for one listener. */\n readonly maxQueued: number;\n now(): number;\n random(): number;\n /** Opens the subscribe socket (not yet accepted). */\n open(channel: string, after: number | undefined): Promise<ClientSocket>;\n /** Runs `task` once after `ms`; returns the way to cancel. Everything scheduled is canceled when the adapter closes. */\n after(ms: number, task: () => void): () => void;\n /** Runs `task` every `ms`; returns the way to cancel. */\n every(ms: number, task: () => void): () => void;\n report(error: unknown, where: string, channel: string): void;\n /** Called once when the link has nothing left to do. */\n forget(link: ChannelLink): void;\n}\n\ninterface Deferred {\n readonly promise: Promise<void>;\n resolve(): void;\n reject(error: unknown): void;\n}\n\nfunction deferred(): Deferred {\n let resolve: () => void = () => undefined;\n let reject: (error: unknown) => void = () => undefined;\n const promise = new Promise<void>((onResolve, onReject) => {\n resolve = onResolve;\n reject = onReject;\n });\n // A waiter may give up first; the rejection must not become an unhandled one.\n promise.catch(() => undefined);\n return { promise, resolve, reject };\n}\n\n/** A queue and the loop that empties it, in the request that created it. */\nclass Pump<T> {\n readonly #queue: T[] = [];\n readonly #max: number;\n readonly #onDrop: () => void;\n #wake: (() => void) | undefined;\n #stopped = false;\n\n /**\n * @param max The most items waiting for the loop.\n * @param onDrop Called for every item refused because the queue is full, so the owner can report it and repair what the\n * listener missed. It runs inside `push`, so it must not call back into this pump.\n */\n constructor(max: number, onDrop: () => void) {\n this.#max = max;\n this.#onDrop = onDrop;\n }\n\n push(item: T): void {\n if (this.#stopped) return;\n if (this.#queue.length >= this.#max) {\n this.#onDrop();\n return;\n }\n this.#queue.push(item);\n const wake = this.#wake;\n this.#wake = undefined;\n wake?.();\n }\n\n stop(): void {\n this.#stopped = true;\n this.#queue.length = 0;\n const wake = this.#wake;\n this.#wake = undefined;\n wake?.();\n }\n\n async run(\n deliver: (item: T) => void,\n report: (error: unknown) => void,\n every: (() => () => void) | undefined,\n idle?: () => void,\n ): Promise<void> {\n const cancel = every?.();\n try {\n while (!this.#stopped) {\n const item = this.#queue.shift();\n if (item === undefined) {\n // The queue just emptied: the owner may refill it (to repair what a full queue refused) before the loop sleeps.\n idle?.();\n if (this.#queue.length > 0) continue;\n await new Promise<void>((resolve) => {\n this.#wake = resolve;\n });\n continue;\n }\n try {\n deliver(item);\n } catch (error) {\n report(error);\n }\n }\n } finally {\n cancel?.();\n }\n }\n}\n\ninterface MessageSub {\n readonly listener: (message: StoredMessage) => void;\n readonly pump: Pump<StoredMessage>;\n /** The highest seq queued for this listener; -1 until the link tells the head. */\n last: number;\n /** True from the first message refused by a full queue until the queue has emptied (one report per overflow). */\n overflowed: boolean;\n}\n\ninterface PresenceSub {\n readonly listener: (event: PresenceEvent) => void;\n readonly pump: Pump<PresenceEvent>;\n /** The members this listener has been given: what a resync is computed against. */\n readonly view: Map<string, Member>;\n /** True after an event was refused by a full queue and until the resync has been queued. */\n stale: boolean;\n}\n\n/** A way to stop one listener. @internal */\nexport type LinkSubscription = MessageSub | PresenceSub;\n\ntype LinkState = \"idle\" | \"connecting\" | \"open\" | \"backoff\" | \"disposed\";\n\n/** @internal */\nexport class ChannelLink {\n readonly channel: string;\n readonly #host: LinkHost;\n readonly #messages = new Set<MessageSub>();\n readonly #presence = new Set<PresenceSub>();\n #state: LinkState = \"idle\";\n #socket: ClientSocket | undefined;\n #closeOwner: Deferred | undefined;\n #attempt = 0;\n /** The highest seq received on this link: where a reconnect resumes. */\n #last = 0;\n #known = false;\n #members: Map<string, Member> | undefined;\n #lastReceived = 0;\n #failures = 0;\n #retryAt = 0;\n #connectedAt = 0;\n #ready: Deferred = deferred();\n #cancelRetry: (() => void) | undefined;\n #cancelConnect: (() => void) | undefined;\n #cancelKeep: (() => void) | undefined;\n\n constructor(channel: string, host: LinkHost) {\n this.channel = channel;\n this.#host = host;\n }\n\n get state(): LinkState {\n return this.#state;\n }\n\n get size(): number {\n return this.#messages.size + this.#presence.size;\n }\n\n // --- listeners -----------------------------------------------------------------------------------------------------\n\n /** Adds a message listener (call it in the request that will receive messages). */\n addMessages(listener: (message: StoredMessage) => void): LinkSubscription {\n const sub: MessageSub = {\n listener,\n pump: new Pump(this.#host.maxQueued, () => this.#dropped(sub)),\n last: this.#state === \"open\" ? this.#last : -1,\n overflowed: false,\n };\n this.#messages.add(sub);\n // A refused message is not retried here: the listener's next message has a gap before it, which the hub fills from history.\n void sub.pump.run(\n listener,\n (error) => this.#host.report(error, \"listener\", this.channel),\n this.#nudger(),\n () => {\n sub.overflowed = false;\n },\n );\n this.#start();\n return sub;\n }\n\n /** Adds a presence listener (call it in the request that will receive events). */\n addPresence(listener: (event: PresenceEvent) => void): LinkSubscription {\n const sub: PresenceSub = {\n listener,\n pump: new Pump(this.#host.maxQueued, () => this.#dropped(sub)),\n view: new Map(),\n stale: false,\n };\n this.#presence.add(sub);\n void sub.pump.run(\n (event) => {\n // The view follows what the listener was given, even when it throws on it.\n if (event.op === \"leave\") sub.view.delete(event.member.id);\n else sub.view.set(event.member.id, event.member);\n listener(event);\n },\n (error) => this.#host.report(error, \"listener\", this.channel),\n this.#nudger(),\n () => this.#resync(sub),\n );\n this.#start();\n return sub;\n }\n\n /** A listener's queue refused an item: reported once per overflow, and a presence listener is marked for a resync. */\n #dropped(sub: MessageSub | PresenceSub): void {\n if (\"view\" in sub) {\n if (sub.stale) return;\n sub.stale = true;\n } else {\n if (sub.overflowed) return;\n sub.overflowed = true;\n }\n this.#host.report(\n new RealtimeError(\n \"limit\",\n \"a listener fell behind: its queue is full and events were dropped\",\n ),\n \"queue\",\n this.channel,\n );\n }\n\n /**\n * Called when a presence listener has caught up: after a drop it is sent the difference between what it was given and the\n * member set the link holds (leaves, joins, changes), so one overflow cannot leave its picture of the room wrong for good.\n * When the difference is larger than the queue, the rest follows at the next catch-up.\n */\n #resync(sub: PresenceSub): void {\n if (!sub.stale) return;\n sub.stale = false;\n const current = this.#members ?? new Map<string, Member>();\n for (const [id, member] of sub.view)\n if (!current.has(id)) sub.pump.push({ channel: this.channel, op: \"leave\", member });\n for (const [id, member] of current) {\n const seen = sub.view.get(id);\n if (seen === undefined) sub.pump.push({ channel: this.channel, op: \"join\", member });\n else if (member.v > seen.v) sub.pump.push({ channel: this.channel, op: \"set\", member });\n }\n }\n\n /** Removes a listener; the last one closes the link. */\n remove(sub: LinkSubscription): void {\n sub.pump.stop();\n this.#messages.delete(sub as MessageSub);\n this.#presence.delete(sub as PresenceSub);\n if (this.size === 0) this.dispose(new RealtimeError(\"closed\", \"the subscription ended\"));\n }\n\n /** Resolves when the socket is open and has told the channel's head; rejects after `ms`. */\n whenReady(ms: number): Promise<void> {\n if (this.#state === \"open\") return Promise.resolve();\n return new Promise<void>((resolve, reject) => {\n const cancel = this.#host.after(ms, () =>\n reject(new RealtimeError(\"unavailable\", \"timed out connecting to the channel object\")),\n );\n this.#ready.promise.then(\n () => {\n cancel();\n resolve();\n },\n (error: unknown) => {\n cancel();\n reject(error);\n },\n );\n });\n }\n\n // --- connecting ----------------------------------------------------------------------------------------------------\n\n /** The pump's periodic check, if pings are on. */\n #nudger(): (() => () => void) | undefined {\n const every = this.#host.keepAliveMs;\n if (every <= 0) return undefined;\n return () => this.#host.every(every, () => this.#check());\n }\n\n #start(): void {\n if (this.#state === \"idle\") this.#connect();\n }\n\n #connect(): void {\n if (this.#state === \"disposed\") return;\n this.#cancelRetry?.();\n this.#cancelRetry = undefined;\n this.#cancelConnect?.();\n this.#state = \"connecting\";\n const attempt = ++this.#attempt;\n this.#connectedAt = this.#host.now();\n this.#cancelConnect = this.#host.after(this.#host.connectTimeoutMs, () => {\n if (attempt === this.#attempt && this.#state === \"connecting\")\n this.#down(attempt, new RealtimeError(\"unavailable\", \"the channel object did not answer\"));\n });\n this.#host.open(this.channel, this.#known ? this.#last : undefined).then(\n (socket) => this.#opened(attempt, socket),\n (error: unknown) => {\n if (attempt === this.#attempt) this.#down(attempt, error);\n },\n );\n }\n\n #opened(attempt: number, socket: ClientSocket): void {\n if (attempt !== this.#attempt || this.#state !== \"connecting\") {\n this.#closeSocket(socket, undefined);\n return;\n }\n const owner = deferred();\n this.#closeOwner = owner;\n this.#socket = socket;\n // Waits in the request that opened the socket, which is the one place guaranteed to be allowed to close it.\n void owner.promise.then(\n () => this.#tryClose(socket),\n () => undefined,\n );\n socket.addEventListener(\"message\", (event) => {\n if (attempt !== this.#attempt || typeof event.data !== \"string\") return;\n this.#received(attempt, event.data);\n });\n socket.addEventListener(\"close\", () => {\n if (attempt === this.#attempt)\n this.#down(attempt, new RealtimeError(\"unavailable\", \"closed\"));\n });\n socket.addEventListener(\"error\", () => {\n if (attempt === this.#attempt) this.#down(attempt, new RealtimeError(\"unavailable\", \"error\"));\n });\n try {\n socket.accept();\n } catch (error) {\n this.#down(attempt, error);\n }\n }\n\n #tryClose(socket: ClientSocket): boolean {\n try {\n socket.close(1000, \"done\");\n return true;\n } catch {\n return false;\n }\n }\n\n /** Closes a socket from wherever this runs; when the runtime refuses, the request that opened it does it. */\n #closeSocket(socket: ClientSocket | undefined, owner: Deferred | undefined): void {\n if (socket === undefined) return;\n if (!this.#tryClose(socket)) owner?.resolve();\n }\n\n /** The socket failed or closed: schedule a new attempt, or end the link when nobody listens. */\n #down(attempt: number, error: unknown): void {\n if (attempt !== this.#attempt || this.#state === \"disposed\") return;\n this.#attempt += 1;\n this.#cancelConnect?.();\n this.#cancelConnect = undefined;\n this.#cancelKeep?.();\n this.#cancelKeep = undefined;\n const socket = this.#socket;\n const owner = this.#closeOwner;\n this.#socket = undefined;\n this.#closeOwner = undefined;\n this.#closeSocket(socket, owner);\n // Waiters keep the promise they hold until the link is open; only an opened link needs a new one.\n if (this.#state === \"open\") this.#ready = deferred();\n if (this.size === 0) {\n this.dispose(error);\n return;\n }\n this.#state = \"backoff\";\n this.#failures += 1;\n const ceiling = Math.min(\n this.#host.reconnectMaxMs,\n this.#host.reconnectMinMs * 2 ** Math.min(this.#failures - 1, 20),\n );\n const delay = Math.floor(this.#host.random() * ceiling);\n this.#retryAt = this.#host.now() + delay;\n this.#cancelRetry?.();\n this.#cancelRetry = this.#host.after(delay, () => this.#connect());\n }\n\n /** Reconnects now, without waiting: the object asked for it (a long replay is continued with a new socket). */\n #resume(attempt: number): void {\n if (attempt !== this.#attempt) return;\n this.#attempt += 1;\n const socket = this.#socket;\n const owner = this.#closeOwner;\n this.#socket = undefined;\n this.#closeOwner = undefined;\n this.#cancelKeep?.();\n this.#cancelKeep = undefined;\n this.#closeSocket(socket, owner);\n this.#state = \"idle\";\n this.#connect();\n }\n\n /**\n * A health check run by every listener's loop (each in its own request) and by the ping timer: whichever request is alive\n * can repair a link whose own request is gone, because a timer or a socket handler of a finished request never runs again.\n */\n #check(): void {\n const now = this.#host.now();\n const grace = Math.max(1000, this.#host.keepAliveMs);\n switch (this.#state) {\n case \"backoff\":\n if (now > this.#retryAt + grace) this.#connect();\n return;\n case \"connecting\":\n if (now > this.#connectedAt + this.#host.connectTimeoutMs * 2) this.#connect();\n return;\n case \"open\": {\n const keep = this.#host.keepAliveMs;\n if (keep > 0 && now - this.#lastReceived > keep * 3) {\n this.#down(\n this.#attempt,\n new RealtimeError(\"unavailable\", \"the channel object went silent\"),\n );\n return;\n }\n try {\n this.#socket?.send(\"ping\");\n } catch {\n // Sending from another request is refused; the owner's own ping covers it.\n }\n return;\n }\n default:\n return;\n }\n }\n\n // --- frames --------------------------------------------------------------------------------------------------------\n\n #received(attempt: number, text: string): void {\n this.#lastReceived = this.#host.now();\n if (text === \"pong\") return;\n const frame = decodeLinkFrame(text);\n if (frame === undefined) return;\n switch (frame.t) {\n case \"ready\": {\n if (this.#known && frame.head < this.#last) {\n // The channel's numbering restarted (it was forgotten): follow it.\n this.#last = frame.head;\n for (const sub of this.#messages) sub.last = Math.min(sub.last, frame.head);\n }\n if (!this.#known) {\n this.#last = frame.head;\n this.#known = true;\n }\n for (const sub of this.#messages) if (sub.last < 0) sub.last = frame.head;\n this.#state = \"open\";\n this.#failures = 0;\n this.#cancelConnect?.();\n this.#cancelConnect = undefined;\n this.#lastReceived = this.#host.now();\n if (this.#host.keepAliveMs > 0) {\n this.#cancelKeep?.();\n this.#cancelKeep = this.#host.every(this.#host.keepAliveMs, () => this.#check());\n }\n this.#ready.resolve();\n return;\n }\n case \"snap\": {\n const next = new Map<string, Member>();\n for (const member of frame.members) next.set(member.id, member);\n const before = this.#members;\n this.#members = next;\n if (before === undefined) return;\n for (const [id, member] of before)\n if (!next.has(id)) this.#fanPresence({ channel: this.channel, op: \"leave\", member });\n for (const [id, member] of next) {\n const old = before.get(id);\n if (old === undefined) this.#fanPresence({ channel: this.channel, op: \"join\", member });\n else if (member.v > old.v)\n this.#fanPresence({ channel: this.channel, op: \"set\", member });\n }\n return;\n }\n case \"msg\": {\n const message = frame.m;\n if (message.seq > this.#last) this.#last = message.seq;\n for (const sub of this.#messages)\n if (message.seq > sub.last) {\n sub.last = message.seq;\n sub.pump.push(message);\n }\n return;\n }\n case \"pres\": {\n const members = this.#members ?? new Map<string, Member>();\n this.#members = members;\n const known = members.get(frame.member.id);\n if (frame.op === \"leave\") {\n if (known === undefined) return;\n members.delete(frame.member.id);\n } else {\n if (known !== undefined && known.v >= frame.member.v && frame.op === \"join\") return;\n if (known !== undefined && known.v > frame.member.v) return;\n members.set(frame.member.id, frame.member);\n }\n this.#fanPresence({ channel: this.channel, op: frame.op, member: frame.member });\n return;\n }\n case \"more\":\n this.#resume(attempt);\n return;\n case \"bye\":\n this.#down(attempt, new RealtimeError(\"unavailable\", frame.why));\n return;\n }\n }\n\n #fanPresence(event: PresenceEvent): void {\n for (const sub of this.#presence) sub.pump.push(event);\n }\n\n // --- ending --------------------------------------------------------------------------------------------------------\n\n /** Ends the link: stops every listener, cancels timers and closes the socket. */\n dispose(reason: unknown): void {\n if (this.#state === \"disposed\") return;\n this.#state = \"disposed\";\n this.#attempt += 1;\n this.#cancelRetry?.();\n this.#cancelConnect?.();\n this.#cancelKeep?.();\n this.#cancelRetry = undefined;\n this.#cancelConnect = undefined;\n this.#cancelKeep = undefined;\n const socket = this.#socket;\n const owner = this.#closeOwner;\n this.#socket = undefined;\n this.#closeOwner = undefined;\n this.#closeSocket(socket, owner);\n this.#ready.reject(reason);\n for (const sub of this.#messages) sub.pump.stop();\n for (const sub of this.#presence) sub.pump.stop();\n this.#messages.clear();\n this.#presence.clear();\n this.#host.forget(this);\n }\n}\n"],"mappings":";;;;;;;;;;;;AA0CA,SAAS,WAAqB;CAC5B,IAAI,gBAA4B,KAAA;CAChC,IAAI,eAAyC,KAAA;CAC7C,MAAM,UAAU,IAAI,SAAe,WAAW,aAAa;EACzD,UAAU;EACV,SAAS;CACX,CAAC;CAED,QAAQ,YAAY,KAAA,CAAS;CAC7B,OAAO;EAAE;EAAS;EAAS;CAAO;AACpC;;AAGA,IAAM,OAAN,MAAc;CACZ,SAAuB,CAAC;CACxB;CACA;CACA;CACA,WAAW;;;;;;CAOX,YAAY,KAAa,QAAoB;EAC3C,KAAK,OAAO;EACZ,KAAK,UAAU;CACjB;CAEA,KAAK,MAAe;EAClB,IAAI,KAAK,UAAU;EACnB,IAAI,KAAK,OAAO,UAAU,KAAK,MAAM;GACnC,KAAK,QAAQ;GACb;EACF;EACA,KAAK,OAAO,KAAK,IAAI;EACrB,MAAM,OAAO,KAAK;EAClB,KAAK,QAAQ,KAAA;EACb,OAAO;CACT;CAEA,OAAa;EACX,KAAK,WAAW;EAChB,KAAK,OAAO,SAAS;EACrB,MAAM,OAAO,KAAK;EAClB,KAAK,QAAQ,KAAA;EACb,OAAO;CACT;CAEA,MAAM,IACJ,SACA,QACA,OACA,MACe;EACf,MAAM,SAAS,QAAQ;EACvB,IAAI;GACF,OAAO,CAAC,KAAK,UAAU;IACrB,MAAM,OAAO,KAAK,OAAO,MAAM;IAC/B,IAAI,SAAS,KAAA,GAAW;KAEtB,OAAO;KACP,IAAI,KAAK,OAAO,SAAS,GAAG;KAC5B,MAAM,IAAI,SAAe,YAAY;MACnC,KAAK,QAAQ;KACf,CAAC;KACD;IACF;IACA,IAAI;KACF,QAAQ,IAAI;IACd,SAAS,OAAO;KACd,OAAO,KAAK;IACd;GACF;EACF,UAAU;GACR,SAAS;EACX;CACF;AACF;;AA0BA,IAAa,cAAb,MAAyB;CACvB;CACA;CACA,4BAAqB,IAAI,IAAgB;CACzC,4BAAqB,IAAI,IAAiB;CAC1C,SAAoB;CACpB;CACA;CACA,WAAW;;CAEX,QAAQ;CACR,SAAS;CACT;CACA,gBAAgB;CAChB,YAAY;CACZ,WAAW;CACX,eAAe;CACf,SAAmB,SAAS;CAC5B;CACA;CACA;CAEA,YAAY,SAAiB,MAAgB;EAC3C,KAAK,UAAU;EACf,KAAK,QAAQ;CACf;CAEA,IAAI,QAAmB;EACrB,OAAO,KAAK;CACd;CAEA,IAAI,OAAe;EACjB,OAAO,KAAK,UAAU,OAAO,KAAK,UAAU;CAC9C;;CAKA,YAAY,UAA8D;EACxE,MAAM,MAAkB;GACtB;GACA,MAAM,IAAI,KAAK,KAAK,MAAM,iBAAiB,KAAK,SAAS,GAAG,CAAC;GAC7D,MAAM,KAAK,WAAW,SAAS,KAAK,QAAQ;GAC5C,YAAY;EACd;EACA,KAAK,UAAU,IAAI,GAAG;EAEtB,IAAS,KAAK,IACZ,WACC,UAAU,KAAK,MAAM,OAAO,OAAO,YAAY,KAAK,OAAO,GAC5D,KAAK,QAAQ,SACP;GACJ,IAAI,aAAa;EACnB,CACF;EACA,KAAK,OAAO;EACZ,OAAO;CACT;;CAGA,YAAY,UAA4D;EACtE,MAAM,MAAmB;GACvB;GACA,MAAM,IAAI,KAAK,KAAK,MAAM,iBAAiB,KAAK,SAAS,GAAG,CAAC;GAC7D,sBAAM,IAAI,IAAI;GACd,OAAO;EACT;EACA,KAAK,UAAU,IAAI,GAAG;EACtB,IAAS,KAAK,KACX,UAAU;GAET,IAAI,MAAM,OAAO,SAAS,IAAI,KAAK,OAAO,MAAM,OAAO,EAAE;QACpD,IAAI,KAAK,IAAI,MAAM,OAAO,IAAI,MAAM,MAAM;GAC/C,SAAS,KAAK;EAChB,IACC,UAAU,KAAK,MAAM,OAAO,OAAO,YAAY,KAAK,OAAO,GAC5D,KAAK,QAAQ,SACP,KAAK,QAAQ,GAAG,CACxB;EACA,KAAK,OAAO;EACZ,OAAO;CACT;;CAGA,SAAS,KAAqC;EAC5C,IAAI,UAAU,KAAK;GACjB,IAAI,IAAI,OAAO;GACf,IAAI,QAAQ;EACd,OAAO;GACL,IAAI,IAAI,YAAY;GACpB,IAAI,aAAa;EACnB;EACA,KAAK,MAAM,OACT,IAAI,cACF,SACA,mEACF,GACA,SACA,KAAK,OACP;CACF;;;;;;CAOA,QAAQ,KAAwB;EAC9B,IAAI,CAAC,IAAI,OAAO;EAChB,IAAI,QAAQ;EACZ,MAAM,UAAU,KAAK,4BAAY,IAAI,IAAoB;EACzD,KAAK,MAAM,CAAC,IAAI,WAAW,IAAI,MAC7B,IAAI,CAAC,QAAQ,IAAI,EAAE,GAAG,IAAI,KAAK,KAAK;GAAE,SAAS,KAAK;GAAS,IAAI;GAAS;EAAO,CAAC;EACpF,KAAK,MAAM,CAAC,IAAI,WAAW,SAAS;GAClC,MAAM,OAAO,IAAI,KAAK,IAAI,EAAE;GAC5B,IAAI,SAAS,KAAA,GAAW,IAAI,KAAK,KAAK;IAAE,SAAS,KAAK;IAAS,IAAI;IAAQ;GAAO,CAAC;QAC9E,IAAI,OAAO,IAAI,KAAK,GAAG,IAAI,KAAK,KAAK;IAAE,SAAS,KAAK;IAAS,IAAI;IAAO;GAAO,CAAC;EACxF;CACF;;CAGA,OAAO,KAA6B;EAClC,IAAI,KAAK,KAAK;EACd,KAAK,UAAU,OAAO,GAAiB;EACvC,KAAK,UAAU,OAAO,GAAkB;EACxC,IAAI,KAAK,SAAS,GAAG,KAAK,QAAQ,IAAI,cAAc,UAAU,wBAAwB,CAAC;CACzF;;CAGA,UAAU,IAA2B;EACnC,IAAI,KAAK,WAAW,QAAQ,OAAO,QAAQ,QAAQ;EACnD,OAAO,IAAI,SAAe,SAAS,WAAW;GAC5C,MAAM,SAAS,KAAK,MAAM,MAAM,UAC9B,OAAO,IAAI,cAAc,eAAe,4CAA4C,CAAC,CACvF;GACA,KAAK,OAAO,QAAQ,WACZ;IACJ,OAAO;IACP,QAAQ;GACV,IACC,UAAmB;IAClB,OAAO;IACP,OAAO,KAAK;GACd,CACF;EACF,CAAC;CACH;;CAKA,UAA0C;EACxC,MAAM,QAAQ,KAAK,MAAM;EACzB,IAAI,SAAS,GAAG,OAAO,KAAA;EACvB,aAAa,KAAK,MAAM,MAAM,aAAa,KAAK,OAAO,CAAC;CAC1D;CAEA,SAAe;EACb,IAAI,KAAK,WAAW,QAAQ,KAAK,SAAS;CAC5C;CAEA,WAAiB;EACf,IAAI,KAAK,WAAW,YAAY;EAChC,KAAK,eAAe;EACpB,KAAK,eAAe,KAAA;EACpB,KAAK,iBAAiB;EACtB,KAAK,SAAS;EACd,MAAM,UAAU,EAAE,KAAK;EACvB,KAAK,eAAe,KAAK,MAAM,IAAI;EACnC,KAAK,iBAAiB,KAAK,MAAM,MAAM,KAAK,MAAM,wBAAwB;GACxE,IAAI,YAAY,KAAK,YAAY,KAAK,WAAW,cAC/C,KAAK,MAAM,SAAS,IAAI,cAAc,eAAe,mCAAmC,CAAC;EAC7F,CAAC;EACD,KAAK,MAAM,KAAK,KAAK,SAAS,KAAK,SAAS,KAAK,QAAQ,KAAA,CAAS,CAAC,CAAC,MACjE,WAAW,KAAK,QAAQ,SAAS,MAAM,IACvC,UAAmB;GAClB,IAAI,YAAY,KAAK,UAAU,KAAK,MAAM,SAAS,KAAK;EAC1D,CACF;CACF;CAEA,QAAQ,SAAiB,QAA4B;EACnD,IAAI,YAAY,KAAK,YAAY,KAAK,WAAW,cAAc;GAC7D,KAAK,aAAa,QAAQ,KAAA,CAAS;GACnC;EACF;EACA,MAAM,QAAQ,SAAS;EACvB,KAAK,cAAc;EACnB,KAAK,UAAU;EAEf,MAAW,QAAQ,WACX,KAAK,UAAU,MAAM,SACrB,KAAA,CACR;EACA,OAAO,iBAAiB,YAAY,UAAU;GAC5C,IAAI,YAAY,KAAK,YAAY,OAAO,MAAM,SAAS,UAAU;GACjE,KAAK,UAAU,SAAS,MAAM,IAAI;EACpC,CAAC;EACD,OAAO,iBAAiB,eAAe;GACrC,IAAI,YAAY,KAAK,UACnB,KAAK,MAAM,SAAS,IAAI,cAAc,eAAe,QAAQ,CAAC;EAClE,CAAC;EACD,OAAO,iBAAiB,eAAe;GACrC,IAAI,YAAY,KAAK,UAAU,KAAK,MAAM,SAAS,IAAI,cAAc,eAAe,OAAO,CAAC;EAC9F,CAAC;EACD,IAAI;GACF,OAAO,OAAO;EAChB,SAAS,OAAO;GACd,KAAK,MAAM,SAAS,KAAK;EAC3B;CACF;CAEA,UAAU,QAA+B;EACvC,IAAI;GACF,OAAO,MAAM,KAAM,MAAM;GACzB,OAAO;EACT,QAAQ;GACN,OAAO;EACT;CACF;;CAGA,aAAa,QAAkC,OAAmC;EAChF,IAAI,WAAW,KAAA,GAAW;EAC1B,IAAI,CAAC,KAAK,UAAU,MAAM,GAAG,OAAO,QAAQ;CAC9C;;CAGA,MAAM,SAAiB,OAAsB;EAC3C,IAAI,YAAY,KAAK,YAAY,KAAK,WAAW,YAAY;EAC7D,KAAK,YAAY;EACjB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB,KAAA;EACtB,KAAK,cAAc;EACnB,KAAK,cAAc,KAAA;EACnB,MAAM,SAAS,KAAK;EACpB,MAAM,QAAQ,KAAK;EACnB,KAAK,UAAU,KAAA;EACf,KAAK,cAAc,KAAA;EACnB,KAAK,aAAa,QAAQ,KAAK;EAE/B,IAAI,KAAK,WAAW,QAAQ,KAAK,SAAS,SAAS;EACnD,IAAI,KAAK,SAAS,GAAG;GACnB,KAAK,QAAQ,KAAK;GAClB;EACF;EACA,KAAK,SAAS;EACd,KAAK,aAAa;EAClB,MAAM,UAAU,KAAK,IACnB,KAAK,MAAM,gBACX,KAAK,MAAM,iBAAiB,KAAK,KAAK,IAAI,KAAK,YAAY,GAAG,EAAE,CAClE;EACA,MAAM,QAAQ,KAAK,MAAM,KAAK,MAAM,OAAO,IAAI,OAAO;EACtD,KAAK,WAAW,KAAK,MAAM,IAAI,IAAI;EACnC,KAAK,eAAe;EACpB,KAAK,eAAe,KAAK,MAAM,MAAM,aAAa,KAAK,SAAS,CAAC;CACnE;;CAGA,QAAQ,SAAuB;EAC7B,IAAI,YAAY,KAAK,UAAU;EAC/B,KAAK,YAAY;EACjB,MAAM,SAAS,KAAK;EACpB,MAAM,QAAQ,KAAK;EACnB,KAAK,UAAU,KAAA;EACf,KAAK,cAAc,KAAA;EACnB,KAAK,cAAc;EACnB,KAAK,cAAc,KAAA;EACnB,KAAK,aAAa,QAAQ,KAAK;EAC/B,KAAK,SAAS;EACd,KAAK,SAAS;CAChB;;;;;CAMA,SAAe;EACb,MAAM,MAAM,KAAK,MAAM,IAAI;EAC3B,MAAM,QAAQ,KAAK,IAAI,KAAM,KAAK,MAAM,WAAW;EACnD,QAAQ,KAAK,QAAb;GACE,KAAK;IACH,IAAI,MAAM,KAAK,WAAW,OAAO,KAAK,SAAS;IAC/C;GACF,KAAK;IACH,IAAI,MAAM,KAAK,eAAe,KAAK,MAAM,mBAAmB,GAAG,KAAK,SAAS;IAC7E;GACF,KAAK,QAAQ;IACX,MAAM,OAAO,KAAK,MAAM;IACxB,IAAI,OAAO,KAAK,MAAM,KAAK,gBAAgB,OAAO,GAAG;KACnD,KAAK,MACH,KAAK,UACL,IAAI,cAAc,eAAe,gCAAgC,CACnE;KACA;IACF;IACA,IAAI;KACF,KAAK,SAAS,KAAK,MAAM;IAC3B,QAAQ,CAER;IACA;GACF;GACA,SACE;EACJ;CACF;CAIA,UAAU,SAAiB,MAAoB;EAC7C,KAAK,gBAAgB,KAAK,MAAM,IAAI;EACpC,IAAI,SAAS,QAAQ;EACrB,MAAM,QAAQ,gBAAgB,IAAI;EAClC,IAAI,UAAU,KAAA,GAAW;EACzB,QAAQ,MAAM,GAAd;GACE,KAAK;IACH,IAAI,KAAK,UAAU,MAAM,OAAO,KAAK,OAAO;KAE1C,KAAK,QAAQ,MAAM;KACnB,KAAK,MAAM,OAAO,KAAK,WAAW,IAAI,OAAO,KAAK,IAAI,IAAI,MAAM,MAAM,IAAI;IAC5E;IACA,IAAI,CAAC,KAAK,QAAQ;KAChB,KAAK,QAAQ,MAAM;KACnB,KAAK,SAAS;IAChB;IACA,KAAK,MAAM,OAAO,KAAK,WAAW,IAAI,IAAI,OAAO,GAAG,IAAI,OAAO,MAAM;IACrE,KAAK,SAAS;IACd,KAAK,YAAY;IACjB,KAAK,iBAAiB;IACtB,KAAK,iBAAiB,KAAA;IACtB,KAAK,gBAAgB,KAAK,MAAM,IAAI;IACpC,IAAI,KAAK,MAAM,cAAc,GAAG;KAC9B,KAAK,cAAc;KACnB,KAAK,cAAc,KAAK,MAAM,MAAM,KAAK,MAAM,mBAAmB,KAAK,OAAO,CAAC;IACjF;IACA,KAAK,OAAO,QAAQ;IACpB;GAEF,KAAK,QAAQ;IACX,MAAM,uBAAO,IAAI,IAAoB;IACrC,KAAK,MAAM,UAAU,MAAM,SAAS,KAAK,IAAI,OAAO,IAAI,MAAM;IAC9D,MAAM,SAAS,KAAK;IACpB,KAAK,WAAW;IAChB,IAAI,WAAW,KAAA,GAAW;IAC1B,KAAK,MAAM,CAAC,IAAI,WAAW,QACzB,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,KAAK,aAAa;KAAE,SAAS,KAAK;KAAS,IAAI;KAAS;IAAO,CAAC;IACrF,KAAK,MAAM,CAAC,IAAI,WAAW,MAAM;KAC/B,MAAM,MAAM,OAAO,IAAI,EAAE;KACzB,IAAI,QAAQ,KAAA,GAAW,KAAK,aAAa;MAAE,SAAS,KAAK;MAAS,IAAI;MAAQ;KAAO,CAAC;UACjF,IAAI,OAAO,IAAI,IAAI,GACtB,KAAK,aAAa;MAAE,SAAS,KAAK;MAAS,IAAI;MAAO;KAAO,CAAC;IAClE;IACA;GACF;GACA,KAAK,OAAO;IACV,MAAM,UAAU,MAAM;IACtB,IAAI,QAAQ,MAAM,KAAK,OAAO,KAAK,QAAQ,QAAQ;IACnD,KAAK,MAAM,OAAO,KAAK,WACrB,IAAI,QAAQ,MAAM,IAAI,MAAM;KAC1B,IAAI,OAAO,QAAQ;KACnB,IAAI,KAAK,KAAK,OAAO;IACvB;IACF;GACF;GACA,KAAK,QAAQ;IACX,MAAM,UAAU,KAAK,4BAAY,IAAI,IAAoB;IACzD,KAAK,WAAW;IAChB,MAAM,QAAQ,QAAQ,IAAI,MAAM,OAAO,EAAE;IACzC,IAAI,MAAM,OAAO,SAAS;KACxB,IAAI,UAAU,KAAA,GAAW;KACzB,QAAQ,OAAO,MAAM,OAAO,EAAE;IAChC,OAAO;KACL,IAAI,UAAU,KAAA,KAAa,MAAM,KAAK,MAAM,OAAO,KAAK,MAAM,OAAO,QAAQ;KAC7E,IAAI,UAAU,KAAA,KAAa,MAAM,IAAI,MAAM,OAAO,GAAG;KACrD,QAAQ,IAAI,MAAM,OAAO,IAAI,MAAM,MAAM;IAC3C;IACA,KAAK,aAAa;KAAE,SAAS,KAAK;KAAS,IAAI,MAAM;KAAI,QAAQ,MAAM;IAAO,CAAC;IAC/E;GACF;GACA,KAAK;IACH,KAAK,QAAQ,OAAO;IACpB;GACF,KAAK;IACH,KAAK,MAAM,SAAS,IAAI,cAAc,eAAe,MAAM,GAAG,CAAC;IAC/D;EACJ;CACF;CAEA,aAAa,OAA4B;EACvC,KAAK,MAAM,OAAO,KAAK,WAAW,IAAI,KAAK,KAAK,KAAK;CACvD;;CAKA,QAAQ,QAAuB;EAC7B,IAAI,KAAK,WAAW,YAAY;EAChC,KAAK,SAAS;EACd,KAAK,YAAY;EACjB,KAAK,eAAe;EACpB,KAAK,iBAAiB;EACtB,KAAK,cAAc;EACnB,KAAK,eAAe,KAAA;EACpB,KAAK,iBAAiB,KAAA;EACtB,KAAK,cAAc,KAAA;EACnB,MAAM,SAAS,KAAK;EACpB,MAAM,QAAQ,KAAK;EACnB,KAAK,UAAU,KAAA;EACf,KAAK,cAAc,KAAA;EACnB,KAAK,aAAa,QAAQ,KAAK;EAC/B,KAAK,OAAO,OAAO,MAAM;EACzB,KAAK,MAAM,OAAO,KAAK,WAAW,IAAI,KAAK,KAAK;EAChD,KAAK,MAAM,OAAO,KAAK,WAAW,IAAI,KAAK,KAAK;EAChD,KAAK,UAAU,MAAM;EACrB,KAAK,UAAU,MAAM;EACrB,KAAK,MAAM,OAAO,IAAI;CACxB;AACF"}
@@ -1,5 +1,4 @@
1
1
  import { ChannelObjectState, ChannelSocket } from "./types.js";
2
- //#region src/cloudflare/object.d.ts
3
2
  /**
4
3
  * The pieces of the platform the object creates: a WebSocket pair, the upgrade response and the auto-response pair. The default
5
4
  * reads the Workers globals; the unit tests pass their own.
@@ -78,5 +77,4 @@ export declare class RealtimeChannelObject {
78
77
  */
79
78
  alarm(): Promise<void>;
80
79
  }
81
- //#endregion
82
80
  //# sourceMappingURL=object.d.ts.map
@@ -2,7 +2,7 @@ import { RealtimeError } from "../errors.js";
2
2
  import { isChannelName } from "../protocol.js";
3
3
  import { isOperation, parseRequest } from "./wire.js";
4
4
  import { ChannelCore } from "./core.js";
5
- //#region src/cloudflare/object.ts
5
+ // src/cloudflare/object.ts
6
6
  function workersPlatform() {
7
7
  return {
8
8
  createPair() {
@@ -208,7 +208,6 @@ var RealtimeChannelObject = class {
208
208
  await this.#core.alarm();
209
209
  }
210
210
  };
211
- //#endregion
212
211
  export { RealtimeChannelObject };
213
212
 
214
213
  //# sourceMappingURL=object.js.map
@@ -1,4 +1,3 @@
1
- //#region src/cloudflare/types.d.ts
2
1
  /**
3
2
  * The few shapes of the Cloudflare Workers runtime the Durable Object adapter touches. They are declared here, structurally, so
4
3
  * the package has no dependency on `@cloudflare/workers-types`: the real `DurableObjectNamespace`, `DurableObjectState` and
@@ -96,5 +95,4 @@ export interface ChannelNamespace<Id = unknown> {
96
95
  readonly locationHint?: string;
97
96
  }): ChannelStub;
98
97
  }
99
- //#endregion
100
98
  //# sourceMappingURL=types.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { byteLength, isChannelName, isEventName, isIdempotencyKey } from "../protocol.js";
2
- //#region src/cloudflare/wire.ts
2
+ // src/cloudflare/wire.ts
3
3
  /** The most bytes of one message (data, event and names) the object stores in one value; the platform's smallest value limit is 128 KiB. @internal */
4
4
  const MAX_ENTRY_BYTES = 122880;
5
5
  /** The most bytes of one member's state. @internal */
@@ -278,7 +278,6 @@ function historyKey(seq) {
278
278
  function seqOfKey(key) {
279
279
  return Number(key.slice(2));
280
280
  }
281
- //#endregion
282
281
  export { MAX_BODY_BYTES, MAX_ENTRY_BYTES, MAX_HISTORY, MAX_MEMBERS, MAX_STATE_BYTES, MAX_TTL_MS, decodeLinkFrame, historyKey, isOperation, isWholeNumber, parseMember, parseRequest, parseStored, seqOfKey };
283
282
 
284
283
  //# sourceMappingURL=wire.js.map
@@ -1,4 +1,3 @@
1
- //#region src/errors.d.ts
2
1
  /**
3
2
  * The codes an error of this package carries. They are the same strings the wire protocol uses in `err` frames.
4
3
  * @since 1.0.0
@@ -21,5 +20,4 @@ export declare class RealtimeError extends Error {
21
20
  readonly cause?: unknown;
22
21
  });
23
22
  }
24
- //#endregion
25
23
  //# sourceMappingURL=errors.d.ts.map
@@ -1,4 +1,4 @@
1
- //#region src/errors.ts
1
+ // src/errors.ts
2
2
  /**
3
3
  * An error with a protocol code. A request handler that throws one answers the client with its code and message; any
4
4
  * other error answers `handler_error` with no message, so internals never reach a client.
@@ -18,7 +18,6 @@ var RealtimeError = class extends Error {
18
18
  this.code = code;
19
19
  }
20
20
  };
21
- //#endregion
22
21
  export { RealtimeError };
23
22
 
24
23
  //# sourceMappingURL=errors.js.map
@@ -1,7 +1,6 @@
1
1
  import { Member } from "./protocol.js";
2
2
  import { AnyMessage, Channel, ChannelDef, ChannelOptions, ChannelSchema, ChannelStatus, MessageMeta, PresenceOf, Realtime, RequestData, RequestReply, RequestsOf, Status } from "./client/types.js";
3
3
  import { Accessor } from "@shadow-js/core";
4
- //#region src/hooks.d.ts
5
4
  /**
6
5
  * What {@link useChannel} returns: the reactive state of a channel and the calls on it.
7
6
  * @since 1.0.0
@@ -106,5 +105,4 @@ export declare function useChannelPresence<Def extends ChannelDef>(channel: Chan
106
105
  * @since 1.0.0
107
106
  */
108
107
  export declare function useRealtimeStatus<Schema extends ChannelSchema>(rt: Realtime<Schema>): Accessor<Status>;
109
- //#endregion
110
108
  //# sourceMappingURL=hooks.d.ts.map
package/dist/dev/hooks.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { RealtimeError } from "./errors.js";
2
2
  import { isServer, onCleanup, useSignal } from "@shadow-js/core";
3
- //#region src/hooks.ts
3
+ // src/hooks.ts
4
4
  /** A channel for the server render: it never connects, listens or publishes. */
5
5
  function inertChannel(name) {
6
6
  const refuse = () => Promise.reject(new RealtimeError("closed", "a realtime channel is not available during the server render"));
@@ -156,7 +156,6 @@ function useRealtimeStatus(rt) {
156
156
  if (!isServer) onCleanup(rt.onStatus((next) => setStatus(next)));
157
157
  return status;
158
158
  }
159
- //#endregion
160
159
  export { useChannel, useChannelEvent, useChannelPresence, useRealtimeStatus };
161
160
 
162
161
  //# sourceMappingURL=hooks.js.map
@@ -1,5 +1,4 @@
1
1
  import { Adapter } from "./adapter.js";
2
- //#region src/memory.d.ts
3
2
  /**
4
3
  * Options of {@link memoryAdapter}.
5
4
  * @since 1.0.0
@@ -12,6 +11,17 @@ export interface MemoryAdapterOptions {
12
11
  * numbering restarts, which a resuming client sees as `reset`. Default: one hour.
13
12
  */
14
13
  readonly idleMs?: number;
14
+ /**
15
+ * Receives a listener's failure (a message or presence listener that threw). Delivery to the other listeners goes on either
16
+ * way. `createHub` passes its own `onError` here. Default: `console.error`, so a bug in a listener is never silent.
17
+ */
18
+ readonly onError?: (error: unknown, context: MemoryErrorContext) => void;
19
+ }
20
+ /** Where an error reported by the memory adapter came from. @since 1.0.0 */
21
+ export interface MemoryErrorContext {
22
+ /** `"listener"` (a message listener) or `"presenceListener"`. */
23
+ readonly where: "listener" | "presenceListener";
24
+ readonly channel: string;
15
25
  }
16
26
  /**
17
27
  * The single-process adapter, and the default of `createHub`. Messages, history, idempotency keys and presence live in memory
@@ -29,5 +39,4 @@ export interface MemoryAdapterOptions {
29
39
  * @since 1.0.0
30
40
  */
31
41
  export declare function memoryAdapter(options?: MemoryAdapterOptions): Adapter;
32
- //#endregion
33
42
  //# sourceMappingURL=memory.d.ts.map
@@ -1,4 +1,7 @@
1
- //#region src/memory.ts
1
+ // src/memory.ts
2
+ /** The most idempotency keys one channel remembers; past it the oldest are forgotten, which only lets a very old resend through. */
3
+ const MAX_KEYS = 65536;
4
+ const KEYS_SWEEP_FROM = 4096;
2
5
  /**
3
6
  * The single-process adapter, and the default of `createHub`. Messages, history, idempotency keys and presence live in memory
4
7
  * of this process; several hubs created over one adapter object share them, which is how tests exercise two servers. Use the Redis
@@ -17,6 +20,14 @@
17
20
  function memoryAdapter(options = {}) {
18
21
  const now = options.now ?? Date.now;
19
22
  const idleMs = options.idleMs ?? 36e5;
23
+ const report = (error, context) => {
24
+ try {
25
+ if (options.onError === void 0) console.error("[realtime] memory adapter", context, error);
26
+ else options.onError(error, context);
27
+ } catch (reporter) {
28
+ console.error("[realtime] memory adapter onError", reporter);
29
+ }
30
+ };
20
31
  let sweepAt = 1024;
21
32
  const channels = /* @__PURE__ */ new Map();
22
33
  const presence = /* @__PURE__ */ new Map();
@@ -42,6 +53,7 @@ function memoryAdapter(options = {}) {
42
53
  seq: 0,
43
54
  log: [],
44
55
  keys: /* @__PURE__ */ new Map(),
56
+ keysSweepAt: KEYS_SWEEP_FROM,
45
57
  listeners: /* @__PURE__ */ new Set(),
46
58
  pending: [],
47
59
  draining: false
@@ -51,14 +63,23 @@ function memoryAdapter(options = {}) {
51
63
  state.lastActive = now();
52
64
  return state;
53
65
  }
66
+ /** Calls each listener with `value`. A listener's failure is its own: delivery to the others goes on (guarantee G13). */
67
+ function deliver(listeners, value, context) {
68
+ for (const listener of [...listeners]) try {
69
+ listener(value);
70
+ } catch (error) {
71
+ report(error, context);
72
+ }
73
+ }
54
74
  /** Delivers queued messages in order; a listener that publishes in turn queues behind the message being delivered. */
55
- function drain(state) {
75
+ function drain(state, channel) {
56
76
  if (state.draining) return;
57
77
  state.draining = true;
58
78
  try {
59
- for (let next = state.pending.shift(); next !== void 0; next = state.pending.shift()) for (const listener of [...state.listeners]) try {
60
- listener(next);
61
- } catch {}
79
+ for (let next = state.pending.shift(); next !== void 0; next = state.pending.shift()) deliver(state.listeners, next, {
80
+ where: "listener",
81
+ channel
82
+ });
62
83
  } finally {
63
84
  state.draining = false;
64
85
  }
@@ -68,9 +89,10 @@ function memoryAdapter(options = {}) {
68
89
  if (presenceDraining) return;
69
90
  presenceDraining = true;
70
91
  try {
71
- for (let next = presenceQueue.shift(); next !== void 0; next = presenceQueue.shift()) for (const listener of [...presenceListeners.get(next.channel) ?? []]) try {
72
- listener(next);
73
- } catch {}
92
+ for (let next = presenceQueue.shift(); next !== void 0; next = presenceQueue.shift()) deliver(presenceListeners.get(next.channel) ?? [], next, {
93
+ where: "presenceListener",
94
+ channel: next.channel
95
+ });
74
96
  } finally {
75
97
  presenceDraining = false;
76
98
  }
@@ -103,8 +125,14 @@ function memoryAdapter(options = {}) {
103
125
  seq: known.seq,
104
126
  duplicate: true
105
127
  };
106
- if (state.keys.size >= 4096) {
107
- for (const [key, value] of state.keys) if (value.expires <= at) state.keys.delete(key);
128
+ const { keys } = state;
129
+ if (keys.size >= state.keysSweepAt) {
130
+ for (const [key, value] of keys) if (value.expires <= at) keys.delete(key);
131
+ state.keysSweepAt = Math.max(KEYS_SWEEP_FROM, keys.size * 2);
132
+ }
133
+ for (const key of keys.keys()) {
134
+ if (keys.size < MAX_KEYS) break;
135
+ keys.delete(key);
108
136
  }
109
137
  }
110
138
  state.seq += 1;
@@ -122,7 +150,7 @@ function memoryAdapter(options = {}) {
122
150
  trim(state, publish.history, publish.historyTtlMs);
123
151
  } else state.log = [];
124
152
  state.pending.push(message);
125
- drain(state);
153
+ drain(state, channel);
126
154
  return {
127
155
  seq: message.seq,
128
156
  duplicate: false
@@ -249,7 +277,6 @@ function memoryAdapter(options = {}) {
249
277
  }
250
278
  };
251
279
  }
252
- //#endregion
253
280
  export { memoryAdapter };
254
281
 
255
282
  //# sourceMappingURL=memory.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"memory.js","names":[],"sources":["../../src/memory.ts"],"sourcesContent":["import type {\n Adapter,\n HistoryResult,\n PresenceRef,\n PublishOptions,\n PublishResult,\n Unsubscribe,\n} from \"./adapter\";\nimport type { Member, PresenceEvent, StoredEntry, StoredMessage } from \"./protocol\";\n\ninterface ChannelState {\n seq: number;\n log: StoredMessage[];\n readonly keys: Map<string, { readonly seq: number; readonly expires: number }>;\n readonly listeners: Set<(message: StoredMessage) => void>;\n readonly pending: StoredMessage[];\n draining: boolean;\n lastActive: number;\n}\n\ninterface PresenceEntry {\n member: Member;\n expires: number;\n}\n\n/**\n * Options of {@link memoryAdapter}.\n * @since 1.0.0\n */\nexport interface MemoryAdapterOptions {\n /** The clock, in epoch milliseconds. Default: `Date.now`. */\n readonly now?: () => number;\n /**\n * Milliseconds a channel with no listener and no retained message is remembered. After that its head is forgotten and its\n * numbering restarts, which a resuming client sees as `reset`. Default: one hour.\n */\n readonly idleMs?: number;\n}\n\n/**\n * The single-process adapter, and the default of `createHub`. Messages, history, idempotency keys and presence live in memory\n * of this process; several hubs created over one adapter object share them, which is how tests exercise two servers. Use the Redis\n * or Cloudflare adapter when more than one process serves clients.\n *\n * @param options The clock.\n * @returns The adapter.\n * @example\n * import { createHub, memoryAdapter } from \"@shadow-js/realtime/server\";\n *\n * const adapter = memoryAdapter();\n * const hub = createHub({ adapter });\n * void hub.close();\n * @since 1.0.0\n */\nexport function memoryAdapter(options: MemoryAdapterOptions = {}): Adapter {\n const now = options.now ?? Date.now;\n const idleMs = options.idleMs ?? 3_600_000;\n let sweepAt = 1024;\n const channels = new Map<string, ChannelState>();\n const presence = new Map<string, Map<string, PresenceEntry>>();\n const presenceListeners = new Map<string, Set<(event: PresenceEvent) => void>>();\n const presenceQueue: PresenceEvent[] = [];\n let presenceDraining = false;\n let closed = false;\n\n /** Forgets channels nobody listens to and nothing retains; runs when the table has doubled since the last sweep. */\n function sweep(): void {\n const cutoff = now() - idleMs;\n for (const [name, state] of channels)\n if (state.listeners.size === 0 && state.lastActive < cutoff) {\n trim(state, state.log.length, idleMs);\n if (state.log.length === 0) channels.delete(name);\n }\n sweepAt = Math.max(1024, channels.size * 2);\n }\n\n function channelOf(name: string): ChannelState {\n let state = channels.get(name);\n if (state === undefined) {\n if (channels.size >= sweepAt) sweep();\n state = {\n lastActive: now(),\n seq: 0,\n log: [],\n keys: new Map(),\n listeners: new Set(),\n pending: [],\n draining: false,\n };\n channels.set(name, state);\n }\n state.lastActive = now();\n return state;\n }\n\n /** Delivers queued messages in order; a listener that publishes in turn queues behind the message being delivered. */\n function drain(state: ChannelState): void {\n if (state.draining) return;\n state.draining = true;\n try {\n for (let next = state.pending.shift(); next !== undefined; next = state.pending.shift()) {\n for (const listener of [...state.listeners]) {\n try {\n listener(next);\n } catch {\n // A listener's failure is its own: delivery to the others goes on (guarantee G13).\n }\n }\n }\n } finally {\n state.draining = false;\n }\n }\n\n function emitPresence(event: PresenceEvent): void {\n presenceQueue.push(event);\n if (presenceDraining) return;\n presenceDraining = true;\n try {\n for (let next = presenceQueue.shift(); next !== undefined; next = presenceQueue.shift()) {\n for (const listener of [...(presenceListeners.get(next.channel) ?? [])]) {\n try {\n listener(next);\n } catch {\n // See `drain`.\n }\n }\n }\n } finally {\n presenceDraining = false;\n }\n }\n\n function trim(state: ChannelState, history: number, ttl: number): void {\n const cutoff = now() - ttl;\n let drop = Math.max(0, state.log.length - history);\n while (drop < state.log.length && (state.log[drop]?.at ?? Infinity) < cutoff) drop += 1;\n if (drop > 0) state.log = state.log.slice(drop);\n }\n\n function live(name: string): Map<string, PresenceEntry> {\n let set = presence.get(name);\n if (set === undefined) {\n set = new Map();\n presence.set(name, set);\n }\n return set;\n }\n\n function check(): void {\n if (closed) throw new Error(\"the adapter is closed.\");\n }\n\n return {\n async publish(\n channel: string,\n entry: StoredEntry,\n publish: PublishOptions,\n ): Promise<PublishResult> {\n check();\n const state = channelOf(channel);\n const at = now();\n if (publish.key !== undefined) {\n const known = state.keys.get(publish.key);\n if (known !== undefined && known.expires > at) return { seq: known.seq, duplicate: true };\n // Expired keys are swept when a new one is added, so the map stays bounded by the rate of publishes.\n if (state.keys.size >= 4096)\n for (const [key, value] of state.keys) if (value.expires <= at) state.keys.delete(key);\n }\n state.seq += 1;\n const message: StoredMessage = { ...entry, seq: state.seq, at };\n if (publish.key !== undefined)\n state.keys.set(publish.key, {\n seq: state.seq,\n expires: at + publish.idempotencyTtlMs,\n });\n if (publish.history > 0) {\n state.log.push(message);\n trim(state, publish.history, publish.historyTtlMs);\n } else {\n state.log = [];\n }\n state.pending.push(message);\n drain(state);\n return { seq: message.seq, duplicate: false };\n },\n\n async subscribe(\n channel: string,\n listener: (message: StoredMessage) => void,\n ): Promise<Unsubscribe> {\n check();\n const state = channelOf(channel);\n state.listeners.add(listener);\n return async () => {\n state.listeners.delete(listener);\n };\n },\n\n async history(channel: string, after: number, limit: number): Promise<HistoryResult> {\n check();\n const state = channels.get(channel);\n if (state === undefined) return { messages: [], head: 0, oldest: 0 };\n const retained = state.log;\n const messages: StoredMessage[] = [];\n for (const message of retained) {\n if (messages.length >= limit) break;\n if (message.seq > after) messages.push(message);\n }\n return { messages, head: state.seq, oldest: retained[0]?.seq ?? 0 };\n },\n\n async presenceSet(channel: string, member: Member, ttlMs: number): Promise<void> {\n check();\n const set = live(channel);\n const known = set.get(member.id);\n if (known !== undefined && known.member.v > member.v) return;\n set.set(member.id, { member, expires: now() + ttlMs });\n if (known === undefined) emitPresence({ channel, op: \"join\", member });\n else if (known.member.v < member.v) emitPresence({ channel, op: \"set\", member });\n },\n\n async presenceRemove(channel: string, id: string): Promise<boolean> {\n check();\n const set = presence.get(channel);\n const known = set?.get(id);\n if (set === undefined || known === undefined) return false;\n set.delete(id);\n if (set.size === 0) presence.delete(channel);\n emitPresence({ channel, op: \"leave\", member: known.member });\n return true;\n },\n\n async presenceList(channel: string): Promise<readonly Member[]> {\n check();\n const at = now();\n const out: Member[] = [];\n for (const entry of presence.get(channel)?.values() ?? [])\n if (entry.expires > at) out.push(entry.member);\n return out;\n },\n\n async presenceTouch(entries, ttlMs): Promise<readonly PresenceRef[]> {\n check();\n const expires = now() + ttlMs;\n const missing: PresenceRef[] = [];\n for (const ref of entries) {\n const entry = presence.get(ref.channel)?.get(ref.id);\n if (entry === undefined) missing.push(ref);\n else entry.expires = expires;\n }\n return missing;\n },\n\n async presenceReap(channel: string): Promise<readonly Member[]> {\n check();\n const set = presence.get(channel);\n if (set === undefined) return [];\n const at = now();\n const removed: Member[] = [];\n for (const [id, entry] of [...set]) {\n if (entry.expires > at) continue;\n set.delete(id);\n removed.push(entry.member);\n emitPresence({ channel, op: \"leave\", member: entry.member });\n }\n if (set.size === 0) presence.delete(channel);\n return removed;\n },\n\n async presenceSubscribe(\n channel: string,\n listener: (event: PresenceEvent) => void,\n ): Promise<Unsubscribe> {\n check();\n let set = presenceListeners.get(channel);\n if (set === undefined) {\n set = new Set();\n presenceListeners.set(channel, set);\n }\n set.add(listener);\n const owner = set;\n return async () => {\n owner.delete(listener);\n if (owner.size === 0 && presenceListeners.get(channel) === owner)\n presenceListeners.delete(channel);\n };\n },\n\n async close(): Promise<void> {\n closed = true;\n channels.clear();\n presence.clear();\n presenceListeners.clear();\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"memory.js","names":[],"sources":["../../src/memory.ts"],"sourcesContent":["import type {\n Adapter,\n HistoryResult,\n PresenceRef,\n PublishOptions,\n PublishResult,\n Unsubscribe,\n} from \"./adapter\";\nimport type { Member, PresenceEvent, StoredEntry, StoredMessage } from \"./protocol\";\n\ninterface ChannelState {\n seq: number;\n log: StoredMessage[];\n readonly keys: Map<string, { readonly seq: number; readonly expires: number }>;\n /** The size of `keys` at which expired entries are swept next. */\n keysSweepAt: number;\n readonly listeners: Set<(message: StoredMessage) => void>;\n readonly pending: StoredMessage[];\n draining: boolean;\n lastActive: number;\n}\n\n/** The most idempotency keys one channel remembers; past it the oldest are forgotten, which only lets a very old resend through. */\nconst MAX_KEYS = 65_536;\nconst KEYS_SWEEP_FROM = 4096;\n\ninterface PresenceEntry {\n member: Member;\n expires: number;\n}\n\n/**\n * Options of {@link memoryAdapter}.\n * @since 1.0.0\n */\nexport interface MemoryAdapterOptions {\n /** The clock, in epoch milliseconds. Default: `Date.now`. */\n readonly now?: () => number;\n /**\n * Milliseconds a channel with no listener and no retained message is remembered. After that its head is forgotten and its\n * numbering restarts, which a resuming client sees as `reset`. Default: one hour.\n */\n readonly idleMs?: number;\n /**\n * Receives a listener's failure (a message or presence listener that threw). Delivery to the other listeners goes on either\n * way. `createHub` passes its own `onError` here. Default: `console.error`, so a bug in a listener is never silent.\n */\n readonly onError?: (error: unknown, context: MemoryErrorContext) => void;\n}\n\n/** Where an error reported by the memory adapter came from. @since 1.0.0 */\nexport interface MemoryErrorContext {\n /** `\"listener\"` (a message listener) or `\"presenceListener\"`. */\n readonly where: \"listener\" | \"presenceListener\";\n readonly channel: string;\n}\n\n/**\n * The single-process adapter, and the default of `createHub`. Messages, history, idempotency keys and presence live in memory\n * of this process; several hubs created over one adapter object share them, which is how tests exercise two servers. Use the Redis\n * or Cloudflare adapter when more than one process serves clients.\n *\n * @param options The clock.\n * @returns The adapter.\n * @example\n * import { createHub, memoryAdapter } from \"@shadow-js/realtime/server\";\n *\n * const adapter = memoryAdapter();\n * const hub = createHub({ adapter });\n * void hub.close();\n * @since 1.0.0\n */\nexport function memoryAdapter(options: MemoryAdapterOptions = {}): Adapter {\n const now = options.now ?? Date.now;\n const idleMs = options.idleMs ?? 3_600_000;\n const report = (error: unknown, context: MemoryErrorContext): void => {\n try {\n if (options.onError === undefined) console.error(\"[realtime] memory adapter\", context, error);\n else options.onError(error, context);\n } catch (reporter) {\n // A reporter that throws must not stop delivery; its failure is the last resort for the console.\n console.error(\"[realtime] memory adapter onError\", reporter);\n }\n };\n let sweepAt = 1024;\n const channels = new Map<string, ChannelState>();\n const presence = new Map<string, Map<string, PresenceEntry>>();\n const presenceListeners = new Map<string, Set<(event: PresenceEvent) => void>>();\n const presenceQueue: PresenceEvent[] = [];\n let presenceDraining = false;\n let closed = false;\n\n /** Forgets channels nobody listens to and nothing retains; runs when the table has doubled since the last sweep. */\n function sweep(): void {\n const cutoff = now() - idleMs;\n for (const [name, state] of channels)\n if (state.listeners.size === 0 && state.lastActive < cutoff) {\n trim(state, state.log.length, idleMs);\n if (state.log.length === 0) channels.delete(name);\n }\n sweepAt = Math.max(1024, channels.size * 2);\n }\n\n function channelOf(name: string): ChannelState {\n let state = channels.get(name);\n if (state === undefined) {\n if (channels.size >= sweepAt) sweep();\n state = {\n lastActive: now(),\n seq: 0,\n log: [],\n keys: new Map(),\n keysSweepAt: KEYS_SWEEP_FROM,\n listeners: new Set(),\n pending: [],\n draining: false,\n };\n channels.set(name, state);\n }\n state.lastActive = now();\n return state;\n }\n\n /** Calls each listener with `value`. A listener's failure is its own: delivery to the others goes on (guarantee G13). */\n function deliver<T>(\n listeners: Iterable<(value: T) => void>,\n value: T,\n context: MemoryErrorContext,\n ): void {\n for (const listener of [...listeners]) {\n try {\n listener(value);\n } catch (error) {\n report(error, context);\n }\n }\n }\n\n /** Delivers queued messages in order; a listener that publishes in turn queues behind the message being delivered. */\n function drain(state: ChannelState, channel: string): void {\n if (state.draining) return;\n state.draining = true;\n try {\n for (let next = state.pending.shift(); next !== undefined; next = state.pending.shift()) {\n deliver(state.listeners, next, { where: \"listener\", channel });\n }\n } finally {\n state.draining = false;\n }\n }\n\n function emitPresence(event: PresenceEvent): void {\n presenceQueue.push(event);\n if (presenceDraining) return;\n presenceDraining = true;\n try {\n for (let next = presenceQueue.shift(); next !== undefined; next = presenceQueue.shift()) {\n deliver(presenceListeners.get(next.channel) ?? [], next, {\n where: \"presenceListener\",\n channel: next.channel,\n });\n }\n } finally {\n presenceDraining = false;\n }\n }\n\n function trim(state: ChannelState, history: number, ttl: number): void {\n const cutoff = now() - ttl;\n let drop = Math.max(0, state.log.length - history);\n while (drop < state.log.length && (state.log[drop]?.at ?? Infinity) < cutoff) drop += 1;\n if (drop > 0) state.log = state.log.slice(drop);\n }\n\n function live(name: string): Map<string, PresenceEntry> {\n let set = presence.get(name);\n if (set === undefined) {\n set = new Map();\n presence.set(name, set);\n }\n return set;\n }\n\n function check(): void {\n if (closed) throw new Error(__DEV__ ? \"the adapter is closed.\" : \"REALTIME_CLOSED\");\n }\n\n return {\n async publish(\n channel: string,\n entry: StoredEntry,\n publish: PublishOptions,\n ): Promise<PublishResult> {\n check();\n const state = channelOf(channel);\n const at = now();\n if (publish.key !== undefined) {\n const known = state.keys.get(publish.key);\n if (known !== undefined && known.expires > at) return { seq: known.seq, duplicate: true };\n // Expired keys are swept each time the map has doubled, so a sweep costs a publish little; the cap holds when none expired.\n const { keys } = state;\n if (keys.size >= state.keysSweepAt) {\n for (const [key, value] of keys) if (value.expires <= at) keys.delete(key);\n state.keysSweepAt = Math.max(KEYS_SWEEP_FROM, keys.size * 2);\n }\n for (const key of keys.keys()) {\n if (keys.size < MAX_KEYS) break;\n keys.delete(key);\n }\n }\n state.seq += 1;\n const message: StoredMessage = { ...entry, seq: state.seq, at };\n if (publish.key !== undefined)\n state.keys.set(publish.key, {\n seq: state.seq,\n expires: at + publish.idempotencyTtlMs,\n });\n if (publish.history > 0) {\n state.log.push(message);\n trim(state, publish.history, publish.historyTtlMs);\n } else {\n state.log = [];\n }\n state.pending.push(message);\n drain(state, channel);\n return { seq: message.seq, duplicate: false };\n },\n\n async subscribe(\n channel: string,\n listener: (message: StoredMessage) => void,\n ): Promise<Unsubscribe> {\n check();\n const state = channelOf(channel);\n state.listeners.add(listener);\n return async () => {\n state.listeners.delete(listener);\n };\n },\n\n async history(channel: string, after: number, limit: number): Promise<HistoryResult> {\n check();\n const state = channels.get(channel);\n if (state === undefined) return { messages: [], head: 0, oldest: 0 };\n const retained = state.log;\n const messages: StoredMessage[] = [];\n for (const message of retained) {\n if (messages.length >= limit) break;\n if (message.seq > after) messages.push(message);\n }\n return { messages, head: state.seq, oldest: retained[0]?.seq ?? 0 };\n },\n\n async presenceSet(channel: string, member: Member, ttlMs: number): Promise<void> {\n check();\n const set = live(channel);\n const known = set.get(member.id);\n if (known !== undefined && known.member.v > member.v) return;\n set.set(member.id, { member, expires: now() + ttlMs });\n if (known === undefined) emitPresence({ channel, op: \"join\", member });\n else if (known.member.v < member.v) emitPresence({ channel, op: \"set\", member });\n },\n\n async presenceRemove(channel: string, id: string): Promise<boolean> {\n check();\n const set = presence.get(channel);\n const known = set?.get(id);\n if (set === undefined || known === undefined) return false;\n set.delete(id);\n if (set.size === 0) presence.delete(channel);\n emitPresence({ channel, op: \"leave\", member: known.member });\n return true;\n },\n\n async presenceList(channel: string): Promise<readonly Member[]> {\n check();\n const at = now();\n const out: Member[] = [];\n for (const entry of presence.get(channel)?.values() ?? [])\n if (entry.expires > at) out.push(entry.member);\n return out;\n },\n\n async presenceTouch(entries, ttlMs): Promise<readonly PresenceRef[]> {\n check();\n const expires = now() + ttlMs;\n const missing: PresenceRef[] = [];\n for (const ref of entries) {\n const entry = presence.get(ref.channel)?.get(ref.id);\n if (entry === undefined) missing.push(ref);\n else entry.expires = expires;\n }\n return missing;\n },\n\n async presenceReap(channel: string): Promise<readonly Member[]> {\n check();\n const set = presence.get(channel);\n if (set === undefined) return [];\n const at = now();\n const removed: Member[] = [];\n for (const [id, entry] of [...set]) {\n if (entry.expires > at) continue;\n set.delete(id);\n removed.push(entry.member);\n emitPresence({ channel, op: \"leave\", member: entry.member });\n }\n if (set.size === 0) presence.delete(channel);\n return removed;\n },\n\n async presenceSubscribe(\n channel: string,\n listener: (event: PresenceEvent) => void,\n ): Promise<Unsubscribe> {\n check();\n let set = presenceListeners.get(channel);\n if (set === undefined) {\n set = new Set();\n presenceListeners.set(channel, set);\n }\n set.add(listener);\n const owner = set;\n return async () => {\n owner.delete(listener);\n if (owner.size === 0 && presenceListeners.get(channel) === owner)\n presenceListeners.delete(channel);\n };\n },\n\n async close(): Promise<void> {\n closed = true;\n channels.clear();\n presence.clear();\n presenceListeners.clear();\n },\n };\n}\n"],"mappings":"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"}
@@ -1,5 +1,4 @@
1
1
  import { ErrorCode } from "./errors.js";
2
- //#region src/protocol.d.ts
3
2
  /**
4
3
  * The wire protocol version this package speaks (https://realtime.shadowjs.net/guides/realtime-protocol/).
5
4
  * @example
@@ -207,5 +206,4 @@ export type ServerFrame = {
207
206
  * @since 1.0.0
208
207
  */
209
208
  export declare function decodeServerFrame(text: string): ServerFrame | undefined;
210
- //#endregion
211
209
  //# sourceMappingURL=protocol.d.ts.map