@tanstack/db 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/collection/events.cjs +2 -1
- package/dist/cjs/collection/events.cjs.map +1 -1
- package/dist/cjs/collection/lifecycle.cjs +11 -5
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/collection/lifecycle.d.cts +1 -1
- package/dist/cjs/collection/state.cjs +1 -1
- package/dist/cjs/collection/state.cjs.map +1 -1
- package/dist/cjs/collection/subscription.cjs +1 -1
- package/dist/cjs/collection/subscription.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/index.cjs +2 -39
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +1 -0
- package/dist/cjs/query/compiler/joins.cjs +11 -9
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +2 -1
- package/dist/cjs/query/compiler/order-by.cjs +4 -5
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/ir.cjs +38 -0
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/ir.d.cts +10 -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/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 +1 -1
- package/dist/esm/collection/lifecycle.js +12 -6
- 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/collection/subscription.js +1 -1
- package/dist/esm/collection/subscription.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/index.d.ts +1 -0
- package/dist/esm/query/compiler/index.js +4 -41
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +2 -1
- package/dist/esm/query/compiler/joins.js +11 -9
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.js +1 -2
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/ir.d.ts +10 -1
- package/dist/esm/query/ir.js +38 -0
- package/dist/esm/query/ir.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/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 +20 -8
- package/src/collection/state.ts +1 -1
- package/src/collection/subscription.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/index.ts +4 -1
- package/src/query/compiler/joins.ts +19 -9
- package/src/query/compiler/order-by.ts +1 -2
- package/src/query/ir.ts +67 -1
- package/src/query/optimizer.ts +28 -6
- package/src/types.ts +8 -2
- package/src/utils/comparison.ts +10 -0
package/dist/cjs/types.d.cts
CHANGED
|
@@ -213,8 +213,14 @@ export interface BaseCollectionConfig<T extends object = Record<string, unknown>
|
|
|
213
213
|
*/
|
|
214
214
|
gcTime?: number;
|
|
215
215
|
/**
|
|
216
|
-
* Whether to start syncing
|
|
217
|
-
*
|
|
216
|
+
* Whether to eagerly start syncing on collection creation.
|
|
217
|
+
* When true, syncing begins immediately. When false, syncing starts when the first subscriber attaches.
|
|
218
|
+
*
|
|
219
|
+
* Note: Even with startSync=true, collections will pause syncing when there are no active
|
|
220
|
+
* subscribers (typically when components querying the collection unmount), resuming when new
|
|
221
|
+
* subscribers attach. This preserves normal staleTime/gcTime behavior.
|
|
222
|
+
*
|
|
223
|
+
* @default false
|
|
218
224
|
*/
|
|
219
225
|
startSync?: boolean;
|
|
220
226
|
/**
|
|
@@ -67,8 +67,15 @@ const defaultComparator = makeComparator({
|
|
|
67
67
|
nulls: `first`,
|
|
68
68
|
stringSort: `locale`
|
|
69
69
|
});
|
|
70
|
+
function normalizeValue(value) {
|
|
71
|
+
if (value instanceof Date) {
|
|
72
|
+
return value.getTime();
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
70
76
|
exports.ascComparator = ascComparator;
|
|
71
77
|
exports.defaultComparator = defaultComparator;
|
|
72
78
|
exports.descComparator = descComparator;
|
|
73
79
|
exports.makeComparator = makeComparator;
|
|
80
|
+
exports.normalizeValue = normalizeValue;
|
|
74
81
|
//# sourceMappingURL=comparison.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comparison.cjs","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.cjs","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;;;;;;"}
|
|
@@ -13,3 +13,7 @@ export declare const descComparator: (a: unknown, b: unknown, opts: CompareOptio
|
|
|
13
13
|
export declare function makeComparator(opts: CompareOptions): (a: any, b: any) => number;
|
|
14
14
|
/** Default comparator orders values in ascending order with nulls first and locale string comparison. */
|
|
15
15
|
export declare const defaultComparator: (a: any, b: any) => number;
|
|
16
|
+
/**
|
|
17
|
+
* Normalize a value for comparison
|
|
18
|
+
*/
|
|
19
|
+
export declare function normalizeValue(value: any): any;
|
|
@@ -11,7 +11,8 @@ class CollectionEventsManager {
|
|
|
11
11
|
}
|
|
12
12
|
this.listeners.get(event).add(callback);
|
|
13
13
|
return () => {
|
|
14
|
-
|
|
14
|
+
var _a;
|
|
15
|
+
(_a = this.listeners.get(event)) == null ? void 0 : _a.delete(callback);
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
once(event, callback) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sources":["../../../src/collection/events.ts"],"sourcesContent":["import type { Collection } from \"./index.js\"\nimport type { CollectionStatus } from \"../types.js\"\n\n/**\n * Event emitted when the collection status changes\n */\nexport interface CollectionStatusChangeEvent {\n type: `status:change`\n collection: Collection\n previousStatus: CollectionStatus\n status: CollectionStatus\n}\n\n/**\n * Event emitted when the collection status changes to a specific status\n */\nexport interface CollectionStatusEvent<T extends CollectionStatus> {\n type: `status:${T}`\n collection: Collection\n previousStatus: CollectionStatus\n status: T\n}\n\n/**\n * Event emitted when the number of subscribers to the collection changes\n */\nexport interface CollectionSubscribersChangeEvent {\n type: `subscribers:change`\n collection: Collection\n previousSubscriberCount: number\n subscriberCount: number\n}\n\nexport type AllCollectionEvents = {\n \"status:change\": CollectionStatusChangeEvent\n \"subscribers:change\": CollectionSubscribersChangeEvent\n} & {\n [K in CollectionStatus as `status:${K}`]: CollectionStatusEvent<K>\n}\n\nexport type CollectionEvent =\n | AllCollectionEvents[keyof AllCollectionEvents]\n | CollectionStatusChangeEvent\n | CollectionSubscribersChangeEvent\n\nexport type CollectionEventHandler<T extends keyof AllCollectionEvents> = (\n event: AllCollectionEvents[T]\n) => void\n\nexport class CollectionEventsManager {\n private collection!: Collection<any, any, any, any, any>\n private listeners = new Map<\n keyof AllCollectionEvents,\n Set<CollectionEventHandler<any>>\n >()\n\n constructor() {}\n\n setDeps(deps: { collection: Collection<any, any, any, any, any> }) {\n this.collection = deps.collection\n }\n\n on<T extends keyof AllCollectionEvents>(\n event: T,\n callback: CollectionEventHandler<T>\n ) {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set())\n }\n this.listeners.get(event)!.add(callback)\n\n return () => {\n this.listeners.get(event)
|
|
1
|
+
{"version":3,"file":"events.js","sources":["../../../src/collection/events.ts"],"sourcesContent":["import type { Collection } from \"./index.js\"\nimport type { CollectionStatus } from \"../types.js\"\n\n/**\n * Event emitted when the collection status changes\n */\nexport interface CollectionStatusChangeEvent {\n type: `status:change`\n collection: Collection\n previousStatus: CollectionStatus\n status: CollectionStatus\n}\n\n/**\n * Event emitted when the collection status changes to a specific status\n */\nexport interface CollectionStatusEvent<T extends CollectionStatus> {\n type: `status:${T}`\n collection: Collection\n previousStatus: CollectionStatus\n status: T\n}\n\n/**\n * Event emitted when the number of subscribers to the collection changes\n */\nexport interface CollectionSubscribersChangeEvent {\n type: `subscribers:change`\n collection: Collection\n previousSubscriberCount: number\n subscriberCount: number\n}\n\nexport type AllCollectionEvents = {\n \"status:change\": CollectionStatusChangeEvent\n \"subscribers:change\": CollectionSubscribersChangeEvent\n} & {\n [K in CollectionStatus as `status:${K}`]: CollectionStatusEvent<K>\n}\n\nexport type CollectionEvent =\n | AllCollectionEvents[keyof AllCollectionEvents]\n | CollectionStatusChangeEvent\n | CollectionSubscribersChangeEvent\n\nexport type CollectionEventHandler<T extends keyof AllCollectionEvents> = (\n event: AllCollectionEvents[T]\n) => void\n\nexport class CollectionEventsManager {\n private collection!: Collection<any, any, any, any, any>\n private listeners = new Map<\n keyof AllCollectionEvents,\n Set<CollectionEventHandler<any>>\n >()\n\n constructor() {}\n\n setDeps(deps: { collection: Collection<any, any, any, any, any> }) {\n this.collection = deps.collection\n }\n\n on<T extends keyof AllCollectionEvents>(\n event: T,\n callback: CollectionEventHandler<T>\n ) {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set())\n }\n this.listeners.get(event)!.add(callback)\n\n return () => {\n this.listeners.get(event)?.delete(callback)\n }\n }\n\n once<T extends keyof AllCollectionEvents>(\n event: T,\n callback: CollectionEventHandler<T>\n ) {\n const unsubscribe = this.on(event, (eventPayload) => {\n callback(eventPayload)\n unsubscribe()\n })\n return unsubscribe\n }\n\n off<T extends keyof AllCollectionEvents>(\n event: T,\n callback: CollectionEventHandler<T>\n ) {\n this.listeners.get(event)?.delete(callback)\n }\n\n waitFor<T extends keyof AllCollectionEvents>(\n event: T,\n timeout?: number\n ): Promise<AllCollectionEvents[T]> {\n return new Promise((resolve, reject) => {\n let timeoutId: NodeJS.Timeout | undefined\n const unsubscribe = this.on(event, (eventPayload) => {\n if (timeoutId) {\n clearTimeout(timeoutId)\n timeoutId = undefined\n }\n resolve(eventPayload)\n unsubscribe()\n })\n if (timeout) {\n timeoutId = setTimeout(() => {\n timeoutId = undefined\n unsubscribe()\n reject(new Error(`Timeout waiting for event ${event}`))\n }, timeout)\n }\n })\n }\n\n emit<T extends keyof AllCollectionEvents>(\n event: T,\n eventPayload: AllCollectionEvents[T]\n ) {\n this.listeners.get(event)?.forEach((listener) => {\n try {\n listener(eventPayload)\n } catch (error) {\n // Re-throw in a microtask to surface the error\n queueMicrotask(() => {\n throw error\n })\n }\n })\n }\n\n emitStatusChange<T extends CollectionStatus>(\n status: T,\n previousStatus: CollectionStatus\n ) {\n this.emit(`status:change`, {\n type: `status:change`,\n collection: this.collection,\n previousStatus,\n status,\n })\n\n // Emit specific status event using type assertion\n const eventKey: `status:${T}` = `status:${status}`\n this.emit(eventKey, {\n type: eventKey,\n collection: this.collection,\n previousStatus,\n status,\n } as AllCollectionEvents[`status:${T}`])\n }\n\n emitSubscribersChange(\n subscriberCount: number,\n previousSubscriberCount: number\n ) {\n this.emit(`subscribers:change`, {\n type: `subscribers:change`,\n collection: this.collection,\n previousSubscriberCount,\n subscriberCount,\n })\n }\n\n cleanup() {\n this.listeners.clear()\n }\n}\n"],"names":[],"mappings":"AAiDO,MAAM,wBAAwB;AAAA,EAOnC,cAAc;AALd,SAAQ,gCAAgB,IAAA;AAAA,EAKT;AAAA,EAEf,QAAQ,MAA2D;AACjE,SAAK,aAAa,KAAK;AAAA,EACzB;AAAA,EAEA,GACE,OACA,UACA;AACA,QAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAAG;AAC9B,WAAK,UAAU,IAAI,OAAO,oBAAI,KAAK;AAAA,IACrC;AACA,SAAK,UAAU,IAAI,KAAK,EAAG,IAAI,QAAQ;AAEvC,WAAO,MAAM;AAtBV;AAuBD,iBAAK,UAAU,IAAI,KAAK,MAAxB,mBAA2B,OAAO;AAAA,IACpC;AAAA,EACF;AAAA,EAEA,KACE,OACA,UACA;AACA,UAAM,cAAc,KAAK,GAAG,OAAO,CAAC,iBAAiB;AACnD,eAAS,YAAY;AACrB,kBAAA;AAAA,IACF,CAAC;AACD,WAAO;AAAA,EACT;AAAA,EAEA,IACE,OACA,UACA;AAzCG;AA0CH,eAAK,UAAU,IAAI,KAAK,MAAxB,mBAA2B,OAAO;AAAA,EACpC;AAAA,EAEA,QACE,OACA,SACiC;AACjC,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAI;AACJ,YAAM,cAAc,KAAK,GAAG,OAAO,CAAC,iBAAiB;AACnD,YAAI,WAAW;AACb,uBAAa,SAAS;AACtB,sBAAY;AAAA,QACd;AACA,gBAAQ,YAAY;AACpB,oBAAA;AAAA,MACF,CAAC;AACD,UAAI,SAAS;AACX,oBAAY,WAAW,MAAM;AAC3B,sBAAY;AACZ,sBAAA;AACA,iBAAO,IAAI,MAAM,6BAA6B,KAAK,EAAE,CAAC;AAAA,QACxD,GAAG,OAAO;AAAA,MACZ;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,KACE,OACA,cACA;AAxEG;AAyEH,eAAK,UAAU,IAAI,KAAK,MAAxB,mBAA2B,QAAQ,CAAC,aAAa;AAC/C,UAAI;AACF,iBAAS,YAAY;AAAA,MACvB,SAAS,OAAO;AAEd,uBAAe,MAAM;AACnB,gBAAM;AAAA,QACR,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA,EAEA,iBACE,QACA,gBACA;AACA,SAAK,KAAK,iBAAiB;AAAA,MACzB,MAAM;AAAA,MACN,YAAY,KAAK;AAAA,MACjB;AAAA,MACA;AAAA,IAAA,CACD;AAGD,UAAM,WAA0B,UAAU,MAAM;AAChD,SAAK,KAAK,UAAU;AAAA,MAClB,MAAM;AAAA,MACN,YAAY,KAAK;AAAA,MACjB;AAAA,MACA;AAAA,IAAA,CACqC;AAAA,EACzC;AAAA,EAEA,sBACE,iBACA,yBACA;AACA,SAAK,KAAK,sBAAsB;AAAA,MAC9B,MAAM;AAAA,MACN,YAAY,KAAK;AAAA,MACjB;AAAA,MACA;AAAA,IAAA,CACD;AAAA,EACH;AAAA,EAEA,UAAU;AACR,SAAK,UAAU,MAAA;AAAA,EACjB;AACF;"}
|
|
@@ -38,7 +38,7 @@ export declare class CollectionLifecycleManager<TOutput extends object = Record<
|
|
|
38
38
|
* Safely update the collection status with validation
|
|
39
39
|
* @private
|
|
40
40
|
*/
|
|
41
|
-
setStatus(newStatus: CollectionStatus): void;
|
|
41
|
+
setStatus(newStatus: CollectionStatus, allowReady?: boolean): void;
|
|
42
42
|
/**
|
|
43
43
|
* Validates that the collection is in a usable state for data operations
|
|
44
44
|
* @private
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InvalidCollectionStatusTransitionError, CollectionInErrorStateError } from "../errors.js";
|
|
1
|
+
import { InvalidCollectionStatusTransitionError, CollectionStateError, CollectionInErrorStateError } from "../errors.js";
|
|
2
2
|
import { safeCancelIdleCallback, safeRequestIdleCallback } from "../utils/browser-polyfills.js";
|
|
3
3
|
class CollectionLifecycleManager {
|
|
4
4
|
/**
|
|
@@ -44,7 +44,12 @@ class CollectionLifecycleManager {
|
|
|
44
44
|
* Safely update the collection status with validation
|
|
45
45
|
* @private
|
|
46
46
|
*/
|
|
47
|
-
setStatus(newStatus) {
|
|
47
|
+
setStatus(newStatus, allowReady = false) {
|
|
48
|
+
if (newStatus === `ready` && !allowReady) {
|
|
49
|
+
throw new CollectionStateError(
|
|
50
|
+
`You can't directly call "setStatus('ready'). You must use markReady instead.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
48
53
|
this.validateStatusTransition(this.status, newStatus);
|
|
49
54
|
const previousStatus = this.status;
|
|
50
55
|
this.status = newStatus;
|
|
@@ -75,8 +80,9 @@ class CollectionLifecycleManager {
|
|
|
75
80
|
* @private - Should only be called by sync implementations
|
|
76
81
|
*/
|
|
77
82
|
markReady() {
|
|
83
|
+
this.validateStatusTransition(this.status, `ready`);
|
|
78
84
|
if (this.status === `loading` || this.status === `initialCommit`) {
|
|
79
|
-
this.setStatus(`ready
|
|
85
|
+
this.setStatus(`ready`, true);
|
|
80
86
|
if (!this.hasBeenReady) {
|
|
81
87
|
this.hasBeenReady = true;
|
|
82
88
|
if (!this.hasReceivedFirstCommit) {
|
|
@@ -86,9 +92,9 @@ class CollectionLifecycleManager {
|
|
|
86
92
|
this.onFirstReadyCallbacks = [];
|
|
87
93
|
callbacks.forEach((callback) => callback());
|
|
88
94
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
if (this.changes.changeSubscriptions.size > 0) {
|
|
96
|
+
this.changes.emitEmptyReadyEvent();
|
|
97
|
+
}
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
100
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.js","sources":["../../../src/collection/lifecycle.ts"],"sourcesContent":["import {\n CollectionInErrorStateError,\n InvalidCollectionStatusTransitionError,\n} from \"../errors\"\nimport {\n safeCancelIdleCallback,\n safeRequestIdleCallback,\n} from \"../utils/browser-polyfills\"\nimport type { IdleCallbackDeadline } from \"../utils/browser-polyfills\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type { CollectionConfig, CollectionStatus } from \"../types\"\nimport type { CollectionEventsManager } from \"./events\"\nimport type { CollectionIndexesManager } from \"./indexes\"\nimport type { CollectionChangesManager } from \"./changes\"\nimport type { CollectionSyncManager } from \"./sync\"\nimport type { CollectionStateManager } from \"./state\"\n\nexport class CollectionLifecycleManager<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n private config: CollectionConfig<TOutput, TKey, TSchema>\n private id: string\n private indexes!: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n private events!: CollectionEventsManager\n private changes!: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n\n public status: CollectionStatus = `idle`\n public hasBeenReady = false\n public hasReceivedFirstCommit = false\n public onFirstReadyCallbacks: Array<() => void> = []\n public gcTimeoutId: ReturnType<typeof setTimeout> | null = null\n private idleCallbackId: number | null = null\n\n /**\n * Creates a new CollectionLifecycleManager instance\n */\n constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string) {\n this.config = config\n this.id = id\n }\n\n setDeps(deps: {\n indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n events: CollectionEventsManager\n changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n state: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n }) {\n this.indexes = deps.indexes\n this.events = deps.events\n this.changes = deps.changes\n this.sync = deps.sync\n this.state = deps.state\n }\n\n /**\n * Validates state transitions to prevent invalid status changes\n */\n public validateStatusTransition(\n from: CollectionStatus,\n to: CollectionStatus\n ): void {\n if (from === to) {\n // Allow same state transitions\n return\n }\n const validTransitions: Record<\n CollectionStatus,\n Array<CollectionStatus>\n > = {\n idle: [`loading`, `error`, `cleaned-up`],\n loading: [`initialCommit`, `ready`, `error`, `cleaned-up`],\n initialCommit: [`ready`, `error`, `cleaned-up`],\n ready: [`cleaned-up`, `error`],\n error: [`cleaned-up`, `idle`],\n \"cleaned-up\": [`loading`, `error`],\n }\n\n if (!validTransitions[from].includes(to)) {\n throw new InvalidCollectionStatusTransitionError(from, to, this.id)\n }\n }\n\n /**\n * Safely update the collection status with validation\n * @private\n */\n public setStatus(newStatus: CollectionStatus): void {\n this.validateStatusTransition(this.status, newStatus)\n const previousStatus = this.status\n this.status = newStatus\n\n // Resolve indexes when collection becomes ready\n if (newStatus === `ready` && !this.indexes.isIndexesResolved) {\n // Resolve indexes asynchronously without blocking\n this.indexes.resolveAllIndexes().catch((error) => {\n console.warn(`Failed to resolve indexes:`, error)\n })\n }\n\n // Emit event\n this.events.emitStatusChange(newStatus, previousStatus)\n }\n\n /**\n * Validates that the collection is in a usable state for data operations\n * @private\n */\n public validateCollectionUsable(operation: string): void {\n switch (this.status) {\n case `error`:\n throw new CollectionInErrorStateError(operation, this.id)\n case `cleaned-up`:\n // Automatically restart the collection when operations are called on cleaned-up collections\n this.sync.startSync()\n break\n }\n }\n\n /**\n * Mark the collection as ready for use\n * This is called by sync implementations to explicitly signal that the collection is ready,\n * providing a more intuitive alternative to using commits for readiness signaling\n * @private - Should only be called by sync implementations\n */\n public markReady(): void {\n // Can transition to ready from loading or initialCommit states\n if (this.status === `loading` || this.status === `initialCommit`) {\n this.setStatus(`ready`)\n\n // Call any registered first ready callbacks (only on first time becoming ready)\n if (!this.hasBeenReady) {\n this.hasBeenReady = true\n\n // Also mark as having received first commit for backwards compatibility\n if (!this.hasReceivedFirstCommit) {\n this.hasReceivedFirstCommit = true\n }\n\n const callbacks = [...this.onFirstReadyCallbacks]\n this.onFirstReadyCallbacks = []\n callbacks.forEach((callback) => callback())\n }\n }\n\n // Always notify dependents when markReady is called, after status is set\n // This ensures live queries get notified when their dependencies become ready\n if (this.changes.changeSubscriptions.size > 0) {\n this.changes.emitEmptyReadyEvent()\n }\n }\n\n /**\n * Start the garbage collection timer\n * Called when the collection becomes inactive (no subscribers)\n */\n public startGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n }\n\n const gcTime = this.config.gcTime ?? 300000 // 5 minutes default\n\n // If gcTime is 0, GC is disabled\n if (gcTime === 0) {\n return\n }\n\n this.gcTimeoutId = setTimeout(() => {\n if (this.changes.activeSubscribersCount === 0) {\n // Schedule cleanup during idle time to avoid blocking the UI thread\n this.scheduleIdleCleanup()\n }\n }, gcTime)\n }\n\n /**\n * Cancel the garbage collection timer\n * Called when the collection becomes active again\n */\n public cancelGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n // Also cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n }\n\n /**\n * Schedule cleanup to run during browser idle time\n * This prevents blocking the UI thread during cleanup operations\n */\n private scheduleIdleCleanup(): void {\n // Cancel any existing idle callback\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n }\n\n // Schedule cleanup with a timeout of 1 second\n // This ensures cleanup happens even if the browser is busy\n this.idleCallbackId = safeRequestIdleCallback(\n (deadline) => {\n // Perform cleanup if we still have no subscribers\n if (this.changes.activeSubscribersCount === 0) {\n const cleanupCompleted = this.performCleanup(deadline)\n // Only clear the callback ID if cleanup actually completed\n if (cleanupCompleted) {\n this.idleCallbackId = null\n }\n } else {\n // No need to cleanup, clear the callback ID\n this.idleCallbackId = null\n }\n },\n { timeout: 1000 }\n )\n }\n\n /**\n * Perform cleanup operations, optionally in chunks during idle time\n * @returns true if cleanup was completed, false if it was rescheduled\n */\n private performCleanup(deadline?: IdleCallbackDeadline): boolean {\n // If we have a deadline, we can potentially split cleanup into chunks\n // For now, we'll do all cleanup at once but check if we have time\n const hasTime =\n !deadline || deadline.timeRemaining() > 0 || deadline.didTimeout\n\n if (hasTime) {\n // Perform all cleanup operations\n this.events.cleanup()\n this.sync.cleanup()\n this.state.cleanup()\n this.changes.cleanup()\n this.indexes.cleanup()\n\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n\n this.hasBeenReady = false\n this.onFirstReadyCallbacks = []\n\n // Set status to cleaned-up\n this.setStatus(`cleaned-up`)\n return true\n } else {\n // If we don't have time, reschedule for the next idle period\n this.scheduleIdleCleanup()\n return false\n }\n }\n\n /**\n * Register a callback to be executed when the collection first becomes ready\n * Useful for preloading collections\n * @param callback Function to call when the collection first becomes ready\n */\n public onFirstReady(callback: () => void): void {\n // If already ready, call immediately\n if (this.hasBeenReady) {\n callback()\n return\n }\n\n this.onFirstReadyCallbacks.push(callback)\n }\n\n public cleanup(): void {\n // Cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n\n // Perform cleanup immediately (used when explicitly called)\n this.performCleanup()\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecycle.js","sources":["../../../src/collection/lifecycle.ts"],"sourcesContent":["import {\n CollectionInErrorStateError,\n CollectionStateError,\n InvalidCollectionStatusTransitionError,\n} from \"../errors\"\nimport {\n safeCancelIdleCallback,\n safeRequestIdleCallback,\n} from \"../utils/browser-polyfills\"\nimport type { IdleCallbackDeadline } from \"../utils/browser-polyfills\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type { CollectionConfig, CollectionStatus } from \"../types\"\nimport type { CollectionEventsManager } from \"./events\"\nimport type { CollectionIndexesManager } from \"./indexes\"\nimport type { CollectionChangesManager } from \"./changes\"\nimport type { CollectionSyncManager } from \"./sync\"\nimport type { CollectionStateManager } from \"./state\"\n\nexport class CollectionLifecycleManager<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n private config: CollectionConfig<TOutput, TKey, TSchema>\n private id: string\n private indexes!: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n private events!: CollectionEventsManager\n private changes!: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n\n public status: CollectionStatus = `idle`\n public hasBeenReady = false\n public hasReceivedFirstCommit = false\n public onFirstReadyCallbacks: Array<() => void> = []\n public gcTimeoutId: ReturnType<typeof setTimeout> | null = null\n private idleCallbackId: number | null = null\n\n /**\n * Creates a new CollectionLifecycleManager instance\n */\n constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string) {\n this.config = config\n this.id = id\n }\n\n setDeps(deps: {\n indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n events: CollectionEventsManager\n changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n state: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n }) {\n this.indexes = deps.indexes\n this.events = deps.events\n this.changes = deps.changes\n this.sync = deps.sync\n this.state = deps.state\n }\n\n /**\n * Validates state transitions to prevent invalid status changes\n */\n public validateStatusTransition(\n from: CollectionStatus,\n to: CollectionStatus\n ): void {\n if (from === to) {\n // Allow same state transitions\n return\n }\n const validTransitions: Record<\n CollectionStatus,\n Array<CollectionStatus>\n > = {\n idle: [`loading`, `error`, `cleaned-up`],\n loading: [`initialCommit`, `ready`, `error`, `cleaned-up`],\n initialCommit: [`ready`, `error`, `cleaned-up`],\n ready: [`cleaned-up`, `error`],\n error: [`cleaned-up`, `idle`],\n \"cleaned-up\": [`loading`, `error`],\n }\n\n if (!validTransitions[from].includes(to)) {\n throw new InvalidCollectionStatusTransitionError(from, to, this.id)\n }\n }\n\n /**\n * Safely update the collection status with validation\n * @private\n */\n public setStatus(\n newStatus: CollectionStatus,\n allowReady: boolean = false\n ): void {\n if (newStatus === `ready` && !allowReady) {\n // setStatus('ready') is an internal method that should not be called directly\n // Instead, use markReady to transition to ready triggering the necessary events\n // and side effects.\n throw new CollectionStateError(\n `You can't directly call \"setStatus('ready'). You must use markReady instead.`\n )\n }\n this.validateStatusTransition(this.status, newStatus)\n const previousStatus = this.status\n this.status = newStatus\n\n // Resolve indexes when collection becomes ready\n if (newStatus === `ready` && !this.indexes.isIndexesResolved) {\n // Resolve indexes asynchronously without blocking\n this.indexes.resolveAllIndexes().catch((error) => {\n console.warn(`Failed to resolve indexes:`, error)\n })\n }\n\n // Emit event\n this.events.emitStatusChange(newStatus, previousStatus)\n }\n\n /**\n * Validates that the collection is in a usable state for data operations\n * @private\n */\n public validateCollectionUsable(operation: string): void {\n switch (this.status) {\n case `error`:\n throw new CollectionInErrorStateError(operation, this.id)\n case `cleaned-up`:\n // Automatically restart the collection when operations are called on cleaned-up collections\n this.sync.startSync()\n break\n }\n }\n\n /**\n * Mark the collection as ready for use\n * This is called by sync implementations to explicitly signal that the collection is ready,\n * providing a more intuitive alternative to using commits for readiness signaling\n * @private - Should only be called by sync implementations\n */\n public markReady(): void {\n this.validateStatusTransition(this.status, `ready`)\n // Can transition to ready from loading or initialCommit states\n if (this.status === `loading` || this.status === `initialCommit`) {\n this.setStatus(`ready`, true)\n\n // Call any registered first ready callbacks (only on first time becoming ready)\n if (!this.hasBeenReady) {\n this.hasBeenReady = true\n\n // Also mark as having received first commit for backwards compatibility\n if (!this.hasReceivedFirstCommit) {\n this.hasReceivedFirstCommit = true\n }\n\n const callbacks = [...this.onFirstReadyCallbacks]\n this.onFirstReadyCallbacks = []\n callbacks.forEach((callback) => callback())\n }\n // Notify dependents when markReady is called, after status is set\n // This ensures live queries get notified when their dependencies become ready\n if (this.changes.changeSubscriptions.size > 0) {\n this.changes.emitEmptyReadyEvent()\n }\n }\n }\n\n /**\n * Start the garbage collection timer\n * Called when the collection becomes inactive (no subscribers)\n */\n public startGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n }\n\n const gcTime = this.config.gcTime ?? 300000 // 5 minutes default\n\n // If gcTime is 0, GC is disabled\n if (gcTime === 0) {\n return\n }\n\n this.gcTimeoutId = setTimeout(() => {\n if (this.changes.activeSubscribersCount === 0) {\n // Schedule cleanup during idle time to avoid blocking the UI thread\n this.scheduleIdleCleanup()\n }\n }, gcTime)\n }\n\n /**\n * Cancel the garbage collection timer\n * Called when the collection becomes active again\n */\n public cancelGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n // Also cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n }\n\n /**\n * Schedule cleanup to run during browser idle time\n * This prevents blocking the UI thread during cleanup operations\n */\n private scheduleIdleCleanup(): void {\n // Cancel any existing idle callback\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n }\n\n // Schedule cleanup with a timeout of 1 second\n // This ensures cleanup happens even if the browser is busy\n this.idleCallbackId = safeRequestIdleCallback(\n (deadline) => {\n // Perform cleanup if we still have no subscribers\n if (this.changes.activeSubscribersCount === 0) {\n const cleanupCompleted = this.performCleanup(deadline)\n // Only clear the callback ID if cleanup actually completed\n if (cleanupCompleted) {\n this.idleCallbackId = null\n }\n } else {\n // No need to cleanup, clear the callback ID\n this.idleCallbackId = null\n }\n },\n { timeout: 1000 }\n )\n }\n\n /**\n * Perform cleanup operations, optionally in chunks during idle time\n * @returns true if cleanup was completed, false if it was rescheduled\n */\n private performCleanup(deadline?: IdleCallbackDeadline): boolean {\n // If we have a deadline, we can potentially split cleanup into chunks\n // For now, we'll do all cleanup at once but check if we have time\n const hasTime =\n !deadline || deadline.timeRemaining() > 0 || deadline.didTimeout\n\n if (hasTime) {\n // Perform all cleanup operations\n this.events.cleanup()\n this.sync.cleanup()\n this.state.cleanup()\n this.changes.cleanup()\n this.indexes.cleanup()\n\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n\n this.hasBeenReady = false\n this.onFirstReadyCallbacks = []\n\n // Set status to cleaned-up\n this.setStatus(`cleaned-up`)\n return true\n } else {\n // If we don't have time, reschedule for the next idle period\n this.scheduleIdleCleanup()\n return false\n }\n }\n\n /**\n * Register a callback to be executed when the collection first becomes ready\n * Useful for preloading collections\n * @param callback Function to call when the collection first becomes ready\n */\n public onFirstReady(callback: () => void): void {\n // If already ready, call immediately\n if (this.hasBeenReady) {\n callback()\n return\n }\n\n this.onFirstReadyCallbacks.push(callback)\n }\n\n public cleanup(): void {\n // Cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n\n // Perform cleanup immediately (used when explicitly called)\n this.performCleanup()\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.js","sources":["../../../src/collection/state.ts"],"sourcesContent":["import { deepEquals } from \"../utils\"\nimport { SortedMap } from \"../SortedMap\"\nimport type { Transaction } from \"../transactions\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n ChangeMessage,\n CollectionConfig,\n OptimisticChangeMessage,\n} from \"../types\"\nimport type { CollectionImpl } from \"./index.js\"\nimport type { CollectionLifecycleManager } from \"./lifecycle\"\nimport type { CollectionChangesManager } from \"./changes\"\nimport type { CollectionIndexesManager } from \"./indexes\"\n\ninterface PendingSyncedTransaction<T extends object = Record<string, unknown>> {\n committed: boolean\n operations: Array<OptimisticChangeMessage<T>>\n truncate?: boolean\n deletedKeys: Set<string | number>\n}\n\nexport class CollectionStateManager<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n public config!: CollectionConfig<TOutput, TKey, TSchema>\n public collection!: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n public lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n public changes!: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n public indexes!: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n\n // Core state - make public for testing\n public transactions: SortedMap<string, Transaction<any>>\n public pendingSyncedTransactions: Array<PendingSyncedTransaction<TOutput>> =\n []\n public syncedData: Map<TKey, TOutput> | SortedMap<TKey, TOutput>\n public syncedMetadata = new Map<TKey, unknown>()\n\n // Optimistic state tracking - make public for testing\n public optimisticUpserts = new Map<TKey, TOutput>()\n public optimisticDeletes = new Set<TKey>()\n\n // Cached size for performance\n public size = 0\n\n // State used for computing the change events\n public syncedKeys = new Set<TKey>()\n public preSyncVisibleState = new Map<TKey, TOutput>()\n public recentlySyncedKeys = new Set<TKey>()\n public hasReceivedFirstCommit = false\n public isCommittingSyncTransactions = false\n\n /**\n * Creates a new CollectionState manager\n */\n constructor(config: CollectionConfig<TOutput, TKey, TSchema>) {\n this.config = config\n this.transactions = new SortedMap<string, Transaction<any>>((a, b) =>\n a.compareCreatedAt(b)\n )\n\n // Set up data storage with optional comparison function\n if (config.compare) {\n this.syncedData = new SortedMap<TKey, TOutput>(config.compare)\n } else {\n this.syncedData = new Map<TKey, TOutput>()\n }\n }\n\n setDeps(deps: {\n collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n }) {\n this.collection = deps.collection\n this.lifecycle = deps.lifecycle\n this.changes = deps.changes\n this.indexes = deps.indexes\n }\n\n /**\n * Get the current value for a key (virtual derived state)\n */\n public get(key: TKey): TOutput | undefined {\n const { optimisticDeletes, optimisticUpserts, syncedData } = this\n // Check if optimistically deleted\n if (optimisticDeletes.has(key)) {\n return undefined\n }\n\n // Check optimistic upserts first\n if (optimisticUpserts.has(key)) {\n return optimisticUpserts.get(key)\n }\n\n // Fall back to synced data\n return syncedData.get(key)\n }\n\n /**\n * Check if a key exists in the collection (virtual derived state)\n */\n public has(key: TKey): boolean {\n const { optimisticDeletes, optimisticUpserts, syncedData } = this\n // Check if optimistically deleted\n if (optimisticDeletes.has(key)) {\n return false\n }\n\n // Check optimistic upserts first\n if (optimisticUpserts.has(key)) {\n return true\n }\n\n // Fall back to synced data\n return syncedData.has(key)\n }\n\n /**\n * Get all keys (virtual derived state)\n */\n public *keys(): IterableIterator<TKey> {\n const { syncedData, optimisticDeletes, optimisticUpserts } = this\n // Yield keys from synced data, skipping any that are deleted.\n for (const key of syncedData.keys()) {\n if (!optimisticDeletes.has(key)) {\n yield key\n }\n }\n // Yield keys from upserts that were not already in synced data.\n for (const key of optimisticUpserts.keys()) {\n if (!syncedData.has(key) && !optimisticDeletes.has(key)) {\n // The optimisticDeletes check is technically redundant if inserts/updates always remove from deletes,\n // but it's safer to keep it.\n yield key\n }\n }\n }\n\n /**\n * Get all values (virtual derived state)\n */\n public *values(): IterableIterator<TOutput> {\n for (const key of this.keys()) {\n const value = this.get(key)\n if (value !== undefined) {\n yield value\n }\n }\n }\n\n /**\n * Get all entries (virtual derived state)\n */\n public *entries(): IterableIterator<[TKey, TOutput]> {\n for (const key of this.keys()) {\n const value = this.get(key)\n if (value !== undefined) {\n yield [key, value]\n }\n }\n }\n\n /**\n * Get all entries (virtual derived state)\n */\n public *[Symbol.iterator](): IterableIterator<[TKey, TOutput]> {\n for (const [key, value] of this.entries()) {\n yield [key, value]\n }\n }\n\n /**\n * Execute a callback for each entry in the collection\n */\n public forEach(\n callbackfn: (value: TOutput, key: TKey, index: number) => void\n ): void {\n let index = 0\n for (const [key, value] of this.entries()) {\n callbackfn(value, key, index++)\n }\n }\n\n /**\n * Create a new array with the results of calling a function for each entry in the collection\n */\n public map<U>(\n callbackfn: (value: TOutput, key: TKey, index: number) => U\n ): Array<U> {\n const result: Array<U> = []\n let index = 0\n for (const [key, value] of this.entries()) {\n result.push(callbackfn(value, key, index++))\n }\n return result\n }\n\n /**\n * Check if the given collection is this collection\n * @param collection The collection to check\n * @returns True if the given collection is this collection, false otherwise\n */\n private isThisCollection(\n collection: CollectionImpl<any, any, any, any, any>\n ): boolean {\n return collection === this.collection\n }\n\n /**\n * Recompute optimistic state from active transactions\n */\n public recomputeOptimisticState(\n triggeredByUserAction: boolean = false\n ): void {\n // Skip redundant recalculations when we're in the middle of committing sync transactions\n if (this.isCommittingSyncTransactions) {\n return\n }\n\n const previousState = new Map(this.optimisticUpserts)\n const previousDeletes = new Set(this.optimisticDeletes)\n\n // Clear current optimistic state\n this.optimisticUpserts.clear()\n this.optimisticDeletes.clear()\n\n const activeTransactions: Array<Transaction<any>> = []\n\n for (const transaction of this.transactions.values()) {\n if (![`completed`, `failed`].includes(transaction.state)) {\n activeTransactions.push(transaction)\n }\n }\n\n // Apply active transactions only (completed transactions are handled by sync operations)\n for (const transaction of activeTransactions) {\n for (const mutation of transaction.mutations) {\n if (this.isThisCollection(mutation.collection) && mutation.optimistic) {\n switch (mutation.type) {\n case `insert`:\n case `update`:\n this.optimisticUpserts.set(\n mutation.key,\n mutation.modified as TOutput\n )\n this.optimisticDeletes.delete(mutation.key)\n break\n case `delete`:\n this.optimisticUpserts.delete(mutation.key)\n this.optimisticDeletes.add(mutation.key)\n break\n }\n }\n }\n }\n\n // Update cached size\n this.size = this.calculateSize()\n\n // Collect events for changes\n const events: Array<ChangeMessage<TOutput, TKey>> = []\n this.collectOptimisticChanges(previousState, previousDeletes, events)\n\n // Filter out events for recently synced keys to prevent duplicates\n // BUT: Only filter out events that are actually from sync operations\n // New user transactions should NOT be filtered even if the key was recently synced\n const filteredEventsBySyncStatus = events.filter((event) => {\n if (!this.recentlySyncedKeys.has(event.key)) {\n return true // Key not recently synced, allow event through\n }\n\n // Key was recently synced - allow if this is a user-triggered action\n if (triggeredByUserAction) {\n return true\n }\n\n // Otherwise filter out duplicate sync events\n return false\n })\n\n // Filter out redundant delete events if there are pending sync transactions\n // that will immediately restore the same data, but only for completed transactions\n // IMPORTANT: Skip complex filtering for user-triggered actions to prevent UI blocking\n if (this.pendingSyncedTransactions.length > 0 && !triggeredByUserAction) {\n const pendingSyncKeys = new Set<TKey>()\n\n // Collect keys from pending sync operations\n for (const transaction of this.pendingSyncedTransactions) {\n for (const operation of transaction.operations) {\n pendingSyncKeys.add(operation.key as TKey)\n }\n }\n\n // Only filter out delete events for keys that:\n // 1. Have pending sync operations AND\n // 2. Are from completed transactions (being cleaned up)\n const filteredEvents = filteredEventsBySyncStatus.filter((event) => {\n if (event.type === `delete` && pendingSyncKeys.has(event.key)) {\n // Check if this delete is from clearing optimistic state of completed transactions\n // We can infer this by checking if we have no remaining optimistic mutations for this key\n const hasActiveOptimisticMutation = activeTransactions.some((tx) =>\n tx.mutations.some(\n (m) => this.isThisCollection(m.collection) && m.key === event.key\n )\n )\n\n if (!hasActiveOptimisticMutation) {\n return false // Skip this delete event as sync will restore the data\n }\n }\n return true\n })\n\n // Update indexes for the filtered events\n if (filteredEvents.length > 0) {\n this.indexes.updateIndexes(filteredEvents)\n }\n this.changes.emitEvents(filteredEvents, triggeredByUserAction)\n } else {\n // Update indexes for all events\n if (filteredEventsBySyncStatus.length > 0) {\n this.indexes.updateIndexes(filteredEventsBySyncStatus)\n }\n // Emit all events if no pending sync transactions\n this.changes.emitEvents(filteredEventsBySyncStatus, triggeredByUserAction)\n }\n }\n\n /**\n * Calculate the current size based on synced data and optimistic changes\n */\n private calculateSize(): number {\n const syncedSize = this.syncedData.size\n const deletesFromSynced = Array.from(this.optimisticDeletes).filter(\n (key) => this.syncedData.has(key) && !this.optimisticUpserts.has(key)\n ).length\n const upsertsNotInSynced = Array.from(this.optimisticUpserts.keys()).filter(\n (key) => !this.syncedData.has(key)\n ).length\n\n return syncedSize - deletesFromSynced + upsertsNotInSynced\n }\n\n /**\n * Collect events for optimistic changes\n */\n private collectOptimisticChanges(\n previousUpserts: Map<TKey, TOutput>,\n previousDeletes: Set<TKey>,\n events: Array<ChangeMessage<TOutput, TKey>>\n ): void {\n const allKeys = new Set([\n ...previousUpserts.keys(),\n ...this.optimisticUpserts.keys(),\n ...previousDeletes,\n ...this.optimisticDeletes,\n ])\n\n for (const key of allKeys) {\n const currentValue = this.get(key)\n const previousValue = this.getPreviousValue(\n key,\n previousUpserts,\n previousDeletes\n )\n\n if (previousValue !== undefined && currentValue === undefined) {\n events.push({ type: `delete`, key, value: previousValue })\n } else if (previousValue === undefined && currentValue !== undefined) {\n events.push({ type: `insert`, key, value: currentValue })\n } else if (\n previousValue !== undefined &&\n currentValue !== undefined &&\n previousValue !== currentValue\n ) {\n events.push({\n type: `update`,\n key,\n value: currentValue,\n previousValue,\n })\n }\n }\n }\n\n /**\n * Get the previous value for a key given previous optimistic state\n */\n private getPreviousValue(\n key: TKey,\n previousUpserts: Map<TKey, TOutput>,\n previousDeletes: Set<TKey>\n ): TOutput | undefined {\n if (previousDeletes.has(key)) {\n return undefined\n }\n if (previousUpserts.has(key)) {\n return previousUpserts.get(key)\n }\n return this.syncedData.get(key)\n }\n\n /**\n * Attempts to commit pending synced transactions if there are no active transactions\n * This method processes operations from pending transactions and applies them to the synced data\n */\n commitPendingTransactions = () => {\n // Check if there are any persisting transaction\n let hasPersistingTransaction = false\n for (const transaction of this.transactions.values()) {\n if (transaction.state === `persisting`) {\n hasPersistingTransaction = true\n break\n }\n }\n\n // pending synced transactions could be either `committed` or still open.\n // we only want to process `committed` transactions here\n const {\n committedSyncedTransactions,\n uncommittedSyncedTransactions,\n hasTruncateSync,\n } = this.pendingSyncedTransactions.reduce(\n (acc, t) => {\n if (t.committed) {\n acc.committedSyncedTransactions.push(t)\n if (t.truncate === true) {\n acc.hasTruncateSync = true\n }\n } else {\n acc.uncommittedSyncedTransactions.push(t)\n }\n return acc\n },\n {\n committedSyncedTransactions: [] as Array<\n PendingSyncedTransaction<TOutput>\n >,\n uncommittedSyncedTransactions: [] as Array<\n PendingSyncedTransaction<TOutput>\n >,\n hasTruncateSync: false,\n }\n )\n\n if (!hasPersistingTransaction || hasTruncateSync) {\n // Set flag to prevent redundant optimistic state recalculations\n this.isCommittingSyncTransactions = true\n\n // First collect all keys that will be affected by sync operations\n const changedKeys = new Set<TKey>()\n for (const transaction of committedSyncedTransactions) {\n for (const operation of transaction.operations) {\n changedKeys.add(operation.key as TKey)\n }\n }\n\n // Use pre-captured state if available (from optimistic scenarios),\n // otherwise capture current state (for pure sync scenarios)\n let currentVisibleState = this.preSyncVisibleState\n if (currentVisibleState.size === 0) {\n // No pre-captured state, capture it now for pure sync operations\n currentVisibleState = new Map<TKey, TOutput>()\n for (const key of changedKeys) {\n const currentValue = this.get(key)\n if (currentValue !== undefined) {\n currentVisibleState.set(key, currentValue)\n }\n }\n }\n\n const events: Array<ChangeMessage<TOutput, TKey>> = []\n const rowUpdateMode = this.config.sync.rowUpdateMode || `partial`\n\n for (const transaction of committedSyncedTransactions) {\n // Handle truncate operations first\n if (transaction.truncate) {\n // TRUNCATE PHASE\n // 1) Emit a delete for every currently-synced key so downstream listeners/indexes\n // observe a clear-before-rebuild. We intentionally skip keys already in\n // optimisticDeletes because their delete was previously emitted by the user.\n for (const key of this.syncedData.keys()) {\n if (this.optimisticDeletes.has(key)) continue\n const previousValue =\n this.optimisticUpserts.get(key) || this.syncedData.get(key)\n if (previousValue !== undefined) {\n events.push({ type: `delete`, key, value: previousValue })\n }\n }\n\n // 2) Clear the authoritative synced base. Subsequent server ops in this\n // same commit will rebuild the base atomically.\n this.syncedData.clear()\n this.syncedMetadata.clear()\n this.syncedKeys.clear()\n\n // 3) Clear currentVisibleState for truncated keys to ensure subsequent operations\n // are compared against the post-truncate state (undefined) rather than pre-truncate state\n // This ensures that re-inserted keys are emitted as INSERT events, not UPDATE events\n for (const key of changedKeys) {\n currentVisibleState.delete(key)\n }\n }\n\n for (const operation of transaction.operations) {\n const key = operation.key as TKey\n this.syncedKeys.add(key)\n\n // Update metadata\n switch (operation.type) {\n case `insert`:\n this.syncedMetadata.set(key, operation.metadata)\n break\n case `update`:\n this.syncedMetadata.set(\n key,\n Object.assign(\n {},\n this.syncedMetadata.get(key),\n operation.metadata\n )\n )\n break\n case `delete`:\n this.syncedMetadata.delete(key)\n break\n }\n\n // Update synced data\n switch (operation.type) {\n case `insert`:\n this.syncedData.set(key, operation.value)\n break\n case `update`: {\n if (rowUpdateMode === `partial`) {\n const updatedValue = Object.assign(\n {},\n this.syncedData.get(key),\n operation.value\n )\n this.syncedData.set(key, updatedValue)\n } else {\n this.syncedData.set(key, operation.value)\n }\n break\n }\n case `delete`:\n this.syncedData.delete(key)\n break\n }\n }\n }\n\n // After applying synced operations, if this commit included a truncate,\n // re-apply optimistic mutations on top of the fresh synced base. This ensures\n // the UI preserves local intent while respecting server rebuild semantics.\n // Ordering: deletes (above) -> server ops (just applied) -> optimistic upserts.\n if (hasTruncateSync) {\n // Avoid duplicating keys that were inserted/updated by synced operations in this commit\n const syncedInsertedOrUpdatedKeys = new Set<TKey>()\n for (const t of committedSyncedTransactions) {\n for (const op of t.operations) {\n if (op.type === `insert` || op.type === `update`) {\n syncedInsertedOrUpdatedKeys.add(op.key as TKey)\n }\n }\n }\n\n // Build re-apply sets from ACTIVE optimistic transactions against the new synced base\n // We do not copy maps; we compute intent directly from transactions to avoid drift.\n const reapplyUpserts = new Map<TKey, TOutput>()\n const reapplyDeletes = new Set<TKey>()\n\n for (const tx of this.transactions.values()) {\n if ([`completed`, `failed`].includes(tx.state)) continue\n for (const mutation of tx.mutations) {\n if (\n !this.isThisCollection(mutation.collection) ||\n !mutation.optimistic\n )\n continue\n const key = mutation.key as TKey\n switch (mutation.type) {\n case `insert`:\n reapplyUpserts.set(key, mutation.modified as TOutput)\n reapplyDeletes.delete(key)\n break\n case `update`: {\n const base = this.syncedData.get(key)\n const next = base\n ? (Object.assign({}, base, mutation.changes) as TOutput)\n : (mutation.modified as TOutput)\n reapplyUpserts.set(key, next)\n reapplyDeletes.delete(key)\n break\n }\n case `delete`:\n reapplyUpserts.delete(key)\n reapplyDeletes.add(key)\n break\n }\n }\n }\n\n // Emit inserts for re-applied upserts, skipping any keys that have an optimistic delete.\n // If the server also inserted/updated the same key in this batch, override that value\n // with the optimistic value to preserve local intent.\n for (const [key, value] of reapplyUpserts) {\n if (reapplyDeletes.has(key)) continue\n if (syncedInsertedOrUpdatedKeys.has(key)) {\n let foundInsert = false\n for (let i = events.length - 1; i >= 0; i--) {\n const evt = events[i]!\n if (evt.key === key && evt.type === `insert`) {\n evt.value = value\n foundInsert = true\n break\n }\n }\n if (!foundInsert) {\n events.push({ type: `insert`, key, value })\n }\n } else {\n events.push({ type: `insert`, key, value })\n }\n }\n\n // Finally, ensure we do NOT insert keys that have an outstanding optimistic delete.\n if (events.length > 0 && reapplyDeletes.size > 0) {\n const filtered: Array<ChangeMessage<TOutput, TKey>> = []\n for (const evt of events) {\n if (evt.type === `insert` && reapplyDeletes.has(evt.key)) {\n continue\n }\n filtered.push(evt)\n }\n events.length = 0\n events.push(...filtered)\n }\n\n // Ensure listeners are active before emitting this critical batch\n if (this.lifecycle.status !== `ready`) {\n this.lifecycle.setStatus(`ready`)\n }\n }\n\n // Maintain optimistic state appropriately\n // Clear optimistic state since sync operations will now provide the authoritative data.\n // Any still-active user transactions will be re-applied below in recompute.\n this.optimisticUpserts.clear()\n this.optimisticDeletes.clear()\n\n // Reset flag and recompute optimistic state for any remaining active transactions\n this.isCommittingSyncTransactions = false\n for (const transaction of this.transactions.values()) {\n if (![`completed`, `failed`].includes(transaction.state)) {\n for (const mutation of transaction.mutations) {\n if (\n this.isThisCollection(mutation.collection) &&\n mutation.optimistic\n ) {\n switch (mutation.type) {\n case `insert`:\n case `update`:\n this.optimisticUpserts.set(\n mutation.key,\n mutation.modified as TOutput\n )\n this.optimisticDeletes.delete(mutation.key)\n break\n case `delete`:\n this.optimisticUpserts.delete(mutation.key)\n this.optimisticDeletes.add(mutation.key)\n break\n }\n }\n }\n }\n }\n\n // Check for redundant sync operations that match completed optimistic operations\n const completedOptimisticOps = new Map<TKey, any>()\n\n for (const transaction of this.transactions.values()) {\n if (transaction.state === `completed`) {\n for (const mutation of transaction.mutations) {\n if (\n this.isThisCollection(mutation.collection) &&\n changedKeys.has(mutation.key)\n ) {\n completedOptimisticOps.set(mutation.key, {\n type: mutation.type,\n value: mutation.modified,\n })\n }\n }\n }\n }\n\n // Now check what actually changed in the final visible state\n for (const key of changedKeys) {\n const previousVisibleValue = currentVisibleState.get(key)\n const newVisibleValue = this.get(key) // This returns the new derived state\n\n // Check if this sync operation is redundant with a completed optimistic operation\n const completedOp = completedOptimisticOps.get(key)\n const isRedundantSync =\n completedOp &&\n newVisibleValue !== undefined &&\n deepEquals(completedOp.value, newVisibleValue)\n\n if (!isRedundantSync) {\n if (\n previousVisibleValue === undefined &&\n newVisibleValue !== undefined\n ) {\n events.push({\n type: `insert`,\n key,\n value: newVisibleValue,\n })\n } else if (\n previousVisibleValue !== undefined &&\n newVisibleValue === undefined\n ) {\n events.push({\n type: `delete`,\n key,\n value: previousVisibleValue,\n })\n } else if (\n previousVisibleValue !== undefined &&\n newVisibleValue !== undefined &&\n !deepEquals(previousVisibleValue, newVisibleValue)\n ) {\n events.push({\n type: `update`,\n key,\n value: newVisibleValue,\n previousValue: previousVisibleValue,\n })\n }\n }\n }\n\n // Update cached size after synced data changes\n this.size = this.calculateSize()\n\n // Update indexes for all events before emitting\n if (events.length > 0) {\n this.indexes.updateIndexes(events)\n }\n\n // End batching and emit all events (combines any batched events with sync events)\n this.changes.emitEvents(events, true)\n\n this.pendingSyncedTransactions = uncommittedSyncedTransactions\n\n // Clear the pre-sync state since sync operations are complete\n this.preSyncVisibleState.clear()\n\n // Clear recently synced keys after a microtask to allow recomputeOptimisticState to see them\n Promise.resolve().then(() => {\n this.recentlySyncedKeys.clear()\n })\n\n // Call any registered one-time commit listeners\n if (!this.hasReceivedFirstCommit) {\n this.hasReceivedFirstCommit = true\n const callbacks = [...this.lifecycle.onFirstReadyCallbacks]\n this.lifecycle.onFirstReadyCallbacks = []\n callbacks.forEach((callback) => callback())\n }\n }\n }\n\n /**\n * Schedule cleanup of a transaction when it completes\n */\n public scheduleTransactionCleanup(transaction: Transaction<any>): void {\n // Only schedule cleanup for transactions that aren't already completed\n if (transaction.state === `completed`) {\n this.transactions.delete(transaction.id)\n return\n }\n\n // Schedule cleanup when the transaction completes\n transaction.isPersisted.promise\n .then(() => {\n // Transaction completed successfully, remove it immediately\n this.transactions.delete(transaction.id)\n })\n .catch(() => {\n // Transaction failed, but we want to keep failed transactions for reference\n // so don't remove it.\n // This empty catch block is necessary to prevent unhandled promise rejections.\n })\n }\n\n /**\n * Capture visible state for keys that will be affected by pending sync operations\n * This must be called BEFORE onTransactionStateChange clears optimistic state\n */\n public capturePreSyncVisibleState(): void {\n if (this.pendingSyncedTransactions.length === 0) return\n\n // Clear any previous capture\n this.preSyncVisibleState.clear()\n\n // Get all keys that will be affected by sync operations\n const syncedKeys = new Set<TKey>()\n for (const transaction of this.pendingSyncedTransactions) {\n for (const operation of transaction.operations) {\n syncedKeys.add(operation.key as TKey)\n }\n }\n\n // Mark keys as about to be synced to suppress intermediate events from recomputeOptimisticState\n for (const key of syncedKeys) {\n this.recentlySyncedKeys.add(key)\n }\n\n // Only capture current visible state for keys that will be affected by sync operations\n // This is much more efficient than capturing the entire collection state\n for (const key of syncedKeys) {\n const currentValue = this.get(key)\n if (currentValue !== undefined) {\n this.preSyncVisibleState.set(key, currentValue)\n }\n }\n }\n\n /**\n * Trigger a recomputation when transactions change\n * This method should be called by the Transaction class when state changes\n */\n public onTransactionStateChange(): void {\n // Check if commitPendingTransactions will be called after this\n // by checking if there are pending sync transactions (same logic as in transactions.ts)\n this.changes.shouldBatchEvents = this.pendingSyncedTransactions.length > 0\n\n // CRITICAL: Capture visible state BEFORE clearing optimistic state\n this.capturePreSyncVisibleState()\n\n this.recomputeOptimisticState(false)\n }\n\n /**\n * Clean up the collection by stopping sync and clearing data\n * This can be called manually or automatically by garbage collection\n */\n public cleanup(): void {\n this.syncedData.clear()\n this.syncedMetadata.clear()\n this.optimisticUpserts.clear()\n this.optimisticDeletes.clear()\n this.size = 0\n this.pendingSyncedTransactions = []\n this.syncedKeys.clear()\n this.hasReceivedFirstCommit = false\n }\n}\n"],"names":[],"mappings":";;AAqBO,MAAM,uBAKX;AAAA;AAAA;AAAA;AAAA,EA+BA,YAAY,QAAkD;AAtB9D,SAAO,4BACL,CAAA;AAEF,SAAO,qCAAqB,IAAA;AAG5B,SAAO,wCAAwB,IAAA;AAC/B,SAAO,wCAAwB,IAAA;AAG/B,SAAO,OAAO;AAGd,SAAO,iCAAiB,IAAA;AACxB,SAAO,0CAA0B,IAAA;AACjC,SAAO,yCAAyB,IAAA;AAChC,SAAO,yBAAyB;AAChC,SAAO,+BAA+B;AAsWtC,SAAA,4BAA4B,MAAM;AAEhC,UAAI,2BAA2B;AAC/B,iBAAW,eAAe,KAAK,aAAa,OAAA,GAAU;AACpD,YAAI,YAAY,UAAU,cAAc;AACtC,qCAA2B;AAC3B;AAAA,QACF;AAAA,MACF;AAIA,YAAM;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,MAAA,IACE,KAAK,0BAA0B;AAAA,QACjC,CAAC,KAAK,MAAM;AACV,cAAI,EAAE,WAAW;AACf,gBAAI,4BAA4B,KAAK,CAAC;AACtC,gBAAI,EAAE,aAAa,MAAM;AACvB,kBAAI,kBAAkB;AAAA,YACxB;AAAA,UACF,OAAO;AACL,gBAAI,8BAA8B,KAAK,CAAC;AAAA,UAC1C;AACA,iBAAO;AAAA,QACT;AAAA,QACA;AAAA,UACE,6BAA6B,CAAA;AAAA,UAG7B,+BAA+B,CAAA;AAAA,UAG/B,iBAAiB;AAAA,QAAA;AAAA,MACnB;AAGF,UAAI,CAAC,4BAA4B,iBAAiB;AAEhD,aAAK,+BAA+B;AAGpC,cAAM,kCAAkB,IAAA;AACxB,mBAAW,eAAe,6BAA6B;AACrD,qBAAW,aAAa,YAAY,YAAY;AAC9C,wBAAY,IAAI,UAAU,GAAW;AAAA,UACvC;AAAA,QACF;AAIA,YAAI,sBAAsB,KAAK;AAC/B,YAAI,oBAAoB,SAAS,GAAG;AAElC,oDAA0B,IAAA;AAC1B,qBAAW,OAAO,aAAa;AAC7B,kBAAM,eAAe,KAAK,IAAI,GAAG;AACjC,gBAAI,iBAAiB,QAAW;AAC9B,kCAAoB,IAAI,KAAK,YAAY;AAAA,YAC3C;AAAA,UACF;AAAA,QACF;AAEA,cAAM,SAA8C,CAAA;AACpD,cAAM,gBAAgB,KAAK,OAAO,KAAK,iBAAiB;AAExD,mBAAW,eAAe,6BAA6B;AAErD,cAAI,YAAY,UAAU;AAKxB,uBAAW,OAAO,KAAK,WAAW,KAAA,GAAQ;AACxC,kBAAI,KAAK,kBAAkB,IAAI,GAAG,EAAG;AACrC,oBAAM,gBACJ,KAAK,kBAAkB,IAAI,GAAG,KAAK,KAAK,WAAW,IAAI,GAAG;AAC5D,kBAAI,kBAAkB,QAAW;AAC/B,uBAAO,KAAK,EAAE,MAAM,UAAU,KAAK,OAAO,eAAe;AAAA,cAC3D;AAAA,YACF;AAIA,iBAAK,WAAW,MAAA;AAChB,iBAAK,eAAe,MAAA;AACpB,iBAAK,WAAW,MAAA;AAKhB,uBAAW,OAAO,aAAa;AAC7B,kCAAoB,OAAO,GAAG;AAAA,YAChC;AAAA,UACF;AAEA,qBAAW,aAAa,YAAY,YAAY;AAC9C,kBAAM,MAAM,UAAU;AACtB,iBAAK,WAAW,IAAI,GAAG;AAGvB,oBAAQ,UAAU,MAAA;AAAA,cAChB,KAAK;AACH,qBAAK,eAAe,IAAI,KAAK,UAAU,QAAQ;AAC/C;AAAA,cACF,KAAK;AACH,qBAAK,eAAe;AAAA,kBAClB;AAAA,kBACA,OAAO;AAAA,oBACL,CAAA;AAAA,oBACA,KAAK,eAAe,IAAI,GAAG;AAAA,oBAC3B,UAAU;AAAA,kBAAA;AAAA,gBACZ;AAEF;AAAA,cACF,KAAK;AACH,qBAAK,eAAe,OAAO,GAAG;AAC9B;AAAA,YAAA;AAIJ,oBAAQ,UAAU,MAAA;AAAA,cAChB,KAAK;AACH,qBAAK,WAAW,IAAI,KAAK,UAAU,KAAK;AACxC;AAAA,cACF,KAAK,UAAU;AACb,oBAAI,kBAAkB,WAAW;AAC/B,wBAAM,eAAe,OAAO;AAAA,oBAC1B,CAAA;AAAA,oBACA,KAAK,WAAW,IAAI,GAAG;AAAA,oBACvB,UAAU;AAAA,kBAAA;AAEZ,uBAAK,WAAW,IAAI,KAAK,YAAY;AAAA,gBACvC,OAAO;AACL,uBAAK,WAAW,IAAI,KAAK,UAAU,KAAK;AAAA,gBAC1C;AACA;AAAA,cACF;AAAA,cACA,KAAK;AACH,qBAAK,WAAW,OAAO,GAAG;AAC1B;AAAA,YAAA;AAAA,UAEN;AAAA,QACF;AAMA,YAAI,iBAAiB;AAEnB,gBAAM,kDAAkC,IAAA;AACxC,qBAAW,KAAK,6BAA6B;AAC3C,uBAAW,MAAM,EAAE,YAAY;AAC7B,kBAAI,GAAG,SAAS,YAAY,GAAG,SAAS,UAAU;AAChD,4CAA4B,IAAI,GAAG,GAAW;AAAA,cAChD;AAAA,YACF;AAAA,UACF;AAIA,gBAAM,qCAAqB,IAAA;AAC3B,gBAAM,qCAAqB,IAAA;AAE3B,qBAAW,MAAM,KAAK,aAAa,OAAA,GAAU;AAC3C,gBAAI,CAAC,aAAa,QAAQ,EAAE,SAAS,GAAG,KAAK,EAAG;AAChD,uBAAW,YAAY,GAAG,WAAW;AACnC,kBACE,CAAC,KAAK,iBAAiB,SAAS,UAAU,KAC1C,CAAC,SAAS;AAEV;AACF,oBAAM,MAAM,SAAS;AACrB,sBAAQ,SAAS,MAAA;AAAA,gBACf,KAAK;AACH,iCAAe,IAAI,KAAK,SAAS,QAAmB;AACpD,iCAAe,OAAO,GAAG;AACzB;AAAA,gBACF,KAAK,UAAU;AACb,wBAAM,OAAO,KAAK,WAAW,IAAI,GAAG;AACpC,wBAAM,OAAO,OACR,OAAO,OAAO,CAAA,GAAI,MAAM,SAAS,OAAO,IACxC,SAAS;AACd,iCAAe,IAAI,KAAK,IAAI;AAC5B,iCAAe,OAAO,GAAG;AACzB;AAAA,gBACF;AAAA,gBACA,KAAK;AACH,iCAAe,OAAO,GAAG;AACzB,iCAAe,IAAI,GAAG;AACtB;AAAA,cAAA;AAAA,YAEN;AAAA,UACF;AAKA,qBAAW,CAAC,KAAK,KAAK,KAAK,gBAAgB;AACzC,gBAAI,eAAe,IAAI,GAAG,EAAG;AAC7B,gBAAI,4BAA4B,IAAI,GAAG,GAAG;AACxC,kBAAI,cAAc;AAClB,uBAAS,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;AAC3C,sBAAM,MAAM,OAAO,CAAC;AACpB,oBAAI,IAAI,QAAQ,OAAO,IAAI,SAAS,UAAU;AAC5C,sBAAI,QAAQ;AACZ,gCAAc;AACd;AAAA,gBACF;AAAA,cACF;AACA,kBAAI,CAAC,aAAa;AAChB,uBAAO,KAAK,EAAE,MAAM,UAAU,KAAK,OAAO;AAAA,cAC5C;AAAA,YACF,OAAO;AACL,qBAAO,KAAK,EAAE,MAAM,UAAU,KAAK,OAAO;AAAA,YAC5C;AAAA,UACF;AAGA,cAAI,OAAO,SAAS,KAAK,eAAe,OAAO,GAAG;AAChD,kBAAM,WAAgD,CAAA;AACtD,uBAAW,OAAO,QAAQ;AACxB,kBAAI,IAAI,SAAS,YAAY,eAAe,IAAI,IAAI,GAAG,GAAG;AACxD;AAAA,cACF;AACA,uBAAS,KAAK,GAAG;AAAA,YACnB;AACA,mBAAO,SAAS;AAChB,mBAAO,KAAK,GAAG,QAAQ;AAAA,UACzB;AAGA,cAAI,KAAK,UAAU,WAAW,SAAS;AACrC,iBAAK,UAAU,UAAU,OAAO;AAAA,UAClC;AAAA,QACF;AAKA,aAAK,kBAAkB,MAAA;AACvB,aAAK,kBAAkB,MAAA;AAGvB,aAAK,+BAA+B;AACpC,mBAAW,eAAe,KAAK,aAAa,OAAA,GAAU;AACpD,cAAI,CAAC,CAAC,aAAa,QAAQ,EAAE,SAAS,YAAY,KAAK,GAAG;AACxD,uBAAW,YAAY,YAAY,WAAW;AAC5C,kBACE,KAAK,iBAAiB,SAAS,UAAU,KACzC,SAAS,YACT;AACA,wBAAQ,SAAS,MAAA;AAAA,kBACf,KAAK;AAAA,kBACL,KAAK;AACH,yBAAK,kBAAkB;AAAA,sBACrB,SAAS;AAAA,sBACT,SAAS;AAAA,oBAAA;AAEX,yBAAK,kBAAkB,OAAO,SAAS,GAAG;AAC1C;AAAA,kBACF,KAAK;AACH,yBAAK,kBAAkB,OAAO,SAAS,GAAG;AAC1C,yBAAK,kBAAkB,IAAI,SAAS,GAAG;AACvC;AAAA,gBAAA;AAAA,cAEN;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAGA,cAAM,6CAA6B,IAAA;AAEnC,mBAAW,eAAe,KAAK,aAAa,OAAA,GAAU;AACpD,cAAI,YAAY,UAAU,aAAa;AACrC,uBAAW,YAAY,YAAY,WAAW;AAC5C,kBACE,KAAK,iBAAiB,SAAS,UAAU,KACzC,YAAY,IAAI,SAAS,GAAG,GAC5B;AACA,uCAAuB,IAAI,SAAS,KAAK;AAAA,kBACvC,MAAM,SAAS;AAAA,kBACf,OAAO,SAAS;AAAA,gBAAA,CACjB;AAAA,cACH;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAGA,mBAAW,OAAO,aAAa;AAC7B,gBAAM,uBAAuB,oBAAoB,IAAI,GAAG;AACxD,gBAAM,kBAAkB,KAAK,IAAI,GAAG;AAGpC,gBAAM,cAAc,uBAAuB,IAAI,GAAG;AAClD,gBAAM,kBACJ,eACA,oBAAoB,UACpB,WAAW,YAAY,OAAO,eAAe;AAE/C,cAAI,CAAC,iBAAiB;AACpB,gBACE,yBAAyB,UACzB,oBAAoB,QACpB;AACA,qBAAO,KAAK;AAAA,gBACV,MAAM;AAAA,gBACN;AAAA,gBACA,OAAO;AAAA,cAAA,CACR;AAAA,YACH,WACE,yBAAyB,UACzB,oBAAoB,QACpB;AACA,qBAAO,KAAK;AAAA,gBACV,MAAM;AAAA,gBACN;AAAA,gBACA,OAAO;AAAA,cAAA,CACR;AAAA,YACH,WACE,yBAAyB,UACzB,oBAAoB,UACpB,CAAC,WAAW,sBAAsB,eAAe,GACjD;AACA,qBAAO,KAAK;AAAA,gBACV,MAAM;AAAA,gBACN;AAAA,gBACA,OAAO;AAAA,gBACP,eAAe;AAAA,cAAA,CAChB;AAAA,YACH;AAAA,UACF;AAAA,QACF;AAGA,aAAK,OAAO,KAAK,cAAA;AAGjB,YAAI,OAAO,SAAS,GAAG;AACrB,eAAK,QAAQ,cAAc,MAAM;AAAA,QACnC;AAGA,aAAK,QAAQ,WAAW,QAAQ,IAAI;AAEpC,aAAK,4BAA4B;AAGjC,aAAK,oBAAoB,MAAA;AAGzB,gBAAQ,UAAU,KAAK,MAAM;AAC3B,eAAK,mBAAmB,MAAA;AAAA,QAC1B,CAAC;AAGD,YAAI,CAAC,KAAK,wBAAwB;AAChC,eAAK,yBAAyB;AAC9B,gBAAM,YAAY,CAAC,GAAG,KAAK,UAAU,qBAAqB;AAC1D,eAAK,UAAU,wBAAwB,CAAA;AACvC,oBAAU,QAAQ,CAAC,aAAa,SAAA,CAAU;AAAA,QAC5C;AAAA,MACF;AAAA,IACF;AAhtBE,SAAK,SAAS;AACd,SAAK,eAAe,IAAI;AAAA,MAAoC,CAAC,GAAG,MAC9D,EAAE,iBAAiB,CAAC;AAAA,IAAA;AAItB,QAAI,OAAO,SAAS;AAClB,WAAK,aAAa,IAAI,UAAyB,OAAO,OAAO;AAAA,IAC/D,OAAO;AACL,WAAK,iCAAiB,IAAA;AAAA,IACxB;AAAA,EACF;AAAA,EAEA,QAAQ,MAKL;AACD,SAAK,aAAa,KAAK;AACvB,SAAK,YAAY,KAAK;AACtB,SAAK,UAAU,KAAK;AACpB,SAAK,UAAU,KAAK;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKO,IAAI,KAAgC;AACzC,UAAM,EAAE,mBAAmB,mBAAmB,WAAA,IAAe;AAE7D,QAAI,kBAAkB,IAAI,GAAG,GAAG;AAC9B,aAAO;AAAA,IACT;AAGA,QAAI,kBAAkB,IAAI,GAAG,GAAG;AAC9B,aAAO,kBAAkB,IAAI,GAAG;AAAA,IAClC;AAGA,WAAO,WAAW,IAAI,GAAG;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKO,IAAI,KAAoB;AAC7B,UAAM,EAAE,mBAAmB,mBAAmB,WAAA,IAAe;AAE7D,QAAI,kBAAkB,IAAI,GAAG,GAAG;AAC9B,aAAO;AAAA,IACT;AAGA,QAAI,kBAAkB,IAAI,GAAG,GAAG;AAC9B,aAAO;AAAA,IACT;AAGA,WAAO,WAAW,IAAI,GAAG;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,CAAQ,OAA+B;AACrC,UAAM,EAAE,YAAY,mBAAmB,kBAAA,IAAsB;AAE7D,eAAW,OAAO,WAAW,QAAQ;AACnC,UAAI,CAAC,kBAAkB,IAAI,GAAG,GAAG;AAC/B,cAAM;AAAA,MACR;AAAA,IACF;AAEA,eAAW,OAAO,kBAAkB,QAAQ;AAC1C,UAAI,CAAC,WAAW,IAAI,GAAG,KAAK,CAAC,kBAAkB,IAAI,GAAG,GAAG;AAGvD,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,CAAQ,SAAoC;AAC1C,eAAW,OAAO,KAAK,QAAQ;AAC7B,YAAM,QAAQ,KAAK,IAAI,GAAG;AAC1B,UAAI,UAAU,QAAW;AACvB,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,CAAQ,UAA6C;AACnD,eAAW,OAAO,KAAK,QAAQ;AAC7B,YAAM,QAAQ,KAAK,IAAI,GAAG;AAC1B,UAAI,UAAU,QAAW;AACvB,cAAM,CAAC,KAAK,KAAK;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,EAAS,OAAO,QAAQ,IAAuC;AAC7D,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,WAAW;AACzC,YAAM,CAAC,KAAK,KAAK;AAAA,IACnB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,QACL,YACM;AACN,QAAI,QAAQ;AACZ,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,WAAW;AACzC,iBAAW,OAAO,KAAK,OAAO;AAAA,IAChC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,IACL,YACU;AACV,UAAM,SAAmB,CAAA;AACzB,QAAI,QAAQ;AACZ,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,WAAW;AACzC,aAAO,KAAK,WAAW,OAAO,KAAK,OAAO,CAAC;AAAA,IAC7C;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,iBACN,YACS;AACT,WAAO,eAAe,KAAK;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKO,yBACL,wBAAiC,OAC3B;AAEN,QAAI,KAAK,8BAA8B;AACrC;AAAA,IACF;AAEA,UAAM,gBAAgB,IAAI,IAAI,KAAK,iBAAiB;AACpD,UAAM,kBAAkB,IAAI,IAAI,KAAK,iBAAiB;AAGtD,SAAK,kBAAkB,MAAA;AACvB,SAAK,kBAAkB,MAAA;AAEvB,UAAM,qBAA8C,CAAA;AAEpD,eAAW,eAAe,KAAK,aAAa,OAAA,GAAU;AACpD,UAAI,CAAC,CAAC,aAAa,QAAQ,EAAE,SAAS,YAAY,KAAK,GAAG;AACxD,2BAAmB,KAAK,WAAW;AAAA,MACrC;AAAA,IACF;AAGA,eAAW,eAAe,oBAAoB;AAC5C,iBAAW,YAAY,YAAY,WAAW;AAC5C,YAAI,KAAK,iBAAiB,SAAS,UAAU,KAAK,SAAS,YAAY;AACrE,kBAAQ,SAAS,MAAA;AAAA,YACf,KAAK;AAAA,YACL,KAAK;AACH,mBAAK,kBAAkB;AAAA,gBACrB,SAAS;AAAA,gBACT,SAAS;AAAA,cAAA;AAEX,mBAAK,kBAAkB,OAAO,SAAS,GAAG;AAC1C;AAAA,YACF,KAAK;AACH,mBAAK,kBAAkB,OAAO,SAAS,GAAG;AAC1C,mBAAK,kBAAkB,IAAI,SAAS,GAAG;AACvC;AAAA,UAAA;AAAA,QAEN;AAAA,MACF;AAAA,IACF;AAGA,SAAK,OAAO,KAAK,cAAA;AAGjB,UAAM,SAA8C,CAAA;AACpD,SAAK,yBAAyB,eAAe,iBAAiB,MAAM;AAKpE,UAAM,6BAA6B,OAAO,OAAO,CAAC,UAAU;AAC1D,UAAI,CAAC,KAAK,mBAAmB,IAAI,MAAM,GAAG,GAAG;AAC3C,eAAO;AAAA,MACT;AAGA,UAAI,uBAAuB;AACzB,eAAO;AAAA,MACT;AAGA,aAAO;AAAA,IACT,CAAC;AAKD,QAAI,KAAK,0BAA0B,SAAS,KAAK,CAAC,uBAAuB;AACvE,YAAM,sCAAsB,IAAA;AAG5B,iBAAW,eAAe,KAAK,2BAA2B;AACxD,mBAAW,aAAa,YAAY,YAAY;AAC9C,0BAAgB,IAAI,UAAU,GAAW;AAAA,QAC3C;AAAA,MACF;AAKA,YAAM,iBAAiB,2BAA2B,OAAO,CAAC,UAAU;AAClE,YAAI,MAAM,SAAS,YAAY,gBAAgB,IAAI,MAAM,GAAG,GAAG;AAG7D,gBAAM,8BAA8B,mBAAmB;AAAA,YAAK,CAAC,OAC3D,GAAG,UAAU;AAAA,cACX,CAAC,MAAM,KAAK,iBAAiB,EAAE,UAAU,KAAK,EAAE,QAAQ,MAAM;AAAA,YAAA;AAAA,UAChE;AAGF,cAAI,CAAC,6BAA6B;AAChC,mBAAO;AAAA,UACT;AAAA,QACF;AACA,eAAO;AAAA,MACT,CAAC;AAGD,UAAI,eAAe,SAAS,GAAG;AAC7B,aAAK,QAAQ,cAAc,cAAc;AAAA,MAC3C;AACA,WAAK,QAAQ,WAAW,gBAAgB,qBAAqB;AAAA,IAC/D,OAAO;AAEL,UAAI,2BAA2B,SAAS,GAAG;AACzC,aAAK,QAAQ,cAAc,0BAA0B;AAAA,MACvD;AAEA,WAAK,QAAQ,WAAW,4BAA4B,qBAAqB;AAAA,IAC3E;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,gBAAwB;AAC9B,UAAM,aAAa,KAAK,WAAW;AACnC,UAAM,oBAAoB,MAAM,KAAK,KAAK,iBAAiB,EAAE;AAAA,MAC3D,CAAC,QAAQ,KAAK,WAAW,IAAI,GAAG,KAAK,CAAC,KAAK,kBAAkB,IAAI,GAAG;AAAA,IAAA,EACpE;AACF,UAAM,qBAAqB,MAAM,KAAK,KAAK,kBAAkB,KAAA,CAAM,EAAE;AAAA,MACnE,CAAC,QAAQ,CAAC,KAAK,WAAW,IAAI,GAAG;AAAA,IAAA,EACjC;AAEF,WAAO,aAAa,oBAAoB;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA,EAKQ,yBACN,iBACA,iBACA,QACM;AACN,UAAM,8BAAc,IAAI;AAAA,MACtB,GAAG,gBAAgB,KAAA;AAAA,MACnB,GAAG,KAAK,kBAAkB,KAAA;AAAA,MAC1B,GAAG;AAAA,MACH,GAAG,KAAK;AAAA,IAAA,CACT;AAED,eAAW,OAAO,SAAS;AACzB,YAAM,eAAe,KAAK,IAAI,GAAG;AACjC,YAAM,gBAAgB,KAAK;AAAA,QACzB;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAGF,UAAI,kBAAkB,UAAa,iBAAiB,QAAW;AAC7D,eAAO,KAAK,EAAE,MAAM,UAAU,KAAK,OAAO,eAAe;AAAA,MAC3D,WAAW,kBAAkB,UAAa,iBAAiB,QAAW;AACpE,eAAO,KAAK,EAAE,MAAM,UAAU,KAAK,OAAO,cAAc;AAAA,MAC1D,WACE,kBAAkB,UAClB,iBAAiB,UACjB,kBAAkB,cAClB;AACA,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN;AAAA,UACA,OAAO;AAAA,UACP;AAAA,QAAA,CACD;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,iBACN,KACA,iBACA,iBACqB;AACrB,QAAI,gBAAgB,IAAI,GAAG,GAAG;AAC5B,aAAO;AAAA,IACT;AACA,QAAI,gBAAgB,IAAI,GAAG,GAAG;AAC5B,aAAO,gBAAgB,IAAI,GAAG;AAAA,IAChC;AACA,WAAO,KAAK,WAAW,IAAI,GAAG;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EA2XO,2BAA2B,aAAqC;AAErE,QAAI,YAAY,UAAU,aAAa;AACrC,WAAK,aAAa,OAAO,YAAY,EAAE;AACvC;AAAA,IACF;AAGA,gBAAY,YAAY,QACrB,KAAK,MAAM;AAEV,WAAK,aAAa,OAAO,YAAY,EAAE;AAAA,IACzC,CAAC,EACA,MAAM,MAAM;AAAA,IAIb,CAAC;AAAA,EACL;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,6BAAmC;AACxC,QAAI,KAAK,0BAA0B,WAAW,EAAG;AAGjD,SAAK,oBAAoB,MAAA;AAGzB,UAAM,iCAAiB,IAAA;AACvB,eAAW,eAAe,KAAK,2BAA2B;AACxD,iBAAW,aAAa,YAAY,YAAY;AAC9C,mBAAW,IAAI,UAAU,GAAW;AAAA,MACtC;AAAA,IACF;AAGA,eAAW,OAAO,YAAY;AAC5B,WAAK,mBAAmB,IAAI,GAAG;AAAA,IACjC;AAIA,eAAW,OAAO,YAAY;AAC5B,YAAM,eAAe,KAAK,IAAI,GAAG;AACjC,UAAI,iBAAiB,QAAW;AAC9B,aAAK,oBAAoB,IAAI,KAAK,YAAY;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,2BAAiC;AAGtC,SAAK,QAAQ,oBAAoB,KAAK,0BAA0B,SAAS;AAGzE,SAAK,2BAAA;AAEL,SAAK,yBAAyB,KAAK;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,UAAgB;AACrB,SAAK,WAAW,MAAA;AAChB,SAAK,eAAe,MAAA;AACpB,SAAK,kBAAkB,MAAA;AACvB,SAAK,kBAAkB,MAAA;AACvB,SAAK,OAAO;AACZ,SAAK,4BAA4B,CAAA;AACjC,SAAK,WAAW,MAAA;AAChB,SAAK,yBAAyB;AAAA,EAChC;AACF;"}
|
|
1
|
+
{"version":3,"file":"state.js","sources":["../../../src/collection/state.ts"],"sourcesContent":["import { deepEquals } from \"../utils\"\nimport { SortedMap } from \"../SortedMap\"\nimport type { Transaction } from \"../transactions\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n ChangeMessage,\n CollectionConfig,\n OptimisticChangeMessage,\n} from \"../types\"\nimport type { CollectionImpl } from \"./index.js\"\nimport type { CollectionLifecycleManager } from \"./lifecycle\"\nimport type { CollectionChangesManager } from \"./changes\"\nimport type { CollectionIndexesManager } from \"./indexes\"\n\ninterface PendingSyncedTransaction<T extends object = Record<string, unknown>> {\n committed: boolean\n operations: Array<OptimisticChangeMessage<T>>\n truncate?: boolean\n deletedKeys: Set<string | number>\n}\n\nexport class CollectionStateManager<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n public config!: CollectionConfig<TOutput, TKey, TSchema>\n public collection!: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n public lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n public changes!: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n public indexes!: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n\n // Core state - make public for testing\n public transactions: SortedMap<string, Transaction<any>>\n public pendingSyncedTransactions: Array<PendingSyncedTransaction<TOutput>> =\n []\n public syncedData: Map<TKey, TOutput> | SortedMap<TKey, TOutput>\n public syncedMetadata = new Map<TKey, unknown>()\n\n // Optimistic state tracking - make public for testing\n public optimisticUpserts = new Map<TKey, TOutput>()\n public optimisticDeletes = new Set<TKey>()\n\n // Cached size for performance\n public size = 0\n\n // State used for computing the change events\n public syncedKeys = new Set<TKey>()\n public preSyncVisibleState = new Map<TKey, TOutput>()\n public recentlySyncedKeys = new Set<TKey>()\n public hasReceivedFirstCommit = false\n public isCommittingSyncTransactions = false\n\n /**\n * Creates a new CollectionState manager\n */\n constructor(config: CollectionConfig<TOutput, TKey, TSchema>) {\n this.config = config\n this.transactions = new SortedMap<string, Transaction<any>>((a, b) =>\n a.compareCreatedAt(b)\n )\n\n // Set up data storage with optional comparison function\n if (config.compare) {\n this.syncedData = new SortedMap<TKey, TOutput>(config.compare)\n } else {\n this.syncedData = new Map<TKey, TOutput>()\n }\n }\n\n setDeps(deps: {\n collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n }) {\n this.collection = deps.collection\n this.lifecycle = deps.lifecycle\n this.changes = deps.changes\n this.indexes = deps.indexes\n }\n\n /**\n * Get the current value for a key (virtual derived state)\n */\n public get(key: TKey): TOutput | undefined {\n const { optimisticDeletes, optimisticUpserts, syncedData } = this\n // Check if optimistically deleted\n if (optimisticDeletes.has(key)) {\n return undefined\n }\n\n // Check optimistic upserts first\n if (optimisticUpserts.has(key)) {\n return optimisticUpserts.get(key)\n }\n\n // Fall back to synced data\n return syncedData.get(key)\n }\n\n /**\n * Check if a key exists in the collection (virtual derived state)\n */\n public has(key: TKey): boolean {\n const { optimisticDeletes, optimisticUpserts, syncedData } = this\n // Check if optimistically deleted\n if (optimisticDeletes.has(key)) {\n return false\n }\n\n // Check optimistic upserts first\n if (optimisticUpserts.has(key)) {\n return true\n }\n\n // Fall back to synced data\n return syncedData.has(key)\n }\n\n /**\n * Get all keys (virtual derived state)\n */\n public *keys(): IterableIterator<TKey> {\n const { syncedData, optimisticDeletes, optimisticUpserts } = this\n // Yield keys from synced data, skipping any that are deleted.\n for (const key of syncedData.keys()) {\n if (!optimisticDeletes.has(key)) {\n yield key\n }\n }\n // Yield keys from upserts that were not already in synced data.\n for (const key of optimisticUpserts.keys()) {\n if (!syncedData.has(key) && !optimisticDeletes.has(key)) {\n // The optimisticDeletes check is technically redundant if inserts/updates always remove from deletes,\n // but it's safer to keep it.\n yield key\n }\n }\n }\n\n /**\n * Get all values (virtual derived state)\n */\n public *values(): IterableIterator<TOutput> {\n for (const key of this.keys()) {\n const value = this.get(key)\n if (value !== undefined) {\n yield value\n }\n }\n }\n\n /**\n * Get all entries (virtual derived state)\n */\n public *entries(): IterableIterator<[TKey, TOutput]> {\n for (const key of this.keys()) {\n const value = this.get(key)\n if (value !== undefined) {\n yield [key, value]\n }\n }\n }\n\n /**\n * Get all entries (virtual derived state)\n */\n public *[Symbol.iterator](): IterableIterator<[TKey, TOutput]> {\n for (const [key, value] of this.entries()) {\n yield [key, value]\n }\n }\n\n /**\n * Execute a callback for each entry in the collection\n */\n public forEach(\n callbackfn: (value: TOutput, key: TKey, index: number) => void\n ): void {\n let index = 0\n for (const [key, value] of this.entries()) {\n callbackfn(value, key, index++)\n }\n }\n\n /**\n * Create a new array with the results of calling a function for each entry in the collection\n */\n public map<U>(\n callbackfn: (value: TOutput, key: TKey, index: number) => U\n ): Array<U> {\n const result: Array<U> = []\n let index = 0\n for (const [key, value] of this.entries()) {\n result.push(callbackfn(value, key, index++))\n }\n return result\n }\n\n /**\n * Check if the given collection is this collection\n * @param collection The collection to check\n * @returns True if the given collection is this collection, false otherwise\n */\n private isThisCollection(\n collection: CollectionImpl<any, any, any, any, any>\n ): boolean {\n return collection === this.collection\n }\n\n /**\n * Recompute optimistic state from active transactions\n */\n public recomputeOptimisticState(\n triggeredByUserAction: boolean = false\n ): void {\n // Skip redundant recalculations when we're in the middle of committing sync transactions\n if (this.isCommittingSyncTransactions) {\n return\n }\n\n const previousState = new Map(this.optimisticUpserts)\n const previousDeletes = new Set(this.optimisticDeletes)\n\n // Clear current optimistic state\n this.optimisticUpserts.clear()\n this.optimisticDeletes.clear()\n\n const activeTransactions: Array<Transaction<any>> = []\n\n for (const transaction of this.transactions.values()) {\n if (![`completed`, `failed`].includes(transaction.state)) {\n activeTransactions.push(transaction)\n }\n }\n\n // Apply active transactions only (completed transactions are handled by sync operations)\n for (const transaction of activeTransactions) {\n for (const mutation of transaction.mutations) {\n if (this.isThisCollection(mutation.collection) && mutation.optimistic) {\n switch (mutation.type) {\n case `insert`:\n case `update`:\n this.optimisticUpserts.set(\n mutation.key,\n mutation.modified as TOutput\n )\n this.optimisticDeletes.delete(mutation.key)\n break\n case `delete`:\n this.optimisticUpserts.delete(mutation.key)\n this.optimisticDeletes.add(mutation.key)\n break\n }\n }\n }\n }\n\n // Update cached size\n this.size = this.calculateSize()\n\n // Collect events for changes\n const events: Array<ChangeMessage<TOutput, TKey>> = []\n this.collectOptimisticChanges(previousState, previousDeletes, events)\n\n // Filter out events for recently synced keys to prevent duplicates\n // BUT: Only filter out events that are actually from sync operations\n // New user transactions should NOT be filtered even if the key was recently synced\n const filteredEventsBySyncStatus = events.filter((event) => {\n if (!this.recentlySyncedKeys.has(event.key)) {\n return true // Key not recently synced, allow event through\n }\n\n // Key was recently synced - allow if this is a user-triggered action\n if (triggeredByUserAction) {\n return true\n }\n\n // Otherwise filter out duplicate sync events\n return false\n })\n\n // Filter out redundant delete events if there are pending sync transactions\n // that will immediately restore the same data, but only for completed transactions\n // IMPORTANT: Skip complex filtering for user-triggered actions to prevent UI blocking\n if (this.pendingSyncedTransactions.length > 0 && !triggeredByUserAction) {\n const pendingSyncKeys = new Set<TKey>()\n\n // Collect keys from pending sync operations\n for (const transaction of this.pendingSyncedTransactions) {\n for (const operation of transaction.operations) {\n pendingSyncKeys.add(operation.key as TKey)\n }\n }\n\n // Only filter out delete events for keys that:\n // 1. Have pending sync operations AND\n // 2. Are from completed transactions (being cleaned up)\n const filteredEvents = filteredEventsBySyncStatus.filter((event) => {\n if (event.type === `delete` && pendingSyncKeys.has(event.key)) {\n // Check if this delete is from clearing optimistic state of completed transactions\n // We can infer this by checking if we have no remaining optimistic mutations for this key\n const hasActiveOptimisticMutation = activeTransactions.some((tx) =>\n tx.mutations.some(\n (m) => this.isThisCollection(m.collection) && m.key === event.key\n )\n )\n\n if (!hasActiveOptimisticMutation) {\n return false // Skip this delete event as sync will restore the data\n }\n }\n return true\n })\n\n // Update indexes for the filtered events\n if (filteredEvents.length > 0) {\n this.indexes.updateIndexes(filteredEvents)\n }\n this.changes.emitEvents(filteredEvents, triggeredByUserAction)\n } else {\n // Update indexes for all events\n if (filteredEventsBySyncStatus.length > 0) {\n this.indexes.updateIndexes(filteredEventsBySyncStatus)\n }\n // Emit all events if no pending sync transactions\n this.changes.emitEvents(filteredEventsBySyncStatus, triggeredByUserAction)\n }\n }\n\n /**\n * Calculate the current size based on synced data and optimistic changes\n */\n private calculateSize(): number {\n const syncedSize = this.syncedData.size\n const deletesFromSynced = Array.from(this.optimisticDeletes).filter(\n (key) => this.syncedData.has(key) && !this.optimisticUpserts.has(key)\n ).length\n const upsertsNotInSynced = Array.from(this.optimisticUpserts.keys()).filter(\n (key) => !this.syncedData.has(key)\n ).length\n\n return syncedSize - deletesFromSynced + upsertsNotInSynced\n }\n\n /**\n * Collect events for optimistic changes\n */\n private collectOptimisticChanges(\n previousUpserts: Map<TKey, TOutput>,\n previousDeletes: Set<TKey>,\n events: Array<ChangeMessage<TOutput, TKey>>\n ): void {\n const allKeys = new Set([\n ...previousUpserts.keys(),\n ...this.optimisticUpserts.keys(),\n ...previousDeletes,\n ...this.optimisticDeletes,\n ])\n\n for (const key of allKeys) {\n const currentValue = this.get(key)\n const previousValue = this.getPreviousValue(\n key,\n previousUpserts,\n previousDeletes\n )\n\n if (previousValue !== undefined && currentValue === undefined) {\n events.push({ type: `delete`, key, value: previousValue })\n } else if (previousValue === undefined && currentValue !== undefined) {\n events.push({ type: `insert`, key, value: currentValue })\n } else if (\n previousValue !== undefined &&\n currentValue !== undefined &&\n previousValue !== currentValue\n ) {\n events.push({\n type: `update`,\n key,\n value: currentValue,\n previousValue,\n })\n }\n }\n }\n\n /**\n * Get the previous value for a key given previous optimistic state\n */\n private getPreviousValue(\n key: TKey,\n previousUpserts: Map<TKey, TOutput>,\n previousDeletes: Set<TKey>\n ): TOutput | undefined {\n if (previousDeletes.has(key)) {\n return undefined\n }\n if (previousUpserts.has(key)) {\n return previousUpserts.get(key)\n }\n return this.syncedData.get(key)\n }\n\n /**\n * Attempts to commit pending synced transactions if there are no active transactions\n * This method processes operations from pending transactions and applies them to the synced data\n */\n commitPendingTransactions = () => {\n // Check if there are any persisting transaction\n let hasPersistingTransaction = false\n for (const transaction of this.transactions.values()) {\n if (transaction.state === `persisting`) {\n hasPersistingTransaction = true\n break\n }\n }\n\n // pending synced transactions could be either `committed` or still open.\n // we only want to process `committed` transactions here\n const {\n committedSyncedTransactions,\n uncommittedSyncedTransactions,\n hasTruncateSync,\n } = this.pendingSyncedTransactions.reduce(\n (acc, t) => {\n if (t.committed) {\n acc.committedSyncedTransactions.push(t)\n if (t.truncate === true) {\n acc.hasTruncateSync = true\n }\n } else {\n acc.uncommittedSyncedTransactions.push(t)\n }\n return acc\n },\n {\n committedSyncedTransactions: [] as Array<\n PendingSyncedTransaction<TOutput>\n >,\n uncommittedSyncedTransactions: [] as Array<\n PendingSyncedTransaction<TOutput>\n >,\n hasTruncateSync: false,\n }\n )\n\n if (!hasPersistingTransaction || hasTruncateSync) {\n // Set flag to prevent redundant optimistic state recalculations\n this.isCommittingSyncTransactions = true\n\n // First collect all keys that will be affected by sync operations\n const changedKeys = new Set<TKey>()\n for (const transaction of committedSyncedTransactions) {\n for (const operation of transaction.operations) {\n changedKeys.add(operation.key as TKey)\n }\n }\n\n // Use pre-captured state if available (from optimistic scenarios),\n // otherwise capture current state (for pure sync scenarios)\n let currentVisibleState = this.preSyncVisibleState\n if (currentVisibleState.size === 0) {\n // No pre-captured state, capture it now for pure sync operations\n currentVisibleState = new Map<TKey, TOutput>()\n for (const key of changedKeys) {\n const currentValue = this.get(key)\n if (currentValue !== undefined) {\n currentVisibleState.set(key, currentValue)\n }\n }\n }\n\n const events: Array<ChangeMessage<TOutput, TKey>> = []\n const rowUpdateMode = this.config.sync.rowUpdateMode || `partial`\n\n for (const transaction of committedSyncedTransactions) {\n // Handle truncate operations first\n if (transaction.truncate) {\n // TRUNCATE PHASE\n // 1) Emit a delete for every currently-synced key so downstream listeners/indexes\n // observe a clear-before-rebuild. We intentionally skip keys already in\n // optimisticDeletes because their delete was previously emitted by the user.\n for (const key of this.syncedData.keys()) {\n if (this.optimisticDeletes.has(key)) continue\n const previousValue =\n this.optimisticUpserts.get(key) || this.syncedData.get(key)\n if (previousValue !== undefined) {\n events.push({ type: `delete`, key, value: previousValue })\n }\n }\n\n // 2) Clear the authoritative synced base. Subsequent server ops in this\n // same commit will rebuild the base atomically.\n this.syncedData.clear()\n this.syncedMetadata.clear()\n this.syncedKeys.clear()\n\n // 3) Clear currentVisibleState for truncated keys to ensure subsequent operations\n // are compared against the post-truncate state (undefined) rather than pre-truncate state\n // This ensures that re-inserted keys are emitted as INSERT events, not UPDATE events\n for (const key of changedKeys) {\n currentVisibleState.delete(key)\n }\n }\n\n for (const operation of transaction.operations) {\n const key = operation.key as TKey\n this.syncedKeys.add(key)\n\n // Update metadata\n switch (operation.type) {\n case `insert`:\n this.syncedMetadata.set(key, operation.metadata)\n break\n case `update`:\n this.syncedMetadata.set(\n key,\n Object.assign(\n {},\n this.syncedMetadata.get(key),\n operation.metadata\n )\n )\n break\n case `delete`:\n this.syncedMetadata.delete(key)\n break\n }\n\n // Update synced data\n switch (operation.type) {\n case `insert`:\n this.syncedData.set(key, operation.value)\n break\n case `update`: {\n if (rowUpdateMode === `partial`) {\n const updatedValue = Object.assign(\n {},\n this.syncedData.get(key),\n operation.value\n )\n this.syncedData.set(key, updatedValue)\n } else {\n this.syncedData.set(key, operation.value)\n }\n break\n }\n case `delete`:\n this.syncedData.delete(key)\n break\n }\n }\n }\n\n // After applying synced operations, if this commit included a truncate,\n // re-apply optimistic mutations on top of the fresh synced base. This ensures\n // the UI preserves local intent while respecting server rebuild semantics.\n // Ordering: deletes (above) -> server ops (just applied) -> optimistic upserts.\n if (hasTruncateSync) {\n // Avoid duplicating keys that were inserted/updated by synced operations in this commit\n const syncedInsertedOrUpdatedKeys = new Set<TKey>()\n for (const t of committedSyncedTransactions) {\n for (const op of t.operations) {\n if (op.type === `insert` || op.type === `update`) {\n syncedInsertedOrUpdatedKeys.add(op.key as TKey)\n }\n }\n }\n\n // Build re-apply sets from ACTIVE optimistic transactions against the new synced base\n // We do not copy maps; we compute intent directly from transactions to avoid drift.\n const reapplyUpserts = new Map<TKey, TOutput>()\n const reapplyDeletes = new Set<TKey>()\n\n for (const tx of this.transactions.values()) {\n if ([`completed`, `failed`].includes(tx.state)) continue\n for (const mutation of tx.mutations) {\n if (\n !this.isThisCollection(mutation.collection) ||\n !mutation.optimistic\n )\n continue\n const key = mutation.key as TKey\n switch (mutation.type) {\n case `insert`:\n reapplyUpserts.set(key, mutation.modified as TOutput)\n reapplyDeletes.delete(key)\n break\n case `update`: {\n const base = this.syncedData.get(key)\n const next = base\n ? (Object.assign({}, base, mutation.changes) as TOutput)\n : (mutation.modified as TOutput)\n reapplyUpserts.set(key, next)\n reapplyDeletes.delete(key)\n break\n }\n case `delete`:\n reapplyUpserts.delete(key)\n reapplyDeletes.add(key)\n break\n }\n }\n }\n\n // Emit inserts for re-applied upserts, skipping any keys that have an optimistic delete.\n // If the server also inserted/updated the same key in this batch, override that value\n // with the optimistic value to preserve local intent.\n for (const [key, value] of reapplyUpserts) {\n if (reapplyDeletes.has(key)) continue\n if (syncedInsertedOrUpdatedKeys.has(key)) {\n let foundInsert = false\n for (let i = events.length - 1; i >= 0; i--) {\n const evt = events[i]!\n if (evt.key === key && evt.type === `insert`) {\n evt.value = value\n foundInsert = true\n break\n }\n }\n if (!foundInsert) {\n events.push({ type: `insert`, key, value })\n }\n } else {\n events.push({ type: `insert`, key, value })\n }\n }\n\n // Finally, ensure we do NOT insert keys that have an outstanding optimistic delete.\n if (events.length > 0 && reapplyDeletes.size > 0) {\n const filtered: Array<ChangeMessage<TOutput, TKey>> = []\n for (const evt of events) {\n if (evt.type === `insert` && reapplyDeletes.has(evt.key)) {\n continue\n }\n filtered.push(evt)\n }\n events.length = 0\n events.push(...filtered)\n }\n\n // Ensure listeners are active before emitting this critical batch\n if (this.lifecycle.status !== `ready`) {\n this.lifecycle.markReady()\n }\n }\n\n // Maintain optimistic state appropriately\n // Clear optimistic state since sync operations will now provide the authoritative data.\n // Any still-active user transactions will be re-applied below in recompute.\n this.optimisticUpserts.clear()\n this.optimisticDeletes.clear()\n\n // Reset flag and recompute optimistic state for any remaining active transactions\n this.isCommittingSyncTransactions = false\n for (const transaction of this.transactions.values()) {\n if (![`completed`, `failed`].includes(transaction.state)) {\n for (const mutation of transaction.mutations) {\n if (\n this.isThisCollection(mutation.collection) &&\n mutation.optimistic\n ) {\n switch (mutation.type) {\n case `insert`:\n case `update`:\n this.optimisticUpserts.set(\n mutation.key,\n mutation.modified as TOutput\n )\n this.optimisticDeletes.delete(mutation.key)\n break\n case `delete`:\n this.optimisticUpserts.delete(mutation.key)\n this.optimisticDeletes.add(mutation.key)\n break\n }\n }\n }\n }\n }\n\n // Check for redundant sync operations that match completed optimistic operations\n const completedOptimisticOps = new Map<TKey, any>()\n\n for (const transaction of this.transactions.values()) {\n if (transaction.state === `completed`) {\n for (const mutation of transaction.mutations) {\n if (\n this.isThisCollection(mutation.collection) &&\n changedKeys.has(mutation.key)\n ) {\n completedOptimisticOps.set(mutation.key, {\n type: mutation.type,\n value: mutation.modified,\n })\n }\n }\n }\n }\n\n // Now check what actually changed in the final visible state\n for (const key of changedKeys) {\n const previousVisibleValue = currentVisibleState.get(key)\n const newVisibleValue = this.get(key) // This returns the new derived state\n\n // Check if this sync operation is redundant with a completed optimistic operation\n const completedOp = completedOptimisticOps.get(key)\n const isRedundantSync =\n completedOp &&\n newVisibleValue !== undefined &&\n deepEquals(completedOp.value, newVisibleValue)\n\n if (!isRedundantSync) {\n if (\n previousVisibleValue === undefined &&\n newVisibleValue !== undefined\n ) {\n events.push({\n type: `insert`,\n key,\n value: newVisibleValue,\n })\n } else if (\n previousVisibleValue !== undefined &&\n newVisibleValue === undefined\n ) {\n events.push({\n type: `delete`,\n key,\n value: previousVisibleValue,\n })\n } else if (\n previousVisibleValue !== undefined &&\n newVisibleValue !== undefined &&\n !deepEquals(previousVisibleValue, newVisibleValue)\n ) {\n events.push({\n type: `update`,\n key,\n value: newVisibleValue,\n previousValue: previousVisibleValue,\n })\n }\n }\n }\n\n // Update cached size after synced data changes\n this.size = this.calculateSize()\n\n // Update indexes for all events before emitting\n if (events.length > 0) {\n this.indexes.updateIndexes(events)\n }\n\n // End batching and emit all events (combines any batched events with sync events)\n this.changes.emitEvents(events, true)\n\n this.pendingSyncedTransactions = uncommittedSyncedTransactions\n\n // Clear the pre-sync state since sync operations are complete\n this.preSyncVisibleState.clear()\n\n // Clear recently synced keys after a microtask to allow recomputeOptimisticState to see them\n Promise.resolve().then(() => {\n this.recentlySyncedKeys.clear()\n })\n\n // Call any registered one-time commit listeners\n if (!this.hasReceivedFirstCommit) {\n this.hasReceivedFirstCommit = true\n const callbacks = [...this.lifecycle.onFirstReadyCallbacks]\n this.lifecycle.onFirstReadyCallbacks = []\n callbacks.forEach((callback) => callback())\n }\n }\n }\n\n /**\n * Schedule cleanup of a transaction when it completes\n */\n public scheduleTransactionCleanup(transaction: Transaction<any>): void {\n // Only schedule cleanup for transactions that aren't already completed\n if (transaction.state === `completed`) {\n this.transactions.delete(transaction.id)\n return\n }\n\n // Schedule cleanup when the transaction completes\n transaction.isPersisted.promise\n .then(() => {\n // Transaction completed successfully, remove it immediately\n this.transactions.delete(transaction.id)\n })\n .catch(() => {\n // Transaction failed, but we want to keep failed transactions for reference\n // so don't remove it.\n // This empty catch block is necessary to prevent unhandled promise rejections.\n })\n }\n\n /**\n * Capture visible state for keys that will be affected by pending sync operations\n * This must be called BEFORE onTransactionStateChange clears optimistic state\n */\n public capturePreSyncVisibleState(): void {\n if (this.pendingSyncedTransactions.length === 0) return\n\n // Clear any previous capture\n this.preSyncVisibleState.clear()\n\n // Get all keys that will be affected by sync operations\n const syncedKeys = new Set<TKey>()\n for (const transaction of this.pendingSyncedTransactions) {\n for (const operation of transaction.operations) {\n syncedKeys.add(operation.key as TKey)\n }\n }\n\n // Mark keys as about to be synced to suppress intermediate events from recomputeOptimisticState\n for (const key of syncedKeys) {\n this.recentlySyncedKeys.add(key)\n }\n\n // Only capture current visible state for keys that will be affected by sync operations\n // This is much more efficient than capturing the entire collection state\n for (const key of syncedKeys) {\n const currentValue = this.get(key)\n if (currentValue !== undefined) {\n this.preSyncVisibleState.set(key, currentValue)\n }\n }\n }\n\n /**\n * Trigger a recomputation when transactions change\n * This method should be called by the Transaction class when state changes\n */\n public onTransactionStateChange(): void {\n // Check if commitPendingTransactions will be called after this\n // by checking if there are pending sync transactions (same logic as in transactions.ts)\n this.changes.shouldBatchEvents = this.pendingSyncedTransactions.length > 0\n\n // CRITICAL: Capture visible state BEFORE clearing optimistic state\n this.capturePreSyncVisibleState()\n\n this.recomputeOptimisticState(false)\n }\n\n /**\n * Clean up the collection by stopping sync and clearing data\n * This can be called manually or automatically by garbage collection\n */\n public cleanup(): void {\n this.syncedData.clear()\n this.syncedMetadata.clear()\n this.optimisticUpserts.clear()\n this.optimisticDeletes.clear()\n this.size = 0\n this.pendingSyncedTransactions = []\n this.syncedKeys.clear()\n this.hasReceivedFirstCommit = false\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ensureIndexForExpression } from "../indexes/auto-index.js";
|
|
2
|
-
import { createFilteredCallback, createFilterFunctionFromExpression } from "./change-events.js";
|
|
3
2
|
import { and } from "../query/builder/functions.js";
|
|
3
|
+
import { createFilteredCallback, createFilterFunctionFromExpression } from "./change-events.js";
|
|
4
4
|
class CollectionSubscription {
|
|
5
5
|
constructor(collection, callback, options) {
|
|
6
6
|
this.collection = collection;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription.js","sources":["../../../src/collection/subscription.ts"],"sourcesContent":["import { ensureIndexForExpression } from \"../indexes/auto-index.js\"\nimport { and } from \"../query/index.js\"\nimport {\n createFilterFunctionFromExpression,\n createFilteredCallback,\n} from \"./change-events.js\"\nimport type { BasicExpression } from \"../query/ir.js\"\nimport type { BaseIndex } from \"../indexes/base-index.js\"\nimport type { ChangeMessage } from \"../types.js\"\nimport type { CollectionImpl } from \"./index.js\"\n\ntype RequestSnapshotOptions = {\n where?: BasicExpression<boolean>\n optimizedOnly?: boolean\n}\n\ntype RequestLimitedSnapshotOptions = {\n minValue?: any\n limit: number\n}\n\ntype CollectionSubscriptionOptions = {\n /** Pre-compiled expression for filtering changes */\n whereExpression?: BasicExpression<boolean>\n /** Callback to call when the subscription is unsubscribed */\n onUnsubscribe?: () => void\n}\n\nexport class CollectionSubscription {\n private loadedInitialState = false\n\n // Flag to indicate that we have sent at least 1 snapshot.\n // While `snapshotSent` is false we filter out all changes from subscription to the collection.\n private snapshotSent = false\n\n // Keep track of the keys we've sent (needed for join and orderBy optimizations)\n private sentKeys = new Set<string | number>()\n\n private filteredCallback: (changes: Array<ChangeMessage<any, any>>) => void\n\n private orderByIndex: BaseIndex<string | number> | undefined\n\n constructor(\n private collection: CollectionImpl<any, any, any, any, any>,\n private callback: (changes: Array<ChangeMessage<any, any>>) => void,\n private options: CollectionSubscriptionOptions\n ) {\n // Auto-index for where expressions if enabled\n if (options.whereExpression) {\n ensureIndexForExpression(options.whereExpression, this.collection)\n }\n\n const callbackWithSentKeysTracking = (\n changes: Array<ChangeMessage<any, any>>\n ) => {\n callback(changes)\n this.trackSentKeys(changes)\n }\n\n this.callback = callbackWithSentKeysTracking\n\n // Create a filtered callback if where clause is provided\n this.filteredCallback = options.whereExpression\n ? createFilteredCallback(this.callback, options)\n : this.callback\n }\n\n setOrderByIndex(index: BaseIndex<any>) {\n this.orderByIndex = index\n }\n\n hasLoadedInitialState() {\n return this.loadedInitialState\n }\n\n hasSentAtLeastOneSnapshot() {\n return this.snapshotSent\n }\n\n emitEvents(changes: Array<ChangeMessage<any, any>>) {\n const newChanges = this.filterAndFlipChanges(changes)\n this.filteredCallback(newChanges)\n }\n\n /**\n * Sends the snapshot to the callback.\n * Returns a boolean indicating if it succeeded.\n * It can only fail if there is no index to fulfill the request\n * and the optimizedOnly option is set to true,\n * or, the entire state was already loaded.\n */\n requestSnapshot(opts?: RequestSnapshotOptions): boolean {\n if (this.loadedInitialState) {\n // Subscription was deoptimized so we already sent the entire initial state\n return false\n }\n\n const stateOpts: RequestSnapshotOptions = {\n where: this.options.whereExpression,\n optimizedOnly: opts?.optimizedOnly ?? false,\n }\n\n if (opts) {\n if (`where` in opts) {\n const snapshotWhereExp = opts.where\n if (stateOpts.where) {\n // Combine the two where expressions\n const subWhereExp = stateOpts.where\n const combinedWhereExp = and(subWhereExp, snapshotWhereExp)\n stateOpts.where = combinedWhereExp\n } else {\n stateOpts.where = snapshotWhereExp\n }\n }\n } else {\n // No options provided so it's loading the entire initial state\n this.loadedInitialState = true\n }\n\n const snapshot = this.collection.currentStateAsChanges(stateOpts)\n\n if (snapshot === undefined) {\n // Couldn't load from indexes\n return false\n }\n\n // Only send changes that have not been sent yet\n const filteredSnapshot = snapshot.filter(\n (change) => !this.sentKeys.has(change.key)\n )\n\n this.snapshotSent = true\n this.callback(filteredSnapshot)\n return true\n }\n\n /**\n * Sends a snapshot that is limited to the first `limit` rows that fulfill the `where` clause and are bigger than `minValue`.\n * Requires a range index to be set with `setOrderByIndex` prior to calling this method.\n * It uses that range index to load the items in the order of the index.\n * Note: it does not send keys that have already been sent before.\n */\n requestLimitedSnapshot({ limit, minValue }: RequestLimitedSnapshotOptions) {\n if (!limit) throw new Error(`limit is required`)\n\n if (!this.orderByIndex) {\n throw new Error(\n `Ordered snapshot was requested but no index was found. You have to call setOrderByIndex before requesting an ordered snapshot.`\n )\n }\n\n const index = this.orderByIndex\n const where = this.options.whereExpression\n const whereFilterFn = where\n ? createFilterFunctionFromExpression(where)\n : undefined\n\n const filterFn = (key: string | number): boolean => {\n if (this.sentKeys.has(key)) {\n return false\n }\n\n const value = this.collection.get(key)\n if (value === undefined) {\n return false\n }\n\n return whereFilterFn?.(value) ?? true\n }\n\n let biggestObservedValue = minValue\n const changes: Array<ChangeMessage<any, string | number>> = []\n let keys: Array<string | number> = index.take(limit, minValue, filterFn)\n\n const valuesNeeded = () => Math.max(limit - changes.length, 0)\n const collectionExhausted = () => keys.length === 0\n\n while (valuesNeeded() > 0 && !collectionExhausted()) {\n for (const key of keys) {\n const value = this.collection.get(key)!\n changes.push({\n type: `insert`,\n key,\n value,\n })\n biggestObservedValue = value\n }\n\n keys = index.take(valuesNeeded(), biggestObservedValue, filterFn)\n }\n\n this.callback(changes)\n }\n\n /**\n * Filters and flips changes for keys that have not been sent yet.\n * Deletes are filtered out for keys that have not been sent yet.\n * Updates are flipped into inserts for keys that have not been sent yet.\n */\n private filterAndFlipChanges(changes: Array<ChangeMessage<any, any>>) {\n if (this.loadedInitialState) {\n // We loaded the entire initial state\n // so no need to filter or flip changes\n return changes\n }\n\n const newChanges = []\n for (const change of changes) {\n let newChange = change\n if (!this.sentKeys.has(change.key)) {\n if (change.type === `update`) {\n newChange = { ...change, type: `insert`, previousValue: undefined }\n } else if (change.type === `delete`) {\n // filter out deletes for keys that have not been sent\n continue\n }\n this.sentKeys.add(change.key)\n }\n newChanges.push(newChange)\n }\n return newChanges\n }\n\n private trackSentKeys(changes: Array<ChangeMessage<any, string | number>>) {\n if (this.loadedInitialState) {\n // No need to track sent keys if we loaded the entire state.\n // Since we sent everything, all keys must have been observed.\n return\n }\n\n for (const change of changes) {\n this.sentKeys.add(change.key)\n }\n }\n\n unsubscribe() {\n this.options.onUnsubscribe?.()\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"subscription.js","sources":["../../../src/collection/subscription.ts"],"sourcesContent":["import { ensureIndexForExpression } from \"../indexes/auto-index.js\"\nimport { and } from \"../query/builder/functions.js\"\nimport {\n createFilterFunctionFromExpression,\n createFilteredCallback,\n} from \"./change-events.js\"\nimport type { BasicExpression } from \"../query/ir.js\"\nimport type { BaseIndex } from \"../indexes/base-index.js\"\nimport type { ChangeMessage } from \"../types.js\"\nimport type { CollectionImpl } from \"./index.js\"\n\ntype RequestSnapshotOptions = {\n where?: BasicExpression<boolean>\n optimizedOnly?: boolean\n}\n\ntype RequestLimitedSnapshotOptions = {\n minValue?: any\n limit: number\n}\n\ntype CollectionSubscriptionOptions = {\n /** Pre-compiled expression for filtering changes */\n whereExpression?: BasicExpression<boolean>\n /** Callback to call when the subscription is unsubscribed */\n onUnsubscribe?: () => void\n}\n\nexport class CollectionSubscription {\n private loadedInitialState = false\n\n // Flag to indicate that we have sent at least 1 snapshot.\n // While `snapshotSent` is false we filter out all changes from subscription to the collection.\n private snapshotSent = false\n\n // Keep track of the keys we've sent (needed for join and orderBy optimizations)\n private sentKeys = new Set<string | number>()\n\n private filteredCallback: (changes: Array<ChangeMessage<any, any>>) => void\n\n private orderByIndex: BaseIndex<string | number> | undefined\n\n constructor(\n private collection: CollectionImpl<any, any, any, any, any>,\n private callback: (changes: Array<ChangeMessage<any, any>>) => void,\n private options: CollectionSubscriptionOptions\n ) {\n // Auto-index for where expressions if enabled\n if (options.whereExpression) {\n ensureIndexForExpression(options.whereExpression, this.collection)\n }\n\n const callbackWithSentKeysTracking = (\n changes: Array<ChangeMessage<any, any>>\n ) => {\n callback(changes)\n this.trackSentKeys(changes)\n }\n\n this.callback = callbackWithSentKeysTracking\n\n // Create a filtered callback if where clause is provided\n this.filteredCallback = options.whereExpression\n ? createFilteredCallback(this.callback, options)\n : this.callback\n }\n\n setOrderByIndex(index: BaseIndex<any>) {\n this.orderByIndex = index\n }\n\n hasLoadedInitialState() {\n return this.loadedInitialState\n }\n\n hasSentAtLeastOneSnapshot() {\n return this.snapshotSent\n }\n\n emitEvents(changes: Array<ChangeMessage<any, any>>) {\n const newChanges = this.filterAndFlipChanges(changes)\n this.filteredCallback(newChanges)\n }\n\n /**\n * Sends the snapshot to the callback.\n * Returns a boolean indicating if it succeeded.\n * It can only fail if there is no index to fulfill the request\n * and the optimizedOnly option is set to true,\n * or, the entire state was already loaded.\n */\n requestSnapshot(opts?: RequestSnapshotOptions): boolean {\n if (this.loadedInitialState) {\n // Subscription was deoptimized so we already sent the entire initial state\n return false\n }\n\n const stateOpts: RequestSnapshotOptions = {\n where: this.options.whereExpression,\n optimizedOnly: opts?.optimizedOnly ?? false,\n }\n\n if (opts) {\n if (`where` in opts) {\n const snapshotWhereExp = opts.where\n if (stateOpts.where) {\n // Combine the two where expressions\n const subWhereExp = stateOpts.where\n const combinedWhereExp = and(subWhereExp, snapshotWhereExp)\n stateOpts.where = combinedWhereExp\n } else {\n stateOpts.where = snapshotWhereExp\n }\n }\n } else {\n // No options provided so it's loading the entire initial state\n this.loadedInitialState = true\n }\n\n const snapshot = this.collection.currentStateAsChanges(stateOpts)\n\n if (snapshot === undefined) {\n // Couldn't load from indexes\n return false\n }\n\n // Only send changes that have not been sent yet\n const filteredSnapshot = snapshot.filter(\n (change) => !this.sentKeys.has(change.key)\n )\n\n this.snapshotSent = true\n this.callback(filteredSnapshot)\n return true\n }\n\n /**\n * Sends a snapshot that is limited to the first `limit` rows that fulfill the `where` clause and are bigger than `minValue`.\n * Requires a range index to be set with `setOrderByIndex` prior to calling this method.\n * It uses that range index to load the items in the order of the index.\n * Note: it does not send keys that have already been sent before.\n */\n requestLimitedSnapshot({ limit, minValue }: RequestLimitedSnapshotOptions) {\n if (!limit) throw new Error(`limit is required`)\n\n if (!this.orderByIndex) {\n throw new Error(\n `Ordered snapshot was requested but no index was found. You have to call setOrderByIndex before requesting an ordered snapshot.`\n )\n }\n\n const index = this.orderByIndex\n const where = this.options.whereExpression\n const whereFilterFn = where\n ? createFilterFunctionFromExpression(where)\n : undefined\n\n const filterFn = (key: string | number): boolean => {\n if (this.sentKeys.has(key)) {\n return false\n }\n\n const value = this.collection.get(key)\n if (value === undefined) {\n return false\n }\n\n return whereFilterFn?.(value) ?? true\n }\n\n let biggestObservedValue = minValue\n const changes: Array<ChangeMessage<any, string | number>> = []\n let keys: Array<string | number> = index.take(limit, minValue, filterFn)\n\n const valuesNeeded = () => Math.max(limit - changes.length, 0)\n const collectionExhausted = () => keys.length === 0\n\n while (valuesNeeded() > 0 && !collectionExhausted()) {\n for (const key of keys) {\n const value = this.collection.get(key)!\n changes.push({\n type: `insert`,\n key,\n value,\n })\n biggestObservedValue = value\n }\n\n keys = index.take(valuesNeeded(), biggestObservedValue, filterFn)\n }\n\n this.callback(changes)\n }\n\n /**\n * Filters and flips changes for keys that have not been sent yet.\n * Deletes are filtered out for keys that have not been sent yet.\n * Updates are flipped into inserts for keys that have not been sent yet.\n */\n private filterAndFlipChanges(changes: Array<ChangeMessage<any, any>>) {\n if (this.loadedInitialState) {\n // We loaded the entire initial state\n // so no need to filter or flip changes\n return changes\n }\n\n const newChanges = []\n for (const change of changes) {\n let newChange = change\n if (!this.sentKeys.has(change.key)) {\n if (change.type === `update`) {\n newChange = { ...change, type: `insert`, previousValue: undefined }\n } else if (change.type === `delete`) {\n // filter out deletes for keys that have not been sent\n continue\n }\n this.sentKeys.add(change.key)\n }\n newChanges.push(newChange)\n }\n return newChanges\n }\n\n private trackSentKeys(changes: Array<ChangeMessage<any, string | number>>) {\n if (this.loadedInitialState) {\n // No need to track sent keys if we loaded the entire state.\n // Since we sent everything, all keys must have been observed.\n return\n }\n\n for (const change of changes) {\n this.sentKeys.add(change.key)\n }\n }\n\n unsubscribe() {\n this.options.onUnsubscribe?.()\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -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
|
class BTreeIndex extends BaseIndex {
|
|
5
5
|
constructor(id, expression, name, options) {
|
|
@@ -32,12 +32,13 @@ class BTreeIndex extends BaseIndex {
|
|
|
32
32
|
`Failed to evaluate index expression for key ${key}: ${error}`
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const normalizedValue = normalizeValue(indexedValue);
|
|
36
|
+
if (this.valueMap.has(normalizedValue)) {
|
|
37
|
+
this.valueMap.get(normalizedValue).add(key);
|
|
37
38
|
} else {
|
|
38
39
|
const keySet = /* @__PURE__ */ new Set([key]);
|
|
39
|
-
this.valueMap.set(
|
|
40
|
-
this.orderedEntries.set(
|
|
40
|
+
this.valueMap.set(normalizedValue, keySet);
|
|
41
|
+
this.orderedEntries.set(normalizedValue, void 0);
|
|
41
42
|
}
|
|
42
43
|
this.indexedKeys.add(key);
|
|
43
44
|
this.updateTimestamp();
|
|
@@ -56,12 +57,13 @@ class BTreeIndex extends BaseIndex {
|
|
|
56
57
|
);
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
const normalizedValue = normalizeValue(indexedValue);
|
|
61
|
+
if (this.valueMap.has(normalizedValue)) {
|
|
62
|
+
const keySet = this.valueMap.get(normalizedValue);
|
|
61
63
|
keySet.delete(key);
|
|
62
64
|
if (keySet.size === 0) {
|
|
63
|
-
this.valueMap.delete(
|
|
64
|
-
this.orderedEntries.delete(
|
|
65
|
+
this.valueMap.delete(normalizedValue);
|
|
66
|
+
this.orderedEntries.delete(normalizedValue);
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
this.indexedKeys.delete(key);
|
|
@@ -134,7 +136,8 @@ class BTreeIndex extends BaseIndex {
|
|
|
134
136
|
* Performs an equality lookup
|
|
135
137
|
*/
|
|
136
138
|
equalityLookup(value) {
|
|
137
|
-
|
|
139
|
+
const normalizedValue = normalizeValue(value);
|
|
140
|
+
return new Set(this.valueMap.get(normalizedValue) ?? []);
|
|
138
141
|
}
|
|
139
142
|
/**
|
|
140
143
|
* Performs a range query with options
|
|
@@ -143,8 +146,10 @@ class BTreeIndex extends BaseIndex {
|
|
|
143
146
|
rangeQuery(options = {}) {
|
|
144
147
|
const { from, to, fromInclusive = true, toInclusive = true } = options;
|
|
145
148
|
const result = /* @__PURE__ */ new Set();
|
|
146
|
-
const
|
|
147
|
-
const
|
|
149
|
+
const normalizedFrom = normalizeValue(from);
|
|
150
|
+
const normalizedTo = normalizeValue(to);
|
|
151
|
+
const fromKey = normalizedFrom ?? this.orderedEntries.minKey();
|
|
152
|
+
const toKey = normalizedTo ?? this.orderedEntries.maxKey();
|
|
148
153
|
this.orderedEntries.forRange(
|
|
149
154
|
fromKey,
|
|
150
155
|
toKey,
|
|
@@ -171,7 +176,7 @@ class BTreeIndex extends BaseIndex {
|
|
|
171
176
|
const keysInResult = /* @__PURE__ */ new Set();
|
|
172
177
|
const result = [];
|
|
173
178
|
const nextKey = (k) => this.orderedEntries.nextHigherKey(k);
|
|
174
|
-
let key = from;
|
|
179
|
+
let key = normalizeValue(from);
|
|
175
180
|
while ((key = nextKey(key)) && result.length < n) {
|
|
176
181
|
const keys = this.valueMap.get(key);
|
|
177
182
|
if (keys) {
|
|
@@ -193,7 +198,8 @@ class BTreeIndex extends BaseIndex {
|
|
|
193
198
|
inArrayLookup(values) {
|
|
194
199
|
const result = /* @__PURE__ */ new Set();
|
|
195
200
|
for (const value of values) {
|
|
196
|
-
const
|
|
201
|
+
const normalizedValue = normalizeValue(value);
|
|
202
|
+
const keys = this.valueMap.get(normalizedValue);
|
|
197
203
|
if (keys) {
|
|
198
204
|
keys.forEach((key) => result.add(key));
|
|
199
205
|
}
|