ultravisor 1.3.1 → 1.3.2

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",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Cyclic process execution with ai integration.",
5
5
  "main": "source/Ultravisor.cjs",
6
6
  "bin": {
@@ -57,7 +57,7 @@
57
57
  "cron": "^4.4.0",
58
58
  "meadow": "^2.0.37",
59
59
  "meadow-connection-sqlite": "^1.0.19",
60
- "meadow-migrationmanager": "^0.0.14",
60
+ "meadow-migrationmanager": "^0.0.15",
61
61
  "orator": "^6.1.1",
62
62
  "orator-authentication": "^1.0.1",
63
63
  "orator-serviceserver-restify": "^2.0.10",
@@ -71,7 +71,7 @@
71
71
  "devDependencies": {
72
72
  "pict-docuserve": "^0.1.5",
73
73
  "puppeteer": "^24.40.0",
74
- "quackage": "^1.2.0"
74
+ "quackage": "^1.2.2"
75
75
  },
76
76
  "mocha": {
77
77
  "diff": true,
@@ -1154,29 +1154,51 @@ class UltravisorBeaconCoordinator extends libPictService
1154
1154
  tmpDefaults.applyToWorkItem(tmpWorkItem, tmpSettings);
1155
1155
  }
1156
1156
 
1157
- // Check for affinity binding — pre-assign to a specific Beacon
1157
+ // Routing for AffinityKey:
1158
+ // 1) Name resolution — if AffinityKey matches a registered
1159
+ // beacon's Name, route directly to that beacon. This is
1160
+ // the explicit "send to this specific beacon" path the
1161
+ // data-platform initiative needs (many beacons of the
1162
+ // same capability, callers must designate which one).
1163
+ // 2) Sticky binding fallback — if no name match, behave like
1164
+ // a session-affinity key: the first item picks any
1165
+ // beacon, subsequent items with the same key go to the
1166
+ // same beacon (existing behavior).
1158
1167
  if (tmpWorkItem.AffinityKey)
1159
1168
  {
1160
- let tmpBinding = this._AffinityBindings[tmpWorkItem.AffinityKey];
1161
-
1162
- if (tmpBinding && this._Beacons[tmpBinding.BeaconID])
1169
+ let tmpNamedBeacon = this.findBeaconByName(tmpWorkItem.AffinityKey);
1170
+ if (tmpNamedBeacon)
1163
1171
  {
1164
- // Check if the binding has expired
1165
- if (new Date(tmpBinding.ExpiresAt) > new Date())
1172
+ tmpWorkItem.AssignedBeaconID = tmpNamedBeacon.BeaconID;
1173
+ tmpWorkItem.Status = 'Assigned';
1174
+ tmpWorkItem.ClaimedAt = new Date().toISOString();
1175
+ if (!tmpNamedBeacon.CurrentWorkItems) tmpNamedBeacon.CurrentWorkItems = [];
1176
+ tmpNamedBeacon.CurrentWorkItems.push(tmpWorkItemHash);
1177
+ this.log.info(`BeaconCoordinator: work item [${tmpWorkItemHash}] routed by name to beacon [${tmpNamedBeacon.BeaconID}] (Name=${tmpNamedBeacon.Name}) via AffinityKey [${tmpWorkItem.AffinityKey}].`);
1178
+ }
1179
+ else
1180
+ {
1181
+ let tmpBinding = this._AffinityBindings[tmpWorkItem.AffinityKey];
1182
+
1183
+ if (tmpBinding && this._Beacons[tmpBinding.BeaconID])
1166
1184
  {
1167
- tmpWorkItem.AssignedBeaconID = tmpBinding.BeaconID;
1168
- tmpWorkItem.Status = 'Assigned';
1169
- tmpWorkItem.ClaimedAt = new Date().toISOString();
1185
+ // Check if the binding has expired
1186
+ if (new Date(tmpBinding.ExpiresAt) > new Date())
1187
+ {
1188
+ tmpWorkItem.AssignedBeaconID = tmpBinding.BeaconID;
1189
+ tmpWorkItem.Status = 'Assigned';
1190
+ tmpWorkItem.ClaimedAt = new Date().toISOString();
1170
1191
 
1171
- let tmpBeacon = this._Beacons[tmpBinding.BeaconID];
1172
- tmpBeacon.CurrentWorkItems.push(tmpWorkItemHash);
1192
+ let tmpBeacon = this._Beacons[tmpBinding.BeaconID];
1193
+ tmpBeacon.CurrentWorkItems.push(tmpWorkItemHash);
1173
1194
 
1174
- this.log.info(`BeaconCoordinator: work item [${tmpWorkItemHash}] pre-assigned to beacon [${tmpBinding.BeaconID}] via affinity [${tmpWorkItem.AffinityKey}].`);
1175
- }
1176
- else
1177
- {
1178
- // Binding expired, clean it up
1179
- delete this._AffinityBindings[tmpWorkItem.AffinityKey];
1195
+ this.log.info(`BeaconCoordinator: work item [${tmpWorkItemHash}] pre-assigned to beacon [${tmpBinding.BeaconID}] via affinity [${tmpWorkItem.AffinityKey}].`);
1196
+ }
1197
+ else
1198
+ {
1199
+ // Binding expired, clean it up
1200
+ delete this._AffinityBindings[tmpWorkItem.AffinityKey];
1201
+ }
1180
1202
  }
1181
1203
  }
1182
1204
  }
@@ -227,6 +227,19 @@ class UltravisorBeaconScheduler extends libPictService
227
227
  return null;
228
228
  }
229
229
 
230
+ // Name-based routing: AffinityKey may designate a specific
231
+ // beacon by Name. Mirrors the resolution Coordinator does at
232
+ // enqueue time; we repeat it here so paths that bypass
233
+ // enqueueWorkItem (e.g. requeue) still honor it.
234
+ if (pItem.AffinityKey && typeof pCoordinator.findBeaconByName === 'function')
235
+ {
236
+ let tmpNamed = pCoordinator.findBeaconByName(pItem.AffinityKey);
237
+ if (tmpNamed)
238
+ {
239
+ return this._beaconCanTake(tmpNamed, pItem) ? tmpNamed : null;
240
+ }
241
+ }
242
+
230
243
  let tmpBestBeacon = null;
231
244
  let tmpBestLoad = Infinity;
232
245
  let tmpBeacons = pCoordinator._Beacons || {};
@@ -1108,26 +1108,35 @@ class UltravisorExecutionEngine extends libPictService
1108
1108
  fProcessNextDownstream();
1109
1109
  };
1110
1110
 
1111
- // Execute the task
1111
+ // Execute the task. Wrap the synchronous portion of the call in
1112
+ // try/catch — a misbehaving executor that throws synchronously
1113
+ // (e.g. simple-get's ERR_INVALID_ARG_TYPE on a non-string body)
1114
+ // would otherwise tear down the entire UV process. Treating the
1115
+ // throw as a task error fires the Error edge and lets the rest
1116
+ // of the operation graph clean up gracefully.
1112
1117
  this.log.info(`[Engine] executeTask: running task type="${tmpNode.Type}" node=${pNodeHash}`);
1113
- tmpTaskInstance.execute(tmpResolvedSettings, tmpTaskContext, (pError, pResult) =>
1118
+ let fHandleTaskError = (pError) =>
1114
1119
  {
1115
- if (pError)
1120
+ this.log.warn(`[Engine] executeTask: TASK ERROR node=${pNodeHash}: ${pError.message}`);
1121
+ this._log(pContext, `Task [${pNodeHash}] error: ${pError.message}`, 'error');
1122
+ if (tmpManifestService)
1116
1123
  {
1117
- this.log.warn(`[Engine] executeTask: TASK ERROR node=${pNodeHash}: ${pError.message}`);
1118
- this._log(pContext, `Task [${pNodeHash}] error: ${pError.message}`, 'error');
1119
- if (tmpManifestService)
1124
+ tmpManifestService.recordTaskError(pContext, pNodeHash, pError);
1125
+ tmpManifestService.recordEvent(pContext, pNodeHash, 'TaskError',
1126
+ `Error in [${pNodeHash}]: ${pError.message}`, 0);
1127
+ }
1128
+ this._enqueueDownstreamEvents(pNodeHash, 'Error', pContext);
1129
+ return fCallback(null);
1130
+ };
1131
+ try
1132
+ {
1133
+ tmpTaskInstance.execute(tmpResolvedSettings, tmpTaskContext, (pError, pResult) =>
1134
+ {
1135
+ if (pError)
1120
1136
  {
1121
- tmpManifestService.recordTaskError(pContext, pNodeHash, pError);
1122
- tmpManifestService.recordEvent(pContext, pNodeHash, 'TaskError',
1123
- `Error in [${pNodeHash}]: ${pError.message}`, 0);
1137
+ return fHandleTaskError(pError);
1124
1138
  }
1125
1139
 
1126
- // Fire error event if the task has one
1127
- this._enqueueDownstreamEvents(pNodeHash, 'Error', pContext);
1128
- return fCallback(null);
1129
- }
1130
-
1131
1140
  if (!pResult)
1132
1141
  {
1133
1142
  this._log(pContext, `Task [${pNodeHash}] returned no result.`, 'warn');
@@ -1257,6 +1266,11 @@ class UltravisorExecutionEngine extends libPictService
1257
1266
  return fCallback(null);
1258
1267
  },
1259
1268
  fFireIntermediateEvent);
1269
+ }
1270
+ catch (pSyncError)
1271
+ {
1272
+ return fHandleTaskError(pSyncError);
1273
+ }
1260
1274
  }
1261
1275
 
1262
1276
  // ====================================================================
@@ -305,16 +305,24 @@ module.exports =
305
305
 
306
306
  let tmpRequestOptions = { url: tmpURL, method: tmpMethod, headers: tmpHeaders, timeout: pResolvedSettings.TimeoutMs || 30000 };
307
307
 
308
- // Parse body for non-GET methods
308
+ // Body for non-GET methods. The HTTP client's `body` option
309
+ // expects a string (or Buffer / stream); object bodies cause
310
+ // `Buffer.byteLength(object)` to throw ERR_INVALID_ARG_TYPE
311
+ // inside simple-get and crash the engine. Pass the configured
312
+ // Body through verbatim — Content-Type tells the server how
313
+ // to interpret it. (Validating-parse the JSON for an early
314
+ // clearer error, but always serialize back to a string.)
309
315
  if (tmpMethod !== 'GET' && pResolvedSettings.Body)
310
316
  {
311
- try
317
+ if (typeof pResolvedSettings.Body === 'string')
312
318
  {
313
- tmpRequestOptions.body = JSON.parse(pResolvedSettings.Body);
319
+ try { JSON.parse(pResolvedSettings.Body); }
320
+ catch (pParseError) { /* ok — non-JSON bodies (e.g. form-encoded) pass through too */ }
321
+ tmpRequestOptions.body = pResolvedSettings.Body;
314
322
  }
315
- catch (pParseError)
323
+ else
316
324
  {
317
- tmpRequestOptions.body = pResolvedSettings.Body;
325
+ tmpRequestOptions.body = JSON.stringify(pResolvedSettings.Body);
318
326
  }
319
327
  }
320
328