@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.
- package/.turbo/turbo-build.log +61 -0
- package/.turbo/turbo-types$colon$check.log +2 -0
- package/dist/api/api.d.ts +2 -0
- package/dist/api/api.js +3 -0
- package/dist/api-CBDGZiLC.d.ts +278 -0
- package/dist/api-CBDGZiLC.d.ts.map +1 -0
- package/dist/api-DgHfYjq2.js +54 -0
- package/dist/api-DgHfYjq2.js.map +1 -0
- package/dist/client/client.d.ts +3 -0
- package/dist/client/client.js +6 -0
- package/dist/client/client.svelte.d.ts +33 -0
- package/dist/client/client.svelte.d.ts.map +1 -0
- package/dist/client/client.svelte.js +123 -0
- package/dist/client/client.svelte.js.map +1 -0
- package/dist/client/react.d.ts +58 -0
- package/dist/client/react.d.ts.map +1 -0
- package/dist/client/react.js +80 -0
- package/dist/client/react.js.map +1 -0
- package/dist/client/vanilla.d.ts +61 -0
- package/dist/client/vanilla.d.ts.map +1 -0
- package/dist/client/vanilla.js +136 -0
- package/dist/client/vanilla.js.map +1 -0
- package/dist/client/vue.d.ts +39 -0
- package/dist/client/vue.d.ts.map +1 -0
- package/dist/client/vue.js +108 -0
- package/dist/client/vue.js.map +1 -0
- package/dist/client-DWjxKDnE.js +703 -0
- package/dist/client-DWjxKDnE.js.map +1 -0
- package/dist/client-XFdAy-IQ.d.ts +287 -0
- package/dist/client-XFdAy-IQ.d.ts.map +1 -0
- package/dist/integrations/astro.d.ts +18 -0
- package/dist/integrations/astro.d.ts.map +1 -0
- package/dist/integrations/astro.js +16 -0
- package/dist/integrations/astro.js.map +1 -0
- package/dist/integrations/next-js.d.ts +15 -0
- package/dist/integrations/next-js.d.ts.map +1 -0
- package/dist/integrations/next-js.js +17 -0
- package/dist/integrations/next-js.js.map +1 -0
- package/dist/integrations/react-ssr.d.ts +19 -0
- package/dist/integrations/react-ssr.d.ts.map +1 -0
- package/dist/integrations/react-ssr.js +38 -0
- package/dist/integrations/react-ssr.js.map +1 -0
- package/dist/integrations/svelte-kit.d.ts +21 -0
- package/dist/integrations/svelte-kit.d.ts.map +1 -0
- package/dist/integrations/svelte-kit.js +18 -0
- package/dist/integrations/svelte-kit.js.map +1 -0
- package/dist/mod.d.ts +3 -0
- package/dist/mod.js +177 -0
- package/dist/mod.js.map +1 -0
- package/dist/route-Bp6eByhz.js +331 -0
- package/dist/route-Bp6eByhz.js.map +1 -0
- package/dist/ssr-tJHqcNSw.js +48 -0
- package/dist/ssr-tJHqcNSw.js.map +1 -0
- package/package.json +127 -0
- package/src/api/api.test.ts +140 -0
- package/src/api/api.ts +106 -0
- package/src/api/error.ts +47 -0
- package/src/api/fragment.test.ts +509 -0
- package/src/api/fragment.ts +277 -0
- package/src/api/internal/path-runtime.test.ts +121 -0
- package/src/api/internal/path-type.test.ts +602 -0
- package/src/api/internal/path.ts +322 -0
- package/src/api/internal/response-stream.ts +118 -0
- package/src/api/internal/route.test.ts +56 -0
- package/src/api/internal/route.ts +9 -0
- package/src/api/request-input-context.test.ts +437 -0
- package/src/api/request-input-context.ts +201 -0
- package/src/api/request-middleware.test.ts +544 -0
- package/src/api/request-middleware.ts +126 -0
- package/src/api/request-output-context.test.ts +626 -0
- package/src/api/request-output-context.ts +175 -0
- package/src/api/route.test.ts +176 -0
- package/src/api/route.ts +152 -0
- package/src/client/client-builder.test.ts +264 -0
- package/src/client/client-error.test.ts +15 -0
- package/src/client/client-error.ts +141 -0
- package/src/client/client-types.test.ts +493 -0
- package/src/client/client.ssr.test.ts +173 -0
- package/src/client/client.svelte.test.ts +837 -0
- package/src/client/client.svelte.ts +278 -0
- package/src/client/client.test.ts +1690 -0
- package/src/client/client.ts +1035 -0
- package/src/client/component.test.svelte +21 -0
- package/src/client/internal/ndjson-streaming.test.ts +457 -0
- package/src/client/internal/ndjson-streaming.ts +248 -0
- package/src/client/react.test.ts +947 -0
- package/src/client/react.ts +241 -0
- package/src/client/vanilla.test.ts +867 -0
- package/src/client/vanilla.ts +265 -0
- package/src/client/vue.test.ts +754 -0
- package/src/client/vue.ts +242 -0
- package/src/http/http-status.ts +60 -0
- package/src/integrations/astro.ts +17 -0
- package/src/integrations/next-js.ts +31 -0
- package/src/integrations/react-ssr.ts +40 -0
- package/src/integrations/svelte-kit.ts +41 -0
- package/src/mod.ts +20 -0
- package/src/util/async.test.ts +85 -0
- package/src/util/async.ts +96 -0
- package/src/util/content-type.test.ts +136 -0
- package/src/util/content-type.ts +84 -0
- package/src/util/nanostores.test.ts +28 -0
- package/src/util/nanostores.ts +65 -0
- package/src/util/ssr.ts +75 -0
- package/src/util/types-util.ts +16 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +21 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,1035 @@
|
|
|
1
|
+
import { nanoquery, type FetcherStore, type MutatorStore } from "@nanostores/query";
|
|
2
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
3
|
+
import { task, type ReadableAtom, type Store } from "nanostores";
|
|
4
|
+
import type { FragnoRouteConfig, HTTPMethod, NonGetHTTPMethod } from "../api/api";
|
|
5
|
+
import {
|
|
6
|
+
buildPath,
|
|
7
|
+
extractPathParams,
|
|
8
|
+
type ExtractPathParams,
|
|
9
|
+
type ExtractPathParamsOrWiden,
|
|
10
|
+
type MaybeExtractPathParamsOrWiden,
|
|
11
|
+
} from "../api/internal/path";
|
|
12
|
+
import { getMountRoute } from "../api/internal/route";
|
|
13
|
+
import { RequestInputContext } from "../api/request-input-context";
|
|
14
|
+
import { RequestOutputContext } from "../api/request-output-context";
|
|
15
|
+
import type { FragnoFragmentSharedConfig, FragnoPublicClientConfig } from "../api/fragment";
|
|
16
|
+
import { FragnoClientApiError, FragnoClientError, FragnoClientFetchError } from "./client-error";
|
|
17
|
+
import type { InferOr } from "../util/types-util";
|
|
18
|
+
import { parseContentType } from "../util/content-type";
|
|
19
|
+
import {
|
|
20
|
+
handleNdjsonStreamingFirstItem,
|
|
21
|
+
type NdjsonStreamingStore,
|
|
22
|
+
} from "./internal/ndjson-streaming";
|
|
23
|
+
import { addStore, getInitialData, SSR_ENABLED } from "../util/ssr";
|
|
24
|
+
import { unwrapObject } from "../util/nanostores";
|
|
25
|
+
import type { FragmentBuilder } from "../api/fragment";
|
|
26
|
+
import {
|
|
27
|
+
type AnyRouteOrFactory,
|
|
28
|
+
type FlattenRouteFactories,
|
|
29
|
+
resolveRouteFactories,
|
|
30
|
+
} from "../api/route";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Symbols used to identify hook types
|
|
34
|
+
*/
|
|
35
|
+
const GET_HOOK_SYMBOL = Symbol("fragno-get-hook");
|
|
36
|
+
const MUTATOR_HOOK_SYMBOL = Symbol("fragno-mutator-hook");
|
|
37
|
+
const STORE_SYMBOL = Symbol("fragno-store");
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract only GET routes from a library config's routes array
|
|
41
|
+
*/
|
|
42
|
+
export type ExtractGetRoutes<
|
|
43
|
+
T extends readonly FragnoRouteConfig<
|
|
44
|
+
HTTPMethod,
|
|
45
|
+
string,
|
|
46
|
+
StandardSchemaV1 | undefined,
|
|
47
|
+
StandardSchemaV1 | undefined,
|
|
48
|
+
string,
|
|
49
|
+
string
|
|
50
|
+
>[],
|
|
51
|
+
> = {
|
|
52
|
+
[K in keyof T]: T[K] extends FragnoRouteConfig<
|
|
53
|
+
infer Method,
|
|
54
|
+
infer Path,
|
|
55
|
+
infer Input,
|
|
56
|
+
infer Output,
|
|
57
|
+
infer ErrorCode,
|
|
58
|
+
infer QueryParams
|
|
59
|
+
>
|
|
60
|
+
? Method extends "GET"
|
|
61
|
+
? FragnoRouteConfig<Method, Path, Input, Output, ErrorCode, QueryParams>
|
|
62
|
+
: never
|
|
63
|
+
: never;
|
|
64
|
+
}[number][];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extract the path from a route configuration for a given method
|
|
68
|
+
*/
|
|
69
|
+
export type ExtractRoutePath<
|
|
70
|
+
T extends readonly FragnoRouteConfig<
|
|
71
|
+
HTTPMethod,
|
|
72
|
+
string,
|
|
73
|
+
StandardSchemaV1 | undefined,
|
|
74
|
+
StandardSchemaV1 | undefined,
|
|
75
|
+
string,
|
|
76
|
+
string
|
|
77
|
+
>[],
|
|
78
|
+
TExpectedMethod extends HTTPMethod = HTTPMethod,
|
|
79
|
+
> = {
|
|
80
|
+
[K in keyof T]: T[K] extends FragnoRouteConfig<
|
|
81
|
+
infer Method,
|
|
82
|
+
infer Path,
|
|
83
|
+
StandardSchemaV1 | undefined,
|
|
84
|
+
StandardSchemaV1 | undefined,
|
|
85
|
+
string,
|
|
86
|
+
string
|
|
87
|
+
>
|
|
88
|
+
? Method extends TExpectedMethod
|
|
89
|
+
? Path
|
|
90
|
+
: never
|
|
91
|
+
: never;
|
|
92
|
+
}[number];
|
|
93
|
+
|
|
94
|
+
export type ExtractGetRoutePaths<
|
|
95
|
+
T extends readonly FragnoRouteConfig<
|
|
96
|
+
HTTPMethod,
|
|
97
|
+
string,
|
|
98
|
+
StandardSchemaV1 | undefined,
|
|
99
|
+
StandardSchemaV1 | undefined,
|
|
100
|
+
string,
|
|
101
|
+
string
|
|
102
|
+
>[],
|
|
103
|
+
> = ExtractRoutePath<T, "GET">;
|
|
104
|
+
|
|
105
|
+
export type ExtractNonGetRoutePaths<
|
|
106
|
+
T extends readonly FragnoRouteConfig<
|
|
107
|
+
HTTPMethod,
|
|
108
|
+
string,
|
|
109
|
+
StandardSchemaV1 | undefined,
|
|
110
|
+
StandardSchemaV1 | undefined,
|
|
111
|
+
string,
|
|
112
|
+
string
|
|
113
|
+
>[],
|
|
114
|
+
> = ExtractRoutePath<T, NonGetHTTPMethod>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Extract the route configuration type(s) for a given path from a routes array.
|
|
118
|
+
* Optionally narrow by HTTP method via the third type parameter.
|
|
119
|
+
*
|
|
120
|
+
* Defaults to extracting all methods for the matching path, producing a union
|
|
121
|
+
* if multiple methods exist for the same path.
|
|
122
|
+
*/
|
|
123
|
+
export type ExtractRouteByPath<
|
|
124
|
+
TRoutes extends readonly FragnoRouteConfig<
|
|
125
|
+
HTTPMethod,
|
|
126
|
+
string,
|
|
127
|
+
StandardSchemaV1 | undefined,
|
|
128
|
+
StandardSchemaV1 | undefined,
|
|
129
|
+
string,
|
|
130
|
+
string
|
|
131
|
+
>[],
|
|
132
|
+
TPath extends string,
|
|
133
|
+
TMethod extends HTTPMethod = HTTPMethod,
|
|
134
|
+
> = {
|
|
135
|
+
[K in keyof TRoutes]: TRoutes[K] extends FragnoRouteConfig<
|
|
136
|
+
infer M,
|
|
137
|
+
TPath,
|
|
138
|
+
infer Input,
|
|
139
|
+
infer Output,
|
|
140
|
+
infer ErrorCode,
|
|
141
|
+
infer QueryParams
|
|
142
|
+
>
|
|
143
|
+
? M extends TMethod
|
|
144
|
+
? FragnoRouteConfig<M, TPath, Input, Output, ErrorCode, QueryParams>
|
|
145
|
+
: never
|
|
146
|
+
: never;
|
|
147
|
+
}[number];
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Extract the output schema type for a specific route path from a routes array
|
|
151
|
+
*/
|
|
152
|
+
export type ExtractOutputSchemaForPath<
|
|
153
|
+
TRoutes extends readonly FragnoRouteConfig<
|
|
154
|
+
HTTPMethod,
|
|
155
|
+
string,
|
|
156
|
+
StandardSchemaV1 | undefined,
|
|
157
|
+
StandardSchemaV1 | undefined
|
|
158
|
+
>[],
|
|
159
|
+
TPath extends string,
|
|
160
|
+
> = {
|
|
161
|
+
[K in keyof TRoutes]: TRoutes[K] extends FragnoRouteConfig<
|
|
162
|
+
infer Method,
|
|
163
|
+
TPath,
|
|
164
|
+
StandardSchemaV1 | undefined,
|
|
165
|
+
infer Output
|
|
166
|
+
>
|
|
167
|
+
? Method extends "GET"
|
|
168
|
+
? Output
|
|
169
|
+
: never
|
|
170
|
+
: never;
|
|
171
|
+
}[number];
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if a path exists as a GET route in the routes array
|
|
175
|
+
*/
|
|
176
|
+
export type IsValidGetRoutePath<
|
|
177
|
+
TRoutes extends readonly FragnoRouteConfig<
|
|
178
|
+
HTTPMethod,
|
|
179
|
+
string,
|
|
180
|
+
StandardSchemaV1 | undefined,
|
|
181
|
+
StandardSchemaV1 | undefined,
|
|
182
|
+
string,
|
|
183
|
+
string
|
|
184
|
+
>[],
|
|
185
|
+
TPath extends string,
|
|
186
|
+
> = TPath extends ExtractGetRoutePaths<TRoutes> ? true : false;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Utility type to ensure only valid GET route paths can be used
|
|
190
|
+
*/
|
|
191
|
+
export type ValidateGetRoutePath<
|
|
192
|
+
TRoutes extends readonly FragnoRouteConfig<
|
|
193
|
+
HTTPMethod,
|
|
194
|
+
string,
|
|
195
|
+
StandardSchemaV1 | undefined,
|
|
196
|
+
StandardSchemaV1 | undefined,
|
|
197
|
+
string,
|
|
198
|
+
string
|
|
199
|
+
>[],
|
|
200
|
+
TPath extends string,
|
|
201
|
+
> =
|
|
202
|
+
TPath extends ExtractGetRoutePaths<TRoutes>
|
|
203
|
+
? TPath
|
|
204
|
+
: `Error: Path '${TPath}' is not a valid GET route. Available GET routes: ${ExtractGetRoutePaths<TRoutes>}`;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Helper type to check if a routes array has any GET routes
|
|
208
|
+
*/
|
|
209
|
+
export type HasGetRoutes<
|
|
210
|
+
T extends readonly FragnoRouteConfig<
|
|
211
|
+
HTTPMethod,
|
|
212
|
+
string,
|
|
213
|
+
StandardSchemaV1 | undefined,
|
|
214
|
+
StandardSchemaV1 | undefined,
|
|
215
|
+
string,
|
|
216
|
+
string
|
|
217
|
+
>[],
|
|
218
|
+
> = ExtractGetRoutePaths<T> extends never ? false : true;
|
|
219
|
+
|
|
220
|
+
export type ObjectContainingStoreField<T extends object> = T extends Store
|
|
221
|
+
? T
|
|
222
|
+
: {
|
|
223
|
+
[K in keyof T]: T[K] extends Store ? { [P in K]: T[P] } & Partial<Omit<T, K>> : never;
|
|
224
|
+
}[keyof T] extends never
|
|
225
|
+
? never
|
|
226
|
+
: T;
|
|
227
|
+
|
|
228
|
+
export type FragnoStoreData<T extends object> = {
|
|
229
|
+
obj: T;
|
|
230
|
+
[STORE_SYMBOL]: true;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export type FragnoClientHookData<
|
|
234
|
+
TMethod extends HTTPMethod,
|
|
235
|
+
TPath extends string,
|
|
236
|
+
TOutputSchema extends StandardSchemaV1,
|
|
237
|
+
TErrorCode extends string,
|
|
238
|
+
TQueryParameters extends string,
|
|
239
|
+
> = {
|
|
240
|
+
route: FragnoRouteConfig<
|
|
241
|
+
TMethod,
|
|
242
|
+
TPath,
|
|
243
|
+
StandardSchemaV1 | undefined,
|
|
244
|
+
TOutputSchema,
|
|
245
|
+
TErrorCode,
|
|
246
|
+
TQueryParameters
|
|
247
|
+
>;
|
|
248
|
+
query(args?: {
|
|
249
|
+
path?: MaybeExtractPathParamsOrWiden<TPath, string>;
|
|
250
|
+
query?: Record<TQueryParameters, string>;
|
|
251
|
+
}): Promise<StandardSchemaV1.InferOutput<TOutputSchema>>;
|
|
252
|
+
store(args?: {
|
|
253
|
+
path?: MaybeExtractPathParamsOrWiden<TPath, string | ReadableAtom<string>>;
|
|
254
|
+
query?: Record<TQueryParameters, string | ReadableAtom<string>>;
|
|
255
|
+
}): FetcherStore<StandardSchemaV1.InferOutput<TOutputSchema>, FragnoClientError<TErrorCode>>;
|
|
256
|
+
[GET_HOOK_SYMBOL]: true;
|
|
257
|
+
} & {
|
|
258
|
+
// Phantom field that preserves the specific TOutputSchema type parameter
|
|
259
|
+
// in the structural type. This makes the type covariant, allowing more
|
|
260
|
+
// specific schema types (like z.ZodString) to be assigned to variables
|
|
261
|
+
// typed with more general schema types (like StandardSchemaV1<any, any>)
|
|
262
|
+
readonly _outputSchema?: TOutputSchema;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export type FragnoClientMutatorData<
|
|
266
|
+
TMethod extends NonGetHTTPMethod,
|
|
267
|
+
TPath extends string,
|
|
268
|
+
TInputSchema extends StandardSchemaV1 | undefined,
|
|
269
|
+
TOutputSchema extends StandardSchemaV1 | undefined,
|
|
270
|
+
TErrorCode extends string,
|
|
271
|
+
TQueryParameters extends string,
|
|
272
|
+
> = {
|
|
273
|
+
route: FragnoRouteConfig<
|
|
274
|
+
TMethod,
|
|
275
|
+
TPath,
|
|
276
|
+
TInputSchema,
|
|
277
|
+
TOutputSchema,
|
|
278
|
+
TErrorCode,
|
|
279
|
+
TQueryParameters
|
|
280
|
+
>;
|
|
281
|
+
|
|
282
|
+
mutateQuery(args?: {
|
|
283
|
+
body?: InferOr<TInputSchema, undefined>;
|
|
284
|
+
path?: MaybeExtractPathParamsOrWiden<TPath, string>;
|
|
285
|
+
query?: Record<TQueryParameters, string>;
|
|
286
|
+
}): Promise<InferOr<TOutputSchema, undefined>>;
|
|
287
|
+
|
|
288
|
+
mutatorStore: MutatorStore<
|
|
289
|
+
{
|
|
290
|
+
body?: InferOr<TInputSchema, undefined>;
|
|
291
|
+
path?: MaybeExtractPathParamsOrWiden<TPath, string | ReadableAtom<string>>;
|
|
292
|
+
query?: Record<TQueryParameters, string | ReadableAtom<string>>;
|
|
293
|
+
},
|
|
294
|
+
InferOr<TOutputSchema, undefined>,
|
|
295
|
+
FragnoClientError<TErrorCode>
|
|
296
|
+
>;
|
|
297
|
+
[MUTATOR_HOOK_SYMBOL]: true;
|
|
298
|
+
} & {
|
|
299
|
+
readonly _inputSchema?: TInputSchema;
|
|
300
|
+
readonly _outputSchema?: TOutputSchema;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
export function buildUrl<TPath extends string>(
|
|
304
|
+
config: {
|
|
305
|
+
baseUrl?: string;
|
|
306
|
+
mountRoute: string;
|
|
307
|
+
path: TPath;
|
|
308
|
+
},
|
|
309
|
+
params: {
|
|
310
|
+
pathParams?: Record<string, string | ReadableAtom<string>>;
|
|
311
|
+
queryParams?: Record<string, string | ReadableAtom<string>>;
|
|
312
|
+
},
|
|
313
|
+
): string {
|
|
314
|
+
const { baseUrl = "", mountRoute, path } = config;
|
|
315
|
+
const { pathParams, queryParams } = params ?? {};
|
|
316
|
+
|
|
317
|
+
const normalizedPathParams = unwrapObject(pathParams) as ExtractPathParams<TPath, string>;
|
|
318
|
+
const normalizedQueryParams = unwrapObject(queryParams) ?? {};
|
|
319
|
+
|
|
320
|
+
const searchParams = new URLSearchParams(normalizedQueryParams);
|
|
321
|
+
const builtPath = buildPath(path, normalizedPathParams ?? {});
|
|
322
|
+
const search = searchParams.toString() ? `?${searchParams.toString()}` : "";
|
|
323
|
+
return `${baseUrl}${mountRoute}${builtPath}${search}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* This method returns an array, which can be passed directly to nanostores.
|
|
328
|
+
*
|
|
329
|
+
* The returned array is always: path, pathParams (In order they appear in the path), queryParams (In alphabetical order)
|
|
330
|
+
* Missing pathParams are replaced with "<missing>".
|
|
331
|
+
* @param path
|
|
332
|
+
* @param params
|
|
333
|
+
* @returns
|
|
334
|
+
*/
|
|
335
|
+
export function getCacheKey<TMethod extends HTTPMethod, TPath extends string>(
|
|
336
|
+
method: TMethod,
|
|
337
|
+
path: TPath,
|
|
338
|
+
params?: {
|
|
339
|
+
pathParams?: Record<string, string | ReadableAtom<string>>;
|
|
340
|
+
queryParams?: Record<string, string | ReadableAtom<string>>;
|
|
341
|
+
},
|
|
342
|
+
): (string | ReadableAtom<string>)[] {
|
|
343
|
+
if (!params) {
|
|
344
|
+
return [method, path];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const { pathParams, queryParams } = params;
|
|
348
|
+
|
|
349
|
+
const pathParamNames = extractPathParams(path);
|
|
350
|
+
const pathParamValues = pathParamNames.map((name) => pathParams?.[name] ?? "<missing>");
|
|
351
|
+
|
|
352
|
+
const queryParamValues = queryParams
|
|
353
|
+
? Object.keys(queryParams)
|
|
354
|
+
.sort()
|
|
355
|
+
.map((key) => queryParams[key])
|
|
356
|
+
: [];
|
|
357
|
+
|
|
358
|
+
return [method, path, ...pathParamValues, ...queryParamValues];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function isStreamingResponse(response: Response): false | "ndjson" | "octet-stream" {
|
|
362
|
+
const contentType = parseContentType(response.headers.get("content-type"));
|
|
363
|
+
|
|
364
|
+
if (!contentType) {
|
|
365
|
+
// Always assume 'normal' JSON by default.
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const isChunked = response.headers.get("transfer-encoding") === "chunked";
|
|
370
|
+
|
|
371
|
+
if (!isChunked) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (contentType.subtype === "octet-stream") {
|
|
376
|
+
// TODO(Wilco): This is not actually supported properly
|
|
377
|
+
return "octet-stream";
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (contentType.subtype === "x-ndjson") {
|
|
381
|
+
return "ndjson";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Type guard to check if a hook is a GET hook
|
|
388
|
+
export function isGetHook<
|
|
389
|
+
TPath extends string,
|
|
390
|
+
TOutputSchema extends StandardSchemaV1,
|
|
391
|
+
TErrorCode extends string,
|
|
392
|
+
TQueryParameters extends string,
|
|
393
|
+
>(
|
|
394
|
+
hook: unknown,
|
|
395
|
+
): hook is FragnoClientHookData<"GET", TPath, TOutputSchema, TErrorCode, TQueryParameters> {
|
|
396
|
+
return (
|
|
397
|
+
typeof hook === "object" &&
|
|
398
|
+
hook !== null &&
|
|
399
|
+
GET_HOOK_SYMBOL in hook &&
|
|
400
|
+
hook[GET_HOOK_SYMBOL] === true
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Type guard to check if a hook is a mutator
|
|
405
|
+
export function isMutatorHook<
|
|
406
|
+
TMethod extends NonGetHTTPMethod,
|
|
407
|
+
TPath extends string,
|
|
408
|
+
TInputSchema extends StandardSchemaV1 | undefined,
|
|
409
|
+
TOutputSchema extends StandardSchemaV1 | undefined,
|
|
410
|
+
TErrorCode extends string,
|
|
411
|
+
TQueryParameters extends string,
|
|
412
|
+
>(
|
|
413
|
+
hook: unknown,
|
|
414
|
+
): hook is FragnoClientMutatorData<
|
|
415
|
+
TMethod,
|
|
416
|
+
TPath,
|
|
417
|
+
TInputSchema,
|
|
418
|
+
TOutputSchema,
|
|
419
|
+
TErrorCode,
|
|
420
|
+
TQueryParameters
|
|
421
|
+
> {
|
|
422
|
+
return (
|
|
423
|
+
typeof hook === "object" &&
|
|
424
|
+
hook !== null &&
|
|
425
|
+
MUTATOR_HOOK_SYMBOL in hook &&
|
|
426
|
+
hook[MUTATOR_HOOK_SYMBOL] === true
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function isStore<TStore extends Store>(obj: unknown): obj is FragnoStoreData<TStore> {
|
|
431
|
+
return (
|
|
432
|
+
typeof obj === "object" && obj !== null && STORE_SYMBOL in obj && obj[STORE_SYMBOL] === true
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
type OnErrorRetryFn = (opts: {
|
|
437
|
+
error: unknown;
|
|
438
|
+
key: string;
|
|
439
|
+
retryCount: number;
|
|
440
|
+
}) => number | undefined;
|
|
441
|
+
|
|
442
|
+
export type CreateHookOptions = {
|
|
443
|
+
/**
|
|
444
|
+
* A function that will be called when an error occurs. Implements an exponential backoff strategy
|
|
445
|
+
* when left undefined. When null, retries will be disabled. The number returned (> 0) by the
|
|
446
|
+
* callback will determine in how many ms to retry next.
|
|
447
|
+
*/
|
|
448
|
+
onErrorRetry?: OnErrorRetryFn | null;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
type OnInvalidateFn<TPath extends string> = (
|
|
452
|
+
invalidate: <TInnerPath extends string>(
|
|
453
|
+
method: HTTPMethod,
|
|
454
|
+
path: TInnerPath,
|
|
455
|
+
params: {
|
|
456
|
+
pathParams?: MaybeExtractPathParamsOrWiden<TInnerPath, string>;
|
|
457
|
+
queryParams?: Record<string, string>;
|
|
458
|
+
},
|
|
459
|
+
) => void,
|
|
460
|
+
params: {
|
|
461
|
+
pathParams: MaybeExtractPathParamsOrWiden<TPath, string>;
|
|
462
|
+
queryParams?: Record<string, string>;
|
|
463
|
+
},
|
|
464
|
+
) => void;
|
|
465
|
+
|
|
466
|
+
export type CacheLine = {
|
|
467
|
+
data: unknown;
|
|
468
|
+
error: unknown;
|
|
469
|
+
retryCount: number;
|
|
470
|
+
created: number;
|
|
471
|
+
expires: number;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
export class ClientBuilder<
|
|
475
|
+
TRoutes extends readonly FragnoRouteConfig<
|
|
476
|
+
HTTPMethod,
|
|
477
|
+
string,
|
|
478
|
+
StandardSchemaV1 | undefined,
|
|
479
|
+
StandardSchemaV1 | undefined,
|
|
480
|
+
string,
|
|
481
|
+
string
|
|
482
|
+
>[],
|
|
483
|
+
TFragmentConfig extends FragnoFragmentSharedConfig<TRoutes>,
|
|
484
|
+
> {
|
|
485
|
+
#publicConfig: FragnoPublicClientConfig;
|
|
486
|
+
#fragmentConfig: TFragmentConfig;
|
|
487
|
+
|
|
488
|
+
#cache = new Map<string, CacheLine>();
|
|
489
|
+
|
|
490
|
+
#createFetcherStore;
|
|
491
|
+
#createMutatorStore;
|
|
492
|
+
#invalidateKeys;
|
|
493
|
+
|
|
494
|
+
constructor(publicConfig: FragnoPublicClientConfig, fragmentConfig: TFragmentConfig) {
|
|
495
|
+
this.#publicConfig = publicConfig;
|
|
496
|
+
this.#fragmentConfig = fragmentConfig;
|
|
497
|
+
|
|
498
|
+
const [createFetcherStore, createMutatorStore, { invalidateKeys }] = nanoquery({
|
|
499
|
+
cache: this.#cache,
|
|
500
|
+
});
|
|
501
|
+
this.#createFetcherStore = createFetcherStore;
|
|
502
|
+
this.#createMutatorStore = createMutatorStore;
|
|
503
|
+
this.#invalidateKeys = invalidateKeys;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
get cacheEntries(): Readonly<Record<string, CacheLine>> {
|
|
507
|
+
return Object.fromEntries(this.#cache.entries());
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
createStore<const T extends object>(obj: T): FragnoStoreData<T> {
|
|
511
|
+
return { obj: obj, [STORE_SYMBOL]: true };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
createHook<TPath extends ExtractGetRoutePaths<TFragmentConfig["routes"]>>(
|
|
515
|
+
path: ValidateGetRoutePath<TFragmentConfig["routes"], TPath>,
|
|
516
|
+
options?: CreateHookOptions,
|
|
517
|
+
): FragnoClientHookData<
|
|
518
|
+
"GET",
|
|
519
|
+
TPath,
|
|
520
|
+
NonNullable<ExtractRouteByPath<TFragmentConfig["routes"], TPath>["outputSchema"]>,
|
|
521
|
+
NonNullable<ExtractRouteByPath<TFragmentConfig["routes"], TPath>["errorCodes"]>[number],
|
|
522
|
+
NonNullable<ExtractRouteByPath<TFragmentConfig["routes"], TPath>["queryParameters"]>[number]
|
|
523
|
+
> {
|
|
524
|
+
const route = this.#fragmentConfig.routes.find(
|
|
525
|
+
(
|
|
526
|
+
r,
|
|
527
|
+
): r is FragnoRouteConfig<
|
|
528
|
+
"GET",
|
|
529
|
+
TPath,
|
|
530
|
+
StandardSchemaV1 | undefined,
|
|
531
|
+
StandardSchemaV1,
|
|
532
|
+
string,
|
|
533
|
+
string
|
|
534
|
+
> => r.path === path && r.method === "GET" && r.outputSchema !== undefined,
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
if (!route) {
|
|
538
|
+
throw new Error(`Route '${path}' not found or is not a GET route with an output schema.`);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return this.#createRouteQueryHook(route, options);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
createMutator<TPath extends ExtractNonGetRoutePaths<TFragmentConfig["routes"]>>(
|
|
545
|
+
method: NonGetHTTPMethod,
|
|
546
|
+
path: TPath,
|
|
547
|
+
onInvalidate?: OnInvalidateFn<TPath>,
|
|
548
|
+
): FragnoClientMutatorData<
|
|
549
|
+
NonGetHTTPMethod, // TODO: This can be any Method, but should be related to TPath
|
|
550
|
+
TPath,
|
|
551
|
+
ExtractRouteByPath<TFragmentConfig["routes"], TPath>["inputSchema"],
|
|
552
|
+
ExtractRouteByPath<TFragmentConfig["routes"], TPath>["outputSchema"],
|
|
553
|
+
NonNullable<ExtractRouteByPath<TFragmentConfig["routes"], TPath>["errorCodes"]>[number],
|
|
554
|
+
NonNullable<ExtractRouteByPath<TFragmentConfig["routes"], TPath>["queryParameters"]>[number]
|
|
555
|
+
> {
|
|
556
|
+
type TRoute = ExtractRouteByPath<TFragmentConfig["routes"], TPath>;
|
|
557
|
+
|
|
558
|
+
const route = this.#fragmentConfig.routes.find(
|
|
559
|
+
(
|
|
560
|
+
r,
|
|
561
|
+
): r is FragnoRouteConfig<
|
|
562
|
+
NonGetHTTPMethod,
|
|
563
|
+
TPath,
|
|
564
|
+
TRoute["inputSchema"],
|
|
565
|
+
TRoute["outputSchema"],
|
|
566
|
+
string,
|
|
567
|
+
string
|
|
568
|
+
> => r.method !== "GET" && r.path === path && r.method === method,
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
if (!route) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
`Route '${path}' not found or is a GET route with an input and output schema.`,
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return this.#createRouteQueryMutator(route, onInvalidate);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
#createRouteQueryHook<
|
|
581
|
+
TPath extends string,
|
|
582
|
+
TInputSchema extends StandardSchemaV1 | undefined,
|
|
583
|
+
TOutputSchema extends StandardSchemaV1,
|
|
584
|
+
TErrorCode extends string,
|
|
585
|
+
TQueryParameters extends string,
|
|
586
|
+
>(
|
|
587
|
+
route: FragnoRouteConfig<
|
|
588
|
+
"GET",
|
|
589
|
+
TPath,
|
|
590
|
+
TInputSchema,
|
|
591
|
+
TOutputSchema,
|
|
592
|
+
TErrorCode,
|
|
593
|
+
TQueryParameters
|
|
594
|
+
>,
|
|
595
|
+
options: CreateHookOptions = {},
|
|
596
|
+
): FragnoClientHookData<"GET", TPath, TOutputSchema, TErrorCode, TQueryParameters> {
|
|
597
|
+
if (route.method !== "GET") {
|
|
598
|
+
throw new Error(
|
|
599
|
+
`Only GET routes are supported for hooks. Route '${route.path}' is a ${route.method} route.`,
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (!route.outputSchema) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
`Output schema is required for GET routes. Route '${route.path}' has no output schema.`,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const baseUrl = this.#publicConfig.baseUrl ?? "";
|
|
610
|
+
const mountRoute = getMountRoute(this.#fragmentConfig);
|
|
611
|
+
|
|
612
|
+
async function callServerSideHandler(params: {
|
|
613
|
+
pathParams?: Record<string, string | ReadableAtom<string>>;
|
|
614
|
+
queryParams?: Record<string, string | ReadableAtom<string>>;
|
|
615
|
+
}): Promise<Response> {
|
|
616
|
+
const { pathParams, queryParams } = params ?? {};
|
|
617
|
+
|
|
618
|
+
const normalizedPathParams = unwrapObject(pathParams) as ExtractPathParams<TPath, string>;
|
|
619
|
+
const normalizedQueryParams = unwrapObject(queryParams) ?? {};
|
|
620
|
+
|
|
621
|
+
const searchParams = new URLSearchParams(normalizedQueryParams);
|
|
622
|
+
|
|
623
|
+
const result = await route.handler(
|
|
624
|
+
RequestInputContext.fromSSRContext({
|
|
625
|
+
method: route.method,
|
|
626
|
+
path: route.path,
|
|
627
|
+
pathParams: normalizedPathParams,
|
|
628
|
+
searchParams,
|
|
629
|
+
}),
|
|
630
|
+
new RequestOutputContext(route.outputSchema),
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
return result;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function executeQuery(params?: {
|
|
637
|
+
pathParams?: Record<string, string | ReadableAtom<string>>;
|
|
638
|
+
queryParams?: Record<string, string | ReadableAtom<string>>;
|
|
639
|
+
}): Promise<Response> {
|
|
640
|
+
const { pathParams, queryParams } = params ?? {};
|
|
641
|
+
|
|
642
|
+
if (typeof window === "undefined") {
|
|
643
|
+
return task(async () => callServerSideHandler({ pathParams, queryParams }));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const url = buildUrl({ baseUrl, mountRoute, path: route.path }, { pathParams, queryParams });
|
|
647
|
+
|
|
648
|
+
let response: Response;
|
|
649
|
+
try {
|
|
650
|
+
response = await fetch(url);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
throw FragnoClientFetchError.fromUnknownFetchError(error);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (!response.ok) {
|
|
656
|
+
throw await FragnoClientApiError.fromResponse<TErrorCode>(response);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return response;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
route,
|
|
664
|
+
store: (args) => {
|
|
665
|
+
const { path, query } = args ?? {};
|
|
666
|
+
|
|
667
|
+
const key = getCacheKey(route.method, route.path, {
|
|
668
|
+
pathParams: path,
|
|
669
|
+
queryParams: query,
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
const store = this.#createFetcherStore<
|
|
673
|
+
StandardSchemaV1.InferOutput<TOutputSchema>,
|
|
674
|
+
FragnoClientError<TErrorCode>
|
|
675
|
+
>(key, {
|
|
676
|
+
fetcher: async (): Promise<StandardSchemaV1.InferOutput<TOutputSchema>> => {
|
|
677
|
+
if (SSR_ENABLED) {
|
|
678
|
+
const initialData = getInitialData(
|
|
679
|
+
key.map((d) => (typeof d === "string" ? d : d.get())).join(""),
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
if (initialData) {
|
|
683
|
+
return initialData;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const response = await executeQuery({ pathParams: path, queryParams: query });
|
|
688
|
+
const isStreaming = isStreamingResponse(response);
|
|
689
|
+
|
|
690
|
+
if (!isStreaming) {
|
|
691
|
+
return response.json() as Promise<StandardSchemaV1.InferOutput<TOutputSchema>>;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (typeof window === "undefined") {
|
|
695
|
+
return [];
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (isStreaming === "ndjson") {
|
|
699
|
+
const storeAdapter: NdjsonStreamingStore<TOutputSchema, TErrorCode> = {
|
|
700
|
+
setData: (value) => {
|
|
701
|
+
store.set({
|
|
702
|
+
...store.get(),
|
|
703
|
+
loading: !(Array.isArray(value) && value.length > 0),
|
|
704
|
+
data: value as InferOr<TOutputSchema, undefined>,
|
|
705
|
+
});
|
|
706
|
+
},
|
|
707
|
+
setError: (value) => {
|
|
708
|
+
store.set({
|
|
709
|
+
...store.get(),
|
|
710
|
+
error: value,
|
|
711
|
+
});
|
|
712
|
+
},
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// Start streaming in background and return first item
|
|
716
|
+
const { firstItem } = await handleNdjsonStreamingFirstItem(response, storeAdapter);
|
|
717
|
+
return [firstItem];
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (isStreaming === "octet-stream") {
|
|
721
|
+
// TODO(Wilco): Implement this
|
|
722
|
+
throw new Error("Octet-stream streaming is not supported.");
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
throw new Error("Unreachable");
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
onErrorRetry: options?.onErrorRetry,
|
|
729
|
+
dedupeTime: Infinity,
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
if (typeof window === "undefined") {
|
|
733
|
+
addStore(store);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return store;
|
|
737
|
+
},
|
|
738
|
+
query: async (args) => {
|
|
739
|
+
const { path, query } = args ?? {};
|
|
740
|
+
|
|
741
|
+
const response = await executeQuery({ pathParams: path, queryParams: query });
|
|
742
|
+
|
|
743
|
+
const isStreaming = isStreamingResponse(response);
|
|
744
|
+
|
|
745
|
+
if (!isStreaming) {
|
|
746
|
+
return (await response.json()) as StandardSchemaV1.InferOutput<TOutputSchema>;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (isStreaming === "ndjson") {
|
|
750
|
+
const { streamingPromise } = await handleNdjsonStreamingFirstItem(response);
|
|
751
|
+
// Resolves once the stream is done
|
|
752
|
+
return await streamingPromise;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (isStreaming === "octet-stream") {
|
|
756
|
+
// TODO(Wilco): Implement this
|
|
757
|
+
throw new Error("Octet-stream streaming is not supported.");
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
throw new Error("Unreachable");
|
|
761
|
+
},
|
|
762
|
+
[GET_HOOK_SYMBOL]: true,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
#createRouteQueryMutator<
|
|
767
|
+
TPath extends string,
|
|
768
|
+
TInputSchema extends StandardSchemaV1 | undefined,
|
|
769
|
+
TOutputSchema extends StandardSchemaV1 | undefined,
|
|
770
|
+
TErrorCode extends string,
|
|
771
|
+
TQueryParameters extends string,
|
|
772
|
+
>(
|
|
773
|
+
route: FragnoRouteConfig<
|
|
774
|
+
NonGetHTTPMethod,
|
|
775
|
+
TPath,
|
|
776
|
+
TInputSchema,
|
|
777
|
+
TOutputSchema,
|
|
778
|
+
TErrorCode,
|
|
779
|
+
TQueryParameters
|
|
780
|
+
>,
|
|
781
|
+
onInvalidate: OnInvalidateFn<TPath> = (invalidate, params) =>
|
|
782
|
+
invalidate("GET", route.path, params),
|
|
783
|
+
): FragnoClientMutatorData<
|
|
784
|
+
NonGetHTTPMethod,
|
|
785
|
+
TPath,
|
|
786
|
+
TInputSchema,
|
|
787
|
+
TOutputSchema,
|
|
788
|
+
TErrorCode,
|
|
789
|
+
TQueryParameters
|
|
790
|
+
> {
|
|
791
|
+
const method = route.method;
|
|
792
|
+
|
|
793
|
+
const baseUrl = this.#publicConfig.baseUrl ?? "";
|
|
794
|
+
const mountRoute = getMountRoute(this.#fragmentConfig);
|
|
795
|
+
|
|
796
|
+
async function executeMutateQuery({
|
|
797
|
+
body,
|
|
798
|
+
path,
|
|
799
|
+
query,
|
|
800
|
+
}: {
|
|
801
|
+
body?: InferOr<TInputSchema, undefined>;
|
|
802
|
+
path?: ExtractPathParamsOrWiden<TPath, string>;
|
|
803
|
+
query?: Record<string, string>;
|
|
804
|
+
}): Promise<Response> {
|
|
805
|
+
if (typeof window === "undefined") {
|
|
806
|
+
return task(async () =>
|
|
807
|
+
route.handler(
|
|
808
|
+
RequestInputContext.fromSSRContext({
|
|
809
|
+
inputSchema: route.inputSchema,
|
|
810
|
+
method,
|
|
811
|
+
path: route.path,
|
|
812
|
+
pathParams: (path ?? {}) as ExtractPathParams<TPath, string>,
|
|
813
|
+
searchParams: new URLSearchParams(query),
|
|
814
|
+
body,
|
|
815
|
+
}),
|
|
816
|
+
new RequestOutputContext(route.outputSchema),
|
|
817
|
+
),
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const url = buildUrl(
|
|
822
|
+
{ baseUrl, mountRoute, path: route.path },
|
|
823
|
+
{ pathParams: path, queryParams: query },
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
let response: Response;
|
|
827
|
+
try {
|
|
828
|
+
response = await fetch(url, {
|
|
829
|
+
method,
|
|
830
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
831
|
+
});
|
|
832
|
+
} catch (error) {
|
|
833
|
+
throw FragnoClientFetchError.fromUnknownFetchError(error);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (!response.ok) {
|
|
837
|
+
throw await FragnoClientApiError.fromResponse<TErrorCode>(response);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return response;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const mutatorStore: FragnoClientMutatorData<
|
|
844
|
+
NonGetHTTPMethod,
|
|
845
|
+
TPath,
|
|
846
|
+
TInputSchema,
|
|
847
|
+
TOutputSchema,
|
|
848
|
+
TErrorCode,
|
|
849
|
+
TQueryParameters
|
|
850
|
+
>["mutatorStore"] = this.#createMutatorStore(
|
|
851
|
+
async ({ data }) => {
|
|
852
|
+
if (typeof window === "undefined") {
|
|
853
|
+
// TODO(Wilco): Handle server-side rendering.
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const { body, path, query } = data as {
|
|
857
|
+
body?: InferOr<TInputSchema, undefined>;
|
|
858
|
+
path?: ExtractPathParamsOrWiden<TPath, string>;
|
|
859
|
+
query?: Record<string, string>;
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
if (typeof body === "undefined" && route.inputSchema !== undefined) {
|
|
863
|
+
throw new Error("Body is required.");
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const response = await executeMutateQuery({ body, path, query });
|
|
867
|
+
|
|
868
|
+
onInvalidate(this.#invalidate.bind(this), {
|
|
869
|
+
pathParams: (path ?? {}) as MaybeExtractPathParamsOrWiden<TPath, string>,
|
|
870
|
+
queryParams: query,
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
if (response.status === 201 || response.status === 204) {
|
|
874
|
+
return undefined;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const isStreaming = isStreamingResponse(response);
|
|
878
|
+
|
|
879
|
+
if (!isStreaming) {
|
|
880
|
+
return response.json();
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (typeof window === "undefined") {
|
|
884
|
+
return [];
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (isStreaming === "ndjson") {
|
|
888
|
+
const storeAdapter: NdjsonStreamingStore<NonNullable<TOutputSchema>, TErrorCode> = {
|
|
889
|
+
setData: (value) => {
|
|
890
|
+
mutatorStore.set({
|
|
891
|
+
...mutatorStore.get(),
|
|
892
|
+
loading: !(Array.isArray(value) && value.length > 0),
|
|
893
|
+
data: value as InferOr<TOutputSchema, undefined>,
|
|
894
|
+
});
|
|
895
|
+
},
|
|
896
|
+
setError: (value) => {
|
|
897
|
+
mutatorStore.set({
|
|
898
|
+
...mutatorStore.get(),
|
|
899
|
+
error: value,
|
|
900
|
+
});
|
|
901
|
+
},
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
// Start streaming in background and return first item
|
|
905
|
+
const { firstItem } = await handleNdjsonStreamingFirstItem(response, storeAdapter);
|
|
906
|
+
|
|
907
|
+
// Return the first item immediately. The streaming will continue in the background
|
|
908
|
+
return [firstItem];
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (isStreaming === "octet-stream") {
|
|
912
|
+
// TODO(Wilco): Implement this
|
|
913
|
+
throw new Error("Octet-stream streaming is not supported.");
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
throw new Error("Unreachable");
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
onError: (error) => {
|
|
920
|
+
console.error("Error in mutatorStore", error);
|
|
921
|
+
},
|
|
922
|
+
},
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
const mutateQuery = (async (data) => {
|
|
926
|
+
// TypeScript infers the fields to not exist, even though they might
|
|
927
|
+
const { body, path, query } = data as {
|
|
928
|
+
body?: InferOr<TInputSchema, undefined>;
|
|
929
|
+
path?: ExtractPathParamsOrWiden<TPath, string>;
|
|
930
|
+
query?: Record<string, string>;
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
if (typeof body === "undefined" && route.inputSchema !== undefined) {
|
|
934
|
+
throw new Error("Body is required for mutateQuery");
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const response = await executeMutateQuery({ body, path, query });
|
|
938
|
+
|
|
939
|
+
if (response.status === 201 || response.status === 204) {
|
|
940
|
+
return undefined;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const isStreaming = isStreamingResponse(response);
|
|
944
|
+
|
|
945
|
+
if (!isStreaming) {
|
|
946
|
+
return response.json();
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (isStreaming === "ndjson") {
|
|
950
|
+
const { streamingPromise } = await handleNdjsonStreamingFirstItem(response);
|
|
951
|
+
// Resolves once the stream is done, i.e. we block until done
|
|
952
|
+
return await streamingPromise;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (isStreaming === "octet-stream") {
|
|
956
|
+
throw new Error("Octet-stream streaming is not supported for mutations");
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
throw new Error("Unreachable");
|
|
960
|
+
}) satisfies FragnoClientMutatorData<
|
|
961
|
+
NonGetHTTPMethod,
|
|
962
|
+
TPath,
|
|
963
|
+
TInputSchema,
|
|
964
|
+
TOutputSchema,
|
|
965
|
+
TErrorCode,
|
|
966
|
+
TQueryParameters
|
|
967
|
+
>["mutateQuery"];
|
|
968
|
+
|
|
969
|
+
return {
|
|
970
|
+
route,
|
|
971
|
+
mutateQuery,
|
|
972
|
+
mutatorStore,
|
|
973
|
+
[MUTATOR_HOOK_SYMBOL]: true,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
#invalidate<TPath extends string>(
|
|
978
|
+
method: HTTPMethod,
|
|
979
|
+
path: TPath,
|
|
980
|
+
params: {
|
|
981
|
+
pathParams?: MaybeExtractPathParamsOrWiden<TPath, string>;
|
|
982
|
+
queryParams?: Record<string, string>;
|
|
983
|
+
},
|
|
984
|
+
) {
|
|
985
|
+
const prefixArray = getCacheKey(method, path, {
|
|
986
|
+
pathParams: params?.pathParams,
|
|
987
|
+
queryParams: params?.queryParams,
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
const prefix = prefixArray.map((k) => (typeof k === "string" ? k : k.get())).join("");
|
|
991
|
+
|
|
992
|
+
this.#invalidateKeys((key) => key.startsWith(prefix));
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export function createClientBuilder<
|
|
997
|
+
TConfig,
|
|
998
|
+
TDeps,
|
|
999
|
+
TServices extends Record<string, unknown>,
|
|
1000
|
+
const TRoutesOrFactories extends readonly AnyRouteOrFactory[],
|
|
1001
|
+
>(
|
|
1002
|
+
fragmentDefinition: FragmentBuilder<TConfig, TDeps, TServices>,
|
|
1003
|
+
publicConfig: FragnoPublicClientConfig,
|
|
1004
|
+
routesOrFactories: TRoutesOrFactories,
|
|
1005
|
+
): ClientBuilder<
|
|
1006
|
+
FlattenRouteFactories<TRoutesOrFactories>,
|
|
1007
|
+
FragnoFragmentSharedConfig<FlattenRouteFactories<TRoutesOrFactories>>
|
|
1008
|
+
> {
|
|
1009
|
+
const definition = fragmentDefinition.definition;
|
|
1010
|
+
|
|
1011
|
+
// For client-side, we resolve route factories with dummy context
|
|
1012
|
+
// This will be removed by the bundle plugin anyway
|
|
1013
|
+
const dummyContext = {
|
|
1014
|
+
config: {} as TConfig,
|
|
1015
|
+
deps: {} as TDeps,
|
|
1016
|
+
services: {} as TServices,
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
const routes = resolveRouteFactories(dummyContext, routesOrFactories);
|
|
1020
|
+
|
|
1021
|
+
const fragmentConfig: FragnoFragmentSharedConfig<FlattenRouteFactories<TRoutesOrFactories>> = {
|
|
1022
|
+
name: definition.name,
|
|
1023
|
+
routes,
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
const mountRoute = publicConfig.mountRoute ?? `/${definition.name}`;
|
|
1027
|
+
const fullPublicConfig = {
|
|
1028
|
+
...publicConfig,
|
|
1029
|
+
mountRoute,
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
return new ClientBuilder(fullPublicConfig, fragmentConfig);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
export * from "./client-error";
|