@cnstra/pg-boss 0.2.0 → 0.4.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  > **⚠️ Experimental**: API may change in future versions.
4
4
 
5
5
  [pg-boss](https://github.com/timgit/pg-boss) (Postgres-backed job queue) integration for
6
- [CNStra](https://cnstra.org). Run each stimulation as a durable job, get Postgres-native
6
+ [CNStra](https://cnstra.org). Run each stimulation as a resumable job, get Postgres-native
7
7
  retry/backoff/scheduling/singleton, and stream **per-hop progress** into any store for a
8
8
  live admin/history view.
9
9
 
@@ -102,7 +102,7 @@ await store.deleteStale(24 * 60 * 60 * 1000);
102
102
  ## What this package does / doesn't
103
103
 
104
104
  - ✅ Job execution, retry/backoff, scheduling, singleton, concurrency → **pg-boss**.
105
- - ✅ Durable frontier **resume** across retries → a pluggable `resume.repository` (in-package
105
+ - ✅ Resumable frontier **resume** across retries → a pluggable `resume.repository` (in-package
106
106
  Postgres, `@cnstra/progress-redis`, or your own); omit it for plain re-run-from-entry.
107
107
  - ✅ Name-based run/attempt/task **observability** → an optional `observe` store (in-memory to
108
108
  just see it, Postgres to keep it — the same model the DevTools render).
package/dist/index.d.mts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { CNSPersistOptionsRegistry, ICNSStimulationRepository, 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';
5
+ export { ICNSResponseLike, TCNSStimulationJobData } from '@cnstra/broker';
4
6
 
5
7
  /**
6
8
  * Minimal, structural view of the pg-boss surface this package touches.
@@ -22,29 +24,6 @@ interface IPgBossLike {
22
24
  work<TData extends object>(name: string, handler: (jobs: IPgBossJob<TData>[]) => Promise<unknown>): Promise<string>;
23
25
  work<TData extends object>(name: string, options: Record<string, unknown>, handler: (jobs: IPgBossJob<TData>[]) => Promise<unknown>): Promise<string>;
24
26
  }
25
- /**
26
- * The payload placed on the queue. It carries the collateral **name** and a
27
- * JSON-serializable payload — never live neuron/collateral object references,
28
- * which are not portable across processes.
29
- */
30
- interface TCNSStimulationJobData {
31
- collateralName: string;
32
- payload: unknown;
33
- /** Optional per-run cap on neuron hops. */
34
- maxNeuronHops?: number;
35
- }
36
- /** Structural view of the response passed to `onResponse` (not exported by core). */
37
- interface ICNSResponseLike {
38
- inputSignal?: {
39
- collateral?: unknown;
40
- payload?: unknown;
41
- };
42
- outputSignal?: {
43
- collateral?: unknown;
44
- payload?: unknown;
45
- };
46
- error?: unknown;
47
- }
48
27
 
49
28
  interface TCNSWorkerOptions {
50
29
  /** Your pg-boss instance (already started). */
@@ -66,7 +45,7 @@ interface TCNSWorkerOptions {
66
45
  */
67
46
  observe?: ICNSStimulationRepository;
68
47
  /**
69
- * Optional durable **resume**. When set, the worker records the run's
48
+ * Optional resumable **resume**. When set, the worker records the run's
70
49
  * outstanding frontier (by name) into `repository` as it advances, and a
71
50
  * retried job (same job id) resumes from that frontier via `cns.activate(...)`
72
51
  * instead of re-running from the entry signal. The record's lifecycle matches
@@ -94,7 +73,23 @@ interface TCNSWorkerOptions {
94
73
  * `waitUntilComplete()` rejects and the error propagates so pg-boss applies its
95
74
  * own retry/backoff policy.
96
75
  */
97
- declare function createCNSWorker(options: TCNSWorkerOptions): Promise<string>;
76
+ /** A registered worker + a graceful `shutdown` (see {@link createCNSWorker}). */
77
+ interface TCNSPgBossWorkerHandle {
78
+ /** The pg-boss `work()` registration id. */
79
+ workId: string;
80
+ /**
81
+ * Graceful shutdown of THIS worker's processing: stop taking new jobs, let
82
+ * in-flight stimulations finish for up to `drainMs` (default 10s), then **abort**
83
+ * whatever is still running — an aborted stimulation's remaining tasks become its
84
+ * frontier, which the resume checkpoint records, so the job's retry resumes there.
85
+ * It does NOT stop or close your `boss` (you own it) or your stores — call
86
+ * `boss.stop(...)` and close your stores after this resolves.
87
+ */
88
+ shutdown(opts?: {
89
+ drainMs?: number;
90
+ }): Promise<void>;
91
+ }
92
+ declare function createCNSWorker(options: TCNSWorkerOptions): Promise<TCNSPgBossWorkerHandle>;
98
93
 
99
94
  /** Commonly used pg-boss send options (a superset is passed through). */
100
95
  interface TCNSSendOptions {
@@ -116,4 +111,4 @@ declare function stimulationJob(registry: CNSPersistOptionsRegistry, collateral:
116
111
  /** Send a stimulation job to a pg-boss queue. Returns the job id. */
117
112
  declare function enqueueStimulation(boss: IPgBossLike, queue: string, job: TCNSStimulationJobData, options?: TCNSSendOptions): Promise<string | null>;
118
113
 
119
- export { type ICNSResponseLike, type IPgBossJob, type IPgBossLike, type TCNSSendOptions, type TCNSStimulationJobData, type TCNSWorkerOptions, createCNSWorker, enqueueStimulation, stimulationJob };
114
+ export { type IPgBossJob, type IPgBossLike, type TCNSPgBossWorkerHandle, type TCNSSendOptions, type TCNSWorkerOptions, createCNSWorker, enqueueStimulation, stimulationJob };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { CNSPersistOptionsRegistry, ICNSStimulationRepository, 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';
5
+ export { ICNSResponseLike, TCNSStimulationJobData } from '@cnstra/broker';
4
6
 
5
7
  /**
6
8
  * Minimal, structural view of the pg-boss surface this package touches.
@@ -22,29 +24,6 @@ interface IPgBossLike {
22
24
  work<TData extends object>(name: string, handler: (jobs: IPgBossJob<TData>[]) => Promise<unknown>): Promise<string>;
23
25
  work<TData extends object>(name: string, options: Record<string, unknown>, handler: (jobs: IPgBossJob<TData>[]) => Promise<unknown>): Promise<string>;
24
26
  }
25
- /**
26
- * The payload placed on the queue. It carries the collateral **name** and a
27
- * JSON-serializable payload — never live neuron/collateral object references,
28
- * which are not portable across processes.
29
- */
30
- interface TCNSStimulationJobData {
31
- collateralName: string;
32
- payload: unknown;
33
- /** Optional per-run cap on neuron hops. */
34
- maxNeuronHops?: number;
35
- }
36
- /** Structural view of the response passed to `onResponse` (not exported by core). */
37
- interface ICNSResponseLike {
38
- inputSignal?: {
39
- collateral?: unknown;
40
- payload?: unknown;
41
- };
42
- outputSignal?: {
43
- collateral?: unknown;
44
- payload?: unknown;
45
- };
46
- error?: unknown;
47
- }
48
27
 
49
28
  interface TCNSWorkerOptions {
50
29
  /** Your pg-boss instance (already started). */
@@ -66,7 +45,7 @@ interface TCNSWorkerOptions {
66
45
  */
67
46
  observe?: ICNSStimulationRepository;
68
47
  /**
69
- * Optional durable **resume**. When set, the worker records the run's
48
+ * Optional resumable **resume**. When set, the worker records the run's
70
49
  * outstanding frontier (by name) into `repository` as it advances, and a
71
50
  * retried job (same job id) resumes from that frontier via `cns.activate(...)`
72
51
  * instead of re-running from the entry signal. The record's lifecycle matches
@@ -94,7 +73,23 @@ interface TCNSWorkerOptions {
94
73
  * `waitUntilComplete()` rejects and the error propagates so pg-boss applies its
95
74
  * own retry/backoff policy.
96
75
  */
97
- declare function createCNSWorker(options: TCNSWorkerOptions): Promise<string>;
76
+ /** A registered worker + a graceful `shutdown` (see {@link createCNSWorker}). */
77
+ interface TCNSPgBossWorkerHandle {
78
+ /** The pg-boss `work()` registration id. */
79
+ workId: string;
80
+ /**
81
+ * Graceful shutdown of THIS worker's processing: stop taking new jobs, let
82
+ * in-flight stimulations finish for up to `drainMs` (default 10s), then **abort**
83
+ * whatever is still running — an aborted stimulation's remaining tasks become its
84
+ * frontier, which the resume checkpoint records, so the job's retry resumes there.
85
+ * It does NOT stop or close your `boss` (you own it) or your stores — call
86
+ * `boss.stop(...)` and close your stores after this resolves.
87
+ */
88
+ shutdown(opts?: {
89
+ drainMs?: number;
90
+ }): Promise<void>;
91
+ }
92
+ declare function createCNSWorker(options: TCNSWorkerOptions): Promise<TCNSPgBossWorkerHandle>;
98
93
 
99
94
  /** Commonly used pg-boss send options (a superset is passed through). */
100
95
  interface TCNSSendOptions {
@@ -116,4 +111,4 @@ declare function stimulationJob(registry: CNSPersistOptionsRegistry, collateral:
116
111
  /** Send a stimulation job to a pg-boss queue. Returns the job id. */
117
112
  declare function enqueueStimulation(boss: IPgBossLike, queue: string, job: TCNSStimulationJobData, options?: TCNSSendOptions): Promise<string | null>;
118
113
 
119
- export { type ICNSResponseLike, type IPgBossJob, type IPgBossLike, type TCNSSendOptions, type TCNSStimulationJobData, type TCNSWorkerOptions, createCNSWorker, enqueueStimulation, stimulationJob };
114
+ export { type IPgBossJob, type IPgBossLike, type TCNSPgBossWorkerHandle, type TCNSSendOptions, type TCNSWorkerOptions, createCNSWorker, enqueueStimulation, stimulationJob };
package/dist/index.js CHANGED
@@ -28,99 +28,53 @@ module.exports = __toCommonJS(index_exports);
28
28
 
29
29
  // src/worker.ts
30
30
  var import_persist = require("@cnstra/persist");
31
- function createCNSWorker(options) {
31
+ var import_broker = require("@cnstra/broker");
32
+ async function createCNSWorker(options) {
32
33
  const { boss, cns, registry, queue, observe, resume, workOptions } = options;
33
34
  const serializer = resume ? new import_persist.CNSProgressSerializer(registry) : void 0;
35
+ const drain = (0, import_broker.createDrainState)();
34
36
  const runJob = /* @__PURE__ */ __name(async (job) => {
35
37
  var _a;
36
- const { collateralName, payload, maxNeuronHops } = job.data;
37
- const collateral = registry.getCollateral(collateralName);
38
- if (!collateral) {
39
- throw new Error(`[@cnstra/pg-boss] Unknown collateral "${collateralName}". Register it in the CNSPersistOptionsRegistry.`);
40
- }
41
- const recorder = resume && serializer ? new import_persist.CNSProgressRecorder({
42
- serializer,
43
- repository: resume.repository,
44
- key: job.id,
45
- strategy: resume.strategy
46
- }) : void 0;
47
- const attemptNumber = ((_a = job.retryCount) != null ? _a : 0) + 1;
48
- const persistor = observe ? new import_persist.CNSStimulationPersistor({
49
- repository: observe,
38
+ await (0, import_broker.runStimulationJob)({
39
+ pkgName: "@cnstra/pg-boss",
40
+ stopping: drain.isStopping(),
41
+ cns,
50
42
  registry,
51
- stimulationId: job.id,
52
- stimulationAttemptId: `${job.id}#${attemptNumber}`,
53
- attemptNumber,
54
- entry: [
55
- {
56
- collateralName,
57
- payload
58
- }
59
- ],
60
- volume: "full"
61
- }) : void 0;
62
- const onResponse = /* @__PURE__ */ __name((resp) => {
63
- const r = resp;
64
- recorder == null ? void 0 : recorder.onResponse(r);
65
- persistor == null ? void 0 : persistor.onResponse(r);
66
- }, "onResponse");
67
- const stimulationContext = {
68
- stimulationId: job.id,
69
- attemptNumber
70
- };
71
- let stimulation;
72
- if (resume && serializer) {
73
- const saved = await resume.repository.load(job.id);
74
- if (saved) {
75
- const { tasks, ctx } = serializer.hydrate(saved);
76
- stimulation = cns.activate(tasks, {
77
- maxNeuronHops,
78
- ctx,
79
- onResponse,
80
- stimulationContext
81
- });
82
- }
83
- }
84
- if (!stimulation) {
85
- stimulation = cns.stimulate(collateral.createSignal(payload), {
86
- maxNeuronHops,
87
- onResponse,
88
- stimulationContext
89
- });
90
- }
91
- try {
92
- await stimulation.waitUntilComplete();
93
- if (resume) await resume.repository.delete(job.id);
94
- } catch (err) {
95
- if (resume && serializer) {
96
- await resume.repository.save(job.id, serializer.serialize(stimulation));
97
- }
98
- throw err;
99
- } finally {
100
- recorder == null ? void 0 : recorder.dispose();
101
- persistor == null ? void 0 : persistor.dispose();
102
- }
43
+ jobId: job.id,
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
55
+ });
103
56
  }, "runJob");
104
57
  const handler = /* @__PURE__ */ __name(async (jobs) => {
105
58
  for (const job of jobs) {
106
59
  await runJob(job);
107
60
  }
108
61
  }, "handler");
109
- return workOptions ? boss.work(queue, workOptions, handler) : boss.work(queue, 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
70
+ };
110
71
  }
111
72
  __name(createCNSWorker, "createCNSWorker");
112
73
 
113
74
  // src/enqueue.ts
75
+ var import_broker2 = require("@cnstra/broker");
114
76
  function stimulationJob(registry, collateral, payload, extra) {
115
- const collateralName = registry.getCollateralName(collateral);
116
- if (!collateralName) {
117
- throw new Error("[@cnstra/pg-boss] Collateral is not registered in the CNSPersistOptionsRegistry; cannot enqueue it by name.");
118
- }
119
- return {
120
- collateralName,
121
- payload,
122
- ...extra
123
- };
77
+ return (0, import_broker2.stimulationJob)(registry, collateral, payload, extra, "@cnstra/pg-boss");
124
78
  }
125
79
  __name(stimulationJob, "stimulationJob");
126
80
  function enqueueStimulation(boss, queue, job, options) {
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 ICNSResponseLike,\n} from './types';\n","import {\n CNSProgressRecorder,\n CNSProgressSerializer,\n CNSStimulationPersistor,\n} from '@cnstra/persist';\nimport type {\n CNSPersistOptionsRegistry,\n ICNSProgressRepository,\n ICNSProgressRecordingStrategy,\n ICNSStimulationRepository,\n} from '@cnstra/persist';\nimport type { ICNS, TCNSStimulationResponse } from '@cnstra/types';\nimport type {\n IPgBossLike,\n IPgBossJob,\n ICNSResponseLike,\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 durable **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 */\nexport function createCNSWorker(options: TCNSWorkerOptions): Promise<string> {\n const { boss, cns, registry, queue, observe, resume, workOptions } =\n options;\n\n // One serializer per worker; the recorder is per-job (holds job-specific key).\n const serializer = resume\n ? new CNSProgressSerializer(registry)\n : undefined;\n\n const runJob = async (\n job: IPgBossJob<TCNSStimulationJobData>\n ): Promise<void> => {\n const { collateralName, payload, maxNeuronHops } = job.data;\n const collateral = registry.getCollateral(collateralName);\n if (!collateral) {\n throw new Error(\n `[@cnstra/pg-boss] Unknown collateral \"${collateralName}\". ` +\n `Register it in the CNSPersistOptionsRegistry.`\n );\n }\n\n // Records the outstanding frontier as the run advances (crash-safety); the\n // explicit save on failure below guarantees the exact final frontier.\n const recorder =\n resume && serializer\n ? new CNSProgressRecorder({\n serializer,\n repository: resume.repository,\n key: job.id,\n strategy: resume.strategy,\n })\n : undefined;\n\n // Observability: stream the canonical name-based Stimulation → Attempt →\n // Task model to `observe`. One job = one stimulation; each pg-boss retry is\n // a new attempt (retryCount is 0-based).\n const attemptNumber = (job.retryCount ?? 0) + 1;\n const persistor = observe\n ? new CNSStimulationPersistor({\n repository: observe,\n registry,\n stimulationId: job.id,\n stimulationAttemptId: `${job.id}#${attemptNumber}`,\n attemptNumber,\n entry: [{ collateralName, payload }],\n volume: 'full',\n })\n : undefined;\n\n const onResponse = (resp: ICNSResponseLike) => {\n const r = resp as unknown as TCNSStimulationResponse;\n // recorder → resume checkpoint; persistor → observability history.\n recorder?.onResponse(r);\n persistor?.onResponse(r);\n };\n\n // Correlate logs to this run: every handler reads `ctx.stimulationContext`\n // (same object on every hop, sync or async) — no wiring on the user's side.\n const stimulationContext = { stimulationId: job.id, attemptNumber };\n\n // Resume from a persisted frontier if this job already has one (a retry of\n // the same job id); otherwise start fresh from the entry signal.\n let stimulation;\n if (resume && serializer) {\n const saved = await resume.repository.load(job.id);\n if (saved) {\n const { tasks, ctx } = serializer.hydrate(saved);\n stimulation = cns.activate(tasks, {\n maxNeuronHops,\n ctx,\n onResponse,\n stimulationContext,\n });\n }\n }\n if (!stimulation) {\n stimulation = cns.stimulate(collateral.createSignal(payload), {\n maxNeuronHops,\n onResponse,\n stimulationContext,\n });\n }\n\n try {\n await stimulation.waitUntilComplete();\n // Job about to complete → its resume checkpoint can go (lifecycle = job row).\n if (resume) await resume.repository.delete(job.id);\n } catch (err) {\n // Persist the exact final frontier before throwing so the retried job\n // resumes from it (the recorder's cadence may still be debouncing).\n if (resume && serializer) {\n await resume.repository.save(\n job.id,\n serializer.serialize(stimulation)\n );\n }\n // Let pg-boss decide about retries/backoff.\n throw err;\n } finally {\n recorder?.dispose();\n persistor?.dispose();\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 return workOptions\n ? boss.work<TCNSStimulationJobData>(queue, workOptions, handler)\n : boss.work<TCNSStimulationJobData>(queue, handler);\n}\n","import type { CNSCollateral } from '@cnstra/core';\nimport type { CNSPersistOptionsRegistry } from '@cnstra/persist';\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 const collateralName = registry.getCollateralName(collateral);\n if (!collateralName) {\n throw new Error(\n '[@cnstra/pg-boss] Collateral is not registered in the ' +\n 'CNSPersistOptionsRegistry; cannot enqueue it by name.'\n );\n }\n return { collateralName, payload, ...extra };\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,qBAIO;AAgEA,SAASA,gBAAgBC,SAA0B;AACtD,QAAM,EAAEC,MAAMC,KAAKC,UAAUC,OAAOC,SAASC,QAAQC,YAAW,IAC5DP;AAGJ,QAAMQ,aAAaF,SACb,IAAIG,qCAAsBN,QAAAA,IAC1BO;AAEN,QAAMC,SAAS,8BACXC,QAAAA;AA9ER;AAgFQ,UAAM,EAAEC,gBAAgBC,SAASC,cAAa,IAAKH,IAAII;AACvD,UAAMC,aAAad,SAASe,cAAcL,cAAAA;AAC1C,QAAI,CAACI,YAAY;AACb,YAAM,IAAIE,MACN,yCAAyCN,cAAAA,kDACU;IAE3D;AAIA,UAAMO,WACFd,UAAUE,aACJ,IAAIa,mCAAoB;MACpBb;MACAc,YAAYhB,OAAOgB;MACnBC,KAAKX,IAAIY;MACTC,UAAUnB,OAAOmB;IACrB,CAAA,IACAf;AAKV,UAAMgB,kBAAiBd,SAAIe,eAAJf,YAAkB,KAAK;AAC9C,UAAMgB,YAAYvB,UACZ,IAAIwB,uCAAwB;MACxBP,YAAYjB;MACZF;MACA2B,eAAelB,IAAIY;MACnBO,sBAAsB,GAAGnB,IAAIY,EAAE,IAAIE,aAAAA;MACnCA;MACAM,OAAO;QAAC;UAAEnB;UAAgBC;QAAQ;;MAClCmB,QAAQ;IACZ,CAAA,IACAvB;AAEN,UAAMwB,aAAa,wBAACC,SAAAA;AAChB,YAAMC,IAAID;AAEVf,2CAAUc,WAAWE;AACrBR,6CAAWM,WAAWE;IAC1B,GALmB;AASnB,UAAMC,qBAAqB;MAAEP,eAAelB,IAAIY;MAAIE;IAAc;AAIlE,QAAIY;AACJ,QAAIhC,UAAUE,YAAY;AACtB,YAAM+B,QAAQ,MAAMjC,OAAOgB,WAAWkB,KAAK5B,IAAIY,EAAE;AACjD,UAAIe,OAAO;AACP,cAAM,EAAEE,OAAOC,IAAG,IAAKlC,WAAWmC,QAAQJ,KAAAA;AAC1CD,sBAAcpC,IAAI0C,SAASH,OAAO;UAC9B1B;UACA2B;UACAR;UACAG;QACJ,CAAA;MACJ;IACJ;AACA,QAAI,CAACC,aAAa;AACdA,oBAAcpC,IAAI2C,UAAU5B,WAAW6B,aAAahC,OAAAA,GAAU;QAC1DC;QACAmB;QACAG;MACJ,CAAA;IACJ;AAEA,QAAI;AACA,YAAMC,YAAYS,kBAAiB;AAEnC,UAAIzC,OAAQ,OAAMA,OAAOgB,WAAW0B,OAAOpC,IAAIY,EAAE;IACrD,SAASyB,KAAK;AAGV,UAAI3C,UAAUE,YAAY;AACtB,cAAMF,OAAOgB,WAAW4B,KACpBtC,IAAIY,IACJhB,WAAW2C,UAAUb,WAAAA,CAAAA;MAE7B;AAEA,YAAMW;IACV,UAAA;AACI7B,2CAAUgC;AACVxB,6CAAWwB;IACf;EACJ,GA7Fe;AA+Ff,QAAMC,UAAU,8BACZC,SAAAA;AAEA,eAAW1C,OAAO0C,MAAM;AACpB,YAAM3C,OAAOC,GAAAA;IACjB;EACJ,GANgB;AAQhB,SAAOL,cACDN,KAAKsD,KAA6BnD,OAAOG,aAAa8C,OAAAA,IACtDpD,KAAKsD,KAA6BnD,OAAOiD,OAAAA;AACnD;AAnHgBtD;;;AC/CT,SAASyD,eACZC,UACAC,YACAC,SACAC,OAAqD;AAErD,QAAMC,iBAAiBJ,SAASK,kBAAkBJ,UAAAA;AAClD,MAAI,CAACG,gBAAgB;AACjB,UAAM,IAAIE,MACN,6GACI;EAEZ;AACA,SAAO;IAAEF;IAAgBF;IAAS,GAAGC;EAAM;AAC/C;AAdgBJ;AAiBT,SAASQ,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","runJob","job","collateralName","payload","maxNeuronHops","data","collateral","getCollateral","Error","recorder","CNSProgressRecorder","repository","key","id","strategy","attemptNumber","retryCount","persistor","CNSStimulationPersistor","stimulationId","stimulationAttemptId","entry","volume","onResponse","resp","r","stimulationContext","stimulation","saved","load","tasks","ctx","hydrate","activate","stimulate","createSignal","waitUntilComplete","delete","err","save","serialize","dispose","handler","jobs","work","stimulationJob","registry","collateral","payload","extra","collateralName","getCollateralName","Error","enqueueStimulation","boss","queue","job","options","send"]}
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
@@ -3,100 +3,54 @@ import {
3
3
  } from "./chunk-2JTKI4GS.mjs";
4
4
 
5
5
  // src/worker.ts
6
- import { CNSProgressRecorder, CNSProgressSerializer, CNSStimulationPersistor } from "@cnstra/persist";
7
- function createCNSWorker(options) {
6
+ import { CNSProgressSerializer } from "@cnstra/persist";
7
+ import { runStimulationJob, createDrainState } from "@cnstra/broker";
8
+ async function createCNSWorker(options) {
8
9
  const { boss, cns, registry, queue, observe, resume, workOptions } = options;
9
10
  const serializer = resume ? new CNSProgressSerializer(registry) : void 0;
11
+ const drain = createDrainState();
10
12
  const runJob = /* @__PURE__ */ __name(async (job) => {
11
13
  var _a;
12
- const { collateralName, payload, maxNeuronHops } = job.data;
13
- const collateral = registry.getCollateral(collateralName);
14
- if (!collateral) {
15
- throw new Error(`[@cnstra/pg-boss] Unknown collateral "${collateralName}". Register it in the CNSPersistOptionsRegistry.`);
16
- }
17
- const recorder = resume && serializer ? new CNSProgressRecorder({
18
- serializer,
19
- repository: resume.repository,
20
- key: job.id,
21
- strategy: resume.strategy
22
- }) : void 0;
23
- const attemptNumber = ((_a = job.retryCount) != null ? _a : 0) + 1;
24
- const persistor = observe ? new CNSStimulationPersistor({
25
- repository: observe,
14
+ await runStimulationJob({
15
+ pkgName: "@cnstra/pg-boss",
16
+ stopping: drain.isStopping(),
17
+ cns,
26
18
  registry,
27
- stimulationId: job.id,
28
- stimulationAttemptId: `${job.id}#${attemptNumber}`,
29
- attemptNumber,
30
- entry: [
31
- {
32
- collateralName,
33
- payload
34
- }
35
- ],
36
- volume: "full"
37
- }) : void 0;
38
- const onResponse = /* @__PURE__ */ __name((resp) => {
39
- const r = resp;
40
- recorder == null ? void 0 : recorder.onResponse(r);
41
- persistor == null ? void 0 : persistor.onResponse(r);
42
- }, "onResponse");
43
- const stimulationContext = {
44
- stimulationId: job.id,
45
- attemptNumber
46
- };
47
- let stimulation;
48
- if (resume && serializer) {
49
- const saved = await resume.repository.load(job.id);
50
- if (saved) {
51
- const { tasks, ctx } = serializer.hydrate(saved);
52
- stimulation = cns.activate(tasks, {
53
- maxNeuronHops,
54
- ctx,
55
- onResponse,
56
- stimulationContext
57
- });
58
- }
59
- }
60
- if (!stimulation) {
61
- stimulation = cns.stimulate(collateral.createSignal(payload), {
62
- maxNeuronHops,
63
- onResponse,
64
- stimulationContext
65
- });
66
- }
67
- try {
68
- await stimulation.waitUntilComplete();
69
- if (resume) await resume.repository.delete(job.id);
70
- } catch (err) {
71
- if (resume && serializer) {
72
- await resume.repository.save(job.id, serializer.serialize(stimulation));
73
- }
74
- throw err;
75
- } finally {
76
- recorder == null ? void 0 : recorder.dispose();
77
- persistor == null ? void 0 : persistor.dispose();
78
- }
19
+ jobId: job.id,
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
31
+ });
79
32
  }, "runJob");
80
33
  const handler = /* @__PURE__ */ __name(async (jobs) => {
81
34
  for (const job of jobs) {
82
35
  await runJob(job);
83
36
  }
84
37
  }, "handler");
85
- return workOptions ? boss.work(queue, workOptions, handler) : boss.work(queue, 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
46
+ };
86
47
  }
87
48
  __name(createCNSWorker, "createCNSWorker");
88
49
 
89
50
  // src/enqueue.ts
51
+ import { stimulationJob as brokerStimulationJob } from "@cnstra/broker";
90
52
  function stimulationJob(registry, collateral, payload, extra) {
91
- const collateralName = registry.getCollateralName(collateral);
92
- if (!collateralName) {
93
- throw new Error("[@cnstra/pg-boss] Collateral is not registered in the CNSPersistOptionsRegistry; cannot enqueue it by name.");
94
- }
95
- return {
96
- collateralName,
97
- payload,
98
- ...extra
99
- };
53
+ return brokerStimulationJob(registry, collateral, payload, extra, "@cnstra/pg-boss");
100
54
  }
101
55
  __name(stimulationJob, "stimulationJob");
102
56
  function enqueueStimulation(boss, queue, job, options) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/worker.ts","../src/enqueue.ts"],"sourcesContent":["import {\n CNSProgressRecorder,\n CNSProgressSerializer,\n CNSStimulationPersistor,\n} from '@cnstra/persist';\nimport type {\n CNSPersistOptionsRegistry,\n ICNSProgressRepository,\n ICNSProgressRecordingStrategy,\n ICNSStimulationRepository,\n} from '@cnstra/persist';\nimport type { ICNS, TCNSStimulationResponse } from '@cnstra/types';\nimport type {\n IPgBossLike,\n IPgBossJob,\n ICNSResponseLike,\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 durable **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 */\nexport function createCNSWorker(options: TCNSWorkerOptions): Promise<string> {\n const { boss, cns, registry, queue, observe, resume, workOptions } =\n options;\n\n // One serializer per worker; the recorder is per-job (holds job-specific key).\n const serializer = resume\n ? new CNSProgressSerializer(registry)\n : undefined;\n\n const runJob = async (\n job: IPgBossJob<TCNSStimulationJobData>\n ): Promise<void> => {\n const { collateralName, payload, maxNeuronHops } = job.data;\n const collateral = registry.getCollateral(collateralName);\n if (!collateral) {\n throw new Error(\n `[@cnstra/pg-boss] Unknown collateral \"${collateralName}\". ` +\n `Register it in the CNSPersistOptionsRegistry.`\n );\n }\n\n // Records the outstanding frontier as the run advances (crash-safety); the\n // explicit save on failure below guarantees the exact final frontier.\n const recorder =\n resume && serializer\n ? new CNSProgressRecorder({\n serializer,\n repository: resume.repository,\n key: job.id,\n strategy: resume.strategy,\n })\n : undefined;\n\n // Observability: stream the canonical name-based Stimulation → Attempt →\n // Task model to `observe`. One job = one stimulation; each pg-boss retry is\n // a new attempt (retryCount is 0-based).\n const attemptNumber = (job.retryCount ?? 0) + 1;\n const persistor = observe\n ? new CNSStimulationPersistor({\n repository: observe,\n registry,\n stimulationId: job.id,\n stimulationAttemptId: `${job.id}#${attemptNumber}`,\n attemptNumber,\n entry: [{ collateralName, payload }],\n volume: 'full',\n })\n : undefined;\n\n const onResponse = (resp: ICNSResponseLike) => {\n const r = resp as unknown as TCNSStimulationResponse;\n // recorder → resume checkpoint; persistor → observability history.\n recorder?.onResponse(r);\n persistor?.onResponse(r);\n };\n\n // Correlate logs to this run: every handler reads `ctx.stimulationContext`\n // (same object on every hop, sync or async) — no wiring on the user's side.\n const stimulationContext = { stimulationId: job.id, attemptNumber };\n\n // Resume from a persisted frontier if this job already has one (a retry of\n // the same job id); otherwise start fresh from the entry signal.\n let stimulation;\n if (resume && serializer) {\n const saved = await resume.repository.load(job.id);\n if (saved) {\n const { tasks, ctx } = serializer.hydrate(saved);\n stimulation = cns.activate(tasks, {\n maxNeuronHops,\n ctx,\n onResponse,\n stimulationContext,\n });\n }\n }\n if (!stimulation) {\n stimulation = cns.stimulate(collateral.createSignal(payload), {\n maxNeuronHops,\n onResponse,\n stimulationContext,\n });\n }\n\n try {\n await stimulation.waitUntilComplete();\n // Job about to complete → its resume checkpoint can go (lifecycle = job row).\n if (resume) await resume.repository.delete(job.id);\n } catch (err) {\n // Persist the exact final frontier before throwing so the retried job\n // resumes from it (the recorder's cadence may still be debouncing).\n if (resume && serializer) {\n await resume.repository.save(\n job.id,\n serializer.serialize(stimulation)\n );\n }\n // Let pg-boss decide about retries/backoff.\n throw err;\n } finally {\n recorder?.dispose();\n persistor?.dispose();\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 return workOptions\n ? boss.work<TCNSStimulationJobData>(queue, workOptions, handler)\n : boss.work<TCNSStimulationJobData>(queue, handler);\n}\n","import type { CNSCollateral } from '@cnstra/core';\nimport type { CNSPersistOptionsRegistry } from '@cnstra/persist';\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 const collateralName = registry.getCollateralName(collateral);\n if (!collateralName) {\n throw new Error(\n '[@cnstra/pg-boss] Collateral is not registered in the ' +\n 'CNSPersistOptionsRegistry; cannot enqueue it by name.'\n );\n }\n return { collateralName, payload, ...extra };\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,SACIA,qBACAC,uBACAC,+BACG;AAgEA,SAASC,gBAAgBC,SAA0B;AACtD,QAAM,EAAEC,MAAMC,KAAKC,UAAUC,OAAOC,SAASC,QAAQC,YAAW,IAC5DP;AAGJ,QAAMQ,aAAaF,SACb,IAAIG,sBAAsBN,QAAAA,IAC1BO;AAEN,QAAMC,SAAS,8BACXC,QAAAA;AA9ER;AAgFQ,UAAM,EAAEC,gBAAgBC,SAASC,cAAa,IAAKH,IAAII;AACvD,UAAMC,aAAad,SAASe,cAAcL,cAAAA;AAC1C,QAAI,CAACI,YAAY;AACb,YAAM,IAAIE,MACN,yCAAyCN,cAAAA,kDACU;IAE3D;AAIA,UAAMO,WACFd,UAAUE,aACJ,IAAIa,oBAAoB;MACpBb;MACAc,YAAYhB,OAAOgB;MACnBC,KAAKX,IAAIY;MACTC,UAAUnB,OAAOmB;IACrB,CAAA,IACAf;AAKV,UAAMgB,kBAAiBd,SAAIe,eAAJf,YAAkB,KAAK;AAC9C,UAAMgB,YAAYvB,UACZ,IAAIwB,wBAAwB;MACxBP,YAAYjB;MACZF;MACA2B,eAAelB,IAAIY;MACnBO,sBAAsB,GAAGnB,IAAIY,EAAE,IAAIE,aAAAA;MACnCA;MACAM,OAAO;QAAC;UAAEnB;UAAgBC;QAAQ;;MAClCmB,QAAQ;IACZ,CAAA,IACAvB;AAEN,UAAMwB,aAAa,wBAACC,SAAAA;AAChB,YAAMC,IAAID;AAEVf,2CAAUc,WAAWE;AACrBR,6CAAWM,WAAWE;IAC1B,GALmB;AASnB,UAAMC,qBAAqB;MAAEP,eAAelB,IAAIY;MAAIE;IAAc;AAIlE,QAAIY;AACJ,QAAIhC,UAAUE,YAAY;AACtB,YAAM+B,QAAQ,MAAMjC,OAAOgB,WAAWkB,KAAK5B,IAAIY,EAAE;AACjD,UAAIe,OAAO;AACP,cAAM,EAAEE,OAAOC,IAAG,IAAKlC,WAAWmC,QAAQJ,KAAAA;AAC1CD,sBAAcpC,IAAI0C,SAASH,OAAO;UAC9B1B;UACA2B;UACAR;UACAG;QACJ,CAAA;MACJ;IACJ;AACA,QAAI,CAACC,aAAa;AACdA,oBAAcpC,IAAI2C,UAAU5B,WAAW6B,aAAahC,OAAAA,GAAU;QAC1DC;QACAmB;QACAG;MACJ,CAAA;IACJ;AAEA,QAAI;AACA,YAAMC,YAAYS,kBAAiB;AAEnC,UAAIzC,OAAQ,OAAMA,OAAOgB,WAAW0B,OAAOpC,IAAIY,EAAE;IACrD,SAASyB,KAAK;AAGV,UAAI3C,UAAUE,YAAY;AACtB,cAAMF,OAAOgB,WAAW4B,KACpBtC,IAAIY,IACJhB,WAAW2C,UAAUb,WAAAA,CAAAA;MAE7B;AAEA,YAAMW;IACV,UAAA;AACI7B,2CAAUgC;AACVxB,6CAAWwB;IACf;EACJ,GA7Fe;AA+Ff,QAAMC,UAAU,8BACZC,SAAAA;AAEA,eAAW1C,OAAO0C,MAAM;AACpB,YAAM3C,OAAOC,GAAAA;IACjB;EACJ,GANgB;AAQhB,SAAOL,cACDN,KAAKsD,KAA6BnD,OAAOG,aAAa8C,OAAAA,IACtDpD,KAAKsD,KAA6BnD,OAAOiD,OAAAA;AACnD;AAnHgBtD;;;AC/CT,SAASyD,eACZC,UACAC,YACAC,SACAC,OAAqD;AAErD,QAAMC,iBAAiBJ,SAASK,kBAAkBJ,UAAAA;AAClD,MAAI,CAACG,gBAAgB;AACjB,UAAM,IAAIE,MACN,6GACI;EAEZ;AACA,SAAO;IAAEF;IAAgBF;IAAS,GAAGC;EAAM;AAC/C;AAdgBJ;AAiBT,SAASQ,mBACZC,MACAC,OACAC,KACAC,SAAyB;AAEzB,SAAOH,KAAKI,KAAKH,OAAOC,KAA0BC,OAAAA;AACtD;AAPgBJ;","names":["CNSProgressRecorder","CNSProgressSerializer","CNSStimulationPersistor","createCNSWorker","options","boss","cns","registry","queue","observe","resume","workOptions","serializer","CNSProgressSerializer","undefined","runJob","job","collateralName","payload","maxNeuronHops","data","collateral","getCollateral","Error","recorder","CNSProgressRecorder","repository","key","id","strategy","attemptNumber","retryCount","persistor","CNSStimulationPersistor","stimulationId","stimulationAttemptId","entry","volume","onResponse","resp","r","stimulationContext","stimulation","saved","load","tasks","ctx","hydrate","activate","stimulate","createSignal","waitUntilComplete","delete","err","save","serialize","dispose","handler","jobs","work","stimulationJob","registry","collateral","payload","extra","collateralName","getCollateralName","Error","enqueueStimulation","boss","queue","job","options","send"]}
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cnstra/pg-boss",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "pg-boss (Postgres) integration for CNStra: run stimulations as jobs and stream per-hop progress",
5
5
  "keywords": [
6
6
  "cnstra",
@@ -33,32 +33,35 @@
33
33
  "dist"
34
34
  ],
35
35
  "scripts": {
36
+ "prepublishOnly": "node ../../scripts/check-drift.mjs --gate",
36
37
  "build": "tsup",
37
38
  "test": "jest",
38
39
  "test:types": "tsc --noEmit",
39
40
  "clean": "rm -rf dist"
40
41
  },
41
42
  "dependencies": {
42
- "@cnstra/types": "^0.1.0"
43
+ "@cnstra/types": "^0.2.0",
44
+ "@cnstra/broker": "^0.2.0"
43
45
  },
44
46
  "peerDependencies": {
45
- "@cnstra/core": "^2.4.0",
46
- "@cnstra/persist": "^0.1.0",
47
- "@cnstra/persist-dto": "^0.1.0",
47
+ "@cnstra/core": "^3.0.0",
48
+ "@cnstra/persist": "^0.2.0",
49
+ "@cnstra/persist-dto": "^0.2.0",
48
50
  "pg-boss": ">=9",
49
51
  "pg": ">=8"
50
52
  },
51
53
  "devDependencies": {
52
- "@cnstra/core": "^2.4.0",
53
- "@cnstra/persist": "^0.1.0",
54
- "@cnstra/persist-dto": "^0.1.0",
54
+ "@cnstra/core": "^3.0.0",
55
+ "@cnstra/persist": "^0.2.0",
56
+ "@cnstra/persist-dto": "^0.2.0",
55
57
  "@types/jest": "29.5.12",
56
58
  "@types/pg": "^8.11.0",
57
59
  "jest": "29.7.0",
58
60
  "pg": "^8.11.0",
59
61
  "ts-jest": "29.1.2",
60
62
  "tsup": "8.5.1",
61
- "typescript": "5.7.2"
63
+ "typescript": "5.7.2",
64
+ "@cnstra/broker": "^0.2.0"
62
65
  },
63
66
  "peerDependenciesMeta": {
64
67
  "pg-boss": {