transactional-ai 0.1.0 → 0.2.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/README.md +120 -27
- package/dist/engine/Lock.d.ts +27 -0
- package/dist/engine/Lock.d.ts.map +1 -0
- package/dist/engine/Lock.js +25 -0
- package/dist/engine/Lock.js.map +1 -0
- package/dist/engine/PostgresStorage.d.ts +13 -0
- package/dist/engine/PostgresStorage.d.ts.map +1 -0
- package/dist/engine/PostgresStorage.js +45 -0
- package/dist/engine/PostgresStorage.js.map +1 -0
- package/dist/engine/RedisStorage.d.ts +11 -0
- package/dist/engine/RedisStorage.d.ts.map +1 -1
- package/dist/engine/RedisStorage.js +34 -1
- package/dist/engine/RedisStorage.js.map +1 -1
- package/dist/engine/Transaction.d.ts +11 -0
- package/dist/engine/Transaction.d.ts.map +1 -1
- package/dist/engine/Transaction.js +62 -26
- package/dist/engine/Transaction.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/package.json +24 -4
package/README.md
CHANGED
|
@@ -5,13 +5,17 @@ Implement the Saga Pattern with persistent rollback and state recovery for Long-
|
|
|
5
5
|
|
|
6
6
|
[](https://www.npmjs.com/package/transactional-ai)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
|
+

|
|
8
9
|
|
|
9
10
|
## Why use this?
|
|
11
|
+
|
|
10
12
|
AI Agents are flaky. Steps fail, APIs time out, and hallucinations happen.
|
|
11
13
|
`transactional-ai` gives you:
|
|
14
|
+
|
|
12
15
|
1. **Automatic Rollbacks**: If step 3 fails, steps 2 and 1 are compensated (undone) automatically.
|
|
13
|
-
2. **
|
|
14
|
-
3. **
|
|
16
|
+
2. **Concurrency Safety**: Distributed locking prevents race conditions when scaling workers.
|
|
17
|
+
3. **Persistence**: Transactions survive process crashes using Redis, Postgres, or File storage.
|
|
18
|
+
4. **Resilience**: Built-in retry policies for flaky LLM APIs.
|
|
15
19
|
|
|
16
20
|
## Installation
|
|
17
21
|
|
|
@@ -24,63 +28,146 @@ npm install transactional-ai
|
|
|
24
28
|
## Quick Start
|
|
25
29
|
|
|
26
30
|
### 1. The "Litmus Test" (Basic Usage)
|
|
31
|
+
|
|
27
32
|
Define a transaction where every action has a compensating rollback action.
|
|
28
33
|
|
|
29
34
|
```typescript
|
|
30
|
-
import { Transaction } from
|
|
35
|
+
import { Transaction } from "transactional-ai";
|
|
31
36
|
|
|
32
37
|
// 1. Create a named transaction (required for resumability)
|
|
33
|
-
const agent = new Transaction(
|
|
38
|
+
const agent = new Transaction("user-onboarding-123");
|
|
34
39
|
|
|
35
40
|
agent
|
|
36
41
|
.step({
|
|
37
|
-
name:
|
|
42
|
+
name: "create-file",
|
|
38
43
|
execute: async (ctx) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
// Do the work
|
|
45
|
+
const file = await googleDrive.createFile("report.txt");
|
|
46
|
+
return file.id;
|
|
42
47
|
},
|
|
43
48
|
compensate: async (fileId) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
49
|
+
// Undo the work
|
|
50
|
+
await googleDrive.deleteFile(fileId);
|
|
51
|
+
},
|
|
47
52
|
})
|
|
48
53
|
.step({
|
|
49
|
-
name:
|
|
54
|
+
name: "email-report",
|
|
50
55
|
execute: async (ctx) => {
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
// Use previous results via context or external state
|
|
57
|
+
await emailService.send(ctx.result);
|
|
53
58
|
},
|
|
54
59
|
compensate: async () => {
|
|
55
|
-
|
|
56
|
-
}
|
|
60
|
+
await emailService.recallLast();
|
|
61
|
+
},
|
|
57
62
|
});
|
|
58
63
|
|
|
59
64
|
// 2. Run it
|
|
60
|
-
await agent.run({ initialData:
|
|
65
|
+
await agent.run({ initialData: "foo" });
|
|
61
66
|
```
|
|
62
67
|
|
|
63
68
|
### Adding Persistence (Redis)
|
|
69
|
+
|
|
64
70
|
To survive process crashes, simply provide a storage adapter.
|
|
65
71
|
|
|
66
72
|
```typescript
|
|
67
|
-
import { Transaction, RedisStorage } from
|
|
73
|
+
import { Transaction, RedisStorage } from "transactional-ai";
|
|
68
74
|
|
|
69
|
-
const storage = new RedisStorage(
|
|
70
|
-
const agent = new Transaction(
|
|
75
|
+
const storage = new RedisStorage("redis://localhost:6379");
|
|
76
|
+
const agent = new Transaction("workflow-id-555", storage);
|
|
71
77
|
|
|
72
78
|
// If the process crashes here, running this code again
|
|
73
79
|
// will automatically SKIP completed steps and resume at the failure point.
|
|
74
80
|
agent
|
|
75
|
-
.step({
|
|
76
|
-
|
|
81
|
+
.step({
|
|
82
|
+
/* ... */
|
|
83
|
+
})
|
|
84
|
+
.step({
|
|
85
|
+
/* ... */
|
|
86
|
+
});
|
|
77
87
|
|
|
78
88
|
await agent.run();
|
|
79
89
|
```
|
|
80
90
|
|
|
91
|
+
## Enterprise Features (v0.2.0)
|
|
92
|
+
|
|
93
|
+
### 1. Distributed Locking (Redis)
|
|
94
|
+
|
|
95
|
+
Prevent race conditions when running multiple agent workers on the same Transaction ID.
|
|
96
|
+
|
|
97
|
+
```typeScript
|
|
98
|
+
import { Transaction, RedisStorage, RedisLock } from 'transactional-ai';
|
|
99
|
+
|
|
100
|
+
const connection = 'redis://localhost:6379';
|
|
101
|
+
const storage = new RedisStorage(connection);
|
|
102
|
+
const lock = new RedisLock(connection);
|
|
103
|
+
|
|
104
|
+
const agent = new Transaction('tx-unique-id', storage, {
|
|
105
|
+
lock: lock,
|
|
106
|
+
lockTTL: 30000 // Hold lock for 30s
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Safe to run across multiple processes/servers
|
|
110
|
+
await agent.run();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 2. Postgres Storage (SQL)
|
|
114
|
+
|
|
115
|
+
Use Postgres for strict ACID compliance and auditability.
|
|
116
|
+
|
|
117
|
+
Prerequisite: Run schema.sql in your database.
|
|
118
|
+
|
|
119
|
+
```typeScript
|
|
120
|
+
import { Transaction, PostgresStorage } from 'transactional-ai';
|
|
121
|
+
|
|
122
|
+
const storage = new PostgresStorage('postgresql://user:pass@localhost:5432/mydb');
|
|
123
|
+
const agent = new Transaction('tx-id', storage);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### Setting Up Postgres
|
|
127
|
+
|
|
128
|
+
Before using PostgresStorage, create the required table in your database:
|
|
129
|
+
|
|
130
|
+
**Option 1: Run the provided schema**
|
|
131
|
+
```bash
|
|
132
|
+
# From project root
|
|
133
|
+
psql -U your_user -d your_database -f schema.sql
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Option 2: Copy-paste this SQL**
|
|
137
|
+
```sql
|
|
138
|
+
CREATE TABLE IF NOT EXISTS transactions (
|
|
139
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
140
|
+
status VARCHAR(50) NOT NULL,
|
|
141
|
+
step_stack JSONB NOT NULL,
|
|
142
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
143
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_transactions_status ON transactions(status);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Important:** The table must exist before running your first transaction. It will NOT be auto-created.
|
|
150
|
+
|
|
151
|
+
### 3. Retry Policies
|
|
152
|
+
|
|
153
|
+
Handle flaky APIs (e.g., OpenAI 500 errors) automatically without failing the transaction.
|
|
154
|
+
|
|
155
|
+
```typeScript
|
|
156
|
+
agent.step({
|
|
157
|
+
name: 'generate-text',
|
|
158
|
+
execute: async () => await openai.createCompletion(...),
|
|
159
|
+
compensate: async () => { /* ... */ },
|
|
160
|
+
retry: {
|
|
161
|
+
attempts: 3, // Try 3 times
|
|
162
|
+
backoffMs: 1000 // Wait 1s between attempts
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
81
167
|
---
|
|
82
168
|
|
|
83
169
|
## CLI Inspector
|
|
170
|
+
|
|
84
171
|
You don't need a complex dashboard to see what your agents are doing. Use the included CLI to inspect transaction logs directly from your terminal.
|
|
85
172
|
|
|
86
173
|
```bash
|
|
@@ -111,15 +198,17 @@ Output:
|
|
|
111
198
|
## Advanced Usage
|
|
112
199
|
|
|
113
200
|
### Audit Mode (Governance)
|
|
201
|
+
|
|
114
202
|
By default, logs are cleared upon success to save storage space. To keep a permanent audit trail for compliance (e.g., "Why did the agent do this?"), enable Audit Mode:
|
|
115
203
|
|
|
116
204
|
```typescript
|
|
117
|
-
const agent = new Transaction(
|
|
118
|
-
|
|
205
|
+
const agent = new Transaction("id", storage, {
|
|
206
|
+
cleanupOnSuccess: false,
|
|
119
207
|
});
|
|
120
208
|
```
|
|
121
209
|
|
|
122
210
|
### Manual Rollbacks
|
|
211
|
+
|
|
123
212
|
The library handles rollbacks automatically on error. You can trigger them manually by throwing an error inside any step:
|
|
124
213
|
|
|
125
214
|
```typescript
|
|
@@ -143,6 +232,7 @@ agent.step({
|
|
|
143
232
|
---
|
|
144
233
|
|
|
145
234
|
## Roadmap
|
|
235
|
+
|
|
146
236
|
[x] Core Saga Engine (Execute/Compensate)
|
|
147
237
|
|
|
148
238
|
[x] Persistence Adapters (File, Redis)
|
|
@@ -151,11 +241,14 @@ agent.step({
|
|
|
151
241
|
|
|
152
242
|
[x] CLI Inspector (tai-inspect)
|
|
153
243
|
|
|
154
|
-
[
|
|
244
|
+
[x] Concurrent Transaction Locking
|
|
245
|
+
|
|
246
|
+
[x] Postgres/SQL Storage Adapter
|
|
155
247
|
|
|
156
|
-
[
|
|
248
|
+
[x] Step Retry Policies
|
|
157
249
|
|
|
158
250
|
---
|
|
159
251
|
|
|
160
252
|
## License
|
|
161
|
-
|
|
253
|
+
|
|
254
|
+
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare class TransactionLockedError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export interface LockAdapter {
|
|
5
|
+
/**
|
|
6
|
+
* Attempts to acquire a lock for the given transaction ID.
|
|
7
|
+
* @param id The transaction ID to lock
|
|
8
|
+
* @param ttlMs How long (in ms) to hold the lock before auto-expiring (safety)
|
|
9
|
+
* @returns A "lock token" if successful, or null if the lock is busy.
|
|
10
|
+
*/
|
|
11
|
+
acquire(id: string, ttlMs: number): Promise<string | null>;
|
|
12
|
+
/**
|
|
13
|
+
* Releases the lock using the token received during acquire.
|
|
14
|
+
* @param id The transaction ID
|
|
15
|
+
* @param token The token received from acquire
|
|
16
|
+
*/
|
|
17
|
+
release(id: string, token: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Default "No Operation" lock for single-process usage.
|
|
21
|
+
* It never blocks.
|
|
22
|
+
*/
|
|
23
|
+
export declare class NoOpLock implements LockAdapter {
|
|
24
|
+
acquire(id: string, ttlMs: number): Promise<string>;
|
|
25
|
+
release(id: string, token: string): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=Lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Lock.d.ts","sourceRoot":"","sources":["../../engine/Lock.ts"],"names":[],"mappings":"AAAA,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAE3D;;;;OAIG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD;AAED;;;GAGG;AACH,qBAAa,QAAS,YAAW,WAAW;IAEpC,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAGnD,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGxD"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NoOpLock = exports.TransactionLockedError = void 0;
|
|
4
|
+
class TransactionLockedError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'TransactionLockedError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.TransactionLockedError = TransactionLockedError;
|
|
11
|
+
/**
|
|
12
|
+
* Default "No Operation" lock for single-process usage.
|
|
13
|
+
* It never blocks.
|
|
14
|
+
*/
|
|
15
|
+
class NoOpLock {
|
|
16
|
+
// FIXED: Added 'ttlMs' to match interface signature
|
|
17
|
+
async acquire(id, ttlMs) {
|
|
18
|
+
return 'dummy-token';
|
|
19
|
+
}
|
|
20
|
+
async release(id, token) {
|
|
21
|
+
// Do nothing
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.NoOpLock = NoOpLock;
|
|
25
|
+
//# sourceMappingURL=Lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Lock.js","sourceRoot":"","sources":["../../engine/Lock.ts"],"names":[],"mappings":";;;AAAA,MAAa,sBAAuB,SAAQ,KAAK;IAC/C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AALD,wDAKC;AAmBD;;;GAGG;AACH,MAAa,QAAQ;IACnB,oDAAoD;IACpD,KAAK,CAAC,OAAO,CAAC,EAAU,EAAE,KAAa;QACrC,OAAO,aAAa,CAAC;IACvB,CAAC;IACD,KAAK,CAAC,OAAO,CAAC,EAAU,EAAE,KAAa;QACrC,aAAa;IACf,CAAC;CACF;AARD,4BAQC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
import { StorageAdapter } from './Storage';
|
|
3
|
+
import { StepContext } from './Transaction';
|
|
4
|
+
export declare class PostgresStorage implements StorageAdapter {
|
|
5
|
+
private pool;
|
|
6
|
+
private tableName;
|
|
7
|
+
constructor(connectionString: string, tableName?: string, pool?: Pool);
|
|
8
|
+
save(txId: string, state: StepContext[]): Promise<void>;
|
|
9
|
+
load(txId: string): Promise<StepContext[] | null>;
|
|
10
|
+
clear(txId: string): Promise<void>;
|
|
11
|
+
disconnect(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=PostgresStorage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PostgresStorage.d.ts","sourceRoot":"","sources":["../../engine/PostgresStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC1B,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,qBAAa,eAAgB,YAAW,cAAc;IACpD,OAAO,CAAC,IAAI,CAAO;IACnB,OAAO,CAAC,SAAS,CAAS;gBAGd,gBAAgB,EAAE,MAAM,EAAE,SAAS,SAAiB,EAAE,IAAI,CAAC,EAAE,IAAI;IAMvE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBvD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC;IAQjD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlC,UAAU;CAGjB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PostgresStorage = void 0;
|
|
4
|
+
const pg_1 = require("pg");
|
|
5
|
+
class PostgresStorage {
|
|
6
|
+
pool;
|
|
7
|
+
tableName;
|
|
8
|
+
// CHANGED: Accept optional 'pool' for testing/sharing
|
|
9
|
+
constructor(connectionString, tableName = 'transactions', pool) {
|
|
10
|
+
this.tableName = tableName;
|
|
11
|
+
// Use provided pool OR create a new one
|
|
12
|
+
this.pool = pool || new pg_1.Pool({ connectionString });
|
|
13
|
+
}
|
|
14
|
+
async save(txId, state) {
|
|
15
|
+
const status = state.length > 0 && state[state.length - 1].status === 'completed'
|
|
16
|
+
? 'pending'
|
|
17
|
+
: 'pending';
|
|
18
|
+
const query = `
|
|
19
|
+
INSERT INTO ${this.tableName} (id, status, step_stack, updated_at)
|
|
20
|
+
VALUES ($1, $2, $3, NOW())
|
|
21
|
+
ON CONFLICT (id)
|
|
22
|
+
DO UPDATE SET
|
|
23
|
+
step_stack = EXCLUDED.step_stack,
|
|
24
|
+
updated_at = NOW(),
|
|
25
|
+
status = EXCLUDED.status;
|
|
26
|
+
`;
|
|
27
|
+
await this.pool.query(query, [txId, status, JSON.stringify(state)]);
|
|
28
|
+
}
|
|
29
|
+
async load(txId) {
|
|
30
|
+
const query = `SELECT step_stack FROM ${this.tableName} WHERE id = $1`;
|
|
31
|
+
const res = await this.pool.query(query, [txId]);
|
|
32
|
+
if (res.rows.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
return res.rows[0].step_stack;
|
|
35
|
+
}
|
|
36
|
+
async clear(txId) {
|
|
37
|
+
const query = `DELETE FROM ${this.tableName} WHERE id = $1`;
|
|
38
|
+
await this.pool.query(query, [txId]);
|
|
39
|
+
}
|
|
40
|
+
async disconnect() {
|
|
41
|
+
await this.pool.end();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.PostgresStorage = PostgresStorage;
|
|
45
|
+
//# sourceMappingURL=PostgresStorage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PostgresStorage.js","sourceRoot":"","sources":["../../engine/PostgresStorage.ts"],"names":[],"mappings":";;;AAAA,2BAA0B;AAI1B,MAAa,eAAe;IAClB,IAAI,CAAO;IACX,SAAS,CAAS;IAE1B,sDAAsD;IACtD,YAAY,gBAAwB,EAAE,SAAS,GAAG,cAAc,EAAE,IAAW;QAC3E,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,wCAAwC;QACxC,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,IAAI,SAAI,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAY,EAAE,KAAoB;QAC3C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW;YAC/E,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,SAAS,CAAC;QAEd,MAAM,KAAK,GAAG;oBACE,IAAI,CAAC,SAAS;;;;;;;KAO7B,CAAC;QAEF,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACtE,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAY;QACrB,MAAM,KAAK,GAAG,0BAA0B,IAAI,CAAC,SAAS,gBAAgB,CAAC;QACvE,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;QAEjD,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACvC,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAA2B,CAAC;IACjD,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAY;QACtB,MAAM,KAAK,GAAG,eAAe,IAAI,CAAC,SAAS,gBAAgB,CAAC;QAC5D,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IACvC,CAAC;IAED,KAAK,CAAC,UAAU;QACd,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;IACxB,CAAC;CACF;AA7CD,0CA6CC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { StorageAdapter } from './Storage';
|
|
2
2
|
import { StepContext } from './Transaction';
|
|
3
|
+
import { LockAdapter } from './Lock';
|
|
3
4
|
export declare class RedisStorage implements StorageAdapter {
|
|
4
5
|
private redis;
|
|
5
6
|
private ttlSeconds;
|
|
@@ -13,4 +14,14 @@ export declare class RedisStorage implements StorageAdapter {
|
|
|
13
14
|
clear(txId: string): Promise<void>;
|
|
14
15
|
disconnect(): Promise<void>;
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Distributed Lock implementation using Redis SET NX
|
|
19
|
+
*/
|
|
20
|
+
export declare class RedisLock implements LockAdapter {
|
|
21
|
+
private redis;
|
|
22
|
+
constructor(connectionString: string);
|
|
23
|
+
acquire(id: string, ttlMs: number): Promise<string | null>;
|
|
24
|
+
release(id: string, token: string): Promise<void>;
|
|
25
|
+
disconnect(): Promise<void>;
|
|
26
|
+
}
|
|
16
27
|
//# sourceMappingURL=RedisStorage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RedisStorage.d.ts","sourceRoot":"","sources":["../../engine/RedisStorage.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"RedisStorage.d.ts","sourceRoot":"","sources":["../../engine/RedisStorage.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAErC,qBAAa,YAAa,YAAW,cAAc;IACjD,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,UAAU,CAAS;IAE3B;;;OAGG;gBACS,gBAAgB,EAAE,MAAM,EAAE,UAAU,SAAO;IAKjD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAOvD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC;IAKjD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlC,UAAU;CAGjB;AAED;;GAEG;AACH,qBAAa,SAAU,YAAW,WAAW;IAC3C,OAAO,CAAC,KAAK,CAAQ;gBAET,gBAAgB,EAAE,MAAM;IAI9B,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAU1D,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBjD,UAAU;CAGjB"}
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.RedisStorage = void 0;
|
|
6
|
+
exports.RedisLock = exports.RedisStorage = void 0;
|
|
7
7
|
const ioredis_1 = __importDefault(require("ioredis"));
|
|
8
8
|
class RedisStorage {
|
|
9
9
|
redis;
|
|
@@ -35,4 +35,37 @@ class RedisStorage {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
exports.RedisStorage = RedisStorage;
|
|
38
|
+
/**
|
|
39
|
+
* Distributed Lock implementation using Redis SET NX
|
|
40
|
+
*/
|
|
41
|
+
class RedisLock {
|
|
42
|
+
redis;
|
|
43
|
+
constructor(connectionString) {
|
|
44
|
+
this.redis = new ioredis_1.default(connectionString);
|
|
45
|
+
}
|
|
46
|
+
async acquire(id, ttlMs) {
|
|
47
|
+
const token = Math.random().toString(36).substring(2);
|
|
48
|
+
const key = `lock:${id}`;
|
|
49
|
+
// FIXED: ioredis requires 'PX' (expiry) before 'NX' (condition)
|
|
50
|
+
const result = await this.redis.set(key, token, 'PX', ttlMs, 'NX');
|
|
51
|
+
return result === 'OK' ? token : null;
|
|
52
|
+
}
|
|
53
|
+
async release(id, token) {
|
|
54
|
+
const key = `lock:${id}`;
|
|
55
|
+
// Safety: Only delete the lock if *we* own it (check token).
|
|
56
|
+
// Lua script guarantees atomicity.
|
|
57
|
+
const script = `
|
|
58
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
59
|
+
return redis.call("del", KEYS[1])
|
|
60
|
+
else
|
|
61
|
+
return 0
|
|
62
|
+
end
|
|
63
|
+
`;
|
|
64
|
+
await this.redis.eval(script, 1, key, token);
|
|
65
|
+
}
|
|
66
|
+
async disconnect() {
|
|
67
|
+
await this.redis.quit();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.RedisLock = RedisLock;
|
|
38
71
|
//# sourceMappingURL=RedisStorage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RedisStorage.js","sourceRoot":"","sources":["../../engine/RedisStorage.ts"],"names":[],"mappings":";;;;;;AAAA,sDAA4B;
|
|
1
|
+
{"version":3,"file":"RedisStorage.js","sourceRoot":"","sources":["../../engine/RedisStorage.ts"],"names":[],"mappings":";;;;;;AAAA,sDAA4B;AAK5B,MAAa,YAAY;IACf,KAAK,CAAQ;IACb,UAAU,CAAS;IAE3B;;;OAGG;IACH,YAAY,gBAAwB,EAAE,UAAU,GAAG,IAAI;QACrD,IAAI,CAAC,KAAK,GAAG,IAAI,iBAAK,CAAC,gBAAgB,CAAC,CAAC;QACzC,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAY,EAAE,KAAoB;QAC3C,MAAM,GAAG,GAAG,MAAM,IAAI,EAAE,CAAC;QACzB,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QACjD,6EAA6E;QAC7E,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAChD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAY;QACrB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAY;QACtB,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;IACrC,CAAC;IAED,iDAAiD;IACjD,KAAK,CAAC,UAAU;QACd,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IAC1B,CAAC;CACF;AAjCD,oCAiCC;AAED;;GAEG;AACH,MAAa,SAAS;IACZ,KAAK,CAAQ;IAErB,YAAY,gBAAwB;QAClC,IAAI,CAAC,KAAK,GAAG,IAAI,iBAAK,CAAC,gBAAgB,CAAC,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,EAAU,EAAE,KAAa;QACrC,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QACtD,MAAM,GAAG,GAAG,QAAQ,EAAE,EAAE,CAAC;QAEzB,gEAAgE;QAChE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAEnE,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,EAAU,EAAE,KAAa;QACrC,MAAM,GAAG,GAAG,QAAQ,EAAE,EAAE,CAAC;QAEzB,6DAA6D;QAC7D,mCAAmC;QACnC,MAAM,MAAM,GAAG;;;;;;KAMd,CAAC;QAEF,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;IAC/C,CAAC;IAED,KAAK,CAAC,UAAU;QACd,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IAC1B,CAAC;CACF;AApCD,8BAoCC"}
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { StorageAdapter } from './Storage';
|
|
2
|
+
import { LockAdapter } from './Lock';
|
|
2
3
|
export type StepContext<T = any> = {
|
|
3
4
|
name: string;
|
|
4
5
|
result?: T;
|
|
5
6
|
status: 'completed' | 'pending';
|
|
6
7
|
};
|
|
8
|
+
export type RetryPolicy = {
|
|
9
|
+
attempts: number;
|
|
10
|
+
backoffMs?: number;
|
|
11
|
+
};
|
|
7
12
|
export type StepDefinition<T> = {
|
|
8
13
|
do: () => Promise<T> | T;
|
|
9
14
|
undo: (result: T) => Promise<void> | void;
|
|
15
|
+
retry?: RetryPolicy;
|
|
10
16
|
};
|
|
11
17
|
type TransactionOptions = {
|
|
12
18
|
cleanupOnSuccess?: boolean;
|
|
19
|
+
lock?: LockAdapter;
|
|
20
|
+
lockTTL?: number;
|
|
13
21
|
};
|
|
14
22
|
export declare class Transaction {
|
|
15
23
|
private stepStack;
|
|
@@ -17,9 +25,12 @@ export declare class Transaction {
|
|
|
17
25
|
private id;
|
|
18
26
|
private storage?;
|
|
19
27
|
private cleanupOnSuccess;
|
|
28
|
+
private lock;
|
|
29
|
+
private lockTTL;
|
|
20
30
|
constructor(id: string, storage?: StorageAdapter, options?: TransactionOptions);
|
|
21
31
|
run(workflow: (tx: Transaction) => Promise<void>): Promise<void>;
|
|
22
32
|
step<T>(name: string, definition: StepDefinition<T>): Promise<T>;
|
|
33
|
+
private executeWithRetry;
|
|
23
34
|
private rollback;
|
|
24
35
|
}
|
|
25
36
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Transaction.d.ts","sourceRoot":"","sources":["../../engine/Transaction.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Transaction.d.ts","sourceRoot":"","sources":["../../engine/Transaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAoC,MAAM,QAAQ,CAAC;AAEvE,MAAM,MAAM,WAAW,CAAC,CAAC,GAAG,GAAG,IAAI;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,CAAC,CAAC;IACX,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;CACjC,CAAC;AAGF,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAC9B,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACzB,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1C,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB,CAAC;AAQF,KAAK,kBAAkB,GAAG;IACxB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,qBAAa,WAAW;IACtB,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,EAAE,CAAS;IACnB,OAAO,CAAC,OAAO,CAAC,CAAiB;IACjC,OAAO,CAAC,gBAAgB,CAAU;IAGlC,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,OAAO,CAAS;gBAEZ,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,OAAO,GAAE,kBAAuB;IAU5E,GAAG,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAwChE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;YAgCxD,gBAAgB;YAmBhB,QAAQ;CAUvB"}
|
|
@@ -1,69 +1,106 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Transaction = void 0;
|
|
4
|
+
const Lock_1 = require("./Lock");
|
|
4
5
|
class Transaction {
|
|
5
6
|
stepStack = [];
|
|
6
7
|
history = new Map();
|
|
7
8
|
id;
|
|
8
9
|
storage;
|
|
9
10
|
cleanupOnSuccess;
|
|
11
|
+
// Locking Props
|
|
12
|
+
lock;
|
|
13
|
+
lockTTL;
|
|
10
14
|
constructor(id, storage, options = {}) {
|
|
11
15
|
this.id = id;
|
|
12
16
|
this.storage = storage;
|
|
13
|
-
// Default to TRUE (clean up) unless told otherwise
|
|
14
17
|
this.cleanupOnSuccess = options.cleanupOnSuccess ?? true;
|
|
18
|
+
// Default to NoOpLock
|
|
19
|
+
this.lock = options.lock ?? new Lock_1.NoOpLock();
|
|
20
|
+
this.lockTTL = options.lockTTL ?? 30000;
|
|
15
21
|
}
|
|
16
22
|
async run(workflow) {
|
|
17
|
-
// 1.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
savedState.forEach(s => this.history.set(s.name, s.result));
|
|
23
|
-
// Note: We can't re-hydrate the 'undo' functions until the code runs again.
|
|
24
|
-
// The 'step' method handles re-hydrating the stack.
|
|
25
|
-
}
|
|
23
|
+
// 1. Acquire Lock
|
|
24
|
+
console.log(`🔒 [Lock] Acquiring lock for ${this.id}...`);
|
|
25
|
+
const lockToken = await this.lock.acquire(this.id, this.lockTTL);
|
|
26
|
+
if (!lockToken) {
|
|
27
|
+
throw new Lock_1.TransactionLockedError(`Transaction '${this.id}' is currently running elsewhere.`);
|
|
26
28
|
}
|
|
27
29
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// 2. Load previous state
|
|
31
|
+
if (this.storage) {
|
|
32
|
+
const savedState = await this.storage.load(this.id);
|
|
33
|
+
if (savedState) {
|
|
34
|
+
console.log(`[Transaction] 🔄 Resuming ${this.id} with ${savedState.length} completed steps.`);
|
|
35
|
+
savedState.forEach(s => this.history.set(s.name, s.result));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// 3. Execute Workflow
|
|
39
|
+
try {
|
|
40
|
+
await workflow(this);
|
|
41
|
+
// 4. Cleanup on Success
|
|
42
|
+
if (this.storage && this.cleanupOnSuccess) {
|
|
43
|
+
await this.storage.clear(this.id);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error(`\n🔴 [Transaction Failed] Error: ${error instanceof Error ? error.message : error}`);
|
|
48
|
+
await this.rollback();
|
|
49
|
+
throw error;
|
|
32
50
|
}
|
|
33
51
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
52
|
+
finally {
|
|
53
|
+
// 5. ALWAYS Release Lock
|
|
54
|
+
console.log(`🔓 [Lock] Releasing...`);
|
|
55
|
+
await this.lock.release(this.id, lockToken);
|
|
38
56
|
}
|
|
39
57
|
}
|
|
40
58
|
async step(name, definition) {
|
|
41
|
-
// 1. Check
|
|
59
|
+
// 1. Check Resumability
|
|
42
60
|
if (this.history.has(name)) {
|
|
43
61
|
console.log(`⏩ [Skip: ${name}] Already completed.`);
|
|
44
62
|
const result = this.history.get(name);
|
|
45
|
-
// CRITICAL: We still need to push the undo handler to the stack
|
|
46
|
-
// so we can rollback if a LATER step fails.
|
|
47
63
|
this.stepStack.push({ name, undo: () => definition.undo(result) });
|
|
48
64
|
return result;
|
|
49
65
|
}
|
|
50
|
-
// 2. If not, Execute
|
|
51
66
|
console.log(`➡️ [Do: ${name}] Executing...`);
|
|
52
|
-
|
|
67
|
+
// 2. Execute with Retry Logic
|
|
68
|
+
// We replace the direct 'definition.do()' call with our new helper
|
|
69
|
+
const result = await this.executeWithRetry(name, definition);
|
|
53
70
|
// 3. Update State
|
|
54
71
|
this.stepStack.push({ name, undo: () => definition.undo(result) });
|
|
55
72
|
// 4. Persist
|
|
56
73
|
if (this.storage) {
|
|
57
74
|
const currentHistory = this.stepStack.map(s => ({
|
|
58
75
|
name: s.name,
|
|
59
|
-
result: s.name === name ? result : this.history.get(s.name),
|
|
76
|
+
result: s.name === name ? result : this.history.get(s.name),
|
|
60
77
|
status: 'completed'
|
|
61
78
|
}));
|
|
62
|
-
// In a real app, map the full history properly
|
|
63
79
|
await this.storage.save(this.id, currentHistory);
|
|
64
80
|
}
|
|
65
81
|
return result;
|
|
66
82
|
}
|
|
83
|
+
// NEW: Retry Helper
|
|
84
|
+
async executeWithRetry(name, def) {
|
|
85
|
+
const attempts = def.retry?.attempts || 1;
|
|
86
|
+
const backoff = def.retry?.backoffMs || 0;
|
|
87
|
+
for (let i = 1; i <= attempts; i++) {
|
|
88
|
+
try {
|
|
89
|
+
return await def.do();
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (i < attempts) {
|
|
93
|
+
console.warn(` ⚠️ [Retry ${i}/${attempts}] Step '${name}' failed. Retrying in ${backoff}ms...`);
|
|
94
|
+
if (backoff > 0)
|
|
95
|
+
await new Promise(r => setTimeout(r, backoff));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
throw err; // Final failure
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
throw new Error('Unreachable');
|
|
103
|
+
}
|
|
67
104
|
async rollback() {
|
|
68
105
|
const stepsToUndo = [...this.stepStack].reverse();
|
|
69
106
|
for (const step of stepsToUndo) {
|
|
@@ -75,7 +112,6 @@ class Transaction {
|
|
|
75
112
|
console.error(` ❌ [CRITICAL] Undo failed for '${step.name}'`, err);
|
|
76
113
|
}
|
|
77
114
|
}
|
|
78
|
-
// Clear storage after rollback so we don't resume into a broken state
|
|
79
115
|
if (this.storage)
|
|
80
116
|
await this.storage.clear(this.id);
|
|
81
117
|
this.stepStack = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Transaction.js","sourceRoot":"","sources":["../../engine/Transaction.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"Transaction.js","sourceRoot":"","sources":["../../engine/Transaction.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,7 @@ export { Transaction } from './engine/Transaction';
|
|
|
2
2
|
export type { StepContext, StepDefinition } from './engine/Transaction';
|
|
3
3
|
export { StorageAdapter } from './engine/Storage';
|
|
4
4
|
export { FileStorage } from './engine/FileStorage';
|
|
5
|
-
export { RedisStorage } from './engine/RedisStorage';
|
|
5
|
+
export { RedisStorage, RedisLock } from './engine/RedisStorage';
|
|
6
|
+
export { PostgresStorage } from './engine/PostgresStorage';
|
|
7
|
+
export { LockAdapter, TransactionLockedError, NoOpLock } from './engine/Lock';
|
|
6
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGxE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGxE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAG3D,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RedisStorage = exports.FileStorage = exports.Transaction = void 0;
|
|
3
|
+
exports.NoOpLock = exports.TransactionLockedError = exports.PostgresStorage = exports.RedisLock = exports.RedisStorage = exports.FileStorage = exports.Transaction = void 0;
|
|
4
4
|
// Export the core engine
|
|
5
5
|
var Transaction_1 = require("./engine/Transaction");
|
|
6
6
|
Object.defineProperty(exports, "Transaction", { enumerable: true, get: function () { return Transaction_1.Transaction; } });
|
|
@@ -8,5 +8,11 @@ var FileStorage_1 = require("./engine/FileStorage");
|
|
|
8
8
|
Object.defineProperty(exports, "FileStorage", { enumerable: true, get: function () { return FileStorage_1.FileStorage; } });
|
|
9
9
|
var RedisStorage_1 = require("./engine/RedisStorage");
|
|
10
10
|
Object.defineProperty(exports, "RedisStorage", { enumerable: true, get: function () { return RedisStorage_1.RedisStorage; } });
|
|
11
|
-
|
|
11
|
+
Object.defineProperty(exports, "RedisLock", { enumerable: true, get: function () { return RedisStorage_1.RedisLock; } });
|
|
12
|
+
var PostgresStorage_1 = require("./engine/PostgresStorage");
|
|
13
|
+
Object.defineProperty(exports, "PostgresStorage", { enumerable: true, get: function () { return PostgresStorage_1.PostgresStorage; } });
|
|
14
|
+
// Export Locking Primitives
|
|
15
|
+
var Lock_1 = require("./engine/Lock");
|
|
16
|
+
Object.defineProperty(exports, "TransactionLockedError", { enumerable: true, get: function () { return Lock_1.TransactionLockedError; } });
|
|
17
|
+
Object.defineProperty(exports, "NoOpLock", { enumerable: true, get: function () { return Lock_1.NoOpLock; } });
|
|
12
18
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";;;AAAA,yBAAyB;AACzB,oDAAmD;AAA1C,0GAAA,WAAW,OAAA;AAKpB,oDAAmD;AAA1C,0GAAA,WAAW,OAAA;AACpB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";;;AAAA,yBAAyB;AACzB,oDAAmD;AAA1C,0GAAA,WAAW,OAAA;AAKpB,oDAAmD;AAA1C,0GAAA,WAAW,OAAA;AACpB,sDAAgE;AAAvD,4GAAA,YAAY,OAAA;AAAE,yGAAA,SAAS,OAAA;AAChC,4DAA2D;AAAlD,kHAAA,eAAe,OAAA;AAExB,4BAA4B;AAC5B,sCAA8E;AAAxD,8GAAA,sBAAsB,OAAA;AAAE,gGAAA,QAAQ,OAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "transactional-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A reliability protocol for AI Agents. Saga pattern with persistent rollback.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,18 +14,36 @@
|
|
|
14
14
|
"build": "tsc",
|
|
15
15
|
"postbuild": "chmod +x dist/bin/inspect.js",
|
|
16
16
|
"test": "jest",
|
|
17
|
-
"prepublishOnly": "npm run build"
|
|
17
|
+
"prepublishOnly": "npm run build",
|
|
18
|
+
"example:failure": "tsx examples/demo-failure.ts",
|
|
19
|
+
"example:resume": "tsx examples/demo-resume.ts",
|
|
20
|
+
"example:openai": "tsx examples/demo-openai-stripe.ts"
|
|
18
21
|
},
|
|
19
22
|
"keywords": [
|
|
20
23
|
"ai",
|
|
21
24
|
"agent",
|
|
22
25
|
"transaction",
|
|
23
26
|
"saga",
|
|
27
|
+
"saga-pattern",
|
|
24
28
|
"reliability",
|
|
25
|
-
"llm"
|
|
29
|
+
"llm",
|
|
30
|
+
"openai",
|
|
31
|
+
"workflow",
|
|
32
|
+
"orchestration",
|
|
33
|
+
"rollback",
|
|
34
|
+
"compensation",
|
|
35
|
+
"typescript"
|
|
26
36
|
],
|
|
27
|
-
"author": "
|
|
37
|
+
"author": "GrafikUi <hello@grafikui.com>",
|
|
28
38
|
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/Grafikui/Transactional-ai.git"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/Grafikui/Transactional-ai/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/Grafikui/Transactional-ai#readme",
|
|
29
47
|
"devDependencies": {
|
|
30
48
|
"@testing-library/jest-dom": "^6.9.1",
|
|
31
49
|
"@testing-library/react": "^16.3.1",
|
|
@@ -41,9 +59,11 @@
|
|
|
41
59
|
},
|
|
42
60
|
"dependencies": {
|
|
43
61
|
"@prisma/client": "6.15.0",
|
|
62
|
+
"@types/pg": "^8.16.0",
|
|
44
63
|
"ioredis": "^5.9.1",
|
|
45
64
|
"node-fetch": "^3.3.2",
|
|
46
65
|
"openai": "^6.15.0",
|
|
66
|
+
"pg": "^8.16.3",
|
|
47
67
|
"prisma": "6.15.0"
|
|
48
68
|
}
|
|
49
69
|
}
|