@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,212 @@
|
|
|
1
|
+
import { createLocalId } from '../helpers';
|
|
2
|
+
import type { Logger } from '../logger';
|
|
3
|
+
import type { ApiFunctions, BatchSync, ConflictResolutionStrategy, FieldConflict, SyncedRecord } from '../types';
|
|
4
|
+
import { SyncAction } from '../types';
|
|
5
|
+
import type { StorageTable } from '../storage/types';
|
|
6
|
+
import { DYNC_STATE_TABLE, type StateHelpers } from './StateManager';
|
|
7
|
+
import type { WithTransaction } from './types';
|
|
8
|
+
|
|
9
|
+
export interface PullContext {
|
|
10
|
+
logger: Logger;
|
|
11
|
+
state: StateHelpers;
|
|
12
|
+
table: <T>(name: string) => StorageTable<T>;
|
|
13
|
+
withTransaction: WithTransaction;
|
|
14
|
+
conflictResolutionStrategy: ConflictResolutionStrategy;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PullAllContext extends PullContext {
|
|
18
|
+
syncApis: Record<string, ApiFunctions>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PullAllBatchContext extends PullContext {
|
|
22
|
+
batchSync: BatchSync;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function pullAll(ctx: PullAllContext): Promise<{ error?: Error; changedTables: string[] }> {
|
|
26
|
+
let firstSyncError: Error | undefined;
|
|
27
|
+
const changedTables: string[] = [];
|
|
28
|
+
for (const [stateKey, api] of Object.entries(ctx.syncApis)) {
|
|
29
|
+
try {
|
|
30
|
+
const lastPulled = ctx.state.getState().lastPulled[stateKey];
|
|
31
|
+
const since = lastPulled ? new Date(lastPulled) : new Date(0);
|
|
32
|
+
|
|
33
|
+
ctx.logger.debug(`[dync] pull:start stateKey=${stateKey} since=${since.toISOString()}`);
|
|
34
|
+
|
|
35
|
+
const serverData = (await api.list(since)) as SyncedRecord[];
|
|
36
|
+
const changed = await processPullData(stateKey, serverData, since, ctx);
|
|
37
|
+
if (changed) changedTables.push(stateKey);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
firstSyncError = firstSyncError ?? (err as Error);
|
|
40
|
+
ctx.logger.error(`[dync] pull:error stateKey=${stateKey}`, err);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { error: firstSyncError, changedTables };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function handleRemoteItemUpdate(table: StorageTable<any>, stateKey: string, localItem: any, remote: any, ctx: PullContext): Promise<void> {
|
|
47
|
+
const pendingChange = ctx.state.getState().pendingChanges.find((p) => p.stateKey === stateKey && p.localId === localItem._localId);
|
|
48
|
+
const conflictStrategy = ctx.conflictResolutionStrategy;
|
|
49
|
+
|
|
50
|
+
if (pendingChange) {
|
|
51
|
+
ctx.logger.debug(`[dync] pull:conflict-strategy:${conflictStrategy} stateKey=${stateKey} id=${remote.id}`);
|
|
52
|
+
|
|
53
|
+
switch (conflictStrategy) {
|
|
54
|
+
case 'local-wins':
|
|
55
|
+
break;
|
|
56
|
+
|
|
57
|
+
case 'remote-wins': {
|
|
58
|
+
const merged = { ...remote, _localId: localItem._localId };
|
|
59
|
+
await table.raw.update(localItem._localId, merged);
|
|
60
|
+
await ctx.state.removePendingChange(localItem._localId, stateKey);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case 'try-shallow-merge': {
|
|
65
|
+
const changes = pendingChange.changes || {};
|
|
66
|
+
const before = pendingChange.before || {};
|
|
67
|
+
const fields: FieldConflict[] = Object.entries(changes)
|
|
68
|
+
.filter(([k, localValue]) => k in before && k in remote && before[k] !== remote[k] && localValue !== remote[k])
|
|
69
|
+
.map(([key, localValue]) => ({ key, localValue, remoteValue: remote[key] }));
|
|
70
|
+
|
|
71
|
+
if (fields.length > 0) {
|
|
72
|
+
ctx.logger.warn(`[dync] pull:${conflictStrategy}:conflicts-found`, JSON.stringify(fields, null, 4));
|
|
73
|
+
// Store or update conflict with CURRENT field values (prevents stale conflicts)
|
|
74
|
+
await ctx.state.setState((syncState) => ({
|
|
75
|
+
...syncState,
|
|
76
|
+
conflicts: {
|
|
77
|
+
...(syncState.conflicts || {}),
|
|
78
|
+
[localItem._localId]: { stateKey, fields },
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
} else {
|
|
82
|
+
const localChangedKeys = Object.keys(changes);
|
|
83
|
+
const preservedLocal: any = { _localId: localItem._localId };
|
|
84
|
+
for (const k of localChangedKeys) {
|
|
85
|
+
if (k in localItem) preservedLocal[k] = localItem[k];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const merged = { ...remote, ...preservedLocal };
|
|
89
|
+
await table.raw.update(localItem._localId, merged);
|
|
90
|
+
|
|
91
|
+
// Clear conflict for the current pending change - it no longer conflicts
|
|
92
|
+
await ctx.state.setState((syncState) => {
|
|
93
|
+
const ss = { ...syncState };
|
|
94
|
+
delete ss.conflicts?.[localItem._localId];
|
|
95
|
+
return ss;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
const merged = { ...localItem, ...remote };
|
|
103
|
+
await table.raw.update(localItem._localId, merged);
|
|
104
|
+
ctx.logger.debug(`[dync] pull:merge-remote stateKey=${stateKey} id=${remote.id}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Batch Mode Pull Operations
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
export async function pullAllBatch(ctx: PullAllBatchContext): Promise<{ error?: Error; changedTables: string[] }> {
|
|
113
|
+
let firstSyncError: Error | undefined;
|
|
114
|
+
const changedTables: string[] = [];
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
// Build since map for all synced tables
|
|
118
|
+
const sinceMap: Record<string, Date> = {};
|
|
119
|
+
for (const tableName of ctx.batchSync.syncTables) {
|
|
120
|
+
const lastPulled = ctx.state.getState().lastPulled[tableName];
|
|
121
|
+
sinceMap[tableName] = lastPulled ? new Date(lastPulled) : new Date(0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ctx.logger.debug(`[dync] pull:batch:start tables=${[...ctx.batchSync.syncTables].join(',')}`, sinceMap);
|
|
125
|
+
|
|
126
|
+
// Single batch pull request
|
|
127
|
+
const serverDataByTable = await ctx.batchSync.pull(sinceMap);
|
|
128
|
+
|
|
129
|
+
// Process each table's data
|
|
130
|
+
for (const [stateKey, serverData] of Object.entries(serverDataByTable)) {
|
|
131
|
+
if (!ctx.batchSync.syncTables.includes(stateKey)) {
|
|
132
|
+
ctx.logger.warn(`[dync] pull:batch:unknown-table stateKey=${stateKey}`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const changed = await processPullData(stateKey, serverData as SyncedRecord[], sinceMap[stateKey]!, ctx);
|
|
138
|
+
if (changed) changedTables.push(stateKey);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
firstSyncError = firstSyncError ?? (err as Error);
|
|
141
|
+
ctx.logger.error(`[dync] pull:batch:error stateKey=${stateKey}`, err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
firstSyncError = err as Error;
|
|
146
|
+
ctx.logger.error(`[dync] pull:batch:error`, err);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { error: firstSyncError, changedTables };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function processPullData(stateKey: string, serverData: SyncedRecord[], since: Date, ctx: PullContext): Promise<boolean> {
|
|
153
|
+
if (!serverData?.length) return false;
|
|
154
|
+
|
|
155
|
+
ctx.logger.debug(`[dync] pull:process stateKey=${stateKey} count=${serverData.length}`);
|
|
156
|
+
|
|
157
|
+
let newest = since;
|
|
158
|
+
let hasChanges = false;
|
|
159
|
+
|
|
160
|
+
await ctx.withTransaction('rw', [stateKey, DYNC_STATE_TABLE], async (tables) => {
|
|
161
|
+
const txTable = tables[stateKey]!;
|
|
162
|
+
const pendingRemovalById = new Set(
|
|
163
|
+
ctx.state
|
|
164
|
+
.getState()
|
|
165
|
+
.pendingChanges.filter((p) => p.stateKey === stateKey && p.action === SyncAction.Remove)
|
|
166
|
+
.map((p) => p.id),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
for (const remote of serverData) {
|
|
170
|
+
const remoteUpdated = new Date(remote.updated_at);
|
|
171
|
+
if (remoteUpdated > newest) newest = remoteUpdated;
|
|
172
|
+
|
|
173
|
+
if (pendingRemovalById.has(remote.id)) {
|
|
174
|
+
ctx.logger.debug(`[dync] pull:skip-pending-remove stateKey=${stateKey} id=${remote.id}`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const localItem = await txTable.where('id').equals(remote.id).first();
|
|
179
|
+
|
|
180
|
+
if (remote.deleted) {
|
|
181
|
+
if (localItem) {
|
|
182
|
+
await txTable.raw.delete(localItem._localId);
|
|
183
|
+
ctx.logger.debug(`[dync] pull:remove stateKey=${stateKey} id=${remote.id}`);
|
|
184
|
+
hasChanges = true;
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
delete remote.deleted;
|
|
190
|
+
|
|
191
|
+
if (localItem) {
|
|
192
|
+
await handleRemoteItemUpdate(txTable, stateKey, localItem, remote, ctx);
|
|
193
|
+
hasChanges = true;
|
|
194
|
+
} else {
|
|
195
|
+
const newLocalItem = { ...remote, _localId: createLocalId() };
|
|
196
|
+
await txTable.raw.add(newLocalItem);
|
|
197
|
+
ctx.logger.debug(`[dync] pull:add stateKey=${stateKey} id=${remote.id}`);
|
|
198
|
+
hasChanges = true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await ctx.state.setState((syncState) => ({
|
|
203
|
+
...syncState,
|
|
204
|
+
lastPulled: {
|
|
205
|
+
...syncState.lastPulled,
|
|
206
|
+
[stateKey]: newest.toISOString(),
|
|
207
|
+
},
|
|
208
|
+
}));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return hasChanges;
|
|
212
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { createLocalId, orderFor } from '../helpers';
|
|
2
|
+
import type { Logger } from '../logger';
|
|
3
|
+
import type { ApiFunctions, BatchPushPayload, BatchPushResult, BatchSync, PendingChange, SyncOptions } from '../types';
|
|
4
|
+
import { SyncAction } from '../types';
|
|
5
|
+
import type { StorageTable } from '../storage/types';
|
|
6
|
+
import { DYNC_STATE_TABLE, type StateHelpers } from './StateManager';
|
|
7
|
+
import type { WithTransaction } from './types';
|
|
8
|
+
|
|
9
|
+
export interface PushContext {
|
|
10
|
+
logger: Logger;
|
|
11
|
+
state: StateHelpers;
|
|
12
|
+
table: <T>(name: string) => StorageTable<T>;
|
|
13
|
+
withTransaction: WithTransaction;
|
|
14
|
+
syncOptions: SyncOptions;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PushAllContext extends PushContext {
|
|
18
|
+
syncApis: Record<string, ApiFunctions>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PushAllBatchContext extends PushContext {
|
|
22
|
+
batchSync: BatchSync;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function handleRemoveSuccess(change: PendingChange, ctx: PushContext): Promise<void> {
|
|
26
|
+
const { stateKey, localId, id } = change;
|
|
27
|
+
ctx.logger.debug(`[dync] push:remove:success stateKey=${stateKey} localId=${localId} id=${id}`);
|
|
28
|
+
await ctx.state.removePendingChange(localId, stateKey);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function handleUpdateSuccess(change: PendingChange, ctx: PushContext): Promise<void> {
|
|
32
|
+
const { stateKey, localId, version, changes } = change;
|
|
33
|
+
ctx.logger.debug(`[dync] push:update:success stateKey=${stateKey} localId=${localId} id=${change.id}`);
|
|
34
|
+
if (ctx.state.samePendingVersion(stateKey, localId, version)) {
|
|
35
|
+
await ctx.state.removePendingChange(localId, stateKey);
|
|
36
|
+
} else {
|
|
37
|
+
await ctx.state.setPendingChangeBefore(stateKey, localId, changes);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function handleCreateSuccess(change: PendingChange, serverResult: { id: unknown; updated_at?: string }, ctx: PushContext): Promise<void> {
|
|
42
|
+
const { stateKey, localId, version, changes, id } = change;
|
|
43
|
+
ctx.logger.debug(`[dync] push:create:success stateKey=${stateKey} localId=${localId} id=${id ?? serverResult.id}`);
|
|
44
|
+
|
|
45
|
+
await ctx.withTransaction('rw', [stateKey, DYNC_STATE_TABLE], async (tables) => {
|
|
46
|
+
const txTable = tables[stateKey]!;
|
|
47
|
+
const wasChanged = (await txTable.raw.update(localId, serverResult)) ?? 0;
|
|
48
|
+
|
|
49
|
+
if (wasChanged && ctx.state.samePendingVersion(stateKey, localId, version)) {
|
|
50
|
+
await ctx.state.removePendingChange(localId, stateKey);
|
|
51
|
+
} else {
|
|
52
|
+
const nextAction = wasChanged ? SyncAction.Update : SyncAction.Remove;
|
|
53
|
+
await ctx.state.updatePendingChange(stateKey, localId, nextAction, serverResult.id);
|
|
54
|
+
if (nextAction === SyncAction.Remove) return;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const finalItem = { ...changes, ...serverResult, _localId: localId };
|
|
59
|
+
ctx.syncOptions.onAfterRemoteAdd?.(stateKey, finalItem);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function pushAll(ctx: PushAllContext): Promise<Error | undefined> {
|
|
63
|
+
let firstSyncError: Error | undefined;
|
|
64
|
+
const changesSnapshot = [...ctx.state.getState().pendingChanges].sort((a, b) => orderFor(a.action) - orderFor(b.action));
|
|
65
|
+
|
|
66
|
+
for (const change of changesSnapshot) {
|
|
67
|
+
try {
|
|
68
|
+
await pushOne(change, ctx);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
firstSyncError = firstSyncError ?? (err as Error);
|
|
71
|
+
ctx.logger.error(`[dync] push:error change=${JSON.stringify(change)}`, err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return firstSyncError;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function pushOne(change: PendingChange, ctx: PushAllContext): Promise<void> {
|
|
78
|
+
const api = ctx.syncApis[change.stateKey];
|
|
79
|
+
if (!api) return;
|
|
80
|
+
|
|
81
|
+
ctx.logger.debug(`[dync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`);
|
|
82
|
+
|
|
83
|
+
const { action, stateKey, localId, id, changes, after } = change;
|
|
84
|
+
|
|
85
|
+
switch (action) {
|
|
86
|
+
case SyncAction.Remove:
|
|
87
|
+
if (!id) {
|
|
88
|
+
ctx.logger.warn(`[dync] push:remove:no-id stateKey=${stateKey} localId=${localId}`);
|
|
89
|
+
await ctx.state.removePendingChange(localId, stateKey);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await api.remove(id);
|
|
93
|
+
await handleRemoveSuccess(change, ctx);
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
case SyncAction.Update: {
|
|
97
|
+
if (ctx.state.hasConflicts(localId)) {
|
|
98
|
+
ctx.logger.warn(`[dync] push:update:skipping-with-conflicts stateKey=${stateKey} localId=${localId} id=${id}`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const exists = await api.update(id, changes, after);
|
|
103
|
+
if (exists) {
|
|
104
|
+
await handleUpdateSuccess(change, ctx);
|
|
105
|
+
} else {
|
|
106
|
+
await handleMissingRemoteRecord(change, ctx);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
case SyncAction.Create: {
|
|
112
|
+
const result = await api.add(changes);
|
|
113
|
+
if (result) {
|
|
114
|
+
await handleCreateSuccess(change, result, ctx);
|
|
115
|
+
} else {
|
|
116
|
+
ctx.logger.warn(`[dync] push:create:no-result stateKey=${stateKey} localId=${localId} id=${id}`);
|
|
117
|
+
if (ctx.state.samePendingVersion(stateKey, localId, change.version)) {
|
|
118
|
+
await ctx.state.removePendingChange(localId, stateKey);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function handleMissingRemoteRecord(change: PendingChange, ctx: PushContext): Promise<void> {
|
|
127
|
+
const { stateKey, localId } = change;
|
|
128
|
+
const strategy = ctx.syncOptions.missingRemoteRecordDuringUpdateStrategy!;
|
|
129
|
+
|
|
130
|
+
let localItem: any;
|
|
131
|
+
|
|
132
|
+
await ctx.withTransaction('rw', [stateKey, DYNC_STATE_TABLE], async (tables) => {
|
|
133
|
+
const txTable = tables[stateKey]!;
|
|
134
|
+
localItem = await txTable.get(localId);
|
|
135
|
+
|
|
136
|
+
if (!localItem) {
|
|
137
|
+
ctx.logger.warn(`[dync] push:missing-remote:no-local-item stateKey=${stateKey} localId=${localId}`);
|
|
138
|
+
await ctx.state.removePendingChange(localId, stateKey);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
switch (strategy) {
|
|
143
|
+
case 'delete-local-record':
|
|
144
|
+
await txTable.raw.delete(localId);
|
|
145
|
+
ctx.logger.debug(`[dync] push:missing-remote:${strategy} stateKey=${stateKey} id=${localItem.id}`);
|
|
146
|
+
break;
|
|
147
|
+
|
|
148
|
+
case 'insert-remote-record': {
|
|
149
|
+
const newItem = {
|
|
150
|
+
...localItem,
|
|
151
|
+
_localId: createLocalId(),
|
|
152
|
+
updated_at: new Date().toISOString(),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
await txTable.raw.add(newItem);
|
|
156
|
+
await txTable.raw.delete(localId);
|
|
157
|
+
|
|
158
|
+
await ctx.state.addPendingChange({
|
|
159
|
+
action: SyncAction.Create,
|
|
160
|
+
stateKey,
|
|
161
|
+
localId: newItem._localId,
|
|
162
|
+
changes: newItem,
|
|
163
|
+
before: null,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
ctx.logger.debug(`[dync] push:missing-remote:${strategy} stateKey=${stateKey} id=${newItem.id}`);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case 'ignore':
|
|
171
|
+
ctx.logger.debug(`[dync] push:missing-remote:${strategy} stateKey=${stateKey} id=${localItem.id}`);
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
default:
|
|
175
|
+
ctx.logger.error(`[dync] push:missing-remote:unknown-strategy stateKey=${stateKey} id=${localItem.id} strategy=${strategy}`);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await ctx.state.removePendingChange(localId, stateKey);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
ctx.syncOptions.onAfterMissingRemoteRecordDuringUpdate?.(strategy, localItem);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Batch Mode Push Operations
|
|
187
|
+
// ============================================================================
|
|
188
|
+
|
|
189
|
+
export async function pushAllBatch(ctx: PushAllBatchContext): Promise<Error | undefined> {
|
|
190
|
+
let firstSyncError: Error | undefined;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const changesSnapshot = [...ctx.state.getState().pendingChanges]
|
|
194
|
+
.filter((change) => ctx.batchSync.syncTables.includes(change.stateKey))
|
|
195
|
+
.sort((a, b) => orderFor(a.action) - orderFor(b.action));
|
|
196
|
+
|
|
197
|
+
if (changesSnapshot.length === 0) {
|
|
198
|
+
ctx.logger.debug('[dync] push:batch:no-changes');
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Filter out changes with conflicts
|
|
203
|
+
const changesToPush = changesSnapshot.filter((change) => {
|
|
204
|
+
if (change.action === SyncAction.Update && ctx.state.hasConflicts(change.localId)) {
|
|
205
|
+
ctx.logger.warn(`[dync] push:batch:skipping-with-conflicts stateKey=${change.stateKey} localId=${change.localId}`);
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (changesToPush.length === 0) {
|
|
212
|
+
ctx.logger.debug('[dync] push:batch:all-skipped');
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Build batch payload
|
|
217
|
+
const payloads: BatchPushPayload[] = changesToPush.map((change) => ({
|
|
218
|
+
table: change.stateKey,
|
|
219
|
+
action: change.action === SyncAction.Create ? 'add' : change.action === SyncAction.Update ? 'update' : 'remove',
|
|
220
|
+
localId: change.localId,
|
|
221
|
+
id: change.id,
|
|
222
|
+
data: change.action === SyncAction.Remove ? undefined : change.changes,
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
ctx.logger.debug(`[dync] push:batch:start count=${payloads.length}`);
|
|
226
|
+
|
|
227
|
+
// Single batch push request
|
|
228
|
+
const results = await ctx.batchSync.push(payloads);
|
|
229
|
+
|
|
230
|
+
// Create a map of results by localId for easy lookup
|
|
231
|
+
const resultMap = new Map<string, BatchPushResult>();
|
|
232
|
+
for (const result of results) {
|
|
233
|
+
resultMap.set(result.localId, result);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Process each result
|
|
237
|
+
for (const change of changesToPush) {
|
|
238
|
+
const result = resultMap.get(change.localId);
|
|
239
|
+
if (!result) {
|
|
240
|
+
ctx.logger.warn(`[dync] push:batch:missing-result localId=${change.localId}`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
await processBatchPushResult(change, result, ctx);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
firstSyncError = firstSyncError ?? (err as Error);
|
|
248
|
+
ctx.logger.error(`[dync] push:batch:error localId=${change.localId}`, err);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch (err) {
|
|
252
|
+
firstSyncError = err as Error;
|
|
253
|
+
ctx.logger.error('[dync] push:batch:error', err);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return firstSyncError;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function processBatchPushResult(change: PendingChange, result: BatchPushResult, ctx: PushAllBatchContext): Promise<void> {
|
|
260
|
+
const { action, stateKey, localId } = change;
|
|
261
|
+
|
|
262
|
+
if (!result.success) {
|
|
263
|
+
if (action === SyncAction.Update) {
|
|
264
|
+
// Update failed - might be missing remote record
|
|
265
|
+
await handleMissingRemoteRecord(change, ctx);
|
|
266
|
+
} else {
|
|
267
|
+
ctx.logger.warn(`[dync] push:batch:failed stateKey=${stateKey} localId=${localId} error=${result.error}`);
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
switch (action) {
|
|
273
|
+
case SyncAction.Remove:
|
|
274
|
+
handleRemoveSuccess(change, ctx);
|
|
275
|
+
break;
|
|
276
|
+
|
|
277
|
+
case SyncAction.Update:
|
|
278
|
+
handleUpdateSuccess(change, ctx);
|
|
279
|
+
break;
|
|
280
|
+
|
|
281
|
+
case SyncAction.Create: {
|
|
282
|
+
const serverResult: { id: unknown; updated_at?: string } = { id: result.id };
|
|
283
|
+
if (result.updated_at) {
|
|
284
|
+
serverResult.updated_at = result.updated_at;
|
|
285
|
+
}
|
|
286
|
+
await handleCreateSuccess(change, serverResult, ctx);
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|