@push-rpc/next 2.0.25 → 3.0.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.
- package/README.md +9 -1
- package/dist/client/RpcClientImpl.d.ts +2 -2
- package/dist/client/RpcClientImpl.js.map +1 -1
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +2 -4
- package/dist/client/index.js.map +1 -1
- package/dist/client/remote.d.ts +8 -6
- package/dist/client/remote.js.map +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.js.map +1 -1
- package/dist/server/RpcServerImpl.d.ts +3 -3
- package/dist/server/RpcServerImpl.js +48 -10
- package/dist/server/RpcServerImpl.js.map +1 -1
- package/dist/server/{LocalSubscriptions.d.ts → ServerSubscriptions.d.ts} +11 -5
- package/dist/server/{LocalSubscriptions.js → ServerSubscriptions.js} +24 -7
- package/dist/server/ServerSubscriptions.js.map +1 -0
- package/dist/server/http.js +1 -1
- package/dist/server/http.js.map +1 -1
- package/dist/server/implementation.d.ts +32 -0
- package/dist/server/{local.js → implementation.js} +21 -7
- package/dist/server/implementation.js.map +1 -0
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/RpcClientImpl.ts +2 -2
- package/src/client/index.ts +4 -6
- package/src/client/remote.ts +10 -13
- package/src/index.ts +20 -3
- package/src/server/RpcServerImpl.ts +76 -13
- package/src/server/{LocalSubscriptions.ts → ServerSubscriptions.ts} +40 -10
- package/src/server/http.ts +1 -1
- package/src/server/implementation.ts +116 -0
- package/src/server/index.ts +12 -9
- package/tests/events.ts +82 -0
- package/tests/testUtils.ts +4 -4
- package/dist/server/LocalSubscriptions.js.map +0 -1
- package/dist/server/local.d.ts +0 -15
- package/dist/server/local.js.map +0 -1
- package/src/server/local.ts +0 -80
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":";;;AAAA,sCAAsF;AAGtF,yDAAgD;AAIzC,KAAK,UAAU,eAAe,CACnC,QAAW,EACX,eAA8F;IAM9F,MAAM,OAAO,GAAG;QACd,GAAG,cAAc;QACjB,GAAG,eAAe;KACnB,CAAA;IAED,MAAM,SAAS,GAAG,IAAI,gCAAa,CAAO,QAAQ,EAAE,OAAO,CAAC,CAAA;IAE5D,MAAM,SAAS,CAAC,KAAK,EAAE,CAAA;IAEvB,OAAO;QACL,QAAQ,EAAE,SAAS,CAAC,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":";;;AAAA,sCAAsF;AAGtF,yDAAgD;AAIzC,KAAK,UAAU,eAAe,CACnC,QAAW,EACX,eAA8F;IAM9F,MAAM,OAAO,GAAG;QACd,GAAG,cAAc;QACjB,GAAG,eAAe;KACnB,CAAA;IAED,MAAM,SAAS,GAAG,IAAI,gCAAa,CAAO,QAAQ,EAAE,OAAO,CAAC,CAAA;IAE5D,MAAM,SAAS,CAAC,KAAK,EAAE,CAAA;IAEvB,OAAO;QACL,QAAQ,EAAE,SAAS,CAAC,cAAc;QAClC,MAAM,EAAE,SAAS;QACjB,UAAU,EAAE,SAAS,CAAC,UAAU;KACjC,CAAA;AACH,CAAC;AAtBD,0CAsBC;AA0BD,MAAM,cAAc,GAAqD;IACvE,IAAI,EAAE,EAAE;IACR,IAAI,EAAE,SAAS;IACf,UAAU,EAAE,EAAE;IACd,YAAY,EAAE,EAAE,GAAG,IAAI,EAAE,gCAAgC;IACzD,aAAa,EAAE,IAAI;IAEnB,KAAK,CAAC,uBAAuB,CAC3B,GAAoB,EACpB,GAAmB;QAEnB,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,yBAAgB,CAAC,CAAA;QAE5C,OAAO;YACL,QAAQ,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,MAAM;SACjE,CAAA;IACH,CAAC;CACF,CAAA"}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import {HttpClient} from "./HttpClient.js"
|
|
|
3
3
|
import {RemoteSubscriptions} from "./RemoteSubscriptions.js"
|
|
4
4
|
import {WebSocketConnection} from "./WebSocketConnection.js"
|
|
5
5
|
import {nanoid} from "nanoid"
|
|
6
|
-
import {createRemote,
|
|
6
|
+
import {createRemote, ServicesClient} from "./remote.js"
|
|
7
7
|
import {ConsumeServicesOptions, RpcClient} from "./index.js"
|
|
8
8
|
import {withMiddlewares} from "../utils/middleware.js"
|
|
9
9
|
|
|
@@ -86,7 +86,7 @@ export class RpcClientImpl<S extends Services<S>> implements RpcClient {
|
|
|
86
86
|
return this.connection._webSocket()
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
createRemote():
|
|
89
|
+
createRemote(): ServicesClient<S> {
|
|
90
90
|
return createRemote<S>({
|
|
91
91
|
call: this.call,
|
|
92
92
|
subscribe: this.subscribe,
|
package/src/client/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {RpcContext, Services} from "../rpc.js"
|
|
2
|
-
import {
|
|
2
|
+
import {ServicesClient} from "./remote.js"
|
|
3
3
|
import {RpcClientImpl} from "./RpcClientImpl.js"
|
|
4
4
|
import {Middleware} from "../utils/middleware.js"
|
|
5
5
|
|
|
@@ -41,7 +41,7 @@ export async function consumeServices<S extends Services<S>>(
|
|
|
41
41
|
overrideOptions: Partial<ConsumeServicesOptions> = {},
|
|
42
42
|
): Promise<{
|
|
43
43
|
client: RpcClient
|
|
44
|
-
remote:
|
|
44
|
+
remote: ServicesClient<S>
|
|
45
45
|
}> {
|
|
46
46
|
if (url.endsWith("/")) {
|
|
47
47
|
throw new Error("URL must not end with /")
|
|
@@ -73,10 +73,8 @@ const defaultOptions: ConsumeServicesOptions = {
|
|
|
73
73
|
middleware: [],
|
|
74
74
|
notificationsMiddleware: [],
|
|
75
75
|
connectOnCreate: false,
|
|
76
|
-
onConnected: () => {
|
|
77
|
-
},
|
|
78
|
-
onDisconnected: () => {
|
|
79
|
-
},
|
|
76
|
+
onConnected: () => {},
|
|
77
|
+
onDisconnected: () => {},
|
|
80
78
|
getHeaders: async () => ({}),
|
|
81
79
|
|
|
82
80
|
getSubscriptionsUrl(url: string): string {
|
package/src/client/remote.ts
CHANGED
|
@@ -3,7 +3,7 @@ import {CallOptions, Consumer, RemoteFunction, Services} from "../rpc.js"
|
|
|
3
3
|
export function createRemote<S extends Services<S>>(
|
|
4
4
|
hooks: RemoteHooks,
|
|
5
5
|
name = "",
|
|
6
|
-
):
|
|
6
|
+
): ServicesClient<S> {
|
|
7
7
|
// start with remote function
|
|
8
8
|
const remoteItem = (...paramsWithCallOptions: unknown[]) => {
|
|
9
9
|
const {params, callOptions} = extractCallOptions(paramsWithCallOptions)
|
|
@@ -99,20 +99,17 @@ export type AddParameters<
|
|
|
99
99
|
TParameters extends [...args: any],
|
|
100
100
|
> = (...args: [...Parameters<TFunction>, ...TParameters]) => ReturnType<TFunction>
|
|
101
101
|
|
|
102
|
-
export type
|
|
102
|
+
export type FunctionClient<F extends RemoteFunction> = {
|
|
103
|
+
subscribe(consumer: Consumer<F>, ...parameters: [...Parameters<F>, CallOptions?]): Promise<void>
|
|
104
|
+
unsubscribe(consumer: Consumer<F>, ...parameters: [...Parameters<F>, CallOptions?]): Promise<void>
|
|
105
|
+
itemName: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type ServicesClient<T extends Services<T>> = {
|
|
103
109
|
[K in keyof T]: T[K] extends RemoteFunction
|
|
104
|
-
? AddParameters<T[K], [CallOptions?]> &
|
|
105
|
-
subscribe(
|
|
106
|
-
consumer: Consumer<T[K]>,
|
|
107
|
-
...parameters: [...Parameters<T[K]>, CallOptions?]
|
|
108
|
-
): Promise<void>
|
|
109
|
-
unsubscribe(
|
|
110
|
-
consumer: Consumer<T[K]>,
|
|
111
|
-
...parameters: [...Parameters<T[K]>, CallOptions?]
|
|
112
|
-
): Promise<void>
|
|
113
|
-
}
|
|
110
|
+
? AddParameters<T[K], [CallOptions?]> & FunctionClient<T[K]>
|
|
114
111
|
: T[K] extends object
|
|
115
|
-
?
|
|
112
|
+
? ServicesClient<T[K]>
|
|
116
113
|
: never
|
|
117
114
|
}
|
|
118
115
|
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import {ServicesClient} from "./client/remote"
|
|
2
|
+
|
|
3
|
+
import type {Services} from "./rpc.js"
|
|
4
|
+
import type {ServicesImplementation} from "./server/implementation.js"
|
|
5
|
+
|
|
6
|
+
export {Services}
|
|
7
|
+
|
|
8
|
+
export type {RemoteFunction, Consumer, RpcContext, RpcConnectionContext} from "./rpc.js"
|
|
2
9
|
export {RpcError, RpcErrors, CallOptions} from "./rpc.js"
|
|
3
10
|
|
|
4
11
|
export type {Middleware} from "./utils/middleware.js"
|
|
@@ -8,12 +15,22 @@ export type {RpcServer, PublishServicesOptions} from "./server/index.js"
|
|
|
8
15
|
export type {HttpServerHooks} from "./server/http.js"
|
|
9
16
|
export {publishServices} from "./server/index.js"
|
|
10
17
|
|
|
11
|
-
export type {
|
|
18
|
+
export type {
|
|
19
|
+
FunctionImplementation,
|
|
20
|
+
ThrottleSettings,
|
|
21
|
+
SubscribeEvent,
|
|
22
|
+
UnsubscribeEvent,
|
|
23
|
+
} from "./server/implementation.js"
|
|
24
|
+
|
|
25
|
+
export {ServicesImplementation}
|
|
12
26
|
|
|
13
27
|
export type {RpcClient, ConsumeServicesOptions, ClientCache} from "./client/index.js"
|
|
14
28
|
export {consumeServices} from "./client/index.js"
|
|
15
29
|
|
|
16
|
-
export type {
|
|
30
|
+
export type {ServicesClient, FunctionClient, AddParameters} from "./client/remote.js"
|
|
17
31
|
|
|
18
32
|
export {log, setLogger} from "./logger.js"
|
|
19
33
|
export {safeStringify, safeParseJson} from "./utils/json.js"
|
|
34
|
+
export type {ExtractPromiseResult} from "./utils/types.js"
|
|
35
|
+
|
|
36
|
+
export type UnifiedServices<T extends Services<T>> = ServicesImplementation<T> & ServicesClient<T>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {PublishServicesOptions, RpcServer} from "./index.js"
|
|
2
|
-
import {
|
|
2
|
+
import {ServerSubscriptions} from "./ServerSubscriptions.js"
|
|
3
3
|
import http from "http"
|
|
4
4
|
import type {ConnectionsServer} from "./ConnectionsServer.js"
|
|
5
5
|
import {serveHttpRequest} from "./http.js"
|
|
@@ -14,14 +14,21 @@ import {
|
|
|
14
14
|
} from "../rpc.js"
|
|
15
15
|
import {log} from "../logger.js"
|
|
16
16
|
import {withMiddlewares} from "../utils/middleware.js"
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
eventEmitterSymbol,
|
|
19
|
+
prepareImplementation,
|
|
20
|
+
ServicesImplementation,
|
|
21
|
+
} from "./implementation.js"
|
|
18
22
|
import {safeParseJson, safeStringify} from "../utils/json.js"
|
|
23
|
+
import EventEmitter from "node:events"
|
|
19
24
|
|
|
20
25
|
export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implements RpcServer {
|
|
21
26
|
constructor(
|
|
22
27
|
private readonly services: S,
|
|
23
28
|
private readonly options: PublishServicesOptions<C>,
|
|
24
29
|
) {
|
|
30
|
+
this.implementation = prepareImplementation(this.localSubscriptions, services)
|
|
31
|
+
|
|
25
32
|
if ("server" in this.options) {
|
|
26
33
|
this.httpServer = this.options.server
|
|
27
34
|
} else {
|
|
@@ -63,7 +70,19 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
|
|
|
63
70
|
this.httpServer,
|
|
64
71
|
{pingInterval: this.options.pingInterval, path: this.options.path},
|
|
65
72
|
(clientId) => {
|
|
66
|
-
this.localSubscriptions.unsubscribeAll(clientId)
|
|
73
|
+
const unsubscribed = this.localSubscriptions.unsubscribeAll(clientId)
|
|
74
|
+
|
|
75
|
+
for (const {itemName, parameters, clientId} of unsubscribed) {
|
|
76
|
+
const item = this.getRemoteFunction(itemName)
|
|
77
|
+
|
|
78
|
+
if (item) {
|
|
79
|
+
item.eventEmitter.emit("unsubscribe", {
|
|
80
|
+
itemName,
|
|
81
|
+
filter: parameters[0],
|
|
82
|
+
clientId,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
67
86
|
},
|
|
68
87
|
!("server" in this.options),
|
|
69
88
|
)
|
|
@@ -104,17 +123,15 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
|
|
|
104
123
|
})
|
|
105
124
|
}
|
|
106
125
|
|
|
107
|
-
createServicesWithTriggers(): ServicesWithTriggers<S> {
|
|
108
|
-
return withTriggers(this.localSubscriptions, this.services)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
126
|
_allSubscriptions() {
|
|
112
127
|
return this.localSubscriptions._allSubscriptions()
|
|
113
128
|
}
|
|
114
129
|
|
|
115
|
-
private readonly localSubscriptions = new
|
|
130
|
+
private readonly localSubscriptions = new ServerSubscriptions()
|
|
116
131
|
private connectionsServer: ConnectionsServer | null = null
|
|
117
|
-
|
|
132
|
+
|
|
133
|
+
public readonly httpServer: http.Server
|
|
134
|
+
public readonly implementation: ServicesImplementation<S>
|
|
118
135
|
|
|
119
136
|
private call = async (
|
|
120
137
|
connectionContext: RpcConnectionContext,
|
|
@@ -187,7 +204,21 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
|
|
|
187
204
|
})
|
|
188
205
|
|
|
189
206
|
if (this.connectionsServer?.isClientSubscribed(connectionContext.clientId)) {
|
|
190
|
-
this.localSubscriptions.subscribe(
|
|
207
|
+
const subscriptionAdded = this.localSubscriptions.subscribe(
|
|
208
|
+
connectionContext.clientId,
|
|
209
|
+
itemName,
|
|
210
|
+
parameters,
|
|
211
|
+
update,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if (subscriptionAdded) {
|
|
215
|
+
item.eventEmitter.emit("subscribe", {
|
|
216
|
+
itemName,
|
|
217
|
+
filter: parameters[0],
|
|
218
|
+
clientId: connectionContext.clientId,
|
|
219
|
+
context: connectionContext,
|
|
220
|
+
})
|
|
221
|
+
}
|
|
191
222
|
}
|
|
192
223
|
|
|
193
224
|
const lastData = await this.invokeLocalFunction(
|
|
@@ -201,7 +232,19 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
|
|
|
201
232
|
|
|
202
233
|
return lastData
|
|
203
234
|
} catch (e) {
|
|
204
|
-
this.localSubscriptions.unsubscribe(
|
|
235
|
+
const unsubscribed = this.localSubscriptions.unsubscribe(
|
|
236
|
+
connectionContext.clientId,
|
|
237
|
+
itemName,
|
|
238
|
+
parameters,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if (unsubscribed) {
|
|
242
|
+
item.eventEmitter.emit("subscribe", {
|
|
243
|
+
itemName,
|
|
244
|
+
filter: parameters[0],
|
|
245
|
+
clientId: connectionContext.clientId,
|
|
246
|
+
})
|
|
247
|
+
}
|
|
205
248
|
|
|
206
249
|
log.error(`Failed to subscribe ${itemName}`, e)
|
|
207
250
|
throw e
|
|
@@ -214,17 +257,34 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
|
|
|
214
257
|
parameters: unknown[],
|
|
215
258
|
) => {
|
|
216
259
|
try {
|
|
217
|
-
this.localSubscriptions.unsubscribe(
|
|
260
|
+
const unsubscribed = this.localSubscriptions.unsubscribe(
|
|
261
|
+
connectionContext.clientId,
|
|
262
|
+
itemName,
|
|
263
|
+
parameters,
|
|
264
|
+
)
|
|
265
|
+
if (unsubscribed) {
|
|
266
|
+
const item = this.getRemoteFunction(itemName)
|
|
267
|
+
|
|
268
|
+
if (item) {
|
|
269
|
+
item.eventEmitter.emit("unsubscribe", {
|
|
270
|
+
itemName,
|
|
271
|
+
filter: parameters[0],
|
|
272
|
+
clientId: connectionContext.clientId,
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
}
|
|
218
276
|
} catch (e) {
|
|
219
277
|
log.error(`Failed to unsubscribe ${itemName}`, e)
|
|
220
278
|
throw e
|
|
221
279
|
}
|
|
222
280
|
}
|
|
223
281
|
|
|
282
|
+
// getRemoteFunction should only take functions from services, and not from implementation in
|
|
283
|
+
// order to not take .trigger and other implementation-specific details
|
|
224
284
|
private getRemoteFunction(
|
|
225
285
|
itemName: string,
|
|
226
286
|
root: any = this.services,
|
|
227
|
-
): {function: RemoteFunction; container: any} | undefined {
|
|
287
|
+
): {function: RemoteFunction; eventEmitter: EventEmitter; container: any} | undefined {
|
|
228
288
|
const parts = itemName.split("/")
|
|
229
289
|
|
|
230
290
|
let item = root
|
|
@@ -239,8 +299,11 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
|
|
|
239
299
|
|
|
240
300
|
if (!item) return undefined
|
|
241
301
|
|
|
302
|
+
item[eventEmitterSymbol] = item[eventEmitterSymbol] ?? new EventEmitter()
|
|
303
|
+
|
|
242
304
|
return {
|
|
243
305
|
function: item,
|
|
306
|
+
eventEmitter: item[eventEmitterSymbol],
|
|
244
307
|
container: parent,
|
|
245
308
|
}
|
|
246
309
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {safeStringify} from "../utils/json.js"
|
|
2
2
|
import {lastValueReducer, throttle} from "../utils/throttle.js"
|
|
3
|
-
import {ThrottleSettings} from "./
|
|
3
|
+
import {ThrottleSettings} from "./implementation.js"
|
|
4
4
|
|
|
5
|
-
export class
|
|
5
|
+
export class ServerSubscriptions {
|
|
6
6
|
subscribe(
|
|
7
7
|
clientId: string,
|
|
8
8
|
itemName: string,
|
|
9
9
|
parameters: unknown[],
|
|
10
10
|
update: (suppliedData?: unknown) => void,
|
|
11
|
-
) {
|
|
11
|
+
): boolean {
|
|
12
12
|
const itemSubscriptions = this.byItem.get(itemName) || {byFilter: new Map()}
|
|
13
13
|
this.byItem.set(itemName, itemSubscriptions)
|
|
14
14
|
|
|
@@ -22,22 +22,30 @@ export class LocalSubscriptions {
|
|
|
22
22
|
|
|
23
23
|
if (!subscriptions.subscribedClients.some((c) => c.clientId == clientId)) {
|
|
24
24
|
subscriptions.subscribedClients.push({clientId, update})
|
|
25
|
+
|
|
26
|
+
return true
|
|
25
27
|
}
|
|
28
|
+
|
|
29
|
+
return false
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
unsubscribe(clientId: string, itemName: string, parameters: unknown[]) {
|
|
32
|
+
unsubscribe(clientId: string, itemName: string, parameters: unknown[]): boolean {
|
|
29
33
|
const itemSubscriptions = this.byItem.get(itemName)
|
|
30
|
-
if (!itemSubscriptions) return
|
|
34
|
+
if (!itemSubscriptions) return false
|
|
31
35
|
|
|
32
36
|
const filterKey = getFilterKey(parameters)
|
|
33
37
|
|
|
34
38
|
const subscriptions = itemSubscriptions.byFilter.get(filterKey)
|
|
35
|
-
if (!subscriptions) return
|
|
39
|
+
if (!subscriptions) return false
|
|
40
|
+
|
|
41
|
+
const oldLength = subscriptions.subscribedClients.length
|
|
36
42
|
|
|
37
43
|
subscriptions.subscribedClients = subscriptions.subscribedClients.filter(
|
|
38
44
|
(subscription) => subscription.clientId != clientId,
|
|
39
45
|
)
|
|
40
46
|
|
|
47
|
+
const unsubscribed = oldLength != subscriptions.subscribedClients.length
|
|
48
|
+
|
|
41
49
|
if (!subscriptions.subscribedClients.length) {
|
|
42
50
|
itemSubscriptions.byFilter.delete(filterKey)
|
|
43
51
|
|
|
@@ -45,14 +53,28 @@ export class LocalSubscriptions {
|
|
|
45
53
|
this.byItem.delete(itemName)
|
|
46
54
|
}
|
|
47
55
|
}
|
|
56
|
+
|
|
57
|
+
return unsubscribed
|
|
48
58
|
}
|
|
49
59
|
|
|
50
|
-
unsubscribeAll(clientId: string) {
|
|
60
|
+
unsubscribeAll(clientId: string): Subscribed[] {
|
|
61
|
+
const r: Subscribed[] = []
|
|
62
|
+
|
|
51
63
|
for (const [itemName, itemSubscriptions] of this.byItem.entries()) {
|
|
52
64
|
for (const [filterKey, subscriptions] of itemSubscriptions.byFilter.entries()) {
|
|
53
|
-
subscriptions.subscribedClients = subscriptions.subscribedClients.filter(
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
subscriptions.subscribedClients = subscriptions.subscribedClients.filter((subscription) => {
|
|
66
|
+
const evict = subscription.clientId == clientId
|
|
67
|
+
|
|
68
|
+
if (evict) {
|
|
69
|
+
r.push({
|
|
70
|
+
itemName,
|
|
71
|
+
parameters: [subscriptions.filter],
|
|
72
|
+
clientId: clientId,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return !evict
|
|
77
|
+
})
|
|
56
78
|
|
|
57
79
|
if (!subscriptions.subscribedClients.length) {
|
|
58
80
|
itemSubscriptions.byFilter.delete(filterKey)
|
|
@@ -63,6 +85,8 @@ export class LocalSubscriptions {
|
|
|
63
85
|
this.byItem.delete(itemName)
|
|
64
86
|
}
|
|
65
87
|
}
|
|
88
|
+
|
|
89
|
+
return r
|
|
66
90
|
}
|
|
67
91
|
|
|
68
92
|
trigger(itemName: string, triggerFilter: Record<string, unknown> = {}, suppliedData?: unknown) {
|
|
@@ -153,3 +177,9 @@ function filterContains(
|
|
|
153
177
|
function getFilterKey(parameters: unknown[]) {
|
|
154
178
|
return safeStringify(parameters?.[0] ?? null)
|
|
155
179
|
}
|
|
180
|
+
|
|
181
|
+
type Subscribed = {
|
|
182
|
+
itemName: string
|
|
183
|
+
parameters: unknown[]
|
|
184
|
+
clientId: string
|
|
185
|
+
}
|
package/src/server/http.ts
CHANGED
|
@@ -16,7 +16,7 @@ export async function serveHttpRequest(
|
|
|
16
16
|
) => Promise<RpcConnectionContext>,
|
|
17
17
|
) {
|
|
18
18
|
// if port is in options - response 404 on other URLs
|
|
19
|
-
//
|
|
19
|
+
// otherwise simply handle request
|
|
20
20
|
|
|
21
21
|
if (req.url?.startsWith(path)) {
|
|
22
22
|
try {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {RemoteFunction, RpcConnectionContext, Services} from "../rpc.js"
|
|
2
|
+
import {ServerSubscriptions} from "./ServerSubscriptions"
|
|
3
|
+
import {ExtractPromiseResult} from "../utils/types.js"
|
|
4
|
+
import {ThrottleArgsReducer} from "../utils/throttle.js"
|
|
5
|
+
import EventEmitter from "node:events"
|
|
6
|
+
|
|
7
|
+
export const eventEmitterSymbol = Symbol("eventEmitter")
|
|
8
|
+
|
|
9
|
+
export type ThrottleSettings<D> = {
|
|
10
|
+
timeout: number
|
|
11
|
+
reducer?: ThrottleArgsReducer<D>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function prepareImplementation<T extends Services<T>>(
|
|
15
|
+
serverSubscriptions: ServerSubscriptions,
|
|
16
|
+
services: T,
|
|
17
|
+
name = "",
|
|
18
|
+
): ServicesImplementation<T> {
|
|
19
|
+
const cachedItems: any = {}
|
|
20
|
+
const skippedProps = ["length", "name", "prototype", "arguments", "caller"]
|
|
21
|
+
|
|
22
|
+
return new Proxy(services, {
|
|
23
|
+
get(target: any, propName: string) {
|
|
24
|
+
// skip internal props
|
|
25
|
+
if (typeof propName != "string") return target[propName]
|
|
26
|
+
|
|
27
|
+
// skip other system props
|
|
28
|
+
if (["then", "catch", "toJSON", ...skippedProps].includes(propName)) return target[propName]
|
|
29
|
+
|
|
30
|
+
const itemName = name ? name + "/" + propName : propName
|
|
31
|
+
|
|
32
|
+
if (typeof target[propName] == "function") {
|
|
33
|
+
const delegate = (...params: unknown[]) => {
|
|
34
|
+
return target[propName](...params)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// bind eventEmitter to source services, so it can be looked up from getRemoteFunction
|
|
38
|
+
target[propName][eventEmitterSymbol] =
|
|
39
|
+
target[propName][eventEmitterSymbol] ?? new EventEmitter()
|
|
40
|
+
|
|
41
|
+
delegate.trigger = (filter: Record<string, unknown> = {}, suppliedData?: unknown) => {
|
|
42
|
+
// triggers are delayed for consumers to receive updates after the current call ends.
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
serverSubscriptions.trigger(itemName, filter, suppliedData)
|
|
45
|
+
}, 0)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
delegate.throttle = (settings: ThrottleSettings<unknown>) => {
|
|
49
|
+
serverSubscriptions.throttleItem(itemName, settings)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
delegate.addEventListener = (eventName: string, listener: (event: unknown) => void) => {
|
|
53
|
+
target[propName][eventEmitterSymbol].addListener(eventName, listener)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
delegate.removeEventListener = (eventName: string, listener: (event: unknown) => void) => {
|
|
57
|
+
target[propName][eventEmitterSymbol].removeListener(eventName, listener)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return delegate
|
|
61
|
+
} else if (!cachedItems[propName]) {
|
|
62
|
+
cachedItems[propName] = prepareImplementation(
|
|
63
|
+
serverSubscriptions,
|
|
64
|
+
services[propName as keyof T] as any,
|
|
65
|
+
itemName,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return cachedItems[propName]
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
set(target, propName, value) {
|
|
73
|
+
cachedItems[propName] = value
|
|
74
|
+
return true
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
ownKeys() {
|
|
78
|
+
return [...skippedProps, ...Object.keys(cachedItems)]
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type SubscribeEvent<F extends RemoteFunction> = {
|
|
84
|
+
itemName: string
|
|
85
|
+
filter: Parameters<F>[0]
|
|
86
|
+
clientId: string
|
|
87
|
+
context: RpcConnectionContext
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// UnsubscribeEVent cannot contain ctx b/c ctx won't be available in case of disconnecting entire client
|
|
91
|
+
export type UnsubscribeEvent<F extends RemoteFunction> = {
|
|
92
|
+
itemName: string
|
|
93
|
+
filter: Parameters<F>[0]
|
|
94
|
+
clientId: string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type FunctionImplementation<F extends RemoteFunction> = {
|
|
98
|
+
trigger(
|
|
99
|
+
filter?: Partial<Parameters<F>[0]>,
|
|
100
|
+
suppliedData?: ExtractPromiseResult<ReturnType<F>>,
|
|
101
|
+
): void
|
|
102
|
+
throttle(settings: ThrottleSettings<ExtractPromiseResult<ReturnType<F>>>): void
|
|
103
|
+
|
|
104
|
+
addEventListener(event: "subscribe", listener: (event: SubscribeEvent<F>) => void): void
|
|
105
|
+
addEventListener(event: "unsubscribe", listener: (event: UnsubscribeEvent<F>) => void): void
|
|
106
|
+
removeEventListener(event: "subscribe", listener: (event: SubscribeEvent<F>) => void): void
|
|
107
|
+
removeEventListener(event: "unsubscribe", listener: (event: UnsubscribeEvent<F>) => void): void
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type ServicesImplementation<T extends Services<T>> = {
|
|
111
|
+
[K in keyof T]: T[K] extends RemoteFunction
|
|
112
|
+
? T[K] & FunctionImplementation<T[K]>
|
|
113
|
+
: T[K] extends object
|
|
114
|
+
? ServicesImplementation<T[K]>
|
|
115
|
+
: never
|
|
116
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {CLIENT_ID_HEADER, RpcConnectionContext, RpcContext, Services} from "../rpc.js"
|
|
2
|
-
import {
|
|
2
|
+
import {ServicesImplementation} from "./implementation.js"
|
|
3
3
|
import {Middleware} from "../utils/middleware.js"
|
|
4
4
|
import {RpcServerImpl} from "./RpcServerImpl.js"
|
|
5
5
|
import http, {IncomingMessage, ServerResponse} from "http"
|
|
@@ -10,7 +10,7 @@ export async function publishServices<S extends Services<S>, C extends RpcContex
|
|
|
10
10
|
overrideOptions: Partial<PublishServicesOptions<C>> & ({port: number} | {server: http.Server}),
|
|
11
11
|
): Promise<{
|
|
12
12
|
server: RpcServer
|
|
13
|
-
services:
|
|
13
|
+
services: ServicesImplementation<S>
|
|
14
14
|
httpServer: http.Server
|
|
15
15
|
}> {
|
|
16
16
|
const options = {
|
|
@@ -23,7 +23,7 @@ export async function publishServices<S extends Services<S>, C extends RpcContex
|
|
|
23
23
|
await rpcServer.start()
|
|
24
24
|
|
|
25
25
|
return {
|
|
26
|
-
services: rpcServer.
|
|
26
|
+
services: rpcServer.implementation,
|
|
27
27
|
server: rpcServer,
|
|
28
28
|
httpServer: rpcServer.httpServer,
|
|
29
29
|
}
|
|
@@ -45,13 +45,13 @@ export type PublishServicesOptions<C extends RpcContext> = {
|
|
|
45
45
|
createServerHooks?(hooks: HttpServerHooks, req: IncomingMessage): HttpServerHooks
|
|
46
46
|
} & (
|
|
47
47
|
| {
|
|
48
|
-
|
|
49
|
-
}
|
|
48
|
+
server: http.Server
|
|
49
|
+
}
|
|
50
50
|
| {
|
|
51
|
-
|
|
52
|
-
}
|
|
51
|
+
port: number
|
|
52
|
+
}
|
|
53
53
|
| {}
|
|
54
|
-
|
|
54
|
+
)
|
|
55
55
|
|
|
56
56
|
const defaultOptions: Omit<PublishServicesOptions<RpcContext>, "port"> = {
|
|
57
57
|
path: "",
|
|
@@ -60,7 +60,10 @@ const defaultOptions: Omit<PublishServicesOptions<RpcContext>, "port"> = {
|
|
|
60
60
|
pingInterval: 30 * 1000, // should be in-sync with client
|
|
61
61
|
subscriptions: true,
|
|
62
62
|
|
|
63
|
-
async createConnectionContext(
|
|
63
|
+
async createConnectionContext(
|
|
64
|
+
req: IncomingMessage,
|
|
65
|
+
res: ServerResponse,
|
|
66
|
+
): Promise<RpcConnectionContext> {
|
|
64
67
|
const header = req.headers[CLIENT_ID_HEADER]
|
|
65
68
|
|
|
66
69
|
return {
|
package/tests/events.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {createTestClient, startTestServer, testClient} from "./testUtils.js"
|
|
2
|
+
import {assert} from "chai"
|
|
3
|
+
import {adelay} from "../src/utils/promises.js"
|
|
4
|
+
|
|
5
|
+
describe("Events", () => {
|
|
6
|
+
it("subscribe event", async () => {
|
|
7
|
+
const services = await startTestServer({
|
|
8
|
+
test: {
|
|
9
|
+
item: async (a: string) => {},
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
let event
|
|
14
|
+
services.test.item.addEventListener("subscribe", (e) => {
|
|
15
|
+
event = e
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const client = await createTestClient<typeof services>()
|
|
19
|
+
|
|
20
|
+
await client.test.item.subscribe(() => {}, "a")
|
|
21
|
+
|
|
22
|
+
assert.deepEqual(event, {
|
|
23
|
+
itemName: "test/item",
|
|
24
|
+
filter: "a",
|
|
25
|
+
clientId: testClient!.clientId,
|
|
26
|
+
context: {
|
|
27
|
+
clientId: testClient!.clientId,
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
it("unsubscribe event", async () => {
|
|
32
|
+
const services = await startTestServer({
|
|
33
|
+
test: {
|
|
34
|
+
item: async (a: string) => {},
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
let event
|
|
39
|
+
services.test.item.addEventListener("unsubscribe", (e) => {
|
|
40
|
+
event = e
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const client = await createTestClient<typeof services>()
|
|
44
|
+
|
|
45
|
+
const listener = () => {}
|
|
46
|
+
await client.test.item.subscribe(listener, "a")
|
|
47
|
+
await client.test.item.unsubscribe(listener, "a")
|
|
48
|
+
|
|
49
|
+
assert.deepEqual(event, {
|
|
50
|
+
itemName: "test/item",
|
|
51
|
+
filter: "a",
|
|
52
|
+
clientId: testClient!.clientId,
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
it("unsubscribe on disconnect WS", async () => {
|
|
56
|
+
const services = await startTestServer({
|
|
57
|
+
test: {
|
|
58
|
+
item: async (a: string) => {},
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
let event
|
|
63
|
+
services.test.item.addEventListener("unsubscribe", (e) => {
|
|
64
|
+
event = e
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const client = await createTestClient<typeof services>()
|
|
68
|
+
|
|
69
|
+
const listener = () => {}
|
|
70
|
+
await client.test.item.subscribe(listener, "a")
|
|
71
|
+
|
|
72
|
+
await testClient!.close()
|
|
73
|
+
|
|
74
|
+
await adelay(100)
|
|
75
|
+
|
|
76
|
+
assert.deepEqual(event, {
|
|
77
|
+
itemName: "test/item",
|
|
78
|
+
filter: "a",
|
|
79
|
+
clientId: testClient!.clientId,
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
})
|