@shadow-js/realtime 0.0.0-stage → 1.0.0
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 +13 -0
- package/LICENSE +21 -0
- package/README.md +114 -2
- package/dist/dev/adapter.d.ts +75 -0
- package/dist/dev/client/core.d.ts +24 -0
- package/dist/dev/client/core.js +845 -0
- package/dist/dev/client/core.js.map +1 -0
- package/dist/dev/client/index.d.ts +5 -0
- package/dist/dev/client/index.js +3 -0
- package/dist/dev/client/presence.js +23 -0
- package/dist/dev/client/presence.js.map +1 -0
- package/dist/dev/client/sse.js +76 -0
- package/dist/dev/client/sse.js.map +1 -0
- package/dist/dev/client/types.d.ts +256 -0
- package/dist/dev/client/util.js +30 -0
- package/dist/dev/client/util.js.map +1 -0
- package/dist/dev/client/ws.js +39 -0
- package/dist/dev/client/ws.js.map +1 -0
- package/dist/dev/cloudflare/adapter.d.ts +80 -0
- package/dist/dev/cloudflare/adapter.js +347 -0
- package/dist/dev/cloudflare/adapter.js.map +1 -0
- package/dist/dev/cloudflare/core.js +701 -0
- package/dist/dev/cloudflare/core.js.map +1 -0
- package/dist/dev/cloudflare/link.js +397 -0
- package/dist/dev/cloudflare/link.js.map +1 -0
- package/dist/dev/cloudflare/object.d.ts +82 -0
- package/dist/dev/cloudflare/object.js +214 -0
- package/dist/dev/cloudflare/object.js.map +1 -0
- package/dist/dev/cloudflare/types.d.ts +100 -0
- package/dist/dev/cloudflare/wire.js +284 -0
- package/dist/dev/cloudflare/wire.js.map +1 -0
- package/dist/dev/cloudflare.d.ts +4 -0
- package/dist/dev/cloudflare.js +3 -0
- package/dist/dev/errors.d.ts +25 -0
- package/dist/dev/errors.js +24 -0
- package/dist/dev/errors.js.map +1 -0
- package/dist/dev/hooks.d.ts +110 -0
- package/dist/dev/hooks.js +162 -0
- package/dist/dev/hooks.js.map +1 -0
- package/dist/dev/index.d.ts +4 -0
- package/dist/dev/index.js +3 -0
- package/dist/dev/memory.d.ts +33 -0
- package/dist/dev/memory.js +255 -0
- package/dist/dev/memory.js.map +1 -0
- package/dist/dev/protocol.d.ts +211 -0
- package/dist/dev/protocol.js +400 -0
- package/dist/dev/protocol.js.map +1 -0
- package/dist/dev/redis/client.d.ts +139 -0
- package/dist/dev/redis/client.js +146 -0
- package/dist/dev/redis/client.js.map +1 -0
- package/dist/dev/redis/scripts.js +275 -0
- package/dist/dev/redis/scripts.js.map +1 -0
- package/dist/dev/redis/topics.d.ts +8 -0
- package/dist/dev/redis/topics.js +377 -0
- package/dist/dev/redis/topics.js.map +1 -0
- package/dist/dev/redis.d.ts +68 -0
- package/dist/dev/redis.js +422 -0
- package/dist/dev/redis.js.map +1 -0
- package/dist/dev/server/http.d.ts +48 -0
- package/dist/dev/server/http.js +286 -0
- package/dist/dev/server/http.js.map +1 -0
- package/dist/dev/server/hub.d.ts +114 -0
- package/dist/dev/server/hub.js +827 -0
- package/dist/dev/server/hub.js.map +1 -0
- package/dist/dev/server/index.d.ts +47 -0
- package/dist/dev/server/index.js +41 -0
- package/dist/dev/server/index.js.map +1 -0
- package/dist/dev/server/limits.d.ts +54 -0
- package/dist/dev/server/limits.js +85 -0
- package/dist/dev/server/limits.js.map +1 -0
- package/dist/dev/server/rules.d.ts +43 -0
- package/dist/dev/server/rules.js +72 -0
- package/dist/dev/server/rules.js.map +1 -0
- package/dist/dev/testing.d.ts +115 -0
- package/dist/dev/testing.js +427 -0
- package/dist/dev/testing.js.map +1 -0
- package/dist/prod/client/core.js +2 -0
- package/dist/prod/client/core.js.map +1 -0
- package/dist/prod/client/index.js +1 -0
- package/dist/prod/client/presence.js +2 -0
- package/dist/prod/client/presence.js.map +1 -0
- package/dist/prod/client/sse.js +5 -0
- package/dist/prod/client/sse.js.map +1 -0
- package/dist/prod/client/util.js +2 -0
- package/dist/prod/client/util.js.map +1 -0
- package/dist/prod/client/ws.js +2 -0
- package/dist/prod/client/ws.js.map +1 -0
- package/dist/prod/cloudflare/adapter.js +3 -0
- package/dist/prod/cloudflare/adapter.js.map +1 -0
- package/dist/prod/cloudflare/core.js +2 -0
- package/dist/prod/cloudflare/core.js.map +1 -0
- package/dist/prod/cloudflare/link.js +2 -0
- package/dist/prod/cloudflare/link.js.map +1 -0
- package/dist/prod/cloudflare/object.js +2 -0
- package/dist/prod/cloudflare/object.js.map +1 -0
- package/dist/prod/cloudflare/wire.js +2 -0
- package/dist/prod/cloudflare/wire.js.map +1 -0
- package/dist/prod/cloudflare.js +1 -0
- package/dist/prod/errors.js +2 -0
- package/dist/prod/errors.js.map +1 -0
- package/dist/prod/hooks.js +2 -0
- package/dist/prod/hooks.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/memory.js +2 -0
- package/dist/prod/memory.js.map +1 -0
- package/dist/prod/protocol.js +2 -0
- package/dist/prod/protocol.js.map +1 -0
- package/dist/prod/redis/client.js +2 -0
- package/dist/prod/redis/client.js.map +1 -0
- package/dist/prod/redis/scripts.js +157 -0
- package/dist/prod/redis/scripts.js.map +1 -0
- package/dist/prod/redis/topics.js +2 -0
- package/dist/prod/redis/topics.js.map +1 -0
- package/dist/prod/redis.js +2 -0
- package/dist/prod/redis.js.map +1 -0
- package/dist/prod/server/http.js +2 -0
- package/dist/prod/server/http.js.map +1 -0
- package/dist/prod/server/hub.js +2 -0
- package/dist/prod/server/hub.js.map +1 -0
- package/dist/prod/server/index.js +2 -0
- package/dist/prod/server/index.js.map +1 -0
- package/dist/prod/server/limits.js +2 -0
- package/dist/prod/server/limits.js.map +1 -0
- package/dist/prod/server/rules.js +2 -0
- package/dist/prod/server/rules.js.map +1 -0
- package/dist/prod/testing.js +2 -0
- package/dist/prod/testing.js.map +1 -0
- package/package.json +105 -4
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { ServerFrame } from "./protocol.js";
|
|
2
|
+
import { Adapter } from "./adapter.js";
|
|
3
|
+
import { Identity } from "./server/rules.js";
|
|
4
|
+
import { Hub } from "./server/index.js";
|
|
5
|
+
//#region src/testing.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* A connection to a hub with no network: frames go in as objects and come out decoded. For tests of an app's channel rules and
|
|
8
|
+
* request handlers.
|
|
9
|
+
* @since 1.0.0
|
|
10
|
+
*/
|
|
11
|
+
export interface TestConnection {
|
|
12
|
+
/** The connection id the hub assigned. */
|
|
13
|
+
readonly id: string;
|
|
14
|
+
/** Every frame the hub sent, decoded, in order. */
|
|
15
|
+
readonly frames: readonly ServerFrame[];
|
|
16
|
+
/** The code and reason the hub closed the connection with, once it did. */
|
|
17
|
+
readonly closedWith: {
|
|
18
|
+
readonly code: number;
|
|
19
|
+
readonly reason: string;
|
|
20
|
+
} | undefined;
|
|
21
|
+
/** Sends one frame (an object, or raw text to test malformed input). */
|
|
22
|
+
send(frame: Readonly<Record<string, unknown>> | string): void;
|
|
23
|
+
/** Resolves with the first frame (already received or yet to come) that satisfies `match`; rejects after `timeoutMs`. */
|
|
24
|
+
until<T extends ServerFrame>(match: (frame: ServerFrame) => frame is T, timeoutMs?: number): Promise<T>;
|
|
25
|
+
/** Resolves when the hub handled every frame sent so far. */
|
|
26
|
+
settled(): Promise<void>;
|
|
27
|
+
/** Closes the connection as a network drop would. */
|
|
28
|
+
close(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Opens a {@link TestConnection} to `hub`.
|
|
32
|
+
*
|
|
33
|
+
* @param hub The hub.
|
|
34
|
+
* @param identity Who the connection is.
|
|
35
|
+
* @returns The connection (its `ready` frame has been received).
|
|
36
|
+
* @example
|
|
37
|
+
* import { createHub } from "@shadow-js/realtime/server";
|
|
38
|
+
* import { connectTest } from "@shadow-js/realtime/testing";
|
|
39
|
+
*
|
|
40
|
+
* const hub = createHub({ channels: { "chat:{room}": { subscribe: true } } });
|
|
41
|
+
* const client = connectTest(hub);
|
|
42
|
+
* client.send({ t: "sub", n: 1, c: "chat:lobby" });
|
|
43
|
+
* await client.settled();
|
|
44
|
+
* client.frames.some((frame) => frame.t === "subbed"); // true
|
|
45
|
+
* await hub.close();
|
|
46
|
+
* @since 1.0.0
|
|
47
|
+
*/
|
|
48
|
+
export declare function connectTest(hub: Hub, identity?: Identity): TestConnection;
|
|
49
|
+
/**
|
|
50
|
+
* A `WebSocket` constructor whose other end is `hub` in the same process: pass it as the `WebSocket` option of `createRealtime` to
|
|
51
|
+
* test client code and server rules together with no network and no server. The hub is real; only the socket is replaced.
|
|
52
|
+
*
|
|
53
|
+
* @param hub The hub the sockets connect to.
|
|
54
|
+
* @param identity Who the connections are.
|
|
55
|
+
* @returns A drop-in for the global `WebSocket`.
|
|
56
|
+
* @example
|
|
57
|
+
* import { createRealtime } from "@shadow-js/realtime/client";
|
|
58
|
+
* import { createHub } from "@shadow-js/realtime/server";
|
|
59
|
+
* import { memoryWebSocket } from "@shadow-js/realtime/testing";
|
|
60
|
+
*
|
|
61
|
+
* const hub = createHub({ channels: { "chat:{room}": { subscribe: true, publish: true } } });
|
|
62
|
+
* const rt = createRealtime({ url: "/rt", transport: "ws", WebSocket: memoryWebSocket(hub) });
|
|
63
|
+
* rt.close();
|
|
64
|
+
* void hub.close();
|
|
65
|
+
* @since 1.0.0
|
|
66
|
+
*/
|
|
67
|
+
export declare function memoryWebSocket(hub: Hub, identity?: Identity): typeof WebSocket;
|
|
68
|
+
/** The test runner the conformance suite registers its tests with (Vitest's `describe`, `it`, `expect` fit). @since 1.0.0 */
|
|
69
|
+
export interface ConformanceRunner {
|
|
70
|
+
describe(name: string, body: () => void): void;
|
|
71
|
+
it(name: string, body: () => Promise<void>, timeoutMs?: number): void;
|
|
72
|
+
expect(value: unknown): {
|
|
73
|
+
toBe(expected: unknown): void;
|
|
74
|
+
toEqual(expected: unknown): void;
|
|
75
|
+
toBeGreaterThan(expected: number): void;
|
|
76
|
+
toHaveLength(expected: number): void;
|
|
77
|
+
toBeLessThanOrEqual(expected: number): void;
|
|
78
|
+
};
|
|
79
|
+
/** Runs after each test (registers cleanup). */
|
|
80
|
+
afterEach(body: () => Promise<void>): void;
|
|
81
|
+
}
|
|
82
|
+
/** Options of {@link adapterConformance}. @since 1.0.0 */
|
|
83
|
+
export interface ConformanceOptions {
|
|
84
|
+
readonly runner: ConformanceRunner;
|
|
85
|
+
/**
|
|
86
|
+
* Returns an adapter attached to `namespace`. Two calls with the same namespace are two servers sharing one store (for a
|
|
87
|
+
* Redis adapter: two clients on one key prefix); different namespaces share nothing. `now` is not passed: the suite uses
|
|
88
|
+
* short real timeouts.
|
|
89
|
+
*/
|
|
90
|
+
readonly connect: (namespace: string) => Adapter | Promise<Adapter>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Registers the tests every adapter must pass (https://realtime.shadowjs.net/guides/realtime-protocol/): atomic gapless numbering, ordered fan-out
|
|
94
|
+
* between servers, retention, idempotency, and presence with exactly-once leave. Run it against your own adapter.
|
|
95
|
+
*
|
|
96
|
+
* @param options The test runner and a way to open adapters on a shared store.
|
|
97
|
+
* @example
|
|
98
|
+
* import { describe, expect, it, afterEach } from "vitest";
|
|
99
|
+
* import { memoryAdapter } from "@shadow-js/realtime/server";
|
|
100
|
+
* import { adapterConformance } from "@shadow-js/realtime/testing";
|
|
101
|
+
*
|
|
102
|
+
* const stores = new Map<string, ReturnType<typeof memoryAdapter>>();
|
|
103
|
+
* adapterConformance({
|
|
104
|
+
* runner: { describe, it, expect, afterEach },
|
|
105
|
+
* connect: (namespace) => {
|
|
106
|
+
* const found = stores.get(namespace) ?? memoryAdapter();
|
|
107
|
+
* stores.set(namespace, found);
|
|
108
|
+
* return found;
|
|
109
|
+
* },
|
|
110
|
+
* });
|
|
111
|
+
* @since 1.0.0
|
|
112
|
+
*/
|
|
113
|
+
export declare function adapterConformance(options: ConformanceOptions): void;
|
|
114
|
+
//#endregion
|
|
115
|
+
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { decodeServerFrame } from "./protocol.js";
|
|
2
|
+
//#region src/testing.ts
|
|
3
|
+
/**
|
|
4
|
+
* Opens a {@link TestConnection} to `hub`.
|
|
5
|
+
*
|
|
6
|
+
* @param hub The hub.
|
|
7
|
+
* @param identity Who the connection is.
|
|
8
|
+
* @returns The connection (its `ready` frame has been received).
|
|
9
|
+
* @example
|
|
10
|
+
* import { createHub } from "@shadow-js/realtime/server";
|
|
11
|
+
* import { connectTest } from "@shadow-js/realtime/testing";
|
|
12
|
+
*
|
|
13
|
+
* const hub = createHub({ channels: { "chat:{room}": { subscribe: true } } });
|
|
14
|
+
* const client = connectTest(hub);
|
|
15
|
+
* client.send({ t: "sub", n: 1, c: "chat:lobby" });
|
|
16
|
+
* await client.settled();
|
|
17
|
+
* client.frames.some((frame) => frame.t === "subbed"); // true
|
|
18
|
+
* await hub.close();
|
|
19
|
+
* @since 1.0.0
|
|
20
|
+
*/
|
|
21
|
+
function connectTest(hub, identity = {}) {
|
|
22
|
+
const frames = [];
|
|
23
|
+
const watchers = [];
|
|
24
|
+
let closedWith;
|
|
25
|
+
const connection = hub.connect({
|
|
26
|
+
send(text) {
|
|
27
|
+
if (closedWith !== void 0) return false;
|
|
28
|
+
const frame = decodeServerFrame(text);
|
|
29
|
+
if (frame === void 0) return true;
|
|
30
|
+
frames.push(frame);
|
|
31
|
+
for (const watcher of [...watchers]) watcher(frame);
|
|
32
|
+
return true;
|
|
33
|
+
},
|
|
34
|
+
close(code, reason) {
|
|
35
|
+
closedWith = {
|
|
36
|
+
code,
|
|
37
|
+
reason
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}, identity);
|
|
41
|
+
return {
|
|
42
|
+
id: connection.id,
|
|
43
|
+
frames,
|
|
44
|
+
get closedWith() {
|
|
45
|
+
return closedWith;
|
|
46
|
+
},
|
|
47
|
+
send(frame) {
|
|
48
|
+
connection.receive(typeof frame === "string" ? frame : JSON.stringify(frame));
|
|
49
|
+
},
|
|
50
|
+
until(match, timeoutMs = 2e3) {
|
|
51
|
+
const found = frames.find(match);
|
|
52
|
+
if (found !== void 0) return Promise.resolve(found);
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
watchers.splice(watchers.indexOf(watcher), 1);
|
|
56
|
+
reject(/* @__PURE__ */ new Error("no matching frame arrived in time"));
|
|
57
|
+
}, timeoutMs);
|
|
58
|
+
const watcher = (frame) => {
|
|
59
|
+
if (!match(frame)) return;
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
watchers.splice(watchers.indexOf(watcher), 1);
|
|
62
|
+
resolve(frame);
|
|
63
|
+
};
|
|
64
|
+
watchers.push(watcher);
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
settled: () => connection.settled(),
|
|
68
|
+
async close() {
|
|
69
|
+
connection.closed();
|
|
70
|
+
await connection.done;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A `WebSocket` constructor whose other end is `hub` in the same process: pass it as the `WebSocket` option of `createRealtime` to
|
|
76
|
+
* test client code and server rules together with no network and no server. The hub is real; only the socket is replaced.
|
|
77
|
+
*
|
|
78
|
+
* @param hub The hub the sockets connect to.
|
|
79
|
+
* @param identity Who the connections are.
|
|
80
|
+
* @returns A drop-in for the global `WebSocket`.
|
|
81
|
+
* @example
|
|
82
|
+
* import { createRealtime } from "@shadow-js/realtime/client";
|
|
83
|
+
* import { createHub } from "@shadow-js/realtime/server";
|
|
84
|
+
* import { memoryWebSocket } from "@shadow-js/realtime/testing";
|
|
85
|
+
*
|
|
86
|
+
* const hub = createHub({ channels: { "chat:{room}": { subscribe: true, publish: true } } });
|
|
87
|
+
* const rt = createRealtime({ url: "/rt", transport: "ws", WebSocket: memoryWebSocket(hub) });
|
|
88
|
+
* rt.close();
|
|
89
|
+
* void hub.close();
|
|
90
|
+
* @since 1.0.0
|
|
91
|
+
*/
|
|
92
|
+
function memoryWebSocket(hub, identity = {}) {
|
|
93
|
+
class MemorySocket {
|
|
94
|
+
onopen = null;
|
|
95
|
+
onmessage = null;
|
|
96
|
+
onclose = null;
|
|
97
|
+
onerror = null;
|
|
98
|
+
readyState = 0;
|
|
99
|
+
url;
|
|
100
|
+
#connection;
|
|
101
|
+
constructor(url) {
|
|
102
|
+
this.url = url;
|
|
103
|
+
queueMicrotask(() => {
|
|
104
|
+
if (this.readyState !== 0) return;
|
|
105
|
+
this.readyState = 1;
|
|
106
|
+
this.#connection = hub.connect({
|
|
107
|
+
send: (frame) => {
|
|
108
|
+
if (this.readyState !== 1) return false;
|
|
109
|
+
queueMicrotask(() => {
|
|
110
|
+
if (this.readyState === 1) this.onmessage?.(new MessageEvent("message", { data: frame }));
|
|
111
|
+
});
|
|
112
|
+
return true;
|
|
113
|
+
},
|
|
114
|
+
close: (code, reason) => this.#finish(code, reason)
|
|
115
|
+
}, identity);
|
|
116
|
+
this.onopen?.(new Event("open"));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
send(text) {
|
|
120
|
+
if (this.readyState !== 1) throw new DOMException("the socket is not open", "InvalidStateError");
|
|
121
|
+
const connection = this.#connection;
|
|
122
|
+
queueMicrotask(() => connection?.receive(text));
|
|
123
|
+
}
|
|
124
|
+
close(code = 1e3) {
|
|
125
|
+
if (this.readyState < 2) this.#finish(code, "");
|
|
126
|
+
}
|
|
127
|
+
#finish(code, reason) {
|
|
128
|
+
if (this.readyState === 3) return;
|
|
129
|
+
this.readyState = 3;
|
|
130
|
+
this.#connection?.closed();
|
|
131
|
+
const event = {
|
|
132
|
+
type: "close",
|
|
133
|
+
code,
|
|
134
|
+
reason,
|
|
135
|
+
wasClean: code !== 1006
|
|
136
|
+
};
|
|
137
|
+
queueMicrotask(() => this.onclose?.(event));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return MemorySocket;
|
|
141
|
+
}
|
|
142
|
+
const WAIT_STEP_MS = 5;
|
|
143
|
+
async function until(condition, ms = 3e3) {
|
|
144
|
+
const stop = Date.now() + ms;
|
|
145
|
+
while (!condition()) {
|
|
146
|
+
if (Date.now() > stop) throw new Error("the condition did not become true in time");
|
|
147
|
+
await new Promise((resolve) => setTimeout(resolve, WAIT_STEP_MS));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Registers the tests every adapter must pass (https://realtime.shadowjs.net/guides/realtime-protocol/): atomic gapless numbering, ordered fan-out
|
|
152
|
+
* between servers, retention, idempotency, and presence with exactly-once leave. Run it against your own adapter.
|
|
153
|
+
*
|
|
154
|
+
* @param options The test runner and a way to open adapters on a shared store.
|
|
155
|
+
* @example
|
|
156
|
+
* import { describe, expect, it, afterEach } from "vitest";
|
|
157
|
+
* import { memoryAdapter } from "@shadow-js/realtime/server";
|
|
158
|
+
* import { adapterConformance } from "@shadow-js/realtime/testing";
|
|
159
|
+
*
|
|
160
|
+
* const stores = new Map<string, ReturnType<typeof memoryAdapter>>();
|
|
161
|
+
* adapterConformance({
|
|
162
|
+
* runner: { describe, it, expect, afterEach },
|
|
163
|
+
* connect: (namespace) => {
|
|
164
|
+
* const found = stores.get(namespace) ?? memoryAdapter();
|
|
165
|
+
* stores.set(namespace, found);
|
|
166
|
+
* return found;
|
|
167
|
+
* },
|
|
168
|
+
* });
|
|
169
|
+
* @since 1.0.0
|
|
170
|
+
*/
|
|
171
|
+
function adapterConformance(options) {
|
|
172
|
+
const { runner } = options;
|
|
173
|
+
const { describe, it, expect } = runner;
|
|
174
|
+
let counter = 0;
|
|
175
|
+
const opened = [];
|
|
176
|
+
const fresh = () => {
|
|
177
|
+
counter += 1;
|
|
178
|
+
return `conformance-${Date.now().toString(36)}-${counter}`;
|
|
179
|
+
};
|
|
180
|
+
const open = async (namespace) => {
|
|
181
|
+
const adapter = await options.connect(namespace);
|
|
182
|
+
opened.push(adapter);
|
|
183
|
+
return adapter;
|
|
184
|
+
};
|
|
185
|
+
runner.afterEach(async () => {
|
|
186
|
+
await Promise.all(opened.splice(0).map((adapter) => adapter.close().catch(() => void 0)));
|
|
187
|
+
});
|
|
188
|
+
const rules = {
|
|
189
|
+
history: 100,
|
|
190
|
+
historyTtlMs: 6e4,
|
|
191
|
+
idempotencyTtlMs: 6e4
|
|
192
|
+
};
|
|
193
|
+
const entry = (n) => ({
|
|
194
|
+
event: "e",
|
|
195
|
+
data: String(n)
|
|
196
|
+
});
|
|
197
|
+
describe("adapter: messages", () => {
|
|
198
|
+
it("[RT-AD-01] numbers messages 1, 2, 3 per channel, independently", async () => {
|
|
199
|
+
const adapter = await open(fresh());
|
|
200
|
+
const seqs = [];
|
|
201
|
+
for (let at = 0; at < 3; at += 1) seqs.push((await adapter.publish("a", entry(at), rules)).seq);
|
|
202
|
+
expect(seqs).toEqual([
|
|
203
|
+
1,
|
|
204
|
+
2,
|
|
205
|
+
3
|
|
206
|
+
]);
|
|
207
|
+
expect((await adapter.publish("b", entry(0), rules)).seq).toBe(1);
|
|
208
|
+
});
|
|
209
|
+
it("[RT-AD-02] delivers to listeners in seq order, and stops after unsubscribe", async () => {
|
|
210
|
+
const adapter = await open(fresh());
|
|
211
|
+
const seen = [];
|
|
212
|
+
const stop = await adapter.subscribe("a", (message) => seen.push(message.seq));
|
|
213
|
+
for (let at = 0; at < 20; at += 1) await adapter.publish("a", entry(at), rules);
|
|
214
|
+
await until(() => seen.length === 20);
|
|
215
|
+
expect(seen).toEqual(Array.from({ length: 20 }, (_, at) => at + 1));
|
|
216
|
+
await stop();
|
|
217
|
+
await adapter.publish("a", entry(99), rules);
|
|
218
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
219
|
+
expect(seen).toHaveLength(20);
|
|
220
|
+
});
|
|
221
|
+
it("[RT-AD-03] numbers concurrent publishes from two servers without a duplicate or a gap, seen in order on both", async () => {
|
|
222
|
+
const namespace = fresh();
|
|
223
|
+
const first = await open(namespace);
|
|
224
|
+
const second = await open(namespace);
|
|
225
|
+
const seenFirst = [];
|
|
226
|
+
const seenSecond = [];
|
|
227
|
+
await first.subscribe("a", (message) => seenFirst.push(message.seq));
|
|
228
|
+
await second.subscribe("a", (message) => seenSecond.push(message.seq));
|
|
229
|
+
const results = await Promise.all(Array.from({ length: 100 }, (_, at) => (at % 2 === 0 ? first : second).publish("a", entry(at), rules)));
|
|
230
|
+
expect(results.map((result) => result.seq).sort((x, y) => x - y)).toEqual(Array.from({ length: 100 }, (_, at) => at + 1));
|
|
231
|
+
await until(() => seenFirst.length === 100 && seenSecond.length === 100);
|
|
232
|
+
expect(seenFirst).toEqual(Array.from({ length: 100 }, (_, at) => at + 1));
|
|
233
|
+
expect(seenSecond).toEqual(Array.from({ length: 100 }, (_, at) => at + 1));
|
|
234
|
+
});
|
|
235
|
+
it("[RT-AD-04] reads history after a seq with a limit, and reports head and oldest", async () => {
|
|
236
|
+
const adapter = await open(fresh());
|
|
237
|
+
for (let at = 1; at <= 10; at += 1) await adapter.publish("a", entry(at), rules);
|
|
238
|
+
const page = await adapter.history("a", 3, 4);
|
|
239
|
+
expect(page.messages.map((message) => message.seq)).toEqual([
|
|
240
|
+
4,
|
|
241
|
+
5,
|
|
242
|
+
6,
|
|
243
|
+
7
|
|
244
|
+
]);
|
|
245
|
+
expect(page.head).toBe(10);
|
|
246
|
+
expect(page.oldest).toBe(1);
|
|
247
|
+
expect((await adapter.history("nothing", 0, 10)).head).toBe(0);
|
|
248
|
+
expect((await adapter.history("a", Number.MAX_SAFE_INTEGER, 0)).head).toBe(10);
|
|
249
|
+
});
|
|
250
|
+
it("[RT-AD-05] keeps only the newest `history` messages", async () => {
|
|
251
|
+
const adapter = await open(fresh());
|
|
252
|
+
const small = {
|
|
253
|
+
...rules,
|
|
254
|
+
history: 3
|
|
255
|
+
};
|
|
256
|
+
for (let at = 1; at <= 8; at += 1) await adapter.publish("a", entry(at), small);
|
|
257
|
+
const page = await adapter.history("a", 0, 100);
|
|
258
|
+
expect(page.messages.map((message) => message.seq)).toEqual([
|
|
259
|
+
6,
|
|
260
|
+
7,
|
|
261
|
+
8
|
|
262
|
+
]);
|
|
263
|
+
expect(page.oldest).toBe(6);
|
|
264
|
+
expect(page.head).toBe(8);
|
|
265
|
+
});
|
|
266
|
+
it("[RT-AD-06] forgets messages older than historyTtlMs but keeps numbering", async () => {
|
|
267
|
+
const adapter = await open(fresh());
|
|
268
|
+
const short = {
|
|
269
|
+
...rules,
|
|
270
|
+
historyTtlMs: 400
|
|
271
|
+
};
|
|
272
|
+
await adapter.publish("a", entry(1), short);
|
|
273
|
+
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
274
|
+
await adapter.publish("a", entry(2), short);
|
|
275
|
+
const page = await adapter.history("a", 0, 100);
|
|
276
|
+
expect(page.messages.map((message) => message.seq)).toEqual([2]);
|
|
277
|
+
expect(page.head).toBe(2);
|
|
278
|
+
});
|
|
279
|
+
it("[RT-AD-07] with history 0 retains nothing but still numbers", async () => {
|
|
280
|
+
const adapter = await open(fresh());
|
|
281
|
+
const none = {
|
|
282
|
+
...rules,
|
|
283
|
+
history: 0
|
|
284
|
+
};
|
|
285
|
+
expect((await adapter.publish("a", entry(1), none)).seq).toBe(1);
|
|
286
|
+
expect((await adapter.publish("a", entry(2), none)).seq).toBe(2);
|
|
287
|
+
const page = await adapter.history("a", 0, 10);
|
|
288
|
+
expect(page.messages).toHaveLength(0);
|
|
289
|
+
expect(page.head).toBe(2);
|
|
290
|
+
expect(page.oldest).toBe(0);
|
|
291
|
+
});
|
|
292
|
+
it("[RT-AD-08] accepts a publish with a known idempotency key once, across servers, until the key expires", async () => {
|
|
293
|
+
const namespace = fresh();
|
|
294
|
+
const first = await open(namespace);
|
|
295
|
+
const second = await open(namespace);
|
|
296
|
+
const keyed = {
|
|
297
|
+
...rules,
|
|
298
|
+
key: "k1",
|
|
299
|
+
idempotencyTtlMs: 400
|
|
300
|
+
};
|
|
301
|
+
const one = await first.publish("a", entry(1), keyed);
|
|
302
|
+
const two = await second.publish("a", entry(2), keyed);
|
|
303
|
+
expect(one).toEqual({
|
|
304
|
+
seq: 1,
|
|
305
|
+
duplicate: false
|
|
306
|
+
});
|
|
307
|
+
expect(two).toEqual({
|
|
308
|
+
seq: 1,
|
|
309
|
+
duplicate: true
|
|
310
|
+
});
|
|
311
|
+
expect((await first.history("a", 0, 10)).messages).toHaveLength(1);
|
|
312
|
+
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
313
|
+
expect((await first.publish("a", entry(3), keyed)).duplicate).toBe(false);
|
|
314
|
+
});
|
|
315
|
+
it("[RT-AD-09] gives two racing publishes with one key a single seq", async () => {
|
|
316
|
+
const namespace = fresh();
|
|
317
|
+
const first = await open(namespace);
|
|
318
|
+
const second = await open(namespace);
|
|
319
|
+
const keyed = {
|
|
320
|
+
...rules,
|
|
321
|
+
key: "race"
|
|
322
|
+
};
|
|
323
|
+
const results = await Promise.all([first.publish("a", entry(1), keyed), second.publish("a", entry(2), keyed)]);
|
|
324
|
+
expect(results.filter((result) => result.duplicate)).toHaveLength(1);
|
|
325
|
+
expect(results[0]?.seq).toBe(results[1]?.seq);
|
|
326
|
+
});
|
|
327
|
+
it("[RT-AD-10] isolates namespaces", async () => {
|
|
328
|
+
const one = await open(fresh());
|
|
329
|
+
const two = await open(fresh());
|
|
330
|
+
await one.publish("a", entry(1), rules);
|
|
331
|
+
expect((await two.history("a", 0, 10)).head).toBe(0);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
describe("adapter: presence", () => {
|
|
335
|
+
const member = (id, v = 1, s = null) => ({
|
|
336
|
+
id,
|
|
337
|
+
v,
|
|
338
|
+
s
|
|
339
|
+
});
|
|
340
|
+
it("[RT-AD-20] lists, updates and removes members and announces join, set and leave to every server", async () => {
|
|
341
|
+
const namespace = fresh();
|
|
342
|
+
const first = await open(namespace);
|
|
343
|
+
const second = await open(namespace);
|
|
344
|
+
const events = [];
|
|
345
|
+
await second.presenceSubscribe("room", (event) => events.push(`${event.op}:${event.member.id}:${event.member.v}`));
|
|
346
|
+
await first.presenceSet("room", member("m1"), 5e3);
|
|
347
|
+
await first.presenceSet("room", member("m1", 2, { x: 1 }), 5e3);
|
|
348
|
+
await first.presenceSet("room", member("m2"), 5e3);
|
|
349
|
+
expect((await second.presenceList("room")).map((item) => item.id).sort()).toEqual(["m1", "m2"]);
|
|
350
|
+
expect(await first.presenceRemove("room", "m1")).toBe(true);
|
|
351
|
+
await until(() => events.length === 4);
|
|
352
|
+
expect(events).toEqual([
|
|
353
|
+
"join:m1:1",
|
|
354
|
+
"set:m1:2",
|
|
355
|
+
"join:m2:1",
|
|
356
|
+
"leave:m1:2"
|
|
357
|
+
]);
|
|
358
|
+
});
|
|
359
|
+
it("[RT-AD-21] ignores an older version of a member", async () => {
|
|
360
|
+
const adapter = await open(fresh());
|
|
361
|
+
await adapter.presenceSet("room", member("m1", 5, "new"), 5e3);
|
|
362
|
+
await adapter.presenceSet("room", member("m1", 3, "old"), 5e3);
|
|
363
|
+
expect((await adapter.presenceList("room"))[0]).toEqual(member("m1", 5, "new"));
|
|
364
|
+
});
|
|
365
|
+
it("[RT-AD-22] removes a member once even when two servers race to remove it", async () => {
|
|
366
|
+
const namespace = fresh();
|
|
367
|
+
const first = await open(namespace);
|
|
368
|
+
const second = await open(namespace);
|
|
369
|
+
const leaves = [];
|
|
370
|
+
await first.presenceSubscribe("room", (event) => event.op === "leave" && leaves.push(event.member.id));
|
|
371
|
+
await first.presenceSet("room", member("m1"), 5e3);
|
|
372
|
+
const results = await Promise.all([first.presenceRemove("room", "m1"), second.presenceRemove("room", "m1")]);
|
|
373
|
+
expect(results.filter(Boolean)).toHaveLength(1);
|
|
374
|
+
await until(() => leaves.length >= 1);
|
|
375
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
376
|
+
expect(leaves).toEqual(["m1"]);
|
|
377
|
+
});
|
|
378
|
+
it("[RT-AD-23] expires members without a heartbeat, and reap announces each leave once across servers", async () => {
|
|
379
|
+
const namespace = fresh();
|
|
380
|
+
const first = await open(namespace);
|
|
381
|
+
const second = await open(namespace);
|
|
382
|
+
const leaves = [];
|
|
383
|
+
await first.presenceSubscribe("room", (event) => event.op === "leave" && leaves.push(event.member.id));
|
|
384
|
+
await first.presenceSet("room", member("gone"), 60);
|
|
385
|
+
await first.presenceSet("room", member("stays"), 5e3);
|
|
386
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
387
|
+
expect((await first.presenceList("room")).map((item) => item.id)).toEqual(["stays"]);
|
|
388
|
+
const reaped = await Promise.all([first.presenceReap("room"), second.presenceReap("room")]);
|
|
389
|
+
expect(reaped.flat().map((item) => item.id)).toEqual(["gone"]);
|
|
390
|
+
await until(() => leaves.length >= 1);
|
|
391
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
392
|
+
expect(leaves).toEqual(["gone"]);
|
|
393
|
+
});
|
|
394
|
+
it("[RT-AD-24] touch extends the life of members and reports the ones that no longer exist", async () => {
|
|
395
|
+
const adapter = await open(fresh());
|
|
396
|
+
await adapter.presenceSet("room", member("m1"), 80);
|
|
397
|
+
const missing = await adapter.presenceTouch([{
|
|
398
|
+
channel: "room",
|
|
399
|
+
id: "m1"
|
|
400
|
+
}, {
|
|
401
|
+
channel: "room",
|
|
402
|
+
id: "ghost"
|
|
403
|
+
}], 5e3);
|
|
404
|
+
expect(missing).toEqual([{
|
|
405
|
+
channel: "room",
|
|
406
|
+
id: "ghost"
|
|
407
|
+
}]);
|
|
408
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
409
|
+
expect((await adapter.presenceList("room")).map((item) => item.id)).toEqual(["m1"]);
|
|
410
|
+
});
|
|
411
|
+
it("[RT-AD-25] stops presence events after unsubscribe", async () => {
|
|
412
|
+
const adapter = await open(fresh());
|
|
413
|
+
const events = [];
|
|
414
|
+
const stop = await adapter.presenceSubscribe("room", (event) => events.push(event.op));
|
|
415
|
+
await adapter.presenceSet("room", member("m1"), 5e3);
|
|
416
|
+
await until(() => events.length === 1);
|
|
417
|
+
await stop();
|
|
418
|
+
await adapter.presenceSet("room", member("m2"), 5e3);
|
|
419
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
420
|
+
expect(events).toEqual(["join"]);
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
425
|
+
export { adapterConformance, connectTest, memoryWebSocket };
|
|
426
|
+
|
|
427
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","names":[],"sources":["../../src/testing.ts"],"sourcesContent":["import type { Adapter, PublishOptions } from \"./adapter\";\nimport {\n decodeServerFrame,\n type Json,\n type Member,\n type ServerFrame,\n type StoredEntry,\n} from \"./protocol\";\nimport type { Hub } from \"./server/index\";\nimport type { Identity } from \"./server/rules\";\n\n/**\n * A connection to a hub with no network: frames go in as objects and come out decoded. For tests of an app's channel rules and\n * request handlers.\n * @since 1.0.0\n */\nexport interface TestConnection {\n /** The connection id the hub assigned. */\n readonly id: string;\n /** Every frame the hub sent, decoded, in order. */\n readonly frames: readonly ServerFrame[];\n /** The code and reason the hub closed the connection with, once it did. */\n readonly closedWith: { readonly code: number; readonly reason: string } | undefined;\n /** Sends one frame (an object, or raw text to test malformed input). */\n send(frame: Readonly<Record<string, unknown>> | string): void;\n /** Resolves with the first frame (already received or yet to come) that satisfies `match`; rejects after `timeoutMs`. */\n until<T extends ServerFrame>(\n match: (frame: ServerFrame) => frame is T,\n timeoutMs?: number,\n ): Promise<T>;\n /** Resolves when the hub handled every frame sent so far. */\n settled(): Promise<void>;\n /** Closes the connection as a network drop would. */\n close(): Promise<void>;\n}\n\n/**\n * Opens a {@link TestConnection} to `hub`.\n *\n * @param hub The hub.\n * @param identity Who the connection is.\n * @returns The connection (its `ready` frame has been received).\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n * import { connectTest } from \"@shadow-js/realtime/testing\";\n *\n * const hub = createHub({ channels: { \"chat:{room}\": { subscribe: true } } });\n * const client = connectTest(hub);\n * client.send({ t: \"sub\", n: 1, c: \"chat:lobby\" });\n * await client.settled();\n * client.frames.some((frame) => frame.t === \"subbed\"); // true\n * await hub.close();\n * @since 1.0.0\n */\nexport function connectTest(hub: Hub, identity: Identity = {}): TestConnection {\n const frames: ServerFrame[] = [];\n const watchers: ((frame: ServerFrame) => void)[] = [];\n let closedWith: { code: number; reason: string } | undefined;\n const connection = hub.connect(\n {\n send(text) {\n if (closedWith !== undefined) return false;\n const frame = decodeServerFrame(text);\n if (frame === undefined) return true;\n frames.push(frame);\n for (const watcher of [...watchers]) watcher(frame);\n return true;\n },\n close(code, reason) {\n closedWith = { code, reason };\n },\n },\n identity,\n );\n return {\n id: connection.id,\n frames,\n get closedWith() {\n return closedWith;\n },\n send(frame) {\n connection.receive(typeof frame === \"string\" ? frame : JSON.stringify(frame));\n },\n until<T extends ServerFrame>(match: (frame: ServerFrame) => frame is T, timeoutMs = 2000) {\n const found = frames.find(match);\n if (found !== undefined) return Promise.resolve(found);\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n watchers.splice(watchers.indexOf(watcher), 1);\n reject(new Error(\"no matching frame arrived in time\"));\n }, timeoutMs);\n const watcher = (frame: ServerFrame): void => {\n if (!match(frame)) return;\n clearTimeout(timer);\n watchers.splice(watchers.indexOf(watcher), 1);\n resolve(frame);\n };\n watchers.push(watcher);\n });\n },\n settled: () => connection.settled(),\n async close() {\n connection.closed();\n await connection.done;\n },\n };\n}\n\n/**\n * A `WebSocket` constructor whose other end is `hub` in the same process: pass it as the `WebSocket` option of `createRealtime` to\n * test client code and server rules together with no network and no server. The hub is real; only the socket is replaced.\n *\n * @param hub The hub the sockets connect to.\n * @param identity Who the connections are.\n * @returns A drop-in for the global `WebSocket`.\n * @example\n * import { createRealtime } from \"@shadow-js/realtime/client\";\n * import { createHub } from \"@shadow-js/realtime/server\";\n * import { memoryWebSocket } from \"@shadow-js/realtime/testing\";\n *\n * const hub = createHub({ channels: { \"chat:{room}\": { subscribe: true, publish: true } } });\n * const rt = createRealtime({ url: \"/rt\", transport: \"ws\", WebSocket: memoryWebSocket(hub) });\n * rt.close();\n * void hub.close();\n * @since 1.0.0\n */\nexport function memoryWebSocket(hub: Hub, identity: Identity = {}): typeof WebSocket {\n class MemorySocket {\n onopen: ((event: Event) => void) | null = null;\n onmessage: ((event: MessageEvent) => void) | null = null;\n onclose: ((event: CloseEvent) => void) | null = null;\n onerror: ((event: Event) => void) | null = null;\n readyState = 0;\n readonly url: string;\n #connection: ReturnType<Hub[\"connect\"]> | undefined;\n\n constructor(url: string) {\n this.url = url;\n queueMicrotask(() => {\n if (this.readyState !== 0) return;\n this.readyState = 1;\n this.#connection = hub.connect(\n {\n send: (frame) => {\n if (this.readyState !== 1) return false;\n queueMicrotask(() => {\n if (this.readyState === 1)\n this.onmessage?.(new MessageEvent(\"message\", { data: frame }));\n });\n return true;\n },\n close: (code, reason) => this.#finish(code, reason),\n },\n identity,\n );\n this.onopen?.(new Event(\"open\"));\n });\n }\n\n send(text: string): void {\n if (this.readyState !== 1)\n throw new DOMException(\"the socket is not open\", \"InvalidStateError\");\n const connection = this.#connection;\n queueMicrotask(() => connection?.receive(text));\n }\n\n close(code = 1000): void {\n if (this.readyState < 2) this.#finish(code, \"\");\n }\n\n #finish(code: number, reason: string): void {\n if (this.readyState === 3) return;\n this.readyState = 3;\n this.#connection?.closed();\n const event = { type: \"close\", code, reason, wasClean: code !== 1006 } as CloseEvent;\n queueMicrotask(() => this.onclose?.(event));\n }\n }\n return MemorySocket as unknown as typeof WebSocket;\n}\n\n/** The test runner the conformance suite registers its tests with (Vitest's `describe`, `it`, `expect` fit). @since 1.0.0 */\nexport interface ConformanceRunner {\n describe(name: string, body: () => void): void;\n it(name: string, body: () => Promise<void>, timeoutMs?: number): void;\n expect(value: unknown): {\n toBe(expected: unknown): void;\n toEqual(expected: unknown): void;\n toBeGreaterThan(expected: number): void;\n toHaveLength(expected: number): void;\n toBeLessThanOrEqual(expected: number): void;\n };\n /** Runs after each test (registers cleanup). */\n afterEach(body: () => Promise<void>): void;\n}\n\n/** Options of {@link adapterConformance}. @since 1.0.0 */\nexport interface ConformanceOptions {\n readonly runner: ConformanceRunner;\n /**\n * Returns an adapter attached to `namespace`. Two calls with the same namespace are two servers sharing one store (for a\n * Redis adapter: two clients on one key prefix); different namespaces share nothing. `now` is not passed: the suite uses\n * short real timeouts.\n */\n readonly connect: (namespace: string) => Adapter | Promise<Adapter>;\n}\n\nconst WAIT_STEP_MS = 5;\n\nasync function until(condition: () => boolean, ms = 3000): Promise<void> {\n const stop = Date.now() + ms;\n while (!condition()) {\n if (Date.now() > stop) throw new Error(\"the condition did not become true in time\");\n await new Promise((resolve) => setTimeout(resolve, WAIT_STEP_MS));\n }\n}\n\n/**\n * Registers the tests every adapter must pass (https://realtime.shadowjs.net/guides/realtime-protocol/): atomic gapless numbering, ordered fan-out\n * between servers, retention, idempotency, and presence with exactly-once leave. Run it against your own adapter.\n *\n * @param options The test runner and a way to open adapters on a shared store.\n * @example\n * import { describe, expect, it, afterEach } from \"vitest\";\n * import { memoryAdapter } from \"@shadow-js/realtime/server\";\n * import { adapterConformance } from \"@shadow-js/realtime/testing\";\n *\n * const stores = new Map<string, ReturnType<typeof memoryAdapter>>();\n * adapterConformance({\n * runner: { describe, it, expect, afterEach },\n * connect: (namespace) => {\n * const found = stores.get(namespace) ?? memoryAdapter();\n * stores.set(namespace, found);\n * return found;\n * },\n * });\n * @since 1.0.0\n */\nexport function adapterConformance(options: ConformanceOptions): void {\n const { runner } = options;\n const { describe, it, expect } = runner;\n let counter = 0;\n const opened: Adapter[] = [];\n const fresh = (): string => {\n counter += 1;\n return `conformance-${Date.now().toString(36)}-${counter}`;\n };\n const open = async (namespace: string): Promise<Adapter> => {\n const adapter = await options.connect(namespace);\n opened.push(adapter);\n return adapter;\n };\n runner.afterEach(async () => {\n await Promise.all(opened.splice(0).map((adapter) => adapter.close().catch(() => undefined)));\n });\n const rules: PublishOptions = { history: 100, historyTtlMs: 60_000, idempotencyTtlMs: 60_000 };\n const entry = (n: number): StoredEntry => ({ event: \"e\", data: String(n) });\n\n describe(\"adapter: messages\", () => {\n it(\"[RT-AD-01] numbers messages 1, 2, 3 per channel, independently\", async () => {\n const adapter = await open(fresh());\n const seqs: number[] = [];\n for (let at = 0; at < 3; at += 1)\n seqs.push((await adapter.publish(\"a\", entry(at), rules)).seq);\n expect(seqs).toEqual([1, 2, 3]);\n expect((await adapter.publish(\"b\", entry(0), rules)).seq).toBe(1);\n });\n\n it(\"[RT-AD-02] delivers to listeners in seq order, and stops after unsubscribe\", async () => {\n const adapter = await open(fresh());\n const seen: number[] = [];\n const stop = await adapter.subscribe(\"a\", (message) => seen.push(message.seq));\n for (let at = 0; at < 20; at += 1) await adapter.publish(\"a\", entry(at), rules);\n await until(() => seen.length === 20);\n expect(seen).toEqual(Array.from({ length: 20 }, (_, at) => at + 1));\n await stop();\n await adapter.publish(\"a\", entry(99), rules);\n await new Promise((resolve) => setTimeout(resolve, 50));\n expect(seen).toHaveLength(20);\n });\n\n it(\"[RT-AD-03] numbers concurrent publishes from two servers without a duplicate or a gap, seen in order on both\", async () => {\n const namespace = fresh();\n const first = await open(namespace);\n const second = await open(namespace);\n const seenFirst: number[] = [];\n const seenSecond: number[] = [];\n await first.subscribe(\"a\", (message) => seenFirst.push(message.seq));\n await second.subscribe(\"a\", (message) => seenSecond.push(message.seq));\n const results = await Promise.all(\n Array.from({ length: 100 }, (_, at) =>\n (at % 2 === 0 ? first : second).publish(\"a\", entry(at), rules),\n ),\n );\n expect(results.map((result) => result.seq).sort((x, y) => x - y)).toEqual(\n Array.from({ length: 100 }, (_, at) => at + 1),\n );\n await until(() => seenFirst.length === 100 && seenSecond.length === 100);\n expect(seenFirst).toEqual(Array.from({ length: 100 }, (_, at) => at + 1));\n expect(seenSecond).toEqual(Array.from({ length: 100 }, (_, at) => at + 1));\n });\n\n it(\"[RT-AD-04] reads history after a seq with a limit, and reports head and oldest\", async () => {\n const adapter = await open(fresh());\n for (let at = 1; at <= 10; at += 1) await adapter.publish(\"a\", entry(at), rules);\n const page = await adapter.history(\"a\", 3, 4);\n expect(page.messages.map((message) => message.seq)).toEqual([4, 5, 6, 7]);\n expect(page.head).toBe(10);\n expect(page.oldest).toBe(1);\n expect((await adapter.history(\"nothing\", 0, 10)).head).toBe(0);\n expect((await adapter.history(\"a\", Number.MAX_SAFE_INTEGER, 0)).head).toBe(10);\n });\n\n it(\"[RT-AD-05] keeps only the newest `history` messages\", async () => {\n const adapter = await open(fresh());\n const small: PublishOptions = { ...rules, history: 3 };\n for (let at = 1; at <= 8; at += 1) await adapter.publish(\"a\", entry(at), small);\n const page = await adapter.history(\"a\", 0, 100);\n expect(page.messages.map((message) => message.seq)).toEqual([6, 7, 8]);\n expect(page.oldest).toBe(6);\n expect(page.head).toBe(8);\n });\n\n it(\"[RT-AD-06] forgets messages older than historyTtlMs but keeps numbering\", async () => {\n const adapter = await open(fresh());\n // 400 ms: an adapter over a network (Redis, a Durable Object in another location) takes milliseconds per call, which a shorter lifetime\n // would count against the message it has just stored.\n const short: PublishOptions = { ...rules, historyTtlMs: 400 };\n await adapter.publish(\"a\", entry(1), short);\n await new Promise((resolve) => setTimeout(resolve, 700));\n await adapter.publish(\"a\", entry(2), short);\n const page = await adapter.history(\"a\", 0, 100);\n expect(page.messages.map((message) => message.seq)).toEqual([2]);\n expect(page.head).toBe(2);\n });\n\n it(\"[RT-AD-07] with history 0 retains nothing but still numbers\", async () => {\n const adapter = await open(fresh());\n const none: PublishOptions = { ...rules, history: 0 };\n expect((await adapter.publish(\"a\", entry(1), none)).seq).toBe(1);\n expect((await adapter.publish(\"a\", entry(2), none)).seq).toBe(2);\n const page = await adapter.history(\"a\", 0, 10);\n expect(page.messages).toHaveLength(0);\n expect(page.head).toBe(2);\n expect(page.oldest).toBe(0);\n });\n\n it(\"[RT-AD-08] accepts a publish with a known idempotency key once, across servers, until the key expires\", async () => {\n const namespace = fresh();\n const first = await open(namespace);\n const second = await open(namespace);\n const keyed: PublishOptions = { ...rules, key: \"k1\", idempotencyTtlMs: 400 };\n const one = await first.publish(\"a\", entry(1), keyed);\n const two = await second.publish(\"a\", entry(2), keyed);\n expect(one).toEqual({ seq: 1, duplicate: false });\n expect(two).toEqual({ seq: 1, duplicate: true });\n expect((await first.history(\"a\", 0, 10)).messages).toHaveLength(1);\n await new Promise((resolve) => setTimeout(resolve, 700));\n expect((await first.publish(\"a\", entry(3), keyed)).duplicate).toBe(false);\n });\n\n it(\"[RT-AD-09] gives two racing publishes with one key a single seq\", async () => {\n const namespace = fresh();\n const first = await open(namespace);\n const second = await open(namespace);\n const keyed: PublishOptions = { ...rules, key: \"race\" };\n const results = await Promise.all([\n first.publish(\"a\", entry(1), keyed),\n second.publish(\"a\", entry(2), keyed),\n ]);\n expect(results.filter((result) => result.duplicate)).toHaveLength(1);\n expect(results[0]?.seq).toBe(results[1]?.seq);\n });\n\n it(\"[RT-AD-10] isolates namespaces\", async () => {\n const one = await open(fresh());\n const two = await open(fresh());\n await one.publish(\"a\", entry(1), rules);\n expect((await two.history(\"a\", 0, 10)).head).toBe(0);\n });\n });\n\n describe(\"adapter: presence\", () => {\n const member = (id: string, v = 1, s: Json = null): Member => ({ id, v, s });\n\n it(\"[RT-AD-20] lists, updates and removes members and announces join, set and leave to every server\", async () => {\n const namespace = fresh();\n const first = await open(namespace);\n const second = await open(namespace);\n const events: string[] = [];\n await second.presenceSubscribe(\"room\", (event) =>\n events.push(`${event.op}:${event.member.id}:${event.member.v}`),\n );\n await first.presenceSet(\"room\", member(\"m1\"), 5000);\n await first.presenceSet(\"room\", member(\"m1\", 2, { x: 1 }), 5000);\n await first.presenceSet(\"room\", member(\"m2\"), 5000);\n expect((await second.presenceList(\"room\")).map((item) => item.id).sort()).toEqual([\n \"m1\",\n \"m2\",\n ]);\n expect(await first.presenceRemove(\"room\", \"m1\")).toBe(true);\n await until(() => events.length === 4);\n expect(events).toEqual([\"join:m1:1\", \"set:m1:2\", \"join:m2:1\", \"leave:m1:2\"]);\n });\n\n it(\"[RT-AD-21] ignores an older version of a member\", async () => {\n const adapter = await open(fresh());\n await adapter.presenceSet(\"room\", member(\"m1\", 5, \"new\"), 5000);\n await adapter.presenceSet(\"room\", member(\"m1\", 3, \"old\"), 5000);\n expect((await adapter.presenceList(\"room\"))[0]).toEqual(member(\"m1\", 5, \"new\"));\n });\n\n it(\"[RT-AD-22] removes a member once even when two servers race to remove it\", async () => {\n const namespace = fresh();\n const first = await open(namespace);\n const second = await open(namespace);\n const leaves: string[] = [];\n await first.presenceSubscribe(\n \"room\",\n (event) => event.op === \"leave\" && leaves.push(event.member.id),\n );\n await first.presenceSet(\"room\", member(\"m1\"), 5000);\n const results = await Promise.all([\n first.presenceRemove(\"room\", \"m1\"),\n second.presenceRemove(\"room\", \"m1\"),\n ]);\n expect(results.filter(Boolean)).toHaveLength(1);\n await until(() => leaves.length >= 1);\n await new Promise((resolve) => setTimeout(resolve, 40));\n expect(leaves).toEqual([\"m1\"]);\n });\n\n it(\"[RT-AD-23] expires members without a heartbeat, and reap announces each leave once across servers\", async () => {\n const namespace = fresh();\n const first = await open(namespace);\n const second = await open(namespace);\n const leaves: string[] = [];\n await first.presenceSubscribe(\n \"room\",\n (event) => event.op === \"leave\" && leaves.push(event.member.id),\n );\n await first.presenceSet(\"room\", member(\"gone\"), 60);\n await first.presenceSet(\"room\", member(\"stays\"), 5000);\n await new Promise((resolve) => setTimeout(resolve, 120));\n expect((await first.presenceList(\"room\")).map((item) => item.id)).toEqual([\"stays\"]);\n const reaped = await Promise.all([first.presenceReap(\"room\"), second.presenceReap(\"room\")]);\n expect(reaped.flat().map((item) => item.id)).toEqual([\"gone\"]);\n await until(() => leaves.length >= 1);\n await new Promise((resolve) => setTimeout(resolve, 40));\n expect(leaves).toEqual([\"gone\"]);\n });\n\n it(\"[RT-AD-24] touch extends the life of members and reports the ones that no longer exist\", async () => {\n const adapter = await open(fresh());\n await adapter.presenceSet(\"room\", member(\"m1\"), 80);\n const missing = await adapter.presenceTouch(\n [\n { channel: \"room\", id: \"m1\" },\n { channel: \"room\", id: \"ghost\" },\n ],\n 5000,\n );\n expect(missing).toEqual([{ channel: \"room\", id: \"ghost\" }]);\n await new Promise((resolve) => setTimeout(resolve, 150));\n expect((await adapter.presenceList(\"room\")).map((item) => item.id)).toEqual([\"m1\"]);\n });\n\n it(\"[RT-AD-25] stops presence events after unsubscribe\", async () => {\n const adapter = await open(fresh());\n const events: string[] = [];\n const stop = await adapter.presenceSubscribe(\"room\", (event) => events.push(event.op));\n await adapter.presenceSet(\"room\", member(\"m1\"), 5000);\n await until(() => events.length === 1);\n await stop();\n await adapter.presenceSet(\"room\", member(\"m2\"), 5000);\n await new Promise((resolve) => setTimeout(resolve, 50));\n expect(events).toEqual([\"join\"]);\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAsDA,SAAgB,YAAY,KAAU,WAAqB,CAAC,GAAmB;CAC7E,MAAM,SAAwB,CAAC;CAC/B,MAAM,WAA6C,CAAC;CACpD,IAAI;CACJ,MAAM,aAAa,IAAI,QACrB;EACE,KAAK,MAAM;GACT,IAAI,eAAe,KAAA,GAAW,OAAO;GACrC,MAAM,QAAQ,kBAAkB,IAAI;GACpC,IAAI,UAAU,KAAA,GAAW,OAAO;GAChC,OAAO,KAAK,KAAK;GACjB,KAAK,MAAM,WAAW,CAAC,GAAG,QAAQ,GAAG,QAAQ,KAAK;GAClD,OAAO;EACT;EACA,MAAM,MAAM,QAAQ;GAClB,aAAa;IAAE;IAAM;GAAO;EAC9B;CACF,GACA,QACF;CACA,OAAO;EACL,IAAI,WAAW;EACf;EACA,IAAI,aAAa;GACf,OAAO;EACT;EACA,KAAK,OAAO;GACV,WAAW,QAAQ,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK,CAAC;EAC9E;EACA,MAA6B,OAA2C,YAAY,KAAM;GACxF,MAAM,QAAQ,OAAO,KAAK,KAAK;GAC/B,IAAI,UAAU,KAAA,GAAW,OAAO,QAAQ,QAAQ,KAAK;GACrD,OAAO,IAAI,SAAY,SAAS,WAAW;IACzC,MAAM,QAAQ,iBAAiB;KAC7B,SAAS,OAAO,SAAS,QAAQ,OAAO,GAAG,CAAC;KAC5C,uBAAO,IAAI,MAAM,mCAAmC,CAAC;IACvD,GAAG,SAAS;IACZ,MAAM,WAAW,UAA6B;KAC5C,IAAI,CAAC,MAAM,KAAK,GAAG;KACnB,aAAa,KAAK;KAClB,SAAS,OAAO,SAAS,QAAQ,OAAO,GAAG,CAAC;KAC5C,QAAQ,KAAK;IACf;IACA,SAAS,KAAK,OAAO;GACvB,CAAC;EACH;EACA,eAAe,WAAW,QAAQ;EAClC,MAAM,QAAQ;GACZ,WAAW,OAAO;GAClB,MAAM,WAAW;EACnB;CACF;AACF;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,gBAAgB,KAAU,WAAqB,CAAC,GAAqB;CACnF,MAAM,aAAa;EACjB,SAA0C;EAC1C,YAAoD;EACpD,UAAgD;EAChD,UAA2C;EAC3C,aAAa;EACb;EACA;EAEA,YAAY,KAAa;GACvB,KAAK,MAAM;GACX,qBAAqB;IACnB,IAAI,KAAK,eAAe,GAAG;IAC3B,KAAK,aAAa;IAClB,KAAK,cAAc,IAAI,QACrB;KACE,OAAO,UAAU;MACf,IAAI,KAAK,eAAe,GAAG,OAAO;MAClC,qBAAqB;OACnB,IAAI,KAAK,eAAe,GACtB,KAAK,YAAY,IAAI,aAAa,WAAW,EAAE,MAAM,MAAM,CAAC,CAAC;MACjE,CAAC;MACD,OAAO;KACT;KACA,QAAQ,MAAM,WAAW,KAAK,QAAQ,MAAM,MAAM;IACpD,GACA,QACF;IACA,KAAK,SAAS,IAAI,MAAM,MAAM,CAAC;GACjC,CAAC;EACH;EAEA,KAAK,MAAoB;GACvB,IAAI,KAAK,eAAe,GACtB,MAAM,IAAI,aAAa,0BAA0B,mBAAmB;GACtE,MAAM,aAAa,KAAK;GACxB,qBAAqB,YAAY,QAAQ,IAAI,CAAC;EAChD;EAEA,MAAM,OAAO,KAAY;GACvB,IAAI,KAAK,aAAa,GAAG,KAAK,QAAQ,MAAM,EAAE;EAChD;EAEA,QAAQ,MAAc,QAAsB;GAC1C,IAAI,KAAK,eAAe,GAAG;GAC3B,KAAK,aAAa;GAClB,KAAK,aAAa,OAAO;GACzB,MAAM,QAAQ;IAAE,MAAM;IAAS;IAAM;IAAQ,UAAU,SAAS;GAAK;GACrE,qBAAqB,KAAK,UAAU,KAAK,CAAC;EAC5C;CACF;CACA,OAAO;AACT;AA4BA,MAAM,eAAe;AAErB,eAAe,MAAM,WAA0B,KAAK,KAAqB;CACvE,MAAM,OAAO,KAAK,IAAI,IAAI;CAC1B,OAAO,CAAC,UAAU,GAAG;EACnB,IAAI,KAAK,IAAI,IAAI,MAAM,MAAM,IAAI,MAAM,2CAA2C;EAClF,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,YAAY,CAAC;CAClE;AACF;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,mBAAmB,SAAmC;CACpE,MAAM,EAAE,WAAW;CACnB,MAAM,EAAE,UAAU,IAAI,WAAW;CACjC,IAAI,UAAU;CACd,MAAM,SAAoB,CAAC;CAC3B,MAAM,cAAsB;EAC1B,WAAW;EACX,OAAO,eAAe,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG;CACnD;CACA,MAAM,OAAO,OAAO,cAAwC;EAC1D,MAAM,UAAU,MAAM,QAAQ,QAAQ,SAAS;EAC/C,OAAO,KAAK,OAAO;EACnB,OAAO;CACT;CACA,OAAO,UAAU,YAAY;EAC3B,MAAM,QAAQ,IAAI,OAAO,OAAO,CAAC,CAAC,CAAC,KAAK,YAAY,QAAQ,MAAM,CAAC,CAAC,YAAY,KAAA,CAAS,CAAC,CAAC;CAC7F,CAAC;CACD,MAAM,QAAwB;EAAE,SAAS;EAAK,cAAc;EAAQ,kBAAkB;CAAO;CAC7F,MAAM,SAAS,OAA4B;EAAE,OAAO;EAAK,MAAM,OAAO,CAAC;CAAE;CAEzE,SAAS,2BAA2B;EAClC,GAAG,kEAAkE,YAAY;GAC/E,MAAM,UAAU,MAAM,KAAK,MAAM,CAAC;GAClC,MAAM,OAAiB,CAAC;GACxB,KAAK,IAAI,KAAK,GAAG,KAAK,GAAG,MAAM,GAC7B,KAAK,MAAM,MAAM,QAAQ,QAAQ,KAAK,MAAM,EAAE,GAAG,KAAK,EAAA,CAAG,GAAG;GAC9D,OAAO,IAAI,CAAC,CAAC,QAAQ;IAAC;IAAG;IAAG;GAAC,CAAC;GAC9B,QAAQ,MAAM,QAAQ,QAAQ,KAAK,MAAM,CAAC,GAAG,KAAK,EAAA,CAAG,GAAG,CAAC,CAAC,KAAK,CAAC;EAClE,CAAC;EAED,GAAG,8EAA8E,YAAY;GAC3F,MAAM,UAAU,MAAM,KAAK,MAAM,CAAC;GAClC,MAAM,OAAiB,CAAC;GACxB,MAAM,OAAO,MAAM,QAAQ,UAAU,MAAM,YAAY,KAAK,KAAK,QAAQ,GAAG,CAAC;GAC7E,KAAK,IAAI,KAAK,GAAG,KAAK,IAAI,MAAM,GAAG,MAAM,QAAQ,QAAQ,KAAK,MAAM,EAAE,GAAG,KAAK;GAC9E,MAAM,YAAY,KAAK,WAAW,EAAE;GACpC,OAAO,IAAI,CAAC,CAAC,QAAQ,MAAM,KAAK,EAAE,QAAQ,GAAG,IAAI,GAAG,OAAO,KAAK,CAAC,CAAC;GAClE,MAAM,KAAK;GACX,MAAM,QAAQ,QAAQ,KAAK,MAAM,EAAE,GAAG,KAAK;GAC3C,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;GACtD,OAAO,IAAI,CAAC,CAAC,aAAa,EAAE;EAC9B,CAAC;EAED,GAAG,gHAAgH,YAAY;GAC7H,MAAM,YAAY,MAAM;GACxB,MAAM,QAAQ,MAAM,KAAK,SAAS;GAClC,MAAM,SAAS,MAAM,KAAK,SAAS;GACnC,MAAM,YAAsB,CAAC;GAC7B,MAAM,aAAuB,CAAC;GAC9B,MAAM,MAAM,UAAU,MAAM,YAAY,UAAU,KAAK,QAAQ,GAAG,CAAC;GACnE,MAAM,OAAO,UAAU,MAAM,YAAY,WAAW,KAAK,QAAQ,GAAG,CAAC;GACrE,MAAM,UAAU,MAAM,QAAQ,IAC5B,MAAM,KAAK,EAAE,QAAQ,IAAI,IAAI,GAAG,QAC7B,KAAK,MAAM,IAAI,QAAQ,OAAA,CAAQ,QAAQ,KAAK,MAAM,EAAE,GAAG,KAAK,CAC/D,CACF;GACA,OAAO,QAAQ,KAAK,WAAW,OAAO,GAAG,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,QAChE,MAAM,KAAK,EAAE,QAAQ,IAAI,IAAI,GAAG,OAAO,KAAK,CAAC,CAC/C;GACA,MAAM,YAAY,UAAU,WAAW,OAAO,WAAW,WAAW,GAAG;GACvE,OAAO,SAAS,CAAC,CAAC,QAAQ,MAAM,KAAK,EAAE,QAAQ,IAAI,IAAI,GAAG,OAAO,KAAK,CAAC,CAAC;GACxE,OAAO,UAAU,CAAC,CAAC,QAAQ,MAAM,KAAK,EAAE,QAAQ,IAAI,IAAI,GAAG,OAAO,KAAK,CAAC,CAAC;EAC3E,CAAC;EAED,GAAG,kFAAkF,YAAY;GAC/F,MAAM,UAAU,MAAM,KAAK,MAAM,CAAC;GAClC,KAAK,IAAI,KAAK,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,QAAQ,QAAQ,KAAK,MAAM,EAAE,GAAG,KAAK;GAC/E,MAAM,OAAO,MAAM,QAAQ,QAAQ,KAAK,GAAG,CAAC;GAC5C,OAAO,KAAK,SAAS,KAAK,YAAY,QAAQ,GAAG,CAAC,CAAC,CAAC,QAAQ;IAAC;IAAG;IAAG;IAAG;GAAC,CAAC;GACxE,OAAO,KAAK,IAAI,CAAC,CAAC,KAAK,EAAE;GACzB,OAAO,KAAK,MAAM,CAAC,CAAC,KAAK,CAAC;GAC1B,QAAQ,MAAM,QAAQ,QAAQ,WAAW,GAAG,EAAE,EAAA,CAAG,IAAI,CAAC,CAAC,KAAK,CAAC;GAC7D,QAAQ,MAAM,QAAQ,QAAQ,KAAK,OAAO,kBAAkB,CAAC,EAAA,CAAG,IAAI,CAAC,CAAC,KAAK,EAAE;EAC/E,CAAC;EAED,GAAG,uDAAuD,YAAY;GACpE,MAAM,UAAU,MAAM,KAAK,MAAM,CAAC;GAClC,MAAM,QAAwB;IAAE,GAAG;IAAO,SAAS;GAAE;GACrD,KAAK,IAAI,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,QAAQ,QAAQ,KAAK,MAAM,EAAE,GAAG,KAAK;GAC9E,MAAM,OAAO,MAAM,QAAQ,QAAQ,KAAK,GAAG,GAAG;GAC9C,OAAO,KAAK,SAAS,KAAK,YAAY,QAAQ,GAAG,CAAC,CAAC,CAAC,QAAQ;IAAC;IAAG;IAAG;GAAC,CAAC;GACrE,OAAO,KAAK,MAAM,CAAC,CAAC,KAAK,CAAC;GAC1B,OAAO,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC;EAC1B,CAAC;EAED,GAAG,2EAA2E,YAAY;GACxF,MAAM,UAAU,MAAM,KAAK,MAAM,CAAC;GAGlC,MAAM,QAAwB;IAAE,GAAG;IAAO,cAAc;GAAI;GAC5D,MAAM,QAAQ,QAAQ,KAAK,MAAM,CAAC,GAAG,KAAK;GAC1C,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,GAAG,CAAC;GACvD,MAAM,QAAQ,QAAQ,KAAK,MAAM,CAAC,GAAG,KAAK;GAC1C,MAAM,OAAO,MAAM,QAAQ,QAAQ,KAAK,GAAG,GAAG;GAC9C,OAAO,KAAK,SAAS,KAAK,YAAY,QAAQ,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;GAC/D,OAAO,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC;EAC1B,CAAC;EAED,GAAG,+DAA+D,YAAY;GAC5E,MAAM,UAAU,MAAM,KAAK,MAAM,CAAC;GAClC,MAAM,OAAuB;IAAE,GAAG;IAAO,SAAS;GAAE;GACpD,QAAQ,MAAM,QAAQ,QAAQ,KAAK,MAAM,CAAC,GAAG,IAAI,EAAA,CAAG,GAAG,CAAC,CAAC,KAAK,CAAC;GAC/D,QAAQ,MAAM,QAAQ,QAAQ,KAAK,MAAM,CAAC,GAAG,IAAI,EAAA,CAAG,GAAG,CAAC,CAAC,KAAK,CAAC;GAC/D,MAAM,OAAO,MAAM,QAAQ,QAAQ,KAAK,GAAG,EAAE;GAC7C,OAAO,KAAK,QAAQ,CAAC,CAAC,aAAa,CAAC;GACpC,OAAO,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC;GACxB,OAAO,KAAK,MAAM,CAAC,CAAC,KAAK,CAAC;EAC5B,CAAC;EAED,GAAG,yGAAyG,YAAY;GACtH,MAAM,YAAY,MAAM;GACxB,MAAM,QAAQ,MAAM,KAAK,SAAS;GAClC,MAAM,SAAS,MAAM,KAAK,SAAS;GACnC,MAAM,QAAwB;IAAE,GAAG;IAAO,KAAK;IAAM,kBAAkB;GAAI;GAC3E,MAAM,MAAM,MAAM,MAAM,QAAQ,KAAK,MAAM,CAAC,GAAG,KAAK;GACpD,MAAM,MAAM,MAAM,OAAO,QAAQ,KAAK,MAAM,CAAC,GAAG,KAAK;GACrD,OAAO,GAAG,CAAC,CAAC,QAAQ;IAAE,KAAK;IAAG,WAAW;GAAM,CAAC;GAChD,OAAO,GAAG,CAAC,CAAC,QAAQ;IAAE,KAAK;IAAG,WAAW;GAAK,CAAC;GAC/C,QAAQ,MAAM,MAAM,QAAQ,KAAK,GAAG,EAAE,EAAA,CAAG,QAAQ,CAAC,CAAC,aAAa,CAAC;GACjE,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,GAAG,CAAC;GACvD,QAAQ,MAAM,MAAM,QAAQ,KAAK,MAAM,CAAC,GAAG,KAAK,EAAA,CAAG,SAAS,CAAC,CAAC,KAAK,KAAK;EAC1E,CAAC;EAED,GAAG,mEAAmE,YAAY;GAChF,MAAM,YAAY,MAAM;GACxB,MAAM,QAAQ,MAAM,KAAK,SAAS;GAClC,MAAM,SAAS,MAAM,KAAK,SAAS;GACnC,MAAM,QAAwB;IAAE,GAAG;IAAO,KAAK;GAAO;GACtD,MAAM,UAAU,MAAM,QAAQ,IAAI,CAChC,MAAM,QAAQ,KAAK,MAAM,CAAC,GAAG,KAAK,GAClC,OAAO,QAAQ,KAAK,MAAM,CAAC,GAAG,KAAK,CACrC,CAAC;GACD,OAAO,QAAQ,QAAQ,WAAW,OAAO,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC;GACnE,OAAO,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAC,KAAK,QAAQ,EAAE,EAAE,GAAG;EAC9C,CAAC;EAED,GAAG,kCAAkC,YAAY;GAC/C,MAAM,MAAM,MAAM,KAAK,MAAM,CAAC;GAC9B,MAAM,MAAM,MAAM,KAAK,MAAM,CAAC;GAC9B,MAAM,IAAI,QAAQ,KAAK,MAAM,CAAC,GAAG,KAAK;GACtC,QAAQ,MAAM,IAAI,QAAQ,KAAK,GAAG,EAAE,EAAA,CAAG,IAAI,CAAC,CAAC,KAAK,CAAC;EACrD,CAAC;CACH,CAAC;CAED,SAAS,2BAA2B;EAClC,MAAM,UAAU,IAAY,IAAI,GAAG,IAAU,UAAkB;GAAE;GAAI;GAAG;EAAE;EAE1E,GAAG,mGAAmG,YAAY;GAChH,MAAM,YAAY,MAAM;GACxB,MAAM,QAAQ,MAAM,KAAK,SAAS;GAClC,MAAM,SAAS,MAAM,KAAK,SAAS;GACnC,MAAM,SAAmB,CAAC;GAC1B,MAAM,OAAO,kBAAkB,SAAS,UACtC,OAAO,KAAK,GAAG,MAAM,GAAG,GAAG,MAAM,OAAO,GAAG,GAAG,MAAM,OAAO,GAAG,CAChE;GACA,MAAM,MAAM,YAAY,QAAQ,OAAO,IAAI,GAAG,GAAI;GAClD,MAAM,MAAM,YAAY,QAAQ,OAAO,MAAM,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,GAAI;GAC/D,MAAM,MAAM,YAAY,QAAQ,OAAO,IAAI,GAAG,GAAI;GAClD,QAAQ,MAAM,OAAO,aAAa,MAAM,EAAA,CAAG,KAAK,SAAS,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAChF,MACA,IACF,CAAC;GACD,OAAO,MAAM,MAAM,eAAe,QAAQ,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI;GAC1D,MAAM,YAAY,OAAO,WAAW,CAAC;GACrC,OAAO,MAAM,CAAC,CAAC,QAAQ;IAAC;IAAa;IAAY;IAAa;GAAY,CAAC;EAC7E,CAAC;EAED,GAAG,mDAAmD,YAAY;GAChE,MAAM,UAAU,MAAM,KAAK,MAAM,CAAC;GAClC,MAAM,QAAQ,YAAY,QAAQ,OAAO,MAAM,GAAG,KAAK,GAAG,GAAI;GAC9D,MAAM,QAAQ,YAAY,QAAQ,OAAO,MAAM,GAAG,KAAK,GAAG,GAAI;GAC9D,QAAQ,MAAM,QAAQ,aAAa,MAAM,EAAA,CAAG,EAAE,CAAC,CAAC,QAAQ,OAAO,MAAM,GAAG,KAAK,CAAC;EAChF,CAAC;EAED,GAAG,4EAA4E,YAAY;GACzF,MAAM,YAAY,MAAM;GACxB,MAAM,QAAQ,MAAM,KAAK,SAAS;GAClC,MAAM,SAAS,MAAM,KAAK,SAAS;GACnC,MAAM,SAAmB,CAAC;GAC1B,MAAM,MAAM,kBACV,SACC,UAAU,MAAM,OAAO,WAAW,OAAO,KAAK,MAAM,OAAO,EAAE,CAChE;GACA,MAAM,MAAM,YAAY,QAAQ,OAAO,IAAI,GAAG,GAAI;GAClD,MAAM,UAAU,MAAM,QAAQ,IAAI,CAChC,MAAM,eAAe,QAAQ,IAAI,GACjC,OAAO,eAAe,QAAQ,IAAI,CACpC,CAAC;GACD,OAAO,QAAQ,OAAO,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;GAC9C,MAAM,YAAY,OAAO,UAAU,CAAC;GACpC,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;GACtD,OAAO,MAAM,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;EAC/B,CAAC;EAED,GAAG,qGAAqG,YAAY;GAClH,MAAM,YAAY,MAAM;GACxB,MAAM,QAAQ,MAAM,KAAK,SAAS;GAClC,MAAM,SAAS,MAAM,KAAK,SAAS;GACnC,MAAM,SAAmB,CAAC;GAC1B,MAAM,MAAM,kBACV,SACC,UAAU,MAAM,OAAO,WAAW,OAAO,KAAK,MAAM,OAAO,EAAE,CAChE;GACA,MAAM,MAAM,YAAY,QAAQ,OAAO,MAAM,GAAG,EAAE;GAClD,MAAM,MAAM,YAAY,QAAQ,OAAO,OAAO,GAAG,GAAI;GACrD,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,GAAG,CAAC;GACvD,QAAQ,MAAM,MAAM,aAAa,MAAM,EAAA,CAAG,KAAK,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;GACnF,MAAM,SAAS,MAAM,QAAQ,IAAI,CAAC,MAAM,aAAa,MAAM,GAAG,OAAO,aAAa,MAAM,CAAC,CAAC;GAC1F,OAAO,OAAO,KAAK,CAAC,CAAC,KAAK,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC;GAC7D,MAAM,YAAY,OAAO,UAAU,CAAC;GACpC,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;GACtD,OAAO,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC;EACjC,CAAC;EAED,GAAG,0FAA0F,YAAY;GACvG,MAAM,UAAU,MAAM,KAAK,MAAM,CAAC;GAClC,MAAM,QAAQ,YAAY,QAAQ,OAAO,IAAI,GAAG,EAAE;GAClD,MAAM,UAAU,MAAM,QAAQ,cAC5B,CACE;IAAE,SAAS;IAAQ,IAAI;GAAK,GAC5B;IAAE,SAAS;IAAQ,IAAI;GAAQ,CACjC,GACA,GACF;GACA,OAAO,OAAO,CAAC,CAAC,QAAQ,CAAC;IAAE,SAAS;IAAQ,IAAI;GAAQ,CAAC,CAAC;GAC1D,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,GAAG,CAAC;GACvD,QAAQ,MAAM,QAAQ,aAAa,MAAM,EAAA,CAAG,KAAK,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;EACpF,CAAC;EAED,GAAG,sDAAsD,YAAY;GACnE,MAAM,UAAU,MAAM,KAAK,MAAM,CAAC;GAClC,MAAM,SAAmB,CAAC;GAC1B,MAAM,OAAO,MAAM,QAAQ,kBAAkB,SAAS,UAAU,OAAO,KAAK,MAAM,EAAE,CAAC;GACrF,MAAM,QAAQ,YAAY,QAAQ,OAAO,IAAI,GAAG,GAAI;GACpD,MAAM,YAAY,OAAO,WAAW,CAAC;GACrC,MAAM,KAAK;GACX,MAAM,QAAQ,YAAY,QAAQ,OAAO,IAAI,GAAG,GAAI;GACpD,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;GACtD,OAAO,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC;EACjC,CAAC;CACH,CAAC;AACH"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{RealtimeError as e}from"../errors.js";import{byteLength as t,decodeServerFrame as n,isChannelName as r,isEventName as i,isIdempotencyKey as a}from"../protocol.js";import{applyPresence as o}from"./presence.js";import{openStream as s}from"./sse.js";import{backoff as c,clientId as ee,resolveUrl as l,sendUrl as u}from"./util.js";import{openSocket as te}from"./ws.js";const ne=/* @__PURE__ */ new Set([1002,1008,1009]),re=/* @__PURE__ */ new Set([`unavailable`,`rate_limited`,`timeout`]),ie=/* @__PURE__ */ new Set([408,425,429]);function d(d){let f=d.reconnect??{},p=f.minDelayMs??250,m=f.maxDelayMs??3e4,ae=f.stableMs??5e3,h=f.random??Math.random,oe=d.maxQueued??100,se=d.queueTtlMs??3e4,ce=d.ackTimeoutMs??1e4,le=(d.transport??`auto`)===`auto`,ue=ee(),g=new AbortController,_=/* @__PURE__ */ new Map,v=/* @__PURE__ */ new Set,y=[],b=/* @__PURE__ */ new Map,x=/* @__PURE__ */ new Set,S=`connecting`,C=d.transport===`sse`?`sse`:`ws`,w=!1,T=!1,E,D=0,O=!1,k,de=65536,fe=``,pe,A=0,j=0,me=0,he=0,ge=0,M=!1,N=0,P,F,_e,ve,ye=!1,I,L=0,R=!1,z=e=>{try{d.onError===void 0?console.error(`[realtime]`,e):d.onError(e)}catch{}},B=e=>{try{e()}catch(e){z(e)}},be=(e,t)=>(d.fetch??globalThis.fetch)(e,t),xe=()=>typeof d.headers==`function`?d.headers():d.headers,V=(e,t)=>{for(let n of[...e])B(()=>n(t))},H=e=>{e!==S&&(S=e,d.onStatus!==void 0&&B(()=>d.onStatus?.(e)),V(x,e))},U=(e,t)=>{e.status!==t&&(e.status=t,V(e.sl,t))},Se=(e,t,n)=>{clearTimeout(e.timer);let r=e.settle;e.settle=void 0,r===void 0?t!==void 0&&e.kind===`pub`&&z(t):r(t,n)},W=(e,t)=>Se(e,t),Ce=(e,t)=>Se(e,void 0,t),G=(t,n)=>{clearTimeout(t.timer),t.timer=setTimeout(()=>{let n=y.indexOf(t);n>=0&&y.splice(n,1),b.get(t.n)===t&&b.delete(t.n),W(t,new e(`timeout`))},n)},K=e=>{let t=b.get(e);return t!==void 0&&b.delete(e),t},we=t=>{if(w)return new e(`closed`);let n=O&&C===`ws`;if(t.kind!==`pres`&&!n&&y.length>=oe)return new e(`queue_full`);if(t.kind===`pres`){let e=y.findIndex(e=>e.kind===`pres`&&e.chan===t.chan);e>=0&&y.splice(e,1)}else G(t,se);y.push(t),O?$():Ye()},Te=(e,t,n,r,i)=>({kind:e,chan:t,event:n,data:r,key:i.key,ack:i.ack??!0,ms:i.ms??0,order:++ge,n:0,timer:void 0,settle:i.settle}),q=n=>{let r=JSON.stringify(n.chan.name),i=n.kind===`req`||n.kind===`pub`&&n.ack;n.n=i?++me:0;let a=n.kind===`pres`?`{"t":"pres","c":${r},"s":${n.data}}`:n.kind===`req`?`{"t":"req","n":${n.n},"c":${r},"e":"${n.event}","d":${n.data},"ms":${n.ms}}`:`{"t":"pub","c":${r},"e":"${n.event}","d":${n.data}${i?`,"n":${n.n}`:``},"k":${JSON.stringify(n.key)}}`;if(t(a)>de)W(n,new e(`too_large`));else return a},Ee=e=>{e.kind!==`pres`&&(e.kind===`req`||e.ack?(b.set(e.n,e),G(e,e.kind===`req`?e.ms+1e3:ce)):clearTimeout(e.timer))},De=t=>{t.sort((e,t)=>e.order-t.order);let n=[];for(let r of t)r.kind===`req`?W(r,new e(`closed`)):(r.kind===`pub`&&G(r,se),n.push(r));y.unshift(...n)},J=e=>!e.closed&&(e.refs>0||e.state!==void 0||e.options.watch===!0||e.hd>0&&C===`ws`),Y=e=>e.state!==void 0,Oe=e=>!Y(e)&&(e.presence.size>0||e.options.watch===!0),X=e=>{e.status===`idle`&&J(e)&&U(e,`subscribing`),v.add(e),Ye()},ke=(e,t)=>{let n=Y(e),r=Oe(e),i=e.sy?e.last:e.options.replay===!0?0:void 0;return e.w={n:t,join:n,watch:r,after:i,ver:e.sv,ready:!n},U(e,e.sy?`resuming`:`subscribing`),{fields:{c:e.name,...i===void 0?{}:{after:i},...e.options.echo===!1?{echo:!1}:{},...n?{presence:e.state}:{},...r?{watch:!0}:{}}}},Ae=e=>{let t=e.w;if(t===void 0||e.state===void 0)return;t.ver=e.sv;let n=Te(`pres`,e,``,JSON.stringify(e.state),{});if(C===`ws`){let e=q(n);e!==void 0&&E?.send(e)}else we(n)},je=e=>{e.sy=!1,e.last=0,e.w=void 0,clearTimeout(e.retry),e.retry=void 0,e.fails=0,e.members.size>0&&(e.members.clear(),e.list=void 0),U(e,e.closed?`closed`:`idle`)},Me=e=>{let t=J(e),n=e.w;if(C===`ws`&&O&&(n!==void 0&&(!t||n.join!==Y(e)||n.watch!==Oe(e))&&(E?.send(`{"t":"unsub","c":${JSON.stringify(e.name)}}`),e.w=void 0),t&&e.w===void 0&&e.retry===void 0)){let t=++me;E?.send(JSON.stringify({t:`sub`,n:t,...ke(e,t).fields}))}if(!t){e.status!==`idle`&&e.status!==`closed`&&je(e);return}let r=e.w;r?.join&&r.ready&&r.ver!==e.sv&&Ae(e)},Ne=()=>{let e=[...v];v.clear();for(let t of e)Me(t)},Z=(e,t)=>{e.closed||(e.closed=!0,_.get(e.name)===e&&_.delete(e.name),e.w!==void 0&&C===`ws`&&O&&E?.send(`{"t":"unsub","c":${JSON.stringify(e.name)}}`),je(e),e.ev.clear(),e.any.clear(),e.gl.clear(),e.presence.clear(),e.sl.clear(),e.refs=0,v.delete(e),t!==void 0&&z(t),Ye())},Pe=(e,t)=>{e.w=void 0,U(e,e.sy?`resuming`:`subscribing`),t?X(e):(e.retry=setTimeout(()=>{e.retry=void 0,X(e)},c(e.fails,p,m,h)),e.fails+=1)},Fe=()=>{let t=[];for(let n of[..._.values()])J(n)&&(t.length>=64?Z(n,new e(`limit`)):t.push(ke(n,t.length+1).fields));return fe=Ie(),t},Ie=()=>{let e=``;for(let t of _.values())J(t)&&(e+=`${t.name}|${t.options.echo===!1?0:1}${+!!Y(t)}${+!!Oe(t)};`);return e},Q=()=>{if(clearTimeout(F),F=void 0,w||E!==void 0)return;D+=1;let t=D;O=!1,M=!1,N=0;let n={rx:()=>{t===D&&(M=!0)},frame:e=>{t===D&&B(()=>We(e,!1))},closed:e=>{t===D&&ze(e)}};try{if(!R&&typeof addEventListener==`function`&&(R=!0,addEventListener(`online`,Le)),C===`sse`){let e=l(d.url,!1);e.searchParams.set(`s`,JSON.stringify(Fe())),E=s(e.toString(),be,xe,n)}else E=te(l(d.url,!0).toString(),d.WebSocket??globalThis.WebSocket,n)}catch(t){D+=1,Xe(new e(`closed`,`cannot open`,{cause:t}));return}P=setTimeout(()=>{t===D&&ze({})},ce)},Le=()=>{F!==void 0&&Q()},Re=()=>{D+=1;let e=E;E=void 0,O=!1,k=void 0,clearTimeout(P),clearTimeout(_e),clearInterval(ve),ve=void 0,e?.close()},ze=t=>{let n=O,r=pe??t.code;if(pe=void 0,Re(),w)return;if(C===`ws`&&b.size>0){let e=[...b.values()];b.clear(),De(e)}for(let e of _.values())e.w=void 0,clearTimeout(e.retry),e.retry=void 0,J(e)&&U(e,e.sy?`resuming`:`subscribing`);let i=t.status!==void 0&&t.status>=400&&t.status<500&&!ie.has(t.status);if((r!==void 0&&ne.has(r)||i)&&f.retryFatal!==!0){let n=r===void 0?`the server answered ${t.status}`:`the server closed the connection with ${r}`;Xe(new e(`closed`,n));return}let a=c(j,p,m,h);j+=1,n?A=0:le&&C===`ws`&&(A+=1,A>=2&&(C=`sse`,a=0)),H(`reconnecting`),F=setTimeout(Q,a)},Be=()=>{if(M)M=!1,N=0;else if(N+=1,N>=2){ze({});return}C===`ws`&&E?.send(`{"t":"ping"}`)},Ve=()=>{Re();for(let e of _.values())e.w=void 0;Q()},He=e=>{if(!O){if(O=!0,k=e.id,de=e.max,A=0,M=!0,N=0,clearTimeout(P),e.hb>0&&(ve=setInterval(Be,e.hb)),_e=setTimeout(()=>{j=0},ae),H(`open`),C===`ws`)for(let e of _.values())v.add(e);$()}},Ue=e=>{let t=[...e.members.values()];e.list=t,V(e.presence,t)},We=(t,n)=>{switch(t.t){case`ready`:He(t);return;case`bye`:pe=t.code;return;case`subbed`:{let e=_.get(t.c),n=e?.w;if(e===void 0||n===void 0||n.n!==t.n)return;(t.reset||n.after===void 0)&&(e.last=t.head);let r={after:n.after??0,head:t.head},i=t.reset?!0:t.gap===!0&&n.after!==void 0&&!(n.after===0&&!e.sy);e.sy=!0,e.fails=0,U(e,`live`),i&&V(e.gl,{...r,reset:t.reset===!0});return}case`unsubbed`:{let n=_.get(t.c);if(n===void 0)return;t.why===`revoked`?(n.w=void 0,Z(n,new e(`forbidden`,`revoked`))):Pe(n,t.why===`gap`||t.why===`reset`);return}case`msg`:{let e=_.get(t.c);if(e===void 0||t.s<=e.last)return;e.last=t.s;let n={seq:t.s,replayed:t.r===1,...t.p===void 0?{}:{publisher:t.p},...t.u===void 0?{}:{user:t.u}};for(let r of[...e.ev.get(t.e)??[]])B(()=>r(t.d,n));for(let r of[...e.any])B(()=>r({event:t.e,data:t.d,meta:n}));return}case`ack`:{let e=K(t.n);e!==void 0&&Ce(e,{seq:t.s,duplicate:t.dup===!0});return}case`rep`:{let e=K(t.n);e!==void 0&&Ce(e,t.d);return}case`err`:{let r=new e(t.code,t.msg),i=t.c===void 0?void 0:_.get(t.c);if(t.n!==void 0){if(!n&&i?.w?.n===t.n){re.has(t.code)?(Pe(i,!1),z(r)):(i.w=void 0,Z(i,r));return}let e=n||C===`ws`?K(t.n):void 0;if(e!==void 0){W(e,r);return}}z(r);return}case`pres`:{let e=_.get(t.c);if(e===void 0)return;o(e.members,t.op,t.m)&&Ue(e),t.op===`sync`&&e.w!==void 0&&(e.w.ready=!0,e.w.join&&e.w.ver!==e.sv&&Ae(e));return}case`pong`:return}},$=()=>{if(O&&!w){if(Ne(),C===`sse`)Ge();else for(let e=y[0];e!==void 0&&O;e=y[0]){let t=q(e);if(t!==void 0){if(E?.send(t)!==!0)return;Ee(e)}y.shift()}}},Ge=()=>{if(ye||I!==void 0||k===void 0)return;let e=[],t=[],n=0;for(;y.length>0&&e.length<64&&n<256e3;){let r=y.shift();if(r===void 0)break;let i=q(r);i!==void 0&&(e.push(r),t.push(i),n+=i.length)}e.length!==0&&(ye=!0,Ke(e,`{"cid":${JSON.stringify(k)},"frames":[${t.join(`,`)}]}`).then(void 0,z))},Ke=async(t,r)=>{for(let e of t)Ee(e);let i=!1;try{let a=new Headers(await xe());a.set(`content-type`,`application/json`);let o=d.postUrl===void 0?u(l(d.url,!1)):l(d.postUrl,!1),s=await be(o,{method:`POST`,headers:a,body:r,signal:g.signal});if(s.ok){let e=await s.json();if(Array.isArray(e.frames))for(let t of e.frames){let e=n(JSON.stringify(t));e!==void 0&&B(()=>We(e,!0))}L=0}else if(s.status>=500||ie.has(s.status))i=!0;else{let n=await s.json().catch(()=>void 0),r=n?.error?.code??`unavailable`;for(let i of t)b.get(i.n)===i&&b.delete(i.n),W(i,new e(r,n?.error?.message))}}catch{if(g.signal.aborted)return;i=!0}ye=!1;let a=t.filter(e=>(e.kind===`req`||e.kind===`pub`&&e.ack)&&b.get(e.n)===e);for(let e of a)b.delete(e.n);if(i){for(let t of a)t.kind===`req`&&W(t,new e(`unavailable`));De(t.filter(e=>e.kind===`pres`||e.kind===`pub`&&(!e.ack||a.includes(e)))),L+=1,I=setTimeout(()=>{I=void 0,$()},c(L-1,p,m,h))}else for(let t of a)W(t,new e(`unavailable`));$()},qe=()=>y.length>0||[..._.values()].some(J),Je=()=>{T=!1,!w&&(Ne(),E===void 0?F===void 0&&qe()&&Q():C===`sse`&&(Ie()!==fe||[..._.values()].some(e=>J(e)&&e.w===void 0&&e.retry===void 0))?Ve():$())};function Ye(){T||w||(T=!0,queueMicrotask(Je))}function Xe(t){if(w)return;w=!0,clearTimeout(F),clearTimeout(I),F=void 0,I=void 0,Re(),g.abort(),R&&typeof removeEventListener==`function`&&removeEventListener(`online`,Le),R=!1;let n=t??new e(`closed`),r=[...y.splice(0),...b.values()];b.clear();for(let e of r)e.settle===void 0?clearTimeout(e.timer):W(e,n);for(let e of[..._.values()])Z(e);_.clear(),v.clear(),H(`closed`),x.clear(),t!==void 0&&z(t)}let Ze=(t,n)=>{let r={name:t,options:n,ev:/* @__PURE__ */ new Map,any:/* @__PURE__ */ new Set,gl:/* @__PURE__ */ new Set,sl:/* @__PURE__ */ new Set,presence:/* @__PURE__ */ new Set,members:/* @__PURE__ */ new Map,list:void 0,status:`idle`,refs:0,hd:0,last:0,sy:!1,state:n.presence,sv:0,w:void 0,retry:void 0,fails:0,closed:!1,hn:void 0};_.set(t,r),J(r)&&X(r);let o=(e,t,n,i)=>r.closed?()=>void 0:(e.add(t),n||(r.refs+=1,X(r)),()=>{e.delete(t)&&!n&&(--r.refs,i?.(),X(r))}),s=(t,n,o,s,c,ee)=>{let l=`null`,u;if(r.closed)u=new e(`closed`);else if(!i(n)||s!==void 0&&!a(s))u=new e(`bad_frame`);else try{l=JSON.stringify(o??null)??`null`}catch(t){u=new e(`bad_frame`,`not JSON`,{cause:t})}return u===void 0?new Promise((e,i)=>{let a=t===`req`,o=Te(t,r,n,l,{key:a?void 0:s??`${ue}:${++he}`,ack:c,ms:ee,settle:c?(t,n)=>{a&&(--r.hd,X(r)),t?i(t):e(n)}:void 0});a&&(r.hd+=1,X(r));let u=we(o);u===void 0?c||e({seq:0,duplicate:!1}):(o.settle=void 0,a&&--r.hd,i(u))}):Promise.reject(u)},c={name:t,get status(){return r.status},get lastSeq(){return r.last},on(e,t){let n=r.ev.get(e);n===void 0&&(n=/* @__PURE__ */ new Set,r.ev.set(e,n));let i=n;return o(i,t,!1,()=>{i.size===0&&r.ev.get(e)===i&&r.ev.delete(e)})},onAny:e=>o(r.any,e),onGap:e=>o(r.gl,e),onStatus:e=>o(r.sl,e,!0),publish:(e,t,n={})=>s(`pub`,e,t,n.key,n.ack!==!1,0),request:(e,t,n={})=>s(`req`,e,t,void 0,!0,n.timeoutMs??1e4),presence:{members:()=>(r.list??=[...r.members.values()],r.list),set(e){r.closed||(r.state=e,r.sv+=1,X(r))},leave(){r.closed||r.state===void 0||(r.state=void 0,r.sv+=1,X(r))},on:e=>o(r.presence,e)},close:()=>Z(r)};return r.hn=c,c};return{channel(t,n={}){if(w)throw new e(`closed`);if(!r(t))throw new e(`bad_channel`,t);return _.get(t)?.hn??Ze(t,n)},get status(){return S},get connectionId(){return k},onStatus(e){return w||x.add(e),()=>{x.delete(e)}},close:()=>Xe()}}export{d as createRealtime};
|
|
2
|
+
//# sourceMappingURL=core.js.map
|