- 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
496 lines
21 KiB
JavaScript
496 lines
21 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
0 && (module.exports = {
|
|
EntryStatus: null,
|
|
readExactRouteCacheEntry: null,
|
|
readRouteCacheEntry: null,
|
|
readSegmentCacheEntry: null,
|
|
requestRouteCacheEntryFromCache: null,
|
|
requestSegmentEntryFromCache: null,
|
|
waitForSegmentCacheEntry: null
|
|
});
|
|
function _export(target, all) {
|
|
for(var name in all)Object.defineProperty(target, name, {
|
|
enumerable: true,
|
|
get: all[name]
|
|
});
|
|
}
|
|
_export(exports, {
|
|
EntryStatus: function() {
|
|
return EntryStatus;
|
|
},
|
|
readExactRouteCacheEntry: function() {
|
|
return readExactRouteCacheEntry;
|
|
},
|
|
readRouteCacheEntry: function() {
|
|
return readRouteCacheEntry;
|
|
},
|
|
readSegmentCacheEntry: function() {
|
|
return readSegmentCacheEntry;
|
|
},
|
|
requestRouteCacheEntryFromCache: function() {
|
|
return requestRouteCacheEntryFromCache;
|
|
},
|
|
requestSegmentEntryFromCache: function() {
|
|
return requestSegmentEntryFromCache;
|
|
},
|
|
waitForSegmentCacheEntry: function() {
|
|
return waitForSegmentCacheEntry;
|
|
}
|
|
});
|
|
const _approuterheaders = require("../app-router-headers");
|
|
const _fetchserverresponse = require("../router-reducer/fetch-server-response");
|
|
const _scheduler = require("./scheduler");
|
|
const _appbuildid = require("../../app-build-id");
|
|
const _createhreffromurl = require("../router-reducer/create-href-from-url");
|
|
const _tuplemap = require("./tuple-map");
|
|
const _lru = require("./lru");
|
|
var EntryStatus = /*#__PURE__*/ function(EntryStatus) {
|
|
EntryStatus[EntryStatus["Pending"] = 0] = "Pending";
|
|
EntryStatus[EntryStatus["Rejected"] = 1] = "Rejected";
|
|
EntryStatus[EntryStatus["Fulfilled"] = 2] = "Fulfilled";
|
|
return EntryStatus;
|
|
}({});
|
|
const routeCacheMap = (0, _tuplemap.createTupleMap)();
|
|
// We use an LRU for memory management. We must update this whenever we add or
|
|
// remove a new cache entry, or when an entry changes size.
|
|
// TODO: I chose the max size somewhat arbitrarily. Consider setting this based
|
|
// on navigator.deviceMemory, or some other heuristic. We should make this
|
|
// customizable via the Next.js config, too.
|
|
const maxRouteLruSize = 10 * 1024 * 1024 // 10 MB
|
|
;
|
|
const routeCacheLru = (0, _lru.createLRU)(maxRouteLruSize, onRouteLRUEviction);
|
|
// TODO: We may eventually store segment entries in a tuple map, too, to
|
|
// account for search params.
|
|
const segmentCacheMap = new Map();
|
|
// NOTE: Segments and Route entries are managed by separate LRUs. We could
|
|
// combine them into a single LRU, but because they are separate types, we'd
|
|
// need to wrap each one in an extra LRU node (to maintain monomorphism, at the
|
|
// cost of additional memory).
|
|
const maxSegmentLruSize = 50 * 1024 * 1024 // 50 MB
|
|
;
|
|
const segmentCacheLru = (0, _lru.createLRU)(maxSegmentLruSize, onSegmentLRUEviction);
|
|
function readExactRouteCacheEntry(now, href, nextUrl) {
|
|
const keypath = nextUrl === null ? [
|
|
href
|
|
] : [
|
|
href,
|
|
nextUrl
|
|
];
|
|
const existingEntry = routeCacheMap.get(keypath);
|
|
if (existingEntry !== null) {
|
|
// Check if the entry is stale
|
|
if (existingEntry.staleAt > now) {
|
|
// Reuse the existing entry.
|
|
// Since this is an access, move the entry to the front of the LRU.
|
|
routeCacheLru.put(existingEntry);
|
|
return existingEntry;
|
|
} else {
|
|
// Evict the stale entry from the cache.
|
|
deleteRouteFromCache(existingEntry, keypath);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function readRouteCacheEntry(now, key) {
|
|
// First check if there's a non-intercepted entry. Most routes cannot be
|
|
// intercepted, so this is the common case.
|
|
const nonInterceptedEntry = readExactRouteCacheEntry(now, key.href, null);
|
|
if (nonInterceptedEntry !== null && !nonInterceptedEntry.couldBeIntercepted) {
|
|
// Found a match, and the route cannot be intercepted. We can reuse it.
|
|
return nonInterceptedEntry;
|
|
}
|
|
// There was no match. Check again but include the Next-Url this time.
|
|
return readExactRouteCacheEntry(now, key.href, key.nextUrl);
|
|
}
|
|
function readSegmentCacheEntry(now, path) {
|
|
const existingEntry = segmentCacheMap.get(path);
|
|
if (existingEntry !== undefined) {
|
|
// Check if the entry is stale
|
|
if (existingEntry.staleAt > now) {
|
|
// Reuse the existing entry.
|
|
// Since this is an access, move the entry to the front of the LRU.
|
|
segmentCacheLru.put(existingEntry);
|
|
return existingEntry;
|
|
} else {
|
|
// Evict the stale entry from the cache.
|
|
deleteSegmentFromCache(existingEntry, path);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function waitForSegmentCacheEntry(pendingEntry) {
|
|
// Because the entry is pending, there's already a in-progress request.
|
|
// Attach a promise to the entry that will resolve when the server responds.
|
|
let promiseWithResolvers = pendingEntry.promise;
|
|
if (promiseWithResolvers === null) {
|
|
promiseWithResolvers = pendingEntry.promise = createPromiseWithResolvers();
|
|
} else {
|
|
// There's already a promise we can use
|
|
}
|
|
return promiseWithResolvers.promise;
|
|
}
|
|
function requestRouteCacheEntryFromCache(now, task) {
|
|
const key = task.key;
|
|
// First check if there's a non-intercepted entry. Most routes cannot be
|
|
// intercepted, so this is the common case.
|
|
const nonInterceptedEntry = readExactRouteCacheEntry(now, key.href, null);
|
|
if (nonInterceptedEntry !== null && !nonInterceptedEntry.couldBeIntercepted) {
|
|
// Found a match, and the route cannot be intercepted. We can reuse it.
|
|
return nonInterceptedEntry;
|
|
}
|
|
// There was no match. Check again but include the Next-Url this time.
|
|
const exactEntry = readExactRouteCacheEntry(now, key.href, key.nextUrl);
|
|
if (exactEntry !== null) {
|
|
return exactEntry;
|
|
}
|
|
// Create a pending entry and spawn a request for its data.
|
|
const pendingEntry = {
|
|
canonicalUrl: null,
|
|
status: 0,
|
|
blockedTasks: null,
|
|
tree: null,
|
|
head: null,
|
|
isHeadPartial: true,
|
|
// If the request takes longer than a minute, a subsequent request should
|
|
// retry instead of waiting for this one.
|
|
//
|
|
// When the response is received, this value will be replaced by a new value
|
|
// based on the stale time sent from the server.
|
|
staleAt: now + 60 * 1000,
|
|
// This is initialized to true because we don't know yet whether the route
|
|
// could be intercepted. It's only set to false once we receive a response
|
|
// from the server.
|
|
couldBeIntercepted: true,
|
|
// LRU-related fields
|
|
keypath: null,
|
|
next: null,
|
|
prev: null,
|
|
size: 0
|
|
};
|
|
(0, _scheduler.spawnPrefetchSubtask)(fetchRouteOnCacheMiss(pendingEntry, task));
|
|
const keypath = key.nextUrl === null ? [
|
|
key.href
|
|
] : [
|
|
key.href,
|
|
key.nextUrl
|
|
];
|
|
routeCacheMap.set(keypath, pendingEntry);
|
|
// Stash the keypath on the entry so we know how to remove it from the map
|
|
// if it gets evicted from the LRU.
|
|
pendingEntry.keypath = keypath;
|
|
routeCacheLru.put(pendingEntry);
|
|
return pendingEntry;
|
|
}
|
|
function requestSegmentEntryFromCache(now, task, route, path, accessToken) {
|
|
const existingEntry = readSegmentCacheEntry(now, path);
|
|
if (existingEntry !== null) {
|
|
return existingEntry;
|
|
}
|
|
// Create a pending entry and spawn a request for its data.
|
|
const pendingEntry = {
|
|
status: 0,
|
|
rsc: null,
|
|
loading: null,
|
|
staleAt: route.staleAt,
|
|
isPartial: true,
|
|
promise: null,
|
|
// LRU-related fields
|
|
key: null,
|
|
next: null,
|
|
prev: null,
|
|
size: 0
|
|
};
|
|
(0, _scheduler.spawnPrefetchSubtask)(fetchSegmentEntryOnCacheMiss(route, pendingEntry, task.key, path, accessToken));
|
|
segmentCacheMap.set(path, pendingEntry);
|
|
// Stash the keypath on the entry so we know how to remove it from the map
|
|
// if it gets evicted from the LRU.
|
|
pendingEntry.key = path;
|
|
segmentCacheLru.put(pendingEntry);
|
|
return pendingEntry;
|
|
}
|
|
function deleteRouteFromCache(entry, keypath) {
|
|
pingBlockedTasks(entry);
|
|
routeCacheMap.delete(keypath);
|
|
routeCacheLru.delete(entry);
|
|
}
|
|
function deleteSegmentFromCache(entry, key) {
|
|
cancelEntryListeners(entry);
|
|
segmentCacheMap.delete(key);
|
|
segmentCacheLru.delete(entry);
|
|
}
|
|
function onRouteLRUEviction(entry) {
|
|
// The LRU evicted this entry. Remove it from the map.
|
|
const keypath = entry.keypath;
|
|
if (keypath !== null) {
|
|
entry.keypath = null;
|
|
pingBlockedTasks(entry);
|
|
routeCacheMap.delete(keypath);
|
|
}
|
|
}
|
|
function onSegmentLRUEviction(entry) {
|
|
// The LRU evicted this entry. Remove it from the map.
|
|
const key = entry.key;
|
|
if (key !== null) {
|
|
entry.key = null;
|
|
cancelEntryListeners(entry);
|
|
segmentCacheMap.delete(key);
|
|
}
|
|
}
|
|
function cancelEntryListeners(entry) {
|
|
if (entry.status === 0 && entry.promise !== null) {
|
|
// There were listeners for this entry. Resolve them with `null` to indicate
|
|
// that the prefetch failed. It's up to the listener to decide how to handle
|
|
// this case.
|
|
// NOTE: We don't currently propagate the reason the prefetch was canceled
|
|
// but we could by accepting a `reason` argument.
|
|
entry.promise.resolve(null);
|
|
entry.promise = null;
|
|
}
|
|
}
|
|
function pingBlockedTasks(entry) {
|
|
const blockedTasks = entry.blockedTasks;
|
|
if (blockedTasks !== null) {
|
|
for (const task of blockedTasks){
|
|
(0, _scheduler.pingPrefetchTask)(task);
|
|
}
|
|
entry.blockedTasks = null;
|
|
}
|
|
}
|
|
function fulfillRouteCacheEntry(entry, tree, head, isHeadPartial, staleAt, couldBeIntercepted, canonicalUrl) {
|
|
const fulfilledEntry = entry;
|
|
fulfilledEntry.status = 2;
|
|
fulfilledEntry.tree = tree;
|
|
fulfilledEntry.head = head;
|
|
fulfilledEntry.isHeadPartial = isHeadPartial;
|
|
fulfilledEntry.staleAt = staleAt;
|
|
fulfilledEntry.couldBeIntercepted = couldBeIntercepted;
|
|
fulfilledEntry.canonicalUrl = canonicalUrl;
|
|
pingBlockedTasks(entry);
|
|
return fulfilledEntry;
|
|
}
|
|
function fulfillSegmentCacheEntry(segmentCacheEntry, rsc, loading, staleAt, isPartial) {
|
|
const fulfilledEntry = segmentCacheEntry;
|
|
fulfilledEntry.status = 2;
|
|
fulfilledEntry.rsc = rsc;
|
|
fulfilledEntry.loading = loading;
|
|
fulfilledEntry.staleAt = staleAt;
|
|
fulfilledEntry.isPartial = isPartial;
|
|
// Resolve any listeners that were waiting for this data.
|
|
if (segmentCacheEntry.promise !== null) {
|
|
segmentCacheEntry.promise.resolve(fulfilledEntry);
|
|
// Free the promise for garbage collection.
|
|
fulfilledEntry.promise = null;
|
|
}
|
|
}
|
|
function rejectRouteCacheEntry(entry, staleAt) {
|
|
const rejectedEntry = entry;
|
|
rejectedEntry.status = 1;
|
|
rejectedEntry.staleAt = staleAt;
|
|
pingBlockedTasks(entry);
|
|
}
|
|
function rejectSegmentCacheEntry(entry, staleAt) {
|
|
const rejectedEntry = entry;
|
|
rejectedEntry.status = 1;
|
|
rejectedEntry.staleAt = staleAt;
|
|
if (entry.promise !== null) {
|
|
// NOTE: We don't currently propagate the reason the prefetch was canceled
|
|
// but we could by accepting a `reason` argument.
|
|
entry.promise.resolve(null);
|
|
entry.promise = null;
|
|
}
|
|
}
|
|
async function fetchRouteOnCacheMiss(entry, task) {
|
|
// This function is allowed to use async/await because it contains the actual
|
|
// fetch that gets issued on a cache miss. Notice though that it does not
|
|
// return anything; it writes the result to the cache entry directly, then
|
|
// pings the scheduler to unblock the corresponding prefetch task.
|
|
const key = task.key;
|
|
const href = key.href;
|
|
const nextUrl = key.nextUrl;
|
|
try {
|
|
const response = await fetchSegmentPrefetchResponse(href, '/_tree', nextUrl);
|
|
if (!response || !response.ok || // 204 is a Cache miss. Though theoretically this shouldn't happen when
|
|
// PPR is enabled, because we always respond to route tree requests, even
|
|
// if it needs to be blockingly generated on demand.
|
|
response.status === 204 || !response.body) {
|
|
// Server responded with an error, or with a miss. We should still cache
|
|
// the response, but we can try again after 10 seconds.
|
|
rejectRouteCacheEntry(entry, Date.now() + 10 * 1000);
|
|
return;
|
|
}
|
|
const prefetchStream = createPrefetchResponseStream(response.body, routeCacheLru, entry);
|
|
const serverData = await (0, _fetchserverresponse.createFromNextReadableStream)(prefetchStream);
|
|
if (serverData.buildId !== (0, _appbuildid.getAppBuildId)()) {
|
|
// The server build does not match the client. Treat as a 404. During
|
|
// an actual navigation, the router will trigger an MPA navigation.
|
|
// TODO: Consider moving the build ID to a response header so we can check
|
|
// it before decoding the response, and so there's one way of checking
|
|
// across all response types.
|
|
rejectRouteCacheEntry(entry, Date.now() + 10 * 1000);
|
|
return;
|
|
}
|
|
// This is a bit convoluted but it's taken from router-reducer and
|
|
// fetch-server-response
|
|
const canonicalUrl = response.redirected ? (0, _createhreffromurl.createHrefFromUrl)((0, _fetchserverresponse.urlToUrlWithoutFlightMarker)(response.url)) : href;
|
|
// Check whether the response varies based on the Next-Url header.
|
|
const varyHeader = response.headers.get('vary');
|
|
const couldBeIntercepted = varyHeader !== null && varyHeader.includes(_approuterheaders.NEXT_URL);
|
|
fulfillRouteCacheEntry(entry, serverData.tree, serverData.head, serverData.isHeadPartial, Date.now() + serverData.staleTime, couldBeIntercepted, canonicalUrl);
|
|
if (!couldBeIntercepted && nextUrl !== null) {
|
|
// This route will never be intercepted. So we can use this entry for all
|
|
// requests to this route, regardless of the Next-Url header. This works
|
|
// because when reading the cache we always check for a valid
|
|
// non-intercepted entry first.
|
|
//
|
|
// Re-key the entry. Since we're in an async task, we must first confirm
|
|
// that the entry hasn't been concurrently modified by a different task.
|
|
const currentKeypath = [
|
|
href,
|
|
nextUrl
|
|
];
|
|
const expectedEntry = routeCacheMap.get(currentKeypath);
|
|
if (expectedEntry === entry) {
|
|
routeCacheMap.delete(currentKeypath);
|
|
const newKeypath = [
|
|
href
|
|
];
|
|
routeCacheMap.set(newKeypath, entry);
|
|
// We don't need to update the LRU because the entry is already in it.
|
|
// But since we changed the keypath, we do need to update that, so we
|
|
// know how to remove it from the map if it gets evicted from the LRU.
|
|
entry.keypath = newKeypath;
|
|
} else {
|
|
// Something else modified this entry already. Since the re-keying is
|
|
// just a performance optimization, we can safely skip it.
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Either the connection itself failed, or something bad happened while
|
|
// decoding the response.
|
|
rejectRouteCacheEntry(entry, Date.now() + 10 * 1000);
|
|
}
|
|
}
|
|
async function fetchSegmentEntryOnCacheMiss(route, segmentCacheEntry, routeKey, segmentPath, accessToken) {
|
|
// This function is allowed to use async/await because it contains the actual
|
|
// fetch that gets issued on a cache miss. Notice though that it does not
|
|
// return anything; it writes the result to the cache entry directly.
|
|
//
|
|
// Segment fetches are non-blocking so we don't need to ping the scheduler
|
|
// on completion.
|
|
const href = routeKey.href;
|
|
try {
|
|
const response = await fetchSegmentPrefetchResponse(href, accessToken === '' ? segmentPath : segmentPath + "." + accessToken, routeKey.nextUrl);
|
|
if (!response || !response.ok || response.status === 204 || // Cache miss
|
|
!response.body) {
|
|
// Server responded with an error, or with a miss. We should still cache
|
|
// the response, but we can try again after 10 seconds.
|
|
rejectSegmentCacheEntry(segmentCacheEntry, Date.now() + 10 * 1000);
|
|
return;
|
|
}
|
|
// Wrap the original stream in a new stream that never closes. That way the
|
|
// Flight client doesn't error if there's a hanging promise.
|
|
const prefetchStream = createPrefetchResponseStream(response.body, segmentCacheLru, segmentCacheEntry);
|
|
const serverData = await (0, _fetchserverresponse.createFromNextReadableStream)(prefetchStream);
|
|
if (serverData.buildId !== (0, _appbuildid.getAppBuildId)()) {
|
|
// The server build does not match the client. Treat as a 404. During
|
|
// an actual navigation, the router will trigger an MPA navigation.
|
|
// TODO: Consider moving the build ID to a response header so we can check
|
|
// it before decoding the response, and so there's one way of checking
|
|
// across all response types.
|
|
rejectSegmentCacheEntry(segmentCacheEntry, Date.now() + 10 * 1000);
|
|
return;
|
|
}
|
|
fulfillSegmentCacheEntry(segmentCacheEntry, serverData.rsc, serverData.loading, // TODO: The server does not currently provide per-segment stale time.
|
|
// So we use the stale time of the route.
|
|
route.staleAt, serverData.isPartial);
|
|
} catch (error) {
|
|
// Either the connection itself failed, or something bad happened while
|
|
// decoding the response.
|
|
rejectSegmentCacheEntry(segmentCacheEntry, Date.now() + 10 * 1000);
|
|
}
|
|
}
|
|
async function fetchSegmentPrefetchResponse(href, segmentPath, nextUrl) {
|
|
const headers = {
|
|
[_approuterheaders.RSC_HEADER]: '1',
|
|
[_approuterheaders.NEXT_ROUTER_PREFETCH_HEADER]: '1',
|
|
[_approuterheaders.NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]: segmentPath
|
|
};
|
|
if (nextUrl !== null) {
|
|
headers[_approuterheaders.NEXT_URL] = nextUrl;
|
|
}
|
|
const fetchPriority = 'low';
|
|
const responsePromise = (0, _fetchserverresponse.createFetch)(new URL(href), headers, fetchPriority);
|
|
(0, _scheduler.trackPrefetchRequestBandwidth)(responsePromise);
|
|
const response = await responsePromise;
|
|
const contentType = response.headers.get('content-type');
|
|
const isFlightResponse = contentType && contentType.startsWith(_approuterheaders.RSC_CONTENT_TYPE_HEADER);
|
|
if (!response.ok || !isFlightResponse) {
|
|
return null;
|
|
}
|
|
return response;
|
|
}
|
|
function createPrefetchResponseStream(originalFlightStream, lru, lruEntry) {
|
|
// 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.
|
|
//
|
|
// While processing the original stream, we also incrementally update the size
|
|
// of the cache entry in the LRU.
|
|
let totalByteLength = 0;
|
|
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);
|
|
// Incrementally update the size of the cache entry in the LRU.
|
|
// NOTE: Since prefetch responses are delivered in a single chunk,
|
|
// it's not really necessary to do this streamingly, but I'm doing it
|
|
// anyway in case this changes in the future.
|
|
totalByteLength += value.byteLength;
|
|
lru.updateSize(lruEntry, totalByteLength);
|
|
continue;
|
|
}
|
|
// The server stream has closed. Exit, but intentionally do not close
|
|
// the target stream.
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
function createPromiseWithResolvers() {
|
|
// Shim of Stage 4 Promise.withResolvers proposal
|
|
let resolve;
|
|
let reject;
|
|
const promise = new Promise((res, rej)=>{
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
return {
|
|
resolve: resolve,
|
|
reject: reject,
|
|
promise
|
|
};
|
|
}
|
|
|
|
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
|
|
Object.defineProperty(exports.default, '__esModule', { value: true });
|
|
Object.assign(exports.default, exports);
|
|
module.exports = exports.default;
|
|
}
|
|
|
|
//# sourceMappingURL=cache.js.map
|