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.
NOTE: inspired by Alar, a new project named Microse
has been started, with new protocol and new syntax, and cross language support,
it will become the successor of Alar so there would be no major updates for
Alar any more, please check the new project out out.
NOTE: Alar is primarily designed for SFN
framework.
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 in the current
scope. That 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
restarting the program.
Alar, on the other hand, based on the namespace and ES6 proxy, it creates a
"soft-link" of the module, and only import the module when truly needed. And
since it's soft-linked, when the module file is changed, it has the ability to
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 scope, so other files can directly use it as a root namespace
without importing the module.
NOTE: Since v5.5, Alar introduced two new syntaxes to get the singleton and
create new instances of the module, they are more light-weight and elegant,
so this document will in favor of them, although the old style still works.
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 via namespace.
(NOTE: Alar offers first priority of the default
export, if a module doesn't
have a default export, Alar will try to load all exports instead.)
declare global {
namespace app {
const bootstrap: ModuleProxy<Bootstrap>
}
}
export default class Bootstrap {
init() {
}
}
declare global {
namespace app {
namespace models {
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().init();
var user = new app.models.user("Mr. Handsome");
console.log(user.getName());
Prototype Module
Any module that exports an object as default will be considered as a prototype
module, when creating a new instance 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 into the new object). However when calling the
singleton of that module, the original object itself will be returned.
declare global {
namespace app {
const config: ModuleProxy<Config>;
}
}
export interface Config {
}
export default <Config>{
}
Remote Service
Alar allows user to easily serve a module remotely, whether in another
process or in another machine.
Example
Say I want to serve a user service in a different process and communicate via
IPC channel, I just have to do this:
declare global {
namespace app {
namespace services {
const user: ModuleProxy<typeof UserService>
}
}
}
export default class UserService {
private users: { firstName: string, lastName: string }[] = [
{ firstName: "David", lastName: "Wood" },
];
async getFullName(firstName: string) {
let user = this.users.find(user => {
return user.firstName === firstName;
});
return user ? `${firstName} ${user.lastName}` : void 0;
}
}
import { App } from "./app";
(async () => {
let service = await App.serve("/tmp/my-app/remote-service.sock");
service.register(app.services.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.services.user);
let fullName = await app.services.user("route").getFullName("David");
console.log(fullName);
})();
Hot-reloading in Remote Service
The local watcher may notice the local file has been 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.ts module imports app.ts
module as well, which starts the watcher, when the user.ts module is changed,
the remote-service.ts will reload the module as expected, and the
index.ts 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 services {
const user: ModuleProxy<UserService>
}
}
}
export default class UserService {
async *getFriends() {
yield "Jane";
yield "Ben";
yield "Albert";
return "We are buddies";
}
}
(async () => {
let generator = app.services.user("route").getFriends();
for await (let name of generator) {
console.log(name);
}
let generator2 = app.services.user("route").getFriends();
while (true) {
let { value, done } = await generator2.next();
console.log(value);
if (done) {
break;
}
}
})();
Life Cycle Support
Since v6.0, Alar provides a new way to support life cycle functions, 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, first calling ModuleProxy.serve()
method to create an
RPC server that is not yet served immediately by passing the second argument
false
, and after all preparations are finished, calling the RpcServer.open()
method to open the channel and initiate bound modules.
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.
NOTE: Life cycle functions are only triggered when serving the module as an RPC
service, and they will not be triggered for local backups. That means, allowing
to fall back to local instance may cause some problems, since they haven't
performed any initiations. To prevent expected behavior, it would better to
disable the local version of the service by calling fallbackToLocal(false)
.
declare global {
namespace app {
namespace services {
const user: ModuleProxy<UserService>
}
}
}
export default class UserService {
async init() {
}
async destroy() {
}
}
(async () => {
let service = App.serve(config, false);
service.register(app.services.user);
await service.open();
})();
For more details, please check the API documentation.