fix IOS sqlite return metadata as first row
This commit is contained in:
@@ -76,15 +76,20 @@ export const useChores = (includeArchive = false) => {
|
||||
refetchOnWindowFocus: true,
|
||||
queryFn: async () => {
|
||||
if (isOfflineFeatureEnabled()) {
|
||||
// Sync from server first (no-op if already syncing or offline)
|
||||
if (networkManager.isOnline) {
|
||||
await syncEngine.sync()
|
||||
}
|
||||
const cursor = await offlineDB.getSyncCursor()
|
||||
if (cursor > 0) {
|
||||
const cached = await offlineDB.getChores(includeArchive)
|
||||
const merged = await mergePendingCreates(cached || [])
|
||||
return { res: merged }
|
||||
try {
|
||||
// Sync from server first (no-op if already syncing or offline)
|
||||
if (networkManager.isOnline) {
|
||||
await syncEngine.sync()
|
||||
}
|
||||
const cursor = await offlineDB.getSyncCursor()
|
||||
if (cursor > 0) {
|
||||
const cached = await offlineDB.getChores(includeArchive)
|
||||
const merged = await mergePendingCreates(cached || [])
|
||||
return { res: merged }
|
||||
}
|
||||
} catch (err) {
|
||||
// A broken cache must not brick the app — fall through to the API
|
||||
console.error('Offline cache read failed, falling back to API', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,17 @@ export const CommandType = {
|
||||
UNARCHIVE_CHORE: 'unarchive_chore',
|
||||
}
|
||||
|
||||
// Parse a stored command payload; a corrupt payload must not take down every
|
||||
// consumer of the queue, so parse failures surface as null payloads.
|
||||
const parsePayload = command => {
|
||||
try {
|
||||
return { ...command, payload: JSON.parse(command.payload) }
|
||||
} catch {
|
||||
console.warn('Skipping corrupt command payload', command.id)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
class CommandQueue {
|
||||
_sanitizeCreatePayload(payload = {}) {
|
||||
const sanitized = { ...payload }
|
||||
@@ -77,7 +88,8 @@ class CommandQueue {
|
||||
const commands = await offlineDB.getCommands()
|
||||
return commands
|
||||
.filter(c => c.status === 'pending' || c.status === 'syncing')
|
||||
.map(c => ({ ...c, payload: JSON.parse(c.payload) }))
|
||||
.map(parsePayload)
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
// Get all failed commands
|
||||
@@ -86,7 +98,8 @@ class CommandQueue {
|
||||
const commands = await offlineDB.getCommands()
|
||||
return commands
|
||||
.filter(c => c.status === 'failed')
|
||||
.map(c => ({ ...c, payload: JSON.parse(c.payload) }))
|
||||
.map(parsePayload)
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
// Get pending commands for a specific entity (for undo/UI)
|
||||
@@ -103,7 +116,8 @@ class CommandQueue {
|
||||
.sort((a, b) => a.createdAt - b.createdAt)
|
||||
return commands
|
||||
.filter(c => c.status === 'pending' || c.status === 'syncing')
|
||||
.map(c => ({ ...c, payload: JSON.parse(c.payload) }))
|
||||
.map(parsePayload)
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
// Cancel/undo a pending command
|
||||
|
||||
@@ -20,6 +20,29 @@ const isNative = () => {
|
||||
return _isNative
|
||||
}
|
||||
|
||||
// The raw CapacitorSQLite query API prepends a metadata row on iOS
|
||||
// (e.g. { ios_columns: ["data"] }). The plugin's SQLiteDBConnection wrapper
|
||||
// strips it, but we call the plugin directly, so filter it here — otherwise
|
||||
// the metadata row reaches JSON.parse(undefined) and crashes offline reads.
|
||||
const queryRows = result =>
|
||||
(result?.values || []).filter(
|
||||
row => row && typeof row === 'object' && !('ios_columns' in row),
|
||||
)
|
||||
|
||||
// Parse a JSON column across rows, skipping corrupt rows instead of letting
|
||||
// one bad row take down the whole offline cache.
|
||||
const parseJsonRows = (result, column) => {
|
||||
const parsed = []
|
||||
for (const row of queryRows(result)) {
|
||||
try {
|
||||
parsed.push(JSON.parse(row[column]))
|
||||
} catch (err) {
|
||||
console.warn('Skipping corrupt offline cache row', err)
|
||||
}
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
// ── SQLite backend (iOS/Android) ──
|
||||
|
||||
class SQLiteBackend {
|
||||
@@ -125,7 +148,7 @@ class SQLiteBackend {
|
||||
statement: 'SELECT data FROM cached_chores',
|
||||
values: [],
|
||||
})
|
||||
const chores = (result.values || []).map(row => JSON.parse(row.data))
|
||||
const chores = parseJsonRows(result, 'data')
|
||||
if (includeArchive) {
|
||||
return chores
|
||||
}
|
||||
@@ -139,10 +162,7 @@ class SQLiteBackend {
|
||||
statement: 'SELECT data FROM cached_chores WHERE id = ?',
|
||||
values: [isNaN(numericId) ? id : numericId],
|
||||
})
|
||||
if (result.values && result.values.length > 0) {
|
||||
return JSON.parse(result.values[0].data)
|
||||
}
|
||||
return null
|
||||
return parseJsonRows(result, 'data')[0] ?? null
|
||||
}
|
||||
|
||||
async deleteChores(ids) {
|
||||
@@ -216,7 +236,7 @@ class SQLiteBackend {
|
||||
'SELECT data FROM cached_history WHERE chore_id = ? ORDER BY performed_at DESC',
|
||||
values: [Number(choreId)],
|
||||
})
|
||||
return (result.values || []).map(row => JSON.parse(row.data))
|
||||
return parseJsonRows(result, 'data')
|
||||
}
|
||||
|
||||
async getHistoryByDays(days) {
|
||||
@@ -229,7 +249,7 @@ class SQLiteBackend {
|
||||
: 'SELECT data FROM cached_history WHERE performed_at >= ? ORDER BY performed_at DESC',
|
||||
values: since === 0 ? [] : [since],
|
||||
})
|
||||
return (result.values || []).map(row => JSON.parse(row.data))
|
||||
return parseJsonRows(result, 'data')
|
||||
}
|
||||
|
||||
async deleteHistory(ids) {
|
||||
@@ -248,10 +268,16 @@ class SQLiteBackend {
|
||||
values: [historyId],
|
||||
})
|
||||
|
||||
if (!existing.values?.length) return
|
||||
const existingRows = queryRows(existing)
|
||||
if (!existingRows.length) return
|
||||
|
||||
const row = existing.values[0]
|
||||
const current = JSON.parse(row.data)
|
||||
const row = existingRows[0]
|
||||
let current
|
||||
try {
|
||||
current = JSON.parse(row.data)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const merged = {
|
||||
...current,
|
||||
...updates,
|
||||
@@ -314,7 +340,7 @@ class SQLiteBackend {
|
||||
statement: 'SELECT * FROM command_queue ORDER BY created_at ASC',
|
||||
values: [],
|
||||
})
|
||||
return (result.values || []).map(row => ({
|
||||
return queryRows(result).map(row => ({
|
||||
id: row.id,
|
||||
commandType: row.command_type,
|
||||
entityId: row.entity_id,
|
||||
@@ -332,7 +358,7 @@ class SQLiteBackend {
|
||||
'SELECT * FROM command_queue WHERE entity_id = ? ORDER BY created_at ASC',
|
||||
values: [entityId],
|
||||
})
|
||||
return (result.values || []).map(row => ({
|
||||
return queryRows(result).map(row => ({
|
||||
id: row.id,
|
||||
commandType: row.command_type,
|
||||
entityId: row.entity_id,
|
||||
@@ -358,9 +384,10 @@ class SQLiteBackend {
|
||||
values: [id],
|
||||
})
|
||||
|
||||
if (!result.values?.length) return
|
||||
const resultRows = queryRows(result)
|
||||
if (!resultRows.length) return
|
||||
|
||||
const row = result.values[0]
|
||||
const row = resultRows[0]
|
||||
await CapacitorSQLite.run({
|
||||
database: DB_NAME,
|
||||
statement: `UPDATE command_queue
|
||||
@@ -403,8 +430,10 @@ class SQLiteBackend {
|
||||
statement: "SELECT value FROM sync_meta WHERE key = 'sync_cursor'",
|
||||
values: [],
|
||||
})
|
||||
if (result.values && result.values.length > 0) {
|
||||
return Number(result.values[0].value)
|
||||
const rows = queryRows(result)
|
||||
if (rows.length > 0) {
|
||||
const cursor = Number(rows[0].value)
|
||||
return Number.isFinite(cursor) ? cursor : 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -424,8 +453,10 @@ class SQLiteBackend {
|
||||
statement: "SELECT value FROM sync_meta WHERE key = 'last_sync_time'",
|
||||
values: [],
|
||||
})
|
||||
if (result.values && result.values.length > 0) {
|
||||
return Number(result.values[0].value)
|
||||
const rows = queryRows(result)
|
||||
if (rows.length > 0) {
|
||||
const time = Number(rows[0].value)
|
||||
return Number.isFinite(time) ? time : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -453,9 +484,10 @@ class SQLiteBackend {
|
||||
statement: 'SELECT value FROM sync_meta WHERE key = ?',
|
||||
values: [key],
|
||||
})
|
||||
if (result.values && result.values.length > 0) {
|
||||
const rows = queryRows(result)
|
||||
if (rows.length > 0) {
|
||||
try {
|
||||
return JSON.parse(result.values[0].value)
|
||||
return JSON.parse(rows[0].value)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user