@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/README.md CHANGED
@@ -35,13 +35,10 @@ await createCNSWorker({
35
35
  cns,
36
36
  registry,
37
37
  queue: 'cns',
38
- // Optional: durably record every step. If onHop returns a Promise, CNStra
39
- // waits for it before running the next hop (per-hop checkpoint barrier).
40
- progress: {
41
- onStarted: (s) => db.insertStimulation(s),
42
- onHop: (h) => db.insertHop(h), // unique(stimulation_id, index) → idempotent
43
- onCompleted: (s) => db.finishStimulation(s),
44
- },
38
+ // Optional: record the name-based Stimulation → Attempt → Task history (the SAME
39
+ // model the DevTools use) into any ICNSStimulationRepository. In-memory just to
40
+ // *see* it; Postgres to keep it. One job = one stimulation; each retry = an attempt.
41
+ // observe: new CNSInMemoryStimulationRepository(),
45
42
  });
46
43
  ```
47
44
 
@@ -66,18 +63,50 @@ await enqueueStimulation(boss, 'cns', job, {
66
63
 
67
64
  ## Retries & resume
68
65
 
69
- - **Cross-process (recommended):** a retried job re-runs the whole flow from the entry signal.
70
- Make neurons **idempotent**, or deduplicate hops with `unique(stimulation_id, index)`.
71
- - **In-process partial resume:** `cns.activate(getFailedTasks())` works only within one process
72
- (it holds live object references) — it is not portable through the queue.
66
+ pg-boss reuses the **same job id** across a job's retries, so with `resume` the worker loads the
67
+ outstanding frontier and continues via `cns.activate(...)` — only the failed branch re-runs, with
68
+ its context. In pg-boss a run = stimulation = job, so the checkpoint is keyed by **job id**.
69
+
70
+ The checkpoint store is **pluggable** — three ways:
71
+
72
+ ```ts
73
+ // 1) In-package Postgres table (cns_pgboss_progress) — opt-in subpath, pulls in `pg`
74
+ import { CNSPgBossProgressRepository } from '@cnstra/pg-boss/postgres-progress';
75
+ resume: { repository: new CNSPgBossProgressRepository({ connectionString: DATABASE_URL }) }
76
+
77
+ // 2) Redis, or any other ICNSProgressRepository — the store is independent of the broker
78
+ import { CNSRedisProgressRepository } from '@cnstra/progress-redis';
79
+ resume: { repository: new CNSRedisProgressRepository({ connection }) }
80
+
81
+ // 3) No resume at all → thin mode: a retried job re-runs the WHOLE flow from the entry signal.
82
+ // (omit `resume`) — make neurons idempotent.
83
+ ```
84
+
85
+ pg-boss's `Job` has no mutable native progress field (unlike BullMQ), so a checkpoint needs a store.
86
+
87
+ **Migrations:** the Postgres table is owned by this package. Auto-created on first use, or manage it
88
+ yourself: `DATABASE_URL=… npx cnstra-pgboss-progress migrate` (or call `ensureSchema(pool)` from
89
+ `@cnstra/pg-boss/postgres-progress`).
90
+
91
+ **Row lifecycle / cleanup:** one upserted row per `job.id`. The worker **deletes it when the job
92
+ succeeds**. A job that fails *permanently* (retries exhausted) keeps its row — pg-boss has no
93
+ terminal-failure hook to key off — so prune those periodically by staleness (a still-retrying job
94
+ keeps `updated_at` fresh, so pick a threshold well above your longest retry window):
95
+
96
+ ```ts
97
+ const store = new CNSPgBossProgressRepository({ connectionString: DATABASE_URL });
98
+ // e.g. in a daily cron: drop checkpoints untouched for over a day
99
+ await store.deleteStale(24 * 60 * 60 * 1000);
100
+ ```
73
101
 
74
102
  ## What this package does / doesn't
75
103
 
76
104
  - ✅ Job execution, retry/backoff, scheduling, singleton, concurrency → **pg-boss**.
77
- - ✅ Per-step (hop) progress records → the `progress` sink (your tables / OIMDB / logs).
78
- - ❌ No built-in Postgres schema or admin UI — you own the store. pg-boss has **no per-step
79
- progress** of its own, so step history is always your tables. See the docs for a schema and
80
- an admin-panel design.
105
+ - ✅ Durable frontier **resume** across retries → a pluggable `resume.repository` (in-package
106
+ Postgres, `@cnstra/progress-redis`, or your own); omit it for plain re-run-from-entry.
107
+ - ✅ Name-based run/attempt/task **observability** → an optional `observe` store (in-memory to
108
+ just see it, Postgres to keep it — the same model the DevTools render).
109
+ - ❌ No built-in admin UI — see `@cnstra/examples` (`demo:admin`) for the CNStra retry admin.
81
110
 
82
111
  See the full guide: https://cnstra.org/docs/integrations/pg-boss
83
112
 
@@ -0,0 +1,10 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
+
6
+ export {
7
+ __name,
8
+ __publicField
9
+ };
10
+ //# sourceMappingURL=chunk-2JTKI4GS.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,35 @@
1
+ import {
2
+ __name
3
+ } from "./chunk-2JTKI4GS.mjs";
4
+
5
+ // src/migrate.ts
6
+ var DEFAULT_TABLE = "cns_pgboss_progress";
7
+ function assertSafeTable(table) {
8
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) {
9
+ throw new Error(`[@cnstra/pg-boss] unsafe progress table name: ${JSON.stringify(table)}`);
10
+ }
11
+ return table;
12
+ }
13
+ __name(assertSafeTable, "assertSafeTable");
14
+ function pgBossProgressSchemaSql(table = DEFAULT_TABLE) {
15
+ assertSafeTable(table);
16
+ return `CREATE TABLE IF NOT EXISTS ${table} (
17
+ key text PRIMARY KEY,
18
+ progress jsonb NOT NULL,
19
+ updated_at timestamptz NOT NULL DEFAULT now()
20
+ );
21
+ CREATE INDEX IF NOT EXISTS ${table}_updated_idx ON ${table} (updated_at)`;
22
+ }
23
+ __name(pgBossProgressSchemaSql, "pgBossProgressSchemaSql");
24
+ async function ensureSchema(db, table = DEFAULT_TABLE) {
25
+ await db.query(pgBossProgressSchemaSql(table));
26
+ }
27
+ __name(ensureSchema, "ensureSchema");
28
+
29
+ export {
30
+ DEFAULT_TABLE,
31
+ assertSafeTable,
32
+ pgBossProgressSchemaSql,
33
+ ensureSchema
34
+ };
35
+ //# sourceMappingURL=chunk-XJFC72LU.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/migrate.ts"],"sourcesContent":["/**\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":";;;;;AAcO,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;","names":["DEFAULT_TABLE","assertSafeTable","table","test","Error","JSON","stringify","pgBossProgressSchemaSql","ensureSchema","db","query"]}
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+
5
+ // src/cli.ts
6
+ var import_pg = require("pg");
7
+
8
+ // src/migrate.ts
9
+ var DEFAULT_TABLE = "cns_pgboss_progress";
10
+ function assertSafeTable(table) {
11
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) {
12
+ throw new Error(`[@cnstra/pg-boss] unsafe progress table name: ${JSON.stringify(table)}`);
13
+ }
14
+ return table;
15
+ }
16
+ __name(assertSafeTable, "assertSafeTable");
17
+ function pgBossProgressSchemaSql(table = DEFAULT_TABLE) {
18
+ assertSafeTable(table);
19
+ return `CREATE TABLE IF NOT EXISTS ${table} (
20
+ key text PRIMARY KEY,
21
+ progress jsonb NOT NULL,
22
+ updated_at timestamptz NOT NULL DEFAULT now()
23
+ );
24
+ CREATE INDEX IF NOT EXISTS ${table}_updated_idx ON ${table} (updated_at)`;
25
+ }
26
+ __name(pgBossProgressSchemaSql, "pgBossProgressSchemaSql");
27
+ async function ensureSchema(db, table = DEFAULT_TABLE) {
28
+ await db.query(pgBossProgressSchemaSql(table));
29
+ }
30
+ __name(ensureSchema, "ensureSchema");
31
+
32
+ // src/cli.ts
33
+ async function main() {
34
+ var _a;
35
+ const cmd = process.argv[2];
36
+ if (cmd !== "migrate") {
37
+ console.error("usage: cnstra-pgboss-progress migrate");
38
+ process.exit(1);
39
+ }
40
+ const url = process.env.DATABASE_URL;
41
+ if (!url) {
42
+ console.error("DATABASE_URL is required");
43
+ process.exit(1);
44
+ }
45
+ const table = (_a = process.env.CNS_PGBOSS_PROGRESS_TABLE) != null ? _a : DEFAULT_TABLE;
46
+ const pool = new import_pg.Pool({
47
+ connectionString: url
48
+ });
49
+ try {
50
+ await ensureSchema(pool, table);
51
+ console.log(`\u2713 ensured progress table "${table}"`);
52
+ } finally {
53
+ await pool.end();
54
+ }
55
+ }
56
+ __name(main, "main");
57
+ void main().catch((e) => {
58
+ console.error(e);
59
+ process.exit(1);
60
+ });
61
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/migrate.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `cnstra-pgboss-progress migrate` — create the pg-boss progress table.\n *\n * DATABASE_URL=postgres://... cnstra-pgboss-progress migrate\n * CNS_PGBOSS_PROGRESS_TABLE=my_table (optional; default cns_pgboss_progress)\n */\nimport { Pool } from 'pg';\nimport { ensureSchema, DEFAULT_TABLE } from './migrate';\n\nasync function main(): Promise<void> {\n const cmd = process.argv[2];\n if (cmd !== 'migrate') {\n console.error('usage: cnstra-pgboss-progress migrate'); // eslint-disable-line no-console\n process.exit(1);\n }\n const url = process.env.DATABASE_URL;\n if (!url) {\n console.error('DATABASE_URL is required'); // eslint-disable-line no-console\n process.exit(1);\n }\n const table = process.env.CNS_PGBOSS_PROGRESS_TABLE ?? DEFAULT_TABLE;\n const pool = new Pool({ connectionString: url });\n try {\n await ensureSchema(pool, table);\n // eslint-disable-next-line no-console\n console.log(`✓ ensured progress table \"${table}\"`);\n } finally {\n await pool.end();\n }\n}\n\nvoid main().catch(e => {\n console.error(e); // eslint-disable-line no-console\n process.exit(1);\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":";;;;;AAOA,gBAAqB;;;ACOd,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;;;ADhCtB,eAAeG,OAAAA;;AACX,QAAMC,MAAMC,QAAQC,KAAK,CAAA;AACzB,MAAIF,QAAQ,WAAW;AACnBG,YAAQC,MAAM,uCAAA;AACdH,YAAQI,KAAK,CAAA;EACjB;AACA,QAAMC,MAAML,QAAQM,IAAIC;AACxB,MAAI,CAACF,KAAK;AACNH,YAAQC,MAAM,0BAAA;AACdH,YAAQI,KAAK,CAAA;EACjB;AACA,QAAMI,SAAQR,aAAQM,IAAIG,8BAAZT,YAAyCU;AACvD,QAAMC,OAAO,IAAIC,eAAK;IAAEC,kBAAkBR;EAAI,CAAA;AAC9C,MAAI;AACA,UAAMS,aAAaH,MAAMH,KAAAA;AAEzBN,YAAQa,IAAI,kCAA6BP,KAAAA,GAAQ;EACrD,UAAA;AACI,UAAMG,KAAKK,IAAG;EAClB;AACJ;AApBelB;AAsBf,KAAKA,KAAAA,EAAOmB,MAAMC,CAAAA,MAAAA;AACdhB,UAAQC,MAAMe,CAAAA;AACdlB,UAAQI,KAAK,CAAA;AACjB,CAAA;","names":["DEFAULT_TABLE","assertSafeTable","table","test","Error","JSON","stringify","pgBossProgressSchemaSql","ensureSchema","db","query","main","cmd","process","argv","console","error","exit","url","env","DATABASE_URL","table","CNS_PGBOSS_PROGRESS_TABLE","DEFAULT_TABLE","pool","Pool","connectionString","ensureSchema","log","end","catch","e"]}
package/dist/cli.mjs ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_TABLE,
4
+ ensureSchema
5
+ } from "./chunk-XJFC72LU.mjs";
6
+ import {
7
+ __name
8
+ } from "./chunk-2JTKI4GS.mjs";
9
+
10
+ // src/cli.ts
11
+ import { Pool } from "pg";
12
+ async function main() {
13
+ var _a;
14
+ const cmd = process.argv[2];
15
+ if (cmd !== "migrate") {
16
+ console.error("usage: cnstra-pgboss-progress migrate");
17
+ process.exit(1);
18
+ }
19
+ const url = process.env.DATABASE_URL;
20
+ if (!url) {
21
+ console.error("DATABASE_URL is required");
22
+ process.exit(1);
23
+ }
24
+ const table = (_a = process.env.CNS_PGBOSS_PROGRESS_TABLE) != null ? _a : DEFAULT_TABLE;
25
+ const pool = new Pool({
26
+ connectionString: url
27
+ });
28
+ try {
29
+ await ensureSchema(pool, table);
30
+ console.log(`\u2713 ensured progress table "${table}"`);
31
+ } finally {
32
+ await pool.end();
33
+ }
34
+ }
35
+ __name(main, "main");
36
+ void main().catch((e) => {
37
+ console.error(e);
38
+ process.exit(1);
39
+ });
40
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `cnstra-pgboss-progress migrate` — create the pg-boss progress table.\n *\n * DATABASE_URL=postgres://... cnstra-pgboss-progress migrate\n * CNS_PGBOSS_PROGRESS_TABLE=my_table (optional; default cns_pgboss_progress)\n */\nimport { Pool } from 'pg';\nimport { ensureSchema, DEFAULT_TABLE } from './migrate';\n\nasync function main(): Promise<void> {\n const cmd = process.argv[2];\n if (cmd !== 'migrate') {\n console.error('usage: cnstra-pgboss-progress migrate'); // eslint-disable-line no-console\n process.exit(1);\n }\n const url = process.env.DATABASE_URL;\n if (!url) {\n console.error('DATABASE_URL is required'); // eslint-disable-line no-console\n process.exit(1);\n }\n const table = process.env.CNS_PGBOSS_PROGRESS_TABLE ?? DEFAULT_TABLE;\n const pool = new Pool({ connectionString: url });\n try {\n await ensureSchema(pool, table);\n // eslint-disable-next-line no-console\n console.log(`✓ ensured progress table \"${table}\"`);\n } finally {\n await pool.end();\n }\n}\n\nvoid main().catch(e => {\n console.error(e); // eslint-disable-line no-console\n process.exit(1);\n});\n"],"mappings":";;;;;;;;;;AAOA,SAASA,YAAY;AAGrB,eAAeC,OAAAA;;AACX,QAAMC,MAAMC,QAAQC,KAAK,CAAA;AACzB,MAAIF,QAAQ,WAAW;AACnBG,YAAQC,MAAM,uCAAA;AACdH,YAAQI,KAAK,CAAA;EACjB;AACA,QAAMC,MAAML,QAAQM,IAAIC;AACxB,MAAI,CAACF,KAAK;AACNH,YAAQC,MAAM,0BAAA;AACdH,YAAQI,KAAK,CAAA;EACjB;AACA,QAAMI,SAAQR,aAAQM,IAAIG,8BAAZT,YAAyCU;AACvD,QAAMC,OAAO,IAAIC,KAAK;IAAEC,kBAAkBR;EAAI,CAAA;AAC9C,MAAI;AACA,UAAMS,aAAaH,MAAMH,KAAAA;AAEzBN,YAAQa,IAAI,kCAA6BP,KAAAA,GAAQ;EACrD,UAAA;AACI,UAAMG,KAAKK,IAAG;EAClB;AACJ;AApBelB;AAsBf,KAAKA,KAAAA,EAAOmB,MAAMC,CAAAA,MAAAA;AACdhB,UAAQC,MAAMe,CAAAA;AACdlB,UAAQI,KAAK,CAAA;AACjB,CAAA;","names":["Pool","main","cmd","process","argv","console","error","exit","url","env","DATABASE_URL","table","CNS_PGBOSS_PROGRESS_TABLE","DEFAULT_TABLE","pool","Pool","connectionString","ensureSchema","log","end","catch","e"]}
package/dist/index.d.mts CHANGED
@@ -1,4 +1,6 @@
1
- import { ICNS, CNSPersistOptionsRegistry, CNSCollateral } from '@cnstra/core';
1
+ import { CNSPersistOptionsRegistry, ICNSStimulationRepository, ICNSProgressRepository, ICNSProgressRecordingStrategy } from '@cnstra/persist';
2
+ import { ICNS } from '@cnstra/types';
3
+ import { CNSCollateral } from '@cnstra/core';
2
4
 
3
5
  /**
4
6
  * Minimal, structural view of the pg-boss surface this package touches.
@@ -12,6 +14,8 @@ interface IPgBossJob<TData = unknown> {
12
14
  id: string;
13
15
  name: string;
14
16
  data: TData;
17
+ /** 0-based retry count (pg-boss `Job.retryCount`); drives the attempt number. */
18
+ retryCount?: number;
15
19
  }
16
20
  interface IPgBossLike {
17
21
  send(name: string, data: object, options?: Record<string, unknown>): Promise<string | null>;
@@ -29,42 +33,6 @@ interface TCNSStimulationJobData {
29
33
  /** Optional per-run cap on neuron hops. */
30
34
  maxNeuronHops?: number;
31
35
  }
32
- /** One per-hop progress record (mirrors the devtools `CNSDTOHop` shape). */
33
- interface TCNSHopRecord {
34
- jobId: string;
35
- index: number;
36
- neuronName: string | null;
37
- inputCollateral: string | null;
38
- outputCollateral: string | null;
39
- inputPayload: unknown;
40
- outputPayload: unknown;
41
- error: string | null;
42
- at: number;
43
- }
44
- /** Lifecycle record for one stimulation (one job). */
45
- interface TCNSStimulationLifecycle {
46
- jobId: string;
47
- collateralName: string;
48
- payload: unknown;
49
- status: 'running' | 'completed' | 'failed';
50
- startedAt: number;
51
- completedAt?: number;
52
- hopCount: number;
53
- error?: string;
54
- }
55
- /**
56
- * Where progress is written (Postgres, OIMDB, logs, …).
57
- *
58
- * If `onHop` returns a Promise, CNStra waits for it **before enqueuing the next
59
- * hop** — i.e. each step is durably committed before the flow advances. This is
60
- * the per-hop checkpoint barrier that makes an admin/progress view truthful
61
- * across restarts.
62
- */
63
- interface ICNSProgressSink {
64
- onStarted?(rec: TCNSStimulationLifecycle): void | Promise<void>;
65
- onHop?(rec: TCNSHopRecord): void | Promise<void>;
66
- onCompleted?(rec: TCNSStimulationLifecycle): void | Promise<void>;
67
- }
68
36
  /** Structural view of the response passed to `onResponse` (not exported by core). */
69
37
  interface ICNSResponseLike {
70
38
  inputSignal?: {
@@ -87,8 +55,33 @@ interface TCNSWorkerOptions {
87
55
  registry: CNSPersistOptionsRegistry;
88
56
  /** Queue name to consume. */
89
57
  queue: string;
90
- /** Optional durable progress sink (started / hop / completed). */
91
- progress?: ICNSProgressSink;
58
+ /**
59
+ * Optional **observability** store: the canonical name-based
60
+ * Stimulation → Attempt → Task history (the SAME model the devtools use),
61
+ * written per hop via a {@link CNSStimulationPersistor}. Pass a
62
+ * `CNSInMemoryStimulationRepository` to just *see* progress (ephemeral), or a
63
+ * Postgres store to keep it. It records history; it does not drive resume —
64
+ * that is `resume` below. One job = one stimulation (`stimulationId` = job id),
65
+ * each pg-boss retry = a new attempt.
66
+ */
67
+ observe?: ICNSStimulationRepository;
68
+ /**
69
+ * Optional durable **resume**. When set, the worker records the run's
70
+ * outstanding frontier (by name) into `repository` as it advances, and a
71
+ * retried job (same job id) resumes from that frontier via `cns.activate(...)`
72
+ * instead of re-running from the entry signal. The record's lifecycle matches
73
+ * the pg-boss job row: dropped on success, kept on failure for the retry.
74
+ *
75
+ * Requires every neuron/collateral that can appear in the frontier to be in
76
+ * the `registry` (not just the entry collaterals). Neurons should still be
77
+ * idempotent — resume narrows re-execution to the unfinished branch, it does
78
+ * not make individual hops exactly-once.
79
+ */
80
+ resume?: {
81
+ repository: ICNSProgressRepository;
82
+ /** When to persist during the run. Defaults to debounce + max-staleness. */
83
+ strategy?: ICNSProgressRecordingStrategy;
84
+ };
92
85
  /** pg-boss `work` options (batchSize, pollingIntervalSeconds, …). */
93
86
  workOptions?: Record<string, unknown>;
94
87
  }
@@ -123,4 +116,4 @@ declare function stimulationJob(registry: CNSPersistOptionsRegistry, collateral:
123
116
  /** Send a stimulation job to a pg-boss queue. Returns the job id. */
124
117
  declare function enqueueStimulation(boss: IPgBossLike, queue: string, job: TCNSStimulationJobData, options?: TCNSSendOptions): Promise<string | null>;
125
118
 
126
- export { type ICNSProgressSink, type ICNSResponseLike, type IPgBossJob, type IPgBossLike, type TCNSHopRecord, type TCNSSendOptions, type TCNSStimulationJobData, type TCNSStimulationLifecycle, type TCNSWorkerOptions, createCNSWorker, enqueueStimulation, stimulationJob };
119
+ export { type ICNSResponseLike, type IPgBossJob, type IPgBossLike, type TCNSSendOptions, type TCNSStimulationJobData, type TCNSWorkerOptions, createCNSWorker, enqueueStimulation, stimulationJob };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { ICNS, CNSPersistOptionsRegistry, CNSCollateral } from '@cnstra/core';
1
+ import { CNSPersistOptionsRegistry, ICNSStimulationRepository, ICNSProgressRepository, ICNSProgressRecordingStrategy } from '@cnstra/persist';
2
+ import { ICNS } from '@cnstra/types';
3
+ import { CNSCollateral } from '@cnstra/core';
2
4
 
3
5
  /**
4
6
  * Minimal, structural view of the pg-boss surface this package touches.
@@ -12,6 +14,8 @@ interface IPgBossJob<TData = unknown> {
12
14
  id: string;
13
15
  name: string;
14
16
  data: TData;
17
+ /** 0-based retry count (pg-boss `Job.retryCount`); drives the attempt number. */
18
+ retryCount?: number;
15
19
  }
16
20
  interface IPgBossLike {
17
21
  send(name: string, data: object, options?: Record<string, unknown>): Promise<string | null>;
@@ -29,42 +33,6 @@ interface TCNSStimulationJobData {
29
33
  /** Optional per-run cap on neuron hops. */
30
34
  maxNeuronHops?: number;
31
35
  }
32
- /** One per-hop progress record (mirrors the devtools `CNSDTOHop` shape). */
33
- interface TCNSHopRecord {
34
- jobId: string;
35
- index: number;
36
- neuronName: string | null;
37
- inputCollateral: string | null;
38
- outputCollateral: string | null;
39
- inputPayload: unknown;
40
- outputPayload: unknown;
41
- error: string | null;
42
- at: number;
43
- }
44
- /** Lifecycle record for one stimulation (one job). */
45
- interface TCNSStimulationLifecycle {
46
- jobId: string;
47
- collateralName: string;
48
- payload: unknown;
49
- status: 'running' | 'completed' | 'failed';
50
- startedAt: number;
51
- completedAt?: number;
52
- hopCount: number;
53
- error?: string;
54
- }
55
- /**
56
- * Where progress is written (Postgres, OIMDB, logs, …).
57
- *
58
- * If `onHop` returns a Promise, CNStra waits for it **before enqueuing the next
59
- * hop** — i.e. each step is durably committed before the flow advances. This is
60
- * the per-hop checkpoint barrier that makes an admin/progress view truthful
61
- * across restarts.
62
- */
63
- interface ICNSProgressSink {
64
- onStarted?(rec: TCNSStimulationLifecycle): void | Promise<void>;
65
- onHop?(rec: TCNSHopRecord): void | Promise<void>;
66
- onCompleted?(rec: TCNSStimulationLifecycle): void | Promise<void>;
67
- }
68
36
  /** Structural view of the response passed to `onResponse` (not exported by core). */
69
37
  interface ICNSResponseLike {
70
38
  inputSignal?: {
@@ -87,8 +55,33 @@ interface TCNSWorkerOptions {
87
55
  registry: CNSPersistOptionsRegistry;
88
56
  /** Queue name to consume. */
89
57
  queue: string;
90
- /** Optional durable progress sink (started / hop / completed). */
91
- progress?: ICNSProgressSink;
58
+ /**
59
+ * Optional **observability** store: the canonical name-based
60
+ * Stimulation → Attempt → Task history (the SAME model the devtools use),
61
+ * written per hop via a {@link CNSStimulationPersistor}. Pass a
62
+ * `CNSInMemoryStimulationRepository` to just *see* progress (ephemeral), or a
63
+ * Postgres store to keep it. It records history; it does not drive resume —
64
+ * that is `resume` below. One job = one stimulation (`stimulationId` = job id),
65
+ * each pg-boss retry = a new attempt.
66
+ */
67
+ observe?: ICNSStimulationRepository;
68
+ /**
69
+ * Optional durable **resume**. When set, the worker records the run's
70
+ * outstanding frontier (by name) into `repository` as it advances, and a
71
+ * retried job (same job id) resumes from that frontier via `cns.activate(...)`
72
+ * instead of re-running from the entry signal. The record's lifecycle matches
73
+ * the pg-boss job row: dropped on success, kept on failure for the retry.
74
+ *
75
+ * Requires every neuron/collateral that can appear in the frontier to be in
76
+ * the `registry` (not just the entry collaterals). Neurons should still be
77
+ * idempotent — resume narrows re-execution to the unfinished branch, it does
78
+ * not make individual hops exactly-once.
79
+ */
80
+ resume?: {
81
+ repository: ICNSProgressRepository;
82
+ /** When to persist during the run. Defaults to debounce + max-staleness. */
83
+ strategy?: ICNSProgressRecordingStrategy;
84
+ };
92
85
  /** pg-boss `work` options (batchSize, pollingIntervalSeconds, …). */
93
86
  workOptions?: Record<string, unknown>;
94
87
  }
@@ -123,4 +116,4 @@ declare function stimulationJob(registry: CNSPersistOptionsRegistry, collateral:
123
116
  /** Send a stimulation job to a pg-boss queue. Returns the job id. */
124
117
  declare function enqueueStimulation(boss: IPgBossLike, queue: string, job: TCNSStimulationJobData, options?: TCNSSendOptions): Promise<string | null>;
125
118
 
126
- export { type ICNSProgressSink, type ICNSResponseLike, type IPgBossJob, type IPgBossLike, type TCNSHopRecord, type TCNSSendOptions, type TCNSStimulationJobData, type TCNSStimulationLifecycle, type TCNSWorkerOptions, createCNSWorker, enqueueStimulation, stimulationJob };
119
+ export { type ICNSResponseLike, type IPgBossJob, type IPgBossLike, type TCNSSendOptions, type TCNSStimulationJobData, type TCNSWorkerOptions, createCNSWorker, enqueueStimulation, stimulationJob };
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
4
  var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
5
6
  var __export = (target, all) => {
6
7
  for (var name in all)
7
8
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -26,105 +27,106 @@ __export(index_exports, {
26
27
  module.exports = __toCommonJS(index_exports);
27
28
 
28
29
  // src/worker.ts
29
- function toHopRecord(jobId, index, resp, cns, registry) {
30
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
31
- const outputCol = (_a = resp.outputSignal) == null ? void 0 : _a.collateral;
32
- const inputCol = (_b = resp.inputSignal) == null ? void 0 : _b.collateral;
33
- const ownerNeuron = outputCol ? cns.network.getParentNeuronByCollateral(outputCol) : null;
34
- return {
35
- jobId,
36
- index,
37
- neuronName: ownerNeuron ? (_c = registry.getNeuronName(ownerNeuron)) != null ? _c : null : null,
38
- inputCollateral: inputCol ? (_d = registry.getCollateralName(inputCol)) != null ? _d : null : null,
39
- outputCollateral: outputCol ? (_e = registry.getCollateralName(outputCol)) != null ? _e : null : null,
40
- inputPayload: (_i = (_h = (_f = resp.inputSignal) == null ? void 0 : _f.payload) != null ? _h : (_g = resp.outputSignal) == null ? void 0 : _g.payload) != null ? _i : null,
41
- outputPayload: outputCol ? (_k = (_j = resp.outputSignal) == null ? void 0 : _j.payload) != null ? _k : null : null,
42
- error: resp.error != null ? String(resp.error) : null,
43
- at: Date.now()
44
- };
45
- }
30
+ var import_persist = require("@cnstra/persist");
46
31
  function createCNSWorker(options) {
47
- const { boss, cns, registry, queue, progress, workOptions } = options;
48
- const runJob = async (job) => {
49
- var _a, _b, _c;
32
+ const { boss, cns, registry, queue, observe, resume, workOptions } = options;
33
+ const serializer = resume ? new import_persist.CNSProgressSerializer(registry) : void 0;
34
+ const runJob = /* @__PURE__ */ __name(async (job) => {
35
+ var _a;
50
36
  const { collateralName, payload, maxNeuronHops } = job.data;
51
37
  const collateral = registry.getCollateral(collateralName);
52
38
  if (!collateral) {
53
- throw new Error(
54
- `[@cnstra/pg-boss] Unknown collateral "${collateralName}". Register it in the CNSPersistOptionsRegistry.`
55
- );
39
+ throw new Error(`[@cnstra/pg-boss] Unknown collateral "${collateralName}". Register it in the CNSPersistOptionsRegistry.`);
56
40
  }
57
- const startedAt = Date.now();
58
- let hopCount = 0;
59
- await ((_a = progress == null ? void 0 : progress.onStarted) == null ? void 0 : _a.call(progress, {
60
- jobId: job.id,
61
- collateralName,
62
- payload,
63
- status: "running",
64
- startedAt,
65
- hopCount: 0
66
- }));
67
- const stimulation = cns.stimulate(collateral.createSignal(payload), {
68
- maxNeuronHops,
69
- onResponse: async (resp) => {
70
- var _a2;
71
- const rec = toHopRecord(
72
- job.id,
73
- hopCount,
74
- resp,
75
- cns,
76
- registry
77
- );
78
- hopCount += 1;
79
- await ((_a2 = progress == null ? void 0 : progress.onHop) == null ? void 0 : _a2.call(progress, rec));
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,
50
+ 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
+ });
80
82
  }
81
- });
83
+ }
84
+ if (!stimulation) {
85
+ stimulation = cns.stimulate(collateral.createSignal(payload), {
86
+ maxNeuronHops,
87
+ onResponse,
88
+ stimulationContext
89
+ });
90
+ }
82
91
  try {
83
92
  await stimulation.waitUntilComplete();
84
- await ((_b = progress == null ? void 0 : progress.onCompleted) == null ? void 0 : _b.call(progress, {
85
- jobId: job.id,
86
- collateralName,
87
- payload,
88
- status: "completed",
89
- startedAt,
90
- completedAt: Date.now(),
91
- hopCount
92
- }));
93
+ if (resume) await resume.repository.delete(job.id);
93
94
  } catch (err) {
94
- await ((_c = progress == null ? void 0 : progress.onCompleted) == null ? void 0 : _c.call(progress, {
95
- jobId: job.id,
96
- collateralName,
97
- payload,
98
- status: "failed",
99
- startedAt,
100
- completedAt: Date.now(),
101
- hopCount,
102
- error: err instanceof Error ? err.message : String(err)
103
- }));
95
+ if (resume && serializer) {
96
+ await resume.repository.save(job.id, serializer.serialize(stimulation));
97
+ }
104
98
  throw err;
99
+ } finally {
100
+ recorder == null ? void 0 : recorder.dispose();
101
+ persistor == null ? void 0 : persistor.dispose();
105
102
  }
106
- };
107
- const handler = async (jobs) => {
103
+ }, "runJob");
104
+ const handler = /* @__PURE__ */ __name(async (jobs) => {
108
105
  for (const job of jobs) {
109
106
  await runJob(job);
110
107
  }
111
- };
108
+ }, "handler");
112
109
  return workOptions ? boss.work(queue, workOptions, handler) : boss.work(queue, handler);
113
110
  }
111
+ __name(createCNSWorker, "createCNSWorker");
114
112
 
115
113
  // src/enqueue.ts
116
114
  function stimulationJob(registry, collateral, payload, extra) {
117
115
  const collateralName = registry.getCollateralName(collateral);
118
116
  if (!collateralName) {
119
- throw new Error(
120
- "[@cnstra/pg-boss] Collateral is not registered in the CNSPersistOptionsRegistry; cannot enqueue it by name."
121
- );
117
+ throw new Error("[@cnstra/pg-boss] Collateral is not registered in the CNSPersistOptionsRegistry; cannot enqueue it by name.");
122
118
  }
123
- return { collateralName, payload, ...extra };
119
+ return {
120
+ collateralName,
121
+ payload,
122
+ ...extra
123
+ };
124
124
  }
125
+ __name(stimulationJob, "stimulationJob");
125
126
  function enqueueStimulation(boss, queue, job, options) {
126
127
  return boss.send(queue, job, options);
127
128
  }
129
+ __name(enqueueStimulation, "enqueueStimulation");
128
130
  // Annotate the CommonJS export names for ESM import in node:
129
131
  0 && (module.exports = {
130
132
  createCNSWorker,