0.1.0-alpha.2 • Published 1 year ago

@mobiusz/device v0.1.0-alpha.2

Weekly downloads
-
License
GPL-3.0+
Repository
github
Last release
1 year ago

@mobiusz/assemblage

lerna made

Donate

paypal

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 the eventBus and eventual options to this hook.
  • onInit or onAsyncInit: Same as beforeInit but one step later.
  • onBeforeDispose: Application called M.dispose([assemblage]) on the assemblage or M.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 toposortbut unused for the time being). (assemblage-manager.ts:231)
  • Call beforeInit for all assemblages before to call init.
  • 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?

Contributors

Author

Benoît Lahoz

License

GPL-3.0+