@tanstack/db-ivm 0.1.1 → 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.
Files changed (92) hide show
  1. package/dist/cjs/hashIndex.cjs +83 -0
  2. package/dist/cjs/hashIndex.cjs.map +1 -0
  3. package/dist/cjs/hashIndex.d.cts +25 -0
  4. package/dist/cjs/hashing/hash.cjs +122 -0
  5. package/dist/cjs/hashing/hash.cjs.map +1 -0
  6. package/dist/cjs/hashing/hash.d.cts +1 -0
  7. package/dist/cjs/hashing/index.d.cts +2 -0
  8. package/dist/cjs/hashing/murmur.cjs +103 -0
  9. package/dist/cjs/hashing/murmur.cjs.map +1 -0
  10. package/dist/cjs/hashing/murmur.d.cts +26 -0
  11. package/dist/cjs/index.cjs +3 -0
  12. package/dist/cjs/index.cjs.map +1 -1
  13. package/dist/cjs/indexes.cjs +97 -45
  14. package/dist/cjs/indexes.cjs.map +1 -1
  15. package/dist/cjs/indexes.d.cts +0 -2
  16. package/dist/cjs/multiset.cjs +2 -1
  17. package/dist/cjs/multiset.cjs.map +1 -1
  18. package/dist/cjs/operators/distinct.cjs +2 -2
  19. package/dist/cjs/operators/distinct.cjs.map +1 -1
  20. package/dist/cjs/operators/groupBy.cjs +5 -4
  21. package/dist/cjs/operators/groupBy.cjs.map +1 -1
  22. package/dist/cjs/operators/groupBy.d.cts +1 -1
  23. package/dist/cjs/operators/index.d.cts +1 -0
  24. package/dist/cjs/operators/orderBy.cjs +3 -1
  25. package/dist/cjs/operators/orderBy.cjs.map +1 -1
  26. package/dist/cjs/operators/orderBy.d.cts +6 -2
  27. package/dist/cjs/operators/tap.cjs +46 -0
  28. package/dist/cjs/operators/tap.cjs.map +1 -0
  29. package/dist/cjs/operators/tap.d.cts +18 -0
  30. package/dist/cjs/operators/topKWithFractionalIndex.cjs +13 -3
  31. package/dist/cjs/operators/topKWithFractionalIndex.cjs.map +1 -1
  32. package/dist/cjs/operators/topKWithFractionalIndex.d.cts +2 -0
  33. package/dist/cjs/utils.cjs +13 -33
  34. package/dist/cjs/utils.cjs.map +1 -1
  35. package/dist/cjs/utils.d.cts +7 -4
  36. package/dist/cjs/valueIndex.cjs +74 -0
  37. package/dist/cjs/valueIndex.cjs.map +1 -0
  38. package/dist/cjs/valueIndex.d.cts +17 -0
  39. package/dist/esm/hashIndex.d.ts +25 -0
  40. package/dist/esm/hashIndex.js +83 -0
  41. package/dist/esm/hashIndex.js.map +1 -0
  42. package/dist/esm/hashing/hash.d.ts +1 -0
  43. package/dist/esm/hashing/hash.js +122 -0
  44. package/dist/esm/hashing/hash.js.map +1 -0
  45. package/dist/esm/hashing/index.d.ts +2 -0
  46. package/dist/esm/hashing/murmur.d.ts +26 -0
  47. package/dist/esm/hashing/murmur.js +103 -0
  48. package/dist/esm/hashing/murmur.js.map +1 -0
  49. package/dist/esm/index.js +3 -0
  50. package/dist/esm/index.js.map +1 -1
  51. package/dist/esm/indexes.d.ts +0 -2
  52. package/dist/esm/indexes.js +98 -46
  53. package/dist/esm/indexes.js.map +1 -1
  54. package/dist/esm/multiset.js +2 -1
  55. package/dist/esm/multiset.js.map +1 -1
  56. package/dist/esm/operators/distinct.js +1 -1
  57. package/dist/esm/operators/distinct.js.map +1 -1
  58. package/dist/esm/operators/groupBy.d.ts +1 -1
  59. package/dist/esm/operators/groupBy.js +5 -4
  60. package/dist/esm/operators/groupBy.js.map +1 -1
  61. package/dist/esm/operators/index.d.ts +1 -0
  62. package/dist/esm/operators/orderBy.d.ts +6 -2
  63. package/dist/esm/operators/orderBy.js +3 -1
  64. package/dist/esm/operators/orderBy.js.map +1 -1
  65. package/dist/esm/operators/tap.d.ts +18 -0
  66. package/dist/esm/operators/tap.js +46 -0
  67. package/dist/esm/operators/tap.js.map +1 -0
  68. package/dist/esm/operators/topKWithFractionalIndex.d.ts +2 -0
  69. package/dist/esm/operators/topKWithFractionalIndex.js +13 -3
  70. package/dist/esm/operators/topKWithFractionalIndex.js.map +1 -1
  71. package/dist/esm/utils.d.ts +7 -4
  72. package/dist/esm/utils.js +13 -33
  73. package/dist/esm/utils.js.map +1 -1
  74. package/dist/esm/valueIndex.d.ts +17 -0
  75. package/dist/esm/valueIndex.js +74 -0
  76. package/dist/esm/valueIndex.js.map +1 -0
  77. package/package.json +1 -3
  78. package/src/hashIndex.ts +94 -0
  79. package/src/hashing/hash.ts +157 -0
  80. package/src/hashing/index.ts +2 -0
  81. package/src/hashing/murmur.ts +128 -0
  82. package/src/indexes.ts +106 -47
  83. package/src/multiset.ts +1 -1
  84. package/src/operators/distinct.ts +4 -4
  85. package/src/operators/groupBy.ts +7 -4
  86. package/src/operators/index.ts +1 -0
  87. package/src/operators/orderBy.ts +8 -2
  88. package/src/operators/tap.ts +53 -0
  89. package/src/operators/topKWithFractionalIndex.ts +14 -2
  90. package/src/operators/topKWithFractionalIndexBTree.ts +7 -0
  91. package/src/utils.ts +31 -48
  92. package/src/valueIndex.ts +78 -0
@@ -0,0 +1,94 @@
1
+ import { DefaultMap } from "./utils.js"
2
+ import { hash } from "./hashing/index.js"
3
+ import type { Hash } from "./hashing/index.js"
4
+
5
+ /**
6
+ * A map from a difference collection trace's keys -> (value, multiplicities) that changed.
7
+ * Used in operations like join and reduce where the operation needs to
8
+ * exploit the key-value structure of the data to run efficiently.
9
+ */
10
+ export class HashIndex<K, V> {
11
+ #inner: DefaultMap<K, DefaultMap<Hash, [V, number]>>
12
+
13
+ constructor() {
14
+ this.#inner = new DefaultMap<K, DefaultMap<Hash, [V, number]>>(
15
+ () => new DefaultMap<Hash, [V, number]>(() => [undefined as any as V, 0])
16
+ )
17
+ // #inner is as map of:
18
+ // {
19
+ // [key]: {
20
+ // [hash(value)]: [value, multiplicity]
21
+ // }
22
+ // }
23
+ }
24
+
25
+ toString(indent = false): string {
26
+ return `HashIndex(${JSON.stringify(
27
+ [...this.#inner].map(([k, valueMap]) => [k, [...valueMap]]),
28
+ undefined,
29
+ indent ? 2 : undefined
30
+ )})`
31
+ }
32
+
33
+ get(key: K): Array<[V, number]> {
34
+ const valueMap = this.#inner.get(key)
35
+ return [...valueMap.values()]
36
+ }
37
+
38
+ getMultiplicity(key: K, value: V): number {
39
+ const valueMap = this.#inner.get(key)
40
+ const valueHash = hash(value)
41
+ const [, multiplicity] = valueMap.get(valueHash)
42
+ return multiplicity
43
+ }
44
+
45
+ entries() {
46
+ return this.#inner.entries()
47
+ }
48
+
49
+ *entriesIterator(): Generator<[K, [V, number]]> {
50
+ for (const [key, valueMap] of this.#inner.entries()) {
51
+ for (const [_valueHash, [value, multiplicity]] of valueMap.entries()) {
52
+ yield [key, [value, multiplicity]]
53
+ }
54
+ }
55
+ }
56
+
57
+ has(key: K): boolean {
58
+ return this.#inner.has(key)
59
+ }
60
+
61
+ delete(key: K): void {
62
+ this.#inner.delete(key)
63
+ }
64
+
65
+ get size(): number {
66
+ return this.#inner.size
67
+ }
68
+
69
+ /**
70
+ * Adds a value to the index and does not return anything
71
+ * except if the addition caused the value to be removed
72
+ * and the key to be left with only a single value.
73
+ * In that case, we return the single remaining value.
74
+ */
75
+ addValue(key: K, value: [V, number]): [V, number] | void {
76
+ const [val, multiplicity] = value
77
+ const valueMap = this.#inner.get(key)
78
+ const valueHash = hash(val)
79
+ const [, existingMultiplicity] = valueMap.get(valueHash)
80
+ const newMultiplicity = existingMultiplicity + multiplicity
81
+ if (multiplicity !== 0) {
82
+ if (newMultiplicity === 0) {
83
+ valueMap.delete(valueHash)
84
+ if (valueMap.size === 1) {
85
+ // Signal that the key only has a single remaining value
86
+ return valueMap.entries().next().value![1]
87
+ }
88
+ } else {
89
+ valueMap.set(valueHash, [val, newMultiplicity])
90
+ }
91
+ }
92
+ this.#inner.set(key, valueMap)
93
+ }
94
+ }
@@ -0,0 +1,157 @@
1
+ import { MurmurHashStream, randomHash } from "./murmur.js"
2
+ import type { Hasher } from "./murmur.js"
3
+
4
+ /*
5
+ * Implementation of structural hashing based on the Composites polyfill implementation:
6
+ * https://github.com/tc39/proposal-composites
7
+ */
8
+
9
+ const TRUE = randomHash()
10
+ const FALSE = randomHash()
11
+ const NULL = randomHash()
12
+ const UNDEFINED = randomHash()
13
+ const KEY = randomHash()
14
+ const FUNCTIONS = randomHash()
15
+ const DATE_MARKER = randomHash()
16
+ const OBJECT_MARKER = randomHash()
17
+ const ARRAY_MARKER = randomHash()
18
+ const MAP_MARKER = randomHash()
19
+ const SET_MARKER = randomHash()
20
+
21
+ const hashCache = new WeakMap<object, number>()
22
+
23
+ export function hash(input: any): number {
24
+ const hasher = new MurmurHashStream()
25
+ updateHasher(hasher, input)
26
+ return hasher.digest()
27
+ }
28
+
29
+ function hashObject(input: object): number {
30
+ const cachedHash = hashCache.get(input)
31
+ if (cachedHash !== undefined) {
32
+ return cachedHash
33
+ }
34
+
35
+ let valueHash: number | undefined
36
+ if (input instanceof Date) {
37
+ valueHash = hashDate(input)
38
+ } else {
39
+ let plainObjectInput = input
40
+ let marker = OBJECT_MARKER
41
+
42
+ if (input instanceof Array) {
43
+ marker = ARRAY_MARKER
44
+ }
45
+
46
+ if (input instanceof Map) {
47
+ marker = MAP_MARKER
48
+ plainObjectInput = [...input.entries()]
49
+ }
50
+
51
+ if (input instanceof Set) {
52
+ marker = SET_MARKER
53
+ plainObjectInput = [...input.entries()]
54
+ }
55
+
56
+ if (
57
+ input instanceof Buffer ||
58
+ input instanceof Uint8Array ||
59
+ input instanceof File
60
+ ) {
61
+ // Deeply hashing these objects would be too costly
62
+ // but we also don't want to ignore them
63
+ // so we track them by reference and cache them in a weak map
64
+ return cachedReferenceHash(input)
65
+ }
66
+
67
+ valueHash = hashPlainObject(plainObjectInput, marker)
68
+ }
69
+
70
+ hashCache.set(input, valueHash)
71
+ return valueHash
72
+ }
73
+
74
+ function hashDate(input: Date): number {
75
+ const hasher = new MurmurHashStream()
76
+ hasher.update(DATE_MARKER)
77
+ hasher.update(input.getTime())
78
+ return hasher.digest()
79
+ }
80
+
81
+ function hashPlainObject(input: object, marker: number): number {
82
+ const hasher = new MurmurHashStream()
83
+
84
+ // Mark the type of the input
85
+ hasher.update(marker)
86
+ const keys = Object.keys(input)
87
+ keys.sort(keySort)
88
+ for (const key of keys) {
89
+ hasher.update(KEY)
90
+ hasher.update(key)
91
+ updateHasher(hasher, input[key as keyof typeof input])
92
+ }
93
+
94
+ return hasher.digest()
95
+ }
96
+
97
+ function updateHasher(hasher: Hasher, input: unknown): void {
98
+ if (input === null) {
99
+ hasher.update(NULL)
100
+ return
101
+ }
102
+ switch (typeof input) {
103
+ case `undefined`:
104
+ hasher.update(UNDEFINED)
105
+ return
106
+ case `boolean`:
107
+ hasher.update(input ? TRUE : FALSE)
108
+ return
109
+ case `number`:
110
+ // Normalize NaNs and -0
111
+ hasher.update(isNaN(input) ? NaN : input === 0 ? 0 : input)
112
+ return
113
+ case `bigint`:
114
+ case `string`:
115
+ case `symbol`:
116
+ hasher.update(input)
117
+ return
118
+ case `object`:
119
+ hasher.update(getCachedHash(input))
120
+ return
121
+ case `function`:
122
+ // Functions are assigned a globally unique ID
123
+ // and that ID is cached in the weak map
124
+ hasher.update(cachedReferenceHash(input))
125
+ return
126
+ default:
127
+ console.warn(
128
+ `Ignored input during hashing because it is of type ${typeof input} which is not supported`
129
+ )
130
+ }
131
+ }
132
+
133
+ function getCachedHash(input: object): number {
134
+ let valueHash = hashCache.get(input)
135
+ if (valueHash === undefined) {
136
+ valueHash = hashObject(input)
137
+ }
138
+ return valueHash
139
+ }
140
+
141
+ let nextRefId = 1
142
+ function cachedReferenceHash(fn: object): number {
143
+ let valueHash = hashCache.get(fn)
144
+ if (valueHash === undefined) {
145
+ valueHash = nextRefId ^ FUNCTIONS
146
+ nextRefId++
147
+ hashCache.set(fn, valueHash)
148
+ }
149
+ return valueHash
150
+ }
151
+
152
+ /**
153
+ * Strings sorted lexicographically.
154
+ */
155
+ function keySort(a: string, b: string): number {
156
+ return a.localeCompare(b)
157
+ }
@@ -0,0 +1,2 @@
1
+ export { hash } from "./hash.js"
2
+ export type { Hash, Hasher } from "./murmur.js"
@@ -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 { DefaultMap } from "./utils.js"
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
- #inner: DefaultMap<K, Map<V, number>>
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.#inner = new DefaultMap<K, Map<V, number>>(() => new Map<V, number>())
14
- // #inner is a map of:
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(${JSON.stringify(
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
- const valueMap = this.#inner.get(key)
30
- return [...valueMap.entries()]
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
- const valueMap = this.#inner.get(key)
35
- return valueMap.get(value) ?? 0
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
- entries() {
39
- return this.#inner.entries()
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
- keys() {
43
- return this.#inner.keys()
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.#inner.has(key)
82
+ return this.#valueIndex.has(key) || this.#hashIndex.has(key)
48
83
  }
49
84
 
50
85
  get size(): number {
51
- return this.#inner.size
86
+ return this.#valueIndex.size + this.#hashIndex.size
52
87
  }
53
88
 
54
89
  addValue(key: K, value: [V, number]): void {
55
- const [val, multiplicity] = value
56
- const valueMap = this.#inner.get(key)
57
- const existingMultiplicity = valueMap.get(val) ?? 0
58
- const newMultiplicity = existingMultiplicity + multiplicity
59
-
60
- if (multiplicity !== 0) {
61
- if (newMultiplicity === 0) {
62
- valueMap.delete(val)
63
- } else {
64
- valueMap.set(val, newMultiplicity)
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, otherValueMap] of other.entries()) {
71
- const thisValueMap = this.#inner.get(key)
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, valueMap] of this.entries()) {
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 valueMap.entries()) {
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, otherValueMap] of other.entries()) {
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 otherValueMap.entries()) {
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
@@ -2,8 +2,8 @@ import {
2
2
  DefaultMap,
3
3
  chunkedArrayPush,
4
4
  globalObjectIdGenerator,
5
- hash,
6
5
  } from "./utils.js"
6
+ import { hash } from "./hashing/index.js"
7
7
 
8
8
  export type MultiSetArray<T> = Array<[T, number]>
9
9
  export type KeyedData<T> = [key: string, value: T]
@@ -1,11 +1,11 @@
1
1
  import { DifferenceStreamWriter, UnaryOperator } from "../graph.js"
2
2
  import { StreamBuilder } from "../d2.js"
3
- import { hash } from "../utils.js"
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<HashedValue, Multiplicity> // keeps track of the number of times each value has been seen
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<HashedValue, [Multiplicity, T]>()
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()) {
@@ -165,13 +165,16 @@ export function sum<T>(
165
165
  /**
166
166
  * Creates a count aggregate function
167
167
  */
168
- export function count<T>(): AggregateFunction<T, number, number> {
168
+ export function count<T>(
169
+ valueExtractor: (value: T) => any = (v) => v
170
+ ): AggregateFunction<T, number, number> {
169
171
  return {
170
- preMap: () => 1,
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 [_, multiplicity] of values) {
174
- totalCount += multiplicity
176
+ for (const [nullMultiplier, multiplicity] of values) {
177
+ totalCount += nullMultiplier * multiplicity
175
178
  }
176
179
  return totalCount
177
180
  },
@@ -1,5 +1,6 @@
1
1
  export * from "./pipe.js"
2
2
  export * from "./map.js"
3
+ export * from "./tap.js"
3
4
  export * from "./filter.js"
4
5
  export * from "./negate.js"
5
6
  export * from "./concat.js"