@cnstra/persist-postgres 0.1.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 ADDED
@@ -0,0 +1,435 @@
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/index.ts
23
+ var index_exports = {};
24
+ __export(index_exports, {
25
+ CNSPostgresStimulationRepository: () => CNSPostgresStimulationRepository,
26
+ DEFAULT_PREFIX: () => DEFAULT_PREFIX,
27
+ assertSafePrefix: () => assertSafePrefix,
28
+ ensureSchema: () => ensureSchema,
29
+ persistPostgresSchemaSql: () => persistPostgresSchemaSql
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/CNSPostgresStimulationRepository.ts
34
+ var import_pg = require("pg");
35
+
36
+ // src/migrate.ts
37
+ var DEFAULT_PREFIX = "cns_";
38
+ function assertSafePrefix(prefix) {
39
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(prefix)) {
40
+ throw new Error(`[@cnstra/persist-postgres] unsafe table prefix: ${JSON.stringify(prefix)}`);
41
+ }
42
+ return prefix;
43
+ }
44
+ __name(assertSafePrefix, "assertSafePrefix");
45
+ function persistPostgresSchemaSql(prefix = DEFAULT_PREFIX) {
46
+ const p = assertSafePrefix(prefix);
47
+ return `
48
+ CREATE TABLE IF NOT EXISTS ${p}stimulation (
49
+ stimulation_id text PRIMARY KEY,
50
+ entry jsonb NOT NULL,
51
+ status text NOT NULL,
52
+ scope_name text,
53
+ created_at timestamptz NOT NULL DEFAULT now(),
54
+ updated_at timestamptz NOT NULL DEFAULT now()
55
+ );
56
+ CREATE INDEX IF NOT EXISTS ${p}stimulation_updated_idx ON ${p}stimulation (updated_at DESC);
57
+ CREATE INDEX IF NOT EXISTS ${p}stimulation_scope_idx ON ${p}stimulation (scope_name);
58
+
59
+ CREATE TABLE IF NOT EXISTS ${p}stimulation_frontier (
60
+ stimulation_id text NOT NULL REFERENCES ${p}stimulation(stimulation_id) ON DELETE CASCADE,
61
+ ord int NOT NULL,
62
+ neuron_name text NOT NULL,
63
+ dendrite_collateral_name text NOT NULL,
64
+ input jsonb,
65
+ PRIMARY KEY (stimulation_id, ord)
66
+ );
67
+
68
+ CREATE TABLE IF NOT EXISTS ${p}stimulation_context (
69
+ stimulation_id text NOT NULL REFERENCES ${p}stimulation(stimulation_id) ON DELETE CASCADE,
70
+ neuron_name text NOT NULL,
71
+ value jsonb NOT NULL,
72
+ PRIMARY KEY (stimulation_id, neuron_name)
73
+ );
74
+
75
+ -- NOTE: no FK on stimulation_id \u2014 over the devtools wire the persistor's
76
+ -- saveStimulation and saveAttempt ride SEPARATE async batches processed
77
+ -- concurrently by the server, so an attempt insert can reach Postgres before its
78
+ -- stimulation row's transaction commits; a hard FK would reject it (23503). Same
79
+ -- exposure as ${p}task; delete() removes attempts explicitly instead of cascading.
80
+ CREATE TABLE IF NOT EXISTS ${p}stimulation_attempt (
81
+ stimulation_attempt_id text PRIMARY KEY,
82
+ stimulation_id text NOT NULL,
83
+ attempt_number int NOT NULL,
84
+ status text NOT NULL,
85
+ started_at bigint NOT NULL,
86
+ completed_at bigint,
87
+ hop_count int NOT NULL,
88
+ has_error boolean NOT NULL,
89
+ replay_of text,
90
+ entry jsonb NOT NULL,
91
+ UNIQUE (stimulation_id, attempt_number)
92
+ );
93
+ CREATE INDEX IF NOT EXISTS ${p}stimulation_attempt_idx ON ${p}stimulation_attempt (stimulation_id, attempt_number);
94
+
95
+ -- NOTE: no FK on stimulation_attempt_id \u2014 the persistor streams settled tasks
96
+ -- (appendTask) as hops complete, BEFORE the attempt marker is flushed (saveAttempt),
97
+ -- so a hard FK would reject the insert. delete() removes tasks explicitly instead.
98
+ CREATE TABLE IF NOT EXISTS ${p}task (
99
+ stimulation_attempt_id text NOT NULL,
100
+ "index" int NOT NULL,
101
+ neuron_name text NOT NULL,
102
+ dendrite_collateral_name text NOT NULL,
103
+ input_index int NOT NULL,
104
+ output jsonb,
105
+ status text NOT NULL,
106
+ error text,
107
+ started_at bigint NOT NULL,
108
+ duration int,
109
+ PRIMARY KEY (stimulation_attempt_id, "index")
110
+ );
111
+ `;
112
+ }
113
+ __name(persistPostgresSchemaSql, "persistPostgresSchemaSql");
114
+ async function ensureSchema(db, prefix = DEFAULT_PREFIX) {
115
+ await db.query(persistPostgresSchemaSql(prefix));
116
+ }
117
+ __name(ensureSchema, "ensureSchema");
118
+
119
+ // src/CNSPostgresStimulationRepository.ts
120
+ var j = /* @__PURE__ */ __name((v) => JSON.stringify(v != null ? v : null), "j");
121
+ var num = /* @__PURE__ */ __name((v) => Number(v), "num");
122
+ var numOrNull = /* @__PURE__ */ __name((v) => v == null ? null : Number(v), "numOrNull");
123
+ var _CNSPostgresStimulationRepository = class _CNSPostgresStimulationRepository {
124
+ constructor(opts) {
125
+ __publicField(this, "db");
126
+ __publicField(this, "pool");
127
+ __publicField(this, "ownsPool");
128
+ __publicField(this, "p");
129
+ __publicField(this, "autoMigrate");
130
+ __publicField(this, "ready");
131
+ var _a, _b;
132
+ if (opts.pool) {
133
+ this.pool = opts.pool;
134
+ this.db = opts.pool;
135
+ this.ownsPool = false;
136
+ } else if (opts.connectionString) {
137
+ this.pool = new import_pg.Pool({
138
+ connectionString: opts.connectionString
139
+ });
140
+ this.db = this.pool;
141
+ this.ownsPool = true;
142
+ } else if (opts.queryable) {
143
+ this.db = opts.queryable;
144
+ this.ownsPool = false;
145
+ } else {
146
+ throw new Error("[@cnstra/persist-postgres] needs `connectionString`, `pool`, or `queryable`");
147
+ }
148
+ this.p = assertSafePrefix((_a = opts.tablePrefix) != null ? _a : DEFAULT_PREFIX);
149
+ this.autoMigrate = (_b = opts.autoMigrate) != null ? _b : true;
150
+ }
151
+ ensure() {
152
+ var _a;
153
+ if (!this.autoMigrate) return Promise.resolve();
154
+ return (_a = this.ready) != null ? _a : this.ready = ensureSchema(this.db, this.p);
155
+ }
156
+ /** Run `fn` in a transaction when a Pool is available; else best-effort inline. */
157
+ async tx(fn) {
158
+ if (!this.pool) return fn(this.db);
159
+ const client = await this.pool.connect();
160
+ try {
161
+ await client.query("BEGIN");
162
+ const out = await fn(client);
163
+ await client.query("COMMIT");
164
+ return out;
165
+ } catch (e) {
166
+ await client.query("ROLLBACK");
167
+ throw e;
168
+ } finally {
169
+ client.release();
170
+ }
171
+ }
172
+ // ── write ──
173
+ async saveStimulation(s) {
174
+ await this.ensure();
175
+ const { p } = this;
176
+ await this.tx(async (db) => {
177
+ var _a;
178
+ await db.query(`INSERT INTO ${p}stimulation (stimulation_id, entry, status, scope_name, updated_at)
179
+ VALUES ($1, $2::jsonb, $3, $4, now())
180
+ ON CONFLICT (stimulation_id)
181
+ DO UPDATE SET entry = EXCLUDED.entry, status = EXCLUDED.status,
182
+ scope_name = EXCLUDED.scope_name, updated_at = now()`, [
183
+ s.stimulationId,
184
+ j(s.entry),
185
+ s.status,
186
+ (_a = s.scopeName) != null ? _a : null
187
+ ]);
188
+ await db.query(`DELETE FROM ${p}stimulation_frontier WHERE stimulation_id = $1`, [
189
+ s.stimulationId
190
+ ]);
191
+ for (let i = 0; i < s.progress.tasks.length; i++) {
192
+ const t = s.progress.tasks[i];
193
+ await db.query(`INSERT INTO ${p}stimulation_frontier
194
+ (stimulation_id, ord, neuron_name, dendrite_collateral_name, input)
195
+ VALUES ($1, $2, $3, $4, $5::jsonb)`, [
196
+ s.stimulationId,
197
+ i,
198
+ t.neuronName,
199
+ t.dendriteCollateralName,
200
+ t.input ? j(t.input) : null
201
+ ]);
202
+ }
203
+ await db.query(`DELETE FROM ${p}stimulation_context WHERE stimulation_id = $1`, [
204
+ s.stimulationId
205
+ ]);
206
+ for (const [neuronName, value] of Object.entries(s.progress.context)) {
207
+ await db.query(`INSERT INTO ${p}stimulation_context (stimulation_id, neuron_name, value)
208
+ VALUES ($1, $2, $3::jsonb)`, [
209
+ s.stimulationId,
210
+ neuronName,
211
+ j(value)
212
+ ]);
213
+ }
214
+ });
215
+ }
216
+ async saveAttempt(a) {
217
+ await this.ensure();
218
+ const { p } = this;
219
+ await this.db.query(`INSERT INTO ${p}stimulation_attempt
220
+ (stimulation_attempt_id, stimulation_id, attempt_number, status, started_at,
221
+ completed_at, hop_count, has_error, replay_of, entry)
222
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10::jsonb)
223
+ ON CONFLICT (stimulation_attempt_id) DO UPDATE SET
224
+ status = EXCLUDED.status, completed_at = EXCLUDED.completed_at,
225
+ hop_count = EXCLUDED.hop_count, has_error = EXCLUDED.has_error,
226
+ replay_of = EXCLUDED.replay_of, entry = EXCLUDED.entry`, [
227
+ a.stimulationAttemptId,
228
+ a.stimulationId,
229
+ a.attemptNumber,
230
+ a.status,
231
+ a.startedAt,
232
+ a.completedAt,
233
+ a.hopCount,
234
+ a.hasError,
235
+ a.replayOf,
236
+ j(a.entry)
237
+ ]);
238
+ }
239
+ async appendTask(t) {
240
+ await this.ensure();
241
+ const { p } = this;
242
+ await this.db.query(`INSERT INTO ${p}task
243
+ (stimulation_attempt_id, "index", neuron_name, dendrite_collateral_name,
244
+ input_index, output, status, error, started_at, duration)
245
+ VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7,$8,$9,$10)
246
+ ON CONFLICT (stimulation_attempt_id, "index") DO NOTHING`, [
247
+ t.stimulationAttemptId,
248
+ t.index,
249
+ t.neuronName,
250
+ t.dendriteCollateralName,
251
+ t.inputIndex,
252
+ t.output ? j(t.output) : null,
253
+ t.status,
254
+ t.error,
255
+ t.startedAt,
256
+ t.duration
257
+ ]);
258
+ }
259
+ // ── read ──
260
+ async getStimulation(stimulationId) {
261
+ await this.ensure();
262
+ const { p } = this;
263
+ const stimRes = await this.db.query(`SELECT entry, status, scope_name FROM ${p}stimulation WHERE stimulation_id = $1`, [
264
+ stimulationId
265
+ ]);
266
+ const row = stimRes.rows[0];
267
+ if (!row) return void 0;
268
+ const [frontier, context] = await Promise.all([
269
+ this.db.query(`SELECT ord, neuron_name, dendrite_collateral_name, input
270
+ FROM ${p}stimulation_frontier WHERE stimulation_id = $1 ORDER BY ord`, [
271
+ stimulationId
272
+ ]),
273
+ this.db.query(`SELECT neuron_name, value FROM ${p}stimulation_context WHERE stimulation_id = $1`, [
274
+ stimulationId
275
+ ])
276
+ ]);
277
+ return {
278
+ stimulationId,
279
+ entry: row.entry,
280
+ status: row.status,
281
+ progress: this.buildProgress(frontier.rows, context.rows),
282
+ ...row.scope_name != null ? {
283
+ scopeName: row.scope_name
284
+ } : {}
285
+ };
286
+ }
287
+ async listStimulations(filter) {
288
+ var _a;
289
+ await this.ensure();
290
+ const { p } = this;
291
+ const scope = (_a = filter == null ? void 0 : filter.scopeName) != null ? _a : null;
292
+ const stimsRes = await this.db.query(`SELECT stimulation_id, entry, status, scope_name FROM ${p}stimulation
293
+ WHERE ($1::text IS NULL OR scope_name = $1)
294
+ ORDER BY updated_at DESC`, [
295
+ scope
296
+ ]);
297
+ const stims = stimsRes.rows;
298
+ if (stims.length === 0) return [];
299
+ const ids = stims.map((r) => r.stimulation_id);
300
+ const [frontierRes, contextRes] = await Promise.all([
301
+ this.db.query(`SELECT stimulation_id, ord, neuron_name, dendrite_collateral_name, input
302
+ FROM ${p}stimulation_frontier WHERE stimulation_id = ANY($1) ORDER BY stimulation_id, ord`, [
303
+ ids
304
+ ]),
305
+ this.db.query(`SELECT stimulation_id, neuron_name, value
306
+ FROM ${p}stimulation_context WHERE stimulation_id = ANY($1)`, [
307
+ ids
308
+ ])
309
+ ]);
310
+ const frontierBy = groupBy(frontierRes.rows, (r) => r.stimulation_id);
311
+ const contextBy = groupBy(contextRes.rows, (r) => r.stimulation_id);
312
+ return stims.map((r) => {
313
+ var _a2, _b;
314
+ return {
315
+ stimulationId: r.stimulation_id,
316
+ entry: r.entry,
317
+ status: r.status,
318
+ progress: this.buildProgress((_a2 = frontierBy.get(r.stimulation_id)) != null ? _a2 : [], (_b = contextBy.get(r.stimulation_id)) != null ? _b : []),
319
+ ...r.scope_name != null ? {
320
+ scopeName: r.scope_name
321
+ } : {}
322
+ };
323
+ });
324
+ }
325
+ async getAttempts(stimulationId) {
326
+ await this.ensure();
327
+ const { p } = this;
328
+ const res = await this.db.query(`SELECT * FROM ${p}stimulation_attempt WHERE stimulation_id = $1 ORDER BY attempt_number`, [
329
+ stimulationId
330
+ ]);
331
+ return res.rows.map(rowToAttempt);
332
+ }
333
+ async getTasks(stimulationAttemptId) {
334
+ await this.ensure();
335
+ const { p } = this;
336
+ const res = await this.db.query(`SELECT * FROM ${p}task WHERE stimulation_attempt_id = $1 ORDER BY "index"`, [
337
+ stimulationAttemptId
338
+ ]);
339
+ return res.rows.map(rowToTask);
340
+ }
341
+ // ── lifecycle ──
342
+ async delete(stimulationId) {
343
+ await this.ensure();
344
+ const { p } = this;
345
+ await this.tx(async (db) => {
346
+ await db.query(`DELETE FROM ${p}task WHERE stimulation_attempt_id IN
347
+ (SELECT stimulation_attempt_id FROM ${p}stimulation_attempt WHERE stimulation_id = $1)`, [
348
+ stimulationId
349
+ ]);
350
+ await db.query(`DELETE FROM ${p}stimulation_attempt WHERE stimulation_id = $1`, [
351
+ stimulationId
352
+ ]);
353
+ await db.query(`DELETE FROM ${p}stimulation WHERE stimulation_id = $1`, [
354
+ stimulationId
355
+ ]);
356
+ });
357
+ }
358
+ async close() {
359
+ if (this.ownsPool && this.pool) await this.pool.end();
360
+ }
361
+ // ── helpers ──
362
+ buildProgress(frontier, context) {
363
+ const tasks = [
364
+ ...frontier
365
+ ].sort((a, b) => a.ord - b.ord).map((f) => ({
366
+ neuronName: f.neuron_name,
367
+ dendriteCollateralName: f.dendrite_collateral_name,
368
+ ...f.input != null ? {
369
+ input: f.input
370
+ } : {}
371
+ }));
372
+ const ctx = {};
373
+ for (const c of context) ctx[c.neuron_name] = c.value;
374
+ return {
375
+ tasks,
376
+ context: ctx
377
+ };
378
+ }
379
+ };
380
+ __name(_CNSPostgresStimulationRepository, "CNSPostgresStimulationRepository");
381
+ var CNSPostgresStimulationRepository = _CNSPostgresStimulationRepository;
382
+ function rowToAttempt(a) {
383
+ var _a;
384
+ return {
385
+ stimulationAttemptId: a.stimulation_attempt_id,
386
+ stimulationId: a.stimulation_id,
387
+ attemptNumber: num(a.attempt_number),
388
+ status: a.status,
389
+ startedAt: num(a.started_at),
390
+ completedAt: numOrNull(a.completed_at),
391
+ hopCount: num(a.hop_count),
392
+ hasError: Boolean(a.has_error),
393
+ replayOf: (_a = a.replay_of) != null ? _a : null,
394
+ entry: a.entry
395
+ };
396
+ }
397
+ __name(rowToAttempt, "rowToAttempt");
398
+ function rowToTask(t) {
399
+ var _a, _b;
400
+ return {
401
+ stimulationAttemptId: t.stimulation_attempt_id,
402
+ index: num(t.index),
403
+ neuronName: t.neuron_name,
404
+ dendriteCollateralName: t.dendrite_collateral_name,
405
+ inputIndex: num(t.input_index),
406
+ output: (_a = t.output) != null ? _a : null,
407
+ status: t.status,
408
+ error: (_b = t.error) != null ? _b : null,
409
+ startedAt: num(t.started_at),
410
+ duration: numOrNull(t.duration)
411
+ };
412
+ }
413
+ __name(rowToTask, "rowToTask");
414
+ function groupBy(rows, key) {
415
+ const m = /* @__PURE__ */ new Map();
416
+ for (const r of rows) {
417
+ const k = key(r);
418
+ const b = m.get(k);
419
+ if (b) b.push(r);
420
+ else m.set(k, [
421
+ r
422
+ ]);
423
+ }
424
+ return m;
425
+ }
426
+ __name(groupBy, "groupBy");
427
+ // Annotate the CommonJS export names for ESM import in node:
428
+ 0 && (module.exports = {
429
+ CNSPostgresStimulationRepository,
430
+ DEFAULT_PREFIX,
431
+ assertSafePrefix,
432
+ ensureSchema,
433
+ persistPostgresSchemaSql
434
+ });
435
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/CNSPostgresStimulationRepository.ts","../src/migrate.ts"],"sourcesContent":["export {\n CNSPostgresStimulationRepository,\n type TCNSPostgresStimulationRepositoryOptions,\n} from './CNSPostgresStimulationRepository';\nexport {\n ensureSchema,\n persistPostgresSchemaSql,\n assertSafePrefix,\n DEFAULT_PREFIX,\n type IPgQueryable,\n} from './migrate';\n","/**\n * Postgres relational store for the CNStra stimulation/attempt/task durable model\n * (`ICNSStimulationRepository`). The stimulation's `progress` is NORMALISED: the\n * outstanding frontier into `<prefix>stimulation_frontier` (one row per task) and\n * per-neuron context into `<prefix>stimulation_context` — rebuilt into `TCNSProgress`\n * on read.\n *\n * Reuses the `IPgQueryable` seam so `pg` is an optional peer: inject any pg\n * client/pool, or pass a `connectionString` for the repo to own a Pool. Behaviourally\n * matches `CNSInMemoryStimulationRepository` (listStimulations newest-first, getAttempts\n * by attemptNumber, getTasks by index, delete cascades).\n */\nimport { Pool } from 'pg';\nimport type { ICNSStimulationRepository } from '@cnstra/persist';\nimport type {\n TCNSStimulationPersisted,\n TCNSStimulationAttemptPersisted,\n TCNSStimulationTaskPersisted,\n TCNSProgress,\n TCNSSerializedTask,\n TCNSSignalRef,\n TCNSStimulationStatus,\n TCNSStimulationTaskStatus,\n} from '@cnstra/persist-dto';\nimport {\n DEFAULT_PREFIX,\n assertSafePrefix,\n ensureSchema,\n type IPgQueryable,\n} from './migrate';\n\nexport interface TCNSPostgresStimulationRepositoryOptions {\n connectionString?: string;\n /** Inject an existing pg Pool (transactions supported; not closed by `close()`). */\n pool?: Pool;\n /** Inject any queryable (reads + best-effort non-transactional writes). */\n queryable?: IPgQueryable;\n /** Table prefix (default `cns_`). */\n tablePrefix?: string;\n /** Lazily ensure the schema on first use (default true). */\n autoMigrate?: boolean;\n}\n\nconst j = (v: unknown): string => JSON.stringify(v ?? null);\nconst num = (v: unknown): number => Number(v);\nconst numOrNull = (v: unknown): number | null => (v == null ? null : Number(v));\n\nexport class CNSPostgresStimulationRepository\n implements ICNSStimulationRepository\n{\n private readonly db: IPgQueryable;\n private readonly pool?: Pool;\n private readonly ownsPool: boolean;\n private readonly p: string;\n private readonly autoMigrate: boolean;\n private ready: Promise<void> | undefined;\n\n constructor(opts: TCNSPostgresStimulationRepositoryOptions) {\n if (opts.pool) {\n this.pool = opts.pool;\n this.db = opts.pool;\n this.ownsPool = false;\n } else if (opts.connectionString) {\n this.pool = new Pool({ connectionString: opts.connectionString });\n this.db = this.pool;\n this.ownsPool = true;\n } else if (opts.queryable) {\n this.db = opts.queryable;\n this.ownsPool = false;\n } else {\n throw new Error(\n '[@cnstra/persist-postgres] needs `connectionString`, `pool`, or `queryable`'\n );\n }\n this.p = assertSafePrefix(opts.tablePrefix ?? DEFAULT_PREFIX);\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.db, this.p));\n }\n\n /** Run `fn` in a transaction when a Pool is available; else best-effort inline. */\n private async tx<T>(fn: (db: IPgQueryable) => Promise<T>): Promise<T> {\n if (!this.pool) return fn(this.db);\n const client = await this.pool.connect();\n try {\n await client.query('BEGIN');\n const out = await fn(client as unknown as IPgQueryable);\n await client.query('COMMIT');\n return out;\n } catch (e) {\n await client.query('ROLLBACK');\n throw e;\n } finally {\n client.release();\n }\n }\n\n // ── write ──\n\n async saveStimulation(s: TCNSStimulationPersisted): Promise<void> {\n await this.ensure();\n const { p } = this;\n await this.tx(async db => {\n await db.query(\n `INSERT INTO ${p}stimulation (stimulation_id, entry, status, scope_name, updated_at)\n VALUES ($1, $2::jsonb, $3, $4, now())\n ON CONFLICT (stimulation_id)\n DO UPDATE SET entry = EXCLUDED.entry, status = EXCLUDED.status,\n scope_name = EXCLUDED.scope_name, updated_at = now()`,\n [s.stimulationId, j(s.entry), s.status, s.scopeName ?? null]\n );\n // Replace the normalised frontier.\n await db.query(\n `DELETE FROM ${p}stimulation_frontier WHERE stimulation_id = $1`,\n [s.stimulationId]\n );\n for (let i = 0; i < s.progress.tasks.length; i++) {\n const t = s.progress.tasks[i];\n await db.query(\n `INSERT INTO ${p}stimulation_frontier\n (stimulation_id, ord, neuron_name, dendrite_collateral_name, input)\n VALUES ($1, $2, $3, $4, $5::jsonb)`,\n [\n s.stimulationId,\n i,\n t.neuronName,\n t.dendriteCollateralName,\n t.input ? j(t.input) : null,\n ]\n );\n }\n // Replace the normalised context.\n await db.query(\n `DELETE FROM ${p}stimulation_context WHERE stimulation_id = $1`,\n [s.stimulationId]\n );\n for (const [neuronName, value] of Object.entries(\n s.progress.context\n )) {\n await db.query(\n `INSERT INTO ${p}stimulation_context (stimulation_id, neuron_name, value)\n VALUES ($1, $2, $3::jsonb)`,\n [s.stimulationId, neuronName, j(value)]\n );\n }\n });\n }\n\n async saveAttempt(a: TCNSStimulationAttemptPersisted): Promise<void> {\n await this.ensure();\n const { p } = this;\n await this.db.query(\n `INSERT INTO ${p}stimulation_attempt\n (stimulation_attempt_id, stimulation_id, attempt_number, status, started_at,\n completed_at, hop_count, has_error, replay_of, entry)\n VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10::jsonb)\n ON CONFLICT (stimulation_attempt_id) DO UPDATE SET\n status = EXCLUDED.status, completed_at = EXCLUDED.completed_at,\n hop_count = EXCLUDED.hop_count, has_error = EXCLUDED.has_error,\n replay_of = EXCLUDED.replay_of, entry = EXCLUDED.entry`,\n [\n a.stimulationAttemptId,\n a.stimulationId,\n a.attemptNumber,\n a.status,\n a.startedAt,\n a.completedAt,\n a.hopCount,\n a.hasError,\n a.replayOf,\n j(a.entry),\n ]\n );\n }\n\n async appendTask(t: TCNSStimulationTaskPersisted): Promise<void> {\n await this.ensure();\n const { p } = this;\n await this.db.query(\n `INSERT INTO ${p}task\n (stimulation_attempt_id, \"index\", neuron_name, dendrite_collateral_name,\n input_index, output, status, error, started_at, duration)\n VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7,$8,$9,$10)\n ON CONFLICT (stimulation_attempt_id, \"index\") DO NOTHING`,\n [\n t.stimulationAttemptId,\n t.index,\n t.neuronName,\n t.dendriteCollateralName,\n t.inputIndex,\n t.output ? j(t.output) : null,\n t.status,\n t.error,\n t.startedAt,\n t.duration,\n ]\n );\n }\n\n // ── read ──\n\n async getStimulation(\n stimulationId: string\n ): Promise<TCNSStimulationPersisted | undefined> {\n await this.ensure();\n const { p } = this;\n const stimRes = await this.db.query(\n `SELECT entry, status, scope_name FROM ${p}stimulation WHERE stimulation_id = $1`,\n [stimulationId]\n );\n const row = stimRes.rows[0] as\n | {\n entry: TCNSSignalRef;\n status: TCNSStimulationStatus;\n scope_name: string | null;\n }\n | undefined;\n if (!row) return undefined;\n const [frontier, context] = await Promise.all([\n this.db.query(\n `SELECT ord, neuron_name, dendrite_collateral_name, input\n FROM ${p}stimulation_frontier WHERE stimulation_id = $1 ORDER BY ord`,\n [stimulationId]\n ),\n this.db.query(\n `SELECT neuron_name, value FROM ${p}stimulation_context WHERE stimulation_id = $1`,\n [stimulationId]\n ),\n ]);\n return {\n stimulationId,\n entry: row.entry,\n status: row.status,\n progress: this.buildProgress(\n frontier.rows as TFrontierRow[],\n context.rows as TContextRow[]\n ),\n ...(row.scope_name != null ? { scopeName: row.scope_name } : {}),\n };\n }\n\n async listStimulations(filter?: {\n scopeName?: string;\n }): Promise<TCNSStimulationPersisted[]> {\n await this.ensure();\n const { p } = this;\n const scope = filter?.scopeName ?? null;\n const stimsRes = await this.db.query(\n `SELECT stimulation_id, entry, status, scope_name FROM ${p}stimulation\n WHERE ($1::text IS NULL OR scope_name = $1)\n ORDER BY updated_at DESC`,\n [scope]\n );\n const stims = stimsRes.rows as TStimulationRow[];\n if (stims.length === 0) return [];\n const ids = stims.map(r => r.stimulation_id);\n const [frontierRes, contextRes] = await Promise.all([\n this.db.query(\n `SELECT stimulation_id, ord, neuron_name, dendrite_collateral_name, input\n FROM ${p}stimulation_frontier WHERE stimulation_id = ANY($1) ORDER BY stimulation_id, ord`,\n [ids]\n ),\n this.db.query(\n `SELECT stimulation_id, neuron_name, value\n FROM ${p}stimulation_context WHERE stimulation_id = ANY($1)`,\n [ids]\n ),\n ]);\n const frontierBy = groupBy(\n frontierRes.rows as TFrontierRow[],\n r => r.stimulation_id\n );\n const contextBy = groupBy(\n contextRes.rows as TContextRow[],\n r => r.stimulation_id\n );\n return stims.map(r => ({\n stimulationId: r.stimulation_id,\n entry: r.entry,\n status: r.status,\n progress: this.buildProgress(\n frontierBy.get(r.stimulation_id) ?? [],\n contextBy.get(r.stimulation_id) ?? []\n ),\n ...(r.scope_name != null ? { scopeName: r.scope_name } : {}),\n }));\n }\n\n async getAttempts(\n stimulationId: string\n ): Promise<TCNSStimulationAttemptPersisted[]> {\n await this.ensure();\n const { p } = this;\n const res = await this.db.query(\n `SELECT * FROM ${p}stimulation_attempt WHERE stimulation_id = $1 ORDER BY attempt_number`,\n [stimulationId]\n );\n return (res.rows as TAttemptRow[]).map(rowToAttempt);\n }\n\n async getTasks(\n stimulationAttemptId: string\n ): Promise<TCNSStimulationTaskPersisted[]> {\n await this.ensure();\n const { p } = this;\n const res = await this.db.query(\n `SELECT * FROM ${p}task WHERE stimulation_attempt_id = $1 ORDER BY \"index\"`,\n [stimulationAttemptId]\n );\n return (res.rows as TTaskRow[]).map(rowToTask);\n }\n\n // ── lifecycle ──\n\n async delete(stimulationId: string): Promise<void> {\n await this.ensure();\n const { p } = this;\n await this.tx(async db => {\n // tasks + attempts have no FK (see migrate.ts) → delete them explicitly,\n // tasks first (they reference attempt ids), then the attempts.\n await db.query(\n `DELETE FROM ${p}task WHERE stimulation_attempt_id IN\n (SELECT stimulation_attempt_id FROM ${p}stimulation_attempt WHERE stimulation_id = $1)`,\n [stimulationId]\n );\n await db.query(\n `DELETE FROM ${p}stimulation_attempt WHERE stimulation_id = $1`,\n [stimulationId]\n );\n // FK ON DELETE CASCADE removes frontier/context.\n await db.query(\n `DELETE FROM ${p}stimulation WHERE stimulation_id = $1`,\n [stimulationId]\n );\n });\n }\n\n async close(): Promise<void> {\n if (this.ownsPool && this.pool) await this.pool.end();\n }\n\n // ── helpers ──\n\n private buildProgress(\n frontier: TFrontierRow[],\n context: TContextRow[]\n ): TCNSProgress {\n const tasks: TCNSSerializedTask[] = [...frontier]\n .sort((a, b) => a.ord - b.ord)\n .map(f => ({\n neuronName: f.neuron_name,\n dendriteCollateralName: f.dendrite_collateral_name,\n ...(f.input != null ? { input: f.input } : {}),\n }));\n const ctx: Record<string, unknown> = {};\n for (const c of context) ctx[c.neuron_name] = c.value;\n return { tasks, context: ctx };\n }\n}\n\n// ── row shapes (jsonb columns come back already parsed by node-pg) ──\ntype TStimulationRow = {\n stimulation_id: string;\n entry: TCNSSignalRef;\n status: TCNSStimulationStatus;\n scope_name: string | null;\n};\ntype TFrontierRow = {\n stimulation_id: string;\n ord: number;\n neuron_name: string;\n dendrite_collateral_name: string;\n input: { collateralName: string; payload?: unknown } | null;\n};\ntype TContextRow = {\n stimulation_id: string;\n neuron_name: string;\n value: unknown;\n};\ntype TAttemptRow = {\n stimulation_attempt_id: string;\n stimulation_id: string;\n attempt_number: number;\n status: TCNSStimulationStatus;\n started_at: string | number;\n completed_at: string | number | null;\n hop_count: number;\n has_error: boolean;\n replay_of: string | null;\n entry: TCNSSignalRef[];\n};\ntype TTaskRow = {\n stimulation_attempt_id: string;\n index: number;\n neuron_name: string;\n dendrite_collateral_name: string;\n input_index: number;\n output: TCNSSignalRef | null;\n status: TCNSStimulationTaskStatus;\n error: string | null;\n started_at: string | number;\n duration: number | null;\n};\n\nfunction rowToAttempt(a: TAttemptRow): TCNSStimulationAttemptPersisted {\n return {\n stimulationAttemptId: a.stimulation_attempt_id,\n stimulationId: a.stimulation_id,\n attemptNumber: num(a.attempt_number),\n status: a.status,\n startedAt: num(a.started_at),\n completedAt: numOrNull(a.completed_at),\n hopCount: num(a.hop_count),\n hasError: Boolean(a.has_error),\n replayOf: a.replay_of ?? null,\n entry: a.entry,\n };\n}\n\nfunction rowToTask(t: TTaskRow): TCNSStimulationTaskPersisted {\n return {\n stimulationAttemptId: t.stimulation_attempt_id,\n index: num(t.index),\n neuronName: t.neuron_name,\n dendriteCollateralName: t.dendrite_collateral_name,\n inputIndex: num(t.input_index),\n output: t.output ?? null,\n status: t.status,\n error: t.error ?? null,\n startedAt: num(t.started_at),\n duration: numOrNull(t.duration),\n };\n}\n\nfunction groupBy<T, K>(rows: T[], key: (r: T) => K): Map<K, T[]> {\n const m = new Map<K, T[]>();\n for (const r of rows) {\n const k = key(r);\n const b = m.get(k);\n if (b) b.push(r);\n else m.set(k, [r]);\n }\n return m;\n}\n","/**\n * Schema / migrations for the CNStra stimulation/attempt/task relational store.\n *\n * The package OWNS this schema — run it explicitly via `cnstra-persist-postgres migrate`\n * or `ensureSchema(db)`, or let the repository auto-create it on first use. Five tables\n * under a configurable prefix (default `cns_`); the stimulation's `progress` is\n * NORMALISED into `stimulation_frontier` (one row per outstanding task) +\n * `stimulation_context` (one row per neuron).\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_PREFIX = 'cns_';\n\n/** Guard the table prefix (it is interpolated into DDL/DML, not parameterised). */\nexport function assertSafePrefix(prefix: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(prefix)) {\n throw new Error(\n `[@cnstra/persist-postgres] unsafe table prefix: ${JSON.stringify(prefix)}`\n );\n }\n return prefix;\n}\n\n/** The full CREATE TABLE + CREATE INDEX DDL (idempotent). */\nexport function persistPostgresSchemaSql(prefix: string = DEFAULT_PREFIX): string {\n const p = assertSafePrefix(prefix);\n return `\nCREATE TABLE IF NOT EXISTS ${p}stimulation (\n stimulation_id text PRIMARY KEY,\n entry jsonb NOT NULL,\n status text NOT NULL,\n scope_name text,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nCREATE INDEX IF NOT EXISTS ${p}stimulation_updated_idx ON ${p}stimulation (updated_at DESC);\nCREATE INDEX IF NOT EXISTS ${p}stimulation_scope_idx ON ${p}stimulation (scope_name);\n\nCREATE TABLE IF NOT EXISTS ${p}stimulation_frontier (\n stimulation_id text NOT NULL REFERENCES ${p}stimulation(stimulation_id) ON DELETE CASCADE,\n ord int NOT NULL,\n neuron_name text NOT NULL,\n dendrite_collateral_name text NOT NULL,\n input jsonb,\n PRIMARY KEY (stimulation_id, ord)\n);\n\nCREATE TABLE IF NOT EXISTS ${p}stimulation_context (\n stimulation_id text NOT NULL REFERENCES ${p}stimulation(stimulation_id) ON DELETE CASCADE,\n neuron_name text NOT NULL,\n value jsonb NOT NULL,\n PRIMARY KEY (stimulation_id, neuron_name)\n);\n\n-- NOTE: no FK on stimulation_id — over the devtools wire the persistor's\n-- saveStimulation and saveAttempt ride SEPARATE async batches processed\n-- concurrently by the server, so an attempt insert can reach Postgres before its\n-- stimulation row's transaction commits; a hard FK would reject it (23503). Same\n-- exposure as ${p}task; delete() removes attempts explicitly instead of cascading.\nCREATE TABLE IF NOT EXISTS ${p}stimulation_attempt (\n stimulation_attempt_id text PRIMARY KEY,\n stimulation_id text NOT NULL,\n attempt_number int NOT NULL,\n status text NOT NULL,\n started_at bigint NOT NULL,\n completed_at bigint,\n hop_count int NOT NULL,\n has_error boolean NOT NULL,\n replay_of text,\n entry jsonb NOT NULL,\n UNIQUE (stimulation_id, attempt_number)\n);\nCREATE INDEX IF NOT EXISTS ${p}stimulation_attempt_idx ON ${p}stimulation_attempt (stimulation_id, attempt_number);\n\n-- NOTE: no FK on stimulation_attempt_id — the persistor streams settled tasks\n-- (appendTask) as hops complete, BEFORE the attempt marker is flushed (saveAttempt),\n-- so a hard FK would reject the insert. delete() removes tasks explicitly instead.\nCREATE TABLE IF NOT EXISTS ${p}task (\n stimulation_attempt_id text NOT NULL,\n \"index\" int NOT NULL,\n neuron_name text NOT NULL,\n dendrite_collateral_name text NOT NULL,\n input_index int NOT NULL,\n output jsonb,\n status text NOT NULL,\n error text,\n started_at bigint NOT NULL,\n duration int,\n PRIMARY KEY (stimulation_attempt_id, \"index\")\n);\n`;\n}\n\n/** Idempotently create all tables + indexes. Safe to run repeatedly. */\nexport async function ensureSchema(\n db: IPgQueryable,\n prefix: string = DEFAULT_PREFIX\n): Promise<void> {\n await db.query(persistPostgresSchemaSql(prefix));\n}\n"],"mappings":"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","names":["DEFAULT_PREFIX","assertSafePrefix","prefix","test","Error","JSON","stringify","persistPostgresSchemaSql","p","ensureSchema","db","query","j","v","JSON","stringify","num","Number","numOrNull","CNSPostgresStimulationRepository","opts","db","pool","ownsPool","p","autoMigrate","ready","connectionString","Pool","queryable","Error","assertSafePrefix","tablePrefix","DEFAULT_PREFIX","ensure","Promise","resolve","ensureSchema","tx","fn","client","connect","query","out","e","release","saveStimulation","s","stimulationId","entry","status","scopeName","i","progress","tasks","length","t","neuronName","dendriteCollateralName","input","value","Object","entries","context","saveAttempt","a","stimulationAttemptId","attemptNumber","startedAt","completedAt","hopCount","hasError","replayOf","appendTask","index","inputIndex","output","error","duration","getStimulation","stimRes","row","rows","undefined","frontier","all","buildProgress","scope_name","listStimulations","filter","scope","stimsRes","stims","ids","map","r","stimulation_id","frontierRes","contextRes","frontierBy","groupBy","contextBy","_a","get","getAttempts","res","rowToAttempt","getTasks","rowToTask","delete","close","end","sort","b","ord","f","neuron_name","dendrite_collateral_name","ctx","c","stimulation_attempt_id","attempt_number","started_at","completed_at","hop_count","Boolean","has_error","replay_of","input_index","key","m","Map","k","push","set"]}