UNPKG

redioactive

Version:

Reactive streams for chaining overlapping promises.

579 lines (578 loc) 27.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.httpTarget = void 0; /* eslint-disable @typescript-eslint/no-extra-semi */ const redio_1 = require("./redio"); const http_common_1 = require("./http-common"); const http_1 = __importStar(require("http")); const https_1 = __importStar(require("https")); const url_1 = require("url"); const dns_1 = require("dns"); const servers = {}; const serversS = {}; const streamIDs = {}; function isPush(c) { return c.type === 'push'; } function noMatch(req, res) { if (res.writableEnded) return; const message = {}; if (req.url && req.method && (req.method === 'GET' || req.method === 'POST')) { const url = new url_1.URL(req.url); if (!Object.keys(streamIDs).find((x) => url.pathname.startsWith(x))) { message.status = 404; message.message = `Redioactive: HTTP/S target: No stream available for pathname "${url.pathname}".`; } } else { message.status = req.method ? 405 : 500; message.message = req.method ? `Redioactive: HTTP/S target: Method ${req.method} not allowed for resource` : `Redioactive: HTTP/S target: Cannot determine method type`; } if (message.status) { res.statusCode = message.status; const result = JSON.stringify(message, null, 2); res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Length', Buffer.byteLength(result, 'utf8')); res.end(result, 'utf8'); } } function httpTarget(uri, options) { if (!options) throw new Error('HTTP options must be specified - for now.'); // Assume pull for now const url = new url_1.URL(uri, `http${options.httpsPort ? 's' : ''}://localhost:${options.httpPort || options.httpsPort}`); let info; let nextExpectedId = -1; let pushResolver; let pushPull = () => { /* void */ }; let pushPullP = Promise.resolve(); url.pathname = url.pathname.replace(/\/+/g, '/'); if (url.pathname.endsWith('/')) { url.pathname = url.pathname.slice(0, -1); } if (uri.toLowerCase().startsWith('http')) { info = (0, redio_1.literal)({ type: 'pull', protocol: uri.toLowerCase().startsWith('https') ? http_common_1.ProtocolType.https : http_common_1.ProtocolType.http, root: url.pathname, idType: http_common_1.IdType.counter, body: http_common_1.BodyType.primitive, delta: http_common_1.DeltaType.one, manifest: {} }); let currentId = 0; let nextId = 0; let initDone = () => { /* void */ }; // eslint-disable-next-line @typescript-eslint/no-explicit-any let initError = () => { /* void */ }; const initialized = new Promise((resolve, reject) => { initDone = resolve; initError = reject; }); let [starter, manifestly] = [false, false]; const protocol = info.protocol === http_common_1.ProtocolType.http ? http_1.default : https_1.default; const agent = new protocol.Agent(Object.assign({ keepAlive: true, host: url.hostname }, (options && options.requestOptions) || {})); dns_1.promises .lookup(url.hostname) .then((host) => { url.hostname = host.address; }, () => { /* Does not matter - fall back on given hostname and default DNS behaviour */ }) .then(() => { const startReq = protocol.request(Object.assign((options && options.requestOptions) || {}, { hostname: url.hostname, protocol: url.protocol, port: url.port, path: `${info.root}/start`, method: 'GET', agent }), (res) => { const location = res.headers['location']; if (res.statusCode !== 302 || location === undefined) { throw new Error(`Redioactive: HTTP/S target: Failed to retrieve stream start details for "${info.root}".`); } res.on('error', initError); currentId = location.slice(location.lastIndexOf('/') + 1); nextId = currentId; let idType = res.headers['redioactive-idtype']; if (Array.isArray(idType)) { idType = idType[0]; } info.idType = idType || http_common_1.IdType.counter; let delta = res.headers['redioactive-deltatype']; if (Array.isArray(delta)) { delta = delta[0]; } info.delta = delta || http_common_1.DeltaType.one; let body = res.headers['redioactive-bodytype']; if (Array.isArray(body)) { body = body[0]; } info.body = body || http_common_1.BodyType.primitive; starter = true; if (manifestly) { initDone(); } }); startReq.on('error', initError); startReq.end(); const maniReq = protocol.request(Object.assign((options && options.requestOptions) || {}, { hostname: url.hostname, protocol: url.protocol, port: url.port, path: `${info.root}/manifest.json`, method: 'GET', agent }), (res) => { if (res.statusCode !== 200 || res.headers['content-type'] !== 'application/json') { throw new Error(`Redioactive: HTTP/S target: Failed to retrieve manifest for stream "${info.root}".`); } res.setEncoding('utf8'); let manifestStr = ''; res.on('data', (chunk) => { manifestStr += chunk; }); res.on('end', () => { info.manifest = JSON.parse(manifestStr); manifestly = true; if (starter) { initDone(); } }); }); maniReq.on('error', initError); maniReq.end(); }); // 1. Do all the initialization and options checks // 2. Make the /start request and set up state // 2a. Get the manifest let streamCounter = 0; return () => new Promise((resolve, reject) => { streamCounter++; // 3. Pull the value // 4. Create the object // 5. Get ready for the next pull or detect end // 6. resolve initialized.then(() => { const valueReq = protocol.request(Object.assign((options && options.requestOptions) || {}, { hostname: url.hostname, protocol: url.protocol, port: url.port, path: `${info.root}/${nextId.toString()}`, method: 'GET', agent }), (res) => { if (res.statusCode !== 200) { throw new Error(`Redioactive: HTTP/S target: Unexpected response code ${res.statusCode}.`); } currentId = nextId; if (!res.headers['content-length']) { throw new Error('Redioactive: HTTP/S target: Content-Length header expected'); } let value = info.body === http_common_1.BodyType.blob ? Buffer.allocUnsafe(+res.headers['content-length']) : ''; const streamSaysNextIs = res.headers['redioactive-nextid']; nextId = Array.isArray(streamSaysNextIs) ? +streamSaysNextIs[0] : streamSaysNextIs ? +streamSaysNextIs : currentId; if (info.body !== http_common_1.BodyType.blob) { res.setEncoding('utf8'); } let bufferPos = 0; res.on('data', (chunk) => { if (!chunkIsString(value)) { bufferPos += chunk.copy(value, bufferPos); } else { value += chunk; } }); res.on('end', () => { let t; if (info.body === http_common_1.BodyType.blob) { const s = {}; if (value.length > 0) { s[(options && options.blob) || 'blob'] = value; } let details = res.headers['redioactive-details'] || '{}'; if (Array.isArray(details)) { details = details[0]; } t = Object.assign(s, JSON.parse(details)); } else { t = JSON.parse(value); } if (typeof t === 'object') { if (Object.keys(t).length === 1 && Object.prototype.hasOwnProperty.call(t, 'end') && t['end'] === true) { resolve(redio_1.end); return; } if (options && typeof options.manifest === 'string') { ; t[options.manifest] = info.manifest; } if (options && options.seqId) { ; t[options.seqId] = currentId; } if (options && options.debug) { ; t['debug_streamCounter'] = streamCounter; t['debug_status'] = res.statusCode; } if (options && typeof options.delta === 'string') { switch (info.delta) { case http_common_1.DeltaType.one: ; t[options.delta] = 1; break; case http_common_1.DeltaType.variable: case http_common_1.DeltaType.fixed: ; t[options.delta] = nextId - currentId; break; default: break; } } } resolve(t); }); res.on('error', reject); }); valueReq.on('error', reject); valueReq.end(); }, reject); // initialized promise complete }); } else { // PUSH let server = undefined; let serverS = undefined; const root = url.pathname; if (options.httpPort) { if (options && !options.extraStreamRoot) { // Set with first element streamIDs[root] = { httpPort: options.httpPort, httpsPort: options.httpsPort }; } server = servers[options.httpPort]; if (!server) { server = options.serverOptions ? (0, http_1.createServer)(options.serverOptions) : (0, http_1.createServer)(); server.keepAliveTimeout = (options && options.keepAliveTimeout) || 5000; servers[options.httpPort] = server; server.listen(options.httpPort, () => { console.log(`Redioactive: HTTP/S target: HTTP push server for stream ${root} listening on ${options.httpPort}`); }); } server.on('request', pushRequest); server.on('error', (err) => { // TODO interrupt and push error? console.error(err); }); } if (options.httpsPort) { if (options && !options.extraStreamRoot) { streamIDs[root] = { httpPort: options.httpPort, httpsPort: options.httpsPort }; } serverS = serversS[options.httpsPort]; if (!serverS) { serverS = options.serverOptions ? (0, https_1.createServer)(options.serverOptions) : (0, https_1.createServer)(); serverS.keepAliveTimeout = (options && options.keepAliveTimeout) || 5000; serversS[options.httpsPort] = serverS; serverS.listen(options.httpsPort, () => { console.log(`Redioactive: HTTP/S source: HTTPS server push for stream ${root} listening on ${options.httpsPort}`); }); } serverS.on('request', pushRequest); serverS.on('error', (err) => { // TODO interrupt and push error? console.error(err); }); } info = (0, redio_1.literal)({ type: 'push', protocol: url.protocol === 'https:' ? http_common_1.ProtocolType.https : http_common_1.ProtocolType.http, root, idType: http_common_1.IdType.counter, body: http_common_1.BodyType.primitive, delta: http_common_1.DeltaType.one, manifest: {}, server, serverS, httpPort: options.httpPort, httpsPort: options.httpsPort }); return () => // eslint-disable-next-line @typescript-eslint/no-unused-vars new Promise((resolve, _reject) => { // console.log('Calling pushPull()') pushPull(); pushResolver = resolve; }); } // end PUSH function pushRequest(req, res) { // TODO make sure error processings works as expected req.on('error', console.error); res.on('error', console.error); if (req.url && isPush(info)) { let path = req.url.replace(/\/+/g, '/'); if (path.endsWith('/')) { path = path.slice(0, -1); } if (path.startsWith(info.root)) { const id = path.slice(info.root.length + 1); console.log(`Processing ${req.method} with url ${req.url} and ${typeof id} id ${id}, expected ${nextExpectedId}`); if (req.method === 'POST') { if (id === 'end') { return endStream(req, res); } if (id === 'manifest.json') { let value = ''; req.setEncoding('utf8'); req.on('data', (chunk) => { value += chunk; }); let idType = req.headers['redioactive-idtype']; if (Array.isArray(idType)) { idType = idType[0]; } info.idType = idType || http_common_1.IdType.counter; let delta = req.headers['redioactive-deltatype']; if (Array.isArray(delta)) { delta = delta[0]; } info.delta = delta || http_common_1.DeltaType.one; let body = req.headers['redioactive-bodytype']; if (Array.isArray(body)) { body = body[0]; } info.body = body || http_common_1.BodyType.primitive; let nextId = req.headers['redioactive-nextid']; if (Array.isArray(nextId)) { nextId = nextId[0]; } if (nextId) { nextExpectedId = info.idType === 'string' ? nextId : +nextId; } req.on('end', () => { info.manifest = JSON.parse(value); res.statusCode = 201; res.setHeader('Location', `${info.root}/manifest.json`); res.end(); }); return; } if (id === nextExpectedId.toString()) { if (!req.headers['content-length']) { throw new Error('Redioactive: HTTP/S target: Content-Length header expected'); } let value = info.body === http_common_1.BodyType.blob ? Buffer.allocUnsafe(+req.headers['content-length']) : ''; const streamSaysNextIs = req.headers['redioactive-nextid']; nextExpectedId = Array.isArray(streamSaysNextIs) ? +streamSaysNextIs[0] : streamSaysNextIs ? +streamSaysNextIs : id; if (info.body !== http_common_1.BodyType.blob) { req.setEncoding('utf8'); } let bufferPos = 0; // console.log('About to wait on pushPull', req.url, pushPullP) pushPullP.then(() => { pushPullP = new Promise((resolve) => { pushPull = resolve; }); req.on('data', (chunk) => { if (!chunkIsString(value)) { bufferPos += chunk.copy(value, bufferPos); } else { value += chunk; } }); req.on('end', () => { let t; if (info.body === http_common_1.BodyType.blob) { const s = {}; if (value.length > 0) { s[(options && options.blob) || 'blob'] = value; } let details = req.headers['redioactive-details'] || '{}'; if (Array.isArray(details)) { details = details[0]; } t = Object.assign(s, JSON.parse(details)); } else { t = JSON.parse(value); } if (typeof t === 'object') { if (Object.keys(t).length === 1 && Object.prototype.hasOwnProperty.call(t, 'end') && t['end'] === true) { console.log('This is the end my friend!'); pushResolver(redio_1.end); endStream(req); res.statusCode = 200; res.end(); return; } if (options && typeof options.manifest === 'string') { ; t[options.manifest] = info.manifest; } if (options && options.seqId) { ; t[options.seqId] = id; } if (options && typeof options.delta === 'string') { switch (info.delta) { case http_common_1.DeltaType.one: ; t[options.delta] = 1; break; case http_common_1.DeltaType.variable: case http_common_1.DeltaType.fixed: ; t[options.delta] = nextExpectedId - +id; break; default: break; } } } pushResolver(t); res.statusCode = 201; res.setHeader('Location', `${info.root}/$id`); res.end(); }); }); // Read data only when ready } return; } if (req.method === 'GET') { if (id === 'debug.json') { return debug(res); } if (id === 'end') { return endStream(req, res); } if (id === 'manifest.json') { return sendManifest(res); } } } } noMatch(req, res); } function debug(res) { const keys = []; // for (const key of tChest.keys()) { // keys.push(key) // } const debugInfo = { info, uri, url, streamIDs, options, // ended, // lowestOfTheLow, // lowWaterMark, // highWaterMark, // nextId, keys }; const debugString = JSON.stringify(debugInfo, null, 2); res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Length', `${Buffer.byteLength(debugString, 'utf8')}`); res.end(debugString, 'utf8'); } function sendManifest(res) { const maniString = JSON.stringify(info.manifest); res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Length', `${Buffer.byteLength(maniString, 'utf8')}`); res.end(maniString, 'utf8'); } function endStream(req, res) { const isSSL = Object.prototype.hasOwnProperty.call(req.socket, 'encrypted'); const port = req.socket.localPort; pushPull(); pushPullP = Promise.resolve(); if (isPush(info)) { try { info.server && info.server.close(() => { isPush(info) && delete streamIDs[info.root]; console.log(`Redioactive: HTTP/S target: ${isSSL ? 'HTTPS' : 'HTTP'} push server for stream ${(isPush(info) && info.root) || 'unknown'} on port ${port} closed.`); }); info.serverS && info.serverS.close(() => { isPush(info) && delete streamIDs[info.root]; console.log(`Redioactive: HTTP/S target: ${isSSL ? 'HTTPS' : 'HTTP'} push server for stream ${(isPush(info) && info.root) || 'unknown'} on port ${port} closed.`); }); if (res) { res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Length', 2); res.end('OK', 'utf8'); } delete streamIDs[info.root]; if (!Object.values(streamIDs).some((x) => isPush(info) && ((x.httpPort && x.httpPort === info.httpPort) || (x.httpsPort && x.httpsPort === info.httpsPort)))) { isPush(info) && info.httpPort && delete servers[info.httpPort]; isPush(info) && info.httpsPort && delete serversS[info.httpsPort]; } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err) { console.error(`Redioactive: HTTP/S target: error closing ${info.protocol} ${info.type} stream: ${err.message}`); } } } function chunkIsString(_x) { return info.body !== http_common_1.BodyType.blob; } } exports.httpTarget = httpTarget;