@syncular/client 0.30.10 → 0.30.12
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/README.md +15 -0
- package/dist/apply.d.ts +5 -1
- package/dist/apply.js +58 -58
- package/dist/blob.js +13 -39
- package/dist/client.js +178 -161
- package/dist/failed-overlay.d.ts +10 -5
- package/dist/failed-overlay.js +118 -36
- package/dist/outbox.d.ts +2 -1
- package/dist/outbox.js +20 -40
- package/dist/outcomes.d.ts +8 -0
- package/dist/outcomes.js +22 -30
- package/dist/previous-version.js +14 -28
- package/dist/schema.d.ts +3 -0
- package/dist/schema.js +89 -133
- package/dist/state.d.ts +6 -9
- package/dist/state.js +66 -32
- package/dist/wasm-database.js +40 -24
- package/dist/worker-host.js +37 -11
- package/package.json +3 -3
- package/src/apply.ts +95 -72
- package/src/blob.ts +17 -39
- package/src/client.ts +312 -214
- package/src/failed-overlay.ts +197 -48
- package/src/outbox.ts +25 -44
- package/src/outcomes.ts +40 -35
- package/src/previous-version.ts +14 -28
- package/src/schema.ts +154 -166
- package/src/state.ts +90 -33
- package/src/wasm-database.ts +56 -29
- package/src/worker-host.ts +34 -12
package/dist/failed-overlay.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
export declare const RETAINED_ROWS = "_syncular_failed_rows";
|
|
1
2
|
import type { ClientDatabase } from './database.js';
|
|
2
|
-
import type { CompiledClientSchema, JsonRowValue } from './schema.js';
|
|
3
|
+
import type { CompiledClientSchema, CompiledClientTable, JsonRowValue } from './schema.js';
|
|
4
|
+
import type { RowValue } from '@syncular/core';
|
|
5
|
+
import type { RetainedCommitRow } from './outcomes.js';
|
|
3
6
|
import type { OutboxCommit, OutboxBeforeImage } from './outbox.js';
|
|
4
7
|
/** Protected failure state. The server base advances independently of intent. */
|
|
5
8
|
export declare function ensureFailedOverlaySchema(db: ClientDatabase): void;
|
|
6
|
-
export declare function retainedBaseWrite(db: ClientDatabase, table:
|
|
7
|
-
export declare function retainFailedRows(db: ClientDatabase, commit: OutboxCommit, images: readonly OutboxBeforeImage[]): void;
|
|
8
|
-
|
|
9
|
-
export declare function
|
|
9
|
+
export declare function retainedBaseWrite(db: ClientDatabase, table: CompiledClientTable, rowId: string, values?: Readonly<Record<string, JsonRowValue>>, version?: number): void;
|
|
10
|
+
export declare function retainFailedRows(db: ClientDatabase, commit: OutboxCommit, images: readonly OutboxBeforeImage[], commitSeq?: number): void;
|
|
11
|
+
/** Match declared keys through SQLite, preserving index affinity and NULL semantics. */
|
|
12
|
+
export declare function uniqueConflicts(db: ClientDatabase, table: CompiledClientTable, values: Readonly<Record<string, RowValue>>): NonNullable<RetainedCommitRow['uniqueConflicts']>;
|
|
13
|
+
export declare function restoreFailedBases(db: ClientDatabase, schema: CompiledClientSchema, absentIntent?: boolean, rowKeys?: readonly string[]): boolean;
|
|
14
|
+
export declare function failedOverlayCommits(db: ClientDatabase, acknowledged?: boolean, rowKeys?: readonly string[]): OutboxCommit[];
|
|
10
15
|
export declare function dropFailedRows(db: ClientDatabase, matches: (table: string, values: Readonly<Record<string, JsonRowValue>>) => boolean): OutboxCommit[];
|
package/dist/failed-overlay.js
CHANGED
|
@@ -1,32 +1,59 @@
|
|
|
1
|
+
export const RETAINED_ROWS = '_syncular_failed_rows';
|
|
1
2
|
import { ClientSyncError } from './errors.js';
|
|
2
|
-
import { jsonToRowValue, quoteIdent } from './schema.js';
|
|
3
|
+
import { mapRowValues, fromSqlValue, jsonToRowValue, quoteIdent, rowValueToJson, SYNC_VERSION_COLUMN, toSqlValue, } from './schema.js';
|
|
3
4
|
import { deleteLocalRow, upsertLocalRow } from './apply.js';
|
|
4
5
|
/** Protected failure state. The server base advances independently of intent. */
|
|
5
6
|
export function ensureFailedOverlaySchema(db) {
|
|
6
|
-
db.exec(`CREATE TABLE IF NOT EXISTS
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ${RETAINED_ROWS}(commit_id TEXT NOT NULL,idx INTEGER NOT NULL,tbl TEXT NOT NULL,id TEXT NOT NULL,at INTEGER NOT NULL,op TEXT NOT NULL,intent TEXT,base TEXT,version INTEGER,unique_conflicts TEXT,commit_seq INTEGER,PRIMARY KEY(commit_id,idx))`);
|
|
8
|
+
const columns = db.query(`PRAGMA table_info(${RETAINED_ROWS})`);
|
|
9
|
+
for (const column of ['unique_conflicts TEXT', 'commit_seq INTEGER'])
|
|
10
|
+
if (!columns.some((row) => row.name === column.split(' ')[0]))
|
|
11
|
+
db.exec(`ALTER TABLE ${RETAINED_ROWS} ADD COLUMN ${column}`);
|
|
11
12
|
}
|
|
12
13
|
export function retainedBaseWrite(db, table, rowId, values, version) {
|
|
13
|
-
db.exec(
|
|
14
|
+
db.exec(`UPDATE ${RETAINED_ROWS} SET base=?,version=? WHERE tbl=? AND id=?`, [
|
|
14
15
|
values === undefined ? null : JSON.stringify(values),
|
|
15
16
|
version ?? null,
|
|
16
|
-
table,
|
|
17
|
+
table.name,
|
|
17
18
|
rowId,
|
|
18
19
|
]);
|
|
20
|
+
if (!table.indexes.some((index) => index.unique))
|
|
21
|
+
return;
|
|
22
|
+
for (const retained of db.query(`SELECT commit_id,idx,intent,unique_conflicts FROM ${RETAINED_ROWS} WHERE tbl=? AND commit_seq IS NULL`, [table.name])) {
|
|
23
|
+
const prior = retained.unique_conflicts === null
|
|
24
|
+
? []
|
|
25
|
+
: JSON.parse(retained.unique_conflicts);
|
|
26
|
+
const conflicts = prior.filter((conflict) => conflict.rowId !== rowId);
|
|
27
|
+
if (values !== undefined && retained.intent !== null) {
|
|
28
|
+
const intent = JSON.parse(retained.intent);
|
|
29
|
+
conflicts.push(...uniqueConflicts(db, table, mapRowValues(intent, jsonToRowValue))
|
|
30
|
+
.filter((conflict) => conflict.rowId === rowId)
|
|
31
|
+
.map((conflict) => ({
|
|
32
|
+
...conflict,
|
|
33
|
+
serverRow: mapRowValues(conflict.serverRow, rowValueToJson),
|
|
34
|
+
})));
|
|
35
|
+
}
|
|
36
|
+
db.exec(`UPDATE ${RETAINED_ROWS} SET unique_conflicts=? WHERE commit_id=? AND idx=?`, [
|
|
37
|
+
conflicts.length
|
|
38
|
+
? JSON.stringify(conflicts.sort((a, b) => table.indexes.findIndex((index) => index.name === a.index) -
|
|
39
|
+
table.indexes.findIndex((index) => index.name === b.index)))
|
|
40
|
+
: null,
|
|
41
|
+
retained.commit_id,
|
|
42
|
+
Number(retained.idx),
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
19
45
|
}
|
|
20
|
-
export function retainFailedRows(db, commit, images) {
|
|
46
|
+
export function retainFailedRows(db, commit, images, commitSeq) {
|
|
21
47
|
for (const [index, operation] of commit.operations.entries()) {
|
|
22
48
|
const image = images.find((image) => image.opIndex === index);
|
|
49
|
+
if (commitSeq !== undefined && (image?.deliverySeq ?? -1) >= commitSeq)
|
|
50
|
+
continue;
|
|
23
51
|
const initial = operation.op === 'upsert'
|
|
24
52
|
? { ...image?.values, ...operation.values }
|
|
25
53
|
: undefined;
|
|
26
54
|
if (!image)
|
|
27
|
-
throw new ClientSyncError('sync.local_corrupt', '
|
|
28
|
-
db.exec(`INSERT INTO
|
|
29
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
55
|
+
throw new ClientSyncError('sync.local_corrupt', 'missing intent before-image');
|
|
56
|
+
db.exec(`INSERT INTO ${RETAINED_ROWS}(commit_id,idx,tbl,id,at,op,intent,base,version,commit_seq)VALUES(?,?,?,?,?,?,?,?,?,?)`, [
|
|
30
57
|
commit.clientCommitId,
|
|
31
58
|
index,
|
|
32
59
|
operation.table,
|
|
@@ -36,42 +63,91 @@ export function retainFailedRows(db, commit, images) {
|
|
|
36
63
|
initial ? JSON.stringify(initial) : null,
|
|
37
64
|
image.values ? JSON.stringify(image.values) : null,
|
|
38
65
|
image.syncVersion ?? null,
|
|
66
|
+
commitSeq ?? null,
|
|
39
67
|
]);
|
|
40
68
|
}
|
|
41
69
|
}
|
|
42
|
-
|
|
70
|
+
/** Match declared keys through SQLite, preserving index affinity and NULL semantics. */
|
|
71
|
+
export function uniqueConflicts(db, table, values) {
|
|
72
|
+
return table.indexes
|
|
73
|
+
.filter((index) => index.unique)
|
|
74
|
+
.flatMap((index) => db
|
|
75
|
+
.query(`SELECT * FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)} !=? AND ${index.columns.map((column) => `${quoteIdent(column)} = ?`).join(' AND ')}`, [
|
|
76
|
+
toSqlValue(values[table.primaryKey] ?? null),
|
|
77
|
+
...index.columns.map((column) => toSqlValue(values[column] ?? null)),
|
|
78
|
+
])
|
|
79
|
+
.map((row) => ({
|
|
80
|
+
index: index.name,
|
|
81
|
+
columns: index.columns,
|
|
82
|
+
rowId: String(row[table.primaryKey]),
|
|
83
|
+
serverRow: Object.fromEntries(table.columns.map((column) => [
|
|
84
|
+
column.name,
|
|
85
|
+
fromSqlValue(column, row[column.name] ?? null),
|
|
86
|
+
])),
|
|
87
|
+
serverVersion: Number(row[SYNC_VERSION_COLUMN]),
|
|
88
|
+
})));
|
|
89
|
+
}
|
|
90
|
+
function retainedRows(db, rowKeys) {
|
|
91
|
+
return db.query(`SELECT * FROM ${RETAINED_ROWS} WHERE ?1 IS NULL OR json_array(tbl,id) IN(SELECT value FROM json_each(?1)) ORDER BY commit_seq,at,commit_id,idx`, [rowKeys ? JSON.stringify(rowKeys) : null]);
|
|
92
|
+
}
|
|
93
|
+
export function restoreFailedBases(db, schema, absentIntent = false, rowKeys) {
|
|
43
94
|
const seen = new Set();
|
|
44
|
-
for (const row of db
|
|
95
|
+
for (const row of retainedRows(db, rowKeys)) {
|
|
45
96
|
const key = JSON.stringify([row.tbl, row.id]);
|
|
46
97
|
if (!absentIntent && seen.has(key))
|
|
47
98
|
continue;
|
|
48
99
|
seen.add(key);
|
|
49
|
-
const table = schema.tables.get(
|
|
100
|
+
const table = schema.tables.get(row.tbl);
|
|
50
101
|
if (!table)
|
|
51
|
-
throw new ClientSyncError('sync.unknown_table', 'retained
|
|
102
|
+
throw new ClientSyncError('sync.unknown_table', 'retained table is unknown');
|
|
52
103
|
const json = absentIntent ? row.intent : row.base;
|
|
104
|
+
if (absentIntent && row.intent !== null) {
|
|
105
|
+
const values = JSON.parse(row.intent);
|
|
106
|
+
const conflicts = uniqueConflicts(db, table, mapRowValues(values, jsonToRowValue));
|
|
107
|
+
// Hydrate a pre-0.30.11 failure after its winner was already imported.
|
|
108
|
+
// Pending and retained overlays cannot supply authoritative row evidence.
|
|
109
|
+
if (row.commit_seq === null && row.unique_conflicts === null) {
|
|
110
|
+
const authorized = conflicts.filter((conflict) => conflict.serverVersion >= 0 &&
|
|
111
|
+
!db.query(`SELECT 1 FROM ${RETAINED_ROWS} WHERE tbl=? AND id=? UNION ALL SELECT 1 FROM _syncular_outbox,json_each(operations)WHERE json_extract(json_each.value,'$.table')=? AND json_extract(json_each.value,'$.rowId')=? LIMIT 1`, [table.name, conflict.rowId, table.name, conflict.rowId]).length);
|
|
112
|
+
if (authorized.length)
|
|
113
|
+
db.exec(`UPDATE ${RETAINED_ROWS} SET unique_conflicts=? WHERE commit_id=? AND idx=?`, [
|
|
114
|
+
JSON.stringify(authorized.map((conflict) => ({
|
|
115
|
+
...conflict,
|
|
116
|
+
serverRow: mapRowValues(conflict.serverRow, rowValueToJson),
|
|
117
|
+
}))),
|
|
118
|
+
row.commit_id,
|
|
119
|
+
row.idx,
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
if (conflicts.length)
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
53
125
|
if (absentIntent &&
|
|
54
126
|
(json === null ||
|
|
55
|
-
db.query(`SELECT 1 FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}
|
|
127
|
+
db.query(`SELECT 1 FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}=?`, [row.id]).length))
|
|
56
128
|
continue;
|
|
57
129
|
if (json === null)
|
|
58
|
-
deleteLocalRow(db, table,
|
|
130
|
+
deleteLocalRow(db, table, row.id, false);
|
|
59
131
|
else {
|
|
60
132
|
const values = JSON.parse(String(json));
|
|
61
133
|
if (absentIntent) {
|
|
62
|
-
const operation = JSON.parse(
|
|
63
|
-
if (
|
|
134
|
+
const operation = JSON.parse(row.op);
|
|
135
|
+
if (row.commit_seq === null &&
|
|
136
|
+
!table.columns.every((column) => Object.hasOwn(operation.values ?? {}, column.name)))
|
|
64
137
|
continue;
|
|
65
138
|
}
|
|
66
|
-
upsertLocalRow(db, table, table.columns.map((column) => jsonToRowValue(values[column.name] ?? null)),
|
|
139
|
+
upsertLocalRow(db, table, table.columns.map((column) => jsonToRowValue(values[column.name] ?? null)), (row.version ?? -1), false);
|
|
67
140
|
}
|
|
68
141
|
}
|
|
142
|
+
return seen.size > 0;
|
|
69
143
|
}
|
|
70
|
-
export function failedOverlayCommits(db) {
|
|
144
|
+
export function failedOverlayCommits(db, acknowledged = false, rowKeys) {
|
|
71
145
|
const commits = new Map();
|
|
72
|
-
for (const row of db
|
|
73
|
-
|
|
74
|
-
|
|
146
|
+
for (const row of retainedRows(db, rowKeys)) {
|
|
147
|
+
if ((row.commit_seq !== null) !== acknowledged)
|
|
148
|
+
continue;
|
|
149
|
+
const id = row.commit_id;
|
|
150
|
+
const operation = JSON.parse(row.op);
|
|
75
151
|
const prior = commits.get(id);
|
|
76
152
|
if (prior)
|
|
77
153
|
prior.operations.push(operation);
|
|
@@ -79,7 +155,7 @@ export function failedOverlayCommits(db) {
|
|
|
79
155
|
commits.set(id, {
|
|
80
156
|
seq: 0,
|
|
81
157
|
clientCommitId: id,
|
|
82
|
-
createdAtMs:
|
|
158
|
+
createdAtMs: row.at,
|
|
83
159
|
operations: [operation],
|
|
84
160
|
});
|
|
85
161
|
}
|
|
@@ -87,16 +163,22 @@ export function failedOverlayCommits(db) {
|
|
|
87
163
|
}
|
|
88
164
|
export function dropFailedRows(db, matches) {
|
|
89
165
|
const ids = new Set();
|
|
90
|
-
for (const row of db
|
|
91
|
-
const json
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
166
|
+
for (const row of retainedRows(db)) {
|
|
167
|
+
for (const json of [row.intent, row.base]) {
|
|
168
|
+
if (json === null)
|
|
169
|
+
continue;
|
|
170
|
+
const values = JSON.parse(String(json));
|
|
171
|
+
if (matches(row.tbl, values))
|
|
172
|
+
ids.add(row.commit_id);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const dropped = [
|
|
176
|
+
...failedOverlayCommits(db),
|
|
177
|
+
...failedOverlayCommits(db, true),
|
|
178
|
+
].filter((commit) => ids.has(commit.clientCommitId));
|
|
179
|
+
for (const id of ids) {
|
|
180
|
+
db.exec(`DELETE FROM ${RETAINED_ROWS} WHERE commit_id=?`, [id]);
|
|
181
|
+
db.exec("UPDATE _syncular_commit_outcomes SET operations=NULL,results='[]' WHERE client_commit_id=?", [id]);
|
|
97
182
|
}
|
|
98
|
-
const dropped = failedOverlayCommits(db).filter((commit) => ids.has(commit.clientCommitId));
|
|
99
|
-
for (const id of ids)
|
|
100
|
-
db.exec('DELETE FROM _syncular_failed_rows WHERE commit_id = ?', [id]);
|
|
101
183
|
return dropped;
|
|
102
184
|
}
|
package/dist/outbox.d.ts
CHANGED
|
@@ -34,12 +34,13 @@ export interface OutboxCommit {
|
|
|
34
34
|
export interface OutboxBeforeImage {
|
|
35
35
|
readonly opIndex: number;
|
|
36
36
|
readonly existed: boolean;
|
|
37
|
+
readonly deliverySeq?: number;
|
|
37
38
|
readonly syncVersion?: number;
|
|
38
39
|
readonly values?: Readonly<Record<string, JsonRowValue>>;
|
|
39
40
|
}
|
|
40
41
|
export declare function appendOutboxCommit(db: ClientDatabase, clientCommitId: string, operations: readonly OutboxOperation[], nowMs: number, beforeImages?: readonly OutboxBeforeImage[]): void;
|
|
41
42
|
/** Pending commits in FIFO creation order (§7.1). Full reads serve replay and the public listing. */
|
|
42
|
-
export declare function listOutbox(db: ClientDatabase): OutboxCommit[];
|
|
43
|
+
export declare function listOutbox(db: ClientDatabase, rowKeys?: readonly string[]): OutboxCommit[];
|
|
43
44
|
/** Keyset pages bound staging; laziness decodes only commits consumed by the encoder. */
|
|
44
45
|
export declare function iterateOutbox(db: ClientDatabase, throughSeq: number): Generator<OutboxCommit>;
|
|
45
46
|
/** Routine status reads never load operation bodies. */
|
package/dist/outbox.js
CHANGED
|
@@ -7,30 +7,20 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { encodeSparseRow, } from '@syncular/core';
|
|
9
9
|
import { ClientSyncError } from './errors.js';
|
|
10
|
-
import { jsonToRowValue, } from './schema.js';
|
|
10
|
+
import { jsonToRowValue, matchesLocalScopes, } from './schema.js';
|
|
11
11
|
export function appendOutboxCommit(db, clientCommitId, operations, nowMs, beforeImages = []) {
|
|
12
12
|
if (operations.length === 0) {
|
|
13
13
|
throw new ClientSyncError('sync.empty_commit', 'a local commit must carry at least one operation (§6.1)');
|
|
14
14
|
}
|
|
15
|
-
db.exec(
|
|
16
|
-
|
|
17
|
-
for (const image of beforeImages) {
|
|
18
|
-
db.exec(`INSERT INTO _syncular_outbox_before_images(
|
|
19
|
-
client_commit_id, op_index, existed, sync_version, values_json
|
|
20
|
-
) VALUES (?, ?, ?, ?, ?)`, [
|
|
21
|
-
clientCommitId,
|
|
22
|
-
image.opIndex,
|
|
23
|
-
image.existed ? 1 : 0,
|
|
24
|
-
image.syncVersion ?? null,
|
|
25
|
-
image.values === undefined ? null : JSON.stringify(image.values),
|
|
26
|
-
]);
|
|
27
|
-
}
|
|
15
|
+
db.exec('INSERT INTO _syncular_outbox(client_commit_id,created_at_ms,operations)VALUES(?,?,?)', [clientCommitId, nowMs, JSON.stringify(operations)]);
|
|
16
|
+
replaceOutboxBeforeImages(db, clientCommitId, beforeImages);
|
|
28
17
|
}
|
|
29
18
|
/** Pending commits in FIFO creation order (§7.1). Full reads serve replay and the public listing. */
|
|
30
|
-
export function listOutbox(db) {
|
|
19
|
+
export function listOutbox(db, rowKeys) {
|
|
31
20
|
return db
|
|
32
|
-
.query(
|
|
33
|
-
|
|
21
|
+
.query(rowKeys === undefined
|
|
22
|
+
? 'SELECT seq, client_commit_id, created_at_ms, operations FROM _syncular_outbox ORDER BY seq ASC'
|
|
23
|
+
: "SELECT * FROM _syncular_outbox WHERE EXISTS(SELECT 1 FROM json_each(operations) WHERE json_array(json_extract(value,'$.table'),json_extract(value,'$.rowId')) IN(SELECT value FROM json_each(?))) ORDER BY seq", rowKeys && [JSON.stringify(rowKeys)])
|
|
34
24
|
.map(decodeOutboxRow);
|
|
35
25
|
}
|
|
36
26
|
function decodeOutboxRow(row) {
|
|
@@ -45,8 +35,7 @@ function decodeOutboxRow(row) {
|
|
|
45
35
|
export function* iterateOutbox(db, throughSeq) {
|
|
46
36
|
let afterSeq = 0;
|
|
47
37
|
while (afterSeq < throughSeq) {
|
|
48
|
-
const rows = db.query(
|
|
49
|
-
WHERE seq > ? AND seq <= ? ORDER BY seq ASC LIMIT 32`, [afterSeq, throughSeq]);
|
|
38
|
+
const rows = db.query('SELECT seq, client_commit_id, created_at_ms, operations FROM _syncular_outbox WHERE seq > ? AND seq <= ? ORDER BY seq ASC LIMIT 32', [afterSeq, throughSeq]);
|
|
50
39
|
if (rows.length === 0)
|
|
51
40
|
return;
|
|
52
41
|
for (const row of rows) {
|
|
@@ -58,26 +47,27 @@ export function* iterateOutbox(db, throughSeq) {
|
|
|
58
47
|
}
|
|
59
48
|
/** Routine status reads never load operation bodies. */
|
|
60
49
|
export function countOutbox(db) {
|
|
61
|
-
return db.query('SELECT COUNT(*)
|
|
50
|
+
return db.query('SELECT COUNT(*)AS count FROM _syncular_outbox')[0]
|
|
62
51
|
.count;
|
|
63
52
|
}
|
|
64
53
|
export function deleteOutboxCommit(db, clientCommitId) {
|
|
65
|
-
db.exec('DELETE FROM _syncular_blob_commit_refs WHERE commit_id
|
|
54
|
+
db.exec('DELETE FROM _syncular_blob_commit_refs WHERE commit_id=?', [
|
|
66
55
|
clientCommitId,
|
|
67
56
|
]);
|
|
68
|
-
db.exec('DELETE FROM _syncular_outbox_before_images WHERE client_commit_id
|
|
69
|
-
db.exec('DELETE FROM _syncular_outbox WHERE client_commit_id
|
|
57
|
+
db.exec('DELETE FROM _syncular_outbox_before_images WHERE client_commit_id=?', [clientCommitId]);
|
|
58
|
+
db.exec('DELETE FROM _syncular_outbox WHERE client_commit_id=?', [
|
|
70
59
|
clientCommitId,
|
|
71
60
|
]);
|
|
72
61
|
}
|
|
73
62
|
export function listOutboxBeforeImages(db, clientCommitId) {
|
|
74
63
|
return db
|
|
75
|
-
.query(
|
|
76
|
-
FROM _syncular_outbox_before_images
|
|
77
|
-
WHERE client_commit_id = ? ORDER BY op_index`, [clientCommitId])
|
|
64
|
+
.query('SELECT op_index,existed,sync_version,values_json,delivery_seq FROM _syncular_outbox_before_images WHERE client_commit_id=? ORDER BY op_index', [clientCommitId])
|
|
78
65
|
.map((row) => ({
|
|
79
66
|
opIndex: row.op_index,
|
|
80
67
|
existed: row.existed === 1,
|
|
68
|
+
...(typeof row.delivery_seq === 'number'
|
|
69
|
+
? { deliverySeq: row.delivery_seq }
|
|
70
|
+
: {}),
|
|
81
71
|
...(typeof row.sync_version === 'number'
|
|
82
72
|
? { syncVersion: row.sync_version }
|
|
83
73
|
: {}),
|
|
@@ -90,11 +80,7 @@ export function listOutboxBeforeImages(db, clientCommitId) {
|
|
|
90
80
|
}
|
|
91
81
|
export function replaceOutboxBeforeImages(db, clientCommitId, replacements) {
|
|
92
82
|
for (const image of replacements) {
|
|
93
|
-
db.exec(
|
|
94
|
-
WHERE client_commit_id = ? AND op_index = ?`, [clientCommitId, image.opIndex]);
|
|
95
|
-
db.exec(`INSERT INTO _syncular_outbox_before_images(
|
|
96
|
-
client_commit_id, op_index, existed, sync_version, values_json
|
|
97
|
-
) VALUES (?, ?, ?, ?, ?)`, [
|
|
83
|
+
db.exec('INSERT INTO _syncular_outbox_before_images(client_commit_id,op_index,existed,sync_version,values_json)VALUES(?,?,?,?,?) ON CONFLICT(client_commit_id,op_index)DO UPDATE SET existed=excluded.existed,sync_version=excluded.sync_version,values_json=excluded.values_json', [
|
|
98
84
|
clientCommitId,
|
|
99
85
|
image.opIndex,
|
|
100
86
|
image.existed ? 1 : 0,
|
|
@@ -119,7 +105,7 @@ function sparseValues(table, values) {
|
|
|
119
105
|
// bump removed (or renamed) it — the commit cannot be expressed now.
|
|
120
106
|
for (const key of Object.keys(values)) {
|
|
121
107
|
if (!table.columnIndex.has(key)) {
|
|
122
|
-
throw new OutboxEncodeError(`outbox
|
|
108
|
+
throw new OutboxEncodeError(`outbox column ${JSON.stringify(key)} is absent from table ${JSON.stringify(table.name)} (§7.4.4)`);
|
|
123
109
|
}
|
|
124
110
|
}
|
|
125
111
|
return table.columns.map((column) => {
|
|
@@ -152,7 +138,7 @@ export async function encodeOutboxCommit(schema, commit, encryption, storedKeyId
|
|
|
152
138
|
const table = schema.tables.get(op.table);
|
|
153
139
|
if (table === undefined) {
|
|
154
140
|
// §7.4.4: the bump removed this table — the commit cannot be encoded.
|
|
155
|
-
throw new OutboxEncodeError(`outbox
|
|
141
|
+
throw new OutboxEncodeError(`outbox table ${JSON.stringify(op.table)} is absent from the current schema (§7.4.4)`);
|
|
156
142
|
}
|
|
157
143
|
if (op.values === undefined) {
|
|
158
144
|
throw new ClientSyncError('sync.invalid_request', `outbox upsert on ${op.table}/${op.rowId} has no values`);
|
|
@@ -193,13 +179,7 @@ export function dropOutboxCommitsInScope(db, table, effective) {
|
|
|
193
179
|
const inScope = commit.operations.some((op) => {
|
|
194
180
|
if (op.table !== table.name || op.values === undefined)
|
|
195
181
|
return false;
|
|
196
|
-
return
|
|
197
|
-
const column = table.scopeColumnByVariable.get(variable);
|
|
198
|
-
if (column === undefined)
|
|
199
|
-
return false;
|
|
200
|
-
const value = op.values?.[column];
|
|
201
|
-
return typeof value === 'string' && values.includes(value);
|
|
202
|
-
});
|
|
182
|
+
return matchesLocalScopes(table, effective, op.values);
|
|
203
183
|
});
|
|
204
184
|
if (inScope) {
|
|
205
185
|
deleteOutboxCommit(db, commit.clientCommitId);
|
package/dist/outcomes.d.ts
CHANGED
|
@@ -51,6 +51,14 @@ export type CommitOperationOutcome = {
|
|
|
51
51
|
readonly rejection: RejectionRecord;
|
|
52
52
|
};
|
|
53
53
|
export interface RetainedCommitRow {
|
|
54
|
+
/** Authorized different-primary-key server rows matching intended unique keys. */
|
|
55
|
+
readonly uniqueConflicts?: readonly {
|
|
56
|
+
readonly index: string;
|
|
57
|
+
readonly columns: readonly string[];
|
|
58
|
+
readonly rowId: string;
|
|
59
|
+
readonly serverRow: Readonly<Record<string, RowValue>>;
|
|
60
|
+
readonly serverVersion: number;
|
|
61
|
+
}[];
|
|
54
62
|
readonly table: string;
|
|
55
63
|
readonly rowId: string;
|
|
56
64
|
/** Complete intended row, or null for a local deletion. */
|
package/dist/outcomes.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { RETAINED_ROWS } from './failed-overlay.js';
|
|
1
2
|
import { ClientSyncError } from './errors.js';
|
|
2
|
-
import { jsonToRowValue, rowValueToJson } from './schema.js';
|
|
3
|
+
import { mapRowValues, jsonToRowValue, rowValueToJson, } from './schema.js';
|
|
3
4
|
function encodeResults(results) {
|
|
4
5
|
const stored = results.map((result) => {
|
|
5
6
|
if (result.status !== 'conflict')
|
|
@@ -8,10 +9,7 @@ function encodeResults(results) {
|
|
|
8
9
|
status: 'conflict',
|
|
9
10
|
conflict: {
|
|
10
11
|
...result.conflict,
|
|
11
|
-
serverRow:
|
|
12
|
-
key,
|
|
13
|
-
rowValueToJson(value),
|
|
14
|
-
])),
|
|
12
|
+
serverRow: mapRowValues(result.conflict.serverRow, rowValueToJson),
|
|
15
13
|
},
|
|
16
14
|
};
|
|
17
15
|
});
|
|
@@ -28,27 +26,32 @@ function decodeResults(raw) {
|
|
|
28
26
|
...result.conflict,
|
|
29
27
|
// Pre-column-version journal entries carry no conflictColumns.
|
|
30
28
|
conflictColumns: result.conflict.conflictColumns ?? [],
|
|
31
|
-
serverRow:
|
|
32
|
-
key,
|
|
33
|
-
jsonToRowValue(value),
|
|
34
|
-
])),
|
|
29
|
+
serverRow: mapRowValues(result.conflict.serverRow, jsonToRowValue),
|
|
35
30
|
},
|
|
36
31
|
};
|
|
37
32
|
});
|
|
38
33
|
}
|
|
39
34
|
function parseOutcome(db, row) {
|
|
40
35
|
const retainedRows = db
|
|
41
|
-
.query(
|
|
36
|
+
.query(`SELECT tbl,id,intent,base,version,unique_conflicts FROM ${RETAINED_ROWS} WHERE commit_seq IS NULL AND commit_id=? ORDER BY idx`, [row.client_commit_id])
|
|
42
37
|
.map((retained) => {
|
|
43
38
|
const decode = (value) => value === null
|
|
44
39
|
? null
|
|
45
40
|
: Object.fromEntries(Object.entries(JSON.parse(String(value))).map(([key, value]) => [key, jsonToRowValue(value)]));
|
|
46
41
|
return {
|
|
47
|
-
table:
|
|
48
|
-
rowId:
|
|
42
|
+
table: retained.tbl,
|
|
43
|
+
rowId: retained.id,
|
|
49
44
|
localRow: decode(retained.intent),
|
|
45
|
+
...(retained.unique_conflicts === null
|
|
46
|
+
? {}
|
|
47
|
+
: {
|
|
48
|
+
uniqueConflicts: JSON.parse(retained.unique_conflicts).map((conflict) => ({
|
|
49
|
+
...conflict,
|
|
50
|
+
serverRow: decode(JSON.stringify(conflict.serverRow)),
|
|
51
|
+
})),
|
|
52
|
+
}),
|
|
50
53
|
serverRow: decode(retained.base),
|
|
51
|
-
serverVersion: retained.version === null ? null :
|
|
54
|
+
serverVersion: retained.version === null ? null : retained.version,
|
|
52
55
|
};
|
|
53
56
|
});
|
|
54
57
|
return {
|
|
@@ -71,9 +74,7 @@ function parseOutcome(db, row) {
|
|
|
71
74
|
};
|
|
72
75
|
}
|
|
73
76
|
export function recordCommitOutcome(db, outcome) {
|
|
74
|
-
db.exec(
|
|
75
|
-
client_commit_id, status, recorded_at_ms, results, operations, resolution
|
|
76
|
-
) VALUES (?, ?, ?, ?, ?, 'active')`, [
|
|
77
|
+
db.exec("INSERT INTO _syncular_commit_outcomes(client_commit_id,status,recorded_at_ms,results,operations,resolution)VALUES(?,?,?,?,?,'active')", [
|
|
77
78
|
outcome.clientCommitId,
|
|
78
79
|
outcome.status,
|
|
79
80
|
outcome.recordedAtMs,
|
|
@@ -85,9 +86,7 @@ export function recordCommitOutcome(db, outcome) {
|
|
|
85
86
|
return commitOutcome(db, outcome.clientCommitId);
|
|
86
87
|
}
|
|
87
88
|
export function commitOutcome(db, clientCommitId) {
|
|
88
|
-
const row = db.query(
|
|
89
|
-
resolution, resolved_at_ms, replacement_client_commit_id
|
|
90
|
-
FROM _syncular_commit_outcomes WHERE client_commit_id = ?`, [clientCommitId])[0];
|
|
89
|
+
const row = db.query('SELECT seq,client_commit_id,status,recorded_at_ms,results,operations,resolution,resolved_at_ms,replacement_client_commit_id FROM _syncular_commit_outcomes WHERE client_commit_id=?', [clientCommitId])[0];
|
|
91
90
|
return row === undefined ? undefined : parseOutcome(db, row);
|
|
92
91
|
}
|
|
93
92
|
export function listCommitOutcomes(db, query = {}) {
|
|
@@ -98,16 +97,11 @@ export function listCommitOutcomes(db, query = {}) {
|
|
|
98
97
|
const where = query.activeOnly
|
|
99
98
|
? "WHERE resolution = 'active' AND status IN ('conflict', 'rejected')"
|
|
100
99
|
: '';
|
|
101
|
-
const rows = db.query(`SELECT seq,
|
|
102
|
-
resolution, resolved_at_ms, replacement_client_commit_id
|
|
103
|
-
FROM _syncular_commit_outcomes ${where}
|
|
104
|
-
ORDER BY seq DESC${limit === undefined ? '' : ' LIMIT ?'}`, limit === undefined ? [] : [limit]);
|
|
100
|
+
const rows = db.query(`SELECT seq,client_commit_id,status,recorded_at_ms,results,operations,resolution,resolved_at_ms,replacement_client_commit_id FROM _syncular_commit_outcomes ${where} ORDER BY seq DESC${limit === undefined ? '' : ' LIMIT ?'}`, limit === undefined ? [] : [limit]);
|
|
105
101
|
return rows.map((row) => parseOutcome(db, row));
|
|
106
102
|
}
|
|
107
103
|
export function persistCommitOutcomeResolution(db, input, nowMs) {
|
|
108
|
-
db.exec(
|
|
109
|
-
SET resolution = ?, resolved_at_ms = ?, replacement_client_commit_id = ?
|
|
110
|
-
WHERE client_commit_id = ? AND resolution = 'active'`, [
|
|
104
|
+
db.exec("UPDATE _syncular_commit_outcomes SET resolution=?,resolved_at_ms=?,replacement_client_commit_id=? WHERE client_commit_id=? AND resolution='active'", [
|
|
111
105
|
input.resolution,
|
|
112
106
|
nowMs,
|
|
113
107
|
input.replacementClientCommitId ?? null,
|
|
@@ -127,11 +121,9 @@ export function pruneCommitOutcomes(db, maxEntries) {
|
|
|
127
121
|
const excess = Math.max(0, (count ?? 0) - maxEntries);
|
|
128
122
|
if (excess === 0)
|
|
129
123
|
return 0;
|
|
130
|
-
const candidates = db.query(
|
|
131
|
-
WHERE status IN ('applied', 'cached') OR resolution != 'active'
|
|
132
|
-
ORDER BY seq ASC LIMIT ?`, [excess]);
|
|
124
|
+
const candidates = db.query("SELECT seq FROM _syncular_commit_outcomes WHERE status IN('applied','cached')OR resolution !='active' ORDER BY seq ASC LIMIT ?", [excess]);
|
|
133
125
|
for (const candidate of candidates) {
|
|
134
|
-
db.exec('DELETE FROM _syncular_commit_outcomes WHERE seq
|
|
126
|
+
db.exec('DELETE FROM _syncular_commit_outcomes WHERE seq=?', [
|
|
135
127
|
candidate.seq,
|
|
136
128
|
]);
|
|
137
129
|
}
|
package/dist/previous-version.js
CHANGED
|
@@ -241,10 +241,10 @@ export function withPreviousVersionContainer(db, fn, removeFile = false) {
|
|
|
241
241
|
}
|
|
242
242
|
/** Read the container's own metadata record; `undefined` when absent/corrupt. */
|
|
243
243
|
export function readPreviousVersionContainer(db) {
|
|
244
|
-
const meta = db.query("SELECT 1 AS present FROM sqlite_master WHERE type
|
|
244
|
+
const meta = db.query("SELECT 1 AS present FROM sqlite_master WHERE type='table' AND name=?", [CONTAINER_META_TABLE]);
|
|
245
245
|
if (meta.length === 0)
|
|
246
246
|
return undefined;
|
|
247
|
-
const raw = db.query(`SELECT record FROM ${quoteIdent(CONTAINER_META_TABLE)} WHERE id
|
|
247
|
+
const raw = db.query(`SELECT record FROM ${quoteIdent(CONTAINER_META_TABLE)} WHERE id=1`)[0]?.record;
|
|
248
248
|
if (raw === undefined)
|
|
249
249
|
return undefined;
|
|
250
250
|
try {
|
|
@@ -287,11 +287,9 @@ function decodeRecord(value) {
|
|
|
287
287
|
};
|
|
288
288
|
}
|
|
289
289
|
function writeRecord(db, record) {
|
|
290
|
-
db.exec(`CREATE TABLE IF NOT EXISTS ${quoteIdent(CONTAINER_META_TABLE)} (
|
|
291
|
-
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
292
|
-
record TEXT NOT NULL)`);
|
|
290
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ${quoteIdent(CONTAINER_META_TABLE)}(id INTEGER PRIMARY KEY CHECK(id=1),record TEXT NOT NULL)`);
|
|
293
291
|
db.exec(`DELETE FROM ${quoteIdent(CONTAINER_META_TABLE)}`);
|
|
294
|
-
db.exec(`INSERT INTO ${quoteIdent(CONTAINER_META_TABLE)}(id,
|
|
292
|
+
db.exec(`INSERT INTO ${quoteIdent(CONTAINER_META_TABLE)}(id,record)VALUES(1,?)`, [JSON.stringify(record)]);
|
|
295
293
|
}
|
|
296
294
|
// ---------------------------------------------------------------------------
|
|
297
295
|
// Bounded capture (D2 / A4)
|
|
@@ -309,9 +307,7 @@ function writeRecord(db, record) {
|
|
|
309
307
|
*/
|
|
310
308
|
export function capturePreviousVersion(replica, container, oldDescriptor, newSchema, config, nowMs) {
|
|
311
309
|
const discovered = new Set(replica
|
|
312
|
-
.query(
|
|
313
|
-
AND name NOT LIKE '_syncular_%'
|
|
314
|
-
AND name NOT LIKE 'sqlite_%'`)
|
|
310
|
+
.query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '_syncular_%' AND name NOT LIKE 'sqlite_%'")
|
|
315
311
|
.map((row) => String(row.name)));
|
|
316
312
|
// Only descriptor-known tables carry semantic types; those are the tables the
|
|
317
313
|
// copy would hold, and the count the table budget bounds.
|
|
@@ -326,7 +322,7 @@ export function capturePreviousVersion(replica, container, oldDescriptor, newSch
|
|
|
326
322
|
let measuredRows = 0;
|
|
327
323
|
const perTableRows = [];
|
|
328
324
|
for (const table of tables) {
|
|
329
|
-
const probeRows = Number(replica.query(`SELECT COUNT(*)
|
|
325
|
+
const probeRows = Number(replica.query(`SELECT COUNT(*)AS count FROM(SELECT 1 FROM ${quoteIdent(table.name)} LIMIT ?)`, [config.maxRows + 1])[0]?.count ?? 0);
|
|
330
326
|
measuredRows += probeRows;
|
|
331
327
|
perTableRows.push(probeRows);
|
|
332
328
|
if (measuredRows > config.maxRows)
|
|
@@ -340,13 +336,10 @@ export function capturePreviousVersion(replica, container, oldDescriptor, newSch
|
|
|
340
336
|
.join(' + ');
|
|
341
337
|
if (table.columns.length === 0)
|
|
342
338
|
continue;
|
|
343
|
-
const oversized = replica.query(`SELECT 1 AS hit FROM ${quoteIdent(table.name)}
|
|
344
|
-
WHERE (${byteSum}) > ? LIMIT 1`, [config.maxRowBytes]);
|
|
339
|
+
const oversized = replica.query(`SELECT 1 AS hit FROM ${quoteIdent(table.name)} WHERE(${byteSum})>? LIMIT 1`, [config.maxRowBytes]);
|
|
345
340
|
if (oversized.length > 0)
|
|
346
341
|
return over(measuredRows, measuredBytes);
|
|
347
|
-
const total = replica.query(`SELECT COALESCE(SUM(bytes),
|
|
348
|
-
SELECT (${byteSum}) AS bytes FROM ${quoteIdent(table.name)} LIMIT ?
|
|
349
|
-
)`, [perTableRows[index] ?? 0])[0]?.total;
|
|
342
|
+
const total = replica.query(`SELECT COALESCE(SUM(bytes),0)AS total FROM(SELECT(${byteSum})AS bytes FROM ${quoteIdent(table.name)} LIMIT ?)`, [perTableRows[index] ?? 0])[0]?.total;
|
|
350
343
|
measuredBytes += Number(total ?? 0);
|
|
351
344
|
if (measuredBytes > config.maxBytes)
|
|
352
345
|
return over(measuredRows, measuredBytes);
|
|
@@ -354,19 +347,14 @@ export function capturePreviousVersion(replica, container, oldDescriptor, newSch
|
|
|
354
347
|
let copiedRows = 0;
|
|
355
348
|
container.transaction(() => {
|
|
356
349
|
dropPreviousVersionContainer(container);
|
|
357
|
-
container.exec(`CREATE TABLE ${quoteIdent(CONTAINER_TABLE)} (
|
|
358
|
-
tbl TEXT NOT NULL,
|
|
359
|
-
row_id TEXT NOT NULL,
|
|
360
|
-
payload TEXT NOT NULL,
|
|
361
|
-
PRIMARY KEY (tbl, row_id))`);
|
|
350
|
+
container.exec(`CREATE TABLE ${quoteIdent(CONTAINER_TABLE)}(tbl TEXT NOT NULL,row_id TEXT NOT NULL,payload TEXT NOT NULL,PRIMARY KEY(tbl,row_id))`);
|
|
362
351
|
for (const table of tables) {
|
|
363
352
|
const selectColumns = table.columns
|
|
364
353
|
.map((column) => quoteIdent(column.name))
|
|
365
354
|
.join(', ');
|
|
366
355
|
let lastRowId = -1;
|
|
367
356
|
for (;;) {
|
|
368
|
-
const rows = replica.query(`SELECT rowid AS _rid
|
|
369
|
-
WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`, [lastRowId, COPY_BATCH_ROWS]);
|
|
357
|
+
const rows = replica.query(`SELECT rowid AS _rid,${selectColumns} FROM ${quoteIdent(table.name)} WHERE rowid>? ORDER BY rowid ASC LIMIT ?`, [lastRowId, COPY_BATCH_ROWS]);
|
|
370
358
|
if (rows.length === 0)
|
|
371
359
|
break;
|
|
372
360
|
for (const row of rows) {
|
|
@@ -375,8 +363,7 @@ export function capturePreviousVersion(replica, container, oldDescriptor, newSch
|
|
|
375
363
|
const value = fromSqlValue({ name: column.name, type: column.type, nullable: true }, (row[column.name] ?? null));
|
|
376
364
|
payload[column.name] = rowValueToJson(value);
|
|
377
365
|
}
|
|
378
|
-
container.exec(`INSERT INTO ${quoteIdent(CONTAINER_TABLE)}(tbl,
|
|
379
|
-
VALUES (?, ?, ?)`, [table.name, rowIdText(table, row), JSON.stringify(payload)]);
|
|
366
|
+
container.exec(`INSERT INTO ${quoteIdent(CONTAINER_TABLE)}(tbl,row_id,payload)VALUES(?,?,?)`, [table.name, rowIdText(table, row), JSON.stringify(payload)]);
|
|
380
367
|
}
|
|
381
368
|
lastRowId = Number(rows[rows.length - 1]?._rid ?? lastRowId);
|
|
382
369
|
copiedRows += rows.length;
|
|
@@ -431,8 +418,7 @@ export function readPreviousVersionRows(db, table, rowIds, limit) {
|
|
|
431
418
|
const where = rowIds.length === 0
|
|
432
419
|
? ''
|
|
433
420
|
: ` AND row_id IN (${rowIds.map(() => '?').join(', ')})`;
|
|
434
|
-
const rows = db.query(`SELECT row_id,
|
|
435
|
-
WHERE tbl = ?${where} ORDER BY row_id ASC LIMIT ?`, [table.name, ...rowIds, limit + 1]);
|
|
421
|
+
const rows = db.query(`SELECT row_id,payload FROM ${quoteIdent(CONTAINER_TABLE)} WHERE tbl=?${where} ORDER BY row_id ASC LIMIT ?`, [table.name, ...rowIds, limit + 1]);
|
|
436
422
|
const truncated = rows.length > limit;
|
|
437
423
|
const visible = truncated ? rows.slice(0, limit) : rows;
|
|
438
424
|
return {
|
|
@@ -455,7 +441,7 @@ export function writePreviousVersionRefusal(db, reason, measurement) {
|
|
|
455
441
|
}
|
|
456
442
|
/** Delete the durable refusal. Part of the orphan sweep and of a successful capture. */
|
|
457
443
|
export function clearPreviousVersionRefusal(db) {
|
|
458
|
-
db.exec('DELETE FROM _syncular_meta WHERE key
|
|
444
|
+
db.exec('DELETE FROM _syncular_meta WHERE key=?', [
|
|
459
445
|
PREVIOUS_VERSION_CONTEXT_KEY,
|
|
460
446
|
]);
|
|
461
447
|
}
|
|
@@ -589,7 +575,7 @@ export function writePreviousVersionAudit(db, audit) {
|
|
|
589
575
|
}
|
|
590
576
|
/** Delete the advisory audit. Part of a discard, never of a normal bump. */
|
|
591
577
|
export function clearPreviousVersionAudit(db) {
|
|
592
|
-
db.exec('DELETE FROM _syncular_meta WHERE key
|
|
578
|
+
db.exec('DELETE FROM _syncular_meta WHERE key=?', [
|
|
593
579
|
PREVIOUS_VERSION_AUDIT_KEY,
|
|
594
580
|
]);
|
|
595
581
|
}
|