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
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalFirstClient = exports.TransactionalClient = void 0;
|
|
4
|
+
const event_emitter_js_1 = require("../reactive/event-emitter.js");
|
|
5
|
+
const sync_worker_js_1 = require("./sync-worker.js");
|
|
6
|
+
const table_manager_js_1 = require("./table-manager.js");
|
|
7
|
+
const outbox_store_js_1 = require("./outbox-store.js");
|
|
8
|
+
const meta_store_js_1 = require("./meta-store.js");
|
|
9
|
+
const schema_manager_js_1 = require("./schema-manager.js");
|
|
10
|
+
const uuid_js_1 = require("../utils/uuid.js");
|
|
11
|
+
const sql_js_1 = require("../utils/sql.js");
|
|
12
|
+
const query_js_1 = require("./query.js");
|
|
13
|
+
class TransactionalClient {
|
|
14
|
+
parent;
|
|
15
|
+
tx;
|
|
16
|
+
queuedTableEvents;
|
|
17
|
+
queuedOutboxIds;
|
|
18
|
+
constructor(parent, tx, queuedTableEvents, queuedOutboxIds) {
|
|
19
|
+
this.parent = parent;
|
|
20
|
+
this.tx = tx;
|
|
21
|
+
this.queuedTableEvents = queuedTableEvents;
|
|
22
|
+
this.queuedOutboxIds = queuedOutboxIds;
|
|
23
|
+
}
|
|
24
|
+
async mutate(tableName, rowId, operation, payload, options = {}) {
|
|
25
|
+
const pk = options.primaryKey ?? this.parent.getPrimaryKey();
|
|
26
|
+
const outboxId = (0, uuid_js_1.generateUUID)();
|
|
27
|
+
const timestamp = options.customTimestamp ?? Date.now();
|
|
28
|
+
await table_manager_js_1.TableManager.applyMutation(this.tx, tableName, rowId, operation, payload, pk, {
|
|
29
|
+
autoSchema: this.parent.autoSchema,
|
|
30
|
+
schemaManager: this.parent.schemaManager,
|
|
31
|
+
});
|
|
32
|
+
if (!options.skipOutbox) {
|
|
33
|
+
await outbox_store_js_1.OutboxStore.insertRecord(this.tx, {
|
|
34
|
+
id: outboxId,
|
|
35
|
+
tableName,
|
|
36
|
+
rowId,
|
|
37
|
+
operation,
|
|
38
|
+
payload,
|
|
39
|
+
timestamp,
|
|
40
|
+
});
|
|
41
|
+
this.queuedOutboxIds.push(outboxId);
|
|
42
|
+
}
|
|
43
|
+
this.queuedTableEvents.push({
|
|
44
|
+
tableName,
|
|
45
|
+
event: {
|
|
46
|
+
operation,
|
|
47
|
+
rowId,
|
|
48
|
+
payload,
|
|
49
|
+
source: 'local',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async insert(tableName, rowIdOrPayload, payloadOrOptions, options) {
|
|
54
|
+
let rowId;
|
|
55
|
+
let payload;
|
|
56
|
+
let opts = {};
|
|
57
|
+
if (typeof rowIdOrPayload === 'string') {
|
|
58
|
+
rowId = rowIdOrPayload;
|
|
59
|
+
payload = payloadOrOptions || {};
|
|
60
|
+
opts = options || {};
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
payload = rowIdOrPayload || {};
|
|
64
|
+
rowId = payload[this.parent.getPrimaryKey()] || (0, uuid_js_1.generateUUID)();
|
|
65
|
+
opts = payloadOrOptions || {};
|
|
66
|
+
}
|
|
67
|
+
await this.mutate(tableName, rowId, 'INSERT', payload, opts);
|
|
68
|
+
return rowId;
|
|
69
|
+
}
|
|
70
|
+
async update(tableName, rowId, payload, options = {}) {
|
|
71
|
+
await this.mutate(tableName, rowId, 'UPDATE', payload, options);
|
|
72
|
+
}
|
|
73
|
+
async delete(tableName, rowId, options = {}) {
|
|
74
|
+
await this.mutate(tableName, rowId, 'DELETE', null, options);
|
|
75
|
+
}
|
|
76
|
+
get rawTx() {
|
|
77
|
+
return this.tx;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.TransactionalClient = TransactionalClient;
|
|
81
|
+
class LocalFirstClient {
|
|
82
|
+
db;
|
|
83
|
+
remote;
|
|
84
|
+
events;
|
|
85
|
+
syncWorker;
|
|
86
|
+
schemaManager;
|
|
87
|
+
autoSchema;
|
|
88
|
+
tables;
|
|
89
|
+
primaryKey;
|
|
90
|
+
constructor(config) {
|
|
91
|
+
this.db = config.local;
|
|
92
|
+
this.remote = config.remote;
|
|
93
|
+
this.events = new event_emitter_js_1.EventEmitter();
|
|
94
|
+
this.primaryKey = config.primaryKey ?? 'id';
|
|
95
|
+
this.autoSchema = config.autoSchema ?? false;
|
|
96
|
+
this.tables = config.tables ?? [];
|
|
97
|
+
this.schemaManager = new schema_manager_js_1.SchemaManager();
|
|
98
|
+
const workerOptions = {
|
|
99
|
+
network: config.network,
|
|
100
|
+
conflictResolver: config.conflictResolver,
|
|
101
|
+
primaryKey: this.primaryKey,
|
|
102
|
+
pushIntervalMs: config.pushIntervalMs,
|
|
103
|
+
pullIntervalMs: config.pullIntervalMs,
|
|
104
|
+
maxRetries: config.maxRetries,
|
|
105
|
+
retry: config.retry,
|
|
106
|
+
logger: config.logger,
|
|
107
|
+
logLevel: config.logLevel,
|
|
108
|
+
autoSchema: this.autoSchema,
|
|
109
|
+
schemaManager: this.schemaManager,
|
|
110
|
+
};
|
|
111
|
+
this.syncWorker = new sync_worker_js_1.SyncWorker(this.db, this.remote, this.events, workerOptions);
|
|
112
|
+
}
|
|
113
|
+
getPrimaryKey() {
|
|
114
|
+
return this.primaryKey;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Initializes the client: creates sync_outbox schema, ensures autoSchema tables if enabled,
|
|
118
|
+
* and starts the background sync worker.
|
|
119
|
+
*/
|
|
120
|
+
async initialize() {
|
|
121
|
+
await outbox_store_js_1.OutboxStore.ensureSchema(this.db);
|
|
122
|
+
await meta_store_js_1.MetaStore.ensureSchema(this.db);
|
|
123
|
+
await outbox_store_js_1.OutboxStore.resetStaleProcessing(this.db);
|
|
124
|
+
if (this.autoSchema && this.tables.length > 0) {
|
|
125
|
+
for (const table of this.tables) {
|
|
126
|
+
await this.schemaManager.ensureTable(this.db, table, this.primaryKey);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
this.syncWorker.start();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Performs an atomic dual-write: mutates the local data table AND writes
|
|
133
|
+
* to sync_outbox within the same database transaction.
|
|
134
|
+
*/
|
|
135
|
+
async mutate(tableName, rowId, operation, payload, options = {}) {
|
|
136
|
+
const pk = options.primaryKey ?? this.primaryKey;
|
|
137
|
+
const outboxId = (0, uuid_js_1.generateUUID)();
|
|
138
|
+
const timestamp = options.customTimestamp ?? Date.now();
|
|
139
|
+
const executeInTransaction = async (tx) => {
|
|
140
|
+
// 1. Mutate local table
|
|
141
|
+
await table_manager_js_1.TableManager.applyMutation(tx, tableName, rowId, operation, payload, pk, {
|
|
142
|
+
autoSchema: this.autoSchema,
|
|
143
|
+
schemaManager: this.schemaManager,
|
|
144
|
+
});
|
|
145
|
+
// 2. Insert into sync_outbox (if not skipped)
|
|
146
|
+
if (!options.skipOutbox) {
|
|
147
|
+
await outbox_store_js_1.OutboxStore.insertRecord(tx, {
|
|
148
|
+
id: outboxId,
|
|
149
|
+
tableName,
|
|
150
|
+
rowId,
|
|
151
|
+
operation,
|
|
152
|
+
payload,
|
|
153
|
+
timestamp,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
if (options.tx) {
|
|
158
|
+
await executeInTransaction(options.tx);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
await this.db.transaction(executeInTransaction);
|
|
162
|
+
}
|
|
163
|
+
// 3. Emit reactive change events
|
|
164
|
+
this.events.emit(tableName, {
|
|
165
|
+
operation,
|
|
166
|
+
rowId,
|
|
167
|
+
payload,
|
|
168
|
+
source: 'local',
|
|
169
|
+
});
|
|
170
|
+
if (!options.skipOutbox) {
|
|
171
|
+
this.events.emit('sync_outbox', { operation: 'INSERT', rowId: outboxId });
|
|
172
|
+
// 4. Trigger immediate debounced push
|
|
173
|
+
this.syncWorker.schedulePush(20);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Executes multiple mutations in a single atomic database transaction.
|
|
178
|
+
* Inserts all corresponding outbox entries together and schedules a single push.
|
|
179
|
+
*/
|
|
180
|
+
async batch(mutations, options = {}) {
|
|
181
|
+
if (!mutations || mutations.length === 0)
|
|
182
|
+
return [];
|
|
183
|
+
const generatedIds = [];
|
|
184
|
+
const queuedEvents = [];
|
|
185
|
+
const queuedOutboxIds = [];
|
|
186
|
+
const executeInTx = async (tx) => {
|
|
187
|
+
for (const m of mutations) {
|
|
188
|
+
const pk = m.primaryKey ?? options.primaryKey ?? this.primaryKey;
|
|
189
|
+
const payload = m.payload ?? null;
|
|
190
|
+
const rowId = m.rowId ||
|
|
191
|
+
(payload && payload[pk] !== undefined && payload[pk] !== null
|
|
192
|
+
? String(payload[pk])
|
|
193
|
+
: (0, uuid_js_1.generateUUID)());
|
|
194
|
+
generatedIds.push(rowId);
|
|
195
|
+
await table_manager_js_1.TableManager.applyMutation(tx, m.tableName, rowId, m.operation, payload, pk, {
|
|
196
|
+
autoSchema: this.autoSchema,
|
|
197
|
+
schemaManager: this.schemaManager,
|
|
198
|
+
});
|
|
199
|
+
const skipOutbox = m.skipOutbox ?? options.skipOutbox ?? false;
|
|
200
|
+
if (!skipOutbox) {
|
|
201
|
+
const outboxId = (0, uuid_js_1.generateUUID)();
|
|
202
|
+
const timestamp = m.customTimestamp ?? options.customTimestamp ?? Date.now();
|
|
203
|
+
await outbox_store_js_1.OutboxStore.insertRecord(tx, {
|
|
204
|
+
id: outboxId,
|
|
205
|
+
tableName: m.tableName,
|
|
206
|
+
rowId,
|
|
207
|
+
operation: m.operation,
|
|
208
|
+
payload,
|
|
209
|
+
timestamp,
|
|
210
|
+
});
|
|
211
|
+
queuedOutboxIds.push(outboxId);
|
|
212
|
+
}
|
|
213
|
+
queuedEvents.push({
|
|
214
|
+
tableName: m.tableName,
|
|
215
|
+
event: {
|
|
216
|
+
operation: m.operation,
|
|
217
|
+
rowId,
|
|
218
|
+
payload,
|
|
219
|
+
source: 'local',
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
if (options.tx) {
|
|
225
|
+
await executeInTx(options.tx);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
await this.db.transaction(executeInTx);
|
|
229
|
+
}
|
|
230
|
+
// Emit all buffered events after transaction commits
|
|
231
|
+
for (const item of queuedEvents) {
|
|
232
|
+
this.events.emit(item.tableName, item.event);
|
|
233
|
+
}
|
|
234
|
+
if (queuedOutboxIds.length > 0) {
|
|
235
|
+
this.events.emit('sync_outbox', { operation: 'INSERT', rowId: 'batch' });
|
|
236
|
+
this.syncWorker.schedulePush(20);
|
|
237
|
+
}
|
|
238
|
+
return generatedIds;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Convenience helper to insert an array of records into a table atomically.
|
|
242
|
+
*/
|
|
243
|
+
async insertMany(tableName, records, options = {}) {
|
|
244
|
+
const pk = options.primaryKey ?? this.primaryKey;
|
|
245
|
+
const mutations = records.map((record) => {
|
|
246
|
+
const rowId = record[pk] || (0, uuid_js_1.generateUUID)();
|
|
247
|
+
return {
|
|
248
|
+
tableName,
|
|
249
|
+
rowId,
|
|
250
|
+
operation: 'INSERT',
|
|
251
|
+
payload: record,
|
|
252
|
+
primaryKey: pk,
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
return await this.batch(mutations, options);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Runs an arbitrary callback inside an atomic transaction with a scoped TransactionalClient.
|
|
259
|
+
* If the callback resolves, all changes and outbox records commit and events are emitted.
|
|
260
|
+
* If the callback throws, SQLite rolls back and no change events are emitted.
|
|
261
|
+
*/
|
|
262
|
+
async transaction(action) {
|
|
263
|
+
const queuedEvents = [];
|
|
264
|
+
const queuedOutboxIds = [];
|
|
265
|
+
const result = await this.db.transaction(async (tx) => {
|
|
266
|
+
const txClient = new TransactionalClient(this, tx, queuedEvents, queuedOutboxIds);
|
|
267
|
+
return await action(txClient);
|
|
268
|
+
});
|
|
269
|
+
for (const item of queuedEvents) {
|
|
270
|
+
this.events.emit(item.tableName, item.event);
|
|
271
|
+
}
|
|
272
|
+
if (queuedOutboxIds.length > 0) {
|
|
273
|
+
this.events.emit('sync_outbox', { operation: 'INSERT', rowId: 'batch' });
|
|
274
|
+
this.syncWorker.schedulePush(20);
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Convenience helper to insert a record into a table.
|
|
280
|
+
* Auto-generates a UUID if rowId is omitted or passed inside payload.
|
|
281
|
+
*
|
|
282
|
+
* Example:
|
|
283
|
+
* await client.insert('todos', { title: 'Buy milk' });
|
|
284
|
+
* await client.insert('todos', 'custom-id', { title: 'Buy milk' });
|
|
285
|
+
*/
|
|
286
|
+
async insert(tableName, rowIdOrPayload, payloadOrOptions, options) {
|
|
287
|
+
let rowId;
|
|
288
|
+
let payload;
|
|
289
|
+
let opts = {};
|
|
290
|
+
if (typeof rowIdOrPayload === 'string') {
|
|
291
|
+
rowId = rowIdOrPayload;
|
|
292
|
+
payload = payloadOrOptions || {};
|
|
293
|
+
opts = options || {};
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
payload = rowIdOrPayload || {};
|
|
297
|
+
rowId = payload[this.primaryKey] || (0, uuid_js_1.generateUUID)();
|
|
298
|
+
opts = payloadOrOptions || {};
|
|
299
|
+
}
|
|
300
|
+
await this.mutate(tableName, rowId, 'INSERT', payload, opts);
|
|
301
|
+
return rowId;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Convenience helper to update an existing record by ID.
|
|
305
|
+
*
|
|
306
|
+
* Example:
|
|
307
|
+
* await client.update('todos', 'todo-1', { completed: 1 });
|
|
308
|
+
*/
|
|
309
|
+
async update(tableName, rowId, payload, options = {}) {
|
|
310
|
+
await this.mutate(tableName, rowId, 'UPDATE', payload, options);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Convenience helper to delete a record by ID.
|
|
314
|
+
*
|
|
315
|
+
* Example:
|
|
316
|
+
* await client.delete('todos', 'todo-1');
|
|
317
|
+
*/
|
|
318
|
+
async delete(tableName, rowId, options = {}) {
|
|
319
|
+
await this.mutate(tableName, rowId, 'DELETE', null, options);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Reads records from a local SQLite table without requiring manual SQL.
|
|
323
|
+
* Supports where filtering with rich operators ($in, $gt, $contains, $or, etc.), order, limit, and offset.
|
|
324
|
+
* Gracefully returns [] if table or columns have not been populated yet on empty schemas.
|
|
325
|
+
*
|
|
326
|
+
* Example:
|
|
327
|
+
* const tenants = await client.findMany('tenants', { where: { status: 'ACTIVE' }, orderBy: 'name' });
|
|
328
|
+
* const expensive = await client.findMany('properties', { where: { rent: { $gte: 2000 } } });
|
|
329
|
+
*/
|
|
330
|
+
async findMany(tableName, options) {
|
|
331
|
+
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
332
|
+
let rows;
|
|
333
|
+
try {
|
|
334
|
+
rows = await this.db.execute(`SELECT * FROM ${safeTable}`);
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
const msg = String(err?.message || err || '');
|
|
338
|
+
if (msg.includes('no such table') || msg.includes('no such column')) {
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
341
|
+
throw err;
|
|
342
|
+
}
|
|
343
|
+
return (0, query_js_1.applyQueryOptions)(rows, options);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Finds a single record by primary key.
|
|
347
|
+
*
|
|
348
|
+
* Example:
|
|
349
|
+
* const tenant = await client.findById('tenants', 'ten-1');
|
|
350
|
+
*/
|
|
351
|
+
async findById(tableName, id) {
|
|
352
|
+
const results = await this.findMany(tableName, {
|
|
353
|
+
where: { [this.primaryKey]: id },
|
|
354
|
+
limit: 1,
|
|
355
|
+
});
|
|
356
|
+
return results.length > 0 ? results[0] : null;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Tracks an existing mutation into sync_outbox without modifying the table.
|
|
360
|
+
* Can accept an external transaction `options.tx` for custom atomic SQL workflows.
|
|
361
|
+
*/
|
|
362
|
+
async trackMutation(tableName, rowId, operation, payload, options = {}) {
|
|
363
|
+
const outboxId = (0, uuid_js_1.generateUUID)();
|
|
364
|
+
const timestamp = options.customTimestamp ?? Date.now();
|
|
365
|
+
const executeInsert = async (tx) => {
|
|
366
|
+
await outbox_store_js_1.OutboxStore.insertRecord(tx, {
|
|
367
|
+
id: outboxId,
|
|
368
|
+
tableName,
|
|
369
|
+
rowId,
|
|
370
|
+
operation,
|
|
371
|
+
payload,
|
|
372
|
+
timestamp,
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
if (options.tx) {
|
|
376
|
+
await executeInsert(options.tx);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
await this.db.transaction(executeInsert);
|
|
380
|
+
}
|
|
381
|
+
this.events.emit(tableName, {
|
|
382
|
+
operation,
|
|
383
|
+
rowId,
|
|
384
|
+
payload,
|
|
385
|
+
source: 'local',
|
|
386
|
+
});
|
|
387
|
+
this.events.emit('sync_outbox', { operation: 'INSERT', rowId: outboxId });
|
|
388
|
+
this.syncWorker.schedulePush(20);
|
|
389
|
+
return outboxId;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Returns real-time telemetry metrics captured by the sync engine.
|
|
393
|
+
*/
|
|
394
|
+
async getMetrics() {
|
|
395
|
+
return await this.syncWorker.getMetrics();
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Fetches outbox records with optional status filtering and pagination.
|
|
399
|
+
*/
|
|
400
|
+
async getOutboxRecords(options) {
|
|
401
|
+
return await outbox_store_js_1.OutboxStore.getAllRecords(this.db, options);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Re-queues failed outbox mutations for retry by resetting their status to PENDING.
|
|
405
|
+
* Triggers an immediate sync push.
|
|
406
|
+
*/
|
|
407
|
+
async retryFailedMutations(ids) {
|
|
408
|
+
const count = await outbox_store_js_1.OutboxStore.retryFailed(this.db, ids);
|
|
409
|
+
if (count > 0) {
|
|
410
|
+
this.events.emit('sync_outbox', { operation: 'UPDATE', rowId: 'retry' });
|
|
411
|
+
this.syncWorker.schedulePush(0);
|
|
412
|
+
}
|
|
413
|
+
return count;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Permanently clears failed mutations (removes poison-pill outbox records).
|
|
417
|
+
*/
|
|
418
|
+
async clearFailedMutations(ids) {
|
|
419
|
+
const count = await outbox_store_js_1.OutboxStore.clearFailed(this.db, ids);
|
|
420
|
+
if (count > 0) {
|
|
421
|
+
this.events.emit('sync_outbox', { operation: 'DELETE', rowId: 'clear' });
|
|
422
|
+
}
|
|
423
|
+
return count;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Retrieves the persisted last_synced_at timestamp cursor (in ms).
|
|
427
|
+
*/
|
|
428
|
+
async getLastSyncedAt() {
|
|
429
|
+
return await this.syncWorker.getLastPulledTimestamp();
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Resets the persistent sync cursor back to 0, forcing the next pull
|
|
433
|
+
* to query all remote records.
|
|
434
|
+
*/
|
|
435
|
+
async resetSyncCursor() {
|
|
436
|
+
await this.syncWorker.resetSyncCursor();
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Stops all active intervals, background sync loops, and network listeners.
|
|
440
|
+
*/
|
|
441
|
+
stop() {
|
|
442
|
+
this.syncWorker.stop();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
exports.LocalFirstClient = LocalFirstClient;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ConflictResolver, ConflictStrategy } from './types.js';
|
|
2
|
+
export declare const lastWriteWinsResolver: ConflictResolver;
|
|
3
|
+
export declare const serverWinsResolver: ConflictResolver;
|
|
4
|
+
export declare const clientWinsResolver: ConflictResolver;
|
|
5
|
+
export declare function getConflictResolver(strategyOrResolver?: ConflictStrategy | ConflictResolver): ConflictResolver;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clientWinsResolver = exports.serverWinsResolver = exports.lastWriteWinsResolver = void 0;
|
|
4
|
+
exports.getConflictResolver = getConflictResolver;
|
|
5
|
+
const lastWriteWinsResolver = (context) => {
|
|
6
|
+
const remoteTime = context.remoteChange.timestamp ?? 0;
|
|
7
|
+
const localTime = context.localOutboxRecord?.timestamp ?? 0;
|
|
8
|
+
if (remoteTime >= localTime) {
|
|
9
|
+
return 'apply-remote';
|
|
10
|
+
}
|
|
11
|
+
return 'keep-local';
|
|
12
|
+
};
|
|
13
|
+
exports.lastWriteWinsResolver = lastWriteWinsResolver;
|
|
14
|
+
const serverWinsResolver = () => {
|
|
15
|
+
return 'apply-remote';
|
|
16
|
+
};
|
|
17
|
+
exports.serverWinsResolver = serverWinsResolver;
|
|
18
|
+
const clientWinsResolver = (context) => {
|
|
19
|
+
if (context.localOutboxRecord) {
|
|
20
|
+
return 'keep-local';
|
|
21
|
+
}
|
|
22
|
+
return 'apply-remote';
|
|
23
|
+
};
|
|
24
|
+
exports.clientWinsResolver = clientWinsResolver;
|
|
25
|
+
function getConflictResolver(strategyOrResolver) {
|
|
26
|
+
if (typeof strategyOrResolver === 'function') {
|
|
27
|
+
return strategyOrResolver;
|
|
28
|
+
}
|
|
29
|
+
switch (strategyOrResolver) {
|
|
30
|
+
case 'server-wins':
|
|
31
|
+
return exports.serverWinsResolver;
|
|
32
|
+
case 'client-wins':
|
|
33
|
+
return exports.clientWinsResolver;
|
|
34
|
+
case 'last-write-wins':
|
|
35
|
+
default:
|
|
36
|
+
return exports.lastWriteWinsResolver;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { LocalDBAdapter, LocalTransaction } from '../adapters/local/index.js';
|
|
2
|
+
export declare class MetaStore {
|
|
3
|
+
/**
|
|
4
|
+
* Initializes the sync_meta schema if it does not already exist.
|
|
5
|
+
* This table is strictly local to the device and is never synced to the remote server.
|
|
6
|
+
*/
|
|
7
|
+
static ensureSchema(db: LocalDBAdapter): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Gets a metadata string value by key.
|
|
10
|
+
*/
|
|
11
|
+
static get(db: LocalDBAdapter, key: string): Promise<string | null>;
|
|
12
|
+
/**
|
|
13
|
+
* Sets a metadata value by key (atomic upsert).
|
|
14
|
+
*/
|
|
15
|
+
static set(db: LocalDBAdapter | LocalTransaction, key: string, value: string): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Retrieves the persisted last_synced_at cursor timestamp (in ms). Returns 0 if never synced.
|
|
18
|
+
*/
|
|
19
|
+
static getLastSyncedAt(db: LocalDBAdapter): Promise<number>;
|
|
20
|
+
/**
|
|
21
|
+
* Persists the last_synced_at cursor timestamp.
|
|
22
|
+
*/
|
|
23
|
+
static setLastSyncedAt(db: LocalDBAdapter | LocalTransaction, timestamp: number): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Clears the persisted cursor (forces next pull to re-query active remote records).
|
|
26
|
+
*/
|
|
27
|
+
static clearLastSyncedAt(db: LocalDBAdapter | LocalTransaction): Promise<void>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MetaStore = void 0;
|
|
4
|
+
class MetaStore {
|
|
5
|
+
/**
|
|
6
|
+
* Initializes the sync_meta schema if it does not already exist.
|
|
7
|
+
* This table is strictly local to the device and is never synced to the remote server.
|
|
8
|
+
*/
|
|
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
|
|
13
|
+
);`);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Gets a metadata string value by key.
|
|
17
|
+
*/
|
|
18
|
+
static async get(db, key) {
|
|
19
|
+
try {
|
|
20
|
+
const rows = await db.execute(`SELECT value FROM sync_meta WHERE key = ? LIMIT 1`, [key]);
|
|
21
|
+
if (rows && rows.length > 0) {
|
|
22
|
+
return rows[0].value ?? null;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Sets a metadata value by key (atomic upsert).
|
|
32
|
+
*/
|
|
33
|
+
static async set(db, key, value) {
|
|
34
|
+
await db.execute(`INSERT OR REPLACE INTO sync_meta (key, value) VALUES (?, ?)`, [key, String(value)]);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Retrieves the persisted last_synced_at cursor timestamp (in ms). Returns 0 if never synced.
|
|
38
|
+
*/
|
|
39
|
+
static async getLastSyncedAt(db) {
|
|
40
|
+
const val = await this.get(db, 'last_synced_at');
|
|
41
|
+
if (!val)
|
|
42
|
+
return 0;
|
|
43
|
+
const parsed = parseInt(val, 10);
|
|
44
|
+
return isNaN(parsed) ? 0 : parsed;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Persists the last_synced_at cursor timestamp.
|
|
48
|
+
*/
|
|
49
|
+
static async setLastSyncedAt(db, timestamp) {
|
|
50
|
+
await this.set(db, 'last_synced_at', String(timestamp));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Clears the persisted cursor (forces next pull to re-query active remote records).
|
|
54
|
+
*/
|
|
55
|
+
static async clearLastSyncedAt(db) {
|
|
56
|
+
try {
|
|
57
|
+
await db.execute(`DELETE FROM sync_meta WHERE key = 'last_synced_at'`);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Ignored if table doesn't exist yet
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.MetaStore = MetaStore;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { LocalDBAdapter, LocalTransaction } from '../adapters/local/index.js';
|
|
2
|
+
import { OutboxRecord, OutboxStatus, SyncOperation } from './types.js';
|
|
3
|
+
export declare class OutboxStore {
|
|
4
|
+
/**
|
|
5
|
+
* Initializes the sync_outbox schema if it does not already exist.
|
|
6
|
+
*/
|
|
7
|
+
static ensureSchema(db: LocalDBAdapter): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Inserts a new outbox entry within an existing database transaction.
|
|
10
|
+
*/
|
|
11
|
+
static insertRecord(tx: LocalTransaction, op: SyncOperation): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Inserts multiple outbox entries within an existing database transaction.
|
|
14
|
+
*/
|
|
15
|
+
static insertRecords(tx: LocalTransaction, ops: SyncOperation[]): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Fetches pending and eligible failed outbox records for synchronization.
|
|
18
|
+
*/
|
|
19
|
+
static getPending(db: LocalDBAdapter, maxRetries?: number, limit?: number): Promise<OutboxRecord[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Finds the latest pending outbox record for a given table and row.
|
|
22
|
+
*/
|
|
23
|
+
static findPendingByRow(tx: LocalTransaction, tableName: string, rowId: string): Promise<OutboxRecord | undefined>;
|
|
24
|
+
/**
|
|
25
|
+
* Marks a batch of outbox IDs as 'PROCESSING'.
|
|
26
|
+
*/
|
|
27
|
+
static markProcessing(db: LocalDBAdapter, ids: string[]): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Resets any orphaned 'PROCESSING' records back to 'PENDING' on startup.
|
|
30
|
+
* Protects against app kills or crashes that happened mid-push.
|
|
31
|
+
*/
|
|
32
|
+
static resetStaleProcessing(db: LocalDBAdapter): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Deletes a batch of successfully pushed outbox entries.
|
|
35
|
+
*/
|
|
36
|
+
static deleteCompleted(db: LocalDBAdapter, ids: string[]): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Deletes a single outbox record within an active transaction.
|
|
39
|
+
*/
|
|
40
|
+
static deleteRecord(tx: LocalTransaction, id: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Updates an outbox record's payload (used during conflict merge).
|
|
43
|
+
*/
|
|
44
|
+
static updatePayload(tx: LocalTransaction, id: string, payload: Record<string, any>): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Marks a batch of outbox IDs as 'FAILED' and increments retry counters.
|
|
47
|
+
*/
|
|
48
|
+
static markFailed(db: LocalDBAdapter, ids: string[], errorMessage: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Retrieves accurate counts of outbox records categorized by status.
|
|
51
|
+
*/
|
|
52
|
+
static getCounts(db: LocalDBAdapter): Promise<{
|
|
53
|
+
pending: number;
|
|
54
|
+
failed: number;
|
|
55
|
+
processing: number;
|
|
56
|
+
total: number;
|
|
57
|
+
}>;
|
|
58
|
+
/**
|
|
59
|
+
* Re-queues failed outbox records for retry by resetting their status to PENDING and resetting retry_count.
|
|
60
|
+
* If ids are specified, only those records are reset. Otherwise, all FAILED records are reset.
|
|
61
|
+
* Returns the count of affected records.
|
|
62
|
+
*/
|
|
63
|
+
static retryFailed(db: LocalDBAdapter, ids?: string[]): Promise<number>;
|
|
64
|
+
/**
|
|
65
|
+
* Permanently clears failed outbox records (removes poison-pill mutations).
|
|
66
|
+
*/
|
|
67
|
+
static clearFailed(db: LocalDBAdapter, ids?: string[]): Promise<number>;
|
|
68
|
+
/**
|
|
69
|
+
* Retrieves all outbox records, optionally filtered by status, ordered by timestamp.
|
|
70
|
+
*/
|
|
71
|
+
static getAllRecords(db: LocalDBAdapter, options?: {
|
|
72
|
+
status?: OutboxStatus;
|
|
73
|
+
limit?: number;
|
|
74
|
+
offset?: number;
|
|
75
|
+
}): Promise<OutboxRecord[]>;
|
|
76
|
+
}
|