@push-rpc/next 2.0.24 → 2.0.26

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 (43) hide show
  1. package/dist/client/HttpClient.js +3 -1
  2. package/dist/client/HttpClient.js.map +1 -1
  3. package/dist/client/RpcClientImpl.d.ts +2 -2
  4. package/dist/client/RpcClientImpl.js.map +1 -1
  5. package/dist/client/index.d.ts +2 -2
  6. package/dist/client/index.js +2 -4
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/client/remote.d.ts +8 -6
  9. package/dist/client/remote.js.map +1 -1
  10. package/dist/index.d.ts +10 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/server/RpcServerImpl.d.ts +3 -3
  13. package/dist/server/RpcServerImpl.js +48 -10
  14. package/dist/server/RpcServerImpl.js.map +1 -1
  15. package/dist/server/{LocalSubscriptions.d.ts → ServerSubscriptions.d.ts} +11 -5
  16. package/dist/server/{LocalSubscriptions.js → ServerSubscriptions.js} +24 -7
  17. package/dist/server/ServerSubscriptions.js.map +1 -0
  18. package/dist/server/http.js +3 -3
  19. package/dist/server/http.js.map +1 -1
  20. package/dist/server/implementation.d.ts +32 -0
  21. package/dist/server/{local.js → implementation.js} +21 -7
  22. package/dist/server/implementation.js.map +1 -0
  23. package/dist/server/index.d.ts +2 -2
  24. package/dist/server/index.js +1 -1
  25. package/dist/server/index.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/client/HttpClient.ts +5 -1
  28. package/src/client/RpcClientImpl.ts +2 -2
  29. package/src/client/index.ts +4 -6
  30. package/src/client/remote.ts +10 -13
  31. package/src/index.ts +20 -3
  32. package/src/server/RpcServerImpl.ts +76 -13
  33. package/src/server/{LocalSubscriptions.ts → ServerSubscriptions.ts} +40 -10
  34. package/src/server/http.ts +3 -3
  35. package/src/server/implementation.ts +116 -0
  36. package/src/server/index.ts +12 -9
  37. package/tests/calls.ts +21 -0
  38. package/tests/events.ts +82 -0
  39. package/tests/testUtils.ts +4 -4
  40. package/dist/server/LocalSubscriptions.js.map +0 -1
  41. package/dist/server/local.d.ts +0 -15
  42. package/dist/server/local.js.map +0 -1
  43. package/src/server/local.ts +0 -80
@@ -0,0 +1 @@
1
+ {"version":3,"file":"implementation.js","sourceRoot":"","sources":["../../src/server/implementation.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  import { RpcConnectionContext, RpcContext, Services } from "../rpc.js";
3
- import { ServicesWithTriggers } from "./local.js";
3
+ import { ServicesImplementation } from "./implementation.js";
4
4
  import { Middleware } from "../utils/middleware.js";
5
5
  import http, { IncomingMessage, ServerResponse } from "http";
6
6
  import { HttpServerHooks } from "./http";
@@ -10,7 +10,7 @@ export declare function publishServices<S extends Services<S>, C extends RpcCont
10
10
  server: http.Server;
11
11
  })): Promise<{
12
12
  server: RpcServer;
13
- services: ServicesWithTriggers<S>;
13
+ services: ServicesImplementation<S>;
14
14
  httpServer: http.Server;
15
15
  }>;
16
16
  export type RpcServer = {
@@ -11,7 +11,7 @@ async function publishServices(services, overrideOptions) {
11
11
  const rpcServer = new RpcServerImpl_js_1.RpcServerImpl(services, options);
12
12
  await rpcServer.start();
13
13
  return {
14
- services: rpcServer.createServicesWithTriggers(),
14
+ services: rpcServer.implementation,
15
15
  server: rpcServer,
16
16
  httpServer: rpcServer.httpServer,
17
17
  };
@@ -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,0BAA0B,EAAE;QAChD,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,CAAC,GAAoB,EAAE,GAAmB;QACrE,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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push-rpc/next",
3
- "version": "2.0.24",
3
+ "version": "2.0.26",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {
@@ -63,7 +63,11 @@ export class HttpClient {
63
63
  contentType && contentType.includes("application/json") ? safeParseJson(text) : text
64
64
 
65
65
  if (hasError || response.status < 200 || response.status >= 300) {
66
- const error = new Error(response.headers.get("x-error") ?? undefined)
66
+ const error = new Error(
67
+ !!response.headers.get("x-error")
68
+ ? decodeURIComponent(response.headers.get("x-error")!!)
69
+ : undefined,
70
+ )
67
71
 
68
72
  Object.assign(error, {code: response.status})
69
73
 
@@ -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, ServicesWithSubscriptions} from "./remote.js"
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(): ServicesWithSubscriptions<S> {
89
+ createRemote(): ServicesClient<S> {
90
90
  return createRemote<S>({
91
91
  call: this.call,
92
92
  subscribe: this.subscribe,
@@ -1,5 +1,5 @@
1
1
  import {RpcContext, Services} from "../rpc.js"
2
- import {ServicesWithSubscriptions} from "./remote.js"
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: ServicesWithSubscriptions<S>
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 {
@@ -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
- ): ServicesWithSubscriptions<S> {
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 ServicesWithSubscriptions<T extends Services<T>> = {
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
- ? ServicesWithSubscriptions<T[K]>
112
+ ? ServicesClient<T[K]>
116
113
  : never
117
114
  }
118
115
 
package/src/index.ts CHANGED
@@ -1,4 +1,11 @@
1
- export type {RemoteFunction, Services, Consumer, RpcContext, RpcConnectionContext} from "./rpc.js"
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 {ServicesWithTriggers} from "./server/local.js"
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 {ServicesWithSubscriptions} from "./client/remote.js"
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 {LocalSubscriptions} from "./LocalSubscriptions.js"
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 {ServicesWithTriggers, withTriggers} from "./local.js"
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 LocalSubscriptions()
130
+ private readonly localSubscriptions = new ServerSubscriptions()
116
131
  private connectionsServer: ConnectionsServer | null = null
117
- readonly httpServer
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(connectionContext.clientId, itemName, parameters, update)
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(connectionContext.clientId, itemName, parameters)
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(connectionContext.clientId, itemName, parameters)
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 "./local.js"
3
+ import {ThrottleSettings} from "./implementation.js"
4
4
 
5
- export class LocalSubscriptions {
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
- (subscription) => subscription.clientId != clientId,
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
+ }
@@ -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
- // oherwise just handle request
19
+ // otherwise simply handle request
20
20
 
21
21
  if (req.url?.startsWith(path)) {
22
22
  try {
@@ -70,7 +70,7 @@ export async function serveHttpRequest(
70
70
  if (e.code && typeof e.code == "number" && e.code >= 100 && e.code < 600) {
71
71
  res.statusCode = e.code
72
72
 
73
- res.setHeader("X-Error", e["message"] ?? "")
73
+ res.setHeader("X-Error", encodeURIComponent(e["message"] ?? ""))
74
74
  const {code, message, stack, ...rest} = e
75
75
  if (Object.keys(rest).length > 0) {
76
76
  res.setHeader("Content-Type", "application/json")
@@ -83,7 +83,7 @@ export async function serveHttpRequest(
83
83
 
84
84
  res.statusCode = 500
85
85
  if (e["message"]) {
86
- res.setHeader("X-Error", e["message"])
86
+ res.setHeader("X-Error", encodeURIComponent(e["message"]))
87
87
  }
88
88
  res.end()
89
89
  return
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  import {CLIENT_ID_HEADER, RpcConnectionContext, RpcContext, Services} from "../rpc.js"
2
- import {ServicesWithTriggers} from "./local.js"
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: ServicesWithTriggers<S>
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.createServicesWithTriggers(),
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
- server: http.Server
49
- }
48
+ server: http.Server
49
+ }
50
50
  | {
51
- port: number
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(req: IncomingMessage, res: ServerResponse): Promise<RpcConnectionContext> {
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 {