@octanejs/tanstack-router 0.1.4 → 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.
- package/package.json +10 -4
- package/src/router.ts +95 -0
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/tanstack-router",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
6
9
|
"octane": {
|
|
7
10
|
"hookSlots": {
|
|
8
11
|
"manual": [
|
|
@@ -37,8 +40,10 @@
|
|
|
37
40
|
"dependencies": {
|
|
38
41
|
"@tanstack/history": "1.162.0",
|
|
39
42
|
"@tanstack/router-core": "1.171.13",
|
|
40
|
-
"@tanstack/store": "^0.9.3"
|
|
41
|
-
|
|
43
|
+
"@tanstack/store": "^0.9.3"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"octane": "0.1.9"
|
|
42
47
|
},
|
|
43
48
|
"devDependencies": {
|
|
44
49
|
"@tanstack/react-router": "1.170.16",
|
|
@@ -46,7 +51,8 @@
|
|
|
46
51
|
"esbuild": "^0.28.1",
|
|
47
52
|
"react": "^19.2.0",
|
|
48
53
|
"react-dom": "^19.2.0",
|
|
49
|
-
"vitest": "^4.1.9"
|
|
54
|
+
"vitest": "^4.1.9",
|
|
55
|
+
"octane": "0.1.9"
|
|
50
56
|
},
|
|
51
57
|
"scripts": {
|
|
52
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
|
|