@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.
- package/CHANGELOG.md +26 -0
- package/README.md +35 -0
- package/dist/dev/adapter.d.ts +0 -2
- package/dist/dev/client/core.d.ts +0 -2
- package/dist/dev/client/core.js +105 -108
- package/dist/dev/client/core.js.map +1 -1
- package/dist/dev/client/presence.js +1 -2
- package/dist/dev/client/sse.js +34 -26
- package/dist/dev/client/sse.js.map +1 -1
- package/dist/dev/client/types.d.ts +6 -2
- package/dist/dev/client/util.js +3 -4
- package/dist/dev/client/util.js.map +1 -1
- package/dist/dev/client/ws.js +1 -2
- package/dist/dev/cloudflare/adapter.d.ts +0 -2
- package/dist/dev/cloudflare/adapter.js +1 -2
- package/dist/dev/cloudflare/core.js +1 -2
- package/dist/dev/cloudflare/link.js +70 -10
- package/dist/dev/cloudflare/link.js.map +1 -1
- package/dist/dev/cloudflare/object.d.ts +0 -2
- package/dist/dev/cloudflare/object.js +1 -2
- package/dist/dev/cloudflare/types.d.ts +0 -2
- package/dist/dev/cloudflare/wire.js +1 -2
- package/dist/dev/errors.d.ts +0 -2
- package/dist/dev/errors.js +1 -2
- package/dist/dev/hooks.d.ts +0 -2
- package/dist/dev/hooks.js +1 -2
- package/dist/dev/memory.d.ts +11 -2
- package/dist/dev/memory.js +39 -12
- package/dist/dev/memory.js.map +1 -1
- package/dist/dev/protocol.d.ts +0 -2
- package/dist/dev/protocol.js +14 -14
- package/dist/dev/protocol.js.map +1 -1
- package/dist/dev/redis/client.d.ts +0 -2
- package/dist/dev/redis/client.js +1 -2
- package/dist/dev/redis/scripts.js +1 -2
- package/dist/dev/redis/topics.d.ts +6 -3
- package/dist/dev/redis/topics.js +5 -3
- package/dist/dev/redis/topics.js.map +1 -1
- package/dist/dev/redis.d.ts +0 -2
- package/dist/dev/redis.js +1 -2
- package/dist/dev/server/http.d.ts +0 -2
- package/dist/dev/server/http.js +38 -26
- package/dist/dev/server/http.js.map +1 -1
- package/dist/dev/server/hub.d.ts +0 -2
- package/dist/dev/server/hub.js +67 -41
- package/dist/dev/server/hub.js.map +1 -1
- package/dist/dev/server/index.d.ts +2 -4
- package/dist/dev/server/index.js +1 -2
- package/dist/dev/server/index.js.map +1 -1
- package/dist/dev/server/limits.d.ts +4 -3
- package/dist/dev/server/limits.js +1 -2
- package/dist/dev/server/limits.js.map +1 -1
- package/dist/dev/server/rules.d.ts +0 -2
- package/dist/dev/server/rules.js +1 -2
- package/dist/dev/server/rules.js.map +1 -1
- package/dist/dev/testing.d.ts +0 -2
- package/dist/dev/testing.js +1 -2
- package/dist/prod/client/core.js +1 -1
- package/dist/prod/client/core.js.map +1 -1
- package/dist/prod/client/sse.js +1 -4
- package/dist/prod/client/sse.js.map +1 -1
- package/dist/prod/client/util.js +1 -1
- package/dist/prod/client/util.js.map +1 -1
- package/dist/prod/cloudflare/link.js +1 -1
- package/dist/prod/cloudflare/link.js.map +1 -1
- package/dist/prod/memory.js +1 -1
- package/dist/prod/memory.js.map +1 -1
- package/dist/prod/protocol.js +1 -1
- package/dist/prod/protocol.js.map +1 -1
- package/dist/prod/redis/topics.js +1 -1
- package/dist/prod/redis/topics.js.map +1 -1
- package/dist/prod/server/http.js +1 -1
- package/dist/prod/server/http.js.map +1 -1
- package/dist/prod/server/hub.js +1 -1
- package/dist/prod/server/hub.js.map +1 -1
- package/dist/prod/server/index.js.map +1 -1
- package/dist/prod/server/limits.js +1 -1
- package/dist/prod/server/limits.js.map +1 -1
- package/dist/prod/server/rules.js +1 -1
- package/dist/prod/server/rules.js.map +1 -1
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.2
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- - Client: adding the same handler to a channel twice no longer leaves a subscription hold that nothing can release.
|
|
8
|
+
- - Client: a POST that never answers (server-sent events transport) is abandoned after the acknowledgment timeout, so the publishes queued behind it are sent.
|
|
9
|
+
- - Server: a stream declaration that names its own `t` or `n` is always read as a subscription, never as a publish or request.
|
|
10
|
+
- - Server: a WebSocket that closes while `authenticate` is still running no longer leaves a connection in the hub.
|
|
11
|
+
- - Server: presence changes posted over HTTP spend from the same rate limit as publishes, and a POST body is read only up to the size limit.
|
|
12
|
+
- - Server: a member whose presence entry expired is written back with its last state instead of `null`; a transport whose `send` throws is closed.
|
|
13
|
+
- - Memory adapter: idempotency keys are capped per channel (65,536), and expired ones are swept when the map doubles.
|
|
14
|
+
- - Redis adapter: a channel whose keys were lost and whose numbering restarted still delivers its messages (the hub tells subscribers `reset`).
|
|
15
|
+
- - Client: the event stream parser holds at most 1 MiB for an unfinished line and event; a longer one closes the stream and reconnects. A tab that becomes visible while the client waits out a reconnect backoff reconnects at once.
|
|
16
|
+
- - Server: `sub` and `unsub` frames spend from a per-connection bucket (the publish rate, a burst that covers all of `maxChannelsPerConnection`), so subscription churn is limited like publishing.
|
|
17
|
+
- - Server: a frame over `maxMessageBytes` is refused when it arrives instead of after the frames queued before it; it counts as an invalid frame.
|
|
18
|
+
- - Server: a presence state posted over HTTP also updates the connection's own copy (on this server directly, on another through the presence event), so a write-back after an expiry carries the newest version and state.
|
|
19
|
+
- - Cloudflare adapter: a listener whose queue overflows is reported (`onError`), and a presence listener is resynced to the real member set (leaves, joins, changes) instead of staying wrong.
|
|
20
|
+
- - Errors while dropping a connection and listener failures in the memory adapter go to `onError` (memory adapter: new `onError` option, default `console.error`) instead of being swallowed.
|
|
21
|
+
- The realtime client takes `reconnect.maxReconnectAttempts` (default unlimited). After that many failed reconnects in a row (the count restarts when a connection stays open `stableMs`) the client is `closed`, pending calls reject, and `onError` receives a `closed` error.
|
|
22
|
+
|
|
23
|
+
## 1.0.1
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Released at 1.0.1 with the other packages, which share one version. No change to this package.
|
|
28
|
+
|
|
3
29
|
## 1.0.0
|
|
4
30
|
|
|
5
31
|
### Added
|
package/README.md
CHANGED
|
@@ -104,6 +104,41 @@ Root, `@shadow-js/realtime`: the wire protocol and shared types.
|
|
|
104
104
|
`@shadow-js/realtime/cloudflare`: `cloudflareAdapter`, `RealtimeChannelObject`.
|
|
105
105
|
`@shadow-js/realtime/testing`: `connectTest`, `adapterConformance`.
|
|
106
106
|
|
|
107
|
+
## Production error codes
|
|
108
|
+
|
|
109
|
+
A production build keeps a short code instead of the development explanation, so message text never reaches user bundles. Search this table for the code. In the explanation, `…` stands for a value the development message names. A code followed by a word names the option, feature or member at fault.
|
|
110
|
+
|
|
111
|
+
| Code | Development explanation |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `REALTIME_CLOSED` | the adapter is closed. |
|
|
114
|
+
| `REALTIME_DATA` | the data is not JSON |
|
|
115
|
+
| `REALTIME_FRAME_1` | after must be a non-negative integer |
|
|
116
|
+
| `REALTIME_FRAME_10` | the frame is not an object |
|
|
117
|
+
| `REALTIME_FRAME_11` | the frame is larger than the limit |
|
|
118
|
+
| `REALTIME_FRAME_12` | unknown frame type |
|
|
119
|
+
| `REALTIME_FRAME_13` | v must be a version |
|
|
120
|
+
| `REALTIME_FRAME_14` | watch must be a boolean |
|
|
121
|
+
| `REALTIME_FRAME_2` | c is not a valid channel name |
|
|
122
|
+
| `REALTIME_FRAME_3` | echo must be a boolean |
|
|
123
|
+
| `REALTIME_FRAME_4` | e is not a valid event name |
|
|
124
|
+
| `REALTIME_FRAME_5` | the frame is not JSON |
|
|
125
|
+
| `REALTIME_FRAME_6` | k is not a valid key |
|
|
126
|
+
| `REALTIME_FRAME_7` | ms must be a positive number |
|
|
127
|
+
| `REALTIME_FRAME_8` | n must be a non-negative integer |
|
|
128
|
+
| `REALTIME_FRAME_9` | sub needs n / req needs n |
|
|
129
|
+
| `REALTIME_LIMIT_1` | limits.history must be at most …. |
|
|
130
|
+
| `REALTIME_LIMIT_2` | unknown limit "…". |
|
|
131
|
+
| `REALTIME_LIMIT_3` | limits.presenceTtlMs must be larger than limits.presenceHeartbeatMs. |
|
|
132
|
+
| `REALTIME_LIMIT_4` | limits.… must be … integer. |
|
|
133
|
+
| `REALTIME_PATTERN_1` | the channel pattern "…" must be 1..128 characters. |
|
|
134
|
+
| `REALTIME_PATTERN_2` | the channel pattern "…" repeats "…". |
|
|
135
|
+
| `REALTIME_PATTERN_3` | the channel pattern "…": {...name} must be the last segment. |
|
|
136
|
+
| `REALTIME_PATTERN_4` | the channel pattern "…" has an invalid segment "…". |
|
|
137
|
+
| `REALTIME_REVOKE` | revoke needs a user, a connection or a channel. |
|
|
138
|
+
| `REALTIME_RULE` | the rule for "…": history must be a non-negative integer. |
|
|
139
|
+
|
|
140
|
+
A `RealtimeError` raised without a message uses its protocol code (`forbidden`, `too_large`, ...) as the message; those codes are listed in the protocol guide at https://realtime.shadowjs.net/guides/realtime-protocol/. The `REALTIME_FRAME_*` codes are the `why` of a refused client frame, sent with the `bad_frame` error.
|
|
141
|
+
|
|
107
142
|
## Learn more
|
|
108
143
|
|
|
109
144
|
See the ShadowJS documentation at https://shadowjs.net, and the realtime guide at https://realtime.shadowjs.net.
|
package/dist/dev/adapter.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Member, PresenceEvent, StoredEntry, StoredMessage } from "./protocol.js";
|
|
2
|
-
//#region src/adapter.d.ts
|
|
3
2
|
/** How a publish is retained and deduplicated. @since 1.0.0 */
|
|
4
3
|
export interface PublishOptions {
|
|
5
4
|
/** Messages kept per channel (0 keeps none). */
|
|
@@ -71,5 +70,4 @@ export interface Adapter {
|
|
|
71
70
|
/** Releases connections and timers. */
|
|
72
71
|
close(): Promise<void>;
|
|
73
72
|
}
|
|
74
|
-
//#endregion
|
|
75
73
|
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ChannelSchema, Realtime, RealtimeOptions } from "./types.js";
|
|
2
|
-
//#region src/client/core.d.ts
|
|
3
2
|
/**
|
|
4
3
|
* Creates the realtime client: one transport (WebSocket, falling back to server-sent ev), reconnects with backoff, a
|
|
5
4
|
* bounded offline queue, resume from the last sequence number and typed channels. It connects when the first channel
|
|
@@ -20,5 +19,4 @@ import { ChannelSchema, Realtime, RealtimeOptions } from "./types.js";
|
|
|
20
19
|
* @since 1.0.0
|
|
21
20
|
*/
|
|
22
21
|
export declare function createRealtime<Schema extends ChannelSchema = ChannelSchema>(options: RealtimeOptions): Realtime<Schema>;
|
|
23
|
-
//#endregion
|
|
24
22
|
//# sourceMappingURL=core.d.ts.map
|
package/dist/dev/client/core.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { RealtimeError } from "../errors.js";
|
|
2
|
-
import { byteLength,
|
|
2
|
+
import { byteLength, isChannelName, isEventName, isIdempotencyKey, serverFrameOf } from "../protocol.js";
|
|
3
3
|
import { applyPresence } from "./presence.js";
|
|
4
4
|
import { openStream } from "./sse.js";
|
|
5
5
|
import { backoff, clientId, resolveUrl, sendUrl } from "./util.js";
|
|
6
6
|
import { openSocket } from "./ws.js";
|
|
7
|
-
|
|
7
|
+
// src/client/core.ts
|
|
8
8
|
/** Close codes that mean the client did something the server will not accept again. */
|
|
9
9
|
const FATAL = /* @__PURE__ */ new Set([
|
|
10
10
|
1002,
|
|
@@ -57,7 +57,6 @@ function createRealtime(options) {
|
|
|
57
57
|
const ackTtl = options.ackTimeoutMs ?? 1e4;
|
|
58
58
|
const auto = (options.transport ?? "auto") === "auto";
|
|
59
59
|
const id = clientId();
|
|
60
|
-
const life = new AbortController();
|
|
61
60
|
const chans = /* @__PURE__ */ new Map();
|
|
62
61
|
const dirty = /* @__PURE__ */ new Set();
|
|
63
62
|
const queue = [];
|
|
@@ -79,13 +78,15 @@ function createRealtime(options) {
|
|
|
79
78
|
let counter = 0;
|
|
80
79
|
let keys = 0;
|
|
81
80
|
let orders = 0;
|
|
82
|
-
|
|
81
|
+
/** Heartbeat intervals since the last thing was received. */
|
|
83
82
|
let silent = 0;
|
|
84
83
|
let connectTimer;
|
|
85
84
|
let reconnectTimer;
|
|
86
85
|
let stableTimer;
|
|
87
86
|
let beatTimer;
|
|
88
87
|
let posting = false;
|
|
88
|
+
/** The POST in flight, which closing the client abandons. */
|
|
89
|
+
let batch;
|
|
89
90
|
let postTimer;
|
|
90
91
|
let postFails = 0;
|
|
91
92
|
let listening = false;
|
|
@@ -126,15 +127,13 @@ function createRealtime(options) {
|
|
|
126
127
|
if (settle !== void 0) settle(error, value);
|
|
127
128
|
else if (error !== void 0 && op.kind === "pub") report(error);
|
|
128
129
|
};
|
|
129
|
-
const fail = (op, error) => finish(op, error);
|
|
130
|
-
const succeed = (op, value) => finish(op, void 0, value);
|
|
131
130
|
const arm = (op, ms) => {
|
|
132
131
|
clearTimeout(op.timer);
|
|
133
132
|
op.timer = setTimeout(() => {
|
|
134
133
|
const at = queue.indexOf(op);
|
|
135
134
|
if (at >= 0) queue.splice(at, 1);
|
|
136
135
|
if (inflight.get(op.n) === op) inflight.delete(op.n);
|
|
137
|
-
|
|
136
|
+
finish(op, new RealtimeError("timeout"));
|
|
138
137
|
}, ms);
|
|
139
138
|
};
|
|
140
139
|
const take = (n) => {
|
|
@@ -159,22 +158,24 @@ function createRealtime(options) {
|
|
|
159
158
|
chan,
|
|
160
159
|
event,
|
|
161
160
|
data,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
ms: extra.ms ?? 0,
|
|
161
|
+
ack: true,
|
|
162
|
+
ms: 0,
|
|
165
163
|
order: ++orders,
|
|
166
164
|
n: 0,
|
|
167
|
-
|
|
168
|
-
settle: extra.settle
|
|
165
|
+
...extra
|
|
169
166
|
});
|
|
167
|
+
/** Whether the server answers an operation (a request, or a publish that asks for an acknowledgment). */
|
|
168
|
+
const answered = (op) => op.kind === "req" || op.kind === "pub" && op.ack;
|
|
169
|
+
/** How long an operation that went out waits for its answer: a request's own time and a second, or the acknowledgment time. */
|
|
170
|
+
const waitOf = (op) => op.kind === "req" ? op.ms + 1e3 : ackTtl;
|
|
170
171
|
/** The frame text of an operation, numbered for an answer. Undefined (and the operation failed) when it is too large. */
|
|
171
172
|
const encode = (op) => {
|
|
172
173
|
const c = JSON.stringify(op.chan.name);
|
|
173
|
-
const wantsAnswer = op
|
|
174
|
+
const wantsAnswer = answered(op);
|
|
174
175
|
op.n = wantsAnswer ? ++counter : 0;
|
|
175
176
|
const text = op.kind === "pres" ? `{"t":"pres","c":${c},"s":${op.data}}` : op.kind === "req" ? `{"t":"req","n":${op.n},"c":${c},"e":"${op.event}","d":${op.data},"ms":${op.ms}}` : `{"t":"pub","c":${c},"e":"${op.event}","d":${op.data}${wantsAnswer ? `,"n":${op.n}` : ""},"k":${JSON.stringify(op.key)}}`;
|
|
176
177
|
if (byteLength(text) > maxBytes) {
|
|
177
|
-
|
|
178
|
+
finish(op, new RealtimeError("too_large"));
|
|
178
179
|
return;
|
|
179
180
|
}
|
|
180
181
|
return text;
|
|
@@ -182,16 +183,16 @@ function createRealtime(options) {
|
|
|
182
183
|
/** Records that an operation went out: it now waits for an answer, or is done. */
|
|
183
184
|
const sent = (op) => {
|
|
184
185
|
if (op.kind === "pres") return;
|
|
185
|
-
if (op
|
|
186
|
+
if (answered(op)) {
|
|
186
187
|
inflight.set(op.n, op);
|
|
187
|
-
arm(op, op
|
|
188
|
+
arm(op, waitOf(op));
|
|
188
189
|
} else clearTimeout(op.timer);
|
|
189
190
|
};
|
|
190
191
|
/** Puts operations that went out on a connection that died back at the head of the queue, in their original order. */
|
|
191
192
|
const requeue = (ops) => {
|
|
192
193
|
ops.sort((a, b) => a.order - b.order);
|
|
193
194
|
const again = [];
|
|
194
|
-
for (const op of ops) if (op.kind === "req")
|
|
195
|
+
for (const op of ops) if (op.kind === "req") finish(op, new RealtimeError("closed"));
|
|
195
196
|
else {
|
|
196
197
|
if (op.kind === "pub") arm(op, queueTtl);
|
|
197
198
|
again.push(op);
|
|
@@ -220,13 +221,13 @@ function createRealtime(options) {
|
|
|
220
221
|
ready: !join
|
|
221
222
|
};
|
|
222
223
|
setChanStatus(chan, chan.sy ? "resuming" : "subscribing");
|
|
223
|
-
return {
|
|
224
|
+
return {
|
|
224
225
|
c: chan.name,
|
|
225
226
|
...after === void 0 ? {} : { after },
|
|
226
227
|
...chan.options.echo === false ? { echo: false } : {},
|
|
227
228
|
...join ? { presence: chan.state } : {},
|
|
228
229
|
...watch ? { watch: true } : {}
|
|
229
|
-
}
|
|
230
|
+
};
|
|
230
231
|
};
|
|
231
232
|
const sendPresence = (chan) => {
|
|
232
233
|
const w = chan.w;
|
|
@@ -252,12 +253,15 @@ function createRealtime(options) {
|
|
|
252
253
|
setChanStatus(chan, chan.closed ? "closed" : "idle");
|
|
253
254
|
};
|
|
254
255
|
/** Brings the w in line with what a channel wants (WebSocket: subscribe, unsubscribe, presence). */
|
|
256
|
+
const unsub = (chan) => {
|
|
257
|
+
link?.send(`{"t":"unsub","c":${JSON.stringify(chan.name)}}`);
|
|
258
|
+
};
|
|
255
259
|
const syncChan = (chan) => {
|
|
256
260
|
const want = wants(chan);
|
|
257
261
|
const w = chan.w;
|
|
258
262
|
if (mode === "ws" && ready) {
|
|
259
263
|
if (w !== void 0 && (!want || w.join !== joins(chan) || w.watch !== watches(chan))) {
|
|
260
|
-
|
|
264
|
+
unsub(chan);
|
|
261
265
|
chan.w = void 0;
|
|
262
266
|
}
|
|
263
267
|
if (want && chan.w === void 0 && chan.retry === void 0) {
|
|
@@ -265,7 +269,7 @@ function createRealtime(options) {
|
|
|
265
269
|
link?.send(JSON.stringify({
|
|
266
270
|
t: "sub",
|
|
267
271
|
n,
|
|
268
|
-
...subscription(chan, n)
|
|
272
|
+
...subscription(chan, n)
|
|
269
273
|
}));
|
|
270
274
|
}
|
|
271
275
|
}
|
|
@@ -285,7 +289,7 @@ function createRealtime(options) {
|
|
|
285
289
|
if (chan.closed) return;
|
|
286
290
|
chan.closed = true;
|
|
287
291
|
if (chans.get(chan.name) === chan) chans.delete(chan.name);
|
|
288
|
-
if (chan.w !== void 0 && mode === "ws" && ready)
|
|
292
|
+
if (chan.w !== void 0 && mode === "ws" && ready) unsub(chan);
|
|
289
293
|
goIdle(chan);
|
|
290
294
|
chan.ev.clear();
|
|
291
295
|
chan.any.clear();
|
|
@@ -311,20 +315,6 @@ function createRealtime(options) {
|
|
|
311
315
|
}, backoff(chan.fails, minDelay, maxDelay, random));
|
|
312
316
|
chan.fails += 1;
|
|
313
317
|
};
|
|
314
|
-
/** Declares the subscriptions of an SSE stream; returns the query entries. */
|
|
315
|
-
const declare = () => {
|
|
316
|
-
const entries = [];
|
|
317
|
-
for (const chan of [...chans.values()]) {
|
|
318
|
-
if (!wants(chan)) continue;
|
|
319
|
-
if (entries.length >= MAX_STREAM_CHANNELS) {
|
|
320
|
-
closeChan(chan, new RealtimeError("limit"));
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
entries.push(subscription(chan, entries.length + 1).fields);
|
|
324
|
-
}
|
|
325
|
-
streamSig = signature();
|
|
326
|
-
return entries;
|
|
327
|
-
};
|
|
328
318
|
/** What the open stream must be reopened for: the channels it carries and how they join. */
|
|
329
319
|
const signature = () => {
|
|
330
320
|
let sig = "";
|
|
@@ -338,11 +328,10 @@ function createRealtime(options) {
|
|
|
338
328
|
gen += 1;
|
|
339
329
|
const g = gen;
|
|
340
330
|
ready = false;
|
|
341
|
-
received = false;
|
|
342
331
|
silent = 0;
|
|
343
332
|
const ev = {
|
|
344
333
|
rx: () => {
|
|
345
|
-
if (g === gen)
|
|
334
|
+
if (g === gen) silent = 0;
|
|
346
335
|
},
|
|
347
336
|
frame: (frame) => {
|
|
348
337
|
if (g === gen) guard(() => onFrame(frame, false));
|
|
@@ -355,10 +344,21 @@ function createRealtime(options) {
|
|
|
355
344
|
if (!listening && typeof addEventListener === "function") {
|
|
356
345
|
listening = true;
|
|
357
346
|
addEventListener("online", onOnline);
|
|
347
|
+
if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisible);
|
|
358
348
|
}
|
|
359
349
|
if (mode === "sse") {
|
|
360
350
|
const url = resolveUrl(options.url, false);
|
|
361
|
-
|
|
351
|
+
const entries = [];
|
|
352
|
+
for (const chan of [...chans.values()]) {
|
|
353
|
+
if (!wants(chan)) continue;
|
|
354
|
+
if (entries.length >= MAX_STREAM_CHANNELS) {
|
|
355
|
+
closeChan(chan, new RealtimeError("limit"));
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
entries.push(subscription(chan, entries.length + 1));
|
|
359
|
+
}
|
|
360
|
+
streamSig = signature();
|
|
361
|
+
url.searchParams.set("s", JSON.stringify(entries));
|
|
362
362
|
link = openStream(url.toString(), doFetch, getHeaders, ev);
|
|
363
363
|
} else link = openSocket(resolveUrl(options.url, true).toString(), options.WebSocket ?? globalThis.WebSocket, ev);
|
|
364
364
|
} catch (error) {
|
|
@@ -373,6 +373,10 @@ function createRealtime(options) {
|
|
|
373
373
|
const onOnline = () => {
|
|
374
374
|
if (reconnectTimer !== void 0) connect();
|
|
375
375
|
};
|
|
376
|
+
/** A tab that was hidden keeps its timers throttled or frozen: when it shows again, a connection waiting out a backoff reconnects now. */
|
|
377
|
+
const onVisible = () => {
|
|
378
|
+
if (document.visibilityState === "visible" && reconnectTimer !== void 0) connect();
|
|
379
|
+
};
|
|
376
380
|
/** Releases the current transport and everything tied to it. */
|
|
377
381
|
const drop = () => {
|
|
378
382
|
gen += 1;
|
|
@@ -409,6 +413,10 @@ function createRealtime(options) {
|
|
|
409
413
|
shutdown(new RealtimeError("closed", why));
|
|
410
414
|
return;
|
|
411
415
|
}
|
|
416
|
+
if (attempt >= (reconnect.maxReconnectAttempts ?? Infinity)) {
|
|
417
|
+
shutdown(new RealtimeError("closed", `gave up after ${reconnect.maxReconnectAttempts} reconnect attempts`));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
412
420
|
let delay = backoff(attempt, minDelay, maxDelay, random);
|
|
413
421
|
attempt += 1;
|
|
414
422
|
if (wasReady) wsFails = 0;
|
|
@@ -424,49 +432,30 @@ function createRealtime(options) {
|
|
|
424
432
|
};
|
|
425
433
|
/** Heartbeat: every interval sends a ping; two intervals with nothing received declare the connection dead. */
|
|
426
434
|
const beat = () => {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
silent += 1;
|
|
432
|
-
if (silent >= 2) {
|
|
433
|
-
linkClosed({});
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
435
|
+
silent += 1;
|
|
436
|
+
if (silent > 2) {
|
|
437
|
+
linkClosed({});
|
|
438
|
+
return;
|
|
436
439
|
}
|
|
437
440
|
if (mode === "ws") link?.send("{\"t\":\"ping\"}");
|
|
438
441
|
};
|
|
439
|
-
const reopen = () => {
|
|
440
|
-
drop();
|
|
441
|
-
for (const chan of chans.values()) chan.w = void 0;
|
|
442
|
-
connect();
|
|
443
|
-
};
|
|
444
|
-
const onReady = (frame) => {
|
|
445
|
-
if (ready) return;
|
|
446
|
-
ready = true;
|
|
447
|
-
connId = frame.id;
|
|
448
|
-
maxBytes = frame.max;
|
|
449
|
-
wsFails = 0;
|
|
450
|
-
received = true;
|
|
451
|
-
silent = 0;
|
|
452
|
-
clearTimeout(connectTimer);
|
|
453
|
-
if (frame.hb > 0) beatTimer = setInterval(beat, frame.hb);
|
|
454
|
-
stableTimer = setTimeout(() => {
|
|
455
|
-
attempt = 0;
|
|
456
|
-
}, stableMs);
|
|
457
|
-
setStatus("open");
|
|
458
|
-
if (mode === "ws") for (const chan of chans.values()) dirty.add(chan);
|
|
459
|
-
flush();
|
|
460
|
-
};
|
|
461
|
-
const notifyPresence = (chan) => {
|
|
462
|
-
const members = [...chan.members.values()];
|
|
463
|
-
chan.list = members;
|
|
464
|
-
emit(chan.presence, members);
|
|
465
|
-
};
|
|
466
442
|
const onFrame = (frame, viaPost) => {
|
|
467
443
|
switch (frame.t) {
|
|
468
444
|
case "ready":
|
|
469
|
-
|
|
445
|
+
if (ready) return;
|
|
446
|
+
ready = true;
|
|
447
|
+
connId = frame.id;
|
|
448
|
+
maxBytes = frame.max;
|
|
449
|
+
wsFails = 0;
|
|
450
|
+
silent = 0;
|
|
451
|
+
clearTimeout(connectTimer);
|
|
452
|
+
if (frame.hb > 0) beatTimer = setInterval(beat, frame.hb);
|
|
453
|
+
stableTimer = setTimeout(() => {
|
|
454
|
+
attempt = 0;
|
|
455
|
+
}, stableMs);
|
|
456
|
+
setStatus("open");
|
|
457
|
+
if (mode === "ws") for (const chan of chans.values()) dirty.add(chan);
|
|
458
|
+
flush();
|
|
470
459
|
return;
|
|
471
460
|
case "bye":
|
|
472
461
|
byeCode = frame.code;
|
|
@@ -476,16 +465,13 @@ function createRealtime(options) {
|
|
|
476
465
|
const w = chan?.w;
|
|
477
466
|
if (chan === void 0 || w === void 0 || w.n !== frame.n) return;
|
|
478
467
|
if (frame.reset || w.after === void 0) chan.last = frame.head;
|
|
479
|
-
const
|
|
480
|
-
after: w.after ?? 0,
|
|
481
|
-
head: frame.head
|
|
482
|
-
};
|
|
483
|
-
const gap = frame.reset ? true : frame.gap === true && w.after !== void 0 && !(w.after === 0 && !chan.sy);
|
|
468
|
+
const gap = frame.reset === true || frame.gap === true && w.after !== void 0 && !(w.after === 0 && !chan.sy);
|
|
484
469
|
chan.sy = true;
|
|
485
470
|
chan.fails = 0;
|
|
486
471
|
setChanStatus(chan, "live");
|
|
487
472
|
if (gap) emit(chan.gl, {
|
|
488
|
-
|
|
473
|
+
after: w.after ?? 0,
|
|
474
|
+
head: frame.head,
|
|
489
475
|
reset: frame.reset === true
|
|
490
476
|
});
|
|
491
477
|
return;
|
|
@@ -519,7 +505,7 @@ function createRealtime(options) {
|
|
|
519
505
|
}
|
|
520
506
|
case "ack": {
|
|
521
507
|
const op = take(frame.n);
|
|
522
|
-
if (op !== void 0)
|
|
508
|
+
if (op !== void 0) finish(op, void 0, {
|
|
523
509
|
seq: frame.s,
|
|
524
510
|
duplicate: frame.dup === true
|
|
525
511
|
});
|
|
@@ -527,7 +513,7 @@ function createRealtime(options) {
|
|
|
527
513
|
}
|
|
528
514
|
case "rep": {
|
|
529
515
|
const op = take(frame.n);
|
|
530
|
-
if (op !== void 0)
|
|
516
|
+
if (op !== void 0) finish(op, void 0, frame.d);
|
|
531
517
|
return;
|
|
532
518
|
}
|
|
533
519
|
case "err": {
|
|
@@ -546,7 +532,7 @@ function createRealtime(options) {
|
|
|
546
532
|
}
|
|
547
533
|
const op = viaPost || mode === "ws" ? take(frame.n) : void 0;
|
|
548
534
|
if (op !== void 0) {
|
|
549
|
-
|
|
535
|
+
finish(op, error);
|
|
550
536
|
return;
|
|
551
537
|
}
|
|
552
538
|
}
|
|
@@ -556,7 +542,10 @@ function createRealtime(options) {
|
|
|
556
542
|
case "pres": {
|
|
557
543
|
const chan = chans.get(frame.c);
|
|
558
544
|
if (chan === void 0) return;
|
|
559
|
-
if (applyPresence(chan.members, frame.op, frame.m))
|
|
545
|
+
if (applyPresence(chan.members, frame.op, frame.m)) {
|
|
546
|
+
chan.list = [...chan.members.values()];
|
|
547
|
+
emit(chan.presence, chan.list);
|
|
548
|
+
}
|
|
560
549
|
if (frame.op === "sync" && chan.w !== void 0) {
|
|
561
550
|
chan.w.ready = true;
|
|
562
551
|
if (chan.w.join && chan.w.ver !== chan.sv) sendPresence(chan);
|
|
@@ -603,6 +592,9 @@ function createRealtime(options) {
|
|
|
603
592
|
const postBatch = async (ops, body) => {
|
|
604
593
|
for (const op of ops) sent(op);
|
|
605
594
|
let again = false;
|
|
595
|
+
const mine = new AbortController();
|
|
596
|
+
batch = mine;
|
|
597
|
+
const patience = setTimeout(() => mine.abort(), Math.max(...ops.map(waitOf)));
|
|
606
598
|
try {
|
|
607
599
|
const headers = new Headers(await getHeaders());
|
|
608
600
|
headers.set("content-type", "application/json");
|
|
@@ -611,12 +603,12 @@ function createRealtime(options) {
|
|
|
611
603
|
method: "POST",
|
|
612
604
|
headers,
|
|
613
605
|
body,
|
|
614
|
-
signal:
|
|
606
|
+
signal: mine.signal
|
|
615
607
|
});
|
|
616
608
|
if (response.ok) {
|
|
617
609
|
const parsed = await response.json();
|
|
618
610
|
if (Array.isArray(parsed.frames)) for (const item of parsed.frames) {
|
|
619
|
-
const frame =
|
|
611
|
+
const frame = serverFrameOf(item);
|
|
620
612
|
if (frame !== void 0) guard(() => onFrame(frame, true));
|
|
621
613
|
}
|
|
622
614
|
postFails = 0;
|
|
@@ -626,38 +618,42 @@ function createRealtime(options) {
|
|
|
626
618
|
const code = refusal?.error?.code ?? "unavailable";
|
|
627
619
|
for (const op of ops) {
|
|
628
620
|
if (inflight.get(op.n) === op) inflight.delete(op.n);
|
|
629
|
-
|
|
621
|
+
finish(op, new RealtimeError(code, refusal?.error?.message));
|
|
630
622
|
}
|
|
631
623
|
}
|
|
632
624
|
} catch {
|
|
633
|
-
if (
|
|
625
|
+
if (finished) return;
|
|
634
626
|
again = true;
|
|
627
|
+
} finally {
|
|
628
|
+
clearTimeout(patience);
|
|
635
629
|
}
|
|
636
630
|
posting = false;
|
|
637
|
-
const unanswered = ops.filter((op) => (op
|
|
631
|
+
const unanswered = ops.filter((op) => answered(op) && inflight.get(op.n) === op);
|
|
638
632
|
for (const op of unanswered) inflight.delete(op.n);
|
|
639
633
|
if (again) {
|
|
640
|
-
for (const op of unanswered) if (op.kind === "req")
|
|
634
|
+
for (const op of unanswered) if (op.kind === "req") finish(op, new RealtimeError("unavailable"));
|
|
641
635
|
requeue(ops.filter((op) => op.kind === "pres" || op.kind === "pub" && (!op.ack || unanswered.includes(op))));
|
|
642
636
|
postFails += 1;
|
|
643
637
|
postTimer = setTimeout(() => {
|
|
644
638
|
postTimer = void 0;
|
|
645
639
|
flush();
|
|
646
640
|
}, backoff(postFails - 1, minDelay, maxDelay, random));
|
|
647
|
-
} else for (const op of unanswered)
|
|
641
|
+
} else for (const op of unanswered) finish(op, new RealtimeError("unavailable"));
|
|
648
642
|
flush();
|
|
649
643
|
};
|
|
650
|
-
const needed = () => queue.length > 0 || [...chans.values()].some(wants);
|
|
651
644
|
const run = () => {
|
|
652
645
|
scheduled = false;
|
|
653
646
|
if (finished) return;
|
|
654
647
|
syncAll();
|
|
655
648
|
if (link === void 0) {
|
|
656
|
-
if (reconnectTimer === void 0 &&
|
|
649
|
+
if (reconnectTimer === void 0 && (queue.length > 0 || [...chans.values()].some(wants))) connect();
|
|
657
650
|
return;
|
|
658
651
|
}
|
|
659
|
-
if (mode === "sse" && (signature() !== streamSig || [...chans.values()].some((c) => wants(c) && c.w === void 0 && c.retry === void 0)))
|
|
660
|
-
|
|
652
|
+
if (mode === "sse" && (signature() !== streamSig || [...chans.values()].some((c) => wants(c) && c.w === void 0 && c.retry === void 0))) {
|
|
653
|
+
drop();
|
|
654
|
+
for (const chan of chans.values()) chan.w = void 0;
|
|
655
|
+
connect();
|
|
656
|
+
} else flush();
|
|
661
657
|
};
|
|
662
658
|
function schedule() {
|
|
663
659
|
if (scheduled || finished) return;
|
|
@@ -672,14 +668,17 @@ function createRealtime(options) {
|
|
|
672
668
|
reconnectTimer = void 0;
|
|
673
669
|
postTimer = void 0;
|
|
674
670
|
drop();
|
|
675
|
-
|
|
676
|
-
if (listening
|
|
677
|
-
|
|
671
|
+
batch?.abort();
|
|
672
|
+
if (listening) {
|
|
673
|
+
if (typeof removeEventListener === "function") removeEventListener("online", onOnline);
|
|
674
|
+
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisible);
|
|
675
|
+
listening = false;
|
|
676
|
+
}
|
|
678
677
|
const closed = error ?? new RealtimeError("closed");
|
|
679
678
|
const pending = [...queue.splice(0), ...inflight.values()];
|
|
680
679
|
inflight.clear();
|
|
681
680
|
for (const op of pending) if (op.settle === void 0) clearTimeout(op.timer);
|
|
682
|
-
else
|
|
681
|
+
else finish(op, closed);
|
|
683
682
|
for (const chan of [...chans.values()]) closeChan(chan);
|
|
684
683
|
chans.clear();
|
|
685
684
|
dirty.clear();
|
|
@@ -697,7 +696,6 @@ function createRealtime(options) {
|
|
|
697
696
|
sl: /* @__PURE__ */ new Set(),
|
|
698
697
|
presence: /* @__PURE__ */ new Set(),
|
|
699
698
|
members: /* @__PURE__ */ new Map(),
|
|
700
|
-
list: void 0,
|
|
701
699
|
status: "idle",
|
|
702
700
|
refs: 0,
|
|
703
701
|
hd: 0,
|
|
@@ -705,23 +703,23 @@ function createRealtime(options) {
|
|
|
705
703
|
sy: false,
|
|
706
704
|
state: channelOptions.presence,
|
|
707
705
|
sv: 0,
|
|
708
|
-
w: void 0,
|
|
709
|
-
retry: void 0,
|
|
710
706
|
fails: 0,
|
|
711
|
-
closed: false
|
|
712
|
-
hn: void 0
|
|
707
|
+
closed: false
|
|
713
708
|
};
|
|
714
709
|
chans.set(name, chan);
|
|
715
710
|
if (wants(chan)) touch(chan);
|
|
716
711
|
/** Registers a listener; unless `passive`, it keeps the channel subscribed until removed. */
|
|
717
712
|
const listen = (set, item, passive, after) => {
|
|
718
713
|
if (chan.closed) return () => void 0;
|
|
714
|
+
let owns = !set.has(item);
|
|
719
715
|
set.add(item);
|
|
720
|
-
if (!passive) {
|
|
716
|
+
if (!passive && owns) {
|
|
721
717
|
chan.refs += 1;
|
|
722
718
|
touch(chan);
|
|
723
719
|
}
|
|
724
720
|
return () => {
|
|
721
|
+
if (!owns) return;
|
|
722
|
+
owns = false;
|
|
725
723
|
if (!set.delete(item) || passive) return;
|
|
726
724
|
chan.refs -= 1;
|
|
727
725
|
after?.();
|
|
@@ -839,7 +837,6 @@ function createRealtime(options) {
|
|
|
839
837
|
close: () => shutdown()
|
|
840
838
|
};
|
|
841
839
|
}
|
|
842
|
-
//#endregion
|
|
843
840
|
export { createRealtime };
|
|
844
841
|
|
|
845
842
|
//# sourceMappingURL=core.js.map
|