ultravisor 1.0.24 → 1.0.26

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 (25) hide show
  1. package/docs/_sidebar.md +1 -0
  2. package/docs/features/persistence-via-databeacon.md +1211 -0
  3. package/package.json +6 -6
  4. package/source/cli/Ultravisor-CLIProgram.cjs +80 -0
  5. package/source/config/Ultravisor-Default-Command-Configuration.cjs +9 -1
  6. package/source/datamodel/Ultravisor-Fleet.json +66 -0
  7. package/source/persistence/UltravisorPersistenceSchema.json +240 -0
  8. package/source/services/Ultravisor-AuthBeaconBridge.cjs +271 -0
  9. package/source/services/Ultravisor-Beacon-Coordinator.cjs +339 -151
  10. package/source/services/Ultravisor-Beacon-Scheduler.cjs +65 -29
  11. package/source/services/Ultravisor-DirectoryDistributor.cjs +280 -0
  12. package/source/services/Ultravisor-ExecutionManifest.cjs +99 -4
  13. package/source/services/Ultravisor-FleetManager.cjs +871 -0
  14. package/source/services/Ultravisor-ManifestStoreBridge.cjs +1134 -0
  15. package/source/services/Ultravisor-QueuePersistenceBridge.cjs +1336 -0
  16. package/source/services/persistence/Ultravisor-Beacon-FleetStore.cjs +570 -0
  17. package/source/web_server/Ultravisor-API-Server.cjs +1185 -90
  18. package/test/fleetstore-smoke.js +152 -0
  19. package/webinterface/package.json +1 -0
  20. package/webinterface/source/Pict-Application-Ultravisor.js +59 -2
  21. package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +12 -0
  22. package/webinterface/source/views/PictView-Ultravisor-Fleet.js +489 -0
  23. package/webinterface/source/views/PictView-Ultravisor-Login.js +74 -0
  24. package/webinterface/source/views/PictView-Ultravisor-TopBar.js +26 -0
  25. package/webinterface/source/views/PictView-Ultravisor-UserManagement.js +159 -0
@@ -0,0 +1,1134 @@
1
+ /**
2
+ * Ultravisor-ManifestStoreBridge
3
+ *
4
+ * Single front door for "where do operation execution manifests live?".
5
+ * Two backends, picked at call time:
6
+ *
7
+ * 1. The optional ultravisor-manifest-beacon (capability =
8
+ * ManifestStore). When connected, every persistence write
9
+ * dispatches to the beacon and reads come back from it.
10
+ *
11
+ * 2. The in-process UltravisorExecutionManifest service. Used as
12
+ * the fallback whenever the beacon isn't connected — preserves
13
+ * the existing on-disk JSON-blob persistence behavior.
14
+ *
15
+ * Symmetric with Ultravisor-QueuePersistenceBridge: same beacon-or-
16
+ * local pattern, same Promise-based API, same fail-open posture.
17
+ *
18
+ * Loop prevention: the coordinator's _isMetaCapability gate skips
19
+ * persistence-recording for ManifestStore dispatches, mirroring how
20
+ * QueuePersistence is skipped. MS_* dispatches are themselves work
21
+ * items routed through the queue, but they don't trigger another
22
+ * round of manifest persistence — otherwise an MS_UpsertManifest
23
+ * write to the beacon would itself create a manifest, which would
24
+ * persist via MS_UpsertManifest, which would create another...
25
+ */
26
+
27
+ const libPictService = require('pict-serviceproviderbase');
28
+ const libFs = require('fs');
29
+ const libPath = require('path');
30
+
31
+ const DEFAULT_TIMEOUT_MS = 5000;
32
+
33
+ // One sweep can't drain a years-long backlog in a single shot —
34
+ // successive reconnects continue from the HWM. Manifests are
35
+ // coarser than queue items, so a smaller per-sweep cap is enough.
36
+ const FLUSH_BATCH_LIMIT = 1000;
37
+
38
+ const HWM_FILENAME = 'persistence-bridge-hwm.json';
39
+
40
+ // Filename for the persisted persistence-beacon assignment (Session 3).
41
+ // See Ultravisor-QueuePersistenceBridge.cjs for the schema; this bridge
42
+ // owns the Manifest top-level key.
43
+ const ASSIGNMENT_FILENAME = 'persistence-assignment.json';
44
+
45
+ // Beacon Tag the lab stamps on a databeacon when it's been assigned
46
+ // as the UV's persistence backend. Same convention as the queue
47
+ // bridge — manifests share the connection so they share the tag.
48
+ const PERSISTENCE_TAG = 'PersistenceConnectionID';
49
+
50
+ const SCHEMA_DESCRIPTOR_PATH = libPath.join(__dirname, '..', 'persistence', 'UltravisorPersistenceSchema.json');
51
+
52
+ const MANIFEST_TABLES = ['UVManifest'];
53
+
54
+ const UV_PROXY_PATH_PATTERNS = ['^/?1\\.0/[^/]+/UV[A-Za-z0-9]*'];
55
+
56
+ class UltravisorManifestStoreBridge extends libPictService
57
+ {
58
+ constructor(pPict, pOptions, pServiceHash)
59
+ {
60
+ super(pPict, pOptions, pServiceHash);
61
+ this.serviceType = 'UltravisorManifestStoreBridge';
62
+ this._TimeoutMs = (pOptions && pOptions.TimeoutMs) || DEFAULT_TIMEOUT_MS;
63
+ // Bootstrap-flush state: HWM keyed by beaconID. We anchor on
64
+ // each manifest's StopTime (terminal runs) or StartTime (still
65
+ // in flight when ultravisor crashed) so HWM advances
66
+ // monotonically. Idempotency is "free" via MS_UpsertManifest
67
+ // — RunHash is the natural key — so a missed HWM only means
68
+ // redundant uploads on the next sweep, not data corruption.
69
+ this._FlushHWMs = this._loadHWMs();
70
+ this._FlushInFlight = new Set();
71
+ // MeadowProxy bootstrap state. Mirrors the queue bridge — see
72
+ // Ultravisor-QueuePersistenceBridge.cjs for the rationale.
73
+ this._BootstrappedBeacons = new Set();
74
+ this._BootstrapInFlight = new Set();
75
+ this._EndpointBaseByBeacon = {};
76
+ this._SchemaDescriptor = this._loadSchemaDescriptor();
77
+ // Explicit lab assignment (Session 3). Tag-scan stays as the
78
+ // CLI-only fallback for sidecar deployments.
79
+ this._PersistenceAssignment = this._loadAssignment();
80
+ this._LastBootstrapError = null;
81
+ this._BootstrappedAt = null;
82
+ }
83
+
84
+ _loadSchemaDescriptor()
85
+ {
86
+ try
87
+ {
88
+ let tmpRaw = libFs.readFileSync(SCHEMA_DESCRIPTOR_PATH, 'utf8');
89
+ return JSON.parse(tmpRaw);
90
+ }
91
+ catch (pErr)
92
+ {
93
+ if (this.log)
94
+ {
95
+ this.log.warn(`ManifestStoreBridge: schema descriptor not loadable at ${SCHEMA_DESCRIPTOR_PATH} (${pErr.message}). MeadowProxy persistence path disabled.`);
96
+ }
97
+ return null;
98
+ }
99
+ }
100
+
101
+ getBeaconID()
102
+ {
103
+ let tmpCoord = this._coord();
104
+ if (!tmpCoord) return null;
105
+ let tmpBeacons = tmpCoord.listBeacons() || [];
106
+ for (let i = 0; i < tmpBeacons.length; i++)
107
+ {
108
+ let tmpCaps = tmpBeacons[i].Capabilities || [];
109
+ if (tmpCaps.indexOf('ManifestStore') >= 0)
110
+ {
111
+ return tmpBeacons[i].BeaconID;
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+
117
+ isBeaconAvailable()
118
+ {
119
+ return this.getBeaconID() !== null;
120
+ }
121
+
122
+ /**
123
+ * Locate a MeadowProxy persistence beacon. Explicit lab assignment
124
+ * wins; tag-scan is the CLI-only fallback. Mirrors the queue
125
+ * bridge — see its getPersistenceBeacon for the rationale.
126
+ */
127
+ getPersistenceBeacon()
128
+ {
129
+ if (!this._SchemaDescriptor) return null;
130
+ if (this._PersistenceAssignment && this._PersistenceAssignment.BeaconID)
131
+ {
132
+ return {
133
+ BeaconID: this._PersistenceAssignment.BeaconID,
134
+ IDBeaconConnection: this._PersistenceAssignment.IDBeaconConnection
135
+ };
136
+ }
137
+ let tmpCoord = this._coord();
138
+ if (!tmpCoord) return null;
139
+ let tmpBeacons = tmpCoord.listBeacons() || [];
140
+ for (let i = 0; i < tmpBeacons.length; i++)
141
+ {
142
+ let tmpBeacon = tmpBeacons[i];
143
+ if (tmpBeacon.Status !== 'Online' && tmpBeacon.Status !== 'Busy') continue;
144
+ let tmpCaps = tmpBeacon.Capabilities || [];
145
+ if (tmpCaps.indexOf('MeadowProxy') < 0) continue;
146
+ let tmpConnID = tmpBeacon.Tags && tmpBeacon.Tags[PERSISTENCE_TAG];
147
+ if (tmpConnID === undefined || tmpConnID === null || tmpConnID === '') continue;
148
+ return { BeaconID: tmpBeacon.BeaconID, IDBeaconConnection: tmpConnID };
149
+ }
150
+ return null;
151
+ }
152
+
153
+ isMeadowProxyMode()
154
+ {
155
+ let tmpAssigned = this.getPersistenceBeacon();
156
+ if (!tmpAssigned) return false;
157
+ return this._BootstrappedBeacons.has(tmpAssigned.BeaconID);
158
+ }
159
+
160
+ // ============== Write API ==============
161
+
162
+ upsertManifest(pManifest)
163
+ {
164
+ return this._writeOrLocal('MS_UpsertManifest', { Manifest: pManifest },
165
+ (pSvc) =>
166
+ {
167
+ // Local fallback: the existing UltravisorExecutionManifest
168
+ // _writeManifest writes to disk in the staging path. We
169
+ // invoke that path directly when no beacon is connected.
170
+ // Caller is responsible for passing a manifest that
171
+ // includes a StagingPath — that's how the in-process
172
+ // service knows where to write.
173
+ if (typeof pSvc._writeManifest === 'function' && pManifest && pManifest.StagingPath)
174
+ {
175
+ pSvc._writeManifest(pManifest, pManifest.StagingPath);
176
+ return { Success: true };
177
+ }
178
+ return { Success: false, Reason: 'Local manifest service has no _writeManifest, or manifest missing StagingPath' };
179
+ });
180
+ }
181
+
182
+ removeManifest(pRunHash)
183
+ {
184
+ return this._writeOrLocal('MS_RemoveManifest', { RunHash: pRunHash },
185
+ (pSvc) =>
186
+ {
187
+ // In-process: drop from the in-memory _Runs map. Disk
188
+ // staging folders aren't pruned here — abandonRun does
189
+ // that, and operators typically run a separate retention
190
+ // sweep.
191
+ if (pSvc._Runs && pSvc._Runs[pRunHash])
192
+ {
193
+ delete pSvc._Runs[pRunHash];
194
+ return { Success: true };
195
+ }
196
+ return { Success: false, Reason: 'Unknown manifest in local store' };
197
+ });
198
+ }
199
+
200
+ // ============== Read API ==============
201
+
202
+ getManifest(pRunHash)
203
+ {
204
+ return this._readOrLocal('MS_GetManifest', { RunHash: pRunHash },
205
+ (pSvc) =>
206
+ {
207
+ let tmpRun = pSvc.getRun ? pSvc.getRun(pRunHash) : null;
208
+ return { Success: !!tmpRun, Manifest: tmpRun };
209
+ });
210
+ }
211
+
212
+ listManifests(pFilter)
213
+ {
214
+ return this._readOrLocal('MS_ListManifests', { Filter: pFilter || {} },
215
+ (pSvc) =>
216
+ {
217
+ let tmpAll = (pSvc.listRuns ? pSvc.listRuns() : []) || [];
218
+ // Apply the same minimal filtering the memory provider
219
+ // supports, so callers see consistent semantics across
220
+ // backends.
221
+ let tmpFilter = pFilter || {};
222
+ if (tmpFilter.OperationHash)
223
+ {
224
+ tmpAll = tmpAll.filter((pM) => pM.OperationHash === tmpFilter.OperationHash);
225
+ }
226
+ if (tmpFilter.Status)
227
+ {
228
+ tmpAll = tmpAll.filter((pM) => pM.Status === tmpFilter.Status);
229
+ }
230
+ let tmpLimit = tmpFilter.Limit || 100;
231
+ return { Success: true, Manifests: tmpAll.slice(0, tmpLimit) };
232
+ });
233
+ }
234
+
235
+ // ============== Bootstrap-flush ==============
236
+
237
+ /**
238
+ * Called by the coordinator when ANY beacon registers (or
239
+ * re-registers after a reconnect). We filter by capability —
240
+ * the notification fans out, but only ManifestStore beacons
241
+ * trigger the flush sweep here.
242
+ *
243
+ * Local source of truth is UltravisorExecutionManifest's
244
+ * in-memory `_Runs` map. Manifests that have already been
245
+ * dropped from memory (because they were persisted to disk
246
+ * and never re-loaded) won't be flushed — the operator-facing
247
+ * /Manifest endpoints already merge live + bridge-historical
248
+ * reads, so historic manifests stay reachable via the merge
249
+ * even if the beacon doesn't have them. Documented assumption:
250
+ * if you crash ultravisor with a finalized-but-not-loaded
251
+ * manifest, the beacon won't backfill it from disk; for that,
252
+ * a future "rescan-on-demand" command would walk the staging
253
+ * dir.
254
+ */
255
+ onBeaconConnected(pBeaconID)
256
+ {
257
+ if (!pBeaconID) return;
258
+ // Try the MeadowProxy bootstrap first; the helper short-circuits
259
+ // if this beacon isn't the assigned persistence backend.
260
+ this._handleMeadowProxyBootstrap(pBeaconID);
261
+ let tmpMSID = this.getBeaconID();
262
+ if (tmpMSID !== pBeaconID) return;
263
+ if (this._FlushInFlight.has(pBeaconID)) return;
264
+ let tmpSvc = this._localService();
265
+ if (!tmpSvc || typeof tmpSvc.listRuns !== 'function') return;
266
+ this._FlushInFlight.add(pBeaconID);
267
+ this._flushManifestsToBeacon(pBeaconID, tmpSvc)
268
+ .then((pCount) =>
269
+ {
270
+ this._FlushInFlight.delete(pBeaconID);
271
+ if (pCount > 0)
272
+ {
273
+ this.log.info(`ManifestStoreBridge: bootstrap-flush pushed ${pCount} manifest(s) to beacon [${pBeaconID}].`);
274
+ }
275
+ })
276
+ .catch((pErr) =>
277
+ {
278
+ this._FlushInFlight.delete(pBeaconID);
279
+ this.log.warn(`ManifestStoreBridge: bootstrap-flush failed: ${pErr && pErr.message}`);
280
+ });
281
+ }
282
+
283
+ async _flushManifestsToBeacon(pBeaconID, pSvc)
284
+ {
285
+ let tmpHWM = this._FlushHWMs[pBeaconID] || '';
286
+ let tmpAll = pSvc.listRuns() || [];
287
+ // StopTime ASC for finalized runs so HWM marches forward
288
+ // monotonically; un-stopped (still-running) runs sort last
289
+ // by falling back to StartTime. _runAnchor() projects the
290
+ // timestamp we use as the HWM key.
291
+ tmpAll.sort((a, b) =>
292
+ {
293
+ let tmpA = this._runAnchor(a);
294
+ let tmpB = this._runAnchor(b);
295
+ if (tmpA < tmpB) return -1;
296
+ if (tmpA > tmpB) return 1;
297
+ return 0;
298
+ });
299
+ if (tmpAll.length > FLUSH_BATCH_LIMIT)
300
+ {
301
+ tmpAll = tmpAll.slice(0, FLUSH_BATCH_LIMIT);
302
+ }
303
+ let tmpPushed = 0;
304
+ for (let i = 0; i < tmpAll.length; i++)
305
+ {
306
+ let tmpRun = tmpAll[i];
307
+ let tmpAnchor = this._runAnchor(tmpRun);
308
+ if (tmpHWM && tmpAnchor && tmpAnchor <= tmpHWM) continue;
309
+ // Build the JSON-serializable shape MS_UpsertManifest
310
+ // expects. Mirrors what UltravisorExecutionManifest's
311
+ // _persistManifestViaBridge produces, kept inline here
312
+ // because the local _Runs entries are full
313
+ // ExecutionContext objects (with closures); we strip to
314
+ // the wire-safe subset.
315
+ let tmpManifest = this._wireSafeManifest(tmpRun);
316
+ if (!tmpManifest) continue;
317
+ let tmpResult = await this._dispatch('MS_UpsertManifest', { Manifest: tmpManifest });
318
+ if (!tmpResult || !tmpResult.Success) break;
319
+ tmpHWM = tmpAnchor || tmpHWM;
320
+ this._FlushHWMs[pBeaconID] = tmpHWM;
321
+ this._saveHWMs();
322
+ tmpPushed += 1;
323
+ }
324
+ return tmpPushed;
325
+ }
326
+
327
+ _runAnchor(pRun)
328
+ {
329
+ if (!pRun) return '';
330
+ // Prefer terminal time so finalized runs cluster cleanly at
331
+ // the front of the sweep order. In-flight runs use StartTime
332
+ // — they re-flush on each subsequent sweep until they finalize.
333
+ return pRun.StopTime || pRun.StartTime || '';
334
+ }
335
+
336
+ _wireSafeManifest(pRun)
337
+ {
338
+ if (!pRun) return null;
339
+ // Match the projection the existing /Manifest endpoint and
340
+ // _persistManifestViaBridge use. Anything carrying closures
341
+ // (PendingEvents, callback queues) gets dropped here.
342
+ return {
343
+ Hash: pRun.Hash,
344
+ OperationHash: pRun.OperationHash,
345
+ OperationName: pRun.OperationName,
346
+ Status: pRun.Status,
347
+ RunMode: pRun.RunMode,
348
+ Live: pRun.Live || false,
349
+ StartTime: pRun.StartTime,
350
+ StopTime: pRun.StopTime,
351
+ ElapsedMs: pRun.ElapsedMs,
352
+ Output: pRun.Output || {},
353
+ GlobalState: pRun.GlobalState || {},
354
+ OperationState: pRun.OperationState || {},
355
+ TaskOutputs: pRun.TaskOutputs || {},
356
+ TaskManifests: pRun.TaskManifests || {},
357
+ WaitingTasks: pRun.WaitingTasks || {},
358
+ TimingSummary: pRun.TimingSummary || null,
359
+ EventLog: pRun.EventLog || [],
360
+ Errors: pRun.Errors || [],
361
+ Log: pRun.Log || [],
362
+ StagingPath: pRun.StagingPath || ''
363
+ };
364
+ }
365
+
366
+ _loadHWMs()
367
+ {
368
+ try
369
+ {
370
+ let tmpPath = this._hwmPath();
371
+ if (!tmpPath || !libFs.existsSync(tmpPath)) return {};
372
+ let tmpDoc = JSON.parse(libFs.readFileSync(tmpPath, 'utf8'));
373
+ return (tmpDoc && tmpDoc.Manifest) || {};
374
+ }
375
+ catch (pErr) { return {}; }
376
+ }
377
+
378
+ _saveHWMs()
379
+ {
380
+ try
381
+ {
382
+ let tmpPath = this._hwmPath();
383
+ if (!tmpPath) return;
384
+ let tmpDoc = {};
385
+ if (libFs.existsSync(tmpPath))
386
+ {
387
+ try { tmpDoc = JSON.parse(libFs.readFileSync(tmpPath, 'utf8')) || {}; }
388
+ catch (e) { tmpDoc = {}; }
389
+ }
390
+ tmpDoc.Manifest = this._FlushHWMs;
391
+ libFs.writeFileSync(tmpPath, JSON.stringify(tmpDoc, null, '\t'), 'utf8');
392
+ }
393
+ catch (pErr)
394
+ {
395
+ this.log.warn(`ManifestStoreBridge: HWM save failed: ${pErr.message}`);
396
+ }
397
+ }
398
+
399
+ _hwmPath()
400
+ {
401
+ let tmpDataPath = (this.fable && this.fable.settings && this.fable.settings.UltravisorFileStorePath)
402
+ || (this.fable && this.fable.settings && this.fable.settings.DataPath)
403
+ || null;
404
+ if (!tmpDataPath) return null;
405
+ return libPath.join(tmpDataPath, HWM_FILENAME);
406
+ }
407
+
408
+ // ============== Persistence assignment (Session 3) ==============
409
+
410
+ setPersistenceAssignment(pBeaconID, pIDBeaconConnection)
411
+ {
412
+ let tmpPrev = this._PersistenceAssignment;
413
+ if (tmpPrev && tmpPrev.BeaconID && tmpPrev.BeaconID !== pBeaconID)
414
+ {
415
+ this._BootstrappedBeacons.delete(tmpPrev.BeaconID);
416
+ delete this._EndpointBaseByBeacon[tmpPrev.BeaconID];
417
+ }
418
+ if (!pBeaconID)
419
+ {
420
+ this._PersistenceAssignment = null;
421
+ }
422
+ else
423
+ {
424
+ this._PersistenceAssignment =
425
+ {
426
+ BeaconID: pBeaconID,
427
+ IDBeaconConnection: pIDBeaconConnection || 0,
428
+ AssignedAt: new Date().toISOString()
429
+ };
430
+ }
431
+ this._LastBootstrapError = null;
432
+ this._BootstrappedAt = null;
433
+ this._saveAssignment();
434
+ if (!pBeaconID) return;
435
+ let tmpCoord = this._coord();
436
+ let tmpBeacon = tmpCoord && typeof tmpCoord.getBeacon === 'function' ? tmpCoord.getBeacon(pBeaconID) : null;
437
+ if (tmpBeacon && (tmpBeacon.Status === 'Online' || tmpBeacon.Status === 'Busy'))
438
+ {
439
+ setImmediate(() => this._handleMeadowProxyBootstrap(pBeaconID));
440
+ }
441
+ }
442
+
443
+ clearPersistenceAssignment()
444
+ {
445
+ this.setPersistenceAssignment(null, 0);
446
+ }
447
+
448
+ getPersistenceStatus()
449
+ {
450
+ let tmpAssigned = this._PersistenceAssignment || null;
451
+ let tmpBeaconID = tmpAssigned && tmpAssigned.BeaconID || null;
452
+ let tmpState;
453
+ if (!tmpBeaconID)
454
+ {
455
+ tmpState = 'unassigned';
456
+ }
457
+ else if (this._LastBootstrapError)
458
+ {
459
+ tmpState = 'error';
460
+ }
461
+ else if (this._BootstrappedBeacons.has(tmpBeaconID))
462
+ {
463
+ tmpState = 'bootstrapped';
464
+ }
465
+ else if (this._BootstrapInFlight.has(tmpBeaconID))
466
+ {
467
+ tmpState = 'bootstrapping';
468
+ }
469
+ else
470
+ {
471
+ tmpState = 'waiting-for-beacon';
472
+ }
473
+ return {
474
+ State: tmpState,
475
+ AssignedBeaconID: tmpBeaconID,
476
+ IDBeaconConnection: tmpAssigned ? (tmpAssigned.IDBeaconConnection || 0) : 0,
477
+ LastError: this._LastBootstrapError || null,
478
+ BootstrappedAt: this._BootstrappedAt || null,
479
+ AssignedAt: tmpAssigned ? (tmpAssigned.AssignedAt || null) : null
480
+ };
481
+ }
482
+
483
+ _loadAssignment()
484
+ {
485
+ try
486
+ {
487
+ let tmpPath = this._assignmentPath();
488
+ if (!tmpPath || !libFs.existsSync(tmpPath)) return null;
489
+ let tmpDoc = JSON.parse(libFs.readFileSync(tmpPath, 'utf8'));
490
+ let tmpEntry = tmpDoc && tmpDoc.Manifest;
491
+ if (!tmpEntry || !tmpEntry.BeaconID) return null;
492
+ return tmpEntry;
493
+ }
494
+ catch (pErr) { return null; }
495
+ }
496
+
497
+ _saveAssignment()
498
+ {
499
+ try
500
+ {
501
+ let tmpPath = this._assignmentPath();
502
+ if (!tmpPath) return;
503
+ let tmpDoc = {};
504
+ if (libFs.existsSync(tmpPath))
505
+ {
506
+ try { tmpDoc = JSON.parse(libFs.readFileSync(tmpPath, 'utf8')) || {}; }
507
+ catch (e) { tmpDoc = {}; }
508
+ }
509
+ tmpDoc.Manifest = this._PersistenceAssignment || null;
510
+ libFs.writeFileSync(tmpPath, JSON.stringify(tmpDoc, null, '\t'), 'utf8');
511
+ }
512
+ catch (pErr)
513
+ {
514
+ if (this.log) this.log.warn(`ManifestStoreBridge: assignment save failed: ${pErr.message}`);
515
+ }
516
+ }
517
+
518
+ _assignmentPath()
519
+ {
520
+ let tmpDataPath = (this.fable && this.fable.settings && this.fable.settings.UltravisorFileStorePath)
521
+ || (this.fable && this.fable.settings && this.fable.settings.DataPath)
522
+ || null;
523
+ if (!tmpDataPath) return null;
524
+ return libPath.join(tmpDataPath, ASSIGNMENT_FILENAME);
525
+ }
526
+
527
+ // ============== Internals ==============
528
+
529
+ _coord()
530
+ {
531
+ let tmpMap = this.fable && this.fable.servicesMap
532
+ && this.fable.servicesMap.UltravisorBeaconCoordinator;
533
+ return tmpMap ? Object.values(tmpMap)[0] : null;
534
+ }
535
+
536
+ _localService()
537
+ {
538
+ // Resolve lazily — the existing UltravisorExecutionManifest
539
+ // service is the local fallback target.
540
+ let tmpMap = this.fable && this.fable.servicesMap
541
+ && this.fable.servicesMap.UltravisorExecutionManifest;
542
+ return tmpMap ? Object.values(tmpMap)[0] : null;
543
+ }
544
+
545
+ _writeOrLocal(pAction, pSettings, fLocal)
546
+ {
547
+ if (this.isMeadowProxyMode())
548
+ {
549
+ return this._dispatchViaMeadowProxy(pAction, pSettings);
550
+ }
551
+ if (this.isBeaconAvailable())
552
+ {
553
+ return this._dispatch(pAction, pSettings);
554
+ }
555
+ let tmpSvc = this._localService();
556
+ if (!tmpSvc)
557
+ {
558
+ return Promise.resolve(
559
+ {
560
+ Available: false, Success: false,
561
+ Reason: 'No manifest backend (beacon or local) available'
562
+ });
563
+ }
564
+ try
565
+ {
566
+ let tmpResult = fLocal(tmpSvc);
567
+ return Promise.resolve(Object.assign({ Available: true }, tmpResult));
568
+ }
569
+ catch (pErr)
570
+ {
571
+ return Promise.resolve(
572
+ {
573
+ Available: true, Success: false,
574
+ Reason: (pErr && pErr.message) || String(pErr)
575
+ });
576
+ }
577
+ }
578
+
579
+ _readOrLocal(pAction, pSettings, fLocal)
580
+ {
581
+ if (this.isMeadowProxyMode())
582
+ {
583
+ return this._dispatchViaMeadowProxy(pAction, pSettings);
584
+ }
585
+ if (this.isBeaconAvailable())
586
+ {
587
+ return this._dispatch(pAction, pSettings);
588
+ }
589
+ let tmpSvc = this._localService();
590
+ if (!tmpSvc)
591
+ {
592
+ return Promise.resolve(
593
+ {
594
+ Available: false, Success: false,
595
+ Reason: 'No manifest backend available',
596
+ Manifest: null, Manifests: []
597
+ });
598
+ }
599
+ try
600
+ {
601
+ let tmpResult = fLocal(tmpSvc);
602
+ return Promise.resolve(Object.assign({ Available: true }, tmpResult));
603
+ }
604
+ catch (pErr)
605
+ {
606
+ return Promise.resolve(
607
+ {
608
+ Available: true, Success: false,
609
+ Reason: (pErr && pErr.message) || String(pErr)
610
+ });
611
+ }
612
+ }
613
+
614
+ _dispatch(pAction, pSettings)
615
+ {
616
+ return new Promise((fResolve) =>
617
+ {
618
+ let tmpCoord = this._coord();
619
+ if (!tmpCoord)
620
+ {
621
+ return fResolve(
622
+ {
623
+ Available: false, Success: false,
624
+ Reason: 'BeaconCoordinator not available'
625
+ });
626
+ }
627
+ tmpCoord.dispatchAndWait(
628
+ {
629
+ Capability: 'ManifestStore',
630
+ Action: pAction,
631
+ Settings: pSettings || {},
632
+ AffinityKey: 'manifest-store',
633
+ TimeoutMs: this._TimeoutMs
634
+ },
635
+ (pError, pResult) =>
636
+ {
637
+ if (pError)
638
+ {
639
+ return fResolve(
640
+ {
641
+ Available: true, Success: false,
642
+ Reason: pError.message || String(pError)
643
+ });
644
+ }
645
+ let tmpOut = (pResult && pResult.Outputs) || {};
646
+ return fResolve(Object.assign({ Available: true }, tmpOut));
647
+ });
648
+ });
649
+ }
650
+
651
+ // ============== MeadowProxy dispatch path ==============
652
+
653
+ /**
654
+ * Translate an MS_* action onto a MeadowProxy REST request and
655
+ * dispatch it through the assigned databeacon.
656
+ *
657
+ * Translation table (see `docs/features/persistence-via-databeacon.md`):
658
+ *
659
+ * MS_UpsertManifest → POST /1.0/<hash>/UVManifest
660
+ * (relies on `Hash` unique index for
661
+ * idempotency; PUT-by-hash needs a hash→ID
662
+ * lookup that lands with the lab UI work).
663
+ * MS_RemoveManifest → deferred (soft-delete needs the IDRecord).
664
+ * MS_GetManifest → GET /1.0/<hash>/UVManifests/FilteredTo/FBV~Hash~EQ~<hash>
665
+ * MS_ListManifests → GET /1.0/<hash>/UVManifests
666
+ */
667
+ _dispatchViaMeadowProxy(pAction, pSettings)
668
+ {
669
+ let tmpAssigned = this.getPersistenceBeacon();
670
+ if (!tmpAssigned)
671
+ {
672
+ return Promise.resolve(
673
+ {
674
+ Available: false, Success: false,
675
+ Reason: 'No MeadowProxy persistence beacon assigned'
676
+ });
677
+ }
678
+ // MS_RemoveManifest is a soft-delete via meadow's DELETE endpoint —
679
+ // because the schema declares `Deleted` as Type='Deleted', meadow
680
+ // flips the column rather than hard-removing. We still need a
681
+ // hash → IDRecord lookup first because meadow's DELETE-by-id
682
+ // addresses by primary key.
683
+ if (pAction === 'MS_RemoveManifest')
684
+ {
685
+ return this._dispatchDeleteByHash(tmpAssigned.BeaconID,
686
+ 'UVManifest', 'IDUVManifest',
687
+ 'Hash', pSettings && pSettings.RunHash);
688
+ }
689
+ return new Promise((fResolve) =>
690
+ {
691
+ let tmpReq;
692
+ try
693
+ {
694
+ tmpReq = this._buildMeadowProxyRequest(pAction, pSettings, tmpAssigned.BeaconID);
695
+ }
696
+ catch (pErr)
697
+ {
698
+ return fResolve(
699
+ {
700
+ Available: true, Success: false,
701
+ Reason: pErr.message || String(pErr)
702
+ });
703
+ }
704
+ if (!tmpReq)
705
+ {
706
+ return fResolve(
707
+ {
708
+ Available: true, Success: false,
709
+ Reason: `Action [${pAction}] is not yet wired through MeadowProxy`
710
+ });
711
+ }
712
+
713
+ let tmpCoord = this._coord();
714
+ if (!tmpCoord)
715
+ {
716
+ return fResolve(
717
+ {
718
+ Available: false, Success: false,
719
+ Reason: 'BeaconCoordinator not available'
720
+ });
721
+ }
722
+ tmpCoord.dispatchAndWait(
723
+ {
724
+ Capability: 'MeadowProxy',
725
+ Action: 'Request',
726
+ Settings: tmpReq,
727
+ AffinityKey: 'manifest-store',
728
+ TimeoutMs: this._TimeoutMs
729
+ },
730
+ (pError, pResult) =>
731
+ {
732
+ if (pError)
733
+ {
734
+ return fResolve(
735
+ {
736
+ Available: true, Success: false,
737
+ Reason: pError.message || String(pError)
738
+ });
739
+ }
740
+ let tmpOut = (pResult && pResult.Outputs) || {};
741
+ let tmpStatus = tmpOut.Status || 0;
742
+ let tmpBody = tmpOut.Body;
743
+ let tmpSuccess = (tmpStatus >= 200 && tmpStatus < 300);
744
+ return fResolve(this._normalizeMeadowProxyResult(pAction, tmpStatus, tmpBody, tmpSuccess));
745
+ });
746
+ });
747
+ }
748
+
749
+ _buildMeadowProxyRequest(pAction, pSettings, pBeaconID)
750
+ {
751
+ let tmpBase = this._endpointBase(pBeaconID, 'UVManifest');
752
+ let tmpUser = this._resolveRemoteUser();
753
+ switch (pAction)
754
+ {
755
+ case 'MS_UpsertManifest':
756
+ {
757
+ let tmpManifest = pSettings && pSettings.Manifest;
758
+ if (!tmpManifest) { throw new Error('MS_UpsertManifest: Manifest is required.'); }
759
+ // Project onto the UVManifest schema. Anything not listed
760
+ // here would be rejected by meadow ("table has no column
761
+ // named X") since we only created the columns the schema
762
+ // descriptor declares. The full manifest payload survives
763
+ // in the ManifestJSON blob.
764
+ let tmpRow =
765
+ {
766
+ Hash: tmpManifest.Hash || '',
767
+ OperationHash: tmpManifest.OperationHash || '',
768
+ OperationName: tmpManifest.OperationName || '',
769
+ Status: tmpManifest.Status || '',
770
+ RunMode: tmpManifest.RunMode || '',
771
+ Live: !!tmpManifest.Live,
772
+ StartTime: tmpManifest.StartTime || '',
773
+ StopTime: tmpManifest.StopTime || '',
774
+ ElapsedMs: tmpManifest.ElapsedMs || 0,
775
+ StagingPath: tmpManifest.StagingPath || '',
776
+ ManifestJSON: typeof tmpManifest.ManifestJSON === 'string'
777
+ ? tmpManifest.ManifestJSON
778
+ : JSON.stringify(tmpManifest)
779
+ };
780
+ return { Method: 'POST', Path: tmpBase, Body: JSON.stringify(tmpRow), RemoteUser: tmpUser };
781
+ }
782
+ case 'MS_GetManifest':
783
+ {
784
+ let tmpHash = pSettings && pSettings.RunHash;
785
+ if (!tmpHash) { throw new Error('MS_GetManifest: RunHash is required.'); }
786
+ return { Method: 'GET', Path: `${tmpBase}s/FilteredTo/FBV~Hash~EQ~${encodeURIComponent(tmpHash)}`, RemoteUser: tmpUser };
787
+ }
788
+ case 'MS_ListManifests':
789
+ {
790
+ return { Method: 'GET', Path: `${tmpBase}s`, RemoteUser: tmpUser };
791
+ }
792
+ default:
793
+ return null;
794
+ }
795
+ }
796
+
797
+ _normalizeMeadowProxyResult(pAction, pStatus, pBody, pSuccess)
798
+ {
799
+ let tmpParsed = null;
800
+ if (typeof pBody === 'string' && pBody.length > 0)
801
+ {
802
+ try { tmpParsed = JSON.parse(pBody); }
803
+ catch (pErr) { tmpParsed = null; }
804
+ }
805
+ else if (typeof pBody === 'object')
806
+ {
807
+ tmpParsed = pBody;
808
+ }
809
+
810
+ switch (pAction)
811
+ {
812
+ case 'MS_GetManifest':
813
+ {
814
+ let tmpRow = Array.isArray(tmpParsed) ? (tmpParsed[0] || null) : null;
815
+ return { Available: true, Success: !!tmpRow, Manifest: tmpRow };
816
+ }
817
+ case 'MS_ListManifests':
818
+ return { Available: true, Success: pSuccess, Manifests: Array.isArray(tmpParsed) ? tmpParsed : [] };
819
+ default:
820
+ if (pSuccess)
821
+ {
822
+ return { Available: true, Success: true, Body: tmpParsed };
823
+ }
824
+ return {
825
+ Available: true, Success: false,
826
+ Status: pStatus,
827
+ Reason: (tmpParsed && (tmpParsed.error || tmpParsed.message)) || `MeadowProxy ${pStatus}`
828
+ };
829
+ }
830
+ }
831
+
832
+ _endpointBase(pBeaconID, pTableName)
833
+ {
834
+ let tmpCache = this._EndpointBaseByBeacon[pBeaconID] || {};
835
+ if (tmpCache[pTableName]) { return tmpCache[pTableName]; }
836
+ return `/1.0/${pTableName}`;
837
+ }
838
+
839
+ /**
840
+ * Pass-through value for MeadowProxy.Request's `RemoteUser` field.
841
+ * See the queue bridge — same rationale, same future plumbing.
842
+ */
843
+ _resolveRemoteUser()
844
+ {
845
+ return 'ultravisor-system';
846
+ }
847
+
848
+ _lookupIDByHash(pBeaconID, pTable, pIDColumn, pHashColumn, pHashValue)
849
+ {
850
+ return new Promise((fResolve) =>
851
+ {
852
+ let tmpCoord = this._coord();
853
+ if (!tmpCoord)
854
+ {
855
+ return fResolve({ Success: false, Reason: 'BeaconCoordinator not available' });
856
+ }
857
+ if (!pHashValue)
858
+ {
859
+ return fResolve({ Success: false, Reason: `${pHashColumn} is required for lookup` });
860
+ }
861
+ let tmpBase = this._endpointBase(pBeaconID, pTable);
862
+ let tmpReq =
863
+ {
864
+ Method: 'GET',
865
+ Path: `${tmpBase}s/FilteredTo/FBV~${pHashColumn}~EQ~${encodeURIComponent(pHashValue)}`,
866
+ RemoteUser: this._resolveRemoteUser()
867
+ };
868
+ tmpCoord.dispatchAndWait(
869
+ {
870
+ Capability: 'MeadowProxy',
871
+ Action: 'Request',
872
+ Settings: tmpReq,
873
+ AffinityKey: 'manifest-store',
874
+ TimeoutMs: this._TimeoutMs
875
+ },
876
+ (pError, pResult) =>
877
+ {
878
+ if (pError) return fResolve({ Success: false, Reason: pError.message || String(pError) });
879
+ let tmpOut = (pResult && pResult.Outputs) || {};
880
+ let tmpStatus = tmpOut.Status || 0;
881
+ if (tmpStatus < 200 || tmpStatus >= 300)
882
+ {
883
+ return fResolve({ Success: false, Reason: `Lookup failed: HTTP ${tmpStatus}` });
884
+ }
885
+ let tmpParsed = null;
886
+ if (typeof tmpOut.Body === 'string' && tmpOut.Body.length > 0)
887
+ {
888
+ try { tmpParsed = JSON.parse(tmpOut.Body); }
889
+ catch (e) { tmpParsed = null; }
890
+ }
891
+ else if (typeof tmpOut.Body === 'object')
892
+ {
893
+ tmpParsed = tmpOut.Body;
894
+ }
895
+ let tmpRow = Array.isArray(tmpParsed) ? (tmpParsed[0] || null) : null;
896
+ if (!tmpRow)
897
+ {
898
+ return fResolve({ Success: true, Found: false, IDRecord: null });
899
+ }
900
+ return fResolve({ Success: true, Found: true, IDRecord: tmpRow[pIDColumn], Row: tmpRow });
901
+ });
902
+ });
903
+ }
904
+
905
+ async _dispatchUpdateByHash(pBeaconID, pTable, pIDColumn, pHashColumn, pHashValue, pPatch)
906
+ {
907
+ let tmpLookup = await this._lookupIDByHash(pBeaconID, pTable, pIDColumn, pHashColumn, pHashValue);
908
+ if (!tmpLookup.Success)
909
+ {
910
+ return { Available: true, Success: false, Reason: tmpLookup.Reason };
911
+ }
912
+ if (!tmpLookup.Found)
913
+ {
914
+ return { Available: true, Success: false, Reason: `${pTable} row with ${pHashColumn}=${pHashValue} not found` };
915
+ }
916
+ let tmpBody = Object.assign({}, pPatch || {});
917
+ tmpBody[pIDColumn] = tmpLookup.IDRecord;
918
+ let tmpBase = this._endpointBase(pBeaconID, pTable);
919
+ return this._putByID(tmpBase, tmpLookup.IDRecord, tmpBody);
920
+ }
921
+
922
+ async _dispatchDeleteByHash(pBeaconID, pTable, pIDColumn, pHashColumn, pHashValue)
923
+ {
924
+ let tmpLookup = await this._lookupIDByHash(pBeaconID, pTable, pIDColumn, pHashColumn, pHashValue);
925
+ if (!tmpLookup.Success)
926
+ {
927
+ return { Available: true, Success: false, Reason: tmpLookup.Reason };
928
+ }
929
+ if (!tmpLookup.Found)
930
+ {
931
+ return { Available: true, Success: true, AlreadyAbsent: true };
932
+ }
933
+ let tmpBase = this._endpointBase(pBeaconID, pTable);
934
+ return new Promise((fResolve) =>
935
+ {
936
+ let tmpCoord = this._coord();
937
+ if (!tmpCoord)
938
+ {
939
+ return fResolve({ Available: false, Success: false, Reason: 'BeaconCoordinator not available' });
940
+ }
941
+ let tmpReq =
942
+ {
943
+ Method: 'DELETE',
944
+ Path: `${tmpBase}/${encodeURIComponent(tmpLookup.IDRecord)}`,
945
+ RemoteUser: this._resolveRemoteUser()
946
+ };
947
+ tmpCoord.dispatchAndWait(
948
+ {
949
+ Capability: 'MeadowProxy',
950
+ Action: 'Request',
951
+ Settings: tmpReq,
952
+ AffinityKey: 'manifest-store',
953
+ TimeoutMs: this._TimeoutMs
954
+ },
955
+ (pError, pResult) =>
956
+ {
957
+ if (pError)
958
+ {
959
+ return fResolve({ Available: true, Success: false, Reason: pError.message || String(pError) });
960
+ }
961
+ let tmpOut = (pResult && pResult.Outputs) || {};
962
+ let tmpStatus = tmpOut.Status || 0;
963
+ let tmpSuccess = (tmpStatus >= 200 && tmpStatus < 300);
964
+ if (!tmpSuccess)
965
+ {
966
+ return fResolve({ Available: true, Success: false, Status: tmpStatus, Reason: `DELETE returned ${tmpStatus}` });
967
+ }
968
+ return fResolve({ Available: true, Success: true });
969
+ });
970
+ });
971
+ }
972
+
973
+ _putByID(pEndpointBase, pIDRecord, pBody)
974
+ {
975
+ return new Promise((fResolve) =>
976
+ {
977
+ let tmpCoord = this._coord();
978
+ if (!tmpCoord)
979
+ {
980
+ return fResolve({ Available: false, Success: false, Reason: 'BeaconCoordinator not available' });
981
+ }
982
+ // Meadow's Update endpoint is `PUT <base>` — PK travels in the body.
983
+ let tmpReq =
984
+ {
985
+ Method: 'PUT',
986
+ Path: pEndpointBase,
987
+ Body: JSON.stringify(pBody),
988
+ RemoteUser: this._resolveRemoteUser()
989
+ };
990
+ tmpCoord.dispatchAndWait(
991
+ {
992
+ Capability: 'MeadowProxy',
993
+ Action: 'Request',
994
+ Settings: tmpReq,
995
+ AffinityKey: 'manifest-store',
996
+ TimeoutMs: this._TimeoutMs
997
+ },
998
+ (pError, pResult) =>
999
+ {
1000
+ if (pError)
1001
+ {
1002
+ return fResolve({ Available: true, Success: false, Reason: pError.message || String(pError) });
1003
+ }
1004
+ let tmpOut = (pResult && pResult.Outputs) || {};
1005
+ let tmpStatus = tmpOut.Status || 0;
1006
+ let tmpSuccess = (tmpStatus >= 200 && tmpStatus < 300);
1007
+ if (!tmpSuccess)
1008
+ {
1009
+ return fResolve({ Available: true, Success: false, Status: tmpStatus, Reason: `PUT returned ${tmpStatus}` });
1010
+ }
1011
+ return fResolve({ Available: true, Success: true });
1012
+ });
1013
+ });
1014
+ }
1015
+
1016
+ // ============== Schema bootstrap state machine ==============
1017
+
1018
+ _handleMeadowProxyBootstrap(pBeaconID)
1019
+ {
1020
+ if (!pBeaconID) return;
1021
+ if (!this._SchemaDescriptor) return;
1022
+ if (this._BootstrappedBeacons.has(pBeaconID)) return;
1023
+ if (this._BootstrapInFlight.has(pBeaconID)) return;
1024
+ let tmpAssigned = this.getPersistenceBeacon();
1025
+ if (!tmpAssigned || tmpAssigned.BeaconID !== pBeaconID) return;
1026
+ this._BootstrapInFlight.add(pBeaconID);
1027
+ this._runBootstrap(pBeaconID, tmpAssigned.IDBeaconConnection)
1028
+ .then((pResult) =>
1029
+ {
1030
+ this._BootstrapInFlight.delete(pBeaconID);
1031
+ if (pResult && pResult.Success)
1032
+ {
1033
+ this._BootstrappedBeacons.add(pBeaconID);
1034
+ this._BootstrappedAt = new Date().toISOString();
1035
+ this._LastBootstrapError = null;
1036
+ this.log.info(`ManifestStoreBridge: MeadowProxy bootstrap complete for beacon [${pBeaconID}].`);
1037
+ }
1038
+ else
1039
+ {
1040
+ this._LastBootstrapError = (pResult && pResult.Reason) || 'Unknown bootstrap error';
1041
+ this.log.warn(`ManifestStoreBridge: MeadowProxy bootstrap failed for beacon [${pBeaconID}]: ${this._LastBootstrapError}`);
1042
+ }
1043
+ })
1044
+ .catch((pErr) =>
1045
+ {
1046
+ this._BootstrapInFlight.delete(pBeaconID);
1047
+ this._LastBootstrapError = (pErr && pErr.message) || String(pErr);
1048
+ this.log.warn(`ManifestStoreBridge: MeadowProxy bootstrap threw for beacon [${pBeaconID}]: ${this._LastBootstrapError}`);
1049
+ });
1050
+ }
1051
+
1052
+ async _runBootstrap(pBeaconID, pIDBeaconConnection)
1053
+ {
1054
+ let tmpCoord = this._coord();
1055
+ if (!tmpCoord) return { Success: false, Reason: 'BeaconCoordinator not available' };
1056
+
1057
+ let tmpEnsure = await this._mestDispatch(tmpCoord, 'DataBeaconSchema', 'EnsureSchema',
1058
+ {
1059
+ IDBeaconConnection: pIDBeaconConnection,
1060
+ SchemaName: this._SchemaDescriptor.SchemaName || 'ultravisor',
1061
+ SchemaJSON: this._SchemaDescriptor
1062
+ });
1063
+ if (!tmpEnsure.Success)
1064
+ {
1065
+ return { Success: false, Reason: `EnsureSchema failed: ${tmpEnsure.Reason}` };
1066
+ }
1067
+
1068
+ // Introspect populates the databeacon's IntrospectedTable cache
1069
+ // so subsequent EnableEndpoint calls find the columns for our
1070
+ // freshly-created UV* tables.
1071
+ let tmpIntrospect = await this._mestDispatch(tmpCoord, 'DataBeaconManagement', 'Introspect',
1072
+ {
1073
+ IDBeaconConnection: pIDBeaconConnection
1074
+ });
1075
+ if (!tmpIntrospect.Success)
1076
+ {
1077
+ return { Success: false, Reason: `Introspect failed: ${tmpIntrospect.Reason}` };
1078
+ }
1079
+
1080
+ let tmpProxy = await this._mestDispatch(tmpCoord, 'DataBeaconManagement', 'UpdateProxyConfig',
1081
+ {
1082
+ PathAllowlist: UV_PROXY_PATH_PATTERNS
1083
+ });
1084
+ if (!tmpProxy.Success)
1085
+ {
1086
+ return { Success: false, Reason: `UpdateProxyConfig failed: ${tmpProxy.Reason}` };
1087
+ }
1088
+
1089
+ this._EndpointBaseByBeacon[pBeaconID] = this._EndpointBaseByBeacon[pBeaconID] || {};
1090
+ for (let i = 0; i < MANIFEST_TABLES.length; i++)
1091
+ {
1092
+ let tmpTable = MANIFEST_TABLES[i];
1093
+ let tmpEnable = await this._mestDispatch(tmpCoord, 'DataBeaconManagement', 'EnableEndpoint',
1094
+ {
1095
+ IDBeaconConnection: pIDBeaconConnection,
1096
+ TableName: tmpTable
1097
+ });
1098
+ if (!tmpEnable.Success)
1099
+ {
1100
+ return { Success: false, Reason: `EnableEndpoint(${tmpTable}) failed: ${tmpEnable.Reason}` };
1101
+ }
1102
+ let tmpBase = (tmpEnable.Outputs && tmpEnable.Outputs.EndpointBase) || `/1.0/${tmpTable}`;
1103
+ this._EndpointBaseByBeacon[pBeaconID][tmpTable] = tmpBase;
1104
+ }
1105
+ return { Success: true };
1106
+ }
1107
+
1108
+ _mestDispatch(pCoord, pCapability, pAction, pSettings)
1109
+ {
1110
+ return new Promise((fResolve) =>
1111
+ {
1112
+ pCoord.dispatchAndWait(
1113
+ {
1114
+ Capability: pCapability,
1115
+ Action: pAction,
1116
+ Settings: pSettings || {},
1117
+ AffinityKey: 'manifest-store-bootstrap',
1118
+ TimeoutMs: this._TimeoutMs * 4
1119
+ },
1120
+ (pError, pResult) =>
1121
+ {
1122
+ if (pError)
1123
+ {
1124
+ return fResolve({ Success: false, Reason: pError.message || String(pError) });
1125
+ }
1126
+ let tmpOut = (pResult && pResult.Outputs) || {};
1127
+ let tmpSuccess = (tmpOut.Success !== false);
1128
+ return fResolve({ Success: tmpSuccess, Reason: tmpOut.Reason || '', Outputs: tmpOut });
1129
+ });
1130
+ });
1131
+ }
1132
+ }
1133
+
1134
+ module.exports = UltravisorManifestStoreBridge;