@coinbase/onchaintestkit
Version:
End-to-end testing toolkit for blockchain applications, powered by Playwright
184 lines (183 loc) • 6.38 kB
JavaScript
"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;