@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,21 @@
1
+ <script lang="ts">
2
+ import type { Readable } from "svelte/store";
3
+ import { useFragno } from "./client.svelte";
4
+
5
+ export let clientObj: Record<string, unknown>;
6
+ export let hookName: string;
7
+ export let args: Record<string, unknown> = {};
8
+
9
+ const fragnoHooks = useFragno(clientObj);
10
+ const hookResult = fragnoHooks[hookName](args);
11
+
12
+ // Export hook results so they can be accessed from tests
13
+ export const loading = hookResult.loading as Readable<boolean>;
14
+ export const data = hookResult.data as Readable<unknown>;
15
+ export const error = hookResult.error as Readable<Error | undefined>;
16
+ export const mutate = hookResult.mutate as CallableFunction;
17
+ </script>
18
+
19
+ <div data-testid="test-component">
20
+ Test Component - Hook: {hookName}
21
+ </div>
@@ -0,0 +1,457 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { handleNdjsonStreamingFirstItem, type NdjsonStreamingStore } from "./ndjson-streaming";
3
+ import { FragnoClientError, FragnoClientFetchAbortError } from "../client-error";
4
+ import { nanoquery } from "@nanostores/query";
5
+ import { z } from "zod";
6
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
7
+ import { createAsyncIteratorFromCallback } from "../../util/async";
8
+
9
+ describe("handleNdjsonStreaming", () => {
10
+ test("should return first item and continue streaming updates", async () => {
11
+ // Create a mock response with streaming body
12
+ const mockReader = {
13
+ read: vi
14
+ .fn()
15
+ .mockResolvedValueOnce({
16
+ done: false,
17
+ value: new TextEncoder().encode('{"id": 1, "name": "Item 1"}\n'),
18
+ })
19
+ .mockResolvedValueOnce({
20
+ done: false,
21
+ value: new TextEncoder().encode('{"id": 2, "name": "Item 2"}\n'),
22
+ })
23
+ .mockResolvedValueOnce({
24
+ done: false,
25
+ value: new TextEncoder().encode('{"id": 3, "name": "Item 3"}\n'),
26
+ })
27
+ .mockResolvedValueOnce({ done: true, value: undefined }),
28
+ releaseLock: vi.fn(),
29
+ };
30
+
31
+ const mockResponse = {
32
+ body: {
33
+ getReader: vi.fn().mockReturnValue(mockReader),
34
+ },
35
+ } as unknown as Response;
36
+
37
+ const mockStore = {
38
+ setData: vi.fn(),
39
+ setError: vi.fn(),
40
+ };
41
+
42
+ const { firstItem, streamingPromise } = await handleNdjsonStreamingFirstItem(
43
+ mockResponse,
44
+ mockStore,
45
+ );
46
+
47
+ // Verify the result structure
48
+ expect(firstItem).toEqual({ id: 1, name: "Item 1" });
49
+ expect(streamingPromise).toBeInstanceOf(Promise);
50
+
51
+ // Wait for the streaming to complete
52
+ const streamingResult = await streamingPromise;
53
+ expect(streamingResult).toEqual([
54
+ { id: 1, name: "Item 1" },
55
+ { id: 2, name: "Item 2" },
56
+ { id: 3, name: "Item 3" },
57
+ ]);
58
+
59
+ // Verify the store was updated with all items
60
+ expect(mockStore.setData).toHaveBeenCalledWith([
61
+ { id: 1, name: "Item 1" },
62
+ { id: 2, name: "Item 2" },
63
+ { id: 3, name: "Item 3" },
64
+ ]);
65
+
66
+ // Verify the reader was properly released
67
+ expect(mockReader.releaseLock).toHaveBeenCalled();
68
+ });
69
+
70
+ test("should handle empty stream", async () => {
71
+ const mockReader = {
72
+ read: vi.fn().mockResolvedValue({ done: true, value: undefined }),
73
+ releaseLock: vi.fn(),
74
+ };
75
+
76
+ const mockResponse = {
77
+ body: {
78
+ getReader: vi.fn().mockReturnValue(mockReader),
79
+ },
80
+ } as unknown as Response;
81
+
82
+ const mockStore = {
83
+ setData: vi.fn(),
84
+ setError: vi.fn(),
85
+ };
86
+
87
+ // Should throw an error for empty stream
88
+ await expect(handleNdjsonStreamingFirstItem(mockResponse, mockStore)).rejects.toThrow(
89
+ "NDJSON stream contained no valid items",
90
+ );
91
+ });
92
+
93
+ test("should handle response without body", async () => {
94
+ const mockResponse = {
95
+ body: null,
96
+ } as unknown as Response;
97
+
98
+ const mockStore = {
99
+ setData: vi.fn(),
100
+ setError: vi.fn(),
101
+ };
102
+
103
+ // Should throw an error for response without body
104
+ await expect(handleNdjsonStreamingFirstItem(mockResponse, mockStore)).rejects.toThrow(
105
+ "Streaming response has no body",
106
+ );
107
+ });
108
+
109
+ test("should handle abort signal that is already aborted", async () => {
110
+ const mockResponse = {
111
+ body: {
112
+ getReader: vi.fn(),
113
+ },
114
+ } as unknown as Response;
115
+
116
+ const mockStore = {
117
+ setData: vi.fn(),
118
+ setError: vi.fn(),
119
+ };
120
+
121
+ const abortController = new AbortController();
122
+ abortController.abort();
123
+
124
+ // Should throw an abort error immediately
125
+ await expect(
126
+ handleNdjsonStreamingFirstItem(mockResponse, mockStore, {
127
+ abortSignal: abortController.signal,
128
+ }),
129
+ ).rejects.toThrow(FragnoClientFetchAbortError);
130
+ });
131
+
132
+ test("should provide streaming promise that can be awaited", async () => {
133
+ const abortController = new AbortController();
134
+
135
+ const mockReader = {
136
+ read: vi
137
+ .fn()
138
+ .mockResolvedValueOnce({
139
+ done: false,
140
+ value: new TextEncoder().encode('{"id": 1, "name": "Item 1"}\n'),
141
+ })
142
+ .mockResolvedValueOnce({
143
+ done: false,
144
+ value: new TextEncoder().encode('{"id": 2, "name": "Item 2"}\n'),
145
+ })
146
+ .mockResolvedValue({ done: true, value: undefined }),
147
+ releaseLock: vi.fn(),
148
+ };
149
+
150
+ const mockResponse = {
151
+ body: {
152
+ getReader: vi.fn().mockReturnValue(mockReader),
153
+ },
154
+ } as unknown as Response;
155
+
156
+ const mockStore = {
157
+ setData: vi.fn(),
158
+ setError: vi.fn(),
159
+ };
160
+
161
+ const result = await handleNdjsonStreamingFirstItem(mockResponse, mockStore, {
162
+ abortSignal: abortController.signal,
163
+ });
164
+
165
+ // The function should succeed in getting the first item
166
+ expect(result.firstItem).toEqual({ id: 1, name: "Item 1" });
167
+
168
+ // The streaming promise should be available for awaiting
169
+ expect(result.streamingPromise).toBeInstanceOf(Promise);
170
+
171
+ // The streaming promise should resolve when all data is processed
172
+ await expect(result.streamingPromise).resolves.toEqual([
173
+ { id: 1, name: "Item 1" },
174
+ { id: 2, name: "Item 2" },
175
+ ]);
176
+
177
+ // Verify all items were processed
178
+ expect(mockStore.setData).toHaveBeenCalledWith([
179
+ { id: 1, name: "Item 1" },
180
+ { id: 2, name: "Item 2" },
181
+ ]);
182
+
183
+ // Verify the reader was properly released
184
+ expect(mockReader.releaseLock).toHaveBeenCalled();
185
+ });
186
+
187
+ test("should handle abort signal with no additional streaming data", async () => {
188
+ const abortController = new AbortController();
189
+
190
+ const mockReader = {
191
+ read: vi
192
+ .fn()
193
+ .mockResolvedValueOnce({
194
+ done: false,
195
+ value: new TextEncoder().encode('{"id": 1, "name": "Item 1"}\n'),
196
+ })
197
+ .mockResolvedValue({ done: true, value: undefined }),
198
+ releaseLock: vi.fn(),
199
+ };
200
+
201
+ const mockResponse = {
202
+ body: {
203
+ getReader: vi.fn().mockReturnValue(mockReader),
204
+ },
205
+ } as unknown as Response;
206
+
207
+ const mockStore = {
208
+ setData: vi.fn(),
209
+ setError: vi.fn(),
210
+ };
211
+
212
+ const result = await handleNdjsonStreamingFirstItem(mockResponse, mockStore, {
213
+ abortSignal: abortController.signal,
214
+ });
215
+
216
+ // Verify the first item is returned
217
+ expect(result.firstItem).toEqual({ id: 1, name: "Item 1" });
218
+
219
+ // The streaming promise should resolve successfully since there's no more data
220
+ await expect(result.streamingPromise).resolves.toEqual([{ id: 1, name: "Item 1" }]);
221
+
222
+ // Verify the reader was properly released
223
+ expect(mockReader.releaseLock).toHaveBeenCalled();
224
+ });
225
+
226
+ test("should complete streaming promise when all data is processed", async () => {
227
+ const mockReader = {
228
+ read: vi
229
+ .fn()
230
+ .mockResolvedValueOnce({
231
+ done: false,
232
+ value: new TextEncoder().encode('{"id": 1, "name": "Item 1"}\n'),
233
+ })
234
+ .mockResolvedValueOnce({
235
+ done: false,
236
+ value: new TextEncoder().encode('{"id": 2, "name": "Item 2"}\n'),
237
+ })
238
+ .mockResolvedValueOnce({ done: true, value: undefined }),
239
+ releaseLock: vi.fn(),
240
+ };
241
+
242
+ const mockResponse = {
243
+ body: {
244
+ getReader: vi.fn().mockReturnValue(mockReader),
245
+ },
246
+ } as unknown as Response;
247
+
248
+ const mockStore = {
249
+ setData: vi.fn(),
250
+ setError: vi.fn(),
251
+ };
252
+
253
+ const result = await handleNdjsonStreamingFirstItem(mockResponse, mockStore);
254
+
255
+ // Verify the first item is returned
256
+ expect(result.firstItem).toEqual({ id: 1, name: "Item 1" });
257
+
258
+ // The streaming promise should resolve successfully
259
+ await expect(result.streamingPromise).resolves.toEqual([
260
+ { id: 1, name: "Item 1" },
261
+ { id: 2, name: "Item 2" },
262
+ ]);
263
+
264
+ // Verify all items were processed
265
+ expect(mockStore.setData).toHaveBeenCalledWith([
266
+ { id: 1, name: "Item 1" },
267
+ { id: 2, name: "Item 2" },
268
+ ]);
269
+
270
+ // Verify the reader was properly released
271
+ expect(mockReader.releaseLock).toHaveBeenCalled();
272
+ });
273
+
274
+ test("should handle streaming errors and set them in the store", async () => {
275
+ const mockReader = {
276
+ read: vi
277
+ .fn()
278
+ .mockResolvedValueOnce({
279
+ done: false,
280
+ value: new TextEncoder().encode('{"id": 1, "name": "Item 1"}\n'),
281
+ })
282
+ .mockRejectedValueOnce(new Error("Network error")),
283
+ releaseLock: vi.fn(),
284
+ };
285
+
286
+ const mockResponse = {
287
+ body: {
288
+ getReader: vi.fn().mockReturnValue(mockReader),
289
+ },
290
+ } as unknown as Response;
291
+
292
+ const mockStore = {
293
+ setData: vi.fn(),
294
+ setError: vi.fn(),
295
+ };
296
+
297
+ const result = await handleNdjsonStreamingFirstItem(mockResponse, mockStore);
298
+
299
+ // Verify the first item is returned
300
+ expect(result.firstItem).toEqual({ id: 1, name: "Item 1" });
301
+
302
+ // The streaming promise should reject with the error
303
+ await expect(result.streamingPromise).rejects.toThrow("Unknown streaming error");
304
+
305
+ // Verify an error was set in the store
306
+ expect(mockStore.setError).toHaveBeenCalledWith(expect.any(Object));
307
+
308
+ // Verify the reader was properly released
309
+ expect(mockReader.releaseLock).toHaveBeenCalled();
310
+ });
311
+
312
+ test("should support abort signal for interrupting hanging operations", async () => {
313
+ // This test validates that the abort signal pattern is correctly implemented
314
+ // The actual interruption behavior depends on the browser's ReadableStream implementation
315
+ const abortController = new AbortController();
316
+
317
+ const mockReader = {
318
+ read: vi
319
+ .fn()
320
+ .mockResolvedValueOnce({
321
+ done: false,
322
+ value: new TextEncoder().encode('{"id": 1, "name": "Item 1"}\n'),
323
+ })
324
+ .mockResolvedValue({ done: true, value: undefined }),
325
+ releaseLock: vi.fn(),
326
+ };
327
+
328
+ const mockResponse = {
329
+ body: {
330
+ getReader: vi.fn().mockReturnValue(mockReader),
331
+ },
332
+ } as unknown as Response;
333
+
334
+ const mockStore = {
335
+ setData: vi.fn(),
336
+ setError: vi.fn(),
337
+ };
338
+
339
+ const result = await handleNdjsonStreamingFirstItem(mockResponse, mockStore, {
340
+ abortSignal: abortController.signal,
341
+ });
342
+
343
+ // Verify the first item is returned
344
+ expect(result.firstItem).toEqual({ id: 1, name: "Item 1" });
345
+
346
+ // The streaming promise should complete successfully since there's no more data
347
+ await expect(result.streamingPromise).resolves.toEqual([{ id: 1, name: "Item 1" }]);
348
+
349
+ expect(mockReader.releaseLock).toHaveBeenCalled();
350
+ });
351
+
352
+ test("should update store as stream progresses", async () => {
353
+ const mockReader = {
354
+ read: vi
355
+ .fn()
356
+ .mockResolvedValueOnce({
357
+ done: false,
358
+ value: new TextEncoder().encode('{"id": 1, "name": "Item 1"}\n'),
359
+ })
360
+ .mockImplementationOnce(async () => {
361
+ await new Promise((resolve) => setTimeout(resolve, 0));
362
+ return {
363
+ done: false,
364
+ value: new TextEncoder().encode('{"id": 2, "name": "Item 2"}\n'),
365
+ };
366
+ })
367
+ .mockImplementationOnce(async () => {
368
+ await new Promise((resolve) => setTimeout(resolve, 0));
369
+ return {
370
+ done: false,
371
+ value: new TextEncoder().encode('{"id": 3, "name": "Item 3"}\n'),
372
+ };
373
+ })
374
+ .mockResolvedValueOnce({ done: true, value: undefined }),
375
+ releaseLock: vi.fn(),
376
+ };
377
+
378
+ const mockResponse = {
379
+ body: {
380
+ getReader: vi.fn().mockReturnValue(mockReader),
381
+ },
382
+ } as unknown as Response;
383
+
384
+ const [, createMutatorStore] = nanoquery();
385
+
386
+ const _schema = z.array(
387
+ z.object({
388
+ id: z.number(),
389
+ name: z.string(),
390
+ }),
391
+ );
392
+
393
+ const mutatorStore = createMutatorStore<
394
+ undefined,
395
+ StandardSchemaV1.InferOutput<typeof _schema>,
396
+ FragnoClientError<string>
397
+ >(() => {
398
+ return Promise.resolve([]);
399
+ });
400
+
401
+ const storeAdapter: NdjsonStreamingStore<typeof _schema, string> = {
402
+ setData: (value) => {
403
+ mutatorStore.set({
404
+ ...mutatorStore.get(),
405
+ data: value,
406
+ });
407
+ },
408
+ setError: (value) => {
409
+ mutatorStore.set({
410
+ ...mutatorStore.get(),
411
+ error: value,
412
+ });
413
+ },
414
+ };
415
+
416
+ const itt = createAsyncIteratorFromCallback(mutatorStore.listen);
417
+ const promise = handleNdjsonStreamingFirstItem(mockResponse, storeAdapter);
418
+
419
+ // We immediately skip to 2 items being available, because normally the fetcher method would
420
+ // return the first item (the `Promise.resolve` above).
421
+ {
422
+ const { value } = await itt.next();
423
+ expect(value).toEqual({
424
+ data: [
425
+ { id: 1, name: "Item 1" },
426
+ { id: 2, name: "Item 2" },
427
+ ],
428
+ loading: false,
429
+ error: undefined,
430
+ mutate: expect.any(Function),
431
+ });
432
+ }
433
+
434
+ {
435
+ const { value } = await itt.next();
436
+ expect(value).toEqual({
437
+ data: [
438
+ { id: 1, name: "Item 1" },
439
+ { id: 2, name: "Item 2" },
440
+ { id: 3, name: "Item 3" },
441
+ ],
442
+ loading: false,
443
+ error: undefined,
444
+ mutate: expect.any(Function),
445
+ });
446
+ }
447
+
448
+ const { streamingPromise } = await promise;
449
+
450
+ const result = await streamingPromise;
451
+ expect(result).toEqual([
452
+ { id: 1, name: "Item 1" },
453
+ { id: 2, name: "Item 2" },
454
+ { id: 3, name: "Item 3" },
455
+ ]);
456
+ });
457
+ });