@dr_nikson/effect-grpc 0.2.0-mvp-9971208edee70b39058f12d8c2a1fcfaeecd3b51 → 3.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +863 -0
  2. package/dist/client.d.ts +38 -3
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.internal.d.ts.map +1 -1
  5. package/dist/client.internal.js +36 -9
  6. package/dist/client.internal.js.map +1 -1
  7. package/dist/client.js +2 -0
  8. package/dist/client.js.map +1 -1
  9. package/dist/grpcException.d.ts +243 -0
  10. package/dist/grpcException.d.ts.map +1 -0
  11. package/dist/grpcException.internal.d.ts +28 -0
  12. package/dist/grpcException.internal.d.ts.map +1 -0
  13. package/dist/grpcException.internal.js +72 -0
  14. package/dist/grpcException.internal.js.map +1 -0
  15. package/dist/grpcException.js +218 -0
  16. package/dist/grpcException.js.map +1 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/protoRuntime.d.ts +7 -6
  22. package/dist/protoRuntime.d.ts.map +1 -1
  23. package/dist/protoRuntime.internal.d.ts +7 -6
  24. package/dist/protoRuntime.internal.d.ts.map +1 -1
  25. package/dist/protoRuntime.internal.js +20 -6
  26. package/dist/protoRuntime.internal.js.map +1 -1
  27. package/dist/protoRuntime.js +2 -0
  28. package/dist/protoRuntime.js.map +1 -1
  29. package/dist/protocGenPlugin.d.ts.map +1 -1
  30. package/dist/protocGenPlugin.js +360 -147
  31. package/dist/protocGenPlugin.js.map +1 -1
  32. package/dist/server.d.ts +10 -6
  33. package/dist/server.d.ts.map +1 -1
  34. package/dist/server.internal.d.ts +6 -5
  35. package/dist/server.internal.d.ts.map +1 -1
  36. package/dist/server.internal.js +51 -14
  37. package/dist/server.internal.js.map +1 -1
  38. package/dist/server.js +5 -1
  39. package/dist/server.js.map +1 -1
  40. package/dist/typeUtils.js +1 -1
  41. package/dist/typeUtils.js.map +1 -1
  42. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,863 @@
1
+ # effect-grpc
2
+
3
+ Type-safe gRPC and Protocol Buffer support for the Effect ecosystem
4
+
5
+ ## Overview
6
+
7
+ `effect-grpc` provides a seamless integration between gRPC/Protocol Buffers and the Effect TypeScript. It enables you to build type-safe, composable gRPC services and clients with all the benefits of Effect's powerful error handling, dependency injection, and functional programming patterns.
8
+
9
+ **Built on battle-tested foundations:** This library is a thin wrapper around industry-standard, production-proven gRPC libraries including [Connect-RPC](https://connectrpc.com/) (Buf's modern gRPC implementation) and [@bufbuild/protobuf](https://github.com/bufbuild/protobuf-es) (official Protocol Buffers runtime for JavaScript/TypeScript), bringing Effect's functional programming benefits to the established gRPC ecosystem.
10
+
11
+ ### Key Features
12
+
13
+ - **Full Type Safety** - Generated TypeScript code from Protocol Buffers with complete type inference
14
+ - **Effect Integration** - Native support for Effect's error handling, tracing, and dependency injection
15
+ - **Code Generation** - Automatic client and server code generation via `protoc-gen-effect` plugin
16
+ - **Connect-RPC** - Built on Connect-RPC for maximum compatibility with gRPC and gRPC-Web
17
+ - **Modular Architecture** - Clean separation between service definitions and implementations
18
+ - **Zero Boilerplate** - Minimal setup required to get started
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install @dr_nikson/effect-grpc
24
+ # gRPC runtime deps
25
+ npm install @bufbuild/protobuf @connectrpc/connect
26
+ ```
27
+
28
+ For code generation, you'll also need:
29
+
30
+ ```bash
31
+ npm install --save-dev @bufbuild/buf @bufbuild/protoc-gen-es
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ This guide will walk you through setting up a simple gRPC service with effect-grpc.
37
+
38
+ ### 1. Define Your Protocol Buffer
39
+
40
+ Create a `.proto` file defining your service:
41
+
42
+ ```protobuf
43
+ // proto/hello.proto
44
+ syntax = "proto3";
45
+
46
+ package example.v1;
47
+
48
+ service HelloService {
49
+ rpc SayHello(HelloRequest) returns (HelloResponse);
50
+ }
51
+
52
+ message HelloRequest {
53
+ string name = 1;
54
+ }
55
+
56
+ message HelloResponse {
57
+ string message = 1;
58
+ }
59
+ ```
60
+
61
+ ### 2. Configure Code Generation
62
+
63
+ Create a `buf.gen.yaml` configuration file:
64
+
65
+ ```yaml
66
+ # buf.gen.yaml
67
+ version: v2
68
+ inputs:
69
+ - directory: proto
70
+ plugins:
71
+ # Generate base Protocol Buffer TypeScript code
72
+ - local: protoc-gen-es
73
+ opt: target=ts,import_extension=js
74
+ out: src/generated
75
+ # Generate Effect-specific code
76
+ - local: protoc-gen-effect
77
+ opt: target=ts,import_extension=js
78
+ out: src/generated
79
+ ```
80
+
81
+ ### 3. Generate TypeScript Code
82
+
83
+ Add the following script to your `package.json`:
84
+
85
+ ```json
86
+ {
87
+ "scripts": {
88
+ "generate:proto": "buf generate"
89
+ }
90
+ }
91
+ ```
92
+
93
+ Then run:
94
+
95
+ ```bash
96
+ npm run generate:proto
97
+ ```
98
+
99
+ This will generate TypeScript files in `src/generated/` with full Effect integration.
100
+
101
+ ### 4. Implement the Server
102
+
103
+ ```typescript
104
+ // src/server.ts
105
+ import { Context, Effect, Layer, LogLevel, Logger } from "effect";
106
+ import { HandlerContext } from "@connectrpc/connect";
107
+ import { EffectGrpcServer } from "@dr_nikson/effect-grpc";
108
+ import { NodeRuntime } from "@effect/platform-node";
109
+
110
+ import * as effectProto from "./generated/example/v1/hello_effect.js";
111
+ import * as proto from "./generated/example/v1/hello_pb.js";
112
+
113
+ // Implement the service (ctx not used, so no need to specify type)
114
+ const HelloServiceLive: effectProto.HelloServiceService = {
115
+ sayHello(request: proto.HelloRequest) {
116
+ return Effect.succeed({
117
+ message: `Hello, ${request.name}!`
118
+ });
119
+ }
120
+ };
121
+
122
+ // Create the service layer
123
+ const helloServiceLayer = effectProto.helloServiceLiveLayer(
124
+ effectProto.HelloServiceTag,
125
+ HelloServiceLive
126
+ );
127
+
128
+ // Build and run the gRPC server
129
+ const program = Effect.gen(function* () {
130
+ const helloService = yield* effectProto.HelloServiceTag;
131
+
132
+ const server: EffectGrpcServer.GrpcServer<"HelloService"> =
133
+ EffectGrpcServer
134
+ .GrpcServerBuilder()
135
+ .withService(helloService)
136
+ .build();
137
+
138
+ return yield* server.run({ host: "localhost", port: 8000 });
139
+ });
140
+
141
+ // Provide dependencies and run
142
+ const layer = Layer.empty.pipe(
143
+ Layer.provideMerge(helloServiceLayer),
144
+ Layer.provideMerge(Logger.minimumLogLevel(LogLevel.Info))
145
+ );
146
+
147
+ NodeRuntime.runMain(Effect.provide(program, layer));
148
+ ```
149
+
150
+ ### 5. Implement the Client
151
+
152
+ ```typescript
153
+ // src/client.ts
154
+ import { Effect, Layer, Logger, LogLevel } from "effect";
155
+ import { EffectGrpcClient } from "@dr_nikson/effect-grpc";
156
+ import { NodeRuntime } from "@effect/platform-node";
157
+
158
+ import * as effectProto from "./generated/example/v1/hello_effect.js";
159
+
160
+ // Create the client layer with configuration
161
+ const helloClientLayer = effectProto.helloServiceClientLiveLayer(
162
+ effectProto.HelloServiceClientTag
163
+ ).pipe(
164
+ Layer.provideMerge(
165
+ Layer.succeed(
166
+ effectProto.HelloServiceConfigTag,
167
+ EffectGrpcClient.GrpcClientConfig({
168
+ baseUrl: new URL("http://localhost:8000")
169
+ })
170
+ )
171
+ )
172
+ );
173
+
174
+ // Use the client
175
+ const program = Effect.gen(function* () {
176
+ const client = yield* effectProto.HelloServiceClientTag;
177
+
178
+ const response = yield* client.sayHello({
179
+ name: "World"
180
+ }, {});
181
+
182
+ yield* Effect.log(`Server responded: ${response.message}`);
183
+ });
184
+
185
+ // Provide dependencies and run
186
+ const dependencies = Layer.empty.pipe(
187
+ Layer.provideMerge(helloClientLayer),
188
+ Layer.provideMerge(EffectGrpcClient.liveGrpcClientRuntimeLayer()),
189
+ Layer.provideMerge(Logger.minimumLogLevel(LogLevel.Info))
190
+ );
191
+
192
+ NodeRuntime.runMain(Effect.provide(program, dependencies));
193
+ ```
194
+
195
+ ## Complete Example
196
+
197
+ For a complete working example, see the [`packages/example`](packages/example) directory in this repository. It demonstrates:
198
+
199
+ - Protocol Buffer definition and code generation
200
+ - Server implementation with Effect
201
+ - Client implementation with Effect
202
+ - Proper project structure and configuration
203
+
204
+ To run the example:
205
+
206
+ ```bash
207
+ # Clone the repository
208
+ git clone https://github.com/dr_nikson/effect-grpc.git
209
+ cd effect-grpc
210
+
211
+ # Install dependencies
212
+ pnpm install
213
+
214
+ # Build the library
215
+ pnpm -r run build
216
+
217
+ # In one terminal, start the server
218
+ cd packages/example
219
+ node dist/server.js
220
+
221
+ # In another terminal, run the client
222
+ node dist/client.js
223
+ ```
224
+
225
+ ## API Reference
226
+
227
+ This section documents the public API exported by `@dr_nikson/effect-grpc`. The library excludes internal runtime APIs from public documentation.
228
+
229
+ ### Server API (`EffectGrpcServer`)
230
+
231
+ The server API provides tools for building and running gRPC servers within Effect programs.
232
+
233
+ #### Core Types
234
+
235
+ ##### `GrpcServer<Services>`
236
+
237
+ Represents a running gRPC server instance.
238
+
239
+ **Type Parameters:**
240
+ - `Services` - Union type of all service tags registered with this server
241
+
242
+ **Methods:**
243
+ - `run(options: { host: string; port: number }): Effect.Effect<never, never, Scope.Scope>` - Starts the server on the specified host and port. Returns an Effect that requires a Scope for resource management.
244
+
245
+ **Example:**
246
+ ```typescript
247
+ const server: EffectGrpcServer.GrpcServer<"UserService" | "ProductService"> =
248
+ EffectGrpcServer.GrpcServerBuilder()
249
+ .withService(userService)
250
+ .withService(productService)
251
+ .build();
252
+
253
+ // Run with proper resource management
254
+ const program = Effect.scoped(
255
+ server.run({ host: "localhost", port: 8000 })
256
+ );
257
+ ```
258
+
259
+ ##### `GrpcServerBuilder<Ctx, Services>`
260
+
261
+ Fluent builder interface for constructing gRPC servers.
262
+
263
+ **Type Parameters:**
264
+ - `Ctx` - Context type available to service handlers (defaults to `HandlerContext`)
265
+ - `Services` - Union of currently registered service tags
266
+
267
+ **Methods:**
268
+ - `withContextTransformer<Ctx1>(f: (originalCtx: HandlerContext, ctx: Ctx) => Effect.Effect<Ctx1>): GrpcServerBuilder<Ctx1, never>` - Transform the handler context. The first parameter is the original Connect-RPC HandlerContext, the second is the current context (defaults to `any`). Must be called before adding services.
269
+ - `withService<S>(service: S): GrpcServerBuilder<Ctx, Services | Tag<S>>` - Add a service (enforces unique tags)
270
+ - `build(): GrpcServer<Services>` - Build the server (requires at least one service)
271
+
272
+ **Example:**
273
+ ```typescript
274
+ // Simple server with HandlerContext
275
+ const server = EffectGrpcServer.GrpcServerBuilder()
276
+ .withService(myService)
277
+ .build();
278
+
279
+ // Server with custom context
280
+ interface AppContext {
281
+ userId: string;
282
+ requestId: string;
283
+ }
284
+
285
+ const serverWithCtx = EffectGrpcServer.GrpcServerBuilder()
286
+ // Ctx is any here, so it is okay to omit second param
287
+ .withContextTransformer((handlerCtx: HandlerContext) =>
288
+ Effect.succeed({
289
+ requestId: crypto.randomUUID()
290
+ })
291
+ )
292
+ // Ctx has `requestId` field now, originalCtx is also available
293
+ .withContextTransformer((handlerCtx: HandlerContext, ctx) =>
294
+ Effect.succeed({
295
+ requestId: ctx.requestId,
296
+ userId: handlerCtx.requestHeader.get("user-id") ?? "anonymous",
297
+ })
298
+ )
299
+ .withService(myService)
300
+ .build();
301
+ ```
302
+
303
+ #### Factory Functions
304
+
305
+ ##### `GrpcServerBuilder()`
306
+
307
+ Creates a new server builder instance with default context.
308
+
309
+ **Returns:** `GrpcServerBuilder<any, never>`
310
+
311
+ **Example:**
312
+ ```typescript
313
+ const builder = EffectGrpcServer.GrpcServerBuilder();
314
+ ```
315
+
316
+ ---
317
+
318
+ ### Client API (`EffectGrpcClient`)
319
+
320
+ The client API provides tools for making gRPC calls from Effect programs.
321
+
322
+ #### Core Types
323
+
324
+ ##### `GrpcClientConfig<Service>`
325
+
326
+ Configuration for connecting to a gRPC service.
327
+
328
+ **Type Parameters:**
329
+ - `Service` - The fully-qualified service name (e.g., "com.example.v1.UserService")
330
+
331
+ **Properties:**
332
+ - `baseUrl: URL` - Base URL for gRPC requests (e.g., `new URL("http://localhost:8000")`)
333
+ - `binaryOptions?: Partial<BinaryReadOptions & BinaryWriteOptions>` - Protocol Buffer binary format options
334
+ - `acceptCompression?: Compression[]` - Accepted response compression algorithms (defaults to ["gzip", "br"])
335
+ - `sendCompression?: Compression` - Compression algorithm for request messages
336
+ - `compressMinBytes?: number` - Minimum message size for compression (defaults to 1024 bytes)
337
+ - `defaultTimeoutMs?: number` - Default timeout for all requests in milliseconds
338
+
339
+ **Example:**
340
+ ```typescript
341
+ const config = EffectGrpcClient.GrpcClientConfig({
342
+ baseUrl: new URL("https://api.example.com"),
343
+ defaultTimeoutMs: 5000,
344
+ acceptCompression: ["gzip", "br"],
345
+ sendCompression: "gzip",
346
+ compressMinBytes: 1024
347
+ });
348
+
349
+ // Create a config tag for dependency injection
350
+ const UserServiceConfigTag = EffectGrpcClient.GrpcClientConfig.makeTag(
351
+ "com.example.v1.UserService"
352
+ );
353
+
354
+ // Provide the config in a layer
355
+ const configLayer = Layer.succeed(UserServiceConfigTag, config);
356
+ ```
357
+
358
+ ##### `RequestMeta`
359
+
360
+ Metadata attached to individual gRPC requests.
361
+
362
+ **Properties:**
363
+ - `headers?: Headers` - HTTP headers to send with the request
364
+ - `contextValues?: ContextValues` - Connect-RPC context values (e.g., timeout overrides)
365
+
366
+ **Example:**
367
+ ```typescript
368
+ const meta: EffectGrpcClient.RequestMeta = {
369
+ headers: new Headers({
370
+ "Authorization": "Bearer token123",
371
+ "X-Request-ID": crypto.randomUUID()
372
+ }),
373
+ contextValues: {
374
+ timeout: 3000 // Override default timeout for this request
375
+ }
376
+ };
377
+
378
+ // Use with generated client
379
+ const response = yield* client.getUser({ userId: "123" }, meta);
380
+ ```
381
+
382
+ #### Factory Functions
383
+
384
+ ##### `liveGrpcClientRuntimeLayer()`
385
+
386
+ Creates the live implementation layer for `GrpcClientRuntime`.
387
+
388
+ **Returns:** `Layer.Layer<GrpcClientRuntime>`
389
+
390
+ **Example:**
391
+ ```typescript
392
+ const layer = Layer.empty.pipe(
393
+ Layer.provideMerge(EffectGrpcClient.liveGrpcClientRuntimeLayer())
394
+ );
395
+ ```
396
+
397
+ ##### `GrpcClientConfig(opts)`
398
+
399
+ Creates a client configuration object.
400
+
401
+ **Parameters:**
402
+ - `opts` - Configuration options (omit the `_Service` type parameter)
403
+
404
+ **Returns:** `GrpcClientConfig<Service>`
405
+
406
+ **Example:**
407
+ ```typescript
408
+ const config = EffectGrpcClient.GrpcClientConfig({
409
+ baseUrl: new URL("http://localhost:8000"),
410
+ defaultTimeoutMs: 5000
411
+ });
412
+ ```
413
+
414
+ ##### `GrpcClientConfig.makeTag(service)`
415
+
416
+ Creates a Context tag for service-specific configuration.
417
+
418
+ **Parameters:**
419
+ - `service` - Fully-qualified service name
420
+
421
+ **Returns:** `Context.Tag<GrpcClientConfig<Service>, GrpcClientConfig<Service>>`
422
+
423
+ **Example:**
424
+ ```typescript
425
+ const UserServiceConfigTag = EffectGrpcClient.GrpcClientConfig.makeTag(
426
+ "com.example.v1.UserService"
427
+ );
428
+ ```
429
+
430
+ ---
431
+
432
+ ### Generated Code API
433
+
434
+ The `protoc-gen-effect` plugin generates TypeScript code from `.proto` files with Effect integration. This section documents the structure of generated code.
435
+
436
+ #### Service Implementation (Server-side)
437
+
438
+ For each service in your `.proto` file, the generator creates:
439
+
440
+ ##### `{ServiceName}ProtoId`
441
+
442
+ Constant and type for the service identifier.
443
+
444
+ **Example:**
445
+ ```typescript
446
+ export const UserServiceProtoId = "com.example.v1.UserService" as const;
447
+ export type UserServiceProtoId = typeof UserServiceProtoId;
448
+ ```
449
+
450
+ ##### `{ServiceName}Service<Ctx>`
451
+
452
+ Interface defining the service implementation contract.
453
+
454
+ **Type Parameters:**
455
+ - `Ctx` - Context type available in method handlers
456
+
457
+ **Example:**
458
+ ```typescript
459
+ export interface UserServiceService<Ctx = any> {
460
+ getUser(
461
+ request: GetUserRequest,
462
+ ctx: Ctx
463
+ ): Effect.Effect<MessageInitShape<typeof GetUserResponseSchema>, GrpcException>;
464
+
465
+ listUsers(
466
+ request: ListUsersRequest,
467
+ ctx: Ctx
468
+ ): Effect.Effect<MessageInitShape<typeof ListUsersResponseSchema>, GrpcException>;
469
+ }
470
+ ```
471
+
472
+ ##### `{ServiceName}ServiceTag`
473
+
474
+ Context tag for the service. Can be used as-is (default context) or called as a function to create a typed tag.
475
+
476
+ **Usage:**
477
+ ```typescript
478
+ // Use default tag directly (when not using ctx parameter in implementation)
479
+ effectProto.UserServiceTag
480
+
481
+ // Create typed tag when you need to access ctx parameter
482
+ interface AppContext {
483
+ userId: string;
484
+ requestId: string;
485
+ }
486
+ const UserServiceAppCtxTag = effectProto.UserServiceTag<AppContext>("AppContext");
487
+
488
+ // With HandlerContext when you need access to request headers
489
+ import { HandlerContext } from "@connectrpc/connect";
490
+ const UserServiceHandlerCtxTag = effectProto.UserServiceTag<HandlerContext>("HandlerContext");
491
+ ```
492
+
493
+ ##### `{serviceName}ServiceLiveLayer(tag, service)`
494
+
495
+ Function that creates a layer from a service implementation.
496
+
497
+ **Parameters:**
498
+ - `tag` - Context tag for the service
499
+ - `service` - Implementation of the service interface
500
+
501
+ **Returns:** `Layer` providing the gRPC service
502
+
503
+ **Example:**
504
+ ```typescript
505
+ // If not using ctx, use default type and tag
506
+ const UserServiceLive: UserServiceService = {
507
+ getUser(request) {
508
+ return Effect.succeed({ user: { id: request.userId, name: "John" } });
509
+ },
510
+ listUsers(request) {
511
+ return Effect.succeed({ users: [] });
512
+ }
513
+ };
514
+
515
+ const userServiceLayer = userServiceLiveLayer(
516
+ effectProto.UserServiceTag,
517
+ UserServiceLive
518
+ );
519
+
520
+ // When you need to access ctx (e.g., HandlerContext for request headers)
521
+ import { HandlerContext } from "@connectrpc/connect";
522
+
523
+ const UserServiceWithCtx: UserServiceService<HandlerContext> = {
524
+ getUser(request, ctx) {
525
+ const authToken = ctx.requestHeader.get("authorization");
526
+ // ... use authToken in your logic
527
+ return Effect.succeed({ user: { id: request.userId, name: "John" } });
528
+ },
529
+ listUsers(request, ctx) {
530
+ return Effect.succeed({ users: [] });
531
+ }
532
+ };
533
+
534
+ const UserServiceHandlerCtxTag = effectProto.UserServiceTag<HandlerContext>("HandlerContext");
535
+ const userServiceLayerWithCtx = userServiceLiveLayer(
536
+ UserServiceHandlerCtxTag,
537
+ UserServiceWithCtx
538
+ );
539
+ ```
540
+
541
+ #### Client Implementation
542
+
543
+ For each service, the generator also creates client-side types:
544
+
545
+ ##### `{ServiceName}Client<Meta>`
546
+
547
+ Interface defining the client API.
548
+
549
+ **Type Parameters:**
550
+ - `Meta` - Type of metadata passed with each request
551
+
552
+ **Example:**
553
+ ```typescript
554
+ export interface UserServiceClient<Meta> {
555
+ getUser(
556
+ request: MessageInitShape<typeof GetUserRequestSchema>,
557
+ meta: Meta
558
+ ): Effect.Effect<GetUserResponse>;
559
+
560
+ listUsers(
561
+ request: MessageInitShape<typeof ListUsersRequestSchema>,
562
+ meta: Meta
563
+ ): Effect.Effect<ListUsersResponse>;
564
+ }
565
+ ```
566
+
567
+ ##### `{ServiceName}ClientTag`
568
+
569
+ Context tag for the client. Can be used as-is (default metadata) or called as a function to create a typed tag.
570
+
571
+ **Usage:**
572
+ ```typescript
573
+ // Use default tag directly (any metadata)
574
+ effectProto.UserServiceClientTag
575
+
576
+ // Create typed tag with custom metadata
577
+ interface AuthMeta {
578
+ authToken: string;
579
+ }
580
+ const UserServiceAuthClientTag = effectProto.UserServiceClientTag<AuthMeta>("AuthMeta");
581
+ ```
582
+
583
+ ##### `{serviceName}ClientLiveLayer(tag)` / `{serviceName}ClientLiveLayer(transformMeta, tag)`
584
+
585
+ Function that creates a client layer (two overloads).
586
+
587
+ **Overload 1: With metadata transformation**
588
+ ```typescript
589
+ {serviceName}ClientLiveLayer<Tag extends {ServiceName}ClientTag<Meta>, Meta>(
590
+ transformMeta: (meta: Meta) => EffectGrpcClient.RequestMeta,
591
+ tag: Tag
592
+ ): Layer.Layer<...>
593
+ ```
594
+
595
+ **Overload 2: Default metadata**
596
+ ```typescript
597
+ {serviceName}ClientLiveLayer<Tag extends {ServiceName}ClientTag>(
598
+ tag: Tag
599
+ ): Layer.Layer<...>
600
+ ```
601
+
602
+ **Example:**
603
+ ```typescript
604
+ // With custom metadata transformation
605
+ interface AuthMeta {
606
+ authToken: string;
607
+ }
608
+
609
+ const UserServiceAuthClientTag = effectProto.UserServiceClientTag<AuthMeta>("AuthMeta");
610
+ const userServiceAuthClientLayer = effectProto.userServiceClientLiveLayer(
611
+ (meta: AuthMeta) => ({
612
+ headers: new Headers({ "Authorization": `Bearer ${meta.authToken}` })
613
+ }),
614
+ UserServiceAuthClientTag
615
+ ).pipe(
616
+ Layer.provideMerge(
617
+ Layer.succeed(effectProto.UserServiceConfigTag, config)
618
+ )
619
+ );
620
+
621
+ // With default metadata
622
+ const userServiceClientLayer = effectProto.userServiceClientLiveLayer(
623
+ effectProto.UserServiceClientTag
624
+ ).pipe(
625
+ Layer.provideMerge(
626
+ Layer.succeed(effectProto.UserServiceConfigTag, config)
627
+ )
628
+ );
629
+ ```
630
+
631
+ ##### `{ServiceName}ConfigTag`
632
+
633
+ Pre-created config tag for the service.
634
+
635
+ **Example:**
636
+ ```typescript
637
+ export const UserServiceConfigTag =
638
+ EffectGrpcClient.GrpcClientConfig.makeTag(UserServiceProtoId);
639
+
640
+ // Use it to provide configuration
641
+ const configLayer = Layer.succeed(
642
+ UserServiceConfigTag,
643
+ EffectGrpcClient.GrpcClientConfig({ baseUrl: new URL("http://localhost:8000") })
644
+ );
645
+ ```
646
+
647
+ ## Advanced Usage
648
+
649
+ ### Error Handling with GrpcException
650
+
651
+ effect-grpc provides `GrpcException`, a typed error that extends Effect's `Data.TaggedError` for handling gRPC errors. All generated service methods return `Effect<Success, GrpcException>`.
652
+
653
+ ```typescript
654
+ import { Effect } from "effect";
655
+ import { GrpcException } from "@dr_nikson/effect-grpc";
656
+ import { Code } from "@connectrpc/connect";
657
+
658
+ import * as effectProto from "./generated/example/v1/user_effect.js";
659
+ import * as proto from "./generated/example/v1/user_pb.js";
660
+
661
+ // Implement the service with error handling (ctx not used, so use default)
662
+ const UserServiceLive: effectProto.UserServiceService = {
663
+ getUser(request: proto.GetUserRequest) {
664
+ return Effect.gen(function* () {
665
+ // Input validation with gRPC status codes
666
+ if (!request.userId) {
667
+ return yield* Effect.fail(
668
+ GrpcException.create(Code.InvalidArgument, "User ID is required")
669
+ );
670
+ }
671
+
672
+ // Convert unknown errors to GrpcException
673
+ const user = yield* Effect.tryPromise({
674
+ try: () => database.findUser(request.userId),
675
+ catch: (error) => GrpcException.from(Code.Internal, error)
676
+ });
677
+
678
+ if (!user) {
679
+ return yield* Effect.fail(
680
+ GrpcException.create(Code.NotFound, "User not found")
681
+ );
682
+ }
683
+
684
+ return { user };
685
+ });
686
+ }
687
+ };
688
+ ```
689
+
690
+ **GrpcException API:**
691
+ - `GrpcException.create(code, message, cause?)` - Create a new exception
692
+ - `GrpcException.from(code, cause)` - Convert any error to GrpcException
693
+ - `GrpcException.withDescription(error, desc)` - Add context description
694
+
695
+ For gRPC status codes and error handling best practices, see [Connect RPC Error Handling](https://connectrpc.com/docs/node/errors).
696
+
697
+ ### Dependency Injection
698
+
699
+ Leverage Effect's powerful dependency injection to compose your services with external dependencies:
700
+
701
+ ```typescript
702
+ import { Context, Effect, Layer } from "effect";
703
+ import { EffectGrpcServer } from "@dr_nikson/effect-grpc";
704
+ import * as effectProto from "./generated/user_effect.js";
705
+
706
+ interface User {
707
+ id: string;
708
+ name: string;
709
+ }
710
+
711
+ // Define a database service tag
712
+ class DatabaseService extends Context.Tag("DatabaseService")<
713
+ DatabaseService,
714
+ {
715
+ readonly getUser: (id: string) => Effect.Effect<User>;
716
+ readonly saveUser: (user: User) => Effect.Effect<void>;
717
+ }
718
+ >() {}
719
+
720
+ // Service implementation class with constructor
721
+ class UserServiceLive implements effectProto.UserServiceService {
722
+ constructor(private readonly db: Context.Tag.Service<typeof DatabaseService>) {}
723
+
724
+ getUser(request: effectProto.GetUserRequest) {
725
+ return this.db.getUser(request.userId).pipe(
726
+ Effect.map(user => ({ user }))
727
+ );
728
+ }
729
+
730
+ updateUser(request: effectProto.UpdateUserRequest) {
731
+ return this.db.saveUser(request.user).pipe(
732
+ Effect.map(() => ({ success: true }))
733
+ );
734
+ }
735
+ }
736
+
737
+ // Wire dependencies through constructor
738
+ const userServiceLayer = Layer.unwrapEffect(
739
+ Effect.gen(function* () {
740
+ const db = yield* DatabaseService;
741
+ const serviceImpl = new UserServiceLive(db);
742
+ return effectProto.userServiceLiveLayer(effectProto.UserServiceTag, serviceImpl);
743
+ })
744
+ );
745
+
746
+ // Create a mock database layer for testing
747
+ const mockDatabaseLayer = Layer.succeed(DatabaseService, {
748
+ getUser: (id) => Effect.succeed({ id, name: "Mock User" }),
749
+ saveUser: (_user) => Effect.succeed(void 0)
750
+ });
751
+
752
+ // Compose all layers together
753
+ const appLayer = Layer.empty.pipe(
754
+ Layer.provideMerge(mockDatabaseLayer),
755
+ Layer.provideMerge(userServiceLayer)
756
+ );
757
+
758
+ // Build and run your server with all dependencies
759
+ const program = Effect.gen(function* () {
760
+ const userService = yield* effectProto.UserServiceTag;
761
+
762
+ const server = EffectGrpcServer.GrpcServerBuilder()
763
+ .withService(userService)
764
+ .build();
765
+
766
+ return yield* server.run({ host: "localhost", port: 8000 });
767
+ }).pipe(
768
+ Effect.provide(appLayer)
769
+ );
770
+ ```
771
+
772
+ ### Request Metadata and Headers
773
+
774
+ Send custom headers and metadata with requests:
775
+
776
+ ```typescript
777
+ const client = yield* HelloClientTag;
778
+
779
+ const response = yield* client.sayHello(
780
+ { name: "World" },
781
+ {
782
+ headers: new Headers({
783
+ "Authorization": "Bearer your-token",
784
+ "X-Request-ID": "123456"
785
+ }),
786
+ contextValues: {
787
+ timeout: 5000 // 5 second timeout
788
+ }
789
+ }
790
+ );
791
+ ```
792
+
793
+
794
+ ## Development
795
+
796
+ ### Building from Source
797
+
798
+ ```bash
799
+ # Clone the repository
800
+ git clone https://github.com/dr_nikson/effect-grpc.git
801
+ cd effect-grpc
802
+
803
+ # Install dependencies
804
+ pnpm install
805
+
806
+ # Build all packages
807
+ pnpm -r run build
808
+
809
+ # Run type tests
810
+ pnpm -r run test:types
811
+ ```
812
+
813
+ ### Project Structure
814
+
815
+ ```
816
+ effect-grpc/
817
+ ├── packages/
818
+ │ ├── effect-grpc/ # Core library
819
+ │ │ ├── src/
820
+ │ │ │ ├── client.ts # Client implementation
821
+ │ │ │ ├── server.ts # Server implementation
822
+ │ │ │ └── index.ts # Public exports
823
+ │ │ └── bin/
824
+ │ │ └── protoc-gen-effect # Protocol Buffer plugin
825
+ │ └── example/ # Example implementation
826
+ │ ├── proto/ # Protocol Buffer definitions
827
+ │ ├── src/
828
+ │ │ ├── generated/ # Generated TypeScript code
829
+ │ │ ├── server.ts # Example server
830
+ │ │ └── client.ts # Example client
831
+ │ └── buf.gen.yaml # Buf configuration
832
+ └── README.md
833
+ ```
834
+
835
+ ## Roadmap
836
+
837
+ - [ ] Support for streaming RPCs (server-streaming, client-streaming, bidirectional)
838
+ - [ ] Interceptor/middleware support
839
+ - [ ] Built-in retry policies with Effect
840
+ - [ ] gRPC reflection support
841
+ - [ ] Browser/gRPC-Web support
842
+ - [ ] Performance optimizations
843
+ - [ ] More comprehensive examples
844
+
845
+ ## Contributing
846
+
847
+ Contributions are welcome! Please feel free to submit a Pull Request.
848
+
849
+ 1. Fork the repository
850
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
851
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
852
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
853
+ 5. Open a Pull Request
854
+
855
+ ## License
856
+
857
+ [Apache License Version 2.0](LICENSE)
858
+
859
+ ## Acknowledgments
860
+
861
+ - [Effect](https://effect.website/) - The core framework this library builds upon
862
+ - [Connect-RPC](https://connectrpc.com/) - The modern gRPC implementation
863
+ - [Buf](https://buf.build/) - Protocol Buffer tooling