@uniflowed/router 0.0.0-alpha.41 → 0.0.0-alpha.42
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/instrumentation.js +92 -0
- package/internal/action-endpoint.js +4 -1
- package/internal/resolve.js +5 -1
- package/internal/runtime.js +8 -2
- package/internal/server-instrumentation.js +12 -0
- package/package.json +6 -4
- package/rsc.js +18 -4
- package/server.js +13 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
export type NavigationTiming = {|
|
|
4
|
+
readonly kind: "document" | "router",
|
|
5
|
+
readonly pathname: string,
|
|
6
|
+
readonly duration: number,
|
|
7
|
+
readonly status: "complete" | "error",
|
|
8
|
+
|};
|
|
9
|
+
|
|
10
|
+
export type ClientInstrumentation = {|
|
|
11
|
+
readonly register?: () => void | Promise<void>,
|
|
12
|
+
readonly onError?: (
|
|
13
|
+
error: mixed,
|
|
14
|
+
context: {| readonly source: "error" | "unhandledrejection" | "navigation" | "startup" |},
|
|
15
|
+
) => void | Promise<void>,
|
|
16
|
+
readonly onNavigation?: (timing: NavigationTiming) => void | Promise<void>,
|
|
17
|
+
|};
|
|
18
|
+
|
|
19
|
+
const observers: Set<ClientInstrumentation> = new Set();
|
|
20
|
+
|
|
21
|
+
function notify(body: () => mixed): void {
|
|
22
|
+
Promise.resolve()
|
|
23
|
+
.then(body)
|
|
24
|
+
.catch((error) => console.error("uf client instrumentation failed", error));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Installed by the client entry before hydration, with disposal during HMR. */
|
|
28
|
+
export async function installClientInstrumentation(
|
|
29
|
+
hooks: ClientInstrumentation,
|
|
30
|
+
): Promise<() => void> {
|
|
31
|
+
const error = (event: ErrorEvent) =>
|
|
32
|
+
notify(() => hooks.onError?.(event.error ?? event.message, { source: "error" }));
|
|
33
|
+
const rejection = (event: PromiseRejectionEvent) =>
|
|
34
|
+
notify(() => hooks.onError?.(event.reason, { source: "unhandledrejection" }));
|
|
35
|
+
window.addEventListener("error", error);
|
|
36
|
+
window.addEventListener("unhandledrejection", rejection);
|
|
37
|
+
observers.add(hooks);
|
|
38
|
+
let performanceObserver = null;
|
|
39
|
+
if (
|
|
40
|
+
typeof PerformanceObserver === "function" &&
|
|
41
|
+
PerformanceObserver.supportedEntryTypes.includes("navigation")
|
|
42
|
+
) {
|
|
43
|
+
performanceObserver = new PerformanceObserver((list) => {
|
|
44
|
+
for (const entry of list.getEntries()) {
|
|
45
|
+
const timing: NavigationTiming = {
|
|
46
|
+
kind: "document",
|
|
47
|
+
pathname: new URL(entry.name).pathname,
|
|
48
|
+
duration: entry.duration,
|
|
49
|
+
status: "complete",
|
|
50
|
+
};
|
|
51
|
+
notify(() => hooks.onNavigation?.(timing));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
performanceObserver.observe({ type: "navigation", buffered: true });
|
|
55
|
+
}
|
|
56
|
+
const dispose = () => {
|
|
57
|
+
window.removeEventListener("error", error);
|
|
58
|
+
window.removeEventListener("unhandledrejection", rejection);
|
|
59
|
+
observers.delete(hooks);
|
|
60
|
+
performanceObserver?.disconnect();
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
await hooks.register?.();
|
|
64
|
+
} catch (failure) {
|
|
65
|
+
notify(() => hooks.onError?.(failure, { source: "startup" }));
|
|
66
|
+
dispose();
|
|
67
|
+
}
|
|
68
|
+
return dispose;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The router's asynchronous navigation work; a hard navigation has browser timing entries. */
|
|
72
|
+
export async function observeNavigation<T>(to: string, body: () => Promise<T>): Promise<T> {
|
|
73
|
+
if (observers.size === 0) return body();
|
|
74
|
+
const started = performance.now();
|
|
75
|
+
const pathname = new URL(to, window.location.href).pathname;
|
|
76
|
+
let status: NavigationTiming["status"] = "complete";
|
|
77
|
+
try {
|
|
78
|
+
return await body();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
status = "error";
|
|
81
|
+
for (const hooks of observers) notify(() => hooks.onError?.(error, { source: "navigation" }));
|
|
82
|
+
throw error;
|
|
83
|
+
} finally {
|
|
84
|
+
const timing: NavigationTiming = {
|
|
85
|
+
kind: "router",
|
|
86
|
+
pathname,
|
|
87
|
+
duration: performance.now() - started,
|
|
88
|
+
status,
|
|
89
|
+
};
|
|
90
|
+
for (const hooks of observers) notify(() => hooks.onNavigation?.(timing));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -88,6 +88,8 @@
|
|
|
88
88
|
// it. Written here because this is the file somebody reads before deciding
|
|
89
89
|
// otherwise.
|
|
90
90
|
|
|
91
|
+
import { traceRequestPhase, reportRequestError } from "@uniflowed/server/instrumentation";
|
|
92
|
+
|
|
91
93
|
import { asResponder, nativeActionAllowed } from "@uniflowed/server/host";
|
|
92
94
|
|
|
93
95
|
import {
|
|
@@ -232,7 +234,7 @@ export function createActionDispatcher(options: {|
|
|
|
232
234
|
// `decodeActionArguments` produced, so what is unchecked here is the
|
|
233
235
|
// shape of the function and not the shape of the payload.
|
|
234
236
|
const call = action as $FlowFixMe;
|
|
235
|
-
result = await call(...args);
|
|
237
|
+
result = await traceRequestPhase("action", () => call(...args));
|
|
236
238
|
} catch (error) {
|
|
237
239
|
report(record, error);
|
|
238
240
|
return refusal(500);
|
|
@@ -430,6 +432,7 @@ function refusal(status: number): Response {
|
|
|
430
432
|
* here and nowhere the caller can read.
|
|
431
433
|
*/
|
|
432
434
|
function report(record: ActionRecord, error: mixed): void {
|
|
435
|
+
reportRequestError(error, "action");
|
|
433
436
|
// eslint-disable-next-line no-console
|
|
434
437
|
console.error(`uf: server action \`${record.export}\` in \`${record.module}\` failed`, error);
|
|
435
438
|
}
|
package/internal/resolve.js
CHANGED
|
@@ -722,6 +722,8 @@ export type ResolveOptions = {|
|
|
|
722
722
|
readonly defer?: boolean,
|
|
723
723
|
/** The route pattern, the moment the URL matches one; see [`resolveMatch`]. */
|
|
724
724
|
readonly onMatch?: (pattern: string) => void,
|
|
725
|
+
/** The server supplies tracing without importing server code into the browser. */
|
|
726
|
+
readonly runLoader?: (body: () => mixed | Promise<mixed>) => Promise<mixed>,
|
|
725
727
|
|};
|
|
726
728
|
|
|
727
729
|
async function resolveRoute(
|
|
@@ -799,7 +801,9 @@ async function resolveRoute(
|
|
|
799
801
|
let data: mixed = options?.data;
|
|
800
802
|
let deferred: ?Promise<mixed> = null;
|
|
801
803
|
if (options?.skipLoader !== true && typeof page.loader === "function") {
|
|
802
|
-
const
|
|
804
|
+
const loader = page.loader;
|
|
805
|
+
const loadData = () => loader({ params: matched.params, searchParams, pathname });
|
|
806
|
+
const running = options?.runLoader == null ? loadData() : options.runLoader(loadData);
|
|
803
807
|
const canDefer =
|
|
804
808
|
options?.defer === true &&
|
|
805
809
|
(matched.route.loading ?? []).length > 0 &&
|
package/internal/runtime.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
"use client";
|
|
22
22
|
|
|
23
|
+
import { observeNavigation } from "../instrumentation.js";
|
|
24
|
+
|
|
23
25
|
import type * as React from "react";
|
|
24
26
|
import {
|
|
25
27
|
Suspense,
|
|
@@ -665,7 +667,9 @@ component ModuleRouter(url: string, initial: ResolvedRoute, children: React.Node
|
|
|
665
667
|
setResolved(next);
|
|
666
668
|
};
|
|
667
669
|
|
|
668
|
-
const navigate =
|
|
670
|
+
const navigate = (to: string, options?: NavigateOptions): Promise<void> =>
|
|
671
|
+
observeNavigation(to, () => navigateTo(to, options));
|
|
672
|
+
const navigateTo = async (to: string, options?: NavigateOptions): Promise<void> => {
|
|
669
673
|
if (!isBrowser()) {
|
|
670
674
|
return;
|
|
671
675
|
}
|
|
@@ -985,7 +989,9 @@ component FlightRouter(flight: Promise<FlightRoot>, children: React.Node) {
|
|
|
985
989
|
// Read once per render, for the reason `ModuleRouter` reads it once.
|
|
986
990
|
const navigation = navigationMode();
|
|
987
991
|
|
|
988
|
-
const navigate =
|
|
992
|
+
const navigate = (to: string, options?: NavigateOptions): Promise<void> =>
|
|
993
|
+
observeNavigation(to, () => navigateTo(to, options));
|
|
994
|
+
const navigateTo = async (to: string, options?: NavigateOptions): Promise<void> => {
|
|
989
995
|
if (!isBrowser()) {
|
|
990
996
|
return;
|
|
991
997
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import { traceRequestPhase } from "@uniflowed/server/instrumentation";
|
|
4
|
+
import { NotFoundError, RedirectError } from "./routing.js";
|
|
5
|
+
|
|
6
|
+
export function traceLoader(body: () => mixed | Promise<mixed>): Promise<mixed> {
|
|
7
|
+
return traceRequestPhase(
|
|
8
|
+
"loader",
|
|
9
|
+
async () => body(),
|
|
10
|
+
(error) => error instanceof RedirectError || error instanceof NotFoundError,
|
|
11
|
+
);
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/router",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.42",
|
|
4
4
|
"description": "The file-system router for Flow React applications: matching, layouts, loaders, navigation, server rendering and hydration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"./handler": "./handler.js",
|
|
28
28
|
"./middleware": "./middleware.js",
|
|
29
29
|
"./native-navigation": "./native-navigation.js",
|
|
30
|
-
"./http-client": "./http-client.js"
|
|
30
|
+
"./http-client": "./http-client.js",
|
|
31
|
+
"./instrumentation": "./instrumentation.js"
|
|
31
32
|
},
|
|
32
33
|
"files": [
|
|
33
34
|
"action.js",
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
"server.js",
|
|
46
47
|
"native-navigation.js",
|
|
47
48
|
"http-client.js",
|
|
49
|
+
"instrumentation.js",
|
|
48
50
|
"!*.test.js"
|
|
49
51
|
],
|
|
50
52
|
"peerDependencies": {
|
|
@@ -69,7 +71,7 @@
|
|
|
69
71
|
}
|
|
70
72
|
},
|
|
71
73
|
"dependencies": {
|
|
72
|
-
"@uniflowed/hooks": "0.0.0-alpha.
|
|
73
|
-
"@uniflowed/server": "0.0.0-alpha.
|
|
74
|
+
"@uniflowed/hooks": "0.0.0-alpha.42",
|
|
75
|
+
"@uniflowed/server": "0.0.0-alpha.42"
|
|
74
76
|
}
|
|
75
77
|
}
|
package/rsc.js
CHANGED
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
// in this graph, and a boundary module without the directive is refused here,
|
|
45
45
|
// with its file named, rather than by React with a sentence about functions.
|
|
46
46
|
|
|
47
|
+
import { traceLoader } from "./internal/server-instrumentation.js";
|
|
48
|
+
|
|
47
49
|
import * as React from "react";
|
|
48
50
|
import { use } from "react";
|
|
49
51
|
// React's Flight server. Only this graph can load it: it refuses to evaluate
|
|
@@ -51,6 +53,7 @@ import { use } from "react";
|
|
|
51
53
|
import { renderToReadableStream } from "react-server-dom-parcel/server";
|
|
52
54
|
|
|
53
55
|
import { noteRoute } from "@uniflowed/server/host";
|
|
56
|
+
import { reportRequestError } from "@uniflowed/server/instrumentation";
|
|
54
57
|
|
|
55
58
|
import { BoundaryReporter } from "./internal/boundaries.js";
|
|
56
59
|
import { routeBoundaries } from "./internal/boundary-data.js";
|
|
@@ -175,7 +178,7 @@ export function createFlightRenderer(options: {|
|
|
|
175
178
|
intercepted ??
|
|
176
179
|
// `onMatch` records the route pattern on the request before the
|
|
177
180
|
// loader runs, so every line the loader logs names its route.
|
|
178
|
-
(await resolveMatch(table, url, { defer, onMatch: noteRoute }));
|
|
181
|
+
(await resolveMatch(table, url, { defer, onMatch: noteRoute, runLoader: traceLoader }));
|
|
179
182
|
} else {
|
|
180
183
|
resolved = await resolveFailure(table, url, failure.error);
|
|
181
184
|
}
|
|
@@ -199,13 +202,20 @@ export function createFlightRenderer(options: {|
|
|
|
199
202
|
</>
|
|
200
203
|
);
|
|
201
204
|
const root: FlightRoot = { route: state, tree };
|
|
202
|
-
const
|
|
205
|
+
const failure = renderFailure(route);
|
|
206
|
+
if (failure != null) reportRequestError(failure, "render");
|
|
207
|
+
const report = (error) => {
|
|
208
|
+
reportRequestError(error, "render");
|
|
209
|
+
if (settings?.onError != null) return settings.onError(error);
|
|
210
|
+
console.error(error);
|
|
211
|
+
return undefined;
|
|
212
|
+
};
|
|
203
213
|
// Inside the route's store, so a server component's `useRoute()` finds the
|
|
204
214
|
// route however many `await`s into the render it asks.
|
|
205
215
|
const stream = withServerRoute(state, () =>
|
|
206
216
|
renderToReadableStream(root, {
|
|
207
217
|
// Handed over as it is, so that the digest it returns reaches the row.
|
|
208
|
-
//
|
|
218
|
+
// Our callback preserves React's console fallback when none was given.
|
|
209
219
|
onError: report,
|
|
210
220
|
signal: settings?.signal,
|
|
211
221
|
}),
|
|
@@ -225,7 +235,11 @@ async function resolveFlightInterception(
|
|
|
225
235
|
return null;
|
|
226
236
|
}
|
|
227
237
|
try {
|
|
228
|
-
const base = await resolveMatch(table, baseUrl, {
|
|
238
|
+
const base = await resolveMatch(table, baseUrl, {
|
|
239
|
+
defer,
|
|
240
|
+
onMatch: noteRoute,
|
|
241
|
+
runLoader: traceLoader,
|
|
242
|
+
});
|
|
229
243
|
return await resolveInterception(table, base, url);
|
|
230
244
|
} catch (error) {
|
|
231
245
|
if (error instanceof RedirectError) {
|
package/server.js
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
// which.
|
|
25
25
|
|
|
26
26
|
import { currentNonce, noteRoute } from "@uniflowed/server/host";
|
|
27
|
+
import { traceLoader } from "./internal/server-instrumentation.js";
|
|
28
|
+
|
|
27
29
|
import * as React from "react";
|
|
28
30
|
|
|
29
31
|
import {
|
|
@@ -335,7 +337,11 @@ export function createRenderer(options: {|
|
|
|
335
337
|
try {
|
|
336
338
|
return {
|
|
337
339
|
kind: "route",
|
|
338
|
-
route: await resolveMatch(table, url, {
|
|
340
|
+
route: await resolveMatch(table, url, {
|
|
341
|
+
defer,
|
|
342
|
+
onMatch: noteRoute,
|
|
343
|
+
runLoader: traceLoader,
|
|
344
|
+
}),
|
|
339
345
|
};
|
|
340
346
|
} catch (error) {
|
|
341
347
|
if (error instanceof RedirectError) {
|
|
@@ -512,3 +518,9 @@ export function shellDocument(assets: RenderAssets): string {
|
|
|
512
518
|
const shell = shellFor(assets);
|
|
513
519
|
return `${shell.open}${shell.body}${shell.close}`;
|
|
514
520
|
}
|
|
521
|
+
|
|
522
|
+
export {
|
|
523
|
+
createInstrumentation,
|
|
524
|
+
instrumentRender,
|
|
525
|
+
traceRequestPhase,
|
|
526
|
+
} from "@uniflowed/server/instrumentation";
|