@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.
- package/README.md +863 -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 +7 -6
- package/dist/protoRuntime.d.ts.map +1 -1
- package/dist/protoRuntime.internal.d.ts +7 -6
- package/dist/protoRuntime.internal.d.ts.map +1 -1
- package/dist/protoRuntime.internal.js +20 -6
- 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 +360 -147
- package/dist/protocGenPlugin.js.map +1 -1
- package/dist/server.d.ts +10 -6
- package/dist/server.d.ts.map +1 -1
- package/dist/server.internal.d.ts +6 -5
- package/dist/server.internal.d.ts.map +1 -1
- package/dist/server.internal.js +51 -14
- package/dist/server.internal.js.map +1 -1
- package/dist/server.js +5 -1
- 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,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
|