@twin.org/engine-core 0.0.2-next.2 → 0.0.2-next.20

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.
@@ -1,9 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ var node_worker_threads = require('node:worker_threads');
3
4
  var core = require('@twin.org/core');
4
5
  var entity = require('@twin.org/entity');
5
6
  var loggingConnectorConsole = require('@twin.org/logging-connector-console');
6
7
  var loggingModels = require('@twin.org/logging-models');
8
+ var loggingService = require('@twin.org/logging-service');
7
9
  var modules = require('@twin.org/modules');
8
10
  var promises = require('node:fs/promises');
9
11
  var path = require('node:path');
@@ -69,13 +71,14 @@ class MemoryStateStorage {
69
71
  */
70
72
  class EngineCore {
71
73
  /**
72
- * Name for the engine logger.
74
+ * Name for the engine logger, used for direct console logging.
73
75
  */
74
- static LOGGER_TYPE_NAME = "engine";
76
+ static LOGGING_TYPE_NAME = "engine-logging-service";
75
77
  /**
76
78
  * Runtime name for the class.
79
+ * @internal
77
80
  */
78
- CLASS_NAME = "EngineCore";
81
+ static _CLASS_NAME = "EngineCore";
79
82
  /**
80
83
  * The core context.
81
84
  */
@@ -86,20 +89,15 @@ class EngineCore {
86
89
  */
87
90
  _stateStorage;
88
91
  /**
89
- * The logging connector for the engine.
92
+ * The logging component for the engine.
90
93
  * @internal
91
94
  */
92
- _engineLoggingConnector;
95
+ _engineLoggingComponent;
93
96
  /**
94
97
  * Skip the bootstrap process.
95
98
  * @internal
96
99
  */
97
100
  _skipBootstrap;
98
- /**
99
- * The logger type name to use.
100
- * @internal
101
- */
102
- _loggerTypeName;
103
101
  /**
104
102
  * The type initialisers.
105
103
  * @internal
@@ -110,6 +108,11 @@ class EngineCore {
110
108
  * @internal
111
109
  */
112
110
  _isStarted;
111
+ /**
112
+ * Is the engine a clone.
113
+ * @internal
114
+ */
115
+ _isClone;
113
116
  /**
114
117
  * Add type initialisers to the engine.
115
118
  * @internal
@@ -133,17 +136,17 @@ class EngineCore {
133
136
  this._skipBootstrap = options.skipBootstrap ?? false;
134
137
  this._populateTypeInitialisers = options.populateTypeInitialisers;
135
138
  this._customBootstrap = options.customBootstrap;
136
- this._loggerTypeName = options.loggerTypeName ?? EngineCore.LOGGER_TYPE_NAME;
137
139
  this._typeInitialisers = [];
138
140
  this._context = {
139
141
  config: options.config,
140
- defaultTypes: {},
142
+ registeredInstances: {},
141
143
  componentInstances: [],
142
- state: { componentStates: {} },
144
+ state: {},
143
145
  stateDirty: false
144
146
  };
145
147
  this._stateStorage = options.stateStorage;
146
148
  this._isStarted = false;
149
+ this._isClone = false;
147
150
  if (core.Is.function(this._populateTypeInitialisers)) {
148
151
  this._populateTypeInitialisers(this, this._context);
149
152
  }
@@ -151,19 +154,27 @@ class EngineCore {
151
154
  /**
152
155
  * Add a type initialiser.
153
156
  * @param type The type to add the initialiser for.
154
- * @param typeConfig The type config.
155
157
  * @param module The name of the module which contains the initialiser method.
156
158
  * @param method The name of the method to call.
157
159
  */
158
- addTypeInitialiser(type, typeConfig, module, method) {
159
- if (!core.Is.empty(typeConfig)) {
160
- this._typeInitialisers.push({
161
- type,
162
- typeConfig,
163
- module,
164
- method
165
- });
166
- }
160
+ addTypeInitialiser(type, module, method) {
161
+ core.Guards.stringValue(EngineCore._CLASS_NAME, "type", type);
162
+ core.Guards.stringValue(EngineCore._CLASS_NAME, "module", module);
163
+ core.Guards.stringValue(EngineCore._CLASS_NAME, "method", method);
164
+ this._typeInitialisers.push({
165
+ type,
166
+ module,
167
+ method
168
+ });
169
+ }
170
+ /**
171
+ * Get the type config for a specific type.
172
+ * @param type The type to get the config for.
173
+ * @returns The type config or undefined if not found.
174
+ */
175
+ getTypeConfig(type) {
176
+ core.Guards.stringValue(EngineCore._CLASS_NAME, "type", type);
177
+ return this._context.config.types?.[type];
167
178
  }
168
179
  /**
169
180
  * Start the engine core.
@@ -174,37 +185,30 @@ class EngineCore {
174
185
  return false;
175
186
  }
176
187
  this.setupEngineLogger();
177
- this.logInfo(core.I18n.formatMessage("engineCore.starting"));
188
+ this.logInfo(core.I18n.formatMessage(`${"engineCore"}.starting`));
178
189
  if (this._context.config.debug) {
179
- this.logInfo(core.I18n.formatMessage("engineCore.debuggingEnabled"));
190
+ this.logInfo(core.I18n.formatMessage(`${"engineCore"}.debuggingEnabled`));
180
191
  }
181
192
  let canContinue;
182
193
  try {
183
194
  canContinue = await this.stateLoad();
184
195
  if (canContinue) {
185
- for (const { type, typeConfig, module, method } of this._typeInitialisers) {
186
- await this.initialiseTypeConfig(type, typeConfig, module, method);
196
+ for (const { type, module, method } of this._typeInitialisers) {
197
+ await this.initialiseTypeConfig(type, module, method);
187
198
  }
188
199
  await this.bootstrap();
189
- this.logInfo(core.I18n.formatMessage("engineCore.componentsStarting"));
200
+ this.logInfo(core.I18n.formatMessage(`${"engineCore"}.componentsStarting`));
190
201
  for (const instance of this._context.componentInstances) {
191
202
  if (core.Is.function(instance.component.start)) {
192
- const instanceName = this.getInstanceName(instance);
193
- this.logInfo(core.I18n.formatMessage("engineCore.componentStarting", {
203
+ this.logInfo(core.I18n.formatMessage(`${"engineCore"}.componentStarting`, {
194
204
  element: instance.instanceType
195
205
  }));
196
- const componentState = this._context.state.componentStates[instanceName] ?? {};
197
- const lastState = core.ObjectHelper.clone(componentState);
198
- await instance.component.start(this._context.state.nodeIdentity, this._loggerTypeName, componentState);
199
- if (!core.ObjectHelper.equal(lastState, componentState)) {
200
- this._context.state.componentStates[instanceName] = componentState;
201
- this._context.stateDirty = true;
202
- }
206
+ await instance.component.start(this._context.state.nodeIdentity, EngineCore.LOGGING_TYPE_NAME);
203
207
  }
204
208
  }
205
- this.logInfo(core.I18n.formatMessage("engineCore.componentsComplete"));
209
+ this.logInfo(core.I18n.formatMessage(`${"engineCore"}.componentsComplete`));
206
210
  }
207
- this.logInfo(core.I18n.formatMessage("engineCore.started"));
211
+ this.logInfo(core.I18n.formatMessage(`${"engineCore"}.started`));
208
212
  this._isStarted = true;
209
213
  }
210
214
  catch (err) {
@@ -223,39 +227,55 @@ class EngineCore {
223
227
  * @returns Nothing.
224
228
  */
225
229
  async stop() {
226
- this.logInfo(core.I18n.formatMessage("engineCore.stopping"));
227
- this.logInfo(core.I18n.formatMessage("engineCore.componentsStopping"));
230
+ this.logInfo(core.I18n.formatMessage(`${"engineCore"}.stopping`));
231
+ this.logInfo(core.I18n.formatMessage(`${"engineCore"}.componentsStopping`));
228
232
  for (const instance of this._context.componentInstances) {
229
233
  if (core.Is.function(instance.component.stop)) {
230
- const instanceName = this.getInstanceName(instance);
231
- const componentState = this._context.state.componentStates[instanceName] ?? {};
232
- const lastState = core.ObjectHelper.clone(componentState);
233
- this.logInfo(core.I18n.formatMessage("engineCore.componentStopping", { element: instance.instanceType }));
234
+ this.logInfo(core.I18n.formatMessage(`${"engineCore"}.componentStopping`, {
235
+ element: instance.instanceType
236
+ }));
234
237
  try {
235
- await instance.component.stop(this._context.state.nodeIdentity, this._loggerTypeName, componentState);
236
- if (!core.ObjectHelper.equal(lastState, componentState)) {
237
- this._context.state.componentStates[instanceName] = componentState;
238
- this._context.stateDirty = true;
239
- }
238
+ await instance.component.stop(this._context.state.nodeIdentity, EngineCore.LOGGING_TYPE_NAME);
240
239
  }
241
240
  catch (err) {
242
- this.logError(new core.GeneralError(this.CLASS_NAME, "componentStopFailed", {
241
+ this.logError(new core.GeneralError(EngineCore._CLASS_NAME, "componentStopFailed", {
243
242
  component: instance.instanceType
244
243
  }, core.BaseError.fromError(err)));
245
244
  }
246
245
  }
247
246
  }
248
247
  await this.stateSave();
249
- this.logInfo(core.I18n.formatMessage("engineCore.componentsStopped"));
250
- this.logInfo(core.I18n.formatMessage("engineCore.stopped"));
248
+ this.logInfo(core.I18n.formatMessage(`${"engineCore"}.componentsStopped`));
249
+ this.logInfo(core.I18n.formatMessage(`${"engineCore"}.stopped`));
250
+ }
251
+ /**
252
+ * Is the engine started.
253
+ * @returns True if the engine is started.
254
+ */
255
+ isStarted() {
256
+ return this._isStarted;
257
+ }
258
+ /**
259
+ * Is this the primary engine instance.
260
+ * @returns True if the engine is the primary instance.
261
+ */
262
+ isPrimary() {
263
+ return node_worker_threads.isMainThread && !this._isClone;
264
+ }
265
+ /**
266
+ * Is this engine instance a clone.
267
+ * @returns True if the engine instance is a clone.
268
+ */
269
+ isClone() {
270
+ return this._isClone;
251
271
  }
252
272
  /**
253
273
  * Log info.
254
274
  * @param message The message to log.
255
275
  */
256
276
  logInfo(message) {
257
- this._engineLoggingConnector?.log({
258
- source: this.CLASS_NAME,
277
+ this._engineLoggingComponent?.log({
278
+ source: EngineCore._CLASS_NAME,
259
279
  level: "info",
260
280
  message
261
281
  });
@@ -273,8 +293,8 @@ class EngineCore {
273
293
  if (this._context.config.debug && core.Is.stringValue(formattedError.stack)) {
274
294
  message += `\n${formattedError.stack}`;
275
295
  }
276
- this._engineLoggingConnector?.log({
277
- source: this.CLASS_NAME,
296
+ this._engineLoggingComponent?.log({
297
+ source: EngineCore._CLASS_NAME,
278
298
  level: "error",
279
299
  message
280
300
  });
@@ -295,11 +315,48 @@ class EngineCore {
295
315
  return this._context.state;
296
316
  }
297
317
  /**
298
- * Get the types for the component.
299
- * @returns The default types.
318
+ * Get all the registered instances.
319
+ * @returns The registered instances.
300
320
  */
301
- getDefaultTypes() {
302
- return this._context.defaultTypes;
321
+ getRegisteredInstances() {
322
+ return this._context.registeredInstances;
323
+ }
324
+ /**
325
+ * Get the registered instance type for the component/connector.
326
+ * @param componentConnectorType The type of the component/connector.
327
+ * @param features The requested features of the component, if not specified the default entry will be retrieved.
328
+ * @returns The instance type matching the criteria if one is registered.
329
+ * @throws If a matching instance was not found.
330
+ */
331
+ getRegisteredInstanceType(componentConnectorType, features) {
332
+ core.Guards.stringValue(EngineCore._CLASS_NAME, "componentConnectorType", componentConnectorType);
333
+ const registeredType = this.getRegisteredInstanceTypeOptional(componentConnectorType, features);
334
+ if (!core.Is.stringValue(registeredType)) {
335
+ throw new core.GeneralError(EngineCore._CLASS_NAME, "instanceTypeNotFound", {
336
+ type: componentConnectorType,
337
+ features: (features ?? ["default"]).join(",")
338
+ });
339
+ }
340
+ return registeredType;
341
+ }
342
+ /**
343
+ * Get the registered instance type for the component/connector if it exists.
344
+ * @param componentConnectorType The type of the component/connector.
345
+ * @param features The requested features of the component, if not specified the default entry will be retrieved.
346
+ * @returns The instance type matching the criteria if one is registered.
347
+ */
348
+ getRegisteredInstanceTypeOptional(componentConnectorType, features) {
349
+ let registeredType;
350
+ const registeredTypes = this._context.registeredInstances[componentConnectorType];
351
+ if (core.Is.arrayValue(registeredTypes)) {
352
+ if (core.Is.arrayValue(features)) {
353
+ registeredType = registeredTypes.find(t => t.features?.every(f => features.includes(f)))?.type;
354
+ }
355
+ else {
356
+ registeredType = registeredTypes[0]?.type;
357
+ }
358
+ }
359
+ return registeredType;
303
360
  }
304
361
  /**
305
362
  * Get the data required to create a clone of the engine.
@@ -315,8 +372,7 @@ class EngineCore {
315
372
  config: this._context.config,
316
373
  state: this._context.state,
317
374
  typeInitialisers: this._typeInitialisers,
318
- entitySchemas,
319
- loggerTypeName: this._loggerTypeName
375
+ entitySchemas
320
376
  };
321
377
  return cloneData;
322
378
  }
@@ -326,20 +382,20 @@ class EngineCore {
326
382
  * @param silent Should the clone be silent.
327
383
  */
328
384
  populateClone(cloneData, silent) {
329
- core.Guards.object(this.CLASS_NAME, "cloneData", cloneData);
330
- core.Guards.object(this.CLASS_NAME, "cloneData.config", cloneData.config);
331
- core.Guards.object(this.CLASS_NAME, "cloneData.state", cloneData.state);
332
- core.Guards.array(this.CLASS_NAME, "cloneData.typeInitialisers", cloneData.typeInitialisers);
333
- this._loggerTypeName = cloneData.loggerTypeName;
385
+ core.Guards.object(EngineCore._CLASS_NAME, "cloneData", cloneData);
386
+ core.Guards.object(EngineCore._CLASS_NAME, "cloneData.config", cloneData.config);
387
+ core.Guards.object(EngineCore._CLASS_NAME, "cloneData.state", cloneData.state);
388
+ core.Guards.array(EngineCore._CLASS_NAME, "cloneData.typeInitialisers", cloneData.typeInitialisers);
334
389
  this._skipBootstrap = true;
390
+ this._isClone = true;
335
391
  if (silent ?? false) {
336
392
  cloneData.config.silent = true;
337
393
  }
338
394
  this._context = {
339
395
  config: cloneData.config,
340
- defaultTypes: {},
396
+ registeredInstances: {},
341
397
  componentInstances: [],
342
- state: { componentStates: {} },
398
+ state: {},
343
399
  stateDirty: false
344
400
  };
345
401
  this._typeInitialisers = cloneData.typeInitialisers;
@@ -355,14 +411,41 @@ class EngineCore {
355
411
  * @param instanceMethod The function to initialise the instance.
356
412
  * @internal
357
413
  */
358
- async initialiseTypeConfig(typeKey, typeConfig, module, method) {
414
+ async initialiseTypeConfig(typeKey, module, method) {
415
+ const typeConfig = this._context.config.types?.[typeKey];
359
416
  if (core.Is.arrayValue(typeConfig)) {
360
417
  const instanceMethod = await modules.ModuleHelper.getModuleEntry(module, method);
361
418
  for (let i = 0; i < typeConfig.length; i++) {
362
- const instanceType = instanceMethod(this, this._context, typeConfig[i], typeConfig[i].overrideInstanceType);
363
- if (core.Is.stringValue(instanceType) &&
364
- (core.Is.empty(this._context.defaultTypes[typeKey]) || typeConfig[i].isDefault)) {
365
- this._context.defaultTypes[typeKey] = instanceType;
419
+ this.logInfo(core.I18n.formatMessage("engineCore.configuring", {
420
+ element: `${typeKey}: ${typeConfig[i].type}`
421
+ }));
422
+ const result = await instanceMethod(this, this._context, typeConfig[i]);
423
+ if (core.Is.stringValue(result.instanceType) && core.Is.object(result.component)) {
424
+ const finalInstanceType = typeConfig[i].overrideInstanceType ?? result.instanceType;
425
+ this._context.componentInstances.push({
426
+ instanceType: finalInstanceType,
427
+ component: result.component
428
+ });
429
+ result.factory?.register(finalInstanceType, () => result.component);
430
+ this._context.registeredInstances[typeKey] ??= [];
431
+ if (typeConfig[i].isDefault ?? false) {
432
+ this._context.registeredInstances[typeKey].unshift({
433
+ type: finalInstanceType,
434
+ features: typeConfig[i].features
435
+ });
436
+ }
437
+ else {
438
+ this._context.registeredInstances[typeKey].push({
439
+ type: finalInstanceType,
440
+ features: typeConfig[i].features
441
+ });
442
+ }
443
+ }
444
+ else {
445
+ throw new core.GeneralError("engineCore", "componentUnknownType", {
446
+ type: typeConfig[i].type,
447
+ componentType: typeKey
448
+ });
366
449
  }
367
450
  }
368
451
  }
@@ -373,7 +456,7 @@ class EngineCore {
373
456
  */
374
457
  setupEngineLogger() {
375
458
  const silent = this._context.config.silent ?? false;
376
- const engineLogger = silent
459
+ const engineLoggerConnector = silent
377
460
  ? new loggingModels.SilentLoggingConnector()
378
461
  : new loggingConnectorConsole.ConsoleLoggingConnector({
379
462
  config: {
@@ -382,12 +465,25 @@ class EngineCore {
382
465
  }
383
466
  });
384
467
  this._context.componentInstances.push({
385
- instanceType: this._loggerTypeName,
386
- component: engineLogger
468
+ instanceType: EngineCore.LOGGING_TYPE_NAME,
469
+ component: engineLoggerConnector
387
470
  });
388
- loggingModels.LoggingConnectorFactory.register(this._loggerTypeName, () => engineLogger);
389
- this._engineLoggingConnector = engineLogger;
390
- this._context.defaultTypes.loggingConnector = this._loggerTypeName;
471
+ loggingModels.LoggingConnectorFactory.register(EngineCore.LOGGING_TYPE_NAME, () => engineLoggerConnector);
472
+ this._context.registeredInstances.loggingConnector = [
473
+ {
474
+ type: EngineCore.LOGGING_TYPE_NAME
475
+ }
476
+ ];
477
+ const engineLoggerComponent = new loggingService.LoggingService({
478
+ loggingConnectorType: EngineCore.LOGGING_TYPE_NAME
479
+ });
480
+ this._engineLoggingComponent = engineLoggerComponent;
481
+ core.ComponentFactory.register(EngineCore.LOGGING_TYPE_NAME, () => engineLoggerComponent);
482
+ this._context.registeredInstances.loggingComponent = [
483
+ {
484
+ type: EngineCore.LOGGING_TYPE_NAME
485
+ }
486
+ ];
391
487
  }
392
488
  /**
393
489
  * Load the state.
@@ -397,10 +493,7 @@ class EngineCore {
397
493
  async stateLoad() {
398
494
  if (this._stateStorage) {
399
495
  try {
400
- this._context.state = ((await this._stateStorage.load(this)) ?? {
401
- componentStates: {}
402
- });
403
- this._context.state.componentStates ??= {};
496
+ this._context.state = ((await this._stateStorage.load(this)) ?? {});
404
497
  this._context.stateDirty = false;
405
498
  return true;
406
499
  }
@@ -436,34 +529,28 @@ class EngineCore {
436
529
  */
437
530
  async bootstrap() {
438
531
  if (!this._skipBootstrap) {
439
- this.logInfo(core.I18n.formatMessage("engineCore.bootstrapStarted"));
532
+ this.logInfo(core.I18n.formatMessage(`${"engineCore"}.bootstrapStarted`));
440
533
  // First bootstrap the components.
441
534
  for (const instance of this._context.componentInstances) {
442
535
  if (core.Is.function(instance.component.bootstrap)) {
443
536
  const instanceName = this.getInstanceName(instance);
444
- this.logInfo(core.I18n.formatMessage("engineCore.bootstrapping", {
537
+ this.logInfo(core.I18n.formatMessage(`${"engineCore"}.bootstrapping`, {
445
538
  element: instanceName
446
539
  }));
447
- const componentState = this._context.state.componentStates[instanceName] ?? {};
448
- const lastState = core.ObjectHelper.clone(componentState);
449
- const bootstrapSuccess = await instance.component.bootstrap(this._loggerTypeName, componentState);
540
+ const bootstrapSuccess = await instance.component.bootstrap(EngineCore.LOGGING_TYPE_NAME);
450
541
  // If the bootstrap method failed then throw an error
451
542
  if (!bootstrapSuccess) {
452
- throw new core.GeneralError(this.CLASS_NAME, "bootstrapFailed", {
543
+ throw new core.GeneralError(EngineCore._CLASS_NAME, "bootstrapFailed", {
453
544
  component: `${instance.component.CLASS_NAME}:${instance.instanceType}`
454
545
  });
455
546
  }
456
- if (!core.ObjectHelper.equal(lastState, componentState)) {
457
- this._context.state.componentStates[instanceName] = componentState;
458
- this._context.stateDirty = true;
459
- }
460
547
  }
461
548
  }
462
549
  // Now perform any custom bootstrap operations
463
550
  if (core.Is.function(this._customBootstrap)) {
464
551
  await this._customBootstrap(this, this._context);
465
552
  }
466
- this.logInfo(core.I18n.formatMessage("engineCore.bootstrapComplete"));
553
+ this.logInfo(core.I18n.formatMessage(`${"engineCore"}.bootstrapComplete`));
467
554
  }
468
555
  }
469
556
  /**
@@ -568,6 +655,51 @@ class FileStateStorage {
568
655
  }
569
656
  }
570
657
 
658
+ // Copyright 2024 IOTA Stiftung.
659
+ // SPDX-License-Identifier: Apache-2.0.
660
+ /**
661
+ * Helper class for engine modules.
662
+ */
663
+ class EngineModuleHelper {
664
+ /**
665
+ * Runtime name for the class.
666
+ */
667
+ static CLASS_NAME = "EngineModuleHelper";
668
+ /**
669
+ * Loads an engine component and constructs it with the relevant dependencies and configuration.
670
+ * @param engineCore The engine core.
671
+ * @param engineModuleConfig The configuration for the module.
672
+ * @returns The instantiated component.
673
+ */
674
+ static async loadComponent(engineCore, engineModuleConfig) {
675
+ const moduleClass = await modules.ModuleHelper.getModuleEntry(engineModuleConfig.moduleName, engineModuleConfig.className);
676
+ const isClass = core.Is.class(moduleClass);
677
+ if (!isClass) {
678
+ throw new core.GeneralError(EngineModuleHelper.CLASS_NAME, "moduleNotClass", {
679
+ moduleName: engineModuleConfig.moduleName,
680
+ className: engineModuleConfig.className
681
+ });
682
+ }
683
+ const constructorOptions = {};
684
+ if (core.Is.arrayValue(engineModuleConfig.dependencies)) {
685
+ for (const dependency of engineModuleConfig.dependencies) {
686
+ if (dependency.isOptional ?? false) {
687
+ constructorOptions[dependency.propertyName] = engineCore.getRegisteredInstanceType(dependency.componentName, dependency.features);
688
+ }
689
+ else {
690
+ constructorOptions[dependency.propertyName] =
691
+ engineCore.getRegisteredInstanceTypeOptional(dependency.componentName, dependency.features);
692
+ }
693
+ }
694
+ }
695
+ if (core.Is.object(engineModuleConfig.config)) {
696
+ constructorOptions.config = engineModuleConfig.config;
697
+ }
698
+ return new moduleClass(constructorOptions);
699
+ }
700
+ }
701
+
571
702
  exports.EngineCore = EngineCore;
703
+ exports.EngineModuleHelper = EngineModuleHelper;
572
704
  exports.FileStateStorage = FileStateStorage;
573
705
  exports.MemoryStateStorage = MemoryStateStorage;