@cnstra/pg-boss 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- import { CNSPersistOptionsRegistry, ICNSStimulationRepository, ICNSProgressRepository, ICNSProgressRecordingStrategy } from '@cnstra/persist';
1
+ import { CNSPersistOptionsRegistry, ICNSStimulationWriter, ICNSProgressRepository, ICNSProgressRecordingStrategy } from '@cnstra/persist';
2
2
  import { ICNS } from '@cnstra/types';
3
3
  import { CNSCollateral } from '@cnstra/core';
4
- import { TCNSStimulationJobData } from '@cnstra/broker';
4
+ import { TCNSStimulationJobOptions, TCNSStimulationJobData } from '@cnstra/broker';
5
5
  export { ICNSResponseLike, TCNSStimulationJobData } from '@cnstra/broker';
6
+ import { TCNSDevToolsActionMiddleware } from '@cnstra/persist-dto';
6
7
 
7
8
  /**
8
9
  * Minimal, structural view of the pg-boss surface this package touches.
@@ -12,22 +13,29 @@ export { ICNSResponseLike, TCNSStimulationJobData } from '@cnstra/broker';
12
13
  * `work` handlers receive an array of jobs (batch). Pass your real `PgBoss`
13
14
  * instance — it satisfies this shape structurally.
14
15
  */
15
- interface IPgBossJob<TData = unknown> {
16
+ interface ICNSPgBossJob<TData = unknown> {
16
17
  id: string;
17
18
  name: string;
18
19
  data: TData;
19
20
  /** 0-based retry count (pg-boss `Job.retryCount`); drives the attempt number. */
20
21
  retryCount?: number;
21
22
  }
22
- interface IPgBossLike {
23
+ interface ICNSPgBossLike {
23
24
  send(name: string, data: object, options?: Record<string, unknown>): Promise<string | null>;
24
- work<TData extends object>(name: string, handler: (jobs: IPgBossJob<TData>[]) => Promise<unknown>): Promise<string>;
25
- work<TData extends object>(name: string, options: Record<string, unknown>, handler: (jobs: IPgBossJob<TData>[]) => Promise<unknown>): Promise<string>;
25
+ work<TData extends object>(name: string, handler: (jobs: ICNSPgBossJob<TData>[]) => Promise<unknown>): Promise<string>;
26
+ work<TData extends object>(name: string, options: Record<string, unknown>, handler: (jobs: ICNSPgBossJob<TData>[]) => Promise<unknown>): Promise<string>;
27
+ /**
28
+ * Mark jobs finished by id (pg-boss `complete(name, ids)`). The batch handler
29
+ * calls it for the jobs that succeeded alongside one that did not, so a single
30
+ * failure does not re-run its neighbours. Optional: a stand-in that never
31
+ * batches has nothing to settle.
32
+ */
33
+ complete?(name: string, ids: string[]): Promise<unknown>;
26
34
  }
27
35
 
28
- interface TCNSWorkerOptions {
36
+ type TCNSWorkerOptions = {
29
37
  /** Your pg-boss instance (already started). */
30
- boss: IPgBossLike;
38
+ boss: ICNSPgBossLike;
31
39
  /** The CNS the jobs run against. */
32
40
  cns: ICNS<any, any>;
33
41
  /** Name ↔ object registry used to reconstruct signals from job data. */
@@ -43,7 +51,7 @@ interface TCNSWorkerOptions {
43
51
  * that is `resume` below. One job = one stimulation (`stimulationId` = job id),
44
52
  * each pg-boss retry = a new attempt.
45
53
  */
46
- observe?: ICNSStimulationRepository;
54
+ observe?: ICNSStimulationWriter;
47
55
  /**
48
56
  * Optional resumable **resume**. When set, the worker records the run's
49
57
  * outstanding frontier (by name) into `repository` as it advances, and a
@@ -63,7 +71,7 @@ interface TCNSWorkerOptions {
63
71
  };
64
72
  /** pg-boss `work` options (batchSize, pollingIntervalSeconds, …). */
65
73
  workOptions?: Record<string, unknown>;
66
- }
74
+ };
67
75
  /**
68
76
  * Register a pg-boss worker that turns each job into a CNStra stimulation.
69
77
  *
@@ -74,7 +82,7 @@ interface TCNSWorkerOptions {
74
82
  * own retry/backoff policy.
75
83
  */
76
84
  /** A registered worker + a graceful `shutdown` (see {@link createCNSWorker}). */
77
- interface TCNSPgBossWorkerHandle {
85
+ type TCNSPgBossWorkerHandle = {
78
86
  /** The pg-boss `work()` registration id. */
79
87
  workId: string;
80
88
  /**
@@ -88,11 +96,11 @@ interface TCNSPgBossWorkerHandle {
88
96
  shutdown(opts?: {
89
97
  drainMs?: number;
90
98
  }): Promise<void>;
91
- }
99
+ };
92
100
  declare function createCNSWorker(options: TCNSWorkerOptions): Promise<TCNSPgBossWorkerHandle>;
93
101
 
94
102
  /** Commonly used pg-boss send options (a superset is passed through). */
95
- interface TCNSSendOptions {
103
+ type TCNSSendOptions = {
96
104
  retryLimit?: number;
97
105
  retryBackoff?: boolean;
98
106
  retryDelay?: number;
@@ -102,13 +110,34 @@ interface TCNSSendOptions {
102
110
  expireInSeconds?: number;
103
111
  priority?: number;
104
112
  [key: string]: unknown;
105
- }
113
+ };
106
114
  /**
107
115
  * Build a queue job from a **registered** collateral. Throws if the collateral
108
116
  * is not in the registry, so a typo can't reach the queue.
109
117
  */
110
- declare function stimulationJob(registry: CNSPersistOptionsRegistry, collateral: CNSCollateral<unknown>, payload: unknown, extra?: Pick<TCNSStimulationJobData, 'maxNeuronHops'>): TCNSStimulationJobData;
118
+ declare function cnsStimulationJob(registry: CNSPersistOptionsRegistry, collateral: CNSCollateral<unknown>, payload: unknown, stimulationJobOptions?: TCNSStimulationJobOptions): TCNSStimulationJobData;
111
119
  /** Send a stimulation job to a pg-boss queue. Returns the job id. */
112
- declare function enqueueStimulation(boss: IPgBossLike, queue: string, job: TCNSStimulationJobData, options?: TCNSSendOptions): Promise<string | null>;
120
+ declare function enqueueCNSStimulation(boss: ICNSPgBossLike, queue: string, job: TCNSStimulationJobData, options?: TCNSSendOptions): Promise<string | null>;
121
+
122
+ /** The two calls this needs beyond `send` — kept structural, like the rest. */
123
+ interface ICNSPgBossActionsLike extends ICNSPgBossLike {
124
+ getJobById(name: string, id: string): Promise<{
125
+ id: string;
126
+ state?: string;
127
+ } | null | undefined>;
128
+ retry(name: string, id: string): Promise<unknown>;
129
+ }
130
+ /**
131
+ * Carries out DevTools actions against a pg-boss queue.
132
+ *
133
+ * Same shape as the BullMQ adapter and for the same reason: **retry is the
134
+ * broker's own retry**, keyed by the job id — which is our `stimulationId` — so
135
+ * the run keeps its identity, its history and its resume checkpoint. A clone or
136
+ * a replay is a new job under a new id.
137
+ *
138
+ * Ownership is decided by asking pg-boss whether it holds the job; anything else
139
+ * falls through to the next link in the chain.
140
+ */
141
+ declare const createCNSPgBossActions: (boss: ICNSPgBossActionsLike, queue: string) => TCNSDevToolsActionMiddleware;
113
142
 
114
- export { type IPgBossJob, type IPgBossLike, type TCNSPgBossWorkerHandle, type TCNSSendOptions, type TCNSWorkerOptions, createCNSWorker, enqueueStimulation, stimulationJob };
143
+ export { type ICNSPgBossActionsLike, type ICNSPgBossJob, type ICNSPgBossLike, type TCNSPgBossWorkerHandle, type TCNSSendOptions, type TCNSWorkerOptions, cnsStimulationJob, createCNSPgBossActions, createCNSWorker, enqueueCNSStimulation };
package/dist/index.js CHANGED
@@ -20,9 +20,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ cnsStimulationJob: () => cnsStimulationJob,
24
+ createCNSPgBossActions: () => createCNSPgBossActions,
23
25
  createCNSWorker: () => createCNSWorker,
24
- enqueueStimulation: () => enqueueStimulation,
25
- stimulationJob: () => stimulationJob
26
+ enqueueCNSStimulation: () => enqueueCNSStimulation
26
27
  });
27
28
  module.exports = __toCommonJS(index_exports);
28
29
 
@@ -32,10 +33,10 @@ var import_broker = require("@cnstra/broker");
32
33
  async function createCNSWorker(options) {
33
34
  const { boss, cns, registry, queue, observe, resume, workOptions } = options;
34
35
  const serializer = resume ? new import_persist.CNSProgressSerializer(registry) : void 0;
35
- const drain = (0, import_broker.createDrainState)();
36
+ const drain = (0, import_broker.createCNSDrainState)();
36
37
  const runJob = /* @__PURE__ */ __name(async (job) => {
37
38
  var _a;
38
- await (0, import_broker.runStimulationJob)({
39
+ await (0, import_broker.runCNSStimulationJob)({
39
40
  pkgName: "@cnstra/pg-boss",
40
41
  stopping: drain.isStopping(),
41
42
  cns,
@@ -44,6 +45,8 @@ async function createCNSWorker(options) {
44
45
  collateralName: job.data.collateralName,
45
46
  payload: job.data.payload,
46
47
  maxNeuronHops: job.data.maxNeuronHops,
48
+ modalityName: job.data.modalityName,
49
+ afferentPathName: job.data.afferentPathName,
47
50
  // Each pg-boss retry is a new attempt (retryCount is 0-based).
48
51
  attemptNumber: ((_a = job.retryCount) != null ? _a : 0) + 1,
49
52
  observe,
@@ -55,9 +58,22 @@ async function createCNSWorker(options) {
55
58
  });
56
59
  }, "runJob");
57
60
  const handler = /* @__PURE__ */ __name(async (jobs) => {
61
+ const completed = [];
62
+ const failures = [];
58
63
  for (const job of jobs) {
59
- await runJob(job);
64
+ try {
65
+ await runJob(job);
66
+ completed.push(job.id);
67
+ } catch (error2) {
68
+ failures.push(error2);
69
+ }
60
70
  }
71
+ if (failures.length === 0) return;
72
+ if (completed.length && boss.complete) await boss.complete(queue, completed);
73
+ if (failures.length === 1) throw failures[0];
74
+ const error = new Error(`${failures.length} of ${jobs.length} jobs failed`);
75
+ error.errors = failures;
76
+ throw error;
61
77
  }, "handler");
62
78
  const workId = await (workOptions ? boss.work(queue, workOptions, handler) : boss.work(queue, handler));
63
79
  const shutdown = /* @__PURE__ */ __name(async (opts) => {
@@ -73,18 +89,43 @@ __name(createCNSWorker, "createCNSWorker");
73
89
 
74
90
  // src/enqueue.ts
75
91
  var import_broker2 = require("@cnstra/broker");
76
- function stimulationJob(registry, collateral, payload, extra) {
77
- return (0, import_broker2.stimulationJob)(registry, collateral, payload, extra, "@cnstra/pg-boss");
92
+ function cnsStimulationJob(registry, collateral, payload, stimulationJobOptions) {
93
+ return (0, import_broker2.cnsStimulationJob)(registry, collateral, payload, stimulationJobOptions, "@cnstra/pg-boss");
78
94
  }
79
- __name(stimulationJob, "stimulationJob");
80
- function enqueueStimulation(boss, queue, job, options) {
95
+ __name(cnsStimulationJob, "cnsStimulationJob");
96
+ function enqueueCNSStimulation(boss, queue, job, options) {
81
97
  return boss.send(queue, job, options);
82
98
  }
83
- __name(enqueueStimulation, "enqueueStimulation");
99
+ __name(enqueueCNSStimulation, "enqueueCNSStimulation");
100
+
101
+ // src/actions.ts
102
+ var createCNSPgBossActions = /* @__PURE__ */ __name((boss, queue) => {
103
+ return async (action, next) => {
104
+ var _a;
105
+ if (action.type === "command") return next();
106
+ if (action.type === "launch") {
107
+ const data = {
108
+ collateralName: action.entry.collateralName,
109
+ payload: action.entry.payload,
110
+ ...((_a = action.options) == null ? void 0 : _a.maxHops) ? {
111
+ maxNeuronHops: action.options.maxHops
112
+ } : {}
113
+ };
114
+ await boss.send(queue, data, {
115
+ id: action.stimulationId
116
+ });
117
+ return;
118
+ }
119
+ const job = await boss.getJobById(queue, action.stimulationId);
120
+ if (!job) return next();
121
+ await boss.retry(queue, action.stimulationId);
122
+ };
123
+ }, "createCNSPgBossActions");
84
124
  // Annotate the CommonJS export names for ESM import in node:
85
125
  0 && (module.exports = {
126
+ cnsStimulationJob,
127
+ createCNSPgBossActions,
86
128
  createCNSWorker,
87
- enqueueStimulation,
88
- stimulationJob
129
+ enqueueCNSStimulation
89
130
  });
90
131
  //# sourceMappingURL=index.js.map
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, 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"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/worker.ts","../src/enqueue.ts","../src/actions.ts"],"sourcesContent":["export { createCNSWorker } from './worker';\nexport type { TCNSWorkerOptions, TCNSPgBossWorkerHandle } from './worker';\n\nexport { enqueueCNSStimulation, cnsStimulationJob } from './enqueue';\n\nexport { createCNSPgBossActions } from './actions';\nexport type { ICNSPgBossActionsLike } from './actions';\nexport type { TCNSSendOptions } from './enqueue';\n\nexport type {\n ICNSPgBossLike,\n ICNSPgBossJob,\n TCNSStimulationJobData,\n ICNSResponseLike,\n} from './types';\n","import { CNSProgressSerializer } from '@cnstra/persist';\nimport type {\n CNSPersistOptionsRegistry,\n ICNSProgressRepository,\n ICNSProgressRecordingStrategy,\n ICNSStimulationWriter,\n} from '@cnstra/persist';\nimport type { ICNS } from '@cnstra/types';\nimport { runCNSStimulationJob, createCNSDrainState } from '@cnstra/broker';\nimport type {\n ICNSPgBossLike,\n ICNSPgBossJob,\n TCNSStimulationJobData,\n} from './types';\n\nexport type TCNSWorkerOptions = {\n /** Your pg-boss instance (already started). */\n boss: ICNSPgBossLike;\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?: ICNSStimulationWriter;\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 type 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 = createCNSDrainState();\n\n const runJob = async (\n job: ICNSPgBossJob<TCNSStimulationJobData>\n ): Promise<void> => {\n await runCNSStimulationJob({\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 modalityName: job.data.modalityName,\n afferentPathName: job.data.afferentPathName,\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 /**\n * pg-boss can hand a worker a whole batch (`workOptions.batchSize`), and it\n * settles that batch as ONE unit: a handler that throws fails every job id in\n * it. A plain `for … await` over the batch therefore turned one bad job into\n * two separate faults — the jobs after it never ran at all, and the jobs\n * before it, already finished, were re-queued and ran a second time.\n *\n * So: every job in the batch runs, whatever its neighbours did, and the ones\n * that finished are completed HERE, before the failure is reported. pg-boss's\n * own fail-by-id only touches rows with `state < 'completed'`, so those keep\n * their result while the failures go to retry on their own.\n */\n const handler = async (\n jobs: ICNSPgBossJob<TCNSStimulationJobData>[]\n ): Promise<void> => {\n const completed: string[] = [];\n const failures: unknown[] = [];\n\n for (const job of jobs) {\n try {\n await runJob(job);\n completed.push(job.id);\n } catch (error) {\n failures.push(error);\n }\n }\n\n if (failures.length === 0) return;\n // Only when something failed: with no failure pg-boss completes the batch\n // itself, and completing it twice is a wasted round trip.\n if (completed.length && boss.complete)\n await boss.complete(queue, completed);\n\n if (failures.length === 1) throw failures[0];\n // Every failure stays reachable on `.errors` — pg-boss stores what is\n // thrown as the job's output, and \"3 of 20 failed\" with no detail is not\n // something anyone can act on.\n const error = new Error(\n `${failures.length} of ${jobs.length} jobs failed`\n ) as Error & { errors: unknown[] };\n error.errors = failures;\n throw error;\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 { cnsStimulationJob as brokerStimulationJob } from '@cnstra/broker';\nimport type { TCNSStimulationJobOptions } from '@cnstra/broker';\nimport type { ICNSPgBossLike, TCNSStimulationJobData } from './types';\n\n/** Commonly used pg-boss send options (a superset is passed through). */\nexport type 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 cnsStimulationJob(\n registry: CNSPersistOptionsRegistry,\n collateral: CNSCollateral<unknown>,\n payload: unknown,\n stimulationJobOptions?: TCNSStimulationJobOptions\n): TCNSStimulationJobData {\n return brokerStimulationJob(registry, collateral, payload, stimulationJobOptions, '@cnstra/pg-boss');\n}\n\n/** Send a stimulation job to a pg-boss queue. Returns the job id. */\nexport function enqueueCNSStimulation(\n boss: ICNSPgBossLike,\n queue: string,\n job: TCNSStimulationJobData,\n options?: TCNSSendOptions\n): Promise<string | null> {\n return boss.send(queue, job, options);\n}\n","import type { TCNSDevToolsActionMiddleware } from '@cnstra/persist-dto';\nimport type { ICNSPgBossLike, TCNSStimulationJobData } from './types';\n\n/** The two calls this needs beyond `send` — kept structural, like the rest. */\nexport interface ICNSPgBossActionsLike extends ICNSPgBossLike {\n getJobById(\n name: string,\n id: string\n ): Promise<{ id: string; state?: string } | null | undefined>;\n retry(name: string, id: string): Promise<unknown>;\n}\n\n/**\n * Carries out DevTools actions against a pg-boss queue.\n *\n * Same shape as the BullMQ adapter and for the same reason: **retry is the\n * broker's own retry**, keyed by the job id — which is our `stimulationId` — so\n * the run keeps its identity, its history and its resume checkpoint. A clone or\n * a replay is a new job under a new id.\n *\n * Ownership is decided by asking pg-boss whether it holds the job; anything else\n * falls through to the next link in the chain.\n */\nexport const createCNSPgBossActions = (\n boss: ICNSPgBossActionsLike,\n queue: string\n): TCNSDevToolsActionMiddleware => {\n return async (action, next) => {\n if (action.type === 'command') return next();\n if (action.type === 'launch') {\n const data: TCNSStimulationJobData = {\n collateralName: action.entry.collateralName,\n payload: action.entry.payload,\n ...(action.options?.maxHops\n ? { maxNeuronHops: action.options.maxHops }\n : {}),\n };\n await boss.send(queue, data, {\n id: action.stimulationId,\n });\n return;\n }\n\n const job = await boss.getJobById(queue, action.stimulationId);\n // Not ours: another queue, or a run that never came from a broker.\n if (!job) return next();\n\n await boss.retry(queue, action.stimulationId);\n };\n};\n"],"mappings":"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","names":["createCNSWorker","options","boss","cns","registry","queue","observe","resume","workOptions","serializer","CNSProgressSerializer","undefined","drain","createCNSDrainState","runJob","job","runCNSStimulationJob","pkgName","stopping","isStopping","jobId","id","collateralName","data","payload","maxNeuronHops","modalityName","afferentPathName","attemptNumber","retryCount","resumeRepo","repository","resumeStrategy","strategy","abort","active","handler","jobs","completed","failures","push","error","length","complete","Error","errors","workId","work","shutdown","opts","drainAndAbort","drainMs","import_broker","cnsStimulationJob","registry","collateral","payload","stimulationJobOptions","brokerStimulationJob","enqueueCNSStimulation","boss","queue","job","options","send","createCNSPgBossActions","boss","queue","action","next","type","data","collateralName","entry","payload","options","maxHops","maxNeuronHops","send","id","stimulationId","job","getJobById","retry"]}
package/dist/index.mjs CHANGED
@@ -4,14 +4,14 @@ import {
4
4
 
5
5
  // src/worker.ts
6
6
  import { CNSProgressSerializer } from "@cnstra/persist";
7
- import { runStimulationJob, createDrainState } from "@cnstra/broker";
7
+ import { runCNSStimulationJob, createCNSDrainState } from "@cnstra/broker";
8
8
  async function createCNSWorker(options) {
9
9
  const { boss, cns, registry, queue, observe, resume, workOptions } = options;
10
10
  const serializer = resume ? new CNSProgressSerializer(registry) : void 0;
11
- const drain = createDrainState();
11
+ const drain = createCNSDrainState();
12
12
  const runJob = /* @__PURE__ */ __name(async (job) => {
13
13
  var _a;
14
- await runStimulationJob({
14
+ await runCNSStimulationJob({
15
15
  pkgName: "@cnstra/pg-boss",
16
16
  stopping: drain.isStopping(),
17
17
  cns,
@@ -20,6 +20,8 @@ async function createCNSWorker(options) {
20
20
  collateralName: job.data.collateralName,
21
21
  payload: job.data.payload,
22
22
  maxNeuronHops: job.data.maxNeuronHops,
23
+ modalityName: job.data.modalityName,
24
+ afferentPathName: job.data.afferentPathName,
23
25
  // Each pg-boss retry is a new attempt (retryCount is 0-based).
24
26
  attemptNumber: ((_a = job.retryCount) != null ? _a : 0) + 1,
25
27
  observe,
@@ -31,9 +33,22 @@ async function createCNSWorker(options) {
31
33
  });
32
34
  }, "runJob");
33
35
  const handler = /* @__PURE__ */ __name(async (jobs) => {
36
+ const completed = [];
37
+ const failures = [];
34
38
  for (const job of jobs) {
35
- await runJob(job);
39
+ try {
40
+ await runJob(job);
41
+ completed.push(job.id);
42
+ } catch (error2) {
43
+ failures.push(error2);
44
+ }
36
45
  }
46
+ if (failures.length === 0) return;
47
+ if (completed.length && boss.complete) await boss.complete(queue, completed);
48
+ if (failures.length === 1) throw failures[0];
49
+ const error = new Error(`${failures.length} of ${jobs.length} jobs failed`);
50
+ error.errors = failures;
51
+ throw error;
37
52
  }, "handler");
38
53
  const workId = await (workOptions ? boss.work(queue, workOptions, handler) : boss.work(queue, handler));
39
54
  const shutdown = /* @__PURE__ */ __name(async (opts) => {
@@ -48,18 +63,43 @@ async function createCNSWorker(options) {
48
63
  __name(createCNSWorker, "createCNSWorker");
49
64
 
50
65
  // src/enqueue.ts
51
- import { stimulationJob as brokerStimulationJob } from "@cnstra/broker";
52
- function stimulationJob(registry, collateral, payload, extra) {
53
- return brokerStimulationJob(registry, collateral, payload, extra, "@cnstra/pg-boss");
66
+ import { cnsStimulationJob as brokerStimulationJob } from "@cnstra/broker";
67
+ function cnsStimulationJob(registry, collateral, payload, stimulationJobOptions) {
68
+ return brokerStimulationJob(registry, collateral, payload, stimulationJobOptions, "@cnstra/pg-boss");
54
69
  }
55
- __name(stimulationJob, "stimulationJob");
56
- function enqueueStimulation(boss, queue, job, options) {
70
+ __name(cnsStimulationJob, "cnsStimulationJob");
71
+ function enqueueCNSStimulation(boss, queue, job, options) {
57
72
  return boss.send(queue, job, options);
58
73
  }
59
- __name(enqueueStimulation, "enqueueStimulation");
74
+ __name(enqueueCNSStimulation, "enqueueCNSStimulation");
75
+
76
+ // src/actions.ts
77
+ var createCNSPgBossActions = /* @__PURE__ */ __name((boss, queue) => {
78
+ return async (action, next) => {
79
+ var _a;
80
+ if (action.type === "command") return next();
81
+ if (action.type === "launch") {
82
+ const data = {
83
+ collateralName: action.entry.collateralName,
84
+ payload: action.entry.payload,
85
+ ...((_a = action.options) == null ? void 0 : _a.maxHops) ? {
86
+ maxNeuronHops: action.options.maxHops
87
+ } : {}
88
+ };
89
+ await boss.send(queue, data, {
90
+ id: action.stimulationId
91
+ });
92
+ return;
93
+ }
94
+ const job = await boss.getJobById(queue, action.stimulationId);
95
+ if (!job) return next();
96
+ await boss.retry(queue, action.stimulationId);
97
+ };
98
+ }, "createCNSPgBossActions");
60
99
  export {
100
+ cnsStimulationJob,
101
+ createCNSPgBossActions,
61
102
  createCNSWorker,
62
- enqueueStimulation,
63
- stimulationJob
103
+ enqueueCNSStimulation
64
104
  };
65
105
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
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"]}
1
+ {"version":3,"sources":["../src/worker.ts","../src/enqueue.ts","../src/actions.ts"],"sourcesContent":["import { CNSProgressSerializer } from '@cnstra/persist';\nimport type {\n CNSPersistOptionsRegistry,\n ICNSProgressRepository,\n ICNSProgressRecordingStrategy,\n ICNSStimulationWriter,\n} from '@cnstra/persist';\nimport type { ICNS } from '@cnstra/types';\nimport { runCNSStimulationJob, createCNSDrainState } from '@cnstra/broker';\nimport type {\n ICNSPgBossLike,\n ICNSPgBossJob,\n TCNSStimulationJobData,\n} from './types';\n\nexport type TCNSWorkerOptions = {\n /** Your pg-boss instance (already started). */\n boss: ICNSPgBossLike;\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?: ICNSStimulationWriter;\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 type 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 = createCNSDrainState();\n\n const runJob = async (\n job: ICNSPgBossJob<TCNSStimulationJobData>\n ): Promise<void> => {\n await runCNSStimulationJob({\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 modalityName: job.data.modalityName,\n afferentPathName: job.data.afferentPathName,\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 /**\n * pg-boss can hand a worker a whole batch (`workOptions.batchSize`), and it\n * settles that batch as ONE unit: a handler that throws fails every job id in\n * it. A plain `for … await` over the batch therefore turned one bad job into\n * two separate faults — the jobs after it never ran at all, and the jobs\n * before it, already finished, were re-queued and ran a second time.\n *\n * So: every job in the batch runs, whatever its neighbours did, and the ones\n * that finished are completed HERE, before the failure is reported. pg-boss's\n * own fail-by-id only touches rows with `state < 'completed'`, so those keep\n * their result while the failures go to retry on their own.\n */\n const handler = async (\n jobs: ICNSPgBossJob<TCNSStimulationJobData>[]\n ): Promise<void> => {\n const completed: string[] = [];\n const failures: unknown[] = [];\n\n for (const job of jobs) {\n try {\n await runJob(job);\n completed.push(job.id);\n } catch (error) {\n failures.push(error);\n }\n }\n\n if (failures.length === 0) return;\n // Only when something failed: with no failure pg-boss completes the batch\n // itself, and completing it twice is a wasted round trip.\n if (completed.length && boss.complete)\n await boss.complete(queue, completed);\n\n if (failures.length === 1) throw failures[0];\n // Every failure stays reachable on `.errors` — pg-boss stores what is\n // thrown as the job's output, and \"3 of 20 failed\" with no detail is not\n // something anyone can act on.\n const error = new Error(\n `${failures.length} of ${jobs.length} jobs failed`\n ) as Error & { errors: unknown[] };\n error.errors = failures;\n throw error;\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 { cnsStimulationJob as brokerStimulationJob } from '@cnstra/broker';\nimport type { TCNSStimulationJobOptions } from '@cnstra/broker';\nimport type { ICNSPgBossLike, TCNSStimulationJobData } from './types';\n\n/** Commonly used pg-boss send options (a superset is passed through). */\nexport type 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 cnsStimulationJob(\n registry: CNSPersistOptionsRegistry,\n collateral: CNSCollateral<unknown>,\n payload: unknown,\n stimulationJobOptions?: TCNSStimulationJobOptions\n): TCNSStimulationJobData {\n return brokerStimulationJob(registry, collateral, payload, stimulationJobOptions, '@cnstra/pg-boss');\n}\n\n/** Send a stimulation job to a pg-boss queue. Returns the job id. */\nexport function enqueueCNSStimulation(\n boss: ICNSPgBossLike,\n queue: string,\n job: TCNSStimulationJobData,\n options?: TCNSSendOptions\n): Promise<string | null> {\n return boss.send(queue, job, options);\n}\n","import type { TCNSDevToolsActionMiddleware } from '@cnstra/persist-dto';\nimport type { ICNSPgBossLike, TCNSStimulationJobData } from './types';\n\n/** The two calls this needs beyond `send` — kept structural, like the rest. */\nexport interface ICNSPgBossActionsLike extends ICNSPgBossLike {\n getJobById(\n name: string,\n id: string\n ): Promise<{ id: string; state?: string } | null | undefined>;\n retry(name: string, id: string): Promise<unknown>;\n}\n\n/**\n * Carries out DevTools actions against a pg-boss queue.\n *\n * Same shape as the BullMQ adapter and for the same reason: **retry is the\n * broker's own retry**, keyed by the job id — which is our `stimulationId` — so\n * the run keeps its identity, its history and its resume checkpoint. A clone or\n * a replay is a new job under a new id.\n *\n * Ownership is decided by asking pg-boss whether it holds the job; anything else\n * falls through to the next link in the chain.\n */\nexport const createCNSPgBossActions = (\n boss: ICNSPgBossActionsLike,\n queue: string\n): TCNSDevToolsActionMiddleware => {\n return async (action, next) => {\n if (action.type === 'command') return next();\n if (action.type === 'launch') {\n const data: TCNSStimulationJobData = {\n collateralName: action.entry.collateralName,\n payload: action.entry.payload,\n ...(action.options?.maxHops\n ? { maxNeuronHops: action.options.maxHops }\n : {}),\n };\n await boss.send(queue, data, {\n id: action.stimulationId,\n });\n return;\n }\n\n const job = await boss.getJobById(queue, action.stimulationId);\n // Not ours: another queue, or a run that never came from a broker.\n if (!job) return next();\n\n await boss.retry(queue, action.stimulationId);\n };\n};\n"],"mappings":"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","names":["CNSProgressSerializer","runCNSStimulationJob","createCNSDrainState","createCNSWorker","options","boss","cns","registry","queue","observe","resume","workOptions","serializer","CNSProgressSerializer","undefined","drain","createCNSDrainState","runJob","job","runCNSStimulationJob","pkgName","stopping","isStopping","jobId","id","collateralName","data","payload","maxNeuronHops","modalityName","afferentPathName","attemptNumber","retryCount","resumeRepo","repository","resumeStrategy","strategy","abort","active","handler","jobs","completed","failures","push","error","length","complete","Error","errors","workId","work","shutdown","opts","drainAndAbort","drainMs","cnsStimulationJob","brokerStimulationJob","cnsStimulationJob","registry","collateral","payload","stimulationJobOptions","brokerStimulationJob","enqueueCNSStimulation","boss","queue","job","options","send","createCNSPgBossActions","boss","queue","action","next","type","data","collateralName","entry","payload","options","maxHops","maxNeuronHops","send","id","stimulationId","job","getJobById","retry"]}
@@ -8,7 +8,7 @@ import { TCNSProgress } from '@cnstra/persist-dto';
8
8
  * In pg-boss a run = stimulation = job (one entity), so the checkpoint is keyed by
9
9
  * `job.id`; there is no separate run store. pg-boss can't hold arbitrary per-hop
10
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
11
+ * field), so the frontier lives in its own table (`<tablePrefix>pgboss_progress`) on
12
12
  * the same Postgres. One upserted row per key; dropped when the job completes.
13
13
  *
14
14
  * OPTIONAL: this is an opt-in export (`@cnstra/pg-boss/postgres-progress`) — importing
@@ -17,24 +17,27 @@ import { TCNSProgress } from '@cnstra/persist-dto';
17
17
  * run pg-boss with `@cnstra/progress-redis` (or any impl) instead.
18
18
  */
19
19
 
20
- interface TCNSPgBossProgressRepositoryOptions {
20
+ type TCNSPgBossProgressRepositoryOptions = {
21
21
  /** Connection string (a Pool is created and owned by the repo). */
22
22
  connectionString?: string;
23
23
  /** Or inject an existing pg Pool (not closed by `close()`). */
24
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. */
25
+ /** Table name prefix (default `cns_`); the table is `<tablePrefix>pgboss_progress`. */
26
+ tablePrefix?: string;
27
+ /** Create or upgrade the table on first use (default true). Set false to manage migrations yourself. */
28
28
  autoMigrate?: boolean;
29
- }
29
+ now?: () => number;
30
+ };
30
31
  declare class CNSPgBossProgressRepository implements ICNSProgressRepository {
31
32
  private readonly pool;
32
33
  private readonly ownsPool;
34
+ private readonly tablePrefix;
33
35
  private readonly table;
34
36
  private readonly autoMigrate;
37
+ private readonly now;
35
38
  private ready;
36
39
  constructor(opts: TCNSPgBossProgressRepositoryOptions);
37
- private ensure;
40
+ private schemaReady;
38
41
  save(key: string, progress: TCNSProgress): Promise<void>;
39
42
  load(key: string): Promise<TCNSProgress | undefined>;
40
43
  delete(key: string): Promise<void>;
@@ -50,26 +53,20 @@ declare class CNSPgBossProgressRepository implements ICNSProgressRepository {
50
53
  close(): Promise<void>;
51
54
  }
52
55
 
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
- }>;
56
+ type TCNSPgBossProgressMigrateOptions = {
57
+ readonly contract?: boolean;
58
+ };
59
+
60
+ declare class CNSPgBossProgressSchema {
61
+ static readonly DEFAULT_TABLE_PREFIX = "cns_";
62
+ private static readonly LONGEST_SUFFIX;
63
+ private static readonly migrator;
64
+ static assertTablePrefix(tablePrefix: string): string;
65
+ static table(tablePrefix?: string): string;
66
+ static migrationTable(tablePrefix?: string): string;
67
+ static migrationSql(tablePrefix?: string, appliedSteps?: readonly number[], options?: TCNSPgBossProgressMigrateOptions): string;
68
+ static migrate(pool: Pool, tablePrefix?: string, options?: TCNSPgBossProgressMigrateOptions): Promise<void>;
69
+ static verify(pool: Pool, tablePrefix?: string): Promise<void>;
66
70
  }
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
71
 
75
- export { CNSPgBossProgressRepository, DEFAULT_TABLE, type IPgQueryable, type TCNSPgBossProgressRepositoryOptions, assertSafeTable, ensureSchema, pgBossProgressSchemaSql };
72
+ export { CNSPgBossProgressRepository, CNSPgBossProgressSchema, type TCNSPgBossProgressMigrateOptions, type TCNSPgBossProgressRepositoryOptions };
@@ -8,7 +8,7 @@ import { TCNSProgress } from '@cnstra/persist-dto';
8
8
  * In pg-boss a run = stimulation = job (one entity), so the checkpoint is keyed by
9
9
  * `job.id`; there is no separate run store. pg-boss can't hold arbitrary per-hop
10
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
11
+ * field), so the frontier lives in its own table (`<tablePrefix>pgboss_progress`) on
12
12
  * the same Postgres. One upserted row per key; dropped when the job completes.
13
13
  *
14
14
  * OPTIONAL: this is an opt-in export (`@cnstra/pg-boss/postgres-progress`) — importing
@@ -17,24 +17,27 @@ import { TCNSProgress } from '@cnstra/persist-dto';
17
17
  * run pg-boss with `@cnstra/progress-redis` (or any impl) instead.
18
18
  */
19
19
 
20
- interface TCNSPgBossProgressRepositoryOptions {
20
+ type TCNSPgBossProgressRepositoryOptions = {
21
21
  /** Connection string (a Pool is created and owned by the repo). */
22
22
  connectionString?: string;
23
23
  /** Or inject an existing pg Pool (not closed by `close()`). */
24
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. */
25
+ /** Table name prefix (default `cns_`); the table is `<tablePrefix>pgboss_progress`. */
26
+ tablePrefix?: string;
27
+ /** Create or upgrade the table on first use (default true). Set false to manage migrations yourself. */
28
28
  autoMigrate?: boolean;
29
- }
29
+ now?: () => number;
30
+ };
30
31
  declare class CNSPgBossProgressRepository implements ICNSProgressRepository {
31
32
  private readonly pool;
32
33
  private readonly ownsPool;
34
+ private readonly tablePrefix;
33
35
  private readonly table;
34
36
  private readonly autoMigrate;
37
+ private readonly now;
35
38
  private ready;
36
39
  constructor(opts: TCNSPgBossProgressRepositoryOptions);
37
- private ensure;
40
+ private schemaReady;
38
41
  save(key: string, progress: TCNSProgress): Promise<void>;
39
42
  load(key: string): Promise<TCNSProgress | undefined>;
40
43
  delete(key: string): Promise<void>;
@@ -50,26 +53,20 @@ declare class CNSPgBossProgressRepository implements ICNSProgressRepository {
50
53
  close(): Promise<void>;
51
54
  }
52
55
 
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
- }>;
56
+ type TCNSPgBossProgressMigrateOptions = {
57
+ readonly contract?: boolean;
58
+ };
59
+
60
+ declare class CNSPgBossProgressSchema {
61
+ static readonly DEFAULT_TABLE_PREFIX = "cns_";
62
+ private static readonly LONGEST_SUFFIX;
63
+ private static readonly migrator;
64
+ static assertTablePrefix(tablePrefix: string): string;
65
+ static table(tablePrefix?: string): string;
66
+ static migrationTable(tablePrefix?: string): string;
67
+ static migrationSql(tablePrefix?: string, appliedSteps?: readonly number[], options?: TCNSPgBossProgressMigrateOptions): string;
68
+ static migrate(pool: Pool, tablePrefix?: string, options?: TCNSPgBossProgressMigrateOptions): Promise<void>;
69
+ static verify(pool: Pool, tablePrefix?: string): Promise<void>;
66
70
  }
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
71
 
75
- export { CNSPgBossProgressRepository, DEFAULT_TABLE, type IPgQueryable, type TCNSPgBossProgressRepositoryOptions, assertSafeTable, ensureSchema, pgBossProgressSchemaSql };
72
+ export { CNSPgBossProgressRepository, CNSPgBossProgressSchema, type TCNSPgBossProgressMigrateOptions, type TCNSPgBossProgressRepositoryOptions };