@rangojs/router 0.1.0 → 0.1.1
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/dist/bin/rango.js +11 -1
- package/dist/types/browser/dev-discovery.d.ts +11 -0
- package/dist/types/browser/navigation-store.d.ts +3 -13
- package/dist/types/browser/prefetch/loader.d.ts +10 -0
- package/dist/types/browser/prefetch/runtime.d.ts +3 -0
- package/dist/types/browser/types.d.ts +4 -16
- package/dist/types/build/generate-manifest.d.ts +6 -0
- package/dist/types/cache/cache-scope.d.ts +7 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +10 -9
- package/dist/types/cache/memory-segment-store.d.ts +6 -6
- package/dist/types/cache/shell-snapshot.d.ts +21 -8
- package/dist/types/cache/types.d.ts +24 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +9 -8
- package/dist/types/dev-discovery-protocol.d.ts +5 -0
- package/dist/types/prerender/store.d.ts +1 -0
- package/dist/types/route-map-builder.d.ts +4 -29
- package/dist/types/router/prerender-match.d.ts +4 -1
- package/dist/types/router/router-interfaces.d.ts +3 -1
- package/dist/types/rsc/handler-context.d.ts +1 -0
- package/dist/types/rsc/render-pipeline.d.ts +3 -2
- package/dist/types/rsc/rsc-rendering.d.ts +5 -10
- package/dist/types/rsc/shell-capture.d.ts +4 -8
- package/dist/types/rsc/types.d.ts +2 -0
- package/dist/types/server/context.d.ts +16 -0
- package/dist/types/server/request-context.d.ts +18 -8
- package/dist/types/server.d.ts +1 -1
- package/dist/types/types/route-entry.d.ts +3 -0
- package/dist/types/vite/discovery/state.d.ts +3 -2
- package/dist/types/vite/utils/manifest-utils.d.ts +1 -1
- package/dist/vite/index.js +194 -107
- package/package.json +7 -2
- package/skills/caching/SKILL.md +1 -1
- package/skills/ppr/SKILL.md +48 -4
- package/src/browser/dev-discovery.ts +66 -0
- package/src/browser/navigation-client.ts +6 -0
- package/src/browser/navigation-store.ts +4 -271
- package/src/browser/partial-update.ts +7 -14
- package/src/browser/prefetch/cache.ts +1 -1
- package/src/browser/prefetch/loader.ts +110 -0
- package/src/browser/prefetch/runtime.ts +7 -0
- package/src/browser/react/Link.tsx +7 -10
- package/src/browser/react/NavigationProvider.tsx +1 -1
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/rsc-router.tsx +4 -2
- package/src/browser/types.ts +4 -27
- package/src/build/generate-manifest.ts +11 -0
- package/src/cache/cache-scope.ts +17 -6
- package/src/cache/cf/cf-cache-store.ts +63 -39
- package/src/cache/memory-segment-store.ts +17 -6
- package/src/cache/shell-snapshot.ts +45 -15
- package/src/cache/types.ts +23 -6
- package/src/cache/vercel/vercel-cache-store.ts +52 -22
- package/src/dev-discovery-protocol.ts +11 -0
- package/src/prerender/build-shell-capture.ts +7 -1
- package/src/prerender/store.ts +5 -0
- package/src/route-definition/dsl-helpers.ts +7 -2
- package/src/route-map-builder.ts +56 -49
- package/src/router/handler-context.ts +13 -5
- package/src/router/lazy-includes.ts +1 -0
- package/src/router/match-api.ts +8 -4
- package/src/router/prerender-match.ts +19 -2
- package/src/router/router-interfaces.ts +4 -0
- package/src/router/segment-resolution/static-store.ts +36 -10
- package/src/router.ts +34 -3
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +3 -1
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +4 -11
- package/src/rsc/render-pipeline.ts +10 -2
- package/src/rsc/rsc-rendering.ts +207 -146
- package/src/rsc/shell-capture.ts +34 -11
- package/src/rsc/types.ts +2 -0
- package/src/server/context.ts +28 -0
- package/src/server/request-context.ts +32 -10
- package/src/server.ts +0 -2
- package/src/testing/dispatch.ts +1 -0
- package/src/types/route-entry.ts +3 -0
- package/src/urls/include-helper.ts +1 -0
- package/src/urls/path-helper.ts +8 -4
- package/src/vite/discovery/bundle-postprocess.ts +10 -7
- package/src/vite/discovery/discover-routers.ts +7 -66
- package/src/vite/discovery/prerender-collection.ts +13 -2
- package/src/vite/discovery/state.ts +4 -4
- package/src/vite/discovery/virtual-module-codegen.ts +75 -42
- package/src/vite/plugins/version-injector.ts +0 -1
- package/src/vite/plugins/virtual-entries.ts +11 -1
- package/src/vite/router-discovery.ts +132 -22
- package/src/vite/utils/manifest-utils.ts +1 -4
- package/src/vite/utils/shared-utils.ts +7 -0
package/dist/vite/index.js
CHANGED
|
@@ -2309,7 +2309,17 @@ async function initializeApp() {
|
|
|
2309
2309
|
// context, including strictMode (default true) from createRouter. StrictMode
|
|
2310
2310
|
// is the default; createRouter({ strictMode: false }) ships the opt-out in the
|
|
2311
2311
|
// payload metadata. StrictMode emits no DOM, so toggling never changes markup.
|
|
2312
|
-
const { strictMode } = await initBrowserApp({ rscStream, deps });
|
|
2312
|
+
const { strictMode, initialPayload } = await initBrowserApp({ rscStream, deps });
|
|
2313
|
+
|
|
2314
|
+
if (import.meta.hot) {
|
|
2315
|
+
const { startDevDiscoveryHandshake } = await import(
|
|
2316
|
+
"@rangojs/router/internal/browser/dev-discovery"
|
|
2317
|
+
);
|
|
2318
|
+
startDevDiscoveryHandshake(
|
|
2319
|
+
initialPayload.metadata?.devDiscoveryEpoch,
|
|
2320
|
+
import.meta.hot
|
|
2321
|
+
);
|
|
2322
|
+
}
|
|
2313
2323
|
|
|
2314
2324
|
const app = createElement(Rango);
|
|
2315
2325
|
hydrateRoot(
|
|
@@ -2520,7 +2530,7 @@ import { resolve } from "node:path";
|
|
|
2520
2530
|
// package.json
|
|
2521
2531
|
var package_default = {
|
|
2522
2532
|
name: "@rangojs/router",
|
|
2523
|
-
version: "0.1.
|
|
2533
|
+
version: "0.1.1",
|
|
2524
2534
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
2525
2535
|
keywords: [
|
|
2526
2536
|
"react",
|
|
@@ -2620,6 +2630,10 @@ var package_default = {
|
|
|
2620
2630
|
"react-server": "./src/rsc/handler.ts",
|
|
2621
2631
|
default: "./src/rsc/handler.ts"
|
|
2622
2632
|
},
|
|
2633
|
+
"./internal/browser/dev-discovery": {
|
|
2634
|
+
types: "./dist/types/browser/dev-discovery.d.ts",
|
|
2635
|
+
default: "./src/browser/dev-discovery.ts"
|
|
2636
|
+
},
|
|
2623
2637
|
"./cache": {
|
|
2624
2638
|
types: "./dist/types/cache/index.d.ts",
|
|
2625
2639
|
"react-server": "./src/cache/index.ts",
|
|
@@ -2699,7 +2713,8 @@ var package_default = {
|
|
|
2699
2713
|
typecheck: "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
2700
2714
|
test: "playwright test",
|
|
2701
2715
|
"test:ui": "playwright test --ui",
|
|
2702
|
-
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-
|
|
2716
|
+
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-basename --project=hmr-prerender --no-deps --workers=1 && RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
2717
|
+
"test:hmr-routes-local": "RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
2703
2718
|
"test:unit": "pnpm run build:types && vitest run",
|
|
2704
2719
|
"test:unit:watch": "vitest",
|
|
2705
2720
|
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
@@ -4236,6 +4251,11 @@ function onwarn(warning, defaultHandler) {
|
|
|
4236
4251
|
}
|
|
4237
4252
|
function getManualChunks(id) {
|
|
4238
4253
|
const normalized = Vite.normalizePath(id);
|
|
4254
|
+
if (/\/browser\/prefetch\/(?:runtime|fetch|queue|observer|policy|resource-ready)\.[cm]?[jt]sx?(?:[?#].*)?$/.test(
|
|
4255
|
+
normalized
|
|
4256
|
+
)) {
|
|
4257
|
+
return void 0;
|
|
4258
|
+
}
|
|
4239
4259
|
if (normalized.includes("node_modules/react/") || normalized.includes("node_modules/react-dom/") || normalized.includes("node_modules/react-server-dom-webpack/") || normalized.includes("node_modules/@vitejs/plugin-rsc/")) {
|
|
4240
4260
|
return "react";
|
|
4241
4261
|
}
|
|
@@ -4673,6 +4693,12 @@ import { readFileSync as readFileSync7 } from "node:fs";
|
|
|
4673
4693
|
import { createRequire as createRequire3, register } from "node:module";
|
|
4674
4694
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
4675
4695
|
|
|
4696
|
+
// src/dev-discovery-protocol.ts
|
|
4697
|
+
var DEV_DISCOVERY_READY_EVENT = "rango:dev-discovery-ready";
|
|
4698
|
+
var DEV_DISCOVERY_QUERY_EVENT = "rango:dev-discovery-query";
|
|
4699
|
+
var DEV_DISCOVERY_PROBE_HEADER = "x-rango-dev-discovery-probe";
|
|
4700
|
+
var DEV_DISCOVERY_EPOCH_HEADER = "x-rango-dev-discovery-epoch";
|
|
4701
|
+
|
|
4676
4702
|
// src/rsc/shell-serve.ts
|
|
4677
4703
|
import React from "react";
|
|
4678
4704
|
|
|
@@ -4955,8 +4981,6 @@ function createDiscoveryState(entryPath, opts) {
|
|
|
4955
4981
|
opts,
|
|
4956
4982
|
mergedRouteManifest: null,
|
|
4957
4983
|
perRouterManifests: [],
|
|
4958
|
-
mergedPrecomputedEntries: null,
|
|
4959
|
-
mergedRouteTrie: null,
|
|
4960
4984
|
perRouterTrieMap: /* @__PURE__ */ new Map(),
|
|
4961
4985
|
perRouterPrecomputedMap: /* @__PURE__ */ new Map(),
|
|
4962
4986
|
perRouterManifestDataMap: /* @__PURE__ */ new Map(),
|
|
@@ -4971,6 +4995,7 @@ function createDiscoveryState(entryPath, opts) {
|
|
|
4971
4995
|
resolvedPrerenderModules: void 0,
|
|
4972
4996
|
resolvedStaticModules: void 0,
|
|
4973
4997
|
discoveryDone: null,
|
|
4998
|
+
devDiscoveryEpoch: opts?.preset === "cloudflare" ? Date.now() : void 0,
|
|
4974
4999
|
devServerOrigin: null,
|
|
4975
5000
|
devServer: null,
|
|
4976
5001
|
selfWrittenGenFiles: /* @__PURE__ */ new Map(),
|
|
@@ -5887,15 +5912,21 @@ async function renderStaticHandlers(state, rscEnv, registry) {
|
|
|
5887
5912
|
if (def.options?.passthrough) continue;
|
|
5888
5913
|
const startHandler = performance.now();
|
|
5889
5914
|
let handled = false;
|
|
5890
|
-
|
|
5915
|
+
const owner = def.$$routerId;
|
|
5916
|
+
const owned = owner !== void 0 ? registry.get(owner) : void 0;
|
|
5917
|
+
const candidates = owned ? [owned] : [...registry.values()];
|
|
5918
|
+
for (const routerInstance of candidates) {
|
|
5891
5919
|
if (!routerInstance.renderStaticSegment) continue;
|
|
5892
5920
|
try {
|
|
5893
5921
|
const result = await routerInstance.renderStaticSegment(
|
|
5894
5922
|
def.handler,
|
|
5895
5923
|
def.$$id,
|
|
5896
|
-
|
|
5924
|
+
// Full route name when stamped; the historical $$routePrefix is a
|
|
5925
|
+
// name PREFIX (never in the root-scope registry) kept as fallback.
|
|
5926
|
+
def.$$routeName ?? def.$$routePrefix,
|
|
5897
5927
|
state.resolvedBuildEnv,
|
|
5898
|
-
!state.isBuildMode
|
|
5928
|
+
!state.isBuildMode,
|
|
5929
|
+
def.$$rootScoped
|
|
5899
5930
|
);
|
|
5900
5931
|
if (result) {
|
|
5901
5932
|
const hasHandles = result.handles !== "";
|
|
@@ -6107,12 +6138,10 @@ async function discoverRouters(state, rscEnv) {
|
|
|
6107
6138
|
throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
|
|
6108
6139
|
}
|
|
6109
6140
|
const newMergedRouteManifest = {};
|
|
6110
|
-
const newMergedPrecomputedEntries = [];
|
|
6111
6141
|
const newPerRouterManifests = [];
|
|
6112
6142
|
const newPerRouterManifestDataMap = /* @__PURE__ */ new Map();
|
|
6113
6143
|
const newPerRouterPrecomputedMap = /* @__PURE__ */ new Map();
|
|
6114
6144
|
const newPerRouterTrieMap = /* @__PURE__ */ new Map();
|
|
6115
|
-
let mergedRouteTrailingSlash = {};
|
|
6116
6145
|
let routerMountIndex = 0;
|
|
6117
6146
|
const allManifests = [];
|
|
6118
6147
|
const clientChunkCtx = state.opts?.clientChunkCtx;
|
|
@@ -6133,6 +6162,7 @@ async function discoverRouters(state, rscEnv) {
|
|
|
6133
6162
|
router.urlpatterns,
|
|
6134
6163
|
routerMountIndex,
|
|
6135
6164
|
{
|
|
6165
|
+
routerId: id,
|
|
6136
6166
|
...router.__basename ? { urlPrefix: router.__basename } : {},
|
|
6137
6167
|
...collectClientFallbackRef ? { collectClientFallbackRef } : {}
|
|
6138
6168
|
}
|
|
@@ -6170,20 +6200,17 @@ async function discoverRouters(state, rscEnv) {
|
|
|
6170
6200
|
newPerRouterManifests.push({
|
|
6171
6201
|
id,
|
|
6172
6202
|
routeManifest: manifest.routeManifest,
|
|
6203
|
+
routeTrailingSlash: manifest.routeTrailingSlash,
|
|
6173
6204
|
routeSearchSchemas: manifest.routeSearchSchemas,
|
|
6174
6205
|
sourceFile: router.__sourceFile,
|
|
6175
6206
|
factoryOnlyPrefixes
|
|
6176
6207
|
});
|
|
6177
|
-
if (manifest.routeTrailingSlash) {
|
|
6178
|
-
Object.assign(mergedRouteTrailingSlash, manifest.routeTrailingSlash);
|
|
6179
|
-
}
|
|
6180
6208
|
const routerPrecomputed = [];
|
|
6181
6209
|
flattenLeafEntries(
|
|
6182
6210
|
manifest.prefixTree,
|
|
6183
6211
|
manifest.routeManifest,
|
|
6184
6212
|
routerPrecomputed
|
|
6185
6213
|
);
|
|
6186
|
-
newMergedPrecomputedEntries.push(...routerPrecomputed);
|
|
6187
6214
|
newPerRouterManifestDataMap.set(id, manifest.routeManifest);
|
|
6188
6215
|
newPerRouterPrecomputedMap.set(id, routerPrecomputed);
|
|
6189
6216
|
console.log(
|
|
@@ -6205,44 +6232,8 @@ async function discoverRouters(state, rscEnv) {
|
|
|
6205
6232
|
allManifests.length,
|
|
6206
6233
|
(performance.now() - manifestGenStart).toFixed(1)
|
|
6207
6234
|
);
|
|
6208
|
-
let newMergedRouteTrie = null;
|
|
6209
6235
|
const trieStart = debug10 ? performance.now() : 0;
|
|
6210
6236
|
if (Object.keys(newMergedRouteManifest).length > 0) {
|
|
6211
|
-
const routeToStaticPrefix = {};
|
|
6212
|
-
for (const { manifest } of allManifests) {
|
|
6213
|
-
for (const name of Object.keys(manifest.routeManifest)) {
|
|
6214
|
-
if (!(name in routeToStaticPrefix)) {
|
|
6215
|
-
routeToStaticPrefix[name] = "";
|
|
6216
|
-
}
|
|
6217
|
-
}
|
|
6218
|
-
buildRouteToStaticPrefix(manifest.prefixTree, routeToStaticPrefix);
|
|
6219
|
-
}
|
|
6220
|
-
const prerenderRouteNames = /* @__PURE__ */ new Set();
|
|
6221
|
-
const passthroughRouteNames = /* @__PURE__ */ new Set();
|
|
6222
|
-
const mergedResponseTypeRoutes = {};
|
|
6223
|
-
for (const { manifest } of allManifests) {
|
|
6224
|
-
if (manifest.prerenderRoutes) {
|
|
6225
|
-
for (const name of manifest.prerenderRoutes) {
|
|
6226
|
-
prerenderRouteNames.add(name);
|
|
6227
|
-
}
|
|
6228
|
-
}
|
|
6229
|
-
if (manifest.passthroughRoutes) {
|
|
6230
|
-
for (const name of manifest.passthroughRoutes) {
|
|
6231
|
-
passthroughRouteNames.add(name);
|
|
6232
|
-
}
|
|
6233
|
-
}
|
|
6234
|
-
if (manifest.responseTypeRoutes) {
|
|
6235
|
-
Object.assign(mergedResponseTypeRoutes, manifest.responseTypeRoutes);
|
|
6236
|
-
}
|
|
6237
|
-
}
|
|
6238
|
-
newMergedRouteTrie = buildRouteTrie(
|
|
6239
|
-
newMergedRouteManifest,
|
|
6240
|
-
routeToStaticPrefix,
|
|
6241
|
-
mergedRouteTrailingSlash,
|
|
6242
|
-
prerenderRouteNames,
|
|
6243
|
-
passthroughRouteNames,
|
|
6244
|
-
mergedResponseTypeRoutes
|
|
6245
|
-
);
|
|
6246
6237
|
for (const { id, manifest } of allManifests) {
|
|
6247
6238
|
const perRouterTrie = buildPerRouterTrie(manifest);
|
|
6248
6239
|
if (perRouterTrie) {
|
|
@@ -6255,15 +6246,10 @@ async function discoverRouters(state, rscEnv) {
|
|
|
6255
6246
|
(performance.now() - trieStart).toFixed(1)
|
|
6256
6247
|
);
|
|
6257
6248
|
state.mergedRouteManifest = newMergedRouteManifest;
|
|
6258
|
-
state.mergedPrecomputedEntries = newMergedPrecomputedEntries;
|
|
6259
6249
|
state.perRouterManifests = newPerRouterManifests;
|
|
6260
6250
|
state.perRouterManifestDataMap = newPerRouterManifestDataMap;
|
|
6261
6251
|
state.perRouterPrecomputedMap = newPerRouterPrecomputedMap;
|
|
6262
6252
|
state.perRouterTrieMap = newPerRouterTrieMap;
|
|
6263
|
-
state.mergedRouteTrie = newMergedRouteTrie;
|
|
6264
|
-
if (serverMod.setRouteTrie && newMergedRouteTrie) {
|
|
6265
|
-
serverMod.setRouteTrie(newMergedRouteTrie);
|
|
6266
|
-
}
|
|
6267
6253
|
const perRouterSetters = [
|
|
6268
6254
|
[newPerRouterManifestDataMap, "setRouterManifest"],
|
|
6269
6255
|
[newPerRouterTrieMap, "setRouterTrie"],
|
|
@@ -6696,12 +6682,18 @@ function supplementGenFilesWithRuntimeRoutes(state) {
|
|
|
6696
6682
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6697
6683
|
import { dirname as dirname4, basename, join as join6 } from "node:path";
|
|
6698
6684
|
var MANIFEST_EXTERNALIZE_THRESHOLD = 512 * 1024;
|
|
6685
|
+
function devDiscoveryBootstrap(state) {
|
|
6686
|
+
if (state.isBuildMode || state.opts?.preset !== "cloudflare" || state.devDiscoveryEpoch === void 0) {
|
|
6687
|
+
return [];
|
|
6688
|
+
}
|
|
6689
|
+
const epoch = state.devDiscoveryEpoch;
|
|
6690
|
+
return [`globalThis.__RANGO_DEV_DISCOVERY_EPOCH = ${epoch};`];
|
|
6691
|
+
}
|
|
6699
6692
|
function generateRoutesManifestModule(state) {
|
|
6700
6693
|
const hasManifest = state.mergedRouteManifest && Object.keys(state.mergedRouteManifest).length > 0;
|
|
6701
6694
|
if (hasManifest) {
|
|
6702
6695
|
const genFileImports = [];
|
|
6703
|
-
const
|
|
6704
|
-
const routersWithoutGenFile = [];
|
|
6696
|
+
const genFileVarByEntry = [];
|
|
6705
6697
|
let varIdx = 0;
|
|
6706
6698
|
for (const entry of state.perRouterManifests) {
|
|
6707
6699
|
if (entry.sourceFile) {
|
|
@@ -6718,16 +6710,19 @@ function generateRoutesManifestModule(state) {
|
|
|
6718
6710
|
genFileImports.push(
|
|
6719
6711
|
`import { NamedRoutes as ${varName} } from ${JSON.stringify(genPath)};`
|
|
6720
6712
|
);
|
|
6721
|
-
|
|
6713
|
+
genFileVarByEntry.push(varName);
|
|
6722
6714
|
} else {
|
|
6723
|
-
|
|
6724
|
-
id: entry.id,
|
|
6725
|
-
manifest: entry.routeManifest
|
|
6726
|
-
});
|
|
6715
|
+
genFileVarByEntry.push(null);
|
|
6727
6716
|
}
|
|
6728
6717
|
}
|
|
6729
|
-
const
|
|
6730
|
-
|
|
6718
|
+
const serverImports = [
|
|
6719
|
+
"setCachedManifest",
|
|
6720
|
+
"setRouterManifest",
|
|
6721
|
+
...state.isBuildMode ? ["registerRouterManifestLoader"] : [],
|
|
6722
|
+
"clearAllRouterData"
|
|
6723
|
+
];
|
|
6724
|
+
const lines2 = [
|
|
6725
|
+
`import { ${serverImports.join(", ")} } from "@rangojs/router/server";`,
|
|
6731
6726
|
...genFileImports,
|
|
6732
6727
|
// Clear stale per-router cached data (manifest, trie, precomputed entries)
|
|
6733
6728
|
// before re-populating. In Cloudflare dev mode, program reloads re-evaluate
|
|
@@ -6736,53 +6731,56 @@ function generateRoutesManifestModule(state) {
|
|
|
6736
6731
|
// handler finds stale trie data and never rebuilds from updated urlpatterns.
|
|
6737
6732
|
`clearAllRouterData();`
|
|
6738
6733
|
];
|
|
6739
|
-
if (
|
|
6740
|
-
|
|
6734
|
+
if (varIdx > 0) {
|
|
6735
|
+
lines2.push(
|
|
6741
6736
|
`function __flat(r) { const o = {}; for (const [k, v] of Object.entries(r)) o[k] = typeof v === "string" ? v : v.path; return o; }`
|
|
6742
6737
|
);
|
|
6743
6738
|
}
|
|
6744
|
-
|
|
6745
|
-
|
|
6739
|
+
const routerMapVars = [];
|
|
6740
|
+
for (const [i, entry] of state.perRouterManifests.entries()) {
|
|
6741
|
+
const mapVar = `__m${i}`;
|
|
6742
|
+
const genVar = genFileVarByEntry[i];
|
|
6743
|
+
const init = genVar ? `__flat(${genVar})` : jsonParseExpression(entry.routeManifest);
|
|
6744
|
+
lines2.push(`const ${mapVar} = ${init};`);
|
|
6745
|
+
routerMapVars.push(mapVar);
|
|
6746
|
+
}
|
|
6747
|
+
if (routerMapVars.length === 1) {
|
|
6748
|
+
lines2.push(`setCachedManifest(${routerMapVars[0]});`);
|
|
6746
6749
|
} else {
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
parts.push(`...${jsonParseExpression(manifest)}`);
|
|
6751
|
-
lines.push(`setCachedManifest({ ${parts.join(", ")} });`);
|
|
6750
|
+
lines2.push(
|
|
6751
|
+
`setCachedManifest({ ${routerMapVars.map((v) => `...${v}`).join(", ")} });`
|
|
6752
|
+
);
|
|
6752
6753
|
}
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
`setRouterManifest(${JSON.stringify(entry.id)}, ${jsonParseExpression(entry.routeManifest)});`
|
|
6754
|
+
for (const [i, entry] of state.perRouterManifests.entries()) {
|
|
6755
|
+
lines2.push(
|
|
6756
|
+
`setRouterManifest(${JSON.stringify(entry.id)}, ${routerMapVars[i]});`
|
|
6757
|
+
);
|
|
6758
|
+
}
|
|
6759
|
+
if (state.isBuildMode) {
|
|
6760
|
+
for (const routerId of state.perRouterManifestDataMap.keys()) {
|
|
6761
|
+
lines2.push(
|
|
6762
|
+
`registerRouterManifestLoader(${JSON.stringify(routerId)}, () => import(${JSON.stringify(VIRTUAL_ROUTES_MANIFEST_ID + "/" + routerId)}));`
|
|
6763
6763
|
);
|
|
6764
6764
|
}
|
|
6765
6765
|
}
|
|
6766
|
-
for (const routerId of state.perRouterManifestDataMap.keys()) {
|
|
6767
|
-
lines.push(
|
|
6768
|
-
`registerRouterManifestLoader(${JSON.stringify(routerId)}, () => import(${JSON.stringify(VIRTUAL_ROUTES_MANIFEST_ID + "/" + routerId)}));`
|
|
6769
|
-
);
|
|
6770
|
-
}
|
|
6771
6766
|
if (!state.isBuildMode && state.devServerOrigin) {
|
|
6772
|
-
|
|
6767
|
+
lines2.push(
|
|
6773
6768
|
`globalThis.__PRERENDER_DEV_URL = ${JSON.stringify(state.devServerOrigin)};`
|
|
6774
6769
|
);
|
|
6775
6770
|
}
|
|
6776
|
-
|
|
6771
|
+
lines2.push(...devDiscoveryBootstrap(state));
|
|
6772
|
+
return lines2.join("\n");
|
|
6777
6773
|
}
|
|
6774
|
+
const lines = [];
|
|
6778
6775
|
if (!state.isBuildMode) {
|
|
6779
6776
|
const origin = state.devServerOrigin || state.devServer?.resolvedUrls?.local?.[0]?.replace(/\/$/, "") || state.devServer && `http://localhost:${state.devServer.config.server.port || 5173}`;
|
|
6780
6777
|
if (origin) {
|
|
6781
6778
|
state.devServerOrigin = origin;
|
|
6782
|
-
|
|
6779
|
+
lines.push(`globalThis.__PRERENDER_DEV_URL = ${JSON.stringify(origin)};`);
|
|
6783
6780
|
}
|
|
6784
6781
|
}
|
|
6785
|
-
|
|
6782
|
+
lines.push(...devDiscoveryBootstrap(state));
|
|
6783
|
+
return lines.join("\n") || `// Route manifest will be populated at runtime`;
|
|
6786
6784
|
}
|
|
6787
6785
|
function shortHash(input) {
|
|
6788
6786
|
let h = 2166136261;
|
|
@@ -6965,7 +6963,7 @@ function postprocessBundle(state) {
|
|
|
6965
6963
|
);
|
|
6966
6964
|
writeFileSync6(manifestPath, manifestCode);
|
|
6967
6965
|
totalBytes += Buffer.byteLength(manifestCode);
|
|
6968
|
-
const injection = `import
|
|
6966
|
+
const injection = `globalThis.__loadStaticManifestModule = () => import("./__static-manifest.js");
|
|
6969
6967
|
`;
|
|
6970
6968
|
writeFileSync6(rscEntryPath, injection + rscCode);
|
|
6971
6969
|
const totalKB = (totalBytes / 1024).toFixed(1);
|
|
@@ -7300,6 +7298,43 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
|
7300
7298
|
if (s.isBuildMode) return;
|
|
7301
7299
|
if (globalThis.__rscRouterDiscoveryActive) return;
|
|
7302
7300
|
s.devServer = server;
|
|
7301
|
+
let workerReadyEpoch;
|
|
7302
|
+
const publishDevDiscoveryReady = (epoch) => {
|
|
7303
|
+
if (epoch <= (workerReadyEpoch ?? -1)) return;
|
|
7304
|
+
workerReadyEpoch = epoch;
|
|
7305
|
+
server.environments?.client?.hot?.send({
|
|
7306
|
+
type: "custom",
|
|
7307
|
+
event: DEV_DISCOVERY_READY_EVENT,
|
|
7308
|
+
data: { epoch }
|
|
7309
|
+
});
|
|
7310
|
+
debugDiscovery?.("hmr: workerd ready at epoch %d", epoch);
|
|
7311
|
+
};
|
|
7312
|
+
if (opts?.preset === "cloudflare") {
|
|
7313
|
+
const registeredClientHotChannels = /* @__PURE__ */ new Set();
|
|
7314
|
+
const registerDevDiscoveryHotChannels = () => {
|
|
7315
|
+
const clientHot = server.environments?.client?.hot;
|
|
7316
|
+
if (clientHot && !registeredClientHotChannels.has(clientHot)) {
|
|
7317
|
+
registeredClientHotChannels.add(clientHot);
|
|
7318
|
+
clientHot.on(
|
|
7319
|
+
DEV_DISCOVERY_QUERY_EVENT,
|
|
7320
|
+
(_payload, client) => {
|
|
7321
|
+
if (workerReadyEpoch === void 0) return;
|
|
7322
|
+
client.send(DEV_DISCOVERY_READY_EVENT, {
|
|
7323
|
+
epoch: workerReadyEpoch
|
|
7324
|
+
});
|
|
7325
|
+
}
|
|
7326
|
+
);
|
|
7327
|
+
}
|
|
7328
|
+
debugDiscovery?.(
|
|
7329
|
+
"hmr: dev discovery browser channel registered (client=%s)",
|
|
7330
|
+
!!clientHot
|
|
7331
|
+
);
|
|
7332
|
+
};
|
|
7333
|
+
registerDevDiscoveryHotChannels();
|
|
7334
|
+
if (server.httpServer && !server.httpServer.listening) {
|
|
7335
|
+
server.httpServer.once("listening", registerDevDiscoveryHotChannels);
|
|
7336
|
+
}
|
|
7337
|
+
}
|
|
7303
7338
|
server.middlewares.use(internalDebugNoCacheMiddleware());
|
|
7304
7339
|
let resolveDiscovery;
|
|
7305
7340
|
const discoveryPromise = new Promise((resolve13) => {
|
|
@@ -7309,9 +7344,11 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
|
7309
7344
|
const beginDiscoveryGate = gate.beginGate;
|
|
7310
7345
|
const resolveDiscoveryGate = gate.resolveGate;
|
|
7311
7346
|
const getDevServerOrigin = () => server.resolvedUrls?.local?.[0]?.replace(/\/$/, "") || `http://localhost:${server.config.server.port || 5173}`;
|
|
7347
|
+
let devServerClosed = false;
|
|
7312
7348
|
let prerenderTempServer = null;
|
|
7313
7349
|
let prerenderNodeRegistry = null;
|
|
7314
7350
|
server.httpServer?.on("close", () => {
|
|
7351
|
+
devServerClosed = true;
|
|
7315
7352
|
if (prerenderTempServer) {
|
|
7316
7353
|
prerenderTempServer.close().catch(() => {
|
|
7317
7354
|
});
|
|
@@ -7587,12 +7624,6 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
|
7587
7624
|
if (s.mergedRouteManifest && serverMod.setCachedManifest) {
|
|
7588
7625
|
serverMod.setCachedManifest(s.mergedRouteManifest);
|
|
7589
7626
|
}
|
|
7590
|
-
if (s.mergedPrecomputedEntries && s.mergedPrecomputedEntries.length > 0 && serverMod.setPrecomputedEntries) {
|
|
7591
|
-
serverMod.setPrecomputedEntries(s.mergedPrecomputedEntries);
|
|
7592
|
-
}
|
|
7593
|
-
if (s.mergedRouteTrie && serverMod.setRouteTrie) {
|
|
7594
|
-
serverMod.setRouteTrie(s.mergedRouteTrie);
|
|
7595
|
-
}
|
|
7596
7627
|
const perRouterSetters = [
|
|
7597
7628
|
[s.perRouterManifestDataMap, "setRouterManifest"],
|
|
7598
7629
|
[s.perRouterTrieMap, "setRouterTrie"],
|
|
@@ -7928,6 +7959,21 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
|
7928
7959
|
writeCombinedRouteTypesWithTracking(s);
|
|
7929
7960
|
}
|
|
7930
7961
|
};
|
|
7962
|
+
const routeShapeSignature = () => JSON.stringify(
|
|
7963
|
+
s.perRouterManifests.map(
|
|
7964
|
+
({
|
|
7965
|
+
id,
|
|
7966
|
+
routeManifest,
|
|
7967
|
+
routeTrailingSlash,
|
|
7968
|
+
routeSearchSchemas
|
|
7969
|
+
}) => ({
|
|
7970
|
+
id,
|
|
7971
|
+
routeManifest,
|
|
7972
|
+
routeTrailingSlash,
|
|
7973
|
+
routeSearchSchemas
|
|
7974
|
+
})
|
|
7975
|
+
)
|
|
7976
|
+
);
|
|
7931
7977
|
const maybeHandleGeneratedRouteFileMutation = (filePath) => {
|
|
7932
7978
|
if (!isGeneratedRouteFile(filePath)) return false;
|
|
7933
7979
|
if (consumeSelfGenWrite(s, filePath)) return true;
|
|
@@ -7941,6 +7987,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
|
7941
7987
|
const rscEnv = server.environments?.rsc;
|
|
7942
7988
|
const hasMainRunner = !!rscEnv?.runner;
|
|
7943
7989
|
if (!hasMainRunner && s.perRouterManifests.length === 0) return;
|
|
7990
|
+
let previousRouteShape = routeShapeSignature();
|
|
7944
7991
|
await gate.runRefreshCycle(async () => {
|
|
7945
7992
|
const hmrStart = performance.now();
|
|
7946
7993
|
try {
|
|
@@ -7989,7 +8036,19 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
|
7989
8036
|
);
|
|
7990
8037
|
s.lastDiscoveryError = null;
|
|
7991
8038
|
}
|
|
7992
|
-
if (rscEnv && !rscEnv.runner)
|
|
8039
|
+
if (rscEnv && !rscEnv.runner) {
|
|
8040
|
+
const nextRouteShape = routeShapeSignature();
|
|
8041
|
+
let expectedEpoch;
|
|
8042
|
+
if (nextRouteShape !== previousRouteShape) {
|
|
8043
|
+
expectedEpoch = Math.max(
|
|
8044
|
+
Date.now(),
|
|
8045
|
+
(s.devDiscoveryEpoch ?? 0) + 1
|
|
8046
|
+
);
|
|
8047
|
+
s.devDiscoveryEpoch = expectedEpoch;
|
|
8048
|
+
}
|
|
8049
|
+
previousRouteShape = nextRouteShape;
|
|
8050
|
+
forceCloudflareWorkerReload(rscEnv, expectedEpoch);
|
|
8051
|
+
}
|
|
7993
8052
|
} catch (err) {
|
|
7994
8053
|
s.lastDiscoveryError = {
|
|
7995
8054
|
message: err?.message ?? String(err),
|
|
@@ -8010,10 +8069,10 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
|
8010
8069
|
}
|
|
8011
8070
|
});
|
|
8012
8071
|
};
|
|
8013
|
-
const forceCloudflareWorkerReload = (rscEnv) => {
|
|
8072
|
+
const forceCloudflareWorkerReload = (rscEnv, expectedEpoch) => {
|
|
8014
8073
|
if (!rscEnv?.hot) return;
|
|
8015
|
-
|
|
8016
|
-
|
|
8074
|
+
const graph = rscEnv.moduleGraph;
|
|
8075
|
+
const reloadWorkerd = () => {
|
|
8017
8076
|
if (graph?.invalidateAll) {
|
|
8018
8077
|
graph.invalidateAll();
|
|
8019
8078
|
debugDiscovery?.("hmr: invalidated workerd rsc module graph");
|
|
@@ -8022,12 +8081,40 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
|
8022
8081
|
debugDiscovery?.(
|
|
8023
8082
|
"hmr: forced workerd rsc env reload (full-reload)"
|
|
8024
8083
|
);
|
|
8025
|
-
}
|
|
8084
|
+
};
|
|
8085
|
+
reloadWorkerd();
|
|
8086
|
+
if (expectedEpoch === void 0) return;
|
|
8087
|
+
void (async () => {
|
|
8088
|
+
const deadline = Date.now() + 15e3;
|
|
8089
|
+
do {
|
|
8090
|
+
if (devServerClosed || expectedEpoch !== s.devDiscoveryEpoch) {
|
|
8091
|
+
return;
|
|
8092
|
+
}
|
|
8093
|
+
await new Promise((resolve13) => setTimeout(resolve13, 25));
|
|
8094
|
+
if (devServerClosed || expectedEpoch !== s.devDiscoveryEpoch) {
|
|
8095
|
+
return;
|
|
8096
|
+
}
|
|
8097
|
+
try {
|
|
8098
|
+
const response = await fetch(getDevServerOrigin() + "/", {
|
|
8099
|
+
cache: "no-store",
|
|
8100
|
+
headers: {
|
|
8101
|
+
[DEV_DISCOVERY_PROBE_HEADER]: String(expectedEpoch)
|
|
8102
|
+
},
|
|
8103
|
+
signal: AbortSignal.timeout(1e3)
|
|
8104
|
+
});
|
|
8105
|
+
if (!devServerClosed && expectedEpoch === s.devDiscoveryEpoch && response.headers.get(DEV_DISCOVERY_EPOCH_HEADER) === String(expectedEpoch)) {
|
|
8106
|
+
publishDevDiscoveryReady(expectedEpoch);
|
|
8107
|
+
return;
|
|
8108
|
+
}
|
|
8109
|
+
reloadWorkerd();
|
|
8110
|
+
} catch {
|
|
8111
|
+
}
|
|
8112
|
+
} while (Date.now() < deadline);
|
|
8026
8113
|
debugDiscovery?.(
|
|
8027
|
-
"hmr: workerd
|
|
8028
|
-
|
|
8114
|
+
"hmr: workerd readiness probe timed out at epoch %d",
|
|
8115
|
+
expectedEpoch
|
|
8029
8116
|
);
|
|
8030
|
-
}
|
|
8117
|
+
})();
|
|
8031
8118
|
};
|
|
8032
8119
|
const scheduleRouteRegeneration = () => {
|
|
8033
8120
|
clearTimeout(routeChangeTimer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -100,6 +100,10 @@
|
|
|
100
100
|
"react-server": "./src/rsc/handler.ts",
|
|
101
101
|
"default": "./src/rsc/handler.ts"
|
|
102
102
|
},
|
|
103
|
+
"./internal/browser/dev-discovery": {
|
|
104
|
+
"types": "./dist/types/browser/dev-discovery.d.ts",
|
|
105
|
+
"default": "./src/browser/dev-discovery.ts"
|
|
106
|
+
},
|
|
103
107
|
"./cache": {
|
|
104
108
|
"types": "./dist/types/cache/index.d.ts",
|
|
105
109
|
"react-server": "./src/cache/index.ts",
|
|
@@ -179,7 +183,8 @@
|
|
|
179
183
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
180
184
|
"test": "playwright test",
|
|
181
185
|
"test:ui": "playwright test --ui",
|
|
182
|
-
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-
|
|
186
|
+
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-basename --project=hmr-prerender --no-deps --workers=1 && RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
187
|
+
"test:hmr-routes-local": "RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
183
188
|
"test:unit": "pnpm run build:types && vitest run",
|
|
184
189
|
"test:unit:watch": "vitest",
|
|
185
190
|
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -171,7 +171,7 @@ export async function POST() {
|
|
|
171
171
|
| `updateTag(...tags)` | awaitable (`Promise<void>`) | server actions | immediate; next read is fresh |
|
|
172
172
|
| `revalidateTag(...tags)` | background (`void`) | route handlers / webhooks | background (non-blocking); next read re-renders fresh |
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
All three built-in stores support tags. For `CFCacheStore`, distributed (cross-colo)
|
|
175
175
|
invalidation requires a `kv` namespace — the tag-invalidation markers live in
|
|
176
176
|
that same namespace; there is **no** separate tag-invalidation store to wire.
|
|
177
177
|
If no tag-capable store is configured, `updateTag`/`revalidateTag` warn and no-op.
|
package/skills/ppr/SKILL.md
CHANGED
|
@@ -170,6 +170,48 @@ On a document GET to a ppr route the router runs:
|
|
|
170
170
|
point is after the chain, an unauthorized request NEVER sees shell bytes — put
|
|
171
171
|
auth middleware anywhere (global or route DSL) and it guards PPR for free.
|
|
172
172
|
|
|
173
|
+
### Soft navigation reuses the captured segment shell
|
|
174
|
+
|
|
175
|
+
A usable shell snapshot also accelerates ordinary partial RSC navigations to
|
|
176
|
+
the same URL. The capture records the canonical document segment tree alongside
|
|
177
|
+
the HTML prelude. On a partial request the server replays only that segment
|
|
178
|
+
record through the normal `matchPartial()` pipeline, which then:
|
|
179
|
+
|
|
180
|
+
- preserves client-owned shared layouts by segment id;
|
|
181
|
+
- returns only new or revalidating destination segments;
|
|
182
|
+
- runs DSL loaders fresh with their normal `loading()` streaming behavior;
|
|
183
|
+
- keeps the existing prefetch key, source scope, and in-flight lock unchanged.
|
|
184
|
+
|
|
185
|
+
This is deliberately invisible to the browser: the response is the same
|
|
186
|
+
`RscPayload` shape as any other partial navigation. Captured item/response values
|
|
187
|
+
and loader-container pins are NOT replayed on this path, so loader reads stay
|
|
188
|
+
live. A route's own `cache()` scope still resolves its normal store, key, TTL,
|
|
189
|
+
SWR, tags, and condition; only the implicit document scope sees the replay
|
|
190
|
+
overlay, and fresh segment writes there stay request-local rather than polluting
|
|
191
|
+
the canonical `doc:` namespace. Intercepts, handler-live holes,
|
|
192
|
+
`transition({ when })`, an active nonce, and an absent/corrupt segment snapshot
|
|
193
|
+
fall open to the ordinary partial path when encountered by the fresh shell
|
|
194
|
+
capture. A transition already replayed from an explicit cache tier remains
|
|
195
|
+
frozen by that tier's normal semantics.
|
|
196
|
+
|
|
197
|
+
Only fresh shells replay. Production may use either a runtime entry or the
|
|
198
|
+
local build manifest; development uses runtime entries only, because probing
|
|
199
|
+
`/__rsc_shell` would block the foreground navigation on an on-demand capture.
|
|
200
|
+
A passive stale read does not claim SWR ownership because partial requests
|
|
201
|
+
cannot recapture the HTML shell. Custom `SegmentCacheStore` implementations
|
|
202
|
+
must set `supportsPassiveShellReads: true` and honor
|
|
203
|
+
`getShell(key, { claimRevalidation: false })` to opt into navigation replay.
|
|
204
|
+
There is still no Flight resume API; this is segment replay followed by normal
|
|
205
|
+
Flight streaming, not reuse of the HTML `prelude`/`postponed` bytes.
|
|
206
|
+
|
|
207
|
+
### Capture-generation invalidation
|
|
208
|
+
|
|
209
|
+
If handler or bake-lane code calls `updateTag()` on one of the shell's own tags
|
|
210
|
+
while capture is running, that generation is rejected. Built-in stores report it;
|
|
211
|
+
Rango warns with the shell key and backs capture off instead of rendering the
|
|
212
|
+
same doomed generation on every request. Move a deterministic self-invalidation
|
|
213
|
+
out of render code if you want the shell to persist.
|
|
214
|
+
|
|
173
215
|
### Opting out per request with `ctx.dynamic()`
|
|
174
216
|
|
|
175
217
|
Middleware and handlers can call `ctx.dynamic()` to force this request back to
|
|
@@ -405,7 +447,8 @@ Four hard edges (each e2e/unit-pinned):
|
|
|
405
447
|
Pitfalls: the session-object bake trap).
|
|
406
448
|
- **A rejecting bake-lane loader refuses.** Error UI never bakes.
|
|
407
449
|
- **Baked containers show CAPTURE-time data** for the shell's lifetime on
|
|
408
|
-
document GETs
|
|
450
|
+
document GETs. Soft navigations may replay the captured handler segments, but
|
|
451
|
+
DSL loaders and their item/response reads remain fresh. That IS the bake
|
|
409
452
|
lane's meaning; if a value must be fresh on every serve, it belongs on the
|
|
410
453
|
live lane (`loading()`) or in a nested promise.
|
|
411
454
|
|
|
@@ -539,9 +582,10 @@ bake-lane container — or key per variant at the CDN tier.
|
|
|
539
582
|
|
|
540
583
|
## What always stays on axis 1
|
|
541
584
|
|
|
542
|
-
Non-GET, RSC/
|
|
543
|
-
|
|
544
|
-
|
|
585
|
+
Non-GET, non-partial RSC/action/loader fetches, partial requests without an
|
|
586
|
+
eligible captured segment snapshot, per-request CSP nonce, `streamMode:
|
|
587
|
+
"allReady"`, redirects, 404s, error renders, routes without `ppr`, and any store
|
|
588
|
+
without the shell family. A stored shell is invalidated when
|
|
545
589
|
`React.version` changes (postponed state is build-coupled), so deploys
|
|
546
590
|
self-heal via recapture.
|
|
547
591
|
|