UNPKG

@coinbase/onchaintestkit

Version:

End-to-end testing toolkit for blockchain applications, powered by Playwright

184 lines (183 loc) 6.38 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConditionWatcher = exports.WatchAbortedError = exports.WatchTimeoutError = void 0; const RetryStrategy_1 = require("./RetryStrategy"); class WatchTimeoutError extends Error { constructor(description, timeout, attempts) { super(`Condition "${description}" timed out after ${timeout}ms (${attempts} attempts)`); this.name = "WatchTimeoutError"; } } exports.WatchTimeoutError = WatchTimeoutError; class WatchAbortedError extends Error { constructor(description) { super(`Condition "${description}" was aborted`); this.name = "WatchAbortedError"; } } exports.WatchAbortedError = WatchAbortedError; /** * Observable-like condition watcher for replacing simple polling patterns */ class ConditionWatcher { constructor(condition, options) { this.startTime = 0; this.attempts = 0; this.condition = condition; this.options = { retryOptions: { strategy: "exponential", config: { baseDelay: 50, maxDelay: 1000, multiplier: 1.2, jitter: true, maxAttempts: 50, }, }, ...options, }; } /** * Create a new condition watcher */ static watch(condition, options) { return new ConditionWatcher(condition, options); } /** * Wait until the condition returns a non-null value */ async waitUntil() { this.startTime = Date.now(); this.attempts = 0; const delayFn = (0, RetryStrategy_1.createDelayFunction)(this.options.retryOptions ?? { strategy: "exponential", config: { baseDelay: 50, maxDelay: 1000, multiplier: 1.2, jitter: true, maxAttempts: 50, }, }); while (this.shouldContinue()) { this.attempts++; // Check for abortion if (this.options.signal?.aborted) { throw new WatchAbortedError(this.options.description || "unknown condition"); } try { const result = await this.condition(); if (result !== null) { return { value: result, timestamp: Date.now(), attempts: this.attempts, }; } } catch (error) { // Log error but continue trying unless it's a critical error console.warn(`Condition check failed on attempt ${this.attempts}:`, error); } // Don't delay after the last attempt or if we're about to timeout const maxAttempts = this.options.retryOptions?.config.maxAttempts ?? 50; if (this.shouldContinue() && this.attempts < maxAttempts) { const delay = delayFn(this.attempts - 1); await this.sleep(delay); } } throw new WatchTimeoutError(this.options.description || "unknown condition", this.options.timeout, this.attempts); } /** * Wait until the condition returns a value that matches the predicate */ async waitUntilMatches(predicate) { const originalCondition = this.condition; this.condition = async () => { const result = await originalCondition(); if (result !== null && predicate(result)) { return result; } return null; }; return this.waitUntil(); } /** * Transform the watched value before returning */ map(transform) { const originalCondition = this.condition; const mappedCondition = async () => { const result = await originalCondition(); return result !== null ? transform(result) : null; }; return new ConditionWatcher(mappedCondition, this.options); } /** * Add a filter to the condition */ filter(predicate) { const originalCondition = this.condition; const filteredCondition = async () => { const result = await originalCondition(); return result !== null && predicate(result) ? result : null; }; return new ConditionWatcher(filteredCondition, this.options); } /** * Add a timeout to the watcher */ timeout(ms) { return new ConditionWatcher(this.condition, { ...this.options, timeout: ms, }); } /** * Add retry configuration */ withRetryOptions(retryOptions) { return new ConditionWatcher(this.condition, { ...this.options, retryOptions, }); } /** * Add a description for better error messages */ describe(description) { return new ConditionWatcher(this.condition, { ...this.options, description, }); } shouldContinue() { const elapsed = Date.now() - this.startTime; const withinTimeout = elapsed < this.options.timeout; const maxAttempts = this.options.retryOptions?.config.maxAttempts ?? 50; const withinAttempts = this.attempts < maxAttempts; return withinTimeout && withinAttempts; } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Static helper methods for common patterns */ static async waitForCondition(condition, timeout, description) { const watcher = ConditionWatcher.watch(condition, { timeout, description }); const result = await watcher.waitUntil(); return result.value; } static async waitForElement(locator, timeout = 5000) { return ConditionWatcher.waitForCondition(locator, timeout, "element to appear"); } static async waitForValue(getter, timeout = 5000, description) { return ConditionWatcher.waitForCondition(async () => { const value = await getter(); return value !== null && value !== undefined ? value : null; }, timeout, description || "value to be available"); } } exports.ConditionWatcher = ConditionWatcher;