@cnstra/pg-boss 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 TCNSHopRecord,\n TCNSStimulationLifecycle,\n ICNSProgressSink,\n ICNSResponseLike,\n} from './types';\n","import type { CNSPersistOptionsRegistry, ICNS } from '@cnstra/core';\nimport type {\n IPgBossLike,\n IPgBossJob,\n ICNSProgressSink,\n ICNSResponseLike,\n TCNSHopRecord,\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 /** Optional durable progress sink (started / hop / completed). */\n progress?: ICNSProgressSink;\n /** pg-boss `work` options (batchSize, pollingIntervalSeconds, …). */\n workOptions?: Record<string, unknown>;\n}\n\n/**\n * Resolve a single response into a durable hop record, turning live object\n * references into stable names via the registry (same derivation devtools uses).\n */\nfunction toHopRecord(\n jobId: string,\n index: number,\n resp: ICNSResponseLike,\n cns: ICNS<any, any>,\n registry: CNSPersistOptionsRegistry\n): TCNSHopRecord {\n const outputCol = resp.outputSignal?.collateral;\n const inputCol = resp.inputSignal?.collateral;\n\n const ownerNeuron = outputCol\n ? cns.network.getParentNeuronByCollateral(outputCol as any)\n : null;\n\n return {\n jobId,\n index,\n neuronName: ownerNeuron\n ? registry.getNeuronName(ownerNeuron as any) ?? null\n : null,\n inputCollateral: inputCol\n ? registry.getCollateralName(inputCol as any) ?? null\n : null,\n outputCollateral: outputCol\n ? registry.getCollateralName(outputCol as any) ?? null\n : null,\n inputPayload:\n resp.inputSignal?.payload ?? resp.outputSignal?.payload ?? null,\n outputPayload: outputCol ? resp.outputSignal?.payload ?? null : null,\n error: resp.error != null ? String(resp.error) : null,\n at: Date.now(),\n };\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, progress, workOptions } = options;\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 const startedAt = Date.now();\n let hopCount = 0;\n\n await progress?.onStarted?.({\n jobId: job.id,\n collateralName,\n payload,\n status: 'running',\n startedAt,\n hopCount: 0,\n });\n\n const stimulation = cns.stimulate(collateral.createSignal(payload), {\n maxNeuronHops,\n onResponse: async (resp: ICNSResponseLike) => {\n const rec = toHopRecord(\n job.id,\n hopCount,\n resp,\n cns,\n registry\n );\n hopCount += 1;\n // Returning a Promise makes CNStra wait for the durable write\n // before enqueuing the next hop (per-hop checkpoint barrier).\n await progress?.onHop?.(rec);\n },\n });\n\n try {\n await stimulation.waitUntilComplete();\n await progress?.onCompleted?.({\n jobId: job.id,\n collateralName,\n payload,\n status: 'completed',\n startedAt,\n completedAt: Date.now(),\n hopCount,\n });\n } catch (err) {\n await progress?.onCompleted?.({\n jobId: job.id,\n collateralName,\n payload,\n status: 'failed',\n startedAt,\n completedAt: Date.now(),\n hopCount,\n error: err instanceof Error ? err.message : String(err),\n });\n // Let pg-boss decide about retries/backoff.\n throw err;\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, CNSPersistOptionsRegistry } from '@cnstra/core';\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":"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","names":["_a"]}
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":"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","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"]}
package/dist/index.mjs CHANGED
@@ -1,103 +1,108 @@
1
+ import {
2
+ __name
3
+ } from "./chunk-2JTKI4GS.mjs";
4
+
1
5
  // src/worker.ts
2
- function toHopRecord(jobId, index, resp, cns, registry) {
3
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
4
- const outputCol = (_a = resp.outputSignal) == null ? void 0 : _a.collateral;
5
- const inputCol = (_b = resp.inputSignal) == null ? void 0 : _b.collateral;
6
- const ownerNeuron = outputCol ? cns.network.getParentNeuronByCollateral(outputCol) : null;
7
- return {
8
- jobId,
9
- index,
10
- neuronName: ownerNeuron ? (_c = registry.getNeuronName(ownerNeuron)) != null ? _c : null : null,
11
- inputCollateral: inputCol ? (_d = registry.getCollateralName(inputCol)) != null ? _d : null : null,
12
- outputCollateral: outputCol ? (_e = registry.getCollateralName(outputCol)) != null ? _e : null : null,
13
- inputPayload: (_i = (_h = (_f = resp.inputSignal) == null ? void 0 : _f.payload) != null ? _h : (_g = resp.outputSignal) == null ? void 0 : _g.payload) != null ? _i : null,
14
- outputPayload: outputCol ? (_k = (_j = resp.outputSignal) == null ? void 0 : _j.payload) != null ? _k : null : null,
15
- error: resp.error != null ? String(resp.error) : null,
16
- at: Date.now()
17
- };
18
- }
6
+ import { CNSProgressRecorder, CNSProgressSerializer, CNSStimulationPersistor } from "@cnstra/persist";
19
7
  function createCNSWorker(options) {
20
- const { boss, cns, registry, queue, progress, workOptions } = options;
21
- const runJob = async (job) => {
22
- var _a, _b, _c;
8
+ const { boss, cns, registry, queue, observe, resume, workOptions } = options;
9
+ const serializer = resume ? new CNSProgressSerializer(registry) : void 0;
10
+ const runJob = /* @__PURE__ */ __name(async (job) => {
11
+ var _a;
23
12
  const { collateralName, payload, maxNeuronHops } = job.data;
24
13
  const collateral = registry.getCollateral(collateralName);
25
14
  if (!collateral) {
26
- throw new Error(
27
- `[@cnstra/pg-boss] Unknown collateral "${collateralName}". Register it in the CNSPersistOptionsRegistry.`
28
- );
15
+ throw new Error(`[@cnstra/pg-boss] Unknown collateral "${collateralName}". Register it in the CNSPersistOptionsRegistry.`);
29
16
  }
30
- const startedAt = Date.now();
31
- let hopCount = 0;
32
- await ((_a = progress == null ? void 0 : progress.onStarted) == null ? void 0 : _a.call(progress, {
33
- jobId: job.id,
34
- collateralName,
35
- payload,
36
- status: "running",
37
- startedAt,
38
- hopCount: 0
39
- }));
40
- const stimulation = cns.stimulate(collateral.createSignal(payload), {
41
- maxNeuronHops,
42
- onResponse: async (resp) => {
43
- var _a2;
44
- const rec = toHopRecord(
45
- job.id,
46
- hopCount,
47
- resp,
48
- cns,
49
- registry
50
- );
51
- hopCount += 1;
52
- await ((_a2 = progress == null ? void 0 : progress.onHop) == null ? void 0 : _a2.call(progress, rec));
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,
26
+ 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
+ });
53
58
  }
54
- });
59
+ }
60
+ if (!stimulation) {
61
+ stimulation = cns.stimulate(collateral.createSignal(payload), {
62
+ maxNeuronHops,
63
+ onResponse,
64
+ stimulationContext
65
+ });
66
+ }
55
67
  try {
56
68
  await stimulation.waitUntilComplete();
57
- await ((_b = progress == null ? void 0 : progress.onCompleted) == null ? void 0 : _b.call(progress, {
58
- jobId: job.id,
59
- collateralName,
60
- payload,
61
- status: "completed",
62
- startedAt,
63
- completedAt: Date.now(),
64
- hopCount
65
- }));
69
+ if (resume) await resume.repository.delete(job.id);
66
70
  } catch (err) {
67
- await ((_c = progress == null ? void 0 : progress.onCompleted) == null ? void 0 : _c.call(progress, {
68
- jobId: job.id,
69
- collateralName,
70
- payload,
71
- status: "failed",
72
- startedAt,
73
- completedAt: Date.now(),
74
- hopCount,
75
- error: err instanceof Error ? err.message : String(err)
76
- }));
71
+ if (resume && serializer) {
72
+ await resume.repository.save(job.id, serializer.serialize(stimulation));
73
+ }
77
74
  throw err;
75
+ } finally {
76
+ recorder == null ? void 0 : recorder.dispose();
77
+ persistor == null ? void 0 : persistor.dispose();
78
78
  }
79
- };
80
- const handler = async (jobs) => {
79
+ }, "runJob");
80
+ const handler = /* @__PURE__ */ __name(async (jobs) => {
81
81
  for (const job of jobs) {
82
82
  await runJob(job);
83
83
  }
84
- };
84
+ }, "handler");
85
85
  return workOptions ? boss.work(queue, workOptions, handler) : boss.work(queue, handler);
86
86
  }
87
+ __name(createCNSWorker, "createCNSWorker");
87
88
 
88
89
  // src/enqueue.ts
89
90
  function stimulationJob(registry, collateral, payload, extra) {
90
91
  const collateralName = registry.getCollateralName(collateral);
91
92
  if (!collateralName) {
92
- throw new Error(
93
- "[@cnstra/pg-boss] Collateral is not registered in the CNSPersistOptionsRegistry; cannot enqueue it by name."
94
- );
93
+ throw new Error("[@cnstra/pg-boss] Collateral is not registered in the CNSPersistOptionsRegistry; cannot enqueue it by name.");
95
94
  }
96
- return { collateralName, payload, ...extra };
95
+ return {
96
+ collateralName,
97
+ payload,
98
+ ...extra
99
+ };
97
100
  }
101
+ __name(stimulationJob, "stimulationJob");
98
102
  function enqueueStimulation(boss, queue, job, options) {
99
103
  return boss.send(queue, job, options);
100
104
  }
105
+ __name(enqueueStimulation, "enqueueStimulation");
101
106
  export {
102
107
  createCNSWorker,
103
108
  enqueueStimulation,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/worker.ts","../src/enqueue.ts"],"sourcesContent":["import type { CNSPersistOptionsRegistry, ICNS } from '@cnstra/core';\nimport type {\n IPgBossLike,\n IPgBossJob,\n ICNSProgressSink,\n ICNSResponseLike,\n TCNSHopRecord,\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 /** Optional durable progress sink (started / hop / completed). */\n progress?: ICNSProgressSink;\n /** pg-boss `work` options (batchSize, pollingIntervalSeconds, …). */\n workOptions?: Record<string, unknown>;\n}\n\n/**\n * Resolve a single response into a durable hop record, turning live object\n * references into stable names via the registry (same derivation devtools uses).\n */\nfunction toHopRecord(\n jobId: string,\n index: number,\n resp: ICNSResponseLike,\n cns: ICNS<any, any>,\n registry: CNSPersistOptionsRegistry\n): TCNSHopRecord {\n const outputCol = resp.outputSignal?.collateral;\n const inputCol = resp.inputSignal?.collateral;\n\n const ownerNeuron = outputCol\n ? cns.network.getParentNeuronByCollateral(outputCol as any)\n : null;\n\n return {\n jobId,\n index,\n neuronName: ownerNeuron\n ? registry.getNeuronName(ownerNeuron as any) ?? null\n : null,\n inputCollateral: inputCol\n ? registry.getCollateralName(inputCol as any) ?? null\n : null,\n outputCollateral: outputCol\n ? registry.getCollateralName(outputCol as any) ?? null\n : null,\n inputPayload:\n resp.inputSignal?.payload ?? resp.outputSignal?.payload ?? null,\n outputPayload: outputCol ? resp.outputSignal?.payload ?? null : null,\n error: resp.error != null ? String(resp.error) : null,\n at: Date.now(),\n };\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, progress, workOptions } = options;\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 const startedAt = Date.now();\n let hopCount = 0;\n\n await progress?.onStarted?.({\n jobId: job.id,\n collateralName,\n payload,\n status: 'running',\n startedAt,\n hopCount: 0,\n });\n\n const stimulation = cns.stimulate(collateral.createSignal(payload), {\n maxNeuronHops,\n onResponse: async (resp: ICNSResponseLike) => {\n const rec = toHopRecord(\n job.id,\n hopCount,\n resp,\n cns,\n registry\n );\n hopCount += 1;\n // Returning a Promise makes CNStra wait for the durable write\n // before enqueuing the next hop (per-hop checkpoint barrier).\n await progress?.onHop?.(rec);\n },\n });\n\n try {\n await stimulation.waitUntilComplete();\n await progress?.onCompleted?.({\n jobId: job.id,\n collateralName,\n payload,\n status: 'completed',\n startedAt,\n completedAt: Date.now(),\n hopCount,\n });\n } catch (err) {\n await progress?.onCompleted?.({\n jobId: job.id,\n collateralName,\n payload,\n status: 'failed',\n startedAt,\n completedAt: Date.now(),\n hopCount,\n error: err instanceof Error ? err.message : String(err),\n });\n // Let pg-boss decide about retries/backoff.\n throw err;\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, CNSPersistOptionsRegistry } from '@cnstra/core';\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":"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","names":["_a"]}
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"]}
@@ -0,0 +1,75 @@
1
+ import { Pool } from 'pg';
2
+ import { ICNSProgressRepository } from '@cnstra/persist';
3
+ import { TCNSProgress } from '@cnstra/persist-dto';
4
+
5
+ /**
6
+ * Postgres-backed resume-progress store for pg-boss, keyed by the pg-boss **job id**.
7
+ *
8
+ * In pg-boss a run = stimulation = job (one entity), so the checkpoint is keyed by
9
+ * `job.id`; there is no separate run store. pg-boss can't hold arbitrary per-hop
10
+ * progress on its job row (its `Job` has data/output/signal but no mutable progress
11
+ * field), so the frontier lives in its own table (`cns_pgboss_progress`, default) on
12
+ * the same Postgres. One upserted row per key; dropped when the job completes.
13
+ *
14
+ * OPTIONAL: this is an opt-in export (`@cnstra/pg-boss/postgres-progress`) — importing
15
+ * `@cnstra/pg-boss` (worker/enqueue) does NOT pull in `pg`. It's just one
16
+ * `ICNSProgressRepository`; the progress store is independent of the broker, so you can
17
+ * run pg-boss with `@cnstra/progress-redis` (or any impl) instead.
18
+ */
19
+
20
+ interface TCNSPgBossProgressRepositoryOptions {
21
+ /** Connection string (a Pool is created and owned by the repo). */
22
+ connectionString?: string;
23
+ /** Or inject an existing pg Pool (not closed by `close()`). */
24
+ pool?: Pool;
25
+ /** Table name (default `cns_pgboss_progress`). */
26
+ table?: string;
27
+ /** Lazily `ensureSchema` on first use (default true). Set false to manage migrations yourself. */
28
+ autoMigrate?: boolean;
29
+ }
30
+ declare class CNSPgBossProgressRepository implements ICNSProgressRepository {
31
+ private readonly pool;
32
+ private readonly ownsPool;
33
+ private readonly table;
34
+ private readonly autoMigrate;
35
+ private ready;
36
+ constructor(opts: TCNSPgBossProgressRepositoryOptions);
37
+ private ensure;
38
+ save(key: string, progress: TCNSProgress): Promise<void>;
39
+ load(key: string): Promise<TCNSProgress | undefined>;
40
+ delete(key: string): Promise<void>;
41
+ /**
42
+ * Prune rows not touched for `olderThanMs` — the safety net for jobs that failed
43
+ * PERMANENTLY (retries exhausted): their frontier row is never `delete`d (pg-boss
44
+ * has no terminal-failure hook), so it would leak. A still-retrying job keeps its
45
+ * `updated_at` fresh, so choose a threshold comfortably larger than your longest
46
+ * retry backoff window and run this on a schedule. Returns the number of rows removed.
47
+ */
48
+ deleteStale(olderThanMs: number): Promise<number>;
49
+ /** Close the owned pool (no-op when a pool was injected). */
50
+ close(): Promise<void>;
51
+ }
52
+
53
+ /**
54
+ * Schema / migrations for the pg-boss progress table (`cns_pgboss_progress`).
55
+ *
56
+ * The package OWNS this schema so you can manage it explicitly (a migration step in
57
+ * your own tooling) instead of relying on the repository's lazy auto-create. Run it
58
+ * via the `cnstra-pgboss-progress migrate` CLI, or call `ensureSchema(db)` yourself
59
+ * with any pg client/pool.
60
+ */
61
+ /** Minimal pg client surface (a `pg` Pool or Client satisfies it). */
62
+ interface IPgQueryable {
63
+ query(text: string, params?: unknown[]): Promise<{
64
+ rows: unknown[];
65
+ }>;
66
+ }
67
+ declare const DEFAULT_TABLE = "cns_pgboss_progress";
68
+ /** Guard the table identifier (it is interpolated into DDL/DML, not parameterised). */
69
+ declare function assertSafeTable(table: string): string;
70
+ /** The CREATE TABLE for the progress store. One upserted row per key (job id). */
71
+ declare function pgBossProgressSchemaSql(table?: string): string;
72
+ /** Idempotently create the progress table. Safe to run repeatedly. */
73
+ declare function ensureSchema(db: IPgQueryable, table?: string): Promise<void>;
74
+
75
+ export { CNSPgBossProgressRepository, DEFAULT_TABLE, type IPgQueryable, type TCNSPgBossProgressRepositoryOptions, assertSafeTable, ensureSchema, pgBossProgressSchemaSql };
@@ -0,0 +1,75 @@
1
+ import { Pool } from 'pg';
2
+ import { ICNSProgressRepository } from '@cnstra/persist';
3
+ import { TCNSProgress } from '@cnstra/persist-dto';
4
+
5
+ /**
6
+ * Postgres-backed resume-progress store for pg-boss, keyed by the pg-boss **job id**.
7
+ *
8
+ * In pg-boss a run = stimulation = job (one entity), so the checkpoint is keyed by
9
+ * `job.id`; there is no separate run store. pg-boss can't hold arbitrary per-hop
10
+ * progress on its job row (its `Job` has data/output/signal but no mutable progress
11
+ * field), so the frontier lives in its own table (`cns_pgboss_progress`, default) on
12
+ * the same Postgres. One upserted row per key; dropped when the job completes.
13
+ *
14
+ * OPTIONAL: this is an opt-in export (`@cnstra/pg-boss/postgres-progress`) — importing
15
+ * `@cnstra/pg-boss` (worker/enqueue) does NOT pull in `pg`. It's just one
16
+ * `ICNSProgressRepository`; the progress store is independent of the broker, so you can
17
+ * run pg-boss with `@cnstra/progress-redis` (or any impl) instead.
18
+ */
19
+
20
+ interface TCNSPgBossProgressRepositoryOptions {
21
+ /** Connection string (a Pool is created and owned by the repo). */
22
+ connectionString?: string;
23
+ /** Or inject an existing pg Pool (not closed by `close()`). */
24
+ pool?: Pool;
25
+ /** Table name (default `cns_pgboss_progress`). */
26
+ table?: string;
27
+ /** Lazily `ensureSchema` on first use (default true). Set false to manage migrations yourself. */
28
+ autoMigrate?: boolean;
29
+ }
30
+ declare class CNSPgBossProgressRepository implements ICNSProgressRepository {
31
+ private readonly pool;
32
+ private readonly ownsPool;
33
+ private readonly table;
34
+ private readonly autoMigrate;
35
+ private ready;
36
+ constructor(opts: TCNSPgBossProgressRepositoryOptions);
37
+ private ensure;
38
+ save(key: string, progress: TCNSProgress): Promise<void>;
39
+ load(key: string): Promise<TCNSProgress | undefined>;
40
+ delete(key: string): Promise<void>;
41
+ /**
42
+ * Prune rows not touched for `olderThanMs` — the safety net for jobs that failed
43
+ * PERMANENTLY (retries exhausted): their frontier row is never `delete`d (pg-boss
44
+ * has no terminal-failure hook), so it would leak. A still-retrying job keeps its
45
+ * `updated_at` fresh, so choose a threshold comfortably larger than your longest
46
+ * retry backoff window and run this on a schedule. Returns the number of rows removed.
47
+ */
48
+ deleteStale(olderThanMs: number): Promise<number>;
49
+ /** Close the owned pool (no-op when a pool was injected). */
50
+ close(): Promise<void>;
51
+ }
52
+
53
+ /**
54
+ * Schema / migrations for the pg-boss progress table (`cns_pgboss_progress`).
55
+ *
56
+ * The package OWNS this schema so you can manage it explicitly (a migration step in
57
+ * your own tooling) instead of relying on the repository's lazy auto-create. Run it
58
+ * via the `cnstra-pgboss-progress migrate` CLI, or call `ensureSchema(db)` yourself
59
+ * with any pg client/pool.
60
+ */
61
+ /** Minimal pg client surface (a `pg` Pool or Client satisfies it). */
62
+ interface IPgQueryable {
63
+ query(text: string, params?: unknown[]): Promise<{
64
+ rows: unknown[];
65
+ }>;
66
+ }
67
+ declare const DEFAULT_TABLE = "cns_pgboss_progress";
68
+ /** Guard the table identifier (it is interpolated into DDL/DML, not parameterised). */
69
+ declare function assertSafeTable(table: string): string;
70
+ /** The CREATE TABLE for the progress store. One upserted row per key (job id). */
71
+ declare function pgBossProgressSchemaSql(table?: string): string;
72
+ /** Idempotently create the progress table. Safe to run repeatedly. */
73
+ declare function ensureSchema(db: IPgQueryable, table?: string): Promise<void>;
74
+
75
+ export { CNSPgBossProgressRepository, DEFAULT_TABLE, type IPgQueryable, type TCNSPgBossProgressRepositoryOptions, assertSafeTable, ensureSchema, pgBossProgressSchemaSql };
@@ -0,0 +1,142 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
21
+
22
+ // src/postgres-progress.ts
23
+ var postgres_progress_exports = {};
24
+ __export(postgres_progress_exports, {
25
+ CNSPgBossProgressRepository: () => CNSPgBossProgressRepository,
26
+ DEFAULT_TABLE: () => DEFAULT_TABLE,
27
+ assertSafeTable: () => assertSafeTable,
28
+ ensureSchema: () => ensureSchema,
29
+ pgBossProgressSchemaSql: () => pgBossProgressSchemaSql
30
+ });
31
+ module.exports = __toCommonJS(postgres_progress_exports);
32
+
33
+ // src/CNSPgBossProgressRepository.ts
34
+ var import_pg = require("pg");
35
+
36
+ // src/migrate.ts
37
+ var DEFAULT_TABLE = "cns_pgboss_progress";
38
+ function assertSafeTable(table) {
39
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) {
40
+ throw new Error(`[@cnstra/pg-boss] unsafe progress table name: ${JSON.stringify(table)}`);
41
+ }
42
+ return table;
43
+ }
44
+ __name(assertSafeTable, "assertSafeTable");
45
+ function pgBossProgressSchemaSql(table = DEFAULT_TABLE) {
46
+ assertSafeTable(table);
47
+ return `CREATE TABLE IF NOT EXISTS ${table} (
48
+ key text PRIMARY KEY,
49
+ progress jsonb NOT NULL,
50
+ updated_at timestamptz NOT NULL DEFAULT now()
51
+ );
52
+ CREATE INDEX IF NOT EXISTS ${table}_updated_idx ON ${table} (updated_at)`;
53
+ }
54
+ __name(pgBossProgressSchemaSql, "pgBossProgressSchemaSql");
55
+ async function ensureSchema(db, table = DEFAULT_TABLE) {
56
+ await db.query(pgBossProgressSchemaSql(table));
57
+ }
58
+ __name(ensureSchema, "ensureSchema");
59
+
60
+ // src/CNSPgBossProgressRepository.ts
61
+ var _CNSPgBossProgressRepository = class _CNSPgBossProgressRepository {
62
+ constructor(opts) {
63
+ __publicField(this, "pool");
64
+ __publicField(this, "ownsPool");
65
+ __publicField(this, "table");
66
+ __publicField(this, "autoMigrate");
67
+ __publicField(this, "ready");
68
+ var _a, _b;
69
+ if (opts.pool) {
70
+ this.pool = opts.pool;
71
+ this.ownsPool = false;
72
+ } else if (opts.connectionString) {
73
+ this.pool = new import_pg.Pool({
74
+ connectionString: opts.connectionString
75
+ });
76
+ this.ownsPool = true;
77
+ } else {
78
+ throw new Error("[@cnstra/pg-boss] CNSPgBossProgressRepository needs `connectionString` or `pool`");
79
+ }
80
+ this.table = assertSafeTable((_a = opts.table) != null ? _a : DEFAULT_TABLE);
81
+ this.autoMigrate = (_b = opts.autoMigrate) != null ? _b : true;
82
+ }
83
+ ensure() {
84
+ var _a;
85
+ if (!this.autoMigrate) return Promise.resolve();
86
+ return (_a = this.ready) != null ? _a : this.ready = ensureSchema(this.pool, this.table);
87
+ }
88
+ async save(key, progress) {
89
+ await this.ensure();
90
+ await this.pool.query(`INSERT INTO ${this.table} (key, progress, updated_at)
91
+ VALUES ($1, $2::jsonb, now())
92
+ ON CONFLICT (key) DO UPDATE SET progress = EXCLUDED.progress, updated_at = now()`, [
93
+ key,
94
+ JSON.stringify(progress)
95
+ ]);
96
+ }
97
+ async load(key) {
98
+ var _a;
99
+ await this.ensure();
100
+ const res = await this.pool.query(`SELECT progress FROM ${this.table} WHERE key = $1`, [
101
+ key
102
+ ]);
103
+ return (_a = res.rows[0]) == null ? void 0 : _a.progress;
104
+ }
105
+ async delete(key) {
106
+ await this.ensure();
107
+ await this.pool.query(`DELETE FROM ${this.table} WHERE key = $1`, [
108
+ key
109
+ ]);
110
+ }
111
+ /**
112
+ * Prune rows not touched for `olderThanMs` — the safety net for jobs that failed
113
+ * PERMANENTLY (retries exhausted): their frontier row is never `delete`d (pg-boss
114
+ * has no terminal-failure hook), so it would leak. A still-retrying job keeps its
115
+ * `updated_at` fresh, so choose a threshold comfortably larger than your longest
116
+ * retry backoff window and run this on a schedule. Returns the number of rows removed.
117
+ */
118
+ async deleteStale(olderThanMs) {
119
+ var _a;
120
+ await this.ensure();
121
+ const res = await this.pool.query(`DELETE FROM ${this.table}
122
+ WHERE updated_at < now() - ($1::bigint * interval '1 millisecond')`, [
123
+ Math.max(0, Math.floor(olderThanMs))
124
+ ]);
125
+ return (_a = res.rowCount) != null ? _a : 0;
126
+ }
127
+ /** Close the owned pool (no-op when a pool was injected). */
128
+ async close() {
129
+ if (this.ownsPool) await this.pool.end();
130
+ }
131
+ };
132
+ __name(_CNSPgBossProgressRepository, "CNSPgBossProgressRepository");
133
+ var CNSPgBossProgressRepository = _CNSPgBossProgressRepository;
134
+ // Annotate the CommonJS export names for ESM import in node:
135
+ 0 && (module.exports = {
136
+ CNSPgBossProgressRepository,
137
+ DEFAULT_TABLE,
138
+ assertSafeTable,
139
+ ensureSchema,
140
+ pgBossProgressSchemaSql
141
+ });
142
+ //# sourceMappingURL=postgres-progress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/postgres-progress.ts","../src/CNSPgBossProgressRepository.ts","../src/migrate.ts"],"sourcesContent":["/**\n * Opt-in Postgres progress store for pg-boss — import from\n * `@cnstra/pg-boss/postgres-progress` (this subpath pulls in `pg`; the main\n * `@cnstra/pg-boss` entry does not).\n */\nexport {\n CNSPgBossProgressRepository,\n type TCNSPgBossProgressRepositoryOptions,\n} from './CNSPgBossProgressRepository';\nexport {\n ensureSchema,\n pgBossProgressSchemaSql,\n assertSafeTable,\n DEFAULT_TABLE,\n type IPgQueryable,\n} from './migrate';\n","/**\n * Postgres-backed resume-progress store for pg-boss, keyed by the pg-boss **job id**.\n *\n * In pg-boss a run = stimulation = job (one entity), so the checkpoint is keyed by\n * `job.id`; there is no separate run store. pg-boss can't hold arbitrary per-hop\n * progress on its job row (its `Job` has data/output/signal but no mutable progress\n * field), so the frontier lives in its own table (`cns_pgboss_progress`, default) on\n * the same Postgres. One upserted row per key; dropped when the job completes.\n *\n * OPTIONAL: this is an opt-in export (`@cnstra/pg-boss/postgres-progress`) — importing\n * `@cnstra/pg-boss` (worker/enqueue) does NOT pull in `pg`. It's just one\n * `ICNSProgressRepository`; the progress store is independent of the broker, so you can\n * run pg-boss with `@cnstra/progress-redis` (or any impl) instead.\n */\nimport { Pool } from 'pg';\nimport type { ICNSProgressRepository } from '@cnstra/persist';\nimport type { TCNSProgress } from '@cnstra/persist-dto';\nimport { DEFAULT_TABLE, assertSafeTable, ensureSchema } from './migrate';\n\nexport interface TCNSPgBossProgressRepositoryOptions {\n /** Connection string (a Pool is created and owned by the repo). */\n connectionString?: string;\n /** Or inject an existing pg Pool (not closed by `close()`). */\n pool?: Pool;\n /** Table name (default `cns_pgboss_progress`). */\n table?: string;\n /** Lazily `ensureSchema` on first use (default true). Set false to manage migrations yourself. */\n autoMigrate?: boolean;\n}\n\nexport class CNSPgBossProgressRepository implements ICNSProgressRepository {\n private readonly pool: Pool;\n private readonly ownsPool: boolean;\n private readonly table: string;\n private readonly autoMigrate: boolean;\n private ready: Promise<void> | undefined;\n\n constructor(opts: TCNSPgBossProgressRepositoryOptions) {\n if (opts.pool) {\n this.pool = opts.pool;\n this.ownsPool = false;\n } else if (opts.connectionString) {\n this.pool = new Pool({ connectionString: opts.connectionString });\n this.ownsPool = true;\n } else {\n throw new Error(\n '[@cnstra/pg-boss] CNSPgBossProgressRepository needs `connectionString` or `pool`'\n );\n }\n this.table = assertSafeTable(opts.table ?? DEFAULT_TABLE);\n this.autoMigrate = opts.autoMigrate ?? true;\n }\n\n private ensure(): Promise<void> {\n if (!this.autoMigrate) return Promise.resolve();\n return (this.ready ??= ensureSchema(this.pool, this.table));\n }\n\n async save(key: string, progress: TCNSProgress): Promise<void> {\n await this.ensure();\n await this.pool.query(\n `INSERT INTO ${this.table} (key, progress, updated_at)\n VALUES ($1, $2::jsonb, now())\n ON CONFLICT (key) DO UPDATE SET progress = EXCLUDED.progress, updated_at = now()`,\n [key, JSON.stringify(progress)]\n );\n }\n\n async load(key: string): Promise<TCNSProgress | undefined> {\n await this.ensure();\n const res = await this.pool.query<{ progress: TCNSProgress }>(\n `SELECT progress FROM ${this.table} WHERE key = $1`,\n [key]\n );\n return res.rows[0]?.progress;\n }\n\n async delete(key: string): Promise<void> {\n await this.ensure();\n await this.pool.query(`DELETE FROM ${this.table} WHERE key = $1`, [key]);\n }\n\n /**\n * Prune rows not touched for `olderThanMs` — the safety net for jobs that failed\n * PERMANENTLY (retries exhausted): their frontier row is never `delete`d (pg-boss\n * has no terminal-failure hook), so it would leak. A still-retrying job keeps its\n * `updated_at` fresh, so choose a threshold comfortably larger than your longest\n * retry backoff window and run this on a schedule. Returns the number of rows removed.\n */\n async deleteStale(olderThanMs: number): Promise<number> {\n await this.ensure();\n const res = await this.pool.query(\n `DELETE FROM ${this.table}\n WHERE updated_at < now() - ($1::bigint * interval '1 millisecond')`,\n [Math.max(0, Math.floor(olderThanMs))]\n );\n return res.rowCount ?? 0;\n }\n\n /** Close the owned pool (no-op when a pool was injected). */\n async close(): Promise<void> {\n if (this.ownsPool) await this.pool.end();\n }\n}\n","/**\n * Schema / migrations for the pg-boss progress table (`cns_pgboss_progress`).\n *\n * The package OWNS this schema so you can manage it explicitly (a migration step in\n * your own tooling) instead of relying on the repository's lazy auto-create. Run it\n * via the `cnstra-pgboss-progress migrate` CLI, or call `ensureSchema(db)` yourself\n * with any pg client/pool.\n */\n\n/** Minimal pg client surface (a `pg` Pool or Client satisfies it). */\nexport interface IPgQueryable {\n query(text: string, params?: unknown[]): Promise<{ rows: unknown[] }>;\n}\n\nexport const DEFAULT_TABLE = 'cns_pgboss_progress';\n\n/** Guard the table identifier (it is interpolated into DDL/DML, not parameterised). */\nexport function assertSafeTable(table: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) {\n throw new Error(\n `[@cnstra/pg-boss] unsafe progress table name: ${JSON.stringify(table)}`\n );\n }\n return table;\n}\n\n/** The CREATE TABLE for the progress store. One upserted row per key (job id). */\nexport function pgBossProgressSchemaSql(table: string = DEFAULT_TABLE): string {\n assertSafeTable(table);\n // `updated_at` + its index back `deleteStale()`: a row is deleted when its job\n // succeeds, but a job that fails PERMANENTLY (retries exhausted) leaves its row —\n // pg-boss has no terminal-failure hook to key off. Pruning by staleness collects\n // those, and the index keeps the sweep cheap.\n return `CREATE TABLE IF NOT EXISTS ${table} (\n key text PRIMARY KEY,\n progress jsonb NOT NULL,\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nCREATE INDEX IF NOT EXISTS ${table}_updated_idx ON ${table} (updated_at)`;\n}\n\n/** Idempotently create the progress table. Safe to run repeatedly. */\nexport async function ensureSchema(\n db: IPgQueryable,\n table: string = DEFAULT_TABLE\n): Promise<void> {\n await db.query(pgBossProgressSchemaSql(table));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;ACcA,gBAAqB;;;ACAd,IAAMA,gBAAgB;AAGtB,SAASC,gBAAgBC,OAAa;AACzC,MAAI,CAAC,2BAA2BC,KAAKD,KAAAA,GAAQ;AACzC,UAAM,IAAIE,MACN,iDAAiDC,KAAKC,UAAUJ,KAAAA,CAAAA,EAAQ;EAEhF;AACA,SAAOA;AACX;AAPgBD;AAUT,SAASM,wBAAwBL,QAAgBF,eAAa;AACjEC,kBAAgBC,KAAAA;AAKhB,SAAO,8BAA8BA,KAAAA;;;;;6BAKZA,KAAAA,mBAAwBA,KAAAA;AACrD;AAZgBK;AAehB,eAAsBC,aAClBC,IACAP,QAAgBF,eAAa;AAE7B,QAAMS,GAAGC,MAAMH,wBAAwBL,KAAAA,CAAAA;AAC3C;AALsBM;;;ADZf,IAAMG,+BAAN,MAAMA,6BAAAA;EAOT,YAAYC,MAA2C;AANtCC;AACAC;AACAC;AACAC;AACTC;AAnCZ;AAsCQ,QAAIL,KAAKC,MAAM;AACX,WAAKA,OAAOD,KAAKC;AACjB,WAAKC,WAAW;IACpB,WAAWF,KAAKM,kBAAkB;AAC9B,WAAKL,OAAO,IAAIM,eAAK;QAAED,kBAAkBN,KAAKM;MAAiB,CAAA;AAC/D,WAAKJ,WAAW;IACpB,OAAO;AACH,YAAM,IAAIM,MACN,kFAAA;IAER;AACA,SAAKL,QAAQM,iBAAgBT,UAAKG,UAALH,YAAcU,aAAAA;AAC3C,SAAKN,eAAcJ,UAAKI,gBAALJ,YAAoB;EAC3C;EAEQW,SAAwB;AArDpC;AAsDQ,QAAI,CAAC,KAAKP,YAAa,QAAOQ,QAAQC,QAAO;AAC7C,YAAQ,UAAKR,UAAL,iBAAKA,QAAUS,aAAa,KAAKb,MAAM,KAAKE,KAAK;EAC7D;EAEA,MAAMY,KAAKC,KAAaC,UAAuC;AAC3D,UAAM,KAAKN,OAAM;AACjB,UAAM,KAAKV,KAAKiB,MACZ,eAAe,KAAKf,KAAK;;gGAGzB;MAACa;MAAKG,KAAKC,UAAUH,QAAAA;KAAU;EAEvC;EAEA,MAAMI,KAAKL,KAAgD;AApE/D;AAqEQ,UAAM,KAAKL,OAAM;AACjB,UAAMW,MAAM,MAAM,KAAKrB,KAAKiB,MACxB,wBAAwB,KAAKf,KAAK,mBAClC;MAACa;KAAI;AAET,YAAOM,SAAIC,KAAK,CAAA,MAATD,mBAAaL;EACxB;EAEA,MAAMO,OAAOR,KAA4B;AACrC,UAAM,KAAKL,OAAM;AACjB,UAAM,KAAKV,KAAKiB,MAAM,eAAe,KAAKf,KAAK,mBAAmB;MAACa;KAAI;EAC3E;;;;;;;;EASA,MAAMS,YAAYC,aAAsC;AAzF5D;AA0FQ,UAAM,KAAKf,OAAM;AACjB,UAAMW,MAAM,MAAM,KAAKrB,KAAKiB,MACxB,eAAe,KAAKf,KAAK;kFAEzB;MAACwB,KAAKC,IAAI,GAAGD,KAAKE,MAAMH,WAAAA,CAAAA;KAAc;AAE1C,YAAOJ,SAAIQ,aAAJR,YAAgB;EAC3B;;EAGA,MAAMS,QAAuB;AACzB,QAAI,KAAK7B,SAAU,OAAM,KAAKD,KAAK+B,IAAG;EAC1C;AACJ;AAzEajC;AAAN,IAAMA,8BAAN;","names":["DEFAULT_TABLE","assertSafeTable","table","test","Error","JSON","stringify","pgBossProgressSchemaSql","ensureSchema","db","query","CNSPgBossProgressRepository","opts","pool","ownsPool","table","autoMigrate","ready","connectionString","Pool","Error","assertSafeTable","DEFAULT_TABLE","ensure","Promise","resolve","ensureSchema","save","key","progress","query","JSON","stringify","load","res","rows","delete","deleteStale","olderThanMs","Math","max","floor","rowCount","close","end"]}