@syncular/client 0.7.0 → 0.9.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 +25 -0
- package/dist/client.d.ts +21 -25
- package/dist/client.js +170 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/invalidation.d.ts +2 -0
- package/dist/invalidation.js +7 -1
- package/dist/outbox.d.ts +5 -0
- package/dist/outcomes.d.ts +81 -0
- package/dist/outcomes.js +131 -0
- package/dist/reactive-store.d.ts +8 -1
- package/dist/reactive-store.js +17 -0
- package/dist/schema.js +12 -0
- package/dist/worker-entry.js +3 -0
- package/dist/worker-host.d.ts +4 -0
- package/dist/worker-host.js +9 -0
- package/dist/worker-protocol.d.ts +4 -0
- package/package.json +3 -3
- package/src/client.ts +265 -43
- package/src/index.ts +1 -0
- package/src/invalidation.ts +9 -1
- package/src/outbox.ts +5 -0
- package/src/outcomes.ts +276 -0
- package/src/reactive-store.ts +28 -0
- package/src/schema.ts +12 -0
- package/src/worker-entry.ts +5 -0
- package/src/worker-host.ts +21 -0
- package/src/worker-protocol.ts +8 -0
package/src/outcomes.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable per-client commit outcomes.
|
|
3
|
+
*
|
|
4
|
+
* The journal is client-local protected database state. A final push result is
|
|
5
|
+
* written in the same SQLite transaction that drains its outbox commit, so a
|
|
6
|
+
* restart can never turn "rejected" into an inferred success. Conflict payloads
|
|
7
|
+
* deliberately stay local; retention never deletes an unresolved failure.
|
|
8
|
+
*/
|
|
9
|
+
import type { RejectionDetails, RowValue } from '@syncular/core';
|
|
10
|
+
import type { ClientDatabase } from './database';
|
|
11
|
+
import { ClientSyncError } from './errors';
|
|
12
|
+
import type { OutboxOperation } from './outbox';
|
|
13
|
+
import { type JsonRowValue, jsonToRowValue, rowValueToJson } from './schema';
|
|
14
|
+
|
|
15
|
+
export interface ConflictRecord {
|
|
16
|
+
readonly clientCommitId: string;
|
|
17
|
+
readonly opIndex: number;
|
|
18
|
+
readonly table: string;
|
|
19
|
+
readonly rowId: string;
|
|
20
|
+
readonly code: string;
|
|
21
|
+
readonly message: string;
|
|
22
|
+
readonly serverVersion: number;
|
|
23
|
+
/** The current server row, decoded — resolve without a round-trip. */
|
|
24
|
+
readonly serverRow: Readonly<Record<string, RowValue>>;
|
|
25
|
+
/** The losing local operation (absent only for malformed op indexes). */
|
|
26
|
+
readonly operation?: OutboxOperation;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RejectionRecord {
|
|
30
|
+
readonly clientCommitId: string;
|
|
31
|
+
readonly opIndex: number;
|
|
32
|
+
readonly code: string;
|
|
33
|
+
readonly message: string;
|
|
34
|
+
readonly retryable: boolean;
|
|
35
|
+
/** Bounded host-declared metadata safe for authorized recovery UI. */
|
|
36
|
+
readonly details?: RejectionDetails;
|
|
37
|
+
readonly operation?: OutboxOperation;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type CommitOutcomeStatus =
|
|
41
|
+
| 'applied'
|
|
42
|
+
| 'cached'
|
|
43
|
+
| 'conflict'
|
|
44
|
+
| 'rejected';
|
|
45
|
+
|
|
46
|
+
export type CommitOutcomeResolution =
|
|
47
|
+
| 'active'
|
|
48
|
+
| 'resolved_keep_server'
|
|
49
|
+
| 'superseded'
|
|
50
|
+
| 'dismissed';
|
|
51
|
+
|
|
52
|
+
export type CommitOperationOutcome =
|
|
53
|
+
| {
|
|
54
|
+
readonly status: 'applied';
|
|
55
|
+
readonly opIndex: number;
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
readonly status: 'conflict';
|
|
59
|
+
readonly conflict: ConflictRecord;
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
readonly status: 'error';
|
|
63
|
+
readonly rejection: RejectionRecord;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export interface CommitOutcome {
|
|
67
|
+
/** Monotonic local journal order; not a server sequence. */
|
|
68
|
+
readonly sequence: number;
|
|
69
|
+
readonly clientCommitId: string;
|
|
70
|
+
readonly status: CommitOutcomeStatus;
|
|
71
|
+
readonly recordedAtMs: number;
|
|
72
|
+
readonly results: readonly CommitOperationOutcome[];
|
|
73
|
+
readonly resolution: CommitOutcomeResolution;
|
|
74
|
+
readonly resolvedAtMs?: number;
|
|
75
|
+
readonly replacementClientCommitId?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CommitOutcomeQuery {
|
|
79
|
+
/** Newest-first result cap. Defaults to all retained entries. */
|
|
80
|
+
readonly limit?: number;
|
|
81
|
+
/** Only unresolved conflict/rejection outcomes. */
|
|
82
|
+
readonly activeOnly?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ResolveCommitOutcomeInput {
|
|
86
|
+
readonly clientCommitId: string;
|
|
87
|
+
readonly resolution: Exclude<CommitOutcomeResolution, 'active'>;
|
|
88
|
+
readonly replacementClientCommitId?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface StoredConflictRecord extends Omit<ConflictRecord, 'serverRow'> {
|
|
92
|
+
readonly serverRow: Readonly<Record<string, JsonRowValue>>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type StoredCommitOperationOutcome =
|
|
96
|
+
| Extract<CommitOperationOutcome, { status: 'applied' }>
|
|
97
|
+
| { readonly status: 'conflict'; readonly conflict: StoredConflictRecord }
|
|
98
|
+
| Extract<CommitOperationOutcome, { status: 'error' }>;
|
|
99
|
+
|
|
100
|
+
function encodeResults(results: readonly CommitOperationOutcome[]): string {
|
|
101
|
+
const stored: StoredCommitOperationOutcome[] = results.map((result) => {
|
|
102
|
+
if (result.status !== 'conflict') return result;
|
|
103
|
+
return {
|
|
104
|
+
status: 'conflict',
|
|
105
|
+
conflict: {
|
|
106
|
+
...result.conflict,
|
|
107
|
+
serverRow: Object.fromEntries(
|
|
108
|
+
Object.entries(result.conflict.serverRow).map(([key, value]) => [
|
|
109
|
+
key,
|
|
110
|
+
rowValueToJson(value),
|
|
111
|
+
]),
|
|
112
|
+
),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
return JSON.stringify(stored);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function decodeResults(raw: string): CommitOperationOutcome[] {
|
|
120
|
+
const stored = JSON.parse(raw) as StoredCommitOperationOutcome[];
|
|
121
|
+
return stored.map((result) => {
|
|
122
|
+
if (result.status !== 'conflict') return result;
|
|
123
|
+
return {
|
|
124
|
+
status: 'conflict',
|
|
125
|
+
conflict: {
|
|
126
|
+
...result.conflict,
|
|
127
|
+
serverRow: Object.fromEntries(
|
|
128
|
+
Object.entries(result.conflict.serverRow).map(([key, value]) => [
|
|
129
|
+
key,
|
|
130
|
+
jsonToRowValue(value),
|
|
131
|
+
]),
|
|
132
|
+
),
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function parseOutcome(row: Readonly<Record<string, unknown>>): CommitOutcome {
|
|
139
|
+
return {
|
|
140
|
+
sequence: row.seq as number,
|
|
141
|
+
clientCommitId: row.client_commit_id as string,
|
|
142
|
+
status: row.status as CommitOutcomeStatus,
|
|
143
|
+
recordedAtMs: row.recorded_at_ms as number,
|
|
144
|
+
results: decodeResults(row.results as string),
|
|
145
|
+
resolution: row.resolution as CommitOutcomeResolution,
|
|
146
|
+
...(typeof row.resolved_at_ms === 'number'
|
|
147
|
+
? { resolvedAtMs: row.resolved_at_ms }
|
|
148
|
+
: {}),
|
|
149
|
+
...(typeof row.replacement_client_commit_id === 'string'
|
|
150
|
+
? { replacementClientCommitId: row.replacement_client_commit_id }
|
|
151
|
+
: {}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function recordCommitOutcome(
|
|
156
|
+
db: ClientDatabase,
|
|
157
|
+
outcome: Omit<CommitOutcome, 'sequence' | 'resolution'>,
|
|
158
|
+
): CommitOutcome {
|
|
159
|
+
db.exec(
|
|
160
|
+
`INSERT INTO _syncular_commit_outcomes(
|
|
161
|
+
client_commit_id, status, recorded_at_ms, results, resolution
|
|
162
|
+
) VALUES (?, ?, ?, ?, 'active')`,
|
|
163
|
+
[
|
|
164
|
+
outcome.clientCommitId,
|
|
165
|
+
outcome.status,
|
|
166
|
+
outcome.recordedAtMs,
|
|
167
|
+
encodeResults(outcome.results),
|
|
168
|
+
],
|
|
169
|
+
);
|
|
170
|
+
return commitOutcome(db, outcome.clientCommitId) as CommitOutcome;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function commitOutcome(
|
|
174
|
+
db: ClientDatabase,
|
|
175
|
+
clientCommitId: string,
|
|
176
|
+
): CommitOutcome | undefined {
|
|
177
|
+
const row = db.query(
|
|
178
|
+
`SELECT seq, client_commit_id, status, recorded_at_ms, results,
|
|
179
|
+
resolution, resolved_at_ms, replacement_client_commit_id
|
|
180
|
+
FROM _syncular_commit_outcomes WHERE client_commit_id = ?`,
|
|
181
|
+
[clientCommitId],
|
|
182
|
+
)[0];
|
|
183
|
+
return row === undefined ? undefined : parseOutcome(row);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function listCommitOutcomes(
|
|
187
|
+
db: ClientDatabase,
|
|
188
|
+
query: CommitOutcomeQuery = {},
|
|
189
|
+
): CommitOutcome[] {
|
|
190
|
+
const limit = query.limit;
|
|
191
|
+
if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1)) {
|
|
192
|
+
throw new ClientSyncError(
|
|
193
|
+
'sync.invalid_request',
|
|
194
|
+
'commit outcome limit must be a positive safe integer',
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const where = query.activeOnly
|
|
198
|
+
? "WHERE resolution = 'active' AND status IN ('conflict', 'rejected')"
|
|
199
|
+
: '';
|
|
200
|
+
const rows = db.query(
|
|
201
|
+
`SELECT seq, client_commit_id, status, recorded_at_ms, results,
|
|
202
|
+
resolution, resolved_at_ms, replacement_client_commit_id
|
|
203
|
+
FROM _syncular_commit_outcomes ${where}
|
|
204
|
+
ORDER BY seq DESC${limit === undefined ? '' : ' LIMIT ?'}`,
|
|
205
|
+
limit === undefined ? [] : [limit],
|
|
206
|
+
);
|
|
207
|
+
return rows.map(parseOutcome);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function persistCommitOutcomeResolution(
|
|
211
|
+
db: ClientDatabase,
|
|
212
|
+
input: ResolveCommitOutcomeInput,
|
|
213
|
+
nowMs: number,
|
|
214
|
+
): CommitOutcome | undefined {
|
|
215
|
+
db.exec(
|
|
216
|
+
`UPDATE _syncular_commit_outcomes
|
|
217
|
+
SET resolution = ?, resolved_at_ms = ?, replacement_client_commit_id = ?
|
|
218
|
+
WHERE client_commit_id = ? AND resolution = 'active'`,
|
|
219
|
+
[
|
|
220
|
+
input.resolution,
|
|
221
|
+
nowMs,
|
|
222
|
+
input.replacementClientCommitId ?? null,
|
|
223
|
+
input.clientCommitId,
|
|
224
|
+
],
|
|
225
|
+
);
|
|
226
|
+
return commitOutcome(db, input.clientCommitId);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Bound journal growth without deleting active failures. If active failures
|
|
231
|
+
* alone exceed the cap the journal intentionally remains over-capacity.
|
|
232
|
+
*/
|
|
233
|
+
export function pruneCommitOutcomes(
|
|
234
|
+
db: ClientDatabase,
|
|
235
|
+
maxEntries: number,
|
|
236
|
+
): number {
|
|
237
|
+
if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) {
|
|
238
|
+
throw new ClientSyncError(
|
|
239
|
+
'sync.invalid_request',
|
|
240
|
+
'outcome retention maxEntries must be a positive safe integer',
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const count = db.query(
|
|
244
|
+
'SELECT COUNT(*) AS count FROM _syncular_commit_outcomes',
|
|
245
|
+
)[0]?.count as number | undefined;
|
|
246
|
+
const excess = Math.max(0, (count ?? 0) - maxEntries);
|
|
247
|
+
if (excess === 0) return 0;
|
|
248
|
+
const candidates = db.query(
|
|
249
|
+
`SELECT seq FROM _syncular_commit_outcomes
|
|
250
|
+
WHERE status IN ('applied', 'cached') OR resolution != 'active'
|
|
251
|
+
ORDER BY seq ASC LIMIT ?`,
|
|
252
|
+
[excess],
|
|
253
|
+
);
|
|
254
|
+
for (const candidate of candidates) {
|
|
255
|
+
db.exec('DELETE FROM _syncular_commit_outcomes WHERE seq = ?', [
|
|
256
|
+
candidate.seq as number,
|
|
257
|
+
]);
|
|
258
|
+
}
|
|
259
|
+
return candidates.length;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function activeFailureRecords(outcomes: readonly CommitOutcome[]): {
|
|
263
|
+
readonly conflicts: ConflictRecord[];
|
|
264
|
+
readonly rejections: RejectionRecord[];
|
|
265
|
+
} {
|
|
266
|
+
const conflicts: ConflictRecord[] = [];
|
|
267
|
+
const rejections: RejectionRecord[] = [];
|
|
268
|
+
for (const outcome of outcomes) {
|
|
269
|
+
if (outcome.resolution !== 'active') continue;
|
|
270
|
+
for (const result of outcome.results) {
|
|
271
|
+
if (result.status === 'conflict') conflicts.push(result.conflict);
|
|
272
|
+
if (result.status === 'error') rejections.push(result.rejection);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return { conflicts, rejections };
|
|
276
|
+
}
|
package/src/reactive-store.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
CommitOutcome,
|
|
2
3
|
QueryReadSpec,
|
|
3
4
|
QuerySnapshot,
|
|
4
5
|
WindowCoverage,
|
|
@@ -49,6 +50,9 @@ export interface ReactiveQueryClient {
|
|
|
49
50
|
readonly rejections:
|
|
50
51
|
| readonly unknown[]
|
|
51
52
|
| (() => readonly unknown[] | Promise<readonly unknown[]>);
|
|
53
|
+
commitOutcomes():
|
|
54
|
+
| readonly CommitOutcome[]
|
|
55
|
+
| Promise<readonly CommitOutcome[]>;
|
|
52
56
|
setWindow(base: WindowBase, units: readonly string[]): void | Promise<void>;
|
|
53
57
|
windowState(base: WindowBase): WindowState | Promise<WindowState>;
|
|
54
58
|
}
|
|
@@ -82,6 +86,12 @@ export interface ConflictStoreSnapshot<
|
|
|
82
86
|
readonly isLoading: boolean;
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
export interface OutcomeStoreSnapshot {
|
|
90
|
+
readonly outcomes: readonly CommitOutcome[];
|
|
91
|
+
readonly error: Error | undefined;
|
|
92
|
+
readonly isLoading: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
85
95
|
function errorOf(value: unknown): Error {
|
|
86
96
|
return value instanceof Error ? value : new Error(String(value));
|
|
87
97
|
}
|
|
@@ -497,6 +507,7 @@ export class ReactiveClientStore {
|
|
|
497
507
|
#offChange: (() => void) | undefined;
|
|
498
508
|
readonly status: ExternalStoreEntry<StatusStoreSnapshot>;
|
|
499
509
|
readonly conflicts: ExternalStoreEntry<ConflictStoreSnapshot>;
|
|
510
|
+
readonly outcomes: ExternalStoreEntry<OutcomeStoreSnapshot>;
|
|
500
511
|
|
|
501
512
|
constructor(readonly client: ReactiveQueryClient) {
|
|
502
513
|
const status = new ValueEntry<StatusStoreSnapshot>(
|
|
@@ -537,10 +548,26 @@ export class ReactiveClientStore {
|
|
|
537
548
|
}
|
|
538
549
|
},
|
|
539
550
|
);
|
|
551
|
+
const outcomes = new ValueEntry<OutcomeStoreSnapshot>(
|
|
552
|
+
{ outcomes: [], error: undefined, isLoading: true },
|
|
553
|
+
async () => {
|
|
554
|
+
try {
|
|
555
|
+
return {
|
|
556
|
+
outcomes: await client.commitOutcomes(),
|
|
557
|
+
error: undefined,
|
|
558
|
+
isLoading: false,
|
|
559
|
+
};
|
|
560
|
+
} catch (error) {
|
|
561
|
+
return { outcomes: [], error: errorOf(error), isLoading: false };
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
);
|
|
540
565
|
this.status = status;
|
|
541
566
|
this.conflicts = conflicts;
|
|
567
|
+
this.outcomes = outcomes;
|
|
542
568
|
status.refresh();
|
|
543
569
|
conflicts.refresh();
|
|
570
|
+
outcomes.refresh();
|
|
544
571
|
this.start();
|
|
545
572
|
}
|
|
546
573
|
|
|
@@ -681,6 +708,7 @@ export class ReactiveClientStore {
|
|
|
681
708
|
if (batch.conflictsChanged || batch.rejectionsChanged) {
|
|
682
709
|
this.conflicts.refresh();
|
|
683
710
|
}
|
|
711
|
+
if (batch.outcomesChanged) this.outcomes.refresh();
|
|
684
712
|
});
|
|
685
713
|
}
|
|
686
714
|
|
package/src/schema.ts
CHANGED
|
@@ -288,6 +288,18 @@ export function ensureLocalSchema(
|
|
|
288
288
|
client_commit_id TEXT NOT NULL UNIQUE,
|
|
289
289
|
created_at_ms INTEGER NOT NULL,
|
|
290
290
|
operations TEXT NOT NULL)`);
|
|
291
|
+
db.exec(`CREATE TABLE IF NOT EXISTS _syncular_commit_outcomes(
|
|
292
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
293
|
+
client_commit_id TEXT NOT NULL UNIQUE,
|
|
294
|
+
status TEXT NOT NULL CHECK(status IN ('applied', 'cached', 'conflict', 'rejected')),
|
|
295
|
+
recorded_at_ms INTEGER NOT NULL,
|
|
296
|
+
results TEXT NOT NULL,
|
|
297
|
+
resolution TEXT NOT NULL DEFAULT 'active'
|
|
298
|
+
CHECK(resolution IN ('active', 'resolved_keep_server', 'superseded', 'dismissed')),
|
|
299
|
+
resolved_at_ms INTEGER,
|
|
300
|
+
replacement_client_commit_id TEXT)`);
|
|
301
|
+
db.exec(`CREATE INDEX IF NOT EXISTS _syncular_commit_outcomes_resolution_seq
|
|
302
|
+
ON _syncular_commit_outcomes(resolution, seq)`);
|
|
291
303
|
db.exec(`CREATE TABLE IF NOT EXISTS _syncular_subscriptions(
|
|
292
304
|
id TEXT PRIMARY KEY,
|
|
293
305
|
tbl TEXT NOT NULL,
|
package/src/worker-entry.ts
CHANGED
|
@@ -378,6 +378,11 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
|
|
|
378
378
|
statusSnapshot: () => requireClient().statusSnapshot(),
|
|
379
379
|
conflicts: () => requireClient().conflicts,
|
|
380
380
|
rejections: () => requireClient().rejections,
|
|
381
|
+
commitOutcome: (clientCommitId) =>
|
|
382
|
+
requireClient().commitOutcome(clientCommitId),
|
|
383
|
+
commitOutcomes: (query) => requireClient().commitOutcomes(query),
|
|
384
|
+
resolveCommitOutcome: (input) =>
|
|
385
|
+
requireClient().resolveCommitOutcome(input),
|
|
381
386
|
schemaFloor: () => requireClient().schemaFloor,
|
|
382
387
|
leaseState: () => requireClient().leaseState,
|
|
383
388
|
upgrading: () => requireClient().upgrading,
|
package/src/worker-host.ts
CHANGED
|
@@ -64,6 +64,11 @@ import {
|
|
|
64
64
|
newTabId,
|
|
65
65
|
} from './multi-tab';
|
|
66
66
|
import type { OutboxCommit } from './outbox';
|
|
67
|
+
import type {
|
|
68
|
+
CommitOutcome,
|
|
69
|
+
CommitOutcomeQuery,
|
|
70
|
+
ResolveCommitOutcomeInput,
|
|
71
|
+
} from './outcomes';
|
|
67
72
|
import type { ClientSchema } from './schema';
|
|
68
73
|
import type { SubscriptionRecord } from './state';
|
|
69
74
|
import type { WindowBase } from './window';
|
|
@@ -385,6 +390,22 @@ export class SyncClientHandle {
|
|
|
385
390
|
return this.#call('rejections', []);
|
|
386
391
|
}
|
|
387
392
|
|
|
393
|
+
commitOutcome(clientCommitId: string): Promise<CommitOutcome | undefined> {
|
|
394
|
+
return this.#call('commitOutcome', [clientCommitId]);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
commitOutcomes(
|
|
398
|
+
query: CommitOutcomeQuery = {},
|
|
399
|
+
): Promise<readonly CommitOutcome[]> {
|
|
400
|
+
return this.#call('commitOutcomes', [query]);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
resolveCommitOutcome(
|
|
404
|
+
input: ResolveCommitOutcomeInput,
|
|
405
|
+
): Promise<CommitOutcome> {
|
|
406
|
+
return this.#call('resolveCommitOutcome', [input]);
|
|
407
|
+
}
|
|
408
|
+
|
|
388
409
|
schemaFloor(): Promise<SchemaFloor | undefined> {
|
|
389
410
|
return this.#call('schemaFloor', []);
|
|
390
411
|
}
|
package/src/worker-protocol.ts
CHANGED
|
@@ -39,6 +39,11 @@ import type {
|
|
|
39
39
|
SyncStatusSnapshot,
|
|
40
40
|
} from './invalidation';
|
|
41
41
|
import type { OutboxCommit } from './outbox';
|
|
42
|
+
import type {
|
|
43
|
+
CommitOutcome,
|
|
44
|
+
CommitOutcomeQuery,
|
|
45
|
+
ResolveCommitOutcomeInput,
|
|
46
|
+
} from './outcomes';
|
|
42
47
|
import type { ClientSchema } from './schema';
|
|
43
48
|
import type { SubscriptionRecord } from './state';
|
|
44
49
|
import type { WindowBase } from './window';
|
|
@@ -133,6 +138,9 @@ export interface WorkerApi {
|
|
|
133
138
|
statusSnapshot(): SyncStatusSnapshot;
|
|
134
139
|
conflicts(): readonly ConflictRecord[];
|
|
135
140
|
rejections(): readonly RejectionRecord[];
|
|
141
|
+
commitOutcome(clientCommitId: string): CommitOutcome | undefined;
|
|
142
|
+
commitOutcomes(query?: CommitOutcomeQuery): readonly CommitOutcome[];
|
|
143
|
+
resolveCommitOutcome(input: ResolveCommitOutcomeInput): CommitOutcome;
|
|
136
144
|
schemaFloor(): SchemaFloor | undefined;
|
|
137
145
|
/** §7.3.5: the opaque auth-lease state, or undefined. */
|
|
138
146
|
leaseState(): LeaseState | undefined;
|