@speedkit/cli 4.15.0 → 4.16.0
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/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/services/integration-api/file-reader.d.ts +2 -1
- package/dist/services/integration-api/file-reader.js +5 -1
- package/dist/services/integration-api/integration-api-factory.js +1 -1
- package/dist/services/integration-api/integration-api-model.d.ts +3 -1
- package/dist/services/integration-api/integration-api-model.js +11 -2
- package/dist/services/onboarding/browser/baqend-response.d.ts +5 -0
- package/dist/services/onboarding/browser/baqend-response.js +9 -0
- package/dist/services/onboarding/fetch-events/speed-kit-rum-pi-request.d.ts +5 -6
- package/dist/services/onboarding/fetch-events/speed-kit-rum-pi-request.js +7 -8
- package/dist/services/onboarding/file-events/file-watcher.d.ts +12 -0
- package/dist/services/onboarding/file-events/file-watcher.js +24 -2
- package/dist/services/onboarding/onboarding-service-factory.js +5 -3
- package/dist/services/onboarding/virtual-orestes-app/index.d.ts +30 -0
- package/dist/services/onboarding/virtual-orestes-app/index.js +77 -14
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [4.16.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.15.1...v4.16.0) (2026-07-13)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **onboarding:** refetch last 10 cached entries after fileChange ([779c6af](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/779c6af13e371af2b6d04621301921920139ca80))
|
|
7
|
+
|
|
8
|
+
## [4.15.1](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.15.0...v4.15.1) (2026-07-08)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* ignore page-examples.json for deployments ([209fec8](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/209fec89d17f7b6c34a5891fb761564cdcdad35c))
|
|
14
|
+
|
|
1
15
|
# [4.15.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.14.0...v4.15.0) (2026-07-03)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -5,13 +5,14 @@ export default class FileReader {
|
|
|
5
5
|
private readonly basePath;
|
|
6
6
|
private readonly whiteList;
|
|
7
7
|
private readonly blockedFolders;
|
|
8
|
+
private readonly blockedFiles;
|
|
8
9
|
allowedExtensions: string[];
|
|
9
10
|
constructor(fileSystem: {
|
|
10
11
|
readFileSync: (path: string, options: "utf-8") => string;
|
|
11
12
|
readdirSync: (path: string, options: {
|
|
12
13
|
withFileTypes: boolean;
|
|
13
14
|
}) => Dirent[];
|
|
14
|
-
}, configName: string, basePath: string, whiteList?: string[], blockedFolders?: string[]);
|
|
15
|
+
}, configName: string, basePath: string, whiteList?: string[], blockedFolders?: string[], blockedFiles?: string[]);
|
|
15
16
|
getFileList(basePath?: string): string[];
|
|
16
17
|
getContent(path: string): Promise<string>;
|
|
17
18
|
private isAllowedFolder;
|
|
@@ -9,13 +9,15 @@ export default class FileReader {
|
|
|
9
9
|
basePath;
|
|
10
10
|
whiteList;
|
|
11
11
|
blockedFolders;
|
|
12
|
+
blockedFiles;
|
|
12
13
|
allowedExtensions = ["js", "es6", "json", "css"];
|
|
13
|
-
constructor(fileSystem, configName, basePath, whiteList = [], blockedFolders = []) {
|
|
14
|
+
constructor(fileSystem, configName, basePath, whiteList = [], blockedFolders = [], blockedFiles = []) {
|
|
14
15
|
this.fileSystem = fileSystem;
|
|
15
16
|
this.configName = configName;
|
|
16
17
|
this.basePath = basePath;
|
|
17
18
|
this.whiteList = whiteList;
|
|
18
19
|
this.blockedFolders = blockedFolders;
|
|
20
|
+
this.blockedFiles = blockedFiles;
|
|
19
21
|
}
|
|
20
22
|
getFileList(basePath = this.basePath) {
|
|
21
23
|
let files = [];
|
|
@@ -54,6 +56,8 @@ export default class FileReader {
|
|
|
54
56
|
}
|
|
55
57
|
isAllowedFile(fileName) {
|
|
56
58
|
const extension = this.getFileExtension(fileName);
|
|
59
|
+
if (this.blockedFiles.some((file) => fileName.includes(file)))
|
|
60
|
+
return false;
|
|
57
61
|
return (this.allowedExtensions.includes(extension) && this.isWhiteListed(fileName));
|
|
58
62
|
}
|
|
59
63
|
getFileExtension(fileName) {
|
|
@@ -38,7 +38,7 @@ export class IntegrationApiFactory {
|
|
|
38
38
|
}
|
|
39
39
|
getFileReader() {
|
|
40
40
|
if (!this.fileReader) {
|
|
41
|
-
this.fileReader = new FileReader(fs, this.context.configName, this.context.basePath, this.context.whitelist, this.context.blockedFolders);
|
|
41
|
+
this.fileReader = new FileReader(fs, this.context.configName, this.context.basePath, this.context.whitelist, this.context.blockedFolders, this.context.blockedFiles);
|
|
42
42
|
}
|
|
43
43
|
return this.fileReader;
|
|
44
44
|
}
|
|
@@ -100,11 +100,13 @@ export interface VirtualFileConfig {
|
|
|
100
100
|
overrideLocalFile?: boolean;
|
|
101
101
|
}
|
|
102
102
|
export declare const DEFAULT_BLOCKED_FOLDERS: string[];
|
|
103
|
+
export declare const DEFAULT_BLOCKED_FILES: string[];
|
|
103
104
|
export declare class IntegrationApiContext {
|
|
104
105
|
readonly basePath: string;
|
|
105
106
|
readonly configName: string;
|
|
106
107
|
readonly whitelist: string[];
|
|
107
108
|
readonly blockedFolders: string[];
|
|
108
109
|
readonly useTestConfig: boolean;
|
|
109
|
-
|
|
110
|
+
readonly blockedFiles: string[];
|
|
111
|
+
constructor(basePath: string, configName: string, whitelist?: string[], blockedFolders?: string[], useTestConfig?: boolean, blockedFiles?: string[]);
|
|
110
112
|
}
|
|
@@ -19,18 +19,27 @@ export var FILE_TYPE;
|
|
|
19
19
|
FILE_TYPE["READONLY_FILE"] = "readonly";
|
|
20
20
|
FILE_TYPE["CUSTOMER_FILE"] = "customer";
|
|
21
21
|
})(FILE_TYPE || (FILE_TYPE = {}));
|
|
22
|
-
export const DEFAULT_BLOCKED_FOLDERS = [
|
|
22
|
+
export const DEFAULT_BLOCKED_FOLDERS = [
|
|
23
|
+
"test",
|
|
24
|
+
".local",
|
|
25
|
+
".git",
|
|
26
|
+
".idea",
|
|
27
|
+
"page-examples",
|
|
28
|
+
];
|
|
29
|
+
export const DEFAULT_BLOCKED_FILES = ["page-examples"];
|
|
23
30
|
export class IntegrationApiContext {
|
|
24
31
|
basePath;
|
|
25
32
|
configName;
|
|
26
33
|
whitelist;
|
|
27
34
|
blockedFolders;
|
|
28
35
|
useTestConfig;
|
|
29
|
-
|
|
36
|
+
blockedFiles;
|
|
37
|
+
constructor(basePath, configName, whitelist = [], blockedFolders = DEFAULT_BLOCKED_FOLDERS, useTestConfig = false, blockedFiles = DEFAULT_BLOCKED_FILES) {
|
|
30
38
|
this.basePath = basePath;
|
|
31
39
|
this.configName = configName;
|
|
32
40
|
this.whitelist = whitelist;
|
|
33
41
|
this.blockedFolders = blockedFolders;
|
|
34
42
|
this.useTestConfig = useTestConfig;
|
|
43
|
+
this.blockedFiles = blockedFiles;
|
|
35
44
|
}
|
|
36
45
|
}
|
|
@@ -7,6 +7,11 @@ export declare class BaqendResponse implements Partial<Protocol.Fetch.FulfillReq
|
|
|
7
7
|
private readonly contentType;
|
|
8
8
|
private readonly contentLength;
|
|
9
9
|
constructor(body: string | Buffer, contentType?: string, headers?: Protocol.Fetch.HeaderEntry[], status?: number);
|
|
10
|
+
/**
|
|
11
|
+
* Epoch ms parsed from the `last-modified` header (set when the response was
|
|
12
|
+
* built), or 0 if absent/unparseable. Used to order cache entries by recency.
|
|
13
|
+
*/
|
|
14
|
+
getLastModified(): number;
|
|
10
15
|
setHeader(name: string, value: string): void;
|
|
11
16
|
protected addCustomHeaders(headers?: Protocol.Fetch.HeaderEntry[]): Protocol.Fetch.HeaderEntry[];
|
|
12
17
|
}
|
|
@@ -23,6 +23,15 @@ export class BaqendResponse {
|
|
|
23
23
|
? body.toString("base64")
|
|
24
24
|
: Buffer.from(body).toString("base64");
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Epoch ms parsed from the `last-modified` header (set when the response was
|
|
28
|
+
* built), or 0 if absent/unparseable. Used to order cache entries by recency.
|
|
29
|
+
*/
|
|
30
|
+
getLastModified() {
|
|
31
|
+
const header = this.responseHeaders.find((h) => h.name.toLowerCase() === "last-modified");
|
|
32
|
+
const timestamp = header ? Date.parse(header.value) : NaN;
|
|
33
|
+
return Number.isNaN(timestamp) ? 0 : timestamp;
|
|
34
|
+
}
|
|
26
35
|
setHeader(name, value) {
|
|
27
36
|
for (const header of this.responseHeaders) {
|
|
28
37
|
if (header.name === name) {
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import { CDPSession } from "puppeteer";
|
|
2
2
|
import { Protocol } from "puppeteer-core";
|
|
3
|
-
import { CliService } from "../../cli/index.js";
|
|
4
3
|
import { CustomerConfig } from "../../integration-api/index.js";
|
|
5
4
|
import { AbortResponse } from "../browser/abort-response.js";
|
|
6
5
|
import { FetchRequestPausedEventHandler } from "../onboarding-model.js";
|
|
7
|
-
import {
|
|
6
|
+
import { VirtualOrestesApp } from "../virtual-orestes-app/index.js";
|
|
8
7
|
/**
|
|
9
8
|
* Intercepts v1/rum/pi requests,
|
|
10
|
-
* if request contains changeDetection,
|
|
9
|
+
* if request contains changeDetection, refresh dh-cache for respective url
|
|
10
|
+
* in the background
|
|
11
11
|
*
|
|
12
12
|
* @implements FetchRequestPausedEventHandler
|
|
13
13
|
*/
|
|
14
14
|
export declare class SpeedKitRumPiRequest implements FetchRequestPausedEventHandler {
|
|
15
15
|
private customerConfig;
|
|
16
|
-
private
|
|
17
|
-
private cli;
|
|
16
|
+
private orestesApp;
|
|
18
17
|
readonly patterns: Protocol.Fetch.RequestPattern[];
|
|
19
|
-
constructor(customerConfig: CustomerConfig,
|
|
18
|
+
constructor(customerConfig: CustomerConfig, orestesApp: VirtualOrestesApp);
|
|
20
19
|
handle(event: Protocol.Fetch.RequestPausedEvent, _: CDPSession): Promise<AbortResponse | Partial<Protocol.Fetch.FulfillRequestRequest> | null>;
|
|
21
20
|
}
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { safe } from "../../../helpers/safe.js";
|
|
2
2
|
/**
|
|
3
3
|
* Intercepts v1/rum/pi requests,
|
|
4
|
-
* if request contains changeDetection,
|
|
4
|
+
* if request contains changeDetection, refresh dh-cache for respective url
|
|
5
|
+
* in the background
|
|
5
6
|
*
|
|
6
7
|
* @implements FetchRequestPausedEventHandler
|
|
7
8
|
*/
|
|
8
9
|
export class SpeedKitRumPiRequest {
|
|
9
10
|
customerConfig;
|
|
10
|
-
|
|
11
|
-
cli;
|
|
11
|
+
orestesApp;
|
|
12
12
|
patterns;
|
|
13
|
-
constructor(customerConfig,
|
|
13
|
+
constructor(customerConfig, orestesApp) {
|
|
14
14
|
this.customerConfig = customerConfig;
|
|
15
|
-
this.
|
|
16
|
-
this.cli = cli;
|
|
15
|
+
this.orestesApp = orestesApp;
|
|
17
16
|
this.patterns = [
|
|
18
17
|
{
|
|
19
18
|
requestStage: "Request",
|
|
@@ -33,7 +32,7 @@ export class SpeedKitRumPiRequest {
|
|
|
33
32
|
return;
|
|
34
33
|
}
|
|
35
34
|
const changeDetection = result.data.changeDetection;
|
|
36
|
-
|
|
37
|
-
this.
|
|
35
|
+
// refresh in the background - do not block the intercepted rum/pi request
|
|
36
|
+
void this.orestesApp.refreshUrl(changeDetection.url);
|
|
38
37
|
}
|
|
39
38
|
}
|
|
@@ -4,6 +4,7 @@ import { DocumentHandlerServer } from "../../document-handler-runtime/document-h
|
|
|
4
4
|
import { Cache } from "../request-cache/cache.js";
|
|
5
5
|
import { ConfigFileInterface, CustomerConfig, FileListInterface } from "../../integration-api/index.js";
|
|
6
6
|
import { BrowserContext } from "../onboarding-model.js";
|
|
7
|
+
import { VirtualOrestesApp } from "../virtual-orestes-app/index.js";
|
|
7
8
|
/**
|
|
8
9
|
* Add different callbacks that will be executed if a local file is changed.
|
|
9
10
|
*/
|
|
@@ -14,7 +15,18 @@ export declare class FileWatcher {
|
|
|
14
15
|
private documentHandler;
|
|
15
16
|
private browserContext;
|
|
16
17
|
private cache;
|
|
18
|
+
private cacheRefresher?;
|
|
17
19
|
constructor(cli: CliService, customerConfig: CustomerConfig, files: FileListInterface, documentHandler: DocumentHandlerServer, browserContext: BrowserContext, cache: Cache);
|
|
20
|
+
setCacheRefresher(cacheRefresher: VirtualOrestesApp): void;
|
|
21
|
+
/**
|
|
22
|
+
* Drop the dh-cache after a config change. When a cache refresher is wired
|
|
23
|
+
* (local mode) the most recent entries are re-warmed in the background so the
|
|
24
|
+
* developer's current pages stay instant and fresh; otherwise the cache is
|
|
25
|
+
* simply cleared.
|
|
26
|
+
*
|
|
27
|
+
* @private
|
|
28
|
+
*/
|
|
29
|
+
private clearOrRefreshCache;
|
|
18
30
|
/**
|
|
19
31
|
* will perform various actions based on the changed file
|
|
20
32
|
*
|
|
@@ -12,6 +12,9 @@ export class FileWatcher {
|
|
|
12
12
|
documentHandler;
|
|
13
13
|
browserContext;
|
|
14
14
|
cache;
|
|
15
|
+
// Set in local mode (see OnboardingServiceFactory). When present, config
|
|
16
|
+
// changes re-warm the dh-cache in the background instead of just clearing it.
|
|
17
|
+
cacheRefresher;
|
|
15
18
|
constructor(cli, customerConfig, files, documentHandler, browserContext, cache) {
|
|
16
19
|
this.cli = cli;
|
|
17
20
|
this.customerConfig = customerConfig;
|
|
@@ -20,6 +23,25 @@ export class FileWatcher {
|
|
|
20
23
|
this.browserContext = browserContext;
|
|
21
24
|
this.cache = cache;
|
|
22
25
|
}
|
|
26
|
+
setCacheRefresher(cacheRefresher) {
|
|
27
|
+
this.cacheRefresher = cacheRefresher;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Drop the dh-cache after a config change. When a cache refresher is wired
|
|
31
|
+
* (local mode) the most recent entries are re-warmed in the background so the
|
|
32
|
+
* developer's current pages stay instant and fresh; otherwise the cache is
|
|
33
|
+
* simply cleared.
|
|
34
|
+
*
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
clearOrRefreshCache() {
|
|
38
|
+
if (this.cacheRefresher) {
|
|
39
|
+
// fire-and-forget: refresh clears the cache first, then re-warms
|
|
40
|
+
void this.cacheRefresher.refreshRecent(10);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.cache.clear();
|
|
44
|
+
}
|
|
23
45
|
/**
|
|
24
46
|
* will perform various actions based on the changed file
|
|
25
47
|
*
|
|
@@ -157,7 +179,7 @@ export class FileWatcher {
|
|
|
157
179
|
this.cli.writeWarning(`changed file: ${file.name}`);
|
|
158
180
|
await this.safeClearPageBrowsersCacheStorage(page);
|
|
159
181
|
await this.documentHandler.buildDocumentHandler();
|
|
160
|
-
this.
|
|
182
|
+
this.clearOrRefreshCache();
|
|
161
183
|
await installResource.buildContent();
|
|
162
184
|
}
|
|
163
185
|
/**
|
|
@@ -170,7 +192,7 @@ export class FileWatcher {
|
|
|
170
192
|
async onChangeDynamicStyleCallback(file, page) {
|
|
171
193
|
this.cli.writeWarning(`changed file: ${file.name}`);
|
|
172
194
|
await this.documentHandler.buildDocumentHandler();
|
|
173
|
-
this.
|
|
195
|
+
this.clearOrRefreshCache();
|
|
174
196
|
const escapedStyles = encodeURI(file.getContent());
|
|
175
197
|
const evaluateResult = await safe(race(Promise.all([
|
|
176
198
|
page.evaluate(this.updateStylesInlineFunction, escapedStyles),
|
|
@@ -83,7 +83,7 @@ export class OnboardingServiceFactory {
|
|
|
83
83
|
const fileWatcher = new FileWatcher(cli, customerConfig, files, documentHandler, browserContext, cache);
|
|
84
84
|
const athenaClient = (await this.getAthenaClient(customerConfig.app)) || null;
|
|
85
85
|
const messageHandler = await this.prepareMessagehandler(athenaClient, files, customerConfig);
|
|
86
|
-
const fetchHandler = await this.getFetchEventHandler(files, customerConfig, documentHandler, cache, cli, browserContext, athenaClient, messageHandler);
|
|
86
|
+
const fetchHandler = await this.getFetchEventHandler(files, customerConfig, documentHandler, cache, cli, browserContext, athenaClient, messageHandler, fileWatcher);
|
|
87
87
|
const extensionBridge = new ExtensionBridge(cli);
|
|
88
88
|
return new OnboardingService(browserContext, customerConfig, fetchHandler, fileWatcher, cli, messageHandler, extensionBridge);
|
|
89
89
|
}
|
|
@@ -145,7 +145,7 @@ export class OnboardingServiceFactory {
|
|
|
145
145
|
const configApiContext = new ConfigApiContext(app);
|
|
146
146
|
return new ConfigApiServiceFactory(configApiContext).getService();
|
|
147
147
|
}
|
|
148
|
-
async getFetchEventHandler(files, customerConfig, documentHandler, cache, cli, browserContext, athenaClient, messageHandler) {
|
|
148
|
+
async getFetchEventHandler(files, customerConfig, documentHandler, cache, cli, browserContext, athenaClient, messageHandler, fileWatcher) {
|
|
149
149
|
const configApi = this.getConfigApi(customerConfig.app);
|
|
150
150
|
const serverConfig = await this.getSpeedKitServerConfig(configApi, cli);
|
|
151
151
|
const agent = this.getCrawlerAgent(customerConfig, cli);
|
|
@@ -167,7 +167,9 @@ export class OnboardingServiceFactory {
|
|
|
167
167
|
];
|
|
168
168
|
if (this.context.local) {
|
|
169
169
|
const orestesApp = new VirtualOrestesApp(customerConfig, crawler, documentHandler, cache, cli, messageHandler);
|
|
170
|
-
|
|
170
|
+
// let config-file changes re-warm the dh-cache in the background
|
|
171
|
+
fileWatcher.setCacheRefresher(orestesApp);
|
|
172
|
+
handlers.push(new SpeedKitAssetRequest(customerConfig, orestesApp), new SpeedKitRumPiRequest(customerConfig, orestesApp));
|
|
171
173
|
}
|
|
172
174
|
return new FetchEventHandler(handlers, customerConfig, cli, configApi);
|
|
173
175
|
}
|
|
@@ -16,6 +16,36 @@ export declare class VirtualOrestesApp {
|
|
|
16
16
|
private localCacheErrors;
|
|
17
17
|
constructor(customerConfig: CustomerConfig, crawler: Crawler, documentHandler: DocumentHandlerServer, cache: Cache, cli: CliService, developmentToolsMessages: DevtoolsExtensionApi);
|
|
18
18
|
fetchAssetResponse(event: Protocol.Fetch.RequestPausedEvent): Promise<Partial<Protocol.Fetch.FulfillRequestRequest> | AbortResponse>;
|
|
19
|
+
/**
|
|
20
|
+
* Re-warm all cached entries for the given origin url in the background.
|
|
21
|
+
*
|
|
22
|
+
* Used when Speed Kit reports a changeDetection for a url: instead of just
|
|
23
|
+
* dropping the entry (which forces a cache-miss on the next request), we
|
|
24
|
+
* rebuild every cached variation of that url in place so the next request is
|
|
25
|
+
* an instant, fresh HIT. Fire-and-forget by design.
|
|
26
|
+
*
|
|
27
|
+
* @param originUrl the origin url reported by changeDetection
|
|
28
|
+
*/
|
|
29
|
+
refreshUrl(originUrl: string): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Clear the whole dh-cache, then re-warm the most recently cached entries in
|
|
32
|
+
* the background (ordered by their `last-modified` header). Used as a
|
|
33
|
+
* replacement for a bare cache clear on config changes: stale entries are
|
|
34
|
+
* dropped, but the last `limit` pages are rebuilt proactively so the
|
|
35
|
+
* developer's current pages stay instant and fresh.
|
|
36
|
+
*
|
|
37
|
+
* @param limit how many of the most recently cached entries to re-warm
|
|
38
|
+
*/
|
|
39
|
+
refreshRecent(limit?: number): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Rebuild a single cache entry (by its full asset request url), replacing the
|
|
42
|
+
* previously cached response. On failure the stale entry is dropped so we
|
|
43
|
+
* never keep serving outdated content.
|
|
44
|
+
*
|
|
45
|
+
* @param requestUrl the full asset request url used as the cache key
|
|
46
|
+
*/
|
|
47
|
+
private refreshEntry;
|
|
48
|
+
private buildResponse;
|
|
19
49
|
private getResponseText;
|
|
20
50
|
private localCacheErrorKey;
|
|
21
51
|
private recordLocalCacheError;
|
|
@@ -28,15 +28,78 @@ export class VirtualOrestesApp {
|
|
|
28
28
|
this.developmentToolsMessages = developmentToolsMessages;
|
|
29
29
|
}
|
|
30
30
|
async fetchAssetResponse(event) {
|
|
31
|
-
const originUrl = this.getAssetUrl(event.request.url);
|
|
32
|
-
const UrlObject = new URL(event.request.url);
|
|
33
|
-
const variation = UrlObject.searchParams.has("bqvariation")
|
|
34
|
-
? UrlObject.searchParams.get("bqvariation").trim()
|
|
35
|
-
: "default";
|
|
36
31
|
const cachedResponse = this.getCachedResponse(event.request.url);
|
|
37
32
|
if (cachedResponse instanceof BaqendResponse) {
|
|
38
33
|
return cachedResponse;
|
|
39
34
|
}
|
|
35
|
+
return this.buildResponse(event.request.url);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Re-warm all cached entries for the given origin url in the background.
|
|
39
|
+
*
|
|
40
|
+
* Used when Speed Kit reports a changeDetection for a url: instead of just
|
|
41
|
+
* dropping the entry (which forces a cache-miss on the next request), we
|
|
42
|
+
* rebuild every cached variation of that url in place so the next request is
|
|
43
|
+
* an instant, fresh HIT. Fire-and-forget by design.
|
|
44
|
+
*
|
|
45
|
+
* @param originUrl the origin url reported by changeDetection
|
|
46
|
+
*/
|
|
47
|
+
async refreshUrl(originUrl) {
|
|
48
|
+
const matchingKeys = [...this.cache.getAll()]
|
|
49
|
+
.map(([key]) => key)
|
|
50
|
+
.filter((key) => key.includes(originUrl));
|
|
51
|
+
if (matchingKeys.length === 0) {
|
|
52
|
+
this.cli.write(`changeDetection for url: ${originUrl} - nothing cached to refresh`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.cli.write(`changeDetection triggered for url: ${originUrl} - refreshing dh-cache in background`);
|
|
56
|
+
for (const key of matchingKeys) {
|
|
57
|
+
await this.refreshEntry(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Clear the whole dh-cache, then re-warm the most recently cached entries in
|
|
62
|
+
* the background (ordered by their `last-modified` header). Used as a
|
|
63
|
+
* replacement for a bare cache clear on config changes: stale entries are
|
|
64
|
+
* dropped, but the last `limit` pages are rebuilt proactively so the
|
|
65
|
+
* developer's current pages stay instant and fresh.
|
|
66
|
+
*
|
|
67
|
+
* @param limit how many of the most recently cached entries to re-warm
|
|
68
|
+
*/
|
|
69
|
+
async refreshRecent(limit = 10) {
|
|
70
|
+
const recentKeys = [...this.cache.getAll()]
|
|
71
|
+
.sort(([, a], [, b]) => b.getLastModified() - a.getLastModified())
|
|
72
|
+
.slice(0, limit)
|
|
73
|
+
.map(([key]) => key);
|
|
74
|
+
this.cache.clear();
|
|
75
|
+
if (recentKeys.length === 0) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.cli.write(`refreshing ${recentKeys.length} recent dh-cache entries in background`);
|
|
79
|
+
for (const key of recentKeys) {
|
|
80
|
+
await this.refreshEntry(key);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Rebuild a single cache entry (by its full asset request url), replacing the
|
|
85
|
+
* previously cached response. On failure the stale entry is dropped so we
|
|
86
|
+
* never keep serving outdated content.
|
|
87
|
+
*
|
|
88
|
+
* @param requestUrl the full asset request url used as the cache key
|
|
89
|
+
*/
|
|
90
|
+
async refreshEntry(requestUrl) {
|
|
91
|
+
const result = await safe(this.buildResponse(requestUrl));
|
|
92
|
+
if (result.success === false) {
|
|
93
|
+
this.cache.removeEntry(requestUrl);
|
|
94
|
+
this.cli.writeError(`failed to refresh dh-cache entry ${requestUrl}: ${result.error}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async buildResponse(requestUrl) {
|
|
98
|
+
const originUrl = this.getAssetUrl(requestUrl);
|
|
99
|
+
const UrlObject = new URL(requestUrl);
|
|
100
|
+
const variation = UrlObject.searchParams.has("bqvariation")
|
|
101
|
+
? UrlObject.searchParams.get("bqvariation").trim()
|
|
102
|
+
: "default";
|
|
40
103
|
const response = await this.crawler.fetchRemote(originUrl, variation);
|
|
41
104
|
// todo check if we can handle more then htmlResponses
|
|
42
105
|
// only handle html responses
|
|
@@ -49,21 +112,21 @@ export class VirtualOrestesApp {
|
|
|
49
112
|
const customHeaders = this.prepareCustomHeaders(originUrl, hasNoStoreFlag);
|
|
50
113
|
// CDN Redirect
|
|
51
114
|
if (originUrl !== response.url) {
|
|
52
|
-
return this.handleCdnRedirect(customHeaders, response,
|
|
115
|
+
return this.handleCdnRedirect(customHeaders, response, requestUrl);
|
|
53
116
|
}
|
|
54
117
|
if (response.status > 300 && response.status < 400) {
|
|
55
|
-
return this.returnRedirect(customHeaders, response,
|
|
118
|
+
return this.returnRedirect(customHeaders, response, requestUrl);
|
|
56
119
|
}
|
|
57
120
|
const { responseContent, textEncoding } = await this.getResponseText(response);
|
|
58
121
|
if (!this.isHtmlContentHeader(response?.headers) ||
|
|
59
122
|
!this.isValidHtmlContent(responseContent)) {
|
|
60
123
|
const customResponse = new BaqendResponse(responseContent, `text/html; charset=${textEncoding}`, customHeaders);
|
|
61
|
-
this.cache.addEntry(
|
|
124
|
+
this.cache.addEntry(requestUrl, customResponse);
|
|
62
125
|
return customResponse;
|
|
63
126
|
}
|
|
64
127
|
const customResponse = await safe(this.documentHandler.transform(`text/html;charset=${textEncoding}`, responseContent, variation, originUrl, this.convertResponseHeadersToOrestesFormat(response)));
|
|
65
128
|
if (customResponse.success === false) {
|
|
66
|
-
this.recordLocalCacheError(originUrl,
|
|
129
|
+
this.recordLocalCacheError(originUrl, requestUrl, variation, customResponse);
|
|
67
130
|
return this.returnErrorResponse(customResponse, customHeaders);
|
|
68
131
|
}
|
|
69
132
|
this.clearLocalCacheError(originUrl, variation);
|
|
@@ -88,7 +151,7 @@ export class VirtualOrestesApp {
|
|
|
88
151
|
const baqendResponse = new BaqendResponse(Buffer.isBuffer(skResponse.body)
|
|
89
152
|
? skResponse.body
|
|
90
153
|
: iconv.encode(skResponse.body, textEncoding), skResponse.headers["content-type"] ?? `text/html;charset=${textEncoding}`, [...customHeaders, ...originHeaders]);
|
|
91
|
-
this.cache.addEntry(
|
|
154
|
+
this.cache.addEntry(requestUrl, baqendResponse);
|
|
92
155
|
return baqendResponse;
|
|
93
156
|
}
|
|
94
157
|
async getResponseText(response) {
|
|
@@ -159,16 +222,16 @@ export class VirtualOrestesApp {
|
|
|
159
222
|
});
|
|
160
223
|
return convertedResponseHeaders;
|
|
161
224
|
}
|
|
162
|
-
returnRedirect(customHeaders, response,
|
|
225
|
+
returnRedirect(customHeaders, response, requestUrl) {
|
|
163
226
|
customHeaders.push({
|
|
164
227
|
name: "location",
|
|
165
228
|
value: response?.headers.get("location") || "",
|
|
166
229
|
});
|
|
167
230
|
const redirectResponse = new BaqendResponse("", null, customHeaders, response.status);
|
|
168
|
-
this.cache.addEntry(
|
|
231
|
+
this.cache.addEntry(requestUrl, redirectResponse);
|
|
169
232
|
return redirectResponse;
|
|
170
233
|
}
|
|
171
|
-
handleCdnRedirect(customHeaders, response,
|
|
234
|
+
handleCdnRedirect(customHeaders, response, requestUrl) {
|
|
172
235
|
customHeaders.push({
|
|
173
236
|
name: "x-debug-response-url",
|
|
174
237
|
value: response.url,
|
|
@@ -181,7 +244,7 @@ export class VirtualOrestesApp {
|
|
|
181
244
|
});
|
|
182
245
|
serverTimingHeader.value += ",error;desc=202";
|
|
183
246
|
const redirectResponse = new BaqendResponse("", null, customHeaders, 400);
|
|
184
|
-
this.cache.addEntry(
|
|
247
|
+
this.cache.addEntry(requestUrl, redirectResponse);
|
|
185
248
|
return redirectResponse;
|
|
186
249
|
}
|
|
187
250
|
getAssetUrl(requestUrl) {
|
package/oclif.manifest.json
CHANGED