@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 +1 -1
- package/src/client.d.ts +2 -1
- package/src/event/event-handler.d.ts +7 -2
- package/src/event/event-handler.js +5 -5
- package/src/record/record-handler.d.ts +33 -1
- package/src/record/record-handler.js +112 -97
- package/src/record/record.d.ts +2 -0
- package/src/record/record.js +74 -12
- package/src/rpc/rpc-handler.d.ts +1 -1
- package/src/utils/legacy-listener.js +252 -0
- package/src/utils/utils.js +2 -0
package/package.json
CHANGED
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
391
|
+
const promises = []
|
|
376
392
|
|
|
377
|
-
|
|
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
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
589
|
-
timeout = options.timeout
|
|
590
|
-
}
|
|
576
|
+
let idx = 0
|
|
591
577
|
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
588
|
+
if (idx < args.length && (args[idx] == null || typeof args[idx] === 'number')) {
|
|
589
|
+
state = args[idx++]
|
|
590
|
+
}
|
|
599
591
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
592
|
+
if (idx < args.length && (args[idx] == null || typeof args[idx] === 'object')) {
|
|
593
|
+
const options = args[idx++] || {}
|
|
603
594
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
}
|
|
595
|
+
if (options.signal !== undefined) {
|
|
596
|
+
signal = options.signal
|
|
607
597
|
}
|
|
608
598
|
|
|
609
|
-
if (
|
|
610
|
-
|
|
599
|
+
if (options.timeout !== undefined) {
|
|
600
|
+
timeout = options.timeout
|
|
611
601
|
}
|
|
612
602
|
|
|
613
|
-
if (
|
|
614
|
-
|
|
603
|
+
if (options.path !== undefined) {
|
|
604
|
+
path = options.path
|
|
615
605
|
}
|
|
616
606
|
|
|
617
|
-
if (
|
|
618
|
-
|
|
607
|
+
if (options.state !== undefined) {
|
|
608
|
+
state = options.state
|
|
619
609
|
}
|
|
620
610
|
|
|
621
|
-
if (
|
|
622
|
-
|
|
611
|
+
if (options.dataOnly !== undefined) {
|
|
612
|
+
dataOnly = options.dataOnly
|
|
623
613
|
}
|
|
624
614
|
|
|
625
|
-
if (
|
|
626
|
-
|
|
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.
|
|
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)
|
|
695
|
+
subscription.record = this.getRecord(name)
|
|
696
|
+
subscription.record._observe(subscription)
|
|
681
697
|
|
|
682
698
|
if (sync) {
|
|
683
|
-
|
|
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
|
|
706
|
-
|
|
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
|
|
764
|
+
const syncMap = new Map()
|
|
765
|
+
for (const sync of this._syncMap.values()) {
|
|
751
766
|
const token = xuid()
|
|
752
|
-
syncMap
|
|
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
|
|
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
|
}
|
package/src/record/record.d.ts
CHANGED
|
@@ -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
|
package/src/record/record.js
CHANGED
|
@@ -20,7 +20,13 @@ class Record {
|
|
|
20
20
|
this._state = C.RECORD_STATE.VOID
|
|
21
21
|
this._refs = 0
|
|
22
22
|
this._subscriptions = []
|
|
23
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
557
|
+
if (this._emittingArr != null) {
|
|
558
|
+
throw new Error('cannot reenter emitUpdate')
|
|
559
|
+
}
|
|
519
560
|
|
|
520
|
-
|
|
521
|
-
|
|
561
|
+
try {
|
|
562
|
+
const arr = this._subscriptions
|
|
563
|
+
const len = arr.length
|
|
522
564
|
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
576
|
+
try {
|
|
577
|
+
const arr = this._observers
|
|
578
|
+
const len = arr.length
|
|
528
579
|
|
|
529
|
-
|
|
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
|
|
package/src/rpc/rpc-handler.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/utils/utils.js
CHANGED
|
@@ -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
|
|