This document describes the internal architecture and design patterns used in Fable.
Fable is built on a Service Provider Pattern architecture that provides dependency injection, service lifecycle management, and centralized configuration.
Services depend on Fable rather than creating their own dependencies. Each service receives a reference to the Fable instance during construction, providing access to all other services and shared configuration.
const libFable = require('fable');
const libFableServiceBase = require('fable-serviceproviderbase');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
class MyService extends libFableServiceBase {
constructor(pFable, pOptions, pServiceHash) {
super(pFable, pOptions, pServiceHash);
this.serviceType = 'MyService';
// Access other services through fable
this.log = this.fable.log;
this.settings = this.fable.settings;
}
}
fable.addAndInstantiateServiceType('MyService', MyService);
console.log('MyService wired up; log + settings accessible:',
typeof fable.MyService.log, typeof fable.MyService.settings);Services can be registered without being instantiated, allowing for on-demand creation when first needed:
const libFable = require('fable');
const libFableServiceBase = require('fable-serviceproviderbase');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
class ExpensiveServiceClass extends libFableServiceBase {
constructor(pFable, pOptions, pServiceHash) {
super(pFable, pOptions, pServiceHash);
this.serviceType = 'ExpensiveService';
console.log('ExpensiveServiceClass instantiated - work happens here.');
}
}
// Register without instantiating
fable.addServiceType('ExpensiveService', ExpensiveServiceClass);
console.log('Registered; no instance yet.');
// Later, when needed
const service = fable.instantiateServiceProvider('ExpensiveService');
console.log('Instantiated:', service.serviceType);Each service type maintains a map of instances, supporting multiple named instances of the same service type:
const libFable = require('fable');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
// Create multiple instances of the same service type
const clientA = fable.instantiateServiceProvider('RestClient', {}, 'api-client');
const clientB = fable.instantiateServiceProvider('RestClient', {}, 'auth-client');
// Access via services map
console.log('api-client:', typeof fable.servicesMap.RestClient['api-client']);
console.log('auth-client:', typeof fable.servicesMap.RestClient['auth-client']);The first instance of each service type becomes the default accessor on the Fable object:
const libFable = require('fable');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
// First instantiation becomes default
fable.instantiateServiceProvider('RestClient', {}, 'primary');
// Now accessible directly
console.log('fable.RestClient:', typeof fable.RestClient); // The 'primary' instance
console.log('fable.services.RestClient:', typeof fable.services.RestClient); // Same as aboveFable initializes in distinct phases to ensure proper dependency ordering:
The lowest level state and service infrastructure is established:
const libFable = require('fable');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
// Inside Fable's constructor, Phase 0 sets up its own service-manager state:
// this.serviceType = 'ServiceManager';
// this.serviceTypes = []; // Array of registered service types
// this.servicesMap = {}; // Map of instantiated services by type and hash
// this.services = {}; // Map of default service instances
// this.serviceClasses = {}; // Map of service class constructors
//
// You can observe these on a constructed instance:
console.log('serviceType:', fable.serviceType);
console.log('serviceTypes count:', fable.serviceTypes.length);
console.log('servicesMap keys:', Object.keys(fable.servicesMap).slice(0, 5), '...');
console.log('services keys:', Object.keys(fable.services).slice(0, 5), '...');Fundamental services required for Fable to operate:
const libFable = require('fable');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
// Inside Fable's constructor, Phase 1 wires up the core utility services:
// this.SettingsManager = new libFableSettings(pSettings);
// this.UUID = new libFableUUID(this.SettingsManager.settings);
// this.Logging = new libFableLog(this.SettingsManager.settings);
// this.Logging.initialize();
//
// On the constructed instance these are all live:
console.log('SettingsManager:', typeof fable.SettingsManager);
console.log('UUID:', typeof fable.UUID);
console.log('Logging:', typeof fable.Logging);Fable registers itself as a service, enabling consistent service access patterns:
const libFable = require('fable');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
// Inside Fable's constructor, Phase 1.5 self-registers:
// this.ServiceManager = this;
// this.connectFable(this);
//
// On the constructed instance, fable.ServiceManager === fable:
console.log('fable.ServiceManager === fable:', fable.ServiceManager === fable);All default services are registered and optionally instantiated:
const libFable = require('fable');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
// Inside Fable's constructor, Phase 2 registers all built-in services:
// // Auto-instantiated (available immediately)
// this.addAndInstantiateServiceType('EnvironmentData', ...);
// this.addAndInstantiateServiceType('Dates', ...);
// this.addAndInstantiateServiceType('DataFormat', ...);
// ...
// // On-demand (registered but not instantiated)
// this.addServiceType('Template', ...);
// this.addServiceType('RestClient', ...);
// ...
//
// On the constructed instance, you can see both:
const autoInstantiated = Object.keys(fable.services).filter(k => fable[k]);
console.log('Auto-instantiated services:', autoInstantiated.join(', '));
console.log('Registered service types:', fable.serviceTypes.join(', '));All Fable services extend CoreServiceProviderBase from fable-serviceproviderbase:
const libFable = require('fable');
const libFableServiceBase = require('fable-serviceproviderbase');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
class MyService extends libFableServiceBase {
constructor(pFable, pOptions, pServiceHash) {
super(pFable, pOptions, pServiceHash);
this.serviceType = 'MyService'; // Required: identifies the service type
}
// Services automatically have:
// - this.fable (reference to Fable instance)
// - this.log (logging via fable.log)
// - this.options (passed options)
// - this.Hash (unique service instance identifier)
}
fable.addAndInstantiateServiceType('MyService', MyService);
const svc = fable.MyService;
console.log('serviceType:', svc.serviceType);
console.log('fable ref:', typeof svc.fable);
console.log('log:', typeof svc.log);
console.log('options:', typeof svc.options);
console.log('Hash:', svc.Hash);Registers a service class without instantiation:
const libFable = require('fable');
const libFableServiceBase = require('fable-serviceproviderbase');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
class MyServiceClass extends libFableServiceBase {
constructor(pFable, pOptions, pServiceHash) { super(pFable, pOptions, pServiceHash); this.serviceType = 'MyService'; }
}
fable.addServiceType('MyService', MyServiceClass);
console.log('Registered:', fable.serviceTypes.includes('MyService'));Registers and immediately creates a default instance:
const libFable = require('fable');
const libFableServiceBase = require('fable-serviceproviderbase');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
class MyServiceClass extends libFableServiceBase {
constructor(pFable, pOptions, pServiceHash) { super(pFable, pOptions, pServiceHash); this.serviceType = 'MyService'; }
}
fable.addAndInstantiateServiceType('MyService', MyServiceClass);
// Creates instance with hash 'MyService-Default'
console.log('Default instance Hash:', fable.MyService.Hash);Creates a new instance of a registered service:
const libFable = require('fable');
const libFableServiceBase = require('fable-serviceproviderbase');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
class MyServiceClass extends libFableServiceBase {
constructor(pFable, pOptions, pServiceHash) { super(pFable, pOptions, pServiceHash); this.serviceType = 'MyService'; }
}
fable.addServiceType('MyService', MyServiceClass);
const service = fable.instantiateServiceProvider('MyService',
{ option: 'value' },
'my-custom-hash'
);
console.log('Service Hash:', service.Hash);
console.log('Service options:', service.options);instantiateServiceProviderFromPrototype(pServiceType, pOptions, pCustomServiceHash, pServicePrototype)
Creates an instance using a custom class that may differ from the registered class:
const libFable = require('fable');
const libFableServiceBase = require('fable-serviceproviderbase');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
class MyServiceClass extends libFableServiceBase {
constructor(pFable, pOptions, pServiceHash) { super(pFable, pOptions, pServiceHash); this.serviceType = 'MyService'; }
}
fable.addServiceType('MyService', MyServiceClass);
class CustomizedService extends MyServiceClass {
constructor(pFable, pOptions, pServiceHash) { super(pFable, pOptions, pServiceHash); this.flavor = 'custom'; }
}
const options = { mode: 'demo' };
const service = fable.instantiateServiceProviderFromPrototype(
'MyService',
options,
'customized',
CustomizedService
);
console.log('Custom flavor:', service.flavor);
console.log('Hash:', service.Hash);Auto-instantiated services are available directly on the Fable instance:
const libFable = require('fable');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
console.log(fable.Dates.dayJS().format('YYYY-MM-DD'));
console.log(fable.Math.addPrecise('1.5', '2.5'));
console.log(fable.DataFormat.formatterDollars(1234.56));All instantiated services are accessible through the services map:
const libFable = require('fable');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
console.log('fable.services.Dates:', typeof fable.services.Dates);
console.log("fable.servicesMap.Dates['Dates-Default']:", typeof fable.servicesMap.Dates['Dates-Default']);Some services provide convenient factory methods:
const libFable = require('fable');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
// Create a new Anticipate instance without registration
const anticipate = fable.newAnticipate();
console.log('anticipate:', typeof anticipate);
// Create a new Manifest instance without registration
const definition = { Scope: 'Demo', Descriptors: { Foo: { Hash: 'foo', Type: 'String' } } };
const manifest = fable.newManyfest(definition);
console.log('manifest scope:', manifest.scope);Settings flow from initialization through all services:
User Config -> SettingsManager -> Fable.settings -> All Services
↓
fable-settings
(precedent-based
config merging)
Services access configuration through:
const libFable = require('fable');
const fable = new libFable({ Product: 'ArchitectureDemo', SomeSetting: 'demo-value' });
// Inside a service, you access config via:
// this.fable.settings.SomeSetting
// this.fable.SettingsManager.settings
//
// At the top level, drop "this.":
console.log('settings.SomeSetting:', fable.settings.SomeSetting);
console.log('SettingsManager.settings type:', typeof fable.SettingsManager.settings);The logging system supports multiple output streams:
Application Code -> fable.log -> Log Router -> Stream 1 (console)
-> Stream 2 (file)
-> Stream 3 (MongoDB)
-> Stream N (custom)
Log levels: trace, debug, info, warn, error, fatal
Custom log providers can be created by extending LogProviderBase:
const libFable = require('fable');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
const LogProviderBase = require('fable-log').LogProviderBase;
class CustomLogProvider extends LogProviderBase {
write(pLogEntry) {
// Custom logging implementation
console.log('CustomLogProvider received:', pLogEntry.msg);
}
}
const provider = new CustomLogProvider(fable.Logging, { level: 'trace' });
provider.initialize();
fable.Logging.addLogger(provider);
fable.log.info('Hello from custom provider demo');Fable supports browser environments through service remapping:
// Shape of the browser remap declared in package.json:
const packageJSONBrowserField = {
"browser": {
"./source/service/Fable-Service-EnvironmentData.js":
"./source/service/Fable-Service-EnvironmentData-Web.js",
"./source/service/Fable-Service-FilePersistence.js":
"./source/service/Fable-Service-FilePersistence-Web.js"
}
};
console.log('Browser remap entries:', Object.keys(packageJSONBrowserField.browser).length);The dist/ folder contains browserified bundles for direct browser use.
const libFable = require('fable');
const libFableServiceBase = require('fable-serviceproviderbase');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
class MyCustomService extends libFableServiceBase {
constructor(pFable, pOptions, pServiceHash) {
super(pFable, pOptions, pServiceHash);
this.serviceType = 'MyCustomService';
}
myMethod() {
this.log.info('Doing something');
return this.fable.Math.addPrecise('1', '2');
}
}
// Register with Fable
fable.addAndInstantiateServiceType('MyCustomService', MyCustomService);
// Use it
console.log('myMethod() returns:', fable.MyCustomService.myMethod());For advanced scenarios, Fable supports an extra initialization callback:
const libFable = require('fable');
const libFableServiceBase = require('fable-serviceproviderbase');
const fable = new libFable({ Product: 'ArchitectureDemo', ProductVersion: '1.0.0' });
fable.extraServiceInitialization = (pService) => {
// Perform additional setup on every service
pService.customProperty = 'value';
return pService;
};
// Subsequent service instantiations now pass through the hook:
class DemoService extends libFableServiceBase {
constructor(pFable, pOptions, pServiceHash) { super(pFable, pOptions, pServiceHash); this.serviceType = 'DemoService'; }
}
fable.addAndInstantiateServiceType('DemoService', DemoService);
console.log('DemoService.customProperty:', fable.DemoService.customProperty);