local-lite 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +421 -362
- package/dist/adapters/local/better-sqlite3.d.ts +2 -0
- package/dist/adapters/local/better-sqlite3.js +46 -25
- package/dist/adapters/local/expo-sqlite.d.ts +5 -1
- package/dist/adapters/local/expo-sqlite.js +40 -7
- package/dist/adapters/local/index.d.ts +1 -0
- package/dist/adapters/local/node-sqlite.d.ts +2 -0
- package/dist/adapters/local/node-sqlite.js +45 -22
- package/dist/adapters/local/op-sqlite.d.ts +1 -0
- package/dist/adapters/local/op-sqlite.js +6 -0
- package/dist/adapters/local/sql-js.d.ts +1 -0
- package/dist/adapters/local/sql-js.js +5 -0
- package/dist/adapters/network/index.js +33 -0
- package/dist/adapters/remote/convex.d.ts +2 -2
- package/dist/adapters/remote/convex.js +8 -12
- package/dist/adapters/remote/demo.d.ts +1 -1
- package/dist/adapters/remote/demo.js +3 -0
- package/dist/adapters/remote/firestore.d.ts +3 -2
- package/dist/adapters/remote/firestore.js +69 -46
- package/dist/adapters/remote/index.d.ts +6 -4
- package/dist/adapters/remote/supabase.d.ts +34 -6
- package/dist/adapters/remote/supabase.js +340 -79
- package/dist/config/index.js +7 -10
- package/dist/config/resolve-local.js +12 -1
- package/dist/config/resolve-remote.js +33 -28
- package/dist/config/types.d.ts +30 -5
- package/dist/core/client.d.ts +133 -5
- package/dist/core/client.js +431 -46
- package/dist/core/index.d.ts +15 -0
- package/dist/core/index.js +30 -0
- package/dist/core/outbox-store.d.ts +8 -4
- package/dist/core/outbox-store.js +84 -65
- package/dist/core/query-builder.d.ts +119 -0
- package/dist/core/query-builder.js +372 -0
- package/dist/core/query.d.ts +8 -0
- package/dist/core/query.js +37 -0
- package/dist/core/schema-manager.d.ts +9 -0
- package/dist/core/schema-manager.js +103 -0
- package/dist/core/sync-worker.d.ts +38 -1
- package/dist/core/sync-worker.js +351 -104
- package/dist/core/table-manager.js +26 -29
- package/dist/core/types.d.ts +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/react/BenchmarkModal.d.ts +32 -0
- package/dist/react/BenchmarkModal.js +777 -0
- package/dist/react/SyncInspector.d.ts +31 -0
- package/dist/react/SyncInspector.js +585 -0
- package/dist/react/index.d.ts +89 -18
- package/dist/react/index.js +450 -105
- package/dist/react/runner.d.ts +7 -0
- package/dist/react/runner.js +42 -0
- package/dist/react/status.d.ts +24 -0
- package/dist/react/status.js +123 -0
- package/dist/react/theme.d.ts +18 -0
- package/dist/react/theme.js +46 -0
- package/dist/schema/generator.d.ts +49 -0
- package/dist/schema/generator.js +209 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +17 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.js +20 -0
- package/package.json +143 -133
package/README.md
CHANGED
|
@@ -1,362 +1,421 @@
|
|
|
1
|
-
# ⚡ local-lite
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* **[
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
1
|
+
# ⚡ local-lite
|
|
2
|
+
|
|
3
|
+
[](https://georgechitechi.github.io/local-lite/)
|
|
4
|
+
[](https://www.npmjs.com/package/local-lite)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
A lightweight, vendor-agnostic **local-first synchronization engine** for React Native, Expo, and Web applications.
|
|
8
|
+
|
|
9
|
+
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).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 📖 Documentation
|
|
14
|
+
|
|
15
|
+
> 🌐 **Interactive Documentation Site:** **[https://georgechitechi.github.io/local-lite/](https://georgechitechi.github.io/local-lite/)**
|
|
16
|
+
|
|
17
|
+
* **[Quickstart & Setup](https://georgechitechi.github.io/local-lite/#/getting-started)** - 5-minute setup with Expo, React Native, or Node.js
|
|
18
|
+
* **[Architecture](https://georgechitechi.github.io/local-lite/#/architecture)** - Transactional outbox pattern, SyncWorker loops, and event pipeline
|
|
19
|
+
* **[Configuration Reference](https://georgechitechi.github.io/local-lite/#/configuration)** - Options for `defineSyncConfig()`, retries, and loggers
|
|
20
|
+
* **[Local SQLite Adapters](https://georgechitechi.github.io/local-lite/#/adapters-local)** - Expo SQLite, OP-SQLite, better-sqlite3, node:sqlite, and LibSQL
|
|
21
|
+
* **[Remote Cloud Adapters](https://georgechitechi.github.io/local-lite/#/adapters-remote)** - Supabase, Convex, Cloud Firestore, and custom adapters
|
|
22
|
+
* **[React & Expo Hooks](https://georgechitechi.github.io/local-lite/#/react-hooks)** - `useLiveQuery()` and `useSyncStatus()`
|
|
23
|
+
* **[Conflict Resolution](https://georgechitechi.github.io/local-lite/#/conflict-resolution)** - LWW, Client-Wins, Server-Wins, and custom resolvers
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Independent core and explicit policies
|
|
28
|
+
|
|
29
|
+
Use `local-lite/core` for the sync engine with your own `LocalDBAdapter` and
|
|
30
|
+
`RemoteDBAdapter`. This entry point does not load provider implementations,
|
|
31
|
+
configuration auto-detection, or React. UI hooks and backend adapters remain optional.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { LocalFirstClient } from 'local-lite/core';
|
|
35
|
+
|
|
36
|
+
const client = new LocalFirstClient({
|
|
37
|
+
local: myLocalAdapter,
|
|
38
|
+
remote: myRemoteAdapter,
|
|
39
|
+
autoSchema: false,
|
|
40
|
+
autoStart: false,
|
|
41
|
+
conflictResolver: myConflictResolver,
|
|
42
|
+
});
|
|
43
|
+
await client.initialize();
|
|
44
|
+
// Writes remain in the outbox until you push manually or enable automatic sync.
|
|
45
|
+
await client.syncWorker.pushLocalChanges();
|
|
46
|
+
// Or enable polling, reconnect handling, and realtime subscriptions:
|
|
47
|
+
client.syncWorker.start();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Authentication, account switching, application schemas, UI reloads, and decisions
|
|
51
|
+
to discard failed writes belong to the caller. The core does not inspect backend
|
|
52
|
+
error codes to delete failed mutations. Conflict resolution and retry settings
|
|
53
|
+
remain configurable; the existing default conflict strategy is last-write-wins.
|
|
54
|
+
|
|
55
|
+
### Compatibility changes from 0.0.3
|
|
56
|
+
|
|
57
|
+
- Missing remote configuration now throws. Temporary demo behavior requires
|
|
58
|
+
`inMemoryRemote: true`; it must never silently acknowledge production writes.
|
|
59
|
+
- `autoSchema` now defaults to `false` in the configuration factory as well as the
|
|
60
|
+
core constructor. Set `autoSchema: true` to retain inferred schema behavior.
|
|
61
|
+
- Migrations run transactionally before sync starts, including when initialization
|
|
62
|
+
is deferred with `autoInitialize: false`. Use idempotent SQL; this is not a
|
|
63
|
+
versioned migration framework.
|
|
64
|
+
- `initialize()` is safe to call repeatedly or concurrently. After `stop()`, call
|
|
65
|
+
`client.syncWorker.start()` to resume. Mutations after stop remain queued.
|
|
66
|
+
- `autoStart: false` prepares storage without automatic sync. Explicit push/pull
|
|
67
|
+
methods remain available. Stop does not cancel adapter calls already in flight.
|
|
68
|
+
- Realtime changes update local rows without advancing the durable pull cursor.
|
|
69
|
+
Custom pull adapters must return complete batches through their checkpoint and
|
|
70
|
+
reject partial failures. Supabase table failures now reject the pull.
|
|
71
|
+
- Custom realtime adapters may return an unsubscribe function. Legacy adapters
|
|
72
|
+
returning void remain supported; stopped workers ignore their later callbacks.
|
|
73
|
+
|
|
74
|
+
## Features
|
|
75
|
+
|
|
76
|
+
Interrupted pushes recover automatically at startup and before the next push cycle,
|
|
77
|
+
including when a temporary database error prevented saving a failure status.
|
|
78
|
+
Use one active sync client per local database. Recovery may replay a remote write,
|
|
79
|
+
so custom remote adapters should handle repeated operation IDs idempotently.
|
|
80
|
+
This recovers sync state; it does not repair corrupt SQLite files or cancel hung adapter calls.
|
|
81
|
+
|
|
82
|
+
* **⚡ 0ms Optimistic Writes:** Write to local SQLite immediately via atomic dual-write (`client.mutate()`).
|
|
83
|
+
* **📦 Transactional Outbox:** Mutations are stored atomically in a local `sync_outbox` table, preventing data loss if the app closes or loses connectivity mid-write.
|
|
84
|
+
* **🌐 Native Cloud Adapters:** First-class sync with Postgres tables (via **Supabase**), reactive document datastores (via **Convex**), and document collections (via **Firebase / Firestore**).
|
|
85
|
+
* **🛡️ Conflict Resolution:** Built-in strategies including **Last-Write-Wins (LWW)**, Server-Wins, Client-Wins, or custom resolver functions.
|
|
86
|
+
* **📶 Network-Aware:** Automatically pauses polling when offline and triggers immediate sync flushes the instant connectivity is restored.
|
|
87
|
+
* **🔄 Infinite-Loop Safe:** Incoming remote deltas update local tables and fire reactive UI listeners without echoing back into the outbox.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Installation
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm install local-lite
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Peer dependencies (install what you use):
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# If using Expo SQLite
|
|
101
|
+
npx expo install expo-sqlite @react-native-community/netinfo
|
|
102
|
+
|
|
103
|
+
# If syncing with Supabase
|
|
104
|
+
npm install @supabase/supabase-js
|
|
105
|
+
|
|
106
|
+
# If syncing with Convex
|
|
107
|
+
npm install convex
|
|
108
|
+
|
|
109
|
+
# If syncing with Firebase / Firestore
|
|
110
|
+
npm install firebase # or @react-native-firebase/firestore
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### 1. Define your Configuration (`sync.config.ts`)
|
|
116
|
+
|
|
117
|
+
#### Option A: With Supabase
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// sync.config.ts
|
|
121
|
+
import { defineSyncConfig, createSyncClient } from 'local-lite';
|
|
122
|
+
import * as SQLite from 'expo-sqlite';
|
|
123
|
+
|
|
124
|
+
export const syncConfig = defineSyncConfig({
|
|
125
|
+
database: () => SQLite.openDatabaseAsync('app.db'),
|
|
126
|
+
tables: ['todos', 'notes'],
|
|
127
|
+
autoSchema: true,
|
|
128
|
+
|
|
129
|
+
// Supabase backend configuration
|
|
130
|
+
supabase: {
|
|
131
|
+
url: process.env.EXPO_PUBLIC_SUPABASE_URL || 'https://xyz.supabase.co',
|
|
132
|
+
anonKey: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || 'YOUR_ANON_KEY',
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
conflictResolver: 'last-write-wins',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export const client = await createSyncClient(syncConfig);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### Option B: With Convex
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// sync.config.ts
|
|
145
|
+
import { defineSyncConfig, createSyncClient } from 'local-lite';
|
|
146
|
+
import { ConvexClient } from 'convex/browser';
|
|
147
|
+
import * as SQLite from 'expo-sqlite';
|
|
148
|
+
|
|
149
|
+
const convexClient = new ConvexClient(process.env.EXPO_PUBLIC_CONVEX_URL!);
|
|
150
|
+
|
|
151
|
+
export const syncConfig = defineSyncConfig({
|
|
152
|
+
database: () => SQLite.openDatabaseAsync('app.db'),
|
|
153
|
+
tables: ['properties', 'tenants'],
|
|
154
|
+
autoSchema: true,
|
|
155
|
+
|
|
156
|
+
// Convex backend configuration
|
|
157
|
+
convex: {
|
|
158
|
+
client: convexClient,
|
|
159
|
+
pushMutation: 'sync:pushBatch', // Or api.sync.pushBatch
|
|
160
|
+
pullQuery: 'sync:pullDeltas', // Or api.sync.pullDeltas
|
|
161
|
+
deletedColumn: 'deleted',
|
|
162
|
+
updatedAtColumn: 'updatedAt',
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
conflictResolver: 'last-write-wins',
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
export const client = await createSyncClient(syncConfig);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### Option C: With Firebase / Cloud Firestore
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// sync.config.ts
|
|
175
|
+
import { defineSyncConfig, createSyncClient } from 'local-lite';
|
|
176
|
+
import { initializeApp } from 'firebase/app';
|
|
177
|
+
import { getFirestore } from 'firebase/firestore';
|
|
178
|
+
import * as SQLite from 'expo-sqlite';
|
|
179
|
+
|
|
180
|
+
const app = initializeApp({
|
|
181
|
+
apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY,
|
|
182
|
+
projectId: process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID,
|
|
183
|
+
});
|
|
184
|
+
const db = getFirestore(app);
|
|
185
|
+
|
|
186
|
+
export const syncConfig = defineSyncConfig({
|
|
187
|
+
database: () => SQLite.openDatabaseAsync('app.db'),
|
|
188
|
+
tables: ['properties', 'tenants'],
|
|
189
|
+
autoSchema: true,
|
|
190
|
+
|
|
191
|
+
// Firebase Firestore backend configuration
|
|
192
|
+
firebase: {
|
|
193
|
+
firestore: db, // Modular Firestore or @react-native-firebase/firestore
|
|
194
|
+
updatedAtColumn: 'updatedAt', // Timestamp field used for delta sync
|
|
195
|
+
deletedColumn: 'deleted', // Soft delete flag
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
conflictResolver: 'last-write-wins',
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
export const client = await createSyncClient(syncConfig);
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### 2. Use in React Native / Expo UI
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
import { useLiveQuery, useSyncStatus } from 'local-lite/react';
|
|
210
|
+
import { client } from './sync.config';
|
|
211
|
+
|
|
212
|
+
export function TodoScreen() {
|
|
213
|
+
// ⚡ Zero-SQL Reactive Live Query (Auto re-renders on local writes AND background remote pulls!)
|
|
214
|
+
const { data: todos } = useLiveQuery(client, 'todos', {
|
|
215
|
+
orderBy: 'id',
|
|
216
|
+
order: 'desc',
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const { pendingCount, isOnline, isSyncing } = useSyncStatus(client);
|
|
220
|
+
|
|
221
|
+
const addTodo = async (title: string) => {
|
|
222
|
+
await client.insert('todos', {
|
|
223
|
+
title,
|
|
224
|
+
completed: 0,
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const toggleTodo = async (id: string, completed: number) => {
|
|
229
|
+
await client.update('todos', id, { completed: completed ? 0 : 1 });
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const removeTodo = async (id: string) => {
|
|
233
|
+
await client.delete('todos', id);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<View>
|
|
238
|
+
<Text>Pending outbox items: {pendingCount}</Text>
|
|
239
|
+
{todos.map(t => <Text key={t.id}>{t.title}</Text>)}
|
|
240
|
+
</View>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Supabase Backend Setup & Schema
|
|
248
|
+
|
|
249
|
+
To synchronize with Supabase using zero-clock-skew delta queries and `deleted: true` soft deletes, execute the following script in your Supabase project (**Dashboard → SQL Editor**):
|
|
250
|
+
|
|
251
|
+
```sql
|
|
252
|
+
-- 1. Automatic Timestamp Trigger
|
|
253
|
+
CREATE OR REPLACE FUNCTION public.set_sync_updated_at()
|
|
254
|
+
RETURNS TRIGGER AS $$
|
|
255
|
+
BEGIN
|
|
256
|
+
NEW.updated_at = timezone('utc'::text, now());
|
|
257
|
+
RETURN NEW;
|
|
258
|
+
END;
|
|
259
|
+
$$ LANGUAGE plpgsql;
|
|
260
|
+
|
|
261
|
+
-- 2. Create Synced Table (e.g. 'todos')
|
|
262
|
+
CREATE TABLE IF NOT EXISTS public.todos (
|
|
263
|
+
id TEXT PRIMARY KEY,
|
|
264
|
+
title TEXT NOT NULL,
|
|
265
|
+
completed INTEGER DEFAULT 0,
|
|
266
|
+
deleted BOOLEAN DEFAULT FALSE NOT NULL,
|
|
267
|
+
created_at TIMESTAMPTZ DEFAULT timezone('utc'::text, now()) NOT NULL,
|
|
268
|
+
updated_at TIMESTAMPTZ DEFAULT timezone('utc'::text, now()) NOT NULL
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
-- 3. Index for high-performance delta sync queries
|
|
272
|
+
CREATE INDEX IF NOT EXISTS idx_todos_sync ON public.todos (updated_at);
|
|
273
|
+
|
|
274
|
+
-- 4. Auto-update updated_at on changes (including soft deletes)
|
|
275
|
+
CREATE OR REPLACE TRIGGER trigger_todos_updated_at
|
|
276
|
+
BEFORE UPDATE ON public.todos
|
|
277
|
+
FOR EACH ROW EXECUTE FUNCTION public.set_sync_updated_at();
|
|
278
|
+
|
|
279
|
+
-- 5. Enable Supabase Realtime broadcast replication
|
|
280
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.todos;
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Convex Backend Setup & Functions
|
|
286
|
+
|
|
287
|
+
In Convex, create a sync module (e.g., `convex/sync.ts`):
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// convex/sync.ts
|
|
291
|
+
import { mutation, query } from "./_generated/server";
|
|
292
|
+
import { v } from "convex/values";
|
|
293
|
+
|
|
294
|
+
// 1. Push batch mutation from outbox
|
|
295
|
+
export const pushBatch = mutation({
|
|
296
|
+
args: {
|
|
297
|
+
operations: v.array(
|
|
298
|
+
v.object({
|
|
299
|
+
id: v.string(),
|
|
300
|
+
tableName: v.string(),
|
|
301
|
+
rowId: v.string(),
|
|
302
|
+
operation: v.string(),
|
|
303
|
+
payload: v.any(),
|
|
304
|
+
timestamp: v.number(),
|
|
305
|
+
})
|
|
306
|
+
),
|
|
307
|
+
},
|
|
308
|
+
handler: async (ctx, args) => {
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
for (const op of args.operations) {
|
|
311
|
+
const table = op.tableName;
|
|
312
|
+
const existing = await ctx.db
|
|
313
|
+
.query(table as any)
|
|
314
|
+
.filter((q) => q.eq(q.field("id"), op.rowId))
|
|
315
|
+
.first();
|
|
316
|
+
|
|
317
|
+
if (op.operation === "DELETE") {
|
|
318
|
+
if (existing) {
|
|
319
|
+
await ctx.db.patch(existing._id, { deleted: true, updatedAt: now });
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
const doc = { ...(op.payload || {}), id: op.rowId, deleted: false, updatedAt: now };
|
|
323
|
+
if (existing) {
|
|
324
|
+
await ctx.db.patch(existing._id, doc);
|
|
325
|
+
} else {
|
|
326
|
+
await ctx.db.insert(table as any, doc);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return true;
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// 2. Pull deltas query since timestamp cursor
|
|
335
|
+
export const pullDeltas = query({
|
|
336
|
+
args: {
|
|
337
|
+
sinceTimestamp: v.number(),
|
|
338
|
+
tables: v.array(v.string()),
|
|
339
|
+
},
|
|
340
|
+
handler: async (ctx, args) => {
|
|
341
|
+
const results: any[] = [];
|
|
342
|
+
for (const table of args.tables) {
|
|
343
|
+
const docs = await ctx.db
|
|
344
|
+
.query(table as any)
|
|
345
|
+
.filter((q) => q.gt(q.field("updatedAt"), args.sinceTimestamp))
|
|
346
|
+
.collect();
|
|
347
|
+
|
|
348
|
+
for (const d of docs) {
|
|
349
|
+
results.push({ ...d, tableName: table });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return results;
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Supported SQLite Drivers
|
|
360
|
+
|
|
361
|
+
`local-lite` is fully driver-agnostic and auto-detects your SQLite instance directly:
|
|
362
|
+
|
|
363
|
+
| Platform | Recommended Driver | Auto-Detected or Subpath Import |
|
|
364
|
+
| :--- | :--- | :--- |
|
|
365
|
+
| **Expo (iOS/Android/Web)** | `expo-sqlite` | `database: () => SQLite.openDatabaseAsync('app.db')` |
|
|
366
|
+
| **Bare React Native** | `@op-engineering/op-sqlite` | `local-lite/op-sqlite` |
|
|
367
|
+
| **Node.js / Electron / Desktop** | `better-sqlite3` | `local-lite/better-sqlite3` |
|
|
368
|
+
| **Edge / Server / Turso** | `@libsql/client` (LibSQL) | `local-lite/libsql` |
|
|
369
|
+
| **Web Browser / PWAs** | `sql.js` (WebAssembly) | `local-lite/sql-js` |
|
|
370
|
+
| **Node.js 22+ (Built-in)** | `node:sqlite` (DatabaseSync) | `local-lite/node-sqlite` |
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Architecture
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
[ App UI ]
|
|
378
|
+
│
|
|
379
|
+
▼
|
|
380
|
+
LocalFirstClient ──(Atomic Transaction)──► Local SQLite Table
|
|
381
|
+
│ │
|
|
382
|
+
▼ ▼
|
|
383
|
+
sync_outbox (Pending queue) EventEmitter (UI refresh)
|
|
384
|
+
│
|
|
385
|
+
▼ (Immediate / Debounced / Network-Aware)
|
|
386
|
+
SyncWorker
|
|
387
|
+
│
|
|
388
|
+
├──► pushChanges() ──► Remote Backend (Supabase / Convex / Firebase / Custom)
|
|
389
|
+
│
|
|
390
|
+
└──◄ fetchChanges() ◄── Remote Deltas (Merged via Conflict Resolver)
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Example App
|
|
396
|
+
|
|
397
|
+
An interactive Expo app is included in the [`example/`](./example) directory demonstrating:
|
|
398
|
+
- Rental property & tenant management application
|
|
399
|
+
- Live SQLite reads and atomic dual-writes
|
|
400
|
+
- Network online/offline toggle simulation
|
|
401
|
+
- Real-time `sync_outbox` inspector
|
|
402
|
+
- Simulated server delta broadcasts
|
|
403
|
+
|
|
404
|
+
To run the example:
|
|
405
|
+
```bash
|
|
406
|
+
cd example
|
|
407
|
+
npm start
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## Testing
|
|
413
|
+
|
|
414
|
+
Run the Vitest test suite:
|
|
415
|
+
```bash
|
|
416
|
+
npm test
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## License
|
|
420
|
+
|
|
421
|
+
MIT
|