universal-api-layer 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/LICENSE +21 -0
- package/README.md +534 -0
- package/SECURITY.md +9 -0
- package/dist/angular/index.d.ts +62 -0
- package/dist/angular/index.js +142 -0
- package/dist/chunk-DXYWZQW5.js +92 -0
- package/dist/chunk-HDW7BOZI.js +764 -0
- package/dist/chunk-YTM3FV65.js +19 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +205 -0
- package/dist/client-ByBlZaRg.d.ts +169 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +26 -0
- package/dist/react/index.d.ts +16 -0
- package/dist/react/index.js +227 -0
- package/dist/types-CVzD7l4p.d.ts +81 -0
- package/dist/vue/index.d.ts +23 -0
- package/dist/vue/index.js +257 -0
- package/dist/websocket/angular.d.ts +9 -0
- package/dist/websocket/angular.js +15 -0
- package/dist/websocket/index.d.ts +7 -0
- package/dist/websocket/index.js +529 -0
- package/dist/websocket/react.d.ts +8 -0
- package/dist/websocket/react.js +23 -0
- package/dist/websocket/vue.d.ts +9 -0
- package/dist/websocket/vue.js +39 -0
- package/package.json +181 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import {
|
|
2
|
+
abortable,
|
|
3
|
+
validateResponse
|
|
4
|
+
} from "../chunk-DXYWZQW5.js";
|
|
5
|
+
import {
|
|
6
|
+
ApiError
|
|
7
|
+
} from "../chunk-YTM3FV65.js";
|
|
8
|
+
|
|
9
|
+
// src/websocket/index.ts
|
|
10
|
+
var retryCodes = /* @__PURE__ */ new Set([1001, 1005, 1006, 1011, 1012, 1013, 1014]);
|
|
11
|
+
var encoder = new TextEncoder();
|
|
12
|
+
function configuration(message) {
|
|
13
|
+
throw new ApiError(message, 0, {}, void 0, "configuration");
|
|
14
|
+
}
|
|
15
|
+
function limit(value, name, minimum = 1) {
|
|
16
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > 2147483647)
|
|
17
|
+
configuration(`${name} is outside its supported integer range`);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function socketUrl(value) {
|
|
21
|
+
let url;
|
|
22
|
+
try {
|
|
23
|
+
url = new URL(value);
|
|
24
|
+
} catch {
|
|
25
|
+
return configuration("An absolute ws(s) URL is required");
|
|
26
|
+
}
|
|
27
|
+
if (!["ws:", "wss:"].includes(url.protocol) || url.username || url.password || value.includes("#") || value.includes("\\"))
|
|
28
|
+
configuration("Invalid WebSocket URL");
|
|
29
|
+
return url;
|
|
30
|
+
}
|
|
31
|
+
function byteSize(value) {
|
|
32
|
+
if (typeof value === "string") return encoder.encode(value).byteLength;
|
|
33
|
+
if (value instanceof Blob) return value.size;
|
|
34
|
+
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value))
|
|
35
|
+
return value.byteLength;
|
|
36
|
+
return configuration("Unsupported WebSocket message data");
|
|
37
|
+
}
|
|
38
|
+
function validateClose(code, reason) {
|
|
39
|
+
if (code !== 1e3 && (!Number.isInteger(code) || code < 3e3 || code > 4999))
|
|
40
|
+
configuration("Close code must be 1000 or between 3000 and 4999");
|
|
41
|
+
if (typeof reason !== "string" || encoder.encode(reason).length > 123)
|
|
42
|
+
configuration("Close reason must be at most 123 UTF-8 bytes");
|
|
43
|
+
}
|
|
44
|
+
function createSocketClient(config) {
|
|
45
|
+
const options = { ...config };
|
|
46
|
+
const format = options.format ?? "json";
|
|
47
|
+
if (format !== "json" && format !== "raw")
|
|
48
|
+
configuration("Socket format must be json or raw");
|
|
49
|
+
const connectTimeout = limit(
|
|
50
|
+
options.connectTimeoutMs ?? 1e4,
|
|
51
|
+
"connectTimeoutMs"
|
|
52
|
+
);
|
|
53
|
+
const messageLimit = limit(
|
|
54
|
+
options.maxMessageSize ?? 1048576,
|
|
55
|
+
"maxMessageSize"
|
|
56
|
+
);
|
|
57
|
+
const pendingLimit = limit(
|
|
58
|
+
options.maxPendingMessages ?? 64,
|
|
59
|
+
"maxPendingMessages"
|
|
60
|
+
);
|
|
61
|
+
const bufferedLimit = limit(
|
|
62
|
+
options.maxBufferedAmount ?? 1048576,
|
|
63
|
+
"maxBufferedAmount"
|
|
64
|
+
);
|
|
65
|
+
const reconnect = options.reconnect === false ? void 0 : {
|
|
66
|
+
maxAttempts: 5,
|
|
67
|
+
baseDelayMs: 500,
|
|
68
|
+
maxDelayMs: 3e4,
|
|
69
|
+
jitter: true,
|
|
70
|
+
...options.reconnect
|
|
71
|
+
};
|
|
72
|
+
if (reconnect) {
|
|
73
|
+
limit(reconnect.maxAttempts, "maxAttempts", 0);
|
|
74
|
+
limit(reconnect.baseDelayMs, "baseDelayMs");
|
|
75
|
+
limit(reconnect.maxDelayMs, "maxDelayMs");
|
|
76
|
+
if (reconnect.baseDelayMs > reconnect.maxDelayMs || typeof reconnect.jitter !== "boolean")
|
|
77
|
+
configuration("Invalid reconnect settings");
|
|
78
|
+
}
|
|
79
|
+
const requiresAuth = options.requiresAuth ?? Boolean(options.sessionClient);
|
|
80
|
+
if (requiresAuth && !options.sessionClient)
|
|
81
|
+
configuration("requiresAuth requires a sessionClient");
|
|
82
|
+
if (typeof options.url !== "string" && typeof options.url !== "function")
|
|
83
|
+
configuration("Socket url must be a string or provider");
|
|
84
|
+
const origins = new Set(
|
|
85
|
+
(options.allowedOrigins ?? []).map((value) => {
|
|
86
|
+
const url = socketUrl(value);
|
|
87
|
+
if (url.pathname !== "/" || url.search)
|
|
88
|
+
configuration("allowedOrigins must contain exact ws(s) origins");
|
|
89
|
+
return url.origin;
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
if (typeof options.url === "string") {
|
|
93
|
+
const origin = socketUrl(options.url).origin;
|
|
94
|
+
if (options.allowedOrigins === void 0) origins.add(origin);
|
|
95
|
+
if (!origins.has(origin)) configuration("WebSocket origin is not allowed");
|
|
96
|
+
} else if (!origins.size)
|
|
97
|
+
configuration("URL providers require allowedOrigins");
|
|
98
|
+
const protocols = [...options.protocols ?? []];
|
|
99
|
+
if (new Set(protocols).size !== protocols.length || protocols.some(
|
|
100
|
+
(value) => typeof value !== "string" || !/^[!#$%&'*+\-.^_`|~\da-z]+$/i.test(value)
|
|
101
|
+
))
|
|
102
|
+
configuration("Invalid WebSocket subprotocols");
|
|
103
|
+
let snapshot = Object.freeze({
|
|
104
|
+
status: "idle",
|
|
105
|
+
reconnectAttempt: 0,
|
|
106
|
+
error: null,
|
|
107
|
+
close: null
|
|
108
|
+
});
|
|
109
|
+
const stateListeners = /* @__PURE__ */ new Set();
|
|
110
|
+
const messageListeners = /* @__PURE__ */ new Set();
|
|
111
|
+
const errorListeners = /* @__PURE__ */ new Set();
|
|
112
|
+
let wanted = false, disposed = false, generation = 0, attempts = 0, pending = 0;
|
|
113
|
+
let socket;
|
|
114
|
+
let controller;
|
|
115
|
+
let timeout, retry;
|
|
116
|
+
let detach, unsubscribeSession;
|
|
117
|
+
let revision;
|
|
118
|
+
let chain = Promise.resolve();
|
|
119
|
+
function listenerError(cause) {
|
|
120
|
+
const error = new ApiError(
|
|
121
|
+
"WebSocket listener failed",
|
|
122
|
+
0,
|
|
123
|
+
{},
|
|
124
|
+
void 0,
|
|
125
|
+
"callback",
|
|
126
|
+
cause
|
|
127
|
+
);
|
|
128
|
+
for (const listener of [...errorListeners]) {
|
|
129
|
+
try {
|
|
130
|
+
listener(error);
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function update(value) {
|
|
136
|
+
snapshot = Object.freeze({
|
|
137
|
+
...snapshot,
|
|
138
|
+
...value,
|
|
139
|
+
reconnectAttempt: attempts
|
|
140
|
+
});
|
|
141
|
+
for (const listener of [...stateListeners]) {
|
|
142
|
+
try {
|
|
143
|
+
listener();
|
|
144
|
+
} catch (cause) {
|
|
145
|
+
listenerError(cause);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function report(error) {
|
|
150
|
+
update({ error });
|
|
151
|
+
for (const listener of [...errorListeners]) {
|
|
152
|
+
try {
|
|
153
|
+
listener(error);
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function sessionValid() {
|
|
159
|
+
const state = options.sessionClient?.getSessionSnapshot();
|
|
160
|
+
return (!state || state.revision === revision) && (!requiresAuth || Boolean(state?.userId));
|
|
161
|
+
}
|
|
162
|
+
const current = (id) => wanted && !disposed && id === generation && sessionValid();
|
|
163
|
+
function release(code = 1e3, reason = "") {
|
|
164
|
+
generation++;
|
|
165
|
+
clearTimeout(timeout);
|
|
166
|
+
clearTimeout(retry);
|
|
167
|
+
timeout = retry = void 0;
|
|
168
|
+
detach?.();
|
|
169
|
+
detach = void 0;
|
|
170
|
+
controller?.abort(
|
|
171
|
+
new ApiError(
|
|
172
|
+
"WebSocket attempt cancelled",
|
|
173
|
+
0,
|
|
174
|
+
{},
|
|
175
|
+
void 0,
|
|
176
|
+
"cancelled"
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
controller = void 0;
|
|
180
|
+
const previous = socket;
|
|
181
|
+
socket = void 0;
|
|
182
|
+
pending = 0;
|
|
183
|
+
chain = Promise.resolve();
|
|
184
|
+
if (previous && previous.readyState < 2) {
|
|
185
|
+
try {
|
|
186
|
+
previous.close(code, reason);
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function stop(code = 1e3, reason = "", state = {}) {
|
|
192
|
+
wanted = false;
|
|
193
|
+
release(code, reason);
|
|
194
|
+
if (!state.keepSession) {
|
|
195
|
+
unsubscribeSession?.();
|
|
196
|
+
unsubscribeSession = void 0;
|
|
197
|
+
}
|
|
198
|
+
update({
|
|
199
|
+
status: state.status ?? "closed",
|
|
200
|
+
...state.close === void 0 ? {} : { close: state.close },
|
|
201
|
+
...state.clearError ? { error: null } : {}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function failed(error, canRetry, close = null) {
|
|
205
|
+
const id = generation;
|
|
206
|
+
report(error);
|
|
207
|
+
if (!current(id)) return;
|
|
208
|
+
release();
|
|
209
|
+
if (canRetry && reconnect && attempts < reconnect.maxAttempts) {
|
|
210
|
+
const delay = Math.min(
|
|
211
|
+
reconnect.maxDelayMs,
|
|
212
|
+
reconnect.baseDelayMs * 2 ** Math.min(attempts, 30)
|
|
213
|
+
);
|
|
214
|
+
attempts++;
|
|
215
|
+
const nextGeneration = generation;
|
|
216
|
+
update({ status: "reconnecting", close });
|
|
217
|
+
if (!current(nextGeneration)) return;
|
|
218
|
+
retry = setTimeout(
|
|
219
|
+
() => {
|
|
220
|
+
retry = void 0;
|
|
221
|
+
if (current(nextGeneration)) void attempt();
|
|
222
|
+
},
|
|
223
|
+
reconnect.jitter ? Math.floor(delay * (0.5 + Math.random() * 0.5)) : delay
|
|
224
|
+
);
|
|
225
|
+
} else {
|
|
226
|
+
wanted = false;
|
|
227
|
+
update({ status: "closed", close });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function attempt() {
|
|
231
|
+
release();
|
|
232
|
+
const id = generation;
|
|
233
|
+
if (!current(id)) return;
|
|
234
|
+
controller = new AbortController();
|
|
235
|
+
const signal = controller.signal;
|
|
236
|
+
timeout = setTimeout(() => {
|
|
237
|
+
if (current(id))
|
|
238
|
+
failed(
|
|
239
|
+
new ApiError(
|
|
240
|
+
"WebSocket connection timed out",
|
|
241
|
+
0,
|
|
242
|
+
{},
|
|
243
|
+
void 0,
|
|
244
|
+
"timeout"
|
|
245
|
+
),
|
|
246
|
+
true
|
|
247
|
+
);
|
|
248
|
+
}, connectTimeout);
|
|
249
|
+
update({ status: attempts ? "reconnecting" : "connecting", close: null });
|
|
250
|
+
try {
|
|
251
|
+
const value = typeof options.url === "string" ? options.url : await abortable(
|
|
252
|
+
() => options.url(signal),
|
|
253
|
+
signal
|
|
254
|
+
);
|
|
255
|
+
if (!current(id)) return;
|
|
256
|
+
const url = socketUrl(value);
|
|
257
|
+
if (!origins.has(url.origin))
|
|
258
|
+
configuration("WebSocket origin is not allowed");
|
|
259
|
+
const factory = options.webSocketFactory ?? ((target, protocols2) => {
|
|
260
|
+
if (typeof globalThis.WebSocket !== "function")
|
|
261
|
+
configuration("WebSocket is unavailable; provide webSocketFactory");
|
|
262
|
+
return new globalThis.WebSocket(target, protocols2);
|
|
263
|
+
});
|
|
264
|
+
const transport = factory(url.href, [...protocols]);
|
|
265
|
+
if (!current(id)) {
|
|
266
|
+
transport.close(1e3);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
socket = transport;
|
|
270
|
+
transport.binaryType = "arraybuffer";
|
|
271
|
+
const onOpen = () => {
|
|
272
|
+
if (!current(id)) return;
|
|
273
|
+
clearTimeout(timeout);
|
|
274
|
+
timeout = void 0;
|
|
275
|
+
update({ status: "open", error: null, close: null });
|
|
276
|
+
};
|
|
277
|
+
const onError = () => {
|
|
278
|
+
if (current(id))
|
|
279
|
+
report(
|
|
280
|
+
new ApiError(
|
|
281
|
+
"WebSocket transport error",
|
|
282
|
+
0,
|
|
283
|
+
{},
|
|
284
|
+
void 0,
|
|
285
|
+
"network"
|
|
286
|
+
)
|
|
287
|
+
);
|
|
288
|
+
};
|
|
289
|
+
const onClose = (event) => {
|
|
290
|
+
if (!current(id)) return;
|
|
291
|
+
const close = Object.freeze({
|
|
292
|
+
code: event.code,
|
|
293
|
+
reason: event.reason,
|
|
294
|
+
wasClean: event.wasClean
|
|
295
|
+
});
|
|
296
|
+
if (event.code === 1e3) {
|
|
297
|
+
stop(1e3, "", { close, keepSession: true });
|
|
298
|
+
} else
|
|
299
|
+
failed(
|
|
300
|
+
new ApiError(
|
|
301
|
+
"WebSocket connection closed",
|
|
302
|
+
0,
|
|
303
|
+
{},
|
|
304
|
+
void 0,
|
|
305
|
+
"network"
|
|
306
|
+
),
|
|
307
|
+
retryCodes.has(event.code),
|
|
308
|
+
close
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
const onMessage = (event) => {
|
|
312
|
+
if (!current(id) || snapshot.status !== "open") return;
|
|
313
|
+
const data = event.data;
|
|
314
|
+
if (!(typeof data === "string" || data instanceof ArrayBuffer || data instanceof Blob)) {
|
|
315
|
+
report(
|
|
316
|
+
new ApiError(
|
|
317
|
+
"Unsupported incoming WebSocket data",
|
|
318
|
+
0,
|
|
319
|
+
{},
|
|
320
|
+
void 0,
|
|
321
|
+
"parse"
|
|
322
|
+
)
|
|
323
|
+
);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (byteSize(data) > messageLimit || pending >= pendingLimit) {
|
|
327
|
+
report(
|
|
328
|
+
new ApiError(
|
|
329
|
+
"WebSocket incoming message limit exceeded",
|
|
330
|
+
0,
|
|
331
|
+
{},
|
|
332
|
+
void 0,
|
|
333
|
+
"backpressure"
|
|
334
|
+
)
|
|
335
|
+
);
|
|
336
|
+
if (current(id)) stop(1e3, "Message limit exceeded");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
pending++;
|
|
340
|
+
chain = chain.then(async () => {
|
|
341
|
+
if (!current(id)) return;
|
|
342
|
+
let parsed = data;
|
|
343
|
+
if (format === "json") {
|
|
344
|
+
if (typeof data !== "string")
|
|
345
|
+
throw new ApiError(
|
|
346
|
+
"Expected a JSON text WebSocket message",
|
|
347
|
+
0,
|
|
348
|
+
{},
|
|
349
|
+
void 0,
|
|
350
|
+
"parse"
|
|
351
|
+
);
|
|
352
|
+
try {
|
|
353
|
+
parsed = JSON.parse(data);
|
|
354
|
+
} catch {
|
|
355
|
+
throw new ApiError(
|
|
356
|
+
"Invalid JSON WebSocket message",
|
|
357
|
+
0,
|
|
358
|
+
{},
|
|
359
|
+
void 0,
|
|
360
|
+
"parse"
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const result = options.schema ? await abortable(
|
|
365
|
+
() => validateResponse(options.schema, parsed, 0),
|
|
366
|
+
signal
|
|
367
|
+
) : parsed;
|
|
368
|
+
for (const listener of [...messageListeners]) {
|
|
369
|
+
if (!current(id)) break;
|
|
370
|
+
try {
|
|
371
|
+
listener(result);
|
|
372
|
+
} catch (cause) {
|
|
373
|
+
listenerError(cause);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}).catch((cause) => {
|
|
377
|
+
if (current(id))
|
|
378
|
+
report(
|
|
379
|
+
cause instanceof ApiError ? cause : new ApiError(
|
|
380
|
+
"WebSocket message processing failed",
|
|
381
|
+
0,
|
|
382
|
+
{},
|
|
383
|
+
void 0,
|
|
384
|
+
"parse",
|
|
385
|
+
cause
|
|
386
|
+
)
|
|
387
|
+
);
|
|
388
|
+
}).finally(() => {
|
|
389
|
+
if (current(id)) pending--;
|
|
390
|
+
});
|
|
391
|
+
};
|
|
392
|
+
transport.addEventListener("open", onOpen);
|
|
393
|
+
transport.addEventListener("error", onError);
|
|
394
|
+
transport.addEventListener("message", onMessage);
|
|
395
|
+
transport.addEventListener("close", onClose);
|
|
396
|
+
detach = () => {
|
|
397
|
+
transport.removeEventListener("open", onOpen);
|
|
398
|
+
transport.removeEventListener("error", onError);
|
|
399
|
+
transport.removeEventListener("message", onMessage);
|
|
400
|
+
transport.removeEventListener("close", onClose);
|
|
401
|
+
};
|
|
402
|
+
} catch (cause) {
|
|
403
|
+
if (current(id))
|
|
404
|
+
failed(
|
|
405
|
+
cause instanceof ApiError ? cause : new ApiError(
|
|
406
|
+
"WebSocket connection setup failed",
|
|
407
|
+
0,
|
|
408
|
+
{},
|
|
409
|
+
void 0,
|
|
410
|
+
"network"
|
|
411
|
+
),
|
|
412
|
+
!(cause instanceof ApiError) || cause.code === "network"
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function send(data) {
|
|
417
|
+
if (!wanted || !sessionValid() || !socket || socket.readyState !== 1 || snapshot.status !== "open")
|
|
418
|
+
throw new ApiError(
|
|
419
|
+
"WebSocket is not open; message was not queued",
|
|
420
|
+
0,
|
|
421
|
+
{},
|
|
422
|
+
void 0,
|
|
423
|
+
"network"
|
|
424
|
+
);
|
|
425
|
+
if (socket.bufferedAmount + byteSize(data) > bufferedLimit)
|
|
426
|
+
throw new ApiError(
|
|
427
|
+
"WebSocket outgoing buffer limit exceeded",
|
|
428
|
+
0,
|
|
429
|
+
{},
|
|
430
|
+
void 0,
|
|
431
|
+
"backpressure"
|
|
432
|
+
);
|
|
433
|
+
try {
|
|
434
|
+
socket.send(data);
|
|
435
|
+
} catch (cause) {
|
|
436
|
+
throw new ApiError(
|
|
437
|
+
"WebSocket send failed; delivery is unknown",
|
|
438
|
+
0,
|
|
439
|
+
{},
|
|
440
|
+
void 0,
|
|
441
|
+
"network",
|
|
442
|
+
cause
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return Object.freeze({
|
|
447
|
+
connect() {
|
|
448
|
+
if (disposed) configuration("Socket client is disposed");
|
|
449
|
+
if (wanted) return;
|
|
450
|
+
if (requiresAuth && !options.sessionClient?.getSessionSnapshot().userId)
|
|
451
|
+
throw new ApiError(
|
|
452
|
+
"An authenticated session is required",
|
|
453
|
+
401,
|
|
454
|
+
{},
|
|
455
|
+
void 0,
|
|
456
|
+
"auth"
|
|
457
|
+
);
|
|
458
|
+
revision = options.sessionClient?.getSessionSnapshot().revision;
|
|
459
|
+
wanted = true;
|
|
460
|
+
attempts = 0;
|
|
461
|
+
unsubscribeSession?.();
|
|
462
|
+
unsubscribeSession = options.sessionClient?.subscribeSession(() => {
|
|
463
|
+
if (!sessionValid())
|
|
464
|
+
stop(1e3, "Session changed", { close: null, clearError: true });
|
|
465
|
+
});
|
|
466
|
+
update({ error: null, close: null });
|
|
467
|
+
if (wanted) void attempt();
|
|
468
|
+
},
|
|
469
|
+
disconnect(code = 1e3, reason = "") {
|
|
470
|
+
validateClose(code, reason);
|
|
471
|
+
if (!disposed) stop(code, reason);
|
|
472
|
+
},
|
|
473
|
+
dispose() {
|
|
474
|
+
if (disposed) return;
|
|
475
|
+
disposed = true;
|
|
476
|
+
stop(1e3, "Disposed", {
|
|
477
|
+
status: "disposed",
|
|
478
|
+
close: null,
|
|
479
|
+
clearError: true
|
|
480
|
+
});
|
|
481
|
+
stateListeners.clear();
|
|
482
|
+
messageListeners.clear();
|
|
483
|
+
errorListeners.clear();
|
|
484
|
+
},
|
|
485
|
+
send,
|
|
486
|
+
sendJson(value) {
|
|
487
|
+
let data;
|
|
488
|
+
try {
|
|
489
|
+
data = JSON.stringify(value);
|
|
490
|
+
} catch (cause) {
|
|
491
|
+
throw new ApiError(
|
|
492
|
+
"Message is not JSON serializable",
|
|
493
|
+
0,
|
|
494
|
+
{},
|
|
495
|
+
void 0,
|
|
496
|
+
"configuration",
|
|
497
|
+
cause
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
if (data === void 0) configuration("Message is not JSON serializable");
|
|
501
|
+
send(data);
|
|
502
|
+
},
|
|
503
|
+
getSnapshot: () => snapshot,
|
|
504
|
+
subscribe(listener) {
|
|
505
|
+
if (disposed) configuration("Socket client is disposed");
|
|
506
|
+
stateListeners.add(listener);
|
|
507
|
+
return () => {
|
|
508
|
+
stateListeners.delete(listener);
|
|
509
|
+
};
|
|
510
|
+
},
|
|
511
|
+
onMessage(listener) {
|
|
512
|
+
if (disposed) configuration("Socket client is disposed");
|
|
513
|
+
messageListeners.add(listener);
|
|
514
|
+
return () => {
|
|
515
|
+
messageListeners.delete(listener);
|
|
516
|
+
};
|
|
517
|
+
},
|
|
518
|
+
onError(listener) {
|
|
519
|
+
if (disposed) configuration("Socket client is disposed");
|
|
520
|
+
errorListeners.add(listener);
|
|
521
|
+
return () => {
|
|
522
|
+
errorListeners.delete(listener);
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
export {
|
|
528
|
+
createSocketClient
|
|
529
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SocketClient, SocketSnapshot } from './index.js';
|
|
2
|
+
import '../client-ByBlZaRg.js';
|
|
3
|
+
|
|
4
|
+
/** Subscribes only; the application owns connect/disconnect and may share a client. */
|
|
5
|
+
declare function useSocketState<T>(client: SocketClient<T>): SocketSnapshot;
|
|
6
|
+
declare function useSocketMessage<T>(client: SocketClient<T>, listener: (message: T) => void): void;
|
|
7
|
+
|
|
8
|
+
export { useSocketMessage, useSocketState };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/websocket/react.ts
|
|
2
|
+
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
3
|
+
function useSocketState(client) {
|
|
4
|
+
return useSyncExternalStore(
|
|
5
|
+
client.subscribe,
|
|
6
|
+
client.getSnapshot,
|
|
7
|
+
client.getSnapshot
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
function useSocketMessage(client, listener) {
|
|
11
|
+
const latest = useRef(listener);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
latest.current = listener;
|
|
14
|
+
});
|
|
15
|
+
useEffect(
|
|
16
|
+
() => client.onMessage((message) => latest.current(message)),
|
|
17
|
+
[client]
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
useSocketMessage,
|
|
22
|
+
useSocketState
|
|
23
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Ref } from 'vue';
|
|
2
|
+
import { SocketClient, SocketSnapshot } from './index.js';
|
|
3
|
+
import '../client-ByBlZaRg.js';
|
|
4
|
+
|
|
5
|
+
/** Subscribes only; the application owns connect/disconnect and may share a client. */
|
|
6
|
+
declare function useSocketState<T>(client: SocketClient<T>): Readonly<Ref<SocketSnapshot>>;
|
|
7
|
+
declare function useSocketMessage<T>(client: SocketClient<T>, listener: (message: T) => void): void;
|
|
8
|
+
|
|
9
|
+
export { useSocketMessage, useSocketState };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApiError
|
|
3
|
+
} from "../chunk-YTM3FV65.js";
|
|
4
|
+
|
|
5
|
+
// src/websocket/vue.ts
|
|
6
|
+
import {
|
|
7
|
+
getCurrentScope,
|
|
8
|
+
onScopeDispose,
|
|
9
|
+
shallowReadonly,
|
|
10
|
+
shallowRef
|
|
11
|
+
} from "vue";
|
|
12
|
+
function assertScope() {
|
|
13
|
+
if (!getCurrentScope())
|
|
14
|
+
throw new ApiError(
|
|
15
|
+
"Socket composables require an active Vue effect scope",
|
|
16
|
+
0,
|
|
17
|
+
{},
|
|
18
|
+
void 0,
|
|
19
|
+
"configuration"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
function useSocketState(client) {
|
|
23
|
+
assertScope();
|
|
24
|
+
const state = shallowRef(client.getSnapshot());
|
|
25
|
+
onScopeDispose(
|
|
26
|
+
client.subscribe(() => {
|
|
27
|
+
state.value = client.getSnapshot();
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
return shallowReadonly(state);
|
|
31
|
+
}
|
|
32
|
+
function useSocketMessage(client, listener) {
|
|
33
|
+
assertScope();
|
|
34
|
+
onScopeDispose(client.onMessage(listener));
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
useSocketMessage,
|
|
38
|
+
useSocketState
|
|
39
|
+
};
|