@nxtedition/deepstream.io-client-js 32.0.19 → 32.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/deepstream.io-client-js",
3
- "version": "32.0.19",
3
+ "version": "32.0.21",
4
4
  "description": "the javascript client for deepstream.io",
5
5
  "homepage": "http://deepstream.io",
6
6
  "type": "module",
package/src/client.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { Paths, Get } from './record/record.js'
3
3
  import type RecordHandler from './record/record-handler.js'
4
4
  import type { RecordStats, ProvideOptions, SyncOptions } from './record/record-handler.js'
5
5
  import type EventHandler from './event/event-handler.js'
6
- import type { EventStats } from './event/event-handler.js'
6
+ import type { EventStats, EventProvideOptions } from './event/event-handler.js'
7
7
  import type RpcHandler from './rpc/rpc-handler.js'
8
8
  import type { RpcStats, RpcMethodDef } from './rpc/rpc-handler.js'
9
9
 
@@ -30,6 +30,7 @@ export type {
30
30
  RpcHandler,
31
31
  RpcMethodDef,
32
32
  ProvideOptions,
33
+ EventProvideOptions,
33
34
  SyncOptions,
34
35
  Paths,
35
36
  Get,
@@ -4,7 +4,7 @@ export default class EventHandler {
4
4
  connected: boolean
5
5
  stats: EventStats
6
6
  subscribe: (name: string, callback: (data: unknown) => void) => void
7
- unsubscribe: (name: string, callback: (data: unknown) => void) => void
7
+ unsubscribe: (name: string, callback?: (data: unknown) => void) => void
8
8
  on: (name: string, callback: (data: unknown) => void) => this
9
9
  once: (name: string, callback: (data: unknown) => void) => this
10
10
  off: (name: string, callback: (data: unknown) => void) => this
@@ -13,10 +13,15 @@ export default class EventHandler {
13
13
  provide: (
14
14
  pattern: string,
15
15
  callback: (name: string) => void,
16
- options: unknown,
16
+ options: EventProvideOptions,
17
17
  ) => (() => void) | void
18
18
  }
19
19
 
20
+ export interface EventProvideOptions {
21
+ mode?: 'unicast' | (string & {})
22
+ stringify?: ((input: unknown) => string) | null
23
+ }
24
+
20
25
  export interface EventStats {
21
26
  emitted: number
22
27
  listeners: number
@@ -1,7 +1,7 @@
1
1
  import * as C from '../constants/constants.js'
2
2
  import * as messageBuilder from '../message/message-builder.js'
3
3
  import * as messageParser from '../message/message-parser.js'
4
- import MulticastListener from '../utils/multicast-listener.js'
4
+ import LegacyListener from '../utils/legacy-listener.js'
5
5
  import UnicastListener from '../utils/unicast-listener.js'
6
6
  import EventEmitter from 'component-emitter2'
7
7
  import * as rxjs from 'rxjs'
@@ -71,12 +71,12 @@ EventHandler.prototype.unsubscribe = function (name, callback) {
71
71
  }
72
72
  }
73
73
 
74
- EventHandler.on = function (name, callback) {
74
+ EventHandler.prototype.on = function (name, callback) {
75
75
  this.subscribe(name, callback)
76
76
  return this
77
77
  }
78
78
 
79
- EventHandler.once = function (name, callback) {
79
+ EventHandler.prototype.once = function (name, callback) {
80
80
  const fn = (...args) => {
81
81
  this.unsubscribe(name, fn)
82
82
  callback(...args)
@@ -85,7 +85,7 @@ EventHandler.once = function (name, callback) {
85
85
  return this
86
86
  }
87
87
 
88
- EventHandler.off = function (name, callback) {
88
+ EventHandler.prototype.off = function (name, callback) {
89
89
  this.unsubscribe(name, callback)
90
90
  return this
91
91
  }
@@ -126,7 +126,7 @@ EventHandler.prototype.provide = function (pattern, callback, options) {
126
126
  const listener =
127
127
  options.mode?.toLowerCase() === 'unicast'
128
128
  ? new UnicastListener(C.TOPIC.EVENT, pattern, callback, this, options)
129
- : new MulticastListener(C.TOPIC.EVENT, pattern, callback, this, options)
129
+ : new LegacyListener(C.TOPIC.EVENT, pattern, callback, this, options)
130
130
 
131
131
  this._listeners.set(pattern, listener)
132
132
  return () => {
@@ -42,6 +42,37 @@ export default class RecordHandler<Records = Record<string, unknown>> {
42
42
  optionsOrRecursive?: ProvideOptions | boolean,
43
43
  ) => Disposer | void
44
44
 
45
+ put: (
46
+ name: string,
47
+ version: string,
48
+ data: Record<string, unknown> | null,
49
+ parent?: string,
50
+ ) => void
51
+
52
+ getAsync: {
53
+ <Name extends string>(
54
+ name: Name,
55
+ options: ObserveOptions,
56
+ ):
57
+ | { value: Lookup<Records, Name>; async: false }
58
+ | { value: Promise<Lookup<Records, Name>>; async: true }
59
+
60
+ <Name extends string, Path extends string | string[]>(
61
+ name: Name,
62
+ path: Path,
63
+ options?: ObserveOptions,
64
+ ):
65
+ | { value: Get<Lookup<Records, Name>, Path>; async: false }
66
+ | { value: Promise<Get<Lookup<Records, Name>, Path>>; async: true }
67
+
68
+ <Name extends string>(
69
+ name: Name,
70
+ state?: number,
71
+ ):
72
+ | { value: Lookup<Records, Name>; async: false }
73
+ | { value: Promise<Lookup<Records, Name>>; async: true }
74
+ }
75
+
45
76
  sync: (options?: SyncOptions) => Promise<void>
46
77
 
47
78
  set: {
@@ -287,12 +318,13 @@ export interface RecordStats {
287
318
  pruning: number
288
319
  patching: number
289
320
  subscriptions: number
321
+ listeners: number
290
322
  }
291
323
 
292
324
  export interface ProvideOptions {
293
325
  recursive?: boolean
294
326
  stringify?: ((input: unknown) => string) | null
295
- mode: undefined | null | 'unicast'
327
+ mode?: null | 'unicast' | (string & {})
296
328
  }
297
329
 
298
330
  export interface SyncOptions {
@@ -1,5 +1,5 @@
1
1
  import Record from './record.js'
2
- import MulticastListener from '../utils/multicast-listener.js'
2
+ import LegacyListener from '../utils/legacy-listener.js'
3
3
  import UnicastListener from '../utils/unicast-listener.js'
4
4
  import * as C from '../constants/constants.js'
5
5
  import * as rxjs from 'rxjs'
@@ -35,7 +35,7 @@ const GET2_DEFAULTS = {
35
35
 
36
36
  function onSync(subscription) {
37
37
  subscription.synced = true
38
- onUpdate(subscription.record, subscription)
38
+ onUpdate(null, subscription)
39
39
  }
40
40
 
41
41
  function onUpdate(record, subscription) {
@@ -84,9 +84,18 @@ function onTimeout(subscription) {
84
84
 
85
85
  subscription.subscriber.error(
86
86
  Object.assign(
87
- new Error(`timeout state: ${subscription.record.name} [${current}<${expected}]`),
87
+ new Error(
88
+ !subscription.synced
89
+ ? `timeout sync: ${subscription.record.name}`
90
+ : `timeout state: ${subscription.record.name} [${current}<${expected}]`,
91
+ ),
88
92
  {
89
93
  code: 'ETIMEDOUT',
94
+ timeout: subscription.timeoutValue,
95
+ expected,
96
+ current,
97
+ synced: subscription.synced,
98
+ name: subscription.record.name,
90
99
  },
91
100
  ),
92
101
  )
@@ -119,7 +128,7 @@ class RecordHandler {
119
128
  }
120
129
 
121
130
  this._syncQueue = []
122
- this._syncMap = {}
131
+ this._syncMap = new Map()
123
132
 
124
133
  this.set = this.set.bind(this)
125
134
  this.get = this.get.bind(this)
@@ -219,13 +228,18 @@ class RecordHandler {
219
228
  * @returns {Record}
220
229
  */
221
230
  getRecord(name) {
222
- invariant(
223
- typeof name === 'string' &&
224
- name.length > 0 &&
225
- name !== '[object Object]' &&
226
- name.length <= 4096,
227
- `invalid name ${name}`,
228
- )
231
+ if (typeof name !== 'string' || name.length === 0) {
232
+ throw new Error('invalid argument: name')
233
+ }
234
+
235
+ if (name.startsWith('null') || name.startsWith('undefined') || name === '[object Object]') {
236
+ this._client._$onError(
237
+ C.TOPIC.RECORD,
238
+ C.EVENT.USER_ERROR,
239
+ 'name should not start with null or undefined',
240
+ name,
241
+ )
242
+ }
229
243
 
230
244
  let record = this._records.get(name)
231
245
 
@@ -261,7 +275,7 @@ class RecordHandler {
261
275
  const listener =
262
276
  options.mode?.toLowerCase() === 'unicast'
263
277
  ? new UnicastListener(C.TOPIC.RECORD, pattern, callback, this, options)
264
- : new MulticastListener(C.TOPIC.RECORD, pattern, callback, this, options)
278
+ : new LegacyListener(C.TOPIC.RECORD, pattern, callback, this, options)
265
279
 
266
280
  this._stats.listeners += 1
267
281
  this._listeners.set(pattern, listener)
@@ -282,7 +296,7 @@ class RecordHandler {
282
296
  // TODO (perf): Slow implementation...
283
297
 
284
298
  const signal = opts?.signal
285
- const timeout = opts?.timeout
299
+ const timeout = opts?.timeout ?? 10 * 60e3
286
300
 
287
301
  let disposers
288
302
  try {
@@ -298,18 +312,17 @@ class RecordHandler {
298
312
  signalPromise?.catch(noop)
299
313
 
300
314
  if (this._patching.size) {
301
- let promises
315
+ const promises = []
302
316
 
303
317
  {
304
318
  const patchingPromises = []
305
319
  for (const callbacks of this._patching.values()) {
306
320
  patchingPromises.push(new Promise((resolve) => callbacks.push(resolve)))
307
321
  }
308
- promises ??= []
309
322
  promises.push(Promise.all(patchingPromises))
310
323
  }
311
324
 
312
- if (timeout) {
325
+ if (timeout && promises.length) {
313
326
  promises.push(
314
327
  new Promise((resolve) => {
315
328
  const patchingTimeout = timers.setTimeout(() => {
@@ -326,30 +339,28 @@ class RecordHandler {
326
339
  )
327
340
  }
328
341
 
329
- if (signalPromise) {
330
- promises ??= []
342
+ if (signalPromise && promises.length) {
331
343
  promises.push(signalPromise)
332
344
  }
333
345
 
334
- if (promises) {
346
+ if (promises.length) {
335
347
  await Promise.race(promises)
348
+ signal?.throwIfAborted()
336
349
  }
337
350
  }
338
351
 
339
352
  if (this._updating.size) {
340
- let promises
353
+ const promises = []
341
354
 
342
355
  {
343
356
  const updatingPromises = []
344
357
  for (const callbacks of this._updating.values()) {
345
358
  updatingPromises.push(new Promise((resolve) => callbacks.push(resolve)))
346
359
  }
347
- promises ??= []
348
360
  promises.push(Promise.all(updatingPromises))
349
361
  }
350
362
 
351
- if (timeout) {
352
- promises ??= []
363
+ if (timeout && promises.length) {
353
364
  promises.push(
354
365
  new Promise((resolve) => {
355
366
  const updatingTimeout = timers.setTimeout(() => {
@@ -366,18 +377,22 @@ class RecordHandler {
366
377
  )
367
378
  }
368
379
 
369
- if (promises) {
380
+ if (signalPromise && promises.length) {
381
+ promises.push(signalPromise)
382
+ }
383
+
384
+ if (promises.length) {
370
385
  await Promise.race(promises)
386
+ signal?.throwIfAborted()
371
387
  }
372
388
  }
373
389
 
374
390
  {
375
- const syncPromise = new Promise((resolve) => this._sync(resolve))
391
+ const promises = []
376
392
 
377
- let promises
393
+ promises.push(new Promise((resolve) => this._sync(resolve)))
378
394
 
379
395
  if (timeout) {
380
- promises ??= []
381
396
  promises.push(
382
397
  new Promise((resolve, reject) => {
383
398
  const serverTimeout = timers.setTimeout(() => {
@@ -390,15 +405,12 @@ class RecordHandler {
390
405
  }
391
406
 
392
407
  if (signalPromise) {
393
- promises ??= []
394
408
  promises.push(signalPromise)
395
409
  }
396
410
 
397
- if (promises) {
398
- promises.push(syncPromise)
411
+ if (promises.length) {
399
412
  await Promise.race(promises)
400
- } else {
401
- await syncPromise
413
+ signal?.throwIfAborted()
402
414
  }
403
415
  }
404
416
  } finally {
@@ -554,78 +566,78 @@ class RecordHandler {
554
566
  * @returns {rxjs.Observable}
555
567
  */
556
568
  _observe(defaults, name, ...args) {
557
- return new rxjs.Observable((subscriber) => {
558
- let path
559
- let state = defaults?.state ?? C.RECORD_STATE.CLIENT
560
- let signal = null
561
- let timeout = defaults?.timeout ?? 0
562
- let dataOnly = defaults?.dataOnly ?? false
563
- let sync = defaults?.sync ?? false
564
-
565
- let idx = 0
566
-
567
- if (
568
- idx < args.length &&
569
- (args[idx] == null ||
570
- typeof args[idx] === 'string' ||
571
- Array.isArray(args[idx]) ||
572
- typeof args[idx] === 'function')
573
- ) {
574
- path = args[idx++]
575
- }
576
-
577
- if (idx < args.length && (args[idx] == null || typeof args[idx] === 'number')) {
578
- state = args[idx++]
579
- }
580
-
581
- if (idx < args.length && (args[idx] == null || typeof args[idx] === 'object')) {
582
- const options = args[idx++] || {}
583
-
584
- if (options.signal !== undefined) {
585
- signal = options.signal
586
- }
569
+ let path
570
+ let state = defaults?.state ?? C.RECORD_STATE.CLIENT
571
+ let signal = null
572
+ let timeout = defaults?.timeout ?? 0
573
+ let dataOnly = defaults?.dataOnly ?? false
574
+ let sync = defaults?.sync ?? false
587
575
 
588
- if (options.timeout !== undefined) {
589
- timeout = options.timeout
590
- }
576
+ let idx = 0
591
577
 
592
- if (options.path !== undefined) {
593
- path = options.path
594
- }
578
+ if (
579
+ idx < args.length &&
580
+ (args[idx] == null ||
581
+ typeof args[idx] === 'string' ||
582
+ Array.isArray(args[idx]) ||
583
+ typeof args[idx] === 'function')
584
+ ) {
585
+ path = args[idx++]
586
+ }
595
587
 
596
- if (options.state !== undefined) {
597
- state = options.state
598
- }
588
+ if (idx < args.length && (args[idx] == null || typeof args[idx] === 'number')) {
589
+ state = args[idx++]
590
+ }
599
591
 
600
- if (options.dataOnly !== undefined) {
601
- dataOnly = options.dataOnly
602
- }
592
+ if (idx < args.length && (args[idx] == null || typeof args[idx] === 'object')) {
593
+ const options = args[idx++] || {}
603
594
 
604
- if (options.sync !== undefined) {
605
- sync = options.sync
606
- }
595
+ if (options.signal !== undefined) {
596
+ signal = options.signal
607
597
  }
608
598
 
609
- if (typeof state === 'string') {
610
- state = C.RECORD_STATE[state.toUpperCase()]
599
+ if (options.timeout !== undefined) {
600
+ timeout = options.timeout
611
601
  }
612
602
 
613
- if (!Number.isInteger(state) || state < 0) {
614
- throw new Error('invalid argument: state')
603
+ if (options.path !== undefined) {
604
+ path = options.path
615
605
  }
616
606
 
617
- if (!Number.isInteger(timeout) || timeout < 0) {
618
- throw new Error('invalid argument: timeout')
607
+ if (options.state !== undefined) {
608
+ state = options.state
619
609
  }
620
610
 
621
- if (typeof dataOnly !== 'boolean') {
622
- throw new Error('invalid argument: dataOnly')
611
+ if (options.dataOnly !== undefined) {
612
+ dataOnly = options.dataOnly
623
613
  }
624
614
 
625
- if (typeof sync !== 'boolean') {
626
- throw new Error('invalid argument: sync')
615
+ if (options.sync !== undefined) {
616
+ sync = options.sync
627
617
  }
618
+ }
628
619
 
620
+ if (typeof state === 'string') {
621
+ state = C.RECORD_STATE[state.toUpperCase()]
622
+ }
623
+
624
+ if (!Number.isInteger(state) || state < 0) {
625
+ throw new Error('invalid argument: state')
626
+ }
627
+
628
+ if (!Number.isInteger(timeout) || timeout < 0) {
629
+ throw new Error('invalid argument: timeout')
630
+ }
631
+
632
+ if (typeof dataOnly !== 'boolean') {
633
+ throw new Error('invalid argument: dataOnly')
634
+ }
635
+
636
+ if (typeof sync !== 'boolean') {
637
+ throw new Error('invalid argument: sync')
638
+ }
639
+
640
+ return new rxjs.Observable((subscriber) => {
629
641
  // TODO (perf): Make a class
630
642
  const subscription = {
631
643
  /** @readonly @type {unknown} */
@@ -651,6 +663,9 @@ class RecordHandler {
651
663
  data: kEmpty,
652
664
  /** @type {boolean} */
653
665
  synced: false,
666
+
667
+ index: -1,
668
+ onUpdate,
654
669
  }
655
670
 
656
671
  subscriber.add(() => {
@@ -666,7 +681,7 @@ class RecordHandler {
666
681
  }
667
682
 
668
683
  if (subscription.record) {
669
- subscription.record.unsubscribe(onUpdate, subscription)
684
+ subscription.record._unobserve(subscription)
670
685
  subscription.record.unref()
671
686
  subscription.record = null
672
687
  }
@@ -677,11 +692,11 @@ class RecordHandler {
677
692
  utils.addAbortListener(subscription.signal, subscription.abort)
678
693
  }
679
694
 
680
- subscription.record = this.getRecord(name).subscribe(onUpdate, subscription)
695
+ subscription.record = this.getRecord(name)
696
+ subscription.record._observe(subscription)
681
697
 
682
698
  if (sync) {
683
- // TODO (fix): What about sync timeout?
684
- this._sync(onSync, sync === true ? 'WEAK' : sync, subscription)
699
+ this._sync(onSync, sync, subscription)
685
700
  } else {
686
701
  onSync(subscription)
687
702
  }
@@ -702,8 +717,8 @@ class RecordHandler {
702
717
  return true
703
718
  }
704
719
 
705
- const sync = this._syncMap[token]
706
- delete this._syncMap[token]
720
+ const sync = this._syncMap.get(token)
721
+ this._syncMap.delete(token)
707
722
 
708
723
  if (!sync) {
709
724
  return true
@@ -746,10 +761,10 @@ class RecordHandler {
746
761
  this._connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.PUT, update)
747
762
  }
748
763
 
749
- const syncMap = {}
750
- for (const sync of Object.values(this._syncMap)) {
764
+ const syncMap = new Map()
765
+ for (const sync of this._syncMap.values()) {
751
766
  const token = xuid()
752
- syncMap[token] = sync
767
+ syncMap.set(token, sync)
753
768
  this._connection.sendMsg(
754
769
  C.TOPIC.RECORD,
755
770
  C.ACTIONS.SYNC,
@@ -784,7 +799,7 @@ class RecordHandler {
784
799
  const token = xuid()
785
800
  const queue = this._syncQueue.splice(0)
786
801
 
787
- this._syncMap[token] = { queue, type }
802
+ this._syncMap.set(token, { queue, type })
788
803
  this._connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SYNC, type ? [token, type] : [token])
789
804
  }, 1)
790
805
  }
@@ -60,6 +60,8 @@ export default class Record<Data = unknown> {
60
60
  get: {
61
61
  // with path
62
62
  <P extends string | string[]>(path: P): Get<Data, P>
63
+ // with function mapper
64
+ <R>(fn: (data: Data) => R): R
63
65
  // without path
64
66
  (): Data
65
67
  (path: undefined | string | string[]): unknown
@@ -20,7 +20,13 @@ class Record {
20
20
  this._state = C.RECORD_STATE.VOID
21
21
  this._refs = 0
22
22
  this._subscriptions = []
23
- this._emitting = false
23
+
24
+ /** @type {Array|null} */
25
+ this._emittingArr = null
26
+ /** @type {number} */
27
+ this._emittingIndex = -1
28
+
29
+ this._observers = []
24
30
 
25
31
  /** @type Map? */ this._updating = null
26
32
  /** @type Array? */ this._patching = null
@@ -88,9 +94,8 @@ class Record {
88
94
  * @returns {Record}
89
95
  */
90
96
  subscribe(fn, opaque = null) {
91
- if (this._emitting) {
97
+ if (this._emittingArr == this._subscriptions) {
92
98
  this._subscriptions = this._subscriptions.slice()
93
- this._emitting = false
94
99
  }
95
100
 
96
101
  this._subscriptions.push(fn, opaque)
@@ -105,9 +110,8 @@ class Record {
105
110
  * @returns {Record}
106
111
  */
107
112
  unsubscribe(fn, opaque = null) {
108
- if (this._emitting) {
113
+ if (this._emittingArr == this._subscriptions) {
109
114
  this._subscriptions = this._subscriptions.slice()
110
- this._emitting = false
111
115
  }
112
116
 
113
117
  let idx = -1
@@ -128,6 +132,41 @@ class Record {
128
132
  return this
129
133
  }
130
134
 
135
+ /**
136
+ * @note Subscribers are unordered.
137
+ * @param {{ index: number, onUpdate: (Record) => void}} subscription
138
+ */
139
+ _observe(subscription) {
140
+ if (subscription.index != null && subscription.index !== -1) {
141
+ throw new Error('already observing')
142
+ }
143
+
144
+ subscription.index = this._observers.push(subscription) - 1
145
+ }
146
+
147
+ /**
148
+ * @param {{ index: number, onUpdate: (Record) => void}} subscription
149
+ */
150
+ _unobserve(subscription) {
151
+ if (subscription.index == null || subscription.index === -1) {
152
+ throw new Error('not observing')
153
+ }
154
+
155
+ if (this._emittingArr === this._observers) {
156
+ // TODO (perf): Shift from start if emitting?
157
+ this._observers = this._observers.slice()
158
+ }
159
+
160
+ const idx = subscription.index
161
+ const tmp = this._observers.pop()
162
+ subscription.index = -1
163
+
164
+ if (tmp !== subscription) {
165
+ this._observers[idx] = tmp
166
+ this._observers[idx].index = idx
167
+ }
168
+ }
169
+
131
170
  get(path) {
132
171
  if (!path) {
133
172
  return this._data
@@ -515,18 +554,41 @@ class Record {
515
554
  }
516
555
 
517
556
  _emitUpdate() {
518
- this._emitting = true
557
+ if (this._emittingArr != null) {
558
+ throw new Error('cannot reenter emitUpdate')
559
+ }
519
560
 
520
- const arr = this._subscriptions
521
- const len = arr.length
561
+ try {
562
+ const arr = this._subscriptions
563
+ const len = arr.length
522
564
 
523
- for (let n = 0; n < len; n += 2) {
524
- arr[n + 0](this, arr[n + 1])
565
+ this._emittingArr = arr
566
+ for (let n = 0; n < len; n += 2) {
567
+ this._emittingIndex = n
568
+ // TODO (fix): What if this throws?
569
+ arr[n + 0](this, arr[n + 1])
570
+ }
571
+ } finally {
572
+ this._emittingArr = null
573
+ this._emittingIndex = -1
525
574
  }
526
575
 
527
- this._handler._client.emit('recordUpdated', this)
576
+ try {
577
+ const arr = this._observers
578
+ const len = arr.length
528
579
 
529
- this._emitting = false
580
+ this._emittingArr = arr
581
+ for (let n = 0; n < len; n++) {
582
+ this._emittingIndex = n
583
+ // TODO (fix): What if this throws?
584
+ arr[n].onUpdate(this, arr[n])
585
+ }
586
+ } finally {
587
+ this._emittingArr = null
588
+ this._emittingIndex = -1
589
+ }
590
+
591
+ this._handler._client.emit('recordUpdated', this)
530
592
  }
531
593
  }
532
594
 
@@ -13,7 +13,7 @@ export default class RpcHandler<
13
13
  callback: (
14
14
  args: Methods[Name][0],
15
15
  response: RpcResponse<Methods[Name][1]>,
16
- ) => Methods[Name][1] | Promise<Methods[Name][1]> | void,
16
+ ) => Methods[Name][1] | Promise<Methods[Name][1]> | Promise<void> | void,
17
17
  ) => UnprovideFn | void
18
18
 
19
19
  unprovide: <Name extends keyof Methods>(name: Name) => void
@@ -0,0 +1,252 @@
1
+ import * as rxjs from 'rxjs'
2
+ import * as C from '../constants/constants.js'
3
+ import { h64ToString, findBigIntPaths } from './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
+ }
@@ -3,6 +3,8 @@ import xxhash from 'xxhash-wasm'
3
3
  const NODE_ENV = typeof process !== 'undefined' && process.env && process.env.NODE_ENV
4
4
  const HASHER = await xxhash()
5
5
 
6
+ // This is a hack to avoid top-level await
7
+ // const HASHER = await xxhash()
6
8
  export const isNode = typeof process !== 'undefined' && process.toString() === '[object process]'
7
9
  export const isProduction = NODE_ENV === 'production'
8
10