@nmtjs/client 0.14.5 → 0.15.0-beta.2
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 +1 -1
- package/README.md +1 -1
- package/dist/clients/runtime.js +80 -0
- package/dist/clients/static.js +26 -0
- package/dist/core.js +410 -0
- package/dist/events.js +33 -0
- package/dist/index.js +5 -0
- package/dist/streams.js +100 -0
- package/dist/transformers.js +8 -0
- package/dist/transport.js +1 -0
- package/dist/types.js +1 -1
- package/package.json +15 -24
- package/dist/common.d.ts +0 -38
- package/dist/common.js +0 -60
- package/dist/runtime.d.ts +0 -21
- package/dist/runtime.js +0 -105
- package/dist/static.d.ts +0 -9
- package/dist/static.js +0 -27
- package/dist/types.d.ts +0 -61
package/LICENSE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copyright (c)
|
|
1
|
+
Copyright (c) 2025 Denys Ilchyshyn
|
|
2
2
|
|
|
3
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
4
|
|
package/README.md
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { IsProcedureContract, IsRouterContract } from '@nmtjs/contract';
|
|
2
|
+
import { BaseClient } from "../core.js";
|
|
3
|
+
export class RuntimeContractTransformer {
|
|
4
|
+
#procedures = new Map();
|
|
5
|
+
constructor(router) {
|
|
6
|
+
const registerProcedures = (r, path = []) => {
|
|
7
|
+
if (IsRouterContract(r)) {
|
|
8
|
+
for (const [key, route] of Object.entries(r.routes)) {
|
|
9
|
+
registerProcedures(route, [...path, key]);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
else if (IsProcedureContract(r)) {
|
|
13
|
+
const fullName = [...path].join('/');
|
|
14
|
+
this.#procedures.set(fullName, r);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
registerProcedures(router);
|
|
18
|
+
}
|
|
19
|
+
encode(_procedure, payload) {
|
|
20
|
+
const procedure = this.#procedures.get(_procedure);
|
|
21
|
+
if (!procedure)
|
|
22
|
+
throw new Error(`Procedure not found: ${_procedure}`);
|
|
23
|
+
return procedure.input.encode(payload);
|
|
24
|
+
}
|
|
25
|
+
decode(_procedure, payload) {
|
|
26
|
+
const procedure = this.#procedures.get(_procedure);
|
|
27
|
+
if (!procedure)
|
|
28
|
+
throw new Error(`Procedure not found: ${_procedure}`);
|
|
29
|
+
return procedure.output.decode(payload);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class RuntimeClient extends BaseClient {
|
|
33
|
+
transformer;
|
|
34
|
+
#procedures = new Map();
|
|
35
|
+
#callers;
|
|
36
|
+
constructor(options, transport, transportOptions) {
|
|
37
|
+
super(options, transport, transportOptions);
|
|
38
|
+
this.resolveProcedures(this.options.contract);
|
|
39
|
+
this.transformer = new RuntimeContractTransformer(this.options.contract);
|
|
40
|
+
this.#callers = this.buildCallers();
|
|
41
|
+
}
|
|
42
|
+
get call() {
|
|
43
|
+
return this.#callers;
|
|
44
|
+
}
|
|
45
|
+
get stream() {
|
|
46
|
+
return this.#callers;
|
|
47
|
+
}
|
|
48
|
+
resolveProcedures(router, path = []) {
|
|
49
|
+
for (const [key, route] of Object.entries(router.routes)) {
|
|
50
|
+
if (IsRouterContract(route)) {
|
|
51
|
+
this.resolveProcedures(route, [...path, key]);
|
|
52
|
+
}
|
|
53
|
+
else if (IsProcedureContract(route)) {
|
|
54
|
+
const fullName = [...path, key].join('/');
|
|
55
|
+
this.#procedures.set(fullName, route);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
buildCallers() {
|
|
60
|
+
const callers = Object.create(null);
|
|
61
|
+
for (const [name, { stream }] of this.#procedures) {
|
|
62
|
+
const parts = name.split('/');
|
|
63
|
+
let current = callers;
|
|
64
|
+
for (let i = 0; i < parts.length; i++) {
|
|
65
|
+
const part = parts[i];
|
|
66
|
+
if (i === parts.length - 1) {
|
|
67
|
+
current[part] = (payload, options) => this._call(name, payload, {
|
|
68
|
+
...options,
|
|
69
|
+
_stream_response: !!stream,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
current[part] = current[part] ?? Object.create(null);
|
|
74
|
+
current = current[part];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return callers;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BaseClient } from "../core.js";
|
|
2
|
+
import { BaseClientTransformer } from "../transformers.js";
|
|
3
|
+
export class StaticClient extends BaseClient {
|
|
4
|
+
transformer;
|
|
5
|
+
constructor(options, transport, transportOptions) {
|
|
6
|
+
super(options, transport, transportOptions);
|
|
7
|
+
this.transformer = new BaseClientTransformer();
|
|
8
|
+
}
|
|
9
|
+
get call() {
|
|
10
|
+
return this.createProxy(Object.create(null), false);
|
|
11
|
+
}
|
|
12
|
+
get stream() {
|
|
13
|
+
return this.createProxy(Object.create(null), true);
|
|
14
|
+
}
|
|
15
|
+
createProxy(target, isStream, path = []) {
|
|
16
|
+
return new Proxy(target, {
|
|
17
|
+
get: (obj, prop) => {
|
|
18
|
+
if (prop === 'then')
|
|
19
|
+
return obj;
|
|
20
|
+
const newPath = [...path, String(prop)];
|
|
21
|
+
const caller = (payload, options) => this._call(newPath.join('/'), payload, options);
|
|
22
|
+
return this.createProxy(caller, isStream, newPath);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { anyAbortSignal, createFuture, MAX_UINT32, noopFn } from '@nmtjs/common';
|
|
2
|
+
import { ClientMessageType, ConnectionType, ErrorCode, ProtocolBlob, ServerMessageType, } from '@nmtjs/protocol';
|
|
3
|
+
import { ProtocolError, ProtocolServerBlobStream, ProtocolServerRPCStream, ProtocolServerStream, versions, } from '@nmtjs/protocol/client';
|
|
4
|
+
import { EventEmitter } from "./events.js";
|
|
5
|
+
import { ClientStreams, ServerStreams } from "./streams.js";
|
|
6
|
+
export { ErrorCode, ProtocolBlob, } from '@nmtjs/protocol';
|
|
7
|
+
export * from "./types.js";
|
|
8
|
+
export class ClientError extends ProtocolError {
|
|
9
|
+
}
|
|
10
|
+
const DEFAULT_RECONNECT_TIMEOUT = 1000;
|
|
11
|
+
const DEFAULT_MAX_RECONNECT_TIMEOUT = 60000;
|
|
12
|
+
/**
|
|
13
|
+
* @todo Add error logging in ClientStreamPull rejection handler for easier debugging
|
|
14
|
+
* @todo Consider edge case where callId/streamId overflow at MAX_UINT32 with existing entries
|
|
15
|
+
*/
|
|
16
|
+
export class BaseClient extends EventEmitter {
|
|
17
|
+
options;
|
|
18
|
+
transportFactory;
|
|
19
|
+
transportOptions;
|
|
20
|
+
_;
|
|
21
|
+
calls = new Map();
|
|
22
|
+
transport;
|
|
23
|
+
protocol;
|
|
24
|
+
messageContext;
|
|
25
|
+
clientStreams = new ClientStreams();
|
|
26
|
+
serverStreams = new ServerStreams();
|
|
27
|
+
rpcStreams = new ServerStreams();
|
|
28
|
+
callId = 0;
|
|
29
|
+
streamId = 0;
|
|
30
|
+
cab = null;
|
|
31
|
+
reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT;
|
|
32
|
+
#auth;
|
|
33
|
+
constructor(options, transportFactory, transportOptions) {
|
|
34
|
+
super();
|
|
35
|
+
this.options = options;
|
|
36
|
+
this.transportFactory = transportFactory;
|
|
37
|
+
this.transportOptions = transportOptions;
|
|
38
|
+
this.protocol = versions[options.protocol];
|
|
39
|
+
const { format, protocol } = this.options;
|
|
40
|
+
this.transport = this.transportFactory({ protocol, format }, this.transportOptions);
|
|
41
|
+
if (this.transport.type === ConnectionType.Bidirectional &&
|
|
42
|
+
this.options.autoreconnect) {
|
|
43
|
+
this.on('disconnected', async (reason) => {
|
|
44
|
+
if (reason === 'server') {
|
|
45
|
+
this.connect();
|
|
46
|
+
}
|
|
47
|
+
else if (reason === 'error') {
|
|
48
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, this.reconnectTimeout));
|
|
49
|
+
const connected = new Promise((_, reject) => this.once('connected', reject));
|
|
50
|
+
this.reconnectTimeout = Math.min(this.reconnectTimeout * 2, DEFAULT_MAX_RECONNECT_TIMEOUT);
|
|
51
|
+
await Promise.race([timeout, connected]).then(this.connect.bind(this), noopFn);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
this.on('connected', () => {
|
|
55
|
+
this.reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT;
|
|
56
|
+
});
|
|
57
|
+
if (globalThis.window) {
|
|
58
|
+
globalThis.window.addEventListener('pageshow', () => {
|
|
59
|
+
if (!this.cab)
|
|
60
|
+
this.connect();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
get auth() {
|
|
66
|
+
return this.#auth;
|
|
67
|
+
}
|
|
68
|
+
set auth(value) {
|
|
69
|
+
this.#auth = value;
|
|
70
|
+
}
|
|
71
|
+
async connect() {
|
|
72
|
+
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
73
|
+
this.cab = new AbortController();
|
|
74
|
+
const protocol = this.protocol;
|
|
75
|
+
const serverStreams = this.serverStreams;
|
|
76
|
+
const transport = {
|
|
77
|
+
send: (buffer) => {
|
|
78
|
+
this.#send(buffer).catch(noopFn);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
this.messageContext = {
|
|
82
|
+
transport,
|
|
83
|
+
encoder: this.options.format,
|
|
84
|
+
decoder: this.options.format,
|
|
85
|
+
addClientStream: (blob) => {
|
|
86
|
+
const streamId = this.#getStreamId();
|
|
87
|
+
return this.clientStreams.add(blob.source, streamId, blob.metadata);
|
|
88
|
+
},
|
|
89
|
+
addServerStream(streamId, metadata) {
|
|
90
|
+
const stream = new ProtocolServerBlobStream(metadata, {
|
|
91
|
+
pull: (controller) => {
|
|
92
|
+
transport.send(protocol.encodeMessage(this, ClientMessageType.ServerStreamPull, { streamId, size: 65535 /* 64kb by default */ }));
|
|
93
|
+
},
|
|
94
|
+
close: () => {
|
|
95
|
+
serverStreams.remove(streamId);
|
|
96
|
+
},
|
|
97
|
+
readableStrategy: { highWaterMark: 0 },
|
|
98
|
+
});
|
|
99
|
+
serverStreams.add(streamId, stream);
|
|
100
|
+
return ({ signal } = {}) => {
|
|
101
|
+
if (signal)
|
|
102
|
+
signal.addEventListener('abort', () => {
|
|
103
|
+
transport.send(protocol.encodeMessage(this, ClientMessageType.ServerStreamAbort, { streamId }));
|
|
104
|
+
serverStreams.abort(streamId);
|
|
105
|
+
}, { once: true });
|
|
106
|
+
return stream;
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
streamId: this.#getStreamId.bind(this),
|
|
110
|
+
};
|
|
111
|
+
return this.transport.connect({
|
|
112
|
+
auth: this.auth,
|
|
113
|
+
application: this.options.application,
|
|
114
|
+
onMessage: this.onMessage.bind(this),
|
|
115
|
+
onConnect: this.onConnect.bind(this),
|
|
116
|
+
onDisconnect: this.onDisconnect.bind(this),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async disconnect() {
|
|
121
|
+
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
122
|
+
this.cab.abort();
|
|
123
|
+
await this.transport.disconnect();
|
|
124
|
+
this.messageContext = null;
|
|
125
|
+
this.cab = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
blob(source, metadata) {
|
|
129
|
+
return ProtocolBlob.from(source, metadata);
|
|
130
|
+
}
|
|
131
|
+
async _call(procedure, payload, options = {}) {
|
|
132
|
+
const timeout = options.timeout ?? this.options.timeout;
|
|
133
|
+
const controller = new AbortController();
|
|
134
|
+
// attach all abort signals
|
|
135
|
+
const signals = [controller.signal];
|
|
136
|
+
if (timeout)
|
|
137
|
+
signals.push(AbortSignal.timeout(timeout));
|
|
138
|
+
if (options.signal)
|
|
139
|
+
signals.push(options.signal);
|
|
140
|
+
if (this.cab?.signal)
|
|
141
|
+
signals.push(this.cab.signal);
|
|
142
|
+
const signal = signals.length ? anyAbortSignal(...signals) : undefined;
|
|
143
|
+
const callId = this.#getCallId();
|
|
144
|
+
const call = createFuture();
|
|
145
|
+
call.procedure = procedure;
|
|
146
|
+
call.signal = signal;
|
|
147
|
+
this.calls.set(callId, call);
|
|
148
|
+
// Check if signal is already aborted before proceeding
|
|
149
|
+
if (signal?.aborted) {
|
|
150
|
+
this.calls.delete(callId);
|
|
151
|
+
const error = new ProtocolError(ErrorCode.ClientRequestError, signal.reason);
|
|
152
|
+
call.reject(error);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
if (signal) {
|
|
156
|
+
signal.addEventListener('abort', () => {
|
|
157
|
+
call.reject(new ProtocolError(ErrorCode.ClientRequestError, signal.reason));
|
|
158
|
+
if (this.transport.type === ConnectionType.Bidirectional &&
|
|
159
|
+
this.messageContext) {
|
|
160
|
+
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.RpcAbort, { callId });
|
|
161
|
+
this.#send(buffer).catch(noopFn);
|
|
162
|
+
}
|
|
163
|
+
}, { once: true });
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const transformedPayload = this.transformer.encode(procedure, payload);
|
|
167
|
+
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
168
|
+
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.Rpc, { callId, procedure, payload: transformedPayload });
|
|
169
|
+
await this.#send(buffer, signal);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const response = await this.transport.call({
|
|
173
|
+
application: this.options.application,
|
|
174
|
+
format: this.options.format,
|
|
175
|
+
auth: this.auth,
|
|
176
|
+
}, { callId, procedure, payload: transformedPayload }, { signal, _stream_response: options._stream_response });
|
|
177
|
+
this.#handleCallResponse(callId, response);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
call.reject(error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const result = call.promise.then((value) => {
|
|
185
|
+
if (value instanceof ProtocolServerRPCStream) {
|
|
186
|
+
return value.createAsyncIterable(() => {
|
|
187
|
+
controller.abort();
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
controller.abort();
|
|
191
|
+
return value;
|
|
192
|
+
}, (err) => {
|
|
193
|
+
controller.abort();
|
|
194
|
+
throw err;
|
|
195
|
+
});
|
|
196
|
+
if (this.options.safe) {
|
|
197
|
+
return await result
|
|
198
|
+
.then((result) => ({ result }))
|
|
199
|
+
.catch((error) => ({ error }))
|
|
200
|
+
.finally(() => {
|
|
201
|
+
this.calls.delete(callId);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
return await result.finally(() => {
|
|
206
|
+
this.calls.delete(callId);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async onConnect() {
|
|
211
|
+
this.emit('connected');
|
|
212
|
+
}
|
|
213
|
+
async onDisconnect(reason) {
|
|
214
|
+
this.emit('disconnected', reason);
|
|
215
|
+
this.clientStreams.clear(reason);
|
|
216
|
+
this.serverStreams.clear(reason);
|
|
217
|
+
this.rpcStreams.clear(reason);
|
|
218
|
+
}
|
|
219
|
+
async onMessage(buffer) {
|
|
220
|
+
if (!this.messageContext)
|
|
221
|
+
return;
|
|
222
|
+
const message = this.protocol.decodeMessage(this.messageContext, buffer);
|
|
223
|
+
switch (message.type) {
|
|
224
|
+
case ServerMessageType.RpcResponse:
|
|
225
|
+
this.#handleRPCResponseMessage(message);
|
|
226
|
+
break;
|
|
227
|
+
case ServerMessageType.RpcStreamResponse:
|
|
228
|
+
this.#handleRPCStreamResponseMessage(message);
|
|
229
|
+
break;
|
|
230
|
+
case ServerMessageType.RpcStreamChunk:
|
|
231
|
+
this.rpcStreams.push(message.callId, message.chunk);
|
|
232
|
+
break;
|
|
233
|
+
case ServerMessageType.RpcStreamEnd:
|
|
234
|
+
this.rpcStreams.end(message.callId);
|
|
235
|
+
this.calls.delete(message.callId);
|
|
236
|
+
break;
|
|
237
|
+
case ServerMessageType.RpcStreamAbort:
|
|
238
|
+
this.rpcStreams.abort(message.callId);
|
|
239
|
+
this.calls.delete(message.callId);
|
|
240
|
+
break;
|
|
241
|
+
case ServerMessageType.ServerStreamPush:
|
|
242
|
+
this.serverStreams.push(message.streamId, message.chunk);
|
|
243
|
+
break;
|
|
244
|
+
case ServerMessageType.ServerStreamEnd:
|
|
245
|
+
this.serverStreams.end(message.streamId);
|
|
246
|
+
break;
|
|
247
|
+
case ServerMessageType.ServerStreamAbort:
|
|
248
|
+
this.serverStreams.abort(message.streamId);
|
|
249
|
+
break;
|
|
250
|
+
case ServerMessageType.ClientStreamPull:
|
|
251
|
+
this.clientStreams.pull(message.streamId, message.size).then((chunk) => {
|
|
252
|
+
if (chunk) {
|
|
253
|
+
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.ClientStreamPush, { streamId: message.streamId, chunk });
|
|
254
|
+
this.#send(buffer).catch(noopFn);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.ClientStreamEnd, { streamId: message.streamId });
|
|
258
|
+
this.#send(buffer).catch(noopFn);
|
|
259
|
+
this.clientStreams.end(message.streamId);
|
|
260
|
+
}
|
|
261
|
+
}, () => {
|
|
262
|
+
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.ClientStreamAbort, { streamId: message.streamId });
|
|
263
|
+
this.#send(buffer).catch(noopFn);
|
|
264
|
+
this.clientStreams.remove(message.streamId);
|
|
265
|
+
});
|
|
266
|
+
break;
|
|
267
|
+
case ServerMessageType.ClientStreamAbort:
|
|
268
|
+
this.clientStreams.abort(message.streamId);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
#handleRPCResponseMessage(message) {
|
|
273
|
+
const { callId, result, error } = message;
|
|
274
|
+
const call = this.calls.get(callId);
|
|
275
|
+
if (!call)
|
|
276
|
+
return;
|
|
277
|
+
if (error) {
|
|
278
|
+
call.reject(new ProtocolError(error.code, error.message, error.data));
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
try {
|
|
282
|
+
const transformed = this.transformer.decode(call.procedure, result);
|
|
283
|
+
call.resolve(transformed);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
call.reject(new ProtocolError(ErrorCode.ClientRequestError, 'Unable to decode response', error));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
#handleRPCStreamResponseMessage(message) {
|
|
291
|
+
const call = this.calls.get(message.callId);
|
|
292
|
+
if (message.error) {
|
|
293
|
+
if (!call)
|
|
294
|
+
return;
|
|
295
|
+
call.reject(new ProtocolError(message.error.code, message.error.message, message.error.data));
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
if (call) {
|
|
299
|
+
const { procedure, signal } = call;
|
|
300
|
+
const stream = new ProtocolServerRPCStream({
|
|
301
|
+
start: (controller) => {
|
|
302
|
+
if (signal) {
|
|
303
|
+
if (signal.aborted)
|
|
304
|
+
controller.error(signal.reason);
|
|
305
|
+
else
|
|
306
|
+
signal.addEventListener('abort', () => {
|
|
307
|
+
controller.error(signal.reason);
|
|
308
|
+
if (this.rpcStreams.has(message.callId)) {
|
|
309
|
+
this.rpcStreams.remove(message.callId);
|
|
310
|
+
this.calls.delete(message.callId);
|
|
311
|
+
if (this.messageContext) {
|
|
312
|
+
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.RpcAbort, { callId: message.callId, reason: signal.reason });
|
|
313
|
+
this.#send(buffer).catch(noopFn);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}, { once: true });
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
transform: (chunk) => {
|
|
320
|
+
return this.transformer.decode(procedure, this.options.format.decode(chunk));
|
|
321
|
+
},
|
|
322
|
+
pull: () => {
|
|
323
|
+
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.RpcPull, { callId: message.callId });
|
|
324
|
+
this.#send(buffer).catch(noopFn);
|
|
325
|
+
},
|
|
326
|
+
readableStrategy: { highWaterMark: 0 },
|
|
327
|
+
});
|
|
328
|
+
this.rpcStreams.add(message.callId, stream);
|
|
329
|
+
call.resolve(stream);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
// Call not found, but stream response received
|
|
333
|
+
// This can happen if the call was aborted or timed out
|
|
334
|
+
// Need to send an abort for the stream to avoid resource leaks from server side
|
|
335
|
+
if (this.messageContext) {
|
|
336
|
+
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.RpcAbort, { callId: message.callId });
|
|
337
|
+
this.#send(buffer).catch(noopFn);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
#handleCallResponse(callId, response) {
|
|
343
|
+
const call = this.calls.get(callId);
|
|
344
|
+
if (response.type === 'rpc_stream') {
|
|
345
|
+
if (call) {
|
|
346
|
+
const stream = new ProtocolServerStream({
|
|
347
|
+
transform: (chunk) => {
|
|
348
|
+
return this.transformer.decode(call.procedure, this.options.format.decode(chunk));
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
this.rpcStreams.add(callId, stream);
|
|
352
|
+
call.resolve(({ signal }) => {
|
|
353
|
+
response.stream.pipeTo(stream.writable, { signal }).catch(noopFn);
|
|
354
|
+
return stream;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
// Call not found, but stream response received
|
|
359
|
+
// This can happen if the call was aborted or timed out
|
|
360
|
+
// Need to cancel the stream to avoid resource leaks from server side
|
|
361
|
+
response.stream.cancel().catch(noopFn);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
else if (response.type === 'blob') {
|
|
365
|
+
if (call) {
|
|
366
|
+
const { metadata, source } = response;
|
|
367
|
+
const stream = new ProtocolServerBlobStream(metadata);
|
|
368
|
+
this.serverStreams.add(this.#getStreamId(), stream);
|
|
369
|
+
call.resolve(({ signal }) => {
|
|
370
|
+
source.pipeTo(stream.writable, { signal }).catch(noopFn);
|
|
371
|
+
return stream;
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
// Call not found, but blob response received
|
|
376
|
+
// This can happen if the call was aborted or timed out
|
|
377
|
+
// Need to cancel the stream to avoid resource leaks from server side
|
|
378
|
+
response.source.cancel().catch(noopFn);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else if (response.type === 'rpc') {
|
|
382
|
+
if (!call)
|
|
383
|
+
return;
|
|
384
|
+
try {
|
|
385
|
+
const transformed = this.transformer.decode(call.procedure, response.result);
|
|
386
|
+
call.resolve(transformed);
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
call.reject(new ProtocolError(ErrorCode.ClientRequestError, 'Unable to decode response', error));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
#send(buffer, signal) {
|
|
394
|
+
if (this.transport.type === ConnectionType.Unidirectional)
|
|
395
|
+
throw new Error('Invalid transport type for send');
|
|
396
|
+
return this.transport.send(buffer, { signal });
|
|
397
|
+
}
|
|
398
|
+
#getStreamId() {
|
|
399
|
+
if (this.streamId >= MAX_UINT32) {
|
|
400
|
+
this.streamId = 0;
|
|
401
|
+
}
|
|
402
|
+
return this.streamId++;
|
|
403
|
+
}
|
|
404
|
+
#getCallId() {
|
|
405
|
+
if (this.callId >= MAX_UINT32) {
|
|
406
|
+
this.callId = 0;
|
|
407
|
+
}
|
|
408
|
+
return this.callId++;
|
|
409
|
+
}
|
|
410
|
+
}
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// TODO: add errors and promise rejections handling
|
|
2
|
+
/**
|
|
3
|
+
* Thin node-like event emitter wrapper around EventTarget
|
|
4
|
+
*/
|
|
5
|
+
export class EventEmitter {
|
|
6
|
+
static once(ee, event) {
|
|
7
|
+
return new Promise((resolve) => ee.once(event, resolve));
|
|
8
|
+
}
|
|
9
|
+
#target = new EventTarget();
|
|
10
|
+
#listeners = new Map();
|
|
11
|
+
on(event, listener, options) {
|
|
12
|
+
const wrapper = (event) => listener(...event.detail);
|
|
13
|
+
this.#listeners.set(listener, wrapper);
|
|
14
|
+
this.#target.addEventListener(event, wrapper, { ...options, once: false });
|
|
15
|
+
return () => this.#target.removeEventListener(event, wrapper);
|
|
16
|
+
}
|
|
17
|
+
once(event, listener, options) {
|
|
18
|
+
return this.on(event, listener, { ...options, once: true });
|
|
19
|
+
}
|
|
20
|
+
off(event, listener) {
|
|
21
|
+
const wrapper = this.#listeners.get(listener);
|
|
22
|
+
if (wrapper)
|
|
23
|
+
this.#target.removeEventListener(event, wrapper);
|
|
24
|
+
}
|
|
25
|
+
emit(event, ...args) {
|
|
26
|
+
return this.#target.dispatchEvent(new CustomEvent(event, { detail: args }));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export const once = (ee, event, signal) => {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
ee.once(event, resolve, { signal });
|
|
32
|
+
});
|
|
33
|
+
};
|
package/dist/index.js
ADDED
package/dist/streams.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ProtocolClientBlobStream } from '@nmtjs/protocol/client';
|
|
2
|
+
export class ClientStreams {
|
|
3
|
+
#collection = new Map();
|
|
4
|
+
get size() {
|
|
5
|
+
return this.#collection.size;
|
|
6
|
+
}
|
|
7
|
+
get(streamId) {
|
|
8
|
+
const stream = this.#collection.get(streamId);
|
|
9
|
+
if (!stream)
|
|
10
|
+
throw new Error('Stream not found');
|
|
11
|
+
return stream;
|
|
12
|
+
}
|
|
13
|
+
add(source, streamId, metadata) {
|
|
14
|
+
const stream = new ProtocolClientBlobStream(source, streamId, metadata);
|
|
15
|
+
this.#collection.set(streamId, stream);
|
|
16
|
+
return stream;
|
|
17
|
+
}
|
|
18
|
+
remove(streamId) {
|
|
19
|
+
this.#collection.delete(streamId);
|
|
20
|
+
}
|
|
21
|
+
async abort(streamId, reason) {
|
|
22
|
+
const stream = this.#collection.get(streamId);
|
|
23
|
+
if (!stream)
|
|
24
|
+
return; // Stream already cleaned up
|
|
25
|
+
await stream.abort(reason);
|
|
26
|
+
this.remove(streamId);
|
|
27
|
+
}
|
|
28
|
+
pull(streamId, size) {
|
|
29
|
+
const stream = this.get(streamId);
|
|
30
|
+
return stream.read(size);
|
|
31
|
+
}
|
|
32
|
+
async end(streamId) {
|
|
33
|
+
await this.get(streamId).end();
|
|
34
|
+
this.remove(streamId);
|
|
35
|
+
}
|
|
36
|
+
async clear(reason) {
|
|
37
|
+
if (reason) {
|
|
38
|
+
const abortPromises = [...this.#collection.values()].map((stream) => stream.abort(reason));
|
|
39
|
+
await Promise.all(abortPromises);
|
|
40
|
+
}
|
|
41
|
+
this.#collection.clear();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export class ServerStreams {
|
|
45
|
+
#collection = new Map();
|
|
46
|
+
#writers = new Map();
|
|
47
|
+
get size() {
|
|
48
|
+
return this.#collection.size;
|
|
49
|
+
}
|
|
50
|
+
has(streamId) {
|
|
51
|
+
return this.#collection.has(streamId);
|
|
52
|
+
}
|
|
53
|
+
get(streamId) {
|
|
54
|
+
const stream = this.#collection.get(streamId);
|
|
55
|
+
if (!stream)
|
|
56
|
+
throw new Error('Stream not found');
|
|
57
|
+
return stream;
|
|
58
|
+
}
|
|
59
|
+
add(streamId, stream) {
|
|
60
|
+
this.#collection.set(streamId, stream);
|
|
61
|
+
this.#writers.set(streamId, stream.writable.getWriter());
|
|
62
|
+
return stream;
|
|
63
|
+
}
|
|
64
|
+
remove(streamId) {
|
|
65
|
+
this.#collection.delete(streamId);
|
|
66
|
+
this.#writers.delete(streamId);
|
|
67
|
+
}
|
|
68
|
+
async abort(streamId) {
|
|
69
|
+
if (this.has(streamId)) {
|
|
70
|
+
const writer = this.#writers.get(streamId);
|
|
71
|
+
if (writer) {
|
|
72
|
+
await writer.abort();
|
|
73
|
+
writer.releaseLock();
|
|
74
|
+
}
|
|
75
|
+
this.remove(streamId);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async push(streamId, chunk) {
|
|
79
|
+
const writer = this.#writers.get(streamId);
|
|
80
|
+
if (writer) {
|
|
81
|
+
return await writer.write(chunk);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async end(streamId) {
|
|
85
|
+
const writer = this.#writers.get(streamId);
|
|
86
|
+
if (writer) {
|
|
87
|
+
await writer.close();
|
|
88
|
+
writer.releaseLock();
|
|
89
|
+
}
|
|
90
|
+
this.remove(streamId);
|
|
91
|
+
}
|
|
92
|
+
async clear(reason) {
|
|
93
|
+
if (reason) {
|
|
94
|
+
const abortPromises = [...this.#writers.values()].map((writer) => writer.abort(reason).finally(() => writer.releaseLock()));
|
|
95
|
+
await Promise.allSettled(abortPromises);
|
|
96
|
+
}
|
|
97
|
+
this.#collection.clear();
|
|
98
|
+
this.#writers.clear();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const ResolvedType = Symbol('ResolvedType');
|
package/package.json
CHANGED
|
@@ -2,42 +2,33 @@
|
|
|
2
2
|
"name": "@nmtjs/client",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"exports": {
|
|
5
|
-
".":
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"module-sync": "./dist/common.js"
|
|
9
|
-
},
|
|
10
|
-
"./runtime": {
|
|
11
|
-
"types": "./dist/runtime.d.ts",
|
|
12
|
-
"import": "./dist/runtime.js",
|
|
13
|
-
"module-sync": "./dist/runtime.js"
|
|
14
|
-
},
|
|
15
|
-
"./static": {
|
|
16
|
-
"types": "./dist/static.d.ts",
|
|
17
|
-
"import": "./dist/static.js",
|
|
18
|
-
"module-sync": "./dist/static.js"
|
|
19
|
-
}
|
|
5
|
+
".": "./dist/index.js",
|
|
6
|
+
"./runtime": "./dist/clients/runtime.js",
|
|
7
|
+
"./static": "./dist/clients/static.js"
|
|
20
8
|
},
|
|
21
9
|
"peerDependencies": {
|
|
22
|
-
"@nmtjs/type": "0.
|
|
23
|
-
"@nmtjs/
|
|
24
|
-
"@nmtjs/protocol": "0.
|
|
25
|
-
"@nmtjs/
|
|
10
|
+
"@nmtjs/type": "0.15.0-beta.2",
|
|
11
|
+
"@nmtjs/contract": "0.15.0-beta.2",
|
|
12
|
+
"@nmtjs/protocol": "0.15.0-beta.2",
|
|
13
|
+
"@nmtjs/common": "0.15.0-beta.2"
|
|
26
14
|
},
|
|
27
15
|
"devDependencies": {
|
|
28
|
-
"@nmtjs/
|
|
29
|
-
"@nmtjs/contract": "0.
|
|
30
|
-
"@nmtjs/common": "0.
|
|
31
|
-
"@nmtjs/
|
|
16
|
+
"@nmtjs/_tests": "0.15.0-beta.2",
|
|
17
|
+
"@nmtjs/contract": "0.15.0-beta.2",
|
|
18
|
+
"@nmtjs/common": "0.15.0-beta.2",
|
|
19
|
+
"@nmtjs/type": "0.15.0-beta.2",
|
|
20
|
+
"@nmtjs/protocol": "0.15.0-beta.2"
|
|
32
21
|
},
|
|
33
22
|
"files": [
|
|
34
23
|
"dist",
|
|
35
24
|
"LICENSE.md",
|
|
36
25
|
"README.md"
|
|
37
26
|
],
|
|
38
|
-
"version": "0.
|
|
27
|
+
"version": "0.15.0-beta.2",
|
|
39
28
|
"scripts": {
|
|
29
|
+
"clean-build": "rm -rf ./dist",
|
|
40
30
|
"build": "tsc",
|
|
31
|
+
"dev": "tsc --watch",
|
|
41
32
|
"type-check": "tsc --noEmit"
|
|
42
33
|
}
|
|
43
34
|
}
|
package/dist/common.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { TypeProvider } from '@nmtjs/common';
|
|
2
|
-
import type { TAnyAPIContract } from '@nmtjs/contract';
|
|
3
|
-
import type { ProtocolBaseClientCallOptions, ProtocolBaseTransformer, ProtocolTransport } from '@nmtjs/protocol/client';
|
|
4
|
-
import { ProtocolError } from '@nmtjs/protocol/client';
|
|
5
|
-
import type { ClientCallers, ResolveAPIRouterRoutes } from './types.ts';
|
|
6
|
-
export { ErrorCode, ProtocolBlob, type ProtocolBlobMetadata, TransportType, } from '@nmtjs/protocol';
|
|
7
|
-
export * from './types.ts';
|
|
8
|
-
export declare class ClientError extends ProtocolError {
|
|
9
|
-
}
|
|
10
|
-
export interface BaseClientOptions<SafeCall extends boolean = false> {
|
|
11
|
-
timeout: number;
|
|
12
|
-
autoreconnect?: boolean;
|
|
13
|
-
safe?: SafeCall;
|
|
14
|
-
}
|
|
15
|
-
export declare abstract class BaseClient<APIContract extends TAnyAPIContract = TAnyAPIContract, SafeCall extends boolean = false, InputTypeProvider extends TypeProvider = TypeProvider, OutputTypeProvider extends TypeProvider = TypeProvider, Routes extends {
|
|
16
|
-
contract: APIContract['router'];
|
|
17
|
-
routes: ResolveAPIRouterRoutes<APIContract['router'], InputTypeProvider, OutputTypeProvider>;
|
|
18
|
-
} = {
|
|
19
|
-
contract: APIContract['router'];
|
|
20
|
-
routes: ResolveAPIRouterRoutes<APIContract['router'], InputTypeProvider, OutputTypeProvider>;
|
|
21
|
-
}> {
|
|
22
|
-
readonly transport: ProtocolTransport;
|
|
23
|
-
readonly options: BaseClientOptions<SafeCall>;
|
|
24
|
-
_: {
|
|
25
|
-
api: Routes;
|
|
26
|
-
safe: SafeCall;
|
|
27
|
-
};
|
|
28
|
-
protected abstract transformer: ProtocolBaseTransformer;
|
|
29
|
-
protected callers: ClientCallers<Routes, SafeCall>;
|
|
30
|
-
auth: any;
|
|
31
|
-
protected reconnectTimeout: number;
|
|
32
|
-
constructor(transport: ProtocolTransport, options: BaseClientOptions<SafeCall>);
|
|
33
|
-
protected _call(procedure: string, payload: any, options: ProtocolBaseClientCallOptions): Promise<any>;
|
|
34
|
-
get call(): ClientCallers<Routes, SafeCall>;
|
|
35
|
-
setAuth(auth: any): void;
|
|
36
|
-
connect(): Promise<void>;
|
|
37
|
-
disconnect(): Promise<void>;
|
|
38
|
-
}
|
package/dist/common.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { noopFn } from '@nmtjs/common';
|
|
2
|
-
import { ProtocolError } from '@nmtjs/protocol/client';
|
|
3
|
-
export { ErrorCode, ProtocolBlob, TransportType, } from '@nmtjs/protocol';
|
|
4
|
-
export * from "./types.js";
|
|
5
|
-
export class ClientError extends ProtocolError {
|
|
6
|
-
}
|
|
7
|
-
const DEFAULT_RECONNECT_TIMEOUT = 1000;
|
|
8
|
-
export class BaseClient {
|
|
9
|
-
transport;
|
|
10
|
-
options;
|
|
11
|
-
_;
|
|
12
|
-
callers;
|
|
13
|
-
auth;
|
|
14
|
-
reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT;
|
|
15
|
-
constructor(transport, options) {
|
|
16
|
-
this.transport = transport;
|
|
17
|
-
this.options = options;
|
|
18
|
-
if (this.options.autoreconnect) {
|
|
19
|
-
this.transport.on('disconnected', async (reason) => {
|
|
20
|
-
if (reason === 'server') {
|
|
21
|
-
this.connect();
|
|
22
|
-
}
|
|
23
|
-
else if (reason === 'error') {
|
|
24
|
-
const timeout = new Promise((resolve) => setTimeout(resolve, this.reconnectTimeout));
|
|
25
|
-
const connected = new Promise((_, reject) => this.transport.once('connected', reject));
|
|
26
|
-
this.reconnectTimeout += DEFAULT_RECONNECT_TIMEOUT;
|
|
27
|
-
await Promise.race([timeout, connected]).then(this.connect.bind(this), noopFn);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
this.transport.on('connected', () => {
|
|
31
|
-
this.reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT;
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
async _call(procedure, payload, options) {
|
|
36
|
-
const call = await this.transport.call(procedure, payload, options, this.transformer);
|
|
37
|
-
if (this.options.safe) {
|
|
38
|
-
return await call.promise
|
|
39
|
-
.then((result) => ({ result }))
|
|
40
|
-
.catch((error) => ({ error }));
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
return await call.promise.catch((error) => {
|
|
44
|
-
throw error;
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
get call() {
|
|
49
|
-
return this.callers;
|
|
50
|
-
}
|
|
51
|
-
setAuth(auth) {
|
|
52
|
-
this.auth = auth;
|
|
53
|
-
}
|
|
54
|
-
connect() {
|
|
55
|
-
return this.transport.connect(this.auth, this.transformer);
|
|
56
|
-
}
|
|
57
|
-
disconnect() {
|
|
58
|
-
return this.transport.disconnect();
|
|
59
|
-
}
|
|
60
|
-
}
|
package/dist/runtime.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { TAnyAPIContract, TAnyProcedureContract, TAnyRouterContract } from '@nmtjs/contract';
|
|
2
|
-
import { ProtocolBaseTransformer } from '@nmtjs/protocol/client';
|
|
3
|
-
import type { RuntimeInputContractTypeProvider, RuntimeOutputContractTypeProvider } from './common.ts';
|
|
4
|
-
import { BaseClient } from './common.ts';
|
|
5
|
-
export declare class RuntimeContractTransformer extends ProtocolBaseTransformer {
|
|
6
|
-
protected procedures: Map<string, TAnyProcedureContract>;
|
|
7
|
-
constructor(procedures: Map<string, TAnyProcedureContract>);
|
|
8
|
-
decodeRPC(procedure: string, payload: any): any;
|
|
9
|
-
decodeRPCChunk(procedure: string, payload: any): unknown;
|
|
10
|
-
encodeRPC(procedure: string, payload: any): unknown;
|
|
11
|
-
protected getProcedureContract(procedure: string): TAnyProcedureContract;
|
|
12
|
-
protected build(router: TAnyRouterContract): void;
|
|
13
|
-
}
|
|
14
|
-
export declare class RuntimeClient<APIContract extends TAnyAPIContract, SafeCall extends boolean = false> extends BaseClient<APIContract, SafeCall, RuntimeInputContractTypeProvider, RuntimeOutputContractTypeProvider> {
|
|
15
|
-
contract: APIContract;
|
|
16
|
-
protected transformer: RuntimeContractTransformer;
|
|
17
|
-
protected procedures: Map<string, TAnyProcedureContract>;
|
|
18
|
-
constructor(contract: APIContract, ...args: ConstructorParameters<typeof BaseClient<APIContract, SafeCall>>);
|
|
19
|
-
protected resolveProcedures(router: TAnyRouterContract): void;
|
|
20
|
-
protected buildCallers(): any;
|
|
21
|
-
}
|
package/dist/runtime.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { IsProcedureContract, IsRouterContract } from '@nmtjs/contract';
|
|
2
|
-
import { ErrorCode } from '@nmtjs/protocol';
|
|
3
|
-
import { ProtocolBaseTransformer } from '@nmtjs/protocol/client';
|
|
4
|
-
import { NeemataTypeError, t } from '@nmtjs/type';
|
|
5
|
-
import { BaseClient, ClientError } from "./common.js";
|
|
6
|
-
export class RuntimeContractTransformer extends ProtocolBaseTransformer {
|
|
7
|
-
procedures;
|
|
8
|
-
constructor(procedures) {
|
|
9
|
-
super();
|
|
10
|
-
this.procedures = procedures;
|
|
11
|
-
}
|
|
12
|
-
decodeRPC(procedure, payload) {
|
|
13
|
-
const contract = this.getProcedureContract(procedure);
|
|
14
|
-
const type = contract.output;
|
|
15
|
-
if (type instanceof t.NeverType)
|
|
16
|
-
return undefined;
|
|
17
|
-
return payload;
|
|
18
|
-
}
|
|
19
|
-
decodeRPCChunk(procedure, payload) {
|
|
20
|
-
const contract = this.getProcedureContract(procedure);
|
|
21
|
-
const type = contract.stream;
|
|
22
|
-
if (!type || type instanceof t.NeverType)
|
|
23
|
-
return undefined;
|
|
24
|
-
return type.decode(payload);
|
|
25
|
-
}
|
|
26
|
-
encodeRPC(procedure, payload) {
|
|
27
|
-
const contract = this.getProcedureContract(procedure);
|
|
28
|
-
const type = contract.input;
|
|
29
|
-
if (type instanceof t.NeverType)
|
|
30
|
-
return undefined;
|
|
31
|
-
try {
|
|
32
|
-
return type.encode(payload);
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
35
|
-
if (error instanceof NeemataTypeError) {
|
|
36
|
-
throw new ClientError(ErrorCode.ValidationError, `Invalid payload for ${procedure}: ${error.message}`, error.issues);
|
|
37
|
-
}
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
getProcedureContract(procedure) {
|
|
42
|
-
const proc = this.procedures.get(procedure);
|
|
43
|
-
if (!proc) {
|
|
44
|
-
throw new ClientError(ErrorCode.NotFound, `Procedure contract not found for procedure: ${procedure}`);
|
|
45
|
-
}
|
|
46
|
-
return proc;
|
|
47
|
-
}
|
|
48
|
-
build(router) {
|
|
49
|
-
const routes = Object.values(router.routes);
|
|
50
|
-
for (const route of routes) {
|
|
51
|
-
if (IsRouterContract(route)) {
|
|
52
|
-
this.build(route);
|
|
53
|
-
}
|
|
54
|
-
else if (IsProcedureContract(route)) {
|
|
55
|
-
this.procedures.set(route.name, route);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
export class RuntimeClient extends BaseClient {
|
|
61
|
-
contract;
|
|
62
|
-
transformer;
|
|
63
|
-
procedures = new Map();
|
|
64
|
-
constructor(contract, ...args) {
|
|
65
|
-
super(...args);
|
|
66
|
-
this.contract = contract;
|
|
67
|
-
this.resolveProcedures(this.contract.router);
|
|
68
|
-
this.transformer = new RuntimeContractTransformer(this.procedures);
|
|
69
|
-
this.callers = this.buildCallers();
|
|
70
|
-
}
|
|
71
|
-
resolveProcedures(router) {
|
|
72
|
-
const routes = Object.values(router.routes);
|
|
73
|
-
for (const route of routes) {
|
|
74
|
-
if (IsRouterContract(route)) {
|
|
75
|
-
this.resolveProcedures(route);
|
|
76
|
-
}
|
|
77
|
-
else if (IsProcedureContract(route)) {
|
|
78
|
-
this.procedures.set(route.name, route);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
buildCallers() {
|
|
83
|
-
const callers = Object.create(null);
|
|
84
|
-
for (const [name, procedure] of this.procedures) {
|
|
85
|
-
const parts = name.split('/');
|
|
86
|
-
let current = callers;
|
|
87
|
-
for (let i = 0; i < parts.length; i++) {
|
|
88
|
-
const part = parts[i];
|
|
89
|
-
if (i === parts.length - 1) {
|
|
90
|
-
current[part] = (payload, options = {}) => this._call(name, payload, {
|
|
91
|
-
timeout: procedure.timeout || options.timeout || this.options.timeout,
|
|
92
|
-
...options,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
if (!current[part]) {
|
|
97
|
-
current[part] = {};
|
|
98
|
-
}
|
|
99
|
-
current = current[part];
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return callers;
|
|
104
|
-
}
|
|
105
|
-
}
|
package/dist/static.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { TAnyAPIContract } from '@nmtjs/contract';
|
|
2
|
-
import { ProtocolBaseTransformer } from '@nmtjs/protocol/client';
|
|
3
|
-
import type { StaticInputContractTypeProvider, StaticOutputContractTypeProvider } from './common.ts';
|
|
4
|
-
import { BaseClient } from './common.ts';
|
|
5
|
-
export declare class StaticClient<APIContract extends TAnyAPIContract, SafeCall extends boolean = false> extends BaseClient<APIContract, SafeCall, StaticInputContractTypeProvider, StaticOutputContractTypeProvider> {
|
|
6
|
-
protected transformer: ProtocolBaseTransformer;
|
|
7
|
-
constructor(...args: ConstructorParameters<typeof BaseClient<APIContract, SafeCall>>);
|
|
8
|
-
protected createProxy(target: any, path?: string[]): any;
|
|
9
|
-
}
|
package/dist/static.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { ProtocolBaseTransformer } from '@nmtjs/protocol/client';
|
|
2
|
-
import { BaseClient } from "./common.js";
|
|
3
|
-
export class StaticClient extends BaseClient {
|
|
4
|
-
transformer;
|
|
5
|
-
constructor(...args) {
|
|
6
|
-
super(...args);
|
|
7
|
-
this.transformer = new ProtocolBaseTransformer();
|
|
8
|
-
this.callers = this.createProxy(Object.create(null));
|
|
9
|
-
}
|
|
10
|
-
createProxy(target, path = []) {
|
|
11
|
-
return new Proxy(target, {
|
|
12
|
-
get: (obj, prop) => {
|
|
13
|
-
// `await client.call.something` or `await client.call.something.nested`
|
|
14
|
-
// without explicitly calling a function implicitly calls .then() on a target
|
|
15
|
-
// FIXME: this basically makes "then" a reserved word for static Client
|
|
16
|
-
if (prop === 'then')
|
|
17
|
-
return obj;
|
|
18
|
-
const newPath = [...path, String(prop)];
|
|
19
|
-
const caller = (payload, options) => this._call(newPath.join('/'), payload, {
|
|
20
|
-
...options,
|
|
21
|
-
timeout: options?.timeout ?? this.options.timeout,
|
|
22
|
-
});
|
|
23
|
-
return this.createProxy(caller, newPath);
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
}
|
package/dist/types.d.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import type { CallTypeProvider, OneOf, TypeProvider } from '@nmtjs/common';
|
|
2
|
-
import type { TAnyAPIContract, TAnyProcedureContract, TAnyRouterContract } from '@nmtjs/contract';
|
|
3
|
-
import type { ProtocolBlobInterface } from '@nmtjs/protocol';
|
|
4
|
-
import type { ProtocolBaseClientCallOptions, ProtocolError, ProtocolServerBlobStream, ProtocolServerStreamInterface } from '@nmtjs/protocol/client';
|
|
5
|
-
import type { BaseTypeAny, t } from '@nmtjs/type';
|
|
6
|
-
import type { PlainType } from '@nmtjs/type/_plain';
|
|
7
|
-
export type ClientOutputType<T> = T extends ProtocolBlobInterface ? ProtocolServerBlobStream : T extends {
|
|
8
|
-
[PlainType]?: true;
|
|
9
|
-
} ? {
|
|
10
|
-
[K in keyof Omit<T, PlainType>]: ClientOutputType<T[K]>;
|
|
11
|
-
} : T;
|
|
12
|
-
export interface StaticInputContractTypeProvider extends TypeProvider {
|
|
13
|
-
output: this['input'] extends BaseTypeAny ? t.infer.decode.input<this['input']> : never;
|
|
14
|
-
}
|
|
15
|
-
export interface RuntimeInputContractTypeProvider extends TypeProvider {
|
|
16
|
-
output: this['input'] extends BaseTypeAny ? t.infer.encode.input<this['input']> : never;
|
|
17
|
-
}
|
|
18
|
-
export interface StaticOutputContractTypeProvider extends TypeProvider {
|
|
19
|
-
output: this['input'] extends BaseTypeAny ? ClientOutputType<t.infer.encodeRaw.output<this['input']>> : never;
|
|
20
|
-
}
|
|
21
|
-
export interface RuntimeOutputContractTypeProvider extends TypeProvider {
|
|
22
|
-
output: this['input'] extends BaseTypeAny ? ClientOutputType<t.infer.decodeRaw.output<this['input']>> : never;
|
|
23
|
-
}
|
|
24
|
-
export type AnyResolvedAPIContractProcedure = {
|
|
25
|
-
contract: TAnyProcedureContract;
|
|
26
|
-
input: any;
|
|
27
|
-
output: any;
|
|
28
|
-
};
|
|
29
|
-
export type AnyResolvedAPIContractRouter = {
|
|
30
|
-
contract: TAnyRouterContract;
|
|
31
|
-
routes: Record<string, AnyResolvedAPIContractRouter | AnyResolvedAPIContractProcedure>;
|
|
32
|
-
};
|
|
33
|
-
export type AnyResolvedAPIContract = Record<string, Record<string, AnyResolvedAPIContractProcedure | AnyResolvedAPIContractRouter>>;
|
|
34
|
-
export type ResolveAPIRouterRoutes<T extends TAnyRouterContract, InputTypeProvider extends TypeProvider = TypeProvider, OutputTypeProvider extends TypeProvider = TypeProvider> = {
|
|
35
|
-
[K in keyof T['routes']]: T['routes'][K] extends TAnyRouterContract ? {
|
|
36
|
-
contract: T['routes'][K];
|
|
37
|
-
routes: ResolveAPIRouterRoutes<T['routes'][K], InputTypeProvider, OutputTypeProvider>;
|
|
38
|
-
} : T['routes'][K] extends TAnyProcedureContract ? {
|
|
39
|
-
contract: T['routes'][K];
|
|
40
|
-
input: CallTypeProvider<InputTypeProvider, T['routes'][K]['input']>;
|
|
41
|
-
output: T['routes'][K]['stream'] extends undefined | t.NeverType ? CallTypeProvider<OutputTypeProvider, T['routes'][K]['output']> : {
|
|
42
|
-
result: CallTypeProvider<OutputTypeProvider, T['routes'][K]['output']>;
|
|
43
|
-
stream: ProtocolServerStreamInterface<CallTypeProvider<OutputTypeProvider, T['routes'][K]['stream']>>;
|
|
44
|
-
};
|
|
45
|
-
} : never;
|
|
46
|
-
};
|
|
47
|
-
export type ResolveAPIContract<C extends TAnyAPIContract = TAnyAPIContract, InputTypeProvider extends TypeProvider = TypeProvider, OutputTypeProvider extends TypeProvider = TypeProvider> = ResolveAPIRouterRoutes<C['router'], InputTypeProvider, OutputTypeProvider>;
|
|
48
|
-
export type ClientCaller<Procedure extends AnyResolvedAPIContractProcedure, SafeCall extends boolean> = (...args: Procedure['input'] extends t.NeverType ? [data?: undefined, options?: Partial<ProtocolBaseClientCallOptions>] : undefined extends t.infer.encode.input<Procedure['contract']['input']> ? [
|
|
49
|
-
data?: Procedure['input'],
|
|
50
|
-
options?: Partial<ProtocolBaseClientCallOptions>
|
|
51
|
-
] : [
|
|
52
|
-
data: Procedure['input'],
|
|
53
|
-
options?: Partial<ProtocolBaseClientCallOptions>
|
|
54
|
-
]) => SafeCall extends true ? Promise<OneOf<[{
|
|
55
|
-
output: Procedure['output'];
|
|
56
|
-
}, {
|
|
57
|
-
error: ProtocolError;
|
|
58
|
-
}]>> : Promise<Procedure['output']>;
|
|
59
|
-
export type ClientCallers<Resolved extends AnyResolvedAPIContractRouter, SafeCall extends boolean> = {
|
|
60
|
-
[K in keyof Resolved['routes']]: Resolved['routes'][K] extends AnyResolvedAPIContractProcedure ? ClientCaller<Resolved['routes'][K], SafeCall> : Resolved['routes'][K] extends AnyResolvedAPIContractRouter ? ClientCallers<Resolved['routes'][K], SafeCall> : never;
|
|
61
|
-
};
|