@dr_nikson/effect-grpc 0.2.0-mvp-9971208edee70b39058f12d8c2a1fcfaeecd3b51 → 3.0.0-alpha.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 +928 -0
- package/dist/client.d.ts +38 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.internal.d.ts.map +1 -1
- package/dist/client.internal.js +36 -9
- package/dist/client.internal.js.map +1 -1
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -1
- package/dist/grpcException.d.ts +243 -0
- package/dist/grpcException.d.ts.map +1 -0
- package/dist/grpcException.internal.d.ts +28 -0
- package/dist/grpcException.internal.d.ts.map +1 -0
- package/dist/grpcException.internal.js +72 -0
- package/dist/grpcException.internal.js.map +1 -0
- package/dist/grpcException.js +218 -0
- package/dist/grpcException.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/protoRuntime.d.ts +5 -4
- package/dist/protoRuntime.d.ts.map +1 -1
- package/dist/protoRuntime.internal.d.ts +4 -3
- package/dist/protoRuntime.internal.d.ts.map +1 -1
- package/dist/protoRuntime.internal.js +16 -2
- package/dist/protoRuntime.internal.js.map +1 -1
- package/dist/protoRuntime.js +2 -0
- package/dist/protoRuntime.js.map +1 -1
- package/dist/protocGenPlugin.d.ts.map +1 -1
- package/dist/protocGenPlugin.js +6 -5
- package/dist/protocGenPlugin.js.map +1 -1
- package/dist/server.d.ts +8 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.internal.d.ts +5 -4
- package/dist/server.internal.d.ts.map +1 -1
- package/dist/server.internal.js +49 -12
- package/dist/server.internal.js.map +1 -1
- package/dist/server.js +4 -0
- package/dist/server.js.map +1 -1
- package/dist/typeUtils.js +1 -1
- package/dist/typeUtils.js.map +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,928 @@
|
|
|
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
|
+
// Create a tag for dependency injection
|
|
114
|
+
const HelloServiceTag = effectProto.HelloService.makeTag<HandlerContext>(
|
|
115
|
+
"HandlerContext"
|
|
116
|
+
);
|
|
117
|
+
type HelloServiceTag = Context.Tag.Identifier<typeof HelloServiceTag>;
|
|
118
|
+
|
|
119
|
+
// Implement the service
|
|
120
|
+
const HelloServiceLive: effectProto.HelloService<HandlerContext> = {
|
|
121
|
+
sayHello(request: proto.HelloRequest) {
|
|
122
|
+
return Effect.succeed({
|
|
123
|
+
message: `Hello, ${request.name}!`
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Create the service layer
|
|
129
|
+
const helloServiceLayer = effectProto.HelloService.liveLayer(HelloServiceLive)(
|
|
130
|
+
HelloServiceTag
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Build and run the gRPC server
|
|
134
|
+
const program = Effect.gen(function* () {
|
|
135
|
+
const helloService = yield* HelloServiceTag;
|
|
136
|
+
|
|
137
|
+
const server: EffectGrpcServer.GrpcServer<"HelloService"> =
|
|
138
|
+
EffectGrpcServer
|
|
139
|
+
.GrpcServerBuilder()
|
|
140
|
+
.withService(helloService)
|
|
141
|
+
.build();
|
|
142
|
+
|
|
143
|
+
return yield* server.run();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Provide dependencies and run
|
|
147
|
+
const layer = Layer.empty.pipe(
|
|
148
|
+
Layer.provideMerge(helloServiceLayer),
|
|
149
|
+
Layer.provideMerge(Logger.minimumLogLevel(LogLevel.Info))
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
NodeRuntime.runMain(Effect.provide(program, layer));
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 5. Implement the Client
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// src/client.ts
|
|
159
|
+
import { Effect, Layer, Logger, LogLevel } from "effect";
|
|
160
|
+
import { EffectGrpcClient } from "@dr_nikson/effect-grpc";
|
|
161
|
+
import { NodeRuntime } from "@effect/platform-node";
|
|
162
|
+
|
|
163
|
+
import * as effectProto from "./generated/example/v1/hello_effect.js";
|
|
164
|
+
|
|
165
|
+
// Create a client tag
|
|
166
|
+
const HelloServiceClientTag = effectProto.HelloServiceClient.makeTag<object>("{}");
|
|
167
|
+
type HelloServiceClientTag = typeof HelloServiceClientTag;
|
|
168
|
+
|
|
169
|
+
// Create the client layer with configuration
|
|
170
|
+
const helloClientLayer = effectProto.HelloServiceClient.liveLayer(
|
|
171
|
+
HelloServiceClientTag
|
|
172
|
+
).pipe(
|
|
173
|
+
Layer.provideMerge(
|
|
174
|
+
Layer.succeed(
|
|
175
|
+
effectProto.HelloServiceConfigTag,
|
|
176
|
+
EffectGrpcClient.GrpcClientConfig({
|
|
177
|
+
baseUrl: "http://localhost:8000"
|
|
178
|
+
})
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Use the client
|
|
184
|
+
const program = Effect.gen(function* () {
|
|
185
|
+
const client = yield* HelloServiceClientTag;
|
|
186
|
+
|
|
187
|
+
const response = yield* client.sayHello({
|
|
188
|
+
name: "World"
|
|
189
|
+
}, {});
|
|
190
|
+
|
|
191
|
+
yield* Effect.log(`Server responded: ${response.message}`);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Provide dependencies and run
|
|
195
|
+
const dependencies = Layer.empty.pipe(
|
|
196
|
+
Layer.provideMerge(helloClientLayer),
|
|
197
|
+
Layer.provideMerge(EffectGrpcClient.liveGrpcClientRuntimeLayer()),
|
|
198
|
+
Layer.provideMerge(Logger.minimumLogLevel(LogLevel.Info))
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
NodeRuntime.runMain(Effect.provide(program, dependencies));
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Complete Example
|
|
205
|
+
|
|
206
|
+
For a complete working example, see the [`packages/example`](packages/example) directory in this repository. It demonstrates:
|
|
207
|
+
|
|
208
|
+
- Protocol Buffer definition and code generation
|
|
209
|
+
- Server implementation with Effect
|
|
210
|
+
- Client implementation with Effect
|
|
211
|
+
- Proper project structure and configuration
|
|
212
|
+
|
|
213
|
+
To run the example:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
# Clone the repository
|
|
217
|
+
git clone https://github.com/dr_nikson/effect-grpc.git
|
|
218
|
+
cd effect-grpc
|
|
219
|
+
|
|
220
|
+
# Install dependencies
|
|
221
|
+
pnpm install
|
|
222
|
+
|
|
223
|
+
# Build the library
|
|
224
|
+
pnpm -r run build
|
|
225
|
+
|
|
226
|
+
# In one terminal, start the server
|
|
227
|
+
cd packages/example
|
|
228
|
+
node dist/server.js
|
|
229
|
+
|
|
230
|
+
# In another terminal, run the client
|
|
231
|
+
node dist/client.js
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## API Reference
|
|
235
|
+
|
|
236
|
+
This section documents the public API exported by `@dr_nikson/effect-grpc`. The library excludes internal runtime APIs from public documentation.
|
|
237
|
+
|
|
238
|
+
### Server API (`EffectGrpcServer`)
|
|
239
|
+
|
|
240
|
+
The server API provides tools for building and running gRPC servers within Effect programs.
|
|
241
|
+
|
|
242
|
+
#### Core Types
|
|
243
|
+
|
|
244
|
+
##### `GrpcServer<Services>`
|
|
245
|
+
|
|
246
|
+
Represents a running gRPC server instance.
|
|
247
|
+
|
|
248
|
+
**Type Parameters:**
|
|
249
|
+
- `Services` - Union type of all service tags registered with this server
|
|
250
|
+
|
|
251
|
+
**Methods:**
|
|
252
|
+
- `run(): Effect.Effect<never, never, Scope.Scope>` - Starts the server and returns an Effect that requires a Scope
|
|
253
|
+
|
|
254
|
+
**Example:**
|
|
255
|
+
```typescript
|
|
256
|
+
const server: EffectGrpcServer.GrpcServer<"UserService" | "ProductService"> =
|
|
257
|
+
EffectGrpcServer.GrpcServerBuilder()
|
|
258
|
+
.withService(userService)
|
|
259
|
+
.withService(productService)
|
|
260
|
+
.build();
|
|
261
|
+
|
|
262
|
+
// Run with proper resource management
|
|
263
|
+
const program = Effect.scoped(server.run());
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
##### `GrpcServerBuilder<Ctx, Services>`
|
|
267
|
+
|
|
268
|
+
Fluent builder interface for constructing gRPC servers.
|
|
269
|
+
|
|
270
|
+
**Type Parameters:**
|
|
271
|
+
- `Ctx` - Context type available to service handlers (defaults to `HandlerContext`)
|
|
272
|
+
- `Services` - Union of currently registered service tags
|
|
273
|
+
|
|
274
|
+
**Methods:**
|
|
275
|
+
- `withContextTransformer<Ctx1>(f: (ctx: Ctx) => Effect.Effect<Ctx1>): GrpcServerBuilder<Ctx1, never>` - Transform the handler context (must be called before adding services)
|
|
276
|
+
- `withService<S>(service: S): GrpcServerBuilder<Ctx, Services | Tag<S>>` - Add a service (enforces unique tags)
|
|
277
|
+
- `build(): GrpcServer<Services>` - Build the server (requires at least one service)
|
|
278
|
+
|
|
279
|
+
**Example:**
|
|
280
|
+
```typescript
|
|
281
|
+
// Simple server with HandlerContext
|
|
282
|
+
const server = EffectGrpcServer.GrpcServerBuilder()
|
|
283
|
+
.withService(myService)
|
|
284
|
+
.build();
|
|
285
|
+
|
|
286
|
+
// Server with custom context
|
|
287
|
+
interface AppContext {
|
|
288
|
+
userId: string;
|
|
289
|
+
requestId: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const serverWithCtx = EffectGrpcServer.GrpcServerBuilder()
|
|
293
|
+
.withContextTransformer((handlerCtx: HandlerContext) =>
|
|
294
|
+
Effect.succeed({
|
|
295
|
+
userId: handlerCtx.requestHeader.get("user-id") ?? "anonymous",
|
|
296
|
+
requestId: crypto.randomUUID()
|
|
297
|
+
})
|
|
298
|
+
)
|
|
299
|
+
.withService(myService)
|
|
300
|
+
.build();
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
##### `GrpcService<Tag, Proto, Ctx>`
|
|
304
|
+
|
|
305
|
+
Represents a gRPC service implementation bound to a specific Protocol Buffer definition.
|
|
306
|
+
|
|
307
|
+
**Type Parameters:**
|
|
308
|
+
- `Tag` - Unique identifier for this service (typically the fully-qualified protobuf name)
|
|
309
|
+
- `Proto` - The Protocol Buffer service definition from generated code
|
|
310
|
+
- `Ctx` - Context type available to service method handlers
|
|
311
|
+
|
|
312
|
+
**Note:** Instances are typically created by the `protoc-gen-effect` code generator.
|
|
313
|
+
|
|
314
|
+
**Example:**
|
|
315
|
+
```typescript
|
|
316
|
+
// Generated by protoc-gen-effect
|
|
317
|
+
const userService: EffectGrpcServer.GrpcService<
|
|
318
|
+
"com.example.UserService",
|
|
319
|
+
typeof UserServiceProto,
|
|
320
|
+
HandlerContext
|
|
321
|
+
> = EffectGrpcServer.GrpcService("com.example.UserService", UserServiceProto)(
|
|
322
|
+
(exec) => ({
|
|
323
|
+
getUser: (req, ctx) => exec.unary(req, ctx, (req, ctx) =>
|
|
324
|
+
Effect.succeed({ user: { id: req.userId, name: "John" } })
|
|
325
|
+
)
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
#### Factory Functions
|
|
331
|
+
|
|
332
|
+
##### `GrpcServerBuilder()`
|
|
333
|
+
|
|
334
|
+
Creates a new server builder instance with default `HandlerContext`.
|
|
335
|
+
|
|
336
|
+
**Returns:** `GrpcServerBuilder<HandlerContext, never>`
|
|
337
|
+
|
|
338
|
+
**Example:**
|
|
339
|
+
```typescript
|
|
340
|
+
const builder = EffectGrpcServer.GrpcServerBuilder();
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
##### `GrpcService(tag, definition)`
|
|
344
|
+
|
|
345
|
+
Creates a GrpcService factory (typically used by code generators).
|
|
346
|
+
|
|
347
|
+
**Parameters:**
|
|
348
|
+
- `tag` - Unique service identifier
|
|
349
|
+
- `definition` - Protocol Buffer service definition
|
|
350
|
+
|
|
351
|
+
**Returns:** Function that accepts implementation and returns `GrpcService`
|
|
352
|
+
|
|
353
|
+
**Example:**
|
|
354
|
+
```typescript
|
|
355
|
+
// This is typically generated, not written manually
|
|
356
|
+
const createService = EffectGrpcServer.GrpcService(
|
|
357
|
+
"com.example.MyService",
|
|
358
|
+
MyServiceProto
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const service = createService<HandlerContext>((exec) => ({
|
|
362
|
+
myMethod: (req, ctx) => exec.unary(req, ctx, (req, ctx) =>
|
|
363
|
+
Effect.succeed({ result: "success" })
|
|
364
|
+
)
|
|
365
|
+
}));
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
### Client API (`EffectGrpcClient`)
|
|
371
|
+
|
|
372
|
+
The client API provides tools for making gRPC calls from Effect programs.
|
|
373
|
+
|
|
374
|
+
#### Core Types
|
|
375
|
+
|
|
376
|
+
##### `GrpcClientRuntime`
|
|
377
|
+
|
|
378
|
+
The runtime service that creates executors for invoking gRPC methods.
|
|
379
|
+
|
|
380
|
+
**Methods:**
|
|
381
|
+
- `makeExecutor<Shape>(serviceDefinition, methodNames, config): Effect.Effect<ClientExecutor<Shape>>` - Creates an executor for specified service methods
|
|
382
|
+
|
|
383
|
+
**Note:** This is primarily used by generated client code, not called directly by users.
|
|
384
|
+
|
|
385
|
+
**Example:**
|
|
386
|
+
```typescript
|
|
387
|
+
const program = Effect.gen(function* () {
|
|
388
|
+
const runtime = yield* EffectGrpcClient.GrpcClientRuntime;
|
|
389
|
+
const config = yield* MyServiceConfigTag;
|
|
390
|
+
|
|
391
|
+
const executor = yield* runtime.makeExecutor(
|
|
392
|
+
MyServiceProto,
|
|
393
|
+
["getUser", "listUsers"],
|
|
394
|
+
config
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
return executor;
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
##### `GrpcClientConfig<Service>`
|
|
402
|
+
|
|
403
|
+
Configuration for connecting to a gRPC service.
|
|
404
|
+
|
|
405
|
+
**Type Parameters:**
|
|
406
|
+
- `Service` - The fully-qualified service name (e.g., "com.example.v1.UserService")
|
|
407
|
+
|
|
408
|
+
**Properties:**
|
|
409
|
+
- `baseUrl: string` - Base URL for gRPC requests (e.g., "http://localhost:8000")
|
|
410
|
+
- `binaryOptions?: Partial<BinaryReadOptions & BinaryWriteOptions>` - Protocol Buffer binary format options
|
|
411
|
+
- `acceptCompression?: Compression[]` - Accepted response compression algorithms (defaults to ["gzip", "br"])
|
|
412
|
+
- `sendCompression?: Compression` - Compression algorithm for request messages
|
|
413
|
+
- `compressMinBytes?: number` - Minimum message size for compression (defaults to 1024 bytes)
|
|
414
|
+
- `defaultTimeoutMs?: number` - Default timeout for all requests in milliseconds
|
|
415
|
+
|
|
416
|
+
**Example:**
|
|
417
|
+
```typescript
|
|
418
|
+
const config = EffectGrpcClient.GrpcClientConfig({
|
|
419
|
+
baseUrl: "https://api.example.com",
|
|
420
|
+
defaultTimeoutMs: 5000,
|
|
421
|
+
acceptCompression: ["gzip", "br"],
|
|
422
|
+
sendCompression: "gzip",
|
|
423
|
+
compressMinBytes: 1024
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Create a config tag for dependency injection
|
|
427
|
+
const UserServiceConfigTag = EffectGrpcClient.GrpcClientConfig.makeTag(
|
|
428
|
+
"com.example.v1.UserService"
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// Provide the config in a layer
|
|
432
|
+
const configLayer = Layer.succeed(UserServiceConfigTag, config);
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
##### `RequestMeta`
|
|
436
|
+
|
|
437
|
+
Metadata attached to individual gRPC requests.
|
|
438
|
+
|
|
439
|
+
**Properties:**
|
|
440
|
+
- `headers?: Headers` - HTTP headers to send with the request
|
|
441
|
+
- `contextValues?: ContextValues` - Connect-RPC context values (e.g., timeout overrides)
|
|
442
|
+
|
|
443
|
+
**Example:**
|
|
444
|
+
```typescript
|
|
445
|
+
const meta: EffectGrpcClient.RequestMeta = {
|
|
446
|
+
headers: new Headers({
|
|
447
|
+
"Authorization": "Bearer token123",
|
|
448
|
+
"X-Request-ID": crypto.randomUUID()
|
|
449
|
+
}),
|
|
450
|
+
contextValues: {
|
|
451
|
+
timeout: 3000 // Override default timeout for this request
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// Use with generated client
|
|
456
|
+
const response = yield* client.getUser({ userId: "123" }, meta);
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
#### Context Tags
|
|
460
|
+
|
|
461
|
+
##### `GrpcClientRuntime`
|
|
462
|
+
|
|
463
|
+
Tag for accessing the gRPC client runtime service.
|
|
464
|
+
|
|
465
|
+
**Usage:**
|
|
466
|
+
```typescript
|
|
467
|
+
const program = Effect.gen(function* () {
|
|
468
|
+
const runtime = yield* EffectGrpcClient.GrpcClientRuntime;
|
|
469
|
+
// runtime is now available
|
|
470
|
+
});
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
#### Factory Functions
|
|
474
|
+
|
|
475
|
+
##### `liveGrpcClientRuntimeLayer()`
|
|
476
|
+
|
|
477
|
+
Creates the live implementation layer for `GrpcClientRuntime`.
|
|
478
|
+
|
|
479
|
+
**Returns:** `Layer.Layer<GrpcClientRuntime>`
|
|
480
|
+
|
|
481
|
+
**Example:**
|
|
482
|
+
```typescript
|
|
483
|
+
const layer = Layer.empty.pipe(
|
|
484
|
+
Layer.provideMerge(EffectGrpcClient.liveGrpcClientRuntimeLayer())
|
|
485
|
+
);
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
##### `GrpcClientConfig(opts)`
|
|
489
|
+
|
|
490
|
+
Creates a client configuration object.
|
|
491
|
+
|
|
492
|
+
**Parameters:**
|
|
493
|
+
- `opts` - Configuration options (omit the `_Service` type parameter)
|
|
494
|
+
|
|
495
|
+
**Returns:** `GrpcClientConfig<Service>`
|
|
496
|
+
|
|
497
|
+
**Example:**
|
|
498
|
+
```typescript
|
|
499
|
+
const config = EffectGrpcClient.GrpcClientConfig({
|
|
500
|
+
baseUrl: "http://localhost:8000",
|
|
501
|
+
defaultTimeoutMs: 5000
|
|
502
|
+
});
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
##### `GrpcClientConfig.makeTag(service)`
|
|
506
|
+
|
|
507
|
+
Creates a Context tag for service-specific configuration.
|
|
508
|
+
|
|
509
|
+
**Parameters:**
|
|
510
|
+
- `service` - Fully-qualified service name
|
|
511
|
+
|
|
512
|
+
**Returns:** `Context.Tag<GrpcClientConfig<Service>, GrpcClientConfig<Service>>`
|
|
513
|
+
|
|
514
|
+
**Example:**
|
|
515
|
+
```typescript
|
|
516
|
+
const UserServiceConfigTag = EffectGrpcClient.GrpcClientConfig.makeTag(
|
|
517
|
+
"com.example.v1.UserService"
|
|
518
|
+
);
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
### Generated Code API
|
|
524
|
+
|
|
525
|
+
The `protoc-gen-effect` plugin generates TypeScript code from `.proto` files with Effect integration. This section documents the structure of generated code.
|
|
526
|
+
|
|
527
|
+
#### Service Implementation (Server-side)
|
|
528
|
+
|
|
529
|
+
For each service in your `.proto` file, the generator creates:
|
|
530
|
+
|
|
531
|
+
##### `{ServiceName}Id`
|
|
532
|
+
|
|
533
|
+
Constant and type for the service identifier.
|
|
534
|
+
|
|
535
|
+
**Example:**
|
|
536
|
+
```typescript
|
|
537
|
+
export const UserServiceId = "com.example.v1.UserService" as const;
|
|
538
|
+
export type UserServiceId = typeof UserServiceId;
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
##### `{ServiceName}Service<Ctx>`
|
|
542
|
+
|
|
543
|
+
Interface defining the service implementation contract.
|
|
544
|
+
|
|
545
|
+
**Type Parameters:**
|
|
546
|
+
- `Ctx` - Context type available in method handlers
|
|
547
|
+
|
|
548
|
+
**Example:**
|
|
549
|
+
```typescript
|
|
550
|
+
export interface UserService<Ctx> {
|
|
551
|
+
getUser(
|
|
552
|
+
request: GetUserRequest,
|
|
553
|
+
ctx: Ctx
|
|
554
|
+
): Effect.Effect<MessageInitShape<typeof GetUserResponseSchema>>;
|
|
555
|
+
|
|
556
|
+
listUsers(
|
|
557
|
+
request: ListUsersRequest,
|
|
558
|
+
ctx: Ctx
|
|
559
|
+
): Effect.Effect<MessageInitShape<typeof ListUsersResponseSchema>>;
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
##### `{ServiceName}Service.makeTag(ctxKey)`
|
|
564
|
+
|
|
565
|
+
Creates a Context tag for the service.
|
|
566
|
+
|
|
567
|
+
**Parameters:**
|
|
568
|
+
- `ctxKey` - String identifier for the context type (e.g., "HandlerContext")
|
|
569
|
+
|
|
570
|
+
**Returns:** `Context.Tag<{ServiceName}GrpcService<Ctx>, {ServiceName}GrpcService<Ctx>>`
|
|
571
|
+
|
|
572
|
+
**Example:**
|
|
573
|
+
```typescript
|
|
574
|
+
const UserServiceTag = UserService.makeTag<HandlerContext>("HandlerContext");
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
##### `{ServiceName}Service.liveLayer(impl)`
|
|
578
|
+
|
|
579
|
+
Creates a layer from a service implementation.
|
|
580
|
+
|
|
581
|
+
**Parameters:**
|
|
582
|
+
- `impl` - Implementation of the service interface
|
|
583
|
+
|
|
584
|
+
**Returns:** Function accepting a tag and returning a `Layer`
|
|
585
|
+
|
|
586
|
+
**Example:**
|
|
587
|
+
```typescript
|
|
588
|
+
const UserServiceLive: UserService<HandlerContext> = {
|
|
589
|
+
getUser(request) {
|
|
590
|
+
return Effect.succeed({ user: { id: request.userId, name: "John" } });
|
|
591
|
+
},
|
|
592
|
+
listUsers(request) {
|
|
593
|
+
return Effect.succeed({ users: [] });
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const UserServiceTag = UserService.makeTag<HandlerContext>("HandlerContext");
|
|
598
|
+
const layer = UserService.liveLayer(UserServiceLive)(UserServiceTag);
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
##### `{ServiceName}GrpcService<Ctx>`
|
|
602
|
+
|
|
603
|
+
Type alias for the complete gRPC service (used with server builder).
|
|
604
|
+
|
|
605
|
+
**Example:**
|
|
606
|
+
```typescript
|
|
607
|
+
type UserServiceGrpc = UserServiceGrpcService<HandlerContext>;
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
#### Client Implementation
|
|
611
|
+
|
|
612
|
+
For each service, the generator also creates client-side types:
|
|
613
|
+
|
|
614
|
+
##### `{ServiceName}Client<Meta>`
|
|
615
|
+
|
|
616
|
+
Interface defining the client API.
|
|
617
|
+
|
|
618
|
+
**Type Parameters:**
|
|
619
|
+
- `Meta` - Type of metadata passed with each request
|
|
620
|
+
|
|
621
|
+
**Example:**
|
|
622
|
+
```typescript
|
|
623
|
+
export interface UserServiceClient<Meta> {
|
|
624
|
+
getUser(
|
|
625
|
+
request: MessageInitShape<typeof GetUserRequestSchema>,
|
|
626
|
+
meta: Meta
|
|
627
|
+
): Effect.Effect<GetUserResponse>;
|
|
628
|
+
|
|
629
|
+
listUsers(
|
|
630
|
+
request: MessageInitShape<typeof ListUsersRequestSchema>,
|
|
631
|
+
meta: Meta
|
|
632
|
+
): Effect.Effect<ListUsersResponse>;
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
##### `{ServiceName}Client.makeTag(metaKey)`
|
|
637
|
+
|
|
638
|
+
Creates a Context tag for the client.
|
|
639
|
+
|
|
640
|
+
**Parameters:**
|
|
641
|
+
- `metaKey` - String identifier for the metadata type
|
|
642
|
+
|
|
643
|
+
**Returns:** `Context.Tag<{ServiceName}Client<Meta>, {ServiceName}Client<Meta>>`
|
|
644
|
+
|
|
645
|
+
**Example:**
|
|
646
|
+
```typescript
|
|
647
|
+
const UserServiceClientTag = UserServiceClient.makeTag<object>("{}");
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
##### `{ServiceName}Client.liveLayer(transformMeta, tag)` / `{ServiceName}Client.liveLayer(tag)`
|
|
651
|
+
|
|
652
|
+
Creates a layer for the client (two overloads).
|
|
653
|
+
|
|
654
|
+
**Overload 1: With metadata transformation**
|
|
655
|
+
```typescript
|
|
656
|
+
liveLayer<Tag extends {ServiceName}ClientTag<Meta>, Meta>(
|
|
657
|
+
transformMeta: (meta: Meta) => EffectGrpcClient.RequestMeta,
|
|
658
|
+
tag: Tag
|
|
659
|
+
): Layer.Layer<...>
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
**Overload 2: Default metadata (empty object)**
|
|
663
|
+
```typescript
|
|
664
|
+
liveLayer<Tag extends {ServiceName}ClientTag<object>>(
|
|
665
|
+
tag: Tag
|
|
666
|
+
): Layer.Layer<...>
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
**Example:**
|
|
670
|
+
```typescript
|
|
671
|
+
// With custom metadata transformation
|
|
672
|
+
interface MyMeta {
|
|
673
|
+
authToken: string;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const clientLayerWithMeta = UserServiceClient.liveLayer(
|
|
677
|
+
(meta: MyMeta) => ({
|
|
678
|
+
headers: new Headers({ "Authorization": `Bearer ${meta.authToken}` })
|
|
679
|
+
}),
|
|
680
|
+
UserServiceClientTag
|
|
681
|
+
).pipe(
|
|
682
|
+
Layer.provideMerge(
|
|
683
|
+
Layer.succeed(UserServiceConfigTag, config)
|
|
684
|
+
)
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
// With default empty metadata
|
|
688
|
+
const clientLayer = UserServiceClient.liveLayer(
|
|
689
|
+
UserServiceClientTag
|
|
690
|
+
).pipe(
|
|
691
|
+
Layer.provideMerge(
|
|
692
|
+
Layer.succeed(UserServiceConfigTag, config)
|
|
693
|
+
)
|
|
694
|
+
);
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
##### `{ServiceName}ConfigTag`
|
|
698
|
+
|
|
699
|
+
Pre-created config tag for the service.
|
|
700
|
+
|
|
701
|
+
**Example:**
|
|
702
|
+
```typescript
|
|
703
|
+
export const UserServiceConfigTag: UserServiceConfigTag =
|
|
704
|
+
EffectGrpcClient.GrpcClientConfig.makeTag(UserServiceId);
|
|
705
|
+
|
|
706
|
+
// Use it to provide configuration
|
|
707
|
+
const configLayer = Layer.succeed(
|
|
708
|
+
UserServiceConfigTag,
|
|
709
|
+
EffectGrpcClient.GrpcClientConfig({ baseUrl: "http://localhost:8000" })
|
|
710
|
+
);
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
## Advanced Usage
|
|
714
|
+
|
|
715
|
+
### Error Handling with GrpcException
|
|
716
|
+
|
|
717
|
+
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>`.
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
import { Effect } from "effect";
|
|
721
|
+
import { HandlerContext } from "@connectrpc/connect";
|
|
722
|
+
import { GrpcException } from "@dr_nikson/effect-grpc";
|
|
723
|
+
import { Code } from "@connectrpc/connect";
|
|
724
|
+
|
|
725
|
+
import * as effectProto from "./generated/example/v1/user_effect.js";
|
|
726
|
+
import * as proto from "./generated/example/v1/user_pb.js";
|
|
727
|
+
|
|
728
|
+
// Implement the service with error handling
|
|
729
|
+
const UserServiceLive: effectProto.UserService<HandlerContext> = {
|
|
730
|
+
getUser(request: proto.GetUserRequest) {
|
|
731
|
+
return Effect.gen(function* () {
|
|
732
|
+
// Input validation with gRPC status codes
|
|
733
|
+
if (!request.userId) {
|
|
734
|
+
return yield* Effect.fail(
|
|
735
|
+
GrpcException.create(Code.InvalidArgument, "User ID is required")
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Convert unknown errors to GrpcException
|
|
740
|
+
const user = yield* Effect.tryPromise({
|
|
741
|
+
try: () => database.findUser(request.userId),
|
|
742
|
+
catch: (error) => GrpcException.from(Code.Internal, error)
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
if (!user) {
|
|
746
|
+
return yield* Effect.fail(
|
|
747
|
+
GrpcException.create(Code.NotFound, "User not found")
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return { user };
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
**GrpcException API:**
|
|
758
|
+
- `GrpcException.create(code, message, cause?)` - Create a new exception
|
|
759
|
+
- `GrpcException.from(code, cause)` - Convert any error to GrpcException
|
|
760
|
+
- `GrpcException.withDescription(error, desc)` - Add context description
|
|
761
|
+
|
|
762
|
+
For gRPC status codes and error handling best practices, see [Connect RPC Error Handling](https://connectrpc.com/docs/node/errors).
|
|
763
|
+
|
|
764
|
+
### Dependency Injection
|
|
765
|
+
|
|
766
|
+
Leverage Effect's powerful dependency injection to compose your services with external dependencies:
|
|
767
|
+
|
|
768
|
+
```typescript
|
|
769
|
+
import { Context, Effect, Layer } from "effect";
|
|
770
|
+
import { HandlerContext } from "@connectrpc/connect";
|
|
771
|
+
import * as effectProto from "./generated/user_effect.js";
|
|
772
|
+
|
|
773
|
+
// Define a database service tag
|
|
774
|
+
class DatabaseService extends Context.Tag("DatabaseService")<
|
|
775
|
+
DatabaseService,
|
|
776
|
+
{
|
|
777
|
+
readonly getUser: (id: string) => Effect.Effect<User>;
|
|
778
|
+
readonly saveUser: (user: User) => Effect.Effect<void>;
|
|
779
|
+
}
|
|
780
|
+
>() {}
|
|
781
|
+
|
|
782
|
+
// Implement your gRPC service with database dependency
|
|
783
|
+
const UserServiceLive: effectProto.UserService<HandlerContext> = {
|
|
784
|
+
getUser(request) {
|
|
785
|
+
return Effect.gen(function* () {
|
|
786
|
+
// Access the database service from context
|
|
787
|
+
const db = yield* DatabaseService;
|
|
788
|
+
|
|
789
|
+
// Use it in your business logic
|
|
790
|
+
const user = yield* db.getUser(request.userId);
|
|
791
|
+
|
|
792
|
+
return { user };
|
|
793
|
+
});
|
|
794
|
+
},
|
|
795
|
+
|
|
796
|
+
updateUser(request) {
|
|
797
|
+
return Effect.gen(function* () {
|
|
798
|
+
const db = yield* DatabaseService;
|
|
799
|
+
|
|
800
|
+
yield* db.saveUser(request.user);
|
|
801
|
+
|
|
802
|
+
return { success: true };
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
// Create service tag and layer
|
|
808
|
+
const UserServiceTag = effectProto.UserService.makeTag<HandlerContext>("HandlerContext");
|
|
809
|
+
const userServiceLayer = effectProto.UserService.liveLayer(UserServiceLive)(UserServiceTag);
|
|
810
|
+
|
|
811
|
+
// Create a mock database layer for testing
|
|
812
|
+
const mockDatabaseLayer = Layer.succeed(DatabaseService, {
|
|
813
|
+
getUser: (id) => Effect.succeed({ id, name: "Mock User" }),
|
|
814
|
+
saveUser: (_user) => Effect.succeed(void 0)
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
// Compose all layers together
|
|
818
|
+
const appLayer = Layer.empty.pipe(
|
|
819
|
+
Layer.provideMerge(mockDatabaseLayer),
|
|
820
|
+
Layer.provideMerge(userServiceLayer)
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
// Build and run your server with all dependencies
|
|
824
|
+
const program = Effect.gen(function* () {
|
|
825
|
+
const userService = yield* UserServiceTag;
|
|
826
|
+
|
|
827
|
+
const server = EffectGrpcServer.GrpcServerBuilder()
|
|
828
|
+
.withService(userService)
|
|
829
|
+
.build();
|
|
830
|
+
|
|
831
|
+
return yield* server.run();
|
|
832
|
+
}).pipe(
|
|
833
|
+
Effect.provide(appLayer)
|
|
834
|
+
);
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
### Request Metadata and Headers
|
|
838
|
+
|
|
839
|
+
Send custom headers and metadata with requests:
|
|
840
|
+
|
|
841
|
+
```typescript
|
|
842
|
+
const client = yield* HelloClientTag;
|
|
843
|
+
|
|
844
|
+
const response = yield* client.sayHello(
|
|
845
|
+
{ name: "World" },
|
|
846
|
+
{
|
|
847
|
+
headers: new Headers({
|
|
848
|
+
"Authorization": "Bearer your-token",
|
|
849
|
+
"X-Request-ID": "123456"
|
|
850
|
+
}),
|
|
851
|
+
contextValues: {
|
|
852
|
+
timeout: 5000 // 5 second timeout
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
);
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
## Development
|
|
860
|
+
|
|
861
|
+
### Building from Source
|
|
862
|
+
|
|
863
|
+
```bash
|
|
864
|
+
# Clone the repository
|
|
865
|
+
git clone https://github.com/dr_nikson/effect-grpc.git
|
|
866
|
+
cd effect-grpc
|
|
867
|
+
|
|
868
|
+
# Install dependencies
|
|
869
|
+
pnpm install
|
|
870
|
+
|
|
871
|
+
# Build all packages
|
|
872
|
+
pnpm -r run build
|
|
873
|
+
|
|
874
|
+
# Run type tests
|
|
875
|
+
pnpm -r run test:types
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
### Project Structure
|
|
879
|
+
|
|
880
|
+
```
|
|
881
|
+
effect-grpc/
|
|
882
|
+
├── packages/
|
|
883
|
+
│ ├── effect-grpc/ # Core library
|
|
884
|
+
│ │ ├── src/
|
|
885
|
+
│ │ │ ├── client.ts # Client implementation
|
|
886
|
+
│ │ │ ├── server.ts # Server implementation
|
|
887
|
+
│ │ │ └── index.ts # Public exports
|
|
888
|
+
│ │ └── bin/
|
|
889
|
+
│ │ └── protoc-gen-effect # Protocol Buffer plugin
|
|
890
|
+
│ └── example/ # Example implementation
|
|
891
|
+
│ ├── proto/ # Protocol Buffer definitions
|
|
892
|
+
│ ├── src/
|
|
893
|
+
│ │ ├── generated/ # Generated TypeScript code
|
|
894
|
+
│ │ ├── server.ts # Example server
|
|
895
|
+
│ │ └── client.ts # Example client
|
|
896
|
+
│ └── buf.gen.yaml # Buf configuration
|
|
897
|
+
└── README.md
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
## Roadmap
|
|
901
|
+
|
|
902
|
+
- [ ] Support for streaming RPCs (server-streaming, client-streaming, bidirectional)
|
|
903
|
+
- [ ] Interceptor/middleware support
|
|
904
|
+
- [ ] Built-in retry policies with Effect
|
|
905
|
+
- [ ] gRPC reflection support
|
|
906
|
+
- [ ] Browser/gRPC-Web support
|
|
907
|
+
- [ ] Performance optimizations
|
|
908
|
+
- [ ] More comprehensive examples
|
|
909
|
+
|
|
910
|
+
## Contributing
|
|
911
|
+
|
|
912
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
913
|
+
|
|
914
|
+
1. Fork the repository
|
|
915
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
916
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
917
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
918
|
+
5. Open a Pull Request
|
|
919
|
+
|
|
920
|
+
## License
|
|
921
|
+
|
|
922
|
+
[Apache License Version 2.0](LICENSE)
|
|
923
|
+
|
|
924
|
+
## Acknowledgments
|
|
925
|
+
|
|
926
|
+
- [Effect](https://effect.website/) - The core framework this library builds upon
|
|
927
|
+
- [Connect-RPC](https://connectrpc.com/) - The modern gRPC implementation
|
|
928
|
+
- [Buf](https://buf.build/) - Protocol Buffer tooling
|