From dd824a7d3562b20b2bf4d9adff4cea3a764dc365 Mon Sep 17 00:00:00 2001 From: streamich Date: Thu, 7 Dec 2023 22:43:52 +0100 Subject: [PATCH 1/8] =?UTF-8?q?feat(json-type-value):=20=F0=9F=8E=B8=20add?= =?UTF-8?q?=20ability=20to=20get=20object=20fieeld?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-type-value/ObjectValue.ts | 15 +++++++++++++-- src/json-type-value/__tests__/ObjectValue.spec.ts | 11 +++++++++++ src/json-type/type/classes/AbstractType.ts | 6 ++++++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 src/json-type-value/__tests__/ObjectValue.spec.ts diff --git a/src/json-type-value/ObjectValue.ts b/src/json-type-value/ObjectValue.ts index 1dc8b7a36e..05d2b7c46d 100644 --- a/src/json-type-value/ObjectValue.ts +++ b/src/json-type-value/ObjectValue.ts @@ -4,8 +4,11 @@ import type {ResolveType} 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 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 @@ -51,6 +54,14 @@ export class ObjectValue> extends Value { return new ObjectValue(extendedType, extendedData) as any; } + public get>>(key: K): Value>[K] extends classes.Type ? ObjectValueToTypeMap>[K] : never> { + 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/__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..4f684197c0 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 = type.system; + if (!system) throw new Error('NO_SYSTEM'); + return system; + } + public getTypeName(): S['__t'] { return this.schema.__t; } From 00fd482baf3adba02bc95e9eb8cfaac951dcdc66 Mon Sep 17 00:00:00 2001 From: streamich Date: Thu, 7 Dec 2023 23:11:01 +0100 Subject: [PATCH 2/8] =?UTF-8?q?feat(reactive-rpc):=20=F0=9F=8E=B8=20add=20?= =?UTF-8?q?initial=20ObjectValueCaller=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-type-value/ObjectValue.ts | 7 +- .../common/rpc/caller/ObjectValueCaller.ts | 116 ++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts diff --git a/src/json-type-value/ObjectValue.ts b/src/json-type-value/ObjectValue.ts index 05d2b7c46d..d7be9ab4ac 100644 --- a/src/json-type-value/ObjectValue.ts +++ b/src/json-type-value/ObjectValue.ts @@ -1,10 +1,11 @@ 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'; 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; @@ -25,6 +26,8 @@ export type ObjectValueToTypeMap = ToObject<{[K in keyof F]: ObjectFieldToTup // never; export class ObjectValue> extends Value { + public static create = (system: TypeSystem) => new ObjectValue(system.t.obj, {}); + public field>( field: F, data: ResolveType>, @@ -54,7 +57,7 @@ export class ObjectValue> extends Value { return new ObjectValue(extendedType, extendedData) as any; } - public get>>(key: K): Value>[K] extends classes.Type ? ObjectValueToTypeMap>[K] : never> { + public get>>(key: K): Value>[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; 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..168e002b95 --- /dev/null +++ b/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts @@ -0,0 +1,116 @@ +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'> { + value: V; +} + +export class ObjectValueCaller>, Ctx = unknown> extends RpcCaller { + protected readonly value: V; + protected readonly system: TypeSystem; + protected readonly methods = new Map | StreamingRpcMethod>(); + + constructor({value, ...rest}: ObjectValueCallerOptions) { + super({ + ...rest, + getMethod: (name) => this.get(name) as any as StaticRpcMethod | StreamingRpcMethod, + }); + this.value = 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.value.get(id) as Value; + if (!fn || !(fn.type instanceof FunctionType || fn.type instanceof FunctionStreamingType)) { + // const system = type.getSystem(); + // fn = system.t.Function(t.any, fn); + throw new Error('NOT_FUNCTION'); + } + const {req, res} = fn.type; + const call = fn.data; + const validator = fn.type.req.validator('object'); + const requestSchema = (fn.type.req as AbstractType).getSchema(); + const isRequestVoid = requestSchema.__t === 'const' && requestSchema.value === undefined; + const validate = isRequestVoid + ? () => {} + : (req: unknown) => { + const error = validator(req); + if (error) { + const message = error.message + (Array.isArray(error?.path) ? ' Path: /' + error.path.join('/') : ''); + throw RpcError.value(RpcError.validation(message, error)); + } + }; + method = + fn 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; + } +} From 167fcdc8d2e5afb10269a3d6778941c155a499c4 Mon Sep 17 00:00:00 2001 From: streamich Date: Thu, 7 Dec 2023 23:45:19 +0100 Subject: [PATCH 3/8] =?UTF-8?q?fix(reactive-rpc):=20=F0=9F=90=9B=20correct?= =?UTF-8?q?ly=20construct=20caller=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/rpc/caller/ObjectValueCaller.ts | 12 ++++++------ .../caller/__tests__/ObjectValueCaller.spec.ts | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 src/reactive-rpc/common/rpc/caller/__tests__/ObjectValueCaller.spec.ts diff --git a/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts b/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts index 168e002b95..d89813fa50 100644 --- a/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts +++ b/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts @@ -62,22 +62,22 @@ export class ObjectValueCaller>, Ctx = unk // fn = system.t.Function(t.any, fn); throw new Error('NOT_FUNCTION'); } - const {req, res} = fn.type; + const fnType = fn.type as FunctionType | FunctionStreamingType; + const {req, res} = fnType; const call = fn.data; - const validator = fn.type.req.validator('object'); - const requestSchema = (fn.type.req as AbstractType).getSchema(); + 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 = validator(req); + 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 = - fn instanceof FunctionType + 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); 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..97339d5129 --- /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 value = 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({value}); + 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'}); +}); From 7671b5ec3c662d19d730e8abf8abfb49bdf7d04b Mon Sep 17 00:00:00 2001 From: streamich Date: Thu, 7 Dec 2023 23:49:41 +0100 Subject: [PATCH 4/8] =?UTF-8?q?style:=20=F0=9F=92=84=20run=20Prettier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-type-value/ObjectValue.ts | 10 ++++++++-- .../common/rpc/caller/ObjectValueCaller.ts | 17 ++++++++++++++--- .../caller/__tests__/ObjectValueCaller.spec.ts | 4 ++-- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/json-type-value/ObjectValue.ts b/src/json-type-value/ObjectValue.ts index d7be9ab4ac..992e01f0cf 100644 --- a/src/json-type-value/ObjectValue.ts +++ b/src/json-type-value/ObjectValue.ts @@ -57,9 +57,15 @@ export class ObjectValue> extends Value { return new ObjectValue(extendedType, extendedData) as any; } - public get>>(key: K): Value>[K] extends classes.Type ? ObjectValueToTypeMap>[K] : classes.Type> { + 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'); + if (!field) throw new Error('NO_FIELD'); const type = field.value; const data = this.data[key]; return new Value(type, data) as any; diff --git a/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts b/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts index d89813fa50..46b93b6421 100644 --- a/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts +++ b/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts @@ -3,7 +3,15 @@ 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 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'; @@ -53,7 +61,9 @@ export class ObjectValueCaller>, Ctx = unk this.system = system; } - public get>(id: K): MethodDefinition[K]> | undefined { + public get>( + id: K, + ): MethodDefinition[K]> | undefined { let method = this.methods.get(id as string) as any; if (method) return method; const fn = this.value.get(id) as Value; @@ -77,7 +87,8 @@ export class ObjectValueCaller>, Ctx = unk throw RpcError.value(RpcError.validation(message, error)); } }; - method = fnType instanceof FunctionType + 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); diff --git a/src/reactive-rpc/common/rpc/caller/__tests__/ObjectValueCaller.spec.ts b/src/reactive-rpc/common/rpc/caller/__tests__/ObjectValueCaller.spec.ts index 97339d5129..6fe037656b 100644 --- a/src/reactive-rpc/common/rpc/caller/__tests__/ObjectValueCaller.spec.ts +++ b/src/reactive-rpc/common/rpc/caller/__tests__/ObjectValueCaller.spec.ts @@ -6,8 +6,8 @@ test('can execute simple calls', async () => { const system = new TypeSystem(); const {t} = system; const value = 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); + .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({value}); const res1 = await caller.call('ping', null, {}); expect(res1.data).toBe('pong'); From 538838935f2b111cb9f22b60d46463ce4a9a602d Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 8 Dec 2023 01:20:24 +0100 Subject: [PATCH 5/8] =?UTF-8?q?refactor(reactive-rpc):=20=F0=9F=92=A1=20up?= =?UTF-8?q?date=20demo=20server=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/rpc/caller/ObjectValueCaller.ts | 14 +- .../__tests__/ObjectValueCaller.spec.ts | 4 +- .../common/testing/buildE2eClient.ts | 3 +- src/server/routes/blocks/index.ts | 37 +++-- src/server/routes/blocks/methods/create.ts | 56 ++++---- src/server/routes/blocks/methods/edit.ts | 87 ++++++------ src/server/routes/blocks/methods/get.ts | 66 ++++----- src/server/routes/blocks/methods/history.ts | 74 +++++----- src/server/routes/blocks/methods/listen.ts | 71 +++++----- src/server/routes/blocks/methods/remove.ts | 48 +++---- src/server/routes/index.ts | 18 ++- src/server/routes/presence/index.ts | 9 +- src/server/routes/presence/methods/listen.ts | 76 +++++----- src/server/routes/presence/methods/remove.ts | 56 ++++---- src/server/routes/presence/methods/update.ts | 132 +++++++++--------- src/server/routes/pubsub/index.ts | 5 +- src/server/routes/pubsub/listen.ts | 51 +++---- src/server/routes/pubsub/publish.ts | 67 ++++----- src/server/routes/routes.ts | 5 +- src/server/routes/types.ts | 11 +- src/server/routes/util/index.ts | 122 +++++++--------- 21 files changed, 471 insertions(+), 541 deletions(-) diff --git a/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts b/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts index 46b93b6421..baa93e32f6 100644 --- a/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts +++ b/src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts @@ -42,20 +42,20 @@ type MethodDefinition = F extends FunctionType export interface ObjectValueCallerOptions>, Ctx = unknown> extends Omit, 'getMethod'> { - value: V; + router: V; } export class ObjectValueCaller>, Ctx = unknown> extends RpcCaller { - protected readonly value: V; + protected readonly router: V; protected readonly system: TypeSystem; protected readonly methods = new Map | StreamingRpcMethod>(); - constructor({value, ...rest}: ObjectValueCallerOptions) { + constructor({router: value, ...rest}: ObjectValueCallerOptions) { super({ ...rest, getMethod: (name) => this.get(name) as any as StaticRpcMethod | StreamingRpcMethod, }); - this.value = value; + this.router = value; const system = value.type.system; if (!system) throw new Error('NO_SYSTEM'); this.system = system; @@ -66,11 +66,9 @@ export class ObjectValueCaller>, Ctx = unk ): MethodDefinition[K]> | undefined { let method = this.methods.get(id as string) as any; if (method) return method; - const fn = this.value.get(id) as Value; + const fn = this.router.get(id) as Value; if (!fn || !(fn.type instanceof FunctionType || fn.type instanceof FunctionStreamingType)) { - // const system = type.getSystem(); - // fn = system.t.Function(t.any, fn); - throw new Error('NOT_FUNCTION'); + return undefined; } const fnType = fn.type as FunctionType | FunctionStreamingType; const {req, res} = fnType; diff --git a/src/reactive-rpc/common/rpc/caller/__tests__/ObjectValueCaller.spec.ts b/src/reactive-rpc/common/rpc/caller/__tests__/ObjectValueCaller.spec.ts index 6fe037656b..65b95cb1e4 100644 --- a/src/reactive-rpc/common/rpc/caller/__tests__/ObjectValueCaller.spec.ts +++ b/src/reactive-rpc/common/rpc/caller/__tests__/ObjectValueCaller.spec.ts @@ -5,10 +5,10 @@ import {ObjectValueCaller} from '../ObjectValueCaller'; test('can execute simple calls', async () => { const system = new TypeSystem(); const {t} = system; - const value = ObjectValue.create(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({value}); + 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'}, {}); diff --git a/src/reactive-rpc/common/testing/buildE2eClient.ts b/src/reactive-rpc/common/testing/buildE2eClient.ts index 68c6259ef0..da0dc579ee 100644 --- a/src/reactive-rpc/common/testing/buildE2eClient.ts +++ b/src/reactive-rpc/common/testing/buildE2eClient.ts @@ -11,6 +11,7 @@ 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'; export interface BuildE2eClientOptions { /** @@ -64,7 +65,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( diff --git a/src/server/routes/blocks/index.ts b/src/server/routes/blocks/index.ts index ebe2dce3c7..ecbd4c040f 100644 --- a/src/server/routes/blocks/index.ts +++ b/src/server/routes/blocks/index.ts @@ -5,24 +5,23 @@ 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); +export const blocks = (d: RouteDeps) => (r: Router) => { + const {system} = d; - // prettier-ignore - return ( - ( create(d) - ( get(d) - ( remove(d) - ( edit(d) - ( listen(d) - ( history(d) - ( r )))))))); - }; + 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 )))))))); +}; diff --git a/src/server/routes/blocks/methods/create.ts b/src/server/routes/blocks/methods/create.ts index 3b82a46d48..5609899fd6 100644 --- a/src/server/routes/blocks/methods/create.ts +++ b/src/server/routes/blocks/methods/create.ts @@ -1,36 +1,30 @@ -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; +export const create = ({t, services}: RouteDeps) => (r: Router) => { + const Request = t.Object( + t.prop('id', t.Ref('BlockId')).options({ + title: 'New block ID', + description: 'The ID of the new block.', + }), + t.prop('patches', t.Array(t.Ref('BlockPatch'))).options({ + title: 'Patches', + description: 'The patches to apply to the document.', + }), + ); - const Request = t.Object( - t.prop('id', t.Ref('BlockId')).options({ - title: 'New block ID', - description: 'The ID of the new block.', - }), - t.prop('patches', t.Array(t.Ref('BlockPatch'))).options({ - title: 'Patches', - description: 'The patches to apply to the document.', - }), - ); + const Response = t.obj; - 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.', + }); - 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 {}; - }); - - 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..9f129cadb0 100644 --- a/src/server/routes/blocks/methods/edit.ts +++ b/src/server/routes/blocks/methods/edit.ts @@ -1,52 +1,47 @@ -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; - const PatchType = t.Ref('BlockPatch'); +export const edit = ({t, services}: RouteDeps) => (r: Router) => { + const PatchType = t.Ref('BlockPatch'); - const Request = t.Object( - t.prop('id', t.Ref('BlockId')).options({ - title: 'Document ID', - description: 'The ID of the document to apply the patch to.', - }), - // This can be inferred from the "seq" of the first patch: - // t.prop('seq', t.Ref('BlockSeq')).options({ - // title: 'Last known sequence number', - // description: - // 'The last known sequence number of the document. ' + - // 'If the document has changed since this sequence number, ' + - // 'the response will contain all the necessary patches for the client to catch up.', - // }), - t.prop('patches', t.Array(PatchType)).options({ - title: 'Patches', - description: 'The patches to apply to the document.', - }), - ); + const Request = t.Object( + t.prop('id', t.Ref('BlockId')).options({ + title: 'Document ID', + description: 'The ID of the document to apply the patch to.', + }), + // This can be inferred from the "seq" of the first patch: + // t.prop('seq', t.Ref('BlockSeq')).options({ + // title: 'Last known sequence number', + // description: + // 'The last known sequence number of the document. ' + + // 'If the document has changed since this sequence number, ' + + // 'the response will contain all the necessary patches for the client to catch up.', + // }), + t.prop('patches', t.Array(PatchType)).options({ + title: 'Patches', + description: 'The patches to apply to the document.', + }), + ); - const Response = t.Object( - t.prop('patches', t.Array(PatchType)).options({ - title: 'Latest patches', - description: 'The list of patches that the client might have missed and should apply to the document.', - }), - ); + const Response = t.Object( + t.prop('patches', t.Array(PatchType)).options({ + title: 'Latest patches', + description: 'The list of patches that the client might have missed and should apply to the document.', + }), + ); - 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..a9bb77ac49 100644 --- a/src/server/routes/blocks/methods/get.ts +++ b/src/server/routes/blocks/methods/get.ts @@ -1,41 +1,35 @@ -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; +export const get = ({t, services}: RouteDeps) => (r: Router) => { + const Request = t.Object( + t.prop('id', t.Ref('BlockId')).options({ + title: 'Block ID', + description: 'The ID of the block to retrieve.', + }), + ); - const Request = t.Object( - t.prop('id', t.Ref('BlockId')).options({ - title: 'Block ID', - description: 'The ID of the block to retrieve.', - }), - ); + const Response = t.Object( + t.prop('block', t.Ref('Block').options({})), + t.prop('patches', t.Array(t.Ref('BlockPatch'))).options({ + title: 'Patches', + description: 'The list of all patches.', + }), + ); - const Response = t.Object( - t.prop('block', t.Ref('Block').options({})), - t.prop('patches', t.Array(t.Ref('BlockPatch'))).options({ - title: 'Patches', - description: 'The list of all patches.', - }), - ); + const Func = t + .Function(Request, Response) + .options({ + title: 'Read Block', + intro: 'Retrieves a block by ID.', + description: 'Fetches a block by ID.', + }); - 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, - }; - }); - - 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..86b215a777 100644 --- a/src/server/routes/blocks/methods/history.ts +++ b/src/server/routes/blocks/methods/history.ts @@ -1,45 +1,39 @@ 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; +export const history = ({t, services}: RouteDeps) => (r: Router) => { + const Request = t.Object( + t.prop('id', t.Ref('BlockId')).options({ + title: 'Block ID', + description: 'The ID of the block.', + }), + t.prop('max', t.num.options({format: 'u32'})).options({ + title: 'Max', + description: 'The maximum sequence number to return.', + }), + t.prop('min', t.num.options({format: 'u32'})).options({ + title: 'Min', + description: 'The minimum sequence number to return.', + }), + ); - const Request = t.Object( - t.prop('id', t.Ref('BlockId')).options({ - title: 'Block ID', - description: 'The ID of the block.', - }), - t.prop('max', t.num.options({format: 'u32'})).options({ - title: 'Max', - description: 'The maximum sequence number to return.', - }), - t.prop('min', t.num.options({format: 'u32'})).options({ - title: 'Min', - description: 'The minimum sequence number to return.', - }), - ); + const Response = t.Object( + t.prop('patches', t.Array(t.Ref('BlockPatch'))).options({ + title: 'Patches', + description: 'The list of patches.', + }), + ); - const Response = t.Object( - t.prop('patches', t.Array(t.Ref('BlockPatch'))).options({ - title: 'Patches', - description: 'The list of 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.', + }); - 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}; - }); - - 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..5bc53bcd4b 100644 --- a/src/server/routes/blocks/methods/listen.ts +++ b/src/server/routes/blocks/methods/listen.ts @@ -1,45 +1,40 @@ 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; - const PatchType = t.Ref('BlockPatch'); +export const listen = ({t, services}: RouteDeps) => (r: Router) => { + const PatchType = t.Ref('BlockPatch'); - const Request = t.Object( - t.prop('id', t.Ref('BlockId')).options({ - title: 'Block ID', - description: 'The ID of the block to subscribe to.', - }), - ); + const Request = t.Object( + t.prop('id', t.Ref('BlockId')).options({ + title: 'Block ID', + description: 'The ID of the block to subscribe to.', + }), + ); - const Response = t.Object( - t.propOpt('deleted', t.Boolean()).options({ - title: 'Deleted', - description: 'Emitted only when the block is deleted.', - }), - t.propOpt('block', t.Ref('Block')).options({ - title: 'Block', - description: 'The whole block object, emitted only when the block is created.', - }), - t.propOpt('patches', t.Array(PatchType)).options({ - title: 'Latest patches', - description: 'Patches that have been applied to the block.', - }), - ); + const Response = t.Object( + t.propOpt('deleted', t.Boolean()).options({ + title: 'Deleted', + description: 'Emitted only when the block is deleted.', + }), + t.propOpt('block', t.Ref('Block')).options({ + title: 'Block', + description: 'The whole block object, emitted only when the block is created.', + }), + t.propOpt('patches', t.Array(PatchType)).options({ + title: 'Latest patches', + description: 'Patches that have been applied to the block.', + }), + ); - 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..e092c28385 100644 --- a/src/server/routes/blocks/methods/remove.ts +++ b/src/server/routes/blocks/methods/remove.ts @@ -1,32 +1,26 @@ -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; +export const remove = ({t, services}: RouteDeps) => (r: Router) => { + const Request = t.Object( + t.prop('id', t.Ref('BlockId')).options({ + title: 'Block ID', + description: 'The ID of the block to delete.', + }), + ); - const Request = t.Object( - t.prop('id', t.Ref('BlockId')).options({ - title: 'Block ID', - description: 'The ID of the block to delete.', - }), - ); + const Response = t.obj; - 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.', + }); - 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 {}; - }); - - 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..130f395447 100644 --- a/src/server/routes/index.ts +++ b/src/server/routes/index.ts @@ -1,21 +1,27 @@ 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 {TypeSystem} from '../../json-type'; +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 system = new TypeSystem(); + 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..a95dd2707d 100644 --- a/src/server/routes/presence/index.ts +++ b/src/server/routes/presence/index.ts @@ -2,13 +2,10 @@ 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); +export const presence = (d: RouteDeps) => (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..54ec376971 100644 --- a/src/server/routes/presence/methods/listen.ts +++ b/src/server/routes/presence/methods/listen.ts @@ -1,47 +1,41 @@ 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; +export const listen = ({t, services}: RouteDeps) => (r: Router) => { + const Request = t.Object( + t.prop('room', t.str).options({ + title: 'Room ID', + description: 'The ID of the room to subscribe to.', + }), + ); - const Request = t.Object( - t.prop('room', t.str).options({ - title: 'Room ID', - description: 'The ID of the room to subscribe to.', - }), - ); - - const Response = t.Object( - t.prop('entries', t.Array(t.Ref('PresenceEntry'))), - t.prop('time', t.num).options({ - title: 'Current time', - description: 'The current server time in milliseconds since the UNIX epoch.', - }), - ); + const Response = t.Object( + t.prop('entries', t.Array(t.Ref('PresenceEntry'))), + t.prop('time', t.num).options({ + title: 'Current time', + description: 'The current server time in milliseconds since the UNIX epoch.', + }), + ); - 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..75015d3b05 100644 --- a/src/server/routes/presence/methods/remove.ts +++ b/src/server/routes/presence/methods/remove.ts @@ -1,35 +1,29 @@ -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; +export const remove = ({t, services}: RouteDeps) => (r: Router) => { + const Request = t.Object( + t.prop('room', t.str).options({ + title: 'Room ID', + description: 'The ID of the room from which to remove the entry.', + }), + t.prop('id', t.str).options({ + title: 'ID of the entry', + description: 'The ID of the entry to remove.', + }), + ); - const Request = t.Object( - t.prop('room', t.str).options({ - title: 'Room ID', - description: 'The ID of the room from which to remove the entry.', - }), - t.prop('id', t.str).options({ - title: 'ID of the entry', - description: 'The ID of the entry to remove.', - }), - ); + const Response = t.obj; - 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. ', + }); - 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 {}; - }); - - 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..a43c5cb3f3 100644 --- a/src/server/routes/presence/methods/update.ts +++ b/src/server/routes/presence/methods/update.ts @@ -1,80 +1,74 @@ 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; - - const Request = t - .Object( - t.prop('room', t.str).options({ - title: 'Room ID', - description: 'The ID of the room to update.', - }), - t.prop('id', t.str).options({ - title: 'ID of the entry', - description: 'The ID of the entry to update.', - }), - t.prop('data', t.any).options({ - title: 'Entry data', - description: 'A map of key-value pairs to update. The object is merged with the existing entry data, if any.', - }), - ) - .options({ - examples: [ - { - title: 'Update user entry', - description: - 'The data section of the entry is merged with the existing data. ' + - 'It can contain any key-value pairs. For example, the `cursor` property is used to store the ' + - 'current cursor position of the user in the room.', - value: { - room: 'my-room', - id: 'user-1', - data: { - name: 'John Doe', - cursor: [123, 456], - }, +export const update = ({t, services}: RouteDeps) => (r: Router) => { + const Request = t + .Object( + t.prop('room', t.str).options({ + title: 'Room ID', + description: 'The ID of the room to update.', + }), + t.prop('id', t.str).options({ + title: 'ID of the entry', + description: 'The ID of the entry to update.', + }), + t.prop('data', t.any).options({ + title: 'Entry data', + description: 'A map of key-value pairs to update. The object is merged with the existing entry data, if any.', + }), + ) + .options({ + examples: [ + { + title: 'Update user entry', + description: + 'The data section of the entry is merged with the existing data. ' + + 'It can contain any key-value pairs. For example, the `cursor` property is used to store the ' + + 'current cursor position of the user in the room.', + value: { + room: 'my-room', + id: 'user-1', + data: { + name: 'John Doe', + cursor: [123, 456], }, }, - ], - }); + }, + ], + }); - const Response = t - .Object( - t.prop('entry', t.Ref('PresenceEntry')), - t.prop('time', t.num).options({ - title: 'Current time', - description: 'The current server time in milliseconds since the UNIX epoch.', - }), - ) - .options({ - title: 'Presence update response', - }); + const Response = t + .Object( + t.prop('entry', t.Ref('PresenceEntry')), + t.prop('time', t.num).options({ + title: 'Current time', + description: 'The current server time in milliseconds since the UNIX epoch.', + }), + ) + .options({ + 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..f210e580d6 100644 --- a/src/server/routes/pubsub/listen.ts +++ b/src/server/routes/pubsub/listen.ts @@ -1,33 +1,28 @@ 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; +export const listen = ({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 req = t.Object( - t.prop('channel', t.str).options({ - title: 'Channel name', - description: 'The name of the channel to subscribe to.', - }), - ); - - const res = 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 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 response = req.pipe( - switchMap((req) => services.pubsub.listen$(req.channel)), - map((message: any) => ({message})), - ); - return response; - }); + const Func = t.Function$(Request, Response); - return router.fn$('pubsub.listen', func); - }; + return r.prop('pubsub.listen', Func, (req) => { + const response = req.pipe( + switchMap((req) => services.pubsub.listen$(req.channel)), + map((message: any) => ({message})), + ); + return response; + }); +}; diff --git a/src/server/routes/pubsub/publish.ts b/src/server/routes/pubsub/publish.ts index efe6c5c348..5f21632b2f 100644 --- a/src/server/routes/pubsub/publish.ts +++ b/src/server/routes/pubsub/publish.ts @@ -1,42 +1,35 @@ -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; +export const publish = ({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.', + }), + t.prop('message', t.any).options({ + title: 'Message', + description: 'The message to publish to the channel. Can be any JSON/CBOR value.', + }), + ); - const req = t.Object( - t.prop('channel', t.str).options({ - title: 'Channel name', - description: 'The name of the channel to publish to.', - }), - t.prop('message', t.any).options({ - title: 'Message', - description: 'The message to publish to the channel. Can be any JSON/CBOR value.', - }), - ); + const Response = t.obj.options({ + title: 'Publish response', + description: 'An empty object.', + }); - const res = t.obj.options({ - title: 'Publish response', - description: 'An empty object.', + 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.', }); - 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 {}; - }); - - 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..b1004c82e0 100644 --- a/src/server/routes/util/index.ts +++ b/src/server/routes/util/index.ts @@ -1,76 +1,62 @@ -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 () => { - return 'pong'; - }); - return router.fn('util.ping', func); - }; +export const ping = ({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'; + }); +}; -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); - }; +export const echo = ({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; - const Request = t.any; - const Response = t.Object( - t.prop('now', t.num), - t.prop( - 'stats', - t.Object( - t.prop('pubsub', t.Object(t.prop('channels', t.num), t.prop('observers', t.num))), - t.prop('presence', t.Object(t.prop('rooms', t.num), t.prop('entries', t.num), t.prop('observers', t.num))), - t.prop('blocks', t.Object(t.prop('blocks', t.num), t.prop('patches', t.num))), - ), +export const info = ({t, services}: RouteDeps) => (r: Router) => { + const Request = t.any; + const Response = t.Object( + t.prop('now', t.num), + t.prop( + 'stats', + t.Object( + t.prop('pubsub', t.Object(t.prop('channels', t.num), t.prop('observers', t.num))), + t.prop('presence', t.Object(t.prop('rooms', t.num), t.prop('entries', t.num), t.prop('observers', t.num))), + t.prop('blocks', t.Object(t.prop('blocks', t.num), t.prop('patches', t.num))), ), - ); - const Func = t.Function(Request, Response).implement(async () => { - return { - now: Date.now(), - stats: { - pubsub: services.pubsub.stats(), - presence: services.presence.stats(), - blocks: services.blocks.stats(), - }, - }; - }); - return router.fn('util.info', Func); - }; + ), + ); + const Func = t.Function(Request, Response); + return r.prop('util.info', Func, async () => { + return { + now: Date.now(), + stats: { + pubsub: services.pubsub.stats(), + presence: services.presence.stats(), + blocks: services.blocks.stats(), + }, + }; + }); +}; -export const schema = - (deps: RouteDeps) => - (router: TypeRouter) => { - const t = router.t; - const Request = t.any; - const Response = t.Object(t.prop('typescript', t.str)); - const Func = t.Function(Request, Response).implement(async () => { - return { - typescript: deps.router.toTypeScript(), - }; - }); - return router.fn('util.schema', Func); - }; +export const schema = ({t, router}: RouteDeps) => (r: Router) => { + const Request = t.any; + const Response = t.Object(t.prop('typescript', t.str)); + const Func = t.Function(Request, Response); + return r.prop('util.schema', Func, async () => { + return { + typescript: router.toTypeScript(), + }; + }); +}; // 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 ))))); From 759e00e30739f9bb87dfbde462df58696aa35333 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 8 Dec 2023 10:05:49 +0100 Subject: [PATCH 6/8] =?UTF-8?q?test(reactive-rpc):=20=F0=9F=92=8D=20make?= =?UTF-8?q?=20all=20demo=20server=20tests=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-type-value/Value.ts | 10 ++++++++++ src/reactive-rpc/common/messages/Value.ts | 10 ---------- src/reactive-rpc/common/testing/buildE2eClient.ts | 12 ++++++++++-- src/server/routes/index.ts | 3 +-- 4 files changed, 21 insertions(+), 14 deletions(-) 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/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/testing/buildE2eClient.ts b/src/reactive-rpc/common/testing/buildE2eClient.ts index da0dc579ee..385b222751 100644 --- a/src/reactive-rpc/common/testing/buildE2eClient.ts +++ b/src/reactive-rpc/common/testing/buildE2eClient.ts @@ -12,6 +12,8 @@ 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 { /** @@ -110,8 +112,14 @@ export const buildE2eClient = >(caller: Caller, op }; }; -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/index.ts b/src/server/routes/index.ts index 130f395447..8d51c1f401 100644 --- a/src/server/routes/index.ts +++ b/src/server/routes/index.ts @@ -2,13 +2,12 @@ import {routes} from './routes'; 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 {TypeSystem} from '../../json-type'; +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 system = new TypeSystem(); const router = ObjectValue.create(system); const deps: RouteDeps = { services, From 6092cccff0b6854c6ea6e911e2c1a0d4e01f7754 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 8 Dec 2023 10:06:13 +0100 Subject: [PATCH 7/8] =?UTF-8?q?style(reactive-rpc):=20=F0=9F=92=84=20run?= =?UTF-8?q?=20Prettier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/testing/buildE2eClient.ts | 8 +- src/server/routes/blocks/index.ts | 20 ++-- src/server/routes/blocks/methods/create.ts | 40 +++---- src/server/routes/blocks/methods/edit.ts | 72 ++++++------ src/server/routes/blocks/methods/get.ts | 50 ++++---- src/server/routes/blocks/methods/history.ts | 58 ++++----- src/server/routes/blocks/methods/listen.ts | 58 ++++----- src/server/routes/blocks/methods/remove.ts | 32 ++--- src/server/routes/presence/index.ts | 4 +- src/server/routes/presence/methods/listen.ts | 54 ++++----- src/server/routes/presence/methods/remove.ts | 40 +++---- src/server/routes/presence/methods/update.ts | 110 +++++++++--------- src/server/routes/pubsub/listen.ts | 46 ++++---- src/server/routes/pubsub/publish.ts | 46 ++++---- src/server/routes/util/index.ts | 104 +++++++++-------- 15 files changed, 377 insertions(+), 365 deletions(-) diff --git a/src/reactive-rpc/common/testing/buildE2eClient.ts b/src/reactive-rpc/common/testing/buildE2eClient.ts index 385b222751..e6fd97612f 100644 --- a/src/reactive-rpc/common/testing/buildE2eClient.ts +++ b/src/reactive-rpc/common/testing/buildE2eClient.ts @@ -112,14 +112,12 @@ export const buildE2eClient = >(caller: Caller, op }; }; -type UnTypeRouterCaller = T extends TypeRouterCaller - ? R - : T extends ObjectValueCaller - ? R : never; +type UnTypeRouterCaller = T extends TypeRouterCaller ? R : T extends ObjectValueCaller ? R : never; type UnTypeRouter = T extends TypeRouter ? R : T extends ObjectValue - ? ObjectValueToTypeMap> : never; + ? 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 ecbd4c040f..b219a7ce78 100644 --- a/src/server/routes/blocks/index.ts +++ b/src/server/routes/blocks/index.ts @@ -7,16 +7,18 @@ import {Block, BlockId, BlockPatch, BlockSeq} from './schema'; import {history} from './methods/history'; import type {RouteDeps, Router, RouterBase} from '../types'; -export const blocks = (d: RouteDeps) => (r: Router) => { - const {system} = d; +export const blocks = + (d: RouteDeps) => + (r: Router) => { + const {system} = d; - system.alias('BlockId', BlockId); - system.alias('BlockSeq', BlockSeq); - system.alias('Block', Block); - system.alias('BlockPatch', BlockPatch); + system.alias('BlockId', BlockId); + system.alias('BlockSeq', BlockSeq); + system.alias('Block', Block); + system.alias('BlockPatch', BlockPatch); - // prettier-ignore - return ( + // prettier-ignore + return ( ( create(d) ( get(d) ( remove(d) @@ -24,4 +26,4 @@ export const blocks = (d: RouteDeps) => (r: Router) => ( listen(d) ( history(d) ( r )))))))); -}; + }; diff --git a/src/server/routes/blocks/methods/create.ts b/src/server/routes/blocks/methods/create.ts index 5609899fd6..2f7dec6121 100644 --- a/src/server/routes/blocks/methods/create.ts +++ b/src/server/routes/blocks/methods/create.ts @@ -1,30 +1,30 @@ import type {RouteDeps, Router, RouterBase} from '../../types'; import type {BlockId, BlockPatch} from '../schema'; -export const create = ({t, services}: RouteDeps) => (r: Router) => { - const Request = t.Object( - t.prop('id', t.Ref('BlockId')).options({ - title: 'New block ID', - description: 'The ID of the new block.', - }), - t.prop('patches', t.Array(t.Ref('BlockPatch'))).options({ - title: 'Patches', - description: 'The patches to apply to the document.', - }), - ); +export const create = + ({t, services}: RouteDeps) => + (r: Router) => { + const Request = t.Object( + t.prop('id', t.Ref('BlockId')).options({ + title: 'New block ID', + description: 'The ID of the new block.', + }), + t.prop('patches', t.Array(t.Ref('BlockPatch'))).options({ + title: 'Patches', + description: 'The patches to apply to the document.', + }), + ); - const Response = t.obj; + const Response = t.obj; - const Func = t - .Function(Request, Response) - .options({ + 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 r.prop('blocks.create', Func, async ({id, patches}) => { - const {block} = await services.blocks.create(id, patches); - return {}; - }); -}; + 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 9f129cadb0..cea15b0ebd 100644 --- a/src/server/routes/blocks/methods/edit.ts +++ b/src/server/routes/blocks/methods/edit.ts @@ -1,47 +1,47 @@ import type {RouteDeps, Router, RouterBase} from '../../types'; import type {BlockId, BlockPatch} from '../schema'; -export const edit = ({t, services}: RouteDeps) => (r: Router) => { - const PatchType = t.Ref('BlockPatch'); +export const edit = + ({t, services}: RouteDeps) => + (r: Router) => { + const PatchType = t.Ref('BlockPatch'); - const Request = t.Object( - t.prop('id', t.Ref('BlockId')).options({ - title: 'Document ID', - description: 'The ID of the document to apply the patch to.', - }), - // This can be inferred from the "seq" of the first patch: - // t.prop('seq', t.Ref('BlockSeq')).options({ - // title: 'Last known sequence number', - // description: - // 'The last known sequence number of the document. ' + - // 'If the document has changed since this sequence number, ' + - // 'the response will contain all the necessary patches for the client to catch up.', - // }), - t.prop('patches', t.Array(PatchType)).options({ - title: 'Patches', - description: 'The patches to apply to the document.', - }), - ); + const Request = t.Object( + t.prop('id', t.Ref('BlockId')).options({ + title: 'Document ID', + description: 'The ID of the document to apply the patch to.', + }), + // This can be inferred from the "seq" of the first patch: + // t.prop('seq', t.Ref('BlockSeq')).options({ + // title: 'Last known sequence number', + // description: + // 'The last known sequence number of the document. ' + + // 'If the document has changed since this sequence number, ' + + // 'the response will contain all the necessary patches for the client to catch up.', + // }), + t.prop('patches', t.Array(PatchType)).options({ + title: 'Patches', + description: 'The patches to apply to the document.', + }), + ); - const Response = t.Object( - t.prop('patches', t.Array(PatchType)).options({ - title: 'Latest patches', - description: 'The list of patches that the client might have missed and should apply to the document.', - }), - ); + const Response = t.Object( + t.prop('patches', t.Array(PatchType)).options({ + title: 'Latest patches', + description: 'The list of patches that the client might have missed and should apply to the document.', + }), + ); - const Func = t - .Function(Request, Response) - .options({ + 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 r.prop('blocks.edit', Func, async ({id, patches}) => { - const res = await services.blocks.edit(id, patches); - return { - patches: res.patches, - }; - }); -}; + 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 a9bb77ac49..7ff48d2d8a 100644 --- a/src/server/routes/blocks/methods/get.ts +++ b/src/server/routes/blocks/methods/get.ts @@ -1,35 +1,35 @@ import type {RouteDeps, Router, RouterBase} from '../../types'; import type {Block, BlockId, BlockPatch} from '../schema'; -export const get = ({t, services}: RouteDeps) => (r: Router) => { - const Request = t.Object( - t.prop('id', t.Ref('BlockId')).options({ - title: 'Block ID', - description: 'The ID of the block to retrieve.', - }), - ); +export const get = + ({t, services}: RouteDeps) => + (r: Router) => { + const Request = t.Object( + t.prop('id', t.Ref('BlockId')).options({ + title: 'Block ID', + description: 'The ID of the block to retrieve.', + }), + ); - const Response = t.Object( - t.prop('block', t.Ref('Block').options({})), - t.prop('patches', t.Array(t.Ref('BlockPatch'))).options({ - title: 'Patches', - description: 'The list of all patches.', - }), - ); + const Response = t.Object( + t.prop('block', t.Ref('Block').options({})), + t.prop('patches', t.Array(t.Ref('BlockPatch'))).options({ + title: 'Patches', + description: 'The list of all patches.', + }), + ); - const Func = t - .Function(Request, Response) - .options({ + const Func = t.Function(Request, Response).options({ title: 'Read Block', intro: 'Retrieves a block by ID.', description: 'Fetches a block by ID.', }); - return r.prop('blocks.get', Func, async ({id}) => { - const {block, patches} = await services.blocks.get(id); - return { - block, - patches, - }; - }); -}; + 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 86b215a777..ea71ef4f2e 100644 --- a/src/server/routes/blocks/methods/history.ts +++ b/src/server/routes/blocks/methods/history.ts @@ -1,39 +1,39 @@ import type {BlockPatch, BlockId} from '../schema'; import type {RouteDeps, Router, RouterBase} from '../../types'; -export const history = ({t, services}: RouteDeps) => (r: Router) => { - const Request = t.Object( - t.prop('id', t.Ref('BlockId')).options({ - title: 'Block ID', - description: 'The ID of the block.', - }), - t.prop('max', t.num.options({format: 'u32'})).options({ - title: 'Max', - description: 'The maximum sequence number to return.', - }), - t.prop('min', t.num.options({format: 'u32'})).options({ - title: 'Min', - description: 'The minimum sequence number to return.', - }), - ); +export const history = + ({t, services}: RouteDeps) => + (r: Router) => { + const Request = t.Object( + t.prop('id', t.Ref('BlockId')).options({ + title: 'Block ID', + description: 'The ID of the block.', + }), + t.prop('max', t.num.options({format: 'u32'})).options({ + title: 'Max', + description: 'The maximum sequence number to return.', + }), + t.prop('min', t.num.options({format: 'u32'})).options({ + title: 'Min', + description: 'The minimum sequence number to return.', + }), + ); - const Response = t.Object( - t.prop('patches', t.Array(t.Ref('BlockPatch'))).options({ - title: 'Patches', - description: 'The list of patches.', - }), - ); + const Response = t.Object( + t.prop('patches', t.Array(t.Ref('BlockPatch'))).options({ + title: 'Patches', + description: 'The list of patches.', + }), + ); - const Func = t - .Function(Request, Response) - .options({ + 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 r.prop('blocks.history', Func, async ({id, min, max}) => { - const {patches} = await services.blocks.history(id, min, max); - return {patches}; - }); -}; + 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 5bc53bcd4b..f9272228af 100644 --- a/src/server/routes/blocks/methods/listen.ts +++ b/src/server/routes/blocks/methods/listen.ts @@ -2,39 +2,39 @@ import {switchMap} from 'rxjs'; import type {RouteDeps, Router, RouterBase} from '../../types'; import type {BlockId, BlockPatch, Block} from '../schema'; -export const listen = ({t, services}: RouteDeps) => (r: Router) => { - const PatchType = t.Ref('BlockPatch'); +export const listen = + ({t, services}: RouteDeps) => + (r: Router) => { + const PatchType = t.Ref('BlockPatch'); - const Request = t.Object( - t.prop('id', t.Ref('BlockId')).options({ - title: 'Block ID', - description: 'The ID of the block to subscribe to.', - }), - ); + const Request = t.Object( + t.prop('id', t.Ref('BlockId')).options({ + title: 'Block ID', + description: 'The ID of the block to subscribe to.', + }), + ); - const Response = t.Object( - t.propOpt('deleted', t.Boolean()).options({ - title: 'Deleted', - description: 'Emitted only when the block is deleted.', - }), - t.propOpt('block', t.Ref('Block')).options({ - title: 'Block', - description: 'The whole block object, emitted only when the block is created.', - }), - t.propOpt('patches', t.Array(PatchType)).options({ - title: 'Latest patches', - description: 'Patches that have been applied to the block.', - }), - ); + const Response = t.Object( + t.propOpt('deleted', t.Boolean()).options({ + title: 'Deleted', + description: 'Emitted only when the block is deleted.', + }), + t.propOpt('block', t.Ref('Block')).options({ + title: 'Block', + description: 'The whole block object, emitted only when the block is created.', + }), + t.propOpt('patches', t.Array(PatchType)).options({ + title: 'Latest patches', + description: 'Patches that have been applied to the block.', + }), + ); - const Func = t - .Function$(Request, Response) - .options({ + const Func = t.Function$(Request, Response).options({ title: 'Listen for block changes', description: 'Subscribe to a block to receive updates when it changes.', }); - return r.prop('blocks.listen', Func, (req$) => { - return req$.pipe(switchMap(({id}) => services.pubsub.listen$(`__block:${id}`))) as any; - }); -}; + 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 e092c28385..ae3400d397 100644 --- a/src/server/routes/blocks/methods/remove.ts +++ b/src/server/routes/blocks/methods/remove.ts @@ -1,26 +1,26 @@ import type {RouteDeps, Router, RouterBase} from '../../types'; import type {BlockId} from '../schema'; -export const remove = ({t, services}: RouteDeps) => (r: Router) => { - const Request = t.Object( - t.prop('id', t.Ref('BlockId')).options({ - title: 'Block ID', - description: 'The ID of the block to delete.', - }), - ); +export const remove = + ({t, services}: RouteDeps) => + (r: Router) => { + const Request = t.Object( + t.prop('id', t.Ref('BlockId')).options({ + title: 'Block ID', + description: 'The ID of the block to delete.', + }), + ); - const Response = t.obj; + const Response = t.obj; - const Func = t - .Function(Request, Response) - .options({ + const Func = t.Function(Request, Response).options({ title: 'Read Block', intro: 'Retrieves a block by ID.', description: 'Fetches a block by ID.', }); - return r.prop('blocks.remove', Func, async ({id}) => { - await services.blocks.remove(id); - return {}; - }); -}; + return r.prop('blocks.remove', Func, async ({id}) => { + await services.blocks.remove(id); + return {}; + }); + }; diff --git a/src/server/routes/presence/index.ts b/src/server/routes/presence/index.ts index a95dd2707d..7a5e92f5be 100644 --- a/src/server/routes/presence/index.ts +++ b/src/server/routes/presence/index.ts @@ -4,7 +4,9 @@ import {remove} from './methods/remove'; import {PresenceEntry} from './schema'; import type {RouteDeps, Router, RouterBase} from '../types'; -export const presence = (d: RouteDeps) => (r: Router) => { +export const presence = + (d: RouteDeps) => + (r: Router) => { d.system.alias('PresenceEntry', PresenceEntry); // prettier-ignore diff --git a/src/server/routes/presence/methods/listen.ts b/src/server/routes/presence/methods/listen.ts index 54ec376971..ee5406956a 100644 --- a/src/server/routes/presence/methods/listen.ts +++ b/src/server/routes/presence/methods/listen.ts @@ -2,25 +2,25 @@ import {map, switchMap} from 'rxjs'; import type {PresenceEntry, TPresenceEntry} from '../schema'; import type {RouteDeps, Router, RouterBase} from '../../types'; -export const listen = ({t, services}: RouteDeps) => (r: Router) => { - const Request = t.Object( - t.prop('room', t.str).options({ - title: 'Room ID', - description: 'The ID of the room to subscribe to.', - }), - ); +export const listen = + ({t, services}: RouteDeps) => + (r: Router) => { + const Request = t.Object( + t.prop('room', t.str).options({ + title: 'Room ID', + description: 'The ID of the room to subscribe to.', + }), + ); - const Response = t.Object( - t.prop('entries', t.Array(t.Ref('PresenceEntry'))), - t.prop('time', t.num).options({ - title: 'Current time', - description: 'The current server time in milliseconds since the UNIX epoch.', - }), - ); + const Response = t.Object( + t.prop('entries', t.Array(t.Ref('PresenceEntry'))), + t.prop('time', t.num).options({ + title: 'Current time', + description: 'The current server time in milliseconds since the UNIX epoch.', + }), + ); - const Func = t - .Function$(Request, Response) - .options({ + const Func = t.Function$(Request, Response).options({ title: 'Subscribe to a room.', intro: 'Subscribes to presence updates in a room.', description: @@ -29,13 +29,13 @@ export const listen = ({t, services}: RouteDeps) => (r: Ro 'a presence entry is updated or deleted. ', }); - return r.prop('presence.listen', Func, (req$) => { - return req$.pipe( - switchMap((req) => services.presence.listen$(req.room)), - map((entries: TPresenceEntry[]) => ({ - entries, - time: Date.now(), - })), - ); - }); -}; + 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 75015d3b05..80f0085bea 100644 --- a/src/server/routes/presence/methods/remove.ts +++ b/src/server/routes/presence/methods/remove.ts @@ -1,29 +1,29 @@ import type {RouteDeps, Router, RouterBase} from '../../types'; -export const remove = ({t, services}: RouteDeps) => (r: Router) => { - const Request = t.Object( - t.prop('room', t.str).options({ - title: 'Room ID', - description: 'The ID of the room from which to remove the entry.', - }), - t.prop('id', t.str).options({ - title: 'ID of the entry', - description: 'The ID of the entry to remove.', - }), - ); +export const remove = + ({t, services}: RouteDeps) => + (r: Router) => { + const Request = t.Object( + t.prop('room', t.str).options({ + title: 'Room ID', + description: 'The ID of the room from which to remove the entry.', + }), + t.prop('id', t.str).options({ + title: 'ID of the entry', + description: 'The ID of the entry to remove.', + }), + ); - const Response = t.obj; + const Response = t.obj; - const Func = t - .Function(Request, Response) - .options({ + 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 r.prop('presence.remove', Func, async ({room, id}) => { - await services.presence.remove(room, id); - return {}; - }); -}; + 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 a43c5cb3f3..a45d65e4f3 100644 --- a/src/server/routes/presence/methods/update.ts +++ b/src/server/routes/presence/methods/update.ts @@ -5,57 +5,57 @@ import type {RouteDeps, Router, RouterBase} from '../../types'; /** Entry TLL in seconds. */ const ttl = 30; -export const update = ({t, services}: RouteDeps) => (r: Router) => { - const Request = t - .Object( - t.prop('room', t.str).options({ - title: 'Room ID', - description: 'The ID of the room to update.', - }), - t.prop('id', t.str).options({ - title: 'ID of the entry', - description: 'The ID of the entry to update.', - }), - t.prop('data', t.any).options({ - title: 'Entry data', - description: 'A map of key-value pairs to update. The object is merged with the existing entry data, if any.', - }), - ) - .options({ - examples: [ - { - title: 'Update user entry', - description: - 'The data section of the entry is merged with the existing data. ' + - 'It can contain any key-value pairs. For example, the `cursor` property is used to store the ' + - 'current cursor position of the user in the room.', - value: { - room: 'my-room', - id: 'user-1', - data: { - name: 'John Doe', - cursor: [123, 456], +export const update = + ({t, services}: RouteDeps) => + (r: Router) => { + const Request = t + .Object( + t.prop('room', t.str).options({ + title: 'Room ID', + description: 'The ID of the room to update.', + }), + t.prop('id', t.str).options({ + title: 'ID of the entry', + description: 'The ID of the entry to update.', + }), + t.prop('data', t.any).options({ + title: 'Entry data', + description: 'A map of key-value pairs to update. The object is merged with the existing entry data, if any.', + }), + ) + .options({ + examples: [ + { + title: 'Update user entry', + description: + 'The data section of the entry is merged with the existing data. ' + + 'It can contain any key-value pairs. For example, the `cursor` property is used to store the ' + + 'current cursor position of the user in the room.', + value: { + room: 'my-room', + id: 'user-1', + data: { + name: 'John Doe', + cursor: [123, 456], + }, }, }, - }, - ], - }); + ], + }); - const Response = t - .Object( - t.prop('entry', t.Ref('PresenceEntry')), - t.prop('time', t.num).options({ - title: 'Current time', - description: 'The current server time in milliseconds since the UNIX epoch.', - }), - ) - .options({ - title: 'Presence update response', - }); + const Response = t + .Object( + t.prop('entry', t.Ref('PresenceEntry')), + t.prop('time', t.num).options({ + title: 'Current time', + description: 'The current server time in milliseconds since the UNIX epoch.', + }), + ) + .options({ + title: 'Presence update response', + }); - const Func = t - .Function(Request, Response) - .options({ + const Func = t.Function(Request, Response).options({ title: 'Update presence entry', intro: 'Update a presence entry in a room.', description: @@ -64,11 +64,11 @@ export const update = ({t, services}: RouteDeps) => (r: Ro `Every time the entry is updated, the TTL is reset to ${ttl} seconds.`, }); - 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(), - }; - }); -}; + 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/listen.ts b/src/server/routes/pubsub/listen.ts index f210e580d6..c5e833a290 100644 --- a/src/server/routes/pubsub/listen.ts +++ b/src/server/routes/pubsub/listen.ts @@ -1,28 +1,30 @@ import {map, switchMap} from 'rxjs'; import type {RouteDeps, Router, RouterBase} from '../types'; -export const listen = ({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.', - }), - ); +export const listen = + ({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 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 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$(Request, Response); + 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 r.prop('pubsub.listen', Func, (req) => { + const response = req.pipe( + switchMap((req) => services.pubsub.listen$(req.channel)), + map((message: any) => ({message})), + ); + return response; + }); + }; diff --git a/src/server/routes/pubsub/publish.ts b/src/server/routes/pubsub/publish.ts index 5f21632b2f..aba6c557e1 100644 --- a/src/server/routes/pubsub/publish.ts +++ b/src/server/routes/pubsub/publish.ts @@ -1,25 +1,25 @@ import type {RouteDeps, Router, RouterBase} from '../types'; -export const publish = ({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.', - }), - t.prop('message', t.any).options({ - title: 'Message', - description: 'The message to publish to the channel. Can be any JSON/CBOR value.', - }), - ); +export const publish = + ({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.', + }), + t.prop('message', t.any).options({ + title: 'Message', + description: 'The message to publish to the channel. Can be any JSON/CBOR value.', + }), + ); - const Response = t.obj.options({ - title: 'Publish response', - description: 'An empty object.', - }); + const Response = t.obj.options({ + title: 'Publish response', + description: 'An empty object.', + }); - const Func = t - .Function(Request, Response) - .options({ + const Func = t.Function(Request, Response).options({ title: 'Publish to channel', intro: 'Publish a message to a channel.', description: @@ -28,8 +28,8 @@ export const publish = ({t, services}: RouteDeps) => (r: R 'The most efficient way to publish a message is to send a primitive or a `Uint8Array` buffer.', }); - return r.prop('pubsub.publish', Func, async ({channel, message}) => { - services.pubsub.publish(channel, message); - return {}; - }); -}; + return r.prop('pubsub.publish', Func, async ({channel, message}) => { + services.pubsub.publish(channel, message); + return {}; + }); + }; diff --git a/src/server/routes/util/index.ts b/src/server/routes/util/index.ts index b1004c82e0..e9c6664b47 100644 --- a/src/server/routes/util/index.ts +++ b/src/server/routes/util/index.ts @@ -1,57 +1,65 @@ import type {RouteDeps, Router, RouterBase} from '../types'; -export const ping = ({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'; - }); -}; +export const ping = + ({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'; + }); + }; -export const echo = ({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 echo = + ({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 = ({t, services}: RouteDeps) => (r: Router) => { - const Request = t.any; - const Response = t.Object( - t.prop('now', t.num), - t.prop( - 'stats', - t.Object( - t.prop('pubsub', t.Object(t.prop('channels', t.num), t.prop('observers', t.num))), - t.prop('presence', t.Object(t.prop('rooms', t.num), t.prop('entries', t.num), t.prop('observers', t.num))), - t.prop('blocks', t.Object(t.prop('blocks', t.num), t.prop('patches', t.num))), +export const info = + ({t, services}: RouteDeps) => + (r: Router) => { + const Request = t.any; + const Response = t.Object( + t.prop('now', t.num), + t.prop( + 'stats', + t.Object( + t.prop('pubsub', t.Object(t.prop('channels', t.num), t.prop('observers', t.num))), + t.prop('presence', t.Object(t.prop('rooms', t.num), t.prop('entries', t.num), t.prop('observers', t.num))), + t.prop('blocks', t.Object(t.prop('blocks', t.num), t.prop('patches', t.num))), + ), ), - ), - ); - const Func = t.Function(Request, Response); - return r.prop('util.info', Func, async () => { - return { - now: Date.now(), - stats: { - pubsub: services.pubsub.stats(), - presence: services.presence.stats(), - blocks: services.blocks.stats(), - }, - }; - }); -}; + ); + const Func = t.Function(Request, Response); + return r.prop('util.info', Func, async () => { + return { + now: Date.now(), + stats: { + pubsub: services.pubsub.stats(), + presence: services.presence.stats(), + blocks: services.blocks.stats(), + }, + }; + }); + }; -export const schema = ({t, router}: RouteDeps) => (r: Router) => { - const Request = t.any; - const Response = t.Object(t.prop('typescript', t.str)); - const Func = t.Function(Request, Response); - return r.prop('util.schema', Func, async () => { - return { - typescript: router.toTypeScript(), - }; - }); -}; +export const schema = + ({t, router}: RouteDeps) => + (r: Router) => { + const Request = t.any; + const Response = t.Object(t.prop('typescript', t.str)); + const Func = t.Function(Request, Response); + return r.prop('util.schema', Func, async () => { + return { + typescript: router.toTypeScript(), + }; + }); + }; // prettier-ignore export const util = (d: RouteDeps) => (r: Router) => From bcee11863cec9085b130e109c539722cfd3b596a Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 8 Dec 2023 10:42:15 +0100 Subject: [PATCH 8/8] =?UTF-8?q?fix(json-type):=20=F0=9F=90=9B=20correct=20?= =?UTF-8?q?system=20dereference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-type/type/classes/AbstractType.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/json-type/type/classes/AbstractType.ts b/src/json-type/type/classes/AbstractType.ts index 4f684197c0..12fdc3d5c8 100644 --- a/src/json-type/type/classes/AbstractType.ts +++ b/src/json-type/type/classes/AbstractType.ts @@ -55,7 +55,7 @@ export abstract class AbstractType implements BaseType< protected abstract schema: S; public getSystem(): TypeSystem { - const system = type.system; + const system = this.system; if (!system) throw new Error('NO_SYSTEM'); return system; }