- Full-featured monitoring dashboard for local web apps - Real-time status tracking with uptime percentages - Visual sparklines for status history - Add/Edit/Delete apps dynamically - Categories and color coding - Auto-refresh every 30 seconds - API endpoints for apps and status management
352 lines
17 KiB
JavaScript
352 lines
17 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
Object.defineProperty(exports, "collectSegmentData", {
|
|
enumerable: true,
|
|
get: function() {
|
|
return collectSegmentData;
|
|
}
|
|
});
|
|
const _jsxruntime = require("react/jsx-runtime");
|
|
const _clientedge = require("react-server-dom-webpack/client.edge");
|
|
const _staticedge = require("react-server-dom-webpack/static.edge");
|
|
const _nodewebstreamshelper = require("../stream-utils/node-web-streams-helper");
|
|
const _constants = require("../../api/constants");
|
|
const _scheduler = require("../../lib/scheduler");
|
|
async function collectSegmentData(fullPageDataBuffer, staleTime, clientModules, serverConsumerManifest) {
|
|
// Traverse the router tree and generate a prefetch response for each segment.
|
|
// A mutable map to collect the results as we traverse the route tree.
|
|
const resultMap = new Map();
|
|
// Before we start, warm up the module cache by decoding the page data once.
|
|
// Then we can assume that any remaining async tasks that occur the next time
|
|
// are due to hanging promises caused by dynamic data access. Note we only
|
|
// have to do this once per page, not per individual segment.
|
|
//
|
|
try {
|
|
await (0, _clientedge.createFromReadableStream)((0, _nodewebstreamshelper.streamFromBuffer)(fullPageDataBuffer), {
|
|
serverConsumerManifest
|
|
});
|
|
await (0, _scheduler.waitAtLeastOneReactRenderTask)();
|
|
} catch {}
|
|
// Create an abort controller that we'll use to stop the stream.
|
|
const abortController = new AbortController();
|
|
const onCompletedProcessingRouteTree = async ()=>{
|
|
// Since all we're doing is decoding and re-encoding a cached prerender, if
|
|
// serializing the stream takes longer than a microtask, it must because of
|
|
// hanging promises caused by dynamic data.
|
|
await (0, _scheduler.waitAtLeastOneReactRenderTask)();
|
|
abortController.abort();
|
|
};
|
|
// Generate a stream for the route tree prefetch. While we're walking the
|
|
// tree, we'll also spawn additional tasks to generate the segment prefetches.
|
|
// The promises for these tasks are pushed to a mutable array that we will
|
|
// await once the route tree is fully rendered.
|
|
const segmentTasks = [];
|
|
const { prelude: treeStream } = await (0, _staticedge.prerender)(// RootTreePrefetch is not a valid return type for a React component, but
|
|
// we need to use a component so that when we decode the original stream
|
|
// inside of it, the side effects are transferred to the new stream.
|
|
// @ts-expect-error
|
|
/*#__PURE__*/ (0, _jsxruntime.jsx)(PrefetchTreeData, {
|
|
fullPageDataBuffer: fullPageDataBuffer,
|
|
serverConsumerManifest: serverConsumerManifest,
|
|
clientModules: clientModules,
|
|
staleTime: staleTime,
|
|
segmentTasks: segmentTasks,
|
|
onCompletedProcessingRouteTree: onCompletedProcessingRouteTree
|
|
}), clientModules, {
|
|
signal: abortController.signal,
|
|
onError () {
|
|
// Ignore any errors. These would have already been reported when
|
|
// we created the full page data.
|
|
}
|
|
});
|
|
// Write the route tree to a special `/_tree` segment.
|
|
const treeBuffer = await (0, _nodewebstreamshelper.streamToBuffer)(treeStream);
|
|
resultMap.set('/_tree', treeBuffer);
|
|
// Now that we've finished rendering the route tree, all the segment tasks
|
|
// should have been spawned. Await them in parallel and write the segment
|
|
// prefetches to the result map.
|
|
for (const [segmentPath, buffer] of (await Promise.all(segmentTasks))){
|
|
resultMap.set(segmentPath, buffer);
|
|
}
|
|
return resultMap;
|
|
}
|
|
async function PrefetchTreeData({ fullPageDataBuffer, serverConsumerManifest, clientModules, staleTime, segmentTasks, onCompletedProcessingRouteTree }) {
|
|
// We're currently rendering a Flight response for the route tree prefetch.
|
|
// Inside this component, decode the Flight stream for the whole page. This is
|
|
// a hack to transfer the side effects from the original Flight stream (e.g.
|
|
// Float preloads) onto the Flight stream for the tree prefetch.
|
|
// TODO: React needs a better way to do this. Needed for Server Actions, too.
|
|
const initialRSCPayload = await (0, _clientedge.createFromReadableStream)(createUnclosingPrefetchStream((0, _nodewebstreamshelper.streamFromBuffer)(fullPageDataBuffer)), {
|
|
serverConsumerManifest
|
|
});
|
|
const buildId = initialRSCPayload.b;
|
|
// FlightDataPath is an unsound type, hence the additional checks.
|
|
const flightDataPaths = initialRSCPayload.f;
|
|
if (flightDataPaths.length !== 1 && flightDataPaths[0].length !== 3) {
|
|
console.error('Internal Next.js error: InitialRSCPayload does not match the expected ' + 'shape for a prerendered page during segment prefetch generation.');
|
|
return null;
|
|
}
|
|
const flightRouterState = flightDataPaths[0][0];
|
|
const seedData = flightDataPaths[0][1];
|
|
const head = flightDataPaths[0][2];
|
|
// Compute the route metadata tree by traversing the FlightRouterState. As we
|
|
// walk the tree, we will also spawn a task to produce a prefetch response for
|
|
// each segment.
|
|
const tree = await collectSegmentDataImpl(flightRouterState, buildId, seedData, fullPageDataBuffer, clientModules, serverConsumerManifest, '', '', segmentTasks);
|
|
const isHeadPartial = await isPartialRSCData(head, clientModules);
|
|
// Notify the abort controller that we're done processing the route tree.
|
|
// Anything async that happens after this point must be due to hanging
|
|
// promises in the original stream.
|
|
onCompletedProcessingRouteTree();
|
|
// Render the route tree to a special `/_tree` segment.
|
|
const treePrefetch = {
|
|
buildId,
|
|
tree,
|
|
head,
|
|
isHeadPartial,
|
|
staleTime
|
|
};
|
|
return treePrefetch;
|
|
}
|
|
async function collectSegmentDataImpl(route, buildId, seedData, fullPageDataBuffer, clientModules, serverConsumerManifest, segmentPathStr, accessToken, segmentTasks) {
|
|
// Metadata about the segment. Sent as part of the tree prefetch. Null if
|
|
// there are no children.
|
|
let slotMetadata = null;
|
|
const children = route[1];
|
|
const seedDataChildren = seedData !== null ? seedData[2] : null;
|
|
for(const parallelRouteKey in children){
|
|
const childRoute = children[parallelRouteKey];
|
|
const childSegment = childRoute[0];
|
|
const childSeedData = seedDataChildren !== null ? seedDataChildren[parallelRouteKey] : null;
|
|
const childSegmentPathStr = segmentPathStr + '/' + encodeChildSegmentAsFilesystemSafePathname(parallelRouteKey, childSegment);
|
|
// Create an access token for each child slot.
|
|
const childAccessToken = await createSegmentAccessToken(segmentPathStr, parallelRouteKey);
|
|
const childTree = await collectSegmentDataImpl(childRoute, buildId, childSeedData, fullPageDataBuffer, clientModules, serverConsumerManifest, childSegmentPathStr, childAccessToken, segmentTasks);
|
|
if (slotMetadata === null) {
|
|
slotMetadata = {};
|
|
}
|
|
slotMetadata[parallelRouteKey] = childTree;
|
|
}
|
|
if (seedData !== null) {
|
|
// Spawn a task to write the segment data to a new Flight stream.
|
|
segmentTasks.push(// Since we're already in the middle of a render, wait until after the
|
|
// current task to escape the current rendering context.
|
|
(0, _scheduler.waitAtLeastOneReactRenderTask)().then(()=>renderSegmentPrefetch(buildId, seedData, segmentPathStr, accessToken, clientModules)));
|
|
} else {
|
|
// This segment does not have any seed data. Skip generating a prefetch
|
|
// response for it. We'll still include it in the route tree, though.
|
|
// TODO: We should encode in the route tree whether a segment is missing
|
|
// so we don't attempt to fetch it for no reason. As of now this shouldn't
|
|
// ever happen in practice, though.
|
|
}
|
|
// Metadata about the segment. Sent to the client as part of the
|
|
// tree prefetch.
|
|
const segment = route[0];
|
|
const isRootLayout = route[4];
|
|
return {
|
|
path: segmentPathStr === '' ? '/' : segmentPathStr,
|
|
token: accessToken,
|
|
slots: slotMetadata,
|
|
extra: [
|
|
segment,
|
|
isRootLayout === true
|
|
]
|
|
};
|
|
}
|
|
async function renderSegmentPrefetch(buildId, seedData, segmentPathStr, accessToken, clientModules) {
|
|
// Render the segment data to a stream.
|
|
// In the future, this is where we can include additional metadata, like the
|
|
// stale time and cache tags.
|
|
const rsc = seedData[1];
|
|
const loading = seedData[3];
|
|
const segmentPrefetch = {
|
|
buildId,
|
|
rsc,
|
|
loading,
|
|
isPartial: await isPartialRSCData(rsc, clientModules)
|
|
};
|
|
// Since all we're doing is decoding and re-encoding a cached prerender, if
|
|
// it takes longer than a microtask, it must because of hanging promises
|
|
// caused by dynamic data. Abort the stream at the end of the current task.
|
|
const abortController = new AbortController();
|
|
(0, _scheduler.waitAtLeastOneReactRenderTask)().then(()=>abortController.abort());
|
|
const { prelude: segmentStream } = await (0, _staticedge.prerender)(segmentPrefetch, clientModules, {
|
|
signal: abortController.signal,
|
|
onError () {
|
|
// Ignore any errors. These would have already been reported when
|
|
// we created the full page data.
|
|
}
|
|
});
|
|
const segmentBuffer = await (0, _nodewebstreamshelper.streamToBuffer)(segmentStream);
|
|
// Add the buffer to the result map.
|
|
if (segmentPathStr === '') {
|
|
return [
|
|
'/',
|
|
segmentBuffer
|
|
];
|
|
} else {
|
|
// The access token is appended to the end of the segment name. To request
|
|
// a segment, the client sends a header like:
|
|
//
|
|
// Next-Router-Segment-Prefetch: /path/to/segment.accesstoken
|
|
//
|
|
// The segment path is provided by the tree prefetch, and the access
|
|
// token is provided in the parent layout's data.
|
|
const fullPath = `${segmentPathStr}.${accessToken}`;
|
|
return [
|
|
fullPath,
|
|
segmentBuffer
|
|
];
|
|
}
|
|
}
|
|
async function isPartialRSCData(rsc, clientModules) {
|
|
// We can determine if a segment contains only partial data if it takes longer
|
|
// than a task to encode, because dynamic data is encoded as an infinite
|
|
// promise. We must do this in a separate Flight prerender from the one that
|
|
// actually generates the prefetch stream because we need to include
|
|
// `isPartial` in the stream itself.
|
|
let isPartial = false;
|
|
const abortController = new AbortController();
|
|
(0, _scheduler.waitAtLeastOneReactRenderTask)().then(()=>{
|
|
// If we haven't yet finished the outer task, then it must be because we
|
|
// accessed dynamic data.
|
|
isPartial = true;
|
|
abortController.abort();
|
|
});
|
|
await (0, _staticedge.prerender)(rsc, clientModules, {
|
|
signal: abortController.signal,
|
|
onError () {}
|
|
});
|
|
return isPartial;
|
|
}
|
|
// TODO: Consider updating or unifying this encoding logic for segments with
|
|
// createRouterCacheKey on the client, perhaps by including it as part of
|
|
// the FlightRouterState. Theoretically the client should never have to do its
|
|
// own encoding of segment keys; it can pass back whatever the server gave it.
|
|
function encodeChildSegmentAsFilesystemSafePathname(parallelRouteKey, segment) {
|
|
// Encode a child segment and its corresponding parallel route key to a
|
|
// filesystem-safe pathname. The format is internal-only and can be somewhat
|
|
// arbitrary as long as there are no collisions, because these will be used
|
|
// as filenames during build and in the incremental cache. They will also
|
|
// be sent by the client to request the corresponding segment, but they
|
|
// do not need to be decodable. The server will merely look for a matching
|
|
// file in the cache.
|
|
//
|
|
// For ease of debugging, the format looks roughly similar to the App Router
|
|
// convention for defining routes in the source, but again the exact format is
|
|
// not important as long as it's consistent between the client and server and
|
|
// meets the above requirements.
|
|
//
|
|
// TODO: If the segment did not read from params, then we can omit the
|
|
// params from the cache key. Need to track this during the prerender somehow.
|
|
let safeSegmentValue;
|
|
if (typeof segment === 'string') {
|
|
safeSegmentValue = encodeParamValue(segment);
|
|
} else {
|
|
// Parameterized segments.
|
|
const [paramName, paramValue, paramType] = segment;
|
|
let paramPrefix;
|
|
switch(paramType){
|
|
case 'c':
|
|
case 'ci':
|
|
paramPrefix = `[...${paramName}]`;
|
|
break;
|
|
case 'oc':
|
|
paramPrefix = `[[...${paramName}]]`;
|
|
break;
|
|
case 'd':
|
|
case 'di':
|
|
paramPrefix = `[${paramName}]`;
|
|
break;
|
|
default:
|
|
throw new Error('Unknown dynamic param type');
|
|
}
|
|
safeSegmentValue = `${paramPrefix}-${encodeParamValue(paramValue)}`;
|
|
}
|
|
let result;
|
|
if (parallelRouteKey === 'children') {
|
|
// Omit the parallel route key for children, since this is the most
|
|
// common case. Saves some bytes.
|
|
result = `${safeSegmentValue}`;
|
|
} else {
|
|
result = `@${parallelRouteKey}/${safeSegmentValue}`;
|
|
}
|
|
return result;
|
|
}
|
|
// Define a regex pattern to match the most common characters found in a route
|
|
// param. It excludes anything that might not be cross-platform filesystem
|
|
// compatible, like |. It does not need to be precise because the fallback is to
|
|
// just base64url-encode the whole parameter, which is fine; we just don't do it
|
|
// by default for compactness, and for easier debugging.
|
|
const simpleParamValueRegex = /^[a-zA-Z0-9\-_@]+$/;
|
|
function encodeParamValue(segment) {
|
|
if (segment === _constants.UNDERSCORE_NOT_FOUND_ROUTE) {
|
|
// TODO: FlightRouterState encodes Not Found routes as "/_not-found". But
|
|
// params typically don't include the leading slash. We should use a
|
|
// different encoding to avoid this special case.
|
|
return '_not-found';
|
|
}
|
|
if (simpleParamValueRegex.test(segment)) {
|
|
return segment;
|
|
}
|
|
// If there are any unsafe characters, base64url-encode the entire segment.
|
|
// We also add a $ prefix so it doesn't collide with the simple case.
|
|
return '$' + Buffer.from(segment, 'utf-8').toString('base64url');
|
|
}
|
|
async function createSegmentAccessToken(parentSegmentPathStr, parallelRouteKey) {
|
|
// Create an access token that the client passes when requesting a segment.
|
|
// The token is sent to the client as part of the parent layout's data.
|
|
//
|
|
// The token is hash of the parent segment path and the parallel route key. A
|
|
// subtle detail here is that it does *not* include the value of the segment
|
|
// itself — the token grants access to the parallel route slot, not the
|
|
// particular segment that is rendered there.
|
|
//
|
|
// TODO: Because this only affects prefetches, this doesn't need to be secure.
|
|
// It's just for obfuscation. But eventually we will use this technique when
|
|
// performing dynamic navigations, to support auth checks in a layout that
|
|
// conditionally renders its slots. At that point we'll need to add a salt.
|
|
// Encode the inputs as Uint8Array
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(parentSegmentPathStr + parallelRouteKey);
|
|
// Use the Web Crypto API to generate a SHA-256 hash.
|
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
// Convert the ArrayBuffer to a hex string
|
|
const hashArray = new Uint8Array(hashBuffer);
|
|
const hashHex = Array.from(hashArray).map((byte)=>byte.toString(16).padStart(2, '0')).join('');
|
|
return hashHex;
|
|
}
|
|
function createUnclosingPrefetchStream(originalFlightStream) {
|
|
// When PPR is enabled, prefetch streams may contain references that never
|
|
// resolve, because that's how we encode dynamic data access. In the decoded
|
|
// object returned by the Flight client, these are reified into hanging
|
|
// promises that suspend during render, which is effectively what we want.
|
|
// The UI resolves when it switches to the dynamic data stream
|
|
// (via useDeferredValue(dynamic, static)).
|
|
//
|
|
// However, the Flight implementation currently errors if the server closes
|
|
// the response before all the references are resolved. As a cheat to work
|
|
// around this, we wrap the original stream in a new stream that never closes,
|
|
// and therefore doesn't error.
|
|
const reader = originalFlightStream.getReader();
|
|
return new ReadableStream({
|
|
async pull (controller) {
|
|
while(true){
|
|
const { done, value } = await reader.read();
|
|
if (!done) {
|
|
// Pass to the target stream and keep consuming the Flight response
|
|
// from the server.
|
|
controller.enqueue(value);
|
|
continue;
|
|
}
|
|
// The server stream has closed. Exit, but intentionally do not close
|
|
// the target stream.
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
//# sourceMappingURL=collect-segment-data.js.map
|