@asaidimu/runtime 1.0.5 → 1.0.6
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/index.js +1 -1
- package/index.mjs +1 -1
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@asaidimu/utils-artifacts"),t=require("@asaidimu/utils-database"),n=require("@asaidimu/utils-error"),r=require("@asaidimu/utils-logger"),i=require("@asaidimu/utils-pipeline"),a=require("@asaidimu/utils-sanitize"),o=require("@asaidimu/utils-store"),s=require("@asaidimu/utils-sync");const c=`__abort__`,l=`__signal__`;var u=class{envs=new Map;use(e,t){return this.envs.set(e,t),this}get(e){return this.envs.get(`global`)?.[e]}list(){return{...this.envs.get(`global`)}}scope(e){let t=this.envs.get(e),n=this.envs.get(`global`);return{get:e=>{if(t&&e in t)return t[e];if(n&&e in n)return n[e]},list:()=>{let e={...n};if(t)for(let n of Object.keys(t))e[n]=t[n];return e}}}},d=class{workflowId;concurrency;capacity;logger;inFlight=0;queueSize=0;closed=!1;constructor(e,t,n,r){this.workflowId=e,this.concurrency=t,this.capacity=n,this.logger=r}async accept(e,t,n){if(this.closed)return{status:`rejected`,reason:`queue_full`};if(this.inFlight<this.concurrency)return this.inFlight++,this.run(e,t,n),{status:`accepted`};if(this.queueSize>=this.capacity)return{status:`rejected`,reason:`queue_full`};this.queueSize++;let r=this.queueSize;return await new Promise(e=>{let t=()=>{if(this.closed){e();return}this.inFlight<this.concurrency?(this.inFlight++,this.queueSize--,e()):setTimeout(t,0)};setTimeout(t,0)}),this.closed?{status:`rejected`,reason:`queue_full`}:(this.run(e,t,n),{status:`queued`,position:r})}settle(e){}close(){this.closed=!0}async run(e,t,n){try{await n(e,t)}finally{this.inFlight--}}},f=class{workflowId;capacity;logger;serializer;queueDepth=0;closed=!1;constructor(e,t,n){this.workflowId=e,this.capacity=t,this.logger=n,this.serializer=new s.Serializer({capacity:t,yieldMode:`macrotask`})}async accept(e,t,n){if(this.closed||this.queueDepth>=this.capacity)return{status:`rejected`,reason:`queue_full`};let r=this.queueDepth;return this.queueDepth++,await this.serializer.do(async()=>{try{await n(e,t)}finally{this.queueDepth--}}),r===0?{status:`accepted`}:{status:`queued`,position:r}}settle(e){}close(){this.closed=!0,this.serializer.close()}},p=class{workflowId;onActive;replacementGracePeriod;deliverSignal;logger;state=`idle`;activeRunId;settleResolve;closed=!1;constructor(e,t,n,r,i){this.workflowId=e,this.onActive=t,this.replacementGracePeriod=n,this.deliverSignal=r,this.logger=i}async accept(e,t,n){if(this.closed)return{status:`rejected`,reason:`singleton_active`};if(this.state===`idle`){this.state=`starting`;let r=await n(e,t);return r||(this.state=`idle`,this.activeRunId=void 0),{status:`accepted`,runId:r}}if(this.state===`terminating`)return{status:`rejected`,reason:`terminating`};switch(this.onActive){case`drop`:return{status:`rejected`,reason:`singleton_active`};case`signal`:{let e=this.activeRunId??``;return e?this.deliverSignal(e,t):this.logger.warn(`[WorkflowRuntime] SingletonLoop "${this.workflowId}": signal requested but activeRunId unknown (state="${this.state}"). Event "${t.type}" dropped.`),{status:`signalled`,runId:e}}case`replace`:return this.handleReplace(e,t,n)}}settle(e){(this.state===`starting`||e===this.activeRunId)&&(this.activeRunId=e,this.state=`running`),this.state===`terminating`&&e===this.activeRunId&&this.settleResolve?.()}close(){this.closed=!0,this.settleResolve?.()}async handleReplace(e,t,n){if(this.state=`terminating`,this.activeRunId&&this.logger.info(`[WorkflowRuntime] SingletonLoop "${this.workflowId}": replacing run "${this.activeRunId}" with new event "${t.type}".`),await Promise.race([new Promise(e=>{this.settleResolve=e}),new Promise(e=>setTimeout(e,this.replacementGracePeriod))]),this.settleResolve=void 0,this.closed)return this.state=`idle`,{status:`rejected`,reason:`singleton_active`};this.state=`starting`,this.activeRunId=void 0;let r=await n(e,t);return r||(this.state=`idle`),{status:`accepted`,runId:r}}},m=class{workflowId;onActive;logger;activeRunId;pending;closed=!1;constructor(e,t,n){this.workflowId=e,this.onActive=t,this.logger=n}async accept(e,t,n){if(this.closed)return{status:`rejected`,reason:`exclusive_active`};if(!this.activeRunId){let r=await n(e,t);return r&&(this.activeRunId=r),{status:`accepted`,runId:r}}return this.onActive===`reject`?{status:`rejected`,reason:`exclusive_active`}:(this.pending={triggerId:e,event:t,spawn:n},{status:`queued`,position:1})}settle(e){if(e===this.activeRunId&&(this.activeRunId=void 0,this.pending&&!this.closed)){let{triggerId:e,event:t,spawn:n}=this.pending;this.pending=void 0,n(e,t).then(e=>{e&&(this.activeRunId=e)})}}close(){this.closed=!0,this.pending=void 0}},h=class{registrations=new Map;byEventType=new Map;busSubscriptions=new Map;bus;resumeCallback;constructor(e){this.bus=e.bus,this.resumeCallback=e.resume}register(e,t){let n=this.registrations.get(e);n||(n=new Map,this.registrations.set(e,n));let r={runId:e,descriptor:t,queue:[],parked:!1};n.set(t.eventType,r);let i=this.byEventType.get(t.eventType);i||(i=new Set,this.byEventType.set(t.eventType,i)),i.add(e),this.acquireBusSubscription(t.eventType)}cancel(e,t){let n=this.registrations.get(e);n&&(n.delete(t),n.size===0&&this.registrations.delete(e),this.removeFromReverseIndex(e,t),this.releaseBusSubscription(t))}onRunPaused(e){let t=this.registrations.get(e);if(!t)return null;for(let e of t.values())if(e.queue.length>0)return e.queue.shift();for(let e of t.values())e.parked=!0;return null}onRunEnded(e){let t=this.registrations.get(e);if(t){for(let[n]of t)this.removeFromReverseIndex(e,n),this.releaseBusSubscription(n);this.registrations.delete(e)}}onEvent(e,t){let n=this.byEventType.get(e);if(!(!n||n.size===0))for(let r of n){let n=this.registrations.get(r);if(!n)continue;let i=n.get(e);if(!i||!this.evaluate(i.descriptor.conditions,t))continue;let a=this.resolve(i.descriptor,t);i.parked?(i.parked=!1,this.resumeCallback(r,a.patch)):i.queue.push(a)}}evaluate(e,t){for(let n of e){let e=this.getField(t,n.field);if(n.op===`exists`){if(e==null)return!1;continue}if(e==null)return!1;try{switch(n.op){case`==`:if(e!=n.value)return!1;break;case`!=`:if(e==n.value)return!1;break;case`>`:if(!(e>n.value))return!1;break;case`>=`:if(!(e>=n.value))return!1;break;case`<`:if(!(e<n.value))return!1;break;case`<=`:if(!(e<=n.value))return!1;break;default:return!1}}catch{return!1}}return!0}resolve(e,t){return{eventPayload:t,patch:{...e.patch??{},__watch_event__:t}}}getField(e,t){let n=t.split(`.`),r=e;for(let e of n){if(r==null||typeof r!=`object`&&!Array.isArray(r))return;let t=Number(e);r=!isNaN(t)&&Array.isArray(r)?r[t]:r[e]}return r}acquireBusSubscription(e){if(this.busSubscriptions.has(e))return;let t=this.bus.subscribe(e,t=>{this.onEvent(e,t)});this.busSubscriptions.set(e,t)}releaseBusSubscription(e){let t=this.byEventType.get(e);if(t&&t.size>0)return;let n=this.busSubscriptions.get(e);if(n){try{n()}catch{}this.busSubscriptions.delete(e)}}removeFromReverseIndex(e,t){let n=this.byEventType.get(t);n&&(n.delete(e),n.size===0&&this.byEventType.delete(t))}},g=class{bus;storeRegistry;timelineStore;pauseService;serviceContainer;serviceStore;workflows=new Map;index=new Map;subscriptions=new Map;pausedRuns=new Map;logger;abortUnsubscribe;signalUnsubscribe;constructor(t){this.bus=t.bus,this.storeRegistry=t.storeRegistry,this.timelineStore=t.timelineStore,this.logger=t.logger?t.logger:new r.Logger([],{scope:`workflow-runtime`}),this.pauseService=new h({bus:this.bus,resume:async(e,t)=>{this.resume(e,t)}}),this.serviceStore=new o.ReactiveDataStore({env:{global:t.env??{}}}),this.serviceContainer=new e.ArtifactContainer(this.serviceStore);for(let e of t.services??[]){if(e.id===`__pause_service__`)throw new n.SystemError({code:n.ErrorCodes.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Service id "__pause_service__" is reserved for the built-in PauseService and cannot be redefined.`});if(e.id===`__env__`)throw new n.SystemError({code:n.ErrorCodes.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Service id "__env__" is reserved for the built-in environment-variable service and cannot be redefined.`});this.serviceContainer.register({key:e.id,factory:e.factory,scope:`singleton`})}this.serviceContainer.register({key:`__pause_service__`,factory:()=>this.pauseService,scope:`singleton`}),this.serviceContainer.register({key:`__env_service__`,factory:async({use:e})=>{let t=await e(e=>e.select(e=>e.env)),n=new u;for(let[e,r]of Object.entries(t??{}))n.use(e,r);return n},scope:`singleton`}),this.serviceContainer.register({key:`__scoped_env_service__`,paramKey:e=>`env:${e.workflowId}`,factory:async({use:e,params:t})=>(await e(e=>e.require(`__env_service__`))).scope(t.workflowId),scope:`singleton`}),this.serviceContainer.register({key:`__sanitizer__`,paramKey:e=>`sanitizer:${e.workflowId}`,factory:async({use:e,params:t})=>{let n=(await e(e=>e.require(`__scoped_env_service__`,{workflowId:t.workflowId}))).list(),r=(0,a.newSecureDefaultConfig)();return r.patterns=[...r.patterns,...(0,a.commonEnvPatterns)([],n)],new a.DocumentSanitizer(r,t.workflowId)},scope:`singleton`}),this.abortUnsubscribe=this.bus.subscribe(c,e=>{this.abortRun(e.run)}),this.signalUnsubscribe=this.bus.subscribe(l,e=>{this.signal(e.runId,e.patch)})}static async createTestTimelineStore(e=`test-timeline-database`){return new i.TimelineStore(await(0,t.DatabaseConnection)({database:e,validate:!0,predicates:{},enableTelemetry:!0},t.createIndexedDbStore))}async register(e,t){if(this.workflows.has(e.id))throw new n.SystemError({code:n.ErrorCodes.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Workflow "${e.id}" is already registered. Call deregister("${e.id}") before registering again.`});let r=i.PipelineRegistry.get(`workflow:${e.id}`,{onExpired:(t,n)=>{this.logger.info(`[WorkflowRuntime] Run "${t}" pause-window expired for workflow "${e.id}". Checkpoint: ${n.pipelineId}.`)},onExportFailed:(t,n)=>{this.logger.error(`[WorkflowRuntime] Deferred export failed for run "${t}" in workflow "${e.id}":`,n)}}),a=async e=>{let n=e.ok?e.value.runId:e.error?.runId;try{await t.onComplete(e)}finally{n&&(s.executionContext.settle(n),this.pauseService.onRunEnded(n),this.pausedRuns.delete(n)),r.prune(),await t.onCleanup?.()}},o=this.buildExecutionContext(e.id,t.mode),s;s={workflow:e,mode:t.mode,executionContext:o,factories:new Map,registry:r,hooks:{onPrepare:t.onPrepare,onComplete:a,onResume:t.onResume,onDispatch:t.onDispatch,onCleanup:t.onCleanup}},this.workflows.set(e.id,s),this.serviceStore.set(t=>({env:{...t.env??{},[e.id]:e.env??{}}}));let c=this.serviceContainer.scope(e.id);if(e.services&&e.services.length>0)for(let t of e.services)t.scope===`run`||t.scope===`transient`||c.register({key:t.id,factory:t.factory,scope:`singleton`});s.container=c;for(let[t,n]of Object.entries(e.triggers)){let r={workflowId:e.id,triggerId:t,trigger:n},i=this.index.get(n.event);i||(i=new Set,this.index.set(n.event,i)),i.add(r),await this.acquireBusSubscription(n.event)}}deregister(e){let t=this.workflows.get(e);if(t){for(let n of Object.values(t.workflow.triggers)){let t=this.index.get(n.event);if(t){for(let n of t)n.workflowId===e&&t.delete(n);t.size===0&&this.index.delete(n.event)}this.releaseBusSubscription(n.event)}for(let t of[`__scoped_env_service__`,`__sanitizer__`])try{this.serviceContainer.unregister(t,{workflowId:e})}catch{}this.serviceStore.set(t=>({env:{...t.env??{},[e]:o.DELETE_SYMBOL}})),t.container?.dispose(),t.executionContext.close(),i.PipelineRegistry.destroy(`workflow:${e}`),t.factories.clear(),this.workflows.delete(e)}}hasWorkflow(e){return this.workflows.has(e)}listWorkflows(){return Array.from(this.workflows.keys())}async stop(){for(let e of Array.from(this.workflows.keys()))this.deregister(e);try{this.abortUnsubscribe()}catch{}try{this.signalUnsubscribe()}catch{}}registry(e){return this.workflows.get(e)?.registry}info(e,t){return this.workflows.get(e)?.registry.get(t)}list(){let e=[];for(let[t,n]of this.workflows)for(let r of n.registry.list())e.push({...r,workflowId:t});return e}async invoke(e,t,r){let i=this.workflows.get(e);if(!i)throw new n.SystemError({code:n.ErrorCodes.NOT_FOUND.code,message:`[WorkflowRuntime] invoke() failed: workflow "${e}" is not registered.`});if(!i.workflow.pipelines[t])throw new n.SystemError({code:n.ErrorCodes.NOT_FOUND.code,message:`[WorkflowRuntime] invoke() failed: no pipeline definition for workflow "${e}" trigger "${t}".`});let a=this.generateRunId(e,t),o=await this.executePipeline(i,t,a,r);return o.ok&&o.value.status===`paused`?(this.pausedRuns.set(a,{workflowId:e,triggerId:t}),await this.drainWatchQueue(a)):await i.hooks.onComplete(o),o.ok?n.Result.ok(o.value,{runId:a,triggerId:t}):n.Result.fail(o.error,{runId:a,triggerId:t})}async resume(e,t={}){let r=this.pausedRuns.get(e);if(!r)throw new n.SystemError({code:n.ErrorCodes.NOT_FOUND.code,message:`[WorkflowRuntime] resume() failed: run "${e}" is not in the paused runs registry. It may have already completed, been aborted, or the runId is incorrect.`});let{workflowId:i,triggerId:a}=r,s=this.workflows.get(i);if(!s)throw new n.SystemError({code:n.ErrorCodes.RESOURCE_RELEASED.code,message:`[WorkflowRuntime] resume() failed: workflow "${i}" is no longer registered. The workflow may have been deregistered while the run was paused.`});let c=this.getOrCreateFactory(s,a),l=s.registry.hold(e),u=await this.createRecorder(s),d=u?[u.asLogSink()]:void 0,f;try{let t=await c.resume(e,{sinks:d});if(!t.ok)throw t.error;f=t.value}catch(t){throw new n.SystemError({code:n.ErrorCodes.INTERNAL_ERROR.code,message:`[WorkflowRuntime] resume() failed to reconstruct context for run "${e}"`,cause:t})}l||this.extendContextContainer(f,s),Object.keys(t).length>0&&f.write(t),f.write({__watch__:o.DELETE_SYMBOL});let p=await this.withRecorder(f,async()=>(await s.hooks.onResume?.(f),await f.run()),u);if(p.ok&&p.value.status===`paused`)await this.drainWatchQueue(e);else{let t=this.workflows.get(i);if(t)try{await t.hooks.onComplete(p)}catch(t){this.logger.error(`[WorkflowRuntime] onComplete hook threw during resume for run "${e}":`,t)}}return p.ok?n.Result.ok(p.value,{runId:e,triggerId:a}):n.Result.fail(p.error,{runId:e,triggerId:a})}async signal(e,t){for(let n of this.workflows.values()){let r=n.registry.get(e);if(r?.context){r.context.write(t);return}}}dispatch(e,t){let n=this.index.get(e);if(!n||n.size===0)return;let r={type:e,payload:t,timestamp:Date.now()};for(let e of n){let t=!0;if(e.trigger.predicate)try{t=e.trigger.predicate(r)}catch(n){this.logger.error(`[WorkflowRuntime] Predicate threw for workflow "${e.workflowId}" trigger "${e.triggerId}":`,n),t=!1}if(!t){this.workflows.get(e.workflowId)?.hooks.onDispatch?.({status:`rejected`,reason:`filtered`});continue}let n=this.workflows.get(e.workflowId);n&&n.executionContext.accept(e.triggerId,r,(e,t)=>this.spawnRun(n,e,t)).then(e=>{n.hooks.onDispatch?.(e)}).catch(t=>{this.logger.error(`[WorkflowRuntime] ExecutionContext.accept() threw for workflow "${e.workflowId}":`,t)})}}async createRecorder(e){if(!this.timelineStore)return;let t;try{t=await this.serviceContainer.require(`__sanitizer__`,{workflowId:e.workflow.id})}catch{}return new i.TimelineRecorder(this.timelineStore,{sanitizer:t})}async withRecorder(e,t,n){if(!n)return t();await n.attach(e),await(0,i.yieldToEventLoop)();try{return await t()}finally{await n.detach().catch(()=>{})}}async executePipeline(e,t,r,i){let a=this.getOrCreateFactory(e,t),o=await this.createRecorder(e),s=o?[o.asLogSink()]:void 0,c;try{c=await a.prepare(void 0,r,{sinks:s}),await c.store.set({__trigger_event__:i})}catch(e){let t=e instanceof n.SystemError?e:new n.SystemError({code:`PIPELINE_PREPARE_FAILED`,message:e instanceof Error?e.message:String(e)});return n.Result.fail(t)}return this.extendContextContainer(c,e),this.withRecorder(c,async()=>(await e.hooks.onPrepare(c),await c.run()),o)}async spawnRun(e,t,n){let r=this.generateRunId(e.workflow.id,t);return this.executePipeline(e,t,r,n).then(async n=>{if(n.ok&&n.value.status===`paused`){this.pausedRuns.set(r,{workflowId:e.workflow.id,triggerId:t}),await this.drainWatchQueue(r);return}try{await e.hooks.onComplete(n)}catch(e){this.logger.error(`[WorkflowRuntime] onComplete hook threw for run "${r}":`,e)}}).catch(e=>{this.logger.error(`[WorkflowRuntime] Pipeline execution failed for run "${r}":`,e)}),r}async drainWatchQueue(e){let t=this.pauseService.onRunPaused(e);if(t!==null)try{await this.resume(e,t.patch)}catch(t){this.logger.error(`[WorkflowRuntime] drainWatchQueue: resume() failed for run "${e}":`,t),this.pauseService.onRunEnded(e),this.pausedRuns.delete(e)}}async abortRun(e){for(let t of this.workflows.values()){let n=t.registry.get(e);if(n?.context){try{n.context.abort()}catch(t){this.logger.error(`[WorkflowRuntime] Error aborting run "${e}":`,t)}break}}this.pauseService.onRunEnded(e),this.pausedRuns.delete(e)}buildExecutionContext(e,t){let n=this.logger.child({scope:`${t.type}:execution-context`});switch(t.type){case`transient`:return new d(e,t.concurrency??10,t.capacity??1e3,n);case`serialized`:return new f(e,t.capacity??1e3,n);case`singleton_loop`:return new p(e,t.onActive??`drop`,t.replacementGracePeriod??5e3,(e,t)=>this.signal(e,{__singleton_event__:t}),n);case`exclusive`:return new m(e,t.onActive??`reject`,n)}}async acquireBusSubscription(e){let t=this.subscriptions.get(e);t||(t=new s.SharedResource(()=>this.bus.subscribe(e,t=>{this.dispatch(e,t)}),t=>{try{t?.(),this.subscriptions.delete(e)}catch{}},{gracePeriod:`sync`}),this.subscriptions.set(e,t)),await t.acquire()}releaseBusSubscription(e){this.subscriptions.get(e)?.release()}async extendContextContainer(e,t){t.container&&e.container.extend(t.container),e.container.extend(this.serviceContainer);let r=await this.serviceContainer.require(`__scoped_env_service__`,{workflowId:t.workflow.id});e.container.register({key:`__env__`,factory:()=>r,scope:`singleton`});for(let t of[`__env_service__`,`__scoped_env_service__`])e.container.register({key:t,factory:()=>{throw new n.SystemError({code:n.ErrorCodes.UNAUTHENTICATED.code,message:`"${t}" is an internal artifact and cannot be accessed from pipeline steps.`})},scope:`singleton`,lazy:!1});if(t.workflow.services)for(let n of t.workflow.services){if(n.scope===`workflow`||n.scope===void 0)continue;let t=n.scope===`run`?`singleton`:`transient`;e.container.register({key:n.id,factory:n.factory,scope:t})}}getOrCreateFactory(e,t){let r=e.factories.get(t);if(r)return r;let a=e.workflow.pipelines[t];if(!a)throw new n.SystemError({code:n.ErrorCodes.NOT_FOUND.code,message:`[WorkflowRuntime] No pipeline definition found for workflow "${e.workflow.id}" trigger "${t}". Ensure workflow.pipelines["${t}"] is populated by the compiler.`});let o=new i.PipelineFactory(a,{storeFactory:async e=>this.storeRegistry.get(e),registry:e.registry});return e.factories.set(t,o),o}generateRunId(e,t){return`${e}:${t}:${Date.now()}:${Math.random().toString(36).slice(2)}`}};exports.ABORT_EVENT=c,exports.SIGNAL_EVENT=l,exports.WorkflowRuntime=g;
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@asaidimu/utils-artifacts"),t=require("@asaidimu/utils-database"),n=require("@asaidimu/utils-error"),r=require("@asaidimu/utils-logger"),i=require("@asaidimu/utils-pipeline"),a=require("@asaidimu/utils-sanitize"),o=require("@asaidimu/utils-store"),s=require("@asaidimu/utils-sync");const c=`__abort__`,l=`__signal__`;var u=class{envs=new Map;use(e,t){return this.envs.set(e,t),this}get(e){return this.envs.get(`global`)?.[e]}list(){return{...this.envs.get(`global`)}}scope(e){let t=this.envs.get(e),n=this.envs.get(`global`);return{get:e=>{if(t&&e in t)return t[e];if(n&&e in n)return n[e]},list:()=>{let e={...n};if(t)for(let n of Object.keys(t))e[n]=t[n];return e}}}},d=class{workflowId;concurrency;capacity;logger;inFlight=0;queueSize=0;closed=!1;constructor(e,t,n,r){this.workflowId=e,this.concurrency=t,this.capacity=n,this.logger=r}async accept(e,t,n){if(this.closed)return{status:`rejected`,reason:`queue_full`};if(this.inFlight<this.concurrency)return this.inFlight++,this.run(e,t,n),{status:`accepted`};if(this.queueSize>=this.capacity)return{status:`rejected`,reason:`queue_full`};this.queueSize++;let r=this.queueSize;return await new Promise(e=>{let t=()=>{if(this.closed){e();return}this.inFlight<this.concurrency?(this.inFlight++,this.queueSize--,e()):setTimeout(t,0)};setTimeout(t,0)}),this.closed?{status:`rejected`,reason:`queue_full`}:(this.run(e,t,n),{status:`queued`,position:r})}settle(e){}close(){this.closed=!0}async run(e,t,n){try{await n(e,t)}finally{this.inFlight--}}},f=class{workflowId;capacity;logger;serializer;queueDepth=0;closed=!1;constructor(e,t,n){this.workflowId=e,this.capacity=t,this.logger=n,this.serializer=new s.Serializer({capacity:t,yieldMode:`macrotask`})}async accept(e,t,n){if(this.closed||this.queueDepth>=this.capacity)return{status:`rejected`,reason:`queue_full`};let r=this.queueDepth;return this.queueDepth++,await this.serializer.do(async()=>{try{await n(e,t)}finally{this.queueDepth--}}),r===0?{status:`accepted`}:{status:`queued`,position:r}}settle(e){}close(){this.closed=!0,this.serializer.close()}},p=class{workflowId;onActive;replacementGracePeriod;deliverSignal;logger;state=`idle`;activeRunId;settleResolve;closed=!1;constructor(e,t,n,r,i){this.workflowId=e,this.onActive=t,this.replacementGracePeriod=n,this.deliverSignal=r,this.logger=i}async accept(e,t,n){if(this.closed)return{status:`rejected`,reason:`singleton_active`};if(this.state===`idle`){this.state=`starting`;let r=await n(e,t);return r||(this.state=`idle`,this.activeRunId=void 0),{status:`accepted`,runId:r}}if(this.state===`terminating`)return{status:`rejected`,reason:`terminating`};switch(this.onActive){case`drop`:return{status:`rejected`,reason:`singleton_active`};case`signal`:{let e=this.activeRunId??``;return e?this.deliverSignal(e,t):this.logger.warn(`[WorkflowRuntime] SingletonLoop "${this.workflowId}": signal requested but activeRunId unknown (state="${this.state}"). Event "${t.type}" dropped.`),{status:`signalled`,runId:e}}case`replace`:return this.handleReplace(e,t,n)}}settle(e){(this.state===`starting`||e===this.activeRunId)&&(this.activeRunId=e,this.state=`running`),this.state===`terminating`&&e===this.activeRunId&&this.settleResolve?.()}close(){this.closed=!0,this.settleResolve?.()}async handleReplace(e,t,n){if(this.state=`terminating`,this.activeRunId&&this.logger.info(`[WorkflowRuntime] SingletonLoop "${this.workflowId}": replacing run "${this.activeRunId}" with new event "${t.type}".`),await Promise.race([new Promise(e=>{this.settleResolve=e}),new Promise(e=>setTimeout(e,this.replacementGracePeriod))]),this.settleResolve=void 0,this.closed)return this.state=`idle`,{status:`rejected`,reason:`singleton_active`};this.state=`starting`,this.activeRunId=void 0;let r=await n(e,t);return r||(this.state=`idle`),{status:`accepted`,runId:r}}},m=class{workflowId;onActive;logger;activeRunId;pending;closed=!1;constructor(e,t,n){this.workflowId=e,this.onActive=t,this.logger=n}async accept(e,t,n){if(this.closed)return{status:`rejected`,reason:`exclusive_active`};if(!this.activeRunId){let r=await n(e,t);return r&&(this.activeRunId=r),{status:`accepted`,runId:r}}return this.onActive===`reject`?{status:`rejected`,reason:`exclusive_active`}:(this.pending={triggerId:e,event:t,spawn:n},{status:`queued`,position:1})}settle(e){if(e===this.activeRunId&&(this.activeRunId=void 0,this.pending&&!this.closed)){let{triggerId:e,event:t,spawn:n}=this.pending;this.pending=void 0,n(e,t).then(e=>{e&&(this.activeRunId=e)})}}close(){this.closed=!0,this.pending=void 0}},h=class{registrations=new Map;byEventType=new Map;busSubscriptions=new Map;bus;resumeCallback;constructor(e){this.bus=e.bus,this.resumeCallback=e.resume}register(e,t){let n=this.registrations.get(e);n||(n=new Map,this.registrations.set(e,n));let r={runId:e,descriptor:t,queue:[],parked:!1};n.set(t.eventType,r);let i=this.byEventType.get(t.eventType);i||(i=new Set,this.byEventType.set(t.eventType,i)),i.add(e),this.acquireBusSubscription(t.eventType)}cancel(e,t){let n=this.registrations.get(e);n&&(n.delete(t),n.size===0&&this.registrations.delete(e),this.removeFromReverseIndex(e,t),this.releaseBusSubscription(t))}onRunPaused(e){let t=this.registrations.get(e);if(!t)return null;for(let e of t.values())if(e.queue.length>0)return e.queue.shift();for(let e of t.values())e.parked=!0;return null}onRunEnded(e){let t=this.registrations.get(e);if(t){for(let[n]of t)this.removeFromReverseIndex(e,n),this.releaseBusSubscription(n);this.registrations.delete(e)}}onEvent(e,t){let n=this.byEventType.get(e);if(!(!n||n.size===0))for(let r of n){let n=this.registrations.get(r);if(!n)continue;let i=n.get(e);if(!i||!this.evaluate(i.descriptor.conditions,t))continue;let a=this.resolve(i.descriptor,t);i.parked?(i.parked=!1,this.resumeCallback(r,a.patch)):i.queue.push(a)}}evaluate(e,t){for(let n of e){let e=this.getField(t,n.field);if(n.op===`exists`){if(e==null)return!1;continue}if(e==null)return!1;try{switch(n.op){case`==`:if(e!=n.value)return!1;break;case`!=`:if(e==n.value)return!1;break;case`>`:if(!(e>n.value))return!1;break;case`>=`:if(!(e>=n.value))return!1;break;case`<`:if(!(e<n.value))return!1;break;case`<=`:if(!(e<=n.value))return!1;break;default:return!1}}catch{return!1}}return!0}resolve(e,t){return{eventPayload:t,patch:{...e.patch??{},__watch_event__:t}}}getField(e,t){let n=t.split(`.`),r=e;for(let e of n){if(r==null||typeof r!=`object`&&!Array.isArray(r))return;let t=Number(e);r=!isNaN(t)&&Array.isArray(r)?r[t]:r[e]}return r}acquireBusSubscription(e){if(this.busSubscriptions.has(e))return;let t=this.bus.subscribe(e,t=>{this.onEvent(e,t)});this.busSubscriptions.set(e,t)}releaseBusSubscription(e){let t=this.byEventType.get(e);if(t&&t.size>0)return;let n=this.busSubscriptions.get(e);if(n){try{n()}catch{}this.busSubscriptions.delete(e)}}removeFromReverseIndex(e,t){let n=this.byEventType.get(t);n&&(n.delete(e),n.size===0&&this.byEventType.delete(t))}},g=class{bus;storeRegistry;timelineStore;pauseService;serviceContainer;serviceStore;workflows=new Map;index=new Map;subscriptions=new Map;pausedRuns=new Map;logger;abortUnsubscribe;signalUnsubscribe;constructor(t){this.bus=t.bus,this.storeRegistry=t.storeRegistry,this.timelineStore=t.timelineStore,this.logger=t.logger?t.logger:new r.Logger([],{scope:`workflow-runtime`}),this.pauseService=new h({bus:this.bus,resume:async(e,t)=>{this.resume(e,t)}}),this.serviceStore=new o.ReactiveDataStore({env:{global:t.env??{}}}),this.serviceContainer=new e.ArtifactContainer(this.serviceStore);for(let e of t.services??[]){if(e.id===`__pause_service__`)throw new n.SystemError({code:n.ErrorCodes.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Service id "__pause_service__" is reserved for the built-in PauseService and cannot be redefined.`});if(e.id===`__env__`)throw new n.SystemError({code:n.ErrorCodes.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Service id "__env__" is reserved for the built-in environment-variable service and cannot be redefined.`});this.serviceContainer.register({key:e.id,factory:e.factory,scope:`singleton`})}this.serviceContainer.register({key:`__pause_service__`,factory:()=>this.pauseService,scope:`singleton`}),this.serviceContainer.register({key:`__env_service__`,factory:async({use:e})=>{let t=await e(e=>e.select(e=>e.env)),n=new u;for(let[e,r]of Object.entries(t??{}))n.use(e,r);return n},scope:`singleton`}),this.serviceContainer.register({key:`__scoped_env_service__`,paramKey:e=>`env:${e.workflowId}`,factory:async({use:e,params:t})=>(await e(e=>e.require(`__env_service__`))).scope(t.workflowId),scope:`singleton`}),this.serviceContainer.register({key:`__sanitizer__`,paramKey:e=>`sanitizer:${e.workflowId}`,factory:async({use:e,params:t})=>{let n=(await e(e=>e.require(`__scoped_env_service__`,{workflowId:t.workflowId}))).list(),r=(0,a.newSecureDefaultConfig)();return r.patterns=[...r.patterns,...(0,a.commonEnvPatterns)([],n)],new a.DocumentSanitizer(r,t.workflowId)},scope:`singleton`}),this.abortUnsubscribe=this.bus.subscribe(c,e=>{this.abortRun(e.run)}),this.signalUnsubscribe=this.bus.subscribe(l,e=>{this.signal(e.runId,e.patch)})}static async createTestTimelineStore(e=`test-timeline-database`){return new i.TimelineStore(await(0,t.DatabaseConnection)({database:e,validate:!0,predicates:{},enableTelemetry:!0},t.createIndexedDbStore))}async register(e,t){if(this.workflows.has(e.id))throw new n.SystemError({code:n.ErrorCodes.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Workflow "${e.id}" is already registered. Call deregister("${e.id}") before registering again.`});let r=i.PipelineRegistry.get(`workflow:${e.id}`,{onExpired:(t,n)=>{this.logger.info(`[WorkflowRuntime] Run "${t}" pause-window expired for workflow "${e.id}". Checkpoint: ${n.pipelineId}.`)},onExportFailed:(t,n)=>{this.logger.error(`[WorkflowRuntime] Deferred export failed for run "${t}" in workflow "${e.id}":`,n)}}),a=async e=>{let n=e.ok?e.value.runId:e.error?.runId;try{n&&(s.executionContext.settle(n),this.pauseService.onRunEnded(n),this.pausedRuns.delete(n)),r.prune()}finally{await t.onComplete(e),await t.onCleanup?.()}},o=this.buildExecutionContext(e.id,t.mode),s;s={workflow:e,mode:t.mode,executionContext:o,factories:new Map,registry:r,hooks:{onPrepare:t.onPrepare,onComplete:a,onResume:t.onResume,onDispatch:t.onDispatch,onCleanup:t.onCleanup}},this.workflows.set(e.id,s),this.serviceStore.set(t=>({env:{...t.env??{},[e.id]:e.env??{}}}));let c=this.serviceContainer.scope(e.id);if(e.services&&e.services.length>0)for(let t of e.services)t.scope===`run`||t.scope===`transient`||c.register({key:t.id,factory:t.factory,scope:`singleton`});s.container=c;for(let[t,n]of Object.entries(e.triggers)){let r={workflowId:e.id,triggerId:t,trigger:n},i=this.index.get(n.event);i||(i=new Set,this.index.set(n.event,i)),i.add(r),await this.acquireBusSubscription(n.event)}}deregister(e){let t=this.workflows.get(e);if(t){for(let n of Object.values(t.workflow.triggers)){let t=this.index.get(n.event);if(t){for(let n of t)n.workflowId===e&&t.delete(n);t.size===0&&this.index.delete(n.event)}this.releaseBusSubscription(n.event)}for(let t of[`__scoped_env_service__`,`__sanitizer__`])try{this.serviceContainer.unregister(t,{workflowId:e})}catch{}this.serviceStore.set(t=>({env:{...t.env??{},[e]:o.DELETE_SYMBOL}})),t.container?.dispose(),t.executionContext.close(),i.PipelineRegistry.destroy(`workflow:${e}`),t.factories.clear(),this.workflows.delete(e)}}hasWorkflow(e){return this.workflows.has(e)}listWorkflows(){return Array.from(this.workflows.keys())}async stop(){for(let e of Array.from(this.workflows.keys()))this.deregister(e);try{this.abortUnsubscribe()}catch{}try{this.signalUnsubscribe()}catch{}}registry(e){return this.workflows.get(e)?.registry}info(e,t){return this.workflows.get(e)?.registry.get(t)}list(){let e=[];for(let[t,n]of this.workflows)for(let r of n.registry.list())e.push({...r,workflowId:t});return e}async invoke(e,t,r){let i=this.workflows.get(e);if(!i)throw new n.SystemError({code:n.ErrorCodes.NOT_FOUND.code,message:`[WorkflowRuntime] invoke() failed: workflow "${e}" is not registered.`});if(!i.workflow.pipelines[t])throw new n.SystemError({code:n.ErrorCodes.NOT_FOUND.code,message:`[WorkflowRuntime] invoke() failed: no pipeline definition for workflow "${e}" trigger "${t}".`});let a=this.generateRunId(e,t),o=await this.executePipeline(i,t,a,r);return o.ok&&o.value.status===`paused`?(this.pausedRuns.set(a,{workflowId:e,triggerId:t}),await this.drainWatchQueue(a)):await i.hooks.onComplete(o),o.ok?n.Result.ok(o.value,{runId:a,triggerId:t}):n.Result.fail(o.error,{runId:a,triggerId:t})}async resume(e,t={}){let r=this.pausedRuns.get(e);if(!r)throw new n.SystemError({code:n.ErrorCodes.NOT_FOUND.code,message:`[WorkflowRuntime] resume() failed: run "${e}" is not in the paused runs registry. It may have already completed, been aborted, or the runId is incorrect.`});let{workflowId:i,triggerId:a}=r,s=this.workflows.get(i);if(!s)throw new n.SystemError({code:n.ErrorCodes.RESOURCE_RELEASED.code,message:`[WorkflowRuntime] resume() failed: workflow "${i}" is no longer registered. The workflow may have been deregistered while the run was paused.`});let c=this.getOrCreateFactory(s,a),l=s.registry.hold(e),u=await this.createRecorder(s),d=u?[u.asLogSink()]:void 0,f;try{let t=await c.resume(e,{sinks:d});if(!t.ok)throw t.error;f=t.value}catch(t){throw new n.SystemError({code:n.ErrorCodes.INTERNAL_ERROR.code,message:`[WorkflowRuntime] resume() failed to reconstruct context for run "${e}"`,cause:t})}l||this.extendContextContainer(f,s),Object.keys(t).length>0&&f.write(t),f.write({__watch__:o.DELETE_SYMBOL});let p=await this.withRecorder(f,async()=>(await s.hooks.onResume?.(f),await f.run()),u);if(p.ok&&p.value.status===`paused`)await this.drainWatchQueue(e);else{let t=this.workflows.get(i);if(t)try{await t.hooks.onComplete(p)}catch(t){this.logger.error(`[WorkflowRuntime] onComplete hook threw during resume for run "${e}":`,t)}}return p.ok?n.Result.ok(p.value,{runId:e,triggerId:a}):n.Result.fail(p.error,{runId:e,triggerId:a})}async signal(e,t){for(let n of this.workflows.values()){let r=n.registry.get(e);if(r?.context){r.context.write(t);return}}}dispatch(e,t){let n=this.index.get(e);if(!n||n.size===0)return;let r={type:e,payload:t,timestamp:Date.now()};for(let e of n){let t=!0;if(e.trigger.predicate)try{t=e.trigger.predicate(r)}catch(n){this.logger.error(`[WorkflowRuntime] Predicate threw for workflow "${e.workflowId}" trigger "${e.triggerId}":`,n),t=!1}if(!t){this.workflows.get(e.workflowId)?.hooks.onDispatch?.({status:`rejected`,reason:`filtered`});continue}let n=this.workflows.get(e.workflowId);n&&n.executionContext.accept(e.triggerId,r,(e,t)=>this.spawnRun(n,e,t)).then(e=>{n.hooks.onDispatch?.(e)}).catch(t=>{this.logger.error(`[WorkflowRuntime] ExecutionContext.accept() threw for workflow "${e.workflowId}":`,t)})}}async createRecorder(e){if(!this.timelineStore)return;let t;try{t=await this.serviceContainer.require(`__sanitizer__`,{workflowId:e.workflow.id})}catch{}return new i.TimelineRecorder(this.timelineStore,{sanitizer:t})}async withRecorder(e,t,n){if(!n)return t();await n.attach(e),await(0,i.yieldToEventLoop)();try{return await t()}finally{await n.detach().catch(()=>{})}}async executePipeline(e,t,r,i){let a=this.getOrCreateFactory(e,t),o=await this.createRecorder(e),s=o?[o.asLogSink()]:void 0,c;try{c=await a.prepare(void 0,r,{sinks:s}),await c.store.set({__trigger_event__:i})}catch(e){let t=e instanceof n.SystemError?e:new n.SystemError({code:`PIPELINE_PREPARE_FAILED`,message:e instanceof Error?e.message:String(e)});return n.Result.fail(t)}return this.extendContextContainer(c,e),this.withRecorder(c,async()=>(await e.hooks.onPrepare(c),await c.run()),o)}async spawnRun(e,t,n){let r=this.generateRunId(e.workflow.id,t);return this.executePipeline(e,t,r,n).then(async n=>{if(n.ok&&n.value.status===`paused`){this.pausedRuns.set(r,{workflowId:e.workflow.id,triggerId:t}),await this.drainWatchQueue(r);return}try{await e.hooks.onComplete(n)}catch(e){this.logger.error(`[WorkflowRuntime] onComplete hook threw for run "${r}":`,e)}}).catch(e=>{this.logger.error(`[WorkflowRuntime] Pipeline execution failed for run "${r}":`,e)}),r}async drainWatchQueue(e){let t=this.pauseService.onRunPaused(e);if(t!==null)try{await this.resume(e,t.patch)}catch(t){this.logger.error(`[WorkflowRuntime] drainWatchQueue: resume() failed for run "${e}":`,t),this.pauseService.onRunEnded(e),this.pausedRuns.delete(e)}}async abortRun(e){for(let t of this.workflows.values()){let n=t.registry.get(e);if(n?.context){try{n.context.abort()}catch(t){this.logger.error(`[WorkflowRuntime] Error aborting run "${e}":`,t)}break}}this.pauseService.onRunEnded(e),this.pausedRuns.delete(e)}buildExecutionContext(e,t){let n=this.logger.child({scope:`${t.type}:execution-context`});switch(t.type){case`transient`:return new d(e,t.concurrency??10,t.capacity??1e3,n);case`serialized`:return new f(e,t.capacity??1e3,n);case`singleton_loop`:return new p(e,t.onActive??`drop`,t.replacementGracePeriod??5e3,(e,t)=>this.signal(e,{__singleton_event__:t}),n);case`exclusive`:return new m(e,t.onActive??`reject`,n)}}async acquireBusSubscription(e){let t=this.subscriptions.get(e);t||(t=new s.SharedResource(()=>this.bus.subscribe(e,t=>{this.dispatch(e,t)}),t=>{try{t?.(),this.subscriptions.delete(e)}catch{}},{gracePeriod:`sync`}),this.subscriptions.set(e,t)),await t.acquire()}releaseBusSubscription(e){this.subscriptions.get(e)?.release()}async extendContextContainer(e,t){t.container&&e.container.extend(t.container),e.container.extend(this.serviceContainer);let r=await this.serviceContainer.require(`__scoped_env_service__`,{workflowId:t.workflow.id});e.container.register({key:`__env__`,factory:()=>r,scope:`singleton`});for(let t of[`__env_service__`,`__scoped_env_service__`])e.container.register({key:t,factory:()=>{throw new n.SystemError({code:n.ErrorCodes.UNAUTHENTICATED.code,message:`"${t}" is an internal artifact and cannot be accessed from pipeline steps.`})},scope:`singleton`,lazy:!1});if(t.workflow.services)for(let n of t.workflow.services){if(n.scope===`workflow`||n.scope===void 0)continue;let t=n.scope===`run`?`singleton`:`transient`;e.container.register({key:n.id,factory:n.factory,scope:t})}}getOrCreateFactory(e,t){let r=e.factories.get(t);if(r)return r;let a=e.workflow.pipelines[t];if(!a)throw new n.SystemError({code:n.ErrorCodes.NOT_FOUND.code,message:`[WorkflowRuntime] No pipeline definition found for workflow "${e.workflow.id}" trigger "${t}". Ensure workflow.pipelines["${t}"] is populated by the compiler.`});let o=new i.PipelineFactory(a,{storeFactory:async e=>this.storeRegistry.get(e),registry:e.registry});return e.factories.set(t,o),o}generateRunId(e,t){return`${e}:${t}:${Date.now()}:${Math.random().toString(36).slice(2)}`}};exports.ABORT_EVENT=c,exports.SIGNAL_EVENT=l,exports.WorkflowRuntime=g;
|
package/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ArtifactContainer as e}from"@asaidimu/utils-artifacts";import{DatabaseConnection as t,createIndexedDbStore as n}from"@asaidimu/utils-database";import{ErrorCodes as r,Result as i,SystemError as a}from"@asaidimu/utils-error";import{Logger as o}from"@asaidimu/utils-logger";import{PipelineFactory as s,PipelineRegistry as c,TimelineRecorder as l,TimelineStore as u,yieldToEventLoop as d}from"@asaidimu/utils-pipeline";import{DocumentSanitizer as f,commonEnvPatterns as p,newSecureDefaultConfig as m}from"@asaidimu/utils-sanitize";import{DELETE_SYMBOL as h,ReactiveDataStore as g}from"@asaidimu/utils-store";import{Serializer as _,SharedResource as v}from"@asaidimu/utils-sync";const y=`__abort__`,b=`__signal__`;var x=class{envs=new Map;use(e,t){return this.envs.set(e,t),this}get(e){return this.envs.get(`global`)?.[e]}list(){return{...this.envs.get(`global`)}}scope(e){let t=this.envs.get(e),n=this.envs.get(`global`);return{get:e=>{if(t&&e in t)return t[e];if(n&&e in n)return n[e]},list:()=>{let e={...n};if(t)for(let n of Object.keys(t))e[n]=t[n];return e}}}},S=class{workflowId;concurrency;capacity;logger;inFlight=0;queueSize=0;closed=!1;constructor(e,t,n,r){this.workflowId=e,this.concurrency=t,this.capacity=n,this.logger=r}async accept(e,t,n){if(this.closed)return{status:`rejected`,reason:`queue_full`};if(this.inFlight<this.concurrency)return this.inFlight++,this.run(e,t,n),{status:`accepted`};if(this.queueSize>=this.capacity)return{status:`rejected`,reason:`queue_full`};this.queueSize++;let r=this.queueSize;return await new Promise(e=>{let t=()=>{if(this.closed){e();return}this.inFlight<this.concurrency?(this.inFlight++,this.queueSize--,e()):setTimeout(t,0)};setTimeout(t,0)}),this.closed?{status:`rejected`,reason:`queue_full`}:(this.run(e,t,n),{status:`queued`,position:r})}settle(e){}close(){this.closed=!0}async run(e,t,n){try{await n(e,t)}finally{this.inFlight--}}},C=class{workflowId;capacity;logger;serializer;queueDepth=0;closed=!1;constructor(e,t,n){this.workflowId=e,this.capacity=t,this.logger=n,this.serializer=new _({capacity:t,yieldMode:`macrotask`})}async accept(e,t,n){if(this.closed||this.queueDepth>=this.capacity)return{status:`rejected`,reason:`queue_full`};let r=this.queueDepth;return this.queueDepth++,await this.serializer.do(async()=>{try{await n(e,t)}finally{this.queueDepth--}}),r===0?{status:`accepted`}:{status:`queued`,position:r}}settle(e){}close(){this.closed=!0,this.serializer.close()}},w=class{workflowId;onActive;replacementGracePeriod;deliverSignal;logger;state=`idle`;activeRunId;settleResolve;closed=!1;constructor(e,t,n,r,i){this.workflowId=e,this.onActive=t,this.replacementGracePeriod=n,this.deliverSignal=r,this.logger=i}async accept(e,t,n){if(this.closed)return{status:`rejected`,reason:`singleton_active`};if(this.state===`idle`){this.state=`starting`;let r=await n(e,t);return r||(this.state=`idle`,this.activeRunId=void 0),{status:`accepted`,runId:r}}if(this.state===`terminating`)return{status:`rejected`,reason:`terminating`};switch(this.onActive){case`drop`:return{status:`rejected`,reason:`singleton_active`};case`signal`:{let e=this.activeRunId??``;return e?this.deliverSignal(e,t):this.logger.warn(`[WorkflowRuntime] SingletonLoop "${this.workflowId}": signal requested but activeRunId unknown (state="${this.state}"). Event "${t.type}" dropped.`),{status:`signalled`,runId:e}}case`replace`:return this.handleReplace(e,t,n)}}settle(e){(this.state===`starting`||e===this.activeRunId)&&(this.activeRunId=e,this.state=`running`),this.state===`terminating`&&e===this.activeRunId&&this.settleResolve?.()}close(){this.closed=!0,this.settleResolve?.()}async handleReplace(e,t,n){if(this.state=`terminating`,this.activeRunId&&this.logger.info(`[WorkflowRuntime] SingletonLoop "${this.workflowId}": replacing run "${this.activeRunId}" with new event "${t.type}".`),await Promise.race([new Promise(e=>{this.settleResolve=e}),new Promise(e=>setTimeout(e,this.replacementGracePeriod))]),this.settleResolve=void 0,this.closed)return this.state=`idle`,{status:`rejected`,reason:`singleton_active`};this.state=`starting`,this.activeRunId=void 0;let r=await n(e,t);return r||(this.state=`idle`),{status:`accepted`,runId:r}}},T=class{workflowId;onActive;logger;activeRunId;pending;closed=!1;constructor(e,t,n){this.workflowId=e,this.onActive=t,this.logger=n}async accept(e,t,n){if(this.closed)return{status:`rejected`,reason:`exclusive_active`};if(!this.activeRunId){let r=await n(e,t);return r&&(this.activeRunId=r),{status:`accepted`,runId:r}}return this.onActive===`reject`?{status:`rejected`,reason:`exclusive_active`}:(this.pending={triggerId:e,event:t,spawn:n},{status:`queued`,position:1})}settle(e){if(e===this.activeRunId&&(this.activeRunId=void 0,this.pending&&!this.closed)){let{triggerId:e,event:t,spawn:n}=this.pending;this.pending=void 0,n(e,t).then(e=>{e&&(this.activeRunId=e)})}}close(){this.closed=!0,this.pending=void 0}},E=class{registrations=new Map;byEventType=new Map;busSubscriptions=new Map;bus;resumeCallback;constructor(e){this.bus=e.bus,this.resumeCallback=e.resume}register(e,t){let n=this.registrations.get(e);n||(n=new Map,this.registrations.set(e,n));let r={runId:e,descriptor:t,queue:[],parked:!1};n.set(t.eventType,r);let i=this.byEventType.get(t.eventType);i||(i=new Set,this.byEventType.set(t.eventType,i)),i.add(e),this.acquireBusSubscription(t.eventType)}cancel(e,t){let n=this.registrations.get(e);n&&(n.delete(t),n.size===0&&this.registrations.delete(e),this.removeFromReverseIndex(e,t),this.releaseBusSubscription(t))}onRunPaused(e){let t=this.registrations.get(e);if(!t)return null;for(let e of t.values())if(e.queue.length>0)return e.queue.shift();for(let e of t.values())e.parked=!0;return null}onRunEnded(e){let t=this.registrations.get(e);if(t){for(let[n]of t)this.removeFromReverseIndex(e,n),this.releaseBusSubscription(n);this.registrations.delete(e)}}onEvent(e,t){let n=this.byEventType.get(e);if(!(!n||n.size===0))for(let r of n){let n=this.registrations.get(r);if(!n)continue;let i=n.get(e);if(!i||!this.evaluate(i.descriptor.conditions,t))continue;let a=this.resolve(i.descriptor,t);i.parked?(i.parked=!1,this.resumeCallback(r,a.patch)):i.queue.push(a)}}evaluate(e,t){for(let n of e){let e=this.getField(t,n.field);if(n.op===`exists`){if(e==null)return!1;continue}if(e==null)return!1;try{switch(n.op){case`==`:if(e!=n.value)return!1;break;case`!=`:if(e==n.value)return!1;break;case`>`:if(!(e>n.value))return!1;break;case`>=`:if(!(e>=n.value))return!1;break;case`<`:if(!(e<n.value))return!1;break;case`<=`:if(!(e<=n.value))return!1;break;default:return!1}}catch{return!1}}return!0}resolve(e,t){return{eventPayload:t,patch:{...e.patch??{},__watch_event__:t}}}getField(e,t){let n=t.split(`.`),r=e;for(let e of n){if(r==null||typeof r!=`object`&&!Array.isArray(r))return;let t=Number(e);r=!isNaN(t)&&Array.isArray(r)?r[t]:r[e]}return r}acquireBusSubscription(e){if(this.busSubscriptions.has(e))return;let t=this.bus.subscribe(e,t=>{this.onEvent(e,t)});this.busSubscriptions.set(e,t)}releaseBusSubscription(e){let t=this.byEventType.get(e);if(t&&t.size>0)return;let n=this.busSubscriptions.get(e);if(n){try{n()}catch{}this.busSubscriptions.delete(e)}}removeFromReverseIndex(e,t){let n=this.byEventType.get(t);n&&(n.delete(e),n.size===0&&this.byEventType.delete(t))}},D=class{bus;storeRegistry;timelineStore;pauseService;serviceContainer;serviceStore;workflows=new Map;index=new Map;subscriptions=new Map;pausedRuns=new Map;logger;abortUnsubscribe;signalUnsubscribe;constructor(t){this.bus=t.bus,this.storeRegistry=t.storeRegistry,this.timelineStore=t.timelineStore,this.logger=t.logger?t.logger:new o([],{scope:`workflow-runtime`}),this.pauseService=new E({bus:this.bus,resume:async(e,t)=>{this.resume(e,t)}}),this.serviceStore=new g({env:{global:t.env??{}}}),this.serviceContainer=new e(this.serviceStore);for(let e of t.services??[]){if(e.id===`__pause_service__`)throw new a({code:r.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Service id "__pause_service__" is reserved for the built-in PauseService and cannot be redefined.`});if(e.id===`__env__`)throw new a({code:r.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Service id "__env__" is reserved for the built-in environment-variable service and cannot be redefined.`});this.serviceContainer.register({key:e.id,factory:e.factory,scope:`singleton`})}this.serviceContainer.register({key:`__pause_service__`,factory:()=>this.pauseService,scope:`singleton`}),this.serviceContainer.register({key:`__env_service__`,factory:async({use:e})=>{let t=await e(e=>e.select(e=>e.env)),n=new x;for(let[e,r]of Object.entries(t??{}))n.use(e,r);return n},scope:`singleton`}),this.serviceContainer.register({key:`__scoped_env_service__`,paramKey:e=>`env:${e.workflowId}`,factory:async({use:e,params:t})=>(await e(e=>e.require(`__env_service__`))).scope(t.workflowId),scope:`singleton`}),this.serviceContainer.register({key:`__sanitizer__`,paramKey:e=>`sanitizer:${e.workflowId}`,factory:async({use:e,params:t})=>{let n=(await e(e=>e.require(`__scoped_env_service__`,{workflowId:t.workflowId}))).list(),r=m();return r.patterns=[...r.patterns,...p([],n)],new f(r,t.workflowId)},scope:`singleton`}),this.abortUnsubscribe=this.bus.subscribe(y,e=>{this.abortRun(e.run)}),this.signalUnsubscribe=this.bus.subscribe(b,e=>{this.signal(e.runId,e.patch)})}static async createTestTimelineStore(e=`test-timeline-database`){return new u(await t({database:e,validate:!0,predicates:{},enableTelemetry:!0},n))}async register(e,t){if(this.workflows.has(e.id))throw new a({code:r.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Workflow "${e.id}" is already registered. Call deregister("${e.id}") before registering again.`});let n=c.get(`workflow:${e.id}`,{onExpired:(t,n)=>{this.logger.info(`[WorkflowRuntime] Run "${t}" pause-window expired for workflow "${e.id}". Checkpoint: ${n.pipelineId}.`)},onExportFailed:(t,n)=>{this.logger.error(`[WorkflowRuntime] Deferred export failed for run "${t}" in workflow "${e.id}":`,n)}}),i=async e=>{let r=e.ok?e.value.runId:e.error?.runId;try{await t.onComplete(e)}finally{r&&(s.executionContext.settle(r),this.pauseService.onRunEnded(r),this.pausedRuns.delete(r)),n.prune(),await t.onCleanup?.()}},o=this.buildExecutionContext(e.id,t.mode),s;s={workflow:e,mode:t.mode,executionContext:o,factories:new Map,registry:n,hooks:{onPrepare:t.onPrepare,onComplete:i,onResume:t.onResume,onDispatch:t.onDispatch,onCleanup:t.onCleanup}},this.workflows.set(e.id,s),this.serviceStore.set(t=>({env:{...t.env??{},[e.id]:e.env??{}}}));let l=this.serviceContainer.scope(e.id);if(e.services&&e.services.length>0)for(let t of e.services)t.scope===`run`||t.scope===`transient`||l.register({key:t.id,factory:t.factory,scope:`singleton`});s.container=l;for(let[t,n]of Object.entries(e.triggers)){let r={workflowId:e.id,triggerId:t,trigger:n},i=this.index.get(n.event);i||(i=new Set,this.index.set(n.event,i)),i.add(r),await this.acquireBusSubscription(n.event)}}deregister(e){let t=this.workflows.get(e);if(t){for(let n of Object.values(t.workflow.triggers)){let t=this.index.get(n.event);if(t){for(let n of t)n.workflowId===e&&t.delete(n);t.size===0&&this.index.delete(n.event)}this.releaseBusSubscription(n.event)}for(let t of[`__scoped_env_service__`,`__sanitizer__`])try{this.serviceContainer.unregister(t,{workflowId:e})}catch{}this.serviceStore.set(t=>({env:{...t.env??{},[e]:h}})),t.container?.dispose(),t.executionContext.close(),c.destroy(`workflow:${e}`),t.factories.clear(),this.workflows.delete(e)}}hasWorkflow(e){return this.workflows.has(e)}listWorkflows(){return Array.from(this.workflows.keys())}async stop(){for(let e of Array.from(this.workflows.keys()))this.deregister(e);try{this.abortUnsubscribe()}catch{}try{this.signalUnsubscribe()}catch{}}registry(e){return this.workflows.get(e)?.registry}info(e,t){return this.workflows.get(e)?.registry.get(t)}list(){let e=[];for(let[t,n]of this.workflows)for(let r of n.registry.list())e.push({...r,workflowId:t});return e}async invoke(e,t,n){let o=this.workflows.get(e);if(!o)throw new a({code:r.NOT_FOUND.code,message:`[WorkflowRuntime] invoke() failed: workflow "${e}" is not registered.`});if(!o.workflow.pipelines[t])throw new a({code:r.NOT_FOUND.code,message:`[WorkflowRuntime] invoke() failed: no pipeline definition for workflow "${e}" trigger "${t}".`});let s=this.generateRunId(e,t),c=await this.executePipeline(o,t,s,n);return c.ok&&c.value.status===`paused`?(this.pausedRuns.set(s,{workflowId:e,triggerId:t}),await this.drainWatchQueue(s)):await o.hooks.onComplete(c),c.ok?i.ok(c.value,{runId:s,triggerId:t}):i.fail(c.error,{runId:s,triggerId:t})}async resume(e,t={}){let n=this.pausedRuns.get(e);if(!n)throw new a({code:r.NOT_FOUND.code,message:`[WorkflowRuntime] resume() failed: run "${e}" is not in the paused runs registry. It may have already completed, been aborted, or the runId is incorrect.`});let{workflowId:o,triggerId:s}=n,c=this.workflows.get(o);if(!c)throw new a({code:r.RESOURCE_RELEASED.code,message:`[WorkflowRuntime] resume() failed: workflow "${o}" is no longer registered. The workflow may have been deregistered while the run was paused.`});let l=this.getOrCreateFactory(c,s),u=c.registry.hold(e),d=await this.createRecorder(c),f=d?[d.asLogSink()]:void 0,p;try{let t=await l.resume(e,{sinks:f});if(!t.ok)throw t.error;p=t.value}catch(t){throw new a({code:r.INTERNAL_ERROR.code,message:`[WorkflowRuntime] resume() failed to reconstruct context for run "${e}"`,cause:t})}u||this.extendContextContainer(p,c),Object.keys(t).length>0&&p.write(t),p.write({__watch__:h});let m=await this.withRecorder(p,async()=>(await c.hooks.onResume?.(p),await p.run()),d);if(m.ok&&m.value.status===`paused`)await this.drainWatchQueue(e);else{let t=this.workflows.get(o);if(t)try{await t.hooks.onComplete(m)}catch(t){this.logger.error(`[WorkflowRuntime] onComplete hook threw during resume for run "${e}":`,t)}}return m.ok?i.ok(m.value,{runId:e,triggerId:s}):i.fail(m.error,{runId:e,triggerId:s})}async signal(e,t){for(let n of this.workflows.values()){let r=n.registry.get(e);if(r?.context){r.context.write(t);return}}}dispatch(e,t){let n=this.index.get(e);if(!n||n.size===0)return;let r={type:e,payload:t,timestamp:Date.now()};for(let e of n){let t=!0;if(e.trigger.predicate)try{t=e.trigger.predicate(r)}catch(n){this.logger.error(`[WorkflowRuntime] Predicate threw for workflow "${e.workflowId}" trigger "${e.triggerId}":`,n),t=!1}if(!t){this.workflows.get(e.workflowId)?.hooks.onDispatch?.({status:`rejected`,reason:`filtered`});continue}let n=this.workflows.get(e.workflowId);n&&n.executionContext.accept(e.triggerId,r,(e,t)=>this.spawnRun(n,e,t)).then(e=>{n.hooks.onDispatch?.(e)}).catch(t=>{this.logger.error(`[WorkflowRuntime] ExecutionContext.accept() threw for workflow "${e.workflowId}":`,t)})}}async createRecorder(e){if(!this.timelineStore)return;let t;try{t=await this.serviceContainer.require(`__sanitizer__`,{workflowId:e.workflow.id})}catch{}return new l(this.timelineStore,{sanitizer:t})}async withRecorder(e,t,n){if(!n)return t();await n.attach(e),await d();try{return await t()}finally{await n.detach().catch(()=>{})}}async executePipeline(e,t,n,r){let o=this.getOrCreateFactory(e,t),s=await this.createRecorder(e),c=s?[s.asLogSink()]:void 0,l;try{l=await o.prepare(void 0,n,{sinks:c}),await l.store.set({__trigger_event__:r})}catch(e){let t=e instanceof a?e:new a({code:`PIPELINE_PREPARE_FAILED`,message:e instanceof Error?e.message:String(e)});return i.fail(t)}return this.extendContextContainer(l,e),this.withRecorder(l,async()=>(await e.hooks.onPrepare(l),await l.run()),s)}async spawnRun(e,t,n){let r=this.generateRunId(e.workflow.id,t);return this.executePipeline(e,t,r,n).then(async n=>{if(n.ok&&n.value.status===`paused`){this.pausedRuns.set(r,{workflowId:e.workflow.id,triggerId:t}),await this.drainWatchQueue(r);return}try{await e.hooks.onComplete(n)}catch(e){this.logger.error(`[WorkflowRuntime] onComplete hook threw for run "${r}":`,e)}}).catch(e=>{this.logger.error(`[WorkflowRuntime] Pipeline execution failed for run "${r}":`,e)}),r}async drainWatchQueue(e){let t=this.pauseService.onRunPaused(e);if(t!==null)try{await this.resume(e,t.patch)}catch(t){this.logger.error(`[WorkflowRuntime] drainWatchQueue: resume() failed for run "${e}":`,t),this.pauseService.onRunEnded(e),this.pausedRuns.delete(e)}}async abortRun(e){for(let t of this.workflows.values()){let n=t.registry.get(e);if(n?.context){try{n.context.abort()}catch(t){this.logger.error(`[WorkflowRuntime] Error aborting run "${e}":`,t)}break}}this.pauseService.onRunEnded(e),this.pausedRuns.delete(e)}buildExecutionContext(e,t){let n=this.logger.child({scope:`${t.type}:execution-context`});switch(t.type){case`transient`:return new S(e,t.concurrency??10,t.capacity??1e3,n);case`serialized`:return new C(e,t.capacity??1e3,n);case`singleton_loop`:return new w(e,t.onActive??`drop`,t.replacementGracePeriod??5e3,(e,t)=>this.signal(e,{__singleton_event__:t}),n);case`exclusive`:return new T(e,t.onActive??`reject`,n)}}async acquireBusSubscription(e){let t=this.subscriptions.get(e);t||(t=new v(()=>this.bus.subscribe(e,t=>{this.dispatch(e,t)}),t=>{try{t?.(),this.subscriptions.delete(e)}catch{}},{gracePeriod:`sync`}),this.subscriptions.set(e,t)),await t.acquire()}releaseBusSubscription(e){this.subscriptions.get(e)?.release()}async extendContextContainer(e,t){t.container&&e.container.extend(t.container),e.container.extend(this.serviceContainer);let n=await this.serviceContainer.require(`__scoped_env_service__`,{workflowId:t.workflow.id});e.container.register({key:`__env__`,factory:()=>n,scope:`singleton`});for(let t of[`__env_service__`,`__scoped_env_service__`])e.container.register({key:t,factory:()=>{throw new a({code:r.UNAUTHENTICATED.code,message:`"${t}" is an internal artifact and cannot be accessed from pipeline steps.`})},scope:`singleton`,lazy:!1});if(t.workflow.services)for(let n of t.workflow.services){if(n.scope===`workflow`||n.scope===void 0)continue;let t=n.scope===`run`?`singleton`:`transient`;e.container.register({key:n.id,factory:n.factory,scope:t})}}getOrCreateFactory(e,t){let n=e.factories.get(t);if(n)return n;let i=e.workflow.pipelines[t];if(!i)throw new a({code:r.NOT_FOUND.code,message:`[WorkflowRuntime] No pipeline definition found for workflow "${e.workflow.id}" trigger "${t}". Ensure workflow.pipelines["${t}"] is populated by the compiler.`});let o=new s(i,{storeFactory:async e=>this.storeRegistry.get(e),registry:e.registry});return e.factories.set(t,o),o}generateRunId(e,t){return`${e}:${t}:${Date.now()}:${Math.random().toString(36).slice(2)}`}};export{y as ABORT_EVENT,b as SIGNAL_EVENT,D as WorkflowRuntime};
|
|
1
|
+
import{ArtifactContainer as e}from"@asaidimu/utils-artifacts";import{DatabaseConnection as t,createIndexedDbStore as n}from"@asaidimu/utils-database";import{ErrorCodes as r,Result as i,SystemError as a}from"@asaidimu/utils-error";import{Logger as o}from"@asaidimu/utils-logger";import{PipelineFactory as s,PipelineRegistry as c,TimelineRecorder as l,TimelineStore as u,yieldToEventLoop as d}from"@asaidimu/utils-pipeline";import{DocumentSanitizer as f,commonEnvPatterns as p,newSecureDefaultConfig as m}from"@asaidimu/utils-sanitize";import{DELETE_SYMBOL as h,ReactiveDataStore as g}from"@asaidimu/utils-store";import{Serializer as _,SharedResource as v}from"@asaidimu/utils-sync";const y=`__abort__`,b=`__signal__`;var x=class{envs=new Map;use(e,t){return this.envs.set(e,t),this}get(e){return this.envs.get(`global`)?.[e]}list(){return{...this.envs.get(`global`)}}scope(e){let t=this.envs.get(e),n=this.envs.get(`global`);return{get:e=>{if(t&&e in t)return t[e];if(n&&e in n)return n[e]},list:()=>{let e={...n};if(t)for(let n of Object.keys(t))e[n]=t[n];return e}}}},S=class{workflowId;concurrency;capacity;logger;inFlight=0;queueSize=0;closed=!1;constructor(e,t,n,r){this.workflowId=e,this.concurrency=t,this.capacity=n,this.logger=r}async accept(e,t,n){if(this.closed)return{status:`rejected`,reason:`queue_full`};if(this.inFlight<this.concurrency)return this.inFlight++,this.run(e,t,n),{status:`accepted`};if(this.queueSize>=this.capacity)return{status:`rejected`,reason:`queue_full`};this.queueSize++;let r=this.queueSize;return await new Promise(e=>{let t=()=>{if(this.closed){e();return}this.inFlight<this.concurrency?(this.inFlight++,this.queueSize--,e()):setTimeout(t,0)};setTimeout(t,0)}),this.closed?{status:`rejected`,reason:`queue_full`}:(this.run(e,t,n),{status:`queued`,position:r})}settle(e){}close(){this.closed=!0}async run(e,t,n){try{await n(e,t)}finally{this.inFlight--}}},C=class{workflowId;capacity;logger;serializer;queueDepth=0;closed=!1;constructor(e,t,n){this.workflowId=e,this.capacity=t,this.logger=n,this.serializer=new _({capacity:t,yieldMode:`macrotask`})}async accept(e,t,n){if(this.closed||this.queueDepth>=this.capacity)return{status:`rejected`,reason:`queue_full`};let r=this.queueDepth;return this.queueDepth++,await this.serializer.do(async()=>{try{await n(e,t)}finally{this.queueDepth--}}),r===0?{status:`accepted`}:{status:`queued`,position:r}}settle(e){}close(){this.closed=!0,this.serializer.close()}},w=class{workflowId;onActive;replacementGracePeriod;deliverSignal;logger;state=`idle`;activeRunId;settleResolve;closed=!1;constructor(e,t,n,r,i){this.workflowId=e,this.onActive=t,this.replacementGracePeriod=n,this.deliverSignal=r,this.logger=i}async accept(e,t,n){if(this.closed)return{status:`rejected`,reason:`singleton_active`};if(this.state===`idle`){this.state=`starting`;let r=await n(e,t);return r||(this.state=`idle`,this.activeRunId=void 0),{status:`accepted`,runId:r}}if(this.state===`terminating`)return{status:`rejected`,reason:`terminating`};switch(this.onActive){case`drop`:return{status:`rejected`,reason:`singleton_active`};case`signal`:{let e=this.activeRunId??``;return e?this.deliverSignal(e,t):this.logger.warn(`[WorkflowRuntime] SingletonLoop "${this.workflowId}": signal requested but activeRunId unknown (state="${this.state}"). Event "${t.type}" dropped.`),{status:`signalled`,runId:e}}case`replace`:return this.handleReplace(e,t,n)}}settle(e){(this.state===`starting`||e===this.activeRunId)&&(this.activeRunId=e,this.state=`running`),this.state===`terminating`&&e===this.activeRunId&&this.settleResolve?.()}close(){this.closed=!0,this.settleResolve?.()}async handleReplace(e,t,n){if(this.state=`terminating`,this.activeRunId&&this.logger.info(`[WorkflowRuntime] SingletonLoop "${this.workflowId}": replacing run "${this.activeRunId}" with new event "${t.type}".`),await Promise.race([new Promise(e=>{this.settleResolve=e}),new Promise(e=>setTimeout(e,this.replacementGracePeriod))]),this.settleResolve=void 0,this.closed)return this.state=`idle`,{status:`rejected`,reason:`singleton_active`};this.state=`starting`,this.activeRunId=void 0;let r=await n(e,t);return r||(this.state=`idle`),{status:`accepted`,runId:r}}},T=class{workflowId;onActive;logger;activeRunId;pending;closed=!1;constructor(e,t,n){this.workflowId=e,this.onActive=t,this.logger=n}async accept(e,t,n){if(this.closed)return{status:`rejected`,reason:`exclusive_active`};if(!this.activeRunId){let r=await n(e,t);return r&&(this.activeRunId=r),{status:`accepted`,runId:r}}return this.onActive===`reject`?{status:`rejected`,reason:`exclusive_active`}:(this.pending={triggerId:e,event:t,spawn:n},{status:`queued`,position:1})}settle(e){if(e===this.activeRunId&&(this.activeRunId=void 0,this.pending&&!this.closed)){let{triggerId:e,event:t,spawn:n}=this.pending;this.pending=void 0,n(e,t).then(e=>{e&&(this.activeRunId=e)})}}close(){this.closed=!0,this.pending=void 0}},E=class{registrations=new Map;byEventType=new Map;busSubscriptions=new Map;bus;resumeCallback;constructor(e){this.bus=e.bus,this.resumeCallback=e.resume}register(e,t){let n=this.registrations.get(e);n||(n=new Map,this.registrations.set(e,n));let r={runId:e,descriptor:t,queue:[],parked:!1};n.set(t.eventType,r);let i=this.byEventType.get(t.eventType);i||(i=new Set,this.byEventType.set(t.eventType,i)),i.add(e),this.acquireBusSubscription(t.eventType)}cancel(e,t){let n=this.registrations.get(e);n&&(n.delete(t),n.size===0&&this.registrations.delete(e),this.removeFromReverseIndex(e,t),this.releaseBusSubscription(t))}onRunPaused(e){let t=this.registrations.get(e);if(!t)return null;for(let e of t.values())if(e.queue.length>0)return e.queue.shift();for(let e of t.values())e.parked=!0;return null}onRunEnded(e){let t=this.registrations.get(e);if(t){for(let[n]of t)this.removeFromReverseIndex(e,n),this.releaseBusSubscription(n);this.registrations.delete(e)}}onEvent(e,t){let n=this.byEventType.get(e);if(!(!n||n.size===0))for(let r of n){let n=this.registrations.get(r);if(!n)continue;let i=n.get(e);if(!i||!this.evaluate(i.descriptor.conditions,t))continue;let a=this.resolve(i.descriptor,t);i.parked?(i.parked=!1,this.resumeCallback(r,a.patch)):i.queue.push(a)}}evaluate(e,t){for(let n of e){let e=this.getField(t,n.field);if(n.op===`exists`){if(e==null)return!1;continue}if(e==null)return!1;try{switch(n.op){case`==`:if(e!=n.value)return!1;break;case`!=`:if(e==n.value)return!1;break;case`>`:if(!(e>n.value))return!1;break;case`>=`:if(!(e>=n.value))return!1;break;case`<`:if(!(e<n.value))return!1;break;case`<=`:if(!(e<=n.value))return!1;break;default:return!1}}catch{return!1}}return!0}resolve(e,t){return{eventPayload:t,patch:{...e.patch??{},__watch_event__:t}}}getField(e,t){let n=t.split(`.`),r=e;for(let e of n){if(r==null||typeof r!=`object`&&!Array.isArray(r))return;let t=Number(e);r=!isNaN(t)&&Array.isArray(r)?r[t]:r[e]}return r}acquireBusSubscription(e){if(this.busSubscriptions.has(e))return;let t=this.bus.subscribe(e,t=>{this.onEvent(e,t)});this.busSubscriptions.set(e,t)}releaseBusSubscription(e){let t=this.byEventType.get(e);if(t&&t.size>0)return;let n=this.busSubscriptions.get(e);if(n){try{n()}catch{}this.busSubscriptions.delete(e)}}removeFromReverseIndex(e,t){let n=this.byEventType.get(t);n&&(n.delete(e),n.size===0&&this.byEventType.delete(t))}},D=class{bus;storeRegistry;timelineStore;pauseService;serviceContainer;serviceStore;workflows=new Map;index=new Map;subscriptions=new Map;pausedRuns=new Map;logger;abortUnsubscribe;signalUnsubscribe;constructor(t){this.bus=t.bus,this.storeRegistry=t.storeRegistry,this.timelineStore=t.timelineStore,this.logger=t.logger?t.logger:new o([],{scope:`workflow-runtime`}),this.pauseService=new E({bus:this.bus,resume:async(e,t)=>{this.resume(e,t)}}),this.serviceStore=new g({env:{global:t.env??{}}}),this.serviceContainer=new e(this.serviceStore);for(let e of t.services??[]){if(e.id===`__pause_service__`)throw new a({code:r.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Service id "__pause_service__" is reserved for the built-in PauseService and cannot be redefined.`});if(e.id===`__env__`)throw new a({code:r.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Service id "__env__" is reserved for the built-in environment-variable service and cannot be redefined.`});this.serviceContainer.register({key:e.id,factory:e.factory,scope:`singleton`})}this.serviceContainer.register({key:`__pause_service__`,factory:()=>this.pauseService,scope:`singleton`}),this.serviceContainer.register({key:`__env_service__`,factory:async({use:e})=>{let t=await e(e=>e.select(e=>e.env)),n=new x;for(let[e,r]of Object.entries(t??{}))n.use(e,r);return n},scope:`singleton`}),this.serviceContainer.register({key:`__scoped_env_service__`,paramKey:e=>`env:${e.workflowId}`,factory:async({use:e,params:t})=>(await e(e=>e.require(`__env_service__`))).scope(t.workflowId),scope:`singleton`}),this.serviceContainer.register({key:`__sanitizer__`,paramKey:e=>`sanitizer:${e.workflowId}`,factory:async({use:e,params:t})=>{let n=(await e(e=>e.require(`__scoped_env_service__`,{workflowId:t.workflowId}))).list(),r=m();return r.patterns=[...r.patterns,...p([],n)],new f(r,t.workflowId)},scope:`singleton`}),this.abortUnsubscribe=this.bus.subscribe(y,e=>{this.abortRun(e.run)}),this.signalUnsubscribe=this.bus.subscribe(b,e=>{this.signal(e.runId,e.patch)})}static async createTestTimelineStore(e=`test-timeline-database`){return new u(await t({database:e,validate:!0,predicates:{},enableTelemetry:!0},n))}async register(e,t){if(this.workflows.has(e.id))throw new a({code:r.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Workflow "${e.id}" is already registered. Call deregister("${e.id}") before registering again.`});let n=c.get(`workflow:${e.id}`,{onExpired:(t,n)=>{this.logger.info(`[WorkflowRuntime] Run "${t}" pause-window expired for workflow "${e.id}". Checkpoint: ${n.pipelineId}.`)},onExportFailed:(t,n)=>{this.logger.error(`[WorkflowRuntime] Deferred export failed for run "${t}" in workflow "${e.id}":`,n)}}),i=async e=>{let r=e.ok?e.value.runId:e.error?.runId;try{r&&(s.executionContext.settle(r),this.pauseService.onRunEnded(r),this.pausedRuns.delete(r)),n.prune()}finally{await t.onComplete(e),await t.onCleanup?.()}},o=this.buildExecutionContext(e.id,t.mode),s;s={workflow:e,mode:t.mode,executionContext:o,factories:new Map,registry:n,hooks:{onPrepare:t.onPrepare,onComplete:i,onResume:t.onResume,onDispatch:t.onDispatch,onCleanup:t.onCleanup}},this.workflows.set(e.id,s),this.serviceStore.set(t=>({env:{...t.env??{},[e.id]:e.env??{}}}));let l=this.serviceContainer.scope(e.id);if(e.services&&e.services.length>0)for(let t of e.services)t.scope===`run`||t.scope===`transient`||l.register({key:t.id,factory:t.factory,scope:`singleton`});s.container=l;for(let[t,n]of Object.entries(e.triggers)){let r={workflowId:e.id,triggerId:t,trigger:n},i=this.index.get(n.event);i||(i=new Set,this.index.set(n.event,i)),i.add(r),await this.acquireBusSubscription(n.event)}}deregister(e){let t=this.workflows.get(e);if(t){for(let n of Object.values(t.workflow.triggers)){let t=this.index.get(n.event);if(t){for(let n of t)n.workflowId===e&&t.delete(n);t.size===0&&this.index.delete(n.event)}this.releaseBusSubscription(n.event)}for(let t of[`__scoped_env_service__`,`__sanitizer__`])try{this.serviceContainer.unregister(t,{workflowId:e})}catch{}this.serviceStore.set(t=>({env:{...t.env??{},[e]:h}})),t.container?.dispose(),t.executionContext.close(),c.destroy(`workflow:${e}`),t.factories.clear(),this.workflows.delete(e)}}hasWorkflow(e){return this.workflows.has(e)}listWorkflows(){return Array.from(this.workflows.keys())}async stop(){for(let e of Array.from(this.workflows.keys()))this.deregister(e);try{this.abortUnsubscribe()}catch{}try{this.signalUnsubscribe()}catch{}}registry(e){return this.workflows.get(e)?.registry}info(e,t){return this.workflows.get(e)?.registry.get(t)}list(){let e=[];for(let[t,n]of this.workflows)for(let r of n.registry.list())e.push({...r,workflowId:t});return e}async invoke(e,t,n){let o=this.workflows.get(e);if(!o)throw new a({code:r.NOT_FOUND.code,message:`[WorkflowRuntime] invoke() failed: workflow "${e}" is not registered.`});if(!o.workflow.pipelines[t])throw new a({code:r.NOT_FOUND.code,message:`[WorkflowRuntime] invoke() failed: no pipeline definition for workflow "${e}" trigger "${t}".`});let s=this.generateRunId(e,t),c=await this.executePipeline(o,t,s,n);return c.ok&&c.value.status===`paused`?(this.pausedRuns.set(s,{workflowId:e,triggerId:t}),await this.drainWatchQueue(s)):await o.hooks.onComplete(c),c.ok?i.ok(c.value,{runId:s,triggerId:t}):i.fail(c.error,{runId:s,triggerId:t})}async resume(e,t={}){let n=this.pausedRuns.get(e);if(!n)throw new a({code:r.NOT_FOUND.code,message:`[WorkflowRuntime] resume() failed: run "${e}" is not in the paused runs registry. It may have already completed, been aborted, or the runId is incorrect.`});let{workflowId:o,triggerId:s}=n,c=this.workflows.get(o);if(!c)throw new a({code:r.RESOURCE_RELEASED.code,message:`[WorkflowRuntime] resume() failed: workflow "${o}" is no longer registered. The workflow may have been deregistered while the run was paused.`});let l=this.getOrCreateFactory(c,s),u=c.registry.hold(e),d=await this.createRecorder(c),f=d?[d.asLogSink()]:void 0,p;try{let t=await l.resume(e,{sinks:f});if(!t.ok)throw t.error;p=t.value}catch(t){throw new a({code:r.INTERNAL_ERROR.code,message:`[WorkflowRuntime] resume() failed to reconstruct context for run "${e}"`,cause:t})}u||this.extendContextContainer(p,c),Object.keys(t).length>0&&p.write(t),p.write({__watch__:h});let m=await this.withRecorder(p,async()=>(await c.hooks.onResume?.(p),await p.run()),d);if(m.ok&&m.value.status===`paused`)await this.drainWatchQueue(e);else{let t=this.workflows.get(o);if(t)try{await t.hooks.onComplete(m)}catch(t){this.logger.error(`[WorkflowRuntime] onComplete hook threw during resume for run "${e}":`,t)}}return m.ok?i.ok(m.value,{runId:e,triggerId:s}):i.fail(m.error,{runId:e,triggerId:s})}async signal(e,t){for(let n of this.workflows.values()){let r=n.registry.get(e);if(r?.context){r.context.write(t);return}}}dispatch(e,t){let n=this.index.get(e);if(!n||n.size===0)return;let r={type:e,payload:t,timestamp:Date.now()};for(let e of n){let t=!0;if(e.trigger.predicate)try{t=e.trigger.predicate(r)}catch(n){this.logger.error(`[WorkflowRuntime] Predicate threw for workflow "${e.workflowId}" trigger "${e.triggerId}":`,n),t=!1}if(!t){this.workflows.get(e.workflowId)?.hooks.onDispatch?.({status:`rejected`,reason:`filtered`});continue}let n=this.workflows.get(e.workflowId);n&&n.executionContext.accept(e.triggerId,r,(e,t)=>this.spawnRun(n,e,t)).then(e=>{n.hooks.onDispatch?.(e)}).catch(t=>{this.logger.error(`[WorkflowRuntime] ExecutionContext.accept() threw for workflow "${e.workflowId}":`,t)})}}async createRecorder(e){if(!this.timelineStore)return;let t;try{t=await this.serviceContainer.require(`__sanitizer__`,{workflowId:e.workflow.id})}catch{}return new l(this.timelineStore,{sanitizer:t})}async withRecorder(e,t,n){if(!n)return t();await n.attach(e),await d();try{return await t()}finally{await n.detach().catch(()=>{})}}async executePipeline(e,t,n,r){let o=this.getOrCreateFactory(e,t),s=await this.createRecorder(e),c=s?[s.asLogSink()]:void 0,l;try{l=await o.prepare(void 0,n,{sinks:c}),await l.store.set({__trigger_event__:r})}catch(e){let t=e instanceof a?e:new a({code:`PIPELINE_PREPARE_FAILED`,message:e instanceof Error?e.message:String(e)});return i.fail(t)}return this.extendContextContainer(l,e),this.withRecorder(l,async()=>(await e.hooks.onPrepare(l),await l.run()),s)}async spawnRun(e,t,n){let r=this.generateRunId(e.workflow.id,t);return this.executePipeline(e,t,r,n).then(async n=>{if(n.ok&&n.value.status===`paused`){this.pausedRuns.set(r,{workflowId:e.workflow.id,triggerId:t}),await this.drainWatchQueue(r);return}try{await e.hooks.onComplete(n)}catch(e){this.logger.error(`[WorkflowRuntime] onComplete hook threw for run "${r}":`,e)}}).catch(e=>{this.logger.error(`[WorkflowRuntime] Pipeline execution failed for run "${r}":`,e)}),r}async drainWatchQueue(e){let t=this.pauseService.onRunPaused(e);if(t!==null)try{await this.resume(e,t.patch)}catch(t){this.logger.error(`[WorkflowRuntime] drainWatchQueue: resume() failed for run "${e}":`,t),this.pauseService.onRunEnded(e),this.pausedRuns.delete(e)}}async abortRun(e){for(let t of this.workflows.values()){let n=t.registry.get(e);if(n?.context){try{n.context.abort()}catch(t){this.logger.error(`[WorkflowRuntime] Error aborting run "${e}":`,t)}break}}this.pauseService.onRunEnded(e),this.pausedRuns.delete(e)}buildExecutionContext(e,t){let n=this.logger.child({scope:`${t.type}:execution-context`});switch(t.type){case`transient`:return new S(e,t.concurrency??10,t.capacity??1e3,n);case`serialized`:return new C(e,t.capacity??1e3,n);case`singleton_loop`:return new w(e,t.onActive??`drop`,t.replacementGracePeriod??5e3,(e,t)=>this.signal(e,{__singleton_event__:t}),n);case`exclusive`:return new T(e,t.onActive??`reject`,n)}}async acquireBusSubscription(e){let t=this.subscriptions.get(e);t||(t=new v(()=>this.bus.subscribe(e,t=>{this.dispatch(e,t)}),t=>{try{t?.(),this.subscriptions.delete(e)}catch{}},{gracePeriod:`sync`}),this.subscriptions.set(e,t)),await t.acquire()}releaseBusSubscription(e){this.subscriptions.get(e)?.release()}async extendContextContainer(e,t){t.container&&e.container.extend(t.container),e.container.extend(this.serviceContainer);let n=await this.serviceContainer.require(`__scoped_env_service__`,{workflowId:t.workflow.id});e.container.register({key:`__env__`,factory:()=>n,scope:`singleton`});for(let t of[`__env_service__`,`__scoped_env_service__`])e.container.register({key:t,factory:()=>{throw new a({code:r.UNAUTHENTICATED.code,message:`"${t}" is an internal artifact and cannot be accessed from pipeline steps.`})},scope:`singleton`,lazy:!1});if(t.workflow.services)for(let n of t.workflow.services){if(n.scope===`workflow`||n.scope===void 0)continue;let t=n.scope===`run`?`singleton`:`transient`;e.container.register({key:n.id,factory:n.factory,scope:t})}}getOrCreateFactory(e,t){let n=e.factories.get(t);if(n)return n;let i=e.workflow.pipelines[t];if(!i)throw new a({code:r.NOT_FOUND.code,message:`[WorkflowRuntime] No pipeline definition found for workflow "${e.workflow.id}" trigger "${t}". Ensure workflow.pipelines["${t}"] is populated by the compiler.`});let o=new s(i,{storeFactory:async e=>this.storeRegistry.get(e),registry:e.registry});return e.factories.set(t,o),o}generateRunId(e,t){return`${e}:${t}:${Date.now()}:${Math.random().toString(36).slice(2)}`}};export{y as ABORT_EVENT,b as SIGNAL_EVENT,D as WorkflowRuntime};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asaidimu/runtime",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "A runtime for workflows built on \"@asaidimu/utils-pipeline\".",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@asaidimu/utils-store": "^10.2.13",
|
|
39
39
|
"@asaidimu/utils-database": "^3.1.15",
|
|
40
40
|
"@asaidimu/utils-sync": "^2.3.6",
|
|
41
|
-
"@asaidimu/utils-pipeline": "^1.3.
|
|
41
|
+
"@asaidimu/utils-pipeline": "^1.3.15",
|
|
42
42
|
"@asaidimu/utils-artifacts": "^8.2.20",
|
|
43
43
|
"@asaidimu/utils-error": "^1.0.0",
|
|
44
44
|
"@asaidimu/utils-events": "^1.2.7",
|