@nxtedition/deepstream.io-client-js 30.0.0 → 31.0.0

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.
@@ -10,10 +10,44 @@ import * as utils from '../utils/utils.js'
10
10
  import xuid from 'xuid'
11
11
  import * as timers from '../utils/timers.js'
12
12
 
13
+ function noop() {}
14
+
13
15
  const kEmpty = Symbol('kEmpty')
14
16
 
17
+ const OBSERVE_DEFAULTS = {
18
+ timeout: 2 * 60e3,
19
+ state: C.RECORD_STATE.SERVER,
20
+ dataOnly: true,
21
+ }
22
+ const OBSERVE2_DEFAULTS = {
23
+ timeout: 2 * 60e3,
24
+ }
25
+ const GET_DEFAULTS = {
26
+ timeout: 2 * 60e3,
27
+ first: true,
28
+ sync: true,
29
+ dataOnly: true,
30
+ }
31
+ const GET2_DEFAULTS = {
32
+ timeout: 2 * 60e3,
33
+ first: true,
34
+ }
35
+
36
+ function onSync(subscription) {
37
+ subscription.synced = true
38
+ onUpdate(null, subscription)
39
+ }
40
+
15
41
  function onUpdate(record, subscription) {
16
- if (subscription.state && record.state < subscription.state) {
42
+ if (!subscription.record) {
43
+ return
44
+ }
45
+
46
+ if (!subscription.synced) {
47
+ return
48
+ }
49
+
50
+ if (subscription.state && subscription.record.state < subscription.state) {
17
51
  return
18
52
  }
19
53
 
@@ -22,7 +56,9 @@ function onUpdate(record, subscription) {
22
56
  subscription.timeout = null
23
57
  }
24
58
 
25
- const data = subscription.path ? record.get(subscription.path) : record.data
59
+ const data = subscription.path
60
+ ? subscription.record.get(subscription.path)
61
+ : subscription.record.data
26
62
 
27
63
  if (subscription.dataOnly) {
28
64
  if (data !== subscription.data) {
@@ -31,12 +67,17 @@ function onUpdate(record, subscription) {
31
67
  }
32
68
  } else {
33
69
  subscription.subscriber.next({
34
- name: record.name,
35
- version: record.version,
36
- state: record.state,
70
+ name: subscription.record.name,
71
+ version: subscription.record.version,
72
+ state: subscription.record.state,
37
73
  data,
38
74
  })
39
75
  }
76
+
77
+ if (subscription.first) {
78
+ subscription.subscriber.complete?.()
79
+ subscription.unsubscribe()
80
+ }
40
81
  }
41
82
 
42
83
  function onTimeout(subscription) {
@@ -53,39 +94,6 @@ function onTimeout(subscription) {
53
94
  )
54
95
  }
55
96
 
56
- function onUpdateFast(rec, opaque) {
57
- const { timeout, resolve, synced, state } = opaque
58
-
59
- if (rec.state >= state && synced) {
60
- timers.clearTimeout(timeout)
61
- rec.unsubscribe(onUpdateFast, opaque)
62
- rec.unref()
63
- resolve(rec.data)
64
- }
65
- }
66
-
67
- function onSyncFast(opaque) {
68
- opaque.synced = true
69
- onUpdateFast(opaque.rec, opaque)
70
- }
71
-
72
- function onTimeoutFast(opaque) {
73
- const { rec, synced, resolve } = opaque
74
- rec.unsubscribe(onUpdateFast, opaque)
75
- rec.unref()
76
-
77
- let err
78
- if (rec.state < opaque.state) {
79
- err = new Error(`timeout state: ${opaque.rec.name} [${opaque.rec.state}<${opaque.state}]`)
80
- } else if (!synced) {
81
- err = new Error(`timeout sync: ${opaque.rec.name} `)
82
- } else {
83
- err = new Error('timeout')
84
- }
85
-
86
- resolve(Promise.reject(Object.assign(err, { code: 'ETIMEDOUT' })))
87
- }
88
-
89
97
  class RecordHandler {
90
98
  constructor(options, connection, client) {
91
99
  this.JSON = jsonPath
@@ -95,12 +103,12 @@ class RecordHandler {
95
103
  this._options = options
96
104
  this._connection = connection
97
105
  this._client = client
98
- this._recordsByName = new Map()
99
- this._recordsByKey = new Map()
106
+ this._records = new Map()
100
107
  this._listeners = new Map()
101
108
  this._pruning = new Set()
102
109
  this._patching = new Map()
103
110
  this._updating = new Map()
111
+ this._putting = new Map()
104
112
 
105
113
  this._connected = 0
106
114
  this._stats = {
@@ -133,7 +141,7 @@ class RecordHandler {
133
141
 
134
142
  for (const rec of pruning) {
135
143
  rec._$dispose()
136
- this._recordsByName.delete(rec.name)
144
+ this._records.delete(rec.name)
137
145
  }
138
146
 
139
147
  this._stats.pruning -= pruning.size
@@ -161,13 +169,16 @@ class RecordHandler {
161
169
  }
162
170
 
163
171
  _onUpdating(rec, value) {
172
+ const callbacks = this._updating.get(rec)
173
+
164
174
  if (value) {
175
+ invariant(!callbacks, 'updating callbacks must not exist')
165
176
  this._stats.updating += 1
166
177
  this._updating.set(rec, [])
167
178
  } else {
168
- this._stats.updating -= 1
179
+ invariant(callbacks, 'updating callbacks must exist')
169
180
 
170
- const callbacks = this._updating.get(rec)
181
+ this._stats.updating -= 1
171
182
  this._updating.delete(rec)
172
183
  for (const callback of callbacks) {
173
184
  callback()
@@ -216,14 +227,13 @@ class RecordHandler {
216
227
  `invalid name ${name}`,
217
228
  )
218
229
 
219
- let record = this._recordsByName.get(name)
230
+ let record = this._records.get(name)
220
231
 
221
232
  if (!record) {
222
233
  record = new Record(name, this)
223
234
  this._stats.records += 1
224
235
  this._stats.created += 1
225
- this._recordsByName.set(record.name, record)
226
- this._recordsByKey.set(record.name, record)
236
+ this._records.set(name, record)
227
237
  }
228
238
 
229
239
  return record.ref()
@@ -256,139 +266,148 @@ class RecordHandler {
256
266
  this._stats.listeners += 1
257
267
  this._listeners.set(pattern, listener)
258
268
 
259
- return () => {
269
+ const disposer = () => {
260
270
  listener._$destroy()
261
271
 
262
272
  this._stats.listeners -= 1
263
273
  this._listeners.delete(pattern)
264
274
  }
275
+ disposer[Symbol.dispose] = disposer
276
+
277
+ return disposer
265
278
  }
266
279
 
267
280
  async sync(opts) {
268
281
  // TODO (fix): Sync pending? What about VOID state?
269
-
270
- let onAbort
282
+ // TODO (perf): Slow implementation...
271
283
 
272
284
  const signal = opts?.signal
273
285
  const timeout = opts?.timeout
274
286
 
275
- const signalPromise = signal
276
- ? new Promise((resolve, reject) => {
277
- onAbort = () => {
278
- reject(signal.reason ?? new utils.AbortError())
279
- }
280
- signal.addEventListener('abort', onAbort)
281
- })
282
- : null
283
- signalPromise?.catch(() => {})
284
-
287
+ let disposers
285
288
  try {
289
+ const signalPromise = signal
290
+ ? new Promise((resolve, reject) => {
291
+ const onAbort = () => reject(signal.reason ?? new utils.AbortError())
292
+ signal.addEventListener('abort', onAbort)
293
+ disposers ??= []
294
+ disposers.push(() => signal.removeEventListener('abort', onAbort))
295
+ })
296
+ : null
297
+
298
+ signalPromise?.catch(noop)
299
+
286
300
  if (this._patching.size) {
287
- let patchingTimeout
288
- const patching = [...this._patching.values()]
289
- await Promise.race([
290
- Promise.all(
291
- patching.map((callbacks) => new Promise((resolve) => callbacks.push(resolve))),
292
- ),
293
- new Promise((resolve) => {
294
- patchingTimeout = timers.setTimeout(
295
- () => {
301
+ let promises
302
+
303
+ {
304
+ const patchingPromises = []
305
+ for (const callbacks of this._patching.values()) {
306
+ patchingPromises.push(new Promise((resolve) => callbacks.push(resolve)))
307
+ }
308
+ promises ??= []
309
+ promises.push(Promise.all(patchingPromises))
310
+ }
311
+
312
+ if (timeout) {
313
+ promises.push(
314
+ new Promise((resolve) => {
315
+ const patchingTimeout = timers.setTimeout(() => {
296
316
  this._client._$onError(
297
317
  C.TOPIC.RECORD,
298
318
  C.EVENT.TIMEOUT,
299
- Object.assign(new Error('sync patching timeout'), {
300
- data: { patching, timeout },
301
- }),
319
+ new Error('sync patching timeout'),
302
320
  )
303
321
  resolve(null)
304
- },
305
- timeout ?? 2 * 60e3,
306
- )
307
- }),
308
- signalPromise,
309
- ]).finally(() => {
310
- timers.clearTimeout(patchingTimeout)
311
- })
322
+ }, timeout)
323
+ disposers ??= []
324
+ disposers.push(() => timers.clearTimeout(patchingTimeout))
325
+ }),
326
+ )
327
+ }
328
+
329
+ if (signalPromise) {
330
+ promises ??= []
331
+ promises.push(signalPromise)
332
+ }
333
+
334
+ if (promises) {
335
+ await Promise.race(promises)
336
+ }
312
337
  }
313
338
 
314
339
  if (this._updating.size) {
315
- let updatingTimeout
316
- const updating = [...this._updating.values()]
317
- await Promise.race([
318
- Promise.all(
319
- updating.map((callbacks) => new Promise((resolve) => callbacks.push(resolve))),
320
- ),
321
- new Promise((resolve) => {
322
- updatingTimeout = timers.setTimeout(
323
- () => {
340
+ let promises
341
+
342
+ {
343
+ const updatingPromises = []
344
+ for (const callbacks of this._updating.values()) {
345
+ updatingPromises.push(new Promise((resolve) => callbacks.push(resolve)))
346
+ }
347
+ promises ??= []
348
+ promises.push(Promise.all(updatingPromises))
349
+ }
350
+
351
+ if (timeout) {
352
+ promises ??= []
353
+ promises.push(
354
+ new Promise((resolve) => {
355
+ const updatingTimeout = timers.setTimeout(() => {
324
356
  this._client._$onError(
325
357
  C.TOPIC.RECORD,
326
358
  C.EVENT.TIMEOUT,
327
- Object.assign(new Error('sync updating timeout'), {
328
- data: { updating, timeout },
329
- }),
359
+ new Error('sync updating timeout'),
330
360
  )
331
361
  resolve(null)
332
- },
333
- timeout ?? 2 * 60e3,
334
- )
335
- }),
336
- signalPromise,
337
- ]).finally(() => {
338
- timers.clearTimeout(updatingTimeout)
339
- })
340
- }
341
-
342
- let serverTimeout
343
- const token = xuid()
344
- return await Promise.race([
345
- await new Promise((resolve) => {
346
- this._syncEmitter.once(token, resolve)
347
- this._connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SYNC, [token])
348
- }),
349
- new Promise((resolve) => {
350
- serverTimeout = timers.setTimeout(
351
- () => {
352
- this._client._$onError(
353
- C.TOPIC.RECORD,
354
- C.EVENT.TIMEOUT,
355
- Object.assign(new Error('sync server timeout'), { data: { token, timeout } }),
356
- )
357
- resolve(null)
358
- },
359
- timeout ?? 2 * 60e3,
362
+ }, timeout)
363
+ disposers ??= []
364
+ disposers.push(() => timers.clearTimeout(updatingTimeout))
365
+ }),
360
366
  )
361
- }),
362
- signalPromise,
363
- ]).finally(() => {
364
- timers.clearTimeout(serverTimeout)
365
- })
366
- } finally {
367
- if (onAbort) {
368
- signal?.removeEventListener('abort', onAbort)
367
+ }
368
+
369
+ if (promises) {
370
+ await Promise.race(promises)
371
+ }
369
372
  }
370
- }
371
- }
372
373
 
373
- _sync(callback, opaque) {
374
- this._syncQueue.push(callback, opaque)
374
+ {
375
+ const syncPromise = new Promise((resolve) => this._sync(resolve))
376
+
377
+ let promises
378
+
379
+ if (timeout) {
380
+ promises ??= []
381
+ promises.push(
382
+ new Promise((resolve, reject) => {
383
+ const serverTimeout = timers.setTimeout(() => {
384
+ reject(new Error('sync server timeout'))
385
+ }, timeout)
386
+ disposers ??= []
387
+ disposers.push(() => timers.clearTimeout(serverTimeout))
388
+ }),
389
+ )
390
+ }
375
391
 
376
- if (this._syncQueue.length > 2) {
377
- return
378
- }
392
+ if (signalPromise) {
393
+ promises ??= []
394
+ promises.push(signalPromise)
395
+ }
379
396
 
380
- setTimeout(() => {
381
- // Token must be universally unique until deepstream properly separates
382
- // sync requests from different sockets.
383
- const token = xuid()
384
- const queue = this._syncQueue.splice(0)
385
- this._syncEmitter.once(token, () => {
386
- for (let n = 0; n < queue.length; n += 2) {
387
- queue[n](queue[n + 1])
397
+ if (promises) {
398
+ promises.push(syncPromise)
399
+ await Promise.race(promises)
400
+ } else {
401
+ await syncPromise
388
402
  }
389
- })
390
- this._connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SYNC, [token, 'WEAK'])
391
- }, 1)
403
+ }
404
+ } finally {
405
+ if (disposers) {
406
+ for (const disposer of disposers) {
407
+ disposer()
408
+ }
409
+ }
410
+ }
392
411
  }
393
412
 
394
413
  set(name, ...args) {
@@ -400,6 +419,35 @@ class RecordHandler {
400
419
  }
401
420
  }
402
421
 
422
+ put(name, version, data, parent) {
423
+ if (typeof name !== 'string' || name.startsWith('_')) {
424
+ throw new Error('invalid argument: name')
425
+ }
426
+
427
+ if (typeof version !== 'string' || !/^\d+-/.test(version)) {
428
+ throw new Error('invalid argument: verison')
429
+ }
430
+
431
+ if (typeof data !== 'object' && data != null) {
432
+ throw new Error('invalid argument: data')
433
+ }
434
+
435
+ if (parent != null && (typeof version !== 'string' || !/^\d+-/.test(version))) {
436
+ throw new Error('invalid argument: parent')
437
+ }
438
+
439
+ const update = [name, version, jsonPath.stringify(data)]
440
+
441
+ if (parent) {
442
+ update.push(parent)
443
+ }
444
+
445
+ this._connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.PUT, update)
446
+
447
+ this._putting.set(update, [])
448
+ this._sync((update) => this._putting.delete(update), 'WEAK', update)
449
+ }
450
+
403
451
  /**
404
452
  *
405
453
  * @param {*} name
@@ -424,14 +472,7 @@ class RecordHandler {
424
472
  * @returns {rxjs.Observable}
425
473
  */
426
474
  observe(...args) {
427
- return this._observe(
428
- {
429
- state: C.RECORD_STATE.SERVER,
430
- timeout: 2 * 60e3,
431
- dataOnly: true,
432
- },
433
- ...args,
434
- )
475
+ return this._observe(OBSERVE_DEFAULTS, ...args)
435
476
  }
436
477
 
437
478
  /**
@@ -439,47 +480,9 @@ class RecordHandler {
439
480
  * @returns {Promise}
440
481
  */
441
482
  get(...args) {
442
- if (args.length === 1 || (args.length === 2 && typeof args[1] === 'number')) {
443
- return new Promise((resolve) => {
444
- const rec = this.getRecord(args[0])
445
- const state = args.length === 2 ? args[1] : C.RECORD_STATE.SERVER
446
- // TODO (perf): We could also skip sync if state is less than SERVER. However,
447
- // there is a potential race where we receive an UPDATE for a previous SUBSCRIBE.
448
- // Unsure how to avoid that. Keep it simple for now and always sync regardless of
449
- // current state.
450
- const synced = state < C.RECORD_STATE.SERVER
451
-
452
- if (rec.state >= state && synced) {
453
- rec.unref()
454
- resolve(rec.data)
455
- } else {
456
- const opaque = {
457
- rec,
458
- state,
459
- resolve,
460
- timeout: null,
461
- synced,
462
- }
463
- opaque.timeout = timers.setTimeout(onTimeoutFast, 2 * 60e3, opaque)
464
- rec.subscribe(onUpdateFast, opaque)
465
-
466
- if (!opaque.synced) {
467
- this._sync(onSyncFast, opaque)
468
- }
469
- }
470
- })
471
- } else {
472
- // Slow path...
473
- // TODO (fix): Missing sync..
474
- return new Promise((resolve, reject) => {
475
- this.observe(...args)
476
- .pipe(rxjs.first())
477
- .subscribe({
478
- next: resolve,
479
- error: reject,
480
- })
481
- })
482
- }
483
+ return new Promise((resolve, reject) => {
484
+ this._subscribe({ next: resolve, error: reject }, GET_DEFAULTS, ...args)
485
+ })
483
486
  }
484
487
 
485
488
  /**
@@ -488,12 +491,7 @@ class RecordHandler {
488
491
  */
489
492
  get2(...args) {
490
493
  return new Promise((resolve, reject) => {
491
- this.observe2(...args)
492
- .pipe(rxjs.first())
493
- .subscribe({
494
- next: resolve,
495
- error: reject,
496
- })
494
+ this._subscribe({ next: resolve, error: reject }, GET2_DEFAULTS, ...args)
497
495
  })
498
496
  }
499
497
 
@@ -502,24 +500,29 @@ class RecordHandler {
502
500
  * @returns {rxjs.Observable<{ name: string, version: string, state: Number, data: any}>}
503
501
  */
504
502
  observe2(...args) {
505
- return this._observe(
506
- {
507
- timeout: 2 * 60e3,
508
- },
509
- ...args,
510
- )
503
+ return this._observe(OBSERVE2_DEFAULTS, ...args)
511
504
  }
512
505
 
513
506
  /**
514
507
  * @returns {rxjs.Observable}
515
508
  */
516
- // TODO (perf): Avoid rest parameters.
517
509
  _observe(defaults, name, ...args) {
510
+ return new rxjs.Observable((subscriber) => {
511
+ this._subscribe(subscriber, defaults, name, ...args)
512
+ })
513
+ }
514
+
515
+ /**
516
+ * @returns {{ unsubscribe: Function }}
517
+ */
518
+ _subscribe(subscriber, defaults, name, ...args) {
518
519
  let path
519
- let state = defaults ? defaults.state : undefined
520
+ let state = defaults?.state
520
521
  let signal
521
- let timeout = defaults ? defaults.timeout : undefined
522
- let dataOnly = defaults ? defaults.dataOnly : undefined
522
+ let timeout = defaults?.timeout
523
+ let dataOnly = defaults?.dataOnly
524
+ let sync = defaults?.sync
525
+ let first = defaults?.first
523
526
 
524
527
  let idx = 0
525
528
 
@@ -559,71 +562,90 @@ class RecordHandler {
559
562
  if (options.dataOnly !== undefined) {
560
563
  dataOnly = options.dataOnly
561
564
  }
565
+
566
+ if (options.sync !== undefined) {
567
+ sync = options.sync
568
+ }
569
+
570
+ if (options.first !== undefined) {
571
+ first = options.first
572
+ }
562
573
  }
563
574
 
564
575
  if (typeof state === 'string') {
565
576
  state = C.RECORD_STATE[state.toUpperCase()]
566
577
  }
567
578
 
568
- // TODO (perf): Avoid subscribe closure allocation.
569
- return new rxjs.Observable((subscriber) => {
570
- const subscription = {
571
- subscriber,
572
- path,
573
- state,
574
- signal,
575
- dataOnly,
576
- data: kEmpty,
577
- timeout: null,
578
- /** @type {Record?} */ record: null,
579
- /** @type {Function?} */ abort: null,
580
- unsubscribe() {
581
- if (this.timeout) {
582
- timers.clearTimeout(this.timeout)
583
- this.timeout = null
584
- }
579
+ // TODO (perf): Make a class
580
+ const subscription = {
581
+ subscriber,
582
+ first,
583
+ path,
584
+ state,
585
+ synced: false,
586
+ signal,
587
+ dataOnly,
588
+ data: kEmpty,
589
+ /** @type {NodeJS.Timeout|Timeout|null} */
590
+ timeout: null,
591
+ /** @type {Record?} */
592
+ record: null,
593
+ /** @type {Function?} */
594
+ abort: null,
595
+ unsubscribe() {
596
+ if (this.timeout) {
597
+ timers.clearTimeout(this.timeout)
598
+ this.timeout = null
599
+ }
585
600
 
586
- if (this.signal) {
587
- utils.removeAbortListener(this.signal, this.abort)
588
- this.signal = null
589
- this.abort = null
590
- }
601
+ if (this.signal) {
602
+ utils.removeAbortListener(this.signal, this.abort)
603
+ this.signal = null
604
+ this.abort = null
605
+ }
591
606
 
592
- if (this.record) {
593
- this.record.unsubscribe(onUpdate, this)
594
- this.record.unref()
595
- this.record = null
596
- }
597
- },
598
- }
607
+ if (this.record) {
608
+ this.record.unsubscribe(onUpdate, this)
609
+ this.record.unref()
610
+ this.record = null
611
+ }
612
+ },
613
+ }
599
614
 
600
- const record = (subscription.record = this.getRecord(name).subscribe(onUpdate, subscription))
615
+ subscription.record = this.getRecord(name).subscribe(onUpdate, subscription)
601
616
 
602
- if (timeout > 0 && state && record.state < state) {
603
- // TODO (perf): Avoid Timer allocation.
604
- subscription.timeout = timers.setTimeout(onTimeout, timeout, subscription)
605
- }
617
+ const record = subscription.record
606
618
 
607
- if (record.version) {
608
- onUpdate(record, subscription)
609
- }
619
+ if (sync && record.state >= C.RECORD_STATE.SERVER) {
620
+ this._sync(onSync, sync === true ? 'WEAK' : sync, subscription)
621
+ } else {
622
+ subscription.synced = true
623
+ }
610
624
 
611
- if (signal) {
612
- // TODO (perf): Avoid abort closure allocation.
613
- subscription.abort = () => subscriber.error(new utils.AbortError())
614
- utils.addAbortListener(signal, subscription.abort)
615
- }
625
+ if (timeout > 0 && state && record.state < state) {
626
+ // TODO (perf): Avoid Timer allocation.
627
+ subscription.timeout = timers.setTimeout(onTimeout, timeout, subscription)
628
+ }
616
629
 
617
- return subscription
618
- })
630
+ if (signal) {
631
+ // TODO (perf): Avoid abort closure allocation.
632
+ subscription.abort = () => subscriber.error(new utils.AbortError())
633
+ utils.addAbortListener(signal, subscription.abort)
634
+ }
635
+
636
+ if (record.version) {
637
+ onUpdate(null, subscription)
638
+ }
639
+
640
+ return subscription
619
641
  }
620
642
 
621
643
  _$handle(message) {
622
- let key
644
+ let name
623
645
  if (message.action === C.ACTIONS.ERROR) {
624
- key = message.data[1]
646
+ name = message.data[1]
625
647
  } else {
626
- key = message.data[0]
648
+ name = message.data[0]
627
649
  }
628
650
 
629
651
  if (message.action === C.ACTIONS.SYNC) {
@@ -631,12 +653,12 @@ class RecordHandler {
631
653
  return true
632
654
  }
633
655
 
634
- const listener = this._listeners.get(key)
656
+ const listener = this._listeners.get(name)
635
657
  if (listener && listener._$onMessage(message)) {
636
658
  return true
637
659
  }
638
660
 
639
- const record = this._recordsByKey.get(key)
661
+ const record = this._records.get(name)
640
662
  if (record && record._$onMessage(message)) {
641
663
  return true
642
664
  }
@@ -649,12 +671,17 @@ class RecordHandler {
649
671
  listener._$onConnectionStateChange(connected)
650
672
  }
651
673
 
652
- for (const record of this._recordsByName.values()) {
674
+ for (const record of this._records.values()) {
653
675
  record._$onConnectionStateChange(connected)
654
676
  }
655
677
 
656
678
  if (connected) {
657
679
  this._connected = Date.now()
680
+
681
+ for (const update of this._putting.keys()) {
682
+ this._connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.PUT, update)
683
+ }
684
+
658
685
  for (const token of this._syncEmitter.eventNames()) {
659
686
  this._connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SYNC, [token])
660
687
  }
@@ -662,6 +689,35 @@ class RecordHandler {
662
689
  this._connected = 0
663
690
  }
664
691
  }
692
+
693
+ _sync(callback, type, opaque) {
694
+ this._syncQueue.push(callback, opaque)
695
+
696
+ if (this._syncQueue.length > 2) {
697
+ return
698
+ }
699
+
700
+ if (type == null) {
701
+ type = null
702
+ } else if (type === true) {
703
+ type = 'WEAK'
704
+ } else if (type !== 'WEAK' && type !== 'STRONG') {
705
+ throw new Error(`invalid sync type: ${type}`)
706
+ }
707
+
708
+ setTimeout(() => {
709
+ // Token must be universally unique until deepstream properly separates
710
+ // sync requests from different sockets.
711
+ const token = xuid()
712
+ const queue = this._syncQueue.splice(0)
713
+ this._syncEmitter.once(token, () => {
714
+ for (let n = 0; n < queue.length; n += 2) {
715
+ queue[n](queue[n + 1])
716
+ }
717
+ })
718
+ this._connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SYNC, type ? [token, type] : [token])
719
+ }, 1)
720
+ }
665
721
  }
666
722
 
667
723
  export default RecordHandler