@brandboostinggmbh/observable-workflows 0.19.0-beta.4 → 0.20.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/dist/index.d.ts +47 -1
- package/dist/index.js +162 -91
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -358,6 +358,48 @@ The library provides comprehensive error handling:
|
|
|
358
358
|
- **Workflow Failures**: Marked with appropriate status and error information
|
|
359
359
|
- **Retry Logic**: Failed workflows can be retried with step result reuse
|
|
360
360
|
- **Validation**: Input validation and type checking
|
|
361
|
+
- **Automatic D1 Retry**: Built-in retry logic with exponential backoff for transient D1 network errors
|
|
362
|
+
|
|
363
|
+
### D1 Network Error Retry
|
|
364
|
+
|
|
365
|
+
The library automatically retries D1 database operations that fail due to transient network errors (e.g., "D1_ERROR: Network connection lost"). This makes workflows more resilient to temporary network issues.
|
|
366
|
+
|
|
367
|
+
**Default Retry Configuration:**
|
|
368
|
+
- Maximum retry attempts: 3
|
|
369
|
+
- Initial delay: 100ms
|
|
370
|
+
- Maximum delay: 5000ms
|
|
371
|
+
- Exponential backoff multiplier: 2
|
|
372
|
+
- Jitter: Enabled
|
|
373
|
+
|
|
374
|
+
**Custom Retry Configuration:**
|
|
375
|
+
|
|
376
|
+
You can customize the retry behavior when creating the workflow context:
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
const workflowContext = createWorkflowContext({
|
|
380
|
+
D1: env.D1,
|
|
381
|
+
retryConfig: {
|
|
382
|
+
maxAttempts: 5, // Retry up to 5 times
|
|
383
|
+
initialDelayMs: 200, // Start with 200ms delay
|
|
384
|
+
maxDelayMs: 10000, // Cap delay at 10 seconds
|
|
385
|
+
backoffMultiplier: 3, // Triple the delay each time
|
|
386
|
+
useJitter: true, // Add randomization to prevent thundering herd
|
|
387
|
+
},
|
|
388
|
+
})
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Manual Retry for Custom Operations:**
|
|
392
|
+
|
|
393
|
+
You can also use the retry utility directly for custom D1 operations:
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
import { retryD1Operation } from '@brandboostinggmbh/observable-workflows'
|
|
397
|
+
|
|
398
|
+
const result = await retryD1Operation(
|
|
399
|
+
() => db.prepare("SELECT * FROM custom_table").first(),
|
|
400
|
+
{ maxAttempts: 3, initialDelayMs: 100 }
|
|
401
|
+
)
|
|
402
|
+
```
|
|
361
403
|
|
|
362
404
|
## Best Practices
|
|
363
405
|
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,38 @@ declare function createStepContext(context: StepContextOptions): Promise<{
|
|
|
18
18
|
declare function ensureTables(db: D1Database): Promise<void>;
|
|
19
19
|
//#endregion
|
|
20
20
|
//#region src/observableWorkflows/helperFunctions.d.ts
|
|
21
|
+
/**
|
|
22
|
+
* Configuration for retry behavior on D1 operations
|
|
23
|
+
*/
|
|
24
|
+
interface RetryConfig$1 {
|
|
25
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
26
|
+
maxAttempts?: number;
|
|
27
|
+
/** Initial delay in milliseconds (default: 100) */
|
|
28
|
+
initialDelayMs?: number;
|
|
29
|
+
/** Maximum delay in milliseconds (default: 5000) */
|
|
30
|
+
maxDelayMs?: number;
|
|
31
|
+
/** Multiplier for exponential backoff (default: 2) */
|
|
32
|
+
backoffMultiplier?: number;
|
|
33
|
+
/** Whether to add jitter to delays (default: true) */
|
|
34
|
+
useJitter?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Retry a D1 operation with exponential backoff for transient network errors
|
|
38
|
+
*
|
|
39
|
+
* @param operation - The async operation to retry
|
|
40
|
+
* @param config - Retry configuration options
|
|
41
|
+
* @returns The result of the operation
|
|
42
|
+
* @throws The last error if all retry attempts fail
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* const result = await retryD1Operation(
|
|
47
|
+
* () => db.prepare("SELECT * FROM table").first(),
|
|
48
|
+
* { maxAttempts: 5, initialDelayMs: 200 }
|
|
49
|
+
* )
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
declare function retryD1Operation<T>(operation: () => Promise<T>, config?: RetryConfig$1): Promise<T>;
|
|
21
53
|
declare function finalizeWorkflowRecord(options: InternalWorkflowContextOptions, {
|
|
22
54
|
workflowStatus,
|
|
23
55
|
endTime,
|
|
@@ -605,11 +637,25 @@ type ExternalBlobStorage = {
|
|
|
605
637
|
get: (id: string) => Promise<string>;
|
|
606
638
|
delete: (...keys: string[]) => Promise<number>;
|
|
607
639
|
};
|
|
640
|
+
type RetryConfig = {
|
|
641
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
642
|
+
maxAttempts?: number;
|
|
643
|
+
/** Initial delay in milliseconds (default: 100) */
|
|
644
|
+
initialDelayMs?: number;
|
|
645
|
+
/** Maximum delay in milliseconds (default: 5000) */
|
|
646
|
+
maxDelayMs?: number;
|
|
647
|
+
/** Multiplier for exponential backoff (default: 2) */
|
|
648
|
+
backoffMultiplier?: number;
|
|
649
|
+
/** Whether to add jitter to delays (default: true) */
|
|
650
|
+
useJitter?: boolean;
|
|
651
|
+
};
|
|
608
652
|
type WorkflowContextOptions = {
|
|
609
653
|
D1: D1Database;
|
|
610
654
|
idFactory?: () => string;
|
|
611
655
|
serializer?: Serializer;
|
|
612
656
|
externalBlobStorage?: ExternalBlobStorage;
|
|
657
|
+
/** Optional retry configuration for D1 operations (defaults: maxAttempts: 3, initialDelayMs: 100, maxDelayMs: 5000, backoffMultiplier: 2, useJitter: true) */
|
|
658
|
+
retryConfig?: RetryConfig;
|
|
613
659
|
};
|
|
614
660
|
type InternalWorkflowContextOptions = WorkflowContextOptions & Required<Pick<WorkflowContextOptions, 'serializer' | 'idFactory'>>;
|
|
615
661
|
type WorkflowContextInstance = {
|
|
@@ -906,4 +952,4 @@ type R2ExternalBlobStorageOptions = {
|
|
|
906
952
|
*/
|
|
907
953
|
declare function createR2ExternalBlobStorage(options: R2ExternalBlobStorageOptions): ExternalBlobStorage;
|
|
908
954
|
//#endregion
|
|
909
|
-
export { ConsoleWrapper, DateRangeFilter, ExternalBlobStorage, HandleWorkflowQueueMessageParams, InternalWorkflowContextOptions, Log, PossibleValueTypeNames, PossibleValueTypes, PropertyFilter, QueueWorkflowContextOptions, R2ExternalBlobStorageOptions, Serializer, Step, StepContextOptions, StepCtx, StepWorkflowStatus, StringFilter, ValueTypeMap, WorkflowContext, WorkflowContextInstance, WorkflowContextOptions, WorkflowEnqueueBatchItem, WorkflowFilter, WorkflowFunction, WorkflowProperty, WorkflowPropertyDefinition, WorkflowQueueMessage, WorkflowRun, WorkflowStatus, createCleanupManager, createLogAccessor, createQueueWorkflowContext, createR2ExternalBlobStorage, createStepContext, createWorkflowContext, createWorkflowDependencies, createWorkflowDependency, defaultIdFactory, defaultSerializer, defineWorkflow, deleteWorkflowDependency, deserializeWithExternalStorage, ensureTables, finalizeWorkflowRecord, insertStepRecordFull, insertWorkflowRecord, pushLogToDB, serializeWithExternalStorage, tryDeserializeObj, updateWorkflow, updateWorkflowName, upsertWorkflowProperty, workflowTableRowToWorkflowRun };
|
|
955
|
+
export { ConsoleWrapper, DateRangeFilter, ExternalBlobStorage, HandleWorkflowQueueMessageParams, InternalWorkflowContextOptions, Log, PossibleValueTypeNames, PossibleValueTypes, PropertyFilter, QueueWorkflowContextOptions, R2ExternalBlobStorageOptions, RetryConfig, Serializer, Step, StepContextOptions, StepCtx, StepWorkflowStatus, StringFilter, ValueTypeMap, WorkflowContext, WorkflowContextInstance, WorkflowContextOptions, WorkflowEnqueueBatchItem, WorkflowFilter, WorkflowFunction, WorkflowProperty, WorkflowPropertyDefinition, WorkflowQueueMessage, WorkflowRun, WorkflowStatus, createCleanupManager, createLogAccessor, createQueueWorkflowContext, createR2ExternalBlobStorage, createStepContext, createWorkflowContext, createWorkflowDependencies, createWorkflowDependency, defaultIdFactory, defaultSerializer, defineWorkflow, deleteWorkflowDependency, deserializeWithExternalStorage, ensureTables, finalizeWorkflowRecord, insertStepRecordFull, insertWorkflowRecord, pushLogToDB, retryD1Operation, serializeWithExternalStorage, tryDeserializeObj, updateWorkflow, updateWorkflowName, upsertWorkflowProperty, workflowTableRowToWorkflowRun };
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Detect the current schema version for each table
|
|
4
4
|
*/
|
|
5
5
|
async function detectSchemaVersion(db) {
|
|
6
|
-
const workflowTableInfo = await db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowTable'`).first();
|
|
6
|
+
const workflowTableInfo = await retryD1Operation(() => db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowTable'`).first());
|
|
7
7
|
let workflowTable = "missing";
|
|
8
8
|
if (workflowTableInfo) {
|
|
9
9
|
const hasInputRef = workflowTableInfo.sql.includes("inputRef");
|
|
@@ -16,7 +16,7 @@ async function detectSchemaVersion(db) {
|
|
|
16
16
|
else if (!hasInputRef && inputHasNotNull) workflowTable = "v1";
|
|
17
17
|
else workflowTable = "v1";
|
|
18
18
|
}
|
|
19
|
-
const stepTableInfo = await db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='StepTable'`).first();
|
|
19
|
+
const stepTableInfo = await retryD1Operation(() => db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='StepTable'`).first());
|
|
20
20
|
let stepTable = "missing";
|
|
21
21
|
if (stepTableInfo) {
|
|
22
22
|
const hasResultRef = stepTableInfo.sql.includes("resultRef");
|
|
@@ -26,7 +26,7 @@ async function detectSchemaVersion(db) {
|
|
|
26
26
|
else if (hasResultRef && hasErrorRef) stepTable = "v2";
|
|
27
27
|
else stepTable = "v1";
|
|
28
28
|
}
|
|
29
|
-
const logTableInfo = await db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='LogTable'`).first();
|
|
29
|
+
const logTableInfo = await retryD1Operation(() => db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='LogTable'`).first());
|
|
30
30
|
let logTable = "missing";
|
|
31
31
|
if (logTableInfo) {
|
|
32
32
|
const hasStepTableCascadeFK = logTableInfo.sql.includes("FOREIGN KEY (instanceId, stepName) REFERENCES StepTable(instanceId, stepName) ON DELETE CASCADE");
|
|
@@ -34,7 +34,7 @@ async function detectSchemaVersion(db) {
|
|
|
34
34
|
if (hasStepTableCascadeFK && hasWorkflowTableFK) logTable = "v5";
|
|
35
35
|
else logTable = "v1";
|
|
36
36
|
}
|
|
37
|
-
const workflowPropertiesInfo = await db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowProperties'`).first();
|
|
37
|
+
const workflowPropertiesInfo = await retryD1Operation(() => db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowProperties'`).first());
|
|
38
38
|
let workflowProperties = "missing";
|
|
39
39
|
if (workflowPropertiesInfo) workflowProperties = "v1";
|
|
40
40
|
const workflowDependenciesInfo = await db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowDependencies'`).first();
|
|
@@ -53,10 +53,10 @@ async function detectSchemaVersion(db) {
|
|
|
53
53
|
* Adds inputRef column and makes input nullable
|
|
54
54
|
*/
|
|
55
55
|
async function migrateWorkflowTableV1ToV2(db) {
|
|
56
|
-
const workflowTableInfo = await db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowTable'`).first();
|
|
56
|
+
const workflowTableInfo = await retryD1Operation(() => db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowTable'`).first());
|
|
57
57
|
const hasInputRef = workflowTableInfo.sql.includes("inputRef");
|
|
58
58
|
const inputHasNotNull = workflowTableInfo.sql.includes("input TEXT NOT NULL");
|
|
59
|
-
if (!hasInputRef || inputHasNotNull) await db.batch([
|
|
59
|
+
if (!hasInputRef || inputHasNotNull) await retryD1Operation(() => db.batch([
|
|
60
60
|
db.prepare(`CREATE TABLE WorkflowTable_new (
|
|
61
61
|
instanceId TEXT NOT NULL,
|
|
62
62
|
workflowType TEXT NOT NULL,
|
|
@@ -78,16 +78,16 @@ async function migrateWorkflowTableV1ToV2(db) {
|
|
|
78
78
|
FROM WorkflowTable`),
|
|
79
79
|
db.prepare(`DROP TABLE WorkflowTable`),
|
|
80
80
|
db.prepare(`ALTER TABLE WorkflowTable_new RENAME TO WorkflowTable`)
|
|
81
|
-
]);
|
|
81
|
+
]));
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
84
|
* Migrate WorkflowTable from V2/V3 to V4 schema
|
|
85
85
|
* Adds triggerId column with UNIQUE constraint
|
|
86
86
|
*/
|
|
87
87
|
async function migrateWorkflowTableV2V3ToV4(db) {
|
|
88
|
-
const workflowTableInfo = await db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowTable'`).first();
|
|
88
|
+
const workflowTableInfo = await retryD1Operation(() => db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowTable'`).first());
|
|
89
89
|
const hasTriggerId = workflowTableInfo.sql.includes("triggerId");
|
|
90
|
-
if (!hasTriggerId) await db.batch([
|
|
90
|
+
if (!hasTriggerId) await retryD1Operation(() => db.batch([
|
|
91
91
|
db.prepare(`CREATE TABLE WorkflowTable_new (
|
|
92
92
|
instanceId TEXT NOT NULL,
|
|
93
93
|
workflowType TEXT NOT NULL,
|
|
@@ -111,7 +111,7 @@ async function migrateWorkflowTableV2V3ToV4(db) {
|
|
|
111
111
|
FROM WorkflowTable`),
|
|
112
112
|
db.prepare(`DROP TABLE WorkflowTable`),
|
|
113
113
|
db.prepare(`ALTER TABLE WorkflowTable_new RENAME TO WorkflowTable`)
|
|
114
|
-
]);
|
|
114
|
+
]));
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
117
|
* Migrate WorkflowTable from V4 to V6 schema
|
|
@@ -127,7 +127,7 @@ async function migrateWorkflowTableV4ToV6(db) {
|
|
|
127
127
|
*/
|
|
128
128
|
async function migrateWorkflowTable(db, currentVersion) {
|
|
129
129
|
if (currentVersion === "missing") {
|
|
130
|
-
await db.prepare(`CREATE TABLE WorkflowTable (
|
|
130
|
+
await retryD1Operation(() => db.prepare(`CREATE TABLE WorkflowTable (
|
|
131
131
|
instanceId TEXT NOT NULL,
|
|
132
132
|
workflowType TEXT NOT NULL,
|
|
133
133
|
workflowName TEXT NOT NULL,
|
|
@@ -144,7 +144,7 @@ async function migrateWorkflowTable(db, currentVersion) {
|
|
|
144
144
|
triggerId TEXT,
|
|
145
145
|
PRIMARY KEY (instanceId),
|
|
146
146
|
UNIQUE (triggerId)
|
|
147
|
-
)`).run();
|
|
147
|
+
)`).run());
|
|
148
148
|
return;
|
|
149
149
|
}
|
|
150
150
|
if (currentVersion === "v1") {
|
|
@@ -168,7 +168,7 @@ async function migrateWorkflowTable(db, currentVersion) {
|
|
|
168
168
|
*/
|
|
169
169
|
async function migrateStepTable(db, currentVersion) {
|
|
170
170
|
if (currentVersion === "missing") {
|
|
171
|
-
await db.prepare(`CREATE TABLE StepTable (
|
|
171
|
+
await retryD1Operation(() => db.prepare(`CREATE TABLE StepTable (
|
|
172
172
|
instanceId TEXT NOT NULL,
|
|
173
173
|
stepName TEXT NOT NULL,
|
|
174
174
|
stepStatus TEXT NOT NULL,
|
|
@@ -182,15 +182,15 @@ async function migrateStepTable(db, currentVersion) {
|
|
|
182
182
|
tenantId TEXT NOT NULL,
|
|
183
183
|
PRIMARY KEY (instanceId, stepName),
|
|
184
184
|
FOREIGN KEY (instanceId) REFERENCES WorkflowTable(instanceId) ON DELETE CASCADE
|
|
185
|
-
)`).run();
|
|
185
|
+
)`).run());
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
188
|
if (currentVersion === "v1") {
|
|
189
|
-
const stepTableInfo = await db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='StepTable'`).first();
|
|
189
|
+
const stepTableInfo = await retryD1Operation(() => db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='StepTable'`).first());
|
|
190
190
|
const hasResultRef = stepTableInfo.sql.includes("resultRef");
|
|
191
191
|
const hasErrorRef = stepTableInfo.sql.includes("errorRef");
|
|
192
|
-
if (!hasResultRef) await db.prepare(`ALTER TABLE StepTable ADD COLUMN resultRef TEXT`).run();
|
|
193
|
-
if (!hasErrorRef) await db.prepare(`ALTER TABLE StepTable ADD COLUMN errorRef TEXT`).run();
|
|
192
|
+
if (!hasResultRef) await retryD1Operation(() => db.prepare(`ALTER TABLE StepTable ADD COLUMN resultRef TEXT`).run());
|
|
193
|
+
if (!hasErrorRef) await retryD1Operation(() => db.prepare(`ALTER TABLE StepTable ADD COLUMN errorRef TEXT`).run());
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
/**
|
|
@@ -198,7 +198,7 @@ async function migrateStepTable(db, currentVersion) {
|
|
|
198
198
|
*/
|
|
199
199
|
async function migrateLogTable(db, currentVersion) {
|
|
200
200
|
if (currentVersion === "missing") {
|
|
201
|
-
await db.prepare(`CREATE TABLE LogTable (
|
|
201
|
+
await retryD1Operation(() => db.prepare(`CREATE TABLE LogTable (
|
|
202
202
|
instanceId TEXT NOT NULL,
|
|
203
203
|
stepName TEXT,
|
|
204
204
|
log TEXT NOT NULL,
|
|
@@ -208,7 +208,7 @@ async function migrateLogTable(db, currentVersion) {
|
|
|
208
208
|
tenantId TEXT NOT NULL,
|
|
209
209
|
FOREIGN KEY (instanceId, stepName) REFERENCES StepTable(instanceId, stepName) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
|
210
210
|
FOREIGN KEY (instanceId) REFERENCES WorkflowTable(instanceId) ON DELETE CASCADE
|
|
211
|
-
)`).run();
|
|
211
|
+
)`).run());
|
|
212
212
|
return;
|
|
213
213
|
}
|
|
214
214
|
}
|
|
@@ -217,7 +217,7 @@ async function migrateLogTable(db, currentVersion) {
|
|
|
217
217
|
*/
|
|
218
218
|
async function migrateWorkflowPropertiesTable(db, currentVersion) {
|
|
219
219
|
if (currentVersion === "missing") {
|
|
220
|
-
await db.prepare(`CREATE TABLE WorkflowProperties (
|
|
220
|
+
await retryD1Operation(() => db.prepare(`CREATE TABLE WorkflowProperties (
|
|
221
221
|
instanceId TEXT NOT NULL,
|
|
222
222
|
key TEXT NOT NULL,
|
|
223
223
|
value TEXT NOT NULL,
|
|
@@ -226,7 +226,7 @@ async function migrateWorkflowPropertiesTable(db, currentVersion) {
|
|
|
226
226
|
PRIMARY KEY (instanceId, key),
|
|
227
227
|
FOREIGN KEY (instanceId) REFERENCES WorkflowTable(instanceId)
|
|
228
228
|
ON DELETE CASCADE
|
|
229
|
-
)`).run();
|
|
229
|
+
)`).run());
|
|
230
230
|
return;
|
|
231
231
|
}
|
|
232
232
|
}
|
|
@@ -251,7 +251,7 @@ async function migrateWorkflowDependenciesTable(db, currentVersion) {
|
|
|
251
251
|
* Create necessary indexes
|
|
252
252
|
*/
|
|
253
253
|
async function createIndexes(db) {
|
|
254
|
-
const existingIndexes = await db.prepare(`SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_%'`).all();
|
|
254
|
+
const existingIndexes = await retryD1Operation(() => db.prepare(`SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_%'`).all());
|
|
255
255
|
const existingIndexNames = new Set(existingIndexes.results?.map((row) => row.name) || []);
|
|
256
256
|
const indexes = [
|
|
257
257
|
{
|
|
@@ -341,7 +341,7 @@ async function createIndexes(db) {
|
|
|
341
341
|
];
|
|
342
342
|
const preparedStatements = [];
|
|
343
343
|
for (const index of indexes) if (!existingIndexNames.has(index.name)) preparedStatements.push(db.prepare(index.sql));
|
|
344
|
-
if (preparedStatements.length > 0) await db.batch(preparedStatements);
|
|
344
|
+
if (preparedStatements.length > 0) await retryD1Operation(() => db.batch(preparedStatements));
|
|
345
345
|
}
|
|
346
346
|
/**
|
|
347
347
|
* Main migration function that ensures all tables exist and are up-to-date.
|
|
@@ -350,7 +350,7 @@ async function createIndexes(db) {
|
|
|
350
350
|
* @param db - D1Database instance
|
|
351
351
|
*/
|
|
352
352
|
async function ensureTables(db) {
|
|
353
|
-
await db.prepare(`PRAGMA foreign_keys = ON`).run();
|
|
353
|
+
await retryD1Operation(() => db.prepare(`PRAGMA foreign_keys = ON`).run());
|
|
354
354
|
const schemaVersion = await detectSchemaVersion(db);
|
|
355
355
|
await migrateWorkflowTable(db, schemaVersion.workflowTable);
|
|
356
356
|
await migrateStepTable(db, schemaVersion.stepTable);
|
|
@@ -362,16 +362,85 @@ async function ensureTables(db) {
|
|
|
362
362
|
|
|
363
363
|
//#endregion
|
|
364
364
|
//#region src/observableWorkflows/helperFunctions.ts
|
|
365
|
+
/**
|
|
366
|
+
* Default retry configuration for D1 operations
|
|
367
|
+
*/
|
|
368
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
369
|
+
maxAttempts: 3,
|
|
370
|
+
initialDelayMs: 100,
|
|
371
|
+
maxDelayMs: 5e3,
|
|
372
|
+
backoffMultiplier: 2,
|
|
373
|
+
useJitter: true
|
|
374
|
+
};
|
|
375
|
+
/**
|
|
376
|
+
* Check if an error is a transient D1 network error that should be retried
|
|
377
|
+
*/
|
|
378
|
+
function isRetriableD1Error(error) {
|
|
379
|
+
if (!error) return false;
|
|
380
|
+
const errorMessage = error.message || String(error);
|
|
381
|
+
const errorString = errorMessage.toLowerCase();
|
|
382
|
+
return errorString.includes("network connection lost") || errorString.includes("d1_error") || errorString.includes("connection reset") || errorString.includes("econnreset") || errorString.includes("timeout") || errorString.includes("etimedout");
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Calculate delay with exponential backoff and optional jitter
|
|
386
|
+
*/
|
|
387
|
+
function calculateDelay(attempt, config) {
|
|
388
|
+
const exponentialDelay = Math.min(config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt), config.maxDelayMs);
|
|
389
|
+
if (config.useJitter) return Math.floor(Math.random() * exponentialDelay);
|
|
390
|
+
return exponentialDelay;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Sleep for the specified number of milliseconds
|
|
394
|
+
*/
|
|
395
|
+
function sleep(ms) {
|
|
396
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Retry a D1 operation with exponential backoff for transient network errors
|
|
400
|
+
*
|
|
401
|
+
* @param operation - The async operation to retry
|
|
402
|
+
* @param config - Retry configuration options
|
|
403
|
+
* @returns The result of the operation
|
|
404
|
+
* @throws The last error if all retry attempts fail
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* ```typescript
|
|
408
|
+
* const result = await retryD1Operation(
|
|
409
|
+
* () => db.prepare("SELECT * FROM table").first(),
|
|
410
|
+
* { maxAttempts: 5, initialDelayMs: 200 }
|
|
411
|
+
* )
|
|
412
|
+
* ```
|
|
413
|
+
*/
|
|
414
|
+
async function retryD1Operation(operation, config = {}) {
|
|
415
|
+
const finalConfig = {
|
|
416
|
+
...DEFAULT_RETRY_CONFIG,
|
|
417
|
+
...config
|
|
418
|
+
};
|
|
419
|
+
let lastError;
|
|
420
|
+
for (let attempt = 0; attempt < finalConfig.maxAttempts; attempt++) try {
|
|
421
|
+
return await operation();
|
|
422
|
+
} catch (error) {
|
|
423
|
+
lastError = error;
|
|
424
|
+
if (!isRetriableD1Error(error)) throw error;
|
|
425
|
+
if (attempt < finalConfig.maxAttempts - 1) {
|
|
426
|
+
const delay = calculateDelay(attempt, finalConfig);
|
|
427
|
+
console.warn(`D1 operation failed with retriable error (attempt ${attempt + 1}/${finalConfig.maxAttempts}), retrying in ${delay}ms:`, error instanceof Error ? error.message : String(error));
|
|
428
|
+
await sleep(delay);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
console.error(`D1 operation failed after ${finalConfig.maxAttempts} attempts:`, lastError instanceof Error ? lastError.message : String(lastError));
|
|
432
|
+
throw lastError;
|
|
433
|
+
}
|
|
365
434
|
async function finalizeWorkflowRecord(options, { workflowStatus, endTime, instanceId, result }) {
|
|
366
435
|
if (result !== void 0) {
|
|
367
436
|
const { data: resultData, externalRef: resultRef } = await serializeWithExternalStorage(result, options.serializer, options.externalBlobStorage);
|
|
368
|
-
return options.D1.prepare(`UPDATE WorkflowTable
|
|
437
|
+
return retryD1Operation(() => options.D1.prepare(`UPDATE WorkflowTable
|
|
369
438
|
SET workflowStatus = ?, endTime = ?, result = ?, resultRef = ?
|
|
370
|
-
WHERE instanceId = ?`).bind(workflowStatus, endTime, resultData, resultRef, instanceId).run();
|
|
439
|
+
WHERE instanceId = ?`).bind(workflowStatus, endTime, resultData, resultRef, instanceId).run());
|
|
371
440
|
}
|
|
372
|
-
return options.D1.prepare(`UPDATE WorkflowTable
|
|
441
|
+
return retryD1Operation(() => options.D1.prepare(`UPDATE WorkflowTable
|
|
373
442
|
SET workflowStatus = ?, endTime = ?
|
|
374
|
-
WHERE instanceId = ?`).bind(workflowStatus, endTime, instanceId).run();
|
|
443
|
+
WHERE instanceId = ?`).bind(workflowStatus, endTime, instanceId).run());
|
|
375
444
|
}
|
|
376
445
|
/**
|
|
377
446
|
* Insert a new workflow record into the database.
|
|
@@ -402,7 +471,7 @@ async function insertWorkflowRecord(options, { instanceId, workflowType, workflo
|
|
|
402
471
|
(instanceId, workflowType, workflowName, workflowMetadata, input, inputRef, tenantId, workflowStatus, startTime, endTime, parentInstanceId, triggerId)
|
|
403
472
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, workflowType, workflowName, options.serializer.serialize(workflowMetadata), inputData, inputRef, tenantId, workflowStatus, startTime, endTime ?? null, parentInstanceId ?? null, triggerId ?? null);
|
|
404
473
|
if (!dependencies || dependencies.length === 0) {
|
|
405
|
-
const result = await insertWorkflowStatement.run();
|
|
474
|
+
const result = await retryD1Operation(() => insertWorkflowStatement.run());
|
|
406
475
|
return {
|
|
407
476
|
success: result.success,
|
|
408
477
|
meta: result.meta
|
|
@@ -416,7 +485,7 @@ async function insertWorkflowRecord(options, { instanceId, workflowType, workflo
|
|
|
416
485
|
tenantId,
|
|
417
486
|
createdAt
|
|
418
487
|
}));
|
|
419
|
-
const results = await options.D1.batch([insertWorkflowStatement, ...dependencyStatements]);
|
|
488
|
+
const results = await retryD1Operation(() => options.D1.batch([insertWorkflowStatement, ...dependencyStatements]));
|
|
420
489
|
const allSucceeded = results.every((result) => result.success);
|
|
421
490
|
return {
|
|
422
491
|
success: allSucceeded,
|
|
@@ -424,12 +493,12 @@ async function insertWorkflowRecord(options, { instanceId, workflowType, workflo
|
|
|
424
493
|
};
|
|
425
494
|
}
|
|
426
495
|
function insertStepRecordFull(context, { instanceId, name, status, metadata, startTime, endTime, result, error, resultRef, errorRef }) {
|
|
427
|
-
return context.D1.prepare(`INSERT INTO StepTable
|
|
496
|
+
return retryD1Operation(() => context.D1.prepare(`INSERT INTO StepTable
|
|
428
497
|
(instanceId, stepName, stepStatus, stepMetadata, startTime, endTime, result, error, resultRef, errorRef, tenantId)
|
|
429
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, name, status, metadata, startTime, endTime, result, error, resultRef ?? null, errorRef ?? null, context.tenantId).run();
|
|
498
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, name, status, metadata, startTime, endTime, result, error, resultRef ?? null, errorRef ?? null, context.tenantId).run());
|
|
430
499
|
}
|
|
431
500
|
async function getStepRecord(context, stepName, instanceId) {
|
|
432
|
-
const existingStep = await context.D1.prepare(`SELECT * FROM StepTable WHERE instanceId = ? AND stepName = ? AND tenantId = ?`).bind(instanceId, stepName, context.tenantId).first();
|
|
501
|
+
const existingStep = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM StepTable WHERE instanceId = ? AND stepName = ? AND tenantId = ?`).bind(instanceId, stepName, context.tenantId).first());
|
|
433
502
|
const row = existingStep;
|
|
434
503
|
if (row) {
|
|
435
504
|
const result = await deserializeWithExternalStorage(row.result, row.resultRef, context.serializer, context.externalBlobStorage);
|
|
@@ -503,9 +572,9 @@ function truncateLogMessage(message) {
|
|
|
503
572
|
}
|
|
504
573
|
function pushLogToDB(options, { instanceId, stepName, message, timestamp, type, logOrder, tenantId }) {
|
|
505
574
|
const truncatedMessage = truncateLogMessage(message);
|
|
506
|
-
return options.D1.prepare(`INSERT INTO LogTable
|
|
575
|
+
return retryD1Operation(() => options.D1.prepare(`INSERT INTO LogTable
|
|
507
576
|
(instanceId, stepName, log, timestamp, type, logOrder, tenantId)
|
|
508
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, stepName, truncatedMessage, timestamp, type, logOrder, tenantId).run();
|
|
577
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, stepName, truncatedMessage, timestamp, type, logOrder, tenantId).run());
|
|
509
578
|
}
|
|
510
579
|
var LogBatcher = class {
|
|
511
580
|
batch = [];
|
|
@@ -540,10 +609,12 @@ var LogBatcher = class {
|
|
|
540
609
|
if (this.batch.length === 0) return;
|
|
541
610
|
const logsToFlush = this.batch.splice(0);
|
|
542
611
|
try {
|
|
543
|
-
|
|
612
|
+
await retryD1Operation(async () => {
|
|
613
|
+
const statements = logsToFlush.map((entry) => this.options.D1.prepare(`INSERT INTO LogTable
|
|
544
614
|
(instanceId, stepName, log, timestamp, type, logOrder, tenantId)
|
|
545
615
|
VALUES (?, ?, ?, ?, ?, ?, ?)`).bind(entry.instanceId, entry.stepName, entry.message, entry.timestamp, entry.type, entry.logOrder, entry.tenantId));
|
|
546
|
-
|
|
616
|
+
return await this.options.D1.batch(statements);
|
|
617
|
+
});
|
|
547
618
|
} catch (error) {
|
|
548
619
|
console.error("Batch log insert failed, falling back to individual inserts:", error);
|
|
549
620
|
await Promise.all(logsToFlush.map((entry) => pushLogToDB(this.options, entry)));
|
|
@@ -671,9 +742,9 @@ async function workflowTableRowToWorkflowRun({ row, serializer, externalBlobStor
|
|
|
671
742
|
};
|
|
672
743
|
}
|
|
673
744
|
async function updateWorkflowName(context, instanceId, newWorkflowName) {
|
|
674
|
-
return await context.D1.prepare(`UPDATE WorkflowTable
|
|
745
|
+
return await retryD1Operation(() => context.D1.prepare(`UPDATE WorkflowTable
|
|
675
746
|
SET workflowName = ?
|
|
676
|
-
WHERE instanceId = ?`).bind(newWorkflowName, instanceId).run();
|
|
747
|
+
WHERE instanceId = ?`).bind(newWorkflowName, instanceId).run());
|
|
677
748
|
}
|
|
678
749
|
/**
|
|
679
750
|
* Update workflow fields by instanceId. Only provided fields will be updated.
|
|
@@ -750,7 +821,7 @@ async function updateWorkflow(context, instanceId, updates) {
|
|
|
750
821
|
};
|
|
751
822
|
bindings.push(instanceId);
|
|
752
823
|
const sql = `UPDATE WorkflowTable SET ${setClauses.join(", ")} WHERE instanceId = ?`;
|
|
753
|
-
return await context.D1.prepare(sql).bind(...bindings).run();
|
|
824
|
+
return await retryD1Operation(() => context.D1.prepare(sql).bind(...bindings).run());
|
|
754
825
|
}
|
|
755
826
|
/**
|
|
756
827
|
* Serialize a workflow property value for storage
|
|
@@ -783,9 +854,9 @@ function deserializeWorkflowPropertyValue(serializedValue, valueType) {
|
|
|
783
854
|
}
|
|
784
855
|
async function upsertWorkflowProperty({ context, instanceId, key, value, tenantId }) {
|
|
785
856
|
const { serializedValue, valueType } = serializeWorkflowPropertyValue(value);
|
|
786
|
-
const res = await context.D1.prepare(`INSERT OR REPLACE INTO WorkflowProperties
|
|
857
|
+
const res = await retryD1Operation(() => context.D1.prepare(`INSERT OR REPLACE INTO WorkflowProperties
|
|
787
858
|
(instanceId, key, value, valueType, tenantId)
|
|
788
|
-
VALUES (?, ?, ?, ?, ?)`).bind(instanceId, key, serializedValue, valueType, tenantId).run();
|
|
859
|
+
VALUES (?, ?, ?, ?, ?)`).bind(instanceId, key, serializedValue, valueType, tenantId).run());
|
|
789
860
|
if (res.error) {
|
|
790
861
|
console.error("Error inserting workflow property:", res.error);
|
|
791
862
|
return false;
|
|
@@ -841,12 +912,12 @@ function prepareWorkflowDependencyStatement({ D1, dependencyWorkflowId, dependen
|
|
|
841
912
|
* ```
|
|
842
913
|
*/
|
|
843
914
|
async function createWorkflowDependency({ context, dependencyWorkflowId, dependentWorkflowId, tenantId }) {
|
|
844
|
-
return await prepareWorkflowDependencyStatement({
|
|
915
|
+
return await retryD1Operation(() => prepareWorkflowDependencyStatement({
|
|
845
916
|
D1: context.D1,
|
|
846
917
|
dependencyWorkflowId,
|
|
847
918
|
dependentWorkflowId,
|
|
848
919
|
tenantId
|
|
849
|
-
}).run();
|
|
920
|
+
}).run());
|
|
850
921
|
}
|
|
851
922
|
/**
|
|
852
923
|
* Create multiple workflow dependencies in a single transaction.
|
|
@@ -879,7 +950,7 @@ async function createWorkflowDependencies({ context, dependencies, tenantId }) {
|
|
|
879
950
|
tenantId,
|
|
880
951
|
createdAt
|
|
881
952
|
}));
|
|
882
|
-
return await context.D1.batch(statements);
|
|
953
|
+
return await retryD1Operation(() => context.D1.batch(statements));
|
|
883
954
|
}
|
|
884
955
|
/**
|
|
885
956
|
* Delete a specific workflow dependency relationship.
|
|
@@ -896,8 +967,8 @@ async function createWorkflowDependencies({ context, dependencies, tenantId }) {
|
|
|
896
967
|
* ```
|
|
897
968
|
*/
|
|
898
969
|
async function deleteWorkflowDependency({ context, dependencyWorkflowId, dependentWorkflowId, tenantId }) {
|
|
899
|
-
return await context.D1.prepare(`DELETE FROM WorkflowDependencies
|
|
900
|
-
WHERE dependencyWorkflowId = ? AND dependentWorkflowId = ? AND tenantId = ?`).bind(dependencyWorkflowId, dependentWorkflowId, tenantId).run();
|
|
970
|
+
return await retryD1Operation(() => context.D1.prepare(`DELETE FROM WorkflowDependencies
|
|
971
|
+
WHERE dependencyWorkflowId = ? AND dependentWorkflowId = ? AND tenantId = ?`).bind(dependencyWorkflowId, dependentWorkflowId, tenantId).run());
|
|
901
972
|
}
|
|
902
973
|
|
|
903
974
|
//#endregion
|
|
@@ -917,7 +988,7 @@ const createCleanupManager = (context) => {
|
|
|
917
988
|
const getAffectedWorkflows = async (config, limit) => {
|
|
918
989
|
const { statuses, statusPlaceholders } = buildStatusesAndPlaceholders(config);
|
|
919
990
|
if (statuses.length === 0) return [];
|
|
920
|
-
const result = await context.D1.prepare(`
|
|
991
|
+
const result = await retryD1Operation(() => context.D1.prepare(`
|
|
921
992
|
SELECT instanceId, inputRef
|
|
922
993
|
FROM WorkflowTable
|
|
923
994
|
WHERE tenantId = ?
|
|
@@ -925,13 +996,13 @@ const createCleanupManager = (context) => {
|
|
|
925
996
|
AND workflowStatus IN (${statusPlaceholders})
|
|
926
997
|
ORDER BY instanceId
|
|
927
998
|
LIMIT ?
|
|
928
|
-
`).bind(context.tenantId, config.deleteWorkflowsOlderThanDays, ...statuses, limit).all();
|
|
999
|
+
`).bind(context.tenantId, config.deleteWorkflowsOlderThanDays, ...statuses, limit).all());
|
|
929
1000
|
return result.results;
|
|
930
1001
|
};
|
|
931
1002
|
const getAffectedSteps = async (config, limit) => {
|
|
932
1003
|
const { statuses, statusPlaceholders } = buildStatusesAndPlaceholders(config);
|
|
933
1004
|
if (statuses.length === 0) return [];
|
|
934
|
-
const result = await context.D1.prepare(`
|
|
1005
|
+
const result = await retryD1Operation(() => context.D1.prepare(`
|
|
935
1006
|
SELECT s.instanceId, s.stepName, s.stepStatus, s.errorRef, s.resultRef
|
|
936
1007
|
FROM StepTable s
|
|
937
1008
|
WHERE s.instanceId IN (
|
|
@@ -944,7 +1015,7 @@ const createCleanupManager = (context) => {
|
|
|
944
1015
|
LIMIT ?
|
|
945
1016
|
)
|
|
946
1017
|
ORDER BY s.instanceId, s.stepName
|
|
947
|
-
`).bind(context.tenantId, config.deleteWorkflowsOlderThanDays, ...statuses, limit).all();
|
|
1018
|
+
`).bind(context.tenantId, config.deleteWorkflowsOlderThanDays, ...statuses, limit).all());
|
|
948
1019
|
return result.results;
|
|
949
1020
|
};
|
|
950
1021
|
const collectExternalStorageKeys = (workflows, steps) => {
|
|
@@ -997,15 +1068,15 @@ const createCleanupManager = (context) => {
|
|
|
997
1068
|
}
|
|
998
1069
|
if (deletedWorkflows.length > 0) {
|
|
999
1070
|
console.log(`Proceeding to delete workflows from the database...`);
|
|
1000
|
-
await context.D1.prepare(`PRAGMA foreign_keys = ON`).run();
|
|
1071
|
+
await retryD1Operation(() => context.D1.prepare(`PRAGMA foreign_keys = ON`).run());
|
|
1001
1072
|
let totalDeletedCount = 0;
|
|
1002
1073
|
for (let i = 0; i < deletedWorkflows.length; i += 100) {
|
|
1003
1074
|
const batch = deletedWorkflows.slice(i, i + 100);
|
|
1004
1075
|
const workflowIds = batch.map(() => "?").join(", ");
|
|
1005
|
-
const result = await context.D1.prepare(`
|
|
1076
|
+
const result = await retryD1Operation(() => context.D1.prepare(`
|
|
1006
1077
|
DELETE FROM WorkflowTable
|
|
1007
1078
|
WHERE instanceId IN (${workflowIds})
|
|
1008
|
-
`).bind(...batch.map((w) => w.instanceId)).run();
|
|
1079
|
+
`).bind(...batch.map((w) => w.instanceId)).run());
|
|
1009
1080
|
totalDeletedCount += result.meta.changes || 0;
|
|
1010
1081
|
}
|
|
1011
1082
|
return {
|
|
@@ -1021,8 +1092,8 @@ const createCleanupManager = (context) => {
|
|
|
1021
1092
|
};
|
|
1022
1093
|
};
|
|
1023
1094
|
const cleanupOrphanedSteps = async (config, limit) => {
|
|
1024
|
-
await context.D1.prepare(`PRAGMA foreign_keys = ON`).run();
|
|
1025
|
-
const orphanedSteps = await context.D1.prepare(`
|
|
1095
|
+
await retryD1Operation(() => context.D1.prepare(`PRAGMA foreign_keys = ON`).run());
|
|
1096
|
+
const orphanedSteps = await retryD1Operation(() => context.D1.prepare(`
|
|
1026
1097
|
SELECT s.instanceId, s.stepName, s.errorRef, s.resultRef
|
|
1027
1098
|
FROM StepTable s
|
|
1028
1099
|
WHERE s.tenantId = ?
|
|
@@ -1032,7 +1103,7 @@ const createCleanupManager = (context) => {
|
|
|
1032
1103
|
)
|
|
1033
1104
|
ORDER BY s.instanceId, s.stepName
|
|
1034
1105
|
LIMIT ?
|
|
1035
|
-
`).bind(context.tenantId, context.tenantId, limit).all();
|
|
1106
|
+
`).bind(context.tenantId, context.tenantId, limit).all());
|
|
1036
1107
|
const orphanedStepResults = orphanedSteps.results;
|
|
1037
1108
|
let deletedExternalStorageKeysCount = 0;
|
|
1038
1109
|
if (config.deleteRefsFromExternalStorage && context.externalBlobStorage && orphanedStepResults.length > 0) {
|
|
@@ -1048,11 +1119,11 @@ const createCleanupManager = (context) => {
|
|
|
1048
1119
|
for (let i = 0; i < orphanedStepResults.length; i += 99) {
|
|
1049
1120
|
const batch = orphanedStepResults.slice(i, i + 99);
|
|
1050
1121
|
const instanceIds = batch.map(() => "?").join(", ");
|
|
1051
|
-
const deletedStepsResult = await context.D1.prepare(`
|
|
1122
|
+
const deletedStepsResult = await retryD1Operation(() => context.D1.prepare(`
|
|
1052
1123
|
DELETE FROM StepTable
|
|
1053
1124
|
WHERE tenantId = ?
|
|
1054
1125
|
AND instanceId IN (${instanceIds})
|
|
1055
|
-
`).bind(context.tenantId, ...batch.map((s) => s.instanceId)).run();
|
|
1126
|
+
`).bind(context.tenantId, ...batch.map((s) => s.instanceId)).run());
|
|
1056
1127
|
totalDeletedSteps += deletedStepsResult.meta.changes || 0;
|
|
1057
1128
|
}
|
|
1058
1129
|
return {
|
|
@@ -1068,8 +1139,8 @@ const createCleanupManager = (context) => {
|
|
|
1068
1139
|
};
|
|
1069
1140
|
};
|
|
1070
1141
|
const cleanupOrphanedLogs = async (limit) => {
|
|
1071
|
-
await context.D1.prepare(`PRAGMA foreign_keys = ON`).run();
|
|
1072
|
-
const deletedLogsResult = await context.D1.prepare(`
|
|
1142
|
+
await retryD1Operation(() => context.D1.prepare(`PRAGMA foreign_keys = ON`).run());
|
|
1143
|
+
const deletedLogsResult = await retryD1Operation(() => context.D1.prepare(`
|
|
1073
1144
|
DELETE FROM LogTable
|
|
1074
1145
|
WHERE tenantId = ?
|
|
1075
1146
|
AND (
|
|
@@ -1084,7 +1155,7 @@ const createCleanupManager = (context) => {
|
|
|
1084
1155
|
))
|
|
1085
1156
|
)
|
|
1086
1157
|
LIMIT ?
|
|
1087
|
-
`).bind(context.tenantId, context.tenantId, context.tenantId, limit).run();
|
|
1158
|
+
`).bind(context.tenantId, context.tenantId, context.tenantId, limit).run());
|
|
1088
1159
|
return { deletedOrphanedLogs: deletedLogsResult.meta.changes || 0 };
|
|
1089
1160
|
};
|
|
1090
1161
|
return {
|
|
@@ -1272,7 +1343,7 @@ const createLogAccessor = (context) => {
|
|
|
1272
1343
|
const limitClause = limit !== void 0 && actualOffset !== void 0 ? "LIMIT ? OFFSET ?" : "";
|
|
1273
1344
|
const sql = `SELECT * FROM StepTable ${whereClause} ORDER BY startTime ASC ${limitClause}`;
|
|
1274
1345
|
if (limit !== void 0 && actualOffset !== void 0) bindings.push(limit, actualOffset);
|
|
1275
|
-
result = await context.D1.prepare(sql).bind(...bindings).all();
|
|
1346
|
+
result = await retryD1Operation(() => context.D1.prepare(sql).bind(...bindings).all());
|
|
1276
1347
|
if (result.results) {
|
|
1277
1348
|
const steps = await Promise.all(result.results.map(async (row) => {
|
|
1278
1349
|
let deserializedResult = null;
|
|
@@ -1299,7 +1370,7 @@ const createLogAccessor = (context) => {
|
|
|
1299
1370
|
const listWorkflows = async (limit, offset, filter, options) => {
|
|
1300
1371
|
const { sql, bindings } = buildFilteredWorkflowQuery(filter, { ignoreTenant: options?.ignoreTenant });
|
|
1301
1372
|
if (options?.debugLogs) console.log("listWorkflows SQL:", sql, "Bindings:", JSON.stringify(bindings));
|
|
1302
|
-
const result = await context.D1.prepare(sql).bind(...bindings, limit, offset).all();
|
|
1373
|
+
const result = await retryD1Operation(() => context.D1.prepare(sql).bind(...bindings, limit, offset).all());
|
|
1303
1374
|
if (options?.debugLogs) console.log("listWorkflows SQL Query executed");
|
|
1304
1375
|
if (result.results) {
|
|
1305
1376
|
const workflows = await Promise.all(result.results.map((row) => workflowTableRowToWorkflowRun({
|
|
@@ -1316,7 +1387,7 @@ const createLogAccessor = (context) => {
|
|
|
1316
1387
|
for (let i = 0; i < instanceIds.length; i += BATCH_SIZE) {
|
|
1317
1388
|
const batchIds = instanceIds.slice(i, i + BATCH_SIZE);
|
|
1318
1389
|
const placeholders = batchIds.map(() => "?").join(", ");
|
|
1319
|
-
const propertiesResult = await context.D1.prepare(`SELECT * FROM WorkflowProperties WHERE instanceId IN (${placeholders})`).bind(...batchIds).all();
|
|
1390
|
+
const propertiesResult = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowProperties WHERE instanceId IN (${placeholders})`).bind(...batchIds).all());
|
|
1320
1391
|
if (propertiesResult.results) for (const row of propertiesResult.results) {
|
|
1321
1392
|
const property = {
|
|
1322
1393
|
key: row.key,
|
|
@@ -1337,10 +1408,10 @@ const createLogAccessor = (context) => {
|
|
|
1337
1408
|
for (let i = 0; i < instanceIds.length; i += BATCH_SIZE) {
|
|
1338
1409
|
const batchIds = instanceIds.slice(i, i + BATCH_SIZE);
|
|
1339
1410
|
const placeholders = batchIds.map(() => "?").join(", ");
|
|
1340
|
-
const dependenciesResult = await context.D1.prepare(`SELECT w.*, wd.createdAt, wd.dependentWorkflowId
|
|
1411
|
+
const dependenciesResult = await retryD1Operation(() => context.D1.prepare(`SELECT w.*, wd.createdAt, wd.dependentWorkflowId
|
|
1341
1412
|
FROM WorkflowDependencies wd
|
|
1342
1413
|
JOIN WorkflowTable w ON wd.dependencyWorkflowId = w.instanceId
|
|
1343
|
-
WHERE wd.dependentWorkflowId IN (${placeholders})`).bind(...batchIds).all();
|
|
1414
|
+
WHERE wd.dependentWorkflowId IN (${placeholders})`).bind(...batchIds).all());
|
|
1344
1415
|
if (dependenciesResult.results) for (const row of dependenciesResult.results) {
|
|
1345
1416
|
const depWorkflow = await workflowTableRowToWorkflowRun({
|
|
1346
1417
|
row,
|
|
@@ -1367,10 +1438,10 @@ const createLogAccessor = (context) => {
|
|
|
1367
1438
|
for (let i = 0; i < instanceIds.length; i += BATCH_SIZE) {
|
|
1368
1439
|
const batchIds = instanceIds.slice(i, i + BATCH_SIZE);
|
|
1369
1440
|
const placeholders = batchIds.map(() => "?").join(", ");
|
|
1370
|
-
const dependentsResult = await context.D1.prepare(`SELECT w.*, wd.createdAt, wd.dependencyWorkflowId
|
|
1441
|
+
const dependentsResult = await retryD1Operation(() => context.D1.prepare(`SELECT w.*, wd.createdAt, wd.dependencyWorkflowId
|
|
1371
1442
|
FROM WorkflowDependencies wd
|
|
1372
1443
|
JOIN WorkflowTable w ON wd.dependentWorkflowId = w.instanceId
|
|
1373
|
-
WHERE wd.dependencyWorkflowId IN (${placeholders})`).bind(...batchIds).all();
|
|
1444
|
+
WHERE wd.dependencyWorkflowId IN (${placeholders})`).bind(...batchIds).all());
|
|
1374
1445
|
if (dependentsResult.results) for (const row of dependentsResult.results) {
|
|
1375
1446
|
const depWorkflow = await workflowTableRowToWorkflowRun({
|
|
1376
1447
|
row,
|
|
@@ -1395,7 +1466,7 @@ const createLogAccessor = (context) => {
|
|
|
1395
1466
|
return [];
|
|
1396
1467
|
};
|
|
1397
1468
|
const getWorkflowByParentId = async (parentInstanceId) => {
|
|
1398
|
-
const result = await context.D1.prepare(`SELECT * FROM WorkflowTable WHERE parentInstanceId = ? AND tenantId = ?`).bind(parentInstanceId, context.tenantId).all();
|
|
1469
|
+
const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowTable WHERE parentInstanceId = ? AND tenantId = ?`).bind(parentInstanceId, context.tenantId).all());
|
|
1399
1470
|
if (result.results) {
|
|
1400
1471
|
const workflows = await Promise.all(result.results.map((row) => workflowTableRowToWorkflowRun({
|
|
1401
1472
|
row,
|
|
@@ -1407,7 +1478,7 @@ const createLogAccessor = (context) => {
|
|
|
1407
1478
|
return null;
|
|
1408
1479
|
};
|
|
1409
1480
|
const getWorkflowByTriggerId = async (triggerId) => {
|
|
1410
|
-
const result = await context.D1.prepare(`SELECT * FROM WorkflowTable WHERE triggerId = ? AND tenantId = ?`).bind(triggerId, context.tenantId).first();
|
|
1481
|
+
const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowTable WHERE triggerId = ? AND tenantId = ?`).bind(triggerId, context.tenantId).first());
|
|
1411
1482
|
if (result) {
|
|
1412
1483
|
const workflow = await workflowTableRowToWorkflowRun({
|
|
1413
1484
|
row: result,
|
|
@@ -1419,12 +1490,12 @@ const createLogAccessor = (context) => {
|
|
|
1419
1490
|
return null;
|
|
1420
1491
|
};
|
|
1421
1492
|
const getWorkflowTypesByTenantId = async (tenantId) => {
|
|
1422
|
-
const result = await context.D1.prepare(`SELECT DISTINCT workflowType FROM WorkflowTable WHERE tenantId = ?`).bind(tenantId).all();
|
|
1493
|
+
const result = await retryD1Operation(() => context.D1.prepare(`SELECT DISTINCT workflowType FROM WorkflowTable WHERE tenantId = ?`).bind(tenantId).all());
|
|
1423
1494
|
if (!result.results) return [];
|
|
1424
1495
|
return result.results.map((row) => row.workflowType);
|
|
1425
1496
|
};
|
|
1426
1497
|
const getWorkflowProperties = async (instanceId) => {
|
|
1427
|
-
const result = await context.D1.prepare(`SELECT * FROM WorkflowProperties WHERE instanceId = ?`).bind(instanceId).all();
|
|
1498
|
+
const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowProperties WHERE instanceId = ?`).bind(instanceId).all());
|
|
1428
1499
|
if (!result.results) return [];
|
|
1429
1500
|
return result.results.map((row) => ({
|
|
1430
1501
|
key: row.key,
|
|
@@ -1434,7 +1505,7 @@ const createLogAccessor = (context) => {
|
|
|
1434
1505
|
};
|
|
1435
1506
|
/** This function gets the basic data of a workflow, without populating any of it's complex fields */
|
|
1436
1507
|
const getWorkflowShallow = async (instanceId, options) => {
|
|
1437
|
-
const result = await context.D1.prepare(`SELECT * FROM WorkflowTable WHERE instanceId = ? AND tenantId = ?`).bind(instanceId, context.tenantId).first();
|
|
1508
|
+
const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowTable WHERE instanceId = ? AND tenantId = ?`).bind(instanceId, context.tenantId).first());
|
|
1438
1509
|
if (!result) return null;
|
|
1439
1510
|
const workflow = await workflowTableRowToWorkflowRun({
|
|
1440
1511
|
row: result,
|
|
@@ -1465,7 +1536,7 @@ const createLogAccessor = (context) => {
|
|
|
1465
1536
|
retryWorkflow.isRetryOf = workflow;
|
|
1466
1537
|
});
|
|
1467
1538
|
workflow.isRetryOf = parentWorkflow;
|
|
1468
|
-
const allLogs = await context.D1.prepare(`SELECT * FROM LogTable WHERE instanceId = ? ORDER BY timestamp ASC, logOrder ASC`).bind(instanceId).all();
|
|
1539
|
+
const allLogs = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM LogTable WHERE instanceId = ? ORDER BY timestamp ASC, logOrder ASC`).bind(instanceId).all());
|
|
1469
1540
|
const logs = allLogs.results?.map((logRow) => ({
|
|
1470
1541
|
instanceId: logRow.instanceId,
|
|
1471
1542
|
stepName: logRow.stepName,
|
|
@@ -1481,10 +1552,10 @@ const createLogAccessor = (context) => {
|
|
|
1481
1552
|
const properties = await getWorkflowProperties(instanceId);
|
|
1482
1553
|
workflow.properties = properties;
|
|
1483
1554
|
if (populateDependencies) {
|
|
1484
|
-
const dependenciesResult = await context.D1.prepare(`SELECT w.*, wd.createdAt
|
|
1555
|
+
const dependenciesResult = await retryD1Operation(() => context.D1.prepare(`SELECT w.*, wd.createdAt
|
|
1485
1556
|
FROM WorkflowDependencies wd
|
|
1486
1557
|
JOIN WorkflowTable w ON wd.dependencyWorkflowId = w.instanceId
|
|
1487
|
-
WHERE wd.dependentWorkflowId = ? AND wd.tenantId = ?`).bind(instanceId, context.tenantId).all();
|
|
1558
|
+
WHERE wd.dependentWorkflowId = ? AND wd.tenantId = ?`).bind(instanceId, context.tenantId).all());
|
|
1488
1559
|
if (dependenciesResult.results) workflow.dependencies = await Promise.all(dependenciesResult.results.map(async (row) => {
|
|
1489
1560
|
const depWorkflow = await workflowTableRowToWorkflowRun({
|
|
1490
1561
|
row,
|
|
@@ -1500,10 +1571,10 @@ const createLogAccessor = (context) => {
|
|
|
1500
1571
|
}));
|
|
1501
1572
|
}
|
|
1502
1573
|
if (populateDependents) {
|
|
1503
|
-
const dependentsResult = await context.D1.prepare(`SELECT w.*, wd.createdAt
|
|
1574
|
+
const dependentsResult = await retryD1Operation(() => context.D1.prepare(`SELECT w.*, wd.createdAt
|
|
1504
1575
|
FROM WorkflowDependencies wd
|
|
1505
1576
|
JOIN WorkflowTable w ON wd.dependentWorkflowId = w.instanceId
|
|
1506
|
-
WHERE wd.dependencyWorkflowId = ? AND wd.tenantId = ?`).bind(instanceId, context.tenantId).all();
|
|
1577
|
+
WHERE wd.dependencyWorkflowId = ? AND wd.tenantId = ?`).bind(instanceId, context.tenantId).all());
|
|
1507
1578
|
if (dependentsResult.results) workflow.dependents = await Promise.all(dependentsResult.results.map(async (row) => {
|
|
1508
1579
|
const depWorkflow = await workflowTableRowToWorkflowRun({
|
|
1509
1580
|
row,
|
|
@@ -1521,7 +1592,7 @@ const createLogAccessor = (context) => {
|
|
|
1521
1592
|
return workflow;
|
|
1522
1593
|
};
|
|
1523
1594
|
const getStep = async (instanceId, stepName) => {
|
|
1524
|
-
const result = await context.D1.prepare(`SELECT * FROM StepTable WHERE instanceId = ? AND stepName = ? AND tenantId = ?`).bind(instanceId, stepName, context.tenantId).all();
|
|
1595
|
+
const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM StepTable WHERE instanceId = ? AND stepName = ? AND tenantId = ?`).bind(instanceId, stepName, context.tenantId).all());
|
|
1525
1596
|
if (!result.results || result.results.length === 0) return null;
|
|
1526
1597
|
const row = result.results[0];
|
|
1527
1598
|
if (!row) return null;
|
|
@@ -1538,7 +1609,7 @@ const createLogAccessor = (context) => {
|
|
|
1538
1609
|
error: deserializedError,
|
|
1539
1610
|
logs: []
|
|
1540
1611
|
};
|
|
1541
|
-
const logResult = await context.D1.prepare(`SELECT * FROM LogTable WHERE instanceId = ? AND stepName = ? ORDER BY logOrder ASC`).bind(instanceId, stepName).all();
|
|
1612
|
+
const logResult = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM LogTable WHERE instanceId = ? AND stepName = ? ORDER BY logOrder ASC`).bind(instanceId, stepName).all());
|
|
1542
1613
|
if (logResult.results) step.logs = logResult.results.map((logRow) => ({
|
|
1543
1614
|
instanceId: logRow.instanceId,
|
|
1544
1615
|
stepName: logRow.stepName,
|
|
@@ -1549,10 +1620,10 @@ const createLogAccessor = (context) => {
|
|
|
1549
1620
|
return step;
|
|
1550
1621
|
};
|
|
1551
1622
|
const getPropertiesKeys = async (instanceId) => {
|
|
1552
|
-
const result = await context.D1.prepare(`
|
|
1623
|
+
const result = await retryD1Operation(() => context.D1.prepare(`
|
|
1553
1624
|
SELECT DISTINCT key, valueType FROM WorkflowProperties
|
|
1554
1625
|
WHERE tenantId = ? AND (? IS NULL OR instanceId = ?)
|
|
1555
|
-
`).bind(context.tenantId, instanceId ?? null, instanceId ?? null).all();
|
|
1626
|
+
`).bind(context.tenantId, instanceId ?? null, instanceId ?? null).all());
|
|
1556
1627
|
console.log(`debug getPropertiesKeys: ${JSON.stringify(result)}`);
|
|
1557
1628
|
if (!result.results) return [];
|
|
1558
1629
|
return result.results.map((row) => ({
|
|
@@ -1575,7 +1646,7 @@ const createLogAccessor = (context) => {
|
|
|
1575
1646
|
bindings.push(toTime);
|
|
1576
1647
|
}
|
|
1577
1648
|
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
|
|
1578
|
-
const result = await context.D1.prepare(`
|
|
1649
|
+
const result = await retryD1Operation(() => context.D1.prepare(`
|
|
1579
1650
|
SELECT
|
|
1580
1651
|
workflowType,
|
|
1581
1652
|
COUNT(*) as workflowCount,
|
|
@@ -1618,7 +1689,7 @@ const createLogAccessor = (context) => {
|
|
|
1618
1689
|
${whereClause}
|
|
1619
1690
|
GROUP BY workflowType
|
|
1620
1691
|
ORDER BY workflowCount DESC
|
|
1621
|
-
`).bind(...bindings).all();
|
|
1692
|
+
`).bind(...bindings).all());
|
|
1622
1693
|
if (!result.results) return [];
|
|
1623
1694
|
return result.results.map((row) => ({
|
|
1624
1695
|
workflowType: row.workflowType,
|
|
@@ -1931,9 +2002,9 @@ async function createStepContext(context) {
|
|
|
1931
2002
|
const stepStatus$1 = "completed";
|
|
1932
2003
|
const { data: resultData, externalRef: resultRef } = await serializeWithExternalStorage(result, context.serializer, context.externalBlobStorage);
|
|
1933
2004
|
const stepError$1 = null;
|
|
1934
|
-
await context.D1.prepare(`UPDATE StepTable
|
|
2005
|
+
await retryD1Operation(() => context.D1.prepare(`UPDATE StepTable
|
|
1935
2006
|
SET stepStatus = ?, endTime = ?, result = ?, error = ?, resultRef = ?, errorRef = ?
|
|
1936
|
-
WHERE instanceId = ? AND stepName = ?`).bind(stepStatus$1, endTime, resultData, stepError$1, resultRef, null, instanceId, stepName).run();
|
|
2007
|
+
WHERE instanceId = ? AND stepName = ?`).bind(stepStatus$1, endTime, resultData, stepError$1, resultRef, null, instanceId, stepName).run());
|
|
1937
2008
|
await logBatcher.destroy();
|
|
1938
2009
|
return result;
|
|
1939
2010
|
} catch (error) {
|
|
@@ -1941,9 +2012,9 @@ async function createStepContext(context) {
|
|
|
1941
2012
|
const stepStatus$1 = "failed";
|
|
1942
2013
|
const stepResult$1 = null;
|
|
1943
2014
|
const { data: errorData, externalRef: errorRef } = await serializeWithExternalStorage(error, context.serializer, context.externalBlobStorage);
|
|
1944
|
-
await context.D1.prepare(`UPDATE StepTable
|
|
2015
|
+
await retryD1Operation(() => context.D1.prepare(`UPDATE StepTable
|
|
1945
2016
|
SET stepStatus = ?, endTime = ?, result = ?, error = ?, resultRef = ?, errorRef = ?
|
|
1946
|
-
WHERE instanceId = ? AND stepName = ?`).bind(stepStatus$1, endTime, stepResult$1, errorData, null, errorRef, instanceId, stepName).run();
|
|
2017
|
+
WHERE instanceId = ? AND stepName = ?`).bind(stepStatus$1, endTime, stepResult$1, errorData, null, errorRef, instanceId, stepName).run());
|
|
1947
2018
|
await logBatcher.destroy();
|
|
1948
2019
|
throw error;
|
|
1949
2020
|
}
|
|
@@ -2098,7 +2169,7 @@ function createWorkflowContext(options) {
|
|
|
2098
2169
|
await ensureTables(options.D1);
|
|
2099
2170
|
ensuredTables = true;
|
|
2100
2171
|
}
|
|
2101
|
-
const oldRun = await options.D1.prepare(`SELECT input, workflowName, tenantId, inputRef FROM WorkflowTable WHERE instanceId = ? `).bind(retryInstanceId).first();
|
|
2172
|
+
const oldRun = await retryD1Operation(() => options.D1.prepare(`SELECT input, workflowName, tenantId, inputRef FROM WorkflowTable WHERE instanceId = ? `).bind(retryInstanceId).first());
|
|
2102
2173
|
const oldWorkflowName = oldRun?.workflowName;
|
|
2103
2174
|
const tenantId = oldRun?.tenantId;
|
|
2104
2175
|
if (!tenantId) throw new Error(`No tenantId found for instanceId ${retryInstanceId}`);
|
|
@@ -2204,4 +2275,4 @@ function createR2ExternalBlobStorage(options) {
|
|
|
2204
2275
|
}
|
|
2205
2276
|
|
|
2206
2277
|
//#endregion
|
|
2207
|
-
export { createCleanupManager, createLogAccessor, createQueueWorkflowContext, createR2ExternalBlobStorage, createStepContext, createWorkflowContext, createWorkflowDependencies, createWorkflowDependency, defaultIdFactory, defaultSerializer, defineWorkflow, deleteWorkflowDependency, deserializeWithExternalStorage, ensureTables, finalizeWorkflowRecord, insertStepRecordFull, insertWorkflowRecord, pushLogToDB, serializeWithExternalStorage, tryDeserializeObj, updateWorkflow, updateWorkflowName, upsertWorkflowProperty, workflowTableRowToWorkflowRun };
|
|
2278
|
+
export { createCleanupManager, createLogAccessor, createQueueWorkflowContext, createR2ExternalBlobStorage, createStepContext, createWorkflowContext, createWorkflowDependencies, createWorkflowDependency, defaultIdFactory, defaultSerializer, defineWorkflow, deleteWorkflowDependency, deserializeWithExternalStorage, ensureTables, finalizeWorkflowRecord, insertStepRecordFull, insertWorkflowRecord, pushLogToDB, retryD1Operation, serializeWithExternalStorage, tryDeserializeObj, updateWorkflow, updateWorkflowName, upsertWorkflowProperty, workflowTableRowToWorkflowRun };
|