@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.
- package/.prettierrc.json +7 -0
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/client/HttpClient.d.ts +10 -0
- package/dist/client/HttpClient.js +67 -0
- package/dist/client/HttpClient.js.map +1 -0
- package/dist/client/RemoteSubscriptions.d.ts +14 -0
- package/dist/client/RemoteSubscriptions.js +84 -0
- package/dist/client/RemoteSubscriptions.js.map +1 -0
- package/dist/client/RpcClientImpl.d.ts +22 -0
- package/dist/client/RpcClientImpl.js +96 -0
- package/dist/client/RpcClientImpl.js.map +1 -0
- package/dist/client/WebSocketConnection.d.ts +33 -0
- package/dist/client/WebSocketConnection.js +152 -0
- package/dist/client/WebSocketConnection.js.map +1 -0
- package/dist/client/index.d.ts +22 -0
- package/dist/client/index.js +28 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/remote.d.ts +14 -0
- package/dist/client/remote.js +66 -0
- package/dist/client/remote.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.js +9 -0
- package/dist/logger.js.map +1 -0
- package/dist/rpc.d.ts +36 -0
- package/dist/rpc.js +32 -0
- package/dist/rpc.js.map +1 -0
- package/dist/server/ConnectionsServer.d.ts +13 -0
- package/dist/server/ConnectionsServer.js +60 -0
- package/dist/server/ConnectionsServer.js.map +1 -0
- package/dist/server/LocalSubscriptions.d.ts +12 -0
- package/dist/server/LocalSubscriptions.js +113 -0
- package/dist/server/LocalSubscriptions.js.map +1 -0
- package/dist/server/RpcServerImpl.d.ts +23 -0
- package/dist/server/RpcServerImpl.js +164 -0
- package/dist/server/RpcServerImpl.js.map +1 -0
- package/dist/server/http.d.ts +9 -0
- package/dist/server/http.js +83 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/index.d.ts +29 -0
- package/dist/server/index.js +31 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/local.d.ts +15 -0
- package/dist/server/local.js +46 -0
- package/dist/server/local.js.map +1 -0
- package/dist/utils/json.d.ts +2 -0
- package/dist/utils/json.js +34 -0
- package/dist/utils/json.js.map +1 -0
- package/dist/utils/middleware.d.ts +2 -0
- package/dist/utils/middleware.js +31 -0
- package/dist/utils/middleware.js.map +1 -0
- package/dist/utils/promises.d.ts +5 -0
- package/dist/utils/promises.js +29 -0
- package/dist/utils/promises.js.map +1 -0
- package/dist/utils/throttle.d.ts +4 -0
- package/dist/utils/throttle.js +40 -0
- package/dist/utils/throttle.js.map +1 -0
- package/dist/utils/types.d.ts +1 -0
- package/dist/utils/types.js +3 -0
- package/dist/utils/types.js.map +1 -0
- package/example/api.ts +15 -0
- package/example/client.ts +16 -0
- package/example/server.ts +37 -0
- package/package.json +34 -0
- package/src/client/HttpClient.ts +80 -0
- package/src/client/RemoteSubscriptions.ts +121 -0
- package/src/client/RpcClientImpl.ts +177 -0
- package/src/client/WebSocketConnection.ts +183 -0
- package/src/client/index.ts +56 -0
- package/src/client/remote.ts +118 -0
- package/src/index.ts +18 -0
- package/src/logger.ts +12 -0
- package/src/rpc.ts +51 -0
- package/src/server/ConnectionsServer.ts +78 -0
- package/src/server/LocalSubscriptions.ts +155 -0
- package/src/server/RpcServerImpl.ts +252 -0
- package/src/server/http.ts +109 -0
- package/src/server/index.ts +65 -0
- package/src/server/local.ts +80 -0
- package/src/utils/json.ts +32 -0
- package/src/utils/middleware.ts +38 -0
- package/src/utils/promises.ts +25 -0
- package/src/utils/throttle.ts +48 -0
- package/src/utils/types.ts +1 -0
- package/tests/calls.ts +215 -0
- package/tests/connection.ts +107 -0
- package/tests/context.ts +176 -0
- package/tests/middleware.ts +112 -0
- package/tests/misc.ts +187 -0
- package/tests/subscriptions.ts +442 -0
- package/tests/testUtils.ts +52 -0
- package/tests/triggers.ts +138 -0
- package/tsconfig.cjs.json +20 -0
- 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
|
+
}
|