@tanstack/db 0.3.2 → 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/{change-events.cjs → collection/change-events.cjs} +13 -42
- package/dist/cjs/collection/change-events.cjs.map +1 -0
- package/dist/{esm/change-events.d.ts → cjs/collection/change-events.d.cts} +6 -6
- package/dist/cjs/collection/changes.cjs +108 -0
- package/dist/cjs/collection/changes.cjs.map +1 -0
- package/dist/cjs/collection/changes.d.cts +53 -0
- package/dist/cjs/{collection-events.cjs → collection/events.cjs} +7 -5
- package/dist/cjs/collection/events.cjs.map +1 -0
- package/dist/cjs/{collection-events.d.cts → collection/events.d.cts} +7 -4
- package/dist/cjs/collection/index.cjs +417 -0
- package/dist/cjs/collection/index.cjs.map +1 -0
- package/dist/{esm/collection.d.ts → cjs/collection/index.d.cts} +46 -184
- package/dist/cjs/collection/indexes.cjs +124 -0
- package/dist/cjs/collection/indexes.cjs.map +1 -0
- package/dist/cjs/collection/indexes.d.cts +47 -0
- package/dist/cjs/collection/lifecycle.cjs +196 -0
- package/dist/cjs/collection/lifecycle.cjs.map +1 -0
- package/dist/cjs/collection/lifecycle.d.cts +81 -0
- package/dist/cjs/collection/mutations.cjs +315 -0
- package/dist/cjs/collection/mutations.cjs.map +1 -0
- package/dist/cjs/collection/mutations.d.cts +33 -0
- package/dist/cjs/collection/state.cjs +597 -0
- package/dist/cjs/collection/state.cjs.map +1 -0
- package/dist/cjs/collection/state.d.cts +122 -0
- package/dist/cjs/collection/subscription.cjs +160 -0
- package/dist/cjs/collection/subscription.cjs.map +1 -0
- package/dist/cjs/collection/subscription.d.cts +57 -0
- package/dist/cjs/collection/sync.cjs +154 -0
- package/dist/cjs/collection/sync.cjs.map +1 -0
- package/dist/cjs/collection/sync.d.cts +34 -0
- package/dist/cjs/index.cjs +8 -8
- package/dist/cjs/index.d.cts +2 -2
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/indexes/auto-index.d.cts +1 -1
- package/dist/cjs/indexes/base-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.d.cts +2 -2
- package/dist/cjs/indexes/btree-index.cjs +2 -2
- package/dist/cjs/indexes/btree-index.cjs.map +1 -1
- package/dist/cjs/indexes/btree-index.d.cts +1 -1
- package/dist/cjs/query/builder/index.cjs +2 -2
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/types.d.cts +1 -1
- package/dist/cjs/query/compiler/index.cjs +5 -2
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +3 -2
- package/dist/cjs/query/compiler/joins.cjs +22 -24
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +3 -2
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +1 -1
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/ir.d.cts +1 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +29 -12
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +3 -0
- package/dist/cjs/query/live/collection-subscriber.cjs +43 -104
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.d.cts +4 -7
- package/dist/cjs/query/live-query-collection.cjs +2 -2
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.d.cts +1 -1
- package/dist/cjs/transactions.cjs +3 -3
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/types.d.cts +12 -10
- package/dist/cjs/utils/browser-polyfills.cjs +22 -0
- package/dist/cjs/utils/browser-polyfills.cjs.map +1 -0
- package/dist/cjs/utils/browser-polyfills.d.cts +9 -0
- package/dist/{cjs/change-events.d.cts → esm/collection/change-events.d.ts} +6 -6
- package/dist/esm/{change-events.js → collection/change-events.js} +13 -42
- package/dist/esm/collection/change-events.js.map +1 -0
- package/dist/esm/collection/changes.d.ts +53 -0
- package/dist/esm/collection/changes.js +108 -0
- package/dist/esm/collection/changes.js.map +1 -0
- package/dist/esm/{collection-events.d.ts → collection/events.d.ts} +7 -4
- package/dist/esm/{collection-events.js → collection/events.js} +7 -5
- package/dist/esm/collection/events.js.map +1 -0
- package/dist/{cjs/collection.d.cts → esm/collection/index.d.ts} +46 -184
- package/dist/esm/collection/index.js +417 -0
- package/dist/esm/collection/index.js.map +1 -0
- package/dist/esm/collection/indexes.d.ts +47 -0
- package/dist/esm/collection/indexes.js +124 -0
- package/dist/esm/collection/indexes.js.map +1 -0
- package/dist/esm/collection/lifecycle.d.ts +81 -0
- package/dist/esm/collection/lifecycle.js +196 -0
- package/dist/esm/collection/lifecycle.js.map +1 -0
- package/dist/esm/collection/mutations.d.ts +33 -0
- package/dist/esm/collection/mutations.js +315 -0
- package/dist/esm/collection/mutations.js.map +1 -0
- package/dist/esm/collection/state.d.ts +122 -0
- package/dist/esm/collection/state.js +597 -0
- package/dist/esm/collection/state.js.map +1 -0
- package/dist/esm/collection/subscription.d.ts +57 -0
- package/dist/esm/collection/subscription.js +160 -0
- package/dist/esm/collection/subscription.js.map +1 -0
- package/dist/esm/collection/sync.d.ts +34 -0
- package/dist/esm/collection/sync.js +154 -0
- package/dist/esm/collection/sync.js.map +1 -0
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/indexes/auto-index.d.ts +1 -1
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/indexes/base-index.d.ts +2 -2
- package/dist/esm/indexes/base-index.js.map +1 -1
- package/dist/esm/indexes/btree-index.d.ts +1 -1
- package/dist/esm/indexes/btree-index.js +2 -2
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/proxy.js +1 -1
- package/dist/esm/query/builder/index.js +1 -1
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +1 -1
- package/dist/esm/query/compiler/index.d.ts +3 -2
- package/dist/esm/query/compiler/index.js +5 -2
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +3 -2
- package/dist/esm/query/compiler/joins.js +22 -24
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +1 -1
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/ir.d.ts +1 -1
- package/dist/esm/query/ir.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.d.ts +3 -0
- package/dist/esm/query/live/collection-config-builder.js +29 -12
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-subscriber.d.ts +4 -7
- package/dist/esm/query/live/collection-subscriber.js +43 -104
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/query/live-query-collection.d.ts +1 -1
- package/dist/esm/query/live-query-collection.js +1 -1
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/transactions.js +3 -3
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +12 -10
- 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 +2 -2
- package/src/{change-events.ts → collection/change-events.ts} +25 -39
- package/src/collection/changes.ts +163 -0
- package/src/{collection-events.ts → collection/events.ts} +8 -6
- package/src/collection/index.ts +808 -0
- package/src/collection/indexes.ts +172 -0
- package/src/collection/lifecycle.ts +289 -0
- package/src/collection/mutations.ts +535 -0
- package/src/collection/state.ts +866 -0
- package/src/collection/subscription.ts +239 -0
- package/src/collection/sync.ts +235 -0
- package/src/index.ts +2 -2
- package/src/indexes/auto-index.ts +1 -1
- package/src/indexes/base-index.ts +3 -3
- package/src/indexes/btree-index.ts +2 -2
- package/src/query/builder/index.ts +1 -1
- package/src/query/builder/types.ts +1 -1
- package/src/query/compiler/index.ts +7 -1
- package/src/query/compiler/joins.ts +28 -41
- package/src/query/compiler/order-by.ts +1 -1
- package/src/query/ir.ts +1 -1
- package/src/query/live/collection-config-builder.ts +48 -22
- package/src/query/live/collection-subscriber.ts +63 -168
- package/src/query/live-query-collection.ts +2 -2
- package/src/transactions.ts +3 -3
- package/src/types.ts +14 -15
- package/src/utils/browser-polyfills.ts +39 -0
- package/dist/cjs/change-events.cjs.map +0 -1
- package/dist/cjs/collection-events.cjs.map +0 -1
- package/dist/cjs/collection.cjs +0 -1625
- package/dist/cjs/collection.cjs.map +0 -1
- package/dist/esm/change-events.js.map +0 -1
- package/dist/esm/collection-events.js.map +0 -1
- package/dist/esm/collection.js +0 -1625
- package/dist/esm/collection.js.map +0 -1
- package/src/collection.ts +0 -2564
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import { CollectionConfig, CollectionStatus } from '../types.js';
|
|
3
|
+
import { CollectionEventsManager } from './events.js';
|
|
4
|
+
import { CollectionIndexesManager } from './indexes.js';
|
|
5
|
+
import { CollectionChangesManager } from './changes.js';
|
|
6
|
+
import { CollectionSyncManager } from './sync.js';
|
|
7
|
+
import { CollectionStateManager } from './state.js';
|
|
8
|
+
export declare class CollectionLifecycleManager<TOutput extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = StandardSchemaV1, TInput extends object = TOutput> {
|
|
9
|
+
private config;
|
|
10
|
+
private id;
|
|
11
|
+
private indexes;
|
|
12
|
+
private events;
|
|
13
|
+
private changes;
|
|
14
|
+
private sync;
|
|
15
|
+
private state;
|
|
16
|
+
status: CollectionStatus;
|
|
17
|
+
hasBeenReady: boolean;
|
|
18
|
+
hasReceivedFirstCommit: boolean;
|
|
19
|
+
onFirstReadyCallbacks: Array<() => void>;
|
|
20
|
+
gcTimeoutId: ReturnType<typeof setTimeout> | null;
|
|
21
|
+
private idleCallbackId;
|
|
22
|
+
/**
|
|
23
|
+
* Creates a new CollectionLifecycleManager instance
|
|
24
|
+
*/
|
|
25
|
+
constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string);
|
|
26
|
+
setDeps(deps: {
|
|
27
|
+
indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>;
|
|
28
|
+
events: CollectionEventsManager;
|
|
29
|
+
changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>;
|
|
30
|
+
sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>;
|
|
31
|
+
state: CollectionStateManager<TOutput, TKey, TSchema, TInput>;
|
|
32
|
+
}): void;
|
|
33
|
+
/**
|
|
34
|
+
* Validates state transitions to prevent invalid status changes
|
|
35
|
+
*/
|
|
36
|
+
validateStatusTransition(from: CollectionStatus, to: CollectionStatus): void;
|
|
37
|
+
/**
|
|
38
|
+
* Safely update the collection status with validation
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
41
|
+
setStatus(newStatus: CollectionStatus): void;
|
|
42
|
+
/**
|
|
43
|
+
* Validates that the collection is in a usable state for data operations
|
|
44
|
+
* @private
|
|
45
|
+
*/
|
|
46
|
+
validateCollectionUsable(operation: string): void;
|
|
47
|
+
/**
|
|
48
|
+
* Mark the collection as ready for use
|
|
49
|
+
* This is called by sync implementations to explicitly signal that the collection is ready,
|
|
50
|
+
* providing a more intuitive alternative to using commits for readiness signaling
|
|
51
|
+
* @private - Should only be called by sync implementations
|
|
52
|
+
*/
|
|
53
|
+
markReady(): void;
|
|
54
|
+
/**
|
|
55
|
+
* Start the garbage collection timer
|
|
56
|
+
* Called when the collection becomes inactive (no subscribers)
|
|
57
|
+
*/
|
|
58
|
+
startGCTimer(): void;
|
|
59
|
+
/**
|
|
60
|
+
* Cancel the garbage collection timer
|
|
61
|
+
* Called when the collection becomes active again
|
|
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;
|
|
74
|
+
/**
|
|
75
|
+
* Register a callback to be executed when the collection first becomes ready
|
|
76
|
+
* Useful for preloading collections
|
|
77
|
+
* @param callback Function to call when the collection first becomes ready
|
|
78
|
+
*/
|
|
79
|
+
onFirstReady(callback: () => void): void;
|
|
80
|
+
cleanup(): void;
|
|
81
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { InvalidCollectionStatusTransitionError, CollectionInErrorStateError } from "../errors.js";
|
|
2
|
+
import { safeCancelIdleCallback, safeRequestIdleCallback } from "../utils/browser-polyfills.js";
|
|
3
|
+
class CollectionLifecycleManager {
|
|
4
|
+
/**
|
|
5
|
+
* Creates a new CollectionLifecycleManager instance
|
|
6
|
+
*/
|
|
7
|
+
constructor(config, id) {
|
|
8
|
+
this.status = `idle`;
|
|
9
|
+
this.hasBeenReady = false;
|
|
10
|
+
this.hasReceivedFirstCommit = false;
|
|
11
|
+
this.onFirstReadyCallbacks = [];
|
|
12
|
+
this.gcTimeoutId = null;
|
|
13
|
+
this.idleCallbackId = null;
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.id = id;
|
|
16
|
+
}
|
|
17
|
+
setDeps(deps) {
|
|
18
|
+
this.indexes = deps.indexes;
|
|
19
|
+
this.events = deps.events;
|
|
20
|
+
this.changes = deps.changes;
|
|
21
|
+
this.sync = deps.sync;
|
|
22
|
+
this.state = deps.state;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Validates state transitions to prevent invalid status changes
|
|
26
|
+
*/
|
|
27
|
+
validateStatusTransition(from, to) {
|
|
28
|
+
if (from === to) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const validTransitions = {
|
|
32
|
+
idle: [`loading`, `error`, `cleaned-up`],
|
|
33
|
+
loading: [`initialCommit`, `ready`, `error`, `cleaned-up`],
|
|
34
|
+
initialCommit: [`ready`, `error`, `cleaned-up`],
|
|
35
|
+
ready: [`cleaned-up`, `error`],
|
|
36
|
+
error: [`cleaned-up`, `idle`],
|
|
37
|
+
"cleaned-up": [`loading`, `error`]
|
|
38
|
+
};
|
|
39
|
+
if (!validTransitions[from].includes(to)) {
|
|
40
|
+
throw new InvalidCollectionStatusTransitionError(from, to, this.id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Safely update the collection status with validation
|
|
45
|
+
* @private
|
|
46
|
+
*/
|
|
47
|
+
setStatus(newStatus) {
|
|
48
|
+
this.validateStatusTransition(this.status, newStatus);
|
|
49
|
+
const previousStatus = this.status;
|
|
50
|
+
this.status = newStatus;
|
|
51
|
+
if (newStatus === `ready` && !this.indexes.isIndexesResolved) {
|
|
52
|
+
this.indexes.resolveAllIndexes().catch((error) => {
|
|
53
|
+
console.warn(`Failed to resolve indexes:`, error);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
this.events.emitStatusChange(newStatus, previousStatus);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Validates that the collection is in a usable state for data operations
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
62
|
+
validateCollectionUsable(operation) {
|
|
63
|
+
switch (this.status) {
|
|
64
|
+
case `error`:
|
|
65
|
+
throw new CollectionInErrorStateError(operation, this.id);
|
|
66
|
+
case `cleaned-up`:
|
|
67
|
+
this.sync.startSync();
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Mark the collection as ready for use
|
|
73
|
+
* This is called by sync implementations to explicitly signal that the collection is ready,
|
|
74
|
+
* providing a more intuitive alternative to using commits for readiness signaling
|
|
75
|
+
* @private - Should only be called by sync implementations
|
|
76
|
+
*/
|
|
77
|
+
markReady() {
|
|
78
|
+
if (this.status === `loading` || this.status === `initialCommit`) {
|
|
79
|
+
this.setStatus(`ready`);
|
|
80
|
+
if (!this.hasBeenReady) {
|
|
81
|
+
this.hasBeenReady = true;
|
|
82
|
+
if (!this.hasReceivedFirstCommit) {
|
|
83
|
+
this.hasReceivedFirstCommit = true;
|
|
84
|
+
}
|
|
85
|
+
const callbacks = [...this.onFirstReadyCallbacks];
|
|
86
|
+
this.onFirstReadyCallbacks = [];
|
|
87
|
+
callbacks.forEach((callback) => callback());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (this.changes.changeSubscriptions.size > 0) {
|
|
91
|
+
this.changes.emitEmptyReadyEvent();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Start the garbage collection timer
|
|
96
|
+
* Called when the collection becomes inactive (no subscribers)
|
|
97
|
+
*/
|
|
98
|
+
startGCTimer() {
|
|
99
|
+
if (this.gcTimeoutId) {
|
|
100
|
+
clearTimeout(this.gcTimeoutId);
|
|
101
|
+
}
|
|
102
|
+
const gcTime = this.config.gcTime ?? 3e5;
|
|
103
|
+
if (gcTime === 0) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.gcTimeoutId = setTimeout(() => {
|
|
107
|
+
if (this.changes.activeSubscribersCount === 0) {
|
|
108
|
+
this.scheduleIdleCleanup();
|
|
109
|
+
}
|
|
110
|
+
}, gcTime);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Cancel the garbage collection timer
|
|
114
|
+
* Called when the collection becomes active again
|
|
115
|
+
*/
|
|
116
|
+
cancelGCTimer() {
|
|
117
|
+
if (this.gcTimeoutId) {
|
|
118
|
+
clearTimeout(this.gcTimeoutId);
|
|
119
|
+
this.gcTimeoutId = null;
|
|
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
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Register a callback to be executed when the collection first becomes ready
|
|
175
|
+
* Useful for preloading collections
|
|
176
|
+
* @param callback Function to call when the collection first becomes ready
|
|
177
|
+
*/
|
|
178
|
+
onFirstReady(callback) {
|
|
179
|
+
if (this.hasBeenReady) {
|
|
180
|
+
callback();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
this.onFirstReadyCallbacks.push(callback);
|
|
184
|
+
}
|
|
185
|
+
cleanup() {
|
|
186
|
+
if (this.idleCallbackId !== null) {
|
|
187
|
+
safeCancelIdleCallback(this.idleCallbackId);
|
|
188
|
+
this.idleCallbackId = null;
|
|
189
|
+
}
|
|
190
|
+
this.performCleanup();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export {
|
|
194
|
+
CollectionLifecycleManager
|
|
195
|
+
};
|
|
196
|
+
//# sourceMappingURL=lifecycle.js.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CollectionImpl } from './index.js';
|
|
2
|
+
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
3
|
+
import { CollectionConfig, InsertConfig, OperationConfig, Transaction as TransactionType, UtilsRecord, WritableDeep } from '../types.js';
|
|
4
|
+
import { CollectionLifecycleManager } from './lifecycle.js';
|
|
5
|
+
import { CollectionStateManager } from './state.js';
|
|
6
|
+
export declare class CollectionMutationsManager<TOutput extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = {}, TSchema extends StandardSchemaV1 = StandardSchemaV1, TInput extends object = TOutput> {
|
|
7
|
+
private lifecycle;
|
|
8
|
+
private state;
|
|
9
|
+
private collection;
|
|
10
|
+
private config;
|
|
11
|
+
private id;
|
|
12
|
+
constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string);
|
|
13
|
+
setDeps(deps: {
|
|
14
|
+
lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>;
|
|
15
|
+
state: CollectionStateManager<TOutput, TKey, TSchema, TInput>;
|
|
16
|
+
collection: CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>;
|
|
17
|
+
}): void;
|
|
18
|
+
private ensureStandardSchema;
|
|
19
|
+
validateData(data: unknown, type: `insert` | `update`, key?: TKey): TOutput | never;
|
|
20
|
+
generateGlobalKey(key: any, item: any): string;
|
|
21
|
+
/**
|
|
22
|
+
* Inserts one or more items into the collection
|
|
23
|
+
*/
|
|
24
|
+
insert: (data: TInput | Array<TInput>, config?: InsertConfig) => TransactionType<Record<string, unknown>> | TransactionType<TOutput>;
|
|
25
|
+
/**
|
|
26
|
+
* Updates one or more items in the collection using a callback function
|
|
27
|
+
*/
|
|
28
|
+
update(keys: (TKey | unknown) | Array<TKey | unknown>, configOrCallback: ((draft: WritableDeep<TInput>) => void) | ((drafts: Array<WritableDeep<TInput>>) => void) | OperationConfig, maybeCallback?: ((draft: WritableDeep<TInput>) => void) | ((drafts: Array<WritableDeep<TInput>>) => void)): TransactionType<Record<string, unknown>> | TransactionType<TOutput>;
|
|
29
|
+
/**
|
|
30
|
+
* Deletes one or more items from the collection
|
|
31
|
+
*/
|
|
32
|
+
delete: (keys: Array<TKey> | TKey, config?: OperationConfig) => TransactionType<any>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { withArrayChangeTracking, withChangeTracking } from "../proxy.js";
|
|
2
|
+
import { getActiveTransaction, createTransaction } from "../transactions.js";
|
|
3
|
+
import { MissingInsertHandlerError, DuplicateKeyError, MissingDeleteHandlerError, NoKeysPassedToDeleteError, DeleteKeyNotFoundError, InvalidSchemaError, SchemaMustBeSynchronousError, SchemaValidationError, UndefinedKeyError, MissingUpdateArgumentError, MissingUpdateHandlerError, NoKeysPassedToUpdateError, UpdateKeyNotFoundError, KeyUpdateNotAllowedError } from "../errors.js";
|
|
4
|
+
class CollectionMutationsManager {
|
|
5
|
+
constructor(config, id) {
|
|
6
|
+
this.insert = (data, config2) => {
|
|
7
|
+
this.lifecycle.validateCollectionUsable(`insert`);
|
|
8
|
+
const state = this.state;
|
|
9
|
+
const ambientTransaction = getActiveTransaction();
|
|
10
|
+
if (!ambientTransaction && !this.config.onInsert) {
|
|
11
|
+
throw new MissingInsertHandlerError();
|
|
12
|
+
}
|
|
13
|
+
const items = Array.isArray(data) ? data : [data];
|
|
14
|
+
const mutations = [];
|
|
15
|
+
items.forEach((item) => {
|
|
16
|
+
var _a, _b;
|
|
17
|
+
const validatedData = this.validateData(item, `insert`);
|
|
18
|
+
const key = this.config.getKey(validatedData);
|
|
19
|
+
if (this.state.has(key)) {
|
|
20
|
+
throw new DuplicateKeyError(key);
|
|
21
|
+
}
|
|
22
|
+
const globalKey = this.generateGlobalKey(key, item);
|
|
23
|
+
const mutation = {
|
|
24
|
+
mutationId: crypto.randomUUID(),
|
|
25
|
+
original: {},
|
|
26
|
+
modified: validatedData,
|
|
27
|
+
// Pick the values from validatedData based on what's passed in - this is for cases
|
|
28
|
+
// where a schema has default values. The validated data has the extra default
|
|
29
|
+
// values but for changes, we just want to show the data that was actually passed in.
|
|
30
|
+
changes: Object.fromEntries(
|
|
31
|
+
Object.keys(item).map((k) => [
|
|
32
|
+
k,
|
|
33
|
+
validatedData[k]
|
|
34
|
+
])
|
|
35
|
+
),
|
|
36
|
+
globalKey,
|
|
37
|
+
key,
|
|
38
|
+
metadata: config2 == null ? void 0 : config2.metadata,
|
|
39
|
+
syncMetadata: ((_b = (_a = this.config.sync).getSyncMetadata) == null ? void 0 : _b.call(_a)) || {},
|
|
40
|
+
optimistic: (config2 == null ? void 0 : config2.optimistic) ?? true,
|
|
41
|
+
type: `insert`,
|
|
42
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
43
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
44
|
+
collection: this.collection
|
|
45
|
+
};
|
|
46
|
+
mutations.push(mutation);
|
|
47
|
+
});
|
|
48
|
+
if (ambientTransaction) {
|
|
49
|
+
ambientTransaction.applyMutations(mutations);
|
|
50
|
+
state.transactions.set(ambientTransaction.id, ambientTransaction);
|
|
51
|
+
state.scheduleTransactionCleanup(ambientTransaction);
|
|
52
|
+
state.recomputeOptimisticState(true);
|
|
53
|
+
return ambientTransaction;
|
|
54
|
+
} else {
|
|
55
|
+
const directOpTransaction = createTransaction({
|
|
56
|
+
mutationFn: async (params) => {
|
|
57
|
+
return await this.config.onInsert({
|
|
58
|
+
transaction: params.transaction,
|
|
59
|
+
collection: this.collection
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
directOpTransaction.applyMutations(mutations);
|
|
64
|
+
directOpTransaction.commit();
|
|
65
|
+
state.transactions.set(directOpTransaction.id, directOpTransaction);
|
|
66
|
+
state.scheduleTransactionCleanup(directOpTransaction);
|
|
67
|
+
state.recomputeOptimisticState(true);
|
|
68
|
+
return directOpTransaction;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
this.delete = (keys, config2) => {
|
|
72
|
+
const state = this.state;
|
|
73
|
+
this.lifecycle.validateCollectionUsable(`delete`);
|
|
74
|
+
const ambientTransaction = getActiveTransaction();
|
|
75
|
+
if (!ambientTransaction && !this.config.onDelete) {
|
|
76
|
+
throw new MissingDeleteHandlerError();
|
|
77
|
+
}
|
|
78
|
+
if (Array.isArray(keys) && keys.length === 0) {
|
|
79
|
+
throw new NoKeysPassedToDeleteError();
|
|
80
|
+
}
|
|
81
|
+
const keysArray = Array.isArray(keys) ? keys : [keys];
|
|
82
|
+
const mutations = [];
|
|
83
|
+
for (const key of keysArray) {
|
|
84
|
+
if (!this.state.has(key)) {
|
|
85
|
+
throw new DeleteKeyNotFoundError(key);
|
|
86
|
+
}
|
|
87
|
+
const globalKey = this.generateGlobalKey(key, this.state.get(key));
|
|
88
|
+
const mutation = {
|
|
89
|
+
mutationId: crypto.randomUUID(),
|
|
90
|
+
original: this.state.get(key),
|
|
91
|
+
modified: this.state.get(key),
|
|
92
|
+
changes: this.state.get(key),
|
|
93
|
+
globalKey,
|
|
94
|
+
key,
|
|
95
|
+
metadata: config2 == null ? void 0 : config2.metadata,
|
|
96
|
+
syncMetadata: state.syncedMetadata.get(key) || {},
|
|
97
|
+
optimistic: (config2 == null ? void 0 : config2.optimistic) ?? true,
|
|
98
|
+
type: `delete`,
|
|
99
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
100
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
101
|
+
collection: this.collection
|
|
102
|
+
};
|
|
103
|
+
mutations.push(mutation);
|
|
104
|
+
}
|
|
105
|
+
if (ambientTransaction) {
|
|
106
|
+
ambientTransaction.applyMutations(mutations);
|
|
107
|
+
state.transactions.set(ambientTransaction.id, ambientTransaction);
|
|
108
|
+
state.scheduleTransactionCleanup(ambientTransaction);
|
|
109
|
+
state.recomputeOptimisticState(true);
|
|
110
|
+
return ambientTransaction;
|
|
111
|
+
}
|
|
112
|
+
const directOpTransaction = createTransaction({
|
|
113
|
+
autoCommit: true,
|
|
114
|
+
mutationFn: async (params) => {
|
|
115
|
+
return this.config.onDelete({
|
|
116
|
+
transaction: params.transaction,
|
|
117
|
+
collection: this.collection
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
directOpTransaction.applyMutations(mutations);
|
|
122
|
+
directOpTransaction.commit();
|
|
123
|
+
state.transactions.set(directOpTransaction.id, directOpTransaction);
|
|
124
|
+
state.scheduleTransactionCleanup(directOpTransaction);
|
|
125
|
+
state.recomputeOptimisticState(true);
|
|
126
|
+
return directOpTransaction;
|
|
127
|
+
};
|
|
128
|
+
this.id = id;
|
|
129
|
+
this.config = config;
|
|
130
|
+
}
|
|
131
|
+
setDeps(deps) {
|
|
132
|
+
this.lifecycle = deps.lifecycle;
|
|
133
|
+
this.state = deps.state;
|
|
134
|
+
this.collection = deps.collection;
|
|
135
|
+
}
|
|
136
|
+
ensureStandardSchema(schema) {
|
|
137
|
+
if (schema && `~standard` in schema) {
|
|
138
|
+
return schema;
|
|
139
|
+
}
|
|
140
|
+
throw new InvalidSchemaError();
|
|
141
|
+
}
|
|
142
|
+
validateData(data, type, key) {
|
|
143
|
+
if (!this.config.schema) return data;
|
|
144
|
+
const standardSchema = this.ensureStandardSchema(this.config.schema);
|
|
145
|
+
if (type === `update` && key) {
|
|
146
|
+
const existingData = this.state.get(key);
|
|
147
|
+
if (existingData && data && typeof data === `object` && typeof existingData === `object`) {
|
|
148
|
+
const mergedData = Object.assign({}, existingData, data);
|
|
149
|
+
const result2 = standardSchema[`~standard`].validate(mergedData);
|
|
150
|
+
if (result2 instanceof Promise) {
|
|
151
|
+
throw new SchemaMustBeSynchronousError();
|
|
152
|
+
}
|
|
153
|
+
if (`issues` in result2 && result2.issues) {
|
|
154
|
+
const typedIssues = result2.issues.map((issue) => {
|
|
155
|
+
var _a;
|
|
156
|
+
return {
|
|
157
|
+
message: issue.message,
|
|
158
|
+
path: (_a = issue.path) == null ? void 0 : _a.map((p) => String(p))
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
throw new SchemaValidationError(type, typedIssues);
|
|
162
|
+
}
|
|
163
|
+
const validatedMergedData = result2.value;
|
|
164
|
+
const modifiedKeys = Object.keys(data);
|
|
165
|
+
const extractedChanges = Object.fromEntries(
|
|
166
|
+
modifiedKeys.map((k) => [k, validatedMergedData[k]])
|
|
167
|
+
);
|
|
168
|
+
return extractedChanges;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const result = standardSchema[`~standard`].validate(data);
|
|
172
|
+
if (result instanceof Promise) {
|
|
173
|
+
throw new SchemaMustBeSynchronousError();
|
|
174
|
+
}
|
|
175
|
+
if (`issues` in result && result.issues) {
|
|
176
|
+
const typedIssues = result.issues.map((issue) => {
|
|
177
|
+
var _a;
|
|
178
|
+
return {
|
|
179
|
+
message: issue.message,
|
|
180
|
+
path: (_a = issue.path) == null ? void 0 : _a.map((p) => String(p))
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
throw new SchemaValidationError(type, typedIssues);
|
|
184
|
+
}
|
|
185
|
+
return result.value;
|
|
186
|
+
}
|
|
187
|
+
generateGlobalKey(key, item) {
|
|
188
|
+
if (typeof key === `undefined`) {
|
|
189
|
+
throw new UndefinedKeyError(item);
|
|
190
|
+
}
|
|
191
|
+
return `KEY::${this.id}/${key}`;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Updates one or more items in the collection using a callback function
|
|
195
|
+
*/
|
|
196
|
+
update(keys, configOrCallback, maybeCallback) {
|
|
197
|
+
if (typeof keys === `undefined`) {
|
|
198
|
+
throw new MissingUpdateArgumentError();
|
|
199
|
+
}
|
|
200
|
+
const state = this.state;
|
|
201
|
+
this.lifecycle.validateCollectionUsable(`update`);
|
|
202
|
+
const ambientTransaction = getActiveTransaction();
|
|
203
|
+
if (!ambientTransaction && !this.config.onUpdate) {
|
|
204
|
+
throw new MissingUpdateHandlerError();
|
|
205
|
+
}
|
|
206
|
+
const isArray = Array.isArray(keys);
|
|
207
|
+
const keysArray = isArray ? keys : [keys];
|
|
208
|
+
if (isArray && keysArray.length === 0) {
|
|
209
|
+
throw new NoKeysPassedToUpdateError();
|
|
210
|
+
}
|
|
211
|
+
const callback = typeof configOrCallback === `function` ? configOrCallback : maybeCallback;
|
|
212
|
+
const config = typeof configOrCallback === `function` ? {} : configOrCallback;
|
|
213
|
+
const currentObjects = keysArray.map((key) => {
|
|
214
|
+
const item = this.state.get(key);
|
|
215
|
+
if (!item) {
|
|
216
|
+
throw new UpdateKeyNotFoundError(key);
|
|
217
|
+
}
|
|
218
|
+
return item;
|
|
219
|
+
});
|
|
220
|
+
let changesArray;
|
|
221
|
+
if (isArray) {
|
|
222
|
+
changesArray = withArrayChangeTracking(
|
|
223
|
+
currentObjects,
|
|
224
|
+
callback
|
|
225
|
+
);
|
|
226
|
+
} else {
|
|
227
|
+
const result = withChangeTracking(
|
|
228
|
+
currentObjects[0],
|
|
229
|
+
callback
|
|
230
|
+
);
|
|
231
|
+
changesArray = [result];
|
|
232
|
+
}
|
|
233
|
+
const mutations = keysArray.map((key, index) => {
|
|
234
|
+
const itemChanges = changesArray[index];
|
|
235
|
+
if (!itemChanges || Object.keys(itemChanges).length === 0) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const originalItem = currentObjects[index];
|
|
239
|
+
const validatedUpdatePayload = this.validateData(
|
|
240
|
+
itemChanges,
|
|
241
|
+
`update`,
|
|
242
|
+
key
|
|
243
|
+
);
|
|
244
|
+
const modifiedItem = Object.assign(
|
|
245
|
+
{},
|
|
246
|
+
originalItem,
|
|
247
|
+
validatedUpdatePayload
|
|
248
|
+
);
|
|
249
|
+
const originalItemId = this.config.getKey(originalItem);
|
|
250
|
+
const modifiedItemId = this.config.getKey(modifiedItem);
|
|
251
|
+
if (originalItemId !== modifiedItemId) {
|
|
252
|
+
throw new KeyUpdateNotAllowedError(originalItemId, modifiedItemId);
|
|
253
|
+
}
|
|
254
|
+
const globalKey = this.generateGlobalKey(modifiedItemId, modifiedItem);
|
|
255
|
+
return {
|
|
256
|
+
mutationId: crypto.randomUUID(),
|
|
257
|
+
original: originalItem,
|
|
258
|
+
modified: modifiedItem,
|
|
259
|
+
// Pick the values from modifiedItem based on what's passed in - this is for cases
|
|
260
|
+
// where a schema has default values or transforms. The modified data has the extra
|
|
261
|
+
// default or transformed values but for changes, we just want to show the data that
|
|
262
|
+
// was actually passed in.
|
|
263
|
+
changes: Object.fromEntries(
|
|
264
|
+
Object.keys(itemChanges).map((k) => [
|
|
265
|
+
k,
|
|
266
|
+
modifiedItem[k]
|
|
267
|
+
])
|
|
268
|
+
),
|
|
269
|
+
globalKey,
|
|
270
|
+
key,
|
|
271
|
+
metadata: config.metadata,
|
|
272
|
+
syncMetadata: state.syncedMetadata.get(key) || {},
|
|
273
|
+
optimistic: config.optimistic ?? true,
|
|
274
|
+
type: `update`,
|
|
275
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
276
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
277
|
+
collection: this.collection
|
|
278
|
+
};
|
|
279
|
+
}).filter(Boolean);
|
|
280
|
+
if (mutations.length === 0) {
|
|
281
|
+
const emptyTransaction = createTransaction({
|
|
282
|
+
mutationFn: async () => {
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
emptyTransaction.commit();
|
|
286
|
+
state.scheduleTransactionCleanup(emptyTransaction);
|
|
287
|
+
return emptyTransaction;
|
|
288
|
+
}
|
|
289
|
+
if (ambientTransaction) {
|
|
290
|
+
ambientTransaction.applyMutations(mutations);
|
|
291
|
+
state.transactions.set(ambientTransaction.id, ambientTransaction);
|
|
292
|
+
state.scheduleTransactionCleanup(ambientTransaction);
|
|
293
|
+
state.recomputeOptimisticState(true);
|
|
294
|
+
return ambientTransaction;
|
|
295
|
+
}
|
|
296
|
+
const directOpTransaction = createTransaction({
|
|
297
|
+
mutationFn: async (params) => {
|
|
298
|
+
return this.config.onUpdate({
|
|
299
|
+
transaction: params.transaction,
|
|
300
|
+
collection: this.collection
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
directOpTransaction.applyMutations(mutations);
|
|
305
|
+
directOpTransaction.commit();
|
|
306
|
+
state.transactions.set(directOpTransaction.id, directOpTransaction);
|
|
307
|
+
state.scheduleTransactionCleanup(directOpTransaction);
|
|
308
|
+
state.recomputeOptimisticState(true);
|
|
309
|
+
return directOpTransaction;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
export {
|
|
313
|
+
CollectionMutationsManager
|
|
314
|
+
};
|
|
315
|
+
//# sourceMappingURL=mutations.js.map
|