@cnstra/pg-boss 0.1.0 → 0.3.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 +45 -16
- package/dist/chunk-2JTKI4GS.mjs +10 -0
- package/dist/chunk-2JTKI4GS.mjs.map +1 -0
- package/dist/chunk-XJFC72LU.mjs +35 -0
- package/dist/chunk-XJFC72LU.mjs.map +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +61 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +40 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +52 -64
- package/dist/index.d.ts +52 -64
- package/dist/index.js +41 -85
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +44 -85
- package/dist/index.mjs.map +1 -1
- package/dist/postgres-progress.d.mts +75 -0
- package/dist/postgres-progress.d.ts +75 -0
- package/dist/postgres-progress.js +142 -0
- package/dist/postgres-progress.js.map +1 -0
- package/dist/postgres-progress.mjs +94 -0
- package/dist/postgres-progress.mjs.map +1 -0
- package/package.json +41 -7
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
4
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
5
6
|
var __export = (target, all) => {
|
|
6
7
|
for (var name in all)
|
|
7
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -26,105 +27,60 @@ __export(index_exports, {
|
|
|
26
27
|
module.exports = __toCommonJS(index_exports);
|
|
27
28
|
|
|
28
29
|
// src/worker.ts
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
error: resp.error != null ? String(resp.error) : null,
|
|
43
|
-
at: Date.now()
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
function createCNSWorker(options) {
|
|
47
|
-
const { boss, cns, registry, queue, progress, workOptions } = options;
|
|
48
|
-
const runJob = async (job) => {
|
|
49
|
-
var _a, _b, _c;
|
|
50
|
-
const { collateralName, payload, maxNeuronHops } = job.data;
|
|
51
|
-
const collateral = registry.getCollateral(collateralName);
|
|
52
|
-
if (!collateral) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
`[@cnstra/pg-boss] Unknown collateral "${collateralName}". Register it in the CNSPersistOptionsRegistry.`
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
const startedAt = Date.now();
|
|
58
|
-
let hopCount = 0;
|
|
59
|
-
await ((_a = progress == null ? void 0 : progress.onStarted) == null ? void 0 : _a.call(progress, {
|
|
30
|
+
var import_persist = require("@cnstra/persist");
|
|
31
|
+
var import_broker = require("@cnstra/broker");
|
|
32
|
+
async function createCNSWorker(options) {
|
|
33
|
+
const { boss, cns, registry, queue, observe, resume, workOptions } = options;
|
|
34
|
+
const serializer = resume ? new import_persist.CNSProgressSerializer(registry) : void 0;
|
|
35
|
+
const drain = (0, import_broker.createDrainState)();
|
|
36
|
+
const runJob = /* @__PURE__ */ __name(async (job) => {
|
|
37
|
+
var _a;
|
|
38
|
+
await (0, import_broker.runStimulationJob)({
|
|
39
|
+
pkgName: "@cnstra/pg-boss",
|
|
40
|
+
stopping: drain.isStopping(),
|
|
41
|
+
cns,
|
|
42
|
+
registry,
|
|
60
43
|
jobId: job.id,
|
|
61
|
-
collateralName,
|
|
62
|
-
payload,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
job.id,
|
|
73
|
-
hopCount,
|
|
74
|
-
resp,
|
|
75
|
-
cns,
|
|
76
|
-
registry
|
|
77
|
-
);
|
|
78
|
-
hopCount += 1;
|
|
79
|
-
await ((_a2 = progress == null ? void 0 : progress.onHop) == null ? void 0 : _a2.call(progress, rec));
|
|
80
|
-
}
|
|
44
|
+
collateralName: job.data.collateralName,
|
|
45
|
+
payload: job.data.payload,
|
|
46
|
+
maxNeuronHops: job.data.maxNeuronHops,
|
|
47
|
+
// Each pg-boss retry is a new attempt (retryCount is 0-based).
|
|
48
|
+
attemptNumber: ((_a = job.retryCount) != null ? _a : 0) + 1,
|
|
49
|
+
observe,
|
|
50
|
+
serializer,
|
|
51
|
+
resumeRepo: resume == null ? void 0 : resume.repository,
|
|
52
|
+
resumeStrategy: resume == null ? void 0 : resume.strategy,
|
|
53
|
+
abort: drain.abort,
|
|
54
|
+
active: drain.active
|
|
81
55
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
await ((_b = progress == null ? void 0 : progress.onCompleted) == null ? void 0 : _b.call(progress, {
|
|
85
|
-
jobId: job.id,
|
|
86
|
-
collateralName,
|
|
87
|
-
payload,
|
|
88
|
-
status: "completed",
|
|
89
|
-
startedAt,
|
|
90
|
-
completedAt: Date.now(),
|
|
91
|
-
hopCount
|
|
92
|
-
}));
|
|
93
|
-
} catch (err) {
|
|
94
|
-
await ((_c = progress == null ? void 0 : progress.onCompleted) == null ? void 0 : _c.call(progress, {
|
|
95
|
-
jobId: job.id,
|
|
96
|
-
collateralName,
|
|
97
|
-
payload,
|
|
98
|
-
status: "failed",
|
|
99
|
-
startedAt,
|
|
100
|
-
completedAt: Date.now(),
|
|
101
|
-
hopCount,
|
|
102
|
-
error: err instanceof Error ? err.message : String(err)
|
|
103
|
-
}));
|
|
104
|
-
throw err;
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
const handler = async (jobs) => {
|
|
56
|
+
}, "runJob");
|
|
57
|
+
const handler = /* @__PURE__ */ __name(async (jobs) => {
|
|
108
58
|
for (const job of jobs) {
|
|
109
59
|
await runJob(job);
|
|
110
60
|
}
|
|
61
|
+
}, "handler");
|
|
62
|
+
const workId = await (workOptions ? boss.work(queue, workOptions, handler) : boss.work(queue, handler));
|
|
63
|
+
const shutdown = /* @__PURE__ */ __name(async (opts) => {
|
|
64
|
+
var _a;
|
|
65
|
+
await drain.drainAndAbort((_a = opts == null ? void 0 : opts.drainMs) != null ? _a : 1e4);
|
|
66
|
+
}, "shutdown");
|
|
67
|
+
return {
|
|
68
|
+
workId,
|
|
69
|
+
shutdown
|
|
111
70
|
};
|
|
112
|
-
return workOptions ? boss.work(queue, workOptions, handler) : boss.work(queue, handler);
|
|
113
71
|
}
|
|
72
|
+
__name(createCNSWorker, "createCNSWorker");
|
|
114
73
|
|
|
115
74
|
// src/enqueue.ts
|
|
75
|
+
var import_broker2 = require("@cnstra/broker");
|
|
116
76
|
function stimulationJob(registry, collateral, payload, extra) {
|
|
117
|
-
|
|
118
|
-
if (!collateralName) {
|
|
119
|
-
throw new Error(
|
|
120
|
-
"[@cnstra/pg-boss] Collateral is not registered in the CNSPersistOptionsRegistry; cannot enqueue it by name."
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
return { collateralName, payload, ...extra };
|
|
77
|
+
return (0, import_broker2.stimulationJob)(registry, collateral, payload, extra, "@cnstra/pg-boss");
|
|
124
78
|
}
|
|
79
|
+
__name(stimulationJob, "stimulationJob");
|
|
125
80
|
function enqueueStimulation(boss, queue, job, options) {
|
|
126
81
|
return boss.send(queue, job, options);
|
|
127
82
|
}
|
|
83
|
+
__name(enqueueStimulation, "enqueueStimulation");
|
|
128
84
|
// Annotate the CommonJS export names for ESM import in node:
|
|
129
85
|
0 && (module.exports = {
|
|
130
86
|
createCNSWorker,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/worker.ts","../src/enqueue.ts"],"sourcesContent":["export { createCNSWorker } from './worker';\nexport type { TCNSWorkerOptions } from './worker';\n\nexport { enqueueStimulation, stimulationJob } from './enqueue';\nexport type { TCNSSendOptions } from './enqueue';\n\nexport type {\n IPgBossLike,\n IPgBossJob,\n TCNSStimulationJobData,\n
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/worker.ts","../src/enqueue.ts"],"sourcesContent":["export { createCNSWorker } from './worker';\nexport type { TCNSWorkerOptions, TCNSPgBossWorkerHandle } from './worker';\n\nexport { enqueueStimulation, stimulationJob } from './enqueue';\nexport type { TCNSSendOptions } from './enqueue';\n\nexport type {\n IPgBossLike,\n IPgBossJob,\n TCNSStimulationJobData,\n ICNSResponseLike,\n} from './types';\n","import { CNSProgressSerializer } from '@cnstra/persist';\nimport type {\n CNSPersistOptionsRegistry,\n ICNSProgressRepository,\n ICNSProgressRecordingStrategy,\n ICNSStimulationRepository,\n} from '@cnstra/persist';\nimport type { ICNS } from '@cnstra/types';\nimport { runStimulationJob, createDrainState } from '@cnstra/broker';\nimport type {\n IPgBossLike,\n IPgBossJob,\n TCNSStimulationJobData,\n} from './types';\n\nexport interface TCNSWorkerOptions {\n /** Your pg-boss instance (already started). */\n boss: IPgBossLike;\n /** The CNS the jobs run against. */\n cns: ICNS<any, any>;\n /** Name ↔ object registry used to reconstruct signals from job data. */\n registry: CNSPersistOptionsRegistry;\n /** Queue name to consume. */\n queue: string;\n /**\n * Optional **observability** store: the canonical name-based\n * Stimulation → Attempt → Task history (the SAME model the devtools use),\n * written per hop via a {@link CNSStimulationPersistor}. Pass a\n * `CNSInMemoryStimulationRepository` to just *see* progress (ephemeral), or a\n * Postgres store to keep it. It records history; it does not drive resume —\n * that is `resume` below. One job = one stimulation (`stimulationId` = job id),\n * each pg-boss retry = a new attempt.\n */\n observe?: ICNSStimulationRepository;\n /**\n * Optional resumable **resume**. When set, the worker records the run's\n * outstanding frontier (by name) into `repository` as it advances, and a\n * retried job (same job id) resumes from that frontier via `cns.activate(...)`\n * instead of re-running from the entry signal. The record's lifecycle matches\n * the pg-boss job row: dropped on success, kept on failure for the retry.\n *\n * Requires every neuron/collateral that can appear in the frontier to be in\n * the `registry` (not just the entry collaterals). Neurons should still be\n * idempotent — resume narrows re-execution to the unfinished branch, it does\n * not make individual hops exactly-once.\n */\n resume?: {\n repository: ICNSProgressRepository;\n /** When to persist during the run. Defaults to debounce + max-staleness. */\n strategy?: ICNSProgressRecordingStrategy;\n };\n /** pg-boss `work` options (batchSize, pollingIntervalSeconds, …). */\n workOptions?: Record<string, unknown>;\n}\n\n/**\n * Register a pg-boss worker that turns each job into a CNStra stimulation.\n *\n * One job = one stimulation. The job carries `{ collateralName, payload }`; the\n * worker reconstructs the entry signal by name, stimulates, optionally streams\n * per-hop progress, and awaits completion. If the stimulation fails or aborts,\n * `waitUntilComplete()` rejects and the error propagates so pg-boss applies its\n * own retry/backoff policy.\n */\n/** A registered worker + a graceful `shutdown` (see {@link createCNSWorker}). */\nexport interface TCNSPgBossWorkerHandle {\n /** The pg-boss `work()` registration id. */\n workId: string;\n /**\n * Graceful shutdown of THIS worker's processing: stop taking new jobs, let\n * in-flight stimulations finish for up to `drainMs` (default 10s), then **abort**\n * whatever is still running — an aborted stimulation's remaining tasks become its\n * frontier, which the resume checkpoint records, so the job's retry resumes there.\n * It does NOT stop or close your `boss` (you own it) or your stores — call\n * `boss.stop(...)` and close your stores after this resolves.\n */\n shutdown(opts?: { drainMs?: number }): Promise<void>;\n}\n\nexport async function createCNSWorker(\n options: TCNSWorkerOptions\n): Promise<TCNSPgBossWorkerHandle> {\n const { boss, cns, registry, queue, observe, resume, workOptions } =\n options;\n\n // One serializer per worker; the per-job pipeline lives in @cnstra/broker.\n const serializer = resume ? new CNSProgressSerializer(registry) : undefined;\n const drain = createDrainState();\n\n const runJob = async (\n job: IPgBossJob<TCNSStimulationJobData>\n ): Promise<void> => {\n await runStimulationJob({\n pkgName: '@cnstra/pg-boss',\n stopping: drain.isStopping(),\n cns,\n registry,\n jobId: job.id,\n collateralName: job.data.collateralName,\n payload: job.data.payload,\n maxNeuronHops: job.data.maxNeuronHops,\n // Each pg-boss retry is a new attempt (retryCount is 0-based).\n attemptNumber: (job.retryCount ?? 0) + 1,\n observe,\n serializer,\n resumeRepo: resume?.repository,\n resumeStrategy: resume?.strategy,\n abort: drain.abort,\n active: drain.active,\n });\n };\n\n const handler = async (\n jobs: IPgBossJob<TCNSStimulationJobData>[]\n ): Promise<void> => {\n for (const job of jobs) {\n await runJob(job);\n }\n };\n\n const workId = await (workOptions\n ? boss.work<TCNSStimulationJobData>(queue, workOptions, handler)\n : boss.work<TCNSStimulationJobData>(queue, handler));\n\n const shutdown = async (opts?: { drainMs?: number }): Promise<void> => {\n await drain.drainAndAbort(opts?.drainMs ?? 10_000);\n };\n\n return { workId, shutdown };\n}\n","import type { CNSCollateral } from '@cnstra/core';\nimport type { CNSPersistOptionsRegistry } from '@cnstra/persist';\nimport { stimulationJob as brokerStimulationJob } from '@cnstra/broker';\nimport type { IPgBossLike, TCNSStimulationJobData } from './types';\n\n/** Commonly used pg-boss send options (a superset is passed through). */\nexport interface TCNSSendOptions {\n retryLimit?: number;\n retryBackoff?: boolean;\n retryDelay?: number;\n /** One in-flight job per key — e.g. one stimulation per entity id. */\n singletonKey?: string;\n startAfter?: number | string | Date;\n expireInSeconds?: number;\n priority?: number;\n [key: string]: unknown;\n}\n\n/**\n * Build a queue job from a **registered** collateral. Throws if the collateral\n * is not in the registry, so a typo can't reach the queue.\n */\nexport function stimulationJob(\n registry: CNSPersistOptionsRegistry,\n collateral: CNSCollateral<unknown>,\n payload: unknown,\n extra?: Pick<TCNSStimulationJobData, 'maxNeuronHops'>\n): TCNSStimulationJobData {\n return brokerStimulationJob(registry, collateral, payload, extra, '@cnstra/pg-boss');\n}\n\n/** Send a stimulation job to a pg-boss queue. Returns the job id. */\nexport function enqueueStimulation(\n boss: IPgBossLike,\n queue: string,\n job: TCNSStimulationJobData,\n options?: TCNSSendOptions\n): Promise<string | null> {\n return boss.send(queue, job as unknown as object, options);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;ACAA,qBAAsC;AAQtC,oBAAoD;AAuEpD,eAAsBA,gBAClBC,SAA0B;AAE1B,QAAM,EAAEC,MAAMC,KAAKC,UAAUC,OAAOC,SAASC,QAAQC,YAAW,IAC5DP;AAGJ,QAAMQ,aAAaF,SAAS,IAAIG,qCAAsBN,QAAAA,IAAYO;AAClE,QAAMC,YAAQC,gCAAAA;AAEd,QAAMC,SAAS,8BACXC,QAAAA;AA1FR;AA4FQ,cAAMC,iCAAkB;MACpBC,SAAS;MACTC,UAAUN,MAAMO,WAAU;MAC1BhB;MACAC;MACAgB,OAAOL,IAAIM;MACXC,gBAAgBP,IAAIQ,KAAKD;MACzBE,SAAST,IAAIQ,KAAKC;MAClBC,eAAeV,IAAIQ,KAAKE;;MAExBC,iBAAgBX,SAAIY,eAAJZ,YAAkB,KAAK;MACvCT;MACAG;MACAmB,YAAYrB,iCAAQsB;MACpBC,gBAAgBvB,iCAAQwB;MACxBC,OAAOpB,MAAMoB;MACbC,QAAQrB,MAAMqB;IAClB,CAAA;EACJ,GArBe;AAuBf,QAAMC,UAAU,8BACZC,SAAAA;AAEA,eAAWpB,OAAOoB,MAAM;AACpB,YAAMrB,OAAOC,GAAAA;IACjB;EACJ,GANgB;AAQhB,QAAMqB,SAAS,OAAO5B,cAChBN,KAAKmC,KAA6BhC,OAAOG,aAAa0B,OAAAA,IACtDhC,KAAKmC,KAA6BhC,OAAO6B,OAAAA;AAE/C,QAAMI,WAAW,8BAAOC,SAAAA;AA5H5B;AA6HQ,UAAM3B,MAAM4B,eAAcD,kCAAME,YAANF,YAAiB,GAAA;EAC/C,GAFiB;AAIjB,SAAO;IAAEH;IAAQE;EAAS;AAC9B;AAlDsBtC;;;AC7EtB,IAAA0C,iBAAuD;AAoBhD,SAASC,eACZC,UACAC,YACAC,SACAC,OAAqD;AAErD,aAAOC,eAAAA,gBAAqBJ,UAAUC,YAAYC,SAASC,OAAO,iBAAA;AACtE;AAPgBJ;AAUT,SAASM,mBACZC,MACAC,OACAC,KACAC,SAAyB;AAEzB,SAAOH,KAAKI,KAAKH,OAAOC,KAA0BC,OAAAA;AACtD;AAPgBJ;","names":["createCNSWorker","options","boss","cns","registry","queue","observe","resume","workOptions","serializer","CNSProgressSerializer","undefined","drain","createDrainState","runJob","job","runStimulationJob","pkgName","stopping","isStopping","jobId","id","collateralName","data","payload","maxNeuronHops","attemptNumber","retryCount","resumeRepo","repository","resumeStrategy","strategy","abort","active","handler","jobs","workId","work","shutdown","opts","drainAndAbort","drainMs","import_broker","stimulationJob","registry","collateral","payload","extra","brokerStimulationJob","enqueueStimulation","boss","queue","job","options","send"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,103 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "./chunk-2JTKI4GS.mjs";
|
|
4
|
+
|
|
1
5
|
// src/worker.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
error: resp.error != null ? String(resp.error) : null,
|
|
16
|
-
at: Date.now()
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
function createCNSWorker(options) {
|
|
20
|
-
const { boss, cns, registry, queue, progress, workOptions } = options;
|
|
21
|
-
const runJob = async (job) => {
|
|
22
|
-
var _a, _b, _c;
|
|
23
|
-
const { collateralName, payload, maxNeuronHops } = job.data;
|
|
24
|
-
const collateral = registry.getCollateral(collateralName);
|
|
25
|
-
if (!collateral) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
`[@cnstra/pg-boss] Unknown collateral "${collateralName}". Register it in the CNSPersistOptionsRegistry.`
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
const startedAt = Date.now();
|
|
31
|
-
let hopCount = 0;
|
|
32
|
-
await ((_a = progress == null ? void 0 : progress.onStarted) == null ? void 0 : _a.call(progress, {
|
|
6
|
+
import { CNSProgressSerializer } from "@cnstra/persist";
|
|
7
|
+
import { runStimulationJob, createDrainState } from "@cnstra/broker";
|
|
8
|
+
async function createCNSWorker(options) {
|
|
9
|
+
const { boss, cns, registry, queue, observe, resume, workOptions } = options;
|
|
10
|
+
const serializer = resume ? new CNSProgressSerializer(registry) : void 0;
|
|
11
|
+
const drain = createDrainState();
|
|
12
|
+
const runJob = /* @__PURE__ */ __name(async (job) => {
|
|
13
|
+
var _a;
|
|
14
|
+
await runStimulationJob({
|
|
15
|
+
pkgName: "@cnstra/pg-boss",
|
|
16
|
+
stopping: drain.isStopping(),
|
|
17
|
+
cns,
|
|
18
|
+
registry,
|
|
33
19
|
jobId: job.id,
|
|
34
|
-
collateralName,
|
|
35
|
-
payload,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
job.id,
|
|
46
|
-
hopCount,
|
|
47
|
-
resp,
|
|
48
|
-
cns,
|
|
49
|
-
registry
|
|
50
|
-
);
|
|
51
|
-
hopCount += 1;
|
|
52
|
-
await ((_a2 = progress == null ? void 0 : progress.onHop) == null ? void 0 : _a2.call(progress, rec));
|
|
53
|
-
}
|
|
20
|
+
collateralName: job.data.collateralName,
|
|
21
|
+
payload: job.data.payload,
|
|
22
|
+
maxNeuronHops: job.data.maxNeuronHops,
|
|
23
|
+
// Each pg-boss retry is a new attempt (retryCount is 0-based).
|
|
24
|
+
attemptNumber: ((_a = job.retryCount) != null ? _a : 0) + 1,
|
|
25
|
+
observe,
|
|
26
|
+
serializer,
|
|
27
|
+
resumeRepo: resume == null ? void 0 : resume.repository,
|
|
28
|
+
resumeStrategy: resume == null ? void 0 : resume.strategy,
|
|
29
|
+
abort: drain.abort,
|
|
30
|
+
active: drain.active
|
|
54
31
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
await ((_b = progress == null ? void 0 : progress.onCompleted) == null ? void 0 : _b.call(progress, {
|
|
58
|
-
jobId: job.id,
|
|
59
|
-
collateralName,
|
|
60
|
-
payload,
|
|
61
|
-
status: "completed",
|
|
62
|
-
startedAt,
|
|
63
|
-
completedAt: Date.now(),
|
|
64
|
-
hopCount
|
|
65
|
-
}));
|
|
66
|
-
} catch (err) {
|
|
67
|
-
await ((_c = progress == null ? void 0 : progress.onCompleted) == null ? void 0 : _c.call(progress, {
|
|
68
|
-
jobId: job.id,
|
|
69
|
-
collateralName,
|
|
70
|
-
payload,
|
|
71
|
-
status: "failed",
|
|
72
|
-
startedAt,
|
|
73
|
-
completedAt: Date.now(),
|
|
74
|
-
hopCount,
|
|
75
|
-
error: err instanceof Error ? err.message : String(err)
|
|
76
|
-
}));
|
|
77
|
-
throw err;
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
const handler = async (jobs) => {
|
|
32
|
+
}, "runJob");
|
|
33
|
+
const handler = /* @__PURE__ */ __name(async (jobs) => {
|
|
81
34
|
for (const job of jobs) {
|
|
82
35
|
await runJob(job);
|
|
83
36
|
}
|
|
37
|
+
}, "handler");
|
|
38
|
+
const workId = await (workOptions ? boss.work(queue, workOptions, handler) : boss.work(queue, handler));
|
|
39
|
+
const shutdown = /* @__PURE__ */ __name(async (opts) => {
|
|
40
|
+
var _a;
|
|
41
|
+
await drain.drainAndAbort((_a = opts == null ? void 0 : opts.drainMs) != null ? _a : 1e4);
|
|
42
|
+
}, "shutdown");
|
|
43
|
+
return {
|
|
44
|
+
workId,
|
|
45
|
+
shutdown
|
|
84
46
|
};
|
|
85
|
-
return workOptions ? boss.work(queue, workOptions, handler) : boss.work(queue, handler);
|
|
86
47
|
}
|
|
48
|
+
__name(createCNSWorker, "createCNSWorker");
|
|
87
49
|
|
|
88
50
|
// src/enqueue.ts
|
|
51
|
+
import { stimulationJob as brokerStimulationJob } from "@cnstra/broker";
|
|
89
52
|
function stimulationJob(registry, collateral, payload, extra) {
|
|
90
|
-
|
|
91
|
-
if (!collateralName) {
|
|
92
|
-
throw new Error(
|
|
93
|
-
"[@cnstra/pg-boss] Collateral is not registered in the CNSPersistOptionsRegistry; cannot enqueue it by name."
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
return { collateralName, payload, ...extra };
|
|
53
|
+
return brokerStimulationJob(registry, collateral, payload, extra, "@cnstra/pg-boss");
|
|
97
54
|
}
|
|
55
|
+
__name(stimulationJob, "stimulationJob");
|
|
98
56
|
function enqueueStimulation(boss, queue, job, options) {
|
|
99
57
|
return boss.send(queue, job, options);
|
|
100
58
|
}
|
|
59
|
+
__name(enqueueStimulation, "enqueueStimulation");
|
|
101
60
|
export {
|
|
102
61
|
createCNSWorker,
|
|
103
62
|
enqueueStimulation,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/worker.ts","../src/enqueue.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"sources":["../src/worker.ts","../src/enqueue.ts"],"sourcesContent":["import { CNSProgressSerializer } from '@cnstra/persist';\nimport type {\n CNSPersistOptionsRegistry,\n ICNSProgressRepository,\n ICNSProgressRecordingStrategy,\n ICNSStimulationRepository,\n} from '@cnstra/persist';\nimport type { ICNS } from '@cnstra/types';\nimport { runStimulationJob, createDrainState } from '@cnstra/broker';\nimport type {\n IPgBossLike,\n IPgBossJob,\n TCNSStimulationJobData,\n} from './types';\n\nexport interface TCNSWorkerOptions {\n /** Your pg-boss instance (already started). */\n boss: IPgBossLike;\n /** The CNS the jobs run against. */\n cns: ICNS<any, any>;\n /** Name ↔ object registry used to reconstruct signals from job data. */\n registry: CNSPersistOptionsRegistry;\n /** Queue name to consume. */\n queue: string;\n /**\n * Optional **observability** store: the canonical name-based\n * Stimulation → Attempt → Task history (the SAME model the devtools use),\n * written per hop via a {@link CNSStimulationPersistor}. Pass a\n * `CNSInMemoryStimulationRepository` to just *see* progress (ephemeral), or a\n * Postgres store to keep it. It records history; it does not drive resume —\n * that is `resume` below. One job = one stimulation (`stimulationId` = job id),\n * each pg-boss retry = a new attempt.\n */\n observe?: ICNSStimulationRepository;\n /**\n * Optional resumable **resume**. When set, the worker records the run's\n * outstanding frontier (by name) into `repository` as it advances, and a\n * retried job (same job id) resumes from that frontier via `cns.activate(...)`\n * instead of re-running from the entry signal. The record's lifecycle matches\n * the pg-boss job row: dropped on success, kept on failure for the retry.\n *\n * Requires every neuron/collateral that can appear in the frontier to be in\n * the `registry` (not just the entry collaterals). Neurons should still be\n * idempotent — resume narrows re-execution to the unfinished branch, it does\n * not make individual hops exactly-once.\n */\n resume?: {\n repository: ICNSProgressRepository;\n /** When to persist during the run. Defaults to debounce + max-staleness. */\n strategy?: ICNSProgressRecordingStrategy;\n };\n /** pg-boss `work` options (batchSize, pollingIntervalSeconds, …). */\n workOptions?: Record<string, unknown>;\n}\n\n/**\n * Register a pg-boss worker that turns each job into a CNStra stimulation.\n *\n * One job = one stimulation. The job carries `{ collateralName, payload }`; the\n * worker reconstructs the entry signal by name, stimulates, optionally streams\n * per-hop progress, and awaits completion. If the stimulation fails or aborts,\n * `waitUntilComplete()` rejects and the error propagates so pg-boss applies its\n * own retry/backoff policy.\n */\n/** A registered worker + a graceful `shutdown` (see {@link createCNSWorker}). */\nexport interface TCNSPgBossWorkerHandle {\n /** The pg-boss `work()` registration id. */\n workId: string;\n /**\n * Graceful shutdown of THIS worker's processing: stop taking new jobs, let\n * in-flight stimulations finish for up to `drainMs` (default 10s), then **abort**\n * whatever is still running — an aborted stimulation's remaining tasks become its\n * frontier, which the resume checkpoint records, so the job's retry resumes there.\n * It does NOT stop or close your `boss` (you own it) or your stores — call\n * `boss.stop(...)` and close your stores after this resolves.\n */\n shutdown(opts?: { drainMs?: number }): Promise<void>;\n}\n\nexport async function createCNSWorker(\n options: TCNSWorkerOptions\n): Promise<TCNSPgBossWorkerHandle> {\n const { boss, cns, registry, queue, observe, resume, workOptions } =\n options;\n\n // One serializer per worker; the per-job pipeline lives in @cnstra/broker.\n const serializer = resume ? new CNSProgressSerializer(registry) : undefined;\n const drain = createDrainState();\n\n const runJob = async (\n job: IPgBossJob<TCNSStimulationJobData>\n ): Promise<void> => {\n await runStimulationJob({\n pkgName: '@cnstra/pg-boss',\n stopping: drain.isStopping(),\n cns,\n registry,\n jobId: job.id,\n collateralName: job.data.collateralName,\n payload: job.data.payload,\n maxNeuronHops: job.data.maxNeuronHops,\n // Each pg-boss retry is a new attempt (retryCount is 0-based).\n attemptNumber: (job.retryCount ?? 0) + 1,\n observe,\n serializer,\n resumeRepo: resume?.repository,\n resumeStrategy: resume?.strategy,\n abort: drain.abort,\n active: drain.active,\n });\n };\n\n const handler = async (\n jobs: IPgBossJob<TCNSStimulationJobData>[]\n ): Promise<void> => {\n for (const job of jobs) {\n await runJob(job);\n }\n };\n\n const workId = await (workOptions\n ? boss.work<TCNSStimulationJobData>(queue, workOptions, handler)\n : boss.work<TCNSStimulationJobData>(queue, handler));\n\n const shutdown = async (opts?: { drainMs?: number }): Promise<void> => {\n await drain.drainAndAbort(opts?.drainMs ?? 10_000);\n };\n\n return { workId, shutdown };\n}\n","import type { CNSCollateral } from '@cnstra/core';\nimport type { CNSPersistOptionsRegistry } from '@cnstra/persist';\nimport { stimulationJob as brokerStimulationJob } from '@cnstra/broker';\nimport type { IPgBossLike, TCNSStimulationJobData } from './types';\n\n/** Commonly used pg-boss send options (a superset is passed through). */\nexport interface TCNSSendOptions {\n retryLimit?: number;\n retryBackoff?: boolean;\n retryDelay?: number;\n /** One in-flight job per key — e.g. one stimulation per entity id. */\n singletonKey?: string;\n startAfter?: number | string | Date;\n expireInSeconds?: number;\n priority?: number;\n [key: string]: unknown;\n}\n\n/**\n * Build a queue job from a **registered** collateral. Throws if the collateral\n * is not in the registry, so a typo can't reach the queue.\n */\nexport function stimulationJob(\n registry: CNSPersistOptionsRegistry,\n collateral: CNSCollateral<unknown>,\n payload: unknown,\n extra?: Pick<TCNSStimulationJobData, 'maxNeuronHops'>\n): TCNSStimulationJobData {\n return brokerStimulationJob(registry, collateral, payload, extra, '@cnstra/pg-boss');\n}\n\n/** Send a stimulation job to a pg-boss queue. Returns the job id. */\nexport function enqueueStimulation(\n boss: IPgBossLike,\n queue: string,\n job: TCNSStimulationJobData,\n options?: TCNSSendOptions\n): Promise<string | null> {\n return boss.send(queue, job as unknown as object, options);\n}\n"],"mappings":";;;;;AAAA,SAASA,6BAA6B;AAQtC,SAASC,mBAAmBC,wBAAwB;AAuEpD,eAAsBC,gBAClBC,SAA0B;AAE1B,QAAM,EAAEC,MAAMC,KAAKC,UAAUC,OAAOC,SAASC,QAAQC,YAAW,IAC5DP;AAGJ,QAAMQ,aAAaF,SAAS,IAAIG,sBAAsBN,QAAAA,IAAYO;AAClE,QAAMC,QAAQC,iBAAAA;AAEd,QAAMC,SAAS,8BACXC,QAAAA;AA1FR;AA4FQ,UAAMC,kBAAkB;MACpBC,SAAS;MACTC,UAAUN,MAAMO,WAAU;MAC1BhB;MACAC;MACAgB,OAAOL,IAAIM;MACXC,gBAAgBP,IAAIQ,KAAKD;MACzBE,SAAST,IAAIQ,KAAKC;MAClBC,eAAeV,IAAIQ,KAAKE;;MAExBC,iBAAgBX,SAAIY,eAAJZ,YAAkB,KAAK;MACvCT;MACAG;MACAmB,YAAYrB,iCAAQsB;MACpBC,gBAAgBvB,iCAAQwB;MACxBC,OAAOpB,MAAMoB;MACbC,QAAQrB,MAAMqB;IAClB,CAAA;EACJ,GArBe;AAuBf,QAAMC,UAAU,8BACZC,SAAAA;AAEA,eAAWpB,OAAOoB,MAAM;AACpB,YAAMrB,OAAOC,GAAAA;IACjB;EACJ,GANgB;AAQhB,QAAMqB,SAAS,OAAO5B,cAChBN,KAAKmC,KAA6BhC,OAAOG,aAAa0B,OAAAA,IACtDhC,KAAKmC,KAA6BhC,OAAO6B,OAAAA;AAE/C,QAAMI,WAAW,8BAAOC,SAAAA;AA5H5B;AA6HQ,UAAM3B,MAAM4B,eAAcD,kCAAME,YAANF,YAAiB,GAAA;EAC/C,GAFiB;AAIjB,SAAO;IAAEH;IAAQE;EAAS;AAC9B;AAlDsBtC;;;AC7EtB,SAAS0C,kBAAkBC,4BAA4B;AAoBhD,SAASC,eACZC,UACAC,YACAC,SACAC,OAAqD;AAErD,SAAOC,qBAAqBJ,UAAUC,YAAYC,SAASC,OAAO,iBAAA;AACtE;AAPgBJ;AAUT,SAASM,mBACZC,MACAC,OACAC,KACAC,SAAyB;AAEzB,SAAOH,KAAKI,KAAKH,OAAOC,KAA0BC,OAAAA;AACtD;AAPgBJ;","names":["CNSProgressSerializer","runStimulationJob","createDrainState","createCNSWorker","options","boss","cns","registry","queue","observe","resume","workOptions","serializer","CNSProgressSerializer","undefined","drain","createDrainState","runJob","job","runStimulationJob","pkgName","stopping","isStopping","jobId","id","collateralName","data","payload","maxNeuronHops","attemptNumber","retryCount","resumeRepo","repository","resumeStrategy","strategy","abort","active","handler","jobs","workId","work","shutdown","opts","drainAndAbort","drainMs","stimulationJob","brokerStimulationJob","stimulationJob","registry","collateral","payload","extra","brokerStimulationJob","enqueueStimulation","boss","queue","job","options","send"]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
import { ICNSProgressRepository } from '@cnstra/persist';
|
|
3
|
+
import { TCNSProgress } from '@cnstra/persist-dto';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Postgres-backed resume-progress store for pg-boss, keyed by the pg-boss **job id**.
|
|
7
|
+
*
|
|
8
|
+
* In pg-boss a run = stimulation = job (one entity), so the checkpoint is keyed by
|
|
9
|
+
* `job.id`; there is no separate run store. pg-boss can't hold arbitrary per-hop
|
|
10
|
+
* progress on its job row (its `Job` has data/output/signal but no mutable progress
|
|
11
|
+
* field), so the frontier lives in its own table (`cns_pgboss_progress`, default) on
|
|
12
|
+
* the same Postgres. One upserted row per key; dropped when the job completes.
|
|
13
|
+
*
|
|
14
|
+
* OPTIONAL: this is an opt-in export (`@cnstra/pg-boss/postgres-progress`) — importing
|
|
15
|
+
* `@cnstra/pg-boss` (worker/enqueue) does NOT pull in `pg`. It's just one
|
|
16
|
+
* `ICNSProgressRepository`; the progress store is independent of the broker, so you can
|
|
17
|
+
* run pg-boss with `@cnstra/progress-redis` (or any impl) instead.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
interface TCNSPgBossProgressRepositoryOptions {
|
|
21
|
+
/** Connection string (a Pool is created and owned by the repo). */
|
|
22
|
+
connectionString?: string;
|
|
23
|
+
/** Or inject an existing pg Pool (not closed by `close()`). */
|
|
24
|
+
pool?: Pool;
|
|
25
|
+
/** Table name (default `cns_pgboss_progress`). */
|
|
26
|
+
table?: string;
|
|
27
|
+
/** Lazily `ensureSchema` on first use (default true). Set false to manage migrations yourself. */
|
|
28
|
+
autoMigrate?: boolean;
|
|
29
|
+
}
|
|
30
|
+
declare class CNSPgBossProgressRepository implements ICNSProgressRepository {
|
|
31
|
+
private readonly pool;
|
|
32
|
+
private readonly ownsPool;
|
|
33
|
+
private readonly table;
|
|
34
|
+
private readonly autoMigrate;
|
|
35
|
+
private ready;
|
|
36
|
+
constructor(opts: TCNSPgBossProgressRepositoryOptions);
|
|
37
|
+
private ensure;
|
|
38
|
+
save(key: string, progress: TCNSProgress): Promise<void>;
|
|
39
|
+
load(key: string): Promise<TCNSProgress | undefined>;
|
|
40
|
+
delete(key: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Prune rows not touched for `olderThanMs` — the safety net for jobs that failed
|
|
43
|
+
* PERMANENTLY (retries exhausted): their frontier row is never `delete`d (pg-boss
|
|
44
|
+
* has no terminal-failure hook), so it would leak. A still-retrying job keeps its
|
|
45
|
+
* `updated_at` fresh, so choose a threshold comfortably larger than your longest
|
|
46
|
+
* retry backoff window and run this on a schedule. Returns the number of rows removed.
|
|
47
|
+
*/
|
|
48
|
+
deleteStale(olderThanMs: number): Promise<number>;
|
|
49
|
+
/** Close the owned pool (no-op when a pool was injected). */
|
|
50
|
+
close(): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Schema / migrations for the pg-boss progress table (`cns_pgboss_progress`).
|
|
55
|
+
*
|
|
56
|
+
* The package OWNS this schema so you can manage it explicitly (a migration step in
|
|
57
|
+
* your own tooling) instead of relying on the repository's lazy auto-create. Run it
|
|
58
|
+
* via the `cnstra-pgboss-progress migrate` CLI, or call `ensureSchema(db)` yourself
|
|
59
|
+
* with any pg client/pool.
|
|
60
|
+
*/
|
|
61
|
+
/** Minimal pg client surface (a `pg` Pool or Client satisfies it). */
|
|
62
|
+
interface IPgQueryable {
|
|
63
|
+
query(text: string, params?: unknown[]): Promise<{
|
|
64
|
+
rows: unknown[];
|
|
65
|
+
}>;
|
|
66
|
+
}
|
|
67
|
+
declare const DEFAULT_TABLE = "cns_pgboss_progress";
|
|
68
|
+
/** Guard the table identifier (it is interpolated into DDL/DML, not parameterised). */
|
|
69
|
+
declare function assertSafeTable(table: string): string;
|
|
70
|
+
/** The CREATE TABLE for the progress store. One upserted row per key (job id). */
|
|
71
|
+
declare function pgBossProgressSchemaSql(table?: string): string;
|
|
72
|
+
/** Idempotently create the progress table. Safe to run repeatedly. */
|
|
73
|
+
declare function ensureSchema(db: IPgQueryable, table?: string): Promise<void>;
|
|
74
|
+
|
|
75
|
+
export { CNSPgBossProgressRepository, DEFAULT_TABLE, type IPgQueryable, type TCNSPgBossProgressRepositoryOptions, assertSafeTable, ensureSchema, pgBossProgressSchemaSql };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
import { ICNSProgressRepository } from '@cnstra/persist';
|
|
3
|
+
import { TCNSProgress } from '@cnstra/persist-dto';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Postgres-backed resume-progress store for pg-boss, keyed by the pg-boss **job id**.
|
|
7
|
+
*
|
|
8
|
+
* In pg-boss a run = stimulation = job (one entity), so the checkpoint is keyed by
|
|
9
|
+
* `job.id`; there is no separate run store. pg-boss can't hold arbitrary per-hop
|
|
10
|
+
* progress on its job row (its `Job` has data/output/signal but no mutable progress
|
|
11
|
+
* field), so the frontier lives in its own table (`cns_pgboss_progress`, default) on
|
|
12
|
+
* the same Postgres. One upserted row per key; dropped when the job completes.
|
|
13
|
+
*
|
|
14
|
+
* OPTIONAL: this is an opt-in export (`@cnstra/pg-boss/postgres-progress`) — importing
|
|
15
|
+
* `@cnstra/pg-boss` (worker/enqueue) does NOT pull in `pg`. It's just one
|
|
16
|
+
* `ICNSProgressRepository`; the progress store is independent of the broker, so you can
|
|
17
|
+
* run pg-boss with `@cnstra/progress-redis` (or any impl) instead.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
interface TCNSPgBossProgressRepositoryOptions {
|
|
21
|
+
/** Connection string (a Pool is created and owned by the repo). */
|
|
22
|
+
connectionString?: string;
|
|
23
|
+
/** Or inject an existing pg Pool (not closed by `close()`). */
|
|
24
|
+
pool?: Pool;
|
|
25
|
+
/** Table name (default `cns_pgboss_progress`). */
|
|
26
|
+
table?: string;
|
|
27
|
+
/** Lazily `ensureSchema` on first use (default true). Set false to manage migrations yourself. */
|
|
28
|
+
autoMigrate?: boolean;
|
|
29
|
+
}
|
|
30
|
+
declare class CNSPgBossProgressRepository implements ICNSProgressRepository {
|
|
31
|
+
private readonly pool;
|
|
32
|
+
private readonly ownsPool;
|
|
33
|
+
private readonly table;
|
|
34
|
+
private readonly autoMigrate;
|
|
35
|
+
private ready;
|
|
36
|
+
constructor(opts: TCNSPgBossProgressRepositoryOptions);
|
|
37
|
+
private ensure;
|
|
38
|
+
save(key: string, progress: TCNSProgress): Promise<void>;
|
|
39
|
+
load(key: string): Promise<TCNSProgress | undefined>;
|
|
40
|
+
delete(key: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Prune rows not touched for `olderThanMs` — the safety net for jobs that failed
|
|
43
|
+
* PERMANENTLY (retries exhausted): their frontier row is never `delete`d (pg-boss
|
|
44
|
+
* has no terminal-failure hook), so it would leak. A still-retrying job keeps its
|
|
45
|
+
* `updated_at` fresh, so choose a threshold comfortably larger than your longest
|
|
46
|
+
* retry backoff window and run this on a schedule. Returns the number of rows removed.
|
|
47
|
+
*/
|
|
48
|
+
deleteStale(olderThanMs: number): Promise<number>;
|
|
49
|
+
/** Close the owned pool (no-op when a pool was injected). */
|
|
50
|
+
close(): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Schema / migrations for the pg-boss progress table (`cns_pgboss_progress`).
|
|
55
|
+
*
|
|
56
|
+
* The package OWNS this schema so you can manage it explicitly (a migration step in
|
|
57
|
+
* your own tooling) instead of relying on the repository's lazy auto-create. Run it
|
|
58
|
+
* via the `cnstra-pgboss-progress migrate` CLI, or call `ensureSchema(db)` yourself
|
|
59
|
+
* with any pg client/pool.
|
|
60
|
+
*/
|
|
61
|
+
/** Minimal pg client surface (a `pg` Pool or Client satisfies it). */
|
|
62
|
+
interface IPgQueryable {
|
|
63
|
+
query(text: string, params?: unknown[]): Promise<{
|
|
64
|
+
rows: unknown[];
|
|
65
|
+
}>;
|
|
66
|
+
}
|
|
67
|
+
declare const DEFAULT_TABLE = "cns_pgboss_progress";
|
|
68
|
+
/** Guard the table identifier (it is interpolated into DDL/DML, not parameterised). */
|
|
69
|
+
declare function assertSafeTable(table: string): string;
|
|
70
|
+
/** The CREATE TABLE for the progress store. One upserted row per key (job id). */
|
|
71
|
+
declare function pgBossProgressSchemaSql(table?: string): string;
|
|
72
|
+
/** Idempotently create the progress table. Safe to run repeatedly. */
|
|
73
|
+
declare function ensureSchema(db: IPgQueryable, table?: string): Promise<void>;
|
|
74
|
+
|
|
75
|
+
export { CNSPgBossProgressRepository, DEFAULT_TABLE, type IPgQueryable, type TCNSPgBossProgressRepositoryOptions, assertSafeTable, ensureSchema, pgBossProgressSchemaSql };
|