@tanstack/db-ivm 0.1.18 → 0.1.20

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 (38) hide show
  1. package/dist/cjs/hashing/hash.cjs +76 -41
  2. package/dist/cjs/hashing/hash.cjs.map +1 -1
  3. package/dist/cjs/hashing/hash.d.cts +2 -0
  4. package/dist/cjs/hashing/murmur.cjs +32 -9
  5. package/dist/cjs/hashing/murmur.cjs.map +1 -1
  6. package/dist/cjs/hashing/murmur.d.cts +1 -0
  7. package/dist/cjs/index.cjs +44 -42
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/index.d.cts +1 -0
  10. package/dist/cjs/operators/groupBy.cjs +1 -1
  11. package/dist/cjs/operators/groupBy.cjs.map +1 -1
  12. package/dist/cjs/utils.cjs +131 -6
  13. package/dist/cjs/utils.cjs.map +1 -1
  14. package/dist/cjs/utils.d.cts +7 -3
  15. package/dist/esm/hashing/hash.d.ts +2 -0
  16. package/dist/esm/hashing/hash.js +78 -43
  17. package/dist/esm/hashing/hash.js.map +1 -1
  18. package/dist/esm/hashing/murmur.d.ts +1 -0
  19. package/dist/esm/hashing/murmur.js +32 -9
  20. package/dist/esm/hashing/murmur.js.map +1 -1
  21. package/dist/esm/index.d.ts +1 -0
  22. package/dist/esm/index.js +17 -15
  23. package/dist/esm/index.js.map +1 -1
  24. package/dist/esm/operators/groupBy.js +1 -1
  25. package/dist/esm/operators/groupBy.js.map +1 -1
  26. package/dist/esm/operators/topKArray.js +1 -1
  27. package/dist/esm/utils.d.ts +7 -3
  28. package/dist/esm/utils.js +131 -6
  29. package/dist/esm/utils.js.map +1 -1
  30. package/package.json +3 -2
  31. package/src/hashing/hash.ts +117 -51
  32. package/src/hashing/murmur.ts +44 -10
  33. package/src/index.ts +1 -0
  34. package/src/operators/groupBy.ts +7 -4
  35. package/src/utils.ts +212 -9
  36. package/dist/cjs/operators/orderByBTree.d.cts +0 -3
  37. package/dist/esm/operators/orderByBTree.d.ts +0 -3
  38. package/src/operators/orderByBTree.ts +0 -20
@@ -62,7 +62,7 @@ export function groupBy<
62
62
  stream: IStreamBuilder<T>,
63
63
  ): IStreamBuilder<KeyValue<string, ResultType>> => {
64
64
  // Special key to store the original key object
65
- const KEY_SENTINEL = `__original_key__`
65
+ const KEY_SENTINEL = Symbol(`original_group_key`)
66
66
 
67
67
  // First map to extract keys and pre-aggregate values
68
68
  const withKeysAndValues = stream.pipe(
@@ -71,7 +71,7 @@ export function groupBy<
71
71
  const keyString = serializeValue(key)
72
72
 
73
73
  // Create values object with pre-aggregated values
74
- const values: Record<string, unknown> = {}
74
+ const values: Record<string | symbol, unknown> = {}
75
75
 
76
76
  // Store the original key object
77
77
  values[KEY_SENTINEL] = key
@@ -81,7 +81,10 @@ export function groupBy<
81
81
  values[name] = aggregate.preMap(data)
82
82
  }
83
83
 
84
- return [keyString, values] as KeyValue<string, Record<string, unknown>>
84
+ return [keyString, values] as KeyValue<
85
+ string,
86
+ Record<string | symbol, unknown>
87
+ >
85
88
  }),
86
89
  )
87
90
 
@@ -99,7 +102,7 @@ export function groupBy<
99
102
  return []
100
103
  }
101
104
 
102
- const result: Record<string, unknown> = {}
105
+ const result: Record<PropertyKey, unknown> = {}
103
106
 
104
107
  // Get the original key from first value in group
105
108
  const originalKey = values[0]?.[0]?.[KEY_SENTINEL]
package/src/utils.ts CHANGED
@@ -185,6 +185,14 @@ function range(start: number, end: number): Array<number> {
185
185
  export function compareKeys(a: string | number, b: string | number): number {
186
186
  // Same type: compare directly
187
187
  if (typeof a === typeof b) {
188
+ if (typeof a === `number` && typeof b === `number`) {
189
+ const aIsNaN = Number.isNaN(a)
190
+ const bIsNaN = Number.isNaN(b)
191
+ if (aIsNaN || bIsNaN) {
192
+ if (aIsNaN && bIsNaN) return 0
193
+ return aIsNaN ? 1 : -1
194
+ }
195
+ }
188
196
  if (a < b) return -1
189
197
  if (a > b) return 1
190
198
  return 0
@@ -193,19 +201,214 @@ export function compareKeys(a: string | number, b: string | number): number {
193
201
  return typeof a === `string` ? -1 : 1
194
202
  }
195
203
 
204
+ type CanonicalValue =
205
+ | readonly [`undefined`]
206
+ | readonly [`null`]
207
+ | readonly [`boolean`, boolean]
208
+ | readonly [`number`, number | `NaN` | `Infinity` | `-Infinity`]
209
+ | readonly [`bigint`, string]
210
+ | readonly [`string`, string]
211
+ | readonly [`date`, number | `Invalid`]
212
+ | readonly [`regexp`, string, string]
213
+ | readonly [`bytes`, Array<number>]
214
+ | readonly [`array`, Array<CanonicalValue>]
215
+ | readonly [`map`, Array<readonly [CanonicalValue, CanonicalValue]>]
216
+ | readonly [`set`, Array<CanonicalValue>]
217
+ | readonly [`object`, Array<readonly [string, CanonicalValue]>]
218
+
196
219
  /**
197
- * Serializes a value for use as a key, handling BigInt and Date values that JSON.stringify cannot handle.
198
- * Uses JSON.stringify with a replacer function to convert BigInt values to strings and Date values to ISO strings.
199
- * This is used for creating string keys in groupBy operations.
220
+ * Serializes a supported query value into one canonical key.
221
+ *
222
+ * JSON's native encoding is not suitable for relation keys: it merges BigInt
223
+ * with strings when a replacer is used, merges NaN with null, drops undefined,
224
+ * and depends on object insertion order. Ordinary JSON values keep their
225
+ * established wire form. Values that need richer types use a reserved prefix
226
+ * plus a structural, type-tagged encoding.
200
227
  */
201
228
  export function serializeValue(value: unknown): string {
202
- return JSON.stringify(value, (_, val) => {
203
- if (typeof val === 'bigint') {
204
- return val.toString()
229
+ if (isJsonSafeStructuralValue(value, new Set())) {
230
+ return JSON.stringify(toStableJsonValue(value))
231
+ }
232
+
233
+ return `~${JSON.stringify(toCanonicalValue(value, new Set()))}`
234
+ }
235
+
236
+ type JsonValue =
237
+ | null
238
+ | boolean
239
+ | number
240
+ | string
241
+ | Array<JsonValue>
242
+ | { [key: string]: JsonValue }
243
+
244
+ function isJsonSafeStructuralValue(
245
+ value: unknown,
246
+ ancestors: Set<object>,
247
+ ): boolean {
248
+ if (value === null) return true
249
+
250
+ switch (typeof value) {
251
+ case `boolean`:
252
+ case `string`:
253
+ return true
254
+ case `number`:
255
+ return Number.isFinite(value)
256
+ case `undefined`:
257
+ case `bigint`:
258
+ case `symbol`:
259
+ case `function`:
260
+ return false
261
+ }
262
+
263
+ return withAcyclicValue(value, ancestors, () => {
264
+ if (
265
+ value instanceof Date ||
266
+ value instanceof RegExp ||
267
+ value instanceof Uint8Array ||
268
+ value instanceof Map ||
269
+ value instanceof Set
270
+ ) {
271
+ return false
205
272
  }
206
- if (val instanceof Date) {
207
- return val.toISOString()
273
+
274
+ return Array.isArray(value)
275
+ ? value.every((item) => isJsonSafeStructuralValue(item, ancestors))
276
+ : Object.keys(value).every((key) =>
277
+ isJsonSafeStructuralValue(
278
+ (value as Record<string, unknown>)[key],
279
+ ancestors,
280
+ ),
281
+ )
282
+ })
283
+ }
284
+
285
+ function toStableJsonValue(value: unknown): JsonValue {
286
+ if (
287
+ value === null ||
288
+ typeof value === `boolean` ||
289
+ typeof value === `number` ||
290
+ typeof value === `string`
291
+ ) {
292
+ return value
293
+ }
294
+
295
+ if (Array.isArray(value)) {
296
+ return value.map(toStableJsonValue)
297
+ }
298
+
299
+ return Object.fromEntries(
300
+ Object.keys(value as object)
301
+ .sort()
302
+ .map((key) => [
303
+ key,
304
+ toStableJsonValue((value as Record<string, unknown>)[key]),
305
+ ]),
306
+ )
307
+ }
308
+
309
+ function toCanonicalValue(
310
+ value: unknown,
311
+ ancestors: Set<object>,
312
+ ): CanonicalValue {
313
+ if (value === undefined) return [`undefined`]
314
+ if (value === null) return [`null`]
315
+
316
+ switch (typeof value) {
317
+ case `boolean`:
318
+ return [`boolean`, value]
319
+ case `number`:
320
+ if (Number.isNaN(value)) return [`number`, `NaN`]
321
+ if (value === Infinity) return [`number`, `Infinity`]
322
+ if (value === -Infinity) return [`number`, `-Infinity`]
323
+ return [`number`, value === 0 ? 0 : value]
324
+ case `bigint`:
325
+ return [`bigint`, value.toString()]
326
+ case `string`:
327
+ return [`string`, value]
328
+ case `symbol`:
329
+ case `function`:
330
+ throw new TypeError(
331
+ `Cannot serialize ${typeof value} as a structural relation key`,
332
+ )
333
+ }
334
+
335
+ return withAcyclicValue(value, ancestors, () => {
336
+ if (value instanceof Date) {
337
+ const timestamp = value.getTime()
338
+ return Number.isNaN(timestamp)
339
+ ? ([`date`, `Invalid`] as const)
340
+ : ([`date`, timestamp] as const)
341
+ }
342
+
343
+ if (value instanceof RegExp) {
344
+ return [`regexp`, value.source, value.flags]
345
+ }
346
+
347
+ if (value instanceof Uint8Array) {
348
+ return [`bytes`, Array.from(value)]
349
+ }
350
+
351
+ if (Array.isArray(value)) {
352
+ return [`array`, value.map((item) => toCanonicalValue(item, ancestors))]
353
+ }
354
+
355
+ if (value instanceof Map) {
356
+ const entries = [...value.entries()].map(
357
+ ([key, entryValue]) =>
358
+ [
359
+ toCanonicalValue(key, ancestors),
360
+ toCanonicalValue(entryValue, ancestors),
361
+ ] as const,
362
+ )
363
+ entries.sort((left, right) =>
364
+ compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),
365
+ )
366
+ return [`map`, entries]
208
367
  }
209
- return val
368
+
369
+ if (value instanceof Set) {
370
+ const entries = [...value].map((entry) =>
371
+ toCanonicalValue(entry, ancestors),
372
+ )
373
+ entries.sort((left, right) =>
374
+ compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),
375
+ )
376
+ return [`set`, entries]
377
+ }
378
+
379
+ const entries = Object.keys(value)
380
+ .sort()
381
+ .map(
382
+ (key) =>
383
+ [
384
+ key,
385
+ toCanonicalValue(
386
+ (value as Record<string, unknown>)[key],
387
+ ancestors,
388
+ ),
389
+ ] as const,
390
+ )
391
+ return [`object`, entries]
210
392
  })
211
393
  }
394
+
395
+ function compareSerializedValues(left: string, right: string): number {
396
+ return left < right ? -1 : left > right ? 1 : 0
397
+ }
398
+
399
+ function withAcyclicValue<T>(
400
+ value: object,
401
+ ancestors: Set<object>,
402
+ encode: () => T,
403
+ ): T {
404
+ if (ancestors.has(value)) {
405
+ throw new TypeError(`Cannot serialize a cyclic structural relation key`)
406
+ }
407
+
408
+ ancestors.add(value)
409
+ try {
410
+ return encode()
411
+ } finally {
412
+ ancestors.delete(value)
413
+ }
414
+ }
@@ -1,3 +0,0 @@
1
- import { KeyValue } from '../types.js';
2
- import { OrderByOptions } from './orderBy.js';
3
- export declare function orderByWithFractionalIndexBTree<T extends KeyValue<unknown, unknown>, Ve = unknown>(valueExtractor: (value: T extends KeyValue<unknown, infer V> ? V : never) => Ve, options?: OrderByOptions<Ve>): (stream: import('../types.js').IStreamBuilder<T>) => import('../types.js').IStreamBuilder<[T extends KeyValue<infer K, unknown> ? K : never, [T extends KeyValue<unknown, infer V> ? V : never, string]]>;
@@ -1,3 +0,0 @@
1
- import { KeyValue } from '../types.js';
2
- import { OrderByOptions } from './orderBy.js';
3
- export declare function orderByWithFractionalIndexBTree<T extends KeyValue<unknown, unknown>, Ve = unknown>(valueExtractor: (value: T extends KeyValue<unknown, infer V> ? V : never) => Ve, options?: OrderByOptions<Ve>): (stream: import('../types.js').IStreamBuilder<T>) => import('../types.js').IStreamBuilder<[T extends KeyValue<infer K, unknown> ? K : never, [T extends KeyValue<unknown, infer V> ? V : never, string]]>;
@@ -1,20 +0,0 @@
1
- import { orderByWithFractionalIndexBase } from './orderBy.js'
2
- import { topKWithFractionalIndexBTree } from './topKWithFractionalIndexBTree.js'
3
- import type { KeyValue } from '../types.js'
4
- import type { OrderByOptions } from './orderBy.js'
5
-
6
- export function orderByWithFractionalIndexBTree<
7
- T extends KeyValue<unknown, unknown>,
8
- Ve = unknown,
9
- >(
10
- valueExtractor: (
11
- value: T extends KeyValue<unknown, infer V> ? V : never,
12
- ) => Ve,
13
- options?: OrderByOptions<Ve>,
14
- ) {
15
- return orderByWithFractionalIndexBase(
16
- topKWithFractionalIndexBTree,
17
- valueExtractor,
18
- options,
19
- )
20
- }