@anfenn/dync 1.0.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/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/capacitor.cjs +228 -0
- package/dist/capacitor.cjs.map +1 -0
- package/dist/capacitor.d.cts +62 -0
- package/dist/capacitor.d.ts +62 -0
- package/dist/capacitor.js +9 -0
- package/dist/capacitor.js.map +1 -0
- package/dist/chunk-LGHOZECP.js +3884 -0
- package/dist/chunk-LGHOZECP.js.map +1 -0
- package/dist/chunk-SQB6E7V2.js +191 -0
- package/dist/chunk-SQB6E7V2.js.map +1 -0
- package/dist/dexie-Bv-fV10P.d.cts +444 -0
- package/dist/dexie-DJFApKsM.d.ts +444 -0
- package/dist/dexie.cjs +381 -0
- package/dist/dexie.cjs.map +1 -0
- package/dist/dexie.d.cts +3 -0
- package/dist/dexie.d.ts +3 -0
- package/dist/dexie.js +343 -0
- package/dist/dexie.js.map +1 -0
- package/dist/expoSqlite.cjs +98 -0
- package/dist/expoSqlite.cjs.map +1 -0
- package/dist/expoSqlite.d.cts +17 -0
- package/dist/expoSqlite.d.ts +17 -0
- package/dist/expoSqlite.js +61 -0
- package/dist/expoSqlite.js.map +1 -0
- package/dist/index.cjs +3916 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/index.shared-CPIge2ZM.d.ts +234 -0
- package/dist/index.shared-YSn6c01d.d.cts +234 -0
- package/dist/node.cjs +126 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +80 -0
- package/dist/node.d.ts +80 -0
- package/dist/node.js +89 -0
- package/dist/node.js.map +1 -0
- package/dist/react/index.cjs +1754 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +40 -0
- package/dist/react/index.d.ts +40 -0
- package/dist/react/index.js +78 -0
- package/dist/react/index.js.map +1 -0
- package/dist/types-CSbIAfu2.d.cts +46 -0
- package/dist/types-CSbIAfu2.d.ts +46 -0
- package/dist/wa-sqlite.cjs +318 -0
- package/dist/wa-sqlite.cjs.map +1 -0
- package/dist/wa-sqlite.d.cts +175 -0
- package/dist/wa-sqlite.d.ts +175 -0
- package/dist/wa-sqlite.js +281 -0
- package/dist/wa-sqlite.js.map +1 -0
- package/package.json +171 -0
- package/src/addVisibilityChangeListener.native.ts +33 -0
- package/src/addVisibilityChangeListener.ts +24 -0
- package/src/capacitor.ts +4 -0
- package/src/core/StateManager.ts +272 -0
- package/src/core/firstLoad.ts +332 -0
- package/src/core/pullOperations.ts +212 -0
- package/src/core/pushOperations.ts +290 -0
- package/src/core/tableEnhancers.ts +457 -0
- package/src/core/types.ts +3 -0
- package/src/createLocalId.native.ts +8 -0
- package/src/createLocalId.ts +6 -0
- package/src/dexie.ts +2 -0
- package/src/expoSqlite.ts +2 -0
- package/src/helpers.ts +87 -0
- package/src/index.native.ts +28 -0
- package/src/index.shared.ts +613 -0
- package/src/index.ts +28 -0
- package/src/logger.ts +26 -0
- package/src/node.ts +4 -0
- package/src/react/index.ts +2 -0
- package/src/react/useDync.ts +156 -0
- package/src/storage/dexie/DexieAdapter.ts +72 -0
- package/src/storage/dexie/DexieQueryContext.ts +14 -0
- package/src/storage/dexie/DexieStorageCollection.ts +124 -0
- package/src/storage/dexie/DexieStorageTable.ts +123 -0
- package/src/storage/dexie/DexieStorageWhereClause.ts +103 -0
- package/src/storage/dexie/helpers.ts +1 -0
- package/src/storage/dexie/index.ts +7 -0
- package/src/storage/memory/MemoryAdapter.ts +55 -0
- package/src/storage/memory/MemoryCollection.ts +215 -0
- package/src/storage/memory/MemoryQueryContext.ts +14 -0
- package/src/storage/memory/MemoryTable.ts +336 -0
- package/src/storage/memory/MemoryWhereClause.ts +134 -0
- package/src/storage/memory/index.ts +7 -0
- package/src/storage/memory/types.ts +24 -0
- package/src/storage/sqlite/SQLiteAdapter.ts +564 -0
- package/src/storage/sqlite/SQLiteCollection.ts +294 -0
- package/src/storage/sqlite/SQLiteTable.ts +604 -0
- package/src/storage/sqlite/SQLiteWhereClause.ts +341 -0
- package/src/storage/sqlite/SqliteQueryContext.ts +30 -0
- package/src/storage/sqlite/drivers/BetterSqlite3Driver.ts +156 -0
- package/src/storage/sqlite/drivers/CapacitorFastSqlDriver.ts +114 -0
- package/src/storage/sqlite/drivers/CapacitorSQLiteDriver.ts +137 -0
- package/src/storage/sqlite/drivers/ExpoSQLiteDriver.native.ts +67 -0
- package/src/storage/sqlite/drivers/WaSqliteDriver.ts +537 -0
- package/src/storage/sqlite/drivers/wa-sqlite-vfs.d.ts +46 -0
- package/src/storage/sqlite/helpers.ts +144 -0
- package/src/storage/sqlite/index.ts +11 -0
- package/src/storage/sqlite/schema.ts +44 -0
- package/src/storage/sqlite/types.ts +164 -0
- package/src/storage/types.ts +112 -0
- package/src/types.ts +186 -0
- package/src/wa-sqlite.ts +4 -0
|
@@ -0,0 +1,3884 @@
|
|
|
1
|
+
// src/logger.ts
|
|
2
|
+
function newLogger(base, min) {
|
|
3
|
+
const order = {
|
|
4
|
+
debug: 10,
|
|
5
|
+
info: 20,
|
|
6
|
+
warn: 30,
|
|
7
|
+
error: 40,
|
|
8
|
+
none: 100
|
|
9
|
+
};
|
|
10
|
+
const threshold = order[min];
|
|
11
|
+
const enabled = (lvl) => order[lvl] >= threshold;
|
|
12
|
+
return {
|
|
13
|
+
debug: (...a) => enabled("debug") && base.debug?.(...a),
|
|
14
|
+
info: (...a) => enabled("info") && base.info?.(...a),
|
|
15
|
+
warn: (...a) => enabled("warn") && base.warn?.(...a),
|
|
16
|
+
error: (...a) => enabled("error") && base.error?.(...a)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/types.ts
|
|
21
|
+
var SERVER_PK = "id";
|
|
22
|
+
var LOCAL_PK = "_localId";
|
|
23
|
+
var UPDATED_AT = "updated_at";
|
|
24
|
+
var SyncAction = /* @__PURE__ */ ((SyncAction2) => {
|
|
25
|
+
SyncAction2["Create"] = "create";
|
|
26
|
+
SyncAction2["Update"] = "update";
|
|
27
|
+
SyncAction2["Remove"] = "remove";
|
|
28
|
+
return SyncAction2;
|
|
29
|
+
})(SyncAction || {});
|
|
30
|
+
|
|
31
|
+
// src/createLocalId.ts
|
|
32
|
+
function createLocalId() {
|
|
33
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
34
|
+
return globalThis.crypto.randomUUID();
|
|
35
|
+
}
|
|
36
|
+
throw new Error("createLocalId(): crypto.randomUUID is not available");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/helpers.ts
|
|
40
|
+
function sleep(ms, signal) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
if (signal?.aborted) {
|
|
43
|
+
resolve();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const timer = setTimeout(resolve, ms);
|
|
47
|
+
signal?.addEventListener("abort", () => {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
resolve();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function orderFor(a) {
|
|
54
|
+
switch (a) {
|
|
55
|
+
case "create" /* Create */:
|
|
56
|
+
return 1;
|
|
57
|
+
case "update" /* Update */:
|
|
58
|
+
return 2;
|
|
59
|
+
case "remove" /* Remove */:
|
|
60
|
+
return 3;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function omitFields(item, fields) {
|
|
64
|
+
const result = { ...item };
|
|
65
|
+
for (const k of fields) delete result[k];
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
function deleteKeyIfEmptyObject(obj, key) {
|
|
69
|
+
if (obj[key] && Object.keys(obj[key]).length === 0) {
|
|
70
|
+
delete obj[key];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/addVisibilityChangeListener.ts
|
|
75
|
+
function addVisibilityChangeListener(add, currentSubscription, onVisibilityChange) {
|
|
76
|
+
if (add && !currentSubscription) {
|
|
77
|
+
const handler = () => {
|
|
78
|
+
onVisibilityChange(document.visibilityState === "visible");
|
|
79
|
+
};
|
|
80
|
+
document.addEventListener("visibilitychange", handler);
|
|
81
|
+
return {
|
|
82
|
+
remove: () => {
|
|
83
|
+
document.removeEventListener("visibilitychange", handler);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
} else if (!add && currentSubscription) {
|
|
87
|
+
currentSubscription.remove();
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
return currentSubscription;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/core/StateManager.ts
|
|
94
|
+
var LOCAL_ONLY_SYNC_FIELDS = [LOCAL_PK, UPDATED_AT];
|
|
95
|
+
var DYNC_STATE_TABLE = "_dync_state";
|
|
96
|
+
var SYNC_STATE_KEY = "sync_state";
|
|
97
|
+
var DEFAULT_STATE = {
|
|
98
|
+
firstLoadDone: false,
|
|
99
|
+
pendingChanges: [],
|
|
100
|
+
lastPulled: {}
|
|
101
|
+
};
|
|
102
|
+
var StateManager = class {
|
|
103
|
+
persistedState;
|
|
104
|
+
syncStatus;
|
|
105
|
+
listeners = /* @__PURE__ */ new Set();
|
|
106
|
+
storageAdapter;
|
|
107
|
+
hydrated = false;
|
|
108
|
+
constructor(ctx) {
|
|
109
|
+
this.storageAdapter = ctx.storageAdapter;
|
|
110
|
+
this.persistedState = DEFAULT_STATE;
|
|
111
|
+
this.syncStatus = ctx.initialStatus ?? "disabled";
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Load state from the database. Called after stores() defines the schema.
|
|
115
|
+
*/
|
|
116
|
+
async hydrate() {
|
|
117
|
+
if (this.hydrated) return;
|
|
118
|
+
if (!this.storageAdapter) {
|
|
119
|
+
throw new Error("Cannot hydrate state without a storage adapter");
|
|
120
|
+
}
|
|
121
|
+
const table = this.storageAdapter.table(DYNC_STATE_TABLE);
|
|
122
|
+
const row = await table.get(SYNC_STATE_KEY);
|
|
123
|
+
if (row?.value) {
|
|
124
|
+
this.persistedState = parseStoredState(row.value);
|
|
125
|
+
}
|
|
126
|
+
this.hydrated = true;
|
|
127
|
+
this.emit();
|
|
128
|
+
}
|
|
129
|
+
emit() {
|
|
130
|
+
this.listeners.forEach((fn) => fn(this.getSyncState()));
|
|
131
|
+
}
|
|
132
|
+
async persist() {
|
|
133
|
+
if (!this.hydrated || !this.storageAdapter) return;
|
|
134
|
+
this.emit();
|
|
135
|
+
const table = this.storageAdapter.table(DYNC_STATE_TABLE);
|
|
136
|
+
await table.put({ [LOCAL_PK]: SYNC_STATE_KEY, value: JSON.stringify(this.persistedState) });
|
|
137
|
+
}
|
|
138
|
+
getState() {
|
|
139
|
+
return clonePersistedState(this.persistedState);
|
|
140
|
+
}
|
|
141
|
+
setState(setterOrState) {
|
|
142
|
+
this.persistedState = resolveNextState(this.persistedState, setterOrState);
|
|
143
|
+
return this.persist();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Set error in memory only without persisting to database.
|
|
147
|
+
* Used when the database itself failed to open.
|
|
148
|
+
*/
|
|
149
|
+
setErrorInMemory(error) {
|
|
150
|
+
this.persistedState = { ...this.persistedState, error };
|
|
151
|
+
this.emit();
|
|
152
|
+
}
|
|
153
|
+
addPendingChange(change) {
|
|
154
|
+
const next = clonePersistedState(this.persistedState);
|
|
155
|
+
const queueItem = next.pendingChanges.find((p) => p.localId === change.localId && p.stateKey === change.stateKey);
|
|
156
|
+
const omittedChanges = omitFields(change.changes, LOCAL_ONLY_SYNC_FIELDS);
|
|
157
|
+
const omittedBefore = omitFields(change.before, LOCAL_ONLY_SYNC_FIELDS);
|
|
158
|
+
const omittedAfter = omitFields(change.after, LOCAL_ONLY_SYNC_FIELDS);
|
|
159
|
+
const hasChanges = Object.keys(omittedChanges || {}).length > 0;
|
|
160
|
+
const action = change.action;
|
|
161
|
+
if (queueItem) {
|
|
162
|
+
if (queueItem.action === "remove" /* Remove */) {
|
|
163
|
+
return Promise.resolve();
|
|
164
|
+
}
|
|
165
|
+
queueItem.version += 1;
|
|
166
|
+
if (action === "remove" /* Remove */) {
|
|
167
|
+
queueItem.action = "remove" /* Remove */;
|
|
168
|
+
} else if (hasChanges) {
|
|
169
|
+
queueItem.changes = { ...queueItem.changes, ...omittedChanges };
|
|
170
|
+
queueItem.after = { ...queueItem.after, ...omittedAfter };
|
|
171
|
+
}
|
|
172
|
+
} else if (action === "remove" /* Remove */ || hasChanges) {
|
|
173
|
+
next.pendingChanges = [...next.pendingChanges];
|
|
174
|
+
next.pendingChanges.push({
|
|
175
|
+
action,
|
|
176
|
+
stateKey: change.stateKey,
|
|
177
|
+
localId: change.localId,
|
|
178
|
+
id: change.id,
|
|
179
|
+
version: 1,
|
|
180
|
+
changes: omittedChanges,
|
|
181
|
+
before: omittedBefore,
|
|
182
|
+
after: omittedAfter
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
this.persistedState = next;
|
|
186
|
+
return this.persist();
|
|
187
|
+
}
|
|
188
|
+
samePendingVersion(stateKey, localId, version) {
|
|
189
|
+
return this.persistedState.pendingChanges.find((p) => p.localId === localId && p.stateKey === stateKey)?.version === version;
|
|
190
|
+
}
|
|
191
|
+
removePendingChange(localId, stateKey) {
|
|
192
|
+
const next = clonePersistedState(this.persistedState);
|
|
193
|
+
next.pendingChanges = next.pendingChanges.filter((p) => !(p.localId === localId && p.stateKey === stateKey));
|
|
194
|
+
this.persistedState = next;
|
|
195
|
+
return this.persist();
|
|
196
|
+
}
|
|
197
|
+
updatePendingChange(stateKey, localId, action, id) {
|
|
198
|
+
const next = clonePersistedState(this.persistedState);
|
|
199
|
+
const changeItem = next.pendingChanges.find((p) => p.stateKey === stateKey && p.localId === localId);
|
|
200
|
+
if (changeItem) {
|
|
201
|
+
changeItem.action = action;
|
|
202
|
+
if (id) changeItem.id = id;
|
|
203
|
+
this.persistedState = next;
|
|
204
|
+
return this.persist();
|
|
205
|
+
}
|
|
206
|
+
return Promise.resolve();
|
|
207
|
+
}
|
|
208
|
+
setPendingChangeBefore(stateKey, localId, before) {
|
|
209
|
+
const next = clonePersistedState(this.persistedState);
|
|
210
|
+
const changeItem = next.pendingChanges.find((p) => p.stateKey === stateKey && p.localId === localId);
|
|
211
|
+
if (changeItem) {
|
|
212
|
+
changeItem.before = { ...changeItem.before ?? {}, ...before };
|
|
213
|
+
this.persistedState = next;
|
|
214
|
+
return this.persist();
|
|
215
|
+
}
|
|
216
|
+
return Promise.resolve();
|
|
217
|
+
}
|
|
218
|
+
hasConflicts(localId) {
|
|
219
|
+
return Boolean(this.persistedState.conflicts?.[localId]);
|
|
220
|
+
}
|
|
221
|
+
getSyncStatus() {
|
|
222
|
+
return this.syncStatus;
|
|
223
|
+
}
|
|
224
|
+
setSyncStatus(status) {
|
|
225
|
+
if (this.syncStatus === status) return;
|
|
226
|
+
this.syncStatus = status;
|
|
227
|
+
this.emit();
|
|
228
|
+
}
|
|
229
|
+
getSyncState() {
|
|
230
|
+
return buildSyncState(this.persistedState, this.syncStatus, this.hydrated);
|
|
231
|
+
}
|
|
232
|
+
subscribe(listener) {
|
|
233
|
+
this.listeners.add(listener);
|
|
234
|
+
return () => this.listeners.delete(listener);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
function parseStoredState(stored) {
|
|
238
|
+
const parsed = JSON.parse(stored);
|
|
239
|
+
if (parsed.pendingChanges) {
|
|
240
|
+
parsed.pendingChanges = parsed.pendingChanges.map((change) => ({
|
|
241
|
+
...change,
|
|
242
|
+
timestamp: change.timestamp ? new Date(change.timestamp) : change.timestamp
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
return parsed;
|
|
246
|
+
}
|
|
247
|
+
function resolveNextState(current, setterOrState) {
|
|
248
|
+
if (typeof setterOrState === "function") {
|
|
249
|
+
return { ...current, ...setterOrState(clonePersistedState(current)) };
|
|
250
|
+
}
|
|
251
|
+
return { ...current, ...setterOrState };
|
|
252
|
+
}
|
|
253
|
+
function buildSyncState(state, status, hydrated) {
|
|
254
|
+
const persisted = clonePersistedState(state);
|
|
255
|
+
const syncState = {
|
|
256
|
+
...persisted,
|
|
257
|
+
status,
|
|
258
|
+
hydrated
|
|
259
|
+
};
|
|
260
|
+
deleteKeyIfEmptyObject(syncState, "conflicts");
|
|
261
|
+
return syncState;
|
|
262
|
+
}
|
|
263
|
+
function clonePersistedState(state) {
|
|
264
|
+
return {
|
|
265
|
+
...state,
|
|
266
|
+
pendingChanges: state.pendingChanges.map((change) => ({
|
|
267
|
+
...change,
|
|
268
|
+
changes: cloneRecord(change.changes),
|
|
269
|
+
before: cloneRecord(change.before),
|
|
270
|
+
after: cloneRecord(change.after)
|
|
271
|
+
})),
|
|
272
|
+
lastPulled: { ...state.lastPulled },
|
|
273
|
+
conflicts: cloneConflicts(state.conflicts)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function cloneConflicts(conflicts) {
|
|
277
|
+
if (!conflicts) return void 0;
|
|
278
|
+
const next = {};
|
|
279
|
+
for (const [key, value] of Object.entries(conflicts)) {
|
|
280
|
+
next[key] = {
|
|
281
|
+
stateKey: value.stateKey,
|
|
282
|
+
fields: value.fields.map((field) => ({ ...field }))
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return next;
|
|
286
|
+
}
|
|
287
|
+
function cloneRecord(record) {
|
|
288
|
+
if (!record) return record;
|
|
289
|
+
return { ...record };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/core/tableEnhancers.ts
|
|
293
|
+
function wrapWithMutationEmitter(table, tableName, emitMutation) {
|
|
294
|
+
const rawAdd = table.raw.add;
|
|
295
|
+
const rawPut = table.raw.put;
|
|
296
|
+
const rawUpdate = table.raw.update;
|
|
297
|
+
const rawDelete = table.raw.delete;
|
|
298
|
+
const rawBulkAdd = table.raw.bulkAdd;
|
|
299
|
+
const rawBulkPut = table.raw.bulkPut;
|
|
300
|
+
const rawBulkUpdate = table.raw.bulkUpdate;
|
|
301
|
+
const rawBulkDelete = table.raw.bulkDelete;
|
|
302
|
+
const rawClear = table.raw.clear;
|
|
303
|
+
table.add = async (item) => {
|
|
304
|
+
const result = await rawAdd(item);
|
|
305
|
+
emitMutation({ type: "add", tableName, keys: [result] });
|
|
306
|
+
return result;
|
|
307
|
+
};
|
|
308
|
+
table.put = async (item) => {
|
|
309
|
+
const result = await rawPut(item);
|
|
310
|
+
emitMutation({ type: "update", tableName, keys: [result] });
|
|
311
|
+
return result;
|
|
312
|
+
};
|
|
313
|
+
table.update = async (key, changes) => {
|
|
314
|
+
const result = await rawUpdate(key, changes);
|
|
315
|
+
if (result > 0) {
|
|
316
|
+
emitMutation({ type: "update", tableName, keys: [key] });
|
|
317
|
+
}
|
|
318
|
+
return result;
|
|
319
|
+
};
|
|
320
|
+
table.delete = async (key) => {
|
|
321
|
+
await rawDelete(key);
|
|
322
|
+
emitMutation({ type: "delete", tableName, keys: [key] });
|
|
323
|
+
};
|
|
324
|
+
table.bulkAdd = async (items) => {
|
|
325
|
+
const result = await rawBulkAdd(items);
|
|
326
|
+
if (items.length > 0) {
|
|
327
|
+
emitMutation({ type: "add", tableName });
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
};
|
|
331
|
+
table.bulkPut = async (items) => {
|
|
332
|
+
const result = await rawBulkPut(items);
|
|
333
|
+
if (items.length > 0) {
|
|
334
|
+
emitMutation({ type: "update", tableName });
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
};
|
|
338
|
+
table.bulkUpdate = async (keysAndChanges) => {
|
|
339
|
+
const result = await rawBulkUpdate(keysAndChanges);
|
|
340
|
+
if (result > 0) {
|
|
341
|
+
emitMutation({ type: "update", tableName, keys: keysAndChanges.map((kc) => kc.key) });
|
|
342
|
+
}
|
|
343
|
+
return result;
|
|
344
|
+
};
|
|
345
|
+
table.bulkDelete = async (keys) => {
|
|
346
|
+
await rawBulkDelete(keys);
|
|
347
|
+
if (keys.length > 0) {
|
|
348
|
+
emitMutation({ type: "delete", tableName });
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
table.clear = async () => {
|
|
352
|
+
await rawClear();
|
|
353
|
+
emitMutation({ type: "delete", tableName });
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function setupEnhancedTables({ owner, tableCache, enhancedTables, getTable }, tableNames) {
|
|
357
|
+
for (const tableName of tableNames) {
|
|
358
|
+
tableCache.delete(tableName);
|
|
359
|
+
enhancedTables.delete(tableName);
|
|
360
|
+
if (!Object.prototype.hasOwnProperty.call(owner, tableName)) {
|
|
361
|
+
Object.defineProperty(owner, tableName, {
|
|
362
|
+
get: () => getTable(tableName),
|
|
363
|
+
enumerable: true,
|
|
364
|
+
configurable: true
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function enhanceSyncTable({ table, tableName, withTransaction, state, enhancedTables, emitMutation }) {
|
|
370
|
+
const rawAdd = table.raw.add;
|
|
371
|
+
const rawPut = table.raw.put;
|
|
372
|
+
const rawUpdate = table.raw.update;
|
|
373
|
+
const rawDelete = table.raw.delete;
|
|
374
|
+
const rawBulkAdd = table.raw.bulkAdd;
|
|
375
|
+
const rawBulkPut = table.raw.bulkPut;
|
|
376
|
+
const rawBulkUpdate = table.raw.bulkUpdate;
|
|
377
|
+
const rawBulkDelete = table.raw.bulkDelete;
|
|
378
|
+
const rawClear = table.raw.clear;
|
|
379
|
+
const wrappedAdd = async (item) => {
|
|
380
|
+
let localId = item._localId;
|
|
381
|
+
if (!localId) localId = createLocalId();
|
|
382
|
+
const syncedItem = {
|
|
383
|
+
...item,
|
|
384
|
+
_localId: localId,
|
|
385
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
386
|
+
};
|
|
387
|
+
let result;
|
|
388
|
+
await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async () => {
|
|
389
|
+
result = await rawAdd(syncedItem);
|
|
390
|
+
await state.addPendingChange({
|
|
391
|
+
action: "create" /* Create */,
|
|
392
|
+
stateKey: tableName,
|
|
393
|
+
localId,
|
|
394
|
+
changes: syncedItem,
|
|
395
|
+
before: null,
|
|
396
|
+
after: syncedItem
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
emitMutation({ type: "add", tableName, keys: [localId] });
|
|
400
|
+
return result;
|
|
401
|
+
};
|
|
402
|
+
const wrappedPut = async (item) => {
|
|
403
|
+
let localId = item._localId;
|
|
404
|
+
if (!localId) localId = createLocalId();
|
|
405
|
+
const syncedItem = {
|
|
406
|
+
...item,
|
|
407
|
+
_localId: localId,
|
|
408
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
409
|
+
};
|
|
410
|
+
let result;
|
|
411
|
+
let isUpdate = false;
|
|
412
|
+
let existingRecord;
|
|
413
|
+
await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async (tables) => {
|
|
414
|
+
const txTable = tables[tableName];
|
|
415
|
+
existingRecord = await txTable.get(localId);
|
|
416
|
+
isUpdate = !!existingRecord;
|
|
417
|
+
result = await rawPut(syncedItem);
|
|
418
|
+
await state.addPendingChange({
|
|
419
|
+
action: isUpdate ? "update" /* Update */ : "create" /* Create */,
|
|
420
|
+
stateKey: tableName,
|
|
421
|
+
localId,
|
|
422
|
+
id: existingRecord?.id,
|
|
423
|
+
changes: syncedItem,
|
|
424
|
+
before: existingRecord ?? null,
|
|
425
|
+
after: syncedItem
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
emitMutation({ type: isUpdate ? "update" : "add", tableName, keys: [localId] });
|
|
429
|
+
return result;
|
|
430
|
+
};
|
|
431
|
+
const wrappedUpdate = async (key, changes) => {
|
|
432
|
+
const updatedChanges = {
|
|
433
|
+
...changes,
|
|
434
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
435
|
+
};
|
|
436
|
+
let result = 0;
|
|
437
|
+
await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async (tables) => {
|
|
438
|
+
const txTable = tables[tableName];
|
|
439
|
+
const record = await txTable.get(key);
|
|
440
|
+
if (!record) {
|
|
441
|
+
throw new Error(`Record with key=${key} not found`);
|
|
442
|
+
}
|
|
443
|
+
result = await rawUpdate(key, updatedChanges) ?? 0;
|
|
444
|
+
if (result > 0) {
|
|
445
|
+
await state.addPendingChange({
|
|
446
|
+
action: "update" /* Update */,
|
|
447
|
+
stateKey: tableName,
|
|
448
|
+
localId: key,
|
|
449
|
+
id: record.id,
|
|
450
|
+
changes: updatedChanges,
|
|
451
|
+
before: record,
|
|
452
|
+
after: { ...record, ...updatedChanges }
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
if (result > 0) {
|
|
457
|
+
emitMutation({ type: "update", tableName, keys: [key] });
|
|
458
|
+
}
|
|
459
|
+
return result;
|
|
460
|
+
};
|
|
461
|
+
const wrappedDelete = async (key) => {
|
|
462
|
+
let deletedLocalId;
|
|
463
|
+
await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async (tables) => {
|
|
464
|
+
const txTable = tables[tableName];
|
|
465
|
+
const record = await txTable.get(key);
|
|
466
|
+
await rawDelete(key);
|
|
467
|
+
if (record) {
|
|
468
|
+
deletedLocalId = record._localId;
|
|
469
|
+
await state.addPendingChange({
|
|
470
|
+
action: "remove" /* Remove */,
|
|
471
|
+
stateKey: tableName,
|
|
472
|
+
localId: record._localId,
|
|
473
|
+
id: record.id,
|
|
474
|
+
changes: null,
|
|
475
|
+
before: record
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
if (deletedLocalId) {
|
|
480
|
+
emitMutation({ type: "delete", tableName, keys: [deletedLocalId] });
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
const wrappedBulkAdd = async (items) => {
|
|
484
|
+
if (items.length === 0) return;
|
|
485
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
486
|
+
const syncedItems = items.map((item) => {
|
|
487
|
+
const localId = item._localId || createLocalId();
|
|
488
|
+
return {
|
|
489
|
+
...item,
|
|
490
|
+
_localId: localId,
|
|
491
|
+
updated_at: now
|
|
492
|
+
};
|
|
493
|
+
});
|
|
494
|
+
let result;
|
|
495
|
+
await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async () => {
|
|
496
|
+
result = await rawBulkAdd(syncedItems);
|
|
497
|
+
for (const syncedItem of syncedItems) {
|
|
498
|
+
await state.addPendingChange({
|
|
499
|
+
action: "create" /* Create */,
|
|
500
|
+
stateKey: tableName,
|
|
501
|
+
localId: syncedItem._localId,
|
|
502
|
+
changes: syncedItem,
|
|
503
|
+
before: null,
|
|
504
|
+
after: syncedItem
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
emitMutation({ type: "add", tableName, keys: syncedItems.map((i) => i._localId) });
|
|
509
|
+
return result;
|
|
510
|
+
};
|
|
511
|
+
const wrappedBulkPut = async (items) => {
|
|
512
|
+
if (items.length === 0) return;
|
|
513
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
514
|
+
const syncedItems = items.map((item) => {
|
|
515
|
+
const localId = item._localId || createLocalId();
|
|
516
|
+
return {
|
|
517
|
+
...item,
|
|
518
|
+
_localId: localId,
|
|
519
|
+
updated_at: now
|
|
520
|
+
};
|
|
521
|
+
});
|
|
522
|
+
const localIds = syncedItems.map((i) => i._localId);
|
|
523
|
+
let result;
|
|
524
|
+
await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async (tables) => {
|
|
525
|
+
const txTable = tables[tableName];
|
|
526
|
+
const existingRecords = await txTable.bulkGet(localIds);
|
|
527
|
+
const existingMap = /* @__PURE__ */ new Map();
|
|
528
|
+
for (let i = 0; i < localIds.length; i++) {
|
|
529
|
+
if (existingRecords[i]) {
|
|
530
|
+
existingMap.set(localIds[i], existingRecords[i]);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
result = await rawBulkPut(syncedItems);
|
|
534
|
+
for (const syncedItem of syncedItems) {
|
|
535
|
+
const existing = existingMap.get(syncedItem._localId);
|
|
536
|
+
await state.addPendingChange({
|
|
537
|
+
action: existing ? "update" /* Update */ : "create" /* Create */,
|
|
538
|
+
stateKey: tableName,
|
|
539
|
+
localId: syncedItem._localId,
|
|
540
|
+
id: existing?.id,
|
|
541
|
+
changes: syncedItem,
|
|
542
|
+
before: existing ?? null,
|
|
543
|
+
after: syncedItem
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
emitMutation({ type: "update", tableName, keys: localIds });
|
|
548
|
+
return result;
|
|
549
|
+
};
|
|
550
|
+
const wrappedBulkUpdate = async (keysAndChanges) => {
|
|
551
|
+
if (keysAndChanges.length === 0) return 0;
|
|
552
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
553
|
+
const updatedKeysAndChanges = keysAndChanges.map(({ key, changes }) => ({
|
|
554
|
+
key,
|
|
555
|
+
changes: {
|
|
556
|
+
...changes,
|
|
557
|
+
updated_at: now
|
|
558
|
+
}
|
|
559
|
+
}));
|
|
560
|
+
let result = 0;
|
|
561
|
+
const updatedKeys = [];
|
|
562
|
+
await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async (tables) => {
|
|
563
|
+
const txTable = tables[tableName];
|
|
564
|
+
const keys = updatedKeysAndChanges.map((kc) => kc.key);
|
|
565
|
+
const records = await txTable.bulkGet(keys);
|
|
566
|
+
const recordMap = /* @__PURE__ */ new Map();
|
|
567
|
+
for (let i = 0; i < keys.length; i++) {
|
|
568
|
+
if (records[i]) {
|
|
569
|
+
recordMap.set(String(keys[i]), records[i]);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
result = await rawBulkUpdate(updatedKeysAndChanges);
|
|
573
|
+
for (const { key, changes } of updatedKeysAndChanges) {
|
|
574
|
+
const record = recordMap.get(String(key));
|
|
575
|
+
if (record) {
|
|
576
|
+
updatedKeys.push(record._localId);
|
|
577
|
+
await state.addPendingChange({
|
|
578
|
+
action: "update" /* Update */,
|
|
579
|
+
stateKey: tableName,
|
|
580
|
+
localId: record._localId,
|
|
581
|
+
id: record.id,
|
|
582
|
+
changes,
|
|
583
|
+
before: record,
|
|
584
|
+
after: { ...record, ...changes }
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
if (updatedKeys.length > 0) {
|
|
590
|
+
emitMutation({ type: "update", tableName, keys: updatedKeys });
|
|
591
|
+
}
|
|
592
|
+
return result;
|
|
593
|
+
};
|
|
594
|
+
const wrappedBulkDelete = async (keys) => {
|
|
595
|
+
if (keys.length === 0) return;
|
|
596
|
+
const deletedLocalIds = [];
|
|
597
|
+
await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async (tables) => {
|
|
598
|
+
const txTable = tables[tableName];
|
|
599
|
+
const records = await txTable.bulkGet(keys);
|
|
600
|
+
await rawBulkDelete(keys);
|
|
601
|
+
for (const record of records) {
|
|
602
|
+
if (record) {
|
|
603
|
+
deletedLocalIds.push(record._localId);
|
|
604
|
+
await state.addPendingChange({
|
|
605
|
+
action: "remove" /* Remove */,
|
|
606
|
+
stateKey: tableName,
|
|
607
|
+
localId: record._localId,
|
|
608
|
+
id: record.id,
|
|
609
|
+
changes: null,
|
|
610
|
+
before: record
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
if (deletedLocalIds.length > 0) {
|
|
616
|
+
emitMutation({ type: "delete", tableName, keys: deletedLocalIds });
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
const wrappedClear = async () => {
|
|
620
|
+
const deletedLocalIds = [];
|
|
621
|
+
await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async (tables) => {
|
|
622
|
+
const txTable = tables[tableName];
|
|
623
|
+
const allRecords = await txTable.toArray();
|
|
624
|
+
await rawClear();
|
|
625
|
+
for (const record of allRecords) {
|
|
626
|
+
if (record._localId) {
|
|
627
|
+
deletedLocalIds.push(record._localId);
|
|
628
|
+
await state.addPendingChange({
|
|
629
|
+
action: "remove" /* Remove */,
|
|
630
|
+
stateKey: tableName,
|
|
631
|
+
localId: record._localId,
|
|
632
|
+
id: record.id,
|
|
633
|
+
changes: null,
|
|
634
|
+
before: record
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
if (deletedLocalIds.length > 0) {
|
|
640
|
+
emitMutation({ type: "delete", tableName, keys: deletedLocalIds });
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
table.add = wrappedAdd;
|
|
644
|
+
table.put = wrappedPut;
|
|
645
|
+
table.update = wrappedUpdate;
|
|
646
|
+
table.delete = wrappedDelete;
|
|
647
|
+
table.bulkAdd = wrappedBulkAdd;
|
|
648
|
+
table.bulkPut = wrappedBulkPut;
|
|
649
|
+
table.bulkUpdate = wrappedBulkUpdate;
|
|
650
|
+
table.bulkDelete = wrappedBulkDelete;
|
|
651
|
+
table.clear = wrappedClear;
|
|
652
|
+
enhancedTables.add(tableName);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/core/pullOperations.ts
|
|
656
|
+
async function pullAll(ctx) {
|
|
657
|
+
let firstSyncError;
|
|
658
|
+
const changedTables = [];
|
|
659
|
+
for (const [stateKey, api] of Object.entries(ctx.syncApis)) {
|
|
660
|
+
try {
|
|
661
|
+
const lastPulled = ctx.state.getState().lastPulled[stateKey];
|
|
662
|
+
const since = lastPulled ? new Date(lastPulled) : /* @__PURE__ */ new Date(0);
|
|
663
|
+
ctx.logger.debug(`[dync] pull:start stateKey=${stateKey} since=${since.toISOString()}`);
|
|
664
|
+
const serverData = await api.list(since);
|
|
665
|
+
const changed = await processPullData(stateKey, serverData, since, ctx);
|
|
666
|
+
if (changed) changedTables.push(stateKey);
|
|
667
|
+
} catch (err) {
|
|
668
|
+
firstSyncError = firstSyncError ?? err;
|
|
669
|
+
ctx.logger.error(`[dync] pull:error stateKey=${stateKey}`, err);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return { error: firstSyncError, changedTables };
|
|
673
|
+
}
|
|
674
|
+
async function handleRemoteItemUpdate(table, stateKey, localItem, remote, ctx) {
|
|
675
|
+
const pendingChange = ctx.state.getState().pendingChanges.find((p) => p.stateKey === stateKey && p.localId === localItem._localId);
|
|
676
|
+
const conflictStrategy = ctx.conflictResolutionStrategy;
|
|
677
|
+
if (pendingChange) {
|
|
678
|
+
ctx.logger.debug(`[dync] pull:conflict-strategy:${conflictStrategy} stateKey=${stateKey} id=${remote.id}`);
|
|
679
|
+
switch (conflictStrategy) {
|
|
680
|
+
case "local-wins":
|
|
681
|
+
break;
|
|
682
|
+
case "remote-wins": {
|
|
683
|
+
const merged = { ...remote, _localId: localItem._localId };
|
|
684
|
+
await table.raw.update(localItem._localId, merged);
|
|
685
|
+
await ctx.state.removePendingChange(localItem._localId, stateKey);
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
case "try-shallow-merge": {
|
|
689
|
+
const changes = pendingChange.changes || {};
|
|
690
|
+
const before = pendingChange.before || {};
|
|
691
|
+
const fields = Object.entries(changes).filter(([k, localValue]) => k in before && k in remote && before[k] !== remote[k] && localValue !== remote[k]).map(([key, localValue]) => ({ key, localValue, remoteValue: remote[key] }));
|
|
692
|
+
if (fields.length > 0) {
|
|
693
|
+
ctx.logger.warn(`[dync] pull:${conflictStrategy}:conflicts-found`, JSON.stringify(fields, null, 4));
|
|
694
|
+
await ctx.state.setState((syncState) => ({
|
|
695
|
+
...syncState,
|
|
696
|
+
conflicts: {
|
|
697
|
+
...syncState.conflicts || {},
|
|
698
|
+
[localItem._localId]: { stateKey, fields }
|
|
699
|
+
}
|
|
700
|
+
}));
|
|
701
|
+
} else {
|
|
702
|
+
const localChangedKeys = Object.keys(changes);
|
|
703
|
+
const preservedLocal = { _localId: localItem._localId };
|
|
704
|
+
for (const k of localChangedKeys) {
|
|
705
|
+
if (k in localItem) preservedLocal[k] = localItem[k];
|
|
706
|
+
}
|
|
707
|
+
const merged = { ...remote, ...preservedLocal };
|
|
708
|
+
await table.raw.update(localItem._localId, merged);
|
|
709
|
+
await ctx.state.setState((syncState) => {
|
|
710
|
+
const ss = { ...syncState };
|
|
711
|
+
delete ss.conflicts?.[localItem._localId];
|
|
712
|
+
return ss;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
} else {
|
|
719
|
+
const merged = { ...localItem, ...remote };
|
|
720
|
+
await table.raw.update(localItem._localId, merged);
|
|
721
|
+
ctx.logger.debug(`[dync] pull:merge-remote stateKey=${stateKey} id=${remote.id}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async function pullAllBatch(ctx) {
|
|
725
|
+
let firstSyncError;
|
|
726
|
+
const changedTables = [];
|
|
727
|
+
try {
|
|
728
|
+
const sinceMap = {};
|
|
729
|
+
for (const tableName of ctx.batchSync.syncTables) {
|
|
730
|
+
const lastPulled = ctx.state.getState().lastPulled[tableName];
|
|
731
|
+
sinceMap[tableName] = lastPulled ? new Date(lastPulled) : /* @__PURE__ */ new Date(0);
|
|
732
|
+
}
|
|
733
|
+
ctx.logger.debug(`[dync] pull:batch:start tables=${[...ctx.batchSync.syncTables].join(",")}`, sinceMap);
|
|
734
|
+
const serverDataByTable = await ctx.batchSync.pull(sinceMap);
|
|
735
|
+
for (const [stateKey, serverData] of Object.entries(serverDataByTable)) {
|
|
736
|
+
if (!ctx.batchSync.syncTables.includes(stateKey)) {
|
|
737
|
+
ctx.logger.warn(`[dync] pull:batch:unknown-table stateKey=${stateKey}`);
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
try {
|
|
741
|
+
const changed = await processPullData(stateKey, serverData, sinceMap[stateKey], ctx);
|
|
742
|
+
if (changed) changedTables.push(stateKey);
|
|
743
|
+
} catch (err) {
|
|
744
|
+
firstSyncError = firstSyncError ?? err;
|
|
745
|
+
ctx.logger.error(`[dync] pull:batch:error stateKey=${stateKey}`, err);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
} catch (err) {
|
|
749
|
+
firstSyncError = err;
|
|
750
|
+
ctx.logger.error(`[dync] pull:batch:error`, err);
|
|
751
|
+
}
|
|
752
|
+
return { error: firstSyncError, changedTables };
|
|
753
|
+
}
|
|
754
|
+
async function processPullData(stateKey, serverData, since, ctx) {
|
|
755
|
+
if (!serverData?.length) return false;
|
|
756
|
+
ctx.logger.debug(`[dync] pull:process stateKey=${stateKey} count=${serverData.length}`);
|
|
757
|
+
let newest = since;
|
|
758
|
+
let hasChanges = false;
|
|
759
|
+
await ctx.withTransaction("rw", [stateKey, DYNC_STATE_TABLE], async (tables) => {
|
|
760
|
+
const txTable = tables[stateKey];
|
|
761
|
+
const pendingRemovalById = new Set(
|
|
762
|
+
ctx.state.getState().pendingChanges.filter((p) => p.stateKey === stateKey && p.action === "remove" /* Remove */).map((p) => p.id)
|
|
763
|
+
);
|
|
764
|
+
for (const remote of serverData) {
|
|
765
|
+
const remoteUpdated = new Date(remote.updated_at);
|
|
766
|
+
if (remoteUpdated > newest) newest = remoteUpdated;
|
|
767
|
+
if (pendingRemovalById.has(remote.id)) {
|
|
768
|
+
ctx.logger.debug(`[dync] pull:skip-pending-remove stateKey=${stateKey} id=${remote.id}`);
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const localItem = await txTable.where("id").equals(remote.id).first();
|
|
772
|
+
if (remote.deleted) {
|
|
773
|
+
if (localItem) {
|
|
774
|
+
await txTable.raw.delete(localItem._localId);
|
|
775
|
+
ctx.logger.debug(`[dync] pull:remove stateKey=${stateKey} id=${remote.id}`);
|
|
776
|
+
hasChanges = true;
|
|
777
|
+
}
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
delete remote.deleted;
|
|
781
|
+
if (localItem) {
|
|
782
|
+
await handleRemoteItemUpdate(txTable, stateKey, localItem, remote, ctx);
|
|
783
|
+
hasChanges = true;
|
|
784
|
+
} else {
|
|
785
|
+
const newLocalItem = { ...remote, _localId: createLocalId() };
|
|
786
|
+
await txTable.raw.add(newLocalItem);
|
|
787
|
+
ctx.logger.debug(`[dync] pull:add stateKey=${stateKey} id=${remote.id}`);
|
|
788
|
+
hasChanges = true;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
await ctx.state.setState((syncState) => ({
|
|
792
|
+
...syncState,
|
|
793
|
+
lastPulled: {
|
|
794
|
+
...syncState.lastPulled,
|
|
795
|
+
[stateKey]: newest.toISOString()
|
|
796
|
+
}
|
|
797
|
+
}));
|
|
798
|
+
});
|
|
799
|
+
return hasChanges;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// src/core/pushOperations.ts
|
|
803
|
+
async function handleRemoveSuccess(change, ctx) {
|
|
804
|
+
const { stateKey, localId, id } = change;
|
|
805
|
+
ctx.logger.debug(`[dync] push:remove:success stateKey=${stateKey} localId=${localId} id=${id}`);
|
|
806
|
+
await ctx.state.removePendingChange(localId, stateKey);
|
|
807
|
+
}
|
|
808
|
+
async function handleUpdateSuccess(change, ctx) {
|
|
809
|
+
const { stateKey, localId, version, changes } = change;
|
|
810
|
+
ctx.logger.debug(`[dync] push:update:success stateKey=${stateKey} localId=${localId} id=${change.id}`);
|
|
811
|
+
if (ctx.state.samePendingVersion(stateKey, localId, version)) {
|
|
812
|
+
await ctx.state.removePendingChange(localId, stateKey);
|
|
813
|
+
} else {
|
|
814
|
+
await ctx.state.setPendingChangeBefore(stateKey, localId, changes);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
async function handleCreateSuccess(change, serverResult, ctx) {
|
|
818
|
+
const { stateKey, localId, version, changes, id } = change;
|
|
819
|
+
ctx.logger.debug(`[dync] push:create:success stateKey=${stateKey} localId=${localId} id=${id ?? serverResult.id}`);
|
|
820
|
+
await ctx.withTransaction("rw", [stateKey, DYNC_STATE_TABLE], async (tables) => {
|
|
821
|
+
const txTable = tables[stateKey];
|
|
822
|
+
const wasChanged = await txTable.raw.update(localId, serverResult) ?? 0;
|
|
823
|
+
if (wasChanged && ctx.state.samePendingVersion(stateKey, localId, version)) {
|
|
824
|
+
await ctx.state.removePendingChange(localId, stateKey);
|
|
825
|
+
} else {
|
|
826
|
+
const nextAction = wasChanged ? "update" /* Update */ : "remove" /* Remove */;
|
|
827
|
+
await ctx.state.updatePendingChange(stateKey, localId, nextAction, serverResult.id);
|
|
828
|
+
if (nextAction === "remove" /* Remove */) return;
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
const finalItem = { ...changes, ...serverResult, _localId: localId };
|
|
832
|
+
ctx.syncOptions.onAfterRemoteAdd?.(stateKey, finalItem);
|
|
833
|
+
}
|
|
834
|
+
async function pushAll(ctx) {
|
|
835
|
+
let firstSyncError;
|
|
836
|
+
const changesSnapshot = [...ctx.state.getState().pendingChanges].sort((a, b) => orderFor(a.action) - orderFor(b.action));
|
|
837
|
+
for (const change of changesSnapshot) {
|
|
838
|
+
try {
|
|
839
|
+
await pushOne(change, ctx);
|
|
840
|
+
} catch (err) {
|
|
841
|
+
firstSyncError = firstSyncError ?? err;
|
|
842
|
+
ctx.logger.error(`[dync] push:error change=${JSON.stringify(change)}`, err);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return firstSyncError;
|
|
846
|
+
}
|
|
847
|
+
async function pushOne(change, ctx) {
|
|
848
|
+
const api = ctx.syncApis[change.stateKey];
|
|
849
|
+
if (!api) return;
|
|
850
|
+
ctx.logger.debug(`[dync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`);
|
|
851
|
+
const { action, stateKey, localId, id, changes, after } = change;
|
|
852
|
+
switch (action) {
|
|
853
|
+
case "remove" /* Remove */:
|
|
854
|
+
if (!id) {
|
|
855
|
+
ctx.logger.warn(`[dync] push:remove:no-id stateKey=${stateKey} localId=${localId}`);
|
|
856
|
+
await ctx.state.removePendingChange(localId, stateKey);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
await api.remove(id);
|
|
860
|
+
await handleRemoveSuccess(change, ctx);
|
|
861
|
+
break;
|
|
862
|
+
case "update" /* Update */: {
|
|
863
|
+
if (ctx.state.hasConflicts(localId)) {
|
|
864
|
+
ctx.logger.warn(`[dync] push:update:skipping-with-conflicts stateKey=${stateKey} localId=${localId} id=${id}`);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const exists = await api.update(id, changes, after);
|
|
868
|
+
if (exists) {
|
|
869
|
+
await handleUpdateSuccess(change, ctx);
|
|
870
|
+
} else {
|
|
871
|
+
await handleMissingRemoteRecord(change, ctx);
|
|
872
|
+
}
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
case "create" /* Create */: {
|
|
876
|
+
const result = await api.add(changes);
|
|
877
|
+
if (result) {
|
|
878
|
+
await handleCreateSuccess(change, result, ctx);
|
|
879
|
+
} else {
|
|
880
|
+
ctx.logger.warn(`[dync] push:create:no-result stateKey=${stateKey} localId=${localId} id=${id}`);
|
|
881
|
+
if (ctx.state.samePendingVersion(stateKey, localId, change.version)) {
|
|
882
|
+
await ctx.state.removePendingChange(localId, stateKey);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
async function handleMissingRemoteRecord(change, ctx) {
|
|
890
|
+
const { stateKey, localId } = change;
|
|
891
|
+
const strategy = ctx.syncOptions.missingRemoteRecordDuringUpdateStrategy;
|
|
892
|
+
let localItem;
|
|
893
|
+
await ctx.withTransaction("rw", [stateKey, DYNC_STATE_TABLE], async (tables) => {
|
|
894
|
+
const txTable = tables[stateKey];
|
|
895
|
+
localItem = await txTable.get(localId);
|
|
896
|
+
if (!localItem) {
|
|
897
|
+
ctx.logger.warn(`[dync] push:missing-remote:no-local-item stateKey=${stateKey} localId=${localId}`);
|
|
898
|
+
await ctx.state.removePendingChange(localId, stateKey);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
switch (strategy) {
|
|
902
|
+
case "delete-local-record":
|
|
903
|
+
await txTable.raw.delete(localId);
|
|
904
|
+
ctx.logger.debug(`[dync] push:missing-remote:${strategy} stateKey=${stateKey} id=${localItem.id}`);
|
|
905
|
+
break;
|
|
906
|
+
case "insert-remote-record": {
|
|
907
|
+
const newItem = {
|
|
908
|
+
...localItem,
|
|
909
|
+
_localId: createLocalId(),
|
|
910
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
911
|
+
};
|
|
912
|
+
await txTable.raw.add(newItem);
|
|
913
|
+
await txTable.raw.delete(localId);
|
|
914
|
+
await ctx.state.addPendingChange({
|
|
915
|
+
action: "create" /* Create */,
|
|
916
|
+
stateKey,
|
|
917
|
+
localId: newItem._localId,
|
|
918
|
+
changes: newItem,
|
|
919
|
+
before: null
|
|
920
|
+
});
|
|
921
|
+
ctx.logger.debug(`[dync] push:missing-remote:${strategy} stateKey=${stateKey} id=${newItem.id}`);
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
case "ignore":
|
|
925
|
+
ctx.logger.debug(`[dync] push:missing-remote:${strategy} stateKey=${stateKey} id=${localItem.id}`);
|
|
926
|
+
break;
|
|
927
|
+
default:
|
|
928
|
+
ctx.logger.error(`[dync] push:missing-remote:unknown-strategy stateKey=${stateKey} id=${localItem.id} strategy=${strategy}`);
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
await ctx.state.removePendingChange(localId, stateKey);
|
|
932
|
+
});
|
|
933
|
+
ctx.syncOptions.onAfterMissingRemoteRecordDuringUpdate?.(strategy, localItem);
|
|
934
|
+
}
|
|
935
|
+
async function pushAllBatch(ctx) {
|
|
936
|
+
let firstSyncError;
|
|
937
|
+
try {
|
|
938
|
+
const changesSnapshot = [...ctx.state.getState().pendingChanges].filter((change) => ctx.batchSync.syncTables.includes(change.stateKey)).sort((a, b) => orderFor(a.action) - orderFor(b.action));
|
|
939
|
+
if (changesSnapshot.length === 0) {
|
|
940
|
+
ctx.logger.debug("[dync] push:batch:no-changes");
|
|
941
|
+
return void 0;
|
|
942
|
+
}
|
|
943
|
+
const changesToPush = changesSnapshot.filter((change) => {
|
|
944
|
+
if (change.action === "update" /* Update */ && ctx.state.hasConflicts(change.localId)) {
|
|
945
|
+
ctx.logger.warn(`[dync] push:batch:skipping-with-conflicts stateKey=${change.stateKey} localId=${change.localId}`);
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
return true;
|
|
949
|
+
});
|
|
950
|
+
if (changesToPush.length === 0) {
|
|
951
|
+
ctx.logger.debug("[dync] push:batch:all-skipped");
|
|
952
|
+
return void 0;
|
|
953
|
+
}
|
|
954
|
+
const payloads = changesToPush.map((change) => ({
|
|
955
|
+
table: change.stateKey,
|
|
956
|
+
action: change.action === "create" /* Create */ ? "add" : change.action === "update" /* Update */ ? "update" : "remove",
|
|
957
|
+
localId: change.localId,
|
|
958
|
+
id: change.id,
|
|
959
|
+
data: change.action === "remove" /* Remove */ ? void 0 : change.changes
|
|
960
|
+
}));
|
|
961
|
+
ctx.logger.debug(`[dync] push:batch:start count=${payloads.length}`);
|
|
962
|
+
const results = await ctx.batchSync.push(payloads);
|
|
963
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
964
|
+
for (const result of results) {
|
|
965
|
+
resultMap.set(result.localId, result);
|
|
966
|
+
}
|
|
967
|
+
for (const change of changesToPush) {
|
|
968
|
+
const result = resultMap.get(change.localId);
|
|
969
|
+
if (!result) {
|
|
970
|
+
ctx.logger.warn(`[dync] push:batch:missing-result localId=${change.localId}`);
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
try {
|
|
974
|
+
await processBatchPushResult(change, result, ctx);
|
|
975
|
+
} catch (err) {
|
|
976
|
+
firstSyncError = firstSyncError ?? err;
|
|
977
|
+
ctx.logger.error(`[dync] push:batch:error localId=${change.localId}`, err);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
} catch (err) {
|
|
981
|
+
firstSyncError = err;
|
|
982
|
+
ctx.logger.error("[dync] push:batch:error", err);
|
|
983
|
+
}
|
|
984
|
+
return firstSyncError;
|
|
985
|
+
}
|
|
986
|
+
async function processBatchPushResult(change, result, ctx) {
|
|
987
|
+
const { action, stateKey, localId } = change;
|
|
988
|
+
if (!result.success) {
|
|
989
|
+
if (action === "update" /* Update */) {
|
|
990
|
+
await handleMissingRemoteRecord(change, ctx);
|
|
991
|
+
} else {
|
|
992
|
+
ctx.logger.warn(`[dync] push:batch:failed stateKey=${stateKey} localId=${localId} error=${result.error}`);
|
|
993
|
+
}
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
switch (action) {
|
|
997
|
+
case "remove" /* Remove */:
|
|
998
|
+
handleRemoveSuccess(change, ctx);
|
|
999
|
+
break;
|
|
1000
|
+
case "update" /* Update */:
|
|
1001
|
+
handleUpdateSuccess(change, ctx);
|
|
1002
|
+
break;
|
|
1003
|
+
case "create" /* Create */: {
|
|
1004
|
+
const serverResult = { id: result.id };
|
|
1005
|
+
if (result.updated_at) {
|
|
1006
|
+
serverResult.updated_at = result.updated_at;
|
|
1007
|
+
}
|
|
1008
|
+
await handleCreateSuccess(change, serverResult, ctx);
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/core/firstLoad.ts
|
|
1015
|
+
var yieldToEventLoop = () => sleep(0);
|
|
1016
|
+
var WRITE_BATCH_SIZE = 200;
|
|
1017
|
+
async function startFirstLoad(ctx) {
|
|
1018
|
+
ctx.logger.debug("[dync] Starting first load...");
|
|
1019
|
+
if (ctx.state.getState().firstLoadDone) {
|
|
1020
|
+
ctx.logger.debug("[dync] First load already completed");
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
let error;
|
|
1024
|
+
for (const [stateKey, api] of Object.entries(ctx.syncApis)) {
|
|
1025
|
+
if (!api.firstLoad) {
|
|
1026
|
+
ctx.logger.error(`[dync] firstLoad:no-api-function stateKey=${stateKey}`);
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
try {
|
|
1030
|
+
ctx.logger.info(`[dync] firstLoad:start stateKey=${stateKey}`);
|
|
1031
|
+
let lastId;
|
|
1032
|
+
let isEmptyTable = true;
|
|
1033
|
+
let batchCount = 0;
|
|
1034
|
+
let totalInserted = 0;
|
|
1035
|
+
let totalUpdated = 0;
|
|
1036
|
+
while (true) {
|
|
1037
|
+
const batch = await api.firstLoad(lastId);
|
|
1038
|
+
if (!batch?.length) break;
|
|
1039
|
+
batchCount++;
|
|
1040
|
+
const { inserted, updated } = await processBatchInChunks(ctx, stateKey, batch, isEmptyTable, lastId === void 0);
|
|
1041
|
+
totalInserted += inserted;
|
|
1042
|
+
totalUpdated += updated;
|
|
1043
|
+
if (ctx.onProgress) {
|
|
1044
|
+
ctx.onProgress({
|
|
1045
|
+
table: stateKey,
|
|
1046
|
+
inserted: totalInserted,
|
|
1047
|
+
updated: totalUpdated,
|
|
1048
|
+
total: totalInserted + totalUpdated
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
if (lastId === void 0) {
|
|
1052
|
+
isEmptyTable = await ctx.table(stateKey).count() === batch.length;
|
|
1053
|
+
}
|
|
1054
|
+
if (lastId !== void 0 && lastId === batch[batch.length - 1].id) {
|
|
1055
|
+
throw new Error(`Duplicate records downloaded, stopping to prevent infinite loop`);
|
|
1056
|
+
}
|
|
1057
|
+
lastId = batch[batch.length - 1].id;
|
|
1058
|
+
if (batchCount % 5 === 0) {
|
|
1059
|
+
await yieldToEventLoop();
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
ctx.logger.info(`[dync] firstLoad:done stateKey=${stateKey} inserted=${totalInserted} updated=${totalUpdated}`);
|
|
1063
|
+
} catch (err) {
|
|
1064
|
+
error = error ?? err;
|
|
1065
|
+
ctx.logger.error(`[dync] firstLoad:error stateKey=${stateKey}`, err);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
await ctx.state.setState((syncState) => ({
|
|
1069
|
+
...syncState,
|
|
1070
|
+
firstLoadDone: true,
|
|
1071
|
+
error
|
|
1072
|
+
}));
|
|
1073
|
+
ctx.logger.debug("[dync] First load completed");
|
|
1074
|
+
}
|
|
1075
|
+
async function processBatchInChunks(ctx, stateKey, batch, isEmptyTable, isFirstBatch) {
|
|
1076
|
+
let newest = new Date(ctx.state.getState().lastPulled[stateKey] || 0);
|
|
1077
|
+
return ctx.withTransaction("rw", [stateKey, DYNC_STATE_TABLE], async (tables) => {
|
|
1078
|
+
const txTable = tables[stateKey];
|
|
1079
|
+
let tableIsEmpty = isEmptyTable;
|
|
1080
|
+
if (isFirstBatch) {
|
|
1081
|
+
const count = await txTable.count();
|
|
1082
|
+
tableIsEmpty = count === 0;
|
|
1083
|
+
}
|
|
1084
|
+
const activeRecords = [];
|
|
1085
|
+
for (const remote of batch) {
|
|
1086
|
+
const remoteUpdated = new Date(remote.updated_at || 0);
|
|
1087
|
+
if (remoteUpdated > newest) newest = remoteUpdated;
|
|
1088
|
+
if (remote.deleted) continue;
|
|
1089
|
+
delete remote.deleted;
|
|
1090
|
+
remote._localId = createLocalId();
|
|
1091
|
+
activeRecords.push(remote);
|
|
1092
|
+
}
|
|
1093
|
+
let inserted = 0;
|
|
1094
|
+
let updated = 0;
|
|
1095
|
+
if (tableIsEmpty) {
|
|
1096
|
+
for (let i = 0; i < activeRecords.length; i += WRITE_BATCH_SIZE) {
|
|
1097
|
+
const chunk = activeRecords.slice(i, i + WRITE_BATCH_SIZE);
|
|
1098
|
+
await txTable.raw.bulkAdd(chunk);
|
|
1099
|
+
inserted += chunk.length;
|
|
1100
|
+
}
|
|
1101
|
+
} else {
|
|
1102
|
+
for (let i = 0; i < activeRecords.length; i += WRITE_BATCH_SIZE) {
|
|
1103
|
+
const chunk = activeRecords.slice(i, i + WRITE_BATCH_SIZE);
|
|
1104
|
+
const chunkResult = await processChunkWithLookup(txTable, chunk);
|
|
1105
|
+
inserted += chunkResult.inserted;
|
|
1106
|
+
updated += chunkResult.updated;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
await ctx.state.setState((syncState) => ({
|
|
1110
|
+
...syncState,
|
|
1111
|
+
lastPulled: {
|
|
1112
|
+
...syncState.lastPulled,
|
|
1113
|
+
[stateKey]: newest.toISOString()
|
|
1114
|
+
}
|
|
1115
|
+
}));
|
|
1116
|
+
return { inserted, updated };
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
async function processChunkWithLookup(txTable, chunk) {
|
|
1120
|
+
const serverIds = chunk.filter((r) => r.id != null).map((r) => r.id);
|
|
1121
|
+
const existingByServerId = /* @__PURE__ */ new Map();
|
|
1122
|
+
if (serverIds.length > 0) {
|
|
1123
|
+
const existingRecords = await txTable.where("id").anyOf(serverIds).toArray();
|
|
1124
|
+
for (const existing of existingRecords) {
|
|
1125
|
+
existingByServerId.set(existing.id, existing);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const toAdd = [];
|
|
1129
|
+
let updated = 0;
|
|
1130
|
+
for (const remote of chunk) {
|
|
1131
|
+
const existing = remote.id != null ? existingByServerId.get(remote.id) : void 0;
|
|
1132
|
+
if (existing) {
|
|
1133
|
+
const merged = Object.assign({}, existing, remote, { _localId: existing._localId });
|
|
1134
|
+
await txTable.raw.update(existing._localId, merged);
|
|
1135
|
+
updated++;
|
|
1136
|
+
} else {
|
|
1137
|
+
toAdd.push(remote);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (toAdd.length > 0) {
|
|
1141
|
+
await txTable.raw.bulkAdd(toAdd);
|
|
1142
|
+
}
|
|
1143
|
+
existingByServerId.clear();
|
|
1144
|
+
return { inserted: toAdd.length, updated };
|
|
1145
|
+
}
|
|
1146
|
+
async function startFirstLoadBatch(ctx) {
|
|
1147
|
+
ctx.logger.debug("[dync] Starting batch first load...");
|
|
1148
|
+
if (ctx.state.getState().firstLoadDone) {
|
|
1149
|
+
ctx.logger.debug("[dync] First load already completed");
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (!ctx.batchSync.firstLoad) {
|
|
1153
|
+
ctx.logger.warn("[dync] firstLoad:batch:no-firstLoad-function");
|
|
1154
|
+
await ctx.state.setState((syncState) => ({
|
|
1155
|
+
...syncState,
|
|
1156
|
+
firstLoadDone: true
|
|
1157
|
+
}));
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
let error;
|
|
1161
|
+
try {
|
|
1162
|
+
ctx.logger.info(`[dync] firstLoad:batch:start tables=${[...ctx.batchSync.syncTables].join(",")}`);
|
|
1163
|
+
const progress = {};
|
|
1164
|
+
for (const tableName of ctx.batchSync.syncTables) {
|
|
1165
|
+
progress[tableName] = { inserted: 0, updated: 0 };
|
|
1166
|
+
}
|
|
1167
|
+
let cursors = {};
|
|
1168
|
+
for (const tableName of ctx.batchSync.syncTables) {
|
|
1169
|
+
cursors[tableName] = void 0;
|
|
1170
|
+
}
|
|
1171
|
+
let batchCount = 0;
|
|
1172
|
+
while (true) {
|
|
1173
|
+
const result = await ctx.batchSync.firstLoad(cursors);
|
|
1174
|
+
if (!result.hasMore && Object.values(result.data).every((d) => !d?.length)) {
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
batchCount++;
|
|
1178
|
+
for (const [stateKey, batch] of Object.entries(result.data)) {
|
|
1179
|
+
if (!ctx.batchSync.syncTables.includes(stateKey)) {
|
|
1180
|
+
ctx.logger.warn(`[dync] firstLoad:batch:unknown-table stateKey=${stateKey}`);
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
if (!batch?.length) continue;
|
|
1184
|
+
const isFirstBatch = progress[stateKey].inserted === 0 && progress[stateKey].updated === 0;
|
|
1185
|
+
const isEmptyTable = isFirstBatch && await ctx.table(stateKey).count() === 0;
|
|
1186
|
+
const { inserted, updated } = await processBatchInChunks(ctx, stateKey, batch, isEmptyTable, isFirstBatch);
|
|
1187
|
+
progress[stateKey].inserted += inserted;
|
|
1188
|
+
progress[stateKey].updated += updated;
|
|
1189
|
+
if (ctx.onProgress) {
|
|
1190
|
+
ctx.onProgress({
|
|
1191
|
+
table: stateKey,
|
|
1192
|
+
inserted: progress[stateKey].inserted,
|
|
1193
|
+
updated: progress[stateKey].updated,
|
|
1194
|
+
total: progress[stateKey].inserted + progress[stateKey].updated
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
cursors = result.cursors;
|
|
1199
|
+
if (batchCount % 5 === 0) {
|
|
1200
|
+
await yieldToEventLoop();
|
|
1201
|
+
}
|
|
1202
|
+
if (!result.hasMore) {
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
for (const [stateKey, p] of Object.entries(progress)) {
|
|
1207
|
+
ctx.logger.info(`[dync] firstLoad:batch:done stateKey=${stateKey} inserted=${p.inserted} updated=${p.updated}`);
|
|
1208
|
+
}
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
error = err;
|
|
1211
|
+
ctx.logger.error("[dync] firstLoad:batch:error", err);
|
|
1212
|
+
}
|
|
1213
|
+
await ctx.state.setState((syncState) => ({
|
|
1214
|
+
...syncState,
|
|
1215
|
+
firstLoadDone: true,
|
|
1216
|
+
error
|
|
1217
|
+
}));
|
|
1218
|
+
ctx.logger.debug("[dync] Batch first load completed");
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// src/index.shared.ts
|
|
1222
|
+
var DEFAULT_SYNC_INTERVAL_MILLIS = 2e3;
|
|
1223
|
+
var DEFAULT_LOGGER = console;
|
|
1224
|
+
var DEFAULT_MIN_LOG_LEVEL = "debug";
|
|
1225
|
+
var DEFAULT_MISSING_REMOTE_RECORD_STRATEGY = "insert-remote-record";
|
|
1226
|
+
var DEFAULT_CONFLICT_RESOLUTION_STRATEGY = "try-shallow-merge";
|
|
1227
|
+
var DyncBase = class {
|
|
1228
|
+
adapter;
|
|
1229
|
+
tableCache = /* @__PURE__ */ new Map();
|
|
1230
|
+
mutationWrappedTables = /* @__PURE__ */ new Set();
|
|
1231
|
+
syncEnhancedTables = /* @__PURE__ */ new Set();
|
|
1232
|
+
mutationListeners = /* @__PURE__ */ new Set();
|
|
1233
|
+
visibilitySubscription;
|
|
1234
|
+
openPromise;
|
|
1235
|
+
disableSyncPromise;
|
|
1236
|
+
disableSyncPromiseResolver;
|
|
1237
|
+
sleepAbortController;
|
|
1238
|
+
closing = false;
|
|
1239
|
+
// Per-table sync mode
|
|
1240
|
+
syncApis = {};
|
|
1241
|
+
// Batch sync mode
|
|
1242
|
+
batchSync;
|
|
1243
|
+
syncedTables = /* @__PURE__ */ new Set();
|
|
1244
|
+
syncOptions;
|
|
1245
|
+
logger;
|
|
1246
|
+
syncTimerStarted = false;
|
|
1247
|
+
mutationsDuringSync = false;
|
|
1248
|
+
state;
|
|
1249
|
+
name;
|
|
1250
|
+
constructor(databaseName, syncApisOrBatchSync, storageAdapter, options) {
|
|
1251
|
+
const isBatchMode = typeof syncApisOrBatchSync.push === "function";
|
|
1252
|
+
if (isBatchMode) {
|
|
1253
|
+
this.batchSync = syncApisOrBatchSync;
|
|
1254
|
+
this.syncedTables = new Set(this.batchSync.syncTables);
|
|
1255
|
+
} else {
|
|
1256
|
+
this.syncApis = syncApisOrBatchSync;
|
|
1257
|
+
this.syncedTables = new Set(Object.keys(this.syncApis));
|
|
1258
|
+
}
|
|
1259
|
+
this.adapter = storageAdapter;
|
|
1260
|
+
this.name = databaseName;
|
|
1261
|
+
this.syncOptions = {
|
|
1262
|
+
syncInterval: DEFAULT_SYNC_INTERVAL_MILLIS,
|
|
1263
|
+
logger: DEFAULT_LOGGER,
|
|
1264
|
+
minLogLevel: DEFAULT_MIN_LOG_LEVEL,
|
|
1265
|
+
missingRemoteRecordDuringUpdateStrategy: DEFAULT_MISSING_REMOTE_RECORD_STRATEGY,
|
|
1266
|
+
conflictResolutionStrategy: DEFAULT_CONFLICT_RESOLUTION_STRATEGY,
|
|
1267
|
+
...options ?? {}
|
|
1268
|
+
};
|
|
1269
|
+
this.logger = newLogger(this.syncOptions.logger, this.syncOptions.minLogLevel);
|
|
1270
|
+
this.state = new StateManager({
|
|
1271
|
+
storageAdapter: this.adapter
|
|
1272
|
+
});
|
|
1273
|
+
const driverInfo = "driverType" in this.adapter ? ` (Driver: ${this.adapter.driverType})` : "";
|
|
1274
|
+
this.logger.debug(`[dync] Initialized with ${this.adapter.type}${driverInfo}`);
|
|
1275
|
+
}
|
|
1276
|
+
version(versionNumber) {
|
|
1277
|
+
const self = this;
|
|
1278
|
+
const schemaOptions = {};
|
|
1279
|
+
let storesDefined = false;
|
|
1280
|
+
const builder = {
|
|
1281
|
+
stores(schema) {
|
|
1282
|
+
const usesStructuredSchema = Object.values(schema).some((def) => typeof def !== "string");
|
|
1283
|
+
const stateTableSchema = usesStructuredSchema ? {
|
|
1284
|
+
columns: {
|
|
1285
|
+
[LOCAL_PK]: { type: "TEXT" },
|
|
1286
|
+
value: { type: "TEXT" }
|
|
1287
|
+
}
|
|
1288
|
+
} : LOCAL_PK;
|
|
1289
|
+
const fullSchema = {
|
|
1290
|
+
...schema,
|
|
1291
|
+
[DYNC_STATE_TABLE]: stateTableSchema
|
|
1292
|
+
};
|
|
1293
|
+
for (const [tableName, tableSchema] of Object.entries(schema)) {
|
|
1294
|
+
const isSyncTable = self.syncedTables.has(tableName);
|
|
1295
|
+
if (typeof tableSchema === "string") {
|
|
1296
|
+
if (isSyncTable) {
|
|
1297
|
+
fullSchema[tableName] = `${LOCAL_PK}, &${SERVER_PK}, ${tableSchema}, ${UPDATED_AT}`;
|
|
1298
|
+
}
|
|
1299
|
+
self.logger.debug(
|
|
1300
|
+
`[dync] Defining ${isSyncTable ? "" : "non-"}sync table '${tableName}' with primary key & indexes '${fullSchema[tableName]}'`
|
|
1301
|
+
);
|
|
1302
|
+
} else {
|
|
1303
|
+
if (isSyncTable) {
|
|
1304
|
+
fullSchema[tableName] = self.injectSyncColumns(tableSchema);
|
|
1305
|
+
}
|
|
1306
|
+
const schemaColumns = Object.keys(fullSchema[tableName].columns ?? {}).join(", ");
|
|
1307
|
+
const schemaIndexes = (fullSchema[tableName].indexes ?? []).map((idx) => idx.columns.join("+")).join(", ");
|
|
1308
|
+
self.logger.debug(
|
|
1309
|
+
`[dync] Defining ${isSyncTable ? "" : "non-"}sync table '${tableName}' with columns ${schemaColumns} and indexes ${schemaIndexes}`
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
storesDefined = true;
|
|
1314
|
+
self.adapter.defineSchema(versionNumber, fullSchema, schemaOptions);
|
|
1315
|
+
self.setupEnhancedTables(Object.keys(schema));
|
|
1316
|
+
return builder;
|
|
1317
|
+
},
|
|
1318
|
+
sqlite(configure) {
|
|
1319
|
+
if (!storesDefined) {
|
|
1320
|
+
throw new Error("Call stores() before registering sqlite migrations");
|
|
1321
|
+
}
|
|
1322
|
+
const sqliteOptions = schemaOptions.sqlite ??= {};
|
|
1323
|
+
const migrations = sqliteOptions.migrations ??= {};
|
|
1324
|
+
const configurator = {
|
|
1325
|
+
upgrade(handler) {
|
|
1326
|
+
migrations.upgrade = handler;
|
|
1327
|
+
},
|
|
1328
|
+
downgrade(handler) {
|
|
1329
|
+
migrations.downgrade = handler;
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
configure(configurator);
|
|
1333
|
+
return builder;
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1336
|
+
return builder;
|
|
1337
|
+
}
|
|
1338
|
+
async open() {
|
|
1339
|
+
if (this.closing) {
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
if (this.openPromise) {
|
|
1343
|
+
return this.openPromise;
|
|
1344
|
+
}
|
|
1345
|
+
this.openPromise = (async () => {
|
|
1346
|
+
if (this.closing) return;
|
|
1347
|
+
await this.adapter.open();
|
|
1348
|
+
if (this.closing) return;
|
|
1349
|
+
await this.state.hydrate();
|
|
1350
|
+
})();
|
|
1351
|
+
return this.openPromise;
|
|
1352
|
+
}
|
|
1353
|
+
async close() {
|
|
1354
|
+
this.closing = true;
|
|
1355
|
+
if (this.openPromise) {
|
|
1356
|
+
await this.openPromise.catch(() => {
|
|
1357
|
+
});
|
|
1358
|
+
this.openPromise = void 0;
|
|
1359
|
+
}
|
|
1360
|
+
await this.enableSync(false);
|
|
1361
|
+
await this.adapter.close();
|
|
1362
|
+
this.tableCache.clear();
|
|
1363
|
+
this.mutationWrappedTables.clear();
|
|
1364
|
+
this.syncEnhancedTables.clear();
|
|
1365
|
+
}
|
|
1366
|
+
async delete() {
|
|
1367
|
+
await this.adapter.delete();
|
|
1368
|
+
this.tableCache.clear();
|
|
1369
|
+
this.mutationWrappedTables.clear();
|
|
1370
|
+
this.syncEnhancedTables.clear();
|
|
1371
|
+
}
|
|
1372
|
+
async query(callback) {
|
|
1373
|
+
return this.adapter.query(callback);
|
|
1374
|
+
}
|
|
1375
|
+
table(name) {
|
|
1376
|
+
if (this.tableCache.has(name)) {
|
|
1377
|
+
return this.tableCache.get(name);
|
|
1378
|
+
}
|
|
1379
|
+
const table = this.adapter.table(name);
|
|
1380
|
+
const isSyncTable = this.syncedTables.has(name);
|
|
1381
|
+
if (isSyncTable && !this.syncEnhancedTables.has(name)) {
|
|
1382
|
+
this.enhanceSyncTable(table, name);
|
|
1383
|
+
} else if (!isSyncTable && !this.mutationWrappedTables.has(name) && name !== DYNC_STATE_TABLE) {
|
|
1384
|
+
wrapWithMutationEmitter(table, name, this.emitMutation.bind(this));
|
|
1385
|
+
this.mutationWrappedTables.add(name);
|
|
1386
|
+
}
|
|
1387
|
+
this.tableCache.set(name, table);
|
|
1388
|
+
return table;
|
|
1389
|
+
}
|
|
1390
|
+
async withTransaction(mode, tableNames, fn) {
|
|
1391
|
+
await this.open();
|
|
1392
|
+
return this.adapter.transaction(mode, tableNames, async () => {
|
|
1393
|
+
const tables = {};
|
|
1394
|
+
for (const tableName of tableNames) {
|
|
1395
|
+
tables[tableName] = this.table(tableName);
|
|
1396
|
+
}
|
|
1397
|
+
return fn(tables);
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
setupEnhancedTables(tableNames) {
|
|
1401
|
+
setupEnhancedTables(
|
|
1402
|
+
{
|
|
1403
|
+
owner: this,
|
|
1404
|
+
tableCache: this.tableCache,
|
|
1405
|
+
enhancedTables: this.syncEnhancedTables,
|
|
1406
|
+
getTable: (name) => this.table(name)
|
|
1407
|
+
},
|
|
1408
|
+
tableNames
|
|
1409
|
+
);
|
|
1410
|
+
for (const tableName of tableNames) {
|
|
1411
|
+
this.mutationWrappedTables.delete(tableName);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
injectSyncColumns(schema) {
|
|
1415
|
+
const columns = schema.columns ?? {};
|
|
1416
|
+
if (columns[LOCAL_PK]) {
|
|
1417
|
+
throw new Error(`Column '${LOCAL_PK}' is auto-injected for sync tables and cannot be defined manually.`);
|
|
1418
|
+
}
|
|
1419
|
+
if (columns[SERVER_PK]) {
|
|
1420
|
+
throw new Error(`Column '${SERVER_PK}' is auto-injected for sync tables and cannot be defined manually.`);
|
|
1421
|
+
}
|
|
1422
|
+
if (columns[UPDATED_AT]) {
|
|
1423
|
+
throw new Error(`Column '${UPDATED_AT}' is auto-injected for sync tables and cannot be defined manually.`);
|
|
1424
|
+
}
|
|
1425
|
+
const injectedColumns = {
|
|
1426
|
+
...columns,
|
|
1427
|
+
[LOCAL_PK]: { type: "TEXT" },
|
|
1428
|
+
[SERVER_PK]: { type: "INTEGER", unique: true },
|
|
1429
|
+
[UPDATED_AT]: { type: "TEXT" }
|
|
1430
|
+
};
|
|
1431
|
+
const userIndexes = schema.indexes ?? [];
|
|
1432
|
+
const hasUpdatedAtIndex = userIndexes.some((idx) => idx.columns.length === 1 && idx.columns[0] === UPDATED_AT);
|
|
1433
|
+
const injectedIndexes = hasUpdatedAtIndex ? userIndexes : [...userIndexes, { columns: [UPDATED_AT] }];
|
|
1434
|
+
return {
|
|
1435
|
+
...schema,
|
|
1436
|
+
columns: injectedColumns,
|
|
1437
|
+
indexes: injectedIndexes
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
enhanceSyncTable(table, tableName) {
|
|
1441
|
+
enhanceSyncTable({
|
|
1442
|
+
table,
|
|
1443
|
+
tableName,
|
|
1444
|
+
withTransaction: this.withTransaction.bind(this),
|
|
1445
|
+
state: this.state,
|
|
1446
|
+
enhancedTables: this.syncEnhancedTables,
|
|
1447
|
+
emitMutation: this.emitMutation.bind(this)
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
async syncOnce() {
|
|
1451
|
+
if (this.closing) {
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (this.syncStatus === "syncing") {
|
|
1455
|
+
this.mutationsDuringSync = true;
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
this.syncStatus = "syncing";
|
|
1459
|
+
this.mutationsDuringSync = false;
|
|
1460
|
+
const pullResult = await this.pullAll();
|
|
1461
|
+
const firstPushSyncError = await this.pushAll();
|
|
1462
|
+
for (const tableName of pullResult.changedTables) {
|
|
1463
|
+
this.emitMutation({ type: "pull", tableName });
|
|
1464
|
+
}
|
|
1465
|
+
this.syncStatus = "idle";
|
|
1466
|
+
await this.state.setState((syncState) => ({
|
|
1467
|
+
...syncState,
|
|
1468
|
+
error: pullResult.error ?? firstPushSyncError
|
|
1469
|
+
}));
|
|
1470
|
+
if (this.mutationsDuringSync) {
|
|
1471
|
+
this.mutationsDuringSync = false;
|
|
1472
|
+
this.syncOnce().catch(() => {
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
async pullAll() {
|
|
1477
|
+
const baseContext = {
|
|
1478
|
+
logger: this.logger,
|
|
1479
|
+
state: this.state,
|
|
1480
|
+
table: this.table.bind(this),
|
|
1481
|
+
withTransaction: this.withTransaction.bind(this),
|
|
1482
|
+
conflictResolutionStrategy: this.syncOptions.conflictResolutionStrategy
|
|
1483
|
+
};
|
|
1484
|
+
if (this.batchSync) {
|
|
1485
|
+
return pullAllBatch({
|
|
1486
|
+
...baseContext,
|
|
1487
|
+
batchSync: this.batchSync
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
return pullAll({
|
|
1491
|
+
...baseContext,
|
|
1492
|
+
syncApis: this.syncApis
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
async pushAll() {
|
|
1496
|
+
const baseContext = {
|
|
1497
|
+
logger: this.logger,
|
|
1498
|
+
state: this.state,
|
|
1499
|
+
table: this.table.bind(this),
|
|
1500
|
+
withTransaction: this.withTransaction.bind(this),
|
|
1501
|
+
syncOptions: this.syncOptions
|
|
1502
|
+
};
|
|
1503
|
+
if (this.batchSync) {
|
|
1504
|
+
return pushAllBatch({
|
|
1505
|
+
...baseContext,
|
|
1506
|
+
batchSync: this.batchSync
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
return pushAll({
|
|
1510
|
+
...baseContext,
|
|
1511
|
+
syncApis: this.syncApis
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
startSyncTimer(start) {
|
|
1515
|
+
if (start) {
|
|
1516
|
+
void this.tryStart();
|
|
1517
|
+
} else {
|
|
1518
|
+
this.syncTimerStarted = false;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
async tryStart() {
|
|
1522
|
+
if (this.syncTimerStarted) return;
|
|
1523
|
+
this.syncTimerStarted = true;
|
|
1524
|
+
while (this.syncTimerStarted) {
|
|
1525
|
+
this.sleepAbortController = new AbortController();
|
|
1526
|
+
await this.syncOnce();
|
|
1527
|
+
await sleep(this.syncOptions.syncInterval, this.sleepAbortController.signal);
|
|
1528
|
+
}
|
|
1529
|
+
this.syncStatus = "disabled";
|
|
1530
|
+
this.disableSyncPromiseResolver?.();
|
|
1531
|
+
}
|
|
1532
|
+
setupVisibilityListener(add) {
|
|
1533
|
+
this.visibilitySubscription = addVisibilityChangeListener(add, this.visibilitySubscription, (isVisible) => this.handleVisibilityChange(isVisible));
|
|
1534
|
+
}
|
|
1535
|
+
handleVisibilityChange(isVisible) {
|
|
1536
|
+
if (isVisible) {
|
|
1537
|
+
this.logger.debug("[dync] sync:start-in-foreground");
|
|
1538
|
+
this.startSyncTimer(true);
|
|
1539
|
+
} else {
|
|
1540
|
+
this.logger.debug("[dync] sync:pause-in-background");
|
|
1541
|
+
this.startSyncTimer(false);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
async startFirstLoad(onProgress) {
|
|
1545
|
+
await this.open();
|
|
1546
|
+
const baseContext = {
|
|
1547
|
+
logger: this.logger,
|
|
1548
|
+
state: this.state,
|
|
1549
|
+
table: this.table.bind(this),
|
|
1550
|
+
withTransaction: this.withTransaction.bind(this),
|
|
1551
|
+
onProgress
|
|
1552
|
+
};
|
|
1553
|
+
if (this.batchSync) {
|
|
1554
|
+
await startFirstLoadBatch({
|
|
1555
|
+
...baseContext,
|
|
1556
|
+
batchSync: this.batchSync
|
|
1557
|
+
});
|
|
1558
|
+
} else {
|
|
1559
|
+
await startFirstLoad({
|
|
1560
|
+
...baseContext,
|
|
1561
|
+
syncApis: this.syncApis
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
for (const tableName of this.syncedTables) {
|
|
1565
|
+
this.emitMutation({ type: "pull", tableName });
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
getSyncState() {
|
|
1569
|
+
return this.state.getSyncState();
|
|
1570
|
+
}
|
|
1571
|
+
async resolveConflict(localId, keepLocal) {
|
|
1572
|
+
const conflict = this.state.getState().conflicts?.[localId];
|
|
1573
|
+
if (!conflict) {
|
|
1574
|
+
this.logger.warn(`[dync] No conflict found for localId: ${localId}`);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
await this.withTransaction("rw", [conflict.stateKey, DYNC_STATE_TABLE], async (tables) => {
|
|
1578
|
+
const txTable = tables[conflict.stateKey];
|
|
1579
|
+
if (!keepLocal) {
|
|
1580
|
+
const item = await txTable.get(localId);
|
|
1581
|
+
if (item) {
|
|
1582
|
+
for (const field of conflict.fields) {
|
|
1583
|
+
item[field.key] = field.remoteValue;
|
|
1584
|
+
}
|
|
1585
|
+
await txTable.raw.update(localId, item);
|
|
1586
|
+
} else {
|
|
1587
|
+
this.logger.warn(`[dync] No local item found for localId: ${localId} to apply remote values`);
|
|
1588
|
+
}
|
|
1589
|
+
await this.state.setState((syncState) => ({
|
|
1590
|
+
...syncState,
|
|
1591
|
+
pendingChanges: syncState.pendingChanges.filter((p) => !(p.localId === localId && p.stateKey === conflict.stateKey))
|
|
1592
|
+
}));
|
|
1593
|
+
}
|
|
1594
|
+
await this.state.setState((syncState) => {
|
|
1595
|
+
const ss = { ...syncState };
|
|
1596
|
+
delete ss.conflicts?.[localId];
|
|
1597
|
+
return ss;
|
|
1598
|
+
});
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
async enableSync(enabled) {
|
|
1602
|
+
if (!enabled) {
|
|
1603
|
+
if (this.syncTimerStarted) {
|
|
1604
|
+
this.disableSyncPromise = new Promise((resolve) => {
|
|
1605
|
+
this.disableSyncPromiseResolver = resolve;
|
|
1606
|
+
});
|
|
1607
|
+
this.sleepAbortController?.abort();
|
|
1608
|
+
this.syncStatus = "disabling";
|
|
1609
|
+
this.startSyncTimer(false);
|
|
1610
|
+
this.setupVisibilityListener(false);
|
|
1611
|
+
return this.disableSyncPromise;
|
|
1612
|
+
}
|
|
1613
|
+
this.syncStatus = "disabled";
|
|
1614
|
+
this.setupVisibilityListener(false);
|
|
1615
|
+
return Promise.resolve();
|
|
1616
|
+
}
|
|
1617
|
+
this.syncStatus = "idle";
|
|
1618
|
+
this.startSyncTimer(true);
|
|
1619
|
+
this.setupVisibilityListener(true);
|
|
1620
|
+
return Promise.resolve();
|
|
1621
|
+
}
|
|
1622
|
+
get syncStatus() {
|
|
1623
|
+
return this.state.getSyncStatus();
|
|
1624
|
+
}
|
|
1625
|
+
set syncStatus(status) {
|
|
1626
|
+
this.state.setSyncStatus(status);
|
|
1627
|
+
}
|
|
1628
|
+
onSyncStateChange(fn) {
|
|
1629
|
+
return this.state.subscribe(fn);
|
|
1630
|
+
}
|
|
1631
|
+
onMutation(fn) {
|
|
1632
|
+
this.mutationListeners.add(fn);
|
|
1633
|
+
return () => this.mutationListeners.delete(fn);
|
|
1634
|
+
}
|
|
1635
|
+
emitMutation(event) {
|
|
1636
|
+
if (event.type === "add" || event.type === "update" || event.type === "delete") {
|
|
1637
|
+
if (this.syncTimerStarted) {
|
|
1638
|
+
this.syncOnce().catch(() => {
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
for (const listener of this.mutationListeners) {
|
|
1643
|
+
listener(event);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
// Public API
|
|
1647
|
+
sync = {
|
|
1648
|
+
enable: this.enableSync.bind(this),
|
|
1649
|
+
startFirstLoad: this.startFirstLoad.bind(this),
|
|
1650
|
+
getState: this.getSyncState.bind(this),
|
|
1651
|
+
resolveConflict: this.resolveConflict.bind(this),
|
|
1652
|
+
onStateChange: this.onSyncStateChange.bind(this),
|
|
1653
|
+
onMutation: this.onMutation.bind(this)
|
|
1654
|
+
};
|
|
1655
|
+
};
|
|
1656
|
+
var DyncConstructor = DyncBase;
|
|
1657
|
+
var Dync = DyncConstructor;
|
|
1658
|
+
|
|
1659
|
+
// src/storage/memory/MemoryQueryContext.ts
|
|
1660
|
+
var MemoryQueryContext = class {
|
|
1661
|
+
constructor(adapter) {
|
|
1662
|
+
this.adapter = adapter;
|
|
1663
|
+
}
|
|
1664
|
+
table(name) {
|
|
1665
|
+
return this.adapter.table(name);
|
|
1666
|
+
}
|
|
1667
|
+
transaction(mode, tableNames, callback) {
|
|
1668
|
+
return this.adapter.transaction(mode, tableNames, callback);
|
|
1669
|
+
}
|
|
1670
|
+
};
|
|
1671
|
+
|
|
1672
|
+
// src/storage/memory/types.ts
|
|
1673
|
+
var createDefaultState = () => ({
|
|
1674
|
+
predicate: () => true,
|
|
1675
|
+
orderBy: void 0,
|
|
1676
|
+
reverse: false,
|
|
1677
|
+
offset: 0,
|
|
1678
|
+
limit: void 0,
|
|
1679
|
+
distinct: false
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
// src/storage/memory/MemoryCollection.ts
|
|
1683
|
+
var MemoryCollection = class _MemoryCollection {
|
|
1684
|
+
table;
|
|
1685
|
+
state;
|
|
1686
|
+
constructor(table, state) {
|
|
1687
|
+
this.table = table;
|
|
1688
|
+
this.state = state;
|
|
1689
|
+
}
|
|
1690
|
+
getState() {
|
|
1691
|
+
return { ...this.state };
|
|
1692
|
+
}
|
|
1693
|
+
matches(record, key) {
|
|
1694
|
+
return this.state.predicate(record, key);
|
|
1695
|
+
}
|
|
1696
|
+
clone(_props) {
|
|
1697
|
+
return new _MemoryCollection(this.table, { ...this.state });
|
|
1698
|
+
}
|
|
1699
|
+
reverse() {
|
|
1700
|
+
return this.withState({ reverse: !this.state.reverse });
|
|
1701
|
+
}
|
|
1702
|
+
offset(offset) {
|
|
1703
|
+
return this.withState({ offset });
|
|
1704
|
+
}
|
|
1705
|
+
limit(count) {
|
|
1706
|
+
return this.withState({ limit: count });
|
|
1707
|
+
}
|
|
1708
|
+
toCollection() {
|
|
1709
|
+
return this.clone();
|
|
1710
|
+
}
|
|
1711
|
+
distinct() {
|
|
1712
|
+
return this.withState({ distinct: true });
|
|
1713
|
+
}
|
|
1714
|
+
jsFilter(predicate) {
|
|
1715
|
+
return this.withState({ predicate: this.combinePredicate(predicate, "and") });
|
|
1716
|
+
}
|
|
1717
|
+
or(index) {
|
|
1718
|
+
return this.table.createWhereClause(index, this);
|
|
1719
|
+
}
|
|
1720
|
+
async first() {
|
|
1721
|
+
const entries = this.materializeEntries(true);
|
|
1722
|
+
return entries.at(0)?.[1];
|
|
1723
|
+
}
|
|
1724
|
+
async last() {
|
|
1725
|
+
const entries = this.materializeEntries(true);
|
|
1726
|
+
return entries.at(-1)?.[1];
|
|
1727
|
+
}
|
|
1728
|
+
async each(callback) {
|
|
1729
|
+
const entries = this.materializeEntries(true);
|
|
1730
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
1731
|
+
const [, record] = entries[index];
|
|
1732
|
+
await callback(record, index);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
async eachKey(callback) {
|
|
1736
|
+
const keys = await this.keys();
|
|
1737
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
1738
|
+
await callback(keys[index], index);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
async eachPrimaryKey(callback) {
|
|
1742
|
+
const keys = await this.primaryKeys();
|
|
1743
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
1744
|
+
await callback(keys[index], index);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
async eachUniqueKey(callback) {
|
|
1748
|
+
const keys = await this.uniqueKeys();
|
|
1749
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
1750
|
+
await callback(keys[index], index);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
async keys() {
|
|
1754
|
+
return this.materializeEntries(false).map(([key, record]) => this.table.resolvePublicKey(record, key));
|
|
1755
|
+
}
|
|
1756
|
+
async primaryKeys() {
|
|
1757
|
+
return this.keys();
|
|
1758
|
+
}
|
|
1759
|
+
async uniqueKeys() {
|
|
1760
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1761
|
+
const keys = [];
|
|
1762
|
+
for (const [key, record] of this.materializeEntries(false)) {
|
|
1763
|
+
const publicKey = this.table.resolvePublicKey(record, key);
|
|
1764
|
+
const signature = JSON.stringify(publicKey);
|
|
1765
|
+
if (!seen.has(signature)) {
|
|
1766
|
+
seen.add(signature);
|
|
1767
|
+
keys.push(publicKey);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
return keys;
|
|
1771
|
+
}
|
|
1772
|
+
async count() {
|
|
1773
|
+
return this.materializeEntries(false).length;
|
|
1774
|
+
}
|
|
1775
|
+
async sortBy(key) {
|
|
1776
|
+
const entries = this.materializeEntries(true);
|
|
1777
|
+
entries.sort((a, b) => this.table.compareValues(a[1][key], b[1][key]));
|
|
1778
|
+
return entries.map(([, record]) => record);
|
|
1779
|
+
}
|
|
1780
|
+
async delete() {
|
|
1781
|
+
const entries = this.materializeEntries(false);
|
|
1782
|
+
for (const [key] of entries) {
|
|
1783
|
+
this.table.deleteByKey(key);
|
|
1784
|
+
}
|
|
1785
|
+
return entries.length;
|
|
1786
|
+
}
|
|
1787
|
+
async modify(changes) {
|
|
1788
|
+
const entries = this.materializeEntries(false);
|
|
1789
|
+
let modified = 0;
|
|
1790
|
+
for (const [key] of entries) {
|
|
1791
|
+
const current = this.table.getMutableRecord(key);
|
|
1792
|
+
if (!current) {
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
if (typeof changes === "function") {
|
|
1796
|
+
const clone = this.table.cloneRecord(current);
|
|
1797
|
+
await changes(clone);
|
|
1798
|
+
clone._localId = current._localId ?? key;
|
|
1799
|
+
this.table.setMutableRecord(key, clone);
|
|
1800
|
+
} else {
|
|
1801
|
+
const updated = { ...current, ...changes, _localId: current._localId ?? key };
|
|
1802
|
+
this.table.setMutableRecord(key, updated);
|
|
1803
|
+
}
|
|
1804
|
+
modified += 1;
|
|
1805
|
+
}
|
|
1806
|
+
return modified;
|
|
1807
|
+
}
|
|
1808
|
+
async toArray() {
|
|
1809
|
+
return this.materializeEntries(true).map(([, record]) => record);
|
|
1810
|
+
}
|
|
1811
|
+
withState(overrides) {
|
|
1812
|
+
return new _MemoryCollection(this.table, {
|
|
1813
|
+
...this.state,
|
|
1814
|
+
...overrides,
|
|
1815
|
+
predicate: overrides.predicate ?? this.state.predicate
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
combinePredicate(predicate, mode) {
|
|
1819
|
+
if (mode === "and") {
|
|
1820
|
+
return (record, key) => this.state.predicate(record, key) && predicate(record);
|
|
1821
|
+
}
|
|
1822
|
+
return (record, key) => this.state.predicate(record, key) || predicate(record);
|
|
1823
|
+
}
|
|
1824
|
+
materializeEntries(clone) {
|
|
1825
|
+
let entries = this.table.entries().filter(([key, record]) => this.state.predicate(record, key));
|
|
1826
|
+
if (this.state.orderBy) {
|
|
1827
|
+
const { index, direction } = this.state.orderBy;
|
|
1828
|
+
entries = [...entries].sort((a, b) => this.table.compareEntries(a[1], b[1], index));
|
|
1829
|
+
if (direction === "desc") {
|
|
1830
|
+
entries.reverse();
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
if (this.state.reverse) {
|
|
1834
|
+
entries = [...entries].reverse();
|
|
1835
|
+
}
|
|
1836
|
+
if (this.state.distinct) {
|
|
1837
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1838
|
+
entries = entries.filter(([, record]) => {
|
|
1839
|
+
const signature = JSON.stringify(record);
|
|
1840
|
+
if (seen.has(signature)) {
|
|
1841
|
+
return false;
|
|
1842
|
+
}
|
|
1843
|
+
seen.add(signature);
|
|
1844
|
+
return true;
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
if (this.state.offset > 0) {
|
|
1848
|
+
entries = entries.slice(this.state.offset);
|
|
1849
|
+
}
|
|
1850
|
+
if (typeof this.state.limit === "number") {
|
|
1851
|
+
entries = entries.slice(0, this.state.limit);
|
|
1852
|
+
}
|
|
1853
|
+
if (clone) {
|
|
1854
|
+
return entries.map(([key, record]) => [key, this.table.cloneRecord(record)]);
|
|
1855
|
+
}
|
|
1856
|
+
return entries;
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
|
|
1860
|
+
// src/storage/memory/MemoryWhereClause.ts
|
|
1861
|
+
var MemoryWhereClause = class {
|
|
1862
|
+
table;
|
|
1863
|
+
index;
|
|
1864
|
+
baseCollection;
|
|
1865
|
+
constructor(table, index, baseCollection) {
|
|
1866
|
+
this.table = table;
|
|
1867
|
+
this.index = index;
|
|
1868
|
+
this.baseCollection = baseCollection;
|
|
1869
|
+
}
|
|
1870
|
+
equals(value) {
|
|
1871
|
+
return this.createCollection((current) => this.table.compareValues(current, value) === 0);
|
|
1872
|
+
}
|
|
1873
|
+
above(value) {
|
|
1874
|
+
return this.createCollection((current) => this.table.compareValues(current, value) > 0);
|
|
1875
|
+
}
|
|
1876
|
+
aboveOrEqual(value) {
|
|
1877
|
+
return this.createCollection((current) => this.table.compareValues(current, value) >= 0);
|
|
1878
|
+
}
|
|
1879
|
+
below(value) {
|
|
1880
|
+
return this.createCollection((current) => this.table.compareValues(current, value) < 0);
|
|
1881
|
+
}
|
|
1882
|
+
belowOrEqual(value) {
|
|
1883
|
+
return this.createCollection((current) => this.table.compareValues(current, value) <= 0);
|
|
1884
|
+
}
|
|
1885
|
+
between(lower, upper, includeLower = true, includeUpper = false) {
|
|
1886
|
+
return this.createCollection((current) => {
|
|
1887
|
+
const lowerCmp = this.table.compareValues(current, lower);
|
|
1888
|
+
const upperCmp = this.table.compareValues(current, upper);
|
|
1889
|
+
const lowerOk = includeLower ? lowerCmp >= 0 : lowerCmp > 0;
|
|
1890
|
+
const upperOk = includeUpper ? upperCmp <= 0 : upperCmp < 0;
|
|
1891
|
+
return lowerOk && upperOk;
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
inAnyRange(ranges, options) {
|
|
1895
|
+
const includeLower = options?.includeLower ?? true;
|
|
1896
|
+
const includeUpper = options?.includeUpper ?? false;
|
|
1897
|
+
return this.createCollection((current) => {
|
|
1898
|
+
for (const [lower, upper] of ranges) {
|
|
1899
|
+
const lowerCmp = this.table.compareValues(current, lower);
|
|
1900
|
+
const upperCmp = this.table.compareValues(current, upper);
|
|
1901
|
+
const lowerOk = includeLower ? lowerCmp >= 0 : lowerCmp > 0;
|
|
1902
|
+
const upperOk = includeUpper ? upperCmp <= 0 : upperCmp < 0;
|
|
1903
|
+
if (lowerOk && upperOk) {
|
|
1904
|
+
return true;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return false;
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
startsWith(prefix) {
|
|
1911
|
+
return this.createCollection((current) => typeof current === "string" && current.startsWith(prefix));
|
|
1912
|
+
}
|
|
1913
|
+
startsWithIgnoreCase(prefix) {
|
|
1914
|
+
return this.createCollection((current) => typeof current === "string" && current.toLowerCase().startsWith(prefix.toLowerCase()));
|
|
1915
|
+
}
|
|
1916
|
+
startsWithAnyOf(...args) {
|
|
1917
|
+
const prefixes = this.flattenArgs(args);
|
|
1918
|
+
return this.createCollection((current) => typeof current === "string" && prefixes.some((prefix) => current.startsWith(prefix)));
|
|
1919
|
+
}
|
|
1920
|
+
startsWithAnyOfIgnoreCase(...args) {
|
|
1921
|
+
const prefixes = this.flattenArgs(args).map((prefix) => prefix.toLowerCase());
|
|
1922
|
+
return this.createCollection((current) => typeof current === "string" && prefixes.some((prefix) => current.toLowerCase().startsWith(prefix)));
|
|
1923
|
+
}
|
|
1924
|
+
equalsIgnoreCase(value) {
|
|
1925
|
+
return this.createCollection((current) => typeof current === "string" && current.toLowerCase() === value.toLowerCase());
|
|
1926
|
+
}
|
|
1927
|
+
anyOf(...args) {
|
|
1928
|
+
const values = this.flattenArgs(args);
|
|
1929
|
+
const valueSet = new Set(values.map((entry) => JSON.stringify(entry)));
|
|
1930
|
+
return this.createCollection((current) => valueSet.has(JSON.stringify(current)));
|
|
1931
|
+
}
|
|
1932
|
+
anyOfIgnoreCase(...args) {
|
|
1933
|
+
const values = this.flattenArgs(args).map((value) => value.toLowerCase());
|
|
1934
|
+
const valueSet = new Set(values);
|
|
1935
|
+
return this.createCollection((current) => typeof current === "string" && valueSet.has(current.toLowerCase()));
|
|
1936
|
+
}
|
|
1937
|
+
noneOf(...args) {
|
|
1938
|
+
const values = this.flattenArgs(args);
|
|
1939
|
+
const valueSet = new Set(values.map((entry) => JSON.stringify(entry)));
|
|
1940
|
+
return this.createCollection((current) => !valueSet.has(JSON.stringify(current)));
|
|
1941
|
+
}
|
|
1942
|
+
notEqual(value) {
|
|
1943
|
+
return this.createCollection((current) => this.table.compareValues(current, value) !== 0);
|
|
1944
|
+
}
|
|
1945
|
+
createCollection(predicate) {
|
|
1946
|
+
const condition = (record, _key) => predicate(this.table.getIndexValue(record, this.index));
|
|
1947
|
+
if (this.baseCollection) {
|
|
1948
|
+
const combined = (record, key) => this.baseCollection.matches(record, key) || predicate(this.table.getIndexValue(record, this.index));
|
|
1949
|
+
return this.table.createCollectionFromPredicate(combined, this.baseCollection);
|
|
1950
|
+
}
|
|
1951
|
+
return this.table.createCollectionFromPredicate(condition);
|
|
1952
|
+
}
|
|
1953
|
+
flattenArgs(args) {
|
|
1954
|
+
if (args.length === 1 && Array.isArray(args[0])) {
|
|
1955
|
+
return args[0];
|
|
1956
|
+
}
|
|
1957
|
+
return args;
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
|
|
1961
|
+
// src/storage/memory/MemoryTable.ts
|
|
1962
|
+
var MemoryTable = class {
|
|
1963
|
+
name;
|
|
1964
|
+
schema = void 0;
|
|
1965
|
+
primaryKey = LOCAL_PK;
|
|
1966
|
+
hook = Object.freeze({});
|
|
1967
|
+
raw;
|
|
1968
|
+
records = /* @__PURE__ */ new Map();
|
|
1969
|
+
constructor(name) {
|
|
1970
|
+
this.name = name;
|
|
1971
|
+
this.raw = {
|
|
1972
|
+
add: async (item) => this.baseAdd(item),
|
|
1973
|
+
put: async (item) => this.basePut(item),
|
|
1974
|
+
update: async (key, changes) => this.baseUpdate(key, changes),
|
|
1975
|
+
delete: async (key) => {
|
|
1976
|
+
this.baseDelete(key);
|
|
1977
|
+
},
|
|
1978
|
+
get: async (key) => this.baseGet(key),
|
|
1979
|
+
bulkAdd: async (items) => this.baseBulkAdd(items),
|
|
1980
|
+
bulkPut: async (items) => this.baseBulkPut(items),
|
|
1981
|
+
bulkUpdate: async (keysAndChanges) => this.baseBulkUpdate(keysAndChanges),
|
|
1982
|
+
bulkDelete: async (keys) => this.baseBulkDelete(keys),
|
|
1983
|
+
clear: async () => this.baseClear()
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
async add(item) {
|
|
1987
|
+
return this.baseAdd(item);
|
|
1988
|
+
}
|
|
1989
|
+
async put(item) {
|
|
1990
|
+
return this.basePut(item);
|
|
1991
|
+
}
|
|
1992
|
+
async update(key, changes) {
|
|
1993
|
+
return this.baseUpdate(key, changes);
|
|
1994
|
+
}
|
|
1995
|
+
async delete(key) {
|
|
1996
|
+
this.baseDelete(key);
|
|
1997
|
+
}
|
|
1998
|
+
async clear() {
|
|
1999
|
+
this.baseClear();
|
|
2000
|
+
}
|
|
2001
|
+
baseClear() {
|
|
2002
|
+
this.records.clear();
|
|
2003
|
+
}
|
|
2004
|
+
async get(key) {
|
|
2005
|
+
const stored = this.baseGet(key);
|
|
2006
|
+
return stored ? this.cloneRecord(stored) : void 0;
|
|
2007
|
+
}
|
|
2008
|
+
async toArray() {
|
|
2009
|
+
return this.entries().map(([, record]) => this.cloneRecord(record));
|
|
2010
|
+
}
|
|
2011
|
+
async count() {
|
|
2012
|
+
return this.records.size;
|
|
2013
|
+
}
|
|
2014
|
+
async bulkAdd(items) {
|
|
2015
|
+
return this.baseBulkAdd(items);
|
|
2016
|
+
}
|
|
2017
|
+
baseBulkAdd(items) {
|
|
2018
|
+
let lastKey = void 0;
|
|
2019
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
2020
|
+
const item = items[index];
|
|
2021
|
+
lastKey = this.baseAdd(item);
|
|
2022
|
+
}
|
|
2023
|
+
return lastKey;
|
|
2024
|
+
}
|
|
2025
|
+
async bulkPut(items) {
|
|
2026
|
+
return this.baseBulkPut(items);
|
|
2027
|
+
}
|
|
2028
|
+
baseBulkPut(items) {
|
|
2029
|
+
let lastKey = void 0;
|
|
2030
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
2031
|
+
const item = items[index];
|
|
2032
|
+
lastKey = this.basePut(item);
|
|
2033
|
+
}
|
|
2034
|
+
return lastKey;
|
|
2035
|
+
}
|
|
2036
|
+
async bulkGet(keys) {
|
|
2037
|
+
return Promise.all(keys.map((key) => this.get(key)));
|
|
2038
|
+
}
|
|
2039
|
+
async bulkUpdate(keysAndChanges) {
|
|
2040
|
+
return this.baseBulkUpdate(keysAndChanges);
|
|
2041
|
+
}
|
|
2042
|
+
baseBulkUpdate(keysAndChanges) {
|
|
2043
|
+
let updatedCount = 0;
|
|
2044
|
+
for (const { key, changes } of keysAndChanges) {
|
|
2045
|
+
const result = this.baseUpdate(key, changes);
|
|
2046
|
+
updatedCount += result;
|
|
2047
|
+
}
|
|
2048
|
+
return updatedCount;
|
|
2049
|
+
}
|
|
2050
|
+
async bulkDelete(keys) {
|
|
2051
|
+
this.baseBulkDelete(keys);
|
|
2052
|
+
}
|
|
2053
|
+
baseBulkDelete(keys) {
|
|
2054
|
+
for (const key of keys) {
|
|
2055
|
+
this.baseDelete(key);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
where(index) {
|
|
2059
|
+
return this.createWhereClause(index);
|
|
2060
|
+
}
|
|
2061
|
+
orderBy(index) {
|
|
2062
|
+
return this.createCollection({
|
|
2063
|
+
orderBy: { index, direction: "asc" }
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
reverse() {
|
|
2067
|
+
return this.createCollection({ reverse: true });
|
|
2068
|
+
}
|
|
2069
|
+
offset(offset) {
|
|
2070
|
+
return this.createCollection({ offset });
|
|
2071
|
+
}
|
|
2072
|
+
limit(count) {
|
|
2073
|
+
return this.createCollection({ limit: count });
|
|
2074
|
+
}
|
|
2075
|
+
mapToClass(_ctor) {
|
|
2076
|
+
return this;
|
|
2077
|
+
}
|
|
2078
|
+
async each(callback) {
|
|
2079
|
+
for (const [, record] of this.entries()) {
|
|
2080
|
+
await callback(this.cloneRecord(record));
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
jsFilter(predicate) {
|
|
2084
|
+
return this.createCollection({ predicate: (record) => predicate(record) });
|
|
2085
|
+
}
|
|
2086
|
+
createCollection(stateOverrides) {
|
|
2087
|
+
const baseState = createDefaultState();
|
|
2088
|
+
const state = {
|
|
2089
|
+
...baseState,
|
|
2090
|
+
...stateOverrides,
|
|
2091
|
+
predicate: stateOverrides?.predicate ?? baseState.predicate
|
|
2092
|
+
};
|
|
2093
|
+
return new MemoryCollection(this, state);
|
|
2094
|
+
}
|
|
2095
|
+
createCollectionFromPredicate(predicate, template) {
|
|
2096
|
+
const baseState = template ? template.getState() : createDefaultState();
|
|
2097
|
+
return new MemoryCollection(this, {
|
|
2098
|
+
...baseState,
|
|
2099
|
+
predicate
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
createWhereClause(index, baseCollection) {
|
|
2103
|
+
return new MemoryWhereClause(this, index, baseCollection);
|
|
2104
|
+
}
|
|
2105
|
+
entries() {
|
|
2106
|
+
return Array.from(this.records.entries());
|
|
2107
|
+
}
|
|
2108
|
+
cloneRecord(record) {
|
|
2109
|
+
return { ...record };
|
|
2110
|
+
}
|
|
2111
|
+
deleteByKey(key) {
|
|
2112
|
+
this.records.delete(key);
|
|
2113
|
+
}
|
|
2114
|
+
getMutableRecord(key) {
|
|
2115
|
+
return this.records.get(key);
|
|
2116
|
+
}
|
|
2117
|
+
setMutableRecord(key, record) {
|
|
2118
|
+
this.records.set(key, { ...record, _localId: record._localId ?? key });
|
|
2119
|
+
}
|
|
2120
|
+
resolvePublicKey(record, key) {
|
|
2121
|
+
if (record._localId !== void 0) {
|
|
2122
|
+
return record._localId;
|
|
2123
|
+
}
|
|
2124
|
+
if (record.id !== void 0) {
|
|
2125
|
+
return record.id;
|
|
2126
|
+
}
|
|
2127
|
+
return key;
|
|
2128
|
+
}
|
|
2129
|
+
getIndexValue(record, index) {
|
|
2130
|
+
if (Array.isArray(index)) {
|
|
2131
|
+
return index.map((key) => record[key]);
|
|
2132
|
+
}
|
|
2133
|
+
return record[index];
|
|
2134
|
+
}
|
|
2135
|
+
compareEntries(left, right, index) {
|
|
2136
|
+
if (Array.isArray(index)) {
|
|
2137
|
+
for (const key of index) {
|
|
2138
|
+
const diff = this.compareValues(left[key], right[key]);
|
|
2139
|
+
if (diff !== 0) {
|
|
2140
|
+
return diff;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return 0;
|
|
2144
|
+
}
|
|
2145
|
+
return this.compareValues(left[index], right[index]);
|
|
2146
|
+
}
|
|
2147
|
+
compareValues(left, right) {
|
|
2148
|
+
const normalizedLeft = this.normalizeComparableValue(left);
|
|
2149
|
+
const normalizedRight = this.normalizeComparableValue(right);
|
|
2150
|
+
if (normalizedLeft < normalizedRight) {
|
|
2151
|
+
return -1;
|
|
2152
|
+
}
|
|
2153
|
+
if (normalizedLeft > normalizedRight) {
|
|
2154
|
+
return 1;
|
|
2155
|
+
}
|
|
2156
|
+
return 0;
|
|
2157
|
+
}
|
|
2158
|
+
normalizeComparableValue(value) {
|
|
2159
|
+
if (Array.isArray(value)) {
|
|
2160
|
+
return value.map((entry) => this.normalizeComparableValue(entry));
|
|
2161
|
+
}
|
|
2162
|
+
if (value instanceof Date) {
|
|
2163
|
+
return value.valueOf();
|
|
2164
|
+
}
|
|
2165
|
+
return value ?? null;
|
|
2166
|
+
}
|
|
2167
|
+
baseAdd(item) {
|
|
2168
|
+
const primaryKey = this.createPrimaryKey(item);
|
|
2169
|
+
const stored = { ...item, _localId: primaryKey };
|
|
2170
|
+
this.records.set(primaryKey, stored);
|
|
2171
|
+
return primaryKey;
|
|
2172
|
+
}
|
|
2173
|
+
basePut(item) {
|
|
2174
|
+
const primaryKey = this.createPrimaryKey(item);
|
|
2175
|
+
const stored = { ...item, _localId: primaryKey };
|
|
2176
|
+
this.records.set(primaryKey, stored);
|
|
2177
|
+
return primaryKey;
|
|
2178
|
+
}
|
|
2179
|
+
baseUpdate(key, changes) {
|
|
2180
|
+
const primaryKey = this.resolveKey(key);
|
|
2181
|
+
if (!primaryKey) {
|
|
2182
|
+
return 0;
|
|
2183
|
+
}
|
|
2184
|
+
const existing = this.records.get(primaryKey);
|
|
2185
|
+
if (!existing) {
|
|
2186
|
+
return 0;
|
|
2187
|
+
}
|
|
2188
|
+
const updated = { ...existing, ...changes, _localId: existing._localId ?? primaryKey };
|
|
2189
|
+
this.records.set(primaryKey, updated);
|
|
2190
|
+
return 1;
|
|
2191
|
+
}
|
|
2192
|
+
baseDelete(key) {
|
|
2193
|
+
const primaryKey = this.resolveKey(key);
|
|
2194
|
+
if (primaryKey) {
|
|
2195
|
+
this.records.delete(primaryKey);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
baseGet(key) {
|
|
2199
|
+
const primaryKey = this.resolveKey(key);
|
|
2200
|
+
if (!primaryKey) {
|
|
2201
|
+
return void 0;
|
|
2202
|
+
}
|
|
2203
|
+
return this.records.get(primaryKey);
|
|
2204
|
+
}
|
|
2205
|
+
createPrimaryKey(item) {
|
|
2206
|
+
if (item._localId && typeof item._localId === "string") {
|
|
2207
|
+
return item._localId;
|
|
2208
|
+
}
|
|
2209
|
+
if (item.id !== void 0 && (typeof item.id === "string" || typeof item.id === "number" || typeof item.id === "bigint")) {
|
|
2210
|
+
return String(item.id);
|
|
2211
|
+
}
|
|
2212
|
+
return createLocalId();
|
|
2213
|
+
}
|
|
2214
|
+
resolveKey(key) {
|
|
2215
|
+
if (typeof key === "string") {
|
|
2216
|
+
return key;
|
|
2217
|
+
}
|
|
2218
|
+
if (typeof key === "number" || typeof key === "bigint") {
|
|
2219
|
+
return String(key);
|
|
2220
|
+
}
|
|
2221
|
+
if (key && typeof key === "object" && "id" in key) {
|
|
2222
|
+
const lookup = key.id;
|
|
2223
|
+
for (const [storedKey, record] of this.records.entries()) {
|
|
2224
|
+
if (record.id === lookup) {
|
|
2225
|
+
return storedKey;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
return void 0;
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
|
|
2233
|
+
// src/storage/memory/MemoryAdapter.ts
|
|
2234
|
+
var MemoryAdapter = class {
|
|
2235
|
+
type = "MemoryAdapter";
|
|
2236
|
+
name;
|
|
2237
|
+
tables = /* @__PURE__ */ new Map();
|
|
2238
|
+
constructor(name) {
|
|
2239
|
+
this.name = name;
|
|
2240
|
+
}
|
|
2241
|
+
async open() {
|
|
2242
|
+
}
|
|
2243
|
+
async close() {
|
|
2244
|
+
this.tables.clear();
|
|
2245
|
+
}
|
|
2246
|
+
async delete() {
|
|
2247
|
+
this.tables.clear();
|
|
2248
|
+
}
|
|
2249
|
+
async query(callback) {
|
|
2250
|
+
return callback(new MemoryQueryContext(this));
|
|
2251
|
+
}
|
|
2252
|
+
defineSchema(_version, schema, _options) {
|
|
2253
|
+
for (const tableName of Object.keys(schema)) {
|
|
2254
|
+
this.ensureTable(tableName);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
table(name) {
|
|
2258
|
+
return this.ensureTable(name);
|
|
2259
|
+
}
|
|
2260
|
+
async transaction(_mode, tableNames, callback) {
|
|
2261
|
+
const tables = {};
|
|
2262
|
+
for (const tableName of tableNames) {
|
|
2263
|
+
tables[tableName] = this.ensureTable(tableName);
|
|
2264
|
+
}
|
|
2265
|
+
return callback({ tables });
|
|
2266
|
+
}
|
|
2267
|
+
ensureTable(name) {
|
|
2268
|
+
if (!this.tables.has(name)) {
|
|
2269
|
+
this.tables.set(name, new MemoryTable(name));
|
|
2270
|
+
}
|
|
2271
|
+
return this.tables.get(name);
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
2274
|
+
|
|
2275
|
+
// src/storage/sqlite/helpers.ts
|
|
2276
|
+
var SQLITE_SCHEMA_VERSION_STATE_KEY = "sqlite_schema_version";
|
|
2277
|
+
var DEFAULT_STREAM_BATCH_SIZE = 200;
|
|
2278
|
+
var createDefaultState2 = () => ({
|
|
2279
|
+
sqlConditions: [],
|
|
2280
|
+
jsPredicate: void 0,
|
|
2281
|
+
orderBy: void 0,
|
|
2282
|
+
reverse: false,
|
|
2283
|
+
offset: 0,
|
|
2284
|
+
limit: void 0,
|
|
2285
|
+
distinct: false
|
|
2286
|
+
});
|
|
2287
|
+
var buildWhereClause = (conditions) => {
|
|
2288
|
+
if (conditions.length === 0) {
|
|
2289
|
+
return { whereClause: "", parameters: [] };
|
|
2290
|
+
}
|
|
2291
|
+
const clauses = [];
|
|
2292
|
+
const parameters = [];
|
|
2293
|
+
for (const condition of conditions) {
|
|
2294
|
+
const built = buildCondition(condition);
|
|
2295
|
+
clauses.push(built.clause);
|
|
2296
|
+
parameters.push(...built.parameters);
|
|
2297
|
+
}
|
|
2298
|
+
return {
|
|
2299
|
+
whereClause: `WHERE ${clauses.join(" AND ")}`,
|
|
2300
|
+
parameters
|
|
2301
|
+
};
|
|
2302
|
+
};
|
|
2303
|
+
var buildCondition = (condition) => {
|
|
2304
|
+
if (condition.type === "or") {
|
|
2305
|
+
if (condition.conditions.length === 0) {
|
|
2306
|
+
return { clause: "0 = 1", parameters: [] };
|
|
2307
|
+
}
|
|
2308
|
+
const subClauses = [];
|
|
2309
|
+
const subParams = [];
|
|
2310
|
+
for (const sub of condition.conditions) {
|
|
2311
|
+
const built = buildCondition(sub);
|
|
2312
|
+
subClauses.push(built.clause);
|
|
2313
|
+
subParams.push(...built.parameters);
|
|
2314
|
+
}
|
|
2315
|
+
return { clause: `(${subClauses.join(" OR ")})`, parameters: subParams };
|
|
2316
|
+
}
|
|
2317
|
+
if (condition.type === "compoundEquals") {
|
|
2318
|
+
const clauses = condition.columns.map((col2) => `${quoteIdentifier(col2)} = ?`);
|
|
2319
|
+
return { clause: `(${clauses.join(" AND ")})`, parameters: condition.values };
|
|
2320
|
+
}
|
|
2321
|
+
const col = quoteIdentifier(condition.column);
|
|
2322
|
+
switch (condition.type) {
|
|
2323
|
+
case "equals": {
|
|
2324
|
+
if (condition.caseInsensitive) {
|
|
2325
|
+
return { clause: `LOWER(${col}) = LOWER(?)`, parameters: [condition.value] };
|
|
2326
|
+
}
|
|
2327
|
+
return { clause: `${col} = ?`, parameters: [condition.value] };
|
|
2328
|
+
}
|
|
2329
|
+
case "comparison": {
|
|
2330
|
+
return { clause: `${col} ${condition.op} ?`, parameters: [condition.value] };
|
|
2331
|
+
}
|
|
2332
|
+
case "between": {
|
|
2333
|
+
const lowerOp = condition.includeLower ? ">=" : ">";
|
|
2334
|
+
const upperOp = condition.includeUpper ? "<=" : "<";
|
|
2335
|
+
return {
|
|
2336
|
+
clause: `(${col} ${lowerOp} ? AND ${col} ${upperOp} ?)`,
|
|
2337
|
+
parameters: [condition.lower, condition.upper]
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
case "in": {
|
|
2341
|
+
if (condition.values.length === 0) {
|
|
2342
|
+
return { clause: "0 = 1", parameters: [] };
|
|
2343
|
+
}
|
|
2344
|
+
const placeholders = condition.values.map(() => "?").join(", ");
|
|
2345
|
+
if (condition.caseInsensitive) {
|
|
2346
|
+
return {
|
|
2347
|
+
clause: `LOWER(${col}) IN (${condition.values.map(() => "LOWER(?)").join(", ")})`,
|
|
2348
|
+
parameters: condition.values
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
return { clause: `${col} IN (${placeholders})`, parameters: condition.values };
|
|
2352
|
+
}
|
|
2353
|
+
case "notIn": {
|
|
2354
|
+
if (condition.values.length === 0) {
|
|
2355
|
+
return { clause: "1 = 1", parameters: [] };
|
|
2356
|
+
}
|
|
2357
|
+
const placeholders = condition.values.map(() => "?").join(", ");
|
|
2358
|
+
return { clause: `${col} NOT IN (${placeholders})`, parameters: condition.values };
|
|
2359
|
+
}
|
|
2360
|
+
case "like": {
|
|
2361
|
+
if (condition.caseInsensitive) {
|
|
2362
|
+
return { clause: `LOWER(${col}) LIKE LOWER(?)`, parameters: [condition.pattern] };
|
|
2363
|
+
}
|
|
2364
|
+
return { clause: `${col} LIKE ?`, parameters: [condition.pattern] };
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
};
|
|
2368
|
+
var cloneValue = (value) => {
|
|
2369
|
+
if (typeof globalThis.structuredClone === "function") {
|
|
2370
|
+
return globalThis.structuredClone(value);
|
|
2371
|
+
}
|
|
2372
|
+
return JSON.parse(JSON.stringify(value ?? null));
|
|
2373
|
+
};
|
|
2374
|
+
var quoteIdentifier = (name) => `"${name.replace(/"/g, '""')}"`;
|
|
2375
|
+
var normalizeComparableValue = (value) => {
|
|
2376
|
+
if (Array.isArray(value)) {
|
|
2377
|
+
return value.map((entry) => normalizeComparableValue(entry));
|
|
2378
|
+
}
|
|
2379
|
+
if (value instanceof Date) {
|
|
2380
|
+
return value.valueOf();
|
|
2381
|
+
}
|
|
2382
|
+
if (value === void 0) {
|
|
2383
|
+
return null;
|
|
2384
|
+
}
|
|
2385
|
+
return value;
|
|
2386
|
+
};
|
|
2387
|
+
|
|
2388
|
+
// src/storage/sqlite/SqliteQueryContext.ts
|
|
2389
|
+
var SqliteQueryContext = class {
|
|
2390
|
+
constructor(driver, adapter) {
|
|
2391
|
+
this.driver = driver;
|
|
2392
|
+
this.adapter = adapter;
|
|
2393
|
+
}
|
|
2394
|
+
table(name) {
|
|
2395
|
+
return this.adapter.table(name);
|
|
2396
|
+
}
|
|
2397
|
+
transaction(mode, tableNames, callback) {
|
|
2398
|
+
return this.adapter.transaction(mode, tableNames, callback);
|
|
2399
|
+
}
|
|
2400
|
+
async execute(statement) {
|
|
2401
|
+
return this.driver.execute(statement);
|
|
2402
|
+
}
|
|
2403
|
+
async run(statement, values) {
|
|
2404
|
+
return this.driver.run(statement, values);
|
|
2405
|
+
}
|
|
2406
|
+
async queryRows(statement, values) {
|
|
2407
|
+
return this.adapter.queryRows(statement, values);
|
|
2408
|
+
}
|
|
2409
|
+
};
|
|
2410
|
+
|
|
2411
|
+
// src/storage/sqlite/SQLiteAdapter.ts
|
|
2412
|
+
var SQLiteAdapter2 = class {
|
|
2413
|
+
type = "SQLiteAdapter";
|
|
2414
|
+
name;
|
|
2415
|
+
options;
|
|
2416
|
+
schemas = /* @__PURE__ */ new Map();
|
|
2417
|
+
versionSchemas = /* @__PURE__ */ new Map();
|
|
2418
|
+
versionOptions = /* @__PURE__ */ new Map();
|
|
2419
|
+
tableCache = /* @__PURE__ */ new Map();
|
|
2420
|
+
driver;
|
|
2421
|
+
openPromise;
|
|
2422
|
+
isOpen = false;
|
|
2423
|
+
schemaApplied = false;
|
|
2424
|
+
transactionDepth = 0;
|
|
2425
|
+
targetVersion = 0;
|
|
2426
|
+
constructor(driver, options = {}) {
|
|
2427
|
+
this.driver = driver;
|
|
2428
|
+
this.name = driver.name;
|
|
2429
|
+
this.options = options;
|
|
2430
|
+
}
|
|
2431
|
+
get driverType() {
|
|
2432
|
+
return this.driver.type;
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Opens the database connection and applies schema.
|
|
2436
|
+
* This is called automatically when performing operations,
|
|
2437
|
+
* so explicit calls are optional but safe (idempotent).
|
|
2438
|
+
* When called explicitly after schema changes, it will run any pending migrations.
|
|
2439
|
+
*/
|
|
2440
|
+
async open() {
|
|
2441
|
+
if (this.isOpen) {
|
|
2442
|
+
await this.runPendingMigrations();
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
return this.ensureOpen();
|
|
2446
|
+
}
|
|
2447
|
+
async ensureOpen() {
|
|
2448
|
+
if (this.isOpen) {
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
if (this.openPromise) {
|
|
2452
|
+
return this.openPromise;
|
|
2453
|
+
}
|
|
2454
|
+
this.openPromise = this.performOpen();
|
|
2455
|
+
try {
|
|
2456
|
+
await this.openPromise;
|
|
2457
|
+
} finally {
|
|
2458
|
+
this.openPromise = void 0;
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
async performOpen() {
|
|
2462
|
+
await this.driver.open();
|
|
2463
|
+
await this.applySchema();
|
|
2464
|
+
await this.runPendingMigrations();
|
|
2465
|
+
this.isOpen = true;
|
|
2466
|
+
}
|
|
2467
|
+
async close() {
|
|
2468
|
+
if (this.driver) {
|
|
2469
|
+
await this.driver.close();
|
|
2470
|
+
}
|
|
2471
|
+
this.isOpen = false;
|
|
2472
|
+
this.tableCache.clear();
|
|
2473
|
+
}
|
|
2474
|
+
async delete() {
|
|
2475
|
+
for (const table of this.schemas.keys()) {
|
|
2476
|
+
await this.execute(`DROP TABLE IF EXISTS ${quoteIdentifier(table)}`);
|
|
2477
|
+
}
|
|
2478
|
+
await this.execute(`DROP TABLE IF EXISTS ${quoteIdentifier(DYNC_STATE_TABLE)}`);
|
|
2479
|
+
this.tableCache.clear();
|
|
2480
|
+
this.schemaApplied = false;
|
|
2481
|
+
this.refreshActiveSchema();
|
|
2482
|
+
}
|
|
2483
|
+
defineSchema(version, schema, options) {
|
|
2484
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
2485
|
+
for (const [tableName, definition] of Object.entries(schema)) {
|
|
2486
|
+
if (typeof definition === "string") {
|
|
2487
|
+
throw new Error(`SQLite adapter requires structured schema definitions. Table '${tableName}' must provide an object-based schema.`);
|
|
2488
|
+
}
|
|
2489
|
+
normalized.set(tableName, this.parseStructuredSchema(tableName, definition));
|
|
2490
|
+
}
|
|
2491
|
+
this.versionSchemas.set(version, normalized);
|
|
2492
|
+
this.versionOptions.set(version, options ?? {});
|
|
2493
|
+
this.refreshActiveSchema();
|
|
2494
|
+
}
|
|
2495
|
+
refreshActiveSchema() {
|
|
2496
|
+
if (!this.versionSchemas.size) {
|
|
2497
|
+
this.schemas.clear();
|
|
2498
|
+
this.targetVersion = 0;
|
|
2499
|
+
this.schemaApplied = false;
|
|
2500
|
+
this.tableCache.clear();
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
const versions = Array.from(this.versionSchemas.keys());
|
|
2504
|
+
const latestVersion = Math.max(...versions);
|
|
2505
|
+
const latestSchema = this.versionSchemas.get(latestVersion);
|
|
2506
|
+
if (!latestSchema) {
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
this.schemas.clear();
|
|
2510
|
+
for (const [name, schema] of latestSchema.entries()) {
|
|
2511
|
+
this.schemas.set(name, schema);
|
|
2512
|
+
}
|
|
2513
|
+
if (this.targetVersion !== latestVersion) {
|
|
2514
|
+
this.tableCache.clear();
|
|
2515
|
+
}
|
|
2516
|
+
this.targetVersion = latestVersion;
|
|
2517
|
+
this.schemaApplied = false;
|
|
2518
|
+
}
|
|
2519
|
+
table(name) {
|
|
2520
|
+
if (!this.schemas.has(name)) {
|
|
2521
|
+
throw new Error(`Table '${name}' is not part of the defined schema`);
|
|
2522
|
+
}
|
|
2523
|
+
if (!this.tableCache.has(name)) {
|
|
2524
|
+
this.tableCache.set(name, new SQLiteTable(this, this.schemas.get(name)));
|
|
2525
|
+
}
|
|
2526
|
+
return this.tableCache.get(name);
|
|
2527
|
+
}
|
|
2528
|
+
async transaction(_mode, tableNames, callback) {
|
|
2529
|
+
const driver = await this.getDriver();
|
|
2530
|
+
const shouldManageTransaction = this.transactionDepth === 0;
|
|
2531
|
+
this.transactionDepth += 1;
|
|
2532
|
+
if (shouldManageTransaction) {
|
|
2533
|
+
this.logSql("BEGIN TRANSACTION");
|
|
2534
|
+
await driver.execute("BEGIN TRANSACTION");
|
|
2535
|
+
}
|
|
2536
|
+
try {
|
|
2537
|
+
const tables = {};
|
|
2538
|
+
for (const tableName of tableNames) {
|
|
2539
|
+
tables[tableName] = this.table(tableName);
|
|
2540
|
+
}
|
|
2541
|
+
const result = await callback({ tables });
|
|
2542
|
+
if (shouldManageTransaction) {
|
|
2543
|
+
this.logSql("COMMIT");
|
|
2544
|
+
await driver.execute("COMMIT");
|
|
2545
|
+
}
|
|
2546
|
+
return result;
|
|
2547
|
+
} catch (err) {
|
|
2548
|
+
if (shouldManageTransaction) {
|
|
2549
|
+
this.logSql("ROLLBACK");
|
|
2550
|
+
await driver.execute("ROLLBACK");
|
|
2551
|
+
}
|
|
2552
|
+
throw err;
|
|
2553
|
+
} finally {
|
|
2554
|
+
this.transactionDepth -= 1;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
async execute(statement, values) {
|
|
2558
|
+
if (values && values.length) {
|
|
2559
|
+
await this.run(statement, values);
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
const driver = await this.getDriver();
|
|
2563
|
+
this.logSql(statement);
|
|
2564
|
+
await driver.execute(statement);
|
|
2565
|
+
}
|
|
2566
|
+
async run(statement, values) {
|
|
2567
|
+
const driver = await this.getDriver();
|
|
2568
|
+
const params = values ?? [];
|
|
2569
|
+
this.logSql(statement, params);
|
|
2570
|
+
return driver.run(statement, params);
|
|
2571
|
+
}
|
|
2572
|
+
async query(arg1, arg2) {
|
|
2573
|
+
if (typeof arg1 === "function") {
|
|
2574
|
+
const driver2 = await this.getDriver();
|
|
2575
|
+
return arg1(new SqliteQueryContext(driver2, this));
|
|
2576
|
+
}
|
|
2577
|
+
const statement = arg1;
|
|
2578
|
+
const values = arg2;
|
|
2579
|
+
const driver = await this.getDriver();
|
|
2580
|
+
const params = values ?? [];
|
|
2581
|
+
this.logSql(statement, params);
|
|
2582
|
+
return driver.query(statement, params);
|
|
2583
|
+
}
|
|
2584
|
+
async queryRows(statement, values) {
|
|
2585
|
+
const result = await this.query(statement, values);
|
|
2586
|
+
const columns = result.columns ?? [];
|
|
2587
|
+
return (result.values ?? []).map((row) => {
|
|
2588
|
+
const record = {};
|
|
2589
|
+
for (let index = 0; index < columns.length; index += 1) {
|
|
2590
|
+
record[columns[index]] = row[index];
|
|
2591
|
+
}
|
|
2592
|
+
return record;
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
/**
|
|
2596
|
+
* Ensures the database is open and returns the driver.
|
|
2597
|
+
* This is the main entry point for all public database operations.
|
|
2598
|
+
*/
|
|
2599
|
+
async getDriver() {
|
|
2600
|
+
await this.ensureOpen();
|
|
2601
|
+
return this.driver;
|
|
2602
|
+
}
|
|
2603
|
+
/**
|
|
2604
|
+
* Internal execute that uses driver directly.
|
|
2605
|
+
* Used during the open process to avoid recursion.
|
|
2606
|
+
*/
|
|
2607
|
+
async internalExecute(statement, values) {
|
|
2608
|
+
if (values && values.length) {
|
|
2609
|
+
await this.internalRun(statement, values);
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
this.logSql(statement);
|
|
2613
|
+
await this.driver.execute(statement);
|
|
2614
|
+
}
|
|
2615
|
+
/**
|
|
2616
|
+
* Internal run that uses driver directly.
|
|
2617
|
+
* Used during the open process to avoid recursion.
|
|
2618
|
+
*/
|
|
2619
|
+
async internalRun(statement, values) {
|
|
2620
|
+
const params = values ?? [];
|
|
2621
|
+
this.logSql(statement, params);
|
|
2622
|
+
return this.driver.run(statement, params);
|
|
2623
|
+
}
|
|
2624
|
+
/**
|
|
2625
|
+
* Internal queryRows that uses driver directly.
|
|
2626
|
+
* Used during the open process to avoid recursion.
|
|
2627
|
+
*/
|
|
2628
|
+
async internalQueryRows(statement, values) {
|
|
2629
|
+
const params = values ?? [];
|
|
2630
|
+
this.logSql(statement, params);
|
|
2631
|
+
const result = await this.driver.query(statement, params);
|
|
2632
|
+
const columns = result.columns ?? [];
|
|
2633
|
+
return (result.values ?? []).map((row) => {
|
|
2634
|
+
const record = {};
|
|
2635
|
+
for (let index = 0; index < columns.length; index += 1) {
|
|
2636
|
+
record[columns[index]] = row[index];
|
|
2637
|
+
}
|
|
2638
|
+
return record;
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
/**
|
|
2642
|
+
* Internal query that uses driver directly.
|
|
2643
|
+
* Used during migrations to avoid recursion.
|
|
2644
|
+
*/
|
|
2645
|
+
async internalQuery(statement, values) {
|
|
2646
|
+
const params = values ?? [];
|
|
2647
|
+
this.logSql(statement, params);
|
|
2648
|
+
return this.driver.query(statement, params);
|
|
2649
|
+
}
|
|
2650
|
+
logSql(statement, parameters) {
|
|
2651
|
+
const { debug } = this.options;
|
|
2652
|
+
if (!debug) {
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
const hasParams = parameters && parameters.length;
|
|
2656
|
+
if (typeof debug === "function") {
|
|
2657
|
+
debug(statement, hasParams ? parameters : void 0);
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
if (debug === true) {
|
|
2661
|
+
if (hasParams) {
|
|
2662
|
+
console.debug("[dync][sqlite]", statement, parameters);
|
|
2663
|
+
} else {
|
|
2664
|
+
console.debug("[dync][sqlite]", statement);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
async getStoredSchemaVersion() {
|
|
2669
|
+
const rows = await this.internalQueryRows(`SELECT value FROM ${quoteIdentifier(DYNC_STATE_TABLE)} WHERE ${quoteIdentifier(LOCAL_PK)} = ? LIMIT 1`, [
|
|
2670
|
+
SQLITE_SCHEMA_VERSION_STATE_KEY
|
|
2671
|
+
]);
|
|
2672
|
+
const rawValue = rows[0]?.value;
|
|
2673
|
+
const parsed = Number(rawValue ?? 0);
|
|
2674
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
2675
|
+
}
|
|
2676
|
+
async setStoredSchemaVersion(version) {
|
|
2677
|
+
await this.internalRun(
|
|
2678
|
+
`INSERT INTO ${quoteIdentifier(DYNC_STATE_TABLE)} (${quoteIdentifier(LOCAL_PK)}, value) VALUES (?, ?) ON CONFLICT(${quoteIdentifier(LOCAL_PK)}) DO UPDATE SET value = excluded.value`,
|
|
2679
|
+
[SQLITE_SCHEMA_VERSION_STATE_KEY, String(version)]
|
|
2680
|
+
);
|
|
2681
|
+
}
|
|
2682
|
+
async runPendingMigrations() {
|
|
2683
|
+
if (!this.versionSchemas.size) {
|
|
2684
|
+
await this.setStoredSchemaVersion(0);
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
const targetVersion = this.targetVersion;
|
|
2688
|
+
const currentVersion = await this.getStoredSchemaVersion();
|
|
2689
|
+
if (currentVersion === targetVersion) {
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
if (currentVersion < targetVersion) {
|
|
2693
|
+
for (let version = currentVersion + 1; version <= targetVersion; version += 1) {
|
|
2694
|
+
await this.runMigrationStep(version, "upgrade", version - 1, version);
|
|
2695
|
+
await this.setStoredSchemaVersion(version);
|
|
2696
|
+
}
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
for (let version = currentVersion; version > targetVersion; version -= 1) {
|
|
2700
|
+
await this.runMigrationStep(version, "downgrade", version, version - 1);
|
|
2701
|
+
await this.setStoredSchemaVersion(version - 1);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
async runMigrationStep(version, direction, fromVersion, toVersion) {
|
|
2705
|
+
const handler = this.getMigrationHandler(version, direction);
|
|
2706
|
+
if (!handler) {
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
const context = {
|
|
2710
|
+
direction,
|
|
2711
|
+
fromVersion,
|
|
2712
|
+
toVersion,
|
|
2713
|
+
execute: (statement) => this.internalExecute(statement),
|
|
2714
|
+
run: (statement, values) => this.internalRun(statement, values),
|
|
2715
|
+
query: (statement, values) => this.internalQuery(statement, values)
|
|
2716
|
+
};
|
|
2717
|
+
await handler(context);
|
|
2718
|
+
}
|
|
2719
|
+
getMigrationHandler(version, direction) {
|
|
2720
|
+
const options = this.versionOptions.get(version);
|
|
2721
|
+
const migrations = options?.sqlite?.migrations;
|
|
2722
|
+
if (!migrations) {
|
|
2723
|
+
return void 0;
|
|
2724
|
+
}
|
|
2725
|
+
return direction === "upgrade" ? migrations.upgrade : migrations.downgrade;
|
|
2726
|
+
}
|
|
2727
|
+
async applySchema() {
|
|
2728
|
+
if (this.schemaApplied) {
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
for (const schema of this.schemas.values()) {
|
|
2732
|
+
await this.internalExecute(this.buildCreateTableStatement(schema));
|
|
2733
|
+
const indexStatements = this.buildIndexStatements(schema);
|
|
2734
|
+
for (const statement of indexStatements) {
|
|
2735
|
+
await this.internalExecute(statement);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
this.schemaApplied = true;
|
|
2739
|
+
}
|
|
2740
|
+
buildCreateTableStatement(schema) {
|
|
2741
|
+
const columns = [];
|
|
2742
|
+
for (const column of Object.values(schema.definition.columns)) {
|
|
2743
|
+
columns.push(this.buildStructuredColumnDefinition(column));
|
|
2744
|
+
}
|
|
2745
|
+
if (schema.definition.source === "structured" && Array.isArray(schema.definition.tableConstraints)) {
|
|
2746
|
+
columns.push(...schema.definition.tableConstraints.filter(Boolean));
|
|
2747
|
+
}
|
|
2748
|
+
const trailingClauses = [schema.definition.withoutRowId ? "WITHOUT ROWID" : void 0, schema.definition.strict ? "STRICT" : void 0].filter(Boolean);
|
|
2749
|
+
const suffix = trailingClauses.length ? ` ${trailingClauses.join(" ")}` : "";
|
|
2750
|
+
return `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(schema.name)} (${columns.join(", ")})${suffix}`;
|
|
2751
|
+
}
|
|
2752
|
+
buildStructuredColumnDefinition(column) {
|
|
2753
|
+
const parts = [];
|
|
2754
|
+
parts.push(quoteIdentifier(column.name));
|
|
2755
|
+
parts.push(this.formatColumnType(column));
|
|
2756
|
+
if (column.name === LOCAL_PK) {
|
|
2757
|
+
parts.push("PRIMARY KEY");
|
|
2758
|
+
}
|
|
2759
|
+
if (column.collate) {
|
|
2760
|
+
parts.push(`COLLATE ${column.collate}`);
|
|
2761
|
+
}
|
|
2762
|
+
if (column.generatedAlwaysAs) {
|
|
2763
|
+
const storage = column.stored ? "STORED" : "VIRTUAL";
|
|
2764
|
+
parts.push(`GENERATED ALWAYS AS (${column.generatedAlwaysAs}) ${storage}`);
|
|
2765
|
+
}
|
|
2766
|
+
if (column.unique) {
|
|
2767
|
+
parts.push("UNIQUE");
|
|
2768
|
+
}
|
|
2769
|
+
if (column.default !== void 0) {
|
|
2770
|
+
parts.push(`DEFAULT ${this.formatDefaultValue(column.default)}`);
|
|
2771
|
+
}
|
|
2772
|
+
if (column.check) {
|
|
2773
|
+
parts.push(`CHECK (${column.check})`);
|
|
2774
|
+
}
|
|
2775
|
+
if (column.references) {
|
|
2776
|
+
parts.push(this.buildReferencesClause(column.references));
|
|
2777
|
+
}
|
|
2778
|
+
if (column.constraints?.length) {
|
|
2779
|
+
parts.push(...column.constraints);
|
|
2780
|
+
}
|
|
2781
|
+
return parts.filter(Boolean).join(" ");
|
|
2782
|
+
}
|
|
2783
|
+
formatColumnType(column) {
|
|
2784
|
+
const declaredType = column.type?.trim().toUpperCase();
|
|
2785
|
+
const base = !declaredType || !declaredType.length ? "NUMERIC" : declaredType === "BOOLEAN" ? "INTEGER" : declaredType;
|
|
2786
|
+
if (column.length && !base.includes("(")) {
|
|
2787
|
+
return `${base}(${column.length})`;
|
|
2788
|
+
}
|
|
2789
|
+
return base;
|
|
2790
|
+
}
|
|
2791
|
+
formatDefaultValue(value) {
|
|
2792
|
+
if (value === null) {
|
|
2793
|
+
return "NULL";
|
|
2794
|
+
}
|
|
2795
|
+
if (typeof value === "number") {
|
|
2796
|
+
return String(value);
|
|
2797
|
+
}
|
|
2798
|
+
if (typeof value === "boolean") {
|
|
2799
|
+
return value ? "1" : "0";
|
|
2800
|
+
}
|
|
2801
|
+
const escaped = String(value).replace(/'/g, "''");
|
|
2802
|
+
return `'${escaped}'`;
|
|
2803
|
+
}
|
|
2804
|
+
buildReferencesClause(reference) {
|
|
2805
|
+
if (typeof reference === "string") {
|
|
2806
|
+
return `REFERENCES ${reference}`;
|
|
2807
|
+
}
|
|
2808
|
+
const parts = [];
|
|
2809
|
+
parts.push(`REFERENCES ${quoteIdentifier(reference.table)}`);
|
|
2810
|
+
if (reference.column) {
|
|
2811
|
+
parts.push(`(${quoteIdentifier(reference.column)})`);
|
|
2812
|
+
}
|
|
2813
|
+
if (reference.match) {
|
|
2814
|
+
parts.push(`MATCH ${reference.match}`);
|
|
2815
|
+
}
|
|
2816
|
+
if (reference.onDelete) {
|
|
2817
|
+
parts.push(`ON DELETE ${reference.onDelete}`);
|
|
2818
|
+
}
|
|
2819
|
+
if (reference.onUpdate) {
|
|
2820
|
+
parts.push(`ON UPDATE ${reference.onUpdate}`);
|
|
2821
|
+
}
|
|
2822
|
+
return parts.join(" ");
|
|
2823
|
+
}
|
|
2824
|
+
buildIndexStatements(schema) {
|
|
2825
|
+
if (schema.definition.source !== "structured" || !schema.definition.indexes?.length) {
|
|
2826
|
+
return [];
|
|
2827
|
+
}
|
|
2828
|
+
const statements = [];
|
|
2829
|
+
schema.definition.indexes.forEach((index, position) => {
|
|
2830
|
+
if (!index.columns?.length) {
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
const indexName = this.generateIndexName(schema, index, position);
|
|
2834
|
+
const columnSegments = index.columns.map((columnName, columnIndex) => {
|
|
2835
|
+
const segments = [quoteIdentifier(columnName)];
|
|
2836
|
+
if (index.collate) {
|
|
2837
|
+
segments.push(`COLLATE ${index.collate}`);
|
|
2838
|
+
}
|
|
2839
|
+
const order = index.orders?.[columnIndex];
|
|
2840
|
+
if (order) {
|
|
2841
|
+
segments.push(order);
|
|
2842
|
+
}
|
|
2843
|
+
return segments.join(" ");
|
|
2844
|
+
});
|
|
2845
|
+
const whereClause = index.where ? ` WHERE ${index.where}` : "";
|
|
2846
|
+
statements.push(
|
|
2847
|
+
`CREATE ${index.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(schema.name)} (${columnSegments.join(", ")})${whereClause}`
|
|
2848
|
+
);
|
|
2849
|
+
});
|
|
2850
|
+
return statements;
|
|
2851
|
+
}
|
|
2852
|
+
generateIndexName(schema, index, position) {
|
|
2853
|
+
if (index.name) {
|
|
2854
|
+
return index.name;
|
|
2855
|
+
}
|
|
2856
|
+
const sanitizedColumns = index.columns.map((column) => column.replace(/[^A-Za-z0-9_]/g, "_")).join("_");
|
|
2857
|
+
const suffix = sanitizedColumns || String(position);
|
|
2858
|
+
return `${schema.name}_${suffix}_idx`;
|
|
2859
|
+
}
|
|
2860
|
+
parseStructuredSchema(tableName, definition) {
|
|
2861
|
+
if (!definition?.columns || !Object.keys(definition.columns).length) {
|
|
2862
|
+
throw new Error(`SQLite schema for table '${tableName}' must define at least one column.`);
|
|
2863
|
+
}
|
|
2864
|
+
if (!definition.columns[LOCAL_PK]) {
|
|
2865
|
+
throw new Error(`SQLite schema for table '${tableName}' must define a column named '${LOCAL_PK}'.`);
|
|
2866
|
+
}
|
|
2867
|
+
const normalizedColumns = this.normalizeColumns(definition.columns);
|
|
2868
|
+
return {
|
|
2869
|
+
name: tableName,
|
|
2870
|
+
definition: {
|
|
2871
|
+
...definition,
|
|
2872
|
+
name: tableName,
|
|
2873
|
+
columns: normalizedColumns,
|
|
2874
|
+
source: "structured"
|
|
2875
|
+
}
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
normalizeColumns(columns) {
|
|
2879
|
+
const normalized = {};
|
|
2880
|
+
for (const [name, column] of Object.entries(columns)) {
|
|
2881
|
+
normalized[name] = {
|
|
2882
|
+
name,
|
|
2883
|
+
...column,
|
|
2884
|
+
nullable: column?.nullable ?? true
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
return normalized;
|
|
2888
|
+
}
|
|
2889
|
+
};
|
|
2890
|
+
|
|
2891
|
+
// src/storage/sqlite/SQLiteWhereClause.ts
|
|
2892
|
+
var SQLiteWhereClause = class {
|
|
2893
|
+
table;
|
|
2894
|
+
index;
|
|
2895
|
+
baseCollection;
|
|
2896
|
+
constructor(table, index, baseCollection) {
|
|
2897
|
+
this.table = table;
|
|
2898
|
+
this.index = index;
|
|
2899
|
+
this.baseCollection = baseCollection;
|
|
2900
|
+
}
|
|
2901
|
+
getColumn() {
|
|
2902
|
+
if (Array.isArray(this.index)) {
|
|
2903
|
+
return this.index[0];
|
|
2904
|
+
}
|
|
2905
|
+
return this.index;
|
|
2906
|
+
}
|
|
2907
|
+
isCompoundIndex() {
|
|
2908
|
+
return Array.isArray(this.index) && this.index.length > 1;
|
|
2909
|
+
}
|
|
2910
|
+
getCompoundColumns() {
|
|
2911
|
+
return Array.isArray(this.index) ? this.index : [this.index];
|
|
2912
|
+
}
|
|
2913
|
+
getCompoundValues(value) {
|
|
2914
|
+
if (Array.isArray(value)) {
|
|
2915
|
+
return value;
|
|
2916
|
+
}
|
|
2917
|
+
return [value];
|
|
2918
|
+
}
|
|
2919
|
+
createCollectionWithCondition(condition) {
|
|
2920
|
+
const base = this.baseCollection ?? this.table.createCollection();
|
|
2921
|
+
return base.addSqlCondition(condition);
|
|
2922
|
+
}
|
|
2923
|
+
createCollectionWithJsPredicate(predicate) {
|
|
2924
|
+
const base = this.baseCollection ?? this.table.createCollection();
|
|
2925
|
+
return base.jsFilter(predicate);
|
|
2926
|
+
}
|
|
2927
|
+
getIndexValue(record) {
|
|
2928
|
+
return this.table.getIndexValue(record, this.index);
|
|
2929
|
+
}
|
|
2930
|
+
flattenArgs(args) {
|
|
2931
|
+
if (args.length === 1 && Array.isArray(args[0])) {
|
|
2932
|
+
return args[0];
|
|
2933
|
+
}
|
|
2934
|
+
return args;
|
|
2935
|
+
}
|
|
2936
|
+
equals(value) {
|
|
2937
|
+
if (this.isCompoundIndex()) {
|
|
2938
|
+
const columns = this.getCompoundColumns();
|
|
2939
|
+
const values = this.getCompoundValues(value);
|
|
2940
|
+
return this.createCollectionWithCondition({
|
|
2941
|
+
type: "compoundEquals",
|
|
2942
|
+
columns,
|
|
2943
|
+
values
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2946
|
+
return this.createCollectionWithCondition({
|
|
2947
|
+
type: "equals",
|
|
2948
|
+
column: this.getColumn(),
|
|
2949
|
+
value
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
above(value) {
|
|
2953
|
+
if (this.isCompoundIndex()) {
|
|
2954
|
+
return this.createCollectionWithJsPredicate((record) => this.table.compareValues(this.getIndexValue(record), value) > 0);
|
|
2955
|
+
}
|
|
2956
|
+
return this.createCollectionWithCondition({
|
|
2957
|
+
type: "comparison",
|
|
2958
|
+
column: this.getColumn(),
|
|
2959
|
+
op: ">",
|
|
2960
|
+
value
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
aboveOrEqual(value) {
|
|
2964
|
+
if (this.isCompoundIndex()) {
|
|
2965
|
+
return this.createCollectionWithJsPredicate((record) => this.table.compareValues(this.getIndexValue(record), value) >= 0);
|
|
2966
|
+
}
|
|
2967
|
+
return this.createCollectionWithCondition({
|
|
2968
|
+
type: "comparison",
|
|
2969
|
+
column: this.getColumn(),
|
|
2970
|
+
op: ">=",
|
|
2971
|
+
value
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
below(value) {
|
|
2975
|
+
if (this.isCompoundIndex()) {
|
|
2976
|
+
return this.createCollectionWithJsPredicate((record) => this.table.compareValues(this.getIndexValue(record), value) < 0);
|
|
2977
|
+
}
|
|
2978
|
+
return this.createCollectionWithCondition({
|
|
2979
|
+
type: "comparison",
|
|
2980
|
+
column: this.getColumn(),
|
|
2981
|
+
op: "<",
|
|
2982
|
+
value
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
belowOrEqual(value) {
|
|
2986
|
+
if (this.isCompoundIndex()) {
|
|
2987
|
+
return this.createCollectionWithJsPredicate((record) => this.table.compareValues(this.getIndexValue(record), value) <= 0);
|
|
2988
|
+
}
|
|
2989
|
+
return this.createCollectionWithCondition({
|
|
2990
|
+
type: "comparison",
|
|
2991
|
+
column: this.getColumn(),
|
|
2992
|
+
op: "<=",
|
|
2993
|
+
value
|
|
2994
|
+
});
|
|
2995
|
+
}
|
|
2996
|
+
between(lower, upper, includeLower = true, includeUpper = false) {
|
|
2997
|
+
if (this.isCompoundIndex()) {
|
|
2998
|
+
return this.createCollectionWithJsPredicate((record) => {
|
|
2999
|
+
const value = this.getIndexValue(record);
|
|
3000
|
+
const lowerCmp = this.table.compareValues(value, lower);
|
|
3001
|
+
const upperCmp = this.table.compareValues(value, upper);
|
|
3002
|
+
const lowerPass = includeLower ? lowerCmp >= 0 : lowerCmp > 0;
|
|
3003
|
+
const upperPass = includeUpper ? upperCmp <= 0 : upperCmp < 0;
|
|
3004
|
+
return lowerPass && upperPass;
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
return this.createCollectionWithCondition({
|
|
3008
|
+
type: "between",
|
|
3009
|
+
column: this.getColumn(),
|
|
3010
|
+
lower,
|
|
3011
|
+
upper,
|
|
3012
|
+
includeLower,
|
|
3013
|
+
includeUpper
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
inAnyRange(ranges, options) {
|
|
3017
|
+
if (this.isCompoundIndex() || ranges.length === 0) {
|
|
3018
|
+
return this.createCollectionWithJsPredicate((record) => {
|
|
3019
|
+
const value = this.getIndexValue(record);
|
|
3020
|
+
return ranges.some(([lower, upper]) => {
|
|
3021
|
+
const lowerCmp = this.table.compareValues(value, lower);
|
|
3022
|
+
const upperCmp = this.table.compareValues(value, upper);
|
|
3023
|
+
const lowerPass = options?.includeLower !== false ? lowerCmp >= 0 : lowerCmp > 0;
|
|
3024
|
+
const upperPass = options?.includeUpper ? upperCmp <= 0 : upperCmp < 0;
|
|
3025
|
+
return lowerPass && upperPass;
|
|
3026
|
+
});
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
const column = this.getColumn();
|
|
3030
|
+
const orConditions = ranges.map(([lower, upper]) => ({
|
|
3031
|
+
type: "between",
|
|
3032
|
+
column,
|
|
3033
|
+
lower,
|
|
3034
|
+
upper,
|
|
3035
|
+
includeLower: options?.includeLower !== false,
|
|
3036
|
+
includeUpper: options?.includeUpper ?? false
|
|
3037
|
+
}));
|
|
3038
|
+
return this.createCollectionWithCondition({
|
|
3039
|
+
type: "or",
|
|
3040
|
+
conditions: orConditions
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
startsWith(prefix) {
|
|
3044
|
+
if (this.isCompoundIndex()) {
|
|
3045
|
+
return this.createCollectionWithJsPredicate((record) => String(this.getIndexValue(record) ?? "").startsWith(prefix));
|
|
3046
|
+
}
|
|
3047
|
+
const escapedPrefix = prefix.replace(/[%_\\]/g, "\\$&");
|
|
3048
|
+
return this.createCollectionWithCondition({
|
|
3049
|
+
type: "like",
|
|
3050
|
+
column: this.getColumn(),
|
|
3051
|
+
pattern: `${escapedPrefix}%`
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
startsWithIgnoreCase(prefix) {
|
|
3055
|
+
if (this.isCompoundIndex()) {
|
|
3056
|
+
return this.createCollectionWithJsPredicate(
|
|
3057
|
+
(record) => String(this.getIndexValue(record) ?? "").toLowerCase().startsWith(prefix.toLowerCase())
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
const escapedPrefix = prefix.replace(/[%_\\]/g, "\\$&");
|
|
3061
|
+
return this.createCollectionWithCondition({
|
|
3062
|
+
type: "like",
|
|
3063
|
+
column: this.getColumn(),
|
|
3064
|
+
pattern: `${escapedPrefix}%`,
|
|
3065
|
+
caseInsensitive: true
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
startsWithAnyOf(...args) {
|
|
3069
|
+
const prefixes = this.flattenArgs(args);
|
|
3070
|
+
if (this.isCompoundIndex() || prefixes.length === 0) {
|
|
3071
|
+
return this.createCollectionWithJsPredicate((record) => {
|
|
3072
|
+
const value = String(this.getIndexValue(record) ?? "");
|
|
3073
|
+
return prefixes.some((prefix) => value.startsWith(prefix));
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
const column = this.getColumn();
|
|
3077
|
+
const orConditions = prefixes.map((prefix) => {
|
|
3078
|
+
const escapedPrefix = prefix.replace(/[%_\\]/g, "\\$&");
|
|
3079
|
+
return {
|
|
3080
|
+
type: "like",
|
|
3081
|
+
column,
|
|
3082
|
+
pattern: `${escapedPrefix}%`
|
|
3083
|
+
};
|
|
3084
|
+
});
|
|
3085
|
+
return this.createCollectionWithCondition({
|
|
3086
|
+
type: "or",
|
|
3087
|
+
conditions: orConditions
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
startsWithAnyOfIgnoreCase(...args) {
|
|
3091
|
+
const prefixes = this.flattenArgs(args);
|
|
3092
|
+
if (this.isCompoundIndex() || prefixes.length === 0) {
|
|
3093
|
+
const lowerPrefixes = prefixes.map((p) => p.toLowerCase());
|
|
3094
|
+
return this.createCollectionWithJsPredicate((record) => {
|
|
3095
|
+
const value = String(this.getIndexValue(record) ?? "").toLowerCase();
|
|
3096
|
+
return lowerPrefixes.some((prefix) => value.startsWith(prefix));
|
|
3097
|
+
});
|
|
3098
|
+
}
|
|
3099
|
+
const column = this.getColumn();
|
|
3100
|
+
const orConditions = prefixes.map((prefix) => {
|
|
3101
|
+
const escapedPrefix = prefix.replace(/[%_\\]/g, "\\$&");
|
|
3102
|
+
return {
|
|
3103
|
+
type: "like",
|
|
3104
|
+
column,
|
|
3105
|
+
pattern: `${escapedPrefix}%`,
|
|
3106
|
+
caseInsensitive: true
|
|
3107
|
+
};
|
|
3108
|
+
});
|
|
3109
|
+
return this.createCollectionWithCondition({
|
|
3110
|
+
type: "or",
|
|
3111
|
+
conditions: orConditions
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
equalsIgnoreCase(value) {
|
|
3115
|
+
if (this.isCompoundIndex()) {
|
|
3116
|
+
return this.createCollectionWithJsPredicate((record) => String(this.getIndexValue(record) ?? "").toLowerCase() === value.toLowerCase());
|
|
3117
|
+
}
|
|
3118
|
+
return this.createCollectionWithCondition({
|
|
3119
|
+
type: "equals",
|
|
3120
|
+
column: this.getColumn(),
|
|
3121
|
+
value,
|
|
3122
|
+
caseInsensitive: true
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
anyOf(...args) {
|
|
3126
|
+
const values = this.flattenArgs(args);
|
|
3127
|
+
if (this.isCompoundIndex()) {
|
|
3128
|
+
const columns = this.getCompoundColumns();
|
|
3129
|
+
const orConditions = values.map((value) => ({
|
|
3130
|
+
type: "compoundEquals",
|
|
3131
|
+
columns,
|
|
3132
|
+
values: this.getCompoundValues(value)
|
|
3133
|
+
}));
|
|
3134
|
+
return this.createCollectionWithCondition({
|
|
3135
|
+
type: "or",
|
|
3136
|
+
conditions: orConditions
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
return this.createCollectionWithCondition({
|
|
3140
|
+
type: "in",
|
|
3141
|
+
column: this.getColumn(),
|
|
3142
|
+
values
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
anyOfIgnoreCase(...args) {
|
|
3146
|
+
const values = this.flattenArgs(args);
|
|
3147
|
+
if (this.isCompoundIndex()) {
|
|
3148
|
+
const lowerValues = values.map((v) => v.toLowerCase());
|
|
3149
|
+
return this.createCollectionWithJsPredicate((record) => {
|
|
3150
|
+
const value = String(this.getIndexValue(record) ?? "").toLowerCase();
|
|
3151
|
+
return lowerValues.includes(value);
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
3154
|
+
return this.createCollectionWithCondition({
|
|
3155
|
+
type: "in",
|
|
3156
|
+
column: this.getColumn(),
|
|
3157
|
+
values,
|
|
3158
|
+
caseInsensitive: true
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
noneOf(...args) {
|
|
3162
|
+
const values = this.flattenArgs(args);
|
|
3163
|
+
if (this.isCompoundIndex()) {
|
|
3164
|
+
return this.createCollectionWithJsPredicate(
|
|
3165
|
+
(record) => values.every((candidate) => this.table.compareValues(this.getIndexValue(record), candidate) !== 0)
|
|
3166
|
+
);
|
|
3167
|
+
}
|
|
3168
|
+
return this.createCollectionWithCondition({
|
|
3169
|
+
type: "notIn",
|
|
3170
|
+
column: this.getColumn(),
|
|
3171
|
+
values
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
notEqual(value) {
|
|
3175
|
+
if (this.isCompoundIndex()) {
|
|
3176
|
+
return this.createCollectionWithJsPredicate((record) => this.table.compareValues(this.getIndexValue(record), value) !== 0);
|
|
3177
|
+
}
|
|
3178
|
+
return this.createCollectionWithCondition({
|
|
3179
|
+
type: "comparison",
|
|
3180
|
+
column: this.getColumn(),
|
|
3181
|
+
op: "!=",
|
|
3182
|
+
value
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3185
|
+
};
|
|
3186
|
+
|
|
3187
|
+
// src/storage/sqlite/SQLiteTable.ts
|
|
3188
|
+
var SQLiteTable = class {
|
|
3189
|
+
name;
|
|
3190
|
+
schema;
|
|
3191
|
+
hook = Object.freeze({});
|
|
3192
|
+
raw;
|
|
3193
|
+
adapter;
|
|
3194
|
+
columnNames;
|
|
3195
|
+
booleanColumns;
|
|
3196
|
+
constructor(adapter, schema) {
|
|
3197
|
+
this.adapter = adapter;
|
|
3198
|
+
this.schema = schema;
|
|
3199
|
+
this.name = schema.name;
|
|
3200
|
+
this.columnNames = Object.keys(schema.definition.columns ?? {});
|
|
3201
|
+
this.booleanColumns = new Set(
|
|
3202
|
+
Object.entries(schema.definition.columns ?? {}).filter(([_, col]) => col.type?.toUpperCase() === "BOOLEAN").map(([name]) => name)
|
|
3203
|
+
);
|
|
3204
|
+
this.raw = Object.freeze({
|
|
3205
|
+
add: this.baseAdd.bind(this),
|
|
3206
|
+
put: this.basePut.bind(this),
|
|
3207
|
+
update: this.baseUpdate.bind(this),
|
|
3208
|
+
delete: this.baseDelete.bind(this),
|
|
3209
|
+
get: this.get.bind(this),
|
|
3210
|
+
bulkAdd: this.baseBulkAdd.bind(this),
|
|
3211
|
+
bulkPut: this.baseBulkPut.bind(this),
|
|
3212
|
+
bulkUpdate: this.baseBulkUpdate.bind(this),
|
|
3213
|
+
bulkDelete: this.baseBulkDelete.bind(this),
|
|
3214
|
+
clear: this.baseClear.bind(this)
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
async add(item) {
|
|
3218
|
+
return this.baseAdd(item);
|
|
3219
|
+
}
|
|
3220
|
+
async put(item) {
|
|
3221
|
+
return this.basePut(item);
|
|
3222
|
+
}
|
|
3223
|
+
async update(key, changes) {
|
|
3224
|
+
return this.baseUpdate(key, changes);
|
|
3225
|
+
}
|
|
3226
|
+
async delete(key) {
|
|
3227
|
+
await this.baseDelete(key);
|
|
3228
|
+
}
|
|
3229
|
+
async clear() {
|
|
3230
|
+
await this.baseClear();
|
|
3231
|
+
}
|
|
3232
|
+
async baseClear() {
|
|
3233
|
+
await this.adapter.execute(`DELETE FROM ${quoteIdentifier(this.name)}`);
|
|
3234
|
+
}
|
|
3235
|
+
async get(key) {
|
|
3236
|
+
if (!key || typeof key !== "string") {
|
|
3237
|
+
return void 0;
|
|
3238
|
+
}
|
|
3239
|
+
const row = await this.fetchRow(key);
|
|
3240
|
+
return row ? this.cloneRecord(row) : void 0;
|
|
3241
|
+
}
|
|
3242
|
+
async toArray() {
|
|
3243
|
+
const entries = await this.getEntries();
|
|
3244
|
+
return entries.map((entry) => this.cloneRecord(entry.value));
|
|
3245
|
+
}
|
|
3246
|
+
async count() {
|
|
3247
|
+
const rows = await this.adapter.queryRows(`SELECT COUNT(*) as count FROM ${quoteIdentifier(this.name)}`);
|
|
3248
|
+
return Number(rows[0]?.count ?? 0);
|
|
3249
|
+
}
|
|
3250
|
+
async bulkAdd(items) {
|
|
3251
|
+
return this.baseBulkAdd(items);
|
|
3252
|
+
}
|
|
3253
|
+
async baseBulkAdd(items) {
|
|
3254
|
+
if (!items.length) return void 0;
|
|
3255
|
+
const columns = this.columnNames;
|
|
3256
|
+
const columnCount = columns.length;
|
|
3257
|
+
const maxParamsPerBatch = 500;
|
|
3258
|
+
const batchSize = Math.max(1, Math.floor(maxParamsPerBatch / columnCount));
|
|
3259
|
+
let lastKey = void 0;
|
|
3260
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
3261
|
+
const batch = items.slice(i, i + batchSize);
|
|
3262
|
+
const records = batch.map((item) => this.prepareRecordForWrite(item));
|
|
3263
|
+
const placeholderRow = `(${columns.map(() => "?").join(", ")})`;
|
|
3264
|
+
const placeholders = records.map(() => placeholderRow).join(", ");
|
|
3265
|
+
const values = [];
|
|
3266
|
+
for (const record of records) {
|
|
3267
|
+
values.push(...this.extractColumnValues(record));
|
|
3268
|
+
}
|
|
3269
|
+
await this.adapter.run(
|
|
3270
|
+
`INSERT INTO ${quoteIdentifier(this.name)} (${columns.map((c) => quoteIdentifier(c)).join(", ")}) VALUES ${placeholders}`,
|
|
3271
|
+
values
|
|
3272
|
+
);
|
|
3273
|
+
lastKey = records[records.length - 1][LOCAL_PK];
|
|
3274
|
+
}
|
|
3275
|
+
return lastKey;
|
|
3276
|
+
}
|
|
3277
|
+
async bulkPut(items) {
|
|
3278
|
+
return this.baseBulkPut(items);
|
|
3279
|
+
}
|
|
3280
|
+
async baseBulkPut(items) {
|
|
3281
|
+
if (!items.length) return void 0;
|
|
3282
|
+
const columns = this.columnNames;
|
|
3283
|
+
const columnCount = columns.length;
|
|
3284
|
+
const maxParamsPerBatch = 500;
|
|
3285
|
+
const batchSize = Math.max(1, Math.floor(maxParamsPerBatch / columnCount));
|
|
3286
|
+
let lastKey = void 0;
|
|
3287
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
3288
|
+
const batch = items.slice(i, i + batchSize);
|
|
3289
|
+
const records = batch.map((item) => this.prepareRecordForWrite(item));
|
|
3290
|
+
const placeholderRow = `(${columns.map(() => "?").join(", ")})`;
|
|
3291
|
+
const placeholders = records.map(() => placeholderRow).join(", ");
|
|
3292
|
+
const values = [];
|
|
3293
|
+
for (const record of records) {
|
|
3294
|
+
values.push(...this.extractColumnValues(record));
|
|
3295
|
+
}
|
|
3296
|
+
await this.adapter.run(
|
|
3297
|
+
`INSERT OR REPLACE INTO ${quoteIdentifier(this.name)} (${columns.map((c) => quoteIdentifier(c)).join(", ")}) VALUES ${placeholders}`,
|
|
3298
|
+
values
|
|
3299
|
+
);
|
|
3300
|
+
lastKey = records[records.length - 1][LOCAL_PK];
|
|
3301
|
+
}
|
|
3302
|
+
return lastKey;
|
|
3303
|
+
}
|
|
3304
|
+
async bulkGet(keys) {
|
|
3305
|
+
if (!keys.length) return [];
|
|
3306
|
+
const validKeys = keys.filter((k) => k && typeof k === "string");
|
|
3307
|
+
if (!validKeys.length) return keys.map(() => void 0);
|
|
3308
|
+
const selectClause = this.buildSelectClause();
|
|
3309
|
+
const placeholders = validKeys.map(() => "?").join(", ");
|
|
3310
|
+
const rows = await this.adapter.queryRows(
|
|
3311
|
+
`SELECT ${selectClause} FROM ${quoteIdentifier(this.name)} WHERE ${quoteIdentifier(LOCAL_PK)} IN (${placeholders})`,
|
|
3312
|
+
validKeys
|
|
3313
|
+
);
|
|
3314
|
+
const recordMap = /* @__PURE__ */ new Map();
|
|
3315
|
+
for (const row of rows) {
|
|
3316
|
+
const record = this.hydrateRow(row);
|
|
3317
|
+
recordMap.set(String(row[LOCAL_PK]), this.cloneRecord(record));
|
|
3318
|
+
}
|
|
3319
|
+
return keys.map((key) => key && typeof key === "string" ? recordMap.get(key) : void 0);
|
|
3320
|
+
}
|
|
3321
|
+
async bulkUpdate(keysAndChanges) {
|
|
3322
|
+
return this.baseBulkUpdate(keysAndChanges);
|
|
3323
|
+
}
|
|
3324
|
+
async baseBulkUpdate(keysAndChanges) {
|
|
3325
|
+
if (!keysAndChanges.length) return 0;
|
|
3326
|
+
let updatedCount = 0;
|
|
3327
|
+
for (const { key, changes } of keysAndChanges) {
|
|
3328
|
+
const result = await this.baseUpdate(key, changes);
|
|
3329
|
+
updatedCount += result;
|
|
3330
|
+
}
|
|
3331
|
+
return updatedCount;
|
|
3332
|
+
}
|
|
3333
|
+
async bulkDelete(keys) {
|
|
3334
|
+
await this.baseBulkDelete(keys);
|
|
3335
|
+
}
|
|
3336
|
+
async baseBulkDelete(keys) {
|
|
3337
|
+
if (!keys.length) return;
|
|
3338
|
+
const validKeys = keys.filter((k) => k && typeof k === "string");
|
|
3339
|
+
if (!validKeys.length) return;
|
|
3340
|
+
const placeholders = validKeys.map(() => "?").join(", ");
|
|
3341
|
+
await this.adapter.run(`DELETE FROM ${quoteIdentifier(this.name)} WHERE ${quoteIdentifier(LOCAL_PK)} IN (${placeholders})`, validKeys);
|
|
3342
|
+
}
|
|
3343
|
+
where(index) {
|
|
3344
|
+
return this.createWhereClause(index);
|
|
3345
|
+
}
|
|
3346
|
+
orderBy(index) {
|
|
3347
|
+
return this.createCollection({
|
|
3348
|
+
orderBy: { index, direction: "asc" }
|
|
3349
|
+
});
|
|
3350
|
+
}
|
|
3351
|
+
reverse() {
|
|
3352
|
+
return this.createCollection({ reverse: true });
|
|
3353
|
+
}
|
|
3354
|
+
offset(offset) {
|
|
3355
|
+
return this.createCollection({ offset });
|
|
3356
|
+
}
|
|
3357
|
+
limit(count) {
|
|
3358
|
+
return this.createCollection({ limit: count });
|
|
3359
|
+
}
|
|
3360
|
+
mapToClass(_ctor) {
|
|
3361
|
+
return this;
|
|
3362
|
+
}
|
|
3363
|
+
async each(callback) {
|
|
3364
|
+
const entries = await this.getEntries();
|
|
3365
|
+
for (const entry of entries) {
|
|
3366
|
+
await callback(this.cloneRecord(entry.value));
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
jsFilter(predicate) {
|
|
3370
|
+
return this.createCollection({ jsPredicate: (record) => predicate(record) });
|
|
3371
|
+
}
|
|
3372
|
+
createCollection(stateOverrides) {
|
|
3373
|
+
return new SQLiteCollection2(this, stateOverrides);
|
|
3374
|
+
}
|
|
3375
|
+
createCollectionFromPredicate(predicate, template) {
|
|
3376
|
+
const baseState = template ? template.getState() : createDefaultState2();
|
|
3377
|
+
const existingPredicate = baseState.jsPredicate;
|
|
3378
|
+
const combinedPredicate = existingPredicate ? (record, key, index) => existingPredicate(record, key, index) && predicate(record, key, index) : predicate;
|
|
3379
|
+
return new SQLiteCollection2(this, {
|
|
3380
|
+
...baseState,
|
|
3381
|
+
jsPredicate: combinedPredicate
|
|
3382
|
+
});
|
|
3383
|
+
}
|
|
3384
|
+
createWhereClause(index, baseCollection) {
|
|
3385
|
+
return new SQLiteWhereClause(this, index, baseCollection);
|
|
3386
|
+
}
|
|
3387
|
+
async *iterateEntries(options) {
|
|
3388
|
+
const selectClause = this.buildSelectClause();
|
|
3389
|
+
const chunkSize = options?.chunkSize ?? DEFAULT_STREAM_BATCH_SIZE;
|
|
3390
|
+
const orderClause = this.buildOrderByClause(options?.orderBy);
|
|
3391
|
+
let offset = 0;
|
|
3392
|
+
while (true) {
|
|
3393
|
+
const statementParts = [`SELECT ${selectClause} FROM ${quoteIdentifier(this.name)}`];
|
|
3394
|
+
if (orderClause) {
|
|
3395
|
+
statementParts.push(orderClause);
|
|
3396
|
+
}
|
|
3397
|
+
statementParts.push(`LIMIT ${chunkSize} OFFSET ${offset}`);
|
|
3398
|
+
const rows = await this.adapter.queryRows(statementParts.join(" "));
|
|
3399
|
+
if (!rows.length) {
|
|
3400
|
+
break;
|
|
3401
|
+
}
|
|
3402
|
+
for (const row of rows) {
|
|
3403
|
+
yield { key: String(row[LOCAL_PK]), value: this.hydrateRow(row) };
|
|
3404
|
+
}
|
|
3405
|
+
if (rows.length < chunkSize) {
|
|
3406
|
+
break;
|
|
3407
|
+
}
|
|
3408
|
+
offset += rows.length;
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
/**
|
|
3412
|
+
* Execute a query with pre-built WHERE clause and parameters.
|
|
3413
|
+
* This is used by SQLiteCollection for native SQL performance.
|
|
3414
|
+
*/
|
|
3415
|
+
async queryWithConditions(options) {
|
|
3416
|
+
const selectClause = this.buildSelectClause();
|
|
3417
|
+
const distinctKeyword = options.distinct ? "DISTINCT " : "";
|
|
3418
|
+
const parts = [`SELECT ${distinctKeyword}${selectClause} FROM ${quoteIdentifier(this.name)}`];
|
|
3419
|
+
if (options.whereClause) {
|
|
3420
|
+
parts.push(options.whereClause);
|
|
3421
|
+
}
|
|
3422
|
+
if (options.orderBy) {
|
|
3423
|
+
parts.push(this.buildOrderByClause(options.orderBy));
|
|
3424
|
+
}
|
|
3425
|
+
if (options.limit !== void 0) {
|
|
3426
|
+
parts.push(`LIMIT ${options.limit}`);
|
|
3427
|
+
}
|
|
3428
|
+
if (options.offset !== void 0 && options.offset > 0) {
|
|
3429
|
+
parts.push(`OFFSET ${options.offset}`);
|
|
3430
|
+
}
|
|
3431
|
+
const rows = await this.adapter.queryRows(parts.join(" "), options.parameters);
|
|
3432
|
+
return rows.map((row) => this.hydrateRow(row));
|
|
3433
|
+
}
|
|
3434
|
+
/**
|
|
3435
|
+
* Execute a COUNT query with pre-built WHERE clause.
|
|
3436
|
+
*/
|
|
3437
|
+
async countWithConditions(options) {
|
|
3438
|
+
const distinctKeyword = options.distinct ? "DISTINCT " : "";
|
|
3439
|
+
const selectClause = this.buildSelectClause();
|
|
3440
|
+
const countExpr = options.distinct ? `COUNT(${distinctKeyword}${selectClause})` : "COUNT(*)";
|
|
3441
|
+
const parts = [`SELECT ${countExpr} as count FROM ${quoteIdentifier(this.name)}`];
|
|
3442
|
+
if (options.whereClause) {
|
|
3443
|
+
parts.push(options.whereClause);
|
|
3444
|
+
}
|
|
3445
|
+
const rows = await this.adapter.queryRows(parts.join(" "), options.parameters);
|
|
3446
|
+
return Number(rows[0]?.count ?? 0);
|
|
3447
|
+
}
|
|
3448
|
+
/**
|
|
3449
|
+
* Execute a DELETE query with pre-built WHERE clause.
|
|
3450
|
+
* Returns the number of deleted rows.
|
|
3451
|
+
*/
|
|
3452
|
+
async deleteWithConditions(options) {
|
|
3453
|
+
const parts = [`DELETE FROM ${quoteIdentifier(this.name)}`];
|
|
3454
|
+
if (options.whereClause) {
|
|
3455
|
+
parts.push(options.whereClause);
|
|
3456
|
+
}
|
|
3457
|
+
const result = await this.adapter.run(parts.join(" "), options.parameters);
|
|
3458
|
+
return result.changes ?? 0;
|
|
3459
|
+
}
|
|
3460
|
+
/**
|
|
3461
|
+
* Execute an UPDATE query with pre-built WHERE clause.
|
|
3462
|
+
* Returns the number of updated rows.
|
|
3463
|
+
*/
|
|
3464
|
+
async updateWithConditions(options) {
|
|
3465
|
+
const changeEntries = Object.entries(options.changes);
|
|
3466
|
+
if (changeEntries.length === 0) {
|
|
3467
|
+
return 0;
|
|
3468
|
+
}
|
|
3469
|
+
const setClauses = changeEntries.map(([column]) => `${quoteIdentifier(column)} = ?`);
|
|
3470
|
+
const setValues = changeEntries.map(([, value]) => this.normalizeColumnValue(value));
|
|
3471
|
+
const parts = [`UPDATE ${quoteIdentifier(this.name)} SET ${setClauses.join(", ")}`];
|
|
3472
|
+
if (options.whereClause) {
|
|
3473
|
+
parts.push(options.whereClause);
|
|
3474
|
+
}
|
|
3475
|
+
const result = await this.adapter.run(parts.join(" "), [...setValues, ...options.parameters]);
|
|
3476
|
+
return result.changes ?? 0;
|
|
3477
|
+
}
|
|
3478
|
+
/**
|
|
3479
|
+
* Query only primary keys with pre-built WHERE clause.
|
|
3480
|
+
* This is more efficient than fetching full rows when only keys are needed.
|
|
3481
|
+
*/
|
|
3482
|
+
async queryKeysWithConditions(options) {
|
|
3483
|
+
const distinctKeyword = options.distinct ? "DISTINCT " : "";
|
|
3484
|
+
const parts = [`SELECT ${distinctKeyword}${quoteIdentifier(LOCAL_PK)} FROM ${quoteIdentifier(this.name)}`];
|
|
3485
|
+
if (options.whereClause) {
|
|
3486
|
+
parts.push(options.whereClause);
|
|
3487
|
+
}
|
|
3488
|
+
if (options.orderBy) {
|
|
3489
|
+
parts.push(this.buildOrderByClause(options.orderBy));
|
|
3490
|
+
}
|
|
3491
|
+
if (options.limit !== void 0) {
|
|
3492
|
+
parts.push(`LIMIT ${options.limit}`);
|
|
3493
|
+
}
|
|
3494
|
+
if (options.offset !== void 0 && options.offset > 0) {
|
|
3495
|
+
parts.push(`OFFSET ${options.offset}`);
|
|
3496
|
+
}
|
|
3497
|
+
const rows = await this.adapter.queryRows(parts.join(" "), options.parameters);
|
|
3498
|
+
return rows.map((row) => String(row[LOCAL_PK]));
|
|
3499
|
+
}
|
|
3500
|
+
async getEntries() {
|
|
3501
|
+
const entries = [];
|
|
3502
|
+
for await (const entry of this.iterateEntries()) {
|
|
3503
|
+
entries.push(entry);
|
|
3504
|
+
}
|
|
3505
|
+
return entries;
|
|
3506
|
+
}
|
|
3507
|
+
cloneRecord(record) {
|
|
3508
|
+
return cloneValue(record);
|
|
3509
|
+
}
|
|
3510
|
+
compareValues(left, right) {
|
|
3511
|
+
const normalizedLeft = normalizeComparableValue(left);
|
|
3512
|
+
const normalizedRight = normalizeComparableValue(right);
|
|
3513
|
+
if (normalizedLeft < normalizedRight) return -1;
|
|
3514
|
+
if (normalizedLeft > normalizedRight) return 1;
|
|
3515
|
+
return 0;
|
|
3516
|
+
}
|
|
3517
|
+
compareByIndex(left, right, index) {
|
|
3518
|
+
if (Array.isArray(index)) {
|
|
3519
|
+
for (const key of index) {
|
|
3520
|
+
const diff = this.compareValues(left[key], right[key]);
|
|
3521
|
+
if (diff !== 0) {
|
|
3522
|
+
return diff;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
return 0;
|
|
3526
|
+
}
|
|
3527
|
+
return this.compareValues(left[index], right[index]);
|
|
3528
|
+
}
|
|
3529
|
+
getIndexValue(record, index) {
|
|
3530
|
+
if (Array.isArray(index)) {
|
|
3531
|
+
return index.map((key) => record[key]);
|
|
3532
|
+
}
|
|
3533
|
+
return record[index];
|
|
3534
|
+
}
|
|
3535
|
+
async replaceRecord(record) {
|
|
3536
|
+
if (!this.columnNames.length) {
|
|
3537
|
+
return;
|
|
3538
|
+
}
|
|
3539
|
+
const assignments = this.columnNames.map((column) => `${quoteIdentifier(column)} = ?`).join(", ");
|
|
3540
|
+
const values = this.extractColumnValues(record);
|
|
3541
|
+
await this.adapter.run(`UPDATE ${quoteIdentifier(this.name)} SET ${assignments} WHERE ${quoteIdentifier(LOCAL_PK)} = ?`, [
|
|
3542
|
+
...values,
|
|
3543
|
+
record[LOCAL_PK]
|
|
3544
|
+
]);
|
|
3545
|
+
}
|
|
3546
|
+
async deleteByPrimaryKey(primaryKey) {
|
|
3547
|
+
await this.adapter.run(`DELETE FROM ${quoteIdentifier(this.name)} WHERE ${quoteIdentifier(LOCAL_PK)} = ?`, [primaryKey]);
|
|
3548
|
+
}
|
|
3549
|
+
async baseAdd(item) {
|
|
3550
|
+
const record = this.prepareRecordForWrite(item);
|
|
3551
|
+
const columns = this.columnNames;
|
|
3552
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
3553
|
+
const values = this.extractColumnValues(record);
|
|
3554
|
+
await this.adapter.run(
|
|
3555
|
+
`INSERT INTO ${quoteIdentifier(this.name)} (${columns.map((column) => quoteIdentifier(column)).join(", ")}) VALUES (${placeholders})`,
|
|
3556
|
+
values
|
|
3557
|
+
);
|
|
3558
|
+
return record[LOCAL_PK];
|
|
3559
|
+
}
|
|
3560
|
+
async basePut(item) {
|
|
3561
|
+
const record = this.prepareRecordForWrite(item);
|
|
3562
|
+
const columns = this.columnNames;
|
|
3563
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
3564
|
+
const values = this.extractColumnValues(record);
|
|
3565
|
+
await this.adapter.run(
|
|
3566
|
+
`INSERT OR REPLACE INTO ${quoteIdentifier(this.name)} (${columns.map((column) => quoteIdentifier(column)).join(", ")}) VALUES (${placeholders})`,
|
|
3567
|
+
values
|
|
3568
|
+
);
|
|
3569
|
+
return record[LOCAL_PK];
|
|
3570
|
+
}
|
|
3571
|
+
async baseUpdate(key, changes) {
|
|
3572
|
+
if (!key || typeof key !== "string") {
|
|
3573
|
+
return 0;
|
|
3574
|
+
}
|
|
3575
|
+
const existing = await this.fetchRow(key);
|
|
3576
|
+
if (!existing) {
|
|
3577
|
+
return 0;
|
|
3578
|
+
}
|
|
3579
|
+
const updated = { ...existing, ...changes };
|
|
3580
|
+
await this.replaceRecord(updated);
|
|
3581
|
+
return 1;
|
|
3582
|
+
}
|
|
3583
|
+
async baseDelete(key) {
|
|
3584
|
+
if (!key || typeof key !== "string") {
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
await this.deleteByPrimaryKey(key);
|
|
3588
|
+
}
|
|
3589
|
+
prepareRecordForWrite(item) {
|
|
3590
|
+
const clone = this.cloneRecord(item);
|
|
3591
|
+
const primaryValue = clone[LOCAL_PK];
|
|
3592
|
+
if (!primaryValue || typeof primaryValue !== "string") {
|
|
3593
|
+
throw new Error(`Missing required primary key field "${LOCAL_PK}" - a string value must be provided`);
|
|
3594
|
+
}
|
|
3595
|
+
return clone;
|
|
3596
|
+
}
|
|
3597
|
+
async fetchRow(primaryKey) {
|
|
3598
|
+
const selectClause = this.buildSelectClause();
|
|
3599
|
+
const rows = await this.adapter.queryRows(`SELECT ${selectClause} FROM ${quoteIdentifier(this.name)} WHERE ${quoteIdentifier(LOCAL_PK)} = ? LIMIT 1`, [
|
|
3600
|
+
primaryKey
|
|
3601
|
+
]);
|
|
3602
|
+
if (!rows.length) {
|
|
3603
|
+
return void 0;
|
|
3604
|
+
}
|
|
3605
|
+
return this.hydrateRow(rows[0]);
|
|
3606
|
+
}
|
|
3607
|
+
buildSelectClause() {
|
|
3608
|
+
const dataColumns = this.columnNames.map((column) => quoteIdentifier(column));
|
|
3609
|
+
return dataColumns.join(", ");
|
|
3610
|
+
}
|
|
3611
|
+
hydrateRow(row) {
|
|
3612
|
+
const record = {};
|
|
3613
|
+
for (const column of this.columnNames) {
|
|
3614
|
+
let value = row[column];
|
|
3615
|
+
if (this.booleanColumns.has(column) && value !== null && value !== void 0) {
|
|
3616
|
+
value = value === 1 || value === true;
|
|
3617
|
+
}
|
|
3618
|
+
record[column] = value;
|
|
3619
|
+
}
|
|
3620
|
+
return record;
|
|
3621
|
+
}
|
|
3622
|
+
buildOrderByClause(orderBy) {
|
|
3623
|
+
const target = orderBy ?? { index: LOCAL_PK, direction: "asc" };
|
|
3624
|
+
const columns = Array.isArray(target.index) ? target.index : [target.index];
|
|
3625
|
+
const direction = target.direction.toUpperCase();
|
|
3626
|
+
const clause = columns.map((column) => `${quoteIdentifier(column)} ${direction}`).join(", ");
|
|
3627
|
+
return `ORDER BY ${clause}`;
|
|
3628
|
+
}
|
|
3629
|
+
extractColumnValues(record) {
|
|
3630
|
+
return this.columnNames.map((column) => this.normalizeColumnValue(record[column]));
|
|
3631
|
+
}
|
|
3632
|
+
normalizeColumnValue(value) {
|
|
3633
|
+
if (value === void 0) {
|
|
3634
|
+
return null;
|
|
3635
|
+
}
|
|
3636
|
+
if (value instanceof Date) {
|
|
3637
|
+
return value.toISOString();
|
|
3638
|
+
}
|
|
3639
|
+
if (typeof value === "boolean") {
|
|
3640
|
+
return value ? 1 : 0;
|
|
3641
|
+
}
|
|
3642
|
+
return value;
|
|
3643
|
+
}
|
|
3644
|
+
};
|
|
3645
|
+
|
|
3646
|
+
// src/storage/sqlite/SQLiteCollection.ts
|
|
3647
|
+
var SQLiteCollection2 = class _SQLiteCollection {
|
|
3648
|
+
table;
|
|
3649
|
+
state;
|
|
3650
|
+
constructor(table, state) {
|
|
3651
|
+
this.table = table;
|
|
3652
|
+
const base = createDefaultState2();
|
|
3653
|
+
this.state = {
|
|
3654
|
+
...base,
|
|
3655
|
+
...state,
|
|
3656
|
+
sqlConditions: state?.sqlConditions ?? base.sqlConditions,
|
|
3657
|
+
jsPredicate: state?.jsPredicate
|
|
3658
|
+
};
|
|
3659
|
+
}
|
|
3660
|
+
getState() {
|
|
3661
|
+
return { ...this.state, sqlConditions: [...this.state.sqlConditions] };
|
|
3662
|
+
}
|
|
3663
|
+
// Add a SQL-expressible condition to this collection
|
|
3664
|
+
addSqlCondition(condition) {
|
|
3665
|
+
return new _SQLiteCollection(this.table, {
|
|
3666
|
+
...this.state,
|
|
3667
|
+
sqlConditions: [...this.state.sqlConditions, condition]
|
|
3668
|
+
});
|
|
3669
|
+
}
|
|
3670
|
+
replicate(overrides) {
|
|
3671
|
+
return new _SQLiteCollection(this.table, {
|
|
3672
|
+
...this.state,
|
|
3673
|
+
...overrides,
|
|
3674
|
+
sqlConditions: overrides?.sqlConditions ?? this.state.sqlConditions,
|
|
3675
|
+
jsPredicate: overrides?.jsPredicate !== void 0 ? overrides.jsPredicate : this.state.jsPredicate
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
withJsPredicate(predicate) {
|
|
3679
|
+
const existingPredicate = this.state.jsPredicate;
|
|
3680
|
+
const combined = existingPredicate ? (record, key, index) => existingPredicate(record, key, index) && predicate(record, key, index) : predicate;
|
|
3681
|
+
return new _SQLiteCollection(this.table, {
|
|
3682
|
+
...this.state,
|
|
3683
|
+
jsPredicate: combined
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3686
|
+
hasJsPredicate() {
|
|
3687
|
+
return this.state.jsPredicate !== void 0;
|
|
3688
|
+
}
|
|
3689
|
+
resolveOrdering() {
|
|
3690
|
+
const base = this.state.orderBy ?? { index: LOCAL_PK, direction: "asc" };
|
|
3691
|
+
const direction = this.state.reverse ? base.direction === "asc" ? "desc" : "asc" : base.direction;
|
|
3692
|
+
return { index: base.index, direction };
|
|
3693
|
+
}
|
|
3694
|
+
/**
|
|
3695
|
+
* Execute a native SQL query with all SQL conditions.
|
|
3696
|
+
* If there's a JS predicate, we fetch rows without LIMIT/OFFSET in SQL
|
|
3697
|
+
* and apply them after JS filtering.
|
|
3698
|
+
*/
|
|
3699
|
+
async executeQuery(options = {}) {
|
|
3700
|
+
const { whereClause, parameters } = buildWhereClause(this.state.sqlConditions);
|
|
3701
|
+
const ordering = options.orderByOverride ?? this.resolveOrdering();
|
|
3702
|
+
const cloneValues = options.clone !== false;
|
|
3703
|
+
const hasJsFilter = this.hasJsPredicate();
|
|
3704
|
+
const distinct = this.state.distinct;
|
|
3705
|
+
const sqlLimit = hasJsFilter ? void 0 : options.limitOverride ?? this.state.limit;
|
|
3706
|
+
const sqlOffset = hasJsFilter ? void 0 : this.state.offset;
|
|
3707
|
+
const rows = await this.table.queryWithConditions({
|
|
3708
|
+
whereClause,
|
|
3709
|
+
parameters,
|
|
3710
|
+
orderBy: ordering,
|
|
3711
|
+
limit: sqlLimit,
|
|
3712
|
+
offset: sqlOffset,
|
|
3713
|
+
distinct
|
|
3714
|
+
});
|
|
3715
|
+
let results = rows.map((row) => ({
|
|
3716
|
+
key: String(row[LOCAL_PK]),
|
|
3717
|
+
value: row
|
|
3718
|
+
}));
|
|
3719
|
+
if (hasJsFilter) {
|
|
3720
|
+
const predicate = this.state.jsPredicate;
|
|
3721
|
+
results = results.filter((entry, index) => predicate(entry.value, entry.key, index));
|
|
3722
|
+
const offset = Math.max(0, this.state.offset ?? 0);
|
|
3723
|
+
const limit = options.limitOverride ?? this.state.limit;
|
|
3724
|
+
if (offset > 0 || limit !== void 0) {
|
|
3725
|
+
const end = limit !== void 0 ? offset + limit : void 0;
|
|
3726
|
+
results = results.slice(offset, end);
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
if (cloneValues) {
|
|
3730
|
+
results = results.map((entry) => ({
|
|
3731
|
+
key: entry.key,
|
|
3732
|
+
value: this.table.cloneRecord(entry.value)
|
|
3733
|
+
}));
|
|
3734
|
+
}
|
|
3735
|
+
return results;
|
|
3736
|
+
}
|
|
3737
|
+
async first() {
|
|
3738
|
+
const entries = await this.executeQuery({ limitOverride: 1 });
|
|
3739
|
+
return entries[0]?.value;
|
|
3740
|
+
}
|
|
3741
|
+
async last() {
|
|
3742
|
+
return this.replicate({ reverse: !this.state.reverse }).first();
|
|
3743
|
+
}
|
|
3744
|
+
async each(callback) {
|
|
3745
|
+
const entries = await this.executeQuery();
|
|
3746
|
+
for (const [index, entry] of entries.entries()) {
|
|
3747
|
+
await callback(entry.value, index);
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
async eachKey(callback) {
|
|
3751
|
+
const entries = await this.executeQuery({ clone: false });
|
|
3752
|
+
for (const [index, entry] of entries.entries()) {
|
|
3753
|
+
await callback(entry.key, index);
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
async eachPrimaryKey(callback) {
|
|
3757
|
+
return this.eachKey(callback);
|
|
3758
|
+
}
|
|
3759
|
+
async eachUniqueKey(callback) {
|
|
3760
|
+
const keys = await this.uniqueKeys();
|
|
3761
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
3762
|
+
await callback(keys[index], index);
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
async keys() {
|
|
3766
|
+
if (!this.hasJsPredicate()) {
|
|
3767
|
+
const { whereClause, parameters } = buildWhereClause(this.state.sqlConditions);
|
|
3768
|
+
const ordering = this.resolveOrdering();
|
|
3769
|
+
return this.table.queryKeysWithConditions({
|
|
3770
|
+
whereClause,
|
|
3771
|
+
parameters,
|
|
3772
|
+
orderBy: ordering,
|
|
3773
|
+
limit: this.state.limit,
|
|
3774
|
+
offset: this.state.offset,
|
|
3775
|
+
distinct: this.state.distinct
|
|
3776
|
+
});
|
|
3777
|
+
}
|
|
3778
|
+
const entries = await this.executeQuery({ clone: false });
|
|
3779
|
+
return entries.map((entry) => entry.key);
|
|
3780
|
+
}
|
|
3781
|
+
async primaryKeys() {
|
|
3782
|
+
return this.keys();
|
|
3783
|
+
}
|
|
3784
|
+
async uniqueKeys() {
|
|
3785
|
+
if (!this.hasJsPredicate()) {
|
|
3786
|
+
const { whereClause, parameters } = buildWhereClause(this.state.sqlConditions);
|
|
3787
|
+
const ordering = this.resolveOrdering();
|
|
3788
|
+
return this.table.queryKeysWithConditions({
|
|
3789
|
+
whereClause,
|
|
3790
|
+
parameters,
|
|
3791
|
+
orderBy: ordering,
|
|
3792
|
+
limit: this.state.limit,
|
|
3793
|
+
offset: this.state.offset,
|
|
3794
|
+
distinct: true
|
|
3795
|
+
});
|
|
3796
|
+
}
|
|
3797
|
+
const keys = await this.keys();
|
|
3798
|
+
return [...new Set(keys)];
|
|
3799
|
+
}
|
|
3800
|
+
async count() {
|
|
3801
|
+
if (!this.hasJsPredicate()) {
|
|
3802
|
+
return this.table.countWithConditions({
|
|
3803
|
+
whereClause: buildWhereClause(this.state.sqlConditions).whereClause,
|
|
3804
|
+
parameters: buildWhereClause(this.state.sqlConditions).parameters,
|
|
3805
|
+
distinct: this.state.distinct
|
|
3806
|
+
});
|
|
3807
|
+
}
|
|
3808
|
+
const entries = await this.executeQuery({ clone: false });
|
|
3809
|
+
return entries.length;
|
|
3810
|
+
}
|
|
3811
|
+
async sortBy(key) {
|
|
3812
|
+
const entries = await this.executeQuery({
|
|
3813
|
+
orderByOverride: { index: key, direction: "asc" }
|
|
3814
|
+
});
|
|
3815
|
+
return entries.map((entry) => entry.value);
|
|
3816
|
+
}
|
|
3817
|
+
distinct() {
|
|
3818
|
+
return this.replicate({ distinct: true });
|
|
3819
|
+
}
|
|
3820
|
+
jsFilter(predicate) {
|
|
3821
|
+
return this.withJsPredicate((record) => predicate(cloneValue(record)));
|
|
3822
|
+
}
|
|
3823
|
+
or(index) {
|
|
3824
|
+
return this.table.createWhereClause(index, this);
|
|
3825
|
+
}
|
|
3826
|
+
clone(_props) {
|
|
3827
|
+
return this.replicate();
|
|
3828
|
+
}
|
|
3829
|
+
reverse() {
|
|
3830
|
+
return this.replicate({ reverse: !this.state.reverse });
|
|
3831
|
+
}
|
|
3832
|
+
offset(offset) {
|
|
3833
|
+
return this.replicate({ offset });
|
|
3834
|
+
}
|
|
3835
|
+
limit(count) {
|
|
3836
|
+
return this.replicate({ limit: count });
|
|
3837
|
+
}
|
|
3838
|
+
toCollection() {
|
|
3839
|
+
return this.replicate();
|
|
3840
|
+
}
|
|
3841
|
+
async delete() {
|
|
3842
|
+
if (!this.hasJsPredicate()) {
|
|
3843
|
+
const { whereClause, parameters } = buildWhereClause(this.state.sqlConditions);
|
|
3844
|
+
return this.table.deleteWithConditions({ whereClause, parameters });
|
|
3845
|
+
}
|
|
3846
|
+
const entries = await this.executeQuery({ clone: false });
|
|
3847
|
+
for (const entry of entries) {
|
|
3848
|
+
await this.table.deleteByPrimaryKey(entry.key);
|
|
3849
|
+
}
|
|
3850
|
+
return entries.length;
|
|
3851
|
+
}
|
|
3852
|
+
async modify(changes) {
|
|
3853
|
+
if (typeof changes !== "function" && !this.hasJsPredicate()) {
|
|
3854
|
+
const { whereClause, parameters } = buildWhereClause(this.state.sqlConditions);
|
|
3855
|
+
return this.table.updateWithConditions({ whereClause, parameters, changes });
|
|
3856
|
+
}
|
|
3857
|
+
const entries = await this.executeQuery();
|
|
3858
|
+
for (const entry of entries) {
|
|
3859
|
+
const draft = cloneValue(entry.value);
|
|
3860
|
+
if (typeof changes === "function") {
|
|
3861
|
+
await changes(draft);
|
|
3862
|
+
} else {
|
|
3863
|
+
Object.assign(draft, changes);
|
|
3864
|
+
}
|
|
3865
|
+
await this.table.replaceRecord(draft);
|
|
3866
|
+
}
|
|
3867
|
+
return entries.length;
|
|
3868
|
+
}
|
|
3869
|
+
async toArray() {
|
|
3870
|
+
const entries = await this.executeQuery();
|
|
3871
|
+
return entries.map((entry) => entry.value);
|
|
3872
|
+
}
|
|
3873
|
+
};
|
|
3874
|
+
|
|
3875
|
+
export {
|
|
3876
|
+
SyncAction,
|
|
3877
|
+
createLocalId,
|
|
3878
|
+
Dync,
|
|
3879
|
+
MemoryQueryContext,
|
|
3880
|
+
MemoryAdapter,
|
|
3881
|
+
SqliteQueryContext,
|
|
3882
|
+
SQLiteAdapter2 as SQLiteAdapter
|
|
3883
|
+
};
|
|
3884
|
+
//# sourceMappingURL=chunk-LGHOZECP.js.map
|