diff --git a/src/json-type-value/ObjectValue.ts b/src/json-type-value/ObjectValue.ts index 1dc8b7a36e..992e01f0cf 100644 --- a/src/json-type-value/ObjectValue.ts +++ b/src/json-type-value/ObjectValue.ts @@ -1,11 +1,15 @@ import {Value} from './Value'; import {toText} from '../json-type/typescript/toText'; -import type {ResolveType} from '../json-type'; +import type {ResolveType, TypeSystem} from '../json-type'; import type * as classes from '../json-type/type'; import type * as ts from '../json-type/typescript/types'; -type UnObjectType = T extends classes.ObjectType ? U : never; -type UnObjectFieldTypeVal = T extends classes.ObjectFieldType ? U : never; +export type UnObjectType = T extends classes.ObjectType ? U : never; +export type UnObjectValue = T extends ObjectValue ? U : never; +export type UnObjectFieldTypeVal = T extends classes.ObjectFieldType ? U : never; +export type ObjectFieldToTuple = F extends classes.ObjectFieldType ? [K, V] : never; +export type ToObject = T extends [string, unknown][] ? {[K in T[number] as K[0]]: K[1]} : never; +export type ObjectValueToTypeMap = ToObject<{[K in keyof F]: ObjectFieldToTuple}>; // export type MergeObjectsTypes = // A extends classes.ObjectType @@ -22,6 +26,8 @@ type UnObjectFieldTypeVal = T extends classes.ObjectFieldType ? // never; export class ObjectValue> extends Value { + public static create = (system: TypeSystem) => new ObjectValue(system.t.obj, {}); + public field>( field: F, data: ResolveType>, @@ -51,6 +57,20 @@ export class ObjectValue> extends Value { return new ObjectValue(extendedType, extendedData) as any; } + public get>>( + key: K, + ): Value< + ObjectValueToTypeMap>[K] extends classes.Type + ? ObjectValueToTypeMap>[K] + : classes.Type + > { + const field = this.type.getField(key); + if (!field) throw new Error('NO_FIELD'); + const type = field.value; + const data = this.data[key]; + return new Value(type, data) as any; + } + public toTypeScriptAst(): ts.TsTypeLiteral { const node: ts.TsTypeLiteral = { node: 'TypeLiteral', diff --git a/src/json-type-value/Value.ts b/src/json-type-value/Value.ts index 367c8fe75c..d8780127e7 100644 --- a/src/json-type-value/Value.ts +++ b/src/json-type-value/Value.ts @@ -1,5 +1,15 @@ +import type {JsonValueCodec} from '../json-pack/codecs/types'; import type {ResolveType, Type} from '../json-type'; export class Value { constructor(public type: T, public data: ResolveType) {} + + public encode(codec: JsonValueCodec): void { + const value = this.data; + const type = this.type; + if (value === undefined) return; + const encoder = codec.encoder; + if (!type) encoder.writeAny(value); + else type.encoder(codec.format)(value, encoder); + } } diff --git a/src/json-type-value/__tests__/ObjectValue.spec.ts b/src/json-type-value/__tests__/ObjectValue.spec.ts new file mode 100644 index 0000000000..717954af15 --- /dev/null +++ b/src/json-type-value/__tests__/ObjectValue.spec.ts @@ -0,0 +1,11 @@ +import {TypeSystem} from '../../json-type/system'; +import {ObjectValue} from '../ObjectValue'; + +test('can retrieve field as Value', () => { + const system = new TypeSystem(); + const {t} = system; + const obj = new ObjectValue(t.Object(t.prop('foo', t.str)), {foo: 'bar'}); + const foo = obj.get('foo'); + expect(foo.type.getTypeName()).toBe('str'); + expect(foo.data).toBe('bar'); +}); diff --git a/src/json-type/type/classes/AbstractType.ts b/src/json-type/type/classes/AbstractType.ts index 3adebb766a..12fdc3d5c8 100644 --- a/src/json-type/type/classes/AbstractType.ts +++ b/src/json-type/type/classes/AbstractType.ts @@ -54,6 +54,12 @@ export abstract class AbstractType implements BaseType< /** @todo Retype this to `Schema`. */ protected abstract schema: S; + public getSystem(): TypeSystem { + const system = this.system; + if (!system) throw new Error('NO_SYSTEM'); + return system; + } + public getTypeName(): S['__t'] { return this.schema.__t; } diff --git a/src/reactive-rpc/common/messages/Value.ts b/src/reactive-rpc/common/messages/Value.ts index 66e564157f..7be44822c9 100644 --- a/src/reactive-rpc/common/messages/Value.ts +++ b/src/reactive-rpc/common/messages/Value.ts @@ -1,5 +1,4 @@ import {Value as V} from '../../../json-type-value/Value'; -import type {JsonValueCodec} from '../../../json-pack/codecs/types'; import type {Type} from '../../../json-type'; /** @@ -9,13 +8,4 @@ export class RpcValue extends V { constructor(public data: V, public type: Type | undefined) { super(type, data); } - - public encode(codec: JsonValueCodec): void { - const value = this.data; - const type = this.type; - if (value === undefined) return; - const encoder = codec.encoder; - if (!type) encoder.writeAny(value); - else type.encoder(codec.format)(value, encoder); - } } diff --git a/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts b/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts new file mode 100644 index 0000000000..baa93e32f6 --- /dev/null +++ b/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts @@ -0,0 +1,125 @@ +import {RpcError} from './error'; +import {RpcCaller, type RpcApiCallerOptions} from './RpcCaller'; +import {type AbstractType, FunctionStreamingType, FunctionType} from '../../../../json-type/type/classes'; +import {StaticRpcMethod, type StaticRpcMethodOptions} from '../methods/StaticRpcMethod'; +import {StreamingRpcMethod, type StreamingRpcMethodOptions} from '../methods/StreamingRpcMethod'; +import { + type ObjectType, + type Schema, + type TypeSystem, + ObjectFieldType, + TypeOf, + SchemaOf, + Type, +} from '../../../../json-type'; +import type {ObjectValue, UnObjectType, UnObjectValue} from '../../../../json-type-value/ObjectValue'; +import type {Value} from '../../../../json-type-value/Value'; +import type {Observable} from 'rxjs'; +import type {RpcValue} from '../../messages/Value'; + +type ObjectFieldToTuple = F extends ObjectFieldType ? [K, V] : never; +type ToObject = T extends [string, unknown][] ? {[K in T[number] as K[0]]: K[1]} : never; +type ObjectFieldsToMap = ToObject<{[K in keyof F]: ObjectFieldToTuple}>; +type ObjectValueToTypeMap = ObjectFieldsToMap>>; + +type MethodReq = F extends FunctionType + ? TypeOf> + : F extends FunctionStreamingType + ? TypeOf> + : never; + +type MethodRes = F extends FunctionType + ? TypeOf> + : F extends FunctionStreamingType + ? TypeOf> + : never; + +type MethodDefinition = F extends FunctionType + ? StaticRpcMethodOptions, MethodRes> + : F extends FunctionStreamingType + ? StreamingRpcMethodOptions, MethodRes> + : never; + +export interface ObjectValueCallerOptions>, Ctx = unknown> + extends Omit, 'getMethod'> { + router: V; +} + +export class ObjectValueCaller>, Ctx = unknown> extends RpcCaller { + protected readonly router: V; + protected readonly system: TypeSystem; + protected readonly methods = new Map | StreamingRpcMethod>(); + + constructor({router: value, ...rest}: ObjectValueCallerOptions) { + super({ + ...rest, + getMethod: (name) => this.get(name) as any as StaticRpcMethod | StreamingRpcMethod, + }); + this.router = value; + const system = value.type.system; + if (!system) throw new Error('NO_SYSTEM'); + this.system = system; + } + + public get>( + id: K, + ): MethodDefinition[K]> | undefined { + let method = this.methods.get(id as string) as any; + if (method) return method; + const fn = this.router.get(id) as Value; + if (!fn || !(fn.type instanceof FunctionType || fn.type instanceof FunctionStreamingType)) { + return undefined; + } + const fnType = fn.type as FunctionType | FunctionStreamingType; + const {req, res} = fnType; + const call = fn.data; + const validator = fnType.req.validator('object'); + const requestSchema = (fnType.req as AbstractType).getSchema(); + const isRequestVoid = requestSchema.__t === 'const' && requestSchema.value === undefined; + const validate = isRequestVoid + ? () => {} + : (req: unknown) => { + const error: any = validator(req); + if (error) { + const message = error.message + (Array.isArray(error?.path) ? ' Path: /' + error.path.join('/') : ''); + throw RpcError.value(RpcError.validation(message, error)); + } + }; + method = + fnType instanceof FunctionType + ? new StaticRpcMethod({req, res, validate, call}) + : new StreamingRpcMethod({req, res, validate, call$: call}); + this.methods.set(id as string, method as any); + return method; + } + + public async call>( + id: K, + request: MethodReq[K]>, + ctx: Ctx, + ): Promise[K]>>> { + return super.call(id as string, request, ctx) as any; + } + + public async callSimple>( + id: K, + request: MethodReq[K]>, + ctx: Ctx = {} as any, + ): Promise[K]>> { + try { + const res = await this.call(id as string, request, ctx); + return res.data; + } catch (err) { + const error = err as RpcValue; + throw error.data; + } + } + + public call$>( + id: K, + request: Observable[K]>>, + ctx: Ctx, + ): Observable[K]>>> { + return super.call$(id as string, request, ctx) as any; + } +} diff --git a/src/reactive-rpc/common/rpc/caller/__tests__/ObjectValueCaller.spec.ts b/src/reactive-rpc/common/rpc/caller/__tests__/ObjectValueCaller.spec.ts new file mode 100644 index 0000000000..65b95cb1e4 --- /dev/null +++ b/src/reactive-rpc/common/rpc/caller/__tests__/ObjectValueCaller.spec.ts @@ -0,0 +1,16 @@ +import {TypeSystem} from '../../../../../json-type'; +import {ObjectValue} from '../../../../../json-type-value/ObjectValue'; +import {ObjectValueCaller} from '../ObjectValueCaller'; + +test('can execute simple calls', async () => { + const system = new TypeSystem(); + const {t} = system; + const router = ObjectValue.create(system) + .prop('ping', t.Function(t.any, t.Const('pong')), async () => 'pong') + .prop('echo', t.Function(t.any, t.any), async (req) => req); + const caller = new ObjectValueCaller({router}); + const res1 = await caller.call('ping', null, {}); + expect(res1.data).toBe('pong'); + const res2 = await caller.callSimple('echo', {foo: 'bar'}, {}); + expect(res2).toEqual({foo: 'bar'}); +}); diff --git a/src/reactive-rpc/common/testing/buildE2eClient.ts b/src/reactive-rpc/common/testing/buildE2eClient.ts index 68c6259ef0..e6fd97612f 100644 --- a/src/reactive-rpc/common/testing/buildE2eClient.ts +++ b/src/reactive-rpc/common/testing/buildE2eClient.ts @@ -11,6 +11,9 @@ import type {Observable} from 'rxjs'; import type {ResolveType} from '../../../json-type'; import type {TypeRouter} from '../../../json-type/system/TypeRouter'; import type {TypeRouterCaller} from '../rpc/caller/TypeRouterCaller'; +import type {RpcCaller} from '../rpc/caller/RpcCaller'; +import type {ObjectValueCaller} from '../rpc/caller/ObjectValueCaller'; +import type {ObjectValue, ObjectValueToTypeMap, UnObjectType} from '../../../json-type-value/ObjectValue'; export interface BuildE2eClientOptions { /** @@ -64,7 +67,7 @@ export interface BuildE2eClientOptions { token?: string; } -export const buildE2eClient = >(caller: Caller, opt: BuildE2eClientOptions) => { +export const buildE2eClient = >(caller: Caller, opt: BuildE2eClientOptions) => { const writer = opt.writer ?? new Writer(Fuzzer.randomInt2(opt.writerDefaultBufferKb ?? [4, 4]) * 1024); const codecs = new RpcCodecs(new Codecs(writer), new RpcMessageCodecs()); const ctx = new ConnectionContext( @@ -109,8 +112,12 @@ export const buildE2eClient = >(caller: Cal }; }; -type UnTypeRouterCaller = T extends TypeRouterCaller ? R : never; -type UnTypeRouter = T extends TypeRouter ? R : never; +type UnTypeRouterCaller = T extends TypeRouterCaller ? R : T extends ObjectValueCaller ? R : never; +type UnTypeRouter = T extends TypeRouter + ? R + : T extends ObjectValue + ? ObjectValueToTypeMap> + : never; type UnwrapFunction = F extends FunctionType ? (req: ResolveType) => Promise> : F extends FunctionStreamingType diff --git a/src/server/routes/blocks/index.ts b/src/server/routes/blocks/index.ts index ebe2dce3c7..b219a7ce78 100644 --- a/src/server/routes/blocks/index.ts +++ b/src/server/routes/blocks/index.ts @@ -5,24 +5,25 @@ import {edit} from './methods/edit'; import {listen} from './methods/listen'; import {Block, BlockId, BlockPatch, BlockSeq} from './schema'; import {history} from './methods/history'; -import type {RoutesBase, TypeRouter} from '../../../json-type/system/TypeRouter'; -import type {RouteDeps} from '../types'; +import type {RouteDeps, Router, RouterBase} from '../types'; export const blocks = (d: RouteDeps) => - (r: TypeRouter) => { - r.system.alias('BlockId', BlockId); - r.system.alias('BlockSeq', BlockSeq); - r.system.alias('Block', Block); - r.system.alias('BlockPatch', BlockPatch); + (r: Router) => { + const {system} = d; + + system.alias('BlockId', BlockId); + system.alias('BlockSeq', BlockSeq); + system.alias('Block', Block); + system.alias('BlockPatch', BlockPatch); // prettier-ignore return ( - ( create(d) - ( get(d) - ( remove(d) - ( edit(d) - ( listen(d) - ( history(d) - ( r )))))))); + ( create(d) + ( get(d) + ( remove(d) + ( edit(d) + ( listen(d) + ( history(d) + ( r )))))))); }; diff --git a/src/server/routes/blocks/methods/create.ts b/src/server/routes/blocks/methods/create.ts index 3b82a46d48..2f7dec6121 100644 --- a/src/server/routes/blocks/methods/create.ts +++ b/src/server/routes/blocks/methods/create.ts @@ -1,12 +1,9 @@ -import type {RoutesBase, TypeRouter} from '../../../../json-type/system/TypeRouter'; -import type {RouteDeps} from '../../types'; +import type {RouteDeps, Router, RouterBase} from '../../types'; import type {BlockId, BlockPatch} from '../schema'; export const create = - ({services}: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; - + ({t, services}: RouteDeps) => + (r: Router) => { const Request = t.Object( t.prop('id', t.Ref('BlockId')).options({ title: 'New block ID', @@ -20,17 +17,14 @@ export const create = const Response = t.obj; - const Func = t - .Function(Request, Response) - .options({ - title: 'Create Block', - intro: 'Creates a new block or applies patches to it.', - description: 'Creates a new block or applies patches to it.', - }) - .implement(async ({id, patches}) => { - const {block} = await services.blocks.create(id, patches); - return {}; - }); + const Func = t.Function(Request, Response).options({ + title: 'Create Block', + intro: 'Creates a new block or applies patches to it.', + description: 'Creates a new block or applies patches to it.', + }); - return router.fn('blocks.create', Func); + return r.prop('blocks.create', Func, async ({id, patches}) => { + const {block} = await services.blocks.create(id, patches); + return {}; + }); }; diff --git a/src/server/routes/blocks/methods/edit.ts b/src/server/routes/blocks/methods/edit.ts index f0505d937f..cea15b0ebd 100644 --- a/src/server/routes/blocks/methods/edit.ts +++ b/src/server/routes/blocks/methods/edit.ts @@ -1,11 +1,9 @@ -import type {RoutesBase, TypeRouter} from '../../../../json-type/system/TypeRouter'; -import type {RouteDeps} from '../../types'; +import type {RouteDeps, Router, RouterBase} from '../../types'; import type {BlockId, BlockPatch} from '../schema'; export const edit = - ({services}: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; + ({t, services}: RouteDeps) => + (r: Router) => { const PatchType = t.Ref('BlockPatch'); const Request = t.Object( @@ -34,19 +32,16 @@ export const edit = }), ); - const Func = t - .Function(Request, Response) - .options({ - title: 'Edit Block', - intro: 'Applies patches to an existing block.', - description: 'Applies patches to an existing document and returns the latest concurrent changes.', - }) - .implement(async ({id, patches}) => { - const res = await services.blocks.edit(id, patches); - return { - patches: res.patches, - }; - }); + const Func = t.Function(Request, Response).options({ + title: 'Edit Block', + intro: 'Applies patches to an existing block.', + description: 'Applies patches to an existing document and returns the latest concurrent changes.', + }); - return router.fn('blocks.edit', Func); + return r.prop('blocks.edit', Func, async ({id, patches}) => { + const res = await services.blocks.edit(id, patches); + return { + patches: res.patches, + }; + }); }; diff --git a/src/server/routes/blocks/methods/get.ts b/src/server/routes/blocks/methods/get.ts index b535bd0d93..7ff48d2d8a 100644 --- a/src/server/routes/blocks/methods/get.ts +++ b/src/server/routes/blocks/methods/get.ts @@ -1,12 +1,9 @@ -import type {RoutesBase, TypeRouter} from '../../../../json-type/system/TypeRouter'; -import type {RouteDeps} from '../../types'; +import type {RouteDeps, Router, RouterBase} from '../../types'; import type {Block, BlockId, BlockPatch} from '../schema'; export const get = - ({services}: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; - + ({t, services}: RouteDeps) => + (r: Router) => { const Request = t.Object( t.prop('id', t.Ref('BlockId')).options({ title: 'Block ID', @@ -22,20 +19,17 @@ export const get = }), ); - const Func = t - .Function(Request, Response) - .options({ - title: 'Read Block', - intro: 'Retrieves a block by ID.', - description: 'Fetches a block by ID.', - }) - .implement(async ({id}) => { - const {block, patches} = await services.blocks.get(id); - return { - block, - patches, - }; - }); + const Func = t.Function(Request, Response).options({ + title: 'Read Block', + intro: 'Retrieves a block by ID.', + description: 'Fetches a block by ID.', + }); - return router.fn('blocks.get', Func); + return r.prop('blocks.get', Func, async ({id}) => { + const {block, patches} = await services.blocks.get(id); + return { + block, + patches, + }; + }); }; diff --git a/src/server/routes/blocks/methods/history.ts b/src/server/routes/blocks/methods/history.ts index e25f22cda3..ea71ef4f2e 100644 --- a/src/server/routes/blocks/methods/history.ts +++ b/src/server/routes/blocks/methods/history.ts @@ -1,12 +1,9 @@ import type {BlockPatch, BlockId} from '../schema'; -import type {RoutesBase, TypeRouter} from '../../../../json-type/system/TypeRouter'; -import type {RouteDeps} from '../../types'; +import type {RouteDeps, Router, RouterBase} from '../../types'; export const history = - ({services}: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; - + ({t, services}: RouteDeps) => + (r: Router) => { const Request = t.Object( t.prop('id', t.Ref('BlockId')).options({ title: 'Block ID', @@ -29,17 +26,14 @@ export const history = }), ); - const Func = t - .Function(Request, Response) - .options({ - title: 'Block History', - intro: 'Fetch block history.', - description: 'Returns a list of specified change patches for a block.', - }) - .implement(async ({id, min, max}) => { - const {patches} = await services.blocks.history(id, min, max); - return {patches}; - }); + const Func = t.Function(Request, Response).options({ + title: 'Block History', + intro: 'Fetch block history.', + description: 'Returns a list of specified change patches for a block.', + }); - return router.fn('blocks.history', Func); + return r.prop('blocks.history', Func, async ({id, min, max}) => { + const {patches} = await services.blocks.history(id, min, max); + return {patches}; + }); }; diff --git a/src/server/routes/blocks/methods/listen.ts b/src/server/routes/blocks/methods/listen.ts index 1801953d04..f9272228af 100644 --- a/src/server/routes/blocks/methods/listen.ts +++ b/src/server/routes/blocks/methods/listen.ts @@ -1,12 +1,10 @@ import {switchMap} from 'rxjs'; -import type {RoutesBase, TypeRouter} from '../../../../json-type/system/TypeRouter'; -import type {RouteDeps} from '../../types'; +import type {RouteDeps, Router, RouterBase} from '../../types'; import type {BlockId, BlockPatch, Block} from '../schema'; export const listen = - ({services}: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; + ({t, services}: RouteDeps) => + (r: Router) => { const PatchType = t.Ref('BlockPatch'); const Request = t.Object( @@ -31,15 +29,12 @@ export const listen = }), ); - const Func = t - .Function$(Request, Response) - .options({ - title: 'Listen for block changes', - description: 'Subscribe to a block to receive updates when it changes.', - }) - .implement((req$) => { - return req$.pipe(switchMap(({id}) => services.pubsub.listen$(`__block:${id}`))) as any; - }); + const Func = t.Function$(Request, Response).options({ + title: 'Listen for block changes', + description: 'Subscribe to a block to receive updates when it changes.', + }); - return router.fn$('blocks.listen', Func); + return r.prop('blocks.listen', Func, (req$) => { + return req$.pipe(switchMap(({id}) => services.pubsub.listen$(`__block:${id}`))) as any; + }); }; diff --git a/src/server/routes/blocks/methods/remove.ts b/src/server/routes/blocks/methods/remove.ts index 1113444f1f..ae3400d397 100644 --- a/src/server/routes/blocks/methods/remove.ts +++ b/src/server/routes/blocks/methods/remove.ts @@ -1,12 +1,9 @@ -import type {RoutesBase, TypeRouter} from '../../../../json-type/system/TypeRouter'; -import type {RouteDeps} from '../../types'; +import type {RouteDeps, Router, RouterBase} from '../../types'; import type {BlockId} from '../schema'; export const remove = - ({services}: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; - + ({t, services}: RouteDeps) => + (r: Router) => { const Request = t.Object( t.prop('id', t.Ref('BlockId')).options({ title: 'Block ID', @@ -16,17 +13,14 @@ export const remove = const Response = t.obj; - const Func = t - .Function(Request, Response) - .options({ - title: 'Read Block', - intro: 'Retrieves a block by ID.', - description: 'Fetches a block by ID.', - }) - .implement(async ({id}) => { - await services.blocks.remove(id); - return {}; - }); + const Func = t.Function(Request, Response).options({ + title: 'Read Block', + intro: 'Retrieves a block by ID.', + description: 'Fetches a block by ID.', + }); - return router.fn('blocks.remove', Func); + return r.prop('blocks.remove', Func, async ({id}) => { + await services.blocks.remove(id); + return {}; + }); }; diff --git a/src/server/routes/index.ts b/src/server/routes/index.ts index 2868b330cf..8d51c1f401 100644 --- a/src/server/routes/index.ts +++ b/src/server/routes/index.ts @@ -1,21 +1,26 @@ import {routes} from './routes'; -import {system} from './system'; -import {TypeRouter} from '../../json-type/system/TypeRouter'; -import {TypeRouterCaller} from '../../reactive-rpc/common/rpc/caller/TypeRouterCaller'; import {RpcError} from '../../reactive-rpc/common/rpc/caller'; import {RpcValue} from '../../reactive-rpc/common/messages/Value'; +import {ObjectValueCaller} from '../../reactive-rpc/common/rpc/caller/ObjectValueCaller'; +import {system} from './system'; +import {ObjectValue} from '../../json-type-value/ObjectValue'; import type {Services} from '../services/Services'; import type {RouteDeps} from './types'; export const createRouter = (services: Services) => { - const router = new TypeRouter({system, routes: {}}); - const deps: RouteDeps = {services, router}; + const router = ObjectValue.create(system); + const deps: RouteDeps = { + services, + router, + system, + t: system.t, + }; return routes(deps)(router); }; export const createCaller = (services: Services) => { const router = createRouter(services); - const caller = new TypeRouterCaller({ + const caller = new ObjectValueCaller({ router, wrapInternalError: (error: unknown) => { if (error instanceof RpcValue) return error; diff --git a/src/server/routes/presence/index.ts b/src/server/routes/presence/index.ts index 6227153dc9..7a5e92f5be 100644 --- a/src/server/routes/presence/index.ts +++ b/src/server/routes/presence/index.ts @@ -2,13 +2,12 @@ import {update} from './methods/update'; import {listen} from './methods/listen'; import {remove} from './methods/remove'; import {PresenceEntry} from './schema'; -import type {RoutesBase, TypeRouter} from '../../../json-type/system/TypeRouter'; -import type {RouteDeps} from '../types'; +import type {RouteDeps, Router, RouterBase} from '../types'; export const presence = (d: RouteDeps) => - (r: TypeRouter) => { - r.system.alias('PresenceEntry', PresenceEntry); + (r: Router) => { + d.system.alias('PresenceEntry', PresenceEntry); // prettier-ignore return ( diff --git a/src/server/routes/presence/methods/listen.ts b/src/server/routes/presence/methods/listen.ts index 5baae8f85f..ee5406956a 100644 --- a/src/server/routes/presence/methods/listen.ts +++ b/src/server/routes/presence/methods/listen.ts @@ -1,13 +1,10 @@ import {map, switchMap} from 'rxjs'; import type {PresenceEntry, TPresenceEntry} from '../schema'; -import type {RoutesBase, TypeRouter} from '../../../../json-type/system/TypeRouter'; -import type {RouteDeps} from '../../types'; +import type {RouteDeps, Router, RouterBase} from '../../types'; export const listen = - ({services}: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; - + ({t, services}: RouteDeps) => + (r: Router) => { const Request = t.Object( t.prop('room', t.str).options({ title: 'Room ID', @@ -23,25 +20,22 @@ export const listen = }), ); - const Func = t - .Function$(Request, Response) - .options({ - title: 'Subscribe to a room.', - intro: 'Subscribes to presence updates in a room.', - description: - 'This method subscribes to presence updates in a room. ' + - 'It returns an array of all current presence entries in the room, and then emits an update whenever ' + - 'a presence entry is updated or deleted. ', - }) - .implement((req$) => { - return req$.pipe( - switchMap((req) => services.presence.listen$(req.room)), - map((entries: TPresenceEntry[]) => ({ - entries, - time: Date.now(), - })), - ); - }); + const Func = t.Function$(Request, Response).options({ + title: 'Subscribe to a room.', + intro: 'Subscribes to presence updates in a room.', + description: + 'This method subscribes to presence updates in a room. ' + + 'It returns an array of all current presence entries in the room, and then emits an update whenever ' + + 'a presence entry is updated or deleted. ', + }); - return router.fn$('presence.listen', Func); + return r.prop('presence.listen', Func, (req$) => { + return req$.pipe( + switchMap((req) => services.presence.listen$(req.room)), + map((entries: TPresenceEntry[]) => ({ + entries, + time: Date.now(), + })), + ); + }); }; diff --git a/src/server/routes/presence/methods/remove.ts b/src/server/routes/presence/methods/remove.ts index 398570a271..80f0085bea 100644 --- a/src/server/routes/presence/methods/remove.ts +++ b/src/server/routes/presence/methods/remove.ts @@ -1,11 +1,8 @@ -import type {RoutesBase, TypeRouter} from '../../../../json-type/system/TypeRouter'; -import type {RouteDeps} from '../../types'; +import type {RouteDeps, Router, RouterBase} from '../../types'; export const remove = - ({services}: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; - + ({t, services}: RouteDeps) => + (r: Router) => { const Request = t.Object( t.prop('room', t.str).options({ title: 'Room ID', @@ -19,17 +16,14 @@ export const remove = const Response = t.obj; - const Func = t - .Function(Request, Response) - .options({ - title: 'Remove a presence entry.', - intro: 'Removes a presence entry from a room and notifies all listeners.', - description: 'This method removes a presence entry from a room and notifies all listeners. ', - }) - .implement(async ({room, id}) => { - await services.presence.remove(room, id); - return {}; - }); + const Func = t.Function(Request, Response).options({ + title: 'Remove a presence entry.', + intro: 'Removes a presence entry from a room and notifies all listeners.', + description: 'This method removes a presence entry from a room and notifies all listeners. ', + }); - return router.fn('presence.remove', Func); + return r.prop('presence.remove', Func, async ({room, id}) => { + await services.presence.remove(room, id); + return {}; + }); }; diff --git a/src/server/routes/presence/methods/update.ts b/src/server/routes/presence/methods/update.ts index 5b300fc8a1..a45d65e4f3 100644 --- a/src/server/routes/presence/methods/update.ts +++ b/src/server/routes/presence/methods/update.ts @@ -1,16 +1,13 @@ import type {ResolveType} from '../../../../json-type'; import type {PresenceEntry} from '../schema'; -import type {RoutesBase, TypeRouter} from '../../../../json-type/system/TypeRouter'; -import type {RouteDeps} from '../../types'; +import type {RouteDeps, Router, RouterBase} from '../../types'; /** Entry TLL in seconds. */ const ttl = 30; export const update = - ({services}: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; - + ({t, services}: RouteDeps) => + (r: Router) => { const Request = t .Object( t.prop('room', t.str).options({ @@ -58,23 +55,20 @@ export const update = title: 'Presence update response', }); - const Func = t - .Function(Request, Response) - .options({ - title: 'Update presence entry', - intro: 'Update a presence entry in a room.', - description: - 'This method updates a presence entry in a room. ' + - `The entry is automatically removed after ${ttl} seconds. ` + - `Every time the entry is updated, the TTL is reset to ${ttl} seconds.`, - }) - .implement(async ({room, id, data}) => { - const entry = (await services.presence.update(room, id, ttl * 1000, data)) as ResolveType; - return { - entry, - time: Date.now(), - }; - }); + const Func = t.Function(Request, Response).options({ + title: 'Update presence entry', + intro: 'Update a presence entry in a room.', + description: + 'This method updates a presence entry in a room. ' + + `The entry is automatically removed after ${ttl} seconds. ` + + `Every time the entry is updated, the TTL is reset to ${ttl} seconds.`, + }); - return router.fn('presence.update', Func); + return r.prop('presence.update', Func, async ({room, id, data}) => { + const entry = (await services.presence.update(room, id, ttl * 1000, data)) as ResolveType; + return { + entry, + time: Date.now(), + }; + }); }; diff --git a/src/server/routes/pubsub/index.ts b/src/server/routes/pubsub/index.ts index aacf55c136..dea1b491be 100644 --- a/src/server/routes/pubsub/index.ts +++ b/src/server/routes/pubsub/index.ts @@ -1,10 +1,9 @@ import {publish} from './publish'; import {listen} from './listen'; -import type {RoutesBase, TypeRouter} from '../../../json-type/system/TypeRouter'; -import type {RouteDeps} from '../types'; +import type {RouteDeps, Router, RouterBase} from '../types'; // prettier-ignore -export const pubsub = (d: RouteDeps) => (r: TypeRouter) => +export const pubsub = (d: RouteDeps) => (r: Router) => ( publish(d) ( listen(d) ( r ))); diff --git a/src/server/routes/pubsub/listen.ts b/src/server/routes/pubsub/listen.ts index ec8e05cd02..c5e833a290 100644 --- a/src/server/routes/pubsub/listen.ts +++ b/src/server/routes/pubsub/listen.ts @@ -1,33 +1,30 @@ import {map, switchMap} from 'rxjs'; -import type {RoutesBase, TypeRouter} from '../../../json-type/system/TypeRouter'; -import type {RouteDeps} from '../types'; +import type {RouteDeps, Router, RouterBase} from '../types'; export const listen = - ({services}: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; - - const req = t.Object( + ({t, services}: RouteDeps) => + (r: Router) => { + const Request = t.Object( t.prop('channel', t.str).options({ title: 'Channel name', description: 'The name of the channel to subscribe to.', }), ); - const res = t.Object( + const Response = t.Object( t.prop('message', t.any).options({ title: 'Subscription message', description: 'A message received from the channel. Emitted every time a message is published to the channel.', }), ); - const func = t.Function$(req, res).implement((req) => { + const Func = t.Function$(Request, Response); + + return r.prop('pubsub.listen', Func, (req) => { const response = req.pipe( switchMap((req) => services.pubsub.listen$(req.channel)), map((message: any) => ({message})), ); return response; }); - - return router.fn$('pubsub.listen', func); }; diff --git a/src/server/routes/pubsub/publish.ts b/src/server/routes/pubsub/publish.ts index efe6c5c348..aba6c557e1 100644 --- a/src/server/routes/pubsub/publish.ts +++ b/src/server/routes/pubsub/publish.ts @@ -1,13 +1,9 @@ -import type {RoutesBase, TypeRouter} from '../../../json-type/system/TypeRouter'; -import type {MyCtx} from '../../services/types'; -import type {RouteDeps} from '../types'; +import type {RouteDeps, Router, RouterBase} from '../types'; export const publish = - ({services}: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; - - const req = t.Object( + ({t, services}: RouteDeps) => + (r: Router) => { + const Request = t.Object( t.prop('channel', t.str).options({ title: 'Channel name', description: 'The name of the channel to publish to.', @@ -18,25 +14,22 @@ export const publish = }), ); - const res = t.obj.options({ + const Response = t.obj.options({ title: 'Publish response', description: 'An empty object.', }); - const func = t - .Function(req, res) - .options({ - title: 'Publish to channel', - intro: 'Publish a message to a channel.', - description: - 'This method publishes a message to a global channel with the given `channel` name. ' + - 'All subscribers to the channel will receive the message. The `message` can be any value. ' + - 'The most efficient way to publish a message is to send a primitive or a `Uint8Array` buffer.', - }) - .implement(async ({channel, message}) => { - services.pubsub.publish(channel, message); - return {}; - }); + const Func = t.Function(Request, Response).options({ + title: 'Publish to channel', + intro: 'Publish a message to a channel.', + description: + 'This method publishes a message to a global channel with the given `channel` name. ' + + 'All subscribers to the channel will receive the message. The `message` can be any value. ' + + 'The most efficient way to publish a message is to send a primitive or a `Uint8Array` buffer.', + }); - return router.fn('pubsub.publish', func); + return r.prop('pubsub.publish', Func, async ({channel, message}) => { + services.pubsub.publish(channel, message); + return {}; + }); }; diff --git a/src/server/routes/routes.ts b/src/server/routes/routes.ts index 5a0dfd216f..3459273faf 100644 --- a/src/server/routes/routes.ts +++ b/src/server/routes/routes.ts @@ -2,11 +2,12 @@ import {util} from './util'; import {pubsub} from './pubsub'; import {presence} from './presence'; import {blocks} from './blocks'; -import type {RoutesBase, TypeRouter} from '../../json-type/system/TypeRouter'; import type {RouteDeps} from './types'; +import type {ObjectValue} from '../../json-type-value/ObjectValue'; +import type {ObjectType} from '../../json-type'; // prettier-ignore -export const routes = (d: RouteDeps) => (r: TypeRouter) => +export const routes = (d: RouteDeps) => >(r: ObjectValue) => ( util(d) ( pubsub(d) ( presence(d) diff --git a/src/server/routes/types.ts b/src/server/routes/types.ts index c6cea26e88..74b441138b 100644 --- a/src/server/routes/types.ts +++ b/src/server/routes/types.ts @@ -1,7 +1,14 @@ -import type {TypeRouter} from '../../json-type/system/TypeRouter'; +import type {ObjectType, TypeSystem} from '../../json-type'; +import type {ObjectValue} from '../../json-type-value/ObjectValue'; +import type {TypeBuilder} from '../../json-type/type/TypeBuilder'; import type {Services} from '../services/Services'; export interface RouteDeps { services: Services; - router: TypeRouter; + system: TypeSystem; + t: TypeBuilder; + router: ObjectValue; } + +export type RouterBase = ObjectType; +export type Router = ObjectValue; diff --git a/src/server/routes/util/index.ts b/src/server/routes/util/index.ts index d4f085a514..e9c6664b47 100644 --- a/src/server/routes/util/index.ts +++ b/src/server/routes/util/index.ts @@ -1,33 +1,28 @@ -import type {RoutesBase, TypeRouter} from '../../../json-type/system/TypeRouter'; -import type {MyCtx} from '../../services/types'; -import type {RouteDeps} from '../types'; +import type {RouteDeps, Router, RouterBase} from '../types'; export const ping = - (deps: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; - const req = t.any; - const res = t.Const('pong'); - const func = t.Function(req, res).implement(async () => { + ({t}: RouteDeps) => + (r: Router) => { + const Request = t.any; + const Response = t.Const('pong'); + const Func = t.Function(Request, Response); + return r.prop('util.ping', Func, async () => { return 'pong'; }); - return router.fn('util.ping', func); }; export const echo = - (deps: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; - const req = t.any; - const res = t.any; - const func = t.Function(req, res).implement(async (msg) => msg); - return router.fn('util.echo', func); + ({t}: RouteDeps) => + (r: Router) => { + const Request = t.any; + const Response = t.any; + const Func = t.Function(Request, Response); + return r.prop('util.echo', Func, async (msg) => msg); }; export const info = - ({services}: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; + ({t, services}: RouteDeps) => + (r: Router) => { const Request = t.any; const Response = t.Object( t.prop('now', t.num), @@ -40,7 +35,8 @@ export const info = ), ), ); - const Func = t.Function(Request, Response).implement(async () => { + const Func = t.Function(Request, Response); + return r.prop('util.info', Func, async () => { return { now: Date.now(), stats: { @@ -50,27 +46,25 @@ export const info = }, }; }); - return router.fn('util.info', Func); }; export const schema = - (deps: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; + ({t, router}: RouteDeps) => + (r: Router) => { const Request = t.any; const Response = t.Object(t.prop('typescript', t.str)); - const Func = t.Function(Request, Response).implement(async () => { + const Func = t.Function(Request, Response); + return r.prop('util.schema', Func, async () => { return { - typescript: deps.router.toTypeScript(), + typescript: router.toTypeScript(), }; }); - return router.fn('util.schema', Func); }; // prettier-ignore -export const util = (deps: RouteDeps) => (r: TypeRouter) => - ( ping(deps) - ( echo(deps) - ( info(deps) - ( schema(deps) +export const util = (d: RouteDeps) => (r: Router) => + ( ping(d) + ( echo(d) + ( info(d) + ( schema(d) ( r )))));