diff --git a/packages/app/tests/module.spec.ts b/packages/app/tests/module.spec.ts index 0647f5ea5..22905f5b0 100644 --- a/packages/app/tests/module.spec.ts +++ b/packages/app/tests/module.spec.ts @@ -387,7 +387,7 @@ test('scoped injector', () => { { const injector = serviceContainer.getInjector(module); - expect(() => injector.get(Service)).toThrow(`Service 'Service' is known but has no value`); + expect(() => injector.get(Service)).toThrow(`Service 'Service' is known but is not available in scope global`); } { diff --git a/packages/app/tests/service-container.spec.ts b/packages/app/tests/service-container.spec.ts index f6e11d672..696c5d0ec 100644 --- a/packages/app/tests/service-container.spec.ts +++ b/packages/app/tests/service-container.spec.ts @@ -118,7 +118,7 @@ test('scopes', () => { const serviceContainer = new ServiceContainer(myModule); const sessionInjector = serviceContainer.getInjectorContext().createChildScope('rpc'); - expect(() => serviceContainer.getInjectorContext().get(SessionHandler)).toThrow(`Service 'SessionHandler' is known but has no value`); + expect(() => serviceContainer.getInjectorContext().get(SessionHandler)).toThrow(`Service 'SessionHandler' is known but is not available`); expect(sessionInjector.get(SessionHandler)).toBeInstanceOf(SessionHandler); expect(serviceContainer.getInjectorContext().get(MyService)).toBeInstanceOf(MyService); diff --git a/packages/bench/.npmignore b/packages/bench/.npmignore new file mode 100644 index 000000000..2b29f2764 --- /dev/null +++ b/packages/bench/.npmignore @@ -0,0 +1 @@ +tests diff --git a/packages/bench/README.md b/packages/bench/README.md new file mode 100644 index 000000000..a66d9c200 --- /dev/null +++ b/packages/bench/README.md @@ -0,0 +1 @@ +# Bench diff --git a/packages/bench/dist/.gitkeep b/packages/bench/dist/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/bench/index.ts b/packages/bench/index.ts new file mode 100644 index 000000000..093d9725e --- /dev/null +++ b/packages/bench/index.ts @@ -0,0 +1,259 @@ +export const AsyncFunction = (async () => { +}).constructor as { new(...args: string[]): Function }; + +function noop() { +} + +type Benchmark = { + name: string; + fn: () => void; + iterations: number; + avgTime: number; + variance: number; + rme: number; + samples: number[], + heapDiff: number; + gcEvents: number[]; +} + +const benchmarks: Benchmark[] = [{ + name: '', + fn: noop, + gcEvents: [], + samples: [], + iterations: 0, + avgTime: 0, + heapDiff: 0, + rme: 0, + variance: 0, +}]; +let benchmarkCurrent = 1; +let current = benchmarks[0]; + +const blocks = ['▁', '▂', '▄', '▅', '▆', '▇', '█']; + +function getBlocks(stats: number[]): string { + const max = Math.max(...stats); + let res = ''; + for (const n of stats) { + const cat = Math.ceil(n / max * 6); + res += (blocks[cat - 1]); + } + + return res; +} + +const Reset = '\x1b[0m'; +const FgGreen = '\x1b[32m'; +const FgYellow = '\x1b[33m'; + +function green(text: string): string { + return `${FgGreen}${text}${Reset}`; +} + +function yellow(text: string): string { + return `${FgYellow}${text}${Reset}`; +} + +function print(...args: any[]) { + process.stdout.write(args.join(' ') + '\n'); +} + +const callGc = global.gc ? global.gc : () => undefined; + +function report(benchmark: Benchmark) { + const hz = 1000 / benchmark.avgTime; + + print( + ' 🏎', + 'x', green(hz.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(14)), 'ops/sec', + '\xb1' + benchmark.rme.toFixed(2).padStart(5) + '%', + yellow(benchmark.avgTime.toLocaleString(undefined, { minimumFractionDigits: 6, maximumFractionDigits: 6 }).padStart(10)), 'ms/op', + '\t' + getBlocks(benchmark.samples), + green(benchmark.name) + (current.fn instanceof AsyncFunction ? ' (async)' : ''), + `\t${benchmark.iterations} samples`, + benchmark.gcEvents.length ? `\t${benchmark.gcEvents.length} gc (${benchmark.gcEvents.reduce((a, b) => a + b, 0)}ms)` : '', + ); +} + +export function benchmark(name: string, fn: () => void) { + benchmarks.push({ + name, fn, + gcEvents: [], + samples: [], + iterations: 0, + avgTime: 0, + heapDiff: 0, + rme: 0, + variance: 0, + }); +} + +export async function run(seconds: number = 1) { + print('Node', process.version); + + while (benchmarkCurrent < benchmarks.length) { + current = benchmarks[benchmarkCurrent]; + try { + if (current.fn instanceof AsyncFunction) { + await testAsync(seconds); + } else { + test(seconds); + } + } catch (error) { + print(`Benchmark ${current.name} failed`, error); + } + benchmarkCurrent++; + report(current); + } + + console.log('done'); +} + +const executors = [ + getExecutor(1), + getExecutor(10), + getExecutor(100), + getExecutor(1000), + getExecutor(10000), + getExecutor(100000), + getExecutor(1000000), +]; + +const asyncExecutors = [ + getAsyncExecutor(1), + getAsyncExecutor(10), + getAsyncExecutor(100), + getAsyncExecutor(1000), + getAsyncExecutor(10000), + getAsyncExecutor(100000), + getAsyncExecutor(1000000), +]; + +const gcObserver = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + current.gcEvents.push(entry.duration); + } +}); +const a = gcObserver.observe({ entryTypes: ['gc'] }); + +function test(seconds: number) { + let iterations = 1; + let samples: number[] = []; + const max = seconds * 1000; + + let executorId = 0; + let executor = executors[executorId]; + //check which executor to use, go up until one round takes more than 5ms + do { + const candidate = executors[executorId++]; + if (!candidate) break; + const start = performance.now(); + candidate(current.fn); + const end = performance.now(); + const time = end - start; + if (time > 5) break; + executor = candidate; + } while (true); + + // warmup + for (let i = 0; i < 100; i++) { + executor(current.fn); + } + + let consumed = 0; + const beforeHeap = process.memoryUsage().heapUsed; + callGc(); + do { + const start = performance.now(); + const r = executor(current.fn); + const end = performance.now(); + const time = end - start; + consumed += time; + samples.push(time / r); + iterations += r; + } while (consumed < max); + + // console.log('executionTimes', executionTimes); + collect(current, beforeHeap, samples, iterations); +} + +function collect(current: Benchmark, beforeHeap: number, samples: number[], iterations: number) { + // remove first 10% of samples + const allSamples = samples.slice(); + samples = samples.slice(Math.floor(samples.length * 0.9)); + + const avgTime = samples.reduce((sum, t) => sum + t, 0) / samples.length; + samples.sort((a, b) => a - b); + + const variance = samples.reduce((sum, t) => sum + Math.pow(t - avgTime, 2), 0) / samples.length; + const rme = (Math.sqrt(variance) / avgTime) * 100; // Relative Margin of Error (RME) + + const afterHeap = process.memoryUsage().heapUsed; + const heapDiff = afterHeap - beforeHeap; + + current.avgTime = avgTime; + current.variance = variance; + current.rme = rme; + current.heapDiff = heapDiff; + current.iterations = iterations; + // pick 20 samples from allSamples, make sure the first and last are included + current.samples = allSamples.filter((v, i) => i === 0 || i === allSamples.length - 1 || i % Math.floor(allSamples.length / 20) === 0); + // current.samples = allSamples; +} + +async function testAsync(seconds: number) { + let iterations = 1; + let samples: number[] = []; + const max = seconds * 1000; + + let executorId = 0; + let executor = asyncExecutors[executorId]; + //check which executor to use, go up until one round takes more than 5ms + do { + const candidate = asyncExecutors[executorId++]; + if (!candidate) break; + const start = performance.now(); + await candidate(current.fn); + const end = performance.now(); + const time = end - start; + if (time > 5) break; + executor = candidate; + } while (true); + + // warmup + for (let i = 0; i < 100; i++) { + executor(current.fn); + } + + let consumed = 0; + const beforeHeap = process.memoryUsage().heapUsed; + callGc(); + do { + const start = performance.now(); + const r = await executor(current.fn); + const end = performance.now(); + const time = end - start; + consumed += time; + samples.push(time / r); + iterations += r; + } while (consumed < max); + + collect(current, beforeHeap, samples, iterations); +} + +function getExecutor(times: number) { + let code = ''; + for (let i = 0; i < times; i++) { + code += 'fn();'; + } + return new Function('fn', code + '; return ' + times); +} + +function getAsyncExecutor(times: number) { + let code = ''; + for (let i = 0; i < times; i++) { + code += 'await fn();'; + } + return new AsyncFunction('fn', code + '; return ' + times); +} diff --git a/packages/bench/package.json b/packages/bench/package.json new file mode 100644 index 000000000..3ca959938 --- /dev/null +++ b/packages/bench/package.json @@ -0,0 +1,26 @@ +{ + "name": "@deepkit/bench", + "version": "1.0.3", + "description": "Deepkit Bench", + "type": "commonjs", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/cjs/index.d.ts", + "exports": { + ".": { + "types": "./dist/cjs/index.d.ts", + "require": "./dist/cjs/index.js", + "default": "./dist/esm/index.js" + } + }, + "sideEffects": false, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "echo '{\"type\": \"module\"}' > ./dist/esm/package.json" + }, + "repository": "https://github.com/deepkit/deepkit-framework", + "author": "Marc J. Schmidt ", + "license": "MIT" +} diff --git a/packages/bench/tsconfig.esm.json b/packages/bench/tsconfig.esm.json new file mode 100644 index 000000000..187dc34dd --- /dev/null +++ b/packages/bench/tsconfig.esm.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist/esm", + "module": "ES2020" + }, + "references": [ + { + "path": "../core/tsconfig.esm.json" + }, + { + "path": "../type/tsconfig.esm.json" + } + ] +} \ No newline at end of file diff --git a/packages/bench/tsconfig.json b/packages/bench/tsconfig.json new file mode 100644 index 000000000..eb45310ab --- /dev/null +++ b/packages/bench/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "noImplicitAny": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "moduleResolution": "node", + "target": "es2020", + "module": "CommonJS", + "esModuleInterop": true, + "outDir": "./dist/cjs", + "declaration": true, + "composite": true, + "types": [ + "node" + ] + }, + "reflection": true, + "include": [ + "index.ts" + ], + "exclude": [ + "tests" + ], + "references": [ + ] +} diff --git a/packages/bson/index.ts b/packages/bson/index.ts index 3e14d6672..c4b41a5b2 100644 --- a/packages/bson/index.ts +++ b/packages/bson/index.ts @@ -11,6 +11,8 @@ export * from './src/model.js'; export * from './src/bson-parser.js'; export { BaseParser } from './src/bson-parser.js'; +export { seekElementSize } from './src/continuation.js'; +export { BSONType } from './src/utils.js'; export * from './src/bson-deserializer.js'; export * from './src/bson-serializer.js'; export * from './src/strings.js'; diff --git a/packages/bson/src/bson-deserializer-templates.ts b/packages/bson/src/bson-deserializer-templates.ts index a3f1fe262..3ca578e35 100644 --- a/packages/bson/src/bson-deserializer-templates.ts +++ b/packages/bson/src/bson-deserializer-templates.ts @@ -40,6 +40,7 @@ import { } from '@deepkit/type'; import { seekElementSize } from './continuation.js'; import { BSONType, digitByteSize, isSerializable } from './utils.js'; +import { BaseParser } from './bson-parser.js'; function getNameComparator(name: string): string { //todo: support utf8 names @@ -74,55 +75,49 @@ export function deserializeAny(type: Type, state: TemplateState) { `); } -export function deserializeNumber(type: Type, state: TemplateState) { - const readBigInt = type.kind === ReflectionKind.bigint ? `state.parser.parseBinaryBigInt()` : `Number(state.parser.parseBinaryBigInt())`; +const numberParsers = createParserLookup(() => 0, [ + [BSONType.INT, parser => parser.parseInt()], + [BSONType.NUMBER, parser => parser.parseNumber()], + [BSONType.LONG, parser => parser.parseLong()], + [BSONType.TIMESTAMP, parser => parser.parseLong()], + [BSONType.BOOLEAN, parser => parser.parseBoolean() ? 1 : 0], + [BSONType.BINARY, parser => Number(parser.parseBinaryBigInt())], + [BSONType.STRING, parser => Number(parser.parseString())], +]); +export function deserializeNumber(type: Type, state: TemplateState) { + state.setContext({ numberParsers }); state.addCode(` - if (state.elementType === ${BSONType.INT}) { - ${state.setter} = state.parser.parseInt(); - } else if (state.elementType === ${BSONType.NULL} || state.elementType === ${BSONType.UNDEFINED}) { - ${state.setter} = 0; - } else if (state.elementType === ${BSONType.NUMBER}) { - ${state.setter} = state.parser.parseNumber(); - } else if (state.elementType === ${BSONType.LONG} || state.elementType === ${BSONType.TIMESTAMP}) { - ${state.setter} = state.parser.parseLong(); - } else if (state.elementType === ${BSONType.BOOLEAN}) { - ${state.setter} = state.parser.parseBoolean() ? 1 : 0; - } else if (state.elementType === ${BSONType.BINARY}) { - ${state.setter} = ${readBigInt}; - } else if (state.elementType === ${BSONType.STRING}) { - ${state.setter} = Number(state.parser.parseString()); - if (isNaN(${state.setter})) { - ${throwInvalidBsonType(type, state)} - } - } else { + ${state.setter} = numberParsers[state.elementType](state.parser); + if (isNaN(${state.setter})) { ${throwInvalidBsonType(type, state)} } `); } +const bigIntParsers = createParserLookup(() => 0n, [ + [BSONType.INT, parser => BigInt(parser.parseInt())], + [BSONType.NUMBER, parser => BigInt(parser.parseNumber())], + [BSONType.LONG, parser => BigInt(parser.parseLong())], + [BSONType.TIMESTAMP, parser => BigInt(parser.parseLong())], + [BSONType.BOOLEAN, parser => BigInt(parser.parseBoolean() ? 1 : 0)], + [BSONType.BINARY, parser => parser.parseBinaryBigInt()], + [BSONType.STRING, parser => BigInt(parser.parseString())], +]); + export function deserializeBigInt(type: Type, state: TemplateState) { const binaryBigInt = binaryBigIntAnnotation.getFirst(type); - const parseBigInt = binaryBigInt === BinaryBigIntType.signed ? 'parseSignedBinaryBigInt' : 'parseBinaryBigInt'; + + state.setContext({ bigIntParsers }); + let lookup = 'bigIntParsers'; + if (binaryBigInt === BinaryBigIntType.signed) { + const customLookup = bigIntParsers.slice(); + customLookup[BSONType.BINARY] = parser => parser.parseSignedBinaryBigInt(); + lookup = state.setVariable('lookup', customLookup); + } state.addCode(` - if (state.elementType === ${BSONType.INT}) { - ${state.setter} = BigInt(state.parser.parseInt()); - } else if (state.elementType === ${BSONType.NULL} || state.elementType === ${BSONType.UNDEFINED}) { - ${state.setter} = 0n; - } else if (state.elementType === ${BSONType.NUMBER}) { - ${state.setter} = BigInt(state.parser.parseNumber()); - } else if (state.elementType === ${BSONType.LONG} || state.elementType === ${BSONType.TIMESTAMP}) { - ${state.setter} = BigInt(state.parser.parseLong()); - } else if (state.elementType === ${BSONType.BOOLEAN}) { - ${state.setter} = BigInt(state.parser.parseBoolean() ? 1 : 0); - } else if (state.elementType === ${BSONType.BINARY} && ${binaryBigInt} !== undefined) { - ${state.setter} = state.parser.${parseBigInt}(); - } else if (state.elementType === ${BSONType.STRING}) { - ${state.setter} = BigInt(state.parser.parseString()); - } else { - ${throwInvalidBsonType(type, state)} - } + ${state.setter} = ${lookup}[state.elementType](state.parser); `); } @@ -205,21 +200,36 @@ export function deserializeUndefined(type: Type, state: TemplateState) { `); } +type Parse = (parser: BaseParser) => any; + +function createParserLookup(defaultParse: Parse, parsers: [elementType: BSONType, fn: Parse][]): Parse[] { + const result = [ + defaultParse, defaultParse, defaultParse, defaultParse, defaultParse, + defaultParse, defaultParse, defaultParse, defaultParse, defaultParse, + defaultParse, defaultParse, defaultParse, defaultParse, defaultParse, + defaultParse, defaultParse, defaultParse, defaultParse, defaultParse, + ]; + for (const [index, parse] of parsers) { + result[index] = parse; + } + return result; +} + +const booleanParsers = createParserLookup(() => 0, [ + [BSONType.BOOLEAN, parser => parser.parseBoolean()], + [BSONType.NULL, parser => 0], + [BSONType.UNDEFINED, parser => 0], + [BSONType.INT, parser => !!parser.parseInt()], + [BSONType.NUMBER, parser => !!parser.parseNumber()], + [BSONType.LONG, parser => !!parser.parseLong()], + [BSONType.TIMESTAMP, parser => !!parser.parseLong()], + [BSONType.STRING, parser => !!Number(parser.parseString())], +]); + export function deserializeBoolean(type: Type, state: TemplateState) { + state.setContext({ booleanParsers }); state.addCode(` - if (state.elementType === ${BSONType.BOOLEAN}) { - ${state.setter} = state.parser.parseBoolean(); - } else if (state.elementType === ${BSONType.NULL} || state.elementType === ${BSONType.UNDEFINED}) { - ${state.setter} = false; - } else if (state.elementType === ${BSONType.INT}) { - ${state.setter} = state.parser.parseInt() ? true : false; - } else if (state.elementType === ${BSONType.NUMBER}) { - ${state.setter} = state.parser.parseNumber() ? true : false; - } else if (state.elementType === ${BSONType.LONG} || state.elementType === ${BSONType.TIMESTAMP}) { - ${state.setter} = state.parser.parseLong() ? true : false; - } else { - ${throwInvalidBsonType(type, state)} - } + ${state.setter} = booleanParsers[state.elementType](state.parser); `); } @@ -538,7 +548,10 @@ export function deserializeArray(type: TypeArray, state: TemplateState) { state.setContext({ digitByteSize }); state.addCode(` - if (state.elementType && state.elementType !== ${BSONType.ARRAY}) ${throwInvalidBsonType({ kind: ReflectionKind.array, type: elementType }, state)} + if (state.elementType && state.elementType !== ${BSONType.ARRAY}) ${throwInvalidBsonType({ + kind: ReflectionKind.array, + type: elementType, + }, state)} { var ${result} = []; const end = state.parser.eatUInt32() + state.parser.offset; diff --git a/packages/bson/src/bson-parser.ts b/packages/bson/src/bson-parser.ts index 40542c04a..5eb6ade6b 100644 --- a/packages/bson/src/bson-parser.ts +++ b/packages/bson/src/bson-parser.ts @@ -8,13 +8,7 @@ * You should have received a copy of the MIT License along with this program. */ -import { - BSON_BINARY_SUBTYPE_BYTE_ARRAY, - BSON_BINARY_SUBTYPE_UUID, - BSONType, - digitByteSize, - TWO_PWR_32_DBL_N, -} from './utils.js'; +import { BSON_BINARY_SUBTYPE_BYTE_ARRAY, BSON_BINARY_SUBTYPE_UUID, BSONType, digitByteSize, TWO_PWR_32_DBL_N } from './utils.js'; import { decodeUTF8 } from './strings.js'; import { nodeBufferToArrayBuffer, ReflectionKind, SerializationError, Type } from '@deepkit/type'; import { hexTable } from './model.js'; @@ -32,16 +26,53 @@ export function decodeUTF8Parser(parser: BaseParser, size: number = parser.size return s; } +export function readUint32LE(buffer: Uint8Array, offset: number): number { + return ( + buffer[offset] | + (buffer[offset + 1] << 8) | + (buffer[offset + 2] << 16) | + (buffer[offset + 3] << 24) >>> 0 + ); +} + +export function readInt32LE(buffer: Uint8Array, offset: number): number { + return ( + buffer[offset] | + (buffer[offset + 1] << 8) | + (buffer[offset + 2] << 16) | + (buffer[offset + 3] << 24) + ); +} + +const float64Buffer = new ArrayBuffer(8); +const u32 = new Uint32Array(float64Buffer); +const f64 = new Float64Array(float64Buffer); + +export function readFloat64LE(buffer: Uint8Array, offset: number): number { + u32[0] = + buffer[offset] | + (buffer[offset + 1] << 8) | + (buffer[offset + 2] << 16) | + (buffer[offset + 3] << 24); + u32[1] = + buffer[offset + 4] | + (buffer[offset + 5] << 8) | + (buffer[offset + 6] << 16) | + (buffer[offset + 7] << 24); + return f64[0]; +} + /** * This is the (slowest) base parser which parses all property names as utf8. */ export class BaseParser { public size: number; - public dataView: DataView; - constructor(public buffer: Uint8Array, public offset: number = 0) { + constructor( + public buffer: Uint8Array, + public offset: number = 0, + ) { this.size = buffer.byteLength; - this.dataView = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); } peek(elementType: number, type?: Type) { @@ -271,7 +302,7 @@ export class BaseParser { } peekUInt32(): number { - return this.dataView.getUint32(this.offset, true); + return readUint32LE(this.buffer, this.offset); } /** @@ -304,18 +335,20 @@ export class BaseParser { } eatInt32(): number { + const value = readInt32LE(this.buffer, this.offset); this.offset += 4; - return this.dataView.getInt32(this.offset - 4, true); + return value; } eatUInt32(): number { + const value = readUint32LE(this.buffer, this.offset); this.offset += 4; - return this.dataView.getUint32(this.offset - 4, true); + return value; } eatDouble(): number { + const value = readFloat64LE(this.buffer, this.offset); this.offset += 8; - const value = this.dataView.getFloat64(this.offset - 8, true); if (isNaN(value)) return 0; return value; } diff --git a/packages/bson/src/bson-serializer.ts b/packages/bson/src/bson-serializer.ts index 1c8a3e6e4..4e053db2e 100644 --- a/packages/bson/src/bson-serializer.ts +++ b/packages/bson/src/bson-serializer.ts @@ -8,15 +8,7 @@ * You should have received a copy of the MIT License along with this program. */ -import { - CompilerContext, - createBuffer, - hasProperty, - isArray, - isIterable, - isObject, - toFastProperties, -} from '@deepkit/core'; +import { CompilerContext, createBuffer, hasProperty, isArray, isIterable, isObject, toFastProperties } from '@deepkit/core'; import { binaryBigIntAnnotation, BinaryBigIntType, @@ -100,14 +92,7 @@ import { deserializeUnion, } from './bson-deserializer-templates.js'; import { seekElementSize } from './continuation.js'; -import { - BSON_BINARY_SUBTYPE_DEFAULT, - BSON_BINARY_SUBTYPE_UUID, - BSONType, - digitByteSize, - isSerializable, - TWO_PWR_32_DBL_N, -} from './utils.js'; +import { BSON_BINARY_SUBTYPE_DEFAULT, BSON_BINARY_SUBTYPE_UUID, BSONType, digitByteSize, isSerializable, TWO_PWR_32_DBL_N } from './utils.js'; // BSON MAX VALUES const BSON_INT32_MAX = 0x7fffffff; @@ -1466,8 +1451,7 @@ function createBSONSerializer(type: Type, serializer: BSONBinarySerializer, nami const code = ` state = state || {}; - const size = sizer(data); - state.writer = state.writer || new Writer(createBuffer(size)); + state.writer = state.writer || new Writer(createBuffer(sizer(data))); const unpopulatedCheck = typeSettings.unpopulatedCheck; typeSettings.unpopulatedCheck = UnpopulatedCheck.ReturnSymbol; diff --git a/packages/bson/tests/bson-parser.spec.ts b/packages/bson/tests/bson-parser.spec.ts index f9fa39441..ba5ff01e0 100644 --- a/packages/bson/tests/bson-parser.spec.ts +++ b/packages/bson/tests/bson-parser.spec.ts @@ -1,7 +1,20 @@ import { expect, test } from '@jest/globals'; import bson, { Binary } from 'bson'; import { deserializeBSON, getBSONDeserializer } from '../src/bson-deserializer.js'; -import { BinaryBigInt, copyAndSetParent, MongoId, nodeBufferToArrayBuffer, PrimaryKey, Reference, ReflectionKind, SignedBinaryBigInt, TypeObjectLiteral, typeOf, uuid, UUID } from '@deepkit/type'; +import { + BinaryBigInt, + copyAndSetParent, + MongoId, + nodeBufferToArrayBuffer, + PrimaryKey, + Reference, + ReflectionKind, + SignedBinaryBigInt, + TypeObjectLiteral, + typeOf, + uuid, + UUID, +} from '@deepkit/type'; import { getClassName } from '@deepkit/core'; import { serializeBSONWithoutOptimiser } from '../src/bson-serializer.js'; import { BSONType } from '../src/utils'; @@ -21,7 +34,7 @@ test('basic number', () => { expect(getBSONDeserializer(undefined, schema)(serialize({ v: true }))).toEqual({ v: 1 }); expect(getBSONDeserializer(undefined, schema)(serialize({ v: false }))).toEqual({ v: 0 }); expect(getBSONDeserializer(undefined, schema)(serialize({ v: -1234 }))).toEqual({ v: -1234 }); - expect(() => getBSONDeserializer(undefined, schema)(serialize({ v: {} }))).toThrow(`Cannot convert bson type OBJECT to number`); + expect(getBSONDeserializer(undefined, schema)(serialize({ v: {} }))).toEqual({ v: 0 }); }); test('basic bigint', () => { @@ -35,7 +48,7 @@ test('basic bigint', () => { expect(getBSONDeserializer(undefined, schema)(serialize({ v: '123' }))).toEqual(obj); expect(getBSONDeserializer(undefined, schema)(serialize({ v: true }))).toEqual({ v: 1n }); expect(getBSONDeserializer(undefined, schema)(serialize({ v: false }))).toEqual({ v: 0n }); - expect(() => getBSONDeserializer(undefined, schema)(serialize({ v: {} }))).toThrow(`Cannot convert bson type OBJECT to bigint`); + expect(getBSONDeserializer(undefined, schema)(serialize({ v: {} }))).toEqual({ v: 0n }); }); test('basic null', () => { @@ -156,7 +169,7 @@ test('basic binary bigint', () => { expect(getBSONDeserializer(undefined, schema)(serialize({ v: '123' }))).toEqual({ v: 123n }); expect(getBSONDeserializer(undefined, schema)(serialize({ v: true }))).toEqual({ v: 1n }); expect(getBSONDeserializer(undefined, schema)(serialize({ v: false }))).toEqual({ v: 0n }); - expect(() => getBSONDeserializer(undefined, schema)(serialize({ v: {} }))).toThrow(`Cannot convert bson type OBJECT to BinaryBigInt`); + expect(getBSONDeserializer(undefined, schema)(serialize({ v: {} }))).toEqual({ v: 0n }); }); test('basic signed binary bigint', () => { @@ -173,7 +186,7 @@ test('basic signed binary bigint', () => { expect(getBSONDeserializer(undefined, schema)(serialize({ v: '123' }))).toEqual({ v: 123n }); expect(getBSONDeserializer(undefined, schema)(serialize({ v: true }))).toEqual({ v: 1n }); expect(getBSONDeserializer(undefined, schema)(serialize({ v: false }))).toEqual({ v: 0n }); - expect(() => getBSONDeserializer(undefined, schema)(serialize({ v: {} }))).toThrow(`Cannot convert bson type OBJECT to SignedBinaryBigInt`); + expect(getBSONDeserializer(undefined, schema)(serialize({ v: {} }))).toEqual({ v: 0n }); }); test('basic string', () => { @@ -198,7 +211,7 @@ test('basic boolean', () => { expect(getBSONDeserializer(undefined, schema)(bson)).toEqual(obj); expect(getBSONDeserializer(undefined, schema)(serialize({ v: 123 }))).toEqual({ v: true }); expect(getBSONDeserializer(undefined, schema)(serialize({ v: 0 }))).toEqual({ v: false }); - expect(() => getBSONDeserializer(undefined, schema)(serialize({ v: '123' }))).toThrow(`Cannot convert bson type STRING to boolean`); + expect(getBSONDeserializer(undefined, schema)(serialize({ v: '123' }))).toEqual({ v: true }); }); test('basic array buffer', () => { diff --git a/packages/core/benchmarks/promise.ts b/packages/core/benchmarks/promise.ts new file mode 100644 index 000000000..d672bfe74 --- /dev/null +++ b/packages/core/benchmarks/promise.ts @@ -0,0 +1,25 @@ +import { benchmark, run } from '@deepkit/bench'; +import { asyncOperation } from '../src/core.js'; + +const sab = new SharedArrayBuffer(1024); +const int32 = new Int32Array(sab); + +benchmark('new Promise', async function benchmarkAction() { + await new Promise(function promiseCallback(resolve) { + resolve(); + }); +}); + +benchmark('asyncOperation', async function benchmarkAction() { + await asyncOperation(function promiseCallback(resolve) { + resolve(); + }); +}); + +benchmark('Atomics.waitAsync', async function benchmarkAction() { + const promise = (Atomics as any).waitAsync(int32, 0, 0); + Atomics.notify(int32, 0); + await promise.value; +}); + +run(); diff --git a/packages/core/package.json b/packages/core/package.json index 0b2742cd5..17bdfddb6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -25,6 +25,7 @@ "to-fast-properties": "^3.0.1" }, "devDependencies": { + "@deepkit/bench": "^1.0.3", "@types/dot-prop": "~4.2.0" }, "scripts": { diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 32e5dcfb1..1fe555902 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -197,8 +197,8 @@ export function isFunction(obj: any): obj is Function { return false; } -const AsyncFunction = (async () => { -}).constructor; +export const AsyncFunction = (async () => { +}).constructor as { new(...args: string[]): Function }; /** * Returns true if given obj is a async function. @@ -526,20 +526,32 @@ export function appendObject(origin: { [k: string]: any }, extend: { [k: string] * ``` * * @public - */ -export async function asyncOperation(executor: (resolve: (value: T) => void, reject: (error: any) => void) => void | Promise): Promise { - try { - return await new Promise(async (resolve, reject) => { + * @reflection never + */ +export function asyncOperation(executor: (resolve: (value: T) => void, reject: (error: any) => void) => void | Promise): Promise { + // const error = new Error; + // return new Promise(function asyncOperation2(resolve, reject) { + // try { + // executor(resolve, reject); + // } catch (e) { + // reject(e); + // } + // }).catch((e) => { + // mergeStack(e, error.stack || ''); + // return e; + // }); + // try { + return new Promise(async function asyncOperationResolve(resolve, reject) { try { await executor(resolve, reject); } catch (e) { reject(e); } }); - } catch (error: any) { - mergeStack(error, createStack()); - throw error; - } + // } catch (error: any) { + // mergeStack(error, createStack()); + // throw error; + // } } /** diff --git a/packages/event/src/event.ts b/packages/event/src/event.ts index a65e8522a..b4fa5a143 100644 --- a/packages/event/src/event.ts +++ b/packages/event/src/event.ts @@ -445,7 +445,7 @@ function buildDispatcher(entries: EventListenerContainerEntry[], eventToken: Eve calls.push(listener.builtFn); } else if (isEventListenerContainerEntryService(listener)) { if (!listener.builtFn) { - const resolve = injector.resolve(listener.module, listener.classType); + const resolve = injector.resolver(listener.module, listener.classType); listener.builtFn = (event, injector) => resolve(injector.scope)[listener.methodName](event); } diff --git a/packages/framework/src/worker.ts b/packages/framework/src/worker.ts index 4abf4d2e0..2ee9a385e 100644 --- a/packages/framework/src/worker.ts +++ b/packages/framework/src/worker.ts @@ -114,7 +114,7 @@ export class RpcServer implements RpcServerInterface { server.on('connection', (ws, req: HttpRequest) => { const connection = createRpcConnection({ - writeBinary(message) { + write(message) { ws.send(message); }, close() { diff --git a/packages/injector/benchmarks/factory.ts b/packages/injector/benchmarks/factory.ts new file mode 100644 index 000000000..f3a29dd1e --- /dev/null +++ b/packages/injector/benchmarks/factory.ts @@ -0,0 +1,371 @@ +import { benchmark, run } from '@deepkit/bench'; +import { InjectorContext } from '../src/injector.js'; +import { InjectorModule } from '../src/module.js'; +import { ClassType, CompilerContext, getClassName } from '@deepkit/core'; + +class ServiceA { +} + +class ServiceB { + constructor(public serviceA: ServiceA) { + } +} + +class ScopedServiceC { + constructor(public serviceA: ServiceA) { + } +} + +function createInjector1() { + function serviceAFactory() { + const instance = new ServiceA(); + serviceA = () => instance; + return instance; + } + + let serviceA = serviceAFactory; + + function serviceBFactory() { + const instance = new ServiceB(serviceA()); + serviceB = () => instance; + return instance; + } + + let serviceB = serviceBFactory; + + return { serviceA, serviceB }; +} + +function createInjector2() { + const instances: any = {}; + const instances2: any = {}; + + const A = { creating: 0, count: 0 }; + const B = { creating: 0, count: 0 }; + + function reset() { + A.creating = 0; + B.creating = 0; + C.creating = 0; + state.creating = 1; + } + + function serviceA() { + if (instances.A) return instances.A; + if (A.creating) throw new Error('circular dependency'); + A.creating = state.creating++; + instances.A = new ServiceA(); + A.creating = 0; + return instances.A; + } + + function serviceB() { + if (instances2.B) return instances2.B; + if (B.creating) throw new Error('circular dependency'); + B.creating = state.creating++; + instances2.B = new ServiceB(serviceA()); + B.creating = 0; + return instances2.B; + } + + const state = { + faulty: 0, + creating: 1, + }; + + const C = { + creating: 0, + count: 0, + }; + + // todo; test this shit with many services + // make a function to generate this code automatically based of an array + function scopedServiceC(instances: any) { + if (instances.C) return instances.C; + if (C.creating) { + reset(); + throw new Error('circular dependency'); + } + if (state.faulty) reset(); + C.creating = state.creating++; + state.faulty++; + C.count++; + instances.C = new ScopedServiceC(serviceA()); + state.faulty--; + C.creating = 0; + return instances.C; + } + + function resolve(id: any) { + switch (id) { + case ScopedServiceC: + return scopedServiceC; + case ServiceA: + return serviceA; + case ServiceB: + return serviceB; + } + } + + function circularCalls() { + // const calls: { when: number, what: string }[] = []; + // if (instanceACreating) calls.push({ when: instanceACreating, what: 'ServiceA' }); + // if (instanceBCreating) calls.push({ when: instanceBCreating, what: 'ServiceB' }); + // if (instanceCCreating) calls.push({ when: instanceCCreating, what: 'ScopedServiceC' }); + // calls.sort((a, b) => a.when - b.when); + // return calls; + } + + function get(token: any, scope?: any) { + switch (token) { + case ScopedServiceC: + return scopedServiceC(scope); + case ServiceA: + return serviceA(); + case ServiceB: + return serviceB(); + } + } + + return { get, resolve }; +} + +interface Injector { + resolve(token: any): (scope?: any) => any; + + get(token: any, scope?: any): any; +} + +function createInjector3(providers: { provide: ClassType, scope?: string }[]): Injector { + const compiler = new CompilerContext(); + const factories: string[] = []; + const init: string[] = []; + const resolver: string[] = []; + + const exports: string[] = []; + const get: string[] = []; + let idx = 0; + let ids = 0; + + function getName(type: ClassType) { + return getClassName(type) + ids++; + } + + const normalized = providers.map(v => ({ + classType: v.provide, + scope: v.scope || '', + name: getName(v.provide), + })); + + for (const provider of normalized) { + const classTypeVar = compiler.reserveVariable(provider.name, provider.classType); + + const state = `s${provider.name}`; + const instance = `instances.${provider.name}`; + const arg = provider.scope ? 'instances' : ''; + const check = provider.scope ? `if (instances.name !== ${JSON.stringify(provider.scope)}) throw new Error('scope not found');` : ''; + + factories.push(` + function factory${provider.name}(${arg}) { + ${check} + if (${instance}) return ${instance}; + if (${state}.creating) { + reset(); + throw new Error('circular dependency'); + } + if (state.faulty) reset(); + ${state}.creating = state.creating++; + state.faulty++; + ${state}.count++; + ${instance} = new ${classTypeVar}(); + state.faulty--; + ${state}.creating = 0; + return ${instance}; + } + ${classTypeVar}[symbol] = factory${provider.name}; + `); + + resolver.push(`case ${classTypeVar}: return factory${provider.name};`); + + init.push(` + const ${state} = { + count: 0, + creating: 0, + }; + `); + } + + // const resolveToken = `switch (token) { ${resolver.join('\n')}}`; + // const resolveToken = `const f = token.f; if (f) return f;` + // const resolveToken = `return map.get(token);`; + const resolveToken = ` + const fn = token[symbol]; + if (fn) return fn; + switch (token) { + + } + `; + + return compiler.build(` + const instances = {}; + const state = { + faulty: 0, + creating: 1, + }; + + const symbol = Symbol('injector'); + + function reset() { + } + + ${init.join('\n')} + ${factories.join('\n')} + + function resolve(token) { + ${resolveToken} + throw new Error('No provider found for ' + token); + } + + function get(token, scope) { + return resolve(token)(scope); + throw new Error('No provider found for ' + token); + } + + return { resolve, get }; + `)(); +} + +const providers = [ + { provide: ServiceA }, + { provide: ServiceB }, + { provide: ScopedServiceC, scope: 'rpc' }, +]; + +for (let i = 0; i < 200; i++) { + class Service { + } + + providers.unshift({ provide: Service }); +} + +console.log(`${providers.length} providers`); +const module = new InjectorModule(providers); +const injector = new InjectorContext(module); +const injector1 = createInjector1(); +const injector2 = createInjector2(); +const injector3 = createInjector3(providers); + +const a = injector.get(ServiceA); +const b = injector.get(ServiceB); + +// if (!(a instanceof ServiceA)) throw new Error('a is not ServiceA'); +// if (!(b instanceof ServiceB)) throw new Error('b is not ServiceB'); + +// benchmark('injector1', () => { +// injector1.serviceA(); +// injector1.serviceB(); +// }); + +benchmark('injector.get', () => { + injector.get(ServiceA); +}); + +const resolver1 = injector.resolver(undefined, ServiceA); + +benchmark('injector resolver', () => { + resolver1(); +}); + +const scope1 = injector.createChildScope('rpc'); +scope1.get(ScopedServiceC); + +benchmark('injector scope create', () => { + const scope = injector.createChildScope('rpc'); +}); + +benchmark('injector scope create & get', () => { + const scope = injector.createChildScope('rpc'); + scope.get(ScopedServiceC); +}); + +const resolvedScopedServiceC = injector.resolver(undefined, ScopedServiceC); +const scope = injector.createChildScope('rpc'); +resolvedScopedServiceC(scope.scope); + +benchmark('injector scope create & resolver', () => { + const scope = injector.createChildScope('rpc'); + resolvedScopedServiceC(scope.scope); +}); + +const injectors = [ + // { name: 'injector2', injector: injector2 }, + { name: 'injector3', injector: injector3 }, +]; + +for (const i of injectors) { + const injector = i.injector; + benchmark(`${i.name}`, () => { + injector.get(ServiceA); + }); + + const resolveA = injector.resolve(ServiceA); + + benchmark(`${i.name} resolver`, () => { + resolveA(); + }); + + benchmark(`${i.name} scope create`, () => { + // const scope = injector2.scopeRpc(); + const scope = {}; + }); + + benchmark(`${i.name} scope create & get`, () => { + // const scope = injector2.scopeRpc(); + // injector2.scopedServiceC(scope); + const scope = {name: 'rpc'}; + injector.get(ScopedServiceC, scope); + }); + + const resolveC = injector.resolve(ScopedServiceC); + benchmark(`${i.name} scope create & resolver`, () => { + // const scope = injector2.scopeRpc(); + // injector2.scopedServiceC(scope); + const scope = {name: 'rpc'}; + resolveC(scope); + }); + + const serviceA = new ServiceA(); + + benchmark(`${i.name} scope create & get baseline`, () => { + const scope: any = {}; + scope.service ||= new ScopedServiceC(serviceA); + }); + + const scope = {name: 'rpc'}; + benchmark(`${i.name} scope get`, () => { + injector.get(ScopedServiceC, scope); + }); + + const resolve = injector.resolve(ScopedServiceC); + + benchmark(`${i.name} scope resolve`, () => { + resolve(scope); + }); +} + +// const state: any = {}; +// +// benchmark('baseline', () => { +// if (!state.instanceA) state.instanceA = new ServiceA(); +// if (!state.instanceB) state.instanceB = new ServiceB(state.instanceA); +// }); +// +// let instanceA = {}; +// let instanceB = {}; +// +// benchmark('baseline2', () => { +// instanceA ||= new ServiceA(); +// instanceB ||= new ServiceB(instanceA); +// }); + +run(); diff --git a/packages/injector/benchmarks/injector.ts b/packages/injector/benchmarks/injector.ts new file mode 100644 index 000000000..37c46db7b --- /dev/null +++ b/packages/injector/benchmarks/injector.ts @@ -0,0 +1,60 @@ +import { benchmark, run } from '@deepkit/bench'; +import { InjectorModule } from '../src/module.js'; +import { InjectorContext } from '../src/injector.js'; + +class Service { +} + +class Service2 { +} + +const module = new InjectorModule([ + Service, + { provide: Service2, scope: 'rpc' }, +]); + +const injector = new InjectorContext(module); + +const message = new Uint8Array(32); + + +benchmark('injector.get', () => { + injector.get(Service); +}); + +const resolver1 = injector.resolver(undefined, Service); + +benchmark('resolver', () => { + resolver1(); +}); + +benchmark('scope create', () => { + const scope = injector.createChildScope('rpc'); +}); + +const scope = injector.createChildScope('rpc'); + +benchmark('scope.get cached', () => { + scope.get(Service2); +}); + +benchmark('scope.get singleton', () => { + scope.get(Service); +}); + +benchmark('scope.get new', () => { + const scope = injector.createChildScope('rpc'); + scope.get(Service2); +}); + +const resolver2 = injector.resolver(module, Service2); + +benchmark('scope resolver', () => { + resolver2(scope.scope); +}); + +benchmark('baseline', () => { + new Service2(); +}); + +run(); diff --git a/packages/injector/package.json b/packages/injector/package.json index 11f6338bb..126af347f 100644 --- a/packages/injector/package.json +++ b/packages/injector/package.json @@ -27,6 +27,7 @@ "@deepkit/type": "^1.0.1" }, "devDependencies": { + "@deepkit/bench": "^1.0.3", "@deepkit/core": "^1.0.3", "@deepkit/type": "^1.0.3", "benchmark": "^2.1.4" diff --git a/packages/injector/src/injector.ts b/packages/injector/src/injector.ts index c7ac3cf54..54213e877 100644 --- a/packages/injector/src/injector.ts +++ b/packages/injector/src/injector.ts @@ -2,7 +2,6 @@ import { isClassProvider, isExistingProvider, isFactoryProvider, - isTransient, isValueProvider, NormalizedProvider, ProviderWithScope, @@ -26,8 +25,8 @@ import { import { ConfigurationProviderRegistry, ConfigureProviderEntry, findModuleForConfig, getScope, InjectorModule, PreparedProvider } from './module.js'; import { isOptional, - isType, isWithAnnotations, + Packed, ReceiveType, reflect, ReflectionClass, @@ -85,25 +84,35 @@ function constructorParameterNotFound(ofName: string, name: string, position: nu ); } -function knownServiceNotfoundError(label: string, scopes: string[], scope?: Scope) { +function knownServiceNotFoundError(label: string, scopes: string[], scope?: Scope) { throw new ServiceNotFoundError( `Service '${label}' is known but has no value.${scopes.length ? ` Available in scopes: ${scopes.join(', ')}, requested scope is ${scope ? scope.name : 'global'}.` : ''}`, ); } +function serviceNotFoundError(label: string) { + throw new ServiceNotFoundError( + `Service '${label}' not found. No matching provider.`, + ); +} + +function knownServiceNotFoundInScope(label: string, scopes: string[], scope?: Scope) { + throw new ServiceNotFoundError( + `Service '${label}' is known but is not available in scope ${scope?.name || 'global'}. Available in scopes: ${scopes.join(', ')}.`, + ); +} + function factoryDependencyNotFound(ofName: string, name: string, position: number, token: any) { const argsCheck: string[] = []; for (let i = 0; i < position; i++) argsCheck.push('✓'); argsCheck.push('?'); - for (const reset of CircularDetectorResets) reset(); throw new DependenciesUnmetError( `Unknown factory dependency argument '${tokenLabel(token)}' of ${ofName}(${argsCheck.join(', ')}). Make sure '${tokenLabel(token)}' is provided.`, ); } function propertyParameterNotFound(ofName: string, name: string, position: number, token: any) { - for (const reset of CircularDetectorResets) reset(); throw new DependenciesUnmetError( `Unknown property parameter ${name} of ${ofName}. Make sure '${tokenLabel(token)}' is provided.`, ); @@ -125,19 +134,28 @@ function createTransientInjectionTarget(destination: Destination | undefined) { return new TransientInjectionTarget(destination.token); } -const CircularDetector: any[] = []; -const CircularDetectorResets: (() => void)[] = []; +interface StackEntry { + label: string, + creation: number; + id: number; + cause: boolean; +} + +function stackToPath(stack: StackEntry[]): string[] { + const cause = stack.find(v => v.cause); + stack.sort((a, b) => a.id - b.id); + const labels = stack.map(v => v.label); + if (cause) labels.push(cause.label); + return labels; +} -function throwCircularDependency() { - const path = CircularDetector.map(tokenLabel).join(' -> '); - CircularDetector.length = 0; - for (const reset of CircularDetectorResets) reset(); +function throwCircularDependency(paths: string[]) { + const path = paths.join(' -> '); throw new CircularDependencyError(`Circular dependency found ${path}`); } export interface Scope { name: string; - instances: { [name: string]: any }; } export type ResolveToken = T extends ClassType ? R : T extends AbstractClassType ? R : T; @@ -149,7 +167,27 @@ export function resolveToken(provider: ProviderWithScope): Token { return provider.provide; } -export type ContainerToken = Exclude>; +/** @reflection never */ +export type ContainerToken = symbol | number | bigint | boolean | string | AbstractClassType | Function; + +export function getContainerTokenFromType(type: Type): ContainerToken { + if (type.kind === ReflectionKind.literal) { + if (type.literal instanceof RegExp) return type.literal.toString(); + return type.literal; + } + if (type.kind === ReflectionKind.class) return type.classType; + if (type.kind === ReflectionKind.function && type.function) return type.function; + if (type.id) return type.id; + return 'unknown'; +} + +export function isType(obj: any): obj is Type { + return obj && typeof obj === 'object' && typeof (obj as any).kind === 'number'; +} + +function isPacked(obj: any): obj is Packed { + return obj && typeof obj === 'object' && typeof (obj as any).length === 'number'; +} /** * Returns a value that can be compared with `===` to check if two tokens are actually equal even though @@ -161,21 +199,10 @@ export function getContainerToken(type: Token): ContainerToken { if (type instanceof TagProvider) return getContainerToken(type.provider.provide); if (isType(type)) { - if (type.id) return type.id; - if (type.kind === ReflectionKind.literal) { - if (type.literal instanceof RegExp) return type.literal.toString(); - return type.literal; - } - if (type.kind === ReflectionKind.class) return type.classType; - if (type.kind === ReflectionKind.function && type.function) return type.function; - throw new Error(`Could not resolve token ${stringifyType(type)}`); + return getContainerTokenFromType(type); } - return type; -} - -export interface InjectorInterface { - get(token: T, scope?: Scope): ResolveToken; + return type as ContainerToken; } /** @@ -215,30 +242,96 @@ export class TransientInjectionTarget { } } +function* forEachDependency(provider: NormalizedProvider): Generator<{ type: Type, optional: boolean }> { + if (isValueProvider(provider)) { + } else if (isClassProvider(provider)) { + let useClass = provider.useClass; + if (!useClass) { + if (isClass(provider.provide)) useClass = provider.provide as ClassType; + if (isType(provider.provide) && provider.provide.kind === ReflectionKind.class) useClass = provider.provide.classType; + if (!useClass) { + throw new Error(`UseClassProvider needs to set either 'useClass' or 'provide' as a ClassType. Got ${provider.provide as any}`); + } + } + + const reflectionClass = ReflectionClass.from(useClass); + + const constructor = reflectionClass.getMethodOrUndefined('constructor'); + if (constructor) { + for (const parameter of constructor.getParameters()) { + const tokenType = getInjectOptions(parameter.getType() as Type); + const type = tokenType || parameter.getType() as Type; + yield { type, optional: !parameter.isValueRequired() }; + } + } + + for (const property of reflectionClass.getProperties()) { + const tokenType = getInjectOptions(property.type); + if (!tokenType) continue; + yield { type: tokenType, optional: !property.isValueRequired() }; + } + } else if (isExistingProvider(provider)) { + for (const item of forEachDependency({ provide: provider.useExisting })) { + yield item; + } + } else if (isFactoryProvider(provider)) { + const reflection = ReflectionFunction.from(provider.useFactory); + for (const parameter of reflection.getParameters()) { + const tokenType = getInjectOptions(parameter.getType() as Type); + const type = tokenType || parameter.getType() as Type; + yield { type, optional: !parameter.isValueRequired() }; + } + } +} + +function isTransientInjectionTargetProvider(prepared: PreparedProvider): boolean { + for (const provider of prepared.providers) { + if (!provider.transient) continue; + for (const { type } of forEachDependency(provider)) { + if (type.kind === ReflectionKind.class && type.classType === TransientInjectionTarget) { + return true; + } + } + } + return false; +} + /** * A factory function for some class. * All properties that are not provided will be resolved using the injector that was used to create the factory. */ export type PartialFactory = (args: Partial<{ [K in keyof C]: C[K] }>) => C; +interface BuiltInjector { + set(token: Token, value: any, scope?: Scope): void; + + get(token: Token, scope?: Scope, optional?: boolean): unknown; + + instantiationCount(token: any, scope?: Scope): number; + + clear(): void; + + resolver(token: Token, scope?: Scope, optional?: boolean): Resolver; + + setter(token: Token, scope?: Scope): Setter; + + collectStack(stack: StackEntry[]): void; +} + +type BuiltNormalizedProvider = NormalizedProvider & { needsDestination?: boolean }; +type BuiltPreparedProvider = PreparedProvider & { factory?: string }; + /** * This is the actual dependency injection container. * Every module has its own injector. * * @reflection never */ -export class Injector implements InjectorInterface { - private resolver?: (token: any, scope?: Scope, destination?: Destination, scopes?: string[], optional?: boolean) => any; - private setter?: (token: any, value: any, scope?: Scope) => any; - private instantiations?: (token: any, scope?: string) => number; +export class Injector { + private built?: BuiltInjector; - /** - * All unscoped provider instances. Scoped instances are attached to `Scope`. - */ - private instances: { [name: string]: any } = {}; - private instantiated: { [name: string]: number } = {}; - - private resolverMap = new Map>; + private resolverMap = new Map>; + private setterMap = new Map>; constructor( public readonly module: InjectorModule, @@ -252,178 +345,463 @@ export class Injector implements InjectorInterface { return new Injector(new InjectorModule(providers, parent?.module), new BuildContext); } - static fromModule(module: InjectorModule, parent?: Injector): Injector { + static fromModule(module: InjectorModule): Injector { return new Injector(module, new BuildContext); } - get(token?: ReceiveType | Token, scope?: Scope): ResolveToken { - if (!this.resolver) throw new Error('Injector was not built'); - if (!token) throw new Error('Token is required'); - return this.getResolver(token as ReceiveType | Token)(scope) as ResolveToken; + get(token?: ReceiveType | Token, scope?: Scope, optional: boolean = false): ResolveToken { + if (!this.built) throw new Error('Injector was not built'); + token = isPacked(token) ? resolveReceiveType(token) : token; + return this.built.get(token, scope, optional) as ResolveToken; + } + + set(token: Token, value: any, scope?: Scope): void { + if (!this.built) throw new Error('Injector was not built'); + this.built.set(getContainerToken(token), value, scope); } - set(token: ContainerToken, value: any, scope?: Scope): void { - if (!this.setter) throw new Error('Injector was not built'); - this.setter(token, value, scope); + instantiationCount(token: any, scope?: Scope): number { + if (!this.built) throw new Error('Injector was not built'); + return this.built.instantiationCount(token, scope); } - instantiationCount(token: any, scope?: string): number { - if (!this.instantiations) throw new Error('Injector was not built'); - return this.instantiations(token, scope); + getSetter(token: ReceiveType | Token): Setter { + let setter = this.setterMap.get(token); + if (!setter) { + setter = this.createSetter(token as ReceiveType | Token); + this.setterMap.set(token, setter); + } + return setter; + } + + getResolver(token?: ReceiveType | Token, label?: string): Resolver { + if (!token) throw new Error('No token provided'); + let resolver = this.resolverMap.get(token); + if (!resolver) { + resolver = this.createResolver(token as ReceiveType | Token, label); + this.resolverMap.set(token, resolver); + } + return resolver as Resolver; } clear() { - this.instances = {}; + this.built?.clear(); } protected build(buildContext: BuildContext): void { - const resolverCompiler = new CompilerContext(); - resolverCompiler.context.set('CircularDetector', CircularDetector); - resolverCompiler.context.set('CircularDetectorResets', CircularDetectorResets); - resolverCompiler.context.set('throwCircularDependency', throwCircularDependency); - resolverCompiler.context.set('knownServiceNotfoundError', knownServiceNotfoundError); - resolverCompiler.context.set('tokenLabel', tokenLabel); - resolverCompiler.context.set('constructorParameterNotFound', constructorParameterNotFound); - resolverCompiler.context.set('functionParameterNotFound', functionParameterNotFound); - resolverCompiler.context.set('propertyParameterNotFound', propertyParameterNotFound); - resolverCompiler.context.set('factoryDependencyNotFound', factoryDependencyNotFound); - resolverCompiler.context.set('transientInjectionTargetUnavailable', transientInjectionTargetUnavailable); - resolverCompiler.context.set('createTransientInjectionTarget', createTransientInjectionTarget); - resolverCompiler.context.set('injector', this); - - const lines: string[] = []; - const resets: string[] = []; - const creating: string[] = []; - - const instantiationCompiler = new CompilerContext(); - instantiationCompiler.context.set('injector', this); - const instantiationLines: string[] = []; - - const setterCompiler = new CompilerContext(); - setterCompiler.context.set('injector', this); - const setterLines: string[] = []; - - for (const prepared of this.module.getPreparedProviders(buildContext)) { - //scopes will be created first, so they are returned instead of the unscoped instance + const compiler = new CompilerContext(); + compiler.set({ + throwCircularDependency, + knownServiceNotFoundInScope, + knownServiceNotFoundError, + serviceNotFoundError, + tokenLabel, + constructorParameterNotFound, + functionParameterNotFound, + propertyParameterNotFound, + factoryDependencyNotFound, + transientInjectionTargetUnavailable, + createTransientInjectionTarget, + getContainerToken, + stackToPath, + 'injector': this, + 'runtimeContext': buildContext.runtimeContext, + createResolver: (token: Token, label?: string) => { + return this.createResolver(token, label); + }, + }); + + const functions: string[] = []; + const init: string[] = []; + const reset: string[] = []; + const collect: string[] = []; + const clear: string[] = []; + const setReferences: string[] = []; + + for (const prepared of this.module.getPreparedProviders(buildContext) as BuiltPreparedProvider[]) { + // scopes will be created first, so they are returned instead of the unscoped instance prepared.providers.sort((a, b) => { if (a.scope && !b.scope) return -1; if (!a.scope && b.scope) return +1; return 0; }); + const label = tokenLabel(prepared.token); - for (const provider of prepared.providers) { - const scope = getScope(provider); - const name = 'i' + this.buildContext.providerIndex.reserve(); - creating.push(`let creating_${name} = false;`); - resets.push(`creating_${name} = false;`); - const accessor = scope ? 'scope.instances.' + name : 'injector.instances.' + name; - let scopeObjectCheck = scope ? ` && scope && scope.name === ${JSON.stringify(scope)}` : ''; - const scopeCheck = scope ? ` && scope === ${JSON.stringify(scope)}` : ''; + // console.log(`${label} got ${prepared.providers.length} providers for module ${getClassName(this.module)}. ` + + // `scopes ${prepared.providers.map(v => v.scope).join(', ')}. ` + + // `resolveFrom=${prepared.resolveFrom ? getClassName(prepared.resolveFrom) : 'none'}. ` + + // `resolve from modules ${prepared.modules.map(getClassName).join(', ')}. `); - const diToken = getContainerToken(prepared.token); + const i = this.buildContext.providerIndex.reserve(); + const name = 'i' + i + '_' + label.replace(/[^a-zA-Z0-9]/g, '_'); - setterLines.push(`case token === ${setterCompiler.reserveVariable('token', diToken)}${scopeObjectCheck}: { - if (${accessor} === undefined) { - injector.instantiated.${name} = injector.instantiated.${name} ? injector.instantiated.${name} + 1 : 1; - } - ${accessor} = value; - break; - }`); + const containerToken = getContainerToken(prepared.token); + const containerTokenVar = compiler.reserveVariable('containerToken', containerToken); - if (prepared.resolveFrom) { - //it's a redirect - lines.push(` - case token === ${resolverCompiler.reserveConst(diToken, 'token')}${scopeObjectCheck}: { - return ${resolverCompiler.reserveConst(prepared.resolveFrom, 'resolveFrom')}.injector.resolver(${resolverCompiler.reserveConst(diToken, 'token')}, scope, destination, scopes); - } - `); + const factoryNames: { + scope: string, + function: string, + }[] = []; + + const setterNames: { + scope: string, + function: string, + }[] = []; + + const instantiationsNames: { + scope: string, + function: string, + }[] = []; - instantiationLines.push(` - case token === ${instantiationCompiler.reserveConst(diToken, 'token')}${scopeCheck}: { - return ${instantiationCompiler.reserveConst(prepared.resolveFrom, 'resolveFrom')}.injector.instantiations(${instantiationCompiler.reserveConst(diToken, 'token')}, scope); + if (prepared.resolveFrom) { + const factory = `factory${name}`; + const setter = `setter${name}`; + const instantiations = `instantiations${name}`; + + const injectorVar = compiler.reserveConst(prepared.resolveFrom!.injector, 'injector'); + + functions.push(` + function ${factory}(scope, optional) { + return ${injectorVar}.built.get(${containerTokenVar}, scope, optional); } - `); - } else { - //we own and instantiate the service - instantiationLines.push(` - case token === ${instantiationCompiler.reserveConst(diToken, 'token')}${scopeCheck}: { - return injector.instantiated.${name} || 0; + + function ${setter}(value, scope) { + ${injectorVar}.built.set(${containerTokenVar}, value, scope); + } + + function ${instantiations}(scope) { + return ${injectorVar}.built.instantiationCount(${containerTokenVar}, scope); } + `); + + if (isClass(containerToken)) { + setReferences.push(` + ${containerTokenVar}[symbolResolver] = () => ${factory}; + ${containerTokenVar}[symbolSetter] = () => ${setter}; + ${containerTokenVar}[symbolInstantiations] = () => ${instantiations}; `); + } - lines.push(this.buildProvider(buildContext, resolverCompiler, name, accessor, scope, prepared.token, provider, prepared.modules)); + if (isType(prepared.token) && prepared.token.kind === ReflectionKind.class) { + const classTypeVar = compiler.reserveVariable('classType', prepared.token.classType); + setReferences.push(` + ${classTypeVar}[symbolResolver] = () => ${factory}; + ${classTypeVar}[symbolSetter] = () => ${setter}; + ${classTypeVar}[symbolInstantiations] = () => ${instantiations}; + `); } - } - } - this.instantiations = instantiationCompiler.build(` - //for ${getClassName(this.module)} - switch (true) { - ${instantiationLines.join('\n')} - } - return 0; - `, 'token', 'scope'); + setReferences.push(` + lookupGetter[${containerTokenVar}] = () => ${factory}; + lookupSetter[${containerTokenVar}] = () => ${setter}; + lookupInstantiations[${containerTokenVar}] = () => ${instantiations}; + `); + } else { + const scopeNames = JSON.stringify(prepared.providers.map(v => v.scope).filter(v => !!v)); + + for (const provider of prepared.providers) { + const scope = getScope(provider); + const container = scope ? 'scope' : 'instances'; + const varName = `${container}.${name}`; + const state = 's' + i + (scope ? '_' + scope : ''); + const check = scope ? `if (!scope || scope.name !== ${JSON.stringify(scope)}) return optional ? undefined : knownServiceNotFoundInScope(${JSON.stringify(label)}, ${scopeNames}, scope);` : ''; + + const factory = `factory${name}` + (scope ? '_' + scope : ''); + const setter = `setter${name}_${scope}`; + const instantiations = `instantiations${name}_${scope}`; + + const code = this.createFactoryCode(buildContext, compiler, varName, prepared.token, provider, prepared.modules); + + init.push(` + const ${state} = { + label: ${JSON.stringify(label)}, + count: 0, + creating: 0, + cause: false, + };`); + + factoryNames.push({ scope, function: factory }); + setterNames.push({ scope, function: setter }); + instantiationsNames.push({ scope, function: instantiations }); + + reset.push(`${state}.creating = 0; ${state}.cause = false;`); + collect.push(`if (${state}.creating) stack.push(${state});`); + clear.push(`${varName} = undefined;`); + + let setDestination = ``; + if (code.needsDestination) { + const tokenVar = compiler.reserveConst(prepared.token, 'token'); + setDestination = `runtimeContext.destination = { token: ${tokenVar} };`; + } - this.setter = setterCompiler.build(` - //for ${getClassName(this.module)} - switch (true) { - ${setterLines.join('\n')} - } - `, 'token', 'value', 'scope'); + let circularCheckBefore = ''; + let circularCheckAfter = ''; + if (code.dependencies) { + circularCheckBefore = ` + if (${state}.creating > 0) { + ${state}.cause = true; + const stack = []; + collectStack(stack); + const paths = stackToPath(stack); + reset(); + throwCircularDependency(paths); + } + ${state}.creating = ++runtimeContext.creation; + `; + circularCheckAfter = `${state}.creating = 0;`; + } - this.resolver = resolverCompiler.raw(` - //for ${getClassName(this.module)} - - ${creating.join('\n')}; + let returnExisting = ``; + if (!provider.transient) { + returnExisting = `if (${varName}) return ${varName};`; + } - CircularDetectorResets.push(() => { - ${resets.join('\n')}; - }); + functions.push(` + //${label}, from ${prepared.modules.map(getClassName).join(', ')} + function ${factory}(scope, optional) { + ${check} + ${returnExisting} + ${circularCheckBefore} + ${setDestination} + try { + ${code.code} + ${state}.count++; + } finally { + ${state}.creating = 0; + } + ${circularCheckAfter} + if (!${varName} && !optional) knownServiceNotFoundError(${JSON.stringify(label)}, ${scopeNames}, scope); + return ${varName}; + } - return function(token, scope, destination, scopes, optional) { - scopes = scopes || []; + function ${setter}(value, scope) { + ${check} + ${varName} = value; + } + + function ${instantiations}(scope) { + ${check} + return ${state}.count; + } + `); + } - switch (true) { - ${lines.join('\n')} + if (factoryNames.length > 1) { + // we need to override lookup/symbol for the scope + // and add a router to correctly route scopes to the function + const routerName = `router${name}`; + prepared.factory = `factory_${routerName}`; + + functions.push(` + function factory_value_${routerName}(scope, optional) { + const name = scope?.name || ''; + switch (name) { + ${factoryNames.map(v => `case ${JSON.stringify(v.scope)}: return ${v.function}(scope, optional);`).join('\n')} + default: knownServiceNotFoundInScope(${JSON.stringify(label)}, ${scopeNames}, scope); + } } + + // scope router for ${factoryNames.map(v => v.scope).join(', ')} + function factory_${routerName}(scope) { + const name = scope?.name || ''; + switch (name) { + ${factoryNames.map(v => `case ${JSON.stringify(v.scope)}: return ${v.function};`).join('\n')} + default: return factory_value_${routerName}; // no scope given, so return route for value itself (slower) + } + } + + function setter_${routerName}(scope) { + const name = scope?.name || ''; + switch (name) { + ${setterNames.map(v => `case ${JSON.stringify(v.scope)}: return ${v.function};`).join('\n')} + default: knownServiceNotFoundInScope(${JSON.stringify(label)}, ${scopeNames}, scope); + } + } + + function instantiations_${routerName}(scope) { + const name = scope?.name || ''; + switch (name) { + ${instantiationsNames.map(v => `case ${JSON.stringify(v.scope)}: return ${v.function};`).join('\n')} + default: knownServiceNotFoundInScope(${JSON.stringify(label)}, ${scopeNames}, scope); + } + } + `); + + if (isClass(containerToken)) { + setReferences.push(` + ${containerTokenVar}[symbolResolver] = factory_${routerName}; + ${containerTokenVar}[symbolSetter] = setter_${routerName}; + ${containerTokenVar}[symbolInstantiations] = instantiations_${routerName}; + `); + } + + if (isType(prepared.token) && prepared.token.kind === ReflectionKind.class) { + const classTypeVar = compiler.reserveVariable('classType', prepared.token.classType); + setReferences.push(` + ${classTypeVar}[symbolResolver] = factory_${routerName}; + ${classTypeVar}[symbolSetter] = setter_${routerName}; + ${classTypeVar}[symbolInstantiations] = instantiations_${routerName}; + `); + } - if (!optional) knownServiceNotfoundError(tokenLabel(token), scopes, scope); + setReferences.push(` + lookupGetter[${containerTokenVar}] = factory_${routerName}; + lookupSetter[${containerTokenVar}] = setter_${routerName}; + lookupInstantiations[${containerTokenVar}] = instantiations_${routerName}; + `); + } else if (factoryNames.length === 1) { + const factory = factoryNames[0].function; + const setter = setterNames[0].function; + const instantiations = instantiationsNames[0].function; + + prepared.factory = factory; + + if (isClass(containerToken)) { + setReferences.push(` + ${containerTokenVar}[symbolResolver] = () => ${factory}; + ${containerTokenVar}[symbolSetter] = () => ${setter}; + ${containerTokenVar}[symbolInstantiations] = () => ${instantiations}; + `); + } + + if (isType(prepared.token) && prepared.token.kind === ReflectionKind.class) { + const classTypeVar = compiler.reserveVariable('classType', prepared.token.classType); + setReferences.push(` + ${classTypeVar}[symbolResolver] = () => ${factory}; + ${classTypeVar}[symbolSetter] = () => ${setter}; + ${classTypeVar}[symbolInstantiations] = () => ${instantiations}; + `); + } + + setReferences.push(` + lookupGetter[${containerTokenVar}] = () => ${factory}; + lookupSetter[${containerTokenVar}] = () => ${setter}; + lookupInstantiations[${containerTokenVar}] = () => ${instantiations}; + `); + } } - `) as any; + } + + // console.log(`built injector for ${getClassName(this.module)}`); + + this.built = compiler.raw(` + //for ${getClassName(this.module)} + + const instances = {}; + const state = { + faulty: 0, + creating: 1 + }; + const lookupGetter = {}; + const lookupSetter = {}; + const lookupInstantiations = {}; + + const symbolResolver = Symbol('resolver'); + const symbolSetter = Symbol('setter'); + const symbolInstantiations = Symbol('instantiations'); + + // collectStack(stack: { label: string, id: number }[]): void + function collectStack(stack) { + ${collect.join('\n')} + ${this.module.imports.map(v => `${compiler.reserveConst(v)}.injector.built.collectStack(stack);`).join('\n')} + } + + function reset() { + runtimeContext.creation = 0; + ${reset.join('\n')} + } + + function noop() {} + + ${init.join('\n')} + + ${functions.join('\n')} + + ${setReferences.join('\n')} + + // resolver(token: Token, scope?: Scope, optional?: boolean): Resolver; + function resolver(token, scope, optional) { + // token could be: Type, ClassType, or primitive + const containerToken = getContainerToken(token); + const fn = containerToken[symbolResolver] || lookupGetter[containerToken]; + if (fn) return fn(scope); + + const resolver = createResolver(token); + if (resolver) { + lookupSetter[containerToken] = resolver; + return resolver; + } + + if (optional) return noop; + throw serviceNotFoundError(tokenLabel(token)); + } + + // setter(token: Token, scope?: Scope): Setter; + function setter(token, scope) { + const containerToken = getContainerToken(token); + const fn = containerToken[symbolSetter] || lookupSetter[containerToken] || serviceNotFoundError(tokenLabel(token)); + if (fn) return fn(scope); + throw serviceNotFoundError(tokenLabel(token)); + } + + // set(token: Token, value: any, scope?: Scope): void; + function set(token, value, scope) { + setter(token)(value, scope); } - protected buildProvider( + // get(token: Token, scope?: Scope, optional?: boolean): unknown; + function get(token, scope, optional) { + return resolver(token, scope)(scope, optional); + } + + // clear(): void; + function clear() { + ${clear.join('\n')} + } + + // instantiationCount(token: any, scope?: string): number; + function instantiationCount(token, scope) { + const containerToken = getContainerToken(token); + const fn = lookupInstantiations[containerToken]; + if (fn) return fn(scope)(scope); + return 0; + } + + return { resolver, setter, get, set, clear, instantiationCount, collectStack }; + `) as any; + } + + protected createFactoryCode( buildContext: BuildContext, compiler: CompilerContext, - name: string, - accessor: string, - scope: string, + varName: string, token: Token, - provider: NormalizedProvider, + provider: BuiltNormalizedProvider, resolveDependenciesFrom: InjectorModule[], ) { let transient = false; let factory: { code: string, dependencies: number } = { code: '', dependencies: 0 }; - const tokenVar = compiler.reserveConst(getContainerToken(token)); if (isValueProvider(provider)) { transient = provider.transient === true; const valueVar = compiler.reserveVariable('useValue', provider.useValue); - factory.code = `${accessor} = ${valueVar};`; + factory.code = `${varName} = ${valueVar};`; } else if (isClassProvider(provider)) { transient = provider.transient === true; let useClass = provider.useClass; if (!useClass) { - if (!isClass(provider.provide)) { + if (isClass(provider.provide)) useClass = provider.provide as ClassType; + if (isType(provider.provide) && provider.provide.kind === ReflectionKind.class) useClass = provider.provide.classType; + if (!useClass) { throw new Error(`UseClassProvider needs to set either 'useClass' or 'provide' as a ClassType. Got ${provider.provide as any}`); } - useClass = provider.provide as ClassType; } - factory = this.createFactory(provider, accessor, compiler, useClass, resolveDependenciesFrom); + factory = this.createFactory(provider, varName, compiler, useClass, resolveDependenciesFrom); } else if (isExistingProvider(provider)) { transient = provider.transient === true; - factory.code = `${accessor} = injector.resolver(${compiler.reserveConst(getContainerToken(provider.useExisting))}, scope, destination)`; + const existingToken = compiler.reserveConst(getContainerToken(provider.useExisting)); + factory.code = `${varName} = injector.built.get(${existingToken}, scope, optional);`; } else if (isFactoryProvider(provider)) { transient = provider.transient === true; const args: string[] = []; @@ -440,7 +818,7 @@ export class Injector implements InjectorInterface { }, provider, compiler, resolveDependenciesFrom, ofName, args.length, 'factoryDependencyNotFound')); } - factory.code = `${accessor} = ${compiler.reserveVariable('factory', provider.useFactory)}(${args.join(', ')});`; + factory.code = `${varName} = ${compiler.reserveVariable('factory', provider.useFactory)}(${args.join(', ')});`; } else { throw new Error('Invalid provider'); } @@ -459,7 +837,7 @@ export class Injector implements InjectorInterface { }); for (const configure of configurations) { - const args: string[] = [accessor]; + const args: string[] = [varName]; const reflection = ReflectionFunction.from(configure.call); const ofName = reflection.name === 'anonymous' ? 'configureProvider' : reflection.name; @@ -474,7 +852,7 @@ export class Injector implements InjectorInterface { const call = `${compiler.reserveVariable('configure', configure.call)}(${args.join(', ')});`; if (configure.options.replace) { - configureProvider.push(`${accessor} = ${call}`); + configureProvider.push(`${varName} = ${call}`); } else { configureProvider.push(call); } @@ -484,43 +862,21 @@ export class Injector implements InjectorInterface { configureProvider.push('//no custom provider setup'); } - let scopeCheck = scope ? ` && scope && scope.name === ${JSON.stringify(scope)}` : ''; - - //circular dependencies can happen, when for example a service with InjectorContext injected manually instantiates a service. - //if that service references back to the first one, it will be a circular loop. So we track that with `creating` state. - const creatingVar = `creating_${name}`; - const circularDependencyCheckStart = factory.dependencies ? `if (${creatingVar}) throwCircularDependency();${creatingVar} = true;` : ''; - const circularDependencyCheckEnd = factory.dependencies ? `${creatingVar} = false;` : ''; - - if (scopeCheck) scopeCheck = `&& scopes.push(${JSON.stringify(scope)}) ${scopeCheck}`; - - return ` - //${tokenLabel(token)}, from ${resolveDependenciesFrom.map(getClassName).join(', ')} - case token === ${tokenVar}${scopeCheck}: { - ${!transient ? `if (${accessor} !== undefined) return ${accessor};` : ''} - CircularDetector.push(${tokenVar}); - ${circularDependencyCheckStart} - injector.instantiated.${name} = injector.instantiated.${name} ? injector.instantiated.${name} + 1 : 1; - try { - ${factory.code} - } finally { - ${circularDependencyCheckEnd} - CircularDetector.pop(); - } - if (${accessor} !== undefined) { - ${configureProvider.join('\n')} - return ${accessor}; - } - if (!optional) { - knownServiceNotfoundError(tokenLabel(token), scopes, scope); - } - return; + return { + transient, + dependencies: factory.dependencies, + needsDestination: !!provider.needsDestination, + code: ` + ${factory.code} + if (${varName} !== undefined) { + ${configureProvider.join('\n')} } - `; + `, + }; } protected createFactory( - provider: NormalizedProvider, + provider: BuiltNormalizedProvider, resolvedName: string, compiler: CompilerContext, classType: ClassType, @@ -572,7 +928,7 @@ export class Injector implements InjectorInterface { protected createFactoryProperty( options: { name: string, type: Type, optional: boolean }, - fromProvider: NormalizedProvider, + fromProvider: BuiltNormalizedProvider, compiler: CompilerContext, resolveDependenciesFrom: InjectorModule[], ofName: string, @@ -580,7 +936,6 @@ export class Injector implements InjectorInterface { notFoundFunction: string, ): string { let of = `${ofName}.${options.name}`; - const destinationVar = compiler.reserveConst({ token: fromProvider.provide }); if (options.type.kind === ReflectionKind.class) { const found = findModuleForConfig(options.type.classType, resolveDependenciesFrom); @@ -593,7 +948,7 @@ export class Injector implements InjectorInterface { if (fromProvider.transient === true) { const tokenVar = compiler.reserveVariable('token', options.type.classType); const orThrow = options.optional ? '' : `?? transientInjectionTargetUnavailable(${JSON.stringify(ofName)}, ${JSON.stringify(options.name)}, ${argPosition}, ${tokenVar})`; - return `createTransientInjectionTarget(destination) ${orThrow}`; + return `createTransientInjectionTarget(runtimeContext.destination) ${orThrow}`; } else { throw new Error(`Cannot inject ${TransientInjectionTarget.name} into ${JSON.stringify(ofName)}.${JSON.stringify(options.name)}, as ${JSON.stringify(ofName)} is not transient`); } @@ -617,7 +972,7 @@ export class Injector implements InjectorInterface { const entries = this.buildContext.tagRegistry.resolve(options.type.classType); const args: string[] = []; for (const entry of entries) { - args.push(`${compiler.reserveConst(entry.module)}.injector.resolver(${compiler.reserveConst(getContainerToken(entry.tagProvider.provider.provide))}, scope, ${destinationVar})`); + args.push(`${compiler.reserveConst(entry.module)}.injector.built.get(${compiler.reserveConst(getContainerToken(entry.tagProvider.provider.provide))}, scope)`); } return `new ${tokenVar}(${resolvedVar} || (${resolvedVar} = [${args.join(', ')}]))`; } @@ -682,7 +1037,7 @@ export class Injector implements InjectorInterface { } } - let foundPreparedProvider: PreparedProvider | undefined = undefined; + let foundPreparedProvider: BuiltPreparedProvider | undefined = undefined; for (const module of resolveDependenciesFrom) { foundPreparedProvider = module.getPreparedProvider(options.type, foundPreparedProvider); } @@ -736,30 +1091,84 @@ export class Injector implements InjectorInterface { //in this case, if the dependency is not optional, we throw an error. const orThrow = options.optional ? '' : `?? ${notFoundFunction}(${JSON.stringify(ofName)}, ${JSON.stringify(options.name)}, ${argPosition}, ${tokenVar})`; + if (isTransientInjectionTargetProvider(foundPreparedProvider)) { + fromProvider.needsDestination = true; + } + + if (foundPreparedProvider.resolveFrom) { + const injectorVar = compiler.reserveConst(foundPreparedProvider.resolveFrom.injector, 'injector'); + return `${injectorVar}.built.get(${tokenVar}, scope, true) ${orThrow}`; + } + const resolveFromModule = foundPreparedProvider.resolveFrom || foundPreparedProvider.modules[0]; if (resolveFromModule === this.module) { - return `injector.resolver(${tokenVar}, scope, ${destinationVar}, undefined, true) ${orThrow}`; + if (foundPreparedProvider.factory) { + return `${foundPreparedProvider.factory}(scope, true) ${orThrow}`; + } + return `resolver(${tokenVar})(scope, true) ${orThrow}`; } - return `${compiler.reserveConst(resolveFromModule)}.injector.resolver(${tokenVar}, scope, ${destinationVar}, undefined, true) ${orThrow}`; + + // go through module injector + return `${compiler.reserveConst(resolveFromModule)}.injector.built.resolver(${tokenVar})(scope, true) ${orThrow}`; } - getResolver(token: ReceiveType | Token, label?: string): Resolver { - let resolver = this.resolverMap.get(token); - if (!resolver) { - resolver = this.createResolver(token as ReceiveType | Token, label); - this.resolverMap.set(token, resolver); + protected resolveType(type: Type): PreparedProvider | undefined { + const resolveDependenciesFrom = [this.module]; + + let foundPreparedProvider: PreparedProvider | undefined = undefined; + for (const module of resolveDependenciesFrom) { + foundPreparedProvider = module.getPreparedProvider(type, foundPreparedProvider); + } + + if (resolveDependenciesFrom[0] !== this.module) { + //the provider was exported from another module, so we need to check if there is a more specific candidate + foundPreparedProvider = this.module.getPreparedProvider(type, foundPreparedProvider); + } + + if (!foundPreparedProvider) { + //go up parent hierarchy + let current: InjectorModule | undefined = this.module; + while (current && !foundPreparedProvider) { + foundPreparedProvider = current.getPreparedProvider(type, foundPreparedProvider); + current = current.parent; + } + } + + return foundPreparedProvider; + } + + protected createSetter(token: ReceiveType | Token, label?: string): Setter { + if (token instanceof TagProvider) token = token.provider.provide; + + // todo remove isClass since it's slow + let type: Type | undefined = isType(token) ? token : isArray(token) || isClass(token) ? resolveReceiveType(token) : undefined; + + if (!type) { + const containerToken = getContainerToken(token as Token); + return this.built!.setter(containerToken); } - return resolver; + + const foundPreparedProvider = this.resolveType(type); + + if (!foundPreparedProvider) { + const t = stringifyType(type, { showFullDefinition: false }); + throw serviceNotFoundError(`${label}: ${t}`); + } + + const containerToken = getContainerToken(foundPreparedProvider.token); + const resolveFromModule = foundPreparedProvider.resolveFrom || foundPreparedProvider.modules[0]; + return resolveFromModule.injector!.built!.setter(containerToken); } - protected createResolver(token: ReceiveType | Token, label?: string): Resolver { + protected createResolver(token: ReceiveType | Token, label?: string): Resolver { if (token instanceof TagProvider) token = token.provider.provide; + // todo remove isClass since it's slow let type: Type | undefined = isType(token) ? token : isArray(token) || isClass(token) ? resolveReceiveType(token) : undefined; if (!type) { const containerToken = getContainerToken(token as Token); - return (scope?: Scope) => this.resolver!(containerToken, scope); + return this.built!.resolver(containerToken); } const resolveDependenciesFrom = [this.module]; @@ -790,7 +1199,7 @@ export class Injector implements InjectorInterface { const entries = this.buildContext.tagRegistry.resolve(type.classType); const args: any[] = []; for (const entry of entries) { - args.push(entry.module.injector!.resolver!(entry.tagProvider.provider.provide)); + args.push(entry.module.injector!.built!.resolver(entry.tagProvider.provider.provide)); } return new type.classType(args); @@ -854,65 +1263,22 @@ export class Injector implements InjectorInterface { } } - let foundPreparedProvider: PreparedProvider | undefined = undefined; - for (const module of resolveDependenciesFrom) { - foundPreparedProvider = module.getPreparedProvider(type, foundPreparedProvider); - } - - if (resolveDependenciesFrom[0] !== this.module) { - //the provider was exported from another module, so we need to check if there is a more specific candidate - foundPreparedProvider = this.module.getPreparedProvider(type, foundPreparedProvider); - } - - if (!foundPreparedProvider) { - //go up parent hierarchy - let current: InjectorModule | undefined = this.module; - while (current && !foundPreparedProvider) { - foundPreparedProvider = current.getPreparedProvider(type, foundPreparedProvider); - current = current.parent; - } - } - - const t = stringifyType(type, { showFullDefinition: false }); - + const foundPreparedProvider = this.resolveType(type); if (!foundPreparedProvider) { if (optional) return () => undefined; - throw new ServiceNotFoundError( - `Service '${label ? label + ': ' : ''}${t}' not found. No matching provider.`, - ); - } - // const allPossibleScopes = foundPreparedProvider.providers.map(getScope); - // const unscoped = allPossibleScopes.includes('') && allPossibleScopes.length === 1; - // - // if (!unscoped && !allPossibleScopes.includes(fromScope)) { - // const t = stringifyType(type, { showFullDefinition: false }); - // throw new ServiceNotFoundError( - // `Service "${t}" can not be received from ${fromScope ? 'scope ' + fromScope : 'no scope'}, ` + - // `since it only exists in scope${allPossibleScopes.length === 1 ? '' : 's'} ${allPossibleScopes.join(', ')}.` - // ); - // } - - const resolveFromModule = foundPreparedProvider.resolveFrom || foundPreparedProvider.modules[0]; + const t = stringifyType(type, { showFullDefinition: false }); + const message = label ? `${label}: ${t}` : t; + throw serviceNotFoundError(message); + } const containerToken = getContainerToken(foundPreparedProvider.token); - const injectorResolver = resolveFromModule.injector!.resolver!; - - const scopes = foundPreparedProvider.providers.map(getScope); - const transient = foundPreparedProvider.providers.some(v => isTransient(v)); - - let instance: any = undefined; - - const resolve = (scope?: Scope, optional?: boolean) => { - return injectorResolver(containerToken, scope, undefined, undefined, optional); + const resolveFromModule = foundPreparedProvider.resolveFrom || foundPreparedProvider.modules[0]; + if (!resolveFromModule.injector!.built) { + throw new Error('Injector was not built'); } - if (scopes.length || transient) return resolve; - - return (scope?: Scope, optional?: boolean) => { - instance = instance || resolve(scope, optional); - return instance; - }; + return resolveFromModule.injector!.built!.resolver(containerToken); } } @@ -926,7 +1292,12 @@ class BuildProviderIndex { export class BuildContext { static ids: number = 0; - public id: number = BuildContext.ids++; + id: number = BuildContext.ids++; + + // this is shared in all built injectors to track the instantiation stack + // for circular dependency detection. + runtimeContext = { creation: 0 }; + tagRegistry: TagRegistry = new TagRegistry; providerIndex: BuildProviderIndex = new BuildProviderIndex; @@ -938,6 +1309,7 @@ export class BuildContext { } export type Resolver = (scope?: Scope, optional?: boolean) => T; +export type Setter = (value: T, scope?: Scope, optional?: boolean) => void; /** * A InjectorContext is responsible for taking a root InjectorModule and build all Injectors. @@ -946,9 +1318,8 @@ export type Resolver = (scope?: Scope, optional?: boolean) => T; */ export class InjectorContext { constructor( - public rootModule: InjectorModule, - public readonly scope?: Scope, - protected buildContext: BuildContext = new BuildContext, + public module: InjectorModule, + public scope?: Scope, ) { } @@ -956,8 +1327,12 @@ export class InjectorContext { * Returns a resolver for the given token. The returned resolver can * be executed to resolve the token. This increases performance in hot paths. */ - resolve(module?: InjectorModule, type?: ReceiveType | Token): Resolver { - return this.getInjector(module || this.rootModule).getResolver(type) as Resolver; + resolver(module?: InjectorModule, type?: ReceiveType | Token): Resolver { + return this.getInjector(module || this.module).getResolver(type) as Resolver; + } + + setter(module?: InjectorModule, type?: ReceiveType | Token): Setter { + return this.getInjector(module || this.module).getSetter(type) as Setter; } /** @@ -966,7 +1341,7 @@ export class InjectorContext { * If there is no provider for the token or the provider returns undefined, it returns undefined. */ getOrUndefined(token?: ReceiveType | Token, module?: InjectorModule): ResolveToken | undefined { - const injector = this.getInjector(module || this.rootModule); + const injector = (module || this.module).getOrCreateInjector(); return injector.get(token, this.scope, true); } @@ -976,17 +1351,18 @@ export class InjectorContext { * If there is no provider for the token or the provider returns undefined, it throws an error. */ get(token?: ReceiveType | Token, module?: InjectorModule): ResolveToken { - const injector = this.getInjector(module || this.rootModule); - return injector.get(token, this.scope); + const injector = (module || this.module).getOrCreateInjector(); + return injector.get(token, this.scope) as ResolveToken; } /** * Returns the instantiation count of the given token. * - * This is either 0 or 1 for normal providers, and >= 0 for transient providers. + * This is either 0 or 1 for normal providers, and >= 0 for transient or scoped providers. */ instantiationCount(token: Token, module?: InjectorModule, scope?: string): number { - return this.getInjector(module || this.rootModule).instantiationCount(token, this.scope ? this.scope.name : scope); + const injector = this.getInjector(module || this.module); + return injector.instantiationCount(token, scope ? { name: scope } : this.scope); } /** @@ -996,30 +1372,24 @@ export class InjectorContext { * outside the injector container and need to be injected into services. */ set(token: T, value: any, module?: InjectorModule): void { - return this.getInjector(module || this.rootModule).set( - getContainerToken(token), - value, - this.scope, - ); + const injector = (module || this.module).getOrCreateInjector(); + return injector.set(token, value, this.scope); } static forProviders(providers: ProviderWithScope[]) { return new InjectorContext(new InjectorModule(providers)); } - /** - * Returns the unscoped injector. Use `.get(T, Scope)` for resolving scoped token. - */ getInjector(module: InjectorModule): Injector { - return module.getOrCreateInjector(this.buildContext); + return module.getOrCreateInjector(); } getRootInjector(): Injector { - return this.getInjector(this.rootModule); + return this.getInjector(this.module); } - public createChildScope(scope: string): InjectorContext { - return new InjectorContext(this.rootModule, { name: scope, instances: {} }, this.buildContext); + createChildScope(scope: string): InjectorContext { + return new InjectorContext(this.module, { name: scope }); } } diff --git a/packages/injector/src/module.ts b/packages/injector/src/module.ts index 755d0ceba..9f43f5d2a 100644 --- a/packages/injector/src/module.ts +++ b/packages/injector/src/module.ts @@ -487,9 +487,11 @@ export class InjectorModule { return this; } - getOrCreateInjector(buildContext: BuildContext): Injector { + getOrCreateInjector(buildContext?: BuildContext): Injector { if (this.injector) return this.injector; + buildContext ||= new BuildContext; + //notify everyone we know to prepare providers if (this.parent) this.parent.getPreparedProviders(buildContext); this.getPreparedProviders(buildContext); diff --git a/packages/injector/src/provider.ts b/packages/injector/src/provider.ts index 9db72fc25..b74ffd41d 100644 --- a/packages/injector/src/provider.ts +++ b/packages/injector/src/provider.ts @@ -21,26 +21,26 @@ export interface ProviderBase { /** @reflection never */ export interface ProviderScope { - scope?: 'module' | 'rpc' | 'http' | 'cli' | string; + scope?: 'rpc' | 'http' | 'cli' | string; } /** @reflection never */ export type Token = symbol | number | bigint | boolean | string | AbstractClassType | Type | TagProvider | Function | T; export function provide( - provider: - | (ProviderBase & ProviderScope & - ( - | { useValue: T } - | { useClass: ClassType } - | { useExisting: any } - | { useFactory: (...args: any[]) => T | undefined } - )) + provider?: + | (ProviderBase & ProviderScope & ( + | { useValue?: T } + | { useClass: ClassType } + | { useExisting: any } + | { useFactory: (...args: any[]) => T | undefined } + )) | ClassType | ((...args: any[]) => T) , type?: ReceiveType, ): NormalizedProvider { + if (!provider) return { provide: resolveReceiveType(type) }; if (isClass(provider)) return { provide: resolveReceiveType(type), useClass: provider }; if (isFunction(provider)) return { provide: resolveReceiveType(type), useFactory: provider }; return { ...provider, provide: resolveReceiveType(type) }; @@ -109,7 +109,7 @@ interface TagRegistryEntry { /** @reflection never */ export class TagRegistry { constructor( - public tags: TagRegistryEntry[] = [] + public tags: TagRegistryEntry[] = [], ) { } @@ -137,7 +137,7 @@ export class Tag = TagProvider> { _2!: () => TP; constructor( - public readonly services: T[] = [] + public readonly services: T[] = [], ) { } diff --git a/packages/injector/tests/injector.spec.ts b/packages/injector/tests/injector.spec.ts index 0582d3da2..7ab0201fe 100644 --- a/packages/injector/tests/injector.spec.ts +++ b/packages/injector/tests/injector.spec.ts @@ -9,7 +9,7 @@ import { TransientInjectionTarget, } from '../src/injector.js'; import { InjectorModule } from '../src/module.js'; -import { ReflectionClass, ReflectionKind } from '@deepkit/type'; +import { ReflectionClass, ReflectionKind, typeOf } from '@deepkit/type'; import { Inject } from '../src/types.js'; import { provide } from '../src/provider.js'; @@ -28,15 +28,35 @@ test('injector basics', () => { const injector = Injector.from([MyServer, Connection]); expect(injector.get(Connection)).toBeInstanceOf(Connection); expect(injector.get(MyServer)).toBeInstanceOf(MyServer); + expect(injector.get()).toBeInstanceOf(Connection); }); -test('type injection', () => { +test('useExisting 1', () => { class Service {} const injector = Injector.from([Service, { provide: 'token', useExisting: Service }]); + expect(injector.get(Service)).toBeInstanceOf(Service); expect(injector.get()).toBeInstanceOf(Service); expect(injector.get('token')).toBeInstanceOf(Service); }); +test('useExisting 2', () => { + class Service {} + class Service2 {} + const injector = Injector.from([Service, provide({useExisting: Service})]); + expect(injector.get(Service)).toBeInstanceOf(Service); + expect(injector.get()).toBeInstanceOf(Service); +}); + +test('useExisting 3', () => { + class Service {} + class Service2 {} + const injector = Injector.from([provide(), provide({useExisting: typeOf()})]); + expect(injector.get(Service)).toBeInstanceOf(Service); + expect(injector.get(Service2)).toBeInstanceOf(Service); + expect(injector.get()).toBeInstanceOf(Service); + expect(injector.get()).toBeInstanceOf(Service); +}); + test('missing dep', () => { class Connection { } @@ -481,6 +501,36 @@ test('injector overwrite provider', () => { } }); +declare var asd: any; + +test('invalid constructor 1', () => { + class Service {} + + class MyServer { + constructor() { + asd.asd = []; + } + } + + const injector = Injector.from([MyServer]); + expect(() => injector.get(MyServer)).toThrow(`asd is not defined`); + expect(() => injector.get(MyServer)).toThrow(`asd is not defined`); +}); + +test('invalid constructor 2', () => { + class Service {} + + class MyServer { + constructor(private service: Service) { + asd.asd = []; + } + } + + const injector = Injector.from([MyServer, Service]); + expect(() => injector.get(MyServer)).toThrow(`asd is not defined`); + expect(() => injector.get(MyServer)).toThrow(`asd is not defined`); +}); + test('injector direct circular dependency', () => { class MyServer { constructor(private myServer: MyServer) { @@ -944,7 +994,7 @@ test('PartialFactory', () => { } const injector = Injector.from([B]); - const factory = injector.get>(); + const factory = injector.getResolver>()(); const a = factory({ num: 5, diff --git a/packages/injector/tests/injector2.spec.ts b/packages/injector/tests/injector2.spec.ts index 7e76cc5ec..6f4f157d2 100644 --- a/packages/injector/tests/injector2.spec.ts +++ b/packages/injector/tests/injector2.spec.ts @@ -42,7 +42,7 @@ test('scoped provider', () => { const context = new InjectorContext(module1); const injector = context.getInjector(module1); - expect(() => injector.get(Service)).toThrow(`Service 'Service' is known but has no value. Available in scopes: http, requested scope is global`); + expect(() => injector.get(Service)).toThrow(`Service 'Service' is known but is not available in scope global. Available in scopes: http.`); expect(context.createChildScope('http').get(Service)).toBeInstanceOf(Service); @@ -88,8 +88,8 @@ test('undefined scoped provider', () => { expect(() => scope.get(Service)).toThrow(`Service 'Service' is known but has no value. Available in scopes: http, requested scope is http`); - const resolver = scope.resolve(module1, Service); - expect(() => resolver()).toThrow(`Service 'Service' is known but has no value. Available in scopes: http, requested scope is global`); + const resolver = scope.resolver(module1, Service); + expect(() => resolver()).toThrow(`Service 'Service' is known but is not available in scope global. Available in scopes: http`); expect(resolver(undefined, true)).toBe(undefined); const controllerOptional = scope.get(ControllerOptional); @@ -109,6 +109,14 @@ test('undefined scoped provider', () => { expect(controllerRequired).toBeInstanceOf(ControllerRequired); expect(controllerRequired.service).toBeInstanceOf(Service); + + const scope2 = context.createChildScope('http'); + const setter = context.setter(module1, Service); + setter(new Service(), scope2.scope); + + expectService = scope2.get(Service); + expect(expectService).toBeInstanceOf(Service); + }); test('scoped provider with dependency to unscoped', () => { @@ -119,7 +127,7 @@ test('scoped provider with dependency to unscoped', () => { const context = new InjectorContext(module1); const injector = context.getInjector(module1); - expect(() => injector.get(Service)).toThrow(`Service 'Service' is known but has no value. Available in scopes: http, requested scope is global`); + expect(() => injector.get(Service)).toThrow(`Service 'Service' is known but is not available in scope global. Available in scopes: http`); const scope = context.createChildScope('http'); expect(scope.get(Service)).toBeInstanceOf(Service); @@ -186,6 +194,7 @@ test('tags', () => { class CollectorManager { constructor(protected collectors: CollectorTag) { + debugger; } getCollectors(): Collector[] { @@ -445,7 +454,7 @@ test('scope merging', () => { expect(request2).toBeInstanceOf(Request); expect(request2.id).toBe(2); //last provider is used - const request3 = injector.createChildScope('unknown').get(Request); + const request3 = injector.get(Request); expect(request3).toBeInstanceOf(Request); expect(request3.id).toBe(-1); //unscoped } @@ -1198,16 +1207,16 @@ test('instantiatedCount scope', () => { const root = new InjectorModule([{ provide: Request, scope: 'rpc' }]).addImport(module1); const injector = new InjectorContext(root); - expect(injector.instantiationCount(Request, module1)).toBe(0); + expect(() => injector.instantiationCount(Request, module1)).toThrow(`Service 'Request' is known but is not available in scope global`) { - expect(injector.createChildScope('http').instantiationCount(Request, module1, 'http')).toBe(0); + expect(injector.createChildScope('http').instantiationCount(Request, module1)).toBe(0); injector.createChildScope('http').get(Request, module1); - expect(injector.instantiationCount(Request, module1)).toBe(0); + expect(injector.instantiationCount(Request, module1, 'http')).toBe(1); - expect(injector.createChildScope('http').instantiationCount(Request, module1, 'http')).toBe(1); + expect(injector.createChildScope('http').instantiationCount(Request, module1)).toBe(1); injector.createChildScope('http').get(Request, module1); - expect(injector.createChildScope('http').instantiationCount(Request, module1, 'http')).toBe(2); + expect(injector.createChildScope('http').instantiationCount(Request, module1)).toBe(2); injector.createChildScope('rpc').get(Request); expect(injector.instantiationCount(Request, undefined, 'http')).toBe(2); expect(injector.instantiationCount(Request, undefined, 'rpc')).toBe(1); @@ -1280,7 +1289,7 @@ test('exported scoped can be replaced for another scope', () => { const root = new InjectorModule().addImport(frameworkModule); const injector = new InjectorContext(root); - expect(() => injector.get(HttpRequest)).toThrow(`Service 'HttpRequest' is known but has no value`); + expect(() => injector.get(HttpRequest)).toThrow(`Service 'HttpRequest' is known but is not available in scope global`); const scopeHttp = injector.createChildScope('http'); const httpRequest1 = scopeHttp.get(HttpRequest); diff --git a/packages/injector/tests/injector3.spec.ts b/packages/injector/tests/injector3.spec.ts new file mode 100644 index 000000000..dabf2001b --- /dev/null +++ b/packages/injector/tests/injector3.spec.ts @@ -0,0 +1,130 @@ +import { expect, test } from '@jest/globals'; +import { InjectorModule } from '../src/module.js'; +import { InjectorContext } from '../src/injector.js'; +import { provide } from '../src/provider.js'; + +test('class + scope support', () => { + class ServiceA { + } + + class ServiceB { + constructor(public serviceA: ServiceA) { + } + } + + class ScopedServiceC { + constructor(public serviceA: ServiceA) { + } + } + + const providers = [ + { provide: ServiceA }, + { provide: ServiceB }, + { provide: ScopedServiceC, scope: 'rpc' }, + ]; + + const module = new InjectorModule(providers); + const injector = new InjectorContext(module); + + const a = injector.get(ServiceA); + const b = injector.get(ServiceB); + + expect(a).toBeInstanceOf(ServiceA); + expect(b).toBeInstanceOf(ServiceB); + + const scope1 = injector.createChildScope('rpc'); + const c1 = scope1.get(ScopedServiceC); + expect(c1).toBeInstanceOf(ScopedServiceC); + + const resolvedScopedServiceC = injector.resolver(undefined, ScopedServiceC); + const scope = injector.createChildScope('rpc'); + const c2 = resolvedScopedServiceC(scope.scope); + expect(c2).toBeInstanceOf(ScopedServiceC); +}); + +test('type + scope support', () => { + class ServiceA { + } + + class ServiceB { + constructor(public serviceA: ServiceA) { + } + } + + class ScopedServiceC { + constructor(public scope: string) { + } + } + + const providers = [ + provide(ServiceA), + provide(ServiceB), + provide({ scope: 'rpc', useValue: new ScopedServiceC('rpc') }), + provide({ scope: 'http', useValue: new ScopedServiceC('http') }), + ]; + + const module = new InjectorModule(providers); + const injector = new InjectorContext(module); + + expect(injector.get(ServiceA)).toBeInstanceOf(ServiceA); + expect(injector.get(ServiceB)).toBeInstanceOf(ServiceB); + expect(injector.get()).toBeInstanceOf(ServiceA); + expect(injector.get()).toBeInstanceOf(ServiceB); + + const scope1 = injector.createChildScope('rpc'); + const c1 = scope1.get(ScopedServiceC); + expect(c1).toBeInstanceOf(ScopedServiceC); + expect(c1.scope).toBe('rpc'); + + const resolvedScopedServiceC = injector.resolver(undefined, ScopedServiceC); + { + const scope = injector.createChildScope('rpc'); + const c2 = resolvedScopedServiceC(scope.scope); + expect(c2).toBeInstanceOf(ScopedServiceC); + expect(c1.scope).toBe('rpc'); + } + { + const scope = injector.createChildScope('http'); + const c2 = resolvedScopedServiceC(scope.scope); + expect(c2).toBeInstanceOf(ScopedServiceC); + expect(c2.scope).toBe('http'); + } +}); + +test('exported provider', () => { + class ModuleA extends InjectorModule { + } + + class ModuleB extends InjectorModule { + } + + class ServiceA { + } + + class ServiceB { + constructor(public serviceA: ServiceA) { + } + } + + const moduleB = new ModuleB([ + { provide: ServiceB, scope: 'rpc' }, + ]).addExport(ServiceB); + + const moduleA = new ModuleA([ + ServiceA, + { provide: ServiceB, scope: 'rpc' }, + ]).addImport(moduleB); + + const injector = new InjectorContext(moduleA); + const a = injector.get(ServiceA); + expect(a).toBeInstanceOf(ServiceA); + + expect(() => injector.get(ServiceB)).toThrowError('Service \'ServiceB\' is known but is not available in scope global. Available in scopes: rpc'); + + const scope = injector.createChildScope('rpc'); + const b1 = scope.get(ServiceB); + expect(b1).toBeInstanceOf(ServiceB); + + const b2 = scope.get(ServiceB, moduleB); + expect(b2).toBeInstanceOf(ServiceB); +}); diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index 9e5897be5..1806cdb54 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -22,6 +22,7 @@ export enum LoggerLevel { log, info, debug, + debug2, // very verbose debug output } declare var process: { @@ -197,6 +198,8 @@ export interface LoggerInterface { info(...message: any[]): void; debug(...message: any[]): void; + + debug2(...message: any[]): void; } export class Logger implements LoggerInterface { @@ -208,7 +211,7 @@ export class Logger implements LoggerInterface { */ level: LoggerLevel = LoggerLevel.info; - protected debugScopes = new Set(); + protected debugScopes = new Map(); protected logData?: LogData; @@ -227,15 +230,19 @@ export class Logger implements LoggerInterface { * This is useful to enable debug logs only for certain parts of your application. */ enableDebugScope(name: string) { - this.debugScopes.add(name); + this.debugScopes.set(name, true); } disableDebugScope(name: string) { + this.debugScopes.set(name, false); + } + + unsetDebugScope(name: string) { this.debugScopes.delete(name); } isDebugScopeEnabled(name: string): boolean { - return this.debugScopes.has(name); + return this.debugScopes.get(name) ?? true; } /** @@ -329,7 +336,7 @@ export class Logger implements LoggerInterface { } is(level: LoggerLevel): boolean { - return level <= this.level || (level === LoggerLevel.debug && this.debugScopes.has(this.scope)); + return level <= this.level || (level >= LoggerLevel.debug && (this.debugScopes.get(this.scope) ?? true)); } protected send(messages: any[], level: LoggerLevel, data: LogData = {}) { @@ -379,6 +386,10 @@ export class Logger implements LoggerInterface { debug(...message: any[]) { this.send(message, LoggerLevel.debug); } + + debug2(...message: any[]) { + this.send(message, LoggerLevel.debug2); + } } /** diff --git a/packages/rpc/.npmignore b/packages/rpc/.npmignore index 2b29f2764..514286556 100644 --- a/packages/rpc/.npmignore +++ b/packages/rpc/.npmignore @@ -1 +1,2 @@ tests +benchmarks diff --git a/packages/rpc/benchmarks/action.ts b/packages/rpc/benchmarks/action.ts new file mode 100644 index 000000000..0cdf455e5 --- /dev/null +++ b/packages/rpc/benchmarks/action.ts @@ -0,0 +1,60 @@ +import { benchmark, run } from '@deepkit/bench'; +import { rpc } from '../src/decorators.js'; +import { ActionDispatcher } from '../src/action.js'; +import { InjectorContext, InjectorModule } from '@deepkit/injector'; +import { getActionOffset, MessageFlag, writeAction } from '../src/protocol.js'; +import { Writer } from '@deepkit/bson'; + +let calls1 = 0; +let calls2 = 0; + +class Controller { + @rpc.action() + myAction() { + calls1++; + } + + @rpc.action() + test2(a: number) { + calls2++; + } +} + +const module = new InjectorModule([ + { provide: Controller, scope: 'rpc' }, +]); + +const injector = new InjectorContext(module); + +const actions = new ActionDispatcher; +actions.build(injector, [{ name: 'test', controller: Controller, module }]); + +const message1 = new Uint8Array(32); +message1[0] = MessageFlag.TypeAction; +writeAction(message1, 0); + +const scope = injector.createChildScope('rpc'); +actions.dispatch(message1, scope.scope!); + +console.log('calls', calls1); + +benchmark('action dispatcher', () => { + // we need to optimise scope. Turn it into an array of instances not a object + actions.dispatch(message1, scope.scope!); +}); + +const message2 = new Uint8Array(3 + 8); +writeAction(message2, 1); +const bodyOffset = getActionOffset(message2[0]) + 2; +const writer = new Writer(message2, bodyOffset); +writer.writeDouble(256); + +benchmark('action with arg', () => { + actions.dispatch(message2, scope.scope!); +}); + +run().then(() => { + console.log('done'); + console.log('calls1', calls1); + console.log('calls2', calls2); +}) diff --git a/packages/rpc/benchmarks/dispatcher.ts b/packages/rpc/benchmarks/dispatcher.ts new file mode 100644 index 000000000..6f0ccbae5 --- /dev/null +++ b/packages/rpc/benchmarks/dispatcher.ts @@ -0,0 +1,73 @@ +import { ContextDispatcher } from '../src/protocol.js'; +import { benchmark, run } from '@deepkit/bench'; + +const message = new Uint8Array([1, 2, 3]); + +function noop() { +} + +class BaseLine extends ContextDispatcher { + protected fn: any = noop; + + create(_fn: any) { + this.fn = _fn; + return 0; + } + + release(id: number) { + } + + dispatch(id: number, message: Uint8Array) { + this.fn(message); + } +} + +function benchmarkReal(dispatcher: ContextDispatcher) { + const ids: number[] = []; + let created = 0; + let peak = 0; + return () => { + // Randomly allocate/release to simulate dynamic usage + if (Math.random() < 0.5 && ids.length > 0) { + const id = ids.pop()!; + dispatcher.release(id); + created--; + } else { + let fn: any = noop; + if (Math.random() < 0.5) { + let i = 0; + fn = (message: any) => { + message[0] = i; + }; + } + const id = dispatcher.create(fn); + created++; + if (created > peak) { + peak = created; + } + ids.push(id); + } + + if (ids.length > 0) { + dispatcher.dispatch(ids[Math.floor(Math.random() * ids.length)], message); + } + }; +} + +// this test is just to monitor the GC of our implementation. this should not allocate any memory +function benchmarkSimple(dispatcher: ContextDispatcher) { + let id = 0; + return () => { + id = dispatcher.create(noop); + dispatcher.dispatch(id, message); + dispatcher.release(id); + }; +} + +benchmark('BaseLine realistic', benchmarkReal(new BaseLine)); +benchmark('ContextDispatcher realistic', benchmarkReal(new ContextDispatcher)); +benchmark('BaseLine simple', benchmarkSimple(new ContextDispatcher)); +benchmark('ContextDispatcher simple', benchmarkSimple(new ContextDispatcher)); +benchmark('baseline', () => undefined); + +run(); diff --git a/packages/rpc/benchmarks/kernel.ts b/packages/rpc/benchmarks/kernel.ts new file mode 100644 index 000000000..40878bc66 --- /dev/null +++ b/packages/rpc/benchmarks/kernel.ts @@ -0,0 +1,69 @@ +import { benchmark run } from '@deepkit/bench'; +import { rpc } from '../src/decorators.js'; +import { RpcKernel } from '../src/server/kernel.js'; +import { MessageFlag, writeAction } from '../src/protocol.js'; + +let calls = 0; + +class Controller { + @rpc.action() + myAction() { + calls++; + } +} + +const kernel = new RpcKernel; +kernel.registerController(Controller, 'main'); + +const message = new Uint8Array(32); +message[0] = MessageFlag.TypeAction; +writeAction(message, 0); + +benchmark('kernel createConnection', () => { + const connection = kernel.createConnection({ + bufferedAmount(): number { + return 0; + }, + clientAddress(): string { + return ''; + }, + close() { + }, + write(data: Uint8Array) { + + }, + }); + + connection.close(); +}); + +const connection = kernel.createConnection({ + bufferedAmount(): number { + return 0; + }, + clientAddress(): string { + return ''; + }, + close() { + }, + write(data: Uint8Array) { + + }, +}); + +connection.feed(message); +connection.feed(message); +console.log('calls', calls); + +benchmark('connection.feed', () => { + connection.feed(message); +}); + +let i = 0; +benchmark('test', () => { + i++; +}); + +run(); + +console.log('i', i); diff --git a/packages/rpc/benchmarks/message.ts b/packages/rpc/benchmarks/message.ts new file mode 100644 index 000000000..386ab1f0c --- /dev/null +++ b/packages/rpc/benchmarks/message.ts @@ -0,0 +1,22 @@ +import { getAction, getContextId, MessageFlag, writeAction, writeContextNoRoute } from '../src/protocol.js'; +import { benchmark, run } from '@deepkit/bench'; +import { RpcAction } from '../src/model.js'; + +const message = new Uint8Array(32); + +// todo: this is faster in ESM. We have to switch back to CJS in @deepkit/run +// => ok fixed by using node 23 +// const b = writeContext; + +benchmark('write', () => { + message[0] = MessageFlag.RouteClient | MessageFlag.ContextExisting | MessageFlag.TypeAction; + writeContextNoRoute(message, 1); + writeAction(message, RpcAction.Ping); +}); + +benchmark('read', () => { + const contextId = getContextId(message); + const action = getAction(message); +}); + +run(); diff --git a/packages/rpc/benchmarks/proxy.ts b/packages/rpc/benchmarks/proxy.ts new file mode 100644 index 000000000..00298964b --- /dev/null +++ b/packages/rpc/benchmarks/proxy.ts @@ -0,0 +1,31 @@ +import { benchmark, run } from '@deepkit/bench'; + +let count = 0; + +function test() { + count++; +} + +const map = { + test: test, +}; + +function noop() { +} + +const proxy: any = new Proxy({}, { + get(target, key) { + const fn = (map as any)[key]; + if (fn) { + return fn; + } + return noop; + }, +}); + +benchmark('action proxy', () => { + proxy.test(); +}); + + +run(); diff --git a/packages/rpc/benchmarks/round-trip.ts b/packages/rpc/benchmarks/round-trip.ts new file mode 100644 index 000000000..792ff4a40 --- /dev/null +++ b/packages/rpc/benchmarks/round-trip.ts @@ -0,0 +1,65 @@ +import { benchmark, run } from '@deepkit/bench'; +import { rpc } from '../src/decorators.js'; +import { RpcKernel } from '../src/server/kernel.js'; +import { isContextFlag, MessageFlag, setContextFlag, writeAction } from '../src/protocol.js'; +import { DirectClient } from '../src/client/client-direct.js'; + +let calls = 0; + +class Controller { + @rpc.action() + myAction() { + calls++; + } +} + +const kernel = new RpcKernel; +kernel.registerController(Controller, 'main'); + +const message = new Uint8Array(32); +message[0] = MessageFlag.TypeAction | MessageFlag.ContextNew; +writeAction(message, 0); +setContextFlag(message, MessageFlag.ContextNew); + +const client = new DirectClient(kernel); +void client.connect(); + +client.write(message); +client.write(message); +client.write(message); +console.log('calls', calls); + +benchmark('action - no response', function benchmarkAction1() { + setContextFlag(message, MessageFlag.ContextNone); + client.write(message); +}); + +function promiseCallback(resolve: any) { + const id = client.selfContext.create(function contextReply(reply) { + if (isContextFlag(reply[0], MessageFlag.ContextEnd)) { + client.selfContext.release(id); + } + resolve(); + }); + client.write(message); +} + +benchmark('action - promise response', async function benchmarkAction2() { + setContextFlag(message, MessageFlag.ContextNew); + await new Promise(promiseCallback); +}); + +benchmark('baseline empty promise', async function benchmarkAction2() { + await new Promise((resolve) =>{ + resolve(); + }); +}); + +run().then(() => { + console.log('calls', calls); + console.log('current context', client.selfContext.current()); + debugger; +}).catch((error: any) => { + console.log(error); +}); + diff --git a/packages/rpc/package.json b/packages/rpc/package.json index d64a3b570..30aada660 100644 --- a/packages/rpc/package.json +++ b/packages/rpc/package.json @@ -34,6 +34,7 @@ "dot-prop": "^5.1.1" }, "devDependencies": { + "@deepkit/bench": "^1.0.3", "@deepkit/bson": "^1.0.3", "@deepkit/core": "^1.0.3", "@deepkit/core-rxjs": "^1.0.3", diff --git a/packages/rpc/spec.md b/packages/rpc/spec.md new file mode 100644 index 000000000..9c04aeb51 --- /dev/null +++ b/packages/rpc/spec.md @@ -0,0 +1,35 @@ +# Architecture + + +``` +Client: + SendMessage() (RouteClient) + RpcClientTransporter + RpcBinaryMessageReader + remoteContext? + + selfContext: Context (RouteClient) + RpcActionClient (RouteClient) + + remote: RpcKernel (RouteServer) + Connection + SendMessage() (RouteServer) + MessageReader + + remoteContext: Context + RpcActionServer + + peers: RpcKernel[] (direct route) + +Server: + RpcKernel + Connection + SendMessage() (RouteServer) + MessageReader + + selfContext: Context (RouteServer) + RpcActionClient (RouteServer) + + remoteContext: Context (RouteClient) + RpcActionServer (RouteClient) +``` diff --git a/packages/rpc/src/action.ts b/packages/rpc/src/action.ts new file mode 100644 index 000000000..fc09af1ce --- /dev/null +++ b/packages/rpc/src/action.ts @@ -0,0 +1,200 @@ +import { ClassType, CompilerContext } from '@deepkit/core'; +import { InjectorContext, InjectorModule, Scope } from '@deepkit/injector'; +import { getActions, RpcAction, rpcClass } from './decorators.js'; +import { getActionOffset, readUint16LE } from './protocol.js'; +import { + binaryBigIntAnnotation, + executeTemplates, + isDateType, + isIntegerType, + isMongoIdType, + isUUIDType, + JitStack, + NamingStrategy, + ReflectionClass, + ReflectionKind, + TemplateState, + Type, + TypeTuple, + TypeTupleMember, +} from '@deepkit/type'; +import { BaseParser, bsonBinarySerializer, BSONType, seekElementSize } from '@deepkit/bson'; + +interface ActionState { + controller: ClassType; + method: Function; + resolver: (scope: Scope) => any; + action: RpcAction; +} + +type Apply = (bodyOffset: number, message: Uint8Array, scope: Scope) => void; + +function isSimpleType(type: Type): boolean { + return type.kind === ReflectionKind.string + || type.kind === ReflectionKind.number + || type.kind === ReflectionKind.boolean + || type.kind === ReflectionKind.bigint + || type.kind === ReflectionKind.object + || type.kind === ReflectionKind.array + || isMongoIdType(type) + || isDateType(type) + || isUUIDType(type); +} + +function getBSONType(type: Type): BSONType { + switch (type.kind) { + case ReflectionKind.string: { + if (isMongoIdType(type)) return BSONType.OID; + if (isUUIDType(type)) return BSONType.BINARY; + return BSONType.STRING; + } + case ReflectionKind.bigint: { + const binaryBigInt = binaryBigIntAnnotation.getFirst(type); + if (binaryBigInt) return BSONType.BINARY; + return BSONType.LONG; + } + case ReflectionKind.number: + if (isIntegerType(type)) return BSONType.INT; + return BSONType.NUMBER; + case ReflectionKind.boolean: + return BSONType.BOOLEAN; + case ReflectionKind.array: + return BSONType.ARRAY; + case ReflectionKind.class: { + if (isDateType(type)) return BSONType.DATE; + return BSONType.OBJECT; + } + } + throw new Error('Needs to be aligned with isSimpleType'); +} + +function createActionApply(state: ActionState): Apply { + const context = new CompilerContext(); + context.set({ + resolver: state.resolver, + action: state.action, + BaseParser, + seekElementSize, + }); + + const parse: string[] = []; + const args: string[] = []; + const methodReflection = ReflectionClass.from(state.controller).getMethod(state.action.name); + const parameters = methodReflection.getParameters(); + + if (parameters.length) { + const parser = new BaseParser(new Uint8Array, 0); + const state = { parser, elementType: 0 }; + context.set({ parser, state }); + // parse.push(`const view = new DataView(message.buffer, message.byteOffset, message.byteLength)`); + // parse.push(`const parser = new BaseParser(message, offset);`); + // parse.push(`const parser = new BaseParser(message, offset); const state = { parser: parser, elementType: 0 };`); + parse.push(`parser.buffer = message; parser.offset = offset; parser.size = message.byteLength;`); + + const isSimpleArguments = methodReflection.getParameters().every(p => !p.isOptional() && isSimpleType(p.type)); + const jitStack = new JitStack(); + const namingStrategy = new NamingStrategy(); + + if (isSimpleArguments) { + let i = 0; + + for (const parameter of parameters) { + const varName = context.reserveVariable('arg' + i++); + const state = new TemplateState(varName, '', context, bsonBinarySerializer.bsonDeserializeRegistry, namingStrategy, jitStack, ['']); + state.target = 'deserialize'; + parse.push(` + state.elementType = ${getBSONType(parameter.type)}; + ${executeTemplates(state, parameter.type)} + `); + args.push(varName); + } + } else { + // use a tuple deserializer + const tuple: TypeTuple = { + kind: ReflectionKind.tuple, + types: [], + }; + for (const parameter of parameters) { + const member: TypeTupleMember = { + kind: ReflectionKind.tupleMember, + type: parameter.type, + name: parameter.name, + parent: tuple, + }; + if (parameter.isOptional()) member.optional = true; + tuple.types.push(member); + } + const varName = context.reserveVariable('args'); + const state = new TemplateState(varName, '', context, bsonBinarySerializer.bsonDeserializeRegistry, namingStrategy, jitStack, ['']); + state.target = 'deserialize'; + parse.push(executeTemplates(state, tuple)); + args.push(`...${varName}`); + } + } + + return context.raw(` +return function apply(offset, message, scope) { + 'use strict'; + + const controller = resolver(scope); + + ${parse.join('\n')} + + return controller.${state.action.name}(${args.join(', ')}); +}; + `) as Apply; +} + +/** + * @reflection never + */ +export class ActionDispatcher { + // protected actions: ActionState[] = []; + protected actions: Apply[] = []; + + protected mapping: { [controllerName: string]: { actionName: string, action: number }[] } = {}; + + getMapping() { + return this.mapping; + } + + build(injector: InjectorContext, controllers: Iterable<{ name: string, controller: ClassType, module: InjectorModule }>) { + this.actions = []; + this.mapping = {}; + + for (const controller of controllers) { + const resolver = injector.resolver(controller.module, controller.controller); + const knownActions = getActions(controller.controller); + + let controllerName = controller.name; + const controllerMapping = this.mapping[controllerName] ||= []; + + const rpcConfig = rpcClass._fetch(controller.controller); + if (rpcConfig) controllerName = rpcConfig.getPath(); + + for (const [name, action] of knownActions) { + controllerMapping.push({ + actionName: name, + action: this.actions.length, + }); + + const state: ActionState = { + controller: controller.controller, + method: controller.controller.prototype[name], + resolver, + action, + }; + const apply = createActionApply(state); + this.actions.push(apply); + } + } + } + + dispatch(message: Uint8Array, scope: Scope) { + const offset = getActionOffset(message[0]); + const actionId = readUint16LE(message, offset); + const apply = this.actions[actionId]; + return apply(offset + 2, message, scope); + } +} + diff --git a/packages/rpc/src/client/action.ts b/packages/rpc/src/client/action.ts index 3173bee4c..b5c9cdd5a 100644 --- a/packages/rpc/src/client/action.ts +++ b/packages/rpc/src/client/action.ts @@ -8,6 +8,9 @@ * You should have received a copy of the MIT License along with this program. */ +/** + * @reflection never + */ import { asyncOperation, ClassType, toFastProperties } from '@deepkit/core'; import { BehaviorSubject, Observable, Subject, Subscriber } from 'rxjs'; import { skip } from 'rxjs/operators'; @@ -16,25 +19,24 @@ import { ActionMode, ActionObservableTypes, IdInterface, + RpcAction, rpcActionObservableNext, rpcActionObservableSubscribeId, - rpcActionType, RpcError, rpcResponseActionCollectionRemove, rpcResponseActionCollectionSort, rpcResponseActionObservable, rpcResponseActionObservableSubscriptionError, rpcResponseActionType, - RpcTypes, WrappedV, } from '../model.js'; -import { rpcDecodeError, RpcMessage } from '../protocol.js'; -import type { WritableClient } from './client.js'; import { EntityState, EntitySubjectStore } from './entity-state.js'; import { assertType, deserializeType, ReflectionKind, Type, TypeObjectLiteral, typeOf } from '@deepkit/type'; import { RpcMessageSubject } from './message-subject.js'; import { ClientProgress, Progress } from '../progress.js'; import { ProgressTracker, ProgressTrackerState } from '@deepkit/core-rxjs'; +import { ContextDispatcher } from '../protocol.js'; +import { WritableClient } from './client.js'; type ControllerStateActionTypes = { mode: ActionMode; @@ -96,33 +98,33 @@ interface ActionState { function handleCollection(entityStore: EntitySubjectStore, types: ControllerStateActionTypes, collection: Collection, messages: RpcMessage[]) { for (const next of messages) { switch (next.type) { - case RpcTypes.ResponseActionCollectionState: { + case RpcAction.ResponseActionCollectionState: { const state = next.parseBody(); collection.setState(state); break; } - case RpcTypes.ResponseActionCollectionSort: { + case RpcAction.ResponseActionCollectionSort: { const body = next.parseBody(); collection.setSort(body.ids); break; } - case RpcTypes.ResponseActionCollectionModel: { + case RpcAction.ResponseActionCollectionModel: { if (!types.collectionQueryModel) throw new RpcError('No collectionQueryModel set'); collection.model.set(next.parseBody(types.collectionQueryModel)); break; } - case RpcTypes.ResponseActionCollectionUpdate: - case RpcTypes.ResponseActionCollectionAdd: { + case RpcAction.ResponseActionCollectionUpdate: + case RpcAction.ResponseActionCollectionAdd: { if (!types.collectionSchema) continue; const incomingItems = next.parseBody(types.collectionSchema).v as IdInterface[]; const items: IdInterface[] = []; for (const item of incomingItems) { if (!entityStore.isRegistered(item.id)) entityStore.register(item); - if (next.type === RpcTypes.ResponseActionCollectionUpdate) { + if (next.type === RpcAction.ResponseActionCollectionUpdate) { entityStore.onSet(item.id, item); } @@ -141,21 +143,21 @@ function handleCollection(entityStore: EntitySubjectStore, types: Controlle }); } - if (next.type === RpcTypes.ResponseActionCollectionAdd) { + if (next.type === RpcAction.ResponseActionCollectionAdd) { collection.add(items); - } else if (next.type === RpcTypes.ResponseActionCollectionUpdate) { + } else if (next.type === RpcAction.ResponseActionCollectionUpdate) { collection.update(items); } break; } - case RpcTypes.ResponseActionCollectionRemove: { + case RpcAction.ResponseActionCollectionRemove: { const ids = next.parseBody().ids; collection.remove(ids); //this unsubscribes its EntitySubject as well break; } - case RpcTypes.ResponseActionCollectionSet: { + case RpcAction.ResponseActionCollectionSet: { if (!types.collectionSchema) continue; const incomingItems = next.parseBody(types.collectionSchema).v as IdInterface[]; const items: IdInterface[] = []; @@ -213,7 +215,7 @@ function actionProtocolError(reply: RpcMessage, subject: RpcMessageSubject, stat function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state: ActionState) { switch (reply.type) { - case RpcTypes.ResponseActionSimple: { + case RpcAction.ResponseActionSimple: { try { const result = reply.parseBody(state.types.resultSchema); resolveAction(state, result.v); @@ -226,13 +228,13 @@ function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state } - case RpcTypes.ResponseEntity: { + case RpcAction.ResponseEntity: { if (!state.types.classType || !state.entityState) throw new RpcError('No classType returned by the rpc action'); resolveAction(state, state.entityState.createEntitySubject(state.types.classType, state.types.resultSchema, reply)); break; } - case RpcTypes.ResponseActionCollectionChange: { + case RpcAction.ResponseActionCollectionChange: { if (!state.collectionRef) throw new RpcError('No collection loaded yet'); if (!state.types.collectionSchema) throw new RpcError('no collectionSchema loaded yet'); if (!state.collectionEntityStore) throw new RpcError('no collectionEntityStore loaded yet'); @@ -245,7 +247,7 @@ function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state break; } - case RpcTypes.ResponseActionCollection: { + case RpcAction.ResponseActionCollection: { if (!state.types.classType) throw new RpcError('No classType returned by the rpc action'); if (!state.types.collectionQueryModel) throw new RpcError('No collectionQueryModel returned by the rpc action'); if (!state.entityState) throw new RpcError('No entityState set'); @@ -254,11 +256,11 @@ function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state state.collectionEntityStore = state.entityState.getStore(state.types.classType); collection.model.change.subscribe(() => { - subject.send(RpcTypes.ActionCollectionModel, collection!.model, state.types.collectionQueryModel); + subject.send(RpcAction.ActionCollectionModel, collection!.model, state.types.collectionQueryModel); }); collection.addTeardown(() => { - subject.send(RpcTypes.ActionCollectionUnsubscribe); + subject.send(RpcAction.ActionCollectionUnsubscribe); subject.release(); }); @@ -268,7 +270,7 @@ function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state break; } - case RpcTypes.ResponseActionObservableError: { + case RpcAction.ResponseActionObservableError: { const body = reply.parseBody(); const error = rpcDecodeError(body); if (state.observableRef) { @@ -281,7 +283,7 @@ function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state break; } - case RpcTypes.ResponseActionObservableComplete: { + case RpcAction.ResponseActionObservableComplete: { const body = reply.parseBody(); if (state.observableRef) { @@ -294,7 +296,7 @@ function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state break; } - case RpcTypes.ResponseActionObservableNext: { + case RpcAction.ResponseActionObservableNext: { if (!state.types.observableNextSchema) throw new RpcError('No observableNextSchema set'); const body = reply.parseBody(state.types.observableNextSchema); @@ -313,7 +315,7 @@ function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state break; } - case RpcTypes.ResponseActionObservable: { + case RpcAction.ResponseActionObservable: { if (state.observableRef) break; const body = reply.parseBody(); @@ -325,18 +327,18 @@ function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state const observable = new Observable((observer) => { const id = state.subscriberId!++; state.subscribers![id] = observer; - subject.send(RpcTypes.ActionObservableSubscribe, { id }); + subject.send(RpcAction.ActionObservableSubscribe, { id }); return { unsubscribe: () => { delete state.subscribers![id]; - subject.send(RpcTypes.ActionObservableUnsubscribe, { id }); + subject.send(RpcAction.ActionObservableUnsubscribe, { id }); }, }; }); state.observableRef = new WeakRef(observable); state.finalizer.register(observable, () => { - subject.send(RpcTypes.ActionObservableDisconnect); + subject.send(RpcAction.ActionObservableDisconnect); subject.release(); }); resolveAction(state, observable); @@ -351,7 +353,7 @@ function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state Subject.prototype.unsubscribe.call(this); if (!freed) { freed = true; - subject.send(RpcTypes.ActionObservableSubjectUnsubscribe); + subject.send(RpcAction.ActionObservableSubjectUnsubscribe); state.finalizer.unregister(this); subject.release(); } @@ -361,7 +363,7 @@ function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state Subject.prototype.complete.call(this); if (!freed) { freed = true; - subject.send(RpcTypes.ActionObservableSubjectUnsubscribe); + subject.send(RpcAction.ActionObservableSubjectUnsubscribe); state.finalizer.unregister(this); subject.release(); } @@ -373,7 +375,7 @@ function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state } state.finalizer.register(observableSubject, () => { - subject.send(RpcTypes.ActionObservableSubjectUnsubscribe); + subject.send(RpcAction.ActionObservableSubjectUnsubscribe); freed = true; subject.release(); }); @@ -391,7 +393,7 @@ function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state Subject.prototype.unsubscribe.call(this); if (!freed) { freed = true; - subject.send(RpcTypes.ActionObservableSubjectUnsubscribe); + subject.send(RpcAction.ActionObservableSubjectUnsubscribe); state.finalizer.unregister(this); } }; @@ -400,7 +402,7 @@ function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state Subject.prototype.complete.call(this); if (!freed) { freed = true; - subject.send(RpcTypes.ActionObservableSubjectUnsubscribe); + subject.send(RpcAction.ActionObservableSubjectUnsubscribe); state.finalizer.unregister(this); } }; @@ -410,13 +412,13 @@ function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state // this is important to handle the stop signal. const oldChanged = observableSubject.changed; observableSubject.changed = function(this: ProgressTracker) { - subject.send(RpcTypes.ActionObservableProgressNext, this.value, typeOf()); + subject.send(RpcAction.ActionObservableProgressNext, this.value, typeOf()); return oldChanged.apply(this); }; } state.finalizer.register(observableSubject, () => { - subject.send(RpcTypes.ActionObservableSubjectUnsubscribe); + subject.send(RpcAction.ActionObservableSubjectUnsubscribe); subject.release(); }); resolveAction(state, observableSubject); @@ -424,12 +426,12 @@ function actionProtocolFull(reply: RpcMessage, subject: RpcMessageSubject, state break; } - case RpcTypes.Error: { + case RpcAction.Error: { actionProtocolError(reply, subject, state); break; } default: { - console.log(`Unexpected type received ${reply.type} ${RpcTypes[reply.type]}`); + console.log(`Unexpected type received ${reply.type} ${RpcAction[reply.type]}`); } } } @@ -438,7 +440,7 @@ function actionProtocol(reply: RpcMessage, subject: RpcMessageSubject, state: Ac try { actionProtocolFull(reply, subject, state); } catch (error) { - console.warn('reply error', reply.id, RpcTypes[reply.type], error); + console.warn('reply error', reply.contextId, RpcAction[reply.type], error); rejectAction(state, `Reply failed for ${state.action}: ${error}`); } } @@ -450,7 +452,10 @@ export class RpcActionClient { heldValue(); }); - constructor(protected client: WritableClient) { + constructor( + protected client: WritableClient, + protected context: ContextDispatcher, + ) { } public action(controller: RpcControllerState, method: string, args: any[], options: { @@ -461,6 +466,7 @@ export class RpcActionClient { const progress = ClientProgress.getNext(); return asyncOperation(async (resolve, reject) => { + // todo: I think it's slower to fetch for 10 different actions always the type, then just initially loading once all types const types = controller.getState(method)?.types || await this.loadActionTypes(controller, method, options); // forwarded caught progress to client sendMessage @@ -476,19 +482,50 @@ export class RpcActionClient { progress, }; - this.client.sendMessage(RpcTypes.Action, { - controller: controller.controller, - method: method, - args, - }, types.callSchema, { - peerId: controller.peerId, - dontWaitForConnection: options.dontWaitForConnection, - timeout: options.timeout, - }).onRejected((error) => { - rejectAction(state, error); - }).onReply(function(reply: RpcMessage, subject: RpcMessageSubject) { - actionProtocol(reply, subject, state); - }); + // { + // const contextId = this.context.next(); + // this.client.registerPromise(contextId); + // await this.client.sendActionType(actionId); + // } + + { + const contextId = this.context.create((message) => { + //todo: how to handle connection breaks in active contextx, like we had before with onRejected + }); + + // todo we want one buffer for all sends + this.client.write(); + + // this.client.registerContext(contextId, { + // reply: (reply: RpcMessage) => { + // actionProtocol(reply, subject, state); + // }, + // error: (error: any) => { + // rejectAction(state, error); + // }, + // }); + // this.client.sendAction(actionId, args); + // this.client.registerContext(contextId) + // .onRejected(function(error) { + // rejectAction(state, error); + // }).onReply(function(reply: RpcMessage, subject: RpcMessageSubject) { + // actionProtocol(reply, subject, state); + // }); + } + + // this.client.sendMessage(RpcTypes.Action, { + // controller: controller.controller, + // method: method, + // args, + // }, types.callSchema, { + // peerId: controller.peerId, + // dontWaitForConnection: options.dontWaitForConnection, + // timeout: options.timeout, + // }).onRejected((error) => { + // rejectAction(state, error); + // }).onReply(function(reply: RpcMessage, subject: RpcMessageSubject) { + // actionProtocol(reply, subject, state); + // }); }); } @@ -508,7 +545,8 @@ export class RpcActionClient { state.promise = asyncOperation(async (resolve, reject) => { try { - const a = this.client.sendMessage(RpcTypes.ActionType, { + // + const a = this.client.sendMessage(RpcAction.Types, { controller: controller.controller, method: method, disableTypeReuse: typeReuseDisabled, @@ -518,7 +556,8 @@ export class RpcActionClient { timeout: options.timeout, }).onRejected(reject); - const parsed = await a.firstThenClose(RpcTypes.ResponseActionType, typeOf()); + // + const parsed = await a.firstThenClose(RpcAction.ResponseActionType, typeOf()); const returnType = deserializeType(parsed.type, { disableReuse: typeReuseDisabled }); diff --git a/packages/rpc/src/client/client-direct.ts b/packages/rpc/src/client/client-direct.ts index f47e99e7a..30ed54092 100644 --- a/packages/rpc/src/client/client-direct.ts +++ b/packages/rpc/src/client/client-direct.ts @@ -26,9 +26,9 @@ export class RpcDirectClientAdapter implements ClientTransportAdapter { public async connect(connection: TransportClientConnection) { let closed = false; const kernelConnection = this.rpcKernel.createConnection({ - writeBinary: (buffer) => { + write: (buffer) => { if (closed) return; - connection.readBinary(buffer); + connection.read(buffer); }, close: () => { closed = true; @@ -47,7 +47,7 @@ export class RpcDirectClientAdapter implements ClientTransportAdapter { closed = true; kernelConnection.close(); }, - writeBinary(buffer) { + write(buffer) { kernelConnection.feed(buffer); }, }); @@ -70,9 +70,9 @@ export class RpcAsyncDirectClientAdapter implements ClientTransportAdapter { public async connect(connection: TransportClientConnection) { const kernelConnection = this.rpcKernel.createConnection({ - writeBinary: (buffer) => { + write: (buffer) => { setTimeout(() => { - connection.readBinary(buffer); + connection.read(buffer); }); }, close: () => { @@ -90,7 +90,7 @@ export class RpcAsyncDirectClientAdapter implements ClientTransportAdapter { close() { kernelConnection.close(); }, - writeBinary(buffer) { + write(buffer) { setTimeout(() => { kernelConnection.feed(buffer); }); diff --git a/packages/rpc/src/client/client-websocket.ts b/packages/rpc/src/client/client-websocket.ts index 0616f00e4..20c6262c5 100644 --- a/packages/rpc/src/client/client-websocket.ts +++ b/packages/rpc/src/client/client-websocket.ts @@ -90,7 +90,8 @@ export class RpcWebSocketClientAdapter implements ClientTransportAdapter { socket.binaryType = 'arraybuffer'; socket.onmessage = (event: MessageEvent) => { - connection.readBinary(new Uint8Array(event.data)); + // todo: make sure this does not copy in browsers + connection.read(new Uint8Array(event.data)); }; let errored = false; @@ -123,7 +124,7 @@ export class RpcWebSocketClientAdapter implements ClientTransportAdapter { close() { socket.close(); }, - writeBinary(message) { + write(message) { socket.send(message); } }); diff --git a/packages/rpc/src/client/client.ts b/packages/rpc/src/client/client.ts index 102b7f6a1..c36552292 100644 --- a/packages/rpc/src/client/client.ts +++ b/packages/rpc/src/client/client.ts @@ -8,34 +8,14 @@ * You should have received a copy of the MIT License along with this program. */ -import { asyncOperation, ClassType, CustomError, formatError, sleep } from '@deepkit/core'; -import { ReceiveType, resolveReceiveType, ValidationError } from '@deepkit/type'; -import { BehaviorSubject, Observable, Subject } from 'rxjs'; -import { - ControllerDefinition, - rpcAuthenticate, - rpcClientId, - RpcError, - rpcPeerDeregister, - rpcPeerRegister, - rpcResponseAuthenticate, - RpcStats, - RpcTypes, -} from '../model.js'; -import { - createErrorMessage, - createRpcMessage, - createRpcMessagePeer, - RpcBinaryMessageReader, - RpcMessage, - RpcMessageDefinition, - RpcMessageRouteType, -} from '../protocol.js'; +import { asyncOperation, CustomError, formatError, sleep } from '@deepkit/core'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { ControllerDefinition, RpcError, RpcStats } from '../model.js'; +import { ContextDispatcher, getContextId, hasContext, isRouteFlag, MessageFlag, setRouteFlag } from '../protocol.js'; import { RpcKernel, RpcKernelConnection } from '../server/kernel.js'; -import { ClientProgress, SingleProgress } from '../progress.js'; +import { SingleProgress } from '../progress.js'; +import { TransportClientConnection, TransportConnection, TransportOptions } from '../transport.js'; import { RpcActionClient, RpcControllerState } from './action.js'; -import { RpcMessageSubject } from './message-subject.js'; -import { createWriter, TransportClientConnection, TransportConnection, TransportMessageWriter, TransportOptions } from '../transport.js'; export class OfflineError extends CustomError { } @@ -45,25 +25,9 @@ export type RemoteController = { [P in keyof T]: T[P] extends (...args: any[]) => any ? PromisifyFn : never }; -export interface ObservableDisconnect { - /** - * Unsubscribes all active subscriptions and cleans the stored Observable instance on the server. - * This signals the server that the created observable from the RPC action is no longer needed. - */ - disconnect(): void; -} - -export type DisconnectableObservable = Observable & ObservableDisconnect; - export interface ClientTransportAdapter { connect(connection: TransportClientConnection): Promise | void; - /** - * Whether ClientId call is needed to get a client id. - * This is disabled for http adapter. - */ - supportsPeers?(): boolean; - /** * Whether Authentication call is needed to authenticate the client. * This is disabled for http adapter (Authorization header is used). @@ -74,17 +38,18 @@ export interface ClientTransportAdapter { export interface WritableClient { clientStats: RpcStats; - sendMessage( - type: number, - body?: T, - receiveType?: ReceiveType, - options?: { - dontWaitForConnection?: boolean, - connectionId?: number, - peerId?: string, - timeout?: number - }, - ): RpcMessageSubject; + write(message: Uint8Array): void; + + // sendMessage( + // type: number, + // body?: T, + // bodyEncoder?: BodyEncoder, + // options?: { + // dontWaitForConnection?: boolean, + // connectionId?: number, + // timeout?: number + // }, + // ): RpcMessageSubject; } export class RpcClientToken { @@ -111,7 +76,6 @@ export class RpcClientTransporter { protected connectionPromise?: Promise; protected connected = false; - protected writer?: TransportMessageWriter; public writerOptions: TransportOptions = new TransportOptions(); public id?: Uint8Array; @@ -138,15 +102,16 @@ export class RpcClientTransporter { */ public readonly errored = new Subject<{ connectionId: number, error: Error }>(); - public reader = new RpcBinaryMessageReader( - (v) => { - this.stats.increase('incoming', 1); - this.onMessage(v); - }, - (id) => { - this.writer!(createRpcMessage(id, RpcTypes.ChunkAck), this.writerOptions, this.stats); - }, - ); + // public reader = new RpcBinaryMessageReader( + // this.context, + // (v) => { + // this.stats.increase('incoming', 1); + // this.onMessage(v); + // }, + // (message) => { + // this.writer!(message, this.writerOptions, this.stats); + // }, + // ); public constructor( public transport: ClientTransportAdapter, @@ -182,7 +147,7 @@ export class RpcClientTransporter { if (!this.transportConnection) return; this.transportConnection = undefined; - this.stats.increase('connections', -1); + this.stats.connections--; this.connection.next(false); this.connected = false; @@ -214,7 +179,7 @@ export class RpcClientTransporter { public async onAuthenticate(token?: any): Promise { } - public onMessage(message: RpcMessage) { + public onMessage(message: Uint8Array) { } public async disconnect() { @@ -247,9 +212,11 @@ export class RpcClientTransporter { onConnected: async (transport: TransportConnection) => { this.transportConnection = transport; - this.stats.increase('connections', 1); - this.stats.increase('totalConnections', 1); - this.writer = createWriter(transport, this.writerOptions, this.reader); + this.stats.connections++; + this.stats.totalConnections++; + + this.writer = transport.write; + // this.writer = createWriter(transport, this.writerOptions, this.reader); this.connected = false; this.connectionTries = 0; @@ -275,18 +242,18 @@ export class RpcClientTransporter { reject(new OfflineError(`Could not connect: ${formatError(error)}`, { cause: error })); }, - read: (message: RpcMessage) => { - this.stats.increase('incoming', 1); + read: (message: Uint8Array) => { + this.stats.incoming++; this.onMessage(message); }, - - readBinary: (buffer: Uint8Array, bytes?: number) => { - this.reader.feed(buffer, bytes); - }, }); }); } + protected writer(message: Uint8Array): void { + + } + /** * Simply connect with login using the token, without auto re-connect. */ @@ -309,68 +276,51 @@ export class RpcClientTransporter { } } - public send(message: RpcMessageDefinition, progress?: SingleProgress) { + public send(message: Uint8Array, progress?: SingleProgress) { if (this.writer === undefined) { throw new RpcError('Transport connection not created yet'); } - try { - this.writer(message, this.writerOptions, this.stats, progress); - } catch (error: any) { - if (error instanceof ValidationError) throw error; - throw new OfflineError(error, { cause: error }); - } - } -} - -export class RpcClientPeer { - constructor( - protected actionClient: RpcActionClient, - protected peerId: string, - protected onDisconnect: (peerId: string) => void, - ) { - - } - - public controller(nameOrDefinition: string | ControllerDefinition, options: { - timeout?: number, - dontWaitForConnection?: true - } = {}): RemoteController { - const controller = new RpcControllerState('string' === typeof nameOrDefinition ? nameOrDefinition : nameOrDefinition.path); - controller.peerId = this.peerId; - - return new Proxy(this, { - get: (target, propertyName) => { - return (...args: any[]) => { - return this.actionClient.action(controller, propertyName as string, args, options); - }; - }, - }) as any as RemoteController; - } - - disconnect() { - this.onDisconnect(this.peerId); + // this.writer(message, this.writerOptions, this.stats, progress); + this.writer(message); } } - -export type RpcEventMessage = { id: number, date: Date, type: number, body: any }; -export type RpcClientEventIncomingMessage = - { event: 'incoming', composite: boolean, messages: RpcEventMessage[] } - & RpcEventMessage; -export type RpcClientEventOutgoingMessage = - { event: 'outgoing', composite: boolean, messages: RpcEventMessage[] } - & RpcEventMessage; - -export type RpcClientEvent = RpcClientEventIncomingMessage | RpcClientEventOutgoingMessage; - -export class RpcBaseClient implements WritableClient { - protected messageId: number = 1; - protected replies = new Map(); - +// export class RpcClientPeer { +// // todo this needs its own RpcActionClient +// +// constructor( +// protected actionClient: RpcActionClient, +// protected peerId: string, +// protected onDisconnect: (peerId: string) => void, +// ) { +// +// } +// +// public controller(nameOrDefinition: string | ControllerDefinition, options: { +// timeout?: number, +// dontWaitForConnection?: true +// } = {}): RemoteController { +// const controller = new RpcControllerState('string' === typeof nameOrDefinition ? nameOrDefinition : nameOrDefinition.path); +// controller.peerId = this.peerId; +// +// return new Proxy(this, { +// //todo: try getOwnPropertyDescriptor to improve performance +// +// get: (target, propertyName) => { +// return this.actionClient.getAction(controller, String(propertyName), options); +// }, +// }) as any as RemoteController; +// } +// +// disconnect() { +// this.onDisconnect(this.peerId); +// } +// } + +export class RpcClient implements WritableClient { public clientStats: RpcStats = new RpcStats; - public actionClient = new RpcActionClient(this); public readonly token = new RpcClientToken(undefined); public readonly transporter: RpcClientTransporter; @@ -378,7 +328,30 @@ export class RpcBaseClient implements WritableClient { public typeReuseDisabled: boolean = false; - public events = new Subject(); + selfContext = new ContextDispatcher(); + protected remoteContext = new ContextDispatcher(); + + public actionClient = new RpcActionClient(this, this.selfContext); + + /** + * For server->client (us) communication. + * This is automatically created when registerController is called. + * Set this property earlier to work with a custom RpcKernel. + */ + public clientKernel?: RpcKernel; + + /** + * Once the server starts actively with first RPC action for the client, + * a RPC connection is created. + */ + protected clientKernelConnection?: RpcKernelConnection; + + /** + * For peer->client(us) communication. + * This is automatically created when registerAsPeer is called. + * Set this property earlier to work with a custom RpcKernel. + */ + public peerKernel?: RpcKernel; constructor( protected transport: ClientTransportAdapter, @@ -391,10 +364,10 @@ export class RpcBaseClient implements WritableClient { } onClose(error?: Error) { - for (const subject of this.replies.values()) { - subject.disconnect(error); - } - this.replies.clear(); + // for (const subject of this.replies.values()) { + // subject.disconnect(error); + // } + // this.replies.clear(); } /** @@ -422,21 +395,29 @@ export class RpcBaseClient implements WritableClient { * ``` */ protected async onAuthenticate(token?: any): Promise { - if (undefined === token) return; - if (this.transport.supportsPeers && !this.transport.supportsPeers()) return; - - const reply: RpcMessage = await this.sendMessage(RpcTypes.Authenticate, { token }, undefined, { dontWaitForConnection: true }) - .waitNextMessage(); - - if (reply.isError()) throw reply.getError(); - - if (reply.type === RpcTypes.AuthenticateResponse) { - const body = reply.parseBody(); - this.username = body.username; - return; - } - - throw new RpcError('Invalid response'); + // if (undefined === token) return; + // // if (this.transport.supportsPeers && !this.transport.supportsPeers()) return; + // + // const id = this.context.create((message: Uint8Array) => { + // this.context.release(id); + // }); + // + // const authEncoder = createBodyEncoder(); + // + // this.send(RpcAction.Authenticate); + // + // const reply: RpcMessage = await this.sendMessage(RpcAction.Authenticate, { token }, undefined, { dontWaitForConnection: true }) + // .waitNextMessage(); + // + // if (reply.isError()) throw reply.getError(); + // + // if (reply.type === RpcAction.AuthenticateResponse) { + // const body = reply.parseBody(); + // this.username = body.username; + // return; + // } + // + // throw new RpcError('Invalid response'); } protected async onHandshake(): Promise { @@ -447,287 +428,159 @@ export class RpcBaseClient implements WritableClient { throw new RpcError('RpcBaseClient does not load its client id, and thus does not support peer message'); } - protected onMessage(message: RpcMessage) { - if (this.events.observers.length) { - this.events.next({ - event: 'incoming', - ...message.debug(), - }); - } - - // console.log('client: received message', message.id, message.type, RpcTypes[message.type], message.routeType); - - if (message.type === RpcTypes.Entity) { - this.actionClient.entityState.handle(message); - } else { - const subject = this.replies.get(message.id); - if (subject) subject.next(message); - } - } - - debug() { - return { - activeMessages: this.replies.size, - }; - } - - public sendMessage( - type: number, - body?: T, - schema?: ReceiveType, - options: { - dontWaitForConnection?: boolean, - connectionId?: number, - peerId?: string, - timeout?: number; - } = {}, - ): RpcMessageSubject { - const resolvedSchema = schema ? resolveReceiveType(schema) : undefined; - if (body && !schema) throw new RpcError('Body given, but not type'); - const id = this.messageId++; - const connectionId = options && options.connectionId ? options.connectionId : this.transporter.connectionId; - const dontWaitForConnection = !!options.dontWaitForConnection; - // const timeout = options && options.timeout ? options.timeout : 0; - - const continuation = (type: number, body?: T, schema?: ReceiveType) => { - if (connectionId === this.transporter.connectionId) { - //send a message with the same id. Don't use sendMessage() again as this would lead to a memory leak - // and a new id generated. We want to use the same id. - if (this.events.observers.length) { - this.events.next({ - event: 'outgoing', - date: new Date, - id, type, body, messages: [], composite: false, - }); - } - const message = createRpcMessage(id, type, body, undefined, schema); - this.transporter.send(message); - } - }; - - const subject = new RpcMessageSubject(continuation, () => { - this.replies.delete(id); - }); - - this.replies.set(id, subject); - - const progress = ClientProgress.getNext(); - if (progress) { - this.transporter.reader.registerProgress(id, progress.download); - } + protected onMessage(message: Uint8Array) { - if (dontWaitForConnection || this.transporter.isConnected()) { - const message = options && options.peerId - ? createRpcMessagePeer(id, type, this.getId(), options.peerId, body, resolvedSchema) - : createRpcMessage(id, type, body, undefined, resolvedSchema); - - if (this.events.observers.length) { - this.events.next({ - event: 'outgoing', - date: new Date, - id, type, body, messages: [], composite: false, - }); - } - - this.transporter.send(message, progress?.upload); - } else { - this.connect().then(() => { - //this.getId() only now available - const message = options && options.peerId - ? createRpcMessagePeer(id, type, this.getId(), options.peerId, body, resolvedSchema) - : createRpcMessage(id, type, body, undefined, resolvedSchema); - - if (this.events.observers.length) { - this.events.next({ - event: 'outgoing', - date: new Date, - id, type, body, messages: [], composite: false, - }); - } - this.transporter.send(message, progress?.upload); - }, () => { - // we ignore errors here since created `RpcMessageSubject` receive them - // onClose(error) - }); - } - - return subject; - } - - async connect(): Promise { - await this.transporter.connect(this.token.get()); - return this; - } - - async disconnect() { - await this.transporter.disconnect(); - } -} - -export class RpcClient extends RpcBaseClient { - protected registeredAsPeer?: string; - - /** - * For server->client (us) communication. - * This is automatically created when registerController is called. - * Set this property earlier to work with a custom RpcKernel. - */ - public clientKernel?: RpcKernel; - - /** - * Once the server starts actively with first RPC action for the client, - * a RPC connection is created. - */ - protected clientKernelConnection?: RpcKernelConnection; - - /** - * For peer->client(us) communication. - * This is automatically created when registerAsPeer is called. - * Set this property earlier to work with a custom RpcKernel. - */ - public peerKernel?: RpcKernel; - - protected peerConnections = new Map(); - - protected async onHandshake(): Promise { - this.clientKernelConnection = undefined; - if (this.transport.supportsPeers && !this.transport.supportsPeers()) return; - - const reply = await this.sendMessage(RpcTypes.ClientId, undefined, undefined, { dontWaitForConnection: true }) - .firstThenClose(RpcTypes.ClientIdResponse); - return reply.id; - } - - protected peerKernelConnection = new Map(); - - public async ping(): Promise { - await this.sendMessage(RpcTypes.Ping).waitNext(RpcTypes.Pong); - } - - protected onMessage(message: RpcMessage) { - if (message.routeType === RpcMessageRouteType.peer) { + if (isRouteFlag(message[0], MessageFlag.RouteDirect)) { if (!this.peerKernel) return; - const peerId = message.getPeerId(); - if (this.registeredAsPeer !== peerId) return; - - let connection = this.peerKernelConnection.get(peerId); - if (!connection) { - //todo: create a connection per message.getSource() - const writer = { + // const peerId = message.getPeerId(); + // if (this.registeredAsPeer !== peerId) return; + // + // let connection = this.peerKernelConnection.get(peerId); + // if (!connection) { + // //todo: create a connection per message.getSource() + // const writer = { + // close: () => { + // if (connection) connection.close(); + // this.peerKernelConnection.delete(peerId); + // }, + // write: (answer: Uint8Array) => { + // replyRoute(answer); + // // should we modify the package? + // this.transporter.send(answer); + // }, + // }; + // + // //todo: set up timeout for idle detection. Make the timeout configurable + // + // const c = this.peerKernel.createConnection(writer); + // if (!(c instanceof RpcKernelConnection)) throw new RpcError('Expected RpcKernelConnection from peerKernel.createConnection'); + // connection = c; + // connection.myPeerId = peerId; //necessary so that the kernel does not redirect the package again. + // this.peerKernelConnection.set(peerId, connection); + // } + // connection.handleMessage(message); + } else if (isRouteFlag(message[0], MessageFlag.RouteServer)) { + if (!this.clientKernel) { + // this.transporter.send(createErrorMessage(message.contextId, 'RpcClient has no controllers registered')); + return; + } + if (!this.clientKernelConnection) { + const c = this.clientKernel.createConnection({ + write: (answer: Uint8Array) => { + setRouteFlag(answer, MessageFlag.RouteServer); + this.transporter.send(answer); + }, close: () => { - if (connection) connection.close(); - this.peerKernelConnection.delete(peerId); + this.transporter.disconnect().catch(() => undefined); }, - write: (answer: RpcMessageDefinition) => { - //should we modify the package? - this.transporter.send(answer); + clientAddress: () => { + return this.transporter.clientAddress(); }, - }; - - //todo: set up timeout for idle detection. Make the timeout configurable - - const c = this.peerKernel.createConnection(writer); - if (!(c instanceof RpcKernelConnection)) throw new RpcError('Expected RpcKernelConnection from peerKernel.createConnection'); - connection = c; - connection.myPeerId = peerId; //necessary so that the kernel does not redirect the package again. - this.peerKernelConnection.set(peerId, connection); + bufferedAmount: () => { + return this.transporter.bufferedAmount(); + }, + }); + // Important to disable since transporter.send chunks already, + // otherwise data is chunked twice and protocol breaks. + c.transportOptions.chunkSize = 0; + if (!(c instanceof RpcKernelConnection)) throw new RpcError('Expected RpcKernelConnection from clientKernel.createConnection'); + this.clientKernelConnection = c; } + // this.clientKernelConnection.handleMessage(message); + return; + } - connection.handleMessage(message); - } else { - if (message.routeType === RpcMessageRouteType.server) { - if (!this.clientKernel) { - this.transporter.send(createErrorMessage(message.id, 'RpcClient has no controllers registered', RpcMessageRouteType.server)); - return; - } - if (!this.clientKernelConnection) { - const c = this.clientKernel.createConnection({ - write: (answer: RpcMessageDefinition) => { - this.transporter.send(answer); - }, - close: () => { - this.transporter.disconnect().catch(() => undefined); - }, - clientAddress: () => { - return this.transporter.clientAddress(); - }, - bufferedAmount: () => { - return this.transporter.bufferedAmount(); - }, - }); - // Important to disable since transporter.send chunks already, - // otherwise data is chunked twice and protocol breaks. - c.transportOptions.chunkSize = 0; - if (!(c instanceof RpcKernelConnection)) throw new RpcError('Expected RpcKernelConnection from clientKernel.createConnection'); - this.clientKernelConnection = c; - } - this.clientKernelConnection.routeType = RpcMessageRouteType.server; - this.clientKernelConnection.handleMessage(message); + try { + if (hasContext(message[0])) { + const contextId = getContextId(message); + this.selfContext.dispatch(contextId, message); return; } - super.onMessage(message); + } catch (error) { + console.log('client.onMessage error', error); } - } - - public getId(): Uint8Array { - if (!this.transporter.id) throw new RpcError('Not fully connected yet'); - return this.transporter.id; - } - /** - * Registers a new controller for the peer's RPC kernel. - * Use `registerAsPeer` first. - */ - public registerPeerController(classType: ClassType, nameOrDefinition: string | ControllerDefinition) { - if (!this.peerKernel) throw new RpcError('Not registered as peer. Call registerAsPeer() first'); - this.peerKernel.registerController(classType, nameOrDefinition); - } - - /** - * Registers a new controller for the server's RPC kernel. - * This is when the server wants to communicate actively with the client (us). - */ - public registerController(classType: ClassType, nameOrDefinition: string | ControllerDefinition) { - if (!this.clientKernel) this.clientKernel = new RpcKernel(); - this.clientKernel.registerController(classType, nameOrDefinition); - } - public async registerAsPeer(id: string) { - if (this.registeredAsPeer) { - throw new RpcError('Already registered as a peer'); - } - - this.peerKernel = new RpcKernel(); - - await this.sendMessage(RpcTypes.PeerRegister, { id }).firstThenClose(RpcTypes.Ack); - this.registeredAsPeer = id; - - return { - deregister: async () => { - await this.sendMessage(RpcTypes.PeerDeregister, { id }).firstThenClose(RpcTypes.Ack); - this.registeredAsPeer = undefined; - }, - }; - } + // console.log('client: received message', message.id, message.type, RpcTypes[message.type], message.routeType); - /** - * Creates a new peer connection, or re-uses an existing non-disconnected one. - * - * Make sure to call disconnect() on it once you're done using it, otherwise the peer - * will leak memory. (connection will be dropped if idle for too long automatically tough) - */ - public peer(peerId: string): RpcClientPeer { - let peer = this.peerConnections.get(peerId); - if (peer) return peer; - peer = new RpcClientPeer(this.actionClient, peerId, () => { - //todo, send disconnect message so the peer can release its kernel connection - // also implement a timeout on peer side - this.peerConnections.delete(peerId); - }); - this.peerConnections.set(peerId, peer); - return peer; - } + // if (message.type === RpcAction.Entity) { + // this.actionClient.entityState.handle(message); + // } else { + // const subject = this.replies.get(message.contextId, + // ); + // if (subject) subject.next(message); + // } + } + + write(message: Uint8Array) { + this.transporter.send(message); + } + + // public sendMessage( + // type: number, + // body?: T, + // bodyEncoder?: BodyEncoder, + // options: { + // dontWaitForConnection?: boolean, + // connectionId?: number, + // timeout?: number; + // } = {}, + // ): RpcMessageSubject { + // const id = this.selfContext.next(); + // + // const connectionId = options && options.connectionId ? options.connectionId : this.transporter.connectionId; + // const dontWaitForConnection = !!options.dontWaitForConnection; + // // const timeout = options && options.timeout ? options.timeout : 0; + // + // const continuation = (type: number, body?: T, schema?: ReceiveType) => { + // if (connectionId === this.transporter.connectionId) { + // //send a message with the same id. Don't use sendMessage() again as this would lead to a memory leak + // // and a new id generated. We want to use the same id. + // // const message = createRpcMessage(id, type, body, undefined, schema); + // // this.transporter.send(message); + // } + // }; + // + // const subject = new RpcMessageSubject(continuation, () => { + // this.replies.delete(id); + // }); + // + // this.replies.set(id, subject); + // + // const progress = ClientProgress.getNext(); + // if (progress) { + // this.transporter.reader.registerProgress(id, progress.download); + // } + // + // if (dontWaitForConnection || this.transporter.isConnected()) { + // // const message = options && options.peerId + // // ? createRpcMessagePeer(id, type, this.getId(), options.peerId, body, resolvedSchema) + // // : createRpcMessage(id, type, body, undefined, resolvedSchema); + // const message = {}; + // + // // this.transporter.send(message, progress?.upload); + // } else { + // this.connect().then(() => { + // //this.getId() only now available + // // const message = options && options.peerId + // // ? createRpcMessagePeer(id, type, this.getId(), options.peerId, body, resolvedSchema) + // // : createRpcMessage(id, type, body, undefined, resolvedSchema); + // const message = createRpcMessage( + // MessageFlag.RouteClient, + // MessageFlag.ContextNew, + // 0, + // {} + // ); + // + // this.transporter.send(message, progress?.upload); + // }, () => { + // // we ignore errors here since created `RpcMessageSubject` receive them + // }); + // } + // + // return subject; + // } public controller(nameOrDefinition: string | ControllerDefinition, options: { timeout?: number, @@ -750,4 +603,120 @@ export class RpcClient extends RpcBaseClient { }) as any as RemoteController; } + + async connect(): Promise { + await this.transporter.connect(this.token.get()); + return this; + } + + async disconnect() { + await this.transporter.disconnect(); + } } + +// export class RpcClient2 extends RpcBaseClient { +// protected registeredAsPeer?: string; +// +// +// protected peerConnections = new Map(); +// +// // protected async onHandshake(): Promise { +// // // this.clientKernelConnection = undefined; +// // // if (this.transport.supportsPeers && !this.transport.supportsPeers()) return; +// // // +// // // const reply = await this.sendMessage(RpcAction.ClientId, undefined, undefined, { dontWaitForConnection: true }) +// // // .firstThenClose(RpcAction.ClientIdResponse); +// // // return reply.id; +// // } +// +// protected peerKernelConnection = new Map(); +// +// public async ping(): Promise { +// // await this.sendMessage(RpcAction.Ping).waitNext(RpcAction.Pong); +// } +// +// protected onMessage(message: Uint8Array) { +// //todo: this is too late. We want chunks also forwarded to the correct receiver +// } +// +// public getId(): Uint8Array { +// if (!this.transporter.id) throw new RpcError('Not fully connected yet'); +// return this.transporter.id; +// } +// +// /** +// * Registers a new controller for the peer's RPC kernel. +// * Use `registerAsPeer` first. +// */ +// public registerPeerController(classType: ClassType, nameOrDefinition: string | ControllerDefinition) { +// if (!this.peerKernel) throw new RpcError('Not registered as peer. Call registerAsPeer() first'); +// this.peerKernel.registerController(classType, nameOrDefinition); +// } +// +// /** +// * Registers a new controller for the server's RPC kernel. +// * This is when the server wants to communicate actively with the client (us). +// */ +// public registerController(classType: ClassType, nameOrDefinition: string | ControllerDefinition) { +// if (!this.clientKernel) this.clientKernel = new RpcKernel(); +// this.clientKernel.registerController(classType, nameOrDefinition); +// } +// +// public async registerAsPeer(id: string) { +// if (this.registeredAsPeer) { +// throw new RpcError('Already registered as a peer'); +// } +// +// this.peerKernel = new RpcKernel(); +// +// await this.sendMessage(RpcAction.PeerRegister, { id }).firstThenClose(RpcAction.Ack); +// this.registeredAsPeer = id; +// +// return { +// deregister: async () => { +// await this.sendMessage(RpcAction.PeerDeregister, { id }).firstThenClose(RpcAction.Ack); +// this.registeredAsPeer = undefined; +// }, +// }; +// } +// +// /** +// * Creates a new peer connection, or re-uses an existing non-disconnected one. +// * +// * Make sure to call disconnect() on it once you're done using it, otherwise the peer +// * will leak memory. (connection will be dropped if idle for too long automatically tough) +// */ +// public peer(peerId: string): RpcClientPeer { +// let peer = this.peerConnections.get(peerId); +// if (peer) return peer; +// peer = new RpcClientPeer(this.actionClient, peerId, () => { +// //todo, send disconnect message so the peer can release its kernel connection +// // also implement a timeout on peer side +// this.peerConnections.delete(peerId); +// }); +// this.peerConnections.set(peerId, peer); +// return peer; +// } +// +// public controller(nameOrDefinition: string | ControllerDefinition, options: { +// timeout?: number, +// dontWaitForConnection?: true, +// typeReuseDisabled?: boolean +// } = {}): RemoteController { +// const controller = new RpcControllerState('string' === typeof nameOrDefinition ? nameOrDefinition : nameOrDefinition.path); +// +// options = options || {}; +// if ('undefined' === typeof options.typeReuseDisabled) { +// options.typeReuseDisabled = this.typeReuseDisabled; +// } +// +// return new Proxy(this, { +// get: (target, propertyName) => { +// return (...args: any[]) => { +// return this.actionClient.action(controller, propertyName as string, args, options); +// }; +// }, +// }) as any as RemoteController; +// } +// +// } diff --git a/packages/rpc/src/client/entity-state.ts b/packages/rpc/src/client/entity-state.ts index cc0d80cf9..f7ffb5833 100644 --- a/packages/rpc/src/client/entity-state.ts +++ b/packages/rpc/src/client/entity-state.ts @@ -9,16 +9,7 @@ */ import { arrayRemoveItem, ClassType, deletePathValue, getPathValue, setPathValue } from '@deepkit/core'; -import { - EntityPatch, - EntitySubject, - IdType, - IdVersionInterface, - rpcEntityPatch, - rpcEntityRemove, - RpcError, - RpcTypes, -} from '../model.js'; +import { EntityPatch, EntitySubject, IdType, IdVersionInterface, RpcAction, rpcEntityPatch, rpcEntityRemove, RpcError } from '../model.js'; import { RpcMessage } from '../protocol.js'; import { getPartialSerializeFunction, ReflectionClass, serializer, TypeObjectLiteral } from '@deepkit/type'; @@ -181,7 +172,7 @@ export class EntityState { } public createEntitySubject(classType: ClassType, bodySchema: TypeObjectLiteral, message: RpcMessage) { - if (message.type !== RpcTypes.ResponseEntity) throw new RpcError('Not a response entity message'); + if (message.type !== RpcAction.ResponseEntity) throw new RpcError('Not a response entity message'); const item = message.parseBody<{ v: any }>(bodySchema).v; const store = this.getStore(classType); @@ -196,7 +187,7 @@ export class EntityState { public handle(entityMessage: RpcMessage) { for (const message of entityMessage.getBodies()) { switch (message.type) { - case RpcTypes.EntityPatch: { + case RpcAction.EntityPatch: { //todo, use specialized ClassSchema, so we get correct instance types returned. We need however first deepkit/bson patch support // at the moment this happens in onPatch using jsonSerializer const body = message.parseBody(); @@ -205,7 +196,7 @@ export class EntityState { break; } - case RpcTypes.EntityRemove: { + case RpcAction.EntityRemove: { const body = message.parseBody(); for (const id of body.ids) { const store = this.getStoreByName(body.entityName); diff --git a/packages/rpc/src/client/http.ts b/packages/rpc/src/client/http.ts index 20a9520a9..58efbe26c 100644 --- a/packages/rpc/src/client/http.ts +++ b/packages/rpc/src/client/http.ts @@ -1,7 +1,7 @@ import { ClientTransportAdapter, RpcClient } from './client.js'; import { TransportClientConnection } from '../transport.js'; import { RpcMessageDefinition } from '../protocol.js'; -import { RpcError, RpcTypes } from '../model.js'; +import { RpcAction, RpcError } from '../model.js'; import { HttpRpcMessage } from '../server/http.js'; import { serialize } from '@deepkit/type'; @@ -75,12 +75,12 @@ export class RpcHttpClientAdapter implements ClientTransportAdapter { let method = 'GET'; let body: any = undefined; - if (message.type === RpcTypes.ActionType) { + if (message.type === RpcAction.Types) { if (!message.body) throw new RpcError('No body given'); const body = message.body.body as { controller: string, method: string, }; path = body.controller + '/' + encodeURIComponent(body.method) + '.type'; method = 'GET'; - } else if (message.type === RpcTypes.Action) { + } else if (message.type === RpcAction.Execute) { if (!message.body) throw new RpcError('No body given'); const messageBody = serialize(message.body.body, undefined, undefined, undefined, message.body.type) as { controller: string, @@ -118,7 +118,7 @@ export class RpcHttpClientAdapter implements ClientTransportAdapter { const composite = 'true' === res.headers['x-message-composite']; const routeType = Number(res.headers['x-message-routetype']); let json = res.body; - if (type === RpcTypes.ResponseActionSimple) { + if (type === RpcAction.ResponseActionSimple) { json = { v: json }; } connection.read(new HttpRpcMessage(message.id, composite, type, routeType, {}, json)); diff --git a/packages/rpc/src/client/message-subject.ts b/packages/rpc/src/client/message-subject.ts index 9bd28ab8e..8f35985a7 100644 --- a/packages/rpc/src/client/message-subject.ts +++ b/packages/rpc/src/client/message-subject.ts @@ -10,7 +10,7 @@ import { asyncOperation, CustomError } from '@deepkit/core'; import { ReceiveType } from '@deepkit/type'; -import { RpcError, RpcTypes } from '../model.js'; +import { RpcAction, RpcError } from '../model.js'; import type { RpcMessage } from '../protocol.js'; export class UnexpectedMessageType extends CustomError { @@ -98,7 +98,7 @@ export class RpcMessageSubject { this.release(); this.rejected = undefined; - if (next.type === RpcTypes.Ack) { + if (next.type === RpcAction.Ack) { return resolve(undefined); } diff --git a/packages/rpc/src/model.ts b/packages/rpc/src/model.ts index 2c0c09cbd..dfc49fd37 100644 --- a/packages/rpc/src/model.ts +++ b/packages/rpc/src/model.ts @@ -27,83 +27,44 @@ export interface IdVersionInterface extends IdInterface { version: number; } -type Writeable = { -readonly [P in keyof T]: T[P] }; - export type NumericKeys = { [K in keyof T]: T[K] extends number ? K : never; }[keyof T]; export class RpcTransportStats { - readonly incomingBytes: number = 0; - readonly outgoingBytes: number = 0; + incomingBytes: number = 0; + outgoingBytes: number = 0; /** * Amount of incoming and outgoing messages. */ - public readonly incoming: number = 0; - public readonly outgoing: number = 0; - - public increase(name: NumericKeys, count: number) { - (this as Writeable)[name] += count; - } + public incoming: number = 0; + public outgoing: number = 0; } export class ActionStats { - readonly observables: number = 0; - readonly subjects: number = 0; - readonly behaviorSubjects: number = 0; - readonly progressTrackers: number = 0; - readonly subscriptions: number = 0; - - public increase(name: NumericKeys, count: number) { - (this as Writeable)[name] += count; - } -} - -export class ForwardedActionStats extends ActionStats { - constructor(protected forward: ActionStats) { - super(); - } - - public increase(name: NumericKeys, count: number) { - this.forward.increase(name, count); - super.increase(name, count); - } + observables: number = 0; + subjects: number = 0; + behaviorSubjects: number = 0; + progressTrackers: number = 0; + subscriptions: number = 0; } export class RpcStats extends RpcTransportStats { - public readonly connections: number = 0; - public readonly totalConnections: number = 0; + public connections: number = 0; + public totalConnections: number = 0; /** * How many actions have been executed. */ - public readonly actions: number = 0; - - public readonly active: ActionStats = new ActionStats; - public readonly total: ActionStats = new ActionStats; + public actions: number = 0; - public increase(name: NumericKeys, count: number) { - (this as Writeable)[name] += count; - } -} - -export class ForwardedRpcStats extends RpcStats { - public readonly active = new ForwardedActionStats(this.forward.active); - public readonly total = new ForwardedActionStats(this.forward.total); - - constructor(protected forward: RpcStats) { - super(); - } - - public increase(name: NumericKeys, count: number) { - this.forward.increase(name, count); - super.increase(name, count); - } + public active: ActionStats = new ActionStats; + public total: ActionStats = new ActionStats; } export class StreamBehaviorSubject extends BehaviorSubject { - public readonly appendSubject = new Subject(); + public appendSubject = new Subject(); protected nextChange?: Subject; protected nextOnAppend = false; @@ -237,8 +198,8 @@ export class EntitySubject extends StreamBehaviorSubject< /** * Patches are in class format. */ - public readonly patches = new Subject(); - public readonly delete = new Subject(); + public patches = new Subject(); + public delete = new Subject(); [IsEntitySubject] = true; @@ -294,26 +255,18 @@ export function ControllerSymbol(path: string, entities: ClassType[] = []): C @entity.name('@error:json') export class JSONError { - constructor(public readonly json: any) { + constructor(public json: any) { } } -export enum RpcTypes { - Ack, - Error, - - //A batched chunk. Used when a single message exceeds a certain size. It's split up in multiple packages, allowing to track progress, - //cancel, and safe memory. Allows to send shorter messages between to not block the connection. Both ways. - Chunk, - ChunkAck, - +export enum RpcAction { Ping, Pong, //client -> server Authenticate, - ActionType, - Action, //T is the parameter type [t.string, t.number, ...] (typed arrays not supported yet) + Types, + Execute, PeerRegister, PeerDeregister, diff --git a/packages/rpc/src/protocol.ts b/packages/rpc/src/protocol.ts index ad4d60dd1..211bd42c0 100644 --- a/packages/rpc/src/protocol.ts +++ b/packages/rpc/src/protocol.ts @@ -7,19 +7,8 @@ * * You should have received a copy of the MIT License along with this program. */ - -import { BsonStreamReader, deserializeBSONWithoutOptimiser, getBSONDeserializer, getBSONSerializer, getBSONSizer, Writer } from '@deepkit/bson'; -import { bufferConcat, ClassType, createBuffer } from '@deepkit/core'; -import { rpcChunk, RpcError, rpcError, RpcTypes } from './model.js'; -import type { SingleProgress } from './progress.js'; -import { deserialize, ReceiveType, ReflectionClass, resolveReceiveType, serialize, Type, typeOf, typeSettings } from '@deepkit/type'; - -export const enum RpcMessageRouteType { - client = 0, - server = 1, - sourceDest = 2, - peer = 3, -} +import { getBSONDeserializer, getBSONSerializer, getBSONSizer, Writer } from '@deepkit/bson'; +import { ReceiveType, resolveReceiveType, Type } from '@deepkit/type'; export interface BodyDecoder { type: Type; @@ -37,641 +26,633 @@ export function createBodyDecoder(type?: ReceiveType): BodyDecoder { return fn; } -/* - * A message is binary data and has the following structure: +export interface BodyEncoder { + type: Type; + + size(value: T): number; + + write(value: T, buffer: Uint8Array, offset: number): void; +} + + +export function createBodyEncoder(type?: ReceiveType): BodyEncoder { + type = resolveReceiveType(type); + const serialize = getBSONSerializer(undefined, type); + const sizer = getBSONSizer(undefined, type); + + return { + type, + size: sizer, + write(value: T, buffer: Uint8Array, offset: number): void { + serialize(value, { writer: new Writer(buffer, offset) }); + }, + }; +} + +/** + */ + +/** + * Message encoding is: + * + *
[body] + * + * header: [routeType] [contextId] [action] + * + * - routeType is not set, when RouteClient or RouteServer + * - routeType is , when RouteSourceDest + * + * - contextId is not set per default + * - contextId is a 16bit unsigned integer, when ContextExisting or TypeChunk or TypeChunkAck * - * [] + * - action is not set, when type is not action + * - action is a 32bit unsigned integer, when TypeAction * - * size: uint32 //total message size - * version: uint8 - * id: uint32 //message id + * - body is parsed when message size is bigger than the header size (remaining bytes) * - * //type of routing: - * //0=client (context from client -> server), //client initiated a message context (message id created on client) - * //1=server (context from server -> client), //server initiated a message context (message id created on server) - * //2=sourceDest //route this message to a specific client using its client id - * //4=peer //route this message to a client using a peer alias (the peer alias needs to be registered). replies will be rewritten to sourceDest + * // simple action without parameters + * [(client | ContextNew | TypeAction), action] -> [(client | ContextExisting | TypeAck), contextId] * - * //when route=0 - * routeConfig: not defined + * // simple action without parameters + * [(server | ContextNew | TypeAction), action] -> [(server | ContextExisting | TypeAck), contextId] * - * //when route=1 - * routeConfig: not defined + * // simple action without parameters + * [(server | ContextNew | TypeAction), action] -> [(server | ContextExisting | TypeAck), contextId] + * [(sourceDestination | ContextExisting | TypeAction), action] -> [(server | ContextExisting | TypeAck), contextId] * - * //when route=2 - * routeConfig: , each 16 bytes, uuid v4 + * // simple action without parameters, no response need + * [(client | NoContext | TypeAction), action] -> [] * - * //when route=3 - * routeConfig: //where source=uuid v4, and peerId=ascii string (terminated by \0) + * // big action (2 bytes to encode action id) + * [(client | NoContext | TypeBigAction), action1, action2] -> [] * - * composite: uint8 //when 1 then there are multiple messageBody, each prefixed with uint32 for their size + * // simple action with parameters + * [(client | ContextNew | TypeAction), action, parameters] -> [(client | ContextExisting | TypeAck), contextId] * - * composite=0 then messageBody=: - * type: uint8 (256 types supported) //supported type - * body: BSON|any //arbitrary payload passed to type + * // chunk ack + * [(TypeChunkAck), contextId] * - * composite=1 then messageBody=: - * size: uint32 - * type: uint8 (256 types supported) //supported type - * body: BSON|any //arbitrary payload passed to type + * // chunk + * [(TypeChunk), contextId] * + * we have 1 byte for the flags to encode the following: + * 2 bits: routeType + * 2 bits: new context/existing context/no context + * 3 bits: OtherAction/TypeAction/TypeBigAction/TypeAck/TypeChunk/TypeChunkAck */ -export class RpcMessage { - protected peerId?: string; - protected source?: string; - protected destination?: string; - - constructor( - public id: number, - public composite: boolean, - public type: number, - public routeType: RpcMessageRouteType, - public bodyOffset: number = 0, - public bodySize: number = 0, - public buffer?: Uint8Array, - ) { - } - - debug() { - return { - type: this.type, - typeString: RpcTypes[this.type], - id: this.id, - date: new Date, - composite: this.composite, - body: this.bodySize ? this.parseGenericBody() : undefined, - messages: this.composite ? this.getBodies().map(message => { - return { - id: message.id, - type: message.type, - date: new Date, - body: message.bodySize ? message.parseGenericBody() : undefined, - }; - }) : [], - }; - } - - getBuffer(): Uint8Array { - if (!this.buffer) throw new RpcError('No buffer'); - return this.buffer; - } - - getPeerId(): string { - if (!this.buffer) throw new RpcError('No buffer'); - if (this.routeType !== RpcMessageRouteType.peer) throw new RpcError(`Message is not routed via peer, but ${this.routeType}`); - if (this.peerId) return this.peerId; - this.peerId = ''; - for (let offset = 10 + 16, c: number = this.buffer[offset]; c !== 0; offset++, c = this.buffer[offset]) { - this.peerId += String.fromCharCode(c); - } - - return this.peerId; - } - - getSource(): Uint8Array { - if (!this.buffer) throw new RpcError('No buffer'); - if (this.routeType !== RpcMessageRouteType.sourceDest && this.routeType !== RpcMessageRouteType.peer) throw new RpcError(`Message is not routed via sourceDest, but ${this.routeType}`); - return this.buffer.subarray(4 + 1 + 4 + 1, 4 + 1 + 4 + 1 + 16); - } - - getDestination(): Uint8Array { - if (!this.buffer) throw new RpcError('No buffer'); - if (this.routeType !== RpcMessageRouteType.sourceDest) throw new RpcError(`Message is not routed via sourceDest, but ${this.routeType}`); - return this.buffer.subarray(4 + 1 + 4 + 1 + 16, 4 + 1 + 4 + 1 + 16 + 16); - } - - getError(): Error { - if (!this.buffer) throw new RpcError('No buffer'); - const error = getBSONDeserializer()(this.buffer, this.bodyOffset); - return rpcDecodeError(error); - } - - isError(): boolean { - return this.type === RpcTypes.Error; - } - - parseGenericBody(): object { - if (!this.bodySize) throw new RpcError('Message has no body'); - if (!this.buffer) throw new RpcError('No buffer'); - if (this.composite) throw new RpcError('Composite message can not be read directly'); - - return deserializeBSONWithoutOptimiser(this.buffer, this.bodyOffset); - } - - parseBody(type?: ReceiveType): T { - if (!this.bodySize) throw new RpcError('Message has no body'); - if (!this.buffer) throw new RpcError('No buffer'); - if (this.composite) throw new RpcError('Composite message can not be read directly'); - // console.log('parseBody raw', deserializeBSONWithoutOptimiser(this.buffer, this.bodyOffset)); - return getBSONDeserializer(undefined, type)(this.buffer, this.bodyOffset); - } - - decodeBody(decoder: BodyDecoder): T { - if (!this.bodySize) throw new RpcError('Message has no body'); - if (!this.buffer) throw new RpcError('No buffer'); - if (this.composite) throw new RpcError('Composite message can not be read directly'); - return decoder(this.buffer, this.bodyOffset); - } - - getBodies(): RpcMessage[] { - if (!this.composite) throw new RpcError('Not a composite message'); - - const messages: RpcMessage[] = []; - const buffer = this.getBuffer(); - const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); - const totalSize = view.getUint32(0, true); - let offset = this.bodyOffset; - - while (offset < totalSize) { - const bodySize = view.getUint32(offset, true); - offset += 4; - const type = view.getUint8(offset++); - - messages.push(new RpcMessage(this.id, false, type, this.routeType, offset, bodySize, buffer)); - offset += bodySize; - } - - return messages; - } +export enum MessageFlag { + // 2 bits for routeType (position 0-1) + RouteClient = 0b00000_00, + RouteServer = 0b00000_01, + RouteDirect = 0b00000_10, + + // 2 bits for contextType (position 2-3) + ContextNone = 0b000_00_00, + ContextNew = 0b000_01_00, + + ContextExisting = 0b000_10_00, + ContextEnd = 0b000_11_00, + + // 3 bits for type (position 4-6) + TypeAck = 0b000_0000, + TypeError = 0b001_0000, + TypeChunk = 0b010_0000, + TypeAction = 0b011_0000, } -export class ErroredRpcMessage extends RpcMessage { - constructor( - public id: number, - public error: Error, - ) { - super(id, false, RpcTypes.Error, 0, 0, 0); - } +export enum BodyFlag { + Observable, + Subject, + BehaviourSubject, + ProgressTracker, - getError(): Error { - return this.error; - } + Reserved = 10, + // anything above 10 is the body size (tuple of values), + // 11 means 1 value, 12 means 2 values, ... } -export function readBinaryRpcMessage(buffer: Uint8Array): RpcMessage { - const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); - const size = view.getUint32(0, true); - if (size !== buffer.byteLength) { - let message = `Message buffer size wrong. Message size=${size}, buffer size=${buffer.byteLength}.`; - let hex = ''; - for (let i = 0; i < buffer.byteLength; i++) { - hex += buffer[i].toString(16).padStart(2, '0'); - } - message += ' Buffer hex: ' - + hex.substr(0, 500) + (hex.length > 500 ? '...' : ''); - throw new RpcError(message); - } - - const id = view.getUint32(5, true); +export type RouteFlag = MessageFlag.RouteClient | MessageFlag.RouteServer | MessageFlag.RouteDirect; - let offset = 9; - const routeType = buffer[offset++]; - - if (routeType === RpcMessageRouteType.peer) { - offset += 16; // - while (buffer[offset++] !== 0) ; //feed until \0 byte - } else if (routeType === RpcMessageRouteType.sourceDest) { - offset += 16 + 16; //uuid is each 16 bytes - } +export type ContextFlag = MessageFlag.ContextNone | MessageFlag.ContextNew | MessageFlag.ContextExisting | MessageFlag.ContextEnd; - const composite = buffer[offset++] === 1; - const type = buffer[offset++]; +export type TypeFlag = + | MessageFlag.TypeAction + | MessageFlag.TypeAck + | MessageFlag.TypeChunk + | MessageFlag.TypeError; - return new RpcMessage(id, composite, type, routeType, offset, size - offset, buffer); +export function isRouteFlag(flags: MessageFlag, routeType: RouteFlag): boolean { + return (flags & 0b11) === routeType; } -export interface RpcCreateMessageDef { - type: number; - schema?: Type; - body?: T; +export function isContextFlag(flags: MessageFlag, contextType: ContextFlag): boolean { + return (flags & 0b1100) === contextType; } -export function createRpcCompositeMessage( - id: number, - type: number, - messages: RpcCreateMessageDef[], - routeType: RpcMessageRouteType.client | RpcMessageRouteType.server = RpcMessageRouteType.client, -): RpcMessageDefinition { - return { - id, - type, - routeType, - composite: messages, - }; +export function hasContext(flags: MessageFlag): boolean { + //either MessageFlag.ContextEnd or MessageFlag.ContextExisting + return (flags & 0b1000) !== 0; } -export function serializeBinaryRpcCompositeMessage(message: RpcMessageDefinition): Uint8Array { - if (!message.composite) throw new RpcError('No messages set'); +export function isTypeFlag(flags: MessageFlag, type: TypeFlag): boolean { + return (flags & 0b1110000) === type; +} - let bodySize = 0; - for (const sub of message.composite) { - bodySize += 4 + 1 + (sub.schema && sub.body ? getBSONSizer(undefined, sub.schema)(sub.body) : 0); - } +export function setRouteFlag(message: Uint8Array, routeType: RouteFlag): void { + message[0] = (message[0] & 0b1111_1100) | routeType; +} - // [routeData] - const messageSize = 4 + 1 + 4 + 1 + 1 + 1 + bodySize; +export function setContextFlag(message: Uint8Array, contextType: ContextFlag): void { + message[0] = (message[0] & 0b1111_0011) | contextType; +} - const writer = new Writer(createBuffer(messageSize)); - writer.writeUint32(messageSize); - writer.writeByte(1); //version - writer.writeUint32(message.id); +export function setTypeFlag(message: Uint8Array, type: TypeFlag): void { + message[0] = (message[0] & 0b1000_1111) | type; +} - writer.writeByte(message.routeType); - writer.writeByte(1); - writer.writeByte(message.type); +export const flagSize = 1; +export const routeParamSize = 16 + 16 + 1; +export const contextIdSize = 2; +export const actionIdSize = 2; - for (const sub of message.composite) { - writer.writeUint32(sub.schema && sub.body ? getBSONSizer(undefined, sub.schema)(sub.body) : 0); - writer.writeByte(sub.type); //type +export function getRouteTypeOffset(flags: MessageFlag): number { + return flagSize; +} - if (sub.schema && sub.body) { - //BSON object contain already their size at the beginning - getBSONSerializer(undefined, sub.schema)(sub.body, { writer }); - } - } +const offsetsContextType = [ + /* 0b0 */ flagSize, // no direct route + /* 0b1 */ flagSize + routeParamSize, // direct route +]; - return writer.buffer; +export function getContextIdOffset(flags: MessageFlag): number { + // we need only the route bit to determine the offset + return offsetsContextType[(flags & 0b10) >>> 1]; } -export function createRpcCompositeMessageSourceDest( - id: number, - source: Uint8Array, - destination: Uint8Array, - type: number, - messages: RpcCreateMessageDef[], -): RpcMessageDefinition { - return { - id, - type, - routeType: RpcMessageRouteType.sourceDest, - composite: messages, - source, - destination, - }; +const offsetsActionType = [ + /* 0b00 */ flagSize, // no direct route, no context + /* 0b01 */ flagSize + routeParamSize, // direct route, no context + + /* 0b10 */ flagSize + contextIdSize, // no direct route, context + /* 0b11 */ flagSize + routeParamSize + contextIdSize, // direct route, context +]; + +export function getActionOffset(flags: MessageFlag): number { + // we need to now the direct route bit, the context bit + // directRoute = 0b00000_10 + // ContextExisting = 0b000_10_00 + // ContextEnd = 0b000_11_00 + const index = ((flags & 0b10) >>> 1) + ((flags & 0b10_00) >>> 2); + return offsetsActionType[index]; } -export function serializeBinaryRpcCompositeMessageSourceDest(message: RpcMessageDefinition): Uint8Array { - if (!message.composite) throw new RpcError('No messages set'); - if (!message.source) throw new RpcError('No source set'); - if (!message.destination) throw new RpcError('No destination set'); - - let bodySize = 0; - for (const sub of message.composite) { - bodySize += 4 + 1 + (sub.schema && sub.body ? getBSONSizer(undefined, sub.schema)(sub.body) : 0); - } +export function getAction(buffer: Uint8Array): number { + const offset = getActionOffset(buffer[0]); + return readUint16LE(buffer, offset); +} - // [routeData] - const messageSize = 4 + 1 + 4 + 1 + (16 + 16) + 1 + 1 + bodySize; - - const writer = new Writer(createBuffer(messageSize)); - writer.writeUint32(messageSize); - writer.writeByte(1); //version - writer.writeUint32(message.id); - - writer.writeByte(RpcMessageRouteType.sourceDest); - if (message.source.byteLength !== 16) throw new RpcError(`Source invalid byteLength of ${message.source.byteLength}`); - if (message.destination.byteLength !== 16) throw new RpcError(`Destination invalid byteLength of ${message.destination.byteLength}`); - writer.writeBuffer(message.source); - writer.writeBuffer(message.destination); - writer.writeByte(1); //composite=true - writer.writeByte(message.type); - - for (const sub of message.composite) { - writer.writeUint32(sub.schema && sub.body ? getBSONSizer(undefined, sub.schema)(sub.body) : 0); - writer.writeByte(sub.type); //type - - if (sub.schema && sub.body) { - //BSON object contain already their size at the beginning - getBSONSerializer(undefined, sub.schema)(sub.body, { writer }); - } - } +/** + * ContextId is a 16bit unsigned integer. + */ +export function getContextId(buffer: Uint8Array): number { + const offset = getContextIdOffset(buffer[0]); + return readUint16LE(buffer, offset); +} - return writer.buffer; +function noop(message: Uint8Array) { } -export interface RpcMessageDefinition { - id: number; - type: number; - routeType: RpcMessageRouteType; - composite?: RpcCreateMessageDef[]; - peerId?: string; - source?: Uint8Array; - destination?: Uint8Array; - body?: { - type: Type; - body: any; - }; +function createArray(): Array<(msg: Uint8Array) => void> { + return [ + noop, noop, noop, noop, noop, noop, noop, noop, noop, noop, + noop, noop, noop, noop, noop, noop, noop, noop, noop, noop, + noop, noop, noop, noop, noop, noop, noop, noop, noop, noop, + noop, noop, noop, noop, noop, noop, noop, noop, noop, noop, + noop, noop, noop, noop, noop, noop, noop, noop, noop, noop, + noop, noop, noop, noop, noop, noop, noop, noop, noop, noop, + noop, noop, noop, noop, noop, noop, noop, noop, noop, noop, + ]; } -export function createRpcMessage( - id: number, type: number, - body?: T, - routeType: RpcMessageRouteType.client | RpcMessageRouteType.server = RpcMessageRouteType.client, - schema?: ReceiveType, -): RpcMessageDefinition { - return { - id, - type, - routeType, - body: body && { - type: resolveReceiveType(schema), - body, - }, - }; +export class ContextDispatcher { + private contexts = createArray(); + private freeSlots: number[] = []; // Stack for free slots + private currentSlot = 1; -} + current(): number { + return this.freeSlots.length ? this.freeSlots[0] : this.currentSlot; + } -export function serializeBinaryRpcMessage(message: RpcMessageDefinition): Uint8Array { - if (message.composite) { - if (message.routeType === RpcMessageRouteType.sourceDest) { - return serializeBinaryRpcCompositeMessageSourceDest(message); + create(cb: (message: Uint8Array) => void): number { + const context = this.freeSlots.pop() || this.currentSlot++; + if (context >= this.contexts.length) { + this.contexts.push(...createArray()); } - return serializeBinaryRpcCompositeMessage(message); + this.contexts[context] = cb; + return context; } - if (message.routeType === RpcMessageRouteType.peer) { - return serializeBinaryRpcMessagePeer(message); + dispatch(id: number, message: Uint8Array) { + this.contexts[id](message); } - if (message.routeType === RpcMessageRouteType.sourceDest) { - return serializeBinaryRpcMessageSourceDest(message); + release(id: number) { + this.contexts[id] = noop; + this.freeSlots.push(id); } - - return serializeBinaryRpcMessageSingleBody(message); } -export function serializeBinaryRpcMessageSingleBody(message: RpcMessageDefinition): Uint8Array { - const bodySize = message.body ? getBSONSizer(undefined, message.body.type)(message.body.body) : 0; - // [routeData] - const messageSize = 4 + 1 + 4 + 1 + 1 + 1 + bodySize; - - const writer = new Writer(createBuffer(messageSize)); - writer.writeUint32(messageSize); - writer.writeByte(1); //version - writer.writeUint32(message.id); - - writer.writeByte(message.routeType); - writer.writeByte(0); //composite=false - writer.writeByte(message.type); - - if (message.body) { - const offset = writer.offset; - const serializer = getBSONSerializer(undefined, message.body.type); - serializer(message.body.body, { writer }); - } - - return writer.buffer; +function writeUint32LE(buffer: Uint8Array, offset: number, value: number) { + buffer[offset] = value & 0xff; + buffer[offset + 1] = (value >> 8) & 0xff; + buffer[offset + 2] = (value >> 16) & 0xff; + buffer[offset + 3] = (value >> 24) & 0xff; } -export function createRpcMessagePeer( - id: number, type: number, - source: Uint8Array, - peerId: string, - body?: T, - schema?: ReceiveType, -): RpcMessageDefinition { - return { - id, - type, - routeType: RpcMessageRouteType.peer, - source, - peerId, - body: body && { - type: resolveReceiveType(schema), - body, - }, - }; +export function writeUint16LE(buffer: Uint8Array, offset: number, value: number) { + buffer[offset] = value & 0xff; + buffer[offset + 1] = (value >> 8) & 0xff; } -export function serializeBinaryRpcMessagePeer(message: RpcMessageDefinition): Uint8Array { - if (!message.peerId) throw new RpcError('No peerId set'); - if (!message.source) throw new RpcError('No source set'); - - const bodySize = message.body ? getBSONSizer(undefined, message.body.type)(message.body.body) : 0; - // [routeData] - const messageSize = 4 + 1 + 4 + 1 + (16 + message.peerId.length + 1) + 1 + 1 + bodySize; - - const writer = new Writer(createBuffer(messageSize)); - writer.writeUint32(messageSize); - writer.writeByte(1); //version - writer.writeUint32(message.id); - - writer.writeByte(RpcMessageRouteType.peer); - if (message.source.byteLength !== 16) throw new RpcError(`Source invalid byteLength of ${message.source.byteLength}`); - writer.writeBuffer(message.source); - writer.writeAsciiString(message.peerId); - writer.writeNull(); - - writer.writeByte(0); //composite=false - writer.writeByte(message.type); - - if (message.body) getBSONSerializer(undefined, message.body.type)(message.body.body, { writer }); - - return writer.buffer; +export function readUint16LE(buffer: Uint8Array, offset: number): number { + return buffer[offset] + (buffer[offset + 1] << 8); } -export function createRpcMessageSourceDest( - id: number, - type: number, - source: Uint8Array, - destination: Uint8Array, - body?: T, - schema?: ReceiveType, -): RpcMessageDefinition { - return { - id, - type, - routeType: RpcMessageRouteType.sourceDest, - source, - destination, - body: body && { - type: resolveReceiveType(schema), - body, - }, - }; +export function getRandomAddress(): Uint8Array { + return crypto.getRandomValues(new Uint8Array(16)); } -export function serializeBinaryRpcMessageSourceDest(message: RpcMessageDefinition): Uint8Array { - if (!message.source) throw new RpcError('No source set'); - if (!message.destination) throw new RpcError('No destination set'); - - const bodySize = message.body ? getBSONSizer(undefined, message.body.type)(message.body.body) : 0; - // [routeData] - const messageSize = 4 + 1 + 4 + 1 + (16 + 16) + 1 + 1 + bodySize; - - const writer = new Writer(createBuffer(messageSize)); - writer.writeUint32(messageSize); - writer.writeByte(1); //version - writer.writeUint32(message.id); - - writer.writeByte(RpcMessageRouteType.sourceDest); - if (message.source.byteLength !== 16) throw new RpcError(`Source invalid byteLength of ${message.source.byteLength}`); - if (message.destination.byteLength !== 16) throw new RpcError(`Destination invalid byteLength of ${message.destination.byteLength}`); - writer.writeBuffer(message.source); - writer.writeBuffer(message.destination); - - writer.writeByte(0); //composite=false - writer.writeByte(message.type); - - if (message.body) getBSONSerializer(undefined, message.body.type)(message.body.body, { writer }); - - return writer.buffer; +export function writeDirectRoute(message: Uint8Array, src: Uint8Array, dst: Uint8Array, port: number) { + // write src to pos 1 + message.set(src, 1); + // write dst to pos 17 + message.set(dst, 17); + message[33] = port; } -export function createRpcMessageSourceDestForBody( - id: number, type: number, - source: Uint8Array, - destination: Uint8Array, - body: Uint8Array, -): Uint8Array { - // [routeData] - const messageSize = 4 + 1 + 4 + 1 + (16 + 16) + 1 + 1 + body.byteLength; - - const writer = new Writer(createBuffer(messageSize)); - writer.writeUint32(messageSize); - writer.writeByte(1); //version - writer.writeUint32(id); - - writer.writeByte(RpcMessageRouteType.sourceDest); - if (source.byteLength !== 16) throw new RpcError(`Source invalid byteLength of ${source.byteLength}`); - if (destination.byteLength !== 16) throw new RpcError(`Destination invalid byteLength of ${destination.byteLength}`); - writer.writeBuffer(source); - writer.writeBuffer(destination); - - writer.writeByte(0); //composite=false - writer.writeByte(type); - - writer.writeBuffer(body); - - return writer.buffer; +export function writeContext(message: Uint8Array, context: number) { + const offset = isRouteFlag(message[0], MessageFlag.RouteDirect) ? 1 + 16 + 16 + 1 : 1; + writeUint16LE(message, offset, context); } -export class RpcBinaryMessageReader { - protected chunks = new Map(); - protected progress = new Map(); - protected chunkAcks = new Map(); - protected streamReader = new BsonStreamReader(this.gotMessage.bind(this)); - - constructor( - protected readonly onMessage: (response: RpcMessage) => void, - protected readonly onChunk?: (id: number) => void, - ) { - } - - public onChunkAck(id: number, callback: Function) { - this.chunkAcks.set(id, callback); - } - - public registerProgress(id: number, progress: SingleProgress) { - this.progress.set(id, progress); - } - - public feed(buffer: Uint8Array, bytes?: number) { - this.streamReader.feed(buffer, bytes); - } - - protected gotMessage(buffer: Uint8Array) { - const message = readBinaryRpcMessage(buffer); - // console.log('reader got', message.id, RpcTypes[message.type], {routeType: message.routeType, bodySize: message.bodySize, byteLength: buffer.byteLength}); - - if (message.type === RpcTypes.ChunkAck) { - const ack = this.chunkAcks.get(message.id); - if (ack) ack(); - } else if (message.type === RpcTypes.Chunk) { - const progress = this.progress.get(message.id); - - const body = message.parseBody(); - let chunks = this.chunks.get(body.id); - if (!chunks) { - chunks = { buffers: [], loaded: 0 }; - this.chunks.set(body.id, chunks); - } - chunks.buffers.push(body.v); - chunks.loaded += body.v.byteLength; - if (this.onChunk) this.onChunk(message.id); - if (progress) progress.set(body.total, chunks.loaded); - - if (chunks.loaded === body.total) { - //we're done - this.progress.delete(message.id); - this.chunks.delete(body.id); - this.chunkAcks.delete(body.id); - const newBuffer = bufferConcat(chunks.buffers, body.total); - const packedMessage = readBinaryRpcMessage(newBuffer); - this.onMessage(packedMessage); - } - } else { - const progress = this.progress.get(message.id); - if (progress) { - progress.set(buffer.byteLength, buffer.byteLength); - this.progress.delete(message.id); - } - this.onMessage(message); - } - } +export function writeContextNoRoute(message: Uint8Array, context: number) { + writeUint16LE(message, 1, context); } -export function readUint32LE(buffer: Uint8Array, offset: number = 0): number { - return buffer[offset] + (buffer[offset + 1] * 2 ** 8) + (buffer[offset + 2] * 2 ** 16) + (buffer[offset + 3] * 2 ** 24); +export function writeAction(message: Uint8Array, id: number) { + const offset = getActionOffset(message[0]); + setTypeFlag(message, MessageFlag.TypeAction); + writeUint16LE(message, offset, id); } -export interface EncodedError { - classType: string; - message: string; - stack: string; - properties?: { [name: string]: any }; +export function getRouteDirectSrc(buffer: Uint8Array): Uint8Array { + return buffer.subarray(1, 17); } -export function rpcEncodeError(error: Error | string): EncodedError { - let classType = ''; - let stack = ''; - let properties: { [name: string]: any } | undefined; - - if ('string' !== typeof error) { - const schema = ReflectionClass.from(error['constructor'] as ClassType); - stack = error.stack || ''; - if (schema.name) { - classType = schema.name; - if (schema.getProperties().length) { - properties = serialize(error, undefined, undefined, undefined, schema.type); - } - } - } - - return { - classType, - properties, - stack, - message: 'string' === typeof error ? error : error.message || '', - }; +export function getRouteDirectDst(buffer: Uint8Array): Uint8Array { + return buffer.subarray(17, 33); } -export function rpcDecodeError(error: EncodedError): Error { - if (error.classType) { - const entity = typeSettings.registeredEntities[error.classType]; - if (!entity) { - throw new RpcError(`Could not find an entity named ${error.classType} for an error thrown. ` + - `Make sure the class is loaded and correctly defined using @entity.name(${JSON.stringify(error.classType)})`); - } - const schema = ReflectionClass.from(entity); - if (error.properties) { - const e = deserialize(error.properties, undefined, undefined, undefined, schema.type) as Error; - e.stack = error.stack + '\nat ___SERVER___'; - return e; - } +export function getRouteDirectPort(buffer: Uint8Array): number { + return buffer[33]; +} - const classType = schema.getClassType()! as ClassType; - return new classType(error.message); +/** + * When RouteDirect, the src/dst are flipped to convert the message to a reply. + */ +export function replyRoute(buffer: Uint8Array): void { + if (isRouteFlag(buffer[0], MessageFlag.RouteDirect)) { + const src = buffer.subarray(1, 5); + const dst = buffer.subarray(5, 9); + buffer.set(dst, 1); + buffer.set(src, 5); } - - const e = new RpcError(error.message); - e.stack = error.stack + '\nat ___SERVER___'; - - return e; } -export function createErrorMessage(id: number, error: Error | string, routeType: RpcMessageRouteType.client | RpcMessageRouteType.server): RpcMessageDefinition { - const extracted = rpcEncodeError(error); +// export function createRpcMessage( +// routeType: MessageFlag, +// contextType: ContextFlag, +// typeFlag: TypeFlag, +// options: { +// src?: number; +// dst?: number; +// port?: number; +// contextId?: number; +// action?: number; +// type?: number; +// body?: T; +// bodyEncoder?: BodyEncoder; +// }, +// ): Uint8Array { +// // Construct the 1-byte header +// const flag = routeType | contextType | typeFlag; +// const header = createBuffer(getHeaderSize(flag)); +// let offset = 0; +// header[offset++] = flag; +// +// // Encode src (4 bytes) and dst (4 bytes) (Only for RouteDirect) +// if (isRouteFlag(flag, MessageFlag.RouteDirect)) { +// if (options.src === undefined || options.dst === undefined || options.port === undefined) { +// throw new Error('RouteDirect requires src, dst, and port'); +// } +// writeUint32LE(header, offset, options.src); +// offset += 4; +// writeUint32LE(header, offset, options.dst); +// offset += 4; +// writeUint16LE(header, offset, options.port); +// offset += 2; +// } +// +// // Encode context ID (4 bytes) (Only for ContextExisting) +// if (isContextFlag(flag, MessageFlag.ContextExisting)) { +// if (options.contextId === undefined) { +// throw new Error('ContextExisting requires contextId'); +// } +// writeUint16LE(header, offset, options.contextId); +// offset += 2; +// } +// +// // Encode action field (variable size) +// if (isTypeFlag(flag, MessageFlag.TypeOther)) { +// if (options.type === undefined) { +// throw new Error('TypeOther requires an 8-bit type'); +// } +// header[offset++] = options.type & 0xff; // 1 byte +// } else if (isTypeFlag(flag, MessageFlag.TypeAction)) { +// if (options.action === undefined) { +// throw new Error('TypeAction requires a 16-bit action'); +// } +// writeUint16LE(header, offset, options.action); +// offset += 2; // 2 bytes +// } else if (isTypeFlag(flag, MessageFlag.TypeBigAction)) { +// if (options.action === undefined) { +// throw new Error('TypeBigAction requires a 32-bit action'); +// } +// writeUint32LE(header, offset, options.action); +// offset += 4; // 4 bytes +// } +// +// // Encode additional data if provided +// // if (options.data) { +// // const dataBytes = serializeData(options.data); +// // const finalBuffer = new Uint8Array(header.length + dataBytes.length); +// // finalBuffer.set(header); +// // finalBuffer.set(dataBytes, header.length); +// // return finalBuffer; +// // } +// +// return header; +// } + +// export function readBinaryRpcMessage(buffer: Uint8Array) { +// const flags = buffer[0]; +// let offset = 1; +// +// if (isRouteFlag(flags, MessageFlag.RouteDirect)) { +// offset += 16 + 16 + 2; +// } +// +// let id = 0; +// if (isContextFlag(flags, MessageFlag.ContextExisting)) { +// id = getContextId(buffer, offset); +// offset += 2; +// } +// +// let type = RpcTypes.Ack; +// let action = 0; +// +// if (flags & MessageFlag.TypeOther) { +// offset += 1; +// type = buffer[offset]; +// } else if (flags & MessageFlag.TypeAction) { +// type = RpcTypes.Action; +// action = buffer[offset] + (buffer[offset + 1] << 8); +// offset += 1; +// } else if (flags & MessageFlag.TypeBigAction) { +// type = RpcTypes.Action; +// action = buffer[offset] + (buffer[offset + 1] << 8) + (buffer[offset + 2] << 16) + (buffer[offset + 3] << 24); +// offset += 2; +// } +// +// return { +// flags, +// contextId: id, +// type, +// action, +// bodyOffset: offset, +// }; +// } + + +// export function createRpcMessagePeer( +// id: number, type: number, +// source: Uint8Array, +// peerId: string, +// body?: T, +// schema?: ReceiveType, +// ): RpcMessageDefinition { +// return { +// id, +// type, +// routeType: RpcMessageRouteType.peer, +// source, +// peerId, +// body: body && { +// type: resolveReceiveType(schema), +// body, +// }, +// }; +// } + +// export function serializeBinaryRpcMessagePeer(message: RpcMessageDefinition): Uint8Array { +// if (!message.peerId) throw new RpcError('No peerId set'); +// if (!message.source) throw new RpcError('No source set'); +// +// const bodySize = message.body ? getBSONSizer(undefined, message.body.type)(message.body.body) : 0; +// // [routeData] +// const messageSize = 4 + 1 + 4 + 1 + (16 + message.peerId.length + 1) + 1 + 1 + bodySize; +// +// const writer = new Writer(createBuffer(messageSize)); +// writer.writeUint32(messageSize); +// writer.writeByte(1); //version +// writer.writeUint32(message.id); +// +// writer.writeByte(RpcMessageRouteType.peer); +// if (message.source.byteLength !== 16) throw new RpcError(`Source invalid byteLength of ${message.source.byteLength}`); +// writer.writeBuffer(message.source); +// writer.writeAsciiString(message.peerId); +// writer.writeNull(); +// +// writer.writeByte(0); //composite=false +// writer.writeByte(message.type); +// +// if (message.body) getBSONSerializer(undefined, message.body.type)(message.body.body, { writer }); +// +// return writer.buffer; +// } + +// export function createRpcMessageSourceDest( +// id: number, +// type: number, +// source: Uint8Array, +// destination: Uint8Array, +// body?: T, +// schema?: ReceiveType, +// ): RpcMessageDefinition { +// return { +// id, +// type, +// routeType: RpcMessageRouteType.sourceDest, +// source, +// destination, +// body: body && { +// type: resolveReceiveType(schema), +// body, +// }, +// }; +// } + +// export function serializeBinaryRpcMessageSourceDest(message: RpcMessageDefinition): Uint8Array { +// if (!message.source) throw new RpcError('No source set'); +// if (!message.destination) throw new RpcError('No destination set'); +// +// const bodySize = message.body ? getBSONSizer(undefined, message.body.type)(message.body.body) : 0; +// // [routeData] +// const messageSize = 4 + 1 + 4 + 1 + (16 + 16) + 1 + 1 + bodySize; +// +// const writer = new Writer(createBuffer(messageSize)); +// writer.writeUint32(messageSize); +// writer.writeByte(1); //version +// writer.writeUint32(message.id); +// +// writer.writeByte(RpcMessageRouteType.sourceDest); +// if (message.source.byteLength !== 16) throw new RpcError(`Source invalid byteLength of ${message.source.byteLength}`); +// if (message.destination.byteLength !== 16) throw new RpcError(`Destination invalid byteLength of ${message.destination.byteLength}`); +// writer.writeBuffer(message.source); +// writer.writeBuffer(message.destination); +// +// writer.writeByte(0); //composite=false +// writer.writeByte(message.type); +// +// if (message.body) getBSONSerializer(undefined, message.body.type)(message.body.body, { writer }); +// +// return writer.buffer; +// } + +// export function createRpcMessageSourceDestForBody( +// id: number, type: number, +// source: Uint8Array, +// destination: Uint8Array, +// body: Uint8Array, +// ): Uint8Array { +// // [routeData] +// const messageSize = 4 + 1 + 4 + 1 + (16 + 16) + 1 + 1 + body.byteLength; +// +// const writer = new Writer(createBuffer(messageSize)); +// writer.writeUint32(messageSize); +// writer.writeByte(1); //version +// writer.writeUint32(id); +// +// writer.writeByte(RpcMessageRouteType.sourceDest); +// if (source.byteLength !== 16) throw new RpcError(`Source invalid byteLength of ${source.byteLength}`); +// if (destination.byteLength !== 16) throw new RpcError(`Destination invalid byteLength of ${destination.byteLength}`); +// writer.writeBuffer(source); +// writer.writeBuffer(destination); +// +// writer.writeByte(0); //composite=false +// writer.writeByte(type); +// +// writer.writeBuffer(body); +// +// return writer.buffer; +// } + +// function createAckReply(message: RpcMessage): Uint8Array { +// if (!message.buffer) throw new RpcError('Cannot create reply, no buffer given'); +// if (!message.contextId) throw new RpcError('Cannot create reply, no context id given'); +// +// // todo reuse cached reply buffer +// +// // reuse router flags +// const flags = message.routeType | MessageFlag.ContextExisting; +// +// const reply = createBuffer(getHeaderSize(flags)); +// setTypeFlag(reply, MessageFlag.TypeAck); +// let offset = 1; +// if (isRouteFlag(reply[0], MessageFlag.RouteDirect)) { +// offset += 16 + 16 + 2; +// for (let i = 0; i < 16; i++) { +// reply[offset + i] = message.buffer[offset + i]; +// } +// } +// +// writeUint32LE(reply, offset, message.contextId); +// offset += 4; +// return reply; +// } - return createRpcMessage(id, RpcTypes.Error, extracted, routeType, typeOf()); +export function readUint32LE(buffer: Uint8Array, offset: number = 0): number { + return buffer[offset] + (buffer[offset + 1] * 2 ** 8) + (buffer[offset + 2] * 2 ** 16) + (buffer[offset + 3] * 2 ** 24); } + +// export interface EncodedError { +// classType: string; +// message: string; +// stack: string; +// properties?: { [name: string]: any }; +// } + +// export function rpcEncodeError(error: Error | string): EncodedError { +// let classType = ''; +// let stack = ''; +// let properties: { [name: string]: any } | undefined; +// +// if ('string' !== typeof error) { +// const schema = ReflectionClass.from(error['constructor'] as ClassType); +// stack = error.stack || ''; +// if (schema.name) { +// classType = schema.name; +// if (schema.getProperties().length) { +// properties = serialize(error, undefined, undefined, undefined, schema.type); +// } +// } +// } +// +// return { +// classType, +// properties, +// stack, +// message: 'string' === typeof error ? error : error.message || '', +// }; +// } + +// export function rpcDecodeError(error: EncodedError): Error { +// if (error.classType) { +// const entity = typeSettings.registeredEntities[error.classType]; +// if (!entity) { +// throw new RpcError(`Could not find an entity named ${error.classType} for an error thrown. ` + +// `Make sure the class is loaded and correctly defined using @entity.name(${JSON.stringify(error.classType)})`); +// } +// const schema = ReflectionClass.from(entity); +// if (error.properties) { +// const e = deserialize(error.properties, undefined, undefined, undefined, schema.type) as Error; +// e.stack = error.stack + '\nat ___SERVER___'; +// return e; +// } +// +// const classType = schema.getClassType()! as ClassType; +// return new classType(error.message); +// } +// +// const e = new RpcError(error.message); +// e.stack = error.stack + '\nat ___SERVER___'; +// +// return e; +// } +// +// export function createErrorMessage(error: any, ...args: any[]): Uint8Array { +// const extracted = rpcEncodeError(error); +// +// return createBuffer(0); +// // return createRpcMessage(id, RpcTypes.Error, extracted, routeType, typeOf()); +// } diff --git a/packages/rpc/src/server/action.ts b/packages/rpc/src/server/action.ts index a42525a9c..5e6d52710 100644 --- a/packages/rpc/src/server/action.ts +++ b/packages/rpc/src/server/action.ts @@ -45,6 +45,7 @@ import { isEntitySubject, NumericKeys, rpcAction, + RpcAction, rpcActionObservableSubscribeId, rpcActionType, RpcError, @@ -54,12 +55,11 @@ import { rpcResponseActionObservableSubscriptionError, rpcResponseActionType, RpcStats, - RpcTypes, } from '../model.js'; import { createBodyDecoder, rpcEncodeError, RpcMessage } from '../protocol.js'; -import { RpcCache, RpcCacheAction, RpcKernelBaseConnection, RpcMessageBuilder } from './kernel.js'; +import { RpcCache, RpcCacheAction, RpcKernelBaseConnection } from './kernel.js'; import { RpcControllerAccess, RpcKernelSecurity, SessionState } from './security.js'; -import { InjectorContext, InjectorModule } from '@deepkit/injector'; +import { InjectorContext } from '@deepkit/injector'; import { LoggerInterface } from '@deepkit/logger'; import { onRpcAction, onRpcControllerAccess, RpcActionTimings, RpcControllerAccessEventStart } from '../events'; import { DataEvent, EventDispatcher } from '@deepkit/event'; @@ -131,7 +131,7 @@ const anyParametersType: Type = { const rpcActionTypeDecoder = createBodyDecoder(); const rpcActionDecoder = createBodyDecoder(); -export class RpcServerAction { +export class RpcActionServer { protected observableSubjects: { [id: number]: { subject: Subject, @@ -163,7 +163,6 @@ export class RpcServerAction { protected stats: RpcStats, protected cache: RpcCache, protected connection: RpcKernelBaseConnection, - protected controllers: Map, protected injector: InjectorContext, protected eventDispatcher: EventDispatcher, protected security: RpcKernelSecurity, @@ -176,7 +175,7 @@ export class RpcServerAction { const body = message.decodeBody(rpcActionTypeDecoder); const types = this.loadTypes(body.controller, body.method); - response.reply(RpcTypes.ResponseActionType, { + response.reply(RpcAction.ResponseActionType, { mode: types.mode, type: serializeType(types.strictSerialization ? types.type : anyType), parameters: serializeType(types.strictSerialization ? types.parameters : anyParametersType), @@ -354,8 +353,8 @@ export class RpcServerAction { public async handle(message: RpcMessage, response: RpcMessageBuilder) { switch (message.type) { - case RpcTypes.ActionObservableSubscribe: { - const observable = this.observables[message.id]; + case RpcAction.ActionObservableSubscribe: { + const observable = this.observables[message.contextId]; if (!observable) return response.error(new RpcError('No observable found')); response.strictSerialization = observable.types.strictSerialization; @@ -374,7 +373,7 @@ export class RpcServerAction { complete: () => { sub.active = false; if (sub.sub) sub.sub.unsubscribe(); - response.reply(RpcTypes.ResponseActionObservableComplete, { + response.reply(RpcAction.ResponseActionObservableComplete, { id: body.id, }); }, @@ -385,20 +384,20 @@ export class RpcServerAction { response.errorLabel = `Observable ${getClassName(observable.classType)}.${observable.method} next serialization error`; sub.sub = observable.observable.subscribe((next) => { if (!sub.active) return; - response.reply(RpcTypes.ResponseActionObservableNext, { + response.reply(RpcAction.ResponseActionObservableNext, { id: body.id, v: next, }, types.observableNextSchema); }, (error) => { this.stats.total.increase('subscriptions', -1); const extracted = rpcEncodeError(this.security.transformError(error)); - response.reply(RpcTypes.ResponseActionObservableError, { + response.reply(RpcAction.ResponseActionObservableError, { ...extracted, id: body.id, }); }, () => { this.stats.total.increase('subscriptions', -1); - response.reply(RpcTypes.ResponseActionObservableComplete, { + response.reply(RpcAction.ResponseActionObservableComplete, { id: body.id, }); }); @@ -406,16 +405,16 @@ export class RpcServerAction { break; } - case RpcTypes.ActionCollectionUnsubscribe: { - const collection = this.collections[message.id]; + case RpcAction.ActionCollectionUnsubscribe: { + const collection = this.collections[message.contextId]; if (!collection) return response.error(new RpcError('No collection found')); collection.unsubscribe(); - delete this.collections[message.id]; + delete this.collections[message.contextId]; break; } - case RpcTypes.ActionCollectionModel: { - const collection = this.collections[message.id]; + case RpcAction.ActionCollectionModel: { + const collection = this.collections[message.contextId]; if (!collection) return response.error(new RpcError('No collection found')); const body = message.parseBody>(); //todo, add correct type argument collection.collection.model.set(body); @@ -423,8 +422,8 @@ export class RpcServerAction { break; } - case RpcTypes.ActionObservableUnsubscribe: { - const observable = this.observables[message.id]; + case RpcAction.ActionObservableUnsubscribe: { + const observable = this.observables[message.contextId]; if (!observable) return response.error(new RpcError('No observable to unsubscribe found')); const body = message.parseBody(); const sub = observable.subscriptions[body.id]; @@ -437,28 +436,28 @@ export class RpcServerAction { break; } - case RpcTypes.ActionObservableDisconnect: { - const observable = this.observables[message.id]; + case RpcAction.ActionObservableDisconnect: { + const observable = this.observables[message.contextId]; if (!observable) return response.error(new RpcError('No observable to disconnect found')); for (const sub of Object.values(observable.subscriptions)) { sub.complete(); //we send all active subscriptions it was completed } this.stats.active.increase('observables', -1); - delete this.observables[message.id]; + delete this.observables[message.contextId]; break; } - case RpcTypes.ActionObservableSubjectUnsubscribe: { //aka completed - const subject = this.observableSubjects[message.id]; + case RpcAction.ActionObservableSubjectUnsubscribe: { //aka completed + const subject = this.observableSubjects[message.contextId]; if (!subject) return response.error(new RpcError('No subject to unsubscribe found')); subject.completedByClient = true; subject.subject.complete(); - delete this.observableSubjects[message.id]; + delete this.observableSubjects[message.contextId]; break; } - case RpcTypes.ActionObservableProgressNext: { //ProgressTracker changes from client (e.g. stop signal) - const observable = this.observables[message.id]; + case RpcAction.ActionObservableProgressNext: { //ProgressTracker changes from client (e.g. stop signal) + const observable = this.observables[message.contextId]; if (!observable || !(observable.observable instanceof ProgressTracker)) return response.error(new RpcError('No observable ProgressTracker to sync found')); response.strictSerialization = observable.types.strictSerialization; observable.observable.next(message.parseBody()); @@ -584,16 +583,16 @@ export class RpcServerAction { }), this.injector); if (isEntitySubject(result)) { - response.reply(RpcTypes.ResponseEntity, { v: result.value }, types.resultSchema); + response.reply(RpcAction.ResponseEntity, { v: result.value }, types.resultSchema); } else if (result instanceof Collection) { const collection = result; if (!types.collectionSchema) throw new RpcError('No collectionSchema set'); if (!types.collectionQueryModel) throw new RpcError('No collectionQueryModel set'); - response.composite(RpcTypes.ResponseActionCollection) - .add(RpcTypes.ResponseActionCollectionModel, collection.model, types.collectionQueryModel) - .add(RpcTypes.ResponseActionCollectionState, collection.state) - .add(RpcTypes.ResponseActionCollectionSet, { v: collection.all() }, types.collectionSchema) + response.composite(RpcAction.ResponseActionCollection) + .add(RpcAction.ResponseActionCollectionModel, collection.model, types.collectionQueryModel) + .add(RpcAction.ResponseActionCollectionState, collection.state) + .add(RpcAction.ResponseActionCollectionSet, { v: collection.all() }, types.collectionSchema) .send(); let unsubscribed = false; @@ -602,35 +601,35 @@ export class RpcServerAction { //everything as one composite message. const eventsSub = collection.event.subscribe(collectForMicrotask((events: CollectionEvent[]) => { if (unsubscribed) return; - const composite = response.composite(RpcTypes.ResponseActionCollectionChange); + const composite = response.composite(RpcAction.ResponseActionCollectionChange); for (const event of events) { if (event.type === 'add') { //when the user has already a EntitySubject on one of those event.items, //then we technically send it unnecessarily. However, we would have to introduce //a new RpcType to send only the IDs, which is not yet implemented. - composite.add(RpcTypes.ResponseActionCollectionAdd, { v: event.items }, types.collectionSchema); + composite.add(RpcAction.ResponseActionCollectionAdd, { v: event.items }, types.collectionSchema); } else if (event.type === 'remove') { - composite.add(RpcTypes.ResponseActionCollectionRemove, { ids: event.ids }); + composite.add(RpcAction.ResponseActionCollectionRemove, { ids: event.ids }); } else if (event.type === 'update') { - composite.add(RpcTypes.ResponseActionCollectionUpdate, { v: event.items }, types.collectionSchema); + composite.add(RpcAction.ResponseActionCollectionUpdate, { v: event.items }, types.collectionSchema); } else if (event.type === 'set') { - composite.add(RpcTypes.ResponseActionCollectionSet, { v: collection.all() }, types.collectionSchema); + composite.add(RpcAction.ResponseActionCollectionSet, { v: collection.all() }, types.collectionSchema); } else if (event.type === 'state') { - composite.add(RpcTypes.ResponseActionCollectionState, collection.state); + composite.add(RpcAction.ResponseActionCollectionState, collection.state); } else if (event.type === 'sort') { - composite.add(RpcTypes.ResponseActionCollectionSort, { ids: event.ids }); + composite.add(RpcAction.ResponseActionCollectionSort, { ids: event.ids }); } } composite.send(); })); collection.addTeardown(() => { - const c = this.collections[message.id]; + const c = this.collections[message.contextId]; if (c) c.unsubscribe(); }); - this.collections[message.id] = { + this.collections[message.contextId] = { collection, unsubscribe: () => { if (unsubscribed) return; @@ -642,7 +641,7 @@ export class RpcServerAction { } else if (isObservable(result)) { let trackingType: NumericKeys = 'observables'; - this.observables[message.id] = { + this.observables[message.contextId] = { observable: result, subscriptions: {}, types, @@ -664,27 +663,27 @@ export class RpcServerAction { } } - this.observableSubjects[message.id] = { + this.observableSubjects[message.contextId] = { subject: result, completedByClient: false, subscription: result.subscribe((next) => { - response.reply(RpcTypes.ResponseActionObservableNext, { - id: message.id, + response.reply(RpcAction.ResponseActionObservableNext, { + id: message.contextId, v: next, }, types.observableNextSchema); }, (error) => { this.stats.active.increase(trackingType, -1); const extracted = rpcEncodeError(this.security.transformError(error)); - response.reply(RpcTypes.ResponseActionObservableError, { + response.reply(RpcAction.ResponseActionObservableError, { ...extracted, - id: message.id, + id: message.contextId, }); }, () => { this.stats.active.increase(trackingType, -1); - const v = this.observableSubjects[message.id]; + const v = this.observableSubjects[message.contextId]; if (v && v.completedByClient) return; //we don't send ResponseActionObservableComplete when the client issued unsubscribe - response.reply(RpcTypes.ResponseActionObservableComplete, { - id: message.id, + response.reply(RpcAction.ResponseActionObservableComplete, { + id: message.contextId, }); }), }; @@ -693,14 +692,14 @@ export class RpcServerAction { this.stats.active.increase(trackingType, 1); this.stats.total.increase(trackingType, 1); - response.reply(RpcTypes.ResponseActionObservable, { type }); + response.reply(RpcAction.ResponseActionObservable, { type }); } else { if (!types.noTypeWarned && isPlainObject(result) && !validV(types.resultSchema)) { types.noTypeWarned = true; this.logger.warn(createNoTypeWarning(controller.controllerClassType, body.method, result)); } - response.reply(RpcTypes.ResponseActionSimple, { v: result }, types.resultSchema); + response.reply(RpcAction.ResponseActionSimple, { v: result }, types.resultSchema); } } catch (error: any) { triggerError(error); diff --git a/packages/rpc/src/server/http.ts b/packages/rpc/src/server/http.ts index 8bf373e78..10ecf47a6 100644 --- a/packages/rpc/src/server/http.ts +++ b/packages/rpc/src/server/http.ts @@ -20,14 +20,14 @@ export interface RpcHttpResponse { export class HttpRpcMessage extends RpcMessage { constructor( - public id: number, + public contextId: number, public composite: boolean, public type: number, public routeType: RpcMessageRouteType, public headers: RpcHttpRequest['headers'], public json?: any, ) { - super(id, composite, type, routeType); + super(contextId, composite, type, routeType); } getJson(): any { @@ -78,7 +78,7 @@ export class HttpRpcMessage extends RpcMessage { const result: RpcMessage[] = []; for (const item of json) { - result.push(new HttpRpcMessage(this.id, false, item.type, this.routeType, this.headers, item.body)); + result.push(new HttpRpcMessage(this.contextId, false, item.type, this.routeType, this.headers, item.body)); } return result; diff --git a/packages/rpc/src/server/kernel.ts b/packages/rpc/src/server/kernel.ts index 1a0689f2f..45994cc47 100644 --- a/packages/rpc/src/server/kernel.ts +++ b/packages/rpc/src/server/kernel.ts @@ -8,180 +8,154 @@ * You should have received a copy of the MIT License along with this program. */ -import { arrayRemoveItem, bufferToString, ClassType, createBuffer, ensureError, getClassName } from '@deepkit/core'; -import { ReceiveType, ReflectionKind, resolveReceiveType, serialize, stringifyUuid, Type, typeOf, writeUuid } from '@deepkit/type'; -import { RpcMessageSubject } from '../client/message-subject.js'; -import { - AuthenticationError, - ControllerDefinition, - ForwardedRpcStats, - rpcAuthenticate, - rpcClientId, - RpcError, - rpcError, - rpcPeerRegister, - rpcResponseAuthenticate, - RpcStats, - RpcTransportStats, - RpcTypes, -} from '../model.js'; -import { - BodyDecoder, - createRpcCompositeMessage, - createRpcCompositeMessageSourceDest, - createRpcMessage, - createRpcMessageSourceDest, - RpcBinaryMessageReader, - RpcCreateMessageDef, - rpcEncodeError, - RpcMessage, - RpcMessageDefinition, - RpcMessageRouteType, - serializeBinaryRpcMessage, -} from '../protocol.js'; -import { ActionTypes, RpcServerAction } from './action.js'; -import { RpcControllerAccess, RpcKernelSecurity, SessionState } from './security.js'; -import { RpcActionClient, RpcControllerState } from '../client/action.js'; -import { RemoteController } from '../client/client.js'; -import { InjectorContext, InjectorModule, NormalizedProvider, Resolver } from '@deepkit/injector'; +import { arrayRemoveItem, ClassType, getClassName } from '@deepkit/core'; +import { ReflectionKind, stringifyUuid, Type } from '@deepkit/type'; +import { ControllerDefinition, RpcError, RpcStats, RpcTransportStats } from '../model.js'; +import { RpcKernelSecurity, SessionState } from './security.js'; +import { InjectorContext, InjectorModule, NormalizedProvider, Setter } from '@deepkit/injector'; import { Logger, LoggerInterface } from '@deepkit/logger'; -import { RpcAction, rpcClass } from '../decorators.js'; +import { rpcClass } from '../decorators.js'; +import { TransportConnection, TransportOptions } from '../transport.js'; +import { EventDispatcher, EventDispatcherUnsubscribe, EventListenerCallback, EventToken } from '@deepkit/event'; +import { onRpcConnection, onRpcConnectionClose } from '../events.js'; import { - createWriter, - RpcBinaryWriter, - TransportBinaryMessageChunkWriter, - TransportConnection, - TransportMessageWriter, - TransportOptions, -} from '../transport.js'; -import { HttpRpcMessage, RpcHttpRequest, RpcHttpResponse } from './http.js'; -import { SingleProgress } from '../progress.js'; -import { DataEvent, EventDispatcher, EventDispatcherUnsubscribe, EventListenerCallback, EventToken } from '@deepkit/event'; -import { onRpcAuth, onRpcConnection, onRpcConnectionClose, RpcAuthEventStart } from '../events'; + actionIdSize, + ContextDispatcher, + contextIdSize, + flagSize, + getContextId, + isContextFlag, + isRouteFlag, + isTypeFlag, + MessageFlag, + routeParamSize, + writeContext, +} from '../protocol.js'; +import { ActionDispatcher } from '../action.js'; const anyType: Type = { kind: ReflectionKind.any }; -export class RpcCompositeMessage { - protected messages: RpcCreateMessageDef[] = []; - - public strictSerialization: boolean = false; - public logValidationErrors: boolean = false; - public errorLabel: string = 'Error in serialization'; - - constructor( - protected stats: RpcTransportStats, - protected logger: Logger, - public type: number, - protected id: number, - protected writer: TransportMessageWriter, - protected transportOptions: TransportOptions, - protected clientId?: Uint8Array, - protected source?: Uint8Array, - protected routeType: RpcMessageRouteType.client | RpcMessageRouteType.server = RpcMessageRouteType.client, - ) { - } - - add(type: number, body?: T, receiveType?: ReceiveType): this { - if (!this.strictSerialization) { - receiveType = anyType; - } - this.messages.push({ type, schema: receiveType ? resolveReceiveType(receiveType) : undefined, body }); - return this; - } - - write(message: RpcMessageDefinition): void { - try { - this.writer(message, this.transportOptions, this.stats); - } catch (error) { - if (this.logValidationErrors) { - this.logger.warn(this.errorLabel, error); - } - throw error; - } - } - - send() { - if (this.clientId && this.source) { - //we route back accordingly - this.write(createRpcCompositeMessageSourceDest(this.id, this.clientId, this.source, this.type, this.messages)); - } else { - this.write(createRpcCompositeMessage(this.id, this.type, this.messages, this.routeType)); - } - } -} - -export class RpcMessageBuilder { - public routeType: RpcMessageRouteType.client | RpcMessageRouteType.server = RpcMessageRouteType.client; - - public strictSerialization: boolean = true; - public logValidationErrors: boolean = false; - - public errorLabel: string = 'Error in serialization'; - - constructor( - protected stats: RpcTransportStats, - protected logger: Logger, - protected writer: TransportMessageWriter, - protected transportOptions: TransportOptions, - protected id: number, - protected clientId?: Uint8Array, - protected source?: Uint8Array, - ) { - } - - protected messageFactory(type: RpcTypes, schemaOrBody?: ReceiveType, data?: T): RpcMessageDefinition { - if (!this.strictSerialization) { - schemaOrBody = anyType; - } - - if (this.source && this.clientId) { - //we route back accordingly - return createRpcMessageSourceDest(this.id, type, this.clientId, this.source, data, schemaOrBody); - } else { - return createRpcMessage(this.id, type, data, this.routeType, schemaOrBody); - } - } - - write(message: RpcMessageDefinition): void { - try { - this.writer(message, this.transportOptions, this.stats); - } catch (error: any) { - if (this.logValidationErrors) { - this.logger.warn(this.errorLabel, error); - } - throw new RpcError(this.errorLabel + ': ' + error.message, { cause: error }); - } - } - - ack(): void { - this.write(this.messageFactory(RpcTypes.Ack)); - } - - error(error: Error | string): void { - const extracted = rpcEncodeError(error); - - this.write(this.messageFactory(RpcTypes.Error, typeOf(), extracted)); - } - - reply(type: number, body?: T, receiveType?: ReceiveType): void { - this.write(this.messageFactory(type, receiveType, body)); - } - - /** - * @deprecated - */ - replyBinary(type: number, body?: Uint8Array): void { - throw new RpcError('replyBinary deprecated'); - } - - composite(type: number): RpcCompositeMessage { - const composite = new RpcCompositeMessage(this.stats, this.logger, type, this.id, this.writer, this.transportOptions, this.clientId, this.source); - composite.strictSerialization = this.strictSerialization; - composite.logValidationErrors = this.logValidationErrors; - composite.errorLabel = this.errorLabel; - return composite; - } -} +// export class RpcCompositeMessage { +// protected messages: RpcCreateMessageDef[] = []; +// +// public strictSerialization: boolean = false; +// public logValidationErrors: boolean = false; +// public errorLabel: string = 'Error in serialization'; +// +// constructor( +// protected stats: RpcTransportStats, +// protected logger: Logger, +// public type: number, +// protected id: number, +// protected writer: TransportMessageWriter, +// protected transportOptions: TransportOptions, +// protected clientId?: Uint8Array, +// protected source?: Uint8Array, +// protected routeType: RpcMessageRouteType.client | RpcMessageRouteType.server = RpcMessageRouteType.client, +// ) { +// } +// +// add(type: number, body?: T, receiveType?: ReceiveType): this { +// if (!this.strictSerialization) { +// receiveType = anyType; +// } +// this.messages.push({ type, schema: receiveType ? resolveReceiveType(receiveType) : undefined, body }); +// return this; +// } +// +// write(message: RpcMessageDefinition): void { +// try { +// this.writer(message, this.transportOptions, this.stats); +// } catch (error) { +// if (this.logValidationErrors) { +// this.logger.warn(this.errorLabel, error); +// } +// throw error; +// } +// } +// +// send() { +// if (this.clientId && this.source) { +// //we route back accordingly +// this.write(createRpcCompositeMessageSourceDest(this.id, this.clientId, this.source, this.type, this.messages)); +// } else { +// this.write(createRpcCompositeMessage(this.id, this.type, this.messages, this.routeType)); +// } +// } +// } + +// export class RpcMessageBuilder { +// public routeType: RpcMessageRouteType.client | RpcMessageRouteType.server = RpcMessageRouteType.client; +// +// public strictSerialization: boolean = true; +// public logValidationErrors: boolean = false; +// +// public errorLabel: string = 'Error in serialization'; +// +// constructor( +// protected stats: RpcTransportStats, +// protected logger: Logger, +// protected writer: TransportMessageWriter, +// protected transportOptions: TransportOptions, +// protected id: number, +// protected clientId?: Uint8Array, +// protected source?: Uint8Array, +// ) { +// } +// +// protected messageFactory(type: RpcTypes, schemaOrBody?: ReceiveType, data?: T): RpcMessageDefinition { +// if (!this.strictSerialization) { +// schemaOrBody = anyType; +// } +// +// if (this.source && this.clientId) { +// //we route back accordingly +// return createRpcMessageSourceDest(this.id, type, this.clientId, this.source, data, schemaOrBody); +// } else { +// return createRpcMessage(this.id, type, data, this.routeType, schemaOrBody); +// } +// } +// +// write(message: RpcMessageDefinition): void { +// try { +// this.writer(message, this.transportOptions, this.stats); +// } catch (error: any) { +// if (this.logValidationErrors) { +// this.logger.warn(this.errorLabel, error); +// } +// throw new RpcError(this.errorLabel + ': ' + error.message, { cause: error }); +// } +// } +// +// ack(): void { +// this.write(this.messageFactory(RpcTypes.Ack)); +// } +// +// error(error: Error | string): void { +// const extracted = rpcEncodeError(error); +// +// this.write(this.messageFactory(RpcTypes.Error, typeOf(), extracted)); +// } +// +// reply(type: number, body?: T, receiveType?: ReceiveType): void { +// this.write(this.messageFactory(type, receiveType, body)); +// } +// +// /** +// * @deprecated +// */ +// replyBinary(type: number, body?: Uint8Array): void { +// throw new RpcError('replyBinary deprecated'); +// } +// +// composite(type: number): RpcCompositeMessage { +// const composite = new RpcCompositeMessage(this.stats, this.logger, type, this.id, this.writer, this.transportOptions, this.clientId, this.source); +// composite.strictSerialization = this.strictSerialization; +// composite.logValidationErrors = this.logValidationErrors; +// composite.errorLabel = this.errorLabel; +// return composite; +// } +// } /** * This is a reference implementation and only works in a single process. @@ -202,88 +176,57 @@ export class RpcPeerExchange { this.registeredPeers.set('string' === typeof id ? id : stringifyUuid(id), writer); } - redirect(message: RpcMessage) { - if (message.routeType == RpcMessageRouteType.peer) { - const peerId = message.getPeerId(); - const writer = this.registeredPeers.get(peerId); - if (!writer) { - //we silently ignore, as a pub/sub would do as well - console.log('NO writer found for peer', peerId); - return; - } - if (writer.writeBinary) writer.writeBinary(message.getBuffer()); - } - - if (message.routeType == RpcMessageRouteType.sourceDest) { - const destination = message.getDestination(); - - //in this implementation we have to stringify it first, since v8 can not index Uint8Arrays - const uuid = stringifyUuid(destination); - const writer = this.registeredPeers.get(uuid); - if (!writer) { - console.log('NO writer found for destination', uuid); - //we silently ignore, as a pub/sub would do as well - return; - } - if (writer.writeBinary) writer.writeBinary(message.getBuffer()); + redirect(message: Uint8Array) { + if (isRouteFlag(message[0], MessageFlag.RouteDirect)) { + // const peerId = message.getPeerId(); + // const writer = this.registeredPeers.get(peerId); + // if (!writer) { + // //we silently ignore, as a pub/sub would do as well + // console.log('NO writer found for peer', peerId); + // return; + // } + // if (writer.writeBinary) writer.writeBinary(message.getBuffer()); + } else if (isRouteFlag(message[0], MessageFlag.RouteDirect)) { + // const destination = message.getDestination(); + // + // //in this implementation we have to stringify it first, since v8 can not index Uint8Arrays + // const uuid = stringifyUuid(destination); + // const writer = this.registeredPeers.get(uuid); + // if (!writer) { + // console.log('NO writer found for destination', uuid); + // //we silently ignore, as a pub/sub would do as well + // return; + // } + // if (writer.writeBinary) writer.writeBinary(message.getBuffer()); } } } -export abstract class RpcKernelBaseConnection { - protected messageId: number = 0; - public sessionState = new SessionState(); - - public writer: TransportMessageWriter; - - protected reader = new RpcBinaryMessageReader( - this.handleMessage.bind(this), - (id: number) => { - this.writer(createRpcMessage(id, RpcTypes.ChunkAck), this.transportOptions, this.stats); - }, - ); - - /** - * Statistics about the server->client communication. - */ - public clientStats: RpcStats = new RpcStats(); +function noop() {} - /** - * When the server wants to execute an action on the client, it uses the actionClient. - */ - protected actionClient: RpcActionClient = new RpcActionClient(this); +export class RpcKernelConnection { + public sessionState = new SessionState(); - protected id: Uint8Array = writeUuid(createBuffer(16)); + protected selfContext = new ContextDispatcher(); - protected replies = new Map(); public transportOptions: TransportOptions = new TransportOptions(); - protected binaryChunkWriter = new TransportBinaryMessageChunkWriter(this.reader, this.transportOptions); protected timeoutTimers: any[] = []; - public readonly onClose: Promise; - protected onCloseResolve?: Function; + public closed: boolean = false; + public stats: RpcStats = new RpcStats; + + protected fastReply = new Uint8Array(flagSize + routeParamSize + contextIdSize + actionIdSize); constructor( - public stats: RpcStats, + protected onClose: (connection: RpcKernelConnection) => void, + protected actionDispatcher: ActionDispatcher, + protected serverStats: RpcStats, protected logger: Logger, public transportConnection: TransportConnection, - protected connections: RpcKernelConnections, protected injector: InjectorContext, protected eventDispatcher: EventDispatcher, ) { - this.stats.increase('connections', 1); - this.stats.increase('totalConnections', 1); - this.writer = createWriter(transportConnection, this.transportOptions, this.reader); - - this.connections.connections.push(this); - this.onClose = new Promise((resolve) => { - this.onCloseResolve = resolve; - }); - } - - write(message: RpcMessageDefinition): void { - this.writer(message, this.transportOptions, this.stats); } /** @@ -291,18 +234,10 @@ export abstract class RpcKernelBaseConnection { * a chunk writer (splitting the message into smaller parts if necessary, * so they can be tracked). */ - sendBinary(message: RpcMessageDefinition, writer: RpcBinaryWriter): void { - this.binaryChunkWriter.write(writer, serializeBinaryRpcMessage(message)); - } - clientAddress(): string | undefined { return this.transportConnection.clientAddress ? this.transportConnection.clientAddress() : undefined; } - createMessageBuilder(): RpcMessageBuilder { - return new RpcMessageBuilder(this.stats, this.logger, this.writer, this.transportOptions, this.messageId++); - } - /** * Creates a regular timer using setTimeout() and automatically cancel it once the connection breaks or server stops. */ @@ -317,341 +252,94 @@ export abstract class RpcKernelBaseConnection { public close(reason: string | Error = 'closed'): void { if (this.closed) return; - this.closed = true; - for (const subject of this.replies.values()) { - subject.disconnect(); - } - this.replies.clear(); - this.stats.increase('connections', -1); + + // todo: close all contexts + + this.serverStats.connections--; + this.eventDispatcher.dispatch(onRpcConnectionClose, () => ({ reason, context: { connection: this, injector: this.injector }, }), this.injector); - for (const timeout of this.timeoutTimers) clearTimeout(timeout); - if (this.onCloseResolve) this.onCloseResolve(); - arrayRemoveItem(this.connections.connections, this); - this.transportConnection.close(); - } - - public feed(buffer: Uint8Array, bytes?: number): void { - this.stats.increase('incomingBytes', bytes ?? buffer.byteLength); - this.reader.feed(buffer, bytes); - } - - public handleMessage(message: RpcMessage): void { - this.stats.increase('incoming', 1); - if (message.routeType === RpcMessageRouteType.server) { - //initiated by the server, so we check replies - const callback = this.replies.get(message.id); - if (callback) { - callback.next(message); - return; - } - } - - const response = new RpcMessageBuilder(this.stats, this.logger, this.writer, this.transportOptions, message.id); - this.onMessage(message, response); - } - - onRequest(basePath: string, request: RpcHttpRequest, response: RpcHttpResponse): void | Promise { - throw new RpcError('Not supported'); - } - - abstract onMessage(message: RpcMessage, response: RpcMessageBuilder): void | Promise; - - public controller(nameOrDefinition: string | ControllerDefinition, timeoutInSeconds = 60): RemoteController { - const controller = new RpcControllerState('string' === typeof nameOrDefinition ? nameOrDefinition : nameOrDefinition.path); - - return new Proxy(this, { - get: (target, propertyName) => { - return (...args: any[]) => { - return this.actionClient.action(controller, propertyName as string, args); - }; - }, - }) as any as RemoteController; - } - - public sendMessage( - type: number, - body?: T, - receiveType?: ReceiveType, - ): RpcMessageSubject { - if (this.closed) throw new RpcError('Connection closed'); - const id = this.messageId++; - const continuation = (type: number, body?: T, receiveType?: ReceiveType) => { - //send a message with the same id. Don't use sendMessage() again as this would lead to a memory leak - // and a new id generated. We want to use the same id. - const message = createRpcMessage(id, type, body, RpcMessageRouteType.server, receiveType); - this.writer(message, this.transportOptions, this.stats); - }; - - const subject = new RpcMessageSubject(continuation, () => { - this.replies.delete(id); - }); - - this.replies.set(id, subject); - - const message = createRpcMessage(id, type, body, RpcMessageRouteType.server, receiveType); - this.writer(message, this.transportOptions, this.stats); - - return subject; - } -} - -export class RpcKernelConnections { - public connections: RpcKernelBaseConnection[] = []; - - public stats: RpcTransportStats = new RpcTransportStats; - - broadcast(buffer: RpcMessageDefinition) { - for (const connection of this.connections) { - connection.writer(buffer, connection.transportOptions, this.stats); - } - } -} - -export interface RpcCacheAction { - controller: RpcControllerAccess; - fn: Function; - types: ActionTypes; - action: RpcAction; - resolver: Resolver; - bodyDecoder: BodyDecoder; - label: string; //controller.action -} - -export class RpcCache { - actionsTypes: { [id: string]: ActionTypes } = {}; - actions: { [id: string]: RpcCacheAction } = {}; -} - -export class RpcKernelConnection extends RpcKernelBaseConnection { - public myPeerId?: string; - protected actionHandler = new RpcServerAction(this.stats, this.cache, this, this.controllers, this.injector, this.eventDispatcher, this.security, this.sessionState, this.logger); - - public routeType: RpcMessageRouteType.client | RpcMessageRouteType.server = RpcMessageRouteType.client; - protected context: { connection: RpcKernelBaseConnection, injector: InjectorContext } = { connection: this, injector: this.injector }; - - constructor( - stats: RpcStats, - logger: Logger, - transport: TransportConnection, - connections: RpcKernelConnections, - injector: InjectorContext, - eventDispatcher: EventDispatcher, - protected cache: RpcCache, - protected controllers: Map, - protected security = new RpcKernelSecurity(), - protected peerExchange: RpcPeerExchange, - ) { - super(stats, logger, transport, connections, injector, eventDispatcher); - this.onClose.then(async () => { - try { - await this.peerExchange.deregister(this.id); - await this.actionHandler.onClose(); - } catch (e) { - logger.error('Could no deregister/action close: ' + e); - } - }); - //register the current client so it can receive messages - this.peerExchange.register(this.id, this.transportConnection); - } + for (const timeout of this.timeoutTimers) clearTimeout(timeout); - public close(): void { - super.close(); + this.transportConnection.close(); + this.onClose(this); } - async onRequest(basePath: string, request: RpcHttpRequest, response: RpcHttpResponse) { - let routeType: any = RpcMessageRouteType.client; - const id = 0; - let source: Uint8Array | undefined = undefined; - if (!basePath.endsWith('/')) basePath += '/'; - if (!basePath.startsWith('/')) basePath = '/' + basePath; - const url = new URL(request.url || '', 'http://localhost/' + basePath); + public feed(message: Uint8Array): void { + this.stats.incomingBytes += message.byteLength; + this.stats.incoming++; + this.serverStats.incomingBytes += message.byteLength; + this.serverStats.incoming++; - try { - const messageResponse = new RpcMessageBuilder(this.stats, this.logger, (message: RpcMessageDefinition, options: TransportOptions, stats: RpcTransportStats, progress?: SingleProgress) => { - response.setHeader('Content-Type', 'application/json'); - response.setHeader('X-Message-Type', message.type); - response.setHeader('X-Message-Composite', String(!!message.composite)); - response.setHeader('X-Message-RouteType', String(message.routeType)); - response.writeHead(200); - - if (message.body) { - let body = serialize(message.body.body, undefined, undefined, undefined, message.body.type); - if (message.type === RpcTypes.ResponseActionSimple) { - body = body.v; - } - response.end(JSON.stringify(body)); - } - }, this.transportOptions, id, this.id, routeType === RpcMessageRouteType.peer ? source : undefined); - messageResponse.routeType = this.routeType; - - const urlPath = url.pathname.substring(basePath.length); - const lastSlash = urlPath.lastIndexOf('/'); - const base: { controller: string, method: string, args?: any[] } = { - controller: urlPath.substring(0, lastSlash), - method: decodeURIComponent(urlPath.substring(lastSlash + 1)), - }; - - let type = false; - if (base.method.endsWith('.type')) { - base.method = base.method.substring(0, base.method.length - 5); - type = true; - } - - if (request.headers['Authorization']) { - const auth = String(request.headers['Authorization']); - const token = auth.startsWith('Bearer ') ? auth.substring(7) : auth; - const session = await this.security.authenticate(token, this); - this.sessionState.setSession(session); - } - - if (type) { - await this.actionHandler.handleActionTypes( - new HttpRpcMessage(1, false, RpcTypes.ActionType, RpcMessageRouteType.client, request.headers, base), - messageResponse, - ); - } else { - const body = request.body && request.body.byteLength > 0 ? JSON.parse(bufferToString(request.body)) : { args: url.searchParams.getAll('arg').map(v => v) }; - base.args = body.args || []; - await this.actionHandler.handleAction( - new HttpRpcMessage(1, false, RpcTypes.Action, RpcMessageRouteType.client, request.headers, base), - messageResponse, - ); - } - } catch (error: any) { - this.logger.error('onRequest failed', error); - response.writeHead(400); - response.end(JSON.stringify({ error: error.message })); - } - } + // todo: change these conditions to array lookup - async onMessage(message: RpcMessage): Promise { - if (message.routeType == RpcMessageRouteType.peer && message.getPeerId() !== this.myPeerId) { - // console.log('Redirect peer message', RpcTypes[message.type]); - if (!await this.security.isAllowedToSendToPeer(this.sessionState.getSession(), message.getPeerId())) { - new RpcMessageBuilder(this.stats, this.logger, this.writer, this.transportOptions, message.id).error(new RpcError('Access denied')); - return; - } - this.peerExchange.redirect(message); + if (isRouteFlag(message[0], MessageFlag.RouteDirect)) { + // todo: forward to another connection (e.g. via broker) return; } - if (message.routeType == RpcMessageRouteType.sourceDest) { - // console.log('Redirect sourceDest message', RpcTypes[message.type]); - this.peerExchange.redirect(message); + if (isTypeFlag(message[0], MessageFlag.TypeChunk)) { + // todo: handle chunks + // this.replies.get(message.id)?.ack(); return; } - if (message.type === RpcTypes.Ping) { - this.writer(createRpcMessage(message.id, RpcTypes.Pong), this.transportOptions, this.stats); + if (isContextFlag(message[0], MessageFlag.ContextExisting)) { + console.log('context existing'); + const contextId = getContextId(message); + // this.selfContext.dispatch(contextId, message); return; } - //all outgoing replies need to be routed to the source via sourceDest messages. - const response = new RpcMessageBuilder(this.stats, this.logger, this.writer, this.transportOptions, message.id, this.id, message.routeType === RpcMessageRouteType.peer ? message.getSource() : undefined); - response.routeType = this.routeType; + // const contextId = getContextId(message); + // if (isContextFlag(message[0], MessageFlag.ContextNew)) { + // this.selfContext.create(() => { + // // here all future message for this particular contextId will be dispatched to + // }) + // } try { - if (message.routeType === RpcMessageRouteType.client) { - switch (message.type) { - case RpcTypes.ClientId: - return response.reply(RpcTypes.ClientIdResponse, { id: this.id }); - case RpcTypes.PeerRegister: - return await this.registerAsPeer(message, response); - case RpcTypes.PeerDeregister: - return this.deregisterAsPeer(message, response); - } - } - - switch (message.type) { - case RpcTypes.Authenticate: - return await this.authenticate(message, response); - case RpcTypes.ActionType: - return await this.actionHandler.handleActionTypes(message, response); - case RpcTypes.Action: - return await this.actionHandler.handleAction(message, response); - default: - return await this.actionHandler.handle(message, response); - } - } catch (error: any) { - response.error(error); - } - } + if (isTypeFlag(message[0], MessageFlag.TypeAction)) { + if (isContextFlag(message[0], MessageFlag.ContextNew)) { + const id = this.selfContext.create(noop); - protected async authenticate(message: RpcMessage, response: RpcMessageBuilder) { - const body = message.parseBody(); - const event = new DataEvent({ - phase: 'start', context: this.context, token: body.token, - }); - await this.eventDispatcher.dispatch(onRpcAuth, event, this.injector); - - try { - let session = event.data.session; - if ('undefined' === typeof session) { - session = await this.security.authenticate(body.token, this); - } - await this.eventDispatcher.dispatch(onRpcAuth, () => ({ - phase: 'success', context: this.context, token: body.token, session, - }), this.injector); - this.sessionState.setSession(session); - - response.reply(RpcTypes.AuthenticateResponse, { username: session.username }); - } catch (error) { - await this.eventDispatcher.dispatch(onRpcAuth, () => ({ - phase: 'fail', context: this.context, token: body.token, error: ensureError(error, RpcError), - }), this.injector); - if (error instanceof AuthenticationError) throw new RpcError(error.message); - this.logger.error('authenticate failed', error); - throw new AuthenticationError('Authentication failed', { cause: error }); - } - } + // todo: execute action + this.actionDispatcher.dispatch(message, this.injector.scope!); - protected async deregisterAsPeer(message: RpcMessage, response: RpcMessageBuilder) { - const body = message.parseBody(); + this.fastReply[0] = MessageFlag.RouteClient | MessageFlag.TypeAck | MessageFlag.ContextEnd; + writeContext(this.fastReply, id); - try { - if (body.id !== this.myPeerId) { - return response.error(new RpcError(`Not registered as that peer`)); + this.selfContext.release(id); + this.transportConnection.write(this.fastReply); + } else { + this.actionDispatcher.dispatch(message, this.injector.scope!); + } + return; } - this.myPeerId = undefined; - await this.peerExchange.deregister(body.id); - response.ack(); } catch (error) { - this.logger.error('deregisterAsPeer failed', error); - response.error(new RpcError('Failed')); + console.log('RpcKernelConnection.feed error', error); } } +} - protected async registerAsPeer(message: RpcMessage, response: RpcMessageBuilder) { - const body = message.parseBody(); - - try { - if (await this.peerExchange.isRegistered(body.id)) { - return response.error(new RpcError(`Peer ${body.id} already registered`)); - } +export class RpcKernelConnections { + public connections: RpcKernelConnection[] = []; - if (!await this.security.isAllowedToRegisterAsPeer(this.sessionState.getSession(), body.id)) { - response.error(new RpcError('Access denied')); - return; - } + public stats: RpcTransportStats = new RpcTransportStats; - await this.peerExchange.register(body.id, this.transportConnection); - this.myPeerId = body.id; - response.ack(); - } catch (error) { - this.logger.error('registerAsPeer failed', error); - response.error(new RpcError('Failed')); + broadcast(message: Uint8Array) { + for (const connection of this.connections) { + connection.feed(message); } } } export type OnConnectionCallback = (connection: RpcKernelConnection, injector: InjectorContext, logger: LoggerInterface) => void; - /** * The kernel is responsible for parsing the message header, redirecting to peer if necessary, loading the body parser, * and encode/send outgoing messages. @@ -659,11 +347,10 @@ export type OnConnectionCallback = (connection: RpcKernelConnection, injector: I * @reflection never */ export class RpcKernel { - public readonly controllers = new Map(); + public readonly controllers = new Map(); - protected cache: RpcCache = new RpcCache; + protected actions = new ActionDispatcher(); - protected peerExchange = new RpcPeerExchange; protected connections = new RpcKernelConnections; protected RpcKernelConnection = RpcKernelConnection; @@ -675,6 +362,8 @@ export class RpcKernel { public injector: InjectorContext; + protected setConnection: Setter = () => undefined; + constructor( injector?: InjectorContext | NormalizedProvider[], protected logger: Logger = new Logger(), @@ -689,7 +378,6 @@ export class RpcKernel { //will be provided when scope is created { provide: RpcKernelConnection, scope: 'rpc', useValue: undefined }, - { provide: RpcKernelBaseConnection, scope: 'rpc', useValue: undefined }, { provide: Logger, useValue: logger }, @@ -728,8 +416,8 @@ export class RpcKernel { */ public registerController(controller: ClassType, id?: string | ControllerDefinition, module?: InjectorModule) { if (this.autoInjector) { - if (!this.injector.rootModule.isProvided(controller)) { - this.injector.rootModule.addProvider({ provide: controller, scope: 'rpc' }); + if (!this.injector.module.isProvided(controller)) { + this.injector.module.addProvider({ provide: controller, scope: 'rpc' }); } } if (!id) { @@ -737,29 +425,43 @@ export class RpcKernel { if (!rpcConfig) throw new RpcError(`Controller ${getClassName(controller)} has no @rpc.controller() decorator and no controller id was provided.`); id = rpcConfig.getPath(); } + const name = 'string' === typeof id ? id : id.path; + this.controllers.set('string' === typeof id ? id : id.path, { + name, controller, - module: module || this.injector.rootModule, + module: module || this.injector.module, }); + this.built = false; } - createConnection(transport: TransportConnection, injector?: InjectorContext): RpcKernelBaseConnection { + protected built = false; + + createConnection(transport: TransportConnection, injector?: InjectorContext): RpcKernelConnection { if (!injector) injector = this.injector.createChildScope('rpc'); + if (!this.built) { + this.setConnection = this.injector.setter(undefined, RpcKernelConnection); + this.actions.build(this.injector, this.controllers.values()); + this.built = true; + } + + this.stats.connections++; + this.stats.totalConnections++; const connection = new this.RpcKernelConnection( - new ForwardedRpcStats(this.stats), + (connection: RpcKernelConnection) => { + arrayRemoveItem(this.connections.connections, connection); + }, + this.actions, + this.stats, this.logger.scoped('rpc:connection'), transport, - this.connections, injector, this.getEventDispatcher(), - this.cache, - this.controllers, - injector.get(RpcKernelSecurity), - this.peerExchange, ); - injector.set(RpcKernelConnection, connection); - injector.set(RpcKernelBaseConnection, connection); + this.connections.connections.push(connection); + this.setConnection(connection, injector.scope); + for (const on of this.onConnectionListeners) on(connection, injector, this.logger); this.getEventDispatcher().dispatch(onRpcConnection, { context: { connection, injector } }); return connection; diff --git a/packages/rpc/src/transport.ts b/packages/rpc/src/transport.ts index 4efa7563c..3d981ca50 100644 --- a/packages/rpc/src/transport.ts +++ b/packages/rpc/src/transport.ts @@ -1,14 +1,3 @@ -import { - createRpcMessage, - readBinaryRpcMessage, - RpcBinaryMessageReader, - RpcMessage, - RpcMessageDefinition, - serializeBinaryRpcMessage, -} from './protocol.js'; -import { SingleProgress } from './progress.js'; -import { rpcChunk, RpcError, RpcTransportStats, RpcTypes } from './model.js'; - export class TransportOptions { /** * Stores big buffers to the file system and stream it from there. @@ -31,24 +20,11 @@ export class TransportOptions { public chunkSize: number = 100_000; } -/** - * @see createWriter - */ -export interface TransportMessageWriter { - (message: RpcMessageDefinition, options: TransportOptions, stats: RpcTransportStats, progress?: SingleProgress): void; -} - export interface TransportConnection { - /** - * Write is used either by Client->Server, or Server->Client. - * The method is responsible to serialize the message and send it over the wire. - */ - write?: TransportMessageWriter; - /** * Same as write, but sends binary directly. This enables chunking automatically. */ - writeBinary?(message: Uint8Array): void; + write(message: Uint8Array): void; bufferedAmount?(): number; @@ -66,17 +42,9 @@ export interface TransportClientConnection { onError(error: Error): void; - /** - * Called when data is received from the other side. - * The method is responsible to deserialize the message. - */ - read(message: RpcMessage): void; - - readBinary(message: Uint8Array, bytes?: number): void; + read(message: Uint8Array): void; } -export type RpcBinaryWriter = (buffer: Uint8Array) => void; - /** * This class acts as a layer between kernel/client and a connection writer. * It automatically chunks long messages into multiple smaller one using the RpcType.Chunks type. @@ -86,71 +54,71 @@ export type RpcBinaryWriter = (buffer: Uint8Array) => void; * It automatically saves big buffer to the file system and streams data from there to not * block valuable memory. */ -export class TransportBinaryMessageChunkWriter { - protected chunkId = 0; - - constructor( - protected reader: RpcBinaryMessageReader, - protected options: TransportOptions, - ) { - } - - /** - * Writes a message buffer to the connection and chunks if necessary. - */ - write(writer: RpcBinaryWriter, message: Uint8Array, progress?: SingleProgress): void { - this.writeFull(writer, message, progress) - .catch(error => console.log('TransportBinaryMessageChunkWriter writeAsync error', error)); - } - - async writeFull(writer: RpcBinaryWriter, buffer: Uint8Array, progress?: SingleProgress): Promise { - if (this.options.chunkSize && buffer.byteLength >= this.options.chunkSize) { - //split up - const chunkId = this.chunkId++; - const message = readBinaryRpcMessage(buffer); //we need the original message-id, so the chunks are correctly assigned in Progress tracker - let offset = 0; - while (offset < buffer.byteLength) { - //todo: check back-pressure and wait if necessary - const slice = buffer.slice(offset, offset + this.options.chunkSize); - const chunkMessage = createRpcMessage(message.id, RpcTypes.Chunk, { - id: chunkId, - total: buffer.byteLength, - v: slice, - }); - offset += slice.byteLength; - const promise = new Promise((resolve) => { - this.reader.onChunkAck(message.id, resolve); - }); - writer(serializeBinaryRpcMessage(chunkMessage)); - await promise; - progress?.set(buffer.byteLength, offset); - } - } else { - writer(buffer); - progress?.set(buffer.byteLength, buffer.byteLength); - } - } -} - -export function createWriter(transport: TransportConnection, options: TransportOptions, reader: RpcBinaryMessageReader): TransportMessageWriter { - if (transport.writeBinary) { - const chunkWriter = new TransportBinaryMessageChunkWriter(reader, options); - const writeBinary = transport.writeBinary; - return (message, options, stats, progress) => { - const buffer = serializeBinaryRpcMessage(message); - stats.increase('outgoing', 1); - stats.increase('outgoingBytes', buffer.byteLength); - chunkWriter.write(writeBinary, buffer, progress); - }; - } - - if (transport.write) { - const write = transport.write; - return (message, options, stats, progress) => { - stats.increase('outgoing', 1); - write(message, options, stats, progress); - }; - } - - throw new RpcError('No write method found on transport'); -} +// export class TransportBinaryMessageChunkWriter { +// protected chunkId = 0; +// +// constructor( +// protected reader: RpcBinaryMessageReader, +// protected options: TransportOptions, +// ) { +// } +// +// /** +// * Writes a message buffer to the connection and chunks if necessary. +// */ +// write(writer: RpcBinaryWriter, message: Uint8Array, progress?: SingleProgress): void { +// this.writeFull(writer, message, progress) +// .catch(error => console.log('TransportBinaryMessageChunkWriter writeAsync error', error)); +// } +// +// async writeFull(writer: RpcBinaryWriter, buffer: Uint8Array, progress?: SingleProgress): Promise { +// if (this.options.chunkSize && buffer.byteLength >= this.options.chunkSize) { +// //split up +// const chunkId = this.chunkId++; +// const message = readBinaryRpcMessage(buffer); //we need the original message-id, so the chunks are correctly assigned in Progress tracker +// let offset = 0; +// while (offset < buffer.byteLength) { +// //todo: check back-pressure and wait if necessary +// const slice = buffer.slice(offset, offset + this.options.chunkSize); +// // const chunkMessage = createRpcMessage(message.contextId, RpcTypes.Chunk, { +// // id: chunkId, +// // total: buffer.byteLength, +// // v: slice, +// // }); +// offset += slice.byteLength; +// const promise = new Promise((resolve) => { +// this.reader.onChunkAck(message.contextId, resolve); +// }); +// // writer(serializeBinaryRpcMessage(chunkMessage)); +// await promise; +// progress?.set(buffer.byteLength, offset); +// } +// } else { +// writer(buffer); +// progress?.set(buffer.byteLength, buffer.byteLength); +// } +// } +// } + +// export function createWriter(transport: TransportConnection, options: TransportOptions, reader: RpcBinaryMessageReader): TransportMessageWriter { +// if (transport.writeBinary) { +// const chunkWriter = new TransportBinaryMessageChunkWriter(reader, options); +// const writeBinary = transport.writeBinary; +// return (message, options, stats, progress) => { +// // const buffer = serializeBinaryRpcMessage(message); +// // stats.increase('outgoing', 1); +// // stats.increase('outgoingBytes', buffer.byteLength); +// // chunkWriter.write(writeBinary, buffer, progress); +// }; +// } +// +// if (transport.write) { +// const write = transport.write; +// return (message, options, stats, progress) => { +// stats.increase('outgoing', 1); +// write(message, options, stats, progress); +// }; +// } +// +// throw new RpcError('No write method found on transport'); +// } diff --git a/packages/rpc/tests/action.spec.ts b/packages/rpc/tests/action.spec.ts new file mode 100644 index 000000000..f46ca1d07 --- /dev/null +++ b/packages/rpc/tests/action.spec.ts @@ -0,0 +1,103 @@ +import { expect, test } from '@jest/globals'; +import { ActionDispatcher } from '../src/action.js'; +import { rpc } from '../src/decorators.js'; +import { InjectorContext, InjectorModule } from '@deepkit/injector'; +import { getActionOffset, writeAction } from '../src/protocol.js'; +import { serializeBSONWithoutOptimiser, Writer } from '@deepkit/bson'; + +test('action', async () => { + const dispatcher = new ActionDispatcher(); + + let calls1 = 0; + const calls2: number[] = []; + const calls3: [number, string][] = []; + const calls4: any[] = []; + + class Controller { + @rpc.action() + test1() { + calls1++; + } + + @rpc.action() + test2(a: number) { + calls2.push(a); + } + + @rpc.action() + test3(a: number, b: string) { + calls3.push([a, b]); + } + + @rpc.action() + test4(a: number | string) { + calls4.push(a); + } + } + + const module = new InjectorModule([{ provide: Controller, scope: 'rpc' }]); + const injector = new InjectorContext(module); + dispatcher.build(injector, [ + { name: 'main', controller: Controller, module }, + ]); + + const scope = injector.createChildScope('rpc'); + + { + const message = new Uint8Array(3); + writeAction(message, 0); + dispatcher.dispatch(message, scope.scope!); + expect(calls1).toBe(1); + } + + { + const message = new Uint8Array(3 + 8); + writeAction(message, 1); + const bodyOffset = getActionOffset(message[0]) + 2; + const writer = new Writer(message, bodyOffset); + writer.writeDouble(256); + dispatcher.dispatch(message, scope.scope!); + console.log('calls2', calls2); + expect(calls2).toEqual([256]); + } + + { + const message = new Uint8Array(3 + 8 + 4 + 5); + writeAction(message, 2); + const bodyOffset = getActionOffset(message[0]) + 2; + const writer = new Writer(message, bodyOffset); + writer.writeDouble(256); + + writer.writeUint32(5); + writer.writeString('1234'); + writer.writeByte(0); + + dispatcher.dispatch(message, scope.scope!); + console.log('calls3', calls3); + expect(calls3).toEqual([[256, '1234']]); + } + + { + const message = new Uint8Array(3 + 32); + writeAction(message, 3); + const bodyOffset = getActionOffset(message[0]) + 2; + const bson = serializeBSONWithoutOptimiser([123]); + message.set(bson, bodyOffset); + + dispatcher.dispatch(message, scope.scope!); + console.log('calls4', calls4); + expect(calls4).toEqual([123]); + } + + { + const message = new Uint8Array(3 + 32); + writeAction(message, 3); + const bodyOffset = getActionOffset(message[0]) + 2; + const bson = serializeBSONWithoutOptimiser(['abc']); + message.set(bson, bodyOffset); + + dispatcher.dispatch(message, scope.scope!); + console.log('calls4', calls4); + expect(calls4).toEqual([123, 'abc']); + } +}); diff --git a/packages/rpc/tests/connection.spec.ts b/packages/rpc/tests/connection.spec.ts index 2f1685940..a6221ea70 100644 --- a/packages/rpc/tests/connection.spec.ts +++ b/packages/rpc/tests/connection.spec.ts @@ -14,7 +14,7 @@ test('connect', async () => { const client = new RpcClient({ connect(connection: TransportClientConnection) { const kernelConnection = kernel.createConnection({ - writeBinary: (buffer) => connection.readBinary(buffer), + write: (buffer) => connection.read(buffer), close: () => { connection.onClose(''); }, @@ -32,7 +32,7 @@ test('connect', async () => { close() { kernelConnection.close(); }, - writeBinary(buffer) { + write(buffer) { kernelConnection.feed(buffer); }, }); diff --git a/packages/rpc/tests/custom-message.spec.ts b/packages/rpc/tests/custom-message.spec.ts index 173949b24..6c29e72e0 100644 --- a/packages/rpc/tests/custom-message.spec.ts +++ b/packages/rpc/tests/custom-message.spec.ts @@ -27,13 +27,13 @@ test('back controller', async () => { class MyRpcKernelConnection extends RpcKernelConnection { async onMessage(message: RpcMessage): Promise { if (message.type === MyTypes.QueryAndAnswer) { - this.write(createRpcMessage<{ v: string }>(message.id, MyTypes.Answer, { v: '42 is the answer' })); + this.write(createRpcMessage<{ v: string }>(message.contextId, MyTypes.Answer, { v: '42 is the answer' })); return; } if (message.type === MyTypes.BroadcastWithAck) { broadcastWithAckCalled = message.parseBody<{v: string}>() - this.write(createRpcMessage(message.id, MyTypes.Ack)); + this.write(createRpcMessage(message.contextId, MyTypes.Ack)); return; } diff --git a/packages/rpc/tests/entity-state.spec.ts b/packages/rpc/tests/entity-state.spec.ts index b5ba145c6..234ea513f 100644 --- a/packages/rpc/tests/entity-state.spec.ts +++ b/packages/rpc/tests/entity-state.spec.ts @@ -1,6 +1,6 @@ import { cast, entity, ReflectionClass } from '@deepkit/type'; import { expect, test } from '@jest/globals'; -import { EntitySubject, rpcEntityPatch, RpcTypes } from '../src/model.js'; +import { EntitySubject, RpcAction, rpcEntityPatch } from '../src/model.js'; import { DirectClient } from '../src/client/client-direct.js'; import { EntitySubjectStore } from '../src/client/entity-state.js'; import { rpc } from '../src/decorators.js'; @@ -86,8 +86,8 @@ test('controller', async () => { setTimeout(() => { this.connection.createMessageBuilder() - .composite(RpcTypes.Entity) - .add(RpcTypes.EntityPatch, { + .composite(RpcAction.Entity) + .add(RpcAction.EntityPatch, { entityName: ReflectionClass.from(MyModel).getName(), id: model.id, version: model.version + 1, diff --git a/packages/rpc/tests/model.spec.ts b/packages/rpc/tests/model.spec.ts deleted file mode 100644 index eb4772a15..000000000 --- a/packages/rpc/tests/model.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { expect, test } from '@jest/globals'; -import { EntitySubject, isEntitySubject } from '../src/model.js'; - -test('entitySubject', async () => { - class User { - id!: string; - } - - expect(isEntitySubject(new EntitySubject(new User))).toBe(true); -}); diff --git a/packages/rpc/tests/protocol.spec.ts b/packages/rpc/tests/protocol.spec.ts new file mode 100644 index 000000000..cc8567302 --- /dev/null +++ b/packages/rpc/tests/protocol.spec.ts @@ -0,0 +1,275 @@ +import { expect, test } from '@jest/globals'; +import { + ContextDispatcher, + contextIdSize, + flagSize, + getAction, + getActionOffset, + getContextId, + getContextIdOffset, + getRandomAddress, + getRouteDirectDst, + getRouteDirectPort, + getRouteDirectSrc, + getRouteTypeOffset, + isContextFlag, + isRouteFlag, + isTypeFlag, + MessageFlag, + readUint16LE, + routeParamSize, + setRouteFlag, + setTypeFlag, + writeAction, + writeContext, + writeDirectRoute, + writeUint16LE, +} from '../src/protocol.js'; +import { RpcAction } from '../src/model.js'; +import { getActions, rpc } from '../src/decorators.js'; +import { InjectorModule } from '@deepkit/injector'; + +test('assume newUin8Array is not copied', () => { + const buffer = new Uint8Array(32); + buffer[0] = 42; + + const reply = buffer.subarray(0, 1); + + expect(reply.buffer === buffer.buffer).toBe(true); + + // Modify `reply[0]` + reply[0] = 99; + + expect(reply[0]).toBe(99); + expect(buffer[0]).toBe(99); +}); + +test('header', () => { + // route type is constant offset + expect(getRouteTypeOffset(0)).toBe(flagSize); + expect(getRouteTypeOffset(MessageFlag.RouteDirect)).toBe(flagSize); + + expect(getContextIdOffset(0)).toBe(flagSize); + expect(getContextIdOffset(MessageFlag.RouteDirect)).toBe(flagSize + routeParamSize); + expect(getContextIdOffset(MessageFlag.ContextNew)).toBe(flagSize); + + expect(getActionOffset(0)).toBe(flagSize); + + expect(getActionOffset(MessageFlag.RouteClient)).toBe(flagSize); + expect(getActionOffset(MessageFlag.RouteServer)).toBe(flagSize); + expect(getActionOffset(MessageFlag.RouteDirect)).toBe(flagSize + routeParamSize); + + expect(getActionOffset(MessageFlag.RouteDirect | MessageFlag.ContextExisting)).toBe(flagSize + routeParamSize + contextIdSize); + expect(getActionOffset(MessageFlag.RouteDirect | MessageFlag.ContextNew)).toBe(flagSize + routeParamSize); + expect(getActionOffset(MessageFlag.RouteDirect | MessageFlag.ContextNone)).toBe(flagSize + routeParamSize); +}); + +test('setRouteFlag', () => { + const message = new Uint8Array(1); + message[0] = MessageFlag.RouteClient | MessageFlag.ContextExisting | MessageFlag.TypeAck; + expect(isRouteFlag(message[0], MessageFlag.RouteClient)).toBe(true); + expect(isRouteFlag(message[0], MessageFlag.RouteServer)).toBe(false); + expect(isRouteFlag(message[0], MessageFlag.RouteDirect)).toBe(false); + + expect(isContextFlag(message[0], MessageFlag.ContextExisting)).toBe(true); + expect(isContextFlag(message[0], MessageFlag.ContextNew)).toBe(false); + expect(isContextFlag(message[0], MessageFlag.ContextNone)).toBe(false); + expect(isContextFlag(message[0], MessageFlag.ContextEnd)).toBe(false); + + expect(isTypeFlag(message[0], MessageFlag.TypeAck)).toBe(true); + expect(isTypeFlag(message[0], MessageFlag.TypeAction)).toBe(false); + expect(isTypeFlag(message[0], MessageFlag.TypeChunk)).toBe(false); + expect(isTypeFlag(message[0], MessageFlag.TypeError)).toBe(false); + + setRouteFlag(message, MessageFlag.RouteServer); + expect(isRouteFlag(message[0], MessageFlag.RouteClient)).toBe(false); + expect(isRouteFlag(message[0], MessageFlag.RouteServer)).toBe(true); + expect(isRouteFlag(message[0], MessageFlag.RouteDirect)).toBe(false); + + setRouteFlag(message, MessageFlag.RouteDirect); + expect(isRouteFlag(message[0], MessageFlag.RouteClient)).toBe(false); + expect(isRouteFlag(message[0], MessageFlag.RouteServer)).toBe(false); + expect(isRouteFlag(message[0], MessageFlag.RouteDirect)).toBe(true); + + setRouteFlag(message, MessageFlag.RouteClient); + expect(isRouteFlag(message[0], MessageFlag.RouteClient)).toBe(true); + expect(isRouteFlag(message[0], MessageFlag.RouteServer)).toBe(false); + expect(isRouteFlag(message[0], MessageFlag.RouteDirect)).toBe(false); +}); + +test('setTypeFlag', () => { + const message = new Uint8Array(1); + message[0] = MessageFlag.RouteServer | MessageFlag.ContextNew | MessageFlag.TypeAck; + expect(isRouteFlag(message[0], MessageFlag.RouteServer)).toBe(true); + expect(isContextFlag(message[0], MessageFlag.ContextNew)).toBe(true); + expect(isTypeFlag(message[0], MessageFlag.TypeAck)).toBe(true); + + setTypeFlag(message, MessageFlag.TypeChunk); + expect(isTypeFlag(message[0], MessageFlag.TypeChunk)).toBe(true); +}); + +test('action', () => { + class Controller { + @rpc.action() + myAction() { + + } + } + + const module = new InjectorModule([ + Controller, + ]); + + const actions2 = getActions(Controller); +}); + +test('dispatcher', () => { + const dispatcher = new ContextDispatcher; + + let called = 0; + const ids: number[] = []; + for (let i = 0; i < 100000; i++) { + const id = dispatcher.create(() => { + called++; + }); + ids.push(id); + } + + const message = new Uint8Array; + for (const id of ids) { + dispatcher.dispatch(id, message); + } + expect(called).toBe(100000); +}); + +test('uint16', () => { + const message = Buffer.allocUnsafe(4); + writeUint16LE(message, 0, 1); + expect(readUint16LE(message, 0)).toBe(1); + + writeUint16LE(message, 0, 126); + expect(readUint16LE(message, 0)).toBe(126); + + writeUint16LE(message, 2, 20000); + expect(readUint16LE(message, 0)).toBe(126); + expect(readUint16LE(message, 2)).toBe(20000); +}); + +test('message - client - context - ack', () => { + const message = Buffer.allocUnsafe(32); + message[0] = MessageFlag.RouteClient | MessageFlag.ContextExisting | MessageFlag.TypeAck; + writeContext(message, 1); + + expect(isContextFlag(message[0], MessageFlag.ContextExisting)).toBe(true); + expect(getContextId(message)).toBe(1); + expect(isTypeFlag(message[0], MessageFlag.TypeAck)).toBe(true); +}); + +test('message - client - new context - ack', () => { + const message = Buffer.allocUnsafe(32); + message[0] = MessageFlag.RouteClient | MessageFlag.ContextNew | MessageFlag.TypeAck; + + expect(isContextFlag(message[0], MessageFlag.ContextNew)).toBe(true); + expect(isTypeFlag(message[0], MessageFlag.TypeAck)).toBe(true); +}); + +test('message - client - end context - ack', () => { + const message = Buffer.allocUnsafe(32); + message[0] = MessageFlag.RouteClient | MessageFlag.ContextEnd | MessageFlag.TypeAck; + writeContext(message, 1); + + expect(isContextFlag(message[0], MessageFlag.ContextEnd)).toBe(true); + expect(getContextId(message)).toBe(1); + expect(isTypeFlag(message[0], MessageFlag.TypeAck)).toBe(true); +}); + +test('message - client - no context - ack', () => { + const message = Buffer.allocUnsafe(32); + message[0] = MessageFlag.RouteClient | MessageFlag.ContextNone | MessageFlag.TypeAck; + + expect(isContextFlag(message[0], MessageFlag.ContextNone)).toBe(true); + expect(isTypeFlag(message[0], MessageFlag.TypeAck)).toBe(true); +}); + +test('message - client - context - action', () => { + const message = Buffer.allocUnsafe(32); + message[0] = MessageFlag.RouteClient | MessageFlag.ContextExisting | MessageFlag.TypeAction; + writeContext(message, 1); + writeAction(message, RpcAction.Ping); + + expect(isContextFlag(message[0], MessageFlag.ContextExisting)).toBe(true); + expect(getContextId(message)).toBe(1); + expect(isTypeFlag(message[0], MessageFlag.TypeAction)).toBe(true); + expect(getAction(message)).toBe(RpcAction.Ping); +}); + +test('message - client - no context - action', () => { + const message = Buffer.allocUnsafe(32); + message[0] = MessageFlag.RouteClient | MessageFlag.ContextNone | MessageFlag.TypeAction; + writeAction(message, RpcAction.Ping); + + expect(isContextFlag(message[0], MessageFlag.ContextNone)).toBe(true); + expect(isTypeFlag(message[0], MessageFlag.TypeAction)).toBe(true); + expect(getAction(message)).toBe(RpcAction.Ping); +}); + +// with RouteDirect +const src = getRandomAddress(); +const dst = getRandomAddress(); +const port = 67; + +test('message - direct route - context - ack', () => { + const message = Buffer.allocUnsafe(255); + message[0] = MessageFlag.RouteDirect | MessageFlag.ContextExisting | MessageFlag.TypeAck; + writeDirectRoute(message, src, dst, port); + writeContext(message, 1); + + expect(isContextFlag(message[0], MessageFlag.ContextExisting)).toBe(true); + expect(getContextId(message)).toBe(1); + expect(Buffer.compare(getRouteDirectSrc(message), src)).toBe(0); + expect(Buffer.compare(getRouteDirectDst(message), dst)).toBe(0); + expect(getRouteDirectPort(message)).toBe(port); + expect(isTypeFlag(message[0], MessageFlag.TypeAck)).toBe(true); +}); + +test('message - direct route - no context - ack', () => { + const message = Buffer.allocUnsafe(255); + message[0] = MessageFlag.RouteDirect | MessageFlag.ContextNone | MessageFlag.TypeAck; + writeDirectRoute(message, src, dst, port); + + expect(isContextFlag(message[0], MessageFlag.ContextNone)).toBe(true); + expect(Buffer.compare(getRouteDirectSrc(message), src)).toBe(0); + expect(Buffer.compare(getRouteDirectDst(message), dst)).toBe(0); + expect(getRouteDirectPort(message)).toBe(port); + expect(isTypeFlag(message[0], MessageFlag.TypeAck)).toBe(true); +}); + +test('message - direct route - context - action', () => { + const message = Buffer.allocUnsafe(255); + message[0] = MessageFlag.RouteDirect | MessageFlag.ContextExisting | MessageFlag.TypeAction; + writeDirectRoute(message, src, dst, port); + writeContext(message, 1); + writeAction(message, RpcAction.Ping); + + expect(isContextFlag(message[0], MessageFlag.ContextExisting)).toBe(true); + expect(getContextId(message)).toBe(1); + expect(Buffer.compare(getRouteDirectSrc(message), src)).toBe(0); + expect(Buffer.compare(getRouteDirectDst(message), dst)).toBe(0); + expect(getRouteDirectPort(message)).toBe(port); + expect(isTypeFlag(message[0], MessageFlag.TypeAction)).toBe(true); + expect(getAction(message)).toBe(RpcAction.Ping); +}); + +test('message - direct route - no context - action', () => { + const message = Buffer.allocUnsafe(255); + message[0] = MessageFlag.RouteDirect | MessageFlag.ContextNone | MessageFlag.TypeAction; + writeDirectRoute(message, src, dst, port); + writeAction(message, RpcAction.Ping); + + expect(isContextFlag(message[0], MessageFlag.ContextNone)).toBe(true); + expect(Buffer.compare(getRouteDirectSrc(message), src)).toBe(0); + expect(Buffer.compare(getRouteDirectDst(message), dst)).toBe(0); + expect(getRouteDirectPort(message)).toBe(port); + expect(isTypeFlag(message[0], MessageFlag.TypeAction)).toBe(true); + expect(getAction(message)).toBe(RpcAction.Ping); +}); diff --git a/packages/rpc/tests/rpc.spec.ts b/packages/rpc/tests/rpc.spec.ts index fff0c7569..7f70470e3 100644 --- a/packages/rpc/tests/rpc.spec.ts +++ b/packages/rpc/tests/rpc.spec.ts @@ -1,25 +1,9 @@ import { expect, test } from '@jest/globals'; import { DirectClient } from '../src/client/client-direct.js'; import { rpc } from '../src/decorators.js'; -import { - createRpcCompositeMessage, - createRpcCompositeMessageSourceDest, - createRpcMessage, - createRpcMessagePeer, - createRpcMessageSourceDest, - readBinaryRpcMessage, - readUint32LE, - RpcBinaryMessageReader, - RpcMessage, - RpcMessageRouteType, - serializeBinaryRpcMessage, -} from '../src/protocol.js'; +import { readUint32LE } from '../src/protocol.js'; import { RpcKernel } from '../src/server/kernel.js'; -import { RpcTypes } from '../src/model.js'; import { Writer } from '@deepkit/bson'; -import { typeOf } from '@deepkit/type'; -import { RpcBinaryWriter, TransportBinaryMessageChunkWriter, TransportOptions } from '../src/transport.js'; -import { Progress } from '../src/progress.js'; import { RpcKernelSecurity } from '../src/server/security.js'; test('readUint32LE', () => { @@ -45,162 +29,153 @@ test('protocol basics', () => { name: string; } - { - const message = createRpcMessage(1024, 123); - const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); - expect(parsed.id).toBe(1024); - expect(parsed.type).toBe(123); - expect(parsed.composite).toBe(false); - expect(parsed.routeType).toBe(RpcMessageRouteType.client); - expect(parsed.bodySize).toBe(0); - expect(() => parsed.parseBody()).toThrowError('no body'); - } - - { - const message = createRpcMessage(1024, 130, { name: 'foo' }); - const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); - expect(parsed.id).toBe(1024); - expect(parsed.type).toBe(130); - expect(parsed.composite).toBe(false); - expect(parsed.routeType).toBe(RpcMessageRouteType.client); - const body = parsed.parseBody(); - expect(body.name).toBe('foo'); - } - - { - const message = createRpcMessage(1024, 130, { name: 'foo' }, RpcMessageRouteType.server); - const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); - expect(parsed.id).toBe(1024); - expect(parsed.type).toBe(130); - expect(parsed.composite).toBe(false); - expect(parsed.routeType).toBe(RpcMessageRouteType.server); - } - - { - const peerSource = Buffer.alloc(16); - peerSource[0] = 22; - const message = createRpcMessagePeer(1024, 130, peerSource, 'myPeer', { name: 'foo' }); - const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); - expect(parsed.id).toBe(1024); - expect(parsed.type).toBe(130); - expect(parsed.composite).toBe(false); - expect(parsed.getPeerId()).toBe('myPeer'); - - const body = parsed.parseBody(); - expect(body.name).toBe('foo'); - } - - { - const source = Buffer.alloc(16); - source[0] = 16; - const destination = Buffer.alloc(16); - destination[0] = 20; - const message = createRpcMessageSourceDest(1024, 130, source, destination, { name: 'foo' }); - const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); - expect(parsed.id).toBe(1024); - expect(parsed.type).toBe(130); - expect(parsed.composite).toBe(false); - expect(parsed.getSource()[0]).toBe(16); - expect(parsed.getDestination()[0]).toBe(20); - const body = parsed.parseBody(); - expect(body.name).toBe('foo'); - } + // { + // const message = createRpcMessage(MessageFlag.RouteClient, MessageFlag.ContextExisting, MessageFlag.TypeOther, { contextId: 1024, type: 123 }); + // const parsed = readBinaryRpcMessage(message); + // expect(parsed.contextId).toBe(1024); + // expect(parsed.type).toBe(123); + // expect(parsed.routeType).toBe(MessageFlag.RouteClient); + // expect(parsed.bodySize).toBe(0); + // expect(() => parsed.parseBody()).toThrowError('no body'); + // } + + // { + // const message = createRpcMessage(1024, 130, { name: 'foo' }); + // const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); + // expect(parsed.id).toBe(1024); + // expect(parsed.type).toBe(130); + // expect(parsed.routeType).toBe(MessageFlag.RouteClient); + // const body = parsed.parseBody(); + // expect(body.name).toBe('foo'); + // } + // + // { + // const message = createRpcMessage(1024, 130, { name: 'foo' }, MessageFlag.RouteServer); + // const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); + // expect(parsed.id).toBe(1024); + // expect(parsed.type).toBe(130); + // expect(parsed.routeType).toBe(MessageFlag.RouteServer); + // } + // + // { + // const peerSource = Buffer.alloc(16); + // peerSource[0] = 22; + // const message = createRpcMessagePeer(1024, 130, peerSource, 'myPeer', { name: 'foo' }); + // const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); + // expect(parsed.id).toBe(1024); + // expect(parsed.type).toBe(130); + // expect(parsed.getPeerId()).toBe('myPeer'); + // + // const body = parsed.parseBody(); + // expect(body.name).toBe('foo'); + // } + // + // { + // const source = Buffer.alloc(16); + // source[0] = 16; + // const destination = Buffer.alloc(16); + // destination[0] = 20; + // const message = createRpcMessageSourceDest(1024, 130, source, destination, { name: 'foo' }); + // const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); + // expect(parsed.id).toBe(1024); + // expect(parsed.type).toBe(130); + // expect(parsed.getSource()[0]).toBe(16); + // expect(parsed.getDestination()[0]).toBe(20); + // const body = parsed.parseBody(); + // expect(body.name).toBe('foo'); + // } }); -test('protocol composite', () => { - interface schema { - name: string; - } - - { - const message = createRpcCompositeMessage(1024, 33, [{ type: 4, schema: typeOf(), body: { name: 'foo' } }]); - - const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); - expect(parsed.id).toBe(1024); - expect(parsed.type).toBe(33); - expect(parsed.composite).toBe(true); - expect(parsed.routeType).toBe(RpcMessageRouteType.client); - expect(() => parsed.parseBody()).toThrow('Composite message can not be read directly'); - - const messages = parsed.getBodies(); - expect(messages.length).toBe(1); - expect(messages[0].type).toBe(4); - expect(messages[0].bodySize).toBeGreaterThan(10); - expect(messages[0].parseBody().name).toBe('foo'); - } - - - { - const message = createRpcCompositeMessage(1024, 5, [{ type: 4 }, { type: 5, schema: typeOf(), body: { name: 'foo' } }]); - - const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); - expect(parsed.id).toBe(1024); - expect(parsed.type).toBe(5); - expect(parsed.composite).toBe(true); - expect(parsed.routeType).toBe(RpcMessageRouteType.client); - expect(() => parsed.parseBody()).toThrow('Composite message can not be read directly'); - - const messages = parsed.getBodies(); - expect(messages.length).toBe(2); - expect(messages[0].type).toBe(4); - expect(messages[0].bodySize).toBe(0); - expect(() => messages[0].parseBody()).toThrow('no body'); - - expect(messages[1].type).toBe(5); - expect(messages[1].bodySize).toBeGreaterThan(10); - expect(messages[1].parseBody().name).toBe('foo'); - } - - { - const message = createRpcCompositeMessage(1024, 6, [{ type: 4, schema: typeOf(), body: { name: 'foo' } }, { - type: 12, - schema: typeOf(), - body: { name: 'bar' }, - }]); - - const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); - expect(parsed.id).toBe(1024); - expect(parsed.type).toBe(6); - expect(parsed.composite).toBe(true); - expect(parsed.routeType).toBe(RpcMessageRouteType.client); - expect(() => parsed.parseBody()).toThrow('Composite message can not be read directly'); - - const messages = parsed.getBodies(); - expect(messages.length).toBe(2); - expect(messages[0].type).toBe(4); - expect(messages[0].parseBody().name).toBe('foo'); - expect(messages[1].type).toBe(12); - expect(messages[1].parseBody().name).toBe('bar'); - } - - { - const source = Buffer.alloc(16); - source[0] = 16; - const destination = Buffer.alloc(16); - destination[0] = 20; - const message = createRpcCompositeMessageSourceDest(1024, source, destination, 55, [{ - type: 4, - schema: typeOf(), - body: { name: 'foo' }, - }, { type: 12, schema: typeOf(), body: { name: 'bar' } }]); - - const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); - expect(parsed.id).toBe(1024); - expect(parsed.type).toBe(55); - expect(parsed.composite).toBe(true); - expect(parsed.routeType).toBe(RpcMessageRouteType.sourceDest); - expect(parsed.getSource()[0]).toBe(16); - expect(parsed.getDestination()[0]).toBe(20); - expect(() => parsed.parseBody()).toThrow('Composite message can not be read directly'); - - const messages = parsed.getBodies(); - expect(messages.length).toBe(2); - expect(messages[0].type).toBe(4); - expect(messages[0].parseBody().name).toBe('foo'); - expect(messages[1].type).toBe(12); - expect(messages[1].parseBody().name).toBe('bar'); - } -}); +// test('protocol composite', () => { +// interface schema { +// name: string; +// } +// +// { +// const message = createRpcCompositeMessage(1024, 33, [{ type: 4, schema: typeOf(), body: { name: 'foo' } }]); +// +// const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); +// expect(parsed.id).toBe(1024); +// expect(parsed.type).toBe(33); +// expect(parsed.routeType).toBe(MessageFlag.RouteClient); +// expect(() => parsed.parseBody()).toThrow('Composite message can not be read directly'); +// +// const messages = parsed.getBodies(); +// expect(messages.length).toBe(1); +// expect(messages[0].type).toBe(4); +// expect(messages[0].bodySize).toBeGreaterThan(10); +// expect(messages[0].parseBody().name).toBe('foo'); +// } +// +// +// { +// const message = createRpcCompositeMessage(1024, 5, [{ type: 4 }, { type: 5, schema: typeOf(), body: { name: 'foo' } }]); +// +// const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); +// expect(parsed.id).toBe(1024); +// expect(parsed.type).toBe(5); +// expect(parsed.routeType).toBe(MessageFlag.RouteClient); +// expect(() => parsed.parseBody()).toThrow('Composite message can not be read directly'); +// +// const messages = parsed.getBodies(); +// expect(messages.length).toBe(2); +// expect(messages[0].type).toBe(4); +// expect(messages[0].bodySize).toBe(0); +// expect(() => messages[0].parseBody()).toThrow('no body'); +// +// expect(messages[1].type).toBe(5); +// expect(messages[1].bodySize).toBeGreaterThan(10); +// expect(messages[1].parseBody().name).toBe('foo'); +// } +// +// { +// const message = createRpcCompositeMessage(1024, 6, [{ type: 4, schema: typeOf(), body: { name: 'foo' } }, { +// type: 12, +// schema: typeOf(), +// body: { name: 'bar' }, +// }]); +// +// const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); +// expect(parsed.id).toBe(1024); +// expect(parsed.type).toBe(6); +// expect(parsed.routeType).toBe(MessageFlag.RouteClient); +// expect(() => parsed.parseBody()).toThrow('Composite message can not be read directly'); +// +// const messages = parsed.getBodies(); +// expect(messages.length).toBe(2); +// expect(messages[0].type).toBe(4); +// expect(messages[0].parseBody().name).toBe('foo'); +// expect(messages[1].type).toBe(12); +// expect(messages[1].parseBody().name).toBe('bar'); +// } +// +// { +// const source = Buffer.alloc(16); +// source[0] = 16; +// const destination = Buffer.alloc(16); +// destination[0] = 20; +// const message = createRpcCompositeMessageSourceDest(1024, source, destination, 55, [{ +// type: 4, +// schema: typeOf(), +// body: { name: 'foo' }, +// }, { type: 12, schema: typeOf(), body: { name: 'bar' } }]); +// +// const parsed = readBinaryRpcMessage(serializeBinaryRpcMessage(message)); +// expect(parsed.id).toBe(1024); +// expect(parsed.type).toBe(55); +// expect(parsed.routeType).toBe(RpcMessageRouteType.sourceDest); +// expect(parsed.getSource()[0]).toBe(16); +// expect(parsed.getDestination()[0]).toBe(20); +// expect(() => parsed.parseBody()).toThrow('Composite message can not be read directly'); +// +// const messages = parsed.getBodies(); +// expect(messages.length).toBe(2); +// expect(messages[0].type).toBe(4); +// expect(messages[0].parseBody().name).toBe('foo'); +// expect(messages[1].type).toBe(12); +// expect(messages[1].parseBody().name).toBe('bar'); +// } +// }); test('rpc kernel handshake', async () => { const kernel = new RpcKernel(); @@ -243,10 +218,12 @@ test('rpc peer', async () => { async isAllowedToSendToPeer() { return true; } + async isAllowedToRegisterAsPeer() { return true; } } + const kernel = new RpcKernel([ { provide: RpcKernelSecurity, useClass: MyRpcSecurity, scope: 'rpc' }, ]); @@ -270,79 +247,79 @@ test('rpc peer', async () => { expect(res).toBe('bar'); }); -test('message chunks', async () => { - const messages: RpcMessage[] = []; - const reader = new RpcBinaryMessageReader(v => messages.push(v)); - - interface schema { - v: string; - } - - const bigString = 'x'.repeat(1_000_000); //1mb - - const buffers: Uint8Array[] = []; - const binaryWriter: RpcBinaryWriter = (b) => { - buffers.push(b); - reader.feed(serializeBinaryRpcMessage(createRpcMessage(2, RpcTypes.ChunkAck))); //confirm chunk, this is done automatically in the kernel - reader.feed(b); //echo back - }; - const writer = new TransportBinaryMessageChunkWriter(reader, new TransportOptions()); - - const message = serializeBinaryRpcMessage(createRpcMessage(2, RpcTypes.ResponseActionSimple, { v: bigString })); - await writer.writeFull(binaryWriter, message); - expect(buffers.length).toBe(11); //total size is 1_000_025, chunk is 100k, so we have 11 packages - - expect(readBinaryRpcMessage(buffers[0]).id).toBe(2); - expect(readBinaryRpcMessage(buffers[0]).type).toBe(RpcTypes.Chunk); - - expect(readBinaryRpcMessage(buffers[10]).id).toBe(2); - expect(readBinaryRpcMessage(buffers[10]).type).toBe(RpcTypes.Chunk); - - expect(messages.length).toBe(1); - const lastReceivedMessage = messages[0]; - expect(lastReceivedMessage.id).toBe(2); - expect(lastReceivedMessage.type).toBe(RpcTypes.ResponseActionSimple); - - const body = lastReceivedMessage.parseBody(); - expect(body.v).toBe(bigString); -}); - -test('message progress', async () => { - const messages: RpcMessage[] = []; - const reader = new RpcBinaryMessageReader(v => messages.push(v)); - - interface schema { - v: string; - } - - const bigString = 'x'.repeat(1_000_000); //1mb - - const binaryWriter: RpcBinaryWriter = (b) => { - reader.feed(serializeBinaryRpcMessage(createRpcMessage(2, RpcTypes.ChunkAck))); //confirm chunk, this is done automatically in the kernel - reader.feed(b); //echo - }; - const writer = new TransportBinaryMessageChunkWriter(reader, new TransportOptions); - - const message = serializeBinaryRpcMessage(createRpcMessage(2, RpcTypes.ResponseActionSimple, { v: bigString })); - const progress = new Progress(); - reader.registerProgress(2, progress.download); - await writer.writeFull(binaryWriter, message, progress.upload); - - await progress.upload.finished; - expect(progress.upload.done).toBe(true); - expect(progress.upload.isStopped).toBe(true); - expect(progress.upload.current).toBe(1_000_025); - expect(progress.upload.total).toBe(1_000_025); - expect(progress.upload.stats).toBe(11); //since 11 packages - - await progress.download.finished; - expect(progress.download.done).toBe(true); - expect(progress.download.isStopped).toBe(true); - expect(progress.download.current).toBe(1_000_025); - expect(progress.download.total).toBe(1_000_025); - expect(progress.download.stats).toBe(11); //since 11 packages - - const lastReceivedMessage = messages[0]; - const body = lastReceivedMessage.parseBody(); - expect(body.v).toBe(bigString); -}); +// test('message chunks', async () => { +// const messages: RpcMessage[] = []; +// const reader = new RpcBinaryMessageReader(v => messages.push(v)); +// +// interface schema { +// v: string; +// } +// +// const bigString = 'x'.repeat(1_000_000); //1mb +// +// const buffers: Uint8Array[] = []; +// const binaryWriter: RpcBinaryWriter = (b) => { +// buffers.push(b); +// reader.feed(serializeBinaryRpcMessage(createRpcMessage(2, RpcTypes.ChunkAck))); //confirm chunk, this is done automatically in the kernel +// reader.feed(b); //echo back +// }; +// const writer = new TransportBinaryMessageChunkWriter(reader, new TransportOptions()); +// +// const message = serializeBinaryRpcMessage(createRpcMessage(2, RpcTypes.ResponseActionSimple, { v: bigString })); +// await writer.writeFull(binaryWriter, message); +// expect(buffers.length).toBe(11); //total size is 1_000_025, chunk is 100k, so we have 11 packages +// +// expect(readBinaryRpcMessage(buffers[0]).id).toBe(2); +// expect(readBinaryRpcMessage(buffers[0]).type).toBe(RpcTypes.Chunk); +// +// expect(readBinaryRpcMessage(buffers[10]).id).toBe(2); +// expect(readBinaryRpcMessage(buffers[10]).type).toBe(RpcTypes.Chunk); +// +// expect(messages.length).toBe(1); +// const lastReceivedMessage = messages[0]; +// expect(lastReceivedMessage.contextId).toBe(2); +// expect(lastReceivedMessage.type).toBe(RpcTypes.ResponseActionSimple); +// +// const body = lastReceivedMessage.parseBody(); +// expect(body.v).toBe(bigString); +// }); + +// test('message progress', async () => { +// const messages: RpcMessage[] = []; +// const reader = new RpcBinaryMessageReader(v => messages.push(v)); +// +// interface schema { +// v: string; +// } +// +// const bigString = 'x'.repeat(1_000_000); //1mb +// +// const binaryWriter: RpcBinaryWriter = (b) => { +// reader.feed(serializeBinaryRpcMessage(createRpcMessage(2, RpcTypes.ChunkAck))); //confirm chunk, this is done automatically in the kernel +// reader.feed(b); //echo +// }; +// const writer = new TransportBinaryMessageChunkWriter(reader, new TransportOptions); +// +// const message = serializeBinaryRpcMessage(createRpcMessage(2, RpcTypes.ResponseActionSimple, { v: bigString })); +// const progress = new Progress(); +// reader.registerProgress(2, progress.download); +// await writer.writeFull(binaryWriter, message, progress.upload); +// +// await progress.upload.finished; +// expect(progress.upload.done).toBe(true); +// expect(progress.upload.isStopped).toBe(true); +// expect(progress.upload.current).toBe(1_000_025); +// expect(progress.upload.total).toBe(1_000_025); +// expect(progress.upload.stats).toBe(11); //since 11 packages +// +// await progress.download.finished; +// expect(progress.download.done).toBe(true); +// expect(progress.download.isStopped).toBe(true); +// expect(progress.download.current).toBe(1_000_025); +// expect(progress.download.total).toBe(1_000_025); +// expect(progress.download.stats).toBe(11); //since 11 packages +// +// const lastReceivedMessage = messages[0]; +// const body = lastReceivedMessage.parseBody(); +// expect(body.v).toBe(bigString); +// }); diff --git a/packages/rpc/tsconfig.json b/packages/rpc/tsconfig.json index ea8f7c314..33638c5bb 100644 --- a/packages/rpc/tsconfig.json +++ b/packages/rpc/tsconfig.json @@ -24,9 +24,14 @@ "es2021.weakref" ] }, - "reflection": true, + "reflection": [ + "./benchmarks/*", + "./src/model.ts", + "./tests/action.spec.ts" + ], "include": [ "src", + "benchmarks", "index.ts" ], "exclude": [ diff --git a/packages/run/.npmignore b/packages/run/.npmignore new file mode 100644 index 000000000..2b29f2764 --- /dev/null +++ b/packages/run/.npmignore @@ -0,0 +1 @@ +tests diff --git a/packages/run/README.md b/packages/run/README.md new file mode 100644 index 000000000..a66d9c200 --- /dev/null +++ b/packages/run/README.md @@ -0,0 +1 @@ +# Bench diff --git a/packages/run/dist/.gitkeep b/packages/run/dist/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/run/hooks.ts b/packages/run/hooks.ts new file mode 100644 index 000000000..256ff6bc0 --- /dev/null +++ b/packages/run/hooks.ts @@ -0,0 +1,57 @@ +import { ModuleKind, readConfigFile, ScriptTarget, transpile } from 'typescript'; +import { existsSync, readFileSync } from 'fs'; +import { dirname, extname } from 'path'; +import { readFile, stat } from 'node:fs/promises'; + +let tsConfigPath = 'tsconfig.json'; +let currentPath = process.cwd(); + +while (currentPath !== '/') { + const path = `${currentPath}/tsconfig.json`; + if (existsSync(path)) { + tsConfigPath = path; + break; + } + const next = dirname(currentPath); + if (next === currentPath) break; + currentPath = next; +} + +const tsConfig = readConfigFile(tsConfigPath, (path) => readFileSync(path, 'utf8')); +const tsConfigNormalized = Object.assign({}, tsConfig?.config.compilerOptions || {}, { + module: ModuleKind.ES2022, // Keep as ESNext for ESM support + target: ScriptTarget.ES2022, // Transpile to ES2020+ for modern ESM support + configFilePath: tsConfigPath, + sourceMap: true, +}); + +async function tryResolveTs(specifier, context, nextResolve) { + if (extname(specifier) === '.js') { + const tsSpecifier = specifier.replace(/\.js$/, '.ts'); + try { + // Check if the .ts file exists before resolving + await stat(new URL(tsSpecifier, context.parentURL)); + return nextResolve(tsSpecifier, context); + } catch { + // If no .ts file is found, fall back to the default resolution + } + } + if (extname(specifier) === '.ts') { + return { url: specifier, shortCircuit: true }; + } + return nextResolve(specifier, context); +} + +export async function resolve(specifier, context, defaultResolve) { + return tryResolveTs(specifier, context, defaultResolve); +} + +export async function load(url, context, nextLoad) { + if (extname(url) === '.ts') { + const path = new URL(url).pathname; + const source = await readFile(path, 'utf8'); + const transpiled = transpile(source, tsConfigNormalized, path); + return { format: 'module', source: transpiled, shortCircuit: true }; + } + return nextLoad(url); +} diff --git a/packages/run/index.ts b/packages/run/index.ts new file mode 100644 index 000000000..7d25ceaa5 --- /dev/null +++ b/packages/run/index.ts @@ -0,0 +1,4 @@ +import { register } from 'node:module'; + +// @ts-ignore +register('./hooks.js', import.meta.url); diff --git a/packages/run/package.json b/packages/run/package.json new file mode 100644 index 000000000..e18f168f8 --- /dev/null +++ b/packages/run/package.json @@ -0,0 +1,29 @@ +{ + "name": "@deepkit/run", + "version": "1.0.3", + "description": "Deepkit Run", + "module": "./dist/esm/index.js", + "main": "./dist/cjs/index.js", + "types": "./dist/cjs/index.d.ts", + "exports": { + ".": { + "types": "./dist/cjs/index.d.ts", + "require": "./dist/cjs/index.js", + "default": "./dist/esm/index.js" + } + }, + "sideEffects": false, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "echo '{\"type\": \"module\"}' > ./dist/esm/package.json" + }, + "repository": "https://github.com/deepkit/deepkit-framework", + "author": "Marc J. Schmidt ", + "license": "MIT", + "dependencies": { + "ts-node": "^10.9.1", + "typescript": "~5.7.3" + } +} diff --git a/packages/run/tsconfig.esm.json b/packages/run/tsconfig.esm.json new file mode 100644 index 000000000..187dc34dd --- /dev/null +++ b/packages/run/tsconfig.esm.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist/esm", + "module": "ES2020" + }, + "references": [ + { + "path": "../core/tsconfig.esm.json" + }, + { + "path": "../type/tsconfig.esm.json" + } + ] +} \ No newline at end of file diff --git a/packages/run/tsconfig.json b/packages/run/tsconfig.json new file mode 100644 index 000000000..65dd35926 --- /dev/null +++ b/packages/run/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "noImplicitAny": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "target": "es2020", + "module": "CommonJS", + "esModuleInterop": true, + "outDir": "./dist/cjs", + "declaration": true, + "composite": true, + "types": [ + "node" + ] + }, + "reflection": true, + "include": [ + "index.ts", + "hooks.ts" + ], + "exclude": [ + "tests" + ], + "references": [ + ] +} diff --git a/packages/type-compiler/src/config.ts b/packages/type-compiler/src/config.ts index be3dbca9f..4e21ca555 100644 --- a/packages/type-compiler/src/config.ts +++ b/packages/type-compiler/src/config.ts @@ -304,7 +304,8 @@ export function getConfigResolver( const resolvedConfig: ResolvedConfig = { path: tsConfigPath, - compilerOptions: config.compilerOptions, + // we want to maintain options passed from tsc API (transpile, Program) + compilerOptions: Object.assign(config.compilerOptions, compilerOptions), exclude: config.exclude, reflection: config.reflection, mergeStrategy: config.mergeStrategy || defaultMergeStrategy, diff --git a/packages/type/src/core.ts b/packages/type/src/core.ts index f86e82199..710b17328 100644 --- a/packages/type/src/core.ts +++ b/packages/type/src/core.ts @@ -144,7 +144,6 @@ export function arrayBufferFrom(data: string, encoding?: string): ArrayBuffer { return nodeBufferToArrayBuffer(Buffer.from(data, encoding as any)); } - /** * Same as Buffer.from(arrayBuffer).toString(encoding), but more in line with the current API. */ diff --git a/packages/type/src/reflection/type.ts b/packages/type/src/reflection/type.ts index f6ed69f28..4860a49fc 100644 --- a/packages/type/src/reflection/type.ts +++ b/packages/type/src/reflection/type.ts @@ -552,7 +552,7 @@ export type FindType = T extends export type InlineRuntimeType | Type | number | string | boolean | bigint> = T extends ReflectionClass ? K : any; export function isType(entry: any): entry is Type { - return 'object' === typeof entry && entry.constructor === Object && 'kind' in entry && 'number' === typeof entry.kind; + return entry && 'object' === typeof entry && 'number' === typeof entry.kind; } export function isBinary(type: Type): boolean { diff --git a/tsconfig.esm.json b/tsconfig.esm.json index 2198e969f..a16e63401 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -1,6 +1,12 @@ { "files": [], "references": [ + { + "path": "packages/run/tsconfig.esm.json" + }, + { + "path": "packages/bench/tsconfig.esm.json" + }, { "path": "packages/angular-ssr/tsconfig.esm.json" }, diff --git a/tsconfig.json b/tsconfig.json index 6d31cdab3..f6c50b0ba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,15 @@ { + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + }, "files": [], "references": [ + { + "path": "packages/run/tsconfig.json" + }, + { + "path": "packages/bench/tsconfig.json" + }, { "path": "packages/angular-ssr/tsconfig.json" }, diff --git a/yarn.lock b/yarn.lock index 7f8b058f4..53802207f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2951,6 +2951,12 @@ __metadata: languageName: unknown linkType: soft +"@deepkit/bench@npm:^1.0.3, @deepkit/bench@workspace:packages/bench": + version: 0.0.0-use.local + resolution: "@deepkit/bench@workspace:packages/bench" + languageName: unknown + linkType: soft + "@deepkit/broker@npm:^1.0.1, @deepkit/broker@npm:^1.0.4, @deepkit/broker@workspace:packages/broker": version: 0.0.0-use.local resolution: "@deepkit/broker@workspace:packages/broker" @@ -3018,6 +3024,7 @@ __metadata: version: 0.0.0-use.local resolution: "@deepkit/core@workspace:packages/core" dependencies: + "@deepkit/bench": "npm:^1.0.3" "@types/dot-prop": "npm:~4.2.0" dot-prop: "npm:^5.1.1" to-fast-properties: "npm:^3.0.1" @@ -3458,6 +3465,7 @@ __metadata: version: 0.0.0-use.local resolution: "@deepkit/injector@workspace:packages/injector" dependencies: + "@deepkit/bench": "npm:^1.0.3" "@deepkit/core": "npm:^1.0.3" "@deepkit/type": "npm:^1.0.3" benchmark: "npm:^2.1.4" @@ -3701,6 +3709,7 @@ __metadata: version: 0.0.0-use.local resolution: "@deepkit/rpc@workspace:packages/rpc" dependencies: + "@deepkit/bench": "npm:^1.0.3" "@deepkit/bson": "npm:^1.0.3" "@deepkit/core": "npm:^1.0.3" "@deepkit/core-rxjs": "npm:^1.0.3" @@ -3723,6 +3732,15 @@ __metadata: languageName: unknown linkType: soft +"@deepkit/run@workspace:packages/run": + version: 0.0.0-use.local + resolution: "@deepkit/run@workspace:packages/run" + dependencies: + ts-node: "npm:^10.9.1" + typescript: "npm:~5.7.3" + languageName: unknown + linkType: soft + "@deepkit/skeletion@workspace:packages/skeleton": version: 0.0.0-use.local resolution: "@deepkit/skeletion@workspace:packages/skeleton"