@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.
@@ -0,0 +1,450 @@
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
+ // Minimal mock helpers
6
+ function createMockConnection(connected = true) {
7
+ return {
8
+ connected,
9
+ messages: [],
10
+ sendMsg(topic, action, data) {
11
+ this.messages.push({ topic, action, data })
12
+ },
13
+ }
14
+ }
15
+
16
+ function createMockClient() {
17
+ return {
18
+ errors: [],
19
+ _$onError(topic, event, err, data) {
20
+ this.errors.push({ topic, event, err, data })
21
+ },
22
+ }
23
+ }
24
+
25
+ function createMockHandler(connection, client) {
26
+ return {
27
+ _client: client,
28
+ _connection: connection,
29
+ }
30
+ }
31
+
32
+ function msg(action, data) {
33
+ return { action, data }
34
+ }
35
+
36
+ // We can't import the Listener directly because it depends on xxhash-wasm (top-level await).
37
+ // Instead, we dynamically import after setting up mocks.
38
+ let Listener
39
+
40
+ describe('LegacyListener', async () => {
41
+ Listener = (await import('../src/utils/legacy-listener.js')).default
42
+
43
+ describe('constructor', () => {
44
+ it('sends LISTEN on construction when connected', () => {
45
+ const connection = createMockConnection(true)
46
+ const client = createMockClient()
47
+ const handler = createMockHandler(connection, client)
48
+
49
+ new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler)
50
+
51
+ assert.equal(connection.messages.length, 1)
52
+ assert.deepEqual(connection.messages[0], {
53
+ topic: C.TOPIC.RECORD,
54
+ action: C.ACTIONS.LISTEN,
55
+ data: ['test/.*'],
56
+ })
57
+ })
58
+
59
+ it('does not send LISTEN when not connected', () => {
60
+ const connection = createMockConnection(false)
61
+ const client = createMockClient()
62
+ const handler = createMockHandler(connection, client)
63
+
64
+ new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler)
65
+
66
+ assert.equal(connection.messages.length, 0)
67
+ })
68
+ })
69
+
70
+ describe('_$onMessage - not connected', () => {
71
+ it('returns true and reports error with correct topic', () => {
72
+ const connection = createMockConnection(true)
73
+ const client = createMockClient()
74
+ const handler = createMockHandler(connection, client)
75
+
76
+ const listener = new Listener(C.TOPIC.EVENT, 'test/.*', () => null, handler)
77
+
78
+ // Simulate disconnect without calling _$onConnectionStateChange
79
+ connection.connected = false
80
+
81
+ const result = listener._$onMessage(
82
+ msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, ['test/.*', 'test/1']),
83
+ )
84
+
85
+ assert.equal(result, true)
86
+ assert.equal(client.errors.length, 1)
87
+ assert.equal(client.errors[0].topic, C.TOPIC.EVENT) // Bug 1: was hardcoded to RECORD
88
+ assert.equal(client.errors[0].event, C.EVENT.NOT_CONNECTED)
89
+ })
90
+ })
91
+
92
+ describe('_$onMessage - SUBSCRIPTION_FOR_PATTERN_FOUND', () => {
93
+ it('creates a provider and sends LISTEN_ACCEPT via microtask', async () => {
94
+ const connection = createMockConnection(true)
95
+ const client = createMockClient()
96
+ const handler = createMockHandler(connection, client)
97
+ const { of } = await import('rxjs')
98
+
99
+ const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => of({ key: 'value' }), handler)
100
+ connection.messages.length = 0
101
+
102
+ const result = listener._$onMessage(
103
+ msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, ['test/.*', 'test/1']),
104
+ )
105
+
106
+ assert.equal(result, true)
107
+ assert.equal(listener.stats.subscriptions, 1)
108
+
109
+ // Wait for microtask to fire provider.send()
110
+ await new Promise((r) => queueMicrotask(r))
111
+
112
+ const acceptMsg = connection.messages.find((m) => m.action === C.ACTIONS.LISTEN_ACCEPT)
113
+ assert.ok(acceptMsg, 'should send LISTEN_ACCEPT')
114
+ assert.deepEqual(acceptMsg.data, ['test/.*', 'test/1'])
115
+ })
116
+
117
+ it('rejects duplicate subscription names', async () => {
118
+ const connection = createMockConnection(true)
119
+ const client = createMockClient()
120
+ const handler = createMockHandler(connection, client)
121
+ const rxjs = await import('rxjs')
122
+
123
+ const listener = new Listener(
124
+ C.TOPIC.RECORD,
125
+ 'test/.*',
126
+ () => rxjs.of({ key: 'val' }),
127
+ handler,
128
+ )
129
+ connection.messages.length = 0
130
+
131
+ listener._$onMessage(msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, ['test/.*', 'test/1']))
132
+ listener._$onMessage(msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, ['test/.*', 'test/1']))
133
+
134
+ assert.equal(client.errors.length, 1)
135
+ assert.ok(String(client.errors[0].err).includes('invalid add'))
136
+ })
137
+
138
+ it('cleans up provider when callback throws (Bug 3)', async () => {
139
+ const connection = createMockConnection(true)
140
+ const client = createMockClient()
141
+ const handler = createMockHandler(connection, client)
142
+
143
+ const listener = new Listener(
144
+ C.TOPIC.RECORD,
145
+ 'test/.*',
146
+ () => {
147
+ throw new Error('callback failed')
148
+ },
149
+ handler,
150
+ )
151
+ connection.messages.length = 0
152
+
153
+ listener._$onMessage(msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, ['test/.*', 'test/1']))
154
+
155
+ // Provider should be removed from map after callback throws
156
+ assert.equal(listener.stats.subscriptions, 0)
157
+ assert.equal(client.errors.length, 1)
158
+ })
159
+
160
+ it('sends LISTEN_REJECT when callback returns null', async () => {
161
+ const connection = createMockConnection(true)
162
+ const client = createMockClient()
163
+ const handler = createMockHandler(connection, client)
164
+
165
+ const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler)
166
+ connection.messages.length = 0
167
+
168
+ listener._$onMessage(msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, ['test/.*', 'test/1']))
169
+
170
+ // rxjs.of(null) emits null synchronously, provider.next(null) sets value$ to null
171
+ // provider.send is queued via microtask — wait for it, then another for delivery
172
+ await new Promise((r) => queueMicrotask(r))
173
+ await new Promise((r) => queueMicrotask(r))
174
+
175
+ // With null callback, value$ stays null, so accepted=false matches initial accepted=false
176
+ // The provider never sends ACCEPT in the first place, so no REJECT is sent either.
177
+ // This is correct behavior: null means "don't provide", so we don't accept or reject.
178
+ assert.equal(listener.stats.subscriptions, 1)
179
+ })
180
+ })
181
+
182
+ describe('_$onMessage - SUBSCRIPTION_FOR_PATTERN_REMOVED', () => {
183
+ it('stops and removes the provider', async () => {
184
+ const connection = createMockConnection(true)
185
+ const client = createMockClient()
186
+ const handler = createMockHandler(connection, client)
187
+
188
+ const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler)
189
+ connection.messages.length = 0
190
+
191
+ listener._$onMessage(msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, ['test/.*', 'test/1']))
192
+ assert.equal(listener.stats.subscriptions, 1)
193
+
194
+ listener._$onMessage(msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_REMOVED, ['test/.*', 'test/1']))
195
+ assert.equal(listener.stats.subscriptions, 0)
196
+ })
197
+
198
+ it('errors on removal of unknown subscription', () => {
199
+ const connection = createMockConnection(true)
200
+ const client = createMockClient()
201
+ const handler = createMockHandler(connection, client)
202
+
203
+ const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler)
204
+
205
+ listener._$onMessage(
206
+ msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_REMOVED, ['test/.*', 'test/unknown']),
207
+ )
208
+
209
+ assert.equal(client.errors.length, 1)
210
+ assert.ok(String(client.errors[0].err).includes('invalid remove'))
211
+ })
212
+ })
213
+
214
+ describe('_$onMessage - LISTEN_ACCEPT', () => {
215
+ it('starts value subscription when provider has value$', async () => {
216
+ const connection = createMockConnection(true)
217
+ const client = createMockClient()
218
+ const handler = createMockHandler(connection, client)
219
+ const rxjs = await import('rxjs')
220
+
221
+ const value$ = new rxjs.BehaviorSubject({ data: 'hello' })
222
+
223
+ const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => value$, handler)
224
+ connection.messages.length = 0
225
+
226
+ listener._$onMessage(msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, ['test/.*', 'test/1']))
227
+
228
+ // Wait for microtask to send LISTEN_ACCEPT
229
+ await new Promise((r) => queueMicrotask(r))
230
+
231
+ // Server sends back LISTEN_ACCEPT
232
+ listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
233
+
234
+ // Should have sent an UPDATE with the value
235
+ const updateMsg = connection.messages.find((m) => m.action === C.ACTIONS.UPDATE)
236
+ assert.ok(updateMsg, 'should send UPDATE after LISTEN_ACCEPT')
237
+ assert.equal(updateMsg.data[0], 'test/1')
238
+ })
239
+
240
+ it('ignores LISTEN_ACCEPT when provider has no value$', async () => {
241
+ const connection = createMockConnection(true)
242
+ const client = createMockClient()
243
+ const handler = createMockHandler(connection, client)
244
+
245
+ const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler)
246
+ connection.messages.length = 0
247
+
248
+ listener._$onMessage(msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, ['test/.*', 'test/1']))
249
+
250
+ listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
251
+
252
+ // No error, no crash
253
+ assert.equal(client.errors.length, 0)
254
+ })
255
+ })
256
+
257
+ describe('_$onMessage - unknown action', () => {
258
+ it('returns false for unrecognized actions', () => {
259
+ const connection = createMockConnection(true)
260
+ const client = createMockClient()
261
+ const handler = createMockHandler(connection, client)
262
+
263
+ const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler)
264
+
265
+ const result = listener._$onMessage(msg(C.ACTIONS.UPDATE, ['test/.*', 'test/1']))
266
+ assert.equal(result, false)
267
+ })
268
+ })
269
+
270
+ describe('_$onConnectionStateChange', () => {
271
+ it('re-sends LISTEN on reconnect', () => {
272
+ const connection = createMockConnection(true)
273
+ const client = createMockClient()
274
+ const handler = createMockHandler(connection, client)
275
+
276
+ const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler)
277
+ connection.messages.length = 0
278
+
279
+ // Simulate disconnect
280
+ connection.connected = false
281
+ listener._$onConnectionStateChange()
282
+
283
+ // Simulate reconnect
284
+ connection.connected = true
285
+ listener._$onConnectionStateChange()
286
+
287
+ assert.equal(connection.messages.length, 1)
288
+ assert.deepEqual(connection.messages[0], {
289
+ topic: C.TOPIC.RECORD,
290
+ action: C.ACTIONS.LISTEN,
291
+ data: ['test/.*'],
292
+ })
293
+ })
294
+
295
+ it('clears all subscriptions on disconnect', () => {
296
+ const connection = createMockConnection(true)
297
+ const client = createMockClient()
298
+ const handler = createMockHandler(connection, client)
299
+
300
+ const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler)
301
+
302
+ listener._$onMessage(msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, ['test/.*', 'test/1']))
303
+ assert.equal(listener.stats.subscriptions, 1)
304
+
305
+ connection.connected = false
306
+ listener._$onConnectionStateChange()
307
+
308
+ assert.equal(listener.stats.subscriptions, 0)
309
+ })
310
+ })
311
+
312
+ describe('_$destroy', () => {
313
+ it('sends UNLISTEN and cleans up when connected', () => {
314
+ const connection = createMockConnection(true)
315
+ const client = createMockClient()
316
+ const handler = createMockHandler(connection, client)
317
+
318
+ const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler)
319
+
320
+ listener._$onMessage(msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, ['test/.*', 'test/1']))
321
+ connection.messages.length = 0
322
+
323
+ listener._$destroy()
324
+
325
+ assert.equal(listener.stats.subscriptions, 0)
326
+ const unlistenMsg = connection.messages.find((m) => m.action === C.ACTIONS.UNLISTEN)
327
+ assert.ok(unlistenMsg, 'should send UNLISTEN')
328
+ })
329
+
330
+ it('does not send UNLISTEN when not connected', () => {
331
+ const connection = createMockConnection(false)
332
+ const client = createMockClient()
333
+ const handler = createMockHandler(connection, client)
334
+
335
+ const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => null, handler)
336
+
337
+ listener._$destroy()
338
+
339
+ const unlistenMsg = connection.messages.find((m) => m.action === C.ACTIONS.UNLISTEN)
340
+ assert.equal(unlistenMsg, undefined)
341
+ })
342
+ })
343
+
344
+ describe('provider.observer - record values', () => {
345
+ it('deduplicates identical values by hash', async () => {
346
+ const connection = createMockConnection(true)
347
+ const client = createMockClient()
348
+ const handler = createMockHandler(connection, client)
349
+ const rxjs = await import('rxjs')
350
+
351
+ const subject = new rxjs.BehaviorSubject({ key: 'value' })
352
+
353
+ const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => subject, handler)
354
+ connection.messages.length = 0
355
+
356
+ listener._$onMessage(msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, ['test/.*', 'test/1']))
357
+ await new Promise((r) => queueMicrotask(r))
358
+
359
+ listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
360
+
361
+ const updateCount = connection.messages.filter((m) => m.action === C.ACTIONS.UPDATE).length
362
+
363
+ // Emit same value again
364
+ subject.next({ key: 'value' })
365
+
366
+ const updateCount2 = connection.messages.filter((m) => m.action === C.ACTIONS.UPDATE).length
367
+ assert.equal(updateCount2, updateCount, 'should not send duplicate UPDATE for same hash')
368
+ })
369
+
370
+ it('handles null values from observer by calling provider.next(null)', async () => {
371
+ const connection = createMockConnection(true)
372
+ const client = createMockClient()
373
+ const handler = createMockHandler(connection, client)
374
+ const rxjs = await import('rxjs')
375
+
376
+ const subject = new rxjs.Subject()
377
+
378
+ const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => subject, handler)
379
+ connection.messages.length = 0
380
+
381
+ listener._$onMessage(msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, ['test/.*', 'test/1']))
382
+ await new Promise((r) => queueMicrotask(r))
383
+
384
+ listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
385
+
386
+ // Emit null - should trigger provider.next(null) which rejects
387
+ subject.next(null)
388
+ await new Promise((r) => queueMicrotask(r))
389
+
390
+ const rejectMsg = connection.messages.find((m) => m.action === C.ACTIONS.LISTEN_REJECT)
391
+ assert.ok(rejectMsg, 'should send LISTEN_REJECT when value becomes null')
392
+ })
393
+
394
+ it('errors on invalid non-object non-string values', async () => {
395
+ const connection = createMockConnection(true)
396
+ const client = createMockClient()
397
+ const handler = createMockHandler(connection, client)
398
+ const rxjs = await import('rxjs')
399
+
400
+ const subject = new rxjs.Subject()
401
+
402
+ const listener = new Listener(C.TOPIC.RECORD, 'test/.*', () => subject, handler)
403
+ connection.messages.length = 0
404
+
405
+ listener._$onMessage(msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, ['test/.*', 'test/1']))
406
+ await new Promise((r) => queueMicrotask(r))
407
+
408
+ listener._$onMessage(msg(C.ACTIONS.LISTEN_ACCEPT, ['test/.*', 'test/1']))
409
+
410
+ subject.next(42)
411
+
412
+ assert.equal(client.errors.length, 1)
413
+ assert.ok(String(client.errors[0].err).includes('invalid value'))
414
+ })
415
+ })
416
+
417
+ describe('provider.error - retry behavior', () => {
418
+ it('retries after error with timeout', async () => {
419
+ const connection = createMockConnection(true)
420
+ const client = createMockClient()
421
+ const handler = createMockHandler(connection, client)
422
+ const rxjs = await import('rxjs')
423
+
424
+ let callCount = 0
425
+ const listener = new Listener(
426
+ C.TOPIC.RECORD,
427
+ 'test/.*',
428
+ () => {
429
+ callCount++
430
+ if (callCount === 1) {
431
+ return rxjs.throwError(() => new Error('temporary'))
432
+ }
433
+ return rxjs.of({ ok: true })
434
+ },
435
+ handler,
436
+ { recursive: true },
437
+ )
438
+ connection.messages.length = 0
439
+
440
+ listener._$onMessage(msg(C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, ['test/.*', 'test/1']))
441
+
442
+ assert.equal(callCount, 1)
443
+ assert.equal(client.errors.length, 1)
444
+
445
+ // The retry timeout is 10 seconds, we don't want to actually wait
446
+ // Just verify the provider is still in the map (will be retried)
447
+ assert.equal(listener.stats.subscriptions, 1)
448
+ })
449
+ })
450
+ })