@timber-js/app 0.1.11 → 0.1.12
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/dist/_chunks/ssr-data-BgSwMbN9.js +38 -0
- package/dist/_chunks/ssr-data-BgSwMbN9.js.map +1 -0
- package/dist/_chunks/{use-cookie-HcvNlW4L.js → use-cookie-D2cZu0jK.js} +3 -37
- package/dist/_chunks/use-cookie-D2cZu0jK.js.map +1 -0
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +8 -0
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/index.js +3 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/ssr-data.d.ts +9 -0
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -1
- package/dist/index.js +5 -11
- package/dist/index.js.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/server/index.js +9 -10
- package/dist/server/index.js.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +7 -0
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +10 -0
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/error-boundary.tsx +22 -0
- package/src/client/ssr-data.ts +7 -0
- package/src/plugins/shims.ts +8 -14
- package/src/server/rsc-entry/index.ts +10 -3
- package/src/server/ssr-entry.ts +8 -0
- package/src/server/tree-builder.ts +35 -10
- package/dist/_chunks/use-cookie-HcvNlW4L.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA2EA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAE/F;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA2EA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAE/F;AAqrBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;8BA/jBpD,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AAikBhD,wBAAiE"}
|
|
@@ -46,6 +46,13 @@ export interface NavContext {
|
|
|
46
46
|
/** Request cookies as name→value pairs. Used by useCookie() during SSR
|
|
47
47
|
* to return correct cookie values before hydration. */
|
|
48
48
|
cookies?: Map<string, string>;
|
|
49
|
+
/**
|
|
50
|
+
* Mutable flag: set by TimberErrorBoundary during SSR when it catches
|
|
51
|
+
* a DenySignal (via digest). This tells the RSC entry that the denial
|
|
52
|
+
* was contained by a slot error boundary and should NOT be promoted
|
|
53
|
+
* to a page-level deny. See LOCAL-298.
|
|
54
|
+
*/
|
|
55
|
+
_denyHandledByBoundary?: boolean;
|
|
49
56
|
}
|
|
50
57
|
/**
|
|
51
58
|
* Handle SSR: decode an RSC stream and render it to hydration-ready HTML.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA6BH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACvC;;;0DAGsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;4DACwD;IACxD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA6BH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACvC;;;0DAGsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;4DACwD;IACxD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CA4EnB;AAED,eAAe,SAAS,CAAC"}
|
|
@@ -40,6 +40,16 @@ export interface TreeBuilderConfig {
|
|
|
40
40
|
loadModule: ModuleLoader;
|
|
41
41
|
/** React.createElement or equivalent. */
|
|
42
42
|
createElement: CreateElement;
|
|
43
|
+
/**
|
|
44
|
+
* Error boundary component for wrapping segments.
|
|
45
|
+
*
|
|
46
|
+
* This is injected by the caller rather than imported directly to avoid
|
|
47
|
+
* pulling 'use client' code into the server barrel (@timber-js/app/server).
|
|
48
|
+
* In the RSC environment, the RSC plugin transforms this import to a
|
|
49
|
+
* client reference proxy — the caller handles the import so the server
|
|
50
|
+
* barrel stays free of client dependencies.
|
|
51
|
+
*/
|
|
52
|
+
errorBoundaryComponent?: unknown;
|
|
43
53
|
}
|
|
44
54
|
/**
|
|
45
55
|
* Framework-injected access gate component.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree-builder.d.ts","sourceRoot":"","sources":["../../src/server/tree-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"tree-builder.d.ts","sourceRoot":"","sources":["../../src/server/tree-builder.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { Component, createElement, type ReactNode } from 'react';
|
|
22
|
+
import { getSsrData } from './ssr-data.js';
|
|
22
23
|
|
|
23
24
|
// ─── Page Unload Detection ───────────────────────────────────────────────────
|
|
24
25
|
// Track whether the page is being unloaded (user refreshed or navigated away).
|
|
@@ -97,6 +98,27 @@ export class TimberErrorBoundary extends Component<
|
|
|
97
98
|
if (_isUnloading) {
|
|
98
99
|
return { hasError: false, error: null };
|
|
99
100
|
}
|
|
101
|
+
|
|
102
|
+
// Report DenySignal handling to prevent page-level promotion.
|
|
103
|
+
// When a slot's error boundary catches a DenySignal, the RSC onError
|
|
104
|
+
// callback has already tracked it globally. Setting this flag tells
|
|
105
|
+
// the RSC entry not to promote the denial to page-level (which would
|
|
106
|
+
// replace the entire SSR response). See LOCAL-298.
|
|
107
|
+
const digest = (error as { digest?: string }).digest;
|
|
108
|
+
if (typeof digest === 'string') {
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(digest);
|
|
111
|
+
if (parsed?.type === 'deny') {
|
|
112
|
+
const ssrData = getSsrData();
|
|
113
|
+
if (ssrData?._navContext) {
|
|
114
|
+
ssrData._navContext._denyHandledByBoundary = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// Not a JSON digest — ignore
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
100
122
|
return { hasError: true, error };
|
|
101
123
|
}
|
|
102
124
|
|
package/src/client/ssr-data.ts
CHANGED
|
@@ -30,6 +30,13 @@ export interface SsrData {
|
|
|
30
30
|
cookies: Map<string, string>;
|
|
31
31
|
/** The request's route params (e.g. { id: '123' }) */
|
|
32
32
|
params: Record<string, string | string[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Mutable reference to NavContext for error boundary → RSC communication.
|
|
35
|
+
* When TimberErrorBoundary catches a DenySignal, it sets
|
|
36
|
+
* `_navContext._denyHandledByBoundary = true` to prevent the RSC entry
|
|
37
|
+
* from promoting the denial to page-level. See LOCAL-298.
|
|
38
|
+
*/
|
|
39
|
+
_navContext?: { _denyHandledByBoundary?: boolean };
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
// ─── ALS-Backed Provider ─────────────────────────────────────────
|
package/src/plugins/shims.ts
CHANGED
|
@@ -173,21 +173,15 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
173
173
|
return 'export {};';
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
176
|
+
// Error module for @timber-js/app/server in client environment.
|
|
177
|
+
// Server modules must never be bundled into the browser — if this
|
|
178
|
+
// module is reached, there is a broken import chain that needs fixing.
|
|
179
179
|
if (id === '\0timber:server-empty') {
|
|
180
|
-
return `
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
export const redirect = stub("redirect");
|
|
186
|
-
export const permanentRedirect = stub("permanentRedirect");
|
|
187
|
-
export const deny = stub("deny");
|
|
188
|
-
export const searchParams = stub("searchParams");
|
|
189
|
-
export const RedirectType = { push: "push", replace: "replace" };
|
|
190
|
-
`;
|
|
180
|
+
return `throw new Error(
|
|
181
|
+
"[timber] @timber-js/app/server was imported from client code. " +
|
|
182
|
+
"Server modules (headers, cookies, redirect, deny, etc.) cannot be used in client components. " +
|
|
183
|
+
"If you need these APIs, move the import to a server component or middleware."
|
|
184
|
+
);`;
|
|
191
185
|
}
|
|
192
186
|
},
|
|
193
187
|
};
|
|
@@ -661,10 +661,17 @@ async function renderRoute(
|
|
|
661
661
|
// Helper: check if render-phase signals were captured and return the
|
|
662
662
|
// appropriate HTTP response. Used after both successful SSR (signal
|
|
663
663
|
// promotion from Suspense) and failed SSR (signal outside Suspense).
|
|
664
|
-
|
|
664
|
+
//
|
|
665
|
+
// When `skipHandledDeny` is true (SSR success path), skip DenySignal
|
|
666
|
+
// promotion if the denial was already handled by a TimberErrorBoundary
|
|
667
|
+
// (e.g., slot error boundary). The boundary sets navContext._denyHandledByBoundary
|
|
668
|
+
// during SSR rendering. See LOCAL-298.
|
|
669
|
+
function checkCapturedSignals(
|
|
670
|
+
skipHandledDeny = false
|
|
671
|
+
): Response | Promise<Response> | null {
|
|
665
672
|
const sig = redirectSignal as RedirectSignal | null;
|
|
666
673
|
if (sig) return buildRedirectResponse(_req, sig, responseHeaders);
|
|
667
|
-
if (denySignal) {
|
|
674
|
+
if (denySignal && !(skipHandledDeny && navContext._denyHandledByBoundary)) {
|
|
668
675
|
return renderDenyPage(
|
|
669
676
|
denySignal,
|
|
670
677
|
segments,
|
|
@@ -703,7 +710,7 @@ async function renderRoute(
|
|
|
703
710
|
// See design/05-streaming.md §"deferSuspenseFor and the Hold Window"
|
|
704
711
|
await new Promise<void>((r) => setTimeout(r, 0));
|
|
705
712
|
|
|
706
|
-
const promoted = checkCapturedSignals();
|
|
713
|
+
const promoted = checkCapturedSignals(/* skipHandledDeny */ true);
|
|
707
714
|
if (promoted) {
|
|
708
715
|
ssrResponse.body?.cancel();
|
|
709
716
|
return promoted;
|
package/src/server/ssr-entry.ts
CHANGED
|
@@ -74,6 +74,13 @@ export interface NavContext {
|
|
|
74
74
|
/** Request cookies as name→value pairs. Used by useCookie() during SSR
|
|
75
75
|
* to return correct cookie values before hydration. */
|
|
76
76
|
cookies?: Map<string, string>;
|
|
77
|
+
/**
|
|
78
|
+
* Mutable flag: set by TimberErrorBoundary during SSR when it catches
|
|
79
|
+
* a DenySignal (via digest). This tells the RSC entry that the denial
|
|
80
|
+
* was contained by a slot error boundary and should NOT be promoted
|
|
81
|
+
* to a page-level deny. See LOCAL-298.
|
|
82
|
+
*/
|
|
83
|
+
_denyHandledByBoundary?: boolean;
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
/**
|
|
@@ -111,6 +118,7 @@ export async function handleSsr(
|
|
|
111
118
|
searchParams: navContext.searchParams,
|
|
112
119
|
cookies: navContext.cookies ?? new Map(),
|
|
113
120
|
params: navContext.params,
|
|
121
|
+
_navContext: navContext,
|
|
114
122
|
};
|
|
115
123
|
|
|
116
124
|
// Run the entire render inside the SSR data ALS scope.
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { SegmentNode, RouteFile } from '#/routing/types.js';
|
|
14
|
-
import { TimberErrorBoundary } from '#/client/error-boundary.js';
|
|
15
14
|
|
|
16
15
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
17
16
|
|
|
@@ -55,6 +54,16 @@ export interface TreeBuilderConfig {
|
|
|
55
54
|
loadModule: ModuleLoader;
|
|
56
55
|
/** React.createElement or equivalent. */
|
|
57
56
|
createElement: CreateElement;
|
|
57
|
+
/**
|
|
58
|
+
* Error boundary component for wrapping segments.
|
|
59
|
+
*
|
|
60
|
+
* This is injected by the caller rather than imported directly to avoid
|
|
61
|
+
* pulling 'use client' code into the server barrel (@timber-js/app/server).
|
|
62
|
+
* In the RSC environment, the RSC plugin transforms this import to a
|
|
63
|
+
* client reference proxy — the caller handles the import so the server
|
|
64
|
+
* barrel stays free of client dependencies.
|
|
65
|
+
*/
|
|
66
|
+
errorBoundaryComponent?: unknown;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
// ─── Component wrappers ──────────────────────────────────────────────────────
|
|
@@ -134,7 +143,8 @@ export interface TreeBuildResult {
|
|
|
134
143
|
* Parallel slots are resolved at each layout level and composed as named props.
|
|
135
144
|
*/
|
|
136
145
|
export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeBuildResult> {
|
|
137
|
-
const { segments, params, searchParams, loadModule, createElement } =
|
|
146
|
+
const { segments, params, searchParams, loadModule, createElement, errorBoundaryComponent } =
|
|
147
|
+
config;
|
|
138
148
|
|
|
139
149
|
if (segments.length === 0) {
|
|
140
150
|
throw new Error('[timber] buildElementTree: empty segment chain');
|
|
@@ -166,7 +176,13 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
|
|
|
166
176
|
const segment = segments[i];
|
|
167
177
|
|
|
168
178
|
// Wrap in error boundaries (status-code files + error.tsx)
|
|
169
|
-
element = await wrapWithErrorBoundaries(
|
|
179
|
+
element = await wrapWithErrorBoundaries(
|
|
180
|
+
segment,
|
|
181
|
+
element,
|
|
182
|
+
loadModule,
|
|
183
|
+
createElement,
|
|
184
|
+
errorBoundaryComponent
|
|
185
|
+
);
|
|
170
186
|
|
|
171
187
|
// Wrap in AccessGate if segment has access.ts
|
|
172
188
|
if (segment.access) {
|
|
@@ -198,7 +214,8 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
|
|
|
198
214
|
params,
|
|
199
215
|
searchParams,
|
|
200
216
|
loadModule,
|
|
201
|
-
createElement
|
|
217
|
+
createElement,
|
|
218
|
+
errorBoundaryComponent
|
|
202
219
|
);
|
|
203
220
|
}
|
|
204
221
|
}
|
|
@@ -229,7 +246,8 @@ async function buildSlotElement(
|
|
|
229
246
|
params: Record<string, string | string[]>,
|
|
230
247
|
searchParams: unknown,
|
|
231
248
|
loadModule: ModuleLoader,
|
|
232
|
-
createElement: CreateElement
|
|
249
|
+
createElement: CreateElement,
|
|
250
|
+
errorBoundaryComponent: unknown
|
|
233
251
|
): Promise<ReactElement> {
|
|
234
252
|
// Load slot page
|
|
235
253
|
const pageModule = slotNode.page ? await loadModule(slotNode.page) : null;
|
|
@@ -249,7 +267,13 @@ async function buildSlotElement(
|
|
|
249
267
|
let element: ReactElement = createElement(PageComponent, { params, searchParams });
|
|
250
268
|
|
|
251
269
|
// Wrap in error boundaries
|
|
252
|
-
element = await wrapWithErrorBoundaries(
|
|
270
|
+
element = await wrapWithErrorBoundaries(
|
|
271
|
+
slotNode,
|
|
272
|
+
element,
|
|
273
|
+
loadModule,
|
|
274
|
+
createElement,
|
|
275
|
+
errorBoundaryComponent
|
|
276
|
+
);
|
|
253
277
|
|
|
254
278
|
// Wrap in SlotAccessGate if slot has access.ts
|
|
255
279
|
if (slotNode.access) {
|
|
@@ -301,7 +325,8 @@ async function wrapWithErrorBoundaries(
|
|
|
301
325
|
segment: SegmentNode,
|
|
302
326
|
element: ReactElement,
|
|
303
327
|
loadModule: ModuleLoader,
|
|
304
|
-
createElement: CreateElement
|
|
328
|
+
createElement: CreateElement,
|
|
329
|
+
errorBoundaryComponent: unknown
|
|
305
330
|
): Promise<ReactElement> {
|
|
306
331
|
// Wrapping is applied inside-out. The last wrap call produces the outermost boundary.
|
|
307
332
|
// Order: specific status → category → error.tsx (outermost)
|
|
@@ -315,7 +340,7 @@ async function wrapWithErrorBoundaries(
|
|
|
315
340
|
const mod = await loadModule(file);
|
|
316
341
|
const Component = mod.default;
|
|
317
342
|
if (Component) {
|
|
318
|
-
element = createElement(
|
|
343
|
+
element = createElement(errorBoundaryComponent, {
|
|
319
344
|
fallbackComponent: Component,
|
|
320
345
|
status,
|
|
321
346
|
children: element,
|
|
@@ -331,7 +356,7 @@ async function wrapWithErrorBoundaries(
|
|
|
331
356
|
const mod = await loadModule(file);
|
|
332
357
|
const Component = mod.default;
|
|
333
358
|
if (Component) {
|
|
334
|
-
element = createElement(
|
|
359
|
+
element = createElement(errorBoundaryComponent, {
|
|
335
360
|
fallbackComponent: Component,
|
|
336
361
|
status: key === '4xx' ? 400 : 500, // category marker
|
|
337
362
|
children: element,
|
|
@@ -346,7 +371,7 @@ async function wrapWithErrorBoundaries(
|
|
|
346
371
|
const errorModule = await loadModule(segment.error);
|
|
347
372
|
const ErrorComponent = errorModule.default;
|
|
348
373
|
if (ErrorComponent) {
|
|
349
|
-
element = createElement(
|
|
374
|
+
element = createElement(errorBoundaryComponent, {
|
|
350
375
|
fallbackComponent: ErrorComponent,
|
|
351
376
|
children: element,
|
|
352
377
|
} satisfies ErrorBoundaryProps);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"use-cookie-HcvNlW4L.js","names":[],"sources":["../../src/client/ssr-data.ts","../../src/client/use-cookie.ts"],"sourcesContent":["/**\n * SSR Data — per-request state for client hooks during server-side rendering.\n *\n * RSC and SSR are separate Vite module graphs (see design/18-build-system.md),\n * so the RSC environment's request-context ALS is not visible to SSR modules.\n * This module provides getter/setter functions that ssr-entry.ts uses to\n * populate per-request data for React's render.\n *\n * Request isolation: On the server, ssr-entry.ts registers an ALS-backed\n * provider via registerSsrDataProvider(). getSsrData() reads from the ALS\n * store, ensuring correct per-request data even when Suspense boundaries\n * resolve asynchronously across concurrent requests. The module-level\n * setSsrData/clearSsrData functions are kept as a fallback for tests\n * and environments without ALS.\n *\n * IMPORTANT: This module must NOT import node:async_hooks or any Node.js-only\n * APIs, as it's imported by 'use client' hooks that are bundled for the browser.\n * The ALS instance lives in ssr-entry.ts (server-only); this module only holds\n * a reference to the provider function.\n */\n\n// ─── Types ────────────────────────────────────────────────────────\n\nexport interface SsrData {\n /** The request's URL pathname (e.g. '/dashboard/settings') */\n pathname: string;\n /** The request's search params as a plain record */\n searchParams: Record<string, string>;\n /** The request's cookies as name→value pairs */\n cookies: Map<string, string>;\n /** The request's route params (e.g. { id: '123' }) */\n params: Record<string, string | string[]>;\n}\n\n// ─── ALS-Backed Provider ─────────────────────────────────────────\n//\n// Server-side code (ssr-entry.ts) registers a provider that reads\n// from AsyncLocalStorage. This avoids importing node:async_hooks\n// in this browser-bundled module.\n\nlet _ssrDataProvider: (() => SsrData | undefined) | undefined;\n\n/**\n * Register an ALS-backed SSR data provider. Called once at module load\n * by ssr-entry.ts to wire up per-request data via AsyncLocalStorage.\n *\n * When registered, getSsrData() reads from the provider (ALS store)\n * instead of module-level state, ensuring correct isolation for\n * concurrent requests with streaming Suspense.\n */\nexport function registerSsrDataProvider(provider: () => SsrData | undefined): void {\n _ssrDataProvider = provider;\n}\n\n// ─── Module-Level Fallback ────────────────────────────────────────\n//\n// Used by tests and as a fallback when no ALS provider is registered.\n\nlet currentSsrData: SsrData | undefined;\n\n/**\n * Set the SSR data for the current request via module-level state.\n *\n * In production, ssr-entry.ts uses ALS (runWithSsrData) instead.\n * This function is retained for tests and as a fallback.\n */\nexport function setSsrData(data: SsrData): void {\n currentSsrData = data;\n}\n\n/**\n * Clear the SSR data after rendering completes.\n *\n * In production, ALS scope handles cleanup automatically.\n * This function is retained for tests and as a fallback.\n */\nexport function clearSsrData(): void {\n currentSsrData = undefined;\n}\n\n/**\n * Read the current request's SSR data. Returns undefined when called\n * outside an SSR render (i.e. on the client after hydration).\n *\n * Prefers the ALS-backed provider when registered (server-side),\n * falling back to module-level state (tests, legacy).\n *\n * Used by client hooks' server snapshot functions.\n */\nexport function getSsrData(): SsrData | undefined {\n if (_ssrDataProvider) {\n return _ssrDataProvider();\n }\n return currentSsrData;\n}\n","/**\n * useCookie — reactive client-side cookie hook.\n *\n * Uses useSyncExternalStore for SSR-safe, reactive cookie access.\n * All components reading the same cookie name re-render on change.\n * No cross-tab sync (intentional — see design/29-cookies.md).\n *\n * See design/29-cookies.md §\"useCookie(name) Hook\"\n */\n\nimport { useSyncExternalStore } from 'react';\nimport { getSsrData } from './ssr-data.js';\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\nexport interface ClientCookieOptions {\n /** URL path scope. Default: '/'. */\n path?: string;\n /** Domain scope. Default: omitted (current domain). */\n domain?: string;\n /** Max age in seconds. */\n maxAge?: number;\n /** Expiration date. */\n expires?: Date;\n /** Cross-site policy. Default: 'lax'. */\n sameSite?: 'strict' | 'lax' | 'none';\n /** Only send over HTTPS. Default: true in production. */\n secure?: boolean;\n}\n\nexport type CookieSetter = (value: string, options?: ClientCookieOptions) => void;\n\n// ─── Module-Level Cookie Store ────────────────────────────────────────────\n\ntype Listener = () => void;\n\n/** Per-name subscriber sets. */\nconst listeners = new Map<string, Set<Listener>>();\n\n/** Parse a cookie name from document.cookie. */\nfunction getCookieValue(name: string): string | undefined {\n if (typeof document === 'undefined') return undefined;\n const match = document.cookie.match(\n new RegExp('(?:^|;\\\\s*)' + name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') + '\\\\s*=\\\\s*([^;]*)')\n );\n return match ? decodeURIComponent(match[1]) : undefined;\n}\n\n/** Serialize options into a cookie string suffix. */\nfunction serializeOptions(options?: ClientCookieOptions): string {\n if (!options) return '; Path=/; SameSite=Lax';\n const parts: string[] = [];\n parts.push(`Path=${options.path ?? '/'}`);\n if (options.domain) parts.push(`Domain=${options.domain}`);\n if (options.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);\n if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);\n const sameSite = options.sameSite ?? 'lax';\n parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);\n if (options.secure) parts.push('Secure');\n return '; ' + parts.join('; ');\n}\n\n/** Notify all subscribers for a given cookie name. */\nfunction notify(name: string): void {\n const subs = listeners.get(name);\n if (subs) {\n for (const fn of subs) fn();\n }\n}\n\n// ─── Hook ─────────────────────────────────────────────────────────────────\n\n/**\n * Reactive hook for reading/writing a client-side cookie.\n *\n * Returns `[value, setCookie, deleteCookie]`:\n * - `value`: current cookie value (string | undefined)\n * - `setCookie`: sets the cookie and triggers re-renders\n * - `deleteCookie`: deletes the cookie and triggers re-renders\n *\n * @param name - Cookie name.\n * @param defaultOptions - Default options for setCookie calls.\n */\nexport function useCookie(\n name: string,\n defaultOptions?: ClientCookieOptions\n): [value: string | undefined, setCookie: CookieSetter, deleteCookie: () => void] {\n const subscribe = (callback: Listener): (() => void) => {\n let subs = listeners.get(name);\n if (!subs) {\n subs = new Set();\n listeners.set(name, subs);\n }\n subs.add(callback);\n return () => {\n subs!.delete(callback);\n if (subs!.size === 0) listeners.delete(name);\n };\n };\n\n const getSnapshot = (): string | undefined => getCookieValue(name);\n const getServerSnapshot = (): string | undefined => getSsrData()?.cookies.get(name);\n\n const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n\n const setCookie: CookieSetter = (newValue: string, options?: ClientCookieOptions) => {\n const merged = { ...defaultOptions, ...options };\n document.cookie = `${name}=${encodeURIComponent(newValue)}${serializeOptions(merged)}`;\n notify(name);\n };\n\n const deleteCookie = (): void => {\n const path = defaultOptions?.path ?? '/';\n const domain = defaultOptions?.domain;\n let cookieStr = `${name}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=${path}`;\n if (domain) cookieStr += `; Domain=${domain}`;\n document.cookie = cookieStr;\n notify(name);\n };\n\n return [value, setCookie, deleteCookie];\n}\n"],"mappings":";;AAwCA,IAAI;AAkBJ,IAAI;;;;;;;AAQJ,SAAgB,WAAW,MAAqB;AAC9C,kBAAiB;;;;;;;;AASnB,SAAgB,eAAqB;AACnC,kBAAiB,KAAA;;;;;;;;;;;AAYnB,SAAgB,aAAkC;AAChD,KAAI,iBACF,QAAO,kBAAkB;AAE3B,QAAO;;;;;;;;;;;;;;ACxDT,IAAM,4BAAY,IAAI,KAA4B;;AAGlD,SAAS,eAAe,MAAkC;AACxD,KAAI,OAAO,aAAa,YAAa,QAAO,KAAA;CAC5C,MAAM,QAAQ,SAAS,OAAO,MAC5B,IAAI,OAAO,gBAAgB,KAAK,QAAQ,uBAAuB,OAAO,GAAG,mBAAmB,CAC7F;AACD,QAAO,QAAQ,mBAAmB,MAAM,GAAG,GAAG,KAAA;;;AAIhD,SAAS,iBAAiB,SAAuC;AAC/D,KAAI,CAAC,QAAS,QAAO;CACrB,MAAM,QAAkB,EAAE;AAC1B,OAAM,KAAK,QAAQ,QAAQ,QAAQ,MAAM;AACzC,KAAI,QAAQ,OAAQ,OAAM,KAAK,UAAU,QAAQ,SAAS;AAC1D,KAAI,QAAQ,WAAW,KAAA,EAAW,OAAM,KAAK,WAAW,QAAQ,SAAS;AACzE,KAAI,QAAQ,QAAS,OAAM,KAAK,WAAW,QAAQ,QAAQ,aAAa,GAAG;CAC3E,MAAM,WAAW,QAAQ,YAAY;AACrC,OAAM,KAAK,YAAY,SAAS,OAAO,EAAE,CAAC,aAAa,GAAG,SAAS,MAAM,EAAE,GAAG;AAC9E,KAAI,QAAQ,OAAQ,OAAM,KAAK,SAAS;AACxC,QAAO,OAAO,MAAM,KAAK,KAAK;;;AAIhC,SAAS,OAAO,MAAoB;CAClC,MAAM,OAAO,UAAU,IAAI,KAAK;AAChC,KAAI,KACF,MAAK,MAAM,MAAM,KAAM,KAAI;;;;;;;;;;;;;AAiB/B,SAAgB,UACd,MACA,gBACgF;CAChF,MAAM,aAAa,aAAqC;EACtD,IAAI,OAAO,UAAU,IAAI,KAAK;AAC9B,MAAI,CAAC,MAAM;AACT,0BAAO,IAAI,KAAK;AAChB,aAAU,IAAI,MAAM,KAAK;;AAE3B,OAAK,IAAI,SAAS;AAClB,eAAa;AACX,QAAM,OAAO,SAAS;AACtB,OAAI,KAAM,SAAS,EAAG,WAAU,OAAO,KAAK;;;CAIhD,MAAM,oBAAwC,eAAe,KAAK;CAClE,MAAM,0BAA8C,YAAY,EAAE,QAAQ,IAAI,KAAK;CAEnF,MAAM,QAAQ,qBAAqB,WAAW,aAAa,kBAAkB;CAE7E,MAAM,aAA2B,UAAkB,YAAkC;EACnF,MAAM,SAAS;GAAE,GAAG;GAAgB,GAAG;GAAS;AAChD,WAAS,SAAS,GAAG,KAAK,GAAG,mBAAmB,SAAS,GAAG,iBAAiB,OAAO;AACpF,SAAO,KAAK;;CAGd,MAAM,qBAA2B;EAC/B,MAAM,OAAO,gBAAgB,QAAQ;EACrC,MAAM,SAAS,gBAAgB;EAC/B,IAAI,YAAY,GAAG,KAAK,4DAA4D;AACpF,MAAI,OAAQ,cAAa,YAAY;AACrC,WAAS,SAAS;AAClB,SAAO,KAAK;;AAGd,QAAO;EAAC;EAAO;EAAW;EAAa"}
|