@tanstack/db-ivm 0.1.18 → 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.
@@ -3,47 +3,52 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const d2 = require("./d2.cjs");
4
4
  const multiset = require("./multiset.cjs");
5
5
  const utils = require("./utils.cjs");
6
- const concat = require("./operators/concat.cjs");
7
- const consolidate = require("./operators/consolidate.cjs");
8
- const count = require("./operators/count.cjs");
9
- const debug = require("./operators/debug.cjs");
10
- const distinct = require("./operators/distinct.cjs");
11
- const filter = require("./operators/filter.cjs");
12
- const groupedTopKWithFractionalIndex = require("./operators/groupedTopKWithFractionalIndex.cjs");
13
- const join = require("./operators/join.cjs");
6
+ const groupBy = require("./operators/groupBy.cjs");
7
+ const pipe = require("./operators/pipe.cjs");
14
8
  const map = require("./operators/map.cjs");
9
+ const tap = require("./operators/tap.cjs");
10
+ const filter = require("./operators/filter.cjs");
15
11
  const negate = require("./operators/negate.cjs");
12
+ const concat = require("./operators/concat.cjs");
13
+ const debug = require("./operators/debug.cjs");
16
14
  const output = require("./operators/output.cjs");
15
+ const consolidate = require("./operators/consolidate.cjs");
16
+ const join = require("./operators/join.cjs");
17
17
  const reduce = require("./operators/reduce.cjs");
18
- const tap = require("./operators/tap.cjs");
19
- const topKWithFractionalIndex = require("./operators/topKWithFractionalIndex.cjs");
20
- const filterBy = require("./operators/filterBy.cjs");
21
- const groupBy = require("./operators/groupBy.cjs");
22
- const groupedOrderBy = require("./operators/groupedOrderBy.cjs");
18
+ const count = require("./operators/count.cjs");
19
+ const distinct = require("./operators/distinct.cjs");
23
20
  const keying = require("./operators/keying.cjs");
24
- const orderBy = require("./operators/orderBy.cjs");
25
- const pipe = require("./operators/pipe.cjs");
26
21
  const topK = require("./operators/topK.cjs");
22
+ const topKWithFractionalIndex = require("./operators/topKWithFractionalIndex.cjs");
23
+ const groupedTopKWithFractionalIndex = require("./operators/groupedTopKWithFractionalIndex.cjs");
24
+ const orderBy = require("./operators/orderBy.cjs");
25
+ const groupedOrderBy = require("./operators/groupedOrderBy.cjs");
26
+ const filterBy = require("./operators/filterBy.cjs");
27
27
  exports.D2 = d2.D2;
28
28
  exports.RootStreamBuilder = d2.RootStreamBuilder;
29
29
  exports.StreamBuilder = d2.StreamBuilder;
30
30
  exports.MultiSet = multiset.MultiSet;
31
31
  exports.compareKeys = utils.compareKeys;
32
32
  exports.serializeValue = utils.serializeValue;
33
+ exports.groupBy = groupBy.groupBy;
34
+ exports.groupByOperators = groupBy.groupByOperators;
35
+ exports.pipe = pipe.pipe;
36
+ exports.MapOperator = map.MapOperator;
37
+ exports.map = map.map;
38
+ exports.TapOperator = tap.TapOperator;
39
+ exports.tap = tap.tap;
40
+ exports.FilterOperator = filter.FilterOperator;
41
+ exports.filter = filter.filter;
42
+ exports.NegateOperator = negate.NegateOperator;
43
+ exports.negate = negate.negate;
33
44
  exports.ConcatOperator = concat.ConcatOperator;
34
45
  exports.concat = concat.concat;
35
- exports.ConsolidateOperator = consolidate.ConsolidateOperator;
36
- exports.consolidate = consolidate.consolidate;
37
- exports.CountOperator = count.CountOperator;
38
- exports.count = count.count;
39
46
  exports.DebugOperator = debug.DebugOperator;
40
47
  exports.debug = debug.debug;
41
- exports.DistinctOperator = distinct.DistinctOperator;
42
- exports.distinct = distinct.distinct;
43
- exports.FilterOperator = filter.FilterOperator;
44
- exports.filter = filter.filter;
45
- exports.GroupedTopKWithFractionalIndexOperator = groupedTopKWithFractionalIndex.GroupedTopKWithFractionalIndexOperator;
46
- exports.groupedTopKWithFractionalIndex = groupedTopKWithFractionalIndex.groupedTopKWithFractionalIndex;
48
+ exports.OutputOperator = output.OutputOperator;
49
+ exports.output = output.output;
50
+ exports.ConsolidateOperator = consolidate.ConsolidateOperator;
51
+ exports.consolidate = consolidate.consolidate;
47
52
  exports.JoinOperator = join.JoinOperator;
48
53
  exports.antiJoin = join.antiJoin;
49
54
  exports.fullJoin = join.fullJoin;
@@ -51,30 +56,25 @@ exports.innerJoin = join.innerJoin;
51
56
  exports.join = join.join;
52
57
  exports.leftJoin = join.leftJoin;
53
58
  exports.rightJoin = join.rightJoin;
54
- exports.MapOperator = map.MapOperator;
55
- exports.map = map.map;
56
- exports.NegateOperator = negate.NegateOperator;
57
- exports.negate = negate.negate;
58
- exports.OutputOperator = output.OutputOperator;
59
- exports.output = output.output;
60
59
  exports.ReduceOperator = reduce.ReduceOperator;
61
60
  exports.reduce = reduce.reduce;
62
- exports.TapOperator = tap.TapOperator;
63
- exports.tap = tap.tap;
64
- exports.TopKWithFractionalIndexOperator = topKWithFractionalIndex.TopKWithFractionalIndexOperator;
65
- exports.topKWithFractionalIndex = topKWithFractionalIndex.topKWithFractionalIndex;
66
- exports.filterBy = filterBy.filterBy;
67
- exports.groupBy = groupBy.groupBy;
68
- exports.groupByOperators = groupBy.groupByOperators;
69
- exports.groupedOrderByWithFractionalIndex = groupedOrderBy.groupedOrderByWithFractionalIndex;
61
+ exports.CountOperator = count.CountOperator;
62
+ exports.count = count.count;
63
+ exports.DistinctOperator = distinct.DistinctOperator;
64
+ exports.distinct = distinct.distinct;
70
65
  exports.keyBy = keying.keyBy;
71
66
  exports.rekey = keying.rekey;
72
67
  exports.unkey = keying.unkey;
68
+ exports.topK = topK.topK;
69
+ exports.topKWithIndex = topK.topKWithIndex;
70
+ exports.TopKWithFractionalIndexOperator = topKWithFractionalIndex.TopKWithFractionalIndexOperator;
71
+ exports.topKWithFractionalIndex = topKWithFractionalIndex.topKWithFractionalIndex;
72
+ exports.GroupedTopKWithFractionalIndexOperator = groupedTopKWithFractionalIndex.GroupedTopKWithFractionalIndexOperator;
73
+ exports.groupedTopKWithFractionalIndex = groupedTopKWithFractionalIndex.groupedTopKWithFractionalIndex;
73
74
  exports.orderBy = orderBy.orderBy;
74
75
  exports.orderByWithFractionalIndex = orderBy.orderByWithFractionalIndex;
75
76
  exports.orderByWithFractionalIndexBase = orderBy.orderByWithFractionalIndexBase;
76
77
  exports.orderByWithIndex = orderBy.orderByWithIndex;
77
- exports.pipe = pipe.pipe;
78
- exports.topK = topK.topK;
79
- exports.topKWithIndex = topK.topKWithIndex;
78
+ exports.groupedOrderByWithFractionalIndex = groupedOrderBy.groupedOrderByWithFractionalIndex;
79
+ exports.filterBy = filterBy.filterBy;
80
80
  //# sourceMappingURL=index.cjs.map
@@ -114,16 +114,133 @@ function compareKeys(a, b) {
114
114
  return typeof a === `string` ? -1 : 1;
115
115
  }
116
116
  function serializeValue(value) {
117
- return JSON.stringify(value, (_, val) => {
118
- if (typeof val === "bigint") {
119
- return val.toString();
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 (val instanceof Date) {
122
- return val.toISOString();
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
- return val;
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;
@@ -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":"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"}
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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"}
@@ -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 for use as a key, handling BigInt and Date values that JSON.stringify cannot handle.
62
- * Uses JSON.stringify with a replacer function to convert BigInt values to strings and Date values to ISO strings.
63
- * This is used for creating string keys in groupBy operations.
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/index.js CHANGED
@@ -1,27 +1,27 @@
1
1
  import { D2, RootStreamBuilder, StreamBuilder } from "./d2.js";
2
2
  import { MultiSet } from "./multiset.js";
3
3
  import { compareKeys, serializeValue } from "./utils.js";
4
- import { ConcatOperator, concat } from "./operators/concat.js";
5
- import { ConsolidateOperator, consolidate } from "./operators/consolidate.js";
6
- import { CountOperator, count } from "./operators/count.js";
7
- import { DebugOperator, debug } from "./operators/debug.js";
8
- import { DistinctOperator, distinct } from "./operators/distinct.js";
9
- import { FilterOperator, filter } from "./operators/filter.js";
10
- import { GroupedTopKWithFractionalIndexOperator, groupedTopKWithFractionalIndex } from "./operators/groupedTopKWithFractionalIndex.js";
11
- import { JoinOperator, antiJoin, fullJoin, innerJoin, join, leftJoin, rightJoin } from "./operators/join.js";
4
+ import { groupBy, groupByOperators } from "./operators/groupBy.js";
5
+ import { pipe } from "./operators/pipe.js";
12
6
  import { MapOperator, map } from "./operators/map.js";
7
+ import { TapOperator, tap } from "./operators/tap.js";
8
+ import { FilterOperator, filter } from "./operators/filter.js";
13
9
  import { NegateOperator, negate } from "./operators/negate.js";
10
+ import { ConcatOperator, concat } from "./operators/concat.js";
11
+ import { DebugOperator, debug } from "./operators/debug.js";
14
12
  import { OutputOperator, output } from "./operators/output.js";
13
+ import { ConsolidateOperator, consolidate } from "./operators/consolidate.js";
14
+ import { JoinOperator, antiJoin, fullJoin, innerJoin, join, leftJoin, rightJoin } from "./operators/join.js";
15
15
  import { ReduceOperator, reduce } from "./operators/reduce.js";
16
- import { TapOperator, tap } from "./operators/tap.js";
17
- import { TopKWithFractionalIndexOperator, topKWithFractionalIndex } from "./operators/topKWithFractionalIndex.js";
18
- import { filterBy } from "./operators/filterBy.js";
19
- import { groupBy, groupByOperators } from "./operators/groupBy.js";
20
- import { groupedOrderByWithFractionalIndex } from "./operators/groupedOrderBy.js";
16
+ import { CountOperator, count } from "./operators/count.js";
17
+ import { DistinctOperator, distinct } from "./operators/distinct.js";
21
18
  import { keyBy, rekey, unkey } from "./operators/keying.js";
22
- import { orderBy, orderByWithFractionalIndex, orderByWithFractionalIndexBase, orderByWithIndex } from "./operators/orderBy.js";
23
- import { pipe } from "./operators/pipe.js";
24
19
  import { topK, topKWithIndex } from "./operators/topK.js";
20
+ import { TopKWithFractionalIndexOperator, topKWithFractionalIndex } from "./operators/topKWithFractionalIndex.js";
21
+ import { GroupedTopKWithFractionalIndexOperator, groupedTopKWithFractionalIndex } from "./operators/groupedTopKWithFractionalIndex.js";
22
+ import { orderBy, orderByWithFractionalIndex, orderByWithFractionalIndexBase, orderByWithIndex } from "./operators/orderBy.js";
23
+ import { groupedOrderByWithFractionalIndex } from "./operators/groupedOrderBy.js";
24
+ import { filterBy } from "./operators/filterBy.js";
25
25
  export {
26
26
  ConcatOperator,
27
27
  ConsolidateOperator,
@@ -1,5 +1,5 @@
1
1
  import { generateKeyBetween } from "fractional-indexing";
2
- import { compareKeys, binarySearch, diffHalfOpen } from "../utils.js";
2
+ import { compareKeys, diffHalfOpen, binarySearch } from "../utils.js";
3
3
  function indexedValue(value, index) {
4
4
  return [value, index];
5
5
  }
@@ -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 for use as a key, handling BigInt and Date values that JSON.stringify cannot handle.
62
- * Uses JSON.stringify with a replacer function to convert BigInt values to strings and Date values to ISO strings.
63
- * This is used for creating string keys in groupBy operations.
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
- return JSON.stringify(value, (_, val) => {
116
- if (typeof val === "bigint") {
117
- return val.toString();
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 (val instanceof Date) {
120
- return val.toISOString();
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
- return val;
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,
@@ -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":"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"}
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.18",
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",
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 for use as a key, handling BigInt and Date values that JSON.stringify cannot handle.
198
- * Uses JSON.stringify with a replacer function to convert BigInt values to strings and Date values to ISO strings.
199
- * This is used for creating string keys in groupBy operations.
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
- return JSON.stringify(value, (_, val) => {
203
- if (typeof val === 'bigint') {
204
- return val.toString()
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
- if (val instanceof Date) {
207
- return val.toISOString()
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
- return val
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
+ }