Skip to content

Commit 6a11a7e

Browse files
committed
Backport core libraries
PR-URL: #184
1 parent 0a07f9b commit 6a11a7e

18 files changed

+627
-318
lines changed

.travis.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ node_js:
55
services:
66
- postgresql
77
before_script:
8-
- psql -f db/install.sql -U postgres
9-
- PGPASSWORD=marcus psql -d application -f db/structure.sql -U marcus
10-
- PGPASSWORD=marcus psql -d application -f db/data.sql -U marcus
8+
- psql -f application/db/install.sql -U postgres
9+
- PGPASSWORD=marcus psql -d application -f application/db/structure.sql -U marcus
10+
- PGPASSWORD=marcus psql -d application -f application/db/data.sql -U marcus
1111
script:
1212
- npm test

application/config/server.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
({
22
host: '127.0.0.1',
3-
balancer: 3330,
3+
balancer: 8000,
44
protocol: 'http',
5-
ports: [3331, 3332],
5+
ports: [8001, 8002],
66
timeout: 5000,
77
concurrency: 1000,
88
queue: {

lib/application.js

Lines changed: 204 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,76 @@
11
'use strict';
22

3-
const api = require('./dependencies.js');
4-
const { path, events, vm, fs, fsp } = api;
3+
const { node, npm } = require('./dependencies.js');
4+
const { path, events, vm, fs, fsp } = node;
5+
const { common } = npm;
6+
57
const security = require('./security.js');
68

79
const SCRIPT_OPTIONS = { timeout: 5000 };
810
const EMPTY_CONTEXT = Object.freeze({});
11+
const MODULE = 2;
912

1013
class Application extends events.EventEmitter {
1114
constructor() {
1215
super();
16+
this.initialization = true;
1317
this.finalization = false;
1418
this.namespaces = ['db'];
15-
this.path = process.cwd();
16-
this.staticPath = path.join(this.path, 'static');
17-
this.api = new Map();
18-
this.domain = new Map();
19+
this.api = {};
1920
this.static = new Map();
21+
this.root = process.cwd();
22+
this.path = path.join(this.root, 'application');
23+
this.apiPath = path.join(this.path, 'api');
24+
this.libPath = path.join(this.path, 'lib');
25+
this.domainPath = path.join(this.path, 'domain');
26+
this.staticPath = path.join(this.path, 'static');
27+
this.starts = [];
2028
}
2129

2230
async init() {
2331
this.createSandbox();
24-
await this.loadPlace('api', path.join(this.path, 'api'));
25-
await this.loadPlace('domain', path.join(this.path, 'domain'));
26-
await this.loadPlace('static', path.join(this.path, 'static'));
32+
await Promise.allSettled([
33+
this.loadPlace('static', this.staticPath),
34+
this.loadPlace('api', this.apiPath),
35+
(async () => {
36+
await this.loadPlace('lib', this.libPath);
37+
await this.loadPlace('domain', this.domainPath);
38+
})(),
39+
]);
40+
await Promise.allSettled(this.starts.map(fn => fn()));
41+
this.starts = null;
42+
this.initialization = true;
2743
}
2844

2945
async shutdown() {
3046
this.finalization = true;
31-
await this.server.close();
47+
await this.stopPlace('domain');
48+
await this.stopPlace('lib');
49+
if (this.server) await this.server.close();
50+
await this.logger.close();
51+
}
52+
53+
async stopPlace(name) {
54+
const place = this.sandbox[name];
55+
for (const moduleName of Object.keys(place)) {
56+
const module = place[moduleName];
57+
if (module.stop) await this.execute(module.stop);
58+
}
3259
}
3360

3461
createSandbox() {
35-
const introspect = async () => [...this.api.keys()];
36-
const application = { security, introspect };
37-
for (const name of this.namespaces) application[name] = this[name];
62+
const { config, namespaces, server: { host, port, protocol } = {} } = this;
63+
const introspect = async interfaces => this.introspect(interfaces);
64+
const worker = { id: 'W' + node.worker.threadId.toString() };
65+
const server = { host, port, protocol };
66+
const application = { security, introspect, worker, server };
67+
const api = {};
68+
const lib = {};
69+
const domain = {};
70+
for (const name of namespaces) application[name] = this[name];
3871
const sandbox = {
39-
console: this.logger, Buffer, application, api,
72+
Buffer, URL, URLSearchParams, Error: this.Error, console: this.console,
73+
application, node, npm, api, lib, domain, config,
4074
setTimeout, setImmediate, setInterval,
4175
clearTimeout, clearImmediate, clearInterval,
4276
};
@@ -52,21 +86,100 @@ class Application extends events.EventEmitter {
5286
try {
5387
const code = await fsp.readFile(fileName, 'utf8');
5488
if (!code) return null;
55-
const src = `'use strict';\ncontext => ${code}`;
89+
const src = '\'use strict\';\ncontext => ' + code;
5690
const options = { filename: fileName, lineOffset: -1 };
5791
const script = new vm.Script(src, options);
5892
return script.runInContext(this.sandbox, SCRIPT_OPTIONS);
5993
} catch (err) {
60-
if (err.code !== 'ENOENT') this.logger.error(err.stack);
94+
if (err.code !== 'ENOENT') {
95+
this.logger.error(err.stack);
96+
}
6197
return null;
6298
}
6399
}
64100

65-
runMethod(methodName, session) {
66-
const script = this.api.get(methodName);
67-
if (!script) return null;
68-
const exp = script(session ? session.context : EMPTY_CONTEXT);
69-
return typeof exp !== 'object' ? { access: 'logged', method: exp } : exp;
101+
getMethod(iname, ver, methodName, context) {
102+
const iface = this.api[iname];
103+
if (!iface) return null;
104+
const version = ver === '*' ? iface.default : parseInt(ver);
105+
const methods = iface[version.toString()];
106+
if (!methods) return null;
107+
const method = methods[methodName];
108+
if (!method) return null;
109+
const exp = method(context);
110+
return typeof exp === 'object' ? exp : { access: 'logged', method: exp };
111+
}
112+
113+
async loadMethod(fileName) {
114+
const rel = fileName.substring(this.apiPath.length + 1);
115+
if (!rel.includes('/')) return;
116+
const [interfaceName, methodFile] = rel.split('/');
117+
if (!methodFile.endsWith('.js')) return;
118+
const name = path.basename(methodFile, '.js');
119+
const [iname, ver] = interfaceName.split('.');
120+
const version = parseInt(ver, 10);
121+
const script = await this.createScript(fileName);
122+
if (!script) return;
123+
let iface = this.api[iname];
124+
const { api } = this.sandbox;
125+
let internalInterface = api[iname];
126+
if (!iface) {
127+
this.api[iname] = iface = { default: version };
128+
api[iname] = internalInterface = {};
129+
}
130+
let methods = iface[ver];
131+
if (!methods) iface[ver] = methods = {};
132+
methods[name] = script;
133+
internalInterface[name] = script(EMPTY_CONTEXT);
134+
if (version > iface.default) iface.default = version;
135+
}
136+
137+
async loadModule(fileName) {
138+
const rel = fileName.substring(this.path.length + 1);
139+
if (!rel.endsWith('.js')) return;
140+
const script = await this.createScript(fileName);
141+
const name = path.basename(rel, '.js');
142+
const namespaces = rel.split(path.sep);
143+
namespaces[namespaces.length - 1] = name;
144+
const exp = script ? script(EMPTY_CONTEXT) : null;
145+
const container = typeof exp === 'function' ? { method: exp } : exp;
146+
const iface = {};
147+
if (container !== null) {
148+
const methods = Object.keys(container);
149+
for (const method of methods) {
150+
const fn = container[method];
151+
if (typeof fn === 'function') {
152+
container[method] = iface[method] = fn.bind(container);
153+
}
154+
}
155+
}
156+
let level = this.sandbox;
157+
const last = namespaces.length - 1;
158+
for (let depth = 0; depth <= last; depth++) {
159+
const namespace = namespaces[depth];
160+
let next = level[namespace];
161+
if (next) {
162+
if (depth === MODULE && namespace === 'stop') {
163+
if (exp === null && level.stop) await this.execute(level.stop);
164+
}
165+
} else {
166+
next = depth === last ? iface : {};
167+
level[namespace] = iface.method || iface;
168+
container.parent = level;
169+
if (depth === MODULE && namespace === 'start') {
170+
this.starts.push(iface.method);
171+
}
172+
}
173+
level = next;
174+
}
175+
}
176+
177+
async execute(fn) {
178+
try {
179+
await fn();
180+
} catch (err) {
181+
this.logger.error(err.stack);
182+
}
70183
}
71184

72185
async loadFile(filePath) {
@@ -75,47 +188,86 @@ class Application extends events.EventEmitter {
75188
const data = await fsp.readFile(filePath);
76189
this.static.set(key, data);
77190
} catch (err) {
78-
if (err.code !== 'ENOENT') this.logger.error(err.stack);
79-
}
80-
}
81-
82-
async loadScript(place, fileName) {
83-
const { name, ext } = path.parse(fileName);
84-
if (ext !== '.js' || name.startsWith('.')) return;
85-
const script = await this.createScript(fileName);
86-
const scripts = this[place];
87-
if (!script) {
88-
scripts.delete(name);
89-
return;
90-
}
91-
if (place === 'domain') {
92-
const config = this.config.sections[name];
93-
this.sandbox.application[name] = { config };
94-
const exp = script(EMPTY_CONTEXT);
95-
if (config) exp.config = config;
96-
this.sandbox.application[name] = exp;
97-
this.sandboxInject(name, exp);
98-
if (exp.start) exp.start();
99-
} else {
100-
scripts.set(name, script);
191+
if (err.code !== 'ENOENT') {
192+
this.logger.error(err.stack);
193+
}
101194
}
102195
}
103196

104197
async loadPlace(place, placePath) {
105198
const files = await fsp.readdir(placePath, { withFileTypes: true });
106-
const isStatic = place === 'static';
107199
for (const file of files) {
200+
if (file.name.startsWith('.')) continue;
108201
const filePath = path.join(placePath, file.name);
109-
if (!isStatic) await this.loadScript(place, filePath);
110-
else if (file.isDirectory()) await this.loadPlace(place, filePath);
111-
else await this.loadFile(filePath);
202+
if (file.isDirectory()) await this.loadPlace(place, filePath);
203+
else if (place === 'api') await this.loadMethod(filePath);
204+
else if (place === 'static') await this.loadFile(filePath);
205+
else await this.loadModule(filePath);
112206
}
113-
fs.watch(placePath, (event, fileName) => {
207+
this.watch(place, placePath);
208+
}
209+
210+
watch(place, placePath) {
211+
fs.watch(placePath, async (event, fileName) => {
212+
if (fileName.startsWith('.')) return;
114213
const filePath = path.join(placePath, fileName);
115-
if (isStatic) this.loadFile(filePath);
116-
else this.loadScript(place, filePath);
214+
try {
215+
const stat = await node.fsp.stat(filePath);
216+
if (stat.isDirectory()) {
217+
this.loadPlace(place, filePath);
218+
return;
219+
}
220+
} catch {
221+
return;
222+
}
223+
if (node.worker.threadId === 1) {
224+
const relPath = filePath.substring(this.path.length);
225+
this.logger.debug('Reload: ' + relPath);
226+
}
227+
if (place === 'api') this.loadMethod(filePath);
228+
else if (place === 'static') this.loadFile(filePath);
229+
else this.loadModule(filePath);
117230
});
118231
}
232+
233+
introspect(interfaces) {
234+
const intro = {};
235+
for (const interfaceName of interfaces) {
236+
const [iname, ver = '*'] = interfaceName.split('.');
237+
const iface = this.api[iname];
238+
if (!iface) continue;
239+
const version = ver === '*' ? iface.default : parseInt(ver);
240+
const methods = iface[version.toString()];
241+
const methodNames = Object.keys(methods);
242+
const interfaceMethods = intro[iname] = {};
243+
for (const methodName of methodNames) {
244+
const exp = methods[methodName](EMPTY_CONTEXT);
245+
const fn = typeof exp === 'object' ? exp.method : exp;
246+
const src = fn.toString();
247+
const signature = common.between(src, '({', '})');
248+
if (signature === '') {
249+
interfaceMethods[methodName] = [];
250+
continue;
251+
}
252+
const args = signature.split(',').map(s => s.trim());
253+
interfaceMethods[methodName] = args;
254+
}
255+
}
256+
return intro;
257+
}
258+
259+
getStaticFile(fileName) {
260+
return this.static.get(fileName);
261+
}
119262
}
120263

121-
module.exports = new Application();
264+
const application = new Application();
265+
266+
application.Error = class extends Error {
267+
constructor(message, code) {
268+
super(message);
269+
this.code = code;
270+
}
271+
};
272+
273+
module.exports = application;

lib/auth.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
'use strict';
22

3-
const { crypto, common } = require('./dependencies.js');
3+
const { node, npm } = require('./dependencies.js');
4+
const { crypto } = node;
5+
const { common } = npm;
46
const application = require('./application.js');
57

68
const BYTE = 256;

0 commit comments

Comments
 (0)