@cnstra/persist-postgres 0.3.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.
package/dist/index.d.mts CHANGED
@@ -1,80 +1,66 @@
1
- import { Pool } from 'pg';
2
1
  import { ICNSStimulationRepository, ICNSStimulationRosterQuery } from '@cnstra/persist';
3
2
  import { TCNSStimulationState, TCNSStimulationAttempt, TCNSStimulationTask, TCNSStimulationRosterFilter, TCNSStimulationRosterPage, TCNSStimulationRosterFacets } from '@cnstra/persist-dto';
3
+ import { Pool } from 'pg';
4
4
 
5
- /**
6
- * Schema / migrations for the CNStra stimulation/attempt/task relational store.
7
- *
8
- * The package OWNS this schema — run it explicitly via `cnstra-persist-postgres migrate`
9
- * or `ensureSchema(db)`, or let the repository auto-create it on first use. Tables
10
- * under a configurable prefix (default `cns_`); the stimulation's `progress` is
11
- * NORMALISED into `stimulation_frontier` (one row per outstanding task).
12
- */
13
- /** Minimal pg client surface (a `pg` Pool or Client satisfies it). */
14
- interface IPgQueryable {
5
+ interface ICNSPgQueryable {
15
6
  query(text: string, params?: unknown[]): Promise<{
16
7
  rows: unknown[];
17
8
  }>;
18
9
  }
19
- declare const DEFAULT_PREFIX = "cns_";
20
- /** Guard the table prefix (it is interpolated into DDL/DML, not parameterised). */
21
- declare function assertSafePrefix(prefix: string): string;
22
- /** The full CREATE TABLE + CREATE INDEX DDL (idempotent). */
23
- declare function persistPostgresSchemaSql(prefix?: string): string;
24
- /** Idempotently create all tables + indexes. Safe to run repeatedly. */
25
- declare function ensureSchema(db: IPgQueryable, prefix?: string): Promise<void>;
26
-
27
- /**
28
- * Postgres relational store for the CNStra stimulation/attempt/task resumable model
29
- * (`ICNSStimulationRepository`). The stimulation's `progress` is NORMALISED: the
30
- * outstanding frontier into `<prefix>stimulation_frontier` (one row per task) —
31
- * rebuilt into `TCNSProgress` on read.
32
- *
33
- * Reuses the `IPgQueryable` seam so `pg` is an optional peer: inject any pg
34
- * client/pool, or pass a `connectionString` for the repo to own a Pool. Behaviourally
35
- * matches `CNSInMemoryStimulationRepository` (listStimulations newest-first, getAttempts
36
- * by attemptNumber, getTasks by index, delete cascades).
37
- */
38
10
 
39
- interface TCNSPostgresStimulationRepositoryOptions {
11
+ type TCNSPostgresStimulationRepositoryOptions = {
40
12
  connectionString?: string;
41
- /** Inject an existing pg Pool (transactions supported; not closed by `close()`). */
42
13
  pool?: Pool;
43
- /** Inject any queryable (reads + best-effort non-transactional writes). */
44
- queryable?: IPgQueryable;
45
- /** Table prefix (default `cns_`). */
14
+ queryable?: ICNSPgQueryable;
46
15
  tablePrefix?: string;
47
- /** Lazily ensure the schema on first use (default true). */
48
16
  autoMigrate?: boolean;
49
- }
17
+ now?: () => number;
18
+ };
19
+
50
20
  declare class CNSPostgresStimulationRepository implements ICNSStimulationRepository, ICNSStimulationRosterQuery {
21
+ private static readonly LOCK_STIMULATION;
22
+ private static readonly RETENTION_BATCH;
51
23
  private readonly db;
52
24
  private readonly pool?;
53
25
  private readonly ownsPool;
54
26
  private readonly p;
55
27
  private readonly autoMigrate;
28
+ private readonly now;
29
+ private readonly taskPartitions;
56
30
  private ready;
57
31
  constructor(opts: TCNSPostgresStimulationRepositoryOptions);
58
- private ensure;
32
+ private schemaReady;
59
33
  /** Run `fn` in a transaction when a Pool is available; else best-effort inline. */
60
34
  private tx;
61
35
  saveStimulation(s: TCNSStimulationState): Promise<void>;
62
36
  saveAttempt(a: TCNSStimulationAttempt): Promise<void>;
37
+ private lockStimulation;
38
+ private rollUpAttempts;
63
39
  appendTask(t: TCNSStimulationTask): Promise<void>;
64
40
  getStimulation(stimulationId: string): Promise<TCNSStimulationState | undefined>;
65
41
  listStimulations(filter?: {
66
42
  scopeName?: string;
43
+ since?: number;
67
44
  }): Promise<TCNSStimulationState[]>;
68
45
  getAttempts(stimulationId: string): Promise<TCNSStimulationAttempt[]>;
69
46
  getTasks(stimulationAttemptId: string): Promise<TCNSStimulationTask[]>;
70
47
  /**
71
- * Derive every filtered roster row in one CTE: `startedAt` = MIN(attempt),
72
- * `completedAt` = the latest attempt's, `hopCount` = SUM, `hasError` from
73
- * status, and the DISTINCT activated neuron names (for the neurons lens +
74
- * filter). Newest-first (`startedAt DESC, stimulationId DESC`) so the keyset
75
- * cursor is stable. `limit` is applied only when paginating (roster); facets
76
- * pull the whole filtered set.
48
+ * Attempts of many stimulations in ONE query, so a page of runs costs one
49
+ * round trip rather than one per run.
77
50
  */
51
+ getAttemptsFor(stimulationIds: readonly string[]): Promise<TCNSStimulationAttempt[]>;
52
+ /** Tasks of many attempts in ONE query — see {@link getAttemptsFor}. */
53
+ getTasksFor(stimulationAttemptIds: readonly string[]): Promise<TCNSStimulationTask[]>;
54
+ /**
55
+ * The CTE every roster read stands on. The roster fields are kept on the
56
+ * stimulation row by `saveAttempt` / `saveStimulation`, so the window and the
57
+ * order come from `stimulation(started_at DESC, stimulation_id)`; the total
58
+ * still counts the whole filtered set. Shared so the facet tallies are counted
59
+ * over exactly the set the roster pages through.
60
+ * The task table is read only when `withNeurons` asks for the neuron names.
61
+ * It binds `$1..$7` (the filter); the keyset cursor belongs to the roster's tail.
62
+ */
63
+ private rosterCte;
78
64
  private selectRoster;
79
65
  queryRoster(filter: TCNSStimulationRosterFilter, opts: {
80
66
  cursor?: string;
@@ -85,15 +71,32 @@ declare class CNSPostgresStimulationRepository implements ICNSStimulationReposit
85
71
  }): Promise<TCNSStimulationRosterFacets>;
86
72
  delete(stimulationId: string): Promise<void>;
87
73
  /**
88
- * Retention: prune stimulations not updated for `olderThanMs` (with their
89
- * attempts/tasks/frontier). A run's `updated_at` stops advancing once it
90
- * settles, so old finished runs age out while an active one stays fresh — the same
91
- * semantics as the in-memory store's `ttlMs` and the pg-boss checkpoint's
92
- * `deleteStale`. Run it on a schedule (cron). Returns the number removed.
74
+ * Retention: prune stimulations not updated for `olderThanMs`, with their
75
+ * attempts and tasks, in batches of one transaction each, then the attempts and
76
+ * tasks older than that whose parent row never arrived or is already gone; last,
77
+ * with a pool, drop the daily task partitions that ended before the window and
78
+ * create the next seven.
79
+ * Returns the number of stimulations removed.
93
80
  */
94
81
  deleteStale(olderThanMs: number): Promise<number>;
95
82
  close(): Promise<void>;
96
- private buildProgress;
97
83
  }
98
84
 
99
- export { CNSPostgresStimulationRepository, DEFAULT_PREFIX, type IPgQueryable, type TCNSPostgresStimulationRepositoryOptions, assertSafePrefix, ensureSchema, persistPostgresSchemaSql };
85
+ type TCNSPostgresStimulationMigrateOptions = {
86
+ readonly contract?: boolean;
87
+ };
88
+
89
+ declare class CNSPostgresStimulationSchema {
90
+ static readonly DEFAULT_TABLE_PREFIX = "cns_";
91
+ private static readonly LONGEST_SUFFIX;
92
+ private static readonly migrator;
93
+ static assertTablePrefix(tablePrefix: string): string;
94
+ static tables(tablePrefix?: string): string[];
95
+ static migrationTable(tablePrefix?: string): string;
96
+ static migrationSql(tablePrefix?: string, appliedSteps?: readonly number[], options?: TCNSPostgresStimulationMigrateOptions): string;
97
+ static migrate(pool: Pool, tablePrefix?: string, options?: TCNSPostgresStimulationMigrateOptions): Promise<void>;
98
+ static verify(database: ICNSPgQueryable, tablePrefix?: string): Promise<void>;
99
+ private static initialSql;
100
+ }
101
+
102
+ export { CNSPostgresStimulationRepository, CNSPostgresStimulationSchema, type ICNSPgQueryable, type TCNSPostgresStimulationMigrateOptions, type TCNSPostgresStimulationRepositoryOptions };
package/dist/index.d.ts CHANGED
@@ -1,80 +1,66 @@
1
- import { Pool } from 'pg';
2
1
  import { ICNSStimulationRepository, ICNSStimulationRosterQuery } from '@cnstra/persist';
3
2
  import { TCNSStimulationState, TCNSStimulationAttempt, TCNSStimulationTask, TCNSStimulationRosterFilter, TCNSStimulationRosterPage, TCNSStimulationRosterFacets } from '@cnstra/persist-dto';
3
+ import { Pool } from 'pg';
4
4
 
5
- /**
6
- * Schema / migrations for the CNStra stimulation/attempt/task relational store.
7
- *
8
- * The package OWNS this schema — run it explicitly via `cnstra-persist-postgres migrate`
9
- * or `ensureSchema(db)`, or let the repository auto-create it on first use. Tables
10
- * under a configurable prefix (default `cns_`); the stimulation's `progress` is
11
- * NORMALISED into `stimulation_frontier` (one row per outstanding task).
12
- */
13
- /** Minimal pg client surface (a `pg` Pool or Client satisfies it). */
14
- interface IPgQueryable {
5
+ interface ICNSPgQueryable {
15
6
  query(text: string, params?: unknown[]): Promise<{
16
7
  rows: unknown[];
17
8
  }>;
18
9
  }
19
- declare const DEFAULT_PREFIX = "cns_";
20
- /** Guard the table prefix (it is interpolated into DDL/DML, not parameterised). */
21
- declare function assertSafePrefix(prefix: string): string;
22
- /** The full CREATE TABLE + CREATE INDEX DDL (idempotent). */
23
- declare function persistPostgresSchemaSql(prefix?: string): string;
24
- /** Idempotently create all tables + indexes. Safe to run repeatedly. */
25
- declare function ensureSchema(db: IPgQueryable, prefix?: string): Promise<void>;
26
-
27
- /**
28
- * Postgres relational store for the CNStra stimulation/attempt/task resumable model
29
- * (`ICNSStimulationRepository`). The stimulation's `progress` is NORMALISED: the
30
- * outstanding frontier into `<prefix>stimulation_frontier` (one row per task) —
31
- * rebuilt into `TCNSProgress` on read.
32
- *
33
- * Reuses the `IPgQueryable` seam so `pg` is an optional peer: inject any pg
34
- * client/pool, or pass a `connectionString` for the repo to own a Pool. Behaviourally
35
- * matches `CNSInMemoryStimulationRepository` (listStimulations newest-first, getAttempts
36
- * by attemptNumber, getTasks by index, delete cascades).
37
- */
38
10
 
39
- interface TCNSPostgresStimulationRepositoryOptions {
11
+ type TCNSPostgresStimulationRepositoryOptions = {
40
12
  connectionString?: string;
41
- /** Inject an existing pg Pool (transactions supported; not closed by `close()`). */
42
13
  pool?: Pool;
43
- /** Inject any queryable (reads + best-effort non-transactional writes). */
44
- queryable?: IPgQueryable;
45
- /** Table prefix (default `cns_`). */
14
+ queryable?: ICNSPgQueryable;
46
15
  tablePrefix?: string;
47
- /** Lazily ensure the schema on first use (default true). */
48
16
  autoMigrate?: boolean;
49
- }
17
+ now?: () => number;
18
+ };
19
+
50
20
  declare class CNSPostgresStimulationRepository implements ICNSStimulationRepository, ICNSStimulationRosterQuery {
21
+ private static readonly LOCK_STIMULATION;
22
+ private static readonly RETENTION_BATCH;
51
23
  private readonly db;
52
24
  private readonly pool?;
53
25
  private readonly ownsPool;
54
26
  private readonly p;
55
27
  private readonly autoMigrate;
28
+ private readonly now;
29
+ private readonly taskPartitions;
56
30
  private ready;
57
31
  constructor(opts: TCNSPostgresStimulationRepositoryOptions);
58
- private ensure;
32
+ private schemaReady;
59
33
  /** Run `fn` in a transaction when a Pool is available; else best-effort inline. */
60
34
  private tx;
61
35
  saveStimulation(s: TCNSStimulationState): Promise<void>;
62
36
  saveAttempt(a: TCNSStimulationAttempt): Promise<void>;
37
+ private lockStimulation;
38
+ private rollUpAttempts;
63
39
  appendTask(t: TCNSStimulationTask): Promise<void>;
64
40
  getStimulation(stimulationId: string): Promise<TCNSStimulationState | undefined>;
65
41
  listStimulations(filter?: {
66
42
  scopeName?: string;
43
+ since?: number;
67
44
  }): Promise<TCNSStimulationState[]>;
68
45
  getAttempts(stimulationId: string): Promise<TCNSStimulationAttempt[]>;
69
46
  getTasks(stimulationAttemptId: string): Promise<TCNSStimulationTask[]>;
70
47
  /**
71
- * Derive every filtered roster row in one CTE: `startedAt` = MIN(attempt),
72
- * `completedAt` = the latest attempt's, `hopCount` = SUM, `hasError` from
73
- * status, and the DISTINCT activated neuron names (for the neurons lens +
74
- * filter). Newest-first (`startedAt DESC, stimulationId DESC`) so the keyset
75
- * cursor is stable. `limit` is applied only when paginating (roster); facets
76
- * pull the whole filtered set.
48
+ * Attempts of many stimulations in ONE query, so a page of runs costs one
49
+ * round trip rather than one per run.
77
50
  */
51
+ getAttemptsFor(stimulationIds: readonly string[]): Promise<TCNSStimulationAttempt[]>;
52
+ /** Tasks of many attempts in ONE query — see {@link getAttemptsFor}. */
53
+ getTasksFor(stimulationAttemptIds: readonly string[]): Promise<TCNSStimulationTask[]>;
54
+ /**
55
+ * The CTE every roster read stands on. The roster fields are kept on the
56
+ * stimulation row by `saveAttempt` / `saveStimulation`, so the window and the
57
+ * order come from `stimulation(started_at DESC, stimulation_id)`; the total
58
+ * still counts the whole filtered set. Shared so the facet tallies are counted
59
+ * over exactly the set the roster pages through.
60
+ * The task table is read only when `withNeurons` asks for the neuron names.
61
+ * It binds `$1..$7` (the filter); the keyset cursor belongs to the roster's tail.
62
+ */
63
+ private rosterCte;
78
64
  private selectRoster;
79
65
  queryRoster(filter: TCNSStimulationRosterFilter, opts: {
80
66
  cursor?: string;
@@ -85,15 +71,32 @@ declare class CNSPostgresStimulationRepository implements ICNSStimulationReposit
85
71
  }): Promise<TCNSStimulationRosterFacets>;
86
72
  delete(stimulationId: string): Promise<void>;
87
73
  /**
88
- * Retention: prune stimulations not updated for `olderThanMs` (with their
89
- * attempts/tasks/frontier). A run's `updated_at` stops advancing once it
90
- * settles, so old finished runs age out while an active one stays fresh — the same
91
- * semantics as the in-memory store's `ttlMs` and the pg-boss checkpoint's
92
- * `deleteStale`. Run it on a schedule (cron). Returns the number removed.
74
+ * Retention: prune stimulations not updated for `olderThanMs`, with their
75
+ * attempts and tasks, in batches of one transaction each, then the attempts and
76
+ * tasks older than that whose parent row never arrived or is already gone; last,
77
+ * with a pool, drop the daily task partitions that ended before the window and
78
+ * create the next seven.
79
+ * Returns the number of stimulations removed.
93
80
  */
94
81
  deleteStale(olderThanMs: number): Promise<number>;
95
82
  close(): Promise<void>;
96
- private buildProgress;
97
83
  }
98
84
 
99
- export { CNSPostgresStimulationRepository, DEFAULT_PREFIX, type IPgQueryable, type TCNSPostgresStimulationRepositoryOptions, assertSafePrefix, ensureSchema, persistPostgresSchemaSql };
85
+ type TCNSPostgresStimulationMigrateOptions = {
86
+ readonly contract?: boolean;
87
+ };
88
+
89
+ declare class CNSPostgresStimulationSchema {
90
+ static readonly DEFAULT_TABLE_PREFIX = "cns_";
91
+ private static readonly LONGEST_SUFFIX;
92
+ private static readonly migrator;
93
+ static assertTablePrefix(tablePrefix: string): string;
94
+ static tables(tablePrefix?: string): string[];
95
+ static migrationTable(tablePrefix?: string): string;
96
+ static migrationSql(tablePrefix?: string, appliedSteps?: readonly number[], options?: TCNSPostgresStimulationMigrateOptions): string;
97
+ static migrate(pool: Pool, tablePrefix?: string, options?: TCNSPostgresStimulationMigrateOptions): Promise<void>;
98
+ static verify(database: ICNSPgQueryable, tablePrefix?: string): Promise<void>;
99
+ private static initialSql;
100
+ }
101
+
102
+ export { CNSPostgresStimulationRepository, CNSPostgresStimulationSchema, type ICNSPgQueryable, type TCNSPostgresStimulationMigrateOptions, type TCNSPostgresStimulationRepositoryOptions };