Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions demo-timeout-cancellation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Demonstration script for timeout and cancellation functionality
// This script shows the new timeout and AbortSignal features working

const { OpenFeature, ErrorCode, ProviderStatus } = require('./packages/server/dist/cjs');

// Mock provider that simulates slow operations
class SlowProvider {
constructor(delay = 100) {
this.delay = delay;
this.metadata = { name: 'slow-demo-provider' };
this.runsOn = 'server';
}

async initialize() {
console.log(`🔄 Provider initializing (${this.delay}ms delay)...`);
await new Promise(resolve => setTimeout(resolve, this.delay));
console.log('✅ Provider initialized successfully');
}

async resolveBooleanEvaluation(flagKey, defaultValue) {
console.log(`🔄 Evaluating flag "${flagKey}" (${this.delay}ms delay)...`);
await new Promise(resolve => setTimeout(resolve, this.delay));
console.log(`✅ Flag "${flagKey}" evaluated to true`);
return { value: true, reason: 'STATIC' };
}

resolveStringEvaluation() { throw new Error('Not implemented'); }
resolveNumberEvaluation() { throw new Error('Not implemented'); }
resolveObjectEvaluation() { throw new Error('Not implemented'); }
}

async function demonstrateTimeoutAndCancellation() {
console.log('🚀 OpenFeature Timeout and Cancellation Demo\n');

try {
// Demo 1: Provider initialization timeout (should succeed)
console.log('📝 Demo 1: Provider initialization with sufficient timeout');
console.log('Setting provider with 200ms timeout, provider takes 100ms...');
const fastProvider = new SlowProvider(100);
await OpenFeature.setProviderAndWait(fastProvider, { timeout: 200 });
console.log('✅ Provider initialization succeeded within timeout\n');

// Demo 2: Flag evaluation timeout (should succeed)
console.log('📝 Demo 2: Flag evaluation with sufficient timeout');
console.log('Evaluating flag with 200ms timeout, evaluation takes 100ms...');
const client = OpenFeature.getClient();
const result1 = await client.getBooleanDetails('demo-flag', false, {}, { timeout: 200 });
console.log(`✅ Flag evaluation succeeded: value=${result1.value}, errorCode=${result1.errorCode || 'none'}\n`);

// Demo 3: Provider initialization timeout (should fail)
console.log('📝 Demo 3: Provider initialization timeout');
console.log('Setting provider with 50ms timeout, provider takes 200ms...');
try {
const slowProvider = new SlowProvider(200);
await OpenFeature.setProviderAndWait(slowProvider, { timeout: 50 });
console.log('❌ Unexpected success - should have timed out');
} catch (error) {
if (error.code === ErrorCode.TIMEOUT) {
console.log(`✅ Provider initialization timed out as expected: ${error.message}\n`);
} else {
console.log(`❌ Unexpected error: ${error.message}\n`);
}
}

// Demo 4: Flag evaluation timeout (should fail)
console.log('📝 Demo 4: Flag evaluation timeout');
console.log('Evaluating flag with 50ms timeout, evaluation takes 200ms...');
// Set a new slow provider for this test
const anotherSlowProvider = new SlowProvider(200);
await OpenFeature.setProviderAndWait(anotherSlowProvider, { timeout: 300 }); // Give it time to initialize

const client2 = OpenFeature.getClient();
const result2 = await client2.getBooleanDetails('timeout-flag', false, {}, { timeout: 50 });

if (result2.errorCode === ErrorCode.TIMEOUT) {
console.log(`✅ Flag evaluation timed out as expected: ${result2.errorMessage}\n`);
} else {
console.log(`❌ Expected timeout, got: errorCode=${result2.errorCode}, value=${result2.value}\n`);
}

// Demo 5: AbortSignal cancellation
console.log('📝 Demo 5: Provider initialization cancellation with AbortSignal');
console.log('Starting provider initialization, will abort after 50ms...');
try {
const controller = new AbortController();
const cancelProvider = new SlowProvider(200);

// Abort after 50ms
setTimeout(() => {
console.log('🛑 Aborting provider initialization...');
controller.abort();
}, 50);

await OpenFeature.setProviderAndWait(cancelProvider, { signal: controller.signal });
console.log('❌ Unexpected success - should have been cancelled');
} catch (error) {
if (error.message.includes('cancelled')) {
console.log(`✅ Provider initialization cancelled as expected: ${error.message}\n`);
} else {
console.log(`❌ Unexpected error: ${error.message}\n`);
}
}

// Demo 6: Flag evaluation cancellation
console.log('📝 Demo 6: Flag evaluation cancellation with AbortSignal');
console.log('Starting flag evaluation, will abort after 50ms...');

// Set up a provider for this test
const evalTestProvider = new SlowProvider(200);
await OpenFeature.setProviderAndWait(evalTestProvider, { timeout: 300 });

const controller2 = new AbortController();
const client3 = OpenFeature.getClient();

// Abort after 50ms
setTimeout(() => {
console.log('🛑 Aborting flag evaluation...');
controller2.abort();
}, 50);

const result3 = await client3.getBooleanDetails('cancel-flag', false, {}, {
signal: controller2.signal
});

if (result3.errorCode === ErrorCode.GENERAL && result3.errorMessage.includes('cancelled')) {
console.log(`✅ Flag evaluation cancelled as expected: ${result3.errorMessage}\n`);
} else {
console.log(`❌ Expected cancellation, got: errorCode=${result3.errorCode}, value=${result3.value}\n`);
}

console.log('🎉 Demo completed! All timeout and cancellation features are working.');

} catch (error) {
console.error('❌ Demo failed with error:', error);
}
}

// Run the demonstration
demonstrateTimeoutAndCancellation().catch(console.error);
138 changes: 138 additions & 0 deletions demo-web-cancellation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Demonstration script for web client cancellation functionality
// Note: Web clients have synchronous evaluations but support cancellation checks

const { OpenFeature, ErrorCode, ProviderStatus } = require('./packages/web/dist/cjs');

// Mock web provider
class WebDemoProvider {
constructor(delay = 100) {
this.delay = delay;
this.metadata = { name: 'web-demo-provider' };
this.runsOn = 'client';
}

async initialize() {
console.log(`🔄 Web provider initializing (${this.delay}ms delay)...`);
await new Promise(resolve => setTimeout(resolve, this.delay));
console.log('✅ Web provider initialized successfully');
}

// Web providers have synchronous evaluation methods
resolveBooleanEvaluation(flagKey, defaultValue) {
console.log(`✅ Web flag "${flagKey}" evaluated synchronously to true`);
return { value: true, reason: 'STATIC' };
}

resolveStringEvaluation() { throw new Error('Not implemented'); }
resolveNumberEvaluation() { throw new Error('Not implemented'); }
resolveObjectEvaluation() { throw new Error('Not implemented'); }
}

// Mock console to capture warnings
const originalConsole = console;
const warnings = [];
console.warn = (...args) => {
warnings.push(args.join(' '));
originalConsole.warn(...args);
};

async function demonstrateWebCancellation() {
console.log('🌐 OpenFeature Web Client Cancellation Demo\n');

try {
// Demo 1: Provider initialization with timeout (should succeed)
console.log('📝 Demo 1: Web provider initialization with timeout');
console.log('Setting provider with 200ms timeout, provider takes 100ms...');
const provider = new WebDemoProvider(100);
await OpenFeature.setProviderAndWait(provider, { timeout: 200 });
console.log('✅ Web provider initialization succeeded within timeout\n');

// Demo 2: Synchronous flag evaluation (no timeout support)
console.log('📝 Demo 2: Synchronous flag evaluation (timeouts not supported)');
const client = OpenFeature.getClient();

// This should work but log a warning about timeout not being supported
const result1 = client.getBooleanDetails('web-flag', false, {
timeout: 1000 // This will trigger a warning
});

console.log(`✅ Flag evaluation completed: value=${result1.value}, errorCode=${result1.errorCode || 'none'}`);

// Check if warning was logged
const timeoutWarning = warnings.find(w => w.includes('Timeout option is not supported'));
if (timeoutWarning) {
console.log('✅ Timeout warning logged as expected for synchronous evaluation\n');
} else {
console.log('❌ Expected timeout warning was not logged\n');
}

// Demo 3: AbortSignal cancellation (should work)
console.log('📝 Demo 3: Flag evaluation cancellation with pre-aborted signal');
const controller = new AbortController();
controller.abort(); // Pre-abort the signal

const result2 = client.getBooleanDetails('cancelled-flag', false, {
signal: controller.signal
});

if (result2.errorCode === ErrorCode.GENERAL && result2.errorMessage.includes('cancelled')) {
console.log(`✅ Flag evaluation cancelled as expected: ${result2.errorMessage}`);
console.log(`✅ Default value returned: ${result2.value}\n`);
} else {
console.log(`❌ Expected cancellation, got: errorCode=${result2.errorCode}, value=${result2.value}\n`);
}

// Demo 4: Provider initialization cancellation
console.log('📝 Demo 4: Web provider initialization cancellation');
console.log('Starting provider initialization, will abort after 50ms...');

try {
const controller2 = new AbortController();
const slowProvider = new WebDemoProvider(200);

// Abort after 50ms
setTimeout(() => {
console.log('🛑 Aborting web provider initialization...');
controller2.abort();
}, 50);

await OpenFeature.setProviderAndWait(slowProvider, { signal: controller2.signal });
console.log('❌ Unexpected success - should have been cancelled');
} catch (error) {
if (error.message.includes('cancelled')) {
console.log(`✅ Web provider initialization cancelled as expected: ${error.message}\n`);
} else {
console.log(`❌ Unexpected error: ${error.message}\n`);
}
}

// Demo 5: Combined timeout warning and cancellation
console.log('📝 Demo 5: Combined timeout warning and cancellation');
const controller3 = new AbortController();
controller3.abort();

const result3 = client.getBooleanDetails('combined-test', false, {
timeout: 5000, // Should trigger warning
signal: controller3.signal // Should cause cancellation
});

if (result3.errorCode === ErrorCode.GENERAL && result3.errorMessage.includes('cancelled')) {
console.log('✅ Cancellation took precedence over timeout');

// Check if both the old warning and potential new warnings exist
const hasTimeoutWarnings = warnings.some(w => w.includes('Timeout option is not supported'));
if (hasTimeoutWarnings) {
console.log('✅ Timeout warnings were logged for synchronous operations\n');
}
}

console.log('🎉 Web client demo completed! Cancellation features are working correctly.');
console.log('📝 Note: Web clients use synchronous evaluations, so timeouts apply only to provider initialization.');

} catch (error) {
console.error('❌ Web demo failed with error:', error);
}
}

// Run the demonstration
demonstrateWebCancellation().catch(console.error);
45 changes: 44 additions & 1 deletion packages/server/src/client/internal/open-feature-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,10 @@ export class OpenFeatureClient implements Client {
this.shortCircuitIfNotReady();

// run the referenced resolver, binding the provider.
const resolution = await resolver.call(this._provider, flagKey, defaultValue, frozenContext, this._logger);
const resolution = await this.withTimeoutAndCancellation(
() => resolver.call(this._provider, flagKey, defaultValue, frozenContext, this._logger),
options
);

const resolutionDetails = {
...resolution,
Expand Down Expand Up @@ -457,4 +460,44 @@ export class OpenFeatureClient implements Client {
flagKey,
};
}

private async withTimeoutAndCancellation<T>(
evaluationFn: () => Promise<T>,
options: FlagEvaluationOptions
): Promise<T> {
const promises: Promise<T>[] = [evaluationFn()];

// Add timeout promise if timeout is specified
if (options.timeout && options.timeout > 0) {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
const timeoutError = new Error(`Evaluation timed out after ${options.timeout}ms`);
(timeoutError as OpenFeatureError).code = ErrorCode.TIMEOUT;
reject(timeoutError);
}, options.timeout);
});
promises.push(timeoutPromise);
}

// Add cancellation promise if AbortSignal is specified
if (options.signal && !options.signal.aborted) {
const cancellationPromise = new Promise<never>((_, reject) => {
const onAbort = () => {
const cancelError = new Error('Evaluation was cancelled');
(cancelError as OpenFeatureError).code = ErrorCode.GENERAL;
reject(cancelError);
};

options.signal!.addEventListener('abort', onAbort, { once: true });
});
promises.push(cancellationPromise);
} else if (options.signal?.aborted) {
// Signal is already aborted, reject immediately
const cancelError = new Error('Evaluation was cancelled');
(cancelError as OpenFeatureError).code = ErrorCode.GENERAL;
throw cancelError;
}

return Promise.race(promises);
}
}
23 changes: 23 additions & 0 deletions packages/server/src/evaluation/evaluation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@ import type { Hook } from '../hooks';
export interface FlagEvaluationOptions {
hooks?: Hook[];
hookHints?: HookHints;
/**
* Timeout in milliseconds for the flag evaluation. If the evaluation takes longer than this timeout,
* it will be rejected with a TIMEOUT error.
*/
timeout?: number;
/**
* AbortSignal for cancelling the flag evaluation. When the signal is aborted, the evaluation
* will be rejected with a GENERAL error.
*/
signal?: AbortSignal;
}

export interface ProviderWaitOptions {
/**
* Timeout in milliseconds for provider initialization. If the provider takes longer than this timeout
* to initialize, the promise will be rejected with a TIMEOUT error.
*/
timeout?: number;
/**
* AbortSignal for cancelling the provider initialization. When the signal is aborted,
* the promise will be rejected with a GENERAL error.
*/
signal?: AbortSignal;
}

export interface Features {
Expand Down
Loading
Loading