local-lite 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +361 -2
- package/dist/adapters/local/better-sqlite3.d.ts +23 -0
- package/dist/adapters/local/better-sqlite3.js +46 -0
- package/dist/adapters/local/expo-sqlite.d.ts +8 -0
- package/dist/adapters/local/expo-sqlite.js +30 -0
- package/dist/adapters/local/index.d.ts +13 -0
- package/dist/adapters/local/index.js +22 -0
- package/dist/adapters/local/libsql.d.ts +24 -0
- package/dist/adapters/local/libsql.js +87 -0
- package/dist/adapters/local/node-sqlite.d.ts +24 -0
- package/dist/adapters/local/node-sqlite.js +44 -0
- package/dist/adapters/local/op-sqlite.d.ts +26 -0
- package/dist/adapters/local/op-sqlite.js +59 -0
- package/dist/adapters/local/sql-js.d.ts +26 -0
- package/dist/adapters/local/sql-js.js +63 -0
- package/dist/adapters/network/index.d.ts +11 -0
- package/dist/adapters/network/index.js +53 -0
- package/dist/adapters/remote/convex.d.ts +71 -0
- package/dist/adapters/remote/convex.js +185 -0
- package/dist/adapters/remote/demo.d.ts +21 -0
- package/dist/adapters/remote/demo.js +46 -0
- package/dist/adapters/remote/firestore.d.ts +64 -0
- package/dist/adapters/remote/firestore.js +302 -0
- package/dist/adapters/remote/index.d.ts +10 -0
- package/dist/adapters/remote/index.js +20 -0
- package/dist/adapters/remote/supabase.d.ts +97 -0
- package/dist/adapters/remote/supabase.js +276 -0
- package/dist/config/index.d.ts +24 -0
- package/dist/config/index.js +101 -0
- package/dist/config/resolve-local.d.ts +7 -0
- package/dist/config/resolve-local.js +68 -0
- package/dist/config/resolve-remote.d.ts +7 -0
- package/dist/config/resolve-remote.js +81 -0
- package/dist/config/types.d.ts +159 -0
- package/dist/config/types.js +2 -0
- package/dist/core/client.d.ts +153 -0
- package/dist/core/client.js +445 -0
- package/dist/core/conflict.d.ts +5 -0
- package/dist/core/conflict.js +38 -0
- package/dist/core/meta-store.d.ts +28 -0
- package/dist/core/meta-store.js +64 -0
- package/dist/core/outbox-store.d.ts +76 -0
- package/dist/core/outbox-store.js +265 -0
- package/dist/core/query.d.ts +13 -0
- package/dist/core/query.js +150 -0
- package/dist/core/schema-manager.d.ts +39 -0
- package/dist/core/schema-manager.js +150 -0
- package/dist/core/sync-worker.d.ts +89 -0
- package/dist/core/sync-worker.js +473 -0
- package/dist/core/table-manager.d.ts +19 -0
- package/dist/core/table-manager.js +74 -0
- package/dist/core/types.d.ts +156 -0
- package/dist/core/types.js +2 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +34 -0
- package/dist/react/index.d.ts +50 -0
- package/dist/react/index.js +174 -0
- package/dist/reactive/event-emitter.d.ts +7 -0
- package/dist/reactive/event-emitter.js +34 -0
- package/dist/utils/logger.d.ts +51 -0
- package/dist/utils/logger.js +169 -0
- package/dist/utils/sql.d.ts +4 -0
- package/dist/utils/sql.js +9 -0
- package/dist/utils/uuid.d.ts +7 -0
- package/dist/utils/uuid.js +27 -0
- package/package.json +131 -4
package/LICENSE
ADDED
|
@@ -0,0 +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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,362 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ⚡ local-lite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A lightweight, vendor-agnostic **local-first synchronization engine** for React Native, Expo, and Web applications.
|
|
4
|
+
|
|
5
|
+
Built around the **Transactional Outbox Pattern**, `local-lite` ensures instant, zero-latency local writes in SQLite while guaranteeing offline resilience and asynchronous synchronization with your backend (including native **Supabase**, **Convex**, and **Firebase / Firestore** support).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 📖 Documentation
|
|
10
|
+
|
|
11
|
+
* **[Quickstart & Setup](docs/getting-started.md)** - 5-minute setup with Expo, React Native, or Node.js
|
|
12
|
+
* **[Architecture](docs/architecture.md)** - Transactional outbox pattern, SyncWorker loops, and event pipeline
|
|
13
|
+
* **[Configuration Reference](docs/configuration.md)** - Options for `defineSyncConfig()`, retries, and loggers
|
|
14
|
+
* **[Local SQLite Adapters](docs/adapters-local.md)** - Expo SQLite, OP-SQLite, better-sqlite3, node:sqlite, and LibSQL
|
|
15
|
+
* **[Remote Cloud Adapters](docs/adapters-remote.md)** - Supabase, Convex, Cloud Firestore, and custom adapters
|
|
16
|
+
* **[React & Expo Hooks](docs/react-hooks.md)** - `useLiveQuery()` and `useOutboxStatus()`
|
|
17
|
+
* **[Conflict Resolution](docs/conflict-resolution.md)** - LWW, Client-Wins, Server-Wins, and custom resolvers
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
* **⚡ 0ms Optimistic Writes:** Write to local SQLite immediately via atomic dual-write (`client.mutate()`).
|
|
24
|
+
* **📦 Transactional Outbox:** Mutations are stored atomically in a local `sync_outbox` table, preventing data loss if the app closes or loses connectivity mid-write.
|
|
25
|
+
* **🌐 Native Cloud Adapters:** First-class sync with Postgres tables (via **Supabase**), reactive document datastores (via **Convex**), and document collections (via **Firebase / Firestore**).
|
|
26
|
+
* **🛡️ Conflict Resolution:** Built-in strategies including **Last-Write-Wins (LWW)**, Server-Wins, Client-Wins, or custom resolver functions.
|
|
27
|
+
* **📶 Network-Aware:** Automatically pauses polling when offline and triggers immediate sync flushes the instant connectivity is restored.
|
|
28
|
+
* **🔄 Infinite-Loop Safe:** Incoming remote deltas update local tables and fire reactive UI listeners without echoing back into the outbox.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install local-lite
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Peer dependencies (install what you use):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# If using Expo SQLite
|
|
42
|
+
npx expo install expo-sqlite @react-native-community/netinfo
|
|
43
|
+
|
|
44
|
+
# If syncing with Supabase
|
|
45
|
+
npm install @supabase/supabase-js
|
|
46
|
+
|
|
47
|
+
# If syncing with Convex
|
|
48
|
+
npm install convex
|
|
49
|
+
|
|
50
|
+
# If syncing with Firebase / Firestore
|
|
51
|
+
npm install firebase # or @react-native-firebase/firestore
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
### 1. Define your Configuration (`sync.config.ts`)
|
|
57
|
+
|
|
58
|
+
#### Option A: With Supabase
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// sync.config.ts
|
|
62
|
+
import { defineSyncConfig, createSyncClient } from 'local-lite';
|
|
63
|
+
import * as SQLite from 'expo-sqlite';
|
|
64
|
+
|
|
65
|
+
export const syncConfig = defineSyncConfig({
|
|
66
|
+
database: () => SQLite.openDatabaseAsync('app.db'),
|
|
67
|
+
tables: ['todos', 'notes'],
|
|
68
|
+
autoSchema: true,
|
|
69
|
+
|
|
70
|
+
// Supabase backend configuration
|
|
71
|
+
supabase: {
|
|
72
|
+
url: process.env.EXPO_PUBLIC_SUPABASE_URL || 'https://xyz.supabase.co',
|
|
73
|
+
anonKey: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || 'YOUR_ANON_KEY',
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
conflictResolver: 'last-write-wins',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const client = await createSyncClient(syncConfig);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### Option B: With Convex
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// sync.config.ts
|
|
86
|
+
import { defineSyncConfig, createSyncClient } from 'local-lite';
|
|
87
|
+
import { ConvexClient } from 'convex/browser';
|
|
88
|
+
import * as SQLite from 'expo-sqlite';
|
|
89
|
+
|
|
90
|
+
const convexClient = new ConvexClient(process.env.EXPO_PUBLIC_CONVEX_URL!);
|
|
91
|
+
|
|
92
|
+
export const syncConfig = defineSyncConfig({
|
|
93
|
+
database: () => SQLite.openDatabaseAsync('app.db'),
|
|
94
|
+
tables: ['properties', 'tenants'],
|
|
95
|
+
autoSchema: true,
|
|
96
|
+
|
|
97
|
+
// Convex backend configuration
|
|
98
|
+
convex: {
|
|
99
|
+
client: convexClient,
|
|
100
|
+
pushMutation: 'sync:pushBatch', // Or api.sync.pushBatch
|
|
101
|
+
pullQuery: 'sync:pullDeltas', // Or api.sync.pullDeltas
|
|
102
|
+
deletedColumn: 'deleted',
|
|
103
|
+
updatedAtColumn: 'updatedAt',
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
conflictResolver: 'last-write-wins',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export const client = await createSyncClient(syncConfig);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### Option C: With Firebase / Cloud Firestore
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// sync.config.ts
|
|
116
|
+
import { defineSyncConfig, createSyncClient } from 'local-lite';
|
|
117
|
+
import { initializeApp } from 'firebase/app';
|
|
118
|
+
import { getFirestore } from 'firebase/firestore';
|
|
119
|
+
import * as SQLite from 'expo-sqlite';
|
|
120
|
+
|
|
121
|
+
const app = initializeApp({
|
|
122
|
+
apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY,
|
|
123
|
+
projectId: process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID,
|
|
124
|
+
});
|
|
125
|
+
const db = getFirestore(app);
|
|
126
|
+
|
|
127
|
+
export const syncConfig = defineSyncConfig({
|
|
128
|
+
database: () => SQLite.openDatabaseAsync('app.db'),
|
|
129
|
+
tables: ['properties', 'tenants'],
|
|
130
|
+
autoSchema: true,
|
|
131
|
+
|
|
132
|
+
// Firebase Firestore backend configuration
|
|
133
|
+
firebase: {
|
|
134
|
+
firestore: db, // Modular Firestore or @react-native-firebase/firestore
|
|
135
|
+
updatedAtColumn: 'updatedAt', // Timestamp field used for delta sync
|
|
136
|
+
deletedColumn: 'deleted', // Soft delete flag
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
conflictResolver: 'last-write-wins',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
export const client = await createSyncClient(syncConfig);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### 2. Use in React Native / Expo UI
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
import { useLiveQuery, useOutboxStatus } from 'local-lite';
|
|
151
|
+
import { client } from './sync.config';
|
|
152
|
+
|
|
153
|
+
export function TodoScreen() {
|
|
154
|
+
// ⚡ Zero-SQL Reactive Live Query (Auto re-renders on local writes AND background remote pulls!)
|
|
155
|
+
const { data: todos } = useLiveQuery(client, 'todos', {
|
|
156
|
+
orderBy: 'id',
|
|
157
|
+
order: 'desc',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const { pendingCount } = useOutboxStatus(client);
|
|
161
|
+
|
|
162
|
+
const addTodo = async (title: string) => {
|
|
163
|
+
await client.insert('todos', {
|
|
164
|
+
title,
|
|
165
|
+
completed: 0,
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const toggleTodo = async (id: string, completed: number) => {
|
|
170
|
+
await client.update('todos', id, { completed: completed ? 0 : 1 });
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const removeTodo = async (id: string) => {
|
|
174
|
+
await client.delete('todos', id);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<View>
|
|
179
|
+
<Text>Pending outbox items: {pendingCount}</Text>
|
|
180
|
+
{todos.map(t => <Text key={t.id}>{t.title}</Text>)}
|
|
181
|
+
</View>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Supabase Backend Setup & Schema
|
|
189
|
+
|
|
190
|
+
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**):
|
|
191
|
+
|
|
192
|
+
```sql
|
|
193
|
+
-- 1. Automatic Timestamp Trigger
|
|
194
|
+
CREATE OR REPLACE FUNCTION public.set_sync_updated_at()
|
|
195
|
+
RETURNS TRIGGER AS $$
|
|
196
|
+
BEGIN
|
|
197
|
+
NEW.updated_at = timezone('utc'::text, now());
|
|
198
|
+
RETURN NEW;
|
|
199
|
+
END;
|
|
200
|
+
$$ LANGUAGE plpgsql;
|
|
201
|
+
|
|
202
|
+
-- 2. Create Synced Table (e.g. 'todos')
|
|
203
|
+
CREATE TABLE IF NOT EXISTS public.todos (
|
|
204
|
+
id TEXT PRIMARY KEY,
|
|
205
|
+
title TEXT NOT NULL,
|
|
206
|
+
completed INTEGER DEFAULT 0,
|
|
207
|
+
deleted BOOLEAN DEFAULT FALSE NOT NULL,
|
|
208
|
+
created_at TIMESTAMPTZ DEFAULT timezone('utc'::text, now()) NOT NULL,
|
|
209
|
+
updated_at TIMESTAMPTZ DEFAULT timezone('utc'::text, now()) NOT NULL
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
-- 3. Index for high-performance delta sync queries
|
|
213
|
+
CREATE INDEX IF NOT EXISTS idx_todos_sync ON public.todos (updated_at);
|
|
214
|
+
|
|
215
|
+
-- 4. Auto-update updated_at on changes (including soft deletes)
|
|
216
|
+
CREATE OR REPLACE TRIGGER trigger_todos_updated_at
|
|
217
|
+
BEFORE UPDATE ON public.todos
|
|
218
|
+
FOR EACH ROW EXECUTE FUNCTION public.set_sync_updated_at();
|
|
219
|
+
|
|
220
|
+
-- 5. Enable Supabase Realtime broadcast replication
|
|
221
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.todos;
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Convex Backend Setup & Functions
|
|
227
|
+
|
|
228
|
+
In Convex, create a sync module (e.g., `convex/sync.ts`):
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// convex/sync.ts
|
|
232
|
+
import { mutation, query } from "./_generated/server";
|
|
233
|
+
import { v } from "convex/values";
|
|
234
|
+
|
|
235
|
+
// 1. Push batch mutation from outbox
|
|
236
|
+
export const pushBatch = mutation({
|
|
237
|
+
args: {
|
|
238
|
+
operations: v.array(
|
|
239
|
+
v.object({
|
|
240
|
+
id: v.string(),
|
|
241
|
+
tableName: v.string(),
|
|
242
|
+
rowId: v.string(),
|
|
243
|
+
operation: v.string(),
|
|
244
|
+
payload: v.any(),
|
|
245
|
+
timestamp: v.number(),
|
|
246
|
+
})
|
|
247
|
+
),
|
|
248
|
+
},
|
|
249
|
+
handler: async (ctx, args) => {
|
|
250
|
+
const now = Date.now();
|
|
251
|
+
for (const op of args.operations) {
|
|
252
|
+
const table = op.tableName;
|
|
253
|
+
const existing = await ctx.db
|
|
254
|
+
.query(table as any)
|
|
255
|
+
.filter((q) => q.eq(q.field("id"), op.rowId))
|
|
256
|
+
.first();
|
|
257
|
+
|
|
258
|
+
if (op.operation === "DELETE") {
|
|
259
|
+
if (existing) {
|
|
260
|
+
await ctx.db.patch(existing._id, { deleted: true, updatedAt: now });
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
const doc = { ...(op.payload || {}), id: op.rowId, deleted: false, updatedAt: now };
|
|
264
|
+
if (existing) {
|
|
265
|
+
await ctx.db.patch(existing._id, doc);
|
|
266
|
+
} else {
|
|
267
|
+
await ctx.db.insert(table as any, doc);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return true;
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// 2. Pull deltas query since timestamp cursor
|
|
276
|
+
export const pullDeltas = query({
|
|
277
|
+
args: {
|
|
278
|
+
sinceTimestamp: v.number(),
|
|
279
|
+
tables: v.array(v.string()),
|
|
280
|
+
},
|
|
281
|
+
handler: async (ctx, args) => {
|
|
282
|
+
const results: any[] = [];
|
|
283
|
+
for (const table of args.tables) {
|
|
284
|
+
const docs = await ctx.db
|
|
285
|
+
.query(table as any)
|
|
286
|
+
.filter((q) => q.gt(q.field("updatedAt"), args.sinceTimestamp))
|
|
287
|
+
.collect();
|
|
288
|
+
|
|
289
|
+
for (const d of docs) {
|
|
290
|
+
results.push({ ...d, tableName: table });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return results;
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Supported SQLite Drivers
|
|
301
|
+
|
|
302
|
+
`local-lite` is fully driver-agnostic and auto-detects your SQLite instance directly:
|
|
303
|
+
|
|
304
|
+
| Platform | Recommended Driver | Auto-Detected or Subpath Import |
|
|
305
|
+
| :--- | :--- | :--- |
|
|
306
|
+
| **Expo (iOS/Android/Web)** | `expo-sqlite` | `database: () => SQLite.openDatabaseAsync('app.db')` |
|
|
307
|
+
| **Bare React Native** | `@op-engineering/op-sqlite` | `local-lite/op-sqlite` |
|
|
308
|
+
| **Node.js / Electron / Desktop** | `better-sqlite3` | `local-lite/better-sqlite3` |
|
|
309
|
+
| **Edge / Server / Turso** | `@libsql/client` (LibSQL) | `local-lite/libsql` |
|
|
310
|
+
| **Web Browser / PWAs** | `sql.js` (WebAssembly) | `local-lite/sql-js` |
|
|
311
|
+
| **Node.js 22+ (Built-in)** | `node:sqlite` (DatabaseSync) | `local-lite/node-sqlite` |
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Architecture
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
[ App UI ]
|
|
319
|
+
│
|
|
320
|
+
▼
|
|
321
|
+
LocalFirstClient ──(Atomic Transaction)──► Local SQLite Table
|
|
322
|
+
│ │
|
|
323
|
+
▼ ▼
|
|
324
|
+
sync_outbox (Pending queue) EventEmitter (UI refresh)
|
|
325
|
+
│
|
|
326
|
+
▼ (Immediate / Debounced / Network-Aware)
|
|
327
|
+
SyncWorker
|
|
328
|
+
│
|
|
329
|
+
├──► pushChanges() ──► Remote Backend (Supabase / Convex / Firebase / Custom)
|
|
330
|
+
│
|
|
331
|
+
└──◄ fetchChanges() ◄── Remote Deltas (Merged via Conflict Resolver)
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Example App
|
|
337
|
+
|
|
338
|
+
An interactive Expo app is included in the [`example/`](./example) directory demonstrating:
|
|
339
|
+
- Rental property & tenant management application
|
|
340
|
+
- Live SQLite reads and atomic dual-writes
|
|
341
|
+
- Network online/offline toggle simulation
|
|
342
|
+
- Real-time `sync_outbox` inspector
|
|
343
|
+
- Simulated server delta broadcasts
|
|
344
|
+
|
|
345
|
+
To run the example:
|
|
346
|
+
```bash
|
|
347
|
+
cd example
|
|
348
|
+
npm start
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Testing
|
|
354
|
+
|
|
355
|
+
Run the Vitest test suite:
|
|
356
|
+
```bash
|
|
357
|
+
npm test
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## License
|
|
361
|
+
|
|
362
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { LocalDBAdapter, LocalTransaction } from './index.js';
|
|
2
|
+
export interface BetterSqliteStatement {
|
|
3
|
+
reader?: boolean;
|
|
4
|
+
all(...params: any[]): any[];
|
|
5
|
+
run(...params: any[]): {
|
|
6
|
+
changes: number;
|
|
7
|
+
lastInsertRowid: number | bigint;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface BetterSqliteDatabase {
|
|
11
|
+
prepare(query: string): BetterSqliteStatement;
|
|
12
|
+
exec?(sql: string): void;
|
|
13
|
+
close?(): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Adapter for better-sqlite3 in Node.js, Electron, Next.js, and server environments.
|
|
17
|
+
*/
|
|
18
|
+
export declare class BetterSQLite3Adapter implements LocalDBAdapter {
|
|
19
|
+
private db;
|
|
20
|
+
constructor(betterSqliteInstance: any);
|
|
21
|
+
execute(query: string, params?: any[]): Promise<any[]>;
|
|
22
|
+
transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BetterSQLite3Adapter = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Adapter for better-sqlite3 in Node.js, Electron, Next.js, and server environments.
|
|
6
|
+
*/
|
|
7
|
+
class BetterSQLite3Adapter {
|
|
8
|
+
db;
|
|
9
|
+
constructor(betterSqliteInstance) {
|
|
10
|
+
this.db = betterSqliteInstance;
|
|
11
|
+
}
|
|
12
|
+
async execute(query, params = []) {
|
|
13
|
+
const trimmed = query.trim();
|
|
14
|
+
const stmt = this.db.prepare(trimmed);
|
|
15
|
+
// If query returns rows (SELECT, PRAGMA) or is marked as reader
|
|
16
|
+
if (stmt.reader || /^SELECT|^PRAGMA/i.test(trimmed)) {
|
|
17
|
+
return stmt.all(...params);
|
|
18
|
+
}
|
|
19
|
+
const info = stmt.run(...params);
|
|
20
|
+
return [info];
|
|
21
|
+
}
|
|
22
|
+
async transaction(action) {
|
|
23
|
+
// Explicit transaction control compatible with async actions
|
|
24
|
+
this.db.prepare('BEGIN IMMEDIATE').run();
|
|
25
|
+
try {
|
|
26
|
+
const txWrapper = {
|
|
27
|
+
execute: async (q, p = []) => {
|
|
28
|
+
return await this.execute(q, p);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
const result = await action(txWrapper);
|
|
32
|
+
this.db.prepare('COMMIT').run();
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
try {
|
|
37
|
+
this.db.prepare('ROLLBACK').run();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Ignore secondary rollback error if connection closed
|
|
41
|
+
}
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.BetterSQLite3Adapter = BetterSQLite3Adapter;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { LocalDBAdapter, LocalTransaction } from './index.js';
|
|
2
|
+
export declare class ExpoSQLiteAdapter implements LocalDBAdapter {
|
|
3
|
+
private db;
|
|
4
|
+
private txMutex;
|
|
5
|
+
constructor(expoDbInstance: any);
|
|
6
|
+
execute(query: string, params?: any[]): Promise<any[]>;
|
|
7
|
+
transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExpoSQLiteAdapter = void 0;
|
|
4
|
+
class ExpoSQLiteAdapter {
|
|
5
|
+
db;
|
|
6
|
+
txMutex = Promise.resolve();
|
|
7
|
+
constructor(expoDbInstance) {
|
|
8
|
+
this.db = expoDbInstance;
|
|
9
|
+
}
|
|
10
|
+
async execute(query, params = []) {
|
|
11
|
+
return await this.db.getAllAsync(query, params);
|
|
12
|
+
}
|
|
13
|
+
async transaction(action) {
|
|
14
|
+
const run = async () => {
|
|
15
|
+
let result;
|
|
16
|
+
await this.db.withTransactionAsync(async () => {
|
|
17
|
+
const txWrapper = {
|
|
18
|
+
execute: async (q, p = []) => await this.db.getAllAsync(q, p)
|
|
19
|
+
};
|
|
20
|
+
result = await action(txWrapper);
|
|
21
|
+
});
|
|
22
|
+
return result;
|
|
23
|
+
};
|
|
24
|
+
// Serialize transactions to prevent SQLite "cannot start a transaction within a transaction"
|
|
25
|
+
const currentTx = this.txMutex.then(run, run);
|
|
26
|
+
this.txMutex = currentTx.catch(() => { });
|
|
27
|
+
return await currentTx;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.ExpoSQLiteAdapter = ExpoSQLiteAdapter;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface LocalTransaction {
|
|
2
|
+
execute(query: string, params?: any[]): Promise<any[]>;
|
|
3
|
+
}
|
|
4
|
+
export interface LocalDBAdapter {
|
|
5
|
+
execute(query: string, params?: any[]): Promise<any[]>;
|
|
6
|
+
transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
|
|
7
|
+
}
|
|
8
|
+
export * from './expo-sqlite.js';
|
|
9
|
+
export * from './op-sqlite.js';
|
|
10
|
+
export * from './better-sqlite3.js';
|
|
11
|
+
export * from './libsql.js';
|
|
12
|
+
export * from './sql-js.js';
|
|
13
|
+
export * from './node-sqlite.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./expo-sqlite.js"), exports);
|
|
18
|
+
__exportStar(require("./op-sqlite.js"), exports);
|
|
19
|
+
__exportStar(require("./better-sqlite3.js"), exports);
|
|
20
|
+
__exportStar(require("./libsql.js"), exports);
|
|
21
|
+
__exportStar(require("./sql-js.js"), exports);
|
|
22
|
+
__exportStar(require("./node-sqlite.js"), exports);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { LocalDBAdapter, LocalTransaction } from './index.js';
|
|
2
|
+
export interface LibSQLRow {
|
|
3
|
+
[key: string]: any;
|
|
4
|
+
}
|
|
5
|
+
export interface LibSQLResultSet {
|
|
6
|
+
columns: string[];
|
|
7
|
+
rows: any[];
|
|
8
|
+
rowsAffected?: number;
|
|
9
|
+
lastInsertRowid?: bigint | number;
|
|
10
|
+
}
|
|
11
|
+
export interface LibSQLClient {
|
|
12
|
+
execute(stmt: any): Promise<LibSQLResultSet>;
|
|
13
|
+
transaction?(mode?: 'write' | 'read' | 'deferred'): Promise<any>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Adapter for @libsql/client (Turso & LibSQL).
|
|
17
|
+
* Supports local SQLite files, embedded replicas, in-memory databases, and server/edge deployments.
|
|
18
|
+
*/
|
|
19
|
+
export declare class LibSQLAdapter implements LocalDBAdapter {
|
|
20
|
+
private client;
|
|
21
|
+
constructor(libsqlClient: any);
|
|
22
|
+
execute(query: string, params?: any[]): Promise<any[]>;
|
|
23
|
+
transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LibSQLAdapter = void 0;
|
|
4
|
+
function normalizeRows(res) {
|
|
5
|
+
if (!res || !res.rows)
|
|
6
|
+
return [];
|
|
7
|
+
const { columns, rows } = res;
|
|
8
|
+
if (rows.length === 0)
|
|
9
|
+
return [];
|
|
10
|
+
// If rows are already standard objects
|
|
11
|
+
if (typeof rows[0] === 'object' && !Array.isArray(rows[0]) && rows[0] !== null) {
|
|
12
|
+
return rows;
|
|
13
|
+
}
|
|
14
|
+
// If rows are tuples/arrays, map them to column names
|
|
15
|
+
if (Array.isArray(rows[0]) && Array.isArray(columns)) {
|
|
16
|
+
return rows.map((rowArr) => {
|
|
17
|
+
const obj = {};
|
|
18
|
+
columns.forEach((col, i) => {
|
|
19
|
+
obj[col] = rowArr[i];
|
|
20
|
+
});
|
|
21
|
+
return obj;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return rows;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Adapter for @libsql/client (Turso & LibSQL).
|
|
28
|
+
* Supports local SQLite files, embedded replicas, in-memory databases, and server/edge deployments.
|
|
29
|
+
*/
|
|
30
|
+
class LibSQLAdapter {
|
|
31
|
+
client;
|
|
32
|
+
constructor(libsqlClient) {
|
|
33
|
+
this.client = libsqlClient;
|
|
34
|
+
}
|
|
35
|
+
async execute(query, params = []) {
|
|
36
|
+
const res = await this.client.execute({
|
|
37
|
+
sql: query,
|
|
38
|
+
args: params,
|
|
39
|
+
});
|
|
40
|
+
return normalizeRows(res);
|
|
41
|
+
}
|
|
42
|
+
async transaction(action) {
|
|
43
|
+
if (typeof this.client.transaction === 'function') {
|
|
44
|
+
const tx = await this.client.transaction('write');
|
|
45
|
+
try {
|
|
46
|
+
const txWrapper = {
|
|
47
|
+
execute: async (q, p = []) => {
|
|
48
|
+
const res = await tx.execute({ sql: q, args: p });
|
|
49
|
+
return normalizeRows(res);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
const result = await action(txWrapper);
|
|
53
|
+
await tx.commit();
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
try {
|
|
58
|
+
await tx.rollback();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// ignore rollback failure
|
|
62
|
+
}
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Fallback using SQL statements
|
|
67
|
+
await this.execute('BEGIN IMMEDIATE');
|
|
68
|
+
try {
|
|
69
|
+
const txWrapper = {
|
|
70
|
+
execute: async (q, p = []) => this.execute(q, p),
|
|
71
|
+
};
|
|
72
|
+
const result = await action(txWrapper);
|
|
73
|
+
await this.execute('COMMIT');
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
try {
|
|
78
|
+
await this.execute('ROLLBACK');
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// ignore
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.LibSQLAdapter = LibSQLAdapter;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { LocalDBAdapter, LocalTransaction } from './index.js';
|
|
2
|
+
export interface NodeSqliteStatement {
|
|
3
|
+
all(...params: any[]): any[];
|
|
4
|
+
run(...params: any[]): {
|
|
5
|
+
changes: number;
|
|
6
|
+
lastInsertRowid: number | bigint;
|
|
7
|
+
};
|
|
8
|
+
get?(...params: any[]): any;
|
|
9
|
+
}
|
|
10
|
+
export interface NodeSqliteDatabaseSync {
|
|
11
|
+
prepare(sql: string): NodeSqliteStatement;
|
|
12
|
+
exec(sql: string): void;
|
|
13
|
+
close?(): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Adapter for Node.js 22+ built-in `node:sqlite` (DatabaseSync).
|
|
17
|
+
* Provides zero-dependency, native SQLite on modern Node.js runtimes.
|
|
18
|
+
*/
|
|
19
|
+
export declare class NodeSQLiteAdapter implements LocalDBAdapter {
|
|
20
|
+
private db;
|
|
21
|
+
constructor(nodeSqliteDb: any);
|
|
22
|
+
execute(query: string, params?: any[]): Promise<any[]>;
|
|
23
|
+
transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
|
|
24
|
+
}
|