@pol-studios/powersync 1.0.0 → 1.0.2

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.
Files changed (51) hide show
  1. package/dist/attachments/index.js +1 -1
  2. package/dist/{chunk-4FJVBR3X.js → chunk-3AYXHQ4W.js} +24 -13
  3. package/dist/chunk-3AYXHQ4W.js.map +1 -0
  4. package/dist/{chunk-BJ36QDFN.js → chunk-7EMDVIZX.js} +1 -1
  5. package/dist/chunk-7EMDVIZX.js.map +1 -0
  6. package/dist/chunk-C2RSTGDC.js +726 -0
  7. package/dist/chunk-C2RSTGDC.js.map +1 -0
  8. package/dist/{chunk-NPNBGCRC.js → chunk-EJ23MXPQ.js} +1 -1
  9. package/dist/{chunk-NPNBGCRC.js.map → chunk-EJ23MXPQ.js.map} +1 -1
  10. package/dist/{chunk-CHRTN5PF.js → chunk-FPTDATY5.js} +1 -1
  11. package/dist/chunk-FPTDATY5.js.map +1 -0
  12. package/dist/chunk-GMFDCVMZ.js +1285 -0
  13. package/dist/chunk-GMFDCVMZ.js.map +1 -0
  14. package/dist/chunk-OLHGI472.js +1 -0
  15. package/dist/{chunk-CFCK2LHI.js → chunk-OTJXIRWX.js} +45 -40
  16. package/dist/chunk-OTJXIRWX.js.map +1 -0
  17. package/dist/{chunk-GBGATW2S.js → chunk-V6LJ6MR2.js} +86 -95
  18. package/dist/chunk-V6LJ6MR2.js.map +1 -0
  19. package/dist/chunk-VJCL2SWD.js +1 -0
  20. package/dist/chunk-VJCL2SWD.js.map +1 -0
  21. package/dist/connector/index.d.ts +1 -2
  22. package/dist/connector/index.js +3 -6
  23. package/dist/core/index.js +2 -2
  24. package/dist/{supabase-connector-D14-kl5v.d.ts → index-Cb-NI0Ct.d.ts} +159 -2
  25. package/dist/index.d.ts +2 -3
  26. package/dist/index.js +12 -13
  27. package/dist/index.native.d.ts +1 -2
  28. package/dist/index.native.js +13 -14
  29. package/dist/index.web.d.ts +1 -2
  30. package/dist/index.web.js +13 -14
  31. package/dist/platform/index.js.map +1 -1
  32. package/dist/platform/index.native.js +1 -1
  33. package/dist/platform/index.web.js +1 -1
  34. package/dist/provider/index.d.ts +6 -1
  35. package/dist/provider/index.js +6 -6
  36. package/dist/sync/index.js +3 -3
  37. package/package.json +35 -10
  38. package/dist/chunk-4FJVBR3X.js.map +0 -1
  39. package/dist/chunk-7BPTGEVG.js +0 -1
  40. package/dist/chunk-BJ36QDFN.js.map +0 -1
  41. package/dist/chunk-CFCK2LHI.js.map +0 -1
  42. package/dist/chunk-CHRTN5PF.js.map +0 -1
  43. package/dist/chunk-FLHDT4TS.js +0 -327
  44. package/dist/chunk-FLHDT4TS.js.map +0 -1
  45. package/dist/chunk-GBGATW2S.js.map +0 -1
  46. package/dist/chunk-Q3LFFMRR.js +0 -925
  47. package/dist/chunk-Q3LFFMRR.js.map +0 -1
  48. package/dist/chunk-T225XEML.js +0 -298
  49. package/dist/chunk-T225XEML.js.map +0 -1
  50. package/dist/index-nae7nzib.d.ts +0 -147
  51. /package/dist/{chunk-7BPTGEVG.js.map → chunk-OLHGI472.js.map} +0 -0
@@ -1,925 +0,0 @@
1
- import {
2
- AttachmentQueue
3
- } from "./chunk-GBGATW2S.js";
4
- import {
5
- DEFAULT_CONNECTION_HEALTH,
6
- DEFAULT_SYNC_METRICS,
7
- DEFAULT_SYNC_STATUS,
8
- HealthMonitor,
9
- MetricsCollector,
10
- SyncStatusTracker
11
- } from "./chunk-CFCK2LHI.js";
12
- import {
13
- SupabaseConnector
14
- } from "./chunk-FLHDT4TS.js";
15
- import {
16
- createSyncError,
17
- extractEntityIds,
18
- extractTableNames
19
- } from "./chunk-CHRTN5PF.js";
20
-
21
- // src/provider/context.ts
22
- import { createContext } from "react";
23
- var PowerSyncContext = createContext(null);
24
- PowerSyncContext.displayName = "PowerSyncContext";
25
- var SyncStatusContext = createContext(null);
26
- SyncStatusContext.displayName = "SyncStatusContext";
27
- var ConnectionHealthContext = createContext(null);
28
- ConnectionHealthContext.displayName = "ConnectionHealthContext";
29
- var SyncMetricsContext = createContext(null);
30
- SyncMetricsContext.displayName = "SyncMetricsContext";
31
- var AttachmentQueueContext = createContext(null);
32
- AttachmentQueueContext.displayName = "AttachmentQueueContext";
33
-
34
- // src/provider/PowerSyncProvider.tsx
35
- import {
36
- useEffect,
37
- useState,
38
- useRef,
39
- useMemo,
40
- useCallback
41
- } from "react";
42
- import { jsx } from "react/jsx-runtime";
43
- function PowerSyncProvider({
44
- config,
45
- children,
46
- onReady,
47
- onError,
48
- onSyncStatusChange
49
- }) {
50
- const {
51
- platform,
52
- schema,
53
- powerSyncUrl,
54
- supabaseClient,
55
- dbFilename = "powersync.db",
56
- connector: connectorConfig,
57
- attachments: attachmentConfig,
58
- sync: syncConfig
59
- } = config;
60
- const logger = platform.logger;
61
- const mergedSyncConfig = {
62
- autoConnect: syncConfig?.autoConnect ?? true,
63
- syncInterval: syncConfig?.syncInterval ?? 0,
64
- enableHealthMonitoring: syncConfig?.enableHealthMonitoring ?? true,
65
- enableMetrics: syncConfig?.enableMetrics ?? true
66
- };
67
- const [db, setDb] = useState(null);
68
- const [connector, setConnector] = useState(null);
69
- const [attachmentQueue, setAttachmentQueue] = useState(null);
70
- const [isReady, setIsReady] = useState(false);
71
- const [isInitializing, setIsInitializing] = useState(true);
72
- const [error, setError] = useState(null);
73
- const [session, setSession] = useState(null);
74
- const [syncStatus, setSyncStatus] = useState(DEFAULT_SYNC_STATUS);
75
- const [pendingMutations, setPendingMutations] = useState([]);
76
- const [syncModeState, setSyncModeState] = useState({
77
- loaded: false,
78
- mode: "push-pull"
79
- });
80
- const [lastSyncedAt, setLastSyncedAt] = useState(null);
81
- const [connectionError, setConnectionError] = useState(null);
82
- const [failedTransactions, setFailedTransactions] = useState([]);
83
- const [completedTransactions, setCompletedTransactions] = useState([]);
84
- const [connectionHealth, setConnectionHealth] = useState(DEFAULT_CONNECTION_HEALTH);
85
- const [syncMetrics, setSyncMetrics] = useState(DEFAULT_SYNC_METRICS);
86
- const statusTrackerRef = useRef(null);
87
- const metricsCollectorRef = useRef(null);
88
- const healthMonitorRef = useRef(null);
89
- const attachmentQueueRef = useRef(null);
90
- const listenerUnsubscribeRef = useRef(null);
91
- const wasSyncingRef = useRef(false);
92
- const initializingRef = useRef(false);
93
- const dbClosedRef = useRef(false);
94
- useEffect(() => {
95
- const statusTracker = new SyncStatusTracker(
96
- platform.storage,
97
- logger,
98
- {
99
- onStatusChange: (status) => {
100
- setSyncStatus(status);
101
- setLastSyncedAt(status.lastSyncedAt);
102
- onSyncStatusChange?.(status);
103
- }
104
- }
105
- );
106
- statusTrackerRef.current = statusTracker;
107
- const metricsCollector = new MetricsCollector(
108
- platform.storage,
109
- logger,
110
- {
111
- onMetricsChange: setSyncMetrics
112
- }
113
- );
114
- metricsCollectorRef.current = metricsCollector;
115
- const healthMonitor = new HealthMonitor(
116
- logger,
117
- {
118
- onHealthChange: setConnectionHealth
119
- }
120
- );
121
- healthMonitorRef.current = healthMonitor;
122
- const initPromise = Promise.all([
123
- statusTracker.init(),
124
- metricsCollector.init()
125
- ]);
126
- const timeoutPromise = new Promise((resolve) => {
127
- setTimeout(() => {
128
- logger.warn("[PowerSyncProvider] Sync mode state load timed out, using default (push-pull)");
129
- resolve([void 0, void 0]);
130
- }, 5e3);
131
- });
132
- Promise.race([initPromise, timeoutPromise]).then(() => {
133
- logger.info("[PowerSyncProvider] Sync mode state loaded:", {
134
- mode: statusTracker.getSyncMode()
135
- });
136
- setSyncModeState({ loaded: true, mode: statusTracker.getSyncMode() });
137
- });
138
- return () => {
139
- statusTracker.dispose();
140
- metricsCollector.dispose();
141
- healthMonitor.dispose();
142
- };
143
- }, [platform, logger, onSyncStatusChange]);
144
- useEffect(() => {
145
- logger.debug("[PowerSyncProvider] Setting up auth listener");
146
- supabaseClient.auth.getSession().then(({ data: { session: initialSession } }) => {
147
- logger.debug("[PowerSyncProvider] Initial session:", !!initialSession);
148
- setSession(initialSession);
149
- });
150
- const { data: { subscription } } = supabaseClient.auth.onAuthStateChange(
151
- (_event, newSession) => {
152
- logger.debug("[PowerSyncProvider] Auth state changed, hasSession:", !!newSession);
153
- setSession(newSession);
154
- }
155
- );
156
- return () => {
157
- subscription.unsubscribe();
158
- };
159
- }, [supabaseClient, logger]);
160
- useEffect(() => {
161
- if (initializingRef.current) {
162
- logger.debug("[PowerSyncProvider] Already initializing, skipping...");
163
- return;
164
- }
165
- initializingRef.current = true;
166
- const controller = { cancelled: false };
167
- const initDatabase = async () => {
168
- try {
169
- logger.info("[PowerSyncProvider] Initializing database...");
170
- const database = await platform.createDatabase({
171
- dbFilename,
172
- schema
173
- });
174
- if (controller.cancelled) {
175
- logger.debug("[PowerSyncProvider] Init cancelled, closing database...");
176
- await database.close();
177
- initializingRef.current = false;
178
- return;
179
- }
180
- logger.info("[PowerSyncProvider] Database initialized");
181
- setDb(database);
182
- setIsReady(true);
183
- setIsInitializing(false);
184
- healthMonitorRef.current?.setDatabase(database);
185
- if (mergedSyncConfig.enableHealthMonitoring) {
186
- healthMonitorRef.current?.start();
187
- }
188
- onReady?.();
189
- } catch (err) {
190
- const initError = err instanceof Error ? err : new Error(String(err));
191
- logger.error("[PowerSyncProvider] Initialization failed:", initError);
192
- if (!controller.cancelled) {
193
- setError(initError);
194
- setIsInitializing(false);
195
- onError?.(initError);
196
- }
197
- initializingRef.current = false;
198
- }
199
- };
200
- initDatabase();
201
- return () => {
202
- controller.cancelled = true;
203
- initializingRef.current = false;
204
- };
205
- }, [platform, dbFilename, schema, logger, mergedSyncConfig.enableHealthMonitoring, onReady, onError]);
206
- useEffect(() => {
207
- if (__DEV__) {
208
- console.log("[PowerSyncProvider] Connect effect triggered:", {
209
- hasDb: !!db,
210
- hasSession: !!session,
211
- autoConnect: mergedSyncConfig.autoConnect,
212
- syncModeState
213
- });
214
- }
215
- logger.info("[PowerSyncProvider] Connect effect - db:", !!db, "session:", !!session, "autoConnect:", mergedSyncConfig.autoConnect, "syncModeLoaded:", syncModeState.loaded, "syncMode:", syncModeState.mode);
216
- if (!db) {
217
- logger.debug("[PowerSyncProvider] Connect effect - waiting for db");
218
- return;
219
- }
220
- if (!session) {
221
- logger.debug("[PowerSyncProvider] Connect effect - no session");
222
- return;
223
- }
224
- if (!mergedSyncConfig.autoConnect) {
225
- logger.debug("[PowerSyncProvider] Connect effect - autoConnect disabled");
226
- return;
227
- }
228
- if (!syncModeState.loaded) {
229
- logger.info("[PowerSyncProvider] Waiting for sync mode state to load...");
230
- return;
231
- }
232
- if (syncModeState.mode === "offline") {
233
- logger.debug("[PowerSyncProvider] Skipping connect - offline mode");
234
- return;
235
- }
236
- const connectPowerSync = async () => {
237
- try {
238
- setConnectionError(null);
239
- const statusTracker = statusTrackerRef.current;
240
- const newConnector = new SupabaseConnector({
241
- supabaseClient,
242
- powerSyncUrl,
243
- schemaRouter: connectorConfig?.schemaRouter,
244
- crudHandler: connectorConfig?.crudHandler,
245
- logger,
246
- // Check if uploads should be performed based on sync mode
247
- shouldUpload: () => statusTrackerRef.current?.shouldUpload() ?? true,
248
- // Clear failures when transaction succeeds
249
- onTransactionSuccess: (entries) => {
250
- if (!statusTracker) return;
251
- const entityIds = extractEntityIds(entries);
252
- entityIds.forEach((id) => {
253
- const failures = statusTracker.getFailuresForEntity(id);
254
- failures.forEach((f) => statusTracker.clearFailure(f.id));
255
- });
256
- setFailedTransactions(statusTracker.getFailedTransactions());
257
- },
258
- // Record failures when transaction fails
259
- onTransactionFailure: (entries, error2, classified) => {
260
- if (!statusTracker) return;
261
- statusTracker.recordTransactionFailure(
262
- entries,
263
- createSyncError(classified, error2.message),
264
- classified.isPermanent,
265
- extractEntityIds(entries),
266
- extractTableNames(entries)
267
- );
268
- setFailedTransactions(statusTracker.getFailedTransactions());
269
- },
270
- // Record completed transactions
271
- onTransactionComplete: (entries) => {
272
- if (!statusTracker) return;
273
- statusTracker.recordTransactionComplete(entries);
274
- setCompletedTransactions(statusTracker.getCompletedTransactions());
275
- }
276
- });
277
- setConnector(newConnector);
278
- if (db.connected) {
279
- logger.debug("[PowerSyncProvider] Already connected, reconnecting...");
280
- await db.disconnect();
281
- }
282
- logger.info("[PowerSyncProvider] Connecting to PowerSync...");
283
- await db.connect(newConnector);
284
- logger.info("[PowerSyncProvider] Connected successfully");
285
- healthMonitorRef.current?.resetReconnectAttempts();
286
- } catch (err) {
287
- const connectError = err instanceof Error ? err : new Error(String(err));
288
- logger.error("[PowerSyncProvider] Connection failed:", connectError);
289
- setConnectionError(connectError);
290
- healthMonitorRef.current?.recordReconnectAttempt();
291
- }
292
- };
293
- connectPowerSync();
294
- }, [db, session, powerSyncUrl, supabaseClient, connectorConfig, mergedSyncConfig.autoConnect, syncModeState, logger]);
295
- useEffect(() => {
296
- if (!db) return;
297
- const initialStatus = db.currentStatus;
298
- if (initialStatus) {
299
- statusTrackerRef.current?.handleStatusChange(initialStatus);
300
- }
301
- const unsubscribe = db.registerListener({
302
- statusChanged: (status) => {
303
- statusTrackerRef.current?.handleStatusChange(status);
304
- const dataFlow = status.dataFlowStatus;
305
- const isDownloading = dataFlow?.downloading ?? false;
306
- const progress = status.downloadProgress;
307
- if (isDownloading && !wasSyncingRef.current) {
308
- metricsCollectorRef.current?.markSyncStart();
309
- }
310
- if (!isDownloading && wasSyncingRef.current) {
311
- const duration = metricsCollectorRef.current?.markSyncEnd();
312
- if (duration !== null && duration !== void 0) {
313
- metricsCollectorRef.current?.recordSync({
314
- durationMs: duration,
315
- success: true,
316
- operationsDownloaded: progress?.totalOperations ?? 0
317
- });
318
- }
319
- }
320
- wasSyncingRef.current = isDownloading;
321
- }
322
- });
323
- listenerUnsubscribeRef.current = unsubscribe;
324
- return () => {
325
- unsubscribe();
326
- listenerUnsubscribeRef.current = null;
327
- };
328
- }, [db]);
329
- const prevPendingCountRef = useRef(0);
330
- const parseCrudRow = useCallback((row) => {
331
- try {
332
- const parsed = JSON.parse(row.data);
333
- const opMap = {
334
- "PUT": "PUT",
335
- "PATCH": "PATCH",
336
- "DELETE": "DELETE"
337
- };
338
- const op = opMap[parsed.op] ?? "PUT";
339
- const entityId = parsed.data?.id ?? parsed.id ?? row.id;
340
- return {
341
- id: entityId,
342
- clientId: parseInt(row.id, 10) || 0,
343
- op,
344
- table: parsed.type ?? "unknown",
345
- opData: parsed.data ?? void 0,
346
- // Ensure null becomes undefined
347
- transactionId: row.tx_id ?? void 0
348
- };
349
- } catch (e) {
350
- logger.warn("[PowerSyncProvider] Failed to parse CRUD entry:", e, row);
351
- return null;
352
- }
353
- }, [logger]);
354
- useEffect(() => {
355
- if (!db) return;
356
- const abortController = new AbortController();
357
- db.watch(
358
- "SELECT COUNT(*) as count FROM ps_crud",
359
- [],
360
- {
361
- onResult: async (results) => {
362
- if (dbClosedRef.current) {
363
- logger.debug("[PowerSyncProvider] Skipping watch callback - database is closed");
364
- return;
365
- }
366
- const count = results.rows?._array?.[0]?.count ?? 0;
367
- const prevCount = prevPendingCountRef.current;
368
- prevPendingCountRef.current = count;
369
- let mutations = [];
370
- if (count > 0) {
371
- try {
372
- if (dbClosedRef.current) {
373
- logger.debug("[PowerSyncProvider] Skipping getAll - database is closed");
374
- return;
375
- }
376
- const rows = await db.getAll(
377
- "SELECT id, tx_id, data FROM ps_crud ORDER BY id ASC"
378
- );
379
- mutations = rows.map(parseCrudRow).filter((entry) => entry !== null);
380
- } catch (e) {
381
- const errorMessage = e instanceof Error ? e.message : String(e);
382
- if (errorMessage.includes("not open") || errorMessage.includes("closed")) {
383
- logger.debug("[PowerSyncProvider] Database closed during CRUD fetch, ignoring");
384
- return;
385
- }
386
- logger.warn("[PowerSyncProvider] Failed to fetch CRUD entries:", e);
387
- }
388
- }
389
- if (dbClosedRef.current) {
390
- return;
391
- }
392
- statusTrackerRef.current?.updatePendingMutations(mutations);
393
- setPendingMutations(mutations);
394
- if (prevCount > 0 && count === 0) {
395
- logger.debug("[PowerSyncProvider] All pending uploads complete, clearing force flag");
396
- statusTrackerRef.current?.clearForceNextUpload();
397
- }
398
- },
399
- onError: (error2) => {
400
- const errorMessage = error2.message ?? "";
401
- if (errorMessage.includes("not open") || errorMessage.includes("closed") || dbClosedRef.current) {
402
- logger.debug("[PowerSyncProvider] Watch error during database close, ignoring");
403
- return;
404
- }
405
- logger.warn("[PowerSyncProvider] Error watching ps_crud:", error2);
406
- }
407
- },
408
- {
409
- signal: abortController.signal,
410
- throttleMs: 100
411
- // Debounce to avoid excessive updates during bulk operations
412
- }
413
- );
414
- return () => {
415
- abortController.abort();
416
- };
417
- }, [db, logger, parseCrudRow]);
418
- useEffect(() => {
419
- if (!db || !attachmentConfig || attachmentQueueRef.current) {
420
- return;
421
- }
422
- const initAttachmentQueue = async () => {
423
- try {
424
- logger.info("[PowerSyncProvider] Initializing attachment queue...");
425
- const queue = new AttachmentQueue({
426
- powersync: db,
427
- platform,
428
- config: attachmentConfig
429
- });
430
- await queue.init();
431
- attachmentQueueRef.current = queue;
432
- setAttachmentQueue(queue);
433
- logger.info("[PowerSyncProvider] Attachment queue initialized");
434
- } catch (err) {
435
- logger.error("[PowerSyncProvider] Attachment queue initialization failed:", err);
436
- }
437
- };
438
- initAttachmentQueue();
439
- return () => {
440
- attachmentQueueRef.current?.dispose();
441
- attachmentQueueRef.current = null;
442
- };
443
- }, [db, attachmentConfig, platform, logger]);
444
- useEffect(() => {
445
- if (db) {
446
- dbClosedRef.current = false;
447
- }
448
- return () => {
449
- logger.info("[PowerSyncProvider] Cleaning up...");
450
- dbClosedRef.current = true;
451
- listenerUnsubscribeRef.current?.();
452
- attachmentQueueRef.current?.dispose();
453
- healthMonitorRef.current?.stop();
454
- if (db) {
455
- (async () => {
456
- try {
457
- await db.disconnect();
458
- await db.close();
459
- } catch (err) {
460
- const errorMessage = err instanceof Error ? err.message : String(err);
461
- if (!errorMessage.includes("not open") && !errorMessage.includes("closed")) {
462
- logger.warn("[PowerSyncProvider] Error during cleanup:", err);
463
- }
464
- }
465
- })();
466
- }
467
- };
468
- }, [db, logger]);
469
- const clearFailure = useCallback((failureId) => {
470
- const tracker = statusTrackerRef.current;
471
- if (!tracker) {
472
- logger.warn("[PowerSyncProvider] Cannot clear failure - tracker not initialized");
473
- return;
474
- }
475
- tracker.clearFailure(failureId);
476
- setFailedTransactions(tracker.getFailedTransactions());
477
- }, [logger]);
478
- const clearAllFailures = useCallback(() => {
479
- const tracker = statusTrackerRef.current;
480
- if (!tracker) {
481
- logger.warn("[PowerSyncProvider] Cannot clear failures - tracker not initialized");
482
- return;
483
- }
484
- tracker.clearAllFailures();
485
- setFailedTransactions(tracker.getFailedTransactions());
486
- }, [logger]);
487
- const clearCompletedHistory = useCallback(() => {
488
- const tracker = statusTrackerRef.current;
489
- if (!tracker) {
490
- logger.warn("[PowerSyncProvider] Cannot clear completed history - tracker not initialized");
491
- return;
492
- }
493
- tracker.clearCompletedHistory();
494
- setCompletedTransactions(tracker.getCompletedTransactions());
495
- }, [logger]);
496
- const setSyncMode = useCallback(async (mode) => {
497
- const tracker = statusTrackerRef.current;
498
- if (!tracker) {
499
- logger.warn("[PowerSyncProvider] Cannot set sync mode - tracker not initialized");
500
- return;
501
- }
502
- await tracker.setSyncMode(mode);
503
- setSyncModeState({ loaded: true, mode });
504
- }, [logger]);
505
- const setForceNextUpload = useCallback((force) => {
506
- const tracker = statusTrackerRef.current;
507
- if (!tracker) {
508
- logger.warn("[PowerSyncProvider] Cannot set force upload - tracker not initialized");
509
- return;
510
- }
511
- tracker.setForceNextUpload(force);
512
- }, [logger]);
513
- const discardPendingMutation = useCallback(async (clientId) => {
514
- if (!db || !connector) {
515
- logger.warn("[PowerSync] Cannot discard - not initialized");
516
- return;
517
- }
518
- if (syncStatus.uploading) {
519
- throw new Error("Cannot discard while upload is in progress");
520
- }
521
- logger.info("[PowerSync] Discarding pending mutation:", clientId);
522
- await db.disconnect();
523
- try {
524
- await db.execute("DELETE FROM ps_crud WHERE id = ?", [clientId]);
525
- logger.info("[PowerSync] Mutation discarded successfully");
526
- } finally {
527
- await db.connect(connector);
528
- }
529
- }, [db, connector, syncStatus.uploading, logger]);
530
- const discardAllPendingMutations = useCallback(async () => {
531
- if (!db || !connector) {
532
- logger.warn("[PowerSync] Cannot discard all - not initialized");
533
- return;
534
- }
535
- if (syncStatus.uploading) {
536
- throw new Error("Cannot discard while upload is in progress");
537
- }
538
- logger.info("[PowerSync] Discarding all pending mutations");
539
- await db.disconnect();
540
- try {
541
- await db.execute("DELETE FROM ps_crud");
542
- logger.info("[PowerSync] All mutations discarded successfully");
543
- } finally {
544
- await db.connect(connector);
545
- }
546
- }, [db, connector, syncStatus.uploading, logger]);
547
- const powerSyncContextValue = useMemo(
548
- () => ({
549
- db,
550
- connector,
551
- attachmentQueue,
552
- isReady,
553
- isInitializing,
554
- error,
555
- schema,
556
- platform
557
- }),
558
- [db, connector, attachmentQueue, isReady, isInitializing, error, schema, platform]
559
- );
560
- const syncStatusContextValue = useMemo(
561
- () => ({
562
- status: syncStatus,
563
- pendingMutations,
564
- pendingCount: pendingMutations.length,
565
- // Expose uploading/downloading directly from syncStatus for reliable activity detection
566
- isUploading: syncStatus.uploading,
567
- isDownloading: syncStatus.downloading,
568
- isPaused: syncModeState.mode === "offline",
569
- syncMode: syncModeState.mode,
570
- lastSyncedAt,
571
- // Connection error for consumers to display
572
- connectionError,
573
- // Failed transaction fields
574
- failedTransactions,
575
- hasUploadErrors: failedTransactions.length > 0,
576
- permanentErrorCount: failedTransactions.filter((f) => f.isPermanent).length,
577
- // Clear failure functions
578
- clearFailure,
579
- clearAllFailures,
580
- // Completed transaction fields
581
- completedTransactions,
582
- clearCompletedHistory,
583
- // Sync mode control functions
584
- setSyncMode,
585
- setForceNextUpload,
586
- // Discard mutation functions
587
- discardPendingMutation,
588
- discardAllPendingMutations
589
- }),
590
- [syncStatus, pendingMutations, syncModeState.mode, lastSyncedAt, connectionError, failedTransactions, clearFailure, clearAllFailures, completedTransactions, clearCompletedHistory, setSyncMode, setForceNextUpload, discardPendingMutation, discardAllPendingMutations]
591
- );
592
- const connectionHealthContextValue = useMemo(
593
- () => ({ health: connectionHealth }),
594
- [connectionHealth]
595
- );
596
- const syncMetricsContextValue = useMemo(
597
- () => ({ metrics: syncMetrics }),
598
- [syncMetrics]
599
- );
600
- return /* @__PURE__ */ jsx(PowerSyncContext.Provider, { value: powerSyncContextValue, children: /* @__PURE__ */ jsx(SyncStatusContext.Provider, { value: syncStatusContextValue, children: /* @__PURE__ */ jsx(ConnectionHealthContext.Provider, { value: connectionHealthContextValue, children: /* @__PURE__ */ jsx(SyncMetricsContext.Provider, { value: syncMetricsContextValue, children: /* @__PURE__ */ jsx(AttachmentQueueContext.Provider, { value: attachmentQueue, children }) }) }) }) });
601
- }
602
-
603
- // src/provider/hooks.ts
604
- import { useContext, useCallback as useCallback2, useMemo as useMemo2, useRef as useRef2, useState as useState2, useEffect as useEffect2 } from "react";
605
- function usePowerSync() {
606
- const context = useContext(PowerSyncContext);
607
- if (!context) {
608
- throw new Error("usePowerSync must be used within a PowerSyncProvider");
609
- }
610
- return context;
611
- }
612
- function useSyncStatus() {
613
- const context = useContext(SyncStatusContext);
614
- if (!context) {
615
- throw new Error("useSyncStatus must be used within a PowerSyncProvider");
616
- }
617
- return context;
618
- }
619
- function useSyncControl() {
620
- const { db, connector, platform } = usePowerSync();
621
- const { setSyncMode: setContextSyncMode, setForceNextUpload } = useSyncStatus();
622
- const scopeRef = useRef2(null);
623
- const setSyncMode = useCallback2(async (mode) => {
624
- await setContextSyncMode(mode);
625
- if (mode === "offline") {
626
- if (db?.connected) {
627
- platform.logger.info("[useSyncControl] Mode changed to offline - disconnecting");
628
- await db.disconnect();
629
- }
630
- } else if (db && connector && !db.connected) {
631
- platform.logger.info("[useSyncControl] Mode changed to", mode, "- reconnecting");
632
- await db.connect(connector);
633
- }
634
- }, [db, connector, platform, setContextSyncMode]);
635
- const syncNow = useCallback2(async () => {
636
- if (!db || !connector) {
637
- platform.logger.warn("[useSyncControl] Cannot sync - database not initialized");
638
- return;
639
- }
640
- setForceNextUpload(true);
641
- platform.logger.info("[useSyncControl] Sync Now triggered - forcing full sync");
642
- if (db.connected) {
643
- await db.disconnect();
644
- }
645
- await db.connect(connector);
646
- platform.logger.info("[useSyncControl] Connected, sync should start automatically");
647
- }, [db, connector, platform, setForceNextUpload]);
648
- const triggerSync = useCallback2(async () => {
649
- if (!db || !connector) {
650
- platform.logger.warn("[useSyncControl] Cannot trigger sync - not initialized");
651
- return;
652
- }
653
- await setContextSyncMode("push-pull");
654
- if (db.connected) {
655
- platform.logger.info("[useSyncControl] Disconnecting to force fresh sync...");
656
- await db.disconnect();
657
- }
658
- platform.logger.info("[useSyncControl] Connecting...");
659
- await db.connect(connector);
660
- platform.logger.info("[useSyncControl] Connected, sync should start automatically");
661
- }, [db, connector, platform, setContextSyncMode]);
662
- const pause = useCallback2(async () => {
663
- await setSyncMode("offline");
664
- platform.logger.info("[useSyncControl] Sync paused");
665
- }, [setSyncMode, platform]);
666
- const resume = useCallback2(async () => {
667
- await setSyncMode("push-pull");
668
- platform.logger.info("[useSyncControl] Sync resumed");
669
- }, [setSyncMode, platform]);
670
- const disconnect = useCallback2(async () => {
671
- if (!db) {
672
- platform.logger.warn("[useSyncControl] Cannot disconnect - not initialized");
673
- return;
674
- }
675
- platform.logger.info("[useSyncControl] Disconnecting...");
676
- await db.disconnect();
677
- platform.logger.info("[useSyncControl] Disconnected");
678
- }, [db, platform]);
679
- const setScope = useCallback2((scope) => {
680
- scopeRef.current = scope;
681
- if (connector && scope) {
682
- connector.setActiveProjectIds(scope.ids);
683
- platform.logger.info("[useSyncControl] Scope set:", scope);
684
- }
685
- }, [connector, platform]);
686
- return useMemo2(
687
- () => ({
688
- triggerSync,
689
- syncNow,
690
- pause,
691
- resume,
692
- disconnect,
693
- setScope,
694
- setSyncMode
695
- }),
696
- [triggerSync, syncNow, pause, resume, disconnect, setScope, setSyncMode]
697
- );
698
- }
699
- function useSyncMode() {
700
- const { syncMode } = useSyncStatus();
701
- const { setSyncMode } = useSyncControl();
702
- return useMemo2(() => ({
703
- mode: syncMode,
704
- setMode: setSyncMode,
705
- canUpload: syncMode === "push-pull",
706
- canDownload: syncMode !== "offline"
707
- }), [syncMode, setSyncMode]);
708
- }
709
- function useConnectionHealth() {
710
- const context = useContext(ConnectionHealthContext);
711
- if (!context) {
712
- throw new Error("useConnectionHealth must be used within a PowerSyncProvider");
713
- }
714
- return context.health;
715
- }
716
- function useSyncMetrics() {
717
- const context = useContext(SyncMetricsContext);
718
- if (!context) {
719
- throw new Error("useSyncMetrics must be used within a PowerSyncProvider");
720
- }
721
- return context.metrics;
722
- }
723
- function useAttachmentQueue() {
724
- return useContext(AttachmentQueueContext);
725
- }
726
- function useDatabase() {
727
- const { db, isReady, error } = usePowerSync();
728
- if (error) {
729
- throw error;
730
- }
731
- if (!isReady || !db) {
732
- throw new Error("PowerSync database is not ready");
733
- }
734
- return db;
735
- }
736
- function usePlatform() {
737
- const { platform } = usePowerSync();
738
- return platform;
739
- }
740
- function useOnlineStatus() {
741
- const { platform } = usePowerSync();
742
- const [isOnline, setIsOnline] = useState2(true);
743
- useEffect2(() => {
744
- platform.network.isConnected().then(setIsOnline);
745
- const unsubscribe = platform.network.addConnectionListener(setIsOnline);
746
- return unsubscribe;
747
- }, [platform]);
748
- return isOnline;
749
- }
750
- function usePendingMutations() {
751
- const { pendingMutations, pendingCount } = useSyncStatus();
752
- return useMemo2(
753
- () => ({
754
- mutations: pendingMutations,
755
- count: pendingCount
756
- }),
757
- [pendingMutations, pendingCount]
758
- );
759
- }
760
- function useIsSyncing() {
761
- const { status } = useSyncStatus();
762
- return status.uploading || status.downloading;
763
- }
764
- function useDownloadProgress() {
765
- const { status } = useSyncStatus();
766
- return status.downloadProgress;
767
- }
768
- var recentlySyncedEntities = /* @__PURE__ */ new Map();
769
- var SYNCED_DISPLAY_DURATION_MS = 3e3;
770
- function useEntitySyncStatus(entityId) {
771
- const { status, pendingMutations, clearFailure, failedTransactions } = useSyncStatus();
772
- const [, forceUpdate] = useState2(0);
773
- const entityPendingMutations = useMemo2(() => {
774
- if (!entityId) return [];
775
- return pendingMutations.filter(
776
- (entry) => entry.id === entityId || String(entry.opData?.id) === entityId
777
- );
778
- }, [entityId, pendingMutations]);
779
- const failedTransaction = useMemo2(() => {
780
- if (!entityId) return null;
781
- return failedTransactions.find(
782
- (ft) => ft.affectedEntityIds.includes(entityId)
783
- ) ?? null;
784
- }, [entityId, failedTransactions]);
785
- const wasSyncingRef = useRef2(false);
786
- const isCurrentlySyncing = entityPendingMutations.length > 0;
787
- useEffect2(() => {
788
- if (!entityId) return;
789
- if (wasSyncingRef.current && !isCurrentlySyncing && !failedTransaction) {
790
- recentlySyncedEntities.set(entityId, Date.now());
791
- const timer = setTimeout(() => {
792
- const syncedAt = recentlySyncedEntities.get(entityId);
793
- if (syncedAt && Date.now() - syncedAt >= SYNCED_DISPLAY_DURATION_MS) {
794
- recentlySyncedEntities.delete(entityId);
795
- forceUpdate((n) => n + 1);
796
- }
797
- }, SYNCED_DISPLAY_DURATION_MS);
798
- return () => clearTimeout(timer);
799
- }
800
- wasSyncingRef.current = isCurrentlySyncing;
801
- }, [entityId, isCurrentlySyncing, failedTransaction]);
802
- const state = useMemo2(() => {
803
- if (!entityId) return "idle";
804
- if (failedTransaction) {
805
- return "error";
806
- }
807
- if (entityPendingMutations.length > 0) {
808
- return "syncing";
809
- }
810
- const syncedAt = recentlySyncedEntities.get(entityId);
811
- if (syncedAt && Date.now() - syncedAt < SYNCED_DISPLAY_DURATION_MS) {
812
- return "synced";
813
- }
814
- return "idle";
815
- }, [entityId, failedTransaction, entityPendingMutations.length]);
816
- const error = failedTransaction?.error ?? null;
817
- const dismiss = useCallback2(() => {
818
- if (failedTransaction) {
819
- clearFailure(failedTransaction.id);
820
- }
821
- }, [failedTransaction, clearFailure]);
822
- return {
823
- state,
824
- error,
825
- pendingOperations: entityPendingMutations.length,
826
- failedTransaction,
827
- dismiss
828
- };
829
- }
830
- function useUploadStatus() {
831
- const { status, pendingCount, clearAllFailures, failedTransactions, hasUploadErrors, permanentErrorCount } = useSyncStatus();
832
- const { triggerSync } = useSyncControl();
833
- const retryAll = useCallback2(async () => {
834
- await triggerSync();
835
- }, [triggerSync]);
836
- const dismissAll = useCallback2(() => {
837
- clearAllFailures();
838
- }, [clearAllFailures]);
839
- return useMemo2(
840
- () => ({
841
- pendingCount,
842
- failedCount: failedTransactions.length,
843
- permanentFailureCount: permanentErrorCount,
844
- hasErrors: hasUploadErrors,
845
- hasPermanentErrors: permanentErrorCount > 0,
846
- failedTransactions,
847
- retryAll,
848
- dismissAll
849
- }),
850
- [
851
- pendingCount,
852
- failedTransactions,
853
- permanentErrorCount,
854
- hasUploadErrors,
855
- retryAll,
856
- dismissAll
857
- ]
858
- );
859
- }
860
- function useSyncActivity() {
861
- const {
862
- pendingMutations,
863
- clearFailure,
864
- failedTransactions,
865
- completedTransactions,
866
- clearCompletedHistory,
867
- isUploading,
868
- isDownloading
869
- } = useSyncStatus();
870
- const { triggerSync } = useSyncControl();
871
- const retryAll = useCallback2(async () => {
872
- await triggerSync();
873
- }, [triggerSync]);
874
- const dismissFailure = useCallback2((failureId) => {
875
- clearFailure(failureId);
876
- }, [clearFailure]);
877
- const clearCompleted = useCallback2(() => {
878
- clearCompletedHistory();
879
- }, [clearCompletedHistory]);
880
- const counts = useMemo2(() => ({
881
- pending: pendingMutations.length,
882
- failed: failedTransactions.length,
883
- completed: completedTransactions.length
884
- }), [pendingMutations.length, failedTransactions.length, completedTransactions.length]);
885
- const hasActivity = isUploading || isDownloading || failedTransactions.length > 0;
886
- return useMemo2(
887
- () => ({
888
- pending: pendingMutations,
889
- failed: failedTransactions,
890
- completed: completedTransactions,
891
- counts,
892
- hasActivity,
893
- retryAll,
894
- dismissFailure,
895
- clearCompleted
896
- }),
897
- [pendingMutations, failedTransactions, completedTransactions, counts, hasActivity, retryAll, dismissFailure, clearCompleted]
898
- );
899
- }
900
-
901
- export {
902
- PowerSyncContext,
903
- SyncStatusContext,
904
- ConnectionHealthContext,
905
- SyncMetricsContext,
906
- AttachmentQueueContext,
907
- PowerSyncProvider,
908
- usePowerSync,
909
- useSyncStatus,
910
- useSyncControl,
911
- useSyncMode,
912
- useConnectionHealth,
913
- useSyncMetrics,
914
- useAttachmentQueue,
915
- useDatabase,
916
- usePlatform,
917
- useOnlineStatus,
918
- usePendingMutations,
919
- useIsSyncing,
920
- useDownloadProgress,
921
- useEntitySyncStatus,
922
- useUploadStatus,
923
- useSyncActivity
924
- };
925
- //# sourceMappingURL=chunk-Q3LFFMRR.js.map