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