@asaidimu/runtime 1.0.0

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.d.ts ADDED
@@ -0,0 +1,153 @@
1
+ import { Result, SystemError } from "@asaidimu/utils-error";
2
+ import { PipelineFactory, PipelineRegistry, PipelineRunResult, RoutingPipelineDefinition, RunContext, RunInfo, TimelineStore } from "@asaidimu/utils-pipeline";
3
+ import { DeepPartial, StoreRegistry } from "@asaidimu/utils-store";
4
+ import { EventBus } from "@asaidimu/utils-events";
5
+
6
+ //#region src/runtime/types/workflow.d.ts
7
+ type WorkflowState<T extends Record<string, any> = Record<string, any>> = T;
8
+ interface WorkflowEvent<Payload = unknown> {
9
+ type: string;
10
+ payload: Payload;
11
+ timestamp: number;
12
+ state?: WorkflowState;
13
+ }
14
+ type WorkflowTrigger = {
15
+ id: string;
16
+ event: string;
17
+ predicate: (event: WorkflowEvent, state?: WorkflowState) => boolean;
18
+ };
19
+ interface Workflow<T extends Record<string, any> = Record<string, any>> {
20
+ id: string;
21
+ label: string;
22
+ triggers: Record<string, WorkflowTrigger>;
23
+ pipelines: Record<string, RoutingPipelineDefinition<WorkflowState<T>>>;
24
+ }
25
+ //#endregion
26
+ //#region src/runtime/types/service.d.ts
27
+ interface RunServiceContext {
28
+ runId: string;
29
+ workflowId: string;
30
+ triggerId: string;
31
+ }
32
+ type ServiceDefinition<T> = {
33
+ id: string;
34
+ scope: "singleton";
35
+ factory: () => T | Promise<T>;
36
+ } | {
37
+ id: string;
38
+ scope: "transient";
39
+ factory: (ctx: RunServiceContext) => T | Promise<T>;
40
+ };
41
+ //#endregion
42
+ //#region src/runtime/types/runtime.d.ts
43
+ declare const ABORT_EVENT: "__abort__";
44
+ declare const SIGNAL_EVENT: "__signal__";
45
+ type WorkflowExecutionMode = {
46
+ type: "transient";
47
+ concurrency?: number;
48
+ capacity?: number;
49
+ } | {
50
+ type: "serialized";
51
+ capacity?: number;
52
+ } | {
53
+ type: "singleton_loop";
54
+ onActive?: "drop" | "signal" | "replace";
55
+ replacementGracePeriod?: number;
56
+ } | {
57
+ type: "exclusive";
58
+ onActive?: "reject" | "queue_single";
59
+ };
60
+ type DispatchResult = {
61
+ status: "accepted";
62
+ runId?: string;
63
+ } | {
64
+ status: "queued";
65
+ position: number;
66
+ } | {
67
+ status: "rejected";
68
+ reason: "queue_full" | "singleton_active" | "exclusive_active" | "terminating" | "filtered";
69
+ } | {
70
+ status: "signalled";
71
+ runId: string;
72
+ };
73
+ type InvokeResult = Result<PipelineRunResult<WorkflowState>, SystemError, {
74
+ runId: string;
75
+ triggerId: string;
76
+ }>;
77
+ interface WorkflowRuntimeOptions {
78
+ bus: EventBus<Record<string, any>>;
79
+ storeRegistry: StoreRegistry<WorkflowState>;
80
+ timelineStore?: TimelineStore<WorkflowState>;
81
+ services?: ServiceDefinition<unknown>[];
82
+ }
83
+ interface RegisterOptions {
84
+ mode: WorkflowExecutionMode;
85
+ onPrepare: (context: Readonly<RunContext<WorkflowState>>) => Promise<void>;
86
+ onComplete: (result: Result<PipelineRunResult<WorkflowState>, SystemError>) => Promise<void>;
87
+ onResume?: (context: Readonly<RunContext<WorkflowState>>) => Promise<void>;
88
+ onCleanup?: () => Promise<void>;
89
+ onDispatch?: (result: DispatchResult) => void;
90
+ }
91
+ //#endregion
92
+ //#region src/runtime/runtime/runtime.d.ts
93
+ declare class WorkflowRuntime {
94
+ private readonly bus;
95
+ private readonly storeRegistry;
96
+ private readonly timelineStore;
97
+ /** Built-in watch service. Always present. */
98
+ private readonly pauseService;
99
+ /** User-defined run-scoped service definitions, instantiated per run. */
100
+ private readonly runServiceDefinitions;
101
+ /** Global container holding all registered services accessible by steps. */
102
+ private readonly serviceContainer;
103
+ private readonly workflows;
104
+ private readonly index;
105
+ private readonly subscriptions;
106
+ /**
107
+ * runId → PausedRunEntry.
108
+ * Populated when a run pauses. Cleared when run ends or is aborted.
109
+ */
110
+ private readonly pausedRuns;
111
+ private readonly abortUnsubscribe;
112
+ private readonly signalUnsubscribe;
113
+ constructor(options: WorkflowRuntimeOptions);
114
+ static createTestTimelineStore(database?: string): Promise<TimelineStore<any>>;
115
+ register(workflow: Workflow, options: RegisterOptions): Promise<void>;
116
+ deregister(workflowId: string): void;
117
+ hasWorkflow(workflowId: string): boolean;
118
+ listWorkflows(): string[];
119
+ stop(): Promise<void>;
120
+ registry(workflowId: string): PipelineRegistry<WorkflowState> | undefined;
121
+ watch(workflowId: string, runId: string): RunInfo<WorkflowState> | undefined;
122
+ listAllRuns(): Array<RunInfo<WorkflowState> & {
123
+ workflowId: string;
124
+ }>;
125
+ invoke(workflowId: string, triggerId: string, event: WorkflowEvent): Promise<InvokeResult>;
126
+ resume(runId: string, patch?: DeepPartial<WorkflowState>): Promise<InvokeResult>;
127
+ signal(runId: string, patch: DeepPartial<WorkflowState>): Promise<void>;
128
+ private dispatch;
129
+ private executePipeline;
130
+ private spawnRun;
131
+ private drainWatchQueue;
132
+ private abortRun;
133
+ private buildExecutionContext;
134
+ private acquireBusSubscription;
135
+ private releaseBusSubscription;
136
+ private getOrCreateFactory;
137
+ private generateRunId;
138
+ }
139
+ //#endregion
140
+ //#region src/runtime/types/pause.d.ts
141
+ type PauseOperator = ">=" | "<=" | "==" | "!=" | ">" | "<" | "exists";
142
+ interface PauseCondition {
143
+ field: string;
144
+ op: PauseOperator;
145
+ value?: unknown;
146
+ }
147
+ interface PauseDescriptor {
148
+ eventType: string;
149
+ conditions: PauseCondition[];
150
+ patch?: DeepPartial<WorkflowState>;
151
+ }
152
+ //#endregion
153
+ export { ABORT_EVENT, type DispatchResult, type InvokeResult, type PauseCondition, type PauseDescriptor, type PauseOperator, type RegisterOptions, SIGNAL_EVENT, type ServiceDefinition, type Workflow, type WorkflowEvent, type WorkflowExecutionMode, WorkflowRuntime, type WorkflowRuntimeOptions, type WorkflowState, type WorkflowTrigger };
package/index.js ADDED
@@ -0,0 +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-pipeline"),i=require("@asaidimu/utils-store"),a=require("@asaidimu/utils-sync");const o=`__abort__`,s=`__signal__`;var c=class{workflowId;concurrency;capacity;inFlight=0;queueSize=0;closed=!1;constructor(e,t,n){this.workflowId=e,this.concurrency=t,this.capacity=n}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--}}},l=class{workflowId;capacity;serializer;queueDepth=0;closed=!1;constructor(e,t){this.workflowId=e,this.capacity=t,this.serializer=new a.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()}},u=class{workflowId;onActive;replacementGracePeriod;deliverSignal;state=`idle`;activeRunId;settleResolve;closed=!1;constructor(e,t,n,r){this.workflowId=e,this.onActive=t,this.replacementGracePeriod=n,this.deliverSignal=r}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):console.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&&console.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}}},d=class{workflowId;onActive;activeRunId;pending;closed=!1;constructor(e,t){this.workflowId=e,this.onActive=t}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}},f=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))}},p=class{bus;storeRegistry;timelineStore;pauseService;runServiceDefinitions=[];serviceContainer;workflows=new Map;index=new Map;subscriptions=new Map;pausedRuns=new Map;abortUnsubscribe;signalUnsubscribe;constructor(t){this.bus=t.bus,this.storeRegistry=t.storeRegistry,this.timelineStore=t.timelineStore,this.pauseService=new f({bus:this.bus,resume:async(e,t)=>{this.resume(e,t)}}),this.serviceContainer=new e.ArtifactContainer(new i.ReactiveDataStore({}));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.`});e.scope===`singleton`?this.serviceContainer.register({key:e.id,factory:()=>e.factory(),scope:`singleton`}):this.runServiceDefinitions.push(e)}this.serviceContainer.register({key:`__pause_service__`,factory:()=>this.pauseService,scope:`singleton`}),this.abortUnsubscribe=this.bus.subscribe(o,e=>{this.abortRun(e.run)}),this.signalUnsubscribe=this.bus.subscribe(s,e=>{this.signal(e.runId,e.patch)})}static async createTestTimelineStore(e=`test-timeline-database`){return new r.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 i=r.PipelineRegistry.get(`workflow:${e.id}`,{onExpired:(t,n)=>{console.info(`[WorkflowRuntime] Run "${t}" pause-window expired for workflow "${e.id}". Checkpoint: ${n.pipelineId}.`)},onExportFailed:(t,n)=>{console.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;n&&(s.executionContext.settle(n),this.pauseService.onRunEnded(n),this.pausedRuns.delete(n)),i.prune();try{await t.onComplete(e)}finally{await t.onCleanup?.()}},o=this.buildExecutionContext(e.id,t.mode),s;s={workflow:e,mode:t.mode,executionContext:o,factories:new Map,registry:i,hooks:{onPrepare:t.onPrepare,onComplete:a,onResume:t.onResume,onDispatch:t.onDispatch,onCleanup:t.onCleanup}},this.workflows.set(e.id,s);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)}t.executionContext.close(),r.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}watch(e,t){return this.workflows.get(e)?.registry.get(t)}listAllRuns(){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:a,triggerId:o}=r,s=this.workflows.get(a);if(!s)throw new n.SystemError({code:n.ErrorCodes.RESOURCE_RELEASED.code,message:`[WorkflowRuntime] resume() failed: workflow "${a}" is no longer registered. The workflow may have been deregistered while the run was paused.`});let c=this.getOrCreateFactory(s,o),l=s.registry.hold(e),u;try{let t=await c.resume(e);if(!t.ok)throw t.error;u=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||u.container.extend(this.serviceContainer),Object.keys(t).length>0&&u.write(t),u.write({__watch__:i.DELETE_SYMBOL}),await s.hooks.onResume?.(u);let d=await u.run();if(d.ok&&d.value.status===`paused`)await this.drainWatchQueue(e);else{let t=this.workflows.get(a);if(t)try{await t.hooks.onComplete(d)}catch(t){console.error(`[WorkflowRuntime] onComplete hook threw during resume for run "${e}":`,t)}}return d.ok?n.Result.ok(d.value,{runId:e,triggerId:o}):n.Result.fail(d.error,{runId:e,triggerId:o})}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){console.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=>{console.error(`[WorkflowRuntime] ExecutionContext.accept() threw for workflow "${e.workflowId}":`,t)})}}async executePipeline(e,t,i,a){let o=this.timelineStore?new r.TimelineRecorder(this.timelineStore,{}):void 0,s=this.getOrCreateFactory(e,t),c;try{c=await s.prepare(void 0,i),o&&await o.attach(c),await c.store.set({__trigger_event__:a})}catch(e){o&&await o.detach().catch(()=>{});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)}c.container.extend(this.serviceContainer),await e.hooks.onPrepare(c);let l=await c.run();return o&&await o.detach().catch(()=>{}),l}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){console.error(`[WorkflowRuntime] onComplete hook threw for run "${r}":`,e)}}).catch(e=>{console.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){console.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){console.error(`[WorkflowRuntime] Error aborting run "${e}":`,t)}break}}this.pauseService.onRunEnded(e),this.pausedRuns.delete(e)}buildExecutionContext(e,t){switch(t.type){case`transient`:return new c(e,t.concurrency??10,t.capacity??1e3);case`serialized`:return new l(e,t.capacity??1e3);case`singleton_loop`:return new u(e,t.onActive??`drop`,t.replacementGracePeriod??5e3,(e,t)=>this.signal(e,{__singleton_event__:t}));case`exclusive`:return new d(e,t.onActive??`reject`)}}async acquireBusSubscription(e){let t=this.subscriptions.get(e);t||(t=new a.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()}getOrCreateFactory(e,t){let i=e.factories.get(t);if(i)return i;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 r.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=o,exports.SIGNAL_EVENT=s,exports.WorkflowRuntime=p;
package/index.mjs ADDED
@@ -0,0 +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{PipelineFactory as o,PipelineRegistry as s,TimelineRecorder as c,TimelineStore as l}from"@asaidimu/utils-pipeline";import{DELETE_SYMBOL as u,ReactiveDataStore as d}from"@asaidimu/utils-store";import{Serializer as f,SharedResource as p}from"@asaidimu/utils-sync";const m=`__abort__`,h=`__signal__`;var g=class{workflowId;concurrency;capacity;inFlight=0;queueSize=0;closed=!1;constructor(e,t,n){this.workflowId=e,this.concurrency=t,this.capacity=n}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--}}},_=class{workflowId;capacity;serializer;queueDepth=0;closed=!1;constructor(e,t){this.workflowId=e,this.capacity=t,this.serializer=new f({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()}},v=class{workflowId;onActive;replacementGracePeriod;deliverSignal;state=`idle`;activeRunId;settleResolve;closed=!1;constructor(e,t,n,r){this.workflowId=e,this.onActive=t,this.replacementGracePeriod=n,this.deliverSignal=r}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):console.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&&console.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}}},y=class{workflowId;onActive;activeRunId;pending;closed=!1;constructor(e,t){this.workflowId=e,this.onActive=t}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}},b=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))}},x=class{bus;storeRegistry;timelineStore;pauseService;runServiceDefinitions=[];serviceContainer;workflows=new Map;index=new Map;subscriptions=new Map;pausedRuns=new Map;abortUnsubscribe;signalUnsubscribe;constructor(t){this.bus=t.bus,this.storeRegistry=t.storeRegistry,this.timelineStore=t.timelineStore,this.pauseService=new b({bus:this.bus,resume:async(e,t)=>{this.resume(e,t)}}),this.serviceContainer=new e(new d({}));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.`});e.scope===`singleton`?this.serviceContainer.register({key:e.id,factory:()=>e.factory(),scope:`singleton`}):this.runServiceDefinitions.push(e)}this.serviceContainer.register({key:`__pause_service__`,factory:()=>this.pauseService,scope:`singleton`}),this.abortUnsubscribe=this.bus.subscribe(m,e=>{this.abortRun(e.run)}),this.signalUnsubscribe=this.bus.subscribe(h,e=>{this.signal(e.runId,e.patch)})}static async createTestTimelineStore(e=`test-timeline-database`){return new l(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=s.get(`workflow:${e.id}`,{onExpired:(t,n)=>{console.info(`[WorkflowRuntime] Run "${t}" pause-window expired for workflow "${e.id}". Checkpoint: ${n.pipelineId}.`)},onExportFailed:(t,n)=>{console.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;r&&(c.executionContext.settle(r),this.pauseService.onRunEnded(r),this.pausedRuns.delete(r)),n.prune();try{await t.onComplete(e)}finally{await t.onCleanup?.()}},o=this.buildExecutionContext(e.id,t.mode),c;c={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,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)}t.executionContext.close(),s.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}watch(e,t){return this.workflows.get(e)?.registry.get(t)}listAllRuns(){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),d=c.registry.hold(e),f;try{let t=await l.resume(e);if(!t.ok)throw t.error;f=t.value}catch(t){throw new a({code:r.INTERNAL_ERROR.code,message:`[WorkflowRuntime] resume() failed to reconstruct context for run "${e}"`,cause:t})}d||f.container.extend(this.serviceContainer),Object.keys(t).length>0&&f.write(t),f.write({__watch__:u}),await c.hooks.onResume?.(f);let p=await f.run();if(p.ok&&p.value.status===`paused`)await this.drainWatchQueue(e);else{let t=this.workflows.get(o);if(t)try{await t.hooks.onComplete(p)}catch(t){console.error(`[WorkflowRuntime] onComplete hook threw during resume for run "${e}":`,t)}}return p.ok?i.ok(p.value,{runId:e,triggerId:s}):i.fail(p.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){console.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=>{console.error(`[WorkflowRuntime] ExecutionContext.accept() threw for workflow "${e.workflowId}":`,t)})}}async executePipeline(e,t,n,r){let o=this.timelineStore?new c(this.timelineStore,{}):void 0,s=this.getOrCreateFactory(e,t),l;try{l=await s.prepare(void 0,n),o&&await o.attach(l),await l.store.set({__trigger_event__:r})}catch(e){o&&await o.detach().catch(()=>{});let t=e instanceof a?e:new a({code:`PIPELINE_PREPARE_FAILED`,message:e instanceof Error?e.message:String(e)});return i.fail(t)}l.container.extend(this.serviceContainer),await e.hooks.onPrepare(l);let u=await l.run();return o&&await o.detach().catch(()=>{}),u}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){console.error(`[WorkflowRuntime] onComplete hook threw for run "${r}":`,e)}}).catch(e=>{console.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){console.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){console.error(`[WorkflowRuntime] Error aborting run "${e}":`,t)}break}}this.pauseService.onRunEnded(e),this.pausedRuns.delete(e)}buildExecutionContext(e,t){switch(t.type){case`transient`:return new g(e,t.concurrency??10,t.capacity??1e3);case`serialized`:return new _(e,t.capacity??1e3);case`singleton_loop`:return new v(e,t.onActive??`drop`,t.replacementGracePeriod??5e3,(e,t)=>this.signal(e,{__singleton_event__:t}));case`exclusive`:return new y(e,t.onActive??`reject`)}}async acquireBusSubscription(e){let t=this.subscriptions.get(e);t||(t=new p(()=>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()}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 s=new o(i,{storeFactory:async e=>this.storeRegistry.get(e),registry:e.registry});return e.factories.set(t,s),s}generateRunId(e,t){return`${e}:${t}:${Date.now()}:${Math.random().toString(36).slice(2)}`}};export{m as ABORT_EVENT,h as SIGNAL_EVENT,x as WorkflowRuntime};
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@asaidimu/runtime",
3
+ "version": "1.0.0",
4
+ "description": "A runtime for workflows built on \"@asaidimu/utils-pipeline\".",
5
+ "main": "index.js",
6
+ "module": "index.mjs",
7
+ "types": "index.d.ts",
8
+ "keywords": [
9
+ "typescript",
10
+ "utility"
11
+ ],
12
+ "author": "Saidimu <47994458+asaidimu@users.noreply.github.com>",
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/asaidimu/erp-utils.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/asaidimu/erp-utils/issues"
20
+ },
21
+ "homepage": "https://github.com/asaidimu/erp-utils/tree/main/src/runtime#readme",
22
+ "files": [
23
+ "./*"
24
+ ],
25
+ "exports": {
26
+ ".": {
27
+ "import": {
28
+ "types": "./index.d.ts",
29
+ "default": "./index.mjs"
30
+ },
31
+ "require": {
32
+ "types": "./index.d.ts",
33
+ "default": "./index.js"
34
+ }
35
+ }
36
+ },
37
+ "dependencies": {
38
+ "@asaidimu/utils-store": "^10.2.9",
39
+ "@asaidimu/utils-database": "^3.1.13",
40
+ "@asaidimu/utils-sync": "^2.3.4",
41
+ "@asaidimu/utils-pipeline": "^1.3.10",
42
+ "@asaidimu/utils-artifacts": "^8.2.16",
43
+ "@asaidimu/utils-error": "^1.0.0",
44
+ "@asaidimu/utils-events": "^1.2.5"
45
+ },
46
+ "publishConfig": {
47
+ "registry": "https://registry.npmjs.org/",
48
+ "tag": "latest",
49
+ "access": "public"
50
+ },
51
+ "release": {
52
+ "plugins": [
53
+ [
54
+ "@semantic-release/npm",
55
+ {
56
+ "pkgRoot": "./dist"
57
+ }
58
+ ],
59
+ [
60
+ "@semantic-release/git",
61
+ {
62
+ "assets": [
63
+ "CHANGELOG.md",
64
+ "package.json"
65
+ ],
66
+ "message": "chore(release): Release @asaidimu/runtime v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
67
+ }
68
+ ]
69
+ ]
70
+ },
71
+ "peerDependencies": {}
72
+ }