duroxide 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Affan Dar and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # duroxide-node
2
+
3
+ Node.js/TypeScript SDK for the [Duroxide](https://github.com/affandar/duroxide) durable execution runtime. Write reliable, long-running workflows in JavaScript using generator functions — backed by a Rust runtime that handles persistence, replay, and fault tolerance.
4
+
5
+ > See [CHANGELOG.md](CHANGELOG.md) for release notes.
6
+
7
+ ## Features
8
+
9
+ - **Durable orchestrations** — generator-based workflows that survive process restarts
10
+ - **Automatic replay** — the Rust runtime replays history on restart, your code picks up where it left off
11
+ - **Activities** — async functions for side effects (API calls, DB writes, etc.)
12
+ - **Timers** — durable delays that persist across restarts
13
+ - **Sub-orchestrations** — compose workflows from smaller workflows
14
+ - **External events** — pause workflows and wait for signals
15
+ - **Fan-out/fan-in** — run tasks in parallel with `ctx.all()` (supports all task types)
16
+ - **Race conditions** — wait for the first of multiple tasks with `ctx.race()` (supports all task types)
17
+ - **Cooperative cancellation** — activities detect when they're no longer needed via `ctx.isCancelled()`
18
+ - **Continue-as-new** — restart orchestrations with fresh history for eternal workflows
19
+ - **Structured tracing** — orchestration and activity logs route through Rust's `tracing` crate
20
+ - **SQLite & PostgreSQL** — pluggable storage backends
21
+
22
+ ## Quick Start
23
+
24
+ ```bash
25
+ npm install duroxide
26
+ ```
27
+
28
+ ```javascript
29
+ const { SqliteProvider, Client, Runtime } = require('duroxide');
30
+
31
+ async function main() {
32
+ // 1. Open a storage backend
33
+ const provider = await SqliteProvider.open('sqlite:myapp.db');
34
+ const client = new Client(provider);
35
+ const runtime = new Runtime(provider);
36
+
37
+ // 2. Register activities (async functions with side effects)
38
+ runtime.registerActivity('Greet', async (ctx, name) => {
39
+ ctx.traceInfo(`greeting ${name}`);
40
+ return `Hello, ${name}!`;
41
+ });
42
+
43
+ // 3. Register orchestrations (generator functions)
44
+ runtime.registerOrchestration('GreetWorkflow', function* (ctx, input) {
45
+ const greeting = yield ctx.scheduleActivity('Greet', input.name);
46
+ ctx.traceInfo(`got: ${greeting}`);
47
+ return greeting;
48
+ });
49
+
50
+ // 4. Start the runtime
51
+ await runtime.start();
52
+
53
+ // 5. Start an orchestration and wait for it
54
+ await client.startOrchestration('greet-1', 'GreetWorkflow', { name: 'World' });
55
+ const result = await client.waitForOrchestration('greet-1');
56
+ console.log(result.output); // "Hello, World!"
57
+
58
+ await runtime.shutdown();
59
+ }
60
+
61
+ main();
62
+ ```
63
+
64
+ ## Why Generators (not async/await)?
65
+
66
+ Duroxide uses `function*` generators instead of `async function` for orchestrations. This is a deliberate design choice — see [Architecture](docs/architecture.md#yield-vs-await) for the full explanation. The short version: generators give Rust full control over when and how each step executes, which is essential for deterministic replay.
67
+
68
+ ```javascript
69
+ // ✅ Orchestrations use yield
70
+ runtime.registerOrchestration('MyWorkflow', function* (ctx, input) {
71
+ const result = yield ctx.scheduleActivity('DoWork', input);
72
+ return result;
73
+ });
74
+
75
+ // ✅ Activities use async/await (normal async functions)
76
+ runtime.registerActivity('DoWork', async (ctx, input) => {
77
+ const data = await fetch(`https://api.example.com/${input}`);
78
+ return data;
79
+ });
80
+ ```
81
+
82
+ ## Orchestration Context API
83
+
84
+ All scheduling methods return descriptors that must be **yielded**:
85
+
86
+ | Method | Description |
87
+ |--------|-------------|
88
+ | `yield ctx.scheduleActivity(name, input)` | Run an activity |
89
+ | `yield ctx.scheduleActivityWithRetry(name, input, retryPolicy)` | Run with retry |
90
+ | `yield ctx.scheduleTimer(delayMs)` | Durable delay |
91
+ | `yield ctx.waitForEvent(eventName)` | Wait for external signal |
92
+ | `yield ctx.scheduleSubOrchestration(name, input)` | Run child workflow (await result) |
93
+ | `yield ctx.scheduleSubOrchestrationWithId(name, id, input)` | Child with explicit ID |
94
+ | `yield ctx.startOrchestration(name, id, input)` | Fire-and-forget orchestration |
95
+ | `yield ctx.all([task1, task2, ...])` | Parallel execution (like `Promise.all`) |
96
+ | `yield ctx.race(task1, task2)` | First-to-complete (like `Promise.race`) |
97
+ | `yield ctx.utcNow()` | Deterministic timestamp |
98
+ | `yield ctx.newGuid()` | Deterministic GUID |
99
+ | `yield ctx.continueAsNew(newInput)` | Restart with fresh history |
100
+
101
+ Tracing methods are **fire-and-forget** (no yield needed):
102
+
103
+ | Method | Description |
104
+ |--------|-------------|
105
+ | `ctx.traceInfo(message)` | INFO log (suppressed during replay) |
106
+ | `ctx.traceWarn(message)` | WARN log |
107
+ | `ctx.traceError(message)` | ERROR log |
108
+ | `ctx.traceDebug(message)` | DEBUG log |
109
+
110
+ ## Storage Backends
111
+
112
+ ### SQLite
113
+
114
+ ```javascript
115
+ const provider = await SqliteProvider.open('sqlite:path/to/db.db');
116
+ // or in-memory:
117
+ const provider = await SqliteProvider.inMemory();
118
+ ```
119
+
120
+ ### PostgreSQL
121
+
122
+ ```javascript
123
+ const provider = await PostgresProvider.connectWithSchema(
124
+ 'postgresql://user:pass@host:5432/db',
125
+ 'my_schema'
126
+ );
127
+ ```
128
+
129
+ ## Logging
130
+
131
+ Duroxide uses Rust's `tracing` crate. Control verbosity with `RUST_LOG`:
132
+
133
+ ```bash
134
+ RUST_LOG=info node app.js # INFO and above
135
+ RUST_LOG=duroxide=debug node app.js # DEBUG for duroxide only
136
+ RUST_LOG=duroxide::activity=info node app.js # Activity traces only
137
+ ```
138
+
139
+ ## Documentation
140
+
141
+ - [Architecture](docs/architecture.md) — how the Rust/JS interop works, yield vs await, limitations
142
+ - [User Guide](docs/user-guide.md) — patterns, recipes, and best practices
143
+
144
+ ## Tests
145
+
146
+ Requires PostgreSQL (see `.env.example`):
147
+
148
+ ```bash
149
+ npm test # e2e tests (23 PG + 1 SQLite smoketest)
150
+ npm run test:races # Race/join composition tests (7 tests)
151
+ npm run test:admin # Admin API tests (14 tests)
152
+ npm run test:scenarios # Scenario tests (6 tests)
153
+ npm run test:all # Everything (50 tests)
154
+ ```
155
+
156
+ ## License
157
+
158
+ [MIT](LICENSE)
package/index.d.ts ADDED
@@ -0,0 +1,220 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /* auto-generated by NAPI-RS */
5
+
6
+ /** Runtime options configurable from JavaScript. */
7
+ export interface JsRuntimeOptions {
8
+ /** Orchestration concurrency (default: 4) */
9
+ orchestrationConcurrency?: number
10
+ /** Worker/activity concurrency (default: 8) */
11
+ workerConcurrency?: number
12
+ /** Dispatcher poll interval in ms (default: 100) */
13
+ dispatcherPollIntervalMs?: number
14
+ /**
15
+ * Worker lock timeout in ms (default: 30000). Controls how often the activity
16
+ * manager renews locks, which affects cancellation detection speed.
17
+ */
18
+ workerLockTimeoutMs?: number
19
+ }
20
+ /** Orchestration status returned to JS. */
21
+ export interface JsOrchestrationStatus {
22
+ status: string
23
+ output?: string
24
+ error?: string
25
+ }
26
+ /** System metrics returned to JS. */
27
+ export interface JsSystemMetrics {
28
+ totalInstances: number
29
+ totalExecutions: number
30
+ runningInstances: number
31
+ completedInstances: number
32
+ failedInstances: number
33
+ totalEvents: number
34
+ }
35
+ /** Queue depths returned to JS. */
36
+ export interface JsQueueDepths {
37
+ orchestratorQueue: number
38
+ workerQueue: number
39
+ timerQueue: number
40
+ }
41
+ /** Instance info returned to JS. */
42
+ export interface JsInstanceInfo {
43
+ instanceId: string
44
+ orchestrationName: string
45
+ orchestrationVersion: string
46
+ currentExecutionId: number
47
+ status: string
48
+ output?: string
49
+ createdAt: number
50
+ updatedAt: number
51
+ parentInstanceId?: string
52
+ }
53
+ /** Execution info returned to JS. */
54
+ export interface JsExecutionInfo {
55
+ executionId: number
56
+ status: string
57
+ output?: string
58
+ startedAt: number
59
+ completedAt?: number
60
+ eventCount: number
61
+ }
62
+ /** Instance tree returned to JS. */
63
+ export interface JsInstanceTree {
64
+ rootId: string
65
+ allIds: Array<string>
66
+ size: number
67
+ }
68
+ /** Delete result returned to JS. */
69
+ export interface JsDeleteInstanceResult {
70
+ instancesDeleted: number
71
+ executionsDeleted: number
72
+ eventsDeleted: number
73
+ queueMessagesDeleted: number
74
+ }
75
+ /** Prune options from JS. */
76
+ export interface JsPruneOptions {
77
+ keepLast?: number
78
+ completedBefore?: number
79
+ }
80
+ /** Prune result returned to JS. */
81
+ export interface JsPruneResult {
82
+ instancesProcessed: number
83
+ executionsDeleted: number
84
+ eventsDeleted: number
85
+ }
86
+ /** Instance filter from JS. */
87
+ export interface JsInstanceFilter {
88
+ instanceIds?: Array<string>
89
+ completedBefore?: number
90
+ limit?: number
91
+ }
92
+ /** A single history event returned to JS. */
93
+ export interface JsEvent {
94
+ eventId: number
95
+ kind: string
96
+ sourceEventId?: number
97
+ timestampMs: number
98
+ }
99
+ /**
100
+ * Emit an activity trace through the current Rust ActivityContext.
101
+ * Delegates to ActivityContext.trace_info/warn/error/debug which includes
102
+ * all structured fields (instance_id, activity_name, activity_id, worker_id, etc.)
103
+ */
104
+ export declare function activityTraceLog(token: string, level: string, message: string): void
105
+ /**
106
+ * Emit an orchestration trace through the Rust OrchestrationContext.
107
+ * Delegates to OrchestrationContext.trace() which checks is_replaying
108
+ * and includes all structured fields (instance_id, orchestration_name, etc.)
109
+ */
110
+ export declare function orchestrationTraceLog(instanceId: string, level: string, message: string): void
111
+ /**
112
+ * Check if an activity's cancellation token has been triggered.
113
+ * Returns true if the activity has been cancelled (e.g., due to losing a race/select).
114
+ */
115
+ export declare function activityIsCancelled(token: string): boolean
116
+ /** Wraps duroxide's Client for use from JavaScript. */
117
+ export declare class JsClient {
118
+ constructor(provider: JsSqliteProvider)
119
+ /** Create a client backed by PostgreSQL. */
120
+ static fromPostgres(provider: JsPostgresProvider): JsClient
121
+ /** Start a new orchestration instance. */
122
+ startOrchestration(instanceId: string, orchestrationName: string, input: string): Promise<void>
123
+ /** Start a new orchestration instance with a specific version. */
124
+ startOrchestrationVersioned(instanceId: string, orchestrationName: string, input: string, version: string): Promise<void>
125
+ /** Get the current status of an orchestration instance. */
126
+ getStatus(instanceId: string): Promise<JsOrchestrationStatus>
127
+ /** Wait for an orchestration to complete (with timeout in milliseconds). */
128
+ waitForOrchestration(instanceId: string, timeoutMs: number): Promise<JsOrchestrationStatus>
129
+ /** Cancel a running orchestration instance. */
130
+ cancelInstance(instanceId: string, reason?: string | undefined | null): Promise<void>
131
+ /** Raise an external event to an orchestration instance. */
132
+ raiseEvent(instanceId: string, eventName: string, data: string): Promise<void>
133
+ /** Get system metrics (if provider supports management). */
134
+ getSystemMetrics(): Promise<JsSystemMetrics>
135
+ /** Get queue depths (if provider supports management). */
136
+ getQueueDepths(): Promise<JsQueueDepths>
137
+ /** List all orchestration instance IDs. */
138
+ listAllInstances(): Promise<Array<string>>
139
+ /** List orchestration instance IDs by status. */
140
+ listInstancesByStatus(status: string): Promise<Array<string>>
141
+ /** Get detailed info about a specific instance. */
142
+ getInstanceInfo(instanceId: string): Promise<JsInstanceInfo>
143
+ /** Get detailed info about a specific execution within an instance. */
144
+ getExecutionInfo(instanceId: string, executionId: number): Promise<JsExecutionInfo>
145
+ /** List execution IDs for an instance. */
146
+ listExecutions(instanceId: string): Promise<Array<number>>
147
+ /** Read the event history for a specific execution. */
148
+ readExecutionHistory(instanceId: string, executionId: number): Promise<Array<JsEvent>>
149
+ /** Get the full instance tree (root + all descendants). */
150
+ getInstanceTree(instanceId: string): Promise<JsInstanceTree>
151
+ /** Delete an orchestration instance and all its data. */
152
+ deleteInstance(instanceId: string, force: boolean): Promise<JsDeleteInstanceResult>
153
+ /** Delete multiple instances matching a filter. */
154
+ deleteInstanceBulk(filter: JsInstanceFilter): Promise<JsDeleteInstanceResult>
155
+ /** Prune old executions from a single instance. */
156
+ pruneExecutions(instanceId: string, options: JsPruneOptions): Promise<JsPruneResult>
157
+ /** Prune old executions from multiple instances matching a filter. */
158
+ pruneExecutionsBulk(filter: JsInstanceFilter, options: JsPruneOptions): Promise<JsPruneResult>
159
+ }
160
+ /** Wraps duroxide-pg's PostgresProvider for use from JavaScript. */
161
+ export declare class JsPostgresProvider {
162
+ /**
163
+ * Connect to a PostgreSQL database.
164
+ * Uses the default "public" schema.
165
+ */
166
+ static connect(databaseUrl: string): Promise<JsPostgresProvider>
167
+ /**
168
+ * Connect to a PostgreSQL database with a custom schema.
169
+ * The schema will be created if it does not exist.
170
+ */
171
+ static connectWithSchema(databaseUrl: string, schema: string): Promise<JsPostgresProvider>
172
+ }
173
+ /** Wraps duroxide's SqliteProvider for use from JavaScript. */
174
+ export declare class JsSqliteProvider {
175
+ /**
176
+ * Open a SQLite database at the given file path.
177
+ * Path should be a sqlite: URL, e.g. "sqlite:./data.db" or "sqlite:/tmp/test.db".
178
+ * The file will be created if it does not exist.
179
+ */
180
+ static open(path: string): Promise<JsSqliteProvider>
181
+ /** Create an in-memory SQLite database (useful for testing). */
182
+ static inMemory(): Promise<JsSqliteProvider>
183
+ }
184
+ /** Builder for the duroxide runtime, wrapping registration and startup. */
185
+ export declare class JsRuntime {
186
+ constructor(provider: JsSqliteProvider, options?: JsRuntimeOptions | undefined | null)
187
+ /** Create a runtime backed by PostgreSQL. */
188
+ static fromPostgres(provider: JsPostgresProvider, options?: JsRuntimeOptions | undefined | null): JsRuntime
189
+ /**
190
+ * Set the generator driver functions (called once from JS before registering orchestrations).
191
+ * These three functions handle: creating generators, driving next steps, and disposing.
192
+ */
193
+ setGeneratorDriver(createFn: (arg: string) => any, nextFn: (arg: string) => any, disposeFn: (arg: string) => any): void
194
+ /**
195
+ * Register a JavaScript activity function.
196
+ * The JS function receives (contextInfoJson, input) and returns a Promise<string>.
197
+ */
198
+ registerActivity(name: string, callback: (arg: string) => any): void
199
+ /**
200
+ * Register a JavaScript orchestration (generator function).
201
+ * The orchestration name is used for both registration and the generator function lookup.
202
+ */
203
+ registerOrchestration(name: string): void
204
+ /** Register a versioned JavaScript orchestration. */
205
+ registerOrchestrationVersioned(name: string, version: string): void
206
+ /**
207
+ * Start the runtime. This processes orchestrations and activities until shutdown.
208
+ *
209
+ * # Safety
210
+ * This is async and takes &mut self. napi-rs requires async &mut methods to be marked unsafe.
211
+ */
212
+ start(): Promise<void>
213
+ /**
214
+ * Shutdown the runtime gracefully.
215
+ *
216
+ * # Safety
217
+ * Must not be called concurrently from multiple threads.
218
+ */
219
+ shutdown(timeoutMs?: number | undefined | null): Promise<void>
220
+ }