@nxtedition/deepstream.io-client-js 30.0.1 → 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.
@@ -0,0 +1,110 @@
1
+ import type RecordHandler from './record-handler.js'
2
+ import type { EmptyObject, SingleKeyObject } from 'type-fest'
3
+
4
+ type Paths<T> = keyof T
5
+ type Get<Data, Path extends string> = Path extends keyof Data ? Data[Path] : unknown
6
+
7
+ export type { EmptyObject } from 'type-fest'
8
+
9
+ // When getting, for convenience, we say the data might be partial under some
10
+ // circumstances.
11
+ //
12
+ // When you e.g. do record.get or record.update, there is always a possibility
13
+ // that the data object is empty. The naive correct type for that would be
14
+ // `Data | EmptyObject`. However, that forces the user to always type guard
15
+ // against the empty object case. This type tries to allow the user to skip
16
+ // that check in some cases, where it should be safe to do so.
17
+ export type GettablePossibleEmpty<Data> = keyof Data extends never
18
+ ? EmptyObject // If there are no keys at all
19
+ : Partial<Data> extends Data
20
+ ? // All properties in Data are already optional, so we can safely return it
21
+ // as is. The user just need to check the properties themselves instead.
22
+ Data
23
+ : SingleKeyObject<Data> extends never
24
+ ? // There are more than one property in Data, and some of them are
25
+ // required. That means that the user must always check for the empty
26
+ // object case.
27
+ Data | EmptyObject
28
+ : // There is exactly one property in Data, and it is required. In this
29
+ // particular case, we can safely use Data as the "empty" type, but
30
+ // with the single property turned optional.
31
+ {
32
+ [K in keyof Data]+?: Data[K]
33
+ }
34
+
35
+ // When setting the data must fully adhere to the Data type, or exactly an
36
+ // empty object.
37
+ export type SettablePossibleEmpty<Data> = Data | EmptyObject
38
+
39
+ export interface WhenOptions {
40
+ timeout?: number
41
+ signal?: AbortSignal
42
+ }
43
+
44
+ export interface UpdateOptions {
45
+ signal?: AbortSignal
46
+ }
47
+
48
+ export default class Record<Data> {
49
+ constructor(name: string, handler: RecordHandler)
50
+
51
+ readonly name: string
52
+ readonly version: string
53
+ readonly data: GettablePossibleEmpty<Data>
54
+ readonly state: number
55
+ readonly refs: number
56
+
57
+ ref(): Record<Data>
58
+ unref(): Record<Data>
59
+ subscribe(callback: (record: Record<Data>) => void, opaque?: unknown): Record<Data>
60
+ unsubscribe(callback: (record: Record<Data>) => void, opaque?: unknown): Record<Data>
61
+
62
+ get: {
63
+ // with path
64
+ <Path extends Paths<Data>, DataAtPath extends Get<Data, Path> = Get<Data, Path>>(
65
+ path: Path,
66
+ ): DataAtPath | undefined
67
+ // without path
68
+ (): GettablePossibleEmpty<Data>
69
+ // implementation
70
+ <Path extends Paths<Data>, DataAtPath extends Get<Data, Path> = Get<Data, Path>>(
71
+ path?: Path,
72
+ ): Path extends undefined ? GettablePossibleEmpty<Data> : DataAtPath | undefined
73
+ }
74
+
75
+ set: {
76
+ // with path
77
+ <Path extends Paths<Data>, DataAtPath extends Get<Data, Path>>(
78
+ path: Path,
79
+ dataAtPath: DataAtPath,
80
+ ): void
81
+ // without path
82
+ (data: SettablePossibleEmpty<Data>): void
83
+ // implementation
84
+ <Path extends Paths<Data>, DataAtPath extends Get<Data, Path>>(
85
+ ...args: [pathOrData: Path | SettablePossibleEmpty<Data>, value?: DataAtPath]
86
+ ): void
87
+ }
88
+
89
+ when: {
90
+ (): Promise<Record<Data>>
91
+ (state: number): Promise<Record<Data>>
92
+ (options: WhenOptions): Promise<Record<Data>>
93
+ (state: number, options: WhenOptions): Promise<Record<Data>>
94
+ }
95
+
96
+ update<
97
+ Path extends Paths<Data>,
98
+ PathOrUpdater extends
99
+ | Path
100
+ | ((data: Readonly<GettablePossibleEmpty<Data>>) => SettablePossibleEmpty<Data>),
101
+ >(
102
+ ...args: PathOrUpdater extends Path
103
+ ? [
104
+ path: Path,
105
+ updater: (dataAtPath: Readonly<Get<Data, Path>> | undefined) => Get<Data, Path>,
106
+ options?: UpdateOptions,
107
+ ]
108
+ : [updater: PathOrUpdater, options?: UpdateOptions]
109
+ ): Promise<void>
110
+ }
@@ -15,7 +15,6 @@ class Record {
15
15
 
16
16
  this._handler = handler
17
17
  this._name = name
18
- this._key = utils.h64ToString(name)
19
18
  this._version = ''
20
19
  this._data = jsonPath.EMPTY
21
20
  this._state = C.RECORD_STATE.VOID
@@ -25,15 +24,7 @@ class Record {
25
24
 
26
25
  /** @type Map? */ this._updating = null
27
26
  /** @type Array? */ this._patching = null
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
27
+ this._subscribed = connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SUBSCRIBE, [this._name])
37
28
  }
38
29
 
39
30
  /** @type {string} */
@@ -71,8 +62,7 @@ class Record {
71
62
  if (this._refs === 1) {
72
63
  this._handler._onPruning(this, false)
73
64
  this._subscribed =
74
- this._subscribed ||
75
- connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SUBSCRIBE, [this._key, this._name])
65
+ this._subscribed || connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SUBSCRIBE, [this._name])
76
66
  }
77
67
  return this
78
68
  }
@@ -88,6 +78,10 @@ class Record {
88
78
  return this
89
79
  }
90
80
 
81
+ [Symbol.dispose]() {
82
+ this.unref()
83
+ }
84
+
91
85
  /**
92
86
  * @param {*} fn
93
87
  * @param {*} opaque
@@ -308,7 +302,7 @@ class Record {
308
302
  .then(() => {
309
303
  const prev = this.get(path)
310
304
  const next = updater(prev, this._version)
311
- if (prev !== next) {
305
+ if (prev !== next && (path || next != null)) {
312
306
  this.set(path, next)
313
307
  }
314
308
  })
@@ -334,8 +328,7 @@ class Record {
334
328
 
335
329
  if (connected) {
336
330
  this._subscribed =
337
- this._refs > 0 &&
338
- connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SUBSCRIBE, [this._key, this._name])
331
+ this._refs > 0 && connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.SUBSCRIBE, [this._name])
339
332
 
340
333
  if (this._updating) {
341
334
  for (const update of this._updating.values()) {
@@ -360,7 +353,7 @@ class Record {
360
353
  invariant(!this._updating, 'must not have updates')
361
354
 
362
355
  if (this._subscribed) {
363
- connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.UNSUBSCRIBE, [this._key])
356
+ connection.sendMsg(C.TOPIC.RECORD, C.ACTIONS.UNSUBSCRIBE, [this._name])
364
357
  this._subscribed = false
365
358
  }
366
359
 
@@ -382,7 +375,7 @@ class Record {
382
375
  const prevVersion = this._version
383
376
  const nextVersion = this._makeVersion(parseInt(prevVersion) + 1)
384
377
 
385
- const update = [this._key, nextVersion, jsonPath.stringify(nextData), prevVersion]
378
+ const update = [this._name, nextVersion, jsonPath.stringify(nextData), prevVersion]
386
379
 
387
380
  if (!this._updating) {
388
381
  this._onUpdating(true)
@@ -0,0 +1,42 @@
1
+ import RpcResponse from './rpc-response.js'
2
+
3
+ export type RpcMethodDef = [arguments: unknown, response: unknown]
4
+
5
+ export default class RpcHandler<Methods extends Record<string, RpcMethodDef>> {
6
+ connected: boolean
7
+ stats: RpcStats
8
+
9
+ provide: <Name extends keyof Methods>(
10
+ name: Name,
11
+ callback: (args: Methods[Name][0], response: RpcResponse<Methods[Name][1]>) => void,
12
+ ) => UnprovideFn
13
+
14
+ unprovide: <Name extends keyof Methods>(name: Name) => void
15
+
16
+ make: {
17
+ <
18
+ Name extends keyof Methods | string,
19
+ Args extends Name extends keyof Methods ? Methods[Name][0] : unknown,
20
+ ReturnValue extends Name extends keyof Methods ? Methods[Name][1] : unknown,
21
+ >(
22
+ name: Name,
23
+ args: Args,
24
+ ): Promise<ReturnValue>
25
+ <
26
+ Name extends keyof Methods | string,
27
+ Args extends Name extends keyof Methods ? Methods[Name][0] : unknown,
28
+ ReturnValue extends Name extends keyof Methods ? Methods[Name][1] : unknown,
29
+ >(
30
+ name: Name,
31
+ args: Args,
32
+ callback: (error: unknown, response: ReturnValue) => void,
33
+ ): void
34
+ }
35
+ }
36
+
37
+ type UnprovideFn = () => void
38
+
39
+ export interface RpcStats {
40
+ listeners: number
41
+ rpcs: number
42
+ }
@@ -0,0 +1,5 @@
1
+ export default class RpcResponse<Data> {
2
+ reject: () => void
3
+ error: (error: Error | string) => void
4
+ send: (data: Data) => void
5
+ }
@@ -1,176 +1,6 @@
1
1
  import * as rxjs from 'rxjs'
2
2
  import * as C from '../constants/constants.js'
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
- }
3
+ import { h64ToString, findBigIntPaths } from '../utils/utils.js'
174
4
 
175
5
  export default class Listener {
176
6
  constructor(topic, pattern, callback, handler, { recursive = false, stringify = null } = {}) {
@@ -221,20 +51,173 @@ export default class Listener {
221
51
  if (message.action === C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND) {
222
52
  if (this._subscriptions.has(name)) {
223
53
  this._error(name, 'invalid add: listener exists')
224
- } else {
225
- this._subscriptions.set(name, new Provider(name, this))
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
226
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)
227
201
  } 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()
231
- } else if (message.action === C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_REMOVED) {
232
202
  const provider = this._subscriptions.get(name)
233
- if (provider) {
234
- provider.dispose()
235
- this._subscriptions.delete(name)
203
+ if (!provider?.value$) {
204
+ return
205
+ }
206
+
207
+ if (provider.valueSubscription) {
208
+ this._error(name, 'invalid accept: listener started')
236
209
  } else {
210
+ // TODO (fix): provider.version = message.data[2]
211
+ provider.valueSubscription = provider.value$.subscribe(provider.observer)
212
+ }
213
+ } else if (message.action === C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_REMOVED) {
214
+ const provider = this._subscriptions.get(name)
215
+
216
+ if (!provider) {
237
217
  this._error(name, 'invalid remove: listener missing')
218
+ } else {
219
+ provider.stop()
220
+ this._subscriptions.delete(provider.name)
238
221
  }
239
222
  } else {
240
223
  return false
@@ -256,7 +239,7 @@ export default class Listener {
256
239
 
257
240
  _reset() {
258
241
  for (const provider of this._subscriptions.values()) {
259
- provider.dispose()
242
+ provider.stop()
260
243
  }
261
244
  this._subscriptions.clear()
262
245
  }
@@ -78,6 +78,10 @@ class Timeout {
78
78
  clear() {
79
79
  this.state = -1
80
80
  }
81
+
82
+ [Symbol.dispose]() {
83
+ this.state = -1
84
+ }
81
85
  }
82
86
 
83
87
  export function setTimeout(callback, delay, opaque) {