@tanstack/db-ivm 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/hashing/hash.cjs +24 -0
- package/dist/cjs/hashing/hash.cjs.map +1 -1
- package/dist/cjs/utils.cjs +123 -6
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +7 -3
- package/dist/esm/hashing/hash.js +24 -0
- package/dist/esm/hashing/hash.js.map +1 -1
- package/dist/esm/utils.d.ts +7 -3
- package/dist/esm/utils.js +123 -6
- package/dist/esm/utils.js.map +1 -1
- package/package.json +3 -2
- package/src/hashing/hash.ts +32 -0
- package/src/utils.ts +204 -9
|
@@ -13,6 +13,21 @@ const ARRAY_MARKER = murmur.randomHash();
|
|
|
13
13
|
const MAP_MARKER = murmur.randomHash();
|
|
14
14
|
const SET_MARKER = murmur.randomHash();
|
|
15
15
|
const UINT8ARRAY_MARKER = murmur.randomHash();
|
|
16
|
+
const TEMPORAL_MARKER = murmur.randomHash();
|
|
17
|
+
const temporalTypes = /* @__PURE__ */ new Set([
|
|
18
|
+
`Temporal.Duration`,
|
|
19
|
+
`Temporal.Instant`,
|
|
20
|
+
`Temporal.PlainDate`,
|
|
21
|
+
`Temporal.PlainDateTime`,
|
|
22
|
+
`Temporal.PlainMonthDay`,
|
|
23
|
+
`Temporal.PlainTime`,
|
|
24
|
+
`Temporal.PlainYearMonth`,
|
|
25
|
+
`Temporal.ZonedDateTime`
|
|
26
|
+
]);
|
|
27
|
+
function isTemporal(input) {
|
|
28
|
+
const tag = input[Symbol.toStringTag];
|
|
29
|
+
return typeof tag === `string` && temporalTypes.has(tag);
|
|
30
|
+
}
|
|
16
31
|
const UINT8ARRAY_CONTENT_HASH_THRESHOLD = 128;
|
|
17
32
|
const hashCache = /* @__PURE__ */ new WeakMap();
|
|
18
33
|
function hash(input) {
|
|
@@ -39,6 +54,8 @@ function hashObject(input) {
|
|
|
39
54
|
}
|
|
40
55
|
} else if (input instanceof File) {
|
|
41
56
|
return cachedReferenceHash(input);
|
|
57
|
+
} else if (isTemporal(input)) {
|
|
58
|
+
valueHash = hashTemporal(input);
|
|
42
59
|
} else {
|
|
43
60
|
let plainObjectInput = input;
|
|
44
61
|
let marker = OBJECT_MARKER;
|
|
@@ -73,6 +90,13 @@ function hashUint8Array(input) {
|
|
|
73
90
|
}
|
|
74
91
|
return hasher.digest();
|
|
75
92
|
}
|
|
93
|
+
function hashTemporal(input) {
|
|
94
|
+
const hasher = new murmur.MurmurHashStream();
|
|
95
|
+
hasher.update(TEMPORAL_MARKER);
|
|
96
|
+
hasher.update(input[Symbol.toStringTag]);
|
|
97
|
+
hasher.update(input.toString());
|
|
98
|
+
return hasher.digest();
|
|
99
|
+
}
|
|
76
100
|
function hashPlainObject(input, marker) {
|
|
77
101
|
const hasher = new murmur.MurmurHashStream();
|
|
78
102
|
hasher.update(marker);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hash.cjs","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()\nconst UINT8ARRAY_MARKER = randomHash()\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>()\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 if (\n // Check if input is a Uint8Array or Buffer\n (typeof Buffer !== `undefined` && input instanceof Buffer) ||\n input instanceof Uint8Array\n ) {\n // For small Uint8Arrays/Buffers (e.g., ULIDs, UUIDs), hash by content\n // to enable proper equality comparisons. For large arrays, hash by reference\n // to avoid performance costs.\n if (input.byteLength <= UINT8ARRAY_CONTENT_HASH_THRESHOLD) {\n valueHash = hashUint8Array(input)\n } else {\n // Deeply hashing large arrays would be too costly\n // so we track them by reference and cache them in a weak map\n return cachedReferenceHash(input)\n }\n } else if (input instanceof File) {\n // Files are always hashed by reference due to their potentially large size\n return cachedReferenceHash(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 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 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 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":["randomHash","MurmurHashStream"],"mappings":";;;AAQA,MAAM,OAAOA,OAAAA,WAAA;AACb,MAAM,QAAQA,OAAAA,WAAA;AACd,MAAM,OAAOA,OAAAA,WAAA;AACb,MAAM,YAAYA,OAAAA,WAAA;AAClB,MAAM,MAAMA,OAAAA,WAAA;AACZ,MAAM,YAAYA,OAAAA,WAAA;AAClB,MAAM,cAAcA,OAAAA,WAAA;AACpB,MAAM,gBAAgBA,OAAAA,WAAA;AACtB,MAAM,eAAeA,OAAAA,WAAA;AACrB,MAAM,aAAaA,OAAAA,WAAA;AACnB,MAAM,aAAaA,OAAAA,WAAA;AACnB,MAAM,oBAAoBA,OAAAA,WAAA;
|
|
1
|
+
{"version":3,"file":"hash.cjs","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()\nconst UINT8ARRAY_MARKER = randomHash()\nconst TEMPORAL_MARKER = randomHash()\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>()\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 if (\n // Check if input is a Uint8Array or Buffer\n (typeof Buffer !== `undefined` && input instanceof Buffer) ||\n input instanceof Uint8Array\n ) {\n // For small Uint8Arrays/Buffers (e.g., ULIDs, UUIDs), hash by content\n // to enable proper equality comparisons. For large arrays, hash by reference\n // to avoid performance costs.\n if (input.byteLength <= UINT8ARRAY_CONTENT_HASH_THRESHOLD) {\n valueHash = hashUint8Array(input)\n } else {\n // Deeply hashing large arrays would be too costly\n // so we track them by reference and cache them in a weak map\n return cachedReferenceHash(input)\n }\n } else if (input instanceof File) {\n // Files are always hashed by reference due to their potentially large size\n return cachedReferenceHash(input)\n } else if (isTemporal(input)) {\n valueHash = hashTemporal(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 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 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(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":["randomHash","MurmurHashStream"],"mappings":"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"}
|
package/dist/cjs/utils.cjs
CHANGED
|
@@ -114,16 +114,133 @@ function compareKeys(a, b) {
|
|
|
114
114
|
return typeof a === `string` ? -1 : 1;
|
|
115
115
|
}
|
|
116
116
|
function serializeValue(value) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
if (isJsonSafeStructuralValue(value, /* @__PURE__ */ new Set())) {
|
|
118
|
+
return JSON.stringify(toStableJsonValue(value));
|
|
119
|
+
}
|
|
120
|
+
return `~${JSON.stringify(toCanonicalValue(value, /* @__PURE__ */ new Set()))}`;
|
|
121
|
+
}
|
|
122
|
+
function isJsonSafeStructuralValue(value, ancestors) {
|
|
123
|
+
if (value === null) return true;
|
|
124
|
+
switch (typeof value) {
|
|
125
|
+
case `boolean`:
|
|
126
|
+
case `string`:
|
|
127
|
+
return true;
|
|
128
|
+
case `number`:
|
|
129
|
+
return Number.isFinite(value);
|
|
130
|
+
case `undefined`:
|
|
131
|
+
case `bigint`:
|
|
132
|
+
case `symbol`:
|
|
133
|
+
case `function`:
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return withAcyclicValue(value, ancestors, () => {
|
|
137
|
+
if (value instanceof Date || value instanceof RegExp || value instanceof Uint8Array || value instanceof Map || value instanceof Set) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return Array.isArray(value) ? value.every((item) => isJsonSafeStructuralValue(item, ancestors)) : Object.keys(value).every(
|
|
141
|
+
(key) => isJsonSafeStructuralValue(
|
|
142
|
+
value[key],
|
|
143
|
+
ancestors
|
|
144
|
+
)
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function toStableJsonValue(value) {
|
|
149
|
+
if (value === null || typeof value === `boolean` || typeof value === `number` || typeof value === `string`) {
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
if (Array.isArray(value)) {
|
|
153
|
+
return value.map(toStableJsonValue);
|
|
154
|
+
}
|
|
155
|
+
return Object.fromEntries(
|
|
156
|
+
Object.keys(value).sort().map((key) => [
|
|
157
|
+
key,
|
|
158
|
+
toStableJsonValue(value[key])
|
|
159
|
+
])
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
function toCanonicalValue(value, ancestors) {
|
|
163
|
+
if (value === void 0) return [`undefined`];
|
|
164
|
+
if (value === null) return [`null`];
|
|
165
|
+
switch (typeof value) {
|
|
166
|
+
case `boolean`:
|
|
167
|
+
return [`boolean`, value];
|
|
168
|
+
case `number`:
|
|
169
|
+
if (Number.isNaN(value)) return [`number`, `NaN`];
|
|
170
|
+
if (value === Infinity) return [`number`, `Infinity`];
|
|
171
|
+
if (value === -Infinity) return [`number`, `-Infinity`];
|
|
172
|
+
return [`number`, value === 0 ? 0 : value];
|
|
173
|
+
case `bigint`:
|
|
174
|
+
return [`bigint`, value.toString()];
|
|
175
|
+
case `string`:
|
|
176
|
+
return [`string`, value];
|
|
177
|
+
case `symbol`:
|
|
178
|
+
case `function`:
|
|
179
|
+
throw new TypeError(
|
|
180
|
+
`Cannot serialize ${typeof value} as a structural relation key`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return withAcyclicValue(value, ancestors, () => {
|
|
184
|
+
if (value instanceof Date) {
|
|
185
|
+
const timestamp = value.getTime();
|
|
186
|
+
return Number.isNaN(timestamp) ? [`date`, `Invalid`] : [`date`, timestamp];
|
|
187
|
+
}
|
|
188
|
+
if (value instanceof RegExp) {
|
|
189
|
+
return [`regexp`, value.source, value.flags];
|
|
190
|
+
}
|
|
191
|
+
if (value instanceof Uint8Array) {
|
|
192
|
+
return [`bytes`, Array.from(value)];
|
|
193
|
+
}
|
|
194
|
+
if (Array.isArray(value)) {
|
|
195
|
+
return [`array`, value.map((item) => toCanonicalValue(item, ancestors))];
|
|
196
|
+
}
|
|
197
|
+
if (value instanceof Map) {
|
|
198
|
+
const entries2 = [...value.entries()].map(
|
|
199
|
+
([key, entryValue]) => [
|
|
200
|
+
toCanonicalValue(key, ancestors),
|
|
201
|
+
toCanonicalValue(entryValue, ancestors)
|
|
202
|
+
]
|
|
203
|
+
);
|
|
204
|
+
entries2.sort(
|
|
205
|
+
(left, right) => compareSerializedValues(JSON.stringify(left), JSON.stringify(right))
|
|
206
|
+
);
|
|
207
|
+
return [`map`, entries2];
|
|
120
208
|
}
|
|
121
|
-
if (
|
|
122
|
-
|
|
209
|
+
if (value instanceof Set) {
|
|
210
|
+
const entries2 = [...value].map(
|
|
211
|
+
(entry) => toCanonicalValue(entry, ancestors)
|
|
212
|
+
);
|
|
213
|
+
entries2.sort(
|
|
214
|
+
(left, right) => compareSerializedValues(JSON.stringify(left), JSON.stringify(right))
|
|
215
|
+
);
|
|
216
|
+
return [`set`, entries2];
|
|
123
217
|
}
|
|
124
|
-
|
|
218
|
+
const entries = Object.keys(value).sort().map(
|
|
219
|
+
(key) => [
|
|
220
|
+
key,
|
|
221
|
+
toCanonicalValue(
|
|
222
|
+
value[key],
|
|
223
|
+
ancestors
|
|
224
|
+
)
|
|
225
|
+
]
|
|
226
|
+
);
|
|
227
|
+
return [`object`, entries];
|
|
125
228
|
});
|
|
126
229
|
}
|
|
230
|
+
function compareSerializedValues(left, right) {
|
|
231
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
232
|
+
}
|
|
233
|
+
function withAcyclicValue(value, ancestors, encode) {
|
|
234
|
+
if (ancestors.has(value)) {
|
|
235
|
+
throw new TypeError(`Cannot serialize a cyclic structural relation key`);
|
|
236
|
+
}
|
|
237
|
+
ancestors.add(value);
|
|
238
|
+
try {
|
|
239
|
+
return encode();
|
|
240
|
+
} finally {
|
|
241
|
+
ancestors.delete(value);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
127
244
|
exports.DefaultMap = DefaultMap;
|
|
128
245
|
exports.ObjectIdGenerator = ObjectIdGenerator;
|
|
129
246
|
exports.binarySearch = binarySearch;
|
package/dist/cjs/utils.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.cjs","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Simple assertion function for runtime checks.\n * Throws an error if the condition is false.\n */\nexport function assert(\n condition: unknown,\n message?: string,\n): asserts condition {\n if (!condition) {\n throw new Error(message || `Assertion failed`)\n }\n}\n\n/**\n * A map that returns a default value for keys that are not present.\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n constructor(\n private defaultValue: () => V,\n entries?: Iterable<[K, V]>,\n ) {\n super(entries)\n }\n\n get(key: K): V {\n if (!this.has(key)) {\n // this.set(key, this.defaultValue())\n return this.defaultValue()\n }\n return super.get(key)!\n }\n\n /**\n * Update the value for a key using a function.\n */\n update(key: K, updater: (value: V) => V): V {\n const value = this.get(key)\n const newValue = updater(value)\n this.set(key, newValue)\n return newValue\n }\n}\n\n// JS engines have various limits on how many args can be passed to a function\n// with a spread operator, so we need to split the operation into chunks\n// 32767 is the max for Chrome 14, all others are higher\n// TODO: investigate the performance of this and other approaches\nconst chunkSize = 30000\nexport function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>) {\n if (other.length <= chunkSize) {\n array.push(...other)\n } else {\n for (let i = 0; i < other.length; i += chunkSize) {\n const chunk = other.slice(i, i + chunkSize)\n array.push(...chunk)\n }\n }\n}\n\nexport function binarySearch<T>(\n array: Array<T>,\n value: T,\n comparator: (a: T, b: T) => number,\n): number {\n let low = 0\n let high = array.length\n while (low < high) {\n const mid = Math.floor((low + high) / 2)\n const comparison = comparator(array[mid]!, value)\n if (comparison < 0) {\n low = mid + 1\n } else if (comparison > 0) {\n high = mid\n } else {\n return mid\n }\n }\n return low\n}\n\n/**\n * Utility for generating unique IDs for objects and values.\n * Uses WeakMap for object reference tracking and consistent hashing for primitives.\n */\nexport class ObjectIdGenerator {\n private objectIds = new WeakMap<object, number>()\n private nextId = 0\n\n /**\n * Get a unique identifier for any value.\n * - Objects: Uses WeakMap for reference-based identity\n * - Primitives: Uses consistent string-based hashing\n */\n getId(value: any): number {\n // For primitives, use a simple hash of their string representation\n if (typeof value !== `object` || value === null) {\n const str = String(value)\n let hashValue = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hashValue = (hashValue << 5) - hashValue + char\n hashValue = hashValue & hashValue // Convert to 32-bit integer\n }\n return hashValue\n }\n\n // For objects, use WeakMap to assign unique IDs\n if (!this.objectIds.has(value)) {\n this.objectIds.set(value, this.nextId++)\n }\n return this.objectIds.get(value)!\n }\n\n /**\n * Get a string representation of the ID for use in composite keys.\n */\n getStringId(value: any): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n if (typeof value !== `object`) return `str_${String(value)}`\n\n return `obj_${this.getId(value)}`\n }\n}\n\n/**\n * Global instance for cases where a shared object ID space is needed.\n */\nexport const globalObjectIdGenerator = new ObjectIdGenerator()\n\nexport function* concatIterable<T>(\n ...iterables: Array<Iterable<T>>\n): Iterable<T> {\n for (const iterable of iterables) {\n yield* iterable\n }\n}\n\nexport function* mapIterable<T, U>(\n it: Iterable<T>,\n fn: (t: T) => U,\n): Iterable<U> {\n for (const t of it) {\n yield fn(t)\n }\n}\n\nexport type HRange = [number, number] // half-open [start, end[ i.e. end is exclusive\n\n/**\n * Computes the difference between two half-open ranges.\n * @param a - The first half-open range\n * @param b - The second half-open range\n * @returns The difference between the two ranges\n */\nexport function diffHalfOpen(a: HRange, b: HRange) {\n const [a1, a2] = a\n const [b1, b2] = b\n\n // A \\ B can be up to two segments (left and right of the overlap)\n const onlyInA: Array<number> = [\n ...range(a1, Math.min(a2, b1)), // left side of A outside B\n ...range(Math.max(a1, b2), a2), // right side of A outside B\n ]\n\n // B \\ A similarly\n const onlyInB: Array<number> = [\n ...range(b1, Math.min(b2, a1)),\n ...range(Math.max(b1, a2), b2),\n ]\n\n return { onlyInA, onlyInB }\n}\n\nfunction range(start: number, end: number): Array<number> {\n const out: Array<number> = []\n for (let i = start; i < end; i++) out.push(i)\n return out\n}\n\n/**\n * Compares two keys (string | number) in a consistent, deterministic way.\n * Handles mixed types by ordering strings before numbers.\n */\nexport function compareKeys(a: string | number, b: string | number): number {\n // Same type: compare directly\n if (typeof a === typeof b) {\n if (a < b) return -1\n if (a > b) return 1\n return 0\n }\n // Different types: strings come before numbers\n return typeof a === `string` ? -1 : 1\n}\n\n/**\n * Serializes a value for use as a key, handling BigInt and Date values that JSON.stringify cannot handle.\n * Uses JSON.stringify with a replacer function to convert BigInt values to strings and Date values to ISO strings.\n * This is used for creating string keys in groupBy operations.\n */\nexport function serializeValue(value: unknown): string {\n return JSON.stringify(value, (_, val) => {\n if (typeof val === 'bigint') {\n return val.toString()\n }\n if (val instanceof Date) {\n return val.toISOString()\n }\n return val\n })\n}\n"],"names":[],"mappings":";;AAgBO,MAAM,mBAAyB,IAAU;AAAA,EAC9C,YACU,cACR,SACA;AACA,UAAM,OAAO;AAHL,SAAA,eAAA;AAAA,EAIV;AAAA,EAEA,IAAI,KAAW;AACb,QAAI,CAAC,KAAK,IAAI,GAAG,GAAG;AAElB,aAAO,KAAK,aAAA;AAAA,IACd;AACA,WAAO,MAAM,IAAI,GAAG;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,KAAQ,SAA6B;AAC1C,UAAM,QAAQ,KAAK,IAAI,GAAG;AAC1B,UAAM,WAAW,QAAQ,KAAK;AAC9B,SAAK,IAAI,KAAK,QAAQ;AACtB,WAAO;AAAA,EACT;AACF;AAMA,MAAM,YAAY;AACX,SAAS,iBAAiB,OAAuB,OAAuB;AAC7E,MAAI,MAAM,UAAU,WAAW;AAC7B,UAAM,KAAK,GAAG,KAAK;AAAA,EACrB,OAAO;AACL,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,WAAW;AAChD,YAAM,QAAQ,MAAM,MAAM,GAAG,IAAI,SAAS;AAC1C,YAAM,KAAK,GAAG,KAAK;AAAA,IACrB;AAAA,EACF;AACF;AAEO,SAAS,aACd,OACA,OACA,YACQ;AACR,MAAI,MAAM;AACV,MAAI,OAAO,MAAM;AACjB,SAAO,MAAM,MAAM;AACjB,UAAM,MAAM,KAAK,OAAO,MAAM,QAAQ,CAAC;AACvC,UAAM,aAAa,WAAW,MAAM,GAAG,GAAI,KAAK;AAChD,QAAI,aAAa,GAAG;AAClB,YAAM,MAAM;AAAA,IACd,WAAW,aAAa,GAAG;AACzB,aAAO;AAAA,IACT,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAMO,MAAM,kBAAkB;AAAA,EAAxB,cAAA;AACL,SAAQ,gCAAgB,QAAA;AACxB,SAAQ,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOjB,MAAM,OAAoB;AAExB,QAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,YAAM,MAAM,OAAO,KAAK;AACxB,UAAI,YAAY;AAChB,eAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,cAAM,OAAO,IAAI,WAAW,CAAC;AAC7B,qBAAa,aAAa,KAAK,YAAY;AAC3C,oBAAY,YAAY;AAAA,MAC1B;AACA,aAAO;AAAA,IACT;AAGA,QAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAAG;AAC9B,WAAK,UAAU,IAAI,OAAO,KAAK,QAAQ;AAAA,IACzC;AACA,WAAO,KAAK,UAAU,IAAI,KAAK;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,OAAoB;AAC9B,QAAI,UAAU,KAAM,QAAO;AAC3B,QAAI,UAAU,OAAW,QAAO;AAChC,QAAI,OAAO,UAAU,iBAAiB,OAAO,OAAO,KAAK,CAAC;AAE1D,WAAO,OAAO,KAAK,MAAM,KAAK,CAAC;AAAA,EACjC;AACF;AAKO,MAAM,0BAA0B,IAAI,kBAAA;AA2BpC,SAAS,aAAa,GAAW,GAAW;AACjD,QAAM,CAAC,IAAI,EAAE,IAAI;AACjB,QAAM,CAAC,IAAI,EAAE,IAAI;AAGjB,QAAM,UAAyB;AAAA,IAC7B,GAAG,MAAM,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;AAAA;AAAA,IAC7B,GAAG,MAAM,KAAK,IAAI,IAAI,EAAE,GAAG,EAAE;AAAA;AAAA,EAAA;AAI/B,QAAM,UAAyB;AAAA,IAC7B,GAAG,MAAM,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;AAAA,IAC7B,GAAG,MAAM,KAAK,IAAI,IAAI,EAAE,GAAG,EAAE;AAAA,EAAA;AAG/B,SAAO,EAAE,SAAS,QAAA;AACpB;AAEA,SAAS,MAAM,OAAe,KAA4B;AACxD,QAAM,MAAqB,CAAA;AAC3B,WAAS,IAAI,OAAO,IAAI,KAAK,IAAK,KAAI,KAAK,CAAC;AAC5C,SAAO;AACT;AAMO,SAAS,YAAY,GAAoB,GAA4B;AAE1E,MAAI,OAAO,MAAM,OAAO,GAAG;AACzB,QAAI,IAAI,EAAG,QAAO;AAClB,QAAI,IAAI,EAAG,QAAO;AAClB,WAAO;AAAA,EACT;AAEA,SAAO,OAAO,MAAM,WAAW,KAAK;AACtC;AAOO,SAAS,eAAe,OAAwB;AACrD,SAAO,KAAK,UAAU,OAAO,CAAC,GAAG,QAAQ;AACvC,QAAI,OAAO,QAAQ,UAAU;AAC3B,aAAO,IAAI,SAAA;AAAA,IACb;AACA,QAAI,eAAe,MAAM;AACvB,aAAO,IAAI,YAAA;AAAA,IACb;AACA,WAAO;AAAA,EACT,CAAC;AACH;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"utils.cjs","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Simple assertion function for runtime checks.\n * Throws an error if the condition is false.\n */\nexport function assert(\n condition: unknown,\n message?: string,\n): asserts condition {\n if (!condition) {\n throw new Error(message || `Assertion failed`)\n }\n}\n\n/**\n * A map that returns a default value for keys that are not present.\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n constructor(\n private defaultValue: () => V,\n entries?: Iterable<[K, V]>,\n ) {\n super(entries)\n }\n\n get(key: K): V {\n if (!this.has(key)) {\n // this.set(key, this.defaultValue())\n return this.defaultValue()\n }\n return super.get(key)!\n }\n\n /**\n * Update the value for a key using a function.\n */\n update(key: K, updater: (value: V) => V): V {\n const value = this.get(key)\n const newValue = updater(value)\n this.set(key, newValue)\n return newValue\n }\n}\n\n// JS engines have various limits on how many args can be passed to a function\n// with a spread operator, so we need to split the operation into chunks\n// 32767 is the max for Chrome 14, all others are higher\n// TODO: investigate the performance of this and other approaches\nconst chunkSize = 30000\nexport function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>) {\n if (other.length <= chunkSize) {\n array.push(...other)\n } else {\n for (let i = 0; i < other.length; i += chunkSize) {\n const chunk = other.slice(i, i + chunkSize)\n array.push(...chunk)\n }\n }\n}\n\nexport function binarySearch<T>(\n array: Array<T>,\n value: T,\n comparator: (a: T, b: T) => number,\n): number {\n let low = 0\n let high = array.length\n while (low < high) {\n const mid = Math.floor((low + high) / 2)\n const comparison = comparator(array[mid]!, value)\n if (comparison < 0) {\n low = mid + 1\n } else if (comparison > 0) {\n high = mid\n } else {\n return mid\n }\n }\n return low\n}\n\n/**\n * Utility for generating unique IDs for objects and values.\n * Uses WeakMap for object reference tracking and consistent hashing for primitives.\n */\nexport class ObjectIdGenerator {\n private objectIds = new WeakMap<object, number>()\n private nextId = 0\n\n /**\n * Get a unique identifier for any value.\n * - Objects: Uses WeakMap for reference-based identity\n * - Primitives: Uses consistent string-based hashing\n */\n getId(value: any): number {\n // For primitives, use a simple hash of their string representation\n if (typeof value !== `object` || value === null) {\n const str = String(value)\n let hashValue = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hashValue = (hashValue << 5) - hashValue + char\n hashValue = hashValue & hashValue // Convert to 32-bit integer\n }\n return hashValue\n }\n\n // For objects, use WeakMap to assign unique IDs\n if (!this.objectIds.has(value)) {\n this.objectIds.set(value, this.nextId++)\n }\n return this.objectIds.get(value)!\n }\n\n /**\n * Get a string representation of the ID for use in composite keys.\n */\n getStringId(value: any): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n if (typeof value !== `object`) return `str_${String(value)}`\n\n return `obj_${this.getId(value)}`\n }\n}\n\n/**\n * Global instance for cases where a shared object ID space is needed.\n */\nexport const globalObjectIdGenerator = new ObjectIdGenerator()\n\nexport function* concatIterable<T>(\n ...iterables: Array<Iterable<T>>\n): Iterable<T> {\n for (const iterable of iterables) {\n yield* iterable\n }\n}\n\nexport function* mapIterable<T, U>(\n it: Iterable<T>,\n fn: (t: T) => U,\n): Iterable<U> {\n for (const t of it) {\n yield fn(t)\n }\n}\n\nexport type HRange = [number, number] // half-open [start, end[ i.e. end is exclusive\n\n/**\n * Computes the difference between two half-open ranges.\n * @param a - The first half-open range\n * @param b - The second half-open range\n * @returns The difference between the two ranges\n */\nexport function diffHalfOpen(a: HRange, b: HRange) {\n const [a1, a2] = a\n const [b1, b2] = b\n\n // A \\ B can be up to two segments (left and right of the overlap)\n const onlyInA: Array<number> = [\n ...range(a1, Math.min(a2, b1)), // left side of A outside B\n ...range(Math.max(a1, b2), a2), // right side of A outside B\n ]\n\n // B \\ A similarly\n const onlyInB: Array<number> = [\n ...range(b1, Math.min(b2, a1)),\n ...range(Math.max(b1, a2), b2),\n ]\n\n return { onlyInA, onlyInB }\n}\n\nfunction range(start: number, end: number): Array<number> {\n const out: Array<number> = []\n for (let i = start; i < end; i++) out.push(i)\n return out\n}\n\n/**\n * Compares two keys (string | number) in a consistent, deterministic way.\n * Handles mixed types by ordering strings before numbers.\n */\nexport function compareKeys(a: string | number, b: string | number): number {\n // Same type: compare directly\n if (typeof a === typeof b) {\n if (a < b) return -1\n if (a > b) return 1\n return 0\n }\n // Different types: strings come before numbers\n return typeof a === `string` ? -1 : 1\n}\n\ntype CanonicalValue =\n | readonly [`undefined`]\n | readonly [`null`]\n | readonly [`boolean`, boolean]\n | readonly [`number`, number | `NaN` | `Infinity` | `-Infinity`]\n | readonly [`bigint`, string]\n | readonly [`string`, string]\n | readonly [`date`, number | `Invalid`]\n | readonly [`regexp`, string, string]\n | readonly [`bytes`, Array<number>]\n | readonly [`array`, Array<CanonicalValue>]\n | readonly [`map`, Array<readonly [CanonicalValue, CanonicalValue]>]\n | readonly [`set`, Array<CanonicalValue>]\n | readonly [`object`, Array<readonly [string, CanonicalValue]>]\n\n/**\n * Serializes a supported query value into one canonical key.\n *\n * JSON's native encoding is not suitable for relation keys: it merges BigInt\n * with strings when a replacer is used, merges NaN with null, drops undefined,\n * and depends on object insertion order. Ordinary JSON values keep their\n * established wire form. Values that need richer types use a reserved prefix\n * plus a structural, type-tagged encoding.\n */\nexport function serializeValue(value: unknown): string {\n if (isJsonSafeStructuralValue(value, new Set())) {\n return JSON.stringify(toStableJsonValue(value))\n }\n\n return `~${JSON.stringify(toCanonicalValue(value, new Set()))}`\n}\n\ntype JsonValue =\n | null\n | boolean\n | number\n | string\n | Array<JsonValue>\n | { [key: string]: JsonValue }\n\nfunction isJsonSafeStructuralValue(\n value: unknown,\n ancestors: Set<object>,\n): boolean {\n if (value === null) return true\n\n switch (typeof value) {\n case `boolean`:\n case `string`:\n return true\n case `number`:\n return Number.isFinite(value)\n case `undefined`:\n case `bigint`:\n case `symbol`:\n case `function`:\n return false\n }\n\n return withAcyclicValue(value, ancestors, () => {\n if (\n value instanceof Date ||\n value instanceof RegExp ||\n value instanceof Uint8Array ||\n value instanceof Map ||\n value instanceof Set\n ) {\n return false\n }\n\n return Array.isArray(value)\n ? value.every((item) => isJsonSafeStructuralValue(item, ancestors))\n : Object.keys(value).every((key) =>\n isJsonSafeStructuralValue(\n (value as Record<string, unknown>)[key],\n ancestors,\n ),\n )\n })\n}\n\nfunction toStableJsonValue(value: unknown): JsonValue {\n if (\n value === null ||\n typeof value === `boolean` ||\n typeof value === `number` ||\n typeof value === `string`\n ) {\n return value\n }\n\n if (Array.isArray(value)) {\n return value.map(toStableJsonValue)\n }\n\n return Object.fromEntries(\n Object.keys(value as object)\n .sort()\n .map((key) => [\n key,\n toStableJsonValue((value as Record<string, unknown>)[key]),\n ]),\n )\n}\n\nfunction toCanonicalValue(\n value: unknown,\n ancestors: Set<object>,\n): CanonicalValue {\n if (value === undefined) return [`undefined`]\n if (value === null) return [`null`]\n\n switch (typeof value) {\n case `boolean`:\n return [`boolean`, value]\n case `number`:\n if (Number.isNaN(value)) return [`number`, `NaN`]\n if (value === Infinity) return [`number`, `Infinity`]\n if (value === -Infinity) return [`number`, `-Infinity`]\n return [`number`, value === 0 ? 0 : value]\n case `bigint`:\n return [`bigint`, value.toString()]\n case `string`:\n return [`string`, value]\n case `symbol`:\n case `function`:\n throw new TypeError(\n `Cannot serialize ${typeof value} as a structural relation key`,\n )\n }\n\n return withAcyclicValue(value, ancestors, () => {\n if (value instanceof Date) {\n const timestamp = value.getTime()\n return Number.isNaN(timestamp)\n ? ([`date`, `Invalid`] as const)\n : ([`date`, timestamp] as const)\n }\n\n if (value instanceof RegExp) {\n return [`regexp`, value.source, value.flags]\n }\n\n if (value instanceof Uint8Array) {\n return [`bytes`, Array.from(value)]\n }\n\n if (Array.isArray(value)) {\n return [`array`, value.map((item) => toCanonicalValue(item, ancestors))]\n }\n\n if (value instanceof Map) {\n const entries = [...value.entries()].map(\n ([key, entryValue]) =>\n [\n toCanonicalValue(key, ancestors),\n toCanonicalValue(entryValue, ancestors),\n ] as const,\n )\n entries.sort((left, right) =>\n compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),\n )\n return [`map`, entries]\n }\n\n if (value instanceof Set) {\n const entries = [...value].map((entry) =>\n toCanonicalValue(entry, ancestors),\n )\n entries.sort((left, right) =>\n compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),\n )\n return [`set`, entries]\n }\n\n const entries = Object.keys(value)\n .sort()\n .map(\n (key) =>\n [\n key,\n toCanonicalValue(\n (value as Record<string, unknown>)[key],\n ancestors,\n ),\n ] as const,\n )\n return [`object`, entries]\n })\n}\n\nfunction compareSerializedValues(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0\n}\n\nfunction withAcyclicValue<T>(\n value: object,\n ancestors: Set<object>,\n encode: () => T,\n): T {\n if (ancestors.has(value)) {\n throw new TypeError(`Cannot serialize a cyclic structural relation key`)\n }\n\n ancestors.add(value)\n try {\n return encode()\n } finally {\n ancestors.delete(value)\n }\n}\n"],"names":["entries"],"mappings":"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"}
|
package/dist/cjs/utils.d.cts
CHANGED
|
@@ -58,8 +58,12 @@ export declare function diffHalfOpen(a: HRange, b: HRange): {
|
|
|
58
58
|
*/
|
|
59
59
|
export declare function compareKeys(a: string | number, b: string | number): number;
|
|
60
60
|
/**
|
|
61
|
-
* Serializes a value
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* Serializes a supported query value into one canonical key.
|
|
62
|
+
*
|
|
63
|
+
* JSON's native encoding is not suitable for relation keys: it merges BigInt
|
|
64
|
+
* with strings when a replacer is used, merges NaN with null, drops undefined,
|
|
65
|
+
* and depends on object insertion order. Ordinary JSON values keep their
|
|
66
|
+
* established wire form. Values that need richer types use a reserved prefix
|
|
67
|
+
* plus a structural, type-tagged encoding.
|
|
64
68
|
*/
|
|
65
69
|
export declare function serializeValue(value: unknown): string;
|
package/dist/esm/hashing/hash.js
CHANGED
|
@@ -11,6 +11,21 @@ const ARRAY_MARKER = randomHash();
|
|
|
11
11
|
const MAP_MARKER = randomHash();
|
|
12
12
|
const SET_MARKER = randomHash();
|
|
13
13
|
const UINT8ARRAY_MARKER = randomHash();
|
|
14
|
+
const TEMPORAL_MARKER = randomHash();
|
|
15
|
+
const temporalTypes = /* @__PURE__ */ new Set([
|
|
16
|
+
`Temporal.Duration`,
|
|
17
|
+
`Temporal.Instant`,
|
|
18
|
+
`Temporal.PlainDate`,
|
|
19
|
+
`Temporal.PlainDateTime`,
|
|
20
|
+
`Temporal.PlainMonthDay`,
|
|
21
|
+
`Temporal.PlainTime`,
|
|
22
|
+
`Temporal.PlainYearMonth`,
|
|
23
|
+
`Temporal.ZonedDateTime`
|
|
24
|
+
]);
|
|
25
|
+
function isTemporal(input) {
|
|
26
|
+
const tag = input[Symbol.toStringTag];
|
|
27
|
+
return typeof tag === `string` && temporalTypes.has(tag);
|
|
28
|
+
}
|
|
14
29
|
const UINT8ARRAY_CONTENT_HASH_THRESHOLD = 128;
|
|
15
30
|
const hashCache = /* @__PURE__ */ new WeakMap();
|
|
16
31
|
function hash(input) {
|
|
@@ -37,6 +52,8 @@ function hashObject(input) {
|
|
|
37
52
|
}
|
|
38
53
|
} else if (input instanceof File) {
|
|
39
54
|
return cachedReferenceHash(input);
|
|
55
|
+
} else if (isTemporal(input)) {
|
|
56
|
+
valueHash = hashTemporal(input);
|
|
40
57
|
} else {
|
|
41
58
|
let plainObjectInput = input;
|
|
42
59
|
let marker = OBJECT_MARKER;
|
|
@@ -71,6 +88,13 @@ function hashUint8Array(input) {
|
|
|
71
88
|
}
|
|
72
89
|
return hasher.digest();
|
|
73
90
|
}
|
|
91
|
+
function hashTemporal(input) {
|
|
92
|
+
const hasher = new MurmurHashStream();
|
|
93
|
+
hasher.update(TEMPORAL_MARKER);
|
|
94
|
+
hasher.update(input[Symbol.toStringTag]);
|
|
95
|
+
hasher.update(input.toString());
|
|
96
|
+
return hasher.digest();
|
|
97
|
+
}
|
|
74
98
|
function hashPlainObject(input, marker) {
|
|
75
99
|
const hasher = new MurmurHashStream();
|
|
76
100
|
hasher.update(marker);
|
|
@@ -1 +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()\nconst UINT8ARRAY_MARKER = randomHash()\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>()\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 if (\n // Check if input is a Uint8Array or Buffer\n (typeof Buffer !== `undefined` && input instanceof Buffer) ||\n input instanceof Uint8Array\n ) {\n // For small Uint8Arrays/Buffers (e.g., ULIDs, UUIDs), hash by content\n // to enable proper equality comparisons. For large arrays, hash by reference\n // to avoid performance costs.\n if (input.byteLength <= UINT8ARRAY_CONTENT_HASH_THRESHOLD) {\n valueHash = hashUint8Array(input)\n } else {\n // Deeply hashing large arrays would be too costly\n // so we track them by reference and cache them in a weak map\n return cachedReferenceHash(input)\n }\n } else if (input instanceof File) {\n // Files are always hashed by reference due to their potentially large size\n return cachedReferenceHash(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 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 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 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":";AAQA,MAAM,OAAO,WAAA;AACb,MAAM,QAAQ,WAAA;AACd,MAAM,OAAO,WAAA;AACb,MAAM,YAAY,WAAA;AAClB,MAAM,MAAM,WAAA;AACZ,MAAM,YAAY,WAAA;AAClB,MAAM,cAAc,WAAA;AACpB,MAAM,gBAAgB,WAAA;AACtB,MAAM,eAAe,WAAA;AACrB,MAAM,aAAa,WAAA;AACnB,MAAM,aAAa,WAAA;AACnB,MAAM,oBAAoB,WAAA;
|
|
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()\nconst UINT8ARRAY_MARKER = randomHash()\nconst TEMPORAL_MARKER = randomHash()\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>()\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 if (\n // Check if input is a Uint8Array or Buffer\n (typeof Buffer !== `undefined` && input instanceof Buffer) ||\n input instanceof Uint8Array\n ) {\n // For small Uint8Arrays/Buffers (e.g., ULIDs, UUIDs), hash by content\n // to enable proper equality comparisons. For large arrays, hash by reference\n // to avoid performance costs.\n if (input.byteLength <= UINT8ARRAY_CONTENT_HASH_THRESHOLD) {\n valueHash = hashUint8Array(input)\n } else {\n // Deeply hashing large arrays would be too costly\n // so we track them by reference and cache them in a weak map\n return cachedReferenceHash(input)\n }\n } else if (input instanceof File) {\n // Files are always hashed by reference due to their potentially large size\n return cachedReferenceHash(input)\n } else if (isTemporal(input)) {\n valueHash = hashTemporal(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 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 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(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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"}
|
package/dist/esm/utils.d.ts
CHANGED
|
@@ -58,8 +58,12 @@ export declare function diffHalfOpen(a: HRange, b: HRange): {
|
|
|
58
58
|
*/
|
|
59
59
|
export declare function compareKeys(a: string | number, b: string | number): number;
|
|
60
60
|
/**
|
|
61
|
-
* Serializes a value
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* Serializes a supported query value into one canonical key.
|
|
62
|
+
*
|
|
63
|
+
* JSON's native encoding is not suitable for relation keys: it merges BigInt
|
|
64
|
+
* with strings when a replacer is used, merges NaN with null, drops undefined,
|
|
65
|
+
* and depends on object insertion order. Ordinary JSON values keep their
|
|
66
|
+
* established wire form. Values that need richer types use a reserved prefix
|
|
67
|
+
* plus a structural, type-tagged encoding.
|
|
64
68
|
*/
|
|
65
69
|
export declare function serializeValue(value: unknown): string;
|
package/dist/esm/utils.js
CHANGED
|
@@ -112,16 +112,133 @@ function compareKeys(a, b) {
|
|
|
112
112
|
return typeof a === `string` ? -1 : 1;
|
|
113
113
|
}
|
|
114
114
|
function serializeValue(value) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
if (isJsonSafeStructuralValue(value, /* @__PURE__ */ new Set())) {
|
|
116
|
+
return JSON.stringify(toStableJsonValue(value));
|
|
117
|
+
}
|
|
118
|
+
return `~${JSON.stringify(toCanonicalValue(value, /* @__PURE__ */ new Set()))}`;
|
|
119
|
+
}
|
|
120
|
+
function isJsonSafeStructuralValue(value, ancestors) {
|
|
121
|
+
if (value === null) return true;
|
|
122
|
+
switch (typeof value) {
|
|
123
|
+
case `boolean`:
|
|
124
|
+
case `string`:
|
|
125
|
+
return true;
|
|
126
|
+
case `number`:
|
|
127
|
+
return Number.isFinite(value);
|
|
128
|
+
case `undefined`:
|
|
129
|
+
case `bigint`:
|
|
130
|
+
case `symbol`:
|
|
131
|
+
case `function`:
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
return withAcyclicValue(value, ancestors, () => {
|
|
135
|
+
if (value instanceof Date || value instanceof RegExp || value instanceof Uint8Array || value instanceof Map || value instanceof Set) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return Array.isArray(value) ? value.every((item) => isJsonSafeStructuralValue(item, ancestors)) : Object.keys(value).every(
|
|
139
|
+
(key) => isJsonSafeStructuralValue(
|
|
140
|
+
value[key],
|
|
141
|
+
ancestors
|
|
142
|
+
)
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function toStableJsonValue(value) {
|
|
147
|
+
if (value === null || typeof value === `boolean` || typeof value === `number` || typeof value === `string`) {
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(value)) {
|
|
151
|
+
return value.map(toStableJsonValue);
|
|
152
|
+
}
|
|
153
|
+
return Object.fromEntries(
|
|
154
|
+
Object.keys(value).sort().map((key) => [
|
|
155
|
+
key,
|
|
156
|
+
toStableJsonValue(value[key])
|
|
157
|
+
])
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
function toCanonicalValue(value, ancestors) {
|
|
161
|
+
if (value === void 0) return [`undefined`];
|
|
162
|
+
if (value === null) return [`null`];
|
|
163
|
+
switch (typeof value) {
|
|
164
|
+
case `boolean`:
|
|
165
|
+
return [`boolean`, value];
|
|
166
|
+
case `number`:
|
|
167
|
+
if (Number.isNaN(value)) return [`number`, `NaN`];
|
|
168
|
+
if (value === Infinity) return [`number`, `Infinity`];
|
|
169
|
+
if (value === -Infinity) return [`number`, `-Infinity`];
|
|
170
|
+
return [`number`, value === 0 ? 0 : value];
|
|
171
|
+
case `bigint`:
|
|
172
|
+
return [`bigint`, value.toString()];
|
|
173
|
+
case `string`:
|
|
174
|
+
return [`string`, value];
|
|
175
|
+
case `symbol`:
|
|
176
|
+
case `function`:
|
|
177
|
+
throw new TypeError(
|
|
178
|
+
`Cannot serialize ${typeof value} as a structural relation key`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return withAcyclicValue(value, ancestors, () => {
|
|
182
|
+
if (value instanceof Date) {
|
|
183
|
+
const timestamp = value.getTime();
|
|
184
|
+
return Number.isNaN(timestamp) ? [`date`, `Invalid`] : [`date`, timestamp];
|
|
185
|
+
}
|
|
186
|
+
if (value instanceof RegExp) {
|
|
187
|
+
return [`regexp`, value.source, value.flags];
|
|
188
|
+
}
|
|
189
|
+
if (value instanceof Uint8Array) {
|
|
190
|
+
return [`bytes`, Array.from(value)];
|
|
191
|
+
}
|
|
192
|
+
if (Array.isArray(value)) {
|
|
193
|
+
return [`array`, value.map((item) => toCanonicalValue(item, ancestors))];
|
|
194
|
+
}
|
|
195
|
+
if (value instanceof Map) {
|
|
196
|
+
const entries2 = [...value.entries()].map(
|
|
197
|
+
([key, entryValue]) => [
|
|
198
|
+
toCanonicalValue(key, ancestors),
|
|
199
|
+
toCanonicalValue(entryValue, ancestors)
|
|
200
|
+
]
|
|
201
|
+
);
|
|
202
|
+
entries2.sort(
|
|
203
|
+
(left, right) => compareSerializedValues(JSON.stringify(left), JSON.stringify(right))
|
|
204
|
+
);
|
|
205
|
+
return [`map`, entries2];
|
|
118
206
|
}
|
|
119
|
-
if (
|
|
120
|
-
|
|
207
|
+
if (value instanceof Set) {
|
|
208
|
+
const entries2 = [...value].map(
|
|
209
|
+
(entry) => toCanonicalValue(entry, ancestors)
|
|
210
|
+
);
|
|
211
|
+
entries2.sort(
|
|
212
|
+
(left, right) => compareSerializedValues(JSON.stringify(left), JSON.stringify(right))
|
|
213
|
+
);
|
|
214
|
+
return [`set`, entries2];
|
|
121
215
|
}
|
|
122
|
-
|
|
216
|
+
const entries = Object.keys(value).sort().map(
|
|
217
|
+
(key) => [
|
|
218
|
+
key,
|
|
219
|
+
toCanonicalValue(
|
|
220
|
+
value[key],
|
|
221
|
+
ancestors
|
|
222
|
+
)
|
|
223
|
+
]
|
|
224
|
+
);
|
|
225
|
+
return [`object`, entries];
|
|
123
226
|
});
|
|
124
227
|
}
|
|
228
|
+
function compareSerializedValues(left, right) {
|
|
229
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
230
|
+
}
|
|
231
|
+
function withAcyclicValue(value, ancestors, encode) {
|
|
232
|
+
if (ancestors.has(value)) {
|
|
233
|
+
throw new TypeError(`Cannot serialize a cyclic structural relation key`);
|
|
234
|
+
}
|
|
235
|
+
ancestors.add(value);
|
|
236
|
+
try {
|
|
237
|
+
return encode();
|
|
238
|
+
} finally {
|
|
239
|
+
ancestors.delete(value);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
125
242
|
export {
|
|
126
243
|
DefaultMap,
|
|
127
244
|
ObjectIdGenerator,
|
package/dist/esm/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Simple assertion function for runtime checks.\n * Throws an error if the condition is false.\n */\nexport function assert(\n condition: unknown,\n message?: string,\n): asserts condition {\n if (!condition) {\n throw new Error(message || `Assertion failed`)\n }\n}\n\n/**\n * A map that returns a default value for keys that are not present.\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n constructor(\n private defaultValue: () => V,\n entries?: Iterable<[K, V]>,\n ) {\n super(entries)\n }\n\n get(key: K): V {\n if (!this.has(key)) {\n // this.set(key, this.defaultValue())\n return this.defaultValue()\n }\n return super.get(key)!\n }\n\n /**\n * Update the value for a key using a function.\n */\n update(key: K, updater: (value: V) => V): V {\n const value = this.get(key)\n const newValue = updater(value)\n this.set(key, newValue)\n return newValue\n }\n}\n\n// JS engines have various limits on how many args can be passed to a function\n// with a spread operator, so we need to split the operation into chunks\n// 32767 is the max for Chrome 14, all others are higher\n// TODO: investigate the performance of this and other approaches\nconst chunkSize = 30000\nexport function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>) {\n if (other.length <= chunkSize) {\n array.push(...other)\n } else {\n for (let i = 0; i < other.length; i += chunkSize) {\n const chunk = other.slice(i, i + chunkSize)\n array.push(...chunk)\n }\n }\n}\n\nexport function binarySearch<T>(\n array: Array<T>,\n value: T,\n comparator: (a: T, b: T) => number,\n): number {\n let low = 0\n let high = array.length\n while (low < high) {\n const mid = Math.floor((low + high) / 2)\n const comparison = comparator(array[mid]!, value)\n if (comparison < 0) {\n low = mid + 1\n } else if (comparison > 0) {\n high = mid\n } else {\n return mid\n }\n }\n return low\n}\n\n/**\n * Utility for generating unique IDs for objects and values.\n * Uses WeakMap for object reference tracking and consistent hashing for primitives.\n */\nexport class ObjectIdGenerator {\n private objectIds = new WeakMap<object, number>()\n private nextId = 0\n\n /**\n * Get a unique identifier for any value.\n * - Objects: Uses WeakMap for reference-based identity\n * - Primitives: Uses consistent string-based hashing\n */\n getId(value: any): number {\n // For primitives, use a simple hash of their string representation\n if (typeof value !== `object` || value === null) {\n const str = String(value)\n let hashValue = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hashValue = (hashValue << 5) - hashValue + char\n hashValue = hashValue & hashValue // Convert to 32-bit integer\n }\n return hashValue\n }\n\n // For objects, use WeakMap to assign unique IDs\n if (!this.objectIds.has(value)) {\n this.objectIds.set(value, this.nextId++)\n }\n return this.objectIds.get(value)!\n }\n\n /**\n * Get a string representation of the ID for use in composite keys.\n */\n getStringId(value: any): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n if (typeof value !== `object`) return `str_${String(value)}`\n\n return `obj_${this.getId(value)}`\n }\n}\n\n/**\n * Global instance for cases where a shared object ID space is needed.\n */\nexport const globalObjectIdGenerator = new ObjectIdGenerator()\n\nexport function* concatIterable<T>(\n ...iterables: Array<Iterable<T>>\n): Iterable<T> {\n for (const iterable of iterables) {\n yield* iterable\n }\n}\n\nexport function* mapIterable<T, U>(\n it: Iterable<T>,\n fn: (t: T) => U,\n): Iterable<U> {\n for (const t of it) {\n yield fn(t)\n }\n}\n\nexport type HRange = [number, number] // half-open [start, end[ i.e. end is exclusive\n\n/**\n * Computes the difference between two half-open ranges.\n * @param a - The first half-open range\n * @param b - The second half-open range\n * @returns The difference between the two ranges\n */\nexport function diffHalfOpen(a: HRange, b: HRange) {\n const [a1, a2] = a\n const [b1, b2] = b\n\n // A \\ B can be up to two segments (left and right of the overlap)\n const onlyInA: Array<number> = [\n ...range(a1, Math.min(a2, b1)), // left side of A outside B\n ...range(Math.max(a1, b2), a2), // right side of A outside B\n ]\n\n // B \\ A similarly\n const onlyInB: Array<number> = [\n ...range(b1, Math.min(b2, a1)),\n ...range(Math.max(b1, a2), b2),\n ]\n\n return { onlyInA, onlyInB }\n}\n\nfunction range(start: number, end: number): Array<number> {\n const out: Array<number> = []\n for (let i = start; i < end; i++) out.push(i)\n return out\n}\n\n/**\n * Compares two keys (string | number) in a consistent, deterministic way.\n * Handles mixed types by ordering strings before numbers.\n */\nexport function compareKeys(a: string | number, b: string | number): number {\n // Same type: compare directly\n if (typeof a === typeof b) {\n if (a < b) return -1\n if (a > b) return 1\n return 0\n }\n // Different types: strings come before numbers\n return typeof a === `string` ? -1 : 1\n}\n\n/**\n * Serializes a value for use as a key, handling BigInt and Date values that JSON.stringify cannot handle.\n * Uses JSON.stringify with a replacer function to convert BigInt values to strings and Date values to ISO strings.\n * This is used for creating string keys in groupBy operations.\n */\nexport function serializeValue(value: unknown): string {\n return JSON.stringify(value, (_, val) => {\n if (typeof val === 'bigint') {\n return val.toString()\n }\n if (val instanceof Date) {\n return val.toISOString()\n }\n return val\n })\n}\n"],"names":[],"mappings":"AAgBO,MAAM,mBAAyB,IAAU;AAAA,EAC9C,YACU,cACR,SACA;AACA,UAAM,OAAO;AAHL,SAAA,eAAA;AAAA,EAIV;AAAA,EAEA,IAAI,KAAW;AACb,QAAI,CAAC,KAAK,IAAI,GAAG,GAAG;AAElB,aAAO,KAAK,aAAA;AAAA,IACd;AACA,WAAO,MAAM,IAAI,GAAG;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,KAAQ,SAA6B;AAC1C,UAAM,QAAQ,KAAK,IAAI,GAAG;AAC1B,UAAM,WAAW,QAAQ,KAAK;AAC9B,SAAK,IAAI,KAAK,QAAQ;AACtB,WAAO;AAAA,EACT;AACF;AAMA,MAAM,YAAY;AACX,SAAS,iBAAiB,OAAuB,OAAuB;AAC7E,MAAI,MAAM,UAAU,WAAW;AAC7B,UAAM,KAAK,GAAG,KAAK;AAAA,EACrB,OAAO;AACL,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,WAAW;AAChD,YAAM,QAAQ,MAAM,MAAM,GAAG,IAAI,SAAS;AAC1C,YAAM,KAAK,GAAG,KAAK;AAAA,IACrB;AAAA,EACF;AACF;AAEO,SAAS,aACd,OACA,OACA,YACQ;AACR,MAAI,MAAM;AACV,MAAI,OAAO,MAAM;AACjB,SAAO,MAAM,MAAM;AACjB,UAAM,MAAM,KAAK,OAAO,MAAM,QAAQ,CAAC;AACvC,UAAM,aAAa,WAAW,MAAM,GAAG,GAAI,KAAK;AAChD,QAAI,aAAa,GAAG;AAClB,YAAM,MAAM;AAAA,IACd,WAAW,aAAa,GAAG;AACzB,aAAO;AAAA,IACT,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAMO,MAAM,kBAAkB;AAAA,EAAxB,cAAA;AACL,SAAQ,gCAAgB,QAAA;AACxB,SAAQ,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOjB,MAAM,OAAoB;AAExB,QAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,YAAM,MAAM,OAAO,KAAK;AACxB,UAAI,YAAY;AAChB,eAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,cAAM,OAAO,IAAI,WAAW,CAAC;AAC7B,qBAAa,aAAa,KAAK,YAAY;AAC3C,oBAAY,YAAY;AAAA,MAC1B;AACA,aAAO;AAAA,IACT;AAGA,QAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAAG;AAC9B,WAAK,UAAU,IAAI,OAAO,KAAK,QAAQ;AAAA,IACzC;AACA,WAAO,KAAK,UAAU,IAAI,KAAK;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,OAAoB;AAC9B,QAAI,UAAU,KAAM,QAAO;AAC3B,QAAI,UAAU,OAAW,QAAO;AAChC,QAAI,OAAO,UAAU,iBAAiB,OAAO,OAAO,KAAK,CAAC;AAE1D,WAAO,OAAO,KAAK,MAAM,KAAK,CAAC;AAAA,EACjC;AACF;AAKO,MAAM,0BAA0B,IAAI,kBAAA;AA2BpC,SAAS,aAAa,GAAW,GAAW;AACjD,QAAM,CAAC,IAAI,EAAE,IAAI;AACjB,QAAM,CAAC,IAAI,EAAE,IAAI;AAGjB,QAAM,UAAyB;AAAA,IAC7B,GAAG,MAAM,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;AAAA;AAAA,IAC7B,GAAG,MAAM,KAAK,IAAI,IAAI,EAAE,GAAG,EAAE;AAAA;AAAA,EAAA;AAI/B,QAAM,UAAyB;AAAA,IAC7B,GAAG,MAAM,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;AAAA,IAC7B,GAAG,MAAM,KAAK,IAAI,IAAI,EAAE,GAAG,EAAE;AAAA,EAAA;AAG/B,SAAO,EAAE,SAAS,QAAA;AACpB;AAEA,SAAS,MAAM,OAAe,KAA4B;AACxD,QAAM,MAAqB,CAAA;AAC3B,WAAS,IAAI,OAAO,IAAI,KAAK,IAAK,KAAI,KAAK,CAAC;AAC5C,SAAO;AACT;AAMO,SAAS,YAAY,GAAoB,GAA4B;AAE1E,MAAI,OAAO,MAAM,OAAO,GAAG;AACzB,QAAI,IAAI,EAAG,QAAO;AAClB,QAAI,IAAI,EAAG,QAAO;AAClB,WAAO;AAAA,EACT;AAEA,SAAO,OAAO,MAAM,WAAW,KAAK;AACtC;AAOO,SAAS,eAAe,OAAwB;AACrD,SAAO,KAAK,UAAU,OAAO,CAAC,GAAG,QAAQ;AACvC,QAAI,OAAO,QAAQ,UAAU;AAC3B,aAAO,IAAI,SAAA;AAAA,IACb;AACA,QAAI,eAAe,MAAM;AACvB,aAAO,IAAI,YAAA;AAAA,IACb;AACA,WAAO;AAAA,EACT,CAAC;AACH;"}
|
|
1
|
+
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Simple assertion function for runtime checks.\n * Throws an error if the condition is false.\n */\nexport function assert(\n condition: unknown,\n message?: string,\n): asserts condition {\n if (!condition) {\n throw new Error(message || `Assertion failed`)\n }\n}\n\n/**\n * A map that returns a default value for keys that are not present.\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n constructor(\n private defaultValue: () => V,\n entries?: Iterable<[K, V]>,\n ) {\n super(entries)\n }\n\n get(key: K): V {\n if (!this.has(key)) {\n // this.set(key, this.defaultValue())\n return this.defaultValue()\n }\n return super.get(key)!\n }\n\n /**\n * Update the value for a key using a function.\n */\n update(key: K, updater: (value: V) => V): V {\n const value = this.get(key)\n const newValue = updater(value)\n this.set(key, newValue)\n return newValue\n }\n}\n\n// JS engines have various limits on how many args can be passed to a function\n// with a spread operator, so we need to split the operation into chunks\n// 32767 is the max for Chrome 14, all others are higher\n// TODO: investigate the performance of this and other approaches\nconst chunkSize = 30000\nexport function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>) {\n if (other.length <= chunkSize) {\n array.push(...other)\n } else {\n for (let i = 0; i < other.length; i += chunkSize) {\n const chunk = other.slice(i, i + chunkSize)\n array.push(...chunk)\n }\n }\n}\n\nexport function binarySearch<T>(\n array: Array<T>,\n value: T,\n comparator: (a: T, b: T) => number,\n): number {\n let low = 0\n let high = array.length\n while (low < high) {\n const mid = Math.floor((low + high) / 2)\n const comparison = comparator(array[mid]!, value)\n if (comparison < 0) {\n low = mid + 1\n } else if (comparison > 0) {\n high = mid\n } else {\n return mid\n }\n }\n return low\n}\n\n/**\n * Utility for generating unique IDs for objects and values.\n * Uses WeakMap for object reference tracking and consistent hashing for primitives.\n */\nexport class ObjectIdGenerator {\n private objectIds = new WeakMap<object, number>()\n private nextId = 0\n\n /**\n * Get a unique identifier for any value.\n * - Objects: Uses WeakMap for reference-based identity\n * - Primitives: Uses consistent string-based hashing\n */\n getId(value: any): number {\n // For primitives, use a simple hash of their string representation\n if (typeof value !== `object` || value === null) {\n const str = String(value)\n let hashValue = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hashValue = (hashValue << 5) - hashValue + char\n hashValue = hashValue & hashValue // Convert to 32-bit integer\n }\n return hashValue\n }\n\n // For objects, use WeakMap to assign unique IDs\n if (!this.objectIds.has(value)) {\n this.objectIds.set(value, this.nextId++)\n }\n return this.objectIds.get(value)!\n }\n\n /**\n * Get a string representation of the ID for use in composite keys.\n */\n getStringId(value: any): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n if (typeof value !== `object`) return `str_${String(value)}`\n\n return `obj_${this.getId(value)}`\n }\n}\n\n/**\n * Global instance for cases where a shared object ID space is needed.\n */\nexport const globalObjectIdGenerator = new ObjectIdGenerator()\n\nexport function* concatIterable<T>(\n ...iterables: Array<Iterable<T>>\n): Iterable<T> {\n for (const iterable of iterables) {\n yield* iterable\n }\n}\n\nexport function* mapIterable<T, U>(\n it: Iterable<T>,\n fn: (t: T) => U,\n): Iterable<U> {\n for (const t of it) {\n yield fn(t)\n }\n}\n\nexport type HRange = [number, number] // half-open [start, end[ i.e. end is exclusive\n\n/**\n * Computes the difference between two half-open ranges.\n * @param a - The first half-open range\n * @param b - The second half-open range\n * @returns The difference between the two ranges\n */\nexport function diffHalfOpen(a: HRange, b: HRange) {\n const [a1, a2] = a\n const [b1, b2] = b\n\n // A \\ B can be up to two segments (left and right of the overlap)\n const onlyInA: Array<number> = [\n ...range(a1, Math.min(a2, b1)), // left side of A outside B\n ...range(Math.max(a1, b2), a2), // right side of A outside B\n ]\n\n // B \\ A similarly\n const onlyInB: Array<number> = [\n ...range(b1, Math.min(b2, a1)),\n ...range(Math.max(b1, a2), b2),\n ]\n\n return { onlyInA, onlyInB }\n}\n\nfunction range(start: number, end: number): Array<number> {\n const out: Array<number> = []\n for (let i = start; i < end; i++) out.push(i)\n return out\n}\n\n/**\n * Compares two keys (string | number) in a consistent, deterministic way.\n * Handles mixed types by ordering strings before numbers.\n */\nexport function compareKeys(a: string | number, b: string | number): number {\n // Same type: compare directly\n if (typeof a === typeof b) {\n if (a < b) return -1\n if (a > b) return 1\n return 0\n }\n // Different types: strings come before numbers\n return typeof a === `string` ? -1 : 1\n}\n\ntype CanonicalValue =\n | readonly [`undefined`]\n | readonly [`null`]\n | readonly [`boolean`, boolean]\n | readonly [`number`, number | `NaN` | `Infinity` | `-Infinity`]\n | readonly [`bigint`, string]\n | readonly [`string`, string]\n | readonly [`date`, number | `Invalid`]\n | readonly [`regexp`, string, string]\n | readonly [`bytes`, Array<number>]\n | readonly [`array`, Array<CanonicalValue>]\n | readonly [`map`, Array<readonly [CanonicalValue, CanonicalValue]>]\n | readonly [`set`, Array<CanonicalValue>]\n | readonly [`object`, Array<readonly [string, CanonicalValue]>]\n\n/**\n * Serializes a supported query value into one canonical key.\n *\n * JSON's native encoding is not suitable for relation keys: it merges BigInt\n * with strings when a replacer is used, merges NaN with null, drops undefined,\n * and depends on object insertion order. Ordinary JSON values keep their\n * established wire form. Values that need richer types use a reserved prefix\n * plus a structural, type-tagged encoding.\n */\nexport function serializeValue(value: unknown): string {\n if (isJsonSafeStructuralValue(value, new Set())) {\n return JSON.stringify(toStableJsonValue(value))\n }\n\n return `~${JSON.stringify(toCanonicalValue(value, new Set()))}`\n}\n\ntype JsonValue =\n | null\n | boolean\n | number\n | string\n | Array<JsonValue>\n | { [key: string]: JsonValue }\n\nfunction isJsonSafeStructuralValue(\n value: unknown,\n ancestors: Set<object>,\n): boolean {\n if (value === null) return true\n\n switch (typeof value) {\n case `boolean`:\n case `string`:\n return true\n case `number`:\n return Number.isFinite(value)\n case `undefined`:\n case `bigint`:\n case `symbol`:\n case `function`:\n return false\n }\n\n return withAcyclicValue(value, ancestors, () => {\n if (\n value instanceof Date ||\n value instanceof RegExp ||\n value instanceof Uint8Array ||\n value instanceof Map ||\n value instanceof Set\n ) {\n return false\n }\n\n return Array.isArray(value)\n ? value.every((item) => isJsonSafeStructuralValue(item, ancestors))\n : Object.keys(value).every((key) =>\n isJsonSafeStructuralValue(\n (value as Record<string, unknown>)[key],\n ancestors,\n ),\n )\n })\n}\n\nfunction toStableJsonValue(value: unknown): JsonValue {\n if (\n value === null ||\n typeof value === `boolean` ||\n typeof value === `number` ||\n typeof value === `string`\n ) {\n return value\n }\n\n if (Array.isArray(value)) {\n return value.map(toStableJsonValue)\n }\n\n return Object.fromEntries(\n Object.keys(value as object)\n .sort()\n .map((key) => [\n key,\n toStableJsonValue((value as Record<string, unknown>)[key]),\n ]),\n )\n}\n\nfunction toCanonicalValue(\n value: unknown,\n ancestors: Set<object>,\n): CanonicalValue {\n if (value === undefined) return [`undefined`]\n if (value === null) return [`null`]\n\n switch (typeof value) {\n case `boolean`:\n return [`boolean`, value]\n case `number`:\n if (Number.isNaN(value)) return [`number`, `NaN`]\n if (value === Infinity) return [`number`, `Infinity`]\n if (value === -Infinity) return [`number`, `-Infinity`]\n return [`number`, value === 0 ? 0 : value]\n case `bigint`:\n return [`bigint`, value.toString()]\n case `string`:\n return [`string`, value]\n case `symbol`:\n case `function`:\n throw new TypeError(\n `Cannot serialize ${typeof value} as a structural relation key`,\n )\n }\n\n return withAcyclicValue(value, ancestors, () => {\n if (value instanceof Date) {\n const timestamp = value.getTime()\n return Number.isNaN(timestamp)\n ? ([`date`, `Invalid`] as const)\n : ([`date`, timestamp] as const)\n }\n\n if (value instanceof RegExp) {\n return [`regexp`, value.source, value.flags]\n }\n\n if (value instanceof Uint8Array) {\n return [`bytes`, Array.from(value)]\n }\n\n if (Array.isArray(value)) {\n return [`array`, value.map((item) => toCanonicalValue(item, ancestors))]\n }\n\n if (value instanceof Map) {\n const entries = [...value.entries()].map(\n ([key, entryValue]) =>\n [\n toCanonicalValue(key, ancestors),\n toCanonicalValue(entryValue, ancestors),\n ] as const,\n )\n entries.sort((left, right) =>\n compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),\n )\n return [`map`, entries]\n }\n\n if (value instanceof Set) {\n const entries = [...value].map((entry) =>\n toCanonicalValue(entry, ancestors),\n )\n entries.sort((left, right) =>\n compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),\n )\n return [`set`, entries]\n }\n\n const entries = Object.keys(value)\n .sort()\n .map(\n (key) =>\n [\n key,\n toCanonicalValue(\n (value as Record<string, unknown>)[key],\n ancestors,\n ),\n ] as const,\n )\n return [`object`, entries]\n })\n}\n\nfunction compareSerializedValues(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0\n}\n\nfunction withAcyclicValue<T>(\n value: object,\n ancestors: Set<object>,\n encode: () => T,\n): T {\n if (ancestors.has(value)) {\n throw new TypeError(`Cannot serialize a cyclic structural relation key`)\n }\n\n ancestors.add(value)\n try {\n return encode()\n } finally {\n ancestors.delete(value)\n }\n}\n"],"names":["entries"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/db-ivm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "Incremental View Maintenance for TanStack DB based on Differential Dataflow",
|
|
5
5
|
"author": "Sam Willis",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/debug": "^4.1.12",
|
|
52
|
-
"@vitest/coverage-istanbul": "^3.2.4"
|
|
52
|
+
"@vitest/coverage-istanbul": "^3.2.4",
|
|
53
|
+
"temporal-polyfill": "^0.3.0"
|
|
53
54
|
},
|
|
54
55
|
"scripts": {
|
|
55
56
|
"build": "vite build",
|
package/src/hashing/hash.ts
CHANGED
|
@@ -18,6 +18,28 @@ const ARRAY_MARKER = randomHash()
|
|
|
18
18
|
const MAP_MARKER = randomHash()
|
|
19
19
|
const SET_MARKER = randomHash()
|
|
20
20
|
const UINT8ARRAY_MARKER = randomHash()
|
|
21
|
+
const TEMPORAL_MARKER = randomHash()
|
|
22
|
+
|
|
23
|
+
const temporalTypes = new Set([
|
|
24
|
+
`Temporal.Duration`,
|
|
25
|
+
`Temporal.Instant`,
|
|
26
|
+
`Temporal.PlainDate`,
|
|
27
|
+
`Temporal.PlainDateTime`,
|
|
28
|
+
`Temporal.PlainMonthDay`,
|
|
29
|
+
`Temporal.PlainTime`,
|
|
30
|
+
`Temporal.PlainYearMonth`,
|
|
31
|
+
`Temporal.ZonedDateTime`,
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
interface TemporalLike {
|
|
35
|
+
[Symbol.toStringTag]: string
|
|
36
|
+
toString: () => string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isTemporal(input: object): input is TemporalLike {
|
|
40
|
+
const tag = (input as Record<symbol, unknown>)[Symbol.toStringTag]
|
|
41
|
+
return typeof tag === `string` && temporalTypes.has(tag)
|
|
42
|
+
}
|
|
21
43
|
|
|
22
44
|
// Maximum byte length for Uint8Arrays to hash by content instead of reference
|
|
23
45
|
// Arrays smaller than this will be hashed by content, allowing proper equality comparisons
|
|
@@ -59,6 +81,8 @@ function hashObject(input: object): number {
|
|
|
59
81
|
} else if (input instanceof File) {
|
|
60
82
|
// Files are always hashed by reference due to their potentially large size
|
|
61
83
|
return cachedReferenceHash(input)
|
|
84
|
+
} else if (isTemporal(input)) {
|
|
85
|
+
valueHash = hashTemporal(input)
|
|
62
86
|
} else {
|
|
63
87
|
let plainObjectInput = input
|
|
64
88
|
let marker = OBJECT_MARKER
|
|
@@ -103,6 +127,14 @@ function hashUint8Array(input: Uint8Array): number {
|
|
|
103
127
|
return hasher.digest()
|
|
104
128
|
}
|
|
105
129
|
|
|
130
|
+
function hashTemporal(input: TemporalLike): number {
|
|
131
|
+
const hasher = new MurmurHashStream()
|
|
132
|
+
hasher.update(TEMPORAL_MARKER)
|
|
133
|
+
hasher.update(input[Symbol.toStringTag])
|
|
134
|
+
hasher.update(input.toString())
|
|
135
|
+
return hasher.digest()
|
|
136
|
+
}
|
|
137
|
+
|
|
106
138
|
function hashPlainObject(input: object, marker: number): number {
|
|
107
139
|
const hasher = new MurmurHashStream()
|
|
108
140
|
|
package/src/utils.ts
CHANGED
|
@@ -193,19 +193,214 @@ export function compareKeys(a: string | number, b: string | number): number {
|
|
|
193
193
|
return typeof a === `string` ? -1 : 1
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
type CanonicalValue =
|
|
197
|
+
| readonly [`undefined`]
|
|
198
|
+
| readonly [`null`]
|
|
199
|
+
| readonly [`boolean`, boolean]
|
|
200
|
+
| readonly [`number`, number | `NaN` | `Infinity` | `-Infinity`]
|
|
201
|
+
| readonly [`bigint`, string]
|
|
202
|
+
| readonly [`string`, string]
|
|
203
|
+
| readonly [`date`, number | `Invalid`]
|
|
204
|
+
| readonly [`regexp`, string, string]
|
|
205
|
+
| readonly [`bytes`, Array<number>]
|
|
206
|
+
| readonly [`array`, Array<CanonicalValue>]
|
|
207
|
+
| readonly [`map`, Array<readonly [CanonicalValue, CanonicalValue]>]
|
|
208
|
+
| readonly [`set`, Array<CanonicalValue>]
|
|
209
|
+
| readonly [`object`, Array<readonly [string, CanonicalValue]>]
|
|
210
|
+
|
|
196
211
|
/**
|
|
197
|
-
* Serializes a value
|
|
198
|
-
*
|
|
199
|
-
*
|
|
212
|
+
* Serializes a supported query value into one canonical key.
|
|
213
|
+
*
|
|
214
|
+
* JSON's native encoding is not suitable for relation keys: it merges BigInt
|
|
215
|
+
* with strings when a replacer is used, merges NaN with null, drops undefined,
|
|
216
|
+
* and depends on object insertion order. Ordinary JSON values keep their
|
|
217
|
+
* established wire form. Values that need richer types use a reserved prefix
|
|
218
|
+
* plus a structural, type-tagged encoding.
|
|
200
219
|
*/
|
|
201
220
|
export function serializeValue(value: unknown): string {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
221
|
+
if (isJsonSafeStructuralValue(value, new Set())) {
|
|
222
|
+
return JSON.stringify(toStableJsonValue(value))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return `~${JSON.stringify(toCanonicalValue(value, new Set()))}`
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
type JsonValue =
|
|
229
|
+
| null
|
|
230
|
+
| boolean
|
|
231
|
+
| number
|
|
232
|
+
| string
|
|
233
|
+
| Array<JsonValue>
|
|
234
|
+
| { [key: string]: JsonValue }
|
|
235
|
+
|
|
236
|
+
function isJsonSafeStructuralValue(
|
|
237
|
+
value: unknown,
|
|
238
|
+
ancestors: Set<object>,
|
|
239
|
+
): boolean {
|
|
240
|
+
if (value === null) return true
|
|
241
|
+
|
|
242
|
+
switch (typeof value) {
|
|
243
|
+
case `boolean`:
|
|
244
|
+
case `string`:
|
|
245
|
+
return true
|
|
246
|
+
case `number`:
|
|
247
|
+
return Number.isFinite(value)
|
|
248
|
+
case `undefined`:
|
|
249
|
+
case `bigint`:
|
|
250
|
+
case `symbol`:
|
|
251
|
+
case `function`:
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return withAcyclicValue(value, ancestors, () => {
|
|
256
|
+
if (
|
|
257
|
+
value instanceof Date ||
|
|
258
|
+
value instanceof RegExp ||
|
|
259
|
+
value instanceof Uint8Array ||
|
|
260
|
+
value instanceof Map ||
|
|
261
|
+
value instanceof Set
|
|
262
|
+
) {
|
|
263
|
+
return false
|
|
205
264
|
}
|
|
206
|
-
|
|
207
|
-
|
|
265
|
+
|
|
266
|
+
return Array.isArray(value)
|
|
267
|
+
? value.every((item) => isJsonSafeStructuralValue(item, ancestors))
|
|
268
|
+
: Object.keys(value).every((key) =>
|
|
269
|
+
isJsonSafeStructuralValue(
|
|
270
|
+
(value as Record<string, unknown>)[key],
|
|
271
|
+
ancestors,
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function toStableJsonValue(value: unknown): JsonValue {
|
|
278
|
+
if (
|
|
279
|
+
value === null ||
|
|
280
|
+
typeof value === `boolean` ||
|
|
281
|
+
typeof value === `number` ||
|
|
282
|
+
typeof value === `string`
|
|
283
|
+
) {
|
|
284
|
+
return value
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (Array.isArray(value)) {
|
|
288
|
+
return value.map(toStableJsonValue)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return Object.fromEntries(
|
|
292
|
+
Object.keys(value as object)
|
|
293
|
+
.sort()
|
|
294
|
+
.map((key) => [
|
|
295
|
+
key,
|
|
296
|
+
toStableJsonValue((value as Record<string, unknown>)[key]),
|
|
297
|
+
]),
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function toCanonicalValue(
|
|
302
|
+
value: unknown,
|
|
303
|
+
ancestors: Set<object>,
|
|
304
|
+
): CanonicalValue {
|
|
305
|
+
if (value === undefined) return [`undefined`]
|
|
306
|
+
if (value === null) return [`null`]
|
|
307
|
+
|
|
308
|
+
switch (typeof value) {
|
|
309
|
+
case `boolean`:
|
|
310
|
+
return [`boolean`, value]
|
|
311
|
+
case `number`:
|
|
312
|
+
if (Number.isNaN(value)) return [`number`, `NaN`]
|
|
313
|
+
if (value === Infinity) return [`number`, `Infinity`]
|
|
314
|
+
if (value === -Infinity) return [`number`, `-Infinity`]
|
|
315
|
+
return [`number`, value === 0 ? 0 : value]
|
|
316
|
+
case `bigint`:
|
|
317
|
+
return [`bigint`, value.toString()]
|
|
318
|
+
case `string`:
|
|
319
|
+
return [`string`, value]
|
|
320
|
+
case `symbol`:
|
|
321
|
+
case `function`:
|
|
322
|
+
throw new TypeError(
|
|
323
|
+
`Cannot serialize ${typeof value} as a structural relation key`,
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return withAcyclicValue(value, ancestors, () => {
|
|
328
|
+
if (value instanceof Date) {
|
|
329
|
+
const timestamp = value.getTime()
|
|
330
|
+
return Number.isNaN(timestamp)
|
|
331
|
+
? ([`date`, `Invalid`] as const)
|
|
332
|
+
: ([`date`, timestamp] as const)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (value instanceof RegExp) {
|
|
336
|
+
return [`regexp`, value.source, value.flags]
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (value instanceof Uint8Array) {
|
|
340
|
+
return [`bytes`, Array.from(value)]
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (Array.isArray(value)) {
|
|
344
|
+
return [`array`, value.map((item) => toCanonicalValue(item, ancestors))]
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (value instanceof Map) {
|
|
348
|
+
const entries = [...value.entries()].map(
|
|
349
|
+
([key, entryValue]) =>
|
|
350
|
+
[
|
|
351
|
+
toCanonicalValue(key, ancestors),
|
|
352
|
+
toCanonicalValue(entryValue, ancestors),
|
|
353
|
+
] as const,
|
|
354
|
+
)
|
|
355
|
+
entries.sort((left, right) =>
|
|
356
|
+
compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),
|
|
357
|
+
)
|
|
358
|
+
return [`map`, entries]
|
|
208
359
|
}
|
|
209
|
-
|
|
360
|
+
|
|
361
|
+
if (value instanceof Set) {
|
|
362
|
+
const entries = [...value].map((entry) =>
|
|
363
|
+
toCanonicalValue(entry, ancestors),
|
|
364
|
+
)
|
|
365
|
+
entries.sort((left, right) =>
|
|
366
|
+
compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),
|
|
367
|
+
)
|
|
368
|
+
return [`set`, entries]
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const entries = Object.keys(value)
|
|
372
|
+
.sort()
|
|
373
|
+
.map(
|
|
374
|
+
(key) =>
|
|
375
|
+
[
|
|
376
|
+
key,
|
|
377
|
+
toCanonicalValue(
|
|
378
|
+
(value as Record<string, unknown>)[key],
|
|
379
|
+
ancestors,
|
|
380
|
+
),
|
|
381
|
+
] as const,
|
|
382
|
+
)
|
|
383
|
+
return [`object`, entries]
|
|
210
384
|
})
|
|
211
385
|
}
|
|
386
|
+
|
|
387
|
+
function compareSerializedValues(left: string, right: string): number {
|
|
388
|
+
return left < right ? -1 : left > right ? 1 : 0
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function withAcyclicValue<T>(
|
|
392
|
+
value: object,
|
|
393
|
+
ancestors: Set<object>,
|
|
394
|
+
encode: () => T,
|
|
395
|
+
): T {
|
|
396
|
+
if (ancestors.has(value)) {
|
|
397
|
+
throw new TypeError(`Cannot serialize a cyclic structural relation key`)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
ancestors.add(value)
|
|
401
|
+
try {
|
|
402
|
+
return encode()
|
|
403
|
+
} finally {
|
|
404
|
+
ancestors.delete(value)
|
|
405
|
+
}
|
|
406
|
+
}
|