@speedkit/cli 4.26.0 → 4.27.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.
Files changed (149) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +203 -15
  3. package/dist/commands/auto-prewarm.d.ts +12 -1
  4. package/dist/commands/auto-prewarm.js +26 -7
  5. package/dist/commands/generate-pop-config.d.ts +1 -0
  6. package/dist/commands/generate-pop-config.js +9 -3
  7. package/dist/commands/prewarm.js +3 -1
  8. package/dist/commands/query/parameter.d.ts +27 -0
  9. package/dist/commands/query/parameter.js +28 -0
  10. package/dist/commands/query/prewarm.d.ts +32 -0
  11. package/dist/commands/query/prewarm.js +32 -0
  12. package/dist/helpers/clipboard.d.ts +19 -0
  13. package/dist/helpers/clipboard.js +70 -0
  14. package/dist/helpers/evaluate-speed-kit-config.d.ts +8 -18
  15. package/dist/helpers/evaluate-speed-kit-config.js +8 -6
  16. package/dist/helpers/evaluate-speed-kit-config.spec.d.ts +1 -0
  17. package/dist/helpers/evaluate-speed-kit-config.spec.js +78 -0
  18. package/dist/models/cli-parameters.d.ts +106 -0
  19. package/dist/models/cli-parameters.js +183 -0
  20. package/dist/models/cli-parameters.spec.d.ts +1 -0
  21. package/dist/models/cli-parameters.spec.js +47 -0
  22. package/dist/services/athena/athena-service-factory.d.ts +4 -1
  23. package/dist/services/athena/athena-service-factory.js +9 -3
  24. package/dist/services/athena/athena-service.d.ts +13 -1
  25. package/dist/services/athena/athena-service.js +27 -5
  26. package/dist/services/athena/athena-service.spec.d.ts +1 -0
  27. package/dist/services/athena/athena-service.spec.js +93 -0
  28. package/dist/services/deploy/handler/install-resource-handler.js +3 -4
  29. package/dist/services/onboarding/onboarding-service-factory.js +2 -2
  30. package/dist/services/pop-config/pop-config-service.d.ts +8 -1
  31. package/dist/services/pop-config/pop-config-service.js +16 -28
  32. package/dist/services/prewarm/assets/asset-api-client.d.ts +1 -1
  33. package/dist/services/prewarm/assets/asset-api-client.js +5 -4
  34. package/dist/services/prewarm/auto-pre-warm-factory.d.ts +20 -2
  35. package/dist/services/prewarm/auto-pre-warm-factory.js +24 -6
  36. package/dist/services/prewarm/csv-reader.d.ts +21 -2
  37. package/dist/services/prewarm/csv-reader.js +71 -9
  38. package/dist/services/prewarm/csv-reader.spec.d.ts +1 -0
  39. package/dist/services/prewarm/csv-reader.spec.js +75 -0
  40. package/dist/services/prewarm/index.d.ts +1 -0
  41. package/dist/services/prewarm/index.js +1 -0
  42. package/dist/services/prewarm/pre-warm-factory.d.ts +0 -2
  43. package/dist/services/prewarm/pre-warm-factory.js +8 -11
  44. package/dist/services/prewarm/pre-warm-model.d.ts +14 -1
  45. package/dist/services/prewarm/pre-warm-model.js +0 -1
  46. package/dist/services/prewarm/pre-warm-service.d.ts +9 -4
  47. package/dist/services/prewarm/pre-warm-service.js +18 -14
  48. package/dist/services/prewarm/prewarm-targets.d.ts +12 -0
  49. package/dist/services/prewarm/prewarm-targets.js +18 -0
  50. package/dist/services/prewarm/prewarm-targets.spec.d.ts +1 -0
  51. package/dist/services/prewarm/prewarm-targets.spec.js +29 -0
  52. package/dist/services/query-builder/error/match-matches-nothing-error.d.ts +11 -0
  53. package/dist/services/query-builder/error/match-matches-nothing-error.js +17 -0
  54. package/dist/services/query-builder/queries/device.d.ts +21 -0
  55. package/dist/services/query-builder/queries/device.js +26 -0
  56. package/dist/services/query-builder/queries/index.d.ts +6 -0
  57. package/dist/services/query-builder/queries/index.js +6 -0
  58. package/dist/services/query-builder/queries/page-filter.d.ts +39 -0
  59. package/dist/services/query-builder/queries/page-filter.js +135 -0
  60. package/dist/services/query-builder/queries/page-source.d.ts +30 -0
  61. package/dist/services/query-builder/queries/page-source.js +79 -0
  62. package/dist/services/query-builder/queries/parameter-query.d.ts +14 -0
  63. package/dist/services/query-builder/queries/parameter-query.js +142 -0
  64. package/dist/services/query-builder/queries/pops-query.d.ts +27 -0
  65. package/dist/services/query-builder/queries/pops-query.js +65 -0
  66. package/dist/services/query-builder/queries/prewarm-query.d.ts +18 -0
  67. package/dist/services/query-builder/queries/prewarm-query.js +227 -0
  68. package/dist/services/query-builder/queries/query-model.d.ts +97 -0
  69. package/dist/services/query-builder/queries/query-model.js +19 -0
  70. package/dist/services/query-builder/queries/spec/golden-configs.d.ts +18 -0
  71. package/dist/services/query-builder/queries/spec/golden-configs.js +116 -0
  72. package/dist/services/query-builder/queries/spec/golden.spec.d.ts +1 -0
  73. package/dist/services/query-builder/queries/spec/golden.spec.js +161 -0
  74. package/dist/services/query-builder/queries/spec/pops-query.spec.d.ts +1 -0
  75. package/dist/services/query-builder/queries/spec/pops-query.spec.js +58 -0
  76. package/dist/services/query-builder/queries/spec/queries.spec.d.ts +1 -0
  77. package/dist/services/query-builder/queries/spec/queries.spec.js +312 -0
  78. package/dist/services/query-builder/queries/spec/reported-bugs.spec.d.ts +1 -0
  79. package/dist/services/query-builder/queries/spec/reported-bugs.spec.js +86 -0
  80. package/dist/services/query-builder/query-builder-factory.d.ts +2 -2
  81. package/dist/services/query-builder/query-builder-factory.js +36 -8
  82. package/dist/services/query-builder/query-builder-model.d.ts +33 -19
  83. package/dist/services/query-builder/query-builder-model.js +5 -5
  84. package/dist/services/query-builder/query-builder-service.d.ts +26 -5
  85. package/dist/services/query-builder/query-builder-service.js +157 -44
  86. package/dist/services/query-builder/query-command.d.ts +27 -0
  87. package/dist/services/query-builder/query-command.js +36 -0
  88. package/dist/services/query-builder/rules/index.d.ts +8 -0
  89. package/dist/services/query-builder/rules/index.js +8 -0
  90. package/dist/services/query-builder/rules/match-pattern.d.ts +51 -0
  91. package/dist/services/query-builder/rules/match-pattern.js +141 -0
  92. package/dist/services/query-builder/rules/pattern-language.d.ts +59 -0
  93. package/dist/services/query-builder/rules/pattern-language.js +159 -0
  94. package/dist/services/query-builder/rules/rule-model.d.ts +107 -0
  95. package/dist/services/query-builder/rules/rule-model.js +54 -0
  96. package/dist/services/query-builder/rules/rule-parser.d.ts +33 -0
  97. package/dist/services/query-builder/rules/rule-parser.js +189 -0
  98. package/dist/services/query-builder/rules/rule-reducer.d.ts +71 -0
  99. package/dist/services/query-builder/rules/rule-reducer.js +383 -0
  100. package/dist/services/query-builder/rules/rule-relevance.d.ts +43 -0
  101. package/dist/services/query-builder/rules/rule-relevance.js +69 -0
  102. package/dist/services/query-builder/rules/rule-to-sql.d.ts +51 -0
  103. package/dist/services/query-builder/rules/rule-to-sql.js +75 -0
  104. package/dist/services/query-builder/rules/spec/pattern-language.spec.d.ts +1 -0
  105. package/dist/services/query-builder/rules/spec/pattern-language.spec.js +90 -0
  106. package/dist/services/query-builder/rules/spec/rule-parser.spec.d.ts +1 -0
  107. package/dist/services/query-builder/rules/spec/rule-parser.spec.js +150 -0
  108. package/dist/services/query-builder/rules/spec/rule-reducer.spec.d.ts +1 -0
  109. package/dist/services/query-builder/rules/spec/rule-reducer.spec.js +169 -0
  110. package/dist/services/query-builder/rules/spec/rule-relevance.spec.d.ts +1 -0
  111. package/dist/services/query-builder/rules/spec/rule-relevance.spec.js +88 -0
  112. package/dist/services/query-builder/rules/spec/rule-to-sql.spec.d.ts +1 -0
  113. package/dist/services/query-builder/rules/spec/rule-to-sql.spec.js +110 -0
  114. package/dist/services/query-builder/rules/spec/runtime-semantics.spec.d.ts +1 -0
  115. package/dist/services/query-builder/rules/spec/runtime-semantics.spec.js +55 -0
  116. package/dist/services/query-builder/rules/spec/strip-parameters.spec.d.ts +1 -0
  117. package/dist/services/query-builder/rules/spec/strip-parameters.spec.js +85 -0
  118. package/dist/services/query-builder/rules/strip-parameters.d.ts +39 -0
  119. package/dist/services/query-builder/rules/strip-parameters.js +197 -0
  120. package/dist/services/query-builder/sql/expression.d.ts +73 -0
  121. package/dist/services/query-builder/sql/expression.js +133 -0
  122. package/dist/services/query-builder/sql/format.d.ts +14 -0
  123. package/dist/services/query-builder/sql/format.js +48 -0
  124. package/dist/services/query-builder/sql/index.d.ts +4 -0
  125. package/dist/services/query-builder/sql/index.js +4 -0
  126. package/dist/services/query-builder/sql/predicate.d.ts +66 -0
  127. package/dist/services/query-builder/sql/predicate.js +124 -0
  128. package/dist/services/query-builder/sql/regex.d.ts +28 -0
  129. package/dist/services/query-builder/sql/regex.js +81 -0
  130. package/dist/services/query-builder/sql/spec/expression.spec.d.ts +1 -0
  131. package/dist/services/query-builder/sql/spec/expression.spec.js +75 -0
  132. package/dist/services/query-builder/sql/spec/format.spec.d.ts +1 -0
  133. package/dist/services/query-builder/sql/spec/format.spec.js +37 -0
  134. package/dist/services/query-builder/sql/spec/predicate.spec.d.ts +1 -0
  135. package/dist/services/query-builder/sql/spec/predicate.spec.js +78 -0
  136. package/dist/services/query-builder/sql/spec/regex.spec.d.ts +1 -0
  137. package/dist/services/query-builder/sql/spec/regex.spec.js +62 -0
  138. package/oclif.manifest.json +401 -100
  139. package/package.json +11 -2
  140. package/dist/commands/build-parameter-query.d.ts +0 -14
  141. package/dist/commands/build-parameter-query.js +0 -25
  142. package/dist/commands/build-prewarm-query.d.ts +0 -16
  143. package/dist/commands/build-prewarm-query.js +0 -37
  144. package/dist/helpers/build-query-helper.d.ts +0 -32
  145. package/dist/helpers/build-query-helper.js +0 -223
  146. package/dist/helpers/get-parsed-config.d.ts +0 -15
  147. package/dist/helpers/get-parsed-config.js +0 -62
  148. package/dist/services/onboarding/dashboard/parameter-query-builder.d.ts +0 -8
  149. package/dist/services/onboarding/dashboard/parameter-query-builder.js +0 -63
@@ -0,0 +1,75 @@
1
+ import { mkdtempSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { expect } from "chai";
5
+ import { describe, it } from "mocha";
6
+ import { CsvReader } from "./csv-reader.js";
7
+ function write(content) {
8
+ const file = join(mkdtempSync(join(tmpdir(), "sk-csv-")), "urls.csv");
9
+ writeFileSync(file, content, "utf8");
10
+ return file;
11
+ }
12
+ /** The urls of a list, for the cases that say nothing about variations. */
13
+ function read(content) {
14
+ return readList(content).map(({ url }) => url);
15
+ }
16
+ function readList(content) {
17
+ return new CsvReader().readList(write(content));
18
+ }
19
+ describe("CsvReader", () => {
20
+ it("should read a bare list of urls", () => {
21
+ expect(read("https://a.de/\nhttps://b.de/\n")).to.deep.equal([
22
+ "https://a.de/",
23
+ "https://b.de/",
24
+ ]);
25
+ });
26
+ it("should read quoted urls", () => {
27
+ expect(read('"https://a.de/"\n"https://b.de/"')).to.deep.equal([
28
+ "https://a.de/",
29
+ "https://b.de/",
30
+ ]);
31
+ });
32
+ it("should take only the first column", () => {
33
+ // What `sk query prewarm --execute` writes: url and hit count.
34
+ expect(read('"url","hits"\n"https://a.de/",12\n"https://b.de/",3')).to.deep.eq(["https://a.de/", "https://b.de/"]);
35
+ });
36
+ it("should skip a header row", () => {
37
+ expect(read("url\nhttps://a.de/")).to.deep.equal(["https://a.de/"]);
38
+ });
39
+ it("should keep a comma inside a quoted url", () => {
40
+ expect(read('"https://a.de/?list=1,2",7')).to.deep.equal([
41
+ "https://a.de/?list=1,2",
42
+ ]);
43
+ });
44
+ it("should read a doubled quote as one quote", () => {
45
+ expect(read('"https://a.de/?q=""x"""')).to.deep.equal([
46
+ 'https://a.de/?q="x"',
47
+ ]);
48
+ });
49
+ it("should drop empty lines", () => {
50
+ expect(read("https://a.de/\n\n\nhttps://b.de/\n")).to.have.lengthOf(2);
51
+ });
52
+ it("should read the variation of a split list", () => {
53
+ // What `sk query prewarm --device desktop,mobile --execute` writes.
54
+ expect(readList('"url","variation","hits"\n"https://a.de/","mobile",12\n"https://a.de/","desktop",3')).to.deep.equal([
55
+ { url: "https://a.de/", variation: "mobile" },
56
+ { url: "https://a.de/", variation: "desktop" },
57
+ ]);
58
+ });
59
+ it("should not read a hit count as a variation", () => {
60
+ expect(readList('"url","hits"\n"https://a.de/",12')).to.deep.equal([
61
+ { url: "https://a.de/" },
62
+ ]);
63
+ });
64
+ it("should not read a second column of a headerless list", () => {
65
+ expect(readList('"https://a.de/","mobile"')).to.deep.equal([
66
+ { url: "https://a.de/" },
67
+ ]);
68
+ });
69
+ it("should keep a comma inside a quoted url of a split list", () => {
70
+ expect(readList('url,variation\n"https://a.de/?list=1,2",mobile')).to.deep.equal([{ url: "https://a.de/?list=1,2", variation: "mobile" }]);
71
+ });
72
+ it("should read a plain list of values", () => {
73
+ expect(new CsvReader().readValues(write("desktop\nmobile\n"))).to.deep.equal(["desktop", "mobile"]);
74
+ });
75
+ });
@@ -2,3 +2,4 @@ export * from "./pre-warm-service.js";
2
2
  export * from "./pre-warm-factory.js";
3
3
  export * from "./auto-pre-warm-factory.js";
4
4
  export * from "./pre-warm-model.js";
5
+ export * from "./prewarm-targets.js";
@@ -2,3 +2,4 @@ export * from "./pre-warm-service.js";
2
2
  export * from "./pre-warm-factory.js";
3
3
  export * from "./auto-pre-warm-factory.js";
4
4
  export * from "./pre-warm-model.js";
5
+ export * from "./prewarm-targets.js";
@@ -4,8 +4,6 @@ export declare class PreWarmFactory {
4
4
  private context;
5
5
  constructor(context: PreWarmContext);
6
6
  getService(): Promise<PreWarmService>;
7
- private getUrls;
8
- private getVariations;
9
7
  }
10
8
  /**
11
9
  * Resolve the list of variations to prewarm: a `variationPath` CSV takes
@@ -1,5 +1,5 @@
1
1
  import { CliContext, CliServiceFactory } from "../cli/index.js";
2
- import { DEFAULT_VARIATION, PreWarmService, VARIATION_SEPARATOR, } from "./index.js";
2
+ import { DEFAULT_VARIATION, PreWarmService, toPrewarmTargets, VARIATION_SEPARATOR, } from "./index.js";
3
3
  import { AssetApiClient } from "./assets/asset-api-client.js";
4
4
  import { CsvReader } from "./csv-reader.js";
5
5
  import { ConfigApiContext, ConfigApiServiceFactory, } from "../config-api/index.js";
@@ -14,15 +14,12 @@ export class PreWarmFactory {
14
14
  const entityManager = await configApi.client.getEntityManager();
15
15
  const csvReader = new CsvReader();
16
16
  const assetApiClient = new AssetApiClient(entityManager.token, this.context.app, this.context.keepParameterSorting);
17
- const urls = this.getUrls(csvReader);
18
- const variations = this.getVariations(csvReader);
19
- return new PreWarmService(assetApiClient, cli, urls, variations, this.context.verbose);
20
- }
21
- getUrls(csvReader) {
22
- return csvReader.readPath(this.context.path);
23
- }
24
- getVariations(csvReader) {
25
- return resolveVariations(csvReader, this.context.variation, this.context.variationPath);
17
+ const rows = csvReader.readList(this.context.path);
18
+ const named = this.context.variation ?? this.context.variationPath;
19
+ if (named && rows.some((row) => row.variation)) {
20
+ cli.writeWarning(`The list names a variation for every url, so ${named} is not used`);
21
+ }
22
+ return new PreWarmService(assetApiClient, cli, toPrewarmTargets(rows, resolveVariations(csvReader, this.context.variation, this.context.variationPath)), this.context.verbose);
26
23
  }
27
24
  }
28
25
  /**
@@ -37,7 +34,7 @@ export function resolveVariations(csvReader, variation, variationPath) {
37
34
  variations = variation.split(VARIATION_SEPARATOR);
38
35
  }
39
36
  if (variationPath) {
40
- variations = csvReader.readPath(variationPath);
37
+ variations = csvReader.readValues(variationPath);
41
38
  }
42
39
  return variations;
43
40
  }
@@ -8,6 +8,20 @@ export declare class PreWarmContext {
8
8
  readonly verbose: boolean;
9
9
  constructor(app: string, path: string, variation?: string, variationPath?: string, keepParameterSorting?: boolean, quiet?: boolean, verbose?: boolean);
10
10
  }
11
+ /**
12
+ * One url to warm, in one variation. A pre-warm list is a list of these: the
13
+ * variation is a fact about the url, not a dimension crossed over the whole
14
+ * list, so a url seen only on phones is warmed only as `mobile`.
15
+ */
16
+ export interface PrewarmTarget {
17
+ readonly url: string;
18
+ readonly variation: string;
19
+ }
20
+ /** A row of a pre-warm list, which may or may not name a variation yet. */
21
+ export interface PrewarmRow {
22
+ readonly url: string;
23
+ readonly variation?: string;
24
+ }
11
25
  export interface AssetItemInterface {
12
26
  done(): void;
13
27
  error?: Error;
@@ -36,4 +50,3 @@ export declare const TIMING_HEADER_KEY = "server-timing";
36
50
  export declare const PROGRESS_BAR_FORMAT = "[{bar}] {percentage}% | ETA: {eta_formatted}s | {value}/{total} | error:{errors} | rate: {rate} | retry: {retry} | batchDur: {batchDuration}";
37
51
  export declare const VARIATION_SEPARATOR = ",";
38
52
  export declare const DEFAULT_VARIATION: string[];
39
- export declare const DEFAULT_VARIATIONS: string[];
@@ -36,4 +36,3 @@ export const TIMING_HEADER_KEY = "server-timing";
36
36
  export const PROGRESS_BAR_FORMAT = "[{bar}] {percentage}% | ETA: {eta_formatted}s | {value}/{total} | error:{errors} | rate: {rate} | retry: {retry} | batchDur: {batchDuration}";
37
37
  export const VARIATION_SEPARATOR = ",";
38
38
  export const DEFAULT_VARIATION = ["default"];
39
- export const DEFAULT_VARIATIONS = ["mobile", "tablet", "tv"];
@@ -1,19 +1,24 @@
1
1
  import { CliServiceInterface } from "../cli/index.js";
2
- import { AssetItemInterface } from "./index.js";
2
+ import { AssetItemInterface, PrewarmTarget } from "./index.js";
3
3
  import { AssetApiClient } from "./assets/asset-api-client.js";
4
4
  export declare class PreWarmService {
5
5
  private assetClient;
6
6
  private cli;
7
- private urls;
8
- private variations;
7
+ private targets;
9
8
  private verbose;
10
9
  private requestsSend;
11
- constructor(assetClient: AssetApiClient, cli: CliServiceInterface, urls: string[], variations: string[], verbose: boolean);
10
+ constructor(assetClient: AssetApiClient, cli: CliServiceInterface, targets: readonly PrewarmTarget[], verbose: boolean);
12
11
  handleAsset(assetClient: AssetApiClient, entry: AssetItemInterface): Promise<void>;
13
12
  run(): Promise<void>;
14
13
  private fetchAssetList;
15
14
  private handleProgress;
16
15
  private fetchUrls;
16
+ /**
17
+ * The request list, in the order the targets were given. The order is the
18
+ * caller's to decide — a list built by device interleaves the variations, so
19
+ * that a run cut short has warmed the busiest entries of each rather than
20
+ * all of one and none of the next.
21
+ */
17
22
  private generateAssetRequestList;
18
23
  private printSummary;
19
24
  private startProgressBar;
@@ -5,15 +5,13 @@ import { AssetList } from "./assets/asset-list.js";
5
5
  export class PreWarmService {
6
6
  assetClient;
7
7
  cli;
8
- urls;
9
- variations;
8
+ targets;
10
9
  verbose;
11
10
  requestsSend = 0;
12
- constructor(assetClient, cli, urls, variations, verbose) {
11
+ constructor(assetClient, cli, targets, verbose) {
13
12
  this.assetClient = assetClient;
14
13
  this.cli = cli;
15
- this.urls = urls;
16
- this.variations = variations;
14
+ this.targets = targets;
17
15
  this.verbose = verbose;
18
16
  }
19
17
  async handleAsset(assetClient, entry) {
@@ -49,7 +47,7 @@ export class PreWarmService {
49
47
  entry.setError(new Error(`status: ${response.status}, sk-error: ${serverTimingErrorCode}`));
50
48
  }
51
49
  async run() {
52
- const assetList = this.generateAssetRequestList(this.urls);
50
+ const assetList = this.generateAssetRequestList(this.targets);
53
51
  await this.fetchUrls(assetList);
54
52
  await this.printSummary(assetList);
55
53
  if (assetList.getRetryCount() === 0) {
@@ -58,10 +56,12 @@ export class PreWarmService {
58
56
  this.cli.write("Retry rateLimited requests.");
59
57
  // reset for minimal output
60
58
  this.requestsSend = 0;
61
- const urlsToRetry = assetList
59
+ // Rebuilt from the items themselves: they already carry the variation they
60
+ // were rate-limited in, and crossing their urls with anything else would
61
+ // both re-expand the list and lose that pairing.
62
+ const retryList = this.generateAssetRequestList(assetList
62
63
  .getByStatus(ItemStatus.RETRY)
63
- .map((entry) => entry.url);
64
- const retryList = this.generateAssetRequestList(urlsToRetry);
64
+ .map(({ url, variation }) => ({ url, variation })));
65
65
  this.assetClient.setIsRetry();
66
66
  await this.fetchUrls(retryList);
67
67
  await this.printSummary(retryList);
@@ -109,12 +109,16 @@ export class PreWarmService {
109
109
  await this.fetchAssetList(assetList);
110
110
  this.stopProgressBar();
111
111
  }
112
- generateAssetRequestList(urls) {
112
+ /**
113
+ * The request list, in the order the targets were given. The order is the
114
+ * caller's to decide — a list built by device interleaves the variations, so
115
+ * that a run cut short has warmed the busiest entries of each rather than
116
+ * all of one and none of the next.
117
+ */
118
+ generateAssetRequestList(targets) {
113
119
  const assetList = new AssetList();
114
- for (const variation of this.variations) {
115
- for (const url of urls) {
116
- assetList.addItem(new AssetItem(url, variation));
117
- }
120
+ for (const { url, variation } of targets) {
121
+ assetList.addItem(new AssetItem(url, variation));
118
122
  }
119
123
  return assetList;
120
124
  }
@@ -0,0 +1,12 @@
1
+ import { PrewarmRow, PrewarmTarget } from "./pre-warm-model.js";
2
+ /**
3
+ * The (url, variation) pairs to warm.
4
+ *
5
+ * A list that already names a variation per url is taken as it stands — its
6
+ * order is the one whoever built it chose, and every pair in it was actually
7
+ * requested. A list of bare urls is crossed with the variations the caller
8
+ * named, url-major: each url is warmed in every variation before the next url
9
+ * is touched, so a run cut short has warmed whole pages rather than one
10
+ * variation of all of them.
11
+ */
12
+ export declare function toPrewarmTargets(rows: readonly PrewarmRow[], variations: readonly string[]): PrewarmTarget[];
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The (url, variation) pairs to warm.
3
+ *
4
+ * A list that already names a variation per url is taken as it stands — its
5
+ * order is the one whoever built it chose, and every pair in it was actually
6
+ * requested. A list of bare urls is crossed with the variations the caller
7
+ * named, url-major: each url is warmed in every variation before the next url
8
+ * is touched, so a run cut short has warmed whole pages rather than one
9
+ * variation of all of them.
10
+ */
11
+ export function toPrewarmTargets(rows, variations) {
12
+ if (rows.some((row) => row.variation)) {
13
+ return rows
14
+ .filter((row) => row.variation)
15
+ .map(({ url, variation }) => ({ url, variation }));
16
+ }
17
+ return rows.flatMap(({ url }) => variations.map((variation) => ({ url, variation })));
18
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { expect } from "chai";
2
+ import { describe, it } from "mocha";
3
+ import { toPrewarmTargets } from "./prewarm-targets.js";
4
+ describe("toPrewarmTargets", () => {
5
+ it("should keep a paired list in the order it was given", () => {
6
+ // The order is the query's interleaving of the variations by hit count:
7
+ // grouping or sorting it here would undo the point of the split.
8
+ const rows = [
9
+ { url: "https://a.de/", variation: "mobile" },
10
+ { url: "https://b.de/", variation: "desktop" },
11
+ { url: "https://a.de/", variation: "desktop" },
12
+ ];
13
+ expect(toPrewarmTargets(rows, ["default"])).to.deep.equal(rows);
14
+ });
15
+ it("should not cross a paired list with the variations", () => {
16
+ expect(toPrewarmTargets([{ url: "https://a.de/", variation: "mobile" }], ["men", "women"])).to.have.lengthOf(1);
17
+ });
18
+ it("should cross a bare list url-major", () => {
19
+ expect(toPrewarmTargets([{ url: "https://a.de/" }, { url: "https://b.de/" }], ["desktop", "mobile"])).to.deep.equal([
20
+ { url: "https://a.de/", variation: "desktop" },
21
+ { url: "https://a.de/", variation: "mobile" },
22
+ { url: "https://b.de/", variation: "desktop" },
23
+ { url: "https://b.de/", variation: "mobile" },
24
+ ]);
25
+ });
26
+ it("should warm nothing when there is no variation to warm in", () => {
27
+ expect(toPrewarmTargets([{ url: "https://a.de/" }], [])).to.deep.equal([]);
28
+ });
29
+ });
@@ -0,0 +1,11 @@
1
+ import ApplicationError from "../../error-handling/error/application-error.js";
2
+ /**
3
+ * A `--match` pattern the config can never let through.
4
+ *
5
+ * The query would be valid and return nothing, after a full Athena scan. This
6
+ * stops before the query is sent.
7
+ */
8
+ export declare class MatchMatchesNothingError extends ApplicationError {
9
+ readonly code = "MATCH_MATCHES_NOTHING";
10
+ constructor(reason: string);
11
+ }
@@ -0,0 +1,17 @@
1
+ import ApplicationError from "../../error-handling/error/application-error.js";
2
+ /**
3
+ * A `--match` pattern the config can never let through.
4
+ *
5
+ * The query would be valid and return nothing, after a full Athena scan. This
6
+ * stops before the query is sent.
7
+ */
8
+ export class MatchMatchesNothingError extends ApplicationError {
9
+ code = "MATCH_MATCHES_NOTHING";
10
+ constructor(reason) {
11
+ super(`--match ${reason}`);
12
+ this.suggestions = [
13
+ "the pattern is matched against the url without its scheme, e.g. www.shop.de/p/1",
14
+ "check the pattern against the config's enabledSites, disabledSites and blacklist",
15
+ ];
16
+ }
17
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The device classes a pre-warm list can be split by.
3
+ *
4
+ * These are the variation names Speed Kit computes from the user agent, not
5
+ * the values `rum.pi` records — see {@link DEVICE_COLUMN_VALUES} for the
6
+ * translation. `Robot` is not one of them: a query never counts bots.
7
+ *
8
+ * A custom variation (`desktop-pdp-a`, `abtest_b`, `men`) can never be derived
9
+ * from a device, so it cannot be a value here — `--variation` puts those into
10
+ * the list by hand.
11
+ */
12
+ export declare const DEVICE_VARIATIONS: readonly ["desktop", "mobile", "tablet"];
13
+ export type DeviceVariation = (typeof DEVICE_VARIATIONS)[number];
14
+ /** How `rum.pi` spells each of them in its `device` column. */
15
+ export declare const DEVICE_COLUMN_VALUES: Record<DeviceVariation, string>;
16
+ /**
17
+ * The requested devices in the canonical order, without duplicates — so the
18
+ * same request always produces the same SQL, whatever order the flag was
19
+ * given in.
20
+ */
21
+ export declare function normalizeDevices(devices: readonly string[] | undefined): DeviceVariation[];
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The device classes a pre-warm list can be split by.
3
+ *
4
+ * These are the variation names Speed Kit computes from the user agent, not
5
+ * the values `rum.pi` records — see {@link DEVICE_COLUMN_VALUES} for the
6
+ * translation. `Robot` is not one of them: a query never counts bots.
7
+ *
8
+ * A custom variation (`desktop-pdp-a`, `abtest_b`, `men`) can never be derived
9
+ * from a device, so it cannot be a value here — `--variation` puts those into
10
+ * the list by hand.
11
+ */
12
+ export const DEVICE_VARIATIONS = ["desktop", "mobile", "tablet"];
13
+ /** How `rum.pi` spells each of them in its `device` column. */
14
+ export const DEVICE_COLUMN_VALUES = {
15
+ desktop: "Desktop",
16
+ mobile: "Mobile",
17
+ tablet: "Tablet",
18
+ };
19
+ /**
20
+ * The requested devices in the canonical order, without duplicates — so the
21
+ * same request always produces the same SQL, whatever order the flag was
22
+ * given in.
23
+ */
24
+ export function normalizeDevices(devices) {
25
+ return DEVICE_VARIATIONS.filter((device) => devices?.includes(device));
26
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./device.js";
2
+ export * from "./page-filter.js";
3
+ export * from "./parameter-query.js";
4
+ export * from "./pops-query.js";
5
+ export * from "./prewarm-query.js";
6
+ export * from "./query-model.js";
@@ -0,0 +1,6 @@
1
+ export * from "./device.js";
2
+ export * from "./page-filter.js";
3
+ export * from "./parameter-query.js";
4
+ export * from "./pops-query.js";
5
+ export * from "./prewarm-query.js";
6
+ export * from "./query-model.js";
@@ -0,0 +1,39 @@
1
+ import { Predicate } from "../sql/index.js";
2
+ import { RuleNote, RuleSubjectExpressions } from "../rules/index.js";
3
+ import { QueryInput } from "./query-model.js";
4
+ export interface RuleFilter {
5
+ readonly approximated: RuleNote[];
6
+ readonly dropped: RuleNote[];
7
+ /** Findings about the config itself, worth reporting on their own. */
8
+ readonly findings: RuleNote[];
9
+ readonly predicate: Predicate;
10
+ /** Rules left out because another rule already decides their pages. */
11
+ readonly reduced: RuleNote[];
12
+ readonly warnings: string[];
13
+ }
14
+ /**
15
+ * Which page impressions a query counts at all: the app's own traffic in the
16
+ * requested window, without the noise the caller asked to leave out.
17
+ *
18
+ * Bots are always excluded — a crawler requests pages no real user asks for,
19
+ * and neither pre-warming nor a parameter analysis should be driven by them.
20
+ * Naming the device classes to keep subsumes that: `Robot` is not one of them,
21
+ * and `in` leaves out a null device the same way `<> 'Robot'` does.
22
+ */
23
+ export declare function buildRowFilter(input: QueryInput): Predicate;
24
+ /**
25
+ * Which of those pages Speed Kit accelerates.
26
+ *
27
+ * The rule sets are combined the way the runtime evaluates them:
28
+ * `enabledSites` decides first and an empty set means every page, then
29
+ * `disabledSites` takes pages away; `whitelist` and `blacklist` do the same
30
+ * for the document as a resource. `disabledScopes` takes away whatever the
31
+ * service worker never claims in the first place.
32
+ *
33
+ * Before that, the sets are ruled against each other. Each set loses the rules
34
+ * a sibling already covers, and the two excluding sets additionally lose every
35
+ * rule that excludes pages the including sets never let through — which is
36
+ * most of a real blacklist, since it guards paths `enabledSites` never
37
+ * allowed. What remains is the part of the config the data can actually see.
38
+ */
39
+ export declare function buildRuleFilter(input: QueryInput, subjects: RuleSubjectExpressions): RuleFilter;
@@ -0,0 +1,135 @@
1
+ import { and, call, column, equals, FALSE, greaterOrEqual, inValues, leaf, literal, not, notEquals, TRUE, } from "../sql/index.js";
2
+ import { absorbRules, asPathWithSearchRule, buildMatchRule, dropUnreachableRules, findShadowedRules, keepRelevantRules, reductionBudget, translateRules, } from "../rules/index.js";
3
+ import { MatchMatchesNothingError } from "../error/match-matches-nothing-error.js";
4
+ import { DEVICE_COLUMN_VALUES, normalizeDevices } from "./device.js";
5
+ import { sinceDate } from "./query-model.js";
6
+ /** The navigation types that are a full document load. */
7
+ const HARD_NAVIGATION_TYPES = ["NAVIGATE", "RELOAD", "BACK_FORWARD"];
8
+ /**
9
+ * Which page impressions a query counts at all: the app's own traffic in the
10
+ * requested window, without the noise the caller asked to leave out.
11
+ *
12
+ * Bots are always excluded — a crawler requests pages no real user asks for,
13
+ * and neither pre-warming nor a parameter analysis should be driven by them.
14
+ * Naming the device classes to keep subsumes that: `Robot` is not one of them,
15
+ * and `in` leaves out a null device the same way `<> 'Robot'` does.
16
+ */
17
+ export function buildRowFilter(input) {
18
+ const devices = normalizeDevices(input.devices);
19
+ return and(equals(column("app"), literal(input.app)), greaterOrEqual(column("date"), literal(sinceDate(input.days, input.now))), devices.length > 0
20
+ ? inValues(column("device"), devices.map((device) => DEVICE_COLUMN_VALUES[device]))
21
+ : notEquals(column("device"), literal("Robot")), input.hardNavigationsOnly
22
+ ? inValues(column("navigationtype"), HARD_NAVIGATION_TYPES)
23
+ : TRUE, input.excludeSuspicious ? not(isSet("suspicious")) : TRUE, input.excludeNoindex ? not(isSet("noindex")) : TRUE);
24
+ }
25
+ /**
26
+ * Which of those pages Speed Kit accelerates.
27
+ *
28
+ * The rule sets are combined the way the runtime evaluates them:
29
+ * `enabledSites` decides first and an empty set means every page, then
30
+ * `disabledSites` takes pages away; `whitelist` and `blacklist` do the same
31
+ * for the document as a resource. `disabledScopes` takes away whatever the
32
+ * service worker never claims in the first place.
33
+ *
34
+ * Before that, the sets are ruled against each other. Each set loses the rules
35
+ * a sibling already covers, and the two excluding sets additionally lose every
36
+ * rule that excludes pages the including sets never let through — which is
37
+ * most of a real blacklist, since it guards paths `enabledSites` never
38
+ * allowed. What remains is the part of the config the data can actually see.
39
+ */
40
+ export function buildRuleFilter(input, subjects) {
41
+ const { ruleSets } = input;
42
+ const approximated = [];
43
+ const dropped = [];
44
+ const reduced = [];
45
+ const warnings = [];
46
+ // One budget for the whole build, so the reductions cannot add up.
47
+ const budget = reductionBudget();
48
+ /** The rules of a set that can apply to a page impression at all. */
49
+ const relevant = (rules) => {
50
+ const result = keepRelevantRules(rules, { hosts: input.hosts });
51
+ approximated.push(...result.approximated);
52
+ dropped.push(...result.dropped);
53
+ return result.rules;
54
+ };
55
+ /** What a sibling rule already covers, dropped. */
56
+ const absorb = (rules) => {
57
+ const absorbed = absorbRules(rules, budget);
58
+ reduced.push(...absorbed.notes);
59
+ return absorbed.rules;
60
+ };
61
+ const translate = (rules, whenEmpty) => {
62
+ const translation = translateRules(rules, subjects, { whenEmpty });
63
+ warnings.push(...translation.warnings);
64
+ return translation.predicate;
65
+ };
66
+ const whitelist = absorb(relevant(ruleSets.whitelist));
67
+ const enabledSites = absorb(relevant(ruleSets.enabledSites));
68
+ // An empty including set lets everything through, so it constrains nothing.
69
+ const reachable = [enabledSites, whitelist].filter((set) => set.length > 0);
70
+ /**
71
+ * Reachability first, absorption second.
72
+ *
73
+ * Both shorten the query, but dropping what can never fire is the reduction
74
+ * that carries a real config — most of a blacklist guards pages
75
+ * `enabledSites` never allowed — while absorbing a sibling is cosmetic and
76
+ * costs a comparison for every pair of entries. Spending the budget in that
77
+ * order means a config that runs out of it still got the reduction worth
78
+ * having, and absorption then works on fewer entries.
79
+ */
80
+ const exclude = (rules) => {
81
+ const reachableOnly = dropUnreachableRules(rules, reachable, budget);
82
+ reduced.push(...reachableOnly.notes);
83
+ return absorb(reachableOnly.rules);
84
+ };
85
+ const excluding = [
86
+ relevant(ruleSets.blacklist),
87
+ relevant(ruleSets.disabledSites),
88
+ relevant(ruleSets.disabledScopes),
89
+ ];
90
+ /**
91
+ * The narrowing the caller asked for, held against what the config takes
92
+ * away.
93
+ *
94
+ * A pattern the excluding rules already cover would build a valid query,
95
+ * scan the whole window and return nothing, so it is refused before the
96
+ * query is handed over. The rules are the ones the query frame leaves, not
97
+ * the ones the reduction leaves: an entry dropped below because
98
+ * `enabledSites` never lets its pages through still says that a pattern
99
+ * aimed at them has nothing to warm.
100
+ *
101
+ * The comparison gets a budget of its own. It costs one comparison per
102
+ * excluding rule, and sharing the build's budget would mean a config large
103
+ * enough to spend it first is silently not checked at all.
104
+ */
105
+ const match = buildMatchRule(input.match);
106
+ if (match) {
107
+ const [shadowed] = findShadowedRules([match], excluding.flat().map(asPathWithSearchRule).filter(Boolean), reductionBudget());
108
+ if (shadowed) {
109
+ throw new MatchMatchesNothingError(shadowed.reason);
110
+ }
111
+ }
112
+ const [blacklist, disabledSites, disabledScopes] = excluding.map(exclude);
113
+ const predicate = and(translate(whitelist, TRUE), not(translate(blacklist, FALSE)), translate(enabledSites, TRUE), not(translate(disabledSites, FALSE)),
114
+ // A page inside a disabled scope is served by an empty service worker, so
115
+ // Speed Kit never sees it at all.
116
+ not(translate(disabledScopes, FALSE)),
117
+ // Last, so the query reads as the config's filter with the caller's
118
+ // narrowing applied to it.
119
+ match ? translate([match], TRUE) : TRUE);
120
+ return {
121
+ approximated,
122
+ dropped,
123
+ findings: findShadowedRules(enabledSites, [...disabledScopes, ...disabledSites, ...blacklist], budget),
124
+ predicate,
125
+ reduced,
126
+ warnings,
127
+ };
128
+ }
129
+ /**
130
+ * A boolean column that may be null. `not column` would drop the null rows
131
+ * along with the true ones, which is not what excluding a flag means.
132
+ */
133
+ function isSet(name) {
134
+ return leaf(call("coalesce", column(name), literal(false)), true);
135
+ }
@@ -0,0 +1,30 @@
1
+ import { RuleSubjectExpressions } from "../rules/index.js";
2
+ import { QueryInput } from "./query-model.js";
3
+ /** The relation every query reads from. */
4
+ export declare const PAGE_IMPRESSIONS = "live.rum.pi";
5
+ /** The name of the common table expression the queries select from. */
6
+ export declare const PAGE_SOURCE = "pages";
7
+ export interface PageSource {
8
+ /** The `pages as (…)` common table expression, without the `with`. */
9
+ readonly cte: string;
10
+ /** The expressions rule conditions are matched against. */
11
+ readonly subjects: RuleSubjectExpressions;
12
+ }
13
+ export interface PageSourceOptions {
14
+ /**
15
+ * Columns of the relation to carry through, beyond `url`. Each query asks
16
+ * for what it reads: `createdat` is only needed where example urls are
17
+ * picked, `responsecause` only where 404s are filtered out.
18
+ */
19
+ readonly columns?: readonly string[];
20
+ }
21
+ /**
22
+ * The rows a query works on: the app's page impressions in the requested
23
+ * window, with everything a rule matches against projected once.
24
+ *
25
+ * Deriving host and path per condition would repeat the same
26
+ * `url_extract_path(url)` a dozen times in a single `where` clause. Naming
27
+ * them here keeps the query readable — which matters, because these queries
28
+ * are read and adjusted by hand in the Athena console.
29
+ */
30
+ export declare function buildPageSource(input: QueryInput, options?: PageSourceOptions): PageSource;