@nmtjs/client 0.0.1

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/LICENSE.md ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2024 Denis Ilchyshyn
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # NeemataJS - RPC application server for real-time applications (proof of concept)
2
+
3
+ ### Built with following in mind:
4
+ - transport-agnostic (like WebSockets, WebTransport, .etc)
5
+ - format-agnostic (like JSON, MessagePack, BSON, .etc)
6
+ - binary data streaming and event subscriptions
7
+ - contract-based API
8
+ - end-to-end type safety
9
+ - CPU-intensive task execution on separate workers
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./lib/client.js";
2
+ export * from "./lib/common.js";
3
+ export * from "./lib/stream.js";
4
+ export * from "./lib/subscription.js";
5
+ export * from "./lib/transport.js";
6
+ export * from "./lib/utils.js";
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../index.ts"],"sourcesContent":["export * from './lib/client.ts'\nexport * from './lib/common.ts'\nexport * from './lib/stream.ts'\nexport * from './lib/subscription.ts'\nexport * from './lib/transport.ts'\nexport * from './lib/utils.ts'\n"],"names":[],"mappings":"AAAA,cAAc,kBAAiB;AAC/B,cAAc,kBAAiB;AAC/B,cAAc,kBAAiB;AAC/B,cAAc,wBAAuB;AACrC,cAAc,qBAAoB;AAClC,cAAc,iBAAgB"}
@@ -0,0 +1,70 @@
1
+ import { compile } from '@nmtjs/contract/compiler';
2
+ import { ContractGuard } from '@nmtjs/contract/guards';
3
+ import { Client } from "./client.js";
4
+ export class RuntimeClient extends Client {
5
+ #callers;
6
+ constructor(services, options){
7
+ super(options, Object.values(services).map((s)=>s.contract.name));
8
+ const callers = {};
9
+ for (const [serviceKey, serviceContract] of Object.entries(services)){
10
+ if (!serviceContract.contract.transports[this.transport.type]) throw new Error(`Transport [${this.transport.type}] not supported for service [${serviceContract.contract.name}]`);
11
+ callers[serviceKey] = {};
12
+ for(const procedureName in serviceContract.contract.procedures){
13
+ const { input, output } = serviceContract.contract.procedures[procedureName];
14
+ callers[serviceKey][procedureName] = this.createCaller(serviceContract.contract.name, procedureName, {
15
+ timeout: serviceContract.contract.timeout,
16
+ transformInput: (data)=>{
17
+ if (ContractGuard.IsNever(data)) return undefined;
18
+ const compiled = serviceContract.compiled.get(input);
19
+ const result = compiled.encode(data);
20
+ if (result.success) {
21
+ return result.value;
22
+ } else {
23
+ console.dir(result.error);
24
+ throw new Error('Failed to encode input', {
25
+ cause: result.error
26
+ });
27
+ }
28
+ },
29
+ transformOutput: (data)=>{
30
+ if (ContractGuard.IsNever(data)) return undefined;
31
+ const compiled = serviceContract.compiled.get(output);
32
+ const result = compiled.decode(data);
33
+ if (result.success) {
34
+ return result.value;
35
+ } else {
36
+ console.dir(result.error);
37
+ throw new Error('Failed to decode output', {
38
+ cause: result.error
39
+ });
40
+ }
41
+ }
42
+ });
43
+ }
44
+ }
45
+ this.#callers = callers;
46
+ }
47
+ get call() {
48
+ return this.#callers;
49
+ }
50
+ }
51
+ export const compileContract = (contract)=>{
52
+ const compiled = new Map();
53
+ for (const procedureContract of Object.values(contract.procedures)){
54
+ const { input, output, events } = procedureContract;
55
+ if (ContractGuard.IsSubscription(procedureContract)) {
56
+ for (const event of Object.values(events)){
57
+ compiled.set(event, compile(event));
58
+ }
59
+ }
60
+ compiled.set(input, compile(input));
61
+ compiled.set(output, compile(output));
62
+ }
63
+ for (const eventContract of Object.values(contract.events)){
64
+ compiled.set(eventContract, compile(eventContract));
65
+ }
66
+ return {
67
+ compiled,
68
+ contract
69
+ };
70
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../lib/client-runtime.ts"],"sourcesContent":["import type {\n Decoded,\n TEventContract,\n TSchema,\n TServiceContract,\n TSubscriptionContract,\n} from '@nmtjs/contract'\nimport { type Compiled, compile } from '@nmtjs/contract/compiler'\nimport { ContractGuard } from '@nmtjs/contract/guards'\nimport { Client, type ClientOptions } from './client.ts'\nimport type { Subscription } from './subscription.ts'\nimport type { ClientCallOptions, InputType, OutputType } from './types.ts'\n\ntype CompiledContract<T extends TServiceContract = TServiceContract> = {\n compiled: Map<TSchema, Compiled>\n contract: T\n}\n\ntype ClientServices = Record<string, CompiledContract>\n\ntype ClientCallers<Services extends ClientServices> = {\n [K in keyof Services]: {\n [P in keyof Services[K]['contract']['procedures']]: (\n ...args: Decoded<\n Services[K]['contract']['procedures'][P]['input']\n > extends never\n ? [options?: ClientCallOptions]\n : [\n data: InputType<\n Decoded<Services[K]['contract']['procedures'][P]['input']>\n >,\n options?: ClientCallOptions,\n ]\n ) => Promise<\n Services[K]['contract']['procedures'][P] extends TSubscriptionContract\n ? {\n payload: Decoded<\n Services[K]['contract']['procedures'][P]['output']\n > extends never\n ? undefined\n : Decoded<Services[K]['contract']['procedures'][P]['output']>\n subscription: Subscription<Services[K]['contract']['procedures'][P]>\n }\n : Decoded<\n Services[K]['contract']['procedures'][P]['static']\n > extends never\n ? void\n : OutputType<\n Decoded<Services[K]['contract']['procedures'][P]['output']>\n >\n >\n }\n}\n\nexport class RuntimeClient<Services extends ClientServices> extends Client {\n #callers: ClientCallers<Services>\n\n constructor(services: Services, options: ClientOptions) {\n super(\n options,\n Object.values(services).map((s) => s.contract.name),\n )\n\n const callers = {} as any\n for (const [serviceKey, serviceContract] of Object.entries(services)) {\n if (!serviceContract.contract.transports[this.transport.type])\n throw new Error(\n `Transport [${this.transport.type}] not supported for service [${serviceContract.contract.name}]`,\n )\n\n callers[serviceKey] = {} as any\n\n for (const procedureName in serviceContract.contract.procedures) {\n const { input, output } =\n serviceContract.contract.procedures[procedureName]\n\n callers[serviceKey][procedureName] = this.createCaller(\n serviceContract.contract.name,\n procedureName,\n {\n timeout: serviceContract.contract.timeout,\n transformInput: (data: any) => {\n if (ContractGuard.IsNever(data)) return undefined\n const compiled = serviceContract.compiled.get(input)!\n const result = compiled.encode(data)\n if (result.success) {\n return result.value\n } else {\n console.dir(result.error)\n throw new Error('Failed to encode input', {\n cause: result.error,\n })\n }\n },\n transformOutput: (data: any) => {\n if (ContractGuard.IsNever(data)) return undefined\n const compiled = serviceContract.compiled.get(output)!\n const result = compiled.decode(data)\n if (result.success) {\n return result.value\n } else {\n console.dir(result.error)\n throw new Error('Failed to decode output', {\n cause: result.error,\n })\n }\n },\n },\n )\n }\n }\n this.#callers = callers\n }\n\n get call() {\n return this.#callers\n }\n}\n\nexport const compileContract = <T extends TServiceContract>(\n contract: T,\n): CompiledContract<T> => {\n const compiled = new Map<TSchema, Compiled>()\n for (const procedureContract of Object.values(contract.procedures)) {\n const { input, output, events } = procedureContract\n if (ContractGuard.IsSubscription(procedureContract)) {\n for (const event of Object.values(events) as TEventContract[]) {\n compiled.set(event, compile(event))\n }\n }\n compiled.set(input, compile(input))\n compiled.set(output, compile(output))\n }\n for (const eventContract of Object.values(contract.events)) {\n compiled.set(eventContract, compile(eventContract))\n }\n\n return {\n compiled,\n contract,\n }\n}\n"],"names":["compile","ContractGuard","Client","RuntimeClient","constructor","services","options","Object","values","map","s","contract","name","callers","serviceKey","serviceContract","entries","transports","transport","type","Error","procedureName","procedures","input","output","createCaller","timeout","transformInput","data","IsNever","undefined","compiled","get","result","encode","success","value","console","dir","error","cause","transformOutput","decode","call","compileContract","Map","procedureContract","events","IsSubscription","event","set","eventContract"],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { Client } from "./client.js";
2
+ export class StaticClient extends Client {
3
+ #callers;
4
+ constructor(services, options){
5
+ super(options, Object.values(services));
6
+ const callers = {};
7
+ for (const [serviceKey, serviceName] of Object.entries(services)){
8
+ callers[serviceKey] = new Proxy(Object(), {
9
+ get: (target, prop, receiver)=>{
10
+ return this.createCaller(serviceName, prop);
11
+ }
12
+ });
13
+ }
14
+ this.#callers = callers;
15
+ }
16
+ get call() {
17
+ return this.#callers;
18
+ }
19
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../lib/client-static.ts"],"sourcesContent":["import type {\n Encoded,\n TServiceContract,\n TSubscriptionContract,\n} from '@nmtjs/contract'\nimport { Client, type ClientOptions } from './client.ts'\nimport type { Subscription } from './subscription.ts'\nimport type { ClientCallOptions, InputType, OutputType } from './types.ts'\n\ntype ClientServices = Record<string, TServiceContract>\n\ntype ClientCallers<Services extends ClientServices> = {\n [K in keyof Services]: {\n [P in keyof Services[K]['procedures']]: (\n ...args: Encoded<Services[K]['procedures'][P]['input']> extends never\n ? [options?: ClientCallOptions]\n : [\n data: InputType<Encoded<Services[K]['procedures'][P]['input']>>,\n options?: ClientCallOptions,\n ]\n ) => Promise<\n Services[K]['procedures'][P] extends TSubscriptionContract\n ? {\n payload: Encoded<\n Services[K]['procedures'][P]['output']\n > extends never\n ? undefined\n : Encoded<Services[K]['procedures'][P]['output']>\n subscription: Subscription<Services[K]['procedures'][P]>\n }\n : Encoded<Services[K]['procedures'][P]['static']> extends never\n ? void\n : OutputType<Encoded<Services[K]['procedures'][P]['output']>>\n >\n }\n}\n\nexport class StaticClient<Services extends ClientServices> extends Client {\n #callers: ClientCallers<Services>\n\n constructor(\n services: { [K in keyof Services]: Services[K]['name'] },\n options: ClientOptions,\n ) {\n super(options, Object.values(services))\n\n const callers = {} as any\n\n for (const [serviceKey, serviceName] of Object.entries(services)) {\n callers[serviceKey] = new Proxy(Object(), {\n get: (target, prop, receiver) => {\n return this.createCaller(serviceName, prop as string)\n },\n })\n }\n\n this.#callers = callers\n }\n\n get call() {\n return this.#callers\n }\n}\n"],"names":["Client","StaticClient","constructor","services","options","Object","values","callers","serviceKey","serviceName","entries","Proxy","get","target","prop","receiver","createCaller","call"],"mappings":"AAKA,SAASA,MAAM,QAA4B,cAAa;AAgCxD,OAAO,MAAMC,qBAAsDD;IACjE,CAAA,OAAQ,CAAyB;IAEjCE,YACEC,QAAwD,EACxDC,OAAsB,CACtB;QACA,KAAK,CAACA,SAASC,OAAOC,MAAM,CAACH;QAE7B,MAAMI,UAAU,CAAC;QAEjB,KAAK,MAAM,CAACC,YAAYC,YAAY,IAAIJ,OAAOK,OAAO,CAACP,UAAW;YAChEI,OAAO,CAACC,WAAW,GAAG,IAAIG,MAAMN,UAAU;gBACxCO,KAAK,CAACC,QAAQC,MAAMC;oBAClB,OAAO,IAAI,CAACC,YAAY,CAACP,aAAaK;gBACxC;YACF;QACF;QAEA,IAAI,CAAC,CAAA,OAAQ,GAAGP;IAClB;IAEA,IAAIU,OAAO;QACT,OAAO,IAAI,CAAC,CAAA,OAAQ;IACtB;AACF"}
@@ -0,0 +1,92 @@
1
+ import { ErrorCode } from '@nmtjs/common';
2
+ import { ClientError } from "./common.js";
3
+ import * as utils from "./utils.js";
4
+ export class Client extends utils.EventEmitter {
5
+ options;
6
+ services;
7
+ transport;
8
+ format;
9
+ auth;
10
+ ids;
11
+ constructor(options, services){
12
+ super();
13
+ this.options = options;
14
+ this.services = services;
15
+ this.ids = {
16
+ call: 0,
17
+ stream: 0
18
+ };
19
+ if (!options.defaultTimeout) options.defaultTimeout = 15000;
20
+ }
21
+ useTransport(transportClass, ...options) {
22
+ this.transport = new transportClass(...options);
23
+ this.transport.client = Object.freeze({
24
+ services: this.services,
25
+ format: this.format,
26
+ auth: this.auth
27
+ });
28
+ return this;
29
+ }
30
+ useFormat(format) {
31
+ this.format = format;
32
+ return this;
33
+ }
34
+ async connect() {
35
+ await this.transport.connect();
36
+ }
37
+ async disconnect() {
38
+ await this.transport.disconnect();
39
+ }
40
+ async reconnect() {
41
+ await this.disconnect();
42
+ await this.connect();
43
+ }
44
+ createCaller(service, procedure, { timeout = this.options.defaultTimeout, transformInput, transformOutput } = {}) {
45
+ return async (payload, options = {})=>{
46
+ const { signal } = options;
47
+ const abortSignal = signal ? AbortSignal.any([
48
+ signal,
49
+ AbortSignal.timeout(timeout)
50
+ ]) : AbortSignal.timeout(timeout);
51
+ const callId = ++this.ids.call;
52
+ if (this.options.debug) {
53
+ console.groupCollapsed(`RPC [${callId}] ${service}/${procedure}`);
54
+ console.log(payload);
55
+ console.groupEnd();
56
+ }
57
+ const callExecution = this.transport.rpc({
58
+ callId,
59
+ service,
60
+ procedure,
61
+ payload: transformInput ? transformInput(payload) : payload,
62
+ signal: abortSignal
63
+ }).then((result)=>{
64
+ if (result.success) return result.value;
65
+ throw new ClientError(result.error.code, result.error.message, result.error.data);
66
+ });
67
+ const callTimeout = utils.forAborted(abortSignal).catch(()=>{
68
+ const error = new ClientError(ErrorCode.RequestTimeout);
69
+ return Promise.reject(error);
70
+ });
71
+ try {
72
+ const response = await Promise.race([
73
+ callTimeout,
74
+ callExecution
75
+ ]);
76
+ if (this.options.debug) {
77
+ console.groupCollapsed(`RPC [${callId}] Success`);
78
+ console.log(response);
79
+ console.groupEnd();
80
+ }
81
+ return transformOutput ? transformOutput(response) : response;
82
+ } catch (error) {
83
+ if (this.options.debug) {
84
+ console.groupCollapsed(`RPC [${callId}] Error`);
85
+ console.log(error);
86
+ console.groupEnd();
87
+ }
88
+ throw error;
89
+ }
90
+ };
91
+ }
92
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../lib/client.ts"],"sourcesContent":["import { type BaseClientFormat, ErrorCode } from '@nmtjs/common'\n\nimport { ClientError } from './common.ts'\nimport type { ClientTransport } from './transport.ts'\nimport type { ClientCallOptions } from './types.ts'\nimport * as utils from './utils.ts'\n\nexport type ClientOptions = {\n defaultTimeout: number\n debug?: boolean\n}\n\nexport abstract class Client extends utils.EventEmitter {\n protected transport!: ClientTransport\n protected format!: BaseClientFormat\n\n auth?: string\n\n private ids = {\n call: 0,\n stream: 0,\n }\n\n constructor(\n protected readonly options: ClientOptions,\n protected services: string[],\n ) {\n super()\n if (!options.defaultTimeout) options.defaultTimeout = 15000\n }\n\n useTransport<T extends new (...args: any[]) => ClientTransport>(\n transportClass: T,\n ...options: ConstructorParameters<T>\n ) {\n this.transport = new transportClass(...options)\n this.transport.client = Object.freeze({\n services: this.services,\n format: this.format,\n auth: this.auth,\n })\n return this as Omit<this, 'useTransport'>\n }\n\n useFormat(format: BaseClientFormat) {\n this.format = format\n return this as Omit<this, 'useFormat'>\n }\n\n async connect() {\n await this.transport.connect()\n }\n\n async disconnect() {\n await this.transport.disconnect()\n }\n\n async reconnect() {\n await this.disconnect()\n await this.connect()\n }\n\n protected createCaller(\n service: string,\n procedure: string,\n {\n timeout = this.options.defaultTimeout,\n transformInput,\n transformOutput,\n }: {\n timeout?: number\n transformInput?: (input: any) => any\n transformOutput?: (output: any) => any\n } = {},\n ) {\n return async (payload: any, options: ClientCallOptions = {}) => {\n const { signal } = options\n\n const abortSignal = signal\n ? AbortSignal.any([signal, AbortSignal.timeout(timeout)])\n : AbortSignal.timeout(timeout)\n\n const callId = ++this.ids.call\n\n if (this.options.debug) {\n console.groupCollapsed(`RPC [${callId}] ${service}/${procedure}`)\n console.log(payload)\n console.groupEnd()\n }\n\n const callExecution = this.transport\n .rpc({\n callId,\n service,\n procedure,\n payload: transformInput ? transformInput(payload) : payload,\n signal: abortSignal,\n })\n .then((result) => {\n if (result.success) return result.value\n throw new ClientError(\n result.error.code,\n result.error.message,\n result.error.data,\n )\n })\n\n const callTimeout = utils.forAborted(abortSignal).catch(() => {\n const error = new ClientError(ErrorCode.RequestTimeout)\n return Promise.reject(error)\n })\n\n try {\n const response = await Promise.race([callTimeout, callExecution])\n\n if (this.options.debug) {\n console.groupCollapsed(`RPC [${callId}] Success`)\n console.log(response)\n console.groupEnd()\n }\n\n return transformOutput ? transformOutput(response) : response\n } catch (error) {\n if (this.options.debug) {\n console.groupCollapsed(`RPC [${callId}] Error`)\n console.log(error)\n console.groupEnd()\n }\n\n throw error\n }\n }\n }\n}\n"],"names":["ErrorCode","ClientError","utils","Client","EventEmitter","transport","format","auth","ids","constructor","options","services","call","stream","defaultTimeout","useTransport","transportClass","client","Object","freeze","useFormat","connect","disconnect","reconnect","createCaller","service","procedure","timeout","transformInput","transformOutput","payload","signal","abortSignal","AbortSignal","any","callId","debug","console","groupCollapsed","log","groupEnd","callExecution","rpc","then","result","success","value","error","code","message","data","callTimeout","forAborted","catch","RequestTimeout","Promise","reject","response","race"],"mappings":"AAAA,SAAgCA,SAAS,QAAQ,gBAAe;AAEhE,SAASC,WAAW,QAAQ,cAAa;AAGzC,YAAYC,WAAW,aAAY;AAOnC,OAAO,MAAeC,eAAeD,MAAME,YAAY;;;IAC3CC,UAA2B;IAC3BC,OAAyB;IAEnCC,KAAa;IAELC,IAGP;IAEDC,YACE,AAAmBC,OAAsB,EACzC,AAAUC,QAAkB,CAC5B;QACA,KAAK;aAHcD,UAAAA;aACTC,WAAAA;aAPJH,MAAM;YACZI,MAAM;YACNC,QAAQ;QACV;QAOE,IAAI,CAACH,QAAQI,cAAc,EAAEJ,QAAQI,cAAc,GAAG;IACxD;IAEAC,aACEC,cAAiB,EACjB,GAAGN,OAAiC,EACpC;QACA,IAAI,CAACL,SAAS,GAAG,IAAIW,kBAAkBN;QACvC,IAAI,CAACL,SAAS,CAACY,MAAM,GAAGC,OAAOC,MAAM,CAAC;YACpCR,UAAU,IAAI,CAACA,QAAQ;YACvBL,QAAQ,IAAI,CAACA,MAAM;YACnBC,MAAM,IAAI,CAACA,IAAI;QACjB;QACA,OAAO,IAAI;IACb;IAEAa,UAAUd,MAAwB,EAAE;QAClC,IAAI,CAACA,MAAM,GAAGA;QACd,OAAO,IAAI;IACb;IAEA,MAAMe,UAAU;QACd,MAAM,IAAI,CAAChB,SAAS,CAACgB,OAAO;IAC9B;IAEA,MAAMC,aAAa;QACjB,MAAM,IAAI,CAACjB,SAAS,CAACiB,UAAU;IACjC;IAEA,MAAMC,YAAY;QAChB,MAAM,IAAI,CAACD,UAAU;QACrB,MAAM,IAAI,CAACD,OAAO;IACpB;IAEUG,aACRC,OAAe,EACfC,SAAiB,EACjB,EACEC,UAAU,IAAI,CAACjB,OAAO,CAACI,cAAc,EACrCc,cAAc,EACdC,eAAe,EAKhB,GAAG,CAAC,CAAC,EACN;QACA,OAAO,OAAOC,SAAcpB,UAA6B,CAAC,CAAC;YACzD,MAAM,EAAEqB,MAAM,EAAE,GAAGrB;YAEnB,MAAMsB,cAAcD,SAChBE,YAAYC,GAAG,CAAC;gBAACH;gBAAQE,YAAYN,OAAO,CAACA;aAAS,IACtDM,YAAYN,OAAO,CAACA;YAExB,MAAMQ,SAAS,EAAE,IAAI,CAAC3B,GAAG,CAACI,IAAI;YAE9B,IAAI,IAAI,CAACF,OAAO,CAAC0B,KAAK,EAAE;gBACtBC,QAAQC,cAAc,CAAC,CAAC,KAAK,EAAEH,OAAO,EAAE,EAAEV,QAAQ,CAAC,EAAEC,UAAU,CAAC;gBAChEW,QAAQE,GAAG,CAACT;gBACZO,QAAQG,QAAQ;YAClB;YAEA,MAAMC,gBAAgB,IAAI,CAACpC,SAAS,CACjCqC,GAAG,CAAC;gBACHP;gBACAV;gBACAC;gBACAI,SAASF,iBAAiBA,eAAeE,WAAWA;gBACpDC,QAAQC;YACV,GACCW,IAAI,CAAC,CAACC;gBACL,IAAIA,OAAOC,OAAO,EAAE,OAAOD,OAAOE,KAAK;gBACvC,MAAM,IAAI7C,YACR2C,OAAOG,KAAK,CAACC,IAAI,EACjBJ,OAAOG,KAAK,CAACE,OAAO,EACpBL,OAAOG,KAAK,CAACG,IAAI;YAErB;YAEF,MAAMC,cAAcjD,MAAMkD,UAAU,CAACpB,aAAaqB,KAAK,CAAC;gBACtD,MAAMN,QAAQ,IAAI9C,YAAYD,UAAUsD,cAAc;gBACtD,OAAOC,QAAQC,MAAM,CAACT;YACxB;YAEA,IAAI;gBACF,MAAMU,WAAW,MAAMF,QAAQG,IAAI,CAAC;oBAACP;oBAAaV;iBAAc;gBAEhE,IAAI,IAAI,CAAC/B,OAAO,CAAC0B,KAAK,EAAE;oBACtBC,QAAQC,cAAc,CAAC,CAAC,KAAK,EAAEH,OAAO,SAAS,CAAC;oBAChDE,QAAQE,GAAG,CAACkB;oBACZpB,QAAQG,QAAQ;gBAClB;gBAEA,OAAOX,kBAAkBA,gBAAgB4B,YAAYA;YACvD,EAAE,OAAOV,OAAO;gBACd,IAAI,IAAI,CAACrC,OAAO,CAAC0B,KAAK,EAAE;oBACtBC,QAAQC,cAAc,CAAC,CAAC,KAAK,EAAEH,OAAO,OAAO,CAAC;oBAC9CE,QAAQE,GAAG,CAACQ;oBACZV,QAAQG,QAAQ;gBAClB;gBAEA,MAAMO;YACR;QACF;IACF;AACF"}
@@ -0,0 +1,9 @@
1
+ export class ClientError extends Error {
2
+ code;
3
+ data;
4
+ constructor(code, message, data){
5
+ super(message);
6
+ this.code = code;
7
+ this.data = data;
8
+ }
9
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../lib/common.ts"],"sourcesContent":["export class ClientError extends Error {\n constructor(\n public code: string,\n message?: string,\n public data?: any,\n ) {\n super(message)\n }\n}\n"],"names":["ClientError","Error","constructor","code","message","data"],"mappings":"AAAA,OAAO,MAAMA,oBAAoBC;;;IAC/BC,YACE,AAAOC,IAAY,EACnBC,OAAgB,EAChB,AAAOC,IAAU,CACjB;QACA,KAAK,CAACD;aAJCD,OAAAA;aAEAE,OAAAA;IAGT;AACF"}
@@ -0,0 +1,51 @@
1
+ export class ClientUpStream {
2
+ id;
3
+ blob;
4
+ reader;
5
+ constructor(id, blob){
6
+ this.id = id;
7
+ this.blob = blob;
8
+ if (this.blob.source instanceof ReadableStream === false) throw new Error('Blob source is not a ReadableStream');
9
+ this.reader = this.blob.source.getReader({
10
+ mode: 'byob'
11
+ });
12
+ }
13
+ }
14
+ export const createClientDownStream = (metadata, pull)=>{
15
+ let bytes = 0;
16
+ const { readable, writable } = new TransformStream({
17
+ start: ()=>pull,
18
+ transform (chunk, controller) {
19
+ if (metadata.size !== -1) {
20
+ bytes += chunk.byteLength;
21
+ if (bytes > metadata.size) {
22
+ const error = new Error('Stream size exceeded');
23
+ controller.error(error);
24
+ } else {
25
+ try {
26
+ controller.enqueue(chunk);
27
+ } catch (error) {
28
+ console.error(error);
29
+ }
30
+ }
31
+ } else {
32
+ controller.enqueue(chunk);
33
+ }
34
+ }
35
+ }, {
36
+ highWaterMark: 1
37
+ });
38
+ const writer = writable.getWriter();
39
+ const blob = {
40
+ get metadata () {
41
+ return metadata;
42
+ },
43
+ get stream () {
44
+ return readable;
45
+ }
46
+ };
47
+ return {
48
+ blob,
49
+ writer
50
+ };
51
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../lib/stream.ts"],"sourcesContent":["import type { ApiBlob, ApiBlobMetadata } from '@nmtjs/common'\nimport type { AnyFn } from './utils.ts'\n\nexport class ClientUpStream {\n readonly reader: ReadableStreamBYOBReader\n\n constructor(\n readonly id: number,\n readonly blob: ApiBlob,\n ) {\n if (this.blob.source instanceof ReadableStream === false)\n throw new Error('Blob source is not a ReadableStream')\n this.reader = this.blob.source.getReader({ mode: 'byob' })\n }\n}\n\nexport type ClientDownStreamBlob = {\n readonly metadata: ApiBlobMetadata\n readonly stream: ReadableStream<Uint8Array>\n}\n\nexport type ClientDownStreamWrapper = {\n writer: WritableStreamDefaultWriter\n blob: ClientDownStreamBlob\n}\n\nexport const createClientDownStream = (\n metadata: ApiBlobMetadata,\n pull: AnyFn,\n): ClientDownStreamWrapper => {\n let bytes = 0\n\n const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>(\n {\n start: () => pull,\n transform(chunk, controller) {\n if (metadata.size !== -1) {\n bytes += chunk.byteLength\n if (bytes > metadata.size) {\n const error = new Error('Stream size exceeded')\n controller.error(error)\n } else {\n try {\n controller.enqueue(chunk)\n } catch (error) {\n console.error(error)\n }\n }\n } else {\n controller.enqueue(chunk)\n }\n },\n },\n { highWaterMark: 1 },\n )\n\n const writer = writable.getWriter()\n\n const blob: ClientDownStreamBlob = {\n get metadata() {\n return metadata\n },\n get stream() {\n return readable\n },\n }\n\n return {\n blob,\n writer,\n }\n}\n"],"names":["ClientUpStream","reader","constructor","id","blob","source","ReadableStream","Error","getReader","mode","createClientDownStream","metadata","pull","bytes","readable","writable","TransformStream","start","transform","chunk","controller","size","byteLength","error","enqueue","console","highWaterMark","writer","getWriter","stream"],"mappings":"AAGA,OAAO,MAAMA;;;IACFC,OAAgC;IAEzCC,YACE,AAASC,EAAU,EACnB,AAASC,IAAa,CACtB;aAFSD,KAAAA;aACAC,OAAAA;QAET,IAAI,IAAI,CAACA,IAAI,CAACC,MAAM,YAAYC,mBAAmB,OACjD,MAAM,IAAIC,MAAM;QAClB,IAAI,CAACN,MAAM,GAAG,IAAI,CAACG,IAAI,CAACC,MAAM,CAACG,SAAS,CAAC;YAAEC,MAAM;QAAO;IAC1D;AACF;AAYA,OAAO,MAAMC,yBAAyB,CACpCC,UACAC;IAEA,IAAIC,QAAQ;IAEZ,MAAM,EAAEC,QAAQ,EAAEC,QAAQ,EAAE,GAAG,IAAIC,gBACjC;QACEC,OAAO,IAAML;QACbM,WAAUC,KAAK,EAAEC,UAAU;YACzB,IAAIT,SAASU,IAAI,KAAK,CAAC,GAAG;gBACxBR,SAASM,MAAMG,UAAU;gBACzB,IAAIT,QAAQF,SAASU,IAAI,EAAE;oBACzB,MAAME,QAAQ,IAAIhB,MAAM;oBACxBa,WAAWG,KAAK,CAACA;gBACnB,OAAO;oBACL,IAAI;wBACFH,WAAWI,OAAO,CAACL;oBACrB,EAAE,OAAOI,OAAO;wBACdE,QAAQF,KAAK,CAACA;oBAChB;gBACF;YACF,OAAO;gBACLH,WAAWI,OAAO,CAACL;YACrB;QACF;IACF,GACA;QAAEO,eAAe;IAAE;IAGrB,MAAMC,SAASZ,SAASa,SAAS;IAEjC,MAAMxB,OAA6B;QACjC,IAAIO,YAAW;YACb,OAAOA;QACT;QACA,IAAIkB,UAAS;YACX,OAAOf;QACT;IACF;IAEA,OAAO;QACLV;QACAuB;IACF;AACF,EAAC"}
@@ -0,0 +1,10 @@
1
+ import { EventEmitter } from "./utils.js";
2
+ export class Subscription extends EventEmitter {
3
+ key;
4
+ unsubscribe;
5
+ constructor(key, unsubscribe){
6
+ super();
7
+ this.key = key;
8
+ this.unsubscribe = unsubscribe;
9
+ }
10
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../lib/subscription.ts"],"sourcesContent":["import type { TSubscriptionContract } from '@nmtjs/contract'\nimport { EventEmitter } from './utils.ts'\n\nexport class Subscription<\n Contact extends TSubscriptionContract = TSubscriptionContract,\n> extends EventEmitter<{\n [K in keyof Contact['events']]: [Contact['events'][K]['static']['payload']]\n}> {\n constructor(\n readonly key: string,\n readonly unsubscribe: () => void,\n ) {\n super()\n }\n}\n"],"names":["EventEmitter","Subscription","constructor","key","unsubscribe"],"mappings":"AACA,SAASA,YAAY,QAAQ,aAAY;AAEzC,OAAO,MAAMC,qBAEHD;;;IAGRE,YACE,AAASC,GAAW,EACpB,AAASC,WAAuB,CAChC;QACA,KAAK;aAHID,MAAAA;aACAC,cAAAA;IAGX;AACF"}
@@ -0,0 +1,4 @@
1
+ import { EventEmitter } from "./utils.js";
2
+ export class ClientTransport extends EventEmitter {
3
+ client;
4
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../lib/transport.ts"],"sourcesContent":["import type { BaseClientFormat } from '@nmtjs/common'\nimport { EventEmitter, type EventMap } from './utils.ts'\n\nexport type ClientTransportRpcCall = {\n service: string\n procedure: string\n callId: number\n payload: any\n signal: AbortSignal\n}\n\nexport type ClientTransportRpcResult =\n | {\n success: false\n error: { code: string; message?: string; data?: any }\n }\n | {\n success: true\n value: any\n }\n\nexport abstract class ClientTransport<\n T extends EventMap = {},\n> extends EventEmitter<\n T & { event: [service: string, event: string, payload: any] }\n> {\n abstract type: string\n\n client!: {\n readonly services: string[]\n readonly format: BaseClientFormat\n readonly auth?: string\n }\n\n abstract connect(): Promise<void>\n abstract disconnect(): Promise<void>\n abstract rpc(\n params: ClientTransportRpcCall,\n ): Promise<ClientTransportRpcResult>\n}\n"],"names":["EventEmitter","ClientTransport","client"],"mappings":"AACA,SAASA,YAAY,QAAuB,aAAY;AAoBxD,OAAO,MAAeC,wBAEZD;IAKRE,OAIC;AAOH"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../lib/types.ts"],"sourcesContent":["import type { ApiBlob, ApiBlobInterface } from '@nmtjs/common'\nimport type { ClientDownStreamBlob } from './stream.ts'\n\nexport type ClientCallOptions = {\n signal?: AbortSignal\n}\n\nexport type InputType<T> = T extends any[]\n ? InputType<T[number]>[]\n : T extends ApiBlobInterface\n ? ApiBlob\n : T extends object\n ? { [K in keyof T]: InputType<T[K]> }\n : T\n\nexport type OutputType<T> = T extends any[]\n ? OutputType<T[number]>[]\n : T extends ApiBlobInterface\n ? ClientDownStreamBlob\n : T extends object\n ? { [K in keyof T]: OutputType<T[K]> }\n : T\n"],"names":[],"mappings":"AAeA,WAMS"}
@@ -0,0 +1,40 @@
1
+ export function forAborted(signal) {
2
+ return new Promise((_, reject)=>{
3
+ const handler = ()=>reject(new Error('aborted'));
4
+ const options = {
5
+ once: true
6
+ };
7
+ signal.addEventListener('abort', handler, options);
8
+ });
9
+ }
10
+ export function onAbort(signal, listener) {
11
+ signal.addEventListener('abort', listener, {
12
+ once: true
13
+ });
14
+ return ()=>signal.removeEventListener('abort', listener);
15
+ }
16
+ export class EventEmitter {
17
+ #target = new EventTarget();
18
+ #listeners = new Map();
19
+ on(event, listener, options) {
20
+ const wrapper = (event)=>listener(...event.detail);
21
+ this.#listeners.set(listener, wrapper);
22
+ this.#target.addEventListener(event, wrapper, options);
23
+ return ()=>this.#target.removeEventListener(event, wrapper);
24
+ }
25
+ once(event, listener) {
26
+ return this.on(event, listener, {
27
+ once: true
28
+ });
29
+ }
30
+ off(event, listener) {
31
+ const wrapper = this.#listeners.get(listener);
32
+ if (wrapper) this.#target.removeEventListener(event, wrapper);
33
+ }
34
+ emit(event, ...args) {
35
+ return this.#target.dispatchEvent(new CustomEvent(event, {
36
+ detail: args
37
+ }));
38
+ }
39
+ }
40
+ export const once = (ee, event)=>new Promise((resolve)=>ee.once(event, resolve));
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../lib/utils.ts"],"sourcesContent":["export type AnyFn = (...args: any[]) => any\n\nexport type EventMap = { [K: string]: any[] }\n\nexport function forAborted(signal: AbortSignal) {\n return new Promise((_, reject) => {\n const handler = () => reject(new Error('aborted'))\n const options = { once: true }\n signal.addEventListener('abort', handler, options)\n })\n}\n\nexport function onAbort(signal: AbortSignal, listener: () => void) {\n signal.addEventListener('abort', listener, { once: true })\n return () => signal.removeEventListener('abort', listener)\n}\n\n/**\n * Very simple node-like event emitter wrapper around EventTarget\n *\n * @todo add errors and promise rejections handling\n */\nexport class EventEmitter<\n Events extends EventMap = EventMap,\n EventNames extends Extract<keyof Events, string> = Extract<\n keyof Events,\n string\n >,\n> {\n #target = new EventTarget()\n #listeners = new Map<AnyFn, AnyFn>()\n\n on<E extends EventNames>(\n event: E | (Object & string),\n listener: (...args: Events[E]) => void,\n options?: AddEventListenerOptions,\n ) {\n const wrapper = (event) => listener(...event.detail)\n this.#listeners.set(listener, wrapper)\n this.#target.addEventListener(event, wrapper, options)\n return () => this.#target.removeEventListener(event, wrapper)\n }\n\n once<E extends EventNames>(\n event: E | (Object & string),\n listener: (...args: Events[E]) => void,\n ) {\n return this.on(event, listener, { once: true })\n }\n\n off(event: EventNames | (Object & string), listener: AnyFn) {\n const wrapper = this.#listeners.get(listener)\n if (wrapper) this.#target.removeEventListener(event, wrapper)\n }\n\n emit<E extends EventNames | (Object & string)>(\n event: E,\n ...args: E extends EventEmitter ? Events[E] : any[]\n ) {\n return this.#target.dispatchEvent(new CustomEvent(event, { detail: args }))\n }\n}\n\nexport const once = (ee: EventEmitter, event: string) =>\n new Promise((resolve) => ee.once(event, resolve))\n"],"names":["forAborted","signal","Promise","_","reject","handler","Error","options","once","addEventListener","onAbort","listener","removeEventListener","EventEmitter","EventTarget","Map","on","event","wrapper","detail","set","off","get","emit","args","dispatchEvent","CustomEvent","ee","resolve"],"mappings":"AAIA,OAAO,SAASA,WAAWC,MAAmB;IAC5C,OAAO,IAAIC,QAAQ,CAACC,GAAGC;QACrB,MAAMC,UAAU,IAAMD,OAAO,IAAIE,MAAM;QACvC,MAAMC,UAAU;YAAEC,MAAM;QAAK;QAC7BP,OAAOQ,gBAAgB,CAAC,SAASJ,SAASE;IAC5C;AACF;AAEA,OAAO,SAASG,QAAQT,MAAmB,EAAEU,QAAoB;IAC/DV,OAAOQ,gBAAgB,CAAC,SAASE,UAAU;QAAEH,MAAM;IAAK;IACxD,OAAO,IAAMP,OAAOW,mBAAmB,CAAC,SAASD;AACnD;AAOA,OAAO,MAAME;IAOX,CAAA,MAAO,GAAG,IAAIC,cAAa;IAC3B,CAAA,SAAU,GAAG,IAAIC,MAAmB;IAEpCC,GACEC,KAA4B,EAC5BN,QAAsC,EACtCJ,OAAiC,EACjC;QACA,MAAMW,UAAU,CAACD,QAAUN,YAAYM,MAAME,MAAM;QACnD,IAAI,CAAC,CAAA,SAAU,CAACC,GAAG,CAACT,UAAUO;QAC9B,IAAI,CAAC,CAAA,MAAO,CAACT,gBAAgB,CAACQ,OAAOC,SAASX;QAC9C,OAAO,IAAM,IAAI,CAAC,CAAA,MAAO,CAACK,mBAAmB,CAACK,OAAOC;IACvD;IAEAV,KACES,KAA4B,EAC5BN,QAAsC,EACtC;QACA,OAAO,IAAI,CAACK,EAAE,CAACC,OAAON,UAAU;YAAEH,MAAM;QAAK;IAC/C;IAEAa,IAAIJ,KAAqC,EAAEN,QAAe,EAAE;QAC1D,MAAMO,UAAU,IAAI,CAAC,CAAA,SAAU,CAACI,GAAG,CAACX;QACpC,IAAIO,SAAS,IAAI,CAAC,CAAA,MAAO,CAACN,mBAAmB,CAACK,OAAOC;IACvD;IAEAK,KACEN,KAAQ,EACR,GAAGO,IAAgD,EACnD;QACA,OAAO,IAAI,CAAC,CAAA,MAAO,CAACC,aAAa,CAAC,IAAIC,YAAYT,OAAO;YAAEE,QAAQK;QAAK;IAC1E;AACF;AAEA,OAAO,MAAMhB,OAAO,CAACmB,IAAkBV,QACrC,IAAIf,QAAQ,CAAC0B,UAAYD,GAAGnB,IAAI,CAACS,OAAOW,UAAS"}
package/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './lib/client.ts'
2
+ export * from './lib/common.ts'
3
+ export * from './lib/stream.ts'
4
+ export * from './lib/subscription.ts'
5
+ export * from './lib/transport.ts'
6
+ export * from './lib/utils.ts'
@@ -0,0 +1,142 @@
1
+ import type {
2
+ Decoded,
3
+ TEventContract,
4
+ TSchema,
5
+ TServiceContract,
6
+ TSubscriptionContract,
7
+ } from '@nmtjs/contract'
8
+ import { type Compiled, compile } from '@nmtjs/contract/compiler'
9
+ import { ContractGuard } from '@nmtjs/contract/guards'
10
+ import { Client, type ClientOptions } from './client.ts'
11
+ import type { Subscription } from './subscription.ts'
12
+ import type { ClientCallOptions, InputType, OutputType } from './types.ts'
13
+
14
+ type CompiledContract<T extends TServiceContract = TServiceContract> = {
15
+ compiled: Map<TSchema, Compiled>
16
+ contract: T
17
+ }
18
+
19
+ type ClientServices = Record<string, CompiledContract>
20
+
21
+ type ClientCallers<Services extends ClientServices> = {
22
+ [K in keyof Services]: {
23
+ [P in keyof Services[K]['contract']['procedures']]: (
24
+ ...args: Decoded<
25
+ Services[K]['contract']['procedures'][P]['input']
26
+ > extends never
27
+ ? [options?: ClientCallOptions]
28
+ : [
29
+ data: InputType<
30
+ Decoded<Services[K]['contract']['procedures'][P]['input']>
31
+ >,
32
+ options?: ClientCallOptions,
33
+ ]
34
+ ) => Promise<
35
+ Services[K]['contract']['procedures'][P] extends TSubscriptionContract
36
+ ? {
37
+ payload: Decoded<
38
+ Services[K]['contract']['procedures'][P]['output']
39
+ > extends never
40
+ ? undefined
41
+ : Decoded<Services[K]['contract']['procedures'][P]['output']>
42
+ subscription: Subscription<Services[K]['contract']['procedures'][P]>
43
+ }
44
+ : Decoded<
45
+ Services[K]['contract']['procedures'][P]['static']
46
+ > extends never
47
+ ? void
48
+ : OutputType<
49
+ Decoded<Services[K]['contract']['procedures'][P]['output']>
50
+ >
51
+ >
52
+ }
53
+ }
54
+
55
+ export class RuntimeClient<Services extends ClientServices> extends Client {
56
+ #callers: ClientCallers<Services>
57
+
58
+ constructor(services: Services, options: ClientOptions) {
59
+ super(
60
+ options,
61
+ Object.values(services).map((s) => s.contract.name),
62
+ )
63
+
64
+ const callers = {} as any
65
+ for (const [serviceKey, serviceContract] of Object.entries(services)) {
66
+ if (!serviceContract.contract.transports[this.transport.type])
67
+ throw new Error(
68
+ `Transport [${this.transport.type}] not supported for service [${serviceContract.contract.name}]`,
69
+ )
70
+
71
+ callers[serviceKey] = {} as any
72
+
73
+ for (const procedureName in serviceContract.contract.procedures) {
74
+ const { input, output } =
75
+ serviceContract.contract.procedures[procedureName]
76
+
77
+ callers[serviceKey][procedureName] = this.createCaller(
78
+ serviceContract.contract.name,
79
+ procedureName,
80
+ {
81
+ timeout: serviceContract.contract.timeout,
82
+ transformInput: (data: any) => {
83
+ if (ContractGuard.IsNever(data)) return undefined
84
+ const compiled = serviceContract.compiled.get(input)!
85
+ const result = compiled.encode(data)
86
+ if (result.success) {
87
+ return result.value
88
+ } else {
89
+ console.dir(result.error)
90
+ throw new Error('Failed to encode input', {
91
+ cause: result.error,
92
+ })
93
+ }
94
+ },
95
+ transformOutput: (data: any) => {
96
+ if (ContractGuard.IsNever(data)) return undefined
97
+ const compiled = serviceContract.compiled.get(output)!
98
+ const result = compiled.decode(data)
99
+ if (result.success) {
100
+ return result.value
101
+ } else {
102
+ console.dir(result.error)
103
+ throw new Error('Failed to decode output', {
104
+ cause: result.error,
105
+ })
106
+ }
107
+ },
108
+ },
109
+ )
110
+ }
111
+ }
112
+ this.#callers = callers
113
+ }
114
+
115
+ get call() {
116
+ return this.#callers
117
+ }
118
+ }
119
+
120
+ export const compileContract = <T extends TServiceContract>(
121
+ contract: T,
122
+ ): CompiledContract<T> => {
123
+ const compiled = new Map<TSchema, Compiled>()
124
+ for (const procedureContract of Object.values(contract.procedures)) {
125
+ const { input, output, events } = procedureContract
126
+ if (ContractGuard.IsSubscription(procedureContract)) {
127
+ for (const event of Object.values(events) as TEventContract[]) {
128
+ compiled.set(event, compile(event))
129
+ }
130
+ }
131
+ compiled.set(input, compile(input))
132
+ compiled.set(output, compile(output))
133
+ }
134
+ for (const eventContract of Object.values(contract.events)) {
135
+ compiled.set(eventContract, compile(eventContract))
136
+ }
137
+
138
+ return {
139
+ compiled,
140
+ contract,
141
+ }
142
+ }
@@ -0,0 +1,63 @@
1
+ import type {
2
+ Encoded,
3
+ TServiceContract,
4
+ TSubscriptionContract,
5
+ } from '@nmtjs/contract'
6
+ import { Client, type ClientOptions } from './client.ts'
7
+ import type { Subscription } from './subscription.ts'
8
+ import type { ClientCallOptions, InputType, OutputType } from './types.ts'
9
+
10
+ type ClientServices = Record<string, TServiceContract>
11
+
12
+ type ClientCallers<Services extends ClientServices> = {
13
+ [K in keyof Services]: {
14
+ [P in keyof Services[K]['procedures']]: (
15
+ ...args: Encoded<Services[K]['procedures'][P]['input']> extends never
16
+ ? [options?: ClientCallOptions]
17
+ : [
18
+ data: InputType<Encoded<Services[K]['procedures'][P]['input']>>,
19
+ options?: ClientCallOptions,
20
+ ]
21
+ ) => Promise<
22
+ Services[K]['procedures'][P] extends TSubscriptionContract
23
+ ? {
24
+ payload: Encoded<
25
+ Services[K]['procedures'][P]['output']
26
+ > extends never
27
+ ? undefined
28
+ : Encoded<Services[K]['procedures'][P]['output']>
29
+ subscription: Subscription<Services[K]['procedures'][P]>
30
+ }
31
+ : Encoded<Services[K]['procedures'][P]['static']> extends never
32
+ ? void
33
+ : OutputType<Encoded<Services[K]['procedures'][P]['output']>>
34
+ >
35
+ }
36
+ }
37
+
38
+ export class StaticClient<Services extends ClientServices> extends Client {
39
+ #callers: ClientCallers<Services>
40
+
41
+ constructor(
42
+ services: { [K in keyof Services]: Services[K]['name'] },
43
+ options: ClientOptions,
44
+ ) {
45
+ super(options, Object.values(services))
46
+
47
+ const callers = {} as any
48
+
49
+ for (const [serviceKey, serviceName] of Object.entries(services)) {
50
+ callers[serviceKey] = new Proxy(Object(), {
51
+ get: (target, prop, receiver) => {
52
+ return this.createCaller(serviceName, prop as string)
53
+ },
54
+ })
55
+ }
56
+
57
+ this.#callers = callers
58
+ }
59
+
60
+ get call() {
61
+ return this.#callers
62
+ }
63
+ }
package/lib/client.ts ADDED
@@ -0,0 +1,134 @@
1
+ import { type BaseClientFormat, ErrorCode } from '@nmtjs/common'
2
+
3
+ import { ClientError } from './common.ts'
4
+ import type { ClientTransport } from './transport.ts'
5
+ import type { ClientCallOptions } from './types.ts'
6
+ import * as utils from './utils.ts'
7
+
8
+ export type ClientOptions = {
9
+ defaultTimeout: number
10
+ debug?: boolean
11
+ }
12
+
13
+ export abstract class Client extends utils.EventEmitter {
14
+ protected transport!: ClientTransport
15
+ protected format!: BaseClientFormat
16
+
17
+ auth?: string
18
+
19
+ private ids = {
20
+ call: 0,
21
+ stream: 0,
22
+ }
23
+
24
+ constructor(
25
+ protected readonly options: ClientOptions,
26
+ protected services: string[],
27
+ ) {
28
+ super()
29
+ if (!options.defaultTimeout) options.defaultTimeout = 15000
30
+ }
31
+
32
+ useTransport<T extends new (...args: any[]) => ClientTransport>(
33
+ transportClass: T,
34
+ ...options: ConstructorParameters<T>
35
+ ) {
36
+ this.transport = new transportClass(...options)
37
+ this.transport.client = Object.freeze({
38
+ services: this.services,
39
+ format: this.format,
40
+ auth: this.auth,
41
+ })
42
+ return this as Omit<this, 'useTransport'>
43
+ }
44
+
45
+ useFormat(format: BaseClientFormat) {
46
+ this.format = format
47
+ return this as Omit<this, 'useFormat'>
48
+ }
49
+
50
+ async connect() {
51
+ await this.transport.connect()
52
+ }
53
+
54
+ async disconnect() {
55
+ await this.transport.disconnect()
56
+ }
57
+
58
+ async reconnect() {
59
+ await this.disconnect()
60
+ await this.connect()
61
+ }
62
+
63
+ protected createCaller(
64
+ service: string,
65
+ procedure: string,
66
+ {
67
+ timeout = this.options.defaultTimeout,
68
+ transformInput,
69
+ transformOutput,
70
+ }: {
71
+ timeout?: number
72
+ transformInput?: (input: any) => any
73
+ transformOutput?: (output: any) => any
74
+ } = {},
75
+ ) {
76
+ return async (payload: any, options: ClientCallOptions = {}) => {
77
+ const { signal } = options
78
+
79
+ const abortSignal = signal
80
+ ? AbortSignal.any([signal, AbortSignal.timeout(timeout)])
81
+ : AbortSignal.timeout(timeout)
82
+
83
+ const callId = ++this.ids.call
84
+
85
+ if (this.options.debug) {
86
+ console.groupCollapsed(`RPC [${callId}] ${service}/${procedure}`)
87
+ console.log(payload)
88
+ console.groupEnd()
89
+ }
90
+
91
+ const callExecution = this.transport
92
+ .rpc({
93
+ callId,
94
+ service,
95
+ procedure,
96
+ payload: transformInput ? transformInput(payload) : payload,
97
+ signal: abortSignal,
98
+ })
99
+ .then((result) => {
100
+ if (result.success) return result.value
101
+ throw new ClientError(
102
+ result.error.code,
103
+ result.error.message,
104
+ result.error.data,
105
+ )
106
+ })
107
+
108
+ const callTimeout = utils.forAborted(abortSignal).catch(() => {
109
+ const error = new ClientError(ErrorCode.RequestTimeout)
110
+ return Promise.reject(error)
111
+ })
112
+
113
+ try {
114
+ const response = await Promise.race([callTimeout, callExecution])
115
+
116
+ if (this.options.debug) {
117
+ console.groupCollapsed(`RPC [${callId}] Success`)
118
+ console.log(response)
119
+ console.groupEnd()
120
+ }
121
+
122
+ return transformOutput ? transformOutput(response) : response
123
+ } catch (error) {
124
+ if (this.options.debug) {
125
+ console.groupCollapsed(`RPC [${callId}] Error`)
126
+ console.log(error)
127
+ console.groupEnd()
128
+ }
129
+
130
+ throw error
131
+ }
132
+ }
133
+ }
134
+ }
package/lib/common.ts ADDED
@@ -0,0 +1,9 @@
1
+ export class ClientError extends Error {
2
+ constructor(
3
+ public code: string,
4
+ message?: string,
5
+ public data?: any,
6
+ ) {
7
+ super(message)
8
+ }
9
+ }
package/lib/stream.ts ADDED
@@ -0,0 +1,72 @@
1
+ import type { ApiBlob, ApiBlobMetadata } from '@nmtjs/common'
2
+ import type { AnyFn } from './utils.ts'
3
+
4
+ export class ClientUpStream {
5
+ readonly reader: ReadableStreamBYOBReader
6
+
7
+ constructor(
8
+ readonly id: number,
9
+ readonly blob: ApiBlob,
10
+ ) {
11
+ if (this.blob.source instanceof ReadableStream === false)
12
+ throw new Error('Blob source is not a ReadableStream')
13
+ this.reader = this.blob.source.getReader({ mode: 'byob' })
14
+ }
15
+ }
16
+
17
+ export type ClientDownStreamBlob = {
18
+ readonly metadata: ApiBlobMetadata
19
+ readonly stream: ReadableStream<Uint8Array>
20
+ }
21
+
22
+ export type ClientDownStreamWrapper = {
23
+ writer: WritableStreamDefaultWriter
24
+ blob: ClientDownStreamBlob
25
+ }
26
+
27
+ export const createClientDownStream = (
28
+ metadata: ApiBlobMetadata,
29
+ pull: AnyFn,
30
+ ): ClientDownStreamWrapper => {
31
+ let bytes = 0
32
+
33
+ const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>(
34
+ {
35
+ start: () => pull,
36
+ transform(chunk, controller) {
37
+ if (metadata.size !== -1) {
38
+ bytes += chunk.byteLength
39
+ if (bytes > metadata.size) {
40
+ const error = new Error('Stream size exceeded')
41
+ controller.error(error)
42
+ } else {
43
+ try {
44
+ controller.enqueue(chunk)
45
+ } catch (error) {
46
+ console.error(error)
47
+ }
48
+ }
49
+ } else {
50
+ controller.enqueue(chunk)
51
+ }
52
+ },
53
+ },
54
+ { highWaterMark: 1 },
55
+ )
56
+
57
+ const writer = writable.getWriter()
58
+
59
+ const blob: ClientDownStreamBlob = {
60
+ get metadata() {
61
+ return metadata
62
+ },
63
+ get stream() {
64
+ return readable
65
+ },
66
+ }
67
+
68
+ return {
69
+ blob,
70
+ writer,
71
+ }
72
+ }
@@ -0,0 +1,15 @@
1
+ import type { TSubscriptionContract } from '@nmtjs/contract'
2
+ import { EventEmitter } from './utils.ts'
3
+
4
+ export class Subscription<
5
+ Contact extends TSubscriptionContract = TSubscriptionContract,
6
+ > extends EventEmitter<{
7
+ [K in keyof Contact['events']]: [Contact['events'][K]['static']['payload']]
8
+ }> {
9
+ constructor(
10
+ readonly key: string,
11
+ readonly unsubscribe: () => void,
12
+ ) {
13
+ super()
14
+ }
15
+ }
@@ -0,0 +1,40 @@
1
+ import type { BaseClientFormat } from '@nmtjs/common'
2
+ import { EventEmitter, type EventMap } from './utils.ts'
3
+
4
+ export type ClientTransportRpcCall = {
5
+ service: string
6
+ procedure: string
7
+ callId: number
8
+ payload: any
9
+ signal: AbortSignal
10
+ }
11
+
12
+ export type ClientTransportRpcResult =
13
+ | {
14
+ success: false
15
+ error: { code: string; message?: string; data?: any }
16
+ }
17
+ | {
18
+ success: true
19
+ value: any
20
+ }
21
+
22
+ export abstract class ClientTransport<
23
+ T extends EventMap = {},
24
+ > extends EventEmitter<
25
+ T & { event: [service: string, event: string, payload: any] }
26
+ > {
27
+ abstract type: string
28
+
29
+ client!: {
30
+ readonly services: string[]
31
+ readonly format: BaseClientFormat
32
+ readonly auth?: string
33
+ }
34
+
35
+ abstract connect(): Promise<void>
36
+ abstract disconnect(): Promise<void>
37
+ abstract rpc(
38
+ params: ClientTransportRpcCall,
39
+ ): Promise<ClientTransportRpcResult>
40
+ }
package/lib/types.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { ApiBlob, ApiBlobInterface } from '@nmtjs/common'
2
+ import type { ClientDownStreamBlob } from './stream.ts'
3
+
4
+ export type ClientCallOptions = {
5
+ signal?: AbortSignal
6
+ }
7
+
8
+ export type InputType<T> = T extends any[]
9
+ ? InputType<T[number]>[]
10
+ : T extends ApiBlobInterface
11
+ ? ApiBlob
12
+ : T extends object
13
+ ? { [K in keyof T]: InputType<T[K]> }
14
+ : T
15
+
16
+ export type OutputType<T> = T extends any[]
17
+ ? OutputType<T[number]>[]
18
+ : T extends ApiBlobInterface
19
+ ? ClientDownStreamBlob
20
+ : T extends object
21
+ ? { [K in keyof T]: OutputType<T[K]> }
22
+ : T
package/lib/utils.ts ADDED
@@ -0,0 +1,65 @@
1
+ export type AnyFn = (...args: any[]) => any
2
+
3
+ export type EventMap = { [K: string]: any[] }
4
+
5
+ export function forAborted(signal: AbortSignal) {
6
+ return new Promise((_, reject) => {
7
+ const handler = () => reject(new Error('aborted'))
8
+ const options = { once: true }
9
+ signal.addEventListener('abort', handler, options)
10
+ })
11
+ }
12
+
13
+ export function onAbort(signal: AbortSignal, listener: () => void) {
14
+ signal.addEventListener('abort', listener, { once: true })
15
+ return () => signal.removeEventListener('abort', listener)
16
+ }
17
+
18
+ /**
19
+ * Very simple node-like event emitter wrapper around EventTarget
20
+ *
21
+ * @todo add errors and promise rejections handling
22
+ */
23
+ export class EventEmitter<
24
+ Events extends EventMap = EventMap,
25
+ EventNames extends Extract<keyof Events, string> = Extract<
26
+ keyof Events,
27
+ string
28
+ >,
29
+ > {
30
+ #target = new EventTarget()
31
+ #listeners = new Map<AnyFn, AnyFn>()
32
+
33
+ on<E extends EventNames>(
34
+ event: E | (Object & string),
35
+ listener: (...args: Events[E]) => void,
36
+ options?: AddEventListenerOptions,
37
+ ) {
38
+ const wrapper = (event) => listener(...event.detail)
39
+ this.#listeners.set(listener, wrapper)
40
+ this.#target.addEventListener(event, wrapper, options)
41
+ return () => this.#target.removeEventListener(event, wrapper)
42
+ }
43
+
44
+ once<E extends EventNames>(
45
+ event: E | (Object & string),
46
+ listener: (...args: Events[E]) => void,
47
+ ) {
48
+ return this.on(event, listener, { once: true })
49
+ }
50
+
51
+ off(event: EventNames | (Object & string), listener: AnyFn) {
52
+ const wrapper = this.#listeners.get(listener)
53
+ if (wrapper) this.#target.removeEventListener(event, wrapper)
54
+ }
55
+
56
+ emit<E extends EventNames | (Object & string)>(
57
+ event: E,
58
+ ...args: E extends EventEmitter ? Events[E] : any[]
59
+ ) {
60
+ return this.#target.dispatchEvent(new CustomEvent(event, { detail: args }))
61
+ }
62
+ }
63
+
64
+ export const once = (ee: EventEmitter, event: string) =>
65
+ new Promise((resolve) => ee.once(event, resolve))
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@nmtjs/client",
3
+ "type": "module",
4
+ "exports": {
5
+ ".": {
6
+ "bun": "./index.ts",
7
+ "default": "./dist/index.js",
8
+ "types": "./index.ts"
9
+ },
10
+ "./runtime": {
11
+ "bun": "./lib/client-runtime.ts",
12
+ "default": "./dist/lib/client-runtime.js",
13
+ "types": "./lib/client-runtime.ts"
14
+ },
15
+ "./static": {
16
+ "bun": "./lib/client-static.ts",
17
+ "default": "./dist/lib/client-static.js",
18
+ "types": "./lib/client-static.ts"
19
+ }
20
+ },
21
+ "peerDependencies": {
22
+ "@nmtjs/contract": "0.0.1",
23
+ "@nmtjs/common": "0.0.1"
24
+ },
25
+ "devDependencies": {
26
+ "@nmtjs/contract": "0.0.1",
27
+ "@nmtjs/common": "0.0.1"
28
+ },
29
+ "files": [
30
+ "index.ts",
31
+ "lib",
32
+ "dist",
33
+ "tsconfig.json",
34
+ "LICENSE.md",
35
+ "README.md"
36
+ ],
37
+ "version": "0.0.1",
38
+ "scripts": {
39
+ "build": "neemata-build -p neutral ./index.ts './lib/**/*.ts'",
40
+ "type-check": "tsc --noEmit"
41
+ }
42
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../tsconfig.json"
3
+ }