@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,122 @@
|
|
|
1
|
+
import { MurmurHashStream, randomHash } from "./murmur.js";
|
|
2
|
+
const TRUE = randomHash();
|
|
3
|
+
const FALSE = randomHash();
|
|
4
|
+
const NULL = randomHash();
|
|
5
|
+
const UNDEFINED = randomHash();
|
|
6
|
+
const KEY = randomHash();
|
|
7
|
+
const FUNCTIONS = randomHash();
|
|
8
|
+
const DATE_MARKER = randomHash();
|
|
9
|
+
const OBJECT_MARKER = randomHash();
|
|
10
|
+
const ARRAY_MARKER = randomHash();
|
|
11
|
+
const MAP_MARKER = randomHash();
|
|
12
|
+
const SET_MARKER = randomHash();
|
|
13
|
+
const hashCache = /* @__PURE__ */ new WeakMap();
|
|
14
|
+
function hash(input) {
|
|
15
|
+
const hasher = new MurmurHashStream();
|
|
16
|
+
updateHasher(hasher, input);
|
|
17
|
+
return hasher.digest();
|
|
18
|
+
}
|
|
19
|
+
function hashObject(input) {
|
|
20
|
+
const cachedHash = hashCache.get(input);
|
|
21
|
+
if (cachedHash !== void 0) {
|
|
22
|
+
return cachedHash;
|
|
23
|
+
}
|
|
24
|
+
let valueHash;
|
|
25
|
+
if (input instanceof Date) {
|
|
26
|
+
valueHash = hashDate(input);
|
|
27
|
+
} else {
|
|
28
|
+
let plainObjectInput = input;
|
|
29
|
+
let marker = OBJECT_MARKER;
|
|
30
|
+
if (input instanceof Array) {
|
|
31
|
+
marker = ARRAY_MARKER;
|
|
32
|
+
}
|
|
33
|
+
if (input instanceof Map) {
|
|
34
|
+
marker = MAP_MARKER;
|
|
35
|
+
plainObjectInput = [...input.entries()];
|
|
36
|
+
}
|
|
37
|
+
if (input instanceof Set) {
|
|
38
|
+
marker = SET_MARKER;
|
|
39
|
+
plainObjectInput = [...input.entries()];
|
|
40
|
+
}
|
|
41
|
+
if (input instanceof Buffer || input instanceof Uint8Array || input instanceof File) {
|
|
42
|
+
return cachedReferenceHash(input);
|
|
43
|
+
}
|
|
44
|
+
valueHash = hashPlainObject(plainObjectInput, marker);
|
|
45
|
+
}
|
|
46
|
+
hashCache.set(input, valueHash);
|
|
47
|
+
return valueHash;
|
|
48
|
+
}
|
|
49
|
+
function hashDate(input) {
|
|
50
|
+
const hasher = new MurmurHashStream();
|
|
51
|
+
hasher.update(DATE_MARKER);
|
|
52
|
+
hasher.update(input.getTime());
|
|
53
|
+
return hasher.digest();
|
|
54
|
+
}
|
|
55
|
+
function hashPlainObject(input, marker) {
|
|
56
|
+
const hasher = new MurmurHashStream();
|
|
57
|
+
hasher.update(marker);
|
|
58
|
+
const keys = Object.keys(input);
|
|
59
|
+
keys.sort(keySort);
|
|
60
|
+
for (const key of keys) {
|
|
61
|
+
hasher.update(KEY);
|
|
62
|
+
hasher.update(key);
|
|
63
|
+
updateHasher(hasher, input[key]);
|
|
64
|
+
}
|
|
65
|
+
return hasher.digest();
|
|
66
|
+
}
|
|
67
|
+
function updateHasher(hasher, input) {
|
|
68
|
+
if (input === null) {
|
|
69
|
+
hasher.update(NULL);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
switch (typeof input) {
|
|
73
|
+
case `undefined`:
|
|
74
|
+
hasher.update(UNDEFINED);
|
|
75
|
+
return;
|
|
76
|
+
case `boolean`:
|
|
77
|
+
hasher.update(input ? TRUE : FALSE);
|
|
78
|
+
return;
|
|
79
|
+
case `number`:
|
|
80
|
+
hasher.update(isNaN(input) ? NaN : input === 0 ? 0 : input);
|
|
81
|
+
return;
|
|
82
|
+
case `bigint`:
|
|
83
|
+
case `string`:
|
|
84
|
+
case `symbol`:
|
|
85
|
+
hasher.update(input);
|
|
86
|
+
return;
|
|
87
|
+
case `object`:
|
|
88
|
+
hasher.update(getCachedHash(input));
|
|
89
|
+
return;
|
|
90
|
+
case `function`:
|
|
91
|
+
hasher.update(cachedReferenceHash(input));
|
|
92
|
+
return;
|
|
93
|
+
default:
|
|
94
|
+
console.warn(
|
|
95
|
+
`Ignored input during hashing because it is of type ${typeof input} which is not supported`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function getCachedHash(input) {
|
|
100
|
+
let valueHash = hashCache.get(input);
|
|
101
|
+
if (valueHash === void 0) {
|
|
102
|
+
valueHash = hashObject(input);
|
|
103
|
+
}
|
|
104
|
+
return valueHash;
|
|
105
|
+
}
|
|
106
|
+
let nextRefId = 1;
|
|
107
|
+
function cachedReferenceHash(fn) {
|
|
108
|
+
let valueHash = hashCache.get(fn);
|
|
109
|
+
if (valueHash === void 0) {
|
|
110
|
+
valueHash = nextRefId ^ FUNCTIONS;
|
|
111
|
+
nextRefId++;
|
|
112
|
+
hashCache.set(fn, valueHash);
|
|
113
|
+
}
|
|
114
|
+
return valueHash;
|
|
115
|
+
}
|
|
116
|
+
function keySort(a, b) {
|
|
117
|
+
return a.localeCompare(b);
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
hash
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=hash.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.js","sources":["../../../src/hashing/hash.ts"],"sourcesContent":["import { MurmurHashStream, randomHash } from \"./murmur.js\"\nimport type { Hasher } from \"./murmur.js\"\n\n/*\n * Implementation of structural hashing based on the Composites polyfill implementation:\n * https://github.com/tc39/proposal-composites\n */\n\nconst TRUE = randomHash()\nconst FALSE = randomHash()\nconst NULL = randomHash()\nconst UNDEFINED = randomHash()\nconst KEY = randomHash()\nconst FUNCTIONS = randomHash()\nconst DATE_MARKER = randomHash()\nconst OBJECT_MARKER = randomHash()\nconst ARRAY_MARKER = randomHash()\nconst MAP_MARKER = randomHash()\nconst SET_MARKER = randomHash()\n\nconst hashCache = new WeakMap<object, number>()\n\nexport function hash(input: any): number {\n const hasher = new MurmurHashStream()\n updateHasher(hasher, input)\n return hasher.digest()\n}\n\nfunction hashObject(input: object): number {\n const cachedHash = hashCache.get(input)\n if (cachedHash !== undefined) {\n return cachedHash\n }\n\n let valueHash: number | undefined\n if (input instanceof Date) {\n valueHash = hashDate(input)\n } else {\n let plainObjectInput = input\n let marker = OBJECT_MARKER\n\n if (input instanceof Array) {\n marker = ARRAY_MARKER\n }\n\n if (input instanceof Map) {\n marker = MAP_MARKER\n plainObjectInput = [...input.entries()]\n }\n\n if (input instanceof Set) {\n marker = SET_MARKER\n plainObjectInput = [...input.entries()]\n }\n\n if (\n input instanceof Buffer ||\n input instanceof Uint8Array ||\n input instanceof File\n ) {\n // Deeply hashing these objects would be too costly\n // but we also don't want to ignore them\n // so we track them by reference and cache them in a weak map\n return cachedReferenceHash(input)\n }\n\n valueHash = hashPlainObject(plainObjectInput, marker)\n }\n\n hashCache.set(input, valueHash)\n return valueHash\n}\n\nfunction hashDate(input: Date): number {\n const hasher = new MurmurHashStream()\n hasher.update(DATE_MARKER)\n hasher.update(input.getTime())\n return hasher.digest()\n}\n\nfunction hashPlainObject(input: object, marker: number): number {\n const hasher = new MurmurHashStream()\n\n // Mark the type of the input\n hasher.update(marker)\n const keys = Object.keys(input)\n keys.sort(keySort)\n for (const key of keys) {\n hasher.update(KEY)\n hasher.update(key)\n updateHasher(hasher, input[key as keyof typeof input])\n }\n\n return hasher.digest()\n}\n\nfunction updateHasher(hasher: Hasher, input: unknown): void {\n if (input === null) {\n hasher.update(NULL)\n return\n }\n switch (typeof input) {\n case `undefined`:\n hasher.update(UNDEFINED)\n return\n case `boolean`:\n hasher.update(input ? TRUE : FALSE)\n return\n case `number`:\n // Normalize NaNs and -0\n hasher.update(isNaN(input) ? NaN : input === 0 ? 0 : input)\n return\n case `bigint`:\n case `string`:\n case `symbol`:\n hasher.update(input)\n return\n case `object`:\n hasher.update(getCachedHash(input))\n return\n case `function`:\n // Functions are assigned a globally unique ID\n // and that ID is cached in the weak map\n hasher.update(cachedReferenceHash(input))\n return\n default:\n console.warn(\n `Ignored input during hashing because it is of type ${typeof input} which is not supported`\n )\n }\n}\n\nfunction getCachedHash(input: object): number {\n let valueHash = hashCache.get(input)\n if (valueHash === undefined) {\n valueHash = hashObject(input)\n }\n return valueHash\n}\n\nlet nextRefId = 1\nfunction cachedReferenceHash(fn: object): number {\n let valueHash = hashCache.get(fn)\n if (valueHash === undefined) {\n valueHash = nextRefId ^ FUNCTIONS\n nextRefId++\n hashCache.set(fn, valueHash)\n }\n return valueHash\n}\n\n/**\n * Strings sorted lexicographically.\n */\nfunction keySort(a: string, b: string): number {\n return a.localeCompare(b)\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type Hash = number;
|
|
2
|
+
export declare function randomHash(): number;
|
|
3
|
+
export interface Hasher {
|
|
4
|
+
update: (val: symbol | string | number | bigint) => void;
|
|
5
|
+
digest: () => number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* This implementation of Murmur hash uses a random initial seed and random markers.
|
|
9
|
+
* This means that hashes aren't deterministic across app restarts.
|
|
10
|
+
* This is intentional in the composites polyfill to be resistent to hash-flooding attacks
|
|
11
|
+
* where malicious users would precompute lots of different objects whose hashes collide with each other.
|
|
12
|
+
*
|
|
13
|
+
* Currently, for ts/db-ivm this is fine because we don't persist client state.
|
|
14
|
+
* However, when we will introduce persistence we will either need to store the seeds or remove the randomness
|
|
15
|
+
* to ensure deterministic hashes across app restarts.
|
|
16
|
+
*/
|
|
17
|
+
export declare class MurmurHashStream implements Hasher {
|
|
18
|
+
private hash;
|
|
19
|
+
private length;
|
|
20
|
+
private carry;
|
|
21
|
+
private carryBytes;
|
|
22
|
+
private _mix;
|
|
23
|
+
private _writeByte;
|
|
24
|
+
update(chunk: symbol | string | number | bigint): void;
|
|
25
|
+
digest(): number;
|
|
26
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const RANDOM_SEED = randomHash();
|
|
2
|
+
const STRING_MARKER = randomHash();
|
|
3
|
+
const BIG_INT_MARKER = randomHash();
|
|
4
|
+
const NEG_BIG_INT_MARKER = randomHash();
|
|
5
|
+
const SYMBOL_MARKER = randomHash();
|
|
6
|
+
function randomHash() {
|
|
7
|
+
return Math.random() * (2 ** 31 - 1) >>> 0;
|
|
8
|
+
}
|
|
9
|
+
class MurmurHashStream {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.hash = RANDOM_SEED;
|
|
12
|
+
this.length = 0;
|
|
13
|
+
this.carry = 0;
|
|
14
|
+
this.carryBytes = 0;
|
|
15
|
+
}
|
|
16
|
+
_mix(k1) {
|
|
17
|
+
k1 = Math.imul(k1, 3432918353);
|
|
18
|
+
k1 = k1 << 15 | k1 >>> 17;
|
|
19
|
+
k1 = Math.imul(k1, 461845907);
|
|
20
|
+
this.hash ^= k1;
|
|
21
|
+
this.hash = this.hash << 13 | this.hash >>> 19;
|
|
22
|
+
this.hash = Math.imul(this.hash, 5) + 3864292196;
|
|
23
|
+
}
|
|
24
|
+
_writeByte(byte) {
|
|
25
|
+
this.carry |= (byte & 255) << 8 * this.carryBytes;
|
|
26
|
+
this.carryBytes++;
|
|
27
|
+
this.length++;
|
|
28
|
+
if (this.carryBytes === 4) {
|
|
29
|
+
this._mix(this.carry >>> 0);
|
|
30
|
+
this.carry = 0;
|
|
31
|
+
this.carryBytes = 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
update(chunk) {
|
|
35
|
+
switch (typeof chunk) {
|
|
36
|
+
case `symbol`: {
|
|
37
|
+
this.update(SYMBOL_MARKER);
|
|
38
|
+
const description = chunk.description;
|
|
39
|
+
if (!description) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
for (let i = 0; i < description.length; i++) {
|
|
43
|
+
const code = description.charCodeAt(i);
|
|
44
|
+
this._writeByte(code & 255);
|
|
45
|
+
this._writeByte(code >>> 8 & 255);
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
case `string`:
|
|
50
|
+
this.update(STRING_MARKER);
|
|
51
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
52
|
+
const code = chunk.charCodeAt(i);
|
|
53
|
+
this._writeByte(code & 255);
|
|
54
|
+
this._writeByte(code >>> 8 & 255);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
case `number`:
|
|
58
|
+
this._writeByte(chunk & 255);
|
|
59
|
+
this._writeByte(chunk >>> 8 & 255);
|
|
60
|
+
this._writeByte(chunk >>> 16 & 255);
|
|
61
|
+
this._writeByte(chunk >>> 24 & 255);
|
|
62
|
+
return;
|
|
63
|
+
case `bigint`: {
|
|
64
|
+
let value = chunk;
|
|
65
|
+
if (value < 0n) {
|
|
66
|
+
value = -value;
|
|
67
|
+
this.update(NEG_BIG_INT_MARKER);
|
|
68
|
+
} else {
|
|
69
|
+
this.update(BIG_INT_MARKER);
|
|
70
|
+
}
|
|
71
|
+
while (value > 0n) {
|
|
72
|
+
this._writeByte(Number(value & 0xffn));
|
|
73
|
+
value >>= 8n;
|
|
74
|
+
}
|
|
75
|
+
if (chunk === 0n) this._writeByte(0);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
default:
|
|
79
|
+
throw new TypeError(`Unsupported input type: ${typeof chunk}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
digest() {
|
|
83
|
+
if (this.carryBytes > 0) {
|
|
84
|
+
let k1 = this.carry >>> 0;
|
|
85
|
+
k1 = Math.imul(k1, 3432918353);
|
|
86
|
+
k1 = k1 << 15 | k1 >>> 17;
|
|
87
|
+
k1 = Math.imul(k1, 461845907);
|
|
88
|
+
this.hash ^= k1;
|
|
89
|
+
}
|
|
90
|
+
this.hash ^= this.length;
|
|
91
|
+
this.hash ^= this.hash >>> 16;
|
|
92
|
+
this.hash = Math.imul(this.hash, 2246822507);
|
|
93
|
+
this.hash ^= this.hash >>> 13;
|
|
94
|
+
this.hash = Math.imul(this.hash, 3266489909);
|
|
95
|
+
this.hash ^= this.hash >>> 16;
|
|
96
|
+
return this.hash >>> 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
MurmurHashStream,
|
|
101
|
+
randomHash
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=murmur.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"murmur.js","sources":["../../../src/hashing/murmur.ts"],"sourcesContent":["/*\n * Implementation of murmur hash based on the Composites polyfill implementation:\n * https://github.com/tc39/proposal-composites\n */\n\nconst RANDOM_SEED = randomHash()\nconst STRING_MARKER = randomHash()\nconst BIG_INT_MARKER = randomHash()\nconst NEG_BIG_INT_MARKER = randomHash()\nconst SYMBOL_MARKER = randomHash()\n\nexport type Hash = number\n\nexport function randomHash() {\n return (Math.random() * (2 ** 31 - 1)) >>> 0\n}\n\nexport interface Hasher {\n update: (val: symbol | string | number | bigint) => void\n digest: () => number\n}\n\n/**\n * This implementation of Murmur hash uses a random initial seed and random markers.\n * This means that hashes aren't deterministic across app restarts.\n * This is intentional in the composites polyfill to be resistent to hash-flooding attacks\n * where malicious users would precompute lots of different objects whose hashes collide with each other.\n *\n * Currently, for ts/db-ivm this is fine because we don't persist client state.\n * However, when we will introduce persistence we will either need to store the seeds or remove the randomness\n * to ensure deterministic hashes across app restarts.\n */\nexport class MurmurHashStream implements Hasher {\n private hash: number = RANDOM_SEED\n private length = 0\n private carry = 0\n private carryBytes = 0\n\n private _mix(k1: number): void {\n k1 = Math.imul(k1, 0xcc9e2d51)\n k1 = (k1 << 15) | (k1 >>> 17)\n k1 = Math.imul(k1, 0x1b873593)\n this.hash ^= k1\n this.hash = (this.hash << 13) | (this.hash >>> 19)\n this.hash = Math.imul(this.hash, 5) + 0xe6546b64\n }\n\n private _writeByte(byte: number): void {\n this.carry |= (byte & 0xff) << (8 * this.carryBytes)\n this.carryBytes++\n this.length++\n\n if (this.carryBytes === 4) {\n this._mix(this.carry >>> 0)\n this.carry = 0\n this.carryBytes = 0\n }\n }\n\n update(chunk: symbol | string | number | bigint): void {\n switch (typeof chunk) {\n case `symbol`: {\n this.update(SYMBOL_MARKER)\n const description = chunk.description\n if (!description) {\n return\n }\n\n for (let i = 0; i < description.length; i++) {\n const code = description.charCodeAt(i)\n this._writeByte(code & 0xff)\n this._writeByte((code >>> 8) & 0xff)\n }\n return\n }\n case `string`:\n this.update(STRING_MARKER)\n for (let i = 0; i < chunk.length; i++) {\n const code = chunk.charCodeAt(i)\n this._writeByte(code & 0xff)\n this._writeByte((code >>> 8) & 0xff)\n }\n return\n case `number`:\n this._writeByte(chunk & 0xff)\n this._writeByte((chunk >>> 8) & 0xff)\n this._writeByte((chunk >>> 16) & 0xff)\n this._writeByte((chunk >>> 24) & 0xff)\n return\n case `bigint`: {\n let value = chunk\n if (value < 0n) {\n value = -value\n this.update(NEG_BIG_INT_MARKER)\n } else {\n this.update(BIG_INT_MARKER)\n }\n while (value > 0n) {\n this._writeByte(Number(value & 0xffn))\n value >>= 8n\n }\n if (chunk === 0n) this._writeByte(0)\n return\n }\n default:\n throw new TypeError(`Unsupported input type: ${typeof chunk}`)\n }\n }\n\n digest(): number {\n if (this.carryBytes > 0) {\n let k1 = this.carry >>> 0\n k1 = Math.imul(k1, 0xcc9e2d51)\n k1 = (k1 << 15) | (k1 >>> 17)\n k1 = Math.imul(k1, 0x1b873593)\n this.hash ^= k1\n }\n\n this.hash ^= this.length\n this.hash ^= this.hash >>> 16\n this.hash = Math.imul(this.hash, 0x85ebca6b)\n this.hash ^= this.hash >>> 13\n this.hash = Math.imul(this.hash, 0xc2b2ae35)\n this.hash ^= this.hash >>> 16\n\n return this.hash >>> 0\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/esm/indexes.d.ts
CHANGED
|
@@ -10,8 +10,6 @@ export declare class Index<K, V> {
|
|
|
10
10
|
toString(indent?: boolean): string;
|
|
11
11
|
get(key: K): Array<[V, number]>;
|
|
12
12
|
getMultiplicity(key: K, value: V): number;
|
|
13
|
-
entries(): MapIterator<[K, Map<V, number>]>;
|
|
14
|
-
keys(): MapIterator<K>;
|
|
15
13
|
has(key: K): boolean;
|
|
16
14
|
get size(): number;
|
|
17
15
|
addValue(key: K, value: [V, number]): void;
|
package/dist/esm/indexes.js
CHANGED
|
@@ -5,75 +5,101 @@ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot
|
|
|
5
5
|
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
6
6
|
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
7
7
|
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
8
|
-
var
|
|
8
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
9
|
+
var _valueIndex, _hashIndex, _Index_instances, entries_fn, entriesIterators_fn;
|
|
9
10
|
import { MultiSet } from "./multiset.js";
|
|
10
|
-
import {
|
|
11
|
+
import { HashIndex } from "./hashIndex.js";
|
|
12
|
+
import { ValueIndex } from "./valueIndex.js";
|
|
13
|
+
import { concatIterable, mapIterable } from "./utils.js";
|
|
11
14
|
class Index {
|
|
12
15
|
constructor() {
|
|
13
|
-
__privateAdd(this,
|
|
14
|
-
|
|
16
|
+
__privateAdd(this, _Index_instances);
|
|
17
|
+
/*
|
|
18
|
+
* This is a hybrid Index that composes a ValueIndex and a HashIndex.
|
|
19
|
+
* Keys that have only one value are stored in the ValueIndex.
|
|
20
|
+
* Keys that have multiple values are stored in the HashIndex, the hash distinguishes between the values.
|
|
21
|
+
* This reduces the amount of hashes we need to compute since often times only a small portion of the keys are updated
|
|
22
|
+
* so we don't have to hash the keys that are never updated.
|
|
23
|
+
*
|
|
24
|
+
* Note: The `valueIndex` and `hashIndex` have disjoint keys.
|
|
25
|
+
* When a key that has only one value gets a new distinct value,
|
|
26
|
+
* it is added to the `hashIndex` and removed from the `valueIndex` and vice versa.
|
|
27
|
+
*/
|
|
28
|
+
__privateAdd(this, _valueIndex);
|
|
29
|
+
__privateAdd(this, _hashIndex);
|
|
30
|
+
__privateSet(this, _valueIndex, new ValueIndex());
|
|
31
|
+
__privateSet(this, _hashIndex, new HashIndex());
|
|
15
32
|
}
|
|
16
33
|
toString(indent = false) {
|
|
17
|
-
return `Index(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
)})`;
|
|
34
|
+
return `Index(
|
|
35
|
+
${__privateGet(this, _valueIndex).toString(indent)},
|
|
36
|
+
${__privateGet(this, _hashIndex).toString(indent)}
|
|
37
|
+
)`;
|
|
22
38
|
}
|
|
23
39
|
get(key) {
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
if (__privateGet(this, _valueIndex).has(key)) {
|
|
41
|
+
return [__privateGet(this, _valueIndex).get(key)];
|
|
42
|
+
}
|
|
43
|
+
return __privateGet(this, _hashIndex).get(key);
|
|
26
44
|
}
|
|
27
45
|
getMultiplicity(key, value) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return __privateGet(this, _inner).entries();
|
|
33
|
-
}
|
|
34
|
-
keys() {
|
|
35
|
-
return __privateGet(this, _inner).keys();
|
|
46
|
+
if (__privateGet(this, _valueIndex).has(key)) {
|
|
47
|
+
return __privateGet(this, _valueIndex).getMultiplicity(key);
|
|
48
|
+
}
|
|
49
|
+
return __privateGet(this, _hashIndex).getMultiplicity(key, value);
|
|
36
50
|
}
|
|
37
51
|
has(key) {
|
|
38
|
-
return __privateGet(this,
|
|
52
|
+
return __privateGet(this, _valueIndex).has(key) || __privateGet(this, _hashIndex).has(key);
|
|
39
53
|
}
|
|
40
54
|
get size() {
|
|
41
|
-
return __privateGet(this,
|
|
55
|
+
return __privateGet(this, _valueIndex).size + __privateGet(this, _hashIndex).size;
|
|
42
56
|
}
|
|
43
57
|
addValue(key, value) {
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
const containedInValueIndex = __privateGet(this, _valueIndex).has(key);
|
|
59
|
+
const containedInHashIndex = __privateGet(this, _hashIndex).has(key);
|
|
60
|
+
if (containedInHashIndex && containedInValueIndex) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Key ${key} is contained in both the value index and the hash index. This should never happen because they should have disjoint keysets.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (!containedInValueIndex && !containedInHashIndex) {
|
|
66
|
+
__privateGet(this, _valueIndex).addValue(key, value);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (containedInValueIndex) {
|
|
70
|
+
try {
|
|
71
|
+
__privateGet(this, _valueIndex).addValue(key, value);
|
|
72
|
+
} catch {
|
|
73
|
+
const existingValue = __privateGet(this, _valueIndex).get(key);
|
|
74
|
+
__privateGet(this, _valueIndex).delete(key);
|
|
75
|
+
__privateGet(this, _hashIndex).addValue(key, existingValue);
|
|
76
|
+
__privateGet(this, _hashIndex).addValue(key, value);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (containedInHashIndex) {
|
|
81
|
+
const singleRemainingValue = __privateGet(this, _hashIndex).addValue(key, value);
|
|
82
|
+
if (singleRemainingValue) {
|
|
83
|
+
__privateGet(this, _hashIndex).delete(key);
|
|
84
|
+
__privateGet(this, _valueIndex).addValue(key, singleRemainingValue);
|
|
53
85
|
}
|
|
86
|
+
return;
|
|
54
87
|
}
|
|
55
88
|
}
|
|
56
89
|
append(other) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const existingMultiplicity = thisValueMap.get(value) ?? 0;
|
|
61
|
-
const newMultiplicity = existingMultiplicity + multiplicity;
|
|
62
|
-
if (newMultiplicity === 0) {
|
|
63
|
-
thisValueMap.delete(value);
|
|
64
|
-
} else {
|
|
65
|
-
thisValueMap.set(value, newMultiplicity);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
90
|
+
var _a;
|
|
91
|
+
for (const [key, value] of __privateMethod(_a = other, _Index_instances, entries_fn).call(_a)) {
|
|
92
|
+
this.addValue(key, value);
|
|
68
93
|
}
|
|
69
94
|
}
|
|
70
95
|
join(other) {
|
|
96
|
+
var _a;
|
|
71
97
|
const result = [];
|
|
72
98
|
if (this.size <= other.size) {
|
|
73
|
-
for (const [key,
|
|
99
|
+
for (const [key, valueIt] of __privateMethod(this, _Index_instances, entriesIterators_fn).call(this)) {
|
|
74
100
|
if (!other.has(key)) continue;
|
|
75
101
|
const otherValues = other.get(key);
|
|
76
|
-
for (const [val1, mul1] of
|
|
102
|
+
for (const [val1, mul1] of valueIt) {
|
|
77
103
|
for (const [val2, mul2] of otherValues) {
|
|
78
104
|
if (mul1 !== 0 && mul2 !== 0) {
|
|
79
105
|
result.push([[key, [val1, val2]], mul1 * mul2]);
|
|
@@ -82,10 +108,10 @@ class Index {
|
|
|
82
108
|
}
|
|
83
109
|
}
|
|
84
110
|
} else {
|
|
85
|
-
for (const [key,
|
|
111
|
+
for (const [key, otherValueIt] of __privateMethod(_a = other, _Index_instances, entriesIterators_fn).call(_a)) {
|
|
86
112
|
if (!this.has(key)) continue;
|
|
87
113
|
const values = this.get(key);
|
|
88
|
-
for (const [val2, mul2] of
|
|
114
|
+
for (const [val2, mul2] of otherValueIt) {
|
|
89
115
|
for (const [val1, mul1] of values) {
|
|
90
116
|
if (mul1 !== 0 && mul2 !== 0) {
|
|
91
117
|
result.push([[key, [val1, val2]], mul1 * mul2]);
|
|
@@ -97,7 +123,33 @@ class Index {
|
|
|
97
123
|
return new MultiSet(result);
|
|
98
124
|
}
|
|
99
125
|
}
|
|
100
|
-
|
|
126
|
+
_valueIndex = new WeakMap();
|
|
127
|
+
_hashIndex = new WeakMap();
|
|
128
|
+
_Index_instances = new WeakSet();
|
|
129
|
+
/**
|
|
130
|
+
* This returns an iterator that iterates over all key-value pairs.
|
|
131
|
+
* @returns An iterable of all key-value pairs (and their multiplicities) in the index.
|
|
132
|
+
*/
|
|
133
|
+
entries_fn = function() {
|
|
134
|
+
return concatIterable(
|
|
135
|
+
__privateGet(this, _valueIndex).entries(),
|
|
136
|
+
__privateGet(this, _hashIndex).entriesIterator()
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
entriesIterators_fn = function* () {
|
|
140
|
+
for (const [key, [value, multiplicity]] of __privateGet(this, _valueIndex).entries()) {
|
|
141
|
+
yield [key, /* @__PURE__ */ new Map([[value, multiplicity]])];
|
|
142
|
+
}
|
|
143
|
+
for (const [key, valueMap] of __privateGet(this, _hashIndex).entries()) {
|
|
144
|
+
yield [
|
|
145
|
+
key,
|
|
146
|
+
mapIterable(valueMap, ([_hash, [value, multiplicity]]) => [
|
|
147
|
+
value,
|
|
148
|
+
multiplicity
|
|
149
|
+
])
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
};
|
|
101
153
|
export {
|
|
102
154
|
Index
|
|
103
155
|
};
|
package/dist/esm/indexes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexes.js","sources":["../../src/indexes.ts"],"sourcesContent":["import { MultiSet } from \"./multiset.js\"\nimport {
|
|
1
|
+
{"version":3,"file":"indexes.js","sources":["../../src/indexes.ts"],"sourcesContent":["import { MultiSet } from \"./multiset.js\"\nimport { HashIndex } from \"./hashIndex.js\"\nimport { ValueIndex } from \"./valueIndex.js\"\nimport { concatIterable, mapIterable } from \"./utils.js\"\n\n/**\n * A map from a difference collection trace's keys -> (value, multiplicities) that changed.\n * Used in operations like join and reduce where the operation needs to\n * exploit the key-value structure of the data to run efficiently.\n */\nexport class Index<K, V> {\n /*\n * This is a hybrid Index that composes a ValueIndex and a HashIndex.\n * Keys that have only one value are stored in the ValueIndex.\n * Keys that have multiple values are stored in the HashIndex, the hash distinguishes between the values.\n * This reduces the amount of hashes we need to compute since often times only a small portion of the keys are updated\n * so we don't have to hash the keys that are never updated.\n *\n * Note: The `valueIndex` and `hashIndex` have disjoint keys.\n * When a key that has only one value gets a new distinct value,\n * it is added to the `hashIndex` and removed from the `valueIndex` and vice versa.\n */\n #valueIndex: ValueIndex<K, V>\n #hashIndex: HashIndex<K, V>\n\n constructor() {\n this.#valueIndex = new ValueIndex<K, V>()\n this.#hashIndex = new HashIndex<K, V>()\n }\n\n toString(indent = false): string {\n return `Index(\\n ${this.#valueIndex.toString(indent)},\\n ${this.#hashIndex.toString(indent)}\\n)`\n }\n\n get(key: K): Array<[V, number]> {\n if (this.#valueIndex.has(key)) {\n return [this.#valueIndex.get(key)!]\n }\n return this.#hashIndex.get(key)\n }\n\n getMultiplicity(key: K, value: V): number {\n if (this.#valueIndex.has(key)) {\n return this.#valueIndex.getMultiplicity(key)\n }\n return this.#hashIndex.getMultiplicity(key, value)\n }\n\n /**\n * This returns an iterator that iterates over all key-value pairs.\n * @returns An iterable of all key-value pairs (and their multiplicities) in the index.\n */\n #entries(): Iterable<[K, [V, number]]> {\n return concatIterable(\n this.#valueIndex.entries(),\n this.#hashIndex.entriesIterator()\n )\n }\n\n /**\n * This method only iterates over the keys and not over the values.\n * Hence, it is more efficient than the `#entries` method.\n * It returns an iterator that you can use if you need to iterate over the values for a given key.\n * @returns An iterator of all *keys* in the index and their corresponding value iterator.\n */\n *#entriesIterators(): Iterable<[K, Iterable<[V, number]>]> {\n for (const [key, [value, multiplicity]] of this.#valueIndex.entries()) {\n yield [key, new Map<V, number>([[value, multiplicity]])]\n }\n for (const [key, valueMap] of this.#hashIndex.entries()) {\n yield [\n key,\n mapIterable(valueMap, ([_hash, [value, multiplicity]]) => [\n value,\n multiplicity,\n ]),\n ]\n }\n }\n\n has(key: K): boolean {\n return this.#valueIndex.has(key) || this.#hashIndex.has(key)\n }\n\n get size(): number {\n return this.#valueIndex.size + this.#hashIndex.size\n }\n\n addValue(key: K, value: [V, number]): void {\n const containedInValueIndex = this.#valueIndex.has(key)\n const containedInHashIndex = this.#hashIndex.has(key)\n\n if (containedInHashIndex && containedInValueIndex) {\n throw new Error(\n `Key ${key} is contained in both the value index and the hash index. This should never happen because they should have disjoint keysets.`\n )\n }\n\n if (!containedInValueIndex && !containedInHashIndex) {\n // This is the first time we see the key\n // Add it to the value index\n this.#valueIndex.addValue(key, value)\n return\n }\n\n if (containedInValueIndex) {\n // This key is already in the value index\n // It could be that it's the same value or a different one\n // If it's a different value we will need to remove the key from the value index\n // and add the key and its two values to the hash index\n try {\n this.#valueIndex.addValue(key, value)\n } catch {\n // This is a different value, need to move the key to the hash index\n const existingValue = this.#valueIndex.get(key)!\n this.#valueIndex.delete(key)\n this.#hashIndex.addValue(key, existingValue)\n this.#hashIndex.addValue(key, value)\n }\n return\n }\n\n if (containedInHashIndex) {\n // This key is already in the hash index so it already has two or more values.\n // However, this new value and multiplicity could cause an existing value to be removed\n // and lead to the key having only a single value in which case we need to move it back to the value index\n const singleRemainingValue = this.#hashIndex.addValue(key, value)\n if (singleRemainingValue) {\n // The key only has a single remaining value so we need to move it back to the value index\n this.#hashIndex.delete(key)\n this.#valueIndex.addValue(key, singleRemainingValue)\n }\n return\n }\n }\n\n append(other: Index<K, V>): void {\n for (const [key, value] of other.#entries()) {\n this.addValue(key, value)\n }\n }\n\n join<V2>(other: Index<K, V2>): MultiSet<[K, [V, V2]]> {\n const result: Array<[[K, [V, V2]], number]> = []\n\n // We want to iterate over the smaller of the two indexes to reduce the\n // number of operations we need to do.\n if (this.size <= other.size) {\n for (const [key, valueIt] of this.#entriesIterators()) {\n if (!other.has(key)) continue\n const otherValues = other.get(key)\n for (const [val1, mul1] of valueIt) {\n for (const [val2, mul2] of otherValues) {\n if (mul1 !== 0 && mul2 !== 0) {\n result.push([[key, [val1, val2]], mul1 * mul2])\n }\n }\n }\n }\n } else {\n for (const [key, otherValueIt] of other.#entriesIterators()) {\n if (!this.has(key)) continue\n const values = this.get(key)\n for (const [val2, mul2] of otherValueIt) {\n for (const [val1, mul1] of values) {\n if (mul1 !== 0 && mul2 !== 0) {\n result.push([[key, [val1, val2]], mul1 * mul2])\n }\n }\n }\n }\n }\n\n return new MultiSet(result)\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/esm/multiset.js
CHANGED
|
@@ -7,7 +7,8 @@ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot
|
|
|
7
7
|
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
8
8
|
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
9
9
|
var _inner, _MultiSet_instances, consolidateKeyed_fn, consolidateUnkeyed_fn;
|
|
10
|
-
import { chunkedArrayPush, globalObjectIdGenerator, DefaultMap
|
|
10
|
+
import { chunkedArrayPush, globalObjectIdGenerator, DefaultMap } from "./utils.js";
|
|
11
|
+
import { hash } from "./hashing/hash.js";
|
|
11
12
|
const _MultiSet = class _MultiSet {
|
|
12
13
|
constructor(data = []) {
|
|
13
14
|
__privateAdd(this, _MultiSet_instances);
|
package/dist/esm/multiset.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multiset.js","sources":["../../src/multiset.ts"],"sourcesContent":["import {\n DefaultMap,\n chunkedArrayPush,\n globalObjectIdGenerator,\n hash,\n} from \"./utils.js\"\n\nexport type MultiSetArray<T> = Array<[T, number]>\nexport type KeyedData<T> = [key: string, value: T]\n\n/**\n * A multiset of data.\n */\nexport class MultiSet<T> {\n #inner: MultiSetArray<T>\n\n constructor(data: MultiSetArray<T> = []) {\n this.#inner = data\n }\n\n toString(indent = false): string {\n return `MultiSet(${JSON.stringify(this.#inner, null, indent ? 2 : undefined)})`\n }\n\n toJSON(): string {\n return JSON.stringify(Array.from(this.getInner()))\n }\n\n static fromJSON<U>(json: string): MultiSet<U> {\n return new MultiSet(JSON.parse(json))\n }\n\n /**\n * Apply a function to all records in the collection.\n */\n map<U>(f: (data: T) => U): MultiSet<U> {\n return new MultiSet(\n this.#inner.map(([data, multiplicity]) => [f(data), multiplicity])\n )\n }\n\n /**\n * Filter out records for which a function f(record) evaluates to False.\n */\n filter(f: (data: T) => boolean): MultiSet<T> {\n return new MultiSet(this.#inner.filter(([data, _]) => f(data)))\n }\n\n /**\n * Negate all multiplicities in the collection.\n */\n negate(): MultiSet<T> {\n return new MultiSet(\n this.#inner.map(([data, multiplicity]) => [data, -multiplicity])\n )\n }\n\n /**\n * Concatenate two collections together.\n */\n concat(other: MultiSet<T>): MultiSet<T> {\n const out: MultiSetArray<T> = []\n chunkedArrayPush(out, this.#inner)\n chunkedArrayPush(out, other.getInner())\n return new MultiSet(out)\n }\n\n /**\n * Produce as output a collection that is logically equivalent to the input\n * but which combines identical instances of the same record into one\n * (record, multiplicity) pair.\n */\n consolidate(): MultiSet<T> {\n // Check if this looks like a keyed multiset (first item is a tuple of length 2)\n if (this.#inner.length > 0) {\n const firstItem = this.#inner[0]?.[0]\n if (Array.isArray(firstItem) && firstItem.length === 2) {\n return this.#consolidateKeyed()\n }\n }\n\n // Fall back to original method for unkeyed data\n return this.#consolidateUnkeyed()\n }\n\n /**\n * Private method for consolidating keyed multisets where keys are strings/numbers\n * and values are compared by reference equality.\n *\n * This method provides significant performance improvements over the hash-based approach\n * by using WeakMap for object reference tracking and avoiding expensive serialization.\n *\n * Special handling for join operations: When values are tuples of length 2 (common in joins),\n * we unpack them and compare each element individually to maintain proper equality semantics.\n */\n #consolidateKeyed(): MultiSet<T> {\n const consolidated = new Map<string, number>()\n const values = new Map<string, T>()\n\n // Use global object ID generator for consistent reference equality\n\n /**\n * Special handler for tuples (arrays of length 2) commonly produced by join operations.\n * Unpacks the tuple and generates an ID based on both elements to ensure proper\n * consolidation of join results like ['A', null] and [null, 'X'].\n */\n const getTupleId = (tuple: Array<any>): string => {\n if (tuple.length !== 2) {\n throw new Error(`Expected tuple of length 2`)\n }\n const [first, second] = tuple\n return `${globalObjectIdGenerator.getStringId(first)}|${globalObjectIdGenerator.getStringId(second)}`\n }\n\n // Process each item in the multiset\n for (const [data, multiplicity] of this.#inner) {\n // Verify this is still a keyed item (should be [key, value] pair)\n if (!Array.isArray(data) || data.length !== 2) {\n // Found non-keyed item, fall back to unkeyed consolidation\n return this.#consolidateUnkeyed()\n }\n\n const [key, value] = data\n\n // Verify key is string or number as expected for keyed multisets\n if (typeof key !== `string` && typeof key !== `number`) {\n // Found non-string/number key, fall back to unkeyed consolidation\n return this.#consolidateUnkeyed()\n }\n\n // Generate value ID with special handling for join tuples\n let valueId: string\n if (Array.isArray(value) && value.length === 2) {\n // Special case: value is a tuple from join operations\n valueId = getTupleId(value)\n } else {\n // Regular case: use reference/value equality\n valueId = globalObjectIdGenerator.getStringId(value)\n }\n\n // Create composite key and consolidate\n const compositeKey = key + `|` + valueId\n consolidated.set(\n compositeKey,\n (consolidated.get(compositeKey) || 0) + multiplicity\n )\n\n // Store the original data for the first occurrence\n if (!values.has(compositeKey)) {\n values.set(compositeKey, data as T)\n }\n }\n\n // Build result array, filtering out zero multiplicities\n const result: MultiSetArray<T> = []\n for (const [compositeKey, multiplicity] of consolidated) {\n if (multiplicity !== 0) {\n result.push([values.get(compositeKey)!, multiplicity])\n }\n }\n\n return new MultiSet(result)\n }\n\n /**\n * Private method for consolidating unkeyed multisets using the original approach.\n */\n #consolidateUnkeyed(): MultiSet<T> {\n const consolidated = new DefaultMap<string | number, number>(() => 0)\n const values = new Map<string, any>()\n\n let hasString = false\n let hasNumber = false\n let hasOther = false\n for (const [data, _] of this.#inner) {\n if (typeof data === `string`) {\n hasString = true\n } else if (typeof data === `number`) {\n hasNumber = true\n } else {\n hasOther = true\n break\n }\n }\n\n const requireJson = hasOther || (hasString && hasNumber)\n\n for (const [data, multiplicity] of this.#inner) {\n const key = requireJson ? hash(data) : (data as string | number)\n if (requireJson && !values.has(key as string)) {\n values.set(key as string, data)\n }\n consolidated.update(key, (count) => count + multiplicity)\n }\n\n const result: MultiSetArray<T> = []\n for (const [key, multiplicity] of consolidated.entries()) {\n if (multiplicity !== 0) {\n const parsedKey = requireJson ? values.get(key as string) : key\n result.push([parsedKey as T, multiplicity])\n }\n }\n\n return new MultiSet(result)\n }\n\n extend(other: MultiSet<T> | MultiSetArray<T>): void {\n const otherArray = other instanceof MultiSet ? other.getInner() : other\n chunkedArrayPush(this.#inner, otherArray)\n }\n\n getInner(): MultiSetArray<T> {\n return this.#inner\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"multiset.js","sources":["../../src/multiset.ts"],"sourcesContent":["import {\n DefaultMap,\n chunkedArrayPush,\n globalObjectIdGenerator,\n} from \"./utils.js\"\nimport { hash } from \"./hashing/index.js\"\n\nexport type MultiSetArray<T> = Array<[T, number]>\nexport type KeyedData<T> = [key: string, value: T]\n\n/**\n * A multiset of data.\n */\nexport class MultiSet<T> {\n #inner: MultiSetArray<T>\n\n constructor(data: MultiSetArray<T> = []) {\n this.#inner = data\n }\n\n toString(indent = false): string {\n return `MultiSet(${JSON.stringify(this.#inner, null, indent ? 2 : undefined)})`\n }\n\n toJSON(): string {\n return JSON.stringify(Array.from(this.getInner()))\n }\n\n static fromJSON<U>(json: string): MultiSet<U> {\n return new MultiSet(JSON.parse(json))\n }\n\n /**\n * Apply a function to all records in the collection.\n */\n map<U>(f: (data: T) => U): MultiSet<U> {\n return new MultiSet(\n this.#inner.map(([data, multiplicity]) => [f(data), multiplicity])\n )\n }\n\n /**\n * Filter out records for which a function f(record) evaluates to False.\n */\n filter(f: (data: T) => boolean): MultiSet<T> {\n return new MultiSet(this.#inner.filter(([data, _]) => f(data)))\n }\n\n /**\n * Negate all multiplicities in the collection.\n */\n negate(): MultiSet<T> {\n return new MultiSet(\n this.#inner.map(([data, multiplicity]) => [data, -multiplicity])\n )\n }\n\n /**\n * Concatenate two collections together.\n */\n concat(other: MultiSet<T>): MultiSet<T> {\n const out: MultiSetArray<T> = []\n chunkedArrayPush(out, this.#inner)\n chunkedArrayPush(out, other.getInner())\n return new MultiSet(out)\n }\n\n /**\n * Produce as output a collection that is logically equivalent to the input\n * but which combines identical instances of the same record into one\n * (record, multiplicity) pair.\n */\n consolidate(): MultiSet<T> {\n // Check if this looks like a keyed multiset (first item is a tuple of length 2)\n if (this.#inner.length > 0) {\n const firstItem = this.#inner[0]?.[0]\n if (Array.isArray(firstItem) && firstItem.length === 2) {\n return this.#consolidateKeyed()\n }\n }\n\n // Fall back to original method for unkeyed data\n return this.#consolidateUnkeyed()\n }\n\n /**\n * Private method for consolidating keyed multisets where keys are strings/numbers\n * and values are compared by reference equality.\n *\n * This method provides significant performance improvements over the hash-based approach\n * by using WeakMap for object reference tracking and avoiding expensive serialization.\n *\n * Special handling for join operations: When values are tuples of length 2 (common in joins),\n * we unpack them and compare each element individually to maintain proper equality semantics.\n */\n #consolidateKeyed(): MultiSet<T> {\n const consolidated = new Map<string, number>()\n const values = new Map<string, T>()\n\n // Use global object ID generator for consistent reference equality\n\n /**\n * Special handler for tuples (arrays of length 2) commonly produced by join operations.\n * Unpacks the tuple and generates an ID based on both elements to ensure proper\n * consolidation of join results like ['A', null] and [null, 'X'].\n */\n const getTupleId = (tuple: Array<any>): string => {\n if (tuple.length !== 2) {\n throw new Error(`Expected tuple of length 2`)\n }\n const [first, second] = tuple\n return `${globalObjectIdGenerator.getStringId(first)}|${globalObjectIdGenerator.getStringId(second)}`\n }\n\n // Process each item in the multiset\n for (const [data, multiplicity] of this.#inner) {\n // Verify this is still a keyed item (should be [key, value] pair)\n if (!Array.isArray(data) || data.length !== 2) {\n // Found non-keyed item, fall back to unkeyed consolidation\n return this.#consolidateUnkeyed()\n }\n\n const [key, value] = data\n\n // Verify key is string or number as expected for keyed multisets\n if (typeof key !== `string` && typeof key !== `number`) {\n // Found non-string/number key, fall back to unkeyed consolidation\n return this.#consolidateUnkeyed()\n }\n\n // Generate value ID with special handling for join tuples\n let valueId: string\n if (Array.isArray(value) && value.length === 2) {\n // Special case: value is a tuple from join operations\n valueId = getTupleId(value)\n } else {\n // Regular case: use reference/value equality\n valueId = globalObjectIdGenerator.getStringId(value)\n }\n\n // Create composite key and consolidate\n const compositeKey = key + `|` + valueId\n consolidated.set(\n compositeKey,\n (consolidated.get(compositeKey) || 0) + multiplicity\n )\n\n // Store the original data for the first occurrence\n if (!values.has(compositeKey)) {\n values.set(compositeKey, data as T)\n }\n }\n\n // Build result array, filtering out zero multiplicities\n const result: MultiSetArray<T> = []\n for (const [compositeKey, multiplicity] of consolidated) {\n if (multiplicity !== 0) {\n result.push([values.get(compositeKey)!, multiplicity])\n }\n }\n\n return new MultiSet(result)\n }\n\n /**\n * Private method for consolidating unkeyed multisets using the original approach.\n */\n #consolidateUnkeyed(): MultiSet<T> {\n const consolidated = new DefaultMap<string | number, number>(() => 0)\n const values = new Map<string, any>()\n\n let hasString = false\n let hasNumber = false\n let hasOther = false\n for (const [data, _] of this.#inner) {\n if (typeof data === `string`) {\n hasString = true\n } else if (typeof data === `number`) {\n hasNumber = true\n } else {\n hasOther = true\n break\n }\n }\n\n const requireJson = hasOther || (hasString && hasNumber)\n\n for (const [data, multiplicity] of this.#inner) {\n const key = requireJson ? hash(data) : (data as string | number)\n if (requireJson && !values.has(key as string)) {\n values.set(key as string, data)\n }\n consolidated.update(key, (count) => count + multiplicity)\n }\n\n const result: MultiSetArray<T> = []\n for (const [key, multiplicity] of consolidated.entries()) {\n if (multiplicity !== 0) {\n const parsedKey = requireJson ? values.get(key as string) : key\n result.push([parsedKey as T, multiplicity])\n }\n }\n\n return new MultiSet(result)\n }\n\n extend(other: MultiSet<T> | MultiSetArray<T>): void {\n const otherArray = other instanceof MultiSet ? other.getInner() : other\n chunkedArrayPush(this.#inner, otherArray)\n }\n\n getInner(): MultiSetArray<T> {\n return this.#inner\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -8,8 +8,8 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
|
|
|
8
8
|
var _by, _values;
|
|
9
9
|
import { UnaryOperator, DifferenceStreamWriter } from "../graph.js";
|
|
10
10
|
import { StreamBuilder } from "../d2.js";
|
|
11
|
-
import { hash } from "../utils.js";
|
|
12
11
|
import { MultiSet } from "../multiset.js";
|
|
12
|
+
import { hash } from "../hashing/hash.js";
|
|
13
13
|
class DistinctOperator extends UnaryOperator {
|
|
14
14
|
// keeps track of the number of times each value has been seen
|
|
15
15
|
constructor(id, input, output, by = (value) => value) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"distinct.js","sources":["../../../src/operators/distinct.ts"],"sourcesContent":["import { DifferenceStreamWriter, UnaryOperator } from \"../graph.js\"\nimport { StreamBuilder } from \"../d2.js\"\nimport { hash } from \"../
|
|
1
|
+
{"version":3,"file":"distinct.js","sources":["../../../src/operators/distinct.ts"],"sourcesContent":["import { DifferenceStreamWriter, UnaryOperator } from \"../graph.js\"\nimport { StreamBuilder } from \"../d2.js\"\nimport { hash } from \"../hashing/index.js\"\nimport { MultiSet } from \"../multiset.js\"\nimport type { Hash } from \"../hashing/index.js\"\nimport type { DifferenceStreamReader } from \"../graph.js\"\nimport type { IStreamBuilder } from \"../types.js\"\n\ntype Multiplicity = number\n\n/**\n * Operator that removes duplicates\n */\nexport class DistinctOperator<T> extends UnaryOperator<T> {\n #by: (value: T) => any\n #values: Map<Hash, Multiplicity> // keeps track of the number of times each value has been seen\n\n constructor(\n id: number,\n input: DifferenceStreamReader<T>,\n output: DifferenceStreamWriter<T>,\n by: (value: T) => any = (value: T) => value\n ) {\n super(id, input, output)\n this.#by = by\n this.#values = new Map()\n }\n\n run(): void {\n const updatedValues = new Map<Hash, [Multiplicity, T]>()\n\n // Compute the new multiplicity for each value\n for (const message of this.inputMessages()) {\n for (const [value, diff] of message.getInner()) {\n const hashedValue = hash(this.#by(value))\n\n const oldMultiplicity =\n updatedValues.get(hashedValue)?.[0] ??\n this.#values.get(hashedValue) ??\n 0\n const newMultiplicity = oldMultiplicity + diff\n\n updatedValues.set(hashedValue, [newMultiplicity, value])\n }\n }\n\n const result: Array<[T, number]> = []\n\n // Check which values became visible or disappeared\n for (const [\n hashedValue,\n [newMultiplicity, value],\n ] of updatedValues.entries()) {\n const oldMultiplicity = this.#values.get(hashedValue) ?? 0\n\n if (newMultiplicity === 0) {\n this.#values.delete(hashedValue)\n } else {\n this.#values.set(hashedValue, newMultiplicity)\n }\n\n if (oldMultiplicity <= 0 && newMultiplicity > 0) {\n // The value wasn't present in the stream\n // but with this change it is now present in the stream\n result.push([value, 1])\n } else if (oldMultiplicity > 0 && newMultiplicity <= 0) {\n // The value was present in the stream\n // but with this change it is no longer present in the stream\n result.push([value, -1])\n }\n }\n\n if (result.length > 0) {\n this.output.sendData(new MultiSet(result))\n }\n }\n}\n\n/**\n * Removes duplicate values\n */\nexport function distinct<T>(by: (value: T) => any = (value: T) => value) {\n return (stream: IStreamBuilder<T>): IStreamBuilder<T> => {\n const output = new StreamBuilder<T>(\n stream.graph,\n new DifferenceStreamWriter<T>()\n )\n const operator = new DistinctOperator<T>(\n stream.graph.getNextOperatorId(),\n stream.connectReader(),\n output.writer,\n by\n )\n stream.graph.addOperator(operator)\n stream.graph.addStream(output.connectReader())\n return output\n }\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAaO,MAAM,yBAA4B,cAAiB;AAAA;AAAA,EAIxD,YACE,IACA,OACA,QACA,KAAwB,CAAC,UAAa,OACtC;AACA,UAAM,IAAI,OAAO,MAAM;AATzB;AACA;AASE,uBAAK,KAAM;AACX,uBAAK,6BAAc,IAAA;AAAA,EACrB;AAAA,EAEA,MAAY;;AACV,UAAM,oCAAoB,IAAA;AAG1B,eAAW,WAAW,KAAK,iBAAiB;AAC1C,iBAAW,CAAC,OAAO,IAAI,KAAK,QAAQ,YAAY;AAC9C,cAAM,cAAc,KAAK,mBAAK,KAAL,WAAS,MAAM;AAExC,cAAM,oBACJ,mBAAc,IAAI,WAAW,MAA7B,mBAAiC,OACjC,mBAAK,SAAQ,IAAI,WAAW,KAC5B;AACF,cAAM,kBAAkB,kBAAkB;AAE1C,sBAAc,IAAI,aAAa,CAAC,iBAAiB,KAAK,CAAC;AAAA,MACzD;AAAA,IACF;AAEA,UAAM,SAA6B,CAAA;AAGnC,eAAW;AAAA,MACT;AAAA,MACA,CAAC,iBAAiB,KAAK;AAAA,IAAA,KACpB,cAAc,WAAW;AAC5B,YAAM,kBAAkB,mBAAK,SAAQ,IAAI,WAAW,KAAK;AAEzD,UAAI,oBAAoB,GAAG;AACzB,2BAAK,SAAQ,OAAO,WAAW;AAAA,MACjC,OAAO;AACL,2BAAK,SAAQ,IAAI,aAAa,eAAe;AAAA,MAC/C;AAEA,UAAI,mBAAmB,KAAK,kBAAkB,GAAG;AAG/C,eAAO,KAAK,CAAC,OAAO,CAAC,CAAC;AAAA,MACxB,WAAW,kBAAkB,KAAK,mBAAmB,GAAG;AAGtD,eAAO,KAAK,CAAC,OAAO,EAAE,CAAC;AAAA,MACzB;AAAA,IACF;AAEA,QAAI,OAAO,SAAS,GAAG;AACrB,WAAK,OAAO,SAAS,IAAI,SAAS,MAAM,CAAC;AAAA,IAC3C;AAAA,EACF;AACF;AA9DE;AACA;AAkEK,SAAS,SAAY,KAAwB,CAAC,UAAa,OAAO;AACvE,SAAO,CAAC,WAAiD;AACvD,UAAM,SAAS,IAAI;AAAA,MACjB,OAAO;AAAA,MACP,IAAI,uBAAA;AAAA,IAA0B;AAEhC,UAAM,WAAW,IAAI;AAAA,MACnB,OAAO,MAAM,kBAAA;AAAA,MACb,OAAO,cAAA;AAAA,MACP,OAAO;AAAA,MACP;AAAA,IAAA;AAEF,WAAO,MAAM,YAAY,QAAQ;AACjC,WAAO,MAAM,UAAU,OAAO,cAAA,CAAe;AAC7C,WAAO;AAAA,EACT;AACF;"}
|