impress
Advanced tools
Comparing version 2.6.10 to 3.0.0-alpha.1
@@ -5,2 +5,9 @@ # Changelog | ||
## [3.0.0-alpha.1][] - 2022-06-25 | ||
- Worker-based multitenancy implementation | ||
- Update to metaschema v2.x | ||
- Initial integration bus implementation (new place `application/bus`) | ||
- Fix shutdown while initialization | ||
## [2.6.10][] - 2022-05-09 | ||
@@ -270,3 +277,4 @@ | ||
[unreleased]: https://github.com/metarhia/impress/compare/v2.6.10...HEAD | ||
[unreleased]: https://github.com/metarhia/impress/compare/v3.0.0-alpha.1...HEAD | ||
[3.0.0-alpha.1]: https://github.com/metarhia/impress/compare/v2.6.10...v3.0.0-alpha.1 | ||
[2.6.10]: https://github.com/metarhia/impress/compare/v2.6.9...v2.6.10 | ||
@@ -273,0 +281,0 @@ [2.6.9]: https://github.com/metarhia/impress/compare/v2.6.8...v2.6.9 |
260
impress.js
@@ -5,2 +5,3 @@ 'use strict'; | ||
const fsp = require('fs').promises; | ||
const { Worker } = require('worker_threads'); | ||
@@ -13,9 +14,7 @@ const path = require('path'); | ||
const { Logger } = require('metalog'); | ||
let logger = null; | ||
const { Planner } = require('./lib/planner.js'); | ||
let finalization = false; | ||
let initialization = true; | ||
const CONFIG_SECTIONS = ['log', 'scale', 'server', 'sessions']; | ||
const PATH = process.cwd(); | ||
const WORKER_PATH = path.join(__dirname, 'lib/worker.js'); | ||
const CFG_PATH = path.join(PATH, 'application/config'); | ||
@@ -25,6 +24,21 @@ const LOG_PATH = path.join(PATH, 'log'); | ||
const LOG_OPTIONS = { path: LOG_PATH, home: PATH, workerId: 0 }; | ||
const CONTEXT = metavm.createContext({ process }); | ||
const CFG_OPTIONS = { mode: process.env.MODE, context: CONTEXT }; | ||
const exit = async (message = 'Can not start Application server') => { | ||
console.error(metautil.replace(message, PATH, '')); | ||
if (logger) await logger.close(); | ||
const impress = { | ||
logger: null, | ||
config: null, | ||
planner: null, | ||
finalized: () => {}, | ||
finalization: false, | ||
initialization: true, | ||
console, | ||
applications: new Map(), | ||
lastWorkerId: 0, | ||
startTimer: null, | ||
}; | ||
const exit = async (message) => { | ||
impress.console.info(message); | ||
if (impress.logger && impress.logger.active) await impress.logger.close(); | ||
process.exit(1); | ||
@@ -35,14 +49,75 @@ }; | ||
const msg = err.stack || err.message || 'no stack trace'; | ||
console.error(`${type} error: ${msg}`); | ||
if (finalization) return; | ||
if (initialization) exit(); | ||
impress.console.error(`${type}: ${msg}`); | ||
if (impress.finalization) return; | ||
if (impress.initialization) exit('Can not start Application server'); | ||
}; | ||
process.on('uncaughtException', logError('uncaughtException')); | ||
process.on('warning', logError('warning')); | ||
process.on('unhandledRejection', logError('unhandledRejection')); | ||
process.on('uncaughtException', logError('Uncaught exception')); | ||
process.on('warning', logError('Warning')); | ||
process.on('unhandledRejection', logError('Unhandled rejection')); | ||
const startWorker = async (app, kind, port, id = ++impress.lastWorkerId) => { | ||
const workerData = { id, kind, path: app.path, port }; | ||
const options = { trackUnmanagedFds: true, workerData }; | ||
const worker = new Worker(WORKER_PATH, options); | ||
if (kind === 'worker') { | ||
app.pool.add(worker); | ||
await app.pool.capture(); | ||
} | ||
app.threads.set(id, worker); | ||
worker.on('exit', (code) => { | ||
if (code !== 0) startWorker(app, kind, port, id); | ||
else app.threads.delete(id); | ||
if (app.threads.size === 0) { | ||
impress.applications.delete(app.path); | ||
if (impress.applications.size === 0) impress.finalized(); | ||
} | ||
}); | ||
const handlers = { | ||
started: ({ kind }) => { | ||
app.ready++; | ||
if (kind === 'worker') app.pool.release(worker); | ||
if (app.threads.size === app.ready) { | ||
clearTimeout(impress.startTimer); | ||
impress.initialization = false; | ||
impress.console.info(`App started: ${app.path}`); | ||
} | ||
}, | ||
task: async ({ action, port, task }) => { | ||
const { planner } = impress; | ||
task.app = app.path; | ||
if (action === 'add') port.postMessage({ id: await planner.add(task) }); | ||
else if (action === 'remove') planner.remove(task.id); | ||
else if (action === 'stop') planner.stop(task.name); | ||
}, | ||
invoke: async (msg) => { | ||
const { status, port, exclusive } = msg; | ||
if (status === 'done') { | ||
app.pool.release(worker); | ||
return; | ||
} | ||
const promisedThread = exclusive ? app.pool.capture() : app.pool.next(); | ||
const next = await promisedThread.catch(() => { | ||
const error = new Error('No thread available'); | ||
port.postMessage({ name: 'error', error }); | ||
return null; | ||
}); | ||
if (!next) return; | ||
next.postMessage(msg, [port]); | ||
}, | ||
}; | ||
worker.on('message', (msg) => { | ||
const handler = handlers[msg.name]; | ||
if (handler) handler(msg); | ||
}); | ||
}; | ||
const validateConfig = async (config) => { | ||
let valid = true; | ||
const schemaPath = path.join(__dirname, 'schemas/config'); | ||
let valid = true; | ||
for (const section of CONFIG_SECTIONS) { | ||
@@ -54,3 +129,3 @@ const fileName = path.join(schemaPath, section + '.js'); | ||
for (const err of checkResult.errors) { | ||
console.error(`${err} in application/config/${section}.js`); | ||
impress.console.error(`${err} in application/config/${section}.js`); | ||
} | ||
@@ -60,107 +135,85 @@ valid = false; | ||
} | ||
if (!valid) exit(); | ||
if (!valid) exit('Application server configuration is invalid'); | ||
}; | ||
(async () => { | ||
const context = metavm.createContext({ process }); | ||
const CFG_OPTIONS = { mode: process.env.MODE, context }; | ||
const config = await new Config(CFG_PATH, CFG_OPTIONS).catch((err) => { | ||
const loadApplication = async (root) => { | ||
impress.console.info(`Start: ${root}`); | ||
const configPath = path.join(root, 'application/config'); | ||
const config = await new Config(configPath, CFG_OPTIONS).catch((err) => { | ||
exit(`Can not read configuration: ${CFG_PATH}\n${err.stack}`); | ||
}); | ||
await validateConfig(config); | ||
logger = await new Logger({ ...LOG_OPTIONS, ...config.log }); | ||
logger.on('error', logError('logger error')); | ||
if (logger.active) global.console = logger.console; | ||
await validateConfig(config); | ||
const { balancer, ports = [], workers = {} } = config.server; | ||
const serversCount = ports.length + (balancer ? 1 : 0); | ||
const schedulerCount = 1; | ||
const schedulerId = serversCount; | ||
const poolSize = workers.pool || 0; | ||
const count = serversCount + schedulerCount + poolSize; | ||
let startTimer = null; | ||
let active = 0; | ||
let starting = 0; | ||
let scheduler = null; | ||
const threads = new Array(count); | ||
const threads = new Map(); | ||
const pool = new metautil.Pool({ timeout: workers.wait }); | ||
const stop = async () => { | ||
finalization = true; | ||
const closing = logger.close(); | ||
for (const worker of threads) { | ||
worker.postMessage({ type: 'event', name: 'stop' }); | ||
} | ||
await closing; | ||
}; | ||
const app = { path: root, config, threads, pool, ready: 0 }; | ||
const start = (id) => { | ||
const workerPath = path.join(__dirname, 'lib/worker.js'); | ||
const worker = new Worker(workerPath, { trackUnmanagedFds: true }); | ||
threads[id] = worker; | ||
if (id === schedulerId) scheduler = worker; | ||
else if (id > schedulerId) pool.add(worker); | ||
if (balancer) await startWorker(app, 'balancer', balancer); | ||
for (const port of ports) await startWorker(app, 'server', port); | ||
const poolSize = workers.pool || 0; | ||
for (let i = 0; i < poolSize; i++) await startWorker(app, 'worker'); | ||
worker.on('exit', (code) => { | ||
active--; | ||
if (code !== 0) start(id); | ||
else if (active === 0) process.exit(0); | ||
else if (active < 0 && id === 0) exit('Application server stopped'); | ||
}); | ||
impress.applications.set(root, app); | ||
}; | ||
worker.on('online', () => { | ||
if (++starting === count) { | ||
startTimer = setTimeout(() => { | ||
if (active !== count) { | ||
console.warn(`Worker ${id} initialization timeout`); | ||
} | ||
}, config.server.timeouts.start); | ||
} | ||
}); | ||
const loadApplications = async () => { | ||
const list = await fsp | ||
.readFile('.applications', 'utf8') | ||
.then((data) => data.split('\n').filter((s) => s.length !== 0)) | ||
.catch(() => [PATH]); | ||
for (const path of list) { | ||
await loadApplication(path); | ||
} | ||
}; | ||
const ITC = { | ||
event: ({ name }) => { | ||
if (name !== 'started') return; | ||
active++; | ||
if (active === count && startTimer) { | ||
clearTimeout(startTimer); | ||
startTimer = null; | ||
} | ||
}, | ||
const stopApplication = (root) => { | ||
const app = impress.applications.get(root); | ||
for (const thread of app.threads.values()) { | ||
thread.postMessage({ name: 'stop' }); | ||
} | ||
}; | ||
task: (msg) => { | ||
if (msg.type !== 'task') return; | ||
const transferList = msg.port ? [msg.port] : undefined; | ||
scheduler.postMessage(msg, transferList); | ||
}, | ||
const stop = async () => { | ||
impress.finalization = true; | ||
const logClosed = impress.logger.close(); | ||
const portsClosed = new Promise((resolve) => { | ||
impress.finalized = resolve; | ||
setTimeout(() => { | ||
impress.console.error('Exit with graceful shutdown timeout'); | ||
resolve(); | ||
}, impress.config.server.timeouts.stop); | ||
}); | ||
for (const app of impress.applications.values()) { | ||
stopApplication(app.path); | ||
} | ||
await Promise.allSettled([logClosed, portsClosed]); | ||
exit('Application server stopped'); | ||
}; | ||
invoke: async (msg) => { | ||
const { name, port, exclusive } = msg; | ||
if (name === 'done') { | ||
if (exclusive) pool.release(worker); | ||
return; | ||
} | ||
if (name !== 'request') return; | ||
const promisedThread = exclusive ? pool.capture() : pool.next(); | ||
const next = await promisedThread.catch(() => { | ||
port.postMessage({ error: new Error('No thread available') }); | ||
return null; | ||
}); | ||
if (!next) return; | ||
next.postMessage(msg, [port]); | ||
}, | ||
}; | ||
(async () => { | ||
const configPath = path.join(PATH, 'application/config'); | ||
const config = await new Config(configPath, CFG_OPTIONS).catch((err) => { | ||
exit(`Can not read configuration: ${CFG_PATH}\n${err.stack}`); | ||
}); | ||
await validateConfig(config); | ||
impress.config = config; | ||
const logger = await new Logger({ ...LOG_OPTIONS, ...config.log }); | ||
logger.on('error', logError('Logger')); | ||
if (logger.active) impress.console = logger.console; | ||
impress.logger = logger; | ||
const tasksPath = path.join(PATH, 'application/tasks'); | ||
const tasksConfig = config.server.scheduler; | ||
impress.planner = await new Planner(tasksPath, tasksConfig, impress); | ||
worker.on('message', (msg) => { | ||
const handler = ITC[msg.type]; | ||
if (handler) handler(msg); | ||
}); | ||
}; | ||
for (let id = 0; id < count; id++) start(id); | ||
process.on('SIGINT', stop); | ||
process.on('SIGTERM', stop); | ||
impress.startTimer = setTimeout(() => { | ||
impress.console.warn(`Initialization timeout`); | ||
}, config.server.timeouts.start); | ||
await loadApplications(); | ||
if (process.stdin.isTTY) { | ||
@@ -173,3 +226,2 @@ process.stdin.setRawMode(true); | ||
} | ||
initialization = false; | ||
})().catch(logError('initialization')); | ||
})().catch(logError('Initialization')); |
@@ -7,8 +7,10 @@ 'use strict'; | ||
const fs = require('fs'); | ||
const { MessageChannel, parentPort, threadId } = require('worker_threads'); | ||
const wt = require('worker_threads'); | ||
const { MessageChannel, parentPort, threadId } = wt; | ||
const workerData = wt.workerData || { path: process.cwd() }; | ||
const metavm = require('metavm'); | ||
const metawatch = require('metawatch'); | ||
const metautil = require('metautil'); | ||
const { Interfaces } = require('./interfaces.js'); | ||
const { Modules } = require('./modules.js'); | ||
const { Services } = require('./services.js'); | ||
const { Resources } = require('./resources.js'); | ||
@@ -40,6 +42,6 @@ const { Schemas } = require('./schemas.js'); | ||
super(); | ||
this.kind = ''; | ||
this.kind = workerData.kind; | ||
this.initialization = true; | ||
this.finalization = false; | ||
this.root = process.cwd(); | ||
this.root = workerData.path; | ||
this.path = path.join(this.root, 'application'); | ||
@@ -53,2 +55,3 @@ | ||
this.db = new Modules('db', this); | ||
this.bus = new Services('bus', this); | ||
this.domain = new Modules('domain', this); | ||
@@ -72,4 +75,3 @@ this.scheduler = new Scheduler(this); | ||
async init(kind) { | ||
this.kind = kind; | ||
async init() { | ||
this.startWatch(); | ||
@@ -85,2 +87,3 @@ this.createSandbox(); | ||
await this.db.load(); | ||
await this.bus.load(); | ||
await this.domain.load(); | ||
@@ -91,5 +94,5 @@ })(), | ||
await Promise.allSettled(this.starts.map((fn) => this.execute(fn))); | ||
this.starts = []; | ||
this.sandbox.application.emit('started'); | ||
await this.api.load(); | ||
if (kind === 'scheduler') await this.scheduler.load(); | ||
const { api } = this.sandbox; | ||
@@ -106,3 +109,2 @@ if (api.auth) { | ||
} | ||
this.starts = []; | ||
this.initialization = false; | ||
@@ -122,2 +124,3 @@ this.sandbox.application.emit('initialized'); | ||
async stopPlace(name) { | ||
if (!this.sandbox) return; | ||
const place = this.sandbox[name]; | ||
@@ -141,2 +144,3 @@ for (const moduleName of Object.keys(place)) { | ||
sandbox.db = this.db.tree; | ||
sandbox.bus = this.bus.tree; | ||
sandbox.domain = this.domain.tree; | ||
@@ -169,3 +173,3 @@ this.sandbox = metavm.createContext(sandbox); | ||
`Failed to execute method: ${err && err.message}`, | ||
err.stack | ||
err.stack, | ||
); | ||
@@ -226,3 +230,3 @@ }); | ||
const data = { method, args }; | ||
const msg = { type: 'invoke', name: 'request', exclusive, data, port }; | ||
const msg = { name: 'invoke', exclusive, data, port }; | ||
return new Promise((resolve, reject) => { | ||
@@ -238,25 +242,2 @@ port2.on('message', ({ error, data }) => { | ||
const application = new Application(); | ||
if (parentPort) { | ||
parentPort.on('message', async ({ type, name, exclusive, data, port }) => { | ||
if (type !== 'invoke' || name !== 'request') return; | ||
const { method, args } = data; | ||
const handler = metautil.namespaceByPath(application.sandbox, method); | ||
if (!handler) { | ||
port.postMessage({ error: new Error('Handler not found') }); | ||
return; | ||
} | ||
try { | ||
const result = await handler(args); | ||
port.postMessage({ data: result }); | ||
} catch (err) { | ||
port.postMessage({ error: err }); | ||
application.console.error(err.stack); | ||
} finally { | ||
parentPort.postMessage({ type: 'invoke', name: 'done', exclusive }); | ||
} | ||
}); | ||
} | ||
module.exports = application; | ||
module.exports = new Application(); |
@@ -8,2 +8,3 @@ 'use strict'; | ||
constructor(place, application) { | ||
this.place = place; | ||
this.path = application.absolute(place); | ||
@@ -18,3 +19,3 @@ this.application = application; | ||
for (const file of files) { | ||
if (file.name.startsWith('.')) continue; | ||
if (file.name.startsWith('.') && !file.name.endsWith('.js')) continue; | ||
const filePath = path.join(targetPath, file.name); | ||
@@ -21,0 +22,0 @@ if (file.isDirectory()) await this.load(filePath); |
@@ -31,3 +31,3 @@ 'use strict'; | ||
'/', | ||
'-' | ||
'-', | ||
); | ||
@@ -34,0 +34,0 @@ const camelKey = metautil.spinalToCamel(key); |
@@ -59,3 +59,3 @@ 'use strict'; | ||
const iface = this.collection[iname]; | ||
if (!iface) return null; | ||
if (!iface) return; | ||
const methods = iface[version.toString()]; | ||
@@ -62,0 +62,0 @@ if (methods) delete methods[name]; |
@@ -28,2 +28,6 @@ 'use strict'; | ||
preprocess(iface) { | ||
return iface; | ||
} | ||
set(relPath, iface) { | ||
@@ -70,3 +74,3 @@ const names = parsePath(relPath); | ||
const relPath = filePath.substring(this.path.length + 1); | ||
this.set(relPath, exports); | ||
this.set(relPath, this.preprocess(exports)); | ||
} catch (err) { | ||
@@ -73,0 +77,0 @@ if (err.code !== 'ENOENT') { |
'use strict'; | ||
const path = require('path'); | ||
const fsp = require('fs').promises; | ||
const { MessageChannel, parentPort } = require('worker_threads'); | ||
const metautil = require('metautil'); | ||
@@ -11,200 +8,21 @@ class Scheduler { | ||
this.application = application; | ||
this.path = application.absolute('tasks'); | ||
this.tasks = new Map(); | ||
this.nextId = 0; | ||
this.executor = false; | ||
this.topics = new Map(); | ||
} | ||
async load() { | ||
this.executor = true; | ||
parentPort.on('message', async ({ type, name, data, port }) => { | ||
if (type !== 'task') return; | ||
if (name === 'add') port.postMessage({ id: await this.add(data) }); | ||
else if (name === 'remove') this.remove(data.id); | ||
else if (name === 'stop') this.stop(data.name); | ||
}); | ||
const now = metautil.nowDate(); | ||
try { | ||
const files = await fsp.readdir(this.path, { withFileTypes: true }); | ||
for (const file of files) { | ||
if (file.isDirectory()) continue; | ||
const { name } = file; | ||
if (!name.endsWith('.json') || name.startsWith('.')) continue; | ||
const base = path.basename(name, '.json'); | ||
const [date, id] = metautil.split(base, '-id-'); | ||
if (date === now) { | ||
const nextId = parseInt(id); | ||
if (nextId > this.nextId) this.nextId = nextId; | ||
} | ||
const filePath = path.join(this.path, name); | ||
const data = await fsp.readFile(filePath, 'utf8'); | ||
this.restore(JSON.parse(data)); | ||
} | ||
} catch (err) { | ||
this.application.console.error(err.stack); | ||
} | ||
} | ||
restore(record) { | ||
const { id, name, every, args, run } = record; | ||
const data = { | ||
id, | ||
name, | ||
every: metautil.parseEvery(every), | ||
success: undefined, | ||
result: null, | ||
error: null, | ||
lastStart: 0, | ||
lastEnd: 0, | ||
executing: false, | ||
runCount: 0, | ||
timer: null, | ||
args, | ||
run, | ||
}; | ||
this.tasks.set(id, data); | ||
return this.start(id); | ||
} | ||
async add(task) { | ||
if (!this.executor) { | ||
const { port1, port2 } = new MessageChannel(); | ||
const msg = { type: 'task', name: 'add', data: task, port: port1 }; | ||
return new Promise((resolve) => { | ||
port2.on('message', ({ id }) => { | ||
resolve(id); | ||
}); | ||
parentPort.postMessage(msg, [port1]); | ||
const { port1, port2 } = new MessageChannel(); | ||
return new Promise((resolve) => { | ||
port2.on('message', ({ id }) => { | ||
resolve(id); | ||
}); | ||
} | ||
const id = metautil.nowDate() + '-id-' + this.nextId.toString(); | ||
task.id = id; | ||
this.nextId++; | ||
const started = this.restore({ id, ...task }); | ||
if (!started) return id; | ||
const filePath = path.join(this.path, id + '.json'); | ||
try { | ||
const data = JSON.stringify(task); | ||
await fsp.writeFile(filePath, data); | ||
} catch (err) { | ||
this.application.console.error(err.stack); | ||
} | ||
return id; | ||
const msg = { name: 'task', action: 'add', port: port1, task }; | ||
parentPort.postMessage(msg, [port1]); | ||
}); | ||
} | ||
async remove(id) { | ||
if (!this.executor) { | ||
const msg = { type: 'task', name: 'remove', data: { id } }; | ||
parentPort.postMessage(msg); | ||
return; | ||
} | ||
const task = this.tasks.get(id); | ||
if (task) { | ||
this.tasks.delete(id); | ||
if (task.timer) { | ||
clearTimeout(task.timer); | ||
task.timer = null; | ||
} | ||
} | ||
const filePath = path.join(this.path, id + '.json'); | ||
try { | ||
await fsp.unlink(filePath); | ||
} catch (err) { | ||
if (err.code !== 'ENOENT') { | ||
this.application.console.error(err.stack); | ||
} | ||
} | ||
parentPort.postMessage({ name: 'task', action: 'remove', task: { id } }); | ||
} | ||
start(id) { | ||
const task = this.tasks.get(id); | ||
if (!task || task.timer) return false; | ||
const next = metautil.nextEvent(task.every); | ||
if (next === -1) { | ||
this.remove(id); | ||
return false; | ||
} | ||
if (next === 0) { | ||
this.execute(task, true); | ||
return true; | ||
} | ||
task.timer = setTimeout(() => { | ||
const once = task.every.interval === 0; | ||
this.execute(task, once); | ||
}, next); | ||
return true; | ||
} | ||
async enter(name) { | ||
let semaphore = this.topics.get(name); | ||
if (!semaphore) { | ||
const config = this.application.config.server.scheduler; | ||
const { concurrency, size, timeout } = config; | ||
semaphore = new metautil.Semaphore(concurrency, size, timeout); | ||
this.topics.set(name, semaphore); | ||
} | ||
return semaphore.enter(); | ||
} | ||
leave(name) { | ||
const semaphore = this.topics.get(name); | ||
if (!semaphore) return; | ||
if (semaphore.empty) { | ||
this.topics.delete(name); | ||
} | ||
semaphore.leave(); | ||
} | ||
async execute(task, once = false) { | ||
if (task.executing) { | ||
this.fail(task, 'Already started task'); | ||
return; | ||
} | ||
task.lastStart = Date.now(); | ||
task.executing = true; | ||
try { | ||
await this.enter(task.name); | ||
task.result = await this.application.invoke({ | ||
method: task.run, | ||
args: task.args, | ||
}); | ||
} catch (err) { | ||
task.error = err; | ||
if (err.message === 'Semaphore timeout') { | ||
this.fail(task, 'Scheduler queue is full'); | ||
} else { | ||
this.application.console.error(err.stack); | ||
} | ||
} finally { | ||
this.leave(task.name); | ||
} | ||
task.success = !task.error; | ||
task.lastEnd = Date.now(); | ||
task.executing = false; | ||
task.runCount++; | ||
task.timer = null; | ||
if (once) this.remove(task.id); | ||
else this.start(task.id); | ||
} | ||
fail(task, reason) { | ||
const { id, name, run, args } = task; | ||
const target = `${name} (${id}) ${run}(${JSON.stringify(args)})`; | ||
const msg = `${reason}, can't execute: ${target}`; | ||
this.application.console.error(msg); | ||
} | ||
stop(name = '') { | ||
if (!this.executor) { | ||
const msg = { type: 'task', name: 'stop', data: { name } }; | ||
parentPort.postMessage(msg); | ||
return; | ||
} | ||
for (const task of this.tasks.values()) { | ||
if (name !== '' && name !== task.name) continue; | ||
this.remove(task.id); | ||
} | ||
parentPort.postMessage({ name: 'task', action: 'stop', task: { name } }); | ||
} | ||
@@ -211,0 +29,0 @@ } |
'use strict'; | ||
const path = require('path'); | ||
const { Model, loadSchema } = require('metaschema'); | ||
const { loadModel, loadSchema } = require('metaschema'); | ||
const { Cache } = require('./cache.js'); | ||
@@ -15,3 +15,3 @@ | ||
await super.load(targetPath); | ||
this.model = await Model.load(targetPath); | ||
this.model = await loadModel(targetPath); | ||
} | ||
@@ -18,0 +18,0 @@ |
@@ -5,3 +5,3 @@ 'use strict'; | ||
const fsp = require('fs').promises; | ||
const { parentPort, threadId } = require('worker_threads'); | ||
const { parentPort, threadId, workerData } = require('worker_threads'); | ||
const { Config } = require('metaconfiguration'); | ||
@@ -11,2 +11,3 @@ const { Logger } = require('metalog'); | ||
const metavm = require('metavm'); | ||
const metautil = require('metautil'); | ||
const { notLoaded } = require('./dependencies.js'); | ||
@@ -20,3 +21,4 @@ const application = require('./application.js'); | ||
if (application.initialization) { | ||
console.info(`Can not start Application in worker ${threadId}`); | ||
console.info(`Initialization error in worker ${threadId}`); | ||
await application.shutdown(); | ||
process.exit(0); | ||
@@ -62,20 +64,11 @@ } | ||
const { balancer, ports = [] } = config.server; | ||
const servingThreads = ports.length + (balancer ? 1 : 0); | ||
await application.init(); | ||
let kind = 'worker'; | ||
if (threadId <= servingThreads) kind = 'server'; | ||
if (threadId === servingThreads + 1) kind = 'scheduler'; | ||
await application.init(kind); | ||
if (kind === 'server') { | ||
application.server = new Server(config.server, application); | ||
const { kind, port } = workerData; | ||
if (kind === 'server' || kind === 'balancer') { | ||
const options = { ...config.server, port, kind }; | ||
application.server = new Server(options, application); | ||
} | ||
console.info(`Application started in worker ${threadId}`); | ||
parentPort.postMessage({ type: 'event', name: 'started' }); | ||
parentPort.on('message', async ({ type, name }) => { | ||
if (type !== 'event' || name !== 'stop') return; | ||
const stop = async () => { | ||
if (application.finalization) return; | ||
@@ -85,3 +78,33 @@ console.info(`Graceful shutdown in worker ${threadId}`); | ||
process.exit(0); | ||
}; | ||
const invoke = async ({ exclusive, data, port }) => { | ||
const { method, args } = data; | ||
const handler = metautil.namespaceByPath(application.sandbox, method); | ||
if (!handler) { | ||
const error = new Error('Handler not found'); | ||
port.postMessage({ name: 'error', error }); | ||
return; | ||
} | ||
const msg = { name: 'invoke', status: 'done' }; | ||
try { | ||
const result = await handler(args); | ||
port.postMessage({ ...msg, data: result }); | ||
} catch (error) { | ||
port.postMessage({ name: 'error', error }); | ||
application.console.error(error.stack); | ||
} finally { | ||
if (exclusive) parentPort.postMessage(msg); | ||
} | ||
}; | ||
const handlers = { stop, invoke }; | ||
parentPort.on('message', async (msg) => { | ||
const handler = handlers[msg.name]; | ||
if (handler) handler(msg); | ||
}); | ||
console.info(`Application started in worker ${threadId}`); | ||
parentPort.postMessage({ name: 'started', kind }); | ||
})().catch(logError(`Can not start Application in worker ${threadId}`)); |
{ | ||
"name": "impress", | ||
"version": "2.6.10", | ||
"version": "3.0.0-alpha.1", | ||
"author": "Timur Shemsedinov <timur.shemsedinov@gmail.com>", | ||
@@ -62,22 +62,22 @@ "description": "Enterprise application server for Node.js", | ||
"types": "tsc -p types/tsconfig.json", | ||
"lint": "eslint . && prettier -c \"**/*.js\" \"**/*.json\" \"**/*.md\" \"**/*.yml\" \"**/*.ts\"", | ||
"fmt": "prettier --write \"**/*.js\" \"**/*.json\" \"**/*.md\" \"**/*.yml\" \"**/*.ts\"" | ||
"lint": "eslint . && prettier -c \"**/*.js\" \"**/*.json\" \"**/*.md\" \"**/*.ts\"", | ||
"fmt": "prettier --write \"**/*.js\" \"**/*.json\" \"**/*.md\" \"**/*.ts\"" | ||
}, | ||
"engines": { | ||
"node": "^12.9 || 14 || 16 || 17 || 18" | ||
"node": "14 || 16 || 18" | ||
}, | ||
"dependencies": { | ||
"metacom": "^2.0.7", | ||
"metaconfiguration": "^2.1.6", | ||
"metacom": "^3.0.0-alpha.1", | ||
"metaconfiguration": "^2.1.7", | ||
"metalog": "^3.1.8", | ||
"metaschema": "^1.4.1", | ||
"metaschema": "^2.0.2", | ||
"metautil": "^3.5.20", | ||
"metavm": "^1.1.0", | ||
"metavm": "^1.2.0", | ||
"metawatch": "^1.0.5" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^17.0.31", | ||
"@types/node": "^18.0.0", | ||
"@types/ws": "^8.5.3", | ||
"eslint": "^8.15.0", | ||
"eslint-config-metarhia": "^7.0.1", | ||
"eslint": "^8.18.0", | ||
"eslint-config-metarhia": "^8.1.0", | ||
"eslint-config-prettier": "^8.5.0", | ||
@@ -87,5 +87,5 @@ "eslint-plugin-import": "^2.26.0", | ||
"metatests": "^0.8.2", | ||
"prettier": "^2.6.2", | ||
"typescript": "^4.6.4" | ||
"prettier": "^2.7.1", | ||
"typescript": "^4.7.4" | ||
} | ||
} |
({ | ||
caption: { type: 'string', required: false }, | ||
description: { type: 'string', required: false }, | ||
access: { type: 'Access', required: false }, | ||
parameters: { type: 'Schema', required: false }, | ||
validate: { type: 'function', required: false }, | ||
access: '?Access', | ||
parameters: '?Schema', | ||
validate: '?Function', | ||
timeout: { type: 'number', required: false }, | ||
queue: { type: 'QueueParameters', required: false }, | ||
serializer: { type: 'Serializer', required: false }, | ||
protocols: { array: 'Protocols', required: false }, | ||
queue: '?QueueParameters', | ||
serializer: '?Serializer', | ||
protocols: '?Protocols', | ||
deprecated: { type: 'boolean', required: false }, | ||
method: 'AsyncFuction', | ||
returns: { type: 'Schema', required: false }, | ||
assert: { type: 'function', required: false }, | ||
script: { type: 'function', required: false }, | ||
examples: { array: 'Example', required: false }, | ||
application: { type: 'Application', required: false }, | ||
returns: '?Schema', | ||
assert: '?Function', | ||
script: '?Function', | ||
examples: '?Example', | ||
application: '?Application', | ||
}); |
@@ -12,3 +12,3 @@ import { Schema } from 'metaschema'; | ||
type Protocols = 'http' | 'https' | 'ws' | 'wss'; | ||
type AsyncFuction = (...args: Array<any>) => Promise<any>; | ||
type AsyncFunction = (...args: Array<any>) => Promise<any>; | ||
type Example = { | ||
@@ -23,3 +23,3 @@ parameters: object; | ||
application: Application; | ||
method?: AsyncFuction; | ||
method?: AsyncFunction; | ||
parameters?: Schema; | ||
@@ -26,0 +26,0 @@ returns?: Schema; |
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
69971
30
1583
1
12
2
+ Addedmetacom@3.2.3(transitive)
+ Addedmetaschema@2.2.0(transitive)
- Removedmetacom@2.0.8(transitive)
- Removedmetaschema@1.4.1(transitive)
Updatedmetacom@^3.0.0-alpha.1
Updatedmetaconfiguration@^2.1.7
Updatedmetaschema@^2.0.2
Updatedmetavm@^1.2.0