@nxtedition/deepstream.io-client-js 32.0.26 → 32.0.28
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/package.json +1 -1
- package/src/event/event-handler.js +4 -0
- package/src/message/connection.js +3 -2
- package/src/record/record-handler.js +19 -13
- package/src/record/record.js +77 -42
- package/src/rpc/rpc-handler.js +3 -2
- package/src/utils/legacy-listener.js +7 -4
- package/src/utils/unicast-listener.js +7 -3
- package/test/legacy-listener.test.js +450 -0
- package/test/unicast-listener.test.js +366 -0
- package/src/utils/multicast-listener.js +0 -252
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import * as C from '../src/constants/constants.js'
|
|
4
|
+
|
|
5
|
+
function createMockConnection(connected = true) {
|
|
6
|
+
return {
|
|
7
|
+
connected,
|
|
8
|
+
messages: [],
|
|
9
|
+
sendMsg(topic, action, data) {
|
|
10
|
+
this.messages.push({ topic, action, data })
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createMockClient() {
|
|
16
|
+
return {
|
|
17
|
+
errors: [],
|
|
18
|
+
_$onError(topic, event, err, data) {
|
|
19
|
+
this.errors.push({ topic, event, err, data })
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createMockHandler(connection, client) {
|
|
25
|
+
return {
|
|
26
|
+
_client: client,
|
|
27
|
+
_connection: connection,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function msg(action, data) {
|
|
32
|
+
return { action, data }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let Listener
|
|
36
|
+
|
|
37
|
+
describe('UnicastListener', async () => {
|
|
38
|
+
Listener = (await import('../src/utils/unicast-listener.js')).default
|
|
39
|
+
|
|
40
|
+
describe('constructor', () => {
|
|
41
|
+
it('sends LISTEN with U flag on construction', () => {
|
|
42
|
+
const connection = createMockConnection(true)
|
|
43
|
+
const client = createMockClient()
|
|
44
|
+
const handler = createMockHandler(connection, client)
|
|
45
|
+
|
|
46
|
+
new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler, {})
|
|
47
|
+
|
|
48
|
+
assert.equal(connection.messages.length, 1)
|
|
49
|
+
assert.deepEqual(connection.messages[0], {
|
|
50
|
+
topic: C.TOPIC.RECORD,
|
|
51
|
+
action: C.ACTIONS.LISTEN,
|
|
52
|
+
data: ['test/.*', 'U'],
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('throws on recursive option', () => {
|
|
57
|
+
const connection = createMockConnection(true)
|
|
58
|
+
const client = createMockClient()
|
|
59
|
+
const handler = createMockHandler(connection, client)
|
|
60
|
+
|
|
61
|
+
assert.throws(
|
|
62
|
+
() => new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler, { recursive: true }),
|
|
63
|
+
/invalid argument: recursive/,
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('throws on stringify option', () => {
|
|
68
|
+
const connection = createMockConnection(true)
|
|
69
|
+
const client = createMockClient()
|
|
70
|
+
const handler = createMockHandler(connection, client)
|
|
71
|
+
|
|
72
|
+
assert.throws(
|
|
73
|
+
() =>
|
|
74
|
+
new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler, {
|
|
75
|
+
stringify: JSON.stringify,
|
|
76
|
+
}),
|
|
77
|
+
/invalid argument: stringify/,
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('_$onMessage - LISTEN_ACCEPT', () => {
|
|
83
|
+
it('calls callback and subscribes when value$ returned', async () => {
|
|
84
|
+
const connection = createMockConnection(true)
|
|
85
|
+
const client = createMockClient()
|
|
86
|
+
const handler = createMockHandler(connection, client)
|
|
87
|
+
const rxjs = await import('rxjs')
|
|
88
|
+
|
|
89
|
+
const value$ = new rxjs.BehaviorSubject('{"key":"value"}')
|
|
90
|
+
|
|
91
|
+
const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => value$, handler, {})
|
|
92
|
+
connection.messages.length = 0
|
|
93
|
+
|
|
94
|
+
const result = listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
|
|
95
|
+
|
|
96
|
+
assert.equal(result, true)
|
|
97
|
+
assert.equal(listener.stats.subscriptions, 1)
|
|
98
|
+
|
|
99
|
+
const updateMsg = connection.messages.find((m) => m.action === C.ACTIONS.UPDATE)
|
|
100
|
+
assert.ok(updateMsg, 'should send UPDATE')
|
|
101
|
+
assert.equal(updateMsg.data[0], 'test/1')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('sends LISTEN_REJECT when callback returns falsy', () => {
|
|
105
|
+
const connection = createMockConnection(true)
|
|
106
|
+
const client = createMockClient()
|
|
107
|
+
const handler = createMockHandler(connection, client)
|
|
108
|
+
|
|
109
|
+
const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler, {})
|
|
110
|
+
connection.messages.length = 0
|
|
111
|
+
|
|
112
|
+
listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
|
|
113
|
+
|
|
114
|
+
assert.equal(listener.stats.subscriptions, 0)
|
|
115
|
+
const rejectMsg = connection.messages.find((m) => m.action === C.ACTIONS.LISTEN_REJECT)
|
|
116
|
+
assert.ok(rejectMsg, 'should send LISTEN_REJECT')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('wraps callback exceptions in throwError and rejects', async () => {
|
|
120
|
+
const connection = createMockConnection(true)
|
|
121
|
+
const client = createMockClient()
|
|
122
|
+
const handler = createMockHandler(connection, client)
|
|
123
|
+
|
|
124
|
+
const listener = new Listener(
|
|
125
|
+
C.TOPIC.RECORD,
|
|
126
|
+
'test/.*',
|
|
127
|
+
() => {
|
|
128
|
+
throw new Error('callback failed')
|
|
129
|
+
},
|
|
130
|
+
handler,
|
|
131
|
+
{},
|
|
132
|
+
)
|
|
133
|
+
connection.messages.length = 0
|
|
134
|
+
|
|
135
|
+
listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
|
|
136
|
+
|
|
137
|
+
// The error is caught and wrapped in rxjs.throwError, which triggers the PIPE error,
|
|
138
|
+
// which triggers the subscription error handler that sends LISTEN_REJECT
|
|
139
|
+
assert.equal(client.errors.length, 1)
|
|
140
|
+
const rejectMsg = connection.messages.find((m) => m.action === C.ACTIONS.LISTEN_REJECT)
|
|
141
|
+
assert.ok(rejectMsg, 'should send LISTEN_REJECT on error')
|
|
142
|
+
// subscription is removed by the error handler
|
|
143
|
+
assert.equal(listener.stats.subscriptions, 0)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('rejects duplicate subscription names', async () => {
|
|
147
|
+
const connection = createMockConnection(true)
|
|
148
|
+
const client = createMockClient()
|
|
149
|
+
const handler = createMockHandler(connection, client)
|
|
150
|
+
const rxjs = await import('rxjs')
|
|
151
|
+
|
|
152
|
+
const listener = new Listener(
|
|
153
|
+
C.TOPIC.RECORD,
|
|
154
|
+
'test/.*',
|
|
155
|
+
() => new rxjs.BehaviorSubject('{"x":1}'),
|
|
156
|
+
handler,
|
|
157
|
+
{},
|
|
158
|
+
)
|
|
159
|
+
connection.messages.length = 0
|
|
160
|
+
|
|
161
|
+
listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
|
|
162
|
+
listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
|
|
163
|
+
|
|
164
|
+
assert.equal(client.errors.length, 1)
|
|
165
|
+
assert.ok(String(client.errors[0].err).includes('invalid accept'))
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe('_$onMessage - LISTEN_REJECT', () => {
|
|
170
|
+
it('unsubscribes and removes provider', async () => {
|
|
171
|
+
const connection = createMockConnection(true)
|
|
172
|
+
const client = createMockClient()
|
|
173
|
+
const handler = createMockHandler(connection, client)
|
|
174
|
+
const rxjs = await import('rxjs')
|
|
175
|
+
|
|
176
|
+
const value$ = new rxjs.BehaviorSubject('{"key":"value"}')
|
|
177
|
+
|
|
178
|
+
const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => value$, handler, {})
|
|
179
|
+
connection.messages.length = 0
|
|
180
|
+
|
|
181
|
+
listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
|
|
182
|
+
assert.equal(listener.stats.subscriptions, 1)
|
|
183
|
+
|
|
184
|
+
listener._$onMessage(msg(C.ACTIONS.LISTEN_REJECT, ['test/.*', 'test/1']))
|
|
185
|
+
assert.equal(listener.stats.subscriptions, 0)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('errors on removal of unknown subscription', () => {
|
|
189
|
+
const connection = createMockConnection(true)
|
|
190
|
+
const client = createMockClient()
|
|
191
|
+
const handler = createMockHandler(connection, client)
|
|
192
|
+
|
|
193
|
+
const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler, {})
|
|
194
|
+
|
|
195
|
+
listener._$onMessage(msg(C.ACTIONS.LISTEN_REJECT, ['test/.*', 'test/unknown']))
|
|
196
|
+
|
|
197
|
+
assert.equal(client.errors.length, 1)
|
|
198
|
+
assert.ok(String(client.errors[0].err).includes('invalid remove'))
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('_$onMessage - unknown action', () => {
|
|
203
|
+
it('returns false for unrecognized actions', () => {
|
|
204
|
+
const connection = createMockConnection(true)
|
|
205
|
+
const client = createMockClient()
|
|
206
|
+
const handler = createMockHandler(connection, client)
|
|
207
|
+
|
|
208
|
+
const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler, {})
|
|
209
|
+
|
|
210
|
+
const result = listener._$onMessage(msg(C.ACTIONS.UPDATE, ['test/.*', 'test/1']))
|
|
211
|
+
assert.equal(result, false)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('_$onConnectionStateChange', () => {
|
|
216
|
+
it('re-sends LISTEN on reconnect', () => {
|
|
217
|
+
const connection = createMockConnection(true)
|
|
218
|
+
const client = createMockClient()
|
|
219
|
+
const handler = createMockHandler(connection, client)
|
|
220
|
+
|
|
221
|
+
const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler, {})
|
|
222
|
+
connection.messages.length = 0
|
|
223
|
+
|
|
224
|
+
listener._$onConnectionStateChange(false)
|
|
225
|
+
listener._$onConnectionStateChange(true)
|
|
226
|
+
|
|
227
|
+
assert.equal(connection.messages.length, 1)
|
|
228
|
+
assert.deepEqual(connection.messages[0], {
|
|
229
|
+
topic: C.TOPIC.RECORD,
|
|
230
|
+
action: C.ACTIONS.LISTEN,
|
|
231
|
+
data: ['test/.*', 'U'],
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('clears subscriptions on disconnect without sending UNLISTEN (Bug 5 fix)', async () => {
|
|
236
|
+
const connection = createMockConnection(true)
|
|
237
|
+
const client = createMockClient()
|
|
238
|
+
const handler = createMockHandler(connection, client)
|
|
239
|
+
const rxjs = await import('rxjs')
|
|
240
|
+
|
|
241
|
+
const listener = new Listener(
|
|
242
|
+
C.TOPIC.RECORD,
|
|
243
|
+
'test/.*',
|
|
244
|
+
() => new rxjs.BehaviorSubject('{"x":1}'),
|
|
245
|
+
handler,
|
|
246
|
+
{},
|
|
247
|
+
)
|
|
248
|
+
connection.messages.length = 0
|
|
249
|
+
|
|
250
|
+
listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
|
|
251
|
+
assert.equal(listener.stats.subscriptions, 1)
|
|
252
|
+
|
|
253
|
+
connection.messages.length = 0
|
|
254
|
+
listener._$onConnectionStateChange(false)
|
|
255
|
+
|
|
256
|
+
assert.equal(listener.stats.subscriptions, 0)
|
|
257
|
+
// Should NOT send UNLISTEN on disconnected connection
|
|
258
|
+
const unlistenMsg = connection.messages.find((m) => m.action === C.ACTIONS.UNLISTEN)
|
|
259
|
+
assert.equal(unlistenMsg, undefined, 'should not send UNLISTEN when disconnected')
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
describe('_$destroy', () => {
|
|
264
|
+
it('sends UNLISTEN and cleans up', async () => {
|
|
265
|
+
const connection = createMockConnection(true)
|
|
266
|
+
const client = createMockClient()
|
|
267
|
+
const handler = createMockHandler(connection, client)
|
|
268
|
+
const rxjs = await import('rxjs')
|
|
269
|
+
|
|
270
|
+
const listener = new Listener(
|
|
271
|
+
C.TOPIC.RECORD,
|
|
272
|
+
'test/.*',
|
|
273
|
+
() => new rxjs.BehaviorSubject('{"x":1}'),
|
|
274
|
+
handler,
|
|
275
|
+
{},
|
|
276
|
+
)
|
|
277
|
+
connection.messages.length = 0
|
|
278
|
+
|
|
279
|
+
listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
|
|
280
|
+
connection.messages.length = 0
|
|
281
|
+
|
|
282
|
+
listener._$destroy()
|
|
283
|
+
|
|
284
|
+
assert.equal(listener.stats.subscriptions, 0)
|
|
285
|
+
const unlistenMsg = connection.messages.find((m) => m.action === C.ACTIONS.UNLISTEN)
|
|
286
|
+
assert.ok(unlistenMsg, 'should send UNLISTEN on destroy')
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
describe('PIPE - value validation', () => {
|
|
291
|
+
it('rejects non-JSON strings', async () => {
|
|
292
|
+
const connection = createMockConnection(true)
|
|
293
|
+
const client = createMockClient()
|
|
294
|
+
const handler = createMockHandler(connection, client)
|
|
295
|
+
const rxjs = await import('rxjs')
|
|
296
|
+
|
|
297
|
+
const subject = new rxjs.BehaviorSubject('not-json')
|
|
298
|
+
|
|
299
|
+
const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => subject, handler, {})
|
|
300
|
+
connection.messages.length = 0
|
|
301
|
+
|
|
302
|
+
listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
|
|
303
|
+
|
|
304
|
+
// Error should have been reported
|
|
305
|
+
assert.equal(client.errors.length, 1)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('accepts valid JSON strings', async () => {
|
|
309
|
+
const connection = createMockConnection(true)
|
|
310
|
+
const client = createMockClient()
|
|
311
|
+
const handler = createMockHandler(connection, client)
|
|
312
|
+
const rxjs = await import('rxjs')
|
|
313
|
+
|
|
314
|
+
const subject = new rxjs.BehaviorSubject('{"valid": true}')
|
|
315
|
+
|
|
316
|
+
const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => subject, handler, {})
|
|
317
|
+
connection.messages.length = 0
|
|
318
|
+
|
|
319
|
+
listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
|
|
320
|
+
|
|
321
|
+
assert.equal(client.errors.length, 0)
|
|
322
|
+
const updateMsg = connection.messages.find((m) => m.action === C.ACTIONS.UPDATE)
|
|
323
|
+
assert.ok(updateMsg)
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('serializes objects to JSON', async () => {
|
|
327
|
+
const connection = createMockConnection(true)
|
|
328
|
+
const client = createMockClient()
|
|
329
|
+
const handler = createMockHandler(connection, client)
|
|
330
|
+
const rxjs = await import('rxjs')
|
|
331
|
+
|
|
332
|
+
const subject = new rxjs.BehaviorSubject({ key: 'value' })
|
|
333
|
+
|
|
334
|
+
const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => subject, handler, {})
|
|
335
|
+
connection.messages.length = 0
|
|
336
|
+
|
|
337
|
+
listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
|
|
338
|
+
|
|
339
|
+
const updateMsg = connection.messages.find((m) => m.action === C.ACTIONS.UPDATE)
|
|
340
|
+
assert.ok(updateMsg)
|
|
341
|
+
assert.equal(updateMsg.data[2], '{"key":"value"}')
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('deduplicates identical serialized values', async () => {
|
|
345
|
+
const connection = createMockConnection(true)
|
|
346
|
+
const client = createMockClient()
|
|
347
|
+
const handler = createMockHandler(connection, client)
|
|
348
|
+
const rxjs = await import('rxjs')
|
|
349
|
+
|
|
350
|
+
const subject = new rxjs.BehaviorSubject({ key: 'value' })
|
|
351
|
+
|
|
352
|
+
const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => subject, handler, {})
|
|
353
|
+
connection.messages.length = 0
|
|
354
|
+
|
|
355
|
+
listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
|
|
356
|
+
|
|
357
|
+
const count1 = connection.messages.filter((m) => m.action === C.ACTIONS.UPDATE).length
|
|
358
|
+
|
|
359
|
+
// Same value, different object reference
|
|
360
|
+
subject.next({ key: 'value' })
|
|
361
|
+
|
|
362
|
+
const count2 = connection.messages.filter((m) => m.action === C.ACTIONS.UPDATE).length
|
|
363
|
+
assert.equal(count2, count1, 'should deduplicate identical serialized values')
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
})
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import * as rxjs from 'rxjs'
|
|
2
|
-
import * as C from '../constants/constants.js'
|
|
3
|
-
import { h64ToString, findBigIntPaths } from '../utils/utils.js'
|
|
4
|
-
|
|
5
|
-
export default class Listener {
|
|
6
|
-
constructor(topic, pattern, callback, handler, { recursive = false, stringify = null } = {}) {
|
|
7
|
-
this._topic = topic
|
|
8
|
-
this._pattern = pattern
|
|
9
|
-
this._callback = callback
|
|
10
|
-
this._handler = handler
|
|
11
|
-
this._client = this._handler._client
|
|
12
|
-
this._connection = this._handler._connection
|
|
13
|
-
this._subscriptions = new Map()
|
|
14
|
-
this._recursive = recursive
|
|
15
|
-
this._stringify = stringify || JSON.stringify
|
|
16
|
-
|
|
17
|
-
this._$onConnectionStateChange()
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
get connected() {
|
|
21
|
-
return this._connection.connected
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
get stats() {
|
|
25
|
-
return {
|
|
26
|
-
subscriptions: this._subscriptions.size,
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
_$destroy() {
|
|
31
|
-
this._reset()
|
|
32
|
-
|
|
33
|
-
if (this.connected) {
|
|
34
|
-
this._connection.sendMsg(this._topic, C.ACTIONS.UNLISTEN, [this._pattern])
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
_$onMessage(message) {
|
|
39
|
-
if (!this.connected) {
|
|
40
|
-
this._client._$onError(
|
|
41
|
-
C.TOPIC.RECORD,
|
|
42
|
-
C.EVENT.NOT_CONNECTED,
|
|
43
|
-
new Error('received message while not connected'),
|
|
44
|
-
message,
|
|
45
|
-
)
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const name = message.data[1]
|
|
50
|
-
|
|
51
|
-
if (message.action === C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND) {
|
|
52
|
-
if (this._subscriptions.has(name)) {
|
|
53
|
-
this._error(name, 'invalid add: listener exists')
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// TODO (refactor): Move to class
|
|
58
|
-
const provider = {
|
|
59
|
-
name,
|
|
60
|
-
value$: null,
|
|
61
|
-
sending: false,
|
|
62
|
-
accepted: false,
|
|
63
|
-
version: null,
|
|
64
|
-
timeout: null,
|
|
65
|
-
patternSubscription: null,
|
|
66
|
-
valueSubscription: null,
|
|
67
|
-
}
|
|
68
|
-
provider.stop = () => {
|
|
69
|
-
if (this.connected && provider.accepted) {
|
|
70
|
-
this._connection.sendMsg(this._topic, C.ACTIONS.LISTEN_REJECT, [
|
|
71
|
-
this._pattern,
|
|
72
|
-
provider.name,
|
|
73
|
-
])
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
provider.value$ = null
|
|
77
|
-
provider.version = null
|
|
78
|
-
provider.accepted = false
|
|
79
|
-
provider.sending = false
|
|
80
|
-
|
|
81
|
-
clearTimeout(provider.timeout)
|
|
82
|
-
provider.timeout = null
|
|
83
|
-
|
|
84
|
-
provider.patternSubscription?.unsubscribe()
|
|
85
|
-
provider.patternSubscription = null
|
|
86
|
-
|
|
87
|
-
provider.valueSubscription?.unsubscribe()
|
|
88
|
-
provider.valueSubscription = null
|
|
89
|
-
}
|
|
90
|
-
provider.send = () => {
|
|
91
|
-
provider.sending = false
|
|
92
|
-
|
|
93
|
-
if (!provider.patternSubscription) {
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const accepted = Boolean(provider.value$)
|
|
98
|
-
if (provider.accepted === accepted) {
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
this._connection.sendMsg(
|
|
103
|
-
this._topic,
|
|
104
|
-
accepted ? C.ACTIONS.LISTEN_ACCEPT : C.ACTIONS.LISTEN_REJECT,
|
|
105
|
-
[this._pattern, provider.name],
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
provider.version = null
|
|
109
|
-
provider.accepted = accepted
|
|
110
|
-
}
|
|
111
|
-
provider.next = (value$) => {
|
|
112
|
-
if (!value$) {
|
|
113
|
-
value$ = null
|
|
114
|
-
} else if (typeof value$.subscribe !== 'function') {
|
|
115
|
-
value$ = rxjs.of(value$) // Compat for recursive with value
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (Boolean(provider.value$) !== Boolean(value$) && !provider.sending) {
|
|
119
|
-
provider.sending = true
|
|
120
|
-
queueMicrotask(provider.send)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
provider.value$ = value$
|
|
124
|
-
|
|
125
|
-
if (provider.valueSubscription) {
|
|
126
|
-
provider.valueSubscription.unsubscribe()
|
|
127
|
-
provider.valueSubscription = provider.value$?.subscribe(provider.observer)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
provider.error = (err) => {
|
|
131
|
-
provider.stop()
|
|
132
|
-
// TODO (feat): backoff retryCount * delay?
|
|
133
|
-
// TODO (feat): backoff option?
|
|
134
|
-
provider.timeout = setTimeout(() => {
|
|
135
|
-
provider.start()
|
|
136
|
-
}, 10e3)
|
|
137
|
-
this._error(provider.name, err)
|
|
138
|
-
}
|
|
139
|
-
provider.observer = {
|
|
140
|
-
next: (value) => {
|
|
141
|
-
if (value == null) {
|
|
142
|
-
provider.next(null) // TODO (fix): This is weird...
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (this._topic === C.TOPIC.EVENT) {
|
|
147
|
-
this._handler.emit(provider.name, value)
|
|
148
|
-
} else if (this._topic === C.TOPIC.RECORD) {
|
|
149
|
-
if (typeof value !== 'object' && typeof value !== 'string') {
|
|
150
|
-
this._error(provider.name, 'invalid value')
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (typeof value !== 'string') {
|
|
155
|
-
try {
|
|
156
|
-
value = this._stringify(value)
|
|
157
|
-
} catch (err) {
|
|
158
|
-
const bigIntPaths = /BigInt/.test(err.message) ? findBigIntPaths(value) : undefined
|
|
159
|
-
this._error(
|
|
160
|
-
Object.assign(new Error(`invalid value: ${value}`), {
|
|
161
|
-
cause: err,
|
|
162
|
-
data: { name: provider.name, bigIntPaths },
|
|
163
|
-
}),
|
|
164
|
-
)
|
|
165
|
-
return
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const body = value
|
|
170
|
-
const hash = h64ToString(body)
|
|
171
|
-
const version = `INF-${hash}`
|
|
172
|
-
|
|
173
|
-
if (provider.version !== version) {
|
|
174
|
-
provider.version = version
|
|
175
|
-
this._connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.UPDATE, [
|
|
176
|
-
provider.name,
|
|
177
|
-
version,
|
|
178
|
-
body,
|
|
179
|
-
])
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
error: provider.error,
|
|
184
|
-
}
|
|
185
|
-
provider.start = () => {
|
|
186
|
-
try {
|
|
187
|
-
const ret$ = this._callback(name)
|
|
188
|
-
if (this._recursive && typeof ret$?.subscribe === 'function') {
|
|
189
|
-
provider.patternSubscription = ret$.subscribe(provider)
|
|
190
|
-
} else {
|
|
191
|
-
provider.patternSubscription = rxjs.of(ret$).subscribe(provider)
|
|
192
|
-
}
|
|
193
|
-
} catch (err) {
|
|
194
|
-
this._error(provider.name, err)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
provider.start()
|
|
199
|
-
|
|
200
|
-
this._subscriptions.set(provider.name, provider)
|
|
201
|
-
} else if (message.action === C.ACTIONS.LISTEN_ACCEPT) {
|
|
202
|
-
const provider = this._subscriptions.get(name)
|
|
203
|
-
if (!provider?.value$) {
|
|
204
|
-
return
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (provider.valueSubscription) {
|
|
208
|
-
this._error(
|
|
209
|
-
name,
|
|
210
|
-
'invalid accept: listener started (pattern:' + this._pattern + ' name:' + name + ')',
|
|
211
|
-
)
|
|
212
|
-
} else {
|
|
213
|
-
// TODO (fix): provider.version = message.data[2]
|
|
214
|
-
provider.valueSubscription = provider.value$.subscribe(provider.observer)
|
|
215
|
-
}
|
|
216
|
-
} else if (message.action === C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_REMOVED) {
|
|
217
|
-
const provider = this._subscriptions.get(name)
|
|
218
|
-
|
|
219
|
-
if (!provider) {
|
|
220
|
-
this._error(
|
|
221
|
-
name,
|
|
222
|
-
'invalid remove: listener missing (pattern:' + this._pattern + ' name:' + name + ')',
|
|
223
|
-
)
|
|
224
|
-
} else {
|
|
225
|
-
provider.stop()
|
|
226
|
-
this._subscriptions.delete(provider.name)
|
|
227
|
-
}
|
|
228
|
-
} else {
|
|
229
|
-
return false
|
|
230
|
-
}
|
|
231
|
-
return true
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
_$onConnectionStateChange() {
|
|
235
|
-
if (this.connected) {
|
|
236
|
-
this._connection.sendMsg(this._topic, C.ACTIONS.LISTEN, [this._pattern])
|
|
237
|
-
} else {
|
|
238
|
-
this._reset()
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
_error(name, err) {
|
|
243
|
-
this._client._$onError(this._topic, C.EVENT.LISTENER_ERROR, err, [this._pattern, name])
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
_reset() {
|
|
247
|
-
for (const provider of this._subscriptions.values()) {
|
|
248
|
-
provider.stop()
|
|
249
|
-
}
|
|
250
|
-
this._subscriptions.clear()
|
|
251
|
-
}
|
|
252
|
-
}
|