@tanstack/db 0.5.23 → 0.5.25

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.
Files changed (69) hide show
  1. package/dist/cjs/collection/change-events.cjs +1 -1
  2. package/dist/cjs/collection/change-events.cjs.map +1 -1
  3. package/dist/cjs/collection/changes.cjs +6 -1
  4. package/dist/cjs/collection/changes.cjs.map +1 -1
  5. package/dist/cjs/collection/lifecycle.cjs +11 -0
  6. package/dist/cjs/collection/lifecycle.cjs.map +1 -1
  7. package/dist/cjs/collection/subscription.cjs +18 -5
  8. package/dist/cjs/collection/subscription.cjs.map +1 -1
  9. package/dist/cjs/collection/subscription.d.cts +7 -1
  10. package/dist/cjs/indexes/base-index.cjs.map +1 -1
  11. package/dist/cjs/indexes/base-index.d.cts +10 -6
  12. package/dist/cjs/indexes/btree-index.cjs +64 -24
  13. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  14. package/dist/cjs/indexes/btree-index.d.cts +31 -9
  15. package/dist/cjs/indexes/reverse-index.cjs +6 -0
  16. package/dist/cjs/indexes/reverse-index.cjs.map +1 -1
  17. package/dist/cjs/indexes/reverse-index.d.cts +4 -2
  18. package/dist/cjs/query/builder/index.cjs +2 -2
  19. package/dist/cjs/query/builder/index.cjs.map +1 -1
  20. package/dist/cjs/query/live/collection-config-builder.cjs +4 -1
  21. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  22. package/dist/cjs/query/live/collection-subscriber.cjs +111 -30
  23. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  24. package/dist/cjs/query/live/collection-subscriber.d.cts +5 -0
  25. package/dist/cjs/types.d.cts +16 -0
  26. package/dist/cjs/utils/comparison.cjs +16 -0
  27. package/dist/cjs/utils/comparison.cjs.map +1 -1
  28. package/dist/cjs/utils/comparison.d.cts +21 -0
  29. package/dist/esm/collection/change-events.js +1 -1
  30. package/dist/esm/collection/change-events.js.map +1 -1
  31. package/dist/esm/collection/changes.js +6 -1
  32. package/dist/esm/collection/changes.js.map +1 -1
  33. package/dist/esm/collection/lifecycle.js +11 -0
  34. package/dist/esm/collection/lifecycle.js.map +1 -1
  35. package/dist/esm/collection/subscription.d.ts +7 -1
  36. package/dist/esm/collection/subscription.js +18 -5
  37. package/dist/esm/collection/subscription.js.map +1 -1
  38. package/dist/esm/indexes/base-index.d.ts +10 -6
  39. package/dist/esm/indexes/base-index.js.map +1 -1
  40. package/dist/esm/indexes/btree-index.d.ts +31 -9
  41. package/dist/esm/indexes/btree-index.js +65 -25
  42. package/dist/esm/indexes/btree-index.js.map +1 -1
  43. package/dist/esm/indexes/reverse-index.d.ts +4 -2
  44. package/dist/esm/indexes/reverse-index.js +6 -0
  45. package/dist/esm/indexes/reverse-index.js.map +1 -1
  46. package/dist/esm/query/builder/index.js +2 -2
  47. package/dist/esm/query/builder/index.js.map +1 -1
  48. package/dist/esm/query/live/collection-config-builder.js +4 -1
  49. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  50. package/dist/esm/query/live/collection-subscriber.d.ts +5 -0
  51. package/dist/esm/query/live/collection-subscriber.js +112 -31
  52. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  53. package/dist/esm/types.d.ts +16 -0
  54. package/dist/esm/utils/comparison.d.ts +21 -0
  55. package/dist/esm/utils/comparison.js +16 -0
  56. package/dist/esm/utils/comparison.js.map +1 -1
  57. package/package.json +1 -1
  58. package/src/collection/change-events.ts +1 -1
  59. package/src/collection/changes.ts +6 -1
  60. package/src/collection/lifecycle.ts +14 -0
  61. package/src/collection/subscription.ts +38 -10
  62. package/src/indexes/base-index.ts +19 -6
  63. package/src/indexes/btree-index.ts +101 -30
  64. package/src/indexes/reverse-index.ts +13 -2
  65. package/src/query/builder/index.ts +4 -4
  66. package/src/query/live/collection-config-builder.ts +4 -5
  67. package/src/query/live/collection-subscriber.ts +173 -50
  68. package/src/types.ts +16 -0
  69. package/src/utils/comparison.ts +34 -0
@@ -1,4 +1,4 @@
1
- import { MultiSet } from '@tanstack/db-ivm'
1
+ import { MultiSet, serializeValue } from '@tanstack/db-ivm'
2
2
  import {
3
3
  normalizeExpressionPaths,
4
4
  normalizeOrderByPaths,
@@ -26,6 +26,11 @@ export class CollectionSubscriber<
26
26
  // Keep track of the biggest value we've sent so far (needed for orderBy optimization)
27
27
  private biggest: any = undefined
28
28
 
29
+ // Track the most recent ordered load request key (cursor + window).
30
+ // This avoids infinite loops from cached data re-writes while still allowing
31
+ // window moves or new keys at the same cursor value to trigger new requests.
32
+ private lastLoadRequestKey: string | undefined
33
+
29
34
  // Track deferred promises for subscription loading states
30
35
  private subscriptionLoadingPromises = new Map<
31
36
  CollectionSubscription,
@@ -37,6 +42,11 @@ export class CollectionSubscriber<
37
42
  // can potentially send the same item to D2 multiple times.
38
43
  private sentToD2Keys = new Set<string | number>()
39
44
 
45
+ // Direct load tracking callback for ordered path (set during subscribeToOrderedChanges,
46
+ // used by loadNextItems for subsequent requestLimitedSnapshot calls)
47
+ private orderedLoadSubsetResult?: (result: Promise<void> | true) => void
48
+ private pendingOrderedLoadPromise: Promise<void> | undefined
49
+
40
50
  constructor(
41
51
  private alias: string,
42
52
  private collectionId: string,
@@ -58,35 +68,29 @@ export class CollectionSubscriber<
58
68
  private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {
59
69
  const orderByInfo = this.getOrderByInfo()
60
70
 
61
- // Track load promises using subscription from the event (avoids circular dependency)
62
- const trackLoadPromise = (subscription: CollectionSubscription) => {
63
- // Guard against duplicate transitions
64
- if (!this.subscriptionLoadingPromises.has(subscription)) {
65
- let resolve: () => void
66
- const promise = new Promise<void>((res) => {
67
- resolve = res
68
- })
69
-
70
- this.subscriptionLoadingPromises.set(subscription, {
71
- resolve: resolve!,
72
- })
71
+ // Direct load promise tracking: pipes loadSubset results straight to the
72
+ // live query collection, avoiding the multi-hop deferred promise chain that
73
+ // can break under microtask timing (e.g., queueMicrotask in TanStack Query).
74
+ const trackLoadResult = (result: Promise<void> | true) => {
75
+ if (result instanceof Promise) {
73
76
  this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(
74
- promise,
77
+ result,
75
78
  )
76
79
  }
77
80
  }
78
81
 
79
82
  // Status change handler - passed to subscribeChanges so it's registered
80
- // BEFORE any snapshot is requested, preventing race conditions
83
+ // BEFORE any snapshot is requested, preventing race conditions.
84
+ // Used as a fallback for status transitions not covered by direct tracking
85
+ // (e.g., truncate-triggered reloads that call trackLoadSubsetPromise directly).
81
86
  const onStatusChange = (event: SubscriptionStatusChangeEvent) => {
82
87
  const subscription = event.subscription as CollectionSubscription
83
88
  if (event.status === `loadingSubset`) {
84
- trackLoadPromise(subscription)
89
+ this.ensureLoadingPromise(subscription)
85
90
  } else {
86
91
  // status is 'ready'
87
92
  const deferred = this.subscriptionLoadingPromises.get(subscription)
88
93
  if (deferred) {
89
- // Clear the map entry FIRST (before resolving)
90
94
  this.subscriptionLoadingPromises.delete(subscription)
91
95
  deferred.resolve()
92
96
  }
@@ -100,6 +104,7 @@ export class CollectionSubscriber<
100
104
  whereExpression,
101
105
  orderByInfo,
102
106
  onStatusChange,
107
+ trackLoadResult,
103
108
  )
104
109
  } else {
105
110
  // If the source alias is lazy then we should not include the initial state
@@ -117,14 +122,13 @@ export class CollectionSubscriber<
117
122
  // Check current status after subscribing - if status is 'loadingSubset', track it.
118
123
  // The onStatusChange listener will catch the transition to 'ready'.
119
124
  if (subscription.status === `loadingSubset`) {
120
- trackLoadPromise(subscription)
125
+ this.ensureLoadingPromise(subscription)
121
126
  }
122
127
 
123
128
  const unsubscribe = () => {
124
129
  // If subscription has a pending promise, resolve it before unsubscribing
125
130
  const deferred = this.subscriptionLoadingPromises.get(subscription)
126
131
  if (deferred) {
127
- // Clear the map entry FIRST (before resolving)
128
132
  this.subscriptionLoadingPromises.delete(subscription)
129
133
  deferred.resolve()
130
134
  }
@@ -197,13 +201,49 @@ export class CollectionSubscriber<
197
201
  this.sendChangesToPipeline(changes)
198
202
  }
199
203
 
200
- // Create subscription with onStatusChange - listener is registered before snapshot
201
- // Note: For non-ordered queries (no limit/offset), we use trackLoadSubsetPromise: false
202
- // which is the default behavior in subscribeChanges
204
+ // Get the query's orderBy and limit to pass to loadSubset.
205
+ // Only include orderBy when it is scoped to this alias and uses simple refs,
206
+ // to avoid leaking cross-collection paths into backend-specific compilers.
207
+ const { orderBy, limit, offset } = this.collectionConfigBuilder.query
208
+ const effectiveLimit =
209
+ limit !== undefined && offset !== undefined ? limit + offset : limit
210
+ const normalizedOrderBy = orderBy
211
+ ? normalizeOrderByPaths(orderBy, this.alias)
212
+ : undefined
213
+ const canPassOrderBy =
214
+ normalizedOrderBy?.every((clause) => {
215
+ const exp = clause.expression
216
+ if (exp.type !== `ref`) {
217
+ return false
218
+ }
219
+ const path = exp.path
220
+ return Array.isArray(path) && path.length === 1
221
+ }) ?? false
222
+ const orderByForSubscription = canPassOrderBy
223
+ ? normalizedOrderBy
224
+ : undefined
225
+ const limitForSubscription = canPassOrderBy ? effectiveLimit : undefined
226
+
227
+ // Track loading via the loadSubset promise directly.
228
+ // requestSnapshot uses trackLoadSubsetPromise: false (needed for truncate handling),
229
+ // so we use onLoadSubsetResult to get the promise and track it ourselves.
230
+ const onLoadSubsetResult = includeInitialState
231
+ ? (result: Promise<void> | true) => {
232
+ if (result instanceof Promise) {
233
+ this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(
234
+ result,
235
+ )
236
+ }
237
+ }
238
+ : undefined
239
+
203
240
  const subscription = this.collection.subscribeChanges(sendChanges, {
204
241
  ...(includeInitialState && { includeInitialState }),
205
242
  whereExpression,
206
243
  onStatusChange,
244
+ orderBy: orderByForSubscription,
245
+ limit: limitForSubscription,
246
+ onLoadSubsetResult,
207
247
  })
208
248
 
209
249
  return subscription
@@ -213,17 +253,39 @@ export class CollectionSubscriber<
213
253
  whereExpression: BasicExpression<boolean> | undefined,
214
254
  orderByInfo: OrderByOptimizationInfo,
215
255
  onStatusChange: (event: SubscriptionStatusChangeEvent) => void,
256
+ onLoadSubsetResult: (result: Promise<void> | true) => void,
216
257
  ): CollectionSubscription {
217
258
  const { orderBy, offset, limit, index } = orderByInfo
218
259
 
260
+ // Store the callback so loadNextItems can also use direct tracking.
261
+ // Track in-flight ordered loads to avoid issuing redundant requests while
262
+ // a previous snapshot is still pending.
263
+ const handleLoadSubsetResult = (result: Promise<void> | true) => {
264
+ if (result instanceof Promise) {
265
+ this.pendingOrderedLoadPromise = result
266
+ result.finally(() => {
267
+ if (this.pendingOrderedLoadPromise === result) {
268
+ this.pendingOrderedLoadPromise = undefined
269
+ }
270
+ })
271
+ }
272
+ onLoadSubsetResult(result)
273
+ }
274
+
275
+ this.orderedLoadSubsetResult = handleLoadSubsetResult
276
+
219
277
  // Use a holder to forward-reference subscription in the callback
220
278
  const subscriptionHolder: { current?: CollectionSubscription } = {}
221
279
 
222
280
  const sendChangesInRange = (
223
281
  changes: Iterable<ChangeMessage<any, string | number>>,
224
282
  ) => {
283
+ const changesArray = Array.isArray(changes) ? changes : [...changes]
284
+
285
+ this.trackSentValues(changesArray, orderByInfo.comparator)
286
+
225
287
  // Split live updates into a delete of the old value and an insert of the new value
226
- const splittedChanges = splitUpdates(changes)
288
+ const splittedChanges = splitUpdates(changesArray)
227
289
  this.sendChangesToPipelineWithTracking(
228
290
  splittedChanges,
229
291
  subscriptionHolder.current!,
@@ -243,6 +305,8 @@ export class CollectionSubscriber<
243
305
  // and allow re-inserts of previously sent keys
244
306
  const truncateUnsubscribe = this.collection.on(`truncate`, () => {
245
307
  this.biggest = undefined
308
+ this.lastLoadRequestKey = undefined
309
+ this.pendingOrderedLoadPromise = undefined
246
310
  this.sentToD2Keys.clear()
247
311
  })
248
312
 
@@ -254,26 +318,27 @@ export class CollectionSubscriber<
254
318
  // Normalize the orderBy clauses such that the references are relative to the collection
255
319
  const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)
256
320
 
257
- // Trigger the snapshot request - onStatusChange listener is already registered
321
+ // Trigger the snapshot request use direct load tracking (trackLoadSubsetPromise: false)
322
+ // to pipe the loadSubset result straight to the live query collection. This bypasses
323
+ // the subscription status → onStatusChange → deferred promise chain which is fragile
324
+ // under microtask timing (e.g., queueMicrotask delays in TanStack Query observers).
258
325
  if (index) {
259
326
  // We have an index on the first orderBy column - use lazy loading optimization
260
- // This works for both single-column and multi-column orderBy:
261
- // - Single-column: index provides exact ordering
262
- // - Multi-column: index provides ordering on first column, secondary sort in memory
263
327
  subscription.setOrderByIndex(index)
264
328
 
265
- // Load the first `offset + limit` values from the index
266
- // i.e. the K items from the collection that fall into the requested range: [offset, offset + limit[
267
329
  subscription.requestLimitedSnapshot({
268
330
  limit: offset + limit,
269
331
  orderBy: normalizedOrderBy,
332
+ trackLoadSubsetPromise: false,
333
+ onLoadSubsetResult: handleLoadSubsetResult,
270
334
  })
271
335
  } else {
272
336
  // No index available (e.g., non-ref expression): pass orderBy/limit to loadSubset
273
- // so the sync layer can optimize if the backend supports it
274
337
  subscription.requestSnapshot({
275
338
  orderBy: normalizedOrderBy,
276
339
  limit: offset + limit,
340
+ trackLoadSubsetPromise: false,
341
+ onLoadSubsetResult: handleLoadSubsetResult,
277
342
  })
278
343
  }
279
344
 
@@ -301,6 +366,11 @@ export class CollectionSubscriber<
301
366
  return true
302
367
  }
303
368
 
369
+ if (this.pendingOrderedLoadPromise) {
370
+ // Wait for in-flight ordered loads to resolve before issuing another request.
371
+ return true
372
+ }
373
+
304
374
  // `dataNeeded` probes the orderBy operator to see if it needs more data
305
375
  // if it needs more data, it returns the number of items it needs
306
376
  const n = dataNeeded()
@@ -320,8 +390,6 @@ export class CollectionSubscriber<
320
390
  return
321
391
  }
322
392
 
323
- const trackedChanges = this.trackSentValues(changes, orderByInfo.comparator)
324
-
325
393
  // Cache the loadMoreIfNeeded callback on the subscription using a symbol property.
326
394
  // This ensures we pass the same function instance to the scheduler each time,
327
395
  // allowing it to deduplicate callbacks when multiple changes arrive during a transaction.
@@ -335,7 +403,7 @@ export class CollectionSubscriber<
335
403
  this.loadMoreIfNeeded.bind(this, subscription)
336
404
 
337
405
  this.sendChangesToPipeline(
338
- trackedChanges,
406
+ changes,
339
407
  subscriptionWithLoader[loadMoreCallbackSymbol],
340
408
  )
341
409
  }
@@ -357,12 +425,25 @@ export class CollectionSubscriber<
357
425
  : undefined
358
426
 
359
427
  // Normalize to array format for minValues
360
- const minValues =
361
- extractedValues !== undefined
362
- ? Array.isArray(extractedValues)
363
- ? extractedValues
364
- : [extractedValues]
365
- : undefined
428
+ let minValues: Array<unknown> | undefined
429
+ if (extractedValues !== undefined) {
430
+ minValues = Array.isArray(extractedValues)
431
+ ? extractedValues
432
+ : [extractedValues]
433
+ }
434
+
435
+ const loadRequestKey = this.getLoadRequestKey({
436
+ minValues,
437
+ offset,
438
+ limit: n,
439
+ })
440
+
441
+ // Skip if we already requested a load for this cursor+window.
442
+ // This prevents infinite loops from cached data re-writes while still allowing
443
+ // window moves (offset/limit changes) to trigger new requests.
444
+ if (this.lastLoadRequestKey === loadRequestKey) {
445
+ return
446
+ }
366
447
 
367
448
  // Normalize the orderBy clauses such that the references are relative to the collection
368
449
  const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)
@@ -373,8 +454,13 @@ export class CollectionSubscriber<
373
454
  orderBy: normalizedOrderBy,
374
455
  limit: n,
375
456
  minValues,
376
- offset,
457
+ // Omit offset so requestLimitedSnapshot can advance the offset based on
458
+ // the number of rows already loaded (supports offset-based backends).
459
+ trackLoadSubsetPromise: false,
460
+ onLoadSubsetResult: this.orderedLoadSubsetResult,
377
461
  })
462
+
463
+ this.lastLoadRequestKey = loadRequestKey
378
464
  }
379
465
 
380
466
  private getWhereClauseForAlias(): BasicExpression<boolean> | undefined {
@@ -397,22 +483,59 @@ export class CollectionSubscriber<
397
483
  return undefined
398
484
  }
399
485
 
400
- private *trackSentValues(
401
- changes: Iterable<ChangeMessage<any, string | number>>,
486
+ private trackSentValues(
487
+ changes: Array<ChangeMessage<any, string | number>>,
402
488
  comparator: (a: any, b: any) => number,
403
- ) {
489
+ ): void {
404
490
  for (const change of changes) {
491
+ if (change.type === `delete`) {
492
+ continue
493
+ }
494
+
495
+ const isNewKey = !this.sentToD2Keys.has(change.key)
496
+
405
497
  // Only track inserts/updates for cursor positioning, not deletes
406
- if (change.type !== `delete`) {
407
- if (!this.biggest) {
408
- this.biggest = change.value
409
- } else if (comparator(this.biggest, change.value) < 0) {
410
- this.biggest = change.value
411
- }
498
+ if (!this.biggest) {
499
+ this.biggest = change.value
500
+ this.lastLoadRequestKey = undefined
501
+ } else if (comparator(this.biggest, change.value) < 0) {
502
+ this.biggest = change.value
503
+ this.lastLoadRequestKey = undefined
504
+ } else if (isNewKey) {
505
+ // New key with same orderBy value - allow another load if needed
506
+ this.lastLoadRequestKey = undefined
412
507
  }
508
+ }
509
+ }
413
510
 
414
- yield change
511
+ private ensureLoadingPromise(subscription: CollectionSubscription) {
512
+ if (this.subscriptionLoadingPromises.has(subscription)) {
513
+ return
415
514
  }
515
+
516
+ let resolve: () => void
517
+ const promise = new Promise<void>((res) => {
518
+ resolve = res
519
+ })
520
+
521
+ this.subscriptionLoadingPromises.set(subscription, {
522
+ resolve: resolve!,
523
+ })
524
+ this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(
525
+ promise,
526
+ )
527
+ }
528
+
529
+ private getLoadRequestKey(options: {
530
+ minValues: Array<unknown> | undefined
531
+ offset: number
532
+ limit: number
533
+ }): string {
534
+ return serializeValue({
535
+ minValues: options.minValues ?? null,
536
+ offset: options.offset,
537
+ limit: options.limit,
538
+ })
416
539
  }
417
540
  }
418
541
 
package/src/types.ts CHANGED
@@ -809,6 +809,22 @@ export interface SubscribeChangesOptions<
809
809
  * @internal
810
810
  */
811
811
  onStatusChange?: (event: SubscriptionStatusChangeEvent) => void
812
+ /**
813
+ * Optional orderBy to include in loadSubset for query-specific cache keys.
814
+ * @internal
815
+ */
816
+ orderBy?: OrderBy
817
+ /**
818
+ * Optional limit to include in loadSubset for query-specific cache keys.
819
+ * @internal
820
+ */
821
+ limit?: number
822
+ /**
823
+ * Callback that receives the loadSubset result (Promise or true) from requestSnapshot.
824
+ * Allows the caller to directly track the loading promise for isReady status.
825
+ * @internal
826
+ */
827
+ onLoadSubsetResult?: (result: Promise<void> | true) => void
812
828
  }
813
829
 
814
830
  export interface SubscribeChangesSnapshotOptions<
@@ -134,10 +134,20 @@ function areUint8ArraysEqual(a: Uint8Array, b: Uint8Array): boolean {
134
134
  */
135
135
  const UINT8ARRAY_NORMALIZE_THRESHOLD = 128
136
136
 
137
+ /**
138
+ * Sentinel value representing undefined in normalized form.
139
+ * This allows distinguishing between "start from beginning" (undefined parameter)
140
+ * and "start from the key undefined" (actual undefined value in the tree).
141
+ */
142
+ export const UNDEFINED_SENTINEL = `__TS_DB_BTREE_UNDEFINED_VALUE__`
143
+
137
144
  /**
138
145
  * Normalize a value for comparison and Map key usage
139
146
  * Converts values that can't be directly compared or used as Map keys
140
147
  * into comparable primitive representations
148
+ *
149
+ * Note: This does NOT convert undefined to a sentinel. Use normalizeForBTree
150
+ * for BTree index operations that need to distinguish undefined values.
141
151
  */
142
152
  export function normalizeValue(value: any): any {
143
153
  if (value instanceof Date) {
@@ -164,6 +174,30 @@ export function normalizeValue(value: any): any {
164
174
  return value
165
175
  }
166
176
 
177
+ /**
178
+ * Normalize a value for BTree index usage.
179
+ * Extends normalizeValue to also convert undefined to a sentinel value.
180
+ * This is needed because the BTree does not properly support `undefined` as a key
181
+ * (it interprets undefined as "start from beginning" in nextHigherPair/nextLowerPair).
182
+ */
183
+ export function normalizeForBTree(value: any): any {
184
+ if (value === undefined) {
185
+ return UNDEFINED_SENTINEL
186
+ }
187
+ return normalizeValue(value)
188
+ }
189
+
190
+ /**
191
+ * Converts the `UNDEFINED_SENTINEL` back to `undefined`.
192
+ * Needed such that the sentinel is converted back to `undefined` before comparison.
193
+ */
194
+ export function denormalizeUndefined(value: any): any {
195
+ if (value === UNDEFINED_SENTINEL) {
196
+ return undefined
197
+ }
198
+ return value
199
+ }
200
+
167
201
  /**
168
202
  * Compare two values for equality, with special handling for Uint8Arrays and Buffers
169
203
  */