local-lite 0.0.0-stage → 0.0.1

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 (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +349 -2
  3. package/dist/adapters/local/better-sqlite3.d.ts +23 -0
  4. package/dist/adapters/local/better-sqlite3.js +46 -0
  5. package/dist/adapters/local/expo-sqlite.d.ts +8 -0
  6. package/dist/adapters/local/expo-sqlite.js +30 -0
  7. package/dist/adapters/local/index.d.ts +13 -0
  8. package/dist/adapters/local/index.js +22 -0
  9. package/dist/adapters/local/libsql.d.ts +24 -0
  10. package/dist/adapters/local/libsql.js +87 -0
  11. package/dist/adapters/local/node-sqlite.d.ts +24 -0
  12. package/dist/adapters/local/node-sqlite.js +44 -0
  13. package/dist/adapters/local/op-sqlite.d.ts +26 -0
  14. package/dist/adapters/local/op-sqlite.js +59 -0
  15. package/dist/adapters/local/sql-js.d.ts +26 -0
  16. package/dist/adapters/local/sql-js.js +63 -0
  17. package/dist/adapters/network/index.d.ts +11 -0
  18. package/dist/adapters/network/index.js +53 -0
  19. package/dist/adapters/remote/convex.d.ts +71 -0
  20. package/dist/adapters/remote/convex.js +185 -0
  21. package/dist/adapters/remote/demo.d.ts +21 -0
  22. package/dist/adapters/remote/demo.js +46 -0
  23. package/dist/adapters/remote/firestore.d.ts +64 -0
  24. package/dist/adapters/remote/firestore.js +302 -0
  25. package/dist/adapters/remote/index.d.ts +10 -0
  26. package/dist/adapters/remote/index.js +20 -0
  27. package/dist/adapters/remote/supabase.d.ts +97 -0
  28. package/dist/adapters/remote/supabase.js +276 -0
  29. package/dist/config/index.d.ts +24 -0
  30. package/dist/config/index.js +101 -0
  31. package/dist/config/resolve-local.d.ts +7 -0
  32. package/dist/config/resolve-local.js +68 -0
  33. package/dist/config/resolve-remote.d.ts +7 -0
  34. package/dist/config/resolve-remote.js +81 -0
  35. package/dist/config/types.d.ts +159 -0
  36. package/dist/config/types.js +2 -0
  37. package/dist/core/client.d.ts +100 -0
  38. package/dist/core/client.js +273 -0
  39. package/dist/core/conflict.d.ts +5 -0
  40. package/dist/core/conflict.js +38 -0
  41. package/dist/core/meta-store.d.ts +28 -0
  42. package/dist/core/meta-store.js +64 -0
  43. package/dist/core/outbox-store.d.ts +45 -0
  44. package/dist/core/outbox-store.js +141 -0
  45. package/dist/core/schema-manager.d.ts +39 -0
  46. package/dist/core/schema-manager.js +150 -0
  47. package/dist/core/sync-worker.d.ts +74 -0
  48. package/dist/core/sync-worker.js +335 -0
  49. package/dist/core/table-manager.d.ts +19 -0
  50. package/dist/core/table-manager.js +74 -0
  51. package/dist/core/types.d.ts +73 -0
  52. package/dist/core/types.js +2 -0
  53. package/dist/index.d.ts +17 -0
  54. package/dist/index.js +33 -0
  55. package/dist/react/index.d.ts +34 -0
  56. package/dist/react/index.js +103 -0
  57. package/dist/reactive/event-emitter.d.ts +7 -0
  58. package/dist/reactive/event-emitter.js +34 -0
  59. package/dist/utils/logger.d.ts +51 -0
  60. package/dist/utils/logger.js +169 -0
  61. package/dist/utils/sql.d.ts +4 -0
  62. package/dist/utils/sql.js +9 -0
  63. package/dist/utils/uuid.d.ts +7 -0
  64. package/dist/utils/uuid.js +27 -0
  65. package/package.json +130 -3
@@ -0,0 +1,45 @@
1
+ import { LocalDBAdapter, LocalTransaction } from '../adapters/local/index.js';
2
+ import { OutboxRecord, 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
+ * Fetches pending and eligible failed outbox records for synchronization.
14
+ */
15
+ static getPending(db: LocalDBAdapter, maxRetries?: number, limit?: number): Promise<OutboxRecord[]>;
16
+ /**
17
+ * Finds the latest pending outbox record for a given table and row.
18
+ */
19
+ static findPendingByRow(tx: LocalTransaction, tableName: string, rowId: string): Promise<OutboxRecord | undefined>;
20
+ /**
21
+ * Marks a batch of outbox IDs as 'PROCESSING'.
22
+ */
23
+ static markProcessing(db: LocalDBAdapter, ids: string[]): Promise<void>;
24
+ /**
25
+ * Resets any orphaned 'PROCESSING' records back to 'PENDING' on startup.
26
+ * Protects against app kills or crashes that happened mid-push.
27
+ */
28
+ static resetStaleProcessing(db: LocalDBAdapter): Promise<void>;
29
+ /**
30
+ * Deletes a batch of successfully pushed outbox entries.
31
+ */
32
+ static deleteCompleted(db: LocalDBAdapter, ids: string[]): Promise<void>;
33
+ /**
34
+ * Deletes a single outbox record within an active transaction.
35
+ */
36
+ static deleteRecord(tx: LocalTransaction, id: string): Promise<void>;
37
+ /**
38
+ * Updates an outbox record's payload (used during conflict merge).
39
+ */
40
+ static updatePayload(tx: LocalTransaction, id: string, payload: Record<string, any>): Promise<void>;
41
+ /**
42
+ * Marks a batch of outbox IDs as 'FAILED' and increments retry counters.
43
+ */
44
+ static markFailed(db: LocalDBAdapter, ids: string[], errorMessage: string): Promise<void>;
45
+ }
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OutboxStore = void 0;
4
+ class OutboxStore {
5
+ /**
6
+ * Initializes the sync_outbox schema if it does not already exist.
7
+ */
8
+ static async ensureSchema(db) {
9
+ await db.execute(`CREATE TABLE IF NOT EXISTS sync_outbox (
10
+ id TEXT PRIMARY KEY,
11
+ table_name TEXT NOT NULL,
12
+ row_id TEXT NOT NULL,
13
+ operation TEXT NOT NULL,
14
+ payload TEXT,
15
+ timestamp INTEGER NOT NULL,
16
+ status TEXT NOT NULL DEFAULT 'PENDING',
17
+ retry_count INTEGER NOT NULL DEFAULT 0,
18
+ last_error TEXT
19
+ );`);
20
+ }
21
+ /**
22
+ * Inserts a new outbox entry within an existing database transaction.
23
+ */
24
+ static async insertRecord(tx, op) {
25
+ await tx.execute(`INSERT INTO sync_outbox (id, table_name, row_id, operation, payload, timestamp, status, retry_count)
26
+ VALUES (?, ?, ?, ?, ?, ?, 'PENDING', 0)`, [
27
+ op.id,
28
+ op.tableName,
29
+ op.rowId,
30
+ op.operation,
31
+ op.payload ? JSON.stringify(op.payload) : null,
32
+ op.timestamp,
33
+ ]);
34
+ }
35
+ /**
36
+ * Fetches pending and eligible failed outbox records for synchronization.
37
+ */
38
+ static async getPending(db, maxRetries = 5, limit = 50) {
39
+ const isInfinite = !Number.isFinite(maxRetries) || maxRetries === -1;
40
+ const query = isInfinite
41
+ ? `SELECT * FROM sync_outbox
42
+ WHERE status = 'PENDING' OR status = 'FAILED'
43
+ ORDER BY timestamp ASC LIMIT ?`
44
+ : `SELECT * FROM sync_outbox
45
+ WHERE status = 'PENDING' OR (status = 'FAILED' AND retry_count < ?)
46
+ ORDER BY timestamp ASC LIMIT ?`;
47
+ const params = isInfinite ? [limit] : [maxRetries, limit];
48
+ const rows = await db.execute(query, params);
49
+ if (!rows || rows.length === 0)
50
+ return [];
51
+ return rows.map((r) => ({
52
+ id: r.id,
53
+ tableName: r.table_name,
54
+ rowId: r.row_id,
55
+ operation: r.operation,
56
+ payload: r.payload ? JSON.parse(r.payload) : null,
57
+ timestamp: Number(r.timestamp),
58
+ status: r.status,
59
+ retryCount: Number(r.retry_count || 0),
60
+ lastError: r.last_error,
61
+ }));
62
+ }
63
+ /**
64
+ * Finds the latest pending outbox record for a given table and row.
65
+ */
66
+ static async findPendingByRow(tx, tableName, rowId) {
67
+ const rows = await tx.execute(`SELECT * FROM sync_outbox
68
+ WHERE table_name = ? AND row_id = ? AND status != 'FAILED'
69
+ ORDER BY timestamp DESC LIMIT 1`, [tableName, rowId]);
70
+ if (!rows || rows.length === 0)
71
+ return undefined;
72
+ const r = rows[0];
73
+ return {
74
+ id: r.id,
75
+ tableName: r.table_name,
76
+ rowId: r.row_id,
77
+ operation: r.operation,
78
+ payload: r.payload ? JSON.parse(r.payload) : null,
79
+ timestamp: Number(r.timestamp),
80
+ status: r.status,
81
+ retryCount: Number(r.retry_count || 0),
82
+ lastError: r.last_error,
83
+ };
84
+ }
85
+ /**
86
+ * Marks a batch of outbox IDs as 'PROCESSING'.
87
+ */
88
+ static async markProcessing(db, ids) {
89
+ if (ids.length === 0)
90
+ return;
91
+ const placeholders = ids.map(() => '?').join(',');
92
+ await db.execute(`UPDATE sync_outbox SET status = 'PROCESSING' WHERE id IN (${placeholders})`, ids);
93
+ }
94
+ /**
95
+ * Resets any orphaned 'PROCESSING' records back to 'PENDING' on startup.
96
+ * Protects against app kills or crashes that happened mid-push.
97
+ */
98
+ static async resetStaleProcessing(db) {
99
+ try {
100
+ await db.execute(`UPDATE sync_outbox SET status = 'PENDING' WHERE status = 'PROCESSING'`);
101
+ }
102
+ catch {
103
+ // Table may not exist yet
104
+ }
105
+ }
106
+ /**
107
+ * Deletes a batch of successfully pushed outbox entries.
108
+ */
109
+ static async deleteCompleted(db, ids) {
110
+ if (ids.length === 0)
111
+ return;
112
+ const placeholders = ids.map(() => '?').join(',');
113
+ await db.execute(`DELETE FROM sync_outbox WHERE id IN (${placeholders})`, ids);
114
+ }
115
+ /**
116
+ * Deletes a single outbox record within an active transaction.
117
+ */
118
+ static async deleteRecord(tx, id) {
119
+ await tx.execute(`DELETE FROM sync_outbox WHERE id = ?`, [id]);
120
+ }
121
+ /**
122
+ * Updates an outbox record's payload (used during conflict merge).
123
+ */
124
+ static async updatePayload(tx, id, payload) {
125
+ await tx.execute(`UPDATE sync_outbox SET payload = ? WHERE id = ?`, [JSON.stringify(payload), id]);
126
+ }
127
+ /**
128
+ * Marks a batch of outbox IDs as 'FAILED' and increments retry counters.
129
+ */
130
+ static async markFailed(db, ids, errorMessage) {
131
+ if (ids.length === 0)
132
+ return;
133
+ const placeholders = ids.map(() => '?').join(',');
134
+ await db.execute(`UPDATE sync_outbox
135
+ SET status = 'FAILED',
136
+ retry_count = retry_count + 1,
137
+ last_error = ?
138
+ WHERE id IN (${placeholders})`, [errorMessage, ...ids]);
139
+ }
140
+ }
141
+ exports.OutboxStore = OutboxStore;
@@ -0,0 +1,39 @@
1
+ export interface DBExecutable {
2
+ execute(query: string, params?: any[]): Promise<any[]>;
3
+ }
4
+ /**
5
+ * Detects the appropriate SQLite column type affinity from a JavaScript runtime value.
6
+ *
7
+ * Mapping:
8
+ * - boolean -> INTEGER (0 or 1)
9
+ * - integer -> INTEGER
10
+ * - float/double -> REAL
11
+ * - string -> TEXT
12
+ * - object/array -> TEXT (JSON formatted)
13
+ * - Uint8Array -> BLOB
14
+ * - null/undefined -> TEXT (flexible SQLite type affinity)
15
+ */
16
+ export declare function detectSQLiteType(value: any): 'INTEGER' | 'REAL' | 'TEXT' | 'BLOB';
17
+ /**
18
+ * Normalizes values for SQLite storage:
19
+ * - Booleans are converted to 1 / 0
20
+ * - Plain objects and arrays are JSON serialized
21
+ */
22
+ export declare function formatValueForStorage(value: any): any;
23
+ export declare class SchemaManager {
24
+ private knownTables;
25
+ private knownColumns;
26
+ /**
27
+ * Resets the schema cache (useful for testing or full database resets).
28
+ */
29
+ clearCache(): void;
30
+ /**
31
+ * Ensures that the target table exists in SQLite with the specified primary key.
32
+ */
33
+ ensureTable(db: DBExecutable, tableName: string, primaryKey?: string): Promise<void>;
34
+ /**
35
+ * Automatically introspects and adds missing columns to the table via ALTER TABLE.
36
+ * Also normalizes payload values for SQLite compatibility.
37
+ */
38
+ ensureColumns(db: DBExecutable, tableName: string, payload: Record<string, any> | null, primaryKey?: string): Promise<Record<string, any>>;
39
+ }
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SchemaManager = void 0;
4
+ exports.detectSQLiteType = detectSQLiteType;
5
+ exports.formatValueForStorage = formatValueForStorage;
6
+ const sql_js_1 = require("../utils/sql.js");
7
+ /**
8
+ * Detects the appropriate SQLite column type affinity from a JavaScript runtime value.
9
+ *
10
+ * Mapping:
11
+ * - boolean -> INTEGER (0 or 1)
12
+ * - integer -> INTEGER
13
+ * - float/double -> REAL
14
+ * - string -> TEXT
15
+ * - object/array -> TEXT (JSON formatted)
16
+ * - Uint8Array -> BLOB
17
+ * - null/undefined -> TEXT (flexible SQLite type affinity)
18
+ */
19
+ function detectSQLiteType(value) {
20
+ if (value === null || value === undefined) {
21
+ return 'TEXT';
22
+ }
23
+ const type = typeof value;
24
+ if (type === 'boolean') {
25
+ return 'INTEGER';
26
+ }
27
+ if (type === 'number') {
28
+ return Number.isInteger(value) ? 'INTEGER' : 'REAL';
29
+ }
30
+ if (type === 'string') {
31
+ return 'TEXT';
32
+ }
33
+ if (value instanceof Uint8Array) {
34
+ return 'BLOB';
35
+ }
36
+ if (type === 'object') {
37
+ return 'TEXT';
38
+ }
39
+ return 'TEXT';
40
+ }
41
+ /**
42
+ * Normalizes values for SQLite storage:
43
+ * - Booleans are converted to 1 / 0
44
+ * - Plain objects and arrays are JSON serialized
45
+ */
46
+ function formatValueForStorage(value) {
47
+ if (value === null || value === undefined) {
48
+ return null;
49
+ }
50
+ if (typeof value === 'boolean') {
51
+ return value ? 1 : 0;
52
+ }
53
+ if (typeof value === 'object' && !(value instanceof Uint8Array)) {
54
+ return JSON.stringify(value);
55
+ }
56
+ return value;
57
+ }
58
+ class SchemaManager {
59
+ knownTables = new Set();
60
+ knownColumns = new Set();
61
+ /**
62
+ * Resets the schema cache (useful for testing or full database resets).
63
+ */
64
+ clearCache() {
65
+ this.knownTables.clear();
66
+ this.knownColumns.clear();
67
+ }
68
+ /**
69
+ * Ensures that the target table exists in SQLite with the specified primary key.
70
+ */
71
+ async ensureTable(db, tableName, primaryKey = 'id') {
72
+ if (this.knownTables.has(tableName)) {
73
+ return;
74
+ }
75
+ const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
76
+ const safePk = (0, sql_js_1.escapeIdentifier)(primaryKey);
77
+ await db.execute(`CREATE TABLE IF NOT EXISTS ${safeTable} (${safePk} TEXT PRIMARY KEY);`);
78
+ this.knownTables.add(tableName);
79
+ this.knownColumns.add(`${tableName}:${primaryKey.toLowerCase()}`);
80
+ }
81
+ /**
82
+ * Automatically introspects and adds missing columns to the table via ALTER TABLE.
83
+ * Also normalizes payload values for SQLite compatibility.
84
+ */
85
+ async ensureColumns(db, tableName, payload, primaryKey = 'id') {
86
+ await this.ensureTable(db, tableName, primaryKey);
87
+ if (!payload || typeof payload !== 'object') {
88
+ return {};
89
+ }
90
+ const keys = Object.keys(payload);
91
+ if (keys.length === 0) {
92
+ return {};
93
+ }
94
+ const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
95
+ let cacheMiss = false;
96
+ for (const key of keys) {
97
+ if (key === primaryKey)
98
+ continue;
99
+ const cacheKey = `${tableName}:${key.toLowerCase()}`;
100
+ if (!this.knownColumns.has(cacheKey)) {
101
+ cacheMiss = true;
102
+ break;
103
+ }
104
+ }
105
+ if (cacheMiss) {
106
+ // Query PRAGMA table_info to discover existing columns from SQLite
107
+ try {
108
+ const tableInfo = await db.execute(`PRAGMA table_info(${safeTable});`);
109
+ if (Array.isArray(tableInfo)) {
110
+ for (const col of tableInfo) {
111
+ if (col && col.name) {
112
+ this.knownColumns.add(`${tableName}:${String(col.name).toLowerCase()}`);
113
+ }
114
+ }
115
+ }
116
+ }
117
+ catch (err) {
118
+ // Fallback for mocks or custom drivers where PRAGMA table_info might not be supported
119
+ }
120
+ // Check for columns that still do not exist, and alter table
121
+ for (const key of keys) {
122
+ if (key === primaryKey)
123
+ continue;
124
+ const cacheKey = `${tableName}:${key.toLowerCase()}`;
125
+ if (!this.knownColumns.has(cacheKey)) {
126
+ const detectedType = detectSQLiteType(payload[key]);
127
+ const safeCol = (0, sql_js_1.escapeIdentifier)(key);
128
+ try {
129
+ await db.execute(`ALTER TABLE ${safeTable} ADD COLUMN ${safeCol} ${detectedType};`);
130
+ }
131
+ catch (err) {
132
+ // Ignore if column already exists (e.g. concurrent race)
133
+ const msg = String(err?.message || err || '');
134
+ if (!msg.toLowerCase().includes('duplicate column')) {
135
+ // Ignore or rethrow
136
+ }
137
+ }
138
+ this.knownColumns.add(cacheKey);
139
+ }
140
+ }
141
+ }
142
+ // Format all values for SQLite storage
143
+ const formatted = {};
144
+ for (const key of keys) {
145
+ formatted[key] = formatValueForStorage(payload[key]);
146
+ }
147
+ return formatted;
148
+ }
149
+ }
150
+ exports.SchemaManager = SchemaManager;
@@ -0,0 +1,74 @@
1
+ import { LocalDBAdapter } from '../adapters/local/index.js';
2
+ import { RemoteDBAdapter } from '../adapters/remote/index.js';
3
+ import { EventEmitter } from '../reactive/event-emitter.js';
4
+ import { ConflictResolver, ConflictStrategy, DeltaChanges, NetworkStatusProvider, RetryConfig } from './types.js';
5
+ import { SchemaManager } from './schema-manager.js';
6
+ import { SyncLogger, CustomLogger, LogLevel } from '../utils/logger.js';
7
+ export interface SyncWorkerOptions {
8
+ pushIntervalMs?: number;
9
+ pullIntervalMs?: number;
10
+ maxRetries?: number;
11
+ retry?: number | RetryConfig;
12
+ logger?: SyncLogger | CustomLogger | boolean;
13
+ logLevel?: LogLevel;
14
+ conflictResolver?: ConflictStrategy | ConflictResolver;
15
+ network?: NetworkStatusProvider;
16
+ primaryKey?: string;
17
+ autoSchema?: boolean;
18
+ schemaManager?: SchemaManager;
19
+ }
20
+ export declare class SyncWorker {
21
+ private db;
22
+ private remote;
23
+ private events;
24
+ private network;
25
+ private conflictResolver;
26
+ private primaryKey;
27
+ private autoSchema;
28
+ private schemaManager?;
29
+ logger: SyncLogger;
30
+ retryConfig: RetryConfig;
31
+ private pushIntervalMs;
32
+ private pullIntervalMs;
33
+ private maxRetries;
34
+ private pushIntervalTimer?;
35
+ private pullIntervalTimer?;
36
+ private pushDebounceTimer?;
37
+ private networkUnsubscribe?;
38
+ private isPushing;
39
+ private isPulling;
40
+ private lastPulledTimestamp;
41
+ private cursorLoaded;
42
+ private deltaQueue;
43
+ constructor(db: LocalDBAdapter, remote: RemoteDBAdapter, events: EventEmitter, options?: SyncWorkerOptions);
44
+ /**
45
+ * Starts background interval push/pull loops and listens for network & realtime updates.
46
+ */
47
+ start(): void;
48
+ /**
49
+ * Stops all active intervals, timers, and listeners.
50
+ */
51
+ stop(): void;
52
+ /**
53
+ * Schedules an immediate push with a short debounce.
54
+ */
55
+ schedulePush(delayMs?: number): void;
56
+ private isOnline;
57
+ /**
58
+ * Pushes pending outbox records to the remote database.
59
+ */
60
+ pushLocalChanges(): Promise<void>;
61
+ private handlePushFailure;
62
+ /**
63
+ * Pulls delta changes from the remote backend since lastPulledTimestamp.
64
+ */
65
+ pullRemoteChanges(): Promise<void>;
66
+ /**
67
+ * Applies incoming server delta changes to local tables with conflict resolution.
68
+ * Serializes execution in a queue to prevent concurrent SQLite transactions.
69
+ */
70
+ applyDeltasLocally(deltas: DeltaChanges): Promise<void>;
71
+ private processDeltasLocally;
72
+ getLastPulledTimestamp(): Promise<number>;
73
+ resetSyncCursor(): Promise<void>;
74
+ }