Skip to content

Feat/v3#12

Merged
chakhsu merged 19 commits into
mainfrom
feat/v3
May 13, 2026
Merged

Feat/v3#12
chakhsu merged 19 commits into
mainfrom
feat/v3

Conversation

@chakhsu

@chakhsu chakhsu commented May 13, 2026

Copy link
Copy Markdown
Owner

No description provided.

chakhsu and others added 19 commits May 12, 2026 11:05
Server-side bugs:
- serverError: err.messages typo masked appended server error info
- unaryCallProxy/clientStreamingCallProxy: return after error callback to
  avoid double invocation
- serverStreamingCallProxy/bidiStreamingCallProxy: return after destroy()
  to avoid ERR_STREAM_DESTROYED from a follow-up end()

Client-side bugs:
- unaryProxy: return after reject; drop redundant outer createClientError
  wrap that double-wrapped errors
- clientStreamProxy: surface gRPC callback errors via writeEnd() instead
  of throwing inside the callback (silent unhandled rejection)
- serverStreamProxy/bidiStreamProxy: drop call.on('error') throw; iterator
  rejectionEvents already propagate errors to consumers
- Clients.prepareUrl: isDefaultClient was inverted, causing user-supplied
  url in get()/getReal() to be ignored in favor of the default client

Refactors carried with the fixes:
- clientFactory: drop dead cacheKeyWithCt branch and constructor self-
  assignment; tighten _loader type; accept undefined addr and fall back
  to _clientAddrMap instead of the 'undefined:undefined' string check
- Clients: remove _initialized flag so clear()+init() actually re-inits
- Server: drop constructor self-assignment; parse listen() port via
  lastIndexOf(':') for IPv6-friendly addresses
- ProtoLoader: drop no-op try/catch; rename shadowed param; cache an
  in-flight init promise so concurrent init() calls share work
- Stream proxy types: replace undeclared Request/Response globals with
  any; replace `Function` with ComposedMiddleware / explicit signatures
- iterator: drop legacy Symbol.asyncIterator polyfill and Function types

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Replace `expect(!x).toBeTruthy()` / `expect(!!x).toBeTruthy()` patterns
with `toBeFalsy()`, `toBeTruthy()`, and `toBeDefined()` so assertions
read directly. No coverage change.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
- Swap joi 18 for zod 4 (runtime dep)
- Rewrite src/schema/loader.ts with zod, preserving passthrough
  semantics for channelOptions/loadOptions via z.record(string, unknown)
- Wrap ZodError from assertProtoFileOptionsOptions in a plain Error to
  keep the "new ProtoLoader() params error: ..." message prefix
- Drop dead attemptClientOptions and now-unused AddressSchema import
  from src/schema/client.ts
- Narrow packagePrefix truthiness on a local in loader.ts so the
  zod-inferred optional type passes tsc
- Add test/schema.test.ts covering 21 cases: protoFileOptions accept/
  reject, default filling, passthrough, AddressSchema union, invalid
  port/host, credentials passthrough

Co-Authored-By: Claude Opus 4.7 <[email protected]>
- Add an "Install" note that Node.js >= 18 is required, matching the
  engines field bumped in the recent dependency upgrade
- Mention runtime validation via zod in the feature list

Co-Authored-By: Claude Opus 4.7 <[email protected]>
…Start

- Replace the abstract "business city" framing with a concrete description
  of what gRPCity does
- Add a "Why gRPCity" section contrasting with raw @grpc/grpc-js
- Annotate each feature with its concrete value rather than a bare label
- Break Quick Start into numbered steps (proto / loader / server / client)
  with one-line guidance per step
- List pnpm and yarn install commands alongside npm
- Drop the duplicate documentation link, link LICENSE in the footer
- Mirror all changes in README_CN.md for parity

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Util unit tests
- test/utils/iterator.test.ts: queueing/blocking, multi-event, multiArgs,
  limit, limit=0, resolution/rejection events, filter, return() cleanup,
  for-await break, invalid limit, incompatible/legacy emitters
- test/utils/compose.test.ts: ordering, sync throw, async reject, double
  next() rejection, non-array/non-function guards, shared context
- test/utils/object.test.ts: nested paths, default fallback, intermediate
  null/undefined/primitive, falsy leaf preservation, non-object root

Server public API
- test/server.test.ts: use() varargs/array/no-arg, listen-time guards on
  add/inject/use, remove(), inject(), listen with host:port string,
  invalid address rejection, idempotent shutdown/forceShutdown

Complex proto fixtures (example/proto/complex/)
- common/types.proto: Status enum + Money (int64/int32)
- inventory/item.proto: Category enum, nested Variant, repeated, map
  <string,double>, bytes, bool, google.protobuf.Timestamp, oneof change,
  oneof kind
- inventory/service.proto: 4 RPC kinds incl. oneof unary
- orders/order.proto, orders/service.proto: cross-package import,
  map<int64, Item>, oneof payment

Complex proto integration
- test/complexProto.test.ts: load/lookup, isDev+packagePrefix path
  rewrite, full-shape unary round-trip exercising every wire type, oneof
  branches, client/server/bidi streaming with rich payloads, multi-
  service registration on a single server, sparse messages with
  defaults=false; uses dynamic port allocation to avoid CI flakes

Coverage: 94.94% statements / 81.98% branches / 96.73% functions
Suites: 11, tests: 96.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
The root tsconfig.json previously only included src/ and did not declare
jest in `types`. Editors opening test/*.ts fell back to an inferred
project with no jest globals, surfacing "Cannot find name 'expect'"
even though `pnpm test` succeeded via tsconfig.test.json.

- Add tsconfig.build.json: dedicated production config with rootDir
  src, outDir lib, types ["node"]; build script switches to it
- Repurpose tsconfig.json as the editor view: rootDir ".", noEmit
  true, include src+test, types ["jest", "node"]; relaxes
  noUnusedLocals/Parameters which are still enforced by the build
  config
- Simplify tsconfig.test.json to just extend the root and enable
  isolatedModules (also silences ts-jest TS151002 warnings)

Build artifacts and test/lint pipelines are unchanged (11 suites,
96 tests still pass).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
In grpc-js 1.14 calling call.destroy(err) on a ServerWritableStream /
ServerDuplexStream tears down the underlying HTTP/2 stream without
running the wrapper's own 'error' handler, so the client never receives
a status trailer and only fails after its deadline elapses (default
10s). The wrapper installs an 'error' listener that turns err into a
gRPC status via serverErrorToStatus and then end()s the stream,
which is exactly the path we want.

Switch the catch in serverStreamingCallProxy and bidiStreamingCallProxy
from call.destroy(err) to call.emit('error', err). Clients now observe
the original error message immediately.

Covered by test/streamErrors.test.ts (server-stream and bidi cases).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Existing stream tests covered only the happy path. Add four focused
suites plus a small dynamic-port harness:

- test/streamErrors.test.ts: server-side throw on each RPC kind
  surfaces on the client (regression coverage for the destroy/emit
  fix and the earlier A6-A8 client-side fixes)
- test/streamLifecycle.test.ts: unary and server-stream deadline
  exceeded, mid-stream client cancel, empty client/bidi streams
- test/streamScale.test.ts: 1000-message client stream, 1000-message
  server stream, 500-round-trip bidi — verifies count and ordering
- test/streamMetadata.test.ts: client sends custom metadata, server
  reads it and echoes back via sendMetadata; assert response headers
  on each RPC kind
- test/helpers/streamHarness.ts: dynamic free-port allocator and a
  shared loader factory so suites don't collide on hard-coded ports

Coverage moves to 95.93% statements / 82.49% branches / 96.83%
functions / 95.68% lines (15 suites, 112 tests).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Metadata was previously only re-exported as a type, so users (and our
own tests) had to depend on @grpc/grpc-js directly to construct
metadata at runtime.

Re-export the runtime values Metadata and credentials, plus the type
shapes StatusObject, ChannelCredentials, ServerCredentials,
MetadataValue, and ChannelOptions. Callers can now construct request
metadata and credentials without importing @grpc/grpc-js:

  import { ProtoLoader, Metadata, credentials } from 'grpcity'

Update test/streamMetadata.test.ts to use the new entry point.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
The Quick Start loader snippet imports with ESM syntax but uses
__dirname, which only exists in CommonJS. Add a note under the snippet
in both READMEs pointing ESM users at import.meta.dirname (Node.js
>= 20.11) or fileURLToPath(import.meta.url) on Node 18.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
- Add a workspace dependency `grpcity: link:..` (and tsx as a devDep) so
  examples can `import { ProtoLoader } from 'grpcity'` instead of the
  ../../lib/index.js paths that exposed the build layout
- Replace `path.dirname(new URL(import.meta.url).pathname)` with the
  cross-platform `path.dirname(fileURLToPath(import.meta.url))` in every
  example loader
- Rewrite example/README.md with setup instructions, a directory index,
  and a recommended reading order; document cert regeneration

Examples touched in this commit: helloworld, stream, reflection (its
loader). The actual stream/reflection rewrites land in the next commit.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
…ection

- Remove example/simple — duplicates helloworld and is the cruder of
  the two
- Remove example/asyncStream and replace example/stream with the async
  version. The old example/stream used the legacy callback-based
  client.call.* API which is no longer the recommended surface
- Remove example/reflection/{asyncStreamServer.js,helloworldServer.js},
  which conflated reflection, multi-service registration, and middleware.
  Replace with example/reflection/server.js focused on reflection only
- Add example/multiService/ to host the two-services-on-one-server +
  middleware demo that was previously buried inside reflection/

Co-Authored-By: Claude Opus 4.7 <[email protected]>
…it samples

Five new self-contained examples covering scenarios that previously had
no runnable demonstration:

- tls/         — mutual TLS using the certs in example/certs
- middleware/  — client- and server-side (ctx, next) patterns
- errors/      — server throws + client deadline; reading
                 GrpcClientError fields (name, code, details)
- typescript/  — minimal TS flow runnable with `pnpm exec tsx`
- loader-init/ — focused walk-through of every loader.init() option
                 (defaults, isDev + packagePrefix, loadOptions overrides,
                 concurrent-safety) with a short README that documents
                 each parameter

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Prettier was flagging example/pnpm-lock.yaml as needing formatting
after the example workspace gained dependencies. Add lib/, coverage/,
and both pnpm-lock.yaml files to .prettierignore so generated and
auto-managed files stop tripping `pnpm lint:prettier`.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Per-call options on every RPC now accept `signal: AbortSignal`.
Behaviour:

- pre-abort: signal.throwIfAborted() runs before any network call,
  so an already-aborted signal surfaces as AbortError without ever
  contacting the server
- mid-call abort: a once-listener calls call.cancel() under the hood;
  the client receives a GrpcClientError with code 1 (CANCELLED)
- the listener is removed on every terminal status event so a
  long-lived signal reused across many RPCs does not accumulate
  listeners (verified by a regression test)
- when no signal is supplied, the proxies skip all of the above,
  keeping the existing zero-allocation fast path

Also:

- src/client/clientSignal.ts: tiny helper that extracts the signal
  from per-call options and returns the rest, so we never forward
  our own concept to grpc-js's CallOptions
- src/client/clientError.ts: createClientError is now idempotent for
  AbortError (passed through) and GrpcClientError (returned as-is)
  to avoid double-wrapping
- src/index.ts: export the new ClientCallOptions type so TS callers
  can spell out `{ signal }` without casts

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Nine cases in test/streamAbort.test.ts:

- pre-abort path for unary, client stream, server stream, and bidi —
  asserts AbortError surfaces without an RPC being issued
- mid-call abort for unary, server stream, and bidi — asserts the
  cancellation propagates and the consumer stops early
- no-signal regression — confirms the zero-allocation path still
  resolves normally
- listener leak guard — reuses one signal across five sequential
  unary calls and asserts getEventListeners(signal, 'abort') is
  empty afterwards, locking in the cleanup contract

Co-Authored-By: Claude Opus 4.7 <[email protected]>
- example/abortSignal/: server + client demonstrating
  (1) pre-abort short-circuit (AbortError, no RPC sent),
  (2) AbortSignal.timeout() cancelling a slow unary,
  (3) one signal aborting a fan-out of concurrent RPCs,
  (4) a baseline call without a signal
- example/README.md: index the new directory and add it to the
  recommended reading order alongside errors/
- README.md / README_CN.md: list AbortSignal in the feature table

Co-Authored-By: Claude Opus 4.7 <[email protected]>
@chakhsu chakhsu merged commit 71ea5eb into main May 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant