@syncular/client 0.10.0 → 0.11.0
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 +5 -0
- package/dist/client.js +3 -0
- package/dist/outcomes.d.ts +6 -0
- package/dist/outcomes.js +10 -4
- package/dist/schema.js +9 -0
- package/package.json +3 -3
- package/src/client.ts +3 -0
- package/src/outcomes.ts +16 -4
- package/src/schema.ts +10 -0
package/README.md
CHANGED
|
@@ -117,6 +117,11 @@ the same SQLite transaction that drains their outbox commit. Conflict entries
|
|
|
117
117
|
retain the losing operation plus `serverVersion`/`serverRow`; active failures
|
|
118
118
|
restore after restart and are never removed by retention. Configure the
|
|
119
119
|
history cap with `limits.outcomeRetentionMaxEntries` (default 1,000).
|
|
120
|
+
Failed outcomes additionally retain the complete ordered local commit envelope
|
|
121
|
+
as `outcome.operations`, so a domain recovery flow can reconstruct siblings
|
|
122
|
+
that rolled back with the terminating operation. It stays in the protected
|
|
123
|
+
client database and is never added to the wire protocol, preferences, or
|
|
124
|
+
telemetry; successful and historical outcomes may omit it.
|
|
120
125
|
|
|
121
126
|
Use `patch(table, rowId, partial, { baseVersion? })` for editor-style partial
|
|
122
127
|
updates. The wire still carries a full row, but the durable local operation
|
package/dist/client.js
CHANGED
|
@@ -1154,6 +1154,7 @@ export class SyncClient {
|
|
|
1154
1154
|
status: 'rejected',
|
|
1155
1155
|
recordedAtMs: this.#now(),
|
|
1156
1156
|
results: [{ status: 'error', rejection }],
|
|
1157
|
+
operations: commit.operations,
|
|
1157
1158
|
});
|
|
1158
1159
|
pruneCommitOutcomes(this.#db, this.#outcomeRetentionMaxEntries);
|
|
1159
1160
|
batch.status();
|
|
@@ -1806,6 +1807,7 @@ export class SyncClient {
|
|
|
1806
1807
|
: 'rejected',
|
|
1807
1808
|
recordedAtMs: this.#now(),
|
|
1808
1809
|
results: outcomeResults,
|
|
1810
|
+
operations: commit.operations,
|
|
1809
1811
|
});
|
|
1810
1812
|
pruneCommitOutcomes(this.#db, this.#outcomeRetentionMaxEntries);
|
|
1811
1813
|
batch.outcomes();
|
|
@@ -2083,6 +2085,7 @@ export class SyncClient {
|
|
|
2083
2085
|
status: 'rejected',
|
|
2084
2086
|
recordedAtMs: this.#now(),
|
|
2085
2087
|
results,
|
|
2088
|
+
operations: commit.operations,
|
|
2086
2089
|
});
|
|
2087
2090
|
}
|
|
2088
2091
|
pruneCommitOutcomes(this.#db, this.#outcomeRetentionMaxEntries);
|
package/dist/outcomes.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export interface CommitOutcome {
|
|
|
51
51
|
readonly status: CommitOutcomeStatus;
|
|
52
52
|
readonly recordedAtMs: number;
|
|
53
53
|
readonly results: readonly CommitOperationOutcome[];
|
|
54
|
+
/**
|
|
55
|
+
* Complete local failed-commit envelope, retained after outbox drain so an
|
|
56
|
+
* authorized application can reconstruct atomic aggregate intent. Absent
|
|
57
|
+
* for successful and historical outcomes. Never sent over the wire.
|
|
58
|
+
*/
|
|
59
|
+
readonly operations?: readonly OutboxOperation[];
|
|
54
60
|
readonly resolution: CommitOutcomeResolution;
|
|
55
61
|
readonly resolvedAtMs?: number;
|
|
56
62
|
readonly replacementClientCommitId?: string;
|
package/dist/outcomes.js
CHANGED
|
@@ -41,6 +41,9 @@ function parseOutcome(row) {
|
|
|
41
41
|
status: row.status,
|
|
42
42
|
recordedAtMs: row.recorded_at_ms,
|
|
43
43
|
results: decodeResults(row.results),
|
|
44
|
+
...(typeof row.operations === 'string'
|
|
45
|
+
? { operations: JSON.parse(row.operations) }
|
|
46
|
+
: {}),
|
|
44
47
|
resolution: row.resolution,
|
|
45
48
|
...(typeof row.resolved_at_ms === 'number'
|
|
46
49
|
? { resolvedAtMs: row.resolved_at_ms }
|
|
@@ -52,17 +55,20 @@ function parseOutcome(row) {
|
|
|
52
55
|
}
|
|
53
56
|
export function recordCommitOutcome(db, outcome) {
|
|
54
57
|
db.exec(`INSERT INTO _syncular_commit_outcomes(
|
|
55
|
-
client_commit_id, status, recorded_at_ms, results, resolution
|
|
56
|
-
) VALUES (?, ?, ?, ?, 'active')`, [
|
|
58
|
+
client_commit_id, status, recorded_at_ms, results, operations, resolution
|
|
59
|
+
) VALUES (?, ?, ?, ?, ?, 'active')`, [
|
|
57
60
|
outcome.clientCommitId,
|
|
58
61
|
outcome.status,
|
|
59
62
|
outcome.recordedAtMs,
|
|
60
63
|
encodeResults(outcome.results),
|
|
64
|
+
outcome.operations === undefined
|
|
65
|
+
? null
|
|
66
|
+
: JSON.stringify(outcome.operations),
|
|
61
67
|
]);
|
|
62
68
|
return commitOutcome(db, outcome.clientCommitId);
|
|
63
69
|
}
|
|
64
70
|
export function commitOutcome(db, clientCommitId) {
|
|
65
|
-
const row = db.query(`SELECT seq, client_commit_id, status, recorded_at_ms, results,
|
|
71
|
+
const row = db.query(`SELECT seq, client_commit_id, status, recorded_at_ms, results, operations,
|
|
66
72
|
resolution, resolved_at_ms, replacement_client_commit_id
|
|
67
73
|
FROM _syncular_commit_outcomes WHERE client_commit_id = ?`, [clientCommitId])[0];
|
|
68
74
|
return row === undefined ? undefined : parseOutcome(row);
|
|
@@ -75,7 +81,7 @@ export function listCommitOutcomes(db, query = {}) {
|
|
|
75
81
|
const where = query.activeOnly
|
|
76
82
|
? "WHERE resolution = 'active' AND status IN ('conflict', 'rejected')"
|
|
77
83
|
: '';
|
|
78
|
-
const rows = db.query(`SELECT seq, client_commit_id, status, recorded_at_ms, results,
|
|
84
|
+
const rows = db.query(`SELECT seq, client_commit_id, status, recorded_at_ms, results, operations,
|
|
79
85
|
resolution, resolved_at_ms, replacement_client_commit_id
|
|
80
86
|
FROM _syncular_commit_outcomes ${where}
|
|
81
87
|
ORDER BY seq DESC${limit === undefined ? '' : ' LIMIT ?'}`, limit === undefined ? [] : [limit]);
|
package/dist/schema.js
CHANGED
|
@@ -190,10 +190,19 @@ export function ensureLocalSchema(db, schema) {
|
|
|
190
190
|
status TEXT NOT NULL CHECK(status IN ('applied', 'cached', 'conflict', 'rejected')),
|
|
191
191
|
recorded_at_ms INTEGER NOT NULL,
|
|
192
192
|
results TEXT NOT NULL,
|
|
193
|
+
operations TEXT,
|
|
193
194
|
resolution TEXT NOT NULL DEFAULT 'active'
|
|
194
195
|
CHECK(resolution IN ('active', 'resolved_keep_server', 'superseded', 'dismissed')),
|
|
195
196
|
resolved_at_ms INTEGER,
|
|
196
197
|
replacement_client_commit_id TEXT)`);
|
|
198
|
+
// Outcomes created before durable aggregate recovery retain no commit
|
|
199
|
+
// envelope. New failed outcomes store it in the protected client DB.
|
|
200
|
+
try {
|
|
201
|
+
db.exec('ALTER TABLE _syncular_commit_outcomes ADD COLUMN operations TEXT');
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// column already exists — the CREATE above included it
|
|
205
|
+
}
|
|
197
206
|
db.exec(`CREATE INDEX IF NOT EXISTS _syncular_commit_outcomes_resolution_seq
|
|
198
207
|
ON _syncular_commit_outcomes(resolution, seq)`);
|
|
199
208
|
db.exec(`CREATE TABLE IF NOT EXISTS _syncular_subscriptions(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Syncular TypeScript client core — offline-first sync over SQLite (WASM/OPFS, Bun, Node)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
83
|
"@sqlite.org/sqlite-wasm": "^3.53.0-build1",
|
|
84
|
-
"@syncular/core": "0.
|
|
84
|
+
"@syncular/core": "0.11.0"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"better-sqlite3": ">=11"
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
|
-
"@syncular/server": "0.
|
|
95
|
+
"@syncular/server": "0.11.0",
|
|
96
96
|
"@types/better-sqlite3": "^7.6.13",
|
|
97
97
|
"better-sqlite3": "^12.11.1"
|
|
98
98
|
}
|
package/src/client.ts
CHANGED
|
@@ -1737,6 +1737,7 @@ export class SyncClient {
|
|
|
1737
1737
|
status: 'rejected',
|
|
1738
1738
|
recordedAtMs: this.#now(),
|
|
1739
1739
|
results: [{ status: 'error', rejection }],
|
|
1740
|
+
operations: commit.operations,
|
|
1740
1741
|
});
|
|
1741
1742
|
pruneCommitOutcomes(this.#db, this.#outcomeRetentionMaxEntries);
|
|
1742
1743
|
batch.status();
|
|
@@ -2543,6 +2544,7 @@ export class SyncClient {
|
|
|
2543
2544
|
: 'rejected',
|
|
2544
2545
|
recordedAtMs: this.#now(),
|
|
2545
2546
|
results: outcomeResults,
|
|
2547
|
+
operations: commit.operations,
|
|
2546
2548
|
});
|
|
2547
2549
|
pruneCommitOutcomes(this.#db, this.#outcomeRetentionMaxEntries);
|
|
2548
2550
|
batch.outcomes();
|
|
@@ -2900,6 +2902,7 @@ export class SyncClient {
|
|
|
2900
2902
|
status: 'rejected',
|
|
2901
2903
|
recordedAtMs: this.#now(),
|
|
2902
2904
|
results,
|
|
2905
|
+
operations: commit.operations,
|
|
2903
2906
|
});
|
|
2904
2907
|
}
|
|
2905
2908
|
pruneCommitOutcomes(this.#db, this.#outcomeRetentionMaxEntries);
|
package/src/outcomes.ts
CHANGED
|
@@ -70,6 +70,12 @@ export interface CommitOutcome {
|
|
|
70
70
|
readonly status: CommitOutcomeStatus;
|
|
71
71
|
readonly recordedAtMs: number;
|
|
72
72
|
readonly results: readonly CommitOperationOutcome[];
|
|
73
|
+
/**
|
|
74
|
+
* Complete local failed-commit envelope, retained after outbox drain so an
|
|
75
|
+
* authorized application can reconstruct atomic aggregate intent. Absent
|
|
76
|
+
* for successful and historical outcomes. Never sent over the wire.
|
|
77
|
+
*/
|
|
78
|
+
readonly operations?: readonly OutboxOperation[];
|
|
73
79
|
readonly resolution: CommitOutcomeResolution;
|
|
74
80
|
readonly resolvedAtMs?: number;
|
|
75
81
|
readonly replacementClientCommitId?: string;
|
|
@@ -142,6 +148,9 @@ function parseOutcome(row: Readonly<Record<string, unknown>>): CommitOutcome {
|
|
|
142
148
|
status: row.status as CommitOutcomeStatus,
|
|
143
149
|
recordedAtMs: row.recorded_at_ms as number,
|
|
144
150
|
results: decodeResults(row.results as string),
|
|
151
|
+
...(typeof row.operations === 'string'
|
|
152
|
+
? { operations: JSON.parse(row.operations) as OutboxOperation[] }
|
|
153
|
+
: {}),
|
|
145
154
|
resolution: row.resolution as CommitOutcomeResolution,
|
|
146
155
|
...(typeof row.resolved_at_ms === 'number'
|
|
147
156
|
? { resolvedAtMs: row.resolved_at_ms }
|
|
@@ -158,13 +167,16 @@ export function recordCommitOutcome(
|
|
|
158
167
|
): CommitOutcome {
|
|
159
168
|
db.exec(
|
|
160
169
|
`INSERT INTO _syncular_commit_outcomes(
|
|
161
|
-
client_commit_id, status, recorded_at_ms, results, resolution
|
|
162
|
-
) VALUES (?, ?, ?, ?, 'active')`,
|
|
170
|
+
client_commit_id, status, recorded_at_ms, results, operations, resolution
|
|
171
|
+
) VALUES (?, ?, ?, ?, ?, 'active')`,
|
|
163
172
|
[
|
|
164
173
|
outcome.clientCommitId,
|
|
165
174
|
outcome.status,
|
|
166
175
|
outcome.recordedAtMs,
|
|
167
176
|
encodeResults(outcome.results),
|
|
177
|
+
outcome.operations === undefined
|
|
178
|
+
? null
|
|
179
|
+
: JSON.stringify(outcome.operations),
|
|
168
180
|
],
|
|
169
181
|
);
|
|
170
182
|
return commitOutcome(db, outcome.clientCommitId) as CommitOutcome;
|
|
@@ -175,7 +187,7 @@ export function commitOutcome(
|
|
|
175
187
|
clientCommitId: string,
|
|
176
188
|
): CommitOutcome | undefined {
|
|
177
189
|
const row = db.query(
|
|
178
|
-
`SELECT seq, client_commit_id, status, recorded_at_ms, results,
|
|
190
|
+
`SELECT seq, client_commit_id, status, recorded_at_ms, results, operations,
|
|
179
191
|
resolution, resolved_at_ms, replacement_client_commit_id
|
|
180
192
|
FROM _syncular_commit_outcomes WHERE client_commit_id = ?`,
|
|
181
193
|
[clientCommitId],
|
|
@@ -198,7 +210,7 @@ export function listCommitOutcomes(
|
|
|
198
210
|
? "WHERE resolution = 'active' AND status IN ('conflict', 'rejected')"
|
|
199
211
|
: '';
|
|
200
212
|
const rows = db.query(
|
|
201
|
-
`SELECT seq, client_commit_id, status, recorded_at_ms, results,
|
|
213
|
+
`SELECT seq, client_commit_id, status, recorded_at_ms, results, operations,
|
|
202
214
|
resolution, resolved_at_ms, replacement_client_commit_id
|
|
203
215
|
FROM _syncular_commit_outcomes ${where}
|
|
204
216
|
ORDER BY seq DESC${limit === undefined ? '' : ' LIMIT ?'}`,
|
package/src/schema.ts
CHANGED
|
@@ -294,10 +294,20 @@ export function ensureLocalSchema(
|
|
|
294
294
|
status TEXT NOT NULL CHECK(status IN ('applied', 'cached', 'conflict', 'rejected')),
|
|
295
295
|
recorded_at_ms INTEGER NOT NULL,
|
|
296
296
|
results TEXT NOT NULL,
|
|
297
|
+
operations TEXT,
|
|
297
298
|
resolution TEXT NOT NULL DEFAULT 'active'
|
|
298
299
|
CHECK(resolution IN ('active', 'resolved_keep_server', 'superseded', 'dismissed')),
|
|
299
300
|
resolved_at_ms INTEGER,
|
|
300
301
|
replacement_client_commit_id TEXT)`);
|
|
302
|
+
// Outcomes created before durable aggregate recovery retain no commit
|
|
303
|
+
// envelope. New failed outcomes store it in the protected client DB.
|
|
304
|
+
try {
|
|
305
|
+
db.exec(
|
|
306
|
+
'ALTER TABLE _syncular_commit_outcomes ADD COLUMN operations TEXT',
|
|
307
|
+
);
|
|
308
|
+
} catch {
|
|
309
|
+
// column already exists — the CREATE above included it
|
|
310
|
+
}
|
|
301
311
|
db.exec(`CREATE INDEX IF NOT EXISTS _syncular_commit_outcomes_resolution_seq
|
|
302
312
|
ON _syncular_commit_outcomes(resolution, seq)`);
|
|
303
313
|
db.exec(`CREATE TABLE IF NOT EXISTS _syncular_subscriptions(
|