@cnstra/persist-postgres 0.2.0 → 0.5.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/CNSPostgresStimulationRepository.ts"],"sourcesContent":["/**\n * Postgres relational store for the CNStra stimulation/attempt/task resumable model\n * (`ICNSStimulationRepository`). The stimulation's `progress` is NORMALISED: the\n * outstanding frontier into `<prefix>stimulation_frontier` (one row per task) —\n * rebuilt into `TCNSProgress` 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 {\n ICNSStimulationRepository,\n ICNSStimulationRosterQuery,\n TCNSRosterEntry,\n} from '@cnstra/persist';\nimport { computeRosterFacets, encodeRosterCursor } from '@cnstra/persist';\nimport type {\n TCNSStimulationState,\n TCNSStimulationAttempt,\n TCNSStimulationTask,\n TCNSProgress,\n TCNSPlannedTaskJSON,\n TCNSSignalJSON,\n ECNSStimulationStatus,\n ECNSStimulationTaskStatus,\n TCNSStimulationRosterFilter,\n TCNSStimulationRosterRow,\n TCNSStimulationRosterPage,\n TCNSStimulationRosterFacets,\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, ICNSStimulationRosterQuery\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: TCNSStimulationState): 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 });\n }\n\n async saveAttempt(a: TCNSStimulationAttempt): 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: TCNSStimulationTask): 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<TCNSStimulationState | 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: TCNSSignalJSON;\n status: ECNSStimulationStatus;\n scope_name: string | null;\n }\n | undefined;\n if (!row) return undefined;\n const frontier = await 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 return {\n stimulationId,\n entry: row.entry,\n status: row.status,\n progress: this.buildProgress(frontier.rows as TFrontierRow[]),\n ...(row.scope_name != null ? { scopeName: row.scope_name } : {}),\n };\n }\n\n async listStimulations(filter?: {\n scopeName?: string;\n }): Promise<TCNSStimulationState[]> {\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 = await 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 const frontierBy = groupBy(\n frontierRes.rows as TFrontierRow[],\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 ),\n ...(r.scope_name != null ? { scopeName: r.scope_name } : {}),\n }));\n }\n\n async getAttempts(\n stimulationId: string\n ): Promise<TCNSStimulationAttempt[]> {\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<TCNSStimulationTask[]> {\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 // ── roster query capability (filter + keyset + facets pushed into SQL) ──\n\n /**\n * Derive every filtered roster row in one CTE: `startedAt` = MIN(attempt),\n * `completedAt` = the latest attempt's, `hopCount` = SUM, `hasError` from\n * status, and the DISTINCT activated neuron names (for the neurons lens +\n * filter). Newest-first (`startedAt DESC, stimulationId DESC`) so the keyset\n * cursor is stable. `limit` is applied only when paginating (roster); facets\n * pull the whole filtered set.\n */\n private async selectRoster(\n filter: TCNSStimulationRosterFilter,\n opts?: { cursor?: string; limit?: number }\n ): Promise<{ entries: TCNSRosterEntry[]; total: number }> {\n await this.ensure();\n const { p } = this;\n // Decode the keyset cursor → the (startedAt, stimulationId) to page past.\n let curStarted: number | null = null;\n let curId: string | null = null;\n if (opts?.cursor) {\n const [ts, id] = Buffer.from(opts.cursor, 'base64')\n .toString('utf8')\n .split('|');\n curStarted = Number(ts);\n curId = id ?? '';\n }\n const params: unknown[] = [\n filter.scopeName ?? null,\n filter.status ?? null,\n filter.entryCollateralName ?? null,\n filter.neuronName ?? null,\n filter.text ? filter.text.toLowerCase() : null,\n filter.since ?? null,\n filter.until ?? null,\n curStarted,\n curId,\n ];\n const limitClause =\n opts?.limit != null ? ` LIMIT ${Math.max(0, opts.limit | 0)}` : '';\n const sql = `\n WITH agg AS (\n SELECT a.stimulation_id,\n MIN(a.started_at) AS started_at,\n SUM(a.hop_count) AS hop_count,\n (ARRAY_AGG(a.completed_at ORDER BY a.attempt_number DESC))[1] AS last_completed_at\n FROM ${p}stimulation_attempt a\n GROUP BY a.stimulation_id\n ),\n neu AS (\n SELECT ta.stimulation_id,\n ARRAY_AGG(DISTINCT t.neuron_name) AS neurons\n FROM ${p}task t\n JOIN ${p}stimulation_attempt ta\n ON ta.stimulation_attempt_id = t.stimulation_attempt_id\n GROUP BY ta.stimulation_id\n ),\n roster AS (\n SELECT s.stimulation_id, s.entry, s.status, s.scope_name,\n COALESCE(agg.started_at, 0) AS started_at,\n CASE WHEN s.status = 'running' THEN NULL\n ELSE agg.last_completed_at END AS completed_at,\n COALESCE(agg.hop_count, 0) AS hop_count,\n (s.status = 'failed') AS has_error,\n COALESCE(neu.neurons, ARRAY[]::text[]) AS neurons\n FROM ${p}stimulation s\n LEFT JOIN agg ON agg.stimulation_id = s.stimulation_id\n LEFT JOIN neu ON neu.stimulation_id = s.stimulation_id\n )\n SELECT *, COUNT(*) OVER() AS total_count FROM roster\n WHERE ($1::text IS NULL OR scope_name = $1)\n AND ($2::text IS NULL OR status = $2)\n AND ($3::text IS NULL OR entry->>'collateralName' = $3)\n AND ($4::text IS NULL OR $4 = ANY(neurons))\n AND ($5::text IS NULL OR lower(stimulation_id) LIKE '%'||$5||'%')\n AND ($6::bigint IS NULL OR started_at >= $6)\n AND ($7::bigint IS NULL OR started_at <= $7)\n AND ($8::bigint IS NULL OR (started_at, stimulation_id) < ($8, $9))\n ORDER BY started_at DESC, stimulation_id DESC${limitClause}`;\n const res = await this.db.query(sql, params);\n const rows = res.rows as TRosterRow[];\n const total = rows.length ? Number(rows[0].total_count) : 0;\n const entries: TCNSRosterEntry[] = rows.map(r => ({\n row: {\n stimulationId: r.stimulation_id,\n scopeName: r.scope_name ?? undefined,\n entry: r.entry as TCNSSignalJSON,\n status: r.status,\n startedAt: Number(r.started_at),\n completedAt: r.completed_at == null ? null : Number(r.completed_at),\n hopCount: Number(r.hop_count),\n hasError: r.has_error,\n },\n neurons: r.neurons ?? [],\n }));\n return { entries, total };\n }\n\n async queryRoster(\n filter: TCNSStimulationRosterFilter,\n opts: { cursor?: string; limit: number }\n ): Promise<TCNSStimulationRosterPage> {\n const { entries, total } = await this.selectRoster(filter, opts);\n const rows: TCNSStimulationRosterRow[] = entries.map(e => e.row);\n const lastRow = rows[rows.length - 1];\n // A full page implies there may be more; the window `total` is authoritative.\n const hasMore = rows.length >= opts.limit && rows.length < total;\n return {\n rows,\n nextCursor: hasMore && lastRow ? encodeRosterCursor(lastRow) : null,\n total,\n };\n }\n\n async queryFacets(\n filter: TCNSStimulationRosterFilter,\n opts?: { metricPath?: string }\n ): Promise<TCNSStimulationRosterFacets> {\n // Filter is pushed into SQL; the small GROUP-BY tally + histogram reuse the\n // shared reference aggregation so counts match the in-memory store exactly.\n const { entries } = await this.selectRoster(filter);\n return computeRosterFacets(entries, opts);\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 the frontier.\n await db.query(\n `DELETE FROM ${p}stimulation WHERE stimulation_id = $1`,\n [stimulationId]\n );\n });\n }\n\n /**\n * Retention: prune stimulations not updated for `olderThanMs` (with their\n * attempts/tasks/frontier). A run's `updated_at` stops advancing once it\n * settles, so old finished runs age out while an active one stays fresh — the same\n * semantics as the in-memory store's `ttlMs` and the pg-boss checkpoint's\n * `deleteStale`. Run it on a schedule (cron). Returns the number removed.\n */\n async deleteStale(olderThanMs: number): Promise<number> {\n await this.ensure();\n const { p } = this;\n const params = [Math.max(0, Math.floor(olderThanMs))];\n const cond = `updated_at < now() - ($1::bigint * interval '1 millisecond')`;\n const staleIds = `SELECT stimulation_id FROM ${p}stimulation WHERE ${cond}`;\n return this.tx(async db => {\n // tasks + attempts have no FK → delete explicitly (tasks first), then the\n // stimulations (FK cascade removes the frontier).\n await db.query(\n `DELETE FROM ${p}task WHERE stimulation_attempt_id IN\n (SELECT stimulation_attempt_id FROM ${p}stimulation_attempt\n WHERE stimulation_id IN (${staleIds}))`,\n params\n );\n await db.query(\n `DELETE FROM ${p}stimulation_attempt WHERE stimulation_id IN (${staleIds})`,\n params\n );\n const res = await db.query(\n `DELETE FROM ${p}stimulation WHERE ${cond} RETURNING stimulation_id`,\n params\n );\n return res.rows.length;\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(frontier: TFrontierRow[]): TCNSProgress {\n const tasks: TCNSPlannedTaskJSON[] = [...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 return { tasks };\n }\n}\n\n// ── row shapes (jsonb columns come back already parsed by node-pg) ──\ntype TStimulationRow = {\n stimulation_id: string;\n entry: TCNSSignalJSON;\n status: ECNSStimulationStatus;\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: TCNSSignalJSON | null;\n};\ntype TAttemptRow = {\n stimulation_attempt_id: string;\n stimulation_id: string;\n attempt_number: number;\n status: ECNSStimulationStatus;\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: TCNSSignalJSON[];\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: TCNSSignalJSON | null;\n status: ECNSStimulationTaskStatus;\n error: string | null;\n started_at: string | number;\n duration: number | null;\n};\n/** One roster CTE row (see `selectRoster`), with the window total attached. */\ntype TRosterRow = {\n stimulation_id: string;\n entry: unknown;\n status: string;\n scope_name: string | null;\n started_at: string | number;\n completed_at: string | number | null;\n hop_count: string | number;\n has_error: boolean;\n neurons: string[] | null;\n total_count: string | number;\n};\n\nfunction rowToAttempt(a: TAttemptRow): TCNSStimulationAttempt {\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): TCNSStimulationTask {\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"],"mappings":"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","names":["Pool","computeRosterFacets","encodeRosterCursor","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","saveAttempt","a","stimulationAttemptId","attemptNumber","startedAt","completedAt","hopCount","hasError","replayOf","appendTask","index","inputIndex","output","error","duration","getStimulation","stimRes","row","rows","undefined","frontier","buildProgress","scope_name","listStimulations","filter","scope","stimsRes","stims","ids","map","r","stimulation_id","frontierRes","frontierBy","groupBy","_a","get","getAttempts","res","rowToAttempt","getTasks","rowToTask","selectRoster","curStarted","curId","cursor","ts","id","Buffer","from","toString","split","params","entryCollateralName","text","toLowerCase","since","until","limitClause","limit","Math","max","sql","total","total_count","entries","_b","started_at","completed_at","hop_count","has_error","neurons","queryRoster","lastRow","hasMore","nextCursor","encodeRosterCursor","queryFacets","computeRosterFacets","delete","deleteStale","olderThanMs","floor","cond","staleIds","close","end","sort","b","ord","f","neuron_name","dendrite_collateral_name","stimulation_attempt_id","attempt_number","Boolean","replay_of","input_index","key","m","Map","k","push","set"]}
1
+ {"version":3,"sources":["../src/CNSPostgresStimulationRepository.ts"],"sourcesContent":["import { Pool } from 'pg';\nimport type {\n ICNSStimulationRepository,\n ICNSStimulationRosterQuery,\n TCNSRosterEntry,\n} from '@cnstra/persist';\nimport { encodeCNSRosterCursor, CNS_HISTOGRAM_BUCKETS } from '@cnstra/persist';\nimport type {\n TCNSStimulationState,\n TCNSStimulationAttempt,\n TCNSStimulationTask,\n TCNSProgress,\n TCNSSignalJSON,\n ECNSStimulationStatus,\n ECNSStimulationTaskStatus,\n TCNSStimulationRosterFilter,\n TCNSStimulationRosterRow,\n TCNSStimulationRosterPage,\n TCNSStimulationRosterFacets,\n} from '@cnstra/persist-dto';\nimport type { ICNSPgQueryable } from './interfaces/ICNSPgQueryable';\nimport { CNSPostgresSchemaPartitionRotation } from '@cnstra/postgres-schema';\nimport type { ICNSPostgresSchemaConnection } from '@cnstra/postgres-schema';\nimport { CNSPostgresStimulationSchema } from './schema/CNSPostgresStimulationSchema';\nimport type { TCNSPostgresStimulationRepositoryOptions } from './types/TCNSPostgresStimulationRepositoryOptions';\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, ICNSStimulationRosterQuery\n{\n private static readonly LOCK_STIMULATION = 7;\n private static readonly RETENTION_BATCH = 200;\n\n private readonly db: ICNSPgQueryable;\n private readonly pool?: Pool;\n private readonly ownsPool: boolean;\n private readonly p: string;\n private readonly autoMigrate: boolean;\n private readonly now: () => number;\n private readonly taskPartitions = new CNSPostgresSchemaPartitionRotation({ premakeDays: 7 });\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 = CNSPostgresStimulationSchema.assertTablePrefix(opts.tablePrefix ?? CNSPostgresStimulationSchema.DEFAULT_TABLE_PREFIX);\n this.autoMigrate = opts.autoMigrate ?? true;\n if (this.autoMigrate && !this.pool) {\n throw new Error(\n '[@cnstra/persist-postgres] a `queryable` cannot hold the migration transaction: pass `pool` or `connectionString`, or set `autoMigrate: false` and run CNSPostgresStimulationSchema.migrate(pool) yourself'\n );\n }\n this.now = opts.now ?? Date.now;\n }\n\n private schemaReady(): Promise<void> {\n if (!this.autoMigrate) return Promise.resolve();\n this.ready ??= CNSPostgresStimulationSchema.migrate(this.pool as Pool, this.p).catch(error => {\n this.ready = undefined;\n throw error;\n });\n return this.ready;\n }\n\n /** Run `fn` in a transaction when a Pool is available; else best-effort inline. */\n private async tx<T>(fn: (db: ICNSPgQueryable) => 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 ICNSPgQueryable);\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: TCNSStimulationState): Promise<void> {\n await this.schemaReady();\n const { p } = this;\n const now = this.now();\n await this.tx(async db => {\n await this.lockStimulation(db, s.stimulationId);\n const upserted = await db.query(\n `INSERT INTO ${p}stimulation (stimulation_id, entry, status, scope_name, progress, updated_at)\n VALUES ($1, $2::jsonb, $3, $4, $5::jsonb, $6)\n ON CONFLICT (stimulation_id)\n DO UPDATE SET entry = EXCLUDED.entry, status = EXCLUDED.status,\n scope_name = EXCLUDED.scope_name, progress = EXCLUDED.progress, updated_at = EXCLUDED.updated_at\n RETURNING (xmax = 0) AS inserted`,\n [s.stimulationId, j(s.entry), s.status, s.scopeName ?? null, j(s.progress), now]\n );\n if ((upserted.rows[0] as { inserted?: boolean } | undefined)?.inserted !== false) await this.rollUpAttempts(db, s.stimulationId, now);\n });\n }\n\n async saveAttempt(a: TCNSStimulationAttempt): Promise<void> {\n await this.schemaReady();\n const { p } = this;\n await this.tx(async db => {\n await this.lockStimulation(db, a.stimulationId);\n await 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 await this.rollUpAttempts(db, a.stimulationId, this.now());\n });\n }\n\n private async lockStimulation(db: ICNSPgQueryable, stimulationId: string): Promise<void> {\n await db.query('SELECT pg_advisory_xact_lock($1, hashtext($2))', [\n CNSPostgresStimulationRepository.LOCK_STIMULATION,\n this.p + stimulationId,\n ]);\n }\n\n private async rollUpAttempts(db: ICNSPgQueryable, stimulationId: string, now: number): Promise<void> {\n const { p } = this;\n await db.query(\n `UPDATE ${p}stimulation s\n SET started_at = COALESCE(a.started_at, 0),\n hop_count = COALESCE(a.hop_count, 0),\n last_completed_at = a.last_completed_at,\n updated_at = GREATEST(s.updated_at, $2)\n FROM (\n SELECT MIN(started_at) AS started_at,\n SUM(hop_count) AS hop_count,\n (ARRAY_AGG(completed_at ORDER BY attempt_number DESC))[1] AS last_completed_at\n FROM ${p}stimulation_attempt WHERE stimulation_id = $1\n ) a\n WHERE s.stimulation_id = $1`,\n [stimulationId, now]\n );\n }\n\n async appendTask(t: TCNSStimulationTask): Promise<void> {\n await this.schemaReady();\n const { p } = this;\n await this.db.query(\n `INSERT INTO ${p}stimulation_task\n (stimulation_attempt_id, task_index, neuron_name, dendrite_collateral_name,\n input_index, output, status, error, started_at, duration_ms, activation_context)\n VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7,$8,$9,$10,$11::jsonb)\n ON CONFLICT (stimulation_attempt_id, task_index, started_at) 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 t.activationContext ? j(t.activationContext) : null,\n ]\n );\n }\n\n // ── read ──\n\n async getStimulation(\n stimulationId: string\n ): Promise<TCNSStimulationState | undefined> {\n await this.schemaReady();\n const { p } = this;\n const res = await this.db.query(\n `SELECT stimulation_id, entry, status, scope_name, progress FROM ${p}stimulation WHERE stimulation_id = $1`,\n [stimulationId]\n );\n const row = res.rows[0] as TStimulationRow | undefined;\n return row ? rowToStimulation(row) : undefined;\n }\n\n async listStimulations(filter?: {\n scopeName?: string;\n since?: number;\n }): Promise<TCNSStimulationState[]> {\n await this.schemaReady();\n const { p } = this;\n const scope = filter?.scopeName ?? null;\n // `since` prunes on `updated_at`, the column this already orders by. It\n // is a prune and not the filter: a run's `started_at` is the minimum over\n // its attempts, and a write always follows the start, so this keeps a\n // superset and the caller's predicate still decides. There is no prune\n // for `until` — a run that started before it may have been written after.\n const since = filter?.since ?? null;\n const res = await this.db.query(\n `SELECT stimulation_id, entry, status, scope_name, progress FROM ${p}stimulation\n WHERE ($1::text IS NULL OR scope_name = $1)\n AND ($2::bigint IS NULL OR updated_at >= $2)\n ORDER BY updated_at DESC`,\n [scope, since]\n );\n return (res.rows as TStimulationRow[]).map(rowToStimulation);\n }\n\n async getAttempts(\n stimulationId: string\n ): Promise<TCNSStimulationAttempt[]> {\n await this.schemaReady();\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<TCNSStimulationTask[]> {\n await this.schemaReady();\n const { p } = this;\n const res = await this.db.query(\n `SELECT * FROM ${p}stimulation_task WHERE stimulation_attempt_id = $1 ORDER BY task_index`,\n [stimulationAttemptId]\n );\n return (res.rows as TTaskRow[]).map(rowToTask);\n }\n\n /**\n * Attempts of many stimulations in ONE query, so a page of runs costs one\n * round trip rather than one per run.\n */\n async getAttemptsFor(\n stimulationIds: readonly string[]\n ): Promise<TCNSStimulationAttempt[]> {\n if (stimulationIds.length === 0) return [];\n await this.schemaReady();\n const { p } = this;\n const res = await this.db.query(\n `SELECT * FROM ${p}stimulation_attempt\n WHERE stimulation_id = ANY($1)\n ORDER BY stimulation_id, attempt_number`,\n [stimulationIds as string[]]\n );\n return (res.rows as TAttemptRow[]).map(rowToAttempt);\n }\n\n /** Tasks of many attempts in ONE query — see {@link getAttemptsFor}. */\n async getTasksFor(\n stimulationAttemptIds: readonly string[]\n ): Promise<TCNSStimulationTask[]> {\n if (stimulationAttemptIds.length === 0) return [];\n await this.schemaReady();\n const { p } = this;\n const res = await this.db.query(\n `SELECT * FROM ${p}stimulation_task\n WHERE stimulation_attempt_id = ANY($1)\n ORDER BY stimulation_attempt_id, task_index`,\n [stimulationAttemptIds as string[]]\n );\n return (res.rows as TTaskRow[]).map(rowToTask);\n }\n\n // ── roster query capability (filter + keyset + facets pushed into SQL) ──\n\n /**\n * The CTE every roster read stands on. The roster fields are kept on the\n * stimulation row by `saveAttempt` / `saveStimulation`, so the window and the\n * order come from `stimulation(started_at DESC, stimulation_id)`; the total\n * still counts the whole filtered set. Shared so the facet tallies are counted\n * over exactly the set the roster pages through.\n * The task table is read only when `withNeurons` asks for the neuron names.\n * It binds `$1..$7` (the filter); the keyset cursor belongs to the roster's tail.\n */\n private rosterCte(withNeurons: boolean): string {\n const { p } = this;\n const neurons = withNeurons\n ? `COALESCE((\n SELECT ARRAY_AGG(DISTINCT t.neuron_name)\n FROM ${p}stimulation_task t\n JOIN ${p}stimulation_attempt a ON a.stimulation_attempt_id = t.stimulation_attempt_id\n WHERE a.stimulation_id = s.stimulation_id\n ), ARRAY[]::text[])`\n : 'ARRAY[]::text[]';\n return `\n WITH roster AS (\n SELECT s.stimulation_id, s.entry, s.status, s.scope_name, s.started_at,\n CASE WHEN s.status = 'running' THEN NULL ELSE s.last_completed_at END AS completed_at,\n s.hop_count,\n (s.status = 'failed') AS has_error,\n ${neurons} AS neurons\n FROM ${p}stimulation s\n WHERE ($6::bigint IS NULL OR s.started_at >= $6)\n AND ($7::bigint IS NULL OR s.started_at <= $7)\n AND ($1::text IS NULL OR s.scope_name = $1)\n AND ($2::text IS NULL OR s.status = $2)\n AND ($3::text IS NULL OR s.entry->>'collateralName' = $3)\n AND ($5::text IS NULL OR strpos(lower(s.stimulation_id), $5) > 0)\n ),\n filtered AS (\n SELECT *, COUNT(*) OVER() AS total_count FROM roster\n WHERE ($4::text IS NULL OR $4 = ANY(neurons))\n )`;\n }\n\n private async selectRoster(\n filter: TCNSStimulationRosterFilter,\n opts?: { cursor?: string; limit?: number }\n ): Promise<{ entries: TCNSRosterEntry[]; total: number }> {\n await this.schemaReady();\n const { p } = this;\n // Decode the keyset cursor → the (startedAt, stimulationId) to page past.\n let curStarted: number | null = null;\n let curId: string | null = null;\n if (opts?.cursor) {\n // Split on the FIRST separator only. A stimulation id is a key the\n // caller chose — a BullMQ job id, an order number — and one\n // containing `|` would otherwise lose everything after it, sending\n // the page to a run that does not exist.\n const decoded = Buffer.from(opts.cursor, 'base64').toString('utf8');\n const at = decoded.indexOf('|');\n const started = Number(decoded.slice(0, at));\n if (at >= 0 && Number.isFinite(started)) {\n curStarted = started;\n curId = decoded.slice(at + 1);\n }\n }\n const params: unknown[] = [\n filter.scopeName ?? null,\n filter.status ?? null,\n filter.entryCollateralName ?? null,\n filter.neuronName ?? null,\n filter.text ? filter.text.toLowerCase() : null,\n filter.since ?? null,\n filter.until ?? null,\n curStarted,\n curId,\n ];\n const limitClause =\n opts?.limit != null && Number.isFinite(opts.limit) ? ` LIMIT ${Math.max(0, Math.floor(opts.limit))}` : '';\n // The listing discards the neuron array; only a neuron filter reads it.\n const sql = `${this.rosterCte(filter.neuronName != null)}\n -- The count is taken INSIDE \\`filtered\\`, before the keyset predicate\n -- narrows the set. Counting after it made \\`total\\` shrink with every\n -- page, while the in-memory reference reports the whole filtered set —\n -- and the two are contractually identical.\n SELECT * FROM filtered\n WHERE ($8::bigint IS NULL OR (started_at, stimulation_id COLLATE \"C\") < ($8, $9::text COLLATE \"C\"))\n ORDER BY started_at DESC, stimulation_id COLLATE \"C\" DESC${limitClause}`;\n const res = await this.db.query(sql, params);\n const rows = res.rows as TRosterRow[];\n // An empty page carries no row to read the window count from — reporting\n // zero there would tell the panel \"no runs\" for a filter that has plenty.\n // Re-run the SAME query with the cursor dropped rather than maintaining a\n // second hand-written count that can drift from this one.\n let total = rows.length ? Number(rows[0].total_count) : 0;\n if (!rows.length && curStarted !== null) {\n const countRes = await this.db.query(sql, [\n ...params.slice(0, 7),\n null,\n null,\n ]);\n const countRows = countRes.rows as TRosterRow[];\n total = countRows.length ? Number(countRows[0].total_count) : 0;\n }\n const entries: TCNSRosterEntry[] = rows.map(r => ({\n row: {\n stimulationId: r.stimulation_id,\n scopeName: r.scope_name ?? undefined,\n entry: r.entry as TCNSSignalJSON,\n status: r.status,\n startedAt: Number(r.started_at),\n completedAt: r.completed_at == null ? null : Number(r.completed_at),\n hopCount: Number(r.hop_count),\n hasError: r.has_error,\n },\n neurons: r.neurons ?? [],\n }));\n return { entries, total };\n }\n\n async queryRoster(\n filter: TCNSStimulationRosterFilter,\n opts: { cursor?: string; limit: number }\n ): Promise<TCNSStimulationRosterPage> {\n // Ask for one row beyond the page. Its presence is what says \"there is\n // another page\" — `total` cannot answer that any more, now that it counts\n // the whole filtered set and knows nothing about how far this cursor walked.\n const { entries, total } = await this.selectRoster(filter, {\n ...opts,\n limit: opts.limit + 1,\n });\n const hasMore = entries.length > opts.limit;\n const rows: TCNSStimulationRosterRow[] = entries\n .slice(0, opts.limit)\n .map(e => e.row);\n const lastRow = rows[rows.length - 1];\n return {\n rows,\n nextCursor: hasMore && lastRow ? encodeCNSRosterCursor(lastRow) : null,\n total,\n };\n }\n\n async queryFacets(\n filter: TCNSStimulationRosterFilter,\n opts?: { metricPath?: string }\n ): Promise<TCNSStimulationRosterFacets> {\n await this.schemaReady();\n // Four tallies of a few dozen rows. Aggregated here rather than in Node so\n // what crosses the wire is the answer, not the material for it.\n const params: unknown[] = [\n filter.scopeName ?? null,\n filter.status ?? null,\n filter.entryCollateralName ?? null,\n filter.neuronName ?? null,\n filter.text ? filter.text.toLowerCase() : null,\n filter.since ?? null,\n filter.until ?? null,\n // The metric path as a jsonb path array (`a.b` → {a,b}); null when the\n // chart is not summing anything.\n opts?.metricPath ? opts.metricPath.split('.') : null,\n ];\n\n const sql = `${this.rosterCte(true)},\n -- The histogram's bucket width is derived from the set's own span, the\n -- same ${CNS_HISTOGRAM_BUCKETS}-bucket rule the reference implementation\n -- uses. Computed here so the whole thing stays one round trip.\n bounds AS (\n SELECT MIN(started_at) AS mn, MAX(started_at) AS mx FROM filtered WHERE started_at > 0\n ),\n step AS (\n SELECT mn,\n GREATEST(1, CEIL((mx - mn)::numeric / ${CNS_HISTOGRAM_BUCKETS})::bigint) AS size\n FROM bounds\n )\n SELECT 'entry' AS kind, entry->>'collateralName' AS key,\n COUNT(*)::bigint AS cnt,\n NULL::bigint AS ts_bucket, NULL::bigint AS completed,\n NULL::bigint AS failed, NULL::bigint AS running,\n NULL::numeric AS metric\n FROM filtered GROUP BY 2\n UNION ALL\n SELECT 'status', status, COUNT(*)::bigint,\n NULL, NULL, NULL, NULL, NULL\n FROM filtered GROUP BY 2\n UNION ALL\n -- One row per (run, neuron): the neuron rail counts runs that touched\n -- a neuron, which is what UNNEST over the per-run DISTINCT array gives.\n SELECT 'neuron', n, COUNT(*)::bigint,\n NULL, NULL, NULL, NULL, NULL\n FROM filtered, UNNEST(neurons) AS n GROUP BY 2\n UNION ALL\n SELECT 'time', NULL, NULL,\n (step.mn + ((f.started_at - step.mn) / step.size) * step.size)::bigint,\n COUNT(*) FILTER (WHERE f.status = 'completed')::bigint,\n COUNT(*) FILTER (WHERE f.status = 'failed')::bigint,\n COUNT(*) FILTER (WHERE f.status NOT IN ('completed', 'failed'))::bigint,\n -- Only actual JSON numbers are summed, matching the reference's\n -- \\`typeof v === 'number'\\`: a payload field holding \"12\" as a\n -- string is not a metric and must not become one here.\n SUM(\n CASE WHEN $8::text[] IS NOT NULL\n AND jsonb_typeof(f.entry->'payload' #> $8::text[]) = 'number'\n THEN (f.entry->'payload' #> $8::text[])::text::numeric\n END\n )\n FROM filtered f, step\n WHERE f.started_at > 0\n GROUP BY 4`;\n\n const res = await this.db.query(sql, params);\n const rows = res.rows as TFacetRow[];\n\n const tally = (kind: string) =>\n rows\n .filter(r => r.kind === kind && r.key != null)\n .map(r => ({ key: r.key as string, count: Number(r.cnt) }))\n // Deterministic on ties: a facet rail that reshuffles between two\n // refreshes of the same data reads as data changing.\n .sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1));\n\n return {\n byEntryCollateral: tally('entry').map(x => ({\n name: x.key,\n count: x.count,\n })),\n byStatus: tally('status').map(x => ({\n status: x.key,\n count: x.count,\n })),\n byNeuron: tally('neuron').map(x => ({ name: x.key, count: x.count })),\n byTime: rows\n .filter(r => r.kind === 'time' && r.ts_bucket != null)\n .map(r => ({\n tsBucket: Number(r.ts_bucket),\n completed: Number(r.completed),\n failed: Number(r.failed),\n running: Number(r.running),\n // Absent rather than 0 when nothing was summed — the reference\n // only sets `metric` once it has a number to put there.\n ...(r.metric == null ? {} : { metric: Number(r.metric) }),\n }))\n .sort((a, b) => a.tsBucket - b.tsBucket),\n };\n }\n\n // ── lifecycle ──\n\n async delete(stimulationId: string): Promise<void> {\n await this.schemaReady();\n const { p } = this;\n await this.tx(async db => {\n await this.lockStimulation(db, stimulationId);\n await db.query(\n `DELETE FROM ${p}stimulation_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(`DELETE FROM ${p}stimulation_attempt WHERE stimulation_id = $1`, [stimulationId]);\n await db.query(`DELETE FROM ${p}stimulation WHERE stimulation_id = $1`, [stimulationId]);\n });\n }\n\n /**\n * Retention: prune stimulations not updated for `olderThanMs`, with their\n * attempts and tasks, in batches of one transaction each, then the attempts and\n * tasks older than that whose parent row never arrived or is already gone; last,\n * with a pool, drop the daily task partitions that ended before the window and\n * create the next seven.\n * Returns the number of stimulations removed.\n */\n async deleteStale(olderThanMs: number): Promise<number> {\n await this.schemaReady();\n const { p } = this;\n const now = this.now();\n const cutoff = now - Math.max(0, Math.floor(olderThanMs));\n let removedCount = 0;\n let after = '';\n for (;;) {\n const batch = await this.tx(async db => {\n const stale = await db.query(\n `SELECT stimulation_id FROM ${p}stimulation\n WHERE updated_at < $1 AND stimulation_id > $2\n ORDER BY stimulation_id LIMIT $3`,\n [cutoff, after, CNSPostgresStimulationRepository.RETENTION_BATCH]\n );\n const candidates = (stale.rows as { stimulation_id: string }[]).map(row => row.stimulation_id);\n if (candidates.length === 0) return { candidates, removed: 0 };\n await db.query(\n `SELECT pg_advisory_xact_lock($1, hashtext($2 || id)) FROM unnest($3::text[]) AS id ORDER BY id`,\n [CNSPostgresStimulationRepository.LOCK_STIMULATION, p, candidates]\n );\n const removed = await db.query(\n `DELETE FROM ${p}stimulation WHERE stimulation_id = ANY($1) AND updated_at < $2 RETURNING stimulation_id`,\n [candidates, cutoff]\n );\n const ids = (removed.rows as { stimulation_id: string }[]).map(row => row.stimulation_id);\n await db.query(\n `DELETE FROM ${p}stimulation_task t\n USING ${p}stimulation_attempt a\n WHERE a.stimulation_attempt_id = t.stimulation_attempt_id AND a.stimulation_id = ANY($1)`,\n [ids]\n );\n await db.query(`DELETE FROM ${p}stimulation_attempt WHERE stimulation_id = ANY($1)`, [ids]);\n return { candidates, removed: ids.length };\n });\n removedCount += batch.removed;\n if (batch.candidates.length < CNSPostgresStimulationRepository.RETENTION_BATCH) break;\n after = batch.candidates[batch.candidates.length - 1];\n }\n let rotationFailure: { error: unknown } | undefined;\n if (this.pool) {\n try {\n await this.taskPartitions.rotate({ transaction: work => this.tx(db => work(db as unknown as ICNSPostgresSchemaConnection)) }, `${p}stimulation_task`, now, cutoff);\n } catch (error) {\n rotationFailure = { error };\n }\n }\n await this.db.query(\n `DELETE FROM ${p}stimulation_attempt a\n WHERE a.started_at < $1\n AND NOT EXISTS (SELECT 1 FROM ${p}stimulation s WHERE s.stimulation_id = a.stimulation_id)`,\n [cutoff]\n );\n await this.db.query(\n `DELETE FROM ${p}stimulation_task t\n WHERE t.started_at < $1\n AND NOT EXISTS (SELECT 1 FROM ${p}stimulation_attempt a WHERE a.stimulation_attempt_id = t.stimulation_attempt_id)`,\n [cutoff]\n );\n if (rotationFailure) throw rotationFailure.error;\n return removedCount;\n }\n\n async close(): Promise<void> {\n if (this.ownsPool && this.pool) await this.pool.end();\n }\n\n}\n\n// ── row shapes (jsonb columns come back already parsed by node-pg) ──\ntype TStimulationRow = {\n stimulation_id: string;\n entry: TCNSSignalJSON;\n status: ECNSStimulationStatus;\n scope_name: string | null;\n progress: TCNSProgress;\n};\ntype TAttemptRow = {\n stimulation_attempt_id: string;\n stimulation_id: string;\n attempt_number: number;\n status: ECNSStimulationStatus;\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: TCNSSignalJSON[];\n};\ntype TTaskRow = {\n stimulation_attempt_id: string;\n task_index: number;\n neuron_name: string;\n dendrite_collateral_name: string;\n input_index: number;\n output: TCNSSignalJSON | null;\n status: ECNSStimulationTaskStatus;\n error: string | null;\n started_at: string | number;\n duration_ms: string | number | null;\n activation_context: TCNSStimulationTask['activationContext'] | null;\n};\n/** One row of the facet union: a tally row (`key`+`cnt`) or a histogram bucket. */\ntype TFacetRow = {\n kind: string;\n key: string | null;\n cnt: string | null;\n ts_bucket: string | null;\n completed: string | null;\n failed: string | null;\n running: string | null;\n metric: string | null;\n};\n\n/** One roster CTE row (see `selectRoster`), with the window total attached. */\ntype TRosterRow = {\n stimulation_id: string;\n entry: unknown;\n status: string;\n scope_name: string | null;\n started_at: string | number;\n completed_at: string | number | null;\n hop_count: string | number;\n has_error: boolean;\n neurons: string[] | null;\n total_count: string | number;\n};\n\nfunction rowToAttempt(a: TAttemptRow): TCNSStimulationAttempt {\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): TCNSStimulationTask {\n return {\n stimulationAttemptId: t.stimulation_attempt_id,\n index: num(t.task_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_ms),\n ...(t.activation_context != null ? { activationContext: t.activation_context } : {}),\n };\n}\n\nfunction rowToStimulation(r: TStimulationRow): TCNSStimulationState {\n return {\n stimulationId: r.stimulation_id,\n entry: r.entry,\n status: r.status,\n progress: r.progress,\n ...(r.scope_name != null ? { scopeName: r.scope_name } : {}),\n };\n}\n"],"mappings":"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","names":["Pool","encodeCNSRosterCursor","CNS_HISTOGRAM_BUCKETS","j","v","JSON","stringify","num","Number","numOrNull","CNSPostgresStimulationRepository","opts","db","pool","ownsPool","p","autoMigrate","now","taskPartitions","CNSPostgresSchemaPartitionRotation","premakeDays","ready","connectionString","Pool","queryable","Error","CNSPostgresStimulationSchema","assertTablePrefix","tablePrefix","DEFAULT_TABLE_PREFIX","Date","schemaReady","Promise","resolve","migrate","catch","error","undefined","tx","fn","client","connect","query","out","e","release","saveStimulation","s","lockStimulation","stimulationId","upserted","entry","status","scopeName","progress","rows","inserted","rollUpAttempts","saveAttempt","a","stimulationAttemptId","attemptNumber","startedAt","completedAt","hopCount","hasError","replayOf","LOCK_STIMULATION","appendTask","t","index","neuronName","dendriteCollateralName","inputIndex","output","duration","activationContext","getStimulation","res","row","rowToStimulation","listStimulations","filter","scope","since","map","getAttempts","rowToAttempt","getTasks","rowToTask","getAttemptsFor","stimulationIds","length","getTasksFor","stimulationAttemptIds","rosterCte","withNeurons","neurons","selectRoster","curStarted","curId","cursor","decoded","Buffer","from","toString","at","indexOf","started","slice","isFinite","params","entryCollateralName","text","toLowerCase","until","limitClause","limit","Math","max","floor","sql","total","total_count","countRes","countRows","entries","r","_a","_b","stimulation_id","scope_name","started_at","completed_at","hop_count","has_error","queryRoster","hasMore","lastRow","nextCursor","encodeCNSRosterCursor","queryFacets","metricPath","split","CNS_HISTOGRAM_BUCKETS","tally","kind","key","count","cnt","sort","b","byEntryCollateral","x","name","byStatus","byNeuron","byTime","ts_bucket","tsBucket","completed","failed","running","metric","delete","deleteStale","olderThanMs","cutoff","removedCount","after","batch","stale","RETENTION_BATCH","candidates","removed","ids","rotationFailure","rotate","transaction","work","close","end","stimulation_attempt_id","attempt_number","Boolean","replay_of","task_index","neuron_name","dendrite_collateral_name","input_index","duration_ms","activation_context"]}
package/package.json CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@cnstra/persist-postgres",
3
- "version": "0.2.0",
4
- "description": "Postgres relational store for CNStra resumable execution (ICNSStimulationRepository: run/attempt/task) — owns its schema + migrations",
3
+ "version": "0.5.0",
4
+ "description": "Postgres relational store for CNStra resumable execution (ICNSStimulationRepository: run/attempt/task) \u2014 owns its schema + migrations",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/abaikov/cnstra.git",
8
+ "directory": "packages/persist-postgres"
9
+ },
5
10
  "keywords": [
6
11
  "cnstra",
7
12
  "postgres",
@@ -29,14 +34,15 @@
29
34
  "dist"
30
35
  ],
31
36
  "scripts": {
37
+ "prepublishOnly": "node ../../scripts/check-drift.mjs --gate",
32
38
  "build": "tsup",
33
39
  "test": "jest",
34
40
  "test:types": "tsc --noEmit",
35
41
  "clean": "rm -rf dist"
36
42
  },
37
43
  "peerDependencies": {
38
- "@cnstra/persist": "^0.1.0",
39
- "@cnstra/persist-dto": "^0.1.0",
44
+ "@cnstra/persist": "^0.2.0",
45
+ "@cnstra/persist-dto": "^0.2.0",
40
46
  "pg": ">=8"
41
47
  },
42
48
  "peerDependenciesMeta": {
@@ -45,8 +51,8 @@
45
51
  }
46
52
  },
47
53
  "devDependencies": {
48
- "@cnstra/persist": "^0.1.0",
49
- "@cnstra/persist-dto": "^0.1.0",
54
+ "@cnstra/persist": "^0.2.0",
55
+ "@cnstra/persist-dto": "^0.2.0",
50
56
  "@types/jest": "29.5.12",
51
57
  "@types/node": "^20.0.0",
52
58
  "@types/pg": "^8.11.0",
@@ -54,7 +60,8 @@
54
60
  "pg": "^8.11.0",
55
61
  "ts-jest": "29.1.2",
56
62
  "tsup": "8.5.1",
57
- "typescript": "5.7.2"
63
+ "typescript": "5.7.2",
64
+ "@cnstra/postgres-schema": "0.1.0"
58
65
  },
59
66
  "author": "Andrei Baikov <abaikov23@gmail.com>",
60
67
  "license": "MIT",
@@ -1,94 +0,0 @@
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
- // src/migrate.ts
7
- var DEFAULT_PREFIX = "cns_";
8
- function assertSafePrefix(prefix) {
9
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(prefix)) {
10
- throw new Error(`[@cnstra/persist-postgres] unsafe table prefix: ${JSON.stringify(prefix)}`);
11
- }
12
- return prefix;
13
- }
14
- __name(assertSafePrefix, "assertSafePrefix");
15
- function persistPostgresSchemaSql(prefix = DEFAULT_PREFIX) {
16
- const p = assertSafePrefix(prefix);
17
- return `
18
- CREATE TABLE IF NOT EXISTS ${p}stimulation (
19
- stimulation_id text PRIMARY KEY,
20
- entry jsonb NOT NULL,
21
- status text NOT NULL,
22
- scope_name text,
23
- created_at timestamptz NOT NULL DEFAULT now(),
24
- updated_at timestamptz NOT NULL DEFAULT now()
25
- );
26
- CREATE INDEX IF NOT EXISTS ${p}stimulation_updated_idx ON ${p}stimulation (updated_at DESC);
27
- CREATE INDEX IF NOT EXISTS ${p}stimulation_scope_idx ON ${p}stimulation (scope_name);
28
-
29
- CREATE TABLE IF NOT EXISTS ${p}stimulation_frontier (
30
- stimulation_id text NOT NULL REFERENCES ${p}stimulation(stimulation_id) ON DELETE CASCADE,
31
- ord int NOT NULL,
32
- neuron_name text NOT NULL,
33
- dendrite_collateral_name text NOT NULL,
34
- input jsonb,
35
- PRIMARY KEY (stimulation_id, ord)
36
- );
37
-
38
- -- NOTE: no FK on stimulation_id \u2014 over the devtools wire the persistor's
39
- -- saveStimulation and saveAttempt ride SEPARATE async batches processed
40
- -- concurrently by the server, so an attempt insert can reach Postgres before its
41
- -- stimulation row's transaction commits; a hard FK would reject it (23503). Same
42
- -- exposure as ${p}task; delete() removes attempts explicitly instead of cascading.
43
- CREATE TABLE IF NOT EXISTS ${p}stimulation_attempt (
44
- stimulation_attempt_id text PRIMARY KEY,
45
- stimulation_id text NOT NULL,
46
- attempt_number int NOT NULL,
47
- status text NOT NULL,
48
- started_at bigint NOT NULL,
49
- completed_at bigint,
50
- hop_count int NOT NULL,
51
- has_error boolean NOT NULL,
52
- replay_of text,
53
- entry jsonb NOT NULL,
54
- UNIQUE (stimulation_id, attempt_number)
55
- );
56
- CREATE INDEX IF NOT EXISTS ${p}stimulation_attempt_idx ON ${p}stimulation_attempt (stimulation_id, attempt_number);
57
- -- roster query: MIN(started_at)/keyset ordering per run.
58
- CREATE INDEX IF NOT EXISTS ${p}stimulation_attempt_started_idx ON ${p}stimulation_attempt (stimulation_id, started_at);
59
-
60
- -- NOTE: no FK on stimulation_attempt_id \u2014 the persistor streams settled tasks
61
- -- (appendTask) as hops complete, BEFORE the attempt marker is flushed (saveAttempt),
62
- -- so a hard FK would reject the insert. delete() removes tasks explicitly instead.
63
- CREATE TABLE IF NOT EXISTS ${p}task (
64
- stimulation_attempt_id text NOT NULL,
65
- "index" int NOT NULL,
66
- neuron_name text NOT NULL,
67
- dendrite_collateral_name text NOT NULL,
68
- input_index int NOT NULL,
69
- output jsonb,
70
- status text NOT NULL,
71
- error text,
72
- started_at bigint NOT NULL,
73
- duration int,
74
- PRIMARY KEY (stimulation_attempt_id, "index")
75
- );
76
- -- roster neurons lens: DISTINCT neuron_name per attempt / neuronName filter.
77
- CREATE INDEX IF NOT EXISTS ${p}task_neuron_idx ON ${p}task (neuron_name);
78
- `;
79
- }
80
- __name(persistPostgresSchemaSql, "persistPostgresSchemaSql");
81
- async function ensureSchema(db, prefix = DEFAULT_PREFIX) {
82
- await db.query(persistPostgresSchemaSql(prefix));
83
- }
84
- __name(ensureSchema, "ensureSchema");
85
-
86
- export {
87
- __name,
88
- __publicField,
89
- DEFAULT_PREFIX,
90
- assertSafePrefix,
91
- persistPostgresSchemaSql,
92
- ensureSchema
93
- };
94
- //# sourceMappingURL=chunk-5FLIKV6I.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/migrate.ts"],"sourcesContent":["/**\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. Tables\n * under a configurable prefix (default `cns_`); the stimulation's `progress` is\n * NORMALISED into `stimulation_frontier` (one row per outstanding task).\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\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-- roster query: MIN(started_at)/keyset ordering per run.\nCREATE INDEX IF NOT EXISTS ${p}stimulation_attempt_started_idx ON ${p}stimulation_attempt (stimulation_id, started_at);\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-- roster neurons lens: DISTINCT neuron_name per attempt / neuronName filter.\nCREATE INDEX IF NOT EXISTS ${p}task_neuron_idx ON ${p}task (neuron_name);\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":";;;;;;AAcO,IAAMA,iBAAiB;AAGvB,SAASC,iBAAiBC,QAAc;AAC3C,MAAI,CAAC,2BAA2BC,KAAKD,MAAAA,GAAS;AAC1C,UAAM,IAAIE,MACN,mDAAmDC,KAAKC,UAAUJ,MAAAA,CAAAA,EAAS;EAEnF;AACA,SAAOA;AACX;AAPgBD;AAUT,SAASM,yBAAyBL,SAAiBF,gBAAc;AACpE,QAAMQ,IAAIP,iBAAiBC,MAAAA;AAC3B,SAAO;6BACkBM,CAAAA;;;;;;;;6BAQAA,CAAAA,8BAA+BA,CAAAA;6BAC/BA,CAAAA,4BAA6BA,CAAAA;;6BAE7BA,CAAAA;8CACiBA,CAAAA;;;;;;;;;;;;iBAY7BA,CAAAA;6BACYA,CAAAA;;;;;;;;;;;;;6BAaAA,CAAAA,8BAA+BA,CAAAA;;6BAE/BA,CAAAA,sCAAuCA,CAAAA;;;;;6BAKvCA,CAAAA;;;;;;;;;;;;;;6BAcAA,CAAAA,sBAAuBA,CAAAA;;AAEpD;AAhEgBD;AAmEhB,eAAsBE,aAClBC,IACAR,SAAiBF,gBAAc;AAE/B,QAAMU,GAAGC,MAAMJ,yBAAyBL,MAAAA,CAAAA;AAC5C;AALsBO;","names":["DEFAULT_PREFIX","assertSafePrefix","prefix","test","Error","JSON","stringify","persistPostgresSchemaSql","p","ensureSchema","db","query"]}