@tanstack/db-ivm 0.1.19 → 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.
- package/dist/cjs/hashing/hash.cjs +76 -41
- package/dist/cjs/hashing/hash.cjs.map +1 -1
- package/dist/cjs/hashing/hash.d.cts +2 -0
- package/dist/cjs/hashing/murmur.cjs +32 -9
- package/dist/cjs/hashing/murmur.cjs.map +1 -1
- package/dist/cjs/hashing/murmur.d.cts +1 -0
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/operators/groupBy.cjs +1 -1
- package/dist/cjs/operators/groupBy.cjs.map +1 -1
- package/dist/cjs/utils.cjs +8 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/esm/hashing/hash.d.ts +2 -0
- package/dist/esm/hashing/hash.js +78 -43
- package/dist/esm/hashing/hash.js.map +1 -1
- package/dist/esm/hashing/murmur.d.ts +1 -0
- package/dist/esm/hashing/murmur.js +32 -9
- package/dist/esm/hashing/murmur.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/operators/groupBy.js +1 -1
- package/dist/esm/operators/groupBy.js.map +1 -1
- package/dist/esm/utils.js +8 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +3 -2
- package/src/hashing/hash.ts +117 -51
- package/src/hashing/murmur.ts +44 -10
- package/src/index.ts +1 -0
- package/src/operators/groupBy.ts +7 -4
- package/src/utils.ts +8 -0
- package/dist/cjs/operators/orderByBTree.d.cts +0 -3
- package/dist/esm/operators/orderByBTree.d.ts +0 -3
- package/src/operators/orderByBTree.ts +0 -20
package/src/hashing/hash.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MurmurHashStream, randomHash } from './murmur.js'
|
|
1
|
+
import { MurmurHashStream, getSymbolIdentity, randomHash } from './murmur.js'
|
|
2
2
|
import type { Hasher } from './murmur.js'
|
|
3
3
|
|
|
4
4
|
/*
|
|
@@ -19,6 +19,10 @@ const MAP_MARKER = randomHash()
|
|
|
19
19
|
const SET_MARKER = randomHash()
|
|
20
20
|
const UINT8ARRAY_MARKER = randomHash()
|
|
21
21
|
const TEMPORAL_MARKER = randomHash()
|
|
22
|
+
// Bound structural recursion and value visits. Shared acyclic subtrees are
|
|
23
|
+
// cached; cycles are rejected rather than given context-dependent hashes.
|
|
24
|
+
const MAX_STRUCTURAL_HASH_WORK = 1_000_000
|
|
25
|
+
const MAX_STRUCTURAL_HASH_DEPTH = 768
|
|
22
26
|
|
|
23
27
|
const temporalTypes = new Set([
|
|
24
28
|
`Temporal.Duration`,
|
|
@@ -48,63 +52,65 @@ const UINT8ARRAY_CONTENT_HASH_THRESHOLD = 128
|
|
|
48
52
|
|
|
49
53
|
const hashCache = new WeakMap<object, number>()
|
|
50
54
|
|
|
55
|
+
/** @internal Register a mutable handle before it enters a structural value. */
|
|
56
|
+
export function registerOpaqueHash(value: object): void {
|
|
57
|
+
cachedReferenceHash(value)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type HashContext = {
|
|
61
|
+
activeObjects: Set<object>
|
|
62
|
+
work: number
|
|
63
|
+
pendingHashes: Map<object, number>
|
|
64
|
+
}
|
|
65
|
+
|
|
51
66
|
export function hash(input: any): number {
|
|
52
67
|
const hasher = new MurmurHashStream()
|
|
53
68
|
updateHasher(hasher, input)
|
|
54
69
|
return hasher.digest()
|
|
55
70
|
}
|
|
56
71
|
|
|
57
|
-
function hashObject(input: object): number {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
function hashObject(input: object, context: HashContext): number {
|
|
73
|
+
if (context.activeObjects.size >= MAX_STRUCTURAL_HASH_DEPTH) {
|
|
74
|
+
throw new RangeError(
|
|
75
|
+
`Value is too complex to hash safely: structural depth`,
|
|
76
|
+
)
|
|
61
77
|
}
|
|
62
78
|
|
|
79
|
+
context.activeObjects.add(input)
|
|
80
|
+
|
|
63
81
|
let valueHash: number | undefined
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
(typeof Buffer !== `undefined` && input instanceof Buffer) ||
|
|
69
|
-
input instanceof Uint8Array
|
|
70
|
-
) {
|
|
71
|
-
// For small Uint8Arrays/Buffers (e.g., ULIDs, UUIDs), hash by content
|
|
72
|
-
// to enable proper equality comparisons. For large arrays, hash by reference
|
|
73
|
-
// to avoid performance costs.
|
|
74
|
-
if (input.byteLength <= UINT8ARRAY_CONTENT_HASH_THRESHOLD) {
|
|
82
|
+
try {
|
|
83
|
+
if (input instanceof Date) {
|
|
84
|
+
valueHash = hashDate(input)
|
|
85
|
+
} else if (isBinaryValue(input)) {
|
|
75
86
|
valueHash = hashUint8Array(input)
|
|
87
|
+
} else if (isTemporal(input)) {
|
|
88
|
+
valueHash = hashTemporal(input)
|
|
76
89
|
} else {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return cachedReferenceHash(input)
|
|
80
|
-
}
|
|
81
|
-
} else if (input instanceof File) {
|
|
82
|
-
// Files are always hashed by reference due to their potentially large size
|
|
83
|
-
return cachedReferenceHash(input)
|
|
84
|
-
} else if (isTemporal(input)) {
|
|
85
|
-
valueHash = hashTemporal(input)
|
|
86
|
-
} else {
|
|
87
|
-
let plainObjectInput = input
|
|
88
|
-
let marker = OBJECT_MARKER
|
|
89
|
-
|
|
90
|
-
if (input instanceof Array) {
|
|
91
|
-
marker = ARRAY_MARKER
|
|
92
|
-
}
|
|
90
|
+
let plainObjectInput = input
|
|
91
|
+
let marker = OBJECT_MARKER
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
93
|
+
if (input instanceof Array) {
|
|
94
|
+
marker = ARRAY_MARKER
|
|
95
|
+
}
|
|
98
96
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
if (input instanceof Map) {
|
|
98
|
+
marker = MAP_MARKER
|
|
99
|
+
plainObjectInput = [...input.entries()]
|
|
100
|
+
}
|
|
103
101
|
|
|
104
|
-
|
|
102
|
+
if (input instanceof Set) {
|
|
103
|
+
marker = SET_MARKER
|
|
104
|
+
plainObjectInput = [...input.entries()]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
valueHash = hashPlainObject(plainObjectInput, marker, context)
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
context.activeObjects.delete(input)
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
|
|
113
|
+
context.pendingHashes.set(input, valueHash)
|
|
108
114
|
return valueHash
|
|
109
115
|
}
|
|
110
116
|
|
|
@@ -135,7 +141,11 @@ function hashTemporal(input: TemporalLike): number {
|
|
|
135
141
|
return hasher.digest()
|
|
136
142
|
}
|
|
137
143
|
|
|
138
|
-
function hashPlainObject(
|
|
144
|
+
function hashPlainObject(
|
|
145
|
+
input: object,
|
|
146
|
+
marker: number,
|
|
147
|
+
context: HashContext,
|
|
148
|
+
): number {
|
|
139
149
|
const hasher = new MurmurHashStream()
|
|
140
150
|
|
|
141
151
|
// Mark the type of the input
|
|
@@ -145,13 +155,28 @@ function hashPlainObject(input: object, marker: number): number {
|
|
|
145
155
|
for (const key of keys) {
|
|
146
156
|
hasher.update(KEY)
|
|
147
157
|
hasher.update(key)
|
|
148
|
-
updateHasher(hasher, input[key as keyof typeof input])
|
|
158
|
+
updateHasher(hasher, input[key as keyof typeof input], context)
|
|
159
|
+
}
|
|
160
|
+
const symbolKeys = Object.getOwnPropertySymbols(input)
|
|
161
|
+
.filter((key) => Object.prototype.propertyIsEnumerable.call(input, key))
|
|
162
|
+
.sort((left, right) => getSymbolIdentity(left) - getSymbolIdentity(right))
|
|
163
|
+
for (const key of symbolKeys) {
|
|
164
|
+
hasher.update(KEY)
|
|
165
|
+
hasher.update(key)
|
|
166
|
+
updateHasher(hasher, input[key as keyof typeof input], context)
|
|
149
167
|
}
|
|
150
168
|
|
|
151
169
|
return hasher.digest()
|
|
152
170
|
}
|
|
153
171
|
|
|
154
|
-
function updateHasher(
|
|
172
|
+
function updateHasher(
|
|
173
|
+
hasher: Hasher,
|
|
174
|
+
input: unknown,
|
|
175
|
+
context?: HashContext,
|
|
176
|
+
): void {
|
|
177
|
+
if (context && ++context.work > MAX_STRUCTURAL_HASH_WORK) {
|
|
178
|
+
throw new RangeError(`Value is too complex to hash safely: structural work`)
|
|
179
|
+
}
|
|
155
180
|
if (input === null) {
|
|
156
181
|
hasher.update(NULL)
|
|
157
182
|
return
|
|
@@ -173,7 +198,7 @@ function updateHasher(hasher: Hasher, input: unknown): void {
|
|
|
173
198
|
hasher.update(input)
|
|
174
199
|
return
|
|
175
200
|
case `object`:
|
|
176
|
-
hasher.update(getCachedHash(input))
|
|
201
|
+
hasher.update(getCachedHash(input, context))
|
|
177
202
|
return
|
|
178
203
|
case `function`:
|
|
179
204
|
// Functions are assigned a globally unique ID
|
|
@@ -187,12 +212,53 @@ function updateHasher(hasher: Hasher, input: unknown): void {
|
|
|
187
212
|
}
|
|
188
213
|
}
|
|
189
214
|
|
|
190
|
-
function getCachedHash(input: object): number {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
215
|
+
function getCachedHash(input: object, context?: HashContext): number {
|
|
216
|
+
if (!context) {
|
|
217
|
+
const cached = hashCache.get(input)
|
|
218
|
+
if (cached !== undefined) return cached
|
|
219
|
+
if (isReferenceHashedObject(input)) return cachedReferenceHash(input)
|
|
220
|
+
|
|
221
|
+
// Only an uncached structural root needs graph traversal state. Commit its
|
|
222
|
+
// cache entries after success so a failed traversal cannot poison retries.
|
|
223
|
+
context = {
|
|
224
|
+
activeObjects: new Set(),
|
|
225
|
+
work: 0,
|
|
226
|
+
pendingHashes: new Map(),
|
|
227
|
+
}
|
|
228
|
+
const result = hashObject(input, context)
|
|
229
|
+
for (const [object, valueHash] of context.pendingHashes) {
|
|
230
|
+
hashCache.set(object, valueHash)
|
|
231
|
+
}
|
|
232
|
+
return result
|
|
194
233
|
}
|
|
195
|
-
|
|
234
|
+
|
|
235
|
+
if (context.activeObjects.has(input)) {
|
|
236
|
+
throw new TypeError(`Cannot hash cyclic structural values`)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Opaque leaves cannot contain structural back-references. Resolve them
|
|
240
|
+
// before entering structural recursion, even when they have user properties.
|
|
241
|
+
if (isReferenceHashedObject(input)) return cachedReferenceHash(input)
|
|
242
|
+
|
|
243
|
+
const valueHash = hashCache.get(input) ?? context.pendingHashes.get(input)
|
|
244
|
+
if (valueHash !== undefined) return valueHash
|
|
245
|
+
|
|
246
|
+
return hashObject(input, context)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isReferenceHashedObject(input: object): boolean {
|
|
250
|
+
return (
|
|
251
|
+
input instanceof File ||
|
|
252
|
+
(isBinaryValue(input) &&
|
|
253
|
+
input.byteLength > UINT8ARRAY_CONTENT_HASH_THRESHOLD)
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function isBinaryValue(input: object): input is Uint8Array {
|
|
258
|
+
return (
|
|
259
|
+
(typeof Buffer !== `undefined` && input instanceof Buffer) ||
|
|
260
|
+
input instanceof Uint8Array
|
|
261
|
+
)
|
|
196
262
|
}
|
|
197
263
|
|
|
198
264
|
let nextRefId = 1
|
package/src/hashing/murmur.ts
CHANGED
|
@@ -9,6 +9,49 @@ const BIG_INT_MARKER = randomHash()
|
|
|
9
9
|
const NEG_BIG_INT_MARKER = randomHash()
|
|
10
10
|
const SYMBOL_MARKER = randomHash()
|
|
11
11
|
|
|
12
|
+
type SymbolIdStore = {
|
|
13
|
+
get: (key: symbol) => number | undefined
|
|
14
|
+
set: (key: symbol, value: number) => unknown
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const symbolIds = createSymbolIdStore()
|
|
18
|
+
const registeredSymbolIds = new Map<string, number>()
|
|
19
|
+
let nextSymbolId = 0
|
|
20
|
+
|
|
21
|
+
export function getSymbolIdentity(symbol: symbol): number {
|
|
22
|
+
const registeredKey = Symbol.keyFor(symbol)
|
|
23
|
+
if (registeredKey !== undefined) {
|
|
24
|
+
let id = registeredSymbolIds.get(registeredKey)
|
|
25
|
+
if (id === undefined) {
|
|
26
|
+
id = ++nextSymbolId
|
|
27
|
+
registeredSymbolIds.set(registeredKey, id)
|
|
28
|
+
}
|
|
29
|
+
return id
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let id = symbolIds.get(symbol)
|
|
33
|
+
if (id === undefined) {
|
|
34
|
+
id = ++nextSymbolId
|
|
35
|
+
symbolIds.set(symbol, id)
|
|
36
|
+
}
|
|
37
|
+
return id
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createSymbolIdStore(): SymbolIdStore {
|
|
41
|
+
const weakIds = new WeakMap<object, number>() as unknown as SymbolIdStore
|
|
42
|
+
const probe = Symbol()
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
weakIds.set(probe, 0)
|
|
46
|
+
if (weakIds.get(probe) === 0) return weakIds
|
|
47
|
+
} catch {
|
|
48
|
+
// Older runtimes reject symbols as weak keys. Retain them rather than
|
|
49
|
+
// merge distinct symbols and corrupt differential state.
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return new Map<symbol, number>()
|
|
53
|
+
}
|
|
54
|
+
|
|
12
55
|
export type Hash = number
|
|
13
56
|
|
|
14
57
|
export function randomHash() {
|
|
@@ -67,16 +110,7 @@ export class MurmurHashStream implements Hasher {
|
|
|
67
110
|
switch (typeof chunk) {
|
|
68
111
|
case `symbol`: {
|
|
69
112
|
this.update(SYMBOL_MARKER)
|
|
70
|
-
|
|
71
|
-
if (!description) {
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
for (let i = 0; i < description.length; i++) {
|
|
76
|
-
const code = description.charCodeAt(i)
|
|
77
|
-
this.writeByte(code & 0xff)
|
|
78
|
-
this.writeByte((code >>> 8) & 0xff)
|
|
79
|
-
}
|
|
113
|
+
this.update(getSymbolIdentity(chunk))
|
|
80
114
|
return
|
|
81
115
|
}
|
|
82
116
|
case `string`:
|
package/src/index.ts
CHANGED
package/src/operators/groupBy.ts
CHANGED
|
@@ -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 = `
|
|
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<
|
|
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<
|
|
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
|
|
@@ -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
|
-
}
|