@mobiusz/assemblage
Donate
Description
@mobiusz/assemblage
contains the Möb:üsz framework's plugins creator and manager.
It proposes an API to create Assemblages, that allow the whole system to quickly integrate new development and tools, including at runtime.
Assemblages are managed modules that make available all the particular elements they need to be integrated in a Möb:üsz ecosystem, both server-side and client-side.
For example, @mobiusz/user
exposes both the User Manager assemblage, that allows to manage, create, delete users in tthe database, and the GUI components to visualize and manipulate the system.
When integrating the assemblage, it's up to the final developper to decide how and when to display the components, imported from the @mobiusz/user
package itself.
@mobiusz/assemblage
exposes a singleton named Mobiusz
to manage all installed assemblages.
One advantage of this pattern is that it avoids duplication of handlers and that everything can be cleanly disposed when calling M.destroy()
or when the application quits unexpectedly.
Targets: node, browser, electron.
Warning
This module is still a work in progress, and API or implementations can change drastically from one version to another.
Use at your own risks!
Install
With yarn (recommended)
yarn add @mobiusz/assemblage
With npm
npm install @mobiusz/assemblage
Usage
Use an Assemblage
To use an assemblage, you have to install and import it where you'd like to use it, e.g. at the main entry point of your application.
yarn add @mobiusz/user
import { Mobiusz as M } from '@mobiusz/assemblage';
// DO THIS...
import { AUser } from '@mobiusz/user';
M.register(AUser) // Registers only this assemblage.
const userManager = new AUser();
await M.use(userManager, options);
// OR THIS...
M.register('@mobiusz/user'); // i.e. in your main entry point, registerrs all exported assemblages.
// Later, instantiate anywhere the assemblages exported by the package.
import { AUser, AAnotherAssemblageExported } from '@mobiusz/user';
const userManager = new AUser();
await M.use(userManager, options);
@mobiusz/assemblage
is at its very first step. The roadmap contains the ability to instantiate assemblages automatically - as they tend to be singletons - and get them with the already existing method M.require('com.example.my-assemblage-name');
As of July, 2022, you can get the assemblage already registered and instantiated elsewhere, by using this method.
const userManager = M.require('io.benoitlahoz.user');
userManager.create(/* options for the user to be created... */);
Messages
Mobiusz singleton object extends @mobiusz/mediator
main object. As its name suggest it's a mediator (an event handler) between registered assemblages.
Other assemblages and client can then listen to messages that are exposed by any registered assemblage, or by Mobiusz itself.
Mobiusz emits these messages:
-
AssemblageRegistered
: When an assemblage is registered by the manager, -
AssemblageInitialized
: When an assemblage has been initialized, -
AssemblageDisposed
: When an assemblage has been destroyed, -
AssemblageUnregistered
: When an assemblage has been unregistered from the manager.
Example
M.on(AssemblageMessages.AssemblageRegistered, ({ name: string, pkgName: string, }) => {
// i.e.: name = 'io.benoitlahoz.user' / pkgName = '@mobiusz/user'
})
User can easily trap messages touse them with another module (note that user is responsible for security):
M.on('*', (args:any[]) => {
// Do something with the event (i.e. pass it to socket.io)
})
Create an Assemblage
A custom library can export one or more assemblages and its objects.
Decorator
The 'assemblage factory' consists a typescript decorator that give all information about the assemblage. Then the factory creates a Proxy to be used by the manager or elsewhere.
The entity for the Assemblage decorator is the following:
export interface AssemblageStaticDescriptionModel {
pkgName: PackageName; // @mobiusz/user
name: AssemblageName; // 'io.benoitlahoz.user'
target?: 'node' | 'browser'; // or undefined
events?: MessageStringList; // An enum of messages to be registered for this assemblage.
dependencies?: PackageName[]; // A list of packages names the assemblage is dependent on.
components?: Record< // A list of package.json entry points for components, with their name and description, for runtime import.
PackageEntryPath,
{
name: NameString;
description: string;
}[]
>;
}
Example
export enum UserMessages {
// Creation.
UserCreate = 'user:create',
UserCreated = 'user:created',
// Invitation.
UserCheckInvitation = 'user:check.invitation',
UserRequestInvitation = 'user:request.invitation',
UserCheckRequest = 'user:check.request',
UserPasswordFromInvitation = 'user:password.from.invitation',
// Credentials.
UserLogin = 'user:login',
UserCheckToken = 'user:check.token',
UserChangePassword = 'user:change.password',
// Connection.
UserAuthenticated = 'user:authenticated',
// Getters.
UserGetAllButMe = 'user:get.all.but.me',
UserGetAll = 'user:get.all',
UserGet = 'user:get',
}
@Assemblage({
name: 'io.benoitlahoz.user',
pkgName: '@mobiusz/user',
target: 'node',
events: UserMessages,
dependencies: ['@mobiusz/db', '@mobiusz/mailer'],
})
export default class extends ManagedDocument implements AssemblageModel {
private _eventBus: any;
constructor() {
// This assemblage extends '@mobiusz/db' ManagedDocument class.
super();
}
public onRegister(): void {
// Nothing to do here for this assemblage. Hook is not mandatory.
}
// eventBus is an object passed by Mobiusz manager to communicate with other assemblages.
public onBeforeInit({ eventBus }, options?: any) {
this._eventBus = eventBus;
// ... omitted for brievity.
// Listens to manager emitting one of our assemblage's message.
this._eventBus.on(
UserMessages.UserCreate,
async (payload: UserCreateModel, callback?: Function) => {
// ...
}
);
// ...
}
public onDispose() {
super.dispose();
}
public async create(description: UserCreateModel): Promise<any> {
// ...
return success;
}
// ...
}
// Then in the main entry point of the library, so we can change implementation at any time while keeping the same name.
export { default as AUser } from './assemblage'
Lifecycle Hooks
The assemblages hooks are called in this order.
-
onRegister
: A static class hook called before instantiating an assemblage. It registers authorized messages. -
onBeforeInit
: Called before 'init' as its name suggest. Mobiusz passes theeventBus
and eventual options to this hook. -
onInit
oronAsyncInit
: Same as beforeInit but one step later. -
onBeforeDispose
: Application calledM.dispose([assemblage])
on the assemblage orM.destroy()
. Time to clean-up! -
onDispose
: Same as beforeDispose but one step later. After this call, assemblage will actually be disposed. -
onUnregister
: Unregister the assemblage and authorized messages.
Errors
UnregisteredAssemblageError
AssemblageInitializationError
NotAnAssemblageError
Tests
Tests are handled with Jest.
yarn test
Todo
- Manage order of declared dependencies between assemblages to order registration (already in place thanks to the package
toposort
but unused for the time being). (assemblage-manager.ts:231) - Call
beforeInit
for all assemblages before to callinit
. - Use
M.require
to get the assemblage's class instead of the instance, so we can 'use' it after call to require? - A CLI with a template to create an assemblage with everything in place (
@mobiusz/cli
). - Register and instantiate assemblages through a configuration file (everything in place yet).
- Public
unregister
method. - Make assemblages singletons by default?