@craftainer/crud-sdk 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,616 @@
1
+ //#region src/cache/policy.ts
2
+ function seconds(value) {
3
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(value, 0) : 0;
4
+ }
5
+ /** Build a policy from a manifest resource object; `overrides` win over the hints. */
6
+ function cachePolicyFromManifest(resource, overrides = {}) {
7
+ const hints = resource.cache ?? {};
8
+ return {
9
+ enabled: overrides.enabled ?? true,
10
+ maxAge: overrides.maxAge ?? seconds(hints.max_age),
11
+ statsTtl: overrides.statsTtl ?? seconds(hints.stats_ttl),
12
+ staleWhileRevalidate: overrides.staleWhileRevalidate ?? seconds(hints.stale_while_revalidate),
13
+ etag: overrides.etag ?? hints.etag !== false,
14
+ revalidateOnly: overrides.revalidateOnly ?? hints.revalidate_only === true
15
+ };
16
+ }
17
+ /**
18
+ * The policy to use under TanStack Query: its `staleTime` decides freshness,
19
+ * so the SDK cache only revalidates (ETag/304) unless told otherwise.
20
+ */
21
+ function tanstackCachePolicy(resource, overrides = {}) {
22
+ return cachePolicyFromManifest(resource, {
23
+ revalidateOnly: true,
24
+ ...overrides
25
+ });
26
+ }
27
+ //#endregion
28
+ //#region node_modules/.pnpm/eventsource-parser@4.1.2/node_modules/eventsource-parser/dist/errors.js
29
+ /**
30
+ * Error thrown when encountering an issue during parsing.
31
+ *
32
+ * @public
33
+ */
34
+ var ParseError = class extends Error {
35
+ constructor(message, options) {
36
+ super(message);
37
+ this.name = "ParseError";
38
+ this.type = options.type;
39
+ this.field = options.field;
40
+ this.value = options.value;
41
+ this.line = options.line;
42
+ }
43
+ };
44
+ //#endregion
45
+ //#region node_modules/.pnpm/eventsource-parser@4.1.2/node_modules/eventsource-parser/dist/parse.js
46
+ /**
47
+ * EventSource/Server-Sent Events parser
48
+ * @see https://html.spec.whatwg.org/multipage/server-sent-events.html
49
+ */
50
+ const LF = 10;
51
+ const CR = 13;
52
+ const SPACE = 32;
53
+ const MAX_FIELD_PREFIX_LENGTH = 6;
54
+ /**
55
+ * Creates a new EventSource parser.
56
+ *
57
+ * @param config - Parser configuration. Accepts callbacks (see {@link ParserCallbacks})
58
+ * and options like `maxBufferSize` (see {@link ParserConfig}).
59
+ *
60
+ * @returns A new EventSource parser, with `feed` and `reset` methods.
61
+ * @public
62
+ */
63
+ function createParser(config) {
64
+ if (typeof config === "function") throw new TypeError("`config` must be an object, got a function instead. Did you mean `createParser({onEvent: fn})`?");
65
+ const { maxBufferSize, onComment, onError, onEvent, onId, onRetry } = config;
66
+ const pendingFragments = [];
67
+ let pendingFragmentsLength = 0;
68
+ let bomPrefix = "";
69
+ let id;
70
+ let data = "";
71
+ let dataLines = 0;
72
+ let eventType;
73
+ let terminated = false;
74
+ let skippingLine = false;
75
+ let skipNextLineFeed = false;
76
+ /**
77
+ * Feeds a chunk of the SSE stream to the parser. Any trailing bytes that do
78
+ * not yet form a complete line are held back and prepended to the next chunk,
79
+ * so callers can pass arbitrary slices of the stream without worrying about
80
+ * line boundaries.
81
+ *
82
+ * Per the SSE spec, one leading UTF-8 BOM is stripped before parsing,
83
+ * even when split across chunks. This handles both the raw 3-byte form (0xEF 0xBB
84
+ * 0xBF) and a single decoded U+FEFF, so a leading BOM is ignored regardless of
85
+ * how the caller decoded the bytes.
86
+ *
87
+ * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream
88
+ */
89
+ function feed(chunk) {
90
+ if (terminated) throw new Error("Cannot feed parser: it was terminated after exceeding the configured max buffer size. Call `reset()` to resume parsing.");
91
+ if (bomPrefix !== void 0) {
92
+ chunk = bomPrefix + chunk;
93
+ if (chunk === "" || chunk === "ï" || chunk === "ï»") {
94
+ bomPrefix = chunk;
95
+ return;
96
+ }
97
+ bomPrefix = void 0;
98
+ chunk = chunk.replace(/^(?:\uFEFF|\xEF\xBB\xBF)/, "");
99
+ }
100
+ if (skippingLine || skipNextLineFeed) {
101
+ chunk = resumeAfterSkip(chunk);
102
+ if (!chunk) return;
103
+ }
104
+ if (!pendingFragments.length) {
105
+ const trailing = processLines(chunk);
106
+ if (trailing !== "") storeTrailing(trailing);
107
+ checkForBufferOverflow();
108
+ return;
109
+ }
110
+ if (chunk.indexOf("\n") === -1 && chunk.indexOf("\r") === -1) {
111
+ if (pendingFragmentsLength < MAX_FIELD_PREFIX_LENGTH) {
112
+ if (!shouldBufferTrailing(pendingFragments.join("") + chunk.slice(0, MAX_FIELD_PREFIX_LENGTH - pendingFragmentsLength))) {
113
+ pendingFragments.length = 0;
114
+ pendingFragmentsLength = 0;
115
+ skippingLine = true;
116
+ return;
117
+ }
118
+ }
119
+ pendingFragments.push(chunk);
120
+ pendingFragmentsLength += chunk.length;
121
+ checkForBufferOverflow();
122
+ return;
123
+ }
124
+ pendingFragments.push(chunk);
125
+ const input = pendingFragments.join("");
126
+ pendingFragments.length = 0;
127
+ pendingFragmentsLength = 0;
128
+ storeTrailing(processLines(input));
129
+ checkForBufferOverflow();
130
+ }
131
+ function resumeAfterSkip(chunk) {
132
+ if (chunk.length === 0) return chunk;
133
+ if (skipNextLineFeed) {
134
+ skipNextLineFeed = false;
135
+ return chunk.charCodeAt(0) === LF ? chunk.slice(1) : chunk;
136
+ }
137
+ const crIndex = chunk.indexOf("\r");
138
+ const lfIndex = chunk.indexOf("\n");
139
+ const lineEnd = crIndex === -1 ? lfIndex : lfIndex === -1 ? crIndex : crIndex < lfIndex ? crIndex : lfIndex;
140
+ if (lineEnd === -1) return "";
141
+ if (lineEnd === chunk.length - 1 && chunk.charCodeAt(lineEnd) === CR) {
142
+ skippingLine = false;
143
+ skipNextLineFeed = true;
144
+ return "";
145
+ }
146
+ skippingLine = false;
147
+ return chunk.slice(lineEnd + (chunk.charCodeAt(lineEnd) === CR && chunk.charCodeAt(lineEnd + 1) === LF ? 2 : 1));
148
+ }
149
+ function storeTrailing(trailing) {
150
+ if (!trailing) return;
151
+ if (trailing.charCodeAt(trailing.length - 1) === CR) {
152
+ parseLine(trailing, 0, trailing.length - 1);
153
+ if (checkForBufferOverflow()) return;
154
+ skipNextLineFeed = true;
155
+ return;
156
+ }
157
+ if (shouldBufferTrailing(trailing)) {
158
+ pendingFragments.push(trailing);
159
+ pendingFragmentsLength = trailing.length;
160
+ return;
161
+ }
162
+ skippingLine = true;
163
+ }
164
+ function shouldBufferTrailing(trailing) {
165
+ const firstCharCode = trailing.charCodeAt(0);
166
+ return firstCharCode === 58 && !!onComment || firstCharCode === 100 && isPotentialField(trailing, "data") || firstCharCode === 101 && isPotentialField(trailing, "event") || firstCharCode === 105 && isPotentialField(trailing, "id") || firstCharCode === 114 && isPotentialField(trailing, "retry");
167
+ }
168
+ function checkForBufferOverflow() {
169
+ if (terminated) return true;
170
+ if (maxBufferSize === void 0) return false;
171
+ if (pendingFragmentsLength + data.length <= maxBufferSize) return false;
172
+ terminated = true;
173
+ pendingFragments.length = 0;
174
+ pendingFragmentsLength = 0;
175
+ id = void 0;
176
+ data = "";
177
+ dataLines = 0;
178
+ eventType = void 0;
179
+ skippingLine = false;
180
+ skipNextLineFeed = false;
181
+ onError === null || onError === void 0 || onError(new ParseError(`Buffered data exceeded max buffer size of ${maxBufferSize} characters`, { type: "max-buffer-size-exceeded" }));
182
+ return true;
183
+ }
184
+ /**
185
+ * Splits `chunk` into SSE lines and dispatches each to the appropriate handler.
186
+ * Returns any trailing bytes that did not terminate with a line break, so the
187
+ * caller can prepend them to the next chunk.
188
+ *
189
+ * The SSE spec permits three line terminators: `\n`, `\r`, and `\r\n`. Real-world
190
+ * streams almost always use plain `\n`, so we take a fast path when no `\r` is
191
+ * present in the chunk. The slow path is spec-correct but does more work per line.
192
+ */
193
+ function processLines(chunk) {
194
+ let searchIndex = 0;
195
+ if (chunk.indexOf("\r") === -1) {
196
+ let lfIndex = chunk.indexOf("\n", searchIndex);
197
+ while (lfIndex !== -1) {
198
+ if (searchIndex === lfIndex) {
199
+ if (id !== void 0) onId === null || onId === void 0 || onId(id);
200
+ if (dataLines > 0) onEvent === null || onEvent === void 0 || onEvent({
201
+ id,
202
+ event: eventType,
203
+ data
204
+ });
205
+ id = void 0;
206
+ data = "";
207
+ dataLines = 0;
208
+ eventType = void 0;
209
+ searchIndex = lfIndex + 1;
210
+ lfIndex = chunk.indexOf("\n", searchIndex);
211
+ continue;
212
+ }
213
+ const firstCharCode = chunk.charCodeAt(searchIndex);
214
+ if (isDataPrefix(chunk, searchIndex, firstCharCode)) {
215
+ const valueStart = chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5;
216
+ const value = chunk.slice(valueStart, lfIndex);
217
+ if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {
218
+ if (id !== void 0) onId === null || onId === void 0 || onId(id);
219
+ onEvent === null || onEvent === void 0 || onEvent({
220
+ id,
221
+ event: eventType,
222
+ data: value
223
+ });
224
+ id = void 0;
225
+ data = "";
226
+ eventType = void 0;
227
+ searchIndex = lfIndex + 2;
228
+ lfIndex = chunk.indexOf("\n", searchIndex);
229
+ continue;
230
+ }
231
+ data = dataLines === 0 ? value : `${data}\n${value}`;
232
+ dataLines++;
233
+ } else if (isEventPrefix(chunk, searchIndex, firstCharCode)) eventType = chunk.slice(chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6, lfIndex) || void 0;
234
+ else parseLine(chunk, searchIndex, lfIndex);
235
+ if (checkForBufferOverflow()) return "";
236
+ searchIndex = lfIndex + 1;
237
+ lfIndex = chunk.indexOf("\n", searchIndex);
238
+ }
239
+ return chunk.slice(searchIndex);
240
+ }
241
+ while (searchIndex < chunk.length) {
242
+ const crIndex = chunk.indexOf("\r", searchIndex);
243
+ const lfIndex = chunk.indexOf("\n", searchIndex);
244
+ let lineEnd = -1;
245
+ if (crIndex !== -1 && lfIndex !== -1) lineEnd = crIndex < lfIndex ? crIndex : lfIndex;
246
+ else if (crIndex !== -1) {
247
+ if (crIndex === chunk.length - 1) lineEnd = -1;
248
+ else lineEnd = crIndex;
249
+ } else if (lfIndex !== -1) lineEnd = lfIndex;
250
+ if (lineEnd === -1) break;
251
+ parseLine(chunk, searchIndex, lineEnd);
252
+ if (checkForBufferOverflow()) return "";
253
+ searchIndex = lineEnd + 1;
254
+ if (chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF) searchIndex++;
255
+ }
256
+ return chunk.slice(searchIndex);
257
+ }
258
+ function parseLine(chunk, start, end) {
259
+ if (start === end) {
260
+ dispatchEvent();
261
+ return;
262
+ }
263
+ const firstCharCode = chunk.charCodeAt(start);
264
+ if (isDataPrefix(chunk, start, firstCharCode)) {
265
+ const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5;
266
+ const value = chunk.slice(valueStart, end);
267
+ data = dataLines === 0 ? value : `${data}\n${value}`;
268
+ dataLines++;
269
+ return;
270
+ }
271
+ if (isEventPrefix(chunk, start, firstCharCode)) {
272
+ eventType = chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || void 0;
273
+ return;
274
+ }
275
+ if (firstCharCode === 105 && chunk.charCodeAt(start + 1) === 100 && chunk.charCodeAt(start + 2) === 58) {
276
+ const value = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end);
277
+ if (!value.includes("\0")) id = value;
278
+ return;
279
+ }
280
+ if (firstCharCode === 58) {
281
+ if (onComment) {
282
+ const line = chunk.slice(start, end);
283
+ onComment(line.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1));
284
+ }
285
+ return;
286
+ }
287
+ const line = chunk.slice(start, end);
288
+ const fieldSeparatorIndex = line.indexOf(":");
289
+ if (fieldSeparatorIndex === -1) {
290
+ processField(line, "", line);
291
+ return;
292
+ }
293
+ const field = line.slice(0, fieldSeparatorIndex);
294
+ const offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1;
295
+ processField(field, line.slice(fieldSeparatorIndex + offset), line);
296
+ }
297
+ function processField(field, value, line) {
298
+ switch (field) {
299
+ case "event":
300
+ eventType = value || void 0;
301
+ break;
302
+ case "data":
303
+ data = dataLines === 0 ? value : `${data}\n${value}`;
304
+ dataLines++;
305
+ break;
306
+ case "id":
307
+ if (!value.includes("\0")) id = value;
308
+ break;
309
+ case "retry":
310
+ if (/^\d+$/.test(value)) onRetry === null || onRetry === void 0 || onRetry(parseInt(value, 10));
311
+ else onError === null || onError === void 0 || onError(new ParseError(`Invalid \`retry\` value: "${value}"`, {
312
+ type: "invalid-retry",
313
+ value,
314
+ line
315
+ }));
316
+ break;
317
+ default: onError === null || onError === void 0 || onError(new ParseError(`Unknown field "${field.length > 20 ? `${field.slice(0, 20)}…` : field}"`, {
318
+ type: "unknown-field",
319
+ field,
320
+ value,
321
+ line
322
+ }));
323
+ }
324
+ }
325
+ function dispatchEvent() {
326
+ if (id !== void 0) onId === null || onId === void 0 || onId(id);
327
+ if (dataLines > 0) onEvent === null || onEvent === void 0 || onEvent({
328
+ id,
329
+ event: eventType,
330
+ data
331
+ });
332
+ id = void 0;
333
+ data = "";
334
+ dataLines = 0;
335
+ eventType = void 0;
336
+ }
337
+ function reset(options = {}) {
338
+ if (options.consume && pendingFragments.length > 0) {
339
+ const incompleteLine = pendingFragments.join("");
340
+ parseLine(incompleteLine, 0, incompleteLine.length);
341
+ }
342
+ bomPrefix = "";
343
+ id = void 0;
344
+ data = "";
345
+ dataLines = 0;
346
+ eventType = void 0;
347
+ pendingFragments.length = 0;
348
+ pendingFragmentsLength = 0;
349
+ terminated = false;
350
+ skippingLine = false;
351
+ skipNextLineFeed = false;
352
+ }
353
+ return {
354
+ feed,
355
+ reset
356
+ };
357
+ }
358
+ /**
359
+ * Checks if `chunk` starts with the literal `data:` at index `i`.
360
+ *
361
+ * Equivalent to `chunk.startsWith('data:', i)`, but benchmarks show this
362
+ * hand-unrolled char-code comparison is ~20% faster on common event types.
363
+ * The caller passes `firstCharCode` (the code at `i`) so it can be reused
364
+ * across prefix checks.
365
+ *
366
+ * ASCII: 'd' = 100, 'a' = 97, 't' = 116, 'a' = 97, ':' = 58
367
+ */
368
+ function isDataPrefix(chunk, i, firstCharCode) {
369
+ return firstCharCode === 100 && chunk.charCodeAt(i + 1) === 97 && chunk.charCodeAt(i + 2) === 116 && chunk.charCodeAt(i + 3) === 97 && chunk.charCodeAt(i + 4) === 58;
370
+ }
371
+ /**
372
+ * Checks if `chunk` starts with the literal `event:` at index `i`.
373
+ *
374
+ * See {@link isDataPrefix} for why this is hand-unrolled rather than using
375
+ * `String.prototype.startsWith`.
376
+ *
377
+ * ASCII: 'e' = 101, 'v' = 118, 'e' = 101, 'n' = 110, 't' = 116, ':' = 58
378
+ */
379
+ function isEventPrefix(chunk, i, firstCharCode) {
380
+ return firstCharCode === 101 && chunk.charCodeAt(i + 1) === 118 && chunk.charCodeAt(i + 2) === 101 && chunk.charCodeAt(i + 3) === 110 && chunk.charCodeAt(i + 4) === 116 && chunk.charCodeAt(i + 5) === 58;
381
+ }
382
+ function isPotentialField(line, field) {
383
+ let i = 1;
384
+ while (i < line.length && i < field.length) {
385
+ if (line.charCodeAt(i) !== field.charCodeAt(i)) return false;
386
+ i++;
387
+ }
388
+ return line.length <= field.length || line.charCodeAt(field.length) === 58;
389
+ }
390
+ //#endregion
391
+ //#region src/events.ts
392
+ /**
393
+ * SSE subscriber that turns `GET {base_path}/events` into callbacks.
394
+ *
395
+ * One connection per (fetch, url), reference-counted and fanned out to every
396
+ * subscriber. The caller passes an already authenticated `fetch`: this is
397
+ * fetch streaming, not `EventSource`, because `EventSource` cannot send an
398
+ * Authorization header. Frames carry no `event:` name: the first has
399
+ * `id: 0` and `{"subscriber_id": ...}`; later ones are record events. A
400
+ * reconnect passes `?{resume_param}=<subscriber_id>` explicitly (never
401
+ * `Last-Event-ID`, which the server would read as a subscriber id) when the
402
+ * stream replays, and always tells subscribers to reset, so the gap is
403
+ * covered even without replay. A stream silent for three keep-alive
404
+ * intervals is dead and is reconnected with exponential backoff and jitter.
405
+ * Events invalidate; they never apply snapshots.
406
+ */
407
+ const BACKOFF_START_MS = 1e3;
408
+ const BACKOFF_MAX_MS = 3e4;
409
+ const DEFAULT_KEEPALIVE_S = 15;
410
+ const DEAD_AFTER_KEEPALIVES = 3;
411
+ const ACTIONS = /* @__PURE__ */ new Set([
412
+ "create",
413
+ "update",
414
+ "delete",
415
+ "restore"
416
+ ]);
417
+ const registry = /* @__PURE__ */ new WeakMap();
418
+ function sleep(ms, signal) {
419
+ return new Promise((resolve) => {
420
+ if (signal.aborted) {
421
+ resolve();
422
+ return;
423
+ }
424
+ const done = () => {
425
+ clearTimeout(timer);
426
+ signal.removeEventListener("abort", done);
427
+ resolve();
428
+ };
429
+ const timer = setTimeout(done, ms);
430
+ signal.addEventListener("abort", done, { once: true });
431
+ });
432
+ }
433
+ var Hub = class {
434
+ handlers = /* @__PURE__ */ new Set();
435
+ options;
436
+ onEmpty;
437
+ controller;
438
+ connected = false;
439
+ subscriberId;
440
+ attempt = 0;
441
+ constructor(options, onEmpty) {
442
+ this.options = options;
443
+ this.onEmpty = onEmpty;
444
+ }
445
+ subscribe(handlers) {
446
+ const entry = {
447
+ onEvent: (event) => handlers.onEvent(event),
448
+ onReset: () => handlers.onReset?.(),
449
+ onStatus: (connected) => handlers.onStatus?.(connected)
450
+ };
451
+ this.handlers.add(entry);
452
+ if (this.controller === void 0) {
453
+ this.controller = new AbortController();
454
+ this.run(this.controller.signal);
455
+ } else if (this.connected) this.guard(() => entry.onStatus?.(true));
456
+ return () => {
457
+ if (!this.handlers.delete(entry)) return;
458
+ this.guard(() => entry.onStatus?.(false));
459
+ if (this.handlers.size === 0) {
460
+ this.controller?.abort();
461
+ this.controller = void 0;
462
+ this.onEmpty();
463
+ }
464
+ };
465
+ }
466
+ async guard(call) {
467
+ try {
468
+ await call();
469
+ } catch {}
470
+ }
471
+ async fan(call) {
472
+ for (const handlers of Array.from(this.handlers)) await this.guard(() => call(handlers));
473
+ }
474
+ async setConnected(connected) {
475
+ if (this.connected === connected) return;
476
+ this.connected = connected;
477
+ await this.fan((handlers) => handlers.onStatus?.(connected));
478
+ }
479
+ backoff() {
480
+ const base = Math.min(BACKOFF_START_MS * 2 ** this.attempt, BACKOFF_MAX_MS);
481
+ this.attempt += 1;
482
+ const jitter = (this.options.random ?? Math.random)();
483
+ return Math.min(base * (1 + .5 * jitter), BACKOFF_MAX_MS);
484
+ }
485
+ /** Connect, read and reconnect until aborted. */
486
+ async run(signal) {
487
+ try {
488
+ while (!signal.aborted) {
489
+ try {
490
+ await this.readOnce(signal);
491
+ } catch {}
492
+ await this.setConnected(false);
493
+ await sleep(this.backoff(), signal);
494
+ }
495
+ } finally {
496
+ await this.setConnected(false);
497
+ }
498
+ }
499
+ resumeUrl() {
500
+ const { url, events } = this.options;
501
+ if (events.replay !== "persistent" || this.subscriberId === void 0) return url;
502
+ const param = events.resume_param ?? "subscriber_id";
503
+ return `${url}${url.includes("?") ? "&" : "?"}${encodeURIComponent(param)}=${encodeURIComponent(this.subscriberId)}`;
504
+ }
505
+ async readOnce(signal) {
506
+ const deadMs = (this.options.events.keepalive_seconds ?? DEFAULT_KEEPALIVE_S) * DEAD_AFTER_KEEPALIVES * 1e3;
507
+ const conn = new AbortController();
508
+ const abortConn = () => {
509
+ conn.abort();
510
+ };
511
+ signal.addEventListener("abort", abortConn, { once: true });
512
+ let timer;
513
+ const arm = () => {
514
+ clearTimeout(timer);
515
+ timer = setTimeout(abortConn, deadMs);
516
+ };
517
+ try {
518
+ arm();
519
+ const response = await this.options.fetch(this.resumeUrl(), {
520
+ headers: { accept: "text/event-stream" },
521
+ signal: conn.signal
522
+ });
523
+ if (!response.ok || response.body === null) throw new Error(`event stream refused: HTTP ${response.status}`);
524
+ const reader = response.body.getReader();
525
+ const decoder = new TextDecoder();
526
+ const pending = [];
527
+ const parser = createParser({ onEvent: (message) => {
528
+ pending.push(message);
529
+ } });
530
+ for (;;) {
531
+ const { done, value } = await reader.read();
532
+ if (done) return;
533
+ arm();
534
+ parser.feed(decoder.decode(value, { stream: true }));
535
+ for (const message of pending.splice(0)) await this.handle(message);
536
+ }
537
+ } finally {
538
+ clearTimeout(timer);
539
+ signal.removeEventListener("abort", abortConn);
540
+ conn.abort();
541
+ }
542
+ }
543
+ async handle(message) {
544
+ let data;
545
+ try {
546
+ data = JSON.parse(message.data);
547
+ } catch {
548
+ return;
549
+ }
550
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return;
551
+ const record = data;
552
+ if ("subscriber_id" in record) {
553
+ this.subscriberId = String(record["subscriber_id"]);
554
+ this.attempt = 0;
555
+ await this.fan((handlers) => handlers.onReset?.());
556
+ await this.setConnected(true);
557
+ return;
558
+ }
559
+ if (record["resource"] !== this.options.events.resource) return;
560
+ const action = record["action"];
561
+ const recordId = record["record_id"];
562
+ if (!ACTIONS.has(action) || !Number.isInteger(recordId)) {
563
+ await this.fan((handlers) => handlers.onReset?.());
564
+ return;
565
+ }
566
+ const snapshot = record["snapshot"];
567
+ const event = {
568
+ resource: String(record["resource"]),
569
+ recordId,
570
+ action,
571
+ snapshot: typeof snapshot === "object" && snapshot !== null ? snapshot : {},
572
+ timestamp: String(record["timestamp"] ?? "")
573
+ };
574
+ await this.fan((handlers) => handlers.onEvent(event));
575
+ }
576
+ };
577
+ /**
578
+ * The shared hub for `(fetch, url)`: the first subscriber opens the stream,
579
+ * the last one aborts it. A later subscribe after that opens a fresh one.
580
+ */
581
+ function getEventHub(options) {
582
+ return { subscribe(handlers) {
583
+ let hubs = registry.get(options.fetch);
584
+ if (hubs === void 0) {
585
+ hubs = /* @__PURE__ */ new Map();
586
+ registry.set(options.fetch, hubs);
587
+ }
588
+ const byUrl = hubs;
589
+ let hub = byUrl.get(options.url);
590
+ if (hub === void 0) {
591
+ hub = new Hub(options, () => {
592
+ byUrl.delete(options.url);
593
+ });
594
+ byUrl.set(options.url, hub);
595
+ }
596
+ return hub.subscribe(handlers);
597
+ } };
598
+ }
599
+ /**
600
+ * Subscribe to one resource's event stream. Returns the unsubscribe; the
601
+ * last unsubscribe aborts the stream. `signal` aborting unsubscribes too.
602
+ */
603
+ function subscribeEvents(options) {
604
+ const { signal } = options;
605
+ if (signal?.aborted === true) return () => void 0;
606
+ const unsubscribe = getEventHub(options).subscribe(options);
607
+ signal?.addEventListener("abort", unsubscribe, { once: true });
608
+ return () => {
609
+ signal?.removeEventListener("abort", unsubscribe);
610
+ unsubscribe();
611
+ };
612
+ }
613
+ //#endregion
614
+ export { tanstackCachePolicy as i, subscribeEvents as n, cachePolicyFromManifest as r, getEventHub as t };
615
+
616
+ //# sourceMappingURL=events-Cqj9Rbon.mjs.map