@timber-js/app 0.2.0-alpha.54 → 0.2.0-alpha.55
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/client/index.js +201 -162
- package/dist/client/index.js.map +1 -1
- package/dist/client/link-pending-store.d.ts +78 -0
- package/dist/client/link-pending-store.d.ts.map +1 -0
- package/dist/client/link.d.ts +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/transition-root.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/package.json +6 -7
- package/src/cli.ts +0 -0
- package/src/client/link-pending-store.ts +136 -0
- package/src/client/link.tsx +59 -3
- package/src/client/navigation-context.ts +4 -3
- package/src/client/transition-root.tsx +13 -1
- package/src/server/html-injectors.ts +16 -0
- package/src/server/node-stream-transforms.ts +18 -13
- package/dist/client/link-status-provider.d.ts +0 -11
- package/dist/client/link-status-provider.d.ts.map +0 -1
- package/src/client/link-status-provider.tsx +0 -30
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link Pending Store — module-level singleton for per-link pending state.
|
|
3
|
+
*
|
|
4
|
+
* Tracks which link instance is currently navigating so that only the
|
|
5
|
+
* clicked link shows pending. All other links remain unaffected — zero
|
|
6
|
+
* wasted re-renders.
|
|
7
|
+
*
|
|
8
|
+
* The store holds:
|
|
9
|
+
* - A reference to the currently navigating link's state setter
|
|
10
|
+
* - A navigation counter to guard against stale clears (race condition
|
|
11
|
+
* when the user clicks Link B before Link A's navigation completes)
|
|
12
|
+
*
|
|
13
|
+
* Flow:
|
|
14
|
+
* 1. Link click handler: setLinkForCurrentNavigation(instance) →
|
|
15
|
+
* resets previous link (urgent), sets new link pending (urgent),
|
|
16
|
+
* stores setter + increments navId
|
|
17
|
+
* 2. TransitionRoot startTransition: captures navId, does async work
|
|
18
|
+
* 3. TransitionRoot commit: resetLinkPending(capturedNavId) →
|
|
19
|
+
* calls setter(IDLE) inside the transition (batched, atomic with tree)
|
|
20
|
+
* Only clears if navId matches (prevents stale T1 from clearing T2's link)
|
|
21
|
+
*
|
|
22
|
+
* SINGLETON GUARANTEE: Uses `globalThis` via `Symbol.for` keys (same
|
|
23
|
+
* pattern as NavigationContext) because the RSC client bundler can
|
|
24
|
+
* duplicate this module across chunks. Module-level variables would
|
|
25
|
+
* create separate instances per chunk.
|
|
26
|
+
*
|
|
27
|
+
* See design/19-client-navigation.md §"Per-Link Pending State"
|
|
28
|
+
*/
|
|
29
|
+
import type { LinkStatus } from './use-link-status.js';
|
|
30
|
+
export interface LinkPendingInstance {
|
|
31
|
+
setLinkStatus: (status: LinkStatus) => void;
|
|
32
|
+
}
|
|
33
|
+
/** Status object indicating link is pending — shared reference */
|
|
34
|
+
export declare const PENDING_LINK_STATUS: LinkStatus;
|
|
35
|
+
/** Status object indicating link is idle — shared reference */
|
|
36
|
+
export declare const IDLE_LINK_STATUS: LinkStatus;
|
|
37
|
+
/**
|
|
38
|
+
* Register the link instance that initiated the current navigation.
|
|
39
|
+
*
|
|
40
|
+
* Called from <Link>'s click handler before router.navigate().
|
|
41
|
+
* - Resets the previous pending link to IDLE (urgent update, immediate)
|
|
42
|
+
* - Does NOT set the new link to PENDING here — the Link's click handler
|
|
43
|
+
* calls setLinkStatus(PENDING) directly for the eager show
|
|
44
|
+
* - Increments the navId counter for stale-clear protection
|
|
45
|
+
*
|
|
46
|
+
* Pass `null` to clear (e.g., for programmatic navigations).
|
|
47
|
+
*/
|
|
48
|
+
export declare function setLinkForCurrentNavigation(link: LinkPendingInstance | null): void;
|
|
49
|
+
/**
|
|
50
|
+
* Get the current navigation ID. Called at the start of the transition
|
|
51
|
+
* to capture the ID before async work begins.
|
|
52
|
+
*/
|
|
53
|
+
export declare function getCurrentNavId(): number;
|
|
54
|
+
/**
|
|
55
|
+
* Reset the current link's pending state to IDLE, but only if the navId
|
|
56
|
+
* matches. Called inside TransitionRoot's startTransition after the async
|
|
57
|
+
* work completes — the setter call is a transition update, so it commits
|
|
58
|
+
* atomically with the new tree.
|
|
59
|
+
*
|
|
60
|
+
* The navId guard prevents stale transitions from clearing a newer
|
|
61
|
+
* navigation's pending state. Scenario:
|
|
62
|
+
* Click A → navId=1, T1 starts
|
|
63
|
+
* Click B → navId=2, A reset to IDLE, T2 starts
|
|
64
|
+
* T1 completes → resetLinkPending(1) → navId is 2, skip ✓
|
|
65
|
+
* T2 completes → resetLinkPending(2) → navId is 2, clear ✓
|
|
66
|
+
*/
|
|
67
|
+
export declare function resetLinkPending(forNavId: number): void;
|
|
68
|
+
/**
|
|
69
|
+
* Clean up the link pending store entirely. Safety net for error paths.
|
|
70
|
+
*/
|
|
71
|
+
export declare function clearLinkPendingSetter(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Unmount a link instance from navigation tracking. Called when a Link
|
|
74
|
+
* component unmounts while it is the current navigation link. Prevents
|
|
75
|
+
* calling setState on an unmounted component.
|
|
76
|
+
*/
|
|
77
|
+
export declare function unmountLinkForCurrentNavigation(link: LinkPendingInstance): void;
|
|
78
|
+
//# sourceMappingURL=link-pending-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-pending-store.d.ts","sourceRoot":"","sources":["../../src/client/link-pending-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAIvD,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;CAC7C;AAMD,kEAAkE;AAClE,eAAO,MAAM,mBAAmB,EAAE,UAA8B,CAAC;AAEjE,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB,EAAE,UAA+B,CAAC;AAmB/D;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,GAAG,IAAI,CAWlF;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAMvD;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAK/E"}
|
package/dist/client/link.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/client/link.tsx"],"names":[],"mappings":"AAoBA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/client/link.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation-context.d.ts","sourceRoot":"","sources":["../../src/client/navigation-context.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAE,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAM7D,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAuCD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,eAAe,GAAG,IAAI,CAM7D;AAMD,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,QAAQ,GACT,EAAE,uBAAuB,GAAG,KAAK,CAAC,YAAY,CAO9C;AA6BD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAE/D;AAED,wBAAgB,kBAAkB,IAAI,eAAe,CAEpD;
|
|
1
|
+
{"version":3,"file":"navigation-context.d.ts","sourceRoot":"","sources":["../../src/client/navigation-context.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAE,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAM7D,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAuCD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,eAAe,GAAG,IAAI,CAM7D;AAMD,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,QAAQ,GACT,EAAE,uBAAuB,GAAG,KAAK,CAAC,YAAY,CAO9C;AA6BD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAE/D;AAED,wBAAgB,kBAAkB,IAAI,eAAe,CAEpD;AA8BD;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAKvD;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,GAAG,KAAK,CAAC,YAAY,CAMrB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transition-root.d.ts","sourceRoot":"","sources":["../../src/client/transition-root.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEzF,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"transition-root.d.ts","sourceRoot":"","sources":["../../src/client/transition-root.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEzF,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAsBlE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,eAAe,GAChB,EAAE;IACD,OAAO,EAAE,SAAS,CAAC;IACnB,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC,GAAG,SAAS,CA2DZ;AAID;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,GAAG,IAAI,CAIzD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,GAChC,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html-injectors.d.ts","sourceRoot":"","sources":["../../src/server/html-injectors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,GAAE,wBAA6B,GACrC,eAAe,CAAC,UAAU,EAAE,UAAU,CAAC,CAgEzC;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,IAAI,eAAe,CAAC,UAAU,EAAE,UAAU,CAAC,CAuChF;AA8ED;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,QAAQ,EAAE,MAAM,GACf,cAAc,CAAC,UAAU,CAAC,CAE5B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,WAAW,EAAE,MAAM,GAClB,cAAc,CAAC,UAAU,CAAC,CAE5B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,eAAe,CAAC,EAAE,MAAM,GACvB,cAAc,CAAC,UAAU,CAAC,CAiC5B;
|
|
1
|
+
{"version":3,"file":"html-injectors.d.ts","sourceRoot":"","sources":["../../src/server/html-injectors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,GAAE,wBAA6B,GACrC,eAAe,CAAC,UAAU,EAAE,UAAU,CAAC,CAgEzC;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,IAAI,eAAe,CAAC,UAAU,EAAE,UAAU,CAAC,CAuChF;AA8ED;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,QAAQ,EAAE,MAAM,GACf,cAAc,CAAC,UAAU,CAAC,CAE5B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,WAAW,EAAE,MAAM,GAClB,cAAc,CAAC,UAAU,CAAC,CAE5B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,eAAe,CAAC,EAAE,MAAM,GACvB,cAAc,CAAC,UAAU,CAAC,CAiC5B;AAmID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,cAAc,CAAC,UAAU,CAAC,EACtC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,SAAS,EACjD,eAAe,CAAC,EAAE,MAAM,GACvB,cAAc,CAAC,UAAU,CAAC,CAiB5B;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,qBAAqB;IACpC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;CACtB;AAqBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,OAAO,CAAA;KAAE,CAAC;IACjE,GAAG,EAAE,OAAO,CAAC;IACb,aAAa,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;CAC7D,GAAG,qBAAqB,CA8DxB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-stream-transforms.d.ts","sourceRoot":"","sources":["../../src/server/node-stream-transforms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAKxC;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,4BAAiC,GAAG,SAAS,CAgDjG;AAqBD;;;;;;GAMG;AACH,wBAAgB,6BAA6B,IAAI,SAAS,CAwCzD;AAID;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CA+ClE;AAID;;;;;;;;;;;;;;;GAeG;AACH;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,SAAS,EACjD,OAAO,CAAC,EAAE,yBAAyB,GAClC,SAAS,
|
|
1
|
+
{"version":3,"file":"node-stream-transforms.d.ts","sourceRoot":"","sources":["../../src/server/node-stream-transforms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAKxC;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,4BAAiC,GAAG,SAAS,CAgDjG;AAqBD;;;;;;GAMG;AACH,wBAAgB,6BAA6B,IAAI,SAAS,CAwCzD;AAID;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CA+ClE;AAID;;;;;;;;;;;;;;;GAeG;AACH;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,SAAS,EACjD,OAAO,CAAC,EAAE,yBAAyB,GAClC,SAAS,CAgJX;AAOD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAwBtE;AAoBD;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,cAAc,EAAE,OAAO,EACvB,eAAe,EAAE,OAAO,GACvB,SAAS,GAAG,IAAI,CA8BlB"}
|
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.55",
|
|
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",
|
|
@@ -83,11 +83,6 @@
|
|
|
83
83
|
"publishConfig": {
|
|
84
84
|
"access": "public"
|
|
85
85
|
},
|
|
86
|
-
"scripts": {
|
|
87
|
-
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
88
|
-
"typecheck": "tsgo --noEmit",
|
|
89
|
-
"prepublishOnly": "pnpm run build"
|
|
90
|
-
},
|
|
91
86
|
"dependencies": {
|
|
92
87
|
"@opentelemetry/api": "^1.9.1",
|
|
93
88
|
"@opentelemetry/context-async-hooks": "^2.6.1",
|
|
@@ -126,5 +121,9 @@
|
|
|
126
121
|
},
|
|
127
122
|
"engines": {
|
|
128
123
|
"node": ">=22.12.0"
|
|
124
|
+
},
|
|
125
|
+
"scripts": {
|
|
126
|
+
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
127
|
+
"typecheck": "tsgo --noEmit"
|
|
129
128
|
}
|
|
130
|
-
}
|
|
129
|
+
}
|
package/src/cli.ts
CHANGED
|
File without changes
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link Pending Store — module-level singleton for per-link pending state.
|
|
3
|
+
*
|
|
4
|
+
* Tracks which link instance is currently navigating so that only the
|
|
5
|
+
* clicked link shows pending. All other links remain unaffected — zero
|
|
6
|
+
* wasted re-renders.
|
|
7
|
+
*
|
|
8
|
+
* The store holds:
|
|
9
|
+
* - A reference to the currently navigating link's state setter
|
|
10
|
+
* - A navigation counter to guard against stale clears (race condition
|
|
11
|
+
* when the user clicks Link B before Link A's navigation completes)
|
|
12
|
+
*
|
|
13
|
+
* Flow:
|
|
14
|
+
* 1. Link click handler: setLinkForCurrentNavigation(instance) →
|
|
15
|
+
* resets previous link (urgent), sets new link pending (urgent),
|
|
16
|
+
* stores setter + increments navId
|
|
17
|
+
* 2. TransitionRoot startTransition: captures navId, does async work
|
|
18
|
+
* 3. TransitionRoot commit: resetLinkPending(capturedNavId) →
|
|
19
|
+
* calls setter(IDLE) inside the transition (batched, atomic with tree)
|
|
20
|
+
* Only clears if navId matches (prevents stale T1 from clearing T2's link)
|
|
21
|
+
*
|
|
22
|
+
* SINGLETON GUARANTEE: Uses `globalThis` via `Symbol.for` keys (same
|
|
23
|
+
* pattern as NavigationContext) because the RSC client bundler can
|
|
24
|
+
* duplicate this module across chunks. Module-level variables would
|
|
25
|
+
* create separate instances per chunk.
|
|
26
|
+
*
|
|
27
|
+
* See design/19-client-navigation.md §"Per-Link Pending State"
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { LinkStatus } from './use-link-status.js';
|
|
31
|
+
|
|
32
|
+
// ─── Types ───────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export interface LinkPendingInstance {
|
|
35
|
+
setLinkStatus: (status: LinkStatus) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Singleton Storage ───────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const LINK_PENDING_KEY = Symbol.for('__timber_link_pending');
|
|
41
|
+
|
|
42
|
+
/** Status object indicating link is pending — shared reference */
|
|
43
|
+
export const PENDING_LINK_STATUS: LinkStatus = { pending: true };
|
|
44
|
+
|
|
45
|
+
/** Status object indicating link is idle — shared reference */
|
|
46
|
+
export const IDLE_LINK_STATUS: LinkStatus = { pending: false };
|
|
47
|
+
|
|
48
|
+
interface LinkPendingState {
|
|
49
|
+
/** The link instance that initiated the current navigation, or null */
|
|
50
|
+
current: LinkPendingInstance | null;
|
|
51
|
+
/** Monotonically increasing counter — guards against stale clears */
|
|
52
|
+
navId: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getStore(): LinkPendingState {
|
|
56
|
+
const g = globalThis as Record<symbol, unknown>;
|
|
57
|
+
if (!g[LINK_PENDING_KEY]) {
|
|
58
|
+
g[LINK_PENDING_KEY] = { current: null, navId: 0 };
|
|
59
|
+
}
|
|
60
|
+
return g[LINK_PENDING_KEY] as LinkPendingState;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Public API ──────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Register the link instance that initiated the current navigation.
|
|
67
|
+
*
|
|
68
|
+
* Called from <Link>'s click handler before router.navigate().
|
|
69
|
+
* - Resets the previous pending link to IDLE (urgent update, immediate)
|
|
70
|
+
* - Does NOT set the new link to PENDING here — the Link's click handler
|
|
71
|
+
* calls setLinkStatus(PENDING) directly for the eager show
|
|
72
|
+
* - Increments the navId counter for stale-clear protection
|
|
73
|
+
*
|
|
74
|
+
* Pass `null` to clear (e.g., for programmatic navigations).
|
|
75
|
+
*/
|
|
76
|
+
export function setLinkForCurrentNavigation(link: LinkPendingInstance | null): void {
|
|
77
|
+
const store = getStore();
|
|
78
|
+
const prev = store.current;
|
|
79
|
+
|
|
80
|
+
// Reset previous pending link to idle (urgent update — shows immediately)
|
|
81
|
+
if (prev && prev !== link) {
|
|
82
|
+
prev.setLinkStatus(IDLE_LINK_STATUS);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
store.current = link;
|
|
86
|
+
store.navId++;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the current navigation ID. Called at the start of the transition
|
|
91
|
+
* to capture the ID before async work begins.
|
|
92
|
+
*/
|
|
93
|
+
export function getCurrentNavId(): number {
|
|
94
|
+
return getStore().navId;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reset the current link's pending state to IDLE, but only if the navId
|
|
99
|
+
* matches. Called inside TransitionRoot's startTransition after the async
|
|
100
|
+
* work completes — the setter call is a transition update, so it commits
|
|
101
|
+
* atomically with the new tree.
|
|
102
|
+
*
|
|
103
|
+
* The navId guard prevents stale transitions from clearing a newer
|
|
104
|
+
* navigation's pending state. Scenario:
|
|
105
|
+
* Click A → navId=1, T1 starts
|
|
106
|
+
* Click B → navId=2, A reset to IDLE, T2 starts
|
|
107
|
+
* T1 completes → resetLinkPending(1) → navId is 2, skip ✓
|
|
108
|
+
* T2 completes → resetLinkPending(2) → navId is 2, clear ✓
|
|
109
|
+
*/
|
|
110
|
+
export function resetLinkPending(forNavId: number): void {
|
|
111
|
+
const store = getStore();
|
|
112
|
+
if (store.navId === forNavId && store.current) {
|
|
113
|
+
store.current.setLinkStatus(IDLE_LINK_STATUS);
|
|
114
|
+
store.current = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Clean up the link pending store entirely. Safety net for error paths.
|
|
120
|
+
*/
|
|
121
|
+
export function clearLinkPendingSetter(): void {
|
|
122
|
+
const store = getStore();
|
|
123
|
+
store.current = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Unmount a link instance from navigation tracking. Called when a Link
|
|
128
|
+
* component unmounts while it is the current navigation link. Prevents
|
|
129
|
+
* calling setState on an unmounted component.
|
|
130
|
+
*/
|
|
131
|
+
export function unmountLinkForCurrentNavigation(link: LinkPendingInstance): void {
|
|
132
|
+
const store = getStore();
|
|
133
|
+
if (store.current === link) {
|
|
134
|
+
store.current = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/client/link.tsx
CHANGED
|
@@ -18,12 +18,26 @@
|
|
|
18
18
|
// - params and fully-resolved string href are mutually exclusive
|
|
19
19
|
// - searchParams and inline query string are mutually exclusive
|
|
20
20
|
|
|
21
|
-
import
|
|
21
|
+
import {
|
|
22
|
+
useState,
|
|
23
|
+
useEffect,
|
|
24
|
+
useRef,
|
|
25
|
+
type AnchorHTMLAttributes,
|
|
26
|
+
type ReactNode,
|
|
27
|
+
type MouseEvent as ReactMouseEvent,
|
|
28
|
+
} from 'react';
|
|
22
29
|
import type { SearchParamsDefinition } from '#/search-params/define.js';
|
|
23
|
-
import {
|
|
30
|
+
import { LinkStatusContext } from './use-link-status.js';
|
|
24
31
|
import { getRouterOrNull } from './router-ref.js';
|
|
25
32
|
import { getSsrData } from './ssr-data.js';
|
|
26
33
|
import { mergePreservedSearchParams } from '#/shared/merge-search-params.js';
|
|
34
|
+
import {
|
|
35
|
+
setLinkForCurrentNavigation,
|
|
36
|
+
unmountLinkForCurrentNavigation,
|
|
37
|
+
IDLE_LINK_STATUS,
|
|
38
|
+
PENDING_LINK_STATUS,
|
|
39
|
+
type LinkPendingInstance,
|
|
40
|
+
} from './link-pending-store.js';
|
|
27
41
|
|
|
28
42
|
// ─── Current Search Params ────────────────────────────────────────
|
|
29
43
|
|
|
@@ -353,6 +367,38 @@ export function Link({
|
|
|
353
367
|
}: LinkProps) {
|
|
354
368
|
const { href: baseHref } = buildLinkProps({ href, params: segmentParams, searchParams });
|
|
355
369
|
|
|
370
|
+
// ─── Per-link pending state (useState) ────────────────────────
|
|
371
|
+
// Each Link has its own pending state. Only the clicked link's
|
|
372
|
+
// setter is invoked during navigation — zero other links re-render.
|
|
373
|
+
//
|
|
374
|
+
// Eager show: click handler calls setLinkStatus(PENDING) directly (urgent).
|
|
375
|
+
// Atomic clear: TransitionRoot calls resetLinkPending(navId) inside
|
|
376
|
+
// startTransition — batched with the new tree commit.
|
|
377
|
+
//
|
|
378
|
+
// See design/19-client-navigation.md §"Per-Link Pending State"
|
|
379
|
+
const [linkStatus, setLinkStatus] = useState(IDLE_LINK_STATUS);
|
|
380
|
+
|
|
381
|
+
// Build the link instance ref for the pending store.
|
|
382
|
+
// The ref is stable across renders — we update the setter on each
|
|
383
|
+
// render to keep it current.
|
|
384
|
+
const linkInstanceRef = useRef<LinkPendingInstance | null>(null);
|
|
385
|
+
if (!linkInstanceRef.current) {
|
|
386
|
+
linkInstanceRef.current = { setLinkStatus };
|
|
387
|
+
} else {
|
|
388
|
+
linkInstanceRef.current.setLinkStatus = setLinkStatus;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Clean up if this link unmounts while it's the current navigation link.
|
|
392
|
+
// Prevents calling setOptimistic on an unmounted component.
|
|
393
|
+
useEffect(() => {
|
|
394
|
+
const instance = linkInstanceRef.current;
|
|
395
|
+
return () => {
|
|
396
|
+
if (instance) {
|
|
397
|
+
unmountLinkForCurrentNavigation(instance);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}, []);
|
|
401
|
+
|
|
356
402
|
// Preserve search params from the current URL when requested.
|
|
357
403
|
// useSearchParams() works during both SSR (reads from request context)
|
|
358
404
|
// and on the client (reads from window.location, reactive to URL changes).
|
|
@@ -401,6 +447,16 @@ export function Link({
|
|
|
401
447
|
const navHref = preserveSearchParams
|
|
402
448
|
? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)
|
|
403
449
|
: resolvedHref;
|
|
450
|
+
|
|
451
|
+
// Eagerly show pending state on this link (urgent update, immediate).
|
|
452
|
+
// Only this Link re-renders — all other Links are unaffected.
|
|
453
|
+
setLinkStatus(PENDING_LINK_STATUS);
|
|
454
|
+
|
|
455
|
+
// Register this link in the pending store so TransitionRoot can
|
|
456
|
+
// reset it to IDLE inside startTransition (atomic with new tree).
|
|
457
|
+
// Also resets any previous pending link to IDLE.
|
|
458
|
+
setLinkForCurrentNavigation(linkInstanceRef.current);
|
|
459
|
+
|
|
404
460
|
void router.navigate(navHref, { scroll: shouldScroll });
|
|
405
461
|
}
|
|
406
462
|
: userOnClick; // External links — just pass through user's onClick
|
|
@@ -423,7 +479,7 @@ export function Link({
|
|
|
423
479
|
|
|
424
480
|
return (
|
|
425
481
|
<a {...rest} href={resolvedHref} onClick={handleClick} onMouseEnter={handleMouseEnter}>
|
|
426
|
-
<
|
|
482
|
+
<LinkStatusContext.Provider value={linkStatus}>{children}</LinkStatusContext.Provider>
|
|
427
483
|
</a>
|
|
428
484
|
);
|
|
429
485
|
}
|
|
@@ -63,7 +63,7 @@ export interface NavigationState {
|
|
|
63
63
|
* variables) because the ESM bundler can duplicate this module across
|
|
64
64
|
* chunks. Module-level variables would create separate instances per
|
|
65
65
|
* chunk — the provider in TransitionRoot (index chunk) would use
|
|
66
|
-
* context A while the consumer in
|
|
66
|
+
* context A while the consumer in useNavigationPending (shared chunk)
|
|
67
67
|
* reads from context B. globalThis guarantees a single instance.
|
|
68
68
|
*
|
|
69
69
|
* See design/27-chunking-strategy.md §"Singleton Safety"
|
|
@@ -168,8 +168,9 @@ export function getNavigationState(): NavigationState {
|
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
170
|
* Separate context for the in-flight navigation URL. Provided by
|
|
171
|
-
* TransitionRoot (urgent useState), consumed by
|
|
172
|
-
* and
|
|
171
|
+
* TransitionRoot (urgent useState), consumed by useNavigationPending
|
|
172
|
+
* and TopLoader. Per-link pending state uses useOptimistic instead
|
|
173
|
+
* (see link-pending-store.ts).
|
|
173
174
|
*
|
|
174
175
|
* Uses globalThis via Symbol.for for the same reason as NavigationContext
|
|
175
176
|
* above — the bundler may duplicate this module across chunks, and module-
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { useState, useTransition, createElement, Fragment, type ReactNode } from 'react';
|
|
26
26
|
import { PendingNavigationProvider } from './navigation-context.js';
|
|
27
27
|
import { TopLoader, type TopLoaderConfig } from './top-loader.js';
|
|
28
|
+
import { getCurrentNavId, resetLinkPending } from './link-pending-store.js';
|
|
28
29
|
|
|
29
30
|
// ─── Module-level functions ──────────────────────────────────────
|
|
30
31
|
|
|
@@ -88,20 +89,31 @@ export function TransitionRoot({
|
|
|
88
89
|
// both apply in the same React commit — making the pending→active transition
|
|
89
90
|
// atomic (no frame where pending is false but the old tree is still visible).
|
|
90
91
|
_navigateTransition = (url: string, perform: () => Promise<ReactNode>) => {
|
|
91
|
-
// Urgent: show pending state immediately
|
|
92
|
+
// Urgent: show pending state immediately (for TopLoader / useNavigationPending)
|
|
92
93
|
setPendingUrl(url);
|
|
93
94
|
|
|
94
95
|
return new Promise<void>((resolve, reject) => {
|
|
95
96
|
startTransition(async () => {
|
|
97
|
+
// Capture the current nav ID before async work begins.
|
|
98
|
+
// Used to guard against stale clears when a newer navigation
|
|
99
|
+
// supersedes this one.
|
|
100
|
+
const navId = getCurrentNavId();
|
|
96
101
|
try {
|
|
97
102
|
const newElement = await perform();
|
|
98
103
|
setElement(newElement);
|
|
99
104
|
// Clear pending inside the transition — commits atomically with new tree
|
|
100
105
|
setPendingUrl(null);
|
|
106
|
+
// Reset per-link pending state. The navId guard ensures a stale
|
|
107
|
+
// transition (T1) doesn't clear a newer navigation's (T2) link.
|
|
108
|
+
// The setter call is a transition update — batched with setElement
|
|
109
|
+
// and setPendingUrl, so pending clears atomically with new tree.
|
|
110
|
+
// See design/19-client-navigation.md §"Per-Link Pending State"
|
|
111
|
+
resetLinkPending(navId);
|
|
101
112
|
resolve();
|
|
102
113
|
} catch (err) {
|
|
103
114
|
// Clear pending on error too
|
|
104
115
|
setPendingUrl(null);
|
|
116
|
+
resetLinkPending(navId);
|
|
105
117
|
reject(err);
|
|
106
118
|
}
|
|
107
119
|
});
|
|
@@ -351,8 +351,14 @@ function createFlightInjectionTransform(
|
|
|
351
351
|
let pullPromise: Promise<void> | null = null;
|
|
352
352
|
|
|
353
353
|
// RSC script chunks waiting to be drained at a safe boundary.
|
|
354
|
+
// pullLoop buffers here; transform(), flush(), and pullLoop's
|
|
355
|
+
// post-yield drain all consume from this buffer.
|
|
354
356
|
const pending: Uint8Array[] = [];
|
|
355
357
|
|
|
358
|
+
// Controller reference — set on first transform() call so pullLoop
|
|
359
|
+
// can drain pending scripts between HTML chunks (after yielding).
|
|
360
|
+
let _controller: TransformStreamDefaultController<Uint8Array> | null = null;
|
|
361
|
+
|
|
356
362
|
async function pullLoop(): Promise<void> {
|
|
357
363
|
// Yield once so the first HTML shell chunk flows through
|
|
358
364
|
// transform() before we start reading RSC data.
|
|
@@ -376,6 +382,14 @@ function createFlightInjectionTransform(
|
|
|
376
382
|
// Once flush() fires, drain without yielding.
|
|
377
383
|
if (!isHtmlDone(machine.state)) {
|
|
378
384
|
await new Promise<void>((r) => setImmediate(r));
|
|
385
|
+
// After yielding, drain pending scripts. This ensures RSC
|
|
386
|
+
// flight data reaches the client at shell-flush time — without
|
|
387
|
+
// this, hydration blocks waiting for data stuck in pending[].
|
|
388
|
+
// Safe because the upstream buffered transform (TIM-528)
|
|
389
|
+
// guarantees chunks end at tag boundaries.
|
|
390
|
+
if (pending.length > 0 && _controller) {
|
|
391
|
+
drainPending(_controller);
|
|
392
|
+
}
|
|
379
393
|
}
|
|
380
394
|
}
|
|
381
395
|
} catch (err) {
|
|
@@ -398,6 +412,8 @@ function createFlightInjectionTransform(
|
|
|
398
412
|
|
|
399
413
|
return new TransformStream<Uint8Array, Uint8Array>({
|
|
400
414
|
transform(chunk, controller) {
|
|
415
|
+
_controller = controller;
|
|
416
|
+
|
|
401
417
|
if (machine.state.phase === 'init') {
|
|
402
418
|
machine.send({ type: 'FIRST_CHUNK' });
|
|
403
419
|
pullPromise = pullLoop();
|
|
@@ -285,16 +285,10 @@ export function createNodeFlightInjector(
|
|
|
285
285
|
|
|
286
286
|
// RSC script chunks waiting to be drained at a safe boundary.
|
|
287
287
|
// pullLoop buffers here; transform() and flush() drain.
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
288
|
+
// RSC script chunks waiting to be drained at a safe boundary.
|
|
289
|
+
// pullLoop buffers here; transform(), flush(), and pullLoop's
|
|
290
|
+
// post-yield drain all consume from this buffer.
|
|
291
291
|
const pending: Buffer[] = [];
|
|
292
|
-
|
|
293
|
-
// pullLoop reads RSC chunks and buffers them as <script> tags in
|
|
294
|
-
// pending[]. It does NOT push directly to the transform output —
|
|
295
|
-
// that would cause scripts to interleave at arbitrary byte
|
|
296
|
-
// boundaries within HTML chunks (TIM-527). Pending scripts are
|
|
297
|
-
// drained only from transform() or flush().
|
|
298
292
|
async function pullLoop(): Promise<void> {
|
|
299
293
|
// Yield once so the first transform() call can process the shell
|
|
300
294
|
// HTML chunk before we start reading RSC data.
|
|
@@ -315,13 +309,24 @@ export function createNodeFlightInjector(
|
|
|
315
309
|
}
|
|
316
310
|
const decoded = decoder.decode(value, { stream: true });
|
|
317
311
|
const scriptBuf = Buffer.from(flightChunkScript(decoded), 'utf-8');
|
|
318
|
-
// Buffer the script — drained by the next transform()
|
|
312
|
+
// Buffer the script — drained by the next transform() call,
|
|
313
|
+
// flush(), or by the scheduled drain below.
|
|
319
314
|
pending.push(scriptBuf);
|
|
320
|
-
// Yield between reads so HTML chunks get
|
|
321
|
-
//
|
|
322
|
-
//
|
|
315
|
+
// Yield between reads so HTML chunks get priority in the event
|
|
316
|
+
// loop — but only while HTML is still streaming. Once flush()
|
|
317
|
+
// fires, read without yielding to drain remaining RSC data.
|
|
323
318
|
if (!isHtmlDone(machine.state)) {
|
|
324
319
|
await new Promise<void>((r) => setImmediate(r));
|
|
320
|
+
// After yielding, if no transform() call drained the buffer
|
|
321
|
+
// (i.e., no new HTML chunk arrived), drain now. This ensures
|
|
322
|
+
// RSC flight data reaches the client at shell-flush time —
|
|
323
|
+
// without this, hydration blocks on createFromReadableStream
|
|
324
|
+
// waiting for data that's stuck in pending[].
|
|
325
|
+
// This is safe: we're between event loop ticks, so no
|
|
326
|
+
// transform() call is mid-execution (no mid-tag risk).
|
|
327
|
+
if (pending.length > 0) {
|
|
328
|
+
drainPending();
|
|
329
|
+
}
|
|
325
330
|
}
|
|
326
331
|
}
|
|
327
332
|
} catch (err) {
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
|
-
/**
|
|
3
|
-
* Client component that reads the pending URL from PendingNavigationContext
|
|
4
|
-
* and provides a scoped LinkStatusContext to children. Renders no extra DOM —
|
|
5
|
-
* just a context provider around children.
|
|
6
|
-
*/
|
|
7
|
-
export declare function LinkStatusProvider({ href, children }: {
|
|
8
|
-
href: string;
|
|
9
|
-
children?: ReactNode;
|
|
10
|
-
}): import("react/jsx-runtime").JSX.Element;
|
|
11
|
-
//# sourceMappingURL=link-status-provider.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"link-status-provider.d.ts","sourceRoot":"","sources":["../../src/client/link-status-provider.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAOvC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,2CAK5F"}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
// LinkStatusProvider — client component that provides per-link pending status
|
|
4
|
-
// via React context. Used inside <Link> to power useLinkStatus().
|
|
5
|
-
//
|
|
6
|
-
// Reads pendingUrl from PendingNavigationContext (provided by TransitionRoot).
|
|
7
|
-
// The pending URL is set as an URGENT update at navigation start (shows
|
|
8
|
-
// immediately) and cleared inside startTransition when the new tree commits
|
|
9
|
-
// (atomic with params/pathname). This eliminates both:
|
|
10
|
-
// 1. The delay before showing the spinner (urgent update, not deferred)
|
|
11
|
-
// 2. The gap between spinner disappearing and active state updating (same commit)
|
|
12
|
-
|
|
13
|
-
import type { ReactNode } from 'react';
|
|
14
|
-
import { LinkStatusContext, type LinkStatus } from './use-link-status.js';
|
|
15
|
-
import { usePendingNavigationUrl } from './navigation-context.js';
|
|
16
|
-
|
|
17
|
-
const NOT_PENDING: LinkStatus = { pending: false };
|
|
18
|
-
const IS_PENDING: LinkStatus = { pending: true };
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Client component that reads the pending URL from PendingNavigationContext
|
|
22
|
-
* and provides a scoped LinkStatusContext to children. Renders no extra DOM —
|
|
23
|
-
* just a context provider around children.
|
|
24
|
-
*/
|
|
25
|
-
export function LinkStatusProvider({ href, children }: { href: string; children?: ReactNode }) {
|
|
26
|
-
const pendingUrl = usePendingNavigationUrl();
|
|
27
|
-
const status = pendingUrl === href ? IS_PENDING : NOT_PENDING;
|
|
28
|
-
|
|
29
|
-
return <LinkStatusContext.Provider value={status}>{children}</LinkStatusContext.Provider>;
|
|
30
|
-
}
|