@tanstack/db 0.3.1 → 0.4.0
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 +90 -0
- package/dist/cjs/collection/events.cjs.map +1 -0
- package/dist/cjs/collection/events.d.cts +53 -0
- 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} +56 -172
- 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 +150 -0
- package/dist/cjs/collection/lifecycle.cjs.map +1 -0
- package/dist/cjs/collection/lifecycle.d.cts +70 -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/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 +53 -0
- package/dist/esm/collection/events.js +90 -0
- package/dist/esm/collection/events.js.map +1 -0
- package/dist/{cjs/collection.d.cts → esm/collection/index.d.ts} +56 -172
- 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 +70 -0
- package/dist/esm/collection/lifecycle.js +150 -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/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 +171 -0
- package/src/collection/index.ts +808 -0
- package/src/collection/indexes.ts +172 -0
- package/src/collection/lifecycle.ts +221 -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/dist/cjs/change-events.cjs.map +0 -1
- package/dist/cjs/collection.cjs +0 -1580
- package/dist/cjs/collection.cjs.map +0 -1
- package/dist/esm/change-events.js.map +0 -1
- package/dist/esm/collection.js +0 -1580
- package/dist/esm/collection.js.map +0 -1
- package/src/collection.ts +0 -2488
package/dist/esm/collection.js
DELETED
|
@@ -1,1580 +0,0 @@
|
|
|
1
|
-
import { withArrayChangeTracking, withChangeTracking } from "./proxy.js";
|
|
2
|
-
import { deepEquals } from "./utils.js";
|
|
3
|
-
import { SortedMap } from "./SortedMap.js";
|
|
4
|
-
import { createSingleRowRefProxy, toExpression } from "./query/builder/ref-proxy.js";
|
|
5
|
-
import { BTreeIndex } from "./indexes/btree-index.js";
|
|
6
|
-
import { LazyIndexWrapper, IndexProxy } from "./indexes/lazy-index.js";
|
|
7
|
-
import { ensureIndexForExpression } from "./indexes/auto-index.js";
|
|
8
|
-
import { getActiveTransaction, createTransaction } from "./transactions.js";
|
|
9
|
-
import { MissingInsertHandlerError, DuplicateKeyError, MissingDeleteHandlerError, NoKeysPassedToDeleteError, DeleteKeyNotFoundError, CollectionRequiresConfigError, CollectionRequiresSyncConfigError, CollectionInErrorStateError, InvalidCollectionStatusTransitionError, NoPendingSyncTransactionWriteError, SyncTransactionAlreadyCommittedWriteError, NoPendingSyncTransactionCommitError, SyncTransactionAlreadyCommittedError, DuplicateKeySyncError, CollectionIsInErrorStateError, SyncCleanupError, NegativeActiveSubscribersError, InvalidSchemaError, UndefinedKeyError, SchemaMustBeSynchronousError, SchemaValidationError, MissingUpdateArgumentError, MissingUpdateHandlerError, NoKeysPassedToUpdateError, UpdateKeyNotFoundError, KeyUpdateNotAllowedError } from "./errors.js";
|
|
10
|
-
import { currentStateAsChanges, createFilteredCallback } from "./change-events.js";
|
|
11
|
-
function createCollection(options) {
|
|
12
|
-
const collection = new CollectionImpl(
|
|
13
|
-
options
|
|
14
|
-
);
|
|
15
|
-
if (options.utils) {
|
|
16
|
-
collection.utils = { ...options.utils };
|
|
17
|
-
} else {
|
|
18
|
-
collection.utils = {};
|
|
19
|
-
}
|
|
20
|
-
return collection;
|
|
21
|
-
}
|
|
22
|
-
class CollectionImpl {
|
|
23
|
-
/**
|
|
24
|
-
* Creates a new Collection instance
|
|
25
|
-
*
|
|
26
|
-
* @param config - Configuration object for the collection
|
|
27
|
-
* @throws Error if sync config is missing
|
|
28
|
-
*/
|
|
29
|
-
constructor(config) {
|
|
30
|
-
this.pendingSyncedTransactions = [];
|
|
31
|
-
this.syncedMetadata = /* @__PURE__ */ new Map();
|
|
32
|
-
this.optimisticUpserts = /* @__PURE__ */ new Map();
|
|
33
|
-
this.optimisticDeletes = /* @__PURE__ */ new Set();
|
|
34
|
-
this._size = 0;
|
|
35
|
-
this.lazyIndexes = /* @__PURE__ */ new Map();
|
|
36
|
-
this.resolvedIndexes = /* @__PURE__ */ new Map();
|
|
37
|
-
this.isIndexesResolved = false;
|
|
38
|
-
this.indexCounter = 0;
|
|
39
|
-
this.changeListeners = /* @__PURE__ */ new Set();
|
|
40
|
-
this.changeKeyListeners = /* @__PURE__ */ new Map();
|
|
41
|
-
this.utils = {};
|
|
42
|
-
this.syncedKeys = /* @__PURE__ */ new Set();
|
|
43
|
-
this.preSyncVisibleState = /* @__PURE__ */ new Map();
|
|
44
|
-
this.recentlySyncedKeys = /* @__PURE__ */ new Set();
|
|
45
|
-
this.hasReceivedFirstCommit = false;
|
|
46
|
-
this.isCommittingSyncTransactions = false;
|
|
47
|
-
this.onFirstReadyCallbacks = [];
|
|
48
|
-
this.hasBeenReady = false;
|
|
49
|
-
this.batchedEvents = [];
|
|
50
|
-
this.shouldBatchEvents = false;
|
|
51
|
-
this._status = `idle`;
|
|
52
|
-
this.activeSubscribersCount = 0;
|
|
53
|
-
this.gcTimeoutId = null;
|
|
54
|
-
this.preloadPromise = null;
|
|
55
|
-
this.syncCleanupFn = null;
|
|
56
|
-
this.id = ``;
|
|
57
|
-
this.commitPendingTransactions = () => {
|
|
58
|
-
let hasPersistingTransaction = false;
|
|
59
|
-
for (const transaction of this.transactions.values()) {
|
|
60
|
-
if (transaction.state === `persisting`) {
|
|
61
|
-
hasPersistingTransaction = true;
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
const {
|
|
66
|
-
committedSyncedTransactions,
|
|
67
|
-
uncommittedSyncedTransactions,
|
|
68
|
-
hasTruncateSync
|
|
69
|
-
} = this.pendingSyncedTransactions.reduce(
|
|
70
|
-
(acc, t) => {
|
|
71
|
-
if (t.committed) {
|
|
72
|
-
acc.committedSyncedTransactions.push(t);
|
|
73
|
-
if (t.truncate === true) {
|
|
74
|
-
acc.hasTruncateSync = true;
|
|
75
|
-
}
|
|
76
|
-
} else {
|
|
77
|
-
acc.uncommittedSyncedTransactions.push(t);
|
|
78
|
-
}
|
|
79
|
-
return acc;
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
committedSyncedTransactions: [],
|
|
83
|
-
uncommittedSyncedTransactions: [],
|
|
84
|
-
hasTruncateSync: false
|
|
85
|
-
}
|
|
86
|
-
);
|
|
87
|
-
if (!hasPersistingTransaction || hasTruncateSync) {
|
|
88
|
-
this.isCommittingSyncTransactions = true;
|
|
89
|
-
const changedKeys = /* @__PURE__ */ new Set();
|
|
90
|
-
for (const transaction of committedSyncedTransactions) {
|
|
91
|
-
for (const operation of transaction.operations) {
|
|
92
|
-
changedKeys.add(operation.key);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
let currentVisibleState = this.preSyncVisibleState;
|
|
96
|
-
if (currentVisibleState.size === 0) {
|
|
97
|
-
currentVisibleState = /* @__PURE__ */ new Map();
|
|
98
|
-
for (const key of changedKeys) {
|
|
99
|
-
const currentValue = this.get(key);
|
|
100
|
-
if (currentValue !== void 0) {
|
|
101
|
-
currentVisibleState.set(key, currentValue);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
const events = [];
|
|
106
|
-
const rowUpdateMode = this.config.sync.rowUpdateMode || `partial`;
|
|
107
|
-
for (const transaction of committedSyncedTransactions) {
|
|
108
|
-
if (transaction.truncate) {
|
|
109
|
-
for (const key of this.syncedData.keys()) {
|
|
110
|
-
if (this.optimisticDeletes.has(key)) continue;
|
|
111
|
-
const previousValue = this.optimisticUpserts.get(key) || this.syncedData.get(key);
|
|
112
|
-
if (previousValue !== void 0) {
|
|
113
|
-
events.push({ type: `delete`, key, value: previousValue });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
this.syncedData.clear();
|
|
117
|
-
this.syncedMetadata.clear();
|
|
118
|
-
this.syncedKeys.clear();
|
|
119
|
-
for (const key of changedKeys) {
|
|
120
|
-
currentVisibleState.delete(key);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
for (const operation of transaction.operations) {
|
|
124
|
-
const key = operation.key;
|
|
125
|
-
this.syncedKeys.add(key);
|
|
126
|
-
switch (operation.type) {
|
|
127
|
-
case `insert`:
|
|
128
|
-
this.syncedMetadata.set(key, operation.metadata);
|
|
129
|
-
break;
|
|
130
|
-
case `update`:
|
|
131
|
-
this.syncedMetadata.set(
|
|
132
|
-
key,
|
|
133
|
-
Object.assign(
|
|
134
|
-
{},
|
|
135
|
-
this.syncedMetadata.get(key),
|
|
136
|
-
operation.metadata
|
|
137
|
-
)
|
|
138
|
-
);
|
|
139
|
-
break;
|
|
140
|
-
case `delete`:
|
|
141
|
-
this.syncedMetadata.delete(key);
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
switch (operation.type) {
|
|
145
|
-
case `insert`:
|
|
146
|
-
this.syncedData.set(key, operation.value);
|
|
147
|
-
break;
|
|
148
|
-
case `update`: {
|
|
149
|
-
if (rowUpdateMode === `partial`) {
|
|
150
|
-
const updatedValue = Object.assign(
|
|
151
|
-
{},
|
|
152
|
-
this.syncedData.get(key),
|
|
153
|
-
operation.value
|
|
154
|
-
);
|
|
155
|
-
this.syncedData.set(key, updatedValue);
|
|
156
|
-
} else {
|
|
157
|
-
this.syncedData.set(key, operation.value);
|
|
158
|
-
}
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
case `delete`:
|
|
162
|
-
this.syncedData.delete(key);
|
|
163
|
-
break;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
if (hasTruncateSync) {
|
|
168
|
-
const syncedInsertedOrUpdatedKeys = /* @__PURE__ */ new Set();
|
|
169
|
-
for (const t of committedSyncedTransactions) {
|
|
170
|
-
for (const op of t.operations) {
|
|
171
|
-
if (op.type === `insert` || op.type === `update`) {
|
|
172
|
-
syncedInsertedOrUpdatedKeys.add(op.key);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
const reapplyUpserts = /* @__PURE__ */ new Map();
|
|
177
|
-
const reapplyDeletes = /* @__PURE__ */ new Set();
|
|
178
|
-
for (const tx of this.transactions.values()) {
|
|
179
|
-
if ([`completed`, `failed`].includes(tx.state)) continue;
|
|
180
|
-
for (const mutation of tx.mutations) {
|
|
181
|
-
if (mutation.collection !== this || !mutation.optimistic) continue;
|
|
182
|
-
const key = mutation.key;
|
|
183
|
-
switch (mutation.type) {
|
|
184
|
-
case `insert`:
|
|
185
|
-
reapplyUpserts.set(key, mutation.modified);
|
|
186
|
-
reapplyDeletes.delete(key);
|
|
187
|
-
break;
|
|
188
|
-
case `update`: {
|
|
189
|
-
const base = this.syncedData.get(key);
|
|
190
|
-
const next = base ? Object.assign({}, base, mutation.changes) : mutation.modified;
|
|
191
|
-
reapplyUpserts.set(key, next);
|
|
192
|
-
reapplyDeletes.delete(key);
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
case `delete`:
|
|
196
|
-
reapplyUpserts.delete(key);
|
|
197
|
-
reapplyDeletes.add(key);
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
for (const [key, value] of reapplyUpserts) {
|
|
203
|
-
if (reapplyDeletes.has(key)) continue;
|
|
204
|
-
if (syncedInsertedOrUpdatedKeys.has(key)) {
|
|
205
|
-
let foundInsert = false;
|
|
206
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
207
|
-
const evt = events[i];
|
|
208
|
-
if (evt.key === key && evt.type === `insert`) {
|
|
209
|
-
evt.value = value;
|
|
210
|
-
foundInsert = true;
|
|
211
|
-
break;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
if (!foundInsert) {
|
|
215
|
-
events.push({ type: `insert`, key, value });
|
|
216
|
-
}
|
|
217
|
-
} else {
|
|
218
|
-
events.push({ type: `insert`, key, value });
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (events.length > 0 && reapplyDeletes.size > 0) {
|
|
222
|
-
const filtered = [];
|
|
223
|
-
for (const evt of events) {
|
|
224
|
-
if (evt.type === `insert` && reapplyDeletes.has(evt.key)) {
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
filtered.push(evt);
|
|
228
|
-
}
|
|
229
|
-
events.length = 0;
|
|
230
|
-
events.push(...filtered);
|
|
231
|
-
}
|
|
232
|
-
if (!this.isReady()) {
|
|
233
|
-
this.setStatus(`ready`);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
this.optimisticUpserts.clear();
|
|
237
|
-
this.optimisticDeletes.clear();
|
|
238
|
-
this.isCommittingSyncTransactions = false;
|
|
239
|
-
for (const transaction of this.transactions.values()) {
|
|
240
|
-
if (![`completed`, `failed`].includes(transaction.state)) {
|
|
241
|
-
for (const mutation of transaction.mutations) {
|
|
242
|
-
if (mutation.collection === this && mutation.optimistic) {
|
|
243
|
-
switch (mutation.type) {
|
|
244
|
-
case `insert`:
|
|
245
|
-
case `update`:
|
|
246
|
-
this.optimisticUpserts.set(
|
|
247
|
-
mutation.key,
|
|
248
|
-
mutation.modified
|
|
249
|
-
);
|
|
250
|
-
this.optimisticDeletes.delete(mutation.key);
|
|
251
|
-
break;
|
|
252
|
-
case `delete`:
|
|
253
|
-
this.optimisticUpserts.delete(mutation.key);
|
|
254
|
-
this.optimisticDeletes.add(mutation.key);
|
|
255
|
-
break;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
const completedOptimisticOps = /* @__PURE__ */ new Map();
|
|
262
|
-
for (const transaction of this.transactions.values()) {
|
|
263
|
-
if (transaction.state === `completed`) {
|
|
264
|
-
for (const mutation of transaction.mutations) {
|
|
265
|
-
if (mutation.collection === this && changedKeys.has(mutation.key)) {
|
|
266
|
-
completedOptimisticOps.set(mutation.key, {
|
|
267
|
-
type: mutation.type,
|
|
268
|
-
value: mutation.modified
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
for (const key of changedKeys) {
|
|
275
|
-
const previousVisibleValue = currentVisibleState.get(key);
|
|
276
|
-
const newVisibleValue = this.get(key);
|
|
277
|
-
const completedOp = completedOptimisticOps.get(key);
|
|
278
|
-
const isRedundantSync = completedOp && newVisibleValue !== void 0 && deepEquals(completedOp.value, newVisibleValue);
|
|
279
|
-
if (!isRedundantSync) {
|
|
280
|
-
if (previousVisibleValue === void 0 && newVisibleValue !== void 0) {
|
|
281
|
-
events.push({
|
|
282
|
-
type: `insert`,
|
|
283
|
-
key,
|
|
284
|
-
value: newVisibleValue
|
|
285
|
-
});
|
|
286
|
-
} else if (previousVisibleValue !== void 0 && newVisibleValue === void 0) {
|
|
287
|
-
events.push({
|
|
288
|
-
type: `delete`,
|
|
289
|
-
key,
|
|
290
|
-
value: previousVisibleValue
|
|
291
|
-
});
|
|
292
|
-
} else if (previousVisibleValue !== void 0 && newVisibleValue !== void 0 && !deepEquals(previousVisibleValue, newVisibleValue)) {
|
|
293
|
-
events.push({
|
|
294
|
-
type: `update`,
|
|
295
|
-
key,
|
|
296
|
-
value: newVisibleValue,
|
|
297
|
-
previousValue: previousVisibleValue
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
this._size = this.calculateSize();
|
|
303
|
-
if (events.length > 0) {
|
|
304
|
-
this.updateIndexes(events);
|
|
305
|
-
}
|
|
306
|
-
this.emitEvents(events, true);
|
|
307
|
-
this.pendingSyncedTransactions = uncommittedSyncedTransactions;
|
|
308
|
-
this.preSyncVisibleState.clear();
|
|
309
|
-
Promise.resolve().then(() => {
|
|
310
|
-
this.recentlySyncedKeys.clear();
|
|
311
|
-
});
|
|
312
|
-
if (!this.hasReceivedFirstCommit) {
|
|
313
|
-
this.hasReceivedFirstCommit = true;
|
|
314
|
-
const callbacks = [...this.onFirstReadyCallbacks];
|
|
315
|
-
this.onFirstReadyCallbacks = [];
|
|
316
|
-
callbacks.forEach((callback) => callback());
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
this.insert = (data, config2) => {
|
|
321
|
-
this.validateCollectionUsable(`insert`);
|
|
322
|
-
const ambientTransaction = getActiveTransaction();
|
|
323
|
-
if (!ambientTransaction && !this.config.onInsert) {
|
|
324
|
-
throw new MissingInsertHandlerError();
|
|
325
|
-
}
|
|
326
|
-
const items = Array.isArray(data) ? data : [data];
|
|
327
|
-
const mutations = [];
|
|
328
|
-
items.forEach((item) => {
|
|
329
|
-
var _a, _b;
|
|
330
|
-
const validatedData = this.validateData(item, `insert`);
|
|
331
|
-
const key = this.getKeyFromItem(validatedData);
|
|
332
|
-
if (this.has(key)) {
|
|
333
|
-
throw new DuplicateKeyError(key);
|
|
334
|
-
}
|
|
335
|
-
const globalKey = this.generateGlobalKey(key, item);
|
|
336
|
-
const mutation = {
|
|
337
|
-
mutationId: crypto.randomUUID(),
|
|
338
|
-
original: {},
|
|
339
|
-
modified: validatedData,
|
|
340
|
-
// Pick the values from validatedData based on what's passed in - this is for cases
|
|
341
|
-
// where a schema has default values. The validated data has the extra default
|
|
342
|
-
// values but for changes, we just want to show the data that was actually passed in.
|
|
343
|
-
changes: Object.fromEntries(
|
|
344
|
-
Object.keys(item).map((k) => [
|
|
345
|
-
k,
|
|
346
|
-
validatedData[k]
|
|
347
|
-
])
|
|
348
|
-
),
|
|
349
|
-
globalKey,
|
|
350
|
-
key,
|
|
351
|
-
metadata: config2 == null ? void 0 : config2.metadata,
|
|
352
|
-
syncMetadata: ((_b = (_a = this.config.sync).getSyncMetadata) == null ? void 0 : _b.call(_a)) || {},
|
|
353
|
-
optimistic: (config2 == null ? void 0 : config2.optimistic) ?? true,
|
|
354
|
-
type: `insert`,
|
|
355
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
356
|
-
updatedAt: /* @__PURE__ */ new Date(),
|
|
357
|
-
collection: this
|
|
358
|
-
};
|
|
359
|
-
mutations.push(mutation);
|
|
360
|
-
});
|
|
361
|
-
if (ambientTransaction) {
|
|
362
|
-
ambientTransaction.applyMutations(mutations);
|
|
363
|
-
this.transactions.set(ambientTransaction.id, ambientTransaction);
|
|
364
|
-
this.scheduleTransactionCleanup(ambientTransaction);
|
|
365
|
-
this.recomputeOptimisticState(true);
|
|
366
|
-
return ambientTransaction;
|
|
367
|
-
} else {
|
|
368
|
-
const directOpTransaction = createTransaction({
|
|
369
|
-
mutationFn: async (params) => {
|
|
370
|
-
return await this.config.onInsert({
|
|
371
|
-
transaction: params.transaction,
|
|
372
|
-
collection: this
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
directOpTransaction.applyMutations(mutations);
|
|
377
|
-
directOpTransaction.commit();
|
|
378
|
-
this.transactions.set(directOpTransaction.id, directOpTransaction);
|
|
379
|
-
this.scheduleTransactionCleanup(directOpTransaction);
|
|
380
|
-
this.recomputeOptimisticState(true);
|
|
381
|
-
return directOpTransaction;
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
this.delete = (keys, config2) => {
|
|
385
|
-
this.validateCollectionUsable(`delete`);
|
|
386
|
-
const ambientTransaction = getActiveTransaction();
|
|
387
|
-
if (!ambientTransaction && !this.config.onDelete) {
|
|
388
|
-
throw new MissingDeleteHandlerError();
|
|
389
|
-
}
|
|
390
|
-
if (Array.isArray(keys) && keys.length === 0) {
|
|
391
|
-
throw new NoKeysPassedToDeleteError();
|
|
392
|
-
}
|
|
393
|
-
const keysArray = Array.isArray(keys) ? keys : [keys];
|
|
394
|
-
const mutations = [];
|
|
395
|
-
for (const key of keysArray) {
|
|
396
|
-
if (!this.has(key)) {
|
|
397
|
-
throw new DeleteKeyNotFoundError(key);
|
|
398
|
-
}
|
|
399
|
-
const globalKey = this.generateGlobalKey(key, this.get(key));
|
|
400
|
-
const mutation = {
|
|
401
|
-
mutationId: crypto.randomUUID(),
|
|
402
|
-
original: this.get(key),
|
|
403
|
-
modified: this.get(key),
|
|
404
|
-
changes: this.get(key),
|
|
405
|
-
globalKey,
|
|
406
|
-
key,
|
|
407
|
-
metadata: config2 == null ? void 0 : config2.metadata,
|
|
408
|
-
syncMetadata: this.syncedMetadata.get(key) || {},
|
|
409
|
-
optimistic: (config2 == null ? void 0 : config2.optimistic) ?? true,
|
|
410
|
-
type: `delete`,
|
|
411
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
412
|
-
updatedAt: /* @__PURE__ */ new Date(),
|
|
413
|
-
collection: this
|
|
414
|
-
};
|
|
415
|
-
mutations.push(mutation);
|
|
416
|
-
}
|
|
417
|
-
if (ambientTransaction) {
|
|
418
|
-
ambientTransaction.applyMutations(mutations);
|
|
419
|
-
this.transactions.set(ambientTransaction.id, ambientTransaction);
|
|
420
|
-
this.scheduleTransactionCleanup(ambientTransaction);
|
|
421
|
-
this.recomputeOptimisticState(true);
|
|
422
|
-
return ambientTransaction;
|
|
423
|
-
}
|
|
424
|
-
const directOpTransaction = createTransaction({
|
|
425
|
-
autoCommit: true,
|
|
426
|
-
mutationFn: async (params) => {
|
|
427
|
-
return this.config.onDelete({
|
|
428
|
-
transaction: params.transaction,
|
|
429
|
-
collection: this
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
directOpTransaction.applyMutations(mutations);
|
|
434
|
-
directOpTransaction.commit();
|
|
435
|
-
this.transactions.set(directOpTransaction.id, directOpTransaction);
|
|
436
|
-
this.scheduleTransactionCleanup(directOpTransaction);
|
|
437
|
-
this.recomputeOptimisticState(true);
|
|
438
|
-
return directOpTransaction;
|
|
439
|
-
};
|
|
440
|
-
if (!config) {
|
|
441
|
-
throw new CollectionRequiresConfigError();
|
|
442
|
-
}
|
|
443
|
-
if (config.id) {
|
|
444
|
-
this.id = config.id;
|
|
445
|
-
} else {
|
|
446
|
-
this.id = crypto.randomUUID();
|
|
447
|
-
}
|
|
448
|
-
if (!config.sync) {
|
|
449
|
-
throw new CollectionRequiresSyncConfigError();
|
|
450
|
-
}
|
|
451
|
-
this.transactions = new SortedMap(
|
|
452
|
-
(a, b) => a.compareCreatedAt(b)
|
|
453
|
-
);
|
|
454
|
-
this.config = {
|
|
455
|
-
...config,
|
|
456
|
-
autoIndex: config.autoIndex ?? `eager`
|
|
457
|
-
};
|
|
458
|
-
if (this.config.compare) {
|
|
459
|
-
this.syncedData = new SortedMap(this.config.compare);
|
|
460
|
-
} else {
|
|
461
|
-
this.syncedData = /* @__PURE__ */ new Map();
|
|
462
|
-
}
|
|
463
|
-
if (config.startSync === true) {
|
|
464
|
-
this.startSync();
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* Register a callback to be executed when the collection first becomes ready
|
|
469
|
-
* Useful for preloading collections
|
|
470
|
-
* @param callback Function to call when the collection first becomes ready
|
|
471
|
-
* @example
|
|
472
|
-
* collection.onFirstReady(() => {
|
|
473
|
-
* console.log('Collection is ready for the first time')
|
|
474
|
-
* // Safe to access collection.state now
|
|
475
|
-
* })
|
|
476
|
-
*/
|
|
477
|
-
onFirstReady(callback) {
|
|
478
|
-
if (this.hasBeenReady) {
|
|
479
|
-
callback();
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
this.onFirstReadyCallbacks.push(callback);
|
|
483
|
-
}
|
|
484
|
-
/**
|
|
485
|
-
* Check if the collection is ready for use
|
|
486
|
-
* Returns true if the collection has been marked as ready by its sync implementation
|
|
487
|
-
* @returns true if the collection is ready, false otherwise
|
|
488
|
-
* @example
|
|
489
|
-
* if (collection.isReady()) {
|
|
490
|
-
* console.log('Collection is ready, data is available')
|
|
491
|
-
* // Safe to access collection.state
|
|
492
|
-
* } else {
|
|
493
|
-
* console.log('Collection is still loading')
|
|
494
|
-
* }
|
|
495
|
-
*/
|
|
496
|
-
isReady() {
|
|
497
|
-
return this._status === `ready`;
|
|
498
|
-
}
|
|
499
|
-
/**
|
|
500
|
-
* Mark the collection as ready for use
|
|
501
|
-
* This is called by sync implementations to explicitly signal that the collection is ready,
|
|
502
|
-
* providing a more intuitive alternative to using commits for readiness signaling
|
|
503
|
-
* @private - Should only be called by sync implementations
|
|
504
|
-
*/
|
|
505
|
-
markReady() {
|
|
506
|
-
if (this._status === `loading` || this._status === `initialCommit`) {
|
|
507
|
-
this.setStatus(`ready`);
|
|
508
|
-
if (!this.hasBeenReady) {
|
|
509
|
-
this.hasBeenReady = true;
|
|
510
|
-
if (!this.hasReceivedFirstCommit) {
|
|
511
|
-
this.hasReceivedFirstCommit = true;
|
|
512
|
-
}
|
|
513
|
-
const callbacks = [...this.onFirstReadyCallbacks];
|
|
514
|
-
this.onFirstReadyCallbacks = [];
|
|
515
|
-
callbacks.forEach((callback) => callback());
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
if (this.changeListeners.size > 0) {
|
|
519
|
-
this.emitEmptyReadyEvent();
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
/**
|
|
523
|
-
* Gets the current status of the collection
|
|
524
|
-
*/
|
|
525
|
-
get status() {
|
|
526
|
-
return this._status;
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* Validates that the collection is in a usable state for data operations
|
|
530
|
-
* @private
|
|
531
|
-
*/
|
|
532
|
-
validateCollectionUsable(operation) {
|
|
533
|
-
switch (this._status) {
|
|
534
|
-
case `error`:
|
|
535
|
-
throw new CollectionInErrorStateError(operation, this.id);
|
|
536
|
-
case `cleaned-up`:
|
|
537
|
-
this.startSync();
|
|
538
|
-
break;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
/**
|
|
542
|
-
* Validates state transitions to prevent invalid status changes
|
|
543
|
-
* @private
|
|
544
|
-
*/
|
|
545
|
-
validateStatusTransition(from, to) {
|
|
546
|
-
if (from === to) {
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
const validTransitions = {
|
|
550
|
-
idle: [`loading`, `error`, `cleaned-up`],
|
|
551
|
-
loading: [`initialCommit`, `ready`, `error`, `cleaned-up`],
|
|
552
|
-
initialCommit: [`ready`, `error`, `cleaned-up`],
|
|
553
|
-
ready: [`cleaned-up`, `error`],
|
|
554
|
-
error: [`cleaned-up`, `idle`],
|
|
555
|
-
"cleaned-up": [`loading`, `error`]
|
|
556
|
-
};
|
|
557
|
-
if (!validTransitions[from].includes(to)) {
|
|
558
|
-
throw new InvalidCollectionStatusTransitionError(from, to, this.id);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Safely update the collection status with validation
|
|
563
|
-
* @private
|
|
564
|
-
*/
|
|
565
|
-
setStatus(newStatus) {
|
|
566
|
-
this.validateStatusTransition(this._status, newStatus);
|
|
567
|
-
this._status = newStatus;
|
|
568
|
-
if (newStatus === `ready` && !this.isIndexesResolved) {
|
|
569
|
-
this.resolveAllIndexes().catch((error) => {
|
|
570
|
-
console.warn(`Failed to resolve indexes:`, error);
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
/**
|
|
575
|
-
* Start sync immediately - internal method for compiled queries
|
|
576
|
-
* This bypasses lazy loading for special cases like live query results
|
|
577
|
-
*/
|
|
578
|
-
startSyncImmediate() {
|
|
579
|
-
this.startSync();
|
|
580
|
-
}
|
|
581
|
-
/**
|
|
582
|
-
* Start the sync process for this collection
|
|
583
|
-
* This is called when the collection is first accessed or preloaded
|
|
584
|
-
*/
|
|
585
|
-
startSync() {
|
|
586
|
-
if (this._status !== `idle` && this._status !== `cleaned-up`) {
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
this.setStatus(`loading`);
|
|
590
|
-
try {
|
|
591
|
-
const cleanupFn = this.config.sync.sync({
|
|
592
|
-
collection: this,
|
|
593
|
-
begin: () => {
|
|
594
|
-
this.pendingSyncedTransactions.push({
|
|
595
|
-
committed: false,
|
|
596
|
-
operations: [],
|
|
597
|
-
deletedKeys: /* @__PURE__ */ new Set()
|
|
598
|
-
});
|
|
599
|
-
},
|
|
600
|
-
write: (messageWithoutKey) => {
|
|
601
|
-
const pendingTransaction = this.pendingSyncedTransactions[this.pendingSyncedTransactions.length - 1];
|
|
602
|
-
if (!pendingTransaction) {
|
|
603
|
-
throw new NoPendingSyncTransactionWriteError();
|
|
604
|
-
}
|
|
605
|
-
if (pendingTransaction.committed) {
|
|
606
|
-
throw new SyncTransactionAlreadyCommittedWriteError();
|
|
607
|
-
}
|
|
608
|
-
const key = this.getKeyFromItem(messageWithoutKey.value);
|
|
609
|
-
if (messageWithoutKey.type === `insert`) {
|
|
610
|
-
const insertingIntoExistingSynced = this.syncedData.has(key);
|
|
611
|
-
const hasPendingDeleteForKey = pendingTransaction.deletedKeys.has(key);
|
|
612
|
-
const isTruncateTransaction = pendingTransaction.truncate === true;
|
|
613
|
-
if (insertingIntoExistingSynced && !hasPendingDeleteForKey && !isTruncateTransaction) {
|
|
614
|
-
throw new DuplicateKeySyncError(key, this.id);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
const message = {
|
|
618
|
-
...messageWithoutKey,
|
|
619
|
-
key
|
|
620
|
-
};
|
|
621
|
-
pendingTransaction.operations.push(message);
|
|
622
|
-
if (messageWithoutKey.type === `delete`) {
|
|
623
|
-
pendingTransaction.deletedKeys.add(key);
|
|
624
|
-
}
|
|
625
|
-
},
|
|
626
|
-
commit: () => {
|
|
627
|
-
const pendingTransaction = this.pendingSyncedTransactions[this.pendingSyncedTransactions.length - 1];
|
|
628
|
-
if (!pendingTransaction) {
|
|
629
|
-
throw new NoPendingSyncTransactionCommitError();
|
|
630
|
-
}
|
|
631
|
-
if (pendingTransaction.committed) {
|
|
632
|
-
throw new SyncTransactionAlreadyCommittedError();
|
|
633
|
-
}
|
|
634
|
-
pendingTransaction.committed = true;
|
|
635
|
-
if (this._status === `loading`) {
|
|
636
|
-
this.setStatus(`initialCommit`);
|
|
637
|
-
}
|
|
638
|
-
this.commitPendingTransactions();
|
|
639
|
-
},
|
|
640
|
-
markReady: () => {
|
|
641
|
-
this.markReady();
|
|
642
|
-
},
|
|
643
|
-
truncate: () => {
|
|
644
|
-
const pendingTransaction = this.pendingSyncedTransactions[this.pendingSyncedTransactions.length - 1];
|
|
645
|
-
if (!pendingTransaction) {
|
|
646
|
-
throw new NoPendingSyncTransactionWriteError();
|
|
647
|
-
}
|
|
648
|
-
if (pendingTransaction.committed) {
|
|
649
|
-
throw new SyncTransactionAlreadyCommittedWriteError();
|
|
650
|
-
}
|
|
651
|
-
pendingTransaction.operations = [];
|
|
652
|
-
pendingTransaction.deletedKeys.clear();
|
|
653
|
-
pendingTransaction.truncate = true;
|
|
654
|
-
}
|
|
655
|
-
});
|
|
656
|
-
this.syncCleanupFn = typeof cleanupFn === `function` ? cleanupFn : null;
|
|
657
|
-
} catch (error) {
|
|
658
|
-
this.setStatus(`error`);
|
|
659
|
-
throw error;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
/**
|
|
663
|
-
* Preload the collection data by starting sync if not already started
|
|
664
|
-
* Multiple concurrent calls will share the same promise
|
|
665
|
-
*/
|
|
666
|
-
preload() {
|
|
667
|
-
if (this.preloadPromise) {
|
|
668
|
-
return this.preloadPromise;
|
|
669
|
-
}
|
|
670
|
-
this.preloadPromise = new Promise((resolve, reject) => {
|
|
671
|
-
if (this._status === `ready`) {
|
|
672
|
-
resolve();
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
if (this._status === `error`) {
|
|
676
|
-
reject(new CollectionIsInErrorStateError());
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
this.onFirstReady(() => {
|
|
680
|
-
resolve();
|
|
681
|
-
});
|
|
682
|
-
if (this._status === `idle` || this._status === `cleaned-up`) {
|
|
683
|
-
try {
|
|
684
|
-
this.startSync();
|
|
685
|
-
} catch (error) {
|
|
686
|
-
reject(error);
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
});
|
|
691
|
-
return this.preloadPromise;
|
|
692
|
-
}
|
|
693
|
-
/**
|
|
694
|
-
* Clean up the collection by stopping sync and clearing data
|
|
695
|
-
* This can be called manually or automatically by garbage collection
|
|
696
|
-
*/
|
|
697
|
-
async cleanup() {
|
|
698
|
-
if (this.gcTimeoutId) {
|
|
699
|
-
clearTimeout(this.gcTimeoutId);
|
|
700
|
-
this.gcTimeoutId = null;
|
|
701
|
-
}
|
|
702
|
-
try {
|
|
703
|
-
if (this.syncCleanupFn) {
|
|
704
|
-
this.syncCleanupFn();
|
|
705
|
-
this.syncCleanupFn = null;
|
|
706
|
-
}
|
|
707
|
-
} catch (error) {
|
|
708
|
-
queueMicrotask(() => {
|
|
709
|
-
if (error instanceof Error) {
|
|
710
|
-
const wrappedError = new SyncCleanupError(this.id, error);
|
|
711
|
-
wrappedError.cause = error;
|
|
712
|
-
wrappedError.stack = error.stack;
|
|
713
|
-
throw wrappedError;
|
|
714
|
-
} else {
|
|
715
|
-
throw new SyncCleanupError(this.id, error);
|
|
716
|
-
}
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
this.syncedData.clear();
|
|
720
|
-
this.syncedMetadata.clear();
|
|
721
|
-
this.optimisticUpserts.clear();
|
|
722
|
-
this.optimisticDeletes.clear();
|
|
723
|
-
this._size = 0;
|
|
724
|
-
this.pendingSyncedTransactions = [];
|
|
725
|
-
this.syncedKeys.clear();
|
|
726
|
-
this.hasReceivedFirstCommit = false;
|
|
727
|
-
this.hasBeenReady = false;
|
|
728
|
-
this.onFirstReadyCallbacks = [];
|
|
729
|
-
this.preloadPromise = null;
|
|
730
|
-
this.batchedEvents = [];
|
|
731
|
-
this.shouldBatchEvents = false;
|
|
732
|
-
this.setStatus(`cleaned-up`);
|
|
733
|
-
return Promise.resolve();
|
|
734
|
-
}
|
|
735
|
-
/**
|
|
736
|
-
* Start the garbage collection timer
|
|
737
|
-
* Called when the collection becomes inactive (no subscribers)
|
|
738
|
-
*/
|
|
739
|
-
startGCTimer() {
|
|
740
|
-
if (this.gcTimeoutId) {
|
|
741
|
-
clearTimeout(this.gcTimeoutId);
|
|
742
|
-
}
|
|
743
|
-
const gcTime = this.config.gcTime ?? 3e5;
|
|
744
|
-
if (gcTime === 0) {
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
this.gcTimeoutId = setTimeout(() => {
|
|
748
|
-
if (this.activeSubscribersCount === 0) {
|
|
749
|
-
this.cleanup();
|
|
750
|
-
}
|
|
751
|
-
}, gcTime);
|
|
752
|
-
}
|
|
753
|
-
/**
|
|
754
|
-
* Cancel the garbage collection timer
|
|
755
|
-
* Called when the collection becomes active again
|
|
756
|
-
*/
|
|
757
|
-
cancelGCTimer() {
|
|
758
|
-
if (this.gcTimeoutId) {
|
|
759
|
-
clearTimeout(this.gcTimeoutId);
|
|
760
|
-
this.gcTimeoutId = null;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
/**
|
|
764
|
-
* Increment the active subscribers count and start sync if needed
|
|
765
|
-
*/
|
|
766
|
-
addSubscriber() {
|
|
767
|
-
this.activeSubscribersCount++;
|
|
768
|
-
this.cancelGCTimer();
|
|
769
|
-
if (this._status === `cleaned-up` || this._status === `idle`) {
|
|
770
|
-
this.startSync();
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
/**
|
|
774
|
-
* Decrement the active subscribers count and start GC timer if needed
|
|
775
|
-
*/
|
|
776
|
-
removeSubscriber() {
|
|
777
|
-
this.activeSubscribersCount--;
|
|
778
|
-
if (this.activeSubscribersCount === 0) {
|
|
779
|
-
this.startGCTimer();
|
|
780
|
-
} else if (this.activeSubscribersCount < 0) {
|
|
781
|
-
throw new NegativeActiveSubscribersError();
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
/**
|
|
785
|
-
* Recompute optimistic state from active transactions
|
|
786
|
-
*/
|
|
787
|
-
recomputeOptimisticState(triggeredByUserAction = false) {
|
|
788
|
-
if (this.isCommittingSyncTransactions) {
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
const previousState = new Map(this.optimisticUpserts);
|
|
792
|
-
const previousDeletes = new Set(this.optimisticDeletes);
|
|
793
|
-
this.optimisticUpserts.clear();
|
|
794
|
-
this.optimisticDeletes.clear();
|
|
795
|
-
const activeTransactions = [];
|
|
796
|
-
for (const transaction of this.transactions.values()) {
|
|
797
|
-
if (![`completed`, `failed`].includes(transaction.state)) {
|
|
798
|
-
activeTransactions.push(transaction);
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
for (const transaction of activeTransactions) {
|
|
802
|
-
for (const mutation of transaction.mutations) {
|
|
803
|
-
if (mutation.collection === this && mutation.optimistic) {
|
|
804
|
-
switch (mutation.type) {
|
|
805
|
-
case `insert`:
|
|
806
|
-
case `update`:
|
|
807
|
-
this.optimisticUpserts.set(
|
|
808
|
-
mutation.key,
|
|
809
|
-
mutation.modified
|
|
810
|
-
);
|
|
811
|
-
this.optimisticDeletes.delete(mutation.key);
|
|
812
|
-
break;
|
|
813
|
-
case `delete`:
|
|
814
|
-
this.optimisticUpserts.delete(mutation.key);
|
|
815
|
-
this.optimisticDeletes.add(mutation.key);
|
|
816
|
-
break;
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
this._size = this.calculateSize();
|
|
822
|
-
const events = [];
|
|
823
|
-
this.collectOptimisticChanges(previousState, previousDeletes, events);
|
|
824
|
-
const filteredEventsBySyncStatus = events.filter((event) => {
|
|
825
|
-
if (!this.recentlySyncedKeys.has(event.key)) {
|
|
826
|
-
return true;
|
|
827
|
-
}
|
|
828
|
-
if (triggeredByUserAction) {
|
|
829
|
-
return true;
|
|
830
|
-
}
|
|
831
|
-
return false;
|
|
832
|
-
});
|
|
833
|
-
if (this.pendingSyncedTransactions.length > 0 && !triggeredByUserAction) {
|
|
834
|
-
const pendingSyncKeys = /* @__PURE__ */ new Set();
|
|
835
|
-
for (const transaction of this.pendingSyncedTransactions) {
|
|
836
|
-
for (const operation of transaction.operations) {
|
|
837
|
-
pendingSyncKeys.add(operation.key);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
const filteredEvents = filteredEventsBySyncStatus.filter((event) => {
|
|
841
|
-
if (event.type === `delete` && pendingSyncKeys.has(event.key)) {
|
|
842
|
-
const hasActiveOptimisticMutation = activeTransactions.some(
|
|
843
|
-
(tx) => tx.mutations.some(
|
|
844
|
-
(m) => m.collection === this && m.key === event.key
|
|
845
|
-
)
|
|
846
|
-
);
|
|
847
|
-
if (!hasActiveOptimisticMutation) {
|
|
848
|
-
return false;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
return true;
|
|
852
|
-
});
|
|
853
|
-
if (filteredEvents.length > 0) {
|
|
854
|
-
this.updateIndexes(filteredEvents);
|
|
855
|
-
}
|
|
856
|
-
this.emitEvents(filteredEvents, triggeredByUserAction);
|
|
857
|
-
} else {
|
|
858
|
-
if (filteredEventsBySyncStatus.length > 0) {
|
|
859
|
-
this.updateIndexes(filteredEventsBySyncStatus);
|
|
860
|
-
}
|
|
861
|
-
this.emitEvents(filteredEventsBySyncStatus, triggeredByUserAction);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
/**
|
|
865
|
-
* Calculate the current size based on synced data and optimistic changes
|
|
866
|
-
*/
|
|
867
|
-
calculateSize() {
|
|
868
|
-
const syncedSize = this.syncedData.size;
|
|
869
|
-
const deletesFromSynced = Array.from(this.optimisticDeletes).filter(
|
|
870
|
-
(key) => this.syncedData.has(key) && !this.optimisticUpserts.has(key)
|
|
871
|
-
).length;
|
|
872
|
-
const upsertsNotInSynced = Array.from(this.optimisticUpserts.keys()).filter(
|
|
873
|
-
(key) => !this.syncedData.has(key)
|
|
874
|
-
).length;
|
|
875
|
-
return syncedSize - deletesFromSynced + upsertsNotInSynced;
|
|
876
|
-
}
|
|
877
|
-
/**
|
|
878
|
-
* Collect events for optimistic changes
|
|
879
|
-
*/
|
|
880
|
-
collectOptimisticChanges(previousUpserts, previousDeletes, events) {
|
|
881
|
-
const allKeys = /* @__PURE__ */ new Set([
|
|
882
|
-
...previousUpserts.keys(),
|
|
883
|
-
...this.optimisticUpserts.keys(),
|
|
884
|
-
...previousDeletes,
|
|
885
|
-
...this.optimisticDeletes
|
|
886
|
-
]);
|
|
887
|
-
for (const key of allKeys) {
|
|
888
|
-
const currentValue = this.get(key);
|
|
889
|
-
const previousValue = this.getPreviousValue(
|
|
890
|
-
key,
|
|
891
|
-
previousUpserts,
|
|
892
|
-
previousDeletes
|
|
893
|
-
);
|
|
894
|
-
if (previousValue !== void 0 && currentValue === void 0) {
|
|
895
|
-
events.push({ type: `delete`, key, value: previousValue });
|
|
896
|
-
} else if (previousValue === void 0 && currentValue !== void 0) {
|
|
897
|
-
events.push({ type: `insert`, key, value: currentValue });
|
|
898
|
-
} else if (previousValue !== void 0 && currentValue !== void 0 && previousValue !== currentValue) {
|
|
899
|
-
events.push({
|
|
900
|
-
type: `update`,
|
|
901
|
-
key,
|
|
902
|
-
value: currentValue,
|
|
903
|
-
previousValue
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
/**
|
|
909
|
-
* Get the previous value for a key given previous optimistic state
|
|
910
|
-
*/
|
|
911
|
-
getPreviousValue(key, previousUpserts, previousDeletes) {
|
|
912
|
-
if (previousDeletes.has(key)) {
|
|
913
|
-
return void 0;
|
|
914
|
-
}
|
|
915
|
-
if (previousUpserts.has(key)) {
|
|
916
|
-
return previousUpserts.get(key);
|
|
917
|
-
}
|
|
918
|
-
return this.syncedData.get(key);
|
|
919
|
-
}
|
|
920
|
-
/**
|
|
921
|
-
* Emit an empty ready event to notify subscribers that the collection is ready
|
|
922
|
-
* This bypasses the normal empty array check in emitEvents
|
|
923
|
-
*/
|
|
924
|
-
emitEmptyReadyEvent() {
|
|
925
|
-
for (const listener of this.changeListeners) {
|
|
926
|
-
listener([]);
|
|
927
|
-
}
|
|
928
|
-
for (const [_key, keyListeners] of this.changeKeyListeners) {
|
|
929
|
-
for (const listener of keyListeners) {
|
|
930
|
-
listener([]);
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
/**
|
|
935
|
-
* Emit events either immediately or batch them for later emission
|
|
936
|
-
*/
|
|
937
|
-
emitEvents(changes, forceEmit = false) {
|
|
938
|
-
if (this.shouldBatchEvents && !forceEmit) {
|
|
939
|
-
this.batchedEvents.push(...changes);
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
let eventsToEmit = changes;
|
|
943
|
-
if (this.batchedEvents.length > 0 && forceEmit) {
|
|
944
|
-
eventsToEmit = [...this.batchedEvents, ...changes];
|
|
945
|
-
this.batchedEvents = [];
|
|
946
|
-
this.shouldBatchEvents = false;
|
|
947
|
-
}
|
|
948
|
-
if (eventsToEmit.length === 0) return;
|
|
949
|
-
for (const listener of this.changeListeners) {
|
|
950
|
-
listener(eventsToEmit);
|
|
951
|
-
}
|
|
952
|
-
if (this.changeKeyListeners.size > 0) {
|
|
953
|
-
const changesByKey = /* @__PURE__ */ new Map();
|
|
954
|
-
for (const change of eventsToEmit) {
|
|
955
|
-
if (this.changeKeyListeners.has(change.key)) {
|
|
956
|
-
if (!changesByKey.has(change.key)) {
|
|
957
|
-
changesByKey.set(change.key, []);
|
|
958
|
-
}
|
|
959
|
-
changesByKey.get(change.key).push(change);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
for (const [key, keyChanges] of changesByKey) {
|
|
963
|
-
const keyListeners = this.changeKeyListeners.get(key);
|
|
964
|
-
for (const listener of keyListeners) {
|
|
965
|
-
listener(keyChanges);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
/**
|
|
971
|
-
* Get the current value for a key (virtual derived state)
|
|
972
|
-
*/
|
|
973
|
-
get(key) {
|
|
974
|
-
if (this.optimisticDeletes.has(key)) {
|
|
975
|
-
return void 0;
|
|
976
|
-
}
|
|
977
|
-
if (this.optimisticUpserts.has(key)) {
|
|
978
|
-
return this.optimisticUpserts.get(key);
|
|
979
|
-
}
|
|
980
|
-
return this.syncedData.get(key);
|
|
981
|
-
}
|
|
982
|
-
/**
|
|
983
|
-
* Check if a key exists in the collection (virtual derived state)
|
|
984
|
-
*/
|
|
985
|
-
has(key) {
|
|
986
|
-
if (this.optimisticDeletes.has(key)) {
|
|
987
|
-
return false;
|
|
988
|
-
}
|
|
989
|
-
if (this.optimisticUpserts.has(key)) {
|
|
990
|
-
return true;
|
|
991
|
-
}
|
|
992
|
-
return this.syncedData.has(key);
|
|
993
|
-
}
|
|
994
|
-
/**
|
|
995
|
-
* Get the current size of the collection (cached)
|
|
996
|
-
*/
|
|
997
|
-
get size() {
|
|
998
|
-
return this._size;
|
|
999
|
-
}
|
|
1000
|
-
/**
|
|
1001
|
-
* Get all keys (virtual derived state)
|
|
1002
|
-
*/
|
|
1003
|
-
*keys() {
|
|
1004
|
-
for (const key of this.syncedData.keys()) {
|
|
1005
|
-
if (!this.optimisticDeletes.has(key)) {
|
|
1006
|
-
yield key;
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
for (const key of this.optimisticUpserts.keys()) {
|
|
1010
|
-
if (!this.syncedData.has(key) && !this.optimisticDeletes.has(key)) {
|
|
1011
|
-
yield key;
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
/**
|
|
1016
|
-
* Get all values (virtual derived state)
|
|
1017
|
-
*/
|
|
1018
|
-
*values() {
|
|
1019
|
-
for (const key of this.keys()) {
|
|
1020
|
-
const value = this.get(key);
|
|
1021
|
-
if (value !== void 0) {
|
|
1022
|
-
yield value;
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
/**
|
|
1027
|
-
* Get all entries (virtual derived state)
|
|
1028
|
-
*/
|
|
1029
|
-
*entries() {
|
|
1030
|
-
for (const key of this.keys()) {
|
|
1031
|
-
const value = this.get(key);
|
|
1032
|
-
if (value !== void 0) {
|
|
1033
|
-
yield [key, value];
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
/**
|
|
1038
|
-
* Get all entries (virtual derived state)
|
|
1039
|
-
*/
|
|
1040
|
-
*[Symbol.iterator]() {
|
|
1041
|
-
for (const [key, value] of this.entries()) {
|
|
1042
|
-
yield [key, value];
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
/**
|
|
1046
|
-
* Execute a callback for each entry in the collection
|
|
1047
|
-
*/
|
|
1048
|
-
forEach(callbackfn) {
|
|
1049
|
-
let index = 0;
|
|
1050
|
-
for (const [key, value] of this.entries()) {
|
|
1051
|
-
callbackfn(value, key, index++);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
/**
|
|
1055
|
-
* Create a new array with the results of calling a function for each entry in the collection
|
|
1056
|
-
*/
|
|
1057
|
-
map(callbackfn) {
|
|
1058
|
-
const result = [];
|
|
1059
|
-
let index = 0;
|
|
1060
|
-
for (const [key, value] of this.entries()) {
|
|
1061
|
-
result.push(callbackfn(value, key, index++));
|
|
1062
|
-
}
|
|
1063
|
-
return result;
|
|
1064
|
-
}
|
|
1065
|
-
/**
|
|
1066
|
-
* Schedule cleanup of a transaction when it completes
|
|
1067
|
-
* @private
|
|
1068
|
-
*/
|
|
1069
|
-
scheduleTransactionCleanup(transaction) {
|
|
1070
|
-
if (transaction.state === `completed`) {
|
|
1071
|
-
this.transactions.delete(transaction.id);
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
transaction.isPersisted.promise.then(() => {
|
|
1075
|
-
this.transactions.delete(transaction.id);
|
|
1076
|
-
}).catch(() => {
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1079
|
-
ensureStandardSchema(schema) {
|
|
1080
|
-
if (schema && `~standard` in schema) {
|
|
1081
|
-
return schema;
|
|
1082
|
-
}
|
|
1083
|
-
throw new InvalidSchemaError();
|
|
1084
|
-
}
|
|
1085
|
-
getKeyFromItem(item) {
|
|
1086
|
-
return this.config.getKey(item);
|
|
1087
|
-
}
|
|
1088
|
-
generateGlobalKey(key, item) {
|
|
1089
|
-
if (typeof key === `undefined`) {
|
|
1090
|
-
throw new UndefinedKeyError(item);
|
|
1091
|
-
}
|
|
1092
|
-
return `KEY::${this.id}/${key}`;
|
|
1093
|
-
}
|
|
1094
|
-
/**
|
|
1095
|
-
* Creates an index on a collection for faster queries.
|
|
1096
|
-
* Indexes significantly improve query performance by allowing constant time lookups
|
|
1097
|
-
* and logarithmic time range queries instead of full scans.
|
|
1098
|
-
*
|
|
1099
|
-
* @template TResolver - The type of the index resolver (constructor or async loader)
|
|
1100
|
-
* @param indexCallback - Function that extracts the indexed value from each item
|
|
1101
|
-
* @param config - Configuration including index type and type-specific options
|
|
1102
|
-
* @returns An index proxy that provides access to the index when ready
|
|
1103
|
-
*
|
|
1104
|
-
* @example
|
|
1105
|
-
* // Create a default B+ tree index
|
|
1106
|
-
* const ageIndex = collection.createIndex((row) => row.age)
|
|
1107
|
-
*
|
|
1108
|
-
* // Create a ordered index with custom options
|
|
1109
|
-
* const ageIndex = collection.createIndex((row) => row.age, {
|
|
1110
|
-
* indexType: BTreeIndex,
|
|
1111
|
-
* options: { compareFn: customComparator },
|
|
1112
|
-
* name: 'age_btree'
|
|
1113
|
-
* })
|
|
1114
|
-
*
|
|
1115
|
-
* // Create an async-loaded index
|
|
1116
|
-
* const textIndex = collection.createIndex((row) => row.content, {
|
|
1117
|
-
* indexType: async () => {
|
|
1118
|
-
* const { FullTextIndex } = await import('./indexes/fulltext.js')
|
|
1119
|
-
* return FullTextIndex
|
|
1120
|
-
* },
|
|
1121
|
-
* options: { language: 'en' }
|
|
1122
|
-
* })
|
|
1123
|
-
*/
|
|
1124
|
-
createIndex(indexCallback, config = {}) {
|
|
1125
|
-
this.validateCollectionUsable(`createIndex`);
|
|
1126
|
-
const indexId = ++this.indexCounter;
|
|
1127
|
-
const singleRowRefProxy = createSingleRowRefProxy();
|
|
1128
|
-
const indexExpression = indexCallback(singleRowRefProxy);
|
|
1129
|
-
const expression = toExpression(indexExpression);
|
|
1130
|
-
const resolver = config.indexType ?? BTreeIndex;
|
|
1131
|
-
const lazyIndex = new LazyIndexWrapper(
|
|
1132
|
-
indexId,
|
|
1133
|
-
expression,
|
|
1134
|
-
config.name,
|
|
1135
|
-
resolver,
|
|
1136
|
-
config.options,
|
|
1137
|
-
this.entries()
|
|
1138
|
-
);
|
|
1139
|
-
this.lazyIndexes.set(indexId, lazyIndex);
|
|
1140
|
-
if (resolver === BTreeIndex) {
|
|
1141
|
-
try {
|
|
1142
|
-
const resolvedIndex = lazyIndex.getResolved();
|
|
1143
|
-
this.resolvedIndexes.set(indexId, resolvedIndex);
|
|
1144
|
-
} catch (error) {
|
|
1145
|
-
console.warn(`Failed to resolve BTreeIndex:`, error);
|
|
1146
|
-
}
|
|
1147
|
-
} else if (typeof resolver === `function` && resolver.prototype) {
|
|
1148
|
-
try {
|
|
1149
|
-
const resolvedIndex = lazyIndex.getResolved();
|
|
1150
|
-
this.resolvedIndexes.set(indexId, resolvedIndex);
|
|
1151
|
-
} catch {
|
|
1152
|
-
this.resolveSingleIndex(indexId, lazyIndex).catch((error) => {
|
|
1153
|
-
console.warn(`Failed to resolve single index:`, error);
|
|
1154
|
-
});
|
|
1155
|
-
}
|
|
1156
|
-
} else if (this.isIndexesResolved) {
|
|
1157
|
-
this.resolveSingleIndex(indexId, lazyIndex).catch((error) => {
|
|
1158
|
-
console.warn(`Failed to resolve single index:`, error);
|
|
1159
|
-
});
|
|
1160
|
-
}
|
|
1161
|
-
return new IndexProxy(indexId, lazyIndex);
|
|
1162
|
-
}
|
|
1163
|
-
/**
|
|
1164
|
-
* Resolve all lazy indexes (called when collection first syncs)
|
|
1165
|
-
* @private
|
|
1166
|
-
*/
|
|
1167
|
-
async resolveAllIndexes() {
|
|
1168
|
-
if (this.isIndexesResolved) return;
|
|
1169
|
-
const resolutionPromises = Array.from(this.lazyIndexes.entries()).map(
|
|
1170
|
-
async ([indexId, lazyIndex]) => {
|
|
1171
|
-
const resolvedIndex = await lazyIndex.resolve();
|
|
1172
|
-
resolvedIndex.build(this.entries());
|
|
1173
|
-
this.resolvedIndexes.set(indexId, resolvedIndex);
|
|
1174
|
-
return { indexId, resolvedIndex };
|
|
1175
|
-
}
|
|
1176
|
-
);
|
|
1177
|
-
await Promise.all(resolutionPromises);
|
|
1178
|
-
this.isIndexesResolved = true;
|
|
1179
|
-
}
|
|
1180
|
-
/**
|
|
1181
|
-
* Resolve a single index immediately
|
|
1182
|
-
* @private
|
|
1183
|
-
*/
|
|
1184
|
-
async resolveSingleIndex(indexId, lazyIndex) {
|
|
1185
|
-
const resolvedIndex = await lazyIndex.resolve();
|
|
1186
|
-
resolvedIndex.build(this.entries());
|
|
1187
|
-
this.resolvedIndexes.set(indexId, resolvedIndex);
|
|
1188
|
-
return resolvedIndex;
|
|
1189
|
-
}
|
|
1190
|
-
/**
|
|
1191
|
-
* Get resolved indexes for query optimization
|
|
1192
|
-
*/
|
|
1193
|
-
get indexes() {
|
|
1194
|
-
return this.resolvedIndexes;
|
|
1195
|
-
}
|
|
1196
|
-
/**
|
|
1197
|
-
* Updates all indexes when the collection changes
|
|
1198
|
-
* @private
|
|
1199
|
-
*/
|
|
1200
|
-
updateIndexes(changes) {
|
|
1201
|
-
for (const index of this.resolvedIndexes.values()) {
|
|
1202
|
-
for (const change of changes) {
|
|
1203
|
-
switch (change.type) {
|
|
1204
|
-
case `insert`:
|
|
1205
|
-
index.add(change.key, change.value);
|
|
1206
|
-
break;
|
|
1207
|
-
case `update`:
|
|
1208
|
-
if (change.previousValue) {
|
|
1209
|
-
index.update(change.key, change.previousValue, change.value);
|
|
1210
|
-
} else {
|
|
1211
|
-
index.add(change.key, change.value);
|
|
1212
|
-
}
|
|
1213
|
-
break;
|
|
1214
|
-
case `delete`:
|
|
1215
|
-
index.remove(change.key, change.value);
|
|
1216
|
-
break;
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
validateData(data, type, key) {
|
|
1222
|
-
if (!this.config.schema) return data;
|
|
1223
|
-
const standardSchema = this.ensureStandardSchema(this.config.schema);
|
|
1224
|
-
if (type === `update` && key) {
|
|
1225
|
-
const existingData = this.get(key);
|
|
1226
|
-
if (existingData && data && typeof data === `object` && typeof existingData === `object`) {
|
|
1227
|
-
const mergedData = Object.assign({}, existingData, data);
|
|
1228
|
-
const result2 = standardSchema[`~standard`].validate(mergedData);
|
|
1229
|
-
if (result2 instanceof Promise) {
|
|
1230
|
-
throw new SchemaMustBeSynchronousError();
|
|
1231
|
-
}
|
|
1232
|
-
if (`issues` in result2 && result2.issues) {
|
|
1233
|
-
const typedIssues = result2.issues.map((issue) => {
|
|
1234
|
-
var _a;
|
|
1235
|
-
return {
|
|
1236
|
-
message: issue.message,
|
|
1237
|
-
path: (_a = issue.path) == null ? void 0 : _a.map((p) => String(p))
|
|
1238
|
-
};
|
|
1239
|
-
});
|
|
1240
|
-
throw new SchemaValidationError(type, typedIssues);
|
|
1241
|
-
}
|
|
1242
|
-
const validatedMergedData = result2.value;
|
|
1243
|
-
const modifiedKeys = Object.keys(data);
|
|
1244
|
-
const extractedChanges = Object.fromEntries(
|
|
1245
|
-
modifiedKeys.map((k) => [k, validatedMergedData[k]])
|
|
1246
|
-
);
|
|
1247
|
-
return extractedChanges;
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
const result = standardSchema[`~standard`].validate(data);
|
|
1251
|
-
if (result instanceof Promise) {
|
|
1252
|
-
throw new SchemaMustBeSynchronousError();
|
|
1253
|
-
}
|
|
1254
|
-
if (`issues` in result && result.issues) {
|
|
1255
|
-
const typedIssues = result.issues.map((issue) => {
|
|
1256
|
-
var _a;
|
|
1257
|
-
return {
|
|
1258
|
-
message: issue.message,
|
|
1259
|
-
path: (_a = issue.path) == null ? void 0 : _a.map((p) => String(p))
|
|
1260
|
-
};
|
|
1261
|
-
});
|
|
1262
|
-
throw new SchemaValidationError(type, typedIssues);
|
|
1263
|
-
}
|
|
1264
|
-
return result.value;
|
|
1265
|
-
}
|
|
1266
|
-
update(keys, configOrCallback, maybeCallback) {
|
|
1267
|
-
if (typeof keys === `undefined`) {
|
|
1268
|
-
throw new MissingUpdateArgumentError();
|
|
1269
|
-
}
|
|
1270
|
-
this.validateCollectionUsable(`update`);
|
|
1271
|
-
const ambientTransaction = getActiveTransaction();
|
|
1272
|
-
if (!ambientTransaction && !this.config.onUpdate) {
|
|
1273
|
-
throw new MissingUpdateHandlerError();
|
|
1274
|
-
}
|
|
1275
|
-
const isArray = Array.isArray(keys);
|
|
1276
|
-
const keysArray = isArray ? keys : [keys];
|
|
1277
|
-
if (isArray && keysArray.length === 0) {
|
|
1278
|
-
throw new NoKeysPassedToUpdateError();
|
|
1279
|
-
}
|
|
1280
|
-
const callback = typeof configOrCallback === `function` ? configOrCallback : maybeCallback;
|
|
1281
|
-
const config = typeof configOrCallback === `function` ? {} : configOrCallback;
|
|
1282
|
-
const currentObjects = keysArray.map((key) => {
|
|
1283
|
-
const item = this.get(key);
|
|
1284
|
-
if (!item) {
|
|
1285
|
-
throw new UpdateKeyNotFoundError(key);
|
|
1286
|
-
}
|
|
1287
|
-
return item;
|
|
1288
|
-
});
|
|
1289
|
-
let changesArray;
|
|
1290
|
-
if (isArray) {
|
|
1291
|
-
changesArray = withArrayChangeTracking(
|
|
1292
|
-
currentObjects,
|
|
1293
|
-
callback
|
|
1294
|
-
);
|
|
1295
|
-
} else {
|
|
1296
|
-
const result = withChangeTracking(
|
|
1297
|
-
currentObjects[0],
|
|
1298
|
-
callback
|
|
1299
|
-
);
|
|
1300
|
-
changesArray = [result];
|
|
1301
|
-
}
|
|
1302
|
-
const mutations = keysArray.map((key, index) => {
|
|
1303
|
-
const itemChanges = changesArray[index];
|
|
1304
|
-
if (!itemChanges || Object.keys(itemChanges).length === 0) {
|
|
1305
|
-
return null;
|
|
1306
|
-
}
|
|
1307
|
-
const originalItem = currentObjects[index];
|
|
1308
|
-
const validatedUpdatePayload = this.validateData(
|
|
1309
|
-
itemChanges,
|
|
1310
|
-
`update`,
|
|
1311
|
-
key
|
|
1312
|
-
);
|
|
1313
|
-
const modifiedItem = Object.assign(
|
|
1314
|
-
{},
|
|
1315
|
-
originalItem,
|
|
1316
|
-
validatedUpdatePayload
|
|
1317
|
-
);
|
|
1318
|
-
const originalItemId = this.getKeyFromItem(originalItem);
|
|
1319
|
-
const modifiedItemId = this.getKeyFromItem(modifiedItem);
|
|
1320
|
-
if (originalItemId !== modifiedItemId) {
|
|
1321
|
-
throw new KeyUpdateNotAllowedError(originalItemId, modifiedItemId);
|
|
1322
|
-
}
|
|
1323
|
-
const globalKey = this.generateGlobalKey(modifiedItemId, modifiedItem);
|
|
1324
|
-
return {
|
|
1325
|
-
mutationId: crypto.randomUUID(),
|
|
1326
|
-
original: originalItem,
|
|
1327
|
-
modified: modifiedItem,
|
|
1328
|
-
// Pick the values from modifiedItem based on what's passed in - this is for cases
|
|
1329
|
-
// where a schema has default values or transforms. The modified data has the extra
|
|
1330
|
-
// default or transformed values but for changes, we just want to show the data that
|
|
1331
|
-
// was actually passed in.
|
|
1332
|
-
changes: Object.fromEntries(
|
|
1333
|
-
Object.keys(itemChanges).map((k) => [
|
|
1334
|
-
k,
|
|
1335
|
-
modifiedItem[k]
|
|
1336
|
-
])
|
|
1337
|
-
),
|
|
1338
|
-
globalKey,
|
|
1339
|
-
key,
|
|
1340
|
-
metadata: config.metadata,
|
|
1341
|
-
syncMetadata: this.syncedMetadata.get(key) || {},
|
|
1342
|
-
optimistic: config.optimistic ?? true,
|
|
1343
|
-
type: `update`,
|
|
1344
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
1345
|
-
updatedAt: /* @__PURE__ */ new Date(),
|
|
1346
|
-
collection: this
|
|
1347
|
-
};
|
|
1348
|
-
}).filter(Boolean);
|
|
1349
|
-
if (mutations.length === 0) {
|
|
1350
|
-
const emptyTransaction = createTransaction({
|
|
1351
|
-
mutationFn: async () => {
|
|
1352
|
-
}
|
|
1353
|
-
});
|
|
1354
|
-
emptyTransaction.commit();
|
|
1355
|
-
this.scheduleTransactionCleanup(emptyTransaction);
|
|
1356
|
-
return emptyTransaction;
|
|
1357
|
-
}
|
|
1358
|
-
if (ambientTransaction) {
|
|
1359
|
-
ambientTransaction.applyMutations(mutations);
|
|
1360
|
-
this.transactions.set(ambientTransaction.id, ambientTransaction);
|
|
1361
|
-
this.scheduleTransactionCleanup(ambientTransaction);
|
|
1362
|
-
this.recomputeOptimisticState(true);
|
|
1363
|
-
return ambientTransaction;
|
|
1364
|
-
}
|
|
1365
|
-
const directOpTransaction = createTransaction({
|
|
1366
|
-
mutationFn: async (params) => {
|
|
1367
|
-
return this.config.onUpdate({
|
|
1368
|
-
transaction: params.transaction,
|
|
1369
|
-
collection: this
|
|
1370
|
-
});
|
|
1371
|
-
}
|
|
1372
|
-
});
|
|
1373
|
-
directOpTransaction.applyMutations(mutations);
|
|
1374
|
-
directOpTransaction.commit();
|
|
1375
|
-
this.transactions.set(directOpTransaction.id, directOpTransaction);
|
|
1376
|
-
this.scheduleTransactionCleanup(directOpTransaction);
|
|
1377
|
-
this.recomputeOptimisticState(true);
|
|
1378
|
-
return directOpTransaction;
|
|
1379
|
-
}
|
|
1380
|
-
/**
|
|
1381
|
-
* Gets the current state of the collection as a Map
|
|
1382
|
-
* @returns Map containing all items in the collection, with keys as identifiers
|
|
1383
|
-
* @example
|
|
1384
|
-
* const itemsMap = collection.state
|
|
1385
|
-
* console.log(`Collection has ${itemsMap.size} items`)
|
|
1386
|
-
*
|
|
1387
|
-
* for (const [key, item] of itemsMap) {
|
|
1388
|
-
* console.log(`${key}: ${item.title}`)
|
|
1389
|
-
* }
|
|
1390
|
-
*
|
|
1391
|
-
* // Check if specific item exists
|
|
1392
|
-
* if (itemsMap.has("todo-1")) {
|
|
1393
|
-
* console.log("Todo 1 exists:", itemsMap.get("todo-1"))
|
|
1394
|
-
* }
|
|
1395
|
-
*/
|
|
1396
|
-
get state() {
|
|
1397
|
-
const result = /* @__PURE__ */ new Map();
|
|
1398
|
-
for (const [key, value] of this.entries()) {
|
|
1399
|
-
result.set(key, value);
|
|
1400
|
-
}
|
|
1401
|
-
return result;
|
|
1402
|
-
}
|
|
1403
|
-
/**
|
|
1404
|
-
* Gets the current state of the collection as a Map, but only resolves when data is available
|
|
1405
|
-
* Waits for the first sync commit to complete before resolving
|
|
1406
|
-
*
|
|
1407
|
-
* @returns Promise that resolves to a Map containing all items in the collection
|
|
1408
|
-
*/
|
|
1409
|
-
stateWhenReady() {
|
|
1410
|
-
if (this.size > 0 || this.isReady()) {
|
|
1411
|
-
return Promise.resolve(this.state);
|
|
1412
|
-
}
|
|
1413
|
-
return this.preload().then(() => this.state);
|
|
1414
|
-
}
|
|
1415
|
-
/**
|
|
1416
|
-
* Gets the current state of the collection as an Array
|
|
1417
|
-
*
|
|
1418
|
-
* @returns An Array containing all items in the collection
|
|
1419
|
-
*/
|
|
1420
|
-
get toArray() {
|
|
1421
|
-
return Array.from(this.values());
|
|
1422
|
-
}
|
|
1423
|
-
/**
|
|
1424
|
-
* Gets the current state of the collection as an Array, but only resolves when data is available
|
|
1425
|
-
* Waits for the first sync commit to complete before resolving
|
|
1426
|
-
*
|
|
1427
|
-
* @returns Promise that resolves to an Array containing all items in the collection
|
|
1428
|
-
*/
|
|
1429
|
-
toArrayWhenReady() {
|
|
1430
|
-
if (this.size > 0 || this.isReady()) {
|
|
1431
|
-
return Promise.resolve(this.toArray);
|
|
1432
|
-
}
|
|
1433
|
-
return this.preload().then(() => this.toArray);
|
|
1434
|
-
}
|
|
1435
|
-
/**
|
|
1436
|
-
* Returns the current state of the collection as an array of changes
|
|
1437
|
-
* @param options - Options including optional where filter
|
|
1438
|
-
* @returns An array of changes
|
|
1439
|
-
* @example
|
|
1440
|
-
* // Get all items as changes
|
|
1441
|
-
* const allChanges = collection.currentStateAsChanges()
|
|
1442
|
-
*
|
|
1443
|
-
* // Get only items matching a condition
|
|
1444
|
-
* const activeChanges = collection.currentStateAsChanges({
|
|
1445
|
-
* where: (row) => row.status === 'active'
|
|
1446
|
-
* })
|
|
1447
|
-
*
|
|
1448
|
-
* // Get only items using a pre-compiled expression
|
|
1449
|
-
* const activeChanges = collection.currentStateAsChanges({
|
|
1450
|
-
* whereExpression: eq(row.status, 'active')
|
|
1451
|
-
* })
|
|
1452
|
-
*/
|
|
1453
|
-
currentStateAsChanges(options = {}) {
|
|
1454
|
-
return currentStateAsChanges(this, options);
|
|
1455
|
-
}
|
|
1456
|
-
/**
|
|
1457
|
-
* Subscribe to changes in the collection
|
|
1458
|
-
* @param callback - Function called when items change
|
|
1459
|
-
* @param options - Subscription options including includeInitialState and where filter
|
|
1460
|
-
* @returns Unsubscribe function - Call this to stop listening for changes
|
|
1461
|
-
* @example
|
|
1462
|
-
* // Basic subscription
|
|
1463
|
-
* const unsubscribe = collection.subscribeChanges((changes) => {
|
|
1464
|
-
* changes.forEach(change => {
|
|
1465
|
-
* console.log(`${change.type}: ${change.key}`, change.value)
|
|
1466
|
-
* })
|
|
1467
|
-
* })
|
|
1468
|
-
*
|
|
1469
|
-
* // Later: unsubscribe()
|
|
1470
|
-
*
|
|
1471
|
-
* @example
|
|
1472
|
-
* // Include current state immediately
|
|
1473
|
-
* const unsubscribe = collection.subscribeChanges((changes) => {
|
|
1474
|
-
* updateUI(changes)
|
|
1475
|
-
* }, { includeInitialState: true })
|
|
1476
|
-
*
|
|
1477
|
-
* @example
|
|
1478
|
-
* // Subscribe only to changes matching a condition
|
|
1479
|
-
* const unsubscribe = collection.subscribeChanges((changes) => {
|
|
1480
|
-
* updateUI(changes)
|
|
1481
|
-
* }, {
|
|
1482
|
-
* includeInitialState: true,
|
|
1483
|
-
* where: (row) => row.status === 'active'
|
|
1484
|
-
* })
|
|
1485
|
-
*
|
|
1486
|
-
* @example
|
|
1487
|
-
* // Subscribe using a pre-compiled expression
|
|
1488
|
-
* const unsubscribe = collection.subscribeChanges((changes) => {
|
|
1489
|
-
* updateUI(changes)
|
|
1490
|
-
* }, {
|
|
1491
|
-
* includeInitialState: true,
|
|
1492
|
-
* whereExpression: eq(row.status, 'active')
|
|
1493
|
-
* })
|
|
1494
|
-
*/
|
|
1495
|
-
subscribeChanges(callback, options = {}) {
|
|
1496
|
-
this.addSubscriber();
|
|
1497
|
-
if (options.whereExpression) {
|
|
1498
|
-
ensureIndexForExpression(options.whereExpression, this);
|
|
1499
|
-
}
|
|
1500
|
-
const filteredCallback = options.where || options.whereExpression ? createFilteredCallback(callback, options) : callback;
|
|
1501
|
-
if (options.includeInitialState) {
|
|
1502
|
-
const initialChanges = this.currentStateAsChanges({
|
|
1503
|
-
where: options.where,
|
|
1504
|
-
whereExpression: options.whereExpression
|
|
1505
|
-
});
|
|
1506
|
-
filteredCallback(initialChanges);
|
|
1507
|
-
}
|
|
1508
|
-
this.changeListeners.add(filteredCallback);
|
|
1509
|
-
return () => {
|
|
1510
|
-
this.changeListeners.delete(filteredCallback);
|
|
1511
|
-
this.removeSubscriber();
|
|
1512
|
-
};
|
|
1513
|
-
}
|
|
1514
|
-
/**
|
|
1515
|
-
* Subscribe to changes for a specific key
|
|
1516
|
-
*/
|
|
1517
|
-
subscribeChangesKey(key, listener, { includeInitialState = false } = {}) {
|
|
1518
|
-
this.addSubscriber();
|
|
1519
|
-
if (!this.changeKeyListeners.has(key)) {
|
|
1520
|
-
this.changeKeyListeners.set(key, /* @__PURE__ */ new Set());
|
|
1521
|
-
}
|
|
1522
|
-
if (includeInitialState) {
|
|
1523
|
-
listener([
|
|
1524
|
-
{
|
|
1525
|
-
type: `insert`,
|
|
1526
|
-
key,
|
|
1527
|
-
value: this.get(key)
|
|
1528
|
-
}
|
|
1529
|
-
]);
|
|
1530
|
-
}
|
|
1531
|
-
this.changeKeyListeners.get(key).add(listener);
|
|
1532
|
-
return () => {
|
|
1533
|
-
const listeners = this.changeKeyListeners.get(key);
|
|
1534
|
-
if (listeners) {
|
|
1535
|
-
listeners.delete(listener);
|
|
1536
|
-
if (listeners.size === 0) {
|
|
1537
|
-
this.changeKeyListeners.delete(key);
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
this.removeSubscriber();
|
|
1541
|
-
};
|
|
1542
|
-
}
|
|
1543
|
-
/**
|
|
1544
|
-
* Capture visible state for keys that will be affected by pending sync operations
|
|
1545
|
-
* This must be called BEFORE onTransactionStateChange clears optimistic state
|
|
1546
|
-
*/
|
|
1547
|
-
capturePreSyncVisibleState() {
|
|
1548
|
-
if (this.pendingSyncedTransactions.length === 0) return;
|
|
1549
|
-
this.preSyncVisibleState.clear();
|
|
1550
|
-
const syncedKeys = /* @__PURE__ */ new Set();
|
|
1551
|
-
for (const transaction of this.pendingSyncedTransactions) {
|
|
1552
|
-
for (const operation of transaction.operations) {
|
|
1553
|
-
syncedKeys.add(operation.key);
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
for (const key of syncedKeys) {
|
|
1557
|
-
this.recentlySyncedKeys.add(key);
|
|
1558
|
-
}
|
|
1559
|
-
for (const key of syncedKeys) {
|
|
1560
|
-
const currentValue = this.get(key);
|
|
1561
|
-
if (currentValue !== void 0) {
|
|
1562
|
-
this.preSyncVisibleState.set(key, currentValue);
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
/**
|
|
1567
|
-
* Trigger a recomputation when transactions change
|
|
1568
|
-
* This method should be called by the Transaction class when state changes
|
|
1569
|
-
*/
|
|
1570
|
-
onTransactionStateChange() {
|
|
1571
|
-
this.shouldBatchEvents = this.pendingSyncedTransactions.length > 0;
|
|
1572
|
-
this.capturePreSyncVisibleState();
|
|
1573
|
-
this.recomputeOptimisticState(false);
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
export {
|
|
1577
|
-
CollectionImpl,
|
|
1578
|
-
createCollection
|
|
1579
|
-
};
|
|
1580
|
-
//# sourceMappingURL=collection.js.map
|