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.
@@ -0,0 +1,764 @@
1
+ import {
2
+ abortable,
3
+ invokeCallback,
4
+ validateResponse
5
+ } from "./chunk-DXYWZQW5.js";
6
+ import {
7
+ ApiError
8
+ } from "./chunk-YTM3FV65.js";
9
+
10
+ // src/core/stream.ts
11
+ async function* readStream(response, signal, assert, format, maxSize) {
12
+ assert();
13
+ const reader = response.body?.getReader();
14
+ if (!reader) {
15
+ if (response.status === 204 || response.status === 205) return;
16
+ throw new ApiError(
17
+ "Response has no readable body",
18
+ response.status,
19
+ {},
20
+ void 0,
21
+ "parse"
22
+ );
23
+ }
24
+ const cancel = () => {
25
+ void reader.cancel(signal.reason).catch(() => {
26
+ });
27
+ };
28
+ signal.addEventListener("abort", cancel, { once: true });
29
+ const decoder = new TextDecoder();
30
+ let line = "", skipLF = false, data = "", hasData = false, event = "", id = "";
31
+ let retry;
32
+ function tooLarge() {
33
+ throw new ApiError(
34
+ "SSE line or event exceeds maxEventSize",
35
+ response.status,
36
+ {},
37
+ void 0,
38
+ "parse"
39
+ );
40
+ }
41
+ function finishLine() {
42
+ const value = line;
43
+ line = "";
44
+ if (!value) {
45
+ const result = hasData ? Object.freeze({
46
+ event: event || "message",
47
+ data: data.slice(0, -1),
48
+ id,
49
+ ...retry === void 0 ? {} : { retry }
50
+ }) : void 0;
51
+ data = "";
52
+ hasData = false;
53
+ event = "";
54
+ return result;
55
+ }
56
+ const colon = value.indexOf(":");
57
+ const field = colon < 0 ? value : value.slice(0, colon);
58
+ let content = colon < 0 ? "" : value.slice(colon + 1);
59
+ if (content.startsWith(" ")) content = content.slice(1);
60
+ if (field === "data") {
61
+ if (data.length + content.length + 1 > maxSize) tooLarge();
62
+ data += content + "\n";
63
+ hasData = true;
64
+ } else if (field === "event") event = content;
65
+ else if (field === "id" && !content.includes("\0")) id = content;
66
+ else if (field === "retry" && /^\d+$/.test(content) && Number.isSafeInteger(Number(content)))
67
+ retry = Number(content);
68
+ return void 0;
69
+ }
70
+ try {
71
+ assert();
72
+ if (format === "sse" && response.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase() !== "text/event-stream")
73
+ throw new ApiError(
74
+ "Expected text/event-stream response",
75
+ response.status,
76
+ {},
77
+ void 0,
78
+ "parse"
79
+ );
80
+ while (true) {
81
+ const part = await abortable(() => reader.read(), signal);
82
+ assert();
83
+ const text = part.done ? decoder.decode() : decoder.decode(part.value, { stream: true });
84
+ if (format === "text") {
85
+ if (text) {
86
+ assert();
87
+ yield text;
88
+ assert();
89
+ }
90
+ } else {
91
+ for (const character of text) {
92
+ assert();
93
+ if (skipLF) {
94
+ skipLF = false;
95
+ if (character === "\n") continue;
96
+ }
97
+ if (character === "\r" || character === "\n") {
98
+ skipLF = character === "\r";
99
+ const message = finishLine();
100
+ if (message) {
101
+ assert();
102
+ yield message;
103
+ }
104
+ } else {
105
+ if (line.length + character.length > maxSize) tooLarge();
106
+ line += character;
107
+ }
108
+ }
109
+ }
110
+ if (part.done) break;
111
+ }
112
+ } finally {
113
+ signal.removeEventListener("abort", cancel);
114
+ void reader.cancel().catch(() => {
115
+ });
116
+ reader.releaseLock();
117
+ }
118
+ }
119
+
120
+ // src/core/session.ts
121
+ function createSessionState(initial) {
122
+ let token = null;
123
+ let snapshot = Object.freeze({
124
+ userId: null,
125
+ tenantId: null,
126
+ revision: 0
127
+ });
128
+ let tokenVersion = 0;
129
+ const requests = /* @__PURE__ */ new Set();
130
+ const listeners = /* @__PURE__ */ new Set();
131
+ function validate(value) {
132
+ if (value !== null && (typeof value !== "object" || typeof value.userId !== "string" || !value.userId.trim() || value.tenantId != null && typeof value.tenantId !== "string" || value.token != null && (typeof value.token !== "string" || !value.token.trim())))
133
+ throw new ApiError(
134
+ "Session requires a non-empty userId and valid token/tenantId",
135
+ 0,
136
+ {},
137
+ void 0,
138
+ "configuration"
139
+ );
140
+ }
141
+ if (initial !== void 0) validate(initial);
142
+ if (initial) {
143
+ validate(initial);
144
+ token = initial.token ?? null;
145
+ snapshot = Object.freeze({
146
+ userId: initial.userId,
147
+ tenantId: initial.tenantId ?? null,
148
+ revision: 0
149
+ });
150
+ }
151
+ const getSnapshot = () => snapshot;
152
+ const subscribe = (listener) => {
153
+ listeners.add(listener);
154
+ return () => {
155
+ listeners.delete(listener);
156
+ };
157
+ };
158
+ const assert = (revision) => {
159
+ if (revision !== snapshot.revision)
160
+ throw new ApiError(
161
+ "Authentication session changed",
162
+ 0,
163
+ {},
164
+ void 0,
165
+ "cancelled"
166
+ );
167
+ };
168
+ function set(value, reason = new ApiError(
169
+ "Authentication session changed",
170
+ 0,
171
+ {},
172
+ void 0,
173
+ "cancelled"
174
+ )) {
175
+ validate(value);
176
+ const userId = value?.userId ?? null, tenantId = value?.tenantId ?? null;
177
+ const changed = value === null || userId !== snapshot.userId || tenantId !== snapshot.tenantId;
178
+ if (token !== (value?.token ?? null)) tokenVersion++;
179
+ token = value?.token ?? null;
180
+ if (!changed) return;
181
+ snapshot = Object.freeze({
182
+ userId,
183
+ tenantId,
184
+ revision: snapshot.revision + 1
185
+ });
186
+ for (const controller of [...requests]) controller.abort(reason);
187
+ const errors = [];
188
+ for (const listener of [...listeners]) {
189
+ try {
190
+ listener();
191
+ } catch (error) {
192
+ errors.push(error);
193
+ }
194
+ }
195
+ if (errors.length)
196
+ throw new ApiError(
197
+ "Session subscriber failed",
198
+ 0,
199
+ {},
200
+ void 0,
201
+ "callback",
202
+ new AggregateError(errors)
203
+ );
204
+ }
205
+ return {
206
+ getSnapshot,
207
+ validate,
208
+ subscribe,
209
+ assert,
210
+ set,
211
+ requests,
212
+ getToken: () => token,
213
+ getTokenVersion: () => tokenVersion,
214
+ updateToken(value, revision, version) {
215
+ assert(revision);
216
+ if (version === tokenVersion) {
217
+ token = value;
218
+ tokenVersion++;
219
+ }
220
+ }
221
+ };
222
+ }
223
+
224
+ // src/core/helpers.ts
225
+ function laravelErrors(data) {
226
+ if (!data || typeof data !== "object" || !("errors" in data) || !data.errors || typeof data.errors !== "object")
227
+ return {};
228
+ return Object.fromEntries(
229
+ Object.entries(data.errors).map(([key, value]) => [
230
+ key,
231
+ Array.isArray(value) ? value.map(String) : [String(value)]
232
+ ])
233
+ );
234
+ }
235
+ function queryIdentity(client, key, url, params, headers, scope) {
236
+ return client.queryKey(key, url, params, headers, scope);
237
+ }
238
+ async function runMutation(client, config, variables) {
239
+ const revision = client.getSessionSnapshot().revision;
240
+ const url = invokeCallback(
241
+ () => typeof config.url === "function" ? config.url(variables) : config.url
242
+ );
243
+ const body = invokeCallback(
244
+ () => config.mapPayload ? config.mapPayload(variables) : variables
245
+ );
246
+ client.assertSession(revision);
247
+ const response = await client.request(url, {
248
+ method: config.method ?? "POST",
249
+ schema: config.schema,
250
+ requiresAuth: config.requiresAuth,
251
+ authRetry: config.authRetry,
252
+ body,
253
+ headers: config.headers,
254
+ signal: config.signal,
255
+ timeoutMs: config.timeoutMs
256
+ });
257
+ client.assertSession(revision);
258
+ const selected = config.select ? invokeCallback(() => config.select(response)) : response;
259
+ client.assertSession(revision);
260
+ return selected;
261
+ }
262
+ function defaultQueryRetry(failureCount, error) {
263
+ return failureCount < 3 && (error.code === "network" || error.code === "timeout" || error.code === "http" && error.status >= 500);
264
+ }
265
+
266
+ // src/core/client.ts
267
+ var DEFAULT_TIMEOUT_MS = 15e3;
268
+ var representationHeaders = [
269
+ "accept",
270
+ "accept-language",
271
+ "content-type"
272
+ ];
273
+ function createApiClient(config = {}) {
274
+ const options = { ...config };
275
+ if (options.getToken && (options.session !== void 0 || options.refreshToken))
276
+ throw new ApiError(
277
+ "Use session/setSession instead of getToken with managed authentication or token refresh",
278
+ 0,
279
+ {},
280
+ void 0,
281
+ "configuration"
282
+ );
283
+ let managedAuth = options.session !== void 0;
284
+ const session = createSessionState(options.session);
285
+ let refreshJob;
286
+ function setSession(value) {
287
+ session.validate(value);
288
+ managedAuth = true;
289
+ const oldVersion = session.getTokenVersion();
290
+ session.set(value);
291
+ if (refreshJob && (refreshJob.revision !== session.getSnapshot().revision || oldVersion !== session.getTokenVersion()))
292
+ refreshJob.controller.abort(
293
+ new ApiError(
294
+ "Session credentials changed",
295
+ 0,
296
+ {},
297
+ void 0,
298
+ "cancelled"
299
+ )
300
+ );
301
+ }
302
+ const logout = () => setSession(null);
303
+ async function refresh(revision, version, signal) {
304
+ session.assert(revision);
305
+ if (session.getTokenVersion() !== version) return;
306
+ if (!refreshJob || refreshJob.revision !== revision || refreshJob.version !== version) {
307
+ const controller = new AbortController();
308
+ const timeout = options.refreshTimeoutMs ?? DEFAULT_TIMEOUT_MS;
309
+ if (!Number.isFinite(timeout) || timeout < 0)
310
+ throw new ApiError(
311
+ "refreshTimeoutMs must be finite and non-negative",
312
+ 0,
313
+ {},
314
+ void 0,
315
+ "configuration"
316
+ );
317
+ const timer = timeout ? setTimeout(
318
+ () => controller.abort(
319
+ new ApiError(
320
+ "Token refresh timed out",
321
+ 0,
322
+ {},
323
+ void 0,
324
+ "auth"
325
+ )
326
+ ),
327
+ timeout
328
+ ) : void 0;
329
+ const job = { revision, version, controller, promise: Promise.resolve() };
330
+ session.requests.add(controller);
331
+ job.promise = abortable(
332
+ () => options.refreshToken(controller.signal, session.getSnapshot()),
333
+ controller.signal
334
+ ).then((token) => {
335
+ session.assert(revision);
336
+ if (session.getTokenVersion() !== version) return;
337
+ if (typeof token !== "string" || !token.trim())
338
+ throw new ApiError(
339
+ "Refresh did not return an access token",
340
+ 401,
341
+ {},
342
+ void 0,
343
+ "auth"
344
+ );
345
+ session.updateToken(token, revision, version);
346
+ }).catch((cause) => {
347
+ session.assert(revision);
348
+ if (session.getTokenVersion() !== version) return;
349
+ const error = new ApiError(
350
+ "Token refresh failed",
351
+ 401,
352
+ {},
353
+ void 0,
354
+ "auth",
355
+ cause
356
+ );
357
+ session.set(null, error);
358
+ throw error;
359
+ }).finally(() => {
360
+ clearTimeout(timer);
361
+ session.requests.delete(controller);
362
+ if (refreshJob === job) refreshJob = void 0;
363
+ });
364
+ void job.promise.catch(() => {
365
+ });
366
+ refreshJob = job;
367
+ }
368
+ const pending = refreshJob.promise;
369
+ await abortable(() => pending, signal);
370
+ }
371
+ const defaultHeaders = new Headers(config.headers);
372
+ const cacheScope = config.cacheScope ?? globalThis.crypto.randomUUID?.() ?? Array.from(
373
+ globalThis.crypto.getRandomValues(new Uint8Array(16)),
374
+ (value) => value.toString(16).padStart(2, "0")
375
+ ).join("");
376
+ const baseUrl = config.baseUrl ?? "";
377
+ const browserOrigin = typeof location !== "undefined" && /^https?:$/.test(location.protocol) ? location.origin : void 0;
378
+ const parseUrl = (value, base) => {
379
+ const parsed = new URL(value, base);
380
+ if (!/^https?:$/.test(parsed.protocol) || parsed.username || parsed.password)
381
+ throw new ApiError(
382
+ "Only HTTP(S) URLs without embedded credentials are supported",
383
+ 0,
384
+ {},
385
+ void 0,
386
+ "configuration"
387
+ );
388
+ return parsed;
389
+ };
390
+ if (/\\|^\/\//.test(baseUrl) || /^[a-z][a-z\d+.-]*:/i.test(baseUrl) && !/^https?:\/\//i.test(baseUrl))
391
+ throw new ApiError("Invalid baseUrl", 0, {}, void 0, "configuration");
392
+ const baseOrigin = /^https?:\/\//i.test(baseUrl) ? parseUrl(baseUrl).origin : browserOrigin;
393
+ const allowedOrigins = new Set(
394
+ (config.allowedOrigins ?? []).map((origin) => parseUrl(origin).origin)
395
+ );
396
+ if (baseOrigin) allowedOrigins.add(baseOrigin);
397
+ function targetUrl(url, params) {
398
+ if (/\\|^\/\//.test(url) || /^[a-z][a-z\d+.-]*:/i.test(url) && !/^https?:\/\//i.test(url))
399
+ throw new ApiError(
400
+ "Invalid request URL",
401
+ 0,
402
+ {},
403
+ void 0,
404
+ "configuration"
405
+ );
406
+ let path = /^https?:\/\//i.test(url) ? url : `${baseUrl.replace(/\/$/, "")}/${url.replace(/^\//, "")}`;
407
+ if (/^https?:\/\//i.test(path)) {
408
+ const parsed = parseUrl(path);
409
+ if (!allowedOrigins.has(parsed.origin))
410
+ throw new ApiError(
411
+ "Request origin is not allowed. Add its exact origin to allowedOrigins to opt in to credential forwarding.",
412
+ 0,
413
+ {},
414
+ void 0,
415
+ "configuration"
416
+ );
417
+ path = parsed.href;
418
+ }
419
+ const query = new URLSearchParams();
420
+ for (const [key, value] of Object.entries(params ?? {}))
421
+ if (value != null)
422
+ for (const item of Array.isArray(value) ? value : [value])
423
+ query.append(key, String(item));
424
+ const hashIndex = path.indexOf("#");
425
+ const beforeHash = hashIndex === -1 ? path : path.slice(0, hashIndex);
426
+ const hash = hashIndex === -1 ? "" : path.slice(hashIndex);
427
+ const encoded = query.toString();
428
+ return beforeHash + (encoded ? (beforeHash.includes("?") ? "&" : "?") + encoded : "") + hash;
429
+ }
430
+ function queryKey(key, url, params, headers, scope) {
431
+ const merged = new Headers(defaultHeaders);
432
+ new Headers(headers).forEach((value, name) => merged.set(name, value));
433
+ const vary = representationHeaders.flatMap(
434
+ (name) => merged.has(name) ? [[name, merged.get(name)]] : []
435
+ );
436
+ return [
437
+ ...key,
438
+ {
439
+ universalApiLayer: 1,
440
+ scope: cacheScope,
441
+ session: session.getSnapshot(),
442
+ account: scope,
443
+ url,
444
+ params,
445
+ headers: vary
446
+ }
447
+ ];
448
+ }
449
+ async function request(url, request2 = {}) {
450
+ for await (const context of transport(url, request2)) {
451
+ const { response, signal, assert } = context;
452
+ try {
453
+ const data = await readData(response, signal);
454
+ const schema = request2.schema === false ? void 0 : request2.schema ?? options.schema;
455
+ const validated = schema ? await abortable(
456
+ () => validateResponse(schema, data, response.status),
457
+ signal
458
+ ) : data;
459
+ assert();
460
+ if (request2.responseMode === "full")
461
+ return Object.freeze({
462
+ data: validated,
463
+ headers: new Headers(response.headers),
464
+ status: response.status,
465
+ statusText: response.statusText,
466
+ url: response.url
467
+ });
468
+ return validated;
469
+ } catch (cause) {
470
+ throw requestError(cause, signal);
471
+ }
472
+ }
473
+ throw new ApiError("Missing response", 0, {}, void 0, "network");
474
+ }
475
+ function requestError(cause, signal) {
476
+ if (signal.aborted) return signal.reason;
477
+ if (cause instanceof ApiError) return cause;
478
+ return new ApiError(
479
+ "Network request failed",
480
+ 0,
481
+ {},
482
+ void 0,
483
+ "network",
484
+ cause
485
+ );
486
+ }
487
+ async function readData(response, signal) {
488
+ const raw = await abortable(() => response.text(), signal);
489
+ let data = raw === "" ? null : raw;
490
+ if (raw && response.headers.get("content-type")?.toLowerCase().includes("json")) {
491
+ try {
492
+ data = JSON.parse(raw);
493
+ } catch (cause) {
494
+ if (response.ok)
495
+ throw new ApiError(
496
+ "Invalid JSON response",
497
+ response.status,
498
+ {},
499
+ void 0,
500
+ "parse",
501
+ cause
502
+ );
503
+ }
504
+ }
505
+ return data;
506
+ }
507
+ async function* stream(url, request2 = {}) {
508
+ if (request2.format !== void 0 && request2.format !== "text" && request2.format !== "sse")
509
+ throw new ApiError(
510
+ "Stream format must be text or sse",
511
+ 0,
512
+ {},
513
+ void 0,
514
+ "configuration"
515
+ );
516
+ const maxEventSize = request2.maxEventSize ?? 1048576;
517
+ if (!Number.isSafeInteger(maxEventSize) || maxEventSize <= 0)
518
+ throw new ApiError(
519
+ "maxEventSize must be a positive safe integer",
520
+ 0,
521
+ {},
522
+ void 0,
523
+ "configuration"
524
+ );
525
+ const headers = new Headers(request2.headers);
526
+ if (request2.format === "sse" && !headers.has("Accept"))
527
+ headers.set("Accept", "text/event-stream");
528
+ for await (const { response, signal, assert } of transport(
529
+ url,
530
+ { ...request2, headers },
531
+ true
532
+ )) {
533
+ try {
534
+ yield* readStream(
535
+ response,
536
+ signal,
537
+ assert,
538
+ request2.format ?? "text",
539
+ maxEventSize
540
+ );
541
+ } catch (cause) {
542
+ throw requestError(cause, signal);
543
+ }
544
+ }
545
+ }
546
+ async function* transport(url, request2 = {}, streaming = false) {
547
+ if (request2.responseMode !== void 0 && request2.responseMode !== "data" && request2.responseMode !== "full")
548
+ throw new ApiError(
549
+ "responseMode must be data or full",
550
+ 0,
551
+ {},
552
+ void 0,
553
+ "configuration"
554
+ );
555
+ const timeout = request2.timeoutMs ?? options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
556
+ const revision = session.getSnapshot().revision;
557
+ if (request2.requiresAuth && !session.getSnapshot().userId)
558
+ throw new ApiError(
559
+ "An authenticated session is required",
560
+ 401,
561
+ {},
562
+ void 0,
563
+ "auth"
564
+ );
565
+ if (timeout !== void 0 && (!Number.isFinite(timeout) || timeout < 0))
566
+ throw new ApiError(
567
+ "timeoutMs must be a finite non-negative number",
568
+ 0,
569
+ {},
570
+ void 0,
571
+ "configuration"
572
+ );
573
+ let target;
574
+ try {
575
+ target = targetUrl(url, request2.params);
576
+ } catch (cause) {
577
+ if (cause instanceof ApiError) throw cause;
578
+ throw new ApiError(
579
+ "Invalid request configuration",
580
+ 0,
581
+ {},
582
+ void 0,
583
+ "configuration",
584
+ cause
585
+ );
586
+ }
587
+ const controller = new AbortController();
588
+ session.requests.add(controller);
589
+ const abort = () => controller.abort(
590
+ new ApiError(
591
+ "Request cancelled",
592
+ 0,
593
+ {},
594
+ void 0,
595
+ "cancelled",
596
+ request2.signal?.reason
597
+ )
598
+ );
599
+ if (request2.signal?.aborted) abort();
600
+ else request2.signal?.addEventListener("abort", abort, { once: true });
601
+ const timer = timeout ? setTimeout(
602
+ () => controller.abort(
603
+ new ApiError("Request timed out", 0, {}, void 0, "timeout")
604
+ ),
605
+ timeout
606
+ ) : void 0;
607
+ const cleanup = () => {
608
+ clearTimeout(timer);
609
+ session.requests.delete(controller);
610
+ request2.signal?.removeEventListener("abort", abort);
611
+ controller.signal.removeEventListener("abort", cleanup);
612
+ };
613
+ controller.signal.addEventListener("abort", cleanup, { once: true });
614
+ try {
615
+ controller.signal.throwIfAborted();
616
+ const headers = new Headers(defaultHeaders);
617
+ new Headers(request2.headers).forEach(
618
+ (value, name) => headers.set(name, value)
619
+ );
620
+ let token;
621
+ try {
622
+ token = await abortable(
623
+ () => managedAuth ? session.getToken() : options.getToken?.(controller.signal),
624
+ controller.signal
625
+ );
626
+ } catch (cause) {
627
+ if (controller.signal.aborted) throw controller.signal.reason;
628
+ throw new ApiError(
629
+ "Authentication token lookup failed",
630
+ 0,
631
+ {},
632
+ void 0,
633
+ "auth",
634
+ cause
635
+ );
636
+ }
637
+ if (token) headers.set("Authorization", `Bearer ${token}`);
638
+ let body;
639
+ if (request2.body !== void 0) {
640
+ if (typeof request2.body === "string" || request2.body instanceof FormData || request2.body instanceof Blob || request2.body instanceof URLSearchParams || request2.body instanceof ArrayBuffer || ArrayBuffer.isView(request2.body))
641
+ body = request2.body;
642
+ else {
643
+ body = invokeCallback(() => JSON.stringify(request2.body));
644
+ if (!headers.has("Content-Type"))
645
+ headers.set("Content-Type", "application/json");
646
+ }
647
+ }
648
+ const send = async () => {
649
+ session.assert(revision);
650
+ if (managedAuth) {
651
+ headers.delete("Authorization");
652
+ const currentToken = session.getToken();
653
+ if (currentToken)
654
+ headers.set("Authorization", `Bearer ${currentToken}`);
655
+ }
656
+ const version2 = session.getTokenVersion();
657
+ const response2 = await abortable(
658
+ () => (options.fetch ?? globalThis.fetch)(target, {
659
+ method: request2.method ?? "GET",
660
+ headers,
661
+ body,
662
+ signal: controller.signal,
663
+ credentials: options.credentials,
664
+ redirect: "error",
665
+ cache: streaming || managedAuth || headers.has("Authorization") ? "no-store" : "default"
666
+ }),
667
+ controller.signal
668
+ );
669
+ session.assert(revision);
670
+ return { response: response2, version: version2 };
671
+ };
672
+ const pendingRefresh = refreshJob?.revision === revision ? refreshJob.promise : void 0;
673
+ if (request2.authRetry !== false && pendingRefresh)
674
+ await abortable(() => pendingRefresh, controller.signal);
675
+ let { response, version } = await send();
676
+ if (response.status === 401 && request2.authRetry !== false && options.refreshToken && session.getSnapshot().userId) {
677
+ void response.body?.cancel().catch(() => {
678
+ });
679
+ await refresh(revision, version, controller.signal);
680
+ ({ response, version } = await send());
681
+ if (response.status === 401) {
682
+ const error = new ApiError(
683
+ "Refreshed credentials were rejected",
684
+ 401,
685
+ {},
686
+ void 0,
687
+ "auth"
688
+ );
689
+ session.set(null, error);
690
+ throw error;
691
+ }
692
+ }
693
+ if (!response.ok) {
694
+ const data = await readData(response, controller.signal);
695
+ throw new ApiError(
696
+ typeof data === "object" && data && "message" in data ? String(data.message) : response.statusText || "Request failed",
697
+ response.status,
698
+ invokeCallback(
699
+ () => (options.normalizeErrors ?? laravelErrors)(data)
700
+ ),
701
+ data
702
+ );
703
+ }
704
+ yield {
705
+ response,
706
+ signal: controller.signal,
707
+ assert: () => {
708
+ controller.signal.throwIfAborted();
709
+ session.assert(revision);
710
+ }
711
+ };
712
+ } catch (cause) {
713
+ if (controller.signal.aborted) throw controller.signal.reason;
714
+ if (cause instanceof ApiError) throw cause;
715
+ throw new ApiError(
716
+ "Network request failed",
717
+ 0,
718
+ {},
719
+ void 0,
720
+ "network",
721
+ cause
722
+ );
723
+ } finally {
724
+ controller.abort(
725
+ new ApiError("Request closed", 0, {}, void 0, "cancelled")
726
+ );
727
+ cleanup();
728
+ }
729
+ }
730
+ return Object.freeze({
731
+ cacheScope,
732
+ queryKey,
733
+ setSession,
734
+ logout,
735
+ getSessionSnapshot: session.getSnapshot,
736
+ subscribeSession: session.subscribe,
737
+ assertSession: session.assert,
738
+ async withSession(operation, revision = session.getSnapshot().revision) {
739
+ session.assert(revision);
740
+ const controller = new AbortController();
741
+ session.requests.add(controller);
742
+ try {
743
+ return await abortable(operation, controller.signal);
744
+ } finally {
745
+ session.requests.delete(controller);
746
+ }
747
+ },
748
+ ownsQuery(key) {
749
+ const value = key.at(-1);
750
+ return !!value && typeof value === "object" && "universalApiLayer" in value && value.universalApiLayer === 1 && "scope" in value && value.scope === cacheScope;
751
+ },
752
+ request,
753
+ stream
754
+ });
755
+ }
756
+
757
+ export {
758
+ laravelErrors,
759
+ queryIdentity,
760
+ runMutation,
761
+ defaultQueryRetry,
762
+ DEFAULT_TIMEOUT_MS,
763
+ createApiClient
764
+ };