@syncvault/sdk 1.1.0 → 1.2.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/README.md +78 -0
- package/package.json +1 -1
- package/src/index.d.ts +87 -0
- package/src/index.js +1 -0
- package/src/offline.js +434 -0
package/README.md
CHANGED
|
@@ -210,3 +210,81 @@ await fetch(`https://api.syncvault.dev/api/entitlements/${userId}`, {
|
|
|
210
210
|
```
|
|
211
211
|
|
|
212
212
|
Never expose the secret token in client-side code.
|
|
213
|
+
|
|
214
|
+
## Offline Sync
|
|
215
|
+
|
|
216
|
+
The SDK supports offline-first sync with local caching and automatic retry.
|
|
217
|
+
|
|
218
|
+
### Basic Usage
|
|
219
|
+
|
|
220
|
+
```javascript
|
|
221
|
+
import { SyncVault, createOfflineClient } from '@syncvault/sdk';
|
|
222
|
+
|
|
223
|
+
const baseClient = new SyncVault({ appToken: 'your_token' });
|
|
224
|
+
const vault = createOfflineClient(baseClient, {
|
|
225
|
+
retryInterval: 30000, // Retry every 30 seconds
|
|
226
|
+
maxRetries: 10, // Max retries per operation
|
|
227
|
+
autoSync: true // Auto-sync when online
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Initialize (loads cache from storage)
|
|
231
|
+
await vault.init();
|
|
232
|
+
|
|
233
|
+
// Authenticate
|
|
234
|
+
await vault.auth('username', 'password');
|
|
235
|
+
|
|
236
|
+
// Put - queues if offline, syncs when online
|
|
237
|
+
await vault.put('data.json', { hello: 'world' });
|
|
238
|
+
|
|
239
|
+
// Get - returns cached data if offline
|
|
240
|
+
const data = await vault.get('data.json');
|
|
241
|
+
|
|
242
|
+
// Check pending operations
|
|
243
|
+
if (vault.hasPendingChanges()) {
|
|
244
|
+
console.log('Pending:', vault.pendingCount());
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Callbacks
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
vault.onSyncSuccess = (op) => {
|
|
252
|
+
console.log('Synced:', op.path);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
vault.onSyncError = (op, error) => {
|
|
256
|
+
console.log('Failed:', op.path, error);
|
|
257
|
+
};
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Manual Sync Control
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
// Manually trigger sync
|
|
264
|
+
await vault.syncPending();
|
|
265
|
+
|
|
266
|
+
// Stop auto-sync
|
|
267
|
+
vault.stopAutoSync();
|
|
268
|
+
|
|
269
|
+
// Clear cache/queue
|
|
270
|
+
await vault.getStore().clearCache();
|
|
271
|
+
await vault.getStore().clearQueue();
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Custom Storage (React Native, etc.)
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
const customStorage = {
|
|
278
|
+
async get(key) {
|
|
279
|
+
return AsyncStorage.getItem(key).then(JSON.parse);
|
|
280
|
+
},
|
|
281
|
+
async set(key, value) {
|
|
282
|
+
await AsyncStorage.setItem(key, JSON.stringify(value));
|
|
283
|
+
},
|
|
284
|
+
async remove(key) {
|
|
285
|
+
await AsyncStorage.removeItem(key);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const vault = createOfflineClient(baseClient, { storage: customStorage });
|
|
290
|
+
```
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -72,3 +72,90 @@ export declare class SyncVault {
|
|
|
72
72
|
|
|
73
73
|
export declare function encrypt(data: unknown, password: string): Promise<string>;
|
|
74
74
|
export declare function decrypt<T = unknown>(encryptedBase64: string, password: string): Promise<T>;
|
|
75
|
+
|
|
76
|
+
// Offline sync types
|
|
77
|
+
export interface PendingOperation {
|
|
78
|
+
id: string;
|
|
79
|
+
type: 'put' | 'delete';
|
|
80
|
+
path: string;
|
|
81
|
+
data?: string;
|
|
82
|
+
createdAt: number;
|
|
83
|
+
retries: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface CacheEntry {
|
|
87
|
+
path: string;
|
|
88
|
+
data: string;
|
|
89
|
+
updatedAt: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface OfflineStorage {
|
|
93
|
+
get(key: string): Promise<unknown | null>;
|
|
94
|
+
set(key: string, value: unknown): Promise<void>;
|
|
95
|
+
remove(key: string): Promise<void>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface OfflineOptions {
|
|
99
|
+
storage?: OfflineStorage;
|
|
100
|
+
retryInterval?: number;
|
|
101
|
+
maxRetries?: number;
|
|
102
|
+
autoSync?: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export declare class OfflineStore {
|
|
106
|
+
constructor(storage?: OfflineStorage | null);
|
|
107
|
+
load(): Promise<void>;
|
|
108
|
+
persist(): Promise<void>;
|
|
109
|
+
getCached(path: string): CacheEntry | null;
|
|
110
|
+
setCache(path: string, data: string): Promise<void>;
|
|
111
|
+
removeCache(path: string): Promise<void>;
|
|
112
|
+
queueOperation(op: Partial<PendingOperation>): Promise<void>;
|
|
113
|
+
getPendingOperations(): PendingOperation[];
|
|
114
|
+
removeOperation(id: string): Promise<void>;
|
|
115
|
+
incrementRetry(id: string): Promise<void>;
|
|
116
|
+
hasPendingOperations(): boolean;
|
|
117
|
+
clearQueue(): Promise<void>;
|
|
118
|
+
clearCache(): Promise<void>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export declare class OfflineSyncVault {
|
|
122
|
+
constructor(baseClient: SyncVault, options?: OfflineOptions);
|
|
123
|
+
|
|
124
|
+
onSyncSuccess: ((op: PendingOperation) => void) | null;
|
|
125
|
+
onSyncError: ((op: PendingOperation, error: Error) => void) | null;
|
|
126
|
+
|
|
127
|
+
init(): Promise<void>;
|
|
128
|
+
|
|
129
|
+
// Auth (proxied to base client)
|
|
130
|
+
auth(username: string, password: string): Promise<User>;
|
|
131
|
+
register(username: string, password: string): Promise<User>;
|
|
132
|
+
setAuth(token: string, password: string): void;
|
|
133
|
+
getAuthUrl(state?: string): string;
|
|
134
|
+
exchangeCode(code: string, password: string): Promise<User>;
|
|
135
|
+
isAuthenticated(): boolean;
|
|
136
|
+
logout(): void;
|
|
137
|
+
|
|
138
|
+
// Data operations with offline support
|
|
139
|
+
put<T = unknown>(path: string, data: T): Promise<PutResponse | { queued: boolean; path: string }>;
|
|
140
|
+
get<T = unknown>(path: string): Promise<T>;
|
|
141
|
+
delete(path: string): Promise<DeleteResponse | { queued: boolean; path: string }>;
|
|
142
|
+
list(): Promise<FileInfo[]>;
|
|
143
|
+
|
|
144
|
+
// Metadata/entitlements (no offline caching)
|
|
145
|
+
getMetadata<T extends Metadata = Metadata>(): Promise<T>;
|
|
146
|
+
setMetadata<T extends Metadata = Metadata>(metadata: T): Promise<T>;
|
|
147
|
+
updateMetadata<T extends Metadata = Metadata>(metadata: Partial<T>): Promise<T>;
|
|
148
|
+
getEntitlements<T extends Entitlements = Entitlements>(): Promise<T>;
|
|
149
|
+
getQuota(): Promise<QuotaInfo>;
|
|
150
|
+
getUser(): Promise<User>;
|
|
151
|
+
|
|
152
|
+
// Sync control
|
|
153
|
+
startAutoSync(): void;
|
|
154
|
+
stopAutoSync(): void;
|
|
155
|
+
syncPending(): Promise<void>;
|
|
156
|
+
hasPendingChanges(): boolean;
|
|
157
|
+
pendingCount(): number;
|
|
158
|
+
getStore(): OfflineStore;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export declare function createOfflineClient(baseClient: SyncVault, options?: OfflineOptions): OfflineSyncVault;
|
package/src/index.js
CHANGED
package/src/offline.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { encrypt, decrypt } from './crypto.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_STORE_NAME = 'syncvault-offline';
|
|
4
|
+
const CACHE_KEY = 'cache';
|
|
5
|
+
const QUEUE_KEY = 'queue';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Storage adapter interface
|
|
9
|
+
*/
|
|
10
|
+
class MemoryStorage {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.data = new Map();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async get(key) {
|
|
16
|
+
return this.data.get(key) || null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async set(key, value) {
|
|
20
|
+
this.data.set(key, value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async remove(key) {
|
|
24
|
+
this.data.delete(key);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* LocalStorage adapter (browser)
|
|
30
|
+
*/
|
|
31
|
+
class LocalStorageAdapter {
|
|
32
|
+
constructor(prefix = DEFAULT_STORE_NAME) {
|
|
33
|
+
this.prefix = prefix;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async get(key) {
|
|
37
|
+
const item = localStorage.getItem(`${this.prefix}:${key}`);
|
|
38
|
+
return item ? JSON.parse(item) : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async set(key, value) {
|
|
42
|
+
localStorage.setItem(`${this.prefix}:${key}`, JSON.stringify(value));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async remove(key) {
|
|
46
|
+
localStorage.removeItem(`${this.prefix}:${key}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Offline store for caching and queue management
|
|
52
|
+
*/
|
|
53
|
+
export class OfflineStore {
|
|
54
|
+
constructor(storage = null) {
|
|
55
|
+
this.storage = storage || this._detectStorage();
|
|
56
|
+
this.cache = {};
|
|
57
|
+
this.queue = [];
|
|
58
|
+
this.loaded = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_detectStorage() {
|
|
62
|
+
if (typeof localStorage !== 'undefined') {
|
|
63
|
+
return new LocalStorageAdapter();
|
|
64
|
+
}
|
|
65
|
+
return new MemoryStorage();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async load() {
|
|
69
|
+
if (this.loaded) return;
|
|
70
|
+
|
|
71
|
+
const cache = await this.storage.get(CACHE_KEY);
|
|
72
|
+
const queue = await this.storage.get(QUEUE_KEY);
|
|
73
|
+
|
|
74
|
+
this.cache = cache || {};
|
|
75
|
+
this.queue = queue || [];
|
|
76
|
+
this.loaded = true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async persist() {
|
|
80
|
+
await this.storage.set(CACHE_KEY, this.cache);
|
|
81
|
+
await this.storage.set(QUEUE_KEY, this.queue);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getCached(path) {
|
|
85
|
+
return this.cache[path] || null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async setCache(path, data) {
|
|
89
|
+
this.cache[path] = {
|
|
90
|
+
path,
|
|
91
|
+
data,
|
|
92
|
+
updatedAt: Date.now()
|
|
93
|
+
};
|
|
94
|
+
await this.persist();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async removeCache(path) {
|
|
98
|
+
delete this.cache[path];
|
|
99
|
+
await this.persist();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async queueOperation(op) {
|
|
103
|
+
op.id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
104
|
+
op.createdAt = Date.now();
|
|
105
|
+
op.retries = 0;
|
|
106
|
+
this.queue.push(op);
|
|
107
|
+
await this.persist();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getPendingOperations() {
|
|
111
|
+
return [...this.queue];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async removeOperation(id) {
|
|
115
|
+
this.queue = this.queue.filter(op => op.id !== id);
|
|
116
|
+
await this.persist();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async incrementRetry(id) {
|
|
120
|
+
const op = this.queue.find(op => op.id === id);
|
|
121
|
+
if (op) {
|
|
122
|
+
op.retries++;
|
|
123
|
+
await this.persist();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
hasPendingOperations() {
|
|
128
|
+
return this.queue.length > 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async clearQueue() {
|
|
132
|
+
this.queue = [];
|
|
133
|
+
await this.persist();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async clearCache() {
|
|
137
|
+
this.cache = {};
|
|
138
|
+
await this.persist();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if error is network-related
|
|
144
|
+
*/
|
|
145
|
+
function isNetworkError(error) {
|
|
146
|
+
if (!error) return false;
|
|
147
|
+
const msg = error.message?.toLowerCase() || '';
|
|
148
|
+
return (
|
|
149
|
+
error.name === 'TypeError' ||
|
|
150
|
+
msg.includes('network') ||
|
|
151
|
+
msg.includes('failed to fetch') ||
|
|
152
|
+
msg.includes('load failed') ||
|
|
153
|
+
msg.includes('offline') ||
|
|
154
|
+
msg.includes('timeout') ||
|
|
155
|
+
msg.includes('econnrefused') ||
|
|
156
|
+
msg.includes('enotfound')
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Offline-capable SyncVault client
|
|
162
|
+
*/
|
|
163
|
+
export class OfflineSyncVault {
|
|
164
|
+
constructor(baseClient, options = {}) {
|
|
165
|
+
this.client = baseClient;
|
|
166
|
+
this.store = new OfflineStore(options.storage);
|
|
167
|
+
this.retryInterval = options.retryInterval || 30000;
|
|
168
|
+
this.maxRetries = options.maxRetries || 10;
|
|
169
|
+
this.autoSync = options.autoSync !== false;
|
|
170
|
+
|
|
171
|
+
this.onSyncSuccess = null;
|
|
172
|
+
this.onSyncError = null;
|
|
173
|
+
|
|
174
|
+
this._syncTimer = null;
|
|
175
|
+
this._initialized = false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async init() {
|
|
179
|
+
if (this._initialized) return;
|
|
180
|
+
await this.store.load();
|
|
181
|
+
this._initialized = true;
|
|
182
|
+
|
|
183
|
+
if (this.autoSync) {
|
|
184
|
+
this.startAutoSync();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Proxy auth methods to base client
|
|
190
|
+
*/
|
|
191
|
+
async auth(username, password) {
|
|
192
|
+
await this.init();
|
|
193
|
+
return this.client.auth(username, password);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async register(username, password) {
|
|
197
|
+
await this.init();
|
|
198
|
+
return this.client.register(username, password);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
setAuth(token, password) {
|
|
202
|
+
this.client.setAuth(token, password);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
getAuthUrl(state) {
|
|
206
|
+
return this.client.getAuthUrl(state);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async exchangeCode(code, password) {
|
|
210
|
+
return this.client.exchangeCode(code, password);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
isAuthenticated() {
|
|
214
|
+
return this.client.isAuthenticated();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
logout() {
|
|
218
|
+
this.client.logout();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Put with offline support
|
|
223
|
+
*/
|
|
224
|
+
async put(path, data) {
|
|
225
|
+
await this.init();
|
|
226
|
+
|
|
227
|
+
const encrypted = await encrypt(data, this.client.password);
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const result = await this.client.put(path, data);
|
|
231
|
+
await this.store.setCache(path, encrypted);
|
|
232
|
+
return result;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (isNetworkError(error)) {
|
|
235
|
+
await this.store.setCache(path, encrypted);
|
|
236
|
+
await this.store.queueOperation({
|
|
237
|
+
type: 'put',
|
|
238
|
+
path,
|
|
239
|
+
data: encrypted
|
|
240
|
+
});
|
|
241
|
+
return { queued: true, path };
|
|
242
|
+
}
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get with offline fallback
|
|
249
|
+
*/
|
|
250
|
+
async get(path) {
|
|
251
|
+
await this.init();
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const result = await this.client.get(path);
|
|
255
|
+
// Update cache with fresh data
|
|
256
|
+
const encrypted = await encrypt(result, this.client.password);
|
|
257
|
+
await this.store.setCache(path, encrypted);
|
|
258
|
+
return result;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (isNetworkError(error)) {
|
|
261
|
+
const cached = this.store.getCached(path);
|
|
262
|
+
if (cached) {
|
|
263
|
+
return decrypt(cached.data, this.client.password);
|
|
264
|
+
}
|
|
265
|
+
throw new Error('Offline and no cached data available');
|
|
266
|
+
}
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Delete with offline support
|
|
273
|
+
*/
|
|
274
|
+
async delete(path) {
|
|
275
|
+
await this.init();
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const result = await this.client.delete(path);
|
|
279
|
+
await this.store.removeCache(path);
|
|
280
|
+
return result;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (isNetworkError(error)) {
|
|
283
|
+
await this.store.removeCache(path);
|
|
284
|
+
await this.store.queueOperation({
|
|
285
|
+
type: 'delete',
|
|
286
|
+
path
|
|
287
|
+
});
|
|
288
|
+
return { queued: true, path };
|
|
289
|
+
}
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* List files (no offline caching for list)
|
|
296
|
+
*/
|
|
297
|
+
async list() {
|
|
298
|
+
return this.client.list();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Proxy metadata/entitlements (no offline for these)
|
|
303
|
+
*/
|
|
304
|
+
async getMetadata() {
|
|
305
|
+
return this.client.getMetadata();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async setMetadata(metadata) {
|
|
309
|
+
return this.client.setMetadata(metadata);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async updateMetadata(metadata) {
|
|
313
|
+
return this.client.updateMetadata(metadata);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async getEntitlements() {
|
|
317
|
+
return this.client.getEntitlements();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async getQuota() {
|
|
321
|
+
return this.client.getQuota();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async getUser() {
|
|
325
|
+
return this.client.getUser();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Start automatic sync of pending operations
|
|
330
|
+
*/
|
|
331
|
+
startAutoSync() {
|
|
332
|
+
if (this._syncTimer) return;
|
|
333
|
+
|
|
334
|
+
this._syncTimer = setInterval(() => {
|
|
335
|
+
this.syncPending();
|
|
336
|
+
}, this.retryInterval);
|
|
337
|
+
|
|
338
|
+
// Also listen for online event in browser
|
|
339
|
+
if (typeof window !== 'undefined') {
|
|
340
|
+
window.addEventListener('online', () => this.syncPending());
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Stop automatic sync
|
|
346
|
+
*/
|
|
347
|
+
stopAutoSync() {
|
|
348
|
+
if (this._syncTimer) {
|
|
349
|
+
clearInterval(this._syncTimer);
|
|
350
|
+
this._syncTimer = null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Manually sync pending operations
|
|
356
|
+
*/
|
|
357
|
+
async syncPending() {
|
|
358
|
+
const ops = this.store.getPendingOperations();
|
|
359
|
+
|
|
360
|
+
for (const op of ops) {
|
|
361
|
+
if (op.retries >= this.maxRetries) {
|
|
362
|
+
await this.store.removeOperation(op.id);
|
|
363
|
+
if (this.onSyncError) {
|
|
364
|
+
this.onSyncError(op, new Error('Max retries exceeded'));
|
|
365
|
+
}
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
if (op.type === 'put') {
|
|
371
|
+
await this._syncPut(op);
|
|
372
|
+
} else if (op.type === 'delete') {
|
|
373
|
+
await this._syncDelete(op);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
await this.store.removeOperation(op.id);
|
|
377
|
+
if (this.onSyncSuccess) {
|
|
378
|
+
this.onSyncSuccess(op);
|
|
379
|
+
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (isNetworkError(error)) {
|
|
382
|
+
await this.store.incrementRetry(op.id);
|
|
383
|
+
} else {
|
|
384
|
+
await this.store.removeOperation(op.id);
|
|
385
|
+
if (this.onSyncError) {
|
|
386
|
+
this.onSyncError(op, error);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async _syncPut(op) {
|
|
394
|
+
await this.client._request('/api/sync/put', {
|
|
395
|
+
method: 'POST',
|
|
396
|
+
body: JSON.stringify({ path: op.path, data: op.data })
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async _syncDelete(op) {
|
|
401
|
+
await this.client._request('/api/sync/delete', {
|
|
402
|
+
method: 'POST',
|
|
403
|
+
body: JSON.stringify({ path: op.path })
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Check if there are pending changes
|
|
409
|
+
*/
|
|
410
|
+
hasPendingChanges() {
|
|
411
|
+
return this.store.hasPendingOperations();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Get number of pending operations
|
|
416
|
+
*/
|
|
417
|
+
pendingCount() {
|
|
418
|
+
return this.store.getPendingOperations().length;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Get the offline store for direct access
|
|
423
|
+
*/
|
|
424
|
+
getStore() {
|
|
425
|
+
return this.store;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Create an offline-capable client
|
|
431
|
+
*/
|
|
432
|
+
export function createOfflineClient(baseClient, options = {}) {
|
|
433
|
+
return new OfflineSyncVault(baseClient, options);
|
|
434
|
+
}
|