@iworkflow/dbos 0.4.0 → 0.6.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/dist/index.js +54 -21
- package/dist/scope.d.ts +2 -0
- package/package.json +2 -2
- package/src/index.ts +65 -18
- package/src/scope.ts +8 -0
package/dist/index.js
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { DBOS, DBOSClient, Error as DBOSErrors } from "@dbos-inc/dbos-sdk";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
stepScope,
|
|
5
|
+
checkPriority,
|
|
6
|
+
DEFAULT_PRIORITY,
|
|
5
7
|
exitSignal,
|
|
6
8
|
Failure,
|
|
7
9
|
FlowError,
|
|
8
10
|
isFlowError
|
|
9
11
|
} from "@iworkflow/core";
|
|
10
12
|
import { Pool } from "pg";
|
|
13
|
+
|
|
14
|
+
// src/scope.ts
|
|
15
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
16
|
+
var alsScope = () => {
|
|
17
|
+
const als = new AsyncLocalStorage;
|
|
18
|
+
return { run: (rt, fn) => als.run(rt, fn), current: () => als.getStore() };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
11
22
|
var resultPoll = () => ({ pollingIntervalMs: Number(process.env.DBOS_RESULT_POLL_MS) || undefined });
|
|
12
23
|
var sysDb = () => process.env.DBOS_SYSTEM_DATABASE_URL ?? "postgres://iworkflow:iworkflow@localhost:55432/iworkflow";
|
|
13
24
|
var HEARTBEAT_DDL = [
|
|
@@ -93,17 +104,20 @@ class Semaphore {
|
|
|
93
104
|
constructor(free) {
|
|
94
105
|
this.free = free;
|
|
95
106
|
}
|
|
96
|
-
async run(fn) {
|
|
107
|
+
async run(fn, priority = DEFAULT_PRIORITY) {
|
|
97
108
|
if (this.free > 0)
|
|
98
109
|
this.free--;
|
|
99
110
|
else
|
|
100
|
-
await new Promise((
|
|
111
|
+
await new Promise((go) => {
|
|
112
|
+
const i = this.queue.findIndex((w) => w.priority > priority);
|
|
113
|
+
this.queue.splice(i < 0 ? this.queue.length : i, 0, { priority, go });
|
|
114
|
+
});
|
|
101
115
|
try {
|
|
102
116
|
return await fn();
|
|
103
117
|
} finally {
|
|
104
118
|
const next = this.queue.shift();
|
|
105
119
|
if (next)
|
|
106
|
-
next();
|
|
120
|
+
next.go();
|
|
107
121
|
else
|
|
108
122
|
this.free++;
|
|
109
123
|
}
|
|
@@ -113,6 +127,7 @@ class Semaphore {
|
|
|
113
127
|
class AttemptTimeout extends Error {
|
|
114
128
|
}
|
|
115
129
|
var sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
130
|
+
var scope = stepScope(alsScope);
|
|
116
131
|
async function withTimeout(p, ms) {
|
|
117
132
|
let timer;
|
|
118
133
|
try {
|
|
@@ -124,14 +139,14 @@ async function withTimeout(p, ms) {
|
|
|
124
139
|
clearTimeout(timer);
|
|
125
140
|
}
|
|
126
141
|
}
|
|
127
|
-
async function attempts(fn, args, opts, pool, alive) {
|
|
142
|
+
async function attempts(fn, args, opts, pool, alive, priority, runtime) {
|
|
128
143
|
const max = opts.attempts ?? 3;
|
|
129
144
|
const started = Date.now();
|
|
130
145
|
let interval = opts.retryIntervalMs ?? 200;
|
|
131
146
|
for (let n = 1;; n++) {
|
|
132
147
|
try {
|
|
133
|
-
const attempt = () => withTimeout(fn(...args), opts.timeoutMs ?? 60000);
|
|
134
|
-
return await (pool ? pool.run(attempt) : attempt());
|
|
148
|
+
const attempt = () => scope.run(runtime(n), () => withTimeout(fn(...args), opts.timeoutMs ?? 60000));
|
|
149
|
+
return await (pool ? pool.run(attempt, priority) : attempt());
|
|
135
150
|
} catch (e) {
|
|
136
151
|
const stop = e instanceof Failure && e.nonRetryable || (opts.nonRetryableTypes ?? []).includes(typeOf(e));
|
|
137
152
|
if (stop || n >= max) {
|
|
@@ -145,15 +160,20 @@ async function attempts(fn, args, opts, pool, alive) {
|
|
|
145
160
|
exhausted
|
|
146
161
|
});
|
|
147
162
|
}
|
|
148
|
-
|
|
163
|
+
const wait = e instanceof Failure && e.retryAfterMs !== undefined ? e.retryAfterMs : interval;
|
|
164
|
+
if (opts.totalTimeoutMs !== undefined && Date.now() - started + wait >= opts.totalTimeoutMs)
|
|
149
165
|
throw new FlowError(e instanceof Error ? e.message : String(e), {
|
|
150
166
|
reason: "timeout",
|
|
151
167
|
type: typeOf(e),
|
|
152
168
|
details: e instanceof Failure ? e.details : undefined
|
|
153
169
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
let left = wait;
|
|
171
|
+
do {
|
|
172
|
+
await sleepMs(Math.min(Math.max(left, 0), 5000));
|
|
173
|
+
if (!await alive().catch(() => true))
|
|
174
|
+
throw new FlowError("процесс прерван — повторы шага остановлены", { reason: "terminated" });
|
|
175
|
+
left -= 5000;
|
|
176
|
+
} while (left > 0);
|
|
157
177
|
interval = Math.min(interval * (opts.backoff ?? 1), opts.maxRetryIntervalMs ?? Infinity);
|
|
158
178
|
}
|
|
159
179
|
}
|
|
@@ -172,7 +192,7 @@ function dbosRuntime() {
|
|
|
172
192
|
pool = new Pool({ connectionString: sysDb(), max: 2 });
|
|
173
193
|
const db = pool;
|
|
174
194
|
const patches = patchBook(db, def.app, def.patches ?? []);
|
|
175
|
-
|
|
195
|
+
const stepRuntime = (n) => ({
|
|
176
196
|
async heartbeat(details) {
|
|
177
197
|
await db.query(`INSERT INTO iwf.heartbeat (workflow_id, step_id, details) VALUES ($1, $2, $3)
|
|
178
198
|
ON CONFLICT (workflow_id, step_id) DO UPDATE SET details = EXCLUDED.details, at = now()`, [...stepKey(), JSON.stringify(details ?? null)]);
|
|
@@ -180,16 +200,18 @@ function dbosRuntime() {
|
|
|
180
200
|
async lastHeartbeat() {
|
|
181
201
|
const r = await db.query("SELECT details FROM iwf.heartbeat WHERE workflow_id = $1 AND step_id = $2", stepKey());
|
|
182
202
|
return r.rows[0]?.details ?? undefined;
|
|
183
|
-
}
|
|
203
|
+
},
|
|
204
|
+
attempt: () => n
|
|
184
205
|
});
|
|
185
206
|
const pools = Object.fromEntries(Object.entries(def.pools ?? {}).map(([p, n]) => [p, new Semaphore(n)]));
|
|
186
|
-
const enqueue = (target, args, opts, reject) => DBOS.enqueueWorkflowWithOptions({
|
|
207
|
+
const enqueue = (target, args, opts, reject, priority) => DBOS.enqueueWorkflowWithOptions({
|
|
187
208
|
queueName: target.app,
|
|
188
209
|
workflowName: target.flow,
|
|
189
210
|
workflowID: opts.id,
|
|
190
211
|
applicationName: target.app,
|
|
191
212
|
...opts.timeoutMs ? { workflowTimeoutMS: opts.timeoutMs } : {},
|
|
192
|
-
...reject ? { workflowIDReusePolicy: "reject" } : {}
|
|
213
|
+
...reject ? { workflowIDReusePolicy: "reject" } : {},
|
|
214
|
+
priority: priority ?? DEFAULT_PRIORITY
|
|
193
215
|
}, ...args).catch(alreadyStarted(opts.id));
|
|
194
216
|
const waitChild = async (h, id) => {
|
|
195
217
|
try {
|
|
@@ -202,13 +224,15 @@ function dbosRuntime() {
|
|
|
202
224
|
const run = async (...args) => {
|
|
203
225
|
const workflowId = DBOS.workflowID;
|
|
204
226
|
await patches.born(workflowId);
|
|
205
|
-
const row = (await db.query("SELECT created_at, parent_workflow_id FROM dbos.workflow_status WHERE workflow_uuid = $1", [
|
|
227
|
+
const row = (await db.query("SELECT created_at, parent_workflow_id, priority FROM dbos.workflow_status WHERE workflow_uuid = $1", [
|
|
206
228
|
workflowId
|
|
207
229
|
])).rows[0];
|
|
208
230
|
const ctx = {
|
|
209
231
|
workflowId,
|
|
210
232
|
parentId: row?.parent_workflow_id ?? undefined,
|
|
211
233
|
startedAt: new Date(Number(row?.created_at)),
|
|
234
|
+
priority: row?.priority || DEFAULT_PRIORITY,
|
|
235
|
+
now: () => DBOS.now(),
|
|
212
236
|
step: (step, stepArgs, opts = {}) => {
|
|
213
237
|
const pool = opts.pool ? pools[opts.pool] : undefined;
|
|
214
238
|
if (opts.pool && !pool)
|
|
@@ -225,16 +249,16 @@ function dbosRuntime() {
|
|
|
225
249
|
await sleepMs(100);
|
|
226
250
|
}
|
|
227
251
|
};
|
|
228
|
-
return DBOS.runStep(() => attempts(def.activities[step], stepArgs, opts, pool, alive), {
|
|
252
|
+
return DBOS.runStep(() => attempts(def.activities[step], stepArgs, opts, pool, alive, checkPriority(opts.priority) ?? ctx.priority, stepRuntime), {
|
|
229
253
|
name: step,
|
|
230
254
|
retriesAllowed: false
|
|
231
255
|
}).catch((e) => {
|
|
232
256
|
throw revive(e);
|
|
233
257
|
});
|
|
234
258
|
},
|
|
235
|
-
child: async (target, childArgs, opts) => waitChild(await enqueue(target, childArgs, opts, opts.onExisting === "reject"), opts.id),
|
|
259
|
+
child: async (target, childArgs, opts) => waitChild(await enqueue(target, childArgs, opts, opts.onExisting === "reject", ctx.priority), opts.id),
|
|
236
260
|
async startChild(target, childArgs, opts) {
|
|
237
|
-
const h = await enqueue(target, childArgs, opts, true);
|
|
261
|
+
const h = await enqueue(target, childArgs, opts, true, ctx.priority);
|
|
238
262
|
return { id: opts.id, result: () => waitChild(h, opts.id) };
|
|
239
263
|
},
|
|
240
264
|
sleep: (ms) => DBOS.sleep(ms),
|
|
@@ -311,20 +335,29 @@ async function dbosClient() {
|
|
|
311
335
|
await client.cancelWorkflow(id);
|
|
312
336
|
}
|
|
313
337
|
});
|
|
314
|
-
|
|
338
|
+
const self = {
|
|
315
339
|
async start(target, args, opts) {
|
|
316
340
|
const h = await client.enqueue({
|
|
317
341
|
queueName: target.app,
|
|
318
342
|
workflowName: target.flow,
|
|
319
343
|
workflowID: opts.id,
|
|
320
344
|
applicationName: target.app,
|
|
321
|
-
...opts.timeoutMs ? { workflowTimeoutMS: opts.timeoutMs } : {}
|
|
345
|
+
...opts.timeoutMs ? { workflowTimeoutMS: opts.timeoutMs } : {},
|
|
346
|
+
priority: checkPriority(opts.priority) ?? DEFAULT_PRIORITY
|
|
322
347
|
}, ...args);
|
|
323
348
|
return wrap(h.workflowID);
|
|
324
349
|
},
|
|
350
|
+
async signalWithStart(target, args, opts, topic, value) {
|
|
351
|
+
const h = await self.start(target, args, opts);
|
|
352
|
+
if (FINISHED.has((await status(h.id))?.status ?? ""))
|
|
353
|
+
throw new FlowError(`процесс ${h.id} уже завершён — id не переиспользуется`, { reason: "already-started" });
|
|
354
|
+
await client.send(h.id, value, topic);
|
|
355
|
+
return h;
|
|
356
|
+
},
|
|
325
357
|
handle: (id) => wrap(id),
|
|
326
358
|
close: () => client.destroy()
|
|
327
359
|
};
|
|
360
|
+
return self;
|
|
328
361
|
}
|
|
329
362
|
export {
|
|
330
363
|
dbosClient,
|
package/dist/scope.d.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iworkflow/dbos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "DBOS adapter for @iworkflow/core",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"workflow",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"prepack": "bun run build"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@iworkflow/core": "^0.
|
|
40
|
+
"@iworkflow/core": "^0.6.0",
|
|
41
41
|
"pg": "^8.0.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
// Дочерний процесс в чужом модуле — enqueueWorkflowWithOptions по имени, без ссылки на функцию.
|
|
3
3
|
import { DBOS, DBOSClient, Error as DBOSErrors, type WorkflowStatus } from "@dbos-inc/dbos-sdk";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
stepScope,
|
|
6
|
+
type StepRuntime,
|
|
7
|
+
checkPriority,
|
|
8
|
+
DEFAULT_PRIORITY,
|
|
6
9
|
exitSignal,
|
|
7
10
|
Failure,
|
|
8
11
|
FlowError,
|
|
@@ -14,10 +17,13 @@ import {
|
|
|
14
17
|
type EngineRuntime,
|
|
15
18
|
type FlowContext,
|
|
16
19
|
type FlowHandle,
|
|
20
|
+
type FlowRef,
|
|
17
21
|
type FlowStatus,
|
|
22
|
+
type StartOptions,
|
|
18
23
|
type StepOptions,
|
|
19
24
|
} from "@iworkflow/core";
|
|
20
25
|
import { Pool } from "pg";
|
|
26
|
+
import { alsScope } from "./scope";
|
|
21
27
|
|
|
22
28
|
// Ожидание результата дочернего процесса — опрос базы, по умолчанию раз в секунду: это и есть
|
|
23
29
|
// задержка каждого перехода между модулями. DBOS_RESULT_POLL_MS уменьшает её ценой запросов к базе.
|
|
@@ -135,17 +141,22 @@ const alreadyStarted = (id: string) => (e: unknown): never => {
|
|
|
135
141
|
// Повторы — свои, внутри одного шага DBOS: в историю пишется уже FlowError с причиной, как у Temporal.
|
|
136
142
|
// Цена: отмена процесса не прерывает повторы шага, а ждёт его конца.
|
|
137
143
|
|
|
144
|
+
/** Предел одновременных шагов пула; ждущие выходят по приоритету (1 — первым), внутри — по очереди. */
|
|
138
145
|
class Semaphore {
|
|
139
|
-
private queue: (
|
|
146
|
+
private queue: { priority: number; go: () => void }[] = [];
|
|
140
147
|
constructor(private free: number) {}
|
|
141
|
-
async run<T>(fn: () => Promise<T
|
|
148
|
+
async run<T>(fn: () => Promise<T>, priority = DEFAULT_PRIORITY): Promise<T> {
|
|
142
149
|
if (this.free > 0) this.free--;
|
|
143
|
-
else
|
|
150
|
+
else
|
|
151
|
+
await new Promise<void>((go) => {
|
|
152
|
+
const i = this.queue.findIndex((w) => w.priority > priority);
|
|
153
|
+
this.queue.splice(i < 0 ? this.queue.length : i, 0, { priority, go });
|
|
154
|
+
});
|
|
144
155
|
try {
|
|
145
156
|
return await fn();
|
|
146
157
|
} finally {
|
|
147
158
|
const next = this.queue.shift();
|
|
148
|
-
if (next) next();
|
|
159
|
+
if (next) next.go();
|
|
149
160
|
else this.free++;
|
|
150
161
|
}
|
|
151
162
|
}
|
|
@@ -155,6 +166,8 @@ class AttemptTimeout extends Error {}
|
|
|
155
166
|
|
|
156
167
|
const sleepMs = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
157
168
|
|
|
169
|
+
const scope = stepScope(alsScope);
|
|
170
|
+
|
|
158
171
|
/** Попытка, урезанная по сроку: брошенная продолжает идти в фоне, её итог отбрасывается. */
|
|
159
172
|
async function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
|
|
160
173
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -175,14 +188,16 @@ async function attempts(
|
|
|
175
188
|
opts: StepOptions,
|
|
176
189
|
pool: Semaphore | undefined,
|
|
177
190
|
alive: () => Promise<boolean>,
|
|
191
|
+
priority: number | undefined,
|
|
192
|
+
runtime: (n: number) => StepRuntime,
|
|
178
193
|
) {
|
|
179
194
|
const max = opts.attempts ?? 3;
|
|
180
195
|
const started = Date.now();
|
|
181
196
|
let interval = opts.retryIntervalMs ?? 200;
|
|
182
197
|
for (let n = 1; ; n++) {
|
|
183
198
|
try {
|
|
184
|
-
const attempt = () => withTimeout(fn(...args), opts.timeoutMs ?? 60_000);
|
|
185
|
-
return await (pool ? pool.run(attempt) : attempt());
|
|
199
|
+
const attempt = () => scope.run(runtime(n), () => withTimeout(fn(...args), opts.timeoutMs ?? 60_000));
|
|
200
|
+
return await (pool ? pool.run(attempt, priority) : attempt());
|
|
186
201
|
} catch (e) {
|
|
187
202
|
const stop = (e instanceof Failure && e.nonRetryable) || (opts.nonRetryableTypes ?? []).includes(typeOf(e));
|
|
188
203
|
if (stop || n >= max) {
|
|
@@ -195,16 +210,24 @@ async function attempts(
|
|
|
195
210
|
exhausted,
|
|
196
211
|
});
|
|
197
212
|
}
|
|
198
|
-
|
|
213
|
+
// Отказ сам назвал срок (FLOOD_WAIT) — ждём его, а не паузу из opts.
|
|
214
|
+
const wait = e instanceof Failure && e.retryAfterMs !== undefined ? e.retryAfterMs : interval;
|
|
215
|
+
if (opts.totalTimeoutMs !== undefined && Date.now() - started + wait >= opts.totalTimeoutMs)
|
|
199
216
|
throw new FlowError(e instanceof Error ? e.message : String(e), {
|
|
200
217
|
reason: "timeout",
|
|
201
218
|
type: typeOf(e),
|
|
202
219
|
details: e instanceof Failure ? e.details : undefined,
|
|
203
220
|
});
|
|
204
|
-
|
|
205
|
-
// После паузы: за неё процесс мог выйти по сроку или быть отменён.
|
|
221
|
+
// Пауза кусками: срок или отмена процесса видны не позже чем через 5 с, а не после всей паузы.
|
|
206
222
|
// Сбой чтения статуса — не повод бросать шаг: повторяем дальше.
|
|
207
|
-
|
|
223
|
+
// do…while: и при нулевой паузе статус процесса проверяется перед каждой попыткой.
|
|
224
|
+
let left = wait;
|
|
225
|
+
do {
|
|
226
|
+
await sleepMs(Math.min(Math.max(left, 0), 5_000));
|
|
227
|
+
if (!(await alive().catch(() => true)))
|
|
228
|
+
throw new FlowError("процесс прерван — повторы шага остановлены", { reason: "terminated" });
|
|
229
|
+
left -= 5_000;
|
|
230
|
+
} while (left > 0);
|
|
208
231
|
interval = Math.min(interval * (opts.backoff ?? 1), opts.maxRetryIntervalMs ?? Infinity);
|
|
209
232
|
}
|
|
210
233
|
}
|
|
@@ -229,7 +252,8 @@ export function dbosRuntime(): EngineRuntime {
|
|
|
229
252
|
pool = new Pool({ connectionString: sysDb(), max: 2 });
|
|
230
253
|
const db = pool;
|
|
231
254
|
const patches = patchBook(db, def.app, def.patches ?? []);
|
|
232
|
-
|
|
255
|
+
// Рантайм попытки шага: прогресс — в iwf.heartbeat по (процесс, шаг), номер — из цикла повторов.
|
|
256
|
+
const stepRuntime = (n: number): StepRuntime => ({
|
|
233
257
|
async heartbeat(details) {
|
|
234
258
|
await db.query(
|
|
235
259
|
`INSERT INTO iwf.heartbeat (workflow_id, step_id, details) VALUES ($1, $2, $3)
|
|
@@ -241,9 +265,16 @@ export function dbosRuntime(): EngineRuntime {
|
|
|
241
265
|
const r = await db.query("SELECT details FROM iwf.heartbeat WHERE workflow_id = $1 AND step_id = $2", stepKey());
|
|
242
266
|
return r.rows[0]?.details ?? undefined;
|
|
243
267
|
},
|
|
268
|
+
attempt: () => n,
|
|
244
269
|
});
|
|
245
270
|
const pools = Object.fromEntries(Object.entries(def.pools ?? {}).map(([p, n]) => [p, new Semaphore(n)]));
|
|
246
|
-
const enqueue = (
|
|
271
|
+
const enqueue = (
|
|
272
|
+
target: { app: string; flow: string },
|
|
273
|
+
args: unknown[],
|
|
274
|
+
opts: ChildOptions,
|
|
275
|
+
reject: boolean,
|
|
276
|
+
priority: number | undefined,
|
|
277
|
+
) =>
|
|
247
278
|
DBOS.enqueueWorkflowWithOptions(
|
|
248
279
|
{
|
|
249
280
|
queueName: target.app,
|
|
@@ -252,6 +283,8 @@ export function dbosRuntime(): EngineRuntime {
|
|
|
252
283
|
applicationName: target.app,
|
|
253
284
|
...(opts.timeoutMs ? { workflowTimeoutMS: opts.timeoutMs } : {}),
|
|
254
285
|
...(reject ? { workflowIDReusePolicy: "reject" as const } : {}),
|
|
286
|
+
// Ребёнок наследует приоритет родителя, как у Temporal.
|
|
287
|
+
priority: priority ?? DEFAULT_PRIORITY,
|
|
255
288
|
},
|
|
256
289
|
...args,
|
|
257
290
|
).catch(alreadyStarted(opts.id));
|
|
@@ -268,7 +301,7 @@ export function dbosRuntime(): EngineRuntime {
|
|
|
268
301
|
await patches.born(workflowId);
|
|
269
302
|
// Строка статуса не меняется после создания: чтение вне шага детерминировано.
|
|
270
303
|
const row = (
|
|
271
|
-
await db.query("SELECT created_at, parent_workflow_id FROM dbos.workflow_status WHERE workflow_uuid = $1", [
|
|
304
|
+
await db.query("SELECT created_at, parent_workflow_id, priority FROM dbos.workflow_status WHERE workflow_uuid = $1", [
|
|
272
305
|
workflowId,
|
|
273
306
|
])
|
|
274
307
|
).rows[0];
|
|
@@ -276,6 +309,8 @@ export function dbosRuntime(): EngineRuntime {
|
|
|
276
309
|
workflowId,
|
|
277
310
|
parentId: row?.parent_workflow_id ?? undefined,
|
|
278
311
|
startedAt: new Date(Number(row?.created_at)),
|
|
312
|
+
priority: row?.priority || DEFAULT_PRIORITY,
|
|
313
|
+
now: () => DBOS.now(),
|
|
279
314
|
step: (step, stepArgs, opts = {}) => {
|
|
280
315
|
const pool = opts.pool ? pools[opts.pool] : undefined;
|
|
281
316
|
if (opts.pool && !pool) throw new Error(`пул "${opts.pool}" не объявлен в AppDefinition.pools модуля ${def.app}`);
|
|
@@ -296,7 +331,7 @@ export function dbosRuntime(): EngineRuntime {
|
|
|
296
331
|
await sleepMs(100);
|
|
297
332
|
}
|
|
298
333
|
};
|
|
299
|
-
return DBOS.runStep(() => attempts(def.activities[step]!, stepArgs, opts, pool, alive), {
|
|
334
|
+
return DBOS.runStep(() => attempts(def.activities[step]!, stepArgs, opts, pool, alive, checkPriority(opts.priority) ?? ctx.priority, stepRuntime), {
|
|
300
335
|
name: step,
|
|
301
336
|
retriesAllowed: false,
|
|
302
337
|
}).catch((e) => {
|
|
@@ -304,10 +339,10 @@ export function dbosRuntime(): EngineRuntime {
|
|
|
304
339
|
}) as Promise<never>;
|
|
305
340
|
},
|
|
306
341
|
child: async (target, childArgs, opts) =>
|
|
307
|
-
waitChild(await enqueue(target, childArgs, opts, opts.onExisting === "reject"), opts.id),
|
|
342
|
+
waitChild(await enqueue(target, childArgs, opts, opts.onExisting === "reject", ctx.priority), opts.id),
|
|
308
343
|
async startChild(target, childArgs, opts) {
|
|
309
344
|
// Процесс DBOS не привязан к родителю: живёт сам, как ABANDON у Temporal.
|
|
310
|
-
const h = await enqueue(target, childArgs, opts, true);
|
|
345
|
+
const h = await enqueue(target, childArgs, opts, true, ctx.priority);
|
|
311
346
|
return { id: opts.id, result: () => waitChild(h, opts.id) };
|
|
312
347
|
},
|
|
313
348
|
sleep: (ms) => DBOS.sleep(ms),
|
|
@@ -398,7 +433,7 @@ export async function dbosClient(): Promise<EngineClient> {
|
|
|
398
433
|
await client.cancelWorkflow(id);
|
|
399
434
|
},
|
|
400
435
|
});
|
|
401
|
-
|
|
436
|
+
const self: EngineClient = {
|
|
402
437
|
async start(target, args, opts) {
|
|
403
438
|
const h = await client.enqueue(
|
|
404
439
|
{
|
|
@@ -407,12 +442,24 @@ export async function dbosClient(): Promise<EngineClient> {
|
|
|
407
442
|
workflowID: opts.id,
|
|
408
443
|
applicationName: target.app,
|
|
409
444
|
...(opts.timeoutMs ? { workflowTimeoutMS: opts.timeoutMs } : {}),
|
|
445
|
+
// Без приоритета DBOS ставит 0 — впереди всех; порт по умолчанию 3, как Temporal.
|
|
446
|
+
priority: checkPriority(opts.priority) ?? DEFAULT_PRIORITY,
|
|
410
447
|
},
|
|
411
448
|
...args,
|
|
412
449
|
);
|
|
413
450
|
return wrap(h.workflowID);
|
|
414
451
|
},
|
|
452
|
+
async signalWithStart<T>(target: FlowRef, args: unknown[], opts: StartOptions, topic: string, value: unknown) {
|
|
453
|
+
// Не атомарно, но без потерь: сигнал ждёт в очереди темы, пока процесс до него дойдёт.
|
|
454
|
+
const h = await self.start<T>(target, args, opts);
|
|
455
|
+
// id завершённого DBOS не переиспользует: сигнал ушёл бы в никуда.
|
|
456
|
+
if (FINISHED.has((await status(h.id))?.status ?? ""))
|
|
457
|
+
throw new FlowError(`процесс ${h.id} уже завершён — id не переиспользуется`, { reason: "already-started" });
|
|
458
|
+
await client.send(h.id, value, topic);
|
|
459
|
+
return h;
|
|
460
|
+
},
|
|
415
461
|
handle: (id) => wrap(id),
|
|
416
462
|
close: () => client.destroy(),
|
|
417
463
|
};
|
|
464
|
+
return self;
|
|
418
465
|
}
|
package/src/scope.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Область шага на AsyncLocalStorage: stepScope ядра берёт её у первого адаптера процесса.
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import type { StepRuntime, StepScope } from "@iworkflow/core";
|
|
4
|
+
|
|
5
|
+
export const alsScope = (): StepScope => {
|
|
6
|
+
const als = new AsyncLocalStorage<StepRuntime>();
|
|
7
|
+
return { run: (rt, fn) => als.run(rt, fn), current: () => als.getStore() };
|
|
8
|
+
};
|