@framers/sql-storage-adapter 0.7.1 → 0.7.3
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/README.md +43 -0
- package/dist/adapters/electron/preload.cjs +289 -0
- package/dist/adapters/electron/renderer.d.ts +9 -0
- package/dist/adapters/electron/renderer.d.ts.map +1 -0
- package/dist/adapters/electron/renderer.js +8 -0
- package/dist/adapters/electron/renderer.js.map +1 -0
- package/dist/cjs/adapters/__tests__/indexedDbAdapter.spec.d.ts +2 -0
- package/dist/cjs/adapters/__tests__/indexedDbAdapter.spec.js +133 -0
- package/dist/cjs/adapters/baseStorageAdapter.d.ts +368 -0
- package/dist/cjs/adapters/baseStorageAdapter.js +744 -0
- package/dist/cjs/adapters/betterSqliteAdapter.d.ts +124 -0
- package/dist/cjs/adapters/betterSqliteAdapter.js +334 -0
- package/dist/cjs/adapters/capacitorSqliteAdapter.d.ts +83 -0
- package/dist/cjs/adapters/capacitorSqliteAdapter.js +250 -0
- package/dist/cjs/adapters/electron/electronMainAdapter.d.ts +251 -0
- package/dist/cjs/adapters/electron/electronMainAdapter.js +392 -0
- package/dist/cjs/adapters/electron/electronRendererAdapter.d.ts +177 -0
- package/dist/cjs/adapters/electron/electronRendererAdapter.js +283 -0
- package/dist/cjs/adapters/electron/index.d.ts +74 -0
- package/dist/cjs/adapters/electron/index.js +86 -0
- package/dist/cjs/adapters/electron/ipc/channels.d.ts +196 -0
- package/dist/cjs/adapters/electron/ipc/channels.js +127 -0
- package/dist/cjs/adapters/electron/ipc/index.d.ts +11 -0
- package/dist/cjs/adapters/electron/ipc/index.js +38 -0
- package/dist/cjs/adapters/electron/ipc/protocol.d.ts +78 -0
- package/dist/cjs/adapters/electron/ipc/protocol.js +341 -0
- package/dist/cjs/adapters/electron/ipc/types.d.ts +248 -0
- package/dist/cjs/adapters/electron/ipc/types.js +15 -0
- package/dist/cjs/adapters/electron/migration/autoMigrator.d.ts +184 -0
- package/dist/cjs/adapters/electron/migration/autoMigrator.js +441 -0
- package/dist/cjs/adapters/electron/migration/index.d.ts +9 -0
- package/dist/cjs/adapters/electron/migration/index.js +29 -0
- package/dist/cjs/adapters/electron/preload.d.ts +126 -0
- package/dist/cjs/adapters/electron/preload.js +233 -0
- package/dist/cjs/adapters/electron/recovery/corruptionDetector.d.ts +214 -0
- package/dist/cjs/adapters/electron/recovery/corruptionDetector.js +359 -0
- package/dist/cjs/adapters/electron/recovery/index.d.ts +11 -0
- package/dist/cjs/adapters/electron/recovery/index.js +34 -0
- package/dist/cjs/adapters/electron/recovery/walCheckpoint.d.ts +186 -0
- package/dist/cjs/adapters/electron/recovery/walCheckpoint.js +263 -0
- package/dist/cjs/adapters/electron/renderer.d.ts +9 -0
- package/dist/cjs/adapters/electron/renderer.js +33 -0
- package/dist/cjs/adapters/electron/window/index.d.ts +9 -0
- package/dist/cjs/adapters/electron/window/index.js +29 -0
- package/dist/cjs/adapters/electron/window/windowManager.d.ts +190 -0
- package/dist/cjs/adapters/electron/window/windowManager.js +321 -0
- package/dist/cjs/adapters/indexedDbAdapter.d.ts +490 -0
- package/dist/cjs/adapters/indexedDbAdapter.js +993 -0
- package/dist/cjs/adapters/postgresAdapter.d.ts +424 -0
- package/dist/cjs/adapters/postgresAdapter.js +889 -0
- package/dist/cjs/adapters/sqlJsAdapter.d.ts +64 -0
- package/dist/cjs/adapters/sqlJsAdapter.js +268 -0
- package/dist/cjs/adapters/supabase.d.ts +58 -0
- package/dist/cjs/adapters/supabase.js +401 -0
- package/dist/cjs/codecs/BrowserBlobCodec.d.ts +14 -0
- package/dist/cjs/codecs/BrowserBlobCodec.js +57 -0
- package/dist/cjs/codecs/NodeBlobCodec.d.ts +14 -0
- package/dist/cjs/codecs/NodeBlobCodec.js +59 -0
- package/dist/cjs/core/contracts/blobCodec.d.ts +15 -0
- package/dist/cjs/core/contracts/blobCodec.js +15 -0
- package/dist/cjs/core/contracts/context.d.ts +221 -0
- package/dist/cjs/core/contracts/context.js +15 -0
- package/dist/cjs/core/contracts/dialect.d.ts +154 -0
- package/dist/cjs/core/contracts/dialect.js +15 -0
- package/dist/cjs/core/contracts/events.d.ts +225 -0
- package/dist/cjs/core/contracts/events.js +15 -0
- package/dist/cjs/core/contracts/exporter.d.ts +13 -0
- package/dist/cjs/core/contracts/exporter.js +15 -0
- package/dist/cjs/core/contracts/extensions.d.ts +73 -0
- package/dist/cjs/core/contracts/extensions.js +15 -0
- package/dist/cjs/core/contracts/features.d.ts +25 -0
- package/dist/cjs/core/contracts/features.js +61 -0
- package/dist/cjs/core/contracts/fts.d.ts +66 -0
- package/dist/cjs/core/contracts/fts.js +15 -0
- package/dist/cjs/core/contracts/hooks.d.ts +481 -0
- package/dist/cjs/core/contracts/hooks.js +140 -0
- package/dist/cjs/core/contracts/index.d.ts +260 -0
- package/dist/cjs/core/contracts/index.js +41 -0
- package/dist/cjs/core/contracts/limitations.d.ts +46 -0
- package/dist/cjs/core/contracts/limitations.js +218 -0
- package/dist/cjs/core/contracts/performance.d.ts +308 -0
- package/dist/cjs/core/contracts/performance.js +144 -0
- package/dist/cjs/core/database.d.ts +220 -0
- package/dist/cjs/core/database.js +136 -0
- package/dist/cjs/core/portable.d.ts +23 -0
- package/dist/cjs/core/portable.js +66 -0
- package/dist/cjs/core/resolver.d.ts +77 -0
- package/dist/cjs/core/resolver.js +235 -0
- package/dist/cjs/dialects/PostgresDialect.d.ts +35 -0
- package/dist/cjs/dialects/PostgresDialect.js +143 -0
- package/dist/cjs/dialects/SqliteDialect.d.ts +35 -0
- package/dist/cjs/dialects/SqliteDialect.js +142 -0
- package/dist/cjs/dialects/shared.d.ts +17 -0
- package/dist/cjs/dialects/shared.js +90 -0
- package/dist/cjs/exporters/PostgresExporter.d.ts +14 -0
- package/dist/cjs/exporters/PostgresExporter.js +61 -0
- package/dist/cjs/exporters/SqliteFileExporter.d.ts +16 -0
- package/dist/cjs/exporters/SqliteFileExporter.js +43 -0
- package/dist/cjs/features/backup/cloudBackup.d.ts +219 -0
- package/dist/cjs/features/backup/cloudBackup.js +209 -0
- package/dist/cjs/features/kv/idb.d.ts +79 -0
- package/dist/cjs/features/kv/idb.js +32 -0
- package/dist/cjs/features/kv/index.d.ts +14 -0
- package/dist/cjs/features/kv/index.js +40 -0
- package/dist/cjs/features/kv/indexedDbKeyValueStore.d.ts +77 -0
- package/dist/cjs/features/kv/indexedDbKeyValueStore.js +116 -0
- package/dist/cjs/features/kv/keyValueStore.d.ts +48 -0
- package/dist/cjs/features/kv/keyValueStore.js +67 -0
- package/dist/cjs/features/kv/sqlKeyValueStore.d.ts +34 -0
- package/dist/cjs/features/kv/sqlKeyValueStore.js +81 -0
- package/dist/cjs/features/kv/valueCodec.d.ts +35 -0
- package/dist/cjs/features/kv/valueCodec.js +426 -0
- package/dist/cjs/features/migrations/dataExport.d.ts +77 -0
- package/dist/cjs/features/migrations/dataExport.js +194 -0
- package/dist/cjs/features/migrations/dataImport.d.ts +54 -0
- package/dist/cjs/features/migrations/dataImport.js +300 -0
- package/dist/cjs/features/migrations/migration.d.ts +89 -0
- package/dist/cjs/features/migrations/migration.js +187 -0
- package/dist/cjs/features/migrations/schemaMigrations.d.ts +93 -0
- package/dist/cjs/features/migrations/schemaMigrations.js +220 -0
- package/dist/cjs/features/sync/conflicts/conflictResolver.d.ts +222 -0
- package/dist/cjs/features/sync/conflicts/conflictResolver.js +366 -0
- package/dist/cjs/features/sync/conflicts/index.d.ts +9 -0
- package/dist/cjs/features/sync/conflicts/index.js +21 -0
- package/dist/cjs/features/sync/crossPlatformSync.d.ts +281 -0
- package/dist/cjs/features/sync/crossPlatformSync.js +551 -0
- package/dist/cjs/features/sync/devices/deviceManager.d.ts +243 -0
- package/dist/cjs/features/sync/devices/deviceManager.js +450 -0
- package/dist/cjs/features/sync/devices/index.d.ts +10 -0
- package/dist/cjs/features/sync/devices/index.js +21 -0
- package/dist/cjs/features/sync/index.d.ts +37 -0
- package/dist/cjs/features/sync/index.js +111 -0
- package/dist/cjs/features/sync/protocol/index.d.ts +11 -0
- package/dist/cjs/features/sync/protocol/index.js +23 -0
- package/dist/cjs/features/sync/protocol/messages.d.ts +348 -0
- package/dist/cjs/features/sync/protocol/messages.js +215 -0
- package/dist/cjs/features/sync/protocol/vectorClock.d.ts +164 -0
- package/dist/cjs/features/sync/protocol/vectorClock.js +256 -0
- package/dist/cjs/features/sync/syncManager.d.ts +348 -0
- package/dist/cjs/features/sync/syncManager.js +455 -0
- package/dist/cjs/features/sync/tables/index.d.ts +10 -0
- package/dist/cjs/features/sync/tables/index.js +21 -0
- package/dist/cjs/features/sync/tables/syncLogManager.d.ts +216 -0
- package/dist/cjs/features/sync/tables/syncLogManager.js +423 -0
- package/dist/cjs/features/sync/transport/httpTransport.d.ts +123 -0
- package/dist/cjs/features/sync/transport/httpTransport.js +346 -0
- package/dist/cjs/features/sync/transport/index.d.ts +12 -0
- package/dist/cjs/features/sync/transport/index.js +25 -0
- package/dist/cjs/features/sync/transport/transport.d.ts +259 -0
- package/dist/cjs/features/sync/transport/transport.js +163 -0
- package/dist/cjs/features/sync/transport/websocketTransport.d.ts +126 -0
- package/dist/cjs/features/sync/transport/websocketTransport.js +342 -0
- package/dist/cjs/fts/PostgresFts.d.ts +29 -0
- package/dist/cjs/fts/PostgresFts.js +77 -0
- package/dist/cjs/fts/SqliteFts5.d.ts +22 -0
- package/dist/cjs/fts/SqliteFts5.js +58 -0
- package/dist/cjs/index.d.ts +30 -0
- package/dist/cjs/index.js +101 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/shared/connectionGate.d.ts +94 -0
- package/dist/cjs/shared/connectionGate.js +77 -0
- package/dist/cjs/shared/errors.d.ts +23 -0
- package/dist/cjs/shared/errors.js +45 -0
- package/dist/cjs/shared/logger.d.ts +26 -0
- package/dist/cjs/shared/logger.js +40 -0
- package/dist/cjs/shared/parameterUtils.d.ts +16 -0
- package/dist/cjs/shared/parameterUtils.js +59 -0
- package/dist/cjs/shared/postgresTarget.d.ts +11 -0
- package/dist/cjs/shared/postgresTarget.js +30 -0
- package/dist/cjs/shared/segment.d.ts +43 -0
- package/dist/cjs/shared/segment.js +63 -0
- package/dist/cjs/shared/serializedTransaction.d.ts +14 -0
- package/dist/cjs/shared/serializedTransaction.js +74 -0
- package/dist/cjs/shared/splitSqlStatements.d.ts +27 -0
- package/dist/cjs/shared/splitSqlStatements.js +163 -0
- package/dist/cjs/shared/sqlPlaceholders.d.ts +48 -0
- package/dist/cjs/shared/sqlPlaceholders.js +263 -0
- package/dist/cjs/types/index.d.ts +29 -0
- package/dist/cjs/types/index.js +33 -0
- package/dist/core/portable.d.ts +23 -0
- package/dist/core/portable.d.ts.map +1 -0
- package/dist/core/portable.js +23 -0
- package/dist/core/portable.js.map +1 -0
- package/dist/features/migrations/schemaMigrations.d.ts +93 -0
- package/dist/features/migrations/schemaMigrations.d.ts.map +1 -0
- package/dist/features/migrations/schemaMigrations.js +216 -0
- package/dist/features/migrations/schemaMigrations.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +139 -24
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
var crossPlatformSync_exports = {};
|
|
19
|
+
__export(crossPlatformSync_exports, {
|
|
20
|
+
CrossPlatformSync: () => CrossPlatformSync,
|
|
21
|
+
createCrossPlatformSync: () => createCrossPlatformSync
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(crossPlatformSync_exports);
|
|
24
|
+
var import_vectorClock = require("./protocol/vectorClock.js");
|
|
25
|
+
var import_messages = require("./protocol/messages.js");
|
|
26
|
+
var import_websocketTransport = require("./transport/websocketTransport.js");
|
|
27
|
+
var import_httpTransport = require("./transport/httpTransport.js");
|
|
28
|
+
var import_conflictResolver = require("./conflicts/conflictResolver.js");
|
|
29
|
+
var import_deviceManager = require("./devices/deviceManager.js");
|
|
30
|
+
var import_syncLogManager = require("./tables/syncLogManager.js");
|
|
31
|
+
function toProtocolDeviceType(deviceType) {
|
|
32
|
+
if (deviceType === "unknown") {
|
|
33
|
+
return "browser";
|
|
34
|
+
}
|
|
35
|
+
return deviceType;
|
|
36
|
+
}
|
|
37
|
+
function capabilitiesToArray(capabilities) {
|
|
38
|
+
const result = [];
|
|
39
|
+
if (capabilities.realTimeSync)
|
|
40
|
+
result.push("realtime");
|
|
41
|
+
if (capabilities.backgroundSync)
|
|
42
|
+
result.push("background");
|
|
43
|
+
if (capabilities.pushNotifications)
|
|
44
|
+
result.push("push");
|
|
45
|
+
if (capabilities.offlineMode)
|
|
46
|
+
result.push("offline");
|
|
47
|
+
result.push("gzip", "delta");
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
class CrossPlatformSync {
|
|
51
|
+
constructor(options) {
|
|
52
|
+
this._transport = null;
|
|
53
|
+
this._syncTimer = null;
|
|
54
|
+
this._isSyncing = false;
|
|
55
|
+
this._disposed = false;
|
|
56
|
+
this._adapter = options.localAdapter;
|
|
57
|
+
this._deviceId = (0, import_vectorClock.generateDeviceId)();
|
|
58
|
+
this._options = {
|
|
59
|
+
localAdapter: options.localAdapter,
|
|
60
|
+
endpoint: options.endpoint,
|
|
61
|
+
authToken: options.authToken ?? "",
|
|
62
|
+
device: options.device ?? {},
|
|
63
|
+
tables: options.tables,
|
|
64
|
+
transport: options.transport ?? "auto",
|
|
65
|
+
transportOptions: options.transportOptions ?? {},
|
|
66
|
+
defaultConflictStrategy: options.defaultConflictStrategy ?? "last-write-wins",
|
|
67
|
+
hooks: options.hooks ?? {},
|
|
68
|
+
syncInterval: options.syncInterval ?? 0,
|
|
69
|
+
realTime: options.realTime ?? true,
|
|
70
|
+
batchSize: options.batchSize ?? 100,
|
|
71
|
+
compression: options.compression ?? true
|
|
72
|
+
};
|
|
73
|
+
this._deviceManager = new import_deviceManager.DeviceManager({
|
|
74
|
+
adapter: this._adapter,
|
|
75
|
+
deviceName: options.device?.name,
|
|
76
|
+
autoDetect: true
|
|
77
|
+
});
|
|
78
|
+
this._syncLogManager = new import_syncLogManager.SyncLogManager(this._adapter, this._deviceId);
|
|
79
|
+
this._conflictResolver = new import_conflictResolver.ConflictResolver({
|
|
80
|
+
defaultStrategy: this._options.defaultConflictStrategy,
|
|
81
|
+
tableStrategies: this._buildTableStrategies(),
|
|
82
|
+
fieldMergers: this._buildFieldMergers(),
|
|
83
|
+
hooks: {
|
|
84
|
+
onConflictDetected: this._options.hooks.onConflictDetected,
|
|
85
|
+
onConflictNeedsResolution: this._options.hooks.onConflictNeedsResolution,
|
|
86
|
+
onConflictResolved: this._options.hooks.onConflictResolved,
|
|
87
|
+
onMergeFailed: this._options.hooks.onMergeFailed
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
this._status = {
|
|
91
|
+
state: "idle",
|
|
92
|
+
connected: false,
|
|
93
|
+
pendingChanges: 0,
|
|
94
|
+
pendingConflicts: 0,
|
|
95
|
+
peers: []
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Initialize the sync manager.
|
|
100
|
+
*/
|
|
101
|
+
async initialize() {
|
|
102
|
+
await this._deviceManager.initialize();
|
|
103
|
+
await this._syncLogManager.initialize();
|
|
104
|
+
await this._initializeTransport();
|
|
105
|
+
this._setupDeviceManagerListeners();
|
|
106
|
+
this._status.device = this._deviceManager.getCurrentDevice() ?? void 0;
|
|
107
|
+
this._status.peers = await this._deviceManager.getPeers();
|
|
108
|
+
if (this._options.syncInterval > 0) {
|
|
109
|
+
this._startSyncTimer();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Perform a sync operation.
|
|
114
|
+
*/
|
|
115
|
+
async sync() {
|
|
116
|
+
if (this._isSyncing) {
|
|
117
|
+
throw new Error("Sync already in progress");
|
|
118
|
+
}
|
|
119
|
+
if (this._disposed) {
|
|
120
|
+
throw new Error("Sync manager has been disposed");
|
|
121
|
+
}
|
|
122
|
+
this._isSyncing = true;
|
|
123
|
+
this._status.state = "syncing";
|
|
124
|
+
const startTime = Date.now();
|
|
125
|
+
this._options.hooks.onSyncStart?.();
|
|
126
|
+
const result = {
|
|
127
|
+
success: false,
|
|
128
|
+
pushed: 0,
|
|
129
|
+
pulled: 0,
|
|
130
|
+
conflicts: 0,
|
|
131
|
+
conflictsResolved: 0,
|
|
132
|
+
duration: 0,
|
|
133
|
+
errors: [],
|
|
134
|
+
vectorClock: this._syncLogManager.getVectorClockData()
|
|
135
|
+
};
|
|
136
|
+
try {
|
|
137
|
+
if (!this._transport?.isConnected) {
|
|
138
|
+
await this._transport?.connect();
|
|
139
|
+
}
|
|
140
|
+
const pushResult = await this._pushChanges();
|
|
141
|
+
result.pushed = pushResult.pushed;
|
|
142
|
+
result.conflicts += pushResult.conflicts;
|
|
143
|
+
const pullResult = await this._pullChanges();
|
|
144
|
+
result.pulled = pullResult.pulled;
|
|
145
|
+
result.conflicts += pullResult.conflicts;
|
|
146
|
+
const resolveResult = await this._resolveConflicts();
|
|
147
|
+
result.conflictsResolved = resolveResult.resolved;
|
|
148
|
+
result.vectorClock = this._syncLogManager.getVectorClockData();
|
|
149
|
+
result.success = true;
|
|
150
|
+
this._status.lastSyncAt = Date.now();
|
|
151
|
+
} catch (error) {
|
|
152
|
+
result.errors.push(error);
|
|
153
|
+
this._status.state = "error";
|
|
154
|
+
this._options.hooks.onSyncError?.(error);
|
|
155
|
+
}
|
|
156
|
+
result.duration = Date.now() - startTime;
|
|
157
|
+
this._isSyncing = false;
|
|
158
|
+
this._status.state = result.success ? "idle" : "error";
|
|
159
|
+
this._status.lastResult = result;
|
|
160
|
+
this._status.pendingChanges = await this._getPendingChangesCount();
|
|
161
|
+
this._status.pendingConflicts = this._conflictResolver.getPendingConflicts().length;
|
|
162
|
+
this._options.hooks.onSyncComplete?.(result);
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Push a single change immediately (for real-time sync).
|
|
167
|
+
*/
|
|
168
|
+
async pushChange(tableName, recordId, operation, data) {
|
|
169
|
+
if (!this._options.realTime)
|
|
170
|
+
return;
|
|
171
|
+
if (!this._transport?.isConnected)
|
|
172
|
+
return;
|
|
173
|
+
const device = this._deviceManager.getCurrentDevice();
|
|
174
|
+
if (!device)
|
|
175
|
+
return;
|
|
176
|
+
const loggedChange = await this._syncLogManager.logChange({
|
|
177
|
+
table: tableName,
|
|
178
|
+
recordId,
|
|
179
|
+
operation,
|
|
180
|
+
oldData: operation === "DELETE" ? data : void 0,
|
|
181
|
+
newData: operation !== "DELETE" ? data : void 0
|
|
182
|
+
});
|
|
183
|
+
const vectorClock = this._syncLogManager.getVectorClockData();
|
|
184
|
+
const message = (0, import_messages.createDeltaPushMessage)(device.deviceId, [loggedChange], vectorClock);
|
|
185
|
+
await this._transport.send(message);
|
|
186
|
+
this._options.hooks.onChangePushed?.(loggedChange);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Get current sync status.
|
|
190
|
+
*/
|
|
191
|
+
getStatus() {
|
|
192
|
+
return { ...this._status };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get pending conflicts.
|
|
196
|
+
*/
|
|
197
|
+
getPendingConflicts() {
|
|
198
|
+
return this._conflictResolver.getPendingConflicts();
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Resolve a conflict manually.
|
|
202
|
+
*/
|
|
203
|
+
async resolveConflict(conflictId, decision, mergedData) {
|
|
204
|
+
return this._conflictResolver.resolveManually(conflictId, decision, mergedData);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Connect to sync server.
|
|
208
|
+
*/
|
|
209
|
+
async connect() {
|
|
210
|
+
await this._transport?.connect();
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Disconnect from sync server.
|
|
214
|
+
*/
|
|
215
|
+
async disconnect() {
|
|
216
|
+
await this._transport?.disconnect();
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Dispose of the sync manager.
|
|
220
|
+
*/
|
|
221
|
+
async dispose() {
|
|
222
|
+
if (this._disposed)
|
|
223
|
+
return;
|
|
224
|
+
this._disposed = true;
|
|
225
|
+
if (this._syncTimer) {
|
|
226
|
+
clearInterval(this._syncTimer);
|
|
227
|
+
this._syncTimer = null;
|
|
228
|
+
}
|
|
229
|
+
await this._transport?.dispose();
|
|
230
|
+
await this._deviceManager.dispose();
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Initialize the transport layer.
|
|
234
|
+
*/
|
|
235
|
+
async _initializeTransport() {
|
|
236
|
+
const transportType = this._options.transport;
|
|
237
|
+
const baseOptions = {
|
|
238
|
+
endpoint: this._options.endpoint,
|
|
239
|
+
authToken: this._options.authToken,
|
|
240
|
+
compression: this._options.compression,
|
|
241
|
+
...this._options.transportOptions
|
|
242
|
+
};
|
|
243
|
+
if (transportType === "websocket" || transportType === "auto") {
|
|
244
|
+
try {
|
|
245
|
+
this._transport = new import_websocketTransport.WebSocketTransport(baseOptions);
|
|
246
|
+
await this._transport.connect();
|
|
247
|
+
this._setupTransportListeners();
|
|
248
|
+
return;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (transportType === "websocket") {
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
console.warn("[CrossPlatformSync] WebSocket failed, falling back to HTTP");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
this._transport = new import_httpTransport.HttpTransport(baseOptions);
|
|
257
|
+
this._setupTransportListeners();
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Set up transport event listeners.
|
|
261
|
+
*/
|
|
262
|
+
_setupTransportListeners() {
|
|
263
|
+
if (!this._transport)
|
|
264
|
+
return;
|
|
265
|
+
this._transport.on("connected", () => {
|
|
266
|
+
this._status.connected = true;
|
|
267
|
+
this._status.state = "idle";
|
|
268
|
+
this._options.hooks.onConnectionStateChange?.(true);
|
|
269
|
+
this._sendHandshake().catch((error) => {
|
|
270
|
+
console.error("[CrossPlatformSync] Handshake failed:", error);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
this._transport.on("disconnected", () => {
|
|
274
|
+
this._status.connected = false;
|
|
275
|
+
this._status.state = "offline";
|
|
276
|
+
this._options.hooks.onConnectionStateChange?.(false);
|
|
277
|
+
});
|
|
278
|
+
this._transport.on("message", ({ message }) => {
|
|
279
|
+
this._handleIncomingMessage(message).catch((error) => {
|
|
280
|
+
console.error("[CrossPlatformSync] Message handling error:", error);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
this._transport.on("error", ({ error }) => {
|
|
284
|
+
console.error("[CrossPlatformSync] Transport error:", error);
|
|
285
|
+
this._status.state = "error";
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Set up device manager listeners.
|
|
290
|
+
*/
|
|
291
|
+
_setupDeviceManagerListeners() {
|
|
292
|
+
this._deviceManager.on("peerDiscovered", (peer) => {
|
|
293
|
+
this._status.peers.push(peer);
|
|
294
|
+
this._options.hooks.onPeerDiscovered?.(peer);
|
|
295
|
+
});
|
|
296
|
+
this._deviceManager.on("presenceChanged", async (event) => {
|
|
297
|
+
const peer = this._status.peers.find((p) => p.deviceId === event.deviceId);
|
|
298
|
+
if (peer) {
|
|
299
|
+
peer.presence = event.currentStatus;
|
|
300
|
+
}
|
|
301
|
+
const currentDevice = this._deviceManager.getCurrentDevice();
|
|
302
|
+
if (event.deviceId === currentDevice?.deviceId) {
|
|
303
|
+
if (this._transport?.isConnected && currentDevice) {
|
|
304
|
+
const deviceInfo = {
|
|
305
|
+
deviceId: currentDevice.deviceId,
|
|
306
|
+
deviceType: toProtocolDeviceType(currentDevice.deviceType),
|
|
307
|
+
deviceName: currentDevice.deviceName
|
|
308
|
+
};
|
|
309
|
+
const message = (0, import_messages.createPresenceMessage)(deviceInfo, event.currentStatus);
|
|
310
|
+
await this._transport.send(message);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Send handshake to server.
|
|
317
|
+
*/
|
|
318
|
+
async _sendHandshake() {
|
|
319
|
+
const device = this._deviceManager.getCurrentDevice();
|
|
320
|
+
if (!device || !this._transport)
|
|
321
|
+
return;
|
|
322
|
+
const deviceInfo = {
|
|
323
|
+
deviceId: device.deviceId,
|
|
324
|
+
deviceType: toProtocolDeviceType(device.deviceType),
|
|
325
|
+
deviceName: device.deviceName
|
|
326
|
+
};
|
|
327
|
+
const vectorClock = this._syncLogManager.getVectorClockData();
|
|
328
|
+
const request = (0, import_messages.createHandshakeRequest)(deviceInfo, vectorClock, Object.keys(this._options.tables), capabilitiesToArray(device.capabilities));
|
|
329
|
+
const response = await this._transport.request(request, 3e4);
|
|
330
|
+
if (response.type === "handshake_response") {
|
|
331
|
+
const responseData = response;
|
|
332
|
+
if (responseData.vectorClock) {
|
|
333
|
+
this._syncLogManager.mergeVectorClock(responseData.vectorClock);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Handle incoming sync message.
|
|
339
|
+
*/
|
|
340
|
+
async _handleIncomingMessage(message) {
|
|
341
|
+
switch (message.type) {
|
|
342
|
+
case "delta_push": {
|
|
343
|
+
const pushMessage = message;
|
|
344
|
+
await this._applyRemoteChanges(pushMessage.batch.changes);
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
case "conflict": {
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case "presence": {
|
|
351
|
+
const presenceMsg = message;
|
|
352
|
+
const peer = this._status.peers.find((p) => p.deviceId === presenceMsg.device.deviceId);
|
|
353
|
+
if (peer) {
|
|
354
|
+
peer.presence = presenceMsg.status;
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Push local changes to server.
|
|
362
|
+
*/
|
|
363
|
+
async _pushChanges() {
|
|
364
|
+
const device = this._deviceManager.getCurrentDevice();
|
|
365
|
+
if (!device || !this._transport?.isConnected) {
|
|
366
|
+
return { pushed: 0, conflicts: 0 };
|
|
367
|
+
}
|
|
368
|
+
const vectorClock = this._syncLogManager.getVectorClockData();
|
|
369
|
+
const changes = await this._syncLogManager.getChangesSince(vectorClock, { limit: this._options.batchSize });
|
|
370
|
+
if (changes.length === 0) {
|
|
371
|
+
return { pushed: 0, conflicts: 0 };
|
|
372
|
+
}
|
|
373
|
+
const pushMessage = (0, import_messages.createDeltaPushMessage)(device.deviceId, changes, vectorClock);
|
|
374
|
+
const response = await this._transport.request(pushMessage, 3e4);
|
|
375
|
+
let conflicts = 0;
|
|
376
|
+
if (response.type === "ack") {
|
|
377
|
+
const changeIds = changes.map((c) => c.changeId);
|
|
378
|
+
await this._syncLogManager.markSynced(changeIds);
|
|
379
|
+
for (const change of changes) {
|
|
380
|
+
this._options.hooks.onChangePushed?.(change);
|
|
381
|
+
}
|
|
382
|
+
} else if (response.type === "conflict") {
|
|
383
|
+
conflicts = 1;
|
|
384
|
+
}
|
|
385
|
+
return { pushed: changes.length, conflicts };
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Pull remote changes from server.
|
|
389
|
+
*/
|
|
390
|
+
async _pullChanges() {
|
|
391
|
+
const device = this._deviceManager.getCurrentDevice();
|
|
392
|
+
if (!device || !this._transport?.isConnected) {
|
|
393
|
+
return { pulled: 0, conflicts: 0 };
|
|
394
|
+
}
|
|
395
|
+
const vectorClock = this._syncLogManager.getVectorClockData();
|
|
396
|
+
const request = (0, import_messages.createDeltaPullRequest)(device.deviceId, vectorClock, Object.keys(this._options.tables), this._options.batchSize);
|
|
397
|
+
const response = await this._transport.request(request, 3e4);
|
|
398
|
+
if (response.type !== "delta_pull_response") {
|
|
399
|
+
return { pulled: 0, conflicts: 0 };
|
|
400
|
+
}
|
|
401
|
+
const { applied, conflicts } = await this._applyRemoteChanges(response.batch.changes);
|
|
402
|
+
if (response.vectorClock) {
|
|
403
|
+
this._syncLogManager.mergeVectorClock(response.vectorClock);
|
|
404
|
+
}
|
|
405
|
+
return { pulled: applied, conflicts };
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Apply remote changes to local database.
|
|
409
|
+
*/
|
|
410
|
+
async _applyRemoteChanges(changes) {
|
|
411
|
+
let applied = 0;
|
|
412
|
+
let conflicts = 0;
|
|
413
|
+
const device = this._deviceManager.getCurrentDevice();
|
|
414
|
+
const localClock = this._syncLogManager.getVectorClockData();
|
|
415
|
+
for (const change of changes) {
|
|
416
|
+
try {
|
|
417
|
+
const localData = await this._getLocalRecord(change.table, change.recordId);
|
|
418
|
+
if (localData && device) {
|
|
419
|
+
const conflict = this._conflictResolver.detectConflict(change.table, change.recordId, localData, change.newData ?? {}, localClock, change.vectorClock, device.deviceId, change.deviceId);
|
|
420
|
+
if (conflict) {
|
|
421
|
+
conflicts++;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
await this._applyChange(change);
|
|
426
|
+
applied++;
|
|
427
|
+
this._options.hooks.onChangePulled?.(change);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
console.error("[CrossPlatformSync] Failed to apply change:", error);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return { applied, conflicts };
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Apply a single change to local database.
|
|
436
|
+
*/
|
|
437
|
+
async _applyChange(change) {
|
|
438
|
+
const tableConfig = this._options.tables[change.table];
|
|
439
|
+
const primaryKey = tableConfig?.primaryKey ?? "id";
|
|
440
|
+
const data = change.newData ?? change.oldData ?? {};
|
|
441
|
+
switch (change.operation) {
|
|
442
|
+
case "INSERT": {
|
|
443
|
+
const columns = Object.keys(data);
|
|
444
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
445
|
+
const values = columns.map((col) => data[col]);
|
|
446
|
+
await this._adapter.run(`INSERT OR REPLACE INTO ${change.table} (${columns.join(", ")}) VALUES (${placeholders})`, values);
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
case "UPDATE": {
|
|
450
|
+
const columns = Object.keys(data).filter((c) => c !== primaryKey);
|
|
451
|
+
const setClause = columns.map((col) => `${col} = ?`).join(", ");
|
|
452
|
+
const values = columns.map((col) => data[col]);
|
|
453
|
+
values.push(change.recordId);
|
|
454
|
+
await this._adapter.run(`UPDATE ${change.table} SET ${setClause} WHERE ${primaryKey} = ?`, values);
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
case "DELETE": {
|
|
458
|
+
await this._adapter.run(`DELETE FROM ${change.table} WHERE ${primaryKey} = ?`, [change.recordId]);
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Get a local record by table and ID.
|
|
465
|
+
*/
|
|
466
|
+
async _getLocalRecord(tableName, recordId) {
|
|
467
|
+
const tableConfig = this._options.tables[tableName];
|
|
468
|
+
const primaryKey = tableConfig?.primaryKey ?? "id";
|
|
469
|
+
const row = await this._adapter.get(`SELECT * FROM ${tableName} WHERE ${primaryKey} = ?`, [recordId]);
|
|
470
|
+
return row ?? null;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Resolve pending conflicts.
|
|
474
|
+
*/
|
|
475
|
+
async _resolveConflicts() {
|
|
476
|
+
const pending = this._conflictResolver.getPendingConflicts();
|
|
477
|
+
let resolved = 0;
|
|
478
|
+
for (const conflict of pending) {
|
|
479
|
+
try {
|
|
480
|
+
const resolution = await this._conflictResolver.resolve(conflict);
|
|
481
|
+
if (resolution.decision === "use_local" || resolution.decision === "use_remote") {
|
|
482
|
+
const data = resolution.decision === "use_local" ? conflict.localData : conflict.remoteData;
|
|
483
|
+
const changeRecord = (0, import_messages.createChangeRecord)(conflict.tableName, conflict.recordId, "UPDATE", conflict.localDeviceId, resolution.mergedClock, data);
|
|
484
|
+
await this._applyChange(changeRecord);
|
|
485
|
+
} else if (resolution.decision === "use_merged" && resolution.mergedData) {
|
|
486
|
+
const changeRecord = (0, import_messages.createChangeRecord)(conflict.tableName, conflict.recordId, "UPDATE", conflict.localDeviceId, resolution.mergedClock, resolution.mergedData);
|
|
487
|
+
await this._applyChange(changeRecord);
|
|
488
|
+
}
|
|
489
|
+
await this._syncLogManager.resolveConflict(conflict.conflictId, resolution.decision);
|
|
490
|
+
resolved++;
|
|
491
|
+
} catch (error) {
|
|
492
|
+
console.error("[CrossPlatformSync] Failed to resolve conflict:", error);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return { resolved };
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Get count of pending changes.
|
|
499
|
+
*/
|
|
500
|
+
async _getPendingChangesCount() {
|
|
501
|
+
return this._syncLogManager.getUnsyncedCount();
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Start sync timer.
|
|
505
|
+
*/
|
|
506
|
+
_startSyncTimer() {
|
|
507
|
+
if (this._syncTimer)
|
|
508
|
+
return;
|
|
509
|
+
this._syncTimer = setInterval(async () => {
|
|
510
|
+
if (!this._isSyncing && this._status.connected) {
|
|
511
|
+
await this.sync().catch((error) => {
|
|
512
|
+
console.error("[CrossPlatformSync] Scheduled sync failed:", error);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}, this._options.syncInterval);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Build table strategies map.
|
|
519
|
+
*/
|
|
520
|
+
_buildTableStrategies() {
|
|
521
|
+
const strategies = {};
|
|
522
|
+
for (const [table, config] of Object.entries(this._options.tables)) {
|
|
523
|
+
if (config.conflictStrategy) {
|
|
524
|
+
strategies[table] = config.conflictStrategy;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return strategies;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Build field mergers map.
|
|
531
|
+
*/
|
|
532
|
+
_buildFieldMergers() {
|
|
533
|
+
const mergers = {};
|
|
534
|
+
for (const [_table, config] of Object.entries(this._options.tables)) {
|
|
535
|
+
if (config.fieldMergers) {
|
|
536
|
+
Object.assign(mergers, config.fieldMergers);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return mergers;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const createCrossPlatformSync = async (options) => {
|
|
543
|
+
const sync = new CrossPlatformSync(options);
|
|
544
|
+
await sync.initialize();
|
|
545
|
+
return sync;
|
|
546
|
+
};
|
|
547
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
548
|
+
0 && (module.exports = {
|
|
549
|
+
CrossPlatformSync,
|
|
550
|
+
createCrossPlatformSync
|
|
551
|
+
});
|