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
@@ -51,8 +51,19 @@ async function resolveLocalAdapter(config) {
51
51
  // 9. Auto-open by name using expo-sqlite if available
52
52
  if (typeof rawDb === 'string' || config.databaseName) {
53
53
  const dbName = typeof rawDb === 'string' ? rawDb : (config.databaseName ?? 'local_lite.db');
54
+ let expoSqlite;
54
55
  try {
55
- const expoSqlite = await import('expo-sqlite');
56
+ if (typeof require === 'function') {
57
+ expoSqlite = require('expo-sqlite');
58
+ }
59
+ }
60
+ catch {
61
+ // ignore
62
+ }
63
+ try {
64
+ if (!expoSqlite) {
65
+ expoSqlite = await import('expo-sqlite');
66
+ }
56
67
  if (expoSqlite && typeof expoSqlite.openDatabaseAsync === 'function') {
57
68
  const expoDb = await expoSqlite.openDatabaseAsync(dbName);
58
69
  return new expo_sqlite_js_1.ExpoSQLiteAdapter(expoDb);
@@ -35,47 +35,52 @@ async function resolveRemoteAdapter(config) {
35
35
  updatedAtColumn: config.convex.updatedAtColumn,
36
36
  });
37
37
  }
38
+ const getSupabaseOptions = () => ({
39
+ tables: config.tables,
40
+ primaryKey: config.primaryKey,
41
+ schema: config.supabase?.schema,
42
+ updatedAtColumn: config.supabase?.updatedAtColumn,
43
+ deletedColumn: config.supabase?.deletedColumn,
44
+ deletedValue: config.supabase?.deletedValue,
45
+ deletedAtColumn: config.supabase?.deletedAtColumn,
46
+ pushRpcName: config.supabase?.pushRpcName,
47
+ channelName: config.supabase?.channelName,
48
+ scopedTables: config.supabase?.scopedTables,
49
+ });
38
50
  // 4. Supabase with pre-configured client
39
51
  if (config.supabase?.client) {
40
- return new supabase_js_1.SupabaseRemoteAdapter(config.supabase.client, {
41
- tables: config.tables,
42
- primaryKey: config.primaryKey,
43
- schema: config.supabase.schema,
44
- updatedAtColumn: config.supabase.updatedAtColumn,
45
- deletedColumn: config.supabase.deletedColumn,
46
- deletedValue: config.supabase.deletedValue,
47
- deletedAtColumn: config.supabase.deletedAtColumn,
48
- pushRpcName: config.supabase.pushRpcName,
49
- channelName: config.supabase.channelName,
50
- });
52
+ return new supabase_js_1.SupabaseRemoteAdapter(config.supabase.client, getSupabaseOptions());
51
53
  }
52
54
  // 5. Supabase with URL + anonKey (lazy dynamic import)
53
55
  if (config.supabase?.url && config.supabase?.anonKey) {
54
56
  let supabaseModule;
57
+ // In CommonJS / React Native Metro environments, synchronous require avoids async chunk split failure
55
58
  try {
56
- // @ts-ignore
57
- supabaseModule = await import('@supabase/supabase-js');
59
+ if (typeof require === 'function') {
60
+ supabaseModule = require('@supabase/supabase-js');
61
+ }
58
62
  }
59
63
  catch {
60
- throw new Error('To initialize Supabase from url and anonKey, please install @supabase/supabase-js or provide a pre-configured client to config.supabase.client');
64
+ // ignore
65
+ }
66
+ if (!supabaseModule && typeof globalThis?.supabase?.createClient === 'function') {
67
+ supabaseModule = globalThis.supabase;
68
+ }
69
+ if (!supabaseModule) {
70
+ try {
71
+ supabaseModule = await import('@supabase/supabase-js');
72
+ }
73
+ catch {
74
+ throw new Error('To initialize Supabase from url and anonKey, please install @supabase/supabase-js or provide a pre-configured client to config.supabase.client');
75
+ }
61
76
  }
62
77
  const createClientFn = supabaseModule.createClient || supabaseModule.default?.createClient;
63
78
  const supabaseClient = createClientFn(config.supabase.url, config.supabase.anonKey);
64
- return new supabase_js_1.SupabaseRemoteAdapter(supabaseClient, {
65
- tables: config.tables,
66
- primaryKey: config.primaryKey,
67
- schema: config.supabase.schema,
68
- updatedAtColumn: config.supabase.updatedAtColumn,
69
- deletedColumn: config.supabase.deletedColumn,
70
- deletedValue: config.supabase.deletedValue,
71
- deletedAtColumn: config.supabase.deletedAtColumn,
72
- pushRpcName: config.supabase.pushRpcName,
73
- channelName: config.supabase.channelName,
74
- });
79
+ return new supabase_js_1.SupabaseRemoteAdapter(supabaseClient, getSupabaseOptions());
75
80
  }
76
- // 6. In-Memory Simulated Remote (Default when no cloud credentials provided)
77
- if (config.inMemoryRemote !== false) {
81
+ // 6. Explicitly selected demo backend only; never acknowledge writes into a temporary backend implicitly.
82
+ if (config.inMemoryRemote === true) {
78
83
  return new demo_js_1.MemoryDemoRemoteAdapter(300);
79
84
  }
80
- throw new Error('No remote adapter configured. Please specify config.supabase, config.convex, config.firebase, or config.remote.');
85
+ throw new Error('No remote adapter configured. Please specify config.supabase, config.convex, config.firebase, or config.remote. For demos only, explicitly set config.inMemoryRemote: true.');
81
86
  }
@@ -13,6 +13,7 @@ export interface SupabaseConfigOptions {
13
13
  deletedAtColumn?: string;
14
14
  pushRpcName?: string;
15
15
  channelName?: string;
16
+ scopedTables?: string[];
16
17
  }
17
18
  export interface ConvexConfigOptions {
18
19
  /**
@@ -74,7 +75,7 @@ export interface LocalLiteConfig {
74
75
  */
75
76
  database?: any | string | (() => Promise<any> | any) | LocalDBAdapter;
76
77
  /**
77
- * Database name if database is not provided. Default: 'local_lite.db'
78
+ * Explicit database filename for the Expo convenience opener when database is not provided.
78
79
  */
79
80
  databaseName?: string;
80
81
  /**
@@ -89,7 +90,7 @@ export interface LocalLiteConfig {
89
90
  migrations?: string[];
90
91
  /**
91
92
  * Automatically create tables and columns dynamically on the fly without manual SQL migrations.
92
- * When true (default if migrations is not provided), tables in `tables` are created automatically,
93
+ * Opt-in (default: false). When true, tables in `tables` are created automatically,
93
94
  * and any missing columns are added dynamically on insert/update/pull.
94
95
  */
95
96
  autoSchema?: boolean;
@@ -114,9 +115,9 @@ export interface LocalLiteConfig {
114
115
  */
115
116
  remote?: RemoteDBAdapter;
116
117
  /**
117
- * Whether to use an in-memory simulated remote backend if Supabase, Convex, or Firebase credentials are not provided.
118
- * Allows running and testing full local-first sync workflows without cloud infrastructure.
119
- * Default: true
118
+ * Explicitly enable a temporary simulated remote for demos when no backend is provided.
119
+ * Acknowledged writes exist only in memory. Never enabled implicitly.
120
+ * Default: false
120
121
  */
121
122
  inMemoryRemote?: boolean;
122
123
  /**
@@ -131,6 +132,12 @@ export interface LocalLiteConfig {
131
132
  * Periodic remote pull interval in ms. Default: 10000
132
133
  */
133
134
  pullIntervalMs?: number;
135
+ /**
136
+ * Automatically pause recurring sync timers and listeners when the app is backgrounded
137
+ * (React Native AppState or Web Page Visibility API), and resume when foregrounded.
138
+ * Default: true
139
+ */
140
+ autoPauseOnBackground?: boolean;
134
141
  /**
135
142
  * Maximum retry attempts for failed push items. Default: 5
136
143
  */
@@ -156,4 +163,22 @@ export interface LocalLiteConfig {
156
163
  * Default: true
157
164
  */
158
165
  autoInitialize?: boolean;
166
+ /**
167
+ * Start automatic sync after initialization. Default: true.
168
+ * Set false to initialize storage and queue writes without timers or subscriptions.
169
+ * Call client.syncWorker.start() to enable automatic sync, or push/pull manually.
170
+ */
171
+ autoStart?: boolean;
172
+ /**
173
+ * Explicitly configured JSON column map per table name.
174
+ */
175
+ jsonColumns?: Record<string, string[]>;
176
+ /**
177
+ * Automatically detect and hydrate stringified JSON objects and arrays across all columns.
178
+ */
179
+ autoHydrateJSON?: boolean;
180
+ /**
181
+ * Scoped synchronization filter (e.g. { org_id: '...' } or dynamic resolver callback).
182
+ */
183
+ syncScope?: Record<string, any> | (() => Record<string, any> | Promise<Record<string, any>>);
159
184
  }
@@ -3,8 +3,9 @@ import { RemoteDBAdapter } from '../adapters/remote/index.js';
3
3
  import { EventEmitter } from '../reactive/event-emitter.js';
4
4
  import { SyncWorker } from './sync-worker.js';
5
5
  import { SchemaManager } from './schema-manager.js';
6
- import { BatchMutation, ConflictResolver, ConflictStrategy, MutateOptions, NetworkStatusProvider, OutboxRecord, OutboxStatus, QueryOptions, RetryConfig, SyncMetrics, SyncOp, TableChangeEvent } from './types.js';
6
+ import { BatchMutation, ConflictResolver, ConflictStrategy, MutateOptions, NetworkStatusProvider, OutboxRecord, OutboxStatus, QueryOptions, RetryConfig, SyncMetrics, SyncOp, TableChangeEvent, WhereFilter } from './types.js';
7
7
  import { SyncLogger, CustomLogger, LogLevel } from '../utils/logger.js';
8
+ import { QueryBuilder, AggregateSpec, AggregateResult } from './query-builder.js';
8
9
  export interface LocalFirstClientConfig {
9
10
  local: LocalDBAdapter;
10
11
  remote: RemoteDBAdapter;
@@ -19,6 +20,13 @@ export interface LocalFirstClientConfig {
19
20
  logLevel?: LogLevel;
20
21
  autoSchema?: boolean;
21
22
  tables?: string[];
23
+ migrations?: string[];
24
+ autoStart?: boolean;
25
+ coalesceOutbox?: boolean;
26
+ jsonColumns?: Record<string, string[]>;
27
+ autoHydrateJSON?: boolean;
28
+ syncScope?: Record<string, any> | (() => Record<string, any> | Promise<Record<string, any>>);
29
+ autoPauseOnBackground?: boolean;
22
30
  }
23
31
  export declare class TransactionalClient {
24
32
  private parent;
@@ -32,7 +40,20 @@ export declare class TransactionalClient {
32
40
  mutate(tableName: string, rowId: string, operation: SyncOp, payload: Record<string, any> | null, options?: MutateOptions): Promise<void>;
33
41
  insert(tableName: string, rowIdOrPayload: string | Record<string, any>, payloadOrOptions?: Record<string, any> | MutateOptions, options?: MutateOptions): Promise<string>;
34
42
  update(tableName: string, rowId: string, payload: Record<string, any>, options?: MutateOptions): Promise<void>;
43
+ updateMany(tableName: string, updates: Array<{
44
+ id: string;
45
+ payload: Record<string, any>;
46
+ }>, options?: MutateOptions): Promise<void>;
35
47
  delete(tableName: string, rowId: string, options?: MutateOptions): Promise<void>;
48
+ findById<T = any>(tableName: string, id: string, options?: {
49
+ includeDeleted?: boolean;
50
+ }): Promise<T | null>;
51
+ findByIds<T = any>(tableName: string, ids: string[], options?: {
52
+ includeDeleted?: boolean;
53
+ }): Promise<T[]>;
54
+ findMany<T = any>(tableName: string, options?: QueryOptions): Promise<T[]>;
55
+ query<T = any>(sql: string, params?: any[]): Promise<T[]>;
56
+ from<T = any>(tableName: string): QueryBuilder<T>;
36
57
  get rawTx(): LocalTransaction;
37
58
  }
38
59
  export declare class LocalFirstClient {
@@ -42,15 +63,32 @@ export declare class LocalFirstClient {
42
63
  syncWorker: SyncWorker;
43
64
  schemaManager: SchemaManager;
44
65
  autoSchema: boolean;
66
+ coalesceOutbox: boolean;
45
67
  tables: string[];
68
+ jsonColumns: Record<string, string[]>;
69
+ autoHydrateJSON: boolean;
46
70
  private primaryKey;
71
+ private migrations;
72
+ private autoStart;
73
+ private initialization?;
47
74
  constructor(config: LocalFirstClientConfig);
48
75
  getPrimaryKey(): string;
76
+ /**
77
+ * Automatically deserializes JSON string columns into JavaScript objects/arrays.
78
+ */
79
+ hydrateRow<T = any>(tableName: string, row: any): T;
80
+ /**
81
+ * Writes a mutation to the outbox table, optionally coalescing with existing PENDING records for the same row.
82
+ * Returns the affected outboxId, or undefined if an offline INSERT was deleted offline before pushing.
83
+ */
84
+ recordOutboxMutation(tx: LocalTransaction, tableName: string, rowId: string, operation: SyncOp, payload: Record<string, any> | null, timestamp: number): Promise<string | undefined>;
49
85
  /**
50
86
  * Initializes the client: creates sync_outbox schema, ensures autoSchema tables if enabled,
51
- * and starts the background sync worker.
87
+ * applies migrations, and optionally starts automatic sync. Concurrent/repeated calls
88
+ * share initialization; failures can be retried. After stop(), explicitly start the worker.
52
89
  */
53
90
  initialize(): Promise<void>;
91
+ private initializeStorage;
54
92
  /**
55
93
  * Performs an atomic dual-write: mutates the local data table AND writes
56
94
  * to sync_outbox within the same database transaction.
@@ -87,6 +125,20 @@ export declare class LocalFirstClient {
87
125
  * await client.update('todos', 'todo-1', { completed: 1 });
88
126
  */
89
127
  update(tableName: string, rowId: string, payload: Record<string, any>, options?: MutateOptions): Promise<void>;
128
+ /**
129
+ * Updates multiple records atomically in a single database transaction.
130
+ * Emits reactive change events and queues outbox entries together.
131
+ *
132
+ * Example:
133
+ * await client.updateMany('todos', [
134
+ * { id: 'todo-1', payload: { completed: 1 } },
135
+ * { id: 'todo-2', payload: { completed: 1 } },
136
+ * ]);
137
+ */
138
+ updateMany(tableName: string, updates: Array<{
139
+ id: string;
140
+ payload: Record<string, any>;
141
+ }>, options?: MutateOptions): Promise<void>;
90
142
  /**
91
143
  * Convenience helper to delete a record by ID.
92
144
  *
@@ -97,12 +149,59 @@ export declare class LocalFirstClient {
97
149
  /**
98
150
  * Reads records from a local SQLite table without requiring manual SQL.
99
151
  * Supports where filtering with rich operators ($in, $gt, $contains, $or, etc.), order, limit, and offset.
152
+ * Pushes simple equality where clauses down to SQLite for high performance, with memory fallback.
100
153
  * Gracefully returns [] if table or columns have not been populated yet on empty schemas.
101
154
  *
155
+ /**
156
+ * Executes a raw relational SQL query with parameters directly against SQLite.
157
+ * Enables complex multi-table JOINs, GROUP BY aggregations (COUNT, SUM, AVG), CTEs, and window functions.
158
+ */
159
+ query<T = any>(sql: string, params?: any[]): Promise<T[]>;
160
+ /**
161
+ * Returns a fluent SQL QueryBuilder for the specified table.
162
+ * Allows writing clean JavaScript method chains (select, join, leftJoin, where, orderBy, groupBy)
163
+ * that automatically compile into optimized native SQLite queries.
164
+ *
102
165
  * Example:
103
- * const tenants = await client.findMany('tenants', { where: { status: 'ACTIVE' }, orderBy: 'name' });
104
- * const expensive = await client.findMany('properties', { where: { rent: { $gte: 2000 } } });
166
+ * const activeRentals = await client
167
+ * .from('contracts')
168
+ * .join('houses', 'contracts.house_id', 'houses.id')
169
+ * .leftJoin('tenants', 'contracts.tenant_id', 'tenants.id')
170
+ * .where({ 'contracts.business_id': bizId, 'contracts.status': 'ACTIVE' })
171
+ * .select('contracts.id', 'contracts.rent_amount', 'houses.house_number', 'tenants.name')
172
+ * .execute();
173
+ */
174
+ from<T = any>(tableName: string): QueryBuilder<T>;
175
+ /**
176
+ * Executes a native SQLite COUNT(*) query and returns the count directly.
177
+ */
178
+ count(tableName: string, options?: {
179
+ where?: WhereFilter;
180
+ }): Promise<number>;
181
+ /**
182
+ * Executes a native SQLite SUM(column) query and returns the sum directly.
105
183
  */
184
+ sum(tableName: string, column: string, options?: {
185
+ where?: WhereFilter;
186
+ }): Promise<number>;
187
+ /**
188
+ * Executes a native SQLite AVG(column) query and returns the average directly.
189
+ */
190
+ avg(tableName: string, column: string, options?: {
191
+ where?: WhereFilter;
192
+ }): Promise<number | null>;
193
+ /**
194
+ * Executes native multi-metric SQLite aggregation in a single query.
195
+ *
196
+ * Example:
197
+ * const stats = await client.aggregate('receipts', {
198
+ * where: { business_id: bizId },
199
+ * count: true,
200
+ * sum: ['amount', 'refunded_amount'],
201
+ * avg: ['amount']
202
+ * });
203
+ */
204
+ aggregate(tableName: string, spec: AggregateSpec): Promise<AggregateResult>;
106
205
  findMany<T = any>(tableName: string, options?: QueryOptions): Promise<T[]>;
107
206
  /**
108
207
  * Finds a single record by primary key.
@@ -110,7 +209,20 @@ export declare class LocalFirstClient {
110
209
  * Example:
111
210
  * const tenant = await client.findById('tenants', 'ten-1');
112
211
  */
113
- findById<T = any>(tableName: string, id: string): Promise<T | null>;
212
+ findById<T = any>(tableName: string, id: string, options?: {
213
+ includeDeleted?: boolean;
214
+ }): Promise<T | null>;
215
+ /**
216
+ * Finds multiple records by primary keys in a single batched SQL query.
217
+ * Eliminates the N-roundtrip bridge overhead of multiple findById calls.
218
+ * Automatically omits soft-deleted records unless includeDeleted: true.
219
+ *
220
+ * Example:
221
+ * const tenants = await client.findByIds('tenants', ['ten-1', 'ten-2', 'ten-3']);
222
+ */
223
+ findByIds<T = any>(tableName: string, ids: string[], options?: {
224
+ includeDeleted?: boolean;
225
+ }): Promise<T[]>;
114
226
  /**
115
227
  * Tracks an existing mutation into sync_outbox without modifying the table.
116
228
  * Can accept an external transaction `options.tx` for custom atomic SQL workflows.
@@ -146,8 +258,24 @@ export declare class LocalFirstClient {
146
258
  * to query all remote records.
147
259
  */
148
260
  resetSyncCursor(): Promise<void>;
261
+ /**
262
+ * Dynamically sets or updates the active sync scope.
263
+ */
264
+ setSyncScope(scope?: Record<string, any> | (() => Record<string, any> | Promise<Record<string, any>>)): void;
149
265
  /**
150
266
  * Stops all active intervals, background sync loops, and network listeners.
151
267
  */
152
268
  stop(): void;
269
+ /**
270
+ * Stops background sync workers, cleans up remote listeners,
271
+ * and closes the local database adapter connection (releasing native locks).
272
+ */
273
+ close(): Promise<void>;
274
+ /**
275
+ * Resets local state by stopping sync and deleting or dropping all managed tables,
276
+ * clearing the sync_outbox and _sync_meta stores, and resetting the sync cursor.
277
+ */
278
+ reset(options?: {
279
+ dropTables?: boolean;
280
+ }): Promise<void>;
153
281
  }