@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.
- package/README.md +60 -20
- package/dist/chunk-BPKIKGKB.mjs +310 -0
- package/dist/chunk-BPKIKGKB.mjs.map +1 -0
- package/dist/cli.js +277 -57
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +6 -7
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +54 -51
- package/dist/index.d.ts +54 -51
- package/dist/index.js +656 -264
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +388 -213
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -7
- package/dist/chunk-5FLIKV6I.mjs +0 -94
- package/dist/chunk-5FLIKV6I.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
CNSPostgresSchemaPartitionRotation,
|
|
3
|
+
CNSPostgresStimulationSchema,
|
|
3
4
|
__name,
|
|
4
|
-
__publicField
|
|
5
|
-
|
|
6
|
-
ensureSchema,
|
|
7
|
-
persistPostgresSchemaSql
|
|
8
|
-
} from "./chunk-5FLIKV6I.mjs";
|
|
5
|
+
__publicField
|
|
6
|
+
} from "./chunk-BPKIKGKB.mjs";
|
|
9
7
|
|
|
10
8
|
// src/CNSPostgresStimulationRepository.ts
|
|
11
9
|
import { Pool } from "pg";
|
|
12
|
-
import {
|
|
10
|
+
import { encodeCNSRosterCursor, CNS_HISTOGRAM_BUCKETS } from "@cnstra/persist";
|
|
13
11
|
var j = /* @__PURE__ */ __name((v) => JSON.stringify(v != null ? v : null), "j");
|
|
14
12
|
var num = /* @__PURE__ */ __name((v) => Number(v), "num");
|
|
15
13
|
var numOrNull = /* @__PURE__ */ __name((v) => v == null ? null : Number(v), "numOrNull");
|
|
@@ -20,8 +18,12 @@ var _CNSPostgresStimulationRepository = class _CNSPostgresStimulationRepository
|
|
|
20
18
|
__publicField(this, "ownsPool");
|
|
21
19
|
__publicField(this, "p");
|
|
22
20
|
__publicField(this, "autoMigrate");
|
|
21
|
+
__publicField(this, "now");
|
|
22
|
+
__publicField(this, "taskPartitions", new CNSPostgresSchemaPartitionRotation({
|
|
23
|
+
premakeDays: 7
|
|
24
|
+
}));
|
|
23
25
|
__publicField(this, "ready");
|
|
24
|
-
var _a, _b;
|
|
26
|
+
var _a, _b, _c;
|
|
25
27
|
if (opts.pool) {
|
|
26
28
|
this.pool = opts.pool;
|
|
27
29
|
this.db = opts.pool;
|
|
@@ -38,13 +40,21 @@ var _CNSPostgresStimulationRepository = class _CNSPostgresStimulationRepository
|
|
|
38
40
|
} else {
|
|
39
41
|
throw new Error("[@cnstra/persist-postgres] needs `connectionString`, `pool`, or `queryable`");
|
|
40
42
|
}
|
|
41
|
-
this.p =
|
|
43
|
+
this.p = CNSPostgresStimulationSchema.assertTablePrefix((_a = opts.tablePrefix) != null ? _a : CNSPostgresStimulationSchema.DEFAULT_TABLE_PREFIX);
|
|
42
44
|
this.autoMigrate = (_b = opts.autoMigrate) != null ? _b : true;
|
|
45
|
+
if (this.autoMigrate && !this.pool) {
|
|
46
|
+
throw new Error("[@cnstra/persist-postgres] a `queryable` cannot hold the migration transaction: pass `pool` or `connectionString`, or set `autoMigrate: false` and run CNSPostgresStimulationSchema.migrate(pool) yourself");
|
|
47
|
+
}
|
|
48
|
+
this.now = (_c = opts.now) != null ? _c : Date.now;
|
|
43
49
|
}
|
|
44
|
-
|
|
50
|
+
schemaReady() {
|
|
45
51
|
var _a;
|
|
46
52
|
if (!this.autoMigrate) return Promise.resolve();
|
|
47
|
-
|
|
53
|
+
(_a = this.ready) != null ? _a : this.ready = CNSPostgresStimulationSchema.migrate(this.pool, this.p).catch((error) => {
|
|
54
|
+
this.ready = void 0;
|
|
55
|
+
throw error;
|
|
56
|
+
});
|
|
57
|
+
return this.ready;
|
|
48
58
|
}
|
|
49
59
|
/** Run `fn` in a transaction when a Pool is available; else best-effort inline. */
|
|
50
60
|
async tx(fn) {
|
|
@@ -64,68 +74,87 @@ var _CNSPostgresStimulationRepository = class _CNSPostgresStimulationRepository
|
|
|
64
74
|
}
|
|
65
75
|
// ── write ──
|
|
66
76
|
async saveStimulation(s) {
|
|
67
|
-
await this.
|
|
77
|
+
await this.schemaReady();
|
|
68
78
|
const { p } = this;
|
|
79
|
+
const now = this.now();
|
|
69
80
|
await this.tx(async (db) => {
|
|
70
|
-
var _a;
|
|
71
|
-
await
|
|
72
|
-
|
|
81
|
+
var _a, _b;
|
|
82
|
+
await this.lockStimulation(db, s.stimulationId);
|
|
83
|
+
const upserted = await db.query(`INSERT INTO ${p}stimulation (stimulation_id, entry, status, scope_name, progress, updated_at)
|
|
84
|
+
VALUES ($1, $2::jsonb, $3, $4, $5::jsonb, $6)
|
|
73
85
|
ON CONFLICT (stimulation_id)
|
|
74
86
|
DO UPDATE SET entry = EXCLUDED.entry, status = EXCLUDED.status,
|
|
75
|
-
scope_name = EXCLUDED.scope_name, updated_at =
|
|
87
|
+
scope_name = EXCLUDED.scope_name, progress = EXCLUDED.progress, updated_at = EXCLUDED.updated_at
|
|
88
|
+
RETURNING (xmax = 0) AS inserted`, [
|
|
76
89
|
s.stimulationId,
|
|
77
90
|
j(s.entry),
|
|
78
91
|
s.status,
|
|
79
|
-
(_a = s.scopeName) != null ? _a : null
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
s.stimulationId
|
|
92
|
+
(_a = s.scopeName) != null ? _a : null,
|
|
93
|
+
j(s.progress),
|
|
94
|
+
now
|
|
83
95
|
]);
|
|
84
|
-
|
|
85
|
-
const t = s.progress.tasks[i];
|
|
86
|
-
await db.query(`INSERT INTO ${p}stimulation_frontier
|
|
87
|
-
(stimulation_id, ord, neuron_name, dendrite_collateral_name, input)
|
|
88
|
-
VALUES ($1, $2, $3, $4, $5::jsonb)`, [
|
|
89
|
-
s.stimulationId,
|
|
90
|
-
i,
|
|
91
|
-
t.neuronName,
|
|
92
|
-
t.dendriteCollateralName,
|
|
93
|
-
t.input ? j(t.input) : null
|
|
94
|
-
]);
|
|
95
|
-
}
|
|
96
|
+
if (((_b = upserted.rows[0]) == null ? void 0 : _b.inserted) !== false) await this.rollUpAttempts(db, s.stimulationId, now);
|
|
96
97
|
});
|
|
97
98
|
}
|
|
98
99
|
async saveAttempt(a) {
|
|
99
|
-
await this.
|
|
100
|
+
await this.schemaReady();
|
|
101
|
+
const { p } = this;
|
|
102
|
+
await this.tx(async (db) => {
|
|
103
|
+
await this.lockStimulation(db, a.stimulationId);
|
|
104
|
+
await db.query(`INSERT INTO ${p}stimulation_attempt
|
|
105
|
+
(stimulation_attempt_id, stimulation_id, attempt_number, status, started_at,
|
|
106
|
+
completed_at, hop_count, has_error, replay_of, entry)
|
|
107
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10::jsonb)
|
|
108
|
+
ON CONFLICT (stimulation_attempt_id) DO UPDATE SET
|
|
109
|
+
status = EXCLUDED.status, completed_at = EXCLUDED.completed_at,
|
|
110
|
+
hop_count = EXCLUDED.hop_count, has_error = EXCLUDED.has_error,
|
|
111
|
+
replay_of = EXCLUDED.replay_of, entry = EXCLUDED.entry`, [
|
|
112
|
+
a.stimulationAttemptId,
|
|
113
|
+
a.stimulationId,
|
|
114
|
+
a.attemptNumber,
|
|
115
|
+
a.status,
|
|
116
|
+
a.startedAt,
|
|
117
|
+
a.completedAt,
|
|
118
|
+
a.hopCount,
|
|
119
|
+
a.hasError,
|
|
120
|
+
a.replayOf,
|
|
121
|
+
j(a.entry)
|
|
122
|
+
]);
|
|
123
|
+
await this.rollUpAttempts(db, a.stimulationId, this.now());
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async lockStimulation(db, stimulationId) {
|
|
127
|
+
await db.query("SELECT pg_advisory_xact_lock($1, hashtext($2))", [
|
|
128
|
+
_CNSPostgresStimulationRepository.LOCK_STIMULATION,
|
|
129
|
+
this.p + stimulationId
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
async rollUpAttempts(db, stimulationId, now) {
|
|
100
133
|
const { p } = this;
|
|
101
|
-
await
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
a.hopCount,
|
|
116
|
-
a.hasError,
|
|
117
|
-
a.replayOf,
|
|
118
|
-
j(a.entry)
|
|
134
|
+
await db.query(`UPDATE ${p}stimulation s
|
|
135
|
+
SET started_at = COALESCE(a.started_at, 0),
|
|
136
|
+
hop_count = COALESCE(a.hop_count, 0),
|
|
137
|
+
last_completed_at = a.last_completed_at,
|
|
138
|
+
updated_at = GREATEST(s.updated_at, $2)
|
|
139
|
+
FROM (
|
|
140
|
+
SELECT MIN(started_at) AS started_at,
|
|
141
|
+
SUM(hop_count) AS hop_count,
|
|
142
|
+
(ARRAY_AGG(completed_at ORDER BY attempt_number DESC))[1] AS last_completed_at
|
|
143
|
+
FROM ${p}stimulation_attempt WHERE stimulation_id = $1
|
|
144
|
+
) a
|
|
145
|
+
WHERE s.stimulation_id = $1`, [
|
|
146
|
+
stimulationId,
|
|
147
|
+
now
|
|
119
148
|
]);
|
|
120
149
|
}
|
|
121
150
|
async appendTask(t) {
|
|
122
|
-
await this.
|
|
151
|
+
await this.schemaReady();
|
|
123
152
|
const { p } = this;
|
|
124
|
-
await this.db.query(`INSERT INTO ${p}
|
|
125
|
-
(stimulation_attempt_id,
|
|
126
|
-
input_index, output, status, error, started_at,
|
|
127
|
-
VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7,$8,$9,$10)
|
|
128
|
-
ON CONFLICT (stimulation_attempt_id,
|
|
153
|
+
await this.db.query(`INSERT INTO ${p}stimulation_task
|
|
154
|
+
(stimulation_attempt_id, task_index, neuron_name, dendrite_collateral_name,
|
|
155
|
+
input_index, output, status, error, started_at, duration_ms, activation_context)
|
|
156
|
+
VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7,$8,$9,$10,$11::jsonb)
|
|
157
|
+
ON CONFLICT (stimulation_attempt_id, task_index, started_at) DO NOTHING`, [
|
|
129
158
|
t.stimulationAttemptId,
|
|
130
159
|
t.index,
|
|
131
160
|
t.neuronName,
|
|
@@ -135,65 +164,37 @@ var _CNSPostgresStimulationRepository = class _CNSPostgresStimulationRepository
|
|
|
135
164
|
t.status,
|
|
136
165
|
t.error,
|
|
137
166
|
t.startedAt,
|
|
138
|
-
t.duration
|
|
167
|
+
t.duration,
|
|
168
|
+
t.activationContext ? j(t.activationContext) : null
|
|
139
169
|
]);
|
|
140
170
|
}
|
|
141
171
|
// ── read ──
|
|
142
172
|
async getStimulation(stimulationId) {
|
|
143
|
-
await this.
|
|
173
|
+
await this.schemaReady();
|
|
144
174
|
const { p } = this;
|
|
145
|
-
const
|
|
146
|
-
stimulationId
|
|
147
|
-
]);
|
|
148
|
-
const row = stimRes.rows[0];
|
|
149
|
-
if (!row) return void 0;
|
|
150
|
-
const frontier = await this.db.query(`SELECT ord, neuron_name, dendrite_collateral_name, input
|
|
151
|
-
FROM ${p}stimulation_frontier WHERE stimulation_id = $1 ORDER BY ord`, [
|
|
175
|
+
const res = await this.db.query(`SELECT stimulation_id, entry, status, scope_name, progress FROM ${p}stimulation WHERE stimulation_id = $1`, [
|
|
152
176
|
stimulationId
|
|
153
177
|
]);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
entry: row.entry,
|
|
157
|
-
status: row.status,
|
|
158
|
-
progress: this.buildProgress(frontier.rows),
|
|
159
|
-
...row.scope_name != null ? {
|
|
160
|
-
scopeName: row.scope_name
|
|
161
|
-
} : {}
|
|
162
|
-
};
|
|
178
|
+
const row = res.rows[0];
|
|
179
|
+
return row ? rowToStimulation(row) : void 0;
|
|
163
180
|
}
|
|
164
181
|
async listStimulations(filter) {
|
|
165
|
-
var _a;
|
|
166
|
-
await this.
|
|
182
|
+
var _a, _b;
|
|
183
|
+
await this.schemaReady();
|
|
167
184
|
const { p } = this;
|
|
168
185
|
const scope = (_a = filter == null ? void 0 : filter.scopeName) != null ? _a : null;
|
|
169
|
-
const
|
|
186
|
+
const since = (_b = filter == null ? void 0 : filter.since) != null ? _b : null;
|
|
187
|
+
const res = await this.db.query(`SELECT stimulation_id, entry, status, scope_name, progress FROM ${p}stimulation
|
|
170
188
|
WHERE ($1::text IS NULL OR scope_name = $1)
|
|
189
|
+
AND ($2::bigint IS NULL OR updated_at >= $2)
|
|
171
190
|
ORDER BY updated_at DESC`, [
|
|
172
|
-
scope
|
|
191
|
+
scope,
|
|
192
|
+
since
|
|
173
193
|
]);
|
|
174
|
-
|
|
175
|
-
if (stims.length === 0) return [];
|
|
176
|
-
const ids = stims.map((r) => r.stimulation_id);
|
|
177
|
-
const frontierRes = await this.db.query(`SELECT stimulation_id, ord, neuron_name, dendrite_collateral_name, input
|
|
178
|
-
FROM ${p}stimulation_frontier WHERE stimulation_id = ANY($1) ORDER BY stimulation_id, ord`, [
|
|
179
|
-
ids
|
|
180
|
-
]);
|
|
181
|
-
const frontierBy = groupBy(frontierRes.rows, (r) => r.stimulation_id);
|
|
182
|
-
return stims.map((r) => {
|
|
183
|
-
var _a2;
|
|
184
|
-
return {
|
|
185
|
-
stimulationId: r.stimulation_id,
|
|
186
|
-
entry: r.entry,
|
|
187
|
-
status: r.status,
|
|
188
|
-
progress: this.buildProgress((_a2 = frontierBy.get(r.stimulation_id)) != null ? _a2 : []),
|
|
189
|
-
...r.scope_name != null ? {
|
|
190
|
-
scopeName: r.scope_name
|
|
191
|
-
} : {}
|
|
192
|
-
};
|
|
193
|
-
});
|
|
194
|
+
return res.rows.map(rowToStimulation);
|
|
194
195
|
}
|
|
195
196
|
async getAttempts(stimulationId) {
|
|
196
|
-
await this.
|
|
197
|
+
await this.schemaReady();
|
|
197
198
|
const { p } = this;
|
|
198
199
|
const res = await this.db.query(`SELECT * FROM ${p}stimulation_attempt WHERE stimulation_id = $1 ORDER BY attempt_number`, [
|
|
199
200
|
stimulationId
|
|
@@ -201,32 +202,92 @@ var _CNSPostgresStimulationRepository = class _CNSPostgresStimulationRepository
|
|
|
201
202
|
return res.rows.map(rowToAttempt);
|
|
202
203
|
}
|
|
203
204
|
async getTasks(stimulationAttemptId) {
|
|
204
|
-
await this.
|
|
205
|
+
await this.schemaReady();
|
|
205
206
|
const { p } = this;
|
|
206
|
-
const res = await this.db.query(`SELECT * FROM ${p}
|
|
207
|
+
const res = await this.db.query(`SELECT * FROM ${p}stimulation_task WHERE stimulation_attempt_id = $1 ORDER BY task_index`, [
|
|
207
208
|
stimulationAttemptId
|
|
208
209
|
]);
|
|
209
210
|
return res.rows.map(rowToTask);
|
|
210
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Attempts of many stimulations in ONE query, so a page of runs costs one
|
|
214
|
+
* round trip rather than one per run.
|
|
215
|
+
*/
|
|
216
|
+
async getAttemptsFor(stimulationIds) {
|
|
217
|
+
if (stimulationIds.length === 0) return [];
|
|
218
|
+
await this.schemaReady();
|
|
219
|
+
const { p } = this;
|
|
220
|
+
const res = await this.db.query(`SELECT * FROM ${p}stimulation_attempt
|
|
221
|
+
WHERE stimulation_id = ANY($1)
|
|
222
|
+
ORDER BY stimulation_id, attempt_number`, [
|
|
223
|
+
stimulationIds
|
|
224
|
+
]);
|
|
225
|
+
return res.rows.map(rowToAttempt);
|
|
226
|
+
}
|
|
227
|
+
/** Tasks of many attempts in ONE query — see {@link getAttemptsFor}. */
|
|
228
|
+
async getTasksFor(stimulationAttemptIds) {
|
|
229
|
+
if (stimulationAttemptIds.length === 0) return [];
|
|
230
|
+
await this.schemaReady();
|
|
231
|
+
const { p } = this;
|
|
232
|
+
const res = await this.db.query(`SELECT * FROM ${p}stimulation_task
|
|
233
|
+
WHERE stimulation_attempt_id = ANY($1)
|
|
234
|
+
ORDER BY stimulation_attempt_id, task_index`, [
|
|
235
|
+
stimulationAttemptIds
|
|
236
|
+
]);
|
|
237
|
+
return res.rows.map(rowToTask);
|
|
238
|
+
}
|
|
211
239
|
// ── roster query capability (filter + keyset + facets pushed into SQL) ──
|
|
212
240
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
241
|
+
* The CTE every roster read stands on. The roster fields are kept on the
|
|
242
|
+
* stimulation row by `saveAttempt` / `saveStimulation`, so the window and the
|
|
243
|
+
* order come from `stimulation(started_at DESC, stimulation_id)`; the total
|
|
244
|
+
* still counts the whole filtered set. Shared so the facet tallies are counted
|
|
245
|
+
* over exactly the set the roster pages through.
|
|
246
|
+
* The task table is read only when `withNeurons` asks for the neuron names.
|
|
247
|
+
* It binds `$1..$7` (the filter); the keyset cursor belongs to the roster's tail.
|
|
219
248
|
*/
|
|
249
|
+
rosterCte(withNeurons) {
|
|
250
|
+
const { p } = this;
|
|
251
|
+
const neurons = withNeurons ? `COALESCE((
|
|
252
|
+
SELECT ARRAY_AGG(DISTINCT t.neuron_name)
|
|
253
|
+
FROM ${p}stimulation_task t
|
|
254
|
+
JOIN ${p}stimulation_attempt a ON a.stimulation_attempt_id = t.stimulation_attempt_id
|
|
255
|
+
WHERE a.stimulation_id = s.stimulation_id
|
|
256
|
+
), ARRAY[]::text[])` : "ARRAY[]::text[]";
|
|
257
|
+
return `
|
|
258
|
+
WITH roster AS (
|
|
259
|
+
SELECT s.stimulation_id, s.entry, s.status, s.scope_name, s.started_at,
|
|
260
|
+
CASE WHEN s.status = 'running' THEN NULL ELSE s.last_completed_at END AS completed_at,
|
|
261
|
+
s.hop_count,
|
|
262
|
+
(s.status = 'failed') AS has_error,
|
|
263
|
+
${neurons} AS neurons
|
|
264
|
+
FROM ${p}stimulation s
|
|
265
|
+
WHERE ($6::bigint IS NULL OR s.started_at >= $6)
|
|
266
|
+
AND ($7::bigint IS NULL OR s.started_at <= $7)
|
|
267
|
+
AND ($1::text IS NULL OR s.scope_name = $1)
|
|
268
|
+
AND ($2::text IS NULL OR s.status = $2)
|
|
269
|
+
AND ($3::text IS NULL OR s.entry->>'collateralName' = $3)
|
|
270
|
+
AND ($5::text IS NULL OR strpos(lower(s.stimulation_id), $5) > 0)
|
|
271
|
+
),
|
|
272
|
+
filtered AS (
|
|
273
|
+
SELECT *, COUNT(*) OVER() AS total_count FROM roster
|
|
274
|
+
WHERE ($4::text IS NULL OR $4 = ANY(neurons))
|
|
275
|
+
)`;
|
|
276
|
+
}
|
|
220
277
|
async selectRoster(filter, opts) {
|
|
221
278
|
var _a, _b, _c, _d, _e, _f;
|
|
222
|
-
await this.
|
|
279
|
+
await this.schemaReady();
|
|
223
280
|
const { p } = this;
|
|
224
281
|
let curStarted = null;
|
|
225
282
|
let curId = null;
|
|
226
283
|
if (opts == null ? void 0 : opts.cursor) {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
284
|
+
const decoded = Buffer.from(opts.cursor, "base64").toString("utf8");
|
|
285
|
+
const at = decoded.indexOf("|");
|
|
286
|
+
const started = Number(decoded.slice(0, at));
|
|
287
|
+
if (at >= 0 && Number.isFinite(started)) {
|
|
288
|
+
curStarted = started;
|
|
289
|
+
curId = decoded.slice(at + 1);
|
|
290
|
+
}
|
|
230
291
|
}
|
|
231
292
|
const params = [
|
|
232
293
|
(_a = filter.scopeName) != null ? _a : null,
|
|
@@ -239,49 +300,27 @@ var _CNSPostgresStimulationRepository = class _CNSPostgresStimulationRepository
|
|
|
239
300
|
curStarted,
|
|
240
301
|
curId
|
|
241
302
|
];
|
|
242
|
-
const limitClause = (opts == null ? void 0 : opts.limit) != null ? ` LIMIT ${Math.max(0, opts.limit
|
|
243
|
-
const sql =
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
),
|
|
252
|
-
neu AS (
|
|
253
|
-
SELECT ta.stimulation_id,
|
|
254
|
-
ARRAY_AGG(DISTINCT t.neuron_name) AS neurons
|
|
255
|
-
FROM ${p}task t
|
|
256
|
-
JOIN ${p}stimulation_attempt ta
|
|
257
|
-
ON ta.stimulation_attempt_id = t.stimulation_attempt_id
|
|
258
|
-
GROUP BY ta.stimulation_id
|
|
259
|
-
),
|
|
260
|
-
roster AS (
|
|
261
|
-
SELECT s.stimulation_id, s.entry, s.status, s.scope_name,
|
|
262
|
-
COALESCE(agg.started_at, 0) AS started_at,
|
|
263
|
-
CASE WHEN s.status = 'running' THEN NULL
|
|
264
|
-
ELSE agg.last_completed_at END AS completed_at,
|
|
265
|
-
COALESCE(agg.hop_count, 0) AS hop_count,
|
|
266
|
-
(s.status = 'failed') AS has_error,
|
|
267
|
-
COALESCE(neu.neurons, ARRAY[]::text[]) AS neurons
|
|
268
|
-
FROM ${p}stimulation s
|
|
269
|
-
LEFT JOIN agg ON agg.stimulation_id = s.stimulation_id
|
|
270
|
-
LEFT JOIN neu ON neu.stimulation_id = s.stimulation_id
|
|
271
|
-
)
|
|
272
|
-
SELECT *, COUNT(*) OVER() AS total_count FROM roster
|
|
273
|
-
WHERE ($1::text IS NULL OR scope_name = $1)
|
|
274
|
-
AND ($2::text IS NULL OR status = $2)
|
|
275
|
-
AND ($3::text IS NULL OR entry->>'collateralName' = $3)
|
|
276
|
-
AND ($4::text IS NULL OR $4 = ANY(neurons))
|
|
277
|
-
AND ($5::text IS NULL OR lower(stimulation_id) LIKE '%'||$5||'%')
|
|
278
|
-
AND ($6::bigint IS NULL OR started_at >= $6)
|
|
279
|
-
AND ($7::bigint IS NULL OR started_at <= $7)
|
|
280
|
-
AND ($8::bigint IS NULL OR (started_at, stimulation_id) < ($8, $9))
|
|
281
|
-
ORDER BY started_at DESC, stimulation_id DESC${limitClause}`;
|
|
303
|
+
const limitClause = (opts == null ? void 0 : opts.limit) != null && Number.isFinite(opts.limit) ? ` LIMIT ${Math.max(0, Math.floor(opts.limit))}` : "";
|
|
304
|
+
const sql = `${this.rosterCte(filter.neuronName != null)}
|
|
305
|
+
-- The count is taken INSIDE \`filtered\`, before the keyset predicate
|
|
306
|
+
-- narrows the set. Counting after it made \`total\` shrink with every
|
|
307
|
+
-- page, while the in-memory reference reports the whole filtered set \u2014
|
|
308
|
+
-- and the two are contractually identical.
|
|
309
|
+
SELECT * FROM filtered
|
|
310
|
+
WHERE ($8::bigint IS NULL OR (started_at, stimulation_id COLLATE "C") < ($8, $9::text COLLATE "C"))
|
|
311
|
+
ORDER BY started_at DESC, stimulation_id COLLATE "C" DESC${limitClause}`;
|
|
282
312
|
const res = await this.db.query(sql, params);
|
|
283
313
|
const rows = res.rows;
|
|
284
|
-
|
|
314
|
+
let total = rows.length ? Number(rows[0].total_count) : 0;
|
|
315
|
+
if (!rows.length && curStarted !== null) {
|
|
316
|
+
const countRes = await this.db.query(sql, [
|
|
317
|
+
...params.slice(0, 7),
|
|
318
|
+
null,
|
|
319
|
+
null
|
|
320
|
+
]);
|
|
321
|
+
const countRows = countRes.rows;
|
|
322
|
+
total = countRows.length ? Number(countRows[0].total_count) : 0;
|
|
323
|
+
}
|
|
285
324
|
const entries = rows.map((r) => {
|
|
286
325
|
var _a2, _b2;
|
|
287
326
|
return {
|
|
@@ -304,26 +343,119 @@ var _CNSPostgresStimulationRepository = class _CNSPostgresStimulationRepository
|
|
|
304
343
|
};
|
|
305
344
|
}
|
|
306
345
|
async queryRoster(filter, opts) {
|
|
307
|
-
const { entries, total } = await this.selectRoster(filter,
|
|
308
|
-
|
|
346
|
+
const { entries, total } = await this.selectRoster(filter, {
|
|
347
|
+
...opts,
|
|
348
|
+
limit: opts.limit + 1
|
|
349
|
+
});
|
|
350
|
+
const hasMore = entries.length > opts.limit;
|
|
351
|
+
const rows = entries.slice(0, opts.limit).map((e) => e.row);
|
|
309
352
|
const lastRow = rows[rows.length - 1];
|
|
310
|
-
const hasMore = rows.length >= opts.limit && rows.length < total;
|
|
311
353
|
return {
|
|
312
354
|
rows,
|
|
313
|
-
nextCursor: hasMore && lastRow ?
|
|
355
|
+
nextCursor: hasMore && lastRow ? encodeCNSRosterCursor(lastRow) : null,
|
|
314
356
|
total
|
|
315
357
|
};
|
|
316
358
|
}
|
|
317
359
|
async queryFacets(filter, opts) {
|
|
318
|
-
|
|
319
|
-
|
|
360
|
+
var _a, _b, _c, _d, _e, _f;
|
|
361
|
+
await this.schemaReady();
|
|
362
|
+
const params = [
|
|
363
|
+
(_a = filter.scopeName) != null ? _a : null,
|
|
364
|
+
(_b = filter.status) != null ? _b : null,
|
|
365
|
+
(_c = filter.entryCollateralName) != null ? _c : null,
|
|
366
|
+
(_d = filter.neuronName) != null ? _d : null,
|
|
367
|
+
filter.text ? filter.text.toLowerCase() : null,
|
|
368
|
+
(_e = filter.since) != null ? _e : null,
|
|
369
|
+
(_f = filter.until) != null ? _f : null,
|
|
370
|
+
// The metric path as a jsonb path array (`a.b` → {a,b}); null when the
|
|
371
|
+
// chart is not summing anything.
|
|
372
|
+
(opts == null ? void 0 : opts.metricPath) ? opts.metricPath.split(".") : null
|
|
373
|
+
];
|
|
374
|
+
const sql = `${this.rosterCte(true)},
|
|
375
|
+
-- The histogram's bucket width is derived from the set's own span, the
|
|
376
|
+
-- same ${CNS_HISTOGRAM_BUCKETS}-bucket rule the reference implementation
|
|
377
|
+
-- uses. Computed here so the whole thing stays one round trip.
|
|
378
|
+
bounds AS (
|
|
379
|
+
SELECT MIN(started_at) AS mn, MAX(started_at) AS mx FROM filtered WHERE started_at > 0
|
|
380
|
+
),
|
|
381
|
+
step AS (
|
|
382
|
+
SELECT mn,
|
|
383
|
+
GREATEST(1, CEIL((mx - mn)::numeric / ${CNS_HISTOGRAM_BUCKETS})::bigint) AS size
|
|
384
|
+
FROM bounds
|
|
385
|
+
)
|
|
386
|
+
SELECT 'entry' AS kind, entry->>'collateralName' AS key,
|
|
387
|
+
COUNT(*)::bigint AS cnt,
|
|
388
|
+
NULL::bigint AS ts_bucket, NULL::bigint AS completed,
|
|
389
|
+
NULL::bigint AS failed, NULL::bigint AS running,
|
|
390
|
+
NULL::numeric AS metric
|
|
391
|
+
FROM filtered GROUP BY 2
|
|
392
|
+
UNION ALL
|
|
393
|
+
SELECT 'status', status, COUNT(*)::bigint,
|
|
394
|
+
NULL, NULL, NULL, NULL, NULL
|
|
395
|
+
FROM filtered GROUP BY 2
|
|
396
|
+
UNION ALL
|
|
397
|
+
-- One row per (run, neuron): the neuron rail counts runs that touched
|
|
398
|
+
-- a neuron, which is what UNNEST over the per-run DISTINCT array gives.
|
|
399
|
+
SELECT 'neuron', n, COUNT(*)::bigint,
|
|
400
|
+
NULL, NULL, NULL, NULL, NULL
|
|
401
|
+
FROM filtered, UNNEST(neurons) AS n GROUP BY 2
|
|
402
|
+
UNION ALL
|
|
403
|
+
SELECT 'time', NULL, NULL,
|
|
404
|
+
(step.mn + ((f.started_at - step.mn) / step.size) * step.size)::bigint,
|
|
405
|
+
COUNT(*) FILTER (WHERE f.status = 'completed')::bigint,
|
|
406
|
+
COUNT(*) FILTER (WHERE f.status = 'failed')::bigint,
|
|
407
|
+
COUNT(*) FILTER (WHERE f.status NOT IN ('completed', 'failed'))::bigint,
|
|
408
|
+
-- Only actual JSON numbers are summed, matching the reference's
|
|
409
|
+
-- \`typeof v === 'number'\`: a payload field holding "12" as a
|
|
410
|
+
-- string is not a metric and must not become one here.
|
|
411
|
+
SUM(
|
|
412
|
+
CASE WHEN $8::text[] IS NOT NULL
|
|
413
|
+
AND jsonb_typeof(f.entry->'payload' #> $8::text[]) = 'number'
|
|
414
|
+
THEN (f.entry->'payload' #> $8::text[])::text::numeric
|
|
415
|
+
END
|
|
416
|
+
)
|
|
417
|
+
FROM filtered f, step
|
|
418
|
+
WHERE f.started_at > 0
|
|
419
|
+
GROUP BY 4`;
|
|
420
|
+
const res = await this.db.query(sql, params);
|
|
421
|
+
const rows = res.rows;
|
|
422
|
+
const tally = /* @__PURE__ */ __name((kind) => rows.filter((r) => r.kind === kind && r.key != null).map((r) => ({
|
|
423
|
+
key: r.key,
|
|
424
|
+
count: Number(r.cnt)
|
|
425
|
+
})).sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1)), "tally");
|
|
426
|
+
return {
|
|
427
|
+
byEntryCollateral: tally("entry").map((x) => ({
|
|
428
|
+
name: x.key,
|
|
429
|
+
count: x.count
|
|
430
|
+
})),
|
|
431
|
+
byStatus: tally("status").map((x) => ({
|
|
432
|
+
status: x.key,
|
|
433
|
+
count: x.count
|
|
434
|
+
})),
|
|
435
|
+
byNeuron: tally("neuron").map((x) => ({
|
|
436
|
+
name: x.key,
|
|
437
|
+
count: x.count
|
|
438
|
+
})),
|
|
439
|
+
byTime: rows.filter((r) => r.kind === "time" && r.ts_bucket != null).map((r) => ({
|
|
440
|
+
tsBucket: Number(r.ts_bucket),
|
|
441
|
+
completed: Number(r.completed),
|
|
442
|
+
failed: Number(r.failed),
|
|
443
|
+
running: Number(r.running),
|
|
444
|
+
// Absent rather than 0 when nothing was summed — the reference
|
|
445
|
+
// only sets `metric` once it has a number to put there.
|
|
446
|
+
...r.metric == null ? {} : {
|
|
447
|
+
metric: Number(r.metric)
|
|
448
|
+
}
|
|
449
|
+
})).sort((a, b) => a.tsBucket - b.tsBucket)
|
|
450
|
+
};
|
|
320
451
|
}
|
|
321
452
|
// ── lifecycle ──
|
|
322
453
|
async delete(stimulationId) {
|
|
323
|
-
await this.
|
|
454
|
+
await this.schemaReady();
|
|
324
455
|
const { p } = this;
|
|
325
456
|
await this.tx(async (db) => {
|
|
326
|
-
await
|
|
457
|
+
await this.lockStimulation(db, stimulationId);
|
|
458
|
+
await db.query(`DELETE FROM ${p}stimulation_task WHERE stimulation_attempt_id IN
|
|
327
459
|
(SELECT stimulation_attempt_id FROM ${p}stimulation_attempt WHERE stimulation_id = $1)`, [
|
|
328
460
|
stimulationId
|
|
329
461
|
]);
|
|
@@ -336,49 +468,93 @@ var _CNSPostgresStimulationRepository = class _CNSPostgresStimulationRepository
|
|
|
336
468
|
});
|
|
337
469
|
}
|
|
338
470
|
/**
|
|
339
|
-
* Retention: prune stimulations not updated for `olderThanMs
|
|
340
|
-
* attempts
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
471
|
+
* Retention: prune stimulations not updated for `olderThanMs`, with their
|
|
472
|
+
* attempts and tasks, in batches of one transaction each, then the attempts and
|
|
473
|
+
* tasks older than that whose parent row never arrived or is already gone; last,
|
|
474
|
+
* with a pool, drop the daily task partitions that ended before the window and
|
|
475
|
+
* create the next seven.
|
|
476
|
+
* Returns the number of stimulations removed.
|
|
344
477
|
*/
|
|
345
478
|
async deleteStale(olderThanMs) {
|
|
346
|
-
await this.
|
|
479
|
+
await this.schemaReady();
|
|
347
480
|
const { p } = this;
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
481
|
+
const now = this.now();
|
|
482
|
+
const cutoff = now - Math.max(0, Math.floor(olderThanMs));
|
|
483
|
+
let removedCount = 0;
|
|
484
|
+
let after = "";
|
|
485
|
+
for (; ; ) {
|
|
486
|
+
const batch = await this.tx(async (db) => {
|
|
487
|
+
const stale = await db.query(`SELECT stimulation_id FROM ${p}stimulation
|
|
488
|
+
WHERE updated_at < $1 AND stimulation_id > $2
|
|
489
|
+
ORDER BY stimulation_id LIMIT $3`, [
|
|
490
|
+
cutoff,
|
|
491
|
+
after,
|
|
492
|
+
_CNSPostgresStimulationRepository.RETENTION_BATCH
|
|
493
|
+
]);
|
|
494
|
+
const candidates = stale.rows.map((row) => row.stimulation_id);
|
|
495
|
+
if (candidates.length === 0) return {
|
|
496
|
+
candidates,
|
|
497
|
+
removed: 0
|
|
498
|
+
};
|
|
499
|
+
await db.query(`SELECT pg_advisory_xact_lock($1, hashtext($2 || id)) FROM unnest($3::text[]) AS id ORDER BY id`, [
|
|
500
|
+
_CNSPostgresStimulationRepository.LOCK_STIMULATION,
|
|
501
|
+
p,
|
|
502
|
+
candidates
|
|
503
|
+
]);
|
|
504
|
+
const removed = await db.query(`DELETE FROM ${p}stimulation WHERE stimulation_id = ANY($1) AND updated_at < $2 RETURNING stimulation_id`, [
|
|
505
|
+
candidates,
|
|
506
|
+
cutoff
|
|
507
|
+
]);
|
|
508
|
+
const ids = removed.rows.map((row) => row.stimulation_id);
|
|
509
|
+
await db.query(`DELETE FROM ${p}stimulation_task t
|
|
510
|
+
USING ${p}stimulation_attempt a
|
|
511
|
+
WHERE a.stimulation_attempt_id = t.stimulation_attempt_id AND a.stimulation_id = ANY($1)`, [
|
|
512
|
+
ids
|
|
513
|
+
]);
|
|
514
|
+
await db.query(`DELETE FROM ${p}stimulation_attempt WHERE stimulation_id = ANY($1)`, [
|
|
515
|
+
ids
|
|
516
|
+
]);
|
|
517
|
+
return {
|
|
518
|
+
candidates,
|
|
519
|
+
removed: ids.length
|
|
520
|
+
};
|
|
521
|
+
});
|
|
522
|
+
removedCount += batch.removed;
|
|
523
|
+
if (batch.candidates.length < _CNSPostgresStimulationRepository.RETENTION_BATCH) break;
|
|
524
|
+
after = batch.candidates[batch.candidates.length - 1];
|
|
525
|
+
}
|
|
526
|
+
let rotationFailure;
|
|
527
|
+
if (this.pool) {
|
|
528
|
+
try {
|
|
529
|
+
await this.taskPartitions.rotate({
|
|
530
|
+
transaction: /* @__PURE__ */ __name((work) => this.tx((db) => work(db)), "transaction")
|
|
531
|
+
}, `${p}stimulation_task`, now, cutoff);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
rotationFailure = {
|
|
534
|
+
error
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
await this.db.query(`DELETE FROM ${p}stimulation_attempt a
|
|
539
|
+
WHERE a.started_at < $1
|
|
540
|
+
AND NOT EXISTS (SELECT 1 FROM ${p}stimulation s WHERE s.stimulation_id = a.stimulation_id)`, [
|
|
541
|
+
cutoff
|
|
542
|
+
]);
|
|
543
|
+
await this.db.query(`DELETE FROM ${p}stimulation_task t
|
|
544
|
+
WHERE t.started_at < $1
|
|
545
|
+
AND NOT EXISTS (SELECT 1 FROM ${p}stimulation_attempt a WHERE a.stimulation_attempt_id = t.stimulation_attempt_id)`, [
|
|
546
|
+
cutoff
|
|
547
|
+
]);
|
|
548
|
+
if (rotationFailure) throw rotationFailure.error;
|
|
549
|
+
return removedCount;
|
|
361
550
|
}
|
|
362
551
|
async close() {
|
|
363
552
|
if (this.ownsPool && this.pool) await this.pool.end();
|
|
364
553
|
}
|
|
365
|
-
// ── helpers ──
|
|
366
|
-
buildProgress(frontier) {
|
|
367
|
-
const tasks = [
|
|
368
|
-
...frontier
|
|
369
|
-
].sort((a, b) => a.ord - b.ord).map((f) => ({
|
|
370
|
-
neuronName: f.neuron_name,
|
|
371
|
-
dendriteCollateralName: f.dendrite_collateral_name,
|
|
372
|
-
...f.input != null ? {
|
|
373
|
-
input: f.input
|
|
374
|
-
} : {}
|
|
375
|
-
}));
|
|
376
|
-
return {
|
|
377
|
-
tasks
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
554
|
};
|
|
381
555
|
__name(_CNSPostgresStimulationRepository, "CNSPostgresStimulationRepository");
|
|
556
|
+
__publicField(_CNSPostgresStimulationRepository, "LOCK_STIMULATION", 7);
|
|
557
|
+
__publicField(_CNSPostgresStimulationRepository, "RETENTION_BATCH", 200);
|
|
382
558
|
var CNSPostgresStimulationRepository = _CNSPostgresStimulationRepository;
|
|
383
559
|
function rowToAttempt(a) {
|
|
384
560
|
var _a;
|
|
@@ -400,7 +576,7 @@ function rowToTask(t) {
|
|
|
400
576
|
var _a, _b;
|
|
401
577
|
return {
|
|
402
578
|
stimulationAttemptId: t.stimulation_attempt_id,
|
|
403
|
-
index: num(t.
|
|
579
|
+
index: num(t.task_index),
|
|
404
580
|
neuronName: t.neuron_name,
|
|
405
581
|
dendriteCollateralName: t.dendrite_collateral_name,
|
|
406
582
|
inputIndex: num(t.input_index),
|
|
@@ -408,28 +584,27 @@ function rowToTask(t) {
|
|
|
408
584
|
status: t.status,
|
|
409
585
|
error: (_b = t.error) != null ? _b : null,
|
|
410
586
|
startedAt: num(t.started_at),
|
|
411
|
-
duration: numOrNull(t.
|
|
587
|
+
duration: numOrNull(t.duration_ms),
|
|
588
|
+
...t.activation_context != null ? {
|
|
589
|
+
activationContext: t.activation_context
|
|
590
|
+
} : {}
|
|
412
591
|
};
|
|
413
592
|
}
|
|
414
593
|
__name(rowToTask, "rowToTask");
|
|
415
|
-
function
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
r
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
return m;
|
|
594
|
+
function rowToStimulation(r) {
|
|
595
|
+
return {
|
|
596
|
+
stimulationId: r.stimulation_id,
|
|
597
|
+
entry: r.entry,
|
|
598
|
+
status: r.status,
|
|
599
|
+
progress: r.progress,
|
|
600
|
+
...r.scope_name != null ? {
|
|
601
|
+
scopeName: r.scope_name
|
|
602
|
+
} : {}
|
|
603
|
+
};
|
|
426
604
|
}
|
|
427
|
-
__name(
|
|
605
|
+
__name(rowToStimulation, "rowToStimulation");
|
|
428
606
|
export {
|
|
429
607
|
CNSPostgresStimulationRepository,
|
|
430
|
-
|
|
431
|
-
assertSafePrefix,
|
|
432
|
-
ensureSchema,
|
|
433
|
-
persistPostgresSchemaSql
|
|
608
|
+
CNSPostgresStimulationSchema
|
|
434
609
|
};
|
|
435
610
|
//# sourceMappingURL=index.mjs.map
|