@rlanz/socket 0.0.1-5 → 0.0.1-7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -160
- package/build/{types-C7Q36ryo.d.ts → base_channel-Dr40l7CW.d.ts} +178 -169
- package/build/chunk-2X6WGCH5.js +120 -0
- package/build/chunk-2X6WGCH5.js.map +1 -0
- package/build/chunk-6PJ4ALJD.js +115 -0
- package/build/chunk-6PJ4ALJD.js.map +1 -0
- package/build/chunk-E6ILCRL3.js +182 -0
- package/build/chunk-E6ILCRL3.js.map +1 -0
- package/build/{chunk-SHH6U4CI.js → chunk-G75J233Z.js} +1 -1
- package/build/chunk-G75J233Z.js.map +1 -0
- package/build/{chunk-B4Y3TNDI.js → chunk-IUQ3GYZZ.js} +5 -24
- package/build/chunk-IUQ3GYZZ.js.map +1 -0
- package/build/chunk-QNOOQXYY.js +899 -0
- package/build/chunk-QNOOQXYY.js.map +1 -0
- package/build/{chunk-ANT2E3LT.js → chunk-YVC7IMDN.js} +21 -5
- package/build/chunk-YVC7IMDN.js.map +1 -0
- package/build/framework-B-4kGmAw.d.ts +13 -0
- package/build/index-Fg-tkHhP.d.ts +150 -0
- package/build/index.d.ts +23 -12
- package/build/index.js +12 -14
- package/build/index.js.map +1 -1
- package/build/providers/socket_provider.d.ts +2 -2
- package/build/providers/socket_provider.js +474 -215
- package/build/providers/socket_provider.js.map +1 -1
- package/build/services/socket.d.ts +2 -2
- package/build/{socket_service-mybqv1hj.d.ts → socket_service-BYEB8LIN.d.ts} +7 -7
- package/build/src/assembler_hook.d.ts +6 -4
- package/build/src/assembler_hook.js +49 -63
- package/build/src/assembler_hook.js.map +1 -1
- package/build/src/client/index.d.ts +3 -147
- package/build/src/client/index.js +4 -837
- package/build/src/client/index.js.map +1 -1
- package/build/src/client/react.d.ts +13 -5
- package/build/src/client/react.js +27 -11
- package/build/src/client/react.js.map +1 -1
- package/build/src/client/types.d.ts +30 -35
- package/build/src/client/vue.d.ts +17 -5
- package/build/src/client/vue.js +31 -13
- package/build/src/client/vue.js.map +1 -1
- package/build/src/decorators.d.ts +3 -2
- package/build/src/decorators.js +1 -1
- package/build/src/health_check.d.ts +2 -2
- package/build/src/health_check.js +14 -3
- package/build/src/health_check.js.map +1 -1
- package/build/src/otel.js +0 -1
- package/build/src/otel.js.map +1 -1
- package/build/src/testing.d.ts +3 -12
- package/build/src/testing.js +1 -1
- package/build/src/types/tracing_channels.d.ts +1 -2
- package/build/src/types.d.ts +1 -1
- package/package.json +2 -9
- package/build/chunk-ANT2E3LT.js.map +0 -1
- package/build/chunk-B4Y3TNDI.js.map +0 -1
- package/build/chunk-HK7Z65DA.js +0 -19
- package/build/chunk-HK7Z65DA.js.map +0 -1
- package/build/chunk-SFAY2ZA4.js +0 -28
- package/build/chunk-SFAY2ZA4.js.map +0 -1
- package/build/chunk-SHH6U4CI.js.map +0 -1
- package/build/chunk-YAX5EHHB.js +0 -453
- package/build/chunk-YAX5EHHB.js.map +0 -1
- package/build/framework-CvxgHwbv.d.ts +0 -12
- package/build/src/client/svelte.d.ts +0 -22
- package/build/src/client/svelte.js +0 -82
- package/build/src/client/svelte.js.map +0 -1
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BindableChannelPattern
|
|
3
|
+
} from "./chunk-6PJ4ALJD.js";
|
|
4
|
+
|
|
5
|
+
// src/client/callback.ts
|
|
6
|
+
function reportClientError(error) {
|
|
7
|
+
try {
|
|
8
|
+
const reportError = globalThis.reportError;
|
|
9
|
+
if (typeof reportError === "function") {
|
|
10
|
+
reportError(error);
|
|
11
|
+
} else {
|
|
12
|
+
console.error(error);
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function callClientHandler(handler, data) {
|
|
18
|
+
try {
|
|
19
|
+
const result = Reflect.apply(handler, void 0, [data]);
|
|
20
|
+
if (result && typeof result.then === "function") {
|
|
21
|
+
void Promise.resolve(result).catch(reportClientError);
|
|
22
|
+
}
|
|
23
|
+
} catch (error) {
|
|
24
|
+
reportClientError(error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/client/presence_snapshot.ts
|
|
29
|
+
var ClientPresenceSnapshot = class _ClientPresenceSnapshot {
|
|
30
|
+
count;
|
|
31
|
+
users;
|
|
32
|
+
#usersById;
|
|
33
|
+
constructor(users) {
|
|
34
|
+
this.users = Object.freeze(users.map((user) => Object.freeze({ ...user })));
|
|
35
|
+
this.count = this.users.length;
|
|
36
|
+
this.#usersById = new Map(this.users.map((user) => [user.id, user]));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validate one transport snapshot for the expected channel.
|
|
40
|
+
*/
|
|
41
|
+
static fromTransport(value, expectedChannel) {
|
|
42
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const data = value;
|
|
46
|
+
if (data.channel !== expectedChannel || !Array.isArray(data.users) || !Number.isInteger(data.count) || data.count !== data.users.length) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const users = [];
|
|
50
|
+
const userIds = /* @__PURE__ */ new Set();
|
|
51
|
+
for (const candidate of data.users) {
|
|
52
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate) || typeof candidate.id !== "string" || typeof candidate.joinedAt !== "string" || userIds.has(candidate.id)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
userIds.add(candidate.id);
|
|
56
|
+
users.push(candidate);
|
|
57
|
+
}
|
|
58
|
+
return new _ClientPresenceSnapshot(users);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Return users that joined or left since the previous snapshot.
|
|
62
|
+
*/
|
|
63
|
+
diff(previous) {
|
|
64
|
+
if (!previous) {
|
|
65
|
+
return { joining: this.users, leaving: [] };
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
joining: this.users.filter((user) => !previous.#usersById.has(user.id)),
|
|
69
|
+
leaving: previous.users.filter((user) => !this.#usersById.has(user.id))
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/client/channel.ts
|
|
75
|
+
var CLIENT_EVENT_PREFIX = "client:";
|
|
76
|
+
var Channel = class {
|
|
77
|
+
/**
|
|
78
|
+
* Channel name.
|
|
79
|
+
*/
|
|
80
|
+
name;
|
|
81
|
+
/**
|
|
82
|
+
* Socket client transport.
|
|
83
|
+
*/
|
|
84
|
+
#transport;
|
|
85
|
+
/**
|
|
86
|
+
* Whether the channel should stay subscribed.
|
|
87
|
+
*/
|
|
88
|
+
#subscribed = false;
|
|
89
|
+
#active = false;
|
|
90
|
+
/**
|
|
91
|
+
* Event handlers grouped by event type.
|
|
92
|
+
*/
|
|
93
|
+
#eventHandlers = /* @__PURE__ */ new Map();
|
|
94
|
+
#managedEventHandlers = /* @__PURE__ */ new Map();
|
|
95
|
+
/**
|
|
96
|
+
* Current presence data.
|
|
97
|
+
*/
|
|
98
|
+
#presence = null;
|
|
99
|
+
/**
|
|
100
|
+
* Handler registered through .here().
|
|
101
|
+
*/
|
|
102
|
+
#hereHandler = null;
|
|
103
|
+
/**
|
|
104
|
+
* Handler registered through .joining().
|
|
105
|
+
*/
|
|
106
|
+
#joiningHandler = null;
|
|
107
|
+
/**
|
|
108
|
+
* Handler registered through .leaving().
|
|
109
|
+
*/
|
|
110
|
+
#leavingHandler = null;
|
|
111
|
+
/**
|
|
112
|
+
* Internal transport handlers to clean up on unsubscribe.
|
|
113
|
+
*/
|
|
114
|
+
#boundPresenceHandler = null;
|
|
115
|
+
#boundEventHandler = null;
|
|
116
|
+
#boundDisconnectHandler = null;
|
|
117
|
+
#listenersAttached = false;
|
|
118
|
+
#pendingSubscribe = null;
|
|
119
|
+
#subscribeToken = 0;
|
|
120
|
+
#managedSubscriptions = 0;
|
|
121
|
+
#pendingUnsubscribe = null;
|
|
122
|
+
constructor(name, transport) {
|
|
123
|
+
this.name = name;
|
|
124
|
+
this.#transport = transport;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Whether the channel should stay subscribed.
|
|
128
|
+
*/
|
|
129
|
+
get subscribed() {
|
|
130
|
+
return this.#subscribed || this.#managedSubscriptions > 0;
|
|
131
|
+
}
|
|
132
|
+
/** Whether a framework lifecycle currently owns this channel. @internal */
|
|
133
|
+
get $managed() {
|
|
134
|
+
return this.#managedSubscriptions > 0;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Whether the channel is active on the server.
|
|
138
|
+
*/
|
|
139
|
+
get active() {
|
|
140
|
+
return this.#active;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* List of present users.
|
|
144
|
+
*/
|
|
145
|
+
get users() {
|
|
146
|
+
return [...this.#presence?.users ?? []];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Number of present users.
|
|
150
|
+
*/
|
|
151
|
+
get count() {
|
|
152
|
+
return this.#presence?.count ?? 0;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Registers a handler called with the present users.
|
|
156
|
+
* Called immediately on subscribe and after each presence change.
|
|
157
|
+
*/
|
|
158
|
+
here(handler) {
|
|
159
|
+
this.#hereHandler = handler;
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Registers a handler called when a user joins.
|
|
164
|
+
*/
|
|
165
|
+
joining(handler) {
|
|
166
|
+
this.#joiningHandler = handler;
|
|
167
|
+
return this;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Registers a handler called when a user leaves.
|
|
171
|
+
*/
|
|
172
|
+
leaving(handler) {
|
|
173
|
+
this.#leavingHandler = handler;
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
listen(event, handler) {
|
|
177
|
+
const handlers = this.#eventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
178
|
+
handlers.add(handler);
|
|
179
|
+
this.#eventHandlers.set(event, handlers);
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
stopListening(event, handler) {
|
|
183
|
+
if (handler) {
|
|
184
|
+
this.#eventHandlers.get(event)?.delete(handler);
|
|
185
|
+
} else {
|
|
186
|
+
this.#eventHandlers.delete(event);
|
|
187
|
+
}
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
/** Registers a lifecycle-owned event handler. @internal */
|
|
191
|
+
$listen(event, handler) {
|
|
192
|
+
const handlers = this.#managedEventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
193
|
+
handlers.add(handler);
|
|
194
|
+
this.#managedEventHandlers.set(event, handlers);
|
|
195
|
+
}
|
|
196
|
+
/** Removes a lifecycle-owned event handler. @internal */
|
|
197
|
+
$stopListening(event, handler) {
|
|
198
|
+
const handlers = this.#managedEventHandlers.get(event);
|
|
199
|
+
handlers?.delete(handler);
|
|
200
|
+
if (handlers?.size === 0) this.#managedEventHandlers.delete(event);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Listens for a client event whispered by another channel member.
|
|
204
|
+
*/
|
|
205
|
+
listenForWhisper(event, handler) {
|
|
206
|
+
const handlers = this.#eventHandlers.get(`${CLIENT_EVENT_PREFIX}${event}`) ?? /* @__PURE__ */ new Set();
|
|
207
|
+
handlers.add(handler);
|
|
208
|
+
this.#eventHandlers.set(`${CLIENT_EVENT_PREFIX}${event}`, handlers);
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Removes a whispered client event listener.
|
|
213
|
+
*/
|
|
214
|
+
stopListeningForWhisper(event, handler) {
|
|
215
|
+
const name = `${CLIENT_EVENT_PREFIX}${event}`;
|
|
216
|
+
if (handler) this.#eventHandlers.get(name)?.delete(handler);
|
|
217
|
+
else this.#eventHandlers.delete(name);
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Subscribes to the channel.
|
|
222
|
+
*
|
|
223
|
+
* client listeners -> subscribe ack -> active channel
|
|
224
|
+
* | | |
|
|
225
|
+
* v v v
|
|
226
|
+
* events ready presence sync send/listen
|
|
227
|
+
*/
|
|
228
|
+
async subscribe(options = {}) {
|
|
229
|
+
this.#subscribed = true;
|
|
230
|
+
return this.#ensureSubscribed(options);
|
|
231
|
+
}
|
|
232
|
+
/** Acquires subscription intent without overriding a direct consumer. @internal */
|
|
233
|
+
$acquire(options = {}) {
|
|
234
|
+
this.#managedSubscriptions++;
|
|
235
|
+
const ready = this.#transport.connected ? this.#ensureSubscribed(options) : Promise.resolve(this);
|
|
236
|
+
let released = false;
|
|
237
|
+
return {
|
|
238
|
+
ready,
|
|
239
|
+
release: () => {
|
|
240
|
+
if (released) return this.#pendingUnsubscribe ?? Promise.resolve();
|
|
241
|
+
released = true;
|
|
242
|
+
this.#managedSubscriptions--;
|
|
243
|
+
return this.#unsubscribeIfUnused();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/** Restores desired subscriptions after the transport reconnects. @internal */
|
|
248
|
+
$resubscribe(options = {}) {
|
|
249
|
+
return this.#ensureSubscribed(options);
|
|
250
|
+
}
|
|
251
|
+
async #ensureSubscribed(options) {
|
|
252
|
+
if (this.#pendingUnsubscribe) {
|
|
253
|
+
await this.#pendingUnsubscribe;
|
|
254
|
+
}
|
|
255
|
+
if (!this.subscribed) return this;
|
|
256
|
+
if (this.#pendingSubscribe) {
|
|
257
|
+
await this.#pendingSubscribe.promise;
|
|
258
|
+
return this;
|
|
259
|
+
}
|
|
260
|
+
if (this.#active) {
|
|
261
|
+
return this;
|
|
262
|
+
}
|
|
263
|
+
const timeout = options.timeout ?? 5e3;
|
|
264
|
+
if (!this.#listenersAttached) {
|
|
265
|
+
this.#setupSocketListeners();
|
|
266
|
+
}
|
|
267
|
+
const token = ++this.#subscribeToken;
|
|
268
|
+
const deferred = Promise.withResolvers();
|
|
269
|
+
this.#pendingSubscribe = {
|
|
270
|
+
promise: deferred.promise,
|
|
271
|
+
resolve: deferred.resolve,
|
|
272
|
+
reject: deferred.reject,
|
|
273
|
+
token
|
|
274
|
+
};
|
|
275
|
+
const { resolve, reject } = this.#pendingSubscribe;
|
|
276
|
+
const timer = setTimeout(() => {
|
|
277
|
+
if (token !== this.#subscribeToken) return;
|
|
278
|
+
this.#subscribeToken++;
|
|
279
|
+
this.#pendingSubscribe = null;
|
|
280
|
+
this.#resetConnectionState();
|
|
281
|
+
reject(new Error(`Subscribe timeout for channel: ${this.name}`));
|
|
282
|
+
}, timeout);
|
|
283
|
+
this.#transport.sendRequest({ type: "subscribe", channel: this.name }, timeout).then((response) => {
|
|
284
|
+
if (token !== this.#subscribeToken) return;
|
|
285
|
+
clearTimeout(timer);
|
|
286
|
+
this.#pendingSubscribe = null;
|
|
287
|
+
if (response.ok) {
|
|
288
|
+
this.#active = true;
|
|
289
|
+
const presenceData = ClientPresenceSnapshot.fromTransport(
|
|
290
|
+
response.data?.presenceData,
|
|
291
|
+
this.name
|
|
292
|
+
);
|
|
293
|
+
if (presenceData) {
|
|
294
|
+
this.#handleInitialPresence(presenceData);
|
|
295
|
+
}
|
|
296
|
+
resolve(this);
|
|
297
|
+
} else {
|
|
298
|
+
this.#resetConnectionState();
|
|
299
|
+
reject(new Error(response.error ?? "Subscribe failed"));
|
|
300
|
+
}
|
|
301
|
+
}).catch((error) => {
|
|
302
|
+
if (token !== this.#subscribeToken) return;
|
|
303
|
+
clearTimeout(timer);
|
|
304
|
+
this.#pendingSubscribe = null;
|
|
305
|
+
this.#resetConnectionState();
|
|
306
|
+
reject(error instanceof Error ? error : new Error("Subscribe failed"));
|
|
307
|
+
});
|
|
308
|
+
await deferred.promise;
|
|
309
|
+
return this;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Unsubscribes from the channel.
|
|
313
|
+
*/
|
|
314
|
+
async unsubscribe(options = {}) {
|
|
315
|
+
this.#subscribed = false;
|
|
316
|
+
this.#clearUserCallbacks();
|
|
317
|
+
return this.#unsubscribeIfUnused(options);
|
|
318
|
+
}
|
|
319
|
+
#unsubscribeIfUnused(options = {}) {
|
|
320
|
+
if (this.subscribed) return Promise.resolve();
|
|
321
|
+
if (this.#pendingUnsubscribe) return this.#pendingUnsubscribe;
|
|
322
|
+
const hadPendingSubscribe = this.#pendingSubscribe !== null;
|
|
323
|
+
this.#subscribeToken++;
|
|
324
|
+
if (this.#pendingSubscribe) {
|
|
325
|
+
this.#pendingSubscribe.reject(new Error(`Unsubscribed from channel: ${this.name}`));
|
|
326
|
+
this.#pendingSubscribe = null;
|
|
327
|
+
}
|
|
328
|
+
if (!hadPendingSubscribe && !this.#active) {
|
|
329
|
+
this.#cleanupTerminal();
|
|
330
|
+
return Promise.resolve();
|
|
331
|
+
}
|
|
332
|
+
this.#active = false;
|
|
333
|
+
const pending = new Promise((resolve) => {
|
|
334
|
+
const timeout = options.timeout ?? 5e3;
|
|
335
|
+
const timer = setTimeout(() => {
|
|
336
|
+
resolve();
|
|
337
|
+
}, timeout);
|
|
338
|
+
this.#transport.sendRequest({ type: "unsubscribe", channel: this.name }, timeout).then(() => {
|
|
339
|
+
clearTimeout(timer);
|
|
340
|
+
resolve();
|
|
341
|
+
}).catch(() => {
|
|
342
|
+
clearTimeout(timer);
|
|
343
|
+
resolve();
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
this.#pendingUnsubscribe = pending;
|
|
347
|
+
void pending.finally(() => {
|
|
348
|
+
if (this.#pendingUnsubscribe !== pending) return;
|
|
349
|
+
this.#pendingUnsubscribe = null;
|
|
350
|
+
this.#resetConnectionState();
|
|
351
|
+
this.#detachTransportListeners();
|
|
352
|
+
if (!this.subscribed) this.#clearCallbacks();
|
|
353
|
+
});
|
|
354
|
+
return pending;
|
|
355
|
+
}
|
|
356
|
+
send(event, data) {
|
|
357
|
+
if (!this.#active) {
|
|
358
|
+
console.warn(`Cannot send to channel ${this.name}: not subscribed`);
|
|
359
|
+
return this;
|
|
360
|
+
}
|
|
361
|
+
const payload = {
|
|
362
|
+
channel: this.name,
|
|
363
|
+
event,
|
|
364
|
+
data
|
|
365
|
+
};
|
|
366
|
+
this.#transport.send({ type: "message", ...payload });
|
|
367
|
+
return this;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Relays a client event to other members of this channel.
|
|
371
|
+
*/
|
|
372
|
+
whisper(event, data) {
|
|
373
|
+
if (!this.#active) {
|
|
374
|
+
console.warn(`Cannot whisper to channel ${this.name}: not subscribed`);
|
|
375
|
+
return this;
|
|
376
|
+
}
|
|
377
|
+
const payload = {
|
|
378
|
+
channel: this.name,
|
|
379
|
+
event,
|
|
380
|
+
data
|
|
381
|
+
};
|
|
382
|
+
this.#transport.send({ type: "whisper", ...payload });
|
|
383
|
+
return this;
|
|
384
|
+
}
|
|
385
|
+
async sendWithAck(event, data) {
|
|
386
|
+
if (!this.#active) {
|
|
387
|
+
throw new Error(`Cannot send to channel ${this.name}: not subscribed`);
|
|
388
|
+
}
|
|
389
|
+
const payload = {
|
|
390
|
+
channel: this.name,
|
|
391
|
+
event,
|
|
392
|
+
data
|
|
393
|
+
};
|
|
394
|
+
const response = await this.#transport.sendRequest({
|
|
395
|
+
type: "message",
|
|
396
|
+
...payload
|
|
397
|
+
});
|
|
398
|
+
if (!response?.ok) {
|
|
399
|
+
throw new Error(response?.error ?? `Channel handler failed: ${event}`);
|
|
400
|
+
}
|
|
401
|
+
return response.data;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Configures transport listeners.
|
|
405
|
+
*/
|
|
406
|
+
#setupSocketListeners() {
|
|
407
|
+
this.#boundPresenceHandler = (data) => {
|
|
408
|
+
const presenceData = ClientPresenceSnapshot.fromTransport(data, this.name);
|
|
409
|
+
if (!presenceData) return;
|
|
410
|
+
this.#handlePresenceUpdate(presenceData);
|
|
411
|
+
};
|
|
412
|
+
this.#transport.on(`channel:${this.name}:presence:update`, this.#boundPresenceHandler);
|
|
413
|
+
this.#boundEventHandler = (data) => {
|
|
414
|
+
if (!data || typeof data !== "object") return;
|
|
415
|
+
const payload = data;
|
|
416
|
+
if (!payload.event) return;
|
|
417
|
+
const handlers = this.#eventHandlers.get(payload.event);
|
|
418
|
+
handlers?.forEach((handler) => callClientHandler(handler, payload.data));
|
|
419
|
+
const managedHandlers = this.#managedEventHandlers.get(payload.event);
|
|
420
|
+
managedHandlers?.forEach((handler) => callClientHandler(handler, payload.data));
|
|
421
|
+
};
|
|
422
|
+
this.#transport.on(`channel:${this.name}:event`, this.#boundEventHandler);
|
|
423
|
+
this.#boundDisconnectHandler = () => {
|
|
424
|
+
this.#resetConnectionState();
|
|
425
|
+
};
|
|
426
|
+
this.#transport.on("disconnect", this.#boundDisconnectHandler);
|
|
427
|
+
this.#listenersAttached = true;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Handles the initial presence snapshot.
|
|
431
|
+
*/
|
|
432
|
+
#handleInitialPresence(data) {
|
|
433
|
+
this.#presence = data;
|
|
434
|
+
if (this.#hereHandler) {
|
|
435
|
+
callClientHandler(this.#hereHandler, [...data.users]);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Handles a presence update.
|
|
440
|
+
*
|
|
441
|
+
* previous IDs current IDs
|
|
442
|
+
* | |
|
|
443
|
+
* +-- diff in -----+--> joining / leaving callbacks
|
|
444
|
+
*/
|
|
445
|
+
#handlePresenceUpdate(data) {
|
|
446
|
+
const { joining, leaving } = data.diff(this.#presence);
|
|
447
|
+
this.#presence = data;
|
|
448
|
+
for (const user of joining) {
|
|
449
|
+
if (this.#joiningHandler) {
|
|
450
|
+
callClientHandler(this.#joiningHandler, user);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
for (const user of leaving) {
|
|
454
|
+
if (this.#leavingHandler) {
|
|
455
|
+
callClientHandler(this.#leavingHandler, user);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (this.#hereHandler) {
|
|
459
|
+
callClientHandler(this.#hereHandler, [...data.users]);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
#resetConnectionState() {
|
|
463
|
+
this.#active = false;
|
|
464
|
+
this.#presence = null;
|
|
465
|
+
}
|
|
466
|
+
#detachTransportListeners() {
|
|
467
|
+
if (this.#boundPresenceHandler) {
|
|
468
|
+
this.#transport.off(`channel:${this.name}:presence:update`, this.#boundPresenceHandler);
|
|
469
|
+
this.#boundPresenceHandler = null;
|
|
470
|
+
}
|
|
471
|
+
if (this.#boundEventHandler) {
|
|
472
|
+
this.#transport.off(`channel:${this.name}:event`, this.#boundEventHandler);
|
|
473
|
+
this.#boundEventHandler = null;
|
|
474
|
+
}
|
|
475
|
+
if (this.#boundDisconnectHandler) {
|
|
476
|
+
this.#transport.off("disconnect", this.#boundDisconnectHandler);
|
|
477
|
+
this.#boundDisconnectHandler = null;
|
|
478
|
+
}
|
|
479
|
+
this.#listenersAttached = false;
|
|
480
|
+
}
|
|
481
|
+
#clearUserCallbacks() {
|
|
482
|
+
this.#eventHandlers.clear();
|
|
483
|
+
this.#hereHandler = null;
|
|
484
|
+
this.#joiningHandler = null;
|
|
485
|
+
this.#leavingHandler = null;
|
|
486
|
+
}
|
|
487
|
+
#clearCallbacks() {
|
|
488
|
+
this.#clearUserCallbacks();
|
|
489
|
+
this.#managedEventHandlers.clear();
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Performs terminal cleanup after the user leaves or the socket is disposed.
|
|
493
|
+
*/
|
|
494
|
+
#cleanupTerminal() {
|
|
495
|
+
this.#resetConnectionState();
|
|
496
|
+
this.#detachTransportListeners();
|
|
497
|
+
this.#clearCallbacks();
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
// src/client/socket_session.ts
|
|
502
|
+
var ClientSocketSession = class {
|
|
503
|
+
#ws = null;
|
|
504
|
+
#state = "disconnected";
|
|
505
|
+
#connectPromise = null;
|
|
506
|
+
#cancelConnect = null;
|
|
507
|
+
#stateHandlers = /* @__PURE__ */ new Set();
|
|
508
|
+
#eventHandlers = /* @__PURE__ */ new Map();
|
|
509
|
+
#pendingRequests = /* @__PURE__ */ new Map();
|
|
510
|
+
#requestId = 0;
|
|
511
|
+
#reconnectAttempts = 0;
|
|
512
|
+
#reconnectTimer = null;
|
|
513
|
+
#manualDisconnect = false;
|
|
514
|
+
#buildUrl;
|
|
515
|
+
#createWebSocket;
|
|
516
|
+
#autoReconnect;
|
|
517
|
+
#reconnectDelay;
|
|
518
|
+
#reconnectMaxDelay;
|
|
519
|
+
#onConnected;
|
|
520
|
+
constructor(options) {
|
|
521
|
+
this.#buildUrl = options.buildUrl;
|
|
522
|
+
this.#createWebSocket = options.createWebSocket ?? ((url) => new WebSocket(url));
|
|
523
|
+
this.#autoReconnect = options.autoReconnect ?? true;
|
|
524
|
+
this.#reconnectDelay = Math.max(0, options.reconnectDelay ?? 250);
|
|
525
|
+
this.#reconnectMaxDelay = Math.max(this.#reconnectDelay, options.reconnectMaxDelay ?? 5e3);
|
|
526
|
+
this.#onConnected = options.onConnected;
|
|
527
|
+
}
|
|
528
|
+
get state() {
|
|
529
|
+
return this.#state;
|
|
530
|
+
}
|
|
531
|
+
get connected() {
|
|
532
|
+
return this.#state === "connected";
|
|
533
|
+
}
|
|
534
|
+
connect() {
|
|
535
|
+
if (this.#state === "connected") {
|
|
536
|
+
return Promise.resolve();
|
|
537
|
+
}
|
|
538
|
+
if (this.#connectPromise) {
|
|
539
|
+
return this.#connectPromise;
|
|
540
|
+
}
|
|
541
|
+
this.#manualDisconnect = false;
|
|
542
|
+
this.#setState("connecting");
|
|
543
|
+
let setupFailed = false;
|
|
544
|
+
const connectPromise = new Promise((resolve, reject) => {
|
|
545
|
+
let ws;
|
|
546
|
+
try {
|
|
547
|
+
ws = this.#createWebSocket(this.#buildUrl());
|
|
548
|
+
} catch (error) {
|
|
549
|
+
setupFailed = true;
|
|
550
|
+
reject(error);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
this.#ws = ws;
|
|
554
|
+
let settled = false;
|
|
555
|
+
const cleanup = () => {
|
|
556
|
+
ws.removeEventListener("open", handleOpen);
|
|
557
|
+
ws.removeEventListener("error", handleError);
|
|
558
|
+
if (this.#connectPromise === connectPromise) {
|
|
559
|
+
this.#connectPromise = null;
|
|
560
|
+
}
|
|
561
|
+
if (this.#cancelConnect === cancelConnect) {
|
|
562
|
+
this.#cancelConnect = null;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
const cancelConnect = (error) => {
|
|
566
|
+
if (settled) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
settled = true;
|
|
570
|
+
cleanup();
|
|
571
|
+
reject(error);
|
|
572
|
+
};
|
|
573
|
+
const handleOpen = () => {
|
|
574
|
+
if (settled || this.#ws !== ws) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
this.#setState("connected");
|
|
578
|
+
if (settled || this.#ws !== ws || !this.connected) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
settled = true;
|
|
582
|
+
cleanup();
|
|
583
|
+
this.#reconnectAttempts = 0;
|
|
584
|
+
this.#emitLocal("connect");
|
|
585
|
+
if (this.#onConnected) {
|
|
586
|
+
this.#callHandler(this.#onConnected);
|
|
587
|
+
}
|
|
588
|
+
resolve();
|
|
589
|
+
};
|
|
590
|
+
const handleError = () => {
|
|
591
|
+
cancelConnect(new Error("WebSocket connection failed"));
|
|
592
|
+
if (this.#ws === ws) {
|
|
593
|
+
this.#setState("disconnected");
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
const handleClose = () => {
|
|
597
|
+
cancelConnect(new Error("WebSocket connection failed"));
|
|
598
|
+
if (this.#ws === ws) {
|
|
599
|
+
this.#handleClose();
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
this.#cancelConnect = cancelConnect;
|
|
603
|
+
ws.addEventListener("open", handleOpen);
|
|
604
|
+
ws.addEventListener("error", handleError);
|
|
605
|
+
ws.addEventListener("message", (event) => {
|
|
606
|
+
if (this.#ws === ws) {
|
|
607
|
+
this.#handleIncoming(event.data);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
ws.addEventListener("close", handleClose);
|
|
611
|
+
});
|
|
612
|
+
this.#connectPromise = connectPromise;
|
|
613
|
+
if (setupFailed) {
|
|
614
|
+
this.#connectPromise = null;
|
|
615
|
+
this.#setState("disconnected");
|
|
616
|
+
}
|
|
617
|
+
return connectPromise;
|
|
618
|
+
}
|
|
619
|
+
disconnect() {
|
|
620
|
+
this.#manualDisconnect = true;
|
|
621
|
+
if (this.#reconnectTimer) {
|
|
622
|
+
clearTimeout(this.#reconnectTimer);
|
|
623
|
+
this.#reconnectTimer = null;
|
|
624
|
+
}
|
|
625
|
+
this.#cancelConnect?.(new Error("Socket disconnected"));
|
|
626
|
+
this.#reconnectAttempts = 0;
|
|
627
|
+
const ws = this.#ws;
|
|
628
|
+
const shouldEmitDisconnect = this.#state !== "disconnected";
|
|
629
|
+
this.#ws = null;
|
|
630
|
+
this.#setState("disconnected");
|
|
631
|
+
if (shouldEmitDisconnect) {
|
|
632
|
+
this.#emitLocal("disconnect");
|
|
633
|
+
}
|
|
634
|
+
ws?.close();
|
|
635
|
+
this.#rejectPending(new Error("Socket disconnected"));
|
|
636
|
+
}
|
|
637
|
+
onStateChange(handler) {
|
|
638
|
+
this.#stateHandlers.add(handler);
|
|
639
|
+
return () => {
|
|
640
|
+
this.#stateHandlers.delete(handler);
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
on(event, handler) {
|
|
644
|
+
if (!this.#eventHandlers.has(event)) {
|
|
645
|
+
this.#eventHandlers.set(event, /* @__PURE__ */ new Set());
|
|
646
|
+
}
|
|
647
|
+
this.#eventHandlers.get(event).add(handler);
|
|
648
|
+
return () => {
|
|
649
|
+
this.off(event, handler);
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
off(event, handler) {
|
|
653
|
+
this.#eventHandlers.get(event)?.delete(handler);
|
|
654
|
+
}
|
|
655
|
+
send(message) {
|
|
656
|
+
const ws = this.#ws;
|
|
657
|
+
if (ws?.readyState !== WebSocket.OPEN) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
ws.send(JSON.stringify(message));
|
|
661
|
+
}
|
|
662
|
+
sendRequest(message, timeout = 5e3) {
|
|
663
|
+
const ws = this.#ws;
|
|
664
|
+
if (ws?.readyState !== WebSocket.OPEN) {
|
|
665
|
+
return Promise.reject(new Error("Socket is not connected"));
|
|
666
|
+
}
|
|
667
|
+
const id = String(++this.#requestId);
|
|
668
|
+
const payload = { ...message, id };
|
|
669
|
+
return new Promise((resolve, reject) => {
|
|
670
|
+
const timer = setTimeout(() => {
|
|
671
|
+
this.#pendingRequests.delete(id);
|
|
672
|
+
reject(new Error("Socket request timed out"));
|
|
673
|
+
}, timeout);
|
|
674
|
+
this.#pendingRequests.set(id, {
|
|
675
|
+
resolve,
|
|
676
|
+
reject,
|
|
677
|
+
timer
|
|
678
|
+
});
|
|
679
|
+
ws.send(JSON.stringify(payload));
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
#handleIncoming(raw) {
|
|
683
|
+
const message = this.#parseMessage(raw);
|
|
684
|
+
if (!message) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (message.type === "ack") {
|
|
688
|
+
this.#resolveAck(message);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (message.type === "event") {
|
|
692
|
+
if (message.channel) {
|
|
693
|
+
if (message.event === "presence:update") {
|
|
694
|
+
this.#emitLocal(`channel:${message.channel}:presence:update`, message.data);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
this.#emitLocal(`channel:${message.channel}:event`, {
|
|
698
|
+
event: message.event,
|
|
699
|
+
data: message.data
|
|
700
|
+
});
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
this.#emitLocal(message.event, message.data);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
#parseMessage(raw) {
|
|
707
|
+
try {
|
|
708
|
+
const data = typeof raw === "string" ? raw : String(raw);
|
|
709
|
+
const message = JSON.parse(data);
|
|
710
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) {
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
if (message.type === "ack" && typeof message.id === "string" && typeof message.ok === "boolean" && (message.error === void 0 || typeof message.error === "string")) {
|
|
714
|
+
return message;
|
|
715
|
+
}
|
|
716
|
+
if (message.type === "event" && typeof message.event === "string" && (message.channel === void 0 || typeof message.channel === "string")) {
|
|
717
|
+
return message;
|
|
718
|
+
}
|
|
719
|
+
return null;
|
|
720
|
+
} catch {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
#resolveAck(message) {
|
|
725
|
+
if (!message.id) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const pending = this.#pendingRequests.get(message.id);
|
|
729
|
+
if (!pending) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
clearTimeout(pending.timer);
|
|
733
|
+
this.#pendingRequests.delete(message.id);
|
|
734
|
+
if (message.ok) {
|
|
735
|
+
pending.resolve({ ok: true, data: message.data });
|
|
736
|
+
} else {
|
|
737
|
+
pending.resolve({ ok: false, error: message.error });
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
#handleClose() {
|
|
741
|
+
const shouldEmitDisconnect = this.#state !== "disconnected";
|
|
742
|
+
this.#ws = null;
|
|
743
|
+
this.#setState("disconnected");
|
|
744
|
+
if (shouldEmitDisconnect) {
|
|
745
|
+
this.#emitLocal("disconnect");
|
|
746
|
+
}
|
|
747
|
+
this.#rejectPending(new Error("Socket disconnected"));
|
|
748
|
+
this.#scheduleReconnect();
|
|
749
|
+
}
|
|
750
|
+
#scheduleReconnect() {
|
|
751
|
+
if (this.#manualDisconnect || !this.#autoReconnect || this.#reconnectTimer) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const delay = Math.min(
|
|
755
|
+
this.#reconnectDelay * 2 ** this.#reconnectAttempts,
|
|
756
|
+
this.#reconnectMaxDelay
|
|
757
|
+
);
|
|
758
|
+
this.#reconnectAttempts += 1;
|
|
759
|
+
this.#reconnectTimer = setTimeout(() => {
|
|
760
|
+
this.#reconnectTimer = null;
|
|
761
|
+
this.connect().catch(() => {
|
|
762
|
+
if (this.#state === "disconnected") {
|
|
763
|
+
this.#scheduleReconnect();
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
}, delay);
|
|
767
|
+
}
|
|
768
|
+
#rejectPending(error) {
|
|
769
|
+
for (const [id, pending] of this.#pendingRequests) {
|
|
770
|
+
clearTimeout(pending.timer);
|
|
771
|
+
pending.reject(error);
|
|
772
|
+
this.#pendingRequests.delete(id);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
#emitLocal(event, data) {
|
|
776
|
+
this.#eventHandlers.get(event)?.forEach((handler) => this.#callHandler(handler, data));
|
|
777
|
+
}
|
|
778
|
+
#setState(state) {
|
|
779
|
+
if (this.#state === state) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
this.#state = state;
|
|
783
|
+
this.#stateHandlers.forEach((handler) => this.#callHandler(handler, state));
|
|
784
|
+
}
|
|
785
|
+
#callHandler(handler, data) {
|
|
786
|
+
callClientHandler(handler, data);
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
// src/client/socket.ts
|
|
791
|
+
var resolveChannel = /* @__PURE__ */ Symbol("resolveChannel");
|
|
792
|
+
function buildChannelName(pattern, parameters = {}) {
|
|
793
|
+
const bindablePattern = BindableChannelPattern.parse(pattern);
|
|
794
|
+
if (!bindablePattern) {
|
|
795
|
+
throw new Error(`Unsupported generated channel pattern: ${pattern}`);
|
|
796
|
+
}
|
|
797
|
+
return bindablePattern.bind(parameters);
|
|
798
|
+
}
|
|
799
|
+
var Socket = class {
|
|
800
|
+
#channels = /* @__PURE__ */ new Map();
|
|
801
|
+
#url;
|
|
802
|
+
#path;
|
|
803
|
+
#session;
|
|
804
|
+
constructor(options = {}) {
|
|
805
|
+
const browserWindow = globalThis;
|
|
806
|
+
const browserOrigin = typeof globalThis === "object" && browserWindow.window?.location?.origin ? browserWindow.window.location.origin : "";
|
|
807
|
+
this.#url = options.url ?? browserOrigin;
|
|
808
|
+
this.#path = options.path ?? "/socket";
|
|
809
|
+
this.#session = new ClientSocketSession({
|
|
810
|
+
buildUrl: () => this.#buildUrl(),
|
|
811
|
+
autoReconnect: options.autoReconnect,
|
|
812
|
+
reconnectDelay: options.reconnectDelay,
|
|
813
|
+
reconnectMaxDelay: options.reconnectMaxDelay,
|
|
814
|
+
onConnected: () => {
|
|
815
|
+
void this.#resubscribeChannels();
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
get state() {
|
|
820
|
+
return this.#session.state;
|
|
821
|
+
}
|
|
822
|
+
get connected() {
|
|
823
|
+
return this.#session.connected;
|
|
824
|
+
}
|
|
825
|
+
connect() {
|
|
826
|
+
return this.#session.connect();
|
|
827
|
+
}
|
|
828
|
+
disconnect() {
|
|
829
|
+
for (const channel of this.#channels.values()) {
|
|
830
|
+
channel.unsubscribe().catch(() => {
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
this.#session.disconnect();
|
|
834
|
+
}
|
|
835
|
+
channel(...args) {
|
|
836
|
+
const [pattern, parameters] = args;
|
|
837
|
+
const name = args.length > 1 ? buildChannelName(pattern, parameters) : pattern;
|
|
838
|
+
return this[resolveChannel](name);
|
|
839
|
+
}
|
|
840
|
+
/** Resolves a channel for framework adapters after their public type checks. @internal */
|
|
841
|
+
[resolveChannel](name) {
|
|
842
|
+
if (!this.#channels.has(name)) {
|
|
843
|
+
this.#channels.set(name, new Channel(name, this));
|
|
844
|
+
}
|
|
845
|
+
return this.#channels.get(name);
|
|
846
|
+
}
|
|
847
|
+
async leave(name) {
|
|
848
|
+
const channel = this.#channels.get(name);
|
|
849
|
+
if (channel) {
|
|
850
|
+
await channel.unsubscribe();
|
|
851
|
+
if (this.#channels.get(name) === channel && !channel.subscribed) {
|
|
852
|
+
this.#channels.delete(name);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
onStateChange(handler) {
|
|
857
|
+
return this.#session.onStateChange(handler);
|
|
858
|
+
}
|
|
859
|
+
on(event, handler) {
|
|
860
|
+
return this.#session.on(event, handler);
|
|
861
|
+
}
|
|
862
|
+
off(event, handler) {
|
|
863
|
+
this.#session.off(event, handler);
|
|
864
|
+
}
|
|
865
|
+
send(message) {
|
|
866
|
+
this.#session.send(message);
|
|
867
|
+
}
|
|
868
|
+
sendRequest(message, timeout = 5e3) {
|
|
869
|
+
return this.#session.sendRequest(message, timeout);
|
|
870
|
+
}
|
|
871
|
+
#buildUrl() {
|
|
872
|
+
const browserGlobal = globalThis;
|
|
873
|
+
const url = new URL(this.#url || browserGlobal.location?.origin || "http://localhost");
|
|
874
|
+
if (url.protocol === "http:") {
|
|
875
|
+
url.protocol = "ws:";
|
|
876
|
+
} else if (url.protocol === "https:") {
|
|
877
|
+
url.protocol = "wss:";
|
|
878
|
+
} else if (url.protocol !== "ws:" && url.protocol !== "wss:") {
|
|
879
|
+
throw new Error(`Unsupported socket URL protocol: ${url.protocol}`);
|
|
880
|
+
}
|
|
881
|
+
url.pathname = this.#path;
|
|
882
|
+
return url.toString();
|
|
883
|
+
}
|
|
884
|
+
async #resubscribeChannels() {
|
|
885
|
+
for (const channel of this.#channels.values()) {
|
|
886
|
+
if (channel.subscribed && !channel.active) {
|
|
887
|
+
await channel.$resubscribe().catch(reportClientError);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
export {
|
|
894
|
+
callClientHandler,
|
|
895
|
+
resolveChannel,
|
|
896
|
+
buildChannelName,
|
|
897
|
+
Socket
|
|
898
|
+
};
|
|
899
|
+
//# sourceMappingURL=chunk-QNOOQXYY.js.map
|