@octanejs/tanstack-router 0.1.5 → 0.1.8

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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/router.ts +95 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/tanstack-router",
3
- "version": "0.1.5",
3
+ "version": "0.1.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -43,7 +43,7 @@
43
43
  "@tanstack/store": "^0.9.3"
44
44
  },
45
45
  "peerDependencies": {
46
- "octane": "0.1.6"
46
+ "octane": "0.1.9"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@tanstack/react-router": "1.170.16",
@@ -52,7 +52,7 @@
52
52
  "react": "^19.2.0",
53
53
  "react-dom": "^19.2.0",
54
54
  "vitest": "^4.1.9",
55
- "octane": "0.1.6"
55
+ "octane": "0.1.9"
56
56
  },
57
57
  "scripts": {
58
58
  "test": "vitest run"
package/src/router.ts CHANGED
@@ -55,6 +55,101 @@ const octaneStoreFactory = (opts: { isServer?: boolean }) => {
55
55
  export class Router extends (RouterCore as any) {
56
56
  constructor(options: any) {
57
57
  super(options, octaneStoreFactory);
58
+
59
+ // router-core starts the resolved-match commit through startViewTransition,
60
+ // whose browser callback may run after router-core's load promise resolves.
61
+ // Track those callbacks so `await router.load()` is a real render-readiness
62
+ // boundary: the active match tree is committed before a consumer's first
63
+ // render or hydration pass.
64
+ const coreLoad = this.load.bind(this);
65
+ const coreStartViewTransition = this.startViewTransition.bind(this);
66
+ const pendingViewCommits = new Set<Promise<void>>();
67
+ const activeLoadScopes = new Set<Set<Promise<void>>>();
68
+
69
+ this.startViewTransition = (fn: () => Promise<void>) => {
70
+ let resolveCommit!: () => void;
71
+ let rejectCommit!: (error: unknown) => void;
72
+ const commit = new Promise<void>((resolve, reject) => {
73
+ resolveCommit = resolve;
74
+ rejectCommit = reject;
75
+ });
76
+ pendingViewCommits.add(commit);
77
+ for (const scope of activeLoadScopes) scope.add(commit);
78
+ // Keep only unsettled callbacks globally. A later load waits for a prior
79
+ // callback so no mutation can land after its readiness boundary, but only
80
+ // the active scopes above own (and therefore propagate) this failure.
81
+ void commit.then(
82
+ () => pendingViewCommits.delete(commit),
83
+ () => pendingViewCommits.delete(commit),
84
+ );
85
+
86
+ const runCommit = async () => {
87
+ try {
88
+ await fn();
89
+ resolveCommit();
90
+ } catch (error) {
91
+ rejectCommit(error);
92
+ }
93
+ };
94
+
95
+ try {
96
+ coreStartViewTransition(runCommit);
97
+ } catch (error) {
98
+ rejectCommit(error);
99
+ throw error;
100
+ }
101
+ };
102
+
103
+ this.load = async (...args: any[]) => {
104
+ const prerequisiteCommits = new Set(pendingViewCommits);
105
+ const viewCommits = new Set<Promise<void>>();
106
+ activeLoadScopes.add(viewCommits);
107
+ let hasLoadError = false;
108
+ let loadError: unknown;
109
+ let result: unknown;
110
+ try {
111
+ result = await coreLoad(...args);
112
+ } catch (error) {
113
+ hasLoadError = true;
114
+ loadError = error;
115
+ } finally {
116
+ // All commits started by this core load are now registered. Stop
117
+ // accepting commits from later navigations before awaiting this scope.
118
+ activeLoadScopes.delete(viewCommits);
119
+ }
120
+
121
+ let hasCommitError = false;
122
+ let commitError: unknown;
123
+ const [, outcomes] = await Promise.all([
124
+ Promise.allSettled(prerequisiteCommits),
125
+ Promise.allSettled(viewCommits),
126
+ ]);
127
+ const rejected = outcomes.find((outcome) => outcome.status === 'rejected');
128
+ if (rejected?.status === 'rejected') {
129
+ hasCommitError = true;
130
+ commitError = rejected.reason;
131
+ }
132
+ if (hasLoadError) throw loadError;
133
+ if (hasCommitError) throw commitError;
134
+
135
+ // RouterCore derives the final HTTP status immediately after its internal
136
+ // load promise resolves. A platform-deferred View Transition can commit the
137
+ // new tree only after that point, so finalize every branch once the
138
+ // render-ready tree is present. RouterCore has already committed a
139
+ // redirect and its HTTP status together, so preserve that authoritative
140
+ // status; otherwise a successful tree must also clear a stale 404/500.
141
+ const state = this.state;
142
+ const statusCode =
143
+ state.redirect != null
144
+ ? state.statusCode
145
+ : this.hasNotFoundMatch()
146
+ ? 404
147
+ : state.matches.some((match: any) => match.status === 'error')
148
+ ? 500
149
+ : 200;
150
+ this.stores.statusCode.set(statusCode);
151
+ return result;
152
+ };
58
153
  }
59
154
  }
60
155