@anfenn/dync 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-LGHOZECP.js → chunk-66PSQW4D.js} +120 -120
- package/dist/chunk-66PSQW4D.js.map +1 -0
- package/dist/index.cjs +119 -119
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{index.shared-YSn6c01d.d.cts → index.shared-BGwvMH8f.d.cts} +3 -3
- package/dist/{index.shared-CPIge2ZM.d.ts → index.shared-CkYsQYyn.d.ts} +3 -3
- package/dist/react/index.cjs +119 -119
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/package.json +6 -4
- package/src/core/StateManager.ts +15 -15
- package/src/core/firstLoad.ts +27 -27
- package/src/core/pullOperations.ts +28 -28
- package/src/core/pushOperations.ts +41 -41
- package/src/core/tableEnhancers.ts +9 -9
- package/src/index.shared.ts +3 -3
- package/src/types.ts +3 -3
- package/dist/chunk-LGHOZECP.js.map +0 -1
package/dist/react/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ApiFunctions, h as SyncOptions, B as BatchSync, D as Dync, i as SyncState } from '../index.shared-
|
|
1
|
+
import { a as ApiFunctions, h as SyncOptions, B as BatchSync, D as Dync, i as SyncState } from '../index.shared-BGwvMH8f.cjs';
|
|
2
2
|
import { c as StorageAdapter } from '../dexie-Bv-fV10P.cjs';
|
|
3
3
|
import '../types-CSbIAfu2.cjs';
|
|
4
4
|
import 'dexie';
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ApiFunctions, h as SyncOptions, B as BatchSync, D as Dync, i as SyncState } from '../index.shared-
|
|
1
|
+
import { a as ApiFunctions, h as SyncOptions, B as BatchSync, D as Dync, i as SyncState } from '../index.shared-CkYsQYyn.js';
|
|
2
2
|
import { c as StorageAdapter } from '../dexie-DJFApKsM.js';
|
|
3
3
|
import '../types-CSbIAfu2.js';
|
|
4
4
|
import 'dexie';
|
package/dist/react/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anfenn/dync",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Write once, run IndexedDB & SQLite with sync anywhere - React, React Native, Expo, Capacitor, Electron & Node.js",
|
|
6
6
|
"keywords": [],
|
|
@@ -96,9 +96,6 @@
|
|
|
96
96
|
"build": "pnpm run format && pnpm run lint && pnpm exec tsc --noEmit && tsup",
|
|
97
97
|
"build:all": "pnpm run build && pnpm --filter react-capacitor build",
|
|
98
98
|
"dev": "tsup --watch",
|
|
99
|
-
"dev:capacitor": "pnpm --filter react-capacitor run dev",
|
|
100
|
-
"dev:expo:web": "pnpm --filter react-native-expo-sqlite web --clear",
|
|
101
|
-
"dev:expo:start": "pnpm --filter react-native-expo-sqlite start --clear",
|
|
102
99
|
"lint": "eslint . --fix \"src/**/*.{js,jsx,ts,tsx,json}\"",
|
|
103
100
|
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
|
104
101
|
"size": "pnpm --filter size size",
|
|
@@ -167,5 +164,10 @@
|
|
|
167
164
|
"esbuild",
|
|
168
165
|
"unrs-resolver"
|
|
169
166
|
]
|
|
167
|
+
},
|
|
168
|
+
"dependencies": {
|
|
169
|
+
"expo": "~54.0.29",
|
|
170
|
+
"react": "19.1.0",
|
|
171
|
+
"react-native": "0.81.5"
|
|
170
172
|
}
|
|
171
173
|
}
|
package/src/core/StateManager.ts
CHANGED
|
@@ -30,10 +30,10 @@ export interface StateHelpers {
|
|
|
30
30
|
setState(setterOrState: PersistedSyncState | ((state: PersistedSyncState) => Partial<PersistedSyncState>)): Promise<void>;
|
|
31
31
|
setErrorInMemory(error: Error): void;
|
|
32
32
|
addPendingChange(change: Omit<PendingChange, 'version'>): Promise<void>;
|
|
33
|
-
samePendingVersion(
|
|
34
|
-
removePendingChange(localId: string,
|
|
35
|
-
updatePendingChange(
|
|
36
|
-
setPendingChangeBefore(
|
|
33
|
+
samePendingVersion(tableName: string, localId: string, version: number): boolean;
|
|
34
|
+
removePendingChange(localId: string, tableName: string): Promise<void>;
|
|
35
|
+
updatePendingChange(tableName: string, localId: string, action: SyncAction, id?: any): Promise<void>;
|
|
36
|
+
setPendingChangeBefore(tableName: string, localId: string, before: any): Promise<void>;
|
|
37
37
|
hasConflicts(localId: string): boolean;
|
|
38
38
|
getSyncStatus(): SyncStatus;
|
|
39
39
|
setSyncStatus(status: SyncStatus): void;
|
|
@@ -111,7 +111,7 @@ export class StateManager implements StateHelpers {
|
|
|
111
111
|
|
|
112
112
|
addPendingChange(change: Omit<PendingChange, 'version'>): Promise<void> {
|
|
113
113
|
const next = clonePersistedState(this.persistedState);
|
|
114
|
-
const queueItem = next.pendingChanges.find((p) => p.localId === change.localId && p.
|
|
114
|
+
const queueItem = next.pendingChanges.find((p) => p.localId === change.localId && p.tableName === change.tableName);
|
|
115
115
|
|
|
116
116
|
const omittedChanges = omitFields(change.changes, LOCAL_ONLY_SYNC_FIELDS);
|
|
117
117
|
const omittedBefore = omitFields(change.before, LOCAL_ONLY_SYNC_FIELDS);
|
|
@@ -136,7 +136,7 @@ export class StateManager implements StateHelpers {
|
|
|
136
136
|
next.pendingChanges = [...next.pendingChanges];
|
|
137
137
|
next.pendingChanges.push({
|
|
138
138
|
action,
|
|
139
|
-
|
|
139
|
+
tableName: change.tableName,
|
|
140
140
|
localId: change.localId,
|
|
141
141
|
id: change.id,
|
|
142
142
|
version: 1,
|
|
@@ -150,20 +150,20 @@ export class StateManager implements StateHelpers {
|
|
|
150
150
|
return this.persist();
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
samePendingVersion(
|
|
154
|
-
return this.persistedState.pendingChanges.find((p) => p.localId === localId && p.
|
|
153
|
+
samePendingVersion(tableName: string, localId: string, version: number): boolean {
|
|
154
|
+
return this.persistedState.pendingChanges.find((p) => p.localId === localId && p.tableName === tableName)?.version === version;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
removePendingChange(localId: string,
|
|
157
|
+
removePendingChange(localId: string, tableName: string): Promise<void> {
|
|
158
158
|
const next = clonePersistedState(this.persistedState);
|
|
159
|
-
next.pendingChanges = next.pendingChanges.filter((p) => !(p.localId === localId && p.
|
|
159
|
+
next.pendingChanges = next.pendingChanges.filter((p) => !(p.localId === localId && p.tableName === tableName));
|
|
160
160
|
this.persistedState = next;
|
|
161
161
|
return this.persist();
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
updatePendingChange(
|
|
164
|
+
updatePendingChange(tableName: string, localId: string, action: SyncAction, id?: any): Promise<void> {
|
|
165
165
|
const next = clonePersistedState(this.persistedState);
|
|
166
|
-
const changeItem = next.pendingChanges.find((p) => p.
|
|
166
|
+
const changeItem = next.pendingChanges.find((p) => p.tableName === tableName && p.localId === localId);
|
|
167
167
|
if (changeItem) {
|
|
168
168
|
changeItem.action = action;
|
|
169
169
|
if (id) changeItem.id = id;
|
|
@@ -173,9 +173,9 @@ export class StateManager implements StateHelpers {
|
|
|
173
173
|
return Promise.resolve();
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
setPendingChangeBefore(
|
|
176
|
+
setPendingChangeBefore(tableName: string, localId: string, before: any): Promise<void> {
|
|
177
177
|
const next = clonePersistedState(this.persistedState);
|
|
178
|
-
const changeItem = next.pendingChanges.find((p) => p.
|
|
178
|
+
const changeItem = next.pendingChanges.find((p) => p.tableName === tableName && p.localId === localId);
|
|
179
179
|
if (changeItem) {
|
|
180
180
|
changeItem.before = { ...(changeItem.before ?? {}), ...before };
|
|
181
181
|
this.persistedState = next;
|
|
@@ -259,7 +259,7 @@ function cloneConflicts(conflicts: SyncState['conflicts'] | undefined): SyncStat
|
|
|
259
259
|
const next: NonNullable<SyncState['conflicts']> = {};
|
|
260
260
|
for (const [key, value] of Object.entries(conflicts)) {
|
|
261
261
|
next[key] = {
|
|
262
|
-
|
|
262
|
+
tableName: value.tableName,
|
|
263
263
|
fields: value.fields.map((field) => ({ ...field })),
|
|
264
264
|
};
|
|
265
265
|
}
|
package/src/core/firstLoad.ts
CHANGED
|
@@ -44,14 +44,14 @@ export async function startFirstLoad(ctx: FirstLoadContext): Promise<void> {
|
|
|
44
44
|
|
|
45
45
|
let error: Error | undefined;
|
|
46
46
|
|
|
47
|
-
for (const [
|
|
47
|
+
for (const [tableName, api] of Object.entries(ctx.syncApis)) {
|
|
48
48
|
if (!api.firstLoad) {
|
|
49
|
-
ctx.logger.error(`[dync] firstLoad:no-api-function
|
|
49
|
+
ctx.logger.error(`[dync] firstLoad:no-api-function tableName=${tableName}`);
|
|
50
50
|
continue;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
try {
|
|
54
|
-
ctx.logger.info(`[dync] firstLoad:start
|
|
54
|
+
ctx.logger.info(`[dync] firstLoad:start tableName=${tableName}`);
|
|
55
55
|
|
|
56
56
|
let lastId: unknown;
|
|
57
57
|
let isEmptyTable = true;
|
|
@@ -66,14 +66,14 @@ export async function startFirstLoad(ctx: FirstLoadContext): Promise<void> {
|
|
|
66
66
|
batchCount++;
|
|
67
67
|
|
|
68
68
|
// Process batch in smaller chunks to manage memory and allow UI updates
|
|
69
|
-
const { inserted, updated } = await processBatchInChunks(ctx,
|
|
69
|
+
const { inserted, updated } = await processBatchInChunks(ctx, tableName, batch, isEmptyTable, lastId === undefined);
|
|
70
70
|
totalInserted += inserted;
|
|
71
71
|
totalUpdated += updated;
|
|
72
72
|
|
|
73
73
|
// Report progress if callback provided
|
|
74
74
|
if (ctx.onProgress) {
|
|
75
75
|
ctx.onProgress({
|
|
76
|
-
table:
|
|
76
|
+
table: tableName,
|
|
77
77
|
inserted: totalInserted,
|
|
78
78
|
updated: totalUpdated,
|
|
79
79
|
total: totalInserted + totalUpdated,
|
|
@@ -82,7 +82,7 @@ export async function startFirstLoad(ctx: FirstLoadContext): Promise<void> {
|
|
|
82
82
|
|
|
83
83
|
// After first batch, we know if table was empty
|
|
84
84
|
if (lastId === undefined) {
|
|
85
|
-
isEmptyTable = (await ctx.table(
|
|
85
|
+
isEmptyTable = (await ctx.table(tableName).count()) === batch.length;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
if (lastId !== undefined && lastId === batch[batch.length - 1].id) {
|
|
@@ -97,10 +97,10 @@ export async function startFirstLoad(ctx: FirstLoadContext): Promise<void> {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
ctx.logger.info(`[dync] firstLoad:done
|
|
100
|
+
ctx.logger.info(`[dync] firstLoad:done tableName=${tableName} inserted=${totalInserted} updated=${totalUpdated}`);
|
|
101
101
|
} catch (err) {
|
|
102
102
|
error = error ?? (err as Error);
|
|
103
|
-
ctx.logger.error(`[dync] firstLoad:error
|
|
103
|
+
ctx.logger.error(`[dync] firstLoad:error tableName=${tableName}`, err);
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -120,15 +120,15 @@ interface BatchResult {
|
|
|
120
120
|
|
|
121
121
|
async function processBatchInChunks(
|
|
122
122
|
ctx: FirstLoadBaseContext,
|
|
123
|
-
|
|
123
|
+
tableName: string,
|
|
124
124
|
batch: RemoteRecord[],
|
|
125
125
|
isEmptyTable: boolean,
|
|
126
126
|
isFirstBatch: boolean,
|
|
127
127
|
): Promise<BatchResult> {
|
|
128
|
-
let newest = new Date(ctx.state.getState().lastPulled[
|
|
128
|
+
let newest = new Date(ctx.state.getState().lastPulled[tableName] || 0);
|
|
129
129
|
|
|
130
|
-
return ctx.withTransaction('rw', [
|
|
131
|
-
const txTable = tables[
|
|
130
|
+
return ctx.withTransaction('rw', [tableName, DYNC_STATE_TABLE], async (tables) => {
|
|
131
|
+
const txTable = tables[tableName]!;
|
|
132
132
|
|
|
133
133
|
// Check if table is empty on first batch
|
|
134
134
|
let tableIsEmpty = isEmptyTable;
|
|
@@ -176,7 +176,7 @@ async function processBatchInChunks(
|
|
|
176
176
|
...syncState,
|
|
177
177
|
lastPulled: {
|
|
178
178
|
...syncState.lastPulled,
|
|
179
|
-
[
|
|
179
|
+
[tableName]: newest.toISOString(),
|
|
180
180
|
},
|
|
181
181
|
}));
|
|
182
182
|
|
|
@@ -274,28 +274,28 @@ export async function startFirstLoadBatch(ctx: FirstLoadBatchContext): Promise<v
|
|
|
274
274
|
batchCount++;
|
|
275
275
|
|
|
276
276
|
// Process each table's data
|
|
277
|
-
for (const [
|
|
278
|
-
if (!ctx.batchSync.syncTables.includes(
|
|
279
|
-
ctx.logger.warn(`[dync] firstLoad:batch:unknown-table
|
|
277
|
+
for (const [tableName, batch] of Object.entries(result.data)) {
|
|
278
|
+
if (!ctx.batchSync.syncTables.includes(tableName)) {
|
|
279
|
+
ctx.logger.warn(`[dync] firstLoad:batch:unknown-table tableName=${tableName}`);
|
|
280
280
|
continue;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
if (!batch?.length) continue;
|
|
284
284
|
|
|
285
|
-
const isFirstBatch = progress[
|
|
286
|
-
const isEmptyTable = isFirstBatch && (await ctx.table(
|
|
285
|
+
const isFirstBatch = progress[tableName]!.inserted === 0 && progress[tableName]!.updated === 0;
|
|
286
|
+
const isEmptyTable = isFirstBatch && (await ctx.table(tableName).count()) === 0;
|
|
287
287
|
|
|
288
|
-
const { inserted, updated } = await processBatchInChunks(ctx,
|
|
289
|
-
progress[
|
|
290
|
-
progress[
|
|
288
|
+
const { inserted, updated } = await processBatchInChunks(ctx, tableName, batch, isEmptyTable, isFirstBatch);
|
|
289
|
+
progress[tableName]!.inserted += inserted;
|
|
290
|
+
progress[tableName]!.updated += updated;
|
|
291
291
|
|
|
292
292
|
// Report progress if callback provided
|
|
293
293
|
if (ctx.onProgress) {
|
|
294
294
|
ctx.onProgress({
|
|
295
|
-
table:
|
|
296
|
-
inserted: progress[
|
|
297
|
-
updated: progress[
|
|
298
|
-
total: progress[
|
|
295
|
+
table: tableName,
|
|
296
|
+
inserted: progress[tableName]!.inserted,
|
|
297
|
+
updated: progress[tableName]!.updated,
|
|
298
|
+
total: progress[tableName]!.inserted + progress[tableName]!.updated,
|
|
299
299
|
});
|
|
300
300
|
}
|
|
301
301
|
}
|
|
@@ -314,8 +314,8 @@ export async function startFirstLoadBatch(ctx: FirstLoadBatchContext): Promise<v
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
// Log completion for each table
|
|
317
|
-
for (const [
|
|
318
|
-
ctx.logger.info(`[dync] firstLoad:batch:done
|
|
317
|
+
for (const [tableName, p] of Object.entries(progress)) {
|
|
318
|
+
ctx.logger.info(`[dync] firstLoad:batch:done tableName=${tableName} inserted=${p.inserted} updated=${p.updated}`);
|
|
319
319
|
}
|
|
320
320
|
} catch (err) {
|
|
321
321
|
error = err as Error;
|
|
@@ -25,30 +25,30 @@ export interface PullAllBatchContext extends PullContext {
|
|
|
25
25
|
export async function pullAll(ctx: PullAllContext): Promise<{ error?: Error; changedTables: string[] }> {
|
|
26
26
|
let firstSyncError: Error | undefined;
|
|
27
27
|
const changedTables: string[] = [];
|
|
28
|
-
for (const [
|
|
28
|
+
for (const [tableName, api] of Object.entries(ctx.syncApis)) {
|
|
29
29
|
try {
|
|
30
|
-
const lastPulled = ctx.state.getState().lastPulled[
|
|
30
|
+
const lastPulled = ctx.state.getState().lastPulled[tableName];
|
|
31
31
|
const since = lastPulled ? new Date(lastPulled) : new Date(0);
|
|
32
32
|
|
|
33
|
-
ctx.logger.debug(`[dync] pull:start
|
|
33
|
+
ctx.logger.debug(`[dync] pull:start tableName=${tableName} since=${since.toISOString()}`);
|
|
34
34
|
|
|
35
35
|
const serverData = (await api.list(since)) as SyncedRecord[];
|
|
36
|
-
const changed = await processPullData(
|
|
37
|
-
if (changed) changedTables.push(
|
|
36
|
+
const changed = await processPullData(tableName, serverData, since, ctx);
|
|
37
|
+
if (changed) changedTables.push(tableName);
|
|
38
38
|
} catch (err) {
|
|
39
39
|
firstSyncError = firstSyncError ?? (err as Error);
|
|
40
|
-
ctx.logger.error(`[dync] pull:error
|
|
40
|
+
ctx.logger.error(`[dync] pull:error tableName=${tableName}`, err);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
return { error: firstSyncError, changedTables };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
async function handleRemoteItemUpdate(table: StorageTable<any>,
|
|
47
|
-
const pendingChange = ctx.state.getState().pendingChanges.find((p) => p.
|
|
46
|
+
async function handleRemoteItemUpdate(table: StorageTable<any>, tableName: string, localItem: any, remote: any, ctx: PullContext): Promise<void> {
|
|
47
|
+
const pendingChange = ctx.state.getState().pendingChanges.find((p) => p.tableName === tableName && p.localId === localItem._localId);
|
|
48
48
|
const conflictStrategy = ctx.conflictResolutionStrategy;
|
|
49
49
|
|
|
50
50
|
if (pendingChange) {
|
|
51
|
-
ctx.logger.debug(`[dync] pull:conflict-strategy:${conflictStrategy}
|
|
51
|
+
ctx.logger.debug(`[dync] pull:conflict-strategy:${conflictStrategy} tableName=${tableName} id=${remote.id}`);
|
|
52
52
|
|
|
53
53
|
switch (conflictStrategy) {
|
|
54
54
|
case 'local-wins':
|
|
@@ -57,7 +57,7 @@ async function handleRemoteItemUpdate(table: StorageTable<any>, stateKey: string
|
|
|
57
57
|
case 'remote-wins': {
|
|
58
58
|
const merged = { ...remote, _localId: localItem._localId };
|
|
59
59
|
await table.raw.update(localItem._localId, merged);
|
|
60
|
-
await ctx.state.removePendingChange(localItem._localId,
|
|
60
|
+
await ctx.state.removePendingChange(localItem._localId, tableName);
|
|
61
61
|
break;
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -75,7 +75,7 @@ async function handleRemoteItemUpdate(table: StorageTable<any>, stateKey: string
|
|
|
75
75
|
...syncState,
|
|
76
76
|
conflicts: {
|
|
77
77
|
...(syncState.conflicts || {}),
|
|
78
|
-
[localItem._localId]: {
|
|
78
|
+
[localItem._localId]: { tableName, fields },
|
|
79
79
|
},
|
|
80
80
|
}));
|
|
81
81
|
} else {
|
|
@@ -101,7 +101,7 @@ async function handleRemoteItemUpdate(table: StorageTable<any>, stateKey: string
|
|
|
101
101
|
} else {
|
|
102
102
|
const merged = { ...localItem, ...remote };
|
|
103
103
|
await table.raw.update(localItem._localId, merged);
|
|
104
|
-
ctx.logger.debug(`[dync] pull:merge-remote
|
|
104
|
+
ctx.logger.debug(`[dync] pull:merge-remote tableName=${tableName} id=${remote.id}`);
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -127,18 +127,18 @@ export async function pullAllBatch(ctx: PullAllBatchContext): Promise<{ error?:
|
|
|
127
127
|
const serverDataByTable = await ctx.batchSync.pull(sinceMap);
|
|
128
128
|
|
|
129
129
|
// Process each table's data
|
|
130
|
-
for (const [
|
|
131
|
-
if (!ctx.batchSync.syncTables.includes(
|
|
132
|
-
ctx.logger.warn(`[dync] pull:batch:unknown-table
|
|
130
|
+
for (const [tableName, serverData] of Object.entries(serverDataByTable)) {
|
|
131
|
+
if (!ctx.batchSync.syncTables.includes(tableName)) {
|
|
132
|
+
ctx.logger.warn(`[dync] pull:batch:unknown-table tableName=${tableName}`);
|
|
133
133
|
continue;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
try {
|
|
137
|
-
const changed = await processPullData(
|
|
138
|
-
if (changed) changedTables.push(
|
|
137
|
+
const changed = await processPullData(tableName, serverData as SyncedRecord[], sinceMap[tableName]!, ctx);
|
|
138
|
+
if (changed) changedTables.push(tableName);
|
|
139
139
|
} catch (err) {
|
|
140
140
|
firstSyncError = firstSyncError ?? (err as Error);
|
|
141
|
-
ctx.logger.error(`[dync] pull:batch:error
|
|
141
|
+
ctx.logger.error(`[dync] pull:batch:error tableName=${tableName}`, err);
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
} catch (err) {
|
|
@@ -149,20 +149,20 @@ export async function pullAllBatch(ctx: PullAllBatchContext): Promise<{ error?:
|
|
|
149
149
|
return { error: firstSyncError, changedTables };
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
async function processPullData(
|
|
152
|
+
async function processPullData(tableName: string, serverData: SyncedRecord[], since: Date, ctx: PullContext): Promise<boolean> {
|
|
153
153
|
if (!serverData?.length) return false;
|
|
154
154
|
|
|
155
|
-
ctx.logger.debug(`[dync] pull:process
|
|
155
|
+
ctx.logger.debug(`[dync] pull:process tableName=${tableName} count=${serverData.length}`);
|
|
156
156
|
|
|
157
157
|
let newest = since;
|
|
158
158
|
let hasChanges = false;
|
|
159
159
|
|
|
160
|
-
await ctx.withTransaction('rw', [
|
|
161
|
-
const txTable = tables[
|
|
160
|
+
await ctx.withTransaction('rw', [tableName, DYNC_STATE_TABLE], async (tables) => {
|
|
161
|
+
const txTable = tables[tableName]!;
|
|
162
162
|
const pendingRemovalById = new Set(
|
|
163
163
|
ctx.state
|
|
164
164
|
.getState()
|
|
165
|
-
.pendingChanges.filter((p) => p.
|
|
165
|
+
.pendingChanges.filter((p) => p.tableName === tableName && p.action === SyncAction.Remove)
|
|
166
166
|
.map((p) => p.id),
|
|
167
167
|
);
|
|
168
168
|
|
|
@@ -171,7 +171,7 @@ async function processPullData(stateKey: string, serverData: SyncedRecord[], sin
|
|
|
171
171
|
if (remoteUpdated > newest) newest = remoteUpdated;
|
|
172
172
|
|
|
173
173
|
if (pendingRemovalById.has(remote.id)) {
|
|
174
|
-
ctx.logger.debug(`[dync] pull:skip-pending-remove
|
|
174
|
+
ctx.logger.debug(`[dync] pull:skip-pending-remove tableName=${tableName} id=${remote.id}`);
|
|
175
175
|
continue;
|
|
176
176
|
}
|
|
177
177
|
|
|
@@ -180,7 +180,7 @@ async function processPullData(stateKey: string, serverData: SyncedRecord[], sin
|
|
|
180
180
|
if (remote.deleted) {
|
|
181
181
|
if (localItem) {
|
|
182
182
|
await txTable.raw.delete(localItem._localId);
|
|
183
|
-
ctx.logger.debug(`[dync] pull:remove
|
|
183
|
+
ctx.logger.debug(`[dync] pull:remove tableName=${tableName} id=${remote.id}`);
|
|
184
184
|
hasChanges = true;
|
|
185
185
|
}
|
|
186
186
|
continue;
|
|
@@ -189,12 +189,12 @@ async function processPullData(stateKey: string, serverData: SyncedRecord[], sin
|
|
|
189
189
|
delete remote.deleted;
|
|
190
190
|
|
|
191
191
|
if (localItem) {
|
|
192
|
-
await handleRemoteItemUpdate(txTable,
|
|
192
|
+
await handleRemoteItemUpdate(txTable, tableName, localItem, remote, ctx);
|
|
193
193
|
hasChanges = true;
|
|
194
194
|
} else {
|
|
195
195
|
const newLocalItem = { ...remote, _localId: createLocalId() };
|
|
196
196
|
await txTable.raw.add(newLocalItem);
|
|
197
|
-
ctx.logger.debug(`[dync] pull:add
|
|
197
|
+
ctx.logger.debug(`[dync] pull:add tableName=${tableName} id=${remote.id}`);
|
|
198
198
|
hasChanges = true;
|
|
199
199
|
}
|
|
200
200
|
}
|
|
@@ -203,7 +203,7 @@ async function processPullData(stateKey: string, serverData: SyncedRecord[], sin
|
|
|
203
203
|
...syncState,
|
|
204
204
|
lastPulled: {
|
|
205
205
|
...syncState.lastPulled,
|
|
206
|
-
[
|
|
206
|
+
[tableName]: newest.toISOString(),
|
|
207
207
|
},
|
|
208
208
|
}));
|
|
209
209
|
});
|
|
@@ -23,40 +23,40 @@ export interface PushAllBatchContext extends PushContext {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
async function handleRemoveSuccess(change: PendingChange, ctx: PushContext): Promise<void> {
|
|
26
|
-
const {
|
|
27
|
-
ctx.logger.debug(`[dync] push:remove:success
|
|
28
|
-
await ctx.state.removePendingChange(localId,
|
|
26
|
+
const { tableName, localId, id } = change;
|
|
27
|
+
ctx.logger.debug(`[dync] push:remove:success tableName=${tableName} localId=${localId} id=${id}`);
|
|
28
|
+
await ctx.state.removePendingChange(localId, tableName);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
async function handleUpdateSuccess(change: PendingChange, ctx: PushContext): Promise<void> {
|
|
32
|
-
const {
|
|
33
|
-
ctx.logger.debug(`[dync] push:update:success
|
|
34
|
-
if (ctx.state.samePendingVersion(
|
|
35
|
-
await ctx.state.removePendingChange(localId,
|
|
32
|
+
const { tableName, localId, version, changes } = change;
|
|
33
|
+
ctx.logger.debug(`[dync] push:update:success tableName=${tableName} localId=${localId} id=${change.id}`);
|
|
34
|
+
if (ctx.state.samePendingVersion(tableName, localId, version)) {
|
|
35
|
+
await ctx.state.removePendingChange(localId, tableName);
|
|
36
36
|
} else {
|
|
37
|
-
await ctx.state.setPendingChangeBefore(
|
|
37
|
+
await ctx.state.setPendingChangeBefore(tableName, localId, changes);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
async function handleCreateSuccess(change: PendingChange, serverResult: { id: unknown; updated_at?: string }, ctx: PushContext): Promise<void> {
|
|
42
|
-
const {
|
|
43
|
-
ctx.logger.debug(`[dync] push:create:success
|
|
42
|
+
const { tableName, localId, version, changes, id } = change;
|
|
43
|
+
ctx.logger.debug(`[dync] push:create:success tableName=${tableName} localId=${localId} id=${id ?? serverResult.id}`);
|
|
44
44
|
|
|
45
|
-
await ctx.withTransaction('rw', [
|
|
46
|
-
const txTable = tables[
|
|
45
|
+
await ctx.withTransaction('rw', [tableName, DYNC_STATE_TABLE], async (tables) => {
|
|
46
|
+
const txTable = tables[tableName]!;
|
|
47
47
|
const wasChanged = (await txTable.raw.update(localId, serverResult)) ?? 0;
|
|
48
48
|
|
|
49
|
-
if (wasChanged && ctx.state.samePendingVersion(
|
|
50
|
-
await ctx.state.removePendingChange(localId,
|
|
49
|
+
if (wasChanged && ctx.state.samePendingVersion(tableName, localId, version)) {
|
|
50
|
+
await ctx.state.removePendingChange(localId, tableName);
|
|
51
51
|
} else {
|
|
52
52
|
const nextAction = wasChanged ? SyncAction.Update : SyncAction.Remove;
|
|
53
|
-
await ctx.state.updatePendingChange(
|
|
53
|
+
await ctx.state.updatePendingChange(tableName, localId, nextAction, serverResult.id);
|
|
54
54
|
if (nextAction === SyncAction.Remove) return;
|
|
55
55
|
}
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
const finalItem = { ...changes, ...serverResult, _localId: localId };
|
|
59
|
-
ctx.syncOptions.onAfterRemoteAdd?.(
|
|
59
|
+
ctx.syncOptions.onAfterRemoteAdd?.(tableName, finalItem);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export async function pushAll(ctx: PushAllContext): Promise<Error | undefined> {
|
|
@@ -75,18 +75,18 @@ export async function pushAll(ctx: PushAllContext): Promise<Error | undefined> {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
async function pushOne(change: PendingChange, ctx: PushAllContext): Promise<void> {
|
|
78
|
-
const api = ctx.syncApis[change.
|
|
78
|
+
const api = ctx.syncApis[change.tableName];
|
|
79
79
|
if (!api) return;
|
|
80
80
|
|
|
81
|
-
ctx.logger.debug(`[dync] push:attempt action=${change.action}
|
|
81
|
+
ctx.logger.debug(`[dync] push:attempt action=${change.action} tableName=${change.tableName} localId=${change.localId}`);
|
|
82
82
|
|
|
83
|
-
const { action,
|
|
83
|
+
const { action, tableName, localId, id, changes, after } = change;
|
|
84
84
|
|
|
85
85
|
switch (action) {
|
|
86
86
|
case SyncAction.Remove:
|
|
87
87
|
if (!id) {
|
|
88
|
-
ctx.logger.warn(`[dync] push:remove:no-id
|
|
89
|
-
await ctx.state.removePendingChange(localId,
|
|
88
|
+
ctx.logger.warn(`[dync] push:remove:no-id tableName=${tableName} localId=${localId}`);
|
|
89
|
+
await ctx.state.removePendingChange(localId, tableName);
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
await api.remove(id);
|
|
@@ -95,7 +95,7 @@ async function pushOne(change: PendingChange, ctx: PushAllContext): Promise<void
|
|
|
95
95
|
|
|
96
96
|
case SyncAction.Update: {
|
|
97
97
|
if (ctx.state.hasConflicts(localId)) {
|
|
98
|
-
ctx.logger.warn(`[dync] push:update:skipping-with-conflicts
|
|
98
|
+
ctx.logger.warn(`[dync] push:update:skipping-with-conflicts tableName=${tableName} localId=${localId} id=${id}`);
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -113,9 +113,9 @@ async function pushOne(change: PendingChange, ctx: PushAllContext): Promise<void
|
|
|
113
113
|
if (result) {
|
|
114
114
|
await handleCreateSuccess(change, result, ctx);
|
|
115
115
|
} else {
|
|
116
|
-
ctx.logger.warn(`[dync] push:create:no-result
|
|
117
|
-
if (ctx.state.samePendingVersion(
|
|
118
|
-
await ctx.state.removePendingChange(localId,
|
|
116
|
+
ctx.logger.warn(`[dync] push:create:no-result tableName=${tableName} localId=${localId} id=${id}`);
|
|
117
|
+
if (ctx.state.samePendingVersion(tableName, localId, change.version)) {
|
|
118
|
+
await ctx.state.removePendingChange(localId, tableName);
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
break;
|
|
@@ -124,25 +124,25 @@ async function pushOne(change: PendingChange, ctx: PushAllContext): Promise<void
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
async function handleMissingRemoteRecord(change: PendingChange, ctx: PushContext): Promise<void> {
|
|
127
|
-
const {
|
|
127
|
+
const { tableName, localId } = change;
|
|
128
128
|
const strategy = ctx.syncOptions.missingRemoteRecordDuringUpdateStrategy!;
|
|
129
129
|
|
|
130
130
|
let localItem: any;
|
|
131
131
|
|
|
132
|
-
await ctx.withTransaction('rw', [
|
|
133
|
-
const txTable = tables[
|
|
132
|
+
await ctx.withTransaction('rw', [tableName, DYNC_STATE_TABLE], async (tables) => {
|
|
133
|
+
const txTable = tables[tableName]!;
|
|
134
134
|
localItem = await txTable.get(localId);
|
|
135
135
|
|
|
136
136
|
if (!localItem) {
|
|
137
|
-
ctx.logger.warn(`[dync] push:missing-remote:no-local-item
|
|
138
|
-
await ctx.state.removePendingChange(localId,
|
|
137
|
+
ctx.logger.warn(`[dync] push:missing-remote:no-local-item tableName=${tableName} localId=${localId}`);
|
|
138
|
+
await ctx.state.removePendingChange(localId, tableName);
|
|
139
139
|
return;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
switch (strategy) {
|
|
143
143
|
case 'delete-local-record':
|
|
144
144
|
await txTable.raw.delete(localId);
|
|
145
|
-
ctx.logger.debug(`[dync] push:missing-remote:${strategy}
|
|
145
|
+
ctx.logger.debug(`[dync] push:missing-remote:${strategy} tableName=${tableName} id=${localItem.id}`);
|
|
146
146
|
break;
|
|
147
147
|
|
|
148
148
|
case 'insert-remote-record': {
|
|
@@ -157,26 +157,26 @@ async function handleMissingRemoteRecord(change: PendingChange, ctx: PushContext
|
|
|
157
157
|
|
|
158
158
|
await ctx.state.addPendingChange({
|
|
159
159
|
action: SyncAction.Create,
|
|
160
|
-
|
|
160
|
+
tableName,
|
|
161
161
|
localId: newItem._localId,
|
|
162
162
|
changes: newItem,
|
|
163
163
|
before: null,
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
-
ctx.logger.debug(`[dync] push:missing-remote:${strategy}
|
|
166
|
+
ctx.logger.debug(`[dync] push:missing-remote:${strategy} tableName=${tableName} id=${newItem.id}`);
|
|
167
167
|
break;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
case 'ignore':
|
|
171
|
-
ctx.logger.debug(`[dync] push:missing-remote:${strategy}
|
|
171
|
+
ctx.logger.debug(`[dync] push:missing-remote:${strategy} tableName=${tableName} id=${localItem.id}`);
|
|
172
172
|
break;
|
|
173
173
|
|
|
174
174
|
default:
|
|
175
|
-
ctx.logger.error(`[dync] push:missing-remote:unknown-strategy
|
|
175
|
+
ctx.logger.error(`[dync] push:missing-remote:unknown-strategy tableName=${tableName} id=${localItem.id} strategy=${strategy}`);
|
|
176
176
|
break;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
await ctx.state.removePendingChange(localId,
|
|
179
|
+
await ctx.state.removePendingChange(localId, tableName);
|
|
180
180
|
});
|
|
181
181
|
|
|
182
182
|
ctx.syncOptions.onAfterMissingRemoteRecordDuringUpdate?.(strategy, localItem);
|
|
@@ -191,7 +191,7 @@ export async function pushAllBatch(ctx: PushAllBatchContext): Promise<Error | un
|
|
|
191
191
|
|
|
192
192
|
try {
|
|
193
193
|
const changesSnapshot = [...ctx.state.getState().pendingChanges]
|
|
194
|
-
.filter((change) => ctx.batchSync.syncTables.includes(change.
|
|
194
|
+
.filter((change) => ctx.batchSync.syncTables.includes(change.tableName))
|
|
195
195
|
.sort((a, b) => orderFor(a.action) - orderFor(b.action));
|
|
196
196
|
|
|
197
197
|
if (changesSnapshot.length === 0) {
|
|
@@ -202,7 +202,7 @@ export async function pushAllBatch(ctx: PushAllBatchContext): Promise<Error | un
|
|
|
202
202
|
// Filter out changes with conflicts
|
|
203
203
|
const changesToPush = changesSnapshot.filter((change) => {
|
|
204
204
|
if (change.action === SyncAction.Update && ctx.state.hasConflicts(change.localId)) {
|
|
205
|
-
ctx.logger.warn(`[dync] push:batch:skipping-with-conflicts
|
|
205
|
+
ctx.logger.warn(`[dync] push:batch:skipping-with-conflicts tableName=${change.tableName} localId=${change.localId}`);
|
|
206
206
|
return false;
|
|
207
207
|
}
|
|
208
208
|
return true;
|
|
@@ -215,7 +215,7 @@ export async function pushAllBatch(ctx: PushAllBatchContext): Promise<Error | un
|
|
|
215
215
|
|
|
216
216
|
// Build batch payload
|
|
217
217
|
const payloads: BatchPushPayload[] = changesToPush.map((change) => ({
|
|
218
|
-
table: change.
|
|
218
|
+
table: change.tableName,
|
|
219
219
|
action: change.action === SyncAction.Create ? 'add' : change.action === SyncAction.Update ? 'update' : 'remove',
|
|
220
220
|
localId: change.localId,
|
|
221
221
|
id: change.id,
|
|
@@ -257,14 +257,14 @@ export async function pushAllBatch(ctx: PushAllBatchContext): Promise<Error | un
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
async function processBatchPushResult(change: PendingChange, result: BatchPushResult, ctx: PushAllBatchContext): Promise<void> {
|
|
260
|
-
const { action,
|
|
260
|
+
const { action, tableName, localId } = change;
|
|
261
261
|
|
|
262
262
|
if (!result.success) {
|
|
263
263
|
if (action === SyncAction.Update) {
|
|
264
264
|
// Update failed - might be missing remote record
|
|
265
265
|
await handleMissingRemoteRecord(change, ctx);
|
|
266
266
|
} else {
|
|
267
|
-
ctx.logger.warn(`[dync] push:batch:failed
|
|
267
|
+
ctx.logger.warn(`[dync] push:batch:failed tableName=${tableName} localId=${localId} error=${result.error}`);
|
|
268
268
|
}
|
|
269
269
|
return;
|
|
270
270
|
}
|