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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LocalDBAdapter, LocalTransaction } from '../adapters/local/index.js';
|
|
2
|
-
import { OutboxRecord, OutboxStatus, SyncOperation } from './types.js';
|
|
2
|
+
import { OutboxRecord, OutboxStatus, SyncOp, SyncOperation } from './types.js';
|
|
3
3
|
export declare class OutboxStore {
|
|
4
4
|
/**
|
|
5
5
|
* Initializes the sync_outbox schema if it does not already exist.
|
|
@@ -10,7 +10,7 @@ export declare class OutboxStore {
|
|
|
10
10
|
*/
|
|
11
11
|
static insertRecord(tx: LocalTransaction, op: SyncOperation): Promise<void>;
|
|
12
12
|
/**
|
|
13
|
-
* Inserts multiple outbox entries within an existing
|
|
13
|
+
* Inserts multiple outbox entries in high-throughput multi-row SQL chunks within an existing transaction.
|
|
14
14
|
*/
|
|
15
15
|
static insertRecords(tx: LocalTransaction, ops: SyncOperation[]): Promise<void>;
|
|
16
16
|
/**
|
|
@@ -39,9 +39,13 @@ export declare class OutboxStore {
|
|
|
39
39
|
*/
|
|
40
40
|
static deleteRecord(tx: LocalTransaction, id: string): Promise<void>;
|
|
41
41
|
/**
|
|
42
|
-
* Updates an outbox record's payload (used during conflict merge).
|
|
42
|
+
* Updates an outbox record's payload (used during conflict merge or coalescing).
|
|
43
43
|
*/
|
|
44
|
-
static updatePayload(tx: LocalTransaction, id: string, payload: Record<string, any
|
|
44
|
+
static updatePayload(tx: LocalTransaction, id: string, payload: Record<string, any>, timestamp?: number): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Updates an outbox record's operation and payload (used during mutation coalescing).
|
|
47
|
+
*/
|
|
48
|
+
static updateOperation(tx: LocalTransaction, id: string, operation: SyncOp, payload: Record<string, any> | null, timestamp?: number): Promise<void>;
|
|
45
49
|
/**
|
|
46
50
|
* Marks a batch of outbox IDs as 'FAILED' and increments retry counters.
|
|
47
51
|
*/
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OutboxStore = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Maps a raw sync_outbox row into an OutboxRecord.
|
|
6
|
+
*/
|
|
7
|
+
function mapOutboxRow(r) {
|
|
8
|
+
return {
|
|
9
|
+
id: r.id,
|
|
10
|
+
tableName: r.table_name,
|
|
11
|
+
rowId: r.row_id,
|
|
12
|
+
operation: r.operation,
|
|
13
|
+
payload: r.payload ? (typeof r.payload === 'string' ? JSON.parse(r.payload) : r.payload) : null,
|
|
14
|
+
timestamp: Number(r.timestamp),
|
|
15
|
+
status: r.status,
|
|
16
|
+
retryCount: Number(r.retry_count || 0),
|
|
17
|
+
lastError: r.last_error,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
4
20
|
class OutboxStore {
|
|
5
21
|
/**
|
|
6
22
|
* Initializes the sync_outbox schema if it does not already exist.
|
|
@@ -17,6 +33,10 @@ class OutboxStore {
|
|
|
17
33
|
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
18
34
|
last_error TEXT
|
|
19
35
|
);`);
|
|
36
|
+
// Indexes for the hot paths: pending scans (push), per-row conflict lookups (pull),
|
|
37
|
+
// and status counts (telemetry). Executed separately: many drivers prepare one statement.
|
|
38
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS idx_sync_outbox_status_ts ON sync_outbox (status, timestamp);`);
|
|
39
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS idx_sync_outbox_row ON sync_outbox (table_name, row_id, timestamp);`);
|
|
20
40
|
}
|
|
21
41
|
/**
|
|
22
42
|
* Inserts a new outbox entry within an existing database transaction.
|
|
@@ -33,11 +53,29 @@ class OutboxStore {
|
|
|
33
53
|
]);
|
|
34
54
|
}
|
|
35
55
|
/**
|
|
36
|
-
* Inserts multiple outbox entries within an existing
|
|
56
|
+
* Inserts multiple outbox entries in high-throughput multi-row SQL chunks within an existing transaction.
|
|
37
57
|
*/
|
|
38
58
|
static async insertRecords(tx, ops) {
|
|
39
|
-
|
|
40
|
-
|
|
59
|
+
if (!ops || ops.length === 0)
|
|
60
|
+
return;
|
|
61
|
+
const CHUNK_SIZE = 50;
|
|
62
|
+
for (let i = 0; i < ops.length; i += CHUNK_SIZE) {
|
|
63
|
+
const chunk = ops.slice(i, i + CHUNK_SIZE);
|
|
64
|
+
const valueClauses = [];
|
|
65
|
+
const params = [];
|
|
66
|
+
for (const op of chunk) {
|
|
67
|
+
valueClauses.push('(?, ?, ?, ?, ?, ?, "PENDING", 0)');
|
|
68
|
+
params.push(op.id, op.tableName, op.rowId, op.operation, op.payload ? JSON.stringify(op.payload) : null, op.timestamp);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await tx.execute(`INSERT INTO sync_outbox (id, table_name, row_id, operation, payload, timestamp, status, retry_count) VALUES ${valueClauses.join(', ')}`, params);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Transparent fallback to per-record insert for mock drivers or custom engines
|
|
75
|
+
for (const op of chunk) {
|
|
76
|
+
await this.insertRecord(tx, op);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
41
79
|
}
|
|
42
80
|
}
|
|
43
81
|
/**
|
|
@@ -56,17 +94,7 @@ class OutboxStore {
|
|
|
56
94
|
const rows = await db.execute(query, params);
|
|
57
95
|
if (!rows || rows.length === 0)
|
|
58
96
|
return [];
|
|
59
|
-
return rows.map(
|
|
60
|
-
id: r.id,
|
|
61
|
-
tableName: r.table_name,
|
|
62
|
-
rowId: r.row_id,
|
|
63
|
-
operation: r.operation,
|
|
64
|
-
payload: r.payload ? (typeof r.payload === 'string' ? JSON.parse(r.payload) : r.payload) : null,
|
|
65
|
-
timestamp: Number(r.timestamp),
|
|
66
|
-
status: r.status,
|
|
67
|
-
retryCount: Number(r.retry_count || 0),
|
|
68
|
-
lastError: r.last_error,
|
|
69
|
-
}));
|
|
97
|
+
return rows.map(mapOutboxRow);
|
|
70
98
|
}
|
|
71
99
|
/**
|
|
72
100
|
* Finds the latest pending outbox record for a given table and row.
|
|
@@ -77,18 +105,7 @@ class OutboxStore {
|
|
|
77
105
|
ORDER BY timestamp DESC LIMIT 1`, [tableName, rowId]);
|
|
78
106
|
if (!rows || rows.length === 0)
|
|
79
107
|
return undefined;
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
id: r.id,
|
|
83
|
-
tableName: r.table_name,
|
|
84
|
-
rowId: r.row_id,
|
|
85
|
-
operation: r.operation,
|
|
86
|
-
payload: r.payload ? (typeof r.payload === 'string' ? JSON.parse(r.payload) : r.payload) : null,
|
|
87
|
-
timestamp: Number(r.timestamp),
|
|
88
|
-
status: r.status,
|
|
89
|
-
retryCount: Number(r.retry_count || 0),
|
|
90
|
-
lastError: r.last_error,
|
|
91
|
-
};
|
|
108
|
+
return mapOutboxRow(rows[0]);
|
|
92
109
|
}
|
|
93
110
|
/**
|
|
94
111
|
* Marks a batch of outbox IDs as 'PROCESSING'.
|
|
@@ -127,10 +144,22 @@ class OutboxStore {
|
|
|
127
144
|
await tx.execute(`DELETE FROM sync_outbox WHERE id = ?`, [id]);
|
|
128
145
|
}
|
|
129
146
|
/**
|
|
130
|
-
* Updates an outbox record's payload (used during conflict merge).
|
|
147
|
+
* Updates an outbox record's payload (used during conflict merge or coalescing).
|
|
131
148
|
*/
|
|
132
|
-
static async updatePayload(tx, id, payload) {
|
|
133
|
-
|
|
149
|
+
static async updatePayload(tx, id, payload, timestamp) {
|
|
150
|
+
if (timestamp) {
|
|
151
|
+
await tx.execute(`UPDATE sync_outbox SET payload = ?, timestamp = ? WHERE id = ?`, [JSON.stringify(payload), timestamp, id]);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
await tx.execute(`UPDATE sync_outbox SET payload = ? WHERE id = ?`, [JSON.stringify(payload), id]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Updates an outbox record's operation and payload (used during mutation coalescing).
|
|
159
|
+
*/
|
|
160
|
+
static async updateOperation(tx, id, operation, payload, timestamp) {
|
|
161
|
+
const ts = timestamp ?? Date.now();
|
|
162
|
+
await tx.execute(`UPDATE sync_outbox SET operation = ?, payload = ?, timestamp = ? WHERE id = ?`, [operation, payload ? JSON.stringify(payload) : null, ts, id]);
|
|
134
163
|
}
|
|
135
164
|
/**
|
|
136
165
|
* Marks a batch of outbox IDs as 'FAILED' and increments retry counters.
|
|
@@ -150,27 +179,23 @@ class OutboxStore {
|
|
|
150
179
|
*/
|
|
151
180
|
static async getCounts(db) {
|
|
152
181
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return { pending: 0, failed: 0, processing: 0, total: 0 };
|
|
156
|
-
}
|
|
182
|
+
// Aggregate in SQLite (index-only scan) instead of loading every row and payload into JS.
|
|
183
|
+
const rows = await db.execute(`SELECT status, COUNT(*) AS count FROM sync_outbox GROUP BY status`);
|
|
157
184
|
let pending = 0;
|
|
158
185
|
let failed = 0;
|
|
159
186
|
let processing = 0;
|
|
160
|
-
|
|
187
|
+
let total = 0;
|
|
188
|
+
for (const row of rows || []) {
|
|
189
|
+
const count = Number(row.count ?? 0);
|
|
190
|
+
total += count;
|
|
161
191
|
if (row.status === 'PENDING')
|
|
162
|
-
pending
|
|
192
|
+
pending = count;
|
|
163
193
|
else if (row.status === 'FAILED')
|
|
164
|
-
failed
|
|
194
|
+
failed = count;
|
|
165
195
|
else if (row.status === 'PROCESSING')
|
|
166
|
-
processing
|
|
196
|
+
processing = count;
|
|
167
197
|
}
|
|
168
|
-
return {
|
|
169
|
-
pending,
|
|
170
|
-
failed,
|
|
171
|
-
processing,
|
|
172
|
-
total: rows.length,
|
|
173
|
-
};
|
|
198
|
+
return { pending, failed, processing, total };
|
|
174
199
|
}
|
|
175
200
|
catch {
|
|
176
201
|
return { pending: 0, failed: 0, processing: 0, total: 0 };
|
|
@@ -231,31 +256,25 @@ class OutboxStore {
|
|
|
231
256
|
*/
|
|
232
257
|
static async getAllRecords(db, options) {
|
|
233
258
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
let mapped = rows.map((r) => ({
|
|
238
|
-
id: r.id,
|
|
239
|
-
tableName: r.table_name,
|
|
240
|
-
rowId: r.row_id,
|
|
241
|
-
operation: r.operation,
|
|
242
|
-
payload: r.payload ? (typeof r.payload === 'string' ? JSON.parse(r.payload) : r.payload) : null,
|
|
243
|
-
timestamp: Number(r.timestamp),
|
|
244
|
-
status: r.status,
|
|
245
|
-
retryCount: Number(r.retry_count || 0),
|
|
246
|
-
lastError: r.last_error,
|
|
247
|
-
}));
|
|
259
|
+
// Filter, order and paginate in SQLite so only the requested page is parsed.
|
|
260
|
+
const params = [];
|
|
261
|
+
let query = `SELECT * FROM sync_outbox`;
|
|
248
262
|
if (options?.status) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
mapped.sort((a, b) => b.timestamp - a.timestamp);
|
|
252
|
-
if (options?.offset && options.offset > 0) {
|
|
253
|
-
mapped = mapped.slice(options.offset);
|
|
263
|
+
query += ` WHERE status = ?`;
|
|
264
|
+
params.push(options.status);
|
|
254
265
|
}
|
|
255
|
-
|
|
256
|
-
|
|
266
|
+
query += ` ORDER BY timestamp DESC`;
|
|
267
|
+
const limit = options?.limit && options.limit > 0 ? options.limit : -1;
|
|
268
|
+
const offset = options?.offset && options.offset > 0 ? options.offset : 0;
|
|
269
|
+
if (limit > 0 || offset > 0) {
|
|
270
|
+
// SQLite requires LIMIT when OFFSET is used; -1 means "no limit".
|
|
271
|
+
query += ` LIMIT ? OFFSET ?`;
|
|
272
|
+
params.push(limit, offset);
|
|
257
273
|
}
|
|
258
|
-
|
|
274
|
+
const rows = await db.execute(query, params);
|
|
275
|
+
if (!rows || rows.length === 0)
|
|
276
|
+
return [];
|
|
277
|
+
return rows.map(mapOutboxRow);
|
|
259
278
|
}
|
|
260
279
|
catch {
|
|
261
280
|
return [];
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { LocalFirstClient, TransactionalClient } from './client.js';
|
|
2
|
+
import { QueryOptions, WhereFilter } from './types.js';
|
|
3
|
+
type ClientLike = LocalFirstClient | TransactionalClient;
|
|
4
|
+
/**
|
|
5
|
+
* Escapes an identifier or compound identifier (e.g. 'table.column' or 'table.col as alias').
|
|
6
|
+
*/
|
|
7
|
+
export declare function formatColumnIdentifier(col: string): string;
|
|
8
|
+
export interface JoinDefinition {
|
|
9
|
+
type: 'INNER' | 'LEFT' | 'CROSS';
|
|
10
|
+
table: string;
|
|
11
|
+
firstCol: string;
|
|
12
|
+
secondCol: string;
|
|
13
|
+
}
|
|
14
|
+
export interface AggregateSpec {
|
|
15
|
+
where?: WhereFilter;
|
|
16
|
+
count?: boolean;
|
|
17
|
+
sum?: string[];
|
|
18
|
+
avg?: string[];
|
|
19
|
+
min?: string[];
|
|
20
|
+
max?: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface AggregateResult {
|
|
23
|
+
count?: number;
|
|
24
|
+
sum?: Record<string, number>;
|
|
25
|
+
avg?: Record<string, number | null>;
|
|
26
|
+
min?: Record<string, any>;
|
|
27
|
+
max?: Record<string, any>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Fluent, type-safe SQL Query Builder for local-first SQLite operations.
|
|
31
|
+
* Allows writing expressive JavaScript code that compiles and executes high-performance SQL under the hood.
|
|
32
|
+
*/
|
|
33
|
+
export declare class QueryBuilder<T = any> {
|
|
34
|
+
private client;
|
|
35
|
+
private tableName;
|
|
36
|
+
private selectCols;
|
|
37
|
+
private joins;
|
|
38
|
+
private whereConditions;
|
|
39
|
+
private orderColumn?;
|
|
40
|
+
private orderDirection;
|
|
41
|
+
private limitCount?;
|
|
42
|
+
private offsetCount?;
|
|
43
|
+
private groupByCols;
|
|
44
|
+
private includeDeleted;
|
|
45
|
+
constructor(client: ClientLike, tableName: string);
|
|
46
|
+
/**
|
|
47
|
+
* Specifies which columns to select.
|
|
48
|
+
* Supports column aliases (e.g. ['id', 'contracts.rent_amount AS rent', 'houses.house_number']).
|
|
49
|
+
*/
|
|
50
|
+
select(...columns: Array<string | string[]>): this;
|
|
51
|
+
/**
|
|
52
|
+
* Inner joins another SQLite table on a matching column expression.
|
|
53
|
+
*/
|
|
54
|
+
join(table: string, firstCol: string, secondCol: string): this;
|
|
55
|
+
/**
|
|
56
|
+
* Left joins another SQLite table on a matching column expression.
|
|
57
|
+
*/
|
|
58
|
+
leftJoin(table: string, firstCol: string, secondCol: string): this;
|
|
59
|
+
/**
|
|
60
|
+
* Adds WHERE criteria to filter results.
|
|
61
|
+
* Supports:
|
|
62
|
+
* - where({ status: 'ACTIVE' })
|
|
63
|
+
* - where('status', 'ACTIVE')
|
|
64
|
+
* - where('end_date', '>=', today)
|
|
65
|
+
*/
|
|
66
|
+
where(columnOrFilter: string | WhereFilter, operatorOrValue?: any, value?: any): this;
|
|
67
|
+
/**
|
|
68
|
+
* Orders query results by column and direction.
|
|
69
|
+
*/
|
|
70
|
+
orderBy(column: string, direction?: 'asc' | 'desc' | 'ASC' | 'DESC'): this;
|
|
71
|
+
/**
|
|
72
|
+
* Groups rows by one or more columns for aggregation.
|
|
73
|
+
*/
|
|
74
|
+
groupBy(...columns: Array<string | string[]>): this;
|
|
75
|
+
/**
|
|
76
|
+
* Limits the number of returned records.
|
|
77
|
+
*/
|
|
78
|
+
limit(count: number): this;
|
|
79
|
+
/**
|
|
80
|
+
* Offsets the starting record position.
|
|
81
|
+
*/
|
|
82
|
+
offset(count: number): this;
|
|
83
|
+
/**
|
|
84
|
+
* Includes soft-deleted rows in results.
|
|
85
|
+
*/
|
|
86
|
+
withDeleted(include?: boolean): this;
|
|
87
|
+
/**
|
|
88
|
+
* Applies standard QueryOptions (select, where, orderBy, limit, offset, includeDeleted) to this query.
|
|
89
|
+
*/
|
|
90
|
+
applyOptions(options?: QueryOptions): this;
|
|
91
|
+
/**
|
|
92
|
+
* Compiles the current builder state into an executable SQL string and parameter list.
|
|
93
|
+
*/
|
|
94
|
+
toSQL(): {
|
|
95
|
+
sql: string;
|
|
96
|
+
params: any[];
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Executes the compiled SQL query against SQLite and returns rows.
|
|
100
|
+
*/
|
|
101
|
+
execute(): Promise<T[]>;
|
|
102
|
+
/**
|
|
103
|
+
* Returns the first matching record or null if not found.
|
|
104
|
+
*/
|
|
105
|
+
first(): Promise<T | null>;
|
|
106
|
+
/**
|
|
107
|
+
* Executes a COUNT(*) in SQLite and returns the numeric count directly.
|
|
108
|
+
*/
|
|
109
|
+
count(): Promise<number>;
|
|
110
|
+
/**
|
|
111
|
+
* Executes a SUM(column) in SQLite and returns the numeric total directly.
|
|
112
|
+
*/
|
|
113
|
+
sum(column: string): Promise<number>;
|
|
114
|
+
/**
|
|
115
|
+
* Executes an AVG(column) in SQLite and returns the average directly.
|
|
116
|
+
*/
|
|
117
|
+
avg(column: string): Promise<number | null>;
|
|
118
|
+
}
|
|
119
|
+
export {};
|