UNPKG

@doeixd/effectively

Version:

Enhanced async/await effects for TypeScript applications. Effectively provides resilient error handling, dependency injection, retry logic, timeouts, circuit breakers, and resource cleanup for Node.js and browser environments. Build testable async workflo

1,559 lines (1,555 loc) 133 kB
'use strict'; const unctx = require('unctx'); const node_async_hooks = require('node:async_hooks'); const neverthrow = require('neverthrow'); const seroval = require('seroval'); const web = require('seroval-plugins/web'); const worker_threads = require('worker_threads'); class ContextValidationError extends Error { _tag = "ContextValidationError"; field; expectedType; actualValue; constructor(field, expectedType, actualValue) { super( `Context validation failed for field '${field}': expected ${expectedType}, got ${typeof actualValue}` ); this.name = "ContextValidationError"; this.field = field; this.expectedType = expectedType; this.actualValue = actualValue; Object.setPrototypeOf(this, ContextValidationError.prototype); } } const noopLogger = { debug: () => { }, info: () => { }, warn: () => { }, error: () => { } }; class BacktrackSignal extends Error { _tag = "BacktrackSignal"; target; value; constructor(target, value) { super("Backtrack signal - this is not an error"); this.name = "BacktrackSignal"; this.target = target; this.value = value; Object.setPrototypeOf(this, BacktrackSignal.prototype); } } function isBacktrackSignal(error) { return error instanceof BacktrackSignal && error._tag === "BacktrackSignal"; } class WorkflowError extends Error { // @ts-ignore - TypeScript version doesn't recognize Error.cause as overridable cause; taskName; taskIndex; constructor(message, cause, taskName, taskIndex) { super(message); this.name = "WorkflowError"; this.cause = cause; this.taskName = taskName; this.taskIndex = taskIndex; Object.setPrototypeOf(this, WorkflowError.prototype); } } class ContextNotFoundError extends Error { constructor(message) { super( message || "Context not found. Make sure you are calling getContext() within a run() execution." ); this.name = "ContextNotFoundError"; } } const UNCTX_INSTANCE_SYMBOL = Symbol("__unctx_instance__"); let globalUnctx = null; function getGlobalUnctx() { if (!globalUnctx) { try { globalUnctx = unctx.createContext({ asyncContext: true, AsyncLocalStorage: node_async_hooks.AsyncLocalStorage }); } catch (error) { globalUnctx = unctx.createContext({ asyncContext: false }); } } return globalUnctx; } let memoizedGlobalDefaultContextObject = null; function getGlobalDefaultContextObjectSingleton() { if (!memoizedGlobalDefaultContextObject) { const controller = new AbortController(); memoizedGlobalDefaultContextObject = { scope: { signal: controller.signal } // Any other essential, minimal global defaults }; const globalUnctxInstance = getGlobalUnctx(); globalUnctxInstance.callAsync(memoizedGlobalDefaultContextObject, () => Promise.resolve()).catch((e) => { console.error( "[Effectively] Failed to initialize global unctx store with default object:", e ); }); } return memoizedGlobalDefaultContextObject; } let currentUnctxInstance = null; function _INTERNAL_setCurrentUnctxInstance(instance) { currentUnctxInstance = instance; } function _INTERNAL_getCurrentUnctxInstance() { return currentUnctxInstance; } function getContextOrUndefinedFromActiveInstance() { if (currentUnctxInstance) { try { const ctx = currentUnctxInstance.use(); return ctx; } catch (error) { console.warn( "[Effectively Internal] Error calling 'use()' on currentUnctxInstance. This might indicate an issue with AsyncLocalStorage or unctx setup. Returning undefined.", error ); return void 0; } } return void 0; } function createTaskFunction(fn, taskNameOrOptions) { const taskFnExecutor = (context, value) => { return Promise.resolve(fn(value)); }; let taskName; let idSymbol; if (typeof taskNameOrOptions === "string") { taskName = taskNameOrOptions; } else if (taskNameOrOptions) { taskName = taskNameOrOptions.name; idSymbol = taskNameOrOptions.idSymbol; } const finalName = taskName || fn.name || "anonymousTask"; Object.defineProperty(taskFnExecutor, "name", { value: finalName, configurable: true }); Object.defineProperty(taskFnExecutor, "__task_id", { value: idSymbol || Symbol(finalName), configurable: true }); const isAsyncGenerator = fn.constructor.name === "AsyncGeneratorFunction"; Object.defineProperty(taskFnExecutor, "__task_type", { value: isAsyncGenerator ? "stream" : "request", configurable: true }); return taskFnExecutor; } function defineTask(fn, taskNameOrOptions) { return createTaskFunction(fn, taskNameOrOptions); } function getContext() { const activeSpecificContext = getContextOrUndefinedFromActiveInstance(); if (activeSpecificContext) { return activeSpecificContext; } const globalDefaultInstance = getGlobalDefaultContextObjectSingleton(); return globalDefaultInstance; } function getContextSafe() { const activeSpecificContext = getContextOrUndefinedFromActiveInstance(); if (activeSpecificContext) { return neverthrow.ok(activeSpecificContext); } const globalDefaultInstance = getGlobalDefaultContextObjectSingleton(); return neverthrow.ok(globalDefaultInstance); } function getContextOrUndefined() { const activeSpecificContext = getContextOrUndefinedFromActiveInstance(); if (activeSpecificContext) { return activeSpecificContext; } const globalDefaultInstance = getGlobalDefaultContextObjectSingleton(); return globalDefaultInstance; } function isAsyncIterable(value) { return value != null && typeof value[Symbol.asyncIterator] === "function"; } async function runImpl(unctxInstance, workflow, initialValue, executionContext, options = {}) { const logger = executionContext?.logger || options.logger || noopLogger; const allSteps = workflow.__steps || [workflow]; let currentIndex = 0; let currentValue = initialValue; let backtrackCount = 0; const maxBacktracks = 1e3; const previousActiveUnctxInstance = _INTERNAL_getCurrentUnctxInstance(); _INTERNAL_setCurrentUnctxInstance(unctxInstance); let finalResult; try { while (currentIndex < allSteps.length) { if (executionContext.scope.signal.aborted) { throw new WorkflowError( "Workflow aborted", executionContext.scope.signal.reason, allSteps[currentIndex]?.name, currentIndex ); } try { const currentTask = allSteps[currentIndex]; currentValue = await unctxInstance.callAsync( executionContext, () => currentTask(executionContext, currentValue) ); currentIndex++; } catch (error) { if (isBacktrackSignal(error)) { backtrackCount++; if (backtrackCount > maxBacktracks) { throw new WorkflowError( `Maximum backtrack limit (${maxBacktracks}) exceeded.`, error ); } const targetIndex = allSteps.findIndex( (step) => step.__task_id === error.target.__task_id ); if (targetIndex === -1) { throw new WorkflowError("BacktrackSignal target not found.", error); } if (targetIndex > currentIndex) { throw new WorkflowError("Cannot backtrack forward.", error); } currentIndex = targetIndex; currentValue = error.value; continue; } if (error instanceof WorkflowError) { throw error; } throw new WorkflowError( `Task failed: ${error instanceof Error ? error.message : String(error)}`, error, allSteps[currentIndex]?.name, currentIndex ); } } finalResult = currentValue; const shouldThrow = !("throw" in options) || options.throw !== false; if (shouldThrow) return finalResult; return neverthrow.ok(finalResult); } catch (error) { const workflowErrorInstance = error instanceof WorkflowError ? error : new WorkflowError("Unhandled error in workflow execution", error); logger.error( `[runImpl] Workflow failed: ${workflowErrorInstance.message}`, workflowErrorInstance ); const scopeController = executionContext.scope.controller; if (scopeController && !scopeController.signal.aborted) { scopeController.abort(workflowErrorInstance); } const shouldThrow = !("throw" in options) || options.throw !== false; if (shouldThrow) throw workflowErrorInstance; return neverthrow.err(workflowErrorInstance); } finally { _INTERNAL_setCurrentUnctxInstance(previousActiveUnctxInstance); const scopeController = executionContext.scope.controller; if (scopeController && !scopeController.signal.aborted && !isAsyncIterable(finalResult)) { scopeController.abort( new DOMException("Workflow scope concluded.", "AbortError") ); } } } function run(workflow, initialValue, options = {}) { const { overrides, parentSignal } = options; const activeSpecificContext = getContextOrUndefinedFromActiveInstance(); const parentContext = activeSpecificContext || getGlobalDefaultContextObjectSingleton(); const unctxForThisRun = activeSpecificContext && activeSpecificContext[UNCTX_INSTANCE_SYMBOL] || getGlobalUnctx(); const controller = new AbortController(); const onParentAbort = () => controller.abort(parentSignal?.reason || parentContext.scope.signal.reason); const signalsToWatch = [parentContext.scope.signal, parentSignal].filter( Boolean ); signalsToWatch.forEach( (sig) => sig.aborted ? onParentAbort() : sig.addEventListener("abort", onParentAbort, { once: true }) ); const newScope = { signal: controller.signal, controller }; const executionContext = { ...parentContext, ...overrides, ...options.logger ? { logger: options.logger } : {}, scope: newScope }; Object.defineProperty(executionContext, UNCTX_INSTANCE_SYMBOL, { value: unctxForThisRun, enumerable: false, writable: false, configurable: true }); if (overrides) { Object.getOwnPropertySymbols(overrides).forEach((symKey) => { executionContext[symKey] = overrides[symKey]; }); } return runImpl( unctxForThisRun, workflow, initialValue, executionContext, options ).finally(() => { signalsToWatch.forEach( (sig) => sig.removeEventListener("abort", onParentAbort) ); }); } function getCurrentOrDefaultContext() { const activeSpecificContext = getContextOrUndefinedFromActiveInstance(); if (activeSpecificContext) { return activeSpecificContext; } return getGlobalDefaultContextObjectSingleton(); } async function _INTERNAL_provideImpl(unctxInstanceForThisScope, parentContextData, overrides, fn, options = {}) { const strategy = options.strategy || "spread"; const previousActiveUnctxInstance = _INTERNAL_getCurrentUnctxInstance(); _INTERNAL_setCurrentUnctxInstance(unctxInstanceForThisScope); let newExecutionContext; try { if (strategy === "proxy") { const currentOverrides = { ...overrides }; const proxyHandler = { get(target, prop, receiver) { if (prop === UNCTX_INSTANCE_SYMBOL) return unctxInstanceForThisScope; if (prop === "scope") return parentContextData.scope; if (Reflect.has(currentOverrides, prop)) return Reflect.get(currentOverrides, prop, receiver); return Reflect.get(parentContextData, prop, receiver); }, has(target, prop) { if (prop === UNCTX_INSTANCE_SYMBOL || prop === "scope") return true; return Reflect.has(currentOverrides, prop) || Reflect.has(parentContextData, prop); }, set(target, prop, value, receiver) { if (prop === "scope" || prop === UNCTX_INSTANCE_SYMBOL) { console.warn( `[Effectively.provideImpl] Attempted to set read-only property: ${String(prop)}` ); return false; } Reflect.set(currentOverrides, prop, value, receiver); return true; }, deleteProperty(target, prop) { if (prop === "scope" || prop === UNCTX_INSTANCE_SYMBOL) { console.warn( `[Effectively.provideImpl] Attempted to delete read-only property: ${String(prop)}` ); return false; } if (Reflect.has(currentOverrides, prop)) return Reflect.deleteProperty(currentOverrides, prop); return true; }, ownKeys(target) { const overrideKeys = Reflect.ownKeys(currentOverrides); const parentKeys = Reflect.ownKeys(parentContextData); return Array.from( /* @__PURE__ */ new Set([ ...overrideKeys, ...parentKeys, UNCTX_INSTANCE_SYMBOL, "scope" ]) ); }, getOwnPropertyDescriptor(target, prop) { if (prop === UNCTX_INSTANCE_SYMBOL) return { value: unctxInstanceForThisScope, writable: false, enumerable: false, configurable: true }; if (prop === "scope") { const parentDesc = Reflect.getOwnPropertyDescriptor( parentContextData, "scope" ); return parentDesc ? { ...parentDesc, writable: false, configurable: false } : void 0; } if (Reflect.has(currentOverrides, prop)) return Reflect.getOwnPropertyDescriptor(currentOverrides, prop); return Reflect.getOwnPropertyDescriptor(parentContextData, prop); } }; newExecutionContext = new Proxy( parentContextData, proxyHandler ); } else { newExecutionContext = { ...parentContextData, ...overrides, // Overrides win scope: parentContextData.scope // Explicitly inherit scope }; Object.defineProperty(newExecutionContext, UNCTX_INSTANCE_SYMBOL, { value: unctxInstanceForThisScope, enumerable: false, writable: false, configurable: false }); Object.getOwnPropertySymbols(overrides).forEach((symKey) => { if (symKey.toString() !== "Symbol(scope)" && symKey !== UNCTX_INSTANCE_SYMBOL) { newExecutionContext[symKey] = overrides[symKey]; } }); } return unctxInstanceForThisScope.callAsync(newExecutionContext, fn); } finally { _INTERNAL_setCurrentUnctxInstance(previousActiveUnctxInstance); } } function provide(overrides, fn, options) { const activeSpecificContext = getContextOrUndefinedFromActiveInstance(); let unctxInstanceForThisScope; let parentContextData; if (activeSpecificContext && activeSpecificContext[UNCTX_INSTANCE_SYMBOL]) { unctxInstanceForThisScope = activeSpecificContext[UNCTX_INSTANCE_SYMBOL]; parentContextData = activeSpecificContext; } else { unctxInstanceForThisScope = getGlobalUnctx(); parentContextData = getGlobalDefaultContextObjectSingleton(); } return _INTERNAL_provideImpl( unctxInstanceForThisScope, parentContextData, overrides, fn, options || { strategy: "spread" } ); } function provideWithProxy(overrides, fn) { const activeSpecificContext = getContextOrUndefinedFromActiveInstance(); let unctxInstanceForThisScope; let parentContextData; if (activeSpecificContext && UNCTX_INSTANCE_SYMBOL in activeSpecificContext) { unctxInstanceForThisScope = activeSpecificContext[UNCTX_INSTANCE_SYMBOL]; parentContextData = activeSpecificContext; } else { unctxInstanceForThisScope = getGlobalUnctx(); parentContextData = getGlobalDefaultContextObjectSingleton(); } return _INTERNAL_provideImpl( unctxInstanceForThisScope, parentContextData, overrides, fn, { strategy: "proxy" } // Explicitly use proxy ); } function defineTaskLocal(fn, taskNameOrOptions) { const activeContext = getContextOrUndefinedFromActiveInstance(); if (!activeContext) { throw new ContextNotFoundError( "defineTaskLocal requires an active specific context. It cannot be called at the top level without an established context from run() or provide()." ); } return createTaskFunction(fn, taskNameOrOptions); } function getContextLocal() { const activeContext = getContextOrUndefinedFromActiveInstance(); if (!activeContext) { throw new ContextNotFoundError( "getContextLocal requires an active specific context. No global fallback is used." ); } return activeContext; } function getContextSafeLocal() { const activeContext = getContextOrUndefinedFromActiveInstance(); if (!activeContext) { return neverthrow.err( new ContextNotFoundError( "getContextSafeLocal requires an active specific context. No global fallback is used." ) ); } return neverthrow.ok(activeContext); } function getContextOrUndefinedLocal() { return getContextOrUndefinedFromActiveInstance(); } function runLocal(workflow, initialValue, options = {}) { const activeContext = getContextOrUndefinedFromActiveInstance(); if (!activeContext) { const error = new ContextNotFoundError( "runLocal requires an active specific context. No global fallback is used." ); const shouldThrow = !("throw" in options) || options.throw !== false; if (shouldThrow) { throw error; } return Promise.resolve( neverthrow.err(new WorkflowError("Context not found for runLocal", error)) ); } const unctxInstanceForThisRun = activeContext[UNCTX_INSTANCE_SYMBOL]; if (!unctxInstanceForThisRun) { const internalError = new Error( "[Effectively Internal Error] runLocal found an active context that is not tagged with its UNCTX_INSTANCE_SYMBOL. Cannot proceed with proper context management." ); console.error(internalError, { activeContext }); const shouldThrow = !("throw" in options) || options.throw !== false; if (shouldThrow) { throw internalError; } return Promise.resolve( neverthrow.err(new WorkflowError(internalError.message, internalError)) ); } return runImpl( unctxInstanceForThisRun, workflow, initialValue, activeContext, // The active context itself is the base (already includes its own defaults + previous overrides) options ); } function provideLocal(overrides, fn, options) { const activeContext = getContextOrUndefinedFromActiveInstance(); if (!activeContext) { throw new ContextNotFoundError( "provideLocal requires an active specific context. No global fallback is used." ); } const unctxInstanceForThisScope = activeContext[UNCTX_INSTANCE_SYMBOL]; if (!unctxInstanceForThisScope) { throw new Error( // This should be a more severe internal error "[Effectively Internal Error] provideLocal found an active context that is not tagged with its UNCTX_INSTANCE_SYMBOL. Cannot create nested scope correctly." ); } return _INTERNAL_provideImpl( unctxInstanceForThisScope, activeContext, // The current active context is the parent overrides, fn, options ); } const defineTaskGlobal = (fn, taskNameOrOptions) => { return createTaskFunction( fn, taskNameOrOptions ); }; const getContextGlobal = () => { return getGlobalDefaultContextObjectSingleton(); }; function runGlobal(workflow, initialValue, options = {}) { const globalUnctx2 = getGlobalUnctx(); const globalDefaults = getGlobalDefaultContextObjectSingleton(); return runImpl( globalUnctx2, workflow, initialValue, globalDefaults, // Base data for this run is the global defaults options ); } const provideGlobal = (overrides, fn, options) => { const globalUnctx2 = getGlobalUnctx(); const globalDefaults = getGlobalDefaultContextObjectSingleton(); return _INTERNAL_provideImpl( globalUnctx2, globalDefaults, // Parent data is the global default object overrides, fn, options // Pass along strategy options ); }; function validateContext(schema, context) { if (!context || typeof context !== "object") { return { isOk: () => false, isErr: () => true, error: new ContextValidationError("root", "object", context) }; } const ctx = context; for (const [field, validator] of Object.entries(schema)) { const value = ctx[field]; if (!validator(value)) { return { isOk: () => false, isErr: () => true, error: new ContextValidationError(field, "valid type", value) }; } } return { isOk: () => true, isErr: () => false, value: context }; } function mergeContexts(contextA, contextB) { return { ...contextA, ...contextB, scope: contextA.scope // Always preserve the original scope }; } function createContextTransformer(transformer) { return (ctx) => { const transformed = transformer(ctx); return { ...transformed, scope: ctx.scope // Always preserve the scope }; }; } function useContextProperty(key) { const context = getContext(); return context[key]; } function requireContextProperties(...requirements) { const context = getContext(); const missing = []; for (const requirement of requirements) { if (!(requirement in context) || context[requirement] === void 0) { missing.push(String(requirement)); } } if (missing.length > 0) { throw new ContextValidationError( missing.join(", "), "defined properties", "undefined" ); } return context; } function withContextEnhancement(enhancement, task) { return async (context, value) => { const enhancedContext = mergeContexts(context, enhancement); return task(enhancedContext, value); }; } function createInjectionToken(description) { return Symbol(description); } function inject(token) { const context = getCurrentOrDefaultContext(); const value = context[token]; if (value === void 0) { throw new Error(`Injection token ${token.toString()} not found in context`); } return value; } function injectOptional(token) { const context = getCurrentOrDefaultContext(); return context[token]; } function createContextProvider(token) { const Provider = (value, task) => { return async (context, input) => { const enhancedContext = { ...context, [token]: value }; return task(enhancedContext, input); }; }; const useValue = () => inject(token); return { Provider, useValue }; } function withScope(providers, task) { return async (context, value) => { let enhancedContext = { ...context }; for (const provider of providers) { enhancedContext = { ...enhancedContext, [provider.provide]: provider.value }; } return task(enhancedContext, value); }; } function withDependencies(dependencies) { return function configureDependencies(taskFactory) { const task = taskFactory(dependencies); return task; }; } function createLazyDependency(factory) { let instance; let loading; return async () => { if (instance !== void 0) { return instance; } if (loading) { return loading; } loading = Promise.resolve(factory()).then((result) => { instance = result; loading = void 0; return result; }); return loading; }; } function createContext(defaultContextDataForC, asyncLocalStorage = node_async_hooks.AsyncLocalStorage) { let localUnctxInstance; try { localUnctxInstance = unctx.createContext({ asyncContext: true, AsyncLocalStorage: asyncLocalStorage }); } catch (e) { console.warn( `[Effectively createContext for ${defaultContextDataForC?.constructor?.name || "C"}] AsyncLocalStorage not available or failed. Falling back to synchronous unctx. Context might not propagate as expected across all async boundaries for this instance.` ); localUnctxInstance = unctx.createContext({ asyncContext: false }); } const toolsDefaultContextData = defaultContextDataForC; const getContextSpecific = () => { try { const ctx = localUnctxInstance.use(); if (!ctx) { throw new ContextNotFoundError( `Specific context (type: ${toolsDefaultContextData?.constructor?.name || "C"}) not found. Ensure getContext() is called within a run() or provide() scope managed by these specific tools.` ); } return ctx; } catch (error) { if (error instanceof ContextNotFoundError) throw error; throw new ContextNotFoundError( `Error retrieving specific context: ${error instanceof Error ? error.message : String(error)}` ); } }; const getContextSafeSpecific = () => { try { return neverthrow.ok(getContextSpecific()); } catch (e) { return neverthrow.err(e); } }; const getContextOrUndefinedSpecific = () => { try { return localUnctxInstance.use(); } catch (error) { return void 0; } }; const getContextOrGlobalSpecific = () => { const activeLocalContext = localUnctxInstance.use(); if (activeLocalContext) { return activeLocalContext; } return getGlobalDefaultContextObjectSingleton(); }; const defineTaskSpecific = (fn, taskNameOrOptions) => { return createTaskFunction(fn, taskNameOrOptions); }; async function provideSpecific(overrides, fn, options) { const parentDataForProvide = localUnctxInstance.use(); const baseContextForThisProvide = parentDataForProvide ? parentDataForProvide : { ...toolsDefaultContextData, scope: { signal: new AbortController().signal } }; return _INTERNAL_provideImpl( localUnctxInstance, baseContextForThisProvide, overrides, fn, options ); } async function runSpecific(workflow, initialValue, options = {}) { const { overrides, parentSignal } = options; const controller = new AbortController(); const onParentAbort = () => controller.abort(parentSignal?.reason); if (parentSignal) { if (parentSignal.aborted) { onParentAbort(); } else { parentSignal.addEventListener("abort", onParentAbort, { once: true }); } } const newScope = { signal: controller.signal, controller }; const executionContext = { ...toolsDefaultContextData, // Base properties for this context type ...overrides, // Runtime overrides for this specific run scope: newScope // The new scope always overrides any default }; Object.defineProperty(executionContext, UNCTX_INSTANCE_SYMBOL, { value: localUnctxInstance, enumerable: false, writable: false, configurable: true }); if (overrides) { Object.getOwnPropertySymbols(overrides).forEach((symKey) => { executionContext[symKey] = overrides[symKey]; }); } return runImpl( localUnctxInstance, workflow, // We can now safely assert the workflow is compatible with C initialValue, executionContext, // Pass the final, constructed object. options ).finally(() => { if (parentSignal) { parentSignal.removeEventListener("abort", onParentAbort); } }); } const injectSpecific = (token) => { const context = getContextSpecific(); const value = context[token]; if (value === void 0) { throw new Error( `[ContextTools(${toolsDefaultContextData?.constructor?.name || "C"})] Injection token ${token.toString()} not found.` ); } return value; }; const injectOptionalSpecific = (token) => { try { const context = getContextSpecific(); return context[token]; } catch { return void 0; } }; return { run: runSpecific, getContext: getContextSpecific, getContextSafe: getContextSafeSpecific, getContextOrUndefined: getContextOrUndefinedSpecific, getContextOrGlobal: getContextOrGlobalSpecific, defineTask: defineTaskSpecific, provide: provideSpecific, inject: injectSpecific, injectOptional: injectOptionalSpecific }; } const run$1 = { __proto__: null, BacktrackSignal: BacktrackSignal, ContextNotFoundError: ContextNotFoundError, ContextValidationError: ContextValidationError, WorkflowError: WorkflowError, _INTERNAL_getCurrentUnctxInstance: _INTERNAL_getCurrentUnctxInstance, _INTERNAL_setCurrentUnctxInstance: _INTERNAL_setCurrentUnctxInstance, createContext: createContext, createContextProvider: createContextProvider, createContextTransformer: createContextTransformer, createInjectionToken: createInjectionToken, createLazyDependency: createLazyDependency, defineTask: defineTask, defineTaskGlobal: defineTaskGlobal, defineTaskLocal: defineTaskLocal, getContext: getContext, getContextGlobal: getContextGlobal, getContextLocal: getContextLocal, getContextOrUndefined: getContextOrUndefined, getContextOrUndefinedFromActiveInstance: getContextOrUndefinedFromActiveInstance, getContextOrUndefinedLocal: getContextOrUndefinedLocal, getContextSafe: getContextSafe, getContextSafeLocal: getContextSafeLocal, inject: inject, injectOptional: injectOptional, isBacktrackSignal: isBacktrackSignal, mergeContexts: mergeContexts, noopLogger: noopLogger, provide: provide, provideGlobal: provideGlobal, provideLocal: provideLocal, provideWithProxy: provideWithProxy, requireContextProperties: requireContextProperties, run: run, runGlobal: runGlobal, runLocal: runLocal, useContextProperty: useContextProperty, validateContext: validateContext, withContextEnhancement: withContextEnhancement, withDependencies: withDependencies, withScope: withScope }; const deepEqual = (a, b) => { if (a === b) return true; if (a && b && typeof a === "object" && typeof b === "object") { if (a.constructor !== b.constructor) return false; if (Array.isArray(a)) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false; return true; } const keys = Object.keys(a); if (keys.length !== Object.keys(b).length) return false; for (const key of keys) { if (!Object.prototype.hasOwnProperty.call(b, key) || !deepEqual(a[key], b[key])) return false; } return true; } return a !== a && b !== b; }; function pipe(value, ...fns) { let currentValue = value; for (const fn of fns) { currentValue = fn(currentValue); } return currentValue; } function flow(...fns) { const { length } = fns; if (length === 0) { return (arg) => arg; } if (length === 1) { return fns[0]; } return (...args) => { let currentValue = fns[0](...args); for (let i = 1; i < length; i++) { currentValue = fns[i](currentValue); } return currentValue; }; } function isGeneratorFunction(fn) { return Object.prototype.toString.call(fn) === "[object GeneratorFunction]"; } function runLiftedGenerator(genFn) { return async (context, value) => { const gen = genFn(value); return new Promise((resolve, reject) => { function iterateGenerator(nextValueToPass) { try { const result = gen.next(nextValueToPass); if (result.done) { resolve(result.value); return; } Promise.resolve(result.value).then( (resolvedYieldedValue) => iterateGenerator(resolvedYieldedValue) ).catch((errFromYieldedPromise) => { try { const resultFromThrow = gen.throw(errFromYieldedPromise); if (resultFromThrow.done) { resolve(resultFromThrow.value); return; } Promise.resolve(resultFromThrow.value).then((res) => iterateGenerator(res)).catch((innerErr) => reject(innerErr)); } catch (errorThrownByGenerator) { reject(errorThrownByGenerator); } }); } catch (errorDuringNext) { reject(errorDuringNext); } } iterateGenerator(); }); }; } function createWorkflow(...steps) { if (steps.length === 0) { const identityTask = async (context, v) => v; Object.defineProperty(identityTask, "name", { value: "identityWorkflow", configurable: true }); Object.defineProperty(identityTask, "__task_id", { value: Symbol("identityWorkflow"), configurable: true, enumerable: false, writable: false }); return identityTask; } const toTask = (stepOrFn) => { if (typeof stepOrFn !== "function") { throw new Error( `Invalid workflow step: expected a function, Task, or generator function, but got ${typeof stepOrFn}.` ); } if (stepOrFn.__task_id) { return stepOrFn; } if (isGeneratorFunction(stepOrFn)) { const liftedGenTask = runLiftedGenerator( stepOrFn ); Object.defineProperty(liftedGenTask, "name", { value: stepOrFn.name || "liftedGeneratorTask", configurable: true }); return liftedGenTask; } const fnStr = String(stepOrFn?.toString?.()); if (stepOrFn.length === 2 && (fnStr.includes("context") || fnStr.includes("ctx") || fnStr.includes("scope"))) { const rawTaskWrapper = async (context, value) => { const result = stepOrFn(context, value); return Promise.resolve(result); }; Object.defineProperty(rawTaskWrapper, "name", { value: stepOrFn.name || "liftedContextAwareFn", configurable: true }); return rawTaskWrapper; } const liftedPlainTask = async (context, value) => { const result = stepOrFn(value); return Promise.resolve(result); }; Object.defineProperty(liftedPlainTask, "name", { value: stepOrFn.name || "liftedValueTransformFn", configurable: true }); return liftedPlainTask; }; const allLiftedTasks = steps.map(toTask); const composedWorkflow = async (context, initialValue) => { let currentValue = initialValue; for (const task of allLiftedTasks) { if (context.scope?.signal?.aborted) { throw new DOMException("Workflow aborted", "AbortError"); } currentValue = await task(context, currentValue); } return currentValue; }; Object.defineProperty(composedWorkflow, "__steps", { value: Object.freeze([...allLiftedTasks]), configurable: true, enumerable: false, writable: false }); const stepNames = allLiftedTasks.map((t) => t.name || "anonymous_step").join("_then_"); Object.defineProperty(composedWorkflow, "name", { value: `workflow(${stepNames || "empty"})`, configurable: true }); Object.defineProperty(composedWorkflow, "__task_id", { value: Symbol(`workflow_${stepNames || "empty"}`), configurable: true, enumerable: false, writable: false }); return composedWorkflow; } const chain = createWorkflow; function fromValue(value) { return async (context, _) => value; } function fromPromise(promise) { return async (context, _) => promise; } function fromPromiseFn(fn) { return async (context, _) => fn(context); } function map(f) { return async (context, value) => { const result = f(value, context); return result instanceof Promise ? result : Promise.resolve(result); }; } function flatMap(f) { const flatMapTask = async (context, value) => { const nextTask = f(value, context); if (typeof nextTask !== "function" || !nextTask.name === void 0) { throw new Error("flatMap function f must return a valid Task."); } return nextTask(context, value); }; Object.defineProperty(flatMapTask, "name", { value: `flatMap(${f.name || "anonymousFn"})`, configurable: true }); return flatMapTask; } function mapTask(task, f) { return async (context, value) => { const result = await task(context, value); return f(result); }; } function andThenTask(task, f) { const andThenCompositionTask = async (context, inputValue) => { const intermediateResult = await task(context, inputValue); const nextTask = f(intermediateResult); if (typeof nextTask !== "function" || nextTask.name === void 0) { throw new Error("andThenTask function f must return a valid Task."); } return nextTask(context, intermediateResult); }; Object.defineProperty(andThenCompositionTask, "name", { value: `andThen(${task.name || "anonymousTask"}, ${f.name || "anonymousFn"})`, configurable: true }); return andThenCompositionTask; } function pick(...keys) { return async (context, value) => { const newObj = {}; for (const key of keys) if (Object.prototype.hasOwnProperty.call(value, key)) newObj[key] = value[key]; return newObj; }; } function when(predicate, task) { return async (context, value) => { return await predicate(value, context) ? task(context, value) : value; }; } function unless(predicate, task) { return when(async (v, c) => !await predicate(v, c), task); } function doWhile(task, predicate) { return async (context, initialValue) => { let currentValue = initialValue; while (await predicate(currentValue, context)) { if (context.scope.signal.aborted) throw new DOMException("Aborted", "AbortError"); currentValue = await task(context, currentValue); } return currentValue; }; } function tap(f) { return async (context, value) => { await f(value, context); return value; }; } function sleep(ms) { return async (context, _) => { if (context.scope.signal.aborted) throw new DOMException("Aborted", "AbortError"); return new Promise((resolve, reject) => { const timer = setTimeout(resolve, ms); context.scope.signal.addEventListener( "abort", () => { clearTimeout(timer); reject(new DOMException("Aborted", "AbortError")); }, { once: true } ); }); }; } function tapError(task, onErrorFn, errorConstructor) { const tappedTaskLogic = async (context, value) => { try { return await task(context, value); } catch (error) { if (isBacktrackSignal(error)) { throw error; } const shouldHandle = !errorConstructor || error instanceof errorConstructor; if (shouldHandle) { try { await onErrorFn(error, context); } catch (tapFnError) { const logger = context.logger || console; logger.error( `[tapError] An error occurred within the error-tapping function itself while handling another error. The original error will be re-thrown.`, { tapFnError, originalError: error } ); } } throw error; } }; Object.defineProperty(tappedTaskLogic, "name", { value: `tapError(${task.name || "anonymousTask"}, ${onErrorFn.name || "onErrorFn"})`, configurable: true }); if (task.__task_id) { Object.defineProperty(tappedTaskLogic, "__task_id", { value: task.__task_id, configurable: true, enumerable: false, writable: false }); } if (task.__steps) { Object.defineProperty(tappedTaskLogic, "__steps", { value: task.__steps, configurable: true, enumerable: false, writable: false }); } return tappedTaskLogic; } function defaultAttemptErrorMapper(caughtError) { if (caughtError instanceof Error) { return caughtError; } return new Error( String( caughtError !== void 0 ? caughtError : "Unknown error during attempt" ) ); } function attempt(task, mapErrorToE = defaultAttemptErrorMapper) { const attemptTaskLogic = async (context, value) => { try { const resultValue = await task(context, value); return neverthrow.ok(resultValue); } catch (error) { if (isBacktrackSignal(error)) { throw error; } return neverthrow.err(mapErrorToE(error)); } }; Object.defineProperty(attemptTaskLogic, "name", { value: `attempt(${task.name || "anonymousTask"})`, configurable: true }); if (task.__task_id) { Object.defineProperty(attemptTaskLogic, "__task_id", { value: task.__task_id, // Or a new Symbol configurable: true, enumerable: false, writable: false }); } if (task.__steps) { Object.defineProperty(attemptTaskLogic, "__steps", { value: task.__steps, configurable: true, enumerable: false, writable: false }); } return attemptTaskLogic; } function withRetry(task, options = {}) { const { attempts = 3, delayMs = 100, backoff = "exponential", // Default shouldRetry to check for non-BacktrackSignal errors. shouldRetry = (error) => !isBacktrackSignal(error), jitter = "none" } = options; if (attempts <= 0) { throw new Error("Retry attempts must be positive."); } const applyJitter = (currentDelay) => { if (jitter === "none") { return currentDelay; } if (jitter === "full") { return currentDelay + Math.random() * currentDelay; } return jitter(currentDelay); }; const retryTaskLogic = async (context, value) => { const logger = context.logger || noopLogger; let lastError = new Error("Task was not attempted."); for (let attemptCount = 0; attemptCount < attempts; attemptCount++) { try { if (context.scope.signal.aborted) { throw context.scope.signal.reason ?? new DOMException("Aborted before attempt", "AbortError"); } return await task(context, value); } catch (error) { lastError = error; if (isBacktrackSignal(error) || !shouldRetry(error) || attemptCount === attempts - 1) { throw error; } let currentDelay = delayMs; if (backoff === "exponential" && attemptCount > 0) { currentDelay = delayMs * 2 ** attemptCount; } currentDelay = applyJitter(currentDelay); logger.warn( `Task '${task.name || "anonymous"}' failed (attempt ${attemptCount + 1}/${attempts}). Retrying in ${Math.round(currentDelay)}ms...`, { originalError: error } // Log the specific error that caused the retry ); try { await sleep(currentDelay)(context, null); } catch (sleepError) { if (sleepError instanceof DOMException && sleepError.name === "AbortError") { logger.warn( `[withRetry - ${task.name || "anonymous"}] Retry delay aborted (attempt ${attemptCount + 1}/${attempts}). Re-throwing original task error.`, { originalError: error, abortReason: sleepError } ); throw lastError; } throw sleepError; } } } throw lastError; }; const enhancedTask = retryTaskLogic; Object.defineProperty(enhancedTask, "name", { value: `withRetry(${task.name || "anonymous"}, attempts=${attempts})`, configurable: true }); if (task.__task_id) { Object.defineProperty(enhancedTask, "__task_id", { value: task.__task_id, // Or a new Symbol for distinctness if enhancers create new backtrackable steps configurable: true, enumerable: false, writable: false }); } if (task.__steps) { Object.defineProperty(enhancedTask, "__steps", { value: task.__steps, configurable: true, enumerable: false, writable: false }); } return enhancedTask; } function withName(task, name) { const namedTask = (ctx, val) => task(ctx, val); Object.defineProperty(namedTask, "name", { value: name, configurable: true }); if (task.__task_id) namedTask.__task_id = task.__task_id; if (task.__steps) namedTask.__steps = task.__steps; return namedTask; } function memoize(task, options) { const cache = /* @__PURE__ */ new Map(); const getCacheKey = options?.cacheKeyFn ? options.cacheKeyFn : (value) => { if (typeof value === "object" && value !== null) { return value; } return value; }; const findInCache = (value, generatedKey) => { if (options?.cacheKeyFn || typeof value !== "object" || value === null) { return cache.get(generatedKey); } for (const [k, v] of cache.entries()) { if (deepEqual(k, value)) { return v; } } return void 0; }; const memoizedTask = async (context, value) => { const cacheKey = getCacheKey(value); const cachedPromise = findInCache(value, cacheKey); if (cachedPromise) { return cachedPromise; } const newPromise = task(context, value).catch((err2) => { if (options?.cacheKeyFn || typeof value !== "object" || value === null) { cache.delete(cacheKey); } else { for (const k of cache.keys()) { if (deepEqual(k, value)) { cache.delete(k); break; } } } throw err2; }); cache.set(cacheKey, newPromise); return newPromise; }; Object.defineProperty(memoizedTask, "name", { value: `memoized(${task.name || "anonymous"})`, configurable: true }); if (task.__task_id) { memoizedTask.__task_id = task.__task_id; } if (task.__steps) { memoizedTask.__steps = task.__steps; } return memoizedTask; } function once(task) { let promise = null; return async (context, value) => { if (promise) return promise; promise = task(context, value); return promise; }; } class TimeoutError extends Error { _tag = "TimeoutError"; // For easier type guarding if needed constructor(taskName, durationMs) { super(`Task '${taskName}' timed out after ${durationMs}ms.`); this.name = "TimeoutError"; Object.setPrototypeOf(this, TimeoutError.prototype); } } function withTimeout(task, durationMs) { if (durationMs < 0) { throw new Error("Timeout durationMs must be non-negative."); } const taskNameForError = task.name || "anonymous"; const timeoutEnhancedTask = (context, value) => { return new Promise((resolve, reject) => { let timerId = void 0; const cleanup = () => { if (timerId !== void 0) { clearTimeout(timerId); timerId = void 0; } context.scope.signal.removeEventListener("abort", onAbort); }; const onAbort = () => { cleanup(); reject( context.scope.signal.reason ?? new DOMException("Aborted", "AbortError") ); }; const onTimeout = () => { cleanup(); reject(new TimeoutError(taskNameForError, durationMs)); }; if (context.scope.signal.aborted) { return onAbort(); } timerId = setTimeout(onTimeout, durationMs); context.scope.signal.addEventListener("abort", onAbort, { once: true }); task(context, value).then((result) => { cleanup(); resolve(result); }).catch((err2) => { cleanup(); reject(err2); }); }); }; Object.defineProperty(timeoutEnhancedTask, "name", { value: `withTimeout(${task.name || "anonymous"}, ${durationMs}ms)`, configurable: true }); if (task.__task_id) { Object.defineProperty(timeoutEnhancedTask, "__task_id", { value: task.__task_id, configurable: true, enumerable: false, writable: false }); } if (task.__steps) { Object.defineProperty(timeoutEnhancedTask, "__steps", { value: task.__steps, configurable: true, enumerable: false, writable: false }); } return timeoutEnhancedTask; } const STATE_TOOLS_KEY = Symbol("effectively.stateTools"); function useState(context) { const tools = context[STATE_TOOLS_KEY]; if (!tools) { throw new Error( "useState() can only be used within a task wrapped by withState(). Ensure withState() is an ancestor enhancer in your workflow." ); } return tools; } function withState(initialState, task) { const statefulTaskLogic = async (context, value) => { let state = typeof initialState === "function" ? initialState(value) : initialState; const tools = { getState: () => state, setState: (updater) => { state = typeof updater === "function" ? updater(state) : updater; } }; const { provide } = await Promise.resolve().then(function () { return run$1; }); const result = await provide( { [STATE_TOOLS_KEY]: tools }, () => task(context, value) // The context for the task will be enhanced by `provide` ); return { result, state }; }; Object.defineProperty(statefulTaskLogic, "name", { value: `withState(${task.name || "anonymousTask"})`, configurable: true }); if (task.__task_id) { Object.defineProperty(statefulTaskLogic, "__task_id", { value: task.__task_id, configurable: true, enumerable: false, writable: false }); } return statefulTaskLogic; } function withThrottle(task, options) { const { limit, intervalMs } = options; if (limit <= 0 || intervalMs <= 0) { throw new Error("Throttle limit and intervalMs must be positive."); } const callQueue = []; let currentTokens = limit; let isProcessingQueue = false; const refillIntervalTime = Math.max(1, intervalMs / limit); const refillInterval = setInterval(() => { if (currentTokens < limit) { currentTokens++; } processQueue(); }, refillIntervalTime); if (typeof refillInterval.unref === "function") { refillInterval.unref(); } const processQueue = async () => { if (isProcessingQueue) return; isProcessingQueue = true; while (callQueue.length > 0 && currentTokens > 0) { const nextCall = callQueue[0]; if (nextCall.context.scope.signal.aborted) { callQueue.shift(); nextCall.reject( nextCall.context.scope.signal.reas