@solidjs/router 2.0.0-next.33 → 2.0.0-next.34

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.
@@ -52,13 +52,40 @@ export function handleFormAction(evt, router, actionBase) {
52
52
  // stays a no-JS fallback.
53
53
  // Client-only actions (`https://action/`) are their module's JS by
54
54
  // definition, so a miss there falls through to native submission.
55
- const handler = actions.get(actionRef) || (serverAction && createServerFormAction(actionRef));
55
+ const handler = findAction(actionRef) || (serverAction && createServerFormAction(actionRef));
56
56
  if (handler) {
57
57
  evt.preventDefault();
58
58
  const data = new FormData(form, evt.submitter);
59
59
  handler.call({ r: router, f: form }, form.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
60
60
  }
61
61
  }
62
+ /**
63
+ * Looks a rendered action url up in the registry. A server-rendered
64
+ * `.with()` url is only registered if this client made the same binding
65
+ * itself; otherwise its base action (the url without `?args`) is rebound to
66
+ * the rendered arguments, so the submission runs through that action's
67
+ * submit and settled hooks and is recorded under its base.
68
+ */
69
+ function findAction(url) {
70
+ const handler = actions.get(url);
71
+ if (handler)
72
+ return handler;
73
+ const query = url.indexOf("?");
74
+ if (query < 0)
75
+ return undefined;
76
+ const base = actions.get(url.slice(0, query));
77
+ const args = new URLSearchParams(url.slice(query)).get("args");
78
+ if (!base || args === null)
79
+ return undefined;
80
+ let bound;
81
+ try {
82
+ bound = JSON.parse(args);
83
+ }
84
+ catch {
85
+ return undefined;
86
+ }
87
+ return Array.isArray(bound) ? base.with(...bound) : undefined;
88
+ }
62
89
  /**
63
90
  * Synthesizes a router action for a server-rendered action url. The url
64
91
  * carries everything an invocation needs — the function id in the path
@@ -90,7 +117,7 @@ function createServerFormAction(url) {
90
117
  * the enctype conversion and the generic invocation happen here.
91
118
  */
92
119
  export function submitServerForm(router, url, form, data) {
93
- const handler = actions.get(url) || createServerFormAction(url);
120
+ const handler = findAction(url) || createServerFormAction(url);
94
121
  // not an address (`<endpoint>/<id>`) — not the server function convention;
95
122
  // nothing can run it, resubmit natively (submit() bypasses the delegated
96
123
  // handler)
@@ -244,9 +271,12 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
244
271
  return url;
245
272
  };
246
273
  fn.with = function (...args) {
274
+ const bound = [...boundArgs, ...args];
247
275
  const uri = new URL(url, mockBase);
248
- uri.searchParams.set("args", hashKey(args));
249
- return toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
276
+ // the server prepends `args` to the submitted arguments, so it must carry
277
+ // the whole binding, not just this call's part of a chain
278
+ uri.searchParams.set("args", hashKey(bound));
279
+ return toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, bound, base, submitHooks, settledHooks);
250
280
  };
251
281
  fn.onSubmit = function (hook) {
252
282
  const id = Symbol("actionOnSubmitHook");
package/dist/index.js CHANGED
@@ -1190,6 +1190,17 @@ let flashDecoder;
1190
1190
  function provideFlashDecoder(decoder) {
1191
1191
  flashDecoder || (flashDecoder = decoder);
1192
1192
  }
1193
+
1194
+ /**
1195
+ * A request's flash cookie and its decode, shared by every router created
1196
+ * for that request. A server render can re-create the router — a suspension
1197
+ * under `<Errored>` retries from the nearest hole above it, which may sit
1198
+ * above the router — so state kept on one router instance would restart the
1199
+ * decode with every re-creation and the seeding read would never settle.
1200
+ * Keyed by the request event, so it lives exactly as long as the request.
1201
+ */
1202
+
1203
+ const flashStates = new WeakMap();
1193
1204
  let preloadIntent;
1194
1205
  function getIntent() {
1195
1206
  return preloadIntent || useOptionalContext(RouterContextObj)?.intent?.();
@@ -1228,63 +1239,68 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1228
1239
  // unread outcome must not haunt a later request's render. Only detection
1229
1240
  // and clearing happen here (the runtime's isomorphic half); the raw header
1230
1241
  // is stashed and decoding waits for the action-provided codec, read when
1231
- // the lazily allocated submissions signal below first initializes.
1232
- let flashCookieHeader;
1242
+ // the lazily allocated submissions signal below first initializes. Both
1243
+ // happen once per request: a router re-created for the same request finds
1244
+ // the request's flash state (see `flashStates`) and neither clears again
1245
+ // nor decodes again.
1246
+ let flash;
1233
1247
  if (isServer) {
1234
1248
  const e = getRequestEvent();
1235
1249
  if (e && !(e.router && e.router.submission)) {
1236
- const cookieHeader = e.request.headers.get("cookie");
1237
- if (hasFlashCookie(cookieHeader)) {
1238
- flashCookieHeader = cookieHeader;
1239
- // one-shot: clear it even when unreadable so it can't haunt later renders
1240
- if (e.response && e.response.headers) e.response.headers.append("Set-Cookie", clearFlashCookie());
1250
+ flash = flashStates.get(e);
1251
+ if (!flash) {
1252
+ const cookieHeader = e.request.headers.get("cookie");
1253
+ if (hasFlashCookie(cookieHeader)) {
1254
+ flashStates.set(e, flash = {
1255
+ cookieHeader
1256
+ });
1257
+ // one-shot: clear it even when unreadable so it can't haunt later renders
1258
+ if (e.response && e.response.headers) e.response.headers.append("Set-Cookie", clearFlashCookie());
1259
+ }
1241
1260
  }
1242
1261
  }
1243
1262
  }
1244
1263
 
1245
- // The decode, at most once per request: the decoder may answer with a
1246
- // Promise (the cookie is encrypted; the runtime's decodeFlashCookie is
1247
- // async), and this cache is what keeps the parked read's rerun from
1248
- // restarting it — resumption finds the settled outcome and just reads it.
1249
- // A decoder that rejects reads as "no flash", matching the runtime's own
1250
- // malformed-cookie semantics.
1251
- let flashDecode;
1252
-
1253
1264
  // The seeding read, as a memo: NotReadyError must surface from a reactive
1254
1265
  // node the graph can park and retry — never from router setup, which no
1255
- // boundary guards — and the memo bounds the recompute to this function;
1256
- // a parked reader resumes into the settled cache above, never a second
1257
- // decode. Created only when a flash cookie actually arrived (server-only
1258
- // by construction: flashCookieHeader is only ever set there), and
1266
+ // boundary guards — and the memo bounds the recompute to this function.
1267
+ // The decode runs at most once per request: the decoder may answer with a
1268
+ // Promise (the cookie is encrypted; the runtime's decodeFlashCookie is
1269
+ // async), and the request's cached state is what keeps a parked read's
1270
+ // rerun — or a re-created router's first read — from restarting it. A
1271
+ // decoder that rejects reads as "no flash", matching the runtime's own
1272
+ // malformed-cookie semantics. Created only when a flash cookie actually
1273
+ // arrived (server-only by construction: `flash` is only ever set there), and
1259
1274
  // - `lazy`: server memos compute eagerly by default — deferred to first
1260
1275
  // read, a request whose submissions are never read never decodes;
1261
1276
  // - `transparent`: the memo exists on the server only, so its owner
1262
1277
  // must not consume a hydration-id slot — the client, which seeds
1263
1278
  // submissions as [] without ever creating this memo, would miss it
1264
1279
  // and every sibling id would shift.
1265
- const flashSubmission = flashCookieHeader !== undefined ? createMemo(() => {
1280
+ const flashSubmission = flash !== undefined ? createMemo(() => {
1281
+ const state = flash;
1266
1282
  if (!flashDecoder) return undefined;
1267
- if (!flashDecode) {
1268
- const promise = flashDecoder(flashCookieHeader).then(value => {
1269
- flashDecode = {
1283
+ if (!state.decode) {
1284
+ const promise = flashDecoder(state.cookieHeader).then(value => {
1285
+ state.decode = {
1270
1286
  done: true,
1271
1287
  value
1272
1288
  };
1273
1289
  }, () => {
1274
- flashDecode = {
1290
+ state.decode = {
1275
1291
  done: true,
1276
1292
  value: undefined
1277
1293
  };
1278
1294
  });
1279
- flashDecode = {
1295
+ state.decode = {
1280
1296
  done: false,
1281
1297
  promise
1282
1298
  };
1283
1299
  }
1284
1300
  // SSR carries the Promise through NotReadyError so the parked
1285
1301
  // reader can resume, exactly like the lazy matches above.
1286
- if (!flashDecode.done) throw new NotReadyError(flashDecode.promise);
1287
- return flashDecode.value;
1302
+ if (!state.decode.done) throw new NotReadyError(state.decode.promise);
1303
+ return state.decode.value;
1288
1304
  }, {
1289
1305
  lazy: true,
1290
1306
  transparent: true
@@ -3086,7 +3102,7 @@ function handleFormAction(evt, router, actionBase) {
3086
3102
  // stays a no-JS fallback.
3087
3103
  // Client-only actions (`https://action/`) are their module's JS by
3088
3104
  // definition, so a miss there falls through to native submission.
3089
- const handler = actions.get(actionRef) || serverAction && createServerFormAction(actionRef);
3105
+ const handler = findAction(actionRef) || serverAction && createServerFormAction(actionRef);
3090
3106
  if (handler) {
3091
3107
  evt.preventDefault();
3092
3108
  const data = new FormData(form, evt.submitter);
@@ -3097,6 +3113,30 @@ function handleFormAction(evt, router, actionBase) {
3097
3113
  }
3098
3114
  }
3099
3115
 
3116
+ /**
3117
+ * Looks a rendered action url up in the registry. A server-rendered
3118
+ * `.with()` url is only registered if this client made the same binding
3119
+ * itself; otherwise its base action (the url without `?args`) is rebound to
3120
+ * the rendered arguments, so the submission runs through that action's
3121
+ * submit and settled hooks and is recorded under its base.
3122
+ */
3123
+ function findAction(url) {
3124
+ const handler = actions.get(url);
3125
+ if (handler) return handler;
3126
+ const query = url.indexOf("?");
3127
+ if (query < 0) return undefined;
3128
+ const base = actions.get(url.slice(0, query));
3129
+ const args = new URLSearchParams(url.slice(query)).get("args");
3130
+ if (!base || args === null) return undefined;
3131
+ let bound;
3132
+ try {
3133
+ bound = JSON.parse(args);
3134
+ } catch {
3135
+ return undefined;
3136
+ }
3137
+ return Array.isArray(bound) ? base.with(...bound) : undefined;
3138
+ }
3139
+
3100
3140
  /**
3101
3141
  * Synthesizes a router action for a server-rendered action url. The url
3102
3142
  * carries everything an invocation needs — the function id in the path
@@ -3130,7 +3170,7 @@ function createServerFormAction(url) {
3130
3170
  * the enctype conversion and the generic invocation happen here.
3131
3171
  */
3132
3172
  function submitServerForm(router, url, form, data) {
3133
- const handler = actions.get(url) || createServerFormAction(url);
3173
+ const handler = findAction(url) || createServerFormAction(url);
3134
3174
  // not an address (`<endpoint>/<id>`) — not the server function convention;
3135
3175
  // nothing can run it, resubmit natively (submit() bypasses the delegated
3136
3176
  // handler)
@@ -3282,9 +3322,12 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
3282
3322
  return url;
3283
3323
  };
3284
3324
  fn.with = function (...args) {
3325
+ const bound = [...boundArgs, ...args];
3285
3326
  const uri = new URL(url, mockBase);
3286
- uri.searchParams.set("args", hashKey(args));
3287
- return toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
3327
+ // the server prepends `args` to the submitted arguments, so it must carry
3328
+ // the whole binding, not just this call's part of a chain
3329
+ uri.searchParams.set("args", hashKey(bound));
3330
+ return toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, bound, base, submitHooks, settledHooks);
3288
3331
  };
3289
3332
  fn.onSubmit = function (hook) {
3290
3333
  const id = Symbol("actionOnSubmitHook");
package/dist/routing.js CHANGED
@@ -556,6 +556,7 @@ let flashDecoder;
556
556
  export function provideFlashDecoder(decoder) {
557
557
  flashDecoder || (flashDecoder = decoder);
558
558
  }
559
+ const flashStates = new WeakMap();
559
560
  let preloadIntent;
560
561
  export function getIntent() {
561
562
  return preloadIntent || useOptionalContext(RouterContextObj)?.intent?.();
@@ -592,56 +593,60 @@ export function createRouterContext(integration, branches, getContext, options =
592
593
  // unread outcome must not haunt a later request's render. Only detection
593
594
  // and clearing happen here (the runtime's isomorphic half); the raw header
594
595
  // is stashed and decoding waits for the action-provided codec, read when
595
- // the lazily allocated submissions signal below first initializes.
596
- let flashCookieHeader;
596
+ // the lazily allocated submissions signal below first initializes. Both
597
+ // happen once per request: a router re-created for the same request finds
598
+ // the request's flash state (see `flashStates`) and neither clears again
599
+ // nor decodes again.
600
+ let flash;
597
601
  if (isServer) {
598
602
  const e = getRequestEvent();
599
603
  if (e && !(e.router && e.router.submission)) {
600
- const cookieHeader = e.request.headers.get("cookie");
601
- if (hasFlashCookie(cookieHeader)) {
602
- flashCookieHeader = cookieHeader;
603
- // one-shot: clear it even when unreadable so it can't haunt later renders
604
- if (e.response && e.response.headers)
605
- e.response.headers.append("Set-Cookie", clearFlashCookie());
604
+ flash = flashStates.get(e);
605
+ if (!flash) {
606
+ const cookieHeader = e.request.headers.get("cookie");
607
+ if (hasFlashCookie(cookieHeader)) {
608
+ flashStates.set(e, (flash = { cookieHeader }));
609
+ // one-shot: clear it even when unreadable so it can't haunt later renders
610
+ if (e.response && e.response.headers)
611
+ e.response.headers.append("Set-Cookie", clearFlashCookie());
612
+ }
606
613
  }
607
614
  }
608
615
  }
609
- // The decode, at most once per request: the decoder may answer with a
610
- // Promise (the cookie is encrypted; the runtime's decodeFlashCookie is
611
- // async), and this cache is what keeps the parked read's rerun from
612
- // restarting it — resumption finds the settled outcome and just reads it.
613
- // A decoder that rejects reads as "no flash", matching the runtime's own
614
- // malformed-cookie semantics.
615
- let flashDecode;
616
616
  // The seeding read, as a memo: NotReadyError must surface from a reactive
617
617
  // node the graph can park and retry — never from router setup, which no
618
- // boundary guards — and the memo bounds the recompute to this function;
619
- // a parked reader resumes into the settled cache above, never a second
620
- // decode. Created only when a flash cookie actually arrived (server-only
621
- // by construction: flashCookieHeader is only ever set there), and
618
+ // boundary guards — and the memo bounds the recompute to this function.
619
+ // The decode runs at most once per request: the decoder may answer with a
620
+ // Promise (the cookie is encrypted; the runtime's decodeFlashCookie is
621
+ // async), and the request's cached state is what keeps a parked read's
622
+ // rerun — or a re-created router's first read — from restarting it. A
623
+ // decoder that rejects reads as "no flash", matching the runtime's own
624
+ // malformed-cookie semantics. Created only when a flash cookie actually
625
+ // arrived (server-only by construction: `flash` is only ever set there), and
622
626
  // - `lazy`: server memos compute eagerly by default — deferred to first
623
627
  // read, a request whose submissions are never read never decodes;
624
628
  // - `transparent`: the memo exists on the server only, so its owner
625
629
  // must not consume a hydration-id slot — the client, which seeds
626
630
  // submissions as [] without ever creating this memo, would miss it
627
631
  // and every sibling id would shift.
628
- const flashSubmission = flashCookieHeader !== undefined
632
+ const flashSubmission = flash !== undefined
629
633
  ? createMemo(() => {
634
+ const state = flash;
630
635
  if (!flashDecoder)
631
636
  return undefined;
632
- if (!flashDecode) {
633
- const promise = flashDecoder(flashCookieHeader).then(value => {
634
- flashDecode = { done: true, value };
637
+ if (!state.decode) {
638
+ const promise = flashDecoder(state.cookieHeader).then(value => {
639
+ state.decode = { done: true, value };
635
640
  }, () => {
636
- flashDecode = { done: true, value: undefined };
641
+ state.decode = { done: true, value: undefined };
637
642
  });
638
- flashDecode = { done: false, promise };
643
+ state.decode = { done: false, promise };
639
644
  }
640
645
  // SSR carries the Promise through NotReadyError so the parked
641
646
  // reader can resume, exactly like the lazy matches above.
642
- if (!flashDecode.done)
643
- throw new NotReadyError(flashDecode.promise);
644
- return flashDecode.value;
647
+ if (!state.decode.done)
648
+ throw new NotReadyError(state.decode.promise);
649
+ return state.decode.value;
645
650
  }, { lazy: true, transparent: true })
646
651
  : undefined;
647
652
  let submissions;
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "2.0.0-next.33",
9
+ "version": "2.0.0-next.34",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",