@brandboostinggmbh/observable-workflows 0.20.0-beta.4 → 0.20.0-beta.6

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +39 -55
  2. package/dist/index.js +226 -170
  3. package/package.json +3 -3
package/dist/index.d.ts CHANGED
@@ -14,14 +14,15 @@ 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
21
22
  /**
22
23
  * Configuration for retry behavior on D1 operations
23
24
  */
24
- interface RetryConfig$1 {
25
+ interface RetryConfig {
25
26
  /** Maximum number of retry attempts (default: 3) */
26
27
  maxAttempts?: number;
27
28
  /** Initial delay in milliseconds (default: 100) */
@@ -49,7 +50,7 @@ interface RetryConfig$1 {
49
50
  * )
50
51
  * ```
51
52
  */
52
- declare function retryD1Operation<T>(operation: () => Promise<T>, config?: RetryConfig$1): Promise<T>;
53
+ declare function retryD1Operation<T>(operation: () => Promise<T>, config?: RetryConfig): Promise<T>;
53
54
  declare function finalizeWorkflowRecord(options: InternalWorkflowContextOptions, {
54
55
  workflowStatus,
55
56
  endTime,
@@ -61,43 +62,8 @@ declare function finalizeWorkflowRecord(options: InternalWorkflowContextOptions,
61
62
  instanceId: string;
62
63
  result?: any;
63
64
  }): Promise<D1Result<Record<string, unknown>>>;
64
- /**
65
- * Insert a new workflow record into the database.
66
- * Optionally creates workflow dependencies atomically in the same transaction.
67
- *
68
- * @example
69
- * ```typescript
70
- * // Create a workflow with dependencies
71
- * await insertWorkflowRecord(context, {
72
- * instanceId: 'workflow-c',
73
- * workflowType: 'process-data',
74
- * workflowName: 'Data Processing',
75
- * workflowMetadata: {},
76
- * input: { data: 'value' },
77
- * workflowStatus: 'pending',
78
- * startTime: Date.now(),
79
- * tenantId: 'tenant-1',
80
- * dependencies: [
81
- * { instanceId: 'workflow-a' },
82
- * { instanceId: 'workflow-b' }
83
- * ] // This workflow depends on A and B
84
- * })
85
- * ```
86
- */
87
- declare function insertWorkflowRecord(options: InternalWorkflowContextOptions, {
88
- instanceId,
89
- workflowType,
90
- workflowName,
91
- workflowMetadata,
92
- input,
93
- workflowStatus,
94
- startTime,
95
- endTime,
96
- parentInstanceId,
97
- tenantId,
98
- triggerId,
99
- dependencies
100
- }: {
65
+ /** Parameters for preparing workflow insert statements */
66
+ interface PrepareWorkflowInsertParams {
101
67
  /** Unique identifier for this workflow instance */
102
68
  instanceId: string;
103
69
  /** Type/category of the workflow */
@@ -122,11 +88,36 @@ declare function insertWorkflowRecord(options: InternalWorkflowContextOptions, {
122
88
  triggerId?: string | null;
123
89
  /**
124
90
  * Optional array of workflow dependencies that this workflow depends on.
125
- * When provided, dependency relationships are created atomically with the workflow insert.
126
- * This ensures that the workflow and all its dependencies are created in a single transaction.
91
+ * When provided, dependency insert statements are included in the returned statements array.
127
92
  */
128
93
  dependencies?: Array<WorkflowDependency>;
129
- }): Promise<{
94
+ }
95
+ /** Result from preparing workflow insert statements */
96
+
97
+ /**
98
+ * Insert a new workflow record into the database.
99
+ * Optionally creates workflow dependencies atomically in the same transaction.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * // Create a workflow with dependencies
104
+ * await insertWorkflowRecord(context, {
105
+ * instanceId: 'workflow-c',
106
+ * workflowType: 'process-data',
107
+ * workflowName: 'Data Processing',
108
+ * workflowMetadata: {},
109
+ * input: { data: 'value' },
110
+ * workflowStatus: 'pending',
111
+ * startTime: Date.now(),
112
+ * tenantId: 'tenant-1',
113
+ * dependencies: [
114
+ * { instanceId: 'workflow-a' },
115
+ * { instanceId: 'workflow-b' }
116
+ * ] // This workflow depends on A and B
117
+ * })
118
+ * ```
119
+ */
120
+ declare function insertWorkflowRecord(options: InternalWorkflowContextOptions, params: PrepareWorkflowInsertParams): Promise<{
130
121
  success: boolean;
131
122
  meta: any;
132
123
  }>;
@@ -231,6 +222,7 @@ declare function workflowTableRowToWorkflowRun({
231
222
  }): Promise<WorkflowRun>;
232
223
  declare function updateWorkflowName(context: {
233
224
  D1: D1Database;
225
+ retryConfig?: RetryConfig;
234
226
  }, instanceId: string, newWorkflowName: string): Promise<D1Result<Record<string, unknown>>>;
235
227
  /**
236
228
  * Update workflow fields by instanceId. Only provided fields will be updated.
@@ -441,6 +433,8 @@ type StepContextOptions = {
441
433
  reuseSuccessfulSteps?: boolean;
442
434
  /** Optional external blob storage for large data that exceeds D1 size limits */
443
435
  externalBlobStorage?: ExternalBlobStorage;
436
+ /** Optional retry configuration for D1 operations */
437
+ retryConfig?: RetryConfig;
444
438
  };
445
439
  type ConsoleWrapper = {
446
440
  log: (message?: any, ...optionalParams: any[]) => void;
@@ -637,18 +631,6 @@ type ExternalBlobStorage = {
637
631
  get: (id: string) => Promise<string>;
638
632
  delete: (...keys: string[]) => Promise<number>;
639
633
  };
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
- };
652
634
  type WorkflowContextOptions = {
653
635
  D1: D1Database;
654
636
  idFactory?: () => string;
@@ -774,6 +756,7 @@ declare const createCleanupManager: (context: {
774
756
  tenantId: string;
775
757
  serializer?: Serializer;
776
758
  externalBlobStorage?: ExternalBlobStorage;
759
+ retryConfig?: RetryConfig;
777
760
  }) => {
778
761
  countAffectedWorkflows: (config: DeleteConfig, limit: number) => Promise<{
779
762
  count: number;
@@ -820,6 +803,7 @@ declare const createLogAccessor: (context: {
820
803
  tenantId: string;
821
804
  serializer?: Serializer;
822
805
  externalBlobStorage?: ExternalBlobStorage;
806
+ retryConfig?: RetryConfig;
823
807
  }) => {
824
808
  listSteps: {
825
809
  (limit: number, offset: number, instanceId?: string): Promise<Step[]>;
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 retryD1Operation(() => 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 retryD1Operation(() => 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 retryD1Operation(() => 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 retryD1Operation(() => 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,8 +52,8 @@ 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 retryD1Operation(() => 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
59
  if (!hasInputRef || inputHasNotNull) await retryD1Operation(() => db.batch([
@@ -78,14 +78,14 @@ 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 retryD1Operation(() => 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
90
  if (!hasTriggerId) await retryD1Operation(() => db.batch([
91
91
  db.prepare(`CREATE TABLE WorkflowTable_new (
@@ -111,21 +111,21 @@ 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
130
  await retryD1Operation(() => db.prepare(`CREATE TABLE WorkflowTable (
131
131
  instanceId TEXT NOT NULL,
@@ -144,29 +144,29 @@ 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
171
  await retryD1Operation(() => db.prepare(`CREATE TABLE StepTable (
172
172
  instanceId TEXT NOT NULL,
@@ -182,21 +182,21 @@ 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 retryD1Operation(() => 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 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());
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
201
  await retryD1Operation(() => db.prepare(`CREATE TABLE LogTable (
202
202
  instanceId TEXT NOT NULL,
@@ -208,14 +208,14 @@ 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
220
  await retryD1Operation(() => db.prepare(`CREATE TABLE WorkflowProperties (
221
221
  instanceId 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 retryD1Operation(() => 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,23 +341,24 @@ 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 retryD1Operation(() => 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 retryD1Operation(() => 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
@@ -397,12 +398,12 @@ function sleep(ms) {
397
398
  }
398
399
  /**
399
400
  * Retry a D1 operation with exponential backoff for transient network errors
400
- *
401
+ *
401
402
  * @param operation - The async operation to retry
402
403
  * @param config - Retry configuration options
403
404
  * @returns The result of the operation
404
405
  * @throws The last error if all retry attempts fail
405
- *
406
+ *
406
407
  * @example
407
408
  * ```typescript
408
409
  * const result = await retryD1Operation(
@@ -436,11 +437,60 @@ async function finalizeWorkflowRecord(options, { workflowStatus, endTime, instan
436
437
  const { data: resultData, externalRef: resultRef } = await serializeWithExternalStorage(result, options.serializer, options.externalBlobStorage);
437
438
  return retryD1Operation(() => options.D1.prepare(`UPDATE WorkflowTable
438
439
  SET workflowStatus = ?, endTime = ?, result = ?, resultRef = ?
439
- WHERE instanceId = ?`).bind(workflowStatus, endTime, resultData, resultRef, instanceId).run());
440
+ WHERE instanceId = ?`).bind(workflowStatus, endTime, resultData, resultRef, instanceId).run(), options.retryConfig);
440
441
  }
441
442
  return retryD1Operation(() => options.D1.prepare(`UPDATE WorkflowTable
442
443
  SET workflowStatus = ?, endTime = ?
443
- WHERE instanceId = ?`).bind(workflowStatus, endTime, instanceId).run());
444
+ WHERE instanceId = ?`).bind(workflowStatus, endTime, instanceId).run(), options.retryConfig);
445
+ }
446
+ /**
447
+ * Prepare D1 statements for inserting a workflow record and its dependencies.
448
+ * This function handles input serialization (including external blob storage for large inputs)
449
+ * and returns prepared statements that can be executed individually or batched with other statements.
450
+ *
451
+ * This is an internal helper used by `insertWorkflowRecord` and `enqueueWorkflowBatch` to enable
452
+ * efficient batching of multiple workflow inserts into a single D1 batch operation.
453
+ *
454
+ * @param options - The internal workflow context options containing D1, serializer, etc.
455
+ * @param params - The workflow record parameters
456
+ * @returns Object containing prepared statements, instanceId, and workflowType
457
+ *
458
+ * @example
459
+ * ```typescript
460
+ * // Prepare statements for multiple workflows, then batch execute
461
+ * const prepared = await Promise.all([
462
+ * prepareWorkflowInsertStatements(options, workflow1Params),
463
+ * prepareWorkflowInsertStatements(options, workflow2Params),
464
+ * ])
465
+ * const allStatements = prepared.flatMap(p => p.statements)
466
+ * await options.D1.batch(allStatements)
467
+ * ```
468
+ *
469
+ * @internal
470
+ */
471
+ async function prepareWorkflowInsertStatements(options, { instanceId, workflowType, workflowName, workflowMetadata, input, workflowStatus, startTime, endTime, parentInstanceId, tenantId, triggerId, dependencies }) {
472
+ const { data: inputData, externalRef: inputRef } = await serializeWithExternalStorage(input, options.serializer, options.externalBlobStorage);
473
+ const insertWorkflowStatement = options.D1.prepare(`INSERT INTO WorkflowTable
474
+ (instanceId, workflowType, workflowName, workflowMetadata, input, inputRef, tenantId, workflowStatus, startTime, endTime, parentInstanceId, triggerId)
475
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, workflowType, workflowName, options.serializer.serialize(workflowMetadata), inputData, inputRef, tenantId, workflowStatus, startTime, endTime ?? null, parentInstanceId ?? null, triggerId ?? null);
476
+ if (!dependencies || dependencies.length === 0) return {
477
+ statements: [insertWorkflowStatement],
478
+ instanceId,
479
+ workflowType
480
+ };
481
+ const createdAt = Date.now();
482
+ const dependencyStatements = dependencies.map((dep) => prepareWorkflowDependencyStatement({
483
+ D1: options.D1,
484
+ dependencyWorkflowId: dep.instanceId,
485
+ dependentWorkflowId: instanceId,
486
+ tenantId,
487
+ createdAt
488
+ }));
489
+ return {
490
+ statements: [insertWorkflowStatement, ...dependencyStatements],
491
+ instanceId,
492
+ workflowType
493
+ };
444
494
  }
445
495
  /**
446
496
  * Insert a new workflow record into the database.
@@ -465,27 +515,16 @@ async function finalizeWorkflowRecord(options, { workflowStatus, endTime, instan
465
515
  * })
466
516
  * ```
467
517
  */
468
- async function insertWorkflowRecord(options, { instanceId, workflowType, workflowName, workflowMetadata, input, workflowStatus, startTime, endTime, parentInstanceId, tenantId, triggerId, dependencies }) {
469
- const { data: inputData, externalRef: inputRef } = await serializeWithExternalStorage(input, options.serializer, options.externalBlobStorage);
470
- const insertWorkflowStatement = options.D1.prepare(`INSERT INTO WorkflowTable
471
- (instanceId, workflowType, workflowName, workflowMetadata, input, inputRef, tenantId, workflowStatus, startTime, endTime, parentInstanceId, triggerId)
472
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, workflowType, workflowName, options.serializer.serialize(workflowMetadata), inputData, inputRef, tenantId, workflowStatus, startTime, endTime ?? null, parentInstanceId ?? null, triggerId ?? null);
473
- if (!dependencies || dependencies.length === 0) {
474
- const result = await retryD1Operation(() => insertWorkflowStatement.run());
518
+ async function insertWorkflowRecord(options, params) {
519
+ const { statements } = await prepareWorkflowInsertStatements(options, params);
520
+ if (statements.length === 1) {
521
+ const result = await retryD1Operation(() => statements[0].run(), options.retryConfig);
475
522
  return {
476
523
  success: result.success,
477
524
  meta: result.meta
478
525
  };
479
526
  }
480
- const createdAt = Date.now();
481
- const dependencyStatements = dependencies.map((dep) => prepareWorkflowDependencyStatement({
482
- D1: options.D1,
483
- dependencyWorkflowId: dep.instanceId,
484
- dependentWorkflowId: instanceId,
485
- tenantId,
486
- createdAt
487
- }));
488
- const results = await retryD1Operation(() => options.D1.batch([insertWorkflowStatement, ...dependencyStatements]));
527
+ const results = await retryD1Operation(() => options.D1.batch(statements), options.retryConfig);
489
528
  const allSucceeded = results.every((result) => result.success);
490
529
  return {
491
530
  success: allSucceeded,
@@ -495,10 +534,10 @@ async function insertWorkflowRecord(options, { instanceId, workflowType, workflo
495
534
  function insertStepRecordFull(context, { instanceId, name, status, metadata, startTime, endTime, result, error, resultRef, errorRef }) {
496
535
  return retryD1Operation(() => context.D1.prepare(`INSERT INTO StepTable
497
536
  (instanceId, stepName, stepStatus, stepMetadata, startTime, endTime, result, error, resultRef, errorRef, tenantId)
498
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, name, status, metadata, startTime, endTime, result, error, resultRef ?? null, errorRef ?? null, context.tenantId).run());
537
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, name, status, metadata, startTime, endTime, result, error, resultRef ?? null, errorRef ?? null, context.tenantId).run(), context.retryConfig);
499
538
  }
500
539
  async function getStepRecord(context, stepName, instanceId) {
501
- const existingStep = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM StepTable WHERE instanceId = ? AND stepName = ? AND tenantId = ?`).bind(instanceId, stepName, context.tenantId).first());
540
+ const existingStep = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM StepTable WHERE instanceId = ? AND stepName = ? AND tenantId = ?`).bind(instanceId, stepName, context.tenantId).first(), context.retryConfig);
502
541
  const row = existingStep;
503
542
  if (row) {
504
543
  const result = await deserializeWithExternalStorage(row.result, row.resultRef, context.serializer, context.externalBlobStorage);
@@ -574,7 +613,7 @@ function pushLogToDB(options, { instanceId, stepName, message, timestamp, type,
574
613
  const truncatedMessage = truncateLogMessage(message);
575
614
  return retryD1Operation(() => options.D1.prepare(`INSERT INTO LogTable
576
615
  (instanceId, stepName, log, timestamp, type, logOrder, tenantId)
577
- VALUES (?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, stepName, truncatedMessage, timestamp, type, logOrder, tenantId).run());
616
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, stepName, truncatedMessage, timestamp, type, logOrder, tenantId).run(), options.retryConfig);
578
617
  }
579
618
  var LogBatcher = class {
580
619
  batch = [];
@@ -614,7 +653,7 @@ var LogBatcher = class {
614
653
  (instanceId, stepName, log, timestamp, type, logOrder, tenantId)
615
654
  VALUES (?, ?, ?, ?, ?, ?, ?)`).bind(entry.instanceId, entry.stepName, entry.message, entry.timestamp, entry.type, entry.logOrder, entry.tenantId));
616
655
  return await this.options.D1.batch(statements);
617
- });
656
+ }, this.options.retryConfig);
618
657
  } catch (error) {
619
658
  console.error("Batch log insert failed, falling back to individual inserts:", error);
620
659
  await Promise.all(logsToFlush.map((entry) => pushLogToDB(this.options, entry)));
@@ -744,7 +783,7 @@ async function workflowTableRowToWorkflowRun({ row, serializer, externalBlobStor
744
783
  async function updateWorkflowName(context, instanceId, newWorkflowName) {
745
784
  return await retryD1Operation(() => context.D1.prepare(`UPDATE WorkflowTable
746
785
  SET workflowName = ?
747
- WHERE instanceId = ?`).bind(newWorkflowName, instanceId).run());
786
+ WHERE instanceId = ?`).bind(newWorkflowName, instanceId).run(), context.retryConfig);
748
787
  }
749
788
  /**
750
789
  * Update workflow fields by instanceId. Only provided fields will be updated.
@@ -821,7 +860,7 @@ async function updateWorkflow(context, instanceId, updates) {
821
860
  };
822
861
  bindings.push(instanceId);
823
862
  const sql = `UPDATE WorkflowTable SET ${setClauses.join(", ")} WHERE instanceId = ?`;
824
- return await retryD1Operation(() => context.D1.prepare(sql).bind(...bindings).run());
863
+ return await retryD1Operation(() => context.D1.prepare(sql).bind(...bindings).run(), context.retryConfig);
825
864
  }
826
865
  /**
827
866
  * Serialize a workflow property value for storage
@@ -856,7 +895,7 @@ async function upsertWorkflowProperty({ context, instanceId, key, value, tenantI
856
895
  const { serializedValue, valueType } = serializeWorkflowPropertyValue(value);
857
896
  const res = await retryD1Operation(() => context.D1.prepare(`INSERT OR REPLACE INTO WorkflowProperties
858
897
  (instanceId, key, value, valueType, tenantId)
859
- VALUES (?, ?, ?, ?, ?)`).bind(instanceId, key, serializedValue, valueType, tenantId).run());
898
+ VALUES (?, ?, ?, ?, ?)`).bind(instanceId, key, serializedValue, valueType, tenantId).run(), context.retryConfig);
860
899
  if (res.error) {
861
900
  console.error("Error inserting workflow property:", res.error);
862
901
  return false;
@@ -917,7 +956,7 @@ async function createWorkflowDependency({ context, dependencyWorkflowId, depende
917
956
  dependencyWorkflowId,
918
957
  dependentWorkflowId,
919
958
  tenantId
920
- }).run());
959
+ }).run(), context.retryConfig);
921
960
  }
922
961
  /**
923
962
  * Create multiple workflow dependencies in a single transaction.
@@ -950,7 +989,7 @@ async function createWorkflowDependencies({ context, dependencies, tenantId }) {
950
989
  tenantId,
951
990
  createdAt
952
991
  }));
953
- return await retryD1Operation(() => context.D1.batch(statements));
992
+ return await retryD1Operation(() => context.D1.batch(statements), context.retryConfig);
954
993
  }
955
994
  /**
956
995
  * Delete a specific workflow dependency relationship.
@@ -968,7 +1007,7 @@ async function createWorkflowDependencies({ context, dependencies, tenantId }) {
968
1007
  */
969
1008
  async function deleteWorkflowDependency({ context, dependencyWorkflowId, dependentWorkflowId, tenantId }) {
970
1009
  return await retryD1Operation(() => context.D1.prepare(`DELETE FROM WorkflowDependencies
971
- WHERE dependencyWorkflowId = ? AND dependentWorkflowId = ? AND tenantId = ?`).bind(dependencyWorkflowId, dependentWorkflowId, tenantId).run());
1010
+ WHERE dependencyWorkflowId = ? AND dependentWorkflowId = ? AND tenantId = ?`).bind(dependencyWorkflowId, dependentWorkflowId, tenantId).run(), context.retryConfig);
972
1011
  }
973
1012
 
974
1013
  //#endregion
@@ -996,7 +1035,7 @@ const createCleanupManager = (context) => {
996
1035
  AND workflowStatus IN (${statusPlaceholders})
997
1036
  ORDER BY instanceId
998
1037
  LIMIT ?
999
- `).bind(context.tenantId, config.deleteWorkflowsOlderThanDays, ...statuses, limit).all());
1038
+ `).bind(context.tenantId, config.deleteWorkflowsOlderThanDays, ...statuses, limit).all(), context.retryConfig);
1000
1039
  return result.results;
1001
1040
  };
1002
1041
  const getAffectedSteps = async (config, limit) => {
@@ -1015,7 +1054,7 @@ const createCleanupManager = (context) => {
1015
1054
  LIMIT ?
1016
1055
  )
1017
1056
  ORDER BY s.instanceId, s.stepName
1018
- `).bind(context.tenantId, config.deleteWorkflowsOlderThanDays, ...statuses, limit).all());
1057
+ `).bind(context.tenantId, config.deleteWorkflowsOlderThanDays, ...statuses, limit).all(), context.retryConfig);
1019
1058
  return result.results;
1020
1059
  };
1021
1060
  const collectExternalStorageKeys = (workflows, steps) => {
@@ -1068,7 +1107,7 @@ const createCleanupManager = (context) => {
1068
1107
  }
1069
1108
  if (deletedWorkflows.length > 0) {
1070
1109
  console.log(`Proceeding to delete workflows from the database...`);
1071
- await retryD1Operation(() => context.D1.prepare(`PRAGMA foreign_keys = ON`).run());
1110
+ await retryD1Operation(() => context.D1.prepare(`PRAGMA foreign_keys = ON`).run(), context.retryConfig);
1072
1111
  let totalDeletedCount = 0;
1073
1112
  for (let i = 0; i < deletedWorkflows.length; i += 100) {
1074
1113
  const batch = deletedWorkflows.slice(i, i + 100);
@@ -1076,7 +1115,7 @@ const createCleanupManager = (context) => {
1076
1115
  const result = await retryD1Operation(() => context.D1.prepare(`
1077
1116
  DELETE FROM WorkflowTable
1078
1117
  WHERE instanceId IN (${workflowIds})
1079
- `).bind(...batch.map((w) => w.instanceId)).run());
1118
+ `).bind(...batch.map((w) => w.instanceId)).run(), context.retryConfig);
1080
1119
  totalDeletedCount += result.meta.changes || 0;
1081
1120
  }
1082
1121
  return {
@@ -1092,7 +1131,7 @@ const createCleanupManager = (context) => {
1092
1131
  };
1093
1132
  };
1094
1133
  const cleanupOrphanedSteps = async (config, limit) => {
1095
- await retryD1Operation(() => context.D1.prepare(`PRAGMA foreign_keys = ON`).run());
1134
+ await retryD1Operation(() => context.D1.prepare(`PRAGMA foreign_keys = ON`).run(), context.retryConfig);
1096
1135
  const orphanedSteps = await retryD1Operation(() => context.D1.prepare(`
1097
1136
  SELECT s.instanceId, s.stepName, s.errorRef, s.resultRef
1098
1137
  FROM StepTable s
@@ -1103,7 +1142,7 @@ const createCleanupManager = (context) => {
1103
1142
  )
1104
1143
  ORDER BY s.instanceId, s.stepName
1105
1144
  LIMIT ?
1106
- `).bind(context.tenantId, context.tenantId, limit).all());
1145
+ `).bind(context.tenantId, context.tenantId, limit).all(), context.retryConfig);
1107
1146
  const orphanedStepResults = orphanedSteps.results;
1108
1147
  let deletedExternalStorageKeysCount = 0;
1109
1148
  if (config.deleteRefsFromExternalStorage && context.externalBlobStorage && orphanedStepResults.length > 0) {
@@ -1123,7 +1162,7 @@ const createCleanupManager = (context) => {
1123
1162
  DELETE FROM StepTable
1124
1163
  WHERE tenantId = ?
1125
1164
  AND instanceId IN (${instanceIds})
1126
- `).bind(context.tenantId, ...batch.map((s) => s.instanceId)).run());
1165
+ `).bind(context.tenantId, ...batch.map((s) => s.instanceId)).run(), context.retryConfig);
1127
1166
  totalDeletedSteps += deletedStepsResult.meta.changes || 0;
1128
1167
  }
1129
1168
  return {
@@ -1139,7 +1178,7 @@ const createCleanupManager = (context) => {
1139
1178
  };
1140
1179
  };
1141
1180
  const cleanupOrphanedLogs = async (limit) => {
1142
- await retryD1Operation(() => context.D1.prepare(`PRAGMA foreign_keys = ON`).run());
1181
+ await retryD1Operation(() => context.D1.prepare(`PRAGMA foreign_keys = ON`).run(), context.retryConfig);
1143
1182
  const deletedLogsResult = await retryD1Operation(() => context.D1.prepare(`
1144
1183
  DELETE FROM LogTable
1145
1184
  WHERE tenantId = ?
@@ -1155,7 +1194,7 @@ const createCleanupManager = (context) => {
1155
1194
  ))
1156
1195
  )
1157
1196
  LIMIT ?
1158
- `).bind(context.tenantId, context.tenantId, context.tenantId, limit).run());
1197
+ `).bind(context.tenantId, context.tenantId, context.tenantId, limit).run(), context.retryConfig);
1159
1198
  return { deletedOrphanedLogs: deletedLogsResult.meta.changes || 0 };
1160
1199
  };
1161
1200
  return {
@@ -1343,7 +1382,7 @@ const createLogAccessor = (context) => {
1343
1382
  const limitClause = limit !== void 0 && actualOffset !== void 0 ? "LIMIT ? OFFSET ?" : "";
1344
1383
  const sql = `SELECT * FROM StepTable ${whereClause} ORDER BY startTime ASC ${limitClause}`;
1345
1384
  if (limit !== void 0 && actualOffset !== void 0) bindings.push(limit, actualOffset);
1346
- result = await retryD1Operation(() => context.D1.prepare(sql).bind(...bindings).all());
1385
+ result = await retryD1Operation(() => context.D1.prepare(sql).bind(...bindings).all(), context.retryConfig);
1347
1386
  if (result.results) {
1348
1387
  const steps = await Promise.all(result.results.map(async (row) => {
1349
1388
  let deserializedResult = null;
@@ -1370,7 +1409,7 @@ const createLogAccessor = (context) => {
1370
1409
  const listWorkflows = async (limit, offset, filter, options) => {
1371
1410
  const { sql, bindings } = buildFilteredWorkflowQuery(filter, { ignoreTenant: options?.ignoreTenant });
1372
1411
  if (options?.debugLogs) console.log("listWorkflows SQL:", sql, "Bindings:", JSON.stringify(bindings));
1373
- const result = await retryD1Operation(() => context.D1.prepare(sql).bind(...bindings, limit, offset).all());
1412
+ const result = await retryD1Operation(() => context.D1.prepare(sql).bind(...bindings, limit, offset).all(), context.retryConfig);
1374
1413
  if (options?.debugLogs) console.log("listWorkflows SQL Query executed");
1375
1414
  if (result.results) {
1376
1415
  const workflows = await Promise.all(result.results.map((row) => workflowTableRowToWorkflowRun({
@@ -1387,7 +1426,7 @@ const createLogAccessor = (context) => {
1387
1426
  for (let i = 0; i < instanceIds.length; i += BATCH_SIZE) {
1388
1427
  const batchIds = instanceIds.slice(i, i + BATCH_SIZE);
1389
1428
  const placeholders = batchIds.map(() => "?").join(", ");
1390
- const propertiesResult = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowProperties WHERE instanceId IN (${placeholders})`).bind(...batchIds).all());
1429
+ const propertiesResult = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowProperties WHERE instanceId IN (${placeholders})`).bind(...batchIds).all(), context.retryConfig);
1391
1430
  if (propertiesResult.results) for (const row of propertiesResult.results) {
1392
1431
  const property = {
1393
1432
  key: row.key,
@@ -1411,7 +1450,7 @@ const createLogAccessor = (context) => {
1411
1450
  const dependenciesResult = await retryD1Operation(() => context.D1.prepare(`SELECT w.*, wd.createdAt, wd.dependentWorkflowId
1412
1451
  FROM WorkflowDependencies wd
1413
1452
  JOIN WorkflowTable w ON wd.dependencyWorkflowId = w.instanceId
1414
- WHERE wd.dependentWorkflowId IN (${placeholders})`).bind(...batchIds).all());
1453
+ WHERE wd.dependentWorkflowId IN (${placeholders})`).bind(...batchIds).all(), context.retryConfig);
1415
1454
  if (dependenciesResult.results) for (const row of dependenciesResult.results) {
1416
1455
  const depWorkflow = await workflowTableRowToWorkflowRun({
1417
1456
  row,
@@ -1441,7 +1480,7 @@ const createLogAccessor = (context) => {
1441
1480
  const dependentsResult = await retryD1Operation(() => context.D1.prepare(`SELECT w.*, wd.createdAt, wd.dependencyWorkflowId
1442
1481
  FROM WorkflowDependencies wd
1443
1482
  JOIN WorkflowTable w ON wd.dependentWorkflowId = w.instanceId
1444
- WHERE wd.dependencyWorkflowId IN (${placeholders})`).bind(...batchIds).all());
1483
+ WHERE wd.dependencyWorkflowId IN (${placeholders})`).bind(...batchIds).all(), context.retryConfig);
1445
1484
  if (dependentsResult.results) for (const row of dependentsResult.results) {
1446
1485
  const depWorkflow = await workflowTableRowToWorkflowRun({
1447
1486
  row,
@@ -1466,7 +1505,7 @@ const createLogAccessor = (context) => {
1466
1505
  return [];
1467
1506
  };
1468
1507
  const getWorkflowByParentId = async (parentInstanceId) => {
1469
- const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowTable WHERE parentInstanceId = ? AND tenantId = ?`).bind(parentInstanceId, context.tenantId).all());
1508
+ const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowTable WHERE parentInstanceId = ? AND tenantId = ?`).bind(parentInstanceId, context.tenantId).all(), context.retryConfig);
1470
1509
  if (result.results) {
1471
1510
  const workflows = await Promise.all(result.results.map((row) => workflowTableRowToWorkflowRun({
1472
1511
  row,
@@ -1478,7 +1517,7 @@ const createLogAccessor = (context) => {
1478
1517
  return null;
1479
1518
  };
1480
1519
  const getWorkflowByTriggerId = async (triggerId) => {
1481
- const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowTable WHERE triggerId = ? AND tenantId = ?`).bind(triggerId, context.tenantId).first());
1520
+ const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowTable WHERE triggerId = ? AND tenantId = ?`).bind(triggerId, context.tenantId).first(), context.retryConfig);
1482
1521
  if (result) {
1483
1522
  const workflow = await workflowTableRowToWorkflowRun({
1484
1523
  row: result,
@@ -1490,12 +1529,12 @@ const createLogAccessor = (context) => {
1490
1529
  return null;
1491
1530
  };
1492
1531
  const getWorkflowTypesByTenantId = async (tenantId) => {
1493
- const result = await retryD1Operation(() => context.D1.prepare(`SELECT DISTINCT workflowType FROM WorkflowTable WHERE tenantId = ?`).bind(tenantId).all());
1532
+ const result = await retryD1Operation(() => context.D1.prepare(`SELECT DISTINCT workflowType FROM WorkflowTable WHERE tenantId = ?`).bind(tenantId).all(), context.retryConfig);
1494
1533
  if (!result.results) return [];
1495
1534
  return result.results.map((row) => row.workflowType);
1496
1535
  };
1497
1536
  const getWorkflowProperties = async (instanceId) => {
1498
- const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowProperties WHERE instanceId = ?`).bind(instanceId).all());
1537
+ const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowProperties WHERE instanceId = ?`).bind(instanceId).all(), context.retryConfig);
1499
1538
  if (!result.results) return [];
1500
1539
  return result.results.map((row) => ({
1501
1540
  key: row.key,
@@ -1505,7 +1544,7 @@ const createLogAccessor = (context) => {
1505
1544
  };
1506
1545
  /** This function gets the basic data of a workflow, without populating any of it's complex fields */
1507
1546
  const getWorkflowShallow = async (instanceId, options) => {
1508
- const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowTable WHERE instanceId = ? AND tenantId = ?`).bind(instanceId, context.tenantId).first());
1547
+ const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM WorkflowTable WHERE instanceId = ? AND tenantId = ?`).bind(instanceId, context.tenantId).first(), context.retryConfig);
1509
1548
  if (!result) return null;
1510
1549
  const workflow = await workflowTableRowToWorkflowRun({
1511
1550
  row: result,
@@ -1536,7 +1575,7 @@ const createLogAccessor = (context) => {
1536
1575
  retryWorkflow.isRetryOf = workflow;
1537
1576
  });
1538
1577
  workflow.isRetryOf = parentWorkflow;
1539
- const allLogs = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM LogTable WHERE instanceId = ? ORDER BY timestamp ASC, logOrder ASC`).bind(instanceId).all());
1578
+ const allLogs = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM LogTable WHERE instanceId = ? ORDER BY timestamp ASC, logOrder ASC`).bind(instanceId).all(), context.retryConfig);
1540
1579
  const logs = allLogs.results?.map((logRow) => ({
1541
1580
  instanceId: logRow.instanceId,
1542
1581
  stepName: logRow.stepName,
@@ -1555,7 +1594,7 @@ const createLogAccessor = (context) => {
1555
1594
  const dependenciesResult = await retryD1Operation(() => context.D1.prepare(`SELECT w.*, wd.createdAt
1556
1595
  FROM WorkflowDependencies wd
1557
1596
  JOIN WorkflowTable w ON wd.dependencyWorkflowId = w.instanceId
1558
- WHERE wd.dependentWorkflowId = ? AND wd.tenantId = ?`).bind(instanceId, context.tenantId).all());
1597
+ WHERE wd.dependentWorkflowId = ? AND wd.tenantId = ?`).bind(instanceId, context.tenantId).all(), context.retryConfig);
1559
1598
  if (dependenciesResult.results) workflow.dependencies = await Promise.all(dependenciesResult.results.map(async (row) => {
1560
1599
  const depWorkflow = await workflowTableRowToWorkflowRun({
1561
1600
  row,
@@ -1574,7 +1613,7 @@ const createLogAccessor = (context) => {
1574
1613
  const dependentsResult = await retryD1Operation(() => context.D1.prepare(`SELECT w.*, wd.createdAt
1575
1614
  FROM WorkflowDependencies wd
1576
1615
  JOIN WorkflowTable w ON wd.dependentWorkflowId = w.instanceId
1577
- WHERE wd.dependencyWorkflowId = ? AND wd.tenantId = ?`).bind(instanceId, context.tenantId).all());
1616
+ WHERE wd.dependencyWorkflowId = ? AND wd.tenantId = ?`).bind(instanceId, context.tenantId).all(), context.retryConfig);
1578
1617
  if (dependentsResult.results) workflow.dependents = await Promise.all(dependentsResult.results.map(async (row) => {
1579
1618
  const depWorkflow = await workflowTableRowToWorkflowRun({
1580
1619
  row,
@@ -1592,7 +1631,7 @@ const createLogAccessor = (context) => {
1592
1631
  return workflow;
1593
1632
  };
1594
1633
  const getStep = async (instanceId, stepName) => {
1595
- const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM StepTable WHERE instanceId = ? AND stepName = ? AND tenantId = ?`).bind(instanceId, stepName, context.tenantId).all());
1634
+ const result = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM StepTable WHERE instanceId = ? AND stepName = ? AND tenantId = ?`).bind(instanceId, stepName, context.tenantId).all(), context.retryConfig);
1596
1635
  if (!result.results || result.results.length === 0) return null;
1597
1636
  const row = result.results[0];
1598
1637
  if (!row) return null;
@@ -1609,7 +1648,7 @@ const createLogAccessor = (context) => {
1609
1648
  error: deserializedError,
1610
1649
  logs: []
1611
1650
  };
1612
- const logResult = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM LogTable WHERE instanceId = ? AND stepName = ? ORDER BY logOrder ASC`).bind(instanceId, stepName).all());
1651
+ const logResult = await retryD1Operation(() => context.D1.prepare(`SELECT * FROM LogTable WHERE instanceId = ? AND stepName = ? ORDER BY logOrder ASC`).bind(instanceId, stepName).all(), context.retryConfig);
1613
1652
  if (logResult.results) step.logs = logResult.results.map((logRow) => ({
1614
1653
  instanceId: logRow.instanceId,
1615
1654
  stepName: logRow.stepName,
@@ -1623,7 +1662,7 @@ const createLogAccessor = (context) => {
1623
1662
  const result = await retryD1Operation(() => context.D1.prepare(`
1624
1663
  SELECT DISTINCT key, valueType FROM WorkflowProperties
1625
1664
  WHERE tenantId = ? AND (? IS NULL OR instanceId = ?)
1626
- `).bind(context.tenantId, instanceId ?? null, instanceId ?? null).all());
1665
+ `).bind(context.tenantId, instanceId ?? null, instanceId ?? null).all(), context.retryConfig);
1627
1666
  console.log(`debug getPropertiesKeys: ${JSON.stringify(result)}`);
1628
1667
  if (!result.results) return [];
1629
1668
  return result.results.map((row) => ({
@@ -1689,7 +1728,7 @@ const createLogAccessor = (context) => {
1689
1728
  ${whereClause}
1690
1729
  GROUP BY workflowType
1691
1730
  ORDER BY workflowCount DESC
1692
- `).bind(...bindings).all());
1731
+ `).bind(...bindings).all(), context.retryConfig);
1693
1732
  if (!result.results) return [];
1694
1733
  return result.results.map((row) => ({
1695
1734
  workflowType: row.workflowType,
@@ -1783,68 +1822,79 @@ function createQueueWorkflowContext(options) {
1783
1822
  queueIdentifier: options.queueIdentifier
1784
1823
  });
1785
1824
  };
1825
+ /**
1826
+ * Enqueue multiple workflows in a single batch operation.
1827
+ *
1828
+ * This method is optimized for efficiency: it prepares all workflow insert statements
1829
+ * in parallel (which may involve external blob storage for large inputs), then executes
1830
+ * them in a single D1 batch operation, followed by a single queue batch send.
1831
+ *
1832
+ * **Network requests:** 2 + M, where M is the number of workflows with large inputs
1833
+ * that require external blob storage.
1834
+ *
1835
+ * **D1 batch limit:** Cloudflare D1 has a limit of ~100 statements per batch.
1836
+ * If you have many workflows with dependencies, this limit may be exceeded.
1837
+ * For very large batches, consider splitting into smaller chunks.
1838
+ *
1839
+ * @param workflows - Array of workflow items to enqueue
1840
+ * @returns Array of scheduled workflow stubs in the same order as inputs
1841
+ */
1786
1842
  const enqueueWorkflowBatch = async (workflows) => {
1787
- const results = await Promise.all(workflows.map(async ({ workflow, tenantId, input, initialName, dependencies }) => {
1843
+ const startTime = Date.now();
1844
+ console.log(`enqueueWorkflowBatch: Starting batch of ${workflows.length} workflows`);
1845
+ const preparedWorkflows = await Promise.all(workflows.map(async ({ workflow, tenantId, input, initialName, dependencies }) => {
1788
1846
  const instanceId = idFactory();
1789
1847
  const triggerId = idFactory();
1790
- const startTime = Date.now();
1791
- if (dependencies && dependencies.length > 0) {
1792
- await insertWorkflowRecord(internalContext, {
1793
- instanceId,
1794
- workflowType: workflow.workflowType,
1795
- workflowName: initialName,
1796
- workflowMetadata: workflow.metadata,
1797
- input,
1798
- workflowStatus: "waiting",
1799
- startTime,
1800
- endTime: null,
1801
- parentInstanceId: void 0,
1802
- tenantId,
1803
- triggerId,
1804
- dependencies
1805
- });
1806
- return {
1807
- instanceId,
1808
- workflowType: workflow.workflowType,
1809
- shouldQueue: false,
1810
- queueMessage: null
1811
- };
1812
- } else {
1813
- await insertWorkflowRecord(internalContext, {
1814
- instanceId,
1815
- workflowType: workflow.workflowType,
1816
- workflowName: initialName,
1817
- workflowMetadata: workflow.metadata,
1818
- input,
1819
- workflowStatus: "scheduled",
1820
- startTime,
1821
- endTime: null,
1822
- parentInstanceId: void 0,
1823
- tenantId,
1824
- triggerId
1825
- });
1826
- return {
1827
- instanceId,
1828
- workflowType: workflow.workflowType,
1829
- shouldQueue: true,
1830
- queueMessage: {
1831
- type: "workflow-run",
1832
- workflowType: workflow.workflowType,
1833
- workflowName: initialName,
1834
- input,
1835
- tenantId,
1836
- triggerId,
1837
- queueIdentifier: options.queueIdentifier,
1838
- scheduledInstanceId: instanceId
1839
- }
1840
- };
1841
- }
1848
+ const hasDependencies = dependencies && dependencies.length > 0;
1849
+ const prepared = await prepareWorkflowInsertStatements(internalContext, {
1850
+ instanceId,
1851
+ workflowType: workflow.workflowType,
1852
+ workflowName: initialName,
1853
+ workflowMetadata: workflow.metadata,
1854
+ input,
1855
+ workflowStatus: hasDependencies ? "waiting" : "scheduled",
1856
+ startTime,
1857
+ endTime: null,
1858
+ parentInstanceId: void 0,
1859
+ tenantId,
1860
+ triggerId,
1861
+ dependencies
1862
+ });
1863
+ return {
1864
+ ...prepared,
1865
+ tenantId,
1866
+ triggerId,
1867
+ initialName,
1868
+ shouldQueue: !hasDependencies
1869
+ };
1842
1870
  }));
1843
- const messagesToQueue = results.filter((r) => r.shouldQueue && r.queueMessage).map((r) => ({ body: r.queueMessage }));
1844
- if (messagesToQueue.length > 0) await options.QUEUE.sendBatch(messagesToQueue);
1845
- return results.map((r) => ({
1846
- instanceId: r.instanceId,
1847
- workflowType: r.workflowType
1871
+ const workflowsWithDeps = preparedWorkflows.filter((p) => !p.shouldQueue).length;
1872
+ const workflowsWithoutDeps = preparedWorkflows.filter((p) => p.shouldQueue).length;
1873
+ console.log(`enqueueWorkflowBatch: Prepared ${preparedWorkflows.length} workflows (${workflowsWithoutDeps} to queue, ${workflowsWithDeps} waiting on dependencies)`);
1874
+ const allStatements = preparedWorkflows.flatMap((p) => p.statements);
1875
+ if (allStatements.length > 0) {
1876
+ console.log(`enqueueWorkflowBatch: Executing D1 batch with ${allStatements.length} statements`);
1877
+ await retryD1Operation(() => options.D1.batch(allStatements), options.retryConfig);
1878
+ }
1879
+ const messagesToQueue = preparedWorkflows.filter((p) => p.shouldQueue).map((p) => ({ body: {
1880
+ type: "workflow-run",
1881
+ workflowType: p.workflowType,
1882
+ workflowName: p.initialName,
1883
+ input: workflows.find((w) => w.workflow.workflowType === p.workflowType && w.initialName === p.initialName)?.input,
1884
+ tenantId: p.tenantId,
1885
+ triggerId: p.triggerId,
1886
+ queueIdentifier: options.queueIdentifier,
1887
+ scheduledInstanceId: p.instanceId
1888
+ } }));
1889
+ if (messagesToQueue.length > 0) {
1890
+ console.log(`enqueueWorkflowBatch: Sending ${messagesToQueue.length} messages to queue`);
1891
+ await options.QUEUE.sendBatch(messagesToQueue);
1892
+ }
1893
+ const elapsed = Date.now() - startTime;
1894
+ console.log(`enqueueWorkflowBatch: Completed in ${elapsed}ms`);
1895
+ return preparedWorkflows.map((p) => ({
1896
+ instanceId: p.instanceId,
1897
+ workflowType: p.workflowType
1848
1898
  }));
1849
1899
  };
1850
1900
  const handleWorkflowQueueMessage = async ({ message, env, workflowResolver }) => {
@@ -1877,7 +1927,8 @@ function createQueueWorkflowContext(options) {
1877
1927
  D1: options.D1,
1878
1928
  externalBlobStorage: options.externalBlobStorage,
1879
1929
  serializer: internalContext.serializer,
1880
- tenantId: "unknown"
1930
+ tenantId: "unknown",
1931
+ retryConfig: options.retryConfig
1881
1932
  });
1882
1933
  const waitingWorkflows = await logAccessor.listWorkflows(250, 0, { workflowStatus: "waiting" }, {
1883
1934
  ignoreTenant: true,
@@ -1924,7 +1975,7 @@ function createQueueWorkflowContext(options) {
1924
1975
  async function createStepContext(context) {
1925
1976
  const instanceId = context.instanceId;
1926
1977
  const reuseSuccessfulSteps = context.reuseSuccessfulSteps ?? true;
1927
- await ensureTables(context.D1);
1978
+ await ensureTables(context.D1, context.retryConfig);
1928
1979
  const step = async (step$1, callback) => {
1929
1980
  const stepNameParam = typeof step$1 === "string" ? step$1 : step$1.name;
1930
1981
  const stepMetadataParam = typeof step$1 === "string" ? void 0 : step$1.metadata;
@@ -2004,7 +2055,7 @@ async function createStepContext(context) {
2004
2055
  const stepError$1 = null;
2005
2056
  await retryD1Operation(() => context.D1.prepare(`UPDATE StepTable
2006
2057
  SET stepStatus = ?, endTime = ?, result = ?, error = ?, resultRef = ?, errorRef = ?
2007
- WHERE instanceId = ? AND stepName = ?`).bind(stepStatus$1, endTime, resultData, stepError$1, resultRef, null, instanceId, stepName).run());
2058
+ WHERE instanceId = ? AND stepName = ?`).bind(stepStatus$1, endTime, resultData, stepError$1, resultRef, null, instanceId, stepName).run(), context.retryConfig);
2008
2059
  await logBatcher.destroy();
2009
2060
  return result;
2010
2061
  } catch (error) {
@@ -2014,7 +2065,7 @@ async function createStepContext(context) {
2014
2065
  const { data: errorData, externalRef: errorRef } = await serializeWithExternalStorage(error, context.serializer, context.externalBlobStorage);
2015
2066
  await retryD1Operation(() => context.D1.prepare(`UPDATE StepTable
2016
2067
  SET stepStatus = ?, endTime = ?, result = ?, error = ?, resultRef = ?, errorRef = ?
2017
- WHERE instanceId = ? AND stepName = ?`).bind(stepStatus$1, endTime, stepResult$1, errorData, null, errorRef, instanceId, stepName).run());
2068
+ WHERE instanceId = ? AND stepName = ?`).bind(stepStatus$1, endTime, stepResult$1, errorData, null, errorRef, instanceId, stepName).run(), context.retryConfig);
2018
2069
  await logBatcher.destroy();
2019
2070
  throw error;
2020
2071
  }
@@ -2033,14 +2084,15 @@ function createWorkflowContext(options) {
2033
2084
  };
2034
2085
  const call = async ({ workflow, input, workflowName, tenantId, parentInstanceId, reuseSuccessfulSteps, triggerId, scheduledInstanceId }) => {
2035
2086
  if (!ensuredTables) {
2036
- await ensureTables(options.D1);
2087
+ await ensureTables(options.D1, options.retryConfig);
2037
2088
  ensuredTables = true;
2038
2089
  }
2039
2090
  const logAccessor = createLogAccessor({
2040
2091
  D1: internalContext.D1,
2041
2092
  externalBlobStorage: internalContext.externalBlobStorage,
2042
2093
  serializer: internalContext.serializer,
2043
- tenantId
2094
+ tenantId,
2095
+ retryConfig: options.retryConfig
2044
2096
  });
2045
2097
  if (scheduledInstanceId) {
2046
2098
  const existingWorkflow = await logAccessor.getWorkflowShallow(scheduledInstanceId);
@@ -2096,7 +2148,8 @@ function createWorkflowContext(options) {
2096
2148
  idFactory: internalContext.idFactory,
2097
2149
  parentInstanceId,
2098
2150
  reuseSuccessfulSteps,
2099
- externalBlobStorage: options.externalBlobStorage
2151
+ externalBlobStorage: options.externalBlobStorage,
2152
+ retryConfig: options.retryConfig
2100
2153
  });
2101
2154
  try {
2102
2155
  const result = await workflow._callback(input, {
@@ -2116,7 +2169,10 @@ function createWorkflowContext(options) {
2116
2169
  }
2117
2170
  },
2118
2171
  async setWorkflowName(newName) {
2119
- await updateWorkflowName({ D1: options.D1 }, instanceId, newName);
2172
+ await updateWorkflowName({
2173
+ D1: options.D1,
2174
+ retryConfig: options.retryConfig
2175
+ }, instanceId, newName);
2120
2176
  },
2121
2177
  async setWorkflowProperty(key, value) {
2122
2178
  await upsertWorkflowProperty({
@@ -2166,10 +2222,10 @@ function createWorkflowContext(options) {
2166
2222
  };
2167
2223
  const retry = async ({ workflow, retryInstanceId, triggerId, retryOptions }) => {
2168
2224
  if (!ensuredTables) {
2169
- await ensureTables(options.D1);
2225
+ await ensureTables(options.D1, options.retryConfig);
2170
2226
  ensuredTables = true;
2171
2227
  }
2172
- const oldRun = await retryD1Operation(() => options.D1.prepare(`SELECT input, workflowName, tenantId, inputRef FROM WorkflowTable WHERE instanceId = ? `).bind(retryInstanceId).first());
2228
+ const oldRun = await retryD1Operation(() => options.D1.prepare(`SELECT input, workflowName, tenantId, inputRef FROM WorkflowTable WHERE instanceId = ? `).bind(retryInstanceId).first(), options.retryConfig);
2173
2229
  const oldWorkflowName = oldRun?.workflowName;
2174
2230
  const tenantId = oldRun?.tenantId;
2175
2231
  if (!tenantId) throw new Error(`No tenantId found for instanceId ${retryInstanceId}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandboostinggmbh/observable-workflows",
3
- "version": "0.20.0-beta.4",
3
+ "version": "0.20.0-beta.6",
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
  }