ultravisor-beacon 0.0.2 → 0.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultravisor-beacon",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Ultravisor Beacon: lightweight beacon client and Fable service for remote task execution",
5
5
  "main": "source/Ultravisor-Beacon-Service.cjs",
6
6
  "scripts": {
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Ultravisor Beacon Address Resolver
3
+ *
4
+ * Parses and resolves Universal Data Addresses — a URI scheme for
5
+ * referencing resources across a federated beacon mesh.
6
+ *
7
+ * Address format:
8
+ * >BeaconID/Context/Path...
9
+ *
10
+ * Components:
11
+ * > — Prefix indicating a beacon-scoped resource
12
+ * BeaconID — The registered beacon's name or ID
13
+ * Special values: '*' (any beacon with required capability),
14
+ * 'ULTRAVISOR' (the orchestrator itself)
15
+ * Context — The namespace within the beacon:
16
+ * 'File' — Filesystem access (content root)
17
+ * 'Staging' — Work item staging area
18
+ * 'Cache' — Cache storage (thumbnails, previews, etc.)
19
+ * 'Projection' — Data query endpoint (e.g. retold-facto)
20
+ * 'Operation' — Operation state/artifacts on the orchestrator
21
+ * Path — The resource path within that context
22
+ *
23
+ * Examples:
24
+ * >RR-BCN-001/File/volume3/Sort/SomeSong.mp3
25
+ * >ULTRAVISOR/Operation/0x732490df0/Stage/Transcoded.avi
26
+ * >RF-BCN-001/Projection/Countries/*
27
+ * >RF-BCN-001/Projection/Countries/FilteredTo/FBV~Name~LK~Col%25
28
+ * >WILDCARD/MediaConversion/Staging/input.jpg (use * for any beacon)
29
+ *
30
+ * This module is Fable-free — usable from both beacon clients and
31
+ * the Ultravisor server.
32
+ */
33
+
34
+ class UltravisorBeaconAddressResolver
35
+ {
36
+ constructor(pOptions)
37
+ {
38
+ let tmpOptions = pOptions || {};
39
+
40
+ // Registry of known beacons and their contexts
41
+ // Map of BeaconID → { Contexts: { ContextName: { BasePath, BaseURL, Writable } } }
42
+ this._BeaconRegistry = {};
43
+
44
+ // Local beacon ID (set when running on a beacon)
45
+ this._LocalBeaconID = tmpOptions.LocalBeaconID || null;
46
+
47
+ // Local context mappings (set when running on a beacon)
48
+ // Map of ContextName → absolute filesystem path
49
+ this._LocalContextPaths = {};
50
+ }
51
+
52
+ // ================================================================
53
+ // Address Parsing
54
+ // ================================================================
55
+
56
+ /**
57
+ * Parse a universal data address string into its components.
58
+ *
59
+ * @param {string} pAddress - The address string (e.g. '>RR-BCN-001/File/photos/img.jpg')
60
+ * @returns {object|null} Parsed address: { BeaconID, Context, Path, Raw }
61
+ * Returns null if the address is not a valid universal address.
62
+ */
63
+ parse(pAddress)
64
+ {
65
+ if (!pAddress || typeof pAddress !== 'string')
66
+ {
67
+ return null;
68
+ }
69
+
70
+ // Must start with >
71
+ if (pAddress.charAt(0) !== '>')
72
+ {
73
+ return null;
74
+ }
75
+
76
+ // Strip the prefix
77
+ let tmpBody = pAddress.substring(1);
78
+
79
+ // Split into segments: BeaconID / Context / Path...
80
+ let tmpFirstSlash = tmpBody.indexOf('/');
81
+ if (tmpFirstSlash < 0)
82
+ {
83
+ // Just a beacon ID with no context or path
84
+ return {
85
+ BeaconID: tmpBody,
86
+ Context: '',
87
+ Path: '',
88
+ Raw: pAddress
89
+ };
90
+ }
91
+
92
+ let tmpBeaconID = tmpBody.substring(0, tmpFirstSlash);
93
+ let tmpRemainder = tmpBody.substring(tmpFirstSlash + 1);
94
+
95
+ let tmpSecondSlash = tmpRemainder.indexOf('/');
96
+ if (tmpSecondSlash < 0)
97
+ {
98
+ // BeaconID and Context, no path
99
+ return {
100
+ BeaconID: tmpBeaconID,
101
+ Context: tmpRemainder,
102
+ Path: '',
103
+ Raw: pAddress
104
+ };
105
+ }
106
+
107
+ let tmpContext = tmpRemainder.substring(0, tmpSecondSlash);
108
+ let tmpPath = tmpRemainder.substring(tmpSecondSlash + 1);
109
+
110
+ return {
111
+ BeaconID: tmpBeaconID,
112
+ Context: tmpContext,
113
+ Path: tmpPath,
114
+ Raw: pAddress
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Check if a string is a universal data address.
120
+ *
121
+ * @param {string} pAddress - The string to check.
122
+ * @returns {boolean} True if the string starts with '>'.
123
+ */
124
+ isUniversalAddress(pAddress)
125
+ {
126
+ return (typeof pAddress === 'string' && pAddress.length > 1 && pAddress.charAt(0) === '>');
127
+ }
128
+
129
+ /**
130
+ * Compose a universal data address from components.
131
+ *
132
+ * @param {string} pBeaconID - The beacon identifier.
133
+ * @param {string} pContext - The context namespace.
134
+ * @param {string} pPath - The resource path.
135
+ * @returns {string} The composed address.
136
+ */
137
+ compose(pBeaconID, pContext, pPath)
138
+ {
139
+ let tmpParts = ['>', pBeaconID];
140
+ if (pContext)
141
+ {
142
+ tmpParts.push('/', pContext);
143
+ if (pPath)
144
+ {
145
+ tmpParts.push('/', pPath);
146
+ }
147
+ }
148
+ return tmpParts.join('');
149
+ }
150
+
151
+ // ================================================================
152
+ // Beacon Registry
153
+ // ================================================================
154
+
155
+ /**
156
+ * Register a beacon and its available contexts.
157
+ *
158
+ * @param {string} pBeaconID - The beacon identifier.
159
+ * @param {object} pContexts - Map of ContextName → { BasePath, BaseURL, Writable, Description }
160
+ */
161
+ registerBeacon(pBeaconID, pContexts)
162
+ {
163
+ this._BeaconRegistry[pBeaconID] = {
164
+ Contexts: pContexts || {}
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Deregister a beacon.
170
+ *
171
+ * @param {string} pBeaconID - The beacon identifier to remove.
172
+ */
173
+ deregisterBeacon(pBeaconID)
174
+ {
175
+ delete this._BeaconRegistry[pBeaconID];
176
+ }
177
+
178
+ /**
179
+ * Get the context definition for a beacon.
180
+ *
181
+ * @param {string} pBeaconID - The beacon identifier.
182
+ * @param {string} pContext - The context name.
183
+ * @returns {object|null} The context definition, or null if not found.
184
+ */
185
+ getBeaconContext(pBeaconID, pContext)
186
+ {
187
+ let tmpBeacon = this._BeaconRegistry[pBeaconID];
188
+ if (!tmpBeacon || !tmpBeacon.Contexts)
189
+ {
190
+ return null;
191
+ }
192
+ return tmpBeacon.Contexts[pContext] || null;
193
+ }
194
+
195
+ /**
196
+ * List all registered beacons and their contexts.
197
+ *
198
+ * @returns {object} Map of BeaconID → { Contexts }
199
+ */
200
+ listBeacons()
201
+ {
202
+ return JSON.parse(JSON.stringify(this._BeaconRegistry));
203
+ }
204
+
205
+ // ================================================================
206
+ // Local Context Configuration
207
+ // ================================================================
208
+
209
+ /**
210
+ * Set the local beacon identity (for resolving self-references).
211
+ *
212
+ * @param {string} pBeaconID - This beacon's ID.
213
+ */
214
+ setLocalBeaconID(pBeaconID)
215
+ {
216
+ this._LocalBeaconID = pBeaconID;
217
+ }
218
+
219
+ /**
220
+ * Register a local context path mapping.
221
+ *
222
+ * @param {string} pContext - The context name (e.g. 'File', 'Staging', 'Cache').
223
+ * @param {string} pBasePath - The absolute filesystem path for this context.
224
+ */
225
+ setLocalContextPath(pContext, pBasePath)
226
+ {
227
+ this._LocalContextPaths[pContext] = pBasePath;
228
+ }
229
+
230
+ // ================================================================
231
+ // Address Resolution
232
+ // ================================================================
233
+
234
+ /**
235
+ * Resolve a universal data address to a local filesystem path.
236
+ *
237
+ * Only works for addresses targeting the local beacon or addresses
238
+ * with explicit local context mappings. Returns null for remote addresses.
239
+ *
240
+ * @param {string} pAddress - The universal address string.
241
+ * @returns {object} Resolution result:
242
+ * { Local: true, Path: '/absolute/path/to/file' }
243
+ * or { Local: false, BeaconID: '...', Context: '...', Path: '...' }
244
+ * or { Error: 'description' }
245
+ */
246
+ resolve(pAddress)
247
+ {
248
+ let tmpParsed = this.parse(pAddress);
249
+ if (!tmpParsed)
250
+ {
251
+ return { Error: `Invalid universal address: ${pAddress}` };
252
+ }
253
+
254
+ // Check if this targets the local beacon
255
+ let tmpIsLocal = false;
256
+ if (tmpParsed.BeaconID === this._LocalBeaconID)
257
+ {
258
+ tmpIsLocal = true;
259
+ }
260
+
261
+ if (tmpIsLocal)
262
+ {
263
+ let tmpBasePath = this._LocalContextPaths[tmpParsed.Context];
264
+ if (!tmpBasePath)
265
+ {
266
+ return { Error: `Unknown local context: ${tmpParsed.Context}` };
267
+ }
268
+
269
+ let tmpResolvedPath = tmpParsed.Path
270
+ ? require('path').join(tmpBasePath, tmpParsed.Path)
271
+ : tmpBasePath;
272
+
273
+ return {
274
+ Local: true,
275
+ Path: tmpResolvedPath,
276
+ BeaconID: tmpParsed.BeaconID,
277
+ Context: tmpParsed.Context
278
+ };
279
+ }
280
+
281
+ // Remote address — return the parsed components for the caller to handle
282
+ return {
283
+ Local: false,
284
+ BeaconID: tmpParsed.BeaconID,
285
+ Context: tmpParsed.Context,
286
+ Path: tmpParsed.Path
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Build a URL for accessing a resource on a remote beacon.
292
+ *
293
+ * Uses the beacon's registered BaseURL for the context.
294
+ *
295
+ * @param {string} pAddress - The universal address string.
296
+ * @returns {string|null} The full URL, or null if the beacon/context is unknown.
297
+ */
298
+ resolveToURL(pAddress)
299
+ {
300
+ let tmpParsed = this.parse(pAddress);
301
+ if (!tmpParsed)
302
+ {
303
+ return null;
304
+ }
305
+
306
+ let tmpContextDef = this.getBeaconContext(tmpParsed.BeaconID, tmpParsed.Context);
307
+ if (!tmpContextDef || !tmpContextDef.BaseURL)
308
+ {
309
+ return null;
310
+ }
311
+
312
+ let tmpBaseURL = tmpContextDef.BaseURL;
313
+ // Ensure trailing slash on base
314
+ if (tmpBaseURL.charAt(tmpBaseURL.length - 1) !== '/')
315
+ {
316
+ tmpBaseURL += '/';
317
+ }
318
+
319
+ return tmpBaseURL + (tmpParsed.Path || '');
320
+ }
321
+
322
+ /**
323
+ * Scan an object's values for universal addresses and return
324
+ * a list of all addresses found. Useful for pre-processing
325
+ * operation settings to identify file transfer requirements.
326
+ *
327
+ * @param {object} pObject - The object to scan (e.g. work item Settings).
328
+ * @returns {Array<{ Key: string, Address: object }>} List of found addresses.
329
+ */
330
+ scanForAddresses(pObject)
331
+ {
332
+ let tmpResults = [];
333
+
334
+ if (!pObject || typeof pObject !== 'object')
335
+ {
336
+ return tmpResults;
337
+ }
338
+
339
+ let tmpKeys = Object.keys(pObject);
340
+ for (let i = 0; i < tmpKeys.length; i++)
341
+ {
342
+ let tmpValue = pObject[tmpKeys[i]];
343
+ if (typeof tmpValue === 'string' && this.isUniversalAddress(tmpValue))
344
+ {
345
+ let tmpParsed = this.parse(tmpValue);
346
+ if (tmpParsed)
347
+ {
348
+ tmpResults.push({ Key: tmpKeys[i], Address: tmpParsed });
349
+ }
350
+ }
351
+ else if (typeof tmpValue === 'object' && tmpValue !== null)
352
+ {
353
+ // Recurse into nested objects
354
+ let tmpNested = this.scanForAddresses(tmpValue);
355
+ for (let j = 0; j < tmpNested.length; j++)
356
+ {
357
+ tmpNested[j].Key = tmpKeys[i] + '.' + tmpNested[j].Key;
358
+ tmpResults.push(tmpNested[j]);
359
+ }
360
+ }
361
+ }
362
+
363
+ return tmpResults;
364
+ }
365
+ }
366
+
367
+ module.exports = UltravisorBeaconAddressResolver;
@@ -397,10 +397,23 @@ class UltravisorBeaconClient
397
397
  let tmpBody = {
398
398
  Name: this._Config.Name,
399
399
  Capabilities: this._Executor.providerRegistry.getCapabilities(),
400
+ ActionSchemas: this._Executor.providerRegistry.getActionSchemas(),
400
401
  MaxConcurrent: this._Config.MaxConcurrent,
401
402
  Tags: this._Config.Tags
402
403
  };
403
404
 
405
+ // Include contexts if any are defined
406
+ if (this._Config.Contexts && Object.keys(this._Config.Contexts).length > 0)
407
+ {
408
+ tmpBody.Contexts = this._Config.Contexts;
409
+ }
410
+
411
+ // Include operations if any are defined
412
+ if (Array.isArray(this._Config.Operations) && this._Config.Operations.length > 0)
413
+ {
414
+ tmpBody.Operations = this._Config.Operations;
415
+ }
416
+
404
417
  this._httpRequest('POST', '/Beacon/Register', tmpBody, fCallback);
405
418
  }
406
419
 
@@ -685,13 +698,23 @@ class UltravisorBeaconClient
685
698
  console.log(`[Beacon] WebSocket connected to ${tmpWSURL}`);
686
699
 
687
700
  // Register over WebSocket
688
- this._wsSend({
701
+ let tmpWSRegPayload = {
689
702
  Action: 'BeaconRegister',
690
703
  Name: this._Config.Name,
691
704
  Capabilities: this._Executor.providerRegistry.getCapabilities(),
705
+ ActionSchemas: this._Executor.providerRegistry.getActionSchemas(),
692
706
  MaxConcurrent: this._Config.MaxConcurrent,
693
707
  Tags: this._Config.Tags
694
- });
708
+ };
709
+ if (this._Config.Contexts && Object.keys(this._Config.Contexts).length > 0)
710
+ {
711
+ tmpWSRegPayload.Contexts = this._Config.Contexts;
712
+ }
713
+ if (Array.isArray(this._Config.Operations) && this._Config.Operations.length > 0)
714
+ {
715
+ tmpWSRegPayload.Operations = this._Config.Operations;
716
+ }
717
+ this._wsSend(tmpWSRegPayload);
695
718
  });
696
719
 
697
720
  this._WebSocket.on('message', (pMessage) =>
@@ -129,6 +129,42 @@ class UltravisorBeaconProviderRegistry
129
129
  return this._Capabilities.slice();
130
130
  }
131
131
 
132
+ /**
133
+ * Get serializable action schemas from all loaded providers.
134
+ *
135
+ * Collects action metadata (Description, SettingsSchema) from every
136
+ * registered provider. Handler functions are NOT included — the
137
+ * CapabilityAdapter's `actions` getter already strips them.
138
+ *
139
+ * @returns {Array<{ Capability: string, Action: string, Description: string, SettingsSchema: Array }>}
140
+ */
141
+ getActionSchemas()
142
+ {
143
+ let tmpSchemas = [];
144
+ let tmpProviderNames = Object.keys(this._Providers);
145
+
146
+ for (let i = 0; i < tmpProviderNames.length; i++)
147
+ {
148
+ let tmpProvider = this._Providers[tmpProviderNames[i]];
149
+ let tmpActions = tmpProvider.actions || {};
150
+ let tmpActionNames = Object.keys(tmpActions);
151
+
152
+ for (let j = 0; j < tmpActionNames.length; j++)
153
+ {
154
+ let tmpActionDef = tmpActions[tmpActionNames[j]];
155
+
156
+ tmpSchemas.push({
157
+ Capability: tmpProvider.Capability,
158
+ Action: tmpActionNames[j],
159
+ Description: tmpActionDef.Description || '',
160
+ SettingsSchema: tmpActionDef.SettingsSchema || []
161
+ });
162
+ }
163
+ }
164
+
165
+ return tmpSchemas;
166
+ }
167
+
132
168
  /**
133
169
  * Get all loaded providers.
134
170
  *
@@ -30,6 +30,7 @@ const libBeaconClient = require('./Ultravisor-Beacon-Client.cjs');
30
30
  const libCapabilityManager = require('./Ultravisor-Beacon-CapabilityManager.cjs');
31
31
  const libConnectivityHTTP = require('./Ultravisor-Beacon-ConnectivityHTTP.cjs');
32
32
  const libConnectivityWebSocket = require('./Ultravisor-Beacon-ConnectivityWebSocket.cjs');
33
+ const libAddressResolver = require('./Ultravisor-Beacon-AddressResolver.cjs');
33
34
 
34
35
  class UltravisorBeaconService extends libFableServiceBase
35
36
  {
@@ -49,14 +50,19 @@ class UltravisorBeaconService extends libFableServiceBase
49
50
  PollIntervalMs: 5000,
50
51
  HeartbeatIntervalMs: 30000,
51
52
  StagingPath: '',
52
- Tags: {}
53
+ Tags: {},
54
+ Contexts: {}
53
55
  }, this.options || {});
54
56
 
55
57
  // Internal components
56
58
  this._CapabilityManager = new libCapabilityManager();
57
59
  this._ConnectivityService = new libConnectivityHTTP(this.options);
60
+ this._AddressResolver = new libAddressResolver();
58
61
  this._ThinClient = null;
59
62
  this._Enabled = false;
63
+
64
+ // Operation definitions to register with the coordinator on connect
65
+ this._Operations = [];
60
66
  }
61
67
 
62
68
  // ================================================================
@@ -94,6 +100,43 @@ class UltravisorBeaconService extends libFableServiceBase
94
100
  return this;
95
101
  }
96
102
 
103
+ /**
104
+ * Register an operation definition to push to the coordinator on connect.
105
+ *
106
+ * Operation definitions use the same graph structure as HypervisorState
107
+ * operations (Hash, Name, Description, Graph with Nodes and Connections).
108
+ * These are sent during beacon registration and stored by the coordinator.
109
+ *
110
+ * @param {object} pOperationDef - Operation definition:
111
+ * {
112
+ * Hash: 'video-ingest',
113
+ * Name: 'Video Ingest Pipeline',
114
+ * Description: '...',
115
+ * Graph: { Nodes: [...], Connections: [...] }
116
+ * }
117
+ * @returns {object} this (for chaining)
118
+ */
119
+ registerOperation(pOperationDef)
120
+ {
121
+ if (!pOperationDef || typeof(pOperationDef) !== 'object')
122
+ {
123
+ if (this.log)
124
+ {
125
+ this.log.warn('UltravisorBeacon: registerOperation requires a valid object.');
126
+ }
127
+ return this;
128
+ }
129
+
130
+ this._Operations.push(pOperationDef);
131
+
132
+ if (this.log)
133
+ {
134
+ this.log.info(`UltravisorBeacon: registered operation [${pOperationDef.Hash || 'auto'}] "${pOperationDef.Name || ''}"`);
135
+ }
136
+
137
+ return this;
138
+ }
139
+
97
140
  /**
98
141
  * Remove a previously registered capability.
99
142
  *
@@ -116,6 +159,51 @@ class UltravisorBeaconService extends libFableServiceBase
116
159
  return this._CapabilityManager.getCapabilityNames();
117
160
  }
118
161
 
162
+ /**
163
+ * Register a data context that this beacon exposes.
164
+ *
165
+ * Contexts define namespaces for the Universal Data Addressing scheme.
166
+ * For example, a retold-remote beacon might expose a 'File' context
167
+ * (content root) and a 'Cache' context (thumbnail cache).
168
+ *
169
+ * @param {string} pContextName - The context name (e.g. 'File', 'Cache', 'Staging').
170
+ * @param {object} pContextDef - Context definition:
171
+ * {
172
+ * BasePath: '/absolute/path', // Local filesystem path
173
+ * BaseURL: '/content/', // URL prefix for remote access
174
+ * Writable: true, // Whether remote writes are allowed
175
+ * Description: 'Content root' // Human-readable description
176
+ * }
177
+ * @returns {object} this (for chaining)
178
+ */
179
+ registerContext(pContextName, pContextDef)
180
+ {
181
+ this.options.Contexts[pContextName] = pContextDef;
182
+
183
+ // Also register with the address resolver for local resolution
184
+ if (pContextDef.BasePath)
185
+ {
186
+ this._AddressResolver.setLocalContextPath(pContextName, pContextDef.BasePath);
187
+ }
188
+
189
+ if (this.log)
190
+ {
191
+ this.log.info(`UltravisorBeacon: registered context [${pContextName}]`);
192
+ }
193
+
194
+ return this;
195
+ }
196
+
197
+ /**
198
+ * Get the address resolver instance.
199
+ *
200
+ * @returns {UltravisorBeaconAddressResolver}
201
+ */
202
+ getAddressResolver()
203
+ {
204
+ return this._AddressResolver;
205
+ }
206
+
119
207
  /**
120
208
  * Enable beacon mode: build providers, create thin client, connect.
121
209
  *
@@ -163,6 +251,8 @@ class UltravisorBeaconService extends libFableServiceBase
163
251
  MaxConcurrent: this.options.MaxConcurrent || 1,
164
252
  StagingPath: this.options.StagingPath || process.cwd(),
165
253
  Tags: this.options.Tags || {},
254
+ Contexts: this.options.Contexts || {},
255
+ Operations: this._Operations.length > 0 ? this._Operations : undefined,
166
256
  // Pass empty Providers array — we'll register adapters directly
167
257
  Providers: []
168
258
  });
@@ -196,6 +286,7 @@ class UltravisorBeaconService extends libFableServiceBase
196
286
  }
197
287
 
198
288
  this._Enabled = true;
289
+ this._AddressResolver.setLocalBeaconID(pBeacon.BeaconID);
199
290
 
200
291
  if (this.log)
201
292
  {
@@ -285,5 +376,6 @@ module.exports.CapabilityManager = libCapabilityManager;
285
376
  module.exports.CapabilityAdapter = require('./Ultravisor-Beacon-CapabilityAdapter.cjs');
286
377
  module.exports.CapabilityProvider = require('./Ultravisor-Beacon-CapabilityProvider.cjs');
287
378
  module.exports.ProviderRegistry = require('./Ultravisor-Beacon-ProviderRegistry.cjs');
379
+ module.exports.AddressResolver = libAddressResolver;
288
380
  module.exports.ConnectivityHTTP = libConnectivityHTTP;
289
381
  module.exports.ConnectivityWebSocket = libConnectivityWebSocket;