@fnndsc/salsa 3.21.2 → 4.0.1
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/files/index.d.ts +46 -3
- package/dist/files/index.js +150 -14
- package/dist/files/index.js.map +1 -1
- package/dist/files/regular_content.d.ts +2 -0
- package/dist/files/regular_content.js +11 -1
- package/dist/files/regular_content.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/vfs/providers/etc.d.ts +7 -2
- package/dist/vfs/providers/etc.js +11 -2
- package/dist/vfs/providers/etc.js.map +1 -1
- package/dist/vfs/providers/native.d.ts +62 -1
- package/dist/vfs/providers/native.js +220 -1
- package/dist/vfs/providers/native.js.map +1 -1
- package/dist/vfs/providers/pacs.d.ts +10 -3
- package/dist/vfs/providers/pacs.js +16 -3
- package/dist/vfs/providers/pacs.js.map +1 -1
- package/dist/vfs/providers/proc.d.ts +84 -9
- package/dist/vfs/providers/proc.js +276 -29
- package/dist/vfs/providers/proc.js.map +1 -1
- package/dist/vfs/providers/procTags.d.ts +26 -4
- package/dist/vfs/providers/procTags.js +40 -0
- package/dist/vfs/providers/procTags.js.map +1 -1
- package/dist/vfs/providers/share.d.ts +7 -2
- package/dist/vfs/providers/share.js +9 -0
- package/dist/vfs/providers/share.js.map +1 -1
- package/dist/vfs/providers/workflows.d.ts +7 -2
- package/dist/vfs/providers/workflows.js +9 -0
- package/dist/vfs/providers/workflows.js.map +1 -1
- package/package.json +3 -3
|
@@ -6,10 +6,15 @@
|
|
|
6
6
|
* immutable and whose freshness is visit-driven.
|
|
7
7
|
*
|
|
8
8
|
* Freshness model (every visit is a cheap delta, never a re-crawl):
|
|
9
|
-
* - ls /proc/jobs → roster delta
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* - ls /proc/jobs → roster delta: each collection's newest page, read
|
|
10
|
+
* unfiltered, brings in the feeds newer than the
|
|
11
|
+
* highest known id, and its total is compared with
|
|
12
|
+
* the last walk's — a total that moved (a share, an
|
|
13
|
+
* unshare, a deletion) promotes the visit to the
|
|
14
|
+
* full index walk, which also runs once the roster
|
|
15
|
+
* is older than {@link ROSTER_FULL_WALK_MS};
|
|
16
|
+
* - a feed the roster lacks, asked for by name or id, is asked of CUBE live
|
|
17
|
+
* and adopted ({@link procFeed_probe});
|
|
13
18
|
* - ls /proc/jobs/N → first visit loads the topology once; every later
|
|
14
19
|
* visit runs {@link feedVisit_sync}: one feed-row
|
|
15
20
|
* fetch (job counters), then, only when something
|
|
@@ -22,6 +27,7 @@
|
|
|
22
27
|
*
|
|
23
28
|
* @module
|
|
24
29
|
*/
|
|
30
|
+
import { type VfsOutcome } from '@fnndsc/fond';
|
|
25
31
|
import { type ProcRosterSyncKind, ProcCache, ProcFeed, Result } from '@fnndsc/cumin';
|
|
26
32
|
import { VFSProvider, VFSItem, CpOptions } from '../provider.js';
|
|
27
33
|
/** Pages of one feed's topology walk fetched at once (PACS plural's precedent: four). */
|
|
@@ -32,6 +38,13 @@ export declare const FEED_RECHECK_MS: number;
|
|
|
32
38
|
export declare const ROSTER_FULL_WALK_MS: number;
|
|
33
39
|
/** Visits to one feed closer together than this share the first visit's result. */
|
|
34
40
|
export declare const FEED_VISIT_COALESCE_MS: number;
|
|
41
|
+
/**
|
|
42
|
+
* Rows of a collection's newest page a roster delta reads. CUBE orders feeds
|
|
43
|
+
* newest first, so the page holds every feed newer than the roster unless
|
|
44
|
+
* more than this many arrived between two visits; it is small because the
|
|
45
|
+
* surfaces re-ask the roster on a timer.
|
|
46
|
+
*/
|
|
47
|
+
export declare const ROSTER_DELTA_PAGE: number;
|
|
35
48
|
/** Lifecycle states for the session's global topology sweep. */
|
|
36
49
|
export type ProcTopologyState = 'idle' | 'running' | 'complete' | 'failed';
|
|
37
50
|
/**
|
|
@@ -103,6 +116,8 @@ export declare class ProcVfsProvider implements VFSProvider {
|
|
|
103
116
|
sort?: 'name' | 'size' | 'date' | 'owner';
|
|
104
117
|
reverse?: boolean;
|
|
105
118
|
}): Promise<Result<VFSItem[]>>;
|
|
119
|
+
/** @inheritdoc */
|
|
120
|
+
readBinary(pathStr: string): Promise<VfsOutcome<Buffer>>;
|
|
106
121
|
/**
|
|
107
122
|
* Raw bytes exist only under the `data` link (an image in a job's output
|
|
108
123
|
* space); everything else in `/proc` is synthesized text, served as its
|
|
@@ -112,16 +127,22 @@ export declare class ProcVfsProvider implements VFSProvider {
|
|
|
112
127
|
* @param pathStr - Absolute `/proc/...` path.
|
|
113
128
|
* @returns The file's bytes.
|
|
114
129
|
*/
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
130
|
+
private bytes_read;
|
|
131
|
+
/** @inheritdoc */
|
|
132
|
+
read(pathStr: string): Promise<VfsOutcome<string>>;
|
|
133
|
+
private text_read;
|
|
134
|
+
feedJobs_cancel(pathStr: string, _options?: {
|
|
118
135
|
recursive?: boolean;
|
|
119
136
|
force?: boolean;
|
|
120
137
|
}): Promise<boolean>;
|
|
121
|
-
|
|
138
|
+
/** @inheritdoc */
|
|
139
|
+
cp(_src: string, _dst: string, _options?: CpOptions): Promise<VfsOutcome>;
|
|
140
|
+
private copy_run;
|
|
122
141
|
mv(_src: string, _dst: string): Promise<boolean>;
|
|
123
142
|
touch(_pathStr: string): Promise<boolean>;
|
|
124
143
|
upload(_localPath: string, _remotePath: string): Promise<boolean>;
|
|
144
|
+
/** @inheritdoc */
|
|
145
|
+
write(pathStr: string, content: string): Promise<VfsOutcome>;
|
|
125
146
|
/**
|
|
126
147
|
* Writes a projected file. One file under /proc takes writes: a feed's
|
|
127
148
|
* note (`/proc/jobs/feed_N/note`), whose content is replaced whole; its
|
|
@@ -131,7 +152,7 @@ export declare class ProcVfsProvider implements VFSProvider {
|
|
|
131
152
|
* @param content - The new content, whole.
|
|
132
153
|
* @returns True when it was written.
|
|
133
154
|
*/
|
|
134
|
-
|
|
155
|
+
private file_write;
|
|
135
156
|
}
|
|
136
157
|
/**
|
|
137
158
|
* Ensures instance topology is loaded for a feed without clearing existing data.
|
|
@@ -157,6 +178,20 @@ export declare function procFeed_ensureLoaded(feedID: number): Promise<void>;
|
|
|
157
178
|
* @param feedID - Feed whose metadata to ensure.
|
|
158
179
|
*/
|
|
159
180
|
export declare function feedMeta_ensure(feedID: number): Promise<void>;
|
|
181
|
+
/**
|
|
182
|
+
* Asks CUBE for one feed, live, and adopts it into the roster: the way a
|
|
183
|
+
* feed shared to this identity, or made public, since the roster was
|
|
184
|
+
* walked is seen when it is asked for by name or id. A feed the roster
|
|
185
|
+
* already holds has its row refreshed. The yardsticks move with an
|
|
186
|
+
* adoption, so the next delta does not walk again for what this ask found.
|
|
187
|
+
*
|
|
188
|
+
* @param feedID - The feed to ask for.
|
|
189
|
+
* @returns The feed as the roster now holds it, or null when CUBE lists it
|
|
190
|
+
* in neither collection for this identity — not shared, or shared to a
|
|
191
|
+
* group the identity is not in.
|
|
192
|
+
* @throws {Error} When not connected.
|
|
193
|
+
*/
|
|
194
|
+
export declare function procFeed_probe(feedID: number): Promise<ProcFeed | null>;
|
|
160
195
|
export declare function feedStatus_refresh(feedID: number): Promise<void>;
|
|
161
196
|
/**
|
|
162
197
|
* Whether every cached instance of a feed carries a terminal status. A fully
|
|
@@ -250,6 +285,46 @@ export declare function procTopologyCatchup_status(): {
|
|
|
250
285
|
* @returns Resolves when no walk is queued or running.
|
|
251
286
|
*/
|
|
252
287
|
export declare function procTopologyCatchup_await(): Promise<void>;
|
|
288
|
+
/**
|
|
289
|
+
* One place a jump can land: a feed, or a node within one.
|
|
290
|
+
*
|
|
291
|
+
* @property kind - A feed, or a node (plugin instance).
|
|
292
|
+
* @property feedId - The feed, or the node's feed.
|
|
293
|
+
* @property nodeId - The node, for a node.
|
|
294
|
+
* @property name - The `/proc` name: `feed_4389`, or `pl-dicom_repack_639204`.
|
|
295
|
+
* @property title - The feed's title.
|
|
296
|
+
*/
|
|
297
|
+
export interface ProcLocation {
|
|
298
|
+
kind: 'feed' | 'node';
|
|
299
|
+
feedId: number;
|
|
300
|
+
nodeId?: number;
|
|
301
|
+
name: string;
|
|
302
|
+
title: string;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* What a jump target names: a feed only (`feed_4389`), a node only
|
|
306
|
+
* (`pl-dicom_repack_639204`, or `_639204`), or a bare number that may be
|
|
307
|
+
* either. Null for anything else.
|
|
308
|
+
*
|
|
309
|
+
* @param target - What the operator typed.
|
|
310
|
+
* @returns The id and which kinds it may name, or null.
|
|
311
|
+
*/
|
|
312
|
+
export declare function procTarget_parse(target: string): {
|
|
313
|
+
id: number;
|
|
314
|
+
feed: boolean;
|
|
315
|
+
node: boolean;
|
|
316
|
+
} | null;
|
|
317
|
+
/**
|
|
318
|
+
* Finds where a jump target lands, asking CUBE live for what the roster
|
|
319
|
+
* and the index lack: a feed shared since the roster was walked, or a node
|
|
320
|
+
* in one. A node's feed is adopted into the roster on the way, so the
|
|
321
|
+
* place the answer names can be entered at once. A bare number that is
|
|
322
|
+
* both a feed and a node answers both; the caller asks which.
|
|
323
|
+
*
|
|
324
|
+
* @param target - A feed id, a node id, or a `feed_N` / `<plugin>_N` name.
|
|
325
|
+
* @returns Every place it names that this identity can see; empty for none.
|
|
326
|
+
*/
|
|
327
|
+
export declare function procTarget_locate(target: string): Promise<ProcLocation[]>;
|
|
253
328
|
/**
|
|
254
329
|
* Forgets all visit bookkeeping (tests, and a cache cleared underneath us).
|
|
255
330
|
*/
|
|
@@ -7,10 +7,15 @@
|
|
|
7
7
|
* immutable and whose freshness is visit-driven.
|
|
8
8
|
*
|
|
9
9
|
* Freshness model (every visit is a cheap delta, never a re-crawl):
|
|
10
|
-
* - ls /proc/jobs → roster delta
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* - ls /proc/jobs → roster delta: each collection's newest page, read
|
|
11
|
+
* unfiltered, brings in the feeds newer than the
|
|
12
|
+
* highest known id, and its total is compared with
|
|
13
|
+
* the last walk's — a total that moved (a share, an
|
|
14
|
+
* unshare, a deletion) promotes the visit to the
|
|
15
|
+
* full index walk, which also runs once the roster
|
|
16
|
+
* is older than {@link ROSTER_FULL_WALK_MS};
|
|
17
|
+
* - a feed the roster lacks, asked for by name or id, is asked of CUBE live
|
|
18
|
+
* and adopted ({@link procFeed_probe});
|
|
14
19
|
* - ls /proc/jobs/N → first visit loads the topology once; every later
|
|
15
20
|
* visit runs {@link feedVisit_sync}: one feed-row
|
|
16
21
|
* fetch (job counters), then, only when something
|
|
@@ -24,7 +29,7 @@
|
|
|
24
29
|
* @module
|
|
25
30
|
*/
|
|
26
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
-
exports.ProcVfsProvider = exports.FEED_VISIT_COALESCE_MS = exports.ROSTER_FULL_WALK_MS = exports.FEED_RECHECK_MS = exports.FEED_WALK_IN_FLIGHT = void 0;
|
|
32
|
+
exports.ProcVfsProvider = exports.ROSTER_DELTA_PAGE = exports.FEED_VISIT_COALESCE_MS = exports.ROSTER_FULL_WALK_MS = exports.FEED_RECHECK_MS = exports.FEED_WALK_IN_FLIGHT = void 0;
|
|
28
33
|
exports.feedInstances_ensureStarted = feedInstances_ensureStarted;
|
|
29
34
|
exports.procFeed_refreshStart = procFeed_refreshStart;
|
|
30
35
|
exports.feedInstances_ensureLoaded = feedInstances_ensureLoaded;
|
|
@@ -32,6 +37,7 @@ exports.feedStatus_derive = feedStatus_derive;
|
|
|
32
37
|
exports.procPath_parse = procPath_parse;
|
|
33
38
|
exports.procFeed_ensureLoaded = procFeed_ensureLoaded;
|
|
34
39
|
exports.feedMeta_ensure = feedMeta_ensure;
|
|
40
|
+
exports.procFeed_probe = procFeed_probe;
|
|
35
41
|
exports.feedStatus_refresh = feedStatus_refresh;
|
|
36
42
|
exports.feedCached_isSettled = feedCached_isSettled;
|
|
37
43
|
exports.feedVisit_sync = feedVisit_sync;
|
|
@@ -41,6 +47,8 @@ exports.procRoster_bootSync = procRoster_bootSync;
|
|
|
41
47
|
exports.procTopologyCatchup_enqueue = procTopologyCatchup_enqueue;
|
|
42
48
|
exports.procTopologyCatchup_status = procTopologyCatchup_status;
|
|
43
49
|
exports.procTopologyCatchup_await = procTopologyCatchup_await;
|
|
50
|
+
exports.procTarget_parse = procTarget_parse;
|
|
51
|
+
exports.procTarget_locate = procTarget_locate;
|
|
44
52
|
exports.procVisitState_reset = procVisitState_reset;
|
|
45
53
|
exports.procDataFacts_start = procDataFacts_start;
|
|
46
54
|
exports.procTopology_warmup = procTopology_warmup;
|
|
@@ -49,6 +57,7 @@ exports.procTopology_retry = procTopology_retry;
|
|
|
49
57
|
exports.procTopology_status = procTopology_status;
|
|
50
58
|
exports.procTopology_await = procTopology_await;
|
|
51
59
|
exports.procCache_refresh = procCache_refresh;
|
|
60
|
+
const fond_1 = require("@fnndsc/fond");
|
|
52
61
|
const cumin_1 = require("@fnndsc/cumin");
|
|
53
62
|
const index_js_1 = require("../../jobs/index.js");
|
|
54
63
|
const feedJoins_js_1 = require("../../dag/feedJoins.js");
|
|
@@ -68,10 +77,18 @@ exports.FEED_RECHECK_MS = 10 * 60 * 1000;
|
|
|
68
77
|
exports.ROSTER_FULL_WALK_MS = 10 * 60 * 1000;
|
|
69
78
|
/** Visits to one feed closer together than this share the first visit's result. */
|
|
70
79
|
exports.FEED_VISIT_COALESCE_MS = 1000;
|
|
80
|
+
/**
|
|
81
|
+
* Rows of a collection's newest page a roster delta reads. CUBE orders feeds
|
|
82
|
+
* newest first, so the page holds every feed newer than the roster unless
|
|
83
|
+
* more than this many arrived between two visits; it is small because the
|
|
84
|
+
* surfaces re-ask the roster on a timer.
|
|
85
|
+
*/
|
|
86
|
+
exports.ROSTER_DELTA_PAGE = 20;
|
|
71
87
|
const feedVisits = new Map();
|
|
72
88
|
const feedVisitInflight = new Map();
|
|
73
89
|
let rosterWalkedAt = 0;
|
|
74
90
|
let rosterSyncInflight = null;
|
|
91
|
+
let rosterTotals = { owned: null, public: null };
|
|
75
92
|
let procTopologyPromise = null;
|
|
76
93
|
let procTopologyFailure;
|
|
77
94
|
let procTopologyResumeState = null;
|
|
@@ -92,14 +109,23 @@ function procFeed_create(feed) {
|
|
|
92
109
|
createdJobs: Number(feed.created_jobs ?? 0),
|
|
93
110
|
};
|
|
94
111
|
}
|
|
95
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Indexes one complete paginated CUBE feed collection into the shared cache.
|
|
114
|
+
*
|
|
115
|
+
* @param page_fetch - Fetches one page of the collection.
|
|
116
|
+
* @param indexed - The feeds indexed so far, keyed by id; this walk adds to it.
|
|
117
|
+
* @returns The collection's total as the walk latched it, or null when no page carried one.
|
|
118
|
+
*/
|
|
96
119
|
async function procFeeds_index(page_fetch, indexed) {
|
|
120
|
+
let total = null;
|
|
97
121
|
for await (const step of (0, cumin_1.listPages_walk)((offset, limit) => page_fetch({ limit, offset }), { pageSize: PAGE })) {
|
|
122
|
+
total = step.total ?? total;
|
|
98
123
|
for (const feed of step.items) {
|
|
99
124
|
const procFeed = procFeed_create(feed);
|
|
100
125
|
indexed.set(procFeed.id, procFeed);
|
|
101
126
|
}
|
|
102
127
|
}
|
|
128
|
+
return total;
|
|
103
129
|
}
|
|
104
130
|
// ── Cache build ────────────────────────────────────────────────────────────
|
|
105
131
|
/**
|
|
@@ -114,11 +140,11 @@ async function procCache_build() {
|
|
|
114
140
|
throw new Error('not connected');
|
|
115
141
|
}
|
|
116
142
|
const indexed = new Map();
|
|
117
|
-
await procFeeds_index((params) => (0, cumin_1.feedsPage_get)(client, params), indexed);
|
|
143
|
+
const ownedTotal = await procFeeds_index((params) => (0, cumin_1.feedsPage_get)(client, params), indexed);
|
|
118
144
|
// Older CUBEs lack a public-feeds endpoint: the contract answers null
|
|
119
145
|
// without a wire call, and an empty first page ends the index loop.
|
|
120
146
|
const publicBefore = indexed.size;
|
|
121
|
-
await procFeeds_index(async (params) => (await (0, cumin_1.publicFeedsPage_get)(client, params)) ?? { data: [], totalCount: 0 }, indexed);
|
|
147
|
+
let publicTotal = await procFeeds_index(async (params) => (await (0, cumin_1.publicFeedsPage_get)(client, params)) ?? { data: [], totalCount: 0 }, indexed);
|
|
122
148
|
// An empty public walk while the cache already holds public feeds is a
|
|
123
149
|
// failed SOURCE, not an authoritative absence: reconciling against it
|
|
124
150
|
// would amputate every public feed from the roster (and the checkpoint
|
|
@@ -129,8 +155,11 @@ async function procCache_build() {
|
|
|
129
155
|
cumin_1.errorStack.stack_push('warning', `proc roster: public-feeds source returned nothing while ${knownPublic.length} public feeds are known; keeping them`);
|
|
130
156
|
for (const feed of knownPublic)
|
|
131
157
|
indexed.set(feed.id, feed);
|
|
158
|
+
// No yardstick from a failed source: the next delta takes what it reads.
|
|
159
|
+
publicTotal = null;
|
|
132
160
|
}
|
|
133
161
|
}
|
|
162
|
+
rosterTotals = { owned: ownedTotal, public: publicTotal };
|
|
134
163
|
const knownBefore = new Set(cache.feedIDs_get());
|
|
135
164
|
const reconciliationTargets = cache.feeds_reconcile(Array.from(indexed.values()));
|
|
136
165
|
for (const feedID of reconciliationTargets)
|
|
@@ -294,11 +323,9 @@ async function procFeed_evict(feedID) {
|
|
|
294
323
|
const client = await cumin_1.chrisConnection.client_get();
|
|
295
324
|
if (!client)
|
|
296
325
|
return;
|
|
297
|
-
const
|
|
298
|
-
const row = own.data[0]
|
|
299
|
-
?? ((await (0, cumin_1.publicFeedsPage_get)(client, { id: feedID, limit: 1, offset: 0 })) ?? { data: [], totalCount: 0 }).data[0];
|
|
326
|
+
const row = await feedRow_fetch(client, feedID);
|
|
300
327
|
if (row)
|
|
301
|
-
cache.feed_add(procFeed_create(row));
|
|
328
|
+
cache.feed_add(procFeed_create(row.feed));
|
|
302
329
|
}
|
|
303
330
|
/**
|
|
304
331
|
* The operator's `proc refresh <feed>`: evicts the feed and STARTS its
|
|
@@ -544,6 +571,11 @@ class ProcVfsProvider {
|
|
|
544
571
|
}
|
|
545
572
|
// /proc/jobs/feed_N — list root instances + feed virtual files
|
|
546
573
|
if (feedID !== null && instanceID === null) {
|
|
574
|
+
// A feed the roster lacks is asked of CUBE first: shared to this
|
|
575
|
+
// identity, or made public, since the walk, it is adopted here and
|
|
576
|
+
// listed as any other; a feed CUBE does not list is an empty listing.
|
|
577
|
+
if (cache.feed_get(feedID) === undefined && (await procFeedAbsent_probe(feedID)) === null)
|
|
578
|
+
return (0, cumin_1.Ok)([]);
|
|
547
579
|
const wasLoaded = cache.topologyLoaded_has(feedID);
|
|
548
580
|
await feedInstances_ensureLoaded(feedID);
|
|
549
581
|
const feed = cache.feed_get(feedID);
|
|
@@ -614,6 +646,10 @@ class ProcVfsProvider {
|
|
|
614
646
|
}
|
|
615
647
|
return (0, cumin_1.Ok)([]);
|
|
616
648
|
}
|
|
649
|
+
/** @inheritdoc */
|
|
650
|
+
async readBinary(pathStr) {
|
|
651
|
+
return (0, fond_1.vfsOutcome_ofResult)(await this.bytes_read(pathStr), 'ENOENT');
|
|
652
|
+
}
|
|
617
653
|
/**
|
|
618
654
|
* Raw bytes exist only under the `data` link (an image in a job's output
|
|
619
655
|
* space); everything else in `/proc` is synthesized text, served as its
|
|
@@ -623,7 +659,7 @@ class ProcVfsProvider {
|
|
|
623
659
|
* @param pathStr - Absolute `/proc/...` path.
|
|
624
660
|
* @returns The file's bytes.
|
|
625
661
|
*/
|
|
626
|
-
async
|
|
662
|
+
async bytes_read(pathStr) {
|
|
627
663
|
await cache_ensure();
|
|
628
664
|
const clean = pathStr.replace(/\/$/, '');
|
|
629
665
|
const { instanceID, virtualFile, dataRemainder } = procPath_parse(clean);
|
|
@@ -641,7 +677,11 @@ class ProcVfsProvider {
|
|
|
641
677
|
const text = await this.read(pathStr);
|
|
642
678
|
return text.ok ? (0, cumin_1.Ok)(Buffer.from(text.value, 'utf8')) : text;
|
|
643
679
|
}
|
|
680
|
+
/** @inheritdoc */
|
|
644
681
|
async read(pathStr) {
|
|
682
|
+
return (0, fond_1.vfsOutcome_ofResult)(await this.text_read(pathStr), 'ENOENT');
|
|
683
|
+
}
|
|
684
|
+
async text_read(pathStr) {
|
|
645
685
|
await cache_ensure();
|
|
646
686
|
const cache = (0, cumin_1.procCache_get)();
|
|
647
687
|
const clean = pathStr.replace(/\/$/, '');
|
|
@@ -667,7 +707,7 @@ class ProcVfsProvider {
|
|
|
667
707
|
const note = await (0, cumin_1.feedNote_get)(feedID);
|
|
668
708
|
return note.ok ? (0, cumin_1.Ok)(note.value.content) : (0, cumin_1.Err)();
|
|
669
709
|
}
|
|
670
|
-
return (0, cumin_1.
|
|
710
|
+
return (0, cumin_1.Err)();
|
|
671
711
|
}
|
|
672
712
|
// Instance-level virtual files — all live or cached-on-first-read
|
|
673
713
|
if (instanceID !== null && virtualFile !== null) {
|
|
@@ -709,9 +749,11 @@ class ProcVfsProvider {
|
|
|
709
749
|
return (0, cumin_1.Ok)(logResult.ok ? logResult.value : '(log unavailable)');
|
|
710
750
|
}
|
|
711
751
|
}
|
|
712
|
-
|
|
752
|
+
// Anything else is no file /proc reads: a folder (the dispatcher then
|
|
753
|
+
// says it is a directory) or nothing there.
|
|
754
|
+
return (0, cumin_1.Err)();
|
|
713
755
|
}
|
|
714
|
-
async
|
|
756
|
+
async feedJobs_cancel(pathStr, _options) {
|
|
715
757
|
await cache_ensure();
|
|
716
758
|
const cache = (0, cumin_1.procCache_get)();
|
|
717
759
|
const clean = pathStr.replace(/\/$/, '');
|
|
@@ -754,10 +796,18 @@ class ProcVfsProvider {
|
|
|
754
796
|
}
|
|
755
797
|
return false;
|
|
756
798
|
}
|
|
757
|
-
|
|
799
|
+
/** @inheritdoc */
|
|
800
|
+
async cp(_src, _dst, _options) {
|
|
801
|
+
return (0, fond_1.vfsOutcome_ofBoolean)(await this.copy_run(_src, _dst, _options), 'EROFS');
|
|
802
|
+
}
|
|
803
|
+
async copy_run(_src, _dst, _options) { return false; }
|
|
758
804
|
async mv(_src, _dst) { return false; }
|
|
759
805
|
async touch(_pathStr) { return false; }
|
|
760
806
|
async upload(_localPath, _remotePath) { return false; }
|
|
807
|
+
/** @inheritdoc */
|
|
808
|
+
async write(pathStr, content) {
|
|
809
|
+
return (0, fond_1.vfsOutcome_ofBoolean)(await this.file_write(pathStr, content), 'EROFS');
|
|
810
|
+
}
|
|
761
811
|
/**
|
|
762
812
|
* Writes a projected file. One file under /proc takes writes: a feed's
|
|
763
813
|
* note (`/proc/jobs/feed_N/note`), whose content is replaced whole; its
|
|
@@ -767,7 +817,7 @@ class ProcVfsProvider {
|
|
|
767
817
|
* @param content - The new content, whole.
|
|
768
818
|
* @returns True when it was written.
|
|
769
819
|
*/
|
|
770
|
-
async
|
|
820
|
+
async file_write(pathStr, content) {
|
|
771
821
|
const { feedID, instanceID, virtualFile } = procPath_parse(pathStr.replace(/\/$/, ''));
|
|
772
822
|
if (feedID !== null && instanceID === null && virtualFile === 'note') {
|
|
773
823
|
const done = await (0, cumin_1.feedNote_update)(feedID, { content });
|
|
@@ -829,14 +879,81 @@ async function feedMeta_ensure(feedID) {
|
|
|
829
879
|
const existing = cache.feed_get(feedID);
|
|
830
880
|
if (existing && existing.creationDate !== '')
|
|
831
881
|
return;
|
|
882
|
+
await procFeed_probe(feedID);
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Reads one feed's row, from the owned-and-shared collection first and the
|
|
886
|
+
* public one after.
|
|
887
|
+
*
|
|
888
|
+
* @param client - Connected chrisapi client.
|
|
889
|
+
* @param feedID - The feed to read.
|
|
890
|
+
* @returns The row and its collection, or null when neither lists the feed for this identity.
|
|
891
|
+
*/
|
|
892
|
+
async function feedRow_fetch(client, feedID) {
|
|
893
|
+
const own = await (0, cumin_1.feedsPage_get)(client, { id: feedID, limit: 1, offset: 0 });
|
|
894
|
+
const ownRow = own.data[0];
|
|
895
|
+
if (ownRow)
|
|
896
|
+
return { feed: ownRow, collection: 'owned' };
|
|
897
|
+
const pub = (await (0, cumin_1.publicFeedsPage_get)(client, { id: feedID, limit: 1, offset: 0 })) ?? { data: [], totalCount: 0 };
|
|
898
|
+
const pubRow = pub.data[0];
|
|
899
|
+
return pubRow ? { feed: pubRow, collection: 'public' } : null;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Asks CUBE for one feed, live, and adopts it into the roster: the way a
|
|
903
|
+
* feed shared to this identity, or made public, since the roster was
|
|
904
|
+
* walked is seen when it is asked for by name or id. A feed the roster
|
|
905
|
+
* already holds has its row refreshed. The yardsticks move with an
|
|
906
|
+
* adoption, so the next delta does not walk again for what this ask found.
|
|
907
|
+
*
|
|
908
|
+
* @param feedID - The feed to ask for.
|
|
909
|
+
* @returns The feed as the roster now holds it, or null when CUBE lists it
|
|
910
|
+
* in neither collection for this identity — not shared, or shared to a
|
|
911
|
+
* group the identity is not in.
|
|
912
|
+
* @throws {Error} When not connected.
|
|
913
|
+
*/
|
|
914
|
+
async function procFeed_probe(feedID) {
|
|
915
|
+
const cache = (0, cumin_1.procCache_get)();
|
|
832
916
|
const client = await cumin_1.chrisConnection.client_get();
|
|
833
917
|
if (!client)
|
|
834
918
|
throw new Error('not connected');
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
cache.
|
|
919
|
+
const row = await feedRow_fetch(client, feedID);
|
|
920
|
+
if (row === null)
|
|
921
|
+
return null;
|
|
922
|
+
const feed = procFeed_create(row.feed);
|
|
923
|
+
const known = cache.feed_get(feedID) !== undefined;
|
|
924
|
+
cache.feed_add(feed);
|
|
925
|
+
if (!known) {
|
|
926
|
+
rosterTotal_bump(row.collection);
|
|
927
|
+
// A public feed is in the public collection whichever one listed it first.
|
|
928
|
+
if (feed.public && row.collection === 'owned')
|
|
929
|
+
rosterTotal_bump('public');
|
|
930
|
+
cache.arrivals_note([feedID]);
|
|
931
|
+
procTopologyCatchup_enqueue([feedID]);
|
|
932
|
+
}
|
|
933
|
+
return feed;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Asks CUBE for a feed the roster lacks, on a visit to its path, and says
|
|
937
|
+
* on the error stack when the ask itself failed; an absent feed is a null.
|
|
938
|
+
*
|
|
939
|
+
* @param feedID - The feed to ask for.
|
|
940
|
+
* @returns The adopted feed, or null.
|
|
941
|
+
*/
|
|
942
|
+
async function procFeedAbsent_probe(feedID) {
|
|
943
|
+
try {
|
|
944
|
+
return await procFeed_probe(feedID);
|
|
945
|
+
}
|
|
946
|
+
catch (error) {
|
|
947
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
948
|
+
cumin_1.errorStack.stack_push('warning', `proc: asking CUBE for feed_${feedID} failed (${msg})`);
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
/** Moves one collection's yardstick by one adopted feed; an unknown yardstick stays unknown. */
|
|
953
|
+
function rosterTotal_bump(collection) {
|
|
954
|
+
const total = rosterTotals[collection];
|
|
955
|
+
if (total !== null)
|
|
956
|
+
rosterTotals[collection] = total + 1;
|
|
840
957
|
}
|
|
841
958
|
async function feedStatus_refresh(feedID) {
|
|
842
959
|
const cache = (0, cumin_1.procCache_get)();
|
|
@@ -1046,19 +1163,72 @@ async function procRoster_run(force) {
|
|
|
1046
1163
|
return procRosterDelta_run(cache, client);
|
|
1047
1164
|
}
|
|
1048
1165
|
/**
|
|
1049
|
-
*
|
|
1050
|
-
*
|
|
1166
|
+
* One collection's newest page, read unfiltered. CUBE orders feeds newest
|
|
1167
|
+
* first, so the page carries every feed newer than the roster's highest id
|
|
1168
|
+
* unless more than a page arrived, and its total is the collection's whole
|
|
1169
|
+
* count — the yardstick a share, an unshare or a deletion moves.
|
|
1170
|
+
*
|
|
1171
|
+
* @param page_fetch - Fetches one page of the collection.
|
|
1172
|
+
* @param maxKnown - The roster's highest feed id (0 for an empty roster).
|
|
1173
|
+
* @returns The newer feeds, the total, and whether the page overflowed.
|
|
1174
|
+
*/
|
|
1175
|
+
async function rosterCollection_peek(page_fetch, maxKnown) {
|
|
1176
|
+
const page = await page_fetch({ limit: exports.ROSTER_DELTA_PAGE, offset: 0 });
|
|
1177
|
+
const rows = page.data.map(procFeed_create);
|
|
1178
|
+
const newer = rows.filter((feed) => feed.id > maxKnown);
|
|
1179
|
+
const overflow = rows.length > 0 && newer.length === rows.length
|
|
1180
|
+
&& page.totalCount !== null && page.totalCount > rows.length;
|
|
1181
|
+
return { newer, total: page.totalCount, overflow };
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Whether a collection's total moved by more than its newer rows account
|
|
1185
|
+
* for: a feed arrived or left under the newest page, which only the full
|
|
1186
|
+
* walk can place. An overflowed page, or a collection without a yardstick,
|
|
1187
|
+
* cannot say.
|
|
1188
|
+
*
|
|
1189
|
+
* @param collection - The collection peeked.
|
|
1190
|
+
* @param peek - What its newest page read.
|
|
1191
|
+
* @returns True when the total moved.
|
|
1192
|
+
*/
|
|
1193
|
+
function rosterTotal_moved(collection, peek) {
|
|
1194
|
+
const expected = rosterTotals[collection];
|
|
1195
|
+
if (expected === null || peek.total === null || peek.overflow)
|
|
1196
|
+
return false;
|
|
1197
|
+
return peek.total !== expected + peek.newer.length;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Brings the roster up to date without walking the whole index: each
|
|
1201
|
+
* collection's newest page is read, its feeds newer than the roster are
|
|
1202
|
+
* adopted, and its total is held against the last walk's. A total that
|
|
1203
|
+
* moved otherwise — a feed shared to this identity, one unshared, one
|
|
1204
|
+
* deleted — promotes the visit to the full walk, which places it. A page
|
|
1205
|
+
* that overflowed is followed by the walk of everything newer than the
|
|
1206
|
+
* roster, by id.
|
|
1051
1207
|
*
|
|
1052
1208
|
* @param cache - The process cache.
|
|
1053
1209
|
* @param client - Connected chrisapi client.
|
|
1054
|
-
* @returns The feed IDs added.
|
|
1210
|
+
* @returns The feed IDs added; after a promotion, the full walk's reconciliation targets.
|
|
1055
1211
|
*/
|
|
1056
1212
|
async function procRosterDelta_run(cache, client) {
|
|
1057
1213
|
const knownIDs = cache.feedIDs_get();
|
|
1058
|
-
const
|
|
1214
|
+
const maxKnown = knownIDs.length > 0 ? Math.max(...knownIDs) : 0;
|
|
1215
|
+
const ownedFetch = (params) => (0, cumin_1.feedsPage_get)(client, params);
|
|
1216
|
+
const publicFetch = async (params) => (await (0, cumin_1.publicFeedsPage_get)(client, params)) ?? { data: [], totalCount: 0 };
|
|
1217
|
+
const owned = await rosterCollection_peek(ownedFetch, maxKnown);
|
|
1218
|
+
const pub = await rosterCollection_peek(publicFetch, maxKnown);
|
|
1219
|
+
if (rosterTotal_moved('owned', owned) || rosterTotal_moved('public', pub)) {
|
|
1220
|
+
cache.rosterSync_progress('full');
|
|
1221
|
+
return procCache_build();
|
|
1222
|
+
}
|
|
1059
1223
|
const fresh = new Map();
|
|
1060
|
-
|
|
1061
|
-
|
|
1224
|
+
for (const feed of [...owned.newer, ...pub.newer])
|
|
1225
|
+
fresh.set(feed.id, feed);
|
|
1226
|
+
if (owned.overflow || pub.overflow) {
|
|
1227
|
+
const minID = maxKnown + 1;
|
|
1228
|
+
await procFeeds_index((params) => ownedFetch({ ...params, min_id: minID }), fresh);
|
|
1229
|
+
await procFeeds_index((params) => publicFetch({ ...params, min_id: minID }), fresh);
|
|
1230
|
+
}
|
|
1231
|
+
rosterTotals = { owned: owned.total, public: pub.total };
|
|
1062
1232
|
for (const feed of fresh.values())
|
|
1063
1233
|
cache.feed_add(feed);
|
|
1064
1234
|
cache.arrivals_note(Array.from(fresh.keys()));
|
|
@@ -1183,6 +1353,82 @@ function procTopologyCatchup_reset() {
|
|
|
1183
1353
|
function procTopologyCatchup_roster(cache) {
|
|
1184
1354
|
return procTopologyCatchup_enqueue(cache.feedIDs_get());
|
|
1185
1355
|
}
|
|
1356
|
+
/**
|
|
1357
|
+
* What a jump target names: a feed only (`feed_4389`), a node only
|
|
1358
|
+
* (`pl-dicom_repack_639204`, or `_639204`), or a bare number that may be
|
|
1359
|
+
* either. Null for anything else.
|
|
1360
|
+
*
|
|
1361
|
+
* @param target - What the operator typed.
|
|
1362
|
+
* @returns The id and which kinds it may name, or null.
|
|
1363
|
+
*/
|
|
1364
|
+
function procTarget_parse(target) {
|
|
1365
|
+
const text = target.trim();
|
|
1366
|
+
const feed = /^feed_(\d+)$/.exec(text);
|
|
1367
|
+
if (feed !== null)
|
|
1368
|
+
return { id: Number(feed[1]), feed: true, node: false };
|
|
1369
|
+
const node = /^[\w.-]*_(\d+)$/.exec(text);
|
|
1370
|
+
if (node !== null)
|
|
1371
|
+
return { id: Number(node[1]), feed: false, node: true };
|
|
1372
|
+
if (/^\d+$/.test(text))
|
|
1373
|
+
return { id: Number(text), feed: true, node: true };
|
|
1374
|
+
return null;
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Finds where a jump target lands, asking CUBE live for what the roster
|
|
1378
|
+
* and the index lack: a feed shared since the roster was walked, or a node
|
|
1379
|
+
* in one. A node's feed is adopted into the roster on the way, so the
|
|
1380
|
+
* place the answer names can be entered at once. A bare number that is
|
|
1381
|
+
* both a feed and a node answers both; the caller asks which.
|
|
1382
|
+
*
|
|
1383
|
+
* @param target - A feed id, a node id, or a `feed_N` / `<plugin>_N` name.
|
|
1384
|
+
* @returns Every place it names that this identity can see; empty for none.
|
|
1385
|
+
*/
|
|
1386
|
+
async function procTarget_locate(target) {
|
|
1387
|
+
const parsed = procTarget_parse(target);
|
|
1388
|
+
if (parsed === null)
|
|
1389
|
+
return [];
|
|
1390
|
+
const cache = (0, cumin_1.procCache_get)();
|
|
1391
|
+
const found = [];
|
|
1392
|
+
if (parsed.feed) {
|
|
1393
|
+
const feed = cache.feed_get(parsed.id) ?? (await procFeedAbsent_probe(parsed.id));
|
|
1394
|
+
if (feed !== null)
|
|
1395
|
+
found.push({ kind: 'feed', feedId: feed.id, name: `feed_${feed.id}`, title: feed.title });
|
|
1396
|
+
}
|
|
1397
|
+
if (parsed.node) {
|
|
1398
|
+
const node = await procNode_find(parsed.id);
|
|
1399
|
+
if (node !== null) {
|
|
1400
|
+
const feed = cache.feed_get(node.feedId) ?? (await procFeedAbsent_probe(node.feedId));
|
|
1401
|
+
if (feed !== null) {
|
|
1402
|
+
found.push({ kind: 'node', feedId: node.feedId, nodeId: parsed.id, name: `${node.pluginName}_${parsed.id}`, title: feed.title });
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return found;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* A node's feed and plugin, from the index or, when the index lacks it,
|
|
1410
|
+
* from CUBE.
|
|
1411
|
+
*
|
|
1412
|
+
* @param nodeId - The plugin instance id.
|
|
1413
|
+
* @returns Its feed and plugin name, or null when CUBE shows no such node to this identity.
|
|
1414
|
+
*/
|
|
1415
|
+
async function procNode_find(nodeId) {
|
|
1416
|
+
const known = (0, cumin_1.procCache_get)().instance_get(nodeId);
|
|
1417
|
+
if (known !== undefined)
|
|
1418
|
+
return { feedId: known.feedID, pluginName: known.pluginName };
|
|
1419
|
+
const client = await cumin_1.chrisConnection.client_get();
|
|
1420
|
+
if (!client)
|
|
1421
|
+
return null;
|
|
1422
|
+
try {
|
|
1423
|
+
const handle = await (0, cumin_1.pluginInstance_get)(client, nodeId);
|
|
1424
|
+
const feedId = Number(handle?.data?.feed_id);
|
|
1425
|
+
const pluginName = String(handle?.data?.plugin_name ?? '');
|
|
1426
|
+
return Number.isFinite(feedId) && feedId > 0 && pluginName !== '' ? { feedId, pluginName } : null;
|
|
1427
|
+
}
|
|
1428
|
+
catch {
|
|
1429
|
+
return null;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1186
1432
|
/**
|
|
1187
1433
|
* Forgets all visit bookkeeping (tests, and a cache cleared underneath us).
|
|
1188
1434
|
*/
|
|
@@ -1191,6 +1437,7 @@ function procVisitState_reset() {
|
|
|
1191
1437
|
feedVisitInflight.clear();
|
|
1192
1438
|
rosterWalkedAt = 0;
|
|
1193
1439
|
rosterSyncInflight = null;
|
|
1440
|
+
rosterTotals = { owned: null, public: null };
|
|
1194
1441
|
procTopologyCatchup_reset();
|
|
1195
1442
|
}
|
|
1196
1443
|
/**
|