@asaidimu/runtime 1.0.3 → 1.0.5
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 +388 -2
- package/index.d.cts +179 -34
- package/index.d.ts +55 -21
- package/index.js +1 -1
- package/index.mjs +1 -1
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -1,3 +1,389 @@
|
|
|
1
|
-
# `@asaidimu/
|
|
1
|
+
# `@asaidimu/runtime`
|
|
2
2
|
|
|
3
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
1482
|
-
removeParent(parent:
|
|
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:
|
|
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
|
|
1539
|
-
* fails locally, each parent is tried in order.
|
|
1540
|
-
*
|
|
1541
|
-
*
|
|
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:
|
|
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>;
|
|
@@ -2240,6 +2341,18 @@ interface TimelineStoreEvents<S = any> {
|
|
|
2240
2341
|
};
|
|
2241
2342
|
}
|
|
2242
2343
|
//#endregion
|
|
2344
|
+
//#region src/pipeline/pollyfill.d.ts
|
|
2345
|
+
type NodeSetImmediate = typeof globalThis extends {
|
|
2346
|
+
setImmediate: infer T;
|
|
2347
|
+
} ? T : never;
|
|
2348
|
+
type NodeClearImmediate = typeof globalThis extends {
|
|
2349
|
+
clearImmediate: infer T;
|
|
2350
|
+
} ? T : never;
|
|
2351
|
+
declare global {
|
|
2352
|
+
function setImmediate(callback: (...args: any[]) => void, ...args: any[]): NodeSetImmediate extends never ? number : ReturnType<any>;
|
|
2353
|
+
function clearImmediate(id: NodeClearImmediate extends never ? number : Parameters<any>[0]): void;
|
|
2354
|
+
}
|
|
2355
|
+
//#endregion
|
|
2243
2356
|
//#region node_modules/.bun/@asaidimu+query@1.1.2/node_modules/@asaidimu/query/index.d.ts
|
|
2244
2357
|
/**
|
|
2245
2358
|
* Query-related type definitions.
|
|
@@ -13523,6 +13636,17 @@ declare class TimelineStore<S = any> implements ITimelineEventSource<S> {
|
|
|
13523
13636
|
private allDocs;
|
|
13524
13637
|
}
|
|
13525
13638
|
//#endregion
|
|
13639
|
+
//#region src/runtime/types/service.d.ts
|
|
13640
|
+
type ServiceDefinition<T> = {
|
|
13641
|
+
id: string;
|
|
13642
|
+
factory: ArtifactFactory<any, any, T>;
|
|
13643
|
+
};
|
|
13644
|
+
type WorkflowServiceDefinition<T = unknown> = {
|
|
13645
|
+
id: string;
|
|
13646
|
+
scope?: "workflow" | "run" | "transient";
|
|
13647
|
+
factory: ArtifactFactory<any, any, T>;
|
|
13648
|
+
};
|
|
13649
|
+
//#endregion
|
|
13526
13650
|
//#region src/runtime/types/workflow.d.ts
|
|
13527
13651
|
type WorkflowState<T extends Record<string, any> = Record<string, any>> = T;
|
|
13528
13652
|
interface WorkflowEvent<Payload = unknown> {
|
|
@@ -13541,24 +13665,10 @@ interface Workflow<T extends Record<string, any> = Record<string, any>> {
|
|
|
13541
13665
|
label: string;
|
|
13542
13666
|
triggers: Record<string, WorkflowTrigger>;
|
|
13543
13667
|
pipelines: Record<string, RoutingPipelineDefinition<WorkflowState<T>>>;
|
|
13668
|
+
services?: WorkflowServiceDefinition[];
|
|
13669
|
+
env?: Record<string, string | undefined>;
|
|
13544
13670
|
}
|
|
13545
13671
|
//#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
13672
|
//#region src/runtime/types/runtime.d.ts
|
|
13563
13673
|
declare const ABORT_EVENT: "__abort__";
|
|
13564
13674
|
declare const SIGNAL_EVENT: "__signal__";
|
|
@@ -13596,9 +13706,11 @@ type InvokeResult = Result<PipelineRunResult<WorkflowState>, SystemError, {
|
|
|
13596
13706
|
}>;
|
|
13597
13707
|
interface WorkflowRuntimeOptions {
|
|
13598
13708
|
bus: EventBus<Record<string, any>>;
|
|
13709
|
+
logger?: SystemLogger;
|
|
13599
13710
|
storeRegistry: StoreRegistry<WorkflowState>;
|
|
13600
13711
|
timelineStore?: TimelineStore<WorkflowState>;
|
|
13601
13712
|
services?: ServiceDefinition<unknown>[];
|
|
13713
|
+
env?: Record<string, string | undefined>;
|
|
13602
13714
|
}
|
|
13603
13715
|
interface RegisterOptions {
|
|
13604
13716
|
mode: WorkflowExecutionMode;
|
|
@@ -13616,10 +13728,10 @@ declare class WorkflowRuntime {
|
|
|
13616
13728
|
private readonly timelineStore;
|
|
13617
13729
|
/** Built-in watch service. Always present. */
|
|
13618
13730
|
private readonly pauseService;
|
|
13619
|
-
/** User-defined run-scoped service definitions, instantiated per run. */
|
|
13620
|
-
private readonly runServiceDefinitions;
|
|
13621
13731
|
/** Global container holding all registered services accessible by steps. */
|
|
13622
13732
|
private readonly serviceContainer;
|
|
13733
|
+
/** Store backing the global container — used to update reactive env state. */
|
|
13734
|
+
private readonly serviceStore;
|
|
13623
13735
|
private readonly workflows;
|
|
13624
13736
|
private readonly index;
|
|
13625
13737
|
private readonly subscriptions;
|
|
@@ -13628,6 +13740,7 @@ declare class WorkflowRuntime {
|
|
|
13628
13740
|
* Populated when a run pauses. Cleared when run ends or is aborted.
|
|
13629
13741
|
*/
|
|
13630
13742
|
private readonly pausedRuns;
|
|
13743
|
+
private readonly logger;
|
|
13631
13744
|
private readonly abortUnsubscribe;
|
|
13632
13745
|
private readonly signalUnsubscribe;
|
|
13633
13746
|
constructor(options: WorkflowRuntimeOptions);
|
|
@@ -13638,14 +13751,28 @@ declare class WorkflowRuntime {
|
|
|
13638
13751
|
listWorkflows(): string[];
|
|
13639
13752
|
stop(): Promise<void>;
|
|
13640
13753
|
registry(workflowId: string): PipelineRegistry<WorkflowState> | undefined;
|
|
13641
|
-
|
|
13642
|
-
|
|
13754
|
+
info(workflowId: string, runId: string): RunInfo<WorkflowState> | undefined;
|
|
13755
|
+
list(): Array<RunInfo<WorkflowState> & {
|
|
13643
13756
|
workflowId: string;
|
|
13644
13757
|
}>;
|
|
13645
13758
|
invoke(workflowId: string, triggerId: string, event: WorkflowEvent): Promise<InvokeResult>;
|
|
13646
13759
|
resume(runId: string, patch?: DeepPartial<WorkflowState>): Promise<InvokeResult>;
|
|
13647
13760
|
signal(runId: string, patch: DeepPartial<WorkflowState>): Promise<void>;
|
|
13648
13761
|
private dispatch;
|
|
13762
|
+
/**
|
|
13763
|
+
* Creates a TimelineRecorder for the given workflow, resolving the
|
|
13764
|
+
* per-workflow sanitizer from the global service container.
|
|
13765
|
+
*
|
|
13766
|
+
* Returns `undefined` when no timelineStore is configured.
|
|
13767
|
+
*/
|
|
13768
|
+
private createRecorder;
|
|
13769
|
+
/**
|
|
13770
|
+
* Attaches a TimelineRecorder (when one is provided) to the run context,
|
|
13771
|
+
* invokes the callback, and detaches it in a finally block.
|
|
13772
|
+
*
|
|
13773
|
+
* When no recorder is provided the callback runs without recording.
|
|
13774
|
+
*/
|
|
13775
|
+
private withRecorder;
|
|
13649
13776
|
private executePipeline;
|
|
13650
13777
|
private spawnRun;
|
|
13651
13778
|
private drainWatchQueue;
|
|
@@ -13653,10 +13780,28 @@ declare class WorkflowRuntime {
|
|
|
13653
13780
|
private buildExecutionContext;
|
|
13654
13781
|
private acquireBusSubscription;
|
|
13655
13782
|
private releaseBusSubscription;
|
|
13783
|
+
/**
|
|
13784
|
+
* Extends `ctx.container` with the workflow's scoped container (if any) and
|
|
13785
|
+
* the global service container, then:
|
|
13786
|
+
* - Eagerly resolves the parameterized __scoped_env_service__ for this
|
|
13787
|
+
* workflow and registers the captured EnvService as __env__ (frozen for
|
|
13788
|
+
* the run).
|
|
13789
|
+
* - Shadows __env_service__ and __scoped_env_service__ so steps cannot
|
|
13790
|
+
* access them via container extension bubbling.
|
|
13791
|
+
* - Registers per‑run and transient workflow services on the run's
|
|
13792
|
+
* container so their factories resolve against the run's state store.
|
|
13793
|
+
*/
|
|
13794
|
+
private extendContextContainer;
|
|
13656
13795
|
private getOrCreateFactory;
|
|
13657
13796
|
private generateRunId;
|
|
13658
13797
|
}
|
|
13659
13798
|
//#endregion
|
|
13799
|
+
//#region src/runtime/types/env.d.ts
|
|
13800
|
+
interface EnvService {
|
|
13801
|
+
get(key: string): string | undefined;
|
|
13802
|
+
list(): Record<string, string | undefined>;
|
|
13803
|
+
}
|
|
13804
|
+
//#endregion
|
|
13660
13805
|
//#region src/runtime/types/pause.d.ts
|
|
13661
13806
|
type PauseOperator = ">=" | "<=" | "==" | "!=" | ">" | "<" | "exists";
|
|
13662
13807
|
interface PauseCondition {
|
|
@@ -13670,4 +13815,4 @@ interface PauseDescriptor {
|
|
|
13670
13815
|
patch?: DeepPartial<WorkflowState>;
|
|
13671
13816
|
}
|
|
13672
13817
|
//#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 };
|
|
13818
|
+
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
|
-
|
|
122
|
-
|
|
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??{}}}),this.serviceContainer=new e.ArtifactContainer(this.serviceStore);for(let e of t.services??[]){if(e.id===`__pause_service__`)throw new n.SystemError({code:n.ErrorCodes.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Service id "__pause_service__" is reserved for the built-in PauseService and cannot be redefined.`});if(e.id===`__env__`)throw new n.SystemError({code:n.ErrorCodes.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Service id "__env__" is reserved for the built-in environment-variable service and cannot be redefined.`});this.serviceContainer.register({key:e.id,factory:e.factory,scope:`singleton`})}this.serviceContainer.register({key:`__pause_service__`,factory:()=>this.pauseService,scope:`singleton`}),this.serviceContainer.register({key:`__env_service__`,factory:async({use:e})=>{let t=await e(e=>e.select(e=>e.env)),n=new u;for(let[e,r]of Object.entries(t??{}))n.use(e,r);return n},scope:`singleton`}),this.serviceContainer.register({key:`__scoped_env_service__`,paramKey:e=>`env:${e.workflowId}`,factory:async({use:e,params:t})=>(await e(e=>e.require(`__env_service__`))).scope(t.workflowId),scope:`singleton`}),this.serviceContainer.register({key:`__sanitizer__`,paramKey:e=>`sanitizer:${e.workflowId}`,factory:async({use:e,params:t})=>{let n=(await e(e=>e.require(`__scoped_env_service__`,{workflowId:t.workflowId}))).list(),r=(0,a.newSecureDefaultConfig)();return r.patterns=[...r.patterns,...(0,a.commonEnvPatterns)([],n)],new a.DocumentSanitizer(r,t.workflowId)},scope:`singleton`}),this.abortUnsubscribe=this.bus.subscribe(c,e=>{this.abortRun(e.run)}),this.signalUnsubscribe=this.bus.subscribe(l,e=>{this.signal(e.runId,e.patch)})}static async createTestTimelineStore(e=`test-timeline-database`){return new i.TimelineStore(await(0,t.DatabaseConnection)({database:e,validate:!0,predicates:{},enableTelemetry:!0},t.createIndexedDbStore))}async register(e,t){if(this.workflows.has(e.id))throw new n.SystemError({code:n.ErrorCodes.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Workflow "${e.id}" is already registered. Call deregister("${e.id}") before registering again.`});let r=i.PipelineRegistry.get(`workflow:${e.id}`,{onExpired:(t,n)=>{this.logger.info(`[WorkflowRuntime] Run "${t}" pause-window expired for workflow "${e.id}". Checkpoint: ${n.pipelineId}.`)},onExportFailed:(t,n)=>{this.logger.error(`[WorkflowRuntime] Deferred export failed for run "${t}" in workflow "${e.id}":`,n)}}),a=async e=>{let n=e.ok?e.value.runId:e.error?.runId;try{await t.onComplete(e)}finally{n&&(s.executionContext.settle(n),this.pauseService.onRunEnded(n),this.pausedRuns.delete(n)),r.prune(),await t.onCleanup?.()}},o=this.buildExecutionContext(e.id,t.mode),s;s={workflow:e,mode:t.mode,executionContext:o,factories:new Map,registry:r,hooks:{onPrepare:t.onPrepare,onComplete:a,onResume:t.onResume,onDispatch:t.onDispatch,onCleanup:t.onCleanup}},this.workflows.set(e.id,s),this.serviceStore.set(t=>({env:{...t.env??{},[e.id]:e.env??{}}}));let c=this.serviceContainer.scope(e.id);if(e.services&&e.services.length>0)for(let t of e.services)t.scope===`run`||t.scope===`transient`||c.register({key:t.id,factory:t.factory,scope:`singleton`});s.container=c;for(let[t,n]of Object.entries(e.triggers)){let r={workflowId:e.id,triggerId:t,trigger:n},i=this.index.get(n.event);i||(i=new Set,this.index.set(n.event,i)),i.add(r),await this.acquireBusSubscription(n.event)}}deregister(e){let t=this.workflows.get(e);if(t){for(let n of Object.values(t.workflow.triggers)){let t=this.index.get(n.event);if(t){for(let n of t)n.workflowId===e&&t.delete(n);t.size===0&&this.index.delete(n.event)}this.releaseBusSubscription(n.event)}for(let t of[`__scoped_env_service__`,`__sanitizer__`])try{this.serviceContainer.unregister(t,{workflowId:e})}catch{}this.serviceStore.set(t=>({env:{...t.env??{},[e]:o.DELETE_SYMBOL}})),t.container?.dispose(),t.executionContext.close(),i.PipelineRegistry.destroy(`workflow:${e}`),t.factories.clear(),this.workflows.delete(e)}}hasWorkflow(e){return this.workflows.has(e)}listWorkflows(){return Array.from(this.workflows.keys())}async stop(){for(let e of Array.from(this.workflows.keys()))this.deregister(e);try{this.abortUnsubscribe()}catch{}try{this.signalUnsubscribe()}catch{}}registry(e){return this.workflows.get(e)?.registry}info(e,t){return this.workflows.get(e)?.registry.get(t)}list(){let e=[];for(let[t,n]of this.workflows)for(let r of n.registry.list())e.push({...r,workflowId:t});return e}async invoke(e,t,r){let i=this.workflows.get(e);if(!i)throw new n.SystemError({code:n.ErrorCodes.NOT_FOUND.code,message:`[WorkflowRuntime] invoke() failed: workflow "${e}" is not registered.`});if(!i.workflow.pipelines[t])throw new n.SystemError({code:n.ErrorCodes.NOT_FOUND.code,message:`[WorkflowRuntime] invoke() failed: no pipeline definition for workflow "${e}" trigger "${t}".`});let a=this.generateRunId(e,t),o=await this.executePipeline(i,t,a,r);return o.ok&&o.value.status===`paused`?(this.pausedRuns.set(a,{workflowId:e,triggerId:t}),await this.drainWatchQueue(a)):await i.hooks.onComplete(o),o.ok?n.Result.ok(o.value,{runId:a,triggerId:t}):n.Result.fail(o.error,{runId:a,triggerId:t})}async resume(e,t={}){let r=this.pausedRuns.get(e);if(!r)throw new n.SystemError({code:n.ErrorCodes.NOT_FOUND.code,message:`[WorkflowRuntime] resume() failed: run "${e}" is not in the paused runs registry. It may have already completed, been aborted, or the runId is incorrect.`});let{workflowId:i,triggerId:a}=r,s=this.workflows.get(i);if(!s)throw new n.SystemError({code:n.ErrorCodes.RESOURCE_RELEASED.code,message:`[WorkflowRuntime] resume() failed: workflow "${i}" is no longer registered. The workflow may have been deregistered while the run was paused.`});let c=this.getOrCreateFactory(s,a),l=s.registry.hold(e),u=await this.createRecorder(s),d=u?[u.asLogSink()]:void 0,f;try{let t=await c.resume(e,{sinks:d});if(!t.ok)throw t.error;f=t.value}catch(t){throw new n.SystemError({code:n.ErrorCodes.INTERNAL_ERROR.code,message:`[WorkflowRuntime] resume() failed to reconstruct context for run "${e}"`,cause:t})}l||this.extendContextContainer(f,s),Object.keys(t).length>0&&f.write(t),f.write({__watch__:o.DELETE_SYMBOL});let p=await this.withRecorder(f,async()=>(await s.hooks.onResume?.(f),await f.run()),u);if(p.ok&&p.value.status===`paused`)await this.drainWatchQueue(e);else{let t=this.workflows.get(i);if(t)try{await t.hooks.onComplete(p)}catch(t){this.logger.error(`[WorkflowRuntime] onComplete hook threw during resume for run "${e}":`,t)}}return p.ok?n.Result.ok(p.value,{runId:e,triggerId:a}):n.Result.fail(p.error,{runId:e,triggerId:a})}async signal(e,t){for(let n of this.workflows.values()){let r=n.registry.get(e);if(r?.context){r.context.write(t);return}}}dispatch(e,t){let n=this.index.get(e);if(!n||n.size===0)return;let r={type:e,payload:t,timestamp:Date.now()};for(let e of n){let t=!0;if(e.trigger.predicate)try{t=e.trigger.predicate(r)}catch(n){this.logger.error(`[WorkflowRuntime] Predicate threw for workflow "${e.workflowId}" trigger "${e.triggerId}":`,n),t=!1}if(!t){this.workflows.get(e.workflowId)?.hooks.onDispatch?.({status:`rejected`,reason:`filtered`});continue}let n=this.workflows.get(e.workflowId);n&&n.executionContext.accept(e.triggerId,r,(e,t)=>this.spawnRun(n,e,t)).then(e=>{n.hooks.onDispatch?.(e)}).catch(t=>{this.logger.error(`[WorkflowRuntime] ExecutionContext.accept() threw for workflow "${e.workflowId}":`,t)})}}async createRecorder(e){if(!this.timelineStore)return;let t;try{t=await this.serviceContainer.require(`__sanitizer__`,{workflowId:e.workflow.id})}catch{}return new i.TimelineRecorder(this.timelineStore,{sanitizer:t})}async withRecorder(e,t,n){if(!n)return t();await n.attach(e),await(0,i.yieldToEventLoop)();try{return await t()}finally{await n.detach().catch(()=>{})}}async executePipeline(e,t,r,i){let a=this.getOrCreateFactory(e,t),o=await this.createRecorder(e),s=o?[o.asLogSink()]:void 0,c;try{c=await a.prepare(void 0,r,{sinks:s}),await c.store.set({__trigger_event__:i})}catch(e){let t=e instanceof n.SystemError?e:new n.SystemError({code:`PIPELINE_PREPARE_FAILED`,message:e instanceof Error?e.message:String(e)});return n.Result.fail(t)}return this.extendContextContainer(c,e),this.withRecorder(c,async()=>(await e.hooks.onPrepare(c),await c.run()),o)}async spawnRun(e,t,n){let r=this.generateRunId(e.workflow.id,t);return this.executePipeline(e,t,r,n).then(async n=>{if(n.ok&&n.value.status===`paused`){this.pausedRuns.set(r,{workflowId:e.workflow.id,triggerId:t}),await this.drainWatchQueue(r);return}try{await e.hooks.onComplete(n)}catch(e){this.logger.error(`[WorkflowRuntime] onComplete hook threw for run "${r}":`,e)}}).catch(e=>{this.logger.error(`[WorkflowRuntime] Pipeline execution failed for run "${r}":`,e)}),r}async drainWatchQueue(e){let t=this.pauseService.onRunPaused(e);if(t!==null)try{await this.resume(e,t.patch)}catch(t){this.logger.error(`[WorkflowRuntime] drainWatchQueue: resume() failed for run "${e}":`,t),this.pauseService.onRunEnded(e),this.pausedRuns.delete(e)}}async abortRun(e){for(let t of this.workflows.values()){let n=t.registry.get(e);if(n?.context){try{n.context.abort()}catch(t){this.logger.error(`[WorkflowRuntime] Error aborting run "${e}":`,t)}break}}this.pauseService.onRunEnded(e),this.pausedRuns.delete(e)}buildExecutionContext(e,t){let n=this.logger.child({scope:`${t.type}:execution-context`});switch(t.type){case`transient`:return new d(e,t.concurrency??10,t.capacity??1e3,n);case`serialized`:return new f(e,t.capacity??1e3,n);case`singleton_loop`:return new p(e,t.onActive??`drop`,t.replacementGracePeriod??5e3,(e,t)=>this.signal(e,{__singleton_event__:t}),n);case`exclusive`:return new m(e,t.onActive??`reject`,n)}}async acquireBusSubscription(e){let t=this.subscriptions.get(e);t||(t=new s.SharedResource(()=>this.bus.subscribe(e,t=>{this.dispatch(e,t)}),t=>{try{t?.(),this.subscriptions.delete(e)}catch{}},{gracePeriod:`sync`}),this.subscriptions.set(e,t)),await t.acquire()}releaseBusSubscription(e){this.subscriptions.get(e)?.release()}async extendContextContainer(e,t){t.container&&e.container.extend(t.container),e.container.extend(this.serviceContainer);let r=await this.serviceContainer.require(`__scoped_env_service__`,{workflowId:t.workflow.id});e.container.register({key:`__env__`,factory:()=>r,scope:`singleton`});for(let t of[`__env_service__`,`__scoped_env_service__`])e.container.register({key:t,factory:()=>{throw new n.SystemError({code:n.ErrorCodes.UNAUTHENTICATED.code,message:`"${t}" is an internal artifact and cannot be accessed from pipeline steps.`})},scope:`singleton`,lazy:!1});if(t.workflow.services)for(let n of t.workflow.services){if(n.scope===`workflow`||n.scope===void 0)continue;let t=n.scope===`run`?`singleton`:`transient`;e.container.register({key:n.id,factory:n.factory,scope:t})}}getOrCreateFactory(e,t){let r=e.factories.get(t);if(r)return r;let a=e.workflow.pipelines[t];if(!a)throw new n.SystemError({code:n.ErrorCodes.NOT_FOUND.code,message:`[WorkflowRuntime] No pipeline definition found for workflow "${e.workflow.id}" trigger "${t}". Ensure workflow.pipelines["${t}"] is populated by the compiler.`});let o=new i.PipelineFactory(a,{storeFactory:async e=>this.storeRegistry.get(e),registry:e.registry});return e.factories.set(t,o),o}generateRunId(e,t){return`${e}:${t}:${Date.now()}:${Math.random().toString(36).slice(2)}`}};exports.ABORT_EVENT=c,exports.SIGNAL_EVENT=l,exports.WorkflowRuntime=g;
|
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,yieldToEventLoop as d}from"@asaidimu/utils-pipeline";import{DocumentSanitizer as f,commonEnvPatterns as p,newSecureDefaultConfig as m}from"@asaidimu/utils-sanitize";import{DELETE_SYMBOL as h,ReactiveDataStore as g}from"@asaidimu/utils-store";import{Serializer as _,SharedResource as v}from"@asaidimu/utils-sync";const y=`__abort__`,b=`__signal__`;var x=class{envs=new Map;use(e,t){return this.envs.set(e,t),this}get(e){return this.envs.get(`global`)?.[e]}list(){return{...this.envs.get(`global`)}}scope(e){let t=this.envs.get(e),n=this.envs.get(`global`);return{get:e=>{if(t&&e in t)return t[e];if(n&&e in n)return n[e]},list:()=>{let e={...n};if(t)for(let n of Object.keys(t))e[n]=t[n];return e}}}},S=class{workflowId;concurrency;capacity;logger;inFlight=0;queueSize=0;closed=!1;constructor(e,t,n,r){this.workflowId=e,this.concurrency=t,this.capacity=n,this.logger=r}async accept(e,t,n){if(this.closed)return{status:`rejected`,reason:`queue_full`};if(this.inFlight<this.concurrency)return this.inFlight++,this.run(e,t,n),{status:`accepted`};if(this.queueSize>=this.capacity)return{status:`rejected`,reason:`queue_full`};this.queueSize++;let r=this.queueSize;return await new Promise(e=>{let t=()=>{if(this.closed){e();return}this.inFlight<this.concurrency?(this.inFlight++,this.queueSize--,e()):setTimeout(t,0)};setTimeout(t,0)}),this.closed?{status:`rejected`,reason:`queue_full`}:(this.run(e,t,n),{status:`queued`,position:r})}settle(e){}close(){this.closed=!0}async run(e,t,n){try{await n(e,t)}finally{this.inFlight--}}},C=class{workflowId;capacity;logger;serializer;queueDepth=0;closed=!1;constructor(e,t,n){this.workflowId=e,this.capacity=t,this.logger=n,this.serializer=new _({capacity:t,yieldMode:`macrotask`})}async accept(e,t,n){if(this.closed||this.queueDepth>=this.capacity)return{status:`rejected`,reason:`queue_full`};let r=this.queueDepth;return this.queueDepth++,await this.serializer.do(async()=>{try{await n(e,t)}finally{this.queueDepth--}}),r===0?{status:`accepted`}:{status:`queued`,position:r}}settle(e){}close(){this.closed=!0,this.serializer.close()}},w=class{workflowId;onActive;replacementGracePeriod;deliverSignal;logger;state=`idle`;activeRunId;settleResolve;closed=!1;constructor(e,t,n,r,i){this.workflowId=e,this.onActive=t,this.replacementGracePeriod=n,this.deliverSignal=r,this.logger=i}async accept(e,t,n){if(this.closed)return{status:`rejected`,reason:`singleton_active`};if(this.state===`idle`){this.state=`starting`;let r=await n(e,t);return r||(this.state=`idle`,this.activeRunId=void 0),{status:`accepted`,runId:r}}if(this.state===`terminating`)return{status:`rejected`,reason:`terminating`};switch(this.onActive){case`drop`:return{status:`rejected`,reason:`singleton_active`};case`signal`:{let e=this.activeRunId??``;return e?this.deliverSignal(e,t):this.logger.warn(`[WorkflowRuntime] SingletonLoop "${this.workflowId}": signal requested but activeRunId unknown (state="${this.state}"). Event "${t.type}" dropped.`),{status:`signalled`,runId:e}}case`replace`:return this.handleReplace(e,t,n)}}settle(e){(this.state===`starting`||e===this.activeRunId)&&(this.activeRunId=e,this.state=`running`),this.state===`terminating`&&e===this.activeRunId&&this.settleResolve?.()}close(){this.closed=!0,this.settleResolve?.()}async handleReplace(e,t,n){if(this.state=`terminating`,this.activeRunId&&this.logger.info(`[WorkflowRuntime] SingletonLoop "${this.workflowId}": replacing run "${this.activeRunId}" with new event "${t.type}".`),await Promise.race([new Promise(e=>{this.settleResolve=e}),new Promise(e=>setTimeout(e,this.replacementGracePeriod))]),this.settleResolve=void 0,this.closed)return this.state=`idle`,{status:`rejected`,reason:`singleton_active`};this.state=`starting`,this.activeRunId=void 0;let r=await n(e,t);return r||(this.state=`idle`),{status:`accepted`,runId:r}}},T=class{workflowId;onActive;logger;activeRunId;pending;closed=!1;constructor(e,t,n){this.workflowId=e,this.onActive=t,this.logger=n}async accept(e,t,n){if(this.closed)return{status:`rejected`,reason:`exclusive_active`};if(!this.activeRunId){let r=await n(e,t);return r&&(this.activeRunId=r),{status:`accepted`,runId:r}}return this.onActive===`reject`?{status:`rejected`,reason:`exclusive_active`}:(this.pending={triggerId:e,event:t,spawn:n},{status:`queued`,position:1})}settle(e){if(e===this.activeRunId&&(this.activeRunId=void 0,this.pending&&!this.closed)){let{triggerId:e,event:t,spawn:n}=this.pending;this.pending=void 0,n(e,t).then(e=>{e&&(this.activeRunId=e)})}}close(){this.closed=!0,this.pending=void 0}},E=class{registrations=new Map;byEventType=new Map;busSubscriptions=new Map;bus;resumeCallback;constructor(e){this.bus=e.bus,this.resumeCallback=e.resume}register(e,t){let n=this.registrations.get(e);n||(n=new Map,this.registrations.set(e,n));let r={runId:e,descriptor:t,queue:[],parked:!1};n.set(t.eventType,r);let i=this.byEventType.get(t.eventType);i||(i=new Set,this.byEventType.set(t.eventType,i)),i.add(e),this.acquireBusSubscription(t.eventType)}cancel(e,t){let n=this.registrations.get(e);n&&(n.delete(t),n.size===0&&this.registrations.delete(e),this.removeFromReverseIndex(e,t),this.releaseBusSubscription(t))}onRunPaused(e){let t=this.registrations.get(e);if(!t)return null;for(let e of t.values())if(e.queue.length>0)return e.queue.shift();for(let e of t.values())e.parked=!0;return null}onRunEnded(e){let t=this.registrations.get(e);if(t){for(let[n]of t)this.removeFromReverseIndex(e,n),this.releaseBusSubscription(n);this.registrations.delete(e)}}onEvent(e,t){let n=this.byEventType.get(e);if(!(!n||n.size===0))for(let r of n){let n=this.registrations.get(r);if(!n)continue;let i=n.get(e);if(!i||!this.evaluate(i.descriptor.conditions,t))continue;let a=this.resolve(i.descriptor,t);i.parked?(i.parked=!1,this.resumeCallback(r,a.patch)):i.queue.push(a)}}evaluate(e,t){for(let n of e){let e=this.getField(t,n.field);if(n.op===`exists`){if(e==null)return!1;continue}if(e==null)return!1;try{switch(n.op){case`==`:if(e!=n.value)return!1;break;case`!=`:if(e==n.value)return!1;break;case`>`:if(!(e>n.value))return!1;break;case`>=`:if(!(e>=n.value))return!1;break;case`<`:if(!(e<n.value))return!1;break;case`<=`:if(!(e<=n.value))return!1;break;default:return!1}}catch{return!1}}return!0}resolve(e,t){return{eventPayload:t,patch:{...e.patch??{},__watch_event__:t}}}getField(e,t){let n=t.split(`.`),r=e;for(let e of n){if(r==null||typeof r!=`object`&&!Array.isArray(r))return;let t=Number(e);r=!isNaN(t)&&Array.isArray(r)?r[t]:r[e]}return r}acquireBusSubscription(e){if(this.busSubscriptions.has(e))return;let t=this.bus.subscribe(e,t=>{this.onEvent(e,t)});this.busSubscriptions.set(e,t)}releaseBusSubscription(e){let t=this.byEventType.get(e);if(t&&t.size>0)return;let n=this.busSubscriptions.get(e);if(n){try{n()}catch{}this.busSubscriptions.delete(e)}}removeFromReverseIndex(e,t){let n=this.byEventType.get(t);n&&(n.delete(e),n.size===0&&this.byEventType.delete(t))}},D=class{bus;storeRegistry;timelineStore;pauseService;serviceContainer;serviceStore;workflows=new Map;index=new Map;subscriptions=new Map;pausedRuns=new Map;logger;abortUnsubscribe;signalUnsubscribe;constructor(t){this.bus=t.bus,this.storeRegistry=t.storeRegistry,this.timelineStore=t.timelineStore,this.logger=t.logger?t.logger:new o([],{scope:`workflow-runtime`}),this.pauseService=new E({bus:this.bus,resume:async(e,t)=>{this.resume(e,t)}}),this.serviceStore=new g({env:{global:t.env??{}}}),this.serviceContainer=new e(this.serviceStore);for(let e of t.services??[]){if(e.id===`__pause_service__`)throw new a({code:r.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Service id "__pause_service__" is reserved for the built-in PauseService and cannot be redefined.`});if(e.id===`__env__`)throw new a({code:r.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Service id "__env__" is reserved for the built-in environment-variable service and cannot be redefined.`});this.serviceContainer.register({key:e.id,factory:e.factory,scope:`singleton`})}this.serviceContainer.register({key:`__pause_service__`,factory:()=>this.pauseService,scope:`singleton`}),this.serviceContainer.register({key:`__env_service__`,factory:async({use:e})=>{let t=await e(e=>e.select(e=>e.env)),n=new x;for(let[e,r]of Object.entries(t??{}))n.use(e,r);return n},scope:`singleton`}),this.serviceContainer.register({key:`__scoped_env_service__`,paramKey:e=>`env:${e.workflowId}`,factory:async({use:e,params:t})=>(await e(e=>e.require(`__env_service__`))).scope(t.workflowId),scope:`singleton`}),this.serviceContainer.register({key:`__sanitizer__`,paramKey:e=>`sanitizer:${e.workflowId}`,factory:async({use:e,params:t})=>{let n=(await e(e=>e.require(`__scoped_env_service__`,{workflowId:t.workflowId}))).list(),r=m();return r.patterns=[...r.patterns,...p([],n)],new f(r,t.workflowId)},scope:`singleton`}),this.abortUnsubscribe=this.bus.subscribe(y,e=>{this.abortRun(e.run)}),this.signalUnsubscribe=this.bus.subscribe(b,e=>{this.signal(e.runId,e.patch)})}static async createTestTimelineStore(e=`test-timeline-database`){return new u(await t({database:e,validate:!0,predicates:{},enableTelemetry:!0},n))}async register(e,t){if(this.workflows.has(e.id))throw new a({code:r.DUPLICATE_KEY.code,message:`[WorkflowRuntime] Workflow "${e.id}" is already registered. Call deregister("${e.id}") before registering again.`});let n=c.get(`workflow:${e.id}`,{onExpired:(t,n)=>{this.logger.info(`[WorkflowRuntime] Run "${t}" pause-window expired for workflow "${e.id}". Checkpoint: ${n.pipelineId}.`)},onExportFailed:(t,n)=>{this.logger.error(`[WorkflowRuntime] Deferred export failed for run "${t}" in workflow "${e.id}":`,n)}}),i=async e=>{let r=e.ok?e.value.runId:e.error?.runId;try{await t.onComplete(e)}finally{r&&(s.executionContext.settle(r),this.pauseService.onRunEnded(r),this.pausedRuns.delete(r)),n.prune(),await t.onCleanup?.()}},o=this.buildExecutionContext(e.id,t.mode),s;s={workflow:e,mode:t.mode,executionContext:o,factories:new Map,registry:n,hooks:{onPrepare:t.onPrepare,onComplete:i,onResume:t.onResume,onDispatch:t.onDispatch,onCleanup:t.onCleanup}},this.workflows.set(e.id,s),this.serviceStore.set(t=>({env:{...t.env??{},[e.id]:e.env??{}}}));let l=this.serviceContainer.scope(e.id);if(e.services&&e.services.length>0)for(let t of e.services)t.scope===`run`||t.scope===`transient`||l.register({key:t.id,factory:t.factory,scope:`singleton`});s.container=l;for(let[t,n]of Object.entries(e.triggers)){let r={workflowId:e.id,triggerId:t,trigger:n},i=this.index.get(n.event);i||(i=new Set,this.index.set(n.event,i)),i.add(r),await this.acquireBusSubscription(n.event)}}deregister(e){let t=this.workflows.get(e);if(t){for(let n of Object.values(t.workflow.triggers)){let t=this.index.get(n.event);if(t){for(let n of t)n.workflowId===e&&t.delete(n);t.size===0&&this.index.delete(n.event)}this.releaseBusSubscription(n.event)}for(let t of[`__scoped_env_service__`,`__sanitizer__`])try{this.serviceContainer.unregister(t,{workflowId:e})}catch{}this.serviceStore.set(t=>({env:{...t.env??{},[e]:h}})),t.container?.dispose(),t.executionContext.close(),c.destroy(`workflow:${e}`),t.factories.clear(),this.workflows.delete(e)}}hasWorkflow(e){return this.workflows.has(e)}listWorkflows(){return Array.from(this.workflows.keys())}async stop(){for(let e of Array.from(this.workflows.keys()))this.deregister(e);try{this.abortUnsubscribe()}catch{}try{this.signalUnsubscribe()}catch{}}registry(e){return this.workflows.get(e)?.registry}info(e,t){return this.workflows.get(e)?.registry.get(t)}list(){let e=[];for(let[t,n]of this.workflows)for(let r of n.registry.list())e.push({...r,workflowId:t});return e}async invoke(e,t,n){let o=this.workflows.get(e);if(!o)throw new a({code:r.NOT_FOUND.code,message:`[WorkflowRuntime] invoke() failed: workflow "${e}" is not registered.`});if(!o.workflow.pipelines[t])throw new a({code:r.NOT_FOUND.code,message:`[WorkflowRuntime] invoke() failed: no pipeline definition for workflow "${e}" trigger "${t}".`});let s=this.generateRunId(e,t),c=await this.executePipeline(o,t,s,n);return c.ok&&c.value.status===`paused`?(this.pausedRuns.set(s,{workflowId:e,triggerId:t}),await this.drainWatchQueue(s)):await o.hooks.onComplete(c),c.ok?i.ok(c.value,{runId:s,triggerId:t}):i.fail(c.error,{runId:s,triggerId:t})}async resume(e,t={}){let n=this.pausedRuns.get(e);if(!n)throw new a({code:r.NOT_FOUND.code,message:`[WorkflowRuntime] resume() failed: run "${e}" is not in the paused runs registry. It may have already completed, been aborted, or the runId is incorrect.`});let{workflowId:o,triggerId:s}=n,c=this.workflows.get(o);if(!c)throw new a({code:r.RESOURCE_RELEASED.code,message:`[WorkflowRuntime] resume() failed: workflow "${o}" is no longer registered. The workflow may have been deregistered while the run was paused.`});let l=this.getOrCreateFactory(c,s),u=c.registry.hold(e),d=await this.createRecorder(c),f=d?[d.asLogSink()]:void 0,p;try{let t=await l.resume(e,{sinks:f});if(!t.ok)throw t.error;p=t.value}catch(t){throw new a({code:r.INTERNAL_ERROR.code,message:`[WorkflowRuntime] resume() failed to reconstruct context for run "${e}"`,cause:t})}u||this.extendContextContainer(p,c),Object.keys(t).length>0&&p.write(t),p.write({__watch__:h});let m=await this.withRecorder(p,async()=>(await c.hooks.onResume?.(p),await p.run()),d);if(m.ok&&m.value.status===`paused`)await this.drainWatchQueue(e);else{let t=this.workflows.get(o);if(t)try{await t.hooks.onComplete(m)}catch(t){this.logger.error(`[WorkflowRuntime] onComplete hook threw during resume for run "${e}":`,t)}}return m.ok?i.ok(m.value,{runId:e,triggerId:s}):i.fail(m.error,{runId:e,triggerId:s})}async signal(e,t){for(let n of this.workflows.values()){let r=n.registry.get(e);if(r?.context){r.context.write(t);return}}}dispatch(e,t){let n=this.index.get(e);if(!n||n.size===0)return;let r={type:e,payload:t,timestamp:Date.now()};for(let e of n){let t=!0;if(e.trigger.predicate)try{t=e.trigger.predicate(r)}catch(n){this.logger.error(`[WorkflowRuntime] Predicate threw for workflow "${e.workflowId}" trigger "${e.triggerId}":`,n),t=!1}if(!t){this.workflows.get(e.workflowId)?.hooks.onDispatch?.({status:`rejected`,reason:`filtered`});continue}let n=this.workflows.get(e.workflowId);n&&n.executionContext.accept(e.triggerId,r,(e,t)=>this.spawnRun(n,e,t)).then(e=>{n.hooks.onDispatch?.(e)}).catch(t=>{this.logger.error(`[WorkflowRuntime] ExecutionContext.accept() threw for workflow "${e.workflowId}":`,t)})}}async createRecorder(e){if(!this.timelineStore)return;let t;try{t=await this.serviceContainer.require(`__sanitizer__`,{workflowId:e.workflow.id})}catch{}return new l(this.timelineStore,{sanitizer:t})}async withRecorder(e,t,n){if(!n)return t();await n.attach(e),await d();try{return await t()}finally{await n.detach().catch(()=>{})}}async executePipeline(e,t,n,r){let o=this.getOrCreateFactory(e,t),s=await this.createRecorder(e),c=s?[s.asLogSink()]:void 0,l;try{l=await o.prepare(void 0,n,{sinks:c}),await l.store.set({__trigger_event__:r})}catch(e){let t=e instanceof a?e:new a({code:`PIPELINE_PREPARE_FAILED`,message:e instanceof Error?e.message:String(e)});return i.fail(t)}return this.extendContextContainer(l,e),this.withRecorder(l,async()=>(await e.hooks.onPrepare(l),await l.run()),s)}async spawnRun(e,t,n){let r=this.generateRunId(e.workflow.id,t);return this.executePipeline(e,t,r,n).then(async n=>{if(n.ok&&n.value.status===`paused`){this.pausedRuns.set(r,{workflowId:e.workflow.id,triggerId:t}),await this.drainWatchQueue(r);return}try{await e.hooks.onComplete(n)}catch(e){this.logger.error(`[WorkflowRuntime] onComplete hook threw for run "${r}":`,e)}}).catch(e=>{this.logger.error(`[WorkflowRuntime] Pipeline execution failed for run "${r}":`,e)}),r}async drainWatchQueue(e){let t=this.pauseService.onRunPaused(e);if(t!==null)try{await this.resume(e,t.patch)}catch(t){this.logger.error(`[WorkflowRuntime] drainWatchQueue: resume() failed for run "${e}":`,t),this.pauseService.onRunEnded(e),this.pausedRuns.delete(e)}}async abortRun(e){for(let t of this.workflows.values()){let n=t.registry.get(e);if(n?.context){try{n.context.abort()}catch(t){this.logger.error(`[WorkflowRuntime] Error aborting run "${e}":`,t)}break}}this.pauseService.onRunEnded(e),this.pausedRuns.delete(e)}buildExecutionContext(e,t){let n=this.logger.child({scope:`${t.type}:execution-context`});switch(t.type){case`transient`:return new S(e,t.concurrency??10,t.capacity??1e3,n);case`serialized`:return new C(e,t.capacity??1e3,n);case`singleton_loop`:return new w(e,t.onActive??`drop`,t.replacementGracePeriod??5e3,(e,t)=>this.signal(e,{__singleton_event__:t}),n);case`exclusive`:return new T(e,t.onActive??`reject`,n)}}async acquireBusSubscription(e){let t=this.subscriptions.get(e);t||(t=new v(()=>this.bus.subscribe(e,t=>{this.dispatch(e,t)}),t=>{try{t?.(),this.subscriptions.delete(e)}catch{}},{gracePeriod:`sync`}),this.subscriptions.set(e,t)),await t.acquire()}releaseBusSubscription(e){this.subscriptions.get(e)?.release()}async extendContextContainer(e,t){t.container&&e.container.extend(t.container),e.container.extend(this.serviceContainer);let n=await this.serviceContainer.require(`__scoped_env_service__`,{workflowId:t.workflow.id});e.container.register({key:`__env__`,factory:()=>n,scope:`singleton`});for(let t of[`__env_service__`,`__scoped_env_service__`])e.container.register({key:t,factory:()=>{throw new a({code:r.UNAUTHENTICATED.code,message:`"${t}" is an internal artifact and cannot be accessed from pipeline steps.`})},scope:`singleton`,lazy:!1});if(t.workflow.services)for(let n of t.workflow.services){if(n.scope===`workflow`||n.scope===void 0)continue;let t=n.scope===`run`?`singleton`:`transient`;e.container.register({key:n.id,factory:n.factory,scope:t})}}getOrCreateFactory(e,t){let n=e.factories.get(t);if(n)return n;let i=e.workflow.pipelines[t];if(!i)throw new a({code:r.NOT_FOUND.code,message:`[WorkflowRuntime] No pipeline definition found for workflow "${e.workflow.id}" trigger "${t}". Ensure workflow.pipelines["${t}"] is populated by the compiler.`});let o=new s(i,{storeFactory:async e=>this.storeRegistry.get(e),registry:e.registry});return e.factories.set(t,o),o}generateRunId(e,t){return`${e}:${t}:${Date.now()}:${Math.random().toString(36).slice(2)}`}};export{y as ABORT_EVENT,b as SIGNAL_EVENT,D as WorkflowRuntime};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asaidimu/runtime",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
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.
|
|
38
|
+
"@asaidimu/utils-store": "^10.2.13",
|
|
39
39
|
"@asaidimu/utils-database": "^3.1.15",
|
|
40
40
|
"@asaidimu/utils-sync": "^2.3.6",
|
|
41
|
-
"@asaidimu/utils-pipeline": "^1.3.
|
|
42
|
-
"@asaidimu/utils-artifacts": "^8.2.
|
|
41
|
+
"@asaidimu/utils-pipeline": "^1.3.14",
|
|
42
|
+
"@asaidimu/utils-artifacts": "^8.2.20",
|
|
43
43
|
"@asaidimu/utils-error": "^1.0.0",
|
|
44
|
-
"@asaidimu/utils-events": "^1.2.7"
|
|
44
|
+
"@asaidimu/utils-events": "^1.2.7",
|
|
45
|
+
"@asaidimu/utils-sanitize": "^1.0.6",
|
|
46
|
+
"@asaidimu/utils-logger": "^1.0.9"
|
|
45
47
|
},
|
|
46
48
|
"publishConfig": {
|
|
47
49
|
"registry": "https://registry.npmjs.org/",
|