@fnndsc/salsa 3.5.1 → 3.6.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.
Files changed (73) hide show
  1. package/README.md +3 -3
  2. package/dist/connect/index.d.ts +11 -0
  3. package/dist/connect/index.js +11 -0
  4. package/dist/connect/index.js.map +1 -1
  5. package/dist/dag/feedJoins.js +4 -1
  6. package/dist/dag/feedJoins.js.map +1 -1
  7. package/dist/feeds/index.d.ts +15 -2
  8. package/dist/feeds/index.js.map +1 -1
  9. package/dist/files/index.d.ts +7 -0
  10. package/dist/files/index.js +12 -11
  11. package/dist/files/index.js.map +1 -1
  12. package/dist/files/regular_content.js +11 -3
  13. package/dist/files/regular_content.js.map +1 -1
  14. package/dist/groups/index.d.ts +59 -0
  15. package/dist/groups/index.js +89 -0
  16. package/dist/groups/index.js.map +1 -1
  17. package/dist/groups/membershipRevision.d.ts +19 -0
  18. package/dist/groups/membershipRevision.js +29 -0
  19. package/dist/groups/membershipRevision.js.map +1 -0
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.js +5 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/jobs/jobOps.js +45 -2
  24. package/dist/jobs/jobOps.js.map +1 -1
  25. package/dist/pipelines/index.d.ts +28 -5
  26. package/dist/pipelines/index.js +110 -48
  27. package/dist/pipelines/index.js.map +1 -1
  28. package/dist/pipelines/invocation.d.ts +80 -0
  29. package/dist/pipelines/invocation.js +290 -0
  30. package/dist/pipelines/invocation.js.map +1 -0
  31. package/dist/pipelines/manifest.d.ts +102 -0
  32. package/dist/pipelines/manifest.js +204 -0
  33. package/dist/pipelines/manifest.js.map +1 -0
  34. package/dist/plugins/index.d.ts +29 -0
  35. package/dist/plugins/index.js +51 -10
  36. package/dist/plugins/index.js.map +1 -1
  37. package/dist/plugins/internal/plugin_executeNewFeed.d.ts +0 -14
  38. package/dist/plugins/internal/plugin_executeNewFeed.js +3 -3
  39. package/dist/plugins/internal/plugin_executeNewFeed.js.map +1 -1
  40. package/dist/plugins/plugin_register.js +5 -5
  41. package/dist/plugins/plugin_register.js.map +1 -1
  42. package/dist/plugins/store_import.js +1 -1
  43. package/dist/plugins/store_import.js.map +1 -1
  44. package/dist/retrieve/watch.d.ts +114 -0
  45. package/dist/retrieve/watch.js +360 -0
  46. package/dist/retrieve/watch.js.map +1 -0
  47. package/dist/store/index.d.ts +17 -4
  48. package/dist/store/index.js +9 -4
  49. package/dist/store/index.js.map +1 -1
  50. package/dist/users/index.d.ts +36 -0
  51. package/dist/users/index.js +50 -0
  52. package/dist/users/index.js.map +1 -0
  53. package/dist/vfs/dispatcher.d.ts +11 -0
  54. package/dist/vfs/dispatcher.js +37 -6
  55. package/dist/vfs/dispatcher.js.map +1 -1
  56. package/dist/vfs/index.d.ts +1 -0
  57. package/dist/vfs/index.js +3 -0
  58. package/dist/vfs/index.js.map +1 -1
  59. package/dist/vfs/provider.d.ts +13 -0
  60. package/dist/vfs/providers/etc.d.ts +2 -0
  61. package/dist/vfs/providers/etc.js +41 -2
  62. package/dist/vfs/providers/etc.js.map +1 -1
  63. package/dist/vfs/providers/native.js +41 -7
  64. package/dist/vfs/providers/native.js.map +1 -1
  65. package/dist/vfs/providers/pacs.d.ts +13 -1
  66. package/dist/vfs/providers/pacs.js +86 -107
  67. package/dist/vfs/providers/pacs.js.map +1 -1
  68. package/dist/vfs/providers/pacsHelpers.js +9 -5
  69. package/dist/vfs/providers/pacsHelpers.js.map +1 -1
  70. package/dist/vfs/providers/proc.d.ts +34 -1
  71. package/dist/vfs/providers/proc.js +301 -62
  72. package/dist/vfs/providers/proc.js.map +1 -1
  73. package/package.json +11 -8
@@ -21,19 +21,23 @@ exports.procFeed_ensureLoaded = procFeed_ensureLoaded;
21
21
  exports.feedMeta_ensure = feedMeta_ensure;
22
22
  exports.feedStatus_refresh = feedStatus_refresh;
23
23
  exports.procTopology_warmup = procTopology_warmup;
24
+ exports.procTopology_reconcileFeeds = procTopology_reconcileFeeds;
25
+ exports.procTopology_retry = procTopology_retry;
24
26
  exports.procTopology_status = procTopology_status;
25
27
  exports.procTopology_await = procTopology_await;
26
28
  exports.procCache_refresh = procCache_refresh;
27
29
  const cumin_1 = require("@fnndsc/cumin");
28
30
  const index_js_1 = require("../../jobs/index.js");
29
31
  /** Virtual filenames inside each instance directory. */
30
- const INSTANCE_FILES = new Set(['status', 'params', 'log']);
32
+ const INSTANCE_FILES = new Set(['status', 'params', 'log', 'data']);
31
33
  /** Virtual filenames inside each feed directory. */
32
34
  const FEED_FILES = new Set(['status', 'title']);
33
35
  const PROC_JOBS_PREFIX = '/proc/jobs';
34
36
  const PAGE = 100;
35
37
  let procTopologyPromise = null;
36
38
  let procTopologyFailure;
39
+ let procTopologyResumeState = null;
40
+ let procTopologyScopedResumeFeedIDs = null;
37
41
  /** Converts a CUBE feed row into the cache's feed model. */
38
42
  function procFeed_create(feed) {
39
43
  return {
@@ -51,7 +55,7 @@ function procFeed_create(feed) {
51
55
  };
52
56
  }
53
57
  /** Indexes one complete paginated CUBE feed collection into the shared cache. */
54
- async function procFeeds_index(cache, page_fetch) {
58
+ async function procFeeds_index(page_fetch, indexed) {
55
59
  let offset = 0;
56
60
  let total = 0;
57
61
  const seenFeedIDs = new Set();
@@ -62,7 +66,8 @@ async function procFeeds_index(cache, page_fetch) {
62
66
  total = page.totalCount;
63
67
  }
64
68
  for (const feed of chunk) {
65
- cache.feed_add(procFeed_create(feed));
69
+ const procFeed = procFeed_create(feed);
70
+ indexed.set(procFeed.id, procFeed);
66
71
  seenFeedIDs.add(Number(feed.id));
67
72
  }
68
73
  if (chunk.length === 0 || (total > 0 && seenFeedIDs.size >= total))
@@ -79,18 +84,20 @@ async function procFeeds_index(cache, page_fetch) {
79
84
  */
80
85
  async function procCache_build() {
81
86
  const cache = (0, cumin_1.procCache_get)();
82
- cache.cache_clear();
83
87
  const client = await cumin_1.chrisConnection.client_get();
84
88
  if (!client) {
85
89
  cumin_1.errorStack.stack_push('error', 'procCache_build: not connected');
86
- return;
90
+ throw new Error('not connected');
87
91
  }
88
92
  const typedClient = client;
89
- await procFeeds_index(cache, typedClient.getFeeds.bind(typedClient));
93
+ const indexed = new Map();
94
+ await procFeeds_index(typedClient.getFeeds.bind(typedClient), indexed);
90
95
  if (typedClient.getPublicFeeds) {
91
- await procFeeds_index(cache, typedClient.getPublicFeeds.bind(typedClient));
96
+ await procFeeds_index(typedClient.getPublicFeeds.bind(typedClient), indexed);
92
97
  }
98
+ const reconciliationTargets = cache.feeds_reconcile(Array.from(indexed.values()));
93
99
  cache.built_set();
100
+ return reconciliationTargets;
94
101
  }
95
102
  /** Ensures the feed index is built, building it on first access. */
96
103
  async function cache_ensure() {
@@ -106,7 +113,7 @@ async function feedInstances_load(feedID) {
106
113
  const cache = (0, cumin_1.procCache_get)();
107
114
  const client = await cumin_1.chrisConnection.client_get();
108
115
  if (!client)
109
- return;
116
+ throw new Error('not connected');
110
117
  const typedClient = client;
111
118
  const instances = new Map();
112
119
  let offset = 0;
@@ -138,6 +145,7 @@ async function feedInstances_load(feedID) {
138
145
  pluginName: String(inst.plugin_name),
139
146
  pluginType: inst.plugin_type !== undefined ? String(inst.plugin_type) : undefined,
140
147
  params: null,
148
+ outputPath: outputPath_normalize(inst.output_path) ?? undefined,
141
149
  status: String(inst.status ?? 'unknown'),
142
150
  });
143
151
  }
@@ -157,8 +165,12 @@ async function feedInstances_ensureLoaded(feedID) {
157
165
  return inflight;
158
166
  const promise = feedInstances_load(feedID);
159
167
  cache.loading_set(feedID, promise);
160
- await promise;
161
- cache.loading_clear(feedID);
168
+ try {
169
+ await promise;
170
+ }
171
+ finally {
172
+ cache.loading_clear(feedID);
173
+ }
162
174
  }
163
175
  // ── Aggregate status ───────────────────────────────────────────────────────
164
176
  /** Derives aggregate feed status from stored job counters — no API call. */
@@ -218,6 +230,79 @@ function params_render(inst) {
218
230
  .map(([k, v]) => `${k}=${String(v)}`)
219
231
  .join('\n');
220
232
  }
233
+ /**
234
+ * Normalizes CUBE's output-path spelling into an absolute CFS path.
235
+ *
236
+ * @param value - Raw `output_path` value from a plugin-instance resource.
237
+ * @returns Absolute CFS path, or null when no usable path was supplied.
238
+ */
239
+ function outputPath_normalize(value) {
240
+ if (typeof value !== 'string' || value.trim() === '')
241
+ return null;
242
+ const clean = value.trim();
243
+ return clean.startsWith('/') ? clean : `/${clean}`;
244
+ }
245
+ /**
246
+ * Resolves and memoizes the authoritative CFS output directory for one job.
247
+ *
248
+ * @param instanceID - Plugin-instance ID whose output path is needed.
249
+ * @returns Absolute output path, or null when CUBE has not recorded one.
250
+ */
251
+ async function instanceOutputPath_ensure(instanceID) {
252
+ const cache = (0, cumin_1.procCache_get)();
253
+ const inst = cache.instance_get(instanceID);
254
+ if (!inst)
255
+ return null;
256
+ if (inst.outputPath !== undefined)
257
+ return inst.outputPath;
258
+ const client = await cumin_1.chrisConnection.client_get();
259
+ if (!client)
260
+ return null;
261
+ try {
262
+ const resource = await client
263
+ .getPluginInstance(instanceID);
264
+ const outputPath = outputPath_normalize(resource?.data?.['output_path']);
265
+ cache.outputPath_update(instanceID, outputPath);
266
+ return outputPath;
267
+ }
268
+ catch (error) {
269
+ // "No CFS output path" downstream must not mask a failed fetch.
270
+ const msg = error instanceof Error ? error.message : String(error);
271
+ cumin_1.errorStack.stack_push("warning", `Could not fetch output path for instance ${instanceID}: ${msg}`);
272
+ return null;
273
+ }
274
+ }
275
+ /**
276
+ * Reads every page of effective parameter values from one plugin instance.
277
+ *
278
+ * @param resource - Detail resource for the instance being inspected.
279
+ * @returns Parameter names mapped to their recorded invocation values.
280
+ */
281
+ async function instanceParams_fetch(resource) {
282
+ const params = {};
283
+ let offset = 0;
284
+ let total = 0;
285
+ let fetched = 0;
286
+ while (true) {
287
+ const page = await resource.getParameters({ limit: PAGE, offset });
288
+ const items = page.data
289
+ ?? (page.getItems ? page.getItems().map((item) => item.data) : []);
290
+ if (offset === 0 && typeof page.totalCount === 'number' && page.totalCount >= 0) {
291
+ total = page.totalCount;
292
+ }
293
+ for (const item of items) {
294
+ if (item.param_name)
295
+ params[item.param_name] = item.value;
296
+ }
297
+ fetched += items.length;
298
+ if (items.length === 0 || (total > 0 && fetched >= total))
299
+ break;
300
+ if (total === 0 && items.length < PAGE)
301
+ break;
302
+ offset += items.length;
303
+ }
304
+ return params;
305
+ }
221
306
  /** Collects all instance IDs for a feed recursively. */
222
307
  function getAllInstanceIDs_forFeed(feedID, cache) {
223
308
  const result = [];
@@ -235,6 +320,28 @@ function getAllInstanceIDs_forFeed(feedID, cache) {
235
320
  */
236
321
  class ProcVfsProvider {
237
322
  prefix = PROC_JOBS_PREFIX;
323
+ /**
324
+ * Resolves the synthetic `data` link for one job only when navigation asks
325
+ * to follow it. This keeps `/proc` topology listings and recursive trees
326
+ * entirely cache-backed.
327
+ *
328
+ * @param pathStr - Absolute `/proc/jobs/.../data` path to resolve.
329
+ * @returns The authoritative CFS output path, or an error when absent.
330
+ */
331
+ async linkTarget_resolve(pathStr) {
332
+ await cache_ensure();
333
+ const clean = pathStr.replace(/\/$/, '');
334
+ const { feedID, instanceID, virtualFile } = procPath_parse(clean);
335
+ if (feedID === null || instanceID === null || virtualFile !== 'data') {
336
+ cumin_1.errorStack.stack_push('error', `Not a /proc job data link: ${pathStr}`);
337
+ return (0, cumin_1.Err)();
338
+ }
339
+ const outputPath = await instanceOutputPath_ensure(instanceID);
340
+ if (outputPath)
341
+ return (0, cumin_1.Ok)(outputPath);
342
+ cumin_1.errorStack.stack_push('error', `No CFS output path is available for job ${instanceID}`);
343
+ return (0, cumin_1.Err)();
344
+ }
238
345
  async list(pathStr, _options) {
239
346
  await cache_ensure();
240
347
  const cache = (0, cumin_1.procCache_get)();
@@ -304,6 +411,15 @@ class ProcVfsProvider {
304
411
  items.push({ name: 'status', type: 'file', size: 0, owner: '', date: '' });
305
412
  items.push({ name: 'params', type: 'file', size: 0, owner: '', date: '' });
306
413
  items.push({ name: 'log', type: 'file', size: 0, owner: '', date: '' });
414
+ // `data` is structural: list it without fetching the per-instance detail
415
+ // endpoint. Its target is resolved only when `cd data` follows the link.
416
+ // A known null means CUBE has no output directory, so omit it entirely.
417
+ if (inst.outputPath !== null) {
418
+ const dataLink = { name: 'data', type: 'link', size: 0, owner: '', date: '' };
419
+ if (inst.outputPath)
420
+ dataLink.target = inst.outputPath;
421
+ items.push(dataLink);
422
+ }
307
423
  for (const childID of childIDs) {
308
424
  const child = cache.instance_get(childID);
309
425
  if (!child)
@@ -354,22 +470,21 @@ class ProcVfsProvider {
354
470
  }
355
471
  if (virtualFile === 'params') {
356
472
  if (inst.params === null) {
357
- // Fetch params on first read and cache permanently
473
+ // CUBE stores the effective run values in the parameter sub-resource;
474
+ // the instance detail payload itself is operational metadata.
358
475
  const client = await cumin_1.chrisConnection.client_get();
359
476
  if (client) {
360
477
  try {
361
- const raw = await client.getPluginInstance(instanceID);
362
- if (raw?.data) {
363
- const p = { ...raw.data };
364
- delete p['id'];
365
- delete p['feed_id'];
366
- delete p['plugin_name'];
367
- delete p['status'];
368
- delete p['previous_id'];
369
- cache.params_update(instanceID, p);
370
- }
478
+ const resource = await client
479
+ .getPluginInstance(instanceID);
480
+ if (resource)
481
+ cache.params_update(instanceID, await instanceParams_fetch(resource));
482
+ }
483
+ catch {
484
+ // Deliberate absorption, adjudicated 2026-08: parameters are a
485
+ // lazy display enrichment; a failed fetch renders as
486
+ // "(no parameters)" and the next read retries.
371
487
  }
372
- catch { /* leave null */ }
373
488
  }
374
489
  }
375
490
  return (0, cumin_1.Ok)(params_render(inst));
@@ -389,12 +504,21 @@ class ProcVfsProvider {
389
504
  if (feedID !== null && instanceID === null) {
390
505
  const allIDs = getAllInstanceIDs_forFeed(feedID, cache);
391
506
  const statusMap = await (0, index_js_1.jobs_statusBatch)(allIDs);
507
+ // Every cancel Result is checked: reporting success while jobs kept
508
+ // running would leave the user believing the feed's work was stopped.
509
+ const failedCancels = [];
392
510
  await Promise.all(allIDs.map(async (id) => {
393
511
  const s = statusMap.get(id) ?? '';
394
512
  if (!(0, cumin_1.status_isTerminal)(s)) {
395
- await (0, index_js_1.job_cancel)(id);
513
+ const cancelResult = await (0, index_js_1.job_cancel)(id);
514
+ if (!cancelResult.ok)
515
+ failedCancels.push(id);
396
516
  }
397
517
  }));
518
+ if (failedCancels.length > 0) {
519
+ cumin_1.errorStack.stack_push('error', `rm: failed to cancel ${failedCancels.length} running job(s) in feed ${feedID}: instance(s) ${failedCancels.sort((a, b) => a - b).join(', ')}`);
520
+ return false;
521
+ }
398
522
  cache.feed_remove(feedID);
399
523
  return true;
400
524
  }
@@ -464,7 +588,7 @@ async function feedMeta_ensure(feedID) {
464
588
  return;
465
589
  const client = await cumin_1.chrisConnection.client_get();
466
590
  if (!client)
467
- return;
591
+ throw new Error('not connected');
468
592
  const typedClient = client;
469
593
  const page = await typedClient.getFeeds({ id: feedID, limit: 1, offset: 0 });
470
594
  const f = (page.data ?? [])[0];
@@ -505,44 +629,42 @@ async function feedStatus_refresh(feedID) {
505
629
  * Background topology warm-up — single paginated sweep of all plugin instances
506
630
  * across all feeds. Replaces the per-feed fan-out that was O(feeds) round trips.
507
631
  *
508
- * Registers itself as the active sweep so that any concurrent
509
- * feedInstances_ensureLoaded call waits for this sweep rather than launching
510
- * its own per-feed API call.
632
+ * Scoped per-feed loads may run concurrently for navigation; the completed
633
+ * global sweep reconciles the cache to its authoritative instance set.
634
+ *
635
+ * @param state - Private pagination and reconciliation state for this sweep.
636
+ * @returns A promise that settles when the complete topology has been reconciled.
511
637
  */
512
- async function procTopology_run() {
638
+ async function procTopology_run(state) {
513
639
  await cache_ensure();
514
640
  const cache = (0, cumin_1.procCache_get)();
641
+ cache.lifecycle_set('reconciling');
642
+ if (state.total > 0) {
643
+ cache.warmup_progress(state.fetchedInstanceIDs.size, state.total);
644
+ }
515
645
  const client = await cumin_1.chrisConnection.client_get();
516
646
  if (!client)
517
- return;
647
+ throw new Error('not connected');
518
648
  const typedClient = client;
519
- let offset = 0;
520
- let total = 0;
521
- const seenInstanceIDs = new Set();
522
- const seenFeedIDs = new Set();
523
649
  while (true) {
524
650
  const page = await typedClient.getPluginInstances({
525
- limit: PAGE, offset,
651
+ limit: PAGE, offset: state.offset,
526
652
  });
527
653
  const chunk = page.data ?? [];
528
- if (offset === 0 && typeof page.totalCount === 'number' && page.totalCount >= 0) {
529
- total = page.totalCount;
530
- if (total > 0)
531
- cache.warmup_progress(0, total);
654
+ if (state.offset === 0 && typeof page.totalCount === 'number' && page.totalCount >= 0) {
655
+ state.total = page.totalCount;
656
+ if (state.total > 0)
657
+ cache.warmup_progress(0, state.total);
532
658
  }
533
659
  for (const inst of chunk) {
534
660
  const instanceID = Number(inst.id);
535
661
  const feedID = Number(inst.feed_id);
662
+ state.fetchedInstanceIDs.add(instanceID);
536
663
  const prevID = (inst.previous_id !== null && inst.previous_id !== undefined)
537
664
  ? Number(inst.previous_id)
538
665
  : null;
539
- if (!cache.feed_get(feedID)) {
540
- cache.feed_add({
541
- id: feedID, title: `feed_${feedID}`, ownerUsername: '', public: false, creationDate: '',
542
- finishedJobs: 0, erroredJobs: 0, startedJobs: 0,
543
- scheduledJobs: 0, cancelledJobs: 0, createdJobs: 0,
544
- });
545
- }
666
+ if (!cache.feed_get(feedID))
667
+ continue;
546
668
  cache.instance_add({
547
669
  id: instanceID,
548
670
  feedID,
@@ -550,36 +672,53 @@ async function procTopology_run() {
550
672
  pluginName: String(inst.plugin_name),
551
673
  pluginType: inst.plugin_type !== undefined ? String(inst.plugin_type) : undefined,
552
674
  params: null,
675
+ outputPath: outputPath_normalize(inst.output_path) ?? undefined,
553
676
  status: String(inst.status ?? 'unknown'),
554
677
  });
555
- seenInstanceIDs.add(instanceID);
556
- seenFeedIDs.add(feedID);
678
+ state.seenInstanceIDs.add(instanceID);
679
+ state.seenFeedIDs.add(feedID);
557
680
  }
558
- if (total > 0)
559
- cache.warmup_progress(seenInstanceIDs.size, total);
560
- if (chunk.length === 0 || (total > 0 && seenInstanceIDs.size >= total))
681
+ if (state.total > 0)
682
+ cache.warmup_progress(state.fetchedInstanceIDs.size, state.total);
683
+ if (chunk.length === 0 || (state.total > 0 && state.fetchedInstanceIDs.size >= state.total))
561
684
  break;
562
- if (total === 0 && chunk.length < PAGE)
685
+ if (state.total === 0 && chunk.length < PAGE)
563
686
  break;
564
- offset += chunk.length;
687
+ state.offset += chunk.length;
565
688
  }
566
- for (const feedID of seenFeedIDs) {
689
+ cache.topology_reconcile(state.seenInstanceIDs);
690
+ for (const feedID of state.seenFeedIDs)
567
691
  cache.topologyLoaded_mark(feedID);
692
+ if (state.total === 0) {
693
+ cache.warmup_progress(state.fetchedInstanceIDs.size, state.fetchedInstanceIDs.size);
568
694
  }
569
- if (total === 0)
570
- cache.warmup_progress(seenInstanceIDs.size, seenInstanceIDs.size);
571
695
  cache.warmup_complete();
572
696
  }
573
697
  /**
574
- * Starts or joins the session's global plugin-instance topology sweep.
698
+ * Creates an empty continuation for a fresh global topology sweep.
575
699
  *
576
- * @returns The single in-flight sweep promise.
700
+ * @returns New private pagination and reconciliation state.
577
701
  */
578
- function procTopology_warmup() {
579
- if (procTopologyPromise)
580
- return procTopologyPromise;
702
+ function procTopologyState_create() {
703
+ return {
704
+ offset: 0,
705
+ total: 0,
706
+ fetchedInstanceIDs: new Set(),
707
+ seenInstanceIDs: new Set(),
708
+ seenFeedIDs: new Set(),
709
+ };
710
+ }
711
+ /**
712
+ * Starts one topology sweep from the supplied private continuation state.
713
+ *
714
+ * @param state - Private pagination and reconciliation state to run or resume.
715
+ * @returns The in-flight topology sweep promise.
716
+ */
717
+ function procTopology_start(state) {
581
718
  procTopologyFailure = undefined;
582
- const sweep = procTopology_run();
719
+ procTopologyResumeState = state;
720
+ procTopologyScopedResumeFeedIDs = null;
721
+ const sweep = procTopology_run(state);
583
722
  procTopologyPromise = sweep;
584
723
  sweep.then(() => {
585
724
  if (procTopologyPromise !== sweep)
@@ -587,6 +726,7 @@ function procTopology_warmup() {
587
726
  procTopologyPromise = null;
588
727
  if ((0, cumin_1.procCache_get)().warmupComplete) {
589
728
  procTopologyFailure = undefined;
729
+ procTopologyResumeState = null;
590
730
  }
591
731
  else {
592
732
  procTopologyFailure = 'the topology sweep ended before the index completed';
@@ -600,6 +740,94 @@ function procTopology_warmup() {
600
740
  });
601
741
  return sweep;
602
742
  }
743
+ /**
744
+ * Runs a feed-scoped topology reconciliation selected after checkpoint restore.
745
+ *
746
+ * @param feedIDs - Canonical feed IDs whose topology needs a fresh load.
747
+ * @returns A promise that settles after the supplied feeds are reconciled.
748
+ */
749
+ async function procTopologyScoped_run(feedIDs) {
750
+ const cache = (0, cumin_1.procCache_get)();
751
+ cache.lifecycle_set('reconciling');
752
+ for (const feedID of feedIDs)
753
+ await procCache_refresh(feedID);
754
+ cache.warmup_complete();
755
+ }
756
+ /**
757
+ * Starts one scoped reconciliation and retains its targets for `proc retry`.
758
+ *
759
+ * @param feedIDs - Canonical feed IDs selected by the fresh feed index.
760
+ * @returns The in-flight scoped reconciliation promise.
761
+ */
762
+ function procTopologyScoped_start(feedIDs) {
763
+ const targets = [...feedIDs];
764
+ procTopologyFailure = undefined;
765
+ procTopologyResumeState = null;
766
+ procTopologyScopedResumeFeedIDs = targets;
767
+ const sweep = procTopologyScoped_run(targets);
768
+ procTopologyPromise = sweep;
769
+ sweep.then(() => {
770
+ if (procTopologyPromise !== sweep)
771
+ return;
772
+ procTopologyPromise = null;
773
+ if ((0, cumin_1.procCache_get)().warmupComplete) {
774
+ procTopologyFailure = undefined;
775
+ procTopologyScopedResumeFeedIDs = null;
776
+ }
777
+ else {
778
+ procTopologyFailure = 'the scoped topology reconciliation ended before the index completed';
779
+ }
780
+ }, (error) => {
781
+ if (procTopologyPromise !== sweep)
782
+ return;
783
+ procTopologyPromise = null;
784
+ (0, cumin_1.procCache_get)().warmup_abort();
785
+ procTopologyFailure = error instanceof Error ? error.message : String(error);
786
+ });
787
+ return sweep;
788
+ }
789
+ /**
790
+ * Starts or joins the session's global plugin-instance topology sweep.
791
+ *
792
+ * @returns The single in-flight sweep promise.
793
+ */
794
+ function procTopology_warmup() {
795
+ if (procTopologyPromise)
796
+ return procTopologyPromise;
797
+ return procTopology_start(procTopologyState_create());
798
+ }
799
+ /**
800
+ * Reconciles topology only for feed IDs selected by a restored-cache feed-index
801
+ * comparison. This preserves the checkpointed topology for unchanged terminal
802
+ * feeds while refreshing new, changed, and active feeds from CUBE.
803
+ *
804
+ * @param feedIDs - Canonical CUBE feed IDs whose topology must be reloaded.
805
+ * @returns A promise that resolves after all supplied feeds have been refreshed.
806
+ */
807
+ function procTopology_reconcileFeeds(feedIDs) {
808
+ if (procTopologyPromise)
809
+ return procTopologyPromise;
810
+ return procTopologyScoped_start(feedIDs);
811
+ }
812
+ /**
813
+ * Continues a failed topology reconciliation from its retained global page or
814
+ * scoped feed set.
815
+ *
816
+ * @returns The resumed sweep promise, or the active sweep when one is running.
817
+ * @throws {Error} When there is no failed sweep continuation to retry.
818
+ */
819
+ function procTopology_retry() {
820
+ if (procTopologyPromise)
821
+ return procTopologyPromise;
822
+ if (procTopologyFailure === undefined) {
823
+ throw new Error('no failed topology sweep to retry');
824
+ }
825
+ if (procTopologyResumeState)
826
+ return procTopology_start(procTopologyResumeState);
827
+ if (procTopologyScopedResumeFeedIDs)
828
+ return procTopologyScoped_start(procTopologyScopedResumeFeedIDs);
829
+ throw new Error('no failed topology sweep to retry');
830
+ }
603
831
  /**
604
832
  * Reports the session topology sweep lifecycle.
605
833
  *
@@ -627,6 +855,13 @@ async function procTopology_await() {
627
855
  }
628
856
  /**
629
857
  * Rebuilds the ProcCache, optionally scoped to one feed.
858
+ *
859
+ * A full refresh returns feeds whose topology merits reload after the feed-index
860
+ * comparison. Callers performing an explicit `proc refresh` can ignore that
861
+ * result and run a global topology sweep; restored startup uses it to stay scoped.
862
+ *
863
+ * @param feedID - One feed to force-refresh, or undefined for the full feed index.
864
+ * @returns IDs whose topology is new, changed, or currently active.
630
865
  */
631
866
  async function procCache_refresh(feedID) {
632
867
  if (feedID !== undefined) {
@@ -643,6 +878,7 @@ async function procCache_refresh(feedID) {
643
878
  }
644
879
  }
645
880
  await feedInstances_ensureLoaded(feedID);
881
+ return [feedID];
646
882
  }
647
883
  else {
648
884
  const activeSweep = procTopologyPromise;
@@ -656,7 +892,10 @@ async function procCache_refresh(feedID) {
656
892
  }
657
893
  procTopologyPromise = null;
658
894
  procTopologyFailure = undefined;
659
- await procCache_build();
895
+ procTopologyResumeState = null;
896
+ procTopologyScopedResumeFeedIDs = null;
897
+ (0, cumin_1.procCache_get)().warmup_reset();
898
+ return procCache_build();
660
899
  }
661
900
  }
662
901
  //# sourceMappingURL=proc.js.map