@syncvault/sdk 1.1.0 → 1.3.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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncvault/sdk",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "SyncVault SDK - Zero-knowledge encrypted sync for Node.js and browsers",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.d.ts CHANGED
@@ -35,6 +35,20 @@ export interface QuotaInfo {
35
35
  unlimited: boolean;
36
36
  }
37
37
 
38
+ export interface SharedVault {
39
+ id: string;
40
+ name: string;
41
+ ownerId: string;
42
+ ownerUsername: string;
43
+ memberCount: number;
44
+ isOwner: boolean;
45
+ createdAt: string;
46
+ }
47
+
48
+ export interface PutOptions {
49
+ updatedAt?: number | Date;
50
+ }
51
+
38
52
  export declare class SyncVault {
39
53
  constructor(options: SyncVaultOptions);
40
54
 
@@ -48,7 +62,7 @@ export declare class SyncVault {
48
62
  register(username: string, password: string): Promise<User>;
49
63
 
50
64
  // Data operations
51
- put<T = unknown>(path: string, data: T): Promise<PutResponse>;
65
+ put<T = unknown>(path: string, data: T, options?: PutOptions): Promise<PutResponse>;
52
66
  get<T = unknown>(path: string): Promise<T>;
53
67
  list(): Promise<FileInfo[]>;
54
68
  delete(path: string): Promise<DeleteResponse>;
@@ -64,6 +78,13 @@ export declare class SyncVault {
64
78
  // Quota info
65
79
  getQuota(): Promise<QuotaInfo>;
66
80
 
81
+ // Shared vaults
82
+ getSharedVaults(): Promise<SharedVault[]>;
83
+ listShared(vaultId: string): Promise<FileInfo[]>;
84
+ putShared<T = unknown>(vaultId: string, path: string, data: T, sharedPassword?: string): Promise<PutResponse>;
85
+ getShared<T = unknown>(vaultId: string, path: string, sharedPassword?: string): Promise<T>;
86
+ deleteShared(vaultId: string, path: string): Promise<DeleteResponse>;
87
+
67
88
  // State
68
89
  isAuthenticated(): boolean;
69
90
  logout(): void;
@@ -72,3 +93,91 @@ export declare class SyncVault {
72
93
 
73
94
  export declare function encrypt(data: unknown, password: string): Promise<string>;
74
95
  export declare function decrypt<T = unknown>(encryptedBase64: string, password: string): Promise<T>;
96
+
97
+ // Offline sync types
98
+ export interface PendingOperation {
99
+ id: string;
100
+ type: 'put' | 'delete';
101
+ path: string;
102
+ data?: string;
103
+ updatedAt?: number;
104
+ createdAt: number;
105
+ retries: number;
106
+ }
107
+
108
+ export interface CacheEntry {
109
+ path: string;
110
+ data: string;
111
+ updatedAt: number;
112
+ }
113
+
114
+ export interface OfflineStorage {
115
+ get(key: string): Promise<unknown | null>;
116
+ set(key: string, value: unknown): Promise<void>;
117
+ remove(key: string): Promise<void>;
118
+ }
119
+
120
+ export interface OfflineOptions {
121
+ storage?: OfflineStorage;
122
+ retryInterval?: number;
123
+ maxRetries?: number;
124
+ autoSync?: boolean;
125
+ }
126
+
127
+ export declare class OfflineStore {
128
+ constructor(storage?: OfflineStorage | null);
129
+ load(): Promise<void>;
130
+ persist(): Promise<void>;
131
+ getCached(path: string): CacheEntry | null;
132
+ setCache(path: string, data: string): Promise<void>;
133
+ removeCache(path: string): Promise<void>;
134
+ queueOperation(op: Partial<PendingOperation>): Promise<void>;
135
+ getPendingOperations(): PendingOperation[];
136
+ removeOperation(id: string): Promise<void>;
137
+ incrementRetry(id: string): Promise<void>;
138
+ hasPendingOperations(): boolean;
139
+ clearQueue(): Promise<void>;
140
+ clearCache(): Promise<void>;
141
+ }
142
+
143
+ export declare class OfflineSyncVault {
144
+ constructor(baseClient: SyncVault, options?: OfflineOptions);
145
+
146
+ onSyncSuccess: ((op: PendingOperation) => void) | null;
147
+ onSyncError: ((op: PendingOperation, error: Error) => void) | null;
148
+
149
+ init(): Promise<void>;
150
+
151
+ // Auth (proxied to base client)
152
+ auth(username: string, password: string): Promise<User>;
153
+ register(username: string, password: string): Promise<User>;
154
+ setAuth(token: string, password: string): void;
155
+ getAuthUrl(state?: string): string;
156
+ exchangeCode(code: string, password: string): Promise<User>;
157
+ isAuthenticated(): boolean;
158
+ logout(): void;
159
+
160
+ // Data operations with offline support
161
+ put<T = unknown>(path: string, data: T): Promise<PutResponse | { queued: boolean; path: string }>;
162
+ get<T = unknown>(path: string): Promise<T>;
163
+ delete(path: string): Promise<DeleteResponse | { queued: boolean; path: string }>;
164
+ list(): Promise<FileInfo[]>;
165
+
166
+ // Metadata/entitlements (no offline caching)
167
+ getMetadata<T extends Metadata = Metadata>(): Promise<T>;
168
+ setMetadata<T extends Metadata = Metadata>(metadata: T): Promise<T>;
169
+ updateMetadata<T extends Metadata = Metadata>(metadata: Partial<T>): Promise<T>;
170
+ getEntitlements<T extends Entitlements = Entitlements>(): Promise<T>;
171
+ getQuota(): Promise<QuotaInfo>;
172
+ getUser(): Promise<User>;
173
+
174
+ // Sync control
175
+ startAutoSync(): void;
176
+ stopAutoSync(): void;
177
+ syncPending(): Promise<void>;
178
+ hasPendingChanges(): boolean;
179
+ pendingCount(): number;
180
+ getStore(): OfflineStore;
181
+ }
182
+
183
+ export declare function createOfflineClient(baseClient: SyncVault, options?: OfflineOptions): OfflineSyncVault;
package/src/index.js CHANGED
@@ -107,15 +107,24 @@ export class SyncVault {
107
107
 
108
108
  /**
109
109
  * Store encrypted data
110
+ * @param {string} path - File path
111
+ * @param {any} data - Data to encrypt and store
112
+ * @param {Object} options - Optional settings
113
+ * @param {number} options.updatedAt - Timestamp for LWW conflict resolution
110
114
  */
111
- async put(path, data) {
115
+ async put(path, data, options = {}) {
112
116
  this._checkAuth();
113
117
 
114
118
  const encrypted = await encrypt(data, this.password);
119
+ const body = { path, data: encrypted };
120
+
121
+ if (options.updatedAt) {
122
+ body.updatedAt = new Date(options.updatedAt).toISOString();
123
+ }
115
124
 
116
125
  const response = await this._request('/api/sync/put', {
117
126
  method: 'POST',
118
- body: JSON.stringify({ path, data: encrypted })
127
+ body: JSON.stringify(body)
119
128
  });
120
129
 
121
130
  return response;
@@ -215,6 +224,63 @@ export class SyncVault {
215
224
  return this._request('/api/sync/quota');
216
225
  }
217
226
 
227
+ // --- Shared Vaults ---
228
+
229
+ /**
230
+ * Get all shared vaults the user has access to in this app
231
+ */
232
+ async getSharedVaults() {
233
+ this._checkAuth();
234
+
235
+ return this._request('/api/sync/shared/vaults');
236
+ }
237
+
238
+ /**
239
+ * List files in a shared vault
240
+ */
241
+ async listShared(vaultId) {
242
+ this._checkAuth();
243
+
244
+ const response = await this._request(`/api/sync/shared/${vaultId}/list`);
245
+ return response.files;
246
+ }
247
+
248
+ /**
249
+ * Store encrypted data in a shared vault
250
+ */
251
+ async putShared(vaultId, path, data, sharedPassword) {
252
+ this._checkAuth();
253
+
254
+ const encrypted = await encrypt(data, sharedPassword || this.password);
255
+
256
+ return this._request(`/api/sync/shared/${vaultId}/put`, {
257
+ method: 'POST',
258
+ body: JSON.stringify({ path, data: encrypted })
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Retrieve and decrypt data from a shared vault
264
+ */
265
+ async getShared(vaultId, path, sharedPassword) {
266
+ this._checkAuth();
267
+
268
+ const response = await this._request(`/api/sync/shared/${vaultId}/get?path=${encodeURIComponent(path)}`);
269
+ return decrypt(response.data, sharedPassword || this.password);
270
+ }
271
+
272
+ /**
273
+ * Delete a file from a shared vault
274
+ */
275
+ async deleteShared(vaultId, path) {
276
+ this._checkAuth();
277
+
278
+ return this._request(`/api/sync/shared/${vaultId}/delete`, {
279
+ method: 'POST',
280
+ body: JSON.stringify({ path })
281
+ });
282
+ }
283
+
218
284
  /**
219
285
  * Check if user is authenticated
220
286
  */
@@ -280,3 +346,4 @@ export class SyncVault {
280
346
  }
281
347
 
282
348
  export { encrypt, decrypt } from './crypto.js';
349
+ export { OfflineSyncVault, OfflineStore, createOfflineClient } from './offline.js';
package/src/offline.js ADDED
@@ -0,0 +1,457 @@
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 (LWW enabled)
223
+ */
224
+ async put(path, data) {
225
+ await this.init();
226
+
227
+ const timestamp = Date.now();
228
+ const encrypted = await encrypt(data, this.client.password);
229
+
230
+ try {
231
+ const result = await this.client.put(path, data, { updatedAt: timestamp });
232
+ await this.store.setCache(path, encrypted);
233
+ return result;
234
+ } catch (error) {
235
+ if (isNetworkError(error)) {
236
+ await this.store.setCache(path, encrypted);
237
+ await this.store.queueOperation({
238
+ type: 'put',
239
+ path,
240
+ data: encrypted,
241
+ updatedAt: timestamp
242
+ });
243
+ return { queued: true, path };
244
+ }
245
+ throw error;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Get with offline fallback
251
+ */
252
+ async get(path) {
253
+ await this.init();
254
+
255
+ try {
256
+ const result = await this.client.get(path);
257
+ // Update cache with fresh data
258
+ const encrypted = await encrypt(result, this.client.password);
259
+ await this.store.setCache(path, encrypted);
260
+ return result;
261
+ } catch (error) {
262
+ if (isNetworkError(error)) {
263
+ const cached = this.store.getCached(path);
264
+ if (cached) {
265
+ return decrypt(cached.data, this.client.password);
266
+ }
267
+ throw new Error('Offline and no cached data available');
268
+ }
269
+ throw error;
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Delete with offline support
275
+ */
276
+ async delete(path) {
277
+ await this.init();
278
+
279
+ try {
280
+ const result = await this.client.delete(path);
281
+ await this.store.removeCache(path);
282
+ return result;
283
+ } catch (error) {
284
+ if (isNetworkError(error)) {
285
+ await this.store.removeCache(path);
286
+ await this.store.queueOperation({
287
+ type: 'delete',
288
+ path
289
+ });
290
+ return { queued: true, path };
291
+ }
292
+ throw error;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * List files (no offline caching for list)
298
+ */
299
+ async list() {
300
+ return this.client.list();
301
+ }
302
+
303
+ /**
304
+ * Proxy metadata/entitlements (no offline for these)
305
+ */
306
+ async getMetadata() {
307
+ return this.client.getMetadata();
308
+ }
309
+
310
+ async setMetadata(metadata) {
311
+ return this.client.setMetadata(metadata);
312
+ }
313
+
314
+ async updateMetadata(metadata) {
315
+ return this.client.updateMetadata(metadata);
316
+ }
317
+
318
+ async getEntitlements() {
319
+ return this.client.getEntitlements();
320
+ }
321
+
322
+ async getQuota() {
323
+ return this.client.getQuota();
324
+ }
325
+
326
+ async getUser() {
327
+ return this.client.getUser();
328
+ }
329
+
330
+ /**
331
+ * Start automatic sync of pending operations
332
+ */
333
+ startAutoSync() {
334
+ if (this._syncTimer) return;
335
+
336
+ this._syncTimer = setInterval(() => {
337
+ this.syncPending();
338
+ }, this.retryInterval);
339
+
340
+ // Also listen for online event in browser
341
+ if (typeof window !== 'undefined') {
342
+ window.addEventListener('online', () => this.syncPending());
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Stop automatic sync
348
+ */
349
+ stopAutoSync() {
350
+ if (this._syncTimer) {
351
+ clearInterval(this._syncTimer);
352
+ this._syncTimer = null;
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Manually sync pending operations
358
+ */
359
+ async syncPending() {
360
+ const ops = this.store.getPendingOperations();
361
+
362
+ for (const op of ops) {
363
+ if (op.retries >= this.maxRetries) {
364
+ await this.store.removeOperation(op.id);
365
+ if (this.onSyncError) {
366
+ this.onSyncError(op, new Error('Max retries exceeded'));
367
+ }
368
+ continue;
369
+ }
370
+
371
+ try {
372
+ if (op.type === 'put') {
373
+ await this._syncPut(op);
374
+ } else if (op.type === 'delete') {
375
+ await this._syncDelete(op);
376
+ }
377
+
378
+ await this.store.removeOperation(op.id);
379
+ if (this.onSyncSuccess) {
380
+ this.onSyncSuccess(op);
381
+ }
382
+ } catch (error) {
383
+ if (isNetworkError(error)) {
384
+ await this.store.incrementRetry(op.id);
385
+ } else {
386
+ await this.store.removeOperation(op.id);
387
+ if (this.onSyncError) {
388
+ this.onSyncError(op, error);
389
+ }
390
+ }
391
+ }
392
+ }
393
+ }
394
+
395
+ async _syncPut(op) {
396
+ const body = { path: op.path, data: op.data };
397
+ if (op.updatedAt) {
398
+ body.updatedAt = new Date(op.updatedAt).toISOString();
399
+ }
400
+
401
+ try {
402
+ await this.client._request('/api/sync/put', {
403
+ method: 'POST',
404
+ body: JSON.stringify(body)
405
+ });
406
+ } catch (error) {
407
+ // LWW conflict - server has newer data, discard local change
408
+ if (error.statusCode === 409 && error.data?.code === 'CONFLICT_STALE') {
409
+ // Fetch fresh data from server to update cache
410
+ try {
411
+ const result = await this.client.get(op.path);
412
+ const encrypted = await encrypt(result, this.client.password);
413
+ await this.store.setCache(op.path, encrypted);
414
+ } catch (cacheErr) {
415
+ console.warn('Failed to update cache after LWW conflict:', cacheErr.message);
416
+ }
417
+ return; // Consider this "synced" - we accepted server's version
418
+ }
419
+ throw error;
420
+ }
421
+ }
422
+
423
+ async _syncDelete(op) {
424
+ await this.client._request('/api/sync/delete', {
425
+ method: 'POST',
426
+ body: JSON.stringify({ path: op.path })
427
+ });
428
+ }
429
+
430
+ /**
431
+ * Check if there are pending changes
432
+ */
433
+ hasPendingChanges() {
434
+ return this.store.hasPendingOperations();
435
+ }
436
+
437
+ /**
438
+ * Get number of pending operations
439
+ */
440
+ pendingCount() {
441
+ return this.store.getPendingOperations().length;
442
+ }
443
+
444
+ /**
445
+ * Get the offline store for direct access
446
+ */
447
+ getStore() {
448
+ return this.store;
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Create an offline-capable client
454
+ */
455
+ export function createOfflineClient(baseClient, options = {}) {
456
+ return new OfflineSyncVault(baseClient, options);
457
+ }