@timber-js/app 0.2.0-alpha.122 → 0.2.0-alpha.123
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/LICENSE +8 -0
- package/dist/_chunks/{actions-Dy8vOjFt.js → actions-ZlcKx0H9.js} +5 -5
- package/dist/_chunks/{actions-Dy8vOjFt.js.map → actions-ZlcKx0H9.js.map} +1 -1
- package/dist/_chunks/{define-CPVQaWbx.js → define-C0V_eKiW.js} +6 -18
- package/dist/_chunks/define-C0V_eKiW.js.map +1 -0
- package/dist/_chunks/{handler-store-CcwNX-gk.js → handler-store-bIAstrxQ.js} +1 -1
- package/dist/_chunks/{handler-store-CcwNX-gk.js.map → handler-store-bIAstrxQ.js.map} +1 -1
- package/dist/_chunks/{merge-search-params-az6ZiBeU.js → merge-search-params-Cm1j99HC.js} +1 -1
- package/dist/_chunks/{merge-search-params-az6ZiBeU.js.map → merge-search-params-Cm1j99HC.js.map} +1 -1
- package/dist/_chunks/{navigation-root-DoRTeBm5.js → navigation-root-Ds_hDtRW.js} +2 -2
- package/dist/_chunks/{navigation-root-DoRTeBm5.js.map → navigation-root-Ds_hDtRW.js.map} +1 -1
- package/dist/_chunks/{resolve-schema-cURaYvaD.js → resolve-schema-DU2nbVOi.js} +1 -1
- package/dist/_chunks/{resolve-schema-cURaYvaD.js.map → resolve-schema-DU2nbVOi.js.map} +1 -1
- package/dist/_chunks/{router-ref-CpvjBqdu.js → router-ref-B2-2kIIC.js} +2 -2
- package/dist/_chunks/{router-ref-CpvjBqdu.js.map → router-ref-B2-2kIIC.js.map} +1 -1
- package/dist/_chunks/{segment-context-EsfQpS--.js → segment-context-Bg0pFbLT.js} +1 -1
- package/dist/_chunks/{segment-context-EsfQpS--.js.map → segment-context-Bg0pFbLT.js.map} +1 -1
- package/dist/_chunks/{ssr-data-m9-cNEIw.js → ssr-data-DVX8S7vj.js} +1 -1
- package/dist/_chunks/{ssr-data-m9-cNEIw.js.map → ssr-data-DVX8S7vj.js.map} +1 -1
- package/dist/_chunks/{use-segment-params-CtIAjqK0.js → use-segment-params-Du7u25cK.js} +3 -8
- package/dist/_chunks/{use-segment-params-CtIAjqK0.js.map → use-segment-params-Du7u25cK.js.map} +1 -1
- package/dist/_chunks/{validation-BMU5MPzP.js → validation-AfEFM5ze.js} +1 -1
- package/dist/_chunks/{validation-BMU5MPzP.js.map → validation-AfEFM5ze.js.map} +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/error-boundary.js +2 -2
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +47 -14
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +67 -32
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +8 -0
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/shallow-url.d.ts +10 -0
- package/dist/client/shallow-url.d.ts.map +1 -0
- package/dist/cookies/index.js +2 -2
- package/dist/index.js +1 -1
- package/dist/params/index.js +1 -1
- package/dist/segment-params/define.d.ts +0 -6
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/internal.js +3 -3
- package/package.json +6 -7
- package/src/cli.ts +0 -0
- package/src/client/browser-entry/action-dispatch.ts +9 -10
- package/src/client/browser-entry/post-hydration.ts +12 -6
- package/src/client/index.ts +1 -5
- package/src/client/link.tsx +25 -12
- package/src/client/router.ts +74 -25
- package/src/client/rsc-fetch.ts +20 -5
- package/src/client/shallow-url.ts +22 -0
- package/src/segment-params/define.ts +2 -29
- package/dist/_chunks/define-CPVQaWbx.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.123",
|
|
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",
|
|
@@ -114,11 +114,6 @@
|
|
|
114
114
|
"publishConfig": {
|
|
115
115
|
"access": "public"
|
|
116
116
|
},
|
|
117
|
-
"scripts": {
|
|
118
|
-
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
119
|
-
"typecheck": "tsgo --noEmit",
|
|
120
|
-
"prepublishOnly": "pnpm run build"
|
|
121
|
-
},
|
|
122
117
|
"dependencies": {
|
|
123
118
|
"@opentelemetry/api": "^1.9.1",
|
|
124
119
|
"@opentelemetry/context-async-hooks": "^2.6.1",
|
|
@@ -162,5 +157,9 @@
|
|
|
162
157
|
},
|
|
163
158
|
"engines": {
|
|
164
159
|
"node": ">=22.12.0"
|
|
160
|
+
},
|
|
161
|
+
"scripts": {
|
|
162
|
+
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
163
|
+
"typecheck": "tsgo --noEmit"
|
|
165
164
|
}
|
|
166
|
-
}
|
|
165
|
+
}
|
package/src/cli.ts
CHANGED
|
File without changes
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { setServerCallback, encodeReply, createFromFetch } from '../../rsc-runtime/browser.js';
|
|
18
|
-
import { getRouter } from '#client-internal';
|
|
18
|
+
import { getRouter, getRouterOrNull } from '#client-internal';
|
|
19
19
|
import { setHardNavigating } from '../navigation-root.js';
|
|
20
20
|
import { isStaleClientReference, triggerStaleReload } from '../stale-reload.js';
|
|
21
21
|
import { getClientDeploymentId, DEPLOYMENT_ID_HEADER, RELOAD_HEADER } from '../rsc-fetch.js';
|
|
@@ -96,15 +96,14 @@ export function setupServerActions(): void {
|
|
|
96
96
|
// Piggybacked response: wrapper object { _action, _tree }
|
|
97
97
|
// Apply the revalidated tree directly — no separate router.refresh() needed.
|
|
98
98
|
const wrapper = decoded as { _action: unknown; _tree: unknown };
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
'[timber]
|
|
106
|
-
|
|
107
|
-
);
|
|
99
|
+
const router = getRouterOrNull();
|
|
100
|
+
if (router) {
|
|
101
|
+
try {
|
|
102
|
+
const headElements = headElementsJson ? JSON.parse(headElementsJson) : null;
|
|
103
|
+
router.applyRevalidation(wrapper._tree, headElements);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.error('[timber] applyRevalidation failed after server action', e);
|
|
106
|
+
}
|
|
108
107
|
}
|
|
109
108
|
return wrapper._action;
|
|
110
109
|
}
|
|
@@ -58,12 +58,18 @@ export function setupPostHydration({
|
|
|
58
58
|
delete (self as unknown as Record<string, unknown>).__timber_segments;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
//
|
|
62
|
-
// The
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
61
|
+
// Cache segment elements from the initial RSC payload after hydration.
|
|
62
|
+
// The element from createFromReadableStream is a Flight thenable that
|
|
63
|
+
// React resolves during hydration. Once resolved (status='fulfilled'),
|
|
64
|
+
// its .value contains the React element tree we can walk.
|
|
65
|
+
// Use requestAnimationFrame to run after hydration commits.
|
|
66
|
+
if (initialElement != null) {
|
|
67
|
+
requestAnimationFrame(() => {
|
|
68
|
+
const el = initialElement as { status?: string; value?: unknown };
|
|
69
|
+
const resolved = el.status === 'fulfilled' && el.value != null ? el.value : initialElement;
|
|
70
|
+
router.cacheElementTree(resolved);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
67
73
|
|
|
68
74
|
// If the hydration path was skipped (no RSC payload), populate the
|
|
69
75
|
// fallback params from server embed here.
|
package/src/client/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ export type { LinkStatus } from './use-link-status';
|
|
|
66
66
|
export { useRouter } from './use-router';
|
|
67
67
|
export type { AppRouterInstance } from './use-router';
|
|
68
68
|
export { usePathname } from './use-pathname';
|
|
69
|
+
export { replaceUrl } from './shallow-url';
|
|
69
70
|
// useSearchParams removed from public exports — lives in next/navigation shim only.
|
|
70
71
|
export { useSelectedLayoutSegment, useSelectedLayoutSegments } from './use-selected-layout-segment';
|
|
71
72
|
|
|
@@ -82,8 +83,3 @@ export { useSegmentParams } from './use-segment-params';
|
|
|
82
83
|
// Cookies — useCookie lives on defineCookie().useCookie().
|
|
83
84
|
// Types still exported for advanced use.
|
|
84
85
|
export type { ClientCookieOptions, CookieSetter } from './use-cookie';
|
|
85
|
-
|
|
86
|
-
// Register the client useSegmentParams hook with defineSegmentParams.
|
|
87
|
-
import { useSegmentParams as _useSegmentParams } from './use-segment-params.js';
|
|
88
|
-
import { _registerUseSegmentParams } from '../segment-params/define.js';
|
|
89
|
-
_registerUseSegmentParams(_useSegmentParams);
|
package/src/client/link.tsx
CHANGED
|
@@ -384,20 +384,28 @@ export function resolveHref(
|
|
|
384
384
|
| SearchParamsDefinition<Record<string, unknown>>
|
|
385
385
|
| undefined;
|
|
386
386
|
values = searchParams;
|
|
387
|
-
if (!definition) {
|
|
388
|
-
throw new Error(
|
|
389
|
-
`<Link> received a flat searchParams object for href "${href}", but no ` +
|
|
390
|
-
`SearchParamsDefinition is registered for that route. ` +
|
|
391
|
-
`Either the route does not export \`searchParams\` from its \`params.ts\`/\`page.tsx\`, ` +
|
|
392
|
-
`or the search-params registry module was not loaded. ` +
|
|
393
|
-
`For external or computed hrefs, pass the legacy \`{ definition, values }\` shape instead.`
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
387
|
}
|
|
397
388
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
389
|
+
if (definition) {
|
|
390
|
+
const qs = definition.serialize(values);
|
|
391
|
+
if (qs) {
|
|
392
|
+
resolvedPath = `${resolvedPath}?${qs}`;
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
// No registered definition — serialize flat object as plain query params.
|
|
396
|
+
const usp = new URLSearchParams();
|
|
397
|
+
for (const [key, val] of Object.entries(values)) {
|
|
398
|
+
if (val === undefined || val === null) continue;
|
|
399
|
+
if (Array.isArray(val)) {
|
|
400
|
+
for (const item of val) usp.append(key, String(item));
|
|
401
|
+
} else {
|
|
402
|
+
usp.set(key, String(val));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const qs = usp.toString();
|
|
406
|
+
if (qs) {
|
|
407
|
+
resolvedPath = `${resolvedPath}?${qs}`;
|
|
408
|
+
}
|
|
401
409
|
}
|
|
402
410
|
}
|
|
403
411
|
|
|
@@ -591,6 +599,11 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
591
599
|
return;
|
|
592
600
|
}
|
|
593
601
|
|
|
602
|
+
// Hash-only hrefs — let the browser handle scroll-to-anchor natively.
|
|
603
|
+
// The Navigation API path already skips these via event.hashChange;
|
|
604
|
+
// this covers the fallback (Safari/Firefox) where we drive navigation manually.
|
|
605
|
+
if (resolvedHref.startsWith('#')) return;
|
|
606
|
+
|
|
594
607
|
// History API fallback — prevent default and navigate via router
|
|
595
608
|
event.preventDefault();
|
|
596
609
|
|
package/src/client/router.ts
CHANGED
|
@@ -215,6 +215,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
215
215
|
const segmentElementCache = new SegmentElementCache();
|
|
216
216
|
|
|
217
217
|
let routerPhase: RouterPhase = { phase: 'idle' };
|
|
218
|
+
let navigationId = 0;
|
|
218
219
|
const pendingListeners = new Set<(pending: boolean) => void>();
|
|
219
220
|
|
|
220
221
|
// AbortController for the current in-flight navigation.
|
|
@@ -287,24 +288,46 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
287
288
|
|
|
288
289
|
/**
|
|
289
290
|
* Merge a partial RSC payload with cached segment elements if segments
|
|
290
|
-
* were skipped
|
|
291
|
-
*
|
|
291
|
+
* were skipped. Returns the merged payload ready for rendering.
|
|
292
|
+
*
|
|
293
|
+
* Segment element caching is deferred to after paint by the caller
|
|
294
|
+
* so it doesn't block the render path.
|
|
292
295
|
*/
|
|
293
|
-
function
|
|
296
|
+
async function mergePayload(
|
|
294
297
|
payload: unknown,
|
|
295
298
|
skippedSegments: string[] | null | undefined
|
|
296
|
-
): unknown {
|
|
297
|
-
let merged = payload;
|
|
298
|
-
|
|
299
|
-
// If segments were skipped, merge the partial payload with cached segments
|
|
299
|
+
): Promise<unknown> {
|
|
300
300
|
if (skippedSegments && skippedSegments.length > 0) {
|
|
301
|
-
|
|
301
|
+
// Segment merging needs a resolved React element tree for cloneElement.
|
|
302
|
+
// Await the payload only when merging is actually needed.
|
|
303
|
+
const resolved =
|
|
304
|
+
payload != null && typeof payload === 'object' && 'then' in payload
|
|
305
|
+
? await (payload as PromiseLike<unknown>)
|
|
306
|
+
: payload;
|
|
307
|
+
return mergeSegmentTree(resolved, skippedSegments, segmentElementCache);
|
|
302
308
|
}
|
|
309
|
+
// No merging needed — return the payload as-is (may be an unresolved
|
|
310
|
+
// Flight thenable). React handles progressive rendering via Suspense.
|
|
311
|
+
return payload;
|
|
312
|
+
}
|
|
303
313
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
314
|
+
/**
|
|
315
|
+
* Schedule segment element caching after paint. Resolves thenable
|
|
316
|
+
* payloads first since tree walking requires resolved React elements.
|
|
317
|
+
* Guards against stale writes: if a newer navigation started before
|
|
318
|
+
* this callback fires, the cache write is skipped.
|
|
319
|
+
*/
|
|
320
|
+
function deferSegmentCaching(payload: unknown): void {
|
|
321
|
+
const capturedNavId = navigationId;
|
|
322
|
+
afterPaint(async () => {
|
|
323
|
+
if (capturedNavId !== navigationId) return;
|
|
324
|
+
const resolved =
|
|
325
|
+
payload != null && typeof payload === 'object' && 'then' in payload
|
|
326
|
+
? await (payload as PromiseLike<unknown>)
|
|
327
|
+
: payload;
|
|
328
|
+
if (capturedNavId !== navigationId) return;
|
|
329
|
+
cacheSegmentElements(resolved, segmentElementCache);
|
|
330
|
+
});
|
|
308
331
|
}
|
|
309
332
|
|
|
310
333
|
/**
|
|
@@ -335,17 +358,27 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
335
358
|
* FetchResult plus the NavigationState (so it can be passed explicitly
|
|
336
359
|
* to wrapPayload/renderRoot without temporal coupling).
|
|
337
360
|
*/
|
|
361
|
+
interface TransitionResult {
|
|
362
|
+
headElements: HeadElement[] | null;
|
|
363
|
+
decodePromise: Promise<void> | null;
|
|
364
|
+
}
|
|
365
|
+
|
|
338
366
|
async function renderViaTransition(
|
|
339
367
|
url: string,
|
|
340
368
|
perform: () => Promise<FetchResult & { navState: NavigationState }>
|
|
341
|
-
): Promise<
|
|
369
|
+
): Promise<TransitionResult> {
|
|
342
370
|
if (deps.navigateTransition) {
|
|
343
371
|
let headElements: HeadElement[] | null = null;
|
|
372
|
+
let decodePromise: Promise<void> | null = null;
|
|
344
373
|
await deps.navigateTransition(url, async (wrapPayload) => {
|
|
345
374
|
const result = await perform();
|
|
346
375
|
headElements = result.headElements;
|
|
347
|
-
|
|
348
|
-
|
|
376
|
+
decodePromise = result.decodePromise;
|
|
377
|
+
// Merge partial payload with cached segments before wrapping.
|
|
378
|
+
// When no segments were skipped, mergePayload returns the payload
|
|
379
|
+
// as-is (possibly still an unresolved Flight thenable — React
|
|
380
|
+
// handles progressive rendering via Suspense).
|
|
381
|
+
const merged = await mergePayload(result.payload, result.skippedSegments);
|
|
349
382
|
// Store the MERGED payload in history — not the partial pre-merge tree.
|
|
350
383
|
// This ensures handlePopState replays the complete tree on back/forward.
|
|
351
384
|
historyStack.push(url, {
|
|
@@ -353,24 +386,28 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
353
386
|
headElements: result.headElements,
|
|
354
387
|
params: result.params,
|
|
355
388
|
});
|
|
389
|
+
// Defer segment element caching to after paint — it only benefits
|
|
390
|
+
// future navigations and doesn't need to block the current render.
|
|
391
|
+
deferSegmentCaching(merged);
|
|
356
392
|
// Pass navState explicitly — wrapPayload wraps element in
|
|
357
393
|
// NavigationProvider with this state, no getNavigationState() needed.
|
|
358
394
|
return wrapPayload(merged, result.navState);
|
|
359
395
|
});
|
|
360
|
-
return headElements;
|
|
396
|
+
return { headElements, decodePromise };
|
|
361
397
|
}
|
|
362
398
|
// Fallback: no transition (tests, no React tree)
|
|
363
399
|
const result = await perform();
|
|
364
400
|
// Merge partial payload with cached segments before rendering
|
|
365
|
-
const merged =
|
|
401
|
+
const merged = await mergePayload(result.payload, result.skippedSegments);
|
|
366
402
|
// Store merged payload in history
|
|
367
403
|
historyStack.push(url, {
|
|
368
404
|
payload: merged,
|
|
369
405
|
headElements: result.headElements,
|
|
370
406
|
params: result.params,
|
|
371
407
|
});
|
|
408
|
+
deferSegmentCaching(merged);
|
|
372
409
|
renderPayload(merged, result.navState);
|
|
373
|
-
return result.headElements;
|
|
410
|
+
return { headElements: result.headElements, decodePromise: result.decodePromise };
|
|
374
411
|
}
|
|
375
412
|
|
|
376
413
|
/** Apply head elements (title, meta tags) to the DOM if available. */
|
|
@@ -414,6 +451,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
414
451
|
let result: FetchResult | undefined = prefetched
|
|
415
452
|
? {
|
|
416
453
|
payload: prefetched.payload,
|
|
454
|
+
decodePromise: null,
|
|
417
455
|
headElements: prefetched.headElements,
|
|
418
456
|
segmentInfo: prefetched.segmentInfo ?? null,
|
|
419
457
|
params: prefetched.params ?? null,
|
|
@@ -466,6 +504,8 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
466
504
|
const externalSignal = options._signal as AbortSignal | undefined;
|
|
467
505
|
const skipHistory = options._skipHistory === true;
|
|
468
506
|
|
|
507
|
+
navigationId++;
|
|
508
|
+
|
|
469
509
|
// Create an abort controller for this navigation. Links to the external
|
|
470
510
|
// signal (Navigation API's event.signal) when provided.
|
|
471
511
|
const navAbort = createNavAbort(externalSignal);
|
|
@@ -499,7 +539,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
499
539
|
setPending(true, url);
|
|
500
540
|
|
|
501
541
|
try {
|
|
502
|
-
const headElements = await renderViaTransition(url, () =>
|
|
542
|
+
const { headElements, decodePromise } = await renderViaTransition(url, () =>
|
|
503
543
|
performNavigationFetch(url, {
|
|
504
544
|
replace,
|
|
505
545
|
signal: navAbort.signal,
|
|
@@ -507,6 +547,10 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
507
547
|
})
|
|
508
548
|
);
|
|
509
549
|
|
|
550
|
+
// Await async RSC decode errors (truncated streams, Flight parse failures)
|
|
551
|
+
// so they surface through this try/catch rather than becoming unhandled.
|
|
552
|
+
if (decodePromise) await decodePromise;
|
|
553
|
+
|
|
510
554
|
// Update document.title and <meta> tags with the new page's metadata
|
|
511
555
|
applyHead(headElements);
|
|
512
556
|
|
|
@@ -575,12 +619,13 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
575
619
|
|
|
576
620
|
async function refresh(): Promise<void> {
|
|
577
621
|
const currentUrl = deps.getCurrentUrl();
|
|
622
|
+
navigationId++;
|
|
578
623
|
const navAbort = createNavAbort();
|
|
579
624
|
|
|
580
625
|
setPending(true, currentUrl);
|
|
581
626
|
|
|
582
627
|
try {
|
|
583
|
-
const headElements = await renderViaTransition(currentUrl, async () => {
|
|
628
|
+
const { headElements, decodePromise } = await renderViaTransition(currentUrl, async () => {
|
|
584
629
|
// No state tree sent — server renders the complete RSC payload
|
|
585
630
|
const result = await fetchRscPayload(
|
|
586
631
|
currentUrl,
|
|
@@ -595,6 +640,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
595
640
|
return { ...result, navState };
|
|
596
641
|
});
|
|
597
642
|
|
|
643
|
+
if (decodePromise) await decodePromise;
|
|
598
644
|
applyHead(headElements);
|
|
599
645
|
} catch (error) {
|
|
600
646
|
// Stale transition (superseded by a newer navigation) or aborted
|
|
@@ -615,6 +661,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
615
661
|
scrollY: number = 0,
|
|
616
662
|
externalSignal?: AbortSignal
|
|
617
663
|
): Promise<void> {
|
|
664
|
+
navigationId++;
|
|
618
665
|
// Scroll position is read from history.state by the caller (browser-entry.ts)
|
|
619
666
|
// and passed in. This is more reliable than tracking scroll per-URL in memory
|
|
620
667
|
// because the browser maintains per-entry state even with duplicate URLs.
|
|
@@ -634,7 +681,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
634
681
|
const navAbort = createNavAbort(externalSignal);
|
|
635
682
|
setPending(true, url);
|
|
636
683
|
try {
|
|
637
|
-
const headElements = await renderViaTransition(url, async () => {
|
|
684
|
+
const { headElements, decodePromise } = await renderViaTransition(url, async () => {
|
|
638
685
|
const stateTree = segmentCache.serializeStateTree(
|
|
639
686
|
segmentElementCache.getMergeablePaths()
|
|
640
687
|
);
|
|
@@ -645,6 +692,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
645
692
|
return { ...result, navState };
|
|
646
693
|
});
|
|
647
694
|
|
|
695
|
+
if (decodePromise) await decodePromise;
|
|
648
696
|
applyHead(headElements);
|
|
649
697
|
restoreScrollAfterPaint(scrollY);
|
|
650
698
|
} catch (error) {
|
|
@@ -697,18 +745,19 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
697
745
|
// Render the piggybacked element tree from a server action response.
|
|
698
746
|
// Updates the current history entry with the fresh payload and applies
|
|
699
747
|
// head elements — same as refresh() but without a server fetch.
|
|
700
|
-
//
|
|
748
|
+
// Revalidation payloads are already resolved (no Flight thenable),
|
|
749
|
+
// so no segment merging is needed — just cache and render.
|
|
701
750
|
const currentUrl = deps.getCurrentUrl();
|
|
702
|
-
const merged = mergeAndCachePayload(element, null);
|
|
703
751
|
historyStack.push(currentUrl, {
|
|
704
|
-
payload:
|
|
752
|
+
payload: element,
|
|
705
753
|
headElements,
|
|
706
754
|
});
|
|
755
|
+
deferSegmentCaching(element);
|
|
707
756
|
// Revalidation doesn't change params/pathname — preserve current state.
|
|
708
757
|
// DO NOT call updateNavigationState(null, ...) here: that normalizes
|
|
709
758
|
// params to {}, clearing dynamic route params on every action response.
|
|
710
759
|
const navState = getNavigationState();
|
|
711
|
-
renderPayload(
|
|
760
|
+
renderPayload(element, navState);
|
|
712
761
|
applyHead(headElements);
|
|
713
762
|
},
|
|
714
763
|
initSegmentCache: (segments: SegmentInfo[]) => updateSegmentCache(segments),
|
package/src/client/rsc-fetch.ts
CHANGED
|
@@ -20,6 +20,14 @@ import type { RouterDeps } from './router';
|
|
|
20
20
|
/** Result of fetching an RSC payload — includes head elements and segment metadata. */
|
|
21
21
|
export interface FetchResult {
|
|
22
22
|
payload: unknown;
|
|
23
|
+
/**
|
|
24
|
+
* Promise that settles when the RSC decode completes or fails.
|
|
25
|
+
* The payload thenable is NOT awaited before returning (for streaming),
|
|
26
|
+
* so callers must monitor this to catch async decode errors
|
|
27
|
+
* (truncated streams, Flight parse failures) that would otherwise
|
|
28
|
+
* become unhandled rejections.
|
|
29
|
+
*/
|
|
30
|
+
decodePromise: Promise<void> | null;
|
|
23
31
|
headElements: HeadElement[] | null;
|
|
24
32
|
/** Segment metadata from X-Timber-Segments header for populating the segment cache. */
|
|
25
33
|
segmentInfo: SegmentInfo[] | null;
|
|
@@ -313,11 +321,17 @@ export async function fetchRscPayload(
|
|
|
313
321
|
});
|
|
314
322
|
// Await headers so headElements/segmentInfo/params are populated.
|
|
315
323
|
await wrappedPromise;
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
324
|
+
// Start decoding but do NOT await — return the in-progress thenable.
|
|
325
|
+
// React can render a Flight thenable directly: it suspends on unresolved
|
|
326
|
+
// parts and progressively renders as chunks arrive, spreading work across
|
|
327
|
+
// frames instead of blocking the main thread in one burst.
|
|
328
|
+
const payload = deps.decodeRsc(wrappedPromise);
|
|
329
|
+
// Track decode completion so callers can catch async errors (truncated
|
|
330
|
+
// streams, Flight parse failures) that fire after fetchRscPayload returns.
|
|
331
|
+
const decodePromise = Promise.resolve(payload).then(() => undefined);
|
|
332
|
+
// Prevent unhandled rejection — the router monitors decodePromise separately.
|
|
333
|
+
decodePromise.catch(() => {});
|
|
334
|
+
return { payload, decodePromise, headElements, segmentInfo, params, skippedSegments };
|
|
321
335
|
}
|
|
322
336
|
// Test/fallback path: return raw text
|
|
323
337
|
const response = await deps.fetch(rscUrl, { headers, redirect: 'manual', signal });
|
|
@@ -330,6 +344,7 @@ export async function fetchRscPayload(
|
|
|
330
344
|
}
|
|
331
345
|
return {
|
|
332
346
|
payload: await response.text(),
|
|
347
|
+
decodePromise: null,
|
|
333
348
|
headElements: extractHeadElements(response),
|
|
334
349
|
segmentInfo: extractSegmentInfo(response),
|
|
335
350
|
params: extractParams(response),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shallow URL replacement — update the browser URL bar without triggering
|
|
3
|
+
* RSC navigation, TopLoader, or any server round-trip.
|
|
4
|
+
*
|
|
5
|
+
* Uses the Navigation API's `info: { shallow: true }` when available (Chrome),
|
|
6
|
+
* which the navigate event handler intercepts with a no-op handler. Falls back
|
|
7
|
+
* to raw `history.replaceState` (Safari/Firefox — no navigate event fired).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getNavigationApi } from './navigation-api.js';
|
|
11
|
+
|
|
12
|
+
export function replaceUrl(url: string): void {
|
|
13
|
+
const nav = getNavigationApi();
|
|
14
|
+
if (nav) {
|
|
15
|
+
nav.navigate(url, {
|
|
16
|
+
history: 'replace',
|
|
17
|
+
info: { shallow: true },
|
|
18
|
+
});
|
|
19
|
+
} else {
|
|
20
|
+
history.replaceState(history.state, '', url);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
isStandardSchema,
|
|
21
21
|
isCodec,
|
|
22
22
|
} from '../schema-bridge.js';
|
|
23
|
+
import { useSegmentParams as clientUseSegmentParams } from '../client/use-segment-params.js';
|
|
23
24
|
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// Server-only ALS reference for .get()
|
|
@@ -28,28 +29,6 @@ import {
|
|
|
28
29
|
// Same pattern as search-params: eagerly registered at server startup
|
|
29
30
|
// to avoid dynamic imports that lose ALS context. See TIM-523.
|
|
30
31
|
let _getSegmentParamsFn: (() => Record<string, string | string[]>) | undefined;
|
|
31
|
-
let _useSegmentParamsHook: (() => Record<string, string | string[]>) | undefined;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Register the client useSegmentParams hook.
|
|
35
|
-
* Called by client entry at module load time to avoid require() at call time.
|
|
36
|
-
* @internal
|
|
37
|
-
*/
|
|
38
|
-
export function _registerUseSegmentParams(hook: () => Record<string, string | string[]>): void {
|
|
39
|
-
_useSegmentParamsHook = hook;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Self-register on the client when the barrel hasn't been imported yet.
|
|
43
|
-
// Handles the edge case where a client component imports only from params.ts
|
|
44
|
-
// without importing anything from @timber-js/app/client (which runs the
|
|
45
|
-
// registration). The dynamic import resolves before React hydration in Vite.
|
|
46
|
-
if (typeof window !== 'undefined' && !_useSegmentParamsHook) {
|
|
47
|
-
import('../client/use-segment-params.js').then((mod) => {
|
|
48
|
-
if (!_useSegmentParamsHook) {
|
|
49
|
-
_useSegmentParamsHook = mod.useSegmentParams;
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
32
|
|
|
54
33
|
/**
|
|
55
34
|
* Register the getSegmentParams function. Called once at module load time
|
|
@@ -311,13 +290,7 @@ export function defineSegmentParams<C extends Record<string, ParamField>>(
|
|
|
311
290
|
// Client hook that reads typed params from the navigation context.
|
|
312
291
|
// Delegates to the existing useSegmentParams hook from use-segment-params.ts.
|
|
313
292
|
function useSegmentParams(): T {
|
|
314
|
-
|
|
315
|
-
throw new Error(
|
|
316
|
-
'[timber] segmentParams.useSegmentParams() requires @timber-js/app/client to be loaded. ' +
|
|
317
|
-
'This hook can only be used in client components.'
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
const raw = _useSegmentParamsHook();
|
|
293
|
+
const raw = clientUseSegmentParams();
|
|
321
294
|
// Params are already coerced by the server pipeline before being sent
|
|
322
295
|
// to the client — return as-is. Re-parsing would break non-idempotent
|
|
323
296
|
// codecs (e.g., z.coerce.number() on an already-parsed number). See TIM-574.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"define-CPVQaWbx.js","names":[],"sources":["../../src/segment-params/define.ts"],"sourcesContent":["/**\n * defineSegmentParams — factory for typed route param coercion.\n *\n * Creates a ParamsDefinition that coerces raw string params from the\n * URL into typed values. Used by exporting from params.ts (segment-level)\n * convention file.\n *\n * Reuses the shared Codec<T> protocol with Standard Schema auto-detection,\n * same pattern as defineSearchParams. Runtime constraints are stricter:\n * - serialize must return string (not null — path segments can't be omitted)\n * - parse throwing → 404 (invalid param value)\n *\n * Design doc: design/07a-route-params-triage.md\n */\n\nimport type { Codec } from '../codec.js';\nimport {\n type StandardSchemaV1,\n validateSync,\n isStandardSchema,\n isCodec,\n} from '../schema-bridge.js';\n\n// ---------------------------------------------------------------------------\n// Server-only ALS reference for .get()\n// ---------------------------------------------------------------------------\n\n// Same pattern as search-params: eagerly registered at server startup\n// to avoid dynamic imports that lose ALS context. See TIM-523.\nlet _getSegmentParamsFn: (() => Record<string, string | string[]>) | undefined;\nlet _useSegmentParamsHook: (() => Record<string, string | string[]>) | undefined;\n\n/**\n * Register the client useSegmentParams hook.\n * Called by client entry at module load time to avoid require() at call time.\n * @internal\n */\nexport function _registerUseSegmentParams(hook: () => Record<string, string | string[]>): void {\n _useSegmentParamsHook = hook;\n}\n\n// Self-register on the client when the barrel hasn't been imported yet.\n// Handles the edge case where a client component imports only from params.ts\n// without importing anything from @timber-js/app/client (which runs the\n// registration). The dynamic import resolves before React hydration in Vite.\nif (typeof window !== 'undefined' && !_useSegmentParamsHook) {\n import('../client/use-segment-params.js').then((mod) => {\n if (!_useSegmentParamsHook) {\n _useSegmentParamsHook = mod.useSegmentParams;\n }\n });\n}\n\n/**\n * Register the getSegmentParams function. Called once at module load time\n * from request-context.ts to avoid dynamic import at call time.\n * @internal\n */\nexport function _setGetSegmentParamsFn(fn: () => Record<string, string | string[]>): void {\n _getSegmentParamsFn = fn;\n}\n\nfunction getSegmentParamsFromAls(): Record<string, string | string[]> {\n if (!_getSegmentParamsFn) {\n throw new Error(\n '[timber] segmentParams.get() is only available on the server. ' +\n 'Use segmentParams.useSegmentParams() on the client.'\n );\n }\n return _getSegmentParamsFn();\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Infer the output type from a Codec or StandardSchemaV1. */\nexport type InferParamField<V> =\n V extends Codec<infer T> ? T : V extends StandardSchemaV1<infer T> ? T : never;\n\n/** Acceptable field value for defineParams: a Codec or a Standard Schema. */\nexport type ParamField<T = unknown> = Codec<T> | StandardSchemaV1<T>;\n\nexport type { StandardSchemaV1 };\n\n/**\n * A typed route params definition.\n *\n * Returned by defineParams(). Provides parse (string → typed) and\n * serialize (typed → string) for each declared param.\n */\nexport interface ParamsDefinition<T extends Record<string, unknown>> {\n /** Parse raw string params into typed values. Throws on invalid values. */\n parse(raw: Record<string, string | string[]>): T;\n\n /** Serialize typed values back to strings for URL construction. */\n serialize(values: T): Record<string, string>;\n\n /**\n * Get typed segment params from the current request context (ALS).\n *\n * Server-only, sync.\n *\n * ```ts\n * // app/products/[id]/params.ts\n * export const segmentParams = defineSegmentParams({ id: z.coerce.number() })\n *\n * // app/products/[id]/page.tsx\n * import { segmentParams } from './params'\n * export default function Page() {\n * const { id } = segmentParams.get() // id: number\n * }\n * ```\n */\n get(): T;\n\n /**\n * Client hook for accessing typed segment params reactively.\n *\n * ```tsx\n * 'use client'\n * import { segmentParams } from './params'\n * export function ProductHeader() {\n * const { id } = segmentParams.useSegmentParams()\n * }\n * ```\n */\n useSegmentParams(): T;\n\n /** Read-only codec map. */\n codecs: { [K in keyof T]: Codec<T[K]> };\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a Standard Schema into a Codec for route params.\n *\n * Unlike fromSchema for search params:\n * - Parse throws on failure (no fallback to default)\n * - Serialize returns string (not null)\n */\nfunction fromParamSchema<T>(fieldName: string, schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n // Route params are always strings (single segment) or string[] (catch-all)\n const input = Array.isArray(value) ? value : value;\n\n const result = validateSync(schema, input);\n if (!result.issues) {\n return result.value;\n }\n\n // For route params, parse failure means the param is invalid → throw\n const messages = result.issues.map((i) => i.message).join(', ');\n throw new Error(`[timber] Param '${fieldName}' coercion failed: ${messages}`);\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n // Catch-all segments produce arrays — join with '/' for path reconstruction\n if (Array.isArray(value)) {\n return value.join('/');\n }\n return String(value);\n },\n };\n}\n\n/**\n * Resolve a field value to a Codec. Auto-detects Standard Schema objects.\n */\nfunction resolveField(fieldName: string, value: ParamField): Codec<unknown> {\n if (isCodec(value)) {\n return value;\n }\n\n if (isStandardSchema(value)) {\n return fromParamSchema(fieldName, value);\n }\n\n throw new Error(\n `[timber] defineSegmentParams: field '${fieldName}' is not a valid codec or Standard Schema. ` +\n `Expected an object with { parse, serialize } methods, or a Standard Schema object ` +\n `(Zod, Valibot, ArkType).`\n );\n}\n\n/**\n * Validate that no codec's serialize returns null.\n * Route params are structural — they must produce a valid path segment.\n */\nfunction validateSerialize(codecMap: Record<string, Codec<unknown>>): void {\n for (const [key, codec] of Object.entries(codecMap)) {\n // Test serialize with a sample parsed value to check for null\n // We can't exhaustively test, but we can check that serialize(parse(\"test\"))\n // doesn't return null for a basic input.\n try {\n const testValue = codec.parse('test');\n const serialized = codec.serialize(testValue);\n if (serialized === null) {\n throw new Error(\n `[timber] defineSegmentParams: field '${key}' codec.serialize() returned null.\\n` +\n ` Route params are path segments — they cannot be omitted.\\n` +\n ` Ensure serialize() always returns a string.`\n );\n }\n } catch (e) {\n // parse('test') may throw for strict codecs (e.g., number-only).\n // That's fine — it means the codec validates. We only care about\n // serialize returning null, which we can't test without a valid value.\n if (e instanceof Error && e.message.includes('returned null')) {\n throw e;\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a ParamsDefinition from a map of codecs and/or Standard Schema objects.\n *\n * ```ts\n * // app/products/[id]/layout.tsx\n * import { defineSegmentParams } from '@timber-js/app/segment-params'\n * import { z } from 'zod/v4'\n *\n * export const segmentParams = defineSegmentParams({\n * id: z.coerce.number().int().positive(),\n * })\n * ```\n */\nexport function defineSegmentParams<C extends Record<string, ParamField>>(\n codecs: C\n): ParamsDefinition<{ [K in keyof C]: InferParamField<C[K]> }> {\n type T = { [K in keyof C]: InferParamField<C[K]> };\n\n const resolvedCodecs: Record<string, Codec<unknown>> = {};\n\n for (const [key, value] of Object.entries(codecs)) {\n resolvedCodecs[key] = resolveField(key, value as ParamField);\n }\n\n // Validate that serialize doesn't return null\n validateSerialize(resolvedCodecs);\n\n // ---- parse ----\n function parse(raw: Record<string, string | string[]>): T {\n const result: Record<string, unknown> = {};\n\n for (const [key, codec] of Object.entries(resolvedCodecs)) {\n const rawValue = raw[key];\n // Skip params not present in the URL at this route depth.\n // This supports TIM-834 inheritance: a parent params.ts can declare\n // codecs for descendant segments. At shallower depths those params\n // don't exist yet — skip them instead of passing undefined to the codec.\n if (rawValue === undefined) continue;\n result[key] = codec.parse(rawValue);\n }\n\n return result as T;\n }\n\n // ---- serialize ----\n function serialize(values: T): Record<string, string> {\n const result: Record<string, string> = {};\n\n for (const [key, codec] of Object.entries(resolvedCodecs)) {\n const serialized = codec.serialize(values[key as keyof T] as unknown);\n if (serialized === null) {\n throw new Error(\n `[timber] params.serialize: field '${key}' serialized to null. ` +\n `Route params must produce a valid path segment.`\n );\n }\n result[key] = serialized;\n }\n\n return result;\n }\n\n // ---- get ----\n // ALS-backed: reads segment params from the current request context.\n // Server-only, sync.\n //\n // The pipeline already coerces params via coerceSegmentParams() which\n // calls parse() and stores typed values in ALS via setSegmentParams().\n // We return those directly instead of re-parsing, because codecs may\n // not be idempotent (e.g., a codec that only accepts raw strings would\n // throw if given an already-parsed value). See TIM-574.\n function get(): T {\n if (typeof window !== 'undefined') {\n throw new Error(\n '[timber] segmentParams.get() is server-only. ' +\n 'Use segmentParams.useSegmentParams() on the client.'\n );\n }\n const params = getSegmentParamsFromAls();\n // params are already coerced by the pipeline — return as-is.\n return params as unknown as T;\n }\n\n // ---- useSegmentParams ----\n // Client hook that reads typed params from the navigation context.\n // Delegates to the existing useSegmentParams hook from use-segment-params.ts.\n function useSegmentParams(): T {\n if (!_useSegmentParamsHook) {\n throw new Error(\n '[timber] segmentParams.useSegmentParams() requires @timber-js/app/client to be loaded. ' +\n 'This hook can only be used in client components.'\n );\n }\n const raw = _useSegmentParamsHook();\n // Params are already coerced by the server pipeline before being sent\n // to the client — return as-is. Re-parsing would break non-idempotent\n // codecs (e.g., z.coerce.number() on an already-parsed number). See TIM-574.\n return raw as unknown as T;\n }\n\n const definition: ParamsDefinition<T> = {\n parse,\n serialize,\n get,\n useSegmentParams,\n codecs: resolvedCodecs as { [K in keyof T]: Codec<T[K]> },\n };\n\n return definition;\n}\n"],"mappings":";;AA6BA,IAAI;AACJ,IAAI;;;;;;AAOJ,SAAgB,0BAA0B,MAAqD;AAC7F,yBAAwB;;AAO1B,IAAI,OAAO,WAAW,eAAe,CAAC,sBACpC,QAAO,oCAAA,MAAA,MAAA,EAAA,EAAA,CAAmC,MAAM,QAAQ;AACtD,KAAI,CAAC,sBACH,yBAAwB,IAAI;EAE9B;;;;;;AAQJ,SAAgB,uBAAuB,IAAmD;AACxF,uBAAsB;;AAGxB,SAAS,0BAA6D;AACpE,KAAI,CAAC,oBACH,OAAM,IAAI,MACR,oHAED;AAEH,QAAO,qBAAqB;;;;;;;;;AA2E9B,SAAS,gBAAmB,WAAmB,QAAuC;AACpF,QAAO;EACL,MAAM,OAAyC;GAI7C,MAAM,SAAS,aAAa,QAFd,MAAM,QAAQ,MAAM,GAAG,QAAQ,MAEH;AAC1C,OAAI,CAAC,OAAO,OACV,QAAO,OAAO;GAIhB,MAAM,WAAW,OAAO,OAAO,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,KAAK;AAC/D,SAAM,IAAI,MAAM,mBAAmB,UAAU,qBAAqB,WAAW;;EAG/E,UAAU,OAAyB;AACjC,OAAI,UAAU,QAAQ,UAAU,KAAA,EAC9B,QAAO;AAGT,OAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,KAAK,IAAI;AAExB,UAAO,OAAO,MAAM;;EAEvB;;;;;AAMH,SAAS,aAAa,WAAmB,OAAmC;AAC1E,KAAI,QAAQ,MAAM,CAChB,QAAO;AAGT,KAAI,iBAAiB,MAAM,CACzB,QAAO,gBAAgB,WAAW,MAAM;AAG1C,OAAM,IAAI,MACR,wCAAwC,UAAU,uJAGnD;;;;;;AAOH,SAAS,kBAAkB,UAAgD;AACzE,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,CAIjD,KAAI;EACF,MAAM,YAAY,MAAM,MAAM,OAAO;AAErC,MADmB,MAAM,UAAU,UAAU,KAC1B,KACjB,OAAM,IAAI,MACR,wCAAwC,IAAI,+IAG7C;UAEI,GAAG;AAIV,MAAI,aAAa,SAAS,EAAE,QAAQ,SAAS,gBAAgB,CAC3D,OAAM;;;;;;;;;;;;;;;;AAuBd,SAAgB,oBACd,QAC6D;CAG7D,MAAM,iBAAiD,EAAE;AAEzD,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,CAC/C,gBAAe,OAAO,aAAa,KAAK,MAAoB;AAI9D,mBAAkB,eAAe;CAGjC,SAAS,MAAM,KAA2C;EACxD,MAAM,SAAkC,EAAE;AAE1C,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,eAAe,EAAE;GACzD,MAAM,WAAW,IAAI;AAKrB,OAAI,aAAa,KAAA,EAAW;AAC5B,UAAO,OAAO,MAAM,MAAM,SAAS;;AAGrC,SAAO;;CAIT,SAAS,UAAU,QAAmC;EACpD,MAAM,SAAiC,EAAE;AAEzC,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,eAAe,EAAE;GACzD,MAAM,aAAa,MAAM,UAAU,OAAO,KAA2B;AACrE,OAAI,eAAe,KACjB,OAAM,IAAI,MACR,qCAAqC,IAAI,uEAE1C;AAEH,UAAO,OAAO;;AAGhB,SAAO;;CAYT,SAAS,MAAS;AAChB,MAAI,OAAO,WAAW,YACpB,OAAM,IAAI,MACR,mGAED;AAIH,SAFe,yBAAyB;;CAQ1C,SAAS,mBAAsB;AAC7B,MAAI,CAAC,sBACH,OAAM,IAAI,MACR,0IAED;AAMH,SAJY,uBAAuB;;AAerC,QARwC;EACtC;EACA;EACA;EACA;EACA,QAAQ;EACT"}
|