@lovrozagar/flare 0.1.2 → 0.2.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/README.md +42 -19
- package/package.json +8 -6
- package/src/broadcast/index.ts +1 -1
- package/src/broadcast/provider.tsx +2 -2
- package/src/caches/index.ts +26 -0
- package/src/components/await.tsx +36 -39
- package/src/components/dev-error-overlay.tsx +8 -9
- package/src/components/direction-script.tsx +2 -2
- package/src/components/locale-script.tsx +2 -2
- package/src/components/reset-css.tsx +1 -1
- package/src/components/ssr-context.tsx +7 -14
- package/src/components/theme-script.tsx +2 -2
- package/src/components/view-transition-css.tsx +1 -1
- package/src/defer/index.ts +22 -0
- package/src/direction/index.tsx +61 -36
- package/src/errors/index.ts +45 -11
- package/src/fonts/component.tsx +2 -1
- package/src/form/index.tsx +18 -12
- package/src/generators/index.ts +73 -30
- package/src/head-client/index.ts +86 -29
- package/src/history/index.ts +6 -0
- package/src/hydrate/index.tsx +90 -60
- package/src/hydration/index.ts +26 -6
- package/src/i18n/index.ts +11 -2
- package/src/image/index.tsx +7 -4
- package/src/intercept-outlet/index.tsx +6 -2
- package/src/lazy/index.tsx +50 -50
- package/src/link/index.tsx +97 -58
- package/src/loader-pipeline/index.ts +119 -38
- package/src/locale/cookie.ts +15 -0
- package/src/locale/index.tsx +24 -35
- package/src/locale.ts +1 -0
- package/src/middleware/builtins/api-proxy.ts +58 -4
- package/src/middleware/builtins/cdn-proxy.ts +24 -3
- package/src/middleware/builtins/i18n.ts +23 -25
- package/src/middleware/builtins/keepalive.ts +2 -3
- package/src/mount/index.ts +8 -1
- package/src/navigation/index.ts +575 -129
- package/src/navigation/types.ts +11 -1
- package/src/navigation-progress/index.tsx +17 -16
- package/src/ndjson-client/index.ts +42 -17
- package/src/ndjson-server/index.ts +78 -29
- package/src/outlet/index.tsx +192 -97
- package/src/outlet/types.ts +9 -1
- package/src/plugins/dev-cdn-cache.ts +18 -5
- package/src/plugins/dev-dashboard/client.tsx +130 -118
- package/src/plugins/dev-prerender.ts +6 -1
- package/src/plugins/dev-server.ts +12 -3
- package/src/plugins/image-plugin.ts +18 -4
- package/src/plugins/index.ts +10 -7
- package/src/plugins/prerender-plugin.ts +3 -3
- package/src/plugins/server-fn.ts +15 -20
- package/src/plugins/solid-jsx.d.ts +3 -3
- package/src/plugins/sx-ast/atomic.ts +5 -5
- package/src/plugins/sx-ast/index.ts +9 -0
- package/src/plugins/sx.d.ts +4 -5
- package/src/plugins/virtual.ts +11 -6
- package/src/prerender/index.ts +68 -32
- package/src/protocol.ts +65 -0
- package/src/query-client/index.tsx +73 -14
- package/src/rewrite/index.ts +10 -4
- package/src/route-builder/create-layout.ts +1 -1
- package/src/route-builder/create-root-layout.ts +1 -1
- package/src/route-builder/types.ts +5 -1
- package/src/router-primitives/types.ts +2 -0
- package/src/security/index.ts +41 -5
- package/src/server/index.ts +1 -2
- package/src/server-fn/get-input.ts +90 -0
- package/src/server-fn/http-error.ts +13 -0
- package/src/server-fn/index.ts +94 -70
- package/src/server-fn-query.ts +9 -17
- package/src/server-handler/etag.ts +4 -2
- package/src/server-handler/headers.ts +1 -2
- package/src/server-handler/index.ts +166 -58
- package/src/server-handler/types.ts +2 -0
- package/src/service-worker/template.ts +10 -42
- package/src/ssr/head-prefix.ts +2 -1
- package/src/ssr/hoist-head-markers.ts +25 -0
- package/src/ssr/index.tsx +109 -92
- package/src/state-parser/index.ts +38 -26
- package/src/styles/index.ts +89 -60
- package/src/testing/index.ts +8 -3
- package/src/theme/index.tsx +35 -32
- package/src/tracing/timing.ts +6 -1
- package/src/url/index.ts +9 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET server-fn input codec. Client-safe (no node:async_hooks).
|
|
3
|
+
*
|
|
4
|
+
* Nested objects/arrays JSON-encode so they do not become `[object Object]`.
|
|
5
|
+
* Primitive arrays use repeated keys (`tag=a&tag=b`) to match the existing
|
|
6
|
+
* multi-value GET parser. Strings that look like JSON literals are quoted so
|
|
7
|
+
* `{ id: "42" }` and `{ id: 42 }` round-trip as distinct values.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function isPrimitive(value: unknown): value is boolean | number | string | null {
|
|
11
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Query tokens that JSON.parse would accept — quote strings that collide with these. */
|
|
15
|
+
function isJsonLiteralToken(val: string): boolean {
|
|
16
|
+
if (val === "true" || val === "false" || val === "null") return true;
|
|
17
|
+
if (val.length >= 2 && val.startsWith('"') && val.endsWith('"')) return true;
|
|
18
|
+
if ((val.startsWith("{") && val.endsWith("}")) || (val.startsWith("[") && val.endsWith("]"))) return true;
|
|
19
|
+
return /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(val);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function encodeGetPrimitive(value: boolean | number | string | null): string {
|
|
23
|
+
if (typeof value === "string") return isJsonLiteralToken(value) ? JSON.stringify(value) : value;
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function appendGetParam(params: URLSearchParams, key: string, value: unknown): void {
|
|
28
|
+
if (value === undefined) return;
|
|
29
|
+
if (isPrimitive(value)) {
|
|
30
|
+
params.append(key, encodeGetPrimitive(value));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
34
|
+
params.append(key, "[]");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(value) && value.every(isPrimitive)) {
|
|
38
|
+
for (const item of value) {
|
|
39
|
+
params.append(key, encodeGetPrimitive(item));
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
params.append(key, JSON.stringify(value));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function serializeGetInput(input: unknown): string {
|
|
47
|
+
if (input === undefined || input === null) return "";
|
|
48
|
+
if (typeof input !== "object" || Array.isArray(input)) {
|
|
49
|
+
const params = new URLSearchParams();
|
|
50
|
+
appendGetParam(params, "value", input);
|
|
51
|
+
return params.toString();
|
|
52
|
+
}
|
|
53
|
+
const params = new URLSearchParams();
|
|
54
|
+
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
|
|
55
|
+
appendGetParam(params, key, value);
|
|
56
|
+
}
|
|
57
|
+
return params.toString();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function serverFnGetUrl(path: string, input: unknown): string {
|
|
61
|
+
if (input === undefined) return path;
|
|
62
|
+
const qs = serializeGetInput(input);
|
|
63
|
+
return qs.length > 0 ? `${path}?${qs}` : path;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseGetValue(val: string): unknown {
|
|
67
|
+
if (!isJsonLiteralToken(val)) return val;
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(val) as unknown;
|
|
70
|
+
} catch {
|
|
71
|
+
return val;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function parseGetSearchParams(searchParams: URLSearchParams): Record<string, unknown> | undefined {
|
|
76
|
+
const obj: Record<string, unknown> = Object.create(null);
|
|
77
|
+
let hasParams = false;
|
|
78
|
+
for (const [key, val] of searchParams) {
|
|
79
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
80
|
+
hasParams = true;
|
|
81
|
+
const parsed = parseGetValue(val);
|
|
82
|
+
const existing = obj[key];
|
|
83
|
+
if (existing !== undefined) {
|
|
84
|
+
obj[key] = Array.isArray(existing) ? [...(existing as unknown[]), parsed] : [existing, parsed];
|
|
85
|
+
} else {
|
|
86
|
+
obj[key] = parsed;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return hasParams ? obj : undefined;
|
|
90
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type FlattenedError, ServerFnValidationError } from "../errors/index.ts";
|
|
2
|
+
|
|
3
|
+
/** Map a failed server-fn HTTP body to the same error the server module throws. */
|
|
4
|
+
export function throwServerFnHttpError(body: unknown, status: number, name: string): never {
|
|
5
|
+
if (typeof body === "object" && body !== null && "errors" in body) {
|
|
6
|
+
throw new ServerFnValidationError((body as { errors: FlattenedError }).errors);
|
|
7
|
+
}
|
|
8
|
+
const errMsg =
|
|
9
|
+
typeof body === "object" && body !== null && "message" in body
|
|
10
|
+
? String((body as Record<string, unknown>).message)
|
|
11
|
+
: `Request failed (${status})`;
|
|
12
|
+
throw new Error(`Server function "${name}" failed: ${errMsg}`);
|
|
13
|
+
}
|
package/src/server-fn/index.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { FORM_FN_FIELD, parseServerFnPathname, serverFnPath } from "../protocol.ts";
|
|
2
2
|
import {
|
|
3
3
|
isNotFoundError,
|
|
4
4
|
isRedirectResponse,
|
|
5
5
|
isServerFnValidationError,
|
|
6
6
|
isUnauthenticatedError,
|
|
7
7
|
isUnauthorizedError,
|
|
8
|
-
ServerFnValidationError,
|
|
9
8
|
} from "../errors/index.ts";
|
|
9
|
+
import { parseGetSearchParams, serverFnGetUrl } from "./get-input.ts";
|
|
10
|
+
import { throwServerFnHttpError } from "./http-error.ts";
|
|
10
11
|
import type { RevalidateFn, RevalidateOptions } from "../revalidation/index.ts";
|
|
11
12
|
import { createRevalidateFn } from "../revalidation/index.ts";
|
|
12
13
|
/* package self-reference: routes through `exports.browser` -> stub on client, real on server.
|
|
@@ -284,22 +285,79 @@ function jsonResponse(data: unknown, status: number): Response {
|
|
|
284
285
|
});
|
|
285
286
|
}
|
|
286
287
|
|
|
288
|
+
/** Max POST body for `/_flare/server-fn/*` (JSON or form). */
|
|
289
|
+
export const SERVER_FN_MAX_BODY_BYTES = 1_048_576;
|
|
290
|
+
|
|
291
|
+
export async function readLimitedBody(
|
|
292
|
+
request: Request,
|
|
293
|
+
maxBytes: number,
|
|
294
|
+
): Promise<{ bytes: ArrayBuffer } | { error: Response }> {
|
|
295
|
+
const declared = Number(request.headers.get("content-length"));
|
|
296
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
297
|
+
return { error: jsonResponse({ message: "Payload too large" }, 413) };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const stream = request.body;
|
|
301
|
+
if (!stream) {
|
|
302
|
+
return { bytes: new ArrayBuffer(0) };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const reader = stream.getReader();
|
|
306
|
+
const chunks: Uint8Array[] = [];
|
|
307
|
+
let size = 0;
|
|
308
|
+
for (;;) {
|
|
309
|
+
const { done, value } = await reader.read();
|
|
310
|
+
if (done) break;
|
|
311
|
+
if (!value) continue;
|
|
312
|
+
size += value.byteLength;
|
|
313
|
+
if (size > maxBytes) {
|
|
314
|
+
await reader.cancel().catch(() => {});
|
|
315
|
+
return { error: jsonResponse({ message: "Payload too large" }, 413) };
|
|
316
|
+
}
|
|
317
|
+
chunks.push(value);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const bytes = new Uint8Array(size);
|
|
321
|
+
let offset = 0;
|
|
322
|
+
for (const chunk of chunks) {
|
|
323
|
+
bytes.set(chunk, offset);
|
|
324
|
+
offset += chunk.byteLength;
|
|
325
|
+
}
|
|
326
|
+
return { bytes: bytes.buffer };
|
|
327
|
+
}
|
|
328
|
+
|
|
287
329
|
/* ── CSRF Origin validation ────────────────────────────────────────── */
|
|
288
330
|
|
|
331
|
+
const ALLOWED_FETCH_SITES = new Set(["none", "same-origin", "same-site"]);
|
|
332
|
+
|
|
289
333
|
/**
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
* server function calls are just as dangerous as POST for CSRF.
|
|
294
|
-
* Missing Origin is allowed since CSRF is a browser-only attack vector
|
|
295
|
-
* and non-browser clients (curl, etc) won't have cookies.
|
|
334
|
+
* CSRF: Origin when present (any method), else Sec-Fetch-Site, else Referer.
|
|
335
|
+
* Top-level GET navigations omit Origin; SameSite=Lax still sends cookies.
|
|
336
|
+
* Missing all three is allowed for non-browser clients (curl, server-to-server).
|
|
296
337
|
*/
|
|
297
338
|
function validateOrigin(request: Request): boolean {
|
|
339
|
+
const requestOrigin = new URL(request.url).origin;
|
|
340
|
+
|
|
298
341
|
const origin = request.headers.get("origin");
|
|
299
|
-
if (
|
|
342
|
+
if (origin !== null) {
|
|
343
|
+
return origin === requestOrigin;
|
|
344
|
+
}
|
|
300
345
|
|
|
301
|
-
const
|
|
302
|
-
|
|
346
|
+
const site = request.headers.get("sec-fetch-site");
|
|
347
|
+
if (site !== null) {
|
|
348
|
+
return ALLOWED_FETCH_SITES.has(site);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const referer = request.headers.get("referer");
|
|
352
|
+
if (referer !== null) {
|
|
353
|
+
try {
|
|
354
|
+
return new URL(referer).origin === requestOrigin;
|
|
355
|
+
} catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return true;
|
|
303
361
|
}
|
|
304
362
|
|
|
305
363
|
/* ── formDataToObject ───────────────────────────────────────────────── */
|
|
@@ -307,7 +365,7 @@ function validateOrigin(request: Request): boolean {
|
|
|
307
365
|
export function formDataToObject(formData: FormData): Record<string, File | string | string[]> {
|
|
308
366
|
const obj: Record<string, File | string | string[]> = Object.create(null);
|
|
309
367
|
for (const [key, val] of formData) {
|
|
310
|
-
if (key ===
|
|
368
|
+
if (key === FORM_FN_FIELD || key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
311
369
|
if (val instanceof File) {
|
|
312
370
|
obj[key] = val;
|
|
313
371
|
continue;
|
|
@@ -330,20 +388,14 @@ export async function handleServerFnRequest(
|
|
|
330
388
|
fns: Map<string, ServerFnRegistration>,
|
|
331
389
|
authenticateFn?: (env: unknown, request: Request) => unknown | Promise<unknown>,
|
|
332
390
|
): Promise<Response> {
|
|
333
|
-
/* parse URL: /
|
|
391
|
+
/* parse URL: /_flare/server-fn/{id}/{name} */
|
|
334
392
|
const url = new URL(request.url);
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
if (segments.length < 3 || segments[0] !== "_fn") {
|
|
393
|
+
const parsed = parseServerFnPathname(url.pathname);
|
|
394
|
+
if (!parsed) {
|
|
338
395
|
return jsonResponse({ message: "Server function not found" }, 404);
|
|
339
396
|
}
|
|
340
397
|
|
|
341
|
-
const id =
|
|
342
|
-
const name = segments[2];
|
|
343
|
-
|
|
344
|
-
if (!id || !name) {
|
|
345
|
-
return jsonResponse({ message: "Server function not found" }, 404);
|
|
346
|
-
}
|
|
398
|
+
const { id, name } = parsed;
|
|
347
399
|
|
|
348
400
|
/* CSRF: validate Origin before any processing */
|
|
349
401
|
if (!validateOrigin(request)) {
|
|
@@ -377,36 +429,26 @@ export async function handleServerFnRequest(
|
|
|
377
429
|
/* parse input — FormData for form submissions, JSON for programmatic calls */
|
|
378
430
|
let input: unknown;
|
|
379
431
|
if (registration.method === "post") {
|
|
432
|
+
const limited = await readLimitedBody(request, SERVER_FN_MAX_BODY_BYTES);
|
|
433
|
+
if ("error" in limited) return limited.error;
|
|
380
434
|
const contentType = request.headers.get("content-type") ?? "";
|
|
381
435
|
if (contentType.includes("multipart/form-data") || contentType.includes("x-www-form-urlencoded")) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
436
|
+
const formRequest = new Request(request.url, {
|
|
437
|
+
body: limited.bytes,
|
|
438
|
+
headers: { "content-type": contentType },
|
|
439
|
+
method: "POST",
|
|
440
|
+
});
|
|
441
|
+
input = formDataToObject(await formRequest.formData());
|
|
442
|
+
} else if (limited.bytes.byteLength > 0) {
|
|
443
|
+
const text = new TextDecoder().decode(limited.bytes);
|
|
444
|
+
try {
|
|
445
|
+
input = JSON.parse(text);
|
|
446
|
+
} catch {
|
|
447
|
+
return jsonResponse({ message: "Invalid JSON" }, 400);
|
|
391
448
|
}
|
|
392
449
|
}
|
|
393
450
|
} else {
|
|
394
|
-
|
|
395
|
-
const obj: Record<string, string | string[]> = Object.create(null);
|
|
396
|
-
let hasParams = false;
|
|
397
|
-
for (const [key, val] of params) {
|
|
398
|
-
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
399
|
-
hasParams = true;
|
|
400
|
-
const existing = obj[key];
|
|
401
|
-
if (existing !== undefined) {
|
|
402
|
-
obj[key] = Array.isArray(existing) ? [...existing, val] : [existing, val];
|
|
403
|
-
} else {
|
|
404
|
-
obj[key] = val;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
if (hasParams) {
|
|
408
|
-
input = obj;
|
|
409
|
-
}
|
|
451
|
+
input = parseGetSearchParams(url.searchParams);
|
|
410
452
|
}
|
|
411
453
|
|
|
412
454
|
/* validate input — ServerFnValidationError propagates to outer catch for structured errors */
|
|
@@ -577,16 +619,12 @@ export function serverFnQueryOptions<TInput, TOutput>(
|
|
|
577
619
|
}
|
|
578
620
|
|
|
579
621
|
/* client: HTTP fetch */
|
|
580
|
-
const url =
|
|
622
|
+
const url = serverFnPath(id, name);
|
|
581
623
|
const method = reg?.method ?? "post";
|
|
582
624
|
|
|
583
625
|
const res =
|
|
584
626
|
method === "get"
|
|
585
|
-
? await fetch(
|
|
586
|
-
config?.input !== undefined
|
|
587
|
-
? `${url}?${new URLSearchParams(config.input as Record<string, string>)}`
|
|
588
|
-
: url,
|
|
589
|
-
)
|
|
627
|
+
? await fetch(serverFnGetUrl(url, config?.input))
|
|
590
628
|
: await fetch(url, {
|
|
591
629
|
body: config?.input !== undefined ? JSON.stringify(config.input) : undefined,
|
|
592
630
|
headers: { "content-type": "application/json" },
|
|
@@ -595,14 +633,7 @@ export function serverFnQueryOptions<TInput, TOutput>(
|
|
|
595
633
|
|
|
596
634
|
if (!res.ok) {
|
|
597
635
|
const body: unknown = await res.json().catch(() => null);
|
|
598
|
-
|
|
599
|
-
throw new ServerFnValidationError((body as { errors: FlattenedError }).errors);
|
|
600
|
-
}
|
|
601
|
-
const errMsg =
|
|
602
|
-
typeof body === "object" && body !== null && "message" in body
|
|
603
|
-
? String((body as Record<string, unknown>).message)
|
|
604
|
-
: `Request failed (${res.status})`;
|
|
605
|
-
throw new Error(`Server function "${name}" failed: ${errMsg}`);
|
|
636
|
+
throwServerFnHttpError(body, res.status, name);
|
|
606
637
|
}
|
|
607
638
|
|
|
608
639
|
const json = (await res.json()) as {
|
|
@@ -658,7 +689,7 @@ export function serverFnMutationOptions<TInput, TOutput>(
|
|
|
658
689
|
}
|
|
659
690
|
|
|
660
691
|
/* client: HTTP fetch — mutations always POST */
|
|
661
|
-
const url =
|
|
692
|
+
const url = serverFnPath(id, name);
|
|
662
693
|
const res = await fetch(url, {
|
|
663
694
|
body: input !== undefined ? JSON.stringify(input) : undefined,
|
|
664
695
|
headers: { "content-type": "application/json" },
|
|
@@ -667,14 +698,7 @@ export function serverFnMutationOptions<TInput, TOutput>(
|
|
|
667
698
|
|
|
668
699
|
if (!res.ok) {
|
|
669
700
|
const body: unknown = await res.json().catch(() => null);
|
|
670
|
-
|
|
671
|
-
throw new ServerFnValidationError((body as { errors: FlattenedError }).errors);
|
|
672
|
-
}
|
|
673
|
-
const errMsg =
|
|
674
|
-
typeof body === "object" && body !== null && "message" in body
|
|
675
|
-
? String((body as Record<string, unknown>).message)
|
|
676
|
-
: `Request failed (${res.status})`;
|
|
677
|
-
throw new Error(`Server function "${name}" failed: ${errMsg}`);
|
|
701
|
+
throwServerFnHttpError(body, res.status, name);
|
|
678
702
|
}
|
|
679
703
|
|
|
680
704
|
const json = (await res.json()) as {
|
package/src/server-fn-query.ts
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* to avoid pulling in node:async_hooks on the client.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { serverFnGetUrl } from "./server-fn/get-input.ts";
|
|
8
|
+
import { throwServerFnHttpError } from "./server-fn/http-error.ts";
|
|
9
|
+
import { serverFnPath } from "./protocol.ts";
|
|
10
|
+
|
|
7
11
|
export interface PiggybackedQuery {
|
|
8
12
|
data: unknown;
|
|
9
13
|
key: unknown[];
|
|
@@ -48,16 +52,12 @@ export function serverFnQueryOptions<TInput, TOutput>(
|
|
|
48
52
|
return serverFn(config?.input as TInput);
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
const url =
|
|
55
|
+
const url = serverFnPath(id, name);
|
|
52
56
|
const method = reg?.method ?? "post";
|
|
53
57
|
|
|
54
58
|
const res =
|
|
55
59
|
method === "get"
|
|
56
|
-
? await fetch(
|
|
57
|
-
config?.input !== undefined
|
|
58
|
-
? `${url}?${new URLSearchParams(config.input as Record<string, string>)}`
|
|
59
|
-
: url,
|
|
60
|
-
)
|
|
60
|
+
? await fetch(serverFnGetUrl(url, config?.input))
|
|
61
61
|
: await fetch(url, {
|
|
62
62
|
body: config?.input !== undefined ? JSON.stringify(config.input) : undefined,
|
|
63
63
|
headers: { "content-type": "application/json" },
|
|
@@ -66,11 +66,7 @@ export function serverFnQueryOptions<TInput, TOutput>(
|
|
|
66
66
|
|
|
67
67
|
if (!res.ok) {
|
|
68
68
|
const body: unknown = await res.json().catch(() => null);
|
|
69
|
-
|
|
70
|
-
typeof body === "object" && body !== null && "message" in body
|
|
71
|
-
? String((body as Record<string, unknown>).message)
|
|
72
|
-
: `Request failed (${res.status})`;
|
|
73
|
-
throw new Error(`Server function request failed: ${errMsg}`);
|
|
69
|
+
throwServerFnHttpError(body, res.status, name);
|
|
74
70
|
}
|
|
75
71
|
|
|
76
72
|
const json = (await res.json()) as {
|
|
@@ -121,7 +117,7 @@ export function serverFnMutationOptions<TInput, TOutput>(
|
|
|
121
117
|
return serverFn(input);
|
|
122
118
|
}
|
|
123
119
|
|
|
124
|
-
const url =
|
|
120
|
+
const url = serverFnPath(id, name);
|
|
125
121
|
const res = await fetch(url, {
|
|
126
122
|
body: input !== undefined ? JSON.stringify(input) : undefined,
|
|
127
123
|
headers: { "content-type": "application/json" },
|
|
@@ -130,11 +126,7 @@ export function serverFnMutationOptions<TInput, TOutput>(
|
|
|
130
126
|
|
|
131
127
|
if (!res.ok) {
|
|
132
128
|
const body: unknown = await res.json().catch(() => null);
|
|
133
|
-
|
|
134
|
-
typeof body === "object" && body !== null && "message" in body
|
|
135
|
-
? String((body as Record<string, unknown>).message)
|
|
136
|
-
: `Request failed (${res.status})`;
|
|
137
|
-
throw new Error(`Server function request failed: ${errMsg}`);
|
|
129
|
+
throwServerFnHttpError(body, res.status, name);
|
|
138
130
|
}
|
|
139
131
|
|
|
140
132
|
const json = (await res.json()) as {
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* the actual bytes while content is semantically identical.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { HEADER_DATA } from "../protocol.ts";
|
|
8
|
+
|
|
7
9
|
/** Compute weak ETag from content using SHA-256, truncated to 16 hex chars */
|
|
8
10
|
export async function computeEtag(body: string): Promise<string> {
|
|
9
11
|
const encoded = new TextEncoder().encode(body);
|
|
@@ -35,11 +37,11 @@ export function weakMatch(ifNoneMatch: string, etag: string): boolean {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
|
-
* Build Vary header value. Always includes `
|
|
40
|
+
* Build Vary header value. Always includes `flare-data` (NDJSON protocol header).
|
|
39
41
|
* Additional values are appended, with deduplication.
|
|
40
42
|
*/
|
|
41
43
|
export function buildVaryHeader(additional?: string[]): string {
|
|
42
|
-
const values = [
|
|
44
|
+
const values = [HEADER_DATA];
|
|
43
45
|
if (additional) {
|
|
44
46
|
for (const v of additional) {
|
|
45
47
|
if (!values.some((existing) => existing.toLowerCase() === v.toLowerCase())) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export type FlareCacheStatus = "HIT" | "MISS" | "STALE";
|
|
2
2
|
export type FlareRenderMode = "ISR" | "SSG" | "SSR";
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
export const FLARE_RENDER_HEADER = "Flare-Render" as const;
|
|
4
|
+
export { FLARE_CACHE_HEADER, FLARE_RENDER_HEADER } from "../protocol.ts";
|