local-lite 0.0.5 → 0.0.7
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/dist/adapters/local/better-sqlite3.d.ts +5 -1
- package/dist/adapters/local/better-sqlite3.js +35 -14
- package/dist/adapters/local/expo-sqlite.d.ts +11 -2
- package/dist/adapters/local/expo-sqlite.js +37 -20
- package/dist/adapters/local/index.d.ts +2 -0
- package/dist/adapters/local/libsql.d.ts +1 -0
- package/dist/adapters/local/libsql.js +1 -0
- package/dist/adapters/local/node-sqlite.d.ts +5 -1
- package/dist/adapters/local/node-sqlite.js +30 -12
- package/dist/adapters/local/op-sqlite.d.ts +1 -0
- package/dist/adapters/local/op-sqlite.js +1 -0
- package/dist/adapters/local/sql-js.d.ts +1 -0
- package/dist/adapters/local/sql-js.js +1 -0
- package/dist/adapters/remote/firestore.js +39 -32
- package/dist/adapters/remote/supabase.d.ts +5 -0
- package/dist/adapters/remote/supabase.js +192 -85
- package/dist/core/client.d.ts +1 -9
- package/dist/core/client.js +123 -105
- package/dist/core/outbox-store.d.ts +5 -0
- package/dist/core/outbox-store.js +29 -4
- package/dist/core/query-builder.d.ts +1 -0
- package/dist/core/query-builder.js +140 -71
- package/dist/core/query.js +28 -32
- package/dist/core/schema-manager.d.ts +4 -15
- package/dist/core/schema-manager.js +45 -135
- package/dist/core/sync-worker.js +7 -2
- package/dist/react/index.js +123 -57
- package/dist/react/status.d.ts +5 -0
- package/dist/react/status.js +20 -12
- package/dist/utils/uuid.d.ts +1 -1
- package/dist/utils/uuid.js +7 -16
- package/package.json +5 -4
package/dist/core/query.js
CHANGED
|
@@ -48,26 +48,14 @@ function matchesField(fieldValue, filter) {
|
|
|
48
48
|
if (ops.$lte !== undefined && !(fieldValue <= ops.$lte)) {
|
|
49
49
|
return false;
|
|
50
50
|
}
|
|
51
|
-
if (ops.$contains !== undefined) {
|
|
52
|
-
const str = fieldValue
|
|
53
|
-
|
|
54
|
-
if (!str.includes(search)) {
|
|
51
|
+
if (ops.$contains !== undefined || ops.$startsWith !== undefined || ops.$endsWith !== undefined) {
|
|
52
|
+
const str = fieldValue == null ? '' : String(fieldValue).toLowerCase();
|
|
53
|
+
if (ops.$contains !== undefined && !str.includes(String(ops.$contains).toLowerCase()))
|
|
55
54
|
return false;
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
if (ops.$startsWith !== undefined) {
|
|
59
|
-
const str = fieldValue === null || fieldValue === undefined ? '' : String(fieldValue).toLowerCase();
|
|
60
|
-
const prefix = String(ops.$startsWith).toLowerCase();
|
|
61
|
-
if (!str.startsWith(prefix)) {
|
|
55
|
+
if (ops.$startsWith !== undefined && !str.startsWith(String(ops.$startsWith).toLowerCase()))
|
|
62
56
|
return false;
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
if (ops.$endsWith !== undefined) {
|
|
66
|
-
const str = fieldValue === null || fieldValue === undefined ? '' : String(fieldValue).toLowerCase();
|
|
67
|
-
const suffix = String(ops.$endsWith).toLowerCase();
|
|
68
|
-
if (!str.endsWith(suffix)) {
|
|
57
|
+
if (ops.$endsWith !== undefined && !str.endsWith(String(ops.$endsWith).toLowerCase()))
|
|
69
58
|
return false;
|
|
70
|
-
}
|
|
71
59
|
}
|
|
72
60
|
if (ops.$null !== undefined) {
|
|
73
61
|
const isNull = fieldValue === null || fieldValue === undefined;
|
|
@@ -131,18 +119,28 @@ function hasExplicitDeletedFilter(where) {
|
|
|
131
119
|
function applyQueryOptions(rows, options) {
|
|
132
120
|
if (!rows || rows.length === 0)
|
|
133
121
|
return [];
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
122
|
+
const needsDeleteFilter = !options?.includeDeleted && !hasExplicitDeletedFilter(options?.where);
|
|
123
|
+
const needsWhere = Boolean(options?.where);
|
|
124
|
+
const needsSort = Boolean(options?.orderBy);
|
|
125
|
+
const needsSlice = (options?.offset !== undefined && options.offset > 0) ||
|
|
126
|
+
(options?.limit !== undefined && options.limit > 0);
|
|
127
|
+
const needsSelect = Array.isArray(options?.select) && options.select.length > 0;
|
|
128
|
+
// Fast-path: nothing to do
|
|
129
|
+
if (!needsDeleteFilter && !needsWhere && !needsSort && !needsSlice && !needsSelect) {
|
|
130
|
+
return rows;
|
|
131
|
+
}
|
|
132
|
+
let result = rows;
|
|
133
|
+
// 0. Soft-delete filtering
|
|
134
|
+
if (needsDeleteFilter) {
|
|
138
135
|
result = result.filter((row) => !isDeletedRecord(row));
|
|
139
136
|
}
|
|
140
137
|
// 1. Where filtering
|
|
141
|
-
if (
|
|
138
|
+
if (needsWhere) {
|
|
142
139
|
result = result.filter((row) => matchesWhere(row, options.where));
|
|
143
140
|
}
|
|
144
141
|
// 2. Order by
|
|
145
|
-
if (
|
|
142
|
+
if (needsSort) {
|
|
143
|
+
result = [...result]; // copy only before sort mutation
|
|
146
144
|
const key = options.orderBy;
|
|
147
145
|
const isAsc = (options.order || 'asc').toLowerCase() === 'asc';
|
|
148
146
|
result.sort((a, b) => {
|
|
@@ -164,21 +162,19 @@ function applyQueryOptions(rows, options) {
|
|
|
164
162
|
});
|
|
165
163
|
}
|
|
166
164
|
// 3. Offset and Limit
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
result = result.slice(0, options.limit);
|
|
165
|
+
if (needsSlice) {
|
|
166
|
+
const offset = options?.offset && options.offset > 0 ? options.offset : 0;
|
|
167
|
+
const limit = options?.limit && options.limit > 0 ? options.limit : undefined;
|
|
168
|
+
result = limit !== undefined ? result.slice(offset, offset + limit) : result.slice(offset);
|
|
172
169
|
}
|
|
173
|
-
// 4. Column
|
|
174
|
-
if (
|
|
170
|
+
// 4. Column projection
|
|
171
|
+
if (needsSelect) {
|
|
175
172
|
const fields = options.select;
|
|
176
173
|
result = result.map((row) => {
|
|
177
174
|
const projected = {};
|
|
178
175
|
for (const field of fields) {
|
|
179
|
-
if (field in row)
|
|
176
|
+
if (field in row)
|
|
180
177
|
projected[field] = row[field];
|
|
181
|
-
}
|
|
182
178
|
}
|
|
183
179
|
return projected;
|
|
184
180
|
});
|
|
@@ -3,15 +3,6 @@ export interface DBExecutable {
|
|
|
3
3
|
}
|
|
4
4
|
/**
|
|
5
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
6
|
*/
|
|
16
7
|
export declare function detectSQLiteType(value: any): 'INTEGER' | 'REAL' | 'TEXT' | 'BLOB';
|
|
17
8
|
/**
|
|
@@ -24,18 +15,16 @@ export declare class SchemaManager {
|
|
|
24
15
|
private knownTables;
|
|
25
16
|
private knownColumns;
|
|
26
17
|
private knownIndexes;
|
|
27
|
-
/**
|
|
28
|
-
* Resets the schema cache (useful for testing or full database resets).
|
|
29
|
-
*/
|
|
30
18
|
clearCache(): void;
|
|
31
|
-
/**
|
|
32
|
-
* Checks whether a column is known to exist on the specified table.
|
|
33
|
-
*/
|
|
34
19
|
hasColumn(tableName: string, column: string): boolean;
|
|
35
20
|
/**
|
|
36
21
|
* Introspects table columns via PRAGMA table_info if not previously cached.
|
|
37
22
|
*/
|
|
38
23
|
ensureTableIntrospected(db: DBExecutable, tableName: string): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Automatically creates single-column and composite FK+deleted indexes for given columns.
|
|
26
|
+
*/
|
|
27
|
+
private autoIndexColumns;
|
|
39
28
|
/**
|
|
40
29
|
* Ensures that the target table exists in SQLite with the specified primary key.
|
|
41
30
|
*/
|
|
@@ -6,36 +6,14 @@ exports.formatValueForStorage = formatValueForStorage;
|
|
|
6
6
|
const sql_js_1 = require("../utils/sql.js");
|
|
7
7
|
/**
|
|
8
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
9
|
*/
|
|
19
10
|
function detectSQLiteType(value) {
|
|
20
|
-
if (
|
|
21
|
-
return 'TEXT';
|
|
22
|
-
}
|
|
23
|
-
const type = typeof value;
|
|
24
|
-
if (type === 'boolean') {
|
|
11
|
+
if (typeof value === 'boolean')
|
|
25
12
|
return 'INTEGER';
|
|
26
|
-
|
|
27
|
-
if (type === 'number') {
|
|
13
|
+
if (typeof value === 'number')
|
|
28
14
|
return Number.isInteger(value) ? 'INTEGER' : 'REAL';
|
|
29
|
-
|
|
30
|
-
if (type === 'string') {
|
|
31
|
-
return 'TEXT';
|
|
32
|
-
}
|
|
33
|
-
if (value instanceof Uint8Array) {
|
|
15
|
+
if (value instanceof Uint8Array)
|
|
34
16
|
return 'BLOB';
|
|
35
|
-
}
|
|
36
|
-
if (type === 'object') {
|
|
37
|
-
return 'TEXT';
|
|
38
|
-
}
|
|
39
17
|
return 'TEXT';
|
|
40
18
|
}
|
|
41
19
|
/**
|
|
@@ -44,32 +22,23 @@ function detectSQLiteType(value) {
|
|
|
44
22
|
* - Plain objects and arrays are JSON serialized
|
|
45
23
|
*/
|
|
46
24
|
function formatValueForStorage(value) {
|
|
47
|
-
if (value === null || value === undefined)
|
|
25
|
+
if (value === null || value === undefined)
|
|
48
26
|
return null;
|
|
49
|
-
|
|
50
|
-
if (typeof value === 'boolean') {
|
|
27
|
+
if (typeof value === 'boolean')
|
|
51
28
|
return value ? 1 : 0;
|
|
52
|
-
|
|
53
|
-
if (typeof value === 'object' && !(value instanceof Uint8Array)) {
|
|
29
|
+
if (typeof value === 'object' && !(value instanceof Uint8Array))
|
|
54
30
|
return JSON.stringify(value);
|
|
55
|
-
}
|
|
56
31
|
return value;
|
|
57
32
|
}
|
|
58
33
|
class SchemaManager {
|
|
59
34
|
knownTables = new Set();
|
|
60
35
|
knownColumns = new Set();
|
|
61
36
|
knownIndexes = new Set();
|
|
62
|
-
/**
|
|
63
|
-
* Resets the schema cache (useful for testing or full database resets).
|
|
64
|
-
*/
|
|
65
37
|
clearCache() {
|
|
66
38
|
this.knownTables.clear();
|
|
67
39
|
this.knownColumns.clear();
|
|
68
40
|
this.knownIndexes.clear();
|
|
69
41
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Checks whether a column is known to exist on the specified table.
|
|
72
|
-
*/
|
|
73
42
|
hasColumn(tableName, column) {
|
|
74
43
|
return this.knownColumns.has(`${tableName}:${column.toLowerCase()}`);
|
|
75
44
|
}
|
|
@@ -84,57 +53,54 @@ class SchemaManager {
|
|
|
84
53
|
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
85
54
|
try {
|
|
86
55
|
const tableInfo = await db.execute(`PRAGMA table_info(${safeTable});`);
|
|
56
|
+
const cols = [];
|
|
87
57
|
if (Array.isArray(tableInfo)) {
|
|
88
58
|
for (const col of tableInfo) {
|
|
89
|
-
if (col
|
|
59
|
+
if (col?.name) {
|
|
90
60
|
this.knownColumns.add(`${tableName}:${String(col.name).toLowerCase()}`);
|
|
61
|
+
cols.push(String(col.name));
|
|
91
62
|
}
|
|
92
63
|
}
|
|
93
64
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
65
|
+
await this.autoIndexColumns(db, tableName, safeTable, cols);
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Automatically creates single-column and composite FK+deleted indexes for given columns.
|
|
71
|
+
*/
|
|
72
|
+
async autoIndexColumns(db, tableName, safeTable, columns) {
|
|
73
|
+
const hasDeleted = this.knownColumns.has(`${tableName}:deleted`);
|
|
74
|
+
for (const key of columns) {
|
|
75
|
+
const lower = key.toLowerCase();
|
|
76
|
+
if (lower.endsWith('_id') || lower === 'deleted' || lower === 'created_at' || lower === 'updated_at') {
|
|
77
|
+
const indexKey = `${tableName}:${lower}`;
|
|
78
|
+
if (!this.knownIndexes.has(indexKey)) {
|
|
79
|
+
this.knownIndexes.add(indexKey);
|
|
80
|
+
try {
|
|
81
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS ${(0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${key}`)} ON ${safeTable} (${(0, sql_js_1.escapeIdentifier)(key)});`);
|
|
110
82
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
catch { }
|
|
122
|
-
}
|
|
83
|
+
catch { }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (hasDeleted && lower.endsWith('_id') && lower !== 'id') {
|
|
87
|
+
const compositeKey = `${tableName}:${lower}_deleted`;
|
|
88
|
+
if (!this.knownIndexes.has(compositeKey)) {
|
|
89
|
+
this.knownIndexes.add(compositeKey);
|
|
90
|
+
try {
|
|
91
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS ${(0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${lower}_del`)} ON ${safeTable} (${(0, sql_js_1.escapeIdentifier)(key)}, "deleted");`);
|
|
123
92
|
}
|
|
93
|
+
catch { }
|
|
124
94
|
}
|
|
125
95
|
}
|
|
126
96
|
}
|
|
127
|
-
catch {
|
|
128
|
-
// Ignore
|
|
129
|
-
}
|
|
130
97
|
}
|
|
131
98
|
/**
|
|
132
99
|
* Ensures that the target table exists in SQLite with the specified primary key.
|
|
133
100
|
*/
|
|
134
101
|
async ensureTable(db, tableName, primaryKey = 'id') {
|
|
135
|
-
if (this.knownTables.has(tableName))
|
|
102
|
+
if (this.knownTables.has(tableName))
|
|
136
103
|
return;
|
|
137
|
-
}
|
|
138
104
|
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
139
105
|
const safePk = (0, sql_js_1.escapeIdentifier)(primaryKey);
|
|
140
106
|
await db.execute(`CREATE TABLE IF NOT EXISTS ${safeTable} (${safePk} TEXT PRIMARY KEY);`);
|
|
@@ -147,102 +113,46 @@ class SchemaManager {
|
|
|
147
113
|
*/
|
|
148
114
|
async ensureColumns(db, tableName, payload, primaryKey = 'id') {
|
|
149
115
|
await this.ensureTable(db, tableName, primaryKey);
|
|
150
|
-
if (!payload || typeof payload !== 'object')
|
|
116
|
+
if (!payload || typeof payload !== 'object')
|
|
151
117
|
return {};
|
|
152
|
-
}
|
|
153
118
|
const keys = Object.keys(payload);
|
|
154
|
-
if (keys.length === 0)
|
|
119
|
+
if (keys.length === 0)
|
|
155
120
|
return {};
|
|
156
|
-
}
|
|
157
121
|
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
158
122
|
let cacheMiss = false;
|
|
159
123
|
for (const key of keys) {
|
|
160
|
-
if (key
|
|
161
|
-
continue;
|
|
162
|
-
const cacheKey = `${tableName}:${key.toLowerCase()}`;
|
|
163
|
-
if (!this.knownColumns.has(cacheKey)) {
|
|
124
|
+
if (key !== primaryKey && !this.knownColumns.has(`${tableName}:${key.toLowerCase()}`)) {
|
|
164
125
|
cacheMiss = true;
|
|
165
126
|
break;
|
|
166
127
|
}
|
|
167
128
|
}
|
|
168
129
|
if (cacheMiss) {
|
|
169
|
-
// Query PRAGMA table_info to discover existing columns from SQLite
|
|
170
130
|
try {
|
|
171
131
|
const tableInfo = await db.execute(`PRAGMA table_info(${safeTable});`);
|
|
172
132
|
if (Array.isArray(tableInfo)) {
|
|
173
133
|
for (const col of tableInfo) {
|
|
174
|
-
if (col
|
|
134
|
+
if (col?.name) {
|
|
175
135
|
this.knownColumns.add(`${tableName}:${String(col.name).toLowerCase()}`);
|
|
176
136
|
}
|
|
177
137
|
}
|
|
178
138
|
}
|
|
179
139
|
}
|
|
180
|
-
catch
|
|
181
|
-
// Fallback for mocks or custom drivers where PRAGMA table_info might not be supported
|
|
182
|
-
}
|
|
183
|
-
// Check for columns that still do not exist, and alter table
|
|
140
|
+
catch { }
|
|
184
141
|
for (const key of keys) {
|
|
185
142
|
if (key === primaryKey)
|
|
186
143
|
continue;
|
|
187
144
|
const cacheKey = `${tableName}:${key.toLowerCase()}`;
|
|
188
145
|
if (!this.knownColumns.has(cacheKey)) {
|
|
189
146
|
const detectedType = detectSQLiteType(payload[key]);
|
|
190
|
-
const safeCol = (0, sql_js_1.escapeIdentifier)(key);
|
|
191
147
|
try {
|
|
192
|
-
await db.execute(`ALTER TABLE ${safeTable} ADD COLUMN ${
|
|
193
|
-
}
|
|
194
|
-
catch (err) {
|
|
195
|
-
// Ignore if column already exists (e.g. concurrent race)
|
|
196
|
-
const msg = String(err?.message || err || '');
|
|
197
|
-
if (!msg.toLowerCase().includes('duplicate column')) {
|
|
198
|
-
// Ignore or rethrow
|
|
199
|
-
}
|
|
148
|
+
await db.execute(`ALTER TABLE ${safeTable} ADD COLUMN ${(0, sql_js_1.escapeIdentifier)(key)} ${detectedType};`);
|
|
200
149
|
}
|
|
150
|
+
catch { }
|
|
201
151
|
this.knownColumns.add(cacheKey);
|
|
202
152
|
}
|
|
203
|
-
// Auto-index foreign keys and common query columns
|
|
204
|
-
const lowerKey = key.toLowerCase();
|
|
205
|
-
if (lowerKey.endsWith('_id') ||
|
|
206
|
-
lowerKey === 'deleted' ||
|
|
207
|
-
lowerKey === 'created_at' ||
|
|
208
|
-
lowerKey === 'updated_at') {
|
|
209
|
-
const indexKey = `${tableName}:${lowerKey}`;
|
|
210
|
-
if (!this.knownIndexes.has(indexKey)) {
|
|
211
|
-
this.knownIndexes.add(indexKey);
|
|
212
|
-
try {
|
|
213
|
-
const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${key}`);
|
|
214
|
-
const safeCol = (0, sql_js_1.escapeIdentifier)(key);
|
|
215
|
-
await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeCol});`);
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
// Ignore if indexes are not supported by adapter or already exist
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
// Auto-create high performance composite index for foreign keys with soft-deletion
|
|
225
|
-
if (this.knownColumns.has(`${tableName}:deleted`)) {
|
|
226
|
-
for (const key of keys) {
|
|
227
|
-
const lowerKey = key.toLowerCase();
|
|
228
|
-
if (lowerKey.endsWith('_id') && lowerKey !== 'id') {
|
|
229
|
-
const compositeKey = `${tableName}:${lowerKey}_deleted`;
|
|
230
|
-
if (!this.knownIndexes.has(compositeKey)) {
|
|
231
|
-
this.knownIndexes.add(compositeKey);
|
|
232
|
-
try {
|
|
233
|
-
const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${lowerKey}_del`);
|
|
234
|
-
const safeFk = (0, sql_js_1.escapeIdentifier)(key);
|
|
235
|
-
const safeDel = (0, sql_js_1.escapeIdentifier)('deleted');
|
|
236
|
-
await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeFk}, ${safeDel});`);
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
// Ignore
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
153
|
}
|
|
154
|
+
await this.autoIndexColumns(db, tableName, safeTable, keys);
|
|
244
155
|
}
|
|
245
|
-
// Format all values for SQLite storage
|
|
246
156
|
const formatted = {};
|
|
247
157
|
for (const key of keys) {
|
|
248
158
|
formatted[key] = formatValueForStorage(payload[key]);
|
package/dist/core/sync-worker.js
CHANGED
|
@@ -611,9 +611,12 @@ class SyncWorker {
|
|
|
611
611
|
const isLastChunk = i + DELTA_CHUNK_SIZE >= totalChanges;
|
|
612
612
|
const queuedEvents = [];
|
|
613
613
|
await this.db.transaction(async (tx) => {
|
|
614
|
+
// Pre-fetch active pending outbox records in 1 query for this chunk to eliminate N+1 bridge roundtrips
|
|
615
|
+
const pendingOutboxMap = await outbox_store_js_1.OutboxStore.getActivePendingMap(tx);
|
|
614
616
|
for (const change of chunk) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
+
const outboxKey = `${change.tableName}:${change.rowId}`;
|
|
618
|
+
// 1. Check in-memory map for pending local mutations for this (table_name, row_id)
|
|
619
|
+
const localPendingRecord = pendingOutboxMap.get(outboxKey);
|
|
617
620
|
// 2. Conflict resolution if there is a pending local change
|
|
618
621
|
if (localPendingRecord) {
|
|
619
622
|
// Only load the current local row when a resolver actually needs it.
|
|
@@ -640,11 +643,13 @@ class SyncWorker {
|
|
|
640
643
|
if (resolution === 'apply-remote') {
|
|
641
644
|
// Remote change wins: remove the superseded local pending outbox mutation
|
|
642
645
|
await outbox_store_js_1.OutboxStore.deleteRecord(tx, localPendingRecord.id);
|
|
646
|
+
pendingOutboxMap.delete(outboxKey);
|
|
643
647
|
}
|
|
644
648
|
else if (typeof resolution === 'object' && resolution.mergePayload) {
|
|
645
649
|
// Merge scenario
|
|
646
650
|
change.payload = resolution.mergePayload;
|
|
647
651
|
await outbox_store_js_1.OutboxStore.updatePayload(tx, localPendingRecord.id, resolution.mergePayload);
|
|
652
|
+
localPendingRecord.payload = resolution.mergePayload;
|
|
648
653
|
}
|
|
649
654
|
}
|
|
650
655
|
// 3. Apply remote change to local database
|