@nxtedition/deepstream.io-client-js 28.0.1 → 30.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/deepstream.io-client-js",
3
- "version": "28.0.1",
3
+ "version": "30.0.0",
4
4
  "description": "the javascript client for deepstream.io",
5
5
  "homepage": "http://deepstream.io",
6
6
  "type": "module",
@@ -1,81 +1,25 @@
1
1
  import * as C from '../constants/constants.js'
2
- import * as utils from '../utils/utils.js'
3
2
 
4
- const poolEncoder = new globalThis.TextEncoder()
5
-
6
- // TODO (fix): Don't assume maxMesageSize is 1MB
7
- const maxMessageSize = 1024 * 1024
8
- const poolSize = maxMessageSize * 8
9
-
10
- let poolBuffer
11
- let poolOffset
12
-
13
- function reallocPool() {
14
- poolBuffer = utils.isNode
15
- ? globalThis.Buffer.allocUnsafe(poolSize)
16
- : new Uint8Array(new ArrayBuffer(poolSize))
17
- poolOffset = 0
18
- }
19
-
20
- function alignPool() {
21
- // Ensure aligned slices
22
- if (poolOffset & 0x7) {
23
- poolOffset |= 0x7
24
- poolOffset++
25
- }
26
- }
27
-
28
- function writeString(dst, str, offset) {
29
- if (utils.isNode) {
30
- return dst.write(str, offset)
31
- } else {
32
- const res = poolEncoder.encodeInto(str, new Uint8Array(dst.buffer, offset))
33
- return res.written
34
- }
35
- }
3
+ const SEP = C.MESSAGE_PART_SEPERATOR
36
4
 
37
5
  export function getMsg(topic, action, data) {
38
6
  if (data && !(data instanceof Array)) {
39
7
  throw new Error('data must be an array')
40
8
  }
41
9
 
42
- if (!poolBuffer || poolOffset + maxMessageSize > poolSize) {
43
- reallocPool()
44
- } else {
45
- alignPool()
46
- }
47
-
48
- const start = poolOffset
49
-
50
- poolBuffer[poolOffset++] = topic.charCodeAt(0)
51
- poolBuffer[poolOffset++] = 31
52
- for (let n = 0; n < action.length; n++) {
53
- poolBuffer[poolOffset++] = action.charCodeAt(n)
54
- }
10
+ const sendData = [topic, action]
55
11
 
56
12
  if (data) {
57
13
  for (let i = 0; i < data.length; i++) {
58
- const type = typeof data[i]
59
- if (data[i] == null) {
60
- poolBuffer[poolOffset++] = 31
61
- poolOffset += 0
62
- } else if (type === 'object') {
63
- poolBuffer[poolOffset++] = 31
64
- poolOffset += writeString(poolBuffer, JSON.stringify(data[i]), poolOffset)
65
- } else if (type === 'string') {
66
- poolBuffer[poolOffset++] = 31
67
- poolOffset += writeString(poolBuffer, data[i], poolOffset)
14
+ if (typeof data[i] === 'object') {
15
+ sendData.push(JSON.stringify(data[i]))
68
16
  } else {
69
- throw new Error('invalid data')
70
- }
71
-
72
- if (poolOffset >= poolSize) {
73
- throw new Error('message too large')
17
+ sendData.push(data[i])
74
18
  }
75
19
  }
76
20
  }
77
21
 
78
- return new Uint8Array(poolBuffer.buffer, start, poolOffset - start)
22
+ return sendData.join(SEP)
79
23
  }
80
24
 
81
25
  export function typed(value) {
@@ -95,7 +95,8 @@ class RecordHandler {
95
95
  this._options = options
96
96
  this._connection = connection
97
97
  this._client = client
98
- this._records = new Map()
98
+ this._recordsByName = new Map()
99
+ this._recordsByKey = new Map()
99
100
  this._listeners = new Map()
100
101
  this._pruning = new Set()
101
102
  this._patching = new Map()
@@ -132,7 +133,7 @@ class RecordHandler {
132
133
 
133
134
  for (const rec of pruning) {
134
135
  rec._$dispose()
135
- this._records.delete(rec.name)
136
+ this._recordsByName.delete(rec.name)
136
137
  }
137
138
 
138
139
  this._stats.pruning -= pruning.size
@@ -215,13 +216,14 @@ class RecordHandler {
215
216
  `invalid name ${name}`,
216
217
  )
217
218
 
218
- let record = this._records.get(name)
219
+ let record = this._recordsByName.get(name)
219
220
 
220
221
  if (!record) {
221
222
  record = new Record(name, this)
222
223
  this._stats.records += 1
223
224
  this._stats.created += 1
224
- this._records.set(name, record)
225
+ this._recordsByName.set(record.name, record)
226
+ this._recordsByKey.set(record.name, record)
225
227
  }
226
228
 
227
229
  return record.ref()
@@ -617,11 +619,11 @@ class RecordHandler {
617
619
  }
618
620
 
619
621
  _$handle(message) {
620
- let name
622
+ let key
621
623
  if (message.action === C.ACTIONS.ERROR) {
622
- name = message.data[1]
624
+ key = message.data[1]
623
625
  } else {
624
- name = message.data[0]
626
+ key = message.data[0]
625
627
  }
626
628
 
627
629
  if (message.action === C.ACTIONS.SYNC) {
@@ -629,12 +631,12 @@ class RecordHandler {
629
631
  return true
630
632
  }
631
633
 
632
- const listener = this._listeners.get(name)
634
+ const listener = this._listeners.get(key)
633
635
  if (listener && listener._$onMessage(message)) {
634
636
  return true
635
637
  }
636
638
 
637
- const record = this._records.get(name)
639
+ const record = this._recordsByKey.get(key)
638
640
  if (record && record._$onMessage(message)) {
639
641
  return true
640
642
  }
@@ -647,7 +649,7 @@ class RecordHandler {
647
649
  listener._$onConnectionStateChange(connected)
648
650
  }
649
651
 
650
- for (const record of this._records.values()) {
652
+ for (const record of this._recordsByName.values()) {
651
653
  record._$onConnectionStateChange(connected)
652
654
  }
653
655
 
@@ -15,6 +15,7 @@ class Record {
15
15
 
16
16
  this._handler = handler
17
17
  this._name = name
18
+ this._key = utils.h64ToString(name)
18
19
  this._version = ''
19
20
  this._data = jsonPath.EMPTY
20
21
  this._state = C.RECORD_STATE.VOID
@@ -24,7 +25,15 @@ class Record {
24
25
 
25
26
  /** @type Map? */ this._updating = null
26
27
  /** @type Array? */ this._patching = null
27
- this._subscribed = connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SUBSCRIBE, [this._name])
28
+ this._subscribed = connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SUBSCRIBE, [
29
+ this._key,
30
+ this._name,
31
+ ])
32
+ }
33
+
34
+ /** @type { string} */
35
+ get key() {
36
+ return this._key
28
37
  }
29
38
 
30
39
  /** @type {string} */
@@ -62,7 +71,8 @@ class Record {
62
71
  if (this._refs === 1) {
63
72
  this._handler._onPruning(this, false)
64
73
  this._subscribed =
65
- this._subscribed || connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SUBSCRIBE, [this._name])
74
+ this._subscribed ||
75
+ connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SUBSCRIBE, [this._key, this._name])
66
76
  }
67
77
  return this
68
78
  }
@@ -324,7 +334,8 @@ class Record {
324
334
 
325
335
  if (connected) {
326
336
  this._subscribed =
327
- this._refs > 0 && connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SUBSCRIBE, [this._name])
337
+ this._refs > 0 &&
338
+ connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SUBSCRIBE, [this._key, this._name])
328
339
 
329
340
  if (this._updating) {
330
341
  for (const update of this._updating.values()) {
@@ -349,7 +360,7 @@ class Record {
349
360
  invariant(!this._updating, 'must not have updates')
350
361
 
351
362
  if (this._subscribed) {
352
- connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.UNSUBSCRIBE, [this._name])
363
+ connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.UNSUBSCRIBE, [this._key])
353
364
  this._subscribed = false
354
365
  }
355
366
 
@@ -371,7 +382,7 @@ class Record {
371
382
  const prevVersion = this._version
372
383
  const nextVersion = this._makeVersion(parseInt(prevVersion) + 1)
373
384
 
374
- const update = [this._name, nextVersion, jsonPath.stringify(nextData), prevVersion]
385
+ const update = [this._key, nextVersion, jsonPath.stringify(nextData), prevVersion]
375
386
 
376
387
  if (!this._updating) {
377
388
  this._onUpdating(true)
@@ -1,6 +1,176 @@
1
1
  import * as rxjs from 'rxjs'
2
2
  import * as C from '../constants/constants.js'
3
3
  import { h64ToString } from '../utils/utils.js'
4
+ import * as timers from '../utils/timers.js'
5
+
6
+ class Provider {
7
+ #sending = false
8
+ #accepted = false
9
+ #value$ = null
10
+ #name
11
+ #key
12
+ #version
13
+ #timeout
14
+ #valueSubscription
15
+ #patternSubscription
16
+ #observer
17
+ #listener
18
+
19
+ constructor(name, listener) {
20
+ this.#name = name
21
+ this.#key = h64ToString(name)
22
+ this.#listener = listener
23
+ this.#observer = {
24
+ next: (value) => {
25
+ if (value == null) {
26
+ this.next(null) // TODO (fix): This is weird...
27
+ return
28
+ }
29
+
30
+ if (this.#listener._topic === C.TOPIC.EVENT) {
31
+ this.#listener._handler.emit(this.#name, value)
32
+ } else if (this.#listener._topic === C.TOPIC.RECORD) {
33
+ if (typeof value !== 'object' && typeof value !== 'string') {
34
+ this.#listener._error(this.#name, 'invalid value')
35
+ return
36
+ }
37
+
38
+ const body = typeof value !== 'string' ? this.#listener._stringify(value) : value
39
+ const hash = h64ToString(body)
40
+ const version = `INF-${hash}`
41
+
42
+ if (this.#version !== version) {
43
+ this.#version = version
44
+ this.#listener._connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.UPDATE, [
45
+ this.#key,
46
+ version,
47
+ body,
48
+ ])
49
+ }
50
+ }
51
+ },
52
+ error: (err) => {
53
+ this.error(err)
54
+ },
55
+ }
56
+
57
+ this.#start()
58
+ }
59
+
60
+ dispose() {
61
+ this.#stop()
62
+ }
63
+
64
+ accept() {
65
+ if (!this.#value$) {
66
+ return
67
+ }
68
+
69
+ if (this.#valueSubscription) {
70
+ this.#listener._error(this.#name, 'invalid accept: listener started')
71
+ } else {
72
+ // TODO (fix): provider.version = message.data[2]
73
+ this.#valueSubscription = this.#value$.subscribe(this.#observer)
74
+ }
75
+ }
76
+
77
+ reject() {
78
+ this.#listener._error(this.#name, 'invalid reject: not implemented')
79
+ }
80
+
81
+ next(value$) {
82
+ if (!value$) {
83
+ value$ = null
84
+ } else if (typeof value$.subscribe !== 'function') {
85
+ value$ = rxjs.of(value$) // Compat for recursive with value
86
+ }
87
+
88
+ if (Boolean(this.#value$) !== Boolean(value$) && !this.#sending) {
89
+ this.#sending = true
90
+ // TODO (fix): Why async?
91
+ queueMicrotask(() => {
92
+ this.#sending = false
93
+
94
+ if (!this.#patternSubscription) {
95
+ return
96
+ }
97
+
98
+ const accepted = Boolean(this.#value$)
99
+ if (this.#accepted === accepted) {
100
+ return
101
+ }
102
+
103
+ this.#listener._connection.sendMsg(
104
+ this.#listener._topic,
105
+ accepted ? C.ACTIONS.LISTEN_ACCEPT : C.ACTIONS.LISTEN_REJECT,
106
+ [this.#listener._pattern, this.#key],
107
+ )
108
+
109
+ this.#version = null
110
+ this.#accepted = accepted
111
+ })
112
+ }
113
+
114
+ this.#value$ = value$
115
+
116
+ if (this.#valueSubscription) {
117
+ this.#valueSubscription.unsubscribe()
118
+ this.#valueSubscription = this.#value$?.subscribe(this.#observer)
119
+ }
120
+ }
121
+
122
+ error(err) {
123
+ this.#stop()
124
+ // TODO (feat): backoff retryCount * delay?
125
+ // TODO (feat): backoff option?
126
+ this.#timeout = timers.setTimeout(
127
+ (provider) => {
128
+ provider.start()
129
+ },
130
+ 10e3,
131
+ this,
132
+ )
133
+ this.#listener._error(this.#name, err)
134
+ }
135
+
136
+ #start() {
137
+ try {
138
+ const ret$ = this.#listener._callback(this.#name)
139
+ if (this.#listener._recursive && typeof ret$?.subscribe === 'function') {
140
+ this.patternSubscription = ret$.subscribe(this)
141
+ } else {
142
+ this.patternSubscription = rxjs.of(ret$).subscribe(this)
143
+ }
144
+ } catch (err) {
145
+ this.#listener._error(this.#name, err)
146
+ }
147
+ }
148
+
149
+ #stop() {
150
+ if (this.#listener.connected && this.#accepted) {
151
+ this.#listener._connection.sendMsg(this.#listener._topic, C.ACTIONS.LISTEN_REJECT, [
152
+ this.#listener._pattern,
153
+ this.#key,
154
+ ])
155
+ }
156
+
157
+ this.#value$ = null
158
+ this.#version = null
159
+ this.#accepted = false
160
+ this.#sending = false
161
+
162
+ if (this.#timeout) {
163
+ timers.clearTimeout(this.#timeout)
164
+ this.#timeout = null
165
+ }
166
+
167
+ this.#patternSubscription?.unsubscribe()
168
+ this.#patternSubscription = null
169
+
170
+ this.#valueSubscription?.unsubscribe()
171
+ this.#valueSubscription = null
172
+ }
173
+ }
4
174
 
5
175
  export default class Listener {
6
176
  constructor(topic, pattern, callback, handler, { recursive = false, stringify = null } = {}) {
@@ -51,158 +221,20 @@ export default class Listener {
51
221
  if (message.action === C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND) {
52
222
  if (this._subscriptions.has(name)) {
53
223
  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
- const body = typeof value !== 'string' ? this._stringify(value) : value
155
- const hash = h64ToString(body)
156
- const version = `INF-${hash}`
157
-
158
- if (provider.version !== version) {
159
- provider.version = version
160
- this._connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.UPDATE, [
161
- provider.name,
162
- version,
163
- body,
164
- ])
165
- }
166
- }
167
- },
168
- error: provider.error,
169
- }
170
- provider.start = () => {
171
- try {
172
- const ret$ = this._callback(name)
173
- if (this._recursive && typeof ret$?.subscribe === 'function') {
174
- provider.patternSubscription = ret$.subscribe(provider)
175
- } else {
176
- provider.patternSubscription = rxjs.of(ret$).subscribe(provider)
177
- }
178
- } catch (err) {
179
- this._error(provider.name, err)
180
- }
181
- }
182
-
183
- provider.start()
184
-
185
- this._subscriptions.set(provider.name, provider)
186
- } else if (message.action === C.ACTIONS.LISTEN_ACCEPT) {
187
- const provider = this._subscriptions.get(name)
188
- if (!provider?.value$) {
189
- return
190
- }
191
-
192
- if (provider.valueSubscription) {
193
- this._error(name, 'invalid accept: listener started')
194
224
  } else {
195
- // TODO (fix): provider.version = message.data[2]
196
- provider.valueSubscription = provider.value$.subscribe(provider.observer)
225
+ this._subscriptions.set(name, new Provider(name, this))
197
226
  }
227
+ } else if (message.action === C.ACTIONS.LISTEN_ACCEPT) {
228
+ this._subscriptions.get(name)?.accept()
229
+ } else if (message.action === C.ACTIONS.LISTEN_REJECT) {
230
+ this._subscriptions.get(name)?.reject()
198
231
  } else if (message.action === C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_REMOVED) {
199
232
  const provider = this._subscriptions.get(name)
200
-
201
- if (!provider) {
202
- this._error(name, 'invalid remove: listener missing')
233
+ if (provider) {
234
+ provider.dispose()
235
+ this._subscriptions.delete(name)
203
236
  } else {
204
- provider.stop()
205
- this._subscriptions.delete(provider.name)
237
+ this._error(name, 'invalid remove: listener missing')
206
238
  }
207
239
  } else {
208
240
  return false
@@ -1,9 +1,19 @@
1
- import * as rxjs from 'rxjs'
2
1
  import * as C from '../constants/constants.js'
3
2
  import { h64ToString } from '../utils/utils.js'
4
3
 
5
- const PIPE = rxjs.pipe(
6
- rxjs.map((value) => {
4
+ class Observer {
5
+ #name
6
+ #key
7
+ #listener
8
+ #version = ''
9
+
10
+ constructor(name, listener) {
11
+ this.#name = name
12
+ this.#key = h64ToString(name)
13
+ this.#listener = listener
14
+ }
15
+
16
+ next(value) {
7
17
  let data
8
18
  if (value && typeof value === 'string') {
9
19
  if (value.charAt(0) !== '{' && value.charAt(0) !== '[') {
@@ -16,10 +26,34 @@ const PIPE = rxjs.pipe(
16
26
  throw new Error(`invalid value: ${value}`)
17
27
  }
18
28
 
19
- return data
20
- }),
21
- rxjs.distinctUntilChanged(),
22
- )
29
+ const version = data ? `INF-${h64ToString(data)}` : ''
30
+ if (this.#version === version) {
31
+ return
32
+ }
33
+
34
+ if (version) {
35
+ this.#listener._connection.sendMsg(this.#listener._topic, C.ACTIONS.UPDATE, [
36
+ this.#key,
37
+ version,
38
+ data,
39
+ ])
40
+ } else {
41
+ this.#listener._connection.sendMsg(this.#listener._topic, C.ACTIONS.LISTEN_REJECT, [
42
+ this.#listener._pattern,
43
+ this.#key,
44
+ ])
45
+ }
46
+
47
+ this.#version = version
48
+ }
49
+ error(err) {
50
+ this.#listener._error(this.#name, err)
51
+ this.#listener._connection.sendMsg(this.#listener._topic, C.ACTIONS.LISTEN_REJECT, [
52
+ this.#listener._pattern,
53
+ this.#key,
54
+ ])
55
+ }
56
+ }
23
57
 
24
58
  export default class Listener {
25
59
  constructor(topic, pattern, callback, handler, opts) {
@@ -65,28 +99,11 @@ export default class Listener {
65
99
  try {
66
100
  value$ = this._callback(name)
67
101
  } catch (err) {
68
- value$ = rxjs.throwError(() => err)
102
+ this._error(name, err)
69
103
  }
70
104
 
71
105
  if (value$) {
72
- const subscription = value$.pipe(PIPE).subscribe({
73
- next: (data) => {
74
- if (data == null) {
75
- this._connection.sendMsg(this._topic, C.ACTIONS.LISTEN_REJECT, [this._pattern, name])
76
- this._subscriptions.delete(name)
77
- subscription.unsubscribe()
78
- } else {
79
- const version = `INF-${h64ToString(data)}`
80
- this._connection.sendMsg(this._topic, C.ACTIONS.UPDATE, [name, version, data])
81
- }
82
- },
83
- error: (err) => {
84
- this._error(name, err)
85
- this._connection.sendMsg(this._topic, C.ACTIONS.LISTEN_REJECT, [this._pattern, name])
86
- this._subscriptions.delete(name)
87
- },
88
- })
89
- this._subscriptions.set(name, subscription)
106
+ this._subscriptions.set(name, value$.subscribe(new Observer(name, this)))
90
107
  } else {
91
108
  this._connection.sendMsg(this._topic, C.ACTIONS.LISTEN_REJECT, [this._pattern, name])
92
109
  }
@@ -177,10 +177,7 @@ export function removeAbortListener(signal, handler) {
177
177
  }
178
178
  }
179
179
 
180
- // This is a hack to avoid top-level await
181
- // const HASHER = await xxhash()
182
- let HASHER
183
- xxhash().then((hasher) => (HASHER = hasher))
180
+ const HASHER = await xxhash()
184
181
 
185
182
  export function h64ToString(str) {
186
183
  return HASHER.h64ToString(str)