pg-workflows 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/dist/client.entry.cjs +39 -9
- package/dist/client.entry.d.cts +30 -1
- package/dist/client.entry.d.ts +30 -1
- package/dist/client.entry.js +1 -1
- package/dist/client.entry.js.map +7 -7
- package/dist/index.cjs +331 -16
- package/dist/index.d.cts +52 -2
- package/dist/index.d.ts +52 -2
- package/dist/index.js +300 -9
- package/dist/index.js.map +12 -10
- package/dist/shared/{chunk-ahxqsytt.js → chunk-5xswmve7.js} +41 -11
- package/dist/shared/chunk-5xswmve7.js.map +16 -0
- package/package.json +11 -1
- package/dist/shared/chunk-ahxqsytt.js.map +0 -16
package/dist/index.cjs
CHANGED
|
@@ -65,6 +65,7 @@ var __export = (target, all) => {
|
|
|
65
65
|
var exports_src = {};
|
|
66
66
|
__export(exports_src, {
|
|
67
67
|
workflow: () => workflow,
|
|
68
|
+
otelPlugin: () => otelPlugin,
|
|
68
69
|
createWorkflowRef: () => createWorkflowRef,
|
|
69
70
|
WorkflowStatus: () => WorkflowStatus,
|
|
70
71
|
WorkflowRunNotFoundError: () => WorkflowRunNotFoundError,
|
|
@@ -83,6 +84,8 @@ var import_pg_boss = require("pg-boss");
|
|
|
83
84
|
var PAUSE_EVENT_NAME = "__internal_pause";
|
|
84
85
|
var WORKFLOW_RUN_QUEUE_NAME = "workflow-run";
|
|
85
86
|
var WORKFLOW_RUN_DLQ_QUEUE_NAME = "workflow_run_dlq";
|
|
87
|
+
var SCHEDULE_QUEUE_PREFIX = "__pgw_schedule_";
|
|
88
|
+
var scheduleQueueNameFor = (workflowId) => `${SCHEDULE_QUEUE_PREFIX}${workflowId}`;
|
|
86
89
|
var DEFAULT_PGBOSS_SCHEMA = "pgboss_v12_pgworkflow";
|
|
87
90
|
var MAX_WORKFLOW_ID_LENGTH = 256;
|
|
88
91
|
var MAX_RESOURCE_ID_LENGTH = 256;
|
|
@@ -94,7 +97,7 @@ var isInvokeChildWorkflowTimelineEntry = (entry) => !!entry && typeof entry ===
|
|
|
94
97
|
|
|
95
98
|
// src/db/migration.ts
|
|
96
99
|
var MIGRATION_LOCK_ID = 738291645;
|
|
97
|
-
var CURRENT_SCHEMA_VERSION =
|
|
100
|
+
var CURRENT_SCHEMA_VERSION = 5;
|
|
98
101
|
async function runMigrations(db) {
|
|
99
102
|
if (await isSchemaUpToDate(db)) {
|
|
100
103
|
return;
|
|
@@ -157,6 +160,9 @@ async function runMigrations(db) {
|
|
|
157
160
|
commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS parent_step_id varchar(256)");
|
|
158
161
|
commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS parent_resource_id varchar(256)");
|
|
159
162
|
}
|
|
163
|
+
if (currentVersion < 5) {
|
|
164
|
+
commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS scheduled_at timestamp with time zone");
|
|
165
|
+
}
|
|
160
166
|
if (currentVersion === 0) {
|
|
161
167
|
commands.push(`INSERT INTO workflow_schema_version (version) VALUES (${CURRENT_SCHEMA_VERSION})`);
|
|
162
168
|
} else {
|
|
@@ -222,7 +228,8 @@ function mapRowToWorkflowRun(row) {
|
|
|
222
228
|
idempotencyKey: row.idempotency_key,
|
|
223
229
|
parentRunId: row.parent_run_id,
|
|
224
230
|
parentStepId: row.parent_step_id,
|
|
225
|
-
parentResourceId: row.parent_resource_id
|
|
231
|
+
parentResourceId: row.parent_resource_id,
|
|
232
|
+
scheduledAt: row.scheduled_at ? new Date(row.scheduled_at) : null
|
|
226
233
|
};
|
|
227
234
|
}
|
|
228
235
|
async function insertWorkflowRun({
|
|
@@ -236,7 +243,8 @@ async function insertWorkflowRun({
|
|
|
236
243
|
idempotencyKey,
|
|
237
244
|
parentRunId,
|
|
238
245
|
parentStepId,
|
|
239
|
-
parentResourceId
|
|
246
|
+
parentResourceId,
|
|
247
|
+
scheduledAt
|
|
240
248
|
}, db) {
|
|
241
249
|
const runId = generateKSUID("run");
|
|
242
250
|
const now = new Date;
|
|
@@ -256,9 +264,10 @@ async function insertWorkflowRun({
|
|
|
256
264
|
idempotency_key,
|
|
257
265
|
parent_run_id,
|
|
258
266
|
parent_step_id,
|
|
259
|
-
parent_resource_id
|
|
267
|
+
parent_resource_id,
|
|
268
|
+
scheduled_at
|
|
260
269
|
)
|
|
261
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
|
270
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
|
262
271
|
ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
|
|
263
272
|
RETURNING *`, [
|
|
264
273
|
runId,
|
|
@@ -276,7 +285,8 @@ async function insertWorkflowRun({
|
|
|
276
285
|
idempotencyKey ?? null,
|
|
277
286
|
parentRunId ?? null,
|
|
278
287
|
parentStepId ?? null,
|
|
279
|
-
parentResourceId ?? null
|
|
288
|
+
parentResourceId ?? null,
|
|
289
|
+
scheduledAt ?? null
|
|
280
290
|
]);
|
|
281
291
|
if (result.rows[0]) {
|
|
282
292
|
return { run: mapRowToWorkflowRun(result.rows[0]), created: true };
|
|
@@ -305,6 +315,23 @@ async function getWorkflowRun({
|
|
|
305
315
|
}
|
|
306
316
|
return mapRowToWorkflowRun(run);
|
|
307
317
|
}
|
|
318
|
+
async function getWorkflowLastRun({
|
|
319
|
+
workflowId,
|
|
320
|
+
resourceId
|
|
321
|
+
}, db) {
|
|
322
|
+
const result = resourceId ? await db.executeSql(`SELECT * FROM workflow_runs
|
|
323
|
+
WHERE workflow_id = $1 AND resource_id = $2
|
|
324
|
+
ORDER BY created_at DESC
|
|
325
|
+
LIMIT 1`, [workflowId, resourceId]) : await db.executeSql(`SELECT * FROM workflow_runs
|
|
326
|
+
WHERE workflow_id = $1
|
|
327
|
+
ORDER BY created_at DESC
|
|
328
|
+
LIMIT 1`, [workflowId]);
|
|
329
|
+
const run = result.rows[0];
|
|
330
|
+
if (!run) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
return mapRowToWorkflowRun(run);
|
|
334
|
+
}
|
|
308
335
|
async function updateWorkflowRun({
|
|
309
336
|
runId,
|
|
310
337
|
resourceId,
|
|
@@ -852,7 +879,9 @@ function createWorkflowRef(id, options) {
|
|
|
852
879
|
handler,
|
|
853
880
|
inputSchema: options?.inputSchema,
|
|
854
881
|
timeout: defineOptions?.timeout,
|
|
855
|
-
retries: defineOptions?.retries
|
|
882
|
+
retries: defineOptions?.retries,
|
|
883
|
+
schedule: defineOptions?.schedule,
|
|
884
|
+
timezone: defineOptions?.timezone
|
|
856
885
|
});
|
|
857
886
|
Object.defineProperty(ref, "id", { value: id, enumerable: true });
|
|
858
887
|
Object.defineProperty(ref, "inputSchema", {
|
|
@@ -862,12 +891,14 @@ function createWorkflowRef(id, options) {
|
|
|
862
891
|
return ref;
|
|
863
892
|
}
|
|
864
893
|
function createWorkflowFactory(plugins = []) {
|
|
865
|
-
const factory = (id, handler, { inputSchema, timeout, retries } = {}) => ({
|
|
894
|
+
const factory = (id, handler, { inputSchema, timeout, retries, schedule, timezone } = {}) => ({
|
|
866
895
|
id,
|
|
867
896
|
handler,
|
|
868
897
|
inputSchema,
|
|
869
898
|
timeout,
|
|
870
899
|
retries,
|
|
900
|
+
schedule,
|
|
901
|
+
timezone,
|
|
871
902
|
plugins: plugins.length > 0 ? plugins : undefined
|
|
872
903
|
});
|
|
873
904
|
factory.use = (plugin) => createWorkflowFactory([
|
|
@@ -979,6 +1010,56 @@ function parseDuration(duration) {
|
|
|
979
1010
|
return ms;
|
|
980
1011
|
}
|
|
981
1012
|
|
|
1013
|
+
// src/schedule.ts
|
|
1014
|
+
var import_cron_parser = require("cron-parser");
|
|
1015
|
+
var CRON_TOKEN = /^[0-9*/,?\-LW#]+$/;
|
|
1016
|
+
function looksLikeCronString(value) {
|
|
1017
|
+
const tokens = value.trim().split(/\s+/);
|
|
1018
|
+
if (tokens.length !== 5 && tokens.length !== 6)
|
|
1019
|
+
return false;
|
|
1020
|
+
return tokens.every((t) => CRON_TOKEN.test(t));
|
|
1021
|
+
}
|
|
1022
|
+
function validateCronExpression(expression, timezone) {
|
|
1023
|
+
try {
|
|
1024
|
+
import_cron_parser.CronExpressionParser.parse(expression, { tz: timezone });
|
|
1025
|
+
} catch (e) {
|
|
1026
|
+
throw new WorkflowEngineError(`Invalid cron expression "${expression}" (timezone: ${timezone}): ${e instanceof Error ? e.message : String(e)}`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
function durationMsToCron(ms, original) {
|
|
1030
|
+
if (ms < MS_PER_MINUTE) {
|
|
1031
|
+
throw new WorkflowEngineError(`Schedule interval must be at least 1 minute; got ${ms}ms from ${JSON.stringify(original)}`);
|
|
1032
|
+
}
|
|
1033
|
+
if (ms % MS_PER_DAY === 0) {
|
|
1034
|
+
const days = ms / MS_PER_DAY;
|
|
1035
|
+
if (days === 1)
|
|
1036
|
+
return "0 0 * * *";
|
|
1037
|
+
throw cronStepError(original, `${days} days`);
|
|
1038
|
+
}
|
|
1039
|
+
if (ms % MS_PER_HOUR === 0) {
|
|
1040
|
+
const hours = ms / MS_PER_HOUR;
|
|
1041
|
+
if (24 % hours === 0)
|
|
1042
|
+
return `0 */${hours} * * *`;
|
|
1043
|
+
throw cronStepError(original, `${hours} hours`);
|
|
1044
|
+
}
|
|
1045
|
+
const minutes = ms / MS_PER_MINUTE;
|
|
1046
|
+
if (Number.isInteger(minutes) && 60 % minutes === 0)
|
|
1047
|
+
return `*/${minutes} * * * *`;
|
|
1048
|
+
throw cronStepError(original, `${minutes} minutes`);
|
|
1049
|
+
}
|
|
1050
|
+
function cronStepError(original, label) {
|
|
1051
|
+
return new WorkflowEngineError(`Schedule interval ${JSON.stringify(original)} (${label}) doesn't map cleanly to a recurring cron expression. Use a value that divides 60 minutes, 24 hours, or 1 day — or pass an explicit cron string.`);
|
|
1052
|
+
}
|
|
1053
|
+
function resolveSchedule(schedule, timezone) {
|
|
1054
|
+
const tz = timezone ?? "UTC";
|
|
1055
|
+
if (typeof schedule === "string" && looksLikeCronString(schedule)) {
|
|
1056
|
+
validateCronExpression(schedule, tz);
|
|
1057
|
+
return { cron: schedule, timezone: tz };
|
|
1058
|
+
}
|
|
1059
|
+
const ms = parseDuration(schedule);
|
|
1060
|
+
return { cron: durationMsToCron(ms, schedule), timezone: tz };
|
|
1061
|
+
}
|
|
1062
|
+
|
|
982
1063
|
// src/engine.ts
|
|
983
1064
|
var LOG_PREFIX2 = "[WorkflowEngine]";
|
|
984
1065
|
var StepTypeToIcon = {
|
|
@@ -1065,10 +1146,41 @@ class WorkflowEngine {
|
|
|
1065
1146
|
await this.boss.work(WORKFLOW_RUN_DLQ_QUEUE_NAME, { pollingIntervalSeconds: 0.5, batchSize: 1 }, (jobs) => this.handleWorkflowRunDlq(jobs));
|
|
1066
1147
|
this.logger.log(`Worker started for queue ${WORKFLOW_RUN_DLQ_QUEUE_NAME}`);
|
|
1067
1148
|
}
|
|
1149
|
+
if (asEngine) {
|
|
1150
|
+
const scheduled = Array.from(this.workflows.values()).flatMap((wf) => wf.schedule == null ? [] : [{ id: wf.id, resolved: resolveSchedule(wf.schedule, wf.timezone) }]);
|
|
1151
|
+
await Promise.allSettled(scheduled.map(({ id, resolved }) => this.registerWorkflowSchedule(id, resolved).catch((error) => {
|
|
1152
|
+
this.logger.error(`Failed to register schedule for "${id}", skipping`, error instanceof Error ? error : new Error(String(error)), { workflowId: id });
|
|
1153
|
+
})));
|
|
1154
|
+
}
|
|
1068
1155
|
this._started = true;
|
|
1069
1156
|
this.logger.log("Workflow engine started!");
|
|
1070
1157
|
}
|
|
1158
|
+
async registerWorkflowSchedule(workflowId, resolvedSchedule) {
|
|
1159
|
+
const scheduleQueueName = scheduleQueueNameFor(workflowId);
|
|
1160
|
+
await this.boss.createQueue(scheduleQueueName);
|
|
1161
|
+
await this.boss.schedule(scheduleQueueName, resolvedSchedule.cron, null, {
|
|
1162
|
+
tz: resolvedSchedule.timezone
|
|
1163
|
+
});
|
|
1164
|
+
await this.boss.work(scheduleQueueName, { batchSize: 1, includeMetadata: true }, async (jobs) => {
|
|
1165
|
+
const scheduledAt = jobs[0]?.startAfter ?? new Date;
|
|
1166
|
+
try {
|
|
1167
|
+
await this.createWorkflowRun({ workflowId, input: {}, scheduledAt });
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
this.logger.error(`Schedule fire failed to start a run for workflow "${workflowId}"`, error instanceof Error ? error : new Error(String(error)), { workflowId });
|
|
1170
|
+
throw error;
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
this.logger.log(`Schedule registered for workflow "${workflowId}": ${resolvedSchedule.cron} (${resolvedSchedule.timezone})`, { workflowId });
|
|
1174
|
+
}
|
|
1175
|
+
async unscheduleWorkflow(workflowId) {
|
|
1176
|
+
try {
|
|
1177
|
+
await this.boss.unschedule(scheduleQueueNameFor(workflowId));
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
this.logger.error(`Failed to unschedule "${workflowId}"`, error instanceof Error ? error : new Error(String(error)), { workflowId });
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1071
1182
|
async stop() {
|
|
1183
|
+
await Promise.allSettled(Array.from(this.workflows.values()).filter((wf) => wf.schedule != null).map((wf) => this.unscheduleWorkflow(wf.id)));
|
|
1072
1184
|
await this.boss.stop();
|
|
1073
1185
|
if (this._ownsPool) {
|
|
1074
1186
|
await this.pool.end();
|
|
@@ -1081,10 +1193,14 @@ class WorkflowEngine {
|
|
|
1081
1193
|
throw new WorkflowEngineError(`Workflow ${definition.id} is already registered`, definition.id);
|
|
1082
1194
|
}
|
|
1083
1195
|
const { steps } = parseWorkflowHandler(definition.handler);
|
|
1196
|
+
const resolvedSchedule = definition.schedule ? resolveSchedule(definition.schedule, definition.timezone) : undefined;
|
|
1084
1197
|
this.workflows.set(definition.id, {
|
|
1085
1198
|
...definition,
|
|
1086
1199
|
steps
|
|
1087
1200
|
});
|
|
1201
|
+
if (this._started && resolvedSchedule) {
|
|
1202
|
+
await this.registerWorkflowSchedule(definition.id, resolvedSchedule);
|
|
1203
|
+
}
|
|
1088
1204
|
this.logger.log(`Registered workflow "${definition.id}" with steps:`);
|
|
1089
1205
|
for (const step of steps.values()) {
|
|
1090
1206
|
const tags = [];
|
|
@@ -1099,10 +1215,17 @@ class WorkflowEngine {
|
|
|
1099
1215
|
return this;
|
|
1100
1216
|
}
|
|
1101
1217
|
async unregisterWorkflow(workflowId) {
|
|
1218
|
+
const existing = this.workflows.get(workflowId);
|
|
1219
|
+
if (existing?.schedule != null && this._started) {
|
|
1220
|
+
await this.unscheduleWorkflow(workflowId);
|
|
1221
|
+
}
|
|
1102
1222
|
this.workflows.delete(workflowId);
|
|
1103
1223
|
return this;
|
|
1104
1224
|
}
|
|
1105
1225
|
async unregisterAllWorkflows() {
|
|
1226
|
+
if (this._started) {
|
|
1227
|
+
await Promise.allSettled(Array.from(this.workflows.values()).filter((wf) => wf.schedule != null).map((wf) => this.unscheduleWorkflow(wf.id)));
|
|
1228
|
+
}
|
|
1106
1229
|
this.workflows.clear();
|
|
1107
1230
|
return this;
|
|
1108
1231
|
}
|
|
@@ -1152,6 +1275,7 @@ class WorkflowEngine {
|
|
|
1152
1275
|
parentRunId,
|
|
1153
1276
|
parentStepId,
|
|
1154
1277
|
parentResourceId,
|
|
1278
|
+
scheduledAt,
|
|
1155
1279
|
enqueue = true,
|
|
1156
1280
|
db
|
|
1157
1281
|
}) {
|
|
@@ -1185,7 +1309,8 @@ class WorkflowEngine {
|
|
|
1185
1309
|
idempotencyKey,
|
|
1186
1310
|
parentRunId,
|
|
1187
1311
|
parentStepId,
|
|
1188
|
-
parentResourceId
|
|
1312
|
+
parentResourceId,
|
|
1313
|
+
scheduledAt
|
|
1189
1314
|
}, targetDb);
|
|
1190
1315
|
const insertAndEnqueue = async (targetDb) => {
|
|
1191
1316
|
const result = await insertRun(targetDb);
|
|
@@ -1368,6 +1493,14 @@ class WorkflowEngine {
|
|
|
1368
1493
|
}
|
|
1369
1494
|
return run;
|
|
1370
1495
|
}
|
|
1496
|
+
async getWorkflowLastRun({
|
|
1497
|
+
workflowId,
|
|
1498
|
+
resourceId
|
|
1499
|
+
}) {
|
|
1500
|
+
validateWorkflowId(workflowId);
|
|
1501
|
+
validateResourceId(resourceId);
|
|
1502
|
+
return getWorkflowLastRun({ workflowId, resourceId }, this.db);
|
|
1503
|
+
}
|
|
1371
1504
|
async updateRun({
|
|
1372
1505
|
runId,
|
|
1373
1506
|
resourceId,
|
|
@@ -1589,21 +1722,33 @@ class WorkflowEngine {
|
|
|
1589
1722
|
};
|
|
1590
1723
|
let step = { ...baseStep };
|
|
1591
1724
|
const plugins = workflow2.plugins ?? [];
|
|
1592
|
-
for (const plugin of plugins) {
|
|
1593
|
-
const extra = plugin.methods(step);
|
|
1594
|
-
step = { ...step, ...extra };
|
|
1595
|
-
}
|
|
1596
1725
|
const context = {
|
|
1597
1726
|
input: run.input,
|
|
1598
1727
|
workflowId: run.workflowId,
|
|
1599
1728
|
runId: run.id,
|
|
1729
|
+
resourceId: run.resourceId ?? undefined,
|
|
1730
|
+
attempt: run.retryCount,
|
|
1600
1731
|
get timeline() {
|
|
1601
1732
|
return run?.timeline ?? {};
|
|
1602
1733
|
},
|
|
1603
1734
|
logger: this.logger,
|
|
1604
|
-
step
|
|
1735
|
+
step,
|
|
1736
|
+
schedule: run.scheduledAt ? { timestamp: run.scheduledAt } : undefined
|
|
1605
1737
|
};
|
|
1606
|
-
const
|
|
1738
|
+
for (const plugin of plugins) {
|
|
1739
|
+
const extra = plugin.methods(step, context);
|
|
1740
|
+
step = { ...step, ...extra };
|
|
1741
|
+
context.step = step;
|
|
1742
|
+
}
|
|
1743
|
+
let next = () => workflow2.handler(context);
|
|
1744
|
+
for (const plugin of [...plugins].reverse()) {
|
|
1745
|
+
if (plugin.wrap) {
|
|
1746
|
+
const inner = next;
|
|
1747
|
+
const wrap = plugin.wrap;
|
|
1748
|
+
next = () => wrap(context, inner);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
const result = await next();
|
|
1607
1752
|
run = await this.getRun({ runId, resourceId: scopedResourceId });
|
|
1608
1753
|
const isLastParsedStep = run.currentStepId === workflow2.steps[workflow2.steps.length - 1]?.id;
|
|
1609
1754
|
const hasPluginSteps = (workflow2.plugins?.length ?? 0) > 0;
|
|
@@ -2136,6 +2281,176 @@ ${error.stack}` : String(error)
|
|
|
2136
2281
|
}, this.db);
|
|
2137
2282
|
}
|
|
2138
2283
|
}
|
|
2284
|
+
// src/plugins/otel.ts
|
|
2285
|
+
var import_api = require("@opentelemetry/api");
|
|
2286
|
+
var DEFAULT_PREFIX = "pg_workflows";
|
|
2287
|
+
function isCachedHit(timeline, stepId, kind) {
|
|
2288
|
+
const entry = timeline[stepId];
|
|
2289
|
+
if (entry && typeof entry === "object" && "output" in entry && entry.output !== undefined) {
|
|
2290
|
+
return true;
|
|
2291
|
+
}
|
|
2292
|
+
if (kind === "invokeChildWorkflow" && timeline[invokeChildWorkflowTimelineKey(stepId)]) {
|
|
2293
|
+
return true;
|
|
2294
|
+
}
|
|
2295
|
+
return false;
|
|
2296
|
+
}
|
|
2297
|
+
function otelPlugin(options = {}) {
|
|
2298
|
+
const tracer = options.tracer ?? import_api.trace.getTracer("pg-workflows");
|
|
2299
|
+
const prefix = options.spanNamePrefix ?? DEFAULT_PREFIX;
|
|
2300
|
+
const extraAttrs = options.attributes;
|
|
2301
|
+
return {
|
|
2302
|
+
name: "opentelemetry",
|
|
2303
|
+
methods: (step, context) => {
|
|
2304
|
+
const wrapVoidish = (kind, base) => {
|
|
2305
|
+
return async (stepId, ...args) => {
|
|
2306
|
+
if (isCachedHit(context.timeline, stepId, kind)) {
|
|
2307
|
+
return base(stepId, ...args);
|
|
2308
|
+
}
|
|
2309
|
+
const capturedCtx = import_api.context.active();
|
|
2310
|
+
const startTime = new Date;
|
|
2311
|
+
let result;
|
|
2312
|
+
let originalErr;
|
|
2313
|
+
let thrownError;
|
|
2314
|
+
try {
|
|
2315
|
+
result = await base(stepId, ...args);
|
|
2316
|
+
} catch (err) {
|
|
2317
|
+
originalErr = err;
|
|
2318
|
+
thrownError = err instanceof Error ? err : new Error(String(err));
|
|
2319
|
+
}
|
|
2320
|
+
const span = tracer.startSpan(`${prefix}.step.${kind}`, {
|
|
2321
|
+
startTime,
|
|
2322
|
+
attributes: { "step.id": stepId, "step.type": kind }
|
|
2323
|
+
}, capturedCtx);
|
|
2324
|
+
if (thrownError) {
|
|
2325
|
+
span.recordException(thrownError);
|
|
2326
|
+
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: thrownError.message });
|
|
2327
|
+
span.end();
|
|
2328
|
+
throw originalErr;
|
|
2329
|
+
}
|
|
2330
|
+
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
2331
|
+
span.end();
|
|
2332
|
+
return result;
|
|
2333
|
+
};
|
|
2334
|
+
};
|
|
2335
|
+
return {
|
|
2336
|
+
run: async (stepId, handler) => {
|
|
2337
|
+
if (isCachedHit(context.timeline, stepId, "run")) {
|
|
2338
|
+
return step.run(stepId, handler);
|
|
2339
|
+
}
|
|
2340
|
+
const capturedCtx = import_api.context.active();
|
|
2341
|
+
const startTime = new Date;
|
|
2342
|
+
let result;
|
|
2343
|
+
let originalErr;
|
|
2344
|
+
let thrownError;
|
|
2345
|
+
try {
|
|
2346
|
+
result = await step.run(stepId, handler);
|
|
2347
|
+
} catch (err) {
|
|
2348
|
+
originalErr = err;
|
|
2349
|
+
thrownError = err instanceof Error ? err : new Error(String(err));
|
|
2350
|
+
}
|
|
2351
|
+
if (result === undefined && !thrownError) {
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
const span = tracer.startSpan(`${prefix}.step.run`, {
|
|
2355
|
+
startTime,
|
|
2356
|
+
attributes: { "step.id": stepId, "step.type": "run" }
|
|
2357
|
+
}, capturedCtx);
|
|
2358
|
+
if (thrownError) {
|
|
2359
|
+
span.recordException(thrownError);
|
|
2360
|
+
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: thrownError.message });
|
|
2361
|
+
span.end();
|
|
2362
|
+
throw originalErr;
|
|
2363
|
+
}
|
|
2364
|
+
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
2365
|
+
span.end();
|
|
2366
|
+
return result;
|
|
2367
|
+
},
|
|
2368
|
+
waitFor: wrapVoidish("waitFor", step.waitFor),
|
|
2369
|
+
delay: wrapVoidish("delay", step.delay),
|
|
2370
|
+
sleep: wrapVoidish("delay", step.delay),
|
|
2371
|
+
waitUntil: wrapVoidish("waitUntil", step.waitUntil),
|
|
2372
|
+
pause: wrapVoidish("pause", step.pause),
|
|
2373
|
+
poll: async (stepId, conditionFn, pollOptions) => {
|
|
2374
|
+
const capturedCtx = import_api.context.active();
|
|
2375
|
+
const startTime = new Date;
|
|
2376
|
+
let result;
|
|
2377
|
+
let originalErr;
|
|
2378
|
+
let thrownError;
|
|
2379
|
+
try {
|
|
2380
|
+
result = await step.poll(stepId, conditionFn, pollOptions);
|
|
2381
|
+
} catch (err) {
|
|
2382
|
+
originalErr = err;
|
|
2383
|
+
thrownError = err instanceof Error ? err : new Error(String(err));
|
|
2384
|
+
}
|
|
2385
|
+
const span = tracer.startSpan(`${prefix}.step.poll`, {
|
|
2386
|
+
startTime,
|
|
2387
|
+
attributes: { "step.id": stepId, "step.type": "poll" }
|
|
2388
|
+
}, capturedCtx);
|
|
2389
|
+
if (thrownError) {
|
|
2390
|
+
span.recordException(thrownError);
|
|
2391
|
+
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: thrownError.message });
|
|
2392
|
+
span.end();
|
|
2393
|
+
throw originalErr;
|
|
2394
|
+
}
|
|
2395
|
+
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
2396
|
+
span.end();
|
|
2397
|
+
return result;
|
|
2398
|
+
},
|
|
2399
|
+
invokeChildWorkflow: async (stepId, refOrParams, inputArg, optionsArg) => {
|
|
2400
|
+
if (isCachedHit(context.timeline, stepId, "invokeChildWorkflow")) {
|
|
2401
|
+
return step.invokeChildWorkflow(stepId, refOrParams, inputArg, optionsArg);
|
|
2402
|
+
}
|
|
2403
|
+
const capturedCtx = import_api.context.active();
|
|
2404
|
+
const startTime = new Date;
|
|
2405
|
+
let result;
|
|
2406
|
+
let originalErr;
|
|
2407
|
+
let thrownError;
|
|
2408
|
+
try {
|
|
2409
|
+
result = await step.invokeChildWorkflow(stepId, refOrParams, inputArg, optionsArg);
|
|
2410
|
+
} catch (err) {
|
|
2411
|
+
originalErr = err;
|
|
2412
|
+
thrownError = err instanceof Error ? err : new Error(String(err));
|
|
2413
|
+
}
|
|
2414
|
+
const span = tracer.startSpan(`${prefix}.step.invokeChildWorkflow`, {
|
|
2415
|
+
startTime,
|
|
2416
|
+
attributes: { "step.id": stepId, "step.type": "invokeChildWorkflow" }
|
|
2417
|
+
}, capturedCtx);
|
|
2418
|
+
if (thrownError) {
|
|
2419
|
+
span.recordException(thrownError);
|
|
2420
|
+
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: thrownError.message });
|
|
2421
|
+
span.end();
|
|
2422
|
+
throw originalErr;
|
|
2423
|
+
}
|
|
2424
|
+
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
2425
|
+
span.end();
|
|
2426
|
+
return result;
|
|
2427
|
+
}
|
|
2428
|
+
};
|
|
2429
|
+
},
|
|
2430
|
+
wrap: (context, next) => tracer.startActiveSpan(`${prefix}.workflow.run`, {
|
|
2431
|
+
attributes: {
|
|
2432
|
+
"workflow.id": context.workflowId,
|
|
2433
|
+
"workflow.run_id": context.runId,
|
|
2434
|
+
"workflow.attempt": context.attempt,
|
|
2435
|
+
...context.resourceId ? { "workflow.resource_id": context.resourceId } : {},
|
|
2436
|
+
...extraAttrs ? extraAttrs(context) : {}
|
|
2437
|
+
}
|
|
2438
|
+
}, async (span) => {
|
|
2439
|
+
try {
|
|
2440
|
+
const result = await next();
|
|
2441
|
+
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
2442
|
+
return result;
|
|
2443
|
+
} catch (err) {
|
|
2444
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2445
|
+
span.recordException(error);
|
|
2446
|
+
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: error.message });
|
|
2447
|
+
throw err;
|
|
2448
|
+
} finally {
|
|
2449
|
+
span.end();
|
|
2450
|
+
}
|
|
2451
|
+
})
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2139
2454
|
|
|
2140
|
-
//# debugId=
|
|
2455
|
+
//# debugId=12905C6BC12C3A1664756E2164756E21
|
|
2141
2456
|
//# sourceMappingURL=index.js.map
|
package/dist/index.d.cts
CHANGED
|
@@ -23,6 +23,8 @@ type WorkflowRun = {
|
|
|
23
23
|
parentRunId: string | null;
|
|
24
24
|
parentStepId: string | null;
|
|
25
25
|
parentResourceId: string | null;
|
|
26
|
+
/** Set when the run was started by a recurring schedule; the timestamp the schedule fired. */
|
|
27
|
+
scheduledAt: Date | null;
|
|
26
28
|
};
|
|
27
29
|
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
28
30
|
type DurationObject = {
|
|
@@ -33,6 +35,7 @@ type DurationObject = {
|
|
|
33
35
|
seconds?: number;
|
|
34
36
|
};
|
|
35
37
|
type Duration = string | DurationObject;
|
|
38
|
+
type Schedule = string | Exclude<Duration, string>;
|
|
36
39
|
declare enum WorkflowStatus {
|
|
37
40
|
PENDING = "pending",
|
|
38
41
|
RUNNING = "running",
|
|
@@ -63,6 +66,18 @@ type WorkflowOptions<I extends InputParameters> = {
|
|
|
63
66
|
timeout?: number;
|
|
64
67
|
retries?: number;
|
|
65
68
|
inputSchema?: I;
|
|
69
|
+
/**
|
|
70
|
+
* Recurring schedule. Accepts a cron expression (`'0 9 * * 1-5'`),
|
|
71
|
+
* a duration string (`'5m'`, `'1 hour'`), or a `DurationObject`.
|
|
72
|
+
*/
|
|
73
|
+
schedule?: Schedule;
|
|
74
|
+
/** IANA timezone for cron expressions. Defaults to UTC. Ignored for duration-based schedules. */
|
|
75
|
+
timezone?: string;
|
|
76
|
+
};
|
|
77
|
+
/** Metadata about a scheduled fire, exposed on `ctx.schedule` for runs triggered by a schedule. */
|
|
78
|
+
type ScheduleContext = {
|
|
79
|
+
/** Time the schedule fired this run. */
|
|
80
|
+
timestamp: Date;
|
|
66
81
|
};
|
|
67
82
|
type StepBaseContext = {
|
|
68
83
|
run: <T>(stepId: string, handler: () => Promise<T>) => Promise<T>;
|
|
@@ -126,7 +141,13 @@ interface WorkflowPlugin<
|
|
|
126
141
|
TStepExt = object
|
|
127
142
|
> {
|
|
128
143
|
name: string;
|
|
129
|
-
methods: (step: TStepBase) => TStepExt;
|
|
144
|
+
methods: (step: TStepBase, context: WorkflowContext) => TStepExt;
|
|
145
|
+
/**
|
|
146
|
+
* Optional middleware around the workflow handler call. Composes in
|
|
147
|
+
* registration order — the first plugin passed to `.use()` wraps everything
|
|
148
|
+
* inside. Implementations MUST call `next()` exactly once.
|
|
149
|
+
*/
|
|
150
|
+
wrap?: (context: WorkflowContext, next: () => Promise<unknown>) => Promise<unknown>;
|
|
130
151
|
}
|
|
131
152
|
type WorkflowContext<
|
|
132
153
|
TInput extends InputParameters = InputParameters,
|
|
@@ -136,8 +157,14 @@ type WorkflowContext<
|
|
|
136
157
|
step: TStep;
|
|
137
158
|
workflowId: string;
|
|
138
159
|
runId: string;
|
|
160
|
+
/** Tenant/scope identifier set when the run was started, if any. */
|
|
161
|
+
resourceId?: string;
|
|
162
|
+
/** Zero-based retry attempt number (= `run.retryCount`). */
|
|
163
|
+
attempt: number;
|
|
139
164
|
timeline: Record<string, unknown>;
|
|
140
165
|
logger: WorkflowLogger;
|
|
166
|
+
/** Set only for runs triggered by a recurring schedule. */
|
|
167
|
+
schedule?: ScheduleContext;
|
|
141
168
|
};
|
|
142
169
|
type WorkflowDefinition<TInput extends InputParameters = InputParameters> = {
|
|
143
170
|
id: string;
|
|
@@ -146,6 +173,8 @@ type WorkflowDefinition<TInput extends InputParameters = InputParameters> = {
|
|
|
146
173
|
inputSchema?: TInput;
|
|
147
174
|
timeout?: number;
|
|
148
175
|
retries?: number;
|
|
176
|
+
schedule?: Schedule;
|
|
177
|
+
timezone?: string;
|
|
149
178
|
plugins?: WorkflowPlugin[];
|
|
150
179
|
};
|
|
151
180
|
type StepInternalDefinition = {
|
|
@@ -321,6 +350,8 @@ declare class WorkflowEngine {
|
|
|
321
350
|
batchSize?: number;
|
|
322
351
|
heartbeatSeconds?: number;
|
|
323
352
|
}): Promise<void>;
|
|
353
|
+
private registerWorkflowSchedule;
|
|
354
|
+
private unscheduleWorkflow;
|
|
324
355
|
stop(): Promise<void>;
|
|
325
356
|
registerWorkflow(definition: WorkflowDefinition<InputParameters>): Promise<WorkflowEngine>;
|
|
326
357
|
unregisterWorkflow(workflowId: string): Promise<WorkflowEngine>;
|
|
@@ -373,6 +404,15 @@ declare class WorkflowEngine {
|
|
|
373
404
|
exclusiveLock?: boolean;
|
|
374
405
|
db?: Db;
|
|
375
406
|
}): Promise<WorkflowRun>;
|
|
407
|
+
/**
|
|
408
|
+
* Fetch the most recently created run for a workflow, optionally scoped to a
|
|
409
|
+
* `resourceId`. Useful for cron-style incremental syncs where the next run
|
|
410
|
+
* needs the previous run's completion timestamp as a cursor.
|
|
411
|
+
*/
|
|
412
|
+
getWorkflowLastRun({ workflowId, resourceId }: {
|
|
413
|
+
workflowId: string;
|
|
414
|
+
resourceId?: string;
|
|
415
|
+
}): Promise<WorkflowRun | null>;
|
|
376
416
|
updateRun({ runId, resourceId, data, expectedStatuses }: {
|
|
377
417
|
runId: string;
|
|
378
418
|
resourceId?: string;
|
|
@@ -452,4 +492,14 @@ declare class WorkflowEngineError extends Error {
|
|
|
452
492
|
declare class WorkflowRunNotFoundError extends WorkflowEngineError {
|
|
453
493
|
constructor(runId?: string, workflowId?: string);
|
|
454
494
|
}
|
|
455
|
-
|
|
495
|
+
import { AttributeValue, Tracer } from "@opentelemetry/api";
|
|
496
|
+
type OtelPluginOptions = {
|
|
497
|
+
/** Tracer to use. Defaults to `trace.getTracer('pg-workflows')`. */
|
|
498
|
+
tracer?: Tracer;
|
|
499
|
+
/** Prefix for all span names. Defaults to `pg_workflows`. */
|
|
500
|
+
spanNamePrefix?: string;
|
|
501
|
+
/** Extra attributes merged onto the workflow.run span. */
|
|
502
|
+
attributes?: (context: WorkflowContext) => Record<string, AttributeValue>;
|
|
503
|
+
};
|
|
504
|
+
declare function otelPlugin(options?: OtelPluginOptions): WorkflowPlugin<StepBaseContext, object>;
|
|
505
|
+
export { workflow, otelPlugin, createWorkflowRef, WorkflowStatus, WorkflowRunProgress, WorkflowRunNotFoundError, WorkflowRun, WorkflowRef, WorkflowPlugin, WorkflowOptions, WorkflowLogger, WorkflowEngineOptions, WorkflowEngineError, WorkflowEngine, WorkflowDefinition, WorkflowContext, WorkflowClientOptions, WorkflowClient, StepBaseContext, StartWorkflowOptions, ScheduleContext, Schedule, OtelPluginOptions, InputParameters, InferInputParameters, Duration };
|