better-grpc 0.2.5 → 0.3.0
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/README.md +96 -5
- package/dist/index.d.ts +67 -40
- package/dist/index.js +179 -76
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ It enables seamless, **bidirectional** communication between a client and a serv
|
|
|
22
22
|
- **No `.proto` files:** No need to write `.proto` files or use `protoc` to generate code.
|
|
23
23
|
- **Simple API:** The API is designed to be simple and intuitive.
|
|
24
24
|
- **Symmetric Experience:** Call client-side functions from the server with the same syntax as calling server-side functions from the client.
|
|
25
|
+
- **Multi-Client Support:** Target specific clients from the server using client IDs, enabling per-client communication patterns.
|
|
25
26
|
|
|
26
27
|
## Installation
|
|
27
28
|
|
|
@@ -146,7 +147,68 @@ for await (const [message] of server.MyService.chat) {
|
|
|
146
147
|
}
|
|
147
148
|
```
|
|
148
149
|
|
|
149
|
-
### 7.
|
|
150
|
+
### 7. Listen for bidi connections on the server
|
|
151
|
+
|
|
152
|
+
The server can use the `.listen()` API to handle incoming bidi stream connections. This is useful for setting up handlers that respond to each client connection:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
server.MyService.chat.listen(({ context, messages, send }) => {
|
|
156
|
+
console.log(`New client connected ${context.client.id}`);
|
|
157
|
+
|
|
158
|
+
(async () => {
|
|
159
|
+
for await (const [message] of messages) {
|
|
160
|
+
console.log('Received:', message);
|
|
161
|
+
await send(`Echo: ${message}`);
|
|
162
|
+
}
|
|
163
|
+
})();
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The `listen` handler receives:
|
|
168
|
+
- `context`: A promise that resolves to the connection context (including metadata if defined)
|
|
169
|
+
- `messages`: An async generator yielding incoming messages from the client
|
|
170
|
+
- `send`: A function to send messages back to the client
|
|
171
|
+
|
|
172
|
+
### 8. Target specific clients
|
|
173
|
+
|
|
174
|
+
When multiple clients are connected, the server can target a specific client using its client ID. Each client is automatically assigned a unique ID.
|
|
175
|
+
|
|
176
|
+
**Getting the client ID on the client side:**
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const client = await createGrpcClient('localhost:50051', myClientImpl);
|
|
180
|
+
|
|
181
|
+
// Access the client's unique ID
|
|
182
|
+
console.log(client.clientID); // e.g., 'abc123-def456-...'
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Getting the client ID on the server side:**
|
|
186
|
+
|
|
187
|
+
In server handlers, the client ID is available via `context.client.id`:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const GreeterServerImpl = GreeterService.Server({
|
|
191
|
+
greet: (name) => async (context) => {
|
|
192
|
+
console.log('Client ID:', context.client.id);
|
|
193
|
+
return `Hello, ${name}!`;
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Targeting a specific client from the server:**
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// Call a specific client by ID
|
|
202
|
+
const clientId = 'some-client-id';
|
|
203
|
+
await server.MyService(clientId).log('Message for specific client');
|
|
204
|
+
|
|
205
|
+
// For bidi streams, targeting a specific client
|
|
206
|
+
await server.MyService(clientId).chat('hello to specific client');
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
By default, server calls target the first connected client. When you need to communicate with a specific client (e.g., in a multi-client scenario), use the client ID selector.
|
|
210
|
+
|
|
211
|
+
### 9. Attach typed metadata
|
|
150
212
|
|
|
151
213
|
Define metadata requirements with [Zod](https://github.com/colinhacks/zod) schemas, and `better-grpc` will automatically type the context on both sides and marshal the payload into gRPC metadata.
|
|
152
214
|
|
|
@@ -165,11 +227,11 @@ abstract class GreeterService extends Service('GreeterService') {
|
|
|
165
227
|
}
|
|
166
228
|
```
|
|
167
229
|
|
|
168
|
-
Server implementations
|
|
230
|
+
Server implementations can optionally return a context-aware function. The outer function receives the request args, and the returned function receives the typed context:
|
|
169
231
|
|
|
170
232
|
```typescript
|
|
171
233
|
const GreeterServerImpl = GreeterService.Server({
|
|
172
|
-
async
|
|
234
|
+
greet: (name) => async (context) => {
|
|
173
235
|
console.log('Request', context.metadata.requestId);
|
|
174
236
|
return `Hello, ${name}!`;
|
|
175
237
|
},
|
|
@@ -215,7 +277,7 @@ A factory function that creates an abstract service class.
|
|
|
215
277
|
|
|
216
278
|
- `server<T>()`
|
|
217
279
|
|
|
218
|
-
Defines a server-side unary function signature. `T` should be a function type. Call the returned descriptor with `({ metadata: z.object({...}) })` to require typed metadata for that RPC. Client code then calls `client.MyService.fn(...args).withMeta({...})`, and server handlers receive the context
|
|
280
|
+
Defines a server-side unary function signature. `T` should be a function type. Call the returned descriptor with `({ metadata: z.object({...}) })` to require typed metadata for that RPC. Client code then calls `client.MyService.fn(...args).withMeta({...})`, and server handlers can return a function to receive the context (or just return a value if they don't need it).
|
|
219
281
|
|
|
220
282
|
- `client<T>()`
|
|
221
283
|
|
|
@@ -227,7 +289,7 @@ Defines a bidirectional stream signature. `T` should be a function type that ret
|
|
|
227
289
|
|
|
228
290
|
- `createGrpcServer(port: number, ...services: ServiceImpl[])`
|
|
229
291
|
|
|
230
|
-
Creates and starts a gRPC server.
|
|
292
|
+
Creates and starts a gRPC server. Returns service callables that can be invoked directly or with a client ID selector: `server.MyService.method()` or `server.MyService(clientId).method()`.
|
|
231
293
|
|
|
232
294
|
- `createGrpcClient(address: string, ...services: ServiceImpl[])`
|
|
233
295
|
|
|
@@ -237,6 +299,35 @@ Creates and starts a gRPC client using `DEFAULT_OPTIONS`.
|
|
|
237
299
|
|
|
238
300
|
Creates and starts a gRPC client with custom gRPC channel options. `DEFAULT_OPTIONS` is exported for easy overrides.
|
|
239
301
|
|
|
302
|
+
### Server-side bidi listen
|
|
303
|
+
|
|
304
|
+
For bidi streams, the server exposes a `.listen()` method to handle incoming connections:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
server.MyService.bidiFn.listen((connection) => {
|
|
308
|
+
// connection.context: Promise<Context> - resolves to typed context/metadata
|
|
309
|
+
// connection.messages: AsyncGenerator - incoming messages from client
|
|
310
|
+
// connection.send: Function - send messages to the client
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Deployment
|
|
315
|
+
|
|
316
|
+
If you deploy behind Traefik (including Dokploy), make sure the **entrypoint** timeouts allow long-lived HTTP/2 streams. Otherwise, bidi streams can be cancelled around the default timeout window.
|
|
317
|
+
|
|
318
|
+
This is a static Traefik setting (not the dynamic `http:` config). Add this to your Traefik config and reload:
|
|
319
|
+
|
|
320
|
+
```yaml
|
|
321
|
+
entryPoints:
|
|
322
|
+
websecure:
|
|
323
|
+
address: :443
|
|
324
|
+
transport:
|
|
325
|
+
respondingTimeouts:
|
|
326
|
+
readTimeout: 0s
|
|
327
|
+
writeTimeout: 0s
|
|
328
|
+
idleTimeout: 0s
|
|
329
|
+
```
|
|
330
|
+
|
|
240
331
|
## Benchmarks
|
|
241
332
|
|
|
242
333
|
### Simple "Hello World"
|
package/dist/index.d.ts
CHANGED
|
@@ -1,39 +1,26 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
1
|
+
import z$1, { z } from 'zod';
|
|
2
2
|
import { ChannelOptions } from 'nice-grpc';
|
|
3
3
|
|
|
4
4
|
type Context<Meta extends z.ZodObject<any> | undefined> = {
|
|
5
5
|
metadata: Meta extends z.ZodObject<any> ? z.infer<Meta> : undefined;
|
|
6
|
+
client: {
|
|
7
|
+
id: string;
|
|
8
|
+
};
|
|
6
9
|
};
|
|
7
|
-
type
|
|
8
|
-
type DefaultContext = Context<undefined>;
|
|
9
|
-
type HasMeta<C extends Context<any>> = C extends Context<infer M> ? (M extends undefined ? false : true) : false;
|
|
10
|
-
type ExtractMeta<C extends Context<any>> = C extends Context<infer M> ? M : undefined;
|
|
11
|
-
type ContextRequiredFn<fn extends (...args: any[]) => any, C extends Context<any>, RequireMeta extends boolean = HasMeta<C>> = RequireMeta extends true ? (...args: Parameters<fn>) => {
|
|
12
|
-
withMeta<M extends ExtractMeta<C>>(metadata: z.infer<M>): ContextRequiredFn<fn, C, false>;
|
|
13
|
-
} : ReturnType<fn>;
|
|
10
|
+
type AnyContext = Context<any>;
|
|
14
11
|
|
|
15
12
|
declare const ScopeTag: unique symbol;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
type
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
};
|
|
23
|
-
type serverSignature<fn extends AnyFn<any>, C extends Context<any>> = (C extends DefaultContext ? <Meta extends z.ZodObject<any> | undefined>(context: {
|
|
24
|
-
metadata?: Meta;
|
|
25
|
-
}) => serverSignature<fn, Context<Meta>> : fn) & {
|
|
26
|
-
[ScopeTag]: "server";
|
|
27
|
-
};
|
|
28
|
-
type clientSignature<fn extends AnyFn<any>> = fn & {
|
|
29
|
-
[ScopeTag]: "client";
|
|
30
|
-
};
|
|
31
|
-
type bidiSignature<fn extends AnyFn<void>, C extends Context<any>> = (C extends DefaultContext ? <Meta extends z.ZodObject<any> | undefined, Ack extends boolean = false>(context: {
|
|
32
|
-
metadata?: Meta;
|
|
33
|
-
ack?: Ack;
|
|
34
|
-
}) => bidiSignature<fn, Context<Meta>> : fn) & {
|
|
35
|
-
[ScopeTag]: "bidi";
|
|
13
|
+
declare const FunctionTag: unique symbol;
|
|
14
|
+
declare const ContextTag: unique symbol;
|
|
15
|
+
type BaseSignature<type extends "server" | "client" | "bidi", fn extends (...args: any[]) => any, C extends AnyContext | undefined> = {
|
|
16
|
+
[ScopeTag]: type;
|
|
17
|
+
[FunctionTag]: fn;
|
|
18
|
+
[ContextTag]: type extends "server" ? (C extends undefined ? Context<undefined> : C) : C;
|
|
36
19
|
};
|
|
20
|
+
type AnyBaseSignature = BaseSignature<any, any, any>;
|
|
21
|
+
type ExtractFn<S extends AnyBaseSignature> = (...args: Parameters<S[typeof FunctionTag]>) => Promise<ReturnType<S[typeof FunctionTag]>>;
|
|
22
|
+
type ValidReturnType<fn extends (...args: any[]) => any> = ReturnType<fn> extends Function | Promise<any> ? never : ReturnType<fn>;
|
|
23
|
+
type ExtractImplFn<S extends AnyBaseSignature> = S[typeof ScopeTag] extends "server" ? ExtractFn<S> | ((...args: Parameters<S[typeof FunctionTag]>) => (context: S[typeof ContextTag]) => Promise<ReturnType<S[typeof FunctionTag]>>) : S[typeof ScopeTag] extends "client" ? ExtractFn<S> : S[typeof ScopeTag] extends "bidi" ? undefined : never;
|
|
37
24
|
type RpcMethodDescriptor = {
|
|
38
25
|
serviceType: "server" | "client" | "bidi";
|
|
39
26
|
methodType: "unary" | "bidi";
|
|
@@ -42,16 +29,52 @@ type RpcMethodDescriptor = {
|
|
|
42
29
|
ack: boolean;
|
|
43
30
|
};
|
|
44
31
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
32
|
+
|
|
33
|
+
type BidiSignature<fn extends (...args: any[]) => any, C extends AnyContext | undefined> = BaseSignature<"bidi", fn, C> & (C extends undefined ? <Meta extends z$1.ZodObject<any>>(context: {
|
|
34
|
+
metadata?: Meta;
|
|
35
|
+
ack?: boolean;
|
|
36
|
+
}) => BidiSignature<fn, Context<Meta>> : {});
|
|
37
|
+
declare function bidi<fn extends (...args: any[]) => ValidReturnType<fn>>(): BidiSignature<fn, undefined>;
|
|
38
|
+
|
|
39
|
+
type ClientSignature<fn extends (...args: any[]) => any, C extends AnyContext | undefined> = BaseSignature<"client", fn, C>;
|
|
40
|
+
declare function client<fn extends (...args: any[]) => ValidReturnType<fn>>(): ClientSignature<fn, undefined>;
|
|
41
|
+
type ClientImpls<T> = {
|
|
42
|
+
[K in keyof T as T[K] extends BaseSignature<"client", any, any> ? K : never]: T[K] extends AnyBaseSignature ? ExtractImplFn<T[K]> : never;
|
|
43
|
+
};
|
|
44
|
+
type ClientCallable<T, WithListen extends boolean = true> = {
|
|
45
|
+
[K in keyof T as T[K] extends BaseSignature<"client", any, any> | BaseSignature<"bidi", any, any> ? K : never]: T[K] extends BaseSignature<"client", any, any> ? ExtractFn<T[K]> : T[K] extends BaseSignature<"bidi", any, infer C> ? WithListen extends true ? BidiCallable$1<T[K], C> : BidiCallableWithoutListen<T[K], C> : never;
|
|
51
46
|
};
|
|
52
|
-
type
|
|
53
|
-
|
|
47
|
+
type BidiContext<C extends AnyContext | undefined> = C extends undefined ? Promise<Context<undefined>> : Promise<C>;
|
|
48
|
+
type BidiCallableBase<S extends BaseSignature<"bidi", any, any>, C extends AnyContext | undefined> = ExtractFn<S> & {
|
|
49
|
+
context: BidiContext<C>;
|
|
50
|
+
} & AsyncGenerator<Parameters<ExtractFn<S>>, void, unknown>;
|
|
51
|
+
type BidiCallable$1<S extends BaseSignature<"bidi", any, any>, C extends AnyContext | undefined> = BidiCallableBase<S, C> & {
|
|
52
|
+
listen(handler: (connection: {
|
|
53
|
+
context: BidiContext<C>;
|
|
54
|
+
messages: AsyncGenerator<Parameters<ExtractFn<S>>, void, unknown>;
|
|
55
|
+
send: ExtractFn<S>;
|
|
56
|
+
}) => void): void;
|
|
54
57
|
};
|
|
58
|
+
type BidiCallableWithoutListen<S extends BaseSignature<"bidi", any, any>, C extends AnyContext | undefined> = BidiCallableBase<S, C>;
|
|
59
|
+
|
|
60
|
+
type ServerSignature<fn extends (...args: any[]) => any, C extends AnyContext | undefined> = BaseSignature<"server", fn, C> & (C extends undefined ? <Meta extends z$1.ZodObject<any>>(context: {
|
|
61
|
+
metadata: Meta;
|
|
62
|
+
}) => ServerSignature<fn, Context<Meta>> : {});
|
|
63
|
+
declare function server<fn extends (...args: any[]) => ValidReturnType<fn>>(): ServerSignature<fn, undefined>;
|
|
64
|
+
type ServerImpls<T> = {
|
|
65
|
+
[K in keyof T as T[K] extends BaseSignature<"server", any, any> ? K : never]: T[K] extends AnyBaseSignature ? ExtractImplFn<T[K]> : never;
|
|
66
|
+
};
|
|
67
|
+
type ServerCallable<T> = {
|
|
68
|
+
[K in keyof T as T[K] extends BaseSignature<"server", any, any> | BaseSignature<"bidi", any, any> ? K : never]: T[K] extends BaseSignature<"server", any, infer C> ? (...args: Parameters<ExtractFn<T[K]>>) => CallableChain<ExtractFn<T[K]>, C> : T[K] extends BaseSignature<"bidi", any, infer C> ? BidiCallable<T[K], C> : never;
|
|
69
|
+
};
|
|
70
|
+
type CallableChain<fn extends (...args: any[]) => any, C extends AnyContext | undefined> = C extends Context<infer Meta> ? Meta extends z$1.ZodObject<any> ? {
|
|
71
|
+
withMeta(meta: z$1.infer<Meta>): ReturnType<fn>;
|
|
72
|
+
} : ReturnType<fn> : ReturnType<fn>;
|
|
73
|
+
type BidiCallable<S extends BaseSignature<"bidi", any, any>, C extends AnyContext | undefined> = (C extends Context<infer Meta> ? Meta extends z$1.ZodObject<any> ? {
|
|
74
|
+
context(context: {
|
|
75
|
+
metadata: z$1.infer<Meta>;
|
|
76
|
+
}): Promise<void>;
|
|
77
|
+
} & ExtractFn<S> : ExtractFn<S> : ExtractFn<S>) & AsyncGenerator<Parameters<ExtractFn<S>>, void, unknown>;
|
|
55
78
|
|
|
56
79
|
declare const ServiceNameTag: unique symbol;
|
|
57
80
|
interface ServiceInstance<N extends string = string> {
|
|
@@ -70,22 +93,26 @@ declare function Service<N extends string>(name: N): (abstract new () => {
|
|
|
70
93
|
readonly [ServiceNameTag]: N;
|
|
71
94
|
}) & {
|
|
72
95
|
serviceName: N;
|
|
73
|
-
Server<T extends AbstractServiceClass, Impl extends
|
|
74
|
-
Client<T extends AbstractServiceClass, Impl extends
|
|
96
|
+
Server<T extends AbstractServiceClass, Impl extends ServerImpls<InstanceType<T>>>(this: T, implementation: Impl): ServiceImpl<T, "server">;
|
|
97
|
+
Client<T extends AbstractServiceClass, Impl extends ClientImpls<InstanceType<T>>>(this: T, implementation: Impl): ServiceImpl<T, "client">;
|
|
75
98
|
};
|
|
76
99
|
type ServiceNameOf<T extends ServiceImpl<any, any>> = T extends ServiceImpl<infer ServiceClass, any> ? InstanceType<ServiceClass>[typeof ServiceNameTag] : never;
|
|
77
|
-
type ServiceCallable<T> = T extends ServiceImpl<infer S, infer Mode> ? Mode extends "server" ?
|
|
100
|
+
type ServiceCallable<T, WithListen extends boolean = true> = T extends ServiceImpl<infer S, infer Mode> ? Mode extends "server" ? ClientCallable<InstanceType<S>, WithListen> : ServerCallable<InstanceType<S>> : never;
|
|
78
101
|
|
|
79
102
|
declare const DEFAULT_OPTIONS: ChannelOptions;
|
|
80
103
|
declare function createGrpcClient<T extends ServiceImpl<any, "client">[]>(address: string, ...serviceImpls: T): Promise<{
|
|
81
104
|
[I in T[number] as ServiceNameOf<I>]: ServiceCallable<I>;
|
|
105
|
+
} & {
|
|
106
|
+
clientID: string;
|
|
82
107
|
}>;
|
|
83
108
|
declare function createGrpcClient<T extends ServiceImpl<any, "client">[]>(address: string, grpcOptions: ChannelOptions, ...serviceImpls: T): Promise<{
|
|
84
109
|
[I in T[number] as ServiceNameOf<I>]: ServiceCallable<I>;
|
|
110
|
+
} & {
|
|
111
|
+
clientID: string;
|
|
85
112
|
}>;
|
|
86
113
|
|
|
87
114
|
declare function createGrpcServer<T extends ServiceImpl<any, "server">[]>(port: number, ...serviceImpls: T): Promise<{
|
|
88
|
-
[I in T[number] as ServiceNameOf<I>]: ServiceCallable<I
|
|
115
|
+
[I in T[number] as ServiceNameOf<I>]: ServiceCallable<I> & ((clientId: string) => ServiceCallable<I, false>);
|
|
89
116
|
}>;
|
|
90
117
|
|
|
91
118
|
export { DEFAULT_OPTIONS, Service, bidi, client, createGrpcClient, createGrpcServer, server };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { pushable } from 'it-pushable';
|
|
2
2
|
import { ChannelCredentials, createChannel, createClientFactory, createServer, Metadata } from 'nice-grpc';
|
|
3
3
|
import { decode, encode } from '@msgpack/msgpack';
|
|
4
|
+
import { Mutex } from 'async-mutex';
|
|
4
5
|
|
|
5
6
|
var __create = Object.create;
|
|
6
7
|
var __defProp = Object.defineProperty;
|
|
@@ -24039,42 +24040,46 @@ var require_src3 = __commonJS({
|
|
|
24039
24040
|
}
|
|
24040
24041
|
});
|
|
24041
24042
|
|
|
24042
|
-
// src/core/
|
|
24043
|
-
function
|
|
24043
|
+
// src/core/bidi.ts
|
|
24044
|
+
function bidi() {
|
|
24044
24045
|
const descriptor = {
|
|
24045
|
-
serviceType: "
|
|
24046
|
-
methodType: "
|
|
24046
|
+
serviceType: "bidi",
|
|
24047
|
+
methodType: "bidi"
|
|
24047
24048
|
};
|
|
24048
|
-
const
|
|
24049
|
+
const configFn = (config) => {
|
|
24049
24050
|
descriptor.config = {
|
|
24050
|
-
metadata:
|
|
24051
|
-
ack: false
|
|
24051
|
+
metadata: config.metadata !== void 0,
|
|
24052
|
+
ack: config.ack ?? false
|
|
24052
24053
|
};
|
|
24053
24054
|
return descriptor;
|
|
24054
24055
|
};
|
|
24055
|
-
Object.assign(
|
|
24056
|
-
return
|
|
24056
|
+
Object.assign(configFn, descriptor);
|
|
24057
|
+
return configFn;
|
|
24057
24058
|
}
|
|
24059
|
+
|
|
24060
|
+
// src/core/client.ts
|
|
24058
24061
|
function client() {
|
|
24059
24062
|
return {
|
|
24060
24063
|
serviceType: "client",
|
|
24061
24064
|
methodType: "unary"
|
|
24062
24065
|
};
|
|
24063
24066
|
}
|
|
24064
|
-
|
|
24067
|
+
|
|
24068
|
+
// src/core/server.ts
|
|
24069
|
+
function server() {
|
|
24065
24070
|
const descriptor = {
|
|
24066
|
-
serviceType: "
|
|
24067
|
-
methodType: "
|
|
24071
|
+
serviceType: "server",
|
|
24072
|
+
methodType: "unary"
|
|
24068
24073
|
};
|
|
24069
|
-
const
|
|
24074
|
+
const contextFn = (context) => {
|
|
24070
24075
|
descriptor.config = {
|
|
24071
|
-
metadata:
|
|
24072
|
-
ack:
|
|
24076
|
+
metadata: context.metadata !== void 0,
|
|
24077
|
+
ack: false
|
|
24073
24078
|
};
|
|
24074
24079
|
return descriptor;
|
|
24075
24080
|
};
|
|
24076
|
-
Object.assign(
|
|
24077
|
-
return
|
|
24081
|
+
Object.assign(contextFn, descriptor);
|
|
24082
|
+
return contextFn;
|
|
24078
24083
|
}
|
|
24079
24084
|
|
|
24080
24085
|
// src/core/service.ts
|
|
@@ -24209,6 +24214,7 @@ function buildServiceProto(base) {
|
|
|
24209
24214
|
|
|
24210
24215
|
// src/runtime/client.ts
|
|
24211
24216
|
var GrpcClient = class {
|
|
24217
|
+
clientID;
|
|
24212
24218
|
address;
|
|
24213
24219
|
serviceImpls;
|
|
24214
24220
|
channel;
|
|
@@ -24221,6 +24227,7 @@ var GrpcClient = class {
|
|
|
24221
24227
|
// bidi that is waiting for context
|
|
24222
24228
|
pendingBidiAck = /* @__PURE__ */ new Map();
|
|
24223
24229
|
constructor(address, grpcOptions, serviceImpls) {
|
|
24230
|
+
this.clientID = crypto.randomUUID();
|
|
24224
24231
|
this.address = address;
|
|
24225
24232
|
this.serviceImpls = serviceImpls;
|
|
24226
24233
|
this.proto = loadProtoFromString(buildProtoString(serviceImpls));
|
|
@@ -24265,7 +24272,11 @@ var GrpcClient = class {
|
|
|
24265
24272
|
switch (`${descriptor.serviceType}:${descriptor.methodType}`) {
|
|
24266
24273
|
case "client:unary": {
|
|
24267
24274
|
const incomingStream = pushable({ objectMode: true });
|
|
24268
|
-
const incomingMessages = client2[name.toUpperCase()](incomingStream
|
|
24275
|
+
const incomingMessages = client2[name.toUpperCase()](incomingStream, {
|
|
24276
|
+
metadata: encodeMetadata({
|
|
24277
|
+
BETTER_GRPC_CLIENT_ID: this.clientID
|
|
24278
|
+
})
|
|
24279
|
+
});
|
|
24269
24280
|
(async () => {
|
|
24270
24281
|
try {
|
|
24271
24282
|
for await (const message of incomingMessages) {
|
|
@@ -24289,7 +24300,12 @@ var GrpcClient = class {
|
|
|
24289
24300
|
outStream
|
|
24290
24301
|
);
|
|
24291
24302
|
this.setStream(`${serviceImpl.serviceClass.serviceName}_IN`, name.toUpperCase(), inStream);
|
|
24292
|
-
const incomingMessages =
|
|
24303
|
+
const incomingMessages = client2[name.toUpperCase()](outStream, {
|
|
24304
|
+
metadata: encodeMetadata({
|
|
24305
|
+
...context?.metadata ?? {},
|
|
24306
|
+
BETTER_GRPC_CLIENT_ID: this.clientID
|
|
24307
|
+
})
|
|
24308
|
+
});
|
|
24293
24309
|
(async () => {
|
|
24294
24310
|
try {
|
|
24295
24311
|
for await (const message of incomingMessages) {
|
|
@@ -24461,15 +24477,30 @@ function createServiceImpl(serviceImpl, grpcServer) {
|
|
|
24461
24477
|
case "server:unary":
|
|
24462
24478
|
grpcImpl[name.toUpperCase()] = async (req, ctx) => {
|
|
24463
24479
|
const [_, value] = decodeRequestMessage(req);
|
|
24464
|
-
const
|
|
24465
|
-
|
|
24480
|
+
const impl = serviceImpl.implementation[name];
|
|
24481
|
+
if (!impl) {
|
|
24482
|
+
throw new Error(`Method ${name} not found`);
|
|
24483
|
+
}
|
|
24484
|
+
let result = await impl(...value ?? []);
|
|
24485
|
+
if (typeof result === "function") {
|
|
24486
|
+
const metadata = decodeMetadata(ctx.metadata);
|
|
24487
|
+
const clientID = metadata.BETTER_GRPC_CLIENT_ID;
|
|
24488
|
+
result = await result({ metadata, client: { id: clientID } });
|
|
24489
|
+
}
|
|
24466
24490
|
return encodeResponseMessage(void 0, result);
|
|
24467
24491
|
};
|
|
24468
24492
|
break;
|
|
24469
24493
|
case "client:unary":
|
|
24470
|
-
grpcImpl[name.toUpperCase()] = async function* (incomingStream) {
|
|
24494
|
+
grpcImpl[name.toUpperCase()] = async function* (incomingStream, ctx) {
|
|
24495
|
+
const metadata = decodeMetadata(ctx.metadata);
|
|
24496
|
+
const clientID = metadata.BETTER_GRPC_CLIENT_ID;
|
|
24497
|
+
await grpcServer.defaultClientMutex.runExclusive(() => {
|
|
24498
|
+
if (!grpcServer.defaultClientID) {
|
|
24499
|
+
grpcServer.setDefaultClient(clientID);
|
|
24500
|
+
}
|
|
24501
|
+
});
|
|
24471
24502
|
const stream = pushable({ objectMode: true });
|
|
24472
|
-
grpcServer.setStream(serviceImpl.serviceClass.serviceName
|
|
24503
|
+
grpcServer.setStream(`${serviceImpl.serviceClass.serviceName}_${clientID}`, name, stream);
|
|
24473
24504
|
(async () => {
|
|
24474
24505
|
try {
|
|
24475
24506
|
for await (const message of incomingStream) {
|
|
@@ -24489,15 +24520,21 @@ function createServiceImpl(serviceImpl, grpcServer) {
|
|
|
24489
24520
|
break;
|
|
24490
24521
|
case "bidi:bidi":
|
|
24491
24522
|
grpcImpl[name.toUpperCase()] = async function* (incomingStream, ctx) {
|
|
24492
|
-
|
|
24493
|
-
|
|
24494
|
-
|
|
24495
|
-
|
|
24496
|
-
|
|
24523
|
+
const metadata = decodeMetadata(ctx.metadata);
|
|
24524
|
+
const clientID = metadata.BETTER_GRPC_CLIENT_ID;
|
|
24525
|
+
await grpcServer.defaultClientMutex.runExclusive(() => {
|
|
24526
|
+
if (!grpcServer.defaultClientID) {
|
|
24527
|
+
grpcServer.setDefaultClient(clientID);
|
|
24528
|
+
}
|
|
24529
|
+
});
|
|
24530
|
+
grpcServer.setContext(serviceImpl.serviceClass.serviceName, name, {
|
|
24531
|
+
metadata,
|
|
24532
|
+
client: { id: clientID }
|
|
24533
|
+
});
|
|
24497
24534
|
const outStream = pushable({ objectMode: true });
|
|
24498
24535
|
const inStream = pushable({ objectMode: true });
|
|
24499
|
-
grpcServer.setStream(`${serviceImpl.serviceClass.serviceName}_OUT`, name, outStream);
|
|
24500
|
-
grpcServer.setStream(`${serviceImpl.serviceClass.serviceName}_IN`, name, inStream);
|
|
24536
|
+
grpcServer.setStream(`${serviceImpl.serviceClass.serviceName}_${clientID}_OUT`, name, outStream);
|
|
24537
|
+
grpcServer.setStream(`${serviceImpl.serviceClass.serviceName}_${clientID}_IN`, name, inStream);
|
|
24501
24538
|
(async () => {
|
|
24502
24539
|
try {
|
|
24503
24540
|
for await (const message of incomingStream) {
|
|
@@ -24517,6 +24554,19 @@ function createServiceImpl(serviceImpl, grpcServer) {
|
|
|
24517
24554
|
outStream.end();
|
|
24518
24555
|
}
|
|
24519
24556
|
})();
|
|
24557
|
+
grpcServer.getBidiConnectionStream(serviceImpl.serviceClass.serviceName, name).push({
|
|
24558
|
+
context: grpcServer.getContext(serviceImpl.serviceClass.serviceName, name),
|
|
24559
|
+
messages: inStream,
|
|
24560
|
+
send: async (...args) => {
|
|
24561
|
+
const ackId = descriptor.config?.ack ? crypto.randomUUID() : void 0;
|
|
24562
|
+
outStream.push(encodeRequestMessage(ackId, args));
|
|
24563
|
+
if (ackId) {
|
|
24564
|
+
return new Promise((resolve) => {
|
|
24565
|
+
grpcServer.pendingBidiAck.set(ackId, resolve);
|
|
24566
|
+
});
|
|
24567
|
+
}
|
|
24568
|
+
}
|
|
24569
|
+
});
|
|
24520
24570
|
yield* outStream;
|
|
24521
24571
|
};
|
|
24522
24572
|
break;
|
|
@@ -24539,6 +24589,9 @@ var GrpcServer = class {
|
|
|
24539
24589
|
contexts = /* @__PURE__ */ new Map();
|
|
24540
24590
|
pendingContext = /* @__PURE__ */ new Map();
|
|
24541
24591
|
pendingBidiAck = /* @__PURE__ */ new Map();
|
|
24592
|
+
defaultClientID = void 0;
|
|
24593
|
+
pendingDefaultClient = void 0;
|
|
24594
|
+
bidiConnections = {};
|
|
24542
24595
|
constructor(address, serviceImpls) {
|
|
24543
24596
|
this.address = address;
|
|
24544
24597
|
this.serviceImpls = serviceImpls;
|
|
@@ -24599,61 +24652,111 @@ var GrpcServer = class {
|
|
|
24599
24652
|
resolve(value);
|
|
24600
24653
|
this.pendingRequests.delete(id);
|
|
24601
24654
|
}
|
|
24655
|
+
async waitingDefaultClient() {
|
|
24656
|
+
if (this.defaultClientID) {
|
|
24657
|
+
return this.defaultClientID;
|
|
24658
|
+
}
|
|
24659
|
+
return new Promise((resolve) => {
|
|
24660
|
+
this.pendingDefaultClient = resolve;
|
|
24661
|
+
});
|
|
24662
|
+
}
|
|
24663
|
+
defaultClientMutex = new Mutex();
|
|
24664
|
+
getBidiConnectionStream(serviceName, methodName) {
|
|
24665
|
+
const key = `${serviceName}.${methodName.toUpperCase()}`;
|
|
24666
|
+
if (!this.bidiConnections[key]) {
|
|
24667
|
+
this.bidiConnections[key] = pushable({ objectMode: true });
|
|
24668
|
+
}
|
|
24669
|
+
return this.bidiConnections[key];
|
|
24670
|
+
}
|
|
24671
|
+
setDefaultClient(clientID) {
|
|
24672
|
+
this.defaultClientID = clientID;
|
|
24673
|
+
if (this.pendingDefaultClient) {
|
|
24674
|
+
this.pendingDefaultClient(clientID);
|
|
24675
|
+
this.pendingDefaultClient = void 0;
|
|
24676
|
+
}
|
|
24677
|
+
}
|
|
24602
24678
|
bindFns() {
|
|
24603
24679
|
for (const serviceImpl of this.serviceImpls) {
|
|
24604
|
-
const
|
|
24605
|
-
|
|
24606
|
-
|
|
24607
|
-
|
|
24608
|
-
|
|
24609
|
-
|
|
24610
|
-
|
|
24611
|
-
const
|
|
24612
|
-
this.pendingRequests.set(requestId, resolve);
|
|
24613
|
-
stream.then((s) => s.push(encodeRequestMessage(requestId, args)));
|
|
24614
|
-
});
|
|
24615
|
-
};
|
|
24616
|
-
break;
|
|
24617
|
-
case "bidi:bidi": {
|
|
24618
|
-
async function* generator(server2) {
|
|
24619
|
-
const inStream = await server2.getStream(
|
|
24620
|
-
`${serviceImpl.serviceClass.serviceName}_IN`,
|
|
24621
|
-
name.toUpperCase()
|
|
24622
|
-
);
|
|
24623
|
-
yield* inStream;
|
|
24624
|
-
}
|
|
24625
|
-
const emitFn = async (...args) => {
|
|
24626
|
-
const outStream = await this.getStream(
|
|
24627
|
-
`${serviceImpl.serviceClass.serviceName}_OUT`,
|
|
24628
|
-
name.toUpperCase()
|
|
24629
|
-
);
|
|
24630
|
-
const ackId = descriptor.config?.ack ? crypto.randomUUID() : void 0;
|
|
24631
|
-
outStream.push(encodeRequestMessage(ackId, args));
|
|
24632
|
-
if (ackId) {
|
|
24680
|
+
const buildServiceCallable = (clientID) => {
|
|
24681
|
+
const callableInstance = {};
|
|
24682
|
+
for (const [name, descriptor] of Object.entries(serviceImpl.methods())) {
|
|
24683
|
+
switch (`${descriptor.serviceType}:${descriptor.methodType}`) {
|
|
24684
|
+
case "client:unary":
|
|
24685
|
+
callableInstance[name] = async (...args) => {
|
|
24686
|
+
const nonNullClientID = clientID ?? await this.waitingDefaultClient();
|
|
24687
|
+
const requestId = crypto.randomUUID();
|
|
24633
24688
|
return new Promise((resolve) => {
|
|
24634
|
-
this.
|
|
24689
|
+
const stream = this.getStream(
|
|
24690
|
+
`${serviceImpl.serviceClass.serviceName}_${nonNullClientID}`,
|
|
24691
|
+
name.toUpperCase()
|
|
24692
|
+
);
|
|
24693
|
+
this.pendingRequests.set(requestId, resolve);
|
|
24694
|
+
stream.then((s) => s.push(encodeRequestMessage(requestId, args)));
|
|
24635
24695
|
});
|
|
24696
|
+
};
|
|
24697
|
+
break;
|
|
24698
|
+
case "bidi:bidi": {
|
|
24699
|
+
async function* generator(server2) {
|
|
24700
|
+
const nonNullClientID = clientID ?? await server2.waitingDefaultClient();
|
|
24701
|
+
console.log(`${serviceImpl.serviceClass.serviceName}_${nonNullClientID}_IN`);
|
|
24702
|
+
const inStream = await server2.getStream(
|
|
24703
|
+
`${serviceImpl.serviceClass.serviceName}_${nonNullClientID}_IN`,
|
|
24704
|
+
name.toUpperCase()
|
|
24705
|
+
);
|
|
24706
|
+
yield* inStream;
|
|
24636
24707
|
}
|
|
24637
|
-
|
|
24638
|
-
|
|
24639
|
-
|
|
24640
|
-
|
|
24641
|
-
|
|
24642
|
-
|
|
24643
|
-
const
|
|
24644
|
-
|
|
24645
|
-
|
|
24646
|
-
return
|
|
24647
|
-
|
|
24708
|
+
const emitFn = async (...args) => {
|
|
24709
|
+
const nonNullClientID = clientID ?? await this.waitingDefaultClient();
|
|
24710
|
+
const outStream = await this.getStream(
|
|
24711
|
+
`${serviceImpl.serviceClass.serviceName}_${nonNullClientID}_OUT`,
|
|
24712
|
+
name.toUpperCase()
|
|
24713
|
+
);
|
|
24714
|
+
const ackId = descriptor.config?.ack ? crypto.randomUUID() : void 0;
|
|
24715
|
+
outStream.push(encodeRequestMessage(ackId, args));
|
|
24716
|
+
if (ackId) {
|
|
24717
|
+
return new Promise((resolve) => {
|
|
24718
|
+
this.pendingBidiAck.set(ackId, resolve);
|
|
24719
|
+
});
|
|
24720
|
+
}
|
|
24721
|
+
};
|
|
24722
|
+
const context = {
|
|
24723
|
+
context: this.getContext(serviceImpl.serviceClass.serviceName, name)
|
|
24724
|
+
};
|
|
24725
|
+
const hybrid = Object.assign(emitFn, {
|
|
24726
|
+
[Symbol.asyncIterator]: () => {
|
|
24727
|
+
const iterator = generator(this);
|
|
24728
|
+
return {
|
|
24729
|
+
next: iterator.next.bind(iterator),
|
|
24730
|
+
return: iterator.return.bind(iterator),
|
|
24731
|
+
throw: iterator.throw.bind(iterator)
|
|
24732
|
+
};
|
|
24733
|
+
}
|
|
24734
|
+
});
|
|
24735
|
+
Object.assign(hybrid, context);
|
|
24736
|
+
if (!clientID) {
|
|
24737
|
+
const listen = {
|
|
24738
|
+
listen: (handler) => {
|
|
24739
|
+
(async () => {
|
|
24740
|
+
const stream = this.getBidiConnectionStream(
|
|
24741
|
+
serviceImpl.serviceClass.serviceName,
|
|
24742
|
+
name
|
|
24743
|
+
);
|
|
24744
|
+
for await (const connection of stream) {
|
|
24745
|
+
handler(connection);
|
|
24746
|
+
}
|
|
24747
|
+
})();
|
|
24748
|
+
}
|
|
24648
24749
|
};
|
|
24750
|
+
Object.assign(hybrid, listen);
|
|
24649
24751
|
}
|
|
24650
|
-
|
|
24651
|
-
|
|
24652
|
-
|
|
24653
|
-
break;
|
|
24752
|
+
callableInstance[name] = hybrid;
|
|
24753
|
+
break;
|
|
24754
|
+
}
|
|
24654
24755
|
}
|
|
24655
24756
|
}
|
|
24656
|
-
|
|
24757
|
+
return callableInstance;
|
|
24758
|
+
};
|
|
24759
|
+
const serviceCallableInstance = Object.assign(buildServiceCallable, buildServiceCallable(void 0));
|
|
24657
24760
|
Object.defineProperty(this, serviceImpl.serviceClass.serviceName, {
|
|
24658
24761
|
value: serviceCallableInstance,
|
|
24659
24762
|
writable: false,
|