@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.204030a9
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/AGENTS.md +4 -0
- package/README.md +242 -55
- package/dist/bin/rango.js +277 -99
- package/dist/vite/index.js +2929 -1132
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +68 -21
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +243 -21
- package/skills/caching/SKILL.md +159 -10
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +262 -51
- package/skills/host-router/SKILL.md +243 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +291 -31
- package/skills/middleware/SKILL.md +49 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +197 -6
- package/skills/prerender/SKILL.md +125 -102
- package/skills/rango/SKILL.md +242 -23
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +91 -8
- package/skills/router-setup/SKILL.md +98 -8
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +511 -188
- package/skills/typesafety/SKILL.md +354 -50
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/__internal.ts +92 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +91 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +183 -18
- package/src/browser/navigation-client.ts +187 -57
- package/src/browser/navigation-store.ts +75 -17
- package/src/browser/navigation-transaction.ts +21 -37
- package/src/browser/partial-update.ts +143 -40
- package/src/browser/prefetch/cache.ts +275 -28
- package/src/browser/prefetch/fetch.ts +191 -46
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +98 -14
- package/src/browser/react/NavigationProvider.tsx +110 -33
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +23 -64
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +43 -10
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +200 -75
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +31 -36
- package/src/browser/types.ts +81 -5
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +69 -26
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -88
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +76 -49
- package/src/cache/cf/cf-cache-store.ts +501 -18
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +5 -1
- package/src/client.tsx +95 -284
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +65 -12
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +2 -5
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +15 -40
- package/src/index.ts +92 -76
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +141 -80
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +435 -260
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +110 -34
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +113 -1
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +105 -39
- package/src/router/intercept-resolution.ts +15 -22
- package/src/router/lazy-includes.ts +12 -9
- package/src/router/loader-resolution.ts +175 -23
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +129 -193
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +136 -106
- package/src/router/match-middleware/cache-store.ts +54 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +124 -18
- package/src/router/metrics.ts +239 -14
- package/src/router/middleware-types.ts +61 -31
- package/src/router/middleware.ts +226 -124
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +118 -19
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +286 -0
- package/src/router/revalidation.ts +85 -9
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +91 -29
- package/src/router/router-options.ts +89 -19
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +240 -23
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +483 -289
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +38 -15
- package/src/router/types.ts +9 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +120 -32
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +524 -370
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -21
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +39 -10
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +69 -89
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +39 -47
- package/src/rsc/ssr-setup.ts +144 -0
- package/src/rsc/types.ts +19 -3
- package/src/search-params.ts +20 -17
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +219 -67
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +285 -63
- package/src/server/cookie-store.ts +28 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +228 -65
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +9 -1
- package/src/static-handler.ts +19 -7
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +98 -0
- package/src/testing/internal/context.ts +151 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +112 -0
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +197 -79
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +37 -6
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +50 -9
- package/src/urls/path-helper.ts +63 -63
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +487 -44
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +63 -91
- package/src/vite/discovery/discover-routers.ts +106 -53
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +222 -107
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +50 -13
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +10 -3
- package/src/vite/plugin-types.ts +111 -72
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +55 -33
- package/src/vite/plugins/expose-id-utils.ts +24 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +544 -317
- package/src/vite/plugins/performance-tracks.ts +92 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +72 -3
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +265 -226
- package/src/vite/router-discovery.ts +924 -137
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +98 -5
- package/src/vite/utils/shared-utils.ts +109 -27
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/route-definition/route-function.ts +0 -119
|
@@ -20,6 +20,15 @@ import {
|
|
|
20
20
|
expandPrerenderRoutes,
|
|
21
21
|
renderStaticHandlers,
|
|
22
22
|
} from "./prerender-collection.js";
|
|
23
|
+
import {
|
|
24
|
+
resolveHostRouterHandlers,
|
|
25
|
+
DiscoveryError,
|
|
26
|
+
type CaughtDiscoveryError,
|
|
27
|
+
} from "./discovery-errors.js";
|
|
28
|
+
import { createRangoDebugger, timed, NS } from "../debug.js";
|
|
29
|
+
import { computeProductionHash } from "../plugins/client-ref-hashing.js";
|
|
30
|
+
|
|
31
|
+
const debug = createRangoDebugger(NS.discovery);
|
|
23
32
|
|
|
24
33
|
/**
|
|
25
34
|
* Import the user's entry via RSC runner, generate manifests for each
|
|
@@ -38,42 +47,40 @@ export async function discoverRouters(
|
|
|
38
47
|
// Import the entry file via RSC environment.
|
|
39
48
|
// For node preset: this is the router file (createRouter() registers in RouterRegistry).
|
|
40
49
|
// For cloudflare preset: this is the worker entry (which imports the router).
|
|
41
|
-
await
|
|
50
|
+
await timed(debug, "inner: import entry", () =>
|
|
51
|
+
rscEnv.runner.import(state.resolvedEntryPath),
|
|
52
|
+
);
|
|
42
53
|
|
|
43
54
|
// Import the router package to access the registry
|
|
44
|
-
const serverMod = await
|
|
55
|
+
const serverMod = await timed(
|
|
56
|
+
debug,
|
|
57
|
+
"inner: import @rangojs/router/server",
|
|
58
|
+
() => rscEnv.runner.import("@rangojs/router/server"),
|
|
59
|
+
);
|
|
45
60
|
let registry: Map<string, any> = serverMod.RouterRegistry;
|
|
46
61
|
|
|
47
62
|
if (!registry || registry.size === 0) {
|
|
48
63
|
// No RSC routers found directly. Check for host routers with lazy handlers
|
|
49
64
|
// that need to be resolved to trigger sub-app createRouter() calls.
|
|
65
|
+
//
|
|
66
|
+
// Handler failures are collected rather than swallowed: when the registry
|
|
67
|
+
// is still empty afterwards, these errors (typically a sub-app whose router
|
|
68
|
+
// module failed to import) are the most likely cause and are surfaced in
|
|
69
|
+
// the terminal "No routers found" error below.
|
|
70
|
+
const discoveryErrors: CaughtDiscoveryError[] = [];
|
|
50
71
|
try {
|
|
51
|
-
const hostMod = await rscEnv.runner.import("@rangojs/router/host");
|
|
52
72
|
const hostRegistry: Map<string, any> | undefined =
|
|
53
|
-
|
|
73
|
+
serverMod.HostRouterRegistry;
|
|
54
74
|
|
|
55
75
|
if (hostRegistry && hostRegistry.size > 0) {
|
|
56
76
|
console.log(
|
|
57
|
-
`[
|
|
77
|
+
`[rango] Found ${hostRegistry.size} host router(s), resolving lazy handlers...`,
|
|
58
78
|
);
|
|
59
79
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
await route.handler();
|
|
65
|
-
} catch {
|
|
66
|
-
// Lazy handler may fail in temp server context, that's OK
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (entry.fallback && typeof entry.fallback.handler === "function") {
|
|
71
|
-
try {
|
|
72
|
-
await entry.fallback.handler();
|
|
73
|
-
} catch {
|
|
74
|
-
// Fallback handler may fail in temp server context
|
|
75
|
-
}
|
|
76
|
-
}
|
|
80
|
+
const handlerErrors = await resolveHostRouterHandlers(hostRegistry);
|
|
81
|
+
discoveryErrors.push(...handlerErrors);
|
|
82
|
+
for (const { context, error } of handlerErrors) {
|
|
83
|
+
debug?.("caught error while resolving %s: %O", context, error);
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
// Re-read RouterRegistry - sub-app createRouter() calls should have populated it
|
|
@@ -88,22 +95,28 @@ export async function discoverRouters(
|
|
|
88
95
|
registry = freshRegistry;
|
|
89
96
|
}
|
|
90
97
|
}
|
|
91
|
-
} catch {
|
|
92
|
-
//
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// Host-router discovery is best-effort; record the failure so it can be
|
|
100
|
+
// surfaced if no routers are found.
|
|
101
|
+
discoveryErrors.push({ context: "host-router discovery", error });
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
// If still no routers after host router resolution, fail
|
|
96
105
|
if (!registry || registry.size === 0) {
|
|
97
|
-
throw new
|
|
98
|
-
`[rsc-router] No routers found in registry after importing ${state.resolvedEntryPath}`,
|
|
99
|
-
);
|
|
106
|
+
throw new DiscoveryError(state.resolvedEntryPath, discoveryErrors);
|
|
100
107
|
}
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
// Import build utilities for manifest generation
|
|
104
|
-
const buildMod = await
|
|
111
|
+
const buildMod = await timed(
|
|
112
|
+
debug,
|
|
113
|
+
"inner: import @rangojs/router/build",
|
|
114
|
+
() => rscEnv.runner.import("@rangojs/router/build"),
|
|
115
|
+
);
|
|
105
116
|
const generateManifestFull = buildMod.generateManifestFull;
|
|
106
117
|
|
|
118
|
+
debug?.("inner: found %d router(s) in registry", registry.size);
|
|
119
|
+
|
|
107
120
|
const nestedRouterConflict = findNestedRouterConflict(
|
|
108
121
|
[...registry.values()]
|
|
109
122
|
.map((router) => router.__sourceFile)
|
|
@@ -131,14 +144,54 @@ export async function discoverRouters(
|
|
|
131
144
|
// Collect all manifests for trie building (avoid re-running generateManifest)
|
|
132
145
|
const allManifests: Array<{ id: string; manifest: any }> = [];
|
|
133
146
|
|
|
147
|
+
// Built-in clientChunks context (present only when the built-in strategy is
|
|
148
|
+
// active). Collect the production hashes of "use client" error/notFound
|
|
149
|
+
// fallback modules so the strategy can route them into app-fallback.
|
|
150
|
+
const clientChunkCtx = state.opts?.clientChunkCtx;
|
|
151
|
+
const collectClientFallbackRef = clientChunkCtx
|
|
152
|
+
? (refKey: string) =>
|
|
153
|
+
clientChunkCtx.fallbackRefs.add(
|
|
154
|
+
computeProductionHash(state.projectRoot, refKey),
|
|
155
|
+
)
|
|
156
|
+
: undefined;
|
|
157
|
+
// Router-level boundary defaults (`createRouter({ defaultErrorBoundary, ... })`)
|
|
158
|
+
// are NOT in EntryData, so generateManifestFull's walk misses them. Collect any
|
|
159
|
+
// "use client" default boundary directly off the router instance. The value is
|
|
160
|
+
// commonly a handler function wrapping the client boundary in server providers,
|
|
161
|
+
// so collectFallbackClientRefs invokes + walks the tree. Routed through buildMod
|
|
162
|
+
// so it runs in the same RSC runner realm the boundary value came from.
|
|
163
|
+
const collectFromBoundaryNode = (node: unknown): void => {
|
|
164
|
+
if (collectClientFallbackRef && buildMod.collectFallbackClientRefs) {
|
|
165
|
+
buildMod.collectFallbackClientRefs(node, collectClientFallbackRef);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const manifestGenStart = debug ? performance.now() : 0;
|
|
134
170
|
for (const [id, router] of registry) {
|
|
135
171
|
if (!router.urlpatterns || !generateManifestFull) {
|
|
136
172
|
continue;
|
|
137
173
|
}
|
|
138
174
|
|
|
139
|
-
const manifest = generateManifestFull(
|
|
175
|
+
const manifest = generateManifestFull(
|
|
176
|
+
router.urlpatterns,
|
|
177
|
+
routerMountIndex,
|
|
178
|
+
{
|
|
179
|
+
...(router.__basename ? { urlPrefix: router.__basename } : {}),
|
|
180
|
+
...(collectClientFallbackRef ? { collectClientFallbackRef } : {}),
|
|
181
|
+
},
|
|
182
|
+
);
|
|
140
183
|
routerMountIndex++;
|
|
141
184
|
allManifests.push({ id, manifest });
|
|
185
|
+
|
|
186
|
+
// Router-level "use client" boundary defaults -> app-fallback (the
|
|
187
|
+
// route-tree errorBoundary()/notFoundBoundary() helpers are already
|
|
188
|
+
// collected inside generateManifestFull via collectClientFallbackRef).
|
|
189
|
+
if (collectClientFallbackRef) {
|
|
190
|
+
collectFromBoundaryNode(router.__defaultErrorBoundary);
|
|
191
|
+
collectFromBoundaryNode(router.__defaultNotFoundBoundary);
|
|
192
|
+
collectFromBoundaryNode(router.__notFound);
|
|
193
|
+
}
|
|
194
|
+
|
|
142
195
|
const routeCount = Object.keys(manifest.routeManifest).length;
|
|
143
196
|
const staticRoutes = Object.values(manifest.routeManifest).filter(
|
|
144
197
|
(p: any) => !p.includes(":") && !p.includes("*"),
|
|
@@ -207,7 +260,7 @@ export async function discoverRouters(
|
|
|
207
260
|
newPerRouterPrecomputedMap.set(id, routerPrecomputed);
|
|
208
261
|
|
|
209
262
|
console.log(
|
|
210
|
-
`[
|
|
263
|
+
`[rango] Router "${id}" -> ${routeCount} routes ` +
|
|
211
264
|
`(${staticRoutes} static, ${dynamicRoutes} dynamic)`,
|
|
212
265
|
);
|
|
213
266
|
}
|
|
@@ -223,7 +276,7 @@ export async function discoverRouters(
|
|
|
223
276
|
);
|
|
224
277
|
if (autoIds.length > 1) {
|
|
225
278
|
console.warn(
|
|
226
|
-
`[
|
|
279
|
+
`[rango] WARNING: ${autoIds.length} routers use auto-generated IDs (${autoIds.join(", ")}). ` +
|
|
227
280
|
`In multi-router setups, each createRouter() must have an explicit \`id\` option ` +
|
|
228
281
|
`to ensure per-router manifest data is matched correctly at runtime. ` +
|
|
229
282
|
`Example: createRouter({ id: "site", ... })`,
|
|
@@ -231,8 +284,15 @@ export async function discoverRouters(
|
|
|
231
284
|
}
|
|
232
285
|
}
|
|
233
286
|
|
|
287
|
+
debug?.(
|
|
288
|
+
"inner: generated manifests for %d router(s) (%sms)",
|
|
289
|
+
allManifests.length,
|
|
290
|
+
(performance.now() - manifestGenStart).toFixed(1),
|
|
291
|
+
);
|
|
292
|
+
|
|
234
293
|
// Build route trie from merged manifest + ancestry
|
|
235
294
|
let newMergedRouteTrie: any = null;
|
|
295
|
+
const trieStart = debug ? performance.now() : 0;
|
|
236
296
|
if (Object.keys(newMergedRouteManifest).length > 0) {
|
|
237
297
|
const buildRouteTrie = buildMod.buildRouteTrie;
|
|
238
298
|
if (buildRouteTrie && mergedRouteAncestry) {
|
|
@@ -268,18 +328,16 @@ export async function discoverRouters(
|
|
|
268
328
|
}
|
|
269
329
|
}
|
|
270
330
|
|
|
331
|
+
// buildRouteTrie reads these via ?.has / ?.[] — empty is observationally
|
|
332
|
+
// identical to undefined, so no empty->undefined coercion is needed.
|
|
271
333
|
newMergedRouteTrie = buildRouteTrie(
|
|
272
334
|
newMergedRouteManifest,
|
|
273
335
|
mergedRouteAncestry,
|
|
274
336
|
routeToStaticPrefix,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
passthroughRouteNames.size > 0 ? passthroughRouteNames : undefined,
|
|
280
|
-
Object.keys(mergedResponseTypeRoutes).length > 0
|
|
281
|
-
? mergedResponseTypeRoutes
|
|
282
|
-
: undefined,
|
|
337
|
+
mergedRouteTrailingSlash,
|
|
338
|
+
prerenderRouteNames,
|
|
339
|
+
passthroughRouteNames,
|
|
340
|
+
mergedResponseTypeRoutes,
|
|
283
341
|
);
|
|
284
342
|
|
|
285
343
|
// Build per-router tries for multi-router isolation.
|
|
@@ -306,26 +364,21 @@ export async function discoverRouters(
|
|
|
306
364
|
manifest.routeManifest,
|
|
307
365
|
manifest._routeAncestry,
|
|
308
366
|
perRouterStaticPrefix,
|
|
309
|
-
manifest.routeTrailingSlash
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
perRouterPrerenderNames && perRouterPrerenderNames.size > 0
|
|
314
|
-
? perRouterPrerenderNames
|
|
315
|
-
: undefined,
|
|
316
|
-
perRouterPassthroughNames && perRouterPassthroughNames.size > 0
|
|
317
|
-
? perRouterPassthroughNames
|
|
318
|
-
: undefined,
|
|
319
|
-
manifest.responseTypeRoutes &&
|
|
320
|
-
Object.keys(manifest.responseTypeRoutes).length > 0
|
|
321
|
-
? manifest.responseTypeRoutes
|
|
322
|
-
: undefined,
|
|
367
|
+
manifest.routeTrailingSlash,
|
|
368
|
+
perRouterPrerenderNames,
|
|
369
|
+
perRouterPassthroughNames,
|
|
370
|
+
manifest.responseTypeRoutes,
|
|
323
371
|
);
|
|
324
372
|
newPerRouterTrieMap.set(id, perRouterTrie);
|
|
325
373
|
}
|
|
326
374
|
}
|
|
327
375
|
}
|
|
328
376
|
|
|
377
|
+
debug?.(
|
|
378
|
+
"inner: trie build done (%sms)",
|
|
379
|
+
(performance.now() - trieStart).toFixed(1),
|
|
380
|
+
);
|
|
381
|
+
|
|
329
382
|
// Commit all local state to the shared discovery state atomically.
|
|
330
383
|
// This ensures a failed re-discovery (e.g. from a transient module
|
|
331
384
|
// evaluation error) preserves the last known-good state.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router discovery error aggregation.
|
|
3
|
+
*
|
|
4
|
+
* During host-router discovery the lazy mounts registered by a host router are
|
|
5
|
+
* invoked to trigger each sub-app's createRouter() registration. Some mount
|
|
6
|
+
* failures are expected in the temporary discovery server context (a sub-app may
|
|
7
|
+
* reference runtime-only bindings), so each is invoked defensively and its error
|
|
8
|
+
* is collected rather than thrown.
|
|
9
|
+
*
|
|
10
|
+
* Previously these errors were discarded with an empty `catch {}`. When a real
|
|
11
|
+
* failure - typically a sub-app whose router module fails to import - left the
|
|
12
|
+
* registry empty, discovery reported the misleading "No routers found" message
|
|
13
|
+
* with no trace of the underlying cause. The collected errors are now surfaced
|
|
14
|
+
* via the `DiscoveryError` thrown at the end of discovery (issue #499).
|
|
15
|
+
*
|
|
16
|
+
* Which entries to invoke is taken from the consumer's declared intent, not
|
|
17
|
+
* inferred from the function's shape. A host route is registered either with
|
|
18
|
+
* `.map((request) => Response)` (an inline request handler, `kind: "handler"`)
|
|
19
|
+
* or `.lazy(() => import("./sub-app"))` (a lazy mount, `kind: "lazy"`). Only
|
|
20
|
+
* `kind === "lazy"` entries are invoked here; inline handlers are never invoked
|
|
21
|
+
* during discovery (they need a Request and register no routers). Because a lazy
|
|
22
|
+
* entry is known to be a module loader, ANY failure it produces - a synchronous
|
|
23
|
+
* throw or a rejected promise - is a genuine discovery failure and is collected.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** An error caught (and previously swallowed) while resolving host routers. */
|
|
27
|
+
export interface CaughtDiscoveryError {
|
|
28
|
+
/** Human-readable description of where the error was caught. */
|
|
29
|
+
context: string;
|
|
30
|
+
/** The caught value (an Error or otherwise). */
|
|
31
|
+
error: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Minimal shape of a host registry entry needed for mount resolution.
|
|
36
|
+
* Mirrors the runtime HostRouterRegistry value without coupling to its type.
|
|
37
|
+
*/
|
|
38
|
+
interface HostRegistryRoute {
|
|
39
|
+
handler?: unknown;
|
|
40
|
+
kind?: string;
|
|
41
|
+
}
|
|
42
|
+
interface HostRegistryEntry {
|
|
43
|
+
routes: HostRegistryRoute[];
|
|
44
|
+
fallback?: HostRegistryRoute | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Indent every non-empty line of `text` by `pad`. */
|
|
48
|
+
function indent(text: string, pad: string): string {
|
|
49
|
+
return text
|
|
50
|
+
.split("\n")
|
|
51
|
+
.map((line) => (line.length > 0 ? pad + line : line))
|
|
52
|
+
.join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Invoke a single lazy mount to trigger its sub-app import (and createRouter()
|
|
57
|
+
* registration), collecting any failure under `context`. The entry is known to
|
|
58
|
+
* be a loader (`kind === "lazy"`), so both a synchronous throw and a rejected
|
|
59
|
+
* promise are genuine failures - no shape heuristics are needed.
|
|
60
|
+
*/
|
|
61
|
+
async function invokeLazyMount(
|
|
62
|
+
loader: () => unknown,
|
|
63
|
+
context: string,
|
|
64
|
+
errors: CaughtDiscoveryError[],
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
try {
|
|
67
|
+
await loader();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
errors.push({ context, error });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Whether a registry route is a `.lazy()` mount with an invokable loader. */
|
|
74
|
+
function isLazyMount(
|
|
75
|
+
route: HostRegistryRoute | null | undefined,
|
|
76
|
+
): route is { handler: () => unknown; kind: "lazy" } {
|
|
77
|
+
return (
|
|
78
|
+
!!route && route.kind === "lazy" && typeof route.handler === "function"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Invoke every lazy mount in the host registry to trigger sub-app
|
|
84
|
+
* createRouter() registration, collecting (not throwing) any failures.
|
|
85
|
+
*
|
|
86
|
+
* Only `.lazy()` entries are invoked; `.map()` inline request handlers are
|
|
87
|
+
* skipped (they need a Request and register no routers). Failures are returned
|
|
88
|
+
* rather than thrown because some mounts legitimately fail in the temporary
|
|
89
|
+
* discovery server context; the caller decides whether the failures matter,
|
|
90
|
+
* which is only when discovery finds no routers at all.
|
|
91
|
+
*/
|
|
92
|
+
export async function resolveHostRouterHandlers(
|
|
93
|
+
hostRegistry: Map<string, HostRegistryEntry>,
|
|
94
|
+
): Promise<CaughtDiscoveryError[]> {
|
|
95
|
+
const errors: CaughtDiscoveryError[] = [];
|
|
96
|
+
|
|
97
|
+
for (const [hostId, entry] of hostRegistry) {
|
|
98
|
+
for (const route of entry.routes) {
|
|
99
|
+
if (isLazyMount(route)) {
|
|
100
|
+
await invokeLazyMount(
|
|
101
|
+
route.handler,
|
|
102
|
+
`host "${hostId}" route handler`,
|
|
103
|
+
errors,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (isLazyMount(entry.fallback)) {
|
|
108
|
+
await invokeLazyMount(
|
|
109
|
+
entry.fallback.handler,
|
|
110
|
+
`host "${hostId}" fallback handler`,
|
|
111
|
+
errors,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return errors;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build the terminal "No routers found" message, appending any errors caught
|
|
121
|
+
* during host-router discovery so the real cause is visible.
|
|
122
|
+
*
|
|
123
|
+
* The aggregated errors are inlined into the message (in addition to being
|
|
124
|
+
* attached via `cause` on `DiscoveryError`) so they survive every caller: the
|
|
125
|
+
* dev/HMR paths log `err.message`, and the build path re-throws using
|
|
126
|
+
* `err.stack`, which begins with the message. None of those callers traverse
|
|
127
|
+
* `cause`, so the message must carry the detail. Each error includes its stack
|
|
128
|
+
* when available.
|
|
129
|
+
*/
|
|
130
|
+
export function formatNoRoutersError(
|
|
131
|
+
entryPath: string | undefined,
|
|
132
|
+
errors: CaughtDiscoveryError[],
|
|
133
|
+
): string {
|
|
134
|
+
const base = `[rango] No routers found in registry after importing ${entryPath}`;
|
|
135
|
+
if (errors.length === 0) {
|
|
136
|
+
return base;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const formatted = errors
|
|
140
|
+
.map(({ context, error }) => {
|
|
141
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
142
|
+
const detail = err.stack ?? err.message;
|
|
143
|
+
return ` - while resolving ${context}:\n${indent(detail, " ")}`;
|
|
144
|
+
})
|
|
145
|
+
.join("\n");
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
`${base}\n\n` +
|
|
149
|
+
`${errors.length} error(s) were caught during host-router discovery and ` +
|
|
150
|
+
`likely explain why no routers were registered:\n${formatted}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Reduce the caught errors to an `ErrorOptions.cause`: a single failure becomes
|
|
156
|
+
* the direct cause; multiple failures are wrapped in an `AggregateError` so
|
|
157
|
+
* each underlying error remains reachable. No errors -> no cause.
|
|
158
|
+
*/
|
|
159
|
+
function toCause(errors: CaughtDiscoveryError[]): unknown {
|
|
160
|
+
if (errors.length === 0) return undefined;
|
|
161
|
+
if (errors.length === 1) return errors[0].error;
|
|
162
|
+
return new AggregateError(
|
|
163
|
+
errors.map((e) => e.error),
|
|
164
|
+
"Multiple host-router handlers failed during discovery",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Thrown when router discovery completes without finding any routers.
|
|
170
|
+
*
|
|
171
|
+
* Carries the entry path and the individual failures caught while resolving
|
|
172
|
+
* host-router lazy handlers. The formatted detail is embedded in `message` (for
|
|
173
|
+
* callers that log `err.message`/`err.stack`) and the underlying error(s) are
|
|
174
|
+
* also attached via `cause` (a single failure directly, multiple wrapped in an
|
|
175
|
+
* `AggregateError`) for cause-aware tooling such as the Vite error overlay.
|
|
176
|
+
*/
|
|
177
|
+
export class DiscoveryError extends Error {
|
|
178
|
+
/** The entry file that was imported before discovery gave up. */
|
|
179
|
+
readonly entryPath: string | undefined;
|
|
180
|
+
/** Individual failures caught while resolving host-router handlers. */
|
|
181
|
+
readonly caught: CaughtDiscoveryError[];
|
|
182
|
+
|
|
183
|
+
constructor(entryPath: string | undefined, caught: CaughtDiscoveryError[]) {
|
|
184
|
+
super(formatNoRoutersError(entryPath, caught));
|
|
185
|
+
const cause = toCause(caught);
|
|
186
|
+
if (cause !== undefined) {
|
|
187
|
+
this.cause = cause;
|
|
188
|
+
}
|
|
189
|
+
this.name = "DiscoveryError";
|
|
190
|
+
this.entryPath = entryPath;
|
|
191
|
+
this.caught = caught;
|
|
192
|
+
Object.setPrototypeOf(this, DiscoveryError.prototype);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { Debugger } from "../debug.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manifest-readiness gate + rediscovery scheduler.
|
|
5
|
+
*
|
|
6
|
+
* Owns the four pieces of state that cooperate to keep
|
|
7
|
+
* `s.discoveryDone` (the promise the manifest virtual module's `load()`
|
|
8
|
+
* hook awaits) consistent across HMR fan-out:
|
|
9
|
+
*
|
|
10
|
+
* - **gatePending**: a Promise has been issued and not yet resolved.
|
|
11
|
+
* Workerd's manifest virtual module load() is blocked on it.
|
|
12
|
+
* - **inProgress**: a refresh's work callback is currently executing.
|
|
13
|
+
* - **queued**: a refresh was attempted while one was already in
|
|
14
|
+
* flight; the active run consumes this in its `finally` and
|
|
15
|
+
* recurses.
|
|
16
|
+
* - **pendingEvents**: a route-file event has been received (gate
|
|
17
|
+
* already reset) but the corresponding refresh's work hasn't started
|
|
18
|
+
* yet — i.e. the debounce hasn't fired. Set in `noteRouteEvent`,
|
|
19
|
+
* cleared at the start of each refresh cycle. Refresh's finally MUST
|
|
20
|
+
* hold the gate if this is true even when `queued` is false,
|
|
21
|
+
* otherwise an event whose debounce fires AFTER the active refresh
|
|
22
|
+
* completes (the "tail-race" window) would observe a resolved gate.
|
|
23
|
+
*
|
|
24
|
+
* The HMR-event flow (cloudflare-stress repro):
|
|
25
|
+
*
|
|
26
|
+
* t=0 Touch 1 → noteRouteEvent → pendingEvents=true, beginGate
|
|
27
|
+
* (gate1 pending)
|
|
28
|
+
* → debounce 100ms
|
|
29
|
+
* t=100 runRefreshCycle(work) → clear pendingEvents, work starts
|
|
30
|
+
* t=750 Touch 2 → noteRouteEvent → pendingEvents=true (no-op gate)
|
|
31
|
+
* → debounce fires at t=850
|
|
32
|
+
* t=800 refresh A's finally → queued=false, pendingEvents=true
|
|
33
|
+
* → HOLD gate (don't resolve)
|
|
34
|
+
* t=850 runRefreshCycle (debounce) → clear pendingEvents, work starts
|
|
35
|
+
* t=1500 refresh B's finally → queued=false, pendingEvents=false
|
|
36
|
+
* → resolveGate (gate1 resolves)
|
|
37
|
+
*
|
|
38
|
+
* @internal Exported only for unit tests.
|
|
39
|
+
*/
|
|
40
|
+
export interface DiscoveryGate {
|
|
41
|
+
/**
|
|
42
|
+
* Reset the gate to a fresh pending Promise via `s.discoveryDone`.
|
|
43
|
+
* No-op when a gate is already pending — file watchers can fire
|
|
44
|
+
* multiple events for one save, and replacing the resolver would
|
|
45
|
+
* orphan the original promise (workerd's manifest load() would hang).
|
|
46
|
+
*/
|
|
47
|
+
beginGate(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the current pending gate. No-op when no gate is pending.
|
|
50
|
+
* Called at the tail of the last refresh cycle in a burst.
|
|
51
|
+
*/
|
|
52
|
+
resolveGate(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Record that a route-file event has arrived. Sets `pendingEvents`
|
|
55
|
+
* and begins the gate. Idempotent for both flags.
|
|
56
|
+
*/
|
|
57
|
+
noteRouteEvent(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Run one refresh cycle, managing queue + pending state around it.
|
|
60
|
+
* If a cycle is already in flight, sets `queued=true` and returns.
|
|
61
|
+
* Otherwise clears `pendingEvents`, runs `work`, and in `finally`:
|
|
62
|
+
*
|
|
63
|
+
* - queued → recurse, gate stays pending
|
|
64
|
+
* - pendingEvents → hold gate (next debounced cycle resolves)
|
|
65
|
+
* - neither → resolveGate
|
|
66
|
+
*/
|
|
67
|
+
runRefreshCycle(work: () => Promise<void>): Promise<void>;
|
|
68
|
+
/** Snapshot of internal state. Test-only. */
|
|
69
|
+
readonly state: () => Readonly<{
|
|
70
|
+
gatePending: boolean;
|
|
71
|
+
inProgress: boolean;
|
|
72
|
+
queued: boolean;
|
|
73
|
+
pendingEvents: boolean;
|
|
74
|
+
}>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** State container the gate writes `discoveryDone` into. */
|
|
78
|
+
export interface GateOwner {
|
|
79
|
+
discoveryDone: Promise<void> | null | undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createDiscoveryGate(
|
|
83
|
+
s: GateOwner,
|
|
84
|
+
debug?: Debugger,
|
|
85
|
+
): DiscoveryGate {
|
|
86
|
+
let gatePending = false;
|
|
87
|
+
let gateResolver: () => void = () => {};
|
|
88
|
+
let inProgress = false;
|
|
89
|
+
let queued = false;
|
|
90
|
+
let pendingEvents = false;
|
|
91
|
+
|
|
92
|
+
const beginGate = (): void => {
|
|
93
|
+
if (gatePending) return;
|
|
94
|
+
s.discoveryDone = new Promise<void>((resolve) => {
|
|
95
|
+
gateResolver = resolve;
|
|
96
|
+
});
|
|
97
|
+
gatePending = true;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const resolveGate = (): void => {
|
|
101
|
+
if (!gatePending) return;
|
|
102
|
+
// Defer resolution while a refresh cycle is in flight or queued, or
|
|
103
|
+
// while an unprocessed route-file event is pending its debounce.
|
|
104
|
+
// Without this guard, cold-start's `discover().then(resolveGate)`
|
|
105
|
+
// could fire while an HMR-triggered runRefreshCycle is mid-flight,
|
|
106
|
+
// prematurely unblocking workerd's manifest load() against the
|
|
107
|
+
// stale cold-start gen. The active cycle's `finally` calls
|
|
108
|
+
// resolveGate again at the tail and finishes the resolution then.
|
|
109
|
+
if (inProgress || queued || pendingEvents) {
|
|
110
|
+
debug?.(
|
|
111
|
+
"hmr: resolveGate deferred — work in flight (inProgress=%s queued=%s pendingEvents=%s)",
|
|
112
|
+
inProgress,
|
|
113
|
+
queued,
|
|
114
|
+
pendingEvents,
|
|
115
|
+
);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
gatePending = false;
|
|
119
|
+
debug?.("hmr: discoveryDone resolved");
|
|
120
|
+
gateResolver();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const noteRouteEvent = (): void => {
|
|
124
|
+
pendingEvents = true;
|
|
125
|
+
beginGate();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const runRefreshCycle = async (work: () => Promise<void>): Promise<void> => {
|
|
129
|
+
if (inProgress) {
|
|
130
|
+
queued = true;
|
|
131
|
+
debug?.("hmr: rediscovery in flight — queued for a follow-up cycle");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Snapshot the current pendingEvents into "we're about to process";
|
|
135
|
+
// events arriving from now on re-set it.
|
|
136
|
+
pendingEvents = false;
|
|
137
|
+
inProgress = true;
|
|
138
|
+
try {
|
|
139
|
+
await work();
|
|
140
|
+
} finally {
|
|
141
|
+
inProgress = false;
|
|
142
|
+
if (queued) {
|
|
143
|
+
queued = false;
|
|
144
|
+
debug?.("hmr: consuming queued rediscovery");
|
|
145
|
+
runRefreshCycle(work).catch((err: unknown) => {
|
|
146
|
+
debug?.(
|
|
147
|
+
"hmr: queued cycle rejected — releasing gate (%s)",
|
|
148
|
+
err instanceof Error ? err.message : String(err),
|
|
149
|
+
);
|
|
150
|
+
// Belt-and-suspenders: even if the queued cycle's own try/catch
|
|
151
|
+
// missed something, ensure workerd doesn't hang.
|
|
152
|
+
resolveGate();
|
|
153
|
+
});
|
|
154
|
+
} else if (pendingEvents) {
|
|
155
|
+
debug?.(
|
|
156
|
+
"hmr: holding gate for pending events (debounce not yet fired)",
|
|
157
|
+
);
|
|
158
|
+
} else {
|
|
159
|
+
resolveGate();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
beginGate,
|
|
166
|
+
resolveGate,
|
|
167
|
+
noteRouteEvent,
|
|
168
|
+
runRefreshCycle,
|
|
169
|
+
state: () => ({ gatePending, inProgress, queued, pendingEvents }),
|
|
170
|
+
};
|
|
171
|
+
}
|