@push-rpc/next 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.prettierrc.json +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +26 -0
  4. package/dist/client/HttpClient.d.ts +10 -0
  5. package/dist/client/HttpClient.js +67 -0
  6. package/dist/client/HttpClient.js.map +1 -0
  7. package/dist/client/RemoteSubscriptions.d.ts +14 -0
  8. package/dist/client/RemoteSubscriptions.js +84 -0
  9. package/dist/client/RemoteSubscriptions.js.map +1 -0
  10. package/dist/client/RpcClientImpl.d.ts +22 -0
  11. package/dist/client/RpcClientImpl.js +96 -0
  12. package/dist/client/RpcClientImpl.js.map +1 -0
  13. package/dist/client/WebSocketConnection.d.ts +33 -0
  14. package/dist/client/WebSocketConnection.js +152 -0
  15. package/dist/client/WebSocketConnection.js.map +1 -0
  16. package/dist/client/index.d.ts +22 -0
  17. package/dist/client/index.js +28 -0
  18. package/dist/client/index.js.map +1 -0
  19. package/dist/client/remote.d.ts +14 -0
  20. package/dist/client/remote.js +66 -0
  21. package/dist/client/remote.js.map +1 -0
  22. package/dist/index.d.ts +12 -0
  23. package/dist/index.js +20 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/logger.d.ts +8 -0
  26. package/dist/logger.js +9 -0
  27. package/dist/logger.js.map +1 -0
  28. package/dist/rpc.d.ts +36 -0
  29. package/dist/rpc.js +32 -0
  30. package/dist/rpc.js.map +1 -0
  31. package/dist/server/ConnectionsServer.d.ts +13 -0
  32. package/dist/server/ConnectionsServer.js +60 -0
  33. package/dist/server/ConnectionsServer.js.map +1 -0
  34. package/dist/server/LocalSubscriptions.d.ts +12 -0
  35. package/dist/server/LocalSubscriptions.js +113 -0
  36. package/dist/server/LocalSubscriptions.js.map +1 -0
  37. package/dist/server/RpcServerImpl.d.ts +23 -0
  38. package/dist/server/RpcServerImpl.js +164 -0
  39. package/dist/server/RpcServerImpl.js.map +1 -0
  40. package/dist/server/http.d.ts +9 -0
  41. package/dist/server/http.js +83 -0
  42. package/dist/server/http.js.map +1 -0
  43. package/dist/server/index.d.ts +29 -0
  44. package/dist/server/index.js +31 -0
  45. package/dist/server/index.js.map +1 -0
  46. package/dist/server/local.d.ts +15 -0
  47. package/dist/server/local.js +46 -0
  48. package/dist/server/local.js.map +1 -0
  49. package/dist/utils/json.d.ts +2 -0
  50. package/dist/utils/json.js +34 -0
  51. package/dist/utils/json.js.map +1 -0
  52. package/dist/utils/middleware.d.ts +2 -0
  53. package/dist/utils/middleware.js +31 -0
  54. package/dist/utils/middleware.js.map +1 -0
  55. package/dist/utils/promises.d.ts +5 -0
  56. package/dist/utils/promises.js +29 -0
  57. package/dist/utils/promises.js.map +1 -0
  58. package/dist/utils/throttle.d.ts +4 -0
  59. package/dist/utils/throttle.js +40 -0
  60. package/dist/utils/throttle.js.map +1 -0
  61. package/dist/utils/types.d.ts +1 -0
  62. package/dist/utils/types.js +3 -0
  63. package/dist/utils/types.js.map +1 -0
  64. package/example/api.ts +15 -0
  65. package/example/client.ts +16 -0
  66. package/example/server.ts +37 -0
  67. package/package.json +34 -0
  68. package/src/client/HttpClient.ts +80 -0
  69. package/src/client/RemoteSubscriptions.ts +121 -0
  70. package/src/client/RpcClientImpl.ts +177 -0
  71. package/src/client/WebSocketConnection.ts +183 -0
  72. package/src/client/index.ts +56 -0
  73. package/src/client/remote.ts +118 -0
  74. package/src/index.ts +18 -0
  75. package/src/logger.ts +12 -0
  76. package/src/rpc.ts +51 -0
  77. package/src/server/ConnectionsServer.ts +78 -0
  78. package/src/server/LocalSubscriptions.ts +155 -0
  79. package/src/server/RpcServerImpl.ts +252 -0
  80. package/src/server/http.ts +109 -0
  81. package/src/server/index.ts +65 -0
  82. package/src/server/local.ts +80 -0
  83. package/src/utils/json.ts +32 -0
  84. package/src/utils/middleware.ts +38 -0
  85. package/src/utils/promises.ts +25 -0
  86. package/src/utils/throttle.ts +48 -0
  87. package/src/utils/types.ts +1 -0
  88. package/tests/calls.ts +215 -0
  89. package/tests/connection.ts +107 -0
  90. package/tests/context.ts +176 -0
  91. package/tests/middleware.ts +112 -0
  92. package/tests/misc.ts +187 -0
  93. package/tests/subscriptions.ts +442 -0
  94. package/tests/testUtils.ts +52 -0
  95. package/tests/triggers.ts +138 -0
  96. package/tsconfig.cjs.json +20 -0
  97. package/tsconfig.json +26 -0
@@ -0,0 +1,80 @@
1
+ import {RemoteFunction, Services} from "../rpc.js"
2
+ import {LocalSubscriptions} from "./LocalSubscriptions.js"
3
+ import {ExtractPromiseResult} from "../utils/types.js"
4
+ import {ThrottleArgsReducer} from "../utils/throttle.js"
5
+
6
+ export type ThrottleSettings<D> = {
7
+ timeout: number
8
+ reducer?: ThrottleArgsReducer<D>
9
+ }
10
+
11
+ export function withTriggers<T extends Services<T>>(
12
+ localSubscriptions: LocalSubscriptions,
13
+ services: T,
14
+ name = ""
15
+ ): ServicesWithTriggers<T> {
16
+ const cachedItems: any = {}
17
+ const skippedProps = ["length", "name", "prototype", "arguments", "caller"]
18
+
19
+ return new Proxy(services, {
20
+ get(target: any, propName: string) {
21
+ // skip internal props
22
+ if (typeof propName != "string") return target[propName]
23
+
24
+ // skip other system props
25
+ if (["then", "catch", "toJSON", ...skippedProps].includes(propName)) return target[propName]
26
+
27
+ const itemName = name ? name + "/" + propName : propName
28
+
29
+ if (typeof target[propName] == "function") {
30
+ const delegate = (...params: unknown[]) => {
31
+ return target[propName](...params)
32
+ }
33
+
34
+ delegate.trigger = (filter: Record<string, unknown> = {}, suppliedData?: unknown) => {
35
+ // triggers are delayed for consumers to receive updates after the current call ends.
36
+ setTimeout(() => {
37
+ localSubscriptions.trigger(itemName, filter, suppliedData)
38
+ }, 0)
39
+ }
40
+
41
+ delegate.throttle = (settings: ThrottleSettings<unknown>) => {
42
+ localSubscriptions.throttleItem(itemName, settings)
43
+ }
44
+
45
+ return delegate
46
+ } else if (!cachedItems[propName]) {
47
+ cachedItems[propName] = withTriggers(
48
+ localSubscriptions,
49
+ services[propName as keyof T] as any,
50
+ itemName
51
+ )
52
+ }
53
+
54
+ return cachedItems[propName]
55
+ },
56
+
57
+ set(target, propName, value) {
58
+ cachedItems[propName] = value
59
+ return true
60
+ },
61
+
62
+ ownKeys() {
63
+ return [...skippedProps, ...Object.keys(cachedItems)]
64
+ },
65
+ })
66
+ }
67
+
68
+ export type ServicesWithTriggers<T extends Services<T>> = {
69
+ [K in keyof T]: T[K] extends RemoteFunction
70
+ ? T[K] & {
71
+ trigger(
72
+ filter?: Partial<Parameters<T[K]>[0]>,
73
+ suppliedData?: ExtractPromiseResult<ReturnType<T[K]>>
74
+ ): void
75
+ throttle(settings: ThrottleSettings<ExtractPromiseResult<ReturnType<T[K]>>>): void
76
+ }
77
+ : T[K] extends object
78
+ ? ServicesWithTriggers<T[K]>
79
+ : never
80
+ }
@@ -0,0 +1,32 @@
1
+ import stringify from "fast-stringify"
2
+
3
+ export function safeStringify(value: any): string {
4
+ // @ts-ignore
5
+ return stringify(value)
6
+ }
7
+
8
+ export function safeParseJson(json: string): any {
9
+ return JSON.parse(json, dateReviver)
10
+ }
11
+
12
+ function dateReviver(key: string, val: any) {
13
+ if (typeof val == "string") {
14
+ if (ISO8601_secs.test(val)) {
15
+ return new Date(val)
16
+ }
17
+
18
+ if (ISO8601.test(val)) {
19
+ return new Date(val)
20
+ }
21
+
22
+ if (ISO8601_date.test(val)) {
23
+ return new Date(val)
24
+ }
25
+ }
26
+
27
+ return val
28
+ }
29
+
30
+ const ISO8601 = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d.\d\d\dZ$/
31
+ const ISO8601_secs = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ$/
32
+ const ISO8601_date = /^\d\d\d\d-\d\d-\d\d$/
@@ -0,0 +1,38 @@
1
+ export type Middleware<Context> = (
2
+ ctx: Context,
3
+ next: (...params: unknown[]) => Promise<unknown>,
4
+ ...params: unknown[]
5
+ ) => Promise<unknown>
6
+
7
+ export function withMiddlewares<Context>(
8
+ ctx: Context,
9
+ middlewares: Middleware<Context>[],
10
+ final: (...params: unknown[]) => Promise<unknown>,
11
+ ...params: any
12
+ ) {
13
+ return (function (next, ...params) {
14
+ let index = -1
15
+ return dispatch(0, ...params)
16
+
17
+ function dispatch(i: number, ...p: unknown[]): Promise<unknown> {
18
+ if (i <= index) return Promise.reject(new Error("next() called multiple times"))
19
+
20
+ // use previous invocation params
21
+ if (!p.length) {
22
+ p = params
23
+ }
24
+
25
+ index = i
26
+
27
+ try {
28
+ if (i === middlewares.length) {
29
+ return Promise.resolve(next(...p))
30
+ } else {
31
+ return Promise.resolve(middlewares[i](ctx, dispatch.bind(null, i + 1), ...p))
32
+ }
33
+ } catch (err) {
34
+ return Promise.reject(err)
35
+ }
36
+ }
37
+ })(final, ...params)
38
+ }
@@ -0,0 +1,25 @@
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
+ export async function adelay(ms: number) {
24
+ return new Promise((r) => setTimeout(r, ms))
25
+ }
@@ -0,0 +1,48 @@
1
+ export type ThrottleArgsReducer<D> = (prevValue: D, newValue: D) => D
2
+
3
+ export function lastValueReducer<D>(prevValue: D, newValue: D): D {
4
+ return newValue
5
+ }
6
+
7
+ export function groupReducer<D>(prevValue: D[], newValue: D[]): D[] {
8
+ if (!Array.isArray(newValue))
9
+ throw new Error("groupReducer should only be used with topics that return arrays")
10
+
11
+ return prevValue ? [...prevValue, ...newValue] : newValue
12
+ }
13
+
14
+ export function throttle<D>(
15
+ callback: (d: D) => void,
16
+ delay: number,
17
+ reducer: ThrottleArgsReducer<D>
18
+ ): (d: D) => void {
19
+ let timer: NodeJS.Timeout
20
+ let lastExec = 0
21
+
22
+ let reducedArg: any
23
+
24
+ function wrapper(this: any, d: D) {
25
+ let self = this
26
+ let elapsed = Date.now() - lastExec
27
+
28
+ function exec() {
29
+ lastExec = Date.now()
30
+ callback.call(self, reducedArg)
31
+ reducedArg = undefined
32
+ }
33
+
34
+ if (timer) {
35
+ clearTimeout(timer)
36
+ }
37
+
38
+ reducedArg = reducer(reducedArg, d)
39
+
40
+ if (elapsed > delay) {
41
+ exec()
42
+ } else {
43
+ timer = setTimeout(exec, delay - elapsed)
44
+ }
45
+ }
46
+
47
+ return wrapper
48
+ }
@@ -0,0 +1 @@
1
+ export type ExtractPromiseResult<Type> = Type extends Promise<infer X> ? X : never
package/tests/calls.ts ADDED
@@ -0,0 +1,215 @@
1
+ import {assert} from "chai"
2
+ import {createTestClient, startTestServer} from "./testUtils.js"
3
+ import {CallOptions, RpcError, RpcErrors} from "../src/index.js"
4
+ import {adelay} from "../src/utils/promises.js"
5
+
6
+ describe("calls", () => {
7
+ it("client call server", async () => {
8
+ const resp = {r: "asf"}
9
+
10
+ const invocation = {
11
+ req: null as unknown,
12
+ }
13
+
14
+ const services = await startTestServer({
15
+ test: {
16
+ async getSomething(req: unknown) {
17
+ invocation.req = req
18
+ return resp
19
+ },
20
+ },
21
+ })
22
+
23
+ const client = await createTestClient<typeof services>()
24
+
25
+ const req = {key: "value"}
26
+ const r = await client.test.getSomething(req)
27
+
28
+ assert.deepEqual(invocation.req, req)
29
+ assert.deepEqual(r, resp)
30
+ })
31
+
32
+ it("error", async () => {
33
+ const message = "bla"
34
+
35
+ const services = await startTestServer({
36
+ test: {
37
+ async getSomething() {
38
+ throw new Error(message)
39
+ },
40
+ },
41
+ })
42
+
43
+ const client = await createTestClient<typeof services>()
44
+
45
+ try {
46
+ await client.test.getSomething()
47
+ assert.fail()
48
+ } catch (e: any) {
49
+ console.log(e)
50
+ assert.equal(e.message, message)
51
+ }
52
+ })
53
+
54
+ it("timeout", async () => {
55
+ const callTimeout = 200
56
+
57
+ const services = await startTestServer({
58
+ test: {
59
+ async longOp() {
60
+ await new Promise((r) => setTimeout(r, 2 * callTimeout))
61
+ },
62
+ },
63
+ })
64
+
65
+ const client = await createTestClient<typeof services>({
66
+ callTimeout,
67
+ })
68
+
69
+ try {
70
+ await client.test.longOp()
71
+ assert.fail()
72
+ } catch (e: any) {
73
+ assert.equal(e.code, RpcErrors.Timeout)
74
+ }
75
+ }).timeout(1000)
76
+
77
+ it("per-call timeout override default", async () => {
78
+ const callTimeout = 200
79
+
80
+ const services = await startTestServer({
81
+ test: {
82
+ async longOp() {
83
+ await adelay(2 * callTimeout)
84
+ },
85
+ },
86
+ })
87
+
88
+ const client = await createTestClient<typeof services>({
89
+ callTimeout: 4 * callTimeout,
90
+ })
91
+
92
+ try {
93
+ await client.test.longOp(new CallOptions({timeout: callTimeout}))
94
+ assert.fail()
95
+ } catch (e: any) {
96
+ assert.equal(e.code, RpcErrors.Timeout)
97
+ }
98
+ }).timeout(5000)
99
+
100
+ it("binds this object", async () => {
101
+ const resp = {r: "asf"}
102
+
103
+ const ss = {
104
+ test: {
105
+ async getSomething() {
106
+ return this.method()
107
+ },
108
+
109
+ async method() {
110
+ return resp
111
+ },
112
+ },
113
+ }
114
+
115
+ const services = await startTestServer(ss)
116
+
117
+ const client = await createTestClient<typeof services>()
118
+
119
+ const r = await client.test.getSomething()
120
+ assert.deepEqual(r, resp)
121
+ })
122
+
123
+ it("binds this class", async () => {
124
+ const resp = {r: "asf"}
125
+
126
+ class B extends A {
127
+ async method() {
128
+ return resp
129
+ }
130
+
131
+ [x: string]: any
132
+ }
133
+
134
+ const services = {
135
+ test: new B(),
136
+ }
137
+
138
+ await startTestServer(services)
139
+
140
+ const client = await createTestClient<typeof services>()
141
+
142
+ const r = await client.test.getSomething()
143
+ assert.deepEqual(r, resp)
144
+ })
145
+
146
+ it("first level lookup", async () => {
147
+ const services = await startTestServer({
148
+ async hello() {
149
+ return "yes"
150
+ },
151
+ })
152
+
153
+ const client = await createTestClient<typeof services>()
154
+
155
+ const r = await client.hello()
156
+ assert.equal("yes", r)
157
+ })
158
+
159
+ it("nested lookup", async () => {
160
+ const services = await startTestServer({
161
+ obj: {
162
+ async hello() {
163
+ return "yes"
164
+ },
165
+ },
166
+ })
167
+
168
+ const client = await createTestClient<typeof services>()
169
+ const r = await client.obj.hello()
170
+ assert.equal("yes", r)
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
+ })
208
+
209
+ abstract class A {
210
+ async getSomething(): Promise<{r: string}> {
211
+ return this.method()
212
+ }
213
+
214
+ abstract method(): Promise<{r: string}>
215
+ }
@@ -0,0 +1,107 @@
1
+ import {assert} from "chai"
2
+ import {createTestClient, startTestServer, testClient, testServer} from "./testUtils.js"
3
+ import WebSocket from "ws"
4
+ import {adelay} from "../src/utils/promises.js"
5
+
6
+ describe("connection", () => {
7
+ it("server close connection on ping timeout", async () => {
8
+ let oldPing: typeof WebSocket.prototype.ping
9
+
10
+ oldPing = WebSocket.prototype.ping
11
+ WebSocket.prototype.ping = () => {}
12
+
13
+ const pingInterval = 100
14
+
15
+ const services = await startTestServer(
16
+ {
17
+ test: {
18
+ async call() {},
19
+ },
20
+ },
21
+ {
22
+ pingInterval,
23
+ }
24
+ )
25
+
26
+ const remote = await createTestClient<typeof services>({
27
+ reconnectDelay: pingInterval * 4, // so we don't reconnect fast and can catch disconnected state
28
+ })
29
+
30
+ await remote.test.call.subscribe(() => {})
31
+ assert.equal(testServer?._allSubscriptions().length, 1)
32
+
33
+ // wait for timeout
34
+ await adelay(pingInterval * 2.5)
35
+
36
+ // should be closed
37
+ assert.equal(testServer?._allSubscriptions().length, 0)
38
+
39
+ WebSocket.prototype.ping = oldPing
40
+ }).timeout(5000)
41
+
42
+ it("client close connection on ping timeout", async () => {
43
+ const pingInterval = 100 // less than server pings, so client will close connection first
44
+
45
+ const services = await startTestServer({
46
+ test: {
47
+ async call() {},
48
+ },
49
+ })
50
+
51
+ const remote = await createTestClient<typeof services>({
52
+ pingInterval,
53
+ reconnectDelay: pingInterval * 4, // so we don't reconnect fast and can catch disconnected state
54
+ })
55
+
56
+ await remote.test.call.subscribe(() => {})
57
+
58
+ assert.equal(testClient?.isConnected(), true)
59
+
60
+ // wait for timeout
61
+ await new Promise((r) => setTimeout(r, pingInterval * 2))
62
+
63
+ // should be closed
64
+ assert.equal(testClient?.isConnected(), false)
65
+ })
66
+
67
+ it("client reconnects on disconnect", async () => {
68
+ const pingInterval = 100 // less than server pings, so client will close connection first
69
+
70
+ const services = await startTestServer({
71
+ test: {
72
+ async call() {},
73
+ },
74
+ })
75
+
76
+ const remote = await createTestClient<typeof services>({
77
+ pingInterval,
78
+ reconnectDelay: 0, // will reconnect after failed ping
79
+ })
80
+
81
+ await remote.test.call.subscribe(() => {})
82
+
83
+ assert.equal(testClient!.isConnected(), true)
84
+
85
+ // wait for timeout
86
+ await new Promise((r) => setTimeout(r, pingInterval * 2))
87
+
88
+ // should be reconnected again
89
+ assert.equal(testClient!.isConnected(), true)
90
+ })
91
+
92
+ it("close will stop reconnection loop", async () => {
93
+ const services = await startTestServer({
94
+ item: async () => "1",
95
+ })
96
+
97
+ const remote = await createTestClient<typeof services>()
98
+
99
+ await remote.item.subscribe(() => {})
100
+
101
+ assert.equal(testClient!.isConnected(), true)
102
+
103
+ await testClient!.close()
104
+
105
+ assert.equal(testClient!.isConnected(), false)
106
+ })
107
+ })