@kasperrt/smolter 0.0.1 → 0.3.0
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/{src/internal → .generated}/client/index.ts +1 -0
- package/.generated/core/environment.ts +43 -0
- package/{src/internal → .generated}/core/errors.ts +1 -1
- package/{src/internal → .generated}/core/index.ts +2 -0
- package/.generated/core/navigation-router.ts +107 -0
- package/.generated/core/navigation.ts +147 -0
- package/.generated/core/presentation.ts +108 -0
- package/.generated/core/resource-types.ts +44 -0
- package/.generated/core/resources.ts +444 -0
- package/.generated/core/responses.ts +86 -0
- package/{src/internal → .generated}/core/router.ts +136 -63
- package/.generated/core/serialize.ts +20 -0
- package/.generated/core/store.ts +25 -0
- package/.generated/core/submission.ts +116 -0
- package/{src/internal → .generated}/core/types.ts +18 -1
- package/.generated/core/url.ts +29 -0
- package/.generated/core/utils.ts +5 -0
- package/.generated/preact/await.tsx +41 -0
- package/.generated/preact/client.tsx +93 -0
- package/.generated/preact/context.ts +59 -0
- package/.generated/preact/document.tsx +198 -0
- package/{src/internal → .generated}/preact/fetcher.ts +43 -23
- package/.generated/preact/form.ts +58 -0
- package/{src/internal → .generated}/preact/hooks.ts +25 -28
- package/.generated/preact/index.ts +34 -0
- package/.generated/preact/lazy-route.ts +100 -0
- package/.generated/preact/navigation-state.ts +9 -0
- package/.generated/preact/navigation.tsx +147 -0
- package/.generated/preact/root-context.ts +21 -0
- package/.generated/preact/root-data.ts +45 -0
- package/.generated/preact/route-hooks.tsx +170 -0
- package/.generated/preact/route-types.ts +76 -0
- package/.generated/preact/router.tsx +107 -0
- package/{src/internal → .generated}/preact/routes.ts +17 -9
- package/.generated/react/context.ts +59 -0
- package/.generated/react/data.ts +25 -0
- package/{src/internal → .generated}/react/fetcher.ts +13 -11
- package/.generated/react/form.ts +58 -0
- package/{src/internal → .generated}/react/hooks.ts +9 -14
- package/{src/internal/preact → .generated/react}/index.ts +5 -1
- package/{src/internal → .generated}/react/link.ts +13 -21
- package/.generated/react/resources/components.tsx +404 -0
- package/{src/internal → .generated}/react/routes.ts +17 -9
- package/.generated/server/application.tsx +185 -0
- package/.generated/server/assets.ts +19 -0
- package/.generated/server/handler.ts +69 -0
- package/.generated/server/http.ts +55 -0
- package/{src/internal → .generated}/server/index.ts +4 -17
- package/.generated/server/node.ts +4 -0
- package/.generated/server/preact.ts +7 -0
- package/.generated/vite/application/browser-boundary.ts +42 -0
- package/.generated/vite/application/lazy-files.ts +136 -0
- package/.generated/vite/application/vite.ts +144 -0
- package/{src/internal → .generated}/vite/index.ts +4 -2
- package/.generated/vite/preact.ts +24 -0
- package/.generated/vite/react.ts +18 -0
- package/{src/internal → .generated}/vite/transport.ts +2 -1
- package/CHANGELOG.md +54 -0
- package/README.md +269 -55
- package/dist/client/deserialize.js +10 -0
- package/dist/{internal/client → client}/index.d.ts +1 -0
- package/dist/client/index.js +46 -0
- package/dist/core/environment.d.ts +12 -0
- package/dist/core/environment.js +23 -0
- package/dist/core/errors.js +7 -0
- package/dist/{internal/core → core}/index.d.ts +2 -0
- package/dist/core/match.js +33 -0
- package/dist/core/navigation-router.d.ts +15 -0
- package/dist/core/navigation-router.js +64 -0
- package/dist/core/navigation.d.ts +15 -0
- package/dist/core/navigation.js +92 -0
- package/dist/core/presentation.d.ts +5 -0
- package/dist/core/presentation.js +69 -0
- package/dist/core/resource-types.d.ts +37 -0
- package/dist/core/resources.d.ts +19 -0
- package/dist/core/resources.js +345 -0
- package/dist/core/responses.d.ts +15 -0
- package/dist/core/responses.js +48 -0
- package/dist/{internal/core → core}/router.d.ts +1 -1
- package/dist/core/router.js +204 -0
- package/dist/core/serialize.d.ts +5 -0
- package/dist/core/serialize.js +17 -0
- package/dist/core/store.d.ts +6 -0
- package/dist/core/store.js +21 -0
- package/dist/core/submission.d.ts +30 -0
- package/dist/core/submission.js +41 -0
- package/dist/{internal/core → core}/types.d.ts +16 -1
- package/dist/core/url.d.ts +7 -0
- package/dist/core/url.js +12 -0
- package/dist/core/utils.d.ts +5 -0
- package/dist/core/utils.js +5 -0
- package/dist/files/index.js +64 -0
- package/dist/preact/await.d.ts +7 -0
- package/dist/preact/await.js +19 -0
- package/dist/preact/client.d.ts +7 -0
- package/dist/preact/client.js +67 -0
- package/dist/preact/context.d.ts +18 -0
- package/dist/preact/context.js +23 -0
- package/dist/preact/document.d.ts +15 -0
- package/dist/preact/document.js +155 -0
- package/dist/{internal/preact → preact}/fetcher.d.ts +9 -5
- package/dist/preact/fetcher.js +45 -0
- package/dist/{internal/preact → preact}/form.d.ts +5 -6
- package/dist/preact/form.js +21 -0
- package/dist/{internal/preact → preact}/hooks.d.ts +7 -6
- package/dist/preact/hooks.js +25 -0
- package/dist/preact/index.d.ts +18 -0
- package/dist/preact/index.js +15 -0
- package/dist/preact/lazy-route.d.ts +19 -0
- package/dist/preact/lazy-route.js +51 -0
- package/dist/preact/navigation-state.d.ts +2 -0
- package/dist/preact/navigation-state.js +9 -0
- package/dist/preact/navigation.d.ts +28 -0
- package/dist/preact/navigation.js +60 -0
- package/dist/preact/root-context.d.ts +13 -0
- package/dist/preact/root-context.js +9 -0
- package/dist/preact/root-data.d.ts +8 -0
- package/dist/preact/root-data.js +28 -0
- package/dist/preact/route-hooks.d.ts +32 -0
- package/dist/preact/route-hooks.js +94 -0
- package/dist/preact/route-types.d.ts +54 -0
- package/dist/preact/router.d.ts +16 -0
- package/dist/preact/router.js +63 -0
- package/dist/{internal/preact → preact}/routes.d.ts +1 -1
- package/dist/preact/routes.js +12 -0
- package/dist/{internal/preact → preact}/runtime.js +1 -1
- package/dist/react/context.d.ts +18 -0
- package/dist/react/context.js +23 -0
- package/dist/react/data.d.ts +5 -0
- package/dist/react/data.js +3 -0
- package/dist/{internal/react → react}/fetcher.d.ts +2 -2
- package/dist/react/fetcher.js +34 -0
- package/dist/{internal/react → react}/form.d.ts +5 -6
- package/dist/react/form.js +21 -0
- package/dist/react/hooks.js +25 -0
- package/dist/{internal/react → react}/index.d.ts +1 -1
- package/dist/react/index.js +10 -0
- package/dist/{internal/react → react}/link.d.ts +2 -2
- package/dist/react/link.js +27 -0
- package/dist/react/resources/components.d.ts +64 -0
- package/dist/react/resources/components.js +239 -0
- package/dist/{internal/react → react}/routes.d.ts +1 -1
- package/dist/react/routes.js +12 -0
- package/dist/react/runtime.js +1 -0
- package/dist/server/application.d.ts +4 -0
- package/dist/server/application.js +119 -0
- package/dist/server/assets.d.ts +5 -0
- package/dist/server/assets.js +7 -0
- package/dist/server/handler.d.ts +19 -0
- package/dist/server/handler.js +22 -0
- package/dist/server/http.d.ts +4 -0
- package/dist/server/http.js +27 -0
- package/dist/{internal/server → server}/index.d.ts +2 -3
- package/dist/server/index.js +76 -0
- package/dist/server/node.d.ts +3 -0
- package/dist/server/node.js +3 -0
- package/dist/server/preact.d.ts +3 -0
- package/dist/server/preact.js +4 -0
- package/dist/vite/application/browser-boundary.d.ts +3 -0
- package/dist/vite/application/browser-boundary.js +35 -0
- package/dist/vite/application/lazy-files.d.ts +2 -0
- package/dist/vite/application/lazy-files.js +87 -0
- package/dist/vite/application/vite.d.ts +9 -0
- package/dist/vite/application/vite.js +101 -0
- package/dist/{internal/vite → vite}/index.d.ts +2 -1
- package/dist/vite/index.js +104 -0
- package/dist/vite/preact.d.ts +11 -0
- package/dist/vite/preact.js +15 -0
- package/dist/vite/react.d.ts +9 -0
- package/dist/vite/react.js +14 -0
- package/dist/{internal/vite → vite}/transport.d.ts +1 -1
- package/dist/vite/transport.js +35 -0
- package/jsr.json +28 -11
- package/package.json +119 -38
- package/dist/client.d.ts +0 -2
- package/dist/client.js +0 -2
- package/dist/core.d.ts +0 -2
- package/dist/core.js +0 -2
- package/dist/files.d.ts +0 -2
- package/dist/files.js +0 -2
- package/dist/internal/client/deserialize.js +0 -11
- package/dist/internal/client/index.js +0 -44
- package/dist/internal/core/errors.js +0 -6
- package/dist/internal/core/index.js +0 -6
- package/dist/internal/core/match.js +0 -27
- package/dist/internal/core/router.js +0 -189
- package/dist/internal/core/types.js +0 -5
- package/dist/internal/core/url.d.ts +0 -2
- package/dist/internal/core/url.js +0 -6
- package/dist/internal/files/index.js +0 -77
- package/dist/internal/preact/context.d.ts +0 -18
- package/dist/internal/preact/context.js +0 -20
- package/dist/internal/preact/fetcher.js +0 -28
- package/dist/internal/preact/form.js +0 -41
- package/dist/internal/preact/hooks.js +0 -35
- package/dist/internal/preact/index.d.ts +0 -9
- package/dist/internal/preact/index.js +0 -7
- package/dist/internal/preact/link.d.ts +0 -12
- package/dist/internal/preact/link.js +0 -28
- package/dist/internal/preact/routes.js +0 -22
- package/dist/internal/react/context.d.ts +0 -18
- package/dist/internal/react/context.js +0 -20
- package/dist/internal/react/fetcher.js +0 -28
- package/dist/internal/react/form.js +0 -41
- package/dist/internal/react/hooks.js +0 -35
- package/dist/internal/react/index.js +0 -7
- package/dist/internal/react/link.js +0 -28
- package/dist/internal/react/routes.js +0 -22
- package/dist/internal/react/runtime.js +0 -2
- package/dist/internal/server/index.js +0 -81
- package/dist/internal/vite/index.js +0 -132
- package/dist/internal/vite/transport.js +0 -42
- package/dist/preact.d.ts +0 -2
- package/dist/preact.js +0 -2
- package/dist/react.d.ts +0 -2
- package/dist/react.js +0 -2
- package/dist/server.d.ts +0 -2
- package/dist/server.js +0 -2
- package/dist/vite.d.ts +0 -2
- package/dist/vite.js +0 -2
- package/src/client.ts +0 -2
- package/src/core.ts +0 -2
- package/src/files.ts +0 -2
- package/src/internal/core/url.ts +0 -6
- package/src/internal/preact/context.ts +0 -46
- package/src/internal/preact/form.ts +0 -79
- package/src/internal/preact/link.ts +0 -46
- package/src/internal/react/context.ts +0 -46
- package/src/internal/react/form.ts +0 -79
- package/src/internal/react/index.ts +0 -13
- package/src/preact.ts +0 -2
- package/src/react.ts +0 -2
- package/src/server.ts +0 -2
- package/src/vite.ts +0 -2
- /package/{src/internal → .generated}/client/deserialize.ts +0 -0
- /package/{src/internal → .generated}/core/match.ts +0 -0
- /package/{src/internal → .generated}/files/index.ts +0 -0
- /package/{src/internal → .generated}/preact/runtime.ts +0 -0
- /package/{src/internal → .generated}/react/runtime.ts +0 -0
- /package/dist/{internal/client → client}/deserialize.d.ts +0 -0
- /package/dist/{internal/core → core}/errors.d.ts +0 -0
- /package/dist/{internal/core → core}/match.d.ts +0 -0
- /package/dist/{internal/files → files}/index.d.ts +0 -0
- /package/dist/{internal/preact → preact}/runtime.d.ts +0 -0
- /package/dist/{internal/react → react}/hooks.d.ts +0 -0
- /package/dist/{internal/react → react}/runtime.d.ts +0 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { safeWrapAsync } from "./errors.ts";
|
|
2
|
+
import type {
|
|
3
|
+
DataResult,
|
|
4
|
+
Params,
|
|
5
|
+
ResourceModule,
|
|
6
|
+
RouteState,
|
|
7
|
+
} from "./resource-types.ts";
|
|
8
|
+
|
|
9
|
+
const IDLE_ROUTE_STATE: RouteState = {
|
|
10
|
+
actionData: null,
|
|
11
|
+
actionError: "",
|
|
12
|
+
actionState: "idle",
|
|
13
|
+
loaderData: null,
|
|
14
|
+
loaderError: "",
|
|
15
|
+
loaderInitialized: false,
|
|
16
|
+
loaderState: "idle",
|
|
17
|
+
navigationState: "idle",
|
|
18
|
+
unexpectedError: false,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class ResourceRouter {
|
|
22
|
+
readonly #cleanupTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
23
|
+
readonly #controllers = new Map<string, AbortController>();
|
|
24
|
+
readonly #inFlightLoads = new Map<string, Promise<DataResult<unknown>>>();
|
|
25
|
+
readonly #listeners = new Map<string, Set<() => void>>();
|
|
26
|
+
readonly #persistentRoutes = new Set<string>();
|
|
27
|
+
readonly #routes: ReadonlyMap<string, ResourceModule>;
|
|
28
|
+
readonly #states = new Map<string, RouteState>();
|
|
29
|
+
|
|
30
|
+
constructor(routes: ReadonlyMap<string, ResourceModule>) {
|
|
31
|
+
this.#routes = routes;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
dispose() {
|
|
35
|
+
for (const controller of this.#controllers.values()) controller.abort();
|
|
36
|
+
|
|
37
|
+
this.#controllers.clear();
|
|
38
|
+
|
|
39
|
+
for (const timer of this.#cleanupTimers.values()) clearTimeout(timer);
|
|
40
|
+
|
|
41
|
+
this.#cleanupTimers.clear();
|
|
42
|
+
this.#inFlightLoads.clear();
|
|
43
|
+
this.#listeners.clear();
|
|
44
|
+
this.#persistentRoutes.clear();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getRoute(routeId: string): ResourceModule | null {
|
|
48
|
+
return this.#routes.get(routeId) ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getSnapshot(routeId: string, params: Params): RouteState {
|
|
52
|
+
return (
|
|
53
|
+
this.#states.get(createRouteKey(routeId, params)) ?? IDLE_ROUTE_STATE
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
hydrateRoute<Data>(routeId: string, data: Data, params: Params = {}) {
|
|
58
|
+
const routeKey = createRouteKey(routeId, params);
|
|
59
|
+
const currentState = this.#states.get(routeKey) ?? IDLE_ROUTE_STATE;
|
|
60
|
+
this.#setState(routeKey, {
|
|
61
|
+
...currentState,
|
|
62
|
+
loaderData: data,
|
|
63
|
+
loaderError: "",
|
|
64
|
+
loaderInitialized: true,
|
|
65
|
+
loaderState: "idle",
|
|
66
|
+
navigationState:
|
|
67
|
+
currentState.actionState === "submitting" ? "submitting" : "idle",
|
|
68
|
+
unexpectedError: false,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
persist(routeId: string, params: Params = {}) {
|
|
73
|
+
const routeKey = createRouteKey(routeId, params);
|
|
74
|
+
this.#persistentRoutes.add(routeKey);
|
|
75
|
+
const cleanupTimer = this.#cleanupTimers.get(routeKey);
|
|
76
|
+
|
|
77
|
+
if (!cleanupTimer) return;
|
|
78
|
+
|
|
79
|
+
clearTimeout(cleanupTimer);
|
|
80
|
+
this.#cleanupTimers.delete(routeKey);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
disposeRoute(routeId: string, params: Params = {}) {
|
|
84
|
+
this.#disposeRouteKey(createRouteKey(routeId, params));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#disposeRouteKey(routeKey: string) {
|
|
88
|
+
this.#persistentRoutes.delete(routeKey);
|
|
89
|
+
const cleanupTimer = this.#cleanupTimers.get(routeKey);
|
|
90
|
+
|
|
91
|
+
if (cleanupTimer) clearTimeout(cleanupTimer);
|
|
92
|
+
|
|
93
|
+
this.#cleanupTimers.delete(routeKey);
|
|
94
|
+
this.#controllers.get(routeKey)?.abort();
|
|
95
|
+
this.#controllers.get(`${routeKey}:action`)?.abort();
|
|
96
|
+
this.#controllers.delete(routeKey);
|
|
97
|
+
this.#controllers.delete(`${routeKey}:action`);
|
|
98
|
+
this.#inFlightLoads.delete(routeKey);
|
|
99
|
+
this.#states.delete(routeKey);
|
|
100
|
+
this.#notify(routeKey);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
load<Data>(
|
|
104
|
+
routeId: string,
|
|
105
|
+
params: Params = {},
|
|
106
|
+
force = false,
|
|
107
|
+
): Promise<DataResult<Data>> {
|
|
108
|
+
const routeKey = createRouteKey(routeId, params);
|
|
109
|
+
const route = this.#routes.get(routeId);
|
|
110
|
+
|
|
111
|
+
if (!route?.loader) {
|
|
112
|
+
return Promise.resolve<DataResult<Data>>({
|
|
113
|
+
data: null,
|
|
114
|
+
error: "Route loader not found.",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const currentState = this.#states.get(routeKey);
|
|
119
|
+
|
|
120
|
+
if (!force && currentState?.loaderInitialized) {
|
|
121
|
+
return Promise.resolve({
|
|
122
|
+
data: currentState?.loaderData as Data,
|
|
123
|
+
error: currentState?.loaderError ?? "",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const currentLoad = this.#inFlightLoads.get(routeKey);
|
|
128
|
+
|
|
129
|
+
if (force && currentLoad) {
|
|
130
|
+
this.#controllers.get(routeKey)?.abort();
|
|
131
|
+
this.#inFlightLoads.delete(routeKey);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!force && currentLoad) return currentLoad as Promise<DataResult<Data>>;
|
|
135
|
+
|
|
136
|
+
const controller = this.#replaceController(routeKey);
|
|
137
|
+
this.#setState(routeKey, {
|
|
138
|
+
...(currentState ?? IDLE_ROUTE_STATE),
|
|
139
|
+
loaderError: "",
|
|
140
|
+
loaderState: "loading",
|
|
141
|
+
navigationState:
|
|
142
|
+
currentState?.actionState === "submitting" ? "submitting" : "loading",
|
|
143
|
+
unexpectedError: false,
|
|
144
|
+
});
|
|
145
|
+
const load = this.#runLoader(route, routeKey, params, controller);
|
|
146
|
+
this.#inFlightLoads.set(routeKey, load);
|
|
147
|
+
|
|
148
|
+
return load as Promise<DataResult<Data>>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
subscribe(routeId: string, params: Params, listener: () => void): () => void {
|
|
152
|
+
const routeKey = createRouteKey(routeId, params);
|
|
153
|
+
const cleanupTimer = this.#cleanupTimers.get(routeKey);
|
|
154
|
+
|
|
155
|
+
if (cleanupTimer) {
|
|
156
|
+
clearTimeout(cleanupTimer);
|
|
157
|
+
this.#cleanupTimers.delete(routeKey);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const listeners = this.#listeners.get(routeKey) ?? new Set<() => void>();
|
|
161
|
+
listeners.add(listener);
|
|
162
|
+
this.#listeners.set(routeKey, listeners);
|
|
163
|
+
|
|
164
|
+
return () => {
|
|
165
|
+
if (!listeners.delete(listener) || listeners.size > 0) return;
|
|
166
|
+
|
|
167
|
+
this.#listeners.delete(routeKey);
|
|
168
|
+
|
|
169
|
+
if (this.#persistentRoutes.has(routeKey)) return;
|
|
170
|
+
|
|
171
|
+
const timer = setTimeout(
|
|
172
|
+
() => this.#disposeRouteKey(routeKey),
|
|
173
|
+
routeCleanupGraceMs,
|
|
174
|
+
);
|
|
175
|
+
this.#cleanupTimers.set(routeKey, timer);
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async submit<Data>(
|
|
180
|
+
routeId: string,
|
|
181
|
+
input: unknown,
|
|
182
|
+
params: Params = {},
|
|
183
|
+
): Promise<DataResult<Data>> {
|
|
184
|
+
const routeKey = createRouteKey(routeId, params);
|
|
185
|
+
const route = this.#routes.get(routeId);
|
|
186
|
+
|
|
187
|
+
if (!route?.action) {
|
|
188
|
+
return { data: null, error: "Route action not found." };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const controllerKey = `${routeKey}:action`;
|
|
192
|
+
const controller = this.#replaceController(controllerKey);
|
|
193
|
+
const currentState = this.#states.get(routeKey) ?? IDLE_ROUTE_STATE;
|
|
194
|
+
this.#setState(routeKey, {
|
|
195
|
+
...currentState,
|
|
196
|
+
actionError: "",
|
|
197
|
+
actionState: "submitting",
|
|
198
|
+
navigationState: "submitting",
|
|
199
|
+
unexpectedError: false,
|
|
200
|
+
});
|
|
201
|
+
const { error, data: result } = await safeWrapAsync(() =>
|
|
202
|
+
route.action?.({ input, params, signal: controller.signal }),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (this.#controllers.get(controllerKey) === controller) {
|
|
206
|
+
this.#controllers.delete(controllerKey);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (controller.signal.aborted) return { data: null, error: "" };
|
|
210
|
+
|
|
211
|
+
const latestState = this.#states.get(routeKey) ?? IDLE_ROUTE_STATE;
|
|
212
|
+
|
|
213
|
+
if (error || !result) {
|
|
214
|
+
const publicError = "The request could not be completed.";
|
|
215
|
+
this.#setState(routeKey, {
|
|
216
|
+
...latestState,
|
|
217
|
+
actionData: null,
|
|
218
|
+
actionError: publicError,
|
|
219
|
+
actionState: "idle",
|
|
220
|
+
navigationState:
|
|
221
|
+
latestState.loaderState === "loading" ? "loading" : "idle",
|
|
222
|
+
unexpectedError: true,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return { data: null, error: publicError, unexpected: true };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.#setState(routeKey, {
|
|
229
|
+
...latestState,
|
|
230
|
+
actionData: result.data,
|
|
231
|
+
actionError: result.error,
|
|
232
|
+
actionState: "idle",
|
|
233
|
+
navigationState:
|
|
234
|
+
latestState.loaderState === "loading" ? "loading" : "idle",
|
|
235
|
+
unexpectedError: false,
|
|
236
|
+
});
|
|
237
|
+
const actionResult = {
|
|
238
|
+
data: result.data as Data | null,
|
|
239
|
+
error: result.error,
|
|
240
|
+
};
|
|
241
|
+
const { error: revalidationError, data: shouldRevalidate } =
|
|
242
|
+
await safeWrapAsync(
|
|
243
|
+
() =>
|
|
244
|
+
route.shouldRevalidate?.({
|
|
245
|
+
actionResult: result,
|
|
246
|
+
defaultShouldRevalidate: Boolean(route.loader),
|
|
247
|
+
params,
|
|
248
|
+
}) ?? Boolean(route.loader),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
if (revalidationError) {
|
|
252
|
+
const publicError = "The request could not be completed.";
|
|
253
|
+
this.#setState(routeKey, {
|
|
254
|
+
...(this.#states.get(routeKey) ?? IDLE_ROUTE_STATE),
|
|
255
|
+
actionError: publicError,
|
|
256
|
+
actionState: "idle",
|
|
257
|
+
unexpectedError: true,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return { data: null, error: publicError, unexpected: true };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (shouldRevalidate) await this.load(routeId, params, true);
|
|
264
|
+
|
|
265
|
+
return actionResult;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async fetchLoader<Data>(
|
|
269
|
+
routeId: string,
|
|
270
|
+
params: Params,
|
|
271
|
+
signal: AbortSignal,
|
|
272
|
+
): Promise<DataResult<Data>> {
|
|
273
|
+
const route = this.#routes.get(routeId);
|
|
274
|
+
|
|
275
|
+
if (!route?.loader) {
|
|
276
|
+
return { data: null, error: "Route loader not found." };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const { error, data: result } = await safeWrapAsync(() =>
|
|
280
|
+
route.loader?.({ params, signal }),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
if (signal.aborted) return { data: null, error: "" };
|
|
284
|
+
|
|
285
|
+
if (error || !result) {
|
|
286
|
+
return {
|
|
287
|
+
data: null,
|
|
288
|
+
error: "The route could not be loaded.",
|
|
289
|
+
unexpected: true,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return result as DataResult<Data>;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async fetchAction<Data>(
|
|
297
|
+
routeId: string,
|
|
298
|
+
input: unknown,
|
|
299
|
+
params: Params,
|
|
300
|
+
signal: AbortSignal,
|
|
301
|
+
): Promise<DataResult<Data>> {
|
|
302
|
+
const route = this.#routes.get(routeId);
|
|
303
|
+
|
|
304
|
+
if (!route?.action) {
|
|
305
|
+
return { data: null, error: "Route action not found." };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const { error, data: result } = await safeWrapAsync(() =>
|
|
309
|
+
route.action?.({ input, params, signal }),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
if (signal.aborted) return { data: null, error: "" };
|
|
313
|
+
|
|
314
|
+
if (error || !result) {
|
|
315
|
+
return {
|
|
316
|
+
data: null,
|
|
317
|
+
error: "The request could not be completed.",
|
|
318
|
+
unexpected: true,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const { error: revalidationError, data: shouldRevalidate } =
|
|
323
|
+
await safeWrapAsync(
|
|
324
|
+
() =>
|
|
325
|
+
route.shouldRevalidate?.({
|
|
326
|
+
actionResult: result,
|
|
327
|
+
defaultShouldRevalidate: Boolean(route.loader),
|
|
328
|
+
params,
|
|
329
|
+
}) ?? Boolean(route.loader),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
if (signal.aborted) return { data: null, error: "" };
|
|
333
|
+
|
|
334
|
+
if (revalidationError) {
|
|
335
|
+
return {
|
|
336
|
+
data: null,
|
|
337
|
+
error: "The request could not be completed.",
|
|
338
|
+
unexpected: true,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (shouldRevalidate) await this.load(routeId, params, true);
|
|
343
|
+
|
|
344
|
+
return result as DataResult<Data>;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
retry(routeId: string, params: Params): Promise<DataResult<unknown>> {
|
|
348
|
+
const routeKey = createRouteKey(routeId, params);
|
|
349
|
+
this.#states.set(routeKey, IDLE_ROUTE_STATE);
|
|
350
|
+
this.#notify(routeKey);
|
|
351
|
+
|
|
352
|
+
return this.load(routeId, params, true);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
reportUnexpected(routeId: string, params: Params) {
|
|
356
|
+
const routeKey = createRouteKey(routeId, params);
|
|
357
|
+
const currentState = this.#states.get(routeKey) ?? IDLE_ROUTE_STATE;
|
|
358
|
+
this.#setState(routeKey, { ...currentState, unexpectedError: true });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async #runLoader(
|
|
362
|
+
route: ResourceModule,
|
|
363
|
+
routeKey: string,
|
|
364
|
+
params: Params,
|
|
365
|
+
controller: AbortController,
|
|
366
|
+
): Promise<DataResult<unknown>> {
|
|
367
|
+
const { error, data: result } = await safeWrapAsync(
|
|
368
|
+
() =>
|
|
369
|
+
route.loader?.({ params, signal: controller.signal }) ??
|
|
370
|
+
Promise.resolve({ data: null, error: "" }),
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
if (this.#controllers.get(routeKey) === controller) {
|
|
374
|
+
this.#controllers.delete(routeKey);
|
|
375
|
+
this.#inFlightLoads.delete(routeKey);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (controller.signal.aborted) return { data: null, error: "" };
|
|
379
|
+
|
|
380
|
+
if (error || !result) {
|
|
381
|
+
const publicError = "The route could not be loaded.";
|
|
382
|
+
this.#setState(routeKey, {
|
|
383
|
+
actionData: null,
|
|
384
|
+
actionError: "",
|
|
385
|
+
actionState: "idle",
|
|
386
|
+
loaderData: null,
|
|
387
|
+
loaderError: publicError,
|
|
388
|
+
loaderInitialized: true,
|
|
389
|
+
loaderState: "idle",
|
|
390
|
+
navigationState: "idle",
|
|
391
|
+
unexpectedError: true,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
return { data: null, error: publicError, unexpected: true };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const currentState = this.#states.get(routeKey) ?? IDLE_ROUTE_STATE;
|
|
398
|
+
this.#setState(routeKey, {
|
|
399
|
+
...currentState,
|
|
400
|
+
loaderData: result.data,
|
|
401
|
+
loaderError: result.error,
|
|
402
|
+
loaderInitialized: true,
|
|
403
|
+
loaderState: "idle",
|
|
404
|
+
navigationState:
|
|
405
|
+
currentState.actionState === "submitting" ? "submitting" : "idle",
|
|
406
|
+
unexpectedError: false,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
#replaceController(key: string) {
|
|
413
|
+
this.#controllers.get(key)?.abort();
|
|
414
|
+
const controller = new AbortController();
|
|
415
|
+
this.#controllers.set(key, controller);
|
|
416
|
+
|
|
417
|
+
return controller;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#setState(routeKey: string, state: RouteState) {
|
|
421
|
+
this.#states.set(routeKey, state);
|
|
422
|
+
this.#notify(routeKey);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
#notify(routeKey: string) {
|
|
426
|
+
for (const listener of this.#listeners.get(routeKey) ?? []) listener();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function createResourceRouter(
|
|
431
|
+
routes: ReadonlyMap<string, ResourceModule>,
|
|
432
|
+
): ResourceRouter {
|
|
433
|
+
return new ResourceRouter(routes);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function createRouteKey(routeId: string, params: Params) {
|
|
437
|
+
const normalizedParams = Object.entries(params).sort(([left], [right]) =>
|
|
438
|
+
left.localeCompare(right),
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
return `${routeId}:${JSON.stringify(normalizedParams)}`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const routeCleanupGraceMs = 100;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export interface RouteError extends Error {
|
|
2
|
+
status: number;
|
|
3
|
+
statusText: string;
|
|
4
|
+
data?: unknown;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface DataResponse<T> {
|
|
8
|
+
value: T;
|
|
9
|
+
init: ResponseInit;
|
|
10
|
+
__routeData: true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function data<T>(value: T, init: ResponseInit = {}): DataResponse<T> {
|
|
14
|
+
return { value, init, __routeData: true };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function redirect(
|
|
18
|
+
location: string,
|
|
19
|
+
init: number | ResponseInit = 302,
|
|
20
|
+
): Response {
|
|
21
|
+
const options = typeof init === "number" ? { status: init } : init;
|
|
22
|
+
const headers = new Headers(options.headers);
|
|
23
|
+
headers.set("Location", location);
|
|
24
|
+
|
|
25
|
+
return new Response(null, {
|
|
26
|
+
...options,
|
|
27
|
+
status: options.status ?? 302,
|
|
28
|
+
headers,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isRouteErrorResponse(value: unknown): value is RouteError {
|
|
33
|
+
return value instanceof Error && "status" in value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function normalizeResult(value: unknown): Promise<unknown> {
|
|
37
|
+
if (value && typeof value === "object" && "__routeError" in value) {
|
|
38
|
+
const error = value.__routeError as {
|
|
39
|
+
message: string;
|
|
40
|
+
status?: number;
|
|
41
|
+
statusText?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return Object.assign(Error(error.message), error);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (value instanceof Response) {
|
|
48
|
+
if (value.headers.has("Location")) return value;
|
|
49
|
+
|
|
50
|
+
if (!value.ok)
|
|
51
|
+
return Object.assign(Error(await value.text()), {
|
|
52
|
+
status: value.status,
|
|
53
|
+
statusText: value.statusText,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return value.json();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (value && typeof value === "object" && "__routeData" in value)
|
|
60
|
+
return resolveData((value as DataResponse<unknown>).value);
|
|
61
|
+
|
|
62
|
+
return resolveData(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Smolter transfers JSON snapshots. Resolve deferred fields before serialization.
|
|
66
|
+
export async function resolveData(value: unknown): Promise<unknown> {
|
|
67
|
+
const resolved = await value;
|
|
68
|
+
|
|
69
|
+
if (Array.isArray(resolved)) return Promise.all(resolved.map(resolveData));
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
resolved &&
|
|
73
|
+
typeof resolved === "object" &&
|
|
74
|
+
Object.getPrototypeOf(resolved) === Object.prototype
|
|
75
|
+
) {
|
|
76
|
+
const entries = await Promise.all(
|
|
77
|
+
Object.entries(resolved).map(
|
|
78
|
+
async ([key, field]) => [key, await resolveData(field)] as const,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return Object.fromEntries(entries);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return resolved;
|
|
86
|
+
}
|