@solidjs/web 2.0.0-rc.3 → 2.0.0-rc.4

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.
@@ -26,6 +26,32 @@ function withMeta(fn, meta) {
26
26
  Object.assign(metadata, meta);
27
27
  return fn;
28
28
  }
29
+ const SERVER_FUNCTION_INVOKE = Symbol.for("solid.ServerFunctionInvoke");
30
+ const INVOKE_OPTION_REDIRECTS = {
31
+ headers: "Session-dynamic headers belong to the prepareRequest hook, declaration metadata to " + "withMeta(fn, meta), and data belongs in the arguments, where it is serialized, typed, " + "and part of the cache key.",
32
+ method: "The method is declaration-scoped: declare the function with GET(fn).",
33
+ body: "The arguments are the body: pass them in the args array.",
34
+ timeout: "Compose timeouts through `signal` with AbortSignal.timeout(ms) (and AbortSignal.any to " + "combine it with a caller signal)."
35
+ };
36
+ function invoke(fn, options, ...args) {
37
+ const channel = typeof fn === "function" && fn[SERVER_FUNCTION_INVOKE];
38
+ if (!channel) {
39
+ throw new Error(isServerFunction(fn) ? "invoke: this wrapper does not forward the invocation channel " + "(SERVER_FUNCTION_INVOKE). Wrappers that share calls (caches, channels) opt in " + "deliberately — a caller's signal cannot own a wire other callers share. Invoke " + "the underlying reference directly, or use the wrapper's own per-call idioms." : "invoke expects a server function reference (or a wrapper that forwards its " + "invocation channel). Per-call options apply at the transport; for a data " + "layer's calls, use its per-call options instead.");
40
+ }
41
+ if (options === null || typeof options !== "object") {
42
+ throw new Error("invoke's second argument is the invocation options bag: invoke(fn, { signal }, ...args)");
43
+ }
44
+ const picked = {};
45
+ for (const key in options) {
46
+ if (key !== "signal" && key !== "keepalive" && key !== "priority") {
47
+ throw new Error(`\`${key}\` is not an invocation option. ` + (INVOKE_OPTION_REDIRECTS[key] || "Options here are strictly invocation-scoped (they vary between calls of the " + "same function): signal, keepalive, priority."));
48
+ }
49
+ }
50
+ if (options.signal !== undefined) picked.signal = options.signal;
51
+ if (options.keepalive !== undefined) picked.keepalive = options.keepalive;
52
+ if (options.priority !== undefined) picked.priority = options.priority;
53
+ return channel(args, picked);
54
+ }
29
55
  const LIVE_SOURCE = Symbol.for("solid.LiveSource");
30
56
  const SERVER_FUNCTION_RPC = Symbol.for("solid.ServerFunctionRPC");
31
57
  function provideServerFunctionRPC(rpc) {
@@ -90,7 +116,23 @@ function subscribeFlightData(consumer) {
90
116
  return () => {
91
117
  };
92
118
  }
93
- const FUNCTION_HEADER = "X-Server-Function-Id";
119
+ function serverFunctionAddress(endpoint, id) {
120
+ const mount = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
121
+ return `${mount}/${encodeURIComponent(id)}`;
122
+ }
123
+ function parseServerFunctionAddress(pathname, endpoint) {
124
+ const mount = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
125
+ if (!pathname.startsWith(mount)) return null;
126
+ const rest = pathname.slice(mount.length);
127
+ if (!rest.startsWith("/")) return null;
128
+ const segment = rest.slice(1);
129
+ if (!segment || segment.includes("/")) return null;
130
+ try {
131
+ return decodeURIComponent(segment);
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
94
136
  const ERROR_HEADER = "X-Server-Function-Error";
95
137
  const ERROR_HEADER_MARKER = "=?1?";
96
138
  const NEEDS_ENCODING = /[^\x20-\x7e\xa0-\xff]/;
@@ -129,7 +171,8 @@ const BodyFormat = {
129
171
  File: "5",
130
172
  ArrayBuffer: "6",
131
173
  Uint8Array: "7",
132
- Json: "8"
174
+ Json: "8",
175
+ Void: "9"
133
176
  };
134
177
  const JSON_SAFE_DEPTH_LIMIT = 4096;
135
178
  const EXIT = {};
@@ -549,6 +592,9 @@ function getServerFunction(id) {
549
592
  throw new Error("invalid server function: " + id);
550
593
  }
551
594
  function registerServerReference(id, fn, name) {
595
+ if (typeof fn !== "function") {
596
+ throw new Error(`Server function${name ? ` \`${name}\`` : ""} (${id}) is not a function: a module-level ` + `"use server" export must evaluate to a server function (got ${fn === null ? "null" : typeof fn}). Move non-function exports out of the directive module.`);
597
+ }
552
598
  registerServerFunction(id, fn);
553
599
  return {
554
600
  id,
@@ -556,6 +602,33 @@ function registerServerReference(id, fn, name) {
556
602
  name
557
603
  };
558
604
  }
605
+ function inProcessInvoker(call) {
606
+ return (args, options) => {
607
+ const signal = options && options.signal;
608
+ if (!signal) return call(...args);
609
+ if (signal.aborted) return Promise.reject(signal.reason);
610
+ return new Promise((resolve, reject) => {
611
+ const onAbort = () => reject(signal.reason);
612
+ signal.addEventListener("abort", onAbort, {
613
+ once: true
614
+ });
615
+ let result;
616
+ try {
617
+ result = call(...args);
618
+ } catch (error) {
619
+ signal.removeEventListener("abort", onAbort);
620
+ return reject(error);
621
+ }
622
+ Promise.resolve(result).then(value => {
623
+ signal.removeEventListener("abort", onAbort);
624
+ resolve(value);
625
+ }, error => {
626
+ signal.removeEventListener("abort", onAbort);
627
+ reject(error);
628
+ });
629
+ });
630
+ };
631
+ }
559
632
  function createServerReference({
560
633
  id,
561
634
  fn,
@@ -566,13 +639,15 @@ function createServerReference({
566
639
  const metadata = name === undefined ? {} : {
567
640
  name
568
641
  };
569
- return new Proxy(fn, {
642
+ const invokeChannel = inProcessInvoker((...args) => proxy(...args));
643
+ const proxy = new Proxy(fn, {
570
644
  get(target, prop) {
571
645
  if (prop === "id") return id;
572
646
  if (prop === "url") {
573
- return `${config.endpoint}?id=${encodeURIComponent(id)}`;
647
+ return serverFunctionAddress(config.endpoint, id);
574
648
  }
575
649
  if (prop === SERVER_FUNCTION_METADATA) return metadata;
650
+ if (prop === SERVER_FUNCTION_INVOKE) return invokeChannel;
576
651
  return target[prop];
577
652
  },
578
653
  apply(target, thisArg, args) {
@@ -609,6 +684,7 @@ function createServerReference({
609
684
  }) : result;
610
685
  }
611
686
  });
687
+ return proxy;
612
688
  }
613
689
  function GET(fn) {
614
690
  if (!isServerFunction(fn) || typeof fn.id !== "string") {
@@ -635,6 +711,7 @@ function live(fn) {
635
711
  return result;
636
712
  };
637
713
  wrapped[SERVER_FUNCTION_METADATA] = metadata;
714
+ wrapped[SERVER_FUNCTION_INVOKE] = inProcessInvoker(wrapped);
638
715
  wrapped.id = fn.id;
639
716
  Object.defineProperty(wrapped, "url", {
640
717
  get: () => fn.url,
@@ -648,24 +725,23 @@ function getServerFunctionInvocation() {
648
725
  function getEventServerFunctionInvocation(event) {
649
726
  return event && INVOCATIONS.get(event);
650
727
  }
651
- function resolveFunctionId(request, url) {
652
- const reference = request.headers.get(FUNCTION_HEADER);
653
- if (reference) {
654
- return reference.split("#")[0];
655
- }
656
- return url.searchParams.get("id");
728
+ function resolveFunctionId(url) {
729
+ return parseServerFunctionAddress(url.pathname, config.endpoint);
657
730
  }
658
731
  async function parseArguments(request, url, instance, codec) {
659
732
  const parsed = [];
660
733
  const bodyFormat = request.method === "POST" ? request.headers.get(BODY_FORMAT_HEADER) : null;
661
- if (!instance || request.method === "GET" || bodyFormat !== BodyFormat.Serialized) {
662
- const args = url.searchParams.get("args");
663
- if (args) {
664
- const result = args.startsWith(";0x") ? await deserializeString(args, codec) : JSON.parse(args);
665
- for (const arg of result) {
666
- parsed.push(arg);
667
- }
734
+ const args = url.searchParams.get("args");
735
+ if (args && (!instance || request.method === "GET" || bodyFormat !== BodyFormat.Serialized)) {
736
+ const result = args.startsWith(";0x") ? await deserializeString(args, codec) : JSON.parse(args);
737
+ if (!Array.isArray(result)) {
738
+ throw new TypeError("Server function arguments must encode an array");
739
+ }
740
+ for (const arg of result) {
741
+ parsed.push(arg);
668
742
  }
743
+ } else if (!args && url.search && (request.method === "GET" || request.method === "HEAD")) {
744
+ parsed.push(url.searchParams);
669
745
  }
670
746
  if (request.method === "POST" && request.body !== null) {
671
747
  const decoded = await extractBody(request.clone(), codec);
@@ -920,6 +996,7 @@ function encodeResult(value, headers, status, codec, signal) {
920
996
  });
921
997
  }
922
998
  if (value === undefined) {
999
+ headers.set(BODY_FORMAT_HEADER, BodyFormat.Void);
923
1000
  return new Response(null, {
924
1001
  status,
925
1002
  headers
@@ -955,6 +1032,17 @@ function sanitizeServerError(value) {
955
1032
  function observeServerFunctionCalls() {
956
1033
  return () => {};
957
1034
  }
1035
+ function serverFunctionUrl(id, boundArgs) {
1036
+ const address = serverFunctionAddress(config.endpoint, id);
1037
+ if (!boundArgs || !boundArgs.length) return address;
1038
+ if (!isJSONSafe(boundArgs)) {
1039
+ throw new Error("Bound arguments in an action url must be JSON-safe: the server reads them the way it " + "reads a form post's, and that convention has no codec. Pass the value through the " + "function's body, or call the reference instead of rendering a url for it.");
1040
+ }
1041
+ return `${address}?args=${encodeURIComponent(JSON.stringify(boundArgs))}`;
1042
+ }
1043
+ function parseServerFunctionUrl(url) {
1044
+ return parseServerFunctionAddress(new URL(url, "http://localhost").pathname, config.endpoint);
1045
+ }
958
1046
  async function matchesOrigin(origin, request, matcher) {
959
1047
  if (matcher === undefined) return origin === new URL(request.url).origin;
960
1048
  if (typeof matcher === "function") return !!(await matcher(origin, request));
@@ -1012,18 +1100,20 @@ function forbiddenResponse() {
1012
1100
  async function handleServerFunctionRequest(request, options = {}) {
1013
1101
  const codec = options.codec !== undefined ? options.codec : getServerFunctionsCodec();
1014
1102
  const url = new URL(request.url);
1103
+ const method = request.method;
1104
+ const functionId = resolveFunctionId(url);
1105
+ const declaredRead = (method === "GET" || method === "HEAD") && functionId !== null && METHODS.get(functionId) === "GET";
1015
1106
  const csrf = options.csrf !== undefined ? options.csrf : config.csrf;
1016
- const protectsRequest = csrf !== false;
1107
+ const protectsRequest = csrf !== false && !declaredRead;
1017
1108
  if (protectsRequest && !(await allowsServerFunctionRequest(request, csrf === true ? {} : csrf))) {
1018
- return forbiddenResponse();
1109
+ return finalizeTransportResponse(forbiddenResponse(), method);
1019
1110
  }
1020
1111
  const instance = request.headers.get(INSTANCE_HEADER);
1021
- const functionId = resolveFunctionId(request, url);
1022
1112
  if (!functionId) {
1023
1113
  const response = new Response(DEV ? "Server function not found" : null, {
1024
1114
  status: 404
1025
1115
  });
1026
- return protectsRequest ? withCSRFVary(response) : response;
1116
+ return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
1027
1117
  }
1028
1118
  let serverFunction;
1029
1119
  try {
@@ -1032,16 +1122,16 @@ async function handleServerFunctionRequest(request, options = {}) {
1032
1122
  const response = new Response(DEV ? `Unknown server function: ${functionId}` : null, {
1033
1123
  status: 404
1034
1124
  });
1035
- return protectsRequest ? withCSRFVary(response) : response;
1125
+ return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
1036
1126
  }
1037
- if (request.method === "GET" && METHODS.get(functionId) !== "GET") {
1127
+ if (method !== "POST" && !declaredRead) {
1038
1128
  const response = new Response(DEV ? `Method not allowed for server function: ${functionId}` : null, {
1039
1129
  status: 405,
1040
1130
  headers: {
1041
- Allow: "POST"
1131
+ Allow: METHODS.get(functionId) === "GET" ? "POST, GET, HEAD" : "POST"
1042
1132
  }
1043
1133
  });
1044
- return protectsRequest ? withCSRFVary(response) : response;
1134
+ return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
1045
1135
  }
1046
1136
  const event = options.createEvent ? options.createEvent(request) : {
1047
1137
  request,
@@ -1054,7 +1144,15 @@ async function handleServerFunctionRequest(request, options = {}) {
1054
1144
  const transformFlightResult = options.transformFlightResult !== undefined ? options.transformFlightResult : config.transformFlightResult;
1055
1145
  const handleNoJS = options.handleNoJS !== undefined ? options.handleNoJS : config.handleNoJS !== undefined ? config.handleNoJS : isFormPost(request) ? defaultNoJSHandler || (defaultNoJSHandler = createNoJSHandler()) : undefined;
1056
1146
  const collectsFlight = !!(flightHook && instance && request.headers.has(SINGLE_FLIGHT_HEADER));
1057
- const parsed = await parseArguments(request, url, instance, codec);
1147
+ let parsed;
1148
+ try {
1149
+ parsed = await parseArguments(request, url, instance, codec);
1150
+ } catch {
1151
+ const response = new Response(DEV ? "Malformed server function arguments" : null, {
1152
+ status: 400
1153
+ });
1154
+ return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
1155
+ }
1058
1156
  const flightContext = {
1059
1157
  id: functionId,
1060
1158
  args: parsed,
@@ -1201,7 +1299,34 @@ async function handleServerFunctionRequest(request, options = {}) {
1201
1299
  }
1202
1300
  };
1203
1301
  const response = commitEventResponse(await dispatch(), event);
1204
- return protectsRequest ? withCSRFVary(response) : response;
1302
+ return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
1303
+ }
1304
+ function finalizeTransportResponse(response, method) {
1305
+ const stripBody = method === "HEAD" && response.body !== null;
1306
+ if (stripBody || !response.headers.has("Cache-Control")) {
1307
+ try {
1308
+ if (!response.headers.has("Cache-Control")) {
1309
+ response.headers.set("Cache-Control", "no-store");
1310
+ }
1311
+ if (!stripBody) return response;
1312
+ response.body.cancel().catch(() => {});
1313
+ return new Response(null, {
1314
+ status: response.status,
1315
+ statusText: response.statusText,
1316
+ headers: response.headers
1317
+ });
1318
+ } catch {
1319
+ const headers = new Headers(response.headers);
1320
+ if (!headers.has("Cache-Control")) headers.set("Cache-Control", "no-store");
1321
+ if (stripBody) response.body.cancel().catch(() => {});
1322
+ return new Response(stripBody ? null : response.body, {
1323
+ status: response.status,
1324
+ statusText: response.statusText,
1325
+ headers
1326
+ });
1327
+ }
1328
+ }
1329
+ return response;
1205
1330
  }
1206
1331
 
1207
- export { ERROR_HEADER, FLASH_COOKIE, FUNCTION_HEADER, GENERIC_SERVER_ERROR_MESSAGE, GET, INSTANCE_HEADER, SINGLE_FLIGHT_HEADER, clearFlashCookie, configureServerFunctionsServer, createNoJSHandler, createServerReference, decodeErrorHeaderValue, decodeFlashCookie, decodeResponse, decodeResponsePayload, encodeErrorHeaderValue, encodeFlashCookie, foldSetCookies, getEventServerFunctionInvocation, getServerFunction, getServerFunctionInvocation, getServerFunctionMetadata, handleServerFunctionRequest, hasFlashCookie, isServerFunction, live, observeServerFunctionCalls, registerServerFunction, registerServerReference, sanitizeServerError, serializeResponseStream, setServerFunctionsDev, subscribeFlightData, withMeta };
1332
+ export { ERROR_HEADER, FLASH_COOKIE, GENERIC_SERVER_ERROR_MESSAGE, GET, INSTANCE_HEADER, SERVER_FUNCTION_INVOKE, SINGLE_FLIGHT_HEADER, clearFlashCookie, configureServerFunctionsServer, createNoJSHandler, createServerReference, decodeErrorHeaderValue, decodeFlashCookie, decodeResponse, decodeResponsePayload, encodeErrorHeaderValue, encodeFlashCookie, foldSetCookies, getEventServerFunctionInvocation, getServerFunction, getServerFunctionInvocation, getServerFunctionMetadata, handleServerFunctionRequest, hasFlashCookie, invoke, isServerFunction, live, observeServerFunctionCalls, parseServerFunctionUrl, registerServerFunction, registerServerReference, sanitizeServerError, serializeResponseStream, serverFunctionUrl, setServerFunctionsDev, subscribeFlightData, withMeta };
@@ -26,6 +26,32 @@ function withMeta(fn, meta) {
26
26
  Object.assign(metadata, meta);
27
27
  return fn;
28
28
  }
29
+ const SERVER_FUNCTION_INVOKE = Symbol.for("solid.ServerFunctionInvoke");
30
+ const INVOKE_OPTION_REDIRECTS = {
31
+ headers: "Session-dynamic headers belong to the prepareRequest hook, declaration metadata to " + "withMeta(fn, meta), and data belongs in the arguments, where it is serialized, typed, " + "and part of the cache key.",
32
+ method: "The method is declaration-scoped: declare the function with GET(fn).",
33
+ body: "The arguments are the body: pass them in the args array.",
34
+ timeout: "Compose timeouts through `signal` with AbortSignal.timeout(ms) (and AbortSignal.any to " + "combine it with a caller signal)."
35
+ };
36
+ function invoke(fn, options, ...args) {
37
+ const channel = typeof fn === "function" && fn[SERVER_FUNCTION_INVOKE];
38
+ if (!channel) {
39
+ throw new Error(isServerFunction(fn) ? "invoke: this wrapper does not forward the invocation channel " + "(SERVER_FUNCTION_INVOKE). Wrappers that share calls (caches, channels) opt in " + "deliberately — a caller's signal cannot own a wire other callers share. Invoke " + "the underlying reference directly, or use the wrapper's own per-call idioms." : "invoke expects a server function reference (or a wrapper that forwards its " + "invocation channel). Per-call options apply at the transport; for a data " + "layer's calls, use its per-call options instead.");
40
+ }
41
+ if (options === null || typeof options !== "object") {
42
+ throw new Error("invoke's second argument is the invocation options bag: invoke(fn, { signal }, ...args)");
43
+ }
44
+ const picked = {};
45
+ for (const key in options) {
46
+ if (key !== "signal" && key !== "keepalive" && key !== "priority") {
47
+ throw new Error(`\`${key}\` is not an invocation option. ` + (INVOKE_OPTION_REDIRECTS[key] || "Options here are strictly invocation-scoped (they vary between calls of the " + "same function): signal, keepalive, priority."));
48
+ }
49
+ }
50
+ if (options.signal !== undefined) picked.signal = options.signal;
51
+ if (options.keepalive !== undefined) picked.keepalive = options.keepalive;
52
+ if (options.priority !== undefined) picked.priority = options.priority;
53
+ return channel(args, picked);
54
+ }
29
55
  const LIVE_SOURCE = Symbol.for("solid.LiveSource");
30
56
  const SERVER_FUNCTION_RPC = Symbol.for("solid.ServerFunctionRPC");
31
57
  function provideServerFunctionRPC(rpc) {
@@ -90,7 +116,23 @@ function subscribeFlightData(consumer) {
90
116
  return () => {
91
117
  };
92
118
  }
93
- const FUNCTION_HEADER = "X-Server-Function-Id";
119
+ function serverFunctionAddress(endpoint, id) {
120
+ const mount = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
121
+ return `${mount}/${encodeURIComponent(id)}`;
122
+ }
123
+ function parseServerFunctionAddress(pathname, endpoint) {
124
+ const mount = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
125
+ if (!pathname.startsWith(mount)) return null;
126
+ const rest = pathname.slice(mount.length);
127
+ if (!rest.startsWith("/")) return null;
128
+ const segment = rest.slice(1);
129
+ if (!segment || segment.includes("/")) return null;
130
+ try {
131
+ return decodeURIComponent(segment);
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
94
136
  const ERROR_HEADER = "X-Server-Function-Error";
95
137
  const ERROR_HEADER_MARKER = "=?1?";
96
138
  const NEEDS_ENCODING = /[^\x20-\x7e\xa0-\xff]/;
@@ -129,7 +171,8 @@ const BodyFormat = {
129
171
  File: "5",
130
172
  ArrayBuffer: "6",
131
173
  Uint8Array: "7",
132
- Json: "8"
174
+ Json: "8",
175
+ Void: "9"
133
176
  };
134
177
  const JSON_SAFE_DEPTH_LIMIT = 4096;
135
178
  const EXIT = {};
@@ -549,6 +592,9 @@ function getServerFunction(id) {
549
592
  throw new Error("invalid server function: " + id);
550
593
  }
551
594
  function registerServerReference(id, fn, name) {
595
+ if (typeof fn !== "function") {
596
+ throw new Error(`Server function${name ? ` \`${name}\`` : ""} (${id}) is not a function: a module-level ` + `"use server" export must evaluate to a server function (got ${fn === null ? "null" : typeof fn}). Move non-function exports out of the directive module.`);
597
+ }
552
598
  registerServerFunction(id, fn);
553
599
  return {
554
600
  id,
@@ -556,6 +602,33 @@ function registerServerReference(id, fn, name) {
556
602
  name
557
603
  };
558
604
  }
605
+ function inProcessInvoker(call) {
606
+ return (args, options) => {
607
+ const signal = options && options.signal;
608
+ if (!signal) return call(...args);
609
+ if (signal.aborted) return Promise.reject(signal.reason);
610
+ return new Promise((resolve, reject) => {
611
+ const onAbort = () => reject(signal.reason);
612
+ signal.addEventListener("abort", onAbort, {
613
+ once: true
614
+ });
615
+ let result;
616
+ try {
617
+ result = call(...args);
618
+ } catch (error) {
619
+ signal.removeEventListener("abort", onAbort);
620
+ return reject(error);
621
+ }
622
+ Promise.resolve(result).then(value => {
623
+ signal.removeEventListener("abort", onAbort);
624
+ resolve(value);
625
+ }, error => {
626
+ signal.removeEventListener("abort", onAbort);
627
+ reject(error);
628
+ });
629
+ });
630
+ };
631
+ }
559
632
  function createServerReference({
560
633
  id,
561
634
  fn,
@@ -566,13 +639,15 @@ function createServerReference({
566
639
  const metadata = name === undefined ? {} : {
567
640
  name
568
641
  };
569
- return new Proxy(fn, {
642
+ const invokeChannel = inProcessInvoker((...args) => proxy(...args));
643
+ const proxy = new Proxy(fn, {
570
644
  get(target, prop) {
571
645
  if (prop === "id") return id;
572
646
  if (prop === "url") {
573
- return `${config.endpoint}?id=${encodeURIComponent(id)}`;
647
+ return serverFunctionAddress(config.endpoint, id);
574
648
  }
575
649
  if (prop === SERVER_FUNCTION_METADATA) return metadata;
650
+ if (prop === SERVER_FUNCTION_INVOKE) return invokeChannel;
576
651
  return target[prop];
577
652
  },
578
653
  apply(target, thisArg, args) {
@@ -609,6 +684,7 @@ function createServerReference({
609
684
  }) : result;
610
685
  }
611
686
  });
687
+ return proxy;
612
688
  }
613
689
  function GET(fn) {
614
690
  if (!isServerFunction(fn) || typeof fn.id !== "string") {
@@ -635,6 +711,7 @@ function live(fn) {
635
711
  return result;
636
712
  };
637
713
  wrapped[SERVER_FUNCTION_METADATA] = metadata;
714
+ wrapped[SERVER_FUNCTION_INVOKE] = inProcessInvoker(wrapped);
638
715
  wrapped.id = fn.id;
639
716
  Object.defineProperty(wrapped, "url", {
640
717
  get: () => fn.url,
@@ -648,24 +725,23 @@ function getServerFunctionInvocation() {
648
725
  function getEventServerFunctionInvocation(event) {
649
726
  return event && INVOCATIONS.get(event);
650
727
  }
651
- function resolveFunctionId(request, url) {
652
- const reference = request.headers.get(FUNCTION_HEADER);
653
- if (reference) {
654
- return reference.split("#")[0];
655
- }
656
- return url.searchParams.get("id");
728
+ function resolveFunctionId(url) {
729
+ return parseServerFunctionAddress(url.pathname, config.endpoint);
657
730
  }
658
731
  async function parseArguments(request, url, instance, codec) {
659
732
  const parsed = [];
660
733
  const bodyFormat = request.method === "POST" ? request.headers.get(BODY_FORMAT_HEADER) : null;
661
- if (!instance || request.method === "GET" || bodyFormat !== BodyFormat.Serialized) {
662
- const args = url.searchParams.get("args");
663
- if (args) {
664
- const result = args.startsWith(";0x") ? await deserializeString(args, codec) : JSON.parse(args);
665
- for (const arg of result) {
666
- parsed.push(arg);
667
- }
734
+ const args = url.searchParams.get("args");
735
+ if (args && (!instance || request.method === "GET" || bodyFormat !== BodyFormat.Serialized)) {
736
+ const result = args.startsWith(";0x") ? await deserializeString(args, codec) : JSON.parse(args);
737
+ if (!Array.isArray(result)) {
738
+ throw new TypeError("Server function arguments must encode an array");
739
+ }
740
+ for (const arg of result) {
741
+ parsed.push(arg);
668
742
  }
743
+ } else if (!args && url.search && (request.method === "GET" || request.method === "HEAD")) {
744
+ parsed.push(url.searchParams);
669
745
  }
670
746
  if (request.method === "POST" && request.body !== null) {
671
747
  const decoded = await extractBody(request.clone(), codec);
@@ -920,6 +996,7 @@ function encodeResult(value, headers, status, codec, signal) {
920
996
  });
921
997
  }
922
998
  if (value === undefined) {
999
+ headers.set(BODY_FORMAT_HEADER, BodyFormat.Void);
923
1000
  return new Response(null, {
924
1001
  status,
925
1002
  headers
@@ -955,6 +1032,17 @@ function sanitizeServerError(value) {
955
1032
  function observeServerFunctionCalls() {
956
1033
  return () => {};
957
1034
  }
1035
+ function serverFunctionUrl(id, boundArgs) {
1036
+ const address = serverFunctionAddress(config.endpoint, id);
1037
+ if (!boundArgs || !boundArgs.length) return address;
1038
+ if (!isJSONSafe(boundArgs)) {
1039
+ throw new Error("Bound arguments in an action url must be JSON-safe: the server reads them the way it " + "reads a form post's, and that convention has no codec. Pass the value through the " + "function's body, or call the reference instead of rendering a url for it.");
1040
+ }
1041
+ return `${address}?args=${encodeURIComponent(JSON.stringify(boundArgs))}`;
1042
+ }
1043
+ function parseServerFunctionUrl(url) {
1044
+ return parseServerFunctionAddress(new URL(url, "http://localhost").pathname, config.endpoint);
1045
+ }
958
1046
  async function matchesOrigin(origin, request, matcher) {
959
1047
  if (matcher === undefined) return origin === new URL(request.url).origin;
960
1048
  if (typeof matcher === "function") return !!(await matcher(origin, request));
@@ -1012,18 +1100,20 @@ function forbiddenResponse() {
1012
1100
  async function handleServerFunctionRequest(request, options = {}) {
1013
1101
  const codec = options.codec !== undefined ? options.codec : getServerFunctionsCodec();
1014
1102
  const url = new URL(request.url);
1103
+ const method = request.method;
1104
+ const functionId = resolveFunctionId(url);
1105
+ const declaredRead = (method === "GET" || method === "HEAD") && functionId !== null && METHODS.get(functionId) === "GET";
1015
1106
  const csrf = options.csrf !== undefined ? options.csrf : config.csrf;
1016
- const protectsRequest = csrf !== false;
1107
+ const protectsRequest = csrf !== false && !declaredRead;
1017
1108
  if (protectsRequest && !(await allowsServerFunctionRequest(request, csrf === true ? {} : csrf))) {
1018
- return forbiddenResponse();
1109
+ return finalizeTransportResponse(forbiddenResponse(), method);
1019
1110
  }
1020
1111
  const instance = request.headers.get(INSTANCE_HEADER);
1021
- const functionId = resolveFunctionId(request, url);
1022
1112
  if (!functionId) {
1023
1113
  const response = new Response(DEV ? "Server function not found" : null, {
1024
1114
  status: 404
1025
1115
  });
1026
- return protectsRequest ? withCSRFVary(response) : response;
1116
+ return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
1027
1117
  }
1028
1118
  let serverFunction;
1029
1119
  try {
@@ -1032,16 +1122,16 @@ async function handleServerFunctionRequest(request, options = {}) {
1032
1122
  const response = new Response(DEV ? `Unknown server function: ${functionId}` : null, {
1033
1123
  status: 404
1034
1124
  });
1035
- return protectsRequest ? withCSRFVary(response) : response;
1125
+ return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
1036
1126
  }
1037
- if (request.method === "GET" && METHODS.get(functionId) !== "GET") {
1127
+ if (method !== "POST" && !declaredRead) {
1038
1128
  const response = new Response(DEV ? `Method not allowed for server function: ${functionId}` : null, {
1039
1129
  status: 405,
1040
1130
  headers: {
1041
- Allow: "POST"
1131
+ Allow: METHODS.get(functionId) === "GET" ? "POST, GET, HEAD" : "POST"
1042
1132
  }
1043
1133
  });
1044
- return protectsRequest ? withCSRFVary(response) : response;
1134
+ return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
1045
1135
  }
1046
1136
  const event = options.createEvent ? options.createEvent(request) : {
1047
1137
  request,
@@ -1054,7 +1144,15 @@ async function handleServerFunctionRequest(request, options = {}) {
1054
1144
  const transformFlightResult = options.transformFlightResult !== undefined ? options.transformFlightResult : config.transformFlightResult;
1055
1145
  const handleNoJS = options.handleNoJS !== undefined ? options.handleNoJS : config.handleNoJS !== undefined ? config.handleNoJS : isFormPost(request) ? defaultNoJSHandler || (defaultNoJSHandler = createNoJSHandler()) : undefined;
1056
1146
  const collectsFlight = !!(flightHook && instance && request.headers.has(SINGLE_FLIGHT_HEADER));
1057
- const parsed = await parseArguments(request, url, instance, codec);
1147
+ let parsed;
1148
+ try {
1149
+ parsed = await parseArguments(request, url, instance, codec);
1150
+ } catch {
1151
+ const response = new Response(DEV ? "Malformed server function arguments" : null, {
1152
+ status: 400
1153
+ });
1154
+ return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
1155
+ }
1058
1156
  const flightContext = {
1059
1157
  id: functionId,
1060
1158
  args: parsed,
@@ -1201,7 +1299,34 @@ async function handleServerFunctionRequest(request, options = {}) {
1201
1299
  }
1202
1300
  };
1203
1301
  const response = commitEventResponse(await dispatch(), event);
1204
- return protectsRequest ? withCSRFVary(response) : response;
1302
+ return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
1303
+ }
1304
+ function finalizeTransportResponse(response, method) {
1305
+ const stripBody = method === "HEAD" && response.body !== null;
1306
+ if (stripBody || !response.headers.has("Cache-Control")) {
1307
+ try {
1308
+ if (!response.headers.has("Cache-Control")) {
1309
+ response.headers.set("Cache-Control", "no-store");
1310
+ }
1311
+ if (!stripBody) return response;
1312
+ response.body.cancel().catch(() => {});
1313
+ return new Response(null, {
1314
+ status: response.status,
1315
+ statusText: response.statusText,
1316
+ headers: response.headers
1317
+ });
1318
+ } catch {
1319
+ const headers = new Headers(response.headers);
1320
+ if (!headers.has("Cache-Control")) headers.set("Cache-Control", "no-store");
1321
+ if (stripBody) response.body.cancel().catch(() => {});
1322
+ return new Response(stripBody ? null : response.body, {
1323
+ status: response.status,
1324
+ statusText: response.statusText,
1325
+ headers
1326
+ });
1327
+ }
1328
+ }
1329
+ return response;
1205
1330
  }
1206
1331
 
1207
- export { ERROR_HEADER, FLASH_COOKIE, FUNCTION_HEADER, GENERIC_SERVER_ERROR_MESSAGE, GET, INSTANCE_HEADER, SINGLE_FLIGHT_HEADER, clearFlashCookie, configureServerFunctionsServer, createNoJSHandler, createServerReference, decodeErrorHeaderValue, decodeFlashCookie, decodeResponse, decodeResponsePayload, encodeErrorHeaderValue, encodeFlashCookie, foldSetCookies, getEventServerFunctionInvocation, getServerFunction, getServerFunctionInvocation, getServerFunctionMetadata, handleServerFunctionRequest, hasFlashCookie, isServerFunction, live, observeServerFunctionCalls, registerServerFunction, registerServerReference, sanitizeServerError, serializeResponseStream, setServerFunctionsDev, subscribeFlightData, withMeta };
1332
+ export { ERROR_HEADER, FLASH_COOKIE, GENERIC_SERVER_ERROR_MESSAGE, GET, INSTANCE_HEADER, SERVER_FUNCTION_INVOKE, SINGLE_FLIGHT_HEADER, clearFlashCookie, configureServerFunctionsServer, createNoJSHandler, createServerReference, decodeErrorHeaderValue, decodeFlashCookie, decodeResponse, decodeResponsePayload, encodeErrorHeaderValue, encodeFlashCookie, foldSetCookies, getEventServerFunctionInvocation, getServerFunction, getServerFunctionInvocation, getServerFunctionMetadata, handleServerFunctionRequest, hasFlashCookie, invoke, isServerFunction, live, observeServerFunctionCalls, parseServerFunctionUrl, registerServerFunction, registerServerReference, sanitizeServerError, serializeResponseStream, serverFunctionUrl, setServerFunctionsDev, subscribeFlightData, withMeta };
package/types/client.d.ts CHANGED
@@ -79,6 +79,8 @@ export interface RequestEvent {
79
79
  export type { CookieOptions } from "./cookies.js";
80
80
  export type { ServerFunction, ServerFunctionMetadata, ServerFunctionRPC } from "./server-functions/shared.js";
81
81
  export declare const waitAsset: (promise: Promise<unknown>) => void;
82
+ export declare let listDriver: ((parent: Node, listFn: any, marker?: Node, lateClassic?: () => void) => boolean) | undefined;
83
+ export declare function installListDriver(driver: typeof listDriver): void;
82
84
  export { DOMWithState, ChildProperties, DOMElements, SVGElements, MathMLElements, VoidElements, RawTextElements, Namespaces, DelegatedEvents } from "./constants.js";
83
85
  /** Client stub — hydration bootstrap is a server-only emit. */
84
86
  export declare function generateHydrationScript(_options?: {
@@ -145,10 +147,10 @@ export declare function spread<T>(node: Element, accessor: T, skipChildren?: Boo
145
147
  export declare function dynamicProperty(props: unknown, key: string): unknown;
146
148
  export declare function applyRef<T extends Element = Element>(r: ((element: NoInfer<T>) => void) | ((element: NoInfer<T>) => void)[], element: T): void;
147
149
  export declare function ref(fn: () => ((element: Element) => void) | ((element: Element) => void)[], element: Element): void;
148
- export declare function rowProof(fn: any): any; /** Compiler-emitted primitive; not for hand-written code. @internal */
150
+ /** Compiler-emitted primitive; not for hand-written code. @internal */
149
151
  export declare function scope<T extends () => any>(fn: T): T;
150
152
  export declare function installHydrationRuntime(): void;
151
- export declare function patchDriver(subject: any, body: any): void; /**
153
+ /**
152
154
  * Compiler-emitted primitive; not for hand-written code.
153
155
  * @internal
154
156
  */
package/types/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Component } from "solid-js";
2
2
  import type { JSX } from "./jsx.js";
3
3
  export * from "./client.js";
4
+ export { patchDriver, rowProof, driveList } from "./patch-driver.js";
4
5
  export * from "./server-mock.js";
5
6
  export * from "./response.js";
6
7
  export type { JSX } from "./jsx.js";
@@ -0,0 +1,3 @@
1
+ export declare function rowProof<T extends Function>(fn: T): T;
2
+ export declare const driveList: (parent: Node, listFn: any, marker?: Node, lateClassic?: () => void) => boolean;
3
+ export declare const patchDriver: (subject: any, body: any) => void;
@@ -16,7 +16,15 @@ export declare const ResponseEnvelope: {
16
16
  export declare function isResponseEnvelope(value: unknown): value is ResponseEnvelope;
17
17
  export declare const HREF: unique symbol;
18
18
  export interface Href {
19
- [HREF]: true;
19
+ /**
20
+ * The brand doubles as a channel: when the slot holds a string it is the
21
+ * value's *logical* path — the routable pathname before an integration's
22
+ * display rendering (eg. a hash router's `#` prefix). `redirect()` prefers
23
+ * it over coercion so Location headers carry routable paths; `toString()`
24
+ * remains the display href for the DOM. `true` brands a value whose
25
+ * string form is already logical.
26
+ */
27
+ [HREF]: true | string;
20
28
  toString(): string;
21
29
  }
22
30
  /** Whether `value` is an `Href`-branded URL-bearing value. */