Alar
Alar is a light-weight framework that provides applications the ability to
auto-load and hot-reload modules, as well as the ability to serve instances
remotely as RPC services.
Prerequisites
Auto-loading and Hot-reloading
In NodeJS (with CommonJS module solution), require
and import
will
immediately load the corresponding module and make a reference "copy" in the
current scope. Which means, if the module doesn't finish initiation, e.g.
circular import, the application may not work as expected, and if the module
file is modified, the application won't be able to reload that module without
restart the program.
Alar, on the other hand, based on namespace and ES6 proxy, it creates a
weak-reference of the module, and only import the module when needed. And since
it's weak-referenced, it will not make any copy to the module, and when the
module file is changed, it can wipe out the memory cache and reload the module
with very few side-effects.
How to use?
In order to use Alar, one must create a root ModuleProxy
instance, and assign
it to the global namespace, so other files can directly use it as a root
namespace without import and share the benefits of declaration merging (in
TypeScript vernacular, if not using is, just ignore any tips and code of
declaration merging that may be discussed).
Example
import { ModuleProxy } from "alar";
declare global {
namespace app { }
}
export const App = global["app"] = new ModuleProxy("app", __dirname);
App.watch();
In other files, just define and export a default class, and merge the type to
the namespace app
, so that another file can access it directly as namespace.
(NOTE: Alar offers first priority of the default
export, if a module doesn't
have default export, Alar will try to load the entire exports object instead.)
declare global {
namespace app {
const bootstrap: ModuleProxy<Bootstrap>
}
}
export default class Bootstrap {
init() {
}
}
declare global {
namespace app {
namespace service {
const user: ModuleProxy<typeof User>
}
}
}
export default class User {
constructor(private name: string) { }
getName() {
return this.name;
}
}
And other files can access to the modules via the namespace:
import "./app";
app.bootstrap.instance().init();
app.bootstrap().init();
var user = app.service.user.create("Mr. Handsome");
console.log(user.getName());
Prototype Module
Any module that exports an object as default will be considered as a prototype
module, when calling create()
of that module, the object will be used as a
prototype (since v4.0.4, a deep clone will be used instead, if an argument is
passed, it will be merged to the new object). However when calling instance()
of that module, the original object itself will be used as the singleton.
declare global {
namespace app {
const config: ModuleProxy<Config>;
}
}
export interface Config {
}
export default <Config>{
}
Remote Service
Alar allows user to easily serve the module remotely, whether in another
process or in another machine.
Example
Say I want to serve the user service in a different process and communicate via
IPC channel, I just have to do this:
declare global {
namespace app {
namespace service {
const user: ModuleProxy<typeof User>
}
}
}
export default class User {
constructor(private name?: string) {}
async getName() {
return this.name;
}
static getInstance() {
return new this("Mr. Handsome");
}
}
import { App } from "./app";
(async () => {
let service = await App.serve("/tmp/my-app/remote-service.sock");
service.register(app.service.user);
console.log("Service started!");
})();
Just try ts-node --files src/remote-service
(or node dist/remote-service
),
and the service will be started immediately.
And in index.ts, connect to the service before using remote functions:
import { App } from "./app";
(async () => {
let service = await App.connect("/tmp/my-app/remote-service.sock");
service.register(app.service.user);
console.log(await app.service.user.instance().getName());
})();
Hot-reloading in Remote Service
The local watcher may notice the local file has changed and try to reload the
local module (and the local singleton), however, it will not affect any remote
instances, that said, the instance served remotely can still be watched and
reloaded on the remote server individually.
In the above example, since the remote-service
module imports app
module as
well, which starts the watcher, when the user
module is changed, the
remote-service
will reload the module as expected, and the index
calls it
remotely will get the new result as expected.
Generator Support
Since version 3.3, Alar supports generators (and async generators) in both local
call and remote call contexts.
declare global {
namespace app {
namespace service {
const user: ModuleProxy<User>
}
}
}
export default class User {
async *getFriends() {
yield "Jane";
yield "Ben";
yield "Albert";
return "We are buddies";
}
}
(async () => {
let generator = app.service.user.instance().getFriends();
for await (let name of generator) {
console.log(name);
}
console.log(await generator);
let generator2 = app.service.user.instance().getFriends();
while (true) {
let { value, done } = await generator2.next();
console.log(value);
if (done) {
break;
}
}
})();
Dependency Injection
Since 3.5.0, Alar add a new method inject(route?: any)
to allow you setting up
dependency for a specific class in a handy way, check this example:
class Article {
@app.service.user.inject()
protected user: User;
getAuthorName(): Promise<string> {
return this.user.getName();
}
}
(async () => {
var article = new Article;
console.log(await article.getAuthorName());
})();
Be noticed that this method differs from the usage of assigning the instance to
an variable, if you use the syntax below to add the instance to a class property,
it will break the hot-reloading feature that Alar provided.
class Article {
protected user = app.services.user.instance();
}
This syntax will make a strong reference to the user module, which will not
allow the program to refer to the new instance after reloading the user module.
But, when using the inject()
method, which ships with instance()
under the
hood, the hot-reloading model will still work fine.
However, Alar doesn't provide a way to inject new instances dynamically, since
every new instance will create a strong reference itself, that makes that kind
of injection less useful.
Life Cycle Support
Since 5.0, Alar now supports life cycle functions, if a service class contains
an init()
method, it will be used to perform asynchronous initiation, for
example, connecting to a database. And if it contains a destroy()
method, it
will be used to perform asynchronous destruction, to release resources.
To enable this feature, after all needed modules are registered (and any other
preparations are done), call the RpcServer.init()
method to perform
initiation process for every registered module.
This feature will still work after hot-reloaded the module. However, there
would be a slight downtime during hot-reloading, and any call would fail until
the service is re-available again.
declare global {
namespace app {
namespace service {
const user: ModuleProxy<User>
}
}
}
export default class User {
async init() {
}
async destroy() {
}
}
(async () => {
server.register(app.services.user);
await server.init();
})();
For more details, please check the API documentation.