local-lite 0.0.15 → 0.0.16

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 (42) hide show
  1. package/dist/adapters/remote/supabase.d.ts +1 -5
  2. package/dist/adapters/remote/supabase.js +2 -2
  3. package/dist/auth/google.d.ts +2 -1
  4. package/dist/auth/google.js +7 -0
  5. package/dist/auth/microsoft.d.ts +3 -3
  6. package/dist/auth/microsoft.js +12 -3
  7. package/dist/backups/cloud-storage.d.ts +22 -6
  8. package/dist/backups/cloud-storage.js +111 -16
  9. package/dist/backups/index.d.ts +3 -15
  10. package/dist/backups/index.js +9 -26
  11. package/dist/backups/onedrive.d.ts +18 -4
  12. package/dist/backups/onedrive.js +69 -12
  13. package/dist/backups/operations.d.ts +39 -0
  14. package/dist/backups/operations.js +74 -0
  15. package/dist/config/resolve-remote.js +0 -1
  16. package/dist/core/client.d.ts +20 -0
  17. package/dist/core/client.js +146 -0
  18. package/dist/index.d.ts +0 -1
  19. package/dist/index.js +0 -1
  20. package/dist/react/SyncInspector.d.ts +0 -2
  21. package/dist/react/SyncInspector.js +0 -2
  22. package/dist/react/auth.d.ts +0 -2
  23. package/dist/react/auth.js +0 -2
  24. package/dist/react/backups.d.ts +28 -0
  25. package/dist/react/backups.js +265 -0
  26. package/dist/react/index.d.ts +3 -2
  27. package/dist/react/index.js +2 -2
  28. package/package.json +1 -11
  29. package/dist/access/client.d.ts +0 -9
  30. package/dist/access/client.js +0 -56
  31. package/dist/access/engine.d.ts +0 -2
  32. package/dist/access/engine.js +0 -46
  33. package/dist/access/errors.d.ts +0 -14
  34. package/dist/access/errors.js +0 -23
  35. package/dist/access/guard.d.ts +0 -24
  36. package/dist/access/guard.js +0 -104
  37. package/dist/access/index.d.ts +0 -5
  38. package/dist/access/index.js +0 -21
  39. package/dist/access/react/index.d.ts +0 -32
  40. package/dist/access/react/index.js +0 -42
  41. package/dist/access/types.d.ts +0 -26
  42. package/dist/access/types.js +0 -2
@@ -0,0 +1,39 @@
1
+ import { type EncryptedBackupOptions, type RestoreEncryptedBackupOptions } from './encryption.js';
2
+ import type { CloudBackupFile } from './cloud-storage.js';
3
+ export type BackupAccessTokenProvider = () => Promise<string>;
4
+ export interface BackupDatabase {
5
+ exportDatabase(): Promise<Uint8Array>;
6
+ importDatabase(data: Uint8Array): Promise<void>;
7
+ }
8
+ export interface BackupStorage {
9
+ save(fileName: string, data: Uint8Array): Promise<void>;
10
+ read(fileName: string): Promise<Uint8Array>;
11
+ list?(): Promise<CloudBackupFile[]>;
12
+ delete?(fileName: string): Promise<void>;
13
+ }
14
+ export interface CloudBackupClient extends BackupStorage {
15
+ connect(): Promise<boolean>;
16
+ disconnect(): Promise<void>;
17
+ isConnected(): Promise<boolean>;
18
+ list(): Promise<CloudBackupFile[]>;
19
+ deleteBackup?(fileIdOrName: string): Promise<void>;
20
+ onStatusChange?(listener: (connected: boolean) => void): () => void;
21
+ backup?(database: ResolvableBackupDatabase, options?: CreateDatabaseBackupOptions & Partial<EncryptedBackupOptions>): Promise<string>;
22
+ restore?(database: ResolvableBackupDatabase, fileNameOrId: string, options?: Partial<RestoreEncryptedBackupOptions>): Promise<void>;
23
+ }
24
+ export interface CreateDatabaseBackupOptions {
25
+ fileName?: string;
26
+ }
27
+ export type ResolvableBackupDatabase = BackupDatabase | {
28
+ exportDatabase(): Promise<Uint8Array>;
29
+ importDatabase(data: Uint8Array): Promise<void>;
30
+ } | {
31
+ db: any;
32
+ exportDatabase?(): Promise<Uint8Array>;
33
+ importDatabase?(data: Uint8Array): Promise<void>;
34
+ };
35
+ export declare function resolveBackupDatabase(target: any): BackupDatabase;
36
+ export declare function createDatabaseBackup(database: ResolvableBackupDatabase, storage: BackupStorage, options?: CreateDatabaseBackupOptions): Promise<string>;
37
+ export declare function restoreDatabaseBackup(database: ResolvableBackupDatabase, storage: BackupStorage, fileName: string): Promise<void>;
38
+ export declare function performBackup(database: ResolvableBackupDatabase, storage: BackupStorage, options?: CreateDatabaseBackupOptions & Partial<EncryptedBackupOptions>): Promise<string>;
39
+ export declare function performRestore(database: ResolvableBackupDatabase, storage: BackupStorage, fileNameOrId: string, options?: Partial<RestoreEncryptedBackupOptions>): Promise<void>;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveBackupDatabase = resolveBackupDatabase;
4
+ exports.createDatabaseBackup = createDatabaseBackup;
5
+ exports.restoreDatabaseBackup = restoreDatabaseBackup;
6
+ exports.performBackup = performBackup;
7
+ exports.performRestore = performRestore;
8
+ const encryption_js_1 = require("./encryption.js");
9
+ function resolveBackupDatabase(target) {
10
+ if (!target) {
11
+ throw new Error('A valid database or LocalFirstClient is required for backup.');
12
+ }
13
+ if (typeof target.exportDatabase === 'function' && typeof target.importDatabase === 'function') {
14
+ return target;
15
+ }
16
+ if (target.db &&
17
+ typeof target.db.exportDatabase === 'function' &&
18
+ typeof target.db.importDatabase === 'function') {
19
+ return target.db;
20
+ }
21
+ throw new Error('Provided target does not implement BackupDatabase. It must provide exportDatabase() and importDatabase().');
22
+ }
23
+ function validateFileName(fileName) {
24
+ if (!fileName || /[\\/]/.test(fileName)) {
25
+ throw new Error('Backup file name must be a single file name.');
26
+ }
27
+ }
28
+ async function createDatabaseBackup(database, storage, options = {}) {
29
+ const resolved = resolveBackupDatabase(database);
30
+ const fileName = options.fileName ?? `local-lite-${new Date().toISOString().replace(/[:.]/g, '-')}.db`;
31
+ validateFileName(fileName);
32
+ const databaseBytes = await resolved.exportDatabase();
33
+ if (databaseBytes.length === 0) {
34
+ throw new Error('Cannot create a backup from an empty database.');
35
+ }
36
+ await storage.save(fileName, databaseBytes);
37
+ return fileName;
38
+ }
39
+ async function restoreDatabaseBackup(database, storage, fileName) {
40
+ validateFileName(fileName);
41
+ const resolved = resolveBackupDatabase(database);
42
+ const databaseBytes = await storage.read(fileName);
43
+ if (databaseBytes.length === 0) {
44
+ throw new Error('Cannot restore an empty database backup.');
45
+ }
46
+ await resolved.importDatabase(databaseBytes);
47
+ }
48
+ async function performBackup(database, storage, options = {}) {
49
+ const resolved = resolveBackupDatabase(database);
50
+ if (options.passphrase && options.passphrase.trim()) {
51
+ return (0, encryption_js_1.createEncryptedDatabaseBackup)(resolved, storage, {
52
+ passphrase: options.passphrase,
53
+ fileName: options.fileName,
54
+ iterations: options.iterations,
55
+ });
56
+ }
57
+ return createDatabaseBackup(resolved, storage, options);
58
+ }
59
+ async function performRestore(database, storage, fileNameOrId, options = {}) {
60
+ const resolved = resolveBackupDatabase(database);
61
+ if (options.passphrase && options.passphrase.trim()) {
62
+ return (0, encryption_js_1.restoreEncryptedDatabaseBackup)(resolved, storage, fileNameOrId, {
63
+ passphrase: options.passphrase,
64
+ });
65
+ }
66
+ const rawBytes = await storage.read(fileNameOrId);
67
+ if ((0, encryption_js_1.isEncryptedBackup)(rawBytes)) {
68
+ throw new Error('This backup is encrypted. A passphrase is required to restore it.');
69
+ }
70
+ if (rawBytes.length === 0) {
71
+ throw new Error('Cannot restore an empty database backup.');
72
+ }
73
+ await resolved.importDatabase(rawBytes);
74
+ }
@@ -46,7 +46,6 @@ async function resolveRemoteAdapter(config) {
46
46
  updatedAtColumn: config.supabase?.updatedAtColumn,
47
47
  deletedColumn: config.supabase?.deletedColumn,
48
48
  deletedValue: config.supabase?.deletedValue,
49
- deletedAtColumn: config.supabase?.deletedAtColumn,
50
49
  pushRpcName: config.supabase?.pushRpcName,
51
50
  channelName: config.supabase?.channelName,
52
51
  scopedTables: config.supabase?.scopedTables,
@@ -8,6 +8,7 @@ import { BatchMutation, ConflictResolver, ConflictStrategy, MutateOptions, Netwo
8
8
  import { SyncLogger, CustomLogger, LogLevel } from '../utils/logger.js';
9
9
  import { QueryBuilder, AggregateSpec, AggregateResult } from './query-builder.js';
10
10
  import { type ExportJsonOptions, type ImportJsonOptions, type ExportCsvOptions, type ImportCsvOptions, type ImportSummary } from '../utils/portability.js';
11
+ import { type BackupStorage, type CreateDatabaseBackupOptions, type EncryptedBackupOptions, type RestoreEncryptedBackupOptions } from '../backups/index.js';
11
12
  export interface LocalFirstClientConfig {
12
13
  local: LocalDBAdapter;
13
14
  remote: RemoteDBAdapter;
@@ -325,4 +326,23 @@ export declare class LocalFirstClient {
325
326
  reset(options?: {
326
327
  dropTables?: boolean;
327
328
  }): Promise<void>;
329
+ private getAllTableNames;
330
+ /**
331
+ * Exports the entire local database state as a portable binary snapshot.
332
+ * If the local adapter has a native export mechanism (e.g. serialize or export),
333
+ * it uses that; otherwise, it exports an atomic JSON snapshot of all tables.
334
+ */
335
+ exportDatabase(): Promise<Uint8Array>;
336
+ /**
337
+ * Imports database state from a backup snapshot.
338
+ */
339
+ importDatabase(data: Uint8Array): Promise<void>;
340
+ /**
341
+ * Creates a backup directly from this client to the given cloud or local storage.
342
+ */
343
+ backup(storage: BackupStorage, options?: CreateDatabaseBackupOptions & Partial<EncryptedBackupOptions>): Promise<string>;
344
+ /**
345
+ * Restores this client's database from a backup in cloud or local storage.
346
+ */
347
+ restore(storage: BackupStorage, fileNameOrId: string, options?: Partial<RestoreEncryptedBackupOptions>): Promise<void>;
328
348
  }
@@ -13,6 +13,7 @@ const sql_js_1 = require("../utils/sql.js");
13
13
  const query_js_1 = require("./query.js");
14
14
  const query_builder_js_1 = require("./query-builder.js");
15
15
  const portability_js_1 = require("../utils/portability.js");
16
+ const index_js_1 = require("../backups/index.js");
16
17
  function hasFunctionFilter(value) {
17
18
  if (typeof value === 'function')
18
19
  return true;
@@ -903,5 +904,150 @@ class LocalFirstClient {
903
904
  await this.resetSyncCursor();
904
905
  this.events.emit('reset', { dropTables: options.dropTables ?? false });
905
906
  }
907
+ async getAllTableNames() {
908
+ const list = new Set(this.tables);
909
+ list.add('sync_outbox');
910
+ list.add('sync_meta');
911
+ try {
912
+ const rows = await this.db.execute(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`);
913
+ if (Array.isArray(rows)) {
914
+ for (const r of rows) {
915
+ if (r.name)
916
+ list.add(r.name);
917
+ }
918
+ }
919
+ }
920
+ catch {
921
+ // Non-SQLite database adapter, fallback to configured tables
922
+ }
923
+ return Array.from(list);
924
+ }
925
+ /**
926
+ * Exports the entire local database state as a portable binary snapshot.
927
+ * If the local adapter has a native export mechanism (e.g. serialize or export),
928
+ * it uses that; otherwise, it exports an atomic JSON snapshot of all tables.
929
+ */
930
+ async exportDatabase() {
931
+ await this.initialize();
932
+ if (typeof this.db.exportDatabase === 'function') {
933
+ return this.db.exportDatabase();
934
+ }
935
+ if (typeof this.db.serialize === 'function') {
936
+ return new Uint8Array(this.db.serialize());
937
+ }
938
+ if (typeof this.db.export === 'function') {
939
+ return new Uint8Array(this.db.export());
940
+ }
941
+ const tables = await this.getAllTableNames();
942
+ const snapshot = {};
943
+ for (const table of tables) {
944
+ const safeTable = (0, sql_js_1.escapeIdentifier)(table);
945
+ try {
946
+ const rows = await this.db.execute(`SELECT * FROM ${safeTable}`);
947
+ snapshot[table] = rows || [];
948
+ }
949
+ catch {
950
+ // Table may not exist yet
951
+ }
952
+ }
953
+ const payload = JSON.stringify({
954
+ format: 'local-lite-backup-v1',
955
+ timestamp: Date.now(),
956
+ tables: snapshot,
957
+ });
958
+ return new TextEncoder().encode(payload);
959
+ }
960
+ /**
961
+ * Imports database state from a backup snapshot.
962
+ */
963
+ async importDatabase(data) {
964
+ if (!data || data.length === 0) {
965
+ throw new Error('Cannot restore an empty database snapshot.');
966
+ }
967
+ await this.initialize();
968
+ if (typeof this.db.importDatabase === 'function') {
969
+ await this.db.importDatabase(data);
970
+ this.schemaManager.clearCache();
971
+ this.events.emit('reset', { dropTables: false });
972
+ return;
973
+ }
974
+ // Check if data is SQLite format (magic header: "SQLite format 3\0")
975
+ const isSqlite = data.length > 16 &&
976
+ data[0] === 0x53 &&
977
+ data[1] === 0x51 &&
978
+ data[2] === 0x4c &&
979
+ data[3] === 0x69 &&
980
+ data[4] === 0x74 &&
981
+ data[5] === 0x65 &&
982
+ data[6] === 0x20 &&
983
+ data[7] === 0x66 &&
984
+ data[8] === 0x6f &&
985
+ data[9] === 0x72 &&
986
+ data[10] === 0x6d &&
987
+ data[11] === 0x61 &&
988
+ data[12] === 0x74 &&
989
+ data[13] === 0x20 &&
990
+ data[14] === 0x33 &&
991
+ data[15] === 0x00;
992
+ if (isSqlite) {
993
+ if (typeof this.db.load === 'function') {
994
+ await this.db.load(data);
995
+ this.schemaManager.clearCache();
996
+ this.events.emit('reset', { dropTables: false });
997
+ return;
998
+ }
999
+ throw new Error('Direct SQLite binary snapshot import requires an adapter with native import/load support (e.g. sql.js).');
1000
+ }
1001
+ // Parse portable JSON backup snapshot
1002
+ try {
1003
+ const text = new TextDecoder().decode(data);
1004
+ const parsed = JSON.parse(text);
1005
+ if (parsed.tables && typeof parsed.tables === 'object') {
1006
+ await this.db.transaction(async (tx) => {
1007
+ for (const [table, rows] of Object.entries(parsed.tables)) {
1008
+ const safeTable = (0, sql_js_1.escapeIdentifier)(table);
1009
+ if (Array.isArray(rows) && rows.length > 0) {
1010
+ await this.schemaManager.ensureTable(tx, table, rows[0]);
1011
+ await tx.execute(`DELETE FROM ${safeTable}`);
1012
+ for (const row of rows) {
1013
+ const cols = Object.keys(row);
1014
+ const safeCols = cols.map(sql_js_1.escapeIdentifier).join(', ');
1015
+ const placeholders = cols.map(() => '?').join(', ');
1016
+ const values = cols.map(c => {
1017
+ const val = row[c];
1018
+ return val !== null && typeof val === 'object' ? JSON.stringify(val) : val;
1019
+ });
1020
+ await tx.execute(`INSERT INTO ${safeTable} (${safeCols}) VALUES (${placeholders})`, values);
1021
+ }
1022
+ }
1023
+ else {
1024
+ try {
1025
+ await tx.execute(`DELETE FROM ${safeTable}`);
1026
+ }
1027
+ catch { }
1028
+ }
1029
+ }
1030
+ });
1031
+ this.schemaManager.clearCache();
1032
+ this.events.emit('reset', { dropTables: false });
1033
+ return;
1034
+ }
1035
+ }
1036
+ catch (err) {
1037
+ throw new Error(`Failed to import backup snapshot: ${err?.message || err}`);
1038
+ }
1039
+ }
1040
+ /**
1041
+ * Creates a backup directly from this client to the given cloud or local storage.
1042
+ */
1043
+ async backup(storage, options) {
1044
+ return (0, index_js_1.performBackup)(this, storage, options);
1045
+ }
1046
+ /**
1047
+ * Restores this client's database from a backup in cloud or local storage.
1048
+ */
1049
+ async restore(storage, fileNameOrId, options) {
1050
+ return (0, index_js_1.performRestore)(this, storage, fileNameOrId, options);
1051
+ }
906
1052
  }
907
1053
  exports.LocalFirstClient = LocalFirstClient;
package/dist/index.d.ts CHANGED
@@ -22,4 +22,3 @@ export * from './utils/portability.js';
22
22
  export * from './schema/index.js';
23
23
  export * from './auth/index.js';
24
24
  export * from './backups/index.js';
25
- export * from './access/index.js';
package/dist/index.js CHANGED
@@ -37,4 +37,3 @@ __exportStar(require("./utils/portability.js"), exports);
37
37
  __exportStar(require("./schema/index.js"), exports);
38
38
  __exportStar(require("./auth/index.js"), exports);
39
39
  __exportStar(require("./backups/index.js"), exports);
40
- __exportStar(require("./access/index.js"), exports);
@@ -27,5 +27,3 @@ export interface SyncInspectorProps {
27
27
  * Displays real-time outbox telemetry, connection status, metrics, and triggers manual push, pull, reset, and benchmarks.
28
28
  */
29
29
  export declare function SyncInspector({ client, position, variant, initialOpen, enableBenchmark, onOpenBenchmark, style, theme, alwaysVisible, renderTrigger, }: SyncInspectorProps): React.JSX.Element | null;
30
- export declare const SyncIndicator: typeof SyncInspector;
31
- export type SyncIndicatorProps = SyncInspectorProps;
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SyncIndicator = void 0;
4
3
  exports.SyncInspector = SyncInspector;
5
4
  const jsx_runtime_1 = require("react/jsx-runtime");
6
5
  const react_1 = require("react");
@@ -582,4 +581,3 @@ const rnStyles = RN && RN.StyleSheet
582
581
  },
583
582
  })
584
583
  : {};
585
- exports.SyncIndicator = SyncInspector;
@@ -21,9 +21,7 @@ export interface UseMicrosoftAuthResult {
21
21
  signOut: () => Promise<void>;
22
22
  refetchUser: () => Promise<MicrosoftUser | null>;
23
23
  }
24
- export type UseOneDriveAuthResult = UseMicrosoftAuthResult;
25
24
  export declare function useMicrosoftAuth(client: MicrosoftAuthClient | null | undefined): UseMicrosoftAuthResult;
26
- export declare const useOneDriveAuth: typeof useMicrosoftAuth;
27
25
  export interface UseGitHubAuthResult {
28
26
  user: GitHubUser | null;
29
27
  isSignedIn: boolean;
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useOneDriveAuth = void 0;
4
3
  exports.useGoogleAuth = useGoogleAuth;
5
4
  exports.useMicrosoftAuth = useMicrosoftAuth;
6
5
  exports.useGitHubAuth = useGitHubAuth;
@@ -184,7 +183,6 @@ function useMicrosoftAuth(client) {
184
183
  refetchUser,
185
184
  };
186
185
  }
187
- exports.useOneDriveAuth = useMicrosoftAuth;
188
186
  function useGitHubAuth(client) {
189
187
  const [user, setUser] = (0, react_1.useState)(null);
190
188
  const [loading, setLoading] = (0, react_1.useState)(true);
@@ -0,0 +1,28 @@
1
+ import type { CloudBackupClient, CloudBackupFile, ResolvableBackupDatabase, GoogleDriveBackupClient, OneDriveBackupClient } from '../backups/index.js';
2
+ export interface UseCloudBackupOptions {
3
+ passphrase?: string;
4
+ autoRefresh?: boolean;
5
+ }
6
+ export interface UseCloudBackupResult {
7
+ isConnected: boolean;
8
+ isLoading: boolean;
9
+ isBackingUp: boolean;
10
+ isRestoring: boolean;
11
+ backups: CloudBackupFile[];
12
+ lastBackupAt: string | null;
13
+ error: Error | null;
14
+ connect: () => Promise<boolean>;
15
+ disconnect: () => Promise<void>;
16
+ refreshBackups: () => Promise<CloudBackupFile[]>;
17
+ backup: (options?: {
18
+ fileName?: string;
19
+ passphrase?: string;
20
+ }) => Promise<string>;
21
+ restore: (fileNameOrId: string, options?: {
22
+ passphrase?: string;
23
+ }) => Promise<void>;
24
+ deleteBackup: (fileNameOrId: string) => Promise<void>;
25
+ }
26
+ export declare function useCloudBackup(client: CloudBackupClient | null | undefined, database?: ResolvableBackupDatabase, options?: UseCloudBackupOptions): UseCloudBackupResult;
27
+ export declare function useGoogleDriveBackup(client: GoogleDriveBackupClient | null | undefined, database?: ResolvableBackupDatabase, options?: UseCloudBackupOptions): UseCloudBackupResult;
28
+ export declare function useOneDriveBackup(client: OneDriveBackupClient | null | undefined, database?: ResolvableBackupDatabase, options?: UseCloudBackupOptions): UseCloudBackupResult;
@@ -0,0 +1,265 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useCloudBackup = useCloudBackup;
4
+ exports.useGoogleDriveBackup = useGoogleDriveBackup;
5
+ exports.useOneDriveBackup = useOneDriveBackup;
6
+ const react_1 = require("react");
7
+ const index_js_1 = require("../backups/index.js");
8
+ function useCloudBackup(client, database, options) {
9
+ const [isConnected, setIsConnected] = (0, react_1.useState)(false);
10
+ const [isLoading, setIsLoading] = (0, react_1.useState)(true);
11
+ const [isBackingUp, setIsBackingUp] = (0, react_1.useState)(false);
12
+ const [isRestoring, setIsRestoring] = (0, react_1.useState)(false);
13
+ const [backups, setBackups] = (0, react_1.useState)([]);
14
+ const [lastBackupAt, setLastBackupAt] = (0, react_1.useState)(null);
15
+ const [error, setError] = (0, react_1.useState)(null);
16
+ const databaseRef = (0, react_1.useRef)(database);
17
+ databaseRef.current = database;
18
+ const defaultPassphrase = options?.passphrase;
19
+ const autoRefresh = options?.autoRefresh ?? true;
20
+ const refreshBackups = (0, react_1.useCallback)(async () => {
21
+ if (!client) {
22
+ setBackups([]);
23
+ return [];
24
+ }
25
+ try {
26
+ const connected = await client.isConnected();
27
+ if (!connected) {
28
+ setBackups([]);
29
+ return [];
30
+ }
31
+ const list = await client.list();
32
+ setBackups(list);
33
+ return list;
34
+ }
35
+ catch (err) {
36
+ const e = err instanceof Error ? err : new Error(String(err));
37
+ setError(e);
38
+ throw e;
39
+ }
40
+ }, [client]);
41
+ (0, react_1.useEffect)(() => {
42
+ if (!client) {
43
+ setIsConnected(false);
44
+ setIsLoading(false);
45
+ setBackups([]);
46
+ return;
47
+ }
48
+ let isMounted = true;
49
+ setIsLoading(true);
50
+ client
51
+ .isConnected()
52
+ .then(async (connected) => {
53
+ if (!isMounted)
54
+ return;
55
+ setIsConnected(connected);
56
+ setIsLoading(false);
57
+ if (connected && autoRefresh) {
58
+ try {
59
+ const list = await client.list();
60
+ if (isMounted)
61
+ setBackups(list);
62
+ }
63
+ catch (listErr) {
64
+ if (isMounted) {
65
+ setError(listErr instanceof Error ? listErr : new Error(String(listErr)));
66
+ }
67
+ }
68
+ }
69
+ })
70
+ .catch(err => {
71
+ if (!isMounted)
72
+ return;
73
+ setError(err instanceof Error ? err : new Error(String(err)));
74
+ setIsLoading(false);
75
+ });
76
+ let unsubscribe;
77
+ if (typeof client.onStatusChange === 'function') {
78
+ unsubscribe = client.onStatusChange(async (connected) => {
79
+ if (!isMounted)
80
+ return;
81
+ setIsConnected(connected);
82
+ if (connected && autoRefresh) {
83
+ try {
84
+ const list = await client.list();
85
+ if (isMounted)
86
+ setBackups(list);
87
+ }
88
+ catch (listErr) {
89
+ if (isMounted) {
90
+ setError(listErr instanceof Error ? listErr : new Error(String(listErr)));
91
+ }
92
+ }
93
+ }
94
+ else if (!connected) {
95
+ setBackups([]);
96
+ }
97
+ });
98
+ }
99
+ return () => {
100
+ isMounted = false;
101
+ if (unsubscribe)
102
+ unsubscribe();
103
+ };
104
+ }, [client, autoRefresh]);
105
+ const connect = (0, react_1.useCallback)(async () => {
106
+ if (!client)
107
+ throw new Error('Cloud backup client is not initialized.');
108
+ setError(null);
109
+ setIsLoading(true);
110
+ try {
111
+ const connected = await client.connect();
112
+ setIsConnected(connected);
113
+ if (connected && autoRefresh) {
114
+ const list = await client.list();
115
+ setBackups(list);
116
+ }
117
+ return connected;
118
+ }
119
+ catch (err) {
120
+ const e = err instanceof Error ? err : new Error(String(err));
121
+ setError(e);
122
+ throw e;
123
+ }
124
+ finally {
125
+ setIsLoading(false);
126
+ }
127
+ }, [client, autoRefresh]);
128
+ const disconnect = (0, react_1.useCallback)(async () => {
129
+ if (!client)
130
+ return;
131
+ setError(null);
132
+ setIsLoading(true);
133
+ try {
134
+ await client.disconnect();
135
+ setIsConnected(false);
136
+ setBackups([]);
137
+ }
138
+ catch (err) {
139
+ const e = err instanceof Error ? err : new Error(String(err));
140
+ setError(e);
141
+ throw e;
142
+ }
143
+ finally {
144
+ setIsLoading(false);
145
+ }
146
+ }, [client]);
147
+ const backup = (0, react_1.useCallback)(async (backupOptions) => {
148
+ if (!client)
149
+ throw new Error('Cloud backup client is not initialized.');
150
+ const currentDb = databaseRef.current;
151
+ if (!currentDb) {
152
+ throw new Error('A database instance or LocalFirstClient must be provided to create a backup.');
153
+ }
154
+ setError(null);
155
+ setIsBackingUp(true);
156
+ try {
157
+ const passphrase = backupOptions?.passphrase ?? defaultPassphrase;
158
+ let createdFileName;
159
+ if (typeof client.backup === 'function') {
160
+ createdFileName = await client.backup(currentDb, {
161
+ fileName: backupOptions?.fileName,
162
+ passphrase,
163
+ });
164
+ }
165
+ else {
166
+ createdFileName = await (0, index_js_1.performBackup)(currentDb, client, {
167
+ fileName: backupOptions?.fileName,
168
+ passphrase,
169
+ });
170
+ }
171
+ setLastBackupAt(new Date().toISOString());
172
+ if (autoRefresh) {
173
+ try {
174
+ const list = await client.list();
175
+ setBackups(list);
176
+ }
177
+ catch {
178
+ // Backup succeeded; silent list update failure is tolerated
179
+ }
180
+ }
181
+ return createdFileName;
182
+ }
183
+ catch (err) {
184
+ const e = err instanceof Error ? err : new Error(String(err));
185
+ setError(e);
186
+ throw e;
187
+ }
188
+ finally {
189
+ setIsBackingUp(false);
190
+ }
191
+ }, [client, defaultPassphrase, autoRefresh]);
192
+ const restore = (0, react_1.useCallback)(async (fileNameOrId, restoreOptions) => {
193
+ if (!client)
194
+ throw new Error('Cloud backup client is not initialized.');
195
+ const currentDb = databaseRef.current;
196
+ if (!currentDb) {
197
+ throw new Error('A database instance or LocalFirstClient must be provided to restore a backup.');
198
+ }
199
+ setError(null);
200
+ setIsRestoring(true);
201
+ try {
202
+ const passphrase = restoreOptions?.passphrase ?? defaultPassphrase;
203
+ if (typeof client.restore === 'function') {
204
+ await client.restore(currentDb, fileNameOrId, { passphrase });
205
+ }
206
+ else {
207
+ await (0, index_js_1.performRestore)(currentDb, client, fileNameOrId, { passphrase });
208
+ }
209
+ }
210
+ catch (err) {
211
+ const e = err instanceof Error ? err : new Error(String(err));
212
+ setError(e);
213
+ throw e;
214
+ }
215
+ finally {
216
+ setIsRestoring(false);
217
+ }
218
+ }, [client, defaultPassphrase]);
219
+ const deleteBackup = (0, react_1.useCallback)(async (fileNameOrId) => {
220
+ if (!client)
221
+ throw new Error('Cloud backup client is not initialized.');
222
+ setError(null);
223
+ try {
224
+ if (typeof client.deleteBackup === 'function') {
225
+ await client.deleteBackup(fileNameOrId);
226
+ }
227
+ else if (typeof client.delete === 'function') {
228
+ await client.delete(fileNameOrId);
229
+ }
230
+ else {
231
+ throw new Error('Delete operation is not supported by this cloud storage client.');
232
+ }
233
+ if (autoRefresh) {
234
+ const list = await client.list();
235
+ setBackups(list);
236
+ }
237
+ }
238
+ catch (err) {
239
+ const e = err instanceof Error ? err : new Error(String(err));
240
+ setError(e);
241
+ throw e;
242
+ }
243
+ }, [client, autoRefresh]);
244
+ return {
245
+ isConnected,
246
+ isLoading,
247
+ isBackingUp,
248
+ isRestoring,
249
+ backups,
250
+ lastBackupAt,
251
+ error,
252
+ connect,
253
+ disconnect,
254
+ refreshBackups,
255
+ backup,
256
+ restore,
257
+ deleteBackup,
258
+ };
259
+ }
260
+ function useGoogleDriveBackup(client, database, options) {
261
+ return useCloudBackup(client, database, options);
262
+ }
263
+ function useOneDriveBackup(client, database, options) {
264
+ return useCloudBackup(client, database, options);
265
+ }
@@ -127,6 +127,7 @@ export * from './runner.js';
127
127
  export * from './status.js';
128
128
  export * from './theme.js';
129
129
  export * from './BenchmarkModal.js';
130
- export { SyncInspector, SyncIndicator } from './SyncInspector.js';
131
- export type { SyncInspectorProps, SyncIndicatorProps } from './SyncInspector.js';
130
+ export { SyncInspector } from './SyncInspector.js';
131
+ export type { SyncInspectorProps } from './SyncInspector.js';
132
132
  export * from './auth.js';
133
+ export * from './backups.js';