1 | # Alar
|
2 |
|
3 | Alar is a light-weight framework that provides applications the ability to
|
4 | auto-load and hot-reload modules, as well as the ability to serve instances
|
5 | remotely 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 |
|
16 | In NodeJS (with CommonJS module solution), `require` and `import` will
|
17 | immediately load the corresponding module and make a reference in the current
|
18 | scope. That means, if the module doesn't finish initiation, e.g. circular
|
19 | import, the application may not work as expected. And if the module file is
|
20 | modified, the application won't be able to reload that module without
|
21 | restarting the program.
|
22 |
|
23 | Alar, 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
|
25 | since it's soft-linked, when the module file is changed, it has the ability to
|
26 | wipe out the memory cache and reload the module with very few side-effects.
|
27 |
|
28 | ### How to use?
|
29 |
|
30 | In order to use Alar, one must create a root `ModuleProxy` instance and assign
|
31 | it to the global scope, so other files can directly use it as a root namespace
|
32 | without 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
|
42 | import { ModuleProxy } from "alar";
|
43 |
|
44 | // Expose and merge the app as a namespace under the global scope.
|
45 | declare global {
|
46 | namespace app { }
|
47 | }
|
48 |
|
49 | // Create the instance.
|
50 | export const App = global["app"] = new ModuleProxy("app", __dirname);
|
51 |
|
52 | // Watch file changes and hot-reload modules.
|
53 | App.watch();
|
54 | ```
|
55 |
|
56 | In other files, just define and export a default class, and merge the type to
|
57 | the 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
|
60 | have 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
|
66 | declare global {
|
67 | namespace app {
|
68 | const bootstrap: ModuleProxy<Bootstrap>
|
69 | }
|
70 | }
|
71 |
|
72 | export default class Bootstrap {
|
73 | init() {
|
74 | // ...
|
75 | }
|
76 | }
|
77 | ```
|
78 |
|
79 | ```typescript
|
80 | // src/models/user.ts
|
81 | declare 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 |
|
91 | export default class User {
|
92 | constructor(private name: string) { }
|
93 |
|
94 | getName() {
|
95 | return this.name;
|
96 | }
|
97 | }
|
98 | ```
|
99 |
|
100 | And other files can access to the modules via the namespace:
|
101 |
|
102 | ```typescript
|
103 | // src/index.ts
|
104 | import "./app";
|
105 |
|
106 | // Calling the module as a function will link to the singleton of the module.
|
107 | app.bootstrap().init();
|
108 |
|
109 | // Using `new` syntax on the module to create a new instance.
|
110 | var user = new app.models.user("Mr. Handsome");
|
111 |
|
112 | console.log(user.getName()); // Mr. Handsome
|
113 | ```
|
114 |
|
115 | ### Prototype Module
|
116 |
|
117 | Any module that exports an object as default will be considered as a prototype
|
118 | module, when creating a new instance of that module, the object will be used as
|
119 | a prototype (since v4.0.4, a deep clone will be used instead, if an argument is
|
120 | passed, it will be merged into the new object). However when calling the
|
121 | singleton of that module, the original object itself will be returned.
|
122 |
|
123 | ```typescript
|
124 | // src/config.ts
|
125 | declare global {
|
126 | namespace app {
|
127 | const config: ModuleProxy<Config>;
|
128 | }
|
129 | }
|
130 |
|
131 | export interface Config {
|
132 | // ...
|
133 | }
|
134 |
|
135 | export default <Config>{
|
136 | // ...
|
137 | }
|
138 | ```
|
139 |
|
140 | ## Remote Service
|
141 |
|
142 | Alar allows user to easily serve a module remotely, whether in another
|
143 | process or in another machine.
|
144 |
|
145 | ### Example
|
146 |
|
147 | Say I want to serve a user service in a different process and communicate via
|
148 | IPC channel, I just have to do this:
|
149 |
|
150 | ```typescript
|
151 | // src/services/user.ts
|
152 | declare 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.
|
162 | export 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
|
181 | import { 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 |
|
192 | Just try `ts-node --files src/remote-service` (or `node dist/remote-service`),
|
193 | and the service will be started immediately.
|
194 |
|
195 | And in **index.ts**, connect to the service before using remote functions:
|
196 |
|
197 | ```typescript
|
198 | // index.ts
|
199 | import { 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 |
|
215 | The local watcher may notice the local file has been changed and try to reload
|
216 | the local module (and the local singleton), however, it will not affect any
|
217 | remote instances, that said, the instance served remotely can still be watched
|
218 | and reloaded on the remote server individually.
|
219 |
|
220 | In the above example, since the **remote-service.ts** module imports **app.ts**
|
221 | module as well, which starts the watcher, when the **user.ts** module is changed,
|
222 | the **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 |
|
227 | Since version 3.3, Alar supports generators (and async generators) in both local
|
228 | call and remote call contexts.
|
229 |
|
230 | ```typescript
|
231 | // src/services/user.ts
|
232 | declare global {
|
233 | namespace app {
|
234 | namespace services {
|
235 | const user: ModuleProxy<UserService>
|
236 | }
|
237 | }
|
238 | }
|
239 |
|
240 | export 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 |
|
288 | Since v6.0, Alar provides a new way to support life cycle functions, it will be
|
289 | used to perform asynchronous initiation, for example, connecting to a database.
|
290 | And if it contains a `destroy()` method, it will be used to perform asynchronous
|
291 | destruction, to release resources.
|
292 |
|
293 | To enable this feature, first calling `ModuleProxy.serve()` method to create an
|
294 | RPC server that is not yet served immediately by passing the second argument
|
295 | `false`, and after all preparations are finished, calling the `RpcServer.open()`
|
296 | method to open the channel and initiate bound modules.
|
297 |
|
298 | This feature will still work after hot-reloaded the module. However, there
|
299 | would be a slight downtime during hot-reloading, and any call would fail until
|
300 | the service is re-available again.
|
301 |
|
302 | NOTE: Life cycle functions are only triggered when serving the module as an RPC
|
303 | service, and they will not be triggered for local backups. That means, allowing
|
304 | to fall back to local instance may cause some problems, since they haven't
|
305 | performed any initiations. To prevent expected behavior, it would better to
|
306 | disable the local version of the service by calling `fallbackToLocal(false)`.
|
307 |
|
308 | ```ts
|
309 | // src/services/user.ts
|
310 | declare global {
|
311 | namespace app {
|
312 | namespace services {
|
313 | const user: ModuleProxy<UserService>
|
314 | }
|
315 | }
|
316 | }
|
317 |
|
318 | export 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 |
|
340 | For more details, please check the [API documentation](./api.md).
|