@fragno-dev/core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/.turbo/turbo-build.log +61 -0
  2. package/.turbo/turbo-types$colon$check.log +2 -0
  3. package/dist/api/api.d.ts +2 -0
  4. package/dist/api/api.js +3 -0
  5. package/dist/api-CBDGZiLC.d.ts +278 -0
  6. package/dist/api-CBDGZiLC.d.ts.map +1 -0
  7. package/dist/api-DgHfYjq2.js +54 -0
  8. package/dist/api-DgHfYjq2.js.map +1 -0
  9. package/dist/client/client.d.ts +3 -0
  10. package/dist/client/client.js +6 -0
  11. package/dist/client/client.svelte.d.ts +33 -0
  12. package/dist/client/client.svelte.d.ts.map +1 -0
  13. package/dist/client/client.svelte.js +123 -0
  14. package/dist/client/client.svelte.js.map +1 -0
  15. package/dist/client/react.d.ts +58 -0
  16. package/dist/client/react.d.ts.map +1 -0
  17. package/dist/client/react.js +80 -0
  18. package/dist/client/react.js.map +1 -0
  19. package/dist/client/vanilla.d.ts +61 -0
  20. package/dist/client/vanilla.d.ts.map +1 -0
  21. package/dist/client/vanilla.js +136 -0
  22. package/dist/client/vanilla.js.map +1 -0
  23. package/dist/client/vue.d.ts +39 -0
  24. package/dist/client/vue.d.ts.map +1 -0
  25. package/dist/client/vue.js +108 -0
  26. package/dist/client/vue.js.map +1 -0
  27. package/dist/client-DWjxKDnE.js +703 -0
  28. package/dist/client-DWjxKDnE.js.map +1 -0
  29. package/dist/client-XFdAy-IQ.d.ts +287 -0
  30. package/dist/client-XFdAy-IQ.d.ts.map +1 -0
  31. package/dist/integrations/astro.d.ts +18 -0
  32. package/dist/integrations/astro.d.ts.map +1 -0
  33. package/dist/integrations/astro.js +16 -0
  34. package/dist/integrations/astro.js.map +1 -0
  35. package/dist/integrations/next-js.d.ts +15 -0
  36. package/dist/integrations/next-js.d.ts.map +1 -0
  37. package/dist/integrations/next-js.js +17 -0
  38. package/dist/integrations/next-js.js.map +1 -0
  39. package/dist/integrations/react-ssr.d.ts +19 -0
  40. package/dist/integrations/react-ssr.d.ts.map +1 -0
  41. package/dist/integrations/react-ssr.js +38 -0
  42. package/dist/integrations/react-ssr.js.map +1 -0
  43. package/dist/integrations/svelte-kit.d.ts +21 -0
  44. package/dist/integrations/svelte-kit.d.ts.map +1 -0
  45. package/dist/integrations/svelte-kit.js +18 -0
  46. package/dist/integrations/svelte-kit.js.map +1 -0
  47. package/dist/mod.d.ts +3 -0
  48. package/dist/mod.js +177 -0
  49. package/dist/mod.js.map +1 -0
  50. package/dist/route-Bp6eByhz.js +331 -0
  51. package/dist/route-Bp6eByhz.js.map +1 -0
  52. package/dist/ssr-tJHqcNSw.js +48 -0
  53. package/dist/ssr-tJHqcNSw.js.map +1 -0
  54. package/package.json +127 -0
  55. package/src/api/api.test.ts +140 -0
  56. package/src/api/api.ts +106 -0
  57. package/src/api/error.ts +47 -0
  58. package/src/api/fragment.test.ts +509 -0
  59. package/src/api/fragment.ts +277 -0
  60. package/src/api/internal/path-runtime.test.ts +121 -0
  61. package/src/api/internal/path-type.test.ts +602 -0
  62. package/src/api/internal/path.ts +322 -0
  63. package/src/api/internal/response-stream.ts +118 -0
  64. package/src/api/internal/route.test.ts +56 -0
  65. package/src/api/internal/route.ts +9 -0
  66. package/src/api/request-input-context.test.ts +437 -0
  67. package/src/api/request-input-context.ts +201 -0
  68. package/src/api/request-middleware.test.ts +544 -0
  69. package/src/api/request-middleware.ts +126 -0
  70. package/src/api/request-output-context.test.ts +626 -0
  71. package/src/api/request-output-context.ts +175 -0
  72. package/src/api/route.test.ts +176 -0
  73. package/src/api/route.ts +152 -0
  74. package/src/client/client-builder.test.ts +264 -0
  75. package/src/client/client-error.test.ts +15 -0
  76. package/src/client/client-error.ts +141 -0
  77. package/src/client/client-types.test.ts +493 -0
  78. package/src/client/client.ssr.test.ts +173 -0
  79. package/src/client/client.svelte.test.ts +837 -0
  80. package/src/client/client.svelte.ts +278 -0
  81. package/src/client/client.test.ts +1690 -0
  82. package/src/client/client.ts +1035 -0
  83. package/src/client/component.test.svelte +21 -0
  84. package/src/client/internal/ndjson-streaming.test.ts +457 -0
  85. package/src/client/internal/ndjson-streaming.ts +248 -0
  86. package/src/client/react.test.ts +947 -0
  87. package/src/client/react.ts +241 -0
  88. package/src/client/vanilla.test.ts +867 -0
  89. package/src/client/vanilla.ts +265 -0
  90. package/src/client/vue.test.ts +754 -0
  91. package/src/client/vue.ts +242 -0
  92. package/src/http/http-status.ts +60 -0
  93. package/src/integrations/astro.ts +17 -0
  94. package/src/integrations/next-js.ts +31 -0
  95. package/src/integrations/react-ssr.ts +40 -0
  96. package/src/integrations/svelte-kit.ts +41 -0
  97. package/src/mod.ts +20 -0
  98. package/src/util/async.test.ts +85 -0
  99. package/src/util/async.ts +96 -0
  100. package/src/util/content-type.test.ts +136 -0
  101. package/src/util/content-type.ts +84 -0
  102. package/src/util/nanostores.test.ts +28 -0
  103. package/src/util/nanostores.ts +65 -0
  104. package/src/util/ssr.ts +75 -0
  105. package/src/util/types-util.ts +16 -0
  106. package/tsconfig.json +10 -0
  107. package/tsdown.config.ts +21 -0
  108. package/vitest.config.ts +10 -0
@@ -0,0 +1,703 @@
1
+ import { RequestInputContext, RequestOutputContext, getMountRoute, resolveRouteFactories } from "./route-Bp6eByhz.js";
2
+ import { SSR_ENABLED, addStore, getInitialData } from "./ssr-tJHqcNSw.js";
3
+ import { task } from "nanostores";
4
+ import { nanoquery } from "@nanostores/query";
5
+
6
+ //#region src/api/internal/path.ts
7
+ /**
8
+ * Extract parameter names from a path pattern at runtime.
9
+ * Examples:
10
+ * - "/users/:id" => ["id"]
11
+ * - "/files/**" => ["**"]
12
+ * - "/files/**:rest" => ["rest"]
13
+ */
14
+ function extractPathParams(pathPattern) {
15
+ const segments = pathPattern.split("/").filter((s) => s.length > 0);
16
+ const names = [];
17
+ for (const segment of segments) {
18
+ if (segment.startsWith(":")) {
19
+ names.push(segment.slice(1));
20
+ continue;
21
+ }
22
+ if (segment === "**") {
23
+ names.push("**");
24
+ continue;
25
+ }
26
+ if (segment.startsWith("**:")) {
27
+ names.push(segment.slice(3));
28
+ continue;
29
+ }
30
+ }
31
+ return names;
32
+ }
33
+ /**
34
+ * Build a concrete path by replacing placeholders in a path pattern with values.
35
+ *
36
+ * Supports the same placeholder syntax as the matcher:
37
+ * - Named parameter ":name" is URL-encoded as a single segment
38
+ * - Anonymous wildcard "**" inserts the remainder as-is (slashes preserved)
39
+ * - Named wildcard "**:name" inserts the remainder from the named key
40
+ *
41
+ * Examples:
42
+ * - buildPath("/users/:id", { id: "123" }) => "/users/123"
43
+ * - buildPath("/files/**", { "**": "a/b" }) => "/files/a/b"
44
+ * - buildPath("/files/**:rest", { rest: "a/b" }) => "/files/a/b"
45
+ */
46
+ function buildPath(pathPattern, params) {
47
+ const patternSegments = pathPattern.split("/");
48
+ const builtSegments = [];
49
+ for (const segment of patternSegments) {
50
+ if (segment.length === 0) {
51
+ builtSegments.push("");
52
+ continue;
53
+ }
54
+ if (segment.startsWith(":")) {
55
+ const name = segment.slice(1);
56
+ const value = params[name];
57
+ if (value === void 0) throw new Error(`Missing value for path parameter :${name}`);
58
+ builtSegments.push(encodeURIComponent(value));
59
+ continue;
60
+ }
61
+ if (segment === "**") {
62
+ const value = params["**"];
63
+ if (value === void 0) throw new Error("Missing value for path wildcard **");
64
+ builtSegments.push(value);
65
+ continue;
66
+ }
67
+ if (segment.startsWith("**:")) {
68
+ const name = segment.slice(3);
69
+ const value = params[name];
70
+ if (value === void 0) throw new Error(`Missing value for path wildcard **:${name}`);
71
+ builtSegments.push(value);
72
+ continue;
73
+ }
74
+ builtSegments.push(segment);
75
+ }
76
+ return builtSegments.join("/");
77
+ }
78
+
79
+ //#endregion
80
+ //#region src/client/client-error.ts
81
+ /**
82
+ * Base error class for all Fragno client errors.
83
+ */
84
+ var FragnoClientError = class extends Error {
85
+ #code;
86
+ constructor(message, code, options = {}) {
87
+ super(message, { cause: options.cause });
88
+ this.name = "FragnoClientError";
89
+ this.#code = code;
90
+ }
91
+ get code() {
92
+ return this.#code;
93
+ }
94
+ };
95
+ var FragnoClientFetchError = class extends FragnoClientError {
96
+ constructor(message, code, options = {}) {
97
+ super(message, code, options);
98
+ this.name = "FragnoClientFetchError";
99
+ }
100
+ static fromUnknownFetchError(error) {
101
+ if (!(error instanceof Error)) return new FragnoClientFetchNetworkError("Network request failed", { cause: error });
102
+ if (error.name === "AbortError") return new FragnoClientFetchAbortError("Request was aborted", { cause: error });
103
+ return new FragnoClientFetchNetworkError("Network request failed", { cause: error });
104
+ }
105
+ };
106
+ /**
107
+ * Error thrown when a network request fails (e.g., no internet connection, DNS failure).
108
+ */
109
+ var FragnoClientFetchNetworkError = class extends FragnoClientFetchError {
110
+ constructor(message = "Network request failed", options = {}) {
111
+ super(message, "NETWORK_ERROR", options);
112
+ this.name = "FragnoClientFetchNetworkError";
113
+ }
114
+ };
115
+ /**
116
+ * Error thrown when a request is aborted (e.g., user cancels request, timeout).
117
+ */
118
+ var FragnoClientFetchAbortError = class extends FragnoClientFetchError {
119
+ constructor(message = "Request was aborted", options = {}) {
120
+ super(message, "ABORT_ERROR", options);
121
+ this.name = "FragnoClientFetchAbortError";
122
+ }
123
+ };
124
+ /**
125
+ * Error thrown when the API result is unexpected, e.g. no json is returned.
126
+ */
127
+ var FragnoClientUnknownApiError = class extends FragnoClientError {
128
+ #status;
129
+ constructor(message = "Unknown API error", status, options = {}) {
130
+ super(message, "UNKNOWN_API_ERROR", options);
131
+ this.name = "FragnoClientUnknownApiError";
132
+ this.#status = status;
133
+ }
134
+ get status() {
135
+ return this.#status;
136
+ }
137
+ };
138
+ var FragnoClientApiError = class FragnoClientApiError extends FragnoClientError {
139
+ #status;
140
+ constructor({ message, code }, status, options = {}) {
141
+ super(message, code, options);
142
+ this.name = "FragnoClientApiError";
143
+ this.#status = status;
144
+ }
145
+ get status() {
146
+ return this.#status;
147
+ }
148
+ /**
149
+ * The error code returned by the API.
150
+ *
151
+ * The type is `TErrorCode` (the set of known error codes for this route), but may also be a string
152
+ * for forward compatibility with future error codes.
153
+ */
154
+ get code() {
155
+ return super.code;
156
+ }
157
+ static async fromResponse(response) {
158
+ const unknown = await response.json();
159
+ const status = response.status;
160
+ if (!("message" in unknown || "code" in unknown)) return new FragnoClientUnknownApiError("Unknown API error", status);
161
+ if (!(typeof unknown.message === "string" && typeof unknown.code === "string")) return new FragnoClientUnknownApiError("Unknown API error", status);
162
+ return new FragnoClientApiError({
163
+ message: unknown.message,
164
+ code: unknown.code
165
+ }, status);
166
+ }
167
+ };
168
+
169
+ //#endregion
170
+ //#region src/util/content-type.ts
171
+ /**
172
+ * Parses a content-type header string into its components
173
+ *
174
+ * @param contentType - The content-type header value to parse
175
+ * @returns A ParsedContentType object or null if the input is invalid
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * const { type, subtype, mediaType, parameters }
180
+ * = parseContentType("application/json; charset=utf-8");
181
+ * console.assert(type === "application");
182
+ * console.assert(subtype === "json");
183
+ * console.assert(mediaType === "application/json");
184
+ * console.assert(parameters["charset"] === "utf-8");
185
+ */
186
+ function parseContentType(contentType) {
187
+ if (!contentType || typeof contentType !== "string") return null;
188
+ const trimmed = contentType.trim();
189
+ if (!trimmed) return null;
190
+ const parts = trimmed.split(";").map((part) => part.trim());
191
+ const mediaType = parts[0];
192
+ if (!mediaType) return null;
193
+ const typeParts = mediaType.split("/");
194
+ if (typeParts.length !== 2) return null;
195
+ const [type, subtype] = typeParts.map((part) => part.trim().toLowerCase());
196
+ if (!type || !subtype) return null;
197
+ const parameters = {};
198
+ for (let i = 1; i < parts.length; i++) {
199
+ const param = parts[i];
200
+ const equalIndex = param.indexOf("=");
201
+ if (equalIndex > 0) {
202
+ const key = param.slice(0, equalIndex).trim().toLowerCase();
203
+ let value = param.slice(equalIndex + 1).trim();
204
+ if (value.startsWith("\"") && value.endsWith("\"")) value = value.slice(1, -1);
205
+ if (key) parameters[key] = value;
206
+ }
207
+ }
208
+ return {
209
+ type,
210
+ subtype,
211
+ mediaType: `${type}/${subtype}`,
212
+ parameters
213
+ };
214
+ }
215
+
216
+ //#endregion
217
+ //#region src/client/internal/ndjson-streaming.ts
218
+ /**
219
+ * Creates a promise that rejects when the abort signal is triggered
220
+ */
221
+ function createAbortPromise(abortSignal) {
222
+ return new Promise((_, reject) => {
223
+ const abortHandler = () => {
224
+ reject(new FragnoClientFetchAbortError("Operation was aborted"));
225
+ };
226
+ if (abortSignal.aborted) abortHandler();
227
+ else abortSignal.addEventListener("abort", abortHandler, { once: true });
228
+ });
229
+ }
230
+ /**
231
+ * Handles NDJSON streaming responses by returning the first item from the fetcher
232
+ * and then continuing to stream updates via the store's mutate method.
233
+ *
234
+ * This makes it so that we can wait until the first chunk before updating the store, if we did
235
+ * not do this, `loading` would briefly be false before the first item would be populated in the
236
+ * result.
237
+ *
238
+ * @param response - The fetch Response object containing the NDJSON stream
239
+ * @param store - The fetcher store to update with streaming data
240
+ * @param abortSignal - Optional AbortSignal to cancel the streaming operation
241
+ * @returns A promise that resolves to an object containing the first item and a streaming promise
242
+ */
243
+ async function handleNdjsonStreamingFirstItem(response, store, options = {}) {
244
+ if (!response.body) throw new FragnoClientFetchError("Streaming response has no body", "NO_BODY");
245
+ const { abortSignal } = options;
246
+ if (abortSignal?.aborted) throw new FragnoClientFetchAbortError("Operation was aborted");
247
+ const decoder = new TextDecoder();
248
+ const reader = response.body.getReader();
249
+ let buffer = "";
250
+ let firstItem = null;
251
+ const items = [];
252
+ try {
253
+ while (firstItem === null) {
254
+ if (abortSignal?.aborted) {
255
+ reader.releaseLock();
256
+ throw new FragnoClientFetchAbortError("Operation was aborted");
257
+ }
258
+ const { done, value } = await (abortSignal ? Promise.race([reader.read(), createAbortPromise(abortSignal)]) : reader.read());
259
+ if (done) break;
260
+ buffer += decoder.decode(value, { stream: true });
261
+ const lines = buffer.split("\n");
262
+ buffer = lines.pop() || "";
263
+ for (const line of lines) {
264
+ if (!line.trim()) continue;
265
+ try {
266
+ const jsonObject = JSON.parse(line);
267
+ items.push(jsonObject);
268
+ if (firstItem === null) {
269
+ firstItem = jsonObject;
270
+ const streamingPromise = continueStreaming(reader, decoder, buffer, items, store, abortSignal);
271
+ return {
272
+ firstItem,
273
+ streamingPromise
274
+ };
275
+ }
276
+ } catch (parseError) {
277
+ throw new FragnoClientUnknownApiError("Failed to parse NDJSON line", 500, { cause: parseError });
278
+ }
279
+ }
280
+ }
281
+ if (firstItem === null) {
282
+ reader.releaseLock();
283
+ throw new FragnoClientUnknownApiError("NDJSON stream contained no valid items", 500);
284
+ }
285
+ reader.releaseLock();
286
+ throw new FragnoClientFetchError("Unexpected end of stream processing", "NO_BODY");
287
+ } catch (error) {
288
+ if (error instanceof FragnoClientError) {
289
+ store?.setError(error);
290
+ throw error;
291
+ } else {
292
+ const clientError = new FragnoClientUnknownApiError("Unknown streaming error", 500, { cause: error });
293
+ store?.setError(clientError);
294
+ throw clientError;
295
+ }
296
+ }
297
+ }
298
+ /**
299
+ * Continues streaming the remaining items in the background
300
+ */
301
+ async function continueStreaming(reader, decoder, initialBuffer, items, store, abortSignal) {
302
+ let buffer = initialBuffer;
303
+ try {
304
+ while (true) {
305
+ if (abortSignal?.aborted) throw new FragnoClientFetchAbortError("Operation was aborted");
306
+ const { done, value } = await (abortSignal ? Promise.race([reader.read(), createAbortPromise(abortSignal)]) : reader.read());
307
+ if (done) {
308
+ if (buffer.trim()) {
309
+ const lines$1 = buffer.split("\n");
310
+ for (const line of lines$1) {
311
+ if (!line.trim()) continue;
312
+ try {
313
+ const jsonObject = JSON.parse(line);
314
+ items.push(jsonObject);
315
+ store?.setData([...items]);
316
+ } catch (parseError) {
317
+ throw new FragnoClientUnknownApiError("Failed to parse NDJSON line", 400, { cause: parseError });
318
+ }
319
+ }
320
+ }
321
+ break;
322
+ }
323
+ buffer += decoder.decode(value, { stream: true });
324
+ const lines = buffer.split("\n");
325
+ buffer = lines.pop() || "";
326
+ for (const line of lines) {
327
+ if (!line.trim()) continue;
328
+ try {
329
+ const jsonObject = JSON.parse(line);
330
+ items.push(jsonObject);
331
+ store?.setData([...items]);
332
+ } catch (parseError) {
333
+ throw new FragnoClientUnknownApiError("Failed to parse NDJSON line", 400, { cause: parseError });
334
+ }
335
+ }
336
+ }
337
+ } catch (error) {
338
+ if (error instanceof FragnoClientError) store?.setError(error);
339
+ else {
340
+ const clientError = new FragnoClientUnknownApiError("Unknown streaming error", 400, { cause: error });
341
+ store?.setError(clientError);
342
+ throw clientError;
343
+ }
344
+ throw error;
345
+ } finally {
346
+ reader.releaseLock();
347
+ }
348
+ return items;
349
+ }
350
+
351
+ //#endregion
352
+ //#region src/util/nanostores.ts
353
+ /**
354
+ * Normalizes a value that could be a plain value, an Atom, or a Vue Ref to a plain value.
355
+ */
356
+ function unwrapAtom(value) {
357
+ if (value && typeof value === "object" && "get" in value && typeof value.get === "function") return value.get();
358
+ return value;
359
+ }
360
+ /**
361
+ * Normalizes an object where values can be plain values, Atoms, or Vue Refs.
362
+ * Returns a new object with all values normalized to plain values.
363
+ */
364
+ function unwrapObject(params) {
365
+ if (!params) return;
366
+ return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, unwrapAtom(value)]));
367
+ }
368
+ function isReadableAtom(value) {
369
+ if (!value) return false;
370
+ if (typeof value !== "object" || value === null) return false;
371
+ if (!("get" in value) || typeof value.get !== "function") return false;
372
+ if (!("lc" in value) || typeof value.lc !== "number") return false;
373
+ if (!("notify" in value) || typeof value.notify !== "function") return false;
374
+ if (!("off" in value) || typeof value.off !== "function") return false;
375
+ if (!("subscribe" in value) || typeof value.subscribe !== "function") return false;
376
+ if (!("value" in value)) return false;
377
+ return true;
378
+ }
379
+
380
+ //#endregion
381
+ //#region src/client/client.ts
382
+ /**
383
+ * Symbols used to identify hook types
384
+ */
385
+ const GET_HOOK_SYMBOL = Symbol("fragno-get-hook");
386
+ const MUTATOR_HOOK_SYMBOL = Symbol("fragno-mutator-hook");
387
+ const STORE_SYMBOL = Symbol("fragno-store");
388
+ function buildUrl(config, params) {
389
+ const { baseUrl = "", mountRoute, path } = config;
390
+ const { pathParams, queryParams } = params ?? {};
391
+ const normalizedPathParams = unwrapObject(pathParams);
392
+ const normalizedQueryParams = unwrapObject(queryParams) ?? {};
393
+ const searchParams = new URLSearchParams(normalizedQueryParams);
394
+ const builtPath = buildPath(path, normalizedPathParams ?? {});
395
+ const search = searchParams.toString() ? `?${searchParams.toString()}` : "";
396
+ return `${baseUrl}${mountRoute}${builtPath}${search}`;
397
+ }
398
+ /**
399
+ * This method returns an array, which can be passed directly to nanostores.
400
+ *
401
+ * The returned array is always: path, pathParams (In order they appear in the path), queryParams (In alphabetical order)
402
+ * Missing pathParams are replaced with "<missing>".
403
+ * @param path
404
+ * @param params
405
+ * @returns
406
+ */
407
+ function getCacheKey(method, path, params) {
408
+ if (!params) return [method, path];
409
+ const { pathParams, queryParams } = params;
410
+ const pathParamValues = extractPathParams(path).map((name) => pathParams?.[name] ?? "<missing>");
411
+ const queryParamValues = queryParams ? Object.keys(queryParams).sort().map((key) => queryParams[key]) : [];
412
+ return [
413
+ method,
414
+ path,
415
+ ...pathParamValues,
416
+ ...queryParamValues
417
+ ];
418
+ }
419
+ function isStreamingResponse(response) {
420
+ const contentType = parseContentType(response.headers.get("content-type"));
421
+ if (!contentType) return false;
422
+ if (!(response.headers.get("transfer-encoding") === "chunked")) return false;
423
+ if (contentType.subtype === "octet-stream") return "octet-stream";
424
+ if (contentType.subtype === "x-ndjson") return "ndjson";
425
+ return false;
426
+ }
427
+ function isGetHook(hook) {
428
+ return typeof hook === "object" && hook !== null && GET_HOOK_SYMBOL in hook && hook[GET_HOOK_SYMBOL] === true;
429
+ }
430
+ function isMutatorHook(hook) {
431
+ return typeof hook === "object" && hook !== null && MUTATOR_HOOK_SYMBOL in hook && hook[MUTATOR_HOOK_SYMBOL] === true;
432
+ }
433
+ function isStore(obj) {
434
+ return typeof obj === "object" && obj !== null && STORE_SYMBOL in obj && obj[STORE_SYMBOL] === true;
435
+ }
436
+ var ClientBuilder = class {
437
+ #publicConfig;
438
+ #fragmentConfig;
439
+ #cache = /* @__PURE__ */ new Map();
440
+ #createFetcherStore;
441
+ #createMutatorStore;
442
+ #invalidateKeys;
443
+ constructor(publicConfig, fragmentConfig) {
444
+ this.#publicConfig = publicConfig;
445
+ this.#fragmentConfig = fragmentConfig;
446
+ const [createFetcherStore, createMutatorStore, { invalidateKeys }] = nanoquery({ cache: this.#cache });
447
+ this.#createFetcherStore = createFetcherStore;
448
+ this.#createMutatorStore = createMutatorStore;
449
+ this.#invalidateKeys = invalidateKeys;
450
+ }
451
+ get cacheEntries() {
452
+ return Object.fromEntries(this.#cache.entries());
453
+ }
454
+ createStore(obj) {
455
+ return {
456
+ obj,
457
+ [STORE_SYMBOL]: true
458
+ };
459
+ }
460
+ createHook(path, options) {
461
+ const route = this.#fragmentConfig.routes.find((r) => r.path === path && r.method === "GET" && r.outputSchema !== void 0);
462
+ if (!route) throw new Error(`Route '${path}' not found or is not a GET route with an output schema.`);
463
+ return this.#createRouteQueryHook(route, options);
464
+ }
465
+ createMutator(method, path, onInvalidate) {
466
+ const route = this.#fragmentConfig.routes.find((r) => r.method !== "GET" && r.path === path && r.method === method);
467
+ if (!route) throw new Error(`Route '${path}' not found or is a GET route with an input and output schema.`);
468
+ return this.#createRouteQueryMutator(route, onInvalidate);
469
+ }
470
+ #createRouteQueryHook(route, options = {}) {
471
+ if (route.method !== "GET") throw new Error(`Only GET routes are supported for hooks. Route '${route.path}' is a ${route.method} route.`);
472
+ if (!route.outputSchema) throw new Error(`Output schema is required for GET routes. Route '${route.path}' has no output schema.`);
473
+ const baseUrl = this.#publicConfig.baseUrl ?? "";
474
+ const mountRoute = getMountRoute(this.#fragmentConfig);
475
+ async function callServerSideHandler(params) {
476
+ const { pathParams, queryParams } = params ?? {};
477
+ const normalizedPathParams = unwrapObject(pathParams);
478
+ const normalizedQueryParams = unwrapObject(queryParams) ?? {};
479
+ const searchParams = new URLSearchParams(normalizedQueryParams);
480
+ return await route.handler(RequestInputContext.fromSSRContext({
481
+ method: route.method,
482
+ path: route.path,
483
+ pathParams: normalizedPathParams,
484
+ searchParams
485
+ }), new RequestOutputContext(route.outputSchema));
486
+ }
487
+ async function executeQuery(params) {
488
+ const { pathParams, queryParams } = params ?? {};
489
+ if (typeof window === "undefined") return task(async () => callServerSideHandler({
490
+ pathParams,
491
+ queryParams
492
+ }));
493
+ const url = buildUrl({
494
+ baseUrl,
495
+ mountRoute,
496
+ path: route.path
497
+ }, {
498
+ pathParams,
499
+ queryParams
500
+ });
501
+ let response;
502
+ try {
503
+ response = await fetch(url);
504
+ } catch (error) {
505
+ throw FragnoClientFetchError.fromUnknownFetchError(error);
506
+ }
507
+ if (!response.ok) throw await FragnoClientApiError.fromResponse(response);
508
+ return response;
509
+ }
510
+ return {
511
+ route,
512
+ store: (args) => {
513
+ const { path, query } = args ?? {};
514
+ const key = getCacheKey(route.method, route.path, {
515
+ pathParams: path,
516
+ queryParams: query
517
+ });
518
+ const store = this.#createFetcherStore(key, {
519
+ fetcher: async () => {
520
+ if (SSR_ENABLED) {
521
+ const initialData = getInitialData(key.map((d) => typeof d === "string" ? d : d.get()).join(""));
522
+ if (initialData) return initialData;
523
+ }
524
+ const response = await executeQuery({
525
+ pathParams: path,
526
+ queryParams: query
527
+ });
528
+ const isStreaming = isStreamingResponse(response);
529
+ if (!isStreaming) return response.json();
530
+ if (typeof window === "undefined") return [];
531
+ if (isStreaming === "ndjson") {
532
+ const { firstItem } = await handleNdjsonStreamingFirstItem(response, {
533
+ setData: (value) => {
534
+ store.set({
535
+ ...store.get(),
536
+ loading: !(Array.isArray(value) && value.length > 0),
537
+ data: value
538
+ });
539
+ },
540
+ setError: (value) => {
541
+ store.set({
542
+ ...store.get(),
543
+ error: value
544
+ });
545
+ }
546
+ });
547
+ return [firstItem];
548
+ }
549
+ if (isStreaming === "octet-stream") throw new Error("Octet-stream streaming is not supported.");
550
+ throw new Error("Unreachable");
551
+ },
552
+ onErrorRetry: options?.onErrorRetry,
553
+ dedupeTime: Infinity
554
+ });
555
+ if (typeof window === "undefined") addStore(store);
556
+ return store;
557
+ },
558
+ query: async (args) => {
559
+ const { path, query } = args ?? {};
560
+ const response = await executeQuery({
561
+ pathParams: path,
562
+ queryParams: query
563
+ });
564
+ const isStreaming = isStreamingResponse(response);
565
+ if (!isStreaming) return await response.json();
566
+ if (isStreaming === "ndjson") {
567
+ const { streamingPromise } = await handleNdjsonStreamingFirstItem(response);
568
+ return await streamingPromise;
569
+ }
570
+ if (isStreaming === "octet-stream") throw new Error("Octet-stream streaming is not supported.");
571
+ throw new Error("Unreachable");
572
+ },
573
+ [GET_HOOK_SYMBOL]: true
574
+ };
575
+ }
576
+ #createRouteQueryMutator(route, onInvalidate = (invalidate, params) => invalidate("GET", route.path, params)) {
577
+ const method = route.method;
578
+ const baseUrl = this.#publicConfig.baseUrl ?? "";
579
+ const mountRoute = getMountRoute(this.#fragmentConfig);
580
+ async function executeMutateQuery({ body, path, query }) {
581
+ if (typeof window === "undefined") return task(async () => route.handler(RequestInputContext.fromSSRContext({
582
+ inputSchema: route.inputSchema,
583
+ method,
584
+ path: route.path,
585
+ pathParams: path ?? {},
586
+ searchParams: new URLSearchParams(query),
587
+ body
588
+ }), new RequestOutputContext(route.outputSchema)));
589
+ const url = buildUrl({
590
+ baseUrl,
591
+ mountRoute,
592
+ path: route.path
593
+ }, {
594
+ pathParams: path,
595
+ queryParams: query
596
+ });
597
+ let response;
598
+ try {
599
+ response = await fetch(url, {
600
+ method,
601
+ body: body !== void 0 ? JSON.stringify(body) : void 0
602
+ });
603
+ } catch (error) {
604
+ throw FragnoClientFetchError.fromUnknownFetchError(error);
605
+ }
606
+ if (!response.ok) throw await FragnoClientApiError.fromResponse(response);
607
+ return response;
608
+ }
609
+ const mutatorStore = this.#createMutatorStore(async ({ data }) => {
610
+ if (typeof window === "undefined") {}
611
+ const { body, path, query } = data;
612
+ if (typeof body === "undefined" && route.inputSchema !== void 0) throw new Error("Body is required.");
613
+ const response = await executeMutateQuery({
614
+ body,
615
+ path,
616
+ query
617
+ });
618
+ onInvalidate(this.#invalidate.bind(this), {
619
+ pathParams: path ?? {},
620
+ queryParams: query
621
+ });
622
+ if (response.status === 201 || response.status === 204) return;
623
+ const isStreaming = isStreamingResponse(response);
624
+ if (!isStreaming) return response.json();
625
+ if (typeof window === "undefined") return [];
626
+ if (isStreaming === "ndjson") {
627
+ const { firstItem } = await handleNdjsonStreamingFirstItem(response, {
628
+ setData: (value) => {
629
+ mutatorStore.set({
630
+ ...mutatorStore.get(),
631
+ loading: !(Array.isArray(value) && value.length > 0),
632
+ data: value
633
+ });
634
+ },
635
+ setError: (value) => {
636
+ mutatorStore.set({
637
+ ...mutatorStore.get(),
638
+ error: value
639
+ });
640
+ }
641
+ });
642
+ return [firstItem];
643
+ }
644
+ if (isStreaming === "octet-stream") throw new Error("Octet-stream streaming is not supported.");
645
+ throw new Error("Unreachable");
646
+ }, { onError: (error) => {
647
+ console.error("Error in mutatorStore", error);
648
+ } });
649
+ const mutateQuery = (async (data) => {
650
+ const { body, path, query } = data;
651
+ if (typeof body === "undefined" && route.inputSchema !== void 0) throw new Error("Body is required for mutateQuery");
652
+ const response = await executeMutateQuery({
653
+ body,
654
+ path,
655
+ query
656
+ });
657
+ if (response.status === 201 || response.status === 204) return;
658
+ const isStreaming = isStreamingResponse(response);
659
+ if (!isStreaming) return response.json();
660
+ if (isStreaming === "ndjson") {
661
+ const { streamingPromise } = await handleNdjsonStreamingFirstItem(response);
662
+ return await streamingPromise;
663
+ }
664
+ if (isStreaming === "octet-stream") throw new Error("Octet-stream streaming is not supported for mutations");
665
+ throw new Error("Unreachable");
666
+ });
667
+ return {
668
+ route,
669
+ mutateQuery,
670
+ mutatorStore,
671
+ [MUTATOR_HOOK_SYMBOL]: true
672
+ };
673
+ }
674
+ #invalidate(method, path, params) {
675
+ const prefix = getCacheKey(method, path, {
676
+ pathParams: params?.pathParams,
677
+ queryParams: params?.queryParams
678
+ }).map((k) => typeof k === "string" ? k : k.get()).join("");
679
+ this.#invalidateKeys((key) => key.startsWith(prefix));
680
+ }
681
+ };
682
+ function createClientBuilder(fragmentDefinition, publicConfig, routesOrFactories) {
683
+ const definition = fragmentDefinition.definition;
684
+ const routes = resolveRouteFactories({
685
+ config: {},
686
+ deps: {},
687
+ services: {}
688
+ }, routesOrFactories);
689
+ const fragmentConfig = {
690
+ name: definition.name,
691
+ routes
692
+ };
693
+ const mountRoute = publicConfig.mountRoute ?? `/${definition.name}`;
694
+ const fullPublicConfig = {
695
+ ...publicConfig,
696
+ mountRoute
697
+ };
698
+ return new ClientBuilder(fullPublicConfig, fragmentConfig);
699
+ }
700
+
701
+ //#endregion
702
+ export { ClientBuilder, FragnoClientApiError, FragnoClientError, FragnoClientFetchAbortError, FragnoClientFetchError, FragnoClientFetchNetworkError, FragnoClientUnknownApiError, buildUrl, createClientBuilder, getCacheKey, isGetHook, isMutatorHook, isReadableAtom, isStore };
703
+ //# sourceMappingURL=client-DWjxKDnE.js.map