@pawabase/client 0.0.0-stage → 0.1.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/README.md +313 -2
- package/dist/cjs/admin.d.ts +187 -0
- package/dist/cjs/admin.d.ts.map +1 -0
- package/dist/cjs/admin.js +235 -0
- package/dist/cjs/admin.js.map +1 -0
- package/dist/cjs/auth.d.ts +448 -0
- package/dist/cjs/auth.d.ts.map +1 -0
- package/dist/cjs/auth.js +1016 -0
- package/dist/cjs/auth.js.map +1 -0
- package/dist/cjs/client.d.ts +103 -0
- package/dist/cjs/client.d.ts.map +1 -0
- package/dist/cjs/client.js +144 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/data.d.ts +220 -0
- package/dist/cjs/data.d.ts.map +1 -0
- package/dist/cjs/data.js +490 -0
- package/dist/cjs/data.js.map +1 -0
- package/dist/cjs/errors.d.ts +167 -0
- package/dist/cjs/errors.d.ts.map +1 -0
- package/dist/cjs/errors.js +316 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/files.d.ts +80 -0
- package/dist/cjs/files.d.ts.map +1 -0
- package/dist/cjs/files.js +167 -0
- package/dist/cjs/files.js.map +1 -0
- package/dist/cjs/functions.d.ts +66 -0
- package/dist/cjs/functions.d.ts.map +1 -0
- package/dist/cjs/functions.js +109 -0
- package/dist/cjs/functions.js.map +1 -0
- package/dist/cjs/http.d.ts +101 -0
- package/dist/cjs/http.d.ts.map +1 -0
- package/dist/cjs/http.js +249 -0
- package/dist/cjs/http.js.map +1 -0
- package/dist/cjs/index.d.ts +22 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +45 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/manage/access.d.ts +136 -0
- package/dist/cjs/manage/access.d.ts.map +1 -0
- package/dist/cjs/manage/access.js +162 -0
- package/dist/cjs/manage/access.js.map +1 -0
- package/dist/cjs/manage/automation.d.ts +106 -0
- package/dist/cjs/manage/automation.d.ts.map +1 -0
- package/dist/cjs/manage/automation.js +144 -0
- package/dist/cjs/manage/automation.js.map +1 -0
- package/dist/cjs/manage/base.d.ts +52 -0
- package/dist/cjs/manage/base.d.ts.map +1 -0
- package/dist/cjs/manage/base.js +69 -0
- package/dist/cjs/manage/base.js.map +1 -0
- package/dist/cjs/manage/data.d.ts +100 -0
- package/dist/cjs/manage/data.d.ts.map +1 -0
- package/dist/cjs/manage/data.js +141 -0
- package/dist/cjs/manage/data.js.map +1 -0
- package/dist/cjs/manage/files.d.ts +45 -0
- package/dist/cjs/manage/files.d.ts.map +1 -0
- package/dist/cjs/manage/files.js +73 -0
- package/dist/cjs/manage/files.js.map +1 -0
- package/dist/cjs/manage/functions.d.ts +48 -0
- package/dist/cjs/manage/functions.d.ts.map +1 -0
- package/dist/cjs/manage/functions.js +61 -0
- package/dist/cjs/manage/functions.js.map +1 -0
- package/dist/cjs/manage/index.d.ts +76 -0
- package/dist/cjs/manage/index.d.ts.map +1 -0
- package/dist/cjs/manage/index.js +134 -0
- package/dist/cjs/manage/index.js.map +1 -0
- package/dist/cjs/manage/lifecycle.d.ts +128 -0
- package/dist/cjs/manage/lifecycle.d.ts.map +1 -0
- package/dist/cjs/manage/lifecycle.js +167 -0
- package/dist/cjs/manage/lifecycle.js.map +1 -0
- package/dist/cjs/manage/mail.d.ts +16 -0
- package/dist/cjs/manage/mail.d.ts.map +1 -0
- package/dist/cjs/manage/mail.js +24 -0
- package/dist/cjs/manage/mail.js.map +1 -0
- package/dist/cjs/manage/observe.d.ts +149 -0
- package/dist/cjs/manage/observe.d.ts.map +1 -0
- package/dist/cjs/manage/observe.js +168 -0
- package/dist/cjs/manage/observe.js.map +1 -0
- package/dist/cjs/manage/realtime.d.ts +52 -0
- package/dist/cjs/manage/realtime.d.ts.map +1 -0
- package/dist/cjs/manage/realtime.js +71 -0
- package/dist/cjs/manage/realtime.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/realtime.d.ts +207 -0
- package/dist/cjs/realtime.d.ts.map +1 -0
- package/dist/cjs/realtime.js +704 -0
- package/dist/cjs/realtime.js.map +1 -0
- package/dist/cjs/session-store.d.ts +16 -0
- package/dist/cjs/session-store.d.ts.map +1 -0
- package/dist/cjs/session-store.js +37 -0
- package/dist/cjs/session-store.js.map +1 -0
- package/dist/cjs/typegen.d.ts +26 -0
- package/dist/cjs/typegen.d.ts.map +1 -0
- package/dist/cjs/typegen.js +167 -0
- package/dist/cjs/typegen.js.map +1 -0
- package/dist/cjs/types.d.ts +178 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +4 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/util.d.ts +27 -0
- package/dist/cjs/util.d.ts.map +1 -0
- package/dist/cjs/util.js +115 -0
- package/dist/cjs/util.js.map +1 -0
- package/dist/cjs/version.d.ts +2 -0
- package/dist/cjs/version.d.ts.map +1 -0
- package/dist/cjs/version.js +5 -0
- package/dist/cjs/version.js.map +1 -0
- package/dist/esm/admin.d.ts +187 -0
- package/dist/esm/admin.d.ts.map +1 -0
- package/dist/esm/admin.js +222 -0
- package/dist/esm/admin.js.map +1 -0
- package/dist/esm/auth.d.ts +448 -0
- package/dist/esm/auth.d.ts.map +1 -0
- package/dist/esm/auth.js +1012 -0
- package/dist/esm/auth.js.map +1 -0
- package/dist/esm/cli/gen-types.d.ts +2 -0
- package/dist/esm/cli/gen-types.d.ts.map +1 -0
- package/dist/esm/cli/gen-types.js +100 -0
- package/dist/esm/cli/gen-types.js.map +1 -0
- package/dist/esm/client.d.ts +103 -0
- package/dist/esm/client.d.ts.map +1 -0
- package/dist/esm/client.js +139 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/data.d.ts +220 -0
- package/dist/esm/data.d.ts.map +1 -0
- package/dist/esm/data.js +485 -0
- package/dist/esm/data.js.map +1 -0
- package/dist/esm/errors.d.ts +167 -0
- package/dist/esm/errors.d.ts.map +1 -0
- package/dist/esm/errors.js +293 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/files.d.ts +80 -0
- package/dist/esm/files.d.ts.map +1 -0
- package/dist/esm/files.js +162 -0
- package/dist/esm/files.js.map +1 -0
- package/dist/esm/functions.d.ts +66 -0
- package/dist/esm/functions.d.ts.map +1 -0
- package/dist/esm/functions.js +103 -0
- package/dist/esm/functions.js.map +1 -0
- package/dist/esm/http.d.ts +101 -0
- package/dist/esm/http.d.ts.map +1 -0
- package/dist/esm/http.js +245 -0
- package/dist/esm/http.js.map +1 -0
- package/dist/esm/index.d.ts +22 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/manage/access.d.ts +136 -0
- package/dist/esm/manage/access.d.ts.map +1 -0
- package/dist/esm/manage/access.js +155 -0
- package/dist/esm/manage/access.js.map +1 -0
- package/dist/esm/manage/automation.d.ts +106 -0
- package/dist/esm/manage/automation.d.ts.map +1 -0
- package/dist/esm/manage/automation.js +134 -0
- package/dist/esm/manage/automation.js.map +1 -0
- package/dist/esm/manage/base.d.ts +52 -0
- package/dist/esm/manage/base.d.ts.map +1 -0
- package/dist/esm/manage/base.js +63 -0
- package/dist/esm/manage/base.js.map +1 -0
- package/dist/esm/manage/data.d.ts +100 -0
- package/dist/esm/manage/data.d.ts.map +1 -0
- package/dist/esm/manage/data.js +130 -0
- package/dist/esm/manage/data.js.map +1 -0
- package/dist/esm/manage/files.d.ts +45 -0
- package/dist/esm/manage/files.d.ts.map +1 -0
- package/dist/esm/manage/files.js +68 -0
- package/dist/esm/manage/files.js.map +1 -0
- package/dist/esm/manage/functions.d.ts +48 -0
- package/dist/esm/manage/functions.d.ts.map +1 -0
- package/dist/esm/manage/functions.js +57 -0
- package/dist/esm/manage/functions.js.map +1 -0
- package/dist/esm/manage/index.d.ts +76 -0
- package/dist/esm/manage/index.d.ts.map +1 -0
- package/dist/esm/manage/index.js +115 -0
- package/dist/esm/manage/index.js.map +1 -0
- package/dist/esm/manage/lifecycle.d.ts +128 -0
- package/dist/esm/manage/lifecycle.d.ts.map +1 -0
- package/dist/esm/manage/lifecycle.js +161 -0
- package/dist/esm/manage/lifecycle.js.map +1 -0
- package/dist/esm/manage/mail.d.ts +16 -0
- package/dist/esm/manage/mail.d.ts.map +1 -0
- package/dist/esm/manage/mail.js +19 -0
- package/dist/esm/manage/mail.js.map +1 -0
- package/dist/esm/manage/observe.d.ts +149 -0
- package/dist/esm/manage/observe.d.ts.map +1 -0
- package/dist/esm/manage/observe.js +161 -0
- package/dist/esm/manage/observe.js.map +1 -0
- package/dist/esm/manage/realtime.d.ts +52 -0
- package/dist/esm/manage/realtime.d.ts.map +1 -0
- package/dist/esm/manage/realtime.js +66 -0
- package/dist/esm/manage/realtime.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/realtime.d.ts +207 -0
- package/dist/esm/realtime.d.ts.map +1 -0
- package/dist/esm/realtime.js +699 -0
- package/dist/esm/realtime.js.map +1 -0
- package/dist/esm/session-store.d.ts +16 -0
- package/dist/esm/session-store.d.ts.map +1 -0
- package/dist/esm/session-store.js +32 -0
- package/dist/esm/session-store.js.map +1 -0
- package/dist/esm/typegen.d.ts +26 -0
- package/dist/esm/typegen.d.ts.map +1 -0
- package/dist/esm/typegen.js +163 -0
- package/dist/esm/typegen.js.map +1 -0
- package/dist/esm/types.d.ts +178 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +3 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/util.d.ts +27 -0
- package/dist/esm/util.d.ts.map +1 -0
- package/dist/esm/util.js +102 -0
- package/dist/esm/util.js.map +1 -0
- package/dist/esm/version.d.ts +2 -0
- package/dist/esm/version.d.ts.map +1 -0
- package/dist/esm/version.js +2 -0
- package/dist/esm/version.js.map +1 -0
- package/package.json +84 -4
- package/src/admin.ts +311 -0
- package/src/auth.ts +1180 -0
- package/src/cli/gen-types.ts +105 -0
- package/src/client.ts +189 -0
- package/src/data.ts +576 -0
- package/src/errors.ts +330 -0
- package/src/files.ts +208 -0
- package/src/functions.ts +131 -0
- package/src/http.ts +354 -0
- package/src/index.ts +56 -0
- package/src/manage/access.ts +206 -0
- package/src/manage/automation.ts +162 -0
- package/src/manage/base.ts +83 -0
- package/src/manage/data.ts +159 -0
- package/src/manage/files.ts +79 -0
- package/src/manage/functions.ts +69 -0
- package/src/manage/index.ts +132 -0
- package/src/manage/lifecycle.ts +208 -0
- package/src/manage/mail.ts +22 -0
- package/src/manage/observe.ts +226 -0
- package/src/manage/realtime.ts +95 -0
- package/src/realtime.ts +765 -0
- package/src/session-store.ts +36 -0
- package/src/typegen.ts +192 -0
- package/src/types.ts +195 -0
- package/src/util.ts +114 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
import { ConfigError, RealtimeError } from "./errors.js";
|
|
2
|
+
import { backoff, encodePath, sleep } from "./util.js";
|
|
3
|
+
const OPEN = 1;
|
|
4
|
+
const FATAL_CLOSE = new Set([4001]);
|
|
5
|
+
const SEEN_LIMIT = 1000;
|
|
6
|
+
export class RealtimeClient {
|
|
7
|
+
http;
|
|
8
|
+
auth;
|
|
9
|
+
options;
|
|
10
|
+
socket = null;
|
|
11
|
+
currentStatus = "idle";
|
|
12
|
+
channels = new Map();
|
|
13
|
+
pending = new Map();
|
|
14
|
+
statusListeners = new Set();
|
|
15
|
+
refs = 0;
|
|
16
|
+
attempts = 0;
|
|
17
|
+
manual = false;
|
|
18
|
+
connecting = null;
|
|
19
|
+
heartbeat = null;
|
|
20
|
+
reconnectTimer = null;
|
|
21
|
+
lastActivity = 0;
|
|
22
|
+
triedRefresh = false;
|
|
23
|
+
authSubscription = null;
|
|
24
|
+
/** The server's id for this connection, once welcomed. */
|
|
25
|
+
connectionId = null;
|
|
26
|
+
constructor(http, auth, options = {}) {
|
|
27
|
+
this.http = http;
|
|
28
|
+
this.auth = auth;
|
|
29
|
+
this.options = options;
|
|
30
|
+
}
|
|
31
|
+
get status() {
|
|
32
|
+
return this.currentStatus;
|
|
33
|
+
}
|
|
34
|
+
onStatusChange(handler) {
|
|
35
|
+
this.statusListeners.add(handler);
|
|
36
|
+
return () => void this.statusListeners.delete(handler);
|
|
37
|
+
}
|
|
38
|
+
setStatus(status) {
|
|
39
|
+
if (this.currentStatus === status)
|
|
40
|
+
return;
|
|
41
|
+
this.currentStatus = status;
|
|
42
|
+
for (const handler of [...this.statusListeners]) {
|
|
43
|
+
try {
|
|
44
|
+
handler(status);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* observers must not break the connection */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// ── channels ──────────────────────────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* A channel by name. Asking twice returns the same object, so listeners are shared.
|
|
54
|
+
* Channel names are authorised by the project's realtime rules.
|
|
55
|
+
*/
|
|
56
|
+
channel(name, options = {}) {
|
|
57
|
+
let channel = this.channels.get(name);
|
|
58
|
+
if (!channel) {
|
|
59
|
+
channel = new RealtimeChannel(this, name, options);
|
|
60
|
+
this.channels.set(name, channel);
|
|
61
|
+
}
|
|
62
|
+
else if (options.presence || options.replay !== undefined) {
|
|
63
|
+
channel.configure(options);
|
|
64
|
+
}
|
|
65
|
+
return channel;
|
|
66
|
+
}
|
|
67
|
+
/** Forget a channel object once it is unsubscribed. */
|
|
68
|
+
forget(name) {
|
|
69
|
+
this.channels.delete(name);
|
|
70
|
+
}
|
|
71
|
+
// ── connection ────────────────────────────────────────────────────────
|
|
72
|
+
url(token) {
|
|
73
|
+
const base = new URL(this.http.config.baseUrl);
|
|
74
|
+
base.protocol = base.protocol === "https:" ? "wss:" : "ws:";
|
|
75
|
+
base.pathname = `${base.pathname.replace(/\/+$/, "")}/realtime/v1/socket`;
|
|
76
|
+
base.search = "";
|
|
77
|
+
base.searchParams.set("apikey", this.http.config.apiKey);
|
|
78
|
+
if (token)
|
|
79
|
+
base.searchParams.set("token", token);
|
|
80
|
+
if (this.options.environment)
|
|
81
|
+
base.searchParams.set("environment", this.options.environment);
|
|
82
|
+
return base.toString();
|
|
83
|
+
}
|
|
84
|
+
/** Open the connection (channels do this for you). Resolves once the server has welcomed it. */
|
|
85
|
+
connect() {
|
|
86
|
+
if (this.currentStatus === "open")
|
|
87
|
+
return Promise.resolve();
|
|
88
|
+
if (this.connecting)
|
|
89
|
+
return this.connecting;
|
|
90
|
+
this.manual = false;
|
|
91
|
+
this.connecting = this.open().finally(() => {
|
|
92
|
+
this.connecting = null;
|
|
93
|
+
});
|
|
94
|
+
return this.connecting;
|
|
95
|
+
}
|
|
96
|
+
async open() {
|
|
97
|
+
const Impl = this.options.webSocket ?? globalThis.WebSocket;
|
|
98
|
+
if (!Impl) {
|
|
99
|
+
throw new ConfigError("No WebSocket implementation found. Pass `realtime: { webSocket }` to createClient (for example the `ws` package on Node 20).");
|
|
100
|
+
}
|
|
101
|
+
this.setStatus(this.attempts > 0 ? "reconnecting" : "connecting");
|
|
102
|
+
const token = (await this.auth?.getAccessToken().catch(() => null)) ?? null;
|
|
103
|
+
const socket = new Impl(this.url(token));
|
|
104
|
+
this.socket = socket;
|
|
105
|
+
this.lastActivity = Date.now();
|
|
106
|
+
const timeoutMs = this.options.connectTimeoutMs ?? 10_000;
|
|
107
|
+
await new Promise((resolve, reject) => {
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
reject(new RealtimeError("connect_timeout", `The realtime connection did not open within ${timeoutMs} ms.`));
|
|
110
|
+
try {
|
|
111
|
+
socket.close(4000, "connect timeout");
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* already closed */
|
|
115
|
+
}
|
|
116
|
+
}, timeoutMs);
|
|
117
|
+
let welcomed = false;
|
|
118
|
+
socket.onmessage = (event) => {
|
|
119
|
+
this.lastActivity = Date.now();
|
|
120
|
+
const frame = this.parse(event.data);
|
|
121
|
+
if (!frame)
|
|
122
|
+
return;
|
|
123
|
+
if (!welcomed && frame["type"] === "welcome") {
|
|
124
|
+
welcomed = true;
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
this.connectionId = typeof frame["connection_id"] === "string" ? frame["connection_id"] : null;
|
|
127
|
+
socket.onmessage = (next) => this.onMessage(next.data);
|
|
128
|
+
this.attempts = 0;
|
|
129
|
+
this.triedRefresh = false;
|
|
130
|
+
this.setStatus("open");
|
|
131
|
+
this.startHeartbeat();
|
|
132
|
+
this.watchAuth();
|
|
133
|
+
void this.resubscribe();
|
|
134
|
+
resolve();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.onMessage(event.data);
|
|
138
|
+
};
|
|
139
|
+
socket.onerror = () => {
|
|
140
|
+
/* the close event follows with the code */
|
|
141
|
+
};
|
|
142
|
+
socket.onclose = (event) => {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
if (!welcomed) {
|
|
145
|
+
reject(new RealtimeError(`closed_${event.code}`, event.reason || `The connection closed (${event.code}) before it opened.`));
|
|
146
|
+
}
|
|
147
|
+
this.onClose(event.code, event.reason ?? "", welcomed);
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** Close the connection and stop reconnecting. Channels keep their listeners and rejoin on the next `connect()`. */
|
|
152
|
+
disconnect() {
|
|
153
|
+
this.manual = true;
|
|
154
|
+
if (this.reconnectTimer)
|
|
155
|
+
clearTimeout(this.reconnectTimer);
|
|
156
|
+
this.reconnectTimer = null;
|
|
157
|
+
this.stopHeartbeat();
|
|
158
|
+
this.authSubscription?.unsubscribe();
|
|
159
|
+
this.authSubscription = null;
|
|
160
|
+
const socket = this.socket;
|
|
161
|
+
this.socket = null;
|
|
162
|
+
this.failPending(new RealtimeError("closed", "The realtime connection was closed."));
|
|
163
|
+
for (const channel of this.channels.values())
|
|
164
|
+
channel.markClosed("disconnected");
|
|
165
|
+
if (socket) {
|
|
166
|
+
socket.onclose = null;
|
|
167
|
+
socket.onmessage = null;
|
|
168
|
+
try {
|
|
169
|
+
socket.close(1000, "client closed");
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
/* already closed */
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
this.attempts = 0;
|
|
176
|
+
this.setStatus("closed");
|
|
177
|
+
}
|
|
178
|
+
onClose(code, reason, wasOpen) {
|
|
179
|
+
this.stopHeartbeat();
|
|
180
|
+
this.socket = null;
|
|
181
|
+
this.failPending(new RealtimeError("closed", "The realtime connection closed."));
|
|
182
|
+
for (const channel of this.channels.values())
|
|
183
|
+
channel.markClosed(reason || `closed (${code})`);
|
|
184
|
+
if (this.manual)
|
|
185
|
+
return;
|
|
186
|
+
if (FATAL_CLOSE.has(code)) {
|
|
187
|
+
this.setStatus("closed");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (code === 4003 && !this.triedRefresh && this.auth) {
|
|
191
|
+
// The token was refused: refresh it once, then try again.
|
|
192
|
+
this.triedRefresh = true;
|
|
193
|
+
void this.auth
|
|
194
|
+
.refreshSession()
|
|
195
|
+
.then(() => this.scheduleReconnect(0))
|
|
196
|
+
.catch(() => this.setStatus("closed"));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (code === 4003) {
|
|
200
|
+
this.setStatus("closed");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
void wasOpen;
|
|
204
|
+
this.scheduleReconnect();
|
|
205
|
+
}
|
|
206
|
+
scheduleReconnect(forcedDelay) {
|
|
207
|
+
const policy = this.options.reconnect;
|
|
208
|
+
if (policy === false) {
|
|
209
|
+
this.setStatus("closed");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const maxAttempts = policy?.maxAttempts ?? Infinity;
|
|
213
|
+
if (this.attempts >= maxAttempts) {
|
|
214
|
+
this.setStatus("closed");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const delay = forcedDelay ?? backoff(this.attempts, { baseDelayMs: policy?.baseDelayMs ?? 500, maxDelayMs: policy?.maxDelayMs ?? 15_000 });
|
|
218
|
+
this.attempts += 1;
|
|
219
|
+
this.setStatus("reconnecting");
|
|
220
|
+
this.reconnectTimer = setTimeout(() => {
|
|
221
|
+
this.reconnectTimer = null;
|
|
222
|
+
this.connecting = this.open()
|
|
223
|
+
.catch(() => {
|
|
224
|
+
// open() rejected without a close event reaching onClose (rare); try again.
|
|
225
|
+
if (!this.manual && !this.socket)
|
|
226
|
+
this.scheduleReconnect();
|
|
227
|
+
})
|
|
228
|
+
.finally(() => {
|
|
229
|
+
this.connecting = null;
|
|
230
|
+
});
|
|
231
|
+
}, delay);
|
|
232
|
+
this.reconnectTimer.unref?.();
|
|
233
|
+
}
|
|
234
|
+
startHeartbeat() {
|
|
235
|
+
this.stopHeartbeat();
|
|
236
|
+
const interval = (this.options.heartbeatSeconds ?? 25) * 1000;
|
|
237
|
+
this.heartbeat = setInterval(() => {
|
|
238
|
+
const socket = this.socket;
|
|
239
|
+
if (!socket || socket.readyState !== OPEN)
|
|
240
|
+
return;
|
|
241
|
+
if (Date.now() - this.lastActivity > interval * 2 + 1000) {
|
|
242
|
+
// Nothing, not even a pong: the link is dead without having closed.
|
|
243
|
+
try {
|
|
244
|
+
socket.close(4000, "heartbeat timeout");
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
/* already closed */
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
socket.send(JSON.stringify({ type: "ping" }));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
/* the close handler will follow */
|
|
256
|
+
}
|
|
257
|
+
}, interval);
|
|
258
|
+
this.heartbeat.unref?.();
|
|
259
|
+
}
|
|
260
|
+
stopHeartbeat() {
|
|
261
|
+
if (this.heartbeat)
|
|
262
|
+
clearInterval(this.heartbeat);
|
|
263
|
+
this.heartbeat = null;
|
|
264
|
+
}
|
|
265
|
+
/** Tell the server about a refreshed (or removed) token without reconnecting. */
|
|
266
|
+
watchAuth() {
|
|
267
|
+
if (!this.auth || this.authSubscription)
|
|
268
|
+
return;
|
|
269
|
+
this.authSubscription = this.auth.onAuthStateChange((event, session) => {
|
|
270
|
+
if (event === "INITIAL_SESSION" || this.currentStatus !== "open")
|
|
271
|
+
return;
|
|
272
|
+
void this.request({ type: "auth", token: session?.access_token ?? null }).catch(() => undefined);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
async resubscribe() {
|
|
276
|
+
for (const channel of [...this.channels.values()]) {
|
|
277
|
+
if (channel.wantsJoin)
|
|
278
|
+
await channel.rejoin().catch(() => undefined);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// ── messages ──────────────────────────────────────────────────────────
|
|
282
|
+
parse(raw) {
|
|
283
|
+
if (typeof raw !== "string") {
|
|
284
|
+
this.options.onProtocolError?.("a binary frame arrived; Pawabase sends JSON text");
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const value = JSON.parse(raw);
|
|
289
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value))
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
/* fall through */
|
|
294
|
+
}
|
|
295
|
+
this.options.onProtocolError?.(`an invalid frame arrived: ${raw.slice(0, 120)}`);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
onMessage(raw) {
|
|
299
|
+
this.lastActivity = Date.now();
|
|
300
|
+
const frame = this.parse(raw);
|
|
301
|
+
if (!frame)
|
|
302
|
+
return;
|
|
303
|
+
switch (frame["type"]) {
|
|
304
|
+
case "ack": {
|
|
305
|
+
const pending = this.take(frame["ref"]);
|
|
306
|
+
if (!pending)
|
|
307
|
+
return;
|
|
308
|
+
if (frame["ok"] === false) {
|
|
309
|
+
pending.reject(new RealtimeError(String(frame["error"] ?? "error"), String(frame["reason"] ?? frame["error"] ?? "The request was refused.")));
|
|
310
|
+
}
|
|
311
|
+
else if (pending.expect === "ack")
|
|
312
|
+
pending.resolve(frame);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
case "history": {
|
|
316
|
+
const pending = this.take(frame["ref"]);
|
|
317
|
+
pending?.resolve(frame);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
case "message": {
|
|
321
|
+
this.channels.get(String(frame["channel"]))?.deliver(frame);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
case "presence_state": {
|
|
325
|
+
this.channels.get(String(frame["channel"]))?.setPresence(frame["members"] ?? []);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
case "presence_diff": {
|
|
329
|
+
this.channels
|
|
330
|
+
.get(String(frame["channel"]))
|
|
331
|
+
?.applyPresence(frame["joins"] ?? [], frame["leaves"] ?? []);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
case "pong":
|
|
335
|
+
case "welcome":
|
|
336
|
+
return;
|
|
337
|
+
case "error":
|
|
338
|
+
this.options.onProtocolError?.(`the server reported: ${String(frame["error"])}`);
|
|
339
|
+
return;
|
|
340
|
+
default:
|
|
341
|
+
this.options.onProtocolError?.(`an unknown frame arrived: ${String(frame["type"])}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
take(ref) {
|
|
345
|
+
if (typeof ref !== "string")
|
|
346
|
+
return undefined;
|
|
347
|
+
const pending = this.pending.get(ref);
|
|
348
|
+
if (!pending)
|
|
349
|
+
return undefined;
|
|
350
|
+
clearTimeout(pending.timer);
|
|
351
|
+
this.pending.delete(ref);
|
|
352
|
+
return pending;
|
|
353
|
+
}
|
|
354
|
+
failPending(error) {
|
|
355
|
+
for (const [ref, pending] of this.pending) {
|
|
356
|
+
clearTimeout(pending.timer);
|
|
357
|
+
pending.reject(error);
|
|
358
|
+
this.pending.delete(ref);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/** Send a frame and wait for its acknowledgement (or, for `history`, its answer). */
|
|
362
|
+
request(frame, expect = "ack") {
|
|
363
|
+
const socket = this.socket;
|
|
364
|
+
if (!socket || socket.readyState !== OPEN) {
|
|
365
|
+
return Promise.reject(new RealtimeError("not_connected", "The realtime connection is not open."));
|
|
366
|
+
}
|
|
367
|
+
const ref = String(++this.refs);
|
|
368
|
+
return new Promise((resolve, reject) => {
|
|
369
|
+
const timeoutMs = this.options.ackTimeoutMs ?? 10_000;
|
|
370
|
+
const timer = setTimeout(() => {
|
|
371
|
+
this.pending.delete(ref);
|
|
372
|
+
reject(new RealtimeError("ack_timeout", `The server did not answer within ${timeoutMs} ms.`));
|
|
373
|
+
}, timeoutMs);
|
|
374
|
+
this.pending.set(ref, { resolve, reject, timer, expect });
|
|
375
|
+
try {
|
|
376
|
+
socket.send(JSON.stringify({ ...frame, ref }));
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
clearTimeout(timer);
|
|
380
|
+
this.pending.delete(ref);
|
|
381
|
+
reject(new RealtimeError("send_failed", "Could not send on the realtime connection.", { cause: error }));
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
// ── HTTP counterparts (no socket needed) ──────────────────────────────
|
|
386
|
+
/** Publish from anywhere, without a socket. The channel's `publish` rule applies. */
|
|
387
|
+
publish(channel, event, payload) {
|
|
388
|
+
return this.http.request({ method: "POST", path: "/realtime/v1/publish", body: { channel, event, payload } });
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Listen to channels over Server-Sent Events: one-way, over plain HTTP, with no socket to manage. Iterate the frames
|
|
392
|
+
* (`welcome`, `message`, `presence_diff`) until you `break` or abort the `signal`. Publish with {@link publish}. At most 20 channels
|
|
393
|
+
* per stream; the signed-in user, if any, is who is listening.
|
|
394
|
+
*
|
|
395
|
+
* ```ts
|
|
396
|
+
* for await (const frame of pawabase.realtime.stream(["chat:lobby"])) if (frame.type === "message") show(frame.payload);
|
|
397
|
+
* ```
|
|
398
|
+
*/
|
|
399
|
+
async *stream(channels, options = {}) {
|
|
400
|
+
const names = Array.isArray(channels) ? channels : [channels];
|
|
401
|
+
if (names.length === 0)
|
|
402
|
+
throw new ConfigError("Pass at least one channel to listen to.");
|
|
403
|
+
const token = (await this.auth?.getAccessToken().catch(() => null)) ?? null;
|
|
404
|
+
const response = await this.http.send({
|
|
405
|
+
path: "/realtime/v1/sse",
|
|
406
|
+
query: { channels: names.join(","), ...(token ? { token } : {}) },
|
|
407
|
+
headers: { accept: "text/event-stream" },
|
|
408
|
+
retry: false,
|
|
409
|
+
timeoutMs: 0, // a stream is meant to stay open
|
|
410
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
411
|
+
});
|
|
412
|
+
if (!response.body)
|
|
413
|
+
return;
|
|
414
|
+
const reader = response.body.getReader();
|
|
415
|
+
const decoder = new TextDecoder();
|
|
416
|
+
let buffer = "";
|
|
417
|
+
let data = [];
|
|
418
|
+
const flush = () => {
|
|
419
|
+
const text = data.join("\n");
|
|
420
|
+
data = [];
|
|
421
|
+
if (!text)
|
|
422
|
+
return null;
|
|
423
|
+
try {
|
|
424
|
+
const frame = JSON.parse(text);
|
|
425
|
+
return frame && typeof frame === "object" && !Array.isArray(frame) ? frame : null;
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
try {
|
|
432
|
+
for (;;) {
|
|
433
|
+
const { done, value } = await reader.read();
|
|
434
|
+
if (done)
|
|
435
|
+
break;
|
|
436
|
+
buffer += decoder.decode(value, { stream: true });
|
|
437
|
+
let newline = buffer.indexOf("\n");
|
|
438
|
+
while (newline >= 0) {
|
|
439
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
440
|
+
buffer = buffer.slice(newline + 1);
|
|
441
|
+
if (line === "") {
|
|
442
|
+
const frame = flush();
|
|
443
|
+
if (frame)
|
|
444
|
+
yield frame;
|
|
445
|
+
}
|
|
446
|
+
else if (line.startsWith("data:")) {
|
|
447
|
+
data.push(line.slice(5).replace(/^ /, ""));
|
|
448
|
+
}
|
|
449
|
+
newline = buffer.indexOf("\n");
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const last = flush();
|
|
453
|
+
if (last)
|
|
454
|
+
yield last;
|
|
455
|
+
}
|
|
456
|
+
finally {
|
|
457
|
+
await reader.cancel().catch(() => undefined);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/** Who is on a channel right now. */
|
|
461
|
+
async presence(channel) {
|
|
462
|
+
return (await this.http.request({ path: `/realtime/v1/channels/${encodePath(channel)}/presence` })).members;
|
|
463
|
+
}
|
|
464
|
+
/** Recent messages on a channel, oldest first. */
|
|
465
|
+
async history(channel, limit = 50) {
|
|
466
|
+
return (await this.http.request({
|
|
467
|
+
path: `/realtime/v1/channels/${encodePath(channel)}/history`,
|
|
468
|
+
query: { limit },
|
|
469
|
+
})).messages;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Wait until the socket is really open, or `ms` pass: after a drop the status can lag the
|
|
473
|
+
* socket for a moment, and a reconnect takes a few. Connects first when idle or closed.
|
|
474
|
+
*/
|
|
475
|
+
async whenOpen(ms = 5000) {
|
|
476
|
+
const deadline = Date.now() + ms;
|
|
477
|
+
for (;;) {
|
|
478
|
+
if (this.currentStatus === "open" && this.socket?.readyState === OPEN)
|
|
479
|
+
return;
|
|
480
|
+
if (Date.now() > deadline)
|
|
481
|
+
throw new RealtimeError("not_connected", "The realtime connection did not open in time.");
|
|
482
|
+
if (this.currentStatus === "idle" || (this.currentStatus === "closed" && !this.manual))
|
|
483
|
+
await this.connect().catch(() => undefined);
|
|
484
|
+
else
|
|
485
|
+
await sleep(20);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/** One named channel: receive messages and presence, publish, and see who is here. */
|
|
490
|
+
export class RealtimeChannel {
|
|
491
|
+
client;
|
|
492
|
+
name;
|
|
493
|
+
handlers = new Map();
|
|
494
|
+
options;
|
|
495
|
+
joined = false;
|
|
496
|
+
joining = null;
|
|
497
|
+
wanted = false;
|
|
498
|
+
members = new Map();
|
|
499
|
+
seen = new Set();
|
|
500
|
+
order = [];
|
|
501
|
+
queue = [];
|
|
502
|
+
waiters = [];
|
|
503
|
+
ended = false;
|
|
504
|
+
constructor(client, name, options) {
|
|
505
|
+
this.client = client;
|
|
506
|
+
this.name = name;
|
|
507
|
+
this.options = { ...options };
|
|
508
|
+
}
|
|
509
|
+
/** @internal */
|
|
510
|
+
configure(options) {
|
|
511
|
+
this.options = { ...this.options, ...options };
|
|
512
|
+
}
|
|
513
|
+
/** True while the channel is subscribed on the open connection. */
|
|
514
|
+
get subscribed() {
|
|
515
|
+
return this.joined;
|
|
516
|
+
}
|
|
517
|
+
/** @internal Whether the channel should rejoin after a reconnect. */
|
|
518
|
+
get wantsJoin() {
|
|
519
|
+
return this.wanted;
|
|
520
|
+
}
|
|
521
|
+
/** Listen to `message`, `presence`, `subscribed`, `closed` or `error`. Returns a function that stops listening. */
|
|
522
|
+
on(event, handler) {
|
|
523
|
+
let set = this.handlers.get(event);
|
|
524
|
+
if (!set)
|
|
525
|
+
this.handlers.set(event, (set = new Set()));
|
|
526
|
+
set.add(handler);
|
|
527
|
+
return () => void set.delete(handler);
|
|
528
|
+
}
|
|
529
|
+
/** Listen to messages with one event name only. */
|
|
530
|
+
onEvent(event, handler) {
|
|
531
|
+
return this.on("message", (message) => {
|
|
532
|
+
if (message.event === event)
|
|
533
|
+
handler(message);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
emit(event, value) {
|
|
537
|
+
for (const handler of [...(this.handlers.get(event) ?? [])]) {
|
|
538
|
+
try {
|
|
539
|
+
handler(value);
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
/* a listener must not break delivery */
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Start receiving. Connects first if needed. Rejects with a {@link RealtimeError} when the
|
|
548
|
+
* channel's rule refuses (`code: "forbidden"`). Safe to call twice.
|
|
549
|
+
*/
|
|
550
|
+
async subscribe() {
|
|
551
|
+
this.ended = false;
|
|
552
|
+
if (this.joined)
|
|
553
|
+
return this;
|
|
554
|
+
if (!this.joining) {
|
|
555
|
+
this.joining = (async () => {
|
|
556
|
+
await this.client.connect();
|
|
557
|
+
await this.join(false);
|
|
558
|
+
// Only a channel that joined is rejoined after a reconnect: the client rejoins
|
|
559
|
+
// on its own at welcome, and must not race this first join.
|
|
560
|
+
this.wanted = true;
|
|
561
|
+
})().finally(() => {
|
|
562
|
+
this.joining = null;
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
await this.joining;
|
|
566
|
+
return this;
|
|
567
|
+
}
|
|
568
|
+
/** @internal Rejoin after a reconnect. */
|
|
569
|
+
async rejoin() {
|
|
570
|
+
await this.join(true);
|
|
571
|
+
}
|
|
572
|
+
async join(rejoining) {
|
|
573
|
+
try {
|
|
574
|
+
await this.client.request({
|
|
575
|
+
type: "subscribe",
|
|
576
|
+
channel: this.name,
|
|
577
|
+
...(this.options.presence ? { presence: this.options.presence } : {}),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
if (error instanceof RealtimeError)
|
|
582
|
+
this.emit("error", error);
|
|
583
|
+
throw error;
|
|
584
|
+
}
|
|
585
|
+
this.joined = true;
|
|
586
|
+
this.emit("subscribed", undefined);
|
|
587
|
+
if (rejoining && (this.options.replay ?? 0) > 0) {
|
|
588
|
+
const history = await this.history(this.options.replay).catch(() => []);
|
|
589
|
+
for (const message of history)
|
|
590
|
+
this.deliver(message);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/** Stop receiving. Pending iterators end. */
|
|
594
|
+
async unsubscribe() {
|
|
595
|
+
this.wanted = false;
|
|
596
|
+
if (this.joined) {
|
|
597
|
+
await this.client.request({ type: "unsubscribe", channel: this.name }).catch(() => undefined);
|
|
598
|
+
}
|
|
599
|
+
this.joined = false;
|
|
600
|
+
this.members.clear();
|
|
601
|
+
this.end("unsubscribed");
|
|
602
|
+
this.client.forget(this.name);
|
|
603
|
+
}
|
|
604
|
+
/** Publish to the channel over the socket. Resolves with the message id once the server accepted it. */
|
|
605
|
+
async send(event, payload) {
|
|
606
|
+
await this.client.whenOpen();
|
|
607
|
+
const ack = await this.client.request({ type: "publish", channel: this.name, event, payload });
|
|
608
|
+
return { id: String(ack["id"] ?? "") };
|
|
609
|
+
}
|
|
610
|
+
/** Set the presence data others see for you. Subscribe first. */
|
|
611
|
+
async track(meta) {
|
|
612
|
+
this.options = { ...this.options, presence: meta };
|
|
613
|
+
await this.client.whenOpen();
|
|
614
|
+
await this.client.request({ type: "presence", channel: this.name, meta });
|
|
615
|
+
}
|
|
616
|
+
/** Recent messages, oldest first. */
|
|
617
|
+
async history(limit = 50) {
|
|
618
|
+
await this.client.whenOpen();
|
|
619
|
+
const reply = await this.client.request({ type: "history", channel: this.name, limit }, "history");
|
|
620
|
+
return reply["messages"] ?? [];
|
|
621
|
+
}
|
|
622
|
+
/** Who is on the channel now, as last told by the server. Empty unless presence is enabled for it. */
|
|
623
|
+
presenceState() {
|
|
624
|
+
return [...this.members.values()];
|
|
625
|
+
}
|
|
626
|
+
// ── delivery (called by the client) ───────────────────────────────────
|
|
627
|
+
/** @internal */
|
|
628
|
+
deliver(message) {
|
|
629
|
+
if (message.id) {
|
|
630
|
+
if (this.seen.has(message.id))
|
|
631
|
+
return;
|
|
632
|
+
this.seen.add(message.id);
|
|
633
|
+
this.order.push(message.id);
|
|
634
|
+
if (this.order.length > SEEN_LIMIT)
|
|
635
|
+
this.seen.delete(this.order.shift());
|
|
636
|
+
}
|
|
637
|
+
const typed = message;
|
|
638
|
+
this.emit("message", typed);
|
|
639
|
+
const waiter = this.waiters.shift();
|
|
640
|
+
if (waiter)
|
|
641
|
+
waiter({ value: typed, done: false });
|
|
642
|
+
else {
|
|
643
|
+
this.queue.push(typed);
|
|
644
|
+
if (this.queue.length > 1000)
|
|
645
|
+
this.queue.shift();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/** @internal */
|
|
649
|
+
setPresence(list) {
|
|
650
|
+
this.members.clear();
|
|
651
|
+
for (const member of list)
|
|
652
|
+
this.members.set(member.presence_ref, member);
|
|
653
|
+
this.emit("presence", { joins: [...this.members.values()], leaves: [], members: this.presenceState() });
|
|
654
|
+
}
|
|
655
|
+
/** @internal */
|
|
656
|
+
applyPresence(joins, leaves) {
|
|
657
|
+
for (const member of leaves)
|
|
658
|
+
this.members.delete(member.presence_ref);
|
|
659
|
+
for (const member of joins)
|
|
660
|
+
this.members.set(member.presence_ref, member);
|
|
661
|
+
this.emit("presence", {
|
|
662
|
+
joins: joins,
|
|
663
|
+
leaves: leaves,
|
|
664
|
+
members: this.presenceState(),
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
/** @internal The connection dropped; the channel will rejoin by itself if it can. */
|
|
668
|
+
markClosed(reason) {
|
|
669
|
+
if (!this.joined)
|
|
670
|
+
return;
|
|
671
|
+
this.joined = false;
|
|
672
|
+
this.members.clear();
|
|
673
|
+
this.emit("closed", { reason });
|
|
674
|
+
}
|
|
675
|
+
end(reason) {
|
|
676
|
+
this.ended = true;
|
|
677
|
+
this.emit("closed", { reason });
|
|
678
|
+
for (const waiter of this.waiters.splice(0))
|
|
679
|
+
waiter({ value: undefined, done: true });
|
|
680
|
+
}
|
|
681
|
+
/** Iterate messages: `for await (const message of channel) { … }`. Ends on `unsubscribe()`. */
|
|
682
|
+
[Symbol.asyncIterator]() {
|
|
683
|
+
return {
|
|
684
|
+
next: () => {
|
|
685
|
+
const ready = this.queue.shift();
|
|
686
|
+
if (ready)
|
|
687
|
+
return Promise.resolve({ value: ready, done: false });
|
|
688
|
+
if (this.ended)
|
|
689
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
690
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
691
|
+
},
|
|
692
|
+
return: async () => {
|
|
693
|
+
await this.unsubscribe();
|
|
694
|
+
return { value: undefined, done: true };
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
//# sourceMappingURL=realtime.js.map
|