local-lite 0.0.21 → 0.0.24
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/LICENSE +21 -21
- package/README.md +45 -45
- package/dist/actions/store.js +15 -15
- package/dist/adapters/remote/convex.d.ts +7 -0
- package/dist/adapters/remote/convex.js +10 -1
- package/dist/adapters/remote/firestore.d.ts +7 -0
- package/dist/adapters/remote/firestore.js +23 -1
- package/dist/adapters/remote/supabase.d.ts +7 -0
- package/dist/adapters/remote/supabase.js +13 -2
- package/dist/auth/session.js +27 -27
- package/dist/core/client.d.ts +15 -1
- package/dist/core/client.js +46 -6
- package/dist/core/maintenance.d.ts +3 -1
- package/dist/core/maintenance.js +6 -2
- package/dist/core/meta-store.js +3 -3
- package/dist/core/outbox-store.js +23 -23
- package/dist/{react → devtools}/BenchmarkModal.d.ts +1 -1
- package/dist/{react → devtools}/BenchmarkModal.js +20 -20
- package/dist/{react → devtools}/SyncInspector.d.ts +3 -2
- package/dist/{react → devtools}/SyncInspector.js +43 -33
- package/dist/devtools/index.d.ts +3 -0
- package/dist/devtools/index.js +20 -0
- package/dist/keys/store.js +14 -14
- package/dist/kv/store.js +13 -13
- package/dist/react/index.d.ts +0 -3
- package/dist/react/index.js +0 -4
- package/dist/scheduler/store.js +28 -28
- package/dist/schema/generator.js +28 -28
- package/dist/search/store.js +15 -15
- package/dist/storage/convex.d.ts +43 -0
- package/dist/storage/convex.js +112 -0
- package/dist/storage/expo.d.ts +132 -0
- package/dist/storage/expo.js +408 -0
- package/dist/storage/factory.d.ts +16 -0
- package/dist/storage/factory.js +22 -0
- package/dist/storage/firebase.d.ts +44 -0
- package/dist/storage/firebase.js +135 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/manager.d.ts +20 -0
- package/dist/storage/manager.js +144 -0
- package/dist/storage/store.js +24 -24
- package/dist/storage/supabase.d.ts +44 -0
- package/dist/storage/supabase.js +83 -0
- package/dist/storage/types.d.ts +29 -3
- package/dist/utils/portability.d.ts +4 -0
- package/dist/utils/portability.js +7 -1
- package/dist/utils/sql.d.ts +5 -0
- package/dist/utils/sql.js +21 -0
- package/package.json +204 -195
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 local-lite contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 local-lite contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -91,11 +91,11 @@ This recovers sync state; it does not repair corrupt SQLite files or cancel hung
|
|
|
91
91
|
* **📶 Network-Aware:** Automatically pauses polling when offline and triggers immediate sync flushes the instant connectivity is restored.
|
|
92
92
|
* **🔄 Infinite-Loop Safe:** Incoming remote deltas update local tables and fire reactive UI listeners without echoing back into the outbox.
|
|
93
93
|
* **☁️ Database Backups:** Export and restore SQLite snapshots independently of your live sync backend.
|
|
94
|
-
* **🔑 Google Sign-In Out of the Box:** Secure, zero-backend Google OAuth 2.0 (PKCE) authentication with automatic token refresh, user profile retrieval, persistent session storage, and `useGoogleAuth()` hook.
|
|
95
|
-
* **📨 Action Outbox & Email Queue:** Enqueue transactional emails, push notifications, and webhooks offline with idempotency keys, automatic retry backoff, crash recovery, and TTL expiration.
|
|
96
|
-
* **⚡ Fast Key-Value & Encrypted Preferences (`client.kv`):** Typed storage, TTL expiration, and AES-256-GCM zero-knowledge secret protection.
|
|
97
|
-
* **🔍 Instant Full-Text Search (`client.search`):** SQLite FTS5 indexer with prefix matching, BM25 rank scoring, and highlighting.
|
|
98
|
-
* **📁 Offline Media & Blob Storage (`client.storage`):** Decoupled binary attachments, SHA-256 verification, and cloud sync queue (S3/Supabase/Firebase).
|
|
94
|
+
* **🔑 Google Sign-In Out of the Box:** Secure, zero-backend Google OAuth 2.0 (PKCE) authentication with automatic token refresh, user profile retrieval, persistent session storage, and `useGoogleAuth()` hook.
|
|
95
|
+
* **📨 Action Outbox & Email Queue:** Enqueue transactional emails, push notifications, and webhooks offline with idempotency keys, automatic retry backoff, crash recovery, and TTL expiration.
|
|
96
|
+
* **⚡ Fast Key-Value & Encrypted Preferences (`client.kv`):** Typed storage, TTL expiration, and AES-256-GCM zero-knowledge secret protection.
|
|
97
|
+
* **🔍 Instant Full-Text Search (`client.search`):** SQLite FTS5 indexer with prefix matching, BM25 rank scoring, and highlighting.
|
|
98
|
+
* **📁 Offline Media & Blob Storage (`client.storage`):** Decoupled binary attachments, SHA-256 verification, and cloud sync queue (S3/Supabase/Firebase).
|
|
99
99
|
* **⏰ Offline Job Scheduler & Background Cron (`client.scheduler`):** Persistent background task runner supporting human schedules and 5-field cron.
|
|
100
100
|
|
|
101
101
|
|
|
@@ -299,46 +299,46 @@ export function UserProfileHeader() {
|
|
|
299
299
|
---
|
|
300
300
|
|
|
301
301
|
|
|
302
|
-
### 4. Offline Actions & Inbuilt Email Queue
|
|
303
|
-
|
|
304
|
-
`local-lite` includes a durable **Generic Action Outbox** (`client.actions`) for side-effects that should not be lost when offline:
|
|
305
|
-
|
|
306
|
-
```typescript
|
|
307
|
-
import { client } from './sync.config';
|
|
308
|
-
|
|
309
|
-
// ⚡ 1. Enqueue transactional email offline (0ms latency, safe if app closes)
|
|
310
|
-
const emailAction = await client.actions.sendEmail({
|
|
311
|
-
to: 'customer@example.com',
|
|
312
|
-
subject: 'Order #1042 Confirmed',
|
|
313
|
-
html: '<h1>Thank you for your purchase!</h1>',
|
|
314
|
-
template: 'order_receipt',
|
|
315
|
-
templateData: { orderId: 1042, total: '$49.99' },
|
|
316
|
-
idempotencyKey: 'order_receipt_1042', // Prevents sending twice
|
|
317
|
-
ttlMs: 24 * 60 * 60 * 1000, // Expire if offline for >24 hours
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// ⚡ 2. Register local dispatcher (or let rows replicate to Supabase/Convex/Firebase)
|
|
321
|
-
client.actions.registerEmailHandler(async (email) => {
|
|
322
|
-
// Dispatch via your transactional provider or backend API
|
|
323
|
-
const response = await fetch('https://api.resend.com/emails', {
|
|
324
|
-
method: 'POST',
|
|
325
|
-
headers: { Authorization: `Bearer ${process.env.RESEND_API_KEY}` },
|
|
326
|
-
body: JSON.stringify(email),
|
|
327
|
-
});
|
|
328
|
-
const data = await response.json();
|
|
329
|
-
return { messageId: data.id };
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// ⚡ 3. Enqueue arbitrary asynchronous actions (e.g., push notifications, analytics)
|
|
333
|
-
await client.actions.enqueue({
|
|
334
|
-
action: 'push_notification',
|
|
335
|
-
payload: { title: 'New chat message', token: 'fcm-xxx' },
|
|
336
|
-
idempotencyKey: 'chat_msg_99',
|
|
337
|
-
});
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
---
|
|
341
|
-
|
|
302
|
+
### 4. Offline Actions & Inbuilt Email Queue
|
|
303
|
+
|
|
304
|
+
`local-lite` includes a durable **Generic Action Outbox** (`client.actions`) for side-effects that should not be lost when offline:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { client } from './sync.config';
|
|
308
|
+
|
|
309
|
+
// ⚡ 1. Enqueue transactional email offline (0ms latency, safe if app closes)
|
|
310
|
+
const emailAction = await client.actions.sendEmail({
|
|
311
|
+
to: 'customer@example.com',
|
|
312
|
+
subject: 'Order #1042 Confirmed',
|
|
313
|
+
html: '<h1>Thank you for your purchase!</h1>',
|
|
314
|
+
template: 'order_receipt',
|
|
315
|
+
templateData: { orderId: 1042, total: '$49.99' },
|
|
316
|
+
idempotencyKey: 'order_receipt_1042', // Prevents sending twice
|
|
317
|
+
ttlMs: 24 * 60 * 60 * 1000, // Expire if offline for >24 hours
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ⚡ 2. Register local dispatcher (or let rows replicate to Supabase/Convex/Firebase)
|
|
321
|
+
client.actions.registerEmailHandler(async (email) => {
|
|
322
|
+
// Dispatch via your transactional provider or backend API
|
|
323
|
+
const response = await fetch('https://api.resend.com/emails', {
|
|
324
|
+
method: 'POST',
|
|
325
|
+
headers: { Authorization: `Bearer ${process.env.RESEND_API_KEY}` },
|
|
326
|
+
body: JSON.stringify(email),
|
|
327
|
+
});
|
|
328
|
+
const data = await response.json();
|
|
329
|
+
return { messageId: data.id };
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// ⚡ 3. Enqueue arbitrary asynchronous actions (e.g., push notifications, analytics)
|
|
333
|
+
await client.actions.enqueue({
|
|
334
|
+
action: 'push_notification',
|
|
335
|
+
payload: { title: 'New chat message', token: 'fcm-xxx' },
|
|
336
|
+
idempotencyKey: 'chat_msg_99',
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
342
|
## Supabase Backend Setup & Schema
|
|
343
343
|
|
|
344
344
|
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**):
|
package/dist/actions/store.js
CHANGED
|
@@ -27,19 +27,19 @@ class ActionStore {
|
|
|
27
27
|
* Initializes the action outbox schema and indexes if they do not exist.
|
|
28
28
|
*/
|
|
29
29
|
static async ensureSchema(db, tableName = exports.DEFAULT_ACTIONS_TABLE) {
|
|
30
|
-
await db.execute(`CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
31
|
-
id TEXT PRIMARY KEY,
|
|
32
|
-
action TEXT NOT NULL,
|
|
33
|
-
payload TEXT NOT NULL,
|
|
34
|
-
idempotency_key TEXT UNIQUE,
|
|
35
|
-
status TEXT NOT NULL DEFAULT 'PENDING',
|
|
36
|
-
created_at INTEGER NOT NULL,
|
|
37
|
-
expires_at INTEGER,
|
|
38
|
-
executed_at INTEGER,
|
|
39
|
-
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
40
|
-
max_retries INTEGER NOT NULL DEFAULT 3,
|
|
41
|
-
last_error TEXT,
|
|
42
|
-
result TEXT
|
|
30
|
+
await db.execute(`CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
action TEXT NOT NULL,
|
|
33
|
+
payload TEXT NOT NULL,
|
|
34
|
+
idempotency_key TEXT UNIQUE,
|
|
35
|
+
status TEXT NOT NULL DEFAULT 'PENDING',
|
|
36
|
+
created_at INTEGER NOT NULL,
|
|
37
|
+
expires_at INTEGER,
|
|
38
|
+
executed_at INTEGER,
|
|
39
|
+
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
40
|
+
max_retries INTEGER NOT NULL DEFAULT 3,
|
|
41
|
+
last_error TEXT,
|
|
42
|
+
result TEXT
|
|
43
43
|
);`);
|
|
44
44
|
await db.execute(`CREATE INDEX IF NOT EXISTS idx_${tableName}_status ON ${tableName} (status, created_at);`);
|
|
45
45
|
await db.execute(`CREATE INDEX IF NOT EXISTS idx_${tableName}_idempotency ON ${tableName} (idempotency_key);`);
|
|
@@ -55,8 +55,8 @@ class ActionStore {
|
|
|
55
55
|
* Inserts an action record into storage.
|
|
56
56
|
*/
|
|
57
57
|
static async insertRecord(db, tableName = exports.DEFAULT_ACTIONS_TABLE, record) {
|
|
58
|
-
await db.execute(`INSERT INTO ${tableName} (
|
|
59
|
-
id, action, payload, idempotency_key, status, created_at, expires_at, executed_at, retry_count, max_retries, last_error, result
|
|
58
|
+
await db.execute(`INSERT INTO ${tableName} (
|
|
59
|
+
id, action, payload, idempotency_key, status, created_at, expires_at, executed_at, retry_count, max_retries, last_error, result
|
|
60
60
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`, [
|
|
61
61
|
record.id,
|
|
62
62
|
record.action,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RemoteDBAdapter } from './index.js';
|
|
2
2
|
import { DeltaChanges, SyncOperation, PushResult } from '../../core/types.js';
|
|
3
3
|
import { SyncLogger, CustomLogger, LogLevel } from '../../utils/logger.js';
|
|
4
|
+
import { createConvexStorageUploader, ConvexStorageUploaderOptions, ConvexStorageUploaderInstance } from '../../storage/convex.js';
|
|
4
5
|
export interface ConvexRemoteAdapterOptions {
|
|
5
6
|
/**
|
|
6
7
|
* Tables synchronized with Convex.
|
|
@@ -68,4 +69,10 @@ export declare class ConvexRemoteAdapter implements RemoteDBAdapter {
|
|
|
68
69
|
* Cleans up live subscriptions.
|
|
69
70
|
*/
|
|
70
71
|
destroy(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Helper to create a remote storage uploader targeting Convex File Storage.
|
|
74
|
+
* Connects local-lite offline blob/file storage directly to Convex File Storage.
|
|
75
|
+
*/
|
|
76
|
+
createStorageUploader(options?: ConvexStorageUploaderOptions): ConvexStorageUploaderInstance;
|
|
71
77
|
}
|
|
78
|
+
export { createConvexStorageUploader, type ConvexStorageUploaderOptions, type ConvexStorageUploaderInstance, };
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ConvexRemoteAdapter = void 0;
|
|
3
|
+
exports.createConvexStorageUploader = exports.ConvexRemoteAdapter = void 0;
|
|
4
4
|
const logger_js_1 = require("../../utils/logger.js");
|
|
5
|
+
const convex_js_1 = require("../../storage/convex.js");
|
|
6
|
+
Object.defineProperty(exports, "createConvexStorageUploader", { enumerable: true, get: function () { return convex_js_1.createConvexStorageUploader; } });
|
|
5
7
|
class ConvexRemoteAdapter {
|
|
6
8
|
client;
|
|
7
9
|
tables;
|
|
@@ -177,5 +179,12 @@ class ConvexRemoteAdapter {
|
|
|
177
179
|
this.unsubscribeLive = undefined;
|
|
178
180
|
}
|
|
179
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Helper to create a remote storage uploader targeting Convex File Storage.
|
|
184
|
+
* Connects local-lite offline blob/file storage directly to Convex File Storage.
|
|
185
|
+
*/
|
|
186
|
+
createStorageUploader(options = {}) {
|
|
187
|
+
return (0, convex_js_1.createConvexStorageUploader)(this.client, options);
|
|
188
|
+
}
|
|
180
189
|
}
|
|
181
190
|
exports.ConvexRemoteAdapter = ConvexRemoteAdapter;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RemoteDBAdapter } from './index.js';
|
|
2
2
|
import { DeltaChanges, SyncOperation, PushResult } from '../../core/types.js';
|
|
3
3
|
import { SyncLogger, CustomLogger, LogLevel } from '../../utils/logger.js';
|
|
4
|
+
import { createFirebaseStorageUploader, FirebaseStorageUploaderOptions, FirebaseStorageUploaderInstance } from '../../storage/firebase.js';
|
|
4
5
|
export interface FirestoreAdapterOptions {
|
|
5
6
|
/**
|
|
6
7
|
* Collections (tables) synchronized with Firestore.
|
|
@@ -62,4 +63,10 @@ export declare class FirestoreRemoteAdapter implements RemoteDBAdapter {
|
|
|
62
63
|
* Cleans up all active real-time listeners.
|
|
63
64
|
*/
|
|
64
65
|
destroy(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Helper to create a remote storage uploader targeting Firebase Cloud Storage.
|
|
68
|
+
* Connects local-lite offline blob/file storage directly to Firebase Storage.
|
|
69
|
+
*/
|
|
70
|
+
createStorageUploader(storageOrApp?: any, options?: FirebaseStorageUploaderOptions): FirebaseStorageUploaderInstance;
|
|
65
71
|
}
|
|
72
|
+
export { FirestoreRemoteAdapter as FirestoreAdapter, createFirebaseStorageUploader, type FirebaseStorageUploaderOptions, type FirebaseStorageUploaderInstance, };
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FirestoreRemoteAdapter = void 0;
|
|
3
|
+
exports.createFirebaseStorageUploader = exports.FirestoreAdapter = exports.FirestoreRemoteAdapter = void 0;
|
|
4
4
|
const logger_js_1 = require("../../utils/logger.js");
|
|
5
|
+
const firebase_js_1 = require("../../storage/firebase.js");
|
|
6
|
+
Object.defineProperty(exports, "createFirebaseStorageUploader", { enumerable: true, get: function () { return firebase_js_1.createFirebaseStorageUploader; } });
|
|
5
7
|
/**
|
|
6
8
|
* Safely extracts epoch milliseconds from numbers, Firestore Timestamps, or ISO strings.
|
|
7
9
|
*/
|
|
@@ -328,5 +330,25 @@ class FirestoreRemoteAdapter {
|
|
|
328
330
|
}
|
|
329
331
|
this.unsubscribers = [];
|
|
330
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Helper to create a remote storage uploader targeting Firebase Cloud Storage.
|
|
335
|
+
* Connects local-lite offline blob/file storage directly to Firebase Storage.
|
|
336
|
+
*/
|
|
337
|
+
createStorageUploader(storageOrApp, options = {}) {
|
|
338
|
+
let client = storageOrApp;
|
|
339
|
+
let opts = options;
|
|
340
|
+
if (storageOrApp &&
|
|
341
|
+
!storageOrApp.ref &&
|
|
342
|
+
!storageOrApp.storage &&
|
|
343
|
+
!storageOrApp.app &&
|
|
344
|
+
typeof storageOrApp === 'object' &&
|
|
345
|
+
('bucket' in storageOrApp || 'storageModule' in storageOrApp || 'basePath' in storageOrApp)) {
|
|
346
|
+
opts = storageOrApp;
|
|
347
|
+
client = this.db.app || this.db;
|
|
348
|
+
}
|
|
349
|
+
const resolvedClient = client || this.db.app || this.db;
|
|
350
|
+
return (0, firebase_js_1.createFirebaseStorageUploader)(resolvedClient, opts);
|
|
351
|
+
}
|
|
331
352
|
}
|
|
332
353
|
exports.FirestoreRemoteAdapter = FirestoreRemoteAdapter;
|
|
354
|
+
exports.FirestoreAdapter = FirestoreRemoteAdapter;
|
|
@@ -2,6 +2,7 @@ import type { SupabaseClient } from '@supabase/supabase-js';
|
|
|
2
2
|
import { RemoteDBAdapter } from './index.js';
|
|
3
3
|
import { DeltaChanges, PushResult, SyncOperation } from '../../core/types.js';
|
|
4
4
|
import { SyncLogger, CustomLogger, LogLevel } from '../../utils/logger.js';
|
|
5
|
+
import { createSupabaseStorageUploader, type SupabaseStorageUploaderOptions, type SupabaseStorageUploaderInstance } from '../../storage/supabase.js';
|
|
5
6
|
export interface SupabaseAdapterOptions {
|
|
6
7
|
/**
|
|
7
8
|
* The list of Postgres tables to sync.
|
|
@@ -123,4 +124,10 @@ export declare class SupabaseRemoteAdapter implements RemoteDBAdapter {
|
|
|
123
124
|
* Clean up active realtime channels.
|
|
124
125
|
*/
|
|
125
126
|
destroy(): void;
|
|
127
|
+
/**
|
|
128
|
+
* Helper to create a remote storage uploader targeting Supabase Storage buckets.
|
|
129
|
+
* Connects local-lite offline blob/file storage directly to Supabase Storage.
|
|
130
|
+
*/
|
|
131
|
+
createStorageUploader(options: SupabaseStorageUploaderOptions): SupabaseStorageUploaderInstance;
|
|
126
132
|
}
|
|
133
|
+
export { SupabaseRemoteAdapter as SupabaseAdapter, createSupabaseStorageUploader, type SupabaseStorageUploaderOptions, type SupabaseStorageUploaderInstance, };
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SupabaseRemoteAdapter = void 0;
|
|
3
|
+
exports.createSupabaseStorageUploader = exports.SupabaseAdapter = exports.SupabaseRemoteAdapter = void 0;
|
|
4
4
|
const logger_js_1 = require("../../utils/logger.js");
|
|
5
|
+
const supabase_js_1 = require("../../storage/supabase.js");
|
|
6
|
+
Object.defineProperty(exports, "createSupabaseStorageUploader", { enumerable: true, get: function () { return supabase_js_1.createSupabaseStorageUploader; } });
|
|
5
7
|
class SupabaseRemoteAdapter {
|
|
6
8
|
clientResolver;
|
|
7
9
|
activeLiveCallback;
|
|
@@ -25,7 +27,8 @@ class SupabaseRemoteAdapter {
|
|
|
25
27
|
typeof clientOrOptions === 'object' &&
|
|
26
28
|
!('channel' in clientOrOptions) &&
|
|
27
29
|
!('from' in clientOrOptions) &&
|
|
28
|
-
!('auth' in clientOrOptions)
|
|
30
|
+
!('auth' in clientOrOptions) &&
|
|
31
|
+
!('storage' in clientOrOptions)) {
|
|
29
32
|
client = null;
|
|
30
33
|
options = clientOrOptions;
|
|
31
34
|
}
|
|
@@ -640,5 +643,13 @@ class SupabaseRemoteAdapter {
|
|
|
640
643
|
this.activeChannel = undefined;
|
|
641
644
|
}
|
|
642
645
|
}
|
|
646
|
+
/**
|
|
647
|
+
* Helper to create a remote storage uploader targeting Supabase Storage buckets.
|
|
648
|
+
* Connects local-lite offline blob/file storage directly to Supabase Storage.
|
|
649
|
+
*/
|
|
650
|
+
createStorageUploader(options) {
|
|
651
|
+
return (0, supabase_js_1.createSupabaseStorageUploader)(this.getClient(), options);
|
|
652
|
+
}
|
|
643
653
|
}
|
|
644
654
|
exports.SupabaseRemoteAdapter = SupabaseRemoteAdapter;
|
|
655
|
+
exports.SupabaseAdapter = SupabaseRemoteAdapter;
|
package/dist/auth/session.js
CHANGED
|
@@ -54,19 +54,19 @@ class SessionManager {
|
|
|
54
54
|
* Ensures the _local_sessions schema exists.
|
|
55
55
|
*/
|
|
56
56
|
async initialize() {
|
|
57
|
-
await this.db.execute(`CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
58
|
-
id TEXT PRIMARY KEY,
|
|
59
|
-
user_id TEXT NOT NULL,
|
|
60
|
-
user_data TEXT NOT NULL,
|
|
61
|
-
token TEXT,
|
|
62
|
-
refresh_token TEXT,
|
|
63
|
-
provider TEXT,
|
|
64
|
-
created_at INTEGER NOT NULL,
|
|
65
|
-
last_active_at INTEGER NOT NULL,
|
|
66
|
-
expires_at INTEGER,
|
|
67
|
-
offline_access_until INTEGER,
|
|
68
|
-
metadata TEXT,
|
|
69
|
-
is_active INTEGER NOT NULL DEFAULT 1
|
|
57
|
+
await this.db.execute(`CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
58
|
+
id TEXT PRIMARY KEY,
|
|
59
|
+
user_id TEXT NOT NULL,
|
|
60
|
+
user_data TEXT NOT NULL,
|
|
61
|
+
token TEXT,
|
|
62
|
+
refresh_token TEXT,
|
|
63
|
+
provider TEXT,
|
|
64
|
+
created_at INTEGER NOT NULL,
|
|
65
|
+
last_active_at INTEGER NOT NULL,
|
|
66
|
+
expires_at INTEGER,
|
|
67
|
+
offline_access_until INTEGER,
|
|
68
|
+
metadata TEXT,
|
|
69
|
+
is_active INTEGER NOT NULL DEFAULT 1
|
|
70
70
|
);`);
|
|
71
71
|
await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_${this.tableName}_user ON ${this.tableName} (user_id);`);
|
|
72
72
|
await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_${this.tableName}_active ON ${this.tableName} (is_active);`);
|
|
@@ -90,20 +90,20 @@ class SessionManager {
|
|
|
90
90
|
// Deactivate other sessions
|
|
91
91
|
await this.db.execute(`UPDATE ${this.tableName} SET is_active = ?;`, [0]);
|
|
92
92
|
}
|
|
93
|
-
const sql = `
|
|
94
|
-
INSERT INTO ${this.tableName} (id, user_id, user_data, token, refresh_token, provider, created_at, last_active_at, expires_at, offline_access_until, metadata, is_active)
|
|
95
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
96
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
97
|
-
user_id = excluded.user_id,
|
|
98
|
-
user_data = excluded.user_data,
|
|
99
|
-
token = excluded.token,
|
|
100
|
-
refresh_token = excluded.refresh_token,
|
|
101
|
-
provider = excluded.provider,
|
|
102
|
-
last_active_at = excluded.last_active_at,
|
|
103
|
-
expires_at = excluded.expires_at,
|
|
104
|
-
offline_access_until = excluded.offline_access_until,
|
|
105
|
-
metadata = excluded.metadata,
|
|
106
|
-
is_active = excluded.is_active;
|
|
93
|
+
const sql = `
|
|
94
|
+
INSERT INTO ${this.tableName} (id, user_id, user_data, token, refresh_token, provider, created_at, last_active_at, expires_at, offline_access_until, metadata, is_active)
|
|
95
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
96
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
97
|
+
user_id = excluded.user_id,
|
|
98
|
+
user_data = excluded.user_data,
|
|
99
|
+
token = excluded.token,
|
|
100
|
+
refresh_token = excluded.refresh_token,
|
|
101
|
+
provider = excluded.provider,
|
|
102
|
+
last_active_at = excluded.last_active_at,
|
|
103
|
+
expires_at = excluded.expires_at,
|
|
104
|
+
offline_access_until = excluded.offline_access_until,
|
|
105
|
+
metadata = excluded.metadata,
|
|
106
|
+
is_active = excluded.is_active;
|
|
107
107
|
`;
|
|
108
108
|
await this.db.execute(sql, [
|
|
109
109
|
id,
|
package/dist/core/client.d.ts
CHANGED
|
@@ -347,7 +347,21 @@ export declare class LocalFirstClient {
|
|
|
347
347
|
dropTables?: boolean;
|
|
348
348
|
includeInternal?: boolean;
|
|
349
349
|
}): Promise<void>;
|
|
350
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Checks if a table name belongs to internal system, FTS, or engine tables.
|
|
352
|
+
*/
|
|
353
|
+
isInternalTable(tableName: string): boolean;
|
|
354
|
+
/**
|
|
355
|
+
* Returns list of database tables.
|
|
356
|
+
* By default, internal tables (FTS shadow tables, local-lite engine tables) are excluded.
|
|
357
|
+
* Pass `{ includeInternal: true }` to include internal tables.
|
|
358
|
+
*/
|
|
359
|
+
getTables(options?: {
|
|
360
|
+
includeInternal?: boolean;
|
|
361
|
+
}): Promise<string[]>;
|
|
362
|
+
getAllTableNames(options?: {
|
|
363
|
+
includeInternal?: boolean;
|
|
364
|
+
}): Promise<string[]>;
|
|
351
365
|
/**
|
|
352
366
|
* Exports the entire local database state as a portable binary snapshot.
|
|
353
367
|
* If the local adapter has a native export mechanism (e.g. serialize or export),
|
package/dist/core/client.js
CHANGED
|
@@ -219,9 +219,21 @@ class LocalFirstClient {
|
|
|
219
219
|
this.keys = new index_js_3.ApiKeyManager(this.db, config.keys);
|
|
220
220
|
this.kv = new index_js_4.KVManager(this.db, this.events, config.kv);
|
|
221
221
|
this.search = new index_js_5.SearchManager(this.db, this.events, (table, id) => this.findById(table, id));
|
|
222
|
+
let storageUploader = config.storage?.uploader;
|
|
223
|
+
if (!storageUploader && config.storage) {
|
|
224
|
+
if (typeof this.remote?.createStorageUploader === 'function' &&
|
|
225
|
+
(config.storage.autoRemote !== false &&
|
|
226
|
+
(config.storage.autoRemote || config.storage.bucket || config.storage.uploadMutation))) {
|
|
227
|
+
try {
|
|
228
|
+
storageUploader = this.remote.createStorageUploader(config.storage);
|
|
229
|
+
}
|
|
230
|
+
catch { }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
222
233
|
this.storage = new index_js_6.BlobStorageManager(this.db, {
|
|
223
234
|
network: config.network,
|
|
224
235
|
...config.storage,
|
|
236
|
+
uploader: storageUploader,
|
|
225
237
|
});
|
|
226
238
|
this.scheduler = new index_js_7.JobScheduler(this.db, config.scheduler);
|
|
227
239
|
this.session = new session_js_1.SessionManager(this.db, this.events, {
|
|
@@ -966,22 +978,50 @@ class LocalFirstClient {
|
|
|
966
978
|
await this.resetSyncCursor();
|
|
967
979
|
this.events.emit('reset', { dropTables: options.dropTables ?? false });
|
|
968
980
|
}
|
|
969
|
-
|
|
981
|
+
/**
|
|
982
|
+
* Checks if a table name belongs to internal system, FTS, or engine tables.
|
|
983
|
+
*/
|
|
984
|
+
isInternalTable(tableName) {
|
|
985
|
+
return (0, sql_js_1.isInternalTable)(tableName);
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Returns list of database tables.
|
|
989
|
+
* By default, internal tables (FTS shadow tables, local-lite engine tables) are excluded.
|
|
990
|
+
* Pass `{ includeInternal: true }` to include internal tables.
|
|
991
|
+
*/
|
|
992
|
+
async getTables(options = {}) {
|
|
993
|
+
const includeInternal = options.includeInternal ?? false;
|
|
994
|
+
const allTables = await this.getAllTableNames(options);
|
|
995
|
+
if (includeInternal) {
|
|
996
|
+
return allTables;
|
|
997
|
+
}
|
|
998
|
+
return allTables.filter(name => !(0, sql_js_1.isInternalTable)(name));
|
|
999
|
+
}
|
|
1000
|
+
async getAllTableNames(options = {}) {
|
|
1001
|
+
const includeInternal = options.includeInternal ?? false;
|
|
970
1002
|
const list = new Set(this.tables);
|
|
971
|
-
|
|
972
|
-
|
|
1003
|
+
if (includeInternal) {
|
|
1004
|
+
list.add('sync_outbox');
|
|
1005
|
+
list.add('sync_meta');
|
|
1006
|
+
}
|
|
973
1007
|
try {
|
|
974
|
-
const rows = await this.db.execute(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`);
|
|
1008
|
+
const rows = await this.db.execute(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'android_%'`);
|
|
975
1009
|
if (Array.isArray(rows)) {
|
|
976
1010
|
for (const r of rows) {
|
|
977
|
-
if (r.name)
|
|
978
|
-
|
|
1011
|
+
if (r.name) {
|
|
1012
|
+
if (includeInternal || !(0, sql_js_1.isInternalTable)(r.name)) {
|
|
1013
|
+
list.add(r.name);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
979
1016
|
}
|
|
980
1017
|
}
|
|
981
1018
|
}
|
|
982
1019
|
catch {
|
|
983
1020
|
// Non-SQLite database adapter, fallback to configured tables
|
|
984
1021
|
}
|
|
1022
|
+
if (!includeInternal) {
|
|
1023
|
+
return Array.from(list).filter(name => !(0, sql_js_1.isInternalTable)(name));
|
|
1024
|
+
}
|
|
985
1025
|
return Array.from(list);
|
|
986
1026
|
}
|
|
987
1027
|
/**
|
|
@@ -62,5 +62,7 @@ export declare class DatabaseMaintenance {
|
|
|
62
62
|
/**
|
|
63
63
|
* Gathers storage metrics: table row counts, outbox status distribution, and database size.
|
|
64
64
|
*/
|
|
65
|
-
getStorageStats(
|
|
65
|
+
getStorageStats(options?: {
|
|
66
|
+
includeInternal?: boolean;
|
|
67
|
+
}): Promise<StorageStats>;
|
|
66
68
|
}
|
package/dist/core/maintenance.js
CHANGED
|
@@ -76,17 +76,21 @@ class DatabaseMaintenance {
|
|
|
76
76
|
/**
|
|
77
77
|
* Gathers storage metrics: table row counts, outbox status distribution, and database size.
|
|
78
78
|
*/
|
|
79
|
-
async getStorageStats() {
|
|
79
|
+
async getStorageStats(options = {}) {
|
|
80
|
+
const includeInternal = options.includeInternal ?? false;
|
|
80
81
|
// 1. Table row counts
|
|
81
82
|
const tableCounts = {};
|
|
82
83
|
let tableNames = [];
|
|
83
84
|
try {
|
|
84
|
-
const masterRows = await this.db.execute(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`);
|
|
85
|
+
const masterRows = await this.db.execute(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'android_%'`);
|
|
85
86
|
tableNames = masterRows.map((r) => r.name);
|
|
86
87
|
}
|
|
87
88
|
catch {
|
|
88
89
|
tableNames = this.getKnownTables();
|
|
89
90
|
}
|
|
91
|
+
if (!includeInternal) {
|
|
92
|
+
tableNames = tableNames.filter(name => !(0, sql_js_1.isInternalTable)(name));
|
|
93
|
+
}
|
|
90
94
|
for (const tableName of tableNames) {
|
|
91
95
|
try {
|
|
92
96
|
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
package/dist/core/meta-store.js
CHANGED
|
@@ -7,9 +7,9 @@ class MetaStore {
|
|
|
7
7
|
* This table is strictly local to the device and is never synced to the remote server.
|
|
8
8
|
*/
|
|
9
9
|
static async ensureSchema(db) {
|
|
10
|
-
await db.execute(`CREATE TABLE IF NOT EXISTS sync_meta (
|
|
11
|
-
key TEXT PRIMARY KEY,
|
|
12
|
-
value TEXT NOT NULL
|
|
10
|
+
await db.execute(`CREATE TABLE IF NOT EXISTS sync_meta (
|
|
11
|
+
key TEXT PRIMARY KEY,
|
|
12
|
+
value TEXT NOT NULL
|
|
13
13
|
);`);
|
|
14
14
|
}
|
|
15
15
|
/**
|