@timber-js/app 0.2.0-alpha.94 → 0.2.0-alpha.95

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.
@@ -44,15 +44,24 @@ export declare function isActionRequest(req: Request): boolean;
44
44
  /**
45
45
  * Signal from handleFormAction to re-render the page with flash data instead of redirecting.
46
46
  *
47
- * Carries `setCookieHeaders` snapshotted from the action's request-context ALS
48
- * scope so the rerender pipeline (which establishes its own fresh cookie jar)
49
- * can append them to the final HTML response. Without this, cookies set inside
50
- * the action via `defineCookie().setCookie()` are silently dropped on the
51
- * no-JS validation-failure path. See LOCAL-740.
47
+ * Carries two cookie-related snapshots taken before the action's ALS scope
48
+ * exits, so the rerender pipeline (which establishes its own fresh cookie
49
+ * jar from the original request) doesn't lose the action's mutations:
50
+ *
51
+ * - `setCookieHeaders`: serialized Set-Cookie headers to append to the
52
+ * final HTML response. Without this, cookies set inside the action are
53
+ * silently dropped from the response. See TIM-836 (LOCAL-740).
54
+ *
55
+ * - `cookieHeader`: the post-action read-your-own-writes view of the
56
+ * cookie jar, serialized as a `Cookie:` request header. The rerender
57
+ * pipeline uses this to clone the request before re-rendering, so the
58
+ * page server components see the action's cookie writes immediately
59
+ * instead of the stale pre-action state. See TIM-837.
52
60
  */
53
61
  export interface FormRerender {
54
62
  rerender: FormFlashData;
55
63
  setCookieHeaders: string[];
64
+ cookieHeader: string;
56
65
  }
57
66
  /**
58
67
  * Handle a server action request.
@@ -1 +1 @@
1
- {"version":3,"file":"action-handler.d.ts","sourceRoot":"","sources":["../../src/server/action-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AASH,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAiB,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAOtE,OAAO,EAAwC,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAE/F,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAMrD,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACnC,0BAA0B;IAC1B,IAAI,EAAE,UAAU,CAAC;IACjB,kEAAkE;IAClE,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,gDAAgD;IAChD,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B;;;;OAIG;IACH,eAAe,CAAC,EAAE,qBAAqB,CAAC;CACzC;AAQD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAarD;AAID;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,aAAa,CAAC;IACxB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,QAAQ,GAAG,YAAY,GAAG,IAAI,CAAC,CAgEzC"}
1
+ {"version":3,"file":"action-handler.d.ts","sourceRoot":"","sources":["../../src/server/action-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AASH,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAiB,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAQtE,OAAO,EAAwC,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAE/F,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAMrD,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACnC,0BAA0B;IAC1B,IAAI,EAAE,UAAU,CAAC;IACjB,kEAAkE;IAClE,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,gDAAgD;IAChD,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B;;;;OAIG;IACH,eAAe,CAAC,EAAE,qBAAqB,CAAC;CACzC;AAQD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAarD;AAuBD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,aAAa,CAAC;IACxB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,QAAQ,GAAG,YAAY,GAAG,IAAI,CAAC,CAmEzC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AAuBA,OAAO,uCAAuC,CAAC;AA0C/C,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAoCtB;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,mBAAmB,EAAE,KAAK,IAAI,GACtF,IAAI,CAEN;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAE5E;AA4jBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAInE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;8BAnTrC,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AAqThD,wBAAiE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AAuBA,OAAO,uCAAuC,CAAC;AA0C/C,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAoCtB;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,mBAAmB,EAAE,KAAK,IAAI,GACtF,IAAI,CAEN;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAE5E;AAklBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAInE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;8BAzUrC,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AA2UhD,wBAAiE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.94",
3
+ "version": "0.2.0-alpha.95",
4
4
  "description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -26,6 +26,7 @@ import {
26
26
  runWithRequestContext,
27
27
  setMutableCookieContext,
28
28
  getSetCookieHeaders,
29
+ getCookiesForSsr,
29
30
  } from './request-context.js';
30
31
  import { handleActionError } from './action-client.js';
31
32
  import { enforceBodyLimits, enforceFieldLimit, type BodyLimitsConfig } from './body-limits.js';
@@ -89,18 +90,46 @@ export function isActionRequest(req: Request): boolean {
89
90
 
90
91
  // ─── Handler ──────────────────────────────────────────────────────────────
91
92
 
93
+ /**
94
+ * Serialize a `Map<string, string>` of cookie name → value as a `Cookie:`
95
+ * request header value. Format: `name1=value1; name2=value2`. Matches the
96
+ * format `parseCookieHeader` in `request-context.ts` reads with, so the
97
+ * rerender pipeline can parse it back into the same RYW state.
98
+ *
99
+ * Used to thread the post-action cookie state from the action's ALS scope
100
+ * into the rerender pipeline's fresh ALS scope. Cookies marked for deletion
101
+ * are already absent from `getCookiesForSsr()`'s map, so they naturally drop
102
+ * out of the synthesized header. See TIM-837.
103
+ */
104
+ function serializeCookieMapAsHeader(cookies: Map<string, string>): string {
105
+ const parts: string[] = [];
106
+ for (const [name, value] of cookies) {
107
+ parts.push(`${name}=${value}`);
108
+ }
109
+ return parts.join('; ');
110
+ }
111
+
92
112
  /**
93
113
  * Signal from handleFormAction to re-render the page with flash data instead of redirecting.
94
114
  *
95
- * Carries `setCookieHeaders` snapshotted from the action's request-context ALS
96
- * scope so the rerender pipeline (which establishes its own fresh cookie jar)
97
- * can append them to the final HTML response. Without this, cookies set inside
98
- * the action via `defineCookie().setCookie()` are silently dropped on the
99
- * no-JS validation-failure path. See LOCAL-740.
115
+ * Carries two cookie-related snapshots taken before the action's ALS scope
116
+ * exits, so the rerender pipeline (which establishes its own fresh cookie
117
+ * jar from the original request) doesn't lose the action's mutations:
118
+ *
119
+ * - `setCookieHeaders`: serialized Set-Cookie headers to append to the
120
+ * final HTML response. Without this, cookies set inside the action are
121
+ * silently dropped from the response. See TIM-836 (LOCAL-740).
122
+ *
123
+ * - `cookieHeader`: the post-action read-your-own-writes view of the
124
+ * cookie jar, serialized as a `Cookie:` request header. The rerender
125
+ * pipeline uses this to clone the request before re-rendering, so the
126
+ * page server components see the action's cookie writes immediately
127
+ * instead of the stale pre-action state. See TIM-837.
100
128
  */
101
129
  export interface FormRerender {
102
130
  rerender: FormFlashData;
103
131
  setCookieHeaders: string[];
132
+ cookieHeader: string;
104
133
  }
105
134
 
106
135
  /**
@@ -173,6 +202,9 @@ export async function handleActionRequest(
173
202
  }
174
203
  } else if (result && 'rerender' in result) {
175
204
  result.setCookieHeaders = getSetCookieHeaders();
205
+ // Snapshot the post-action RYW cookie state so the rerender pipeline
206
+ // can re-parse it into a fresh ALS scope. See TIM-837.
207
+ result.cookieHeader = serializeCookieMapAsHeader(getCookiesForSsr());
176
208
  }
177
209
  return result;
178
210
  });
@@ -359,6 +391,7 @@ async function handleFormAction(
359
391
  },
360
392
  // Filled in by handleActionRequest before the ALS scope exits.
361
393
  setCookieHeaders: [],
394
+ cookieHeader: '',
362
395
  };
363
396
  }
364
397
 
@@ -388,6 +421,7 @@ async function handleFormAction(
388
421
  );
389
422
  }
390
423
 
391
- // setCookieHeaders is filled in by handleActionRequest before the ALS scope exits.
392
- return { rerender: actionResult, setCookieHeaders: [] };
424
+ // setCookieHeaders + cookieHeader are filled in by handleActionRequest
425
+ // before the ALS scope exits.
426
+ return { rerender: actionResult, setCookieHeaders: [], cookieHeader: '' };
393
427
  }
@@ -446,11 +446,33 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
446
446
  // Re-render the page with the action result as flash data.
447
447
  // Server components read it via getFormFlash() and pass it to
448
448
  // client form components as the initial useActionState value.
449
- const response = await runWithFormFlash(formRerender.rerender, () => pipeline(req));
449
+ //
450
+ // Build a synthetic GET request for the rerender pipeline:
451
+ // - Same URL (so route matching lands on the same page)
452
+ // - Cookie header replaced with the post-action RYW snapshot
453
+ // so server components see the action's writes (TIM-837)
454
+ // - Method GET because the rerender is conceptually a page
455
+ // render, not a re-POST. The pipeline doesn't branch on
456
+ // method for page rendering, and constructing a POST without
457
+ // a body is awkward across Request implementations.
458
+ const rerenderHeaders = new Headers(req.headers);
459
+ if (formRerender.cookieHeader) {
460
+ rerenderHeaders.set('cookie', formRerender.cookieHeader);
461
+ } else {
462
+ rerenderHeaders.delete('cookie');
463
+ }
464
+ const rerenderReq = new Request(req.url, {
465
+ method: 'GET',
466
+ headers: rerenderHeaders,
467
+ });
468
+ const response = await runWithFormFlash(formRerender.rerender, () =>
469
+ pipeline(rerenderReq)
470
+ );
450
471
  // Apply Set-Cookie headers snapshotted from the action's ALS scope.
451
472
  // The pipeline above runs in its own request context with a fresh
452
473
  // cookie jar, so cookies set inside the action would otherwise be
453
- // silently dropped on the no-JS rerender path. See LOCAL-740.
474
+ // silently dropped on the no-JS rerender path. See TIM-836
475
+ // (LOCAL-740).
454
476
  for (const value of formRerender.setCookieHeaders) {
455
477
  response.headers.append('Set-Cookie', value);
456
478
  }