@le-space/playwright 0.6.34 → 0.6.36

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/cleanup-cli.js CHANGED
@@ -1,8 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- resolveAlephApiHosts,
4
- waitForAlephInstanceDeletion
5
- } from "./chunk-YAODEYKV.js";
6
2
 
7
3
  // src/cleanup-cli.ts
8
4
  import { createHash } from "crypto";
@@ -10,6 +6,67 @@ import { appendFile, mkdir, writeFile } from "fs/promises";
10
6
  import { dirname } from "path";
11
7
  import process from "process";
12
8
  import { eraseInstanceOnCrn, forgetAlephMessages } from "@le-space/core";
9
+
10
+ // src/aleph-instance.ts
11
+ var SUPPORTED_ALEPH_API_HOSTS = ["https://api2.aleph.im", "https://api.aleph.im"];
12
+ var DEFAULT_ALEPH_SCHEDULER_URL = "https://scheduler.api.aleph.cloud";
13
+ var API3_HOST_PATTERN = /(^|\.)api3\.aleph\.im$/iu;
14
+ function delay(ms) {
15
+ return new Promise((resolve) => setTimeout(resolve, ms));
16
+ }
17
+ function normalizeApiOrigin(value) {
18
+ try {
19
+ const url = new URL(value);
20
+ if (API3_HOST_PATTERN.test(url.hostname)) return null;
21
+ return url.origin;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+ function resolveAlephApiHosts(candidates) {
27
+ const allowed = new Set((candidates ?? SUPPORTED_ALEPH_API_HOSTS).map(normalizeApiOrigin).filter((value) => value != null));
28
+ const selected = SUPPORTED_ALEPH_API_HOSTS.filter((host) => allowed.has(host));
29
+ return selected.length > 0 ? [...selected] : [...SUPPORTED_ALEPH_API_HOSTS];
30
+ }
31
+ async function waitForAlephInstanceDeletion(options) {
32
+ const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
33
+ if (!fetchImpl) throw new Error("A fetch implementation is required for cleanup verification");
34
+ const deadline = Date.now() + (options.timeoutMs ?? 5 * 6e4);
35
+ const hosts = resolveAlephApiHosts(options.apiHosts);
36
+ const schedulerUrl = new URL(`/api/v0/allocation/${options.instanceHash}`, options.schedulerUrl ?? DEFAULT_ALEPH_SCHEDULER_URL);
37
+ let lastSummary = "Deletion has not been observed yet.";
38
+ while (Date.now() < deadline) {
39
+ const observations = [];
40
+ let replicasForgotten = true;
41
+ for (const apiHost of hosts) {
42
+ try {
43
+ const response = await fetchImpl(new URL(`/api/v0/messages/${options.instanceHash}`, apiHost), { cache: "no-cache" });
44
+ const payload = await response.json().catch(() => null);
45
+ const forgotten = payload?.status === "forgotten" || Boolean(payload?.forgotten_by?.length);
46
+ replicasForgotten &&= forgotten;
47
+ observations.push(`${apiHost}: ${forgotten ? "forgotten" : payload?.status ?? `HTTP ${response.status}`}`);
48
+ } catch (error) {
49
+ replicasForgotten = false;
50
+ observations.push(`${apiHost}: ${error instanceof Error ? error.message : String(error)}`);
51
+ }
52
+ }
53
+ let unallocated = false;
54
+ try {
55
+ const response = await fetchImpl(schedulerUrl, { cache: "no-cache" });
56
+ const payload = await response.json().catch(() => null);
57
+ unallocated = response.status === 404 || payload?.error === "VM is not allocated to any node";
58
+ observations.push(`scheduler: ${unallocated ? "unallocated" : `HTTP ${response.status}`}`);
59
+ } catch (error) {
60
+ observations.push(`scheduler: ${error instanceof Error ? error.message : String(error)}`);
61
+ }
62
+ lastSummary = observations.join("; ");
63
+ if (replicasForgotten && unallocated) return lastSummary;
64
+ await delay(options.pollIntervalMs ?? 2e3);
65
+ }
66
+ throw new Error(`Aleph INSTANCE ${options.instanceHash} was not deleted within ${options.timeoutMs ?? 5 * 6e4}ms: ${lastSummary}`);
67
+ }
68
+
69
+ // src/cleanup-cli.ts
13
70
  function parseCleanupCliArgs(argv, env) {
14
71
  const flags = /* @__PURE__ */ new Map();
15
72
  for (let index = 0; index < argv.length; index += 1) {
package/index.d.ts CHANGED
@@ -111,12 +111,17 @@ interface WaitForPubsubSubscriberOptions {
111
111
  }
112
112
  interface RelayButtonDriverOptions {
113
113
  launcherName?: string | RegExp;
114
+ /** Accessible label of the instance-name field (@le-space/ui's Svelte form labels it, no placeholder). */
115
+ instanceNameLabel?: string;
114
116
  instanceNamePlaceholder?: string;
117
+ /** Accessible label of the SSH-key field (@le-space/ui's Svelte form labels it, no placeholder). */
118
+ sshPublicKeyLabel?: string;
115
119
  sshPublicKeyPlaceholder?: string;
116
120
  connectWalletName?: string;
117
121
  deployButtonName?: string;
118
122
  deleteButtonName?: string;
119
123
  refreshButtonName?: string;
124
+ advancedToggleName?: string;
120
125
  }
121
126
  interface ProvisionRelayOptions {
122
127
  accountAddress: string;
@@ -203,6 +208,42 @@ declare function selectExpiredAlephPlaywrightRunners(options: {
203
208
  ttlMs?: number;
204
209
  }): AlephRunnerJanitorSelection;
205
210
  declare function waitForPubsubSubscriber(page: Page, options: WaitForPubsubSubscriberOptions): Promise<string[]>;
211
+ /**
212
+ * Default console filter for libp2p connectivity diagnostics — keeps the
213
+ * relay/dial/peer/webrtc/circuit/reservation/connect lines plus errors/warnings
214
+ * and drops the rest of the (very chatty) browser console.
215
+ */
216
+ declare const LIBP2P_DIAGNOSTIC_CONSOLE_FILTER: RegExp;
217
+ interface ProgressLogger {
218
+ /** Log a free-form progress line: `[label HH:MM:SS.mmm] message`. */
219
+ progress(message: string): void;
220
+ /** Log a stage marker: `[label HH:MM:SS.mmm] stage: name (detail)`. */
221
+ stage(name: string, detail?: string): void;
222
+ }
223
+ /**
224
+ * A tiny timestamped stage/progress logger so consumers stream what a remote
225
+ * E2E is doing instead of going silent for minutes. Mirrors simple-todo's
226
+ * `[remote-e2e] stage: …` format so both consumers read the same way.
227
+ */
228
+ declare function createProgressLogger(options?: {
229
+ label?: string;
230
+ log?: (line: string) => void;
231
+ }): ProgressLogger;
232
+ interface ForwardBrowserConsoleOptions {
233
+ /** Prefix identifying the browser, e.g. `local` or `aleph-remote`. */
234
+ label: string;
235
+ log?: (line: string) => void;
236
+ /** Only forward matching lines. Defaults to {@link LIBP2P_DIAGNOSTIC_CONSOLE_FILTER}; pass `null` to forward everything. */
237
+ filter?: RegExp | ((text: string) => boolean) | null;
238
+ /** Truncate each forwarded line to this length (default 300). */
239
+ maxLength?: number;
240
+ }
241
+ /**
242
+ * Forward a page's browser console + page errors into the test log so libp2p
243
+ * connection/discovery activity is visible in CI. Filtered by default to the
244
+ * connectivity-relevant lines.
245
+ */
246
+ declare function forwardBrowserConsole(page: Page, options: ForwardBrowserConsoleOptions): void;
206
247
  declare function installEip1193WalletMock(context: BrowserContext, account: RelayWalletAccount): Promise<void>;
207
248
  declare class RelayButtonDriver {
208
249
  readonly page: Page;
@@ -210,6 +251,15 @@ declare class RelayButtonDriver {
210
251
  constructor(page: Page, options?: RelayButtonDriverOptions);
211
252
  deployButton(): Locator;
212
253
  instance(instanceName: string): Locator;
254
+ /**
255
+ * The instance-name field, matched by accessible label OR placeholder so the
256
+ * driver works for both @le-space/ui builds: the Svelte form labels the field
257
+ * (`<label><span>Instance Name</span><input>`) with no placeholder, while the
258
+ * React build renders a `placeholder="Instance name"`.
259
+ */
260
+ instanceNameField(): Locator;
261
+ /** The SSH-key field, matched by accessible label OR placeholder (see {@link instanceNameField}). */
262
+ sshPublicKeyField(): Locator;
213
263
  prepare(options: {
214
264
  instanceName: string;
215
265
  sshPublicKey: string;
@@ -266,4 +316,4 @@ declare function createRelayTest(options: CreateRelayTestOptions): _playwright_t
266
316
  relayLifecycle: RelayLifecycleFixture;
267
317
  }, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
268
318
 
269
- export { type AlephChromiumConnector, type AlephCostEvidence, type AlephCreditBalanceSnapshot, type AlephPricingSnapshot, type AlephRemoteBrowserOptions, type AlephRunnerInstanceCandidate, type AlephRunnerJanitorSelection, type CleanupRelayHooks, type CleanupRelayOptions, type CleanupRelayResult, type CreateRelayTestOptions, DEFAULT_ALEPH_SCHEDULER_URL, PLAYWRIGHT_RUNNER_VERSION, type ProvisionRelayOptions, type ProvisionedRelay, type RelayAddressPolicy, RelayButtonDriver, type RelayButtonDriverOptions, type RelayEvidence, type RelayEvidenceStep, type RelayLifecycleFixture, type RelayWalletAccount, SUPPORTED_ALEPH_API_HOSTS, type WaitForPubsubSubscriberOptions, appendRelayGithubSummary, buildAlephCostEvidence, cleanupRelay, connectAlephChromium, createRelayEvidence, createRelayTest, findAlephInstanceHash, formatAlephCostGithubSummary, formatRelayGithubSummary, installEip1193WalletMock, provisionRelay, resolveAlephApiHosts, selectBrowserRelayAddresses, selectExpiredAlephPlaywrightRunners, updateRelayEvidenceStep, waitForAlephInstanceDeletion, waitForAlephMessageForgotten, waitForBootstrapRegistration, waitForDeployableManifest, waitForPubsubSubscriber, writeRelayEvidence };
319
+ export { type AlephChromiumConnector, type AlephCostEvidence, type AlephCreditBalanceSnapshot, type AlephPricingSnapshot, type AlephRemoteBrowserOptions, type AlephRunnerInstanceCandidate, type AlephRunnerJanitorSelection, type CleanupRelayHooks, type CleanupRelayOptions, type CleanupRelayResult, type CreateRelayTestOptions, DEFAULT_ALEPH_SCHEDULER_URL, type ForwardBrowserConsoleOptions, LIBP2P_DIAGNOSTIC_CONSOLE_FILTER, PLAYWRIGHT_RUNNER_VERSION, type ProgressLogger, type ProvisionRelayOptions, type ProvisionedRelay, type RelayAddressPolicy, RelayButtonDriver, type RelayButtonDriverOptions, type RelayEvidence, type RelayEvidenceStep, type RelayLifecycleFixture, type RelayWalletAccount, SUPPORTED_ALEPH_API_HOSTS, type WaitForPubsubSubscriberOptions, appendRelayGithubSummary, buildAlephCostEvidence, cleanupRelay, connectAlephChromium, createProgressLogger, createRelayEvidence, createRelayTest, findAlephInstanceHash, formatAlephCostGithubSummary, formatRelayGithubSummary, forwardBrowserConsole, installEip1193WalletMock, provisionRelay, resolveAlephApiHosts, selectBrowserRelayAddresses, selectExpiredAlephPlaywrightRunners, updateRelayEvidenceStep, waitForAlephInstanceDeletion, waitForAlephMessageForgotten, waitForBootstrapRegistration, waitForDeployableManifest, waitForPubsubSubscriber, writeRelayEvidence };
package/index.js CHANGED
@@ -1,11 +1,3 @@
1
- import {
2
- DEFAULT_ALEPH_SCHEDULER_URL,
3
- SUPPORTED_ALEPH_API_HOSTS,
4
- delay,
5
- resolveAlephApiHosts,
6
- waitForAlephInstanceDeletion
7
- } from "./chunk-YAODEYKV.js";
8
-
9
1
  // src/index.ts
10
2
  import { createHash } from "crypto";
11
3
  import { appendFile, mkdir, writeFile } from "fs/promises";
@@ -13,6 +5,67 @@ import { dirname } from "path";
13
5
  import { fetchAlephBootstrapPosts } from "@le-space/aleph-bootstrap";
14
6
  import { eraseInstanceOnCrn, forgetAlephMessages } from "@le-space/core";
15
7
  import { test as playwrightTest } from "@playwright/test";
8
+
9
+ // src/aleph-instance.ts
10
+ var SUPPORTED_ALEPH_API_HOSTS = ["https://api2.aleph.im", "https://api.aleph.im"];
11
+ var DEFAULT_ALEPH_SCHEDULER_URL = "https://scheduler.api.aleph.cloud";
12
+ var API3_HOST_PATTERN = /(^|\.)api3\.aleph\.im$/iu;
13
+ function delay(ms) {
14
+ return new Promise((resolve) => setTimeout(resolve, ms));
15
+ }
16
+ function normalizeApiOrigin(value) {
17
+ try {
18
+ const url = new URL(value);
19
+ if (API3_HOST_PATTERN.test(url.hostname)) return null;
20
+ return url.origin;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ function resolveAlephApiHosts(candidates) {
26
+ const allowed = new Set((candidates ?? SUPPORTED_ALEPH_API_HOSTS).map(normalizeApiOrigin).filter((value) => value != null));
27
+ const selected = SUPPORTED_ALEPH_API_HOSTS.filter((host) => allowed.has(host));
28
+ return selected.length > 0 ? [...selected] : [...SUPPORTED_ALEPH_API_HOSTS];
29
+ }
30
+ async function waitForAlephInstanceDeletion(options) {
31
+ const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
32
+ if (!fetchImpl) throw new Error("A fetch implementation is required for cleanup verification");
33
+ const deadline = Date.now() + (options.timeoutMs ?? 5 * 6e4);
34
+ const hosts = resolveAlephApiHosts(options.apiHosts);
35
+ const schedulerUrl = new URL(`/api/v0/allocation/${options.instanceHash}`, options.schedulerUrl ?? DEFAULT_ALEPH_SCHEDULER_URL);
36
+ let lastSummary = "Deletion has not been observed yet.";
37
+ while (Date.now() < deadline) {
38
+ const observations = [];
39
+ let replicasForgotten = true;
40
+ for (const apiHost of hosts) {
41
+ try {
42
+ const response = await fetchImpl(new URL(`/api/v0/messages/${options.instanceHash}`, apiHost), { cache: "no-cache" });
43
+ const payload = await response.json().catch(() => null);
44
+ const forgotten = payload?.status === "forgotten" || Boolean(payload?.forgotten_by?.length);
45
+ replicasForgotten &&= forgotten;
46
+ observations.push(`${apiHost}: ${forgotten ? "forgotten" : payload?.status ?? `HTTP ${response.status}`}`);
47
+ } catch (error) {
48
+ replicasForgotten = false;
49
+ observations.push(`${apiHost}: ${error instanceof Error ? error.message : String(error)}`);
50
+ }
51
+ }
52
+ let unallocated = false;
53
+ try {
54
+ const response = await fetchImpl(schedulerUrl, { cache: "no-cache" });
55
+ const payload = await response.json().catch(() => null);
56
+ unallocated = response.status === 404 || payload?.error === "VM is not allocated to any node";
57
+ observations.push(`scheduler: ${unallocated ? "unallocated" : `HTTP ${response.status}`}`);
58
+ } catch (error) {
59
+ observations.push(`scheduler: ${error instanceof Error ? error.message : String(error)}`);
60
+ }
61
+ lastSummary = observations.join("; ");
62
+ if (replicasForgotten && unallocated) return lastSummary;
63
+ await delay(options.pollIntervalMs ?? 2e3);
64
+ }
65
+ throw new Error(`Aleph INSTANCE ${options.instanceHash} was not deleted within ${options.timeoutMs ?? 5 * 6e4}ms: ${lastSummary}`);
66
+ }
67
+
68
+ // src/index.ts
16
69
  var PLAYWRIGHT_RUNNER_VERSION = "1.61.1";
17
70
  function requireFiniteNumber(value, label) {
18
71
  const number = Number(value);
@@ -153,6 +206,34 @@ async function waitForPubsubSubscriber(page, options) {
153
206
  `PubSub subscriber ${options.peerId} was not stable on ${options.topic} within ${timeoutMs}ms; last subscribers: ${subscribers.join(", ") || "none"}`
154
207
  );
155
208
  }
209
+ var LIBP2P_DIAGNOSTIC_CONSOLE_FILTER = /error|warn|relay|dial|peer|webrtc|circuit|reservation|connect/i;
210
+ function createProgressLogger(options = {}) {
211
+ const label = options.label ?? "e2e";
212
+ const log = options.log ?? ((line) => console.log(line));
213
+ const stamp = () => (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
214
+ return {
215
+ progress(message) {
216
+ log(`[${label} ${stamp()}] ${message}`);
217
+ },
218
+ stage(name, detail) {
219
+ log(`[${label} ${stamp()}] stage: ${name}${detail ? ` (${detail})` : ""}`);
220
+ }
221
+ };
222
+ }
223
+ function forwardBrowserConsole(page, options) {
224
+ const log = options.log ?? ((line) => console.log(line));
225
+ const maxLength = options.maxLength ?? 300;
226
+ const filter = options.filter === void 0 ? LIBP2P_DIAGNOSTIC_CONSOLE_FILTER : options.filter;
227
+ const matches = (text) => {
228
+ if (!filter) return true;
229
+ return filter instanceof RegExp ? filter.test(text) : filter(text);
230
+ };
231
+ page.on("console", (message) => {
232
+ const text = message.text();
233
+ if (matches(text)) log(`[${options.label} ${message.type()}] ${text}`.slice(0, maxLength));
234
+ });
235
+ page.on("pageerror", (error) => log(`[${options.label} pageerror] ${error.message}`.slice(0, maxLength)));
236
+ }
156
237
  async function installEip1193WalletMock(context, account) {
157
238
  await context.exposeBinding("__relayE2eWalletRequest", async (_source, request) => {
158
239
  const { method, params = [] } = request;
@@ -201,12 +282,15 @@ var RelayButtonDriver = class {
201
282
  this.page = page;
202
283
  this.options = {
203
284
  launcherName: options.launcherName ?? "Relay Button",
285
+ instanceNameLabel: options.instanceNameLabel ?? "Instance Name",
204
286
  instanceNamePlaceholder: options.instanceNamePlaceholder ?? "Instance name",
287
+ sshPublicKeyLabel: options.sshPublicKeyLabel ?? "SSH Public Key",
205
288
  sshPublicKeyPlaceholder: options.sshPublicKeyPlaceholder ?? "SSH public key",
206
289
  connectWalletName: options.connectWalletName ?? "Connect MetaMask",
207
290
  deployButtonName: options.deployButtonName ?? "Deploy Relay",
208
291
  deleteButtonName: options.deleteButtonName ?? "Delete",
209
- refreshButtonName: options.refreshButtonName ?? "Refresh"
292
+ refreshButtonName: options.refreshButtonName ?? "Refresh",
293
+ advancedToggleName: options.advancedToggleName ?? "Advanced"
210
294
  };
211
295
  }
212
296
  deployButton() {
@@ -217,15 +301,28 @@ var RelayButtonDriver = class {
217
301
  instance(instanceName) {
218
302
  return this.page.locator("details").filter({ hasText: instanceName }).first();
219
303
  }
304
+ /**
305
+ * The instance-name field, matched by accessible label OR placeholder so the
306
+ * driver works for both @le-space/ui builds: the Svelte form labels the field
307
+ * (`<label><span>Instance Name</span><input>`) with no placeholder, while the
308
+ * React build renders a `placeholder="Instance name"`.
309
+ */
310
+ instanceNameField() {
311
+ return this.page.getByLabel(this.options.instanceNameLabel).or(this.page.getByPlaceholder(this.options.instanceNamePlaceholder)).first();
312
+ }
313
+ /** The SSH-key field, matched by accessible label OR placeholder (see {@link instanceNameField}). */
314
+ sshPublicKeyField() {
315
+ return this.page.getByLabel(this.options.sshPublicKeyLabel).or(this.page.getByPlaceholder(this.options.sshPublicKeyPlaceholder)).first();
316
+ }
220
317
  async prepare(options) {
221
318
  const launcher = this.page.getByRole("button", {
222
319
  name: this.options.launcherName
223
320
  });
224
321
  await launcher.waitFor({ state: "visible", timeout: 6e4 });
225
322
  await launcher.click();
226
- await this.page.getByPlaceholder(this.options.instanceNamePlaceholder).fill(options.instanceName);
227
- await this.page.getByText("Advanced", { exact: true }).click();
228
- await this.page.getByPlaceholder(this.options.sshPublicKeyPlaceholder).fill(options.sshPublicKey);
323
+ await this.instanceNameField().fill(options.instanceName);
324
+ await this.page.getByText(this.options.advancedToggleName, { exact: true }).click();
325
+ await this.sshPublicKeyField().fill(options.sshPublicKey);
229
326
  await this.page.getByRole("button", {
230
327
  name: this.options.connectWalletName,
231
328
  exact: true
@@ -723,6 +820,7 @@ function createRelayTest(options) {
723
820
  }
724
821
  export {
725
822
  DEFAULT_ALEPH_SCHEDULER_URL,
823
+ LIBP2P_DIAGNOSTIC_CONSOLE_FILTER,
726
824
  PLAYWRIGHT_RUNNER_VERSION,
727
825
  RelayButtonDriver,
728
826
  SUPPORTED_ALEPH_API_HOSTS,
@@ -730,11 +828,13 @@ export {
730
828
  buildAlephCostEvidence,
731
829
  cleanupRelay,
732
830
  connectAlephChromium,
831
+ createProgressLogger,
733
832
  createRelayEvidence,
734
833
  createRelayTest,
735
834
  findAlephInstanceHash,
736
835
  formatAlephCostGithubSummary,
737
836
  formatRelayGithubSummary,
837
+ forwardBrowserConsole,
738
838
  installEip1193WalletMock,
739
839
  provisionRelay,
740
840
  resolveAlephApiHosts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/playwright",
3
- "version": "0.6.34",
3
+ "version": "0.6.36",
4
4
  "description": "Reusable Playwright fixtures and Relay Button lifecycle helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,8 +27,8 @@
27
27
  "url": "https://github.com/NiKrause/relay-button/issues"
28
28
  },
29
29
  "dependencies": {
30
- "@le-space/aleph-bootstrap": "0.6.34",
31
- "@le-space/core": "0.6.34"
30
+ "@le-space/aleph-bootstrap": "0.6.36",
31
+ "@le-space/core": "0.6.36"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@playwright/test": ">=1.61.1 <1.62.0"
package/chunk-YAODEYKV.js DELETED
@@ -1,66 +0,0 @@
1
- // src/aleph-instance.ts
2
- var SUPPORTED_ALEPH_API_HOSTS = ["https://api2.aleph.im", "https://api.aleph.im"];
3
- var DEFAULT_ALEPH_SCHEDULER_URL = "https://scheduler.api.aleph.cloud";
4
- var API3_HOST_PATTERN = /(^|\.)api3\.aleph\.im$/iu;
5
- function delay(ms) {
6
- return new Promise((resolve) => setTimeout(resolve, ms));
7
- }
8
- function normalizeApiOrigin(value) {
9
- try {
10
- const url = new URL(value);
11
- if (API3_HOST_PATTERN.test(url.hostname)) return null;
12
- return url.origin;
13
- } catch {
14
- return null;
15
- }
16
- }
17
- function resolveAlephApiHosts(candidates) {
18
- const allowed = new Set((candidates ?? SUPPORTED_ALEPH_API_HOSTS).map(normalizeApiOrigin).filter((value) => value != null));
19
- const selected = SUPPORTED_ALEPH_API_HOSTS.filter((host) => allowed.has(host));
20
- return selected.length > 0 ? [...selected] : [...SUPPORTED_ALEPH_API_HOSTS];
21
- }
22
- async function waitForAlephInstanceDeletion(options) {
23
- const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
24
- if (!fetchImpl) throw new Error("A fetch implementation is required for cleanup verification");
25
- const deadline = Date.now() + (options.timeoutMs ?? 5 * 6e4);
26
- const hosts = resolveAlephApiHosts(options.apiHosts);
27
- const schedulerUrl = new URL(`/api/v0/allocation/${options.instanceHash}`, options.schedulerUrl ?? DEFAULT_ALEPH_SCHEDULER_URL);
28
- let lastSummary = "Deletion has not been observed yet.";
29
- while (Date.now() < deadline) {
30
- const observations = [];
31
- let replicasForgotten = true;
32
- for (const apiHost of hosts) {
33
- try {
34
- const response = await fetchImpl(new URL(`/api/v0/messages/${options.instanceHash}`, apiHost), { cache: "no-cache" });
35
- const payload = await response.json().catch(() => null);
36
- const forgotten = payload?.status === "forgotten" || Boolean(payload?.forgotten_by?.length);
37
- replicasForgotten &&= forgotten;
38
- observations.push(`${apiHost}: ${forgotten ? "forgotten" : payload?.status ?? `HTTP ${response.status}`}`);
39
- } catch (error) {
40
- replicasForgotten = false;
41
- observations.push(`${apiHost}: ${error instanceof Error ? error.message : String(error)}`);
42
- }
43
- }
44
- let unallocated = false;
45
- try {
46
- const response = await fetchImpl(schedulerUrl, { cache: "no-cache" });
47
- const payload = await response.json().catch(() => null);
48
- unallocated = response.status === 404 || payload?.error === "VM is not allocated to any node";
49
- observations.push(`scheduler: ${unallocated ? "unallocated" : `HTTP ${response.status}`}`);
50
- } catch (error) {
51
- observations.push(`scheduler: ${error instanceof Error ? error.message : String(error)}`);
52
- }
53
- lastSummary = observations.join("; ");
54
- if (replicasForgotten && unallocated) return lastSummary;
55
- await delay(options.pollIntervalMs ?? 2e3);
56
- }
57
- throw new Error(`Aleph INSTANCE ${options.instanceHash} was not deleted within ${options.timeoutMs ?? 5 * 6e4}ms: ${lastSummary}`);
58
- }
59
-
60
- export {
61
- SUPPORTED_ALEPH_API_HOSTS,
62
- DEFAULT_ALEPH_SCHEDULER_URL,
63
- delay,
64
- resolveAlephApiHosts,
65
- waitForAlephInstanceDeletion
66
- };