@syncular/client 0.30.11 → 0.30.13
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 +7 -0
- package/dist/apply.d.ts +5 -1
- package/dist/apply.js +44 -36
- package/dist/client.d.ts +3 -0
- package/dist/client.js +205 -181
- package/dist/failed-overlay.d.ts +4 -3
- package/dist/failed-overlay.js +58 -43
- package/dist/outbox.d.ts +2 -1
- package/dist/outbox.js +14 -24
- package/dist/outcomes.js +6 -5
- package/dist/schema.d.ts +1 -0
- package/dist/schema.js +63 -72
- package/dist/worker-entry.js +14 -35
- package/dist/worker-host.d.ts +1 -0
- package/dist/worker-host.js +3 -0
- package/dist/worker-protocol.d.ts +1 -0
- package/package.json +3 -3
- package/src/apply.ts +81 -50
- package/src/client.ts +263 -226
- package/src/failed-overlay.ts +68 -57
- package/src/outbox.ts +19 -28
- package/src/outcomes.ts +7 -6
- package/src/schema.ts +81 -88
- package/src/worker-entry.ts +13 -50
- package/src/worker-host.ts +4 -0
- package/src/worker-protocol.ts +1 -0
package/src/failed-overlay.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export const RETAINED_ROWS = '_syncular_failed_rows';
|
|
1
2
|
import { ClientSyncError } from './errors';
|
|
2
3
|
import type { ClientDatabase } from './database';
|
|
3
4
|
import type {
|
|
@@ -26,16 +27,12 @@ import { deleteLocalRow, upsertLocalRow } from './apply';
|
|
|
26
27
|
/** Protected failure state. The server base advances independently of intent. */
|
|
27
28
|
export function ensureFailedOverlaySchema(db: ClientDatabase): void {
|
|
28
29
|
db.exec(
|
|
29
|
-
|
|
30
|
+
`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))`,
|
|
30
31
|
);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.
|
|
35
|
-
)
|
|
36
|
-
db.exec(
|
|
37
|
-
'ALTER TABLE _syncular_failed_rows ADD COLUMN unique_conflicts TEXT',
|
|
38
|
-
);
|
|
32
|
+
const columns = db.query(`PRAGMA table_info(${RETAINED_ROWS})`);
|
|
33
|
+
for (const column of ['unique_conflicts TEXT', 'commit_seq INTEGER'])
|
|
34
|
+
if (!columns.some((row) => row.name === column.split(' ')[0]))
|
|
35
|
+
db.exec(`ALTER TABLE ${RETAINED_ROWS} ADD COLUMN ${column}`);
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
export function retainedBaseWrite(
|
|
@@ -45,18 +42,15 @@ export function retainedBaseWrite(
|
|
|
45
42
|
values?: Readonly<Record<string, JsonRowValue>>,
|
|
46
43
|
version?: number,
|
|
47
44
|
): void {
|
|
48
|
-
db.exec(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
rowId,
|
|
55
|
-
],
|
|
56
|
-
);
|
|
45
|
+
db.exec(`UPDATE ${RETAINED_ROWS} SET base=?,version=? WHERE tbl=? AND id=?`, [
|
|
46
|
+
values === undefined ? null : JSON.stringify(values),
|
|
47
|
+
version ?? null,
|
|
48
|
+
table.name,
|
|
49
|
+
rowId,
|
|
50
|
+
]);
|
|
57
51
|
if (!table.indexes.some((index) => index.unique)) return;
|
|
58
52
|
for (const retained of db.query(
|
|
59
|
-
|
|
53
|
+
`SELECT commit_id,idx,intent,unique_conflicts FROM ${RETAINED_ROWS} WHERE tbl=? AND commit_seq IS NULL`,
|
|
60
54
|
[table.name],
|
|
61
55
|
)) {
|
|
62
56
|
const prior: readonly (Omit<
|
|
@@ -65,11 +59,11 @@ export function retainedBaseWrite(
|
|
|
65
59
|
> & { serverRow: Readonly<Record<string, JsonRowValue>> })[] =
|
|
66
60
|
retained.unique_conflicts === null
|
|
67
61
|
? []
|
|
68
|
-
: JSON.parse(
|
|
62
|
+
: JSON.parse(retained.unique_conflicts as string);
|
|
69
63
|
const conflicts = prior.filter((conflict) => conflict.rowId !== rowId);
|
|
70
64
|
if (values !== undefined && retained.intent !== null) {
|
|
71
65
|
const intent: Record<string, JsonRowValue> = JSON.parse(
|
|
72
|
-
|
|
66
|
+
retained.intent as string,
|
|
73
67
|
);
|
|
74
68
|
conflicts.push(
|
|
75
69
|
...uniqueConflicts(db, table, mapRowValues(intent, jsonToRowValue))
|
|
@@ -81,7 +75,7 @@ export function retainedBaseWrite(
|
|
|
81
75
|
);
|
|
82
76
|
}
|
|
83
77
|
db.exec(
|
|
84
|
-
|
|
78
|
+
`UPDATE ${RETAINED_ROWS} SET unique_conflicts=? WHERE commit_id=? AND idx=?`,
|
|
85
79
|
[
|
|
86
80
|
conflicts.length
|
|
87
81
|
? JSON.stringify(
|
|
@@ -92,7 +86,7 @@ export function retainedBaseWrite(
|
|
|
92
86
|
),
|
|
93
87
|
)
|
|
94
88
|
: null,
|
|
95
|
-
|
|
89
|
+
retained.commit_id as string,
|
|
96
90
|
Number(retained.idx),
|
|
97
91
|
],
|
|
98
92
|
);
|
|
@@ -103,9 +97,12 @@ export function retainFailedRows(
|
|
|
103
97
|
db: ClientDatabase,
|
|
104
98
|
commit: OutboxCommit,
|
|
105
99
|
images: readonly OutboxBeforeImage[],
|
|
100
|
+
commitSeq?: number,
|
|
106
101
|
): void {
|
|
107
102
|
for (const [index, operation] of commit.operations.entries()) {
|
|
108
103
|
const image = images.find((image) => image.opIndex === index);
|
|
104
|
+
if (commitSeq !== undefined && (image?.deliverySeq ?? -1) >= commitSeq)
|
|
105
|
+
continue;
|
|
109
106
|
const initial =
|
|
110
107
|
operation.op === 'upsert'
|
|
111
108
|
? { ...image?.values, ...operation.values }
|
|
@@ -113,10 +110,10 @@ export function retainFailedRows(
|
|
|
113
110
|
if (!image)
|
|
114
111
|
throw new ClientSyncError(
|
|
115
112
|
'sync.local_corrupt',
|
|
116
|
-
'
|
|
113
|
+
'missing intent before-image',
|
|
117
114
|
);
|
|
118
115
|
db.exec(
|
|
119
|
-
|
|
116
|
+
`INSERT INTO ${RETAINED_ROWS}(commit_id,idx,tbl,id,at,op,intent,base,version,commit_seq)VALUES(?,?,?,?,?,?,?,?,?,?)`,
|
|
120
117
|
[
|
|
121
118
|
commit.clientCommitId,
|
|
122
119
|
index,
|
|
@@ -127,6 +124,7 @@ export function retainFailedRows(
|
|
|
127
124
|
initial ? JSON.stringify(initial) : null,
|
|
128
125
|
image.values ? JSON.stringify(image.values) : null,
|
|
129
126
|
image.syncVersion ?? null,
|
|
127
|
+
commitSeq ?? null,
|
|
130
128
|
],
|
|
131
129
|
);
|
|
132
130
|
}
|
|
@@ -166,28 +164,34 @@ export function uniqueConflicts(
|
|
|
166
164
|
);
|
|
167
165
|
}
|
|
168
166
|
|
|
167
|
+
function retainedRows(db: ClientDatabase, rowKeys?: readonly string[]) {
|
|
168
|
+
return db.query(
|
|
169
|
+
`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`,
|
|
170
|
+
[rowKeys ? JSON.stringify(rowKeys) : null],
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
169
174
|
export function restoreFailedBases(
|
|
170
175
|
db: ClientDatabase,
|
|
171
176
|
schema: CompiledClientSchema,
|
|
172
177
|
absentIntent = false,
|
|
173
|
-
|
|
178
|
+
rowKeys?: readonly string[],
|
|
179
|
+
): boolean {
|
|
174
180
|
const seen = new Set<string>();
|
|
175
|
-
for (const row of db
|
|
176
|
-
'SELECT * FROM _syncular_failed_rows ORDER BY at,commit_id,idx',
|
|
177
|
-
)) {
|
|
181
|
+
for (const row of retainedRows(db, rowKeys)) {
|
|
178
182
|
const key = JSON.stringify([row.tbl, row.id]);
|
|
179
183
|
if (!absentIntent && seen.has(key)) continue;
|
|
180
184
|
seen.add(key);
|
|
181
|
-
const table = schema.tables.get(
|
|
185
|
+
const table = schema.tables.get(row.tbl as string);
|
|
182
186
|
if (!table)
|
|
183
187
|
throw new ClientSyncError(
|
|
184
188
|
'sync.unknown_table',
|
|
185
|
-
'retained
|
|
189
|
+
'retained table is unknown',
|
|
186
190
|
);
|
|
187
191
|
const json = absentIntent ? row.intent : row.base;
|
|
188
192
|
if (absentIntent && row.intent !== null) {
|
|
189
193
|
const values: Record<string, JsonRowValue> = JSON.parse(
|
|
190
|
-
|
|
194
|
+
row.intent as string,
|
|
191
195
|
);
|
|
192
196
|
const conflicts = uniqueConflicts(
|
|
193
197
|
db,
|
|
@@ -196,18 +200,18 @@ export function restoreFailedBases(
|
|
|
196
200
|
);
|
|
197
201
|
// Hydrate a pre-0.30.11 failure after its winner was already imported.
|
|
198
202
|
// Pending and retained overlays cannot supply authoritative row evidence.
|
|
199
|
-
if (row.unique_conflicts === null) {
|
|
203
|
+
if (row.commit_seq === null && row.unique_conflicts === null) {
|
|
200
204
|
const authorized = conflicts.filter(
|
|
201
205
|
(conflict) =>
|
|
202
206
|
conflict.serverVersion >= 0 &&
|
|
203
207
|
!db.query(
|
|
204
|
-
|
|
208
|
+
`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`,
|
|
205
209
|
[table.name, conflict.rowId, table.name, conflict.rowId],
|
|
206
210
|
).length,
|
|
207
211
|
);
|
|
208
212
|
if (authorized.length)
|
|
209
213
|
db.exec(
|
|
210
|
-
|
|
214
|
+
`UPDATE ${RETAINED_ROWS} SET unique_conflicts=? WHERE commit_id=? AND idx=?`,
|
|
211
215
|
[
|
|
212
216
|
JSON.stringify(
|
|
213
217
|
authorized.map((conflict) => ({
|
|
@@ -215,8 +219,8 @@ export function restoreFailedBases(
|
|
|
215
219
|
serverRow: mapRowValues(conflict.serverRow, rowValueToJson),
|
|
216
220
|
})),
|
|
217
221
|
),
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
row.commit_id as string,
|
|
223
|
+
row.idx as number,
|
|
220
224
|
],
|
|
221
225
|
);
|
|
222
226
|
}
|
|
@@ -227,16 +231,17 @@ export function restoreFailedBases(
|
|
|
227
231
|
(json === null ||
|
|
228
232
|
db.query(
|
|
229
233
|
`SELECT 1 FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}=?`,
|
|
230
|
-
[
|
|
234
|
+
[row.id as string],
|
|
231
235
|
).length)
|
|
232
236
|
)
|
|
233
237
|
continue;
|
|
234
|
-
if (json === null) deleteLocalRow(db, table,
|
|
238
|
+
if (json === null) deleteLocalRow(db, table, row.id as string, false);
|
|
235
239
|
else {
|
|
236
240
|
const values: Record<string, JsonRowValue> = JSON.parse(String(json));
|
|
237
241
|
if (absentIntent) {
|
|
238
|
-
const operation: OutboxOperation = JSON.parse(
|
|
242
|
+
const operation: OutboxOperation = JSON.parse(row.op as string);
|
|
239
243
|
if (
|
|
244
|
+
row.commit_seq === null &&
|
|
240
245
|
!table.columns.every((column) =>
|
|
241
246
|
Object.hasOwn(operation.values ?? {}, column.name),
|
|
242
247
|
)
|
|
@@ -249,14 +254,19 @@ export function restoreFailedBases(
|
|
|
249
254
|
table.columns.map((column) =>
|
|
250
255
|
jsonToRowValue(values[column.name] ?? null),
|
|
251
256
|
),
|
|
252
|
-
|
|
257
|
+
(row.version ?? -1) as number,
|
|
253
258
|
false,
|
|
254
259
|
);
|
|
255
260
|
}
|
|
256
261
|
}
|
|
262
|
+
return seen.size > 0;
|
|
257
263
|
}
|
|
258
264
|
|
|
259
|
-
export function failedOverlayCommits(
|
|
265
|
+
export function failedOverlayCommits(
|
|
266
|
+
db: ClientDatabase,
|
|
267
|
+
acknowledged = false,
|
|
268
|
+
rowKeys?: readonly string[],
|
|
269
|
+
): OutboxCommit[] {
|
|
260
270
|
const commits = new Map<
|
|
261
271
|
string,
|
|
262
272
|
{
|
|
@@ -266,18 +276,17 @@ export function failedOverlayCommits(db: ClientDatabase): OutboxCommit[] {
|
|
|
266
276
|
operations: OutboxOperation[];
|
|
267
277
|
}
|
|
268
278
|
>();
|
|
269
|
-
for (const row of db
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
const operation: OutboxOperation = JSON.parse(String(row.op));
|
|
279
|
+
for (const row of retainedRows(db, rowKeys)) {
|
|
280
|
+
if ((row.commit_seq !== null) !== acknowledged) continue;
|
|
281
|
+
const id = row.commit_id as string;
|
|
282
|
+
const operation: OutboxOperation = JSON.parse(row.op as string);
|
|
274
283
|
const prior = commits.get(id);
|
|
275
284
|
if (prior) prior.operations.push(operation);
|
|
276
285
|
else
|
|
277
286
|
commits.set(id, {
|
|
278
287
|
seq: 0,
|
|
279
288
|
clientCommitId: id,
|
|
280
|
-
createdAtMs:
|
|
289
|
+
createdAtMs: row.at as number,
|
|
281
290
|
operations: [operation],
|
|
282
291
|
});
|
|
283
292
|
}
|
|
@@ -292,17 +301,19 @@ export function dropFailedRows(
|
|
|
292
301
|
) => boolean,
|
|
293
302
|
): OutboxCommit[] {
|
|
294
303
|
const ids = new Set<string>();
|
|
295
|
-
for (const row of db
|
|
296
|
-
const json
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
304
|
+
for (const row of retainedRows(db)) {
|
|
305
|
+
for (const json of [row.intent, row.base]) {
|
|
306
|
+
if (json === null) continue;
|
|
307
|
+
const values: Record<string, JsonRowValue> = JSON.parse(String(json));
|
|
308
|
+
if (matches(row.tbl as string, values)) ids.add(row.commit_id as string);
|
|
309
|
+
}
|
|
300
310
|
}
|
|
301
|
-
const dropped =
|
|
302
|
-
|
|
303
|
-
|
|
311
|
+
const dropped = [
|
|
312
|
+
...failedOverlayCommits(db),
|
|
313
|
+
...failedOverlayCommits(db, true),
|
|
314
|
+
].filter((commit) => ids.has(commit.clientCommitId));
|
|
304
315
|
for (const id of ids) {
|
|
305
|
-
db.exec(
|
|
316
|
+
db.exec(`DELETE FROM ${RETAINED_ROWS} WHERE commit_id=?`, [id]);
|
|
306
317
|
db.exec(
|
|
307
318
|
"UPDATE _syncular_commit_outcomes SET operations=NULL,results='[]' WHERE client_commit_id=?",
|
|
308
319
|
[id],
|
package/src/outbox.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type CompiledClientTable,
|
|
21
21
|
type JsonRowValue,
|
|
22
22
|
jsonToRowValue,
|
|
23
|
+
matchesLocalScopes,
|
|
23
24
|
} from './schema';
|
|
24
25
|
|
|
25
26
|
export interface OutboxOperation {
|
|
@@ -48,6 +49,7 @@ export interface OutboxCommit {
|
|
|
48
49
|
export interface OutboxBeforeImage {
|
|
49
50
|
readonly opIndex: number;
|
|
50
51
|
readonly existed: boolean;
|
|
52
|
+
readonly deliverySeq?: number;
|
|
51
53
|
readonly syncVersion?: number;
|
|
52
54
|
readonly values?: Readonly<Record<string, JsonRowValue>>;
|
|
53
55
|
}
|
|
@@ -69,25 +71,20 @@ export function appendOutboxCommit(
|
|
|
69
71
|
'INSERT INTO _syncular_outbox(client_commit_id,created_at_ms,operations)VALUES(?,?,?)',
|
|
70
72
|
[clientCommitId, nowMs, JSON.stringify(operations)],
|
|
71
73
|
);
|
|
72
|
-
|
|
73
|
-
db.exec(
|
|
74
|
-
'INSERT INTO _syncular_outbox_before_images(client_commit_id,op_index,existed,sync_version,values_json)VALUES(?,?,?,?,?)',
|
|
75
|
-
[
|
|
76
|
-
clientCommitId,
|
|
77
|
-
image.opIndex,
|
|
78
|
-
image.existed ? 1 : 0,
|
|
79
|
-
image.syncVersion ?? null,
|
|
80
|
-
image.values === undefined ? null : JSON.stringify(image.values),
|
|
81
|
-
],
|
|
82
|
-
);
|
|
83
|
-
}
|
|
74
|
+
replaceOutboxBeforeImages(db, clientCommitId, beforeImages);
|
|
84
75
|
}
|
|
85
76
|
|
|
86
77
|
/** Pending commits in FIFO creation order (§7.1). Full reads serve replay and the public listing. */
|
|
87
|
-
export function listOutbox(
|
|
78
|
+
export function listOutbox(
|
|
79
|
+
db: ClientDatabase,
|
|
80
|
+
rowKeys?: readonly string[],
|
|
81
|
+
): OutboxCommit[] {
|
|
88
82
|
return db
|
|
89
83
|
.query(
|
|
90
|
-
|
|
84
|
+
rowKeys === undefined
|
|
85
|
+
? 'SELECT seq, client_commit_id, created_at_ms, operations FROM _syncular_outbox ORDER BY seq ASC'
|
|
86
|
+
: "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",
|
|
87
|
+
rowKeys && [JSON.stringify(rowKeys)],
|
|
91
88
|
)
|
|
92
89
|
.map(decodeOutboxRow);
|
|
93
90
|
}
|
|
@@ -149,12 +146,15 @@ export function listOutboxBeforeImages(
|
|
|
149
146
|
): OutboxBeforeImage[] {
|
|
150
147
|
return db
|
|
151
148
|
.query(
|
|
152
|
-
'SELECT op_index,existed,sync_version,values_json FROM _syncular_outbox_before_images WHERE client_commit_id=? ORDER BY op_index',
|
|
149
|
+
'SELECT op_index,existed,sync_version,values_json,delivery_seq FROM _syncular_outbox_before_images WHERE client_commit_id=? ORDER BY op_index',
|
|
153
150
|
[clientCommitId],
|
|
154
151
|
)
|
|
155
152
|
.map((row) => ({
|
|
156
153
|
opIndex: row.op_index as number,
|
|
157
154
|
existed: row.existed === 1,
|
|
155
|
+
...(typeof row.delivery_seq === 'number'
|
|
156
|
+
? { deliverySeq: row.delivery_seq }
|
|
157
|
+
: {}),
|
|
158
158
|
...(typeof row.sync_version === 'number'
|
|
159
159
|
? { syncVersion: row.sync_version }
|
|
160
160
|
: {}),
|
|
@@ -175,11 +175,7 @@ export function replaceOutboxBeforeImages(
|
|
|
175
175
|
): void {
|
|
176
176
|
for (const image of replacements) {
|
|
177
177
|
db.exec(
|
|
178
|
-
'
|
|
179
|
-
[clientCommitId, image.opIndex],
|
|
180
|
-
);
|
|
181
|
-
db.exec(
|
|
182
|
-
'INSERT INTO _syncular_outbox_before_images(client_commit_id,op_index,existed,sync_version,values_json)VALUES(?,?,?,?,?)',
|
|
178
|
+
'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',
|
|
183
179
|
[
|
|
184
180
|
clientCommitId,
|
|
185
181
|
image.opIndex,
|
|
@@ -212,7 +208,7 @@ function sparseValues(
|
|
|
212
208
|
for (const key of Object.keys(values)) {
|
|
213
209
|
if (!table.columnIndex.has(key)) {
|
|
214
210
|
throw new OutboxEncodeError(
|
|
215
|
-
`outbox
|
|
211
|
+
`outbox column ${JSON.stringify(key)} is absent from table ${JSON.stringify(table.name)} (§7.4.4)`,
|
|
216
212
|
);
|
|
217
213
|
}
|
|
218
214
|
}
|
|
@@ -256,7 +252,7 @@ export async function encodeOutboxCommit(
|
|
|
256
252
|
if (table === undefined) {
|
|
257
253
|
// §7.4.4: the bump removed this table — the commit cannot be encoded.
|
|
258
254
|
throw new OutboxEncodeError(
|
|
259
|
-
`outbox
|
|
255
|
+
`outbox table ${JSON.stringify(op.table)} is absent from the current schema (§7.4.4)`,
|
|
260
256
|
);
|
|
261
257
|
}
|
|
262
258
|
if (op.values === undefined) {
|
|
@@ -310,12 +306,7 @@ export function dropOutboxCommitsInScope(
|
|
|
310
306
|
for (const commit of listOutbox(db)) {
|
|
311
307
|
const inScope = commit.operations.some((op) => {
|
|
312
308
|
if (op.table !== table.name || op.values === undefined) return false;
|
|
313
|
-
return
|
|
314
|
-
const column = table.scopeColumnByVariable.get(variable);
|
|
315
|
-
if (column === undefined) return false;
|
|
316
|
-
const value = op.values?.[column];
|
|
317
|
-
return typeof value === 'string' && values.includes(value);
|
|
318
|
-
});
|
|
309
|
+
return matchesLocalScopes(table, effective, op.values);
|
|
319
310
|
});
|
|
320
311
|
if (inScope) {
|
|
321
312
|
deleteOutboxCommit(db, commit.clientCommitId);
|
package/src/outcomes.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RETAINED_ROWS } from './failed-overlay';
|
|
1
2
|
/**
|
|
2
3
|
* Durable per-client commit outcomes.
|
|
3
4
|
*
|
|
@@ -174,8 +175,8 @@ function parseOutcome(
|
|
|
174
175
|
): CommitOutcome {
|
|
175
176
|
const retainedRows: RetainedCommitRow[] = db
|
|
176
177
|
.query(
|
|
177
|
-
|
|
178
|
-
[
|
|
178
|
+
`SELECT tbl,id,intent,base,version,unique_conflicts FROM ${RETAINED_ROWS} WHERE commit_seq IS NULL AND commit_id=? ORDER BY idx`,
|
|
179
|
+
[row.client_commit_id as string],
|
|
179
180
|
)
|
|
180
181
|
.map((retained) => {
|
|
181
182
|
const decode = (value: unknown): Record<string, RowValue> | null =>
|
|
@@ -187,14 +188,14 @@ function parseOutcome(
|
|
|
187
188
|
).map(([key, value]) => [key, jsonToRowValue(value)]),
|
|
188
189
|
);
|
|
189
190
|
return {
|
|
190
|
-
table:
|
|
191
|
-
rowId:
|
|
191
|
+
table: retained.tbl as string,
|
|
192
|
+
rowId: retained.id as string,
|
|
192
193
|
localRow: decode(retained.intent),
|
|
193
194
|
...(retained.unique_conflicts === null
|
|
194
195
|
? {}
|
|
195
196
|
: {
|
|
196
197
|
uniqueConflicts: (
|
|
197
|
-
JSON.parse(
|
|
198
|
+
JSON.parse(retained.unique_conflicts as string) as NonNullable<
|
|
198
199
|
RetainedCommitRow['uniqueConflicts']
|
|
199
200
|
>
|
|
200
201
|
).map((conflict) => ({
|
|
@@ -204,7 +205,7 @@ function parseOutcome(
|
|
|
204
205
|
}),
|
|
205
206
|
serverRow: decode(retained.base),
|
|
206
207
|
serverVersion:
|
|
207
|
-
retained.version === null ? null :
|
|
208
|
+
retained.version === null ? null : (retained.version as number),
|
|
208
209
|
};
|
|
209
210
|
});
|
|
210
211
|
return {
|