@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 +7 -0
- package/README.md +9 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/client-runtime.js +70 -0
- package/dist/lib/client-runtime.js.map +1 -0
- package/dist/lib/client-static.js +19 -0
- package/dist/lib/client-static.js.map +1 -0
- package/dist/lib/client.js +92 -0
- package/dist/lib/client.js.map +1 -0
- package/dist/lib/common.js +9 -0
- package/dist/lib/common.js.map +1 -0
- package/dist/lib/stream.js +51 -0
- package/dist/lib/stream.js.map +1 -0
- package/dist/lib/subscription.js +10 -0
- package/dist/lib/subscription.js.map +1 -0
- package/dist/lib/transport.js +4 -0
- package/dist/lib/transport.js.map +1 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/utils.js +40 -0
- package/dist/lib/utils.js.map +1 -0
- package/index.ts +6 -0
- package/lib/client-runtime.ts +142 -0
- package/lib/client-static.ts +63 -0
- package/lib/client.ts +134 -0
- package/lib/common.ts +9 -0
- package/lib/stream.ts +72 -0
- package/lib/subscription.ts +15 -0
- package/lib/transport.ts +40 -0
- package/lib/types.ts +22 -0
- package/lib/utils.ts +65 -0
- package/package.json +42 -0
- package/tsconfig.json +3 -0
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 @@
|
|
|
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":"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"}
|
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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,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
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
|
+
}
|
package/lib/transport.ts
ADDED
|
@@ -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