Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "packages/scrawn/proto"]
path = packages/scrawn/proto
url = https://github.com/ScrawnDotDev/.proto
4 changes: 3 additions & 1 deletion packages/scrawn/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"gen": "cd proto && bunx buf generate",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
"test:coverage": "vitest run --coverage",
"proto:pull": "git submodule update --remote --merge proto",
"proto:push": "cd proto && git add . && git commit -s -m \"$1\" && git push && cd .."
},
"dependencies": {
"@bufbuild/protobuf": "1.7.2",
Expand Down
1 change: 1 addition & 0 deletions packages/scrawn/proto
Submodule proto added at 165648
22 changes: 0 additions & 22 deletions packages/scrawn/proto/auth/v1/auth.proto

This file was deleted.

8 changes: 0 additions & 8 deletions packages/scrawn/proto/buf.gen.yaml

This file was deleted.

74 changes: 0 additions & 74 deletions packages/scrawn/proto/event/v1/event.proto

This file was deleted.

16 changes: 0 additions & 16 deletions packages/scrawn/proto/payment/v1/payment.proto

This file was deleted.

19 changes: 13 additions & 6 deletions packages/scrawn/src/core/grpc/requestBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {
MethodInput,
MethodOutput,
RequestState,
UnaryMethodFn,
} from './types.js';
import type { GrpcCallContext } from './callContext.js';

Expand Down Expand Up @@ -152,14 +153,20 @@ export class RequestBuilder<
this.ctx.logCallStart();
this.ctx.log.debug(`Payload: ${JSON.stringify(this.payload)}`);

// The actual gRPC call - fully type-safe!
const response = await (this.ctx.client[this.ctx.methodName] as any)(
this.payload,
{ headers: this.ctx.getHeaders() }
);
// The actual gRPC call.
// Type assertion is required because TypeScript cannot track the relationship
// between a dynamic key access and the resulting function signature in mapped types.
// This is safe because:
// 1. methodName is constrained to ServiceMethodNames<S>
// 2. MethodInput/MethodOutput are derived from the same service definition
const method = this.ctx.client[this.ctx.methodName] as UnaryMethodFn<
MethodInput<S, M>,
MethodOutput<S, M>
>;
const response = await method(this.payload, { headers: this.ctx.getHeaders() });

this.ctx.logCallSuccess();
return response as MethodOutput<S, M>;
return response;
} catch (error) {
this.ctx.logCallError(error);
throw error;
Expand Down
18 changes: 14 additions & 4 deletions packages/scrawn/src/core/grpc/streamRequestBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
ServiceMethodNames,
MethodInput,
MethodOutput,
ClientStreamingMethodFn,
} from './types.js';
import type { GrpcCallContext } from './callContext.js';

Expand Down Expand Up @@ -101,14 +102,23 @@ export class StreamRequestBuilder<
try {
this.ctx.logCallStart();

// The actual client-streaming gRPC call
const response = await (this.ctx.client[this.ctx.methodName] as any)(
iterable,
// The actual client-streaming gRPC call.
// Type assertion is required because TypeScript cannot track the relationship
// between a dynamic key access and the resulting function signature in mapped types.
// This is safe because:
// 1. methodName is constrained to ServiceMethodNames<S>
// 2. MethodInput/MethodOutput are derived from the same service definition
const method = this.ctx.client[this.ctx.methodName] as ClientStreamingMethodFn<
MethodInput<S, M>,
MethodOutput<S, M>
>;
const response = await method(
iterable as AsyncIterable<Partial<MethodInput<S, M>>>,
{ headers: this.ctx.getHeaders() }
);

this.ctx.logCallSuccess();
return response as MethodOutput<S, M>;
return response;
} catch (error) {
this.ctx.logCallError(error);
throw error;
Expand Down
68 changes: 63 additions & 5 deletions packages/scrawn/src/core/grpc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,66 @@ export type ClientStreamingMethodNames<S extends ServiceType> = MethodsOfKind<S,
*/
export type ServerStreamingMethodNames<S extends ServiceType> = MethodsOfKind<S, 'server_streaming'>;

/**
* Extract bidirectional-streaming method names from a service.
* Bidi-streaming methods accept a stream of requests and return a stream of responses.
*/
export type BidiStreamingMethodNames<S extends ServiceType> = MethodsOfKind<S, 'bidi_streaming'>;
/**
* Extract bidirectional-streaming method names from a service.
* Bidi-streaming methods accept a stream of requests and return a stream of responses.
*/
export type BidiStreamingMethodNames<S extends ServiceType> = MethodsOfKind<S, 'bidi_streaming'>;

/**
* Options passed to gRPC method calls.
* Matches CallOptions from @connectrpc/connect.
*/
export interface GrpcCallOptions {
headers?: Headers;
signal?: AbortSignal;
timeoutMs?: number;
onHeader?: (headers: globalThis.Headers) => void;
onTrailer?: (trailers: globalThis.Headers) => void;
}

/**
* Function signature for a unary gRPC method.
* Takes a partial message and options, returns a promise of the response.
*
* @template I - Input message type
* @template O - Output message type
*/
export type UnaryMethodFn<I, O> = (
request: Partial<I>,
options?: GrpcCallOptions
) => Promise<O>;

/**
* Function signature for a client-streaming gRPC method.
* Takes an async iterable of partial messages and options, returns a promise of the response.
*
* @template I - Input message type
* @template O - Output message type
*/
export type ClientStreamingMethodFn<I, O> = (
request: AsyncIterable<Partial<I>>,
options?: GrpcCallOptions
) => Promise<O>;

/**
* Get the method function type from a Client for a specific method.
* This properly extracts the callable type for dynamic method access.
*
* Note: Due to TypeScript limitations with mapped types and dynamic keys,
* this type is used with type assertions when accessing client methods
* via computed property names. The assertion is safe because:
* 1. The method name M is constrained to ServiceMethodNames<S>
* 2. The input/output types are derived from the same service definition
*
* @template S - The gRPC service type
* @template M - The method name on the service
*/
export type ClientMethod<
S extends ServiceType,
M extends ServiceMethodNames<S>
> = S['methods'][M] extends { kind: 'unary' }
? UnaryMethodFn<MethodInput<S, M>, MethodOutput<S, M>>
: S['methods'][M] extends { kind: 'client_streaming' }
? ClientStreamingMethodFn<MethodInput<S, M>, MethodOutput<S, M>>
: never;
Loading
Loading