@fragno-dev/core 0.1.11 → 0.2.2

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 (155) hide show
  1. package/.turbo/turbo-build.log +87 -69
  2. package/CHANGELOG.md +79 -0
  3. package/dist/api/api.d.ts +21 -2
  4. package/dist/api/api.d.ts.map +1 -1
  5. package/dist/api/api.js +2 -1
  6. package/dist/api/api.js.map +1 -1
  7. package/dist/api/bind-services.d.ts +0 -1
  8. package/dist/api/bind-services.d.ts.map +1 -1
  9. package/dist/api/bind-services.js.map +1 -1
  10. package/dist/api/error.d.ts.map +1 -1
  11. package/dist/api/error.js.map +1 -1
  12. package/dist/api/fragment-definition-builder.d.ts +32 -40
  13. package/dist/api/fragment-definition-builder.d.ts.map +1 -1
  14. package/dist/api/fragment-definition-builder.js +15 -21
  15. package/dist/api/fragment-definition-builder.js.map +1 -1
  16. package/dist/api/fragment-instantiator.d.ts +51 -30
  17. package/dist/api/fragment-instantiator.d.ts.map +1 -1
  18. package/dist/api/fragment-instantiator.js +201 -52
  19. package/dist/api/fragment-instantiator.js.map +1 -1
  20. package/dist/api/request-context-storage.d.ts +4 -0
  21. package/dist/api/request-context-storage.d.ts.map +1 -1
  22. package/dist/api/request-context-storage.js +6 -0
  23. package/dist/api/request-context-storage.js.map +1 -1
  24. package/dist/api/request-input-context.d.ts +57 -1
  25. package/dist/api/request-input-context.d.ts.map +1 -1
  26. package/dist/api/request-input-context.js +67 -0
  27. package/dist/api/request-input-context.js.map +1 -1
  28. package/dist/api/request-middleware.d.ts +2 -2
  29. package/dist/api/request-middleware.d.ts.map +1 -1
  30. package/dist/api/request-middleware.js.map +1 -1
  31. package/dist/api/request-output-context.d.ts +1 -1
  32. package/dist/api/request-output-context.d.ts.map +1 -1
  33. package/dist/api/request-output-context.js.map +1 -1
  34. package/dist/api/route-caller.d.ts +30 -0
  35. package/dist/api/route-caller.d.ts.map +1 -0
  36. package/dist/api/route-caller.js +63 -0
  37. package/dist/api/route-caller.js.map +1 -0
  38. package/dist/api/route-handler-input-options.d.ts.map +1 -1
  39. package/dist/api/route.d.ts +8 -8
  40. package/dist/api/route.d.ts.map +1 -1
  41. package/dist/api/route.js.map +1 -1
  42. package/dist/api/shared-types.d.ts.map +1 -1
  43. package/dist/client/client-error.d.ts.map +1 -1
  44. package/dist/client/client-error.js.map +1 -1
  45. package/dist/client/client.d.ts +90 -50
  46. package/dist/client/client.d.ts.map +1 -1
  47. package/dist/client/client.js +128 -16
  48. package/dist/client/client.js.map +1 -1
  49. package/dist/client/client.svelte.d.ts +6 -5
  50. package/dist/client/client.svelte.d.ts.map +1 -1
  51. package/dist/client/client.svelte.js +10 -2
  52. package/dist/client/client.svelte.js.map +1 -1
  53. package/dist/client/internal/ndjson-streaming.js.map +1 -1
  54. package/dist/client/react.d.ts +5 -4
  55. package/dist/client/react.d.ts.map +1 -1
  56. package/dist/client/react.js +104 -12
  57. package/dist/client/react.js.map +1 -1
  58. package/dist/client/solid.d.ts +7 -5
  59. package/dist/client/solid.d.ts.map +1 -1
  60. package/dist/client/solid.js +23 -9
  61. package/dist/client/solid.js.map +1 -1
  62. package/dist/client/vanilla.d.ts +16 -4
  63. package/dist/client/vanilla.d.ts.map +1 -1
  64. package/dist/client/vanilla.js +21 -1
  65. package/dist/client/vanilla.js.map +1 -1
  66. package/dist/client/vue.d.ts +10 -4
  67. package/dist/client/vue.d.ts.map +1 -1
  68. package/dist/client/vue.js +24 -1
  69. package/dist/client/vue.js.map +1 -1
  70. package/dist/id.d.ts +2 -0
  71. package/dist/id.js +3 -0
  72. package/dist/internal/cuid.d.ts +16 -0
  73. package/dist/internal/cuid.d.ts.map +1 -0
  74. package/dist/internal/cuid.js +82 -0
  75. package/dist/internal/cuid.js.map +1 -0
  76. package/dist/internal/trace-context.d.ts +23 -0
  77. package/dist/internal/trace-context.d.ts.map +1 -0
  78. package/dist/internal/trace-context.js +14 -0
  79. package/dist/internal/trace-context.js.map +1 -0
  80. package/dist/mod-client.d.ts +7 -20
  81. package/dist/mod-client.d.ts.map +1 -1
  82. package/dist/mod-client.js +25 -13
  83. package/dist/mod-client.js.map +1 -1
  84. package/dist/mod.d.ts +8 -6
  85. package/dist/mod.js +3 -1
  86. package/dist/runtime.d.ts +15 -0
  87. package/dist/runtime.d.ts.map +1 -0
  88. package/dist/runtime.js +33 -0
  89. package/dist/runtime.js.map +1 -0
  90. package/dist/test/test.d.ts +6 -6
  91. package/dist/test/test.d.ts.map +1 -1
  92. package/dist/test/test.js.map +1 -1
  93. package/dist/util/ssr.js.map +1 -1
  94. package/package.json +42 -52
  95. package/src/api/api.test.ts +3 -1
  96. package/src/api/api.ts +28 -0
  97. package/src/api/bind-services.ts +0 -5
  98. package/src/api/error.ts +1 -0
  99. package/src/api/fragment-definition-builder.extend.test.ts +2 -1
  100. package/src/api/fragment-definition-builder.test.ts +2 -1
  101. package/src/api/fragment-definition-builder.ts +56 -112
  102. package/src/api/fragment-instantiator.test.ts +311 -166
  103. package/src/api/fragment-instantiator.ts +470 -131
  104. package/src/api/fragment-services.test.ts +1 -0
  105. package/src/api/internal/path-runtime.test.ts +8 -0
  106. package/src/api/internal/path-type.test.ts +3 -1
  107. package/src/api/internal/route.test.ts +1 -0
  108. package/src/api/request-context-storage.ts +7 -0
  109. package/src/api/request-input-context.test.ts +156 -2
  110. package/src/api/request-input-context.ts +87 -1
  111. package/src/api/request-middleware.test.ts +43 -2
  112. package/src/api/request-middleware.ts +4 -3
  113. package/src/api/request-output-context.test.ts +3 -1
  114. package/src/api/request-output-context.ts +2 -1
  115. package/src/api/route-caller.test.ts +195 -0
  116. package/src/api/route-caller.ts +167 -0
  117. package/src/api/route-handler-input-options.ts +2 -1
  118. package/src/api/route.test.ts +4 -2
  119. package/src/api/route.ts +9 -3
  120. package/src/api/shared-types.ts +2 -1
  121. package/src/client/client-builder.test.ts +4 -2
  122. package/src/client/client-error.test.ts +2 -1
  123. package/src/client/client-error.ts +1 -1
  124. package/src/client/client-types.test.ts +19 -5
  125. package/src/client/client.ssr.test.ts +6 -4
  126. package/src/client/client.svelte.test.ts +18 -9
  127. package/src/client/client.svelte.ts +38 -13
  128. package/src/client/client.test.ts +244 -10
  129. package/src/client/client.ts +473 -148
  130. package/src/client/internal/ndjson-streaming.test.ts +6 -3
  131. package/src/client/internal/ndjson-streaming.ts +1 -0
  132. package/src/client/react.test.ts +176 -6
  133. package/src/client/react.ts +226 -31
  134. package/src/client/solid.test.ts +29 -5
  135. package/src/client/solid.ts +60 -22
  136. package/src/client/vanilla.test.ts +148 -6
  137. package/src/client/vanilla.ts +63 -9
  138. package/src/client/vue.test.ts +397 -8
  139. package/src/client/vue.ts +74 -4
  140. package/src/id.ts +1 -0
  141. package/src/internal/cuid.test.ts +164 -0
  142. package/src/internal/cuid.ts +133 -0
  143. package/src/internal/trace-context.ts +35 -0
  144. package/src/mod-client.ts +55 -9
  145. package/src/mod.ts +9 -3
  146. package/src/runtime.ts +48 -0
  147. package/src/test/test.test.ts +4 -2
  148. package/src/test/test.ts +14 -7
  149. package/src/util/async.test.ts +1 -0
  150. package/src/util/content-type.test.ts +1 -0
  151. package/src/util/nanostores.test.ts +3 -1
  152. package/src/util/ssr.ts +1 -0
  153. package/tsconfig.json +1 -1
  154. package/tsdown.config.ts +2 -0
  155. package/vitest.config.ts +2 -1
@@ -0,0 +1,167 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+
3
+ import type { ExtractRouteByPath, ExtractRoutePath } from "../client/client";
4
+ import type { InferOrUnknown } from "../util/types-util";
5
+ import type { HTTPMethod } from "./api";
6
+ import type { FragnoResponse } from "./fragno-response";
7
+ import { parseFragnoResponse } from "./fragno-response";
8
+ import { buildPath, type ExtractPathParams } from "./internal/path";
9
+ import type { AnyFragnoRouteConfig } from "./route";
10
+ import type { RouteHandlerInputOptions } from "./route-handler-input-options";
11
+
12
+ export type RouteCallerConfig = {
13
+ baseUrl: string | URL;
14
+ mountRoute?: string;
15
+ baseHeaders?: HeadersInit;
16
+ fetch: (request: Request) => Promise<Response>;
17
+ redirect?: RequestRedirect;
18
+ };
19
+
20
+ type ArrayBufferViewOfArrayBuffer = ArrayBufferView & { buffer: ArrayBuffer };
21
+
22
+ type FragmentLike = {
23
+ routes?: readonly AnyFragnoRouteConfig[];
24
+ callRoute?: (...args: never[]) => Promise<unknown>;
25
+ };
26
+
27
+ type RouteCallerPath<
28
+ TRoutes extends readonly AnyFragnoRouteConfig[],
29
+ TMethod extends HTTPMethod,
30
+ > = [ExtractRoutePath<TRoutes, TMethod>] extends [never]
31
+ ? string
32
+ : ExtractRoutePath<TRoutes, TMethod>;
33
+
34
+ type RouteCallerMatch<
35
+ TRoutes extends readonly AnyFragnoRouteConfig[],
36
+ TMethod extends HTTPMethod,
37
+ TPath extends string,
38
+ > = [ExtractRouteByPath<TRoutes, TPath, TMethod>] extends [never]
39
+ ? AnyFragnoRouteConfig
40
+ : ExtractRouteByPath<TRoutes, TPath, TMethod>;
41
+
42
+ export type RouteCallerForFragment<TFragment extends FragmentLike> = TFragment extends {
43
+ routes: infer TRoutes extends readonly AnyFragnoRouteConfig[];
44
+ }
45
+ ? <TMethod extends HTTPMethod, TPath extends RouteCallerPath<TRoutes, TMethod>>(
46
+ method: TMethod,
47
+ path: TPath,
48
+ inputOptions?: RouteHandlerInputOptions<
49
+ TPath,
50
+ RouteCallerMatch<TRoutes, TMethod, TPath>["inputSchema"]
51
+ >,
52
+ ) => Promise<
53
+ FragnoResponse<
54
+ InferOrUnknown<NonNullable<RouteCallerMatch<TRoutes, TMethod, TPath>["outputSchema"]>>
55
+ >
56
+ >
57
+ : TFragment extends { callRoute: (...args: never[]) => Promise<unknown> }
58
+ ? TFragment["callRoute"]
59
+ : never;
60
+
61
+ function isArrayBufferView(value: unknown): value is ArrayBufferViewOfArrayBuffer {
62
+ return ArrayBuffer.isView(value) && value.buffer instanceof ArrayBuffer;
63
+ }
64
+
65
+ function buildMountedPath(mountRoute: string, pathname: string): string {
66
+ const normalizedMount =
67
+ mountRoute === "/" ? "" : mountRoute.endsWith("/") ? mountRoute.slice(0, -1) : mountRoute;
68
+ const pathPart = pathname.startsWith("/") ? pathname : `/${pathname}`;
69
+ if (!normalizedMount) {
70
+ return pathPart;
71
+ }
72
+ const mountPart = normalizedMount.startsWith("/") ? normalizedMount : `/${normalizedMount}`;
73
+ return `${mountPart}${pathPart}`;
74
+ }
75
+
76
+ export function createRouteCaller<TFragment extends FragmentLike>(
77
+ config: RouteCallerConfig,
78
+ ): RouteCallerForFragment<TFragment> {
79
+ const { baseUrl, fetch } = config;
80
+ const mountRoute = config.mountRoute ?? "";
81
+ const baseHeaders = config.baseHeaders ? new Headers(config.baseHeaders) : undefined;
82
+ const redirect = config.redirect ?? "manual";
83
+
84
+ const callRoute = async <TPath extends string>(
85
+ method: HTTPMethod,
86
+ path: TPath,
87
+ inputOptions?: RouteHandlerInputOptions<TPath, StandardSchemaV1 | undefined>,
88
+ ): Promise<FragnoResponse<unknown>> => {
89
+ const headers = baseHeaders ? new Headers(baseHeaders) : new Headers();
90
+ const explicitHeaders = inputOptions?.headers
91
+ ? inputOptions.headers instanceof Headers
92
+ ? inputOptions.headers
93
+ : new Headers(inputOptions.headers)
94
+ : null;
95
+
96
+ if (explicitHeaders) {
97
+ for (const [key, value] of explicitHeaders.entries()) {
98
+ headers.set(key, value);
99
+ }
100
+ }
101
+
102
+ const hasExplicitContentType = explicitHeaders?.has("content-type") ?? false;
103
+
104
+ const searchParams =
105
+ inputOptions?.query instanceof URLSearchParams
106
+ ? new URLSearchParams(inputOptions.query)
107
+ : new URLSearchParams();
108
+
109
+ if (inputOptions?.query && !(inputOptions.query instanceof URLSearchParams)) {
110
+ for (const [key, value] of Object.entries(inputOptions.query)) {
111
+ if (value !== undefined) {
112
+ searchParams.set(key, value);
113
+ }
114
+ }
115
+ }
116
+
117
+ const pathParams = (inputOptions?.pathParams ?? {}) as ExtractPathParams<TPath>;
118
+ const pathname = buildPath(path, pathParams);
119
+ const url = new URL(buildMountedPath(mountRoute, pathname), baseUrl);
120
+ url.search = searchParams.toString();
121
+
122
+ let body: BodyInit | undefined;
123
+ if (inputOptions && "body" in inputOptions) {
124
+ const rawBody = (inputOptions as { body?: unknown }).body;
125
+
126
+ if (rawBody instanceof FormData || rawBody instanceof Blob) {
127
+ body = rawBody;
128
+ if (!hasExplicitContentType) {
129
+ headers.delete("content-type");
130
+ }
131
+ } else if (rawBody instanceof ReadableStream) {
132
+ body = rawBody;
133
+ if (!hasExplicitContentType) {
134
+ headers.delete("content-type");
135
+ }
136
+ } else if (rawBody instanceof ArrayBuffer) {
137
+ body = rawBody;
138
+ if (!hasExplicitContentType) {
139
+ headers.delete("content-type");
140
+ }
141
+ } else if (isArrayBufferView(rawBody)) {
142
+ body = rawBody;
143
+ if (!hasExplicitContentType) {
144
+ headers.delete("content-type");
145
+ }
146
+ } else if (rawBody !== undefined) {
147
+ body = JSON.stringify(rawBody);
148
+ if (!hasExplicitContentType) {
149
+ headers.set("content-type", "application/json");
150
+ }
151
+ }
152
+ }
153
+
154
+ const response = await fetch(
155
+ new Request(url, {
156
+ method,
157
+ headers,
158
+ body,
159
+ redirect,
160
+ }),
161
+ );
162
+
163
+ return parseFragnoResponse(response);
164
+ };
165
+
166
+ return callRoute as RouteCallerForFragment<TFragment>;
167
+ }
@@ -1,6 +1,7 @@
1
1
  import type { StandardSchemaV1 } from "@standard-schema/spec";
2
- import type { ExtractPathParams } from "./internal/path";
2
+
3
3
  import type { InferOr } from "../util/types-util";
4
+ import type { ExtractPathParams } from "./internal/path";
4
5
 
5
6
  /**
6
7
  * Options for calling a route handler
@@ -1,8 +1,10 @@
1
1
  import { test, expect, expectTypeOf, describe } from "vitest";
2
- import { defineRoute, defineRoutes } from "./route";
3
- import { defineFragment } from "./fragment-definition-builder";
2
+
4
3
  import { z } from "zod";
5
4
 
5
+ import { defineFragment } from "./fragment-definition-builder";
6
+ import { defineRoute, defineRoutes } from "./route";
7
+
6
8
  describe("defineRoute", () => {
7
9
  test("defineRoute no inputSchema", () => {
8
10
  const route = defineRoute({
package/src/api/route.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  // oxlint-disable no-explicit-any
2
2
 
3
3
  import type { StandardSchemaV1 } from "@standard-schema/spec";
4
+
4
5
  import type { FragnoRouteConfig, HTTPMethod, RequestThisContext } from "./api";
5
- import type { FragmentDefinition } from "./fragment-definition-builder";
6
6
  import type { BoundServices } from "./bind-services";
7
+ import type { FragmentDefinition } from "./fragment-definition-builder";
7
8
 
8
9
  export type AnyFragnoRouteConfig = FragnoRouteConfig<HTTPMethod, string, any, any, any, any, any>;
9
10
 
@@ -182,18 +183,19 @@ export type AnyFragmentDefinition = FragmentDefinition<
182
183
  any,
183
184
  any,
184
185
  any,
186
+ any,
185
187
  any
186
188
  >;
187
189
 
188
190
  // Extract config from FragmentDefinition
189
191
  export type ExtractFragmentConfig<T> =
190
- T extends FragmentDefinition<infer TConfig, any, any, any, any, any, any, any, any, any>
192
+ T extends FragmentDefinition<infer TConfig, any, any, any, any, any, any, any, any, any, any>
191
193
  ? TConfig
192
194
  : never;
193
195
 
194
196
  // Extract deps from FragmentDefinition
195
197
  export type ExtractFragmentDeps<T> =
196
- T extends FragmentDefinition<any, any, infer TDeps, any, any, any, any, any, any, any>
198
+ T extends FragmentDefinition<any, any, infer TDeps, any, any, any, any, any, any, any, any>
197
199
  ? TDeps
198
200
  : never;
199
201
 
@@ -211,6 +213,7 @@ export type ExtractFragmentServices<T> =
211
213
  any,
212
214
  any,
213
215
  any,
216
+ any,
214
217
  any
215
218
  >
216
219
  ? BoundServices<TBaseServices & TServices>
@@ -228,6 +231,7 @@ export type ExtractFragmentServiceDeps<T> =
228
231
  any,
229
232
  any,
230
233
  any,
234
+ any,
231
235
  any
232
236
  >
233
237
  ? TServiceDependencies
@@ -245,6 +249,7 @@ export type ExtractFragmentServiceThisContext<T> =
245
249
  any,
246
250
  infer TServiceThisContext,
247
251
  any,
252
+ any,
248
253
  any
249
254
  >
250
255
  ? TServiceThisContext
@@ -262,6 +267,7 @@ export type ExtractFragmentHandlerThisContext<T> =
262
267
  any,
263
268
  any,
264
269
  infer THandlerThisContext,
270
+ any,
265
271
  any
266
272
  >
267
273
  ? THandlerThisContext
@@ -1,6 +1,7 @@
1
- import type { HTTPMethod } from "./api";
2
1
  import type { StandardSchemaV1 } from "@standard-schema/spec";
3
2
 
3
+ import type { HTTPMethod } from "./api";
4
+
4
5
  /**
5
6
  * Public configuration for Fragno fragments on the server side.
6
7
  */
@@ -1,9 +1,11 @@
1
1
  import { test, expect, expectTypeOf, describe } from "vitest";
2
+
2
3
  import { z } from "zod";
3
- import { createClientBuilder } from "./client";
4
- import { defineRoute } from "../api/route";
4
+
5
5
  import { defineFragment } from "../api/fragment-definition-builder";
6
+ import { defineRoute } from "../api/route";
6
7
  import type { FragnoPublicClientConfig } from "../api/shared-types";
8
+ import { createClientBuilder } from "./client";
7
9
 
8
10
  // Test route configurations
9
11
  const testFragment = defineFragment("test-fragment").build();
@@ -1,7 +1,8 @@
1
1
  import { test, expect, describe } from "vitest";
2
- import { FragnoClientApiError, FragnoClientUnknownApiError } from "./client-error";
2
+
3
3
  import { FragnoApiError } from "../api/error";
4
4
  import { RequestOutputContext } from "../api/request-output-context";
5
+ import { FragnoClientApiError, FragnoClientUnknownApiError } from "./client-error";
5
6
 
6
7
  describe("Error Conversion", () => {
7
8
  test("should convert API error to client error", async () => {
@@ -112,7 +112,7 @@ export class FragnoClientApiError<
112
112
  * The type is `TErrorCode` (the set of known error codes for this route), but may also be a string
113
113
  * for forward compatibility with future error codes.
114
114
  */
115
- get code(): TErrorCode | (string & {}) {
115
+ override get code(): TErrorCode | (string & {}) {
116
116
  return super.code as TErrorCode | (string & {});
117
117
  }
118
118
 
@@ -1,18 +1,22 @@
1
1
  import { test, expectTypeOf, describe } from "vitest";
2
+
2
3
  import { z } from "zod";
4
+
5
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
6
+
3
7
  import { type FragnoRouteConfig, type HTTPMethod } from "../api/api";
4
- import { defineRoute } from "../api/route";
8
+ import { type AnyFragnoRouteConfig, defineRoute } from "../api/route";
5
9
  import type {
6
10
  ExtractGetRoutes,
7
11
  ExtractGetRoutePaths,
8
12
  ExtractOutputSchemaForPath,
9
13
  ExtractRouteByPath,
14
+ ExtractRoutePath,
10
15
  IsValidGetRoutePath,
11
16
  ValidateGetRoutePath,
12
17
  HasGetRoutes,
13
18
  FragnoClientMutatorData,
14
19
  } from "./client";
15
- import type { StandardSchemaV1 } from "@standard-schema/spec";
16
20
 
17
21
  // Test route configurations for type testing
18
22
  const _testRoutes = [
@@ -150,12 +154,12 @@ test("ExtractOutputSchemaForPath type tests", () => {
150
154
 
151
155
  // Note: Routes without output schema have complex type inference, skipping direct test
152
156
 
153
- // Should be never for non-existent path
157
+ // Non-existent paths currently widen to unknown, while routes without an output schema stay optional.
154
158
  type NonExistentSchema = ExtractOutputSchemaForPath<typeof _testRoutes, "/nonexistent">;
155
- expectTypeOf<NonExistentSchema>().toEqualTypeOf<never>();
159
+ expectTypeOf<NonExistentSchema>().toEqualTypeOf<unknown>();
156
160
 
157
161
  type PathWithNoSchema = ExtractOutputSchemaForPath<typeof _testRoutes, "/home">;
158
- expectTypeOf<PathWithNoSchema>().toEqualTypeOf<StandardSchemaV1<unknown, unknown> | undefined>();
162
+ expectTypeOf<PathWithNoSchema>().toEqualTypeOf<StandardSchemaV1<unknown, unknown>>();
159
163
  });
160
164
 
161
165
  test("IsValidGetRoutePath type tests", () => {
@@ -343,6 +347,16 @@ test("GET route with outputSchema", () => {
343
347
  expectTypeOf<ConstPaths>().toEqualTypeOf<"/test">();
344
348
  });
345
349
 
350
+ test("route extraction falls back gracefully for widened route types", () => {
351
+ type BroadRoutes = readonly AnyFragnoRouteConfig[];
352
+
353
+ expectTypeOf<ExtractRoutePath<BroadRoutes, "POST">>().toEqualTypeOf<string>();
354
+ expectTypeOf<ExtractGetRoutePaths<BroadRoutes>>().toEqualTypeOf<string>();
355
+ expectTypeOf<
356
+ ExtractRouteByPath<BroadRoutes, "/users/:id", "POST">
357
+ >().toExtend<AnyFragnoRouteConfig>();
358
+ });
359
+
346
360
  describe("ExtractRouteByPath", () => {
347
361
  const _fragmentConfig = {
348
362
  name: "test-fragment",
@@ -7,12 +7,14 @@
7
7
  */
8
8
 
9
9
  import { describe, expect, test } from "vitest";
10
- import { type FragnoPublicClientConfig } from "./client";
11
- import { createClientBuilder } from "./client";
12
- import { defineRoute } from "../api/route";
13
- import { defineFragment } from "../api/fragment-definition-builder";
10
+
14
11
  import { z } from "zod";
12
+
13
+ import { defineFragment } from "../api/fragment-definition-builder";
14
+ import { defineRoute } from "../api/route";
15
15
  import { createAsyncIteratorFromCallback, waitForAsyncIterator } from "../util/async";
16
+ import { type FragnoPublicClientConfig } from "./client";
17
+ import { createClientBuilder } from "./client";
16
18
 
17
19
  describe("server side rendering", () => {
18
20
  const testFragmentDefinition = defineFragment("test-fragment").build();
@@ -1,16 +1,19 @@
1
1
  import { test, expect, describe, vi, beforeEach, afterEach, assert } from "vitest";
2
- import { type FragnoPublicClientConfig } from "./client";
3
- import { createClientBuilder } from "./client";
2
+
3
+ import { atom, computed } from "nanostores";
4
+ import { writable, readable, get, derived } from "svelte/store";
5
+ import { z } from "zod";
6
+
4
7
  import { render } from "@testing-library/svelte";
5
- import { defineRoute } from "../api/route";
8
+
6
9
  import { defineFragment } from "../api/fragment-definition-builder";
7
- import { z } from "zod";
8
- import { readableToAtom, useFragno } from "./client.svelte";
9
- import { writable, readable, get, derived } from "svelte/store";
10
+ import { RequestOutputContext } from "../api/request-output-context";
11
+ import { defineRoute } from "../api/route";
12
+ import { type FragnoPublicClientConfig } from "./client";
13
+ import { createClientBuilder } from "./client";
10
14
  import { FragnoClientUnknownApiError } from "./client-error";
15
+ import { readableToAtom, useFragno } from "./client.svelte";
11
16
  import TestComponent from "./component.test.svelte";
12
- import { atom, computed } from "nanostores";
13
- import { RequestOutputContext } from "../api/request-output-context";
14
17
 
15
18
  function renderHook(
16
19
  clientObj: Record<string, unknown>,
@@ -766,7 +769,13 @@ describe("useFragno", () => {
766
769
  expect((get(hook.data) as TestData | undefined)?.id).toBe(456);
767
770
  });
768
771
 
769
- expect(fetch).toHaveBeenCalledTimes(2);
772
+ const requestedIds = vi
773
+ .mocked(global.fetch)
774
+ .mock.calls.map(([input]) => String(input).match(/\/users\/([^/]+)/)?.[1])
775
+ .filter((id): id is string => id !== undefined);
776
+
777
+ expect(requestedIds).toContain("123");
778
+ expect(requestedIds).toContain("456");
770
779
  });
771
780
  });
772
781
 
@@ -1,6 +1,12 @@
1
- import type { StandardSchemaV1 } from "@standard-schema/spec";
2
1
  import { atom, type ReadableAtom } from "nanostores";
2
+ import { onDestroy } from "svelte";
3
+ import { writable, type Readable, get } from "svelte/store";
4
+
5
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
6
+
3
7
  import type { NonGetHTTPMethod } from "../api/api";
8
+ import type { MaybeExtractPathParamsOrWiden, QueryParamsHint } from "../api/internal/path";
9
+ import type { InferOr } from "../util/types-util";
4
10
  import {
5
11
  isGetHook,
6
12
  isMutatorHook,
@@ -8,13 +14,10 @@ import {
8
14
  type FragnoClientHookData,
9
15
  type FragnoClientMutatorData,
10
16
  type FragnoStoreData,
17
+ type FragnoStoreFactoryData,
18
+ type FragnoStoreObjectData,
11
19
  } from "./client";
12
20
  import type { FragnoClientError } from "./client-error";
13
- import type { InferOr } from "../util/types-util";
14
- import type { MaybeExtractPathParamsOrWiden, QueryParamsHint } from "../api/internal/path";
15
-
16
- import { writable, type Readable, get } from "svelte/store";
17
- import { onDestroy } from "svelte";
18
21
 
19
22
  export type FragnoSvelteHook<
20
23
  _TMethod extends "GET",
@@ -223,10 +226,30 @@ function createSvelteMutator<
223
226
  };
224
227
  }
225
228
 
226
- export function createSvelteStore<T extends object>(hook: FragnoStoreData<T>): T {
227
- // Since nanostores already implement Svelte's store contract,
228
- // we can return the store object directly for use with $ syntax
229
- return hook.obj;
229
+ export type FragnoSvelteStore<T extends object, TArgs extends unknown[] = []> = TArgs extends []
230
+ ? T
231
+ : (...args: TArgs) => T;
232
+
233
+ export function createSvelteStore<T extends object, TArgs extends unknown[]>(
234
+ hook: FragnoStoreData<T, TArgs>,
235
+ ): FragnoSvelteStore<T, TArgs> {
236
+ if ("obj" in hook) {
237
+ // Since nanostores already implement Svelte's store contract,
238
+ // we can return the store object directly for use with $ syntax
239
+ return hook.obj as FragnoSvelteStore<T, TArgs>;
240
+ }
241
+
242
+ return ((...args: TArgs) => {
243
+ const value = hook.factory(...args);
244
+ const disposer = value[Symbol.dispose as keyof typeof value];
245
+ if (typeof disposer === "function") {
246
+ onDestroy(() => {
247
+ disposer.call(value);
248
+ });
249
+ }
250
+
251
+ return value;
252
+ }) as FragnoSvelteStore<T, TArgs>;
230
253
  }
231
254
 
232
255
  export function useFragno<T extends Record<string, unknown>>(
@@ -249,9 +272,11 @@ export function useFragno<T extends Record<string, unknown>>(
249
272
  infer TQueryParameters
250
273
  >
251
274
  ? FragnoSvelteMutator<M, TPath, TInputSchema, TOutputSchema, TErrorCode, TQueryParameters>
252
- : T[K] extends FragnoStoreData<infer TStoreObj>
253
- ? TStoreObj
254
- : T[K];
275
+ : T[K] extends FragnoStoreObjectData<infer TStoreObj>
276
+ ? FragnoSvelteStore<TStoreObj, []>
277
+ : T[K] extends FragnoStoreFactoryData<infer TStoreObj, infer TStoreArgs>
278
+ ? FragnoSvelteStore<TStoreObj, TStoreArgs>
279
+ : T[K];
255
280
  } {
256
281
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
257
282
  const result = {} as any;