@fire-spark/realtime 0.0.1
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/dist/index.d.ts +80 -0
- package/dist/index.js +4096 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4096 @@
|
|
|
1
|
+
import { isStorefrontNotFound, services, storefrontJson } from "@fire-spark/core";
|
|
2
|
+
//#region ../../node_modules/.pnpm/@supabase+realtime-js@2.100.1/node_modules/@supabase/realtime-js/dist/module/lib/websocket-factory.js
|
|
3
|
+
/**
|
|
4
|
+
* Utilities for creating WebSocket instances across runtimes.
|
|
5
|
+
*/
|
|
6
|
+
var WebSocketFactory = class {
|
|
7
|
+
/**
|
|
8
|
+
* Static-only utility – prevent instantiation.
|
|
9
|
+
*/
|
|
10
|
+
constructor() {}
|
|
11
|
+
static detectEnvironment() {
|
|
12
|
+
var _a;
|
|
13
|
+
if (typeof WebSocket !== "undefined") return {
|
|
14
|
+
type: "native",
|
|
15
|
+
constructor: WebSocket
|
|
16
|
+
};
|
|
17
|
+
if (typeof globalThis !== "undefined" && typeof globalThis.WebSocket !== "undefined") return {
|
|
18
|
+
type: "native",
|
|
19
|
+
constructor: globalThis.WebSocket
|
|
20
|
+
};
|
|
21
|
+
if (typeof global !== "undefined" && typeof global.WebSocket !== "undefined") return {
|
|
22
|
+
type: "native",
|
|
23
|
+
constructor: global.WebSocket
|
|
24
|
+
};
|
|
25
|
+
if (typeof globalThis !== "undefined" && typeof globalThis.WebSocketPair !== "undefined" && typeof globalThis.WebSocket === "undefined") return {
|
|
26
|
+
type: "cloudflare",
|
|
27
|
+
error: "Cloudflare Workers detected. WebSocket clients are not supported in Cloudflare Workers.",
|
|
28
|
+
workaround: "Use Cloudflare Workers WebSocket API for server-side WebSocket handling, or deploy to a different runtime."
|
|
29
|
+
};
|
|
30
|
+
if (typeof globalThis !== "undefined" && globalThis.EdgeRuntime || typeof navigator !== "undefined" && ((_a = navigator.userAgent) === null || _a === void 0 ? void 0 : _a.includes("Vercel-Edge"))) return {
|
|
31
|
+
type: "unsupported",
|
|
32
|
+
error: "Edge runtime detected (Vercel Edge/Netlify Edge). WebSockets are not supported in edge functions.",
|
|
33
|
+
workaround: "Use serverless functions or a different deployment target for WebSocket functionality."
|
|
34
|
+
};
|
|
35
|
+
const _process = globalThis["process"];
|
|
36
|
+
if (_process) {
|
|
37
|
+
const processVersions = _process["versions"];
|
|
38
|
+
if (processVersions && processVersions["node"]) {
|
|
39
|
+
const versionString = processVersions["node"];
|
|
40
|
+
const nodeVersion = parseInt(versionString.replace(/^v/, "").split(".")[0]);
|
|
41
|
+
if (nodeVersion >= 22) {
|
|
42
|
+
if (typeof globalThis.WebSocket !== "undefined") return {
|
|
43
|
+
type: "native",
|
|
44
|
+
constructor: globalThis.WebSocket
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
type: "unsupported",
|
|
48
|
+
error: `Node.js ${nodeVersion} detected but native WebSocket not found.`,
|
|
49
|
+
workaround: "Provide a WebSocket implementation via the transport option."
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
type: "unsupported",
|
|
54
|
+
error: `Node.js ${nodeVersion} detected without native WebSocket support.`,
|
|
55
|
+
workaround: "For Node.js < 22, install \"ws\" package and provide it via the transport option:\nimport ws from \"ws\"\nnew RealtimeClient(url, { transport: ws })"
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
type: "unsupported",
|
|
61
|
+
error: "Unknown JavaScript runtime without WebSocket support.",
|
|
62
|
+
workaround: "Ensure you're running in a supported environment (browser, Node.js, Deno) or provide a custom WebSocket implementation."
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns the best available WebSocket constructor for the current runtime.
|
|
67
|
+
*
|
|
68
|
+
* @category Realtime
|
|
69
|
+
*
|
|
70
|
+
* @example Example with error handling
|
|
71
|
+
* ```ts
|
|
72
|
+
* try {
|
|
73
|
+
* const WS = WebSocketFactory.getWebSocketConstructor()
|
|
74
|
+
* const socket = new WS('wss://example.com/socket')
|
|
75
|
+
* } catch (error) {
|
|
76
|
+
* console.error('WebSocket not available in this environment.', error)
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
static getWebSocketConstructor() {
|
|
81
|
+
const env = this.detectEnvironment();
|
|
82
|
+
if (env.constructor) return env.constructor;
|
|
83
|
+
let errorMessage = env.error || "WebSocket not supported in this environment.";
|
|
84
|
+
if (env.workaround) errorMessage += `\n\nSuggested solution: ${env.workaround}`;
|
|
85
|
+
throw new Error(errorMessage);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Detects whether the runtime can establish WebSocket connections.
|
|
89
|
+
*
|
|
90
|
+
* @category Realtime
|
|
91
|
+
*
|
|
92
|
+
* @example Example in a Node.js script
|
|
93
|
+
* ```ts
|
|
94
|
+
* if (!WebSocketFactory.isWebSocketSupported()) {
|
|
95
|
+
* console.error('WebSockets are required for this script.')
|
|
96
|
+
* process.exitCode = 1
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
static isWebSocketSupported() {
|
|
101
|
+
try {
|
|
102
|
+
const env = this.detectEnvironment();
|
|
103
|
+
return env.type === "native" || env.type === "ws";
|
|
104
|
+
} catch (_a) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region ../../node_modules/.pnpm/@supabase+realtime-js@2.100.1/node_modules/@supabase/realtime-js/dist/module/lib/constants.js
|
|
111
|
+
const DEFAULT_VERSION = `realtime-js/2.100.1`;
|
|
112
|
+
const VSN_1_0_0 = "1.0.0";
|
|
113
|
+
const VSN_2_0_0 = "2.0.0";
|
|
114
|
+
const DEFAULT_VSN$1 = VSN_2_0_0;
|
|
115
|
+
const DEFAULT_TIMEOUT$1 = 1e4;
|
|
116
|
+
const CHANNEL_STATES$1 = {
|
|
117
|
+
closed: "closed",
|
|
118
|
+
errored: "errored",
|
|
119
|
+
joined: "joined",
|
|
120
|
+
joining: "joining",
|
|
121
|
+
leaving: "leaving"
|
|
122
|
+
};
|
|
123
|
+
const CHANNEL_EVENTS$1 = {
|
|
124
|
+
close: "phx_close",
|
|
125
|
+
error: "phx_error",
|
|
126
|
+
join: "phx_join",
|
|
127
|
+
reply: "phx_reply",
|
|
128
|
+
leave: "phx_leave",
|
|
129
|
+
access_token: "access_token"
|
|
130
|
+
};
|
|
131
|
+
const CONNECTION_STATE = {
|
|
132
|
+
connecting: "connecting",
|
|
133
|
+
open: "open",
|
|
134
|
+
closing: "closing",
|
|
135
|
+
closed: "closed"
|
|
136
|
+
};
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region ../../node_modules/.pnpm/@supabase+realtime-js@2.100.1/node_modules/@supabase/realtime-js/dist/module/lib/serializer.js
|
|
139
|
+
var Serializer = class {
|
|
140
|
+
constructor(allowedMetadataKeys) {
|
|
141
|
+
this.HEADER_LENGTH = 1;
|
|
142
|
+
this.USER_BROADCAST_PUSH_META_LENGTH = 6;
|
|
143
|
+
this.KINDS = {
|
|
144
|
+
userBroadcastPush: 3,
|
|
145
|
+
userBroadcast: 4
|
|
146
|
+
};
|
|
147
|
+
this.BINARY_ENCODING = 0;
|
|
148
|
+
this.JSON_ENCODING = 1;
|
|
149
|
+
this.BROADCAST_EVENT = "broadcast";
|
|
150
|
+
this.allowedMetadataKeys = [];
|
|
151
|
+
this.allowedMetadataKeys = allowedMetadataKeys !== null && allowedMetadataKeys !== void 0 ? allowedMetadataKeys : [];
|
|
152
|
+
}
|
|
153
|
+
encode(msg, callback) {
|
|
154
|
+
if (msg.event === this.BROADCAST_EVENT && !(msg.payload instanceof ArrayBuffer) && typeof msg.payload.event === "string") return callback(this._binaryEncodeUserBroadcastPush(msg));
|
|
155
|
+
let payload = [
|
|
156
|
+
msg.join_ref,
|
|
157
|
+
msg.ref,
|
|
158
|
+
msg.topic,
|
|
159
|
+
msg.event,
|
|
160
|
+
msg.payload
|
|
161
|
+
];
|
|
162
|
+
return callback(JSON.stringify(payload));
|
|
163
|
+
}
|
|
164
|
+
_binaryEncodeUserBroadcastPush(message) {
|
|
165
|
+
var _a;
|
|
166
|
+
if (this._isArrayBuffer((_a = message.payload) === null || _a === void 0 ? void 0 : _a.payload)) return this._encodeBinaryUserBroadcastPush(message);
|
|
167
|
+
else return this._encodeJsonUserBroadcastPush(message);
|
|
168
|
+
}
|
|
169
|
+
_encodeBinaryUserBroadcastPush(message) {
|
|
170
|
+
var _a, _b;
|
|
171
|
+
const userPayload = (_b = (_a = message.payload) === null || _a === void 0 ? void 0 : _a.payload) !== null && _b !== void 0 ? _b : /* @__PURE__ */ new ArrayBuffer(0);
|
|
172
|
+
return this._encodeUserBroadcastPush(message, this.BINARY_ENCODING, userPayload);
|
|
173
|
+
}
|
|
174
|
+
_encodeJsonUserBroadcastPush(message) {
|
|
175
|
+
var _a, _b;
|
|
176
|
+
const userPayload = (_b = (_a = message.payload) === null || _a === void 0 ? void 0 : _a.payload) !== null && _b !== void 0 ? _b : {};
|
|
177
|
+
const encodedUserPayload = new TextEncoder().encode(JSON.stringify(userPayload)).buffer;
|
|
178
|
+
return this._encodeUserBroadcastPush(message, this.JSON_ENCODING, encodedUserPayload);
|
|
179
|
+
}
|
|
180
|
+
_encodeUserBroadcastPush(message, encodingType, encodedPayload) {
|
|
181
|
+
var _a, _b;
|
|
182
|
+
const topic = message.topic;
|
|
183
|
+
const ref = (_a = message.ref) !== null && _a !== void 0 ? _a : "";
|
|
184
|
+
const joinRef = (_b = message.join_ref) !== null && _b !== void 0 ? _b : "";
|
|
185
|
+
const userEvent = message.payload.event;
|
|
186
|
+
const rest = this.allowedMetadataKeys ? this._pick(message.payload, this.allowedMetadataKeys) : {};
|
|
187
|
+
const metadata = Object.keys(rest).length === 0 ? "" : JSON.stringify(rest);
|
|
188
|
+
if (joinRef.length > 255) throw new Error(`joinRef length ${joinRef.length} exceeds maximum of 255`);
|
|
189
|
+
if (ref.length > 255) throw new Error(`ref length ${ref.length} exceeds maximum of 255`);
|
|
190
|
+
if (topic.length > 255) throw new Error(`topic length ${topic.length} exceeds maximum of 255`);
|
|
191
|
+
if (userEvent.length > 255) throw new Error(`userEvent length ${userEvent.length} exceeds maximum of 255`);
|
|
192
|
+
if (metadata.length > 255) throw new Error(`metadata length ${metadata.length} exceeds maximum of 255`);
|
|
193
|
+
const metaLength = this.USER_BROADCAST_PUSH_META_LENGTH + joinRef.length + ref.length + topic.length + userEvent.length + metadata.length;
|
|
194
|
+
const header = new ArrayBuffer(this.HEADER_LENGTH + metaLength);
|
|
195
|
+
let view = new DataView(header);
|
|
196
|
+
let offset = 0;
|
|
197
|
+
view.setUint8(offset++, this.KINDS.userBroadcastPush);
|
|
198
|
+
view.setUint8(offset++, joinRef.length);
|
|
199
|
+
view.setUint8(offset++, ref.length);
|
|
200
|
+
view.setUint8(offset++, topic.length);
|
|
201
|
+
view.setUint8(offset++, userEvent.length);
|
|
202
|
+
view.setUint8(offset++, metadata.length);
|
|
203
|
+
view.setUint8(offset++, encodingType);
|
|
204
|
+
Array.from(joinRef, (char) => view.setUint8(offset++, char.charCodeAt(0)));
|
|
205
|
+
Array.from(ref, (char) => view.setUint8(offset++, char.charCodeAt(0)));
|
|
206
|
+
Array.from(topic, (char) => view.setUint8(offset++, char.charCodeAt(0)));
|
|
207
|
+
Array.from(userEvent, (char) => view.setUint8(offset++, char.charCodeAt(0)));
|
|
208
|
+
Array.from(metadata, (char) => view.setUint8(offset++, char.charCodeAt(0)));
|
|
209
|
+
var combined = new Uint8Array(header.byteLength + encodedPayload.byteLength);
|
|
210
|
+
combined.set(new Uint8Array(header), 0);
|
|
211
|
+
combined.set(new Uint8Array(encodedPayload), header.byteLength);
|
|
212
|
+
return combined.buffer;
|
|
213
|
+
}
|
|
214
|
+
decode(rawPayload, callback) {
|
|
215
|
+
if (this._isArrayBuffer(rawPayload)) return callback(this._binaryDecode(rawPayload));
|
|
216
|
+
if (typeof rawPayload === "string") {
|
|
217
|
+
const [join_ref, ref, topic, event, payload] = JSON.parse(rawPayload);
|
|
218
|
+
return callback({
|
|
219
|
+
join_ref,
|
|
220
|
+
ref,
|
|
221
|
+
topic,
|
|
222
|
+
event,
|
|
223
|
+
payload
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return callback({});
|
|
227
|
+
}
|
|
228
|
+
_binaryDecode(buffer) {
|
|
229
|
+
const view = new DataView(buffer);
|
|
230
|
+
const kind = view.getUint8(0);
|
|
231
|
+
const decoder = new TextDecoder();
|
|
232
|
+
switch (kind) {
|
|
233
|
+
case this.KINDS.userBroadcast: return this._decodeUserBroadcast(buffer, view, decoder);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
_decodeUserBroadcast(buffer, view, decoder) {
|
|
237
|
+
const topicSize = view.getUint8(1);
|
|
238
|
+
const userEventSize = view.getUint8(2);
|
|
239
|
+
const metadataSize = view.getUint8(3);
|
|
240
|
+
const payloadEncoding = view.getUint8(4);
|
|
241
|
+
let offset = this.HEADER_LENGTH + 4;
|
|
242
|
+
const topic = decoder.decode(buffer.slice(offset, offset + topicSize));
|
|
243
|
+
offset = offset + topicSize;
|
|
244
|
+
const userEvent = decoder.decode(buffer.slice(offset, offset + userEventSize));
|
|
245
|
+
offset = offset + userEventSize;
|
|
246
|
+
const metadata = decoder.decode(buffer.slice(offset, offset + metadataSize));
|
|
247
|
+
offset = offset + metadataSize;
|
|
248
|
+
const payload = buffer.slice(offset, buffer.byteLength);
|
|
249
|
+
const parsedPayload = payloadEncoding === this.JSON_ENCODING ? JSON.parse(decoder.decode(payload)) : payload;
|
|
250
|
+
const data = {
|
|
251
|
+
type: this.BROADCAST_EVENT,
|
|
252
|
+
event: userEvent,
|
|
253
|
+
payload: parsedPayload
|
|
254
|
+
};
|
|
255
|
+
if (metadataSize > 0) data["meta"] = JSON.parse(metadata);
|
|
256
|
+
return {
|
|
257
|
+
join_ref: null,
|
|
258
|
+
ref: null,
|
|
259
|
+
topic,
|
|
260
|
+
event: this.BROADCAST_EVENT,
|
|
261
|
+
payload: data
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
_isArrayBuffer(buffer) {
|
|
265
|
+
var _a;
|
|
266
|
+
return buffer instanceof ArrayBuffer || ((_a = buffer === null || buffer === void 0 ? void 0 : buffer.constructor) === null || _a === void 0 ? void 0 : _a.name) === "ArrayBuffer";
|
|
267
|
+
}
|
|
268
|
+
_pick(obj, keys) {
|
|
269
|
+
if (!obj || typeof obj !== "object") return {};
|
|
270
|
+
return Object.fromEntries(Object.entries(obj).filter(([key]) => keys.includes(key)));
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region ../../node_modules/.pnpm/@supabase+realtime-js@2.100.1/node_modules/@supabase/realtime-js/dist/module/lib/transformers.js
|
|
275
|
+
/**
|
|
276
|
+
* Helpers to convert the change Payload into native JS types.
|
|
277
|
+
*/
|
|
278
|
+
var PostgresTypes;
|
|
279
|
+
(function(PostgresTypes) {
|
|
280
|
+
PostgresTypes["abstime"] = "abstime";
|
|
281
|
+
PostgresTypes["bool"] = "bool";
|
|
282
|
+
PostgresTypes["date"] = "date";
|
|
283
|
+
PostgresTypes["daterange"] = "daterange";
|
|
284
|
+
PostgresTypes["float4"] = "float4";
|
|
285
|
+
PostgresTypes["float8"] = "float8";
|
|
286
|
+
PostgresTypes["int2"] = "int2";
|
|
287
|
+
PostgresTypes["int4"] = "int4";
|
|
288
|
+
PostgresTypes["int4range"] = "int4range";
|
|
289
|
+
PostgresTypes["int8"] = "int8";
|
|
290
|
+
PostgresTypes["int8range"] = "int8range";
|
|
291
|
+
PostgresTypes["json"] = "json";
|
|
292
|
+
PostgresTypes["jsonb"] = "jsonb";
|
|
293
|
+
PostgresTypes["money"] = "money";
|
|
294
|
+
PostgresTypes["numeric"] = "numeric";
|
|
295
|
+
PostgresTypes["oid"] = "oid";
|
|
296
|
+
PostgresTypes["reltime"] = "reltime";
|
|
297
|
+
PostgresTypes["text"] = "text";
|
|
298
|
+
PostgresTypes["time"] = "time";
|
|
299
|
+
PostgresTypes["timestamp"] = "timestamp";
|
|
300
|
+
PostgresTypes["timestamptz"] = "timestamptz";
|
|
301
|
+
PostgresTypes["timetz"] = "timetz";
|
|
302
|
+
PostgresTypes["tsrange"] = "tsrange";
|
|
303
|
+
PostgresTypes["tstzrange"] = "tstzrange";
|
|
304
|
+
})(PostgresTypes || (PostgresTypes = {}));
|
|
305
|
+
/**
|
|
306
|
+
* Takes an array of columns and an object of string values then converts each string value
|
|
307
|
+
* to its mapped type.
|
|
308
|
+
*
|
|
309
|
+
* @param {{name: String, type: String}[]} columns
|
|
310
|
+
* @param {Object} record
|
|
311
|
+
* @param {Object} options The map of various options that can be applied to the mapper
|
|
312
|
+
* @param {Array} options.skipTypes The array of types that should not be converted
|
|
313
|
+
*
|
|
314
|
+
* @example convertChangeData([{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age:'33'}, {})
|
|
315
|
+
* //=>{ first_name: 'Paul', age: 33 }
|
|
316
|
+
*/
|
|
317
|
+
const convertChangeData = (columns, record, options = {}) => {
|
|
318
|
+
var _a;
|
|
319
|
+
const skipTypes = (_a = options.skipTypes) !== null && _a !== void 0 ? _a : [];
|
|
320
|
+
if (!record) return {};
|
|
321
|
+
return Object.keys(record).reduce((acc, rec_key) => {
|
|
322
|
+
acc[rec_key] = convertColumn(rec_key, columns, record, skipTypes);
|
|
323
|
+
return acc;
|
|
324
|
+
}, {});
|
|
325
|
+
};
|
|
326
|
+
/**
|
|
327
|
+
* Converts the value of an individual column.
|
|
328
|
+
*
|
|
329
|
+
* @param {String} columnName The column that you want to convert
|
|
330
|
+
* @param {{name: String, type: String}[]} columns All of the columns
|
|
331
|
+
* @param {Object} record The map of string values
|
|
332
|
+
* @param {Array} skipTypes An array of types that should not be converted
|
|
333
|
+
* @return {object} Useless information
|
|
334
|
+
*
|
|
335
|
+
* @example convertColumn('age', [{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age: '33'}, [])
|
|
336
|
+
* //=> 33
|
|
337
|
+
* @example convertColumn('age', [{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age: '33'}, ['int4'])
|
|
338
|
+
* //=> "33"
|
|
339
|
+
*/
|
|
340
|
+
const convertColumn = (columnName, columns, record, skipTypes) => {
|
|
341
|
+
const column = columns.find((x) => x.name === columnName);
|
|
342
|
+
const colType = column === null || column === void 0 ? void 0 : column.type;
|
|
343
|
+
const value = record[columnName];
|
|
344
|
+
if (colType && !skipTypes.includes(colType)) return convertCell(colType, value);
|
|
345
|
+
return noop(value);
|
|
346
|
+
};
|
|
347
|
+
/**
|
|
348
|
+
* If the value of the cell is `null`, returns null.
|
|
349
|
+
* Otherwise converts the string value to the correct type.
|
|
350
|
+
* @param {String} type A postgres column type
|
|
351
|
+
* @param {String} value The cell value
|
|
352
|
+
*
|
|
353
|
+
* @example convertCell('bool', 't')
|
|
354
|
+
* //=> true
|
|
355
|
+
* @example convertCell('int8', '10')
|
|
356
|
+
* //=> 10
|
|
357
|
+
* @example convertCell('_int4', '{1,2,3,4}')
|
|
358
|
+
* //=> [1,2,3,4]
|
|
359
|
+
*/
|
|
360
|
+
const convertCell = (type, value) => {
|
|
361
|
+
if (type.charAt(0) === "_") {
|
|
362
|
+
const dataType = type.slice(1, type.length);
|
|
363
|
+
return toArray(value, dataType);
|
|
364
|
+
}
|
|
365
|
+
switch (type) {
|
|
366
|
+
case PostgresTypes.bool: return toBoolean(value);
|
|
367
|
+
case PostgresTypes.float4:
|
|
368
|
+
case PostgresTypes.float8:
|
|
369
|
+
case PostgresTypes.int2:
|
|
370
|
+
case PostgresTypes.int4:
|
|
371
|
+
case PostgresTypes.int8:
|
|
372
|
+
case PostgresTypes.numeric:
|
|
373
|
+
case PostgresTypes.oid: return toNumber(value);
|
|
374
|
+
case PostgresTypes.json:
|
|
375
|
+
case PostgresTypes.jsonb: return toJson(value);
|
|
376
|
+
case PostgresTypes.timestamp: return toTimestampString(value);
|
|
377
|
+
case PostgresTypes.abstime:
|
|
378
|
+
case PostgresTypes.date:
|
|
379
|
+
case PostgresTypes.daterange:
|
|
380
|
+
case PostgresTypes.int4range:
|
|
381
|
+
case PostgresTypes.int8range:
|
|
382
|
+
case PostgresTypes.money:
|
|
383
|
+
case PostgresTypes.reltime:
|
|
384
|
+
case PostgresTypes.text:
|
|
385
|
+
case PostgresTypes.time:
|
|
386
|
+
case PostgresTypes.timestamptz:
|
|
387
|
+
case PostgresTypes.timetz:
|
|
388
|
+
case PostgresTypes.tsrange:
|
|
389
|
+
case PostgresTypes.tstzrange: return noop(value);
|
|
390
|
+
default: return noop(value);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
const noop = (value) => {
|
|
394
|
+
return value;
|
|
395
|
+
};
|
|
396
|
+
const toBoolean = (value) => {
|
|
397
|
+
switch (value) {
|
|
398
|
+
case "t": return true;
|
|
399
|
+
case "f": return false;
|
|
400
|
+
default: return value;
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
const toNumber = (value) => {
|
|
404
|
+
if (typeof value === "string") {
|
|
405
|
+
const parsedValue = parseFloat(value);
|
|
406
|
+
if (!Number.isNaN(parsedValue)) return parsedValue;
|
|
407
|
+
}
|
|
408
|
+
return value;
|
|
409
|
+
};
|
|
410
|
+
const toJson = (value) => {
|
|
411
|
+
if (typeof value === "string") try {
|
|
412
|
+
return JSON.parse(value);
|
|
413
|
+
} catch (_a) {
|
|
414
|
+
return value;
|
|
415
|
+
}
|
|
416
|
+
return value;
|
|
417
|
+
};
|
|
418
|
+
/**
|
|
419
|
+
* Converts a Postgres Array into a native JS array
|
|
420
|
+
*
|
|
421
|
+
* @example toArray('{}', 'int4')
|
|
422
|
+
* //=> []
|
|
423
|
+
* @example toArray('{"[2021-01-01,2021-12-31)","(2021-01-01,2021-12-32]"}', 'daterange')
|
|
424
|
+
* //=> ['[2021-01-01,2021-12-31)', '(2021-01-01,2021-12-32]']
|
|
425
|
+
* @example toArray([1,2,3,4], 'int4')
|
|
426
|
+
* //=> [1,2,3,4]
|
|
427
|
+
*/
|
|
428
|
+
const toArray = (value, type) => {
|
|
429
|
+
if (typeof value !== "string") return value;
|
|
430
|
+
const lastIdx = value.length - 1;
|
|
431
|
+
const closeBrace = value[lastIdx];
|
|
432
|
+
if (value[0] === "{" && closeBrace === "}") {
|
|
433
|
+
let arr;
|
|
434
|
+
const valTrim = value.slice(1, lastIdx);
|
|
435
|
+
try {
|
|
436
|
+
arr = JSON.parse("[" + valTrim + "]");
|
|
437
|
+
} catch (_) {
|
|
438
|
+
arr = valTrim ? valTrim.split(",") : [];
|
|
439
|
+
}
|
|
440
|
+
return arr.map((val) => convertCell(type, val));
|
|
441
|
+
}
|
|
442
|
+
return value;
|
|
443
|
+
};
|
|
444
|
+
/**
|
|
445
|
+
* Fixes timestamp to be ISO-8601. Swaps the space between the date and time for a 'T'
|
|
446
|
+
* See https://github.com/supabase/supabase/issues/18
|
|
447
|
+
*
|
|
448
|
+
* @example toTimestampString('2019-09-10 00:00:00')
|
|
449
|
+
* //=> '2019-09-10T00:00:00'
|
|
450
|
+
*/
|
|
451
|
+
const toTimestampString = (value) => {
|
|
452
|
+
if (typeof value === "string") return value.replace(" ", "T");
|
|
453
|
+
return value;
|
|
454
|
+
};
|
|
455
|
+
const httpEndpointURL = (socketUrl) => {
|
|
456
|
+
const wsUrl = new URL(socketUrl);
|
|
457
|
+
wsUrl.protocol = wsUrl.protocol.replace(/^ws/i, "http");
|
|
458
|
+
wsUrl.pathname = wsUrl.pathname.replace(/\/+$/, "").replace(/\/socket\/websocket$/i, "").replace(/\/socket$/i, "").replace(/\/websocket$/i, "");
|
|
459
|
+
if (wsUrl.pathname === "" || wsUrl.pathname === "/") wsUrl.pathname = "/api/broadcast";
|
|
460
|
+
else wsUrl.pathname = wsUrl.pathname + "/api/broadcast";
|
|
461
|
+
return wsUrl.href;
|
|
462
|
+
};
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region ../../node_modules/.pnpm/@supabase+phoenix@0.4.0/node_modules/@supabase/phoenix/priv/static/phoenix.mjs
|
|
465
|
+
var closure = (value) => {
|
|
466
|
+
if (typeof value === "function") return value;
|
|
467
|
+
else {
|
|
468
|
+
let closure2 = function() {
|
|
469
|
+
return value;
|
|
470
|
+
};
|
|
471
|
+
return closure2;
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
var globalSelf = typeof self !== "undefined" ? self : null;
|
|
475
|
+
var phxWindow = typeof window !== "undefined" ? window : null;
|
|
476
|
+
var global$1 = globalSelf || phxWindow || globalThis;
|
|
477
|
+
var DEFAULT_VSN = "2.0.0";
|
|
478
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
479
|
+
var WS_CLOSE_NORMAL = 1e3;
|
|
480
|
+
var SOCKET_STATES = (
|
|
481
|
+
/** @type {const} */
|
|
482
|
+
{
|
|
483
|
+
connecting: 0,
|
|
484
|
+
open: 1,
|
|
485
|
+
closing: 2,
|
|
486
|
+
closed: 3
|
|
487
|
+
});
|
|
488
|
+
var CHANNEL_STATES = (
|
|
489
|
+
/** @type {const} */
|
|
490
|
+
{
|
|
491
|
+
closed: "closed",
|
|
492
|
+
errored: "errored",
|
|
493
|
+
joined: "joined",
|
|
494
|
+
joining: "joining",
|
|
495
|
+
leaving: "leaving"
|
|
496
|
+
});
|
|
497
|
+
var CHANNEL_EVENTS = (
|
|
498
|
+
/** @type {const} */
|
|
499
|
+
{
|
|
500
|
+
close: "phx_close",
|
|
501
|
+
error: "phx_error",
|
|
502
|
+
join: "phx_join",
|
|
503
|
+
reply: "phx_reply",
|
|
504
|
+
leave: "phx_leave"
|
|
505
|
+
});
|
|
506
|
+
var TRANSPORTS = (
|
|
507
|
+
/** @type {const} */
|
|
508
|
+
{
|
|
509
|
+
longpoll: "longpoll",
|
|
510
|
+
websocket: "websocket"
|
|
511
|
+
});
|
|
512
|
+
var XHR_STATES = (
|
|
513
|
+
/** @type {const} */
|
|
514
|
+
{ complete: 4 });
|
|
515
|
+
var AUTH_TOKEN_PREFIX = "base64url.bearer.phx.";
|
|
516
|
+
var Push = class {
|
|
517
|
+
/**
|
|
518
|
+
* Initializes the Push
|
|
519
|
+
* @param {Channel} channel - The Channel
|
|
520
|
+
* @param {ChannelEvent} event - The event, for example `"phx_join"`
|
|
521
|
+
* @param {() => Record<string, unknown>} payload - The payload, for example `{user_id: 123}`
|
|
522
|
+
* @param {number} timeout - The push timeout in milliseconds
|
|
523
|
+
*/
|
|
524
|
+
constructor(channel, event, payload, timeout) {
|
|
525
|
+
this.channel = channel;
|
|
526
|
+
this.event = event;
|
|
527
|
+
this.payload = payload || function() {
|
|
528
|
+
return {};
|
|
529
|
+
};
|
|
530
|
+
this.receivedResp = null;
|
|
531
|
+
this.timeout = timeout;
|
|
532
|
+
this.timeoutTimer = null;
|
|
533
|
+
this.recHooks = [];
|
|
534
|
+
this.sent = false;
|
|
535
|
+
this.ref = void 0;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
*
|
|
539
|
+
* @param {number} timeout
|
|
540
|
+
*/
|
|
541
|
+
resend(timeout) {
|
|
542
|
+
this.timeout = timeout;
|
|
543
|
+
this.reset();
|
|
544
|
+
this.send();
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
*
|
|
548
|
+
*/
|
|
549
|
+
send() {
|
|
550
|
+
if (this.hasReceived("timeout")) return;
|
|
551
|
+
this.startTimeout();
|
|
552
|
+
this.sent = true;
|
|
553
|
+
this.channel.socket.push({
|
|
554
|
+
topic: this.channel.topic,
|
|
555
|
+
event: this.event,
|
|
556
|
+
payload: this.payload(),
|
|
557
|
+
ref: this.ref,
|
|
558
|
+
join_ref: this.channel.joinRef()
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
*
|
|
563
|
+
* @param {string} status
|
|
564
|
+
* @param {(response: any) => void} callback
|
|
565
|
+
*/
|
|
566
|
+
receive(status, callback) {
|
|
567
|
+
if (this.hasReceived(status)) callback(this.receivedResp.response);
|
|
568
|
+
this.recHooks.push({
|
|
569
|
+
status,
|
|
570
|
+
callback
|
|
571
|
+
});
|
|
572
|
+
return this;
|
|
573
|
+
}
|
|
574
|
+
reset() {
|
|
575
|
+
this.cancelRefEvent();
|
|
576
|
+
this.ref = null;
|
|
577
|
+
this.refEvent = null;
|
|
578
|
+
this.receivedResp = null;
|
|
579
|
+
this.sent = false;
|
|
580
|
+
}
|
|
581
|
+
destroy() {
|
|
582
|
+
this.cancelRefEvent();
|
|
583
|
+
this.cancelTimeout();
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* @private
|
|
587
|
+
*/
|
|
588
|
+
matchReceive({ status, response, _ref }) {
|
|
589
|
+
this.recHooks.filter((h) => h.status === status).forEach((h) => h.callback(response));
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* @private
|
|
593
|
+
*/
|
|
594
|
+
cancelRefEvent() {
|
|
595
|
+
if (!this.refEvent) return;
|
|
596
|
+
this.channel.off(this.refEvent);
|
|
597
|
+
}
|
|
598
|
+
cancelTimeout() {
|
|
599
|
+
clearTimeout(this.timeoutTimer);
|
|
600
|
+
this.timeoutTimer = null;
|
|
601
|
+
}
|
|
602
|
+
startTimeout() {
|
|
603
|
+
if (this.timeoutTimer) this.cancelTimeout();
|
|
604
|
+
this.ref = this.channel.socket.makeRef();
|
|
605
|
+
this.refEvent = this.channel.replyEventName(this.ref);
|
|
606
|
+
this.channel.on(this.refEvent, (payload) => {
|
|
607
|
+
this.cancelRefEvent();
|
|
608
|
+
this.cancelTimeout();
|
|
609
|
+
this.receivedResp = payload;
|
|
610
|
+
this.matchReceive(payload);
|
|
611
|
+
});
|
|
612
|
+
this.timeoutTimer = setTimeout(() => {
|
|
613
|
+
this.trigger("timeout", {});
|
|
614
|
+
}, this.timeout);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* @private
|
|
618
|
+
*/
|
|
619
|
+
hasReceived(status) {
|
|
620
|
+
return this.receivedResp && this.receivedResp.status === status;
|
|
621
|
+
}
|
|
622
|
+
trigger(status, response) {
|
|
623
|
+
this.channel.trigger(this.refEvent, {
|
|
624
|
+
status,
|
|
625
|
+
response
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
var Timer = class {
|
|
630
|
+
/**
|
|
631
|
+
* @param {() => void} callback
|
|
632
|
+
* @param {(tries: number) => number} timerCalc
|
|
633
|
+
*/
|
|
634
|
+
constructor(callback, timerCalc) {
|
|
635
|
+
this.callback = callback;
|
|
636
|
+
this.timerCalc = timerCalc;
|
|
637
|
+
this.timer = void 0;
|
|
638
|
+
this.tries = 0;
|
|
639
|
+
}
|
|
640
|
+
reset() {
|
|
641
|
+
this.tries = 0;
|
|
642
|
+
clearTimeout(this.timer);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Cancels any previous scheduleTimeout and schedules callback
|
|
646
|
+
*/
|
|
647
|
+
scheduleTimeout() {
|
|
648
|
+
clearTimeout(this.timer);
|
|
649
|
+
this.timer = setTimeout(() => {
|
|
650
|
+
this.tries = this.tries + 1;
|
|
651
|
+
this.callback();
|
|
652
|
+
}, this.timerCalc(this.tries + 1));
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
var Channel = class {
|
|
656
|
+
/**
|
|
657
|
+
* @param {string} topic
|
|
658
|
+
* @param {Params | (() => Params)} params
|
|
659
|
+
* @param {Socket} socket
|
|
660
|
+
*/
|
|
661
|
+
constructor(topic, params, socket) {
|
|
662
|
+
this.state = CHANNEL_STATES.closed;
|
|
663
|
+
this.topic = topic;
|
|
664
|
+
this.params = closure(params || {});
|
|
665
|
+
this.socket = socket;
|
|
666
|
+
this.bindings = [];
|
|
667
|
+
this.bindingRef = 0;
|
|
668
|
+
this.timeout = this.socket.timeout;
|
|
669
|
+
this.joinedOnce = false;
|
|
670
|
+
this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout);
|
|
671
|
+
this.pushBuffer = [];
|
|
672
|
+
this.stateChangeRefs = [];
|
|
673
|
+
this.rejoinTimer = new Timer(() => {
|
|
674
|
+
if (this.socket.isConnected()) this.rejoin();
|
|
675
|
+
}, this.socket.rejoinAfterMs);
|
|
676
|
+
this.stateChangeRefs.push(this.socket.onError(() => this.rejoinTimer.reset()));
|
|
677
|
+
this.stateChangeRefs.push(this.socket.onOpen(() => {
|
|
678
|
+
this.rejoinTimer.reset();
|
|
679
|
+
if (this.isErrored()) this.rejoin();
|
|
680
|
+
}));
|
|
681
|
+
this.joinPush.receive("ok", () => {
|
|
682
|
+
this.state = CHANNEL_STATES.joined;
|
|
683
|
+
this.rejoinTimer.reset();
|
|
684
|
+
this.pushBuffer.forEach((pushEvent) => pushEvent.send());
|
|
685
|
+
this.pushBuffer = [];
|
|
686
|
+
});
|
|
687
|
+
this.joinPush.receive("error", (reason) => {
|
|
688
|
+
this.state = CHANNEL_STATES.errored;
|
|
689
|
+
if (this.socket.hasLogger()) this.socket.log("channel", `error ${this.topic}`, reason);
|
|
690
|
+
if (this.socket.isConnected()) this.rejoinTimer.scheduleTimeout();
|
|
691
|
+
});
|
|
692
|
+
this.onClose(() => {
|
|
693
|
+
this.rejoinTimer.reset();
|
|
694
|
+
if (this.socket.hasLogger()) this.socket.log("channel", `close ${this.topic}`);
|
|
695
|
+
this.state = CHANNEL_STATES.closed;
|
|
696
|
+
this.socket.remove(this);
|
|
697
|
+
});
|
|
698
|
+
this.onError((reason) => {
|
|
699
|
+
if (this.socket.hasLogger()) this.socket.log("channel", `error ${this.topic}`, reason);
|
|
700
|
+
if (this.isJoining()) this.joinPush.reset();
|
|
701
|
+
this.state = CHANNEL_STATES.errored;
|
|
702
|
+
if (this.socket.isConnected()) this.rejoinTimer.scheduleTimeout();
|
|
703
|
+
});
|
|
704
|
+
this.joinPush.receive("timeout", () => {
|
|
705
|
+
if (this.socket.hasLogger()) this.socket.log("channel", `timeout ${this.topic}`, this.joinPush.timeout);
|
|
706
|
+
new Push(this, CHANNEL_EVENTS.leave, closure({}), this.timeout).send();
|
|
707
|
+
this.state = CHANNEL_STATES.errored;
|
|
708
|
+
this.joinPush.reset();
|
|
709
|
+
if (this.socket.isConnected()) this.rejoinTimer.scheduleTimeout();
|
|
710
|
+
});
|
|
711
|
+
this.on(CHANNEL_EVENTS.reply, (payload, ref) => {
|
|
712
|
+
this.trigger(this.replyEventName(ref), payload);
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Join the channel
|
|
717
|
+
* @param {number} timeout
|
|
718
|
+
* @returns {Push}
|
|
719
|
+
*/
|
|
720
|
+
join(timeout = this.timeout) {
|
|
721
|
+
if (this.joinedOnce) throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance");
|
|
722
|
+
else {
|
|
723
|
+
this.timeout = timeout;
|
|
724
|
+
this.joinedOnce = true;
|
|
725
|
+
this.rejoin();
|
|
726
|
+
return this.joinPush;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Teardown the channel.
|
|
731
|
+
*
|
|
732
|
+
* Destroys and stops related timers.
|
|
733
|
+
*/
|
|
734
|
+
teardown() {
|
|
735
|
+
this.pushBuffer.forEach((push) => push.destroy());
|
|
736
|
+
this.pushBuffer = [];
|
|
737
|
+
this.rejoinTimer.reset();
|
|
738
|
+
this.joinPush.destroy();
|
|
739
|
+
this.state = CHANNEL_STATES.closed;
|
|
740
|
+
this.bindings = [];
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Hook into channel close
|
|
744
|
+
* @param {ChannelBindingCallback} callback
|
|
745
|
+
*/
|
|
746
|
+
onClose(callback) {
|
|
747
|
+
this.on(CHANNEL_EVENTS.close, callback);
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Hook into channel errors
|
|
751
|
+
* @param {ChannelOnErrorCallback} callback
|
|
752
|
+
* @return {number}
|
|
753
|
+
*/
|
|
754
|
+
onError(callback) {
|
|
755
|
+
return this.on(CHANNEL_EVENTS.error, (reason) => callback(reason));
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Subscribes on channel events
|
|
759
|
+
*
|
|
760
|
+
* Subscription returns a ref counter, which can be used later to
|
|
761
|
+
* unsubscribe the exact event listener
|
|
762
|
+
*
|
|
763
|
+
* @example
|
|
764
|
+
* const ref1 = channel.on("event", do_stuff)
|
|
765
|
+
* const ref2 = channel.on("event", do_other_stuff)
|
|
766
|
+
* channel.off("event", ref1)
|
|
767
|
+
* // Since unsubscription, do_stuff won't fire,
|
|
768
|
+
* // while do_other_stuff will keep firing on the "event"
|
|
769
|
+
*
|
|
770
|
+
* @param {string} event
|
|
771
|
+
* @param {ChannelBindingCallback} callback
|
|
772
|
+
* @returns {number} ref
|
|
773
|
+
*/
|
|
774
|
+
on(event, callback) {
|
|
775
|
+
let ref = this.bindingRef++;
|
|
776
|
+
this.bindings.push({
|
|
777
|
+
event,
|
|
778
|
+
ref,
|
|
779
|
+
callback
|
|
780
|
+
});
|
|
781
|
+
return ref;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Unsubscribes off of channel events
|
|
785
|
+
*
|
|
786
|
+
* Use the ref returned from a channel.on() to unsubscribe one
|
|
787
|
+
* handler, or pass nothing for the ref to unsubscribe all
|
|
788
|
+
* handlers for the given event.
|
|
789
|
+
*
|
|
790
|
+
* @example
|
|
791
|
+
* // Unsubscribe the do_stuff handler
|
|
792
|
+
* const ref1 = channel.on("event", do_stuff)
|
|
793
|
+
* channel.off("event", ref1)
|
|
794
|
+
*
|
|
795
|
+
* // Unsubscribe all handlers from event
|
|
796
|
+
* channel.off("event")
|
|
797
|
+
*
|
|
798
|
+
* @param {string} event
|
|
799
|
+
* @param {number} [ref]
|
|
800
|
+
*/
|
|
801
|
+
off(event, ref) {
|
|
802
|
+
this.bindings = this.bindings.filter((bind) => {
|
|
803
|
+
return !(bind.event === event && (typeof ref === "undefined" || ref === bind.ref));
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* @private
|
|
808
|
+
*/
|
|
809
|
+
canPush() {
|
|
810
|
+
return this.socket.isConnected() && this.isJoined();
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Sends a message `event` to phoenix with the payload `payload`.
|
|
814
|
+
* Phoenix receives this in the `handle_in(event, payload, socket)`
|
|
815
|
+
* function. if phoenix replies or it times out (default 10000ms),
|
|
816
|
+
* then optionally the reply can be received.
|
|
817
|
+
*
|
|
818
|
+
* @example
|
|
819
|
+
* channel.push("event")
|
|
820
|
+
* .receive("ok", payload => console.log("phoenix replied:", payload))
|
|
821
|
+
* .receive("error", err => console.log("phoenix errored", err))
|
|
822
|
+
* .receive("timeout", () => console.log("timed out pushing"))
|
|
823
|
+
* @param {string} event
|
|
824
|
+
* @param {Object} payload
|
|
825
|
+
* @param {number} [timeout]
|
|
826
|
+
* @returns {Push}
|
|
827
|
+
*/
|
|
828
|
+
push(event, payload, timeout = this.timeout) {
|
|
829
|
+
payload = payload || {};
|
|
830
|
+
if (!this.joinedOnce) throw new Error(`tried to push '${event}' to '${this.topic}' before joining. Use channel.join() before pushing events`);
|
|
831
|
+
let pushEvent = new Push(this, event, function() {
|
|
832
|
+
return payload;
|
|
833
|
+
}, timeout);
|
|
834
|
+
if (this.canPush()) pushEvent.send();
|
|
835
|
+
else {
|
|
836
|
+
pushEvent.startTimeout();
|
|
837
|
+
this.pushBuffer.push(pushEvent);
|
|
838
|
+
}
|
|
839
|
+
return pushEvent;
|
|
840
|
+
}
|
|
841
|
+
/** Leaves the channel
|
|
842
|
+
*
|
|
843
|
+
* Unsubscribes from server events, and
|
|
844
|
+
* instructs channel to terminate on server
|
|
845
|
+
*
|
|
846
|
+
* Triggers onClose() hooks
|
|
847
|
+
*
|
|
848
|
+
* To receive leave acknowledgements, use the `receive`
|
|
849
|
+
* hook to bind to the server ack, ie:
|
|
850
|
+
*
|
|
851
|
+
* @example
|
|
852
|
+
* channel.leave().receive("ok", () => alert("left!") )
|
|
853
|
+
*
|
|
854
|
+
* @param {number} timeout
|
|
855
|
+
* @returns {Push}
|
|
856
|
+
*/
|
|
857
|
+
leave(timeout = this.timeout) {
|
|
858
|
+
this.rejoinTimer.reset();
|
|
859
|
+
this.joinPush.cancelTimeout();
|
|
860
|
+
this.state = CHANNEL_STATES.leaving;
|
|
861
|
+
let onClose = () => {
|
|
862
|
+
if (this.socket.hasLogger()) this.socket.log("channel", `leave ${this.topic}`);
|
|
863
|
+
this.trigger(CHANNEL_EVENTS.close, "leave");
|
|
864
|
+
};
|
|
865
|
+
let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), timeout);
|
|
866
|
+
leavePush.receive("ok", () => onClose()).receive("timeout", () => onClose());
|
|
867
|
+
leavePush.send();
|
|
868
|
+
if (!this.canPush()) leavePush.trigger("ok", {});
|
|
869
|
+
return leavePush;
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Overridable message hook
|
|
873
|
+
*
|
|
874
|
+
* Receives all events for specialized message handling
|
|
875
|
+
* before dispatching to the channel callbacks.
|
|
876
|
+
*
|
|
877
|
+
* Must return the payload, modified or unmodified
|
|
878
|
+
* @type{ChannelOnMessage}
|
|
879
|
+
*/
|
|
880
|
+
onMessage(_event, payload, _ref) {
|
|
881
|
+
return payload;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Overridable filter hook
|
|
885
|
+
*
|
|
886
|
+
* If this function returns `true`, `binding`'s callback will be called.
|
|
887
|
+
*
|
|
888
|
+
* @type{ChannelFilterBindings}
|
|
889
|
+
*/
|
|
890
|
+
filterBindings(_binding, _payload, _ref) {
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
893
|
+
isMember(topic, event, payload, joinRef) {
|
|
894
|
+
if (this.topic !== topic) return false;
|
|
895
|
+
if (joinRef && joinRef !== this.joinRef()) {
|
|
896
|
+
if (this.socket.hasLogger()) this.socket.log("channel", "dropping outdated message", {
|
|
897
|
+
topic,
|
|
898
|
+
event,
|
|
899
|
+
payload,
|
|
900
|
+
joinRef
|
|
901
|
+
});
|
|
902
|
+
return false;
|
|
903
|
+
} else return true;
|
|
904
|
+
}
|
|
905
|
+
joinRef() {
|
|
906
|
+
return this.joinPush.ref;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* @private
|
|
910
|
+
*/
|
|
911
|
+
rejoin(timeout = this.timeout) {
|
|
912
|
+
if (this.isLeaving()) return;
|
|
913
|
+
this.socket.leaveOpenTopic(this.topic);
|
|
914
|
+
this.state = CHANNEL_STATES.joining;
|
|
915
|
+
this.joinPush.resend(timeout);
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* @param {string} event
|
|
919
|
+
* @param {unknown} [payload]
|
|
920
|
+
* @param {?string} [ref]
|
|
921
|
+
* @param {?string} [joinRef]
|
|
922
|
+
*/
|
|
923
|
+
trigger(event, payload, ref, joinRef) {
|
|
924
|
+
let handledPayload = this.onMessage(event, payload, ref, joinRef);
|
|
925
|
+
if (payload && !handledPayload) throw new Error("channel onMessage callbacks must return the payload, modified or unmodified");
|
|
926
|
+
let eventBindings = this.bindings.filter((bind) => bind.event === event && this.filterBindings(bind, payload, ref));
|
|
927
|
+
for (let i = 0; i < eventBindings.length; i++) eventBindings[i].callback(handledPayload, ref, joinRef || this.joinRef());
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* @param {string} ref
|
|
931
|
+
*/
|
|
932
|
+
replyEventName(ref) {
|
|
933
|
+
return `chan_reply_${ref}`;
|
|
934
|
+
}
|
|
935
|
+
isClosed() {
|
|
936
|
+
return this.state === CHANNEL_STATES.closed;
|
|
937
|
+
}
|
|
938
|
+
isErrored() {
|
|
939
|
+
return this.state === CHANNEL_STATES.errored;
|
|
940
|
+
}
|
|
941
|
+
isJoined() {
|
|
942
|
+
return this.state === CHANNEL_STATES.joined;
|
|
943
|
+
}
|
|
944
|
+
isJoining() {
|
|
945
|
+
return this.state === CHANNEL_STATES.joining;
|
|
946
|
+
}
|
|
947
|
+
isLeaving() {
|
|
948
|
+
return this.state === CHANNEL_STATES.leaving;
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
var Ajax = class {
|
|
952
|
+
static request(method, endPoint, headers, body, timeout, ontimeout, callback) {
|
|
953
|
+
if (global$1.XDomainRequest) {
|
|
954
|
+
let req = new global$1.XDomainRequest();
|
|
955
|
+
return this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback);
|
|
956
|
+
} else if (global$1.XMLHttpRequest) {
|
|
957
|
+
let req = new global$1.XMLHttpRequest();
|
|
958
|
+
return this.xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback);
|
|
959
|
+
} else if (global$1.fetch && global$1.AbortController) return this.fetchRequest(method, endPoint, headers, body, timeout, ontimeout, callback);
|
|
960
|
+
else throw new Error("No suitable XMLHttpRequest implementation found");
|
|
961
|
+
}
|
|
962
|
+
static fetchRequest(method, endPoint, headers, body, timeout, ontimeout, callback) {
|
|
963
|
+
let options = {
|
|
964
|
+
method,
|
|
965
|
+
headers,
|
|
966
|
+
body
|
|
967
|
+
};
|
|
968
|
+
let controller = null;
|
|
969
|
+
if (timeout) {
|
|
970
|
+
controller = new AbortController();
|
|
971
|
+
setTimeout(() => controller.abort(), timeout);
|
|
972
|
+
options.signal = controller.signal;
|
|
973
|
+
}
|
|
974
|
+
global$1.fetch(endPoint, options).then((response) => response.text()).then((data) => this.parseJSON(data)).then((data) => callback && callback(data)).catch((err) => {
|
|
975
|
+
if (err.name === "AbortError" && ontimeout) ontimeout();
|
|
976
|
+
else callback && callback(null);
|
|
977
|
+
});
|
|
978
|
+
return controller;
|
|
979
|
+
}
|
|
980
|
+
static xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback) {
|
|
981
|
+
req.timeout = timeout;
|
|
982
|
+
req.open(method, endPoint);
|
|
983
|
+
req.onload = () => {
|
|
984
|
+
let response = this.parseJSON(req.responseText);
|
|
985
|
+
callback && callback(response);
|
|
986
|
+
};
|
|
987
|
+
if (ontimeout) req.ontimeout = ontimeout;
|
|
988
|
+
req.onprogress = () => {};
|
|
989
|
+
req.send(body);
|
|
990
|
+
return req;
|
|
991
|
+
}
|
|
992
|
+
static xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback) {
|
|
993
|
+
req.open(method, endPoint, true);
|
|
994
|
+
req.timeout = timeout;
|
|
995
|
+
for (let [key, value] of Object.entries(headers)) req.setRequestHeader(key, value);
|
|
996
|
+
req.onerror = () => callback && callback(null);
|
|
997
|
+
req.onreadystatechange = () => {
|
|
998
|
+
if (req.readyState === XHR_STATES.complete && callback) callback(this.parseJSON(req.responseText));
|
|
999
|
+
};
|
|
1000
|
+
if (ontimeout) req.ontimeout = ontimeout;
|
|
1001
|
+
req.send(body);
|
|
1002
|
+
return req;
|
|
1003
|
+
}
|
|
1004
|
+
static parseJSON(resp) {
|
|
1005
|
+
if (!resp || resp === "") return null;
|
|
1006
|
+
try {
|
|
1007
|
+
return JSON.parse(resp);
|
|
1008
|
+
} catch {
|
|
1009
|
+
console && console.log("failed to parse JSON response", resp);
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
static serialize(obj, parentKey) {
|
|
1014
|
+
let queryStr = [];
|
|
1015
|
+
for (var key in obj) {
|
|
1016
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
1017
|
+
let paramKey = parentKey ? `${parentKey}[${key}]` : key;
|
|
1018
|
+
let paramVal = obj[key];
|
|
1019
|
+
if (typeof paramVal === "object") queryStr.push(this.serialize(paramVal, paramKey));
|
|
1020
|
+
else queryStr.push(encodeURIComponent(paramKey) + "=" + encodeURIComponent(paramVal));
|
|
1021
|
+
}
|
|
1022
|
+
return queryStr.join("&");
|
|
1023
|
+
}
|
|
1024
|
+
static appendParams(url, params) {
|
|
1025
|
+
if (Object.keys(params).length === 0) return url;
|
|
1026
|
+
return `${url}${url.match(/\?/) ? "&" : "?"}${this.serialize(params)}`;
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
var arrayBufferToBase64 = (buffer) => {
|
|
1030
|
+
let binary = "";
|
|
1031
|
+
let bytes = new Uint8Array(buffer);
|
|
1032
|
+
let len = bytes.byteLength;
|
|
1033
|
+
for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
|
|
1034
|
+
return btoa(binary);
|
|
1035
|
+
};
|
|
1036
|
+
var LongPoll = class {
|
|
1037
|
+
constructor(endPoint, protocols) {
|
|
1038
|
+
if (protocols && protocols.length === 2 && protocols[1].startsWith(AUTH_TOKEN_PREFIX)) this.authToken = atob(protocols[1].slice(AUTH_TOKEN_PREFIX.length));
|
|
1039
|
+
this.endPoint = null;
|
|
1040
|
+
this.token = null;
|
|
1041
|
+
this.skipHeartbeat = true;
|
|
1042
|
+
this.reqs = /* @__PURE__ */ new Set();
|
|
1043
|
+
this.awaitingBatchAck = false;
|
|
1044
|
+
this.currentBatch = null;
|
|
1045
|
+
this.currentBatchTimer = null;
|
|
1046
|
+
this.batchBuffer = [];
|
|
1047
|
+
this.onopen = function() {};
|
|
1048
|
+
this.onerror = function() {};
|
|
1049
|
+
this.onmessage = function() {};
|
|
1050
|
+
this.onclose = function() {};
|
|
1051
|
+
this.pollEndpoint = this.normalizeEndpoint(endPoint);
|
|
1052
|
+
this.readyState = SOCKET_STATES.connecting;
|
|
1053
|
+
setTimeout(() => this.poll(), 0);
|
|
1054
|
+
}
|
|
1055
|
+
normalizeEndpoint(endPoint) {
|
|
1056
|
+
return endPoint.replace("ws://", "http://").replace("wss://", "https://").replace(new RegExp("(.*)/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll);
|
|
1057
|
+
}
|
|
1058
|
+
endpointURL() {
|
|
1059
|
+
return Ajax.appendParams(this.pollEndpoint, { token: this.token });
|
|
1060
|
+
}
|
|
1061
|
+
closeAndRetry(code, reason, wasClean) {
|
|
1062
|
+
this.close(code, reason, wasClean);
|
|
1063
|
+
this.readyState = SOCKET_STATES.connecting;
|
|
1064
|
+
}
|
|
1065
|
+
ontimeout() {
|
|
1066
|
+
this.onerror("timeout");
|
|
1067
|
+
this.closeAndRetry(1005, "timeout", false);
|
|
1068
|
+
}
|
|
1069
|
+
isActive() {
|
|
1070
|
+
return this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting;
|
|
1071
|
+
}
|
|
1072
|
+
poll() {
|
|
1073
|
+
const headers = { "Accept": "application/json" };
|
|
1074
|
+
if (this.authToken) headers["X-Phoenix-AuthToken"] = this.authToken;
|
|
1075
|
+
this.ajax("GET", headers, null, () => this.ontimeout(), (resp) => {
|
|
1076
|
+
if (resp) {
|
|
1077
|
+
var { status, token, messages } = resp;
|
|
1078
|
+
if (status === 410 && this.token !== null) {
|
|
1079
|
+
this.onerror(410);
|
|
1080
|
+
this.closeAndRetry(3410, "session_gone", false);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
this.token = token;
|
|
1084
|
+
} else status = 0;
|
|
1085
|
+
switch (status) {
|
|
1086
|
+
case 200:
|
|
1087
|
+
messages.forEach((msg) => {
|
|
1088
|
+
setTimeout(() => this.onmessage({ data: msg }), 0);
|
|
1089
|
+
});
|
|
1090
|
+
this.poll();
|
|
1091
|
+
break;
|
|
1092
|
+
case 204:
|
|
1093
|
+
this.poll();
|
|
1094
|
+
break;
|
|
1095
|
+
case 410:
|
|
1096
|
+
this.readyState = SOCKET_STATES.open;
|
|
1097
|
+
this.onopen({});
|
|
1098
|
+
this.poll();
|
|
1099
|
+
break;
|
|
1100
|
+
case 403:
|
|
1101
|
+
this.onerror(403);
|
|
1102
|
+
this.close(1008, "forbidden", false);
|
|
1103
|
+
break;
|
|
1104
|
+
case 0:
|
|
1105
|
+
case 500:
|
|
1106
|
+
this.onerror(500);
|
|
1107
|
+
this.closeAndRetry(1011, "internal server error", 500);
|
|
1108
|
+
break;
|
|
1109
|
+
default: throw new Error(`unhandled poll status ${status}`);
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
send(body) {
|
|
1114
|
+
if (typeof body !== "string") body = arrayBufferToBase64(body);
|
|
1115
|
+
if (this.currentBatch) this.currentBatch.push(body);
|
|
1116
|
+
else if (this.awaitingBatchAck) this.batchBuffer.push(body);
|
|
1117
|
+
else {
|
|
1118
|
+
this.currentBatch = [body];
|
|
1119
|
+
this.currentBatchTimer = setTimeout(() => {
|
|
1120
|
+
this.batchSend(this.currentBatch);
|
|
1121
|
+
this.currentBatch = null;
|
|
1122
|
+
}, 0);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
batchSend(messages) {
|
|
1126
|
+
this.awaitingBatchAck = true;
|
|
1127
|
+
this.ajax("POST", { "Content-Type": "application/x-ndjson" }, messages.join("\n"), () => this.onerror("timeout"), (resp) => {
|
|
1128
|
+
this.awaitingBatchAck = false;
|
|
1129
|
+
if (!resp || resp.status !== 200) {
|
|
1130
|
+
this.onerror(resp && resp.status);
|
|
1131
|
+
this.closeAndRetry(1011, "internal server error", false);
|
|
1132
|
+
} else if (this.batchBuffer.length > 0) {
|
|
1133
|
+
this.batchSend(this.batchBuffer);
|
|
1134
|
+
this.batchBuffer = [];
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
close(code, reason, wasClean) {
|
|
1139
|
+
for (let req of this.reqs) req.abort();
|
|
1140
|
+
this.readyState = SOCKET_STATES.closed;
|
|
1141
|
+
let opts = Object.assign({
|
|
1142
|
+
code: 1e3,
|
|
1143
|
+
reason: void 0,
|
|
1144
|
+
wasClean: true
|
|
1145
|
+
}, {
|
|
1146
|
+
code,
|
|
1147
|
+
reason,
|
|
1148
|
+
wasClean
|
|
1149
|
+
});
|
|
1150
|
+
this.batchBuffer = [];
|
|
1151
|
+
clearTimeout(this.currentBatchTimer);
|
|
1152
|
+
this.currentBatchTimer = null;
|
|
1153
|
+
if (typeof CloseEvent !== "undefined") this.onclose(new CloseEvent("close", opts));
|
|
1154
|
+
else this.onclose(opts);
|
|
1155
|
+
}
|
|
1156
|
+
ajax(method, headers, body, onCallerTimeout, callback) {
|
|
1157
|
+
let req;
|
|
1158
|
+
let ontimeout = () => {
|
|
1159
|
+
this.reqs.delete(req);
|
|
1160
|
+
onCallerTimeout();
|
|
1161
|
+
};
|
|
1162
|
+
req = Ajax.request(method, this.endpointURL(), headers, body, this.timeout, ontimeout, (resp) => {
|
|
1163
|
+
this.reqs.delete(req);
|
|
1164
|
+
if (this.isActive()) callback(resp);
|
|
1165
|
+
});
|
|
1166
|
+
this.reqs.add(req);
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
var Presence = class _Presence {
|
|
1170
|
+
/**
|
|
1171
|
+
* Initializes the Presence
|
|
1172
|
+
* @param {Channel} channel - The Channel
|
|
1173
|
+
* @param {PresenceOptions} [opts] - The options, for example `{events: {state: "state", diff: "diff"}}`
|
|
1174
|
+
*/
|
|
1175
|
+
constructor(channel, opts = {}) {
|
|
1176
|
+
let events = opts.events || {
|
|
1177
|
+
state: "presence_state",
|
|
1178
|
+
diff: "presence_diff"
|
|
1179
|
+
};
|
|
1180
|
+
this.state = {};
|
|
1181
|
+
this.pendingDiffs = [];
|
|
1182
|
+
this.channel = channel;
|
|
1183
|
+
this.joinRef = null;
|
|
1184
|
+
this.caller = {
|
|
1185
|
+
onJoin: function() {},
|
|
1186
|
+
onLeave: function() {},
|
|
1187
|
+
onSync: function() {}
|
|
1188
|
+
};
|
|
1189
|
+
this.channel.on(events.state, (newState) => {
|
|
1190
|
+
let { onJoin, onLeave, onSync } = this.caller;
|
|
1191
|
+
this.joinRef = this.channel.joinRef();
|
|
1192
|
+
this.state = _Presence.syncState(this.state, newState, onJoin, onLeave);
|
|
1193
|
+
this.pendingDiffs.forEach((diff) => {
|
|
1194
|
+
this.state = _Presence.syncDiff(this.state, diff, onJoin, onLeave);
|
|
1195
|
+
});
|
|
1196
|
+
this.pendingDiffs = [];
|
|
1197
|
+
onSync();
|
|
1198
|
+
});
|
|
1199
|
+
this.channel.on(events.diff, (diff) => {
|
|
1200
|
+
let { onJoin, onLeave, onSync } = this.caller;
|
|
1201
|
+
if (this.inPendingSyncState()) this.pendingDiffs.push(diff);
|
|
1202
|
+
else {
|
|
1203
|
+
this.state = _Presence.syncDiff(this.state, diff, onJoin, onLeave);
|
|
1204
|
+
onSync();
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* @param {PresenceOnJoin} callback
|
|
1210
|
+
*/
|
|
1211
|
+
onJoin(callback) {
|
|
1212
|
+
this.caller.onJoin = callback;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* @param {PresenceOnLeave} callback
|
|
1216
|
+
*/
|
|
1217
|
+
onLeave(callback) {
|
|
1218
|
+
this.caller.onLeave = callback;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* @param {PresenceOnSync} callback
|
|
1222
|
+
*/
|
|
1223
|
+
onSync(callback) {
|
|
1224
|
+
this.caller.onSync = callback;
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Returns the array of presences, with selected metadata.
|
|
1228
|
+
*
|
|
1229
|
+
* @template [T=PresenceState]
|
|
1230
|
+
* @param {((key: string, obj: PresenceState) => T)} [by]
|
|
1231
|
+
*
|
|
1232
|
+
* @returns {T[]}
|
|
1233
|
+
*/
|
|
1234
|
+
list(by) {
|
|
1235
|
+
return _Presence.list(this.state, by);
|
|
1236
|
+
}
|
|
1237
|
+
inPendingSyncState() {
|
|
1238
|
+
return !this.joinRef || this.joinRef !== this.channel.joinRef();
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Used to sync the list of presences on the server
|
|
1242
|
+
* with the client's state. An optional `onJoin` and `onLeave` callback can
|
|
1243
|
+
* be provided to react to changes in the client's local presences across
|
|
1244
|
+
* disconnects and reconnects with the server.
|
|
1245
|
+
*
|
|
1246
|
+
* @param {Record<string, PresenceState>} currentState
|
|
1247
|
+
* @param {Record<string, PresenceState>} newState
|
|
1248
|
+
* @param {PresenceOnJoin} onJoin
|
|
1249
|
+
* @param {PresenceOnLeave} onLeave
|
|
1250
|
+
*
|
|
1251
|
+
* @returns {Record<string, PresenceState>}
|
|
1252
|
+
*/
|
|
1253
|
+
static syncState(currentState, newState, onJoin, onLeave) {
|
|
1254
|
+
let state = this.clone(currentState);
|
|
1255
|
+
let joins = {};
|
|
1256
|
+
let leaves = {};
|
|
1257
|
+
this.map(state, (key, presence) => {
|
|
1258
|
+
if (!newState[key]) leaves[key] = presence;
|
|
1259
|
+
});
|
|
1260
|
+
this.map(newState, (key, newPresence) => {
|
|
1261
|
+
let currentPresence = state[key];
|
|
1262
|
+
if (currentPresence) {
|
|
1263
|
+
let newRefs = newPresence.metas.map((m) => m.phx_ref);
|
|
1264
|
+
let curRefs = currentPresence.metas.map((m) => m.phx_ref);
|
|
1265
|
+
let joinedMetas = newPresence.metas.filter((m) => curRefs.indexOf(m.phx_ref) < 0);
|
|
1266
|
+
let leftMetas = currentPresence.metas.filter((m) => newRefs.indexOf(m.phx_ref) < 0);
|
|
1267
|
+
if (joinedMetas.length > 0) {
|
|
1268
|
+
joins[key] = newPresence;
|
|
1269
|
+
joins[key].metas = joinedMetas;
|
|
1270
|
+
}
|
|
1271
|
+
if (leftMetas.length > 0) {
|
|
1272
|
+
leaves[key] = this.clone(currentPresence);
|
|
1273
|
+
leaves[key].metas = leftMetas;
|
|
1274
|
+
}
|
|
1275
|
+
} else joins[key] = newPresence;
|
|
1276
|
+
});
|
|
1277
|
+
return this.syncDiff(state, {
|
|
1278
|
+
joins,
|
|
1279
|
+
leaves
|
|
1280
|
+
}, onJoin, onLeave);
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
*
|
|
1284
|
+
* Used to sync a diff of presence join and leave
|
|
1285
|
+
* events from the server, as they happen. Like `syncState`, `syncDiff`
|
|
1286
|
+
* accepts optional `onJoin` and `onLeave` callbacks to react to a user
|
|
1287
|
+
* joining or leaving from a device.
|
|
1288
|
+
*
|
|
1289
|
+
* @param {Record<string, PresenceState>} state
|
|
1290
|
+
* @param {PresenceDiff} diff
|
|
1291
|
+
* @param {PresenceOnJoin} onJoin
|
|
1292
|
+
* @param {PresenceOnLeave} onLeave
|
|
1293
|
+
*
|
|
1294
|
+
* @returns {Record<string, PresenceState>}
|
|
1295
|
+
*/
|
|
1296
|
+
static syncDiff(state, diff, onJoin, onLeave) {
|
|
1297
|
+
let { joins, leaves } = this.clone(diff);
|
|
1298
|
+
if (!onJoin) onJoin = function() {};
|
|
1299
|
+
if (!onLeave) onLeave = function() {};
|
|
1300
|
+
this.map(joins, (key, newPresence) => {
|
|
1301
|
+
let currentPresence = state[key];
|
|
1302
|
+
state[key] = this.clone(newPresence);
|
|
1303
|
+
if (currentPresence) {
|
|
1304
|
+
let joinedRefs = state[key].metas.map((m) => m.phx_ref);
|
|
1305
|
+
let curMetas = currentPresence.metas.filter((m) => joinedRefs.indexOf(m.phx_ref) < 0);
|
|
1306
|
+
state[key].metas.unshift(...curMetas);
|
|
1307
|
+
}
|
|
1308
|
+
onJoin(key, currentPresence, newPresence);
|
|
1309
|
+
});
|
|
1310
|
+
this.map(leaves, (key, leftPresence) => {
|
|
1311
|
+
let currentPresence = state[key];
|
|
1312
|
+
if (!currentPresence) return;
|
|
1313
|
+
let refsToRemove = leftPresence.metas.map((m) => m.phx_ref);
|
|
1314
|
+
currentPresence.metas = currentPresence.metas.filter((p) => {
|
|
1315
|
+
return refsToRemove.indexOf(p.phx_ref) < 0;
|
|
1316
|
+
});
|
|
1317
|
+
onLeave(key, currentPresence, leftPresence);
|
|
1318
|
+
if (currentPresence.metas.length === 0) delete state[key];
|
|
1319
|
+
});
|
|
1320
|
+
return state;
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Returns the array of presences, with selected metadata.
|
|
1324
|
+
*
|
|
1325
|
+
* @template [T=PresenceState]
|
|
1326
|
+
* @param {Record<string, PresenceState>} presences
|
|
1327
|
+
* @param {((key: string, obj: PresenceState) => T)} [chooser]
|
|
1328
|
+
*
|
|
1329
|
+
* @returns {T[]}
|
|
1330
|
+
*/
|
|
1331
|
+
static list(presences, chooser) {
|
|
1332
|
+
if (!chooser) chooser = function(key, pres) {
|
|
1333
|
+
return pres;
|
|
1334
|
+
};
|
|
1335
|
+
return this.map(presences, (key, presence) => {
|
|
1336
|
+
return chooser(key, presence);
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* @template T
|
|
1341
|
+
* @param {Record<string, PresenceState>} obj
|
|
1342
|
+
* @param {(key: string, obj: PresenceState) => T} func
|
|
1343
|
+
*/
|
|
1344
|
+
static map(obj, func) {
|
|
1345
|
+
return Object.getOwnPropertyNames(obj).map((key) => func(key, obj[key]));
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* @template T
|
|
1349
|
+
* @param {T} obj
|
|
1350
|
+
* @returns {T}
|
|
1351
|
+
*/
|
|
1352
|
+
static clone(obj) {
|
|
1353
|
+
return JSON.parse(JSON.stringify(obj));
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
var serializer_default = {
|
|
1357
|
+
HEADER_LENGTH: 1,
|
|
1358
|
+
META_LENGTH: 4,
|
|
1359
|
+
KINDS: {
|
|
1360
|
+
push: 0,
|
|
1361
|
+
reply: 1,
|
|
1362
|
+
broadcast: 2
|
|
1363
|
+
},
|
|
1364
|
+
/**
|
|
1365
|
+
* @template T
|
|
1366
|
+
* @param {Message<Record<string, any>>} msg
|
|
1367
|
+
* @param {(msg: ArrayBuffer | string) => T} callback
|
|
1368
|
+
* @returns {T}
|
|
1369
|
+
*/
|
|
1370
|
+
encode(msg, callback) {
|
|
1371
|
+
if (msg.payload.constructor === ArrayBuffer) return callback(this.binaryEncode(msg));
|
|
1372
|
+
else {
|
|
1373
|
+
let payload = [
|
|
1374
|
+
msg.join_ref,
|
|
1375
|
+
msg.ref,
|
|
1376
|
+
msg.topic,
|
|
1377
|
+
msg.event,
|
|
1378
|
+
msg.payload
|
|
1379
|
+
];
|
|
1380
|
+
return callback(JSON.stringify(payload));
|
|
1381
|
+
}
|
|
1382
|
+
},
|
|
1383
|
+
/**
|
|
1384
|
+
* @template T
|
|
1385
|
+
* @param {ArrayBuffer | string} rawPayload
|
|
1386
|
+
* @param {(msg: Message<unknown>) => T} callback
|
|
1387
|
+
* @returns {T}
|
|
1388
|
+
*/
|
|
1389
|
+
decode(rawPayload, callback) {
|
|
1390
|
+
if (rawPayload.constructor === ArrayBuffer) return callback(this.binaryDecode(rawPayload));
|
|
1391
|
+
else {
|
|
1392
|
+
let [join_ref, ref, topic, event, payload] = JSON.parse(rawPayload);
|
|
1393
|
+
return callback({
|
|
1394
|
+
join_ref,
|
|
1395
|
+
ref,
|
|
1396
|
+
topic,
|
|
1397
|
+
event,
|
|
1398
|
+
payload
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
},
|
|
1402
|
+
/** @private */
|
|
1403
|
+
binaryEncode(message) {
|
|
1404
|
+
let { join_ref, ref, event, topic, payload } = message;
|
|
1405
|
+
let metaLength = this.META_LENGTH + join_ref.length + ref.length + topic.length + event.length;
|
|
1406
|
+
let header = new ArrayBuffer(this.HEADER_LENGTH + metaLength);
|
|
1407
|
+
let view = new DataView(header);
|
|
1408
|
+
let offset = 0;
|
|
1409
|
+
view.setUint8(offset++, this.KINDS.push);
|
|
1410
|
+
view.setUint8(offset++, join_ref.length);
|
|
1411
|
+
view.setUint8(offset++, ref.length);
|
|
1412
|
+
view.setUint8(offset++, topic.length);
|
|
1413
|
+
view.setUint8(offset++, event.length);
|
|
1414
|
+
Array.from(join_ref, (char) => view.setUint8(offset++, char.charCodeAt(0)));
|
|
1415
|
+
Array.from(ref, (char) => view.setUint8(offset++, char.charCodeAt(0)));
|
|
1416
|
+
Array.from(topic, (char) => view.setUint8(offset++, char.charCodeAt(0)));
|
|
1417
|
+
Array.from(event, (char) => view.setUint8(offset++, char.charCodeAt(0)));
|
|
1418
|
+
var combined = new Uint8Array(header.byteLength + payload.byteLength);
|
|
1419
|
+
combined.set(new Uint8Array(header), 0);
|
|
1420
|
+
combined.set(new Uint8Array(payload), header.byteLength);
|
|
1421
|
+
return combined.buffer;
|
|
1422
|
+
},
|
|
1423
|
+
/**
|
|
1424
|
+
* @private
|
|
1425
|
+
*/
|
|
1426
|
+
binaryDecode(buffer) {
|
|
1427
|
+
let view = new DataView(buffer);
|
|
1428
|
+
let kind = view.getUint8(0);
|
|
1429
|
+
let decoder = new TextDecoder();
|
|
1430
|
+
switch (kind) {
|
|
1431
|
+
case this.KINDS.push: return this.decodePush(buffer, view, decoder);
|
|
1432
|
+
case this.KINDS.reply: return this.decodeReply(buffer, view, decoder);
|
|
1433
|
+
case this.KINDS.broadcast: return this.decodeBroadcast(buffer, view, decoder);
|
|
1434
|
+
}
|
|
1435
|
+
},
|
|
1436
|
+
/** @private */
|
|
1437
|
+
decodePush(buffer, view, decoder) {
|
|
1438
|
+
let joinRefSize = view.getUint8(1);
|
|
1439
|
+
let topicSize = view.getUint8(2);
|
|
1440
|
+
let eventSize = view.getUint8(3);
|
|
1441
|
+
let offset = this.HEADER_LENGTH + this.META_LENGTH - 1;
|
|
1442
|
+
let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize));
|
|
1443
|
+
offset = offset + joinRefSize;
|
|
1444
|
+
let topic = decoder.decode(buffer.slice(offset, offset + topicSize));
|
|
1445
|
+
offset = offset + topicSize;
|
|
1446
|
+
let event = decoder.decode(buffer.slice(offset, offset + eventSize));
|
|
1447
|
+
offset = offset + eventSize;
|
|
1448
|
+
return {
|
|
1449
|
+
join_ref: joinRef,
|
|
1450
|
+
ref: null,
|
|
1451
|
+
topic,
|
|
1452
|
+
event,
|
|
1453
|
+
payload: buffer.slice(offset, buffer.byteLength)
|
|
1454
|
+
};
|
|
1455
|
+
},
|
|
1456
|
+
/** @private */
|
|
1457
|
+
decodeReply(buffer, view, decoder) {
|
|
1458
|
+
let joinRefSize = view.getUint8(1);
|
|
1459
|
+
let refSize = view.getUint8(2);
|
|
1460
|
+
let topicSize = view.getUint8(3);
|
|
1461
|
+
let eventSize = view.getUint8(4);
|
|
1462
|
+
let offset = this.HEADER_LENGTH + this.META_LENGTH;
|
|
1463
|
+
let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize));
|
|
1464
|
+
offset = offset + joinRefSize;
|
|
1465
|
+
let ref = decoder.decode(buffer.slice(offset, offset + refSize));
|
|
1466
|
+
offset = offset + refSize;
|
|
1467
|
+
let topic = decoder.decode(buffer.slice(offset, offset + topicSize));
|
|
1468
|
+
offset = offset + topicSize;
|
|
1469
|
+
let event = decoder.decode(buffer.slice(offset, offset + eventSize));
|
|
1470
|
+
offset = offset + eventSize;
|
|
1471
|
+
let payload = {
|
|
1472
|
+
status: event,
|
|
1473
|
+
response: buffer.slice(offset, buffer.byteLength)
|
|
1474
|
+
};
|
|
1475
|
+
return {
|
|
1476
|
+
join_ref: joinRef,
|
|
1477
|
+
ref,
|
|
1478
|
+
topic,
|
|
1479
|
+
event: CHANNEL_EVENTS.reply,
|
|
1480
|
+
payload
|
|
1481
|
+
};
|
|
1482
|
+
},
|
|
1483
|
+
/** @private */
|
|
1484
|
+
decodeBroadcast(buffer, view, decoder) {
|
|
1485
|
+
let topicSize = view.getUint8(1);
|
|
1486
|
+
let eventSize = view.getUint8(2);
|
|
1487
|
+
let offset = this.HEADER_LENGTH + 2;
|
|
1488
|
+
let topic = decoder.decode(buffer.slice(offset, offset + topicSize));
|
|
1489
|
+
offset = offset + topicSize;
|
|
1490
|
+
let event = decoder.decode(buffer.slice(offset, offset + eventSize));
|
|
1491
|
+
offset = offset + eventSize;
|
|
1492
|
+
return {
|
|
1493
|
+
join_ref: null,
|
|
1494
|
+
ref: null,
|
|
1495
|
+
topic,
|
|
1496
|
+
event,
|
|
1497
|
+
payload: buffer.slice(offset, buffer.byteLength)
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
var Socket = class {
|
|
1502
|
+
/** Initializes the Socket *
|
|
1503
|
+
*
|
|
1504
|
+
* For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)
|
|
1505
|
+
*
|
|
1506
|
+
* @constructor
|
|
1507
|
+
* @param {string} endPoint - The string WebSocket endpoint, ie, `"ws://example.com/socket"`,
|
|
1508
|
+
* `"wss://example.com"`
|
|
1509
|
+
* `"/socket"` (inherited host & protocol)
|
|
1510
|
+
* @param {SocketOptions} [opts] - Optional configuration
|
|
1511
|
+
*/
|
|
1512
|
+
constructor(endPoint, opts = {}) {
|
|
1513
|
+
this.stateChangeCallbacks = {
|
|
1514
|
+
open: [],
|
|
1515
|
+
close: [],
|
|
1516
|
+
error: [],
|
|
1517
|
+
message: []
|
|
1518
|
+
};
|
|
1519
|
+
this.channels = [];
|
|
1520
|
+
this.sendBuffer = [];
|
|
1521
|
+
this.ref = 0;
|
|
1522
|
+
this.fallbackRef = null;
|
|
1523
|
+
this.timeout = opts.timeout || DEFAULT_TIMEOUT;
|
|
1524
|
+
this.transport = opts.transport || global$1.WebSocket || LongPoll;
|
|
1525
|
+
this.conn = void 0;
|
|
1526
|
+
this.primaryPassedHealthCheck = false;
|
|
1527
|
+
this.longPollFallbackMs = opts.longPollFallbackMs;
|
|
1528
|
+
this.fallbackTimer = null;
|
|
1529
|
+
this.sessionStore = opts.sessionStorage || global$1 && global$1.sessionStorage;
|
|
1530
|
+
this.establishedConnections = 0;
|
|
1531
|
+
this.defaultEncoder = serializer_default.encode.bind(serializer_default);
|
|
1532
|
+
this.defaultDecoder = serializer_default.decode.bind(serializer_default);
|
|
1533
|
+
this.closeWasClean = true;
|
|
1534
|
+
this.disconnecting = false;
|
|
1535
|
+
this.binaryType = opts.binaryType || "arraybuffer";
|
|
1536
|
+
this.connectClock = 1;
|
|
1537
|
+
this.pageHidden = false;
|
|
1538
|
+
this.encode = void 0;
|
|
1539
|
+
this.decode = void 0;
|
|
1540
|
+
if (this.transport !== LongPoll) {
|
|
1541
|
+
this.encode = opts.encode || this.defaultEncoder;
|
|
1542
|
+
this.decode = opts.decode || this.defaultDecoder;
|
|
1543
|
+
} else {
|
|
1544
|
+
this.encode = this.defaultEncoder;
|
|
1545
|
+
this.decode = this.defaultDecoder;
|
|
1546
|
+
}
|
|
1547
|
+
let awaitingConnectionOnPageShow = null;
|
|
1548
|
+
if (phxWindow && phxWindow.addEventListener) {
|
|
1549
|
+
phxWindow.addEventListener("pagehide", (_e) => {
|
|
1550
|
+
if (this.conn) {
|
|
1551
|
+
this.disconnect();
|
|
1552
|
+
awaitingConnectionOnPageShow = this.connectClock;
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
phxWindow.addEventListener("pageshow", (_e) => {
|
|
1556
|
+
if (awaitingConnectionOnPageShow === this.connectClock) {
|
|
1557
|
+
awaitingConnectionOnPageShow = null;
|
|
1558
|
+
this.connect();
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
phxWindow.addEventListener("visibilitychange", () => {
|
|
1562
|
+
if (document.visibilityState === "hidden") this.pageHidden = true;
|
|
1563
|
+
else {
|
|
1564
|
+
this.pageHidden = false;
|
|
1565
|
+
if (!this.isConnected() && !this.closeWasClean) this.teardown(() => this.connect());
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 3e4;
|
|
1570
|
+
this.autoSendHeartbeat = opts.autoSendHeartbeat ?? true;
|
|
1571
|
+
this.heartbeatCallback = opts.heartbeatCallback ?? (() => {});
|
|
1572
|
+
this.rejoinAfterMs = (tries) => {
|
|
1573
|
+
if (opts.rejoinAfterMs) return opts.rejoinAfterMs(tries);
|
|
1574
|
+
else return [
|
|
1575
|
+
1e3,
|
|
1576
|
+
2e3,
|
|
1577
|
+
5e3
|
|
1578
|
+
][tries - 1] || 1e4;
|
|
1579
|
+
};
|
|
1580
|
+
this.reconnectAfterMs = (tries) => {
|
|
1581
|
+
if (opts.reconnectAfterMs) return opts.reconnectAfterMs(tries);
|
|
1582
|
+
else return [
|
|
1583
|
+
10,
|
|
1584
|
+
50,
|
|
1585
|
+
100,
|
|
1586
|
+
150,
|
|
1587
|
+
200,
|
|
1588
|
+
250,
|
|
1589
|
+
500,
|
|
1590
|
+
1e3,
|
|
1591
|
+
2e3
|
|
1592
|
+
][tries - 1] || 5e3;
|
|
1593
|
+
};
|
|
1594
|
+
this.logger = opts.logger || null;
|
|
1595
|
+
if (!this.logger && opts.debug) this.logger = (kind, msg, data) => {
|
|
1596
|
+
console.log(`${kind}: ${msg}`, data);
|
|
1597
|
+
};
|
|
1598
|
+
this.longpollerTimeout = opts.longpollerTimeout || 2e4;
|
|
1599
|
+
this.params = closure(opts.params || {});
|
|
1600
|
+
this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`;
|
|
1601
|
+
this.vsn = opts.vsn || DEFAULT_VSN;
|
|
1602
|
+
this.heartbeatTimeoutTimer = null;
|
|
1603
|
+
this.heartbeatTimer = null;
|
|
1604
|
+
this.heartbeatSentAt = null;
|
|
1605
|
+
this.pendingHeartbeatRef = null;
|
|
1606
|
+
this.reconnectTimer = new Timer(() => {
|
|
1607
|
+
if (this.pageHidden) {
|
|
1608
|
+
this.log("Not reconnecting as page is hidden!");
|
|
1609
|
+
this.teardown();
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
this.teardown(async () => {
|
|
1613
|
+
if (opts.beforeReconnect) await opts.beforeReconnect();
|
|
1614
|
+
this.connect();
|
|
1615
|
+
});
|
|
1616
|
+
}, this.reconnectAfterMs);
|
|
1617
|
+
this.authToken = opts.authToken;
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Returns the LongPoll transport reference
|
|
1621
|
+
*/
|
|
1622
|
+
getLongPollTransport() {
|
|
1623
|
+
return LongPoll;
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Disconnects and replaces the active transport
|
|
1627
|
+
*
|
|
1628
|
+
* @param {SocketTransport} newTransport - The new transport class to instantiate
|
|
1629
|
+
*
|
|
1630
|
+
*/
|
|
1631
|
+
replaceTransport(newTransport) {
|
|
1632
|
+
this.connectClock++;
|
|
1633
|
+
this.closeWasClean = true;
|
|
1634
|
+
clearTimeout(this.fallbackTimer);
|
|
1635
|
+
this.reconnectTimer.reset();
|
|
1636
|
+
if (this.conn) {
|
|
1637
|
+
this.conn.close();
|
|
1638
|
+
this.conn = null;
|
|
1639
|
+
}
|
|
1640
|
+
this.transport = newTransport;
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Returns the socket protocol
|
|
1644
|
+
*
|
|
1645
|
+
* @returns {"wss" | "ws"}
|
|
1646
|
+
*/
|
|
1647
|
+
protocol() {
|
|
1648
|
+
return location.protocol.match(/^https/) ? "wss" : "ws";
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* The fully qualified socket url
|
|
1652
|
+
*
|
|
1653
|
+
* @returns {string}
|
|
1654
|
+
*/
|
|
1655
|
+
endPointURL() {
|
|
1656
|
+
let uri = Ajax.appendParams(Ajax.appendParams(this.endPoint, this.params()), { vsn: this.vsn });
|
|
1657
|
+
if (uri.charAt(0) !== "/") return uri;
|
|
1658
|
+
if (uri.charAt(1) === "/") return `${this.protocol()}:${uri}`;
|
|
1659
|
+
return `${this.protocol()}://${location.host}${uri}`;
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Disconnects the socket
|
|
1663
|
+
*
|
|
1664
|
+
* See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes for valid status codes.
|
|
1665
|
+
*
|
|
1666
|
+
* @param {() => void} [callback] - Optional callback which is called after socket is disconnected.
|
|
1667
|
+
* @param {number} [code] - A status code for disconnection (Optional).
|
|
1668
|
+
* @param {string} [reason] - A textual description of the reason to disconnect. (Optional)
|
|
1669
|
+
*/
|
|
1670
|
+
disconnect(callback, code, reason) {
|
|
1671
|
+
this.connectClock++;
|
|
1672
|
+
this.disconnecting = true;
|
|
1673
|
+
this.closeWasClean = true;
|
|
1674
|
+
clearTimeout(this.fallbackTimer);
|
|
1675
|
+
this.reconnectTimer.reset();
|
|
1676
|
+
this.teardown(() => {
|
|
1677
|
+
this.disconnecting = false;
|
|
1678
|
+
callback && callback();
|
|
1679
|
+
}, code, reason);
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* @param {Params} [params] - [DEPRECATED] The params to send when connecting, for example `{user_id: userToken}`
|
|
1683
|
+
*
|
|
1684
|
+
* Passing params to connect is deprecated; pass them in the Socket constructor instead:
|
|
1685
|
+
* `new Socket("/socket", {params: {user_id: userToken}})`.
|
|
1686
|
+
*/
|
|
1687
|
+
connect(params) {
|
|
1688
|
+
if (params) {
|
|
1689
|
+
console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor");
|
|
1690
|
+
this.params = closure(params);
|
|
1691
|
+
}
|
|
1692
|
+
if (this.conn && !this.disconnecting) return;
|
|
1693
|
+
if (this.longPollFallbackMs && this.transport !== LongPoll) this.connectWithFallback(LongPoll, this.longPollFallbackMs);
|
|
1694
|
+
else this.transportConnect();
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Logs the message. Override `this.logger` for specialized logging. noops by default
|
|
1698
|
+
* @param {string} kind
|
|
1699
|
+
* @param {string} msg
|
|
1700
|
+
* @param {Object} data
|
|
1701
|
+
*/
|
|
1702
|
+
log(kind, msg, data) {
|
|
1703
|
+
this.logger && this.logger(kind, msg, data);
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Returns true if a logger has been set on this socket.
|
|
1707
|
+
*/
|
|
1708
|
+
hasLogger() {
|
|
1709
|
+
return this.logger !== null;
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Registers callbacks for connection open events
|
|
1713
|
+
*
|
|
1714
|
+
* @example socket.onOpen(function(){ console.info("the socket was opened") })
|
|
1715
|
+
*
|
|
1716
|
+
* @param {SocketOnOpen} callback
|
|
1717
|
+
*/
|
|
1718
|
+
onOpen(callback) {
|
|
1719
|
+
let ref = this.makeRef();
|
|
1720
|
+
this.stateChangeCallbacks.open.push([ref, callback]);
|
|
1721
|
+
return ref;
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Registers callbacks for connection close events
|
|
1725
|
+
* @param {SocketOnClose} callback
|
|
1726
|
+
* @returns {string}
|
|
1727
|
+
*/
|
|
1728
|
+
onClose(callback) {
|
|
1729
|
+
let ref = this.makeRef();
|
|
1730
|
+
this.stateChangeCallbacks.close.push([ref, callback]);
|
|
1731
|
+
return ref;
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Registers callbacks for connection error events
|
|
1735
|
+
*
|
|
1736
|
+
* @example socket.onError(function(error){ alert("An error occurred") })
|
|
1737
|
+
*
|
|
1738
|
+
* @param {SocketOnError} callback
|
|
1739
|
+
* @returns {string}
|
|
1740
|
+
*/
|
|
1741
|
+
onError(callback) {
|
|
1742
|
+
let ref = this.makeRef();
|
|
1743
|
+
this.stateChangeCallbacks.error.push([ref, callback]);
|
|
1744
|
+
return ref;
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Registers callbacks for connection message events
|
|
1748
|
+
* @param {SocketOnMessage} callback
|
|
1749
|
+
* @returns {string}
|
|
1750
|
+
*/
|
|
1751
|
+
onMessage(callback) {
|
|
1752
|
+
let ref = this.makeRef();
|
|
1753
|
+
this.stateChangeCallbacks.message.push([ref, callback]);
|
|
1754
|
+
return ref;
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Sets a callback that receives lifecycle events for internal heartbeat messages.
|
|
1758
|
+
* Useful for instrumenting connection health (e.g. sent/ok/timeout/disconnected).
|
|
1759
|
+
* @param {HeartbeatCallback} callback
|
|
1760
|
+
*/
|
|
1761
|
+
onHeartbeat(callback) {
|
|
1762
|
+
this.heartbeatCallback = callback;
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Pings the server and invokes the callback with the RTT in milliseconds
|
|
1766
|
+
* @param {(timeDelta: number) => void} callback
|
|
1767
|
+
*
|
|
1768
|
+
* Returns true if the ping was pushed or false if unable to be pushed.
|
|
1769
|
+
*/
|
|
1770
|
+
ping(callback) {
|
|
1771
|
+
if (!this.isConnected()) return false;
|
|
1772
|
+
let ref = this.makeRef();
|
|
1773
|
+
let startTime = Date.now();
|
|
1774
|
+
this.push({
|
|
1775
|
+
topic: "phoenix",
|
|
1776
|
+
event: "heartbeat",
|
|
1777
|
+
payload: {},
|
|
1778
|
+
ref
|
|
1779
|
+
});
|
|
1780
|
+
let onMsgRef = this.onMessage((msg) => {
|
|
1781
|
+
if (msg.ref === ref) {
|
|
1782
|
+
this.off([onMsgRef]);
|
|
1783
|
+
callback(Date.now() - startTime);
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
return true;
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* @private
|
|
1790
|
+
*
|
|
1791
|
+
* @param {Function}
|
|
1792
|
+
*/
|
|
1793
|
+
transportName(transport) {
|
|
1794
|
+
switch (transport) {
|
|
1795
|
+
case LongPoll: return "LongPoll";
|
|
1796
|
+
default: return transport.name;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* @private
|
|
1801
|
+
*/
|
|
1802
|
+
transportConnect() {
|
|
1803
|
+
this.connectClock++;
|
|
1804
|
+
this.closeWasClean = false;
|
|
1805
|
+
let protocols = void 0;
|
|
1806
|
+
if (this.authToken) protocols = ["phoenix", `${AUTH_TOKEN_PREFIX}${btoa(this.authToken).replace(/=/g, "")}`];
|
|
1807
|
+
this.conn = new this.transport(this.endPointURL(), protocols);
|
|
1808
|
+
this.conn.binaryType = this.binaryType;
|
|
1809
|
+
this.conn.timeout = this.longpollerTimeout;
|
|
1810
|
+
this.conn.onopen = () => this.onConnOpen();
|
|
1811
|
+
this.conn.onerror = (error) => this.onConnError(error);
|
|
1812
|
+
this.conn.onmessage = (event) => this.onConnMessage(event);
|
|
1813
|
+
this.conn.onclose = (event) => this.onConnClose(event);
|
|
1814
|
+
}
|
|
1815
|
+
getSession(key) {
|
|
1816
|
+
return this.sessionStore && this.sessionStore.getItem(key);
|
|
1817
|
+
}
|
|
1818
|
+
storeSession(key, val) {
|
|
1819
|
+
this.sessionStore && this.sessionStore.setItem(key, val);
|
|
1820
|
+
}
|
|
1821
|
+
connectWithFallback(fallbackTransport, fallbackThreshold = 2500) {
|
|
1822
|
+
clearTimeout(this.fallbackTimer);
|
|
1823
|
+
let established = false;
|
|
1824
|
+
let primaryTransport = true;
|
|
1825
|
+
let openRef, errorRef;
|
|
1826
|
+
let fallbackTransportName = this.transportName(fallbackTransport);
|
|
1827
|
+
let fallback = (reason) => {
|
|
1828
|
+
this.log("transport", `falling back to ${fallbackTransportName}...`, reason);
|
|
1829
|
+
this.off([openRef, errorRef]);
|
|
1830
|
+
primaryTransport = false;
|
|
1831
|
+
this.replaceTransport(fallbackTransport);
|
|
1832
|
+
this.transportConnect();
|
|
1833
|
+
};
|
|
1834
|
+
if (this.getSession(`phx:fallback:${fallbackTransportName}`)) return fallback("memorized");
|
|
1835
|
+
this.fallbackTimer = setTimeout(fallback, fallbackThreshold);
|
|
1836
|
+
errorRef = this.onError((reason) => {
|
|
1837
|
+
this.log("transport", "error", reason);
|
|
1838
|
+
if (primaryTransport && !established) {
|
|
1839
|
+
clearTimeout(this.fallbackTimer);
|
|
1840
|
+
fallback(reason);
|
|
1841
|
+
}
|
|
1842
|
+
});
|
|
1843
|
+
if (this.fallbackRef) this.off([this.fallbackRef]);
|
|
1844
|
+
this.fallbackRef = this.onOpen(() => {
|
|
1845
|
+
established = true;
|
|
1846
|
+
if (!primaryTransport) {
|
|
1847
|
+
let fallbackTransportName2 = this.transportName(fallbackTransport);
|
|
1848
|
+
if (!this.primaryPassedHealthCheck) this.storeSession(`phx:fallback:${fallbackTransportName2}`, "true");
|
|
1849
|
+
return this.log("transport", `established ${fallbackTransportName2} fallback`);
|
|
1850
|
+
}
|
|
1851
|
+
clearTimeout(this.fallbackTimer);
|
|
1852
|
+
this.fallbackTimer = setTimeout(fallback, fallbackThreshold);
|
|
1853
|
+
this.ping((rtt) => {
|
|
1854
|
+
this.log("transport", "connected to primary after", rtt);
|
|
1855
|
+
this.primaryPassedHealthCheck = true;
|
|
1856
|
+
clearTimeout(this.fallbackTimer);
|
|
1857
|
+
});
|
|
1858
|
+
});
|
|
1859
|
+
this.transportConnect();
|
|
1860
|
+
}
|
|
1861
|
+
clearHeartbeats() {
|
|
1862
|
+
clearTimeout(this.heartbeatTimer);
|
|
1863
|
+
clearTimeout(this.heartbeatTimeoutTimer);
|
|
1864
|
+
}
|
|
1865
|
+
onConnOpen() {
|
|
1866
|
+
if (this.hasLogger()) this.log("transport", `connected to ${this.endPointURL()}`);
|
|
1867
|
+
this.closeWasClean = false;
|
|
1868
|
+
this.disconnecting = false;
|
|
1869
|
+
this.establishedConnections++;
|
|
1870
|
+
this.flushSendBuffer();
|
|
1871
|
+
this.reconnectTimer.reset();
|
|
1872
|
+
if (this.autoSendHeartbeat) this.resetHeartbeat();
|
|
1873
|
+
this.triggerStateCallbacks("open");
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* @private
|
|
1877
|
+
*/
|
|
1878
|
+
heartbeatTimeout() {
|
|
1879
|
+
if (this.pendingHeartbeatRef) {
|
|
1880
|
+
this.pendingHeartbeatRef = null;
|
|
1881
|
+
this.heartbeatSentAt = null;
|
|
1882
|
+
if (this.hasLogger()) this.log("transport", "heartbeat timeout. Attempting to re-establish connection");
|
|
1883
|
+
try {
|
|
1884
|
+
this.heartbeatCallback("timeout");
|
|
1885
|
+
} catch (e) {
|
|
1886
|
+
this.log("error", "error in heartbeat callback", e);
|
|
1887
|
+
}
|
|
1888
|
+
this.triggerChanError();
|
|
1889
|
+
this.closeWasClean = false;
|
|
1890
|
+
this.teardown(() => this.reconnectTimer.scheduleTimeout(), WS_CLOSE_NORMAL, "heartbeat timeout");
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
resetHeartbeat() {
|
|
1894
|
+
if (this.conn && this.conn.skipHeartbeat) return;
|
|
1895
|
+
this.pendingHeartbeatRef = null;
|
|
1896
|
+
this.clearHeartbeats();
|
|
1897
|
+
this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs);
|
|
1898
|
+
}
|
|
1899
|
+
teardown(callback, code, reason) {
|
|
1900
|
+
if (!this.conn) return callback && callback();
|
|
1901
|
+
const connToClose = this.conn;
|
|
1902
|
+
this.waitForBufferDone(connToClose, () => {
|
|
1903
|
+
if (code) connToClose.close(code, reason || "");
|
|
1904
|
+
else connToClose.close();
|
|
1905
|
+
this.waitForSocketClosed(connToClose, () => {
|
|
1906
|
+
if (this.conn === connToClose) {
|
|
1907
|
+
this.conn.onopen = function() {};
|
|
1908
|
+
this.conn.onerror = function() {};
|
|
1909
|
+
this.conn.onmessage = function() {};
|
|
1910
|
+
this.conn.onclose = function() {};
|
|
1911
|
+
this.conn = null;
|
|
1912
|
+
}
|
|
1913
|
+
callback && callback();
|
|
1914
|
+
});
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
waitForBufferDone(conn, callback, tries = 1) {
|
|
1918
|
+
if (tries === 5 || !conn.bufferedAmount) {
|
|
1919
|
+
callback();
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
setTimeout(() => {
|
|
1923
|
+
this.waitForBufferDone(conn, callback, tries + 1);
|
|
1924
|
+
}, 150 * tries);
|
|
1925
|
+
}
|
|
1926
|
+
waitForSocketClosed(conn, callback, tries = 1) {
|
|
1927
|
+
if (tries === 5 || conn.readyState === SOCKET_STATES.closed) {
|
|
1928
|
+
callback();
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
setTimeout(() => {
|
|
1932
|
+
this.waitForSocketClosed(conn, callback, tries + 1);
|
|
1933
|
+
}, 150 * tries);
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* @param {CloseEvent} event
|
|
1937
|
+
*/
|
|
1938
|
+
onConnClose(event) {
|
|
1939
|
+
if (this.conn) this.conn.onclose = () => {};
|
|
1940
|
+
if (this.hasLogger()) this.log("transport", "close", event);
|
|
1941
|
+
this.triggerChanError();
|
|
1942
|
+
this.clearHeartbeats();
|
|
1943
|
+
if (!this.closeWasClean) this.reconnectTimer.scheduleTimeout();
|
|
1944
|
+
this.triggerStateCallbacks("close", event);
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* @private
|
|
1948
|
+
* @param {Event} error
|
|
1949
|
+
*/
|
|
1950
|
+
onConnError(error) {
|
|
1951
|
+
if (this.hasLogger()) this.log("transport", error);
|
|
1952
|
+
let transportBefore = this.transport;
|
|
1953
|
+
let establishedBefore = this.establishedConnections;
|
|
1954
|
+
this.triggerStateCallbacks("error", error, transportBefore, establishedBefore);
|
|
1955
|
+
if (transportBefore === this.transport || establishedBefore > 0) this.triggerChanError();
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* @private
|
|
1959
|
+
*/
|
|
1960
|
+
triggerChanError() {
|
|
1961
|
+
this.channels.forEach((channel) => {
|
|
1962
|
+
if (!(channel.isErrored() || channel.isLeaving() || channel.isClosed())) channel.trigger(CHANNEL_EVENTS.error);
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* @returns {string}
|
|
1967
|
+
*/
|
|
1968
|
+
connectionState() {
|
|
1969
|
+
switch (this.conn && this.conn.readyState) {
|
|
1970
|
+
case SOCKET_STATES.connecting: return "connecting";
|
|
1971
|
+
case SOCKET_STATES.open: return "open";
|
|
1972
|
+
case SOCKET_STATES.closing: return "closing";
|
|
1973
|
+
default: return "closed";
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
/**
|
|
1977
|
+
* @returns {boolean}
|
|
1978
|
+
*/
|
|
1979
|
+
isConnected() {
|
|
1980
|
+
return this.connectionState() === "open";
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
*
|
|
1984
|
+
* @param {Channel} channel
|
|
1985
|
+
*/
|
|
1986
|
+
remove(channel) {
|
|
1987
|
+
this.off(channel.stateChangeRefs);
|
|
1988
|
+
this.channels = this.channels.filter((c) => c !== channel);
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations.
|
|
1992
|
+
*
|
|
1993
|
+
* @param {string[]} refs - list of refs returned by calls to
|
|
1994
|
+
* `onOpen`, `onClose`, `onError,` and `onMessage`
|
|
1995
|
+
*/
|
|
1996
|
+
off(refs) {
|
|
1997
|
+
for (let key in this.stateChangeCallbacks) this.stateChangeCallbacks[key] = this.stateChangeCallbacks[key].filter(([ref]) => {
|
|
1998
|
+
return refs.indexOf(ref) === -1;
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Initiates a new channel for the given topic
|
|
2003
|
+
*
|
|
2004
|
+
* @param {string} topic
|
|
2005
|
+
* @param {Params | (() => Params)} [chanParams]- Parameters for the channel
|
|
2006
|
+
* @returns {Channel}
|
|
2007
|
+
*/
|
|
2008
|
+
channel(topic, chanParams = {}) {
|
|
2009
|
+
let chan = new Channel(topic, chanParams, this);
|
|
2010
|
+
this.channels.push(chan);
|
|
2011
|
+
return chan;
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* @param {Message<Record<string, any>>} data
|
|
2015
|
+
*/
|
|
2016
|
+
push(data) {
|
|
2017
|
+
if (this.hasLogger()) {
|
|
2018
|
+
let { topic, event, payload, ref, join_ref } = data;
|
|
2019
|
+
this.log("push", `${topic} ${event} (${join_ref}, ${ref})`, payload);
|
|
2020
|
+
}
|
|
2021
|
+
if (this.isConnected()) this.encode(data, (result) => this.conn.send(result));
|
|
2022
|
+
else this.sendBuffer.push(() => this.encode(data, (result) => this.conn.send(result)));
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Return the next message ref, accounting for overflows
|
|
2026
|
+
* @returns {string}
|
|
2027
|
+
*/
|
|
2028
|
+
makeRef() {
|
|
2029
|
+
let newRef = this.ref + 1;
|
|
2030
|
+
if (newRef === this.ref) this.ref = 0;
|
|
2031
|
+
else this.ref = newRef;
|
|
2032
|
+
return this.ref.toString();
|
|
2033
|
+
}
|
|
2034
|
+
sendHeartbeat() {
|
|
2035
|
+
if (!this.isConnected()) {
|
|
2036
|
+
try {
|
|
2037
|
+
this.heartbeatCallback("disconnected");
|
|
2038
|
+
} catch (e) {
|
|
2039
|
+
this.log("error", "error in heartbeat callback", e);
|
|
2040
|
+
}
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
if (this.pendingHeartbeatRef) {
|
|
2044
|
+
this.heartbeatTimeout();
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
this.pendingHeartbeatRef = this.makeRef();
|
|
2048
|
+
this.heartbeatSentAt = Date.now();
|
|
2049
|
+
this.push({
|
|
2050
|
+
topic: "phoenix",
|
|
2051
|
+
event: "heartbeat",
|
|
2052
|
+
payload: {},
|
|
2053
|
+
ref: this.pendingHeartbeatRef
|
|
2054
|
+
});
|
|
2055
|
+
try {
|
|
2056
|
+
this.heartbeatCallback("sent");
|
|
2057
|
+
} catch (e) {
|
|
2058
|
+
this.log("error", "error in heartbeat callback", e);
|
|
2059
|
+
}
|
|
2060
|
+
this.heartbeatTimeoutTimer = setTimeout(() => this.heartbeatTimeout(), this.heartbeatIntervalMs);
|
|
2061
|
+
}
|
|
2062
|
+
flushSendBuffer() {
|
|
2063
|
+
if (this.isConnected() && this.sendBuffer.length > 0) {
|
|
2064
|
+
this.sendBuffer.forEach((callback) => callback());
|
|
2065
|
+
this.sendBuffer = [];
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* @param {MessageEvent<any>} rawMessage
|
|
2070
|
+
*/
|
|
2071
|
+
onConnMessage(rawMessage) {
|
|
2072
|
+
this.decode(rawMessage.data, (msg) => {
|
|
2073
|
+
let { topic, event, payload, ref, join_ref } = msg;
|
|
2074
|
+
if (ref && ref === this.pendingHeartbeatRef) {
|
|
2075
|
+
const latency = this.heartbeatSentAt ? Date.now() - this.heartbeatSentAt : void 0;
|
|
2076
|
+
this.clearHeartbeats();
|
|
2077
|
+
try {
|
|
2078
|
+
this.heartbeatCallback(payload.status === "ok" ? "ok" : "error", latency);
|
|
2079
|
+
} catch (e) {
|
|
2080
|
+
this.log("error", "error in heartbeat callback", e);
|
|
2081
|
+
}
|
|
2082
|
+
this.pendingHeartbeatRef = null;
|
|
2083
|
+
this.heartbeatSentAt = null;
|
|
2084
|
+
if (this.autoSendHeartbeat) this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs);
|
|
2085
|
+
}
|
|
2086
|
+
if (this.hasLogger()) this.log("receive", `${payload.status || ""} ${topic} ${event} ${ref && "(" + ref + ")" || ""}`.trim(), payload);
|
|
2087
|
+
for (let i = 0; i < this.channels.length; i++) {
|
|
2088
|
+
const channel = this.channels[i];
|
|
2089
|
+
if (!channel.isMember(topic, event, payload, join_ref)) continue;
|
|
2090
|
+
channel.trigger(event, payload, ref, join_ref);
|
|
2091
|
+
}
|
|
2092
|
+
this.triggerStateCallbacks("message", msg);
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* @private
|
|
2097
|
+
* @template {keyof SocketStateChangeCallbacks} K
|
|
2098
|
+
* @param {K} event
|
|
2099
|
+
* @param {...Parameters<SocketStateChangeCallbacks[K][number][1]>} args
|
|
2100
|
+
* @returns {void}
|
|
2101
|
+
*/
|
|
2102
|
+
triggerStateCallbacks(event, ...args) {
|
|
2103
|
+
try {
|
|
2104
|
+
this.stateChangeCallbacks[event].forEach(([_, callback]) => {
|
|
2105
|
+
try {
|
|
2106
|
+
callback(...args);
|
|
2107
|
+
} catch (e) {
|
|
2108
|
+
this.log("error", `error in ${event} callback`, e);
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
} catch (e) {
|
|
2112
|
+
this.log("error", `error triggering ${event} callbacks`, e);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
leaveOpenTopic(topic) {
|
|
2116
|
+
let dupChannel = this.channels.find((c) => c.topic === topic && (c.isJoined() || c.isJoining()));
|
|
2117
|
+
if (dupChannel) {
|
|
2118
|
+
if (this.hasLogger()) this.log("transport", `leaving duplicate topic "${topic}"`);
|
|
2119
|
+
dupChannel.leave();
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
};
|
|
2123
|
+
//#endregion
|
|
2124
|
+
//#region ../../node_modules/.pnpm/@supabase+realtime-js@2.100.1/node_modules/@supabase/realtime-js/dist/module/phoenix/presenceAdapter.js
|
|
2125
|
+
var PresenceAdapter = class PresenceAdapter {
|
|
2126
|
+
constructor(channel, opts) {
|
|
2127
|
+
const phoenixOptions = phoenixPresenceOptions(opts);
|
|
2128
|
+
this.presence = new Presence(channel.getChannel(), phoenixOptions);
|
|
2129
|
+
this.presence.onJoin((key, currentPresence, newPresence) => {
|
|
2130
|
+
const onJoinPayload = PresenceAdapter.onJoinPayload(key, currentPresence, newPresence);
|
|
2131
|
+
channel.getChannel().trigger("presence", onJoinPayload);
|
|
2132
|
+
});
|
|
2133
|
+
this.presence.onLeave((key, currentPresence, leftPresence) => {
|
|
2134
|
+
const onLeavePayload = PresenceAdapter.onLeavePayload(key, currentPresence, leftPresence);
|
|
2135
|
+
channel.getChannel().trigger("presence", onLeavePayload);
|
|
2136
|
+
});
|
|
2137
|
+
this.presence.onSync(() => {
|
|
2138
|
+
channel.getChannel().trigger("presence", { event: "sync" });
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
get state() {
|
|
2142
|
+
return PresenceAdapter.transformState(this.presence.state);
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* @private
|
|
2146
|
+
* Remove 'metas' key
|
|
2147
|
+
* Change 'phx_ref' to 'presence_ref'
|
|
2148
|
+
* Remove 'phx_ref' and 'phx_ref_prev'
|
|
2149
|
+
*
|
|
2150
|
+
* @example Transform state
|
|
2151
|
+
* // returns {
|
|
2152
|
+
* abc123: [
|
|
2153
|
+
* { presence_ref: '2', user_id: 1 },
|
|
2154
|
+
* { presence_ref: '3', user_id: 2 }
|
|
2155
|
+
* ]
|
|
2156
|
+
* }
|
|
2157
|
+
* RealtimePresence.transformState({
|
|
2158
|
+
* abc123: {
|
|
2159
|
+
* metas: [
|
|
2160
|
+
* { phx_ref: '2', phx_ref_prev: '1' user_id: 1 },
|
|
2161
|
+
* { phx_ref: '3', user_id: 2 }
|
|
2162
|
+
* ]
|
|
2163
|
+
* }
|
|
2164
|
+
* })
|
|
2165
|
+
*
|
|
2166
|
+
*/
|
|
2167
|
+
static transformState(state) {
|
|
2168
|
+
state = cloneState(state);
|
|
2169
|
+
return Object.getOwnPropertyNames(state).reduce((newState, key) => {
|
|
2170
|
+
const presences = state[key];
|
|
2171
|
+
newState[key] = transformState(presences);
|
|
2172
|
+
return newState;
|
|
2173
|
+
}, {});
|
|
2174
|
+
}
|
|
2175
|
+
static onJoinPayload(key, currentPresence, newPresence) {
|
|
2176
|
+
return {
|
|
2177
|
+
event: "join",
|
|
2178
|
+
key,
|
|
2179
|
+
currentPresences: parseCurrentPresences(currentPresence),
|
|
2180
|
+
newPresences: transformState(newPresence)
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
static onLeavePayload(key, currentPresence, leftPresence) {
|
|
2184
|
+
return {
|
|
2185
|
+
event: "leave",
|
|
2186
|
+
key,
|
|
2187
|
+
currentPresences: parseCurrentPresences(currentPresence),
|
|
2188
|
+
leftPresences: transformState(leftPresence)
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
};
|
|
2192
|
+
function transformState(presences) {
|
|
2193
|
+
return presences.metas.map((presence) => {
|
|
2194
|
+
presence["presence_ref"] = presence["phx_ref"];
|
|
2195
|
+
delete presence["phx_ref"];
|
|
2196
|
+
delete presence["phx_ref_prev"];
|
|
2197
|
+
return presence;
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
function cloneState(state) {
|
|
2201
|
+
return JSON.parse(JSON.stringify(state));
|
|
2202
|
+
}
|
|
2203
|
+
function phoenixPresenceOptions(opts) {
|
|
2204
|
+
return (opts === null || opts === void 0 ? void 0 : opts.events) && { events: opts.events };
|
|
2205
|
+
}
|
|
2206
|
+
function parseCurrentPresences(currentPresences) {
|
|
2207
|
+
return (currentPresences === null || currentPresences === void 0 ? void 0 : currentPresences.metas) ? transformState(currentPresences) : [];
|
|
2208
|
+
}
|
|
2209
|
+
//#endregion
|
|
2210
|
+
//#region ../../node_modules/.pnpm/@supabase+realtime-js@2.100.1/node_modules/@supabase/realtime-js/dist/module/RealtimePresence.js
|
|
2211
|
+
var REALTIME_PRESENCE_LISTEN_EVENTS;
|
|
2212
|
+
(function(REALTIME_PRESENCE_LISTEN_EVENTS) {
|
|
2213
|
+
REALTIME_PRESENCE_LISTEN_EVENTS["SYNC"] = "sync";
|
|
2214
|
+
REALTIME_PRESENCE_LISTEN_EVENTS["JOIN"] = "join";
|
|
2215
|
+
REALTIME_PRESENCE_LISTEN_EVENTS["LEAVE"] = "leave";
|
|
2216
|
+
})(REALTIME_PRESENCE_LISTEN_EVENTS || (REALTIME_PRESENCE_LISTEN_EVENTS = {}));
|
|
2217
|
+
var RealtimePresence = class {
|
|
2218
|
+
get state() {
|
|
2219
|
+
return this.presenceAdapter.state;
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Creates a Presence helper that keeps the local presence state in sync with the server.
|
|
2223
|
+
*
|
|
2224
|
+
* @param channel - The realtime channel to bind to.
|
|
2225
|
+
* @param opts - Optional custom event names, e.g. `{ events: { state: 'state', diff: 'diff' } }`.
|
|
2226
|
+
*
|
|
2227
|
+
* @category Realtime
|
|
2228
|
+
*
|
|
2229
|
+
* @example Example for a presence channel
|
|
2230
|
+
* ```ts
|
|
2231
|
+
* const presence = new RealtimePresence(channel)
|
|
2232
|
+
*
|
|
2233
|
+
* channel.on('presence', ({ event, key }) => {
|
|
2234
|
+
* console.log(`Presence ${event} on ${key}`)
|
|
2235
|
+
* })
|
|
2236
|
+
* ```
|
|
2237
|
+
*/
|
|
2238
|
+
constructor(channel, opts) {
|
|
2239
|
+
this.channel = channel;
|
|
2240
|
+
this.presenceAdapter = new PresenceAdapter(this.channel.channelAdapter, opts);
|
|
2241
|
+
}
|
|
2242
|
+
};
|
|
2243
|
+
//#endregion
|
|
2244
|
+
//#region ../../node_modules/.pnpm/@supabase+realtime-js@2.100.1/node_modules/@supabase/realtime-js/dist/module/phoenix/channelAdapter.js
|
|
2245
|
+
var ChannelAdapter = class {
|
|
2246
|
+
constructor(socket, topic, params) {
|
|
2247
|
+
const phoenixParams = phoenixChannelParams(params);
|
|
2248
|
+
this.channel = socket.getSocket().channel(topic, phoenixParams);
|
|
2249
|
+
this.socket = socket;
|
|
2250
|
+
}
|
|
2251
|
+
get state() {
|
|
2252
|
+
return this.channel.state;
|
|
2253
|
+
}
|
|
2254
|
+
set state(state) {
|
|
2255
|
+
this.channel.state = state;
|
|
2256
|
+
}
|
|
2257
|
+
get joinedOnce() {
|
|
2258
|
+
return this.channel.joinedOnce;
|
|
2259
|
+
}
|
|
2260
|
+
get joinPush() {
|
|
2261
|
+
return this.channel.joinPush;
|
|
2262
|
+
}
|
|
2263
|
+
get rejoinTimer() {
|
|
2264
|
+
return this.channel.rejoinTimer;
|
|
2265
|
+
}
|
|
2266
|
+
on(event, callback) {
|
|
2267
|
+
return this.channel.on(event, callback);
|
|
2268
|
+
}
|
|
2269
|
+
off(event, refNumber) {
|
|
2270
|
+
this.channel.off(event, refNumber);
|
|
2271
|
+
}
|
|
2272
|
+
subscribe(timeout) {
|
|
2273
|
+
return this.channel.join(timeout);
|
|
2274
|
+
}
|
|
2275
|
+
unsubscribe(timeout) {
|
|
2276
|
+
return this.channel.leave(timeout);
|
|
2277
|
+
}
|
|
2278
|
+
teardown() {
|
|
2279
|
+
this.channel.teardown();
|
|
2280
|
+
}
|
|
2281
|
+
onClose(callback) {
|
|
2282
|
+
this.channel.onClose(callback);
|
|
2283
|
+
}
|
|
2284
|
+
onError(callback) {
|
|
2285
|
+
return this.channel.onError(callback);
|
|
2286
|
+
}
|
|
2287
|
+
push(event, payload, timeout) {
|
|
2288
|
+
let push;
|
|
2289
|
+
try {
|
|
2290
|
+
push = this.channel.push(event, payload, timeout);
|
|
2291
|
+
} catch (error) {
|
|
2292
|
+
throw `tried to push '${event}' to '${this.channel.topic}' before joining. Use channel.subscribe() before pushing events`;
|
|
2293
|
+
}
|
|
2294
|
+
if (this.channel.pushBuffer.length > 100) {
|
|
2295
|
+
const removedPush = this.channel.pushBuffer.shift();
|
|
2296
|
+
removedPush.cancelTimeout();
|
|
2297
|
+
this.socket.log("channel", `discarded push due to buffer overflow: ${removedPush.event}`, removedPush.payload());
|
|
2298
|
+
}
|
|
2299
|
+
return push;
|
|
2300
|
+
}
|
|
2301
|
+
updateJoinPayload(payload) {
|
|
2302
|
+
const oldPayload = this.channel.joinPush.payload();
|
|
2303
|
+
this.channel.joinPush.payload = () => Object.assign(Object.assign({}, oldPayload), payload);
|
|
2304
|
+
}
|
|
2305
|
+
canPush() {
|
|
2306
|
+
return this.socket.isConnected() && this.state === CHANNEL_STATES$1.joined;
|
|
2307
|
+
}
|
|
2308
|
+
isJoined() {
|
|
2309
|
+
return this.state === CHANNEL_STATES$1.joined;
|
|
2310
|
+
}
|
|
2311
|
+
isJoining() {
|
|
2312
|
+
return this.state === CHANNEL_STATES$1.joining;
|
|
2313
|
+
}
|
|
2314
|
+
isClosed() {
|
|
2315
|
+
return this.state === CHANNEL_STATES$1.closed;
|
|
2316
|
+
}
|
|
2317
|
+
isLeaving() {
|
|
2318
|
+
return this.state === CHANNEL_STATES$1.leaving;
|
|
2319
|
+
}
|
|
2320
|
+
updateFilterBindings(filterBindings) {
|
|
2321
|
+
this.channel.filterBindings = filterBindings;
|
|
2322
|
+
}
|
|
2323
|
+
updatePayloadTransform(callback) {
|
|
2324
|
+
this.channel.onMessage = callback;
|
|
2325
|
+
}
|
|
2326
|
+
/**
|
|
2327
|
+
* @internal
|
|
2328
|
+
*/
|
|
2329
|
+
getChannel() {
|
|
2330
|
+
return this.channel;
|
|
2331
|
+
}
|
|
2332
|
+
};
|
|
2333
|
+
function phoenixChannelParams(options) {
|
|
2334
|
+
return { config: Object.assign({
|
|
2335
|
+
broadcast: {
|
|
2336
|
+
ack: false,
|
|
2337
|
+
self: false
|
|
2338
|
+
},
|
|
2339
|
+
presence: {
|
|
2340
|
+
key: "",
|
|
2341
|
+
enabled: false
|
|
2342
|
+
},
|
|
2343
|
+
private: false
|
|
2344
|
+
}, options.config) };
|
|
2345
|
+
}
|
|
2346
|
+
//#endregion
|
|
2347
|
+
//#region ../../node_modules/.pnpm/@supabase+realtime-js@2.100.1/node_modules/@supabase/realtime-js/dist/module/RealtimeChannel.js
|
|
2348
|
+
var REALTIME_POSTGRES_CHANGES_LISTEN_EVENT;
|
|
2349
|
+
(function(REALTIME_POSTGRES_CHANGES_LISTEN_EVENT) {
|
|
2350
|
+
REALTIME_POSTGRES_CHANGES_LISTEN_EVENT["ALL"] = "*";
|
|
2351
|
+
REALTIME_POSTGRES_CHANGES_LISTEN_EVENT["INSERT"] = "INSERT";
|
|
2352
|
+
REALTIME_POSTGRES_CHANGES_LISTEN_EVENT["UPDATE"] = "UPDATE";
|
|
2353
|
+
REALTIME_POSTGRES_CHANGES_LISTEN_EVENT["DELETE"] = "DELETE";
|
|
2354
|
+
})(REALTIME_POSTGRES_CHANGES_LISTEN_EVENT || (REALTIME_POSTGRES_CHANGES_LISTEN_EVENT = {}));
|
|
2355
|
+
var REALTIME_LISTEN_TYPES;
|
|
2356
|
+
(function(REALTIME_LISTEN_TYPES) {
|
|
2357
|
+
REALTIME_LISTEN_TYPES["BROADCAST"] = "broadcast";
|
|
2358
|
+
REALTIME_LISTEN_TYPES["PRESENCE"] = "presence";
|
|
2359
|
+
REALTIME_LISTEN_TYPES["POSTGRES_CHANGES"] = "postgres_changes";
|
|
2360
|
+
REALTIME_LISTEN_TYPES["SYSTEM"] = "system";
|
|
2361
|
+
})(REALTIME_LISTEN_TYPES || (REALTIME_LISTEN_TYPES = {}));
|
|
2362
|
+
var REALTIME_SUBSCRIBE_STATES;
|
|
2363
|
+
(function(REALTIME_SUBSCRIBE_STATES) {
|
|
2364
|
+
REALTIME_SUBSCRIBE_STATES["SUBSCRIBED"] = "SUBSCRIBED";
|
|
2365
|
+
REALTIME_SUBSCRIBE_STATES["TIMED_OUT"] = "TIMED_OUT";
|
|
2366
|
+
REALTIME_SUBSCRIBE_STATES["CLOSED"] = "CLOSED";
|
|
2367
|
+
REALTIME_SUBSCRIBE_STATES["CHANNEL_ERROR"] = "CHANNEL_ERROR";
|
|
2368
|
+
})(REALTIME_SUBSCRIBE_STATES || (REALTIME_SUBSCRIBE_STATES = {}));
|
|
2369
|
+
/** A channel is the basic building block of Realtime
|
|
2370
|
+
* and narrows the scope of data flow to subscribed clients.
|
|
2371
|
+
* You can think of a channel as a chatroom where participants are able to see who's online
|
|
2372
|
+
* and send and receive messages.
|
|
2373
|
+
*/
|
|
2374
|
+
var RealtimeChannel = class RealtimeChannel {
|
|
2375
|
+
get state() {
|
|
2376
|
+
return this.channelAdapter.state;
|
|
2377
|
+
}
|
|
2378
|
+
set state(state) {
|
|
2379
|
+
this.channelAdapter.state = state;
|
|
2380
|
+
}
|
|
2381
|
+
get joinedOnce() {
|
|
2382
|
+
return this.channelAdapter.joinedOnce;
|
|
2383
|
+
}
|
|
2384
|
+
get timeout() {
|
|
2385
|
+
return this.socket.timeout;
|
|
2386
|
+
}
|
|
2387
|
+
get joinPush() {
|
|
2388
|
+
return this.channelAdapter.joinPush;
|
|
2389
|
+
}
|
|
2390
|
+
get rejoinTimer() {
|
|
2391
|
+
return this.channelAdapter.rejoinTimer;
|
|
2392
|
+
}
|
|
2393
|
+
/**
|
|
2394
|
+
* Creates a channel that can broadcast messages, sync presence, and listen to Postgres changes.
|
|
2395
|
+
*
|
|
2396
|
+
* The topic determines which realtime stream you are subscribing to. Config options let you
|
|
2397
|
+
* enable acknowledgement for broadcasts, presence tracking, or private channels.
|
|
2398
|
+
*
|
|
2399
|
+
* @category Realtime
|
|
2400
|
+
*
|
|
2401
|
+
* @example Example for a public channel
|
|
2402
|
+
* ```ts
|
|
2403
|
+
* import RealtimeClient from '@supabase/realtime-js'
|
|
2404
|
+
*
|
|
2405
|
+
* const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
|
|
2406
|
+
* params: { apikey: 'public-anon-key' },
|
|
2407
|
+
* })
|
|
2408
|
+
* const channel = new RealtimeChannel('realtime:public:messages', { config: {} }, client)
|
|
2409
|
+
* ```
|
|
2410
|
+
*/
|
|
2411
|
+
constructor(topic, params = { config: {} }, socket) {
|
|
2412
|
+
var _a, _b;
|
|
2413
|
+
this.topic = topic;
|
|
2414
|
+
this.params = params;
|
|
2415
|
+
this.socket = socket;
|
|
2416
|
+
this.bindings = {};
|
|
2417
|
+
this.subTopic = topic.replace(/^realtime:/i, "");
|
|
2418
|
+
this.params.config = Object.assign({
|
|
2419
|
+
broadcast: {
|
|
2420
|
+
ack: false,
|
|
2421
|
+
self: false
|
|
2422
|
+
},
|
|
2423
|
+
presence: {
|
|
2424
|
+
key: "",
|
|
2425
|
+
enabled: false
|
|
2426
|
+
},
|
|
2427
|
+
private: false
|
|
2428
|
+
}, params.config);
|
|
2429
|
+
this.channelAdapter = new ChannelAdapter(this.socket.socketAdapter, topic, this.params);
|
|
2430
|
+
this.presence = new RealtimePresence(this);
|
|
2431
|
+
this._onClose(() => {
|
|
2432
|
+
this.socket._remove(this);
|
|
2433
|
+
});
|
|
2434
|
+
this._updateFilterTransform();
|
|
2435
|
+
this.broadcastEndpointURL = httpEndpointURL(this.socket.socketAdapter.endPointURL());
|
|
2436
|
+
this.private = this.params.config.private || false;
|
|
2437
|
+
if (!this.private && ((_b = (_a = this.params.config) === null || _a === void 0 ? void 0 : _a.broadcast) === null || _b === void 0 ? void 0 : _b.replay)) throw `tried to use replay on public channel '${this.topic}'. It must be a private channel.`;
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* Subscribe registers your client with the server
|
|
2441
|
+
* @category Realtime
|
|
2442
|
+
*/
|
|
2443
|
+
subscribe(callback, timeout = this.timeout) {
|
|
2444
|
+
var _a, _b, _c;
|
|
2445
|
+
if (!this.socket.isConnected()) this.socket.connect();
|
|
2446
|
+
if (this.channelAdapter.isClosed()) {
|
|
2447
|
+
const { config: { broadcast, presence, private: isPrivate } } = this.params;
|
|
2448
|
+
const postgres_changes = (_b = (_a = this.bindings.postgres_changes) === null || _a === void 0 ? void 0 : _a.map((r) => r.filter)) !== null && _b !== void 0 ? _b : [];
|
|
2449
|
+
const presence_enabled = !!this.bindings[REALTIME_LISTEN_TYPES.PRESENCE] && this.bindings[REALTIME_LISTEN_TYPES.PRESENCE].length > 0 || ((_c = this.params.config.presence) === null || _c === void 0 ? void 0 : _c.enabled) === true;
|
|
2450
|
+
const accessTokenPayload = {};
|
|
2451
|
+
const config = {
|
|
2452
|
+
broadcast,
|
|
2453
|
+
presence: Object.assign(Object.assign({}, presence), { enabled: presence_enabled }),
|
|
2454
|
+
postgres_changes,
|
|
2455
|
+
private: isPrivate
|
|
2456
|
+
};
|
|
2457
|
+
if (this.socket.accessTokenValue) accessTokenPayload.access_token = this.socket.accessTokenValue;
|
|
2458
|
+
this._onError((reason) => {
|
|
2459
|
+
callback === null || callback === void 0 || callback(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, reason);
|
|
2460
|
+
});
|
|
2461
|
+
this._onClose(() => callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.CLOSED));
|
|
2462
|
+
this.updateJoinPayload(Object.assign({ config }, accessTokenPayload));
|
|
2463
|
+
this._updateFilterMessage();
|
|
2464
|
+
this.channelAdapter.subscribe(timeout).receive("ok", async ({ postgres_changes }) => {
|
|
2465
|
+
if (!this.socket._isManualToken()) this.socket.setAuth();
|
|
2466
|
+
if (postgres_changes === void 0) {
|
|
2467
|
+
callback === null || callback === void 0 || callback(REALTIME_SUBSCRIBE_STATES.SUBSCRIBED);
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
this._updatePostgresBindings(postgres_changes, callback);
|
|
2471
|
+
}).receive("error", (error) => {
|
|
2472
|
+
this.state = CHANNEL_STATES$1.errored;
|
|
2473
|
+
callback === null || callback === void 0 || callback(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, new Error(JSON.stringify(Object.values(error).join(", ") || "error")));
|
|
2474
|
+
}).receive("timeout", () => {
|
|
2475
|
+
callback === null || callback === void 0 || callback(REALTIME_SUBSCRIBE_STATES.TIMED_OUT);
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
return this;
|
|
2479
|
+
}
|
|
2480
|
+
_updatePostgresBindings(postgres_changes, callback) {
|
|
2481
|
+
var _a;
|
|
2482
|
+
const clientPostgresBindings = this.bindings.postgres_changes;
|
|
2483
|
+
const bindingsLen = (_a = clientPostgresBindings === null || clientPostgresBindings === void 0 ? void 0 : clientPostgresBindings.length) !== null && _a !== void 0 ? _a : 0;
|
|
2484
|
+
const newPostgresBindings = [];
|
|
2485
|
+
for (let i = 0; i < bindingsLen; i++) {
|
|
2486
|
+
const clientPostgresBinding = clientPostgresBindings[i];
|
|
2487
|
+
const { filter: { event, schema, table, filter } } = clientPostgresBinding;
|
|
2488
|
+
const serverPostgresFilter = postgres_changes && postgres_changes[i];
|
|
2489
|
+
if (serverPostgresFilter && serverPostgresFilter.event === event && RealtimeChannel.isFilterValueEqual(serverPostgresFilter.schema, schema) && RealtimeChannel.isFilterValueEqual(serverPostgresFilter.table, table) && RealtimeChannel.isFilterValueEqual(serverPostgresFilter.filter, filter)) newPostgresBindings.push(Object.assign(Object.assign({}, clientPostgresBinding), { id: serverPostgresFilter.id }));
|
|
2490
|
+
else {
|
|
2491
|
+
this.unsubscribe();
|
|
2492
|
+
this.state = CHANNEL_STATES$1.errored;
|
|
2493
|
+
callback === null || callback === void 0 || callback(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, /* @__PURE__ */ new Error("mismatch between server and client bindings for postgres changes"));
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
this.bindings.postgres_changes = newPostgresBindings;
|
|
2498
|
+
if (this.state != CHANNEL_STATES$1.errored && callback) callback(REALTIME_SUBSCRIBE_STATES.SUBSCRIBED);
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* Returns the current presence state for this channel.
|
|
2502
|
+
*
|
|
2503
|
+
* The shape is a map keyed by presence key (for example a user id) where each entry contains the
|
|
2504
|
+
* tracked metadata for that user.
|
|
2505
|
+
*
|
|
2506
|
+
* @category Realtime
|
|
2507
|
+
*/
|
|
2508
|
+
presenceState() {
|
|
2509
|
+
return this.presence.state;
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Sends the supplied payload to the presence tracker so other subscribers can see that this
|
|
2513
|
+
* client is online. Use `untrack` to stop broadcasting presence for the same key.
|
|
2514
|
+
*
|
|
2515
|
+
* @category Realtime
|
|
2516
|
+
*/
|
|
2517
|
+
async track(payload, opts = {}) {
|
|
2518
|
+
return await this.send({
|
|
2519
|
+
type: "presence",
|
|
2520
|
+
event: "track",
|
|
2521
|
+
payload
|
|
2522
|
+
}, opts.timeout || this.timeout);
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* Removes the current presence state for this client.
|
|
2526
|
+
*
|
|
2527
|
+
* @category Realtime
|
|
2528
|
+
*/
|
|
2529
|
+
async untrack(opts = {}) {
|
|
2530
|
+
return await this.send({
|
|
2531
|
+
type: "presence",
|
|
2532
|
+
event: "untrack"
|
|
2533
|
+
}, opts);
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Listen to realtime events on this channel.
|
|
2537
|
+
* @category Realtime
|
|
2538
|
+
*
|
|
2539
|
+
* @remarks
|
|
2540
|
+
* - By default, Broadcast and Presence are enabled for all projects.
|
|
2541
|
+
* - By default, listening to database changes is disabled for new projects due to database performance and security concerns. You can turn it on by managing Realtime's [replication](/docs/guides/api#realtime-api-overview).
|
|
2542
|
+
* - You can receive the "previous" data for updates and deletes by setting the table's `REPLICA IDENTITY` to `FULL` (e.g., `ALTER TABLE your_table REPLICA IDENTITY FULL;`).
|
|
2543
|
+
* - Row level security is not applied to delete statements. When RLS is enabled and replica identity is set to full, only the primary key is sent to clients.
|
|
2544
|
+
*
|
|
2545
|
+
* @example Listen to broadcast messages
|
|
2546
|
+
* ```js
|
|
2547
|
+
* const channel = supabase.channel("room1")
|
|
2548
|
+
*
|
|
2549
|
+
* channel.on("broadcast", { event: "cursor-pos" }, (payload) => {
|
|
2550
|
+
* console.log("Cursor position received!", payload);
|
|
2551
|
+
* }).subscribe((status) => {
|
|
2552
|
+
* if (status === "SUBSCRIBED") {
|
|
2553
|
+
* channel.send({
|
|
2554
|
+
* type: "broadcast",
|
|
2555
|
+
* event: "cursor-pos",
|
|
2556
|
+
* payload: { x: Math.random(), y: Math.random() },
|
|
2557
|
+
* });
|
|
2558
|
+
* }
|
|
2559
|
+
* });
|
|
2560
|
+
* ```
|
|
2561
|
+
*
|
|
2562
|
+
* @example Listen to presence sync
|
|
2563
|
+
* ```js
|
|
2564
|
+
* const channel = supabase.channel('room1')
|
|
2565
|
+
* channel
|
|
2566
|
+
* .on('presence', { event: 'sync' }, () => {
|
|
2567
|
+
* console.log('Synced presence state: ', channel.presenceState())
|
|
2568
|
+
* })
|
|
2569
|
+
* .subscribe(async (status) => {
|
|
2570
|
+
* if (status === 'SUBSCRIBED') {
|
|
2571
|
+
* await channel.track({ online_at: new Date().toISOString() })
|
|
2572
|
+
* }
|
|
2573
|
+
* })
|
|
2574
|
+
* ```
|
|
2575
|
+
*
|
|
2576
|
+
* @example Listen to presence join
|
|
2577
|
+
* ```js
|
|
2578
|
+
* const channel = supabase.channel('room1')
|
|
2579
|
+
* channel
|
|
2580
|
+
* .on('presence', { event: 'join' }, ({ newPresences }) => {
|
|
2581
|
+
* console.log('Newly joined presences: ', newPresences)
|
|
2582
|
+
* })
|
|
2583
|
+
* .subscribe(async (status) => {
|
|
2584
|
+
* if (status === 'SUBSCRIBED') {
|
|
2585
|
+
* await channel.track({ online_at: new Date().toISOString() })
|
|
2586
|
+
* }
|
|
2587
|
+
* })
|
|
2588
|
+
* ```
|
|
2589
|
+
*
|
|
2590
|
+
* @example Listen to presence leave
|
|
2591
|
+
* ```js
|
|
2592
|
+
* const channel = supabase.channel('room1')
|
|
2593
|
+
* channel
|
|
2594
|
+
* .on('presence', { event: 'leave' }, ({ leftPresences }) => {
|
|
2595
|
+
* console.log('Newly left presences: ', leftPresences)
|
|
2596
|
+
* })
|
|
2597
|
+
* .subscribe(async (status) => {
|
|
2598
|
+
* if (status === 'SUBSCRIBED') {
|
|
2599
|
+
* await channel.track({ online_at: new Date().toISOString() })
|
|
2600
|
+
* await channel.untrack()
|
|
2601
|
+
* }
|
|
2602
|
+
* })
|
|
2603
|
+
* ```
|
|
2604
|
+
*
|
|
2605
|
+
* @example Listen to all database changes
|
|
2606
|
+
* ```js
|
|
2607
|
+
* supabase
|
|
2608
|
+
* .channel('room1')
|
|
2609
|
+
* .on('postgres_changes', { event: '*', schema: '*' }, payload => {
|
|
2610
|
+
* console.log('Change received!', payload)
|
|
2611
|
+
* })
|
|
2612
|
+
* .subscribe()
|
|
2613
|
+
* ```
|
|
2614
|
+
*
|
|
2615
|
+
* @example Listen to a specific table
|
|
2616
|
+
* ```js
|
|
2617
|
+
* supabase
|
|
2618
|
+
* .channel('room1')
|
|
2619
|
+
* .on('postgres_changes', { event: '*', schema: 'public', table: 'countries' }, payload => {
|
|
2620
|
+
* console.log('Change received!', payload)
|
|
2621
|
+
* })
|
|
2622
|
+
* .subscribe()
|
|
2623
|
+
* ```
|
|
2624
|
+
*
|
|
2625
|
+
* @example Listen to inserts
|
|
2626
|
+
* ```js
|
|
2627
|
+
* supabase
|
|
2628
|
+
* .channel('room1')
|
|
2629
|
+
* .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'countries' }, payload => {
|
|
2630
|
+
* console.log('Change received!', payload)
|
|
2631
|
+
* })
|
|
2632
|
+
* .subscribe()
|
|
2633
|
+
* ```
|
|
2634
|
+
*
|
|
2635
|
+
* @exampleDescription Listen to updates
|
|
2636
|
+
* By default, Supabase will send only the updated record. If you want to receive the previous values as well you can
|
|
2637
|
+
* enable full replication for the table you are listening to:
|
|
2638
|
+
*
|
|
2639
|
+
* ```sql
|
|
2640
|
+
* alter table "your_table" replica identity full;
|
|
2641
|
+
* ```
|
|
2642
|
+
*
|
|
2643
|
+
* @example Listen to updates
|
|
2644
|
+
* ```js
|
|
2645
|
+
* supabase
|
|
2646
|
+
* .channel('room1')
|
|
2647
|
+
* .on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'countries' }, payload => {
|
|
2648
|
+
* console.log('Change received!', payload)
|
|
2649
|
+
* })
|
|
2650
|
+
* .subscribe()
|
|
2651
|
+
* ```
|
|
2652
|
+
*
|
|
2653
|
+
* @exampleDescription Listen to deletes
|
|
2654
|
+
* By default, Supabase does not send deleted records. If you want to receive the deleted record you can
|
|
2655
|
+
* enable full replication for the table you are listening to:
|
|
2656
|
+
*
|
|
2657
|
+
* ```sql
|
|
2658
|
+
* alter table "your_table" replica identity full;
|
|
2659
|
+
* ```
|
|
2660
|
+
*
|
|
2661
|
+
* @example Listen to deletes
|
|
2662
|
+
* ```js
|
|
2663
|
+
* supabase
|
|
2664
|
+
* .channel('room1')
|
|
2665
|
+
* .on('postgres_changes', { event: 'DELETE', schema: 'public', table: 'countries' }, payload => {
|
|
2666
|
+
* console.log('Change received!', payload)
|
|
2667
|
+
* })
|
|
2668
|
+
* .subscribe()
|
|
2669
|
+
* ```
|
|
2670
|
+
*
|
|
2671
|
+
* @exampleDescription Listen to multiple events
|
|
2672
|
+
* You can chain listeners if you want to listen to multiple events for each table.
|
|
2673
|
+
*
|
|
2674
|
+
* @example Listen to multiple events
|
|
2675
|
+
* ```js
|
|
2676
|
+
* supabase
|
|
2677
|
+
* .channel('room1')
|
|
2678
|
+
* .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'countries' }, handleRecordInserted)
|
|
2679
|
+
* .on('postgres_changes', { event: 'DELETE', schema: 'public', table: 'countries' }, handleRecordDeleted)
|
|
2680
|
+
* .subscribe()
|
|
2681
|
+
* ```
|
|
2682
|
+
*
|
|
2683
|
+
* @exampleDescription Listen to row level changes
|
|
2684
|
+
* You can listen to individual rows using the format `{table}:{col}=eq.{val}` - where `{col}` is the column name, and `{val}` is the value which you want to match.
|
|
2685
|
+
*
|
|
2686
|
+
* @example Listen to row level changes
|
|
2687
|
+
* ```js
|
|
2688
|
+
* supabase
|
|
2689
|
+
* .channel('room1')
|
|
2690
|
+
* .on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'countries', filter: 'id=eq.200' }, handleRecordUpdated)
|
|
2691
|
+
* .subscribe()
|
|
2692
|
+
* ```
|
|
2693
|
+
*/
|
|
2694
|
+
on(type, filter, callback) {
|
|
2695
|
+
if (this.channelAdapter.isJoined() && type === REALTIME_LISTEN_TYPES.PRESENCE) {
|
|
2696
|
+
this.socket.log("channel", `cannot add presence callbacks for ${this.topic} after joining.`);
|
|
2697
|
+
throw new Error("cannot add presence callbacks after joining a channel");
|
|
2698
|
+
}
|
|
2699
|
+
return this._on(type, filter, callback);
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Sends a broadcast message explicitly via REST API.
|
|
2703
|
+
*
|
|
2704
|
+
* This method always uses the REST API endpoint regardless of WebSocket connection state.
|
|
2705
|
+
* Useful when you want to guarantee REST delivery or when gradually migrating from implicit REST fallback.
|
|
2706
|
+
*
|
|
2707
|
+
* @param event The name of the broadcast event
|
|
2708
|
+
* @param payload Payload to be sent (required)
|
|
2709
|
+
* @param opts Options including timeout
|
|
2710
|
+
* @returns Promise resolving to object with success status, and error details if failed
|
|
2711
|
+
*
|
|
2712
|
+
* @category Realtime
|
|
2713
|
+
*/
|
|
2714
|
+
async httpSend(event, payload, opts = {}) {
|
|
2715
|
+
var _a;
|
|
2716
|
+
if (payload === void 0 || payload === null) return Promise.reject("Payload is required for httpSend()");
|
|
2717
|
+
const headers = {
|
|
2718
|
+
apikey: this.socket.apiKey ? this.socket.apiKey : "",
|
|
2719
|
+
"Content-Type": "application/json"
|
|
2720
|
+
};
|
|
2721
|
+
if (this.socket.accessTokenValue) headers["Authorization"] = `Bearer ${this.socket.accessTokenValue}`;
|
|
2722
|
+
const options = {
|
|
2723
|
+
method: "POST",
|
|
2724
|
+
headers,
|
|
2725
|
+
body: JSON.stringify({ messages: [{
|
|
2726
|
+
topic: this.subTopic,
|
|
2727
|
+
event,
|
|
2728
|
+
payload,
|
|
2729
|
+
private: this.private
|
|
2730
|
+
}] })
|
|
2731
|
+
};
|
|
2732
|
+
const response = await this._fetchWithTimeout(this.broadcastEndpointURL, options, (_a = opts.timeout) !== null && _a !== void 0 ? _a : this.timeout);
|
|
2733
|
+
if (response.status === 202) return { success: true };
|
|
2734
|
+
let errorMessage = response.statusText;
|
|
2735
|
+
try {
|
|
2736
|
+
const errorBody = await response.json();
|
|
2737
|
+
errorMessage = errorBody.error || errorBody.message || errorMessage;
|
|
2738
|
+
} catch (_b) {}
|
|
2739
|
+
return Promise.reject(new Error(errorMessage));
|
|
2740
|
+
}
|
|
2741
|
+
/**
|
|
2742
|
+
* Sends a message into the channel.
|
|
2743
|
+
*
|
|
2744
|
+
* @param args Arguments to send to channel
|
|
2745
|
+
* @param args.type The type of event to send
|
|
2746
|
+
* @param args.event The name of the event being sent
|
|
2747
|
+
* @param args.payload Payload to be sent
|
|
2748
|
+
* @param opts Options to be used during the send process
|
|
2749
|
+
*
|
|
2750
|
+
* @category Realtime
|
|
2751
|
+
*
|
|
2752
|
+
* @remarks
|
|
2753
|
+
* - When using REST you don't need to subscribe to the channel
|
|
2754
|
+
* - REST calls are only available from 2.37.0 onwards
|
|
2755
|
+
*
|
|
2756
|
+
* @example Send a message via websocket
|
|
2757
|
+
* ```js
|
|
2758
|
+
* const channel = supabase.channel('room1')
|
|
2759
|
+
*
|
|
2760
|
+
* channel.subscribe((status) => {
|
|
2761
|
+
* if (status === 'SUBSCRIBED') {
|
|
2762
|
+
* channel.send({
|
|
2763
|
+
* type: 'broadcast',
|
|
2764
|
+
* event: 'cursor-pos',
|
|
2765
|
+
* payload: { x: Math.random(), y: Math.random() },
|
|
2766
|
+
* })
|
|
2767
|
+
* }
|
|
2768
|
+
* })
|
|
2769
|
+
* ```
|
|
2770
|
+
*
|
|
2771
|
+
* @exampleResponse Send a message via websocket
|
|
2772
|
+
* ```js
|
|
2773
|
+
* ok | timed out | error
|
|
2774
|
+
* ```
|
|
2775
|
+
*
|
|
2776
|
+
* @example Send a message via REST
|
|
2777
|
+
* ```js
|
|
2778
|
+
* supabase
|
|
2779
|
+
* .channel('room1')
|
|
2780
|
+
* .httpSend('cursor-pos', { x: Math.random(), y: Math.random() })
|
|
2781
|
+
* ```
|
|
2782
|
+
*/
|
|
2783
|
+
async send(args, opts = {}) {
|
|
2784
|
+
var _a, _b;
|
|
2785
|
+
if (!this.channelAdapter.canPush() && args.type === "broadcast") {
|
|
2786
|
+
console.warn("Realtime send() is automatically falling back to REST API. This behavior will be deprecated in the future. Please use httpSend() explicitly for REST delivery.");
|
|
2787
|
+
const { event, payload: endpoint_payload } = args;
|
|
2788
|
+
const headers = {
|
|
2789
|
+
apikey: this.socket.apiKey ? this.socket.apiKey : "",
|
|
2790
|
+
"Content-Type": "application/json"
|
|
2791
|
+
};
|
|
2792
|
+
if (this.socket.accessTokenValue) headers["Authorization"] = `Bearer ${this.socket.accessTokenValue}`;
|
|
2793
|
+
const options = {
|
|
2794
|
+
method: "POST",
|
|
2795
|
+
headers,
|
|
2796
|
+
body: JSON.stringify({ messages: [{
|
|
2797
|
+
topic: this.subTopic,
|
|
2798
|
+
event,
|
|
2799
|
+
payload: endpoint_payload,
|
|
2800
|
+
private: this.private
|
|
2801
|
+
}] })
|
|
2802
|
+
};
|
|
2803
|
+
try {
|
|
2804
|
+
const response = await this._fetchWithTimeout(this.broadcastEndpointURL, options, (_a = opts.timeout) !== null && _a !== void 0 ? _a : this.timeout);
|
|
2805
|
+
await ((_b = response.body) === null || _b === void 0 ? void 0 : _b.cancel());
|
|
2806
|
+
return response.ok ? "ok" : "error";
|
|
2807
|
+
} catch (error) {
|
|
2808
|
+
if (error.name === "AbortError") return "timed out";
|
|
2809
|
+
else return "error";
|
|
2810
|
+
}
|
|
2811
|
+
} else return new Promise((resolve) => {
|
|
2812
|
+
var _a, _b, _c;
|
|
2813
|
+
const push = this.channelAdapter.push(args.type, args, opts.timeout || this.timeout);
|
|
2814
|
+
if (args.type === "broadcast" && !((_c = (_b = (_a = this.params) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.broadcast) === null || _c === void 0 ? void 0 : _c.ack)) resolve("ok");
|
|
2815
|
+
push.receive("ok", () => resolve("ok"));
|
|
2816
|
+
push.receive("error", () => resolve("error"));
|
|
2817
|
+
push.receive("timeout", () => resolve("timed out"));
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
/**
|
|
2821
|
+
* Updates the payload that will be sent the next time the channel joins (reconnects).
|
|
2822
|
+
* Useful for rotating access tokens or updating config without re-creating the channel.
|
|
2823
|
+
*
|
|
2824
|
+
* @category Realtime
|
|
2825
|
+
*/
|
|
2826
|
+
updateJoinPayload(payload) {
|
|
2827
|
+
this.channelAdapter.updateJoinPayload(payload);
|
|
2828
|
+
}
|
|
2829
|
+
/**
|
|
2830
|
+
* Leaves the channel.
|
|
2831
|
+
*
|
|
2832
|
+
* Unsubscribes from server events, and instructs channel to terminate on server.
|
|
2833
|
+
* Triggers onClose() hooks.
|
|
2834
|
+
*
|
|
2835
|
+
* To receive leave acknowledgements, use the a `receive` hook to bind to the server ack, ie:
|
|
2836
|
+
* channel.unsubscribe().receive("ok", () => alert("left!") )
|
|
2837
|
+
*
|
|
2838
|
+
* @category Realtime
|
|
2839
|
+
*/
|
|
2840
|
+
async unsubscribe(timeout = this.timeout) {
|
|
2841
|
+
return new Promise((resolve) => {
|
|
2842
|
+
this.channelAdapter.unsubscribe(timeout).receive("ok", () => resolve("ok")).receive("timeout", () => resolve("timed out")).receive("error", () => resolve("error"));
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Destroys and stops related timers.
|
|
2847
|
+
*
|
|
2848
|
+
* @category Realtime
|
|
2849
|
+
*/
|
|
2850
|
+
teardown() {
|
|
2851
|
+
this.channelAdapter.teardown();
|
|
2852
|
+
}
|
|
2853
|
+
/** @internal */
|
|
2854
|
+
async _fetchWithTimeout(url, options, timeout) {
|
|
2855
|
+
const controller = new AbortController();
|
|
2856
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
2857
|
+
const response = await this.socket.fetch(url, Object.assign(Object.assign({}, options), { signal: controller.signal }));
|
|
2858
|
+
clearTimeout(id);
|
|
2859
|
+
return response;
|
|
2860
|
+
}
|
|
2861
|
+
/** @internal */
|
|
2862
|
+
_on(type, filter, callback) {
|
|
2863
|
+
const typeLower = type.toLocaleLowerCase();
|
|
2864
|
+
const binding = {
|
|
2865
|
+
type: typeLower,
|
|
2866
|
+
filter,
|
|
2867
|
+
callback,
|
|
2868
|
+
ref: this.channelAdapter.on(type, callback)
|
|
2869
|
+
};
|
|
2870
|
+
if (this.bindings[typeLower]) this.bindings[typeLower].push(binding);
|
|
2871
|
+
else this.bindings[typeLower] = [binding];
|
|
2872
|
+
this._updateFilterMessage();
|
|
2873
|
+
return this;
|
|
2874
|
+
}
|
|
2875
|
+
/**
|
|
2876
|
+
* Registers a callback that will be executed when the channel closes.
|
|
2877
|
+
*
|
|
2878
|
+
* @internal
|
|
2879
|
+
*/
|
|
2880
|
+
_onClose(callback) {
|
|
2881
|
+
this.channelAdapter.onClose(callback);
|
|
2882
|
+
}
|
|
2883
|
+
/**
|
|
2884
|
+
* Registers a callback that will be executed when the channel encounteres an error.
|
|
2885
|
+
*
|
|
2886
|
+
* @internal
|
|
2887
|
+
*/
|
|
2888
|
+
_onError(callback) {
|
|
2889
|
+
this.channelAdapter.onError(callback);
|
|
2890
|
+
}
|
|
2891
|
+
/** @internal */
|
|
2892
|
+
_updateFilterMessage() {
|
|
2893
|
+
this.channelAdapter.updateFilterBindings((binding, payload, ref) => {
|
|
2894
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
2895
|
+
const typeLower = binding.event.toLocaleLowerCase();
|
|
2896
|
+
if (this._notThisChannelEvent(typeLower, ref)) return false;
|
|
2897
|
+
const bind = (_a = this.bindings[typeLower]) === null || _a === void 0 ? void 0 : _a.find((bind) => bind.ref === binding.ref);
|
|
2898
|
+
if (!bind) return true;
|
|
2899
|
+
if ([
|
|
2900
|
+
"broadcast",
|
|
2901
|
+
"presence",
|
|
2902
|
+
"postgres_changes"
|
|
2903
|
+
].includes(typeLower)) if ("id" in bind) {
|
|
2904
|
+
const bindId = bind.id;
|
|
2905
|
+
const bindEvent = (_b = bind.filter) === null || _b === void 0 ? void 0 : _b.event;
|
|
2906
|
+
return bindId && ((_c = payload.ids) === null || _c === void 0 ? void 0 : _c.includes(bindId)) && (bindEvent === "*" || (bindEvent === null || bindEvent === void 0 ? void 0 : bindEvent.toLocaleLowerCase()) === ((_d = payload.data) === null || _d === void 0 ? void 0 : _d.type.toLocaleLowerCase()));
|
|
2907
|
+
} else {
|
|
2908
|
+
const bindEvent = (_f = (_e = bind === null || bind === void 0 ? void 0 : bind.filter) === null || _e === void 0 ? void 0 : _e.event) === null || _f === void 0 ? void 0 : _f.toLocaleLowerCase();
|
|
2909
|
+
return bindEvent === "*" || bindEvent === ((_g = payload === null || payload === void 0 ? void 0 : payload.event) === null || _g === void 0 ? void 0 : _g.toLocaleLowerCase());
|
|
2910
|
+
}
|
|
2911
|
+
else return bind.type.toLocaleLowerCase() === typeLower;
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
/** @internal */
|
|
2915
|
+
_notThisChannelEvent(event, ref) {
|
|
2916
|
+
const { close, error, leave, join } = CHANNEL_EVENTS$1;
|
|
2917
|
+
return ref && [
|
|
2918
|
+
close,
|
|
2919
|
+
error,
|
|
2920
|
+
leave,
|
|
2921
|
+
join
|
|
2922
|
+
].includes(event) && ref !== this.joinPush.ref;
|
|
2923
|
+
}
|
|
2924
|
+
/** @internal */
|
|
2925
|
+
_updateFilterTransform() {
|
|
2926
|
+
this.channelAdapter.updatePayloadTransform((event, payload, ref) => {
|
|
2927
|
+
if (typeof payload === "object" && "ids" in payload) {
|
|
2928
|
+
const postgresChanges = payload.data;
|
|
2929
|
+
const { schema, table, commit_timestamp, type, errors } = postgresChanges;
|
|
2930
|
+
return Object.assign(Object.assign({}, {
|
|
2931
|
+
schema,
|
|
2932
|
+
table,
|
|
2933
|
+
commit_timestamp,
|
|
2934
|
+
eventType: type,
|
|
2935
|
+
new: {},
|
|
2936
|
+
old: {},
|
|
2937
|
+
errors
|
|
2938
|
+
}), this._getPayloadRecords(postgresChanges));
|
|
2939
|
+
}
|
|
2940
|
+
return payload;
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
/**
|
|
2944
|
+
* Compares two optional filter values for equality.
|
|
2945
|
+
* Treats undefined, null, and empty string as equivalent empty values.
|
|
2946
|
+
* @internal
|
|
2947
|
+
*/
|
|
2948
|
+
static isFilterValueEqual(serverValue, clientValue) {
|
|
2949
|
+
return (serverValue !== null && serverValue !== void 0 ? serverValue : void 0) === (clientValue !== null && clientValue !== void 0 ? clientValue : void 0);
|
|
2950
|
+
}
|
|
2951
|
+
/** @internal */
|
|
2952
|
+
_getPayloadRecords(payload) {
|
|
2953
|
+
const records = {
|
|
2954
|
+
new: {},
|
|
2955
|
+
old: {}
|
|
2956
|
+
};
|
|
2957
|
+
if (payload.type === "INSERT" || payload.type === "UPDATE") records.new = convertChangeData(payload.columns, payload.record);
|
|
2958
|
+
if (payload.type === "UPDATE" || payload.type === "DELETE") records.old = convertChangeData(payload.columns, payload.old_record);
|
|
2959
|
+
return records;
|
|
2960
|
+
}
|
|
2961
|
+
};
|
|
2962
|
+
//#endregion
|
|
2963
|
+
//#region ../../node_modules/.pnpm/@supabase+realtime-js@2.100.1/node_modules/@supabase/realtime-js/dist/module/phoenix/socketAdapter.js
|
|
2964
|
+
var SocketAdapter = class {
|
|
2965
|
+
constructor(endPoint, options) {
|
|
2966
|
+
this.socket = new Socket(endPoint, options);
|
|
2967
|
+
}
|
|
2968
|
+
get timeout() {
|
|
2969
|
+
return this.socket.timeout;
|
|
2970
|
+
}
|
|
2971
|
+
get endPoint() {
|
|
2972
|
+
return this.socket.endPoint;
|
|
2973
|
+
}
|
|
2974
|
+
get transport() {
|
|
2975
|
+
return this.socket.transport;
|
|
2976
|
+
}
|
|
2977
|
+
get heartbeatIntervalMs() {
|
|
2978
|
+
return this.socket.heartbeatIntervalMs;
|
|
2979
|
+
}
|
|
2980
|
+
get heartbeatCallback() {
|
|
2981
|
+
return this.socket.heartbeatCallback;
|
|
2982
|
+
}
|
|
2983
|
+
set heartbeatCallback(callback) {
|
|
2984
|
+
this.socket.heartbeatCallback = callback;
|
|
2985
|
+
}
|
|
2986
|
+
get heartbeatTimer() {
|
|
2987
|
+
return this.socket.heartbeatTimer;
|
|
2988
|
+
}
|
|
2989
|
+
get pendingHeartbeatRef() {
|
|
2990
|
+
return this.socket.pendingHeartbeatRef;
|
|
2991
|
+
}
|
|
2992
|
+
get reconnectTimer() {
|
|
2993
|
+
return this.socket.reconnectTimer;
|
|
2994
|
+
}
|
|
2995
|
+
get vsn() {
|
|
2996
|
+
return this.socket.vsn;
|
|
2997
|
+
}
|
|
2998
|
+
get encode() {
|
|
2999
|
+
return this.socket.encode;
|
|
3000
|
+
}
|
|
3001
|
+
get decode() {
|
|
3002
|
+
return this.socket.decode;
|
|
3003
|
+
}
|
|
3004
|
+
get reconnectAfterMs() {
|
|
3005
|
+
return this.socket.reconnectAfterMs;
|
|
3006
|
+
}
|
|
3007
|
+
get sendBuffer() {
|
|
3008
|
+
return this.socket.sendBuffer;
|
|
3009
|
+
}
|
|
3010
|
+
get stateChangeCallbacks() {
|
|
3011
|
+
return this.socket.stateChangeCallbacks;
|
|
3012
|
+
}
|
|
3013
|
+
connect() {
|
|
3014
|
+
this.socket.connect();
|
|
3015
|
+
}
|
|
3016
|
+
disconnect(callback, code, reason, timeout = 1e4) {
|
|
3017
|
+
return new Promise((resolve) => {
|
|
3018
|
+
setTimeout(() => resolve("timeout"), timeout);
|
|
3019
|
+
this.socket.disconnect(() => {
|
|
3020
|
+
callback();
|
|
3021
|
+
resolve("ok");
|
|
3022
|
+
}, code, reason);
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
3025
|
+
push(data) {
|
|
3026
|
+
this.socket.push(data);
|
|
3027
|
+
}
|
|
3028
|
+
log(kind, msg, data) {
|
|
3029
|
+
this.socket.log(kind, msg, data);
|
|
3030
|
+
}
|
|
3031
|
+
makeRef() {
|
|
3032
|
+
return this.socket.makeRef();
|
|
3033
|
+
}
|
|
3034
|
+
onOpen(callback) {
|
|
3035
|
+
this.socket.onOpen(callback);
|
|
3036
|
+
}
|
|
3037
|
+
onClose(callback) {
|
|
3038
|
+
this.socket.onClose(callback);
|
|
3039
|
+
}
|
|
3040
|
+
onError(callback) {
|
|
3041
|
+
this.socket.onError(callback);
|
|
3042
|
+
}
|
|
3043
|
+
onMessage(callback) {
|
|
3044
|
+
this.socket.onMessage(callback);
|
|
3045
|
+
}
|
|
3046
|
+
isConnected() {
|
|
3047
|
+
return this.socket.isConnected();
|
|
3048
|
+
}
|
|
3049
|
+
isConnecting() {
|
|
3050
|
+
return this.socket.connectionState() == CONNECTION_STATE.connecting;
|
|
3051
|
+
}
|
|
3052
|
+
isDisconnecting() {
|
|
3053
|
+
return this.socket.connectionState() == CONNECTION_STATE.closing;
|
|
3054
|
+
}
|
|
3055
|
+
connectionState() {
|
|
3056
|
+
return this.socket.connectionState();
|
|
3057
|
+
}
|
|
3058
|
+
endPointURL() {
|
|
3059
|
+
return this.socket.endPointURL();
|
|
3060
|
+
}
|
|
3061
|
+
sendHeartbeat() {
|
|
3062
|
+
this.socket.sendHeartbeat();
|
|
3063
|
+
}
|
|
3064
|
+
/**
|
|
3065
|
+
* @internal
|
|
3066
|
+
*/
|
|
3067
|
+
getSocket() {
|
|
3068
|
+
return this.socket;
|
|
3069
|
+
}
|
|
3070
|
+
};
|
|
3071
|
+
//#endregion
|
|
3072
|
+
//#region ../../node_modules/.pnpm/@supabase+realtime-js@2.100.1/node_modules/@supabase/realtime-js/dist/module/RealtimeClient.js
|
|
3073
|
+
const CONNECTION_TIMEOUTS = {
|
|
3074
|
+
HEARTBEAT_INTERVAL: 25e3,
|
|
3075
|
+
RECONNECT_DELAY: 10,
|
|
3076
|
+
HEARTBEAT_TIMEOUT_FALLBACK: 100
|
|
3077
|
+
};
|
|
3078
|
+
const RECONNECT_INTERVALS = [
|
|
3079
|
+
1e3,
|
|
3080
|
+
2e3,
|
|
3081
|
+
5e3,
|
|
3082
|
+
1e4
|
|
3083
|
+
];
|
|
3084
|
+
const DEFAULT_RECONNECT_FALLBACK = 1e4;
|
|
3085
|
+
const WORKER_SCRIPT = `
|
|
3086
|
+
addEventListener("message", (e) => {
|
|
3087
|
+
if (e.data.event === "start") {
|
|
3088
|
+
setInterval(() => postMessage({ event: "keepAlive" }), e.data.interval);
|
|
3089
|
+
}
|
|
3090
|
+
});`;
|
|
3091
|
+
var RealtimeClient = class {
|
|
3092
|
+
get endPoint() {
|
|
3093
|
+
return this.socketAdapter.endPoint;
|
|
3094
|
+
}
|
|
3095
|
+
get timeout() {
|
|
3096
|
+
return this.socketAdapter.timeout;
|
|
3097
|
+
}
|
|
3098
|
+
get transport() {
|
|
3099
|
+
return this.socketAdapter.transport;
|
|
3100
|
+
}
|
|
3101
|
+
get heartbeatCallback() {
|
|
3102
|
+
return this.socketAdapter.heartbeatCallback;
|
|
3103
|
+
}
|
|
3104
|
+
get heartbeatIntervalMs() {
|
|
3105
|
+
return this.socketAdapter.heartbeatIntervalMs;
|
|
3106
|
+
}
|
|
3107
|
+
get heartbeatTimer() {
|
|
3108
|
+
if (this.worker) return this._workerHeartbeatTimer;
|
|
3109
|
+
return this.socketAdapter.heartbeatTimer;
|
|
3110
|
+
}
|
|
3111
|
+
get pendingHeartbeatRef() {
|
|
3112
|
+
if (this.worker) return this._pendingWorkerHeartbeatRef;
|
|
3113
|
+
return this.socketAdapter.pendingHeartbeatRef;
|
|
3114
|
+
}
|
|
3115
|
+
get reconnectTimer() {
|
|
3116
|
+
return this.socketAdapter.reconnectTimer;
|
|
3117
|
+
}
|
|
3118
|
+
get vsn() {
|
|
3119
|
+
return this.socketAdapter.vsn;
|
|
3120
|
+
}
|
|
3121
|
+
get encode() {
|
|
3122
|
+
return this.socketAdapter.encode;
|
|
3123
|
+
}
|
|
3124
|
+
get decode() {
|
|
3125
|
+
return this.socketAdapter.decode;
|
|
3126
|
+
}
|
|
3127
|
+
get reconnectAfterMs() {
|
|
3128
|
+
return this.socketAdapter.reconnectAfterMs;
|
|
3129
|
+
}
|
|
3130
|
+
get sendBuffer() {
|
|
3131
|
+
return this.socketAdapter.sendBuffer;
|
|
3132
|
+
}
|
|
3133
|
+
get stateChangeCallbacks() {
|
|
3134
|
+
return this.socketAdapter.stateChangeCallbacks;
|
|
3135
|
+
}
|
|
3136
|
+
/**
|
|
3137
|
+
* Initializes the Socket.
|
|
3138
|
+
*
|
|
3139
|
+
* @param endPoint The string WebSocket endpoint, ie, "ws://example.com/socket", "wss://example.com", "/socket" (inherited host & protocol)
|
|
3140
|
+
* @param httpEndpoint The string HTTP endpoint, ie, "https://example.com", "/" (inherited host & protocol)
|
|
3141
|
+
* @param options.transport The Websocket Transport, for example WebSocket. This can be a custom implementation
|
|
3142
|
+
* @param options.timeout The default timeout in milliseconds to trigger push timeouts.
|
|
3143
|
+
* @param options.params The optional params to pass when connecting.
|
|
3144
|
+
* @param options.headers Deprecated: headers cannot be set on websocket connections and this option will be removed in the future.
|
|
3145
|
+
* @param options.heartbeatIntervalMs The millisec interval to send a heartbeat message.
|
|
3146
|
+
* @param options.heartbeatCallback The optional function to handle heartbeat status and latency.
|
|
3147
|
+
* @param options.logger The optional function for specialized logging, ie: logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }
|
|
3148
|
+
* @param options.logLevel Sets the log level for Realtime
|
|
3149
|
+
* @param options.encode The function to encode outgoing messages. Defaults to JSON: (payload, callback) => callback(JSON.stringify(payload))
|
|
3150
|
+
* @param options.decode The function to decode incoming messages. Defaults to Serializer's decode.
|
|
3151
|
+
* @param options.reconnectAfterMs he optional function that returns the millsec reconnect interval. Defaults to stepped backoff off.
|
|
3152
|
+
* @param options.worker Use Web Worker to set a side flow. Defaults to false.
|
|
3153
|
+
* @param options.workerUrl The URL of the worker script. Defaults to https://realtime.supabase.com/worker.js that includes a heartbeat event call to keep the connection alive.
|
|
3154
|
+
* @param options.vsn The protocol version to use when connecting. Supported versions are "1.0.0" and "2.0.0". Defaults to "2.0.0".
|
|
3155
|
+
*
|
|
3156
|
+
* @category Realtime
|
|
3157
|
+
*
|
|
3158
|
+
* @example Example for a public channel
|
|
3159
|
+
* ```ts
|
|
3160
|
+
* import RealtimeClient from '@supabase/realtime-js'
|
|
3161
|
+
*
|
|
3162
|
+
* const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
|
|
3163
|
+
* params: { apikey: 'public-anon-key' },
|
|
3164
|
+
* })
|
|
3165
|
+
* client.connect()
|
|
3166
|
+
* ```
|
|
3167
|
+
*/
|
|
3168
|
+
constructor(endPoint, options) {
|
|
3169
|
+
var _a;
|
|
3170
|
+
this.channels = new Array();
|
|
3171
|
+
this.accessTokenValue = null;
|
|
3172
|
+
this.accessToken = null;
|
|
3173
|
+
this.apiKey = null;
|
|
3174
|
+
this.httpEndpoint = "";
|
|
3175
|
+
/** @deprecated headers cannot be set on websocket connections */
|
|
3176
|
+
this.headers = {};
|
|
3177
|
+
this.params = {};
|
|
3178
|
+
this.ref = 0;
|
|
3179
|
+
this.serializer = new Serializer();
|
|
3180
|
+
this._manuallySetToken = false;
|
|
3181
|
+
this._authPromise = null;
|
|
3182
|
+
this._workerHeartbeatTimer = void 0;
|
|
3183
|
+
this._pendingWorkerHeartbeatRef = null;
|
|
3184
|
+
/**
|
|
3185
|
+
* Use either custom fetch, if provided, or default fetch to make HTTP requests
|
|
3186
|
+
*
|
|
3187
|
+
* @internal
|
|
3188
|
+
*/
|
|
3189
|
+
this._resolveFetch = (customFetch) => {
|
|
3190
|
+
if (customFetch) return (...args) => customFetch(...args);
|
|
3191
|
+
return (...args) => fetch(...args);
|
|
3192
|
+
};
|
|
3193
|
+
if (!((_a = options === null || options === void 0 ? void 0 : options.params) === null || _a === void 0 ? void 0 : _a.apikey)) throw new Error("API key is required to connect to Realtime");
|
|
3194
|
+
this.apiKey = options.params.apikey;
|
|
3195
|
+
const socketAdapterOptions = this._initializeOptions(options);
|
|
3196
|
+
this.socketAdapter = new SocketAdapter(endPoint, socketAdapterOptions);
|
|
3197
|
+
this.httpEndpoint = httpEndpointURL(endPoint);
|
|
3198
|
+
this.fetch = this._resolveFetch(options === null || options === void 0 ? void 0 : options.fetch);
|
|
3199
|
+
}
|
|
3200
|
+
/**
|
|
3201
|
+
* Connects the socket, unless already connected.
|
|
3202
|
+
*
|
|
3203
|
+
* @category Realtime
|
|
3204
|
+
*/
|
|
3205
|
+
connect() {
|
|
3206
|
+
if (this.isConnecting() || this.isDisconnecting() || this.isConnected()) return;
|
|
3207
|
+
if (this.accessToken && !this._authPromise) this._setAuthSafely("connect");
|
|
3208
|
+
this._setupConnectionHandlers();
|
|
3209
|
+
try {
|
|
3210
|
+
this.socketAdapter.connect();
|
|
3211
|
+
} catch (error) {
|
|
3212
|
+
const errorMessage = error.message;
|
|
3213
|
+
if (errorMessage.includes("Node.js")) throw new Error(`${errorMessage}\n\nTo use Realtime in Node.js, you need to provide a WebSocket implementation:
|
|
3214
|
+
|
|
3215
|
+
Option 1: Use Node.js 22+ which has native WebSocket support
|
|
3216
|
+
Option 2: Install and provide the "ws" package:
|
|
3217
|
+
|
|
3218
|
+
npm install ws
|
|
3219
|
+
|
|
3220
|
+
import ws from "ws"
|
|
3221
|
+
const client = new RealtimeClient(url, {
|
|
3222
|
+
...options,
|
|
3223
|
+
transport: ws
|
|
3224
|
+
})`);
|
|
3225
|
+
throw new Error(`WebSocket not available: ${errorMessage}`);
|
|
3226
|
+
}
|
|
3227
|
+
this._handleNodeJsRaceCondition();
|
|
3228
|
+
}
|
|
3229
|
+
/**
|
|
3230
|
+
* Returns the URL of the websocket.
|
|
3231
|
+
* @returns string The URL of the websocket.
|
|
3232
|
+
*
|
|
3233
|
+
* @category Realtime
|
|
3234
|
+
*/
|
|
3235
|
+
endpointURL() {
|
|
3236
|
+
return this.socketAdapter.endPointURL();
|
|
3237
|
+
}
|
|
3238
|
+
/**
|
|
3239
|
+
* Disconnects the socket.
|
|
3240
|
+
*
|
|
3241
|
+
* @param code A numeric status code to send on disconnect.
|
|
3242
|
+
* @param reason A custom reason for the disconnect.
|
|
3243
|
+
*
|
|
3244
|
+
* @category Realtime
|
|
3245
|
+
*/
|
|
3246
|
+
async disconnect(code, reason) {
|
|
3247
|
+
if (this.isDisconnecting()) return "ok";
|
|
3248
|
+
return await this.socketAdapter.disconnect(() => {
|
|
3249
|
+
clearInterval(this._workerHeartbeatTimer);
|
|
3250
|
+
this._terminateWorker();
|
|
3251
|
+
}, code, reason);
|
|
3252
|
+
}
|
|
3253
|
+
/**
|
|
3254
|
+
* Returns all created channels
|
|
3255
|
+
*
|
|
3256
|
+
* @category Realtime
|
|
3257
|
+
*/
|
|
3258
|
+
getChannels() {
|
|
3259
|
+
return this.channels;
|
|
3260
|
+
}
|
|
3261
|
+
/**
|
|
3262
|
+
* Unsubscribes, removes and tears down a single channel
|
|
3263
|
+
* @param channel A RealtimeChannel instance
|
|
3264
|
+
*
|
|
3265
|
+
* @category Realtime
|
|
3266
|
+
*/
|
|
3267
|
+
async removeChannel(channel) {
|
|
3268
|
+
const status = await channel.unsubscribe();
|
|
3269
|
+
if (status === "ok") channel.teardown();
|
|
3270
|
+
if (this.channels.length === 0) this.disconnect();
|
|
3271
|
+
return status;
|
|
3272
|
+
}
|
|
3273
|
+
/**
|
|
3274
|
+
* Unsubscribes, removes and tears down all channels
|
|
3275
|
+
*
|
|
3276
|
+
* @category Realtime
|
|
3277
|
+
*/
|
|
3278
|
+
async removeAllChannels() {
|
|
3279
|
+
const promises = this.channels.map(async (channel) => {
|
|
3280
|
+
const result = await channel.unsubscribe();
|
|
3281
|
+
channel.teardown();
|
|
3282
|
+
return result;
|
|
3283
|
+
});
|
|
3284
|
+
const result = await Promise.all(promises);
|
|
3285
|
+
this.disconnect();
|
|
3286
|
+
return result;
|
|
3287
|
+
}
|
|
3288
|
+
/**
|
|
3289
|
+
* Logs the message.
|
|
3290
|
+
*
|
|
3291
|
+
* For customized logging, `this.logger` can be overridden in Client constructor.
|
|
3292
|
+
*
|
|
3293
|
+
* @category Realtime
|
|
3294
|
+
*/
|
|
3295
|
+
log(kind, msg, data) {
|
|
3296
|
+
this.socketAdapter.log(kind, msg, data);
|
|
3297
|
+
}
|
|
3298
|
+
/**
|
|
3299
|
+
* Returns the current state of the socket.
|
|
3300
|
+
*
|
|
3301
|
+
* @category Realtime
|
|
3302
|
+
*/
|
|
3303
|
+
connectionState() {
|
|
3304
|
+
return this.socketAdapter.connectionState() || CONNECTION_STATE.closed;
|
|
3305
|
+
}
|
|
3306
|
+
/**
|
|
3307
|
+
* Returns `true` is the connection is open.
|
|
3308
|
+
*
|
|
3309
|
+
* @category Realtime
|
|
3310
|
+
*/
|
|
3311
|
+
isConnected() {
|
|
3312
|
+
return this.socketAdapter.isConnected();
|
|
3313
|
+
}
|
|
3314
|
+
/**
|
|
3315
|
+
* Returns `true` if the connection is currently connecting.
|
|
3316
|
+
*
|
|
3317
|
+
* @category Realtime
|
|
3318
|
+
*/
|
|
3319
|
+
isConnecting() {
|
|
3320
|
+
return this.socketAdapter.isConnecting();
|
|
3321
|
+
}
|
|
3322
|
+
/**
|
|
3323
|
+
* Returns `true` if the connection is currently disconnecting.
|
|
3324
|
+
*
|
|
3325
|
+
* @category Realtime
|
|
3326
|
+
*/
|
|
3327
|
+
isDisconnecting() {
|
|
3328
|
+
return this.socketAdapter.isDisconnecting();
|
|
3329
|
+
}
|
|
3330
|
+
/**
|
|
3331
|
+
* Creates (or reuses) a {@link RealtimeChannel} for the provided topic.
|
|
3332
|
+
*
|
|
3333
|
+
* Topics are automatically prefixed with `realtime:` to match the Realtime service.
|
|
3334
|
+
* If a channel with the same topic already exists it will be returned instead of creating
|
|
3335
|
+
* a duplicate connection.
|
|
3336
|
+
*
|
|
3337
|
+
* @category Realtime
|
|
3338
|
+
*/
|
|
3339
|
+
channel(topic, params = { config: {} }) {
|
|
3340
|
+
const realtimeTopic = `realtime:${topic}`;
|
|
3341
|
+
const exists = this.getChannels().find((c) => c.topic === realtimeTopic);
|
|
3342
|
+
if (!exists) {
|
|
3343
|
+
const chan = new RealtimeChannel(`realtime:${topic}`, params, this);
|
|
3344
|
+
this.channels.push(chan);
|
|
3345
|
+
return chan;
|
|
3346
|
+
} else return exists;
|
|
3347
|
+
}
|
|
3348
|
+
/**
|
|
3349
|
+
* Push out a message if the socket is connected.
|
|
3350
|
+
*
|
|
3351
|
+
* If the socket is not connected, the message gets enqueued within a local buffer, and sent out when a connection is next established.
|
|
3352
|
+
*
|
|
3353
|
+
* @category Realtime
|
|
3354
|
+
*/
|
|
3355
|
+
push(data) {
|
|
3356
|
+
this.socketAdapter.push(data);
|
|
3357
|
+
}
|
|
3358
|
+
/**
|
|
3359
|
+
* Sets the JWT access token used for channel subscription authorization and Realtime RLS.
|
|
3360
|
+
*
|
|
3361
|
+
* If param is null it will use the `accessToken` callback function or the token set on the client.
|
|
3362
|
+
*
|
|
3363
|
+
* On callback used, it will set the value of the token internal to the client.
|
|
3364
|
+
*
|
|
3365
|
+
* When a token is explicitly provided, it will be preserved across channel operations
|
|
3366
|
+
* (including removeChannel and resubscribe). The `accessToken` callback will not be
|
|
3367
|
+
* invoked until `setAuth()` is called without arguments.
|
|
3368
|
+
*
|
|
3369
|
+
* @param token A JWT string to override the token set on the client.
|
|
3370
|
+
*
|
|
3371
|
+
* @example Setting the authorization header
|
|
3372
|
+
* // Use a manual token (preserved across resubscribes, ignores accessToken callback)
|
|
3373
|
+
* client.realtime.setAuth('my-custom-jwt')
|
|
3374
|
+
*
|
|
3375
|
+
* // Switch back to using the accessToken callback
|
|
3376
|
+
* client.realtime.setAuth()
|
|
3377
|
+
*
|
|
3378
|
+
* @category Realtime
|
|
3379
|
+
*/
|
|
3380
|
+
async setAuth(token = null) {
|
|
3381
|
+
this._authPromise = this._performAuth(token);
|
|
3382
|
+
try {
|
|
3383
|
+
await this._authPromise;
|
|
3384
|
+
} finally {
|
|
3385
|
+
this._authPromise = null;
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
/**
|
|
3389
|
+
* Returns true if the current access token was explicitly set via setAuth(token),
|
|
3390
|
+
* false if it was obtained via the accessToken callback.
|
|
3391
|
+
* @internal
|
|
3392
|
+
*/
|
|
3393
|
+
_isManualToken() {
|
|
3394
|
+
return this._manuallySetToken;
|
|
3395
|
+
}
|
|
3396
|
+
/**
|
|
3397
|
+
* Sends a heartbeat message if the socket is connected.
|
|
3398
|
+
*
|
|
3399
|
+
* @category Realtime
|
|
3400
|
+
*/
|
|
3401
|
+
async sendHeartbeat() {
|
|
3402
|
+
this.socketAdapter.sendHeartbeat();
|
|
3403
|
+
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Sets a callback that receives lifecycle events for internal heartbeat messages.
|
|
3406
|
+
* Useful for instrumenting connection health (e.g. sent/ok/timeout/disconnected).
|
|
3407
|
+
*
|
|
3408
|
+
* @category Realtime
|
|
3409
|
+
*/
|
|
3410
|
+
onHeartbeat(callback) {
|
|
3411
|
+
this.socketAdapter.heartbeatCallback = this._wrapHeartbeatCallback(callback);
|
|
3412
|
+
}
|
|
3413
|
+
/**
|
|
3414
|
+
* Return the next message ref, accounting for overflows
|
|
3415
|
+
*
|
|
3416
|
+
* @internal
|
|
3417
|
+
*/
|
|
3418
|
+
_makeRef() {
|
|
3419
|
+
return this.socketAdapter.makeRef();
|
|
3420
|
+
}
|
|
3421
|
+
/**
|
|
3422
|
+
* Removes a channel from RealtimeClient
|
|
3423
|
+
*
|
|
3424
|
+
* @param channel An open subscription.
|
|
3425
|
+
*
|
|
3426
|
+
* @internal
|
|
3427
|
+
*/
|
|
3428
|
+
_remove(channel) {
|
|
3429
|
+
this.channels = this.channels.filter((c) => c.topic !== channel.topic);
|
|
3430
|
+
}
|
|
3431
|
+
/**
|
|
3432
|
+
* Perform the actual auth operation
|
|
3433
|
+
* @internal
|
|
3434
|
+
*/
|
|
3435
|
+
async _performAuth(token = null) {
|
|
3436
|
+
let tokenToSend;
|
|
3437
|
+
let isManualToken = false;
|
|
3438
|
+
if (token) {
|
|
3439
|
+
tokenToSend = token;
|
|
3440
|
+
isManualToken = true;
|
|
3441
|
+
} else if (this.accessToken) try {
|
|
3442
|
+
tokenToSend = await this.accessToken();
|
|
3443
|
+
} catch (e) {
|
|
3444
|
+
this.log("error", "Error fetching access token from callback", e);
|
|
3445
|
+
tokenToSend = this.accessTokenValue;
|
|
3446
|
+
}
|
|
3447
|
+
else tokenToSend = this.accessTokenValue;
|
|
3448
|
+
if (isManualToken) this._manuallySetToken = true;
|
|
3449
|
+
else if (this.accessToken) this._manuallySetToken = false;
|
|
3450
|
+
if (this.accessTokenValue != tokenToSend) {
|
|
3451
|
+
this.accessTokenValue = tokenToSend;
|
|
3452
|
+
this.channels.forEach((channel) => {
|
|
3453
|
+
const payload = {
|
|
3454
|
+
access_token: tokenToSend,
|
|
3455
|
+
version: DEFAULT_VERSION
|
|
3456
|
+
};
|
|
3457
|
+
tokenToSend && channel.updateJoinPayload(payload);
|
|
3458
|
+
if (channel.joinedOnce && channel.channelAdapter.isJoined()) channel.channelAdapter.push(CHANNEL_EVENTS$1.access_token, { access_token: tokenToSend });
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
/**
|
|
3463
|
+
* Wait for any in-flight auth operations to complete
|
|
3464
|
+
* @internal
|
|
3465
|
+
*/
|
|
3466
|
+
async _waitForAuthIfNeeded() {
|
|
3467
|
+
if (this._authPromise) await this._authPromise;
|
|
3468
|
+
}
|
|
3469
|
+
/**
|
|
3470
|
+
* Safely call setAuth with standardized error handling
|
|
3471
|
+
* @internal
|
|
3472
|
+
*/
|
|
3473
|
+
_setAuthSafely(context = "general") {
|
|
3474
|
+
if (!this._isManualToken()) this.setAuth().catch((e) => {
|
|
3475
|
+
this.log("error", `Error setting auth in ${context}`, e);
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3478
|
+
/** @internal */
|
|
3479
|
+
_setupConnectionHandlers() {
|
|
3480
|
+
this.socketAdapter.onOpen(() => {
|
|
3481
|
+
(this._authPromise || (this.accessToken && !this.accessTokenValue ? this.setAuth() : Promise.resolve())).catch((e) => {
|
|
3482
|
+
this.log("error", "error waiting for auth on connect", e);
|
|
3483
|
+
});
|
|
3484
|
+
if (this.worker && !this.workerRef) this._startWorkerHeartbeat();
|
|
3485
|
+
});
|
|
3486
|
+
this.socketAdapter.onClose(() => {
|
|
3487
|
+
if (this.worker && this.workerRef) this._terminateWorker();
|
|
3488
|
+
});
|
|
3489
|
+
this.socketAdapter.onMessage((message) => {
|
|
3490
|
+
if (message.ref && message.ref === this._pendingWorkerHeartbeatRef) this._pendingWorkerHeartbeatRef = null;
|
|
3491
|
+
});
|
|
3492
|
+
}
|
|
3493
|
+
/** @internal */
|
|
3494
|
+
_handleNodeJsRaceCondition() {
|
|
3495
|
+
if (this.socketAdapter.isConnected()) this.socketAdapter.getSocket().onConnOpen();
|
|
3496
|
+
}
|
|
3497
|
+
/** @internal */
|
|
3498
|
+
_wrapHeartbeatCallback(heartbeatCallback) {
|
|
3499
|
+
return (status, latency) => {
|
|
3500
|
+
if (status == "sent") this._setAuthSafely();
|
|
3501
|
+
if (heartbeatCallback) heartbeatCallback(status, latency);
|
|
3502
|
+
};
|
|
3503
|
+
}
|
|
3504
|
+
/** @internal */
|
|
3505
|
+
_startWorkerHeartbeat() {
|
|
3506
|
+
if (this.workerUrl) this.log("worker", `starting worker for from ${this.workerUrl}`);
|
|
3507
|
+
else this.log("worker", `starting default worker`);
|
|
3508
|
+
const objectUrl = this._workerObjectUrl(this.workerUrl);
|
|
3509
|
+
this.workerRef = new Worker(objectUrl);
|
|
3510
|
+
this.workerRef.onerror = (error) => {
|
|
3511
|
+
this.log("worker", "worker error", error.message);
|
|
3512
|
+
this._terminateWorker();
|
|
3513
|
+
this.disconnect();
|
|
3514
|
+
};
|
|
3515
|
+
this.workerRef.onmessage = (event) => {
|
|
3516
|
+
if (event.data.event === "keepAlive") this.sendHeartbeat();
|
|
3517
|
+
};
|
|
3518
|
+
this.workerRef.postMessage({
|
|
3519
|
+
event: "start",
|
|
3520
|
+
interval: this.heartbeatIntervalMs
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
/**
|
|
3524
|
+
* Terminate the Web Worker and clear the reference
|
|
3525
|
+
* @internal
|
|
3526
|
+
*/
|
|
3527
|
+
_terminateWorker() {
|
|
3528
|
+
if (this.workerRef) {
|
|
3529
|
+
this.log("worker", "terminating worker");
|
|
3530
|
+
this.workerRef.terminate();
|
|
3531
|
+
this.workerRef = void 0;
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
/** @internal */
|
|
3535
|
+
_workerObjectUrl(url) {
|
|
3536
|
+
let result_url;
|
|
3537
|
+
if (url) result_url = url;
|
|
3538
|
+
else {
|
|
3539
|
+
const blob = new Blob([WORKER_SCRIPT], { type: "application/javascript" });
|
|
3540
|
+
result_url = URL.createObjectURL(blob);
|
|
3541
|
+
}
|
|
3542
|
+
return result_url;
|
|
3543
|
+
}
|
|
3544
|
+
/**
|
|
3545
|
+
* Initialize socket options with defaults
|
|
3546
|
+
* @internal
|
|
3547
|
+
*/
|
|
3548
|
+
_initializeOptions(options) {
|
|
3549
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
3550
|
+
this.worker = (_a = options === null || options === void 0 ? void 0 : options.worker) !== null && _a !== void 0 ? _a : false;
|
|
3551
|
+
this.accessToken = (_b = options === null || options === void 0 ? void 0 : options.accessToken) !== null && _b !== void 0 ? _b : null;
|
|
3552
|
+
const result = {};
|
|
3553
|
+
result.timeout = (_c = options === null || options === void 0 ? void 0 : options.timeout) !== null && _c !== void 0 ? _c : DEFAULT_TIMEOUT$1;
|
|
3554
|
+
result.heartbeatIntervalMs = (_d = options === null || options === void 0 ? void 0 : options.heartbeatIntervalMs) !== null && _d !== void 0 ? _d : CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL;
|
|
3555
|
+
result.transport = (_e = options === null || options === void 0 ? void 0 : options.transport) !== null && _e !== void 0 ? _e : WebSocketFactory.getWebSocketConstructor();
|
|
3556
|
+
result.params = options === null || options === void 0 ? void 0 : options.params;
|
|
3557
|
+
result.logger = options === null || options === void 0 ? void 0 : options.logger;
|
|
3558
|
+
result.heartbeatCallback = this._wrapHeartbeatCallback(options === null || options === void 0 ? void 0 : options.heartbeatCallback);
|
|
3559
|
+
result.reconnectAfterMs = (_f = options === null || options === void 0 ? void 0 : options.reconnectAfterMs) !== null && _f !== void 0 ? _f : ((tries) => {
|
|
3560
|
+
return RECONNECT_INTERVALS[tries - 1] || DEFAULT_RECONNECT_FALLBACK;
|
|
3561
|
+
});
|
|
3562
|
+
let defaultEncode;
|
|
3563
|
+
let defaultDecode;
|
|
3564
|
+
const vsn = (_g = options === null || options === void 0 ? void 0 : options.vsn) !== null && _g !== void 0 ? _g : DEFAULT_VSN$1;
|
|
3565
|
+
switch (vsn) {
|
|
3566
|
+
case VSN_1_0_0:
|
|
3567
|
+
defaultEncode = (payload, callback) => {
|
|
3568
|
+
return callback(JSON.stringify(payload));
|
|
3569
|
+
};
|
|
3570
|
+
defaultDecode = (payload, callback) => {
|
|
3571
|
+
return callback(JSON.parse(payload));
|
|
3572
|
+
};
|
|
3573
|
+
break;
|
|
3574
|
+
case VSN_2_0_0:
|
|
3575
|
+
defaultEncode = this.serializer.encode.bind(this.serializer);
|
|
3576
|
+
defaultDecode = this.serializer.decode.bind(this.serializer);
|
|
3577
|
+
break;
|
|
3578
|
+
default: throw new Error(`Unsupported serializer version: ${result.vsn}`);
|
|
3579
|
+
}
|
|
3580
|
+
result.vsn = vsn;
|
|
3581
|
+
result.encode = (_h = options === null || options === void 0 ? void 0 : options.encode) !== null && _h !== void 0 ? _h : defaultEncode;
|
|
3582
|
+
result.decode = (_j = options === null || options === void 0 ? void 0 : options.decode) !== null && _j !== void 0 ? _j : defaultDecode;
|
|
3583
|
+
result.beforeReconnect = this._reconnectAuth.bind(this);
|
|
3584
|
+
if ((options === null || options === void 0 ? void 0 : options.logLevel) || (options === null || options === void 0 ? void 0 : options.log_level)) {
|
|
3585
|
+
this.logLevel = options.logLevel || options.log_level;
|
|
3586
|
+
result.params = Object.assign(Object.assign({}, result.params), { log_level: this.logLevel });
|
|
3587
|
+
}
|
|
3588
|
+
if (this.worker) {
|
|
3589
|
+
if (typeof window !== "undefined" && !window.Worker) throw new Error("Web Worker is not supported");
|
|
3590
|
+
this.workerUrl = options === null || options === void 0 ? void 0 : options.workerUrl;
|
|
3591
|
+
result.autoSendHeartbeat = !this.worker;
|
|
3592
|
+
}
|
|
3593
|
+
return result;
|
|
3594
|
+
}
|
|
3595
|
+
/** @internal */
|
|
3596
|
+
async _reconnectAuth() {
|
|
3597
|
+
await this._waitForAuthIfNeeded();
|
|
3598
|
+
if (!this.isConnected()) this.connect();
|
|
3599
|
+
}
|
|
3600
|
+
};
|
|
3601
|
+
//#endregion
|
|
3602
|
+
//#region src/client.ts
|
|
3603
|
+
const createRealtimeSocket = (url, key) => {
|
|
3604
|
+
return new RealtimeClient(`${url.replace(/\/$/, "")}/realtime/v1`, { params: { apikey: key } });
|
|
3605
|
+
};
|
|
3606
|
+
//#endregion
|
|
3607
|
+
//#region src/listen.types.ts
|
|
3608
|
+
const MENU_CHANGED_EVENT = "menu.changed";
|
|
3609
|
+
const AVAILABILITY_CHANGED_EVENT = "availability.changed";
|
|
3610
|
+
const ORDER_CHANGED_EVENT = "order.changed";
|
|
3611
|
+
const KIOSK_CONFIG_CHANGED_EVENT = "kiosk.config.changed";
|
|
3612
|
+
const KIOSK_COMMAND_EVENT = "kiosk.command";
|
|
3613
|
+
const CMS_CHANGED_EVENT = "cms.changed";
|
|
3614
|
+
const PAYMENTS_CHANGED_EVENT = "payments.changed";
|
|
3615
|
+
const FULFILLMENTS_CHANGED_EVENT = "fulfillments.changed";
|
|
3616
|
+
const REALTIME_REFRESH_MS = 27e4;
|
|
3617
|
+
//#endregion
|
|
3618
|
+
//#region src/scope.ts
|
|
3619
|
+
const ANY = "__ANY__";
|
|
3620
|
+
const fieldOverlaps = (published, listener) => {
|
|
3621
|
+
if (!published || published === ANY) return true;
|
|
3622
|
+
if (!listener || listener === ANY) return true;
|
|
3623
|
+
return published === listener;
|
|
3624
|
+
};
|
|
3625
|
+
const menuScopeOverlaps = (published, listener = {}) => {
|
|
3626
|
+
if (!fieldOverlaps(published.fulfillment_id, listener.fulfillment_id)) return false;
|
|
3627
|
+
if (!fieldOverlaps(published.store_id, listener.store_id)) return false;
|
|
3628
|
+
if (!fieldOverlaps(published.channel_id, listener.channel_id)) return false;
|
|
3629
|
+
if (!fieldOverlaps(published.brand_id, listener.brand_id)) return false;
|
|
3630
|
+
return true;
|
|
3631
|
+
};
|
|
3632
|
+
//#endregion
|
|
3633
|
+
//#region src/token.ts
|
|
3634
|
+
const fetchRealtimeToken = (scope = {}, options) => {
|
|
3635
|
+
return storefrontJson(...[
|
|
3636
|
+
"/api/storefront/v1/realtime/token",
|
|
3637
|
+
{
|
|
3638
|
+
method: "POST",
|
|
3639
|
+
body: JSON.stringify(scope)
|
|
3640
|
+
},
|
|
3641
|
+
options
|
|
3642
|
+
]);
|
|
3643
|
+
};
|
|
3644
|
+
//#endregion
|
|
3645
|
+
//#region src/listen.ts
|
|
3646
|
+
const SCOPE_KEYS = [
|
|
3647
|
+
"fulfillment_id",
|
|
3648
|
+
"store_id",
|
|
3649
|
+
"channel_id",
|
|
3650
|
+
"brand_id"
|
|
3651
|
+
];
|
|
3652
|
+
const isMenuChangedPayload = (value) => {
|
|
3653
|
+
if (!value || typeof value !== "object") return false;
|
|
3654
|
+
const rec = value;
|
|
3655
|
+
for (const key of SCOPE_KEYS) {
|
|
3656
|
+
if (rec[key] === void 0) continue;
|
|
3657
|
+
if (typeof rec[key] !== "string") return false;
|
|
3658
|
+
}
|
|
3659
|
+
return true;
|
|
3660
|
+
};
|
|
3661
|
+
const isWorkspaceChangedPayload = (value) => {
|
|
3662
|
+
if (!value || typeof value !== "object") return false;
|
|
3663
|
+
const rec = value;
|
|
3664
|
+
if (rec.merchant_id === void 0) return true;
|
|
3665
|
+
return typeof rec.merchant_id === "string";
|
|
3666
|
+
};
|
|
3667
|
+
const bindWorkspaceEvent = (channel, event, handler) => {
|
|
3668
|
+
if (!handler) return;
|
|
3669
|
+
channel.on("broadcast", { event }, (msg) => {
|
|
3670
|
+
const { payload } = msg;
|
|
3671
|
+
if (!isWorkspaceChangedPayload(payload)) return;
|
|
3672
|
+
handler(payload);
|
|
3673
|
+
});
|
|
3674
|
+
};
|
|
3675
|
+
const bindStorefrontEvents = (socket, topic, options) => {
|
|
3676
|
+
const { menuChanged, availabilityChanged } = options.on;
|
|
3677
|
+
const { paymentsChanged, fulfillmentsChanged } = options.on;
|
|
3678
|
+
if (!Object.values(options.on).some(Boolean)) return null;
|
|
3679
|
+
const channel = socket.channel(topic, { config: { private: true } });
|
|
3680
|
+
if (menuChanged) channel.on("broadcast", { event: MENU_CHANGED_EVENT }, (msg) => {
|
|
3681
|
+
const { payload } = msg;
|
|
3682
|
+
if (!isMenuChangedPayload(payload)) return;
|
|
3683
|
+
if (!menuScopeOverlaps(payload, options.scope)) return;
|
|
3684
|
+
menuChanged(payload);
|
|
3685
|
+
});
|
|
3686
|
+
bindWorkspaceEvent(channel, AVAILABILITY_CHANGED_EVENT, availabilityChanged);
|
|
3687
|
+
bindWorkspaceEvent(channel, PAYMENTS_CHANGED_EVENT, paymentsChanged);
|
|
3688
|
+
bindWorkspaceEvent(channel, FULFILLMENTS_CHANGED_EVENT, fulfillmentsChanged);
|
|
3689
|
+
channel.subscribe((status, err) => {
|
|
3690
|
+
if (status !== "CHANNEL_ERROR" && status !== "TIMED_OUT") return;
|
|
3691
|
+
const detail = err instanceof Error ? err.message : status;
|
|
3692
|
+
console.error("[realtime] subscribe failed", detail);
|
|
3693
|
+
});
|
|
3694
|
+
return channel;
|
|
3695
|
+
};
|
|
3696
|
+
const listen = (options, deps = {}) => {
|
|
3697
|
+
const createClient = deps.createClient ?? createRealtimeSocket;
|
|
3698
|
+
const setIntervalFn = deps.setIntervalFn ?? setInterval;
|
|
3699
|
+
const clearIntervalFn = deps.clearIntervalFn ?? clearInterval;
|
|
3700
|
+
let stopped = false;
|
|
3701
|
+
let socket = null;
|
|
3702
|
+
let channel = null;
|
|
3703
|
+
let refreshId = null;
|
|
3704
|
+
const http = {
|
|
3705
|
+
baseUrl: options.baseUrl,
|
|
3706
|
+
accessToken: options.accessToken,
|
|
3707
|
+
fetchImpl: options.fetchImpl
|
|
3708
|
+
};
|
|
3709
|
+
const teardown = () => {
|
|
3710
|
+
if (refreshId !== null) clearIntervalFn(refreshId);
|
|
3711
|
+
refreshId = null;
|
|
3712
|
+
channel?.unsubscribe();
|
|
3713
|
+
channel = null;
|
|
3714
|
+
socket?.disconnect();
|
|
3715
|
+
socket = null;
|
|
3716
|
+
};
|
|
3717
|
+
const applyToken = async (existing) => {
|
|
3718
|
+
const token = await fetchRealtimeToken(options.scope, http);
|
|
3719
|
+
if (stopped) return null;
|
|
3720
|
+
if (existing) {
|
|
3721
|
+
await existing.setAuth(token.access_token);
|
|
3722
|
+
return existing;
|
|
3723
|
+
}
|
|
3724
|
+
const next = createClient(token.url, token.key);
|
|
3725
|
+
await next.setAuth(token.access_token);
|
|
3726
|
+
if (stopped) {
|
|
3727
|
+
next.disconnect();
|
|
3728
|
+
return null;
|
|
3729
|
+
}
|
|
3730
|
+
next.connect();
|
|
3731
|
+
if (stopped) {
|
|
3732
|
+
next.disconnect();
|
|
3733
|
+
return null;
|
|
3734
|
+
}
|
|
3735
|
+
channel = bindStorefrontEvents(next, token.topic, options);
|
|
3736
|
+
return next;
|
|
3737
|
+
};
|
|
3738
|
+
const start = async () => {
|
|
3739
|
+
socket = await applyToken();
|
|
3740
|
+
if (stopped) {
|
|
3741
|
+
teardown();
|
|
3742
|
+
return;
|
|
3743
|
+
}
|
|
3744
|
+
if (!socket) return;
|
|
3745
|
+
refreshId = setIntervalFn(() => {
|
|
3746
|
+
if (!socket) return;
|
|
3747
|
+
applyToken(socket).catch((error) => {
|
|
3748
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3749
|
+
console.error("[realtime] token refresh failed", message);
|
|
3750
|
+
});
|
|
3751
|
+
}, REALTIME_REFRESH_MS);
|
|
3752
|
+
if (stopped) teardown();
|
|
3753
|
+
};
|
|
3754
|
+
start().catch((error) => {
|
|
3755
|
+
if (stopped) return;
|
|
3756
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3757
|
+
console.error("[realtime] listen failed", message);
|
|
3758
|
+
});
|
|
3759
|
+
return () => {
|
|
3760
|
+
stopped = true;
|
|
3761
|
+
teardown();
|
|
3762
|
+
};
|
|
3763
|
+
};
|
|
3764
|
+
//#endregion
|
|
3765
|
+
//#region src/listenKiosk.ts
|
|
3766
|
+
const isConfigPayload = (value) => {
|
|
3767
|
+
if (!value || typeof value !== "object") return false;
|
|
3768
|
+
return typeof value.device_uid === "string";
|
|
3769
|
+
};
|
|
3770
|
+
const COMMAND_TYPES = {
|
|
3771
|
+
refresh_cache: true,
|
|
3772
|
+
unlink: true,
|
|
3773
|
+
reload: true,
|
|
3774
|
+
check_ota: true
|
|
3775
|
+
};
|
|
3776
|
+
const isCommandPayload = (value) => {
|
|
3777
|
+
if (!value || typeof value !== "object") return false;
|
|
3778
|
+
const rec = value;
|
|
3779
|
+
if (typeof rec.type !== "string") return false;
|
|
3780
|
+
if (!(rec.type in COMMAND_TYPES)) return false;
|
|
3781
|
+
if (rec.device_uid === void 0) return true;
|
|
3782
|
+
return typeof rec.device_uid === "string";
|
|
3783
|
+
};
|
|
3784
|
+
const isCmsPayload = (value) => {
|
|
3785
|
+
if (!value || typeof value !== "object") return false;
|
|
3786
|
+
return typeof value.name === "string";
|
|
3787
|
+
};
|
|
3788
|
+
const cmsBlockTopic = (workspaceTopic, name) => {
|
|
3789
|
+
return `${workspaceTopic}:cms:${name}`;
|
|
3790
|
+
};
|
|
3791
|
+
const subscribeLogged = (channel) => {
|
|
3792
|
+
channel.subscribe((status, err) => {
|
|
3793
|
+
if (status !== "CHANNEL_ERROR" && status !== "TIMED_OUT") return;
|
|
3794
|
+
const detail = err instanceof Error ? err.message : status;
|
|
3795
|
+
console.error("[realtime] kiosk subscribe failed", detail);
|
|
3796
|
+
});
|
|
3797
|
+
return channel;
|
|
3798
|
+
};
|
|
3799
|
+
const bindCmsEvents = (socket, token, options) => {
|
|
3800
|
+
const onCmsChanged = options.on.cmsChanged;
|
|
3801
|
+
const names = [...new Set(options.cmsBlocks ?? [])];
|
|
3802
|
+
if (!onCmsChanged) return [];
|
|
3803
|
+
return names.map((name) => {
|
|
3804
|
+
const topic = cmsBlockTopic(token.workspace_topic, name);
|
|
3805
|
+
const channel = socket.channel(topic, { config: { private: true } });
|
|
3806
|
+
channel.on("broadcast", { event: CMS_CHANGED_EVENT }, (msg) => {
|
|
3807
|
+
const { payload } = msg;
|
|
3808
|
+
if (!isCmsPayload(payload)) return;
|
|
3809
|
+
onCmsChanged(payload);
|
|
3810
|
+
});
|
|
3811
|
+
return subscribeLogged(channel);
|
|
3812
|
+
});
|
|
3813
|
+
};
|
|
3814
|
+
const bindKioskEvents = (socket, topic, options) => {
|
|
3815
|
+
const onConfigChanged = options.on.configChanged;
|
|
3816
|
+
const onCommand = options.on.command;
|
|
3817
|
+
if (!onConfigChanged && !onCommand) return null;
|
|
3818
|
+
const channel = socket.channel(topic, { config: { private: true } });
|
|
3819
|
+
if (onConfigChanged) {
|
|
3820
|
+
const event = KIOSK_CONFIG_CHANGED_EVENT;
|
|
3821
|
+
channel.on("broadcast", { event }, (msg) => {
|
|
3822
|
+
const { payload } = msg;
|
|
3823
|
+
if (!isConfigPayload(payload)) return;
|
|
3824
|
+
onConfigChanged(payload);
|
|
3825
|
+
});
|
|
3826
|
+
}
|
|
3827
|
+
if (onCommand) channel.on("broadcast", { event: KIOSK_COMMAND_EVENT }, (msg) => {
|
|
3828
|
+
const { payload } = msg;
|
|
3829
|
+
if (!isCommandPayload(payload)) return;
|
|
3830
|
+
onCommand(payload);
|
|
3831
|
+
});
|
|
3832
|
+
return subscribeLogged(channel);
|
|
3833
|
+
};
|
|
3834
|
+
const listenKiosk = (options, deps = {}) => {
|
|
3835
|
+
const createClient = deps.createClient ?? createRealtimeSocket;
|
|
3836
|
+
const setIntervalFn = deps.setIntervalFn ?? setInterval;
|
|
3837
|
+
const clearIntervalFn = deps.clearIntervalFn ?? clearInterval;
|
|
3838
|
+
let stopped = false;
|
|
3839
|
+
let socket = null;
|
|
3840
|
+
let channels = [];
|
|
3841
|
+
let refreshId = null;
|
|
3842
|
+
const teardown = () => {
|
|
3843
|
+
if (refreshId !== null) clearIntervalFn(refreshId);
|
|
3844
|
+
refreshId = null;
|
|
3845
|
+
for (const channel of channels) channel.unsubscribe();
|
|
3846
|
+
channels = [];
|
|
3847
|
+
socket?.disconnect();
|
|
3848
|
+
socket = null;
|
|
3849
|
+
};
|
|
3850
|
+
const applyToken = async (existing) => {
|
|
3851
|
+
const token = await options.fetchToken();
|
|
3852
|
+
if (stopped) return null;
|
|
3853
|
+
if (existing) {
|
|
3854
|
+
await existing.setAuth(token.access_token);
|
|
3855
|
+
return existing;
|
|
3856
|
+
}
|
|
3857
|
+
const next = createClient(token.url, token.key);
|
|
3858
|
+
await next.setAuth(token.access_token);
|
|
3859
|
+
if (stopped) {
|
|
3860
|
+
next.disconnect();
|
|
3861
|
+
return null;
|
|
3862
|
+
}
|
|
3863
|
+
next.connect();
|
|
3864
|
+
if (stopped) {
|
|
3865
|
+
next.disconnect();
|
|
3866
|
+
return null;
|
|
3867
|
+
}
|
|
3868
|
+
const deviceChannel = bindKioskEvents(next, token.topic, options);
|
|
3869
|
+
const cmsChannels = bindCmsEvents(next, token, options);
|
|
3870
|
+
channels = [...deviceChannel ? [deviceChannel] : [], ...cmsChannels];
|
|
3871
|
+
return next;
|
|
3872
|
+
};
|
|
3873
|
+
const start = async () => {
|
|
3874
|
+
socket = await applyToken();
|
|
3875
|
+
if (stopped) {
|
|
3876
|
+
teardown();
|
|
3877
|
+
return;
|
|
3878
|
+
}
|
|
3879
|
+
if (!socket) return;
|
|
3880
|
+
refreshId = setIntervalFn(() => {
|
|
3881
|
+
if (!socket) return;
|
|
3882
|
+
applyToken(socket).catch((error) => {
|
|
3883
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3884
|
+
console.error("[realtime] kiosk token refresh failed", message);
|
|
3885
|
+
});
|
|
3886
|
+
}, REALTIME_REFRESH_MS);
|
|
3887
|
+
if (stopped) teardown();
|
|
3888
|
+
};
|
|
3889
|
+
start().catch((error) => {
|
|
3890
|
+
if (stopped) return;
|
|
3891
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3892
|
+
console.error("[realtime] listenKiosk failed", message);
|
|
3893
|
+
});
|
|
3894
|
+
return () => {
|
|
3895
|
+
stopped = true;
|
|
3896
|
+
teardown();
|
|
3897
|
+
};
|
|
3898
|
+
};
|
|
3899
|
+
//#endregion
|
|
3900
|
+
//#region src/listenOrder.ts
|
|
3901
|
+
const RUN_STATUSES = [
|
|
3902
|
+
"PROCESSING",
|
|
3903
|
+
"WAITING",
|
|
3904
|
+
"SUCCESS",
|
|
3905
|
+
"FAILED"
|
|
3906
|
+
];
|
|
3907
|
+
const isRunStatus = (value) => {
|
|
3908
|
+
return RUN_STATUSES.some((status) => status === value);
|
|
3909
|
+
};
|
|
3910
|
+
const isOrderChangedPayload = (value) => {
|
|
3911
|
+
if (!value || typeof value !== "object") return false;
|
|
3912
|
+
const rec = value;
|
|
3913
|
+
if (typeof rec.order_id !== "string") return false;
|
|
3914
|
+
if (typeof rec.order_fulfillment_id !== "string") return false;
|
|
3915
|
+
if (typeof rec.checkout_state !== "string") return false;
|
|
3916
|
+
return isRunStatus(rec.run_status);
|
|
3917
|
+
};
|
|
3918
|
+
const callbackFromNode = (node) => {
|
|
3919
|
+
const run = node.data.callback_run;
|
|
3920
|
+
if (!run) return null;
|
|
3921
|
+
const ok = run.response !== void 0;
|
|
3922
|
+
const b1 = {
|
|
3923
|
+
node_id: node.id,
|
|
3924
|
+
status: node.data.mappedStatus,
|
|
3925
|
+
ok
|
|
3926
|
+
};
|
|
3927
|
+
const b2 = {
|
|
3928
|
+
attempt: run.attempt,
|
|
3929
|
+
errors: run.errors
|
|
3930
|
+
};
|
|
3931
|
+
const b3 = {
|
|
3932
|
+
response: run.response,
|
|
3933
|
+
started_at: run.started_at
|
|
3934
|
+
};
|
|
3935
|
+
const b4 = {
|
|
3936
|
+
finished_at: run.finished_at,
|
|
3937
|
+
duration_ms: run.duration_ms
|
|
3938
|
+
};
|
|
3939
|
+
return {
|
|
3940
|
+
...b1,
|
|
3941
|
+
...b2,
|
|
3942
|
+
...b3,
|
|
3943
|
+
...b4
|
|
3944
|
+
};
|
|
3945
|
+
};
|
|
3946
|
+
const orderChangedFromFulfillment = (dto) => {
|
|
3947
|
+
const current = dto.current_node_ids;
|
|
3948
|
+
const current_states = dto.workflow.nodes.filter((node) => {
|
|
3949
|
+
return current.includes(node.id);
|
|
3950
|
+
}).map((node) => node.data.mappedStatus);
|
|
3951
|
+
const callbacks = [];
|
|
3952
|
+
for (const node of dto.workflow.nodes) {
|
|
3953
|
+
const item = callbackFromNode(node);
|
|
3954
|
+
if (item) callbacks.push(item);
|
|
3955
|
+
}
|
|
3956
|
+
const status = dto.fulfillment_status ?? current_states[0] ?? "";
|
|
3957
|
+
const customer_id = dto.customer_id ?? "";
|
|
3958
|
+
const payment_status = dto.payment_status ?? "";
|
|
3959
|
+
const b1 = {
|
|
3960
|
+
order_id: dto.order_id,
|
|
3961
|
+
order_fulfillment_id: dto.id
|
|
3962
|
+
};
|
|
3963
|
+
const b2 = {
|
|
3964
|
+
customer_id,
|
|
3965
|
+
status
|
|
3966
|
+
};
|
|
3967
|
+
const b3 = {
|
|
3968
|
+
payment_status,
|
|
3969
|
+
current_states
|
|
3970
|
+
};
|
|
3971
|
+
const b4 = {
|
|
3972
|
+
run_status: dto.status,
|
|
3973
|
+
checkout_state: dto.checkout_state
|
|
3974
|
+
};
|
|
3975
|
+
const b5 = {
|
|
3976
|
+
entered_node_ids: current,
|
|
3977
|
+
callbacks,
|
|
3978
|
+
error: dto.error
|
|
3979
|
+
};
|
|
3980
|
+
const b6 = { emitted_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3981
|
+
return {
|
|
3982
|
+
...b1,
|
|
3983
|
+
...b2,
|
|
3984
|
+
...b3,
|
|
3985
|
+
...b4,
|
|
3986
|
+
...b5,
|
|
3987
|
+
...b6
|
|
3988
|
+
};
|
|
3989
|
+
};
|
|
3990
|
+
const listenOrder = (options, deps = {}) => {
|
|
3991
|
+
const createClient = deps.createClient ?? createRealtimeSocket;
|
|
3992
|
+
const setIntervalFn = deps.setIntervalFn ?? setInterval;
|
|
3993
|
+
const clearIntervalFn = deps.clearIntervalFn ?? clearInterval;
|
|
3994
|
+
let stopped = false;
|
|
3995
|
+
let socket = null;
|
|
3996
|
+
let channel = null;
|
|
3997
|
+
let refreshId = null;
|
|
3998
|
+
const http = {
|
|
3999
|
+
baseUrl: options.baseUrl,
|
|
4000
|
+
accessToken: options.accessToken,
|
|
4001
|
+
fetchImpl: options.fetchImpl
|
|
4002
|
+
};
|
|
4003
|
+
const deliverSnapshot = async () => {
|
|
4004
|
+
const dto = await services.orders.getFulfillment(options.orderId, http);
|
|
4005
|
+
if (stopped) return;
|
|
4006
|
+
options.on.changed(orderChangedFromFulfillment(dto));
|
|
4007
|
+
};
|
|
4008
|
+
const bindOrderEvents = (next, topic) => {
|
|
4009
|
+
const ch = next.channel(topic, { config: { private: true } });
|
|
4010
|
+
ch.on("broadcast", { event: ORDER_CHANGED_EVENT }, (msg) => {
|
|
4011
|
+
const { payload } = msg;
|
|
4012
|
+
if (!isOrderChangedPayload(payload)) return;
|
|
4013
|
+
if (payload.order_id !== options.orderId) return;
|
|
4014
|
+
options.on.changed(payload);
|
|
4015
|
+
});
|
|
4016
|
+
ch.subscribe((status, err) => {
|
|
4017
|
+
if (status === "SUBSCRIBED") {
|
|
4018
|
+
options.on.subscribed?.();
|
|
4019
|
+
deliverSnapshot().catch((error) => {
|
|
4020
|
+
if (isStorefrontNotFound(error)) return;
|
|
4021
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
4022
|
+
console.error("[realtime] order snapshot failed", raw);
|
|
4023
|
+
});
|
|
4024
|
+
return;
|
|
4025
|
+
}
|
|
4026
|
+
if (status !== "CHANNEL_ERROR" && status !== "TIMED_OUT") return;
|
|
4027
|
+
const detail = err instanceof Error ? err.message : status;
|
|
4028
|
+
console.error("[realtime] subscribe failed", detail);
|
|
4029
|
+
});
|
|
4030
|
+
return ch;
|
|
4031
|
+
};
|
|
4032
|
+
const teardown = () => {
|
|
4033
|
+
if (refreshId !== null) clearIntervalFn(refreshId);
|
|
4034
|
+
refreshId = null;
|
|
4035
|
+
channel?.unsubscribe();
|
|
4036
|
+
channel = null;
|
|
4037
|
+
socket?.disconnect();
|
|
4038
|
+
socket = null;
|
|
4039
|
+
};
|
|
4040
|
+
const applyToken = async (existing) => {
|
|
4041
|
+
const token = await fetchRealtimeToken({}, http);
|
|
4042
|
+
if (stopped) return null;
|
|
4043
|
+
if (existing) {
|
|
4044
|
+
await existing.setAuth(token.access_token);
|
|
4045
|
+
return existing;
|
|
4046
|
+
}
|
|
4047
|
+
const next = createClient(token.url, token.key);
|
|
4048
|
+
await next.setAuth(token.access_token);
|
|
4049
|
+
if (stopped) {
|
|
4050
|
+
next.disconnect();
|
|
4051
|
+
return null;
|
|
4052
|
+
}
|
|
4053
|
+
next.connect();
|
|
4054
|
+
if (stopped) {
|
|
4055
|
+
next.disconnect();
|
|
4056
|
+
return null;
|
|
4057
|
+
}
|
|
4058
|
+
const topic = token.customer_topic;
|
|
4059
|
+
channel = bindOrderEvents(next, topic);
|
|
4060
|
+
return next;
|
|
4061
|
+
};
|
|
4062
|
+
const start = async () => {
|
|
4063
|
+
socket = await applyToken();
|
|
4064
|
+
if (stopped) {
|
|
4065
|
+
teardown();
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
4068
|
+
if (!socket) return;
|
|
4069
|
+
refreshId = setIntervalFn(() => {
|
|
4070
|
+
if (!socket) return;
|
|
4071
|
+
applyToken(socket).catch((error) => {
|
|
4072
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4073
|
+
console.error("[realtime] token refresh failed", message);
|
|
4074
|
+
});
|
|
4075
|
+
}, REALTIME_REFRESH_MS);
|
|
4076
|
+
if (stopped) teardown();
|
|
4077
|
+
};
|
|
4078
|
+
start().catch((error) => {
|
|
4079
|
+
if (stopped) return;
|
|
4080
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4081
|
+
console.error("[realtime] listenOrder failed", message);
|
|
4082
|
+
});
|
|
4083
|
+
return () => {
|
|
4084
|
+
stopped = true;
|
|
4085
|
+
teardown();
|
|
4086
|
+
};
|
|
4087
|
+
};
|
|
4088
|
+
//#endregion
|
|
4089
|
+
//#region src/index.ts
|
|
4090
|
+
const realtime = {
|
|
4091
|
+
listen,
|
|
4092
|
+
listenKiosk,
|
|
4093
|
+
listenOrder
|
|
4094
|
+
};
|
|
4095
|
+
//#endregion
|
|
4096
|
+
export { AVAILABILITY_CHANGED_EVENT, CMS_CHANGED_EVENT, KIOSK_COMMAND_EVENT, KIOSK_CONFIG_CHANGED_EVENT, MENU_CHANGED_EVENT, ORDER_CHANGED_EVENT, listen, listenKiosk, listenOrder, realtime };
|