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 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. Attach typed metadata
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 receive the typed metadata as the first argument:
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 greet(context, name) {
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 object as the first argument.
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 PrependContext<C extends Context<any>, Args extends any[]> = [C, ...Args];
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
- type AnyFn<R> = (...args: any[]) => R;
17
- type BidiType<fn extends AnyFn<any>, C extends Context<any>, type extends "server" | "client"> = fn & AsyncGenerator<Parameters<fn>, void, unknown> & BidiContextType<C, type>;
18
- type BidiContextType<C extends Context<any>, type extends "server" | "client"> = type extends "client" ? {
19
- context: Promise<C>;
20
- } : {
21
- context(context: C): Promise<void>;
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
- declare function server<fn extends AnyFn<any>>(): serverSignature<(...args: Parameters<fn>) => Promise<ReturnType<fn>>, DefaultContext>;
46
- declare function client<fn extends AnyFn<any>>(): clientSignature<(...args: Parameters<fn>) => Promise<ReturnType<fn>>>;
47
- declare function bidi<fn extends AnyFn<void>>(..._: ReturnType<fn> extends void ? [] : ["Return type must be void"]): bidiSignature<(...args: Parameters<fn>) => Promise<void>, DefaultContext>;
48
- type Unwrap<T, type extends "server" | "client", ContextChain extends boolean = false> = T extends serverSignature<infer F, infer C> ? C extends DefaultContext ? F : ContextChain extends true ? ContextRequiredFn<F, C> : (...args: PrependContext<C, Parameters<F>>) => ReturnType<F> : T extends clientSignature<infer F> ? F : T extends bidiSignature<infer F, infer C> ? BidiType<F, C, type> : never;
49
- type ServerFn<T, IncludeBidi extends boolean = true, ContextChain extends boolean = false> = {
50
- [K in keyof T as T[K] extends serverSignature<AnyFn<any>, any> | (IncludeBidi extends true ? bidiSignature<AnyFn<void>, any> : never) ? K : never]: Unwrap<T[K], "server", ContextChain>;
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 ClientFn<T, IncludeBidi extends boolean = true> = {
53
- [K in keyof T as T[K] extends clientSignature<AnyFn<any>> | (IncludeBidi extends true ? bidiSignature<AnyFn<void>, any> : never) ? K : never]: Unwrap<T[K], "client">;
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 ServerFn<InstanceType<T>, false>>(this: T, implementation: Impl): ServiceImpl<T, "server">;
74
- Client<T extends AbstractServiceClass, Impl extends ClientFn<InstanceType<T>, false>>(this: T, implementation: Impl): ServiceImpl<T, "client">;
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" ? ClientFn<InstanceType<S>> : ServerFn<InstanceType<S>, true, true> : never;
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/rpc-signatures.ts
24043
- function server() {
24043
+ // src/core/bidi.ts
24044
+ function bidi() {
24044
24045
  const descriptor = {
24045
- serviceType: "server",
24046
- methodType: "unary"
24046
+ serviceType: "bidi",
24047
+ methodType: "bidi"
24047
24048
  };
24048
- const contextFn = (context) => {
24049
+ const configFn = (config) => {
24049
24050
  descriptor.config = {
24050
- metadata: context.metadata !== void 0,
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(contextFn, descriptor);
24056
- return contextFn;
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
- function bidi(..._) {
24067
+
24068
+ // src/core/server.ts
24069
+ function server() {
24065
24070
  const descriptor = {
24066
- serviceType: "bidi",
24067
- methodType: "bidi"
24071
+ serviceType: "server",
24072
+ methodType: "unary"
24068
24073
  };
24069
- const configFn = (config) => {
24074
+ const contextFn = (context) => {
24070
24075
  descriptor.config = {
24071
- metadata: config.metadata !== void 0,
24072
- ack: config.ack ?? false
24076
+ metadata: context.metadata !== void 0,
24077
+ ack: false
24073
24078
  };
24074
24079
  return descriptor;
24075
24080
  };
24076
- Object.assign(configFn, descriptor);
24077
- return configFn;
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 = context ? client2[name.toUpperCase()](outStream, { metadata: encodeMetadata(context.metadata) }) : client2[name.toUpperCase()](outStream);
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 args = descriptor.config?.metadata ? [{ metadata: decodeMetadata(ctx.metadata) }, ...value ?? []] : value ?? [];
24465
- const result = await serviceImpl.implementation[name](...args);
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, name, stream);
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
- if (descriptor.config?.metadata) {
24493
- grpcServer.setContext(serviceImpl.serviceClass.serviceName, name, {
24494
- metadata: decodeMetadata(ctx.metadata)
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 serviceCallableInstance = {};
24605
- for (const [name, descriptor] of Object.entries(serviceImpl.methods())) {
24606
- switch (`${descriptor.serviceType}:${descriptor.methodType}`) {
24607
- case "client:unary":
24608
- serviceCallableInstance[name] = async (...args) => {
24609
- const requestId = crypto.randomUUID();
24610
- return new Promise((resolve) => {
24611
- const stream = this.getStream(serviceImpl.serviceClass.serviceName, name.toUpperCase());
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.pendingBidiAck.set(ackId, resolve);
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
- const context = {
24639
- context: this.getContext(serviceImpl.serviceClass.serviceName, name)
24640
- };
24641
- const hybrid = Object.assign(emitFn, {
24642
- [Symbol.asyncIterator]: () => {
24643
- const iterator = generator(this);
24644
- return {
24645
- next: iterator.next.bind(iterator),
24646
- return: iterator.return.bind(iterator),
24647
- throw: iterator.throw.bind(iterator)
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
- Object.assign(hybrid, context);
24652
- serviceCallableInstance[name] = hybrid;
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,