refactor: new truss API

This commit is contained in:
cha0s 2018-12-23 07:57:32 -06:00
parent e68d3e8e5a
commit 89f98034d3
11 changed files with 131 additions and 4131 deletions

1
TODO.txt Normal file
View File

@ -0,0 +1 @@
- webpack babel config should use .babelrc

View File

@ -1,75 +0,0 @@
const path = require('path');
const {spawnSync} = require('child_process');
const fs = require('fs');
const dotenv = require('dotenv');
const mkdirp = require('mkdirp');
const {emitObject, emitString} = require('./compose');
['.common.env', '.production.env', '.env'].forEach((filename) => {
dotenv.config({path: path.join(__dirname, filename)});
});
const cwd = process.cwd();
const distPath = path.join(cwd, 'dist', 'production');
const services = process.env.SERVICES.split(',');
services.push('gateway');
const composeFile = emitString(emitObject(services));
mkdirp.sync(distPath);
fs.writeFileSync(
path.join(distPath, 'docker-compose.yml'), composeFile
);
fs.copyFileSync(
path.join(cwd, '.common.env'),
path.join(distPath, '.env'),
);
fs.appendFileSync(
path.join(distPath, '.env'),
fs.readFileSync(path.join(cwd, '.production.env')),
);
fs.appendFileSync(
path.join(distPath, '.env'),
fs.readFileSync(path.join(cwd, '.env')),
);
for (const service of services) {
const servicePath = path.join(cwd, 'services', service);
const serviceDistPath = path.join(distPath, service);
mkdirp.sync(serviceDistPath);
spawnSync('docker', [
'run',
'--env-file', './.common.env',
'--env-file', './.production.env',
'--env-file', './.env',
'-e', 'DEBUG=truss:*',
'-e', 'DEBUG_COLORS=1',
'-e', 'DEBUG_HIDE_DATE=1',
'-e', 'NODE_PRESERVE_SYMLINKS=1',
'-v', `${path.join(cwd, 'lib')}:/var/node/lib:ro`,
'-v', `${servicePath}:/var/node/src:ro`,
'-v', `${serviceDistPath}:/var/node/dist`,
'docker.i12e.cha0s.io/cha0s6983/truss-dev',
'yarn', 'run', 'build',
], {
stdio: 'inherit',
});
require('rimraf').sync(
path.join(serviceDistPath, '{node_modules,package.json,yarn.lock}')
)
}

161
bundle.js
View File

@ -1,161 +0,0 @@
const path = require('path');
const {fork} = require('child_process');
const chalk = require('chalk');
const dotenv = require('dotenv');
['.common.env', '.dev.env', '.env'].forEach((filename) => {
dotenv.config({path: path.join(__dirname, filename)});
});
const services = process.env.SERVICES.split(',');
services.push('gateway');
const longestServiceNameLength = services.reduce((l, r) => {
return (r.length > l.length) ? r : l;
}, '').length;
let color = 1;
const colors = [
'redBright', 'greenBright', 'yellowBright', 'blueBright', 'magentaBright',
'cyanBright', 'whiteBright', 'red', 'green', 'yellow', 'blue', 'magenta',
'cyan', 'white', 'gray',
];
const children = [];
const childrenOfService = {};
const bundleServices = (process.env.BUNDLE_SERVICES || '').split(',');
const serviceCounts = bundleServices.reduce((l, r) => {
const [service, count = 1] = r.split(':');
if (service in l) { l[service] = count; }
return l;
}, services.reduce((l, r) => {
l[r] = 1;
return l;
}, {}));
const cwd = process.cwd();
services.forEach((service) => {
for (let i = 0; i < serviceCounts[service]; ++i) {
forkService(service);
}
});
function forkService(service) {
const servicePath = path.join(cwd, 'services', service);
let index;
const colorizer = chalk[colors[color++]];
const dresser = (line) => {
const prefix = colorizer(`${(index + '').padStart(2, '0')} » ${
service.padEnd(longestServiceNameLength)
} |`);
console.error(`${prefix} ${line}`);
};
const logger = (message) => {
message.split('\n').forEach(dresser);
};
const child = fork(servicePath, [], {
env: Object.assign({
SOURCE_PATH: servicePath,
}, process.env),
stdio: 'pipe',
});
const forwardStream = (data) => {
logger(data.toString('utf8').replace(/\n$/, ''));
};
child.stdout.on('data', forwardStream);
child.stderr.on('data', forwardStream);
childrenOfService[service] = childrenOfService[service] || [];
childrenOfService[service].push(child);
index = children.length;
logger(`forking...`);
children.push(child);
const createChildActions = (child) => {
const actions = createActions(child);
return (action) => {
actions[action.type](action);
};
};
child.on('message', createChildActions(child));
}
function createActions (child) {
return {
listening: () => (child.listening = true),
connect: ({payload: {clientId, service}}) => {
const candidate = randomService(service);
if (candidate) {
candidate.send({
type: 'connection_from',
payload: {
clientId,
from: children.indexOf(child),
},
});
child.send({
type: 'connection_to',
payload: {
clientId,
to: children.indexOf(candidate),
},
});
}
else {
child.send({
type: 'connection_error',
payload: {
clientId,
},
});
}
},
client_send: ({payload}) => {
children[payload.to].send({
type: 'server_recv',
payload: {
clientId: payload.clientId,
data: payload.data,
},
});
},
server_send: ({payload}) => {
children[payload.to].send({
type: 'client_recv',
payload: {
clientId: payload.clientId,
data: payload.data,
},
});
},
};
}
function randomService(service) {
const candidates = [...childrenOfService[service]];
while (candidates.length > 0) {
const index = Math.floor(Math.random() * candidates.length);
const [candidate] = candidates.splice(index, 1);
if (candidate.listening) {
return candidate;
}
}
}

View File

@ -1,66 +0,0 @@
const yaml = require('js-yaml');
exports.emitString = function(object) {
return yaml.safeDump(object);
}
exports.emitObject = function(services) {
services = services.slice(0);
if (-1 === services.indexOf('gateway')) {
services.push('gateway');
}
const composeFile = {
version: '3',
services: {},
};
for (const service of services) {
composeFile.services[service] = emitService(service);
const modulePath = `./services/${service}/compose`;
try {
require(modulePath)(composeFile.services[service], composeFile);
}
catch (error) {
const message = `Cannot find module '${modulePath}'`;
if (message !== error.message) {
console.error(error);
process.exit(1);
}
}
}
return composeFile;
}
function emitService(service) {
const definition = {};
definition.image = `docker.i12e.cha0s.io/cha0s6983/truss-${
'production' === process.env.NODE_ENV ? 'production': 'dev'
}`;
if ('production' === process.env.NODE_ENV) {
definition.env_file = [
'.env',
];
}
else {
definition.env_file = [
'.common.env', `.dev.env`, '.env',
];
}
if ('production' === process.env.NODE_ENV) {
definition.volumes = [
`./${service}:/var/node/dist`,
];
}
else {
definition.volumes = [
'./lib:/var/node/lib',
`./services/${service}:/var/node/src`,
`./dist/dev/${service}:/var/node/dist`,
];
}
return definition;
}

44
dev.js
View File

@ -1,44 +0,0 @@
const {spawn} = require('child_process');
const path = require('path');
const dotenv = require('dotenv');
['.common.env', '.dev.env', '.env'].forEach((filename) => {
dotenv.config({path: path.join(__dirname, filename)});
});
const debug = require('debug')('truss:dev');
const {emitObject, emitString} = require('./compose');
const services = process.env.SERVICES.split(',');
services.push('gateway');
const composeFile = emitString(emitObject(services));
debug('Compose file:');
debug(composeFile);
debug('Ensuring dist directory exists...');
require('mkdirp').sync(path.join(process.cwd(), 'dist', 'dev'));
['down', 'up'].reduce((promise, op) => {
const child = spawn('docker-compose', [
'-f', '-',
op,
], {
env: process.env,
stdio: ['pipe', 'inherit', 'inherit'],
});
child.stdin.write(composeFile);
return promise.then(() => {
child.stdin.end();
return new Promise((resolve) => {
child.on('exit', () => {
resolve();
});
});
});
}, Promise.resolve());

View File

@ -4,20 +4,14 @@
"description": "", "description": "",
"author": "cha0s", "author": "cha0s",
"scripts": { "scripts": {
"bundle": "node bundle.js", "bundle": "node -e '' -r @truss/core/task/bundle",
"build": "node build.js", "build": "node -e '' -r @truss/docker/task/build",
"start": "npm run dev", "start": "npm run dev",
"dev": "node dev.js" "dev": "node -e '' -r @truss/docker/task/development"
}, },
"dependencies": { "dependencies": {
"@truss/truss": "^1.2.5", "@truss/core": "1.x",
"chalk": "^2.4.1", "@truss/docker": "1.x"
"debug": "^3.1.0",
"dotenv": "^6.0.0",
"js-yaml": "^3.12.0",
"mkdirp": "^0.5.1",
"rimraf": "^2.6.2",
"supports-color": "^5.5.0"
}, },
"license": "UNLICENSED" "license": "UNLICENSED"
} }

View File

@ -0,0 +1,35 @@
const {sendActionToService} = require('@truss/comm');
module.exports = (serviceMap) => ({
'truss/hook-services': ({payload: {hook, args}}) => {
if (!(hook in serviceMap.hooks)) { return []; }
return serviceMap.hooks[hook];
},
'truss/invoke': ({payload: {hook, args}}) => {
if (!(hook in serviceMap.hooks)) { return {}; }
return invokeHook(serviceMap.hooks[hook], hook, args);
},
'truss/invoke-flat': ({payload: {hook, args}}) => {
if (!(hook in serviceMap.hooks)) { return []; }
return invokeHookFlat(serviceMap.hooks[hook], hook, args);
},
});
function invokeHookFlat(services, hook, args) {
debug(`invoking hook flat(${hook}(${JSON.stringify(args, null, ' ')}))...`);
invokeHookFlatInternal(services, hook, args);
}
function invokeHookFlatInternal(services, hook, args) {
const action = {type: 'truss/hook', payload: {hook, args}};
return Promise.all(services.map((service) => {
return sendActionToService(action, service);
}));
}
function invokeHook(services, hook, args) {
debug(`invoking hook ${hook}(${JSON.stringify(args, null, ' ')})...`);
return invokeHookFlatInternal(services, hook, args).then((result) => {
return result.reduce((l, r, i) => (l[services[i]] = r, l), {});
});
}

View File

@ -2,7 +2,7 @@ const http = require('http');
const debug = require('debug')('truss:gateway'); const debug = require('debug')('truss:gateway');
import {createDispatcher, sendActionToService} from '@truss/truss'; const {createDispatcher, sendActionToService} = require('@truss/comm');
let listener = require('./listener').default; let listener = require('./listener').default;
@ -21,24 +21,27 @@ const schemas$ = services.map((service) => {
// build service map // build service map
const reduceServiceMap$ = Promise.all(schemas$).then((schemas) => { const reduceServiceMap$ = Promise.all(schemas$).then((schemas) => {
return schemas.reduce((l, r, i) => { return schemas.reduce((serviceMap, schema, i) => {
for (const type of (r.executors || [])) { const service = services[i];
if (l.executors[type]) { for (const type of (schema.executors || [])) {
throw new Error(` if (serviceMap.executors[type]) {
Only one executor may be specified per action type! "${services[i]}" tried to throw new RangeError(`
register an executor but "${l.executors[type]}" already registered one for Only one executor may be specified per action type! "${service}"
${type}. tried to register an executor but "${serviceMap.executors[type]}"
`); already registered one for "${type}".
`.replace(/\s+/g, ' ').trim());
} }
l.executors[type] = services[i]; serviceMap.executors[type] = service;
} }
for (const type of (r.listeners || [])) { for (const type of (schema.listeners || [])) {
(l.listeners[type] = l.listeners[type] || []).push(services[i]); serviceMap.listeners[type] = serviceMap.listeners[type] || [];
serviceMap.listeners[type].push(service);
} }
for (const hook of (r.hooks || [])) { for (const hook of (schema.hooks || [])) {
(l.hooks[hook] = l.hooks[hook] || []).push(services[i]); serviceMap.hooks[hook] = serviceMap.hooks[hook] || [];
serviceMap.hooks[hook].push(service);
} }
return l; return serviceMap;
}, { }, {
executors: {}, executors: {},
hooks: {}, hooks: {},
@ -51,53 +54,27 @@ const httpServer = http.createServer();
reduceServiceMap$.then((serviceMap) => { reduceServiceMap$.then((serviceMap) => {
debug(`service map: ${JSON.stringify(serviceMap, null, ' ')}`); debug(`service map: ${JSON.stringify(serviceMap, null, ' ')}`);
const port = process.env.GATEWAY_PORT || 8000; const dispatcher = createDispatcher();
httpServer.listen(port); dispatcher.setArgs(serviceMap);
httpServer.on('listening', () => { dispatcher.lookupActions(require('./actions'));
debug(`HTTP listening on port ${port}...`); if (module.hot) {
}) module.hot.accept('./actions', () => {
httpServer.on('request', (req, res) => { dispatcher.lookupActions(require('./actions'));
listener(serviceMap, req, res); });
}
const server = dispatcher.connect();
server.on('listening', () => {
const port = process.env.GATEWAY_PORT || 8000;
httpServer.listen(port);
httpServer.on('listening', () => {
debug(`HTTP listening on port ${port}...`);
})
httpServer.on('request', (req, res) => {
listener(serviceMap, req, res);
});
}); });
// hooks
createDispatcher({
'truss/hook-services': ({payload: {hook, args}}) => {
if (!(hook in serviceMap.hooks)) { return []; }
return serviceMap.hooks[hook];
},
'truss/invoke': ({payload: {hook, args}}) => {
if (!(hook in serviceMap.hooks)) { return {}; }
return invokeHook(serviceMap.hooks[hook], hook, args);
},
'truss/invoke-flat': ({payload: {hook, args}}) => {
if (!(hook in serviceMap.hooks)) { return []; }
return invokeHookFlat(serviceMap.hooks[hook], hook, args);
},
}).connect();
}).catch(console.error); }).catch(console.error);
function invokeHookFlat(services, hook, args) {
debug(`invoking hook flat(${hook}(${JSON.stringify(args, null, ' ')}))...`);
invokeHookFlatInternal(services, hook, args);
}
function invokeHookFlatInternal(services, hook, args) {
const action = {type: 'truss/hook', payload: {hook, args}};
return Promise.all(services.map((service) => {
return sendActionToService(action, service);
}));
}
function invokeHook(services, hook, args) {
debug(`invoking hook ${hook}(${JSON.stringify(args, null, ' ')})...`);
return invokeHookFlatInternal(services, hook, args).then((result) => {
return result.reduce((l, r, i) => (l[services[i]] = r, l), {});
});
}
if (module.hot) { if (module.hot) {
module.hot.accept('./listener', () => { module.hot.accept('./listener', () => {
listener = require('./listener').default; listener = require('./listener').default;

View File

@ -1,40 +1,37 @@
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const debug = require('debug')('truss:gateway:listener'); import {sendActionToService} from '@truss/comm';
import {sendActionToService} from '@truss/truss'; const debug = require('debug')('truss:gateway:listener');
const parser = bodyParser.json(); const parser = bodyParser.json();
export default function(serviceMap, req, res) { parser(req, res, () => { export default function(serviceMap, req, res) { parser(req, res, () => {
debug(`HTTP ${req.method} ${req.url}`); debug(`HTTP ${req.method} ${req.url}`);
// map to action // map to action
const action = {type: undefined, payload: {url: req.url}}; const action = {type: undefined, payload: {url: req.url}};
switch (req.method) { switch (req.method) {
case 'POST': case 'POST':
// truss action
action.type = req.url.startsWith('/truss/action/') ? if (req.url.startsWith('/truss/action/')) {
req.url.substr('/truss/action/'.length): 'truss/http-post' action.type = req.url.substr('/truss/action/'.length);
; }
// truss/http-post
else {
action.type = 'truss/http-post';
}
action.payload.body = req.body; action.payload.body = req.body;
break; break;
default: default:
// truss/http-(get|delete|...)
// truss/http-(GET|DELETE|...)
action.type = `truss/http-${req.method.toLowerCase()}`; action.type = `truss/http-${req.method.toLowerCase()}`;
action.payload.headers = req.headers; action.payload.headers = req.headers;
break; break;
} }
// listeners... // listeners...
for (service of serviceMap.listeners[action.type] || []) { for (service of serviceMap.listeners[action.type] || []) {
sendActionToService(action, service).done(); sendActionToService(action, service).done();
} }
// executor // executor
if (!(action.type in serviceMap.executors)) { if (!(action.type in serviceMap.executors)) {
debug(`No executor for ${action.type}!`); debug(`No executor for ${action.type}!`);
@ -46,12 +43,9 @@ export default function(serviceMap, req, res) { parser(req, res, () => {
} }
sendActionToService( sendActionToService(
action, serviceMap.executors[action.type] action, serviceMap.executors[action.type]
).then(({status, headers, html}) => { ).then(({status, headers, html}) => {
// deliver // deliver
res.writeHead(status || 200, headers); res.writeHead(status || 200, headers);
res.end(html); res.end(html);
}).catch(console.error); }).catch(console.error);
})}; })};

View File

@ -4,14 +4,15 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "node -e '' -r '@truss/truss/task/build'", "build": "node -e '' -r '@truss/webpack/task/build'",
"default": "yarn run dev", "default": "yarn run dev",
"dev": "node -e '' -r '@truss/truss/task/scaffold'" "dev": "node -e '' -r '@truss/webpack/task/scaffold'"
}, },
"author": "cha0s", "author": "cha0s",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@truss/truss": "1.x", "@truss/comm": "1.x",
"@truss/webpack": "1.x",
"body-parser": "1.18.3", "body-parser": "1.18.3",
"debug": "3.1.0" "debug": "3.1.0"
} }

3734
yarn.lock

File diff suppressed because it is too large Load Diff