local-lite 0.0.9 → 0.0.11
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 +42 -40
- package/dist/adapters/remote/demo.d.ts +10 -0
- package/dist/adapters/remote/demo.js +21 -1
- package/dist/backups/cloud-storage.d.ts +61 -0
- package/dist/backups/cloud-storage.js +295 -0
- package/dist/backups/index.d.ts +14 -0
- package/dist/backups/index.js +32 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +2 -1
- package/dist/config/resolve-local.js +5 -11
- package/dist/config/resolve-remote.d.ts +1 -1
- package/dist/config/resolve-remote.js +16 -19
- package/dist/config/types.d.ts +7 -0
- package/dist/core/sync-worker.js +1 -0
- package/dist/utils/dynamic.d.ts +6 -0
- package/dist/utils/dynamic.js +32 -0
- package/package.json +150 -144
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Built around the **Transactional Outbox Pattern**, `local-lite` ensures instant,
|
|
|
19
19
|
* **[Configuration Reference](https://georgechitechi.github.io/local-lite/#/configuration)** - Options for `defineSyncConfig()`, retries, and loggers
|
|
20
20
|
* **[Local SQLite Adapters](https://georgechitechi.github.io/local-lite/#/adapters-local)** - Expo SQLite, OP-SQLite, better-sqlite3, node:sqlite, and LibSQL
|
|
21
21
|
* **[Remote Cloud Adapters](https://georgechitechi.github.io/local-lite/#/adapters-remote)** - Supabase, Convex, Cloud Firestore, and custom adapters
|
|
22
|
+
* **[Database Backups](https://georgechitechi.github.io/local-lite/#/adapters-local?id=database-backups)** - SQLite snapshots with a Google Drive OAuth/PKCE client
|
|
22
23
|
* **[React & Expo Hooks](https://georgechitechi.github.io/local-lite/#/react-hooks)** - `useLiveQuery()` and `useSyncStatus()`
|
|
23
24
|
* **[Conflict Resolution](https://georgechitechi.github.io/local-lite/#/conflict-resolution)** - LWW, Client-Wins, Server-Wins, and custom resolvers
|
|
24
25
|
|
|
@@ -85,6 +86,7 @@ This recovers sync state; it does not repair corrupt SQLite files or cancel hung
|
|
|
85
86
|
* **🛡️ Conflict Resolution:** Built-in strategies including **Last-Write-Wins (LWW)**, Server-Wins, Client-Wins, or custom resolver functions.
|
|
86
87
|
* **📶 Network-Aware:** Automatically pauses polling when offline and triggers immediate sync flushes the instant connectivity is restored.
|
|
87
88
|
* **🔄 Infinite-Loop Safe:** Incoming remote deltas update local tables and fire reactive UI listeners without echoing back into the outbox.
|
|
89
|
+
* **☁️ Database Backups:** Export and restore SQLite snapshots independently of your live sync backend.
|
|
88
90
|
|
|
89
91
|
---
|
|
90
92
|
|
|
@@ -246,46 +248,46 @@ export function TodoScreen() {
|
|
|
246
248
|
|
|
247
249
|
## Supabase Backend Setup & Schema
|
|
248
250
|
|
|
249
|
-
To synchronize with Supabase using zero-clock-skew delta queries and `deleted: true` soft deletes, execute the following script in your Supabase project (**Dashboard → SQL Editor**):
|
|
250
|
-
|
|
251
|
-
```sql
|
|
252
|
-
-- Ensure every application table has the soft-delete column expected by local-lite.
|
|
253
|
-
DO $$
|
|
254
|
-
DECLARE
|
|
255
|
-
table_record RECORD;
|
|
256
|
-
BEGIN
|
|
257
|
-
FOR table_record IN
|
|
258
|
-
SELECT table_schema, table_name
|
|
259
|
-
FROM information_schema.tables
|
|
260
|
-
WHERE table_schema = 'public'
|
|
261
|
-
AND table_type = 'BASE TABLE'
|
|
262
|
-
LOOP
|
|
263
|
-
EXECUTE format(
|
|
264
|
-
'ALTER TABLE %I.%I ADD COLUMN IF NOT EXISTS deleted BOOLEAN',
|
|
265
|
-
table_record.table_schema,
|
|
266
|
-
table_record.table_name
|
|
267
|
-
);
|
|
268
|
-
EXECUTE format(
|
|
269
|
-
'UPDATE %I.%I SET deleted = FALSE WHERE deleted IS NULL',
|
|
270
|
-
table_record.table_schema,
|
|
271
|
-
table_record.table_name
|
|
272
|
-
);
|
|
273
|
-
EXECUTE format(
|
|
274
|
-
'ALTER TABLE %I.%I ALTER COLUMN deleted SET DEFAULT FALSE',
|
|
275
|
-
table_record.table_schema,
|
|
276
|
-
table_record.table_name
|
|
277
|
-
);
|
|
278
|
-
EXECUTE format(
|
|
279
|
-
'ALTER TABLE %I.%I ALTER COLUMN deleted SET NOT NULL',
|
|
280
|
-
table_record.table_schema,
|
|
281
|
-
table_record.table_name
|
|
282
|
-
);
|
|
283
|
-
END LOOP;
|
|
284
|
-
END $$;
|
|
285
|
-
|
|
286
|
-
-- 1. Automatic Timestamp Trigger
|
|
287
|
-
CREATE OR REPLACE FUNCTION public.set_sync_updated_at()
|
|
288
|
-
RETURNS TRIGGER AS $$
|
|
251
|
+
To synchronize with Supabase using zero-clock-skew delta queries and `deleted: true` soft deletes, execute the following script in your Supabase project (**Dashboard → SQL Editor**):
|
|
252
|
+
|
|
253
|
+
```sql
|
|
254
|
+
-- Ensure every application table has the soft-delete column expected by local-lite.
|
|
255
|
+
DO $$
|
|
256
|
+
DECLARE
|
|
257
|
+
table_record RECORD;
|
|
258
|
+
BEGIN
|
|
259
|
+
FOR table_record IN
|
|
260
|
+
SELECT table_schema, table_name
|
|
261
|
+
FROM information_schema.tables
|
|
262
|
+
WHERE table_schema = 'public'
|
|
263
|
+
AND table_type = 'BASE TABLE'
|
|
264
|
+
LOOP
|
|
265
|
+
EXECUTE format(
|
|
266
|
+
'ALTER TABLE %I.%I ADD COLUMN IF NOT EXISTS deleted BOOLEAN',
|
|
267
|
+
table_record.table_schema,
|
|
268
|
+
table_record.table_name
|
|
269
|
+
);
|
|
270
|
+
EXECUTE format(
|
|
271
|
+
'UPDATE %I.%I SET deleted = FALSE WHERE deleted IS NULL',
|
|
272
|
+
table_record.table_schema,
|
|
273
|
+
table_record.table_name
|
|
274
|
+
);
|
|
275
|
+
EXECUTE format(
|
|
276
|
+
'ALTER TABLE %I.%I ALTER COLUMN deleted SET DEFAULT FALSE',
|
|
277
|
+
table_record.table_schema,
|
|
278
|
+
table_record.table_name
|
|
279
|
+
);
|
|
280
|
+
EXECUTE format(
|
|
281
|
+
'ALTER TABLE %I.%I ALTER COLUMN deleted SET NOT NULL',
|
|
282
|
+
table_record.table_schema,
|
|
283
|
+
table_record.table_name
|
|
284
|
+
);
|
|
285
|
+
END LOOP;
|
|
286
|
+
END $$;
|
|
287
|
+
|
|
288
|
+
-- 1. Automatic Timestamp Trigger
|
|
289
|
+
CREATE OR REPLACE FUNCTION public.set_sync_updated_at()
|
|
290
|
+
RETURNS TRIGGER AS $$
|
|
289
291
|
BEGIN
|
|
290
292
|
NEW.updated_at = timezone('utc'::text, now());
|
|
291
293
|
RETURN NEW;
|
|
@@ -19,3 +19,13 @@ export declare class MemoryDemoRemoteAdapter implements RemoteDBAdapter {
|
|
|
19
19
|
emitLiveChanges(changes: DeltaChanges): void;
|
|
20
20
|
clear(): void;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Built-in No-Op Remote Adapter for local-only / offline operation.
|
|
24
|
+
* Acknowledges all local outbox pushes with 0 latency, performs no remote pulls,
|
|
25
|
+
* and maintains zero cloud backend connections.
|
|
26
|
+
*/
|
|
27
|
+
export declare class NoopRemoteAdapter implements RemoteDBAdapter {
|
|
28
|
+
pushChanges(_operations: SyncOperation[]): Promise<boolean>;
|
|
29
|
+
fetchChanges(_sinceTimestamp: number): Promise<DeltaChanges>;
|
|
30
|
+
subscribeToLiveChanges(_callback: (changes: DeltaChanges) => void): () => void;
|
|
31
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MemoryDemoRemoteAdapter = void 0;
|
|
3
|
+
exports.NoopRemoteAdapter = exports.MemoryDemoRemoteAdapter = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Built-in In-Memory Demo Remote Adapter.
|
|
6
6
|
* Useful for prototyping, offline testing, and previewing without a live backend.
|
|
@@ -47,3 +47,23 @@ class MemoryDemoRemoteAdapter {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
exports.MemoryDemoRemoteAdapter = MemoryDemoRemoteAdapter;
|
|
50
|
+
/**
|
|
51
|
+
* Built-in No-Op Remote Adapter for local-only / offline operation.
|
|
52
|
+
* Acknowledges all local outbox pushes with 0 latency, performs no remote pulls,
|
|
53
|
+
* and maintains zero cloud backend connections.
|
|
54
|
+
*/
|
|
55
|
+
class NoopRemoteAdapter {
|
|
56
|
+
async pushChanges(_operations) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
async fetchChanges(_sinceTimestamp) {
|
|
60
|
+
return {
|
|
61
|
+
timestamp: Date.now(),
|
|
62
|
+
changes: [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
subscribeToLiveChanges(_callback) {
|
|
66
|
+
return () => { };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.NoopRemoteAdapter = NoopRemoteAdapter;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { BackupStorage } from './index.js';
|
|
2
|
+
export interface CloudBackupFile {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
modifiedAt?: string;
|
|
6
|
+
size?: number;
|
|
7
|
+
}
|
|
8
|
+
export type BackupAccessTokenProvider = () => Promise<string>;
|
|
9
|
+
export interface GoogleDriveTokens {
|
|
10
|
+
accessToken: string;
|
|
11
|
+
refreshToken?: string;
|
|
12
|
+
expiresAt: number;
|
|
13
|
+
}
|
|
14
|
+
export interface GoogleDriveTokenStore {
|
|
15
|
+
get(): Promise<GoogleDriveTokens | null>;
|
|
16
|
+
set(tokens: GoogleDriveTokens): Promise<void>;
|
|
17
|
+
remove(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export interface GoogleDriveKeyValueStorage {
|
|
20
|
+
getItem(key: string): Promise<unknown>;
|
|
21
|
+
setItem(key: string, value: unknown): Promise<void>;
|
|
22
|
+
removeItem(key: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export interface GoogleDriveAuthorizationRequest {
|
|
25
|
+
authorizationUrl: string;
|
|
26
|
+
redirectUri: string;
|
|
27
|
+
}
|
|
28
|
+
export interface GoogleDriveAuthorizationResponse {
|
|
29
|
+
code: string;
|
|
30
|
+
state: string;
|
|
31
|
+
}
|
|
32
|
+
export interface GoogleDriveBackupClientOptions {
|
|
33
|
+
clientId: string;
|
|
34
|
+
redirectUri: string;
|
|
35
|
+
authorize: (request: GoogleDriveAuthorizationRequest) => Promise<GoogleDriveAuthorizationResponse | null>;
|
|
36
|
+
tokenStore: GoogleDriveTokenStore;
|
|
37
|
+
randomBytes?: (length: number) => Uint8Array;
|
|
38
|
+
fetch?: typeof fetch;
|
|
39
|
+
}
|
|
40
|
+
export declare function createGoogleDriveTokenStore(storage: GoogleDriveKeyValueStorage, key?: string): GoogleDriveTokenStore;
|
|
41
|
+
export declare class GoogleDriveBackupStorage implements BackupStorage {
|
|
42
|
+
private readonly getAccessToken;
|
|
43
|
+
private readonly fetcher;
|
|
44
|
+
constructor(getAccessToken: BackupAccessTokenProvider, fetcher?: typeof fetch);
|
|
45
|
+
save(fileName: string, data: Uint8Array): Promise<void>;
|
|
46
|
+
read(fileId: string): Promise<Uint8Array>;
|
|
47
|
+
list(): Promise<CloudBackupFile[]>;
|
|
48
|
+
}
|
|
49
|
+
export declare class GoogleDriveBackupClient implements BackupStorage {
|
|
50
|
+
private readonly options;
|
|
51
|
+
private readonly storage;
|
|
52
|
+
private readonly fetcher;
|
|
53
|
+
constructor(options: GoogleDriveBackupClientOptions);
|
|
54
|
+
connect(): Promise<boolean>;
|
|
55
|
+
disconnect(): Promise<void>;
|
|
56
|
+
isConnected(): Promise<boolean>;
|
|
57
|
+
getValidAccessToken(): Promise<string>;
|
|
58
|
+
save(fileName: string, data: Uint8Array): Promise<void>;
|
|
59
|
+
read(fileId: string): Promise<Uint8Array>;
|
|
60
|
+
list(): Promise<CloudBackupFile[]>;
|
|
61
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GoogleDriveBackupClient = exports.GoogleDriveBackupStorage = void 0;
|
|
4
|
+
exports.createGoogleDriveTokenStore = createGoogleDriveTokenStore;
|
|
5
|
+
const sha2_js_1 = require("@noble/hashes/sha2.js");
|
|
6
|
+
const utils_js_1 = require("@noble/hashes/utils.js");
|
|
7
|
+
const GOOGLE_API = 'https://www.googleapis.com/drive/v3';
|
|
8
|
+
const GOOGLE_UPLOAD_API = 'https://www.googleapis.com/upload/drive/v3';
|
|
9
|
+
const GOOGLE_AUTHORIZATION_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
10
|
+
const GOOGLE_TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token';
|
|
11
|
+
const GOOGLE_DRIVE_SCOPE = 'https://www.googleapis.com/auth/drive.file';
|
|
12
|
+
const BACKUP_FOLDER = 'backups';
|
|
13
|
+
function secureRandomBytes(length) {
|
|
14
|
+
const crypto = globalThis.crypto;
|
|
15
|
+
if (!crypto?.getRandomValues) {
|
|
16
|
+
throw new Error('Secure random number generation is unavailable.');
|
|
17
|
+
}
|
|
18
|
+
return crypto.getRandomValues(new Uint8Array(length));
|
|
19
|
+
}
|
|
20
|
+
function base64UrlEncode(bytes) {
|
|
21
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
22
|
+
let result = '';
|
|
23
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
24
|
+
const first = bytes[index];
|
|
25
|
+
const second = bytes[index + 1];
|
|
26
|
+
const third = bytes[index + 2];
|
|
27
|
+
result += alphabet[first >> 2];
|
|
28
|
+
result += alphabet[((first & 3) << 4) | (second === undefined ? 0 : second >> 4)];
|
|
29
|
+
if (second !== undefined) {
|
|
30
|
+
result += alphabet[((second & 15) << 2) | (third === undefined ? 0 : third >> 6)];
|
|
31
|
+
}
|
|
32
|
+
if (third !== undefined)
|
|
33
|
+
result += alphabet[third & 63];
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
function validateGoogleTokens(value) {
|
|
38
|
+
if (!value || typeof value !== 'object')
|
|
39
|
+
return false;
|
|
40
|
+
const tokens = value;
|
|
41
|
+
return (typeof tokens.accessToken === 'string' &&
|
|
42
|
+
typeof tokens.expiresAt === 'number' &&
|
|
43
|
+
(tokens.refreshToken === undefined || typeof tokens.refreshToken === 'string'));
|
|
44
|
+
}
|
|
45
|
+
function createGoogleDriveTokenStore(storage, key = 'local-lite-google-drive-backup-tokens') {
|
|
46
|
+
return {
|
|
47
|
+
get: async () => {
|
|
48
|
+
const tokens = await storage.getItem(key);
|
|
49
|
+
return validateGoogleTokens(tokens) ? tokens : null;
|
|
50
|
+
},
|
|
51
|
+
set: tokens => storage.setItem(key, tokens),
|
|
52
|
+
remove: () => storage.removeItem(key),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function parseTokenResponse(response) {
|
|
56
|
+
try {
|
|
57
|
+
return (await response.json());
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new Error(`Google OAuth request failed (${response.status}).`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function createStoredTokens(payload, prior) {
|
|
64
|
+
if (!payload.access_token) {
|
|
65
|
+
throw new Error('Google OAuth did not return an access token.');
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
accessToken: payload.access_token,
|
|
69
|
+
refreshToken: payload.refresh_token ?? prior?.refreshToken,
|
|
70
|
+
expiresAt: Date.now() + (payload.expires_in ?? 3600) * 1000,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function throwTokenError(payload, status) {
|
|
74
|
+
throw new Error(payload.error_description ||
|
|
75
|
+
payload.error ||
|
|
76
|
+
`Google OAuth request failed (${status}).`);
|
|
77
|
+
}
|
|
78
|
+
async function parseApiError(response) {
|
|
79
|
+
let detail = '';
|
|
80
|
+
try {
|
|
81
|
+
const body = (await response.json());
|
|
82
|
+
detail =
|
|
83
|
+
(typeof body.error === 'object' ? body.error?.message : body.error) ??
|
|
84
|
+
body.error_description ??
|
|
85
|
+
body.message ??
|
|
86
|
+
'';
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
detail = response.statusText;
|
|
90
|
+
}
|
|
91
|
+
return new Error(`Google Drive request failed (${response.status})${detail ? `: ${detail}` : ''}`);
|
|
92
|
+
}
|
|
93
|
+
async function getGoogleFolderId(token, fetcher) {
|
|
94
|
+
const query = encodeURIComponent(`name = '${BACKUP_FOLDER}' and mimeType = 'application/vnd.google-apps.folder' and trashed = false`);
|
|
95
|
+
const lookup = await fetcher(`${GOOGLE_API}/files?q=${query}&spaces=drive&fields=files(id,name)&pageSize=100`, { headers: { Authorization: `Bearer ${token}` } });
|
|
96
|
+
if (!lookup.ok)
|
|
97
|
+
throw await parseApiError(lookup);
|
|
98
|
+
const result = (await lookup.json());
|
|
99
|
+
if (result.files?.[0]?.id)
|
|
100
|
+
return result.files[0].id;
|
|
101
|
+
const create = await fetcher(`${GOOGLE_API}/files?fields=id`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: {
|
|
104
|
+
Authorization: `Bearer ${token}`,
|
|
105
|
+
'Content-Type': 'application/json',
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
name: BACKUP_FOLDER,
|
|
109
|
+
mimeType: 'application/vnd.google-apps.folder',
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
if (!create.ok)
|
|
113
|
+
throw await parseApiError(create);
|
|
114
|
+
const folder = (await create.json());
|
|
115
|
+
if (!folder.id)
|
|
116
|
+
throw new Error('Google Drive did not return a backup folder ID.');
|
|
117
|
+
return folder.id;
|
|
118
|
+
}
|
|
119
|
+
class GoogleDriveBackupStorage {
|
|
120
|
+
getAccessToken;
|
|
121
|
+
fetcher;
|
|
122
|
+
constructor(getAccessToken, fetcher = fetch) {
|
|
123
|
+
this.getAccessToken = getAccessToken;
|
|
124
|
+
this.fetcher = fetcher;
|
|
125
|
+
}
|
|
126
|
+
async save(fileName, data) {
|
|
127
|
+
const token = await this.getAccessToken();
|
|
128
|
+
const folderId = await getGoogleFolderId(token, this.fetcher);
|
|
129
|
+
const session = await this.fetcher(`${GOOGLE_UPLOAD_API}/files?uploadType=resumable&fields=id`, {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: {
|
|
132
|
+
Authorization: `Bearer ${token}`,
|
|
133
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
|
134
|
+
'X-Upload-Content-Type': 'application/octet-stream',
|
|
135
|
+
'X-Upload-Content-Length': String(data.byteLength),
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
name: fileName,
|
|
139
|
+
mimeType: 'application/octet-stream',
|
|
140
|
+
parents: [folderId],
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
if (!session.ok)
|
|
144
|
+
throw await parseApiError(session);
|
|
145
|
+
const uploadUrl = session.headers.get('Location');
|
|
146
|
+
if (!uploadUrl) {
|
|
147
|
+
throw new Error('Google Drive did not return a resumable upload URL.');
|
|
148
|
+
}
|
|
149
|
+
const uploaded = await this.fetcher(uploadUrl, {
|
|
150
|
+
method: 'PUT',
|
|
151
|
+
headers: {
|
|
152
|
+
Authorization: `Bearer ${token}`,
|
|
153
|
+
'Content-Type': 'application/octet-stream',
|
|
154
|
+
'Content-Range': `bytes 0-${data.byteLength - 1}/${data.byteLength}`,
|
|
155
|
+
},
|
|
156
|
+
body: data.slice().buffer,
|
|
157
|
+
});
|
|
158
|
+
if (!uploaded.ok)
|
|
159
|
+
throw await parseApiError(uploaded);
|
|
160
|
+
}
|
|
161
|
+
async read(fileId) {
|
|
162
|
+
const token = await this.getAccessToken();
|
|
163
|
+
const response = await this.fetcher(`${GOOGLE_API}/files/${encodeURIComponent(fileId)}?alt=media`, { headers: { Authorization: `Bearer ${token}` } });
|
|
164
|
+
if (!response.ok)
|
|
165
|
+
throw await parseApiError(response);
|
|
166
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
167
|
+
}
|
|
168
|
+
async list() {
|
|
169
|
+
const token = await this.getAccessToken();
|
|
170
|
+
const folderId = await getGoogleFolderId(token, this.fetcher);
|
|
171
|
+
const query = encodeURIComponent(`'${folderId}' in parents and trashed = false`);
|
|
172
|
+
const response = await this.fetcher(`${GOOGLE_API}/files?q=${query}&fields=files(id,name,size,modifiedTime)&orderBy=modifiedTime%20desc&pageSize=100`, { headers: { Authorization: `Bearer ${token}` } });
|
|
173
|
+
if (!response.ok)
|
|
174
|
+
throw await parseApiError(response);
|
|
175
|
+
const result = (await response.json());
|
|
176
|
+
return (result.files ?? [])
|
|
177
|
+
.filter(file => file.name.endsWith('.db'))
|
|
178
|
+
.map(file => ({
|
|
179
|
+
id: file.id,
|
|
180
|
+
name: file.name,
|
|
181
|
+
modifiedAt: file.modifiedTime,
|
|
182
|
+
size: file.size ? Number(file.size) : undefined,
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
exports.GoogleDriveBackupStorage = GoogleDriveBackupStorage;
|
|
187
|
+
class GoogleDriveBackupClient {
|
|
188
|
+
options;
|
|
189
|
+
storage;
|
|
190
|
+
fetcher;
|
|
191
|
+
constructor(options) {
|
|
192
|
+
this.options = options;
|
|
193
|
+
if (!options.clientId.trim())
|
|
194
|
+
throw new Error('Google OAuth client ID is required.');
|
|
195
|
+
if (!options.redirectUri.trim())
|
|
196
|
+
throw new Error('Google OAuth redirect URI is required.');
|
|
197
|
+
this.fetcher = options.fetch ?? fetch;
|
|
198
|
+
this.storage = new GoogleDriveBackupStorage(() => this.getValidAccessToken(), this.fetcher);
|
|
199
|
+
}
|
|
200
|
+
async connect() {
|
|
201
|
+
const randomBytes = this.options.randomBytes ?? secureRandomBytes;
|
|
202
|
+
const verifierBytes = randomBytes(32);
|
|
203
|
+
const stateBytes = randomBytes(32);
|
|
204
|
+
if (verifierBytes.length !== 32 || stateBytes.length !== 32) {
|
|
205
|
+
throw new Error('The randomBytes provider returned an invalid number of bytes.');
|
|
206
|
+
}
|
|
207
|
+
const codeVerifier = base64UrlEncode(verifierBytes);
|
|
208
|
+
const codeChallenge = base64UrlEncode((0, sha2_js_1.sha256)((0, utils_js_1.utf8ToBytes)(codeVerifier)));
|
|
209
|
+
const state = base64UrlEncode(stateBytes);
|
|
210
|
+
const authorizationParams = new URLSearchParams({
|
|
211
|
+
client_id: this.options.clientId,
|
|
212
|
+
redirect_uri: this.options.redirectUri,
|
|
213
|
+
response_type: 'code',
|
|
214
|
+
scope: GOOGLE_DRIVE_SCOPE,
|
|
215
|
+
access_type: 'offline',
|
|
216
|
+
prompt: 'consent',
|
|
217
|
+
code_challenge: codeChallenge,
|
|
218
|
+
code_challenge_method: 'S256',
|
|
219
|
+
state,
|
|
220
|
+
});
|
|
221
|
+
const authorization = await this.options.authorize({
|
|
222
|
+
authorizationUrl: `${GOOGLE_AUTHORIZATION_ENDPOINT}?${authorizationParams}`,
|
|
223
|
+
redirectUri: this.options.redirectUri,
|
|
224
|
+
});
|
|
225
|
+
if (!authorization)
|
|
226
|
+
return false;
|
|
227
|
+
if (authorization.state !== state) {
|
|
228
|
+
throw new Error('Google OAuth state validation failed. Please try connecting again.');
|
|
229
|
+
}
|
|
230
|
+
if (!authorization.code) {
|
|
231
|
+
throw new Error('Google OAuth did not return an authorization code.');
|
|
232
|
+
}
|
|
233
|
+
const response = await this.fetcher(GOOGLE_TOKEN_ENDPOINT, {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
236
|
+
body: new URLSearchParams({
|
|
237
|
+
client_id: this.options.clientId,
|
|
238
|
+
code: authorization.code,
|
|
239
|
+
code_verifier: codeVerifier,
|
|
240
|
+
grant_type: 'authorization_code',
|
|
241
|
+
redirect_uri: this.options.redirectUri,
|
|
242
|
+
}).toString(),
|
|
243
|
+
});
|
|
244
|
+
const payload = await parseTokenResponse(response);
|
|
245
|
+
if (!response.ok)
|
|
246
|
+
throwTokenError(payload, response.status);
|
|
247
|
+
const prior = await this.options.tokenStore.get();
|
|
248
|
+
await this.options.tokenStore.set(createStoredTokens(payload, prior));
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
async disconnect() {
|
|
252
|
+
await this.options.tokenStore.remove();
|
|
253
|
+
}
|
|
254
|
+
async isConnected() {
|
|
255
|
+
const tokens = await this.options.tokenStore.get();
|
|
256
|
+
return validateGoogleTokens(tokens);
|
|
257
|
+
}
|
|
258
|
+
async getValidAccessToken() {
|
|
259
|
+
const tokens = await this.options.tokenStore.get();
|
|
260
|
+
if (!validateGoogleTokens(tokens)) {
|
|
261
|
+
throw new Error('Connect Google Drive before using cloud backups.');
|
|
262
|
+
}
|
|
263
|
+
if (tokens.expiresAt > Date.now() + 60_000)
|
|
264
|
+
return tokens.accessToken;
|
|
265
|
+
if (!tokens.refreshToken) {
|
|
266
|
+
throw new Error('Your Google Drive sign-in expired. Disconnect and connect the account again.');
|
|
267
|
+
}
|
|
268
|
+
const response = await this.fetcher(GOOGLE_TOKEN_ENDPOINT, {
|
|
269
|
+
method: 'POST',
|
|
270
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
271
|
+
body: new URLSearchParams({
|
|
272
|
+
client_id: this.options.clientId,
|
|
273
|
+
grant_type: 'refresh_token',
|
|
274
|
+
refresh_token: tokens.refreshToken,
|
|
275
|
+
scope: GOOGLE_DRIVE_SCOPE,
|
|
276
|
+
}).toString(),
|
|
277
|
+
});
|
|
278
|
+
const payload = await parseTokenResponse(response);
|
|
279
|
+
if (!response.ok)
|
|
280
|
+
throwTokenError(payload, response.status);
|
|
281
|
+
const updatedTokens = createStoredTokens(payload, tokens);
|
|
282
|
+
await this.options.tokenStore.set(updatedTokens);
|
|
283
|
+
return updatedTokens.accessToken;
|
|
284
|
+
}
|
|
285
|
+
save(fileName, data) {
|
|
286
|
+
return this.storage.save(fileName, data);
|
|
287
|
+
}
|
|
288
|
+
read(fileId) {
|
|
289
|
+
return this.storage.read(fileId);
|
|
290
|
+
}
|
|
291
|
+
list() {
|
|
292
|
+
return this.storage.list();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
exports.GoogleDriveBackupClient = GoogleDriveBackupClient;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { type BackupAccessTokenProvider, type CloudBackupFile, type GoogleDriveAuthorizationRequest, type GoogleDriveAuthorizationResponse, type GoogleDriveBackupClientOptions, type GoogleDriveKeyValueStorage, type GoogleDriveTokenStore, type GoogleDriveTokens, GoogleDriveBackupClient, GoogleDriveBackupStorage, createGoogleDriveTokenStore, } from './cloud-storage.js';
|
|
2
|
+
export interface BackupDatabase {
|
|
3
|
+
exportDatabase(): Promise<Uint8Array>;
|
|
4
|
+
importDatabase(data: Uint8Array): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export interface BackupStorage {
|
|
7
|
+
save(fileName: string, data: Uint8Array): Promise<void>;
|
|
8
|
+
read(fileName: string): Promise<Uint8Array>;
|
|
9
|
+
}
|
|
10
|
+
export interface CreateDatabaseBackupOptions {
|
|
11
|
+
fileName?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function createDatabaseBackup(database: BackupDatabase, storage: BackupStorage, options?: CreateDatabaseBackupOptions): Promise<string>;
|
|
14
|
+
export declare function restoreDatabaseBackup(database: BackupDatabase, storage: BackupStorage, fileName: string): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createGoogleDriveTokenStore = exports.GoogleDriveBackupStorage = exports.GoogleDriveBackupClient = void 0;
|
|
4
|
+
exports.createDatabaseBackup = createDatabaseBackup;
|
|
5
|
+
exports.restoreDatabaseBackup = restoreDatabaseBackup;
|
|
6
|
+
var cloud_storage_js_1 = require("./cloud-storage.js");
|
|
7
|
+
Object.defineProperty(exports, "GoogleDriveBackupClient", { enumerable: true, get: function () { return cloud_storage_js_1.GoogleDriveBackupClient; } });
|
|
8
|
+
Object.defineProperty(exports, "GoogleDriveBackupStorage", { enumerable: true, get: function () { return cloud_storage_js_1.GoogleDriveBackupStorage; } });
|
|
9
|
+
Object.defineProperty(exports, "createGoogleDriveTokenStore", { enumerable: true, get: function () { return cloud_storage_js_1.createGoogleDriveTokenStore; } });
|
|
10
|
+
function validateFileName(fileName) {
|
|
11
|
+
if (!fileName || /[\\/]/.test(fileName)) {
|
|
12
|
+
throw new Error('Backup file name must be a single file name.');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function createDatabaseBackup(database, storage, options = {}) {
|
|
16
|
+
const fileName = options.fileName ?? `local-lite-${new Date().toISOString().replace(/[:.]/g, '-')}.db`;
|
|
17
|
+
validateFileName(fileName);
|
|
18
|
+
const databaseBytes = await database.exportDatabase();
|
|
19
|
+
if (databaseBytes.length === 0) {
|
|
20
|
+
throw new Error('Cannot create a backup from an empty database.');
|
|
21
|
+
}
|
|
22
|
+
await storage.save(fileName, databaseBytes);
|
|
23
|
+
return fileName;
|
|
24
|
+
}
|
|
25
|
+
async function restoreDatabaseBackup(database, storage, fileName) {
|
|
26
|
+
validateFileName(fileName);
|
|
27
|
+
const databaseBytes = await storage.read(fileName);
|
|
28
|
+
if (databaseBytes.length === 0) {
|
|
29
|
+
throw new Error('Cannot restore an empty database backup.');
|
|
30
|
+
}
|
|
31
|
+
await database.importDatabase(databaseBytes);
|
|
32
|
+
}
|
package/dist/config/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { NodeSQLiteAdapter } from '../adapters/local/node-sqlite.js';
|
|
|
13
13
|
export { SupabaseRemoteAdapter } from '../adapters/remote/supabase.js';
|
|
14
14
|
export { ConvexRemoteAdapter } from '../adapters/remote/convex.js';
|
|
15
15
|
export { FirestoreRemoteAdapter } from '../adapters/remote/firestore.js';
|
|
16
|
-
export { MemoryDemoRemoteAdapter } from '../adapters/remote/demo.js';
|
|
16
|
+
export { MemoryDemoRemoteAdapter, NoopRemoteAdapter } from '../adapters/remote/demo.js';
|
|
17
17
|
/**
|
|
18
18
|
* Type-safe configuration helper function (similar to defineConfig in Vite/Tailwind).
|
|
19
19
|
*/
|
package/dist/config/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.MemoryDemoRemoteAdapter = exports.FirestoreRemoteAdapter = exports.ConvexRemoteAdapter = exports.SupabaseRemoteAdapter = exports.NodeSQLiteAdapter = exports.SqlJsAdapter = exports.LibSQLAdapter = exports.BetterSQLite3Adapter = exports.OpSQLiteAdapter = exports.ExpoSQLiteAdapter = exports.LocalFirstClient = exports.resolveRemoteAdapter = exports.resolveLocalAdapter = void 0;
|
|
17
|
+
exports.NoopRemoteAdapter = exports.MemoryDemoRemoteAdapter = exports.FirestoreRemoteAdapter = exports.ConvexRemoteAdapter = exports.SupabaseRemoteAdapter = exports.NodeSQLiteAdapter = exports.SqlJsAdapter = exports.LibSQLAdapter = exports.BetterSQLite3Adapter = exports.OpSQLiteAdapter = exports.ExpoSQLiteAdapter = exports.LocalFirstClient = exports.resolveRemoteAdapter = exports.resolveLocalAdapter = void 0;
|
|
18
18
|
exports.defineSyncConfig = defineSyncConfig;
|
|
19
19
|
exports.createSyncClient = createSyncClient;
|
|
20
20
|
const client_js_1 = require("../core/client.js");
|
|
@@ -51,6 +51,7 @@ var firestore_js_1 = require("../adapters/remote/firestore.js");
|
|
|
51
51
|
Object.defineProperty(exports, "FirestoreRemoteAdapter", { enumerable: true, get: function () { return firestore_js_1.FirestoreRemoteAdapter; } });
|
|
52
52
|
var demo_js_1 = require("../adapters/remote/demo.js");
|
|
53
53
|
Object.defineProperty(exports, "MemoryDemoRemoteAdapter", { enumerable: true, get: function () { return demo_js_1.MemoryDemoRemoteAdapter; } });
|
|
54
|
+
Object.defineProperty(exports, "NoopRemoteAdapter", { enumerable: true, get: function () { return demo_js_1.NoopRemoteAdapter; } });
|
|
54
55
|
/**
|
|
55
56
|
* Type-safe configuration helper function (similar to defineConfig in Vite/Tailwind).
|
|
56
57
|
*/
|
|
@@ -7,6 +7,8 @@ const better_sqlite3_js_1 = require("../adapters/local/better-sqlite3.js");
|
|
|
7
7
|
const libsql_js_1 = require("../adapters/local/libsql.js");
|
|
8
8
|
const sql_js_js_1 = require("../adapters/local/sql-js.js");
|
|
9
9
|
const node_sqlite_js_1 = require("../adapters/local/node-sqlite.js");
|
|
10
|
+
const dynamic_js_1 = require("../utils/dynamic.js");
|
|
11
|
+
const EXPO_SQLITE_PKG = ['expo', 'sqlite'].join('-');
|
|
10
12
|
/**
|
|
11
13
|
* Resolves or instantiates the appropriate LocalDBAdapter based on the provided configuration.
|
|
12
14
|
* Auto-detects Expo SQLite, OP-SQLite, better-sqlite3, LibSQL, sql.js, and Node.js built-in sqlite.
|
|
@@ -51,19 +53,11 @@ async function resolveLocalAdapter(config) {
|
|
|
51
53
|
// 9. Auto-open by name using expo-sqlite if available
|
|
52
54
|
if (typeof rawDb === 'string' || config.databaseName) {
|
|
53
55
|
const dbName = typeof rawDb === 'string' ? rawDb : (config.databaseName ?? 'local_lite.db');
|
|
54
|
-
let expoSqlite;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
expoSqlite = require('expo-sqlite');
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
// ignore
|
|
56
|
+
let expoSqlite = (0, dynamic_js_1.safeDynamicRequire)(EXPO_SQLITE_PKG);
|
|
57
|
+
if (!expoSqlite) {
|
|
58
|
+
expoSqlite = await (0, dynamic_js_1.safeDynamicImport)(EXPO_SQLITE_PKG);
|
|
62
59
|
}
|
|
63
60
|
try {
|
|
64
|
-
if (!expoSqlite) {
|
|
65
|
-
expoSqlite = await import('expo-sqlite');
|
|
66
|
-
}
|
|
67
61
|
if (expoSqlite && typeof expoSqlite.openDatabaseAsync === 'function') {
|
|
68
62
|
const expoDb = await expoSqlite.openDatabaseAsync(dbName);
|
|
69
63
|
return new expo_sqlite_js_1.ExpoSQLiteAdapter(expoDb);
|
|
@@ -2,6 +2,6 @@ import { RemoteDBAdapter } from '../adapters/remote/index.js';
|
|
|
2
2
|
import { LocalLiteConfig } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Resolves or instantiates the appropriate RemoteDBAdapter based on the provided configuration.
|
|
5
|
-
* Supports Supabase, Convex, Firebase / Firestore, in-memory demo adapter, or custom adapters.
|
|
5
|
+
* Supports Supabase, Convex, Firebase / Firestore, in-memory demo adapter, local-only mode, or custom adapters.
|
|
6
6
|
*/
|
|
7
7
|
export declare function resolveRemoteAdapter(config: LocalLiteConfig): Promise<RemoteDBAdapter>;
|
|
@@ -5,9 +5,13 @@ const supabase_js_1 = require("../adapters/remote/supabase.js");
|
|
|
5
5
|
const convex_js_1 = require("../adapters/remote/convex.js");
|
|
6
6
|
const firestore_js_1 = require("../adapters/remote/firestore.js");
|
|
7
7
|
const demo_js_1 = require("../adapters/remote/demo.js");
|
|
8
|
+
const dynamic_js_1 = require("../utils/dynamic.js");
|
|
9
|
+
// Construct dynamically to prevent Metro, Webpack, and Rollup from treating
|
|
10
|
+
// optional peer dependencies as unconditional build-time dependencies.
|
|
11
|
+
const SUPABASE_PKG = ['@supabase', 'supabase-js'].join('/');
|
|
8
12
|
/**
|
|
9
13
|
* Resolves or instantiates the appropriate RemoteDBAdapter based on the provided configuration.
|
|
10
|
-
* Supports Supabase, Convex, Firebase / Firestore, in-memory demo adapter, or custom adapters.
|
|
14
|
+
* Supports Supabase, Convex, Firebase / Firestore, in-memory demo adapter, local-only mode, or custom adapters.
|
|
11
15
|
*/
|
|
12
16
|
async function resolveRemoteAdapter(config) {
|
|
13
17
|
// 1. Explicit custom remote adapter
|
|
@@ -53,34 +57,27 @@ async function resolveRemoteAdapter(config) {
|
|
|
53
57
|
}
|
|
54
58
|
// 5. Supabase with URL + anonKey (lazy dynamic import)
|
|
55
59
|
if (config.supabase?.url && config.supabase?.anonKey) {
|
|
56
|
-
let supabaseModule;
|
|
57
|
-
// In CommonJS / React Native Metro environments, synchronous require avoids async chunk split failure
|
|
58
|
-
try {
|
|
59
|
-
if (typeof require === 'function') {
|
|
60
|
-
supabaseModule = require('@supabase/supabase-js');
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
// ignore
|
|
65
|
-
}
|
|
60
|
+
let supabaseModule = (0, dynamic_js_1.safeDynamicRequire)(SUPABASE_PKG);
|
|
66
61
|
if (!supabaseModule && typeof globalThis?.supabase?.createClient === 'function') {
|
|
67
62
|
supabaseModule = globalThis.supabase;
|
|
68
63
|
}
|
|
69
64
|
if (!supabaseModule) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
throw new Error('To initialize Supabase from url and anonKey, please install @supabase/supabase-js or provide a pre-configured client to config.supabase.client');
|
|
75
|
-
}
|
|
65
|
+
supabaseModule = await (0, dynamic_js_1.safeDynamicImport)(SUPABASE_PKG);
|
|
66
|
+
}
|
|
67
|
+
if (!supabaseModule) {
|
|
68
|
+
throw new Error('To initialize Supabase from url and anonKey, please install @supabase/supabase-js or provide a pre-configured client to config.supabase.client');
|
|
76
69
|
}
|
|
77
70
|
const createClientFn = supabaseModule.createClient || supabaseModule.default?.createClient;
|
|
78
71
|
const supabaseClient = createClientFn(config.supabase.url, config.supabase.anonKey);
|
|
79
72
|
return new supabase_js_1.SupabaseRemoteAdapter(supabaseClient, getSupabaseOptions());
|
|
80
73
|
}
|
|
81
|
-
// 6.
|
|
74
|
+
// 6. Local-only standalone mode
|
|
75
|
+
if (config.localOnly === true) {
|
|
76
|
+
return new demo_js_1.NoopRemoteAdapter();
|
|
77
|
+
}
|
|
78
|
+
// 7. Explicitly selected demo backend only; never acknowledge writes into a temporary backend implicitly.
|
|
82
79
|
if (config.inMemoryRemote === true) {
|
|
83
80
|
return new demo_js_1.MemoryDemoRemoteAdapter(300);
|
|
84
81
|
}
|
|
85
|
-
throw new Error('No remote adapter configured. Please specify config.supabase, config.convex, config.firebase, or config.remote. For
|
|
82
|
+
throw new Error('No remote adapter configured. Please specify config.supabase, config.convex, config.firebase, or config.remote. For local-only use, set config.localOnly: true. For demos, set config.inMemoryRemote: true.');
|
|
86
83
|
}
|
package/dist/config/types.d.ts
CHANGED
|
@@ -120,6 +120,13 @@ export interface LocalLiteConfig {
|
|
|
120
120
|
* Default: false
|
|
121
121
|
*/
|
|
122
122
|
inMemoryRemote?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Run purely local-only without requiring any remote cloud backend.
|
|
125
|
+
* Disables cloud sync pushes and pulls while preserving all local SQLite features,
|
|
126
|
+
* schema management, outbox tracking, and transactions.
|
|
127
|
+
* Default: false
|
|
128
|
+
*/
|
|
129
|
+
localOnly?: boolean;
|
|
123
130
|
/**
|
|
124
131
|
* Conflict resolution strategy. Default: 'last-write-wins'
|
|
125
132
|
*/
|
package/dist/core/sync-worker.js
CHANGED
|
@@ -7,6 +7,7 @@ const outbox_store_js_1 = require("./outbox-store.js");
|
|
|
7
7
|
const meta_store_js_1 = require("./meta-store.js");
|
|
8
8
|
const table_manager_js_1 = require("./table-manager.js");
|
|
9
9
|
const logger_js_1 = require("../utils/logger.js");
|
|
10
|
+
const REACT_NATIVE_PKG = ['react', 'native'].join('-');
|
|
10
11
|
/** Maximum outbox records sent per push cycle. */
|
|
11
12
|
const PUSH_BATCH_SIZE = 50;
|
|
12
13
|
/** Maximum incoming delta changes applied in a single database transaction. */
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safely loads an optional dependency at runtime without triggering
|
|
3
|
+
* ahead-of-time static analysis in bundlers like Metro, Webpack, or Rollup.
|
|
4
|
+
*/
|
|
5
|
+
export declare function safeDynamicRequire(pkgName: string): any;
|
|
6
|
+
export declare function safeDynamicImport(pkgName: string): Promise<any>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.safeDynamicRequire = safeDynamicRequire;
|
|
4
|
+
exports.safeDynamicImport = safeDynamicImport;
|
|
5
|
+
/**
|
|
6
|
+
* Safely loads an optional dependency at runtime without triggering
|
|
7
|
+
* ahead-of-time static analysis in bundlers like Metro, Webpack, or Rollup.
|
|
8
|
+
*/
|
|
9
|
+
function safeDynamicRequire(pkgName) {
|
|
10
|
+
try {
|
|
11
|
+
const req = typeof __non_webpack_require__ !== 'undefined'
|
|
12
|
+
? __non_webpack_require__
|
|
13
|
+
: typeof require === 'function'
|
|
14
|
+
? require
|
|
15
|
+
: undefined;
|
|
16
|
+
return req ? req(pkgName) : undefined;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function safeDynamicImport(pkgName) {
|
|
23
|
+
try {
|
|
24
|
+
// Metro's AST worker rejects `import(dynamicVariable)` with "Invalid call at line ...: import(...)".
|
|
25
|
+
// Evaluating via new Function bypasses the bundler AST parser entirely while working in Node/browser.
|
|
26
|
+
const dynamicImport = new Function('specifier', 'return import(specifier);');
|
|
27
|
+
return await dynamicImport(pkgName);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,144 +1,150 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "local-lite",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Never let a lost connection break a promise. Local-first sync that faithfully keeps data safe until you're back online.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"files": [
|
|
9
|
-
"dist",
|
|
10
|
-
"README.md",
|
|
11
|
-
"LICENSE"
|
|
12
|
-
],
|
|
13
|
-
"exports": {
|
|
14
|
-
".": {
|
|
15
|
-
"types": "./dist/index.d.ts",
|
|
16
|
-
"import": "./dist/index.js",
|
|
17
|
-
"default": "./dist/index.js"
|
|
18
|
-
},
|
|
19
|
-
"./react": {
|
|
20
|
-
"types": "./dist/react/index.d.ts",
|
|
21
|
-
"import": "./dist/react/index.js",
|
|
22
|
-
"default": "./dist/react/index.js"
|
|
23
|
-
},
|
|
24
|
-
"./config": {
|
|
25
|
-
"types": "./dist/config/index.d.ts",
|
|
26
|
-
"import": "./dist/config/index.js",
|
|
27
|
-
"default": "./dist/config/index.js"
|
|
28
|
-
},
|
|
29
|
-
"./schema": {
|
|
30
|
-
"types": "./dist/schema/index.d.ts",
|
|
31
|
-
"import": "./dist/schema/index.js",
|
|
32
|
-
"default": "./dist/schema/index.js"
|
|
33
|
-
},
|
|
34
|
-
"./supabase": {
|
|
35
|
-
"types": "./dist/adapters/remote/supabase.d.ts",
|
|
36
|
-
"import": "./dist/adapters/remote/supabase.js",
|
|
37
|
-
"default": "./dist/adapters/remote/supabase.js"
|
|
38
|
-
},
|
|
39
|
-
"./convex": {
|
|
40
|
-
"types": "./dist/adapters/remote/convex.d.ts",
|
|
41
|
-
"import": "./dist/adapters/remote/convex.js",
|
|
42
|
-
"default": "./dist/adapters/remote/convex.js"
|
|
43
|
-
},
|
|
44
|
-
"./firebase": {
|
|
45
|
-
"types": "./dist/adapters/remote/firestore.d.ts",
|
|
46
|
-
"import": "./dist/adapters/remote/firestore.js",
|
|
47
|
-
"default": "./dist/adapters/remote/firestore.js"
|
|
48
|
-
},
|
|
49
|
-
"./
|
|
50
|
-
"types": "./dist/
|
|
51
|
-
"import": "./dist/
|
|
52
|
-
"default": "./dist/
|
|
53
|
-
},
|
|
54
|
-
"./
|
|
55
|
-
"types": "./dist/adapters/local/
|
|
56
|
-
"import": "./dist/adapters/local/
|
|
57
|
-
"default": "./dist/adapters/local/
|
|
58
|
-
},
|
|
59
|
-
"./
|
|
60
|
-
"types": "./dist/adapters/local/
|
|
61
|
-
"import": "./dist/adapters/local/
|
|
62
|
-
"default": "./dist/adapters/local/
|
|
63
|
-
},
|
|
64
|
-
"./
|
|
65
|
-
"types": "./dist/adapters/local/
|
|
66
|
-
"import": "./dist/adapters/local/
|
|
67
|
-
"default": "./dist/adapters/local/
|
|
68
|
-
},
|
|
69
|
-
"./
|
|
70
|
-
"types": "./dist/adapters/local/
|
|
71
|
-
"import": "./dist/adapters/local/
|
|
72
|
-
"default": "./dist/adapters/local/
|
|
73
|
-
},
|
|
74
|
-
"./
|
|
75
|
-
"types": "./dist/adapters/local/
|
|
76
|
-
"import": "./dist/adapters/local/
|
|
77
|
-
"default": "./dist/adapters/local/
|
|
78
|
-
},
|
|
79
|
-
"./
|
|
80
|
-
"types": "./dist/
|
|
81
|
-
"import": "./dist/
|
|
82
|
-
"default": "./dist/
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
"
|
|
143
|
-
|
|
144
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "local-lite",
|
|
3
|
+
"version": "0.0.11",
|
|
4
|
+
"description": "Never let a lost connection break a promise. Local-first sync that faithfully keeps data safe until you're back online.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./react": {
|
|
20
|
+
"types": "./dist/react/index.d.ts",
|
|
21
|
+
"import": "./dist/react/index.js",
|
|
22
|
+
"default": "./dist/react/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./config": {
|
|
25
|
+
"types": "./dist/config/index.d.ts",
|
|
26
|
+
"import": "./dist/config/index.js",
|
|
27
|
+
"default": "./dist/config/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./schema": {
|
|
30
|
+
"types": "./dist/schema/index.d.ts",
|
|
31
|
+
"import": "./dist/schema/index.js",
|
|
32
|
+
"default": "./dist/schema/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./supabase": {
|
|
35
|
+
"types": "./dist/adapters/remote/supabase.d.ts",
|
|
36
|
+
"import": "./dist/adapters/remote/supabase.js",
|
|
37
|
+
"default": "./dist/adapters/remote/supabase.js"
|
|
38
|
+
},
|
|
39
|
+
"./convex": {
|
|
40
|
+
"types": "./dist/adapters/remote/convex.d.ts",
|
|
41
|
+
"import": "./dist/adapters/remote/convex.js",
|
|
42
|
+
"default": "./dist/adapters/remote/convex.js"
|
|
43
|
+
},
|
|
44
|
+
"./firebase": {
|
|
45
|
+
"types": "./dist/adapters/remote/firestore.d.ts",
|
|
46
|
+
"import": "./dist/adapters/remote/firestore.js",
|
|
47
|
+
"default": "./dist/adapters/remote/firestore.js"
|
|
48
|
+
},
|
|
49
|
+
"./backups": {
|
|
50
|
+
"types": "./dist/backups/index.d.ts",
|
|
51
|
+
"import": "./dist/backups/index.js",
|
|
52
|
+
"default": "./dist/backups/index.js"
|
|
53
|
+
},
|
|
54
|
+
"./expo-sqlite": {
|
|
55
|
+
"types": "./dist/adapters/local/expo-sqlite.d.ts",
|
|
56
|
+
"import": "./dist/adapters/local/expo-sqlite.js",
|
|
57
|
+
"default": "./dist/adapters/local/expo-sqlite.js"
|
|
58
|
+
},
|
|
59
|
+
"./op-sqlite": {
|
|
60
|
+
"types": "./dist/adapters/local/op-sqlite.d.ts",
|
|
61
|
+
"import": "./dist/adapters/local/op-sqlite.js",
|
|
62
|
+
"default": "./dist/adapters/local/op-sqlite.js"
|
|
63
|
+
},
|
|
64
|
+
"./better-sqlite3": {
|
|
65
|
+
"types": "./dist/adapters/local/better-sqlite3.d.ts",
|
|
66
|
+
"import": "./dist/adapters/local/better-sqlite3.js",
|
|
67
|
+
"default": "./dist/adapters/local/better-sqlite3.js"
|
|
68
|
+
},
|
|
69
|
+
"./libsql": {
|
|
70
|
+
"types": "./dist/adapters/local/libsql.d.ts",
|
|
71
|
+
"import": "./dist/adapters/local/libsql.js",
|
|
72
|
+
"default": "./dist/adapters/local/libsql.js"
|
|
73
|
+
},
|
|
74
|
+
"./sql-js": {
|
|
75
|
+
"types": "./dist/adapters/local/sql-js.d.ts",
|
|
76
|
+
"import": "./dist/adapters/local/sql-js.js",
|
|
77
|
+
"default": "./dist/adapters/local/sql-js.js"
|
|
78
|
+
},
|
|
79
|
+
"./node-sqlite": {
|
|
80
|
+
"types": "./dist/adapters/local/node-sqlite.d.ts",
|
|
81
|
+
"import": "./dist/adapters/local/node-sqlite.js",
|
|
82
|
+
"default": "./dist/adapters/local/node-sqlite.js"
|
|
83
|
+
},
|
|
84
|
+
"./core": {
|
|
85
|
+
"types": "./dist/core/index.d.ts",
|
|
86
|
+
"import": "./dist/core/index.js",
|
|
87
|
+
"default": "./dist/core/index.js"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"scripts": {
|
|
91
|
+
"build": "tsc",
|
|
92
|
+
"test": "vitest run",
|
|
93
|
+
"test:watch": "vitest",
|
|
94
|
+
"prepublishOnly": "npm run test && npm run build"
|
|
95
|
+
},
|
|
96
|
+
"peerDependencies": {
|
|
97
|
+
"@libsql/client": "^0.6.0",
|
|
98
|
+
"@op-engineering/op-sqlite": "^0.1.0",
|
|
99
|
+
"@supabase/supabase-js": "^2.0.0",
|
|
100
|
+
"better-sqlite3": "^8.0.0",
|
|
101
|
+
"convex": ">=1.0.0",
|
|
102
|
+
"expo-sqlite": ">=57.0.0",
|
|
103
|
+
"firebase": ">=9.0.0",
|
|
104
|
+
"react": ">=18.0.0",
|
|
105
|
+
"sql.js": "^1.8.0"
|
|
106
|
+
},
|
|
107
|
+
"peerDependenciesMeta": {
|
|
108
|
+
"react": {
|
|
109
|
+
"optional": true
|
|
110
|
+
},
|
|
111
|
+
"@supabase/supabase-js": {
|
|
112
|
+
"optional": true
|
|
113
|
+
},
|
|
114
|
+
"convex": {
|
|
115
|
+
"optional": true
|
|
116
|
+
},
|
|
117
|
+
"firebase": {
|
|
118
|
+
"optional": true
|
|
119
|
+
},
|
|
120
|
+
"expo-sqlite": {
|
|
121
|
+
"optional": true
|
|
122
|
+
},
|
|
123
|
+
"@op-engineering/op-sqlite": {
|
|
124
|
+
"optional": true
|
|
125
|
+
},
|
|
126
|
+
"better-sqlite3": {
|
|
127
|
+
"optional": true
|
|
128
|
+
},
|
|
129
|
+
"@libsql/client": {
|
|
130
|
+
"optional": true
|
|
131
|
+
},
|
|
132
|
+
"sql.js": {
|
|
133
|
+
"optional": true
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"devDependencies": {
|
|
137
|
+
"@supabase/supabase-js": "^2.118.0",
|
|
138
|
+
"@types/node": "^26.6.5",
|
|
139
|
+
"@types/react": "^19.3.0",
|
|
140
|
+
"@types/uuid": "^11.0.0",
|
|
141
|
+
"expo-sqlite": "^58.0.13",
|
|
142
|
+
"react": "^19.3.1",
|
|
143
|
+
"typescript": "^7.0.2",
|
|
144
|
+
"vitest": "^5.0.5"
|
|
145
|
+
},
|
|
146
|
+
"dependencies": {
|
|
147
|
+
"@noble/hashes": "^2.4.0",
|
|
148
|
+
"uuid": "^14.0.3"
|
|
149
|
+
}
|
|
150
|
+
}
|