@nxtedition/deepstream.io-client-js 32.0.22 → 32.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/deepstream.io-client-js",
3
- "version": "32.0.22",
3
+ "version": "32.0.23",
4
4
  "description": "the javascript client for deepstream.io",
5
5
  "homepage": "http://deepstream.io",
6
6
  "type": "module",
package/src/client.d.ts CHANGED
@@ -3,14 +3,25 @@ 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
 
10
+ export interface DeepstreamClientOptions {
11
+ reconnectIntervalIncrement?: number
12
+ maxReconnectInterval?: number
13
+ maxReconnectAttempts?: number
14
+ maxPacketSize?: number
15
+ batchSize?: number
16
+ schedule?: ((fn: () => void) => void) | null
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ logger?: any
19
+ }
20
+
10
21
  export default function <
11
22
  Records extends Record<string, unknown> = Record<string, unknown>,
12
23
  Methods extends Record<string, RpcMethodDef> = Record<string, RpcMethodDef>,
13
- >(url: string, options?: unknown): DeepstreamClient<Records, Methods>
24
+ >(url: string, options?: DeepstreamClientOptions): DeepstreamClient<Records, Methods>
14
25
 
15
26
  export type {
16
27
  DsRecord,
@@ -19,9 +30,12 @@ export type {
19
30
  RpcHandler,
20
31
  RpcMethodDef,
21
32
  ProvideOptions,
33
+ EventProvideOptions,
22
34
  SyncOptions,
23
35
  Paths,
24
36
  Get,
37
+ ConnectionStateName,
38
+ DeepstreamErrorEventName,
25
39
  }
26
40
 
27
41
  type RecordStateConstants = Readonly<{
@@ -77,6 +91,36 @@ type EventConstants = Readonly<{
77
91
  }>
78
92
  type EventKey = keyof EventConstants
79
93
  type EventName = EventConstants[EventKey]
94
+ type DeepstreamErrorEventName = Exclude<
95
+ EventName,
96
+ 'connectionStateChanged' | 'connected' | 'MAX_RECONNECTION_ATTEMPTS_REACHED'
97
+ >
98
+
99
+ export interface DeepstreamError extends Error {
100
+ topic?: string
101
+ event?: EventName | null
102
+ data?: unknown
103
+ }
104
+
105
+ export interface DeepstreamMessage {
106
+ raw: string | null
107
+ topic: string | null
108
+ action: string | null
109
+ data: string[]
110
+ }
111
+
112
+ export interface DeepstreamClientEventMap {
113
+ connectionStateChanged: (state: ConnectionStateName) => void
114
+ connected: (connected: boolean) => void
115
+ MAX_RECONNECTION_ATTEMPTS_REACHED: (attempt: number) => void
116
+ error: (error: DeepstreamError) => void
117
+ recv: (message: DeepstreamMessage) => void
118
+ send: (message: DeepstreamMessage) => void
119
+ }
120
+
121
+ type DeepstreamErrorEventMap = {
122
+ [K in DeepstreamErrorEventName]: (error: DeepstreamError) => void
123
+ }
80
124
 
81
125
  export interface DeepstreamClient<
82
126
  Records extends Record<string, unknown> = Record<string, unknown>,
@@ -87,11 +131,21 @@ export interface DeepstreamClient<
87
131
  rpc: RpcHandler<Methods>
88
132
  record: RecordHandler<Records>
89
133
  user: string | null
90
- on: (evt: EventName, callback: (...args: unknown[]) => void) => void
91
- off: (evt: EventName, callback: (...args: unknown[]) => void) => void
134
+ on<K extends keyof (DeepstreamClientEventMap & DeepstreamErrorEventMap)>(
135
+ evt: K,
136
+ callback: (DeepstreamClientEventMap & DeepstreamErrorEventMap)[K],
137
+ ): this
138
+ off<K extends keyof (DeepstreamClientEventMap & DeepstreamErrorEventMap)>(
139
+ evt: K,
140
+ callback: (DeepstreamClientEventMap & DeepstreamErrorEventMap)[K],
141
+ ): this
92
142
  getConnectionState: () => ConnectionStateName
93
143
  close: () => void
94
- login: unknown
144
+ login(callback: (success: boolean, authData: unknown) => void): this
145
+ login(
146
+ authParams: Record<string, unknown>,
147
+ callback: (success: boolean, authData: unknown) => void,
148
+ ): this
95
149
  stats: {
96
150
  record: RecordStats
97
151
  rpc: RpcStats
@@ -1,7 +1,8 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import make, { type DeepstreamClient } from './client.js'
2
+ import make, { type DeepstreamClient, type DeepstreamError } from './client.js'
3
3
  import { expectAssignable, expectError, expectType } from 'tsd'
4
4
  import type { Observable } from 'rxjs'
5
+ import type { EmptyObject } from 'type-fest'
5
6
 
6
7
  interface Records extends Record<string, unknown> {
7
8
  o: {
@@ -22,6 +23,11 @@ interface Records extends Record<string, unknown> {
22
23
  }
23
24
  }
24
25
  }
26
+ possiblyEmpty:
27
+ | {
28
+ pe1: string
29
+ }
30
+ | EmptyObject
25
31
  c: Circular
26
32
  m: {
27
33
  m1: string
@@ -62,7 +68,7 @@ expectAssignable<{ n0?: { n1: { n2: { n3: string } } } } | undefined>(await ds.r
62
68
  expectAssignable<{ n1: { n2: { n3: string } } } | undefined>(await ds.record.get('n', 'n0'))
63
69
 
64
70
  // set withouth path
65
- ds.record.set('n', {}) // empty should always work
71
+ ds.record.set('possiblyEmpty', {}) // empty should always work
66
72
  ds.record.set('n', { n0: { n1: { n2: { n3: 'test' } } } })
67
73
  expectError(ds.record.set('n', { n0: {} })) // nested props are required
68
74
 
@@ -96,10 +102,14 @@ expectError(ds.record.set('n', 'n1.x2', {}))
96
102
  expectError(ds.record.set('n', 'n1.n2.n3', { n4: 22 }))
97
103
 
98
104
  expectAssignable<string>(await ds.record.get('p', 'p1'))
105
+ expectAssignable<{ name: string; version: string; state: number; data: string }>(
106
+ await ds.record.get2('p', 'p1'),
107
+ )
99
108
  expectAssignable<string>(await ds.record.get('p', 'p1', { signal: new AbortController().signal }))
100
109
  expectAssignable<string>(await ds.record.get('p', { path: 'p1' }))
101
110
  expectAssignable<string | undefined>(await ds.record.get('p', 'p2'))
102
111
  expectAssignable<unknown>(await ds.record.get('p', 'x1'))
112
+ expectAssignable<string | undefined>(await ds.record.get('possiblyEmpty', 'pe1'))
103
113
 
104
114
  // observe with options
105
115
  expectAssignable<Observable<{ p1: string; p2?: string; p3: { p4: string } }>>(
@@ -179,6 +189,16 @@ expectAssignable<Promise<void>>(
179
189
  )
180
190
  expectAssignable<Promise<void>>(ds.record.update('p', 'p1', (data) => data, { timeout: 5000 }))
181
191
 
192
+ // update: updater receives version as second argument
193
+ ds.record.update('p', (data, version) => {
194
+ expectType<string>(version)
195
+ return data
196
+ })
197
+ ds.record.update('p', 'p1', (data, version) => {
198
+ expectType<string>(version)
199
+ return data
200
+ })
201
+
182
202
  // Circular
183
203
  expectAssignable<string | undefined>(await ds.record.get('c', 'a.b1'))
184
204
 
@@ -210,6 +230,19 @@ expectAssignable<Promise<typeof rec>>(rec.when({ state: 2, timeout: 5000 }))
210
230
  expectAssignable<Promise<typeof rec>>(rec.when(2, { timeout: 5000 }))
211
231
  expectAssignable<Promise<typeof rec>>(rec.when(2, { signal: new AbortController().signal }))
212
232
 
233
+ // Record.subscribe: callback receives (record, opaque)
234
+ rec.subscribe((record, opaque) => {
235
+ expectType<typeof rec>(record)
236
+ expectType<unknown>(opaque)
237
+ })
238
+ rec.subscribe((record, opaque) => {}, 'my-opaque-token')
239
+
240
+ // Record.unsubscribe: same callback signature
241
+ rec.unsubscribe((record, opaque) => {
242
+ expectType<typeof rec>(record)
243
+ expectType<unknown>(opaque)
244
+ })
245
+
213
246
  // Record.update with options
214
247
  expectAssignable<Promise<void>>(rec.update((x) => x, { signal: new AbortController().signal }))
215
248
  expectAssignable<Promise<void>>(rec.update((x) => x, { timeout: 5000 }))
@@ -220,7 +253,68 @@ expectAssignable<Promise<void>>(
220
253
  expectAssignable<Promise<void>>(rec.update('o0', (x) => x, { timeout: 5000 }))
221
254
  expectAssignable<Promise<void>>(rec.update('o0', (x) => x, { state: 2 }))
222
255
 
256
+ // Record.update: updater receives version as second argument
257
+ rec.update((x, version) => {
258
+ expectType<string>(version)
259
+ return x
260
+ })
261
+ rec.update('o0', (x, version) => {
262
+ expectType<string>(version)
263
+ return x
264
+ })
265
+
223
266
  const state = 'VOID'
224
267
  expectType<0>(ds.record.STATE[state])
225
268
  const unknownState: string = 'VOID'
226
269
  expectType<number>(ds.record.STATE[unknownState])
270
+
271
+ // record.getRecord: [Symbol.dispose] is present
272
+ const recDispose = ds.record.getRecord('o')
273
+ recDispose[Symbol.dispose]()
274
+
275
+ // record.provide: returns Disposer | void
276
+ expectAssignable<(() => void) | void>(ds.record.provide('pattern*', () => ({})))
277
+ const recordDisposer = ds.record.provide('pattern*', () => ({}))
278
+ if (recordDisposer) {
279
+ recordDisposer()
280
+ recordDisposer[Symbol.dispose]()
281
+ }
282
+
283
+ // event.provide: returns (() => void) | void
284
+ expectAssignable<(() => void) | void>(ds.event.provide('pattern*', () => {}, {}))
285
+
286
+ // client.on/off: 'error' is a valid event name
287
+ ds.on('error', (err) => {})
288
+ ds.off('error', (err) => {})
289
+ expectError(ds.on('unknownEvent', () => {}))
290
+
291
+ // client.on: callback arg types per event
292
+ ds.on('error', (err) => {
293
+ expectType<DeepstreamError>(err)
294
+ })
295
+ ds.on('connectionError', (err) => {
296
+ expectType<DeepstreamError>(err)
297
+ })
298
+ ds.on('connectionStateChanged', (state) => {
299
+ expectType<
300
+ | 'CLOSED'
301
+ | 'AWAITING_CONNECTION'
302
+ | 'CHALLENGING'
303
+ | 'AWAITING_AUTHENTICATION'
304
+ | 'AUTHENTICATING'
305
+ | 'OPEN'
306
+ | 'ERROR'
307
+ | 'RECONNECTING'
308
+ >(state)
309
+ })
310
+ ds.on('connected', (connected) => {
311
+ expectType<boolean>(connected)
312
+ })
313
+ ds.on('MAX_RECONNECTION_ATTEMPTS_REACHED', (attempt) => {
314
+ expectType<number>(attempt)
315
+ })
316
+
317
+ // client.on: wrong callback arg types are errors
318
+ expectError(ds.on('connectionStateChanged', (state: number) => {}))
319
+ expectError(ds.on('connected', (connected: string) => {}))
320
+ expectError(ds.on('MAX_RECONNECTION_ATTEMPTS_REACHED', (attempt: string) => {}))
@@ -4,13 +4,22 @@ export default class EventHandler {
4
4
  connected: boolean
5
5
  stats: EventStats
6
6
  subscribe: (name: string, callback: (data: unknown) => void) => void
7
- unsubscribe: (name: string, callback: (data: unknown) => void) => void
7
+ unsubscribe: (name: string, callback?: (data: unknown) => void) => void
8
8
  on: (name: string, callback: (data: unknown) => void) => this
9
9
  once: (name: string, callback: (data: unknown) => void) => this
10
10
  off: (name: string, callback: (data: unknown) => void) => this
11
11
  observe: <Data>(name: string) => Observable<Data>
12
12
  emit: <Data>(name: string, data?: Data) => void
13
- provide: (pattern: string, callback: (name: string) => void, options: unknown) => () => void
13
+ provide: (
14
+ pattern: string,
15
+ callback: (name: string) => void,
16
+ options: EventProvideOptions,
17
+ ) => (() => void) | void
18
+ }
19
+
20
+ export interface EventProvideOptions {
21
+ mode?: 'unicast' | (string & {})
22
+ stringify?: ((input: unknown) => string) | null
14
23
  }
15
24
 
16
25
  export interface EventStats {
@@ -71,21 +71,21 @@ 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
- this.unsubscribe(fn)
82
- callback(...args)
81
+ this.unsubscribe(name, fn)
82
+ callback(name, ...args)
83
83
  }
84
84
  this.subscribe(name, fn)
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
  }
@@ -1,12 +1,6 @@
1
1
  import type { Observable } from 'rxjs'
2
2
  import type DsRecord from './record.js'
3
- import type {
4
- EmptyObject,
5
- Get,
6
- UpdateOptions,
7
- ObserveOptions,
8
- ObserveOptionsWithPath,
9
- } from './record.js'
3
+ import type { Get, UpdateOptions, ObserveOptions, ObserveOptionsWithPath } from './record.js'
10
4
 
11
5
  type Lookup<Table, Name> = Name extends keyof Table ? Table[Name] : unknown
12
6
 
@@ -32,8 +26,8 @@ export default class RecordHandler<Records = Record<string, unknown>> {
32
26
  }
33
27
 
34
28
  JSON: {
35
- EMPTY: EmptyObject
36
- EMPTY_OBJ: EmptyObject
29
+ EMPTY: Record<string, unknown>
30
+ EMPTY_OBJ: Record<string, unknown>
37
31
  EMPTY_ARR: []
38
32
  }
39
33
 
@@ -46,13 +40,44 @@ export default class RecordHandler<Records = Record<string, unknown>> {
46
40
  pattern: string,
47
41
  callback: (key: string) => unknown,
48
42
  optionsOrRecursive?: ProvideOptions | boolean,
49
- ) => Disposer
43
+ ) => Disposer | void
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
+ }
50
75
 
51
76
  sync: (options?: SyncOptions) => Promise<void>
52
77
 
53
78
  set: {
54
79
  // without path:
55
- <Name extends string>(name: Name, data: Lookup<Records, Name> | EmptyObject): void
80
+ <Name extends string>(name: Name, data: Lookup<Records, Name>): void
56
81
 
57
82
  // with path:
58
83
  <Name extends string, Path extends string | string[]>(
@@ -67,14 +92,17 @@ export default class RecordHandler<Records = Record<string, unknown>> {
67
92
  update: {
68
93
  <Name extends string>(
69
94
  name: Name,
70
- updater: (data: Lookup<Records, Name>) => Lookup<Records, Name> | EmptyObject,
95
+ updater: (data: Lookup<Records, Name>, version: string) => Lookup<Records, Name>,
71
96
  options?: UpdateOptions,
72
97
  ): Promise<void>
73
98
 
74
99
  <Name extends string, Path extends string | string[]>(
75
100
  name: Name,
76
101
  path: Path,
77
- updater: (data: Get<Lookup<Records, Name>, Path>) => Get<Lookup<Records, Name>, Path>,
102
+ updater: (
103
+ data: Get<Lookup<Records, Name>, Path>,
104
+ version: string,
105
+ ) => Get<Lookup<Records, Name>, Path>,
78
106
  options?: UpdateOptions,
79
107
  ): Promise<void>
80
108
  }
@@ -113,6 +141,73 @@ export default class RecordHandler<Records = Record<string, unknown>> {
113
141
  ): Observable<Get<Lookup<Records, Name>, Path>>
114
142
  }
115
143
 
144
+ observe2: {
145
+ <Name extends string>(
146
+ name: Name,
147
+ options: ObserveOptions,
148
+ ): Observable<{
149
+ name: string
150
+ version: string
151
+ state: number
152
+ data: Lookup<Records, Name>
153
+ }>
154
+
155
+ <Name extends string, Path extends string | string[]>(
156
+ name: Name,
157
+ options: ObserveOptionsWithPath<Path>,
158
+ ): Observable<{
159
+ name: string
160
+ version: string
161
+ state: number
162
+ data: Get<Lookup<Records, Name>, Path>
163
+ }>
164
+
165
+ <Name extends string>(
166
+ name: Name,
167
+ state?: number,
168
+ options?: ObserveOptions,
169
+ ): Observable<{
170
+ name: string
171
+ version: string
172
+ state: number
173
+ data: Lookup<Records, Name>
174
+ }>
175
+
176
+ <Name extends string, Path extends string | string[]>(
177
+ name: Name,
178
+ state?: number,
179
+ options?: ObserveOptionsWithPath<Path>,
180
+ ): Observable<{
181
+ name: string
182
+ version: string
183
+ state: number
184
+ data: Get<Lookup<Records, Name>, Path>
185
+ }>
186
+
187
+ <Name extends string, Path extends string | string[]>(
188
+ name: Name,
189
+ path: Path,
190
+ options?: ObserveOptionsWithPath<Path>,
191
+ ): Observable<{
192
+ name: string
193
+ version: string
194
+ state: number
195
+ data: Get<Lookup<Records, Name>, Path>
196
+ }>
197
+
198
+ <Name extends string, Path extends string | string[]>(
199
+ name: Name,
200
+ path: Path,
201
+ state?: number,
202
+ options?: ObserveOptionsWithPath<Path>,
203
+ ): Observable<{
204
+ name: string
205
+ version: string
206
+ state: number
207
+ data: Get<Lookup<Records, Name>, Path>
208
+ }>
209
+ }
210
+
116
211
  get: {
117
212
  <Name extends string>(name: Name, options: ObserveOptions): Promise<Lookup<Records, Name>>
118
213
 
@@ -147,11 +242,11 @@ export default class RecordHandler<Records = Record<string, unknown>> {
147
242
  ): Promise<Get<Lookup<Records, Name>, Path>>
148
243
  }
149
244
 
150
- observe2: {
245
+ get2: {
151
246
  <Name extends string>(
152
247
  name: Name,
153
248
  options: ObserveOptions,
154
- ): Observable<{
249
+ ): Promise<{
155
250
  name: string
156
251
  version: string
157
252
  state: number
@@ -161,7 +256,7 @@ export default class RecordHandler<Records = Record<string, unknown>> {
161
256
  <Name extends string, Path extends string | string[]>(
162
257
  name: Name,
163
258
  options: ObserveOptionsWithPath<Path>,
164
- ): Observable<{
259
+ ): Promise<{
165
260
  name: string
166
261
  version: string
167
262
  state: number
@@ -172,7 +267,7 @@ export default class RecordHandler<Records = Record<string, unknown>> {
172
267
  name: Name,
173
268
  state?: number,
174
269
  options?: ObserveOptions,
175
- ): Observable<{
270
+ ): Promise<{
176
271
  name: string
177
272
  version: string
178
273
  state: number
@@ -183,7 +278,7 @@ export default class RecordHandler<Records = Record<string, unknown>> {
183
278
  name: Name,
184
279
  state?: number,
185
280
  options?: ObserveOptionsWithPath<Path>,
186
- ): Observable<{
281
+ ): Promise<{
187
282
  name: string
188
283
  version: string
189
284
  state: number
@@ -194,7 +289,7 @@ export default class RecordHandler<Records = Record<string, unknown>> {
194
289
  name: Name,
195
290
  path: Path,
196
291
  options?: ObserveOptionsWithPath<Path>,
197
- ): Observable<{
292
+ ): Promise<{
198
293
  name: string
199
294
  version: string
200
295
  state: number
@@ -206,7 +301,7 @@ export default class RecordHandler<Records = Record<string, unknown>> {
206
301
  path: Path,
207
302
  state?: number,
208
303
  options?: ObserveOptionsWithPath<Path>,
209
- ): Observable<{
304
+ ): Promise<{
210
305
  name: string
211
306
  version: string
212
307
  state: number
@@ -223,12 +318,13 @@ export interface RecordStats {
223
318
  pruning: number
224
319
  patching: number
225
320
  subscriptions: number
321
+ listeners: number
226
322
  }
227
323
 
228
324
  export interface ProvideOptions {
229
325
  recursive?: boolean
230
326
  stringify?: ((input: unknown) => string) | null
231
- mode: undefined | null | 'unicast'
327
+ mode?: null | 'unicast' | (string & {})
232
328
  }
233
329
 
234
330
  export interface SyncOptions {
@@ -3,7 +3,6 @@ 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'
6
- import invariant from 'invariant'
7
6
  import jsonPath from '@nxtedition/json-path'
8
7
  import * as utils from '../utils/utils.js'
9
8
  import xuid from 'xuid'
@@ -121,10 +120,6 @@ class RecordHandler {
121
120
  this._stats = {
122
121
  updating: 0,
123
122
  created: 0,
124
- destroyed: 0,
125
- records: 0,
126
- pruning: 0,
127
- patching: 0,
128
123
  }
129
124
 
130
125
  this._syncQueue = []
@@ -161,12 +156,6 @@ class RecordHandler {
161
156
  }
162
157
 
163
158
  _onPruning(rec, value) {
164
- if (value) {
165
- this._stats.pruning += 1
166
- } else {
167
- this._stats.pruning -= 1
168
- }
169
-
170
159
  if (value) {
171
160
  this._pruning.add(rec)
172
161
  } else {
@@ -175,16 +164,10 @@ class RecordHandler {
175
164
  }
176
165
 
177
166
  _onUpdating(rec, value) {
178
- const callbacks = this._updating.get(rec)
179
-
180
167
  if (value) {
181
- invariant(!callbacks, 'updating callbacks must not exist')
182
- this._stats.updating += 1
183
168
  this._updating.set(rec, [])
184
169
  } else {
185
- invariant(callbacks, 'updating callbacks must exist')
186
-
187
- this._stats.updating -= 1
170
+ const callbacks = this._updating.get(rec)
188
171
  this._updating.delete(rec)
189
172
  for (const callback of callbacks) {
190
173
  callback()
@@ -194,11 +177,8 @@ class RecordHandler {
194
177
 
195
178
  _onPatching(rec, value) {
196
179
  if (value) {
197
- this._stats.patching += 1
198
180
  this._patching.set(rec, [])
199
181
  } else {
200
- this._stats.patching -= 1
201
-
202
182
  const callbacks = this._patching.get(rec)
203
183
  this._patching.delete(rec)
204
184
  for (const callback of callbacks) {
@@ -213,13 +193,18 @@ class RecordHandler {
213
193
 
214
194
  get stats() {
215
195
  let subscriptions = 0
216
- for (const listener of this._listeners.values()) {
217
- subscriptions += listener.subscriptions ?? 0
196
+ for (const { stats } of this._listeners.values()) {
197
+ subscriptions += stats.subscriptions ?? 0
218
198
  }
219
199
 
220
200
  return {
221
201
  ...this._stats,
222
202
  subscriptions,
203
+ patching: this._patching.size,
204
+ updating: this._updating.size,
205
+ putting: this._putting.size,
206
+ records: this._records.size,
207
+ listeners: this._listeners.size,
223
208
  }
224
209
  }
225
210
 
@@ -1,36 +1,15 @@
1
1
  import type RecordHandler from './record-handler.js'
2
- import type { Get, EmptyObject, SingleKeyObject } from 'type-fest'
3
- export type { Get, Paths, EmptyObject } from 'type-fest'
2
+ import type { Get as _Get, AllUnionFields } from 'type-fest'
3
+ export type { Paths } from 'type-fest'
4
4
 
5
- // When getting, for convenience, we say the data might be partial under some
6
- // circumstances.
7
- //
8
- // When you e.g. do record.get or record.update, there is always a possibility
9
- // that the data object is empty. The naive correct type for that would be
10
- // `Data | EmptyObject`. However, that forces the user to always type guard
11
- // against the empty object case. This type tries to allow the user to skip
12
- // that check in some cases, where it should be safe to do so.
13
- export type GettablePossibleEmpty<Data> = keyof Data extends never
14
- ? EmptyObject // If there are no keys at all
15
- : Partial<Data> extends Data
16
- ? // All properties in Data are already optional, so we can safely return it
17
- // as is. The user just need to check the properties themselves instead.
18
- Data
19
- : SingleKeyObject<Data> extends never
20
- ? // There are more than one property in Data, and some of them are
21
- // required. That means that the user must always check for the empty
22
- // object case.
23
- Data | EmptyObject
24
- : // There is exactly one property in Data, and it is required. In this
25
- // particular case, we can safely use Data as the "empty" type, but
26
- // with the single property turned optional.
27
- {
28
- [K in keyof Data]+?: Data[K]
29
- }
30
-
31
- // When setting the data must fully adhere to the Data type, or exactly an
32
- // empty object.
33
- export type SettablePossibleEmpty<Data> = Data | EmptyObject
5
+ // HACK: Wrap type-fest's Get to get rid of EmptyObject from union
6
+ type RemoveSymbolKeys<T> = {
7
+ [K in keyof T as K extends symbol ? never : K]: T[K]
8
+ }
9
+ export type Get<BaseType, Path extends string | readonly string[]> = _Get<
10
+ RemoveSymbolKeys<AllUnionFields<BaseType>>,
11
+ Path
12
+ >
34
13
 
35
14
  export interface WhenOptions {
36
15
  signal?: AbortSignal
@@ -45,6 +24,7 @@ export interface UpdateOptions {
45
24
  }
46
25
 
47
26
  export interface ObserveOptions {
27
+ key?: string
48
28
  signal?: AbortSignal
49
29
  timeout?: number
50
30
  state?: number
@@ -67,12 +47,21 @@ export default class Record<Data = unknown> {
67
47
 
68
48
  ref(): Record<Data>
69
49
  unref(): Record<Data>
70
- subscribe(callback: (record: Record<Data>) => void, opaque?: unknown): Record<Data>
71
- unsubscribe(callback: (record: Record<Data>) => void, opaque?: unknown): Record<Data>
50
+ [Symbol.dispose](): void
51
+ subscribe(
52
+ callback: (record: Record<Data>, opaque: unknown) => void,
53
+ opaque?: unknown,
54
+ ): Record<Data>
55
+ unsubscribe(
56
+ callback: (record: Record<Data>, opaque: unknown) => void,
57
+ opaque?: unknown,
58
+ ): Record<Data>
72
59
 
73
60
  get: {
74
61
  // with path
75
62
  <P extends string | string[]>(path: P): Get<Data, P>
63
+ // with function mapper
64
+ <R>(fn: (data: Data) => R): R
76
65
  // without path
77
66
  (): Data
78
67
  (path: undefined | string | string[]): unknown
@@ -85,7 +74,7 @@ export default class Record<Data = unknown> {
85
74
  dataAtPath: unknown extends Get<Data, P> ? never : Get<Data, P>,
86
75
  ): void
87
76
  // without path
88
- (data: SettablePossibleEmpty<Data>): void
77
+ (data: Data): void
89
78
  }
90
79
 
91
80
  when: {
@@ -97,13 +86,13 @@ export default class Record<Data = unknown> {
97
86
  update: {
98
87
  // without path
99
88
  (
100
- updater: (data: Readonly<Data>) => SettablePossibleEmpty<Data>,
89
+ updater: (data: Readonly<Data>, version: string) => Data,
101
90
  options?: UpdateOptions,
102
91
  ): Promise<void>
103
92
  // with path
104
93
  <P extends string | string[]>(
105
94
  path: P,
106
- updater: (dataAtPath: Readonly<Get<Data, P>>) => Get<Data, P>,
95
+ updater: (dataAtPath: Readonly<Get<Data, P>>, version: string) => Get<Data, P>,
107
96
  options?: UpdateOptions,
108
97
  ): Promise<void>
109
98
  }
@@ -10,8 +10,11 @@ export default class RpcHandler<
10
10
 
11
11
  provide: <Name extends keyof Methods>(
12
12
  name: Name,
13
- callback: (args: Methods[Name][0], response: RpcResponse<Methods[Name][1]>) => void,
14
- ) => UnprovideFn
13
+ callback: (
14
+ args: Methods[Name][0],
15
+ response: RpcResponse<Methods[Name][1]>,
16
+ ) => Methods[Name][1] | Promise<Methods[Name][1]> | Promise<void> | void,
17
+ ) => UnprovideFn | void
15
18
 
16
19
  unprovide: <Name extends keyof Methods>(name: Name) => void
17
20
 
@@ -22,7 +25,7 @@ export default class RpcHandler<
22
25
  ReturnValue extends Name extends keyof Methods ? Methods[Name][1] : unknown,
23
26
  >(
24
27
  name: Name,
25
- args: Args,
28
+ args?: Args,
26
29
  ): Promise<ReturnValue>
27
30
  <
28
31
  Name extends keyof Methods | string,
@@ -30,7 +33,7 @@ export default class RpcHandler<
30
33
  ReturnValue extends Name extends keyof Methods ? Methods[Name][1] : unknown,
31
34
  >(
32
35
  name: Name,
33
- args: Args,
36
+ args: Args | undefined,
34
37
  callback: (error: unknown, response: ReturnValue) => void,
35
38
  ): void
36
39
  }
@@ -0,0 +1,40 @@
1
+ import make from '../client.js'
2
+ import { expectAssignable, expectError, expectType } from 'tsd'
3
+
4
+ interface Methods extends Record<string, [unknown, unknown]> {
5
+ greet: [{ name: string }, { message: string }]
6
+ }
7
+
8
+ const ds = make<Record<string, unknown>, Methods>('')
9
+
10
+ // provide: callback may return void, a value, or a Promise — all valid
11
+ ds.rpc.provide('greet', (_args, _response) => {})
12
+ ds.rpc.provide('greet', (_args, _response) => ({ message: 'hello' }))
13
+ ds.rpc.provide('greet', async (_args, _response) => ({ message: 'hello' }))
14
+ // async callback that uses response.send() directly — returns Promise<void>
15
+ ds.rpc.provide('greet', async (_args, response) => {
16
+ response.send({ message: 'hello' })
17
+ })
18
+
19
+ // provide: returning the wrong shape is an error
20
+ expectError(ds.rpc.provide('greet', (_args, _response) => ({ notMessage: 'hello' })))
21
+
22
+ // provide: response.completed is boolean
23
+ ds.rpc.provide('greet', (_args, response) => {
24
+ expectType<boolean>(response.completed)
25
+ })
26
+
27
+ // provide: return type is UnprovideFn | void
28
+ expectAssignable<(() => void) | void>(ds.rpc.provide('greet', () => {}))
29
+
30
+ // make: args is optional (no args)
31
+ expectAssignable<Promise<{ message: string }>>(ds.rpc.make('greet'))
32
+ // make: args provided
33
+ expectAssignable<Promise<{ message: string }>>(ds.rpc.make('greet', { name: 'world' }))
34
+ // make: args explicitly undefined
35
+ expectAssignable<Promise<{ message: string }>>(ds.rpc.make('greet', undefined))
36
+ // make: callback form — args required positionally but can be undefined
37
+ ds.rpc.make('greet', undefined, (err, res) => {
38
+ expectType<unknown>(err)
39
+ expectAssignable<{ message: string } | undefined>(res)
40
+ })
@@ -1,4 +1,5 @@
1
1
  export default class RpcResponse<Data> {
2
+ completed: boolean
2
3
  reject: () => void
3
4
  error: (error: Error | string) => void
4
5
  send: (data: Data) => 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/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
+ }
@@ -83,14 +83,6 @@ export function setTimeout(callback, timeoutDuration) {
83
83
  }
84
84
  }
85
85
 
86
- export function setInterval(callback, intervalDuration) {
87
- if (intervalDuration !== null) {
88
- return setInterval(callback, intervalDuration)
89
- } else {
90
- return -1
91
- }
92
- }
93
-
94
86
  export function compareRev(a, b) {
95
87
  if (!a) {
96
88
  return b ? -1 : 0