# gRPCClient.jl

## Overview

gRPCClient.jl is a production-grade Julia gRPC client built on libcurl for HTTP/2 transport. It integrates with ProtoBuf.jl for code generation.

- **Version**: 1.0.1
- **Requires**: Julia >= 1.10 (streaming requires Julia >= 1.12)
- **Dependencies**: LibCURL, ProtoBuf ~1.2.1, PrecompileTools, FileWatching

## Code Generation

### Setup

```julia
using ProtoBuf
using gRPCClient  # must come before protojl — __init__ registers the service codegen hook

protojl("path/to/service.proto", "proto/search/root", "output/dir")
```

`using gRPCClient` is sufficient; `grpc_register_service_codegen()` is called automatically in `__init__`. The explicit call in `test/protoc.sh` is redundant but harmless.

### protojl Arguments

```julia
protojl(proto_file, proto_search_root, output_dir)
```

- `proto_file`: path to the `.proto` file to compile (absolute, or relative to CWD)
- `proto_search_root`: directory used to resolve `import` statements in the proto file
- `output_dir`: where generated Julia files are written

### Generated File Layout

For a proto file with `package foo;` and service definitions, `protojl` generates:

```
output_dir/
  foo/
    foo.jl          # thin module wrapper: module foo; include("foo_pb.jl"); end
    foo_pb.jl       # all message structs + gRPCClient service client constructors
```

The `_pb.jl` file is structured as:

```julia
# Autogenerated using ProtoBuf.jl ...
import ProtoBuf as PB
import gRPCClient         # injected when any service is present
using ProtoBuf: OneOf
using ProtoBuf.EnumX: @enumx

export MyResponse, MyRequest

# --- Message structs with ProtoBuf encode/decode ---
struct MyRequest
    field_a::UInt64
    field_b::Vector{UInt64}
end
PB.default_values(::Type{MyRequest}) = (;field_a = zero(UInt64), field_b = Vector{UInt64}())
PB.field_numbers(::Type{MyRequest}) = (;field_a = 1, field_b = 2)
# ... decode/encode/_encoded_size methods ...

# gRPCClient.jl BEGIN
MyService_MyRPC_Client(
    host, port;
    secure=false,
    grpc=gRPCClient.grpc_global_handle(),
    deadline=10,
    keepalive=60,
    max_send_message_length = 4*1024*1024,
    max_recieve_message_length = 4*1024*1024,
) = gRPCClient.gRPCServiceClient{MyRequest, false, MyResponse, false}(
    host, port, "/foo.MyService/MyRPC";
    ...
)
export MyService_MyRPC_Client
# gRPCClient.jl END
```

Key observations:
- The `# gRPCClient.jl BEGIN` / `# gRPCClient.jl END` markers delimit the injected service block.
- `import gRPCClient` is added at the top only when at least one service is present.
- Client constructors are exported when the proto has a package namespace or `always_use_modules` is set.

### Client Constructor Naming

```
{ServiceName}_{RPCName}_Client
```

For `service MyService { rpc DoThing(...) ... }` → `MyService_DoThing_Client`

### RPC Path Format

```
/{package}.{ServiceName}/{RPCName}
```

For `package foo; service MyService { rpc DoThing ... }` → `/foo.MyService/DoThing`

### gRPCServiceClient Type Parameters

```julia
gRPCClient.gRPCServiceClient{TRequest, SRequest, TResponse, SResponse}
```

| Parameter  | Type | Meaning                      |
|------------|------|------------------------------|
| `TRequest` | Type | Protobuf request message type |
| `SRequest` | Bool | `true` = client streaming     |
| `TResponse`| Type | Protobuf response message type|
| `SResponse`| Bool | `true` = server streaming     |

RPC variant → type parameters:
- Unary: `{TReq, false, TResp, false}`
- Server streaming: `{TReq, false, TResp, true}`
- Client streaming: `{TReq, true, TResp, false}`
- Bidirectional: `{TReq, true, TResp, true}`

### Cross-Package Types

When a service in `pkg_a` uses message types from `pkg_b` (via proto `import`), the generated constructor uses the package-namespaced type name:

```julia
# proto: service ExtService { rpc ExtRPC(ext_types.ExtRequest) returns (ext_types.ExtResponse) {} }
gRPCClient.gRPCServiceClient{ext_types.ExtRequest, false, ext_types.ExtResponse, false}(...)
```

The namespace prefix comes from `rpc.request_type.package_namespace` (not `rpc.package_namespace`).

## Using Generated Stubs

Generated files are loaded with `include`, not `using`:

```julia
using gRPCClient

# Load message types and client constructors
include("gen/foo/foo_pb.jl")

# Construct a client
client = MyService_MyRPC_Client("localhost", 50051)

# Use message constructors with positional args matching field declaration order
req = MyRequest(42, zeros(UInt64, 10))
```

## API Reference

### Initialization / Shutdown

```julia
grpc_init()          # called automatically on `using gRPCClient`
grpc_shutdown()      # clean shutdown: closes connections, cancels in-flight requests
grpc_global_handle() # returns the shared gRPCCURL (libcurl multi handle)
```

Custom handle for isolation (separate connection pool + semaphore):

```julia
h = gRPCCURL()
grpc_init(h)
client = MyService_MyRPC_Client("host", 50051; grpc=h)
# ...
grpc_shutdown(h)
```

### Client Constructor Parameters

All generated constructors share the same keyword parameters:

| Parameter                  | Default         | Description |
|----------------------------|-----------------|-------------|
| `secure`                   | `false`         | `true` = HTTPS/gRPCS, `false` = HTTP/gRPC |
| `grpc`                     | global handle   | `gRPCCURL` instance |
| `deadline`                 | `10`            | Timeout in seconds; raises `gRPCServiceCallException(GRPC_DEADLINE_EXCEEDED)` |
| `keepalive`                | `60`            | TCP keepalive interval (seconds); sets both idle and probe interval |
| `max_send_message_length`  | `4*1024*1024`   | Max bytes per outgoing message |
| `max_recieve_message_length`| `4*1024*1024`  | Max bytes per incoming message (note: typo in API — `recieve`) |

### Unary RPC

```julia
# Synchronous (simplest)
response = grpc_sync_request(client, request)

# Async request/await (batch parallel requests)
req = grpc_async_request(client, request)     # returns gRPCRequest immediately
response = grpc_async_await(client, req)      # blocks until done, returns response

# Async with channel (out-of-order responses)
ch = Channel{gRPCAsyncChannelResponse{MyResponse}}(N)
grpc_async_request(client, request, ch, index)   # index is an Int64 correlation ID
cr = take!(ch)
!isnothing(cr.ex) && throw(cr.ex)
response = cr.response   # cr.index == the index passed in
```

`gRPCAsyncChannelResponse` fields: `.response`, `.ex` (exception or `nothing`), `.index`.

### Streaming RPC (Julia >= 1.12 required)

**Client streaming** (many requests → one response):

```julia
client = MyService_MyRPC_Client("localhost", 50051)   # SRequest=true
request_c = Channel{MyRequest}(16)
req = grpc_async_request(client, request_c)
put!(request_c, MyRequest(...))
# ...
close(request_c)                         # signals end-of-stream to server
response = grpc_async_await(client, req) # returns the single response
```

**Server streaming** (one request → many responses):

```julia
client = MyService_MyRPC_Client("localhost", 50051)   # SResponse=true
response_c = Channel{MyResponse}(16)
req = grpc_async_request(client, request, response_c)
for resp in response_c                   # channel closes automatically when stream ends
    # process resp
end
grpc_async_await(req)                    # raise any exceptions (no return value)
```

**Bidirectional streaming**:

```julia
client = MyService_MyRPC_Client("localhost", 50051)   # SRequest=true, SResponse=true
request_c = Channel{MyRequest}(16)
response_c = Channel{MyResponse}(16)
req = grpc_async_request(client, request_c, response_c)
put!(request_c, MyRequest(...))
for resp in response_c
    # process resp; can interleave puts to request_c
end
close(request_c)
grpc_async_await(req)
```

Note: For streaming variants, `grpc_async_await` does **not** return the response — it only raises exceptions. The response data flows through the channel.

### Exceptions

```julia
struct gRPCServiceCallException <: gRPCException
    grpc_status::Int   # gRPC status code integer
    message::String
end
```

Status code constants exported: `GRPC_OK`, `GRPC_CANCELLED`, `GRPC_UNKNOWN`, `GRPC_INVALID_ARGUMENT`, `GRPC_DEADLINE_EXCEEDED`, `GRPC_NOT_FOUND`, `GRPC_ALREADY_EXISTS`, `GRPC_PERMISSION_DENIED`, `GRPC_RESOURCE_EXHAUSTED`, `GRPC_FAILED_PRECONDITION`, `GRPC_ABORTED`, `GRPC_OUT_OF_RANGE`, `GRPC_UNIMPLEMENTED`, `GRPC_INTERNAL`, `GRPC_UNAVAILABLE`, `GRPC_DATA_LOSS`, `GRPC_UNAUTHENTICATED`.

## Test Infrastructure

### Go Test Server

```bash
cd test/go
go build -o grpc_test_server
./grpc_test_server          # listens on :8001 by default
```

### Running Tests

```bash
# Default: expects server already running on localhost:8001
julia --project test/runtests.jl

# Auto-start Go server from within Julia test run
JULIA_GRPCCLIENT_TEST_START_SERVER=go julia --project test/runtests.jl

# Custom host/port
GRPC_TEST_SERVER_HOST=myhost GRPC_TEST_SERVER_PORT=9000 julia --project test/runtests.jl
```

### Regenerating Stubs

```bash
cd test
bash protoc.sh
```

This regenerates `test/gen/` from `test/proto/test.proto` and also regenerates Python stubs in `test/python/`.

## Proto → Julia Type Mapping

| Proto type      | Julia type        |
|-----------------|-------------------|
| `uint32`        | `UInt32`          |
| `uint64`        | `UInt64`          |
| `int32`         | `Int32`           |
| `int64`         | `Int64`           |
| `float`         | `Float32`         |
| `double`        | `Float64`         |
| `bool`          | `Bool`            |
| `string`        | `String`          |
| `bytes`         | `Vector{UInt8}`   |
| `repeated T`    | `Vector{<julia T>}`|
| `message M`     | generated struct  |
| `enum E`        | `@enumx` enum     |
| `oneof`         | `OneOf`           |

Struct fields are **positional** in the constructor — order matches proto field declaration order. Default values are defined by `PB.default_values`.

## Common Pitfalls

1. **Load order**: `using gRPCClient` must precede `protojl`. If you call `protojl` before loading gRPCClient, no service client constructors will be generated.

2. **Streaming on Julia < 1.12**: Streaming support is conditionally compiled. A warning is emitted and streaming functions are unavailable.

3. **`grpc_async_await` return value**: For streaming clients, `grpc_async_await(req)` returns nothing — do not assign it. For unary clients, `grpc_async_await(client, req)` returns the decoded response.

4. **Channel closing**: For server/bidirectional streaming, the response channel is closed by the library when the stream ends. Do not close it yourself from the consumer side (causes `InvalidStateException` internally, which is handled gracefully).

5. **`max_recieve_message_length` spelling**: The keyword is spelled with the typo (`recieve` not `receive`) — match it exactly.

6. **Include vs using**: Always `include("gen/foo/foo_pb.jl")` — the generated files are plain scripts, not registered packages.
