@tanstack/db 0.4.0 → 0.4.1
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/lifecycle.cjs +58 -12
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/collection/lifecycle.d.cts +11 -0
- package/dist/cjs/utils/browser-polyfills.cjs +22 -0
- package/dist/cjs/utils/browser-polyfills.cjs.map +1 -0
- package/dist/cjs/utils/browser-polyfills.d.cts +9 -0
- package/dist/esm/collection/lifecycle.d.ts +11 -0
- package/dist/esm/collection/lifecycle.js +58 -12
- package/dist/esm/collection/lifecycle.js.map +1 -1
- package/dist/esm/utils/browser-polyfills.d.ts +9 -0
- package/dist/esm/utils/browser-polyfills.js +22 -0
- package/dist/esm/utils/browser-polyfills.js.map +1 -0
- package/package.json +1 -1
- package/src/collection/lifecycle.ts +85 -17
- package/src/utils/browser-polyfills.ts +39 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const errors = require("../errors.cjs");
|
|
4
|
+
const browserPolyfills = require("../utils/browser-polyfills.cjs");
|
|
4
5
|
class CollectionLifecycleManager {
|
|
5
6
|
/**
|
|
6
7
|
* Creates a new CollectionLifecycleManager instance
|
|
@@ -11,6 +12,7 @@ class CollectionLifecycleManager {
|
|
|
11
12
|
this.hasReceivedFirstCommit = false;
|
|
12
13
|
this.onFirstReadyCallbacks = [];
|
|
13
14
|
this.gcTimeoutId = null;
|
|
15
|
+
this.idleCallbackId = null;
|
|
14
16
|
this.config = config;
|
|
15
17
|
this.id = id;
|
|
16
18
|
}
|
|
@@ -105,7 +107,7 @@ class CollectionLifecycleManager {
|
|
|
105
107
|
}
|
|
106
108
|
this.gcTimeoutId = setTimeout(() => {
|
|
107
109
|
if (this.changes.activeSubscribersCount === 0) {
|
|
108
|
-
this.
|
|
110
|
+
this.scheduleIdleCleanup();
|
|
109
111
|
}
|
|
110
112
|
}, gcTime);
|
|
111
113
|
}
|
|
@@ -118,6 +120,57 @@ class CollectionLifecycleManager {
|
|
|
118
120
|
clearTimeout(this.gcTimeoutId);
|
|
119
121
|
this.gcTimeoutId = null;
|
|
120
122
|
}
|
|
123
|
+
if (this.idleCallbackId !== null) {
|
|
124
|
+
browserPolyfills.safeCancelIdleCallback(this.idleCallbackId);
|
|
125
|
+
this.idleCallbackId = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Schedule cleanup to run during browser idle time
|
|
130
|
+
* This prevents blocking the UI thread during cleanup operations
|
|
131
|
+
*/
|
|
132
|
+
scheduleIdleCleanup() {
|
|
133
|
+
if (this.idleCallbackId !== null) {
|
|
134
|
+
browserPolyfills.safeCancelIdleCallback(this.idleCallbackId);
|
|
135
|
+
}
|
|
136
|
+
this.idleCallbackId = browserPolyfills.safeRequestIdleCallback(
|
|
137
|
+
(deadline) => {
|
|
138
|
+
if (this.changes.activeSubscribersCount === 0) {
|
|
139
|
+
const cleanupCompleted = this.performCleanup(deadline);
|
|
140
|
+
if (cleanupCompleted) {
|
|
141
|
+
this.idleCallbackId = null;
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
this.idleCallbackId = null;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
{ timeout: 1e3 }
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Perform cleanup operations, optionally in chunks during idle time
|
|
152
|
+
* @returns true if cleanup was completed, false if it was rescheduled
|
|
153
|
+
*/
|
|
154
|
+
performCleanup(deadline) {
|
|
155
|
+
const hasTime = !deadline || deadline.timeRemaining() > 0 || deadline.didTimeout;
|
|
156
|
+
if (hasTime) {
|
|
157
|
+
this.events.cleanup();
|
|
158
|
+
this.sync.cleanup();
|
|
159
|
+
this.state.cleanup();
|
|
160
|
+
this.changes.cleanup();
|
|
161
|
+
this.indexes.cleanup();
|
|
162
|
+
if (this.gcTimeoutId) {
|
|
163
|
+
clearTimeout(this.gcTimeoutId);
|
|
164
|
+
this.gcTimeoutId = null;
|
|
165
|
+
}
|
|
166
|
+
this.hasBeenReady = false;
|
|
167
|
+
this.onFirstReadyCallbacks = [];
|
|
168
|
+
this.setStatus(`cleaned-up`);
|
|
169
|
+
return true;
|
|
170
|
+
} else {
|
|
171
|
+
this.scheduleIdleCleanup();
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
121
174
|
}
|
|
122
175
|
/**
|
|
123
176
|
* Register a callback to be executed when the collection first becomes ready
|
|
@@ -132,18 +185,11 @@ class CollectionLifecycleManager {
|
|
|
132
185
|
this.onFirstReadyCallbacks.push(callback);
|
|
133
186
|
}
|
|
134
187
|
cleanup() {
|
|
135
|
-
this.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
this.changes.cleanup();
|
|
139
|
-
this.indexes.cleanup();
|
|
140
|
-
if (this.gcTimeoutId) {
|
|
141
|
-
clearTimeout(this.gcTimeoutId);
|
|
142
|
-
this.gcTimeoutId = null;
|
|
188
|
+
if (this.idleCallbackId !== null) {
|
|
189
|
+
browserPolyfills.safeCancelIdleCallback(this.idleCallbackId);
|
|
190
|
+
this.idleCallbackId = null;
|
|
143
191
|
}
|
|
144
|
-
this.
|
|
145
|
-
this.onFirstReadyCallbacks = [];
|
|
146
|
-
this.setStatus(`cleaned-up`);
|
|
192
|
+
this.performCleanup();
|
|
147
193
|
}
|
|
148
194
|
}
|
|
149
195
|
exports.CollectionLifecycleManager = CollectionLifecycleManager;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.cjs","sources":["../../../src/collection/lifecycle.ts"],"sourcesContent":["import {\n CollectionInErrorStateError,\n InvalidCollectionStatusTransitionError,\n} from \"../errors\"\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\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 // We call the main collection cleanup, not just the one for the\n // lifecycle manager\n this.cleanup()\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 }\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 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 }\n}\n"],"names":["InvalidCollectionStatusTransitionError","CollectionInErrorStateError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecycle.cjs","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":["InvalidCollectionStatusTransitionError","CollectionInErrorStateError","safeCancelIdleCallback","safeRequestIdleCallback"],"mappings":"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"}
|
|
@@ -18,6 +18,7 @@ export declare class CollectionLifecycleManager<TOutput extends object = Record<
|
|
|
18
18
|
hasReceivedFirstCommit: boolean;
|
|
19
19
|
onFirstReadyCallbacks: Array<() => void>;
|
|
20
20
|
gcTimeoutId: ReturnType<typeof setTimeout> | null;
|
|
21
|
+
private idleCallbackId;
|
|
21
22
|
/**
|
|
22
23
|
* Creates a new CollectionLifecycleManager instance
|
|
23
24
|
*/
|
|
@@ -60,6 +61,16 @@ export declare class CollectionLifecycleManager<TOutput extends object = Record<
|
|
|
60
61
|
* Called when the collection becomes active again
|
|
61
62
|
*/
|
|
62
63
|
cancelGCTimer(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Schedule cleanup to run during browser idle time
|
|
66
|
+
* This prevents blocking the UI thread during cleanup operations
|
|
67
|
+
*/
|
|
68
|
+
private scheduleIdleCleanup;
|
|
69
|
+
/**
|
|
70
|
+
* Perform cleanup operations, optionally in chunks during idle time
|
|
71
|
+
* @returns true if cleanup was completed, false if it was rescheduled
|
|
72
|
+
*/
|
|
73
|
+
private performCleanup;
|
|
63
74
|
/**
|
|
64
75
|
* Register a callback to be executed when the collection first becomes ready
|
|
65
76
|
* Useful for preloading collections
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const requestIdleCallbackPolyfill = (callback) => {
|
|
4
|
+
const timeout = 0;
|
|
5
|
+
const timeoutId = setTimeout(() => {
|
|
6
|
+
callback({
|
|
7
|
+
didTimeout: true,
|
|
8
|
+
// Always indicate timeout for the polyfill
|
|
9
|
+
timeRemaining: () => 50
|
|
10
|
+
// Return some time remaining for polyfill
|
|
11
|
+
});
|
|
12
|
+
}, timeout);
|
|
13
|
+
return timeoutId;
|
|
14
|
+
};
|
|
15
|
+
const cancelIdleCallbackPolyfill = (id) => {
|
|
16
|
+
clearTimeout(id);
|
|
17
|
+
};
|
|
18
|
+
const safeRequestIdleCallback = typeof window !== `undefined` && `requestIdleCallback` in window ? (callback, options) => window.requestIdleCallback(callback, options) : (callback, _options) => requestIdleCallbackPolyfill(callback);
|
|
19
|
+
const safeCancelIdleCallback = typeof window !== `undefined` && `cancelIdleCallback` in window ? (id) => window.cancelIdleCallback(id) : cancelIdleCallbackPolyfill;
|
|
20
|
+
exports.safeCancelIdleCallback = safeCancelIdleCallback;
|
|
21
|
+
exports.safeRequestIdleCallback = safeRequestIdleCallback;
|
|
22
|
+
//# sourceMappingURL=browser-polyfills.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-polyfills.cjs","sources":["../../../src/utils/browser-polyfills.ts"],"sourcesContent":["// Type definitions for requestIdleCallback - compatible with existing browser types\nexport type IdleCallbackDeadline = {\n didTimeout: boolean\n timeRemaining: () => number\n}\n\nexport type IdleCallbackFunction = (deadline: IdleCallbackDeadline) => void\n\nconst requestIdleCallbackPolyfill = (\n callback: IdleCallbackFunction\n): number => {\n // Use a very small timeout for the polyfill to simulate idle time\n const timeout = 0\n const timeoutId = setTimeout(() => {\n callback({\n didTimeout: true, // Always indicate timeout for the polyfill\n timeRemaining: () => 50, // Return some time remaining for polyfill\n })\n }, timeout)\n return timeoutId as unknown as number\n}\n\nconst cancelIdleCallbackPolyfill = (id: number): void => {\n clearTimeout(id as unknown as ReturnType<typeof setTimeout>)\n}\n\nexport const safeRequestIdleCallback: (\n callback: IdleCallbackFunction,\n options?: { timeout?: number }\n) => number =\n typeof window !== `undefined` && `requestIdleCallback` in window\n ? (callback, options) =>\n (window as any).requestIdleCallback(callback, options)\n : (callback, _options) => requestIdleCallbackPolyfill(callback)\n\nexport const safeCancelIdleCallback: (id: number) => void =\n typeof window !== `undefined` && `cancelIdleCallback` in window\n ? (id) => (window as any).cancelIdleCallback(id)\n : cancelIdleCallbackPolyfill\n"],"names":[],"mappings":";;AAQA,MAAM,8BAA8B,CAClC,aACW;AAEX,QAAM,UAAU;AAChB,QAAM,YAAY,WAAW,MAAM;AACjC,aAAS;AAAA,MACP,YAAY;AAAA;AAAA,MACZ,eAAe,MAAM;AAAA;AAAA,IAAA,CACtB;AAAA,EACH,GAAG,OAAO;AACV,SAAO;AACT;AAEA,MAAM,6BAA6B,CAAC,OAAqB;AACvD,eAAa,EAA8C;AAC7D;AAEO,MAAM,0BAIX,OAAO,WAAW,eAAe,yBAAyB,SACtD,CAAC,UAAU,YACR,OAAe,oBAAoB,UAAU,OAAO,IACvD,CAAC,UAAU,aAAa,4BAA4B,QAAQ;AAE3D,MAAM,yBACX,OAAO,WAAW,eAAe,wBAAwB,SACrD,CAAC,OAAQ,OAAe,mBAAmB,EAAE,IAC7C;;;"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type IdleCallbackDeadline = {
|
|
2
|
+
didTimeout: boolean;
|
|
3
|
+
timeRemaining: () => number;
|
|
4
|
+
};
|
|
5
|
+
export type IdleCallbackFunction = (deadline: IdleCallbackDeadline) => void;
|
|
6
|
+
export declare const safeRequestIdleCallback: (callback: IdleCallbackFunction, options?: {
|
|
7
|
+
timeout?: number;
|
|
8
|
+
}) => number;
|
|
9
|
+
export declare const safeCancelIdleCallback: (id: number) => void;
|
|
@@ -18,6 +18,7 @@ export declare class CollectionLifecycleManager<TOutput extends object = Record<
|
|
|
18
18
|
hasReceivedFirstCommit: boolean;
|
|
19
19
|
onFirstReadyCallbacks: Array<() => void>;
|
|
20
20
|
gcTimeoutId: ReturnType<typeof setTimeout> | null;
|
|
21
|
+
private idleCallbackId;
|
|
21
22
|
/**
|
|
22
23
|
* Creates a new CollectionLifecycleManager instance
|
|
23
24
|
*/
|
|
@@ -60,6 +61,16 @@ export declare class CollectionLifecycleManager<TOutput extends object = Record<
|
|
|
60
61
|
* Called when the collection becomes active again
|
|
61
62
|
*/
|
|
62
63
|
cancelGCTimer(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Schedule cleanup to run during browser idle time
|
|
66
|
+
* This prevents blocking the UI thread during cleanup operations
|
|
67
|
+
*/
|
|
68
|
+
private scheduleIdleCleanup;
|
|
69
|
+
/**
|
|
70
|
+
* Perform cleanup operations, optionally in chunks during idle time
|
|
71
|
+
* @returns true if cleanup was completed, false if it was rescheduled
|
|
72
|
+
*/
|
|
73
|
+
private performCleanup;
|
|
63
74
|
/**
|
|
64
75
|
* Register a callback to be executed when the collection first becomes ready
|
|
65
76
|
* Useful for preloading collections
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { InvalidCollectionStatusTransitionError, CollectionInErrorStateError } from "../errors.js";
|
|
2
|
+
import { safeCancelIdleCallback, safeRequestIdleCallback } from "../utils/browser-polyfills.js";
|
|
2
3
|
class CollectionLifecycleManager {
|
|
3
4
|
/**
|
|
4
5
|
* Creates a new CollectionLifecycleManager instance
|
|
@@ -9,6 +10,7 @@ class CollectionLifecycleManager {
|
|
|
9
10
|
this.hasReceivedFirstCommit = false;
|
|
10
11
|
this.onFirstReadyCallbacks = [];
|
|
11
12
|
this.gcTimeoutId = null;
|
|
13
|
+
this.idleCallbackId = null;
|
|
12
14
|
this.config = config;
|
|
13
15
|
this.id = id;
|
|
14
16
|
}
|
|
@@ -103,7 +105,7 @@ class CollectionLifecycleManager {
|
|
|
103
105
|
}
|
|
104
106
|
this.gcTimeoutId = setTimeout(() => {
|
|
105
107
|
if (this.changes.activeSubscribersCount === 0) {
|
|
106
|
-
this.
|
|
108
|
+
this.scheduleIdleCleanup();
|
|
107
109
|
}
|
|
108
110
|
}, gcTime);
|
|
109
111
|
}
|
|
@@ -116,6 +118,57 @@ class CollectionLifecycleManager {
|
|
|
116
118
|
clearTimeout(this.gcTimeoutId);
|
|
117
119
|
this.gcTimeoutId = null;
|
|
118
120
|
}
|
|
121
|
+
if (this.idleCallbackId !== null) {
|
|
122
|
+
safeCancelIdleCallback(this.idleCallbackId);
|
|
123
|
+
this.idleCallbackId = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Schedule cleanup to run during browser idle time
|
|
128
|
+
* This prevents blocking the UI thread during cleanup operations
|
|
129
|
+
*/
|
|
130
|
+
scheduleIdleCleanup() {
|
|
131
|
+
if (this.idleCallbackId !== null) {
|
|
132
|
+
safeCancelIdleCallback(this.idleCallbackId);
|
|
133
|
+
}
|
|
134
|
+
this.idleCallbackId = safeRequestIdleCallback(
|
|
135
|
+
(deadline) => {
|
|
136
|
+
if (this.changes.activeSubscribersCount === 0) {
|
|
137
|
+
const cleanupCompleted = this.performCleanup(deadline);
|
|
138
|
+
if (cleanupCompleted) {
|
|
139
|
+
this.idleCallbackId = null;
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
this.idleCallbackId = null;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
{ timeout: 1e3 }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Perform cleanup operations, optionally in chunks during idle time
|
|
150
|
+
* @returns true if cleanup was completed, false if it was rescheduled
|
|
151
|
+
*/
|
|
152
|
+
performCleanup(deadline) {
|
|
153
|
+
const hasTime = !deadline || deadline.timeRemaining() > 0 || deadline.didTimeout;
|
|
154
|
+
if (hasTime) {
|
|
155
|
+
this.events.cleanup();
|
|
156
|
+
this.sync.cleanup();
|
|
157
|
+
this.state.cleanup();
|
|
158
|
+
this.changes.cleanup();
|
|
159
|
+
this.indexes.cleanup();
|
|
160
|
+
if (this.gcTimeoutId) {
|
|
161
|
+
clearTimeout(this.gcTimeoutId);
|
|
162
|
+
this.gcTimeoutId = null;
|
|
163
|
+
}
|
|
164
|
+
this.hasBeenReady = false;
|
|
165
|
+
this.onFirstReadyCallbacks = [];
|
|
166
|
+
this.setStatus(`cleaned-up`);
|
|
167
|
+
return true;
|
|
168
|
+
} else {
|
|
169
|
+
this.scheduleIdleCleanup();
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
119
172
|
}
|
|
120
173
|
/**
|
|
121
174
|
* Register a callback to be executed when the collection first becomes ready
|
|
@@ -130,18 +183,11 @@ class CollectionLifecycleManager {
|
|
|
130
183
|
this.onFirstReadyCallbacks.push(callback);
|
|
131
184
|
}
|
|
132
185
|
cleanup() {
|
|
133
|
-
this.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.changes.cleanup();
|
|
137
|
-
this.indexes.cleanup();
|
|
138
|
-
if (this.gcTimeoutId) {
|
|
139
|
-
clearTimeout(this.gcTimeoutId);
|
|
140
|
-
this.gcTimeoutId = null;
|
|
186
|
+
if (this.idleCallbackId !== null) {
|
|
187
|
+
safeCancelIdleCallback(this.idleCallbackId);
|
|
188
|
+
this.idleCallbackId = null;
|
|
141
189
|
}
|
|
142
|
-
this.
|
|
143
|
-
this.onFirstReadyCallbacks = [];
|
|
144
|
-
this.setStatus(`cleaned-up`);
|
|
190
|
+
this.performCleanup();
|
|
145
191
|
}
|
|
146
192
|
}
|
|
147
193
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.js","sources":["../../../src/collection/lifecycle.ts"],"sourcesContent":["import {\n CollectionInErrorStateError,\n InvalidCollectionStatusTransitionError,\n} from \"../errors\"\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\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 // We call the main collection cleanup, not just the one for the\n // lifecycle manager\n this.cleanup()\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 }\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 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 }\n}\n"],"names":[],"mappings":"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"}
|
|
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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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type IdleCallbackDeadline = {
|
|
2
|
+
didTimeout: boolean;
|
|
3
|
+
timeRemaining: () => number;
|
|
4
|
+
};
|
|
5
|
+
export type IdleCallbackFunction = (deadline: IdleCallbackDeadline) => void;
|
|
6
|
+
export declare const safeRequestIdleCallback: (callback: IdleCallbackFunction, options?: {
|
|
7
|
+
timeout?: number;
|
|
8
|
+
}) => number;
|
|
9
|
+
export declare const safeCancelIdleCallback: (id: number) => void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const requestIdleCallbackPolyfill = (callback) => {
|
|
2
|
+
const timeout = 0;
|
|
3
|
+
const timeoutId = setTimeout(() => {
|
|
4
|
+
callback({
|
|
5
|
+
didTimeout: true,
|
|
6
|
+
// Always indicate timeout for the polyfill
|
|
7
|
+
timeRemaining: () => 50
|
|
8
|
+
// Return some time remaining for polyfill
|
|
9
|
+
});
|
|
10
|
+
}, timeout);
|
|
11
|
+
return timeoutId;
|
|
12
|
+
};
|
|
13
|
+
const cancelIdleCallbackPolyfill = (id) => {
|
|
14
|
+
clearTimeout(id);
|
|
15
|
+
};
|
|
16
|
+
const safeRequestIdleCallback = typeof window !== `undefined` && `requestIdleCallback` in window ? (callback, options) => window.requestIdleCallback(callback, options) : (callback, _options) => requestIdleCallbackPolyfill(callback);
|
|
17
|
+
const safeCancelIdleCallback = typeof window !== `undefined` && `cancelIdleCallback` in window ? (id) => window.cancelIdleCallback(id) : cancelIdleCallbackPolyfill;
|
|
18
|
+
export {
|
|
19
|
+
safeCancelIdleCallback,
|
|
20
|
+
safeRequestIdleCallback
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=browser-polyfills.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-polyfills.js","sources":["../../../src/utils/browser-polyfills.ts"],"sourcesContent":["// Type definitions for requestIdleCallback - compatible with existing browser types\nexport type IdleCallbackDeadline = {\n didTimeout: boolean\n timeRemaining: () => number\n}\n\nexport type IdleCallbackFunction = (deadline: IdleCallbackDeadline) => void\n\nconst requestIdleCallbackPolyfill = (\n callback: IdleCallbackFunction\n): number => {\n // Use a very small timeout for the polyfill to simulate idle time\n const timeout = 0\n const timeoutId = setTimeout(() => {\n callback({\n didTimeout: true, // Always indicate timeout for the polyfill\n timeRemaining: () => 50, // Return some time remaining for polyfill\n })\n }, timeout)\n return timeoutId as unknown as number\n}\n\nconst cancelIdleCallbackPolyfill = (id: number): void => {\n clearTimeout(id as unknown as ReturnType<typeof setTimeout>)\n}\n\nexport const safeRequestIdleCallback: (\n callback: IdleCallbackFunction,\n options?: { timeout?: number }\n) => number =\n typeof window !== `undefined` && `requestIdleCallback` in window\n ? (callback, options) =>\n (window as any).requestIdleCallback(callback, options)\n : (callback, _options) => requestIdleCallbackPolyfill(callback)\n\nexport const safeCancelIdleCallback: (id: number) => void =\n typeof window !== `undefined` && `cancelIdleCallback` in window\n ? (id) => (window as any).cancelIdleCallback(id)\n : cancelIdleCallbackPolyfill\n"],"names":[],"mappings":"AAQA,MAAM,8BAA8B,CAClC,aACW;AAEX,QAAM,UAAU;AAChB,QAAM,YAAY,WAAW,MAAM;AACjC,aAAS;AAAA,MACP,YAAY;AAAA;AAAA,MACZ,eAAe,MAAM;AAAA;AAAA,IAAA,CACtB;AAAA,EACH,GAAG,OAAO;AACV,SAAO;AACT;AAEA,MAAM,6BAA6B,CAAC,OAAqB;AACvD,eAAa,EAA8C;AAC7D;AAEO,MAAM,0BAIX,OAAO,WAAW,eAAe,yBAAyB,SACtD,CAAC,UAAU,YACR,OAAe,oBAAoB,UAAU,OAAO,IACvD,CAAC,UAAU,aAAa,4BAA4B,QAAQ;AAE3D,MAAM,yBACX,OAAO,WAAW,eAAe,wBAAwB,SACrD,CAAC,OAAQ,OAAe,mBAAmB,EAAE,IAC7C;"}
|
package/package.json
CHANGED
|
@@ -2,6 +2,11 @@ import {
|
|
|
2
2
|
CollectionInErrorStateError,
|
|
3
3
|
InvalidCollectionStatusTransitionError,
|
|
4
4
|
} from "../errors"
|
|
5
|
+
import {
|
|
6
|
+
safeCancelIdleCallback,
|
|
7
|
+
safeRequestIdleCallback,
|
|
8
|
+
} from "../utils/browser-polyfills"
|
|
9
|
+
import type { IdleCallbackDeadline } from "../utils/browser-polyfills"
|
|
5
10
|
import type { StandardSchemaV1 } from "@standard-schema/spec"
|
|
6
11
|
import type { CollectionConfig, CollectionStatus } from "../types"
|
|
7
12
|
import type { CollectionEventsManager } from "./events"
|
|
@@ -29,6 +34,7 @@ export class CollectionLifecycleManager<
|
|
|
29
34
|
public hasReceivedFirstCommit = false
|
|
30
35
|
public onFirstReadyCallbacks: Array<() => void> = []
|
|
31
36
|
public gcTimeoutId: ReturnType<typeof setTimeout> | null = null
|
|
37
|
+
private idleCallbackId: number | null = null
|
|
32
38
|
|
|
33
39
|
/**
|
|
34
40
|
* Creates a new CollectionLifecycleManager instance
|
|
@@ -167,9 +173,8 @@ export class CollectionLifecycleManager<
|
|
|
167
173
|
|
|
168
174
|
this.gcTimeoutId = setTimeout(() => {
|
|
169
175
|
if (this.changes.activeSubscribersCount === 0) {
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
this.cleanup()
|
|
176
|
+
// Schedule cleanup during idle time to avoid blocking the UI thread
|
|
177
|
+
this.scheduleIdleCleanup()
|
|
173
178
|
}
|
|
174
179
|
}, gcTime)
|
|
175
180
|
}
|
|
@@ -183,6 +188,77 @@ export class CollectionLifecycleManager<
|
|
|
183
188
|
clearTimeout(this.gcTimeoutId)
|
|
184
189
|
this.gcTimeoutId = null
|
|
185
190
|
}
|
|
191
|
+
// Also cancel any pending idle cleanup
|
|
192
|
+
if (this.idleCallbackId !== null) {
|
|
193
|
+
safeCancelIdleCallback(this.idleCallbackId)
|
|
194
|
+
this.idleCallbackId = null
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Schedule cleanup to run during browser idle time
|
|
200
|
+
* This prevents blocking the UI thread during cleanup operations
|
|
201
|
+
*/
|
|
202
|
+
private scheduleIdleCleanup(): void {
|
|
203
|
+
// Cancel any existing idle callback
|
|
204
|
+
if (this.idleCallbackId !== null) {
|
|
205
|
+
safeCancelIdleCallback(this.idleCallbackId)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Schedule cleanup with a timeout of 1 second
|
|
209
|
+
// This ensures cleanup happens even if the browser is busy
|
|
210
|
+
this.idleCallbackId = safeRequestIdleCallback(
|
|
211
|
+
(deadline) => {
|
|
212
|
+
// Perform cleanup if we still have no subscribers
|
|
213
|
+
if (this.changes.activeSubscribersCount === 0) {
|
|
214
|
+
const cleanupCompleted = this.performCleanup(deadline)
|
|
215
|
+
// Only clear the callback ID if cleanup actually completed
|
|
216
|
+
if (cleanupCompleted) {
|
|
217
|
+
this.idleCallbackId = null
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
// No need to cleanup, clear the callback ID
|
|
221
|
+
this.idleCallbackId = null
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
{ timeout: 1000 }
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Perform cleanup operations, optionally in chunks during idle time
|
|
230
|
+
* @returns true if cleanup was completed, false if it was rescheduled
|
|
231
|
+
*/
|
|
232
|
+
private performCleanup(deadline?: IdleCallbackDeadline): boolean {
|
|
233
|
+
// If we have a deadline, we can potentially split cleanup into chunks
|
|
234
|
+
// For now, we'll do all cleanup at once but check if we have time
|
|
235
|
+
const hasTime =
|
|
236
|
+
!deadline || deadline.timeRemaining() > 0 || deadline.didTimeout
|
|
237
|
+
|
|
238
|
+
if (hasTime) {
|
|
239
|
+
// Perform all cleanup operations
|
|
240
|
+
this.events.cleanup()
|
|
241
|
+
this.sync.cleanup()
|
|
242
|
+
this.state.cleanup()
|
|
243
|
+
this.changes.cleanup()
|
|
244
|
+
this.indexes.cleanup()
|
|
245
|
+
|
|
246
|
+
if (this.gcTimeoutId) {
|
|
247
|
+
clearTimeout(this.gcTimeoutId)
|
|
248
|
+
this.gcTimeoutId = null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.hasBeenReady = false
|
|
252
|
+
this.onFirstReadyCallbacks = []
|
|
253
|
+
|
|
254
|
+
// Set status to cleaned-up
|
|
255
|
+
this.setStatus(`cleaned-up`)
|
|
256
|
+
return true
|
|
257
|
+
} else {
|
|
258
|
+
// If we don't have time, reschedule for the next idle period
|
|
259
|
+
this.scheduleIdleCleanup()
|
|
260
|
+
return false
|
|
261
|
+
}
|
|
186
262
|
}
|
|
187
263
|
|
|
188
264
|
/**
|
|
@@ -201,21 +277,13 @@ export class CollectionLifecycleManager<
|
|
|
201
277
|
}
|
|
202
278
|
|
|
203
279
|
public cleanup(): void {
|
|
204
|
-
|
|
205
|
-
this.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
this.indexes.cleanup()
|
|
209
|
-
|
|
210
|
-
if (this.gcTimeoutId) {
|
|
211
|
-
clearTimeout(this.gcTimeoutId)
|
|
212
|
-
this.gcTimeoutId = null
|
|
280
|
+
// Cancel any pending idle cleanup
|
|
281
|
+
if (this.idleCallbackId !== null) {
|
|
282
|
+
safeCancelIdleCallback(this.idleCallbackId)
|
|
283
|
+
this.idleCallbackId = null
|
|
213
284
|
}
|
|
214
285
|
|
|
215
|
-
|
|
216
|
-
this.
|
|
217
|
-
|
|
218
|
-
// Set status to cleaned-up
|
|
219
|
-
this.setStatus(`cleaned-up`)
|
|
286
|
+
// Perform cleanup immediately (used when explicitly called)
|
|
287
|
+
this.performCleanup()
|
|
220
288
|
}
|
|
221
289
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Type definitions for requestIdleCallback - compatible with existing browser types
|
|
2
|
+
export type IdleCallbackDeadline = {
|
|
3
|
+
didTimeout: boolean
|
|
4
|
+
timeRemaining: () => number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type IdleCallbackFunction = (deadline: IdleCallbackDeadline) => void
|
|
8
|
+
|
|
9
|
+
const requestIdleCallbackPolyfill = (
|
|
10
|
+
callback: IdleCallbackFunction
|
|
11
|
+
): number => {
|
|
12
|
+
// Use a very small timeout for the polyfill to simulate idle time
|
|
13
|
+
const timeout = 0
|
|
14
|
+
const timeoutId = setTimeout(() => {
|
|
15
|
+
callback({
|
|
16
|
+
didTimeout: true, // Always indicate timeout for the polyfill
|
|
17
|
+
timeRemaining: () => 50, // Return some time remaining for polyfill
|
|
18
|
+
})
|
|
19
|
+
}, timeout)
|
|
20
|
+
return timeoutId as unknown as number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const cancelIdleCallbackPolyfill = (id: number): void => {
|
|
24
|
+
clearTimeout(id as unknown as ReturnType<typeof setTimeout>)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const safeRequestIdleCallback: (
|
|
28
|
+
callback: IdleCallbackFunction,
|
|
29
|
+
options?: { timeout?: number }
|
|
30
|
+
) => number =
|
|
31
|
+
typeof window !== `undefined` && `requestIdleCallback` in window
|
|
32
|
+
? (callback, options) =>
|
|
33
|
+
(window as any).requestIdleCallback(callback, options)
|
|
34
|
+
: (callback, _options) => requestIdleCallbackPolyfill(callback)
|
|
35
|
+
|
|
36
|
+
export const safeCancelIdleCallback: (id: number) => void =
|
|
37
|
+
typeof window !== `undefined` && `cancelIdleCallback` in window
|
|
38
|
+
? (id) => (window as any).cancelIdleCallback(id)
|
|
39
|
+
: cancelIdleCallbackPolyfill
|