Skip to content
Open
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
50 changes: 48 additions & 2 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { type AnyCodec, tk, type VoidCodec } from 'typekind';
import { type AnyCodec, TupleCodec, tk, type VoidCodec } from 'typekind';

// TODO: handler/stream naming could be improved
// TODO: handler/stream naming could be improved - use method as general name
// for handler or stream ie service have methods

export type AnyApi =
| AnyRouterApi
| AnyServiceApi
| AnyHandlerApi
| AnyStreamApi;

export type AnyRouterApi = RouterApi<RouterSpec>;
export type AnyServiceApi = ServiceApi<HandlerApis>;
Expand Down Expand Up @@ -30,6 +37,10 @@ export class HandlerApi<Input extends AnyCodec, Output extends AnyCodec> {
public readonly input: Input,
public readonly output: Output,
) {}

getParamCodec(index: number): AnyCodec {
return getParamCodec(this, index);
}
}

export class StreamApi<
Expand All @@ -44,8 +55,43 @@ export class StreamApi<
public readonly value: Value,
public readonly output: Output,
) {}

getParamCodec(index: number): AnyCodec {
return getParamCodec(this, index);
}
}

function getParamCodec(
api: AnyHandlerApi | AnyStreamApi,
index: number,
): AnyCodec {
const inputCodec = api.input;

if (inputCodec instanceof TupleCodec) {
const argCodec = (inputCodec as TupleCodec<AnyCodec[]>).codecs[index];
if (argCodec === undefined) {
throw Error('rpc method argument index out of bounds');
}
return argCodec;
}

if (index !== 0) {
throw Error('rpc method argument index out of bounds');
}

return inputCodec;
}

export type InferApiType<T extends AnyApi> = T extends AnyRouterApi
? InferRouterType<T>
: T extends AnyServiceApi
? InferServiceType<T>
: T extends AnyHandlerApi
? InferHandlerType<T>
: T extends AnyStreamApi
? InferStreamType<T>
: never;

export type InferRouterType<T extends AnyRouterApi> = {
[K in keyof T['routes']]: T['routes'][K] extends AnyServiceApi
? InferServiceType<T['routes'][K]>
Expand Down
2 changes: 2 additions & 0 deletions src/next/batch-scheduler-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// TODO
export type IBatchScheduler = unknown;
143 changes: 143 additions & 0 deletions src/next/client-dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: need any for inference */

import type { AnyRouterApi, InferRouterType } from '../api.js';
import { HandlerApi, StreamApi } from '../api.js';
import type { Rpc } from '../rpc-type.js';
import type {
ClientNextConfig,
IClient,
InferOperationResult,
OperationBuilderReturn,
} from './client-type.js';
import { type OpId, PlanBuilder } from './plan-builder.js';
import { isRpcProxy, ProxyState, unwrapOpProxy } from './proxy.js';
import type { TransportPlanRequest } from './transport.js';

export type DefineCallback<
Router extends AnyRouterApi,
Params extends Record<string, unknown>,
Return extends OperationBuilderReturn,
> = (api: Rpc<InferRouterType<Router>>, params: Params) => Return;

export class ClientDev<Router extends AnyRouterApi> implements IClient<Router> {
private definedOperations: string[] = [];

constructor(public readonly config: ClientNextConfig<Router>) {}

define<
Args extends any[],
Params extends Record<string, any>,
Return extends OperationBuilderReturn,
>(
name: string,
prepare: (...args: Args) => Params,
callback: DefineCallback<Router, Params, Return>,
): (...args: Args) => InferOperationResult<Return>;

define<
Args extends any[],
Params extends Record<string, any>,
Return extends OperationBuilderReturn,
>(
name: string,
prepare: (...args: Args) => Params,
keys: () => (keyof Params)[],
callback: DefineCallback<Router, Params, Return>,
): (...args: Args) => InferOperationResult<Return>;

define<
Args extends any[],
Params extends Record<string, any>,
Return extends OperationBuilderReturn,
>(
name: string,
prepare: (...args: Args) => Params,
callbackOrKeysArg:
| DefineCallback<Router, Params, Return>
| (() => (keyof Params)[]),
callbackArg?: DefineCallback<Router, Params, Return>,
): (...args: Args) => InferOperationResult<Return> {
const getKeys =
callbackArg === undefined
? undefined
: (callbackOrKeysArg as () => (keyof Params)[]);

const callback = callbackArg
? (callbackArg as DefineCallback<Router, Params, Return>)
: (callbackOrKeysArg as DefineCallback<Router, Params, Return>);

if (this.definedOperations.includes(name)) {
throw Error('operation already defined');
}

const operation = (...args: Args): InferOperationResult<Return> => {
const params = prepare(...args);
const builder = new PlanBuilder();
const api = this.config.router;
const proxy = ProxyState.createProxy<Router>(undefined, builder, api);
// set T to unknown to prevent everything from collapsing to any and
// maintain some type safety here
const result = callback(proxy, params) as OperationBuilderReturn<unknown>;

let streams: TransportPlanRequest['streams'];
let outputs: TransportPlanRequest['outputs'];

if (isRpcProxy(result)) {
const { api, op } = unwrapOpProxy(result);

if (api instanceof HandlerApi) {
outputs = op.id;
} else if (api instanceof StreamApi) {
streams = op.id;
} else {
throw Error('expected handler or stream');
}
} else {
let s: Record<string, OpId> | undefined;
let o: Record<string, OpId> | undefined;

for (const [key, value] of Object.entries(result)) {
const { api, op } = unwrapOpProxy(value);

if (api instanceof HandlerApi) {
o ??= {};
o[key] = op.id;
} else if (api instanceof StreamApi) {
s ??= {};
s[key] = op.id;
} else {
throw Error('expected handler or stream');
}
}

if (s) streams = s;
if (o) outputs = o;
}

const keys = getKeys?.() as string[] | undefined;

const request: TransportPlanRequest = {
operation: name,
params,
keys,
plan: {}, // XXX
streams,
outputs,
};

throw Error('todo: send to transport');
};

this.definedOperations.push(name);

return operation;
}

get api(): Rpc<InferRouterType<Router>> {
// TODO: create a proxy to construct a single, unbatched request

// TODO: how to tree-shake

throw Error('todo: implement unbatched requests');
}
}
82 changes: 82 additions & 0 deletions src/next/client-prod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: need any for inference */

import type { AnyRouterApi, InferRouterType } from '../api.js';
import type { Rpc } from '../rpc-type.js';
import type {
ClientNextConfig,
IClient,
InferOperationResult,
OperationBuilderReturn,
} from './client-type.js';
import type { TransportPersistentRequest } from './transport.js';

export type DefineCallback<
Router extends AnyRouterApi,
Params extends Record<string, unknown>,
Return extends OperationBuilderReturn,
> = (api: Rpc<InferRouterType<Router>>, params: Params) => Return;

export class ClientProd<Router extends AnyRouterApi>
implements IClient<Router>
{
private operations: Record<string, (...args: any[]) => any> = {};

constructor(public readonly config: ClientNextConfig<Router>) {}

define<
Args extends any[],
Params extends Record<string, any>,
Return extends OperationBuilderReturn,
>(
name: string,
prepare: (...args: Args) => Params,
callback: DefineCallback<Router, Params, Return>,
): (...args: Args) => InferOperationResult<Return>;

define<
Args extends any[],
Params extends Record<string, any>,
Return extends OperationBuilderReturn,
>(
name: string,
prepare: (...args: Args) => Params,
keys: () => (keyof Params)[],
callback: DefineCallback<Router, Params, Return>,
): (...args: Args) => InferOperationResult<Return>;

define<
Args extends any[],
Params extends Record<string, any>,
Return extends OperationBuilderReturn,
>(
name: string,
prepare: (...args: Args) => Params,
_callbackOrKeysArg:
| DefineCallback<Router, Params, Return>
| (() => (keyof Params)[]),
_callbackArg?: DefineCallback<Router, Params, Return>,
): (...args: Args) => InferOperationResult<Return> {
let operation = this.operations[name];

if (operation) {
return operation;
}

operation = (...args: Args): InferOperationResult<Return> => {
const params = prepare(...args);
const request: TransportPersistentRequest = {
operation: name,
params,
};
throw Error('todo: send to transport');
};

this.operations[name] = operation;

return operation;
}

get api(): Rpc<InferRouterType<Router>> {
throw Error('todo: implement unbatched requests');
}
}
53 changes: 53 additions & 0 deletions src/next/client-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: need any for inference */

import type { Simplify } from 'typekind';
import type { AnyRouterApi, InferRouterType } from '../api.js';
import type { Rpc } from '../rpc-type.js';
import type { DefineCallback } from './client-dev.js';
import type { SplitAsyncIterable } from './split-async-iterable.js';

// need any to correctly infer return types at usage, but set T to unknown for
// typing internal code
export type OperationBuilderReturn<T = any> = Rpc<T> | Record<string, Rpc<T>>;

export type InferOperationOutput<T> = T extends Rpc<infer U>
? U extends AsyncGenerator<infer T, infer TReturn, infer TNext>
? SplitAsyncIterable<T, TReturn, TNext>
: Promise<U>
: never;

export type InferOperationResult<T> = T extends Rpc<infer U>
? U extends AsyncGenerator<infer T, infer TReturn, infer TNext>
? SplitAsyncIterable<T, TReturn, TNext>
: Promise<U>
: Simplify<{ [K in keyof T]: InferOperationOutput<T[K]> }>;

export interface ClientNextConfig<Router extends AnyRouterApi> {
router: Router;
}

export interface IClient<Router extends AnyRouterApi> {
define<
Args extends any[],
Params extends Record<string, any>,
Return extends OperationBuilderReturn,
>(
name: string,
prepare: (...args: Args) => Params,
callback: DefineCallback<Router, Params, Return>,
): (...args: Args) => InferOperationResult<Return>;

define<
Args extends any[],
Params extends Record<string, any>,
Return extends OperationBuilderReturn,
>(
name: string,
prepare: (...args: Args) => Params,
keys: () => (keyof Params)[],
callback: DefineCallback<Router, Params, Return>,
): (...args: Args) => InferOperationResult<Return>;

// TODO: will probably remove for tree-shaking
get api(): Rpc<InferRouterType<Router>>;
}
14 changes: 14 additions & 0 deletions src/next/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { AnyRouterApi } from '../api.js';
import { ClientDev } from './client-dev.js';
import { ClientProd } from './client-prod.js';
import type { ClientNextConfig, IClient } from './client-type.js';

export function createClient<Router extends AnyRouterApi>(
config: ClientNextConfig<Router>,
): IClient<Router> {
if (import.meta.env.DEV) {
return new ClientDev(config);
} else {
return new ClientProd(config);
}
}
Loading