@brandboostinggmbh/observable-workflows 0.20.0-beta.5 → 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.
package/dist/index.d.ts CHANGED
@@ -62,43 +62,8 @@ declare function finalizeWorkflowRecord(options: InternalWorkflowContextOptions,
62
62
  instanceId: string;
63
63
  result?: any;
64
64
  }): Promise<D1Result<Record<string, unknown>>>;
65
- /**
66
- * Insert a new workflow record into the database.
67
- * Optionally creates workflow dependencies atomically in the same transaction.
68
- *
69
- * @example
70
- * ```typescript
71
- * // Create a workflow with dependencies
72
- * await insertWorkflowRecord(context, {
73
- * instanceId: 'workflow-c',
74
- * workflowType: 'process-data',
75
- * workflowName: 'Data Processing',
76
- * workflowMetadata: {},
77
- * input: { data: 'value' },
78
- * workflowStatus: 'pending',
79
- * startTime: Date.now(),
80
- * tenantId: 'tenant-1',
81
- * dependencies: [
82
- * { instanceId: 'workflow-a' },
83
- * { instanceId: 'workflow-b' }
84
- * ] // This workflow depends on A and B
85
- * })
86
- * ```
87
- */
88
- declare function insertWorkflowRecord(options: InternalWorkflowContextOptions, {
89
- instanceId,
90
- workflowType,
91
- workflowName,
92
- workflowMetadata,
93
- input,
94
- workflowStatus,
95
- startTime,
96
- endTime,
97
- parentInstanceId,
98
- tenantId,
99
- triggerId,
100
- dependencies
101
- }: {
65
+ /** Parameters for preparing workflow insert statements */
66
+ interface PrepareWorkflowInsertParams {
102
67
  /** Unique identifier for this workflow instance */
103
68
  instanceId: string;
104
69
  /** Type/category of the workflow */
@@ -123,11 +88,36 @@ declare function insertWorkflowRecord(options: InternalWorkflowContextOptions, {
123
88
  triggerId?: string | null;
124
89
  /**
125
90
  * Optional array of workflow dependencies that this workflow depends on.
126
- * When provided, dependency relationships are created atomically with the workflow insert.
127
- * 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.
128
92
  */
129
93
  dependencies?: Array<WorkflowDependency>;
130
- }): 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<{
131
121
  success: boolean;
132
122
  meta: any;
133
123
  }>;
package/dist/index.js CHANGED
@@ -444,6 +444,55 @@ async function finalizeWorkflowRecord(options, { workflowStatus, endTime, instan
444
444
  WHERE instanceId = ?`).bind(workflowStatus, endTime, instanceId).run(), options.retryConfig);
445
445
  }
446
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
+ };
494
+ }
495
+ /**
447
496
  * Insert a new workflow record into the database.
448
497
  * Optionally creates workflow dependencies atomically in the same transaction.
449
498
  *
@@ -466,27 +515,16 @@ async function finalizeWorkflowRecord(options, { workflowStatus, endTime, instan
466
515
  * })
467
516
  * ```
468
517
  */
469
- async function insertWorkflowRecord(options, { instanceId, workflowType, workflowName, workflowMetadata, input, workflowStatus, startTime, endTime, parentInstanceId, tenantId, triggerId, dependencies }) {
470
- const { data: inputData, externalRef: inputRef } = await serializeWithExternalStorage(input, options.serializer, options.externalBlobStorage);
471
- const insertWorkflowStatement = options.D1.prepare(`INSERT INTO WorkflowTable
472
- (instanceId, workflowType, workflowName, workflowMetadata, input, inputRef, tenantId, workflowStatus, startTime, endTime, parentInstanceId, triggerId)
473
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).bind(instanceId, workflowType, workflowName, options.serializer.serialize(workflowMetadata), inputData, inputRef, tenantId, workflowStatus, startTime, endTime ?? null, parentInstanceId ?? null, triggerId ?? null);
474
- if (!dependencies || dependencies.length === 0) {
475
- const result = await retryD1Operation(() => insertWorkflowStatement.run(), options.retryConfig);
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);
476
522
  return {
477
523
  success: result.success,
478
524
  meta: result.meta
479
525
  };
480
526
  }
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
- const results = await retryD1Operation(() => options.D1.batch([insertWorkflowStatement, ...dependencyStatements]), options.retryConfig);
527
+ const results = await retryD1Operation(() => options.D1.batch(statements), options.retryConfig);
490
528
  const allSucceeded = results.every((result) => result.success);
491
529
  return {
492
530
  success: allSucceeded,
@@ -1784,68 +1822,79 @@ function createQueueWorkflowContext(options) {
1784
1822
  queueIdentifier: options.queueIdentifier
1785
1823
  });
1786
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
+ */
1787
1842
  const enqueueWorkflowBatch = async (workflows) => {
1788
- 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 }) => {
1789
1846
  const instanceId = idFactory();
1790
1847
  const triggerId = idFactory();
1791
- const startTime = Date.now();
1792
- if (dependencies && dependencies.length > 0) {
1793
- await insertWorkflowRecord(internalContext, {
1794
- instanceId,
1795
- workflowType: workflow.workflowType,
1796
- workflowName: initialName,
1797
- workflowMetadata: workflow.metadata,
1798
- input,
1799
- workflowStatus: "waiting",
1800
- startTime,
1801
- endTime: null,
1802
- parentInstanceId: void 0,
1803
- tenantId,
1804
- triggerId,
1805
- dependencies
1806
- });
1807
- return {
1808
- instanceId,
1809
- workflowType: workflow.workflowType,
1810
- shouldQueue: false,
1811
- queueMessage: null
1812
- };
1813
- } else {
1814
- await insertWorkflowRecord(internalContext, {
1815
- instanceId,
1816
- workflowType: workflow.workflowType,
1817
- workflowName: initialName,
1818
- workflowMetadata: workflow.metadata,
1819
- input,
1820
- workflowStatus: "scheduled",
1821
- startTime,
1822
- endTime: null,
1823
- parentInstanceId: void 0,
1824
- tenantId,
1825
- triggerId
1826
- });
1827
- return {
1828
- instanceId,
1829
- workflowType: workflow.workflowType,
1830
- shouldQueue: true,
1831
- queueMessage: {
1832
- type: "workflow-run",
1833
- workflowType: workflow.workflowType,
1834
- workflowName: initialName,
1835
- input,
1836
- tenantId,
1837
- triggerId,
1838
- queueIdentifier: options.queueIdentifier,
1839
- scheduledInstanceId: instanceId
1840
- }
1841
- };
1842
- }
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
+ };
1843
1870
  }));
1844
- const messagesToQueue = results.filter((r) => r.shouldQueue && r.queueMessage).map((r) => ({ body: r.queueMessage }));
1845
- if (messagesToQueue.length > 0) await options.QUEUE.sendBatch(messagesToQueue);
1846
- return results.map((r) => ({
1847
- instanceId: r.instanceId,
1848
- 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
1849
1898
  }));
1850
1899
  };
1851
1900
  const handleWorkflowQueueMessage = async ({ message, env, workflowResolver }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandboostinggmbh/observable-workflows",
3
- "version": "0.20.0-beta.5",
3
+ "version": "0.20.0-beta.6",
4
4
  "description": "My awesome typescript library",
5
5
  "type": "module",
6
6
  "license": "MIT",