UNPKG

6.15 kBPlain TextView Raw
1import { extname, sep } from "path";
2import { watch, FSWatcher } from "chokidar";
3import startsWith = require("lodash/startsWith");
4import { RpcOptions, RpcChannel } from './rpc/channel';
5import { RpcClient, ClientOptions } from "./rpc/client";
6import { RpcServer } from "./rpc/server";
7import { ModuleProxyBase } from "./proxy";
8import * as util from './util';
9
10export {
11 ModuleProxyBase,
12 RpcOptions,
13 RpcChannel,
14 RpcServer,
15 RpcClient,
16 ClientOptions,
17 FSWatcher,
18 util
19};
20
21// Auto-Load And Remote.
22
23declare global {
24 type FunctionPropertyNames<T> = {
25 [K in keyof T]: T[K] extends Function ? K : never
26 }[keyof T];
27 type NonFunctionPropertyNames<T> = {
28 [K in keyof T]: T[K] extends Function ? never : K
29 }[keyof T];
30 type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
31 type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
32 type Voidable<T> = {
33 [K in keyof T]: T[K] | void
34 }
35
36 interface ModuleConstructor<T> {
37 new(...args: any[]): T;
38 getInstance?(): T;
39 }
40
41 interface ModuleProxy<T, A1 = any, A2 = any, A3 = any, A4 = any, A5 = any> {
42 /** The name (with namespace) of the module. */
43 readonly name: string;
44 /** The path (without extension) of the module. */
45 readonly path: string;
46 /** The very exports object of the module. */
47 readonly exports: any;
48 /** The very prototype of the module. */
49 readonly proto: T;
50 /** The very class constructor of the module. */
51 readonly ctor: ModuleConstructor<T>;
52
53 /** Creates a new instance of the module. */
54 create(): T;
55 create(arg1: A1): T;
56 create(arg1: A1, arg2: A2): T;
57 create(arg1: A1, arg2: A2, arg3: A3): T;
58 create(arg1: A1, arg2: A2, arg3: A3, arg4: A4): T;
59 create(arg1: A1, arg2: A2, arg3: A3, arg4: A4, arg5: A5): T;
60
61 /**
62 * Gets the local singleton or a remote instance of the module, if
63 * connected to one or more remote instances, the module proxy will
64 * automatically calculate the `route` and direct the traffic to the
65 * corresponding remote instance.
66 */
67 instance(local: symbol): T;
68 instance(route?: any): FunctionProperties<T> &
69 Voidable<Readonly<NonFunctionProperties<T>>>;
70
71 /**
72 * If the module is registered as remote service, however when no RPC
73 * channel is available, by default, `instance()` will fail to the local
74 * instance, using this method to disable the default behavior.
75 */
76 noLocal(): this;
77
78 /**
79 * Allowing the current module to be injected as a dependency bound to a
80 * property of another class instance.
81 */
82 inject(route?: any): PropertyDecorator;
83 }
84}
85
86export interface ModuleLoader {
87 [x: string]: any;
88 /**
89 * Extension name of the module file, by default, it's `.js` (or `.ts` in
90 * ts-node).
91 */
92 extension: string | string[],
93 /** Loads module from the given file or cache. */
94 load(filename: string): any;
95 /** Unloads the module in cache if the file is modified. */
96 unload(filename: string): void;
97}
98
99export class ModuleProxy extends ModuleProxyBase {
100 /**
101 * If passed to the `ModuleProxy<T>.instance()`, the method will always
102 * return the local instance.
103 */
104 local = util.local;
105
106 get exports() {
107 return {};
108 }
109
110 /** Serves an RPC service according to the given configuration. */
111 serve(config: string | RpcOptions): Promise<RpcServer> {
112 return new RpcServer(<any>config).open();
113 }
114
115 /** Connects an RPC service according to the given configuration. */
116 connect(config: string | ClientOptions): Promise<RpcClient> {
117 return new RpcClient(<any>config).open();
118 }
119
120 /** Resolves the given path to a module name. */
121 resolve(path: string): string {
122 let dir = this.path + sep;
123
124 if (startsWith(path, dir)) {
125 let modPath = path.slice(dir.length),
126 ext = extname(modPath);
127
128 if (Array.isArray(this.loader.extension)) {
129 if (this.loader.extension.includes(ext)) {
130 modPath = modPath.slice(0, -ext.length);
131 } else {
132 return;
133 }
134 } else if (ext === this.loader.extension) {
135 modPath = modPath.slice(0, -ext.length);
136 } else if (ext) {
137 return;
138 }
139
140 return this.name + "." + modPath.replace(/\\|\//g, ".");
141 } else {
142 return;
143 }
144 }
145
146 /** Watches file change and reload the corresponding module. */
147 watch(listener?: (event: "change" | "unlink", filename: string) => void) {
148 let { path } = this;
149 let clearCache = (event: string, filename: string, cb: Function) => {
150 let name = this.resolve(filename);
151
152 if (name) {
153 delete this.singletons[name];
154 this.loader.unload(filename);
155 cb && cb(event, filename);
156 }
157 };
158
159 return watch(path, {
160 awaitWriteFinish: true,
161 followSymlinks: false,
162 ignored: /\.(js\.map|d\.ts)$/
163 }).on("change", (filename) => {
164 clearCache("change", filename, listener);
165 }).on("unlink", (filename) => {
166 clearCache("unlink", filename, listener);
167 }).on("unlinkDir", dirname => {
168 dirname = dirname + sep;
169
170 for (let filename in require.cache) {
171 if (startsWith(filename, dirname)) {
172 clearCache("unlink", filename, listener);
173 }
174 }
175 });
176 }
177
178 /** Sets a custom loader to resolve the module. */
179 setLoader(loader: ModuleLoader) {
180 this.loader = loader;
181 }
182}
183
184export default ModuleProxy;
\No newline at end of file