@push-rpc/next 2.0.0-beta.1

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 (97) hide show
  1. package/.prettierrc.json +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +26 -0
  4. package/dist/client/HttpClient.d.ts +10 -0
  5. package/dist/client/HttpClient.js +67 -0
  6. package/dist/client/HttpClient.js.map +1 -0
  7. package/dist/client/RemoteSubscriptions.d.ts +14 -0
  8. package/dist/client/RemoteSubscriptions.js +84 -0
  9. package/dist/client/RemoteSubscriptions.js.map +1 -0
  10. package/dist/client/RpcClientImpl.d.ts +22 -0
  11. package/dist/client/RpcClientImpl.js +96 -0
  12. package/dist/client/RpcClientImpl.js.map +1 -0
  13. package/dist/client/WebSocketConnection.d.ts +33 -0
  14. package/dist/client/WebSocketConnection.js +152 -0
  15. package/dist/client/WebSocketConnection.js.map +1 -0
  16. package/dist/client/index.d.ts +22 -0
  17. package/dist/client/index.js +28 -0
  18. package/dist/client/index.js.map +1 -0
  19. package/dist/client/remote.d.ts +14 -0
  20. package/dist/client/remote.js +66 -0
  21. package/dist/client/remote.js.map +1 -0
  22. package/dist/index.d.ts +12 -0
  23. package/dist/index.js +20 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/logger.d.ts +8 -0
  26. package/dist/logger.js +9 -0
  27. package/dist/logger.js.map +1 -0
  28. package/dist/rpc.d.ts +36 -0
  29. package/dist/rpc.js +32 -0
  30. package/dist/rpc.js.map +1 -0
  31. package/dist/server/ConnectionsServer.d.ts +13 -0
  32. package/dist/server/ConnectionsServer.js +60 -0
  33. package/dist/server/ConnectionsServer.js.map +1 -0
  34. package/dist/server/LocalSubscriptions.d.ts +12 -0
  35. package/dist/server/LocalSubscriptions.js +113 -0
  36. package/dist/server/LocalSubscriptions.js.map +1 -0
  37. package/dist/server/RpcServerImpl.d.ts +23 -0
  38. package/dist/server/RpcServerImpl.js +164 -0
  39. package/dist/server/RpcServerImpl.js.map +1 -0
  40. package/dist/server/http.d.ts +9 -0
  41. package/dist/server/http.js +83 -0
  42. package/dist/server/http.js.map +1 -0
  43. package/dist/server/index.d.ts +29 -0
  44. package/dist/server/index.js +31 -0
  45. package/dist/server/index.js.map +1 -0
  46. package/dist/server/local.d.ts +15 -0
  47. package/dist/server/local.js +46 -0
  48. package/dist/server/local.js.map +1 -0
  49. package/dist/utils/json.d.ts +2 -0
  50. package/dist/utils/json.js +34 -0
  51. package/dist/utils/json.js.map +1 -0
  52. package/dist/utils/middleware.d.ts +2 -0
  53. package/dist/utils/middleware.js +31 -0
  54. package/dist/utils/middleware.js.map +1 -0
  55. package/dist/utils/promises.d.ts +5 -0
  56. package/dist/utils/promises.js +29 -0
  57. package/dist/utils/promises.js.map +1 -0
  58. package/dist/utils/throttle.d.ts +4 -0
  59. package/dist/utils/throttle.js +40 -0
  60. package/dist/utils/throttle.js.map +1 -0
  61. package/dist/utils/types.d.ts +1 -0
  62. package/dist/utils/types.js +3 -0
  63. package/dist/utils/types.js.map +1 -0
  64. package/example/api.ts +15 -0
  65. package/example/client.ts +16 -0
  66. package/example/server.ts +37 -0
  67. package/package.json +34 -0
  68. package/src/client/HttpClient.ts +80 -0
  69. package/src/client/RemoteSubscriptions.ts +121 -0
  70. package/src/client/RpcClientImpl.ts +177 -0
  71. package/src/client/WebSocketConnection.ts +183 -0
  72. package/src/client/index.ts +56 -0
  73. package/src/client/remote.ts +118 -0
  74. package/src/index.ts +18 -0
  75. package/src/logger.ts +12 -0
  76. package/src/rpc.ts +51 -0
  77. package/src/server/ConnectionsServer.ts +78 -0
  78. package/src/server/LocalSubscriptions.ts +155 -0
  79. package/src/server/RpcServerImpl.ts +252 -0
  80. package/src/server/http.ts +109 -0
  81. package/src/server/index.ts +65 -0
  82. package/src/server/local.ts +80 -0
  83. package/src/utils/json.ts +32 -0
  84. package/src/utils/middleware.ts +38 -0
  85. package/src/utils/promises.ts +25 -0
  86. package/src/utils/throttle.ts +48 -0
  87. package/src/utils/types.ts +1 -0
  88. package/tests/calls.ts +215 -0
  89. package/tests/connection.ts +107 -0
  90. package/tests/context.ts +176 -0
  91. package/tests/middleware.ts +112 -0
  92. package/tests/misc.ts +187 -0
  93. package/tests/subscriptions.ts +442 -0
  94. package/tests/testUtils.ts +52 -0
  95. package/tests/triggers.ts +138 -0
  96. package/tsconfig.cjs.json +20 -0
  97. package/tsconfig.json +26 -0
@@ -0,0 +1,121 @@
1
+ import {safeStringify} from "../utils/json.js"
2
+
3
+ export class RemoteSubscriptions {
4
+ subscribe(
5
+ initialData: unknown,
6
+ itemName: string,
7
+ parameters: unknown[],
8
+ consumer: (d: unknown) => void
9
+ ) {
10
+ this.addSubscription(itemName, parameters, consumer)
11
+
12
+ this.consume(itemName, parameters, initialData)
13
+ }
14
+
15
+ unsubscribe(itemName: string, parameters: unknown[], consumer: (d: unknown) => void): boolean {
16
+ const parametersKey = getParametersKey(parameters)
17
+
18
+ return this.removeSubscription(itemName, parametersKey, consumer)
19
+ }
20
+
21
+ private addSubscription(itemName: string, parameters: unknown[], consumer: (d: unknown) => void) {
22
+ const itemSubscriptions = this.byItem.get(itemName) || {byParameters: new Map()}
23
+ this.byItem.set(itemName, itemSubscriptions)
24
+
25
+ const parametersKey = getParametersKey(parameters)
26
+ const parameterSubscriptions = itemSubscriptions.byParameters.get(parametersKey) || {
27
+ parameters,
28
+ cached: null,
29
+ consumers: [],
30
+ }
31
+ itemSubscriptions.byParameters.set(parametersKey, parameterSubscriptions)
32
+ parameterSubscriptions.consumers.push(consumer)
33
+ }
34
+
35
+ private removeSubscription(
36
+ itemName: string,
37
+ parametersKey: string,
38
+ consumer: (d: unknown) => void
39
+ ): boolean {
40
+ const itemSubscriptions = this.byItem.get(itemName)
41
+ if (!itemSubscriptions) return false
42
+
43
+ const filterSubscriptions = itemSubscriptions.byParameters.get(parametersKey)
44
+ if (!filterSubscriptions) return false
45
+
46
+ const index = filterSubscriptions.consumers.indexOf(consumer)
47
+ if (index == -1) return false
48
+
49
+ filterSubscriptions.consumers.splice(index, 1)
50
+
51
+ if (!filterSubscriptions.consumers.length) {
52
+ itemSubscriptions.byParameters.delete(parametersKey)
53
+
54
+ if (itemSubscriptions.byParameters.size == 0) {
55
+ this.byItem.delete(itemName)
56
+ }
57
+
58
+ return true
59
+ }
60
+
61
+ return false
62
+ }
63
+
64
+ getCached(itemName: string, parameters: unknown[]): unknown | undefined {
65
+ const parametersKey = getParametersKey(parameters)
66
+
67
+ const itemSubscriptions = this.byItem.get(itemName)
68
+ if (!itemSubscriptions) return
69
+
70
+ const filterSubscriptions = itemSubscriptions.byParameters.get(parametersKey)
71
+
72
+ return filterSubscriptions?.cached
73
+ }
74
+
75
+ consume(itemName: string, parameters: unknown[], data: unknown) {
76
+ const parametersKey = getParametersKey(parameters)
77
+
78
+ const itemSubscriptions = this.byItem.get(itemName)
79
+ if (!itemSubscriptions) return
80
+
81
+ const filterSubscriptions = itemSubscriptions.byParameters.get(parametersKey)
82
+
83
+ if (!filterSubscriptions) return
84
+
85
+ filterSubscriptions.cached = data
86
+ filterSubscriptions.consumers.forEach((consumer) => {
87
+ consumer(data)
88
+ })
89
+ }
90
+
91
+ getAllSubscriptions(): Array<
92
+ [itemName: string, parameters: unknown[], consumers: Array<(d: unknown) => void>]
93
+ > {
94
+ const result: Array<[string, unknown[], Array<(d: unknown) => void>]> = []
95
+
96
+ for (const [itemName, itemSubscriptions] of this.byItem) {
97
+ for (const [, parameterSubscriptions] of itemSubscriptions.byParameters) {
98
+ result.push([itemName, parameterSubscriptions.parameters, parameterSubscriptions.consumers])
99
+ }
100
+ }
101
+
102
+ return result
103
+ }
104
+
105
+ private byItem: Map<string, ItemSubscription> = new Map()
106
+ }
107
+
108
+ type ItemSubscription = {
109
+ byParameters: Map<
110
+ string,
111
+ {
112
+ parameters: unknown[]
113
+ cached: unknown
114
+ consumers: Array<(d: unknown) => void>
115
+ }
116
+ >
117
+ }
118
+
119
+ function getParametersKey(parameters: unknown[]) {
120
+ return safeStringify(parameters)
121
+ }
@@ -0,0 +1,177 @@
1
+ import {CallOptions, InvocationType, RpcContext, Services} from "../rpc.js"
2
+ import {HttpClient} from "./HttpClient.js"
3
+ import {RemoteSubscriptions} from "./RemoteSubscriptions.js"
4
+ import {WebSocketConnection} from "./WebSocketConnection.js"
5
+ import {nanoid} from "nanoid"
6
+ import {createRemote, ServicesWithSubscriptions} from "./remote.js"
7
+ import {ConsumeServicesOptions, RpcClient} from "./index.js"
8
+ import {withMiddlewares} from "../utils/middleware.js"
9
+
10
+ export class RpcClientImpl<S extends Services<S>> implements RpcClient {
11
+ constructor(
12
+ url: string,
13
+ private readonly options: ConsumeServicesOptions
14
+ ) {
15
+ this.httpClient = new HttpClient(url, this.clientId)
16
+ this.remoteSubscriptions = new RemoteSubscriptions()
17
+
18
+ this.connection = new WebSocketConnection(
19
+ url,
20
+ this.clientId,
21
+ {
22
+ errorDelayMaxDuration: options.errorDelayMaxDuration,
23
+ reconnectDelay: options.reconnectDelay,
24
+ pingInterval: options.pingInterval,
25
+ },
26
+ (itemName, parameters, data) => {
27
+ this.remoteSubscriptions.consume(itemName, parameters, data)
28
+ },
29
+ this.resubscribe
30
+ )
31
+ }
32
+
33
+ private readonly clientId = nanoid()
34
+ private readonly httpClient: HttpClient
35
+ private readonly remoteSubscriptions: RemoteSubscriptions
36
+ private readonly connection: WebSocketConnection
37
+
38
+ isConnected() {
39
+ return this.connection.isConnected()
40
+ }
41
+
42
+ close() {
43
+ return this.connection.close()
44
+ }
45
+
46
+ _allSubscriptions() {
47
+ const result: Array<
48
+ [itemName: string, parameters: unknown[], consumers: (d: unknown) => void]
49
+ > = []
50
+
51
+ for (const [
52
+ itemName,
53
+ parameters,
54
+ consumers,
55
+ ] of this.remoteSubscriptions.getAllSubscriptions()) {
56
+ for (const consumer of consumers) {
57
+ result.push([itemName, parameters, consumer])
58
+ }
59
+ }
60
+ return result
61
+ }
62
+
63
+ _webSocket() {
64
+ return this.connection._webSocket()
65
+ }
66
+
67
+ createRemote(): ServicesWithSubscriptions<S> {
68
+ return createRemote<S>({
69
+ call: this.call,
70
+ subscribe: this.subscribe,
71
+ unsubscribe: this.unsubscribe,
72
+ })
73
+ }
74
+
75
+ private call = (
76
+ itemName: string,
77
+ parameters: unknown[],
78
+ callOptions?: CallOptions
79
+ ): Promise<unknown> => {
80
+ return this.invoke(
81
+ itemName,
82
+ InvocationType.Call,
83
+ (...parameters) =>
84
+ this.httpClient.call(
85
+ itemName,
86
+ parameters,
87
+ callOptions?.timeout ?? this.options.callTimeout
88
+ ),
89
+ parameters
90
+ )
91
+ }
92
+
93
+ private subscribe = async (
94
+ itemName: string,
95
+ parameters: unknown[],
96
+ consumer: (d: unknown) => void,
97
+ callOptions?: CallOptions
98
+ ): Promise<void> => {
99
+ const cached = this.remoteSubscriptions.getCached(itemName, parameters)
100
+
101
+ if (cached !== undefined) {
102
+ consumer(cached)
103
+ }
104
+
105
+ if (this.options.subscribe) {
106
+ this.connection.connect().catch((e) => {
107
+ // ignored
108
+ })
109
+ }
110
+
111
+ const data = await this.invoke(
112
+ itemName,
113
+ InvocationType.Subscribe,
114
+ (...parameters) =>
115
+ this.httpClient.subscribe(
116
+ itemName,
117
+ parameters,
118
+ callOptions?.timeout ?? this.options.callTimeout
119
+ ),
120
+ parameters
121
+ )
122
+ this.remoteSubscriptions.subscribe(data, itemName, parameters, consumer)
123
+ }
124
+
125
+ private unsubscribe = async (
126
+ itemName: string,
127
+ parameters: unknown[],
128
+ consumer: (d: unknown) => void,
129
+ callOptions?: CallOptions
130
+ ) => {
131
+ const noSubscriptionsLeft = this.remoteSubscriptions.unsubscribe(itemName, parameters, consumer)
132
+
133
+ if (noSubscriptionsLeft) {
134
+ await this.invoke(
135
+ itemName,
136
+ InvocationType.Unsubscribe,
137
+ (...parameters) =>
138
+ this.httpClient.unsubscribe(
139
+ itemName,
140
+ parameters,
141
+ callOptions?.timeout ?? this.options.callTimeout
142
+ ),
143
+ parameters
144
+ )
145
+ }
146
+ }
147
+
148
+ private resubscribe = () => {
149
+ for (const [itemName, params, consumers] of this.remoteSubscriptions.getAllSubscriptions()) {
150
+ this.httpClient
151
+ .subscribe(itemName, params, this.options.callTimeout)
152
+ .then((data) => {
153
+ this.remoteSubscriptions.consume(itemName, params, data)
154
+ })
155
+ .catch((e) => {
156
+ for (const consumer of consumers) {
157
+ this.remoteSubscriptions.unsubscribe(itemName, params, consumer)
158
+ }
159
+ })
160
+ }
161
+ }
162
+
163
+ private invoke(
164
+ itemName: string,
165
+ invocationType: InvocationType,
166
+ next: (...params: unknown[]) => Promise<unknown>,
167
+ parameters: unknown[]
168
+ ) {
169
+ const ctx: RpcContext = {
170
+ clientId: this.clientId,
171
+ itemName,
172
+ invocationType: invocationType,
173
+ }
174
+
175
+ return withMiddlewares(ctx, this.options.middleware, next, ...parameters)
176
+ }
177
+ }
@@ -0,0 +1,183 @@
1
+ import WebSocket from "ws"
2
+ import {log} from "../logger.js"
3
+ import {safeParseJson} from "../utils/json.js"
4
+ import {adelay} from "../utils/promises.js"
5
+
6
+ export class WebSocketConnection {
7
+ constructor(
8
+ private readonly url: string,
9
+ private readonly clientId: string,
10
+ private readonly options: {
11
+ reconnectDelay: number
12
+ errorDelayMaxDuration: number
13
+ pingInterval: number
14
+ },
15
+ private readonly consume: (itemName: string, parameters: unknown[], data: unknown) => void,
16
+ private readonly onConnected: () => void
17
+ ) {
18
+ this.url = url
19
+ this.clientId = clientId
20
+ }
21
+
22
+ async close() {
23
+ this.disconnectedMark = true
24
+
25
+ if (this.socket) {
26
+ this.socket!.terminate()
27
+ this.socket = null
28
+ }
29
+
30
+ if (this.pingTimeout) {
31
+ clearTimeout(this.pingTimeout)
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Connect to the server, on each disconnect try to disconnect.
37
+ * Resolves at first successful connect. Reconnection loop continues even after resolution
38
+ * Never rejects
39
+ */
40
+ connect() {
41
+ if (this.socket || !this.disconnectedMark) return Promise.resolve()
42
+
43
+ this.disconnectedMark = false
44
+
45
+ return new Promise<void>(async (resolve) => {
46
+ let onFirstConnection = resolve
47
+ let errorDelay = 0
48
+
49
+ while (true) {
50
+ // connect, and wait for ...
51
+ await new Promise<void>((resolve) => {
52
+ // 1. ...disconnected
53
+ const connectionPromise = this.establishConnection(resolve)
54
+
55
+ connectionPromise.then(
56
+ () => {
57
+ // first reconnect after successful connection is done without delay
58
+ errorDelay = 0
59
+
60
+ // signal about first connection
61
+ onFirstConnection()
62
+ onFirstConnection = () => {}
63
+ },
64
+ () => {
65
+ // 2. ... unable to establish connection
66
+ resolve()
67
+ }
68
+ )
69
+ })
70
+
71
+ // disconnected while connecting?
72
+ if (this.disconnectedMark) {
73
+ return
74
+ }
75
+
76
+ await adelay(this.options.reconnectDelay + errorDelay)
77
+
78
+ // disconnected while waiting?
79
+ if (this.disconnectedMark) {
80
+ return
81
+ }
82
+
83
+ errorDelay = Math.round(Math.random() * this.options.errorDelayMaxDuration)
84
+ }
85
+ })
86
+ }
87
+
88
+ public isConnected() {
89
+ return this.socket !== null
90
+ }
91
+
92
+ /**
93
+ * Connect this to server
94
+ *
95
+ * Resolves on successful connection, rejects on connection error or connection timeout
96
+ */
97
+ private async establishConnection(onDisconnected: () => void): Promise<void> {
98
+ return new Promise(async (resolve, reject) => {
99
+ try {
100
+ const socket = new WebSocket(this.url, this.clientId)
101
+
102
+ let connected = false
103
+
104
+ socket.on("open", () => {
105
+ this.socket = socket
106
+ connected = true
107
+ resolve()
108
+
109
+ this.heartbeat()
110
+
111
+ this.onConnected()
112
+ })
113
+
114
+ socket.on("ping", () => {
115
+ this.heartbeat()
116
+ })
117
+
118
+ socket.on("close", () => {
119
+ this.socket = null
120
+
121
+ if (connected) {
122
+ onDisconnected()
123
+ }
124
+
125
+ if (this.pingTimeout) {
126
+ clearTimeout(this.pingTimeout)
127
+ }
128
+ })
129
+
130
+ socket.on("error", (e) => {
131
+ if (!connected) {
132
+ reject(e)
133
+ }
134
+
135
+ log.warn("WS connection error", e.message)
136
+
137
+ try {
138
+ socket.close()
139
+ } catch (e) {
140
+ // ignore
141
+ }
142
+ })
143
+
144
+ socket.on("message", (message) => {
145
+ this.receiveSocketMessage(message)
146
+ })
147
+ } catch (e) {
148
+ reject(e)
149
+ }
150
+ })
151
+ }
152
+
153
+ private socket: WebSocket | null = null
154
+ private disconnectedMark = true
155
+ private pingTimeout: NodeJS.Timeout | null = null
156
+
157
+ private heartbeat() {
158
+ if (this.pingTimeout) {
159
+ clearTimeout(this.pingTimeout)
160
+ }
161
+
162
+ this.pingTimeout = setTimeout(() => {
163
+ this.socket?.terminate()
164
+ }, this.options.pingInterval * 1.5)
165
+ }
166
+
167
+ private async receiveSocketMessage(rawMessage: WebSocket.RawData) {
168
+ try {
169
+ const msg = rawMessage.toString()
170
+
171
+ const [itemName, data, ...parameters] = safeParseJson(msg)
172
+
173
+ this.consume(itemName, parameters, data)
174
+ } catch (e) {
175
+ log.warn("Invalid message received", e)
176
+ }
177
+ }
178
+
179
+ // test-only
180
+ _webSocket() {
181
+ return this.socket
182
+ }
183
+ }
@@ -0,0 +1,56 @@
1
+ import {RpcContext, Services} from "../rpc.js"
2
+ import {ServicesWithSubscriptions} from "./remote.js"
3
+ import WebSocket from "ws"
4
+ import {RpcClientImpl} from "./RpcClientImpl.js"
5
+ import {Middleware} from "../utils/middleware.js"
6
+
7
+ export type RpcClient = {
8
+ isConnected(): boolean
9
+ close(): Promise<void>
10
+
11
+ // test-only
12
+ _allSubscriptions(): Array<any[]>
13
+ _webSocket(): WebSocket | null
14
+ }
15
+
16
+ export type ConsumeServicesOptions = {
17
+ callTimeout: number
18
+ subscribe: boolean
19
+ reconnectDelay: number
20
+ errorDelayMaxDuration: number
21
+ pingInterval: number
22
+ middleware: Middleware<RpcContext>[]
23
+ }
24
+
25
+ export async function consumeServices<S extends Services<S>>(
26
+ url: string,
27
+ overrideOptions: Partial<ConsumeServicesOptions> = {}
28
+ ): Promise<{
29
+ client: RpcClient
30
+ remote: ServicesWithSubscriptions<S>
31
+ }> {
32
+ if (url.endsWith("/")) {
33
+ throw new Error("URL must not end with /")
34
+ }
35
+
36
+ const options = {
37
+ ...defaultOptions,
38
+ ...overrideOptions,
39
+ }
40
+
41
+ const client = new RpcClientImpl<S>(url, options)
42
+
43
+ return {
44
+ client,
45
+ remote: client.createRemote(),
46
+ }
47
+ }
48
+
49
+ const defaultOptions: ConsumeServicesOptions = {
50
+ callTimeout: 5 * 1000,
51
+ subscribe: true,
52
+ reconnectDelay: 0,
53
+ errorDelayMaxDuration: 15 * 1000,
54
+ pingInterval: 30 * 1000, // should be in-sync with server
55
+ middleware: [],
56
+ }
@@ -0,0 +1,118 @@
1
+ import {CallOptions, Consumer, RemoteFunction, Services} from "../rpc.js"
2
+
3
+ export function createRemote<S extends Services<S>>(
4
+ hooks: RemoteHooks,
5
+ name = ""
6
+ ): ServicesWithSubscriptions<S> {
7
+ // start with remote function
8
+ const remoteItem = (...paramsWithCallOptions: unknown[]) => {
9
+ const {params, callOptions} = extractCallOptions(paramsWithCallOptions)
10
+
11
+ return hooks.call(name, params, callOptions)
12
+ }
13
+
14
+ // add subscription methods
15
+ const subscription = {
16
+ subscribe: (consumer: (d: unknown) => void, ...paramsWithCallOptions: unknown[]) => {
17
+ const {params, callOptions} = extractCallOptions(paramsWithCallOptions)
18
+ return hooks.subscribe(name, params, consumer, callOptions)
19
+ },
20
+ unsubscribe: (consumer: (d: unknown) => void, ...paramsWithCallOptions: unknown[]) => {
21
+ const {params, callOptions} = extractCallOptions(paramsWithCallOptions)
22
+ return hooks.unsubscribe(name, params, consumer, callOptions)
23
+ },
24
+ }
25
+
26
+ Object.assign(remoteItem, subscription)
27
+
28
+ // then add proxy creating subitems
29
+
30
+ const cachedItems: any = {}
31
+
32
+ return new Proxy(remoteItem, {
33
+ get(target: any, propName: any) {
34
+ // skip internal props
35
+ if (typeof propName != "string") return target[propName]
36
+
37
+ // skip other system props
38
+ if (["then", "catch", "toJSON", ...skippedRemoteProps].includes(propName))
39
+ return target[propName]
40
+
41
+ // skip subscription methods
42
+ if (Object.keys(subscription).includes(propName)) return target[propName]
43
+
44
+ if (!cachedItems[propName]) {
45
+ cachedItems[propName] = createRemote(hooks, name ? name + "/" + propName : propName)
46
+ }
47
+
48
+ return cachedItems[propName]
49
+ },
50
+
51
+ set(target, propName, value) {
52
+ cachedItems[propName] = value
53
+ return true
54
+ },
55
+
56
+ // Used in resubscribe
57
+ ownKeys() {
58
+ return [...skippedRemoteProps, ...Object.keys(cachedItems)]
59
+ },
60
+ })
61
+ }
62
+
63
+ function extractCallOptions(params: unknown[]): {params: unknown[]; callOptions?: CallOptions} {
64
+ if (
65
+ params.length > 0 &&
66
+ typeof params[params.length - 1] == "object" &&
67
+ params[params.length - 1] != null &&
68
+ (params[params.length - 1] as any).kind == CallOptions.KIND
69
+ ) {
70
+ const options = params.pop() as CallOptions
71
+ return {
72
+ params,
73
+ callOptions: options,
74
+ }
75
+ }
76
+
77
+ return {params}
78
+ }
79
+
80
+ export type RemoteHooks = {
81
+ call(itemName: string, parameters: unknown[], callOptions?: CallOptions): Promise<unknown>
82
+ subscribe(
83
+ itemName: string,
84
+ parameters: unknown[],
85
+ consumer: (d: unknown) => void,
86
+ callOptions?: CallOptions
87
+ ): Promise<void>
88
+ unsubscribe(
89
+ itemName: string,
90
+ parameters: unknown[],
91
+ consumer: (d: unknown) => void,
92
+ callOptions?: CallOptions
93
+ ): Promise<void>
94
+ }
95
+
96
+ export type AddParameters<
97
+ TFunction extends (...args: any) => any,
98
+ TParameters extends [...args: any],
99
+ > = (...args: [...Parameters<TFunction>, ...TParameters]) => ReturnType<TFunction>
100
+
101
+ export type ServicesWithSubscriptions<T extends Services<T>> = {
102
+ [K in keyof T]: T[K] extends RemoteFunction
103
+ ? AddParameters<T[K], [CallOptions?]> & {
104
+ subscribe(
105
+ consumer: Consumer<T[K]>,
106
+ ...parameters: [...Parameters<T[K]>, CallOptions?]
107
+ ): Promise<void>
108
+ unsubscribe(
109
+ consumer: Consumer<T[K]>,
110
+ ...parameters: [...Parameters<T[K]>, CallOptions?]
111
+ ): Promise<void>
112
+ }
113
+ : T[K] extends object
114
+ ? ServicesWithSubscriptions<T[K]>
115
+ : never
116
+ }
117
+
118
+ const skippedRemoteProps = ["length", "name", "prototype", "arguments", "caller"]
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ export type {RemoteFunction, Services, Consumer, RpcContext, RpcConnectionContext} from "./rpc.js"
2
+ export {RpcError, RpcErrors, CallOptions} from "./rpc.js"
3
+
4
+ export type {Middleware} from "./utils/middleware.js"
5
+ export {withMiddlewares} from "./utils/middleware.js"
6
+
7
+ export type {RpcServer, PublishServicesOptions} from "./server/index.js"
8
+ export {publishServices} from "./server/index.js"
9
+
10
+ export type {ServicesWithTriggers} from "./server/local.js"
11
+
12
+ export type {RpcClient, ConsumeServicesOptions} from "./client/index.js"
13
+ export {consumeServices} from "./client/index.js"
14
+
15
+ export type {ServicesWithSubscriptions} from "./client/remote.js"
16
+
17
+ export {log, setLogger} from "./logger.js"
18
+ export {safeStringify, safeParseJson} from "./utils/json.js"
package/src/logger.ts ADDED
@@ -0,0 +1,12 @@
1
+ export interface Logger {
2
+ info(s: unknown, ...params: unknown[]): void
3
+ error(s: unknown, ...params: unknown[]): void
4
+ warn(s: unknown, ...params: unknown[]): void
5
+ debug(s: unknown, ...params: unknown[]): void
6
+ }
7
+
8
+ export let log: Logger = console
9
+
10
+ export function setLogger(l: Logger) {
11
+ log = l
12
+ }