@tanstack/db 0.4.0 → 0.4.2
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/collection/events.cjs +2 -1
- package/dist/cjs/collection/events.cjs.map +1 -1
- package/dist/cjs/collection/lifecycle.cjs +69 -17
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/collection/lifecycle.d.cts +12 -1
- package/dist/cjs/collection/state.cjs +1 -1
- package/dist/cjs/collection/state.cjs.map +1 -1
- package/dist/cjs/indexes/btree-index.cjs +19 -13
- package/dist/cjs/indexes/btree-index.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +1 -1
- package/dist/cjs/query/compiler/evaluators.cjs +3 -2
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +6 -2
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.cjs +2 -1
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/optimizer.cjs +8 -3
- package/dist/cjs/query/optimizer.cjs.map +1 -1
- package/dist/cjs/query/optimizer.d.cts +2 -0
- package/dist/cjs/types.d.cts +8 -2
- package/dist/cjs/utils/browser-polyfills.cjs +22 -0
- package/dist/cjs/utils/browser-polyfills.cjs.map +1 -0
- package/dist/cjs/utils/browser-polyfills.d.cts +9 -0
- package/dist/cjs/utils/comparison.cjs +7 -0
- package/dist/cjs/utils/comparison.cjs.map +1 -1
- package/dist/cjs/utils/comparison.d.cts +4 -0
- package/dist/esm/collection/events.js +2 -1
- package/dist/esm/collection/events.js.map +1 -1
- package/dist/esm/collection/lifecycle.d.ts +12 -1
- package/dist/esm/collection/lifecycle.js +70 -18
- package/dist/esm/collection/lifecycle.js.map +1 -1
- package/dist/esm/collection/state.js +1 -1
- package/dist/esm/collection/state.js.map +1 -1
- package/dist/esm/indexes/btree-index.js +20 -14
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +1 -1
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/compiler/evaluators.js +3 -2
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/group-by.js +6 -2
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/joins.js +2 -1
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/optimizer.d.ts +2 -0
- package/dist/esm/query/optimizer.js +8 -3
- package/dist/esm/query/optimizer.js.map +1 -1
- package/dist/esm/types.d.ts +8 -2
- package/dist/esm/utils/browser-polyfills.d.ts +9 -0
- package/dist/esm/utils/browser-polyfills.js +22 -0
- package/dist/esm/utils/browser-polyfills.js.map +1 -0
- package/dist/esm/utils/comparison.d.ts +4 -0
- package/dist/esm/utils/comparison.js +8 -1
- package/dist/esm/utils/comparison.js.map +1 -1
- package/package.json +2 -2
- package/src/collection/events.ts +1 -1
- package/src/collection/lifecycle.ts +105 -25
- package/src/collection/state.ts +1 -1
- package/src/indexes/btree-index.ts +24 -14
- package/src/query/builder/functions.ts +3 -3
- package/src/query/compiler/evaluators.ts +3 -2
- package/src/query/compiler/group-by.ts +15 -2
- package/src/query/compiler/joins.ts +6 -1
- package/src/query/optimizer.ts +28 -6
- package/src/types.ts +8 -2
- package/src/utils/browser-polyfills.ts +39 -0
- package/src/utils/comparison.ts +10 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comparison.js","sources":["../../../src/utils/comparison.ts"],"sourcesContent":["import type { CompareOptions } from \"../query/builder/types\"\n\n// WeakMap to store stable IDs for objects\nconst objectIds = new WeakMap<object, number>()\nlet nextObjectId = 1\n\n/**\n * Get or create a stable ID for an object\n */\nfunction getObjectId(obj: object): number {\n if (objectIds.has(obj)) {\n return objectIds.get(obj)!\n }\n const id = nextObjectId++\n objectIds.set(obj, id)\n return id\n}\n\n/**\n * Universal comparison function for all data types\n * Handles null/undefined, strings, arrays, dates, objects, and primitives\n * Always sorts null/undefined values first\n */\nexport const ascComparator = (a: any, b: any, opts: CompareOptions): number => {\n const { nulls } = opts\n\n // Handle null/undefined\n if (a == null && b == null) return 0\n if (a == null) return nulls === `first` ? -1 : 1\n if (b == null) return nulls === `first` ? 1 : -1\n\n // if a and b are both strings, compare them based on locale\n if (typeof a === `string` && typeof b === `string`) {\n if (opts.stringSort === `locale`) {\n return a.localeCompare(b, opts.locale, opts.localeOptions)\n }\n // For lexical sort we rely on direct comparison for primitive values\n }\n\n // if a and b are both arrays, compare them element by element\n if (Array.isArray(a) && Array.isArray(b)) {\n for (let i = 0; i < Math.min(a.length, b.length); i++) {\n const result = ascComparator(a[i], b[i], opts)\n if (result !== 0) {\n return result\n }\n }\n // All elements are equal up to the minimum length\n return a.length - b.length\n }\n\n // If both are dates, compare them\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() - b.getTime()\n }\n\n // If at least one of the values is an object, use stable IDs for comparison\n const aIsObject = typeof a === `object`\n const bIsObject = typeof b === `object`\n\n if (aIsObject || bIsObject) {\n // If both are objects, compare their stable IDs\n if (aIsObject && bIsObject) {\n const aId = getObjectId(a)\n const bId = getObjectId(b)\n return aId - bId\n }\n\n // If only one is an object, objects come after primitives\n if (aIsObject) return 1\n if (bIsObject) return -1\n }\n\n // For primitive values, use direct comparison\n if (a < b) return -1\n if (a > b) return 1\n return 0\n}\n\n/**\n * Descending comparator function for ordering values\n * Handles null/undefined as largest values (opposite of ascending)\n */\nexport const descComparator = (\n a: unknown,\n b: unknown,\n opts: CompareOptions\n): number => {\n return ascComparator(b, a, {\n ...opts,\n nulls: opts.nulls === `first` ? `last` : `first`,\n })\n}\n\nexport function makeComparator(\n opts: CompareOptions\n): (a: any, b: any) => number {\n return (a, b) => {\n if (opts.direction === `asc`) {\n return ascComparator(a, b, opts)\n } else {\n return descComparator(a, b, opts)\n }\n }\n}\n\n/** Default comparator orders values in ascending order with nulls first and locale string comparison. */\nexport const defaultComparator = makeComparator({\n direction: `asc`,\n nulls: `first`,\n stringSort: `locale`,\n})\n"],"names":[],"mappings":"AAGA,MAAM,gCAAgB,QAAA;AACtB,IAAI,eAAe;AAKnB,SAAS,YAAY,KAAqB;AACxC,MAAI,UAAU,IAAI,GAAG,GAAG;AACtB,WAAO,UAAU,IAAI,GAAG;AAAA,EAC1B;AACA,QAAM,KAAK;AACX,YAAU,IAAI,KAAK,EAAE;AACrB,SAAO;AACT;AAOO,MAAM,gBAAgB,CAAC,GAAQ,GAAQ,SAAiC;AAC7E,QAAM,EAAE,UAAU;AAGlB,MAAI,KAAK,QAAQ,KAAK,KAAM,QAAO;AACnC,MAAI,KAAK,KAAM,QAAO,UAAU,UAAU,KAAK;AAC/C,MAAI,KAAK,KAAM,QAAO,UAAU,UAAU,IAAI;AAG9C,MAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UAAU;AAClD,QAAI,KAAK,eAAe,UAAU;AAChC,aAAO,EAAE,cAAc,GAAG,KAAK,QAAQ,KAAK,aAAa;AAAA,IAC3D;AAAA,EAEF;AAGA,MAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GAAG;AACxC,aAAS,IAAI,GAAG,IAAI,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK;AACrD,YAAM,SAAS,cAAc,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI;AAC7C,UAAI,WAAW,GAAG;AAChB,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,EAAE;AAAA,EACtB;AAGA,MAAI,aAAa,QAAQ,aAAa,MAAM;AAC1C,WAAO,EAAE,YAAY,EAAE,QAAA;AAAA,EACzB;AAGA,QAAM,YAAY,OAAO,MAAM;AAC/B,QAAM,YAAY,OAAO,MAAM;AAE/B,MAAI,aAAa,WAAW;AAE1B,QAAI,aAAa,WAAW;AAC1B,YAAM,MAAM,YAAY,CAAC;AACzB,YAAM,MAAM,YAAY,CAAC;AACzB,aAAO,MAAM;AAAA,IACf;AAGA,QAAI,UAAW,QAAO;AACtB,QAAI,UAAW,QAAO;AAAA,EACxB;AAGA,MAAI,IAAI,EAAG,QAAO;AAClB,MAAI,IAAI,EAAG,QAAO;AAClB,SAAO;AACT;AAMO,MAAM,iBAAiB,CAC5B,GACA,GACA,SACW;AACX,SAAO,cAAc,GAAG,GAAG;AAAA,IACzB,GAAG;AAAA,IACH,OAAO,KAAK,UAAU,UAAU,SAAS;AAAA,EAAA,CAC1C;AACH;AAEO,SAAS,eACd,MAC4B;AAC5B,SAAO,CAAC,GAAG,MAAM;AACf,QAAI,KAAK,cAAc,OAAO;AAC5B,aAAO,cAAc,GAAG,GAAG,IAAI;AAAA,IACjC,OAAO;AACL,aAAO,eAAe,GAAG,GAAG,IAAI;AAAA,IAClC;AAAA,EACF;AACF;AAGO,MAAM,oBAAoB,eAAe;AAAA,EAC9C,WAAW;AAAA,EACX,OAAO;AAAA,EACP,YAAY;AACd,CAAC;"}
|
|
1
|
+
{"version":3,"file":"comparison.js","sources":["../../../src/utils/comparison.ts"],"sourcesContent":["import type { CompareOptions } from \"../query/builder/types\"\n\n// WeakMap to store stable IDs for objects\nconst objectIds = new WeakMap<object, number>()\nlet nextObjectId = 1\n\n/**\n * Get or create a stable ID for an object\n */\nfunction getObjectId(obj: object): number {\n if (objectIds.has(obj)) {\n return objectIds.get(obj)!\n }\n const id = nextObjectId++\n objectIds.set(obj, id)\n return id\n}\n\n/**\n * Universal comparison function for all data types\n * Handles null/undefined, strings, arrays, dates, objects, and primitives\n * Always sorts null/undefined values first\n */\nexport const ascComparator = (a: any, b: any, opts: CompareOptions): number => {\n const { nulls } = opts\n\n // Handle null/undefined\n if (a == null && b == null) return 0\n if (a == null) return nulls === `first` ? -1 : 1\n if (b == null) return nulls === `first` ? 1 : -1\n\n // if a and b are both strings, compare them based on locale\n if (typeof a === `string` && typeof b === `string`) {\n if (opts.stringSort === `locale`) {\n return a.localeCompare(b, opts.locale, opts.localeOptions)\n }\n // For lexical sort we rely on direct comparison for primitive values\n }\n\n // if a and b are both arrays, compare them element by element\n if (Array.isArray(a) && Array.isArray(b)) {\n for (let i = 0; i < Math.min(a.length, b.length); i++) {\n const result = ascComparator(a[i], b[i], opts)\n if (result !== 0) {\n return result\n }\n }\n // All elements are equal up to the minimum length\n return a.length - b.length\n }\n\n // If both are dates, compare them\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() - b.getTime()\n }\n\n // If at least one of the values is an object, use stable IDs for comparison\n const aIsObject = typeof a === `object`\n const bIsObject = typeof b === `object`\n\n if (aIsObject || bIsObject) {\n // If both are objects, compare their stable IDs\n if (aIsObject && bIsObject) {\n const aId = getObjectId(a)\n const bId = getObjectId(b)\n return aId - bId\n }\n\n // If only one is an object, objects come after primitives\n if (aIsObject) return 1\n if (bIsObject) return -1\n }\n\n // For primitive values, use direct comparison\n if (a < b) return -1\n if (a > b) return 1\n return 0\n}\n\n/**\n * Descending comparator function for ordering values\n * Handles null/undefined as largest values (opposite of ascending)\n */\nexport const descComparator = (\n a: unknown,\n b: unknown,\n opts: CompareOptions\n): number => {\n return ascComparator(b, a, {\n ...opts,\n nulls: opts.nulls === `first` ? `last` : `first`,\n })\n}\n\nexport function makeComparator(\n opts: CompareOptions\n): (a: any, b: any) => number {\n return (a, b) => {\n if (opts.direction === `asc`) {\n return ascComparator(a, b, opts)\n } else {\n return descComparator(a, b, opts)\n }\n }\n}\n\n/** Default comparator orders values in ascending order with nulls first and locale string comparison. */\nexport const defaultComparator = makeComparator({\n direction: `asc`,\n nulls: `first`,\n stringSort: `locale`,\n})\n\n/**\n * Normalize a value for comparison\n */\nexport function normalizeValue(value: any): any {\n if (value instanceof Date) {\n return value.getTime()\n }\n return value\n}\n"],"names":[],"mappings":"AAGA,MAAM,gCAAgB,QAAA;AACtB,IAAI,eAAe;AAKnB,SAAS,YAAY,KAAqB;AACxC,MAAI,UAAU,IAAI,GAAG,GAAG;AACtB,WAAO,UAAU,IAAI,GAAG;AAAA,EAC1B;AACA,QAAM,KAAK;AACX,YAAU,IAAI,KAAK,EAAE;AACrB,SAAO;AACT;AAOO,MAAM,gBAAgB,CAAC,GAAQ,GAAQ,SAAiC;AAC7E,QAAM,EAAE,UAAU;AAGlB,MAAI,KAAK,QAAQ,KAAK,KAAM,QAAO;AACnC,MAAI,KAAK,KAAM,QAAO,UAAU,UAAU,KAAK;AAC/C,MAAI,KAAK,KAAM,QAAO,UAAU,UAAU,IAAI;AAG9C,MAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UAAU;AAClD,QAAI,KAAK,eAAe,UAAU;AAChC,aAAO,EAAE,cAAc,GAAG,KAAK,QAAQ,KAAK,aAAa;AAAA,IAC3D;AAAA,EAEF;AAGA,MAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GAAG;AACxC,aAAS,IAAI,GAAG,IAAI,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK;AACrD,YAAM,SAAS,cAAc,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI;AAC7C,UAAI,WAAW,GAAG;AAChB,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,EAAE;AAAA,EACtB;AAGA,MAAI,aAAa,QAAQ,aAAa,MAAM;AAC1C,WAAO,EAAE,YAAY,EAAE,QAAA;AAAA,EACzB;AAGA,QAAM,YAAY,OAAO,MAAM;AAC/B,QAAM,YAAY,OAAO,MAAM;AAE/B,MAAI,aAAa,WAAW;AAE1B,QAAI,aAAa,WAAW;AAC1B,YAAM,MAAM,YAAY,CAAC;AACzB,YAAM,MAAM,YAAY,CAAC;AACzB,aAAO,MAAM;AAAA,IACf;AAGA,QAAI,UAAW,QAAO;AACtB,QAAI,UAAW,QAAO;AAAA,EACxB;AAGA,MAAI,IAAI,EAAG,QAAO;AAClB,MAAI,IAAI,EAAG,QAAO;AAClB,SAAO;AACT;AAMO,MAAM,iBAAiB,CAC5B,GACA,GACA,SACW;AACX,SAAO,cAAc,GAAG,GAAG;AAAA,IACzB,GAAG;AAAA,IACH,OAAO,KAAK,UAAU,UAAU,SAAS;AAAA,EAAA,CAC1C;AACH;AAEO,SAAS,eACd,MAC4B;AAC5B,SAAO,CAAC,GAAG,MAAM;AACf,QAAI,KAAK,cAAc,OAAO;AAC5B,aAAO,cAAc,GAAG,GAAG,IAAI;AAAA,IACjC,OAAO;AACL,aAAO,eAAe,GAAG,GAAG,IAAI;AAAA,IAClC;AAAA,EACF;AACF;AAGO,MAAM,oBAAoB,eAAe;AAAA,EAC9C,WAAW;AAAA,EACX,OAAO;AAAA,EACP,YAAY;AACd,CAAC;AAKM,SAAS,eAAe,OAAiB;AAC9C,MAAI,iBAAiB,MAAM;AACzB,WAAO,MAAM,QAAA;AAAA,EACf;AACA,SAAO;AACT;"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/db",
|
|
3
3
|
"description": "A reactive client store for building super fast apps on sync",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.2",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@standard-schema/spec": "^1.0.0",
|
|
7
|
-
"@tanstack/db-ivm": "0.1.
|
|
7
|
+
"@tanstack/db-ivm": "0.1.9"
|
|
8
8
|
},
|
|
9
9
|
"devDependencies": {
|
|
10
10
|
"@vitest/coverage-istanbul": "^3.2.4",
|
package/src/collection/events.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CollectionInErrorStateError,
|
|
3
|
+
CollectionStateError,
|
|
3
4
|
InvalidCollectionStatusTransitionError,
|
|
4
5
|
} from "../errors"
|
|
6
|
+
import {
|
|
7
|
+
safeCancelIdleCallback,
|
|
8
|
+
safeRequestIdleCallback,
|
|
9
|
+
} from "../utils/browser-polyfills"
|
|
10
|
+
import type { IdleCallbackDeadline } from "../utils/browser-polyfills"
|
|
5
11
|
import type { StandardSchemaV1 } from "@standard-schema/spec"
|
|
6
12
|
import type { CollectionConfig, CollectionStatus } from "../types"
|
|
7
13
|
import type { CollectionEventsManager } from "./events"
|
|
@@ -29,6 +35,7 @@ export class CollectionLifecycleManager<
|
|
|
29
35
|
public hasReceivedFirstCommit = false
|
|
30
36
|
public onFirstReadyCallbacks: Array<() => void> = []
|
|
31
37
|
public gcTimeoutId: ReturnType<typeof setTimeout> | null = null
|
|
38
|
+
private idleCallbackId: number | null = null
|
|
32
39
|
|
|
33
40
|
/**
|
|
34
41
|
* Creates a new CollectionLifecycleManager instance
|
|
@@ -84,7 +91,18 @@ export class CollectionLifecycleManager<
|
|
|
84
91
|
* Safely update the collection status with validation
|
|
85
92
|
* @private
|
|
86
93
|
*/
|
|
87
|
-
public setStatus(
|
|
94
|
+
public setStatus(
|
|
95
|
+
newStatus: CollectionStatus,
|
|
96
|
+
allowReady: boolean = false
|
|
97
|
+
): void {
|
|
98
|
+
if (newStatus === `ready` && !allowReady) {
|
|
99
|
+
// setStatus('ready') is an internal method that should not be called directly
|
|
100
|
+
// Instead, use markReady to transition to ready triggering the necessary events
|
|
101
|
+
// and side effects.
|
|
102
|
+
throw new CollectionStateError(
|
|
103
|
+
`You can't directly call "setStatus('ready'). You must use markReady instead.`
|
|
104
|
+
)
|
|
105
|
+
}
|
|
88
106
|
this.validateStatusTransition(this.status, newStatus)
|
|
89
107
|
const previousStatus = this.status
|
|
90
108
|
this.status = newStatus
|
|
@@ -123,9 +141,10 @@ export class CollectionLifecycleManager<
|
|
|
123
141
|
* @private - Should only be called by sync implementations
|
|
124
142
|
*/
|
|
125
143
|
public markReady(): void {
|
|
144
|
+
this.validateStatusTransition(this.status, `ready`)
|
|
126
145
|
// Can transition to ready from loading or initialCommit states
|
|
127
146
|
if (this.status === `loading` || this.status === `initialCommit`) {
|
|
128
|
-
this.setStatus(`ready
|
|
147
|
+
this.setStatus(`ready`, true)
|
|
129
148
|
|
|
130
149
|
// Call any registered first ready callbacks (only on first time becoming ready)
|
|
131
150
|
if (!this.hasBeenReady) {
|
|
@@ -140,12 +159,11 @@ export class CollectionLifecycleManager<
|
|
|
140
159
|
this.onFirstReadyCallbacks = []
|
|
141
160
|
callbacks.forEach((callback) => callback())
|
|
142
161
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
this.changes.emitEmptyReadyEvent()
|
|
162
|
+
// Notify dependents when markReady is called, after status is set
|
|
163
|
+
// This ensures live queries get notified when their dependencies become ready
|
|
164
|
+
if (this.changes.changeSubscriptions.size > 0) {
|
|
165
|
+
this.changes.emitEmptyReadyEvent()
|
|
166
|
+
}
|
|
149
167
|
}
|
|
150
168
|
}
|
|
151
169
|
|
|
@@ -167,9 +185,8 @@ export class CollectionLifecycleManager<
|
|
|
167
185
|
|
|
168
186
|
this.gcTimeoutId = setTimeout(() => {
|
|
169
187
|
if (this.changes.activeSubscribersCount === 0) {
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
this.cleanup()
|
|
188
|
+
// Schedule cleanup during idle time to avoid blocking the UI thread
|
|
189
|
+
this.scheduleIdleCleanup()
|
|
173
190
|
}
|
|
174
191
|
}, gcTime)
|
|
175
192
|
}
|
|
@@ -183,6 +200,77 @@ export class CollectionLifecycleManager<
|
|
|
183
200
|
clearTimeout(this.gcTimeoutId)
|
|
184
201
|
this.gcTimeoutId = null
|
|
185
202
|
}
|
|
203
|
+
// Also cancel any pending idle cleanup
|
|
204
|
+
if (this.idleCallbackId !== null) {
|
|
205
|
+
safeCancelIdleCallback(this.idleCallbackId)
|
|
206
|
+
this.idleCallbackId = null
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Schedule cleanup to run during browser idle time
|
|
212
|
+
* This prevents blocking the UI thread during cleanup operations
|
|
213
|
+
*/
|
|
214
|
+
private scheduleIdleCleanup(): void {
|
|
215
|
+
// Cancel any existing idle callback
|
|
216
|
+
if (this.idleCallbackId !== null) {
|
|
217
|
+
safeCancelIdleCallback(this.idleCallbackId)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Schedule cleanup with a timeout of 1 second
|
|
221
|
+
// This ensures cleanup happens even if the browser is busy
|
|
222
|
+
this.idleCallbackId = safeRequestIdleCallback(
|
|
223
|
+
(deadline) => {
|
|
224
|
+
// Perform cleanup if we still have no subscribers
|
|
225
|
+
if (this.changes.activeSubscribersCount === 0) {
|
|
226
|
+
const cleanupCompleted = this.performCleanup(deadline)
|
|
227
|
+
// Only clear the callback ID if cleanup actually completed
|
|
228
|
+
if (cleanupCompleted) {
|
|
229
|
+
this.idleCallbackId = null
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// No need to cleanup, clear the callback ID
|
|
233
|
+
this.idleCallbackId = null
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
{ timeout: 1000 }
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Perform cleanup operations, optionally in chunks during idle time
|
|
242
|
+
* @returns true if cleanup was completed, false if it was rescheduled
|
|
243
|
+
*/
|
|
244
|
+
private performCleanup(deadline?: IdleCallbackDeadline): boolean {
|
|
245
|
+
// If we have a deadline, we can potentially split cleanup into chunks
|
|
246
|
+
// For now, we'll do all cleanup at once but check if we have time
|
|
247
|
+
const hasTime =
|
|
248
|
+
!deadline || deadline.timeRemaining() > 0 || deadline.didTimeout
|
|
249
|
+
|
|
250
|
+
if (hasTime) {
|
|
251
|
+
// Perform all cleanup operations
|
|
252
|
+
this.events.cleanup()
|
|
253
|
+
this.sync.cleanup()
|
|
254
|
+
this.state.cleanup()
|
|
255
|
+
this.changes.cleanup()
|
|
256
|
+
this.indexes.cleanup()
|
|
257
|
+
|
|
258
|
+
if (this.gcTimeoutId) {
|
|
259
|
+
clearTimeout(this.gcTimeoutId)
|
|
260
|
+
this.gcTimeoutId = null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.hasBeenReady = false
|
|
264
|
+
this.onFirstReadyCallbacks = []
|
|
265
|
+
|
|
266
|
+
// Set status to cleaned-up
|
|
267
|
+
this.setStatus(`cleaned-up`)
|
|
268
|
+
return true
|
|
269
|
+
} else {
|
|
270
|
+
// If we don't have time, reschedule for the next idle period
|
|
271
|
+
this.scheduleIdleCleanup()
|
|
272
|
+
return false
|
|
273
|
+
}
|
|
186
274
|
}
|
|
187
275
|
|
|
188
276
|
/**
|
|
@@ -201,21 +289,13 @@ export class CollectionLifecycleManager<
|
|
|
201
289
|
}
|
|
202
290
|
|
|
203
291
|
public cleanup(): void {
|
|
204
|
-
|
|
205
|
-
this.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
this.indexes.cleanup()
|
|
209
|
-
|
|
210
|
-
if (this.gcTimeoutId) {
|
|
211
|
-
clearTimeout(this.gcTimeoutId)
|
|
212
|
-
this.gcTimeoutId = null
|
|
292
|
+
// Cancel any pending idle cleanup
|
|
293
|
+
if (this.idleCallbackId !== null) {
|
|
294
|
+
safeCancelIdleCallback(this.idleCallbackId)
|
|
295
|
+
this.idleCallbackId = null
|
|
213
296
|
}
|
|
214
297
|
|
|
215
|
-
|
|
216
|
-
this.
|
|
217
|
-
|
|
218
|
-
// Set status to cleaned-up
|
|
219
|
-
this.setStatus(`cleaned-up`)
|
|
298
|
+
// Perform cleanup immediately (used when explicitly called)
|
|
299
|
+
this.performCleanup()
|
|
220
300
|
}
|
|
221
301
|
}
|
package/src/collection/state.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BTree } from "../utils/btree.js"
|
|
2
|
-
import { defaultComparator } from "../utils/comparison.js"
|
|
2
|
+
import { defaultComparator, normalizeValue } from "../utils/comparison.js"
|
|
3
3
|
import { BaseIndex } from "./base-index.js"
|
|
4
4
|
import type { BasicExpression } from "../query/ir.js"
|
|
5
5
|
import type { IndexOperation } from "./base-index.js"
|
|
@@ -71,15 +71,18 @@ export class BTreeIndex<
|
|
|
71
71
|
)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// Normalize the value for Map key usage
|
|
75
|
+
const normalizedValue = normalizeValue(indexedValue)
|
|
76
|
+
|
|
74
77
|
// Check if this value already exists
|
|
75
|
-
if (this.valueMap.has(
|
|
78
|
+
if (this.valueMap.has(normalizedValue)) {
|
|
76
79
|
// Add to existing set
|
|
77
|
-
this.valueMap.get(
|
|
80
|
+
this.valueMap.get(normalizedValue)!.add(key)
|
|
78
81
|
} else {
|
|
79
82
|
// Create new set for this value
|
|
80
83
|
const keySet = new Set<TKey>([key])
|
|
81
|
-
this.valueMap.set(
|
|
82
|
-
this.orderedEntries.set(
|
|
84
|
+
this.valueMap.set(normalizedValue, keySet)
|
|
85
|
+
this.orderedEntries.set(normalizedValue, undefined)
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
this.indexedKeys.add(key)
|
|
@@ -101,16 +104,19 @@ export class BTreeIndex<
|
|
|
101
104
|
return
|
|
102
105
|
}
|
|
103
106
|
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
// Normalize the value for Map key usage
|
|
108
|
+
const normalizedValue = normalizeValue(indexedValue)
|
|
109
|
+
|
|
110
|
+
if (this.valueMap.has(normalizedValue)) {
|
|
111
|
+
const keySet = this.valueMap.get(normalizedValue)!
|
|
106
112
|
keySet.delete(key)
|
|
107
113
|
|
|
108
114
|
// If set is now empty, remove the entry entirely
|
|
109
115
|
if (keySet.size === 0) {
|
|
110
|
-
this.valueMap.delete(
|
|
116
|
+
this.valueMap.delete(normalizedValue)
|
|
111
117
|
|
|
112
118
|
// Remove from ordered entries
|
|
113
|
-
this.orderedEntries.delete(
|
|
119
|
+
this.orderedEntries.delete(normalizedValue)
|
|
114
120
|
}
|
|
115
121
|
}
|
|
116
122
|
|
|
@@ -195,7 +201,8 @@ export class BTreeIndex<
|
|
|
195
201
|
* Performs an equality lookup
|
|
196
202
|
*/
|
|
197
203
|
equalityLookup(value: any): Set<TKey> {
|
|
198
|
-
|
|
204
|
+
const normalizedValue = normalizeValue(value)
|
|
205
|
+
return new Set(this.valueMap.get(normalizedValue) ?? [])
|
|
199
206
|
}
|
|
200
207
|
|
|
201
208
|
/**
|
|
@@ -206,8 +213,10 @@ export class BTreeIndex<
|
|
|
206
213
|
const { from, to, fromInclusive = true, toInclusive = true } = options
|
|
207
214
|
const result = new Set<TKey>()
|
|
208
215
|
|
|
209
|
-
const
|
|
210
|
-
const
|
|
216
|
+
const normalizedFrom = normalizeValue(from)
|
|
217
|
+
const normalizedTo = normalizeValue(to)
|
|
218
|
+
const fromKey = normalizedFrom ?? this.orderedEntries.minKey()
|
|
219
|
+
const toKey = normalizedTo ?? this.orderedEntries.maxKey()
|
|
211
220
|
|
|
212
221
|
this.orderedEntries.forRange(
|
|
213
222
|
fromKey,
|
|
@@ -240,7 +249,7 @@ export class BTreeIndex<
|
|
|
240
249
|
const keysInResult: Set<TKey> = new Set()
|
|
241
250
|
const result: Array<TKey> = []
|
|
242
251
|
const nextKey = (k?: any) => this.orderedEntries.nextHigherKey(k)
|
|
243
|
-
let key = from
|
|
252
|
+
let key = normalizeValue(from)
|
|
244
253
|
|
|
245
254
|
while ((key = nextKey(key)) && result.length < n) {
|
|
246
255
|
const keys = this.valueMap.get(key)
|
|
@@ -266,7 +275,8 @@ export class BTreeIndex<
|
|
|
266
275
|
const result = new Set<TKey>()
|
|
267
276
|
|
|
268
277
|
for (const value of values) {
|
|
269
|
-
const
|
|
278
|
+
const normalizedValue = normalizeValue(value)
|
|
279
|
+
const keys = this.valueMap.get(normalizedValue)
|
|
270
280
|
if (keys) {
|
|
271
281
|
keys.forEach((key) => result.add(key))
|
|
272
282
|
}
|
|
@@ -53,10 +53,10 @@ type ExtractType<T> =
|
|
|
53
53
|
// Helper type to determine aggregate return type based on input nullability
|
|
54
54
|
type AggregateReturnType<T> =
|
|
55
55
|
ExtractType<T> extends infer U
|
|
56
|
-
? U extends number | undefined | null
|
|
56
|
+
? U extends number | undefined | null | Date | bigint
|
|
57
57
|
? Aggregate<U>
|
|
58
|
-
: Aggregate<number | undefined | null>
|
|
59
|
-
: Aggregate<number | undefined | null>
|
|
58
|
+
: Aggregate<number | undefined | null | Date | bigint>
|
|
59
|
+
: Aggregate<number | undefined | null | Date | bigint>
|
|
60
60
|
|
|
61
61
|
// Helper type to determine string function return type based on input nullability
|
|
62
62
|
type StringFunctionReturnType<T> =
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
UnknownExpressionTypeError,
|
|
4
4
|
UnknownFunctionError,
|
|
5
5
|
} from "../../errors.js"
|
|
6
|
+
import { normalizeValue } from "../../utils/comparison.js"
|
|
6
7
|
import type { BasicExpression, Func, PropRef } from "../ir.js"
|
|
7
8
|
import type { NamespacedRow } from "../../types.js"
|
|
8
9
|
|
|
@@ -142,8 +143,8 @@ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any {
|
|
|
142
143
|
const argA = compiledArgs[0]!
|
|
143
144
|
const argB = compiledArgs[1]!
|
|
144
145
|
return (data) => {
|
|
145
|
-
const a = argA(data)
|
|
146
|
-
const b = argB(data)
|
|
146
|
+
const a = normalizeValue(argA(data))
|
|
147
|
+
const b = normalizeValue(argB(data))
|
|
147
148
|
return a === b
|
|
148
149
|
}
|
|
149
150
|
}
|
|
@@ -349,6 +349,19 @@ function getAggregateFunction(aggExpr: Aggregate) {
|
|
|
349
349
|
return typeof value === `number` ? value : value != null ? Number(value) : 0
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
// Create a value extractor function for the expression to aggregate
|
|
353
|
+
const valueExtractorWithDate = ([, namespacedRow]: [
|
|
354
|
+
string,
|
|
355
|
+
NamespacedRow,
|
|
356
|
+
]) => {
|
|
357
|
+
const value = compiledExpr(namespacedRow)
|
|
358
|
+
return typeof value === `number` || value instanceof Date
|
|
359
|
+
? value
|
|
360
|
+
: value != null
|
|
361
|
+
? Number(value)
|
|
362
|
+
: 0
|
|
363
|
+
}
|
|
364
|
+
|
|
352
365
|
// Create a raw value extractor function for the expression to aggregate
|
|
353
366
|
const rawValueExtractor = ([, namespacedRow]: [string, NamespacedRow]) => {
|
|
354
367
|
return compiledExpr(namespacedRow)
|
|
@@ -363,9 +376,9 @@ function getAggregateFunction(aggExpr: Aggregate) {
|
|
|
363
376
|
case `avg`:
|
|
364
377
|
return avg(valueExtractor)
|
|
365
378
|
case `min`:
|
|
366
|
-
return min(
|
|
379
|
+
return min(valueExtractorWithDate)
|
|
367
380
|
case `max`:
|
|
368
|
-
return max(
|
|
381
|
+
return max(valueExtractorWithDate)
|
|
369
382
|
default:
|
|
370
383
|
throw new UnsupportedAggregateFunctionError(aggExpr.name)
|
|
371
384
|
}
|
|
@@ -204,7 +204,12 @@ function processJoin(
|
|
|
204
204
|
lazyFrom.type === `queryRef` &&
|
|
205
205
|
(lazyFrom.query.limit || lazyFrom.query.offset)
|
|
206
206
|
|
|
207
|
-
|
|
207
|
+
// If join expressions contain computed values (like concat functions)
|
|
208
|
+
// we don't optimize the join because we don't have an index over the computed values
|
|
209
|
+
const hasComputedJoinExpr =
|
|
210
|
+
mainExpr.type === `func` || joinedExpr.type === `func`
|
|
211
|
+
|
|
212
|
+
if (!limitedSubquery && !hasComputedJoinExpr) {
|
|
208
213
|
// This join can be optimized by having the active collection
|
|
209
214
|
// dynamically load keys into the lazy collection
|
|
210
215
|
// based on the value of the joinKey and by looking up
|
package/src/query/optimizer.ts
CHANGED
|
@@ -142,6 +142,8 @@ export interface AnalyzedWhereClause {
|
|
|
142
142
|
expression: BasicExpression<boolean>
|
|
143
143
|
/** Set of table/source aliases that this WHERE clause touches */
|
|
144
144
|
touchedSources: Set<string>
|
|
145
|
+
/** Whether this clause contains namespace-only references that prevent pushdown */
|
|
146
|
+
hasNamespaceOnlyRef: boolean
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
/**
|
|
@@ -486,19 +488,31 @@ function splitAndClausesRecursive(
|
|
|
486
488
|
* This determines whether a clause can be pushed down to a specific table
|
|
487
489
|
* or must remain in the main query (for multi-source clauses like join conditions).
|
|
488
490
|
*
|
|
491
|
+
* Special handling for namespace-only references in outer joins:
|
|
492
|
+
* WHERE clauses that reference only a table namespace (e.g., isUndefined(special), eq(special, value))
|
|
493
|
+
* rather than specific properties (e.g., isUndefined(special.id), eq(special.id, value)) are treated as
|
|
494
|
+
* multi-source to prevent incorrect predicate pushdown that would change join semantics.
|
|
495
|
+
*
|
|
489
496
|
* @param clause - The WHERE expression to analyze
|
|
490
497
|
* @returns Analysis result with the expression and touched source aliases
|
|
491
498
|
*
|
|
492
499
|
* @example
|
|
493
500
|
* ```typescript
|
|
494
|
-
* // eq(users.department_id, 1) -> touches ['users']
|
|
495
|
-
* // eq(users.id, posts.user_id) -> touches ['users', 'posts']
|
|
501
|
+
* // eq(users.department_id, 1) -> touches ['users'], hasNamespaceOnlyRef: false
|
|
502
|
+
* // eq(users.id, posts.user_id) -> touches ['users', 'posts'], hasNamespaceOnlyRef: false
|
|
503
|
+
* // isUndefined(special) -> touches ['special'], hasNamespaceOnlyRef: true (prevents pushdown)
|
|
504
|
+
* // eq(special, someValue) -> touches ['special'], hasNamespaceOnlyRef: true (prevents pushdown)
|
|
505
|
+
* // isUndefined(special.id) -> touches ['special'], hasNamespaceOnlyRef: false (allows pushdown)
|
|
506
|
+
* // eq(special.id, 5) -> touches ['special'], hasNamespaceOnlyRef: false (allows pushdown)
|
|
496
507
|
* ```
|
|
497
508
|
*/
|
|
498
509
|
function analyzeWhereClause(
|
|
499
510
|
clause: BasicExpression<boolean>
|
|
500
511
|
): AnalyzedWhereClause {
|
|
512
|
+
// Track which table aliases this WHERE clause touches
|
|
501
513
|
const touchedSources = new Set<string>()
|
|
514
|
+
// Track whether this clause contains namespace-only references that prevent pushdown
|
|
515
|
+
let hasNamespaceOnlyRef = false
|
|
502
516
|
|
|
503
517
|
/**
|
|
504
518
|
* Recursively collect all table aliases referenced in an expression
|
|
@@ -511,6 +525,13 @@ function analyzeWhereClause(
|
|
|
511
525
|
const firstElement = expr.path[0]
|
|
512
526
|
if (firstElement) {
|
|
513
527
|
touchedSources.add(firstElement)
|
|
528
|
+
|
|
529
|
+
// If the path has only one element (just the namespace),
|
|
530
|
+
// this is a namespace-only reference that should not be pushed down
|
|
531
|
+
// This applies to ANY function, not just existence-checking functions
|
|
532
|
+
if (expr.path.length === 1) {
|
|
533
|
+
hasNamespaceOnlyRef = true
|
|
534
|
+
}
|
|
514
535
|
}
|
|
515
536
|
}
|
|
516
537
|
break
|
|
@@ -537,6 +558,7 @@ function analyzeWhereClause(
|
|
|
537
558
|
return {
|
|
538
559
|
expression: clause,
|
|
539
560
|
touchedSources,
|
|
561
|
+
hasNamespaceOnlyRef,
|
|
540
562
|
}
|
|
541
563
|
}
|
|
542
564
|
|
|
@@ -557,15 +579,15 @@ function groupWhereClauses(
|
|
|
557
579
|
|
|
558
580
|
// Categorize each clause based on how many sources it touches
|
|
559
581
|
for (const clause of analyzedClauses) {
|
|
560
|
-
if (clause.touchedSources.size === 1) {
|
|
561
|
-
// Single source clause - can be optimized
|
|
582
|
+
if (clause.touchedSources.size === 1 && !clause.hasNamespaceOnlyRef) {
|
|
583
|
+
// Single source clause without namespace-only references - can be optimized
|
|
562
584
|
const source = Array.from(clause.touchedSources)[0]!
|
|
563
585
|
if (!singleSource.has(source)) {
|
|
564
586
|
singleSource.set(source, [])
|
|
565
587
|
}
|
|
566
588
|
singleSource.get(source)!.push(clause.expression)
|
|
567
|
-
} else if (clause.touchedSources.size > 1) {
|
|
568
|
-
// Multi-source clause - must stay in main query
|
|
589
|
+
} else if (clause.touchedSources.size > 1 || clause.hasNamespaceOnlyRef) {
|
|
590
|
+
// Multi-source clause or namespace-only reference - must stay in main query
|
|
569
591
|
multiSource.push(clause.expression)
|
|
570
592
|
}
|
|
571
593
|
// Skip clauses that touch no sources (constants) - they don't need optimization
|
package/src/types.ts
CHANGED
|
@@ -334,8 +334,14 @@ export interface BaseCollectionConfig<
|
|
|
334
334
|
*/
|
|
335
335
|
gcTime?: number
|
|
336
336
|
/**
|
|
337
|
-
* Whether to start syncing
|
|
338
|
-
*
|
|
337
|
+
* Whether to eagerly start syncing on collection creation.
|
|
338
|
+
* When true, syncing begins immediately. When false, syncing starts when the first subscriber attaches.
|
|
339
|
+
*
|
|
340
|
+
* Note: Even with startSync=true, collections will pause syncing when there are no active
|
|
341
|
+
* subscribers (typically when components querying the collection unmount), resuming when new
|
|
342
|
+
* subscribers attach. This preserves normal staleTime/gcTime behavior.
|
|
343
|
+
*
|
|
344
|
+
* @default false
|
|
339
345
|
*/
|
|
340
346
|
startSync?: boolean
|
|
341
347
|
/**
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Type definitions for requestIdleCallback - compatible with existing browser types
|
|
2
|
+
export type IdleCallbackDeadline = {
|
|
3
|
+
didTimeout: boolean
|
|
4
|
+
timeRemaining: () => number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type IdleCallbackFunction = (deadline: IdleCallbackDeadline) => void
|
|
8
|
+
|
|
9
|
+
const requestIdleCallbackPolyfill = (
|
|
10
|
+
callback: IdleCallbackFunction
|
|
11
|
+
): number => {
|
|
12
|
+
// Use a very small timeout for the polyfill to simulate idle time
|
|
13
|
+
const timeout = 0
|
|
14
|
+
const timeoutId = setTimeout(() => {
|
|
15
|
+
callback({
|
|
16
|
+
didTimeout: true, // Always indicate timeout for the polyfill
|
|
17
|
+
timeRemaining: () => 50, // Return some time remaining for polyfill
|
|
18
|
+
})
|
|
19
|
+
}, timeout)
|
|
20
|
+
return timeoutId as unknown as number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const cancelIdleCallbackPolyfill = (id: number): void => {
|
|
24
|
+
clearTimeout(id as unknown as ReturnType<typeof setTimeout>)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const safeRequestIdleCallback: (
|
|
28
|
+
callback: IdleCallbackFunction,
|
|
29
|
+
options?: { timeout?: number }
|
|
30
|
+
) => number =
|
|
31
|
+
typeof window !== `undefined` && `requestIdleCallback` in window
|
|
32
|
+
? (callback, options) =>
|
|
33
|
+
(window as any).requestIdleCallback(callback, options)
|
|
34
|
+
: (callback, _options) => requestIdleCallbackPolyfill(callback)
|
|
35
|
+
|
|
36
|
+
export const safeCancelIdleCallback: (id: number) => void =
|
|
37
|
+
typeof window !== `undefined` && `cancelIdleCallback` in window
|
|
38
|
+
? (id) => (window as any).cancelIdleCallback(id)
|
|
39
|
+
: cancelIdleCallbackPolyfill
|
package/src/utils/comparison.ts
CHANGED
|
@@ -110,3 +110,13 @@ export const defaultComparator = makeComparator({
|
|
|
110
110
|
nulls: `first`,
|
|
111
111
|
stringSort: `locale`,
|
|
112
112
|
})
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Normalize a value for comparison
|
|
116
|
+
*/
|
|
117
|
+
export function normalizeValue(value: any): any {
|
|
118
|
+
if (value instanceof Date) {
|
|
119
|
+
return value.getTime()
|
|
120
|
+
}
|
|
121
|
+
return value
|
|
122
|
+
}
|