@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.
@@ -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"}
@@ -1,4 +1,4 @@
1
- import type { AnchorHTMLAttributes, ReactNode } from 'react';
1
+ import { type AnchorHTMLAttributes, type ReactNode } from 'react';
2
2
  import type { SearchParamsDefinition } from '#/search-params/define.js';
3
3
  export type OnNavigateEvent = {
4
4
  preventDefault: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/client/link.tsx"],"names":[],"mappings":"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"}
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;AA6BD;;;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
+ {"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;AAqBlE;;;;;;;;;;;;;;;;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,CAgDZ;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
+ {"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;AAmHD;;;;;;;;;;;;;;;;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
+ {"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,CA2IX;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"}
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.54",
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
+ }
@@ -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 type { AnchorHTMLAttributes, ReactNode, MouseEvent as ReactMouseEvent } from 'react';
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 { LinkStatusProvider } from './link-status-provider.js';
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
- <LinkStatusProvider href={resolvedHref}>{children}</LinkStatusProvider>
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 LinkStatusProvider (shared chunk)
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 LinkStatusProvider
172
- * and useNavigationPending.
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
- // Scripts are NEVER pushed directly from pullLoop they are only
289
- // emitted from transform() (after a complete HTML chunk) or flush().
290
- // This guarantees scripts never land mid-tag. See TIM-527/TIM-529.
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() or flush().
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 a chance to flow
321
- // through transform() first — but only while HTML is still
322
- // streaming. Once flush() fires, drain without yielding.
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
- }