@antelopejs/dms-frontend 0.6.4 → 0.7.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.
@@ -114,11 +114,21 @@ function writePublicEndpoints(workspaceDir, layers) {
114
114
  };
115
115
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(workspaceDir, config_1.PUBLIC_ENDPOINTS_FILE), `${JSON.stringify(declared, null, 2)}\n`);
116
116
  }
117
+ function holdsValue(value) {
118
+ if (typeof value === "string")
119
+ return value !== "";
120
+ if (value !== null && typeof value === "object")
121
+ return Object.values(value).some(holdsValue);
122
+ return false;
123
+ }
117
124
  function writeServerOptions(workspaceDir, layers) {
118
125
  const modules = Object.fromEntries(layers.flatMap((layer) => layer.privateOptions
119
126
  ? [[(0, layers_1.getLayerSafeName)(layer), layer.privateOptions]]
120
127
  : []));
121
- (0, config_1.writeSecretBearingFile)((0, node_path_1.join)(workspaceDir, config_1.SERVER_OPTIONS_FILE), `${JSON.stringify({ modules, environment: manifest_secrets_1.MANIFEST_SECRET_ENV }, null, 2)}\n`);
128
+ const write = Object.values(modules).some(holdsValue)
129
+ ? config_1.writeSecretBearingFile
130
+ : config_1.writeShareableFile;
131
+ write((0, node_path_1.join)(workspaceDir, config_1.SERVER_OPTIONS_FILE), `${JSON.stringify({ modules, environment: manifest_secrets_1.MANIFEST_SECRET_ENV }, null, 2)}\n`);
122
132
  }
123
133
  function writeFrontendModuleRegistry(workspaceDir, layers) {
124
134
  const registry = (0, derived_outputs_1.createFrontendModuleRegistry)(workspaceDir, layers);
@@ -14,3 +14,14 @@ export interface ModuleSetFollowOptions {
14
14
  export declare function moduleSetFingerprint(modules: readonly ModuleSetMember[]): string;
15
15
  export declare function followModuleSet(options: ModuleSetFollowOptions): () => void;
16
16
  export declare function reportFrontendReady(backendUrl: string, moduleNames: readonly string[], bootstrapSecret?: string): Promise<boolean>;
17
+ export type BuildDrift = {
18
+ status: "matches";
19
+ served: string;
20
+ } | {
21
+ status: "drifted";
22
+ served: string;
23
+ } | {
24
+ status: "unknown";
25
+ reason: string;
26
+ };
27
+ export declare function compareWithBackend(backendUrl: string, builtDigest: string, digestOf: (modules: readonly ManifestModule[]) => string, bootstrapSecret?: string): Promise<BuildDrift>;
@@ -4,6 +4,7 @@ exports.MODULE_SET_POLL_INTERVAL_MS = void 0;
4
4
  exports.moduleSetFingerprint = moduleSetFingerprint;
5
5
  exports.followModuleSet = followModuleSet;
6
6
  exports.reportFrontendReady = reportFrontendReady;
7
+ exports.compareWithBackend = compareWithBackend;
7
8
  const manifest_1 = require("./manifest");
8
9
  const workspace_1 = require("./workspace");
9
10
  exports.MODULE_SET_POLL_INTERVAL_MS = 3000;
@@ -82,3 +83,20 @@ async function reportFrontendReady(backendUrl, moduleNames, bootstrapSecret) {
82
83
  return false;
83
84
  }
84
85
  }
86
+ const DRIFT_CHECK_TIMEOUT_MS = 10_000;
87
+ async function compareWithBackend(backendUrl, builtDigest, digestOf, bootstrapSecret) {
88
+ try {
89
+ const manifest = await Promise.race([
90
+ (0, manifest_1.fetchManifest)(backendUrl, undefined, bootstrapSecret),
91
+ new Promise((_resolve, reject) => setTimeout(() => reject(new Error("the backend did not answer in time")), DRIFT_CHECK_TIMEOUT_MS).unref()),
92
+ ]);
93
+ const served = digestOf(manifest.modules);
94
+ return { status: served === builtDigest ? "matches" : "drifted", served };
95
+ }
96
+ catch (error) {
97
+ return {
98
+ status: "unknown",
99
+ reason: error instanceof Error ? error.message : String(error),
100
+ };
101
+ }
102
+ }
@@ -0,0 +1,9 @@
1
+ import type { FrontendModuleOptions, ManifestModule } from "./workspace";
2
+ export declare function withoutPrivateValues(modules: readonly ManifestModule[]): ManifestModule[];
3
+ export declare const PRIVATE_OPTIONS_VARIABLE = "DMS_PRIVATE_OPTIONS";
4
+ export interface UnfilledPrivateValue {
5
+ module: string;
6
+ path: string[];
7
+ }
8
+ export declare function parsePrivateOptions(raw: string | undefined): Record<string, FrontendModuleOptions>;
9
+ export declare function unfilledPrivateValues(modules: Readonly<Record<string, FrontendModuleOptions>>, given: Readonly<Record<string, FrontendModuleOptions>>, covered: readonly (readonly string[])[]): UnfilledPrivateValue[];
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PRIVATE_OPTIONS_VARIABLE = void 0;
4
+ exports.withoutPrivateValues = withoutPrivateValues;
5
+ exports.parsePrivateOptions = parsePrivateOptions;
6
+ exports.unfilledPrivateValues = unfilledPrivateValues;
7
+ const config_1 = require("./config");
8
+ function emptied(value) {
9
+ if (typeof value === "string")
10
+ return "";
11
+ if (Array.isArray(value))
12
+ return value.map(emptied);
13
+ if (value !== null && typeof value === "object")
14
+ return emptiedOptions(value);
15
+ return value;
16
+ }
17
+ function emptiedOptions(options) {
18
+ return Object.fromEntries(Object.entries(options).map(([key, value]) => [key, emptied(value)]));
19
+ }
20
+ function withoutPrivateValues(modules) {
21
+ return modules.map((module) => module.privateOptions
22
+ ? { ...module, privateOptions: emptiedOptions(module.privateOptions) }
23
+ : { ...module });
24
+ }
25
+ exports.PRIVATE_OPTIONS_VARIABLE = "DMS_PRIVATE_OPTIONS";
26
+ function isOptions(value) {
27
+ return typeof value === "object" && value !== null && !Array.isArray(value);
28
+ }
29
+ function parsePrivateOptions(raw) {
30
+ if (!raw?.trim())
31
+ return {};
32
+ let given;
33
+ try {
34
+ given = JSON.parse(raw);
35
+ }
36
+ catch {
37
+ given = undefined;
38
+ }
39
+ if (!isOptions(given) || !Object.values(given).every(isOptions)) {
40
+ throw new config_1.UsageError({
41
+ title: `${exports.PRIVATE_OPTIONS_VARIABLE} is not a JSON object of options by module id`,
42
+ fixes: [
43
+ `Set it like ${exports.PRIVATE_OPTIONS_VARIABLE}='{"<module id>":{"<option>":"<value>"}}', with the ids of generated-server-options.json`,
44
+ ],
45
+ });
46
+ }
47
+ return given;
48
+ }
49
+ function readPath(options, path) {
50
+ let value = options;
51
+ for (const key of path) {
52
+ if (typeof value !== "object" || value === null)
53
+ return undefined;
54
+ if (!Object.prototype.hasOwnProperty.call(value, key))
55
+ return undefined;
56
+ value = value[key];
57
+ }
58
+ return value;
59
+ }
60
+ function* emptyStrings(value, path = []) {
61
+ if (value === "")
62
+ yield path;
63
+ else if (typeof value === "object" && value !== null)
64
+ for (const [key, child] of Object.entries(value))
65
+ yield* emptyStrings(child, [...path, key]);
66
+ }
67
+ function unfilledPrivateValues(modules, given, covered) {
68
+ const isCovered = (path) => covered.some((other) => other.length === path.length &&
69
+ other.every((key, index) => key === path[index]));
70
+ return Object.entries(modules).flatMap(([module, options]) => [...emptyStrings(options)]
71
+ .filter((path) => path.length > 0 && !isCovered(path))
72
+ .filter((path) => {
73
+ const value = readPath(given[module], path);
74
+ return typeof value !== "string" || value === "";
75
+ })
76
+ .map((path) => ({ module, path })));
77
+ }
@@ -0,0 +1,25 @@
1
+ import type { Manifest, ManifestModule } from "./workspace";
2
+ export declare const SNAPSHOT_FILE = "snapshot.json";
3
+ export declare const SNAPSHOT_LAYERS_DIR = "layers";
4
+ export declare const BUILD_ORIGIN_FILE = ".build-origin.json";
5
+ declare const SNAPSHOT_FORMAT = 1;
6
+ export interface FrontendSnapshot {
7
+ format: typeof SNAPSHOT_FORMAT;
8
+ backendUrl: string;
9
+ takenAt: string;
10
+ digest: string;
11
+ manifest: Manifest;
12
+ }
13
+ export interface BuildOrigin {
14
+ source: "snapshot" | "backend";
15
+ backendUrl: string;
16
+ takenAt: string;
17
+ digest: string;
18
+ authenticated: boolean;
19
+ }
20
+ export declare function manifestDigest(modules: readonly ManifestModule[]): string;
21
+ export declare function writeSnapshot(dir: string, backendUrl: string, manifest: Manifest, layersSource: string): FrontendSnapshot;
22
+ export declare function readSnapshot(dir: string): FrontendSnapshot;
23
+ export declare function writeBuildOrigin(workspaceDir: string, origin: BuildOrigin): void;
24
+ export declare function readBuildOrigin(workspaceDir: string): BuildOrigin | undefined;
25
+ export {};
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BUILD_ORIGIN_FILE = exports.SNAPSHOT_LAYERS_DIR = exports.SNAPSHOT_FILE = void 0;
4
+ exports.manifestDigest = manifestDigest;
5
+ exports.writeSnapshot = writeSnapshot;
6
+ exports.readSnapshot = readSnapshot;
7
+ exports.writeBuildOrigin = writeBuildOrigin;
8
+ exports.readBuildOrigin = readBuildOrigin;
9
+ const node_crypto_1 = require("node:crypto");
10
+ const node_fs_1 = require("node:fs");
11
+ const node_path_1 = require("node:path");
12
+ const cli_1 = require("@antelopejs/core/cli");
13
+ const config_1 = require("./config");
14
+ const module_set_1 = require("./module-set");
15
+ const private_values_1 = require("./private-values");
16
+ exports.SNAPSHOT_FILE = "snapshot.json";
17
+ exports.SNAPSHOT_LAYERS_DIR = "layers";
18
+ exports.BUILD_ORIGIN_FILE = ".build-origin.json";
19
+ const SNAPSHOT_FORMAT = 1;
20
+ const DIGEST_LENGTH = 16;
21
+ function manifestDigest(modules) {
22
+ return (0, node_crypto_1.createHash)("sha256")
23
+ .update((0, module_set_1.moduleSetFingerprint)(modules.map(({ path: _path, ...module }) => module)))
24
+ .digest("hex")
25
+ .slice(0, DIGEST_LENGTH);
26
+ }
27
+ function writeSnapshot(dir, backendUrl, manifest, layersSource) {
28
+ const modules = (0, private_values_1.withoutPrivateValues)(manifest.modules).map(({ path: _path, ...module }) => module);
29
+ const snapshot = {
30
+ format: SNAPSHOT_FORMAT,
31
+ backendUrl: (0, config_1.canonicalizeBackendUrl)(backendUrl),
32
+ takenAt: new Date().toISOString(),
33
+ digest: manifestDigest(modules),
34
+ manifest: { pack: manifest.pack, modules },
35
+ };
36
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
37
+ const layers = (0, node_path_1.join)(dir, exports.SNAPSHOT_LAYERS_DIR);
38
+ (0, node_fs_1.rmSync)(layers, { recursive: true, force: true });
39
+ (0, node_fs_1.cpSync)(layersSource, layers, { recursive: true });
40
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, exports.SNAPSHOT_FILE), `${JSON.stringify(snapshot, null, 2)}\n`);
41
+ return snapshot;
42
+ }
43
+ function readSnapshot(dir) {
44
+ const file = (0, node_path_1.join)(dir, exports.SNAPSHOT_FILE);
45
+ const refresh = "Take one: ajs dms snapshot -b <backend url> -o <dir>";
46
+ if (!(0, node_fs_1.existsSync)(file)) {
47
+ throw new cli_1.CliError({
48
+ title: `No snapshot in ${dir}`,
49
+ reason: `${exports.SNAPSHOT_FILE} is missing.`,
50
+ fixes: [refresh],
51
+ });
52
+ }
53
+ let snapshot;
54
+ try {
55
+ snapshot = JSON.parse((0, node_fs_1.readFileSync)(file, "utf8"));
56
+ }
57
+ catch (error) {
58
+ throw new cli_1.CliError({ title: `${file} is not valid JSON`, fixes: [refresh] }, { cause: error });
59
+ }
60
+ if (snapshot.format !== SNAPSHOT_FORMAT ||
61
+ !Array.isArray(snapshot.manifest?.modules) ||
62
+ typeof snapshot.backendUrl !== "string") {
63
+ throw new cli_1.CliError({
64
+ title: `${file} is not a snapshot this loader can build from`,
65
+ reason: `It needs format ${SNAPSHOT_FORMAT}, a backend URL and a manifest.`,
66
+ fixes: [refresh],
67
+ });
68
+ }
69
+ if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(dir, exports.SNAPSHOT_LAYERS_DIR))) {
70
+ throw new cli_1.CliError({
71
+ title: `The snapshot in ${dir} has no ${exports.SNAPSHOT_LAYERS_DIR} directory`,
72
+ fixes: [refresh],
73
+ });
74
+ }
75
+ return snapshot;
76
+ }
77
+ function writeBuildOrigin(workspaceDir, origin) {
78
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(workspaceDir, exports.BUILD_ORIGIN_FILE), `${JSON.stringify(origin, null, 2)}\n`);
79
+ }
80
+ function readBuildOrigin(workspaceDir) {
81
+ try {
82
+ const origin = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(workspaceDir, exports.BUILD_ORIGIN_FILE), "utf8"));
83
+ return typeof origin.digest === "string"
84
+ ? origin
85
+ : undefined;
86
+ }
87
+ catch {
88
+ return undefined;
89
+ }
90
+ }
@@ -2,6 +2,7 @@ import { type ChildProcess, type SpawnOptions } from "node:child_process";
2
2
  import { type PathMapper } from "./child-output";
3
3
  import { type ManifestSecrets } from "./manifest-secrets";
4
4
  import { ResolvedLayer } from "./workspace";
5
+ import { type BuildOrigin, type FrontendSnapshot } from "./snapshot";
5
6
  export declare function computeDepsHash(layerPaths: string[], workspacePackage?: string, installInputs?: readonly string[]): string;
6
7
  export declare function needsInstall(workspaceDir: string, layerPaths: string[], force: boolean): boolean;
7
8
  export declare function saveDepsHash(workspaceDir: string, layerPaths: string[]): void;
@@ -27,6 +28,11 @@ export interface SetupWorkspaceOptions {
27
28
  clientUrl?: string;
28
29
  workspaceKey?: string;
29
30
  bootstrapSecret?: string;
31
+ workspaceDir?: string;
32
+ snapshot?: {
33
+ dir: string;
34
+ snapshot: FrontendSnapshot;
35
+ };
30
36
  beforeInstall?: () => Promise<void>;
31
37
  }
32
38
  export interface SetupWorkspaceResult {
@@ -35,5 +41,6 @@ export interface SetupWorkspaceResult {
35
41
  manifestFromCache: boolean;
36
42
  manifestFetchedAt?: string;
37
43
  manifestSecrets: ManifestSecrets;
44
+ buildOrigin: BuildOrigin;
38
45
  }
39
46
  export declare function setupWorkspace(opts: SetupWorkspaceOptions): Promise<SetupWorkspaceResult>;
@@ -19,6 +19,7 @@ const manifest_secrets_1 = require("./manifest-secrets");
19
19
  const renderer_range_1 = require("./renderer-range");
20
20
  const materialize_1 = require("./materialize");
21
21
  const workspace_1 = require("./workspace");
22
+ const snapshot_1 = require("./snapshot");
22
23
  const cancellation_1 = require("./cancellation");
23
24
  const output_1 = require("./output");
24
25
  const config_1 = require("./config");
@@ -219,14 +220,25 @@ function manifestCacheUse(mode, offline) {
219
220
  return "offline";
220
221
  return mode === "build" ? "never" : "fallback";
221
222
  }
222
- async function setupWorkspace(opts) {
223
+ async function snapshotLayers({ dir, snapshot }, workspaceDir) {
224
+ const { manifest, takenAt } = snapshot;
225
+ const cacheDir = (0, node_path_1.join)(dir, snapshot_1.SNAPSHOT_LAYERS_DIR);
226
+ (0, layers_1.assertCachedArchivesExist)(manifest.modules, cacheDir, takenAt);
227
+ (0, manifest_1.writeCachedManifest)(workspaceDir, manifest, "drop", takenAt);
228
+ return {
229
+ manifest,
230
+ fromCache: false,
231
+ layers: await (0, layers_1.buildLayersFromCache)(manifest.modules, cacheDir),
232
+ };
233
+ }
234
+ async function servedLayers(opts, workspaceDir) {
223
235
  const { backendUrl, force, mode, offline, clientUrl, bootstrapSecret } = opts;
224
- const workspaceKey = opts.workspaceKey ?? (0, config_1.canonicalizeBackendUrl)(backendUrl);
225
- const workspaceDir = (0, workspace_1.ensureWorkspace)(workspaceKey);
226
- (0, workspace_1.writeWorkspaceMeta)(workspaceDir, backendUrl, workspaceKey);
227
- const resolved = await (0, manifest_1.resolveManifest)(workspaceDir, backendUrl, manifestCacheUse(mode, offline), clientUrl, bootstrapSecret);
236
+ const resolved = await (0, manifest_1.resolveManifest)(workspaceDir, backendUrl, manifestCacheUse(mode, offline), {
237
+ clientUrl,
238
+ bootstrapSecret,
239
+ privateValues: mode === "build" ? "drop" : "keep",
240
+ });
228
241
  const { manifest, fromCache, fetchedAt } = resolved;
229
- let layers;
230
242
  if (mode === "dev") {
231
243
  (0, layers_1.assertLayerPathsServed)(manifest.modules, backendUrl, {
232
244
  bootstrapSecret,
@@ -235,26 +247,45 @@ async function setupWorkspace(opts) {
235
247
  if (fromCache) {
236
248
  (0, manifest_1.assertCachedLayerPathsExist)(manifest.modules, fetchedAt);
237
249
  }
238
- layers = (0, layers_1.buildLayersFromPaths)(manifest.modules);
250
+ return { ...resolved, layers: (0, layers_1.buildLayersFromPaths)(manifest.modules) };
239
251
  }
240
- else {
241
- const cacheDir = (0, node_path_1.join)(workspaceDir, ".layers-cache");
242
- if (fromCache) {
243
- if (!(0, node_fs_1.existsSync)(cacheDir)) {
244
- throw new cli_1.CliError({
245
- title: "No layers archive was downloaded for this workspace",
246
- fixes: [
247
- "Run once without --offline, with the backend reachable, first",
248
- ],
249
- });
250
- }
251
- (0, layers_1.assertCachedArchivesExist)(manifest.modules, cacheDir, fetchedAt);
252
+ const cacheDir = (0, node_path_1.join)(workspaceDir, ".layers-cache");
253
+ if (fromCache) {
254
+ if (!(0, node_fs_1.existsSync)(cacheDir)) {
255
+ throw new cli_1.CliError({
256
+ title: "No layers archive was downloaded for this workspace",
257
+ fixes: [
258
+ "Run once without --offline, with the backend reachable, first",
259
+ ],
260
+ });
252
261
  }
253
- else {
254
- await (0, layers_1.downloadAndExtractLayers)(backendUrl, manifest.pack, cacheDir, force, bootstrapSecret);
255
- }
256
- layers = await (0, layers_1.buildLayersFromCache)(manifest.modules, cacheDir);
262
+ (0, layers_1.assertCachedArchivesExist)(manifest.modules, cacheDir, fetchedAt);
263
+ }
264
+ else {
265
+ await (0, layers_1.downloadAndExtractLayers)(backendUrl, manifest.pack, cacheDir, force, bootstrapSecret);
257
266
  }
267
+ return {
268
+ ...resolved,
269
+ layers: await (0, layers_1.buildLayersFromCache)(manifest.modules, cacheDir),
270
+ };
271
+ }
272
+ async function setupWorkspace(opts) {
273
+ const { backendUrl, force } = opts;
274
+ const workspaceKey = opts.workspaceKey ?? (0, config_1.canonicalizeBackendUrl)(backendUrl);
275
+ const workspaceDir = opts.workspaceDir
276
+ ? (0, workspace_1.ensureExplicitWorkspace)(opts.workspaceDir)
277
+ : (0, workspace_1.ensureWorkspace)(workspaceKey);
278
+ (0, workspace_1.writeWorkspaceMeta)(workspaceDir, backendUrl, workspaceKey);
279
+ const { manifest, fromCache, fetchedAt, layers } = opts.snapshot
280
+ ? await snapshotLayers(opts.snapshot, workspaceDir)
281
+ : await servedLayers(opts, workspaceDir);
282
+ const buildOrigin = {
283
+ source: opts.snapshot ? "snapshot" : "backend",
284
+ backendUrl: (0, config_1.canonicalizeBackendUrl)(backendUrl),
285
+ takenAt: opts.snapshot?.snapshot.takenAt ?? fetchedAt ?? new Date().toISOString(),
286
+ digest: (0, snapshot_1.manifestDigest)(manifest.modules),
287
+ authenticated: !!opts.snapshot || (!!opts.bootstrapSecret && !fromCache),
288
+ };
258
289
  (0, renderer_range_1.assertLayersSupportRenderer)(layers, { workspaceDir });
259
290
  (0, materialize_1.copyStaticTemplates)(workspaceDir);
260
291
  (0, materialize_1.writeWorkspacePackageJson)(workspaceDir, layers);
@@ -274,5 +305,6 @@ async function setupWorkspace(opts) {
274
305
  manifestFromCache: fromCache,
275
306
  manifestFetchedAt: fetchedAt,
276
307
  manifestSecrets: (0, manifest_secrets_1.collectManifestSecrets)(manifest.modules),
308
+ buildOrigin,
277
309
  };
278
310
  }
@@ -56,6 +56,7 @@ export declare function projectWorkspaceKey(projectDir: string): string;
56
56
  export declare function projectDirFromWorkspaceKey(workspaceKey: string | undefined): string | undefined;
57
57
  export declare function getWorkspaceDir(backendUrl: string): string;
58
58
  export declare function ensureWorkspace(workspaceKey: string): string;
59
+ export declare function ensureExplicitWorkspace(dir: string): string;
59
60
  export interface WorkspaceEntry {
60
61
  dir: string;
61
62
  backendUrl: string;
package/dist/workspace.js CHANGED
@@ -6,6 +6,7 @@ exports.projectWorkspaceKey = projectWorkspaceKey;
6
6
  exports.projectDirFromWorkspaceKey = projectDirFromWorkspaceKey;
7
7
  exports.getWorkspaceDir = getWorkspaceDir;
8
8
  exports.ensureWorkspace = ensureWorkspace;
9
+ exports.ensureExplicitWorkspace = ensureExplicitWorkspace;
9
10
  exports.listWorkspaces = listWorkspaces;
10
11
  exports.directorySize = directorySize;
11
12
  exports.writeWorkspaceMeta = writeWorkspaceMeta;
@@ -38,6 +39,11 @@ function ensureWorkspace(workspaceKey) {
38
39
  (0, node_fs_1.chmodSync)(dir, config_1.WORKSPACE_DIR_MODE);
39
40
  return dir;
40
41
  }
42
+ function ensureExplicitWorkspace(dir) {
43
+ const absolute = (0, node_path_1.resolve)(dir);
44
+ (0, node_fs_1.mkdirSync)(absolute, { recursive: true });
45
+ return absolute;
46
+ }
41
47
  function listWorkspaces() {
42
48
  const listing = { workspaces: [], skipped: [] };
43
49
  if (!(0, node_fs_1.existsSync)(config_1.DMS_FRONTEND_HOME))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-frontend",
3
- "version": "0.6.4",
3
+ "version": "0.7.1",
4
4
  "description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
5
5
  "keywords": [
6
6
  "antelope",
@@ -4,7 +4,7 @@
4
4
  // allows.
5
5
 
6
6
  import { GENERIC_ERROR_MESSAGE } from "./auth/backend.mjs";
7
- import { isColdStart } from "./backend-wake.mjs";
7
+ import { isWaitable } from "./backend-wake.mjs";
8
8
  import { isFrontendVisit, redirectFrontendVisit } from "./inertia.mjs";
9
9
 
10
10
  // What a JSON caller gets when the backend fails: the generic i18n key of the
@@ -47,7 +47,7 @@ async function refusalCode(response) {
47
47
 
48
48
  /** The error for a failed backend response, with its typed code if any. */
49
49
  export async function backendResponseError(response) {
50
- const retryAfter = isColdStart(response)
50
+ const retryAfter = isWaitable(response)
51
51
  ? (response.headers.get("retry-after") ?? undefined)
52
52
  : undefined;
53
53
  return new BackendResponseError(
@@ -1,14 +1,14 @@
1
- // Waiting out a backend the platform is waking from sleep.
1
+ // Waiting out a backend that is waking from sleep or still starting.
2
2
  //
3
3
  // A platform that scales the backend to zero answers the first requests with
4
4
  // a 503 while it starts a replica, marked with the cold-start header and a
5
- // Retry-After in whole seconds: all three make the signal, and a 503 missing
6
- // one of them goes out as it came. The header is a reserved resume
7
- // convention, not a proof of where the answer came from: a backend may set it
8
- // too, and then gets the same bounded wait. Only a GET or a HEAD is repeated;
9
- // a POST, PATCH or DELETE is sent once.
5
+ // Retry-After in whole seconds; a DMS backend that listens before it is ready
6
+ // answers 503 with a Retry-After too, without the header. A 503 with a
7
+ // Retry-After in whole seconds is the signal, with or without the header; a
8
+ // 503 without one goes out as it came. Only a GET or a HEAD is repeated; a
9
+ // POST, PATCH or DELETE is sent once.
10
10
  //
11
- // The wait starts with the first cold-start answer and lasts at most the
11
+ // The wait starts with the first waitable answer and lasts at most the
12
12
  // budget, the repeated requests included: one still waiting for its answer
13
13
  // when the budget runs out is aborted, and no request starts after it. It
14
14
  // ends at once when the client leaves.
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { setTimeout as sleep } from "node:timers/promises";
20
20
 
21
- /** Header marking a 503 as a cold start to wait out. */
21
+ /** Header a platform marks the 503 of a backend it is waking with. */
22
22
  export const COLD_START_HEADER = "x-antelope-cold-start";
23
23
  const COLD_START_VALUE = "1";
24
24
  const UNAVAILABLE = 503;
@@ -34,12 +34,14 @@ const MS_PER_SECOND = 1_000;
34
34
  const WHOLE_SECONDS = /^\d+$/;
35
35
  const WHOLE_MILLISECONDS = /^\d+$/;
36
36
 
37
- /** Whether `response` is a 503 marked as a cold start, with its Retry-After. */
38
- export function isColdStart(response) {
37
+ /**
38
+ * Whether `response` is a 503 asking to come back after a Retry-After in
39
+ * whole seconds: a platform waking the backend (with the cold-start header)
40
+ * or a backend still starting.
41
+ */
42
+ export function isWaitable(response) {
39
43
  return (
40
- response.status === UNAVAILABLE &&
41
- response.headers.get(COLD_START_HEADER) === COLD_START_VALUE &&
42
- WHOLE_SECONDS.test(retryAfter(response))
44
+ response.status === UNAVAILABLE && WHOLE_SECONDS.test(retryAfter(response))
43
45
  );
44
46
  }
45
47
 
@@ -57,7 +59,7 @@ export function wakeBudgetMs(env = process.env) {
57
59
  return WHOLE_MILLISECONDS.test(raw) ? Number(raw) : DEFAULT_BUDGET_MS;
58
60
  }
59
61
 
60
- /** The pause a cold-start answer's Retry-After asks for, kept between bounds. */
62
+ /** The pause a waitable answer's Retry-After asks for, kept between bounds. */
61
63
  function retryDelayMs(response) {
62
64
  return Math.min(
63
65
  MAX_DELAY_MS,
@@ -66,17 +68,15 @@ function retryDelayMs(response) {
66
68
  }
67
69
 
68
70
  /**
69
- * What the client gets once the budget is spent: the last cold-start answer's
70
- * status and headers, without the body its pause already discarded.
71
+ * What the client gets once the budget is spent: the last waitable answer's
72
+ * status, Retry-After and cold-start header if it had one, without the body
73
+ * its pause already discarded.
71
74
  */
72
75
  function expired(cold) {
73
- return new Response(null, {
74
- status: UNAVAILABLE,
75
- headers: {
76
- [COLD_START_HEADER]: COLD_START_VALUE,
77
- "retry-after": retryAfter(cold),
78
- },
79
- });
76
+ const headers = { "retry-after": retryAfter(cold) };
77
+ if (cold.headers.get(COLD_START_HEADER) === COLD_START_VALUE)
78
+ headers[COLD_START_HEADER] = COLD_START_VALUE;
79
+ return new Response(null, { status: UNAVAILABLE, headers });
80
80
  }
81
81
 
82
82
  /**
@@ -102,9 +102,9 @@ async function repeat(url, init, deadline, now) {
102
102
  }
103
103
 
104
104
  /**
105
- * `fetch`, waiting out cold-start answers to a GET or a HEAD until `budgetMs`
105
+ * `fetch`, waiting out waitable 503s to a GET or a HEAD until `budgetMs`
106
106
  * from the first of them is spent or MAX_REPEATS repeats are sent. Then the
107
- * caller gets a cold-start 503 with its `Retry-After` to relay. The client
107
+ * caller gets the 503 with its `Retry-After` to relay. The client
108
108
  * leaving, through `init.signal`, ends the wait with its AbortError.
109
109
  */
110
110
  export async function fetchWaking(
@@ -116,7 +116,7 @@ export async function fetchWaking(
116
116
  let response = await fetch(url, init);
117
117
  if (!RETRYABLE_METHODS.has(method)) return response;
118
118
  const deadline = now() + budgetMs;
119
- for (let repeats = 0; isColdStart(response); repeats += 1) {
119
+ for (let repeats = 0; isWaitable(response); repeats += 1) {
120
120
  const cold = response;
121
121
  const delay = retryDelayMs(cold);
122
122
  if (repeats >= MAX_REPEATS || now() + delay > deadline) return cold;