@ayepi/work 0.1.0 → 0.2.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 +30 -18
- package/ayepi-work-deps-schedule.md +312 -0
- package/ayepi-work-ports.md +408 -0
- package/ayepi-work.md +926 -0
- package/dist/index.cjs +234 -56
- package/dist/index.d.cts +151 -50
- package/dist/index.d.ts +151 -50
- package/dist/index.js +234 -57
- package/package.json +5 -4
package/dist/index.cjs
CHANGED
|
@@ -88,6 +88,16 @@ const defaultCodec = {
|
|
|
88
88
|
*/
|
|
89
89
|
/** A v4 UUID. Thin wrapper over `node:crypto` so callers don't import it directly. */
|
|
90
90
|
const uuid = () => (0, node_crypto.randomUUID)();
|
|
91
|
+
/** The active work-id generator (default {@link uuid}); swapped by {@link setIdGenerator}. */
|
|
92
|
+
let idGenerator = uuid;
|
|
93
|
+
/**
|
|
94
|
+
* Override how work/group ids are generated process-wide — e.g. sortable or prefixed ids. Affects
|
|
95
|
+
* **build-time** work ids (builders are minted outside a system) and any engine ids without their own
|
|
96
|
+
* `generateId`. Pass nothing to reset to the default UUID generator.
|
|
97
|
+
*/
|
|
98
|
+
const setIdGenerator = (fn) => void (idGenerator = fn ?? uuid);
|
|
99
|
+
/** Generate an id via the active generator. */
|
|
100
|
+
const genId = () => idGenerator();
|
|
91
101
|
/** Resolve after `ms` (an unref'd timer, so it never keeps the process alive). */
|
|
92
102
|
function sleep(ms) {
|
|
93
103
|
return new Promise((resolve) => {
|
|
@@ -426,7 +436,7 @@ const rehydrate = (s) => ({
|
|
|
426
436
|
});
|
|
427
437
|
/** Build a {@link DEPENDENCY_TYPE} work item from a (re-usable) input — a fresh queue id, same key. */
|
|
428
438
|
const buildDependency = (input) => ({
|
|
429
|
-
id:
|
|
439
|
+
id: genId(),
|
|
430
440
|
type: DEPENDENCY_TYPE,
|
|
431
441
|
input
|
|
432
442
|
});
|
|
@@ -436,7 +446,7 @@ const buildDependency = (input) => ({
|
|
|
436
446
|
*/
|
|
437
447
|
function dependency(opts) {
|
|
438
448
|
return buildDependency({
|
|
439
|
-
key:
|
|
449
|
+
key: genId(),
|
|
440
450
|
on: opts.on.map(toId),
|
|
441
451
|
queue: opts.queue.map(toSerialized),
|
|
442
452
|
config: opts.config ?? "all-success",
|
|
@@ -459,12 +469,9 @@ const dependencyHandler = async (input, ctx) => {
|
|
|
459
469
|
});
|
|
460
470
|
const freshById = new Map(unknownIds.map((id, i) => [id, fresh[i]]));
|
|
461
471
|
const states = input.on.map((id) => resolved[id] ? { status: resolved[id] } : freshById.get(id));
|
|
462
|
-
if (conditionMet(input.config, states)) {
|
|
463
|
-
if (await ctx.claim(`dep:${input.key}:fired`)) ctx.queue(input.queue.map(rehydrate));
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
472
|
+
if (conditionMet(input.config, states)) return await ctx.claim(`dep:${input.key}:fired`) ? ctx.queue(input.queue.map(rehydrate)) : ctx.void();
|
|
466
473
|
if (input.deadline !== void 0 && Date.now() >= input.deadline) throw new Error(`dependency: timed out waiting for [${input.on.join(", ")}]`);
|
|
467
|
-
ctx.queue(buildDependency({
|
|
474
|
+
return ctx.queue(buildDependency({
|
|
468
475
|
...input,
|
|
469
476
|
resolved
|
|
470
477
|
}), { delay: input.poll });
|
|
@@ -611,6 +618,7 @@ const WORK_METRICS = {
|
|
|
611
618
|
retried: "work.retried",
|
|
612
619
|
deferred: "work.deferred",
|
|
613
620
|
rescheduled: "work.rescheduled",
|
|
621
|
+
expired: "work.expired",
|
|
614
622
|
active: "work.active",
|
|
615
623
|
pending: "work.pending",
|
|
616
624
|
running: "work.running",
|
|
@@ -643,6 +651,7 @@ const createWorkStats = (metrics = (0, _ayepi_core_stats.createMetrics)()) => {
|
|
|
643
651
|
retried: metrics.counter(WORK_METRICS.retried, l, { description: "attempt-advancing retries" }),
|
|
644
652
|
deferred: metrics.counter(WORK_METRICS.deferred, l, { description: "handler/classifier reschedules" }),
|
|
645
653
|
rescheduled: metrics.counter(WORK_METRICS.rescheduled, l, { description: "early-arrival re-pushes" }),
|
|
654
|
+
expired: metrics.counter(WORK_METRICS.expired, l, { description: "items expired past their deadline" }),
|
|
646
655
|
active: metrics.gauge(WORK_METRICS.active, l, { description: "items in flight (pending + running)" }),
|
|
647
656
|
pending: metrics.gauge(WORK_METRICS.pending, l, { description: "items admitted, awaiting a doer slot" }),
|
|
648
657
|
running: metrics.gauge(WORK_METRICS.running, l, { description: "items executing" }),
|
|
@@ -738,6 +747,7 @@ const createWorkStats = (metrics = (0, _ayepi_core_stats.createMetrics)()) => {
|
|
|
738
747
|
h.lastFailed.set(at);
|
|
739
748
|
},
|
|
740
749
|
retried: (type) => void handles(type).retried.inc(),
|
|
750
|
+
expired: (type) => void handles(type).expired.inc(),
|
|
741
751
|
deferred: (type, delayMs) => {
|
|
742
752
|
const h = handles(type);
|
|
743
753
|
h.deferred.inc();
|
|
@@ -757,23 +767,26 @@ const createWorkStats = (metrics = (0, _ayepi_core_stats.createMetrics)()) => {
|
|
|
757
767
|
*
|
|
758
768
|
* Defining a work type with {@link defineWork} yields a **callable builder**: call it
|
|
759
769
|
* with the work's exact input and you get a type-checked, queueable {@link Work} (with
|
|
760
|
-
* a build-time id)
|
|
761
|
-
* `
|
|
762
|
-
*
|
|
770
|
+
* a build-time id). A handler **returns a {@link WorkResult}** — `ctx.result(value)`,
|
|
771
|
+
* `ctx.queue(...)`, `ctx.void()`, or a `.next(...)` dependency chain — so each work carries
|
|
772
|
+
* two inferred types: its *awaited-alone* result `S` and its *group* contribution `G`.
|
|
773
|
+
* {@link createWork} takes a `const` tuple of builders and produces a {@link WorkSystem} whose
|
|
774
|
+
* `enqueue` is fully checked — by instance or by name.
|
|
763
775
|
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
776
|
+
* `enqueue(root).group()` resolves to `root`'s `G` — a **precise union from the workflow
|
|
777
|
+
* structure**, not the whole registry. All durations are **milliseconds**.
|
|
766
778
|
*
|
|
767
779
|
* @module
|
|
768
780
|
*/
|
|
769
781
|
const build = (name, input) => ({
|
|
770
|
-
id:
|
|
782
|
+
id: genId(),
|
|
771
783
|
type: name,
|
|
772
784
|
input
|
|
773
785
|
});
|
|
774
786
|
/**
|
|
775
|
-
* Define a work type.
|
|
776
|
-
*
|
|
787
|
+
* Define a work type. The handler **returns a {@link WorkResult}** — `ctx.result(value)`,
|
|
788
|
+
* `ctx.queue(...)`, `ctx.void()`, or a `.next(...)` chain — and its `S`/`G` types are inferred
|
|
789
|
+
* from that return: `S` is what `.result()` resolves to alone, `G` what it contributes to the group.
|
|
777
790
|
*/
|
|
778
791
|
function defineWork(name, handler, opts = {}) {
|
|
779
792
|
return Object.assign((input) => build(name, input), {
|
|
@@ -786,10 +799,10 @@ function defineWork(name, handler, opts = {}) {
|
|
|
786
799
|
});
|
|
787
800
|
}
|
|
788
801
|
/**
|
|
789
|
-
* Define a **batched** work type. Items still enqueue, retry, prioritize, and join
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
802
|
+
* Define a **batched** work type. Items still enqueue, retry, prioritize, and join groups
|
|
803
|
+
* individually, but execute together via {@link BatchConfig.run} once `size` accumulate or `maxWait`
|
|
804
|
+
* ms elapse — so each `.result()` resolves to its aligned output `O` (which is also its group
|
|
805
|
+
* contribution). The per-type {@link WorkOptions.doer} governs how many *batches* run at once.
|
|
793
806
|
*/
|
|
794
807
|
function defineBatchWork(name, config) {
|
|
795
808
|
const { size, maxWait, run, ...options } = config;
|
|
@@ -798,7 +811,7 @@ function defineBatchWork(name, config) {
|
|
|
798
811
|
maxWait,
|
|
799
812
|
run
|
|
800
813
|
};
|
|
801
|
-
const handler = async (input) => (await run([input]))[0];
|
|
814
|
+
const handler = async (input, ctx) => ctx.result((await run([input]))[0]);
|
|
802
815
|
return Object.assign((input) => build(name, input), {
|
|
803
816
|
type: name,
|
|
804
817
|
def: {
|
|
@@ -859,6 +872,8 @@ const DEP_RETRY_ATTEMPTS = 1;
|
|
|
859
872
|
const REDRIVE_DEFAULT = 10;
|
|
860
873
|
const unref = (t) => void t.unref?.();
|
|
861
874
|
const errString = (err) => err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
875
|
+
/** Brand stamped on a runtime `WorkResult` node — distinguishes a handler instruction from a raw value. */
|
|
876
|
+
const WR_BRAND = Symbol.for("ayepi.work.result");
|
|
862
877
|
/**
|
|
863
878
|
* Create a work system. Zero-config (`createWork()`) uses the bundled in-memory backend
|
|
864
879
|
* and an {@link unlimitedDoer}; pass `work: [...] as const` for a typed registry, a
|
|
@@ -881,6 +896,8 @@ function createWork(opts = {}) {
|
|
|
881
896
|
const logWith = opts.logWith ?? identityLogWith;
|
|
882
897
|
const now = opts.now ?? Date.now;
|
|
883
898
|
const random = opts.random ?? Math.random;
|
|
899
|
+
const newId = opts.generateId ?? genId;
|
|
900
|
+
const strictReturnDefault = opts.strictReturn ?? true;
|
|
884
901
|
const attemptsOf = (r) => r.attempts ?? (0, _ayepi_core_retry.getDefaultRetryOptions)().attempts ?? 1;
|
|
885
902
|
const stats = createWorkStats(opts.metrics);
|
|
886
903
|
const k = (suffix) => prefix + suffix;
|
|
@@ -944,11 +961,13 @@ function createWork(opts = {}) {
|
|
|
944
961
|
const resolveOptions = (type, input, qOpts) => {
|
|
945
962
|
const tOpts = registry.get(type)?.def.options;
|
|
946
963
|
const computed = tOpts?.options?.(input) ?? {};
|
|
964
|
+
const timeout = qOpts?.timeout ?? computed.timeout ?? tOpts?.timeout;
|
|
947
965
|
return {
|
|
948
966
|
delay: qOpts?.delay ?? computed.delay ?? 0,
|
|
949
967
|
runAt: qOpts?.runAt ?? computed.runAt,
|
|
950
968
|
priority: qOpts?.priority ?? computed.priority ?? tOpts?.priority ?? 0,
|
|
951
969
|
group: qOpts?.group ?? computed.group ?? tOpts?.group,
|
|
970
|
+
deadline: qOpts?.deadline ?? computed.deadline ?? (timeout !== void 0 ? now() + timeout : void 0),
|
|
952
971
|
retry: {
|
|
953
972
|
...(0, _ayepi_core_retry.getDefaultRetryOptions)(),
|
|
954
973
|
...opts.retry,
|
|
@@ -1010,16 +1029,56 @@ function createWork(opts = {}) {
|
|
|
1010
1029
|
});
|
|
1011
1030
|
maybeUnhandled(groupId);
|
|
1012
1031
|
};
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1032
|
+
const isWR = (x) => typeof x === "object" && x !== null && x[WR_BRAND] === true;
|
|
1033
|
+
const toItems = (x) => Array.isArray(x) ? [...x] : [x];
|
|
1034
|
+
const makeNode = (data) => {
|
|
1035
|
+
const node = {
|
|
1036
|
+
[WR_BRAND]: true,
|
|
1037
|
+
data,
|
|
1038
|
+
next(nextItems, condition = "all-success", options) {
|
|
1039
|
+
if (data.kind !== "queue") throw new Error(".next() is only valid on ctx.queue(...)");
|
|
1040
|
+
const q = toItems(nextItems);
|
|
1041
|
+
data.chains.push({
|
|
1042
|
+
on: data.frontier,
|
|
1043
|
+
queue: q,
|
|
1044
|
+
condition,
|
|
1045
|
+
options
|
|
1046
|
+
});
|
|
1047
|
+
data.frontier = q;
|
|
1048
|
+
return node;
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
return node;
|
|
1052
|
+
};
|
|
1053
|
+
/** Collect the Work ids to wait on within items (recursing into nested `queue` nodes). */
|
|
1054
|
+
const workIdsOf = (items) => items.flatMap((it) => isWR(it) ? it.data.kind === "queue" ? workIdsOf(it.data.items) : [] : [it.id]);
|
|
1055
|
+
const makeContext = (meta, created) => {
|
|
1056
|
+
const reg = (n) => (created.add(n), n);
|
|
1015
1057
|
return {
|
|
1016
|
-
id:
|
|
1017
|
-
groupId:
|
|
1018
|
-
attempt:
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1058
|
+
id: meta.id,
|
|
1059
|
+
groupId: meta.groupId,
|
|
1060
|
+
attempt: meta.attempt,
|
|
1061
|
+
parent: meta.parent,
|
|
1062
|
+
dependents: meta.dependents,
|
|
1063
|
+
result: (value, options) => reg(makeNode({
|
|
1064
|
+
kind: "result",
|
|
1065
|
+
value,
|
|
1066
|
+
final: !!options?.final,
|
|
1067
|
+
append: options?.append
|
|
1068
|
+
})),
|
|
1069
|
+
queue: (items, options) => {
|
|
1070
|
+
const arr = toItems(items);
|
|
1071
|
+
return reg(makeNode({
|
|
1072
|
+
kind: "queue",
|
|
1073
|
+
items: arr,
|
|
1074
|
+
options,
|
|
1075
|
+
chains: [],
|
|
1076
|
+
frontier: arr
|
|
1077
|
+
}));
|
|
1078
|
+
},
|
|
1079
|
+
void: () => reg(makeNode({ kind: "void" })),
|
|
1080
|
+
states: storeStates,
|
|
1081
|
+
claim: storeClaim
|
|
1023
1082
|
};
|
|
1024
1083
|
};
|
|
1025
1084
|
const storeStates = (ids) => Promise.all(ids.map(readState));
|
|
@@ -1028,7 +1087,7 @@ function createWork(opts = {}) {
|
|
|
1028
1087
|
const pushEnvelope = (env, delay) => port(() => Promise.resolve(queueFor(env.type).push(JSON.stringify(env), { delay })).then(() => void 0));
|
|
1029
1088
|
/** Resolve when a deferred/scheduled item should next run (absolute epoch ms). */
|
|
1030
1089
|
const resolveRunAt = (when) => when.runAt ?? now() + (when.delay ?? 0);
|
|
1031
|
-
const submitQueued = (id, type, input, groupId, ro) => {
|
|
1090
|
+
const submitQueued = (id, type, input, groupId, ro, meta) => {
|
|
1032
1091
|
const queueAt = now();
|
|
1033
1092
|
const startAt = ro.runAt ?? queueAt + ro.delay;
|
|
1034
1093
|
return (async () => {
|
|
@@ -1054,7 +1113,10 @@ function createWork(opts = {}) {
|
|
|
1054
1113
|
attempt: 1,
|
|
1055
1114
|
priority: ro.priority,
|
|
1056
1115
|
group: ro.group,
|
|
1057
|
-
retry: ro.retry
|
|
1116
|
+
retry: ro.retry,
|
|
1117
|
+
deadline: ro.deadline,
|
|
1118
|
+
parent: meta?.parent,
|
|
1119
|
+
dependents: meta?.dependents
|
|
1058
1120
|
}, Math.max(0, startAt - queueAt));
|
|
1059
1121
|
} catch (err) {
|
|
1060
1122
|
await undoHold(groupId);
|
|
@@ -1066,10 +1128,14 @@ function createWork(opts = {}) {
|
|
|
1066
1128
|
id,
|
|
1067
1129
|
type,
|
|
1068
1130
|
groupId,
|
|
1131
|
+
parent: meta?.parent,
|
|
1132
|
+
dependents: meta?.dependents,
|
|
1069
1133
|
at: queueAt
|
|
1070
1134
|
});
|
|
1071
1135
|
})();
|
|
1072
1136
|
};
|
|
1137
|
+
/** Submit a built {@link Work} (used by the result-apply walk), carrying its parent/dependents metadata. */
|
|
1138
|
+
const submitWork = (work, groupId, options, meta) => submitQueued(work.id, work.type, work.input, groupId, resolveOptions(work.type, work.input, options), meta);
|
|
1073
1139
|
/** Re-enter the queue for a retry: a fresh delivery with `attempt + 1` and a recomputed `startAt`. */
|
|
1074
1140
|
const rePush = async (env, delay) => {
|
|
1075
1141
|
const startAt = now() + delay;
|
|
@@ -1092,11 +1158,11 @@ function createWork(opts = {}) {
|
|
|
1092
1158
|
};
|
|
1093
1159
|
const enqueueImpl = (a, b, c) => {
|
|
1094
1160
|
const fromName = typeof a === "string";
|
|
1095
|
-
const id = fromName ?
|
|
1161
|
+
const id = fromName ? newId() : a.id;
|
|
1096
1162
|
const type = fromName ? a : a.type;
|
|
1097
1163
|
const input = fromName ? b : a.input;
|
|
1098
1164
|
const qOpts = fromName ? c : b;
|
|
1099
|
-
const groupId =
|
|
1165
|
+
const groupId = newId();
|
|
1100
1166
|
const ro = resolveOptions(type, input, qOpts);
|
|
1101
1167
|
return makeHandle(id, type, groupId, ro.skipQueue ? runImmediate(id, type, input, groupId, ro) : submitQueued(id, type, input, groupId, ro));
|
|
1102
1168
|
};
|
|
@@ -1367,6 +1433,50 @@ function createWork(opts = {}) {
|
|
|
1367
1433
|
attempt: env.attempt,
|
|
1368
1434
|
error,
|
|
1369
1435
|
willRetry: false,
|
|
1436
|
+
parent: env.parent,
|
|
1437
|
+
dependents: env.dependents,
|
|
1438
|
+
at
|
|
1439
|
+
});
|
|
1440
|
+
await settleGroup(env.groupId);
|
|
1441
|
+
};
|
|
1442
|
+
/** Whether an item is past its deadline (so it should expire rather than run or retry). */
|
|
1443
|
+
const expired = (env) => env.deadline !== void 0 && now() > env.deadline;
|
|
1444
|
+
/** Terminal expiry: like a dead-letter, but emits `'expired'` (the item didn't start+finish by its deadline). */
|
|
1445
|
+
const expireItem = async (p, env, q) => {
|
|
1446
|
+
const at = now();
|
|
1447
|
+
const error = "deadline exceeded";
|
|
1448
|
+
stats.expired(env.type);
|
|
1449
|
+
await port(() => Promise.resolve(store.set(k(`item:${env.id}:error`), error, RESULT_TTL)));
|
|
1450
|
+
await setState({
|
|
1451
|
+
id: env.id,
|
|
1452
|
+
type: env.type,
|
|
1453
|
+
status: "dead",
|
|
1454
|
+
attempt: env.attempt,
|
|
1455
|
+
error,
|
|
1456
|
+
queueAt: env.queueAt,
|
|
1457
|
+
startAt: env.startAt,
|
|
1458
|
+
endAt: at,
|
|
1459
|
+
priority: env.priority,
|
|
1460
|
+
group: env.group
|
|
1461
|
+
}, env.groupId);
|
|
1462
|
+
if (p && q) {
|
|
1463
|
+
await Promise.resolve(q.deadLetter?.(p.body, error));
|
|
1464
|
+
await port(() => Promise.resolve(q.ack(p)));
|
|
1465
|
+
}
|
|
1466
|
+
notify({
|
|
1467
|
+
kind: "done",
|
|
1468
|
+
id: env.id,
|
|
1469
|
+
groupId: env.groupId,
|
|
1470
|
+
error
|
|
1471
|
+
});
|
|
1472
|
+
emit({
|
|
1473
|
+
kind: "expired",
|
|
1474
|
+
id: env.id,
|
|
1475
|
+
type: env.type,
|
|
1476
|
+
groupId: env.groupId,
|
|
1477
|
+
deadline: env.deadline,
|
|
1478
|
+
parent: env.parent,
|
|
1479
|
+
dependents: env.dependents,
|
|
1370
1480
|
at
|
|
1371
1481
|
});
|
|
1372
1482
|
await settleGroup(env.groupId);
|
|
@@ -1399,6 +1509,8 @@ function createWork(opts = {}) {
|
|
|
1399
1509
|
type: env.type,
|
|
1400
1510
|
groupId: env.groupId,
|
|
1401
1511
|
attempt: env.attempt,
|
|
1512
|
+
parent: env.parent,
|
|
1513
|
+
dependents: env.dependents,
|
|
1402
1514
|
at: runAt
|
|
1403
1515
|
});
|
|
1404
1516
|
};
|
|
@@ -1436,6 +1548,8 @@ function createWork(opts = {}) {
|
|
|
1436
1548
|
groupId: env.groupId,
|
|
1437
1549
|
attempt: env.attempt,
|
|
1438
1550
|
result: output,
|
|
1551
|
+
parent: env.parent,
|
|
1552
|
+
dependents: env.dependents,
|
|
1439
1553
|
at
|
|
1440
1554
|
});
|
|
1441
1555
|
await settleGroup(env.groupId);
|
|
@@ -1443,6 +1557,10 @@ function createWork(opts = {}) {
|
|
|
1443
1557
|
const finishFailure = async (p, env, runAt, err, q) => {
|
|
1444
1558
|
if (env.attempt < attemptsOf(env.retry)) {
|
|
1445
1559
|
const delay = (0, _ayepi_core_retry.backoff)(env.attempt, env.retry, random);
|
|
1560
|
+
if (env.deadline !== void 0 && now() + delay > env.deadline) {
|
|
1561
|
+
await expireItem(p, env, q);
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1446
1564
|
stats.retried(env.type);
|
|
1447
1565
|
emit({
|
|
1448
1566
|
kind: "failed",
|
|
@@ -1452,6 +1570,8 @@ function createWork(opts = {}) {
|
|
|
1452
1570
|
attempt: env.attempt,
|
|
1453
1571
|
error: errString(err),
|
|
1454
1572
|
willRetry: true,
|
|
1573
|
+
parent: env.parent,
|
|
1574
|
+
dependents: env.dependents,
|
|
1455
1575
|
at: now()
|
|
1456
1576
|
});
|
|
1457
1577
|
if (p && q) await port(() => Promise.resolve(q.ack(p)));
|
|
@@ -1500,35 +1620,85 @@ function createWork(opts = {}) {
|
|
|
1500
1620
|
}
|
|
1501
1621
|
await finishFailure(p, env, runAt, err, q);
|
|
1502
1622
|
};
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
groupId:
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1623
|
+
/** Write a group-result contribution: `final` locks it; `append` folds into the existing value (read-modify-write, best-effort under concurrency). */
|
|
1624
|
+
const writeGroupResult = async (groupId, value, final, append) => {
|
|
1625
|
+
const resultKey = k(`group:${groupId}:result`);
|
|
1626
|
+
const finalKey = k(`group:${groupId}:final`);
|
|
1627
|
+
if (await Promise.resolve(store.get(finalKey)) !== void 0) return;
|
|
1628
|
+
let toWrite = value;
|
|
1629
|
+
if (append) {
|
|
1630
|
+
const ex = await Promise.resolve(store.get(resultKey));
|
|
1631
|
+
toWrite = append(ex === void 0 ? void 0 : globalCodec.parse(ex));
|
|
1632
|
+
}
|
|
1633
|
+
await port(() => Promise.resolve(store.set(resultKey, globalCodec.stringify(toWrite), RESULT_TTL)).then(() => void 0));
|
|
1634
|
+
if (final) await port(() => Promise.resolve(store.set(finalKey, "1", RESULT_TTL)).then(() => void 0));
|
|
1635
|
+
};
|
|
1636
|
+
/**
|
|
1637
|
+
* Run the returned WorkResult tree: enqueue works, build native dependencies (`.next`), write group
|
|
1638
|
+
* contributions. `assignDeps` (the `on` list when this is a fired dependency) tags non-dependency
|
|
1639
|
+
* children's `dependents`.
|
|
1640
|
+
*/
|
|
1641
|
+
const applyNode = (node, env, pending, assignDeps) => {
|
|
1642
|
+
const d = node.data;
|
|
1643
|
+
if (d.kind === "void") return;
|
|
1644
|
+
if (d.kind === "result") {
|
|
1645
|
+
pending.push(writeGroupResult(env.groupId, d.value, d.final, d.append));
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
for (const item of d.items) if (isWR(item)) applyNode(item, env, pending, assignDeps);
|
|
1649
|
+
else pending.push(submitWork(item, env.groupId, d.options, {
|
|
1650
|
+
parent: env.id,
|
|
1651
|
+
dependents: item.type === "@work/dependency" ? void 0 : assignDeps
|
|
1652
|
+
}));
|
|
1653
|
+
for (const chain of d.chains) {
|
|
1654
|
+
const dep = dependency({
|
|
1655
|
+
on: workIdsOf(chain.on),
|
|
1656
|
+
queue: chain.queue,
|
|
1657
|
+
config: chain.condition
|
|
1658
|
+
});
|
|
1659
|
+
pending.push(submitWork(dep, env.groupId, chain.options, { parent: env.id }));
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
/** Strict-return: every created WorkResult must be reachable from the returned `root` (else it silently does nothing). */
|
|
1663
|
+
const reachableCheck = (root, created) => {
|
|
1664
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1665
|
+
const walk = (n) => {
|
|
1666
|
+
if (seen.has(n)) return;
|
|
1667
|
+
seen.add(n);
|
|
1668
|
+
if (n.data.kind === "queue") {
|
|
1669
|
+
for (const it of n.data.items) if (isWR(it)) walk(it);
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
walk(root);
|
|
1673
|
+
for (const n of created) if (!seen.has(n)) throw new Error(`work "${root.data.kind}" created a ctx.queue()/result()/void() that was not returned (set strictReturn:false to allow detached instructions)`);
|
|
1674
|
+
};
|
|
1517
1675
|
/**
|
|
1518
1676
|
* The task handed to a doer: run a single item (leased from queue `q`, or `skipQueue` with `p`/`q`
|
|
1519
1677
|
* null). `release` is the {@link claim} teardown — {@link scoped} runs it in `finally`, so the
|
|
1520
|
-
* active-set entry + heartbeat are always cleaned up regardless of how the run ends.
|
|
1678
|
+
* active-set entry + heartbeat are always cleaned up regardless of how the run ends. The handler
|
|
1679
|
+
* returns a {@link WorkResult}; we then apply its tree (enqueue children, write group contributions).
|
|
1521
1680
|
*/
|
|
1522
1681
|
const execute = (p, env, input, def, q, release) => scoped(release, async () => {
|
|
1523
1682
|
const runAt = now();
|
|
1524
1683
|
markRunningSync(env, runAt);
|
|
1525
1684
|
const running = persistRunning(env, runAt).catch((err) => report(err, "commit"));
|
|
1526
|
-
const
|
|
1527
|
-
const ctx =
|
|
1528
|
-
|
|
1685
|
+
const created = /* @__PURE__ */ new Set();
|
|
1686
|
+
const ctx = makeContext({
|
|
1687
|
+
id: env.id,
|
|
1688
|
+
groupId: env.groupId,
|
|
1689
|
+
attempt: env.attempt,
|
|
1690
|
+
parent: env.parent,
|
|
1691
|
+
dependents: env.dependents
|
|
1692
|
+
}, created);
|
|
1693
|
+
let itemValue;
|
|
1529
1694
|
try {
|
|
1530
|
-
|
|
1695
|
+
const ret = await logWith(merge(opts.logContext?.(input, env.type), def.options.logContext?.(input)), () => Promise.resolve(def.handler(input, ctx)));
|
|
1696
|
+
if (!isWR(ret)) throw new Error(`work "${env.type}" handler must return a WorkResult — ctx.result(...), ctx.queue(...), or ctx.void()`);
|
|
1697
|
+
if (def.options.strictReturn ?? strictReturnDefault) reachableCheck(ret, created);
|
|
1698
|
+
const pending = [];
|
|
1699
|
+
applyNode(ret, env, pending, env.type === "@work/dependency" ? input.on : void 0);
|
|
1531
1700
|
await Promise.all(pending);
|
|
1701
|
+
itemValue = ret.data.kind === "result" ? ret.data.value : void 0;
|
|
1532
1702
|
} catch (err) {
|
|
1533
1703
|
await running;
|
|
1534
1704
|
await handleFailure(p, env, runAt, err, q);
|
|
@@ -1536,7 +1706,7 @@ function createWork(opts = {}) {
|
|
|
1536
1706
|
}
|
|
1537
1707
|
try {
|
|
1538
1708
|
await running;
|
|
1539
|
-
await finishSuccess(p, env, runAt,
|
|
1709
|
+
await finishSuccess(p, env, runAt, itemValue, q);
|
|
1540
1710
|
} catch (err) {
|
|
1541
1711
|
report(err, "commit");
|
|
1542
1712
|
}
|
|
@@ -1555,7 +1725,7 @@ function createWork(opts = {}) {
|
|
|
1555
1725
|
await Promise.all(items.map((it) => handleFailure(it.p, it.env, runAt, err, it.q)));
|
|
1556
1726
|
return;
|
|
1557
1727
|
}
|
|
1558
|
-
await Promise.all(items.map((it, i) => Promise.resolve(finishSuccess(it.p, it.env, runAt, outputs[i], it.q)).catch((err) => report(err, "commit"))));
|
|
1728
|
+
await Promise.all(items.map((it, i) => Promise.resolve(writeGroupResult(it.env.groupId, outputs[i], false)).then(() => finishSuccess(it.p, it.env, runAt, outputs[i], it.q)).catch((err) => report(err, "commit"))));
|
|
1559
1729
|
});
|
|
1560
1730
|
const batchers = /* @__PURE__ */ new Map();
|
|
1561
1731
|
const batcherFor = (type) => {
|
|
@@ -1608,6 +1778,10 @@ function createWork(opts = {}) {
|
|
|
1608
1778
|
await Promise.resolve(q.ack(p));
|
|
1609
1779
|
return false;
|
|
1610
1780
|
}
|
|
1781
|
+
if (expired(env)) {
|
|
1782
|
+
await expireItem(p, env, q);
|
|
1783
|
+
return false;
|
|
1784
|
+
}
|
|
1611
1785
|
const due = now();
|
|
1612
1786
|
if (env.startAt - due > SCHED_TOLERANCE) {
|
|
1613
1787
|
await reschedule(p, env, q, env.startAt);
|
|
@@ -1781,7 +1955,10 @@ function createWork(opts = {}) {
|
|
|
1781
1955
|
await sleep(pollInterval);
|
|
1782
1956
|
}
|
|
1783
1957
|
};
|
|
1784
|
-
registry.set(DEPENDENCY_TYPE, defineWork(DEPENDENCY_TYPE, dependencyHandler, {
|
|
1958
|
+
registry.set(DEPENDENCY_TYPE, defineWork(DEPENDENCY_TYPE, dependencyHandler, {
|
|
1959
|
+
retry: { attempts: DEP_RETRY_ATTEMPTS },
|
|
1960
|
+
strictReturn: false
|
|
1961
|
+
}));
|
|
1785
1962
|
const start = () => {
|
|
1786
1963
|
if (running) return;
|
|
1787
1964
|
running = true;
|
|
@@ -1893,7 +2070,7 @@ function adaptiveDelay(opts = {}) {
|
|
|
1893
2070
|
* ```ts
|
|
1894
2071
|
* import { defineWork, createWork } from '@ayepi/work'
|
|
1895
2072
|
*
|
|
1896
|
-
* const add = defineWork('add', (i: { a: number; b: number }) => i.a + i.b)
|
|
2073
|
+
* const add = defineWork('add', (i: { a: number; b: number }, ctx) => ctx.result(i.a + i.b))
|
|
1897
2074
|
* const w = createWork({ work: [add] as const })
|
|
1898
2075
|
*
|
|
1899
2076
|
* const sum = await w.enqueue(add({ a: 1, b: 2 })).result() // 3, typed as number
|
|
@@ -2009,6 +2186,7 @@ Object.defineProperty(exports, "setDefaultRetryOptions", {
|
|
|
2009
2186
|
return _ayepi_core_retry.setDefaultRetryOptions;
|
|
2010
2187
|
}
|
|
2011
2188
|
});
|
|
2189
|
+
exports.setIdGenerator = setIdGenerator;
|
|
2012
2190
|
exports.start = start;
|
|
2013
2191
|
exports.stop = stop;
|
|
2014
2192
|
Object.defineProperty(exports, "unlimitedDoer", {
|