UNPKG

9.37 kBMarkdownView Raw
1# Alar
2
3Alar is a light-weight framework that provides applications the ability to
4auto-load and hot-reload modules, as well as the ability to serve instances
5remotely as RPC services.
6
7*NOTE: Alar is primarily designed for [SFN](https://github.com/hyurl/sfn)*
8*framework.*
9
10## Prerequisites
11
12- Node.js `v8.3.0+`
13
14## Auto-loading and Hot-reloading
15
16In NodeJS (with CommonJS module solution), `require` and `import` will
17immediately load the corresponding module and make a reference in the current
18scope. That means, if the module doesn't finish initiation, e.g. circular
19import, the application may not work as expected. And if the module file is
20modified, the application won't be able to reload that module without
21restarting the program.
22
23Alar, on the other hand, based on the namespace and ES6 proxy, it creates a
24*"soft-link"* of the module, and only import the module when truly needed. And
25since it's soft-linked, when the module file is changed, it has the ability to
26wipe out the memory cache and reload the module with very few side-effects.
27
28### How to use?
29
30In order to use Alar, one must create a root `ModuleProxy` instance and assign
31it to the global scope, so other files can directly use it as a root namespace
32without importing the module.
33
34**NOTE: Since v5.5, Alar introduced two new syntaxes to get the singleton and**
35**create new instances of the module, they are more light-weight and elegant,**
36**so this document will in favor of them, although the old style still works.**
37
38### Example
39
40```typescript
41// src/app.ts
42import { ModuleProxy } from "alar";
43
44// Expose and merge the app as a namespace under the global scope.
45declare global {
46 namespace app { }
47}
48
49// Create the instance.
50export const App = global["app"] = new ModuleProxy("app", __dirname);
51
52// Watch file changes and hot-reload modules.
53App.watch();
54```
55
56In other files, just define and export a default class, and merge the type to
57the namespace `app`, so that another file can access it directly via namespace.
58
59(NOTE: Alar offers first priority of the `default` export, if a module doesn't
60have a default export, Alar will try to load all exports instead.)
61
62```typescript
63// Be aware that the namespace must correspond to the filename.
64
65// src/bootstrap.ts
66declare global {
67 namespace app {
68 const bootstrap: ModuleProxy<Bootstrap>
69 }
70}
71
72export default class Bootstrap {
73 init() {
74 // ...
75 }
76}
77```
78
79```typescript
80// src/models/user.ts
81declare global {
82 namespace app {
83 namespace models {
84 // Since v5.0, a module class with parameters must use the signature
85 // `typeof T`.
86 const user: ModuleProxy<typeof User>
87 }
88 }
89}
90
91export default class User {
92 constructor(private name: string) { }
93
94 getName() {
95 return this.name;
96 }
97}
98```
99
100And other files can access to the modules via the namespace:
101
102```typescript
103// src/index.ts
104import "./app";
105
106// Calling the module as a function will link to the singleton of the module.
107app.bootstrap().init();
108
109// Using `new` syntax on the module to create a new instance.
110var user = new app.models.user("Mr. Handsome");
111
112console.log(user.getName()); // Mr. Handsome
113```
114
115### Prototype Module
116
117Any module that exports an object as default will be considered as a prototype
118module, when creating a new instance of that module, the object will be used as
119a prototype (since v4.0.4, a deep clone will be used instead, if an argument is
120passed, it will be merged into the new object). However when calling the
121singleton of that module, the original object itself will be returned.
122
123```typescript
124// src/config.ts
125declare global {
126 namespace app {
127 const config: ModuleProxy<Config>;
128 }
129}
130
131export interface Config {
132 // ...
133}
134
135export default <Config>{
136 // ...
137}
138```
139
140## Remote Service
141
142Alar allows user to easily serve a module remotely, whether in another
143process or in another machine.
144
145### Example
146
147Say I want to serve a user service in a different process and communicate via
148IPC channel, I just have to do this:
149
150```typescript
151// src/services/user.ts
152declare global {
153 namespace app {
154 namespace services {
155 const user: ModuleProxy<typeof UserService>
156 }
157 }
158}
159
160// It is recommended not to define the constructor and use a non-parameter
161// constructor.
162export default class UserService {
163 private users: { firstName: string, lastName: string }[] = [
164 { firstName: "David", lastName: "Wood" },
165 // ...
166 ];
167
168 // Any method that will potentially be called remotely should be async.
169 async getFullName(firstName: string) {
170 let user = this.users.find(user => {
171 return user.firstName === firstName;
172 });
173
174 return user ? `${firstName} ${user.lastName}` : void 0;
175 }
176}
177```
178
179```typescript
180// src/remote-service.ts
181import { App } from "./app";
182
183(async () => {
184 let service = await App.serve("/tmp/my-app/remote-service.sock");
185
186 service.register(app.services.user);
187
188 console.log("Service started!");
189})();
190```
191
192Just try `ts-node --files src/remote-service` (or `node dist/remote-service`),
193and the service will be started immediately.
194
195And in **index.ts**, connect to the service before using remote functions:
196
197```typescript
198// index.ts
199import { App } from "./app";
200
201(async () => {
202 let service = await App.connect("/tmp/my-app/remote-service.sock");
203
204 service.register(app.services.user);
205
206 // Accessing the instance in local style but actually calling remote.
207 // Since v6.0, the **route** argument for the module must be explicit.
208 let fullName = await app.services.user("route").getFullName("David");
209 console.log(fullName); // David Wood
210})();
211```
212
213### Hot-reloading in Remote Service
214
215The local watcher may notice the local file has been changed and try to reload
216the local module (and the local singleton), however, it will not affect any
217remote instances, that said, the instance served remotely can still be watched
218and reloaded on the remote server individually.
219
220In the above example, since the **remote-service.ts** module imports **app.ts**
221module as well, which starts the watcher, when the **user.ts** module is changed,
222the **remote-service.ts** will reload the module as expected, and the
223**index.ts** calls it remotely will get the new result as expected.
224
225## Generator Support
226
227Since version 3.3, Alar supports generators (and async generators) in both local
228call and remote call contexts.
229
230```typescript
231// src/services/user.ts
232declare global {
233 namespace app {
234 namespace services {
235 const user: ModuleProxy<UserService>
236 }
237 }
238}
239
240export default class UserService {
241 // ...
242 async *getFriends() {
243 yield "Jane";
244 yield "Ben";
245 yield "Albert";
246 return "We are buddies";
247 }
248}
249
250// index.ts
251(async () => {
252 // Whether calling the local instance or a remote instance, the following
253 // program produces the same result.
254
255 let generator = app.services.user("route").getFriends();
256
257 for await (let name of generator) {
258 console.log(name);
259 // Jane
260 // Ben
261 // Albert
262 }
263
264 // The following usage gets the same result.
265 let generator2 = app.services.user("route").getFriends();
266
267 while (true) {
268 let { value, done } = await generator2.next();
269
270 console.log(value);
271 // NOTE: calling next() will return the returning value of the generator
272 // as well, so the output would be:
273 //
274 // Jane
275 // Ben
276 // Albert
277 // We are buddies
278
279 if (done) {
280 break;
281 }
282 }
283})();
284```
285
286## Life Cycle Support
287
288Since v6.0, Alar provides a new way to support life cycle functions, it will be
289used to perform asynchronous initiation, for example, connecting to a database.
290And if it contains a `destroy()` method, it will be used to perform asynchronous
291destruction, to release resources.
292
293To enable this feature, first calling `ModuleProxy.serve()` method to create an
294RPC server that is not yet served immediately by passing the second argument
295`false`, and after all preparations are finished, calling the `RpcServer.open()`
296method to open the channel and initiate bound modules.
297
298This feature will still work after hot-reloaded the module. However, there
299would be a slight downtime during hot-reloading, and any call would fail until
300the service is re-available again.
301
302NOTE: Life cycle functions are only triggered when serving the module as an RPC
303service, and they will not be triggered for local backups. That means, allowing
304to fall back to local instance may cause some problems, since they haven't
305performed any initiations. To prevent expected behavior, it would better to
306disable the local version of the service by calling `fallbackToLocal(false)`.
307
308```ts
309// src/services/user.ts
310declare global {
311 namespace app {
312 namespace services {
313 const user: ModuleProxy<UserService>
314 }
315 }
316}
317
318export default class UserService {
319 async init() {
320 // ...
321 }
322
323 async destroy() {
324 // ...
325 }
326}
327
328
329(async () => {
330 let service = App.serve(config, false); // pass false to serve()
331
332 service.register(app.services.user);
333
334 // other preparations...
335
336 await service.open();
337})();
338```
339
340For more details, please check the [API documentation](./api.md).