@sanoma-ai/workflows 0.11.16 → 0.11.18
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 +16 -8
- package/dist/call.d.ts +35 -4
- package/dist/call.js +128 -40
- package/dist/call.js.map +1 -1
- package/dist/client.d.ts +17 -8
- package/dist/client.js +90 -51
- package/dist/client.js.map +1 -1
- package/dist/collection.d.ts +25 -7
- package/dist/collection.js +236 -91
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +24 -1
- package/dist/config.js +48 -10
- package/dist/config.js.map +1 -1
- package/dist/db/company.d.ts +20 -0
- package/dist/db/company.js +9 -1
- package/dist/db/company.js.map +1 -1
- package/dist/describe.js +1 -1
- package/dist/describe.js.map +1 -1
- package/dist/description.d.ts +6 -3
- package/dist/description.js +5 -4
- package/dist/description.js.map +1 -1
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +3 -2
- package/dist/errors.js.map +1 -1
- package/dist/grid.js +3 -2
- package/dist/grid.js.map +1 -1
- package/dist/host-derive.d.ts +13 -8
- package/dist/host-derive.js +26 -13
- package/dist/host-derive.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +7 -0
- package/dist/ledger.js +32 -2
- package/dist/ledger.js.map +1 -1
- package/dist/local-sandbox.d.ts +17 -0
- package/dist/local-sandbox.js +131 -0
- package/dist/local-sandbox.js.map +1 -0
- package/dist/op.d.ts +10 -2
- package/dist/op.js.map +1 -1
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +20 -4
- package/dist/paths.js.map +1 -1
- package/dist/resource.d.ts +7 -1
- package/dist/resource.js +5 -1
- package/dist/resource.js.map +1 -1
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +9 -6
- package/dist/resources.js.map +1 -1
- package/dist/runner-box.d.ts +23 -0
- package/dist/runner-box.js +181 -0
- package/dist/runner-box.js.map +1 -0
- package/dist/runner-protocol.d.ts +73 -0
- package/dist/runner-protocol.js +34 -0
- package/dist/runner-protocol.js.map +1 -0
- package/dist/sandbox-contract.d.ts +109 -0
- package/dist/sandbox-contract.js +26 -0
- package/dist/sandbox-contract.js.map +1 -0
- package/dist/sandbox-runner.cjs +7 -0
- package/dist/scenario.js +16 -7
- package/dist/scenario.js.map +1 -1
- package/dist/schedules.d.ts +11 -3
- package/dist/schedules.js +15 -3
- package/dist/schedules.js.map +1 -1
- package/dist/schema.js +6 -3
- package/dist/schema.js.map +1 -1
- package/dist/secret.d.ts +2 -1
- package/dist/secret.js +3 -2
- package/dist/secret.js.map +1 -1
- package/dist/shared.d.ts +39 -5
- package/dist/shared.js +13 -1
- package/dist/shared.js.map +1 -1
- package/dist/sites.js +10 -8
- package/dist/sites.js.map +1 -1
- package/dist/source.d.ts +21 -0
- package/dist/source.js.map +1 -1
- package/drizzle/company/0003_ledger_target.sql +2 -0
- package/drizzle/company/meta/0003_snapshot.json +845 -0
- package/drizzle/company/meta/_journal.json +7 -0
- package/package.json +5 -2
- package/src/call.ts +181 -60
- package/src/client.ts +118 -67
- package/src/collection.ts +265 -97
- package/src/config.ts +65 -16
- package/src/db/company.ts +9 -1
- package/src/describe.ts +1 -1
- package/src/description.ts +5 -4
- package/src/errors.ts +3 -2
- package/src/grid.ts +3 -1
- package/src/host-derive.ts +38 -18
- package/src/index.ts +15 -2
- package/src/ledger.ts +42 -2
- package/src/local-sandbox.ts +133 -0
- package/src/op.ts +10 -2
- package/src/paths.ts +22 -4
- package/src/resource.ts +16 -2
- package/src/resources.ts +8 -6
- package/src/runner-box.ts +218 -0
- package/src/runner-protocol.ts +61 -0
- package/src/sandbox-contract.ts +121 -0
- package/src/sandbox-runner.ts +187 -0
- package/src/scenario.ts +16 -6
- package/src/schedules.ts +19 -4
- package/src/schema.ts +14 -3
- package/src/secret.ts +6 -2
- package/src/shared.ts +40 -4
- package/src/sites.ts +11 -7
- package/src/source.ts +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanoma-ai/workflows",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.18",
|
|
4
4
|
"description": "Business processes as TypeScript functions, run durably on DBOS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"approvals",
|
|
@@ -122,9 +122,12 @@
|
|
|
122
122
|
},
|
|
123
123
|
"devDependencies": {
|
|
124
124
|
"@types/pg": "^8.23.1",
|
|
125
|
+
"@types/ws": "8.18.2",
|
|
125
126
|
"drizzle-kit": "0.31.11",
|
|
127
|
+
"esbuild": "0.28.2",
|
|
126
128
|
"msw": "^3.0.3",
|
|
127
129
|
"srvx": "^0.11.22",
|
|
130
|
+
"ws": "8.22.0",
|
|
128
131
|
"zod": "^4.6.5"
|
|
129
132
|
},
|
|
130
133
|
"peerDependencies": {
|
|
@@ -135,7 +138,7 @@
|
|
|
135
138
|
},
|
|
136
139
|
"//": "randexp is a dependency only because zod-schema-faker, which makes up scenario inputs, needs it as a peer.",
|
|
137
140
|
"scripts": {
|
|
138
|
-
"build": "tsc -p tsconfig.build.json",
|
|
141
|
+
"build": "tsc -p tsconfig.build.json && esbuild src/sandbox-runner.ts --bundle --platform=node --target=node22 --format=cjs --minify --external:bufferutil --external:utf-8-validate --outfile=dist/sandbox-runner.cjs --log-level=warning",
|
|
139
142
|
"clean": "rm -rf dist",
|
|
140
143
|
"watch": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
141
144
|
"generate": "drizzle-kit generate --config drizzle/company.config.ts && drizzle-kit generate --config drizzle/wakeup.config.ts"
|
package/src/call.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
SanomaError,
|
|
28
28
|
} from "./errors.ts";
|
|
29
29
|
import type { Collection } from "./collection.ts";
|
|
30
|
-
import { currentGroup, entry, skipped, write, writeFailure } from "./ledger.ts";
|
|
30
|
+
import { currentGroup, entry, type LedgerBody, skipped, write, writeFailure } from "./ledger.ts";
|
|
31
31
|
import { shown } from "./log.ts";
|
|
32
32
|
import { type CallContext, isOp, type Op } from "./op.ts";
|
|
33
33
|
import { callAt, callName, type CallNode } from "./outline.ts";
|
|
@@ -42,8 +42,10 @@ import {
|
|
|
42
42
|
CALL_WORKFLOW,
|
|
43
43
|
CHANGE_MERGE,
|
|
44
44
|
DATA_VENDOR,
|
|
45
|
+
entryOpsOf,
|
|
45
46
|
errorMessage,
|
|
46
47
|
fitsOp,
|
|
48
|
+
isBatch,
|
|
47
49
|
LOGIN_CODE_WORKFLOW,
|
|
48
50
|
problemLine,
|
|
49
51
|
type Saved,
|
|
@@ -293,7 +295,7 @@ function strict<T extends object>(obj: T, path: string, workflow: string): T {
|
|
|
293
295
|
}
|
|
294
296
|
|
|
295
297
|
/** The resource instance the call acts on, from the operation's `target`, when it declares one. */
|
|
296
|
-
function targetOf(op: Op, input: unknown): { target?: string } {
|
|
298
|
+
export function targetOf(op: Op, input: unknown): { target?: string } {
|
|
297
299
|
if (!op.target) return {};
|
|
298
300
|
const target: unknown = op.target(input);
|
|
299
301
|
if (typeof target !== "string") {
|
|
@@ -302,18 +304,104 @@ function targetOf(op: Op, input: unknown): { target?: string } {
|
|
|
302
304
|
return { target };
|
|
303
305
|
}
|
|
304
306
|
|
|
307
|
+
/** One call as the policy decides it and the ledger records it: an operation and its parsed input. */
|
|
308
|
+
export interface Call {
|
|
309
|
+
op: Op;
|
|
310
|
+
input: unknown;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* The calls a call makes: a collection's `saveMany` (`isBatch`) makes one per entry, the
|
|
315
|
+
* collection's `save` or `delete` with the entry as its input, and these are what the policy
|
|
316
|
+
* decides and the ledger records, so a record's history reads the same whether it was saved
|
|
317
|
+
* alone or in a batch; any other call makes itself.
|
|
318
|
+
*/
|
|
319
|
+
export function callsOf(op: Op, input: unknown, ops: ReadonlyMap<string, Op>): Call[] {
|
|
320
|
+
if (!isBatch(op)) return [{ op, input }];
|
|
321
|
+
const { records, deletes } = input as { records: unknown[]; deletes: unknown[] };
|
|
322
|
+
const [save, remove] = entryOpsOf(op).map((id) => ops.get(id)!);
|
|
323
|
+
return [...records.map((e) => ({ op: save!, input: e })), ...deletes.map((e) => ({ op: remove!, input: e }))];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The outputs of the calls a call made, from its output: a batch's, one per entry, in order. */
|
|
327
|
+
export const outputsOf = (op: Op, output: unknown): unknown[] => (isBatch(op) ? (output as unknown[]) : [output]);
|
|
328
|
+
|
|
329
|
+
/** The key of the record a collection's `save` or `delete` acts on. */
|
|
330
|
+
const keyOf = (op: Op, input: unknown, collection: Collection): string =>
|
|
331
|
+
op.name === "save"
|
|
332
|
+
? ((input as { record: Record<string, unknown> }).record[collection.key] as string)
|
|
333
|
+
: (input as { key: string }).key;
|
|
334
|
+
|
|
335
|
+
/** A batch's denied entries: each one's key, with its decision. */
|
|
336
|
+
function denialsOf(
|
|
337
|
+
op: Op,
|
|
338
|
+
calls: readonly Call[],
|
|
339
|
+
decisions: readonly RecordedDecision[],
|
|
340
|
+
collections: ReadonlyMap<string, Collection>,
|
|
341
|
+
): { key: string; decision: Extract<RecordedDecision, { kind: "deny" }> }[] {
|
|
342
|
+
const collection = collections.get(op.resource)!;
|
|
343
|
+
return calls.flatMap((c, i) => {
|
|
344
|
+
const decision = decisions[i]!;
|
|
345
|
+
return decision.kind === "deny" ? [{ key: keyOf(c.op, c.input, collection), decision }] : [];
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
305
349
|
/**
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
* Runs inside a step, so it must have no side effects of its own.
|
|
350
|
+
* The decision for a call from those of the calls it makes: a batch is denied when any entry is
|
|
351
|
+
* (the reason naming each denied key), else held when any is (by the first hold), else allowed;
|
|
352
|
+
* any other call's is its own.
|
|
310
353
|
*/
|
|
354
|
+
export function decisionOf(
|
|
355
|
+
op: Op,
|
|
356
|
+
calls: readonly Call[],
|
|
357
|
+
decisions: readonly RecordedDecision[],
|
|
358
|
+
collections: ReadonlyMap<string, Collection>,
|
|
359
|
+
): RecordedDecision {
|
|
360
|
+
if (!isBatch(op)) return decisions[0]!;
|
|
361
|
+
const version = decisions[0]?.policyVersion === undefined ? {} : { policyVersion: decisions[0].policyVersion };
|
|
362
|
+
const denied = denialsOf(op, calls, decisions, collections);
|
|
363
|
+
if (denied.length) {
|
|
364
|
+
const reasons = [...new Set(denied.flatMap(({ decision }) => decision.reasons ?? []))];
|
|
365
|
+
const reason = denied.map(({ key, decision }) => `${key}: ${decision.reason}`).join("; ");
|
|
366
|
+
return { kind: "deny", reason, ...(reasons.length && { reasons }), ...version };
|
|
367
|
+
}
|
|
368
|
+
return decisions.find((d) => d.kind === "approve") ?? { kind: "allow", ...version };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** The keys of a batch's denied entries, for its `policy_denied` (`denied`); nothing for any other call. */
|
|
372
|
+
export function deniedKeys(
|
|
373
|
+
op: Op,
|
|
374
|
+
calls: readonly Call[],
|
|
375
|
+
decisions: readonly RecordedDecision[],
|
|
376
|
+
collections: ReadonlyMap<string, Collection>,
|
|
377
|
+
): { denied?: string[] } {
|
|
378
|
+
if (!isBatch(op)) return {};
|
|
379
|
+
return { denied: denialsOf(op, calls, decisions, collections).map(({ key }) => key) };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** `decideEach` for one call. */
|
|
311
383
|
export async function decide(
|
|
312
384
|
run: Run,
|
|
313
385
|
op: Op,
|
|
314
386
|
input: unknown,
|
|
315
387
|
connection?: ConnectionSnapshot,
|
|
316
388
|
): Promise<RecordedDecision> {
|
|
389
|
+
const [decision] = await decideEach(run, [{ op, input }], connection);
|
|
390
|
+
return decision!;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Asks the policy about each of the calls a call makes (`callsOf`), in order, with the record a
|
|
395
|
+
* collection's `save` or `delete` replaces as `before` (`beforeOf`, each read keyed by its place).
|
|
396
|
+
* The runtime's own rule first (`runtimeHold`, over the running instance's people): while it
|
|
397
|
+
* holds or denies a call, the company's policy is not asked about it. Runs inside a step, so it
|
|
398
|
+
* must have no side effects of its own.
|
|
399
|
+
*/
|
|
400
|
+
export async function decideEach(
|
|
401
|
+
run: Run,
|
|
402
|
+
calls: readonly Call[],
|
|
403
|
+
connection?: ConnectionSnapshot,
|
|
404
|
+
): Promise<RecordedDecision[]> {
|
|
317
405
|
const seen = {
|
|
318
406
|
id: run.id,
|
|
319
407
|
workflow: run.workflow,
|
|
@@ -321,20 +409,33 @@ export async function decide(
|
|
|
321
409
|
(a) => a.requestedBy !== "policy" || a.connection === connection?.revision,
|
|
322
410
|
),
|
|
323
411
|
};
|
|
324
|
-
// Only a merge is the rule's: no other call reads the people's files.
|
|
325
|
-
const held =
|
|
326
|
-
op.id === CHANGE_MERGE ? runtimeHold(op.id, input, seen.approvals, readPeople(run.state.root).people) : undefined;
|
|
327
|
-
if (held) return held;
|
|
328
412
|
const { policy } = run.state;
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
413
|
+
const decisions: RecordedDecision[] = [];
|
|
414
|
+
for (const [i, { op, input }] of calls.entries()) {
|
|
415
|
+
// Only a merge is the rule's: no other call reads the people's files.
|
|
416
|
+
const held =
|
|
417
|
+
op.id === CHANGE_MERGE ? runtimeHold(op.id, input, seen.approvals, readPeople(run.state.root).people) : undefined;
|
|
418
|
+
if (held) {
|
|
419
|
+
decisions.push(held);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
// Keyed by the call (`seq` was taken for it) and its place, so a fake answers each read afresh.
|
|
423
|
+
const before = await beforeOf(policy, op, input, run.state.collections, (get, key) =>
|
|
424
|
+
driverOf(run, get)?.(
|
|
425
|
+
{ key },
|
|
426
|
+
{
|
|
427
|
+
idempotencyKey: `${run.id}:${run.seq}:before${i ? `:${i}` : ""}`,
|
|
428
|
+
runId: run.id,
|
|
429
|
+
opId: get,
|
|
430
|
+
attempt: 1,
|
|
431
|
+
secrets: secretsOf(run),
|
|
432
|
+
},
|
|
433
|
+
),
|
|
434
|
+
);
|
|
435
|
+
const call = { ...callOf(op, input, run.actor, seen), ...before, ...(connection && { connection }) };
|
|
436
|
+
decisions.push(ask(policy, op.id, () => policy(call), run.state));
|
|
437
|
+
}
|
|
438
|
+
return decisions;
|
|
338
439
|
}
|
|
339
440
|
|
|
340
441
|
/** The run's function for an operation: its own fake's in a sandbox run, never a vendor's; else the driver's. */
|
|
@@ -368,10 +469,7 @@ export async function beforeOf(
|
|
|
368
469
|
): Promise<{ before?: unknown }> {
|
|
369
470
|
const collection = op.vendor === DATA_VENDOR ? collections.get(op.resource) : undefined;
|
|
370
471
|
if (policy === allowAll || !collection || (op.name !== "save" && op.name !== "delete")) return {};
|
|
371
|
-
const key =
|
|
372
|
-
op.name === "save"
|
|
373
|
-
? (input as { record: Record<string, unknown> }).record[collection.key]
|
|
374
|
-
: (input as { key: unknown }).key;
|
|
472
|
+
const key = keyOf(op, input, collection);
|
|
375
473
|
let saved: Saved | null | undefined;
|
|
376
474
|
try {
|
|
377
475
|
saved = (await read(`${DATA_VENDOR}.${op.resource}.get`, key)) as Saved | null | undefined;
|
|
@@ -511,11 +609,15 @@ async function callOp(run: Run, id: string, input: unknown, place: Placed, ref?:
|
|
|
511
609
|
});
|
|
512
610
|
}
|
|
513
611
|
const parsed = parseOrThrow(op.input, input, `The input to ${op.id} does not match its schema`, { op: op.id });
|
|
612
|
+
// The calls this one makes (a batch's entries), each decided and recorded on its own.
|
|
613
|
+
const calls = callsOf(op, parsed, run.state.ops);
|
|
514
614
|
// The call's identity (its ledger seq and id, and the driver's idempotency key) is taken
|
|
515
615
|
// before the first await, so it depends only on the order the workflow made its calls. A
|
|
516
616
|
// replay skips the step bodies but counts the same way. A call held for an approval is
|
|
517
|
-
// numbered before the approval's records, so its op.called sorts ahead of them.
|
|
518
|
-
|
|
617
|
+
// numbered before the approval's records, so its op.called sorts ahead of them. A batch
|
|
618
|
+
// takes a seq per entry.
|
|
619
|
+
const seq = run.seq;
|
|
620
|
+
run.seq += calls.length;
|
|
519
621
|
// The decision is recorded as a step, so a replay reuses it instead of asking a policy
|
|
520
622
|
// that may have changed since. The policy must still be deterministic: a run that fails
|
|
521
623
|
// before the step is recorded asks again.
|
|
@@ -536,34 +638,57 @@ async function callOp(run: Run, id: string, input: unknown, place: Placed, ref?:
|
|
|
536
638
|
// Read in the step, so a replay reuses the first answer, and asked again after each
|
|
537
639
|
// approval, so a call made after a long wait sees the files as they are then.
|
|
538
640
|
if (run.workflow !== APPLY_WORKFLOW && isChangeOp(op, run.state.types)) await refuseDeclared(run, op, parsed);
|
|
539
|
-
return
|
|
641
|
+
return decideEach(run, calls, connection);
|
|
540
642
|
},
|
|
541
643
|
{ name: `policy:${op.id}` },
|
|
542
644
|
);
|
|
543
|
-
let
|
|
645
|
+
let decisions = await decideStep();
|
|
646
|
+
let decision = decisionOf(op, calls, decisions, run.state.collections);
|
|
647
|
+
// The records keep the first decisions: a hold stays a hold once its approval let the call through.
|
|
648
|
+
const decided = decisions;
|
|
544
649
|
const logged = recorded(op, parsed, run.workflow);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
650
|
+
/** The call's records, one per call it made, numbered from `seq`, each with its decision and what `outcome` says of it. */
|
|
651
|
+
const records = (outcome: (i: number) => Partial<LedgerBody>, at?: number) =>
|
|
652
|
+
calls.map((c, i) =>
|
|
653
|
+
entry(
|
|
654
|
+
run,
|
|
655
|
+
{
|
|
656
|
+
type: "op.called",
|
|
657
|
+
op: c.op.id,
|
|
658
|
+
...place,
|
|
659
|
+
...targetOf(c.op, c.input),
|
|
660
|
+
effect: c.op.effect,
|
|
661
|
+
input: recorded(c.op, c.input, run.workflow),
|
|
662
|
+
decision: decided[i]!,
|
|
663
|
+
...(connection && { connection }),
|
|
664
|
+
...outcome(i),
|
|
665
|
+
} as LedgerBody,
|
|
666
|
+
{ seq: seq + i, ...(at !== undefined && { at }) },
|
|
667
|
+
),
|
|
668
|
+
);
|
|
669
|
+
const writeFailures = async (outcome: (i: number) => Partial<LedgerBody>, err: unknown, at?: number) => {
|
|
670
|
+
for (const record of records(outcome, at)) await writeFailure(run, record, err);
|
|
671
|
+
};
|
|
554
672
|
|
|
555
673
|
// Each hold is lifted in turn: once one is approved, the policy is asked again, with it.
|
|
556
674
|
for (;;) {
|
|
557
675
|
if (decision.kind === "deny") {
|
|
558
|
-
const err = new PolicyDeniedError(
|
|
559
|
-
|
|
676
|
+
const err = new PolicyDeniedError(
|
|
677
|
+
op.id,
|
|
678
|
+
decision.reason,
|
|
679
|
+
deniedKeys(op, calls, decisions, run.state.collections),
|
|
680
|
+
);
|
|
681
|
+
// A deny after an approval is recorded as the deny, with its reason, not as the hold.
|
|
682
|
+
await writeFailures((i) => ({ decision: decisions[i]!, error: errorInfo(err), durationMs: 0 }), err);
|
|
560
683
|
throw err;
|
|
561
684
|
}
|
|
562
685
|
if (decision.kind !== "approve") break;
|
|
563
686
|
const title = decision.title ?? `${op.id} needs ${approverLabel(decision.approver)}`;
|
|
564
687
|
try {
|
|
565
|
-
// A hold covers the call it held.
|
|
566
|
-
|
|
688
|
+
// A hold covers the call it held and the calls of a batch it held, each decided again once it is approved.
|
|
689
|
+
const held = calls.filter((_, i) => decisions[i]!.kind === "approve").map((c) => c.op.id);
|
|
690
|
+
const covers = [...new Set([op.id, ...held])];
|
|
691
|
+
await awaitApproval(run, title, { approver: decision.approver, covers }, place, {
|
|
567
692
|
op: op.id,
|
|
568
693
|
seq,
|
|
569
694
|
input: logged,
|
|
@@ -575,12 +700,12 @@ async function callOp(run: Run, id: string, input: unknown, place: Placed, ref?:
|
|
|
575
700
|
const code = errorCode(err);
|
|
576
701
|
if (code === "approval_rejected" || code === "approval_withdrawn") {
|
|
577
702
|
const approval = (err as SanomaError).data.approvalId as string;
|
|
578
|
-
|
|
579
|
-
await writeFailure(run, record, err);
|
|
703
|
+
await writeFailures(() => ({ approval, error: errorInfo(err), durationMs: 0 }), err);
|
|
580
704
|
}
|
|
581
705
|
throw err;
|
|
582
706
|
}
|
|
583
|
-
|
|
707
|
+
decisions = await decideStep();
|
|
708
|
+
decision = decisionOf(op, calls, decisions, run.state.collections);
|
|
584
709
|
}
|
|
585
710
|
|
|
586
711
|
const started = Date.now();
|
|
@@ -619,27 +744,23 @@ async function callOp(run: Run, id: string, input: unknown, place: Placed, ref?:
|
|
|
619
744
|
},
|
|
620
745
|
);
|
|
621
746
|
} catch (err) {
|
|
622
|
-
// The step failed for good; on replay DBOS rethrows the recorded error, and
|
|
623
|
-
//
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
);
|
|
629
|
-
if (isInfrastructureError(err, run.state)) skipped(record, `the call was interrupted (${errorMessage(err)})`);
|
|
630
|
-
else await writeFailure(run, record, err);
|
|
747
|
+
// The step failed for good; on replay DBOS rethrows the recorded error, and these records
|
|
748
|
+
// get the same ids. A success record would too, so a call has one op.called at most.
|
|
749
|
+
const failed = () => ({ error: errorInfo(err), durationMs: Date.now() - started, ...(attempt ? { attempt } : {}) });
|
|
750
|
+
if (isInfrastructureError(err, run.state)) {
|
|
751
|
+
for (const record of records(failed, started)) skipped(record, `the call was interrupted (${errorMessage(err)})`);
|
|
752
|
+
} else await writeFailures(failed, err, started);
|
|
631
753
|
throw err;
|
|
632
754
|
}
|
|
633
755
|
const { output, at, durationMs } = result;
|
|
756
|
+
const outputs = outputsOf(op, output);
|
|
634
757
|
try {
|
|
635
|
-
|
|
636
|
-
run,
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
),
|
|
642
|
-
);
|
|
758
|
+
for (const record of records(
|
|
759
|
+
(i) => ({ output: recorded(calls[i]!.op, outputs[i], run.workflow), durationMs, attempt: result.attempt }),
|
|
760
|
+
at,
|
|
761
|
+
)) {
|
|
762
|
+
await write(run, record);
|
|
763
|
+
}
|
|
643
764
|
} catch (err) {
|
|
644
765
|
// The vendor acted, and nothing records it. A workflow that caught this and called again
|
|
645
766
|
// would repeat the side effect under a new key, so the run makes no further calls.
|