@toa.io/bindings.http 0.20.0-dev.8 → 1.0.0-alpha.312

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@toa.io/bindings.http",
3
- "version": "0.20.0-dev.8",
3
+ "version": "1.0.0-alpha.312",
4
+ "type": "module",
4
5
  "description": "Toa HTTP Binding",
5
6
  "author": "temich <tema.gurtovoy@gmail.com>",
6
7
  "homepage": "https://github.com/toa-io/toa#readme",
7
- "main": "src/index.js",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/toa-io/toa.git"
@@ -15,15 +15,22 @@
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
18
- "scripts": {
19
- "test": "echo \"Error: run tests from root\" && exit 1"
20
- },
18
+ "files": [
19
+ "transpiled",
20
+ "readme.md",
21
+ "!**/tsconfig.tsbuildinfo",
22
+ "!**/*.test.*"
23
+ ],
24
+ "main": "transpiled/index.js",
25
+ "types": "transpiled/index.d.ts",
21
26
  "dependencies": {
22
- "@toa.io/console": "0.20.0-dev.8",
23
- "@toa.io/core": "0.20.0-dev.8",
24
- "agentkeepalive": "4.2.1",
25
- "express": "4.18.1",
26
- "node-fetch": "2.6.7"
27
+ "@toa.io/core": "1.0.0-alpha.312",
28
+ "@toa.io/definitions": "1.0.0-alpha.312",
29
+ "@toa.io/generic": "1.0.0-alpha.311",
30
+ "openspan": "1.0.0-alpha.305"
31
+ },
32
+ "scripts": {
33
+ "transpile": "tsc"
27
34
  },
28
- "gitHead": "d8c08ec7def70ad93de06541af497cce8c4c1b50"
35
+ "gitHead": "3cdff9b9779eeb6ae83913fefc0b502a10a0236a"
29
36
  }
package/readme.md ADDED
@@ -0,0 +1,40 @@
1
+ # Toa HTTP binding
2
+
3
+ Carries [streamed calls](/documentation/streams.md) — a call whose input holds a stream, which no
4
+ other binding can carry. It declines everything else, so ordinary calls, tasks, events and addressed
5
+ calls go over the binding beside it.
6
+
7
+ A call is made to a component, not to a process: whichever replica answers at the component's
8
+ address takes it, over a connection opened for that call and closed when it ends. A connection kept
9
+ for the next call would send it to the same replica, so none is kept: a call in flight costs a
10
+ socket at each end, and a call that is neither large nor long costs more than it saves. See
11
+ [what it costs](/documentation/streams.md#what-it-costs).
12
+
13
+ ## Annotation
14
+
15
+ An address per component, as the context has it:
16
+
17
+ ```yaml
18
+ # context.toa.yaml
19
+ http:
20
+ media.videos: http://media-videos:8005
21
+ ```
22
+
23
+ Absent, a component is addressed at the `Service` deployment renders for it — its label, on port
24
+ `8005` — which is what a deployment normally leaves unsaid. What a key states is used for a
25
+ component and for the components under it: `.` is every component of the context.
26
+
27
+ A process reads the same map for the other side of it: it listens on the port in the address of each
28
+ component it serves. Where several components in one process name several ports, it listens on each.
29
+
30
+ Keys are folded by environment like every other key of a context, which is where a development
31
+ machine states the ports its processes take:
32
+
33
+ ```yaml
34
+ http@local:
35
+ media.videos: http://127.0.0.1:31006
36
+ media.files: http://127.0.0.1:31007
37
+ ```
38
+
39
+ Components composed in one process call each other without any of this: a stream is passed as it is
40
+ where there is nothing in between.
@@ -0,0 +1,15 @@
1
+ import { Connector } from '@toa.io/core';
2
+ import type { Readable } from 'node:stream';
3
+ import type { Locator } from '@toa.io/core';
4
+ import type { bindings, Reply, Request } from '@toa.io/core/types';
5
+ /**
6
+ * Carries a call that holds a stream, and nothing else: what a call without one is answered with
7
+ * is `false`, and the transmission goes on to the binding beside this one.
8
+ */
9
+ export declare class Consumer extends Connector {
10
+ private readonly locator;
11
+ private readonly endpoint;
12
+ constructor(locator: Locator, endpoint: string);
13
+ request(request: Request, terms?: bindings.Terms): Promise<Reply | Readable | false>;
14
+ private send;
15
+ }
@@ -0,0 +1,73 @@
1
+ import * as http from 'node:http';
2
+ import { Connector, exceptions } from '@toa.io/core';
3
+ import { address } from './address.js';
4
+ import { HEADER, PATH, detach } from './envelope.js';
5
+ import { read } from './reply.js';
6
+ import { path } from './Producer.js';
7
+ /**
8
+ * Carries a call that holds a stream, and nothing else: what a call without one is answered with
9
+ * is `false`, and the transmission goes on to the binding beside this one.
10
+ */
11
+ export class Consumer extends Connector {
12
+ locator;
13
+ endpoint;
14
+ constructor(locator, endpoint) {
15
+ super();
16
+ this.locator = locator;
17
+ this.endpoint = endpoint;
18
+ }
19
+ async request(request, terms) {
20
+ const carried = detach(request);
21
+ if (carried === undefined)
22
+ return false;
23
+ const url = new URL(path(this.locator, this.endpoint), address(this.locator));
24
+ return await this.send(url, carried.envelope, carried.path, carried.stream, terms);
25
+ }
26
+ // eslint-disable-next-line max-params
27
+ async send(url, envelope, property, stream, terms) {
28
+ const message = await new Promise((resolve, reject) => {
29
+ const request = http.request(url, {
30
+ method: 'POST',
31
+ // a connection kept for the next call would send it where this one went
32
+ agent: false,
33
+ signal: terms?.signal,
34
+ headers: {
35
+ [HEADER]: JSON.stringify(envelope),
36
+ [PATH]: property,
37
+ 'content-type': 'application/octet-stream'
38
+ }
39
+ }, resolve);
40
+ request.on('error', reject);
41
+ stream.pipe(request);
42
+ }).catch((error) => {
43
+ throw unreachable(url, error);
44
+ });
45
+ // nothing at that address serves this endpoint: a replica of another version, or another
46
+ // process that has taken the address since
47
+ if (message.statusCode === 404)
48
+ throw new exceptions.UnreachableException(`Nothing at '${url.host}' serves '${this.locator.id}.${this.endpoint}'`);
49
+ if (message.statusCode !== 200)
50
+ throw new exceptions.SystemException(new Error(`'${url.host}' answered ${message.statusCode} to a streamed call`));
51
+ return read(message);
52
+ }
53
+ }
54
+ /**
55
+ * A replica that is starting, restarting or being deployed is one that answers in a moment. What
56
+ * failed for any other reason — a caller that stopped waiting, a stream of its own that broke — is
57
+ * its own failure, and is handed on as it is.
58
+ */
59
+ function unreachable(url, error) {
60
+ const code = error.code;
61
+ if (code === undefined || !DIAL.has(code))
62
+ return error;
63
+ return new exceptions.UnreachableException(`Nothing answered at '${url.host}' (${code})`, error);
64
+ }
65
+ const DIAL = new Set([
66
+ 'ECONNREFUSED',
67
+ 'ENOTFOUND',
68
+ 'EHOSTUNREACH',
69
+ 'ENETUNREACH',
70
+ 'ETIMEDOUT',
71
+ 'EAI_AGAIN'
72
+ ]);
73
+ //# sourceMappingURL=Consumer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Consumer.js","sourceRoot":"","sources":["../source/Consumer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { Consumer } from './Consumer.ts';
2
+ import type { Component, Connector, Locator } from '@toa.io/core';
3
+ /**
4
+ * A server per port the components of this process are addressed at — one, where they share an
5
+ * address, and one each where a development machine gives them their own.
6
+ */
7
+ export declare class Factory {
8
+ private readonly servers;
9
+ producer(locator: Locator, endpoints: string[], component: Component, _stateful?: string[], streamed?: string[]): Connector;
10
+ consumer(locator: Locator, endpoint: string): Consumer;
11
+ private server;
12
+ }
@@ -0,0 +1,29 @@
1
+ import { Consumer } from './Consumer.js';
2
+ import { Producer } from './Producer.js';
3
+ import { Server } from './Server.js';
4
+ import { port } from './address.js';
5
+ /**
6
+ * A server per port the components of this process are addressed at — one, where they share an
7
+ * address, and one each where a development machine gives them their own.
8
+ */
9
+ export class Factory {
10
+ servers = new Map();
11
+ // eslint-disable-next-line max-params
12
+ producer(locator, endpoints, component, _stateful, streamed = []) {
13
+ // this binding carries a call that holds a stream, so it serves the endpoints that take one
14
+ const carried = endpoints.filter((endpoint) => streamed.includes(endpoint));
15
+ return new Producer(this.server(port(locator)), locator, carried, component);
16
+ }
17
+ consumer(locator, endpoint) {
18
+ return new Consumer(locator, endpoint);
19
+ }
20
+ server(number) {
21
+ let server = this.servers.get(number);
22
+ if (server === undefined) {
23
+ server = new Server(number);
24
+ this.servers.set(number, server);
25
+ }
26
+ return server;
27
+ }
28
+ }
29
+ //# sourceMappingURL=Factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Factory.js","sourceRoot":"","sources":["../source/Factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AAGnC;;;GAGG;AACH,MAAM,OAAO,OAAO;IACD,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAA;IAEpD,sCAAsC;IAC/B,QAAQ,CACb,OAAgB,EAChB,SAAmB,EACnB,SAAoB,EACpB,SAAoB,EACpB,QAAQ,GAAa,EAAE;QAEvB,4FAA4F;QAC5F,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAA;QAE3E,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,CAAA;IAC9E,CAAC;IAEM,QAAQ,CAAC,OAAgB,EAAE,QAAgB;QAChD,OAAO,IAAI,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IACxC,CAAC;IAEO,MAAM,CAAC,MAAc;QAC3B,IAAI,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QAErC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,CAAA;YAC3B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAClC,CAAC;QAED,OAAO,MAAM,CAAA;IACf,CAAC;CACF"}
@@ -0,0 +1,21 @@
1
+ import { Connector } from '@toa.io/core';
2
+ import type { Component, Locator } from '@toa.io/core';
3
+ import type { Server } from './Server.ts';
4
+ /** Serves the endpoints of one component that take a stream, on the server of its own port. */
5
+ export declare class Producer extends Connector {
6
+ private readonly server;
7
+ private readonly locator;
8
+ private readonly endpoints;
9
+ private readonly component;
10
+ private readonly pending;
11
+ constructor(server: Server, locator: Locator, endpoints: string[], component: Component);
12
+ protected open(): Promise<void>;
13
+ /**
14
+ * Takes no new call before the component it calls is taken apart, and waits for the calls it
15
+ * holds: the component is a dependency, and is disconnected only once this has returned.
16
+ */
17
+ protected close(): Promise<void>;
18
+ private invoke;
19
+ }
20
+ /** What a call to an endpoint is addressed to, which is what a caller writes on the request. */
21
+ export declare function path(locator: Locator, endpoint: string): string;
@@ -0,0 +1,49 @@
1
+ import { Connector, deliveries } from '@toa.io/core';
2
+ /** Serves the endpoints of one component that take a stream, on the server of its own port. */
3
+ export class Producer extends Connector {
4
+ server;
5
+ locator;
6
+ endpoints;
7
+ component;
8
+ pending = new Set();
9
+ // eslint-disable-next-line max-params
10
+ constructor(server, locator, endpoints, component) {
11
+ super();
12
+ this.server = server;
13
+ this.locator = locator;
14
+ this.endpoints = endpoints;
15
+ this.component = component;
16
+ this.depends(server);
17
+ this.depends(component);
18
+ }
19
+ async open() {
20
+ for (const endpoint of this.endpoints)
21
+ this.server.route(path(this.locator, endpoint), async (request) => await this.invoke(endpoint, request));
22
+ }
23
+ /**
24
+ * Takes no new call before the component it calls is taken apart, and waits for the calls it
25
+ * holds: the component is a dependency, and is disconnected only once this has returned.
26
+ */
27
+ async close() {
28
+ for (const endpoint of this.endpoints)
29
+ this.server.unroute(path(this.locator, endpoint));
30
+ await Promise.allSettled(this.pending);
31
+ }
32
+ async invoke(endpoint, request) {
33
+ const promise = this.component.invoke(endpoint, request);
34
+ this.pending.add(promise);
35
+ deliveries.taken();
36
+ try {
37
+ return (await promise);
38
+ }
39
+ finally {
40
+ this.pending.delete(promise);
41
+ deliveries.done();
42
+ }
43
+ }
44
+ }
45
+ /** What a call to an endpoint is addressed to, which is what a caller writes on the request. */
46
+ export function path(locator, endpoint) {
47
+ return `/${locator.namespace}/${locator.name}/${endpoint}`;
48
+ }
49
+ //# sourceMappingURL=Producer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Producer.js","sourceRoot":"","sources":["../source/Producer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAMpD,+FAA+F;AAC/F,MAAM,OAAO,QAAS,SAAQ,SAAS;IACpB,MAAM,CAAQ;IACd,OAAO,CAAS;IAChB,SAAS,CAAU;IACnB,SAAS,CAAW;IACpB,OAAO,GAAG,IAAI,GAAG,EAAoB,CAAA;IAEtD,sCAAsC;IACtC,YACE,MAAc,EACd,OAAgB,EAChB,SAAmB,EACnB,SAAoB;QAEpB,KAAK,EAAE,CAAA;QAEP,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;QAC1B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;QAE1B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QACpB,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IACzB,CAAC;IAEkB,KAAK,CAAC,IAAI;QAC3B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS;YACnC,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,EAC5B,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CACxD,CAAA;IACL,CAAC;IAED;;;OAGG;IACgB,KAAK,CAAC,KAAK;QAC5B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS;YAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;QAExF,MAAM,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACxC,CAAC;IAEO,KAAK,CAAC,MAAM,CAAC,QAAgB,EAAE,OAAgB;QACrD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAExD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QACzB,UAAU,CAAC,KAAK,EAAE,CAAA;QAElB,IAAI,CAAC;YACH,OAAO,CAAC,MAAM,OAAO,CAAqB,CAAA;QAC5C,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;YAC5B,UAAU,CAAC,IAAI,EAAE,CAAA;QACnB,CAAC;IACH,CAAC;CACF;AAED,gGAAgG;AAChG,MAAM,UAAU,IAAI,CAAC,OAAgB,EAAE,QAAgB;IACrD,OAAO,IAAI,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,IAAI,IAAI,QAAQ,EAAE,CAAA;AAC5D,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { Connector } from '@toa.io/core';
2
+ import type { Readable } from 'node:stream';
3
+ import type { Reply, Request } from '@toa.io/core/types';
4
+ export type Invoke = (request: Request) => Promise<Reply | Readable>;
5
+ /**
6
+ * What answers streamed calls on one port, for every component of this process whose address
7
+ * names that port.
8
+ */
9
+ export declare class Server extends Connector {
10
+ private readonly port;
11
+ private readonly routes;
12
+ private readonly server;
13
+ constructor(port: number);
14
+ route(path: string, invoke: Invoke): void;
15
+ unroute(path: string): void;
16
+ protected open(): Promise<void>;
17
+ protected close(): Promise<void>;
18
+ private listener;
19
+ }
@@ -0,0 +1,70 @@
1
+ import * as http from 'node:http';
2
+ import { Connector } from '@toa.io/core';
3
+ import { console } from 'openspan';
4
+ import { HEADER, PATH, attach } from './envelope.js';
5
+ import { write } from './reply.js';
6
+ /**
7
+ * What answers streamed calls on one port, for every component of this process whose address
8
+ * names that port.
9
+ */
10
+ export class Server extends Connector {
11
+ port;
12
+ routes = new Map();
13
+ server;
14
+ constructor(port) {
15
+ super();
16
+ this.port = port;
17
+ this.server = http.createServer(this.listener.bind(this));
18
+ }
19
+ route(path, invoke) {
20
+ if (this.routes.has(path))
21
+ throw new Error(`Endpoint '${path}' is already served`);
22
+ this.routes.set(path, invoke);
23
+ }
24
+ unroute(path) {
25
+ this.routes.delete(path);
26
+ }
27
+ async open() {
28
+ await new Promise((resolve, reject) => {
29
+ this.server.on('error', reject);
30
+ this.server.listen(this.port, () => {
31
+ this.server.off('error', reject);
32
+ console.info('Streamed calls are served', { port: this.port });
33
+ resolve();
34
+ });
35
+ });
36
+ }
37
+ async close() {
38
+ await new Promise((resolve) => {
39
+ // a call in flight holds its connection, and is waited for; nothing else is kept
40
+ this.server.closeIdleConnections();
41
+ this.server.close(() => resolve());
42
+ });
43
+ }
44
+ listener(request, response) {
45
+ const invoke = request.method === 'POST' ? this.routes.get(request.url ?? '') : undefined;
46
+ if (invoke === undefined) {
47
+ response.writeHead(404);
48
+ response.end();
49
+ return;
50
+ }
51
+ const header = request.headers[HEADER];
52
+ const path = request.headers[PATH];
53
+ if (typeof header !== 'string' || typeof path !== 'string') {
54
+ response.writeHead(400);
55
+ response.end();
56
+ return;
57
+ }
58
+ const envelope = JSON.parse(header);
59
+ attach(envelope, path, request);
60
+ invoke(envelope)
61
+ .then(async (reply) => await write(response, reply))
62
+ .catch((error) => {
63
+ console.error('Streamed call failed', { error });
64
+ if (!response.headersSent)
65
+ response.writeHead(500);
66
+ response.end();
67
+ });
68
+ }
69
+ }
70
+ //# sourceMappingURL=Server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Server.js","sourceRoot":"","sources":["../source/Server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { Locator } from '@toa.io/core';
2
+ /**
3
+ * Where a component answers a streamed call: what the context states for it, or the service a
4
+ * deployment renders for it, which is what an unstated address resolves to.
5
+ */
6
+ export declare function address(locator: Locator): URL;
7
+ /**
8
+ * The port a process serving this component listens on, which is the one in the component's own
9
+ * address: one line says where a component is, and both sides read it.
10
+ */
11
+ export declare function port(locator: Locator): number;
@@ -0,0 +1,28 @@
1
+ import { environment } from '@toa.io/generic';
2
+ import { PORT, VARIABLE } from '@toa.io/definitions/bindings.http';
3
+ /**
4
+ * Where a component answers a streamed call: what the context states for it, or the service a
5
+ * deployment renders for it, which is what an unstated address resolves to.
6
+ */
7
+ export function address(locator) {
8
+ return new URL(stated(locator) ?? `http://${locator.hostname()}:${PORT}`);
9
+ }
10
+ /**
11
+ * The port a process serving this component listens on, which is the one in the component's own
12
+ * address: one line says where a component is, and both sides read it.
13
+ */
14
+ export function port(locator) {
15
+ const url = address(locator);
16
+ return url.port === '' ? PORT : Number(url.port);
17
+ }
18
+ /** The deepest key that names this component, as a context states one for all of them. */
19
+ function stated(locator) {
20
+ const value = environment.get(VARIABLE);
21
+ if (value === undefined)
22
+ return undefined;
23
+ const map = JSON.parse(value);
24
+ const namespace = locator.namespace;
25
+ const broader = namespace === undefined ? undefined : map[namespace];
26
+ return map[locator.id] ?? broader ?? map['.'];
27
+ }
28
+ //# sourceMappingURL=address.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.js","sourceRoot":"","sources":["../source/address.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAA;AAGlE;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,OAAgB;IACtC,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,UAAU,OAAO,CAAC,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,CAAA;AAC3E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,IAAI,CAAC,OAAgB;IACnC,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAE5B,OAAO,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AAClD,CAAC;AAED,0FAA0F;AAC1F,SAAS,MAAM,CAAC,OAAgB;IAC9B,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAEvC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAEzC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAA2B,CAAA;IACvD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAA;IAEnC,MAAM,OAAO,GAAG,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IAEpE,OAAO,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;AAC/C,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { Readable } from 'node:stream';
2
+ import type { Request } from '@toa.io/core/types';
3
+ /** The header a call travels in, beside the stream it carries as the body. */
4
+ export declare const HEADER = "toa-request";
5
+ /** Where in the input the stream was taken from, so that it is put back where it was. */
6
+ export declare const PATH = "toa-stream";
7
+ export interface Carried {
8
+ /** the input property the stream was taken from, and its `stream` member where it has one */
9
+ path: string;
10
+ stream: Readable;
11
+ /** what is left of the call once the stream is out of it */
12
+ envelope: Request;
13
+ }
14
+ /**
15
+ * The stream a call carries, taken out of the input it was put in — the property itself where its
16
+ * caller said nothing about it, and the property's `stream` where a route filled one. What is
17
+ * handed back is a copy: the input belongs to whoever made the call.
18
+ */
19
+ export declare function detach(request: Request): Carried | undefined;
20
+ /** Puts a stream back where the call carried it, on the side that serves the call. */
21
+ export declare function attach(request: Request, path: string, stream: Readable): void;
@@ -0,0 +1,52 @@
1
+ import { Readable } from 'node:stream';
2
+ /** The header a call travels in, beside the stream it carries as the body. */
3
+ export const HEADER = 'toa-request';
4
+ /** Where in the input the stream was taken from, so that it is put back where it was. */
5
+ export const PATH = 'toa-stream';
6
+ /**
7
+ * The stream a call carries, taken out of the input it was put in — the property itself where its
8
+ * caller said nothing about it, and the property's `stream` where a route filled one. What is
9
+ * handed back is a copy: the input belongs to whoever made the call.
10
+ */
11
+ export function detach(request) {
12
+ const input = request.input;
13
+ if (input === null || typeof input !== 'object')
14
+ return undefined;
15
+ for (const [key, value] of Object.entries(input)) {
16
+ if (value instanceof Readable)
17
+ return { path: key, stream: value, envelope: without(request, key) };
18
+ if (value !== null && typeof value === 'object' && 'stream' in value) {
19
+ const property = value;
20
+ const stream = property.stream;
21
+ if (stream instanceof Readable) {
22
+ const rest = { ...property };
23
+ delete rest.stream;
24
+ return { path: key + '.stream', stream, envelope: without(request, key, rest) };
25
+ }
26
+ }
27
+ }
28
+ return undefined;
29
+ }
30
+ /** Puts a stream back where the call carried it, on the side that serves the call. */
31
+ export function attach(request, path, stream) {
32
+ const input = (request.input ?? {});
33
+ const [key, nested] = path.split('.');
34
+ if (nested === undefined)
35
+ input[key] = stream;
36
+ else {
37
+ const property = (input[key] ?? {});
38
+ property[nested] = stream;
39
+ input[key] = property;
40
+ }
41
+ request.input = input;
42
+ }
43
+ /** What is left of a call once the stream is out of it. The input belongs to whoever made it. */
44
+ function without(request, key, rest) {
45
+ const copy = { ...request.input };
46
+ if (rest === undefined)
47
+ delete copy[key];
48
+ else
49
+ copy[key] = rest;
50
+ return { ...request, input: copy };
51
+ }
52
+ //# sourceMappingURL=envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.js","sourceRoot":"","sources":["../source/envelope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAGtC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,MAAM,GAAG,aAAa,CAAA;AAEnC,yFAAyF;AACzF,MAAM,CAAC,MAAM,IAAI,GAAG,YAAY,CAAA;AAUhC;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAC,OAAgB;IACrC,MAAM,KAAK,GAAY,OAAO,CAAC,KAAK,CAAA;IAEpC,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IAEjE,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,IAAI,KAAK,YAAY,QAAQ;YAC3B,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,CAAA;QAEtE,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,QAAQ,IAAI,KAAK,EAAE,CAAC;YACrE,MAAM,QAAQ,GAAG,KAAgC,CAAA;YACjD,MAAM,MAAM,GAAY,QAAQ,CAAC,MAAM,CAAA;YAEvC,IAAI,MAAM,YAAY,QAAQ,EAAE,CAAC;gBAC/B,MAAM,IAAI,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAA;gBAE5B,OAAO,IAAI,CAAC,MAAM,CAAA;gBAElB,OAAO,EAAE,IAAI,EAAE,GAAG,GAAG,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,CAAA;YACjF,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,MAAM,CAAC,OAAgB,EAAE,IAAY,EAAE,MAAgB;IACrE,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAA4B,CAAA;IAC9D,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAErC,IAAI,MAAM,KAAK,SAAS;QAAE,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAAA;SACxC,CAAC;QACJ,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAA4B,CAAA;QAE9D,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,CAAA;QACzB,KAAK,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAA;IACvB,CAAC;IAED,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;AACvB,CAAC;AAED,iGAAiG;AACjG,SAAS,OAAO,CAAC,OAAgB,EAAE,GAAW,EAAE,IAAa;IAC3D,MAAM,IAAI,GAA4B,EAAE,GAAI,OAAO,CAAC,KAAgB,EAAE,CAAA;IAEtE,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAA;;QACnC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAA;IAErB,OAAO,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;AACpC,CAAC"}
@@ -0,0 +1 @@
1
+ export { Factory } from './Factory.ts';
@@ -0,0 +1,2 @@
1
+ export { Factory } from './Factory.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../source/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,16 @@
1
+ import { Readable } from 'node:stream';
2
+ import type { IncomingMessage, ServerResponse } from 'node:http';
3
+ import type { Reply } from '@toa.io/core/types';
4
+ export declare const JSON_TYPE = "application/json";
5
+ export declare const BYTES_TYPE = "application/octet-stream";
6
+ export declare const VALUES_TYPE = "application/x-ndjson";
7
+ /** How a stream ended, said once it is too late to say it in a status. */
8
+ export declare const STATUS = "toa-status";
9
+ /**
10
+ * A value is the envelope, a stream is the body — bytes as they are, values newline-delimited —
11
+ * and how the stream ended is a trailer, so a body that stops without one is a stream that was
12
+ * cut rather than one that finished.
13
+ */
14
+ export declare function write(response: ServerResponse, reply: Reply | Readable): Promise<void>;
15
+ /** What a caller is handed: the envelope a value came back as, or the stream a stream did. */
16
+ export declare function read(message: IncomingMessage): Promise<Reply> | Readable;
@@ -0,0 +1,128 @@
1
+ import { Readable } from 'node:stream';
2
+ import { pipeline } from 'node:stream/promises';
3
+ export const JSON_TYPE = 'application/json';
4
+ export const BYTES_TYPE = 'application/octet-stream';
5
+ export const VALUES_TYPE = 'application/x-ndjson';
6
+ /** How a stream ended, said once it is too late to say it in a status. */
7
+ export const STATUS = 'toa-status';
8
+ const OK = 'ok';
9
+ /**
10
+ * A value is the envelope, a stream is the body — bytes as they are, values newline-delimited —
11
+ * and how the stream ended is a trailer, so a body that stops without one is a stream that was
12
+ * cut rather than one that finished.
13
+ */
14
+ export async function write(response, reply) {
15
+ if (!(reply instanceof Readable)) {
16
+ const body = JSON.stringify(reply ?? null);
17
+ response.writeHead(200, { 'content-type': JSON_TYPE });
18
+ response.end(body);
19
+ return;
20
+ }
21
+ await stream(response, reply);
22
+ }
23
+ async function stream(response, source) {
24
+ // one iterator throughout: taking the first chunk with a `for await` that breaks would close
25
+ // the stream it was read from
26
+ const iterator = source[Symbol.asyncIterator]();
27
+ const first = await iterator.next();
28
+ // `Readable.from` makes every generator an object mode stream, whatever it yields, so what the
29
+ // body is, is settled by what came out of it rather than by how it was made
30
+ const bytes = !source.readableObjectMode || Buffer.isBuffer(first.value);
31
+ response.writeHead(200, {
32
+ 'content-type': bytes ? BYTES_TYPE : VALUES_TYPE,
33
+ trailer: STATUS
34
+ });
35
+ try {
36
+ await pipeline(frames(first, iterator, bytes), response, { end: false });
37
+ response.addTrailers({ [STATUS]: OK });
38
+ }
39
+ catch (error) {
40
+ // the status is written, so what failed is said where it still can be
41
+ response.addTrailers({ [STATUS]: JSON.stringify(reason(error)) });
42
+ }
43
+ response.end();
44
+ }
45
+ async function* frames(first, rest, bytes) {
46
+ if (first.done !== true)
47
+ yield frame(first.value, bytes);
48
+ for (;;) {
49
+ const next = await rest.next();
50
+ if (next.done === true)
51
+ return;
52
+ yield frame(next.value, bytes);
53
+ }
54
+ }
55
+ /** What a caller is told of a stream that failed after it had started. */
56
+ function reason(error) {
57
+ const carried = error;
58
+ return carried?.exception ?? carried?.message ?? null;
59
+ }
60
+ function frame(chunk, bytes) {
61
+ if (bytes) {
62
+ if (Buffer.isBuffer(chunk))
63
+ return chunk;
64
+ if (typeof chunk === 'string')
65
+ return Buffer.from(chunk);
66
+ throw new Error('A stream carries bytes and values, and is served as neither');
67
+ }
68
+ if (Buffer.isBuffer(chunk))
69
+ throw new Error('A stream carries bytes and values, and is served as neither');
70
+ return Buffer.from(JSON.stringify(chunk) + '\n');
71
+ }
72
+ /** What a caller is handed: the envelope a value came back as, or the stream a stream did. */
73
+ export function read(message) {
74
+ const type = message.headers['content-type'];
75
+ if (type === BYTES_TYPE)
76
+ return checked(message, message);
77
+ if (type === VALUES_TYPE)
78
+ return checked(message, values(message));
79
+ return envelope(message);
80
+ }
81
+ async function envelope(message) {
82
+ const chunks = [];
83
+ for await (const chunk of message)
84
+ chunks.push(chunk);
85
+ const body = Buffer.concat(chunks).toString();
86
+ return body === '' ? null : JSON.parse(body);
87
+ }
88
+ function values(message) {
89
+ return Readable.from(lines(message), { objectMode: true });
90
+ }
91
+ async function* lines(message) {
92
+ let rest = '';
93
+ for await (const chunk of message) {
94
+ const text = rest + chunk.toString();
95
+ const parts = text.split('\n');
96
+ rest = parts.pop() ?? '';
97
+ for (const part of parts)
98
+ if (part !== '')
99
+ yield JSON.parse(part);
100
+ }
101
+ if (rest !== '')
102
+ yield JSON.parse(rest);
103
+ }
104
+ /**
105
+ * A stream is whole only where its trailer says so: a body that ends without one was cut, and one
106
+ * whose trailer carries an exception failed after it had started.
107
+ *
108
+ * Pulled rather than piped, so that what arrived before a failure is read before the failure is
109
+ * raised: a chunk that got through is never dropped for the sake of reporting the one that did not.
110
+ */
111
+ function checked(message, source) {
112
+ return Readable.from(pull(message, source), { objectMode: source.readableObjectMode });
113
+ }
114
+ async function* pull(message, source) {
115
+ for await (const chunk of source)
116
+ yield chunk;
117
+ const status = message.trailers[STATUS];
118
+ if (status !== OK)
119
+ throw failure(status);
120
+ }
121
+ function failure(status) {
122
+ if (status === undefined)
123
+ return new Error('The reply stream ended without saying that it was whole');
124
+ const error = new Error('The reply stream failed');
125
+ error.cause = JSON.parse(status);
126
+ return error;
127
+ }
128
+ //# sourceMappingURL=reply.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reply.js","sourceRoot":"","sources":["../source/reply.ts"],"names":[],"mappings":"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"}
package/src/consumer.js DELETED
@@ -1,49 +0,0 @@
1
- 'use strict'
2
-
3
- const fetch = require('node-fetch')
4
- const { Connector } = require('@toa.io/core')
5
-
6
- const resource = require('./resource')
7
-
8
- class Consumer extends Connector {
9
- #agent
10
- #locator
11
- #endpoint
12
- #url
13
-
14
- constructor (agent, locator, endpoint) {
15
- super()
16
-
17
- this.#agent = agent
18
- this.#locator = locator
19
- this.#endpoint = endpoint
20
- }
21
-
22
- async open () {
23
- this.#url = Consumer.#locate(this.#locator, this.#endpoint)
24
- }
25
-
26
- async request (request) {
27
- const method = resource.method
28
-
29
- const reply = await fetch(this.#url, {
30
- method,
31
- body: JSON.stringify(request),
32
- headers: { 'Content-Type': 'application/json' },
33
- agent: this.#agent
34
- })
35
-
36
- return reply.json()
37
- }
38
-
39
- static #locate (locator, endpoint) {
40
- const host = locator.hostname()
41
- const path = resource.path(locator, endpoint)
42
-
43
- return `http://${host}:${PORT}${path}`
44
- }
45
- }
46
-
47
- const PORT = 3000
48
-
49
- exports.Consumer = Consumer
package/src/factory.js DELETED
@@ -1,32 +0,0 @@
1
- 'use strict'
2
-
3
- const Agent = require('agentkeepalive')
4
-
5
- const { Consumer } = require('./consumer')
6
- const { Producer } = require('./producer')
7
- const { Server } = require('./server')
8
-
9
- class Factory {
10
- #server
11
- #agents = {}
12
-
13
- constructor () {
14
- this.#server = new Server()
15
- }
16
-
17
- producer (locator, endpoints, target) {
18
- return new Producer(this.#server, locator, endpoints, target)
19
- }
20
-
21
- consumer (locator, endpoint) {
22
- const id = locator.id
23
-
24
- if (!this.#agents[id]) this.#agents[id] = new Agent(AGENT_OPTIONS)
25
-
26
- return new Consumer(this.#agents[id], locator, endpoint)
27
- }
28
- }
29
-
30
- const AGENT_OPTIONS = { keepAlive: true }
31
-
32
- exports.Factory = Factory
package/src/index.js DELETED
@@ -1,6 +0,0 @@
1
- 'use strict'
2
-
3
- const { Factory } = require('./factory')
4
-
5
- exports.Factory = Factory
6
- exports.properties = { async: false }
package/src/producer.js DELETED
@@ -1,39 +0,0 @@
1
- 'use strict'
2
-
3
- const { Connector } = require('@toa.io/core')
4
-
5
- const resource = require('./resource')
6
-
7
- class Producer extends Connector {
8
- #server
9
- #locator
10
- #endpoints
11
- #producer
12
-
13
- constructor (server, locator, endpoints, producer) {
14
- super()
15
-
16
- this.#server = server
17
- this.#locator = locator
18
- this.#endpoints = endpoints
19
- this.#producer = producer
20
-
21
- this.#bind()
22
-
23
- this.depends(server)
24
- this.depends(producer)
25
- }
26
-
27
- #bind () {
28
- this.#endpoints.map((endpoint) => this.#endpoint(endpoint))
29
- }
30
-
31
- #endpoint (endpoint) {
32
- const method = resource.method
33
- const path = resource.path(this.#locator, endpoint)
34
-
35
- this.#server.reply(method, path, async (request) => this.#producer.invoke(endpoint, request))
36
- }
37
- }
38
-
39
- exports.Producer = Producer
package/src/resource.js DELETED
@@ -1,7 +0,0 @@
1
- 'use strict'
2
-
3
- const path = (locator, endpoint) => ['', locator.namespace, locator.name, endpoint].join('/')
4
- const method = 'POST'
5
-
6
- exports.path = path
7
- exports.method = method
package/src/server.js DELETED
@@ -1,61 +0,0 @@
1
- 'use strict'
2
-
3
- const express = require('express')
4
-
5
- const { Connector } = require('@toa.io/core')
6
- const { console } = require('@toa.io/console')
7
-
8
- class Server extends Connector {
9
- #app
10
- #server
11
-
12
- #port
13
-
14
- constructor () {
15
- super()
16
-
17
- this.#app = express()
18
- this.#app.disable('x-powered-by')
19
- this.#app.use(express.json())
20
-
21
- this.#port = PORT
22
- }
23
-
24
- reply (verb, route, invocation) {
25
- this.#app[verb.toLowerCase()](route, async (req, res) => {
26
- const reply = await invocation(req.body)
27
-
28
- res.json(reply)
29
- })
30
- }
31
-
32
- async open () {
33
- return new Promise((resolve, reject) => {
34
- this.#server = this.#app.listen(this.#port, () => {
35
- console.info(`HTTP server started at :${this.#port}`)
36
-
37
- this.#server.off('error', reject)
38
- resolve()
39
- })
40
-
41
- this.#server.on('error', reject)
42
- })
43
- }
44
-
45
- async close () {
46
- return new Promise((resolve, reject) => {
47
- this.#server.close(() => {
48
- console.info(`HTTP server at :${this.#port} stopped`)
49
-
50
- this.#server.off('error', reject)
51
- resolve()
52
- })
53
-
54
- this.#server.on('error', reject)
55
- })
56
- }
57
- }
58
-
59
- const PORT = 3000
60
-
61
- exports.Server = Server