@tanstack/db-ivm 0.1.22 → 0.1.24
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/d2.cjs +12 -2
- package/dist/cjs/d2.cjs.map +1 -1
- package/dist/cjs/hashing/hash.cjs +75 -121
- package/dist/cjs/hashing/hash.cjs.map +1 -1
- package/dist/cjs/hashing/temporal.cjs +18 -0
- package/dist/cjs/hashing/temporal.cjs.map +1 -0
- package/dist/cjs/hashing/temporal.d.cts +5 -0
- package/dist/cjs/multiset.cjs +47 -92
- package/dist/cjs/multiset.cjs.map +1 -1
- package/dist/cjs/multiset.d.cts +16 -0
- package/dist/cjs/operators/groupBy.cjs +2 -2
- package/dist/cjs/operators/groupBy.cjs.map +1 -1
- package/dist/cjs/utils.cjs +15 -61
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +3 -32
- package/dist/esm/d2.js +12 -2
- package/dist/esm/d2.js.map +1 -1
- package/dist/esm/hashing/hash.js +75 -121
- package/dist/esm/hashing/hash.js.map +1 -1
- package/dist/esm/hashing/temporal.d.ts +5 -0
- package/dist/esm/hashing/temporal.js +18 -0
- package/dist/esm/hashing/temporal.js.map +1 -0
- package/dist/esm/multiset.d.ts +16 -0
- package/dist/esm/multiset.js +48 -93
- package/dist/esm/multiset.js.map +1 -1
- package/dist/esm/operators/groupBy.js +2 -2
- package/dist/esm/operators/groupBy.js.map +1 -1
- package/dist/esm/utils.d.ts +3 -32
- package/dist/esm/utils.js +15 -61
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/d2.ts +15 -2
- package/src/hashing/hash.ts +118 -172
- package/src/hashing/temporal.ts +25 -0
- package/src/multiset.ts +73 -132
- package/src/operators/groupBy.ts +8 -2
- package/src/utils.ts +20 -76
package/dist/esm/hashing/hash.js
CHANGED
|
@@ -1,35 +1,21 @@
|
|
|
1
1
|
import { MurmurHashStream, randomHash, getSymbolIdentity } from "./murmur.js";
|
|
2
|
+
import { isTemporal } from "./temporal.js";
|
|
2
3
|
const TRUE = randomHash();
|
|
3
4
|
const FALSE = randomHash();
|
|
4
5
|
const NULL = randomHash();
|
|
5
6
|
const UNDEFINED = randomHash();
|
|
6
7
|
const KEY = randomHash();
|
|
7
8
|
const FUNCTIONS = randomHash();
|
|
8
|
-
const DATE_MARKER = randomHash();
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
};
|
|
15
|
-
const
|
|
16
|
-
const TEMPORAL_MARKER = randomHash();
|
|
9
|
+
const DATE_MARKER = { hash: randomHash() };
|
|
10
|
+
const REGEXP_MARKER = { hash: randomHash() };
|
|
11
|
+
const OBJECT_MARKER = { hash: randomHash() };
|
|
12
|
+
const ARRAY_MARKER = { hash: randomHash() };
|
|
13
|
+
const MAP_MARKER = { hash: randomHash() };
|
|
14
|
+
const SET_MARKER = { hash: randomHash() };
|
|
15
|
+
const UINT8ARRAY_MARKER = { hash: randomHash() };
|
|
16
|
+
const TEMPORAL_MARKER = { hash: randomHash() };
|
|
17
17
|
const MAX_STRUCTURAL_HASH_WORK = 1e6;
|
|
18
18
|
const MAX_STRUCTURAL_HASH_DEPTH = 768;
|
|
19
|
-
const temporalTypes = /* @__PURE__ */ new Set([
|
|
20
|
-
`Temporal.Duration`,
|
|
21
|
-
`Temporal.Instant`,
|
|
22
|
-
`Temporal.PlainDate`,
|
|
23
|
-
`Temporal.PlainDateTime`,
|
|
24
|
-
`Temporal.PlainMonthDay`,
|
|
25
|
-
`Temporal.PlainTime`,
|
|
26
|
-
`Temporal.PlainYearMonth`,
|
|
27
|
-
`Temporal.ZonedDateTime`
|
|
28
|
-
]);
|
|
29
|
-
function isTemporal(input) {
|
|
30
|
-
const tag = input[Symbol.toStringTag];
|
|
31
|
-
return typeof tag === `string` && temporalTypes.has(tag);
|
|
32
|
-
}
|
|
33
19
|
const UINT8ARRAY_CONTENT_HASH_THRESHOLD = 128;
|
|
34
20
|
const hashCache = /* @__PURE__ */ new WeakMap();
|
|
35
21
|
const referenceValues = /* @__PURE__ */ new WeakSet();
|
|
@@ -50,65 +36,51 @@ function hashObject(input, context) {
|
|
|
50
36
|
context.activeObjects.add(input);
|
|
51
37
|
let valueHash;
|
|
52
38
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
39
|
+
const [marker, header, body] = objectParts(input);
|
|
40
|
+
const hasher = new MurmurHashStream();
|
|
41
|
+
hasher.update(marker.hash);
|
|
42
|
+
for (const value of header)
|
|
43
|
+
if (body) updateHasher(hasher, value, context);
|
|
44
|
+
else hasher.update(value);
|
|
45
|
+
if (body) {
|
|
46
|
+
const keys = Object.keys(body).sort(keySort);
|
|
47
|
+
keys.push(
|
|
48
|
+
...Object.getOwnPropertySymbols(body).filter(
|
|
49
|
+
(key) => Object.prototype.propertyIsEnumerable.call(body, key)
|
|
50
|
+
).sort(
|
|
51
|
+
(left, right) => getSymbolIdentity(left) - getSymbolIdentity(right)
|
|
52
|
+
)
|
|
65
53
|
);
|
|
54
|
+
for (const key of keys) {
|
|
55
|
+
hasher.update(KEY);
|
|
56
|
+
hasher.update(key);
|
|
57
|
+
updateHasher(hasher, body[key], context);
|
|
58
|
+
}
|
|
66
59
|
}
|
|
60
|
+
valueHash = hasher.digest();
|
|
67
61
|
} finally {
|
|
68
62
|
context.activeObjects.delete(input);
|
|
69
63
|
}
|
|
70
64
|
context.pendingHashes.set(input, valueHash);
|
|
71
65
|
return valueHash;
|
|
72
66
|
}
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return hasher.digest();
|
|
78
|
-
}
|
|
79
|
-
function hashUint8Array(input) {
|
|
80
|
-
const hasher = new MurmurHashStream();
|
|
81
|
-
hasher.update(UINT8ARRAY_MARKER);
|
|
82
|
-
hasher.update(input.byteLength);
|
|
83
|
-
for (let i = 0; i < input.byteLength; i++) {
|
|
84
|
-
hasher.writeByte(input[i]);
|
|
85
|
-
}
|
|
86
|
-
return hasher.digest();
|
|
87
|
-
}
|
|
88
|
-
function hashTemporal(input) {
|
|
89
|
-
const hasher = new MurmurHashStream();
|
|
90
|
-
hasher.update(TEMPORAL_MARKER);
|
|
91
|
-
hasher.update(input[Symbol.toStringTag]);
|
|
92
|
-
hasher.update(input.toString());
|
|
93
|
-
return hasher.digest();
|
|
94
|
-
}
|
|
95
|
-
function hashPlainObject(input, marker, context) {
|
|
96
|
-
const hasher = new MurmurHashStream();
|
|
97
|
-
hasher.update(marker);
|
|
98
|
-
const keys = Object.keys(input);
|
|
99
|
-
keys.sort(keySort);
|
|
100
|
-
for (const key of keys) {
|
|
101
|
-
hasher.update(KEY);
|
|
102
|
-
hasher.update(key);
|
|
103
|
-
updateHasher(hasher, input[key], context);
|
|
104
|
-
}
|
|
105
|
-
const symbolKeys = Object.getOwnPropertySymbols(input).filter((key) => Object.prototype.propertyIsEnumerable.call(input, key)).sort((left, right) => getSymbolIdentity(left) - getSymbolIdentity(right));
|
|
106
|
-
for (const key of symbolKeys) {
|
|
107
|
-
hasher.update(KEY);
|
|
108
|
-
hasher.update(key);
|
|
109
|
-
updateHasher(hasher, input[key], context);
|
|
67
|
+
function objectParts(input) {
|
|
68
|
+
if (input instanceof Date) {
|
|
69
|
+
const time = input.getTime();
|
|
70
|
+
return [DATE_MARKER, [Number.isNaN(time) ? `invalid` : time]];
|
|
110
71
|
}
|
|
111
|
-
|
|
72
|
+
if (isBinaryValue(input))
|
|
73
|
+
return [
|
|
74
|
+
UINT8ARRAY_MARKER,
|
|
75
|
+
[String.fromCharCode.apply(null, input)]
|
|
76
|
+
];
|
|
77
|
+
if (isTemporal(input))
|
|
78
|
+
return [TEMPORAL_MARKER, [input[Symbol.toStringTag], input.toString()]];
|
|
79
|
+
if (input instanceof RegExp)
|
|
80
|
+
return [REGEXP_MARKER, [input.source, input.flags, input.lastIndex], input];
|
|
81
|
+
if (input instanceof Map) return [MAP_MARKER, [], [...input.entries()]];
|
|
82
|
+
if (input instanceof Set) return [SET_MARKER, [], [...input.values()]];
|
|
83
|
+
return input instanceof Array ? [ARRAY_MARKER, [input.length], input] : [OBJECT_MARKER, [], input];
|
|
112
84
|
}
|
|
113
85
|
function updateHasher(hasher, input, context) {
|
|
114
86
|
if (context && ++context.work > MAX_STRUCTURAL_HASH_WORK) {
|
|
@@ -138,76 +110,58 @@ function updateHasher(hasher, input, context) {
|
|
|
138
110
|
return;
|
|
139
111
|
case `function`:
|
|
140
112
|
hasher.update(cachedReferenceHash(input));
|
|
141
|
-
return;
|
|
142
|
-
default:
|
|
143
|
-
console.warn(
|
|
144
|
-
`Ignored input during hashing because it is of type ${typeof input} which is not supported`
|
|
145
|
-
);
|
|
146
113
|
}
|
|
147
114
|
}
|
|
148
115
|
function getCachedHash(input, context) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
context
|
|
154
|
-
|
|
155
|
-
work: 0,
|
|
156
|
-
pendingHashes: /* @__PURE__ */ new Map()
|
|
157
|
-
};
|
|
158
|
-
const result = hashObject(input, context);
|
|
159
|
-
for (const [object, valueHash2] of context.pendingHashes) {
|
|
160
|
-
hashCache.set(object, valueHash2);
|
|
116
|
+
const cached = hashCache.get(input) ?? context?.pendingHashes.get(input);
|
|
117
|
+
if (cached !== void 0) return cached;
|
|
118
|
+
if (isReferenceHashedObject(input)) return cachedReferenceHash(input);
|
|
119
|
+
if (context) {
|
|
120
|
+
if (context.activeObjects.has(input)) {
|
|
121
|
+
throw new TypeError(`Cannot hash cyclic structural values`);
|
|
161
122
|
}
|
|
162
|
-
return
|
|
123
|
+
return hashObject(input, context);
|
|
163
124
|
}
|
|
164
|
-
|
|
165
|
-
|
|
125
|
+
const root = {
|
|
126
|
+
activeObjects: /* @__PURE__ */ new Set(),
|
|
127
|
+
work: 0,
|
|
128
|
+
pendingHashes: /* @__PURE__ */ new Map()
|
|
129
|
+
};
|
|
130
|
+
const result = hashObject(input, root);
|
|
131
|
+
for (const [object, valueHash] of root.pendingHashes) {
|
|
132
|
+
hashCache.set(object, valueHash);
|
|
166
133
|
}
|
|
167
|
-
|
|
168
|
-
const valueHash = hashCache.get(input) ?? context.pendingHashes.get(input);
|
|
169
|
-
if (valueHash !== void 0) return valueHash;
|
|
170
|
-
return hashObject(input, context);
|
|
134
|
+
return result;
|
|
171
135
|
}
|
|
172
136
|
function isReferenceHashedObject(input) {
|
|
173
|
-
return input instanceof File || isBinaryValue(input) && input.byteLength > UINT8ARRAY_CONTENT_HASH_THRESHOLD;
|
|
137
|
+
return referenceValues.has(input) || typeof File !== `undefined` && input instanceof File || isBinaryValue(input) && input.byteLength > UINT8ARRAY_CONTENT_HASH_THRESHOLD;
|
|
174
138
|
}
|
|
175
139
|
function isBinaryValue(input) {
|
|
176
140
|
return typeof Buffer !== `undefined` && input instanceof Buffer || input instanceof Uint8Array;
|
|
177
141
|
}
|
|
178
|
-
function structuralShape(input) {
|
|
179
|
-
if (input instanceof Map) return [`map`, [...input.entries()]];
|
|
180
|
-
if (input instanceof Set) return [`set`, [...input.values()]];
|
|
181
|
-
return [input instanceof Array ? `array` : `object`, input];
|
|
182
|
-
}
|
|
183
142
|
function equalHashValues(left, right) {
|
|
184
143
|
const compared = /* @__PURE__ */ new Map();
|
|
144
|
+
const keys = (value) => Reflect.ownKeys(value).filter(
|
|
145
|
+
(key) => Object.prototype.propertyIsEnumerable.call(value, key)
|
|
146
|
+
);
|
|
185
147
|
function equal(a, b) {
|
|
186
148
|
if (a === b || Number.isNaN(a) && Number.isNaN(b)) return true;
|
|
187
|
-
if (a === null || b === null || typeof a !== `object` || typeof b !== `object`)
|
|
149
|
+
if (a === null || b === null || typeof a !== `object` || typeof b !== `object` || isReferenceHashedObject(a) || isReferenceHashedObject(b))
|
|
188
150
|
return false;
|
|
189
|
-
if (
|
|
151
|
+
if (Array.isArray(a) && Array.isArray(b) && a.length !== b.length)
|
|
190
152
|
return false;
|
|
191
|
-
if (a instanceof Date || b instanceof Date)
|
|
192
|
-
return a instanceof Date && b instanceof Date && equal(a.getTime(), b.getTime());
|
|
193
|
-
if (isBinaryValue(a) || isBinaryValue(b))
|
|
194
|
-
return isBinaryValue(a) && isBinaryValue(b) && a.byteLength === b.byteLength && a.every((value, index) => value === b[index]);
|
|
195
|
-
if (isTemporal(a) || isTemporal(b))
|
|
196
|
-
return isTemporal(a) && isTemporal(b) && a[Symbol.toStringTag] === b[Symbol.toStringTag] && a.toString() === b.toString();
|
|
197
153
|
const peers = compared.get(a);
|
|
198
154
|
if (peers?.has(b)) return true;
|
|
155
|
+
const [aMarker, aHeader, aBody] = objectParts(a);
|
|
156
|
+
const [bMarker, bHeader, bBody] = objectParts(b);
|
|
157
|
+
if (aMarker !== bMarker || !aHeader.every((value, index) => value === bHeader[index]))
|
|
158
|
+
return false;
|
|
159
|
+
if (!aBody || !bBody) return true;
|
|
199
160
|
if (peers) peers.add(b);
|
|
200
161
|
else compared.set(a, /* @__PURE__ */ new Set([b]));
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const keys = (value) => Reflect.ownKeys(value).filter(
|
|
205
|
-
(key) => Object.prototype.propertyIsEnumerable.call(value, key)
|
|
206
|
-
);
|
|
207
|
-
const aKeys = keys(aShape);
|
|
208
|
-
if (aKeys.length !== keys(bShape).length) return false;
|
|
209
|
-
return aKeys.every(
|
|
210
|
-
(key) => Object.prototype.propertyIsEnumerable.call(bShape, key) && equal(aShape[key], bShape[key])
|
|
162
|
+
const aKeys = keys(aBody);
|
|
163
|
+
return aKeys.length === keys(bBody).length && aKeys.every(
|
|
164
|
+
(key) => Object.prototype.propertyIsEnumerable.call(bBody, key) && equal(aBody[key], bBody[key])
|
|
211
165
|
);
|
|
212
166
|
}
|
|
213
167
|
return equal(left, right);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hash.js","sources":["../../../src/hashing/hash.ts"],"sourcesContent":["import { MurmurHashStream, getSymbolIdentity, 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 STRUCTURAL_MARKERS = {\n object: randomHash(),\n array: randomHash(),\n map: randomHash(),\n set: randomHash(),\n}\nconst UINT8ARRAY_MARKER = randomHash()\nconst TEMPORAL_MARKER = randomHash()\n// Bound structural recursion and value visits. Shared acyclic subtrees are\n// cached; cycles are rejected rather than given context-dependent hashes.\nconst MAX_STRUCTURAL_HASH_WORK = 1_000_000\nconst MAX_STRUCTURAL_HASH_DEPTH = 768\n\nconst temporalTypes = new Set([\n `Temporal.Duration`,\n `Temporal.Instant`,\n `Temporal.PlainDate`,\n `Temporal.PlainDateTime`,\n `Temporal.PlainMonthDay`,\n `Temporal.PlainTime`,\n `Temporal.PlainYearMonth`,\n `Temporal.ZonedDateTime`,\n])\n\ninterface TemporalLike {\n [Symbol.toStringTag]: string\n toString: () => string\n}\n\nfunction isTemporal(input: object): input is TemporalLike {\n const tag = (input as Record<symbol, unknown>)[Symbol.toStringTag]\n return typeof tag === `string` && temporalTypes.has(tag)\n}\n\n// Maximum byte length for Uint8Arrays to hash by content instead of reference\n// Arrays smaller than this will be hashed by content, allowing proper equality comparisons\n// for small arrays like ULIDs (16 bytes) while still avoiding performance costs for large arrays\nconst UINT8ARRAY_CONTENT_HASH_THRESHOLD = 128\n\nconst hashCache = new WeakMap<object, number>()\nconst referenceValues = new WeakSet<object>()\n\n/** @internal Register a mutable handle before it enters a structural value. */\nexport function registerOpaqueHash(value: object): void {\n cachedReferenceHash(value)\n}\n\ntype HashContext = {\n activeObjects: Set<object>\n work: number\n pendingHashes: Map<object, number>\n}\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, context: HashContext): number {\n if (context.activeObjects.size >= MAX_STRUCTURAL_HASH_DEPTH) {\n throw new RangeError(\n `Value is too complex to hash safely: structural depth`,\n )\n }\n\n context.activeObjects.add(input)\n\n let valueHash: number | undefined\n try {\n if (input instanceof Date) {\n valueHash = hashDate(input)\n } else if (isBinaryValue(input)) {\n valueHash = hashUint8Array(input)\n } else if (isTemporal(input)) {\n valueHash = hashTemporal(input)\n } else {\n const [kind, plainObjectInput] = structuralShape(input)\n valueHash = hashPlainObject(\n plainObjectInput,\n STRUCTURAL_MARKERS[kind],\n context,\n )\n }\n } finally {\n context.activeObjects.delete(input)\n }\n\n context.pendingHashes.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 hashUint8Array(input: Uint8Array): number {\n const hasher = new MurmurHashStream()\n hasher.update(UINT8ARRAY_MARKER)\n // Hash the byte length first to differentiate arrays of different sizes\n hasher.update(input.byteLength)\n // Hash each byte in the array\n for (let i = 0; i < input.byteLength; i++) {\n hasher.writeByte(input[i]!)\n }\n return hasher.digest()\n}\n\nfunction hashTemporal(input: TemporalLike): number {\n const hasher = new MurmurHashStream()\n hasher.update(TEMPORAL_MARKER)\n hasher.update(input[Symbol.toStringTag])\n hasher.update(input.toString())\n return hasher.digest()\n}\n\nfunction hashPlainObject(\n input: object,\n marker: number,\n context: HashContext,\n): 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], context)\n }\n const symbolKeys = Object.getOwnPropertySymbols(input)\n .filter((key) => Object.prototype.propertyIsEnumerable.call(input, key))\n .sort((left, right) => getSymbolIdentity(left) - getSymbolIdentity(right))\n for (const key of symbolKeys) {\n hasher.update(KEY)\n hasher.update(key)\n updateHasher(hasher, input[key as keyof typeof input], context)\n }\n\n return hasher.digest()\n}\n\nfunction updateHasher(\n hasher: Hasher,\n input: unknown,\n context?: HashContext,\n): void {\n if (context && ++context.work > MAX_STRUCTURAL_HASH_WORK) {\n throw new RangeError(`Value is too complex to hash safely: structural work`)\n }\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, context))\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, context?: HashContext): number {\n if (!context) {\n const cached = hashCache.get(input)\n if (cached !== undefined) return cached\n if (isReferenceHashedObject(input)) return cachedReferenceHash(input)\n\n // Only an uncached structural root needs graph traversal state. Commit its\n // cache entries after success so a failed traversal cannot poison retries.\n context = {\n activeObjects: new Set(),\n work: 0,\n pendingHashes: new Map(),\n }\n const result = hashObject(input, context)\n for (const [object, valueHash] of context.pendingHashes) {\n hashCache.set(object, valueHash)\n }\n return result\n }\n\n if (context.activeObjects.has(input)) {\n throw new TypeError(`Cannot hash cyclic structural values`)\n }\n\n // Opaque leaves cannot contain structural back-references. Resolve them\n // before entering structural recursion, even when they have user properties.\n if (isReferenceHashedObject(input)) return cachedReferenceHash(input)\n\n const valueHash = hashCache.get(input) ?? context.pendingHashes.get(input)\n if (valueHash !== undefined) return valueHash\n\n return hashObject(input, context)\n}\n\nfunction isReferenceHashedObject(input: object): boolean {\n return (\n input instanceof File ||\n (isBinaryValue(input) &&\n input.byteLength > UINT8ARRAY_CONTENT_HASH_THRESHOLD)\n )\n}\n\nfunction isBinaryValue(input: object): input is Uint8Array {\n return (\n (typeof Buffer !== `undefined` && input instanceof Buffer) ||\n input instanceof Uint8Array\n )\n}\n\nfunction structuralShape(\n input: object,\n): [keyof typeof STRUCTURAL_MARKERS, object] {\n if (input instanceof Map) return [`map`, [...input.entries()]]\n if (input instanceof Set) return [`set`, [...input.values()]]\n return [input instanceof Array ? `array` : `object`, input]\n}\n\n/** @internal Compare immutable structural values without computing a digest.\n * Pair memoization also permits cyclic values that structural hashing rejects.\n * Reference-valued leaves remain opaque, including registered mutable handles.\n */\nexport function equalHashValues(left: unknown, right: unknown): boolean {\n const compared = new Map<object, Set<object>>()\n function equal(a: unknown, b: unknown): boolean {\n if (a === b || (Number.isNaN(a) && Number.isNaN(b))) return true\n if (\n a === null ||\n b === null ||\n typeof a !== `object` ||\n typeof b !== `object`\n )\n return false\n if (\n referenceValues.has(a) ||\n referenceValues.has(b) ||\n isReferenceHashedObject(a) ||\n isReferenceHashedObject(b)\n )\n return false\n if (a instanceof Date || b instanceof Date)\n return (\n a instanceof Date &&\n b instanceof Date &&\n equal(a.getTime(), b.getTime())\n )\n if (isBinaryValue(a) || isBinaryValue(b))\n return (\n isBinaryValue(a) &&\n isBinaryValue(b) &&\n a.byteLength === b.byteLength &&\n a.every((value, index) => value === b[index])\n )\n if (isTemporal(a) || isTemporal(b))\n return (\n isTemporal(a) &&\n isTemporal(b) &&\n a[Symbol.toStringTag] === b[Symbol.toStringTag] &&\n a.toString() === b.toString()\n )\n\n // Revisited pairs close cycles and avoid expanding shared subtrees.\n const peers = compared.get(a)\n if (peers?.has(b)) return true\n if (peers) peers.add(b)\n else compared.set(a, new Set([b]))\n const [aKind, aShape] = structuralShape(a)\n const [bKind, bShape] = structuralShape(b)\n if (aKind !== bKind) return false\n const keys = (value: object) =>\n Reflect.ownKeys(value).filter((key) =>\n Object.prototype.propertyIsEnumerable.call(value, key),\n )\n const aKeys = keys(aShape)\n if (aKeys.length !== keys(bShape).length) return false\n return aKeys.every(\n (key) =>\n Object.prototype.propertyIsEnumerable.call(bShape, key) &&\n equal(aShape[key as keyof object], bShape[key as keyof object]),\n )\n }\n return equal(left, right)\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 referenceValues.add(fn)\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":["valueHash"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hash.js","sources":["../../../src/hashing/hash.ts"],"sourcesContent":["import { MurmurHashStream, getSymbolIdentity, randomHash } from './murmur.js'\nimport { isTemporal } from './temporal.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()\n// Identity is distinct even if two random hash values happen to collide.\nconst DATE_MARKER = { hash: randomHash() }\nconst REGEXP_MARKER = { hash: randomHash() }\nconst OBJECT_MARKER = { hash: randomHash() }\nconst ARRAY_MARKER = { hash: randomHash() }\nconst MAP_MARKER = { hash: randomHash() }\nconst SET_MARKER = { hash: randomHash() }\nconst UINT8ARRAY_MARKER = { hash: randomHash() }\nconst TEMPORAL_MARKER = { hash: randomHash() }\n// Bound structural recursion and value visits. Shared acyclic subtrees are\n// cached; cycles are rejected rather than given context-dependent hashes.\nconst MAX_STRUCTURAL_HASH_WORK = 1_000_000\nconst MAX_STRUCTURAL_HASH_DEPTH = 768\n\n// Maximum byte length for Uint8Arrays to hash by content instead of reference\n// Arrays smaller than this will be hashed by content, allowing proper equality comparisons\n// for small arrays like ULIDs (16 bytes) while still avoiding performance costs for large arrays\nconst UINT8ARRAY_CONTENT_HASH_THRESHOLD = 128\n\nconst hashCache = new WeakMap<object, number>()\nconst referenceValues = new WeakSet<object>()\n\n/** @internal Register a mutable handle before it enters a structural value. */\nexport function registerOpaqueHash(value: object): void {\n cachedReferenceHash(value)\n}\n\ntype HashContext = {\n activeObjects: Set<object>\n work: number\n pendingHashes: Map<object, number>\n}\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, context: HashContext): number {\n if (context.activeObjects.size >= MAX_STRUCTURAL_HASH_DEPTH) {\n throw new RangeError(\n `Value is too complex to hash safely: structural depth`,\n )\n }\n\n context.activeObjects.add(input)\n\n let valueHash: number | undefined\n try {\n const [marker, header, body] = objectParts(input)\n const hasher = new MurmurHashStream()\n hasher.update(marker.hash)\n // Header values of a type with a body (array length, RegExp fields)\n // count toward the work cap; Date, binary, and Temporal headers do not.\n for (const value of header)\n if (body) updateHasher(hasher, value, context)\n else hasher.update(value as string | number)\n if (body) {\n // Sorted string keys, then enumerable symbol keys by identity.\n const keys: Array<string | symbol> = Object.keys(body).sort(keySort)\n keys.push(\n ...Object.getOwnPropertySymbols(body)\n .filter((key) =>\n Object.prototype.propertyIsEnumerable.call(body, key),\n )\n .sort(\n (left, right) => getSymbolIdentity(left) - getSymbolIdentity(right),\n ),\n )\n for (const key of keys) {\n hasher.update(KEY)\n hasher.update(key)\n updateHasher(hasher, body[key as keyof typeof body], context)\n }\n }\n valueHash = hasher.digest()\n } finally {\n context.activeObjects.delete(input)\n }\n\n context.pendingHashes.set(input, valueHash)\n return valueHash\n}\n\n/**\n * The one type dispatch shared by structural hashing and equality: a type\n * marker, primitive header values, and an optional object whose enumerable\n * own properties are compared structurally.\n */\ntype ObjectParts = [\n marker: Readonly<{ hash: number }>,\n // A RegExp `lastIndex` can hold any value; the other headers are strings\n // or numbers.\n header: ReadonlyArray<unknown>,\n body?: object,\n]\n\nfunction objectParts(input: object): ObjectParts {\n if (input instanceof Date) {\n const time = input.getTime()\n // Invalid dates are equal to each other; headers compare with `===`.\n return [DATE_MARKER, [Number.isNaN(time) ? `invalid` : time]]\n }\n if (isBinaryValue(input))\n return [\n UINT8ARRAY_MARKER,\n [String.fromCharCode.apply(null, input as unknown as Array<number>)],\n ]\n if (isTemporal(input))\n return [TEMPORAL_MARKER, [input[Symbol.toStringTag], input.toString()]]\n if (input instanceof RegExp)\n return [REGEXP_MARKER, [input.source, input.flags, input.lastIndex], input]\n if (input instanceof Map) return [MAP_MARKER, [], [...input.entries()]]\n if (input instanceof Set) return [SET_MARKER, [], [...input.values()]]\n return input instanceof Array\n ? [ARRAY_MARKER, [input.length], input]\n : [OBJECT_MARKER, [], input]\n}\n\nfunction updateHasher(\n hasher: Hasher,\n input: unknown,\n context?: HashContext,\n): void {\n if (context && ++context.work > MAX_STRUCTURAL_HASH_WORK) {\n throw new RangeError(`Value is too complex to hash safely: structural work`)\n }\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, context))\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 }\n}\n\nfunction getCachedHash(input: object, context?: HashContext): number {\n // Active objects are neither cached nor pending, so the cycle check below\n // still sees every back-reference.\n const cached = hashCache.get(input) ?? context?.pendingHashes.get(input)\n if (cached !== undefined) return cached\n\n // Opaque leaves cannot contain structural back-references. Resolve them\n // before entering structural recursion, even when they have user properties.\n if (isReferenceHashedObject(input)) return cachedReferenceHash(input)\n\n if (context) {\n if (context.activeObjects.has(input)) {\n throw new TypeError(`Cannot hash cyclic structural values`)\n }\n return hashObject(input, context)\n }\n\n // Only an uncached structural root needs graph traversal state. Commit its\n // cache entries after success so a failed traversal cannot poison retries.\n const root: HashContext = {\n activeObjects: new Set(),\n work: 0,\n pendingHashes: new Map(),\n }\n const result = hashObject(input, root)\n for (const [object, valueHash] of root.pendingHashes) {\n hashCache.set(object, valueHash)\n }\n return result\n}\n\nfunction isReferenceHashedObject(input: object): boolean {\n return (\n referenceValues.has(input) ||\n (typeof File !== `undefined` && input instanceof File) ||\n (isBinaryValue(input) &&\n input.byteLength > UINT8ARRAY_CONTENT_HASH_THRESHOLD)\n )\n}\n\n// Buffer can come from another realm than Uint8Array (for example under jsdom).\nfunction isBinaryValue(input: object): input is Uint8Array {\n return (\n (typeof Buffer !== `undefined` && input instanceof Buffer) ||\n input instanceof Uint8Array\n )\n}\n\n/** @internal Compare immutable structural values without computing a digest.\n * Pair memoization also permits cyclic values that structural hashing rejects.\n * Reference-valued leaves remain opaque, including registered mutable handles.\n */\nexport function equalHashValues(left: unknown, right: unknown): boolean {\n const compared = new Map<object, Set<object>>()\n const keys = (value: object) =>\n Reflect.ownKeys(value).filter((key) =>\n Object.prototype.propertyIsEnumerable.call(value, key),\n )\n function equal(a: unknown, b: unknown): boolean {\n if (a === b || (Number.isNaN(a) && Number.isNaN(b))) return true\n if (\n a === null ||\n b === null ||\n typeof a !== `object` ||\n typeof b !== `object` ||\n isReferenceHashedObject(a) ||\n isReferenceHashedObject(b)\n )\n return false\n // Arrays from another realm take the object marker but keep this check.\n if (Array.isArray(a) && Array.isArray(b) && a.length !== b.length)\n return false\n\n // Revisited pairs close cycles and avoid expanding shared subtrees. Check\n // before objectParts copies Map or Set entries; record only pairs with a\n // body, after their markers and headers match.\n const peers = compared.get(a)\n if (peers?.has(b)) return true\n const [aMarker, aHeader, aBody] = objectParts(a)\n const [bMarker, bHeader, bBody] = objectParts(b)\n if (\n aMarker !== bMarker ||\n !aHeader.every((value, index) => value === bHeader[index])\n )\n return false\n if (!aBody || !bBody) return true\n if (peers) peers.add(b)\n else compared.set(a, new Set([b]))\n const aKeys = keys(aBody)\n return (\n aKeys.length === keys(bBody).length &&\n aKeys.every(\n (key) =>\n Object.prototype.propertyIsEnumerable.call(bBody, key) &&\n equal(aBody[key as keyof object], bBody[key as keyof object]),\n )\n )\n }\n return equal(left, right)\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 referenceValues.add(fn)\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,18 @@
|
|
|
1
|
+
const temporalTypes = /* @__PURE__ */ new Set([
|
|
2
|
+
`Temporal.Duration`,
|
|
3
|
+
`Temporal.Instant`,
|
|
4
|
+
`Temporal.PlainDate`,
|
|
5
|
+
`Temporal.PlainDateTime`,
|
|
6
|
+
`Temporal.PlainMonthDay`,
|
|
7
|
+
`Temporal.PlainTime`,
|
|
8
|
+
`Temporal.PlainYearMonth`,
|
|
9
|
+
`Temporal.ZonedDateTime`
|
|
10
|
+
]);
|
|
11
|
+
function isTemporal(input) {
|
|
12
|
+
const tag = input[Symbol.toStringTag];
|
|
13
|
+
return typeof tag === `string` && temporalTypes.has(tag) && Object.getPrototypeOf(input)?.[Symbol.toStringTag] === tag;
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
isTemporal
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=temporal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temporal.js","sources":["../../../src/hashing/temporal.ts"],"sourcesContent":["const temporalTypes = new Set([\n `Temporal.Duration`,\n `Temporal.Instant`,\n `Temporal.PlainDate`,\n `Temporal.PlainDateTime`,\n `Temporal.PlainMonthDay`,\n `Temporal.PlainTime`,\n `Temporal.PlainYearMonth`,\n `Temporal.ZonedDateTime`,\n])\n\nexport interface TemporalLike {\n [Symbol.toStringTag]: string\n toString: () => string\n}\n\nexport function isTemporal(input: object): input is TemporalLike {\n const tag = (input as Record<symbol, unknown>)[Symbol.toStringTag]\n // An own tag on a plain object does not make it a Temporal value.\n return (\n typeof tag === `string` &&\n temporalTypes.has(tag) &&\n Object.getPrototypeOf(input)?.[Symbol.toStringTag] === tag\n )\n}\n"],"names":[],"mappings":"AAAA,MAAM,oCAAoB,IAAI;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAOM,SAAS,WAAW,OAAsC;AAC/D,QAAM,MAAO,MAAkC,OAAO,WAAW;AAEjE,SACE,OAAO,QAAQ,YACf,cAAc,IAAI,GAAG,KACrB,OAAO,eAAe,KAAK,IAAI,OAAO,WAAW,MAAM;AAE3D;"}
|
package/dist/esm/multiset.d.ts
CHANGED
|
@@ -29,6 +29,22 @@ export declare class MultiSet<T> {
|
|
|
29
29
|
* Produce as output a collection that is logically equivalent to the input
|
|
30
30
|
* but which combines identical instances of the same record into one
|
|
31
31
|
* (record, multiplicity) pair.
|
|
32
|
+
*
|
|
33
|
+
* Record identity depends on the shape of the whole multiset:
|
|
34
|
+
*
|
|
35
|
+
* - Keyed: every record is a `[key, value]` pair with a string or number
|
|
36
|
+
* key. Keys and primitive values compare by value, and object values by
|
|
37
|
+
* reference. A value that is an array of length 2 is a join tuple, and
|
|
38
|
+
* its two elements compare by the same rule. The keyed identity text
|
|
39
|
+
* has known collisions: https://github.com/TanStack/db/issues/1948
|
|
40
|
+
* - Unkeyed, one primitive type: every record is a string, or every record
|
|
41
|
+
* is a number. Records compare as `Map` keys do, and a `-0` record is
|
|
42
|
+
* returned as `0`.
|
|
43
|
+
* - Otherwise, records compare by structure (`hash`).
|
|
44
|
+
*
|
|
45
|
+
* Each identity keeps its first record in input order with the summed
|
|
46
|
+
* multiplicity. Identities whose sum is zero are dropped. The input is not
|
|
47
|
+
* changed.
|
|
32
48
|
*/
|
|
33
49
|
consolidate(): MultiSet<T>;
|
|
34
50
|
extend(other: MultiSet<T> | MultiSetArray<T>): void;
|
package/dist/esm/multiset.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chunkedArrayPush,
|
|
1
|
+
import { chunkedArrayPush, getStringId } from "./utils.js";
|
|
2
2
|
import { hash } from "./hashing/hash.js";
|
|
3
3
|
class MultiSet {
|
|
4
4
|
#inner;
|
|
@@ -49,102 +49,42 @@ class MultiSet {
|
|
|
49
49
|
* Produce as output a collection that is logically equivalent to the input
|
|
50
50
|
* but which combines identical instances of the same record into one
|
|
51
51
|
* (record, multiplicity) pair.
|
|
52
|
-
*/
|
|
53
|
-
consolidate() {
|
|
54
|
-
if (this.#inner.length > 0) {
|
|
55
|
-
const firstItem = this.#inner[0]?.[0];
|
|
56
|
-
if (Array.isArray(firstItem) && firstItem.length === 2) {
|
|
57
|
-
return this.#consolidateKeyed();
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return this.#consolidateUnkeyed();
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Private method for consolidating keyed multisets where keys are strings/numbers
|
|
64
|
-
* and values are compared by reference equality.
|
|
65
52
|
*
|
|
66
|
-
*
|
|
67
|
-
* by using WeakMap for object reference tracking and avoiding expensive serialization.
|
|
53
|
+
* Record identity depends on the shape of the whole multiset:
|
|
68
54
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
for (const [data, multiplicity] of this.#inner) {
|
|
83
|
-
if (!Array.isArray(data) || data.length !== 2) {
|
|
84
|
-
return this.#consolidateUnkeyed();
|
|
85
|
-
}
|
|
86
|
-
const [key, value] = data;
|
|
87
|
-
if (typeof key !== `string` && typeof key !== `number`) {
|
|
88
|
-
return this.#consolidateUnkeyed();
|
|
89
|
-
}
|
|
90
|
-
let valueId;
|
|
91
|
-
if (Array.isArray(value) && value.length === 2) {
|
|
92
|
-
valueId = getTupleId(value);
|
|
93
|
-
} else {
|
|
94
|
-
valueId = globalObjectIdGenerator.getStringId(value);
|
|
95
|
-
}
|
|
96
|
-
const compositeKey = key + `|` + valueId;
|
|
97
|
-
consolidated.set(
|
|
98
|
-
compositeKey,
|
|
99
|
-
(consolidated.get(compositeKey) || 0) + multiplicity
|
|
100
|
-
);
|
|
101
|
-
if (!values.has(compositeKey)) {
|
|
102
|
-
values.set(compositeKey, data);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
const result = [];
|
|
106
|
-
for (const [compositeKey, multiplicity] of consolidated) {
|
|
107
|
-
if (multiplicity !== 0) {
|
|
108
|
-
result.push([values.get(compositeKey), multiplicity]);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return new MultiSet(result);
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Private method for consolidating unkeyed multisets using the original approach.
|
|
55
|
+
* - Keyed: every record is a `[key, value]` pair with a string or number
|
|
56
|
+
* key. Keys and primitive values compare by value, and object values by
|
|
57
|
+
* reference. A value that is an array of length 2 is a join tuple, and
|
|
58
|
+
* its two elements compare by the same rule. The keyed identity text
|
|
59
|
+
* has known collisions: https://github.com/TanStack/db/issues/1948
|
|
60
|
+
* - Unkeyed, one primitive type: every record is a string, or every record
|
|
61
|
+
* is a number. Records compare as `Map` keys do, and a `-0` record is
|
|
62
|
+
* returned as `0`.
|
|
63
|
+
* - Otherwise, records compare by structure (`hash`).
|
|
64
|
+
*
|
|
65
|
+
* Each identity keeps its first record in input order with the summed
|
|
66
|
+
* multiplicity. Identities whose sum is zero are dropped. The input is not
|
|
67
|
+
* changed.
|
|
115
68
|
*/
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
hasNumber = true;
|
|
127
|
-
} else {
|
|
128
|
-
hasOther = true;
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
const requireJson = hasOther || hasString && hasNumber;
|
|
133
|
-
for (const [data, multiplicity] of this.#inner) {
|
|
134
|
-
const key = requireJson ? hash(data) : data;
|
|
135
|
-
if (requireJson && !values.has(key)) {
|
|
136
|
-
values.set(key, data);
|
|
137
|
-
}
|
|
138
|
-
consolidated.update(key, (count) => count + multiplicity);
|
|
139
|
-
}
|
|
140
|
-
const result = [];
|
|
141
|
-
for (const [key, multiplicity] of consolidated.entries()) {
|
|
142
|
-
if (multiplicity !== 0) {
|
|
143
|
-
const parsedKey = requireJson ? values.get(key) : key;
|
|
144
|
-
result.push([parsedKey, multiplicity]);
|
|
145
|
-
}
|
|
69
|
+
consolidate() {
|
|
70
|
+
const inner = this.#inner;
|
|
71
|
+
if (inner.every(
|
|
72
|
+
([data]) => Array.isArray(data) && data.length === 2 && (typeof data[0] === `string` || typeof data[0] === `number`)
|
|
73
|
+
)) {
|
|
74
|
+
return consolidateBy(inner, (data) => {
|
|
75
|
+
const [key, value] = data;
|
|
76
|
+
const valueId = Array.isArray(value) && value.length === 2 ? `${getStringId(value[0])}|${getStringId(value[1])}` : getStringId(value);
|
|
77
|
+
return key + `|` + valueId;
|
|
78
|
+
});
|
|
146
79
|
}
|
|
147
|
-
|
|
80
|
+
let primitiveType;
|
|
81
|
+
const requireHash = inner.some(([data]) => {
|
|
82
|
+
const type = typeof data;
|
|
83
|
+
if (type !== `string` && type !== `number`) return true;
|
|
84
|
+
primitiveType ??= type;
|
|
85
|
+
return type !== primitiveType;
|
|
86
|
+
});
|
|
87
|
+
return requireHash ? consolidateBy(inner, hash) : consolidateBy(inner, (data) => data, true);
|
|
148
88
|
}
|
|
149
89
|
extend(other) {
|
|
150
90
|
const otherArray = other instanceof MultiSet ? other.getInner() : other;
|
|
@@ -159,6 +99,21 @@ class MultiSet {
|
|
|
159
99
|
return this.#inner;
|
|
160
100
|
}
|
|
161
101
|
}
|
|
102
|
+
function consolidateBy(inner, identityOf, keepIdentity = false) {
|
|
103
|
+
const consolidated = /* @__PURE__ */ new Map();
|
|
104
|
+
for (const [data, multiplicity] of inner) {
|
|
105
|
+
const identity = identityOf(data);
|
|
106
|
+
const entry = consolidated.get(identity);
|
|
107
|
+
if (entry) entry[1] += multiplicity;
|
|
108
|
+
else consolidated.set(identity, [data, multiplicity]);
|
|
109
|
+
}
|
|
110
|
+
const result = [];
|
|
111
|
+
for (const [identity, [data, multiplicity]] of consolidated) {
|
|
112
|
+
if (multiplicity !== 0)
|
|
113
|
+
result.push([keepIdentity ? identity : data, multiplicity]);
|
|
114
|
+
}
|
|
115
|
+
return new MultiSet(result);
|
|
116
|
+
}
|
|
162
117
|
export {
|
|
163
118
|
MultiSet
|
|
164
119
|
};
|
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} 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 add(item: T, multiplicity: number): void {\n if (multiplicity !== 0) {\n this.#inner.push([item, multiplicity])\n }\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 { chunkedArrayPush, getStringId } 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 * Record identity depends on the shape of the whole multiset:\n *\n * - Keyed: every record is a `[key, value]` pair with a string or number\n * key. Keys and primitive values compare by value, and object values by\n * reference. A value that is an array of length 2 is a join tuple, and\n * its two elements compare by the same rule. The keyed identity text\n * has known collisions: https://github.com/TanStack/db/issues/1948\n * - Unkeyed, one primitive type: every record is a string, or every record\n * is a number. Records compare as `Map` keys do, and a `-0` record is\n * returned as `0`.\n * - Otherwise, records compare by structure (`hash`).\n *\n * Each identity keeps its first record in input order with the summed\n * multiplicity. Identities whose sum is zero are dropped. The input is not\n * changed.\n */\n consolidate(): MultiSet<T> {\n const inner = this.#inner\n // Keyed multisets hold [string | number key, value] pairs. Their values are\n // compared by reference, which avoids hashing. Join tuples (values of\n // length 2) are unpacked so ['A', null] and [null, 'X'] stay distinct.\n if (\n inner.every(\n ([data]) =>\n Array.isArray(data) &&\n data.length === 2 &&\n (typeof data[0] === `string` || typeof data[0] === `number`),\n )\n ) {\n return consolidateBy(inner, (data) => {\n const [key, value] = data as [string | number, unknown]\n const valueId =\n Array.isArray(value) && value.length === 2\n ? `${getStringId(value[0])}|${getStringId(value[1])}`\n : getStringId(value)\n return key + `|` + valueId\n })\n }\n\n // Unkeyed data of a single primitive type is its own identity, and the\n // result holds the Map key, so -0 becomes 0. Anything else is compared\n // structurally by hash.\n let primitiveType: string | undefined\n const requireHash = inner.some(([data]) => {\n const type = typeof data\n if (type !== `string` && type !== `number`) return true\n primitiveType ??= type\n return type !== primitiveType\n })\n return requireHash\n ? consolidateBy(inner, hash)\n : consolidateBy(inner, (data) => data, true)\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 add(item: T, multiplicity: number): void {\n if (multiplicity !== 0) {\n this.#inner.push([item, multiplicity])\n }\n }\n\n getInner(): MultiSetArray<T> {\n return this.#inner\n }\n}\n\n/**\n * Sums multiplicities of records with the same identity, keeping the first\n * record seen for each identity (or its Map key, which writes -0 as 0, when\n * `keepIdentity` is set) and dropping records whose sum is zero.\n */\nfunction consolidateBy<T>(\n inner: MultiSetArray<T>,\n identityOf: (data: T) => unknown,\n keepIdentity = false,\n): MultiSet<T> {\n const consolidated = new Map<unknown, [T, number]>()\n for (const [data, multiplicity] of inner) {\n const identity = identityOf(data)\n const entry = consolidated.get(identity)\n if (entry) entry[1] += multiplicity\n else consolidated.set(identity, [data, multiplicity])\n }\n const result: MultiSetArray<T> = []\n for (const [identity, [data, multiplicity]] of consolidated) {\n if (multiplicity !== 0)\n result.push([keepIdentity ? (identity as T) : data, multiplicity])\n }\n return new MultiSet(result)\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -122,7 +122,7 @@ function min(valueExtractor) {
|
|
|
122
122
|
reduce: (values) => {
|
|
123
123
|
let minValue;
|
|
124
124
|
for (const [value, _multiplicity] of values) {
|
|
125
|
-
if (
|
|
125
|
+
if (value !== void 0 && (minValue === void 0 || value < minValue)) {
|
|
126
126
|
minValue = value;
|
|
127
127
|
}
|
|
128
128
|
}
|
|
@@ -137,7 +137,7 @@ function max(valueExtractor) {
|
|
|
137
137
|
reduce: (values) => {
|
|
138
138
|
let maxValue;
|
|
139
139
|
for (const [value, _multiplicity] of values) {
|
|
140
|
-
if (
|
|
140
|
+
if (value !== void 0 && (maxValue === void 0 || value > maxValue)) {
|
|
141
141
|
maxValue = value;
|
|
142
142
|
}
|
|
143
143
|
}
|