@brandboostinggmbh/observable-workflows 0.19.0-beta.4 → 0.20.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
@@ -14,10 +14,43 @@ declare function createStepContext(context: StepContextOptions): Promise<{
14
14
  * This function is idempotent and can be safely run multiple times.
15
15
  *
16
16
  * @param db - D1Database instance
17
+ * @param retryConfig - Optional retry configuration for D1 operations
17
18
  */
18
- declare function ensureTables(db: D1Database): Promise<void>;
19
+ declare function ensureTables(db: D1Database, retryConfig?: RetryConfig): Promise<void>;
19
20
  //#endregion
20
21
  //#region src/observableWorkflows/helperFunctions.d.ts
22
+ /**
23
+ * Configuration for retry behavior on D1 operations
24
+ */
25
+ interface RetryConfig {
26
+ /** Maximum number of retry attempts (default: 3) */
27
+ maxAttempts?: number;
28
+ /** Initial delay in milliseconds (default: 100) */
29
+ initialDelayMs?: number;
30
+ /** Maximum delay in milliseconds (default: 5000) */
31
+ maxDelayMs?: number;
32
+ /** Multiplier for exponential backoff (default: 2) */
33
+ backoffMultiplier?: number;
34
+ /** Whether to add jitter to delays (default: true) */
35
+ useJitter?: boolean;
36
+ }
37
+ /**
38
+ * Retry a D1 operation with exponential backoff for transient network errors
39
+ *
40
+ * @param operation - The async operation to retry
41
+ * @param config - Retry configuration options
42
+ * @returns The result of the operation
43
+ * @throws The last error if all retry attempts fail
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * const result = await retryD1Operation(
48
+ * () => db.prepare("SELECT * FROM table").first(),
49
+ * { maxAttempts: 5, initialDelayMs: 200 }
50
+ * )
51
+ * ```
52
+ */
53
+ declare function retryD1Operation<T>(operation: () => Promise<T>, config?: RetryConfig): Promise<T>;
21
54
  declare function finalizeWorkflowRecord(options: InternalWorkflowContextOptions, {
22
55
  workflowStatus,
23
56
  endTime,
@@ -199,6 +232,7 @@ declare function workflowTableRowToWorkflowRun({
199
232
  }): Promise<WorkflowRun>;
200
233
  declare function updateWorkflowName(context: {
201
234
  D1: D1Database;
235
+ retryConfig?: RetryConfig;
202
236
  }, instanceId: string, newWorkflowName: string): Promise<D1Result<Record<string, unknown>>>;
203
237
  /**
204
238
  * Update workflow fields by instanceId. Only provided fields will be updated.
@@ -409,6 +443,8 @@ type StepContextOptions = {
409
443
  reuseSuccessfulSteps?: boolean;
410
444
  /** Optional external blob storage for large data that exceeds D1 size limits */
411
445
  externalBlobStorage?: ExternalBlobStorage;
446
+ /** Optional retry configuration for D1 operations */
447
+ retryConfig?: RetryConfig;
412
448
  };
413
449
  type ConsoleWrapper = {
414
450
  log: (message?: any, ...optionalParams: any[]) => void;
@@ -610,6 +646,8 @@ type WorkflowContextOptions = {
610
646
  idFactory?: () => string;
611
647
  serializer?: Serializer;
612
648
  externalBlobStorage?: ExternalBlobStorage;
649
+ /** Optional retry configuration for D1 operations (defaults: maxAttempts: 3, initialDelayMs: 100, maxDelayMs: 5000, backoffMultiplier: 2, useJitter: true) */
650
+ retryConfig?: RetryConfig;
613
651
  };
614
652
  type InternalWorkflowContextOptions = WorkflowContextOptions & Required<Pick<WorkflowContextOptions, 'serializer' | 'idFactory'>>;
615
653
  type WorkflowContextInstance = {
@@ -728,6 +766,7 @@ declare const createCleanupManager: (context: {
728
766
  tenantId: string;
729
767
  serializer?: Serializer;
730
768
  externalBlobStorage?: ExternalBlobStorage;
769
+ retryConfig?: RetryConfig;
731
770
  }) => {
732
771
  countAffectedWorkflows: (config: DeleteConfig, limit: number) => Promise<{
733
772
  count: number;
@@ -774,6 +813,7 @@ declare const createLogAccessor: (context: {
774
813
  tenantId: string;
775
814
  serializer?: Serializer;
776
815
  externalBlobStorage?: ExternalBlobStorage;
816
+ retryConfig?: RetryConfig;
777
817
  }) => {
778
818
  listSteps: {
779
819
  (limit: number, offset: number, instanceId?: string): Promise<Step[]>;
@@ -906,4 +946,4 @@ type R2ExternalBlobStorageOptions = {
906
946
  */
907
947
  declare function createR2ExternalBlobStorage(options: R2ExternalBlobStorageOptions): ExternalBlobStorage;
908
948
  //#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 };
949
+ 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
@@ -2,8 +2,8 @@
2
2
  /**
3
3
  * Detect the current schema version for each table
4
4
  */
5
- async function detectSchemaVersion(db) {
6
- const workflowTableInfo = await db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowTable'`).first();
5
+ async function detectSchemaVersion(db, retryConfig) {
6
+ const workflowTableInfo = await retryD1Operation(() => db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowTable'`).first(), retryConfig);
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(), retryConfig);
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(), retryConfig);
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(), retryConfig);
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();
@@ -52,11 +52,11 @@ async function detectSchemaVersion(db) {
52
52
  * Migrate WorkflowTable from V1 to V2 schema
53
53
  * Adds inputRef column and makes input nullable
54
54
  */
55
- async function migrateWorkflowTableV1ToV2(db) {
56
- const workflowTableInfo = await db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowTable'`).first();
55
+ async function migrateWorkflowTableV1ToV2(db, retryConfig) {
56
+ const workflowTableInfo = await retryD1Operation(() => db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowTable'`).first(), retryConfig);
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
+ ]), retryConfig);
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
- async function migrateWorkflowTableV2V3ToV4(db) {
88
- const workflowTableInfo = await db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowTable'`).first();
87
+ async function migrateWorkflowTableV2V3ToV4(db, retryConfig) {
88
+ const workflowTableInfo = await retryD1Operation(() => db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowTable'`).first(), retryConfig);
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,23 +111,23 @@ 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
+ ]), retryConfig);
115
115
  }
116
116
  /**
117
117
  * Migrate WorkflowTable from V4 to V6 schema
118
118
  * Adds result and resultRef columns for external storage support
119
119
  */
120
- async function migrateWorkflowTableV4ToV6(db) {
121
- const workflowTableInfo = await db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowTable'`).first();
120
+ async function migrateWorkflowTableV4ToV6(db, retryConfig) {
121
+ const workflowTableInfo = await retryD1Operation(() => db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='WorkflowTable'`).first(), retryConfig);
122
122
  const hasResultRef = workflowTableInfo.sql.includes("resultRef");
123
- if (!hasResultRef) await db.batch([db.prepare(`ALTER TABLE WorkflowTable ADD COLUMN result TEXT`), db.prepare(`ALTER TABLE WorkflowTable ADD COLUMN resultRef TEXT`)]);
123
+ if (!hasResultRef) await retryD1Operation(() => db.batch([db.prepare(`ALTER TABLE WorkflowTable ADD COLUMN result TEXT`), db.prepare(`ALTER TABLE WorkflowTable ADD COLUMN resultRef TEXT`)]), retryConfig);
124
124
  }
125
125
  /**
126
126
  * Create or migrate WorkflowTable to the latest schema
127
127
  */
128
- async function migrateWorkflowTable(db, currentVersion) {
128
+ async function migrateWorkflowTable(db, currentVersion, retryConfig) {
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,31 +144,31 @@ async function migrateWorkflowTable(db, currentVersion) {
144
144
  triggerId TEXT,
145
145
  PRIMARY KEY (instanceId),
146
146
  UNIQUE (triggerId)
147
- )`).run();
147
+ )`).run(), retryConfig);
148
148
  return;
149
149
  }
150
150
  if (currentVersion === "v1") {
151
- await migrateWorkflowTableV1ToV2(db);
152
- await migrateWorkflowTableV2V3ToV4(db);
153
- await migrateWorkflowTableV4ToV6(db);
151
+ await migrateWorkflowTableV1ToV2(db, retryConfig);
152
+ await migrateWorkflowTableV2V3ToV4(db, retryConfig);
153
+ await migrateWorkflowTableV4ToV6(db, retryConfig);
154
154
  return;
155
155
  }
156
156
  if (currentVersion === "v2") {
157
- await migrateWorkflowTableV2V3ToV4(db);
158
- await migrateWorkflowTableV4ToV6(db);
157
+ await migrateWorkflowTableV2V3ToV4(db, retryConfig);
158
+ await migrateWorkflowTableV4ToV6(db, retryConfig);
159
159
  return;
160
160
  }
161
161
  if (currentVersion === "v4") {
162
- await migrateWorkflowTableV4ToV6(db);
162
+ await migrateWorkflowTableV4ToV6(db, retryConfig);
163
163
  return;
164
164
  }
165
165
  }
166
166
  /**
167
167
  * Create or migrate StepTable to the latest schema
168
168
  */
169
- async function migrateStepTable(db, currentVersion) {
169
+ async function migrateStepTable(db, currentVersion, retryConfig) {
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,23 +182,23 @@ 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(), retryConfig);
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(), retryConfig);
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(), retryConfig);
193
+ if (!hasErrorRef) await retryD1Operation(() => db.prepare(`ALTER TABLE StepTable ADD COLUMN errorRef TEXT`).run(), retryConfig);
194
194
  }
195
195
  }
196
196
  /**
197
197
  * Create or migrate LogTable to the latest schema
198
198
  */
199
- async function migrateLogTable(db, currentVersion) {
199
+ async function migrateLogTable(db, currentVersion, retryConfig) {
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,16 +208,16 @@ 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(), retryConfig);
212
212
  return;
213
213
  }
214
214
  }
215
215
  /**
216
216
  * Create or migrate WorkflowProperties table to the latest schema
217
217
  */
218
- async function migrateWorkflowPropertiesTable(db, currentVersion) {
218
+ async function migrateWorkflowPropertiesTable(db, currentVersion, retryConfig) {
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,16 +226,16 @@ 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(), retryConfig);
230
230
  return;
231
231
  }
232
232
  }
233
233
  /**
234
234
  * Create or migrate WorkflowDependencies table to the latest schema
235
235
  */
236
- async function migrateWorkflowDependenciesTable(db, currentVersion) {
236
+ async function migrateWorkflowDependenciesTable(db, currentVersion, retryConfig) {
237
237
  if (currentVersion === "missing") {
238
- await db.prepare(`CREATE TABLE WorkflowDependencies (
238
+ await retryD1Operation(() => db.prepare(`CREATE TABLE WorkflowDependencies (
239
239
  dependencyWorkflowId TEXT NOT NULL,
240
240
  dependentWorkflowId TEXT NOT NULL,
241
241
  tenantId TEXT NOT NULL,
@@ -243,15 +243,15 @@ async function migrateWorkflowDependenciesTable(db, currentVersion) {
243
243
  PRIMARY KEY (dependencyWorkflowId, dependentWorkflowId),
244
244
  FOREIGN KEY (dependencyWorkflowId) REFERENCES WorkflowTable(instanceId) ON DELETE CASCADE,
245
245
  FOREIGN KEY (dependentWorkflowId) REFERENCES WorkflowTable(instanceId) ON DELETE CASCADE
246
- )`).run();
246
+ )`).run(), retryConfig);
247
247
  return;
248
248
  }
249
249
  }
250
250
  /**
251
251
  * Create necessary indexes
252
252
  */
253
- async function createIndexes(db) {
254
- const existingIndexes = await db.prepare(`SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_%'`).all();
253
+ async function createIndexes(db, retryConfig) {
254
+ const existingIndexes = await retryD1Operation(() => db.prepare(`SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_%'`).all(), retryConfig);
255
255
  const existingIndexNames = new Set(existingIndexes.results?.map((row) => row.name) || []);
256
256
  const indexes = [
257
257
  {
@@ -341,37 +341,107 @@ 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), retryConfig);
345
345
  }
346
346
  /**
347
347
  * Main migration function that ensures all tables exist and are up-to-date.
348
348
  * This function is idempotent and can be safely run multiple times.
349
349
  *
350
350
  * @param db - D1Database instance
351
+ * @param retryConfig - Optional retry configuration for D1 operations
351
352
  */
352
- async function ensureTables(db) {
353
- await db.prepare(`PRAGMA foreign_keys = ON`).run();
354
- const schemaVersion = await detectSchemaVersion(db);
355
- await migrateWorkflowTable(db, schemaVersion.workflowTable);
356
- await migrateStepTable(db, schemaVersion.stepTable);
357
- await migrateLogTable(db, schemaVersion.logTable);
358
- await migrateWorkflowPropertiesTable(db, schemaVersion.workflowProperties);
359
- await migrateWorkflowDependenciesTable(db, schemaVersion.workflowDependencies);
360
- await createIndexes(db);
353
+ async function ensureTables(db, retryConfig) {
354
+ await retryD1Operation(() => db.prepare(`PRAGMA foreign_keys = ON`).run(), retryConfig);
355
+ const schemaVersion = await detectSchemaVersion(db, retryConfig);
356
+ await migrateWorkflowTable(db, schemaVersion.workflowTable, retryConfig);
357
+ await migrateStepTable(db, schemaVersion.stepTable, retryConfig);
358
+ await migrateLogTable(db, schemaVersion.logTable, retryConfig);
359
+ await migrateWorkflowPropertiesTable(db, schemaVersion.workflowProperties, retryConfig);
360
+ await migrateWorkflowDependenciesTable(db, schemaVersion.workflowDependencies, retryConfig);
361
+ await createIndexes(db, retryConfig);
361
362
  }
362
363
 
363
364
  //#endregion
364
365
  //#region src/observableWorkflows/helperFunctions.ts
366
+ /**
367
+ * Default retry configuration for D1 operations
368
+ */
369
+ const DEFAULT_RETRY_CONFIG = {
370
+ maxAttempts: 3,
371
+ initialDelayMs: 100,
372
+ maxDelayMs: 5e3,
373
+ backoffMultiplier: 2,
374
+ useJitter: true
375
+ };
376
+ /**
377
+ * Check if an error is a transient D1 network error that should be retried
378
+ */
379
+ function isRetriableD1Error(error) {
380
+ if (!error) return false;
381
+ const errorMessage = error.message || String(error);
382
+ const errorString = errorMessage.toLowerCase();
383
+ return errorString.includes("network connection lost") || errorString.includes("d1_error") || errorString.includes("connection reset") || errorString.includes("econnreset") || errorString.includes("timeout") || errorString.includes("etimedout");
384
+ }
385
+ /**
386
+ * Calculate delay with exponential backoff and optional jitter
387
+ */
388
+ function calculateDelay(attempt, config) {
389
+ const exponentialDelay = Math.min(config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt), config.maxDelayMs);
390
+ if (config.useJitter) return Math.floor(Math.random() * exponentialDelay);
391
+ return exponentialDelay;
392
+ }
393
+ /**
394
+ * Sleep for the specified number of milliseconds
395
+ */
396
+ function sleep(ms) {
397
+ return new Promise((resolve) => setTimeout(resolve, ms));
398
+ }
399
+ /**
400
+ * Retry a D1 operation with exponential backoff for transient network errors
401
+ *
402
+ * @param operation - The async operation to retry
403
+ * @param config - Retry configuration options
404
+ * @returns The result of the operation
405
+ * @throws The last error if all retry attempts fail
406
+ *
407
+ * @example
408
+ * ```typescript
409
+ * const result = await retryD1Operation(
410
+ * () => db.prepare("SELECT * FROM table").first(),
411
+ * { maxAttempts: 5, initialDelayMs: 200 }
412
+ * )
413
+ * ```
414
+ */
415
+ async function retryD1Operation(operation, config = {}) {
416
+ const finalConfig = {
417
+ ...DEFAULT_RETRY_CONFIG,
418
+ ...config
419
+ };
420
+ let lastError;
421
+ for (let attempt = 0; attempt < finalConfig.maxAttempts; attempt++) try {
422
+ return await operation();
423
+ } catch (error) {
424
+ lastError = error;
425
+ if (!isRetriableD1Error(error)) throw error;
426
+ if (attempt < finalConfig.maxAttempts - 1) {
427
+ const delay = calculateDelay(attempt, finalConfig);
428
+ console.warn(`D1 operation failed with retriable error (attempt ${attempt + 1}/${finalConfig.maxAttempts}), retrying in ${delay}ms:`, error instanceof Error ? error.message : String(error));
429
+ await sleep(delay);
430
+ }
431
+ }
432
+ console.error(`D1 operation failed after ${finalConfig.maxAttempts} attempts:`, lastError instanceof Error ? lastError.message : String(lastError));
433
+ throw lastError;
434
+ }
365
435
  async function finalizeWorkflowRecord(options, { workflowStatus, endTime, instanceId, result }) {
366
436
  if (result !== void 0) {
367
437
  const { data: resultData, externalRef: resultRef } = await serializeWithExternalStorage(result, options.serializer, options.externalBlobStorage);
368
- return options.D1.prepare(`UPDATE WorkflowTable
438
+ return retryD1Operation(() => options.D1.prepare(`UPDATE WorkflowTable
369
439
  SET workflowStatus = ?, endTime = ?, result = ?, resultRef = ?
370
- WHERE instanceId = ?`).bind(workflowStatus, endTime, resultData, resultRef, instanceId).run();
440
+ WHERE instanceId = ?`).bind(workflowStatus, endTime, resultData, resultRef, instanceId).run(), options.retryConfig);
371
441
  }
372
- return options.D1.prepare(`UPDATE WorkflowTable
442
+ return retryD1Operation(() => options.D1.prepare(`UPDATE WorkflowTable
373
443
  SET workflowStatus = ?, endTime = ?
374
- WHERE instanceId = ?`).bind(workflowStatus, endTime, instanceId).run();
444
+ WHERE instanceId = ?`).bind(workflowStatus, endTime, instanceId).run(), options.retryConfig);
375
445
  }
376
446
  /**
377
447
  * Insert a new workflow record into the database.
@@ -402,7 +472,7 @@ async function insertWorkflowRecord(options, { instanceId, workflowType, workflo
402
472
  (instanceId, workflowType, workflowName, workflowMetadata, input, inputRef, tenantId, workflowStatus, startTime, endTime, parentInstanceId, triggerId)
403
473
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, workflowType, workflowName, options.serializer.serialize(workflowMetadata), inputData, inputRef, tenantId, workflowStatus, startTime, endTime ?? null, parentInstanceId ?? null, triggerId ?? null);
404
474
  if (!dependencies || dependencies.length === 0) {
405
- const result = await insertWorkflowStatement.run();
475
+ const result = await retryD1Operation(() => insertWorkflowStatement.run(), options.retryConfig);
406
476
  return {
407
477
  success: result.success,
408
478
  meta: result.meta
@@ -416,7 +486,7 @@ async function insertWorkflowRecord(options, { instanceId, workflowType, workflo
416
486
  tenantId,
417
487
  createdAt
418
488
  }));
419
- const results = await options.D1.batch([insertWorkflowStatement, ...dependencyStatements]);
489
+ const results = await retryD1Operation(() => options.D1.batch([insertWorkflowStatement, ...dependencyStatements]), options.retryConfig);
420
490
  const allSucceeded = results.every((result) => result.success);
421
491
  return {
422
492
  success: allSucceeded,
@@ -424,12 +494,12 @@ async function insertWorkflowRecord(options, { instanceId, workflowType, workflo
424
494
  };
425
495
  }
426
496
  function insertStepRecordFull(context, { instanceId, name, status, metadata, startTime, endTime, result, error, resultRef, errorRef }) {
427
- return context.D1.prepare(`INSERT INTO StepTable
497
+ return retryD1Operation(() => context.D1.prepare(`INSERT INTO StepTable
428
498
  (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();
499
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, name, status, metadata, startTime, endTime, result, error, resultRef ?? null, errorRef ?? null, context.tenantId).run(), context.retryConfig);
430
500
  }
431
501
  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();
502
+ const existingStep = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM StepTable WHERE instanceId = ? AND stepName = ? AND tenantId = ?`).bind(instanceId, stepName, context.tenantId).first(), context.retryConfig);
433
503
  const row = existingStep;
434
504
  if (row) {
435
505
  const result = await deserializeWithExternalStorage(row.result, row.resultRef, context.serializer, context.externalBlobStorage);
@@ -503,9 +573,9 @@ function truncateLogMessage(message) {
503
573
  }
504
574
  function pushLogToDB(options, { instanceId, stepName, message, timestamp, type, logOrder, tenantId }) {
505
575
  const truncatedMessage = truncateLogMessage(message);
506
- return options.D1.prepare(`INSERT INTO LogTable
576
+ return retryD1Operation(() => options.D1.prepare(`INSERT INTO LogTable
507
577
  (instanceId, stepName, log, timestamp, type, logOrder, tenantId)
508
- VALUES (?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, stepName, truncatedMessage, timestamp, type, logOrder, tenantId).run();
578
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, stepName, truncatedMessage, timestamp, type, logOrder, tenantId).run(), options.retryConfig);
509
579
  }
510
580
  var LogBatcher = class {
511
581
  batch = [];
@@ -540,10 +610,12 @@ var LogBatcher = class {
540
610
  if (this.batch.length === 0) return;
541
611
  const logsToFlush = this.batch.splice(0);
542
612
  try {
543
- const statements = logsToFlush.map((entry) => this.options.D1.prepare(`INSERT INTO LogTable
613
+ await retryD1Operation(async () => {
614
+ const statements = logsToFlush.map((entry) => this.options.D1.prepare(`INSERT INTO LogTable
544
615
  (instanceId, stepName, log, timestamp, type, logOrder, tenantId)
545
616
  VALUES (?, ?, ?, ?, ?, ?, ?)`).bind(entry.instanceId, entry.stepName, entry.message, entry.timestamp, entry.type, entry.logOrder, entry.tenantId));
546
- await this.options.D1.batch(statements);
617
+ return await this.options.D1.batch(statements);
618
+ }, this.options.retryConfig);
547
619
  } catch (error) {
548
620
  console.error("Batch log insert failed, falling back to individual inserts:", error);
549
621
  await Promise.all(logsToFlush.map((entry) => pushLogToDB(this.options, entry)));
@@ -671,9 +743,9 @@ async function workflowTableRowToWorkflowRun({ row, serializer, externalBlobStor
671
743
  };
672
744
  }
673
745
  async function updateWorkflowName(context, instanceId, newWorkflowName) {
674
- return await context.D1.prepare(`UPDATE WorkflowTable
746
+ return await retryD1Operation(() => context.D1.prepare(`UPDATE WorkflowTable
675
747
  SET workflowName = ?
676
- WHERE instanceId = ?`).bind(newWorkflowName, instanceId).run();
748
+ WHERE instanceId = ?`).bind(newWorkflowName, instanceId).run(), context.retryConfig);
677
749
  }
678
750
  /**
679
751
  * Update workflow fields by instanceId. Only provided fields will be updated.
@@ -750,7 +822,7 @@ async function updateWorkflow(context, instanceId, updates) {
750
822
  };
751
823
  bindings.push(instanceId);
752
824
  const sql = `UPDATE WorkflowTable SET ${setClauses.join(", ")} WHERE instanceId = ?`;
753
- return await context.D1.prepare(sql).bind(...bindings).run();
825
+ return await retryD1Operation(() => context.D1.prepare(sql).bind(...bindings).run(), context.retryConfig);
754
826
  }
755
827
  /**
756
828
  * Serialize a workflow property value for storage
@@ -783,9 +855,9 @@ function deserializeWorkflowPropertyValue(serializedValue, valueType) {
783
855
  }
784
856
  async function upsertWorkflowProperty({ context, instanceId, key, value, tenantId }) {
785
857
  const { serializedValue, valueType } = serializeWorkflowPropertyValue(value);
786
- const res = await context.D1.prepare(`INSERT OR REPLACE INTO WorkflowProperties
858
+ const res = await retryD1Operation(() => context.D1.prepare(`INSERT OR REPLACE INTO WorkflowProperties
787
859
  (instanceId, key, value, valueType, tenantId)
788
- VALUES (?, ?, ?, ?, ?)`).bind(instanceId, key, serializedValue, valueType, tenantId).run();
860
+ VALUES (?, ?, ?, ?, ?)`).bind(instanceId, key, serializedValue, valueType, tenantId).run(), context.retryConfig);
789
861
  if (res.error) {
790
862
  console.error("Error inserting workflow property:", res.error);
791
863
  return false;
@@ -841,12 +913,12 @@ function prepareWorkflowDependencyStatement({ D1, dependencyWorkflowId, dependen
841
913
  * ```
842
914
  */
843
915
  async function createWorkflowDependency({ context, dependencyWorkflowId, dependentWorkflowId, tenantId }) {
844
- return await prepareWorkflowDependencyStatement({
916
+ return await retryD1Operation(() => prepareWorkflowDependencyStatement({
845
917
  D1: context.D1,
846
918
  dependencyWorkflowId,
847
919
  dependentWorkflowId,
848
920
  tenantId
849
- }).run();
921
+ }).run(), context.retryConfig);
850
922
  }
851
923
  /**
852
924
  * Create multiple workflow dependencies in a single transaction.
@@ -879,7 +951,7 @@ async function createWorkflowDependencies({ context, dependencies, tenantId }) {
879
951
  tenantId,
880
952
  createdAt
881
953
  }));
882
- return await context.D1.batch(statements);
954
+ return await retryD1Operation(() => context.D1.batch(statements), context.retryConfig);
883
955
  }
884
956
  /**
885
957
  * Delete a specific workflow dependency relationship.
@@ -896,8 +968,8 @@ async function createWorkflowDependencies({ context, dependencies, tenantId }) {
896
968
  * ```
897
969
  */
898
970
  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();
971
+ return await retryD1Operation(() => context.D1.prepare(`DELETE FROM WorkflowDependencies
972
+ WHERE dependencyWorkflowId = ? AND dependentWorkflowId = ? AND tenantId = ?`).bind(dependencyWorkflowId, dependentWorkflowId, tenantId).run(), context.retryConfig);
901
973
  }
902
974
 
903
975
  //#endregion
@@ -917,7 +989,7 @@ const createCleanupManager = (context) => {
917
989
  const getAffectedWorkflows = async (config, limit) => {
918
990
  const { statuses, statusPlaceholders } = buildStatusesAndPlaceholders(config);
919
991
  if (statuses.length === 0) return [];
920
- const result = await context.D1.prepare(`
992
+ const result = await retryD1Operation(() => context.D1.prepare(`
921
993
  SELECT instanceId, inputRef
922
994
  FROM WorkflowTable
923
995
  WHERE tenantId = ?
@@ -925,13 +997,13 @@ const createCleanupManager = (context) => {
925
997
  AND workflowStatus IN (${statusPlaceholders})
926
998
  ORDER BY instanceId
927
999
  LIMIT ?
928
- `).bind(context.tenantId, config.deleteWorkflowsOlderThanDays, ...statuses, limit).all();
1000
+ `).bind(context.tenantId, config.deleteWorkflowsOlderThanDays, ...statuses, limit).all(), context.retryConfig);
929
1001
  return result.results;
930
1002
  };
931
1003
  const getAffectedSteps = async (config, limit) => {
932
1004
  const { statuses, statusPlaceholders } = buildStatusesAndPlaceholders(config);
933
1005
  if (statuses.length === 0) return [];
934
- const result = await context.D1.prepare(`
1006
+ const result = await retryD1Operation(() => context.D1.prepare(`
935
1007
  SELECT s.instanceId, s.stepName, s.stepStatus, s.errorRef, s.resultRef
936
1008
  FROM StepTable s
937
1009
  WHERE s.instanceId IN (
@@ -944,7 +1016,7 @@ const createCleanupManager = (context) => {
944
1016
  LIMIT ?
945
1017
  )
946
1018
  ORDER BY s.instanceId, s.stepName
947
- `).bind(context.tenantId, config.deleteWorkflowsOlderThanDays, ...statuses, limit).all();
1019
+ `).bind(context.tenantId, config.deleteWorkflowsOlderThanDays, ...statuses, limit).all(), context.retryConfig);
948
1020
  return result.results;
949
1021
  };
950
1022
  const collectExternalStorageKeys = (workflows, steps) => {
@@ -997,15 +1069,15 @@ const createCleanupManager = (context) => {
997
1069
  }
998
1070
  if (deletedWorkflows.length > 0) {
999
1071
  console.log(`Proceeding to delete workflows from the database...`);
1000
- await context.D1.prepare(`PRAGMA foreign_keys = ON`).run();
1072
+ await retryD1Operation(() => context.D1.prepare(`PRAGMA foreign_keys = ON`).run(), context.retryConfig);
1001
1073
  let totalDeletedCount = 0;
1002
1074
  for (let i = 0; i < deletedWorkflows.length; i += 100) {
1003
1075
  const batch = deletedWorkflows.slice(i, i + 100);
1004
1076
  const workflowIds = batch.map(() => "?").join(", ");
1005
- const result = await context.D1.prepare(`
1077
+ const result = await retryD1Operation(() => context.D1.prepare(`
1006
1078
  DELETE FROM WorkflowTable
1007
1079
  WHERE instanceId IN (${workflowIds})
1008
- `).bind(...batch.map((w) => w.instanceId)).run();
1080
+ `).bind(...batch.map((w) => w.instanceId)).run(), context.retryConfig);
1009
1081
  totalDeletedCount += result.meta.changes || 0;
1010
1082
  }
1011
1083
  return {
@@ -1021,8 +1093,8 @@ const createCleanupManager = (context) => {
1021
1093
  };
1022
1094
  };
1023
1095
  const cleanupOrphanedSteps = async (config, limit) => {
1024
- await context.D1.prepare(`PRAGMA foreign_keys = ON`).run();
1025
- const orphanedSteps = await context.D1.prepare(`
1096
+ await retryD1Operation(() => context.D1.prepare(`PRAGMA foreign_keys = ON`).run(), context.retryConfig);
1097
+ const orphanedSteps = await retryD1Operation(() => context.D1.prepare(`
1026
1098
  SELECT s.instanceId, s.stepName, s.errorRef, s.resultRef
1027
1099
  FROM StepTable s
1028
1100
  WHERE s.tenantId = ?
@@ -1032,7 +1104,7 @@ const createCleanupManager = (context) => {
1032
1104
  )
1033
1105
  ORDER BY s.instanceId, s.stepName
1034
1106
  LIMIT ?
1035
- `).bind(context.tenantId, context.tenantId, limit).all();
1107
+ `).bind(context.tenantId, context.tenantId, limit).all(), context.retryConfig);
1036
1108
  const orphanedStepResults = orphanedSteps.results;
1037
1109
  let deletedExternalStorageKeysCount = 0;
1038
1110
  if (config.deleteRefsFromExternalStorage && context.externalBlobStorage && orphanedStepResults.length > 0) {
@@ -1048,11 +1120,11 @@ const createCleanupManager = (context) => {
1048
1120
  for (let i = 0; i < orphanedStepResults.length; i += 99) {
1049
1121
  const batch = orphanedStepResults.slice(i, i + 99);
1050
1122
  const instanceIds = batch.map(() => "?").join(", ");
1051
- const deletedStepsResult = await context.D1.prepare(`
1123
+ const deletedStepsResult = await retryD1Operation(() => context.D1.prepare(`
1052
1124
  DELETE FROM StepTable
1053
1125
  WHERE tenantId = ?
1054
1126
  AND instanceId IN (${instanceIds})
1055
- `).bind(context.tenantId, ...batch.map((s) => s.instanceId)).run();
1127
+ `).bind(context.tenantId, ...batch.map((s) => s.instanceId)).run(), context.retryConfig);
1056
1128
  totalDeletedSteps += deletedStepsResult.meta.changes || 0;
1057
1129
  }
1058
1130
  return {
@@ -1068,8 +1140,8 @@ const createCleanupManager = (context) => {
1068
1140
  };
1069
1141
  };
1070
1142
  const cleanupOrphanedLogs = async (limit) => {
1071
- await context.D1.prepare(`PRAGMA foreign_keys = ON`).run();
1072
- const deletedLogsResult = await context.D1.prepare(`
1143
+ await retryD1Operation(() => context.D1.prepare(`PRAGMA foreign_keys = ON`).run(), context.retryConfig);
1144
+ const deletedLogsResult = await retryD1Operation(() => context.D1.prepare(`
1073
1145
  DELETE FROM LogTable
1074
1146
  WHERE tenantId = ?
1075
1147
  AND (
@@ -1084,7 +1156,7 @@ const createCleanupManager = (context) => {
1084
1156
  ))
1085
1157
  )
1086
1158
  LIMIT ?
1087
- `).bind(context.tenantId, context.tenantId, context.tenantId, limit).run();
1159
+ `).bind(context.tenantId, context.tenantId, context.tenantId, limit).run(), context.retryConfig);
1088
1160
  return { deletedOrphanedLogs: deletedLogsResult.meta.changes || 0 };
1089
1161
  };
1090
1162
  return {
@@ -1272,7 +1344,7 @@ const createLogAccessor = (context) => {
1272
1344
  const limitClause = limit !== void 0 && actualOffset !== void 0 ? "LIMIT ? OFFSET ?" : "";
1273
1345
  const sql = `SELECT * FROM StepTable ${whereClause} ORDER BY startTime ASC ${limitClause}`;
1274
1346
  if (limit !== void 0 && actualOffset !== void 0) bindings.push(limit, actualOffset);
1275
- result = await context.D1.prepare(sql).bind(...bindings).all();
1347
+ result = await retryD1Operation(() => context.D1.prepare(sql).bind(...bindings).all(), context.retryConfig);
1276
1348
  if (result.results) {
1277
1349
  const steps = await Promise.all(result.results.map(async (row) => {
1278
1350
  let deserializedResult = null;
@@ -1299,7 +1371,7 @@ const createLogAccessor = (context) => {
1299
1371
  const listWorkflows = async (limit, offset, filter, options) => {
1300
1372
  const { sql, bindings } = buildFilteredWorkflowQuery(filter, { ignoreTenant: options?.ignoreTenant });
1301
1373
  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();
1374
+ const result = await retryD1Operation(() => context.D1.prepare(sql).bind(...bindings, limit, offset).all(), context.retryConfig);
1303
1375
  if (options?.debugLogs) console.log("listWorkflows SQL Query executed");
1304
1376
  if (result.results) {
1305
1377
  const workflows = await Promise.all(result.results.map((row) => workflowTableRowToWorkflowRun({
@@ -1316,7 +1388,7 @@ const createLogAccessor = (context) => {
1316
1388
  for (let i = 0; i < instanceIds.length; i += BATCH_SIZE) {
1317
1389
  const batchIds = instanceIds.slice(i, i + BATCH_SIZE);
1318
1390
  const placeholders = batchIds.map(() => "?").join(", ");
1319
- const propertiesResult = await context.D1.prepare(`SELECT * FROM WorkflowProperties WHERE instanceId IN (${placeholders})`).bind(...batchIds).all();
1391
+ const propertiesResult = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowProperties WHERE instanceId IN (${placeholders})`).bind(...batchIds).all(), context.retryConfig);
1320
1392
  if (propertiesResult.results) for (const row of propertiesResult.results) {
1321
1393
  const property = {
1322
1394
  key: row.key,
@@ -1337,10 +1409,10 @@ const createLogAccessor = (context) => {
1337
1409
  for (let i = 0; i < instanceIds.length; i += BATCH_SIZE) {
1338
1410
  const batchIds = instanceIds.slice(i, i + BATCH_SIZE);
1339
1411
  const placeholders = batchIds.map(() => "?").join(", ");
1340
- const dependenciesResult = await context.D1.prepare(`SELECT w.*, wd.createdAt, wd.dependentWorkflowId
1412
+ const dependenciesResult = await retryD1Operation(() => context.D1.prepare(`SELECT w.*, wd.createdAt, wd.dependentWorkflowId
1341
1413
  FROM WorkflowDependencies wd
1342
1414
  JOIN WorkflowTable w ON wd.dependencyWorkflowId = w.instanceId
1343
- WHERE wd.dependentWorkflowId IN (${placeholders})`).bind(...batchIds).all();
1415
+ WHERE wd.dependentWorkflowId IN (${placeholders})`).bind(...batchIds).all(), context.retryConfig);
1344
1416
  if (dependenciesResult.results) for (const row of dependenciesResult.results) {
1345
1417
  const depWorkflow = await workflowTableRowToWorkflowRun({
1346
1418
  row,
@@ -1367,10 +1439,10 @@ const createLogAccessor = (context) => {
1367
1439
  for (let i = 0; i < instanceIds.length; i += BATCH_SIZE) {
1368
1440
  const batchIds = instanceIds.slice(i, i + BATCH_SIZE);
1369
1441
  const placeholders = batchIds.map(() => "?").join(", ");
1370
- const dependentsResult = await context.D1.prepare(`SELECT w.*, wd.createdAt, wd.dependencyWorkflowId
1442
+ const dependentsResult = await retryD1Operation(() => context.D1.prepare(`SELECT w.*, wd.createdAt, wd.dependencyWorkflowId
1371
1443
  FROM WorkflowDependencies wd
1372
1444
  JOIN WorkflowTable w ON wd.dependentWorkflowId = w.instanceId
1373
- WHERE wd.dependencyWorkflowId IN (${placeholders})`).bind(...batchIds).all();
1445
+ WHERE wd.dependencyWorkflowId IN (${placeholders})`).bind(...batchIds).all(), context.retryConfig);
1374
1446
  if (dependentsResult.results) for (const row of dependentsResult.results) {
1375
1447
  const depWorkflow = await workflowTableRowToWorkflowRun({
1376
1448
  row,
@@ -1395,7 +1467,7 @@ const createLogAccessor = (context) => {
1395
1467
  return [];
1396
1468
  };
1397
1469
  const getWorkflowByParentId = async (parentInstanceId) => {
1398
- const result = await context.D1.prepare(`SELECT * FROM WorkflowTable WHERE parentInstanceId = ? AND tenantId = ?`).bind(parentInstanceId, context.tenantId).all();
1470
+ const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowTable WHERE parentInstanceId = ? AND tenantId = ?`).bind(parentInstanceId, context.tenantId).all(), context.retryConfig);
1399
1471
  if (result.results) {
1400
1472
  const workflows = await Promise.all(result.results.map((row) => workflowTableRowToWorkflowRun({
1401
1473
  row,
@@ -1407,7 +1479,7 @@ const createLogAccessor = (context) => {
1407
1479
  return null;
1408
1480
  };
1409
1481
  const getWorkflowByTriggerId = async (triggerId) => {
1410
- const result = await context.D1.prepare(`SELECT * FROM WorkflowTable WHERE triggerId = ? AND tenantId = ?`).bind(triggerId, context.tenantId).first();
1482
+ const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowTable WHERE triggerId = ? AND tenantId = ?`).bind(triggerId, context.tenantId).first(), context.retryConfig);
1411
1483
  if (result) {
1412
1484
  const workflow = await workflowTableRowToWorkflowRun({
1413
1485
  row: result,
@@ -1419,12 +1491,12 @@ const createLogAccessor = (context) => {
1419
1491
  return null;
1420
1492
  };
1421
1493
  const getWorkflowTypesByTenantId = async (tenantId) => {
1422
- const result = await context.D1.prepare(`SELECT DISTINCT workflowType FROM WorkflowTable WHERE tenantId = ?`).bind(tenantId).all();
1494
+ const result = await retryD1Operation(() => context.D1.prepare(`SELECT DISTINCT workflowType FROM WorkflowTable WHERE tenantId = ?`).bind(tenantId).all(), context.retryConfig);
1423
1495
  if (!result.results) return [];
1424
1496
  return result.results.map((row) => row.workflowType);
1425
1497
  };
1426
1498
  const getWorkflowProperties = async (instanceId) => {
1427
- const result = await context.D1.prepare(`SELECT * FROM WorkflowProperties WHERE instanceId = ?`).bind(instanceId).all();
1499
+ const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowProperties WHERE instanceId = ?`).bind(instanceId).all(), context.retryConfig);
1428
1500
  if (!result.results) return [];
1429
1501
  return result.results.map((row) => ({
1430
1502
  key: row.key,
@@ -1434,7 +1506,7 @@ const createLogAccessor = (context) => {
1434
1506
  };
1435
1507
  /** This function gets the basic data of a workflow, without populating any of it's complex fields */
1436
1508
  const getWorkflowShallow = async (instanceId, options) => {
1437
- const result = await context.D1.prepare(`SELECT * FROM WorkflowTable WHERE instanceId = ? AND tenantId = ?`).bind(instanceId, context.tenantId).first();
1509
+ const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowTable WHERE instanceId = ? AND tenantId = ?`).bind(instanceId, context.tenantId).first(), context.retryConfig);
1438
1510
  if (!result) return null;
1439
1511
  const workflow = await workflowTableRowToWorkflowRun({
1440
1512
  row: result,
@@ -1465,7 +1537,7 @@ const createLogAccessor = (context) => {
1465
1537
  retryWorkflow.isRetryOf = workflow;
1466
1538
  });
1467
1539
  workflow.isRetryOf = parentWorkflow;
1468
- const allLogs = await context.D1.prepare(`SELECT * FROM LogTable WHERE instanceId = ? ORDER BY timestamp ASC, logOrder ASC`).bind(instanceId).all();
1540
+ const allLogs = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM LogTable WHERE instanceId = ? ORDER BY timestamp ASC, logOrder ASC`).bind(instanceId).all(), context.retryConfig);
1469
1541
  const logs = allLogs.results?.map((logRow) => ({
1470
1542
  instanceId: logRow.instanceId,
1471
1543
  stepName: logRow.stepName,
@@ -1481,10 +1553,10 @@ const createLogAccessor = (context) => {
1481
1553
  const properties = await getWorkflowProperties(instanceId);
1482
1554
  workflow.properties = properties;
1483
1555
  if (populateDependencies) {
1484
- const dependenciesResult = await context.D1.prepare(`SELECT w.*, wd.createdAt
1556
+ const dependenciesResult = await retryD1Operation(() => context.D1.prepare(`SELECT w.*, wd.createdAt
1485
1557
  FROM WorkflowDependencies wd
1486
1558
  JOIN WorkflowTable w ON wd.dependencyWorkflowId = w.instanceId
1487
- WHERE wd.dependentWorkflowId = ? AND wd.tenantId = ?`).bind(instanceId, context.tenantId).all();
1559
+ WHERE wd.dependentWorkflowId = ? AND wd.tenantId = ?`).bind(instanceId, context.tenantId).all(), context.retryConfig);
1488
1560
  if (dependenciesResult.results) workflow.dependencies = await Promise.all(dependenciesResult.results.map(async (row) => {
1489
1561
  const depWorkflow = await workflowTableRowToWorkflowRun({
1490
1562
  row,
@@ -1500,10 +1572,10 @@ const createLogAccessor = (context) => {
1500
1572
  }));
1501
1573
  }
1502
1574
  if (populateDependents) {
1503
- const dependentsResult = await context.D1.prepare(`SELECT w.*, wd.createdAt
1575
+ const dependentsResult = await retryD1Operation(() => context.D1.prepare(`SELECT w.*, wd.createdAt
1504
1576
  FROM WorkflowDependencies wd
1505
1577
  JOIN WorkflowTable w ON wd.dependentWorkflowId = w.instanceId
1506
- WHERE wd.dependencyWorkflowId = ? AND wd.tenantId = ?`).bind(instanceId, context.tenantId).all();
1578
+ WHERE wd.dependencyWorkflowId = ? AND wd.tenantId = ?`).bind(instanceId, context.tenantId).all(), context.retryConfig);
1507
1579
  if (dependentsResult.results) workflow.dependents = await Promise.all(dependentsResult.results.map(async (row) => {
1508
1580
  const depWorkflow = await workflowTableRowToWorkflowRun({
1509
1581
  row,
@@ -1521,7 +1593,7 @@ const createLogAccessor = (context) => {
1521
1593
  return workflow;
1522
1594
  };
1523
1595
  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();
1596
+ const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM StepTable WHERE instanceId = ? AND stepName = ? AND tenantId = ?`).bind(instanceId, stepName, context.tenantId).all(), context.retryConfig);
1525
1597
  if (!result.results || result.results.length === 0) return null;
1526
1598
  const row = result.results[0];
1527
1599
  if (!row) return null;
@@ -1538,7 +1610,7 @@ const createLogAccessor = (context) => {
1538
1610
  error: deserializedError,
1539
1611
  logs: []
1540
1612
  };
1541
- const logResult = await context.D1.prepare(`SELECT * FROM LogTable WHERE instanceId = ? AND stepName = ? ORDER BY logOrder ASC`).bind(instanceId, stepName).all();
1613
+ const logResult = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM LogTable WHERE instanceId = ? AND stepName = ? ORDER BY logOrder ASC`).bind(instanceId, stepName).all(), context.retryConfig);
1542
1614
  if (logResult.results) step.logs = logResult.results.map((logRow) => ({
1543
1615
  instanceId: logRow.instanceId,
1544
1616
  stepName: logRow.stepName,
@@ -1549,10 +1621,10 @@ const createLogAccessor = (context) => {
1549
1621
  return step;
1550
1622
  };
1551
1623
  const getPropertiesKeys = async (instanceId) => {
1552
- const result = await context.D1.prepare(`
1624
+ const result = await retryD1Operation(() => context.D1.prepare(`
1553
1625
  SELECT DISTINCT key, valueType FROM WorkflowProperties
1554
1626
  WHERE tenantId = ? AND (? IS NULL OR instanceId = ?)
1555
- `).bind(context.tenantId, instanceId ?? null, instanceId ?? null).all();
1627
+ `).bind(context.tenantId, instanceId ?? null, instanceId ?? null).all(), context.retryConfig);
1556
1628
  console.log(`debug getPropertiesKeys: ${JSON.stringify(result)}`);
1557
1629
  if (!result.results) return [];
1558
1630
  return result.results.map((row) => ({
@@ -1575,7 +1647,7 @@ const createLogAccessor = (context) => {
1575
1647
  bindings.push(toTime);
1576
1648
  }
1577
1649
  const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
1578
- const result = await context.D1.prepare(`
1650
+ const result = await retryD1Operation(() => context.D1.prepare(`
1579
1651
  SELECT
1580
1652
  workflowType,
1581
1653
  COUNT(*) as workflowCount,
@@ -1618,7 +1690,7 @@ const createLogAccessor = (context) => {
1618
1690
  ${whereClause}
1619
1691
  GROUP BY workflowType
1620
1692
  ORDER BY workflowCount DESC
1621
- `).bind(...bindings).all();
1693
+ `).bind(...bindings).all(), context.retryConfig);
1622
1694
  if (!result.results) return [];
1623
1695
  return result.results.map((row) => ({
1624
1696
  workflowType: row.workflowType,
@@ -1806,7 +1878,8 @@ function createQueueWorkflowContext(options) {
1806
1878
  D1: options.D1,
1807
1879
  externalBlobStorage: options.externalBlobStorage,
1808
1880
  serializer: internalContext.serializer,
1809
- tenantId: "unknown"
1881
+ tenantId: "unknown",
1882
+ retryConfig: options.retryConfig
1810
1883
  });
1811
1884
  const waitingWorkflows = await logAccessor.listWorkflows(250, 0, { workflowStatus: "waiting" }, {
1812
1885
  ignoreTenant: true,
@@ -1853,7 +1926,7 @@ function createQueueWorkflowContext(options) {
1853
1926
  async function createStepContext(context) {
1854
1927
  const instanceId = context.instanceId;
1855
1928
  const reuseSuccessfulSteps = context.reuseSuccessfulSteps ?? true;
1856
- await ensureTables(context.D1);
1929
+ await ensureTables(context.D1, context.retryConfig);
1857
1930
  const step = async (step$1, callback) => {
1858
1931
  const stepNameParam = typeof step$1 === "string" ? step$1 : step$1.name;
1859
1932
  const stepMetadataParam = typeof step$1 === "string" ? void 0 : step$1.metadata;
@@ -1931,9 +2004,9 @@ async function createStepContext(context) {
1931
2004
  const stepStatus$1 = "completed";
1932
2005
  const { data: resultData, externalRef: resultRef } = await serializeWithExternalStorage(result, context.serializer, context.externalBlobStorage);
1933
2006
  const stepError$1 = null;
1934
- await context.D1.prepare(`UPDATE StepTable
2007
+ await retryD1Operation(() => context.D1.prepare(`UPDATE StepTable
1935
2008
  SET stepStatus = ?, endTime = ?, result = ?, error = ?, resultRef = ?, errorRef = ?
1936
- WHERE instanceId = ? AND stepName = ?`).bind(stepStatus$1, endTime, resultData, stepError$1, resultRef, null, instanceId, stepName).run();
2009
+ WHERE instanceId = ? AND stepName = ?`).bind(stepStatus$1, endTime, resultData, stepError$1, resultRef, null, instanceId, stepName).run(), context.retryConfig);
1937
2010
  await logBatcher.destroy();
1938
2011
  return result;
1939
2012
  } catch (error) {
@@ -1941,9 +2014,9 @@ async function createStepContext(context) {
1941
2014
  const stepStatus$1 = "failed";
1942
2015
  const stepResult$1 = null;
1943
2016
  const { data: errorData, externalRef: errorRef } = await serializeWithExternalStorage(error, context.serializer, context.externalBlobStorage);
1944
- await context.D1.prepare(`UPDATE StepTable
2017
+ await retryD1Operation(() => context.D1.prepare(`UPDATE StepTable
1945
2018
  SET stepStatus = ?, endTime = ?, result = ?, error = ?, resultRef = ?, errorRef = ?
1946
- WHERE instanceId = ? AND stepName = ?`).bind(stepStatus$1, endTime, stepResult$1, errorData, null, errorRef, instanceId, stepName).run();
2019
+ WHERE instanceId = ? AND stepName = ?`).bind(stepStatus$1, endTime, stepResult$1, errorData, null, errorRef, instanceId, stepName).run(), context.retryConfig);
1947
2020
  await logBatcher.destroy();
1948
2021
  throw error;
1949
2022
  }
@@ -1962,14 +2035,15 @@ function createWorkflowContext(options) {
1962
2035
  };
1963
2036
  const call = async ({ workflow, input, workflowName, tenantId, parentInstanceId, reuseSuccessfulSteps, triggerId, scheduledInstanceId }) => {
1964
2037
  if (!ensuredTables) {
1965
- await ensureTables(options.D1);
2038
+ await ensureTables(options.D1, options.retryConfig);
1966
2039
  ensuredTables = true;
1967
2040
  }
1968
2041
  const logAccessor = createLogAccessor({
1969
2042
  D1: internalContext.D1,
1970
2043
  externalBlobStorage: internalContext.externalBlobStorage,
1971
2044
  serializer: internalContext.serializer,
1972
- tenantId
2045
+ tenantId,
2046
+ retryConfig: options.retryConfig
1973
2047
  });
1974
2048
  if (scheduledInstanceId) {
1975
2049
  const existingWorkflow = await logAccessor.getWorkflowShallow(scheduledInstanceId);
@@ -2025,7 +2099,8 @@ function createWorkflowContext(options) {
2025
2099
  idFactory: internalContext.idFactory,
2026
2100
  parentInstanceId,
2027
2101
  reuseSuccessfulSteps,
2028
- externalBlobStorage: options.externalBlobStorage
2102
+ externalBlobStorage: options.externalBlobStorage,
2103
+ retryConfig: options.retryConfig
2029
2104
  });
2030
2105
  try {
2031
2106
  const result = await workflow._callback(input, {
@@ -2045,7 +2120,10 @@ function createWorkflowContext(options) {
2045
2120
  }
2046
2121
  },
2047
2122
  async setWorkflowName(newName) {
2048
- await updateWorkflowName({ D1: options.D1 }, instanceId, newName);
2123
+ await updateWorkflowName({
2124
+ D1: options.D1,
2125
+ retryConfig: options.retryConfig
2126
+ }, instanceId, newName);
2049
2127
  },
2050
2128
  async setWorkflowProperty(key, value) {
2051
2129
  await upsertWorkflowProperty({
@@ -2095,10 +2173,10 @@ function createWorkflowContext(options) {
2095
2173
  };
2096
2174
  const retry = async ({ workflow, retryInstanceId, triggerId, retryOptions }) => {
2097
2175
  if (!ensuredTables) {
2098
- await ensureTables(options.D1);
2176
+ await ensureTables(options.D1, options.retryConfig);
2099
2177
  ensuredTables = true;
2100
2178
  }
2101
- const oldRun = await options.D1.prepare(`SELECT input, workflowName, tenantId, inputRef FROM WorkflowTable WHERE instanceId = ? `).bind(retryInstanceId).first();
2179
+ const oldRun = await retryD1Operation(() => options.D1.prepare(`SELECT input, workflowName, tenantId, inputRef FROM WorkflowTable WHERE instanceId = ? `).bind(retryInstanceId).first(), options.retryConfig);
2102
2180
  const oldWorkflowName = oldRun?.workflowName;
2103
2181
  const tenantId = oldRun?.tenantId;
2104
2182
  if (!tenantId) throw new Error(`No tenantId found for instanceId ${retryInstanceId}`);
@@ -2204,4 +2282,4 @@ function createR2ExternalBlobStorage(options) {
2204
2282
  }
2205
2283
 
2206
2284
  //#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 };
2285
+ 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandboostinggmbh/observable-workflows",
3
- "version": "0.19.0-beta.4",
3
+ "version": "0.20.0-beta.5",
4
4
  "description": "My awesome typescript library",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -53,7 +53,7 @@
53
53
  "test": "vitest run",
54
54
  "typecheck": "tsc --noEmit",
55
55
  "format": "prettier --cache --write .",
56
- "release": "bumpp && pnpm publish",
57
- "release:beta": "bumpp --preid beta && pnpm publish --tag beta"
56
+ "release": "npm login && bumpp && pnpm publish",
57
+ "release:beta": "npm login && bumpp --preid beta && pnpm publish --tag beta"
58
58
  }
59
59
  }