ultravisor-beacon 0.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.
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Ultravisor Beacon Provider Registry
3
+ *
4
+ * Manages loaded capability providers, routes work items to the
5
+ * correct provider based on Capability:Action, and aggregates the
6
+ * capabilities list for beacon registration.
7
+ *
8
+ * Provider loading supports three sources:
9
+ * - Built-in name ('Shell', 'FileSystem') → resolves to ./providers/
10
+ * - Local file path ('./my-provider.cjs' or absolute) → require(resolved)
11
+ * - npm package name ('ultravisor-provider-ml') → require(name)
12
+ */
13
+
14
+ const libPath = require('path');
15
+
16
+ class UltravisorBeaconProviderRegistry
17
+ {
18
+ constructor()
19
+ {
20
+ // Map of 'Capability:Action' → { provider, actionDef }
21
+ this._ActionHandlers = {};
22
+
23
+ // Map of 'Capability' → { provider, defaultAction }
24
+ this._DefaultHandlers = {};
25
+
26
+ // All loaded providers by Name
27
+ this._Providers = {};
28
+
29
+ // Aggregate capabilities list (string[])
30
+ this._Capabilities = [];
31
+ }
32
+
33
+ /**
34
+ * Register a provider instance.
35
+ *
36
+ * @param {object} pProvider - Provider instance (extends CapabilityProvider or duck-types it)
37
+ * @returns {boolean} true if registered successfully
38
+ */
39
+ registerProvider(pProvider)
40
+ {
41
+ if (!pProvider || !pProvider.Capability)
42
+ {
43
+ console.error('[ProviderRegistry] Provider must have a Capability.');
44
+ return false;
45
+ }
46
+
47
+ let tmpActions = pProvider.actions || {};
48
+ let tmpActionNames = Object.keys(tmpActions);
49
+
50
+ if (tmpActionNames.length === 0)
51
+ {
52
+ console.warn(`[ProviderRegistry] Provider "${pProvider.Name}" declares no actions.`);
53
+ }
54
+
55
+ // Index each action by composite key
56
+ for (let i = 0; i < tmpActionNames.length; i++)
57
+ {
58
+ let tmpKey = pProvider.Capability + ':' + tmpActionNames[i];
59
+ this._ActionHandlers[tmpKey] = {
60
+ provider: pProvider,
61
+ actionDef: tmpActions[tmpActionNames[i]]
62
+ };
63
+ }
64
+
65
+ // First declared action is the default for capability-only routing
66
+ if (tmpActionNames.length > 0)
67
+ {
68
+ this._DefaultHandlers[pProvider.Capability] = {
69
+ provider: pProvider,
70
+ defaultAction: tmpActionNames[0]
71
+ };
72
+ }
73
+
74
+ // Update aggregate capabilities list
75
+ let tmpCapabilities = pProvider.getCapabilities();
76
+ for (let i = 0; i < tmpCapabilities.length; i++)
77
+ {
78
+ if (this._Capabilities.indexOf(tmpCapabilities[i]) === -1)
79
+ {
80
+ this._Capabilities.push(tmpCapabilities[i]);
81
+ }
82
+ }
83
+
84
+ this._Providers[pProvider.Name] = pProvider;
85
+
86
+ console.log(`[ProviderRegistry] Registered "${pProvider.Name}" → ` +
87
+ `${pProvider.Capability} [${tmpActionNames.join(', ')}]`);
88
+
89
+ return true;
90
+ }
91
+
92
+ /**
93
+ * Resolve a Capability+Action to a provider and action name.
94
+ *
95
+ * @param {string} pCapability - The capability to match
96
+ * @param {string} [pAction] - Optional action within the capability
97
+ * @returns {{ provider: object, action: string }|null}
98
+ */
99
+ resolve(pCapability, pAction)
100
+ {
101
+ // Try exact Capability:Action match first
102
+ if (pAction)
103
+ {
104
+ let tmpKey = pCapability + ':' + pAction;
105
+ let tmpHandler = this._ActionHandlers[tmpKey];
106
+ if (tmpHandler)
107
+ {
108
+ return { provider: tmpHandler.provider, action: pAction };
109
+ }
110
+ }
111
+
112
+ // Fall back to default action for the capability
113
+ let tmpDefault = this._DefaultHandlers[pCapability];
114
+ if (tmpDefault)
115
+ {
116
+ return { provider: tmpDefault.provider, action: tmpDefault.defaultAction };
117
+ }
118
+
119
+ return null;
120
+ }
121
+
122
+ /**
123
+ * Get the aggregate capabilities list for beacon registration.
124
+ *
125
+ * @returns {string[]}
126
+ */
127
+ getCapabilities()
128
+ {
129
+ return this._Capabilities.slice();
130
+ }
131
+
132
+ /**
133
+ * Get all loaded providers.
134
+ *
135
+ * @returns {object} Map of provider Name → instance
136
+ */
137
+ getProviders()
138
+ {
139
+ return this._Providers;
140
+ }
141
+
142
+ /**
143
+ * Load a provider from a source descriptor.
144
+ *
145
+ * @param {object} pDescriptor - { Source, Config }
146
+ * Source: 'Shell' (built-in), './my-provider.cjs' (local), 'npm-pkg' (npm)
147
+ * Config: per-provider config object (passed to constructor)
148
+ * @returns {boolean} true if loaded and registered successfully
149
+ */
150
+ loadProvider(pDescriptor)
151
+ {
152
+ let tmpSource = (pDescriptor && pDescriptor.Source) ? pDescriptor.Source : '';
153
+ let tmpConfig = (pDescriptor && pDescriptor.Config) ? pDescriptor.Config : {};
154
+
155
+ if (!tmpSource)
156
+ {
157
+ console.error('[ProviderRegistry] Provider descriptor must have a Source.');
158
+ return false;
159
+ }
160
+
161
+ let tmpProviderModule = null;
162
+
163
+ // Built-in providers
164
+ let tmpBuiltIns = {
165
+ 'Shell': libPath.join(__dirname, 'providers', 'Ultravisor-Beacon-Provider-Shell.cjs'),
166
+ 'FileSystem': libPath.join(__dirname, 'providers', 'Ultravisor-Beacon-Provider-FileSystem.cjs'),
167
+ 'LLM': libPath.join(__dirname, 'providers', 'Ultravisor-Beacon-Provider-LLM.cjs')
168
+ };
169
+
170
+ try
171
+ {
172
+ if (tmpBuiltIns[tmpSource])
173
+ {
174
+ tmpProviderModule = require(tmpBuiltIns[tmpSource]);
175
+ }
176
+ else if (tmpSource.startsWith('.') || tmpSource.startsWith('/'))
177
+ {
178
+ // Local file path — resolve relative to cwd
179
+ tmpProviderModule = require(libPath.resolve(tmpSource));
180
+ }
181
+ else
182
+ {
183
+ // npm package
184
+ tmpProviderModule = require(tmpSource);
185
+ }
186
+ }
187
+ catch (pError)
188
+ {
189
+ console.error(`[ProviderRegistry] Failed to load provider from "${tmpSource}": ${pError.message}`);
190
+ return false;
191
+ }
192
+
193
+ if (!tmpProviderModule)
194
+ {
195
+ console.error(`[ProviderRegistry] Could not load provider from: ${tmpSource}`);
196
+ return false;
197
+ }
198
+
199
+ // Support class exports, factory functions, and pre-instantiated singletons
200
+ let tmpProvider;
201
+
202
+ if (typeof tmpProviderModule === 'function' &&
203
+ tmpProviderModule.prototype &&
204
+ typeof tmpProviderModule.prototype.execute === 'function')
205
+ {
206
+ // Class with execute on prototype — instantiate it
207
+ tmpProvider = new tmpProviderModule(tmpConfig);
208
+ }
209
+ else if (typeof tmpProviderModule === 'function')
210
+ {
211
+ // Factory function
212
+ tmpProvider = tmpProviderModule(tmpConfig);
213
+ }
214
+ else if (typeof tmpProviderModule === 'object' &&
215
+ typeof tmpProviderModule.execute === 'function')
216
+ {
217
+ // Pre-instantiated singleton
218
+ tmpProvider = tmpProviderModule;
219
+ }
220
+ else
221
+ {
222
+ console.error(`[ProviderRegistry] Invalid provider export from "${tmpSource}": ` +
223
+ `must be a class, factory function, or object with execute().`);
224
+ return false;
225
+ }
226
+
227
+ return this.registerProvider(tmpProvider);
228
+ }
229
+
230
+ /**
231
+ * Load all providers from a config array.
232
+ *
233
+ * @param {Array<{ Source: string, Config?: object }>} pDescriptors
234
+ * @returns {number} count of successfully loaded providers
235
+ */
236
+ loadProviders(pDescriptors)
237
+ {
238
+ if (!Array.isArray(pDescriptors))
239
+ {
240
+ return 0;
241
+ }
242
+
243
+ let tmpCount = 0;
244
+
245
+ for (let i = 0; i < pDescriptors.length; i++)
246
+ {
247
+ if (this.loadProvider(pDescriptors[i]))
248
+ {
249
+ tmpCount++;
250
+ }
251
+ }
252
+
253
+ return tmpCount;
254
+ }
255
+
256
+ /**
257
+ * Initialize all loaded providers sequentially.
258
+ * Called before the beacon starts polling.
259
+ *
260
+ * @param {function} fCallback - function(pError)
261
+ */
262
+ initializeAll(fCallback)
263
+ {
264
+ let tmpProviderNames = Object.keys(this._Providers);
265
+ let tmpIndex = 0;
266
+
267
+ let fNext = (pError) =>
268
+ {
269
+ if (pError)
270
+ {
271
+ return fCallback(pError);
272
+ }
273
+
274
+ if (tmpIndex >= tmpProviderNames.length)
275
+ {
276
+ return fCallback(null);
277
+ }
278
+
279
+ let tmpProviderName = tmpProviderNames[tmpIndex++];
280
+ let tmpProvider = this._Providers[tmpProviderName];
281
+
282
+ if (typeof tmpProvider.initialize === 'function')
283
+ {
284
+ tmpProvider.initialize(fNext);
285
+ }
286
+ else
287
+ {
288
+ fNext(null);
289
+ }
290
+ };
291
+
292
+ fNext(null);
293
+ }
294
+
295
+ /**
296
+ * Shut down all loaded providers sequentially.
297
+ * Called when the beacon is stopping.
298
+ *
299
+ * @param {function} fCallback - function(pError)
300
+ */
301
+ shutdownAll(fCallback)
302
+ {
303
+ let tmpProviderNames = Object.keys(this._Providers);
304
+ let tmpIndex = 0;
305
+
306
+ let fNext = (pError) =>
307
+ {
308
+ if (tmpIndex >= tmpProviderNames.length)
309
+ {
310
+ return fCallback(pError || null);
311
+ }
312
+
313
+ let tmpProviderName = tmpProviderNames[tmpIndex++];
314
+ let tmpProvider = this._Providers[tmpProviderName];
315
+
316
+ if (typeof tmpProvider.shutdown === 'function')
317
+ {
318
+ tmpProvider.shutdown(fNext);
319
+ }
320
+ else
321
+ {
322
+ fNext(null);
323
+ }
324
+ };
325
+
326
+ fNext(null);
327
+ }
328
+ }
329
+
330
+ module.exports = UltravisorBeaconProviderRegistry;
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Ultravisor Beacon Service
3
+ *
4
+ * A Fable service that turns any Fable/Pict application into an
5
+ * Ultravisor beacon. Host applications register capabilities
6
+ * (with action handlers), then call enable() to connect to an
7
+ * Ultravisor server and begin accepting remote work items.
8
+ *
9
+ * Beacon mode is opt-in and disabled by default.
10
+ *
11
+ * This service composes three internal components:
12
+ * - CapabilityManager: stores registered capabilities
13
+ * - ConnectivityHTTP: HTTP transport configuration
14
+ * - BeaconClient (thin client): handles polling, auth, heartbeat
15
+ *
16
+ * Usage:
17
+ * let libBeaconService = require('ultravisor-beacon');
18
+ * pFable.addAndInstantiateServiceType('UltravisorBeacon', libBeaconService, {
19
+ * ServerURL: 'http://localhost:54321',
20
+ * Name: 'my-app-beacon'
21
+ * });
22
+ * let tmpBeacon = pFable.services.UltravisorBeacon;
23
+ * tmpBeacon.registerCapability({ Capability: 'MyApp', actions: { ... } });
24
+ * tmpBeacon.enable(function(pError) { ... });
25
+ */
26
+
27
+ const libFableServiceBase = require('fable-serviceproviderbase');
28
+
29
+ const libBeaconClient = require('./Ultravisor-Beacon-Client.cjs');
30
+ const libCapabilityManager = require('./Ultravisor-Beacon-CapabilityManager.cjs');
31
+ const libConnectivityHTTP = require('./Ultravisor-Beacon-ConnectivityHTTP.cjs');
32
+
33
+ class UltravisorBeaconService extends libFableServiceBase
34
+ {
35
+ constructor(pFable, pOptions, pServiceHash)
36
+ {
37
+ super(pFable, pOptions, pServiceHash);
38
+
39
+ this.serviceType = 'UltravisorBeacon';
40
+
41
+ // Merge defaults
42
+ this.options = Object.assign({
43
+ Enabled: false,
44
+ ServerURL: 'http://localhost:54321',
45
+ Name: '',
46
+ Password: '',
47
+ MaxConcurrent: 1,
48
+ PollIntervalMs: 5000,
49
+ HeartbeatIntervalMs: 30000,
50
+ StagingPath: '',
51
+ Tags: {},
52
+ Transport: 'HTTP'
53
+ }, this.options || {});
54
+
55
+ // Internal components
56
+ this._CapabilityManager = new libCapabilityManager();
57
+ this._ConnectivityService = new libConnectivityHTTP(this.options);
58
+ this._ThinClient = null;
59
+ this._Enabled = false;
60
+ }
61
+
62
+ // ================================================================
63
+ // Public API
64
+ // ================================================================
65
+
66
+ /**
67
+ * Register a capability from the host application.
68
+ *
69
+ * @param {object} pDescriptor - Capability descriptor:
70
+ * {
71
+ * Capability: 'ContentSystem',
72
+ * Name: 'ContentSystemProvider',
73
+ * actions: {
74
+ * 'ReadFile': {
75
+ * Description: 'Read a content file',
76
+ * SettingsSchema: [{ Name: 'FilePath', DataType: 'String', Required: true }],
77
+ * Handler: function(pWorkItem, pContext, fCallback, fReportProgress) { ... }
78
+ * }
79
+ * },
80
+ * initialize: function(fCallback) { ... }, // optional
81
+ * shutdown: function(fCallback) { ... } // optional
82
+ * }
83
+ * @returns {object} this (for chaining)
84
+ */
85
+ registerCapability(pDescriptor)
86
+ {
87
+ this._CapabilityManager.registerCapability(pDescriptor);
88
+
89
+ if (this.log)
90
+ {
91
+ this.log.info(`UltravisorBeacon: registered capability [${pDescriptor.Capability}]`);
92
+ }
93
+
94
+ return this;
95
+ }
96
+
97
+ /**
98
+ * Remove a previously registered capability.
99
+ *
100
+ * @param {string} pCapabilityName
101
+ * @returns {object} this (for chaining)
102
+ */
103
+ removeCapability(pCapabilityName)
104
+ {
105
+ this._CapabilityManager.removeCapability(pCapabilityName);
106
+ return this;
107
+ }
108
+
109
+ /**
110
+ * Get the list of registered capability names.
111
+ *
112
+ * @returns {string[]}
113
+ */
114
+ getCapabilityNames()
115
+ {
116
+ return this._CapabilityManager.getCapabilityNames();
117
+ }
118
+
119
+ /**
120
+ * Enable beacon mode: build providers, create thin client, connect.
121
+ *
122
+ * @param {function} fCallback - function(pError, pBeacon)
123
+ */
124
+ enable(fCallback)
125
+ {
126
+ if (this._Enabled)
127
+ {
128
+ if (this.log)
129
+ {
130
+ this.log.warn('UltravisorBeacon: already enabled.');
131
+ }
132
+ return fCallback(null);
133
+ }
134
+
135
+ // Determine beacon name
136
+ let tmpName = this.options.Name;
137
+ if (!tmpName && this.fable && this.fable.settings && this.fable.settings.Product)
138
+ {
139
+ tmpName = this.fable.settings.Product;
140
+ }
141
+ if (!tmpName)
142
+ {
143
+ tmpName = 'beacon-worker';
144
+ }
145
+
146
+ // Build adapter instances from registered capabilities
147
+ let tmpAdapters = this._CapabilityManager.buildProviderDescriptors();
148
+
149
+ if (tmpAdapters.length === 0)
150
+ {
151
+ if (this.log)
152
+ {
153
+ this.log.warn('UltravisorBeacon: no capabilities registered. Beacon will have no providers.');
154
+ }
155
+ }
156
+
157
+ // Get transport config
158
+ let tmpTransportConfig = this._ConnectivityService.getTransportConfig();
159
+
160
+ // Build thin client config
161
+ let tmpClientConfig = Object.assign({}, tmpTransportConfig, {
162
+ Name: tmpName,
163
+ MaxConcurrent: this.options.MaxConcurrent || 1,
164
+ StagingPath: this.options.StagingPath || process.cwd(),
165
+ Tags: this.options.Tags || {},
166
+ // Pass empty Providers array — we'll register adapters directly
167
+ Providers: []
168
+ });
169
+
170
+ // Create thin client
171
+ this._ThinClient = new libBeaconClient(tmpClientConfig);
172
+
173
+ // Register each adapter directly with the thin client's provider registry
174
+ for (let i = 0; i < tmpAdapters.length; i++)
175
+ {
176
+ this._ThinClient._Executor.providerRegistry.registerProvider(tmpAdapters[i]);
177
+ }
178
+
179
+ if (this.log)
180
+ {
181
+ this.log.info(`UltravisorBeacon: enabling beacon "${tmpName}" → ${tmpTransportConfig.ServerURL}`);
182
+ this.log.info(`UltravisorBeacon: capabilities: [${this._CapabilityManager.getCapabilityNames().join(', ')}]`);
183
+ }
184
+
185
+ // Start the thin client (authenticate, register, begin polling)
186
+ this._ThinClient.start((pError, pBeacon) =>
187
+ {
188
+ if (pError)
189
+ {
190
+ this._ThinClient = null;
191
+ if (this.log)
192
+ {
193
+ this.log.error(`UltravisorBeacon: enable failed: ${pError.message}`);
194
+ }
195
+ return fCallback(pError);
196
+ }
197
+
198
+ this._Enabled = true;
199
+
200
+ if (this.log)
201
+ {
202
+ this.log.info(`UltravisorBeacon: enabled as ${pBeacon.BeaconID}`);
203
+ }
204
+
205
+ return fCallback(null, pBeacon);
206
+ });
207
+ }
208
+
209
+ /**
210
+ * Disable beacon mode: stop polling, deregister, disconnect.
211
+ *
212
+ * @param {function} fCallback - function(pError)
213
+ */
214
+ disable(fCallback)
215
+ {
216
+ if (!this._Enabled || !this._ThinClient)
217
+ {
218
+ if (this.log)
219
+ {
220
+ this.log.warn('UltravisorBeacon: not enabled.');
221
+ }
222
+ return fCallback(null);
223
+ }
224
+
225
+ if (this.log)
226
+ {
227
+ this.log.info('UltravisorBeacon: disabling...');
228
+ }
229
+
230
+ this._ThinClient.stop((pError) =>
231
+ {
232
+ this._Enabled = false;
233
+ this._ThinClient = null;
234
+
235
+ if (pError && this.log)
236
+ {
237
+ this.log.warn(`UltravisorBeacon: disable warning: ${pError.message}`);
238
+ }
239
+ else if (this.log)
240
+ {
241
+ this.log.info('UltravisorBeacon: disabled.');
242
+ }
243
+
244
+ return fCallback(pError || null);
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Check if beacon mode is currently enabled.
250
+ *
251
+ * @returns {boolean}
252
+ */
253
+ isEnabled()
254
+ {
255
+ return this._Enabled;
256
+ }
257
+
258
+ /**
259
+ * Get the underlying thin client instance (for advanced usage).
260
+ * Returns null if beacon is not enabled.
261
+ *
262
+ * @returns {object|null}
263
+ */
264
+ getThinClient()
265
+ {
266
+ return this._ThinClient;
267
+ }
268
+
269
+ /**
270
+ * Get the capability manager instance.
271
+ *
272
+ * @returns {object}
273
+ */
274
+ getCapabilityManager()
275
+ {
276
+ return this._CapabilityManager;
277
+ }
278
+ }
279
+
280
+ module.exports = UltravisorBeaconService;
281
+
282
+ // Also export sub-components for direct usage
283
+ module.exports.BeaconClient = libBeaconClient;
284
+ module.exports.CapabilityManager = libCapabilityManager;
285
+ module.exports.CapabilityAdapter = require('./Ultravisor-Beacon-CapabilityAdapter.cjs');
286
+ module.exports.CapabilityProvider = require('./Ultravisor-Beacon-CapabilityProvider.cjs');
287
+ module.exports.ProviderRegistry = require('./Ultravisor-Beacon-ProviderRegistry.cjs');
288
+ module.exports.ConnectivityHTTP = libConnectivityHTTP;