@fnndsc/cumin 3.17.1 → 3.19.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.
@@ -0,0 +1,314 @@
1
+ "use strict";
2
+ /**
3
+ * @file The PACS query index: what has already been asked, and answered.
4
+ *
5
+ * CUBE keeps every PACS query it has run, criteria and result both, so a
6
+ * question asked before need not be asked again. It cannot be *searched*
7
+ * for, though: the queries collection accepts no parameter over the stored
8
+ * criteria, and every row of every page drags its compressed result along —
9
+ * kilobytes each, thousands of rows. Looking a question up by sweeping that
10
+ * collection would cost megabytes, and would cost them most often on a
11
+ * MISS, which is the common case for a genuinely new query. A client that
12
+ * searched naively would make every new query slower than not searching.
13
+ *
14
+ * So the collection is walked once, in the background, and what is learned
15
+ * is kept here: criteria and server in, the stored query's id out. A lookup
16
+ * is then a map read.
17
+ *
18
+ * The index is ADVISORY. A hit is a claim to be confirmed — the caller
19
+ * decodes the stored result before serving it, and any failure (the query
20
+ * deleted, the result unreadable) falls through to a fresh query. A stale
21
+ * entry can therefore cost a wasted lookup and never a wrong answer, which
22
+ * is what makes it safe to keep across restarts and to fill lazily.
23
+ *
24
+ * It is also the log itself. Each entry carries the criteria and owner the
25
+ * VFS renders into `/net/pacs/queries/<name>`, so that listing can be
26
+ * served from here — complete, rather than the first page of it.
27
+ *
28
+ * @module
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.QueryIndex = void 0;
32
+ exports.queryKey_build = queryKey_build;
33
+ exports.queryCriteria_parse = queryCriteria_parse;
34
+ exports.queryIndex_get = queryIndex_get;
35
+ /**
36
+ * Separates the server from the criteria in a key, and one criterion from
37
+ * the next.
38
+ *
39
+ * ASCII record and unit separators, written as escapes: a PACS identifier
40
+ * or a DICOM value may contain anything printable — including the spaces,
41
+ * colons and commas that would otherwise let two different questions
42
+ * collide on one key.
43
+ */
44
+ const KEY_SERVER_SEPARATOR = '\u001e';
45
+ /** Separates one criterion from the next in a key. */
46
+ const KEY_PAIR_SEPARATOR = '\u001f';
47
+ /**
48
+ * Builds the key a question is filed under.
49
+ *
50
+ * Two questions are the same question when their criteria sets are equal
51
+ * and they were asked of the same PACS. Key order and surrounding
52
+ * whitespace are normalized away because they are how the question was
53
+ * *typed*, not what it asked. Values are otherwise left exactly as given:
54
+ * they are the PACS's own vocabulary, and mise does not get to reinterpret
55
+ * them — a case fold here would silently claim that two DICOM values match
56
+ * when only the PACS can say so.
57
+ *
58
+ * The owner is part of the question. The index holds the whole log —
59
+ * every identity's queries, because that is what `/net/pacs/queries` lists
60
+ * — while a replay may only ever serve back an answer to the identity that
61
+ * asked for it. Keying by owner makes those two facts one lookup instead of
62
+ * a filter somebody can forget.
63
+ *
64
+ * @param criteria - The criteria as asked.
65
+ * @param server - The PACS identifier the query was asked of.
66
+ * @param owner - The identity that asked.
67
+ * @returns The key, stable for equal questions.
68
+ */
69
+ function queryKey_build(criteria, server, owner) {
70
+ const pairs = [];
71
+ for (const [name, value] of Object.entries(criteria)) {
72
+ const key = name.trim();
73
+ const held = String(value ?? '').trim();
74
+ if (key.length === 0 || held.length === 0)
75
+ continue;
76
+ pairs.push(`${key}=${held}`);
77
+ }
78
+ pairs.sort();
79
+ return `${owner.trim()}${KEY_SERVER_SEPARATOR}${server.trim()}${KEY_SERVER_SEPARATOR}${pairs.join(KEY_PAIR_SEPARATOR)}`;
80
+ }
81
+ /**
82
+ * Parses the criteria CUBE stores as a JSON string on a query record.
83
+ *
84
+ * @param raw - The record's `query` field.
85
+ * @returns The criteria, or null when the field is absent or unparseable.
86
+ */
87
+ function queryCriteria_parse(raw) {
88
+ if (typeof raw !== 'string' || raw.trim().length === 0)
89
+ return null;
90
+ try {
91
+ const parsed = JSON.parse(raw);
92
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
93
+ return null;
94
+ const criteria = {};
95
+ for (const [name, value] of Object.entries(parsed)) {
96
+ if (value === undefined || value === null)
97
+ continue;
98
+ criteria[name] = String(value);
99
+ }
100
+ return criteria;
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
106
+ /**
107
+ * What has already been asked of which PACS, and where the answer is kept.
108
+ */
109
+ class QueryIndex {
110
+ constructor() {
111
+ /**
112
+ * Every record, by its query id. This is the log: asking the same thing
113
+ * twice leaves two records, and both remain reachable.
114
+ */
115
+ this.entries = new Map();
116
+ /**
117
+ * Question to the newest record answering it. This is the lookup: a
118
+ * replay wants the most recent answer, not every answer.
119
+ */
120
+ this.byQuestion = new Map();
121
+ this.floor = null;
122
+ this.listeners = new Set();
123
+ }
124
+ /**
125
+ * Files a query under its question.
126
+ *
127
+ * The newest answer to a question wins: asking the same thing twice
128
+ * leaves the later record indexed, which is the one whose age a replay
129
+ * should state.
130
+ *
131
+ * @param entry - What was asked, of whom, and what came back.
132
+ */
133
+ entry_note(entry) {
134
+ this.entries.set(entry.queryId, entry);
135
+ const key = queryKey_build(entry.criteria, entry.server, entry.owner);
136
+ const heldId = this.byQuestion.get(key);
137
+ const held = heldId === undefined ? undefined : this.entries.get(heldId);
138
+ // Strictly newer, so a tie leaves the incumbent in place: what this
139
+ // process learned outranks what a restored checkpoint claims.
140
+ if (held === undefined || Date.parse(entry.answeredAt) > Date.parse(held.answeredAt)) {
141
+ this.byQuestion.set(key, entry.queryId);
142
+ }
143
+ this.change_emit();
144
+ }
145
+ /**
146
+ * Looks a question up.
147
+ *
148
+ * @param criteria - The criteria being asked.
149
+ * @param server - The PACS being asked.
150
+ * @param owner - The identity asking; another identity's answer is not
151
+ * an answer to this question.
152
+ * @returns What is known, or null when this question is new to the index.
153
+ */
154
+ entry_find(criteria, server, owner) {
155
+ const queryId = this.byQuestion.get(queryKey_build(criteria, server, owner));
156
+ if (queryId === undefined)
157
+ return null;
158
+ return this.entries.get(queryId) ?? null;
159
+ }
160
+ /**
161
+ * Forgets one query, for when its stored answer turns out to be gone.
162
+ *
163
+ * @param criteria - The criteria it was filed under.
164
+ * @param server - The PACS it was asked of.
165
+ * @param owner - The identity that asked.
166
+ */
167
+ entry_drop(criteria, server, owner) {
168
+ const key = queryKey_build(criteria, server, owner);
169
+ const queryId = this.byQuestion.get(key);
170
+ if (queryId === undefined)
171
+ return;
172
+ this.byQuestion.delete(key);
173
+ this.entries.delete(queryId);
174
+ // An older record may still answer this question; the newest of what
175
+ // is left takes the pointer back.
176
+ let heir = null;
177
+ for (const entry of this.entries.values()) {
178
+ if (queryKey_build(entry.criteria, entry.server, entry.owner) !== key)
179
+ continue;
180
+ if (heir === null || Date.parse(entry.answeredAt) > Date.parse(heir.answeredAt))
181
+ heir = entry;
182
+ }
183
+ if (heir !== null)
184
+ this.byQuestion.set(key, heir.queryId);
185
+ this.change_emit();
186
+ }
187
+ /**
188
+ * Every record, for the callers that render the log rather than search it.
189
+ *
190
+ * This is the whole log — every query, not one per question — because
191
+ * that is what `/net/pacs/queries` lists and what an operator went
192
+ * looking for when they could not find a query they knew they had run.
193
+ */
194
+ entries_all() {
195
+ return [...this.entries.values()];
196
+ }
197
+ /** How many records the index holds. */
198
+ size_get() {
199
+ return this.entries.size;
200
+ }
201
+ /** How many distinct questions those records answer. */
202
+ questions_count() {
203
+ return this.byQuestion.size;
204
+ }
205
+ /**
206
+ * The newest record held, which is where a resumed sweep starts.
207
+ *
208
+ * A completed sweep has walked everything down to the floor, so the only
209
+ * thing a later boot can be missing is what arrived since. Asking CUBE
210
+ * for records at or after this turns a rebuild into a top-up.
211
+ *
212
+ * @returns An ISO timestamp, or null when the index is empty.
213
+ */
214
+ newest_get() {
215
+ let newest = null;
216
+ for (const entry of this.entries.values()) {
217
+ if (newest === null || Date.parse(entry.answeredAt) > Date.parse(newest)) {
218
+ newest = entry.answeredAt;
219
+ }
220
+ }
221
+ return newest;
222
+ }
223
+ /**
224
+ * The oldest record the back-fill has walked, where it resumes.
225
+ *
226
+ * @returns An ISO timestamp, or null before the first sweep.
227
+ */
228
+ floor_get() {
229
+ return this.floor;
230
+ }
231
+ /**
232
+ * Records how far back the sweep has reached.
233
+ *
234
+ * @param at - The oldest record walked, as an ISO timestamp.
235
+ */
236
+ floor_set(at) {
237
+ if (this.floor !== null && Date.parse(this.floor) <= Date.parse(at))
238
+ return;
239
+ this.floor = at;
240
+ this.change_emit();
241
+ }
242
+ /**
243
+ * Creates a persistence-safe copy.
244
+ *
245
+ * @returns The snapshot.
246
+ */
247
+ snapshot_create() {
248
+ const entries = [];
249
+ for (const entry of this.entries.values()) {
250
+ entries.push({ key: queryKey_build(entry.criteria, entry.server, entry.owner), ...entry });
251
+ }
252
+ return { entries, floor: this.floor };
253
+ }
254
+ /**
255
+ * Merges a snapshot in, keeping anything this process already learned:
256
+ * a query indexed since the checkpoint was written is newer than the
257
+ * checkpoint by definition.
258
+ *
259
+ * @param snapshot - A validated snapshot.
260
+ */
261
+ snapshot_restore(snapshot) {
262
+ for (const entry of snapshot.entries) {
263
+ if (this.entries.has(entry.queryId))
264
+ continue;
265
+ const { key: _key, ...held } = entry;
266
+ this.entry_note(held);
267
+ }
268
+ if (snapshot.floor !== null && (this.floor === null || Date.parse(snapshot.floor) < Date.parse(this.floor))) {
269
+ this.floor = snapshot.floor;
270
+ }
271
+ this.change_emit();
272
+ }
273
+ /**
274
+ * Registers a listener told after any mutation.
275
+ *
276
+ * @param listener - The listener.
277
+ * @returns A function that removes it.
278
+ */
279
+ changeListener_add(listener) {
280
+ this.listeners.add(listener);
281
+ return () => { this.listeners.delete(listener); };
282
+ }
283
+ /** Empties the index. For tests, and for a change of identity. */
284
+ reset() {
285
+ this.entries.clear();
286
+ this.byQuestion.clear();
287
+ this.floor = null;
288
+ this.change_emit();
289
+ }
290
+ /** Tells every listener. A listener that throws must not stop the rest. */
291
+ change_emit() {
292
+ for (const listener of this.listeners) {
293
+ try {
294
+ listener();
295
+ }
296
+ catch {
297
+ /* a listener's failure is its own */
298
+ }
299
+ }
300
+ }
301
+ }
302
+ exports.QueryIndex = QueryIndex;
303
+ let queryIndex = null;
304
+ /**
305
+ * The process-wide query index.
306
+ *
307
+ * @returns The singleton.
308
+ */
309
+ function queryIndex_get() {
310
+ if (queryIndex === null)
311
+ queryIndex = new QueryIndex();
312
+ return queryIndex;
313
+ }
314
+ //# sourceMappingURL=queryIndex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queryIndex.js","sourceRoot":"","sources":["../../src/cache/queryIndex.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /** Minimum spacing between two writes of the query-index checkpoint. */
2
+ export declare const QUERY_INDEX_CHECKPOINT_FLOOR_MS: number;
3
+ /**
4
+ * Outcome of attempting to restore a query-index checkpoint.
5
+ *
6
+ * @property restored - Whether validated entries reached the index.
7
+ * @property count - Number of restored queries.
8
+ * @property writtenAt - Checkpoint creation time when restoration succeeded.
9
+ * @property reason - Human-readable reason when restoration was skipped.
10
+ */
11
+ export interface QueryIndexRestoreResult {
12
+ restored: boolean;
13
+ count: number;
14
+ writtenAt?: string;
15
+ reason?: string;
16
+ }
17
+ /**
18
+ * Builds the identity-keyed checkpoint path without exposing the identity.
19
+ *
20
+ * @param identity - Canonical ChRIS identity string.
21
+ * @param root - Checkpoint directory override.
22
+ * @returns Versioned checkpoint path.
23
+ */
24
+ export declare function queryIndexCheckpointPath_get(identity: string, root?: string): string;
25
+ /**
26
+ * Restores a matching, validated checkpoint into the index.
27
+ *
28
+ * A rejected file is not an error worth raising: the index is advisory, so
29
+ * an unreadable checkpoint costs a sweep and nothing else.
30
+ *
31
+ * @param identity - Canonical ChRIS identity string.
32
+ * @param root - Checkpoint directory override.
33
+ * @returns Restoration outcome without throwing for absent or invalid files.
34
+ */
35
+ export declare function queryIndexCheckpoint_restore(identity: string, root?: string): Promise<QueryIndexRestoreResult>;
36
+ /**
37
+ * Atomically saves the query index.
38
+ *
39
+ * @param identity - Canonical ChRIS identity string.
40
+ * @param root - Checkpoint directory override.
41
+ * @returns Promise resolved after the mode-0600 file is durable in place.
42
+ */
43
+ export declare function queryIndexCheckpoint_save(identity: string, root?: string): Promise<void>;
44
+ /**
45
+ * Starts a throttled checkpoint writer for index mutations: a write at
46
+ * least `delayMs` after the first pending mutation, and no more often than
47
+ * once per `floorMs`, the last change never dropped.
48
+ *
49
+ * @param identity - Canonical ChRIS identity string.
50
+ * @param root - Checkpoint directory override.
51
+ * @param delayMs - Debounce interval in milliseconds.
52
+ * @param floorMs - Minimum spacing between two writes.
53
+ * @returns Function that stops watching and cancels a pending save.
54
+ */
55
+ export declare function queryIndexCheckpoint_watch(identity: string, root?: string, delayMs?: number, floorMs?: number): () => void;
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QUERY_INDEX_CHECKPOINT_FLOOR_MS = void 0;
4
+ exports.queryIndexCheckpointPath_get = queryIndexCheckpointPath_get;
5
+ exports.queryIndexCheckpoint_restore = queryIndexCheckpoint_restore;
6
+ exports.queryIndexCheckpoint_save = queryIndexCheckpoint_save;
7
+ exports.queryIndexCheckpoint_watch = queryIndexCheckpoint_watch;
8
+ /**
9
+ * @file Persistent checkpoint for the PACS query index.
10
+ *
11
+ * The index is expensive to build and cheap to keep: the back-fill walks a
12
+ * collection whose every row drags a compressed result, so the sweep is a
13
+ * cost worth paying once rather than at every boot. Written to disk and
14
+ * restored at start, it comes back with its floor, and the sweep resumes
15
+ * from there instead of starting over.
16
+ *
17
+ * Same shape as the listing checkpoint next door: one identity-keyed,
18
+ * mode-0600 file under a mode-0700 folder, written by same-directory atomic
19
+ * rename, throttled so a burst of queries coalesces into one write.
20
+ *
21
+ * @module
22
+ */
23
+ const crypto_1 = require("crypto");
24
+ const fs_1 = require("fs");
25
+ const os_1 = require("os");
26
+ const path_1 = require("path");
27
+ const queryIndex_1 = require("./queryIndex");
28
+ const QUERY_INDEX_SCHEMA = 1;
29
+ /** Minimum spacing between two writes of the query-index checkpoint. */
30
+ exports.QUERY_INDEX_CHECKPOINT_FLOOR_MS = 10 * 1000;
31
+ /** Returns the default directory for persistent index files. */
32
+ function queryIndexRoot_get() {
33
+ const cacheHome = process.env.XDG_CACHE_HOME ?? (0, path_1.join)((0, os_1.homedir)(), '.cache');
34
+ return (0, path_1.join)(cacheHome, 'chell', 'vfs');
35
+ }
36
+ /**
37
+ * Builds the identity-keyed checkpoint path without exposing the identity.
38
+ *
39
+ * @param identity - Canonical ChRIS identity string.
40
+ * @param root - Checkpoint directory override.
41
+ * @returns Versioned checkpoint path.
42
+ */
43
+ function queryIndexCheckpointPath_get(identity, root = queryIndexRoot_get()) {
44
+ const key = (0, crypto_1.createHash)('sha256').update(identity).digest('hex').slice(0, 16);
45
+ return (0, path_1.join)(root, `pacs-queries-${key}-v${QUERY_INDEX_SCHEMA}.json`);
46
+ }
47
+ /** Reports whether a value is a structurally valid persisted entry. */
48
+ function entry_check(value) {
49
+ if (!value || typeof value !== 'object')
50
+ return false;
51
+ const entry = value;
52
+ return typeof entry.key === 'string' && entry.key.length > 0 &&
53
+ typeof entry.queryId === 'number' && Number.isFinite(entry.queryId) &&
54
+ typeof entry.server === 'string' &&
55
+ typeof entry.owner === 'string' &&
56
+ typeof entry.answeredAt === 'string' && !Number.isNaN(Date.parse(entry.answeredAt)) &&
57
+ typeof entry.hasResult === 'boolean' &&
58
+ entry.criteria !== undefined && entry.criteria !== null && typeof entry.criteria === 'object';
59
+ }
60
+ /** Reports whether parsed JSON is a compatible checkpoint for an identity. */
61
+ function file_check(value, identity) {
62
+ if (!value || typeof value !== 'object')
63
+ return false;
64
+ const file = value;
65
+ return file.schemaVersion === QUERY_INDEX_SCHEMA &&
66
+ file.identity === identity &&
67
+ typeof file.writtenAt === 'string' &&
68
+ !Number.isNaN(Date.parse(file.writtenAt)) &&
69
+ (file.floor === null || (typeof file.floor === 'string' && !Number.isNaN(Date.parse(file.floor)))) &&
70
+ Array.isArray(file.entries) &&
71
+ file.entries.every(entry_check);
72
+ }
73
+ /**
74
+ * Restores a matching, validated checkpoint into the index.
75
+ *
76
+ * A rejected file is not an error worth raising: the index is advisory, so
77
+ * an unreadable checkpoint costs a sweep and nothing else.
78
+ *
79
+ * @param identity - Canonical ChRIS identity string.
80
+ * @param root - Checkpoint directory override.
81
+ * @returns Restoration outcome without throwing for absent or invalid files.
82
+ */
83
+ async function queryIndexCheckpoint_restore(identity, root = queryIndexRoot_get()) {
84
+ try {
85
+ const raw = await fs_1.promises.readFile(queryIndexCheckpointPath_get(identity, root), 'utf8');
86
+ const parsed = JSON.parse(raw);
87
+ if (!file_check(parsed, identity)) {
88
+ return { restored: false, count: 0, reason: 'incompatible query index' };
89
+ }
90
+ const snapshot = { entries: parsed.entries, floor: parsed.floor };
91
+ (0, queryIndex_1.queryIndex_get)().snapshot_restore(snapshot);
92
+ return { restored: true, count: parsed.entries.length, writtenAt: parsed.writtenAt };
93
+ }
94
+ catch (error) {
95
+ const code = error.code;
96
+ return {
97
+ restored: false,
98
+ count: 0,
99
+ reason: code === 'ENOENT' ? 'no query index' : (error instanceof Error ? error.message : String(error)),
100
+ };
101
+ }
102
+ }
103
+ /**
104
+ * Atomically saves the query index.
105
+ *
106
+ * @param identity - Canonical ChRIS identity string.
107
+ * @param root - Checkpoint directory override.
108
+ * @returns Promise resolved after the mode-0600 file is durable in place.
109
+ */
110
+ async function queryIndexCheckpoint_save(identity, root = queryIndexRoot_get()) {
111
+ const path = queryIndexCheckpointPath_get(identity, root);
112
+ const temporaryPath = `${path}.${process.pid}-${(0, crypto_1.randomUUID)()}.tmp`;
113
+ const snapshot = (0, queryIndex_1.queryIndex_get)().snapshot_create();
114
+ const file = {
115
+ schemaVersion: QUERY_INDEX_SCHEMA,
116
+ identity,
117
+ writtenAt: new Date().toISOString(),
118
+ floor: snapshot.floor,
119
+ entries: snapshot.entries,
120
+ };
121
+ await fs_1.promises.mkdir((0, path_1.dirname)(path), { recursive: true, mode: 0o700 });
122
+ await fs_1.promises.chmod((0, path_1.dirname)(path), 0o700);
123
+ await fs_1.promises.writeFile(temporaryPath, `${JSON.stringify(file)}\n`, { encoding: 'utf8', mode: 0o600 });
124
+ await fs_1.promises.rename(temporaryPath, path);
125
+ await fs_1.promises.chmod(path, 0o600);
126
+ }
127
+ /**
128
+ * Starts a throttled checkpoint writer for index mutations: a write at
129
+ * least `delayMs` after the first pending mutation, and no more often than
130
+ * once per `floorMs`, the last change never dropped.
131
+ *
132
+ * @param identity - Canonical ChRIS identity string.
133
+ * @param root - Checkpoint directory override.
134
+ * @param delayMs - Debounce interval in milliseconds.
135
+ * @param floorMs - Minimum spacing between two writes.
136
+ * @returns Function that stops watching and cancels a pending save.
137
+ */
138
+ function queryIndexCheckpoint_watch(identity, root = queryIndexRoot_get(), delayMs = 2000, floorMs = exports.QUERY_INDEX_CHECKPOINT_FLOOR_MS) {
139
+ let timer = null;
140
+ let lastWrite = 0;
141
+ const listener_remove = (0, queryIndex_1.queryIndex_get)().changeListener_add(() => {
142
+ if (timer)
143
+ return;
144
+ const wait = Math.max(delayMs, lastWrite + floorMs - Date.now());
145
+ timer = setTimeout(() => {
146
+ timer = null;
147
+ lastWrite = Date.now();
148
+ void queryIndexCheckpoint_save(identity, root).catch(() => { });
149
+ }, wait);
150
+ timer.unref();
151
+ });
152
+ return () => {
153
+ listener_remove();
154
+ if (timer)
155
+ clearTimeout(timer);
156
+ timer = null;
157
+ };
158
+ }
159
+ //# sourceMappingURL=queryIndexCheckpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queryIndexCheckpoint.js","sourceRoot":"","sources":["../../src/cache/queryIndexCheckpoint.ts"],"names":[],"mappings":"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"}
@@ -95,6 +95,35 @@ export interface PageWalkStep<T> {
95
95
  * ```
96
96
  */
97
97
  export declare function listPages_walk<T>(page_fetch: (offset: number, limit: number) => Promise<ListPage<T>>, options?: PageWalkOptions): AsyncGenerator<PageWalkStep<T>, void, void>;
98
+ /**
99
+ * Options for a windowed walk. See {@link listPages_walkWindowed}.
100
+ *
101
+ * @property pageSize - Rows requested per page. Defaults to 100.
102
+ * @property inFlight - Pages fetched concurrently once the total is known.
103
+ * Defaults to 4.
104
+ */
105
+ export interface WindowedPageWalkOptions {
106
+ pageSize?: number;
107
+ inFlight?: number;
108
+ }
109
+ /**
110
+ * Walks every page of a paginated CUBE list with several pages in flight.
111
+ *
112
+ * The one pagination loop, windowed: the first page is fetched alone to
113
+ * learn the server's total, and every further offset is then known, so up
114
+ * to `inFlight` pages are fetched at once and yielded as each lands — in
115
+ * completion order, which is why this walk suits a consumer that keys rows
116
+ * by id and commits the whole set when it is done, and not one that reads
117
+ * rows in server order or resumes from an offset. A server that reports no
118
+ * total is walked one page at a time, exactly as {@link listPages_walk}
119
+ * does. A fetch error ends the walk once the pages already in flight have
120
+ * settled, and propagates.
121
+ *
122
+ * @param page_fetch - Fetches one page at the given offset and limit.
123
+ * @param options - Page size and window width. See WindowedPageWalkOptions.
124
+ * @returns Async generator yielding a PageWalkStep per fetched page.
125
+ */
126
+ export declare function listPages_walkWindowed<T>(page_fetch: (offset: number, limit: number) => Promise<ListPage<T>>, options?: WindowedPageWalkOptions): AsyncGenerator<PageWalkStep<T>, void, void>;
98
127
  /**
99
128
  * Drains every page of a paginated CUBE list into one array.
100
129
  *
@@ -333,6 +362,33 @@ export interface ComputeResourceData {
333
362
  name?: string;
334
363
  [key: string]: unknown;
335
364
  }
365
+ /**
366
+ * Wire shape of one registered plugin row, as served by `getPlugins`.
367
+ *
368
+ * @property id - Plugin id.
369
+ * @property name - The plugin's registered name (`pl-dcm2niix`).
370
+ * @property version - Its registered version.
371
+ * @property type - `ds`, `fs` or `ts`: what the node consumes and produces.
372
+ * @property title - The human title, when the plugin declared one.
373
+ * @property authors - Declared authorship.
374
+ * @property description - What the plugin does.
375
+ * @property documentation - Where its source or docs live.
376
+ * @property category - The plugin's declared category.
377
+ * @property creation_date - ISO registration timestamp.
378
+ */
379
+ export interface PluginData {
380
+ id?: number;
381
+ name?: string;
382
+ version?: string;
383
+ type?: string;
384
+ title?: string;
385
+ authors?: string;
386
+ description?: string;
387
+ documentation?: string;
388
+ category?: string;
389
+ creation_date?: string;
390
+ [key: string]: unknown;
391
+ }
336
392
  /**
337
393
  * Typed handle over one plugin detail resource (metadata surfaces only).
338
394
  */
@@ -412,6 +468,41 @@ export declare function collectionPage_wrap<T>(collection: {
412
468
  totalCount: number;
413
469
  hasNextPage: boolean;
414
470
  }, rows: T[]): ListPage<T>;
471
+ /**
472
+ * One registered plugin resolved from CUBE: its row, and a handle onto the
473
+ * sub-collections that hang off it.
474
+ *
475
+ * @property data - The plugin's wire payload.
476
+ * @property handle - Typed access to its parameters and compute resources.
477
+ */
478
+ export interface PluginFound {
479
+ data: PluginData;
480
+ handle: PluginHandle;
481
+ }
482
+ /**
483
+ * Fetches one registered plugin by exact name and version.
484
+ *
485
+ * @param client - Connected chrisapi client.
486
+ * @param name - Exact plugin name.
487
+ * @param version - Exact plugin version.
488
+ * @returns The plugin and a handle onto it, or null when CUBE has no such
489
+ * plugin at that version.
490
+ * @throws {Error} Propagates chrisapi/network errors.
491
+ */
492
+ export declare function plugin_find(client: Client, name: string, version: string): Promise<PluginFound | null>;
493
+ /**
494
+ * Reads every parameter a plugin declares, to exhaustion.
495
+ *
496
+ * A plugin's parameter list is the one collection a surface cannot ask
497
+ * twice about: a form built from a truncated list silently omits flags the
498
+ * plugin requires. The single-page `{ limit: 100 }` idiom this replaces lost
499
+ * the tail of any plugin with more, and said nothing.
500
+ *
501
+ * @param handle - A plugin handle.
502
+ * @returns Every declared parameter, in server order.
503
+ * @throws {Error} Propagates chrisapi/network errors.
504
+ */
505
+ export declare function pluginParameters_drain(handle: PluginHandle): Promise<PluginParameterData[]>;
415
506
  /**
416
507
  * Fetches one registered pipeline's detail resource as a typed handle.
417
508
  *
@@ -430,6 +521,19 @@ export declare function pipeline_get(client: Client, pipelineID: number): Promis
430
521
  * @throws {Error} Propagates chrisapi/network errors.
431
522
  */
432
523
  export declare function pipelineSourceFilesPage_get(client: Client, params: Record<string, unknown>): Promise<ListPage<PipelineSourceFileData>>;
524
+ /**
525
+ * Drains every pipeline source file matching a filter.
526
+ *
527
+ * The registered pipelines' source files are a collection, not a page: a
528
+ * caller asking which file a pipeline came from wants the answer, and
529
+ * `{ limit: 1000 }` was a guess at how many that could be (#401).
530
+ *
531
+ * @param client - Connected chrisapi client.
532
+ * @param filter - Server-side filter (a `pipeline_id`, an `fname`), or none.
533
+ * @returns Every matching source file, in server order.
534
+ * @throws {Error} Propagates chrisapi/network errors.
535
+ */
536
+ export declare function pipelineSourceFiles_drain(client: Client, filter?: Record<string, unknown>): Promise<PipelineSourceFileData[]>;
433
537
  /**
434
538
  * Creates a CUBE download token for authenticated file access and LONK.
435
539
  *