@pol-studios/powersync 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-42IJ25Q4.js +45 -0
- package/dist/chunk-42IJ25Q4.js.map +1 -0
- package/dist/{chunk-Q3LFFMRR.js → chunk-H7HZMI4H.js} +2 -2
- package/dist/chunk-MB2RC3NS.js +686 -0
- package/dist/chunk-MB2RC3NS.js.map +1 -0
- package/dist/{chunk-4FJVBR3X.js → chunk-PANEMMTU.js} +8 -3
- package/dist/chunk-PANEMMTU.js.map +1 -0
- package/dist/chunk-VJCL2SWD.js +1 -0
- package/dist/connector/index.d.ts +1 -2
- package/dist/connector/index.js +2 -5
- package/dist/{supabase-connector-D14-kl5v.d.ts → index-D952Qr38.d.ts} +152 -2
- package/dist/index.d.ts +2 -3
- package/dist/index.js +9 -9
- package/dist/index.native.d.ts +1 -2
- package/dist/index.native.js +10 -10
- package/dist/index.web.d.ts +1 -2
- package/dist/index.web.js +9 -9
- package/dist/platform/index.native.js +1 -1
- package/dist/provider/index.d.ts +1 -1
- package/dist/provider/index.js +2 -2
- package/package.json +33 -10
- package/dist/chunk-4FJVBR3X.js.map +0 -1
- package/dist/chunk-7BPTGEVG.js +0 -1
- package/dist/chunk-FLHDT4TS.js +0 -327
- package/dist/chunk-FLHDT4TS.js.map +0 -1
- package/dist/chunk-T225XEML.js +0 -298
- package/dist/chunk-T225XEML.js.map +0 -1
- package/dist/index-nae7nzib.d.ts +0 -147
- /package/dist/{chunk-Q3LFFMRR.js.map → chunk-H7HZMI4H.js.map} +0 -0
- /package/dist/{chunk-7BPTGEVG.js.map → chunk-VJCL2SWD.js.map} +0 -0
package/dist/chunk-T225XEML.js
DELETED
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
SupabaseConnector
|
|
3
|
-
} from "./chunk-FLHDT4TS.js";
|
|
4
|
-
|
|
5
|
-
// src/conflicts/detect.ts
|
|
6
|
-
var DEFAULT_IGNORED_FIELDS = ["updatedAt", "createdAt", "_version", "id"];
|
|
7
|
-
async function detectConflicts(table, recordId, localVersion, serverVersion, pendingChanges, supabase, config) {
|
|
8
|
-
const ignoredFields = /* @__PURE__ */ new Set([
|
|
9
|
-
...DEFAULT_IGNORED_FIELDS,
|
|
10
|
-
...config?.ignoredFields ?? []
|
|
11
|
-
]);
|
|
12
|
-
const filteredPendingChanges = {};
|
|
13
|
-
for (const [field, value] of Object.entries(pendingChanges)) {
|
|
14
|
-
if (!ignoredFields.has(field)) {
|
|
15
|
-
filteredPendingChanges[field] = value;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
if (localVersion === serverVersion) {
|
|
19
|
-
return {
|
|
20
|
-
hasConflict: false,
|
|
21
|
-
conflicts: [],
|
|
22
|
-
nonConflictingChanges: Object.keys(filteredPendingChanges),
|
|
23
|
-
table,
|
|
24
|
-
recordId
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
const { data: auditLogs, error } = await supabase.schema("core").from("AuditLog").select("oldRecord, newRecord, changeBy, changeAt").eq("tableName", table).eq("recordId_text", recordId).order("changeAt", { ascending: false }).limit(20);
|
|
28
|
-
if (error) {
|
|
29
|
-
console.warn("[detectConflicts] Failed to query AuditLog:", error);
|
|
30
|
-
return {
|
|
31
|
-
hasConflict: false,
|
|
32
|
-
conflicts: [],
|
|
33
|
-
nonConflictingChanges: Object.keys(filteredPendingChanges),
|
|
34
|
-
table,
|
|
35
|
-
recordId
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
const serverChanges = /* @__PURE__ */ new Map();
|
|
39
|
-
for (const log of auditLogs ?? []) {
|
|
40
|
-
const oldRec = log.oldRecord;
|
|
41
|
-
const newRec = log.newRecord;
|
|
42
|
-
if (!oldRec || !newRec) continue;
|
|
43
|
-
for (const [field, newValue] of Object.entries(newRec)) {
|
|
44
|
-
if (ignoredFields.has(field)) continue;
|
|
45
|
-
if (oldRec[field] !== newValue && !serverChanges.has(field)) {
|
|
46
|
-
serverChanges.set(field, {
|
|
47
|
-
newValue,
|
|
48
|
-
changedBy: log.changeBy,
|
|
49
|
-
changedAt: new Date(log.changeAt)
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
const conflicts = [];
|
|
55
|
-
const nonConflictingChanges = [];
|
|
56
|
-
for (const [field, localValue] of Object.entries(filteredPendingChanges)) {
|
|
57
|
-
if (serverChanges.has(field)) {
|
|
58
|
-
const serverChange = serverChanges.get(field);
|
|
59
|
-
conflicts.push({
|
|
60
|
-
field,
|
|
61
|
-
localValue,
|
|
62
|
-
serverValue: serverChange.newValue,
|
|
63
|
-
changedBy: serverChange.changedBy,
|
|
64
|
-
changedAt: serverChange.changedAt
|
|
65
|
-
});
|
|
66
|
-
} else {
|
|
67
|
-
nonConflictingChanges.push(field);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
hasConflict: conflicts.length > 0,
|
|
72
|
-
conflicts,
|
|
73
|
-
nonConflictingChanges,
|
|
74
|
-
table,
|
|
75
|
-
recordId
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
async function hasVersionColumn(table, db) {
|
|
79
|
-
try {
|
|
80
|
-
const result = await db.getAll(
|
|
81
|
-
`PRAGMA table_info("${table}")`
|
|
82
|
-
);
|
|
83
|
-
return result.some((col) => col.name === "_version");
|
|
84
|
-
} catch {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
async function fetchServerVersion(table, recordId, schema, supabase) {
|
|
89
|
-
const query = schema === "public" ? supabase.from(table) : supabase.schema(schema).from(table);
|
|
90
|
-
const { data, error } = await query.select("_version").eq("id", recordId).single();
|
|
91
|
-
if (error || !data) {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
return data._version ?? null;
|
|
95
|
-
}
|
|
96
|
-
async function getLocalVersion(table, recordId, db) {
|
|
97
|
-
const result = await db.get(
|
|
98
|
-
`SELECT _version FROM "${table}" WHERE id = ?`,
|
|
99
|
-
[recordId]
|
|
100
|
-
);
|
|
101
|
-
return result?._version ?? null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// src/connector/conflict-aware-connector.ts
|
|
105
|
-
var ConflictAwareConnector = class extends SupabaseConnector {
|
|
106
|
-
conflictHandler;
|
|
107
|
-
conflictConfig;
|
|
108
|
-
supabaseClient;
|
|
109
|
-
schemaRouterFn;
|
|
110
|
-
// Cache for version column existence checks
|
|
111
|
-
versionColumnCache = /* @__PURE__ */ new Map();
|
|
112
|
-
constructor(options) {
|
|
113
|
-
super(options);
|
|
114
|
-
this.conflictHandler = options.conflictHandler;
|
|
115
|
-
this.conflictConfig = options.conflictDetection;
|
|
116
|
-
this.supabaseClient = options.supabaseClient;
|
|
117
|
-
this.schemaRouterFn = options.schemaRouter ?? (() => "public");
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Override uploadData to check for conflicts before uploading.
|
|
121
|
-
*
|
|
122
|
-
* For each CRUD entry in the transaction:
|
|
123
|
-
* 1. Check if table has _version column (cached)
|
|
124
|
-
* 2. If yes, compare local vs server version
|
|
125
|
-
* 3. On version mismatch, query AuditLog for field conflicts
|
|
126
|
-
* 4. If conflicts found, call handler to determine resolution
|
|
127
|
-
* 5. Apply resolution or skip entry based on handler response
|
|
128
|
-
*/
|
|
129
|
-
async uploadData(database) {
|
|
130
|
-
if (this.conflictConfig?.enabled === false) {
|
|
131
|
-
return super.uploadData(database);
|
|
132
|
-
}
|
|
133
|
-
const transaction = await database.getNextCrudTransaction();
|
|
134
|
-
if (!transaction) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
const { crud } = transaction;
|
|
138
|
-
const skipTables = new Set(this.conflictConfig?.skipTables ?? []);
|
|
139
|
-
const entriesToProcess = [];
|
|
140
|
-
const skippedEntries = [];
|
|
141
|
-
for (const entry of crud) {
|
|
142
|
-
if (entry.op === "DELETE") {
|
|
143
|
-
entriesToProcess.push(entry);
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (skipTables.has(entry.table)) {
|
|
147
|
-
entriesToProcess.push(entry);
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
const hasVersion = await this.checkVersionColumn(entry.table, database);
|
|
151
|
-
if (!hasVersion) {
|
|
152
|
-
entriesToProcess.push(entry);
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
const localVersion = await getLocalVersion(entry.table, entry.id, database);
|
|
156
|
-
const schema = this.schemaRouterFn(entry.table);
|
|
157
|
-
const serverVersion = await fetchServerVersion(
|
|
158
|
-
entry.table,
|
|
159
|
-
entry.id,
|
|
160
|
-
schema,
|
|
161
|
-
this.supabaseClient
|
|
162
|
-
);
|
|
163
|
-
if (localVersion === null || serverVersion === null) {
|
|
164
|
-
entriesToProcess.push(entry);
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const conflictResult = await detectConflicts(
|
|
168
|
-
entry.table,
|
|
169
|
-
entry.id,
|
|
170
|
-
localVersion,
|
|
171
|
-
serverVersion,
|
|
172
|
-
entry.opData ?? {},
|
|
173
|
-
this.supabaseClient,
|
|
174
|
-
this.conflictConfig
|
|
175
|
-
);
|
|
176
|
-
if (!conflictResult.hasConflict) {
|
|
177
|
-
entriesToProcess.push(entry);
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
if (this.conflictHandler) {
|
|
181
|
-
const resolution = await this.conflictHandler.onConflict(conflictResult);
|
|
182
|
-
if (resolution === null) {
|
|
183
|
-
skippedEntries.push(entry);
|
|
184
|
-
if (__DEV__) {
|
|
185
|
-
console.log("[ConflictAwareConnector] Conflict queued for UI resolution:", {
|
|
186
|
-
table: entry.table,
|
|
187
|
-
id: entry.id,
|
|
188
|
-
conflicts: conflictResult.conflicts.map((c) => c.field)
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
switch (resolution.action) {
|
|
194
|
-
case "overwrite":
|
|
195
|
-
entriesToProcess.push(entry);
|
|
196
|
-
break;
|
|
197
|
-
case "keep-server":
|
|
198
|
-
skippedEntries.push(entry);
|
|
199
|
-
break;
|
|
200
|
-
case "partial":
|
|
201
|
-
const partialEntry = {
|
|
202
|
-
...entry,
|
|
203
|
-
opData: this.filterFields(entry.opData ?? {}, resolution.fields)
|
|
204
|
-
};
|
|
205
|
-
entriesToProcess.push(partialEntry);
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
} else {
|
|
209
|
-
console.warn("[ConflictAwareConnector] Conflict detected but no handler:", {
|
|
210
|
-
table: entry.table,
|
|
211
|
-
id: entry.id,
|
|
212
|
-
conflicts: conflictResult.conflicts
|
|
213
|
-
});
|
|
214
|
-
entriesToProcess.push(entry);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
if (entriesToProcess.length === 0) {
|
|
218
|
-
if (__DEV__) {
|
|
219
|
-
console.log("[ConflictAwareConnector] All entries skipped due to conflicts");
|
|
220
|
-
}
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
try {
|
|
224
|
-
for (const entry of entriesToProcess) {
|
|
225
|
-
await this.processEntry(entry);
|
|
226
|
-
}
|
|
227
|
-
await transaction.complete();
|
|
228
|
-
} catch (error) {
|
|
229
|
-
console.error("[ConflictAwareConnector] Upload failed:", error);
|
|
230
|
-
throw error;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Check if a table has a _version column (cached).
|
|
235
|
-
*/
|
|
236
|
-
async checkVersionColumn(table, db) {
|
|
237
|
-
if (this.versionColumnCache.has(table)) {
|
|
238
|
-
return this.versionColumnCache.get(table);
|
|
239
|
-
}
|
|
240
|
-
const hasVersion = await hasVersionColumn(table, db);
|
|
241
|
-
this.versionColumnCache.set(table, hasVersion);
|
|
242
|
-
return hasVersion;
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Filter opData to only include specified fields.
|
|
246
|
-
*/
|
|
247
|
-
filterFields(opData, fields) {
|
|
248
|
-
const fieldSet = new Set(fields);
|
|
249
|
-
const filtered = {};
|
|
250
|
-
for (const [key, value] of Object.entries(opData)) {
|
|
251
|
-
if (fieldSet.has(key)) {
|
|
252
|
-
filtered[key] = value;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return filtered;
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Process a single CRUD entry - delegates to parent's private method.
|
|
259
|
-
*
|
|
260
|
-
* Note: This is a workaround since processCrudEntry is private in parent.
|
|
261
|
-
* We replicate the logic here for now.
|
|
262
|
-
*/
|
|
263
|
-
async processEntry(entry) {
|
|
264
|
-
const table = entry.table;
|
|
265
|
-
const id = entry.id;
|
|
266
|
-
const schema = this.schemaRouterFn(table);
|
|
267
|
-
const query = schema === "public" ? this.supabaseClient.from(table) : this.supabaseClient.schema(schema).from(table);
|
|
268
|
-
switch (entry.op) {
|
|
269
|
-
case "PUT": {
|
|
270
|
-
const { error } = await query.upsert(
|
|
271
|
-
{ id, ...entry.opData },
|
|
272
|
-
{ onConflict: "id" }
|
|
273
|
-
).select();
|
|
274
|
-
if (error) throw new Error(`Upsert failed for ${schema}.${table}: ${error.message}`);
|
|
275
|
-
break;
|
|
276
|
-
}
|
|
277
|
-
case "PATCH": {
|
|
278
|
-
const { error } = await query.update(entry.opData).eq("id", id).select();
|
|
279
|
-
if (error) throw new Error(`Update failed for ${schema}.${table}: ${error.message}`);
|
|
280
|
-
break;
|
|
281
|
-
}
|
|
282
|
-
case "DELETE": {
|
|
283
|
-
const { error } = await query.delete().eq("id", id).select();
|
|
284
|
-
if (error) throw new Error(`Delete failed for ${schema}.${table}: ${error.message}`);
|
|
285
|
-
break;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
export {
|
|
292
|
-
detectConflicts,
|
|
293
|
-
hasVersionColumn,
|
|
294
|
-
fetchServerVersion,
|
|
295
|
-
getLocalVersion,
|
|
296
|
-
ConflictAwareConnector
|
|
297
|
-
};
|
|
298
|
-
//# sourceMappingURL=chunk-T225XEML.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/conflicts/detect.ts","../src/connector/conflict-aware-connector.ts"],"sourcesContent":["/**\n * Conflict Detection for @pol-studios/powersync\n *\n * Provides version-based conflict detection using AuditLog attribution.\n * Only queries AuditLog when version mismatch is detected.\n */\n\nimport type { SupabaseClient } from '@supabase/supabase-js';\nimport type { AbstractPowerSyncDatabase } from '../core/types';\nimport type { FieldConflict, ConflictCheckResult, ConflictDetectionConfig } from './types';\n\nconst DEFAULT_IGNORED_FIELDS = ['updatedAt', 'createdAt', '_version', 'id'];\n\n/**\n * Detect conflicts between local pending changes and server state.\n *\n * Uses a two-step approach:\n * 1. Version match (local._version == server._version) → Sync immediately\n * 2. Version mismatch → Query AuditLog for field changes and attribution\n *\n * @param table - The table name\n * @param recordId - The record ID\n * @param localVersion - Version number from local SQLite\n * @param serverVersion - Version number from server (fetched separately)\n * @param pendingChanges - Fields with local changes to sync\n * @param supabase - Supabase client for AuditLog queries\n * @param config - Optional detection configuration\n * @returns Conflict check result with field-level details\n */\nexport async function detectConflicts(\n table: string,\n recordId: string,\n localVersion: number,\n serverVersion: number,\n pendingChanges: Record<string, unknown>,\n supabase: SupabaseClient,\n config?: ConflictDetectionConfig\n): Promise<ConflictCheckResult> {\n const ignoredFields = new Set([\n ...DEFAULT_IGNORED_FIELDS,\n ...(config?.ignoredFields ?? []),\n ]);\n\n // Filter out ignored fields from pending changes\n const filteredPendingChanges: Record<string, unknown> = {};\n for (const [field, value] of Object.entries(pendingChanges)) {\n if (!ignoredFields.has(field)) {\n filteredPendingChanges[field] = value;\n }\n }\n\n // Step 1: Version match = no conflict possible\n if (localVersion === serverVersion) {\n return {\n hasConflict: false,\n conflicts: [],\n nonConflictingChanges: Object.keys(filteredPendingChanges),\n table,\n recordId,\n };\n }\n\n // Step 2: Version mismatch - query AuditLog for changes since our version\n const { data: auditLogs, error } = await supabase\n .schema('core')\n .from('AuditLog')\n .select('oldRecord, newRecord, changeBy, changeAt')\n .eq('tableName', table)\n .eq('recordId_text', recordId)\n .order('changeAt', { ascending: false })\n .limit(20); // Recent changes should be sufficient\n\n if (error) {\n console.warn('[detectConflicts] Failed to query AuditLog:', error);\n // On error, assume no conflict and let sync proceed\n // (Server will reject if there's a real issue)\n return {\n hasConflict: false,\n conflicts: [],\n nonConflictingChanges: Object.keys(filteredPendingChanges),\n table,\n recordId,\n };\n }\n\n // Build map of server-changed fields with attribution\n // Key: field name, Value: most recent change info\n const serverChanges = new Map<string, {\n newValue: unknown;\n changedBy: string | null;\n changedAt: Date;\n }>();\n\n for (const log of auditLogs ?? []) {\n const oldRec = log.oldRecord as Record<string, unknown> | null;\n const newRec = log.newRecord as Record<string, unknown> | null;\n if (!oldRec || !newRec) continue;\n\n for (const [field, newValue] of Object.entries(newRec)) {\n // Skip ignored fields\n if (ignoredFields.has(field)) continue;\n\n // Only track if field actually changed AND we don't already have a more recent change\n if (oldRec[field] !== newValue && !serverChanges.has(field)) {\n serverChanges.set(field, {\n newValue,\n changedBy: log.changeBy as string | null,\n changedAt: new Date(log.changeAt as string),\n });\n }\n }\n }\n\n // Compare pending changes against server changes\n const conflicts: FieldConflict[] = [];\n const nonConflictingChanges: string[] = [];\n\n for (const [field, localValue] of Object.entries(filteredPendingChanges)) {\n if (serverChanges.has(field)) {\n // This field was changed on server - conflict!\n const serverChange = serverChanges.get(field)!;\n conflicts.push({\n field,\n localValue,\n serverValue: serverChange.newValue,\n changedBy: serverChange.changedBy,\n changedAt: serverChange.changedAt,\n });\n } else {\n // Field wasn't changed on server - safe to sync\n nonConflictingChanges.push(field);\n }\n }\n\n return {\n hasConflict: conflicts.length > 0,\n conflicts,\n nonConflictingChanges,\n table,\n recordId,\n };\n}\n\n/**\n * Check if a table has a _version column for conflict detection.\n *\n * @param table - The table name\n * @param db - PowerSync database instance\n * @returns True if the table has version tracking\n */\nexport async function hasVersionColumn(\n table: string,\n db: AbstractPowerSyncDatabase\n): Promise<boolean> {\n try {\n // Query the PowerSync internal schema for column info\n const result = await db.getAll<{ name: string }>(\n `PRAGMA table_info(\"${table}\")`\n );\n return result.some(col => col.name === '_version');\n } catch {\n return false;\n }\n}\n\n/**\n * Fetch the current server version for a record.\n *\n * @param table - The table name\n * @param recordId - The record ID\n * @param schema - The Supabase schema (default: 'public')\n * @param supabase - Supabase client\n * @returns The server version number, or null if record not found\n */\nexport async function fetchServerVersion(\n table: string,\n recordId: string,\n schema: string,\n supabase: SupabaseClient\n): Promise<number | null> {\n const query = schema === 'public'\n ? supabase.from(table)\n : (supabase.schema(schema) as unknown as ReturnType<typeof supabase.schema>).from(table);\n\n const { data, error } = await query\n .select('_version')\n .eq('id', recordId)\n .single();\n\n if (error || !data) {\n return null;\n }\n\n return (data as { _version?: number })._version ?? null;\n}\n\n/**\n * Get the local version for a record from PowerSync SQLite.\n *\n * @param table - The table name\n * @param recordId - The record ID\n * @param db - PowerSync database instance\n * @returns The local version number, or null if not found\n */\nexport async function getLocalVersion(\n table: string,\n recordId: string,\n db: AbstractPowerSyncDatabase\n): Promise<number | null> {\n const result = await db.get<{ _version?: number }>(\n `SELECT _version FROM \"${table}\" WHERE id = ?`,\n [recordId]\n );\n return result?._version ?? null;\n}\n","/**\n * Conflict-Aware Connector for @pol-studios/powersync\n *\n * Extends SupabaseConnector with version-based conflict detection.\n * Tables with a _version column automatically get conflict checking.\n */\n\nimport type { SupabaseClient } from '@supabase/supabase-js';\nimport { SupabaseConnector } from './supabase-connector';\nimport type { SupabaseConnectorOptions, SchemaRouter } from './types';\nimport type { AbstractPowerSyncDatabase, CrudEntry } from '../core/types';\nimport type { ConflictHandler, ConflictDetectionConfig, ConflictCheckResult } from '../conflicts/types';\nimport { detectConflicts, fetchServerVersion, getLocalVersion, hasVersionColumn } from '../conflicts/detect';\n\n/**\n * Options for ConflictAwareConnector.\n */\nexport interface ConflictAwareConnectorOptions extends SupabaseConnectorOptions {\n /** Handler for conflict resolution. If not provided, conflicts are logged. */\n conflictHandler?: ConflictHandler;\n /** Configuration for conflict detection behavior */\n conflictDetection?: ConflictDetectionConfig;\n}\n\n/**\n * A PowerSync connector with built-in conflict detection.\n *\n * This connector extends SupabaseConnector to add version-based conflict\n * detection for tables with a `_version` column. When a conflict is detected,\n * it calls the provided conflict handler to determine how to proceed.\n *\n * @example\n * ```typescript\n * const connector = new ConflictAwareConnector({\n * supabaseClient: supabase,\n * powerSyncUrl: POWERSYNC_URL,\n * conflictHandler: {\n * onConflict: async (result) => {\n * // Queue for UI resolution\n * conflictContext.addConflict(result);\n * return null; // Don't proceed with upload\n * },\n * },\n * });\n * ```\n */\nexport class ConflictAwareConnector extends SupabaseConnector {\n private readonly conflictHandler?: ConflictHandler;\n private readonly conflictConfig?: ConflictDetectionConfig;\n private readonly supabaseClient: SupabaseClient;\n private readonly schemaRouterFn: SchemaRouter;\n\n // Cache for version column existence checks\n private versionColumnCache = new Map<string, boolean>();\n\n constructor(options: ConflictAwareConnectorOptions) {\n super(options);\n this.conflictHandler = options.conflictHandler;\n this.conflictConfig = options.conflictDetection;\n this.supabaseClient = options.supabaseClient;\n this.schemaRouterFn = options.schemaRouter ?? (() => 'public');\n }\n\n /**\n * Override uploadData to check for conflicts before uploading.\n *\n * For each CRUD entry in the transaction:\n * 1. Check if table has _version column (cached)\n * 2. If yes, compare local vs server version\n * 3. On version mismatch, query AuditLog for field conflicts\n * 4. If conflicts found, call handler to determine resolution\n * 5. Apply resolution or skip entry based on handler response\n */\n async uploadData(database: AbstractPowerSyncDatabase): Promise<void> {\n // If conflict detection is disabled, use base implementation\n if (this.conflictConfig?.enabled === false) {\n return super.uploadData(database);\n }\n\n const transaction = await database.getNextCrudTransaction();\n if (!transaction) {\n return;\n }\n\n const { crud } = transaction;\n const skipTables = new Set(this.conflictConfig?.skipTables ?? []);\n const entriesToProcess: CrudEntry[] = [];\n const skippedEntries: CrudEntry[] = [];\n\n for (const entry of crud) {\n // Skip DELETE operations - no conflict checking needed\n if (entry.op === 'DELETE') {\n entriesToProcess.push(entry);\n continue;\n }\n\n // Skip tables in the skip list\n if (skipTables.has(entry.table)) {\n entriesToProcess.push(entry);\n continue;\n }\n\n // Check for version column (cached)\n const hasVersion = await this.checkVersionColumn(entry.table, database);\n if (!hasVersion) {\n entriesToProcess.push(entry);\n continue;\n }\n\n // Get local and server versions\n const localVersion = await getLocalVersion(entry.table, entry.id, database);\n const schema = this.schemaRouterFn(entry.table);\n const serverVersion = await fetchServerVersion(\n entry.table,\n entry.id,\n schema,\n this.supabaseClient\n );\n\n // If we can't get versions, skip conflict check and proceed\n if (localVersion === null || serverVersion === null) {\n entriesToProcess.push(entry);\n continue;\n }\n\n // Detect conflicts\n const conflictResult = await detectConflicts(\n entry.table,\n entry.id,\n localVersion,\n serverVersion,\n entry.opData ?? {},\n this.supabaseClient,\n this.conflictConfig\n );\n\n if (!conflictResult.hasConflict) {\n entriesToProcess.push(entry);\n continue;\n }\n\n // Handle conflict\n if (this.conflictHandler) {\n const resolution = await this.conflictHandler.onConflict(conflictResult);\n\n if (resolution === null) {\n // Queue for UI - skip this entry\n skippedEntries.push(entry);\n if (__DEV__) {\n console.log('[ConflictAwareConnector] Conflict queued for UI resolution:', {\n table: entry.table,\n id: entry.id,\n conflicts: conflictResult.conflicts.map(c => c.field),\n });\n }\n continue;\n }\n\n switch (resolution.action) {\n case 'overwrite':\n // Proceed with upload (overwrite server)\n entriesToProcess.push(entry);\n break;\n\n case 'keep-server':\n // Discard local changes - skip this entry\n skippedEntries.push(entry);\n break;\n\n case 'partial':\n // Only sync specified fields\n const partialEntry: CrudEntry = {\n ...entry,\n opData: this.filterFields(entry.opData ?? {}, resolution.fields),\n };\n entriesToProcess.push(partialEntry);\n break;\n }\n } else {\n // No handler - log conflict and proceed with upload\n console.warn('[ConflictAwareConnector] Conflict detected but no handler:', {\n table: entry.table,\n id: entry.id,\n conflicts: conflictResult.conflicts,\n });\n entriesToProcess.push(entry);\n }\n }\n\n // If all entries were skipped, complete the transaction without uploading\n if (entriesToProcess.length === 0) {\n if (__DEV__) {\n console.log('[ConflictAwareConnector] All entries skipped due to conflicts');\n }\n // Don't complete the transaction - leave entries in queue for later\n return;\n }\n\n // Process remaining entries using parent's logic\n // We need to manually process since we've modified the entries\n try {\n for (const entry of entriesToProcess) {\n await this.processEntry(entry);\n }\n await transaction.complete();\n } catch (error) {\n console.error('[ConflictAwareConnector] Upload failed:', error);\n throw error;\n }\n }\n\n /**\n * Check if a table has a _version column (cached).\n */\n private async checkVersionColumn(\n table: string,\n db: AbstractPowerSyncDatabase\n ): Promise<boolean> {\n if (this.versionColumnCache.has(table)) {\n return this.versionColumnCache.get(table)!;\n }\n\n const hasVersion = await hasVersionColumn(table, db);\n this.versionColumnCache.set(table, hasVersion);\n return hasVersion;\n }\n\n /**\n * Filter opData to only include specified fields.\n */\n private filterFields(\n opData: Record<string, unknown>,\n fields: string[]\n ): Record<string, unknown> {\n const fieldSet = new Set(fields);\n const filtered: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(opData)) {\n if (fieldSet.has(key)) {\n filtered[key] = value;\n }\n }\n return filtered;\n }\n\n /**\n * Process a single CRUD entry - delegates to parent's private method.\n *\n * Note: This is a workaround since processCrudEntry is private in parent.\n * We replicate the logic here for now.\n */\n private async processEntry(entry: CrudEntry): Promise<void> {\n const table = entry.table;\n const id = entry.id;\n const schema = this.schemaRouterFn(table);\n\n const query = schema === 'public'\n ? this.supabaseClient.from(table)\n : (this.supabaseClient.schema(schema) as unknown as ReturnType<typeof this.supabaseClient.schema>).from(table);\n\n switch (entry.op) {\n case 'PUT': {\n const { error } = await query.upsert(\n { id, ...entry.opData },\n { onConflict: 'id' }\n ).select();\n if (error) throw new Error(`Upsert failed for ${schema}.${table}: ${error.message}`);\n break;\n }\n\n case 'PATCH': {\n const { error } = await query\n .update(entry.opData)\n .eq('id', id)\n .select();\n if (error) throw new Error(`Update failed for ${schema}.${table}: ${error.message}`);\n break;\n }\n\n case 'DELETE': {\n const { error } = await query.delete().eq('id', id).select();\n if (error) throw new Error(`Delete failed for ${schema}.${table}: ${error.message}`);\n break;\n }\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index-nae7nzib.d.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { S as SupabaseConnector, a as SupabaseConnectorOptions } from './supabase-connector-D14-kl5v.js';
|
|
2
|
-
import { A as AbstractPowerSyncDatabase } from './types-afHtE1U_.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Conflict Types for @pol-studios/powersync
|
|
6
|
-
*
|
|
7
|
-
* Provides types for version-based conflict detection using AuditLog attribution.
|
|
8
|
-
*/
|
|
9
|
-
/**
|
|
10
|
-
* Represents a field-level conflict where both local and server changed the same field.
|
|
11
|
-
*/
|
|
12
|
-
interface FieldConflict {
|
|
13
|
-
/** The field name that has conflicting changes */
|
|
14
|
-
field: string;
|
|
15
|
-
/** The local (pending) value */
|
|
16
|
-
localValue: unknown;
|
|
17
|
-
/** The current server value */
|
|
18
|
-
serverValue: unknown;
|
|
19
|
-
/** User who made the server change (from AuditLog.changeBy) */
|
|
20
|
-
changedBy: string | null;
|
|
21
|
-
/** When the server change occurred (from AuditLog.changeAt) */
|
|
22
|
-
changedAt: Date;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Result of checking for conflicts between local changes and server state.
|
|
26
|
-
*/
|
|
27
|
-
interface ConflictCheckResult {
|
|
28
|
-
/** Whether any field conflicts were detected */
|
|
29
|
-
hasConflict: boolean;
|
|
30
|
-
/** List of field-level conflicts */
|
|
31
|
-
conflicts: FieldConflict[];
|
|
32
|
-
/** Fields that can safely sync (no server changes) */
|
|
33
|
-
nonConflictingChanges: string[];
|
|
34
|
-
/** The table name */
|
|
35
|
-
table: string;
|
|
36
|
-
/** The record ID */
|
|
37
|
-
recordId: string;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* User's resolution choice for a conflict.
|
|
41
|
-
*/
|
|
42
|
-
type ConflictResolution = {
|
|
43
|
-
action: 'overwrite';
|
|
44
|
-
} | {
|
|
45
|
-
action: 'keep-server';
|
|
46
|
-
} | {
|
|
47
|
-
action: 'partial';
|
|
48
|
-
fields: string[];
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* Handler for conflict events in the connector.
|
|
52
|
-
*/
|
|
53
|
-
interface ConflictHandler {
|
|
54
|
-
/**
|
|
55
|
-
* Called when a conflict is detected during upload.
|
|
56
|
-
*
|
|
57
|
-
* @param result - The conflict check result
|
|
58
|
-
* @returns Resolution to apply, or null to queue for UI resolution
|
|
59
|
-
*/
|
|
60
|
-
onConflict: (result: ConflictCheckResult) => Promise<ConflictResolution | null>;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Configuration for conflict detection behavior.
|
|
64
|
-
*/
|
|
65
|
-
interface ConflictDetectionConfig {
|
|
66
|
-
/** Tables to skip conflict detection (opt-out). Default: [] */
|
|
67
|
-
skipTables?: string[];
|
|
68
|
-
/** Fields to ignore in conflict detection. Default: ['updatedAt', 'createdAt'] */
|
|
69
|
-
ignoredFields?: string[];
|
|
70
|
-
/** Whether to enable conflict detection. Default: true */
|
|
71
|
-
enabled?: boolean;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Conflict-Aware Connector for @pol-studios/powersync
|
|
76
|
-
*
|
|
77
|
-
* Extends SupabaseConnector with version-based conflict detection.
|
|
78
|
-
* Tables with a _version column automatically get conflict checking.
|
|
79
|
-
*/
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Options for ConflictAwareConnector.
|
|
83
|
-
*/
|
|
84
|
-
interface ConflictAwareConnectorOptions extends SupabaseConnectorOptions {
|
|
85
|
-
/** Handler for conflict resolution. If not provided, conflicts are logged. */
|
|
86
|
-
conflictHandler?: ConflictHandler;
|
|
87
|
-
/** Configuration for conflict detection behavior */
|
|
88
|
-
conflictDetection?: ConflictDetectionConfig;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* A PowerSync connector with built-in conflict detection.
|
|
92
|
-
*
|
|
93
|
-
* This connector extends SupabaseConnector to add version-based conflict
|
|
94
|
-
* detection for tables with a `_version` column. When a conflict is detected,
|
|
95
|
-
* it calls the provided conflict handler to determine how to proceed.
|
|
96
|
-
*
|
|
97
|
-
* @example
|
|
98
|
-
* ```typescript
|
|
99
|
-
* const connector = new ConflictAwareConnector({
|
|
100
|
-
* supabaseClient: supabase,
|
|
101
|
-
* powerSyncUrl: POWERSYNC_URL,
|
|
102
|
-
* conflictHandler: {
|
|
103
|
-
* onConflict: async (result) => {
|
|
104
|
-
* // Queue for UI resolution
|
|
105
|
-
* conflictContext.addConflict(result);
|
|
106
|
-
* return null; // Don't proceed with upload
|
|
107
|
-
* },
|
|
108
|
-
* },
|
|
109
|
-
* });
|
|
110
|
-
* ```
|
|
111
|
-
*/
|
|
112
|
-
declare class ConflictAwareConnector extends SupabaseConnector {
|
|
113
|
-
private readonly conflictHandler?;
|
|
114
|
-
private readonly conflictConfig?;
|
|
115
|
-
private readonly supabaseClient;
|
|
116
|
-
private readonly schemaRouterFn;
|
|
117
|
-
private versionColumnCache;
|
|
118
|
-
constructor(options: ConflictAwareConnectorOptions);
|
|
119
|
-
/**
|
|
120
|
-
* Override uploadData to check for conflicts before uploading.
|
|
121
|
-
*
|
|
122
|
-
* For each CRUD entry in the transaction:
|
|
123
|
-
* 1. Check if table has _version column (cached)
|
|
124
|
-
* 2. If yes, compare local vs server version
|
|
125
|
-
* 3. On version mismatch, query AuditLog for field conflicts
|
|
126
|
-
* 4. If conflicts found, call handler to determine resolution
|
|
127
|
-
* 5. Apply resolution or skip entry based on handler response
|
|
128
|
-
*/
|
|
129
|
-
uploadData(database: AbstractPowerSyncDatabase): Promise<void>;
|
|
130
|
-
/**
|
|
131
|
-
* Check if a table has a _version column (cached).
|
|
132
|
-
*/
|
|
133
|
-
private checkVersionColumn;
|
|
134
|
-
/**
|
|
135
|
-
* Filter opData to only include specified fields.
|
|
136
|
-
*/
|
|
137
|
-
private filterFields;
|
|
138
|
-
/**
|
|
139
|
-
* Process a single CRUD entry - delegates to parent's private method.
|
|
140
|
-
*
|
|
141
|
-
* Note: This is a workaround since processCrudEntry is private in parent.
|
|
142
|
-
* We replicate the logic here for now.
|
|
143
|
-
*/
|
|
144
|
-
private processEntry;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export { ConflictAwareConnector as C, type FieldConflict as F, type ConflictAwareConnectorOptions as a, type ConflictCheckResult as b, type ConflictResolution as c, type ConflictHandler as d, type ConflictDetectionConfig as e };
|
|
File without changes
|
|
File without changes
|