local-lite 0.0.22 → 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/auth/session.js +27 -27
- package/dist/core/client.d.ts +15 -1
- package/dist/core/client.js +34 -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/store.js +24 -24
- 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 -199
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,
|
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
|
@@ -978,22 +978,50 @@ class LocalFirstClient {
|
|
|
978
978
|
await this.resetSyncCursor();
|
|
979
979
|
this.events.emit('reset', { dropTables: options.dropTables ?? false });
|
|
980
980
|
}
|
|
981
|
-
|
|
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;
|
|
982
1002
|
const list = new Set(this.tables);
|
|
983
|
-
|
|
984
|
-
|
|
1003
|
+
if (includeInternal) {
|
|
1004
|
+
list.add('sync_outbox');
|
|
1005
|
+
list.add('sync_meta');
|
|
1006
|
+
}
|
|
985
1007
|
try {
|
|
986
|
-
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_%'`);
|
|
987
1009
|
if (Array.isArray(rows)) {
|
|
988
1010
|
for (const r of rows) {
|
|
989
|
-
if (r.name)
|
|
990
|
-
|
|
1011
|
+
if (r.name) {
|
|
1012
|
+
if (includeInternal || !(0, sql_js_1.isInternalTable)(r.name)) {
|
|
1013
|
+
list.add(r.name);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
991
1016
|
}
|
|
992
1017
|
}
|
|
993
1018
|
}
|
|
994
1019
|
catch {
|
|
995
1020
|
// Non-SQLite database adapter, fallback to configured tables
|
|
996
1021
|
}
|
|
1022
|
+
if (!includeInternal) {
|
|
1023
|
+
return Array.from(list).filter(name => !(0, sql_js_1.isInternalTable)(name));
|
|
1024
|
+
}
|
|
997
1025
|
return Array.from(list);
|
|
998
1026
|
}
|
|
999
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
|
/**
|
|
@@ -22,16 +22,16 @@ class OutboxStore {
|
|
|
22
22
|
* Initializes the sync_outbox schema if it does not already exist.
|
|
23
23
|
*/
|
|
24
24
|
static async ensureSchema(db) {
|
|
25
|
-
await db.execute(`CREATE TABLE IF NOT EXISTS sync_outbox (
|
|
26
|
-
id TEXT PRIMARY KEY,
|
|
27
|
-
table_name TEXT NOT NULL,
|
|
28
|
-
row_id TEXT NOT NULL,
|
|
29
|
-
operation TEXT NOT NULL,
|
|
30
|
-
payload TEXT,
|
|
31
|
-
timestamp INTEGER NOT NULL,
|
|
32
|
-
status TEXT NOT NULL DEFAULT 'PENDING',
|
|
33
|
-
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
34
|
-
last_error TEXT
|
|
25
|
+
await db.execute(`CREATE TABLE IF NOT EXISTS sync_outbox (
|
|
26
|
+
id TEXT PRIMARY KEY,
|
|
27
|
+
table_name TEXT NOT NULL,
|
|
28
|
+
row_id TEXT NOT NULL,
|
|
29
|
+
operation TEXT NOT NULL,
|
|
30
|
+
payload TEXT,
|
|
31
|
+
timestamp INTEGER NOT NULL,
|
|
32
|
+
status TEXT NOT NULL DEFAULT 'PENDING',
|
|
33
|
+
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
34
|
+
last_error TEXT
|
|
35
35
|
);`);
|
|
36
36
|
// Indexes for the hot paths: pending scans (push), per-row conflict lookups (pull),
|
|
37
37
|
// and status counts (telemetry). Executed separately: many drivers prepare one statement.
|
|
@@ -43,7 +43,7 @@ class OutboxStore {
|
|
|
43
43
|
* Inserts a new outbox entry within an existing database transaction.
|
|
44
44
|
*/
|
|
45
45
|
static async insertRecord(tx, op) {
|
|
46
|
-
await tx.execute(`INSERT INTO sync_outbox (id, table_name, row_id, operation, payload, timestamp, status, retry_count)
|
|
46
|
+
await tx.execute(`INSERT INTO sync_outbox (id, table_name, row_id, operation, payload, timestamp, status, retry_count)
|
|
47
47
|
VALUES (?, ?, ?, ?, ?, ?, 'PENDING', 0)`, [
|
|
48
48
|
op.id,
|
|
49
49
|
op.tableName,
|
|
@@ -85,11 +85,11 @@ class OutboxStore {
|
|
|
85
85
|
static async getPending(db, maxRetries = 5, limit = 50) {
|
|
86
86
|
const isInfinite = !Number.isFinite(maxRetries) || maxRetries === -1;
|
|
87
87
|
const query = isInfinite
|
|
88
|
-
? `SELECT * FROM sync_outbox
|
|
89
|
-
WHERE status = 'PENDING' OR status = 'FAILED'
|
|
88
|
+
? `SELECT * FROM sync_outbox
|
|
89
|
+
WHERE status = 'PENDING' OR status = 'FAILED'
|
|
90
90
|
ORDER BY timestamp ASC LIMIT ?`
|
|
91
|
-
: `SELECT * FROM sync_outbox
|
|
92
|
-
WHERE status = 'PENDING' OR (status = 'FAILED' AND retry_count < ?)
|
|
91
|
+
: `SELECT * FROM sync_outbox
|
|
92
|
+
WHERE status = 'PENDING' OR (status = 'FAILED' AND retry_count < ?)
|
|
93
93
|
ORDER BY timestamp ASC LIMIT ?`;
|
|
94
94
|
const params = isInfinite ? [limit] : [maxRetries, limit];
|
|
95
95
|
const rows = await db.execute(query, params);
|
|
@@ -101,8 +101,8 @@ class OutboxStore {
|
|
|
101
101
|
* Finds the latest pending outbox record for a given table and row.
|
|
102
102
|
*/
|
|
103
103
|
static async findPendingByRow(tx, tableName, rowId) {
|
|
104
|
-
const rows = await tx.execute(`SELECT * FROM sync_outbox
|
|
105
|
-
WHERE table_name = ? AND row_id = ? AND status != 'FAILED'
|
|
104
|
+
const rows = await tx.execute(`SELECT * FROM sync_outbox
|
|
105
|
+
WHERE table_name = ? AND row_id = ? AND status != 'FAILED'
|
|
106
106
|
ORDER BY timestamp DESC LIMIT 1`, [tableName, rowId]);
|
|
107
107
|
if (!rows || rows.length === 0)
|
|
108
108
|
return undefined;
|
|
@@ -193,10 +193,10 @@ class OutboxStore {
|
|
|
193
193
|
if (ids.length === 0)
|
|
194
194
|
return;
|
|
195
195
|
const placeholders = ids.map(() => '?').join(',');
|
|
196
|
-
await db.execute(`UPDATE sync_outbox
|
|
197
|
-
SET status = 'FAILED',
|
|
198
|
-
retry_count = retry_count + 1,
|
|
199
|
-
last_error = ?
|
|
196
|
+
await db.execute(`UPDATE sync_outbox
|
|
197
|
+
SET status = 'FAILED',
|
|
198
|
+
retry_count = retry_count + 1,
|
|
199
|
+
last_error = ?
|
|
200
200
|
WHERE id IN (${placeholders})`, [errorMessage, ...ids]);
|
|
201
201
|
}
|
|
202
202
|
/**
|
|
@@ -235,8 +235,8 @@ class OutboxStore {
|
|
|
235
235
|
try {
|
|
236
236
|
if (ids && ids.length > 0) {
|
|
237
237
|
const placeholders = ids.map(() => '?').join(',');
|
|
238
|
-
await db.execute(`UPDATE sync_outbox
|
|
239
|
-
SET status = 'PENDING', retry_count = 0, last_error = NULL
|
|
238
|
+
await db.execute(`UPDATE sync_outbox
|
|
239
|
+
SET status = 'PENDING', retry_count = 0, last_error = NULL
|
|
240
240
|
WHERE status = 'FAILED' AND id IN (${placeholders})`, ids);
|
|
241
241
|
return ids.length;
|
|
242
242
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { LocalFirstClient } from '../core/client.js';
|
|
3
|
-
import { type SyncThemeColors } from '
|
|
3
|
+
import { type SyncThemeColors } from '../react/theme.js';
|
|
4
4
|
export interface BuiltinBenchmarkResult {
|
|
5
5
|
name: string;
|
|
6
6
|
category: 'Local-Lite Client' | 'ACID / Transactions' | 'Outbox / Sync' | 'Memory / JSON';
|
|
@@ -37,7 +37,7 @@ exports.runSQLiteBenchmark = runSQLiteBenchmark;
|
|
|
37
37
|
exports.BenchmarkModal = BenchmarkModal;
|
|
38
38
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
39
39
|
const react_1 = __importStar(require("react"));
|
|
40
|
-
const theme_js_1 = require("
|
|
40
|
+
const theme_js_1 = require("../react/theme.js");
|
|
41
41
|
let RN;
|
|
42
42
|
try {
|
|
43
43
|
RN = require('react-native');
|
|
@@ -68,27 +68,27 @@ async function runSQLiteBenchmark(clientOrDb, onProgress) {
|
|
|
68
68
|
await db.execute('DROP TABLE IF EXISTS "_sync_bench_tags"').catch(() => { });
|
|
69
69
|
await db.execute('DROP TABLE IF EXISTS "_sync_bench_temp"').catch(() => { });
|
|
70
70
|
await db.execute('DROP TABLE IF EXISTS "_sync_bench_outbox"').catch(() => { });
|
|
71
|
-
await db.execute(`
|
|
72
|
-
CREATE TABLE IF NOT EXISTS "_sync_bench_temp" (
|
|
73
|
-
id TEXT PRIMARY KEY,
|
|
74
|
-
title TEXT,
|
|
75
|
-
score INTEGER,
|
|
76
|
-
metadata TEXT,
|
|
77
|
-
created_at TEXT
|
|
78
|
-
)
|
|
71
|
+
await db.execute(`
|
|
72
|
+
CREATE TABLE IF NOT EXISTS "_sync_bench_temp" (
|
|
73
|
+
id TEXT PRIMARY KEY,
|
|
74
|
+
title TEXT,
|
|
75
|
+
score INTEGER,
|
|
76
|
+
metadata TEXT,
|
|
77
|
+
created_at TEXT
|
|
78
|
+
)
|
|
79
79
|
`);
|
|
80
80
|
await db.execute('CREATE INDEX IF NOT EXISTS "idx_sync_bench_score" ON "_sync_bench_temp" (score)');
|
|
81
|
-
await db.execute(`
|
|
82
|
-
CREATE TABLE IF NOT EXISTS "_sync_bench_outbox" (
|
|
83
|
-
id TEXT PRIMARY KEY,
|
|
84
|
-
tableName TEXT NOT NULL,
|
|
85
|
-
rowId TEXT NOT NULL,
|
|
86
|
-
operation TEXT NOT NULL,
|
|
87
|
-
payload TEXT,
|
|
88
|
-
timestamp INTEGER NOT NULL,
|
|
89
|
-
status TEXT DEFAULT 'PENDING',
|
|
90
|
-
retryCount INTEGER DEFAULT 0
|
|
91
|
-
)
|
|
81
|
+
await db.execute(`
|
|
82
|
+
CREATE TABLE IF NOT EXISTS "_sync_bench_outbox" (
|
|
83
|
+
id TEXT PRIMARY KEY,
|
|
84
|
+
tableName TEXT NOT NULL,
|
|
85
|
+
rowId TEXT NOT NULL,
|
|
86
|
+
operation TEXT NOT NULL,
|
|
87
|
+
payload TEXT,
|
|
88
|
+
timestamp INTEGER NOT NULL,
|
|
89
|
+
status TEXT DEFAULT 'PENDING',
|
|
90
|
+
retryCount INTEGER DEFAULT 0
|
|
91
|
+
)
|
|
92
92
|
`);
|
|
93
93
|
await db.execute('CREATE INDEX IF NOT EXISTS "idx_sync_bench_outbox" ON "_sync_bench_outbox" (status, timestamp)');
|
|
94
94
|
// 1. client.batch() Ingestion (500 rows in single atomic transaction)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { LocalFirstClient } from '../core/client.js';
|
|
3
|
-
import { type SyncThemeColors } from '
|
|
3
|
+
import { type SyncThemeColors } from '../react/theme.js';
|
|
4
4
|
export interface SyncInspectorProps {
|
|
5
5
|
client: LocalFirstClient | null | undefined;
|
|
6
|
+
title?: string;
|
|
6
7
|
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'top' | 'bottom' | 'inline';
|
|
7
8
|
variant?: 'pill' | 'compact' | 'icon' | 'minimal';
|
|
8
9
|
initialOpen?: boolean;
|
|
@@ -26,4 +27,4 @@ export interface SyncInspectorProps {
|
|
|
26
27
|
* Respects device safe areas and active theme (dark/light/custom).
|
|
27
28
|
* Displays real-time outbox telemetry, connection status, metrics, and triggers manual push, pull, reset, and benchmarks.
|
|
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 function SyncInspector({ client, position, variant, initialOpen, enableBenchmark, onOpenBenchmark, style, theme, alwaysVisible, renderTrigger, title, }: SyncInspectorProps): React.JSX.Element | null;
|