@push-rpc/next 2.0.19 → 2.0.21

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.
@@ -2,7 +2,6 @@ import {PublishServicesOptions, RpcServer} from "./index.js"
2
2
  import {LocalSubscriptions} from "./LocalSubscriptions.js"
3
3
  import http from "http"
4
4
  import type {ConnectionsServer} from "./ConnectionsServer.js"
5
- import {PromiseCache} from "../utils/promises.js"
6
5
  import {serveHttpRequest} from "./http.js"
7
6
  import {
8
7
  InvocationType,
@@ -21,7 +20,7 @@ import {safeParseJson, safeStringify} from "../utils/json.js"
21
20
  export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implements RpcServer {
22
21
  constructor(
23
22
  private readonly services: S,
24
- private readonly options: PublishServicesOptions<C>
23
+ private readonly options: PublishServicesOptions<C>,
25
24
  ) {
26
25
  if ("server" in this.options) {
27
26
  this.httpServer = this.options.server
@@ -50,7 +49,7 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
50
49
  res,
51
50
  options.path,
52
51
  options.createServerHooks ? options.createServerHooks(hooks, req) : hooks,
53
- options.createConnectionContext
52
+ options.createConnectionContext,
54
53
  ).catch((e) => {
55
54
  log.warn("Unhandled error serving HTTP request", e)
56
55
  })
@@ -66,7 +65,7 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
66
65
  (clientId) => {
67
66
  this.localSubscriptions.unsubscribeAll(clientId)
68
67
  },
69
- !("server" in this.options)
68
+ !("server" in this.options),
70
69
  )
71
70
  }
72
71
 
@@ -114,14 +113,13 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
114
113
  }
115
114
 
116
115
  private readonly localSubscriptions = new LocalSubscriptions()
117
- private readonly invocationCache = new PromiseCache()
118
116
  private connectionsServer: ConnectionsServer | null = null
119
117
  readonly httpServer
120
118
 
121
119
  private call = async (
122
120
  connectionContext: RpcConnectionContext,
123
121
  itemName: string,
124
- parameters: unknown[]
122
+ parameters: unknown[],
125
123
  ): Promise<unknown> => {
126
124
  const item = this.getRemoteFunction(itemName)
127
125
 
@@ -135,7 +133,7 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
135
133
  itemName,
136
134
  item,
137
135
  parameters,
138
- InvocationType.Call
136
+ InvocationType.Call,
139
137
  )
140
138
  } catch (e) {
141
139
  log.error(`Cannot call item ${itemName}.`, e)
@@ -146,7 +144,7 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
146
144
  private subscribe = async (
147
145
  connectionContext: RpcConnectionContext,
148
146
  itemName: string,
149
- parameters: unknown[]
147
+ parameters: unknown[],
150
148
  ) => {
151
149
  const item = this.getRemoteFunction(itemName)
152
150
 
@@ -167,7 +165,7 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
167
165
  itemName,
168
166
  item,
169
167
  parameters,
170
- InvocationType.Trigger
168
+ InvocationType.Trigger,
171
169
  )
172
170
 
173
171
  const newDataJson = safeStringify(newData)
@@ -178,7 +176,7 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
178
176
  connectionContext.clientId,
179
177
  itemName,
180
178
  parameters,
181
- newData
179
+ newData,
182
180
  )
183
181
  }
184
182
  } catch (e) {
@@ -186,14 +184,16 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
186
184
  }
187
185
  })
188
186
 
189
- this.localSubscriptions.subscribe(connectionContext.clientId, itemName, parameters, update)
187
+ if (this.connectionsServer?.isClientSubscribed(connectionContext.clientId)) {
188
+ this.localSubscriptions.subscribe(connectionContext.clientId, itemName, parameters, update)
189
+ }
190
190
 
191
191
  const lastData = await this.invokeLocalFunction(
192
192
  connectionContext,
193
193
  itemName,
194
194
  item,
195
195
  parameters,
196
- InvocationType.Subscribe
196
+ InvocationType.Subscribe,
197
197
  )
198
198
  lastDataJson = safeStringify(lastData)
199
199
 
@@ -209,7 +209,7 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
209
209
  private unsubscribe = async (
210
210
  connectionContext: RpcConnectionContext,
211
211
  itemName: string,
212
- parameters: unknown[]
212
+ parameters: unknown[],
213
213
  ) => {
214
214
  try {
215
215
  this.localSubscriptions.unsubscribe(connectionContext.clientId, itemName, parameters)
@@ -221,7 +221,7 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
221
221
 
222
222
  private getRemoteFunction(
223
223
  itemName: string,
224
- root: any = this.services
224
+ root: any = this.services,
225
225
  ): {function: RemoteFunction; container: any} | undefined {
226
226
  const parts = itemName.split("/")
227
227
 
@@ -248,22 +248,18 @@ export class RpcServerImpl<S extends Services<S>, C extends RpcContext> implemen
248
248
  itemName: string,
249
249
  item: {function: RemoteFunction; container: any},
250
250
  parameters: unknown[],
251
- invocationType: InvocationType
251
+ invocationType: InvocationType,
252
252
  ): Promise<unknown> {
253
- return this.invocationCache.invoke(
254
- {clientId: connectionContext.clientId, itemName, parameters},
255
- () => {
256
- const parametersCopy: unknown[] = safeParseJson(safeStringify(parameters))
253
+ const parametersCopy: unknown[] = safeParseJson(safeStringify(parameters))
257
254
 
258
- const ctx = safeParseJson(safeStringify(connectionContext)) as C
259
- ctx.itemName = itemName
260
- ctx.invocationType = invocationType
255
+ const ctx = safeParseJson(safeStringify(connectionContext)) as C
256
+ ctx.itemName = itemName
257
+ ctx.invocationType = invocationType
261
258
 
262
- const invokeItem = (...params: unknown[]) => {
263
- return item.function.call(item.container, ...params, ctx)
264
- }
265
- return withMiddlewares<C>(ctx, this.options.middleware, invokeItem, ...parametersCopy)
266
- }
267
- )
259
+ const invokeItem = (...params: unknown[]) => {
260
+ return item.function.call(item.container, ...params, ctx)
261
+ }
262
+
263
+ return withMiddlewares<C>(ctx, this.options.middleware, invokeItem, ...parametersCopy)
268
264
  }
269
265
  }
@@ -1,25 +1,3 @@
1
- export class PromiseCache {
2
- invoke<T>(cacheKey: unknown, supplier: () => Promise<T>): Promise<T> {
3
- const key = JSON.stringify(cacheKey)
4
-
5
- if (!this.cache[key]) {
6
- this.cache[key] = supplier()
7
- .then((r) => {
8
- delete this.cache[key]
9
- return r
10
- })
11
- .catch((e) => {
12
- delete this.cache[key]
13
- throw e
14
- })
15
- }
16
-
17
- return this.cache[key]
18
- }
19
-
20
- private cache: {[key: string]: Promise<any>} = {}
21
- }
22
-
23
1
  export async function adelay(ms: number) {
24
2
  return new Promise((r) => setTimeout(r, ms))
25
3
  }
package/tests/calls.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import {assert} from "chai"
2
2
  import {createTestClient, startTestServer} from "./testUtils.js"
3
- import {CallOptions, RpcError, RpcErrors} from "../src/index.js"
3
+ import {CallOptions, RpcErrors} from "../src/index.js"
4
4
  import {adelay} from "../src/utils/promises.js"
5
5
 
6
6
  describe("calls", () => {
@@ -169,41 +169,6 @@ describe("calls", () => {
169
169
  const r = await client.obj.hello()
170
170
  assert.equal("yes", r)
171
171
  })
172
-
173
- it("concurrent call cache", async () => {
174
- const item = {r: "1"}
175
- let supplied = 0
176
-
177
- const server = {
178
- test: {
179
- item: async () => {
180
- await adelay(1)
181
- supplied++
182
- return item
183
- },
184
- },
185
- }
186
-
187
- await startTestServer(server)
188
-
189
- const client = await createTestClient<typeof server>()
190
-
191
- let item1
192
- client.test.item().then((item) => {
193
- item1 = item
194
- })
195
-
196
- let item2
197
- client.test.item().then((item) => {
198
- item2 = item
199
- })
200
-
201
- await adelay(50)
202
- assert.deepEqual(item1, item)
203
- assert.deepEqual(item2, item)
204
-
205
- assert.equal(supplied, 1)
206
- })
207
172
  })
208
173
 
209
174
  abstract class A {
package/tests/context.ts CHANGED
@@ -1,4 +1,4 @@
1
- import {createTestClient, startTestServer} from "./testUtils.js"
1
+ import {createTestClient, startTestServer, testClient, testServer} from "./testUtils.js"
2
2
  import {assert} from "chai"
3
3
  import {adelay} from "../src/utils/promises.js"
4
4
  import {RpcContext} from "../src/index.js"
@@ -41,7 +41,7 @@ describe("context", () => {
41
41
  async createConnectionContext() {
42
42
  return {clientId: "test", newKey: "bla"}
43
43
  },
44
- }
44
+ },
45
45
  )
46
46
 
47
47
  const client = await createTestClient<typeof services>()
@@ -75,30 +75,25 @@ describe("context", () => {
75
75
  it("available in trigger", async () => {
76
76
  let ctx = null
77
77
 
78
- const services = await startTestServer(
79
- {
80
- test: {
81
- async call(passedCtx?: any) {
82
- ctx = passedCtx
83
- },
78
+ const services = await startTestServer({
79
+ test: {
80
+ async call(passedCtx?: any) {
81
+ ctx = passedCtx
84
82
  },
85
83
  },
86
- {
87
- async createConnectionContext() {
88
- return {clientId: "test"}
89
- },
90
- }
91
- )
84
+ })
92
85
 
93
- const client = await createTestClient<typeof services>()
86
+ const client = await createTestClient<typeof services>({
87
+ connectOnCreate: true,
88
+ })
94
89
 
95
90
  await client.test.call.subscribe(() => {})
96
- assert.equal(ctx!.clientId, "test")
91
+ assert.equal(ctx!.clientId, testClient?.clientId)
97
92
 
98
93
  ctx = null
99
94
  services.test.call.trigger()
100
95
  await adelay(20)
101
- assert.equal(ctx!.clientId, "test")
96
+ assert.equal(ctx!.clientId, testClient?.clientId)
102
97
  assert.equal(ctx!.invocationType, InvocationType.Trigger)
103
98
  })
104
99
 
@@ -107,26 +102,19 @@ describe("context", () => {
107
102
 
108
103
  let count = 0
109
104
 
110
- const services = await startTestServer(
111
- {
112
- test: {
113
- async call(passedCtx?: any) {
114
- ctx = passedCtx
105
+ const services = await startTestServer({
106
+ test: {
107
+ async call(passedCtx?: any) {
108
+ ctx = passedCtx
115
109
 
116
- if (!count) {
117
- ctx.modified = true
118
- }
110
+ if (!count) {
111
+ ctx.modified = true
112
+ }
119
113
 
120
- count++
121
- },
114
+ count++
122
115
  },
123
116
  },
124
- {
125
- async createConnectionContext() {
126
- return {clientId: "test"}
127
- },
128
- }
129
- )
117
+ })
130
118
 
131
119
  const client = await createTestClient<typeof services>()
132
120
 
@@ -160,7 +148,7 @@ describe("context", () => {
160
148
  return next(ctx)
161
149
  },
162
150
  ],
163
- }
151
+ },
164
152
  )
165
153
 
166
154
  const client = await createTestClient<typeof services>()
@@ -44,7 +44,7 @@ describe("middleware", () => {
44
44
  return next()
45
45
  },
46
46
  ],
47
- }
47
+ },
48
48
  )
49
49
 
50
50
  const remote = await createTestClient<typeof services>()
@@ -128,6 +128,7 @@ describe("middleware", () => {
128
128
  return next((r as number) + 1)
129
129
  },
130
130
  ],
131
+ connectOnCreate: true,
131
132
  })
132
133
 
133
134
  let response
@@ -135,9 +136,12 @@ describe("middleware", () => {
135
136
  (r) => {
136
137
  response = r
137
138
  },
138
- {param: 1}
139
+ {param: 1},
139
140
  )
140
141
 
142
+ assert.equal(response, 1)
143
+ assert.equal(count, 2)
144
+
141
145
  services.remote.trigger({param: 1})
142
146
 
143
147
  await adelay(20)
@@ -172,6 +176,7 @@ describe("middleware", () => {
172
176
  throw new Error("Test error")
173
177
  },
174
178
  ],
179
+ connectOnCreate: true,
175
180
  })
176
181
 
177
182
  await client.remote.subscribe((r) => {
@@ -227,7 +232,7 @@ describe("middleware", () => {
227
232
  throw new Error("Error")
228
233
  },
229
234
  ],
230
- }
235
+ },
231
236
  )
232
237
 
233
238
  const client = await createTestClient<typeof services>({})
@@ -1,10 +1,8 @@
1
1
  import {assert} from "chai"
2
2
  import {createTestClient, startTestServer, testClient, testServer} from "./testUtils.js"
3
3
  import {adelay} from "../src/utils/promises.js"
4
- import {CallOptions, RpcConnectionContext, RpcErrors} from "../src/index.js"
5
- import {IncomingMessage} from "http"
6
- import {CLIENT_ID_HEADER} from "../src/rpc.js"
7
- import WebSocket from "ws"
4
+ import {CallOptions, RpcErrors} from "../src/index.js"
5
+ import {setTestWebSocketConnectionDelay} from "../src/client/WebSocketConnection.js"
8
6
 
9
7
  describe("Subscriptions", () => {
10
8
  it("subscribe delivers data", async () => {
@@ -140,41 +138,6 @@ describe("Subscriptions", () => {
140
138
  assert.equal(testServer?._allSubscriptions()[0][0], "test/item")
141
139
  })
142
140
 
143
- it("concurrent subscribe cache", async () => {
144
- const item = {r: "1"}
145
- let supplied = 0
146
-
147
- const server = {
148
- test: {
149
- item: async () => {
150
- await adelay(20)
151
- supplied++
152
- return item
153
- },
154
- },
155
- }
156
-
157
- await startTestServer(server)
158
-
159
- const client = await createTestClient<typeof server>()
160
-
161
- let item1
162
- client.test.item.subscribe((item) => {
163
- item1 = item
164
- })
165
-
166
- let item2
167
- client.test.item.subscribe((item) => {
168
- item2 = item
169
- })
170
-
171
- await adelay(50)
172
- assert.deepEqual(item1, item)
173
- assert.deepEqual(item2, item)
174
-
175
- assert.equal(supplied, 1)
176
- })
177
-
178
141
  it("subscribe use client cached value", async () => {
179
142
  const item = {r: "1"}
180
143
 
@@ -482,23 +445,7 @@ describe("Subscriptions", () => {
482
445
  })
483
446
 
484
447
  // delay client connection open by 10ms
485
- let oldAddEL: typeof WebSocket.prototype.addEventListener
486
-
487
- oldAddEL = WebSocket.prototype.addEventListener
488
- WebSocket.prototype.addEventListener = function (eventName: any, callback: any) {
489
- if (eventName == "open") {
490
- oldAddEL.apply(this, [
491
- eventName,
492
- () => {
493
- setTimeout(callback, 10)
494
- },
495
- ])
496
-
497
- return
498
- }
499
-
500
- return oldAddEL.apply(this, [eventName, callback])
501
- }
448
+ delayWebsocketConnection(10)
502
449
 
503
450
  const client = await createTestClient<typeof services>()
504
451
 
@@ -514,7 +461,7 @@ describe("Subscriptions", () => {
514
461
  assert.equal(testClient!._allSubscriptions().length, 0)
515
462
  assert.equal(testServer!._allSubscriptions().length, 0)
516
463
 
517
- WebSocket.prototype.addEventListener = oldAddEL
464
+ cancelWebsocketConnectionDelay()
518
465
  })
519
466
 
520
467
  it("skip unchanged data", async () => {
@@ -587,6 +534,7 @@ describe("Subscriptions", () => {
587
534
 
588
535
  const client = await createTestClient<typeof services>({
589
536
  callTimeout: 2 * delay,
537
+ connectOnCreate: true,
590
538
  })
591
539
 
592
540
  let received = 0
@@ -605,6 +553,8 @@ describe("Subscriptions", () => {
605
553
  assert.equal(received, 2)
606
554
  })
607
555
 
556
+ // Currently not working, b/c paused is boolean
557
+ // To implement it, need to replace it with counter
608
558
  it.skip("two concurrent subscribes and trigger", async () => {
609
559
  const delay = 50
610
560
 
@@ -694,62 +644,133 @@ describe("Subscriptions", () => {
694
644
  assert.equal(received, 1)
695
645
  })
696
646
 
697
- it("subscribe waits for connection", async () => {
698
- const delay = 50
647
+ it("can subscribe while disconnected", async () => {
648
+ const item = {r: "1"}
699
649
 
700
- let connectedClients = 0
701
- let serverCalled = 0
650
+ delayWebsocketConnection(50)
702
651
 
703
- const services = await startTestServer(
704
- {
705
- test: {
706
- async op(params: {key: number}): Promise<number> {
707
- serverCalled++
708
- return 1
709
- },
710
- },
652
+ const services = await startTestServer({
653
+ test: {
654
+ item: async () => item,
711
655
  },
712
- {
713
- async createConnectionContext(req: IncomingMessage): Promise<RpcConnectionContext> {
714
- const header = req.headers[CLIENT_ID_HEADER]
656
+ })
715
657
 
716
- connectedClients++
658
+ const remote = await createTestClient<typeof services>()
717
659
 
718
- return {
719
- clientId: (Array.isArray(header) ? header[0] : header) || "anon",
720
- }
660
+ let receivedItem
661
+ await remote.test.item.subscribe((item) => {
662
+ receivedItem = item
663
+ })
664
+
665
+ assert.deepEqual(receivedItem, item)
666
+
667
+ // trigger sends item
668
+ item.r = "2"
669
+ services.test.item.trigger()
670
+ await adelay(60)
671
+ assert.deepEqual(receivedItem, item)
672
+
673
+ cancelWebsocketConnectionDelay()
674
+ })
675
+
676
+ it("connect during subscribe", async () => {
677
+ const item = {r: "1"}
678
+
679
+ delayWebsocketConnection(30)
680
+
681
+ const services = await startTestServer({
682
+ test: {
683
+ item: async () => {
684
+ await adelay(50)
685
+ return item
721
686
  },
722
687
  },
723
- )
688
+ })
724
689
 
725
- const client = await createTestClient<typeof services>({
726
- callTimeout: 2 * delay,
690
+ const remote = await createTestClient<typeof services>()
691
+
692
+ let receivedItem
693
+ await remote.test.item.subscribe((item) => {
694
+ receivedItem = item
727
695
  })
728
696
 
729
- let received1
730
- let received2
697
+ assert.deepEqual(receivedItem, item)
698
+
699
+ // trigger sends item
700
+ item.r = "2"
701
+ services.test.item.trigger()
702
+ await adelay(100)
703
+ assert.deepEqual(receivedItem, item)
704
+
705
+ cancelWebsocketConnectionDelay()
706
+ })
707
+
708
+ it("disconnect during subscribe", async () => {
709
+ const item = {r: "1"}
731
710
 
732
- client.test.op.subscribe(
733
- (val) => {
734
- received1 = val
711
+ const services = await startTestServer({
712
+ test: {
713
+ item: async () => item,
735
714
  },
736
- {key: 1},
737
- )
715
+ })
738
716
 
739
- await adelay(40)
717
+ const consume1 = await createTestClient<typeof services>({
718
+ connectOnCreate: true,
719
+ })
720
+
721
+ let received = null
740
722
 
741
- client.test.op.subscribe(
742
- (val) => {
743
- received2 = val
723
+ // disconnect WS
724
+ testClient!._webSocket()!.close()
725
+
726
+ await consume1.test.item.subscribe((item) => {
727
+ received = item
728
+ })
729
+
730
+ assert.deepEqual(received, item)
731
+
732
+ item.r = "2"
733
+ services.test.item.trigger()
734
+ await adelay(100)
735
+
736
+ // still getting the update b/c on WS reconnect client will resubscribe
737
+ assert.deepEqual(received, item)
738
+ })
739
+
740
+ it("do not add subscriptions without connection", async () => {
741
+ const item = {r: "1"}
742
+
743
+ delayWebsocketConnection(100)
744
+
745
+ const services = await startTestServer({
746
+ test: {
747
+ item: async () => {
748
+ return item
749
+ },
744
750
  },
745
- {key: 2},
746
- )
751
+ })
747
752
 
748
- await adelay(1.5 * delay)
753
+ const remote = await createTestClient<typeof services>()
749
754
 
750
- assert.equal(received1, 1)
751
- assert.equal(received2, 1)
752
- assert.equal(serverCalled, 2)
753
- assert.equal(testServer!._allSubscriptions().length, 2)
755
+ let receivedItem
756
+ await remote.test.item.subscribe((item) => {
757
+ receivedItem = item
758
+ })
759
+
760
+ assert.deepEqual(receivedItem, item)
761
+
762
+ const length = testServer!._allSubscriptions().length
763
+
764
+ assert.equal(length, 0)
765
+
766
+ cancelWebsocketConnectionDelay()
754
767
  })
755
768
  })
769
+
770
+ function delayWebsocketConnection(ms: number) {
771
+ setTestWebSocketConnectionDelay(ms)
772
+ }
773
+
774
+ function cancelWebsocketConnectionDelay() {
775
+ setTestWebSocketConnectionDelay(0)
776
+ }
@@ -20,7 +20,7 @@ export let testServer: RpcServer | null = null
20
20
 
21
21
  export async function startTestServer<S extends Services<S>, C extends RpcContext>(
22
22
  local: S,
23
- options: Partial<PublishServicesOptions<C>> = {}
23
+ options: Partial<PublishServicesOptions<C>> = {},
24
24
  ): Promise<ServicesWithTriggers<S>> {
25
25
  const r = await publishServices<S, C>(local, {
26
26
  port: TEST_PORT,
@@ -34,7 +34,7 @@ export async function startTestServer<S extends Services<S>, C extends RpcContex
34
34
  export let testClient: RpcClient | null = null
35
35
 
36
36
  export async function createTestClient<S extends Services<S>>(
37
- options?: Partial<ConsumeServicesOptions>
37
+ options?: Partial<ConsumeServicesOptions>,
38
38
  ): Promise<ServicesWithSubscriptions<S>> {
39
39
  if (!options) options = {}
40
40
  if (!options.middleware) options.middleware = []
package/tests/triggers.ts CHANGED
@@ -22,7 +22,9 @@ describe("Subscription triggers", () => {
22
22
  timeout: 0,
23
23
  })
24
24
 
25
- const remote = await createTestClient<typeof services>()
25
+ const remote = await createTestClient<typeof services>({
26
+ connectOnCreate: true,
27
+ })
26
28
 
27
29
  let item1
28
30
  let item2
@@ -68,7 +70,9 @@ describe("Subscription triggers", () => {
68
70
  timeout: throttleTimeout,
69
71
  })
70
72
 
71
- const remote = await createTestClient<typeof services>()
73
+ const remote = await createTestClient<typeof services>({
74
+ connectOnCreate: true,
75
+ })
72
76
 
73
77
  let count = 0
74
78
  let item = null