@nmtjs/protocol 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE.md +7 -0
  2. package/README.md +9 -0
  3. package/dist/client/events.js +41 -0
  4. package/dist/client/events.js.map +1 -0
  5. package/dist/client/format.js +2 -0
  6. package/dist/client/format.js.map +1 -0
  7. package/dist/client/index.js +4 -0
  8. package/dist/client/index.js.map +1 -0
  9. package/dist/client/protocol.js +311 -0
  10. package/dist/client/protocol.js.map +1 -0
  11. package/dist/client/stream.js +99 -0
  12. package/dist/client/stream.js.map +1 -0
  13. package/dist/common/binary.js +25 -0
  14. package/dist/common/binary.js.map +1 -0
  15. package/dist/common/blob.js +42 -0
  16. package/dist/common/blob.js.map +1 -0
  17. package/dist/common/enums.js +44 -0
  18. package/dist/common/enums.js.map +1 -0
  19. package/dist/common/index.js +4 -0
  20. package/dist/common/index.js.map +1 -0
  21. package/dist/common/types.js +1 -0
  22. package/dist/common/types.js.map +1 -0
  23. package/dist/server/api.js +1 -0
  24. package/dist/server/api.js.map +1 -0
  25. package/dist/server/connection.js +21 -0
  26. package/dist/server/connection.js.map +1 -0
  27. package/dist/server/constants.js +1 -0
  28. package/dist/server/constants.js.map +1 -0
  29. package/dist/server/format.js +48 -0
  30. package/dist/server/format.js.map +1 -0
  31. package/dist/server/index.js +10 -0
  32. package/dist/server/index.js.map +1 -0
  33. package/dist/server/injectables.js +22 -0
  34. package/dist/server/injectables.js.map +1 -0
  35. package/dist/server/protocol.js +293 -0
  36. package/dist/server/protocol.js.map +1 -0
  37. package/dist/server/registry.js +19 -0
  38. package/dist/server/registry.js.map +1 -0
  39. package/dist/server/stream.js +30 -0
  40. package/dist/server/stream.js.map +1 -0
  41. package/dist/server/transport.js +7 -0
  42. package/dist/server/transport.js.map +1 -0
  43. package/dist/server/utils.js +10 -0
  44. package/dist/server/utils.js.map +1 -0
  45. package/lib/client/events.ts +66 -0
  46. package/lib/client/format.ts +22 -0
  47. package/lib/client/index.ts +4 -0
  48. package/lib/client/protocol.ts +440 -0
  49. package/lib/client/stream.ts +116 -0
  50. package/lib/common/binary.ts +60 -0
  51. package/lib/common/blob.ts +70 -0
  52. package/lib/common/enums.ts +46 -0
  53. package/lib/common/index.ts +4 -0
  54. package/lib/common/types.ts +64 -0
  55. package/lib/server/api.ts +47 -0
  56. package/lib/server/connection.ts +57 -0
  57. package/lib/server/constants.ts +4 -0
  58. package/lib/server/format.ts +107 -0
  59. package/lib/server/index.ts +10 -0
  60. package/lib/server/injectables.ts +51 -0
  61. package/lib/server/protocol.ts +422 -0
  62. package/lib/server/registry.ts +24 -0
  63. package/lib/server/stream.ts +43 -0
  64. package/lib/server/transport.ts +36 -0
  65. package/lib/server/utils.ts +22 -0
  66. package/package.json +39 -0
  67. package/tsconfig.json +3 -0
@@ -0,0 +1,60 @@
1
+ // TODO: get rid of lib DOM somehow...
2
+ /// <reference lib="dom" />
3
+
4
+ const utf8decoder = new TextDecoder()
5
+ const utf8encoder = new TextEncoder()
6
+
7
+ export type BinaryTypes = {
8
+ Int8: number
9
+ Int16: number
10
+ Int32: number
11
+ Uint8: number
12
+ Uint16: number
13
+ Uint32: number
14
+ Float32: number
15
+ Float64: number
16
+ BigInt64: bigint
17
+ BigUint64: bigint
18
+ }
19
+
20
+ export const encodeNumber = <T extends keyof BinaryTypes>(
21
+ value: BinaryTypes[T],
22
+ type: T,
23
+ littleEndian = false,
24
+ ) => {
25
+ const bytesNeeded = globalThis[`${type}Array`].BYTES_PER_ELEMENT
26
+ const ab = new ArrayBuffer(bytesNeeded)
27
+ const dv = new DataView(ab)
28
+ dv[`set${type}`](0, value as never, littleEndian)
29
+ return ab
30
+ }
31
+
32
+ export const decodeNumber = <T extends keyof BinaryTypes>(
33
+ buffer: ArrayBuffer,
34
+ type: T,
35
+ offset = 0,
36
+ littleEndian = false,
37
+ ): BinaryTypes[T] => {
38
+ const view = new DataView(buffer)
39
+ return view[`get${type}`](offset, littleEndian) as BinaryTypes[T]
40
+ }
41
+
42
+ export const encodeText = (text: string) =>
43
+ new Uint8Array(utf8encoder.encode(text)).buffer as ArrayBuffer
44
+
45
+ export const decodeText = (buffer: Parameters<typeof utf8decoder.decode>[0]) =>
46
+ utf8decoder.decode(buffer)
47
+
48
+ export const concat = (...buffers: ArrayBuffer[]) => {
49
+ const totalLength = buffers.reduce(
50
+ (acc, buffer) => acc + buffer.byteLength,
51
+ 0,
52
+ )
53
+ const view = new Uint8Array(totalLength)
54
+ let offset = 0
55
+ for (const buffer of buffers) {
56
+ view.set(new Uint8Array(buffer), offset)
57
+ offset += buffer.byteLength
58
+ }
59
+ return view.buffer
60
+ }
@@ -0,0 +1,70 @@
1
+ export type ProtocolBlobMetadata = {
2
+ type: string
3
+ size: number
4
+ filename?: string
5
+ }
6
+
7
+ export interface ProtocolBlobInterface {
8
+ readonly metadata: ProtocolBlobMetadata
9
+ }
10
+
11
+ export class ProtocolBlob implements ProtocolBlobInterface {
12
+ public readonly metadata: ProtocolBlobMetadata
13
+ public readonly source: any
14
+
15
+ constructor(
16
+ source: any,
17
+ size = -1,
18
+ type = 'application/octet-stream',
19
+ filename?: string,
20
+ ) {
21
+ if (size < -1 || size === 0) throw new Error('Blob size is invalid')
22
+
23
+ this.source = source
24
+ this.metadata = {
25
+ size,
26
+ type,
27
+ filename,
28
+ }
29
+ }
30
+
31
+ static from(
32
+ source: any,
33
+ metadata: {
34
+ size?: number
35
+ type?: string
36
+ filename?: string
37
+ } = {},
38
+ ) {
39
+ let _source: any = undefined
40
+
41
+ if (source instanceof globalThis.ReadableStream) {
42
+ _source = source
43
+ } else if ('File' in globalThis && source instanceof globalThis.File) {
44
+ _source = source.stream()
45
+ metadata.size = source.size
46
+ metadata.filename = source.name
47
+ } else if (source instanceof globalThis.Blob) {
48
+ _source = source.stream()
49
+ metadata.size = source.size
50
+ } else if (typeof source === 'string') {
51
+ const blob = new Blob([source])
52
+ _source = blob.stream()
53
+ metadata.size = blob.size
54
+ metadata.type = metadata.type || 'text/plain'
55
+ } else if (source instanceof globalThis.ArrayBuffer) {
56
+ const blob = new Blob([source])
57
+ _source = blob.stream()
58
+ metadata.size = blob.size
59
+ } else {
60
+ _source = source
61
+ }
62
+
63
+ return new ProtocolBlob(
64
+ _source,
65
+ metadata.size,
66
+ metadata.type,
67
+ metadata.filename,
68
+ )
69
+ }
70
+ }
@@ -0,0 +1,46 @@
1
+ export enum ClientMessageType {
2
+ Rpc = 10,
3
+ RpcAbort = 11,
4
+ RpcStreamAbort = 12,
5
+
6
+ ClientStreamPush = 20,
7
+ ClientStreamEnd = 21,
8
+ ClientStreamAbort = 22,
9
+ ServerStreamAbort = 23,
10
+ ServerStreamPull = 24,
11
+ }
12
+
13
+ export enum ServerMessageType {
14
+ Event = 1,
15
+
16
+ RpcResponse = 10,
17
+ RpcStreamResponse = 11,
18
+ RpcStreamChunk = 12,
19
+ RpcStreamAbort = 13,
20
+
21
+ ServerStreamPush = 20,
22
+ ServerStreamEnd = 21,
23
+ ServerStreamAbort = 22,
24
+ ClientStreamAbort = 23,
25
+ ClientStreamPull = 24,
26
+ }
27
+
28
+ export enum TransportType {
29
+ Bidirectional = 'Bidirectional',
30
+ Unidirectional = 'Unidirectional',
31
+ }
32
+
33
+ export enum ErrorCode {
34
+ ValidationError = 'ValidationError',
35
+ BadRequest = 'BadRequest',
36
+ NotFound = 'NotFound',
37
+ Forbidden = 'Forbidden',
38
+ Unauthorized = 'Unauthorized',
39
+ InternalServerError = 'InternalServerError',
40
+ NotAcceptable = 'NotAcceptable',
41
+ RequestTimeout = 'RequestTimeout',
42
+ GatewayTimeout = 'GatewayTimeout',
43
+ ServiceUnavailable = 'ServiceUnavailable',
44
+ ClientRequestError = 'ClientRequestError',
45
+ ConnectionError = 'ConnectionError',
46
+ }
@@ -0,0 +1,4 @@
1
+ export * from './types.ts'
2
+ export * from './enums.ts'
3
+ export * from './binary.ts'
4
+ export * from './blob.ts'
@@ -0,0 +1,64 @@
1
+ import type { ProtocolServerBlobStream } from '../client/stream.ts'
2
+ import type {
3
+ ProtocolBlob,
4
+ ProtocolBlobInterface,
5
+ ProtocolBlobMetadata,
6
+ } from './blob.ts'
7
+
8
+ export type ProtocolRPC = {
9
+ callId: number
10
+ namespace: string
11
+ procedure: string
12
+ payload: any
13
+ }
14
+
15
+ export type ProtocolRPCResponse =
16
+ | {
17
+ callId: number
18
+ error: any
19
+ payload?: never
20
+ }
21
+ | {
22
+ callId: number
23
+ payload: any
24
+ error?: never
25
+ }
26
+
27
+ export interface EncodeRPCContext {
28
+ getStream: (id: number) => any
29
+ addStream: (blob: ProtocolBlob) => {
30
+ id: number
31
+ metadata: ProtocolBlobMetadata
32
+ }
33
+ }
34
+
35
+ export interface DecodeRPCContext {
36
+ getStream: (id: number) => any
37
+ addStream: (id: number, metadata: ProtocolBlobMetadata) => any
38
+ }
39
+
40
+ export interface BaseClientDecoder {
41
+ decode(buffer: ArrayBuffer): any
42
+ decodeRPC(buffer: ArrayBuffer, context: DecodeRPCContext): ProtocolRPCResponse
43
+ }
44
+
45
+ export interface BaseClientEncoder {
46
+ encode(data: any): ArrayBuffer
47
+ encodeRPC(rpc: ProtocolRPC, context: EncodeRPCContext): ArrayBuffer
48
+ }
49
+
50
+ export type InputType<T> = T extends any[]
51
+ ? InputType<T[number]>[]
52
+ : T extends ProtocolBlobInterface
53
+ ? ProtocolBlob
54
+ : T extends object
55
+ ? { [K in keyof T]: InputType<T[K]> }
56
+ : T
57
+
58
+ export type OutputType<T> = T extends any[]
59
+ ? OutputType<T[number]>[]
60
+ : T extends ProtocolBlobInterface
61
+ ? ProtocolServerBlobStream
62
+ : T extends object
63
+ ? { [K in keyof T]: OutputType<T[K]> }
64
+ : T
@@ -0,0 +1,47 @@
1
+ import type { Container } from '@nmtjs/core'
2
+ import type { Hook } from '@nmtjs/core'
3
+ import type { Connection } from './connection.ts'
4
+
5
+ export type ProtocolApiCallOptions = {
6
+ connection: Connection
7
+ namespace: string
8
+ procedure: string
9
+ container: Container
10
+ payload: any
11
+ signal: AbortSignal
12
+ }
13
+
14
+ export type ProtocolAnyIterable<T> =
15
+ | (() => AsyncGenerator<T>)
16
+ // | (() => Generator<T>)
17
+ | AsyncIterable<T>
18
+ // | Iterable<T>
19
+
20
+ export interface ProtocolApiCallBaseResult {
21
+ output: unknown
22
+ }
23
+ export interface ProtocolApiCallSubscriptionResult
24
+ extends ProtocolApiCallBaseResult {
25
+ subscription: never
26
+ }
27
+
28
+ export interface ProtocolApiCallIterableResult
29
+ extends ProtocolApiCallBaseResult {
30
+ iterable: ProtocolAnyIterable<unknown>
31
+ }
32
+
33
+ export type ProtocolApiCallResult =
34
+ | ProtocolApiCallBaseResult
35
+ | ProtocolApiCallSubscriptionResult
36
+ | ProtocolApiCallIterableResult
37
+
38
+ export interface ProtocolApi {
39
+ call(options: ProtocolApiCallOptions): Promise<ProtocolApiCallResult>
40
+ }
41
+
42
+ declare module '@nmtjs/core' {
43
+ export interface HookType {
44
+ [Hook.OnConnect]: (connection: Connection) => any
45
+ [Hook.OnDisconnect]: (connection: Connection) => any
46
+ }
47
+ }
@@ -0,0 +1,57 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ // import type { TAnyEventContract } from '@nmtjs/contract'
3
+ import type { InteractivePromise } from '@nmtjs/common'
4
+ import type { Container } from '@nmtjs/core'
5
+ import type { ProtocolApiCallResult } from './api.ts'
6
+ import type { BaseServerDecoder, BaseServerEncoder } from './format.ts'
7
+ import type { ProtocolClientStream, ProtocolServerStream } from './stream.ts'
8
+
9
+ // export type NotifyFn = <T extends TAnyEventContract>(
10
+ // connection: Connection,
11
+ // contract: T,
12
+ // payload: t.infer.input.decoded<T['payload']>,
13
+ // ) => Promise<boolean>
14
+
15
+ // export type ConnectionNotifyFn = (
16
+ // contract: TAnyEventContract,
17
+ // payload: unknown,
18
+ // ) => Promise<boolean>
19
+
20
+ export type ConnectionOptions<Data = unknown> = {
21
+ id?: string
22
+ data: Data
23
+ }
24
+
25
+ export class Connection<Data = unknown> {
26
+ readonly id: string
27
+ readonly data: Data
28
+
29
+ constructor(options: ConnectionOptions<Data>) {
30
+ this.id = options.id ?? randomUUID()
31
+ this.data = options.data
32
+ }
33
+ }
34
+
35
+ export type ConnectionCall<T = unknown> = InteractivePromise<T> & {
36
+ abort: AbortController['abort']
37
+ }
38
+
39
+ export class ConnectionContext {
40
+ streamId = 1
41
+ calls = new Map<number, ConnectionCall<ProtocolApiCallResult>>()
42
+ clientStreams = new Map<number, ProtocolClientStream>()
43
+ serverStreams = new Map<number, ProtocolServerStream>()
44
+ container: Container
45
+ format: {
46
+ encoder: BaseServerEncoder
47
+ decoder: BaseServerDecoder
48
+ }
49
+
50
+ constructor(
51
+ container: ConnectionContext['container'],
52
+ format: ConnectionContext['format'],
53
+ ) {
54
+ this.container = container
55
+ this.format = format
56
+ }
57
+ }
@@ -0,0 +1,4 @@
1
+ export const kTransportPlugin: unique symbol = Symbol.for(
2
+ 'neemata:TransportPluginKey',
3
+ )
4
+ export type kTransportPlugin = typeof kTransportPlugin
@@ -0,0 +1,107 @@
1
+ import { type Pattern, match } from '@nmtjs/core'
2
+ import type {
3
+ DecodeRPCContext,
4
+ EncodeRPCContext,
5
+ ProtocolRPC,
6
+ ProtocolRPCResponse,
7
+ } from '../common/types.ts'
8
+
9
+ export interface BaseServerDecoder {
10
+ accept: Pattern[]
11
+ decode(buffer: ArrayBuffer): any
12
+ decodeRPC(buffer: ArrayBuffer, context: DecodeRPCContext): ProtocolRPC
13
+ }
14
+
15
+ export interface BaseServerEncoder {
16
+ contentType: string
17
+ encode(data: any): ArrayBuffer
18
+ encodeRPC(rpc: ProtocolRPCResponse, context: EncodeRPCContext): ArrayBuffer
19
+ }
20
+
21
+ export abstract class BaseServerFormat
22
+ implements BaseServerDecoder, BaseServerEncoder
23
+ {
24
+ abstract accept: Pattern[]
25
+ abstract contentType: string
26
+
27
+ abstract encode(data: any): ArrayBuffer
28
+ abstract encodeRPC(
29
+ rpc: ProtocolRPCResponse,
30
+ context: EncodeRPCContext,
31
+ ): ArrayBuffer
32
+ abstract decode(buffer: ArrayBuffer): any
33
+ abstract decodeRPC(
34
+ buffer: ArrayBuffer,
35
+ context: DecodeRPCContext,
36
+ ): ProtocolRPC
37
+ }
38
+
39
+ export const parseContentTypes = (types: string) => {
40
+ if (types === '*/*') return ['*/*']
41
+ return types
42
+ .split(',')
43
+ .map((t) => {
44
+ const [type, ...rest] = t.split(';')
45
+ const params = new Map(
46
+ rest.map((p) =>
47
+ p
48
+ .trim()
49
+ .split('=')
50
+ .slice(0, 2)
51
+ .map((p) => p.trim()),
52
+ ) as [string, string][],
53
+ )
54
+ return {
55
+ type,
56
+ q: params.has('q') ? Number.parseFloat(params.get('q')!) : 1,
57
+ }
58
+ })
59
+ .sort((a, b) => {
60
+ if (a.type === '*/*') return 1
61
+ if (b.type === '*/*') return -1
62
+ return b.q - a.q ? -1 : 1
63
+ })
64
+ .map((t) => t.type)
65
+ }
66
+
67
+ export class Format {
68
+ decoders = new Map<Pattern, BaseServerDecoder>()
69
+ encoders = new Map<Pattern, BaseServerEncoder>()
70
+
71
+ constructor(formats: BaseServerFormat[]) {
72
+ for (const format of formats) {
73
+ this.encoders.set(format.contentType, format)
74
+ for (const acceptType of format.accept) {
75
+ this.decoders.set(acceptType, format)
76
+ }
77
+ }
78
+ }
79
+
80
+ supportsDecoder(contentType: string, throwIfUnsupported = false) {
81
+ return this.supports(this.decoders, contentType, throwIfUnsupported)
82
+ }
83
+
84
+ supportsEncoder(contentType: string, throwIfUnsupported = false) {
85
+ return this.supports(this.encoders, contentType, throwIfUnsupported)
86
+ }
87
+
88
+ private supports<T extends BaseServerEncoder | BaseServerDecoder>(
89
+ formats: Map<Pattern, T>,
90
+ contentType: string,
91
+ throwIfUnsupported = false,
92
+ ): T | null {
93
+ // TODO: Use node:utils.MIMEType (not implemented yet in Deno and Bun yet)
94
+ const types = parseContentTypes(contentType)
95
+
96
+ for (const type of types) {
97
+ for (const [pattern, format] of formats) {
98
+ if (type === '*/*' || match(type, pattern)) return format
99
+ }
100
+ }
101
+
102
+ if (throwIfUnsupported)
103
+ throw new Error(`No supported format found: ${contentType}`)
104
+
105
+ return null
106
+ }
107
+ }
@@ -0,0 +1,10 @@
1
+ export * from './api.ts'
2
+ export * from './connection.ts'
3
+ export * from './constants.ts'
4
+ export * from './injectables.ts'
5
+ export * from './format.ts'
6
+ export * from './protocol.ts'
7
+ export * from './registry.ts'
8
+ export * from './stream.ts'
9
+ export * from './transport.ts'
10
+ export * from './utils.ts'
@@ -0,0 +1,51 @@
1
+ import {
2
+ Scope,
3
+ createFactoryInjectable,
4
+ createLazyInjectable,
5
+ } from '@nmtjs/core'
6
+
7
+ const connection = createLazyInjectable<unknown, Scope.Connection>(
8
+ Scope.Connection,
9
+ 'RPC connection',
10
+ )
11
+
12
+ const connectionData = createLazyInjectable<unknown, Scope.Connection>(
13
+ Scope.Connection,
14
+ "RPC connection's data",
15
+ )
16
+
17
+ const transportStopSignal = createLazyInjectable<AbortSignal>(
18
+ Scope.Global,
19
+ 'Transport stop signal',
20
+ )
21
+
22
+ const rpcClientAbortSignal = createLazyInjectable<AbortSignal, Scope.Call>(
23
+ Scope.Call,
24
+ 'RPC client abort signal',
25
+ )
26
+
27
+ const rpcTimeoutSignal = createLazyInjectable<AbortSignal, Scope.Call>(
28
+ Scope.Call,
29
+ 'RPC timeout signal',
30
+ )
31
+
32
+ const rpcAbortSignal = createFactoryInjectable(
33
+ {
34
+ dependencies: {
35
+ rpcTimeoutSignal,
36
+ rpcClientAbortSignal,
37
+ transportStopSignal,
38
+ },
39
+ factory: (ctx) => AbortSignal.any(Object.values(ctx)),
40
+ },
41
+ 'Any RPC abort signal',
42
+ )
43
+
44
+ export const ProtocolInjectables = {
45
+ connection,
46
+ connectionData,
47
+ transportStopSignal,
48
+ rpcClientAbortSignal,
49
+ rpcTimeoutSignal,
50
+ rpcAbortSignal,
51
+ } as const