@tanstack/db-ivm 0.1.2 → 0.1.3
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/hashIndex.cjs +83 -0
- package/dist/cjs/hashIndex.cjs.map +1 -0
- package/dist/cjs/hashIndex.d.cts +25 -0
- package/dist/cjs/hashing/hash.cjs +122 -0
- package/dist/cjs/hashing/hash.cjs.map +1 -0
- package/dist/cjs/hashing/hash.d.cts +1 -0
- package/dist/cjs/hashing/index.d.cts +2 -0
- package/dist/cjs/hashing/murmur.cjs +103 -0
- package/dist/cjs/hashing/murmur.cjs.map +1 -0
- package/dist/cjs/hashing/murmur.d.cts +26 -0
- package/dist/cjs/indexes.cjs +97 -45
- package/dist/cjs/indexes.cjs.map +1 -1
- package/dist/cjs/indexes.d.cts +0 -2
- package/dist/cjs/multiset.cjs +2 -1
- package/dist/cjs/multiset.cjs.map +1 -1
- package/dist/cjs/operators/distinct.cjs +2 -2
- package/dist/cjs/operators/distinct.cjs.map +1 -1
- package/dist/cjs/operators/groupBy.cjs +5 -4
- package/dist/cjs/operators/groupBy.cjs.map +1 -1
- package/dist/cjs/operators/groupBy.d.cts +1 -1
- package/dist/cjs/utils.cjs +13 -33
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +7 -4
- package/dist/cjs/valueIndex.cjs +74 -0
- package/dist/cjs/valueIndex.cjs.map +1 -0
- package/dist/cjs/valueIndex.d.cts +17 -0
- package/dist/esm/hashIndex.d.ts +25 -0
- package/dist/esm/hashIndex.js +83 -0
- package/dist/esm/hashIndex.js.map +1 -0
- package/dist/esm/hashing/hash.d.ts +1 -0
- package/dist/esm/hashing/hash.js +122 -0
- package/dist/esm/hashing/hash.js.map +1 -0
- package/dist/esm/hashing/index.d.ts +2 -0
- package/dist/esm/hashing/murmur.d.ts +26 -0
- package/dist/esm/hashing/murmur.js +103 -0
- package/dist/esm/hashing/murmur.js.map +1 -0
- package/dist/esm/indexes.d.ts +0 -2
- package/dist/esm/indexes.js +98 -46
- package/dist/esm/indexes.js.map +1 -1
- package/dist/esm/multiset.js +2 -1
- package/dist/esm/multiset.js.map +1 -1
- package/dist/esm/operators/distinct.js +1 -1
- package/dist/esm/operators/distinct.js.map +1 -1
- package/dist/esm/operators/groupBy.d.ts +1 -1
- package/dist/esm/operators/groupBy.js +5 -4
- package/dist/esm/operators/groupBy.js.map +1 -1
- package/dist/esm/utils.d.ts +7 -4
- package/dist/esm/utils.js +13 -33
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/valueIndex.d.ts +17 -0
- package/dist/esm/valueIndex.js +74 -0
- package/dist/esm/valueIndex.js.map +1 -0
- package/package.json +1 -3
- package/src/hashIndex.ts +94 -0
- package/src/hashing/hash.ts +157 -0
- package/src/hashing/index.ts +2 -0
- package/src/hashing/murmur.ts +128 -0
- package/src/indexes.ts +106 -47
- package/src/multiset.ts +1 -1
- package/src/operators/distinct.ts +4 -4
- package/src/operators/groupBy.ts +7 -4
- package/src/utils.ts +31 -48
- package/src/valueIndex.ts +78 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Implementation of murmur hash based on the Composites polyfill implementation:
|
|
3
|
+
* https://github.com/tc39/proposal-composites
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const RANDOM_SEED = randomHash()
|
|
7
|
+
const STRING_MARKER = randomHash()
|
|
8
|
+
const BIG_INT_MARKER = randomHash()
|
|
9
|
+
const NEG_BIG_INT_MARKER = randomHash()
|
|
10
|
+
const SYMBOL_MARKER = randomHash()
|
|
11
|
+
|
|
12
|
+
export type Hash = number
|
|
13
|
+
|
|
14
|
+
export function randomHash() {
|
|
15
|
+
return (Math.random() * (2 ** 31 - 1)) >>> 0
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Hasher {
|
|
19
|
+
update: (val: symbol | string | number | bigint) => void
|
|
20
|
+
digest: () => number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* This implementation of Murmur hash uses a random initial seed and random markers.
|
|
25
|
+
* This means that hashes aren't deterministic across app restarts.
|
|
26
|
+
* This is intentional in the composites polyfill to be resistent to hash-flooding attacks
|
|
27
|
+
* where malicious users would precompute lots of different objects whose hashes collide with each other.
|
|
28
|
+
*
|
|
29
|
+
* Currently, for ts/db-ivm this is fine because we don't persist client state.
|
|
30
|
+
* However, when we will introduce persistence we will either need to store the seeds or remove the randomness
|
|
31
|
+
* to ensure deterministic hashes across app restarts.
|
|
32
|
+
*/
|
|
33
|
+
export class MurmurHashStream implements Hasher {
|
|
34
|
+
private hash: number = RANDOM_SEED
|
|
35
|
+
private length = 0
|
|
36
|
+
private carry = 0
|
|
37
|
+
private carryBytes = 0
|
|
38
|
+
|
|
39
|
+
private _mix(k1: number): void {
|
|
40
|
+
k1 = Math.imul(k1, 0xcc9e2d51)
|
|
41
|
+
k1 = (k1 << 15) | (k1 >>> 17)
|
|
42
|
+
k1 = Math.imul(k1, 0x1b873593)
|
|
43
|
+
this.hash ^= k1
|
|
44
|
+
this.hash = (this.hash << 13) | (this.hash >>> 19)
|
|
45
|
+
this.hash = Math.imul(this.hash, 5) + 0xe6546b64
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private _writeByte(byte: number): void {
|
|
49
|
+
this.carry |= (byte & 0xff) << (8 * this.carryBytes)
|
|
50
|
+
this.carryBytes++
|
|
51
|
+
this.length++
|
|
52
|
+
|
|
53
|
+
if (this.carryBytes === 4) {
|
|
54
|
+
this._mix(this.carry >>> 0)
|
|
55
|
+
this.carry = 0
|
|
56
|
+
this.carryBytes = 0
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
update(chunk: symbol | string | number | bigint): void {
|
|
61
|
+
switch (typeof chunk) {
|
|
62
|
+
case `symbol`: {
|
|
63
|
+
this.update(SYMBOL_MARKER)
|
|
64
|
+
const description = chunk.description
|
|
65
|
+
if (!description) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < description.length; i++) {
|
|
70
|
+
const code = description.charCodeAt(i)
|
|
71
|
+
this._writeByte(code & 0xff)
|
|
72
|
+
this._writeByte((code >>> 8) & 0xff)
|
|
73
|
+
}
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
case `string`:
|
|
77
|
+
this.update(STRING_MARKER)
|
|
78
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
79
|
+
const code = chunk.charCodeAt(i)
|
|
80
|
+
this._writeByte(code & 0xff)
|
|
81
|
+
this._writeByte((code >>> 8) & 0xff)
|
|
82
|
+
}
|
|
83
|
+
return
|
|
84
|
+
case `number`:
|
|
85
|
+
this._writeByte(chunk & 0xff)
|
|
86
|
+
this._writeByte((chunk >>> 8) & 0xff)
|
|
87
|
+
this._writeByte((chunk >>> 16) & 0xff)
|
|
88
|
+
this._writeByte((chunk >>> 24) & 0xff)
|
|
89
|
+
return
|
|
90
|
+
case `bigint`: {
|
|
91
|
+
let value = chunk
|
|
92
|
+
if (value < 0n) {
|
|
93
|
+
value = -value
|
|
94
|
+
this.update(NEG_BIG_INT_MARKER)
|
|
95
|
+
} else {
|
|
96
|
+
this.update(BIG_INT_MARKER)
|
|
97
|
+
}
|
|
98
|
+
while (value > 0n) {
|
|
99
|
+
this._writeByte(Number(value & 0xffn))
|
|
100
|
+
value >>= 8n
|
|
101
|
+
}
|
|
102
|
+
if (chunk === 0n) this._writeByte(0)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
default:
|
|
106
|
+
throw new TypeError(`Unsupported input type: ${typeof chunk}`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
digest(): number {
|
|
111
|
+
if (this.carryBytes > 0) {
|
|
112
|
+
let k1 = this.carry >>> 0
|
|
113
|
+
k1 = Math.imul(k1, 0xcc9e2d51)
|
|
114
|
+
k1 = (k1 << 15) | (k1 >>> 17)
|
|
115
|
+
k1 = Math.imul(k1, 0x1b873593)
|
|
116
|
+
this.hash ^= k1
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.hash ^= this.length
|
|
120
|
+
this.hash ^= this.hash >>> 16
|
|
121
|
+
this.hash = Math.imul(this.hash, 0x85ebca6b)
|
|
122
|
+
this.hash ^= this.hash >>> 13
|
|
123
|
+
this.hash = Math.imul(this.hash, 0xc2b2ae35)
|
|
124
|
+
this.hash ^= this.hash >>> 16
|
|
125
|
+
|
|
126
|
+
return this.hash >>> 0
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/indexes.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { MultiSet } from "./multiset.js"
|
|
2
|
-
import {
|
|
2
|
+
import { HashIndex } from "./hashIndex.js"
|
|
3
|
+
import { ValueIndex } from "./valueIndex.js"
|
|
4
|
+
import { concatIterable, mapIterable } from "./utils.js"
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* A map from a difference collection trace's keys -> (value, multiplicities) that changed.
|
|
@@ -7,77 +9,134 @@ import { DefaultMap } from "./utils.js"
|
|
|
7
9
|
* exploit the key-value structure of the data to run efficiently.
|
|
8
10
|
*/
|
|
9
11
|
export class Index<K, V> {
|
|
10
|
-
|
|
12
|
+
/*
|
|
13
|
+
* This is a hybrid Index that composes a ValueIndex and a HashIndex.
|
|
14
|
+
* Keys that have only one value are stored in the ValueIndex.
|
|
15
|
+
* Keys that have multiple values are stored in the HashIndex, the hash distinguishes between the values.
|
|
16
|
+
* This reduces the amount of hashes we need to compute since often times only a small portion of the keys are updated
|
|
17
|
+
* so we don't have to hash the keys that are never updated.
|
|
18
|
+
*
|
|
19
|
+
* Note: The `valueIndex` and `hashIndex` have disjoint keys.
|
|
20
|
+
* When a key that has only one value gets a new distinct value,
|
|
21
|
+
* it is added to the `hashIndex` and removed from the `valueIndex` and vice versa.
|
|
22
|
+
*/
|
|
23
|
+
#valueIndex: ValueIndex<K, V>
|
|
24
|
+
#hashIndex: HashIndex<K, V>
|
|
11
25
|
|
|
12
26
|
constructor() {
|
|
13
|
-
this.#
|
|
14
|
-
|
|
15
|
-
// {
|
|
16
|
-
// [key]: Map<V, number> // Direct value-to-multiplicity mapping
|
|
17
|
-
// }
|
|
27
|
+
this.#valueIndex = new ValueIndex<K, V>()
|
|
28
|
+
this.#hashIndex = new HashIndex<K, V>()
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
toString(indent = false): string {
|
|
21
|
-
return `Index(${
|
|
22
|
-
[...this.#inner].map(([k, valueMap]) => [k, [...valueMap]]),
|
|
23
|
-
undefined,
|
|
24
|
-
indent ? ` ` : undefined
|
|
25
|
-
)})`
|
|
32
|
+
return `Index(\n ${this.#valueIndex.toString(indent)},\n ${this.#hashIndex.toString(indent)}\n)`
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
get(key: K): Array<[V, number]> {
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
if (this.#valueIndex.has(key)) {
|
|
37
|
+
return [this.#valueIndex.get(key)!]
|
|
38
|
+
}
|
|
39
|
+
return this.#hashIndex.get(key)
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
getMultiplicity(key: K, value: V): number {
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
if (this.#valueIndex.has(key)) {
|
|
44
|
+
return this.#valueIndex.getMultiplicity(key)
|
|
45
|
+
}
|
|
46
|
+
return this.#hashIndex.getMultiplicity(key, value)
|
|
36
47
|
}
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
/**
|
|
50
|
+
* This returns an iterator that iterates over all key-value pairs.
|
|
51
|
+
* @returns An iterable of all key-value pairs (and their multiplicities) in the index.
|
|
52
|
+
*/
|
|
53
|
+
#entries(): Iterable<[K, [V, number]]> {
|
|
54
|
+
return concatIterable(
|
|
55
|
+
this.#valueIndex.entries(),
|
|
56
|
+
this.#hashIndex.entriesIterator()
|
|
57
|
+
)
|
|
40
58
|
}
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
/**
|
|
61
|
+
* This method only iterates over the keys and not over the values.
|
|
62
|
+
* Hence, it is more efficient than the `#entries` method.
|
|
63
|
+
* It returns an iterator that you can use if you need to iterate over the values for a given key.
|
|
64
|
+
* @returns An iterator of all *keys* in the index and their corresponding value iterator.
|
|
65
|
+
*/
|
|
66
|
+
*#entriesIterators(): Iterable<[K, Iterable<[V, number]>]> {
|
|
67
|
+
for (const [key, [value, multiplicity]] of this.#valueIndex.entries()) {
|
|
68
|
+
yield [key, new Map<V, number>([[value, multiplicity]])]
|
|
69
|
+
}
|
|
70
|
+
for (const [key, valueMap] of this.#hashIndex.entries()) {
|
|
71
|
+
yield [
|
|
72
|
+
key,
|
|
73
|
+
mapIterable(valueMap, ([_hash, [value, multiplicity]]) => [
|
|
74
|
+
value,
|
|
75
|
+
multiplicity,
|
|
76
|
+
]),
|
|
77
|
+
]
|
|
78
|
+
}
|
|
44
79
|
}
|
|
45
80
|
|
|
46
81
|
has(key: K): boolean {
|
|
47
|
-
return this.#
|
|
82
|
+
return this.#valueIndex.has(key) || this.#hashIndex.has(key)
|
|
48
83
|
}
|
|
49
84
|
|
|
50
85
|
get size(): number {
|
|
51
|
-
return this.#
|
|
86
|
+
return this.#valueIndex.size + this.#hashIndex.size
|
|
52
87
|
}
|
|
53
88
|
|
|
54
89
|
addValue(key: K, value: [V, number]): void {
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
const containedInValueIndex = this.#valueIndex.has(key)
|
|
91
|
+
const containedInHashIndex = this.#hashIndex.has(key)
|
|
92
|
+
|
|
93
|
+
if (containedInHashIndex && containedInValueIndex) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Key ${key} is contained in both the value index and the hash index. This should never happen because they should have disjoint keysets.`
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!containedInValueIndex && !containedInHashIndex) {
|
|
100
|
+
// This is the first time we see the key
|
|
101
|
+
// Add it to the value index
|
|
102
|
+
this.#valueIndex.addValue(key, value)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (containedInValueIndex) {
|
|
107
|
+
// This key is already in the value index
|
|
108
|
+
// It could be that it's the same value or a different one
|
|
109
|
+
// If it's a different value we will need to remove the key from the value index
|
|
110
|
+
// and add the key and its two values to the hash index
|
|
111
|
+
try {
|
|
112
|
+
this.#valueIndex.addValue(key, value)
|
|
113
|
+
} catch {
|
|
114
|
+
// This is a different value, need to move the key to the hash index
|
|
115
|
+
const existingValue = this.#valueIndex.get(key)!
|
|
116
|
+
this.#valueIndex.delete(key)
|
|
117
|
+
this.#hashIndex.addValue(key, existingValue)
|
|
118
|
+
this.#hashIndex.addValue(key, value)
|
|
65
119
|
}
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (containedInHashIndex) {
|
|
124
|
+
// This key is already in the hash index so it already has two or more values.
|
|
125
|
+
// However, this new value and multiplicity could cause an existing value to be removed
|
|
126
|
+
// and lead to the key having only a single value in which case we need to move it back to the value index
|
|
127
|
+
const singleRemainingValue = this.#hashIndex.addValue(key, value)
|
|
128
|
+
if (singleRemainingValue) {
|
|
129
|
+
// The key only has a single remaining value so we need to move it back to the value index
|
|
130
|
+
this.#hashIndex.delete(key)
|
|
131
|
+
this.#valueIndex.addValue(key, singleRemainingValue)
|
|
132
|
+
}
|
|
133
|
+
return
|
|
66
134
|
}
|
|
67
135
|
}
|
|
68
136
|
|
|
69
137
|
append(other: Index<K, V>): void {
|
|
70
|
-
for (const [key,
|
|
71
|
-
|
|
72
|
-
for (const [value, multiplicity] of otherValueMap.entries()) {
|
|
73
|
-
const existingMultiplicity = thisValueMap.get(value) ?? 0
|
|
74
|
-
const newMultiplicity = existingMultiplicity + multiplicity
|
|
75
|
-
if (newMultiplicity === 0) {
|
|
76
|
-
thisValueMap.delete(value)
|
|
77
|
-
} else {
|
|
78
|
-
thisValueMap.set(value, newMultiplicity)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
138
|
+
for (const [key, value] of other.#entries()) {
|
|
139
|
+
this.addValue(key, value)
|
|
81
140
|
}
|
|
82
141
|
}
|
|
83
142
|
|
|
@@ -87,10 +146,10 @@ export class Index<K, V> {
|
|
|
87
146
|
// We want to iterate over the smaller of the two indexes to reduce the
|
|
88
147
|
// number of operations we need to do.
|
|
89
148
|
if (this.size <= other.size) {
|
|
90
|
-
for (const [key,
|
|
149
|
+
for (const [key, valueIt] of this.#entriesIterators()) {
|
|
91
150
|
if (!other.has(key)) continue
|
|
92
151
|
const otherValues = other.get(key)
|
|
93
|
-
for (const [val1, mul1] of
|
|
152
|
+
for (const [val1, mul1] of valueIt) {
|
|
94
153
|
for (const [val2, mul2] of otherValues) {
|
|
95
154
|
if (mul1 !== 0 && mul2 !== 0) {
|
|
96
155
|
result.push([[key, [val1, val2]], mul1 * mul2])
|
|
@@ -99,10 +158,10 @@ export class Index<K, V> {
|
|
|
99
158
|
}
|
|
100
159
|
}
|
|
101
160
|
} else {
|
|
102
|
-
for (const [key,
|
|
161
|
+
for (const [key, otherValueIt] of other.#entriesIterators()) {
|
|
103
162
|
if (!this.has(key)) continue
|
|
104
163
|
const values = this.get(key)
|
|
105
|
-
for (const [val2, mul2] of
|
|
164
|
+
for (const [val2, mul2] of otherValueIt) {
|
|
106
165
|
for (const [val1, mul1] of values) {
|
|
107
166
|
if (mul1 !== 0 && mul2 !== 0) {
|
|
108
167
|
result.push([[key, [val1, val2]], mul1 * mul2])
|
package/src/multiset.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { DifferenceStreamWriter, UnaryOperator } from "../graph.js"
|
|
2
2
|
import { StreamBuilder } from "../d2.js"
|
|
3
|
-
import { hash } from "../
|
|
3
|
+
import { hash } from "../hashing/index.js"
|
|
4
4
|
import { MultiSet } from "../multiset.js"
|
|
5
|
+
import type { Hash } from "../hashing/index.js"
|
|
5
6
|
import type { DifferenceStreamReader } from "../graph.js"
|
|
6
7
|
import type { IStreamBuilder } from "../types.js"
|
|
7
8
|
|
|
8
|
-
type HashedValue = string
|
|
9
9
|
type Multiplicity = number
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -13,7 +13,7 @@ type Multiplicity = number
|
|
|
13
13
|
*/
|
|
14
14
|
export class DistinctOperator<T> extends UnaryOperator<T> {
|
|
15
15
|
#by: (value: T) => any
|
|
16
|
-
#values: Map<
|
|
16
|
+
#values: Map<Hash, Multiplicity> // keeps track of the number of times each value has been seen
|
|
17
17
|
|
|
18
18
|
constructor(
|
|
19
19
|
id: number,
|
|
@@ -27,7 +27,7 @@ export class DistinctOperator<T> extends UnaryOperator<T> {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
run(): void {
|
|
30
|
-
const updatedValues = new Map<
|
|
30
|
+
const updatedValues = new Map<Hash, [Multiplicity, T]>()
|
|
31
31
|
|
|
32
32
|
// Compute the new multiplicity for each value
|
|
33
33
|
for (const message of this.inputMessages()) {
|
package/src/operators/groupBy.ts
CHANGED
|
@@ -165,13 +165,16 @@ export function sum<T>(
|
|
|
165
165
|
/**
|
|
166
166
|
* Creates a count aggregate function
|
|
167
167
|
*/
|
|
168
|
-
export function count<T>(
|
|
168
|
+
export function count<T>(
|
|
169
|
+
valueExtractor: (value: T) => any = (v) => v
|
|
170
|
+
): AggregateFunction<T, number, number> {
|
|
169
171
|
return {
|
|
170
|
-
|
|
172
|
+
// Count only not-null values (the `== null` comparison gives true for both null and undefined)
|
|
173
|
+
preMap: (data: T) => (valueExtractor(data) == null ? 0 : 1),
|
|
171
174
|
reduce: (values: Array<[number, number]>) => {
|
|
172
175
|
let totalCount = 0
|
|
173
|
-
for (const [
|
|
174
|
-
totalCount += multiplicity
|
|
176
|
+
for (const [nullMultiplier, multiplicity] of values) {
|
|
177
|
+
totalCount += nullMultiplier * multiplicity
|
|
175
178
|
}
|
|
176
179
|
return totalCount
|
|
177
180
|
},
|
package/src/utils.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Simple assertion function for runtime checks.
|
|
3
|
+
* Throws an error if the condition is false.
|
|
4
|
+
*/
|
|
5
|
+
export function assert(
|
|
6
|
+
condition: unknown,
|
|
7
|
+
message?: string
|
|
8
|
+
): asserts condition {
|
|
9
|
+
if (!condition) {
|
|
10
|
+
throw new Error(message || `Assertion failed`)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
2
13
|
|
|
3
14
|
/**
|
|
4
15
|
* A map that returns a default value for keys that are not present.
|
|
@@ -13,7 +24,8 @@ export class DefaultMap<K, V> extends Map<K, V> {
|
|
|
13
24
|
|
|
14
25
|
get(key: K): V {
|
|
15
26
|
if (!this.has(key)) {
|
|
16
|
-
this.set(key, this.defaultValue())
|
|
27
|
+
// this.set(key, this.defaultValue())
|
|
28
|
+
return this.defaultValue()
|
|
17
29
|
}
|
|
18
30
|
return super.get(key)!
|
|
19
31
|
}
|
|
@@ -45,52 +57,6 @@ export function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>) {
|
|
|
45
57
|
}
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
const hashCache = new WeakMap()
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Replacer function for JSON.stringify that converts unsupported types to strings
|
|
52
|
-
*/
|
|
53
|
-
function hashReplacer(_key: string, value: any): any {
|
|
54
|
-
if (typeof value === `bigint`) {
|
|
55
|
-
return String(value)
|
|
56
|
-
} else if (typeof value === `symbol`) {
|
|
57
|
-
return String(value)
|
|
58
|
-
} else if (typeof value === `function`) {
|
|
59
|
-
return String(value)
|
|
60
|
-
} else if (value === undefined) {
|
|
61
|
-
return `undefined`
|
|
62
|
-
} else if (value instanceof Map) {
|
|
63
|
-
return `Map(${JSON.stringify(Array.from(value.entries()), hashReplacer)})`
|
|
64
|
-
} else if (value instanceof Set) {
|
|
65
|
-
return `Set(${JSON.stringify(Array.from(value.values()), hashReplacer)})`
|
|
66
|
-
}
|
|
67
|
-
return value
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* A hash method that caches the hash of a value in a week map
|
|
72
|
-
*/
|
|
73
|
-
export function hash(data: any): string {
|
|
74
|
-
if (
|
|
75
|
-
data === null ||
|
|
76
|
-
data === undefined ||
|
|
77
|
-
(typeof data !== `object` && typeof data !== `function`)
|
|
78
|
-
) {
|
|
79
|
-
// Can't be cached in the weak map because it's not an object
|
|
80
|
-
const serialized = JSON.stringify(data, hashReplacer)
|
|
81
|
-
return murmurhash.murmur3(serialized).toString(16)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (hashCache.has(data)) {
|
|
85
|
-
return hashCache.get(data)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const serialized = JSON.stringify(data, hashReplacer)
|
|
89
|
-
const hashValue = murmurhash.murmur3(serialized).toString(16)
|
|
90
|
-
hashCache.set(data, hashValue)
|
|
91
|
-
return hashValue
|
|
92
|
-
}
|
|
93
|
-
|
|
94
60
|
export function binarySearch<T>(
|
|
95
61
|
array: Array<T>,
|
|
96
62
|
value: T,
|
|
@@ -161,3 +127,20 @@ export class ObjectIdGenerator {
|
|
|
161
127
|
* Global instance for cases where a shared object ID space is needed.
|
|
162
128
|
*/
|
|
163
129
|
export const globalObjectIdGenerator = new ObjectIdGenerator()
|
|
130
|
+
|
|
131
|
+
export function* concatIterable<T>(
|
|
132
|
+
...iterables: Array<Iterable<T>>
|
|
133
|
+
): Iterable<T> {
|
|
134
|
+
for (const iterable of iterables) {
|
|
135
|
+
yield* iterable
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function* mapIterable<T, U>(
|
|
140
|
+
it: Iterable<T>,
|
|
141
|
+
fn: (t: T) => U
|
|
142
|
+
): Iterable<U> {
|
|
143
|
+
for (const t of it) {
|
|
144
|
+
yield fn(t)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { hash } from "./hashing/index.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A map from a difference collection trace's keys -> (value, multiplicities) that changed.
|
|
5
|
+
* Used in operations like join and reduce where the operation needs to
|
|
6
|
+
* exploit the key-value structure of the data to run efficiently.
|
|
7
|
+
*/
|
|
8
|
+
export class ValueIndex<K, V> {
|
|
9
|
+
#inner: Map<K, [V, number]> // Maps key to the value and its multiplicity
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this.#inner = new Map()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
toString(indent = false): string {
|
|
16
|
+
return `ValueIndex(${JSON.stringify(
|
|
17
|
+
[...this.#inner.entries()],
|
|
18
|
+
undefined,
|
|
19
|
+
indent ? 2 : undefined
|
|
20
|
+
)})`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get(key: K): [V, number] | undefined {
|
|
24
|
+
return this.#inner.get(key)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getMultiplicity(key: K): number {
|
|
28
|
+
return this.get(key)?.[1] ?? 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
entries() {
|
|
32
|
+
return this.#inner.entries()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
has(key: K): boolean {
|
|
36
|
+
return this.#inner.has(key)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
delete(key: K): void {
|
|
40
|
+
this.#inner.delete(key)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get size(): number {
|
|
44
|
+
return this.#inner.size
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
addValue(key: K, v: [V, number]): void {
|
|
48
|
+
const [value, multiplicity] = v
|
|
49
|
+
|
|
50
|
+
if (multiplicity === 0) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (this.has(key)) {
|
|
55
|
+
const [currValue, currMultiplicity] = this.get(key)!
|
|
56
|
+
if (hash(value) === hash(currValue)) {
|
|
57
|
+
// Update the multiplicity
|
|
58
|
+
this.#setMultiplicity(key, value, currMultiplicity + multiplicity)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
// Different value, not allowed.
|
|
62
|
+
// ValueIndex only supports one value per key.
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Cannot add value for key ${key} because it already exists in ValueIndex with a different value`
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.#inner.set(key, [value, multiplicity])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#setMultiplicity(key: K, value: V, multiplicity: number): void {
|
|
72
|
+
if (multiplicity === 0) {
|
|
73
|
+
this.#inner.delete(key)
|
|
74
|
+
} else {
|
|
75
|
+
this.#inner.set(key, [value, multiplicity])
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|