@asaidimu/runtime 1.0.2 → 1.0.4

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/README.md CHANGED
@@ -1,3 +1,389 @@
1
- # `@asaidimu/utils-runtime`
1
+ # `@asaidimu/runtime`
2
2
 
3
- This package provides runtime utilities.
3
+ A workflow runtime for executing pipelines built on `@asaidimu/utils-pipeline`. Routes bus events to workflow triggers, manages run lifecycle (invoke, pause, resume, abort), and provides execution-mode concurrency control.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @asaidimu/runtime
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```ts
14
+ import { WorkflowRuntime } from "@asaidimu/runtime";
15
+ import { createEventBus } from "@core/events";
16
+ import { StoreRegistry } from "@core/store";
17
+
18
+ const bus = createEventBus();
19
+ const storeRegistry = new StoreRegistry();
20
+
21
+ const runtime = new WorkflowRuntime({ bus, storeRegistry });
22
+
23
+ await runtime.register(myWorkflow, {
24
+ mode: { type: "transient", concurrency: 10 },
25
+ onPrepare: async (ctx) => {
26
+ /* set up state */
27
+ },
28
+ onComplete: async (result) => {
29
+ /* handle completion */
30
+ },
31
+ });
32
+ ```
33
+
34
+ ## API
35
+
36
+ ### `WorkflowRuntime`
37
+
38
+ #### Constructor
39
+
40
+ ```ts
41
+ new WorkflowRuntime(options: WorkflowRuntimeOptions)
42
+ ```
43
+
44
+ | Option | Type | Description |
45
+ | --------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
46
+ | `bus` | `EventBus` | Event bus for dispatching and subscribing to workflow events |
47
+ | `storeRegistry` | `StoreRegistry` | Registry for creating workflow state stores |
48
+ | `timelineStore` | `TimelineStore` | Optional store for recording execution timelines. When set, all log messages emitted by pipeline steps via `pcxt.logger` are automatically captured as timeline events with source `"logger"`. |
49
+ | `services` | `ServiceDefinition[]` | Optional global singleton services injected into all pipeline runs |
50
+ | `env` | `Record<string, string \| undefined>` | Optional global environment variables (defaults to `process.env`). Per-workflow overrides can be set on the `Workflow` definition. |
51
+
52
+ #### `static createTestTimelineStore(database?)`
53
+
54
+ Creates a `TimelineStore` backed by IndexedDB, useful in test harnesses.
55
+
56
+ ```ts
57
+ const timelineStore =
58
+ await WorkflowRuntime.createTestTimelineStore("my-test-db");
59
+ ```
60
+
61
+ #### `register(workflow, options)`
62
+
63
+ Registers a workflow with the runtime. Wires up triggers to bus events and execution-mode enforcement. If the workflow declares `services`, a scoped child container is created so those services are visible only to runs of that workflow. If the workflow declares `env`, those variables override the global env layer for runs of this workflow.
64
+
65
+ ```ts
66
+ runtime.register(workflow, {
67
+ mode: { type: "transient", concurrency: 10, capacity: 100 },
68
+ onPrepare: async (ctx) => { ... },
69
+ onComplete: async (result) => { ... },
70
+ onResume: async (ctx) => { ... },
71
+ onCleanup: async () => { ... },
72
+ onDispatch: (result) => { ... },
73
+ });
74
+ ```
75
+
76
+ **Hook order on completion:**
77
+
78
+ 1. `onComplete(result)` — fires immediately when the run finishes
79
+ 2. Internal bookkeeping (`settle`, `onRunEnded`, `registry.prune`)
80
+ 3. `onCleanup()` — fires after internal cleanup is done
81
+
82
+ This ordering ensures the UI is notified before any cleanup runs, and `onCleanup` runs after internal state is settled (safe to deregister the workflow).
83
+
84
+ #### `deregister(workflowId)`
85
+
86
+ Removes a workflow, releases all bus subscriptions, and disposes the workflow's scoped service container.
87
+
88
+ #### `hasWorkflow(workflowId)`
89
+
90
+ Returns `true` if the workflow is registered.
91
+
92
+ #### `listWorkflows()`
93
+
94
+ Returns an array of registered workflow IDs.
95
+
96
+ #### `invoke(workflowId, triggerId, event)`
97
+
98
+ Directly executes a pipeline, bypassing the bus and execution context. Returns a promise that resolves when the run completes or pauses.
99
+
100
+ ```ts
101
+ const result = await runtime.invoke("my-workflow", "my-trigger", event);
102
+
103
+ if (result.ok) {
104
+ if (result.value.status === "paused") {
105
+ const runId = result.metadata!.runId;
106
+ // Resume later via runtime.resume(runId, patch)
107
+ }
108
+ }
109
+ ```
110
+
111
+ #### `resume(runId, patch?)`
112
+
113
+ Resumes a paused run with an optional state patch. Handles:
114
+
115
+ - Pause-window expiry (reconstructs context from checkpoint)
116
+ - Watch drain — if the resumed run re-pauses, checks for buffered events
117
+ - Service injection for reconstructed contexts
118
+
119
+ #### `signal(runId, patch)`
120
+
121
+ Writes a state patch directly into a running run's context.
122
+
123
+ #### `abort(runId)`
124
+
125
+ Aborts a running or paused run.
126
+
127
+ #### `registry(workflowId)`
128
+
129
+ Returns the `PipelineRegistry` for a workflow, or `undefined`.
130
+
131
+ #### `watch(workflowId, runId)`
132
+
133
+ Returns `RunInfo` for a specific run, or `undefined`.
134
+
135
+ #### `listAllRuns()`
136
+
137
+ Returns an array of all runs across all registered workflows.
138
+
139
+ #### `stop()`
140
+
141
+ Gracefully stops the runtime. Deregisters all workflows and unsubscribes from abort/signal events.
142
+
143
+ ## Execution Modes
144
+
145
+ | Mode | Description |
146
+ | ---------------- | -------------------------------------------------------------------------- |
147
+ | `transient` | Concurrency-limited. Drops when queue hits capacity. |
148
+ | `serialized` | FIFO queue, one run at a time. Uses `Serializer`. |
149
+ | `singleton_loop` | At most one active run. On busy: `drop`, `signal`, or `replace`. |
150
+ | `exclusive` | At most one active run. On busy: `reject` or `queue_single` (latest-wins). |
151
+
152
+ ## Pause / Resume
153
+
154
+ Pipelines can pause at a stage boundary. The runtime:
155
+
156
+ 1. Buffers matching events **before** the pause (pre-pause window) and **between** resume cycles (inter-resume buffer).
157
+ 2. On pause, immediately drains any buffered events via `resume()`.
158
+ 3. If the queue is empty, the run "parks" — the next matching bus event triggers resume automatically.
159
+
160
+ The `PauseService` is available to steps as `services.__pause_service__` and provides `register()`, `cancel()`, and condition-based event matching with AND semantics across operators: `==`, `!=`, `>`, `>=`, `<`, `<=`, `exists`.
161
+
162
+ ## Services
163
+
164
+ ### Global services
165
+
166
+ Services available to every pipeline run can be injected at construction. They are registered as singletons on the runtime's internal `ArtifactContainer`.
167
+
168
+ ```ts
169
+ new WorkflowRuntime({
170
+ bus,
171
+ storeRegistry,
172
+ services: [
173
+ {
174
+ id: "my-api",
175
+ factory: () => new ApiClient(), // context is available but unused
176
+ },
177
+ {
178
+ id: "my-context",
179
+ factory: (ctx) => new RequestContext(ctx.runId),
180
+ // ctx has state(), use(), select(), onCleanup(), etc.
181
+ },
182
+ ],
183
+ });
184
+ ```
185
+
186
+ The `factory` receives a full `ArtifactFactoryContext` allowing service factories to use `ctx.use()`, `ctx.select()`, and other artifact container features for dependency injection.
187
+
188
+ `ServiceDefinition` has no `scope` field — global services are always `"singleton"`.
189
+
190
+ ### Workflow-level services
191
+
192
+ Workflows can declare their own services with three scope options:
193
+
194
+ | Scope | Lifetime | Container | Artifact scope |
195
+ | ------------- | ----------------------------------------- | ------------------------------------ | -------------- |
196
+ | `"workflow"` | As long as the workflow is registered | Scoped child of the global container | `"singleton"` |
197
+ | `"run"` | For the duration of a single pipeline run | Run's context container (per-run) | `"singleton"` |
198
+ | `"transient"` | Per-resolution (new instance each time) | Run's context container (per-run) | `"transient"` |
199
+
200
+ **`"workflow"`** (default) services are registered in a scoped child container, isolating them to runs of that workflow. They **cannot** access run state via `select()` because their factory runs against the scoped container's store.
201
+
202
+ **`"run"`** and **`"transient"`** services are registered directly on each run's context container. Their factories resolve against the **run's state store**, so they can use `deps.select()` to read pipeline state and react to state changes.
203
+
204
+ ```ts
205
+ const workflow = {
206
+ id: "my-workflow",
207
+ label: "My Workflow",
208
+ triggers: { ... },
209
+ pipelines: { ... },
210
+ services: [
211
+ // workflow-scoped: shared across all runs, no run state access
212
+ { id: "config", scope: "workflow", factory: () => loadConfig() },
213
+
214
+ // run-scoped: singleton per run, can access run state
215
+ {
216
+ id: "userProfile",
217
+ scope: "run",
218
+ factory: async (ctx) => {
219
+ const userId = await ctx.use((deps) =>
220
+ deps.select((s: any) => s.userId),
221
+ );
222
+ return fetchProfile(userId);
223
+ },
224
+ },
225
+
226
+ // transient: new instance per resolution, can access run state
227
+ { id: "requestId", scope: "transient", factory: () => crypto.randomUUID() },
228
+ ],
229
+ };
230
+ ```
231
+
232
+ ### Reserved service IDs
233
+
234
+ The following service IDs are reserved and cannot be redefined:
235
+
236
+ | ID | Description |
237
+ | ------------------- | ------------------------------------------------------------ |
238
+ | `__pause_service__` | Built-in `PauseService` for pause/resume workflows |
239
+ | `__env__` | Built-in environment variable service for layered env access |
240
+
241
+ Attempting to redefine either throws a `SystemError` with code `DUPLICATE_KEY`.
242
+
243
+ ---
244
+
245
+ ### Environment variables (`__env__`)
246
+
247
+ The runtime provides a built-in `__env__` service for reading environment variables. It supports two layers:
248
+
249
+ - **Global** — set via `WorkflowRuntimeOptions.env` (falls back to `process.env`)
250
+ - **Per-workflow** — set via `Workflow.env`, overrides the global layer for that workflow
251
+
252
+ ```ts
253
+ const runtime = new WorkflowRuntime({
254
+ bus,
255
+ storeRegistry,
256
+ env: { DATABASE_URL: "postgres://..." }, // global default
257
+ });
258
+
259
+ const workflow = {
260
+ id: "my-workflow",
261
+ env: { DATABASE_URL: "postgres://override..." }, // per-workflow override
262
+ // ...
263
+ };
264
+ ```
265
+
266
+ Steps access env vars via `deps.require("__env__")` and call `.get()`:
267
+
268
+ ```ts
269
+ action: async (ctx) => {
270
+ const env = await ctx.use((deps) => deps.require("__env__"));
271
+ const dbUrl = env.get("DATABASE_URL"); // workflow env wins, then global
272
+ };
273
+ ```
274
+
275
+ Resolution order:
276
+
277
+ 1. Per-workflow env layer (if set on `Workflow.env`)
278
+ 2. Global env layer (`WorkflowRuntimeOptions.env` or `process.env`)
279
+ 3. `undefined`
280
+
281
+ #### `EnvService` API
282
+
283
+ ```ts
284
+ interface EnvService {
285
+ get(key: string): string | undefined;
286
+ }
287
+ ```
288
+
289
+ Only `.get()` is exposed — plain property access (`env.KEY`) is not supported. The service is read-only.
290
+
291
+ ### Do's and Don'ts of Services
292
+
293
+ #### Do
294
+
295
+ - **Do** use `"run"`-scoped services when the factory needs to read pipeline state via `select()` — they resolve against the run's state store.
296
+ - **Do** use `"workflow"`-scoped services for shared configuration, API clients, or connections that are identical across all runs of a workflow.
297
+ - **Do** use `__env__` for reading configuration — it correctly resolves per-workflow overrides over the global defaults.
298
+ - **Do** keep service factories pure where possible — prefer injecting state via `select()` over side-effectful construction.
299
+ - **Do** use `ctx.onCleanup()` inside factories to release resources (close sockets, free handles) when the run ends.
300
+
301
+ #### Don't
302
+
303
+ - **Don't** attempt to register a service with ID `__pause_service__` or `__env__` — both are reserved and throw `DUPLICATE_KEY`.
304
+ - **Don't** use `"workflow"`-scoped services for per-run state — they are singletons shared across all runs and cannot access run-specific state via `select()`.
305
+ - **Don't** cache per-run data in `"workflow"`-scoped or global singletons — it will leak across runs.
306
+ - **Don't** mutate the env service or treat it as a plain object — always use `.get("KEY")`.
307
+ - **Don't** assume `process.env` is available — the runtime may be configured with an explicit `env` object that does not include it.
308
+
309
+ ### Service resolution
310
+
311
+ Steps resolve services via `ctx.use()`:
312
+
313
+ ```ts
314
+ action: async (ctx) => {
315
+ const svc = await ctx.use((deps) => deps.require("my-api"));
316
+ };
317
+ ```
318
+
319
+ The `ctx.use()` callback receives a `UseDependencyContext` with `resolve()`, `require()`, and `select()`.
320
+
321
+ ## Container Architecture
322
+
323
+ The runtime uses an `ArtifactContainer` (from `@core/artifacts`) as its internal service container:
324
+
325
+ ```
326
+ Global service container (ArtifactContainer)
327
+ ├── Global singleton services
328
+ ├── __pause_service__ (built-in PauseService)
329
+ ├── __env__ (unscoped fallback — rarely hit)
330
+ └── Scoped containers per workflow
331
+ ├── Workflow-scoped services ("workflow" scope)
332
+ └── Run context extends with scoped __env__ (global + per-workflow layers)
333
+ ```
334
+
335
+ When a pipeline run starts, the run's context container is **extended** with:
336
+
337
+ 1. The workflow's scoped container (for `"workflow"`-scoped services)
338
+ 2. The global service container (for global services and `__pause_service__`)
339
+ 3. `"run"`-scoped and `"transient"` services are registered directly on the run's container, so their factories resolve against the run's state store.
340
+
341
+ On `deregister()`, the workflow's scoped container is disposed, cleaning up all workflow-level artifacts.
342
+
343
+ ### Container capabilities
344
+
345
+ The underlying `ArtifactContainer` provides:
346
+
347
+ | Feature | Description |
348
+ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
349
+ | `register(template)` | Register an artifact with scope (`singleton` / `transient`), lazy/eager loading, timeouts, retries, param key, and serialization opt-in |
350
+ | `resolve(key)` | Resolve an artifact, falling back through parent containers |
351
+ | `require(key)` | Like `resolve` but returns the instance directly, throws on error |
352
+ | `peek(key)` | Synchronous non-resolving lookup |
353
+ | `invalidate(key)` | Invalidate a cached artifact, optionally force rebuild |
354
+ | `extend(parent)` | Add a parent container for fallback resolution |
355
+ | `scope(name)` | Create a namespaced `ScopedContainer` |
356
+ | `watch(key)` | Create an observer that fires on change |
357
+ | `on(event)` | Subscribe to lifecycle events (`build:start`, `build:complete`, `build:error`, `artifact:invalidated`, `artifact:disposed`, `artifact:registered`, `stream:emit`, `container:dispose`) |
358
+ | `export()` | Serialize singleton artifacts for persistence |
359
+ | `from(bundle)` | Static factory: create container from store + optional bundle + templates |
360
+ | `debugInfo()` | Snapshot of all artifacts' status, dependencies, and dependents |
361
+
362
+ ## Internal Architecture
363
+
364
+ ```
365
+ Bus events → dispatch index → ExecutionContext.accept() → spawnRun()
366
+
367
+ ┌────────────────────────────────────────┤
368
+ │ │
369
+ pipeline runs returns runId
370
+ │ │
371
+ ┌───────┴───────┐ ExecutionContext
372
+ │ │ updates inFlight
373
+ paused completed / activeRunId
374
+ │ │
375
+ drainWatchQueue onComplete() → UI notified
376
+ │ │
377
+ resume() bookkeeping
378
+ │ │
379
+ ┌───────┴───────┐ onCleanup()
380
+ │ │
381
+ re-paused completed → onComplete()
382
+ ```
383
+
384
+ ## Testing
385
+
386
+ ```bash
387
+ npm test
388
+ npm run test:watch
389
+ ```
package/index.d.cts CHANGED
@@ -1030,7 +1030,7 @@ interface ArtifactObserver<TRegistry, K extends keyof TRegistry> {
1030
1030
  * @template TArtifact The resolved type of the artifact instance.
1031
1031
  * @template TRegistry The type mapping of all artifacts in the container.
1032
1032
  */
1033
- interface ArtifactTemplate<TState extends object, TArtifact, TRegistry extends Record<string, any> = Record<string, any>, TExtra extends Record<string, any> = {}> {
1033
+ interface ArtifactTemplate<TState extends object, TArtifact, TRegistry extends Record<string, any> = Record<string, any>, TExtra extends Record<string, any> = {}, K extends Record<string, unknown> = Record<string, unknown>> {
1034
1034
  /** The unique key identifying this artifact within the registry. */
1035
1035
  key: keyof TRegistry;
1036
1036
  /** The factory function responsible for creating the artifact's instance. */
@@ -1064,7 +1064,7 @@ interface ArtifactTemplate<TState extends object, TArtifact, TRegistry extends R
1064
1064
  */
1065
1065
  debounce?: number;
1066
1066
  /** If defined, the artifact is parameterized. Receives the user‑supplied params and returns a unique string key. */
1067
- paramKey?: (params: Record<string, unknown>) => string;
1067
+ paramKey?: (params: K) => string;
1068
1068
  virtual?: true;
1069
1069
  /**
1070
1070
  * If `true`, the artifact's instance can be serialized and included in
@@ -1351,6 +1351,19 @@ declare class ArtifactRegistry<TRegistry extends Record<string, any> = Record<st
1351
1351
  * Levels are ordered by increasing severity: trace, debug, info, warn, error.
1352
1352
  */
1353
1353
  type LogLevel = "trace" | "debug" | "info" | "warn" | "error";
1354
+ /**
1355
+ * Defines a destination where system logs are outputted or processed.
1356
+ * Custom sinks (e.g., File, Logstash, Sentry) must implement this interface.
1357
+ */
1358
+ interface LogSink {
1359
+ /**
1360
+ * Dispatches a structured log entry to the underlying destination.
1361
+ *
1362
+ * @param record - The complete log object containing metadata and payload.
1363
+ * @returns A void or Promise that resolves when the log has been safely handed off.
1364
+ */
1365
+ write(record: SystemLog): void | Promise<void>;
1366
+ }
1354
1367
  /**
1355
1368
  * Structure representing a fully contextualized system log entry ready for sinking.
1356
1369
  */
@@ -1367,7 +1380,8 @@ interface SystemLog {
1367
1380
  timestamp: number;
1368
1381
  }
1369
1382
  /**
1370
- * Primary logger interface providing level-specific log dispatching and context-chaining.
1383
+ * Primary logger interface providing level-specific log dispatching, context-chaining,
1384
+ * and dynamic sink management.
1371
1385
  */
1372
1386
  interface SystemLogger {
1373
1387
  /** Logs high-volume, extremely fine-grained diagnostic details. */
@@ -1394,6 +1408,22 @@ interface SystemLogger {
1394
1408
  * @param context - The metadata to append to all subsequent logs emitted by the child logger.
1395
1409
  */
1396
1410
  child(context: object): SystemLogger;
1411
+ /**
1412
+ * Adds a new sink to this logger instance.
1413
+ * The sink will receive all logs emitted by this logger and its descendants (unless
1414
+ * a descendant removes it). This operation does not affect the parent logger.
1415
+ *
1416
+ * @param sink - The LogSink to add.
1417
+ */
1418
+ addSink(sink: LogSink): void;
1419
+ /**
1420
+ * Removes a previously added sink from this logger instance.
1421
+ * Returns `true` if the sink was found and removed, otherwise `false`.
1422
+ * This operation does not affect the parent logger.
1423
+ *
1424
+ * @param sink - The LogSink to remove.
1425
+ */
1426
+ removeSink(sink: LogSink): boolean;
1397
1427
  }
1398
1428
  //#endregion
1399
1429
  //#region src/artifacts/observer.d.ts
@@ -1467,7 +1497,14 @@ declare class ArtifactObserverManager<TRegistry extends Record<string, any>, TSt
1467
1497
  }
1468
1498
  //#endregion
1469
1499
  //#region src/artifacts/manager.d.ts
1470
- declare class ArtifactManager<TRegistry extends Record<string, any>, TState extends object> {
1500
+ /** Minimal interface for parent-chain resolution operations. */
1501
+ interface ParentChainManager {
1502
+ build(key: string, parentPath?: Array<any>, templateKey?: string): Promise<KeyedResolvedArtifact<any, any>>;
1503
+ invalidate(key: string, replace?: boolean, fatal?: boolean): Promise<void>;
1504
+ has(key: string): boolean;
1505
+ getCached(key: string): CachedArtifact<any, any> | undefined;
1506
+ }
1507
+ declare class ArtifactManager<TRegistry extends Record<string, any>, TState extends object> implements ParentChainManager {
1471
1508
  private readonly registry;
1472
1509
  private readonly cache;
1473
1510
  private readonly graph;
@@ -1476,10 +1513,14 @@ declare class ArtifactManager<TRegistry extends Record<string, any>, TState exte
1476
1513
  private readonly logger;
1477
1514
  private readonly events?;
1478
1515
  /** Parent containers used as fallback during resolution */
1479
- readonly parentContainers: ArtifactManager<any, any>[];
1516
+ readonly parentContainers: ParentChainManager[];
1480
1517
  constructor(registry: ArtifactRegistry<TRegistry, TState>, cache: ArtifactCache<TRegistry>, graph: ArtifactDependencyGraph, store: Pick<DataStore<TState>, "watch" | "get" | "set">, observer: ArtifactObserverManager<TRegistry, TState>, logger: SystemLogger, events?: EventBus<ArtifactLifecycleEventMap> | undefined);
1481
- addParent(parent: ArtifactManager<any, any>): void;
1482
- removeParent(parent: ArtifactManager<any, any>): void;
1518
+ addParent(parent: ParentChainManager): void;
1519
+ removeParent(parent: ParentChainManager): void;
1520
+ /** @inheritdoc */
1521
+ has(key: string): boolean;
1522
+ /** @inheritdoc */
1523
+ getCached(key: string): CachedArtifact<any, any> | undefined;
1483
1524
  build(key: string, parentPath?: Array<keyof TRegistry>, templateKey?: string): Promise<KeyedResolvedArtifact<TRegistry, any>>;
1484
1525
  private buildFromParent;
1485
1526
  private executeBuild;
@@ -1501,7 +1542,51 @@ declare class ArtifactManager<TRegistry extends Record<string, any>, TState exte
1501
1542
  computeParamKey<K extends keyof TRegistry>(key: K, params: Record<string, unknown>): string;
1502
1543
  }
1503
1544
  //#endregion
1545
+ //#region src/artifacts/scoped-container.d.ts
1546
+ declare class ScopedContainer<TRegistry extends Record<string, any> = Record<string, any>, TState extends object = any> implements ContainerLike {
1547
+ private readonly container;
1548
+ readonly name: string;
1549
+ private readonly prefix;
1550
+ private readonly keys;
1551
+ private disposed;
1552
+ private readonly scopedManager;
1553
+ constructor(container: ArtifactContainer<TRegistry, TState>, name: string);
1554
+ /**
1555
+ * Returns a scope-aware parent-chain manager that mangles keys with the
1556
+ * scope prefix. When used via extend(), the child's parentContainers will
1557
+ * only see scoped artifacts — not the underlying container's global ones.
1558
+ */
1559
+ get manager(): ParentChainManager;
1560
+ /** Expose the parent container's event bus so this scope can act as an extend parent. */
1561
+ get events(): EventBus<ArtifactLifecycleEventMap>;
1562
+ private ensureNotDisposed;
1563
+ private mangle;
1564
+ register<K extends keyof TRegistry>(params: ArtifactTemplate<TState, TRegistry[K], TRegistry>): () => void;
1565
+ dispose(): Promise<void>;
1566
+ resolve<K extends keyof TRegistry>(key: K, params?: Record<string, unknown>): Promise<KeyedResolvedArtifact<TRegistry, K>>;
1567
+ require<K extends keyof TRegistry>(key: K, params?: Record<string, unknown>): Promise<TRegistry[K]>;
1568
+ has<K extends keyof TRegistry>(key: K): boolean;
1569
+ peek<K extends keyof TRegistry>(key: K, params?: Record<string, unknown>): TRegistry[K] | undefined;
1570
+ invalidate<K extends keyof TRegistry>(key: K, options?: {
1571
+ replace?: boolean;
1572
+ params?: Record<string, unknown>;
1573
+ }): Promise<void>;
1574
+ unregister<K extends keyof TRegistry>(key: K, params?: Record<string, unknown>): Promise<void>;
1575
+ watch<K extends keyof TRegistry>(key: K, params?: Record<string, unknown>, ttl?: number): ArtifactObserver<TRegistry, K>;
1576
+ notifyObservers(key: string): void;
1577
+ hasWatchers(key: string): boolean;
1578
+ }
1579
+ //#endregion
1504
1580
  //#region src/artifacts/container.d.ts
1581
+ /** Minimum interface required for something to act as a parent via extend(). */
1582
+ interface ContainerLike {
1583
+ readonly manager: ParentChainManager;
1584
+ readonly events: EventBus<ArtifactLifecycleEventMap>;
1585
+ has(key: any): boolean;
1586
+ peek(key: any, params?: Record<string, unknown>): any;
1587
+ resolve(key: any, params?: Record<string, unknown>): Promise<any>;
1588
+ watch(key: any, params?: Record<string, unknown>, ttl?: number): ArtifactObserver<any, any>;
1589
+ }
1505
1590
  declare class ArtifactContainer<TRegistry extends Record<string, any> = Record<string, any>, TState extends object = any> {
1506
1591
  private readonly registry;
1507
1592
  private readonly cache;
@@ -1512,9 +1597,11 @@ declare class ArtifactContainer<TRegistry extends Record<string, any> = Record<s
1512
1597
  private readonly store;
1513
1598
  readonly events: EventBus<ArtifactLifecycleEventMap>;
1514
1599
  /** Parent containers used as fallback during resolution */
1515
- readonly parents: ArtifactContainer<any, any>[];
1600
+ readonly parents: ContainerLike[];
1516
1601
  /** Cleanup functions for parent event subscriptions */
1517
1602
  private parentCleanups;
1603
+ /** Track used scope names to enforce uniqueness */
1604
+ readonly scopeNames: Set<string>;
1518
1605
  constructor(store: Pick<DataStore<TState>, "watch" | "get" | "set" | "subset">, options?: {
1519
1606
  logger?: SystemLogger;
1520
1607
  });
@@ -1535,14 +1622,28 @@ declare class ArtifactContainer<TRegistry extends Record<string, any> = Record<s
1535
1622
  once<TEventName extends keyof ArtifactLifecycleEventMap>(eventName: TEventName, callback: (payload: ArtifactLifecycleEventMap[TEventName]) => void): () => void;
1536
1623
  once(eventName: "*", callback: (payload: ArtifactLifecycleEventMap[keyof ArtifactLifecycleEventMap], event: keyof ArtifactLifecycleEventMap) => void): () => void;
1537
1624
  /**
1538
- * Extends this container with a parent container. When resolution of a key
1539
- * fails locally, each parent is tried in order. Returned artifacts are cached
1540
- * as lightweight proxies with the instance from the parent. Invalidation
1541
- * propagates from parent to child transparently.
1625
+ * Extends this container with a parent container or scoped container.
1626
+ * When resolution of a key fails locally, each parent is tried in order.
1627
+ * Invalidation propagates from parent to child transparently.
1628
+ *
1629
+ * When the parent is an `ArtifactContainer`, read-only methods (`has`,
1630
+ * `peek`, `watch`) also delegate to it. For `ScopedContainer` parents,
1631
+ * only invalidation propagation and manager wiring are established.
1542
1632
  *
1543
1633
  * @returns A function to un-extend the parent.
1544
1634
  */
1545
- extend(parent: ArtifactContainer<any, any>): () => void;
1635
+ extend(parent: ContainerLike): () => void;
1636
+ /**
1637
+ * Creates a scoped view of this container. Artifacts registered on the
1638
+ * scoped container get their keys prefixed with `"<name>::"`, making them
1639
+ * invisible to other scopes but discoverable from this container.
1640
+ *
1641
+ * Scope names must be unique per container — a duplicate name throws.
1642
+ *
1643
+ * Dependencies resolved inside a scoped artifact's factory (via `use()`) try
1644
+ * the scoped key first, then fall back to the unscoped (global) key.
1645
+ */
1646
+ scope(name: string): ScopedContainer<TRegistry, TState>;
1546
1647
  notifyObservers(key: string): void;
1547
1648
  hasWatchers(key: string): boolean;
1548
1649
  dispose(): Promise<void>;
@@ -13523,6 +13624,17 @@ declare class TimelineStore<S = any> implements ITimelineEventSource<S> {
13523
13624
  private allDocs;
13524
13625
  }
13525
13626
  //#endregion
13627
+ //#region src/runtime/types/service.d.ts
13628
+ type ServiceDefinition<T> = {
13629
+ id: string;
13630
+ factory: ArtifactFactory<any, any, T>;
13631
+ };
13632
+ type WorkflowServiceDefinition<T = unknown> = {
13633
+ id: string;
13634
+ scope?: "workflow" | "run" | "transient";
13635
+ factory: ArtifactFactory<any, any, T>;
13636
+ };
13637
+ //#endregion
13526
13638
  //#region src/runtime/types/workflow.d.ts
13527
13639
  type WorkflowState<T extends Record<string, any> = Record<string, any>> = T;
13528
13640
  interface WorkflowEvent<Payload = unknown> {
@@ -13541,24 +13653,10 @@ interface Workflow<T extends Record<string, any> = Record<string, any>> {
13541
13653
  label: string;
13542
13654
  triggers: Record<string, WorkflowTrigger>;
13543
13655
  pipelines: Record<string, RoutingPipelineDefinition<WorkflowState<T>>>;
13656
+ services?: WorkflowServiceDefinition[];
13657
+ env?: Record<string, string | undefined>;
13544
13658
  }
13545
13659
  //#endregion
13546
- //#region src/runtime/types/service.d.ts
13547
- interface RunServiceContext {
13548
- runId: string;
13549
- workflowId: string;
13550
- triggerId: string;
13551
- }
13552
- type ServiceDefinition<T> = {
13553
- id: string;
13554
- scope: "singleton";
13555
- factory: () => T | Promise<T>;
13556
- } | {
13557
- id: string;
13558
- scope: "transient";
13559
- factory: (ctx: RunServiceContext) => T | Promise<T>;
13560
- };
13561
- //#endregion
13562
13660
  //#region src/runtime/types/runtime.d.ts
13563
13661
  declare const ABORT_EVENT: "__abort__";
13564
13662
  declare const SIGNAL_EVENT: "__signal__";
@@ -13596,9 +13694,11 @@ type InvokeResult = Result<PipelineRunResult<WorkflowState>, SystemError, {
13596
13694
  }>;
13597
13695
  interface WorkflowRuntimeOptions {
13598
13696
  bus: EventBus<Record<string, any>>;
13697
+ logger?: SystemLogger;
13599
13698
  storeRegistry: StoreRegistry<WorkflowState>;
13600
13699
  timelineStore?: TimelineStore<WorkflowState>;
13601
13700
  services?: ServiceDefinition<unknown>[];
13701
+ env?: Record<string, string | undefined>;
13602
13702
  }
13603
13703
  interface RegisterOptions {
13604
13704
  mode: WorkflowExecutionMode;
@@ -13616,10 +13716,10 @@ declare class WorkflowRuntime {
13616
13716
  private readonly timelineStore;
13617
13717
  /** Built-in watch service. Always present. */
13618
13718
  private readonly pauseService;
13619
- /** User-defined run-scoped service definitions, instantiated per run. */
13620
- private readonly runServiceDefinitions;
13621
13719
  /** Global container holding all registered services accessible by steps. */
13622
13720
  private readonly serviceContainer;
13721
+ /** Store backing the global container — used to update reactive env state. */
13722
+ private readonly serviceStore;
13623
13723
  private readonly workflows;
13624
13724
  private readonly index;
13625
13725
  private readonly subscriptions;
@@ -13628,6 +13728,7 @@ declare class WorkflowRuntime {
13628
13728
  * Populated when a run pauses. Cleared when run ends or is aborted.
13629
13729
  */
13630
13730
  private readonly pausedRuns;
13731
+ private readonly logger;
13631
13732
  private readonly abortUnsubscribe;
13632
13733
  private readonly signalUnsubscribe;
13633
13734
  constructor(options: WorkflowRuntimeOptions);
@@ -13638,14 +13739,28 @@ declare class WorkflowRuntime {
13638
13739
  listWorkflows(): string[];
13639
13740
  stop(): Promise<void>;
13640
13741
  registry(workflowId: string): PipelineRegistry<WorkflowState> | undefined;
13641
- watch(workflowId: string, runId: string): RunInfo<WorkflowState> | undefined;
13642
- listAllRuns(): Array<RunInfo<WorkflowState> & {
13742
+ info(workflowId: string, runId: string): RunInfo<WorkflowState> | undefined;
13743
+ list(): Array<RunInfo<WorkflowState> & {
13643
13744
  workflowId: string;
13644
13745
  }>;
13645
13746
  invoke(workflowId: string, triggerId: string, event: WorkflowEvent): Promise<InvokeResult>;
13646
13747
  resume(runId: string, patch?: DeepPartial<WorkflowState>): Promise<InvokeResult>;
13647
13748
  signal(runId: string, patch: DeepPartial<WorkflowState>): Promise<void>;
13648
13749
  private dispatch;
13750
+ /**
13751
+ * Creates a TimelineRecorder for the given workflow, resolving the
13752
+ * per-workflow sanitizer from the global service container.
13753
+ *
13754
+ * Returns `undefined` when no timelineStore is configured.
13755
+ */
13756
+ private createRecorder;
13757
+ /**
13758
+ * Attaches a TimelineRecorder (when one is provided) to the run context,
13759
+ * invokes the callback, and detaches it in a finally block.
13760
+ *
13761
+ * When no recorder is provided the callback runs without recording.
13762
+ */
13763
+ private withRecorder;
13649
13764
  private executePipeline;
13650
13765
  private spawnRun;
13651
13766
  private drainWatchQueue;
@@ -13653,10 +13768,28 @@ declare class WorkflowRuntime {
13653
13768
  private buildExecutionContext;
13654
13769
  private acquireBusSubscription;
13655
13770
  private releaseBusSubscription;
13771
+ /**
13772
+ * Extends `ctx.container` with the workflow's scoped container (if any) and
13773
+ * the global service container, then:
13774
+ * - Eagerly resolves the parameterized __scoped_env_service__ for this
13775
+ * workflow and registers the captured EnvService as __env__ (frozen for
13776
+ * the run).
13777
+ * - Shadows __env_service__ and __scoped_env_service__ so steps cannot
13778
+ * access them via container extension bubbling.
13779
+ * - Registers per‑run and transient workflow services on the run's
13780
+ * container so their factories resolve against the run's state store.
13781
+ */
13782
+ private extendContextContainer;
13656
13783
  private getOrCreateFactory;
13657
13784
  private generateRunId;
13658
13785
  }
13659
13786
  //#endregion
13787
+ //#region src/runtime/types/env.d.ts
13788
+ interface EnvService {
13789
+ get(key: string): string | undefined;
13790
+ list(): Record<string, string | undefined>;
13791
+ }
13792
+ //#endregion
13660
13793
  //#region src/runtime/types/pause.d.ts
13661
13794
  type PauseOperator = ">=" | "<=" | "==" | "!=" | ">" | "<" | "exists";
13662
13795
  interface PauseCondition {
@@ -13670,4 +13803,4 @@ interface PauseDescriptor {
13670
13803
  patch?: DeepPartial<WorkflowState>;
13671
13804
  }
13672
13805
  //#endregion
13673
- 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 };
13806
+ export { ABORT_EVENT, type DispatchResult, type EnvService, type InvokeResult, type PauseCondition, type PauseDescriptor, type PauseOperator, type RegisterOptions, SIGNAL_EVENT, type ServiceDefinition, type Workflow, type WorkflowEvent, type WorkflowExecutionMode, WorkflowRuntime, type WorkflowRuntimeOptions, type WorkflowServiceDefinition, type WorkflowState, type WorkflowTrigger };
package/index.d.ts CHANGED
@@ -1,8 +1,21 @@
1
+ import { ArtifactContainer, ArtifactFactory } from "@asaidimu/utils-artifacts";
1
2
  import { Result, SystemError } from "@asaidimu/utils-error";
3
+ import { SystemLogger } from "@asaidimu/utils-logger";
2
4
  import { PipelineFactory, PipelineRegistry, PipelineRunResult, RoutingPipelineDefinition, RunContext, RunInfo, TimelineStore } from "@asaidimu/utils-pipeline";
3
5
  import { DeepPartial, StoreRegistry } from "@asaidimu/utils-store";
4
6
  import { EventBus } from "@asaidimu/utils-events";
5
7
 
8
+ //#region src/runtime/types/service.d.ts
9
+ type ServiceDefinition<T> = {
10
+ id: string;
11
+ factory: ArtifactFactory<any, any, T>;
12
+ };
13
+ type WorkflowServiceDefinition<T = unknown> = {
14
+ id: string;
15
+ scope?: "workflow" | "run" | "transient";
16
+ factory: ArtifactFactory<any, any, T>;
17
+ };
18
+ //#endregion
6
19
  //#region src/runtime/types/workflow.d.ts
7
20
  type WorkflowState<T extends Record<string, any> = Record<string, any>> = T;
8
21
  interface WorkflowEvent<Payload = unknown> {
@@ -21,24 +34,10 @@ interface Workflow<T extends Record<string, any> = Record<string, any>> {
21
34
  label: string;
22
35
  triggers: Record<string, WorkflowTrigger>;
23
36
  pipelines: Record<string, RoutingPipelineDefinition<WorkflowState<T>>>;
37
+ services?: WorkflowServiceDefinition[];
38
+ env?: Record<string, string | undefined>;
24
39
  }
25
40
  //#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
41
  //#region src/runtime/types/runtime.d.ts
43
42
  declare const ABORT_EVENT: "__abort__";
44
43
  declare const SIGNAL_EVENT: "__signal__";
@@ -76,9 +75,11 @@ type InvokeResult = Result<PipelineRunResult<WorkflowState>, SystemError, {
76
75
  }>;
77
76
  interface WorkflowRuntimeOptions {
78
77
  bus: EventBus<Record<string, any>>;
78
+ logger?: SystemLogger;
79
79
  storeRegistry: StoreRegistry<WorkflowState>;
80
80
  timelineStore?: TimelineStore<WorkflowState>;
81
81
  services?: ServiceDefinition<unknown>[];
82
+ env?: Record<string, string | undefined>;
82
83
  }
83
84
  interface RegisterOptions {
84
85
  mode: WorkflowExecutionMode;
@@ -96,10 +97,10 @@ declare class WorkflowRuntime {
96
97
  private readonly timelineStore;
97
98
  /** Built-in watch service. Always present. */
98
99
  private readonly pauseService;
99
- /** User-defined run-scoped service definitions, instantiated per run. */
100
- private readonly runServiceDefinitions;
101
100
  /** Global container holding all registered services accessible by steps. */
102
101
  private readonly serviceContainer;
102
+ /** Store backing the global container — used to update reactive env state. */
103
+ private readonly serviceStore;
103
104
  private readonly workflows;
104
105
  private readonly index;
105
106
  private readonly subscriptions;
@@ -108,6 +109,7 @@ declare class WorkflowRuntime {
108
109
  * Populated when a run pauses. Cleared when run ends or is aborted.
109
110
  */
110
111
  private readonly pausedRuns;
112
+ private readonly logger;
111
113
  private readonly abortUnsubscribe;
112
114
  private readonly signalUnsubscribe;
113
115
  constructor(options: WorkflowRuntimeOptions);
@@ -118,14 +120,28 @@ declare class WorkflowRuntime {
118
120
  listWorkflows(): string[];
119
121
  stop(): Promise<void>;
120
122
  registry(workflowId: string): PipelineRegistry<WorkflowState> | undefined;
121
- watch(workflowId: string, runId: string): RunInfo<WorkflowState> | undefined;
122
- listAllRuns(): Array<RunInfo<WorkflowState> & {
123
+ info(workflowId: string, runId: string): RunInfo<WorkflowState> | undefined;
124
+ list(): Array<RunInfo<WorkflowState> & {
123
125
  workflowId: string;
124
126
  }>;
125
127
  invoke(workflowId: string, triggerId: string, event: WorkflowEvent): Promise<InvokeResult>;
126
128
  resume(runId: string, patch?: DeepPartial<WorkflowState>): Promise<InvokeResult>;
127
129
  signal(runId: string, patch: DeepPartial<WorkflowState>): Promise<void>;
128
130
  private dispatch;
131
+ /**
132
+ * Creates a TimelineRecorder for the given workflow, resolving the
133
+ * per-workflow sanitizer from the global service container.
134
+ *
135
+ * Returns `undefined` when no timelineStore is configured.
136
+ */
137
+ private createRecorder;
138
+ /**
139
+ * Attaches a TimelineRecorder (when one is provided) to the run context,
140
+ * invokes the callback, and detaches it in a finally block.
141
+ *
142
+ * When no recorder is provided the callback runs without recording.
143
+ */
144
+ private withRecorder;
129
145
  private executePipeline;
130
146
  private spawnRun;
131
147
  private drainWatchQueue;
@@ -133,10 +149,28 @@ declare class WorkflowRuntime {
133
149
  private buildExecutionContext;
134
150
  private acquireBusSubscription;
135
151
  private releaseBusSubscription;
152
+ /**
153
+ * Extends `ctx.container` with the workflow's scoped container (if any) and
154
+ * the global service container, then:
155
+ * - Eagerly resolves the parameterized __scoped_env_service__ for this
156
+ * workflow and registers the captured EnvService as __env__ (frozen for
157
+ * the run).
158
+ * - Shadows __env_service__ and __scoped_env_service__ so steps cannot
159
+ * access them via container extension bubbling.
160
+ * - Registers per‑run and transient workflow services on the run's
161
+ * container so their factories resolve against the run's state store.
162
+ */
163
+ private extendContextContainer;
136
164
  private getOrCreateFactory;
137
165
  private generateRunId;
138
166
  }
139
167
  //#endregion
168
+ //#region src/runtime/types/env.d.ts
169
+ interface EnvService {
170
+ get(key: string): string | undefined;
171
+ list(): Record<string, string | undefined>;
172
+ }
173
+ //#endregion
140
174
  //#region src/runtime/types/pause.d.ts
141
175
  type PauseOperator = ">=" | "<=" | "==" | "!=" | ">" | "<" | "exists";
142
176
  interface PauseCondition {
@@ -150,4 +184,4 @@ interface PauseDescriptor {
150
184
  patch?: DeepPartial<WorkflowState>;
151
185
  }
152
186
  //#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 };
187
+ export { ABORT_EVENT, type DispatchResult, type EnvService, type InvokeResult, type PauseCondition, type PauseDescriptor, type PauseOperator, type RegisterOptions, SIGNAL_EVENT, type ServiceDefinition, type Workflow, type WorkflowEvent, type WorkflowExecutionMode, WorkflowRuntime, type WorkflowRuntimeOptions, type WorkflowServiceDefinition, type WorkflowState, type WorkflowTrigger };
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-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;
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??process.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}),await s.hooks.onResume?.(f);let p=await this.withRecorder(f,async()=>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);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),await e.hooks.onPrepare(c),this.withRecorder(c,async()=>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{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};
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}from"@asaidimu/utils-pipeline";import{DocumentSanitizer as d,commonEnvPatterns as f,newSecureDefaultConfig as p}from"@asaidimu/utils-sanitize";import{DELETE_SYMBOL as m,ReactiveDataStore as h}from"@asaidimu/utils-store";import{Serializer as g,SharedResource as _}from"@asaidimu/utils-sync";const v=`__abort__`,y=`__signal__`;var b=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}}}},x=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--}}},S=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 g({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()}},C=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}}},w=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}},T=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))}},E=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 T({bus:this.bus,resume:async(e,t)=>{this.resume(e,t)}}),this.serviceStore=new h({env:{global:t.env??process.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 b;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=p();return r.patterns=[...r.patterns,...f([],n)],new d(r,t.workflowId)},scope:`singleton`}),this.abortUnsubscribe=this.bus.subscribe(v,e=>{this.abortRun(e.run)}),this.signalUnsubscribe=this.bus.subscribe(y,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]:m}})),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__:m}),await c.hooks.onResume?.(p);let h=await this.withRecorder(p,async()=>await p.run(),d);if(h.ok&&h.value.status===`paused`)await this.drainWatchQueue(e);else{let t=this.workflows.get(o);if(t)try{await t.hooks.onComplete(h)}catch(t){this.logger.error(`[WorkflowRuntime] onComplete hook threw during resume for run "${e}":`,t)}}return h.ok?i.ok(h.value,{runId:e,triggerId:s}):i.fail(h.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);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),await e.hooks.onPrepare(l),this.withRecorder(l,async()=>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 x(e,t.concurrency??10,t.capacity??1e3,n);case`serialized`:return new S(e,t.capacity??1e3,n);case`singleton_loop`:return new C(e,t.onActive??`drop`,t.replacementGracePeriod??5e3,(e,t)=>this.signal(e,{__singleton_event__:t}),n);case`exclusive`:return new w(e,t.onActive??`reject`,n)}}async acquireBusSubscription(e){let t=this.subscriptions.get(e);t||(t=new _(()=>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{v as ABORT_EVENT,y as SIGNAL_EVENT,E as WorkflowRuntime};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asaidimu/runtime",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "A runtime for workflows built on \"@asaidimu/utils-pipeline\".",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -35,13 +35,15 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@asaidimu/utils-store": "^10.2.11",
39
- "@asaidimu/utils-database": "^3.1.14",
40
- "@asaidimu/utils-sync": "^2.3.5",
41
- "@asaidimu/utils-pipeline": "^1.3.11",
42
- "@asaidimu/utils-artifacts": "^8.2.18",
38
+ "@asaidimu/utils-store": "^10.2.12",
39
+ "@asaidimu/utils-database": "^3.1.15",
40
+ "@asaidimu/utils-sync": "^2.3.6",
41
+ "@asaidimu/utils-pipeline": "^1.3.12",
42
+ "@asaidimu/utils-artifacts": "^8.2.19",
43
43
  "@asaidimu/utils-error": "^1.0.0",
44
- "@asaidimu/utils-events": "^1.2.6"
44
+ "@asaidimu/utils-events": "^1.2.7",
45
+ "@asaidimu/utils-sanitize": "^1.0.5",
46
+ "@asaidimu/utils-logger": "^1.0.9"
45
47
  },
46
48
  "publishConfig": {
47
49
  "registry": "https://registry.npmjs.org/",