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.
Files changed (63) hide show
  1. package/README.md +421 -362
  2. package/dist/adapters/local/better-sqlite3.d.ts +2 -0
  3. package/dist/adapters/local/better-sqlite3.js +46 -25
  4. package/dist/adapters/local/expo-sqlite.d.ts +5 -1
  5. package/dist/adapters/local/expo-sqlite.js +40 -7
  6. package/dist/adapters/local/index.d.ts +1 -0
  7. package/dist/adapters/local/node-sqlite.d.ts +2 -0
  8. package/dist/adapters/local/node-sqlite.js +45 -22
  9. package/dist/adapters/local/op-sqlite.d.ts +1 -0
  10. package/dist/adapters/local/op-sqlite.js +6 -0
  11. package/dist/adapters/local/sql-js.d.ts +1 -0
  12. package/dist/adapters/local/sql-js.js +5 -0
  13. package/dist/adapters/network/index.js +33 -0
  14. package/dist/adapters/remote/convex.d.ts +2 -2
  15. package/dist/adapters/remote/convex.js +8 -12
  16. package/dist/adapters/remote/demo.d.ts +1 -1
  17. package/dist/adapters/remote/demo.js +3 -0
  18. package/dist/adapters/remote/firestore.d.ts +3 -2
  19. package/dist/adapters/remote/firestore.js +69 -46
  20. package/dist/adapters/remote/index.d.ts +6 -4
  21. package/dist/adapters/remote/supabase.d.ts +34 -6
  22. package/dist/adapters/remote/supabase.js +340 -79
  23. package/dist/config/index.js +7 -10
  24. package/dist/config/resolve-local.js +12 -1
  25. package/dist/config/resolve-remote.js +33 -28
  26. package/dist/config/types.d.ts +30 -5
  27. package/dist/core/client.d.ts +133 -5
  28. package/dist/core/client.js +431 -46
  29. package/dist/core/index.d.ts +15 -0
  30. package/dist/core/index.js +30 -0
  31. package/dist/core/outbox-store.d.ts +8 -4
  32. package/dist/core/outbox-store.js +84 -65
  33. package/dist/core/query-builder.d.ts +119 -0
  34. package/dist/core/query-builder.js +372 -0
  35. package/dist/core/query.d.ts +8 -0
  36. package/dist/core/query.js +37 -0
  37. package/dist/core/schema-manager.d.ts +9 -0
  38. package/dist/core/schema-manager.js +103 -0
  39. package/dist/core/sync-worker.d.ts +38 -1
  40. package/dist/core/sync-worker.js +351 -104
  41. package/dist/core/table-manager.js +26 -29
  42. package/dist/core/types.d.ts +8 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +2 -0
  45. package/dist/react/BenchmarkModal.d.ts +32 -0
  46. package/dist/react/BenchmarkModal.js +777 -0
  47. package/dist/react/SyncInspector.d.ts +31 -0
  48. package/dist/react/SyncInspector.js +585 -0
  49. package/dist/react/index.d.ts +89 -18
  50. package/dist/react/index.js +450 -105
  51. package/dist/react/runner.d.ts +7 -0
  52. package/dist/react/runner.js +42 -0
  53. package/dist/react/status.d.ts +24 -0
  54. package/dist/react/status.js +123 -0
  55. package/dist/react/theme.d.ts +18 -0
  56. package/dist/react/theme.js +46 -0
  57. package/dist/schema/generator.d.ts +49 -0
  58. package/dist/schema/generator.js +209 -0
  59. package/dist/schema/index.d.ts +1 -0
  60. package/dist/schema/index.js +17 -0
  61. package/dist/utils/logger.d.ts +4 -0
  62. package/dist/utils/logger.js +20 -0
  63. 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 database transaction.
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>): Promise<void>;
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 database transaction.
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
- for (const op of ops) {
40
- await this.insertRecord(tx, op);
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((r) => ({
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
- const r = rows[0];
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
- await tx.execute(`UPDATE sync_outbox SET payload = ? WHERE id = ?`, [JSON.stringify(payload), id]);
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
- const rows = await db.execute(`SELECT * FROM sync_outbox`);
154
- if (!rows || rows.length === 0) {
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
- for (const row of rows) {
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
- const rows = await db.execute(`SELECT * FROM sync_outbox`);
235
- if (!rows || rows.length === 0)
236
- return [];
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
- mapped = mapped.filter((r) => r.status === options.status);
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
- if (options?.limit && options.limit > 0) {
256
- mapped = mapped.slice(0, options.limit);
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
- return mapped;
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 {};