@syncular/client 0.30.16 → 0.31.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 +20 -0
- package/dist/client.d.ts +102 -2
- package/dist/client.js +668 -146
- package/dist/diagnostics.d.ts +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +8 -3
- package/dist/failed-overlay.js +12 -8
- package/dist/http.d.ts +3 -3
- package/dist/http.js +88 -61
- package/dist/outcomes.d.ts +9 -1
- package/dist/outcomes.js +186 -34
- package/dist/schema.d.ts +8 -0
- package/dist/schema.js +31 -1
- package/dist/state.js +5 -4
- package/dist/sync-scheduler.js +8 -0
- package/dist/worker-entry.js +3 -0
- package/dist/worker-host.d.ts +3 -2
- package/dist/worker-host.js +3 -0
- package/dist/worker-protocol.d.ts +2 -2
- package/package.json +3 -3
- package/src/client.ts +936 -185
- package/src/diagnostics.ts +2 -0
- package/src/errors.ts +10 -4
- package/src/failed-overlay.ts +10 -15
- package/src/http.ts +198 -96
- package/src/outcomes.ts +230 -46
- package/src/schema.ts +42 -1
- package/src/state.ts +14 -4
- package/src/sync-scheduler.ts +10 -0
- package/src/worker-entry.ts +2 -0
- package/src/worker-host.ts +10 -1
- package/src/worker-protocol.ts +2 -1
package/dist/diagnostics.d.ts
CHANGED
|
@@ -113,7 +113,7 @@ export interface DiagnosticQueryFailure {
|
|
|
113
113
|
readonly id: string;
|
|
114
114
|
/** Distinct generated table names the query depends on, ascending. */
|
|
115
115
|
readonly tables: readonly string[];
|
|
116
|
-
readonly code: 'client.storage_corrupt' | 'client.storage_full' | 'client.storage_io' | 'client.query_failed';
|
|
116
|
+
readonly code: 'client.storage_corrupt' | 'client.storage_full' | 'client.storage_io' | 'client.storage_busy' | 'sync.local_corrupt' | 'client.query_failed';
|
|
117
117
|
/** SQLite extended result code, when the driver exposed one. */
|
|
118
118
|
readonly sqliteCode?: number;
|
|
119
119
|
/** First failure since this id's last successful read. */
|
package/dist/errors.d.ts
CHANGED
|
@@ -65,6 +65,6 @@ export declare class RealtimeUnavailableError extends ClientSyncError {
|
|
|
65
65
|
*/
|
|
66
66
|
export declare function classifySqliteFailure(error: unknown): {
|
|
67
67
|
readonly sqliteCode: number | undefined;
|
|
68
|
-
readonly code: typeof STORAGE_CORRUPT_CODE | typeof STORAGE_FULL_CODE | typeof STORAGE_IO_CODE | undefined;
|
|
68
|
+
readonly code: typeof STORAGE_CORRUPT_CODE | typeof STORAGE_FULL_CODE | typeof STORAGE_IO_CODE | typeof STORAGE_BUSY_CODE | undefined;
|
|
69
69
|
readonly error: unknown;
|
|
70
70
|
};
|
package/dist/errors.js
CHANGED
|
@@ -98,7 +98,8 @@ export function classifySqliteFailure(error) {
|
|
|
98
98
|
sqliteCode: typeof sqliteCode === 'number' ? sqliteCode : undefined,
|
|
99
99
|
code: error.code === STORAGE_FULL_CODE ||
|
|
100
100
|
error.code === STORAGE_IO_CODE ||
|
|
101
|
-
error.code === STORAGE_CORRUPT_CODE
|
|
101
|
+
error.code === STORAGE_CORRUPT_CODE ||
|
|
102
|
+
error.code === STORAGE_BUSY_CODE
|
|
102
103
|
? error.code
|
|
103
104
|
: undefined,
|
|
104
105
|
error: retained,
|
|
@@ -124,7 +125,9 @@ export function classifySqliteFailure(error) {
|
|
|
124
125
|
? STORAGE_IO_CODE
|
|
125
126
|
: primary === 11 || primary === 26
|
|
126
127
|
? STORAGE_CORRUPT_CODE
|
|
127
|
-
:
|
|
128
|
+
: primary === 5 || primary === 6
|
|
129
|
+
? STORAGE_BUSY_CODE
|
|
130
|
+
: undefined;
|
|
128
131
|
if (code === undefined)
|
|
129
132
|
return { sqliteCode, code, error };
|
|
130
133
|
const rollback = error instanceof Error && 'rollbackError' in error
|
|
@@ -134,7 +137,9 @@ export function classifySqliteFailure(error) {
|
|
|
134
137
|
? 'local SQLite storage is full'
|
|
135
138
|
: primary === 10
|
|
136
139
|
? 'local SQLite storage I/O failed'
|
|
137
|
-
:
|
|
140
|
+
: code === STORAGE_BUSY_CODE
|
|
141
|
+
? 'local SQLite storage is busy'
|
|
142
|
+
: 'local SQLite storage is corrupt', code === STORAGE_BUSY_CODE, {
|
|
138
143
|
sqliteCode,
|
|
139
144
|
sqliteMessage: error instanceof Error ? error.message : String(error),
|
|
140
145
|
...(rollback !== undefined
|
package/dist/failed-overlay.js
CHANGED
|
@@ -96,12 +96,6 @@ export function overlayScopePredicate(table, id) {
|
|
|
96
96
|
function retainedRows(db, scope, acknowledged) {
|
|
97
97
|
return db.query(`SELECT * FROM ${RETAINED_ROWS} WHERE (${overlayScopePredicate('tbl', 'id')})${acknowledged === undefined ? '' : ` AND commit_seq IS${acknowledged ? ' NOT' : ''} NULL`} ORDER BY commit_seq,at,commit_id,idx`, [scope ? JSON.stringify(scope) : null]);
|
|
98
98
|
}
|
|
99
|
-
function retainedTable(schema, name) {
|
|
100
|
-
const table = schema.tables.get(name);
|
|
101
|
-
if (!table)
|
|
102
|
-
throw new ClientSyncError('sync.unknown_table', 'retained table is unknown');
|
|
103
|
-
return table;
|
|
104
|
-
}
|
|
105
99
|
export function restoreFailedBases(db, schema, scope) {
|
|
106
100
|
const seen = new Set();
|
|
107
101
|
for (const row of retainedRows(db, scope)) {
|
|
@@ -109,7 +103,12 @@ export function restoreFailedBases(db, schema, scope) {
|
|
|
109
103
|
if (seen.has(key))
|
|
110
104
|
continue;
|
|
111
105
|
seen.add(key);
|
|
112
|
-
|
|
106
|
+
// §7.4.4: a retained row whose table the current schema removed has no
|
|
107
|
+
// mirror to project; its persisted intent and outcome stay available, only
|
|
108
|
+
// this projection is skipped.
|
|
109
|
+
const table = schema.tables.get(row.tbl);
|
|
110
|
+
if (table === undefined)
|
|
111
|
+
continue;
|
|
113
112
|
if (row.base === null)
|
|
114
113
|
deleteLocalRow(db, table, row.id, false);
|
|
115
114
|
else {
|
|
@@ -125,7 +124,12 @@ export function failedOverlayCommits(db, acknowledged = false, scope, schema) {
|
|
|
125
124
|
const id = row.commit_id;
|
|
126
125
|
let operation = JSON.parse(row.op);
|
|
127
126
|
if (schema && operation.op === 'upsert') {
|
|
128
|
-
|
|
127
|
+
// §7.4.4: the table this retained row belongs to is gone; the persisted
|
|
128
|
+
// outcome and intent stay available, only the removed-table projection is
|
|
129
|
+
// skipped.
|
|
130
|
+
const table = schema.tables.get(operation.table);
|
|
131
|
+
if (table === undefined)
|
|
132
|
+
continue;
|
|
129
133
|
if (acknowledged) {
|
|
130
134
|
if (!db.query(`SELECT 1 FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}=?`, [operation.rowId]).length)
|
|
131
135
|
operation = {
|
package/dist/http.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser transport bindings (§1.1, §5.4/§5.5, §8.1): fetch-based sync
|
|
3
|
-
* transport,
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* transport, descriptor-selected signed-URL or direct segment delivery, and
|
|
4
|
+
* WebSocket realtime connectors. Adapter tests inject fetch and socket surfaces;
|
|
5
|
+
* protocol core conformance uses loopback.
|
|
6
6
|
*/
|
|
7
7
|
import type { BlobTransport } from './blob.js';
|
|
8
8
|
import type { RealtimeConnector, RemoteOperationTransport, RemoteOperationRealtimeConnector, SegmentDownloader, SyncTransport } from './transport.js';
|
package/dist/http.js
CHANGED
|
@@ -1,23 +1,50 @@
|
|
|
1
1
|
import { SSP2_CONTENT_TYPE } from './content-type.js';
|
|
2
2
|
import { ClientSyncError } from './errors.js';
|
|
3
|
-
async function
|
|
4
|
-
|
|
3
|
+
async function transfer(message, operation, httpStatus) {
|
|
4
|
+
try {
|
|
5
|
+
return await operation();
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
// Classify only known exception kinds. Injected errors, including
|
|
9
|
+
// ClientSyncError, are untrusted and never become the returned cause.
|
|
10
|
+
const causeKind = error instanceof DOMException && error.name === 'TimeoutError'
|
|
11
|
+
? 'timeout'
|
|
12
|
+
: error instanceof DOMException && error.name === 'AbortError'
|
|
13
|
+
? 'aborted'
|
|
14
|
+
: httpStatus !== undefined
|
|
15
|
+
? 'body'
|
|
16
|
+
: error instanceof TypeError
|
|
17
|
+
? 'network'
|
|
18
|
+
: 'unknown';
|
|
19
|
+
throw new ClientSyncError('sync.transport_failed', message, true, {
|
|
20
|
+
causeKind,
|
|
21
|
+
...(httpStatus !== undefined ? { httpStatus } : {}),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function throwHttpError(response, defaultCode = 'sync.transport_failed') {
|
|
26
|
+
let code = defaultCode;
|
|
5
27
|
let message = 'HTTP request failed';
|
|
6
|
-
let retryable = response.status >= 500 ||
|
|
28
|
+
let retryable = response.status >= 500 ||
|
|
29
|
+
response.status === 429 ||
|
|
30
|
+
defaultCode === 'sync.auth_required';
|
|
7
31
|
try {
|
|
8
32
|
const body = (await response.json());
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
33
|
+
// Only the blob download endpoint can establish absence.
|
|
34
|
+
if (body.code !== 'blob.not_found' || defaultCode === 'blob.not_found') {
|
|
35
|
+
if (typeof body.code === 'string')
|
|
36
|
+
code = body.code;
|
|
37
|
+
if (typeof body.message === 'string')
|
|
38
|
+
message = body.message;
|
|
39
|
+
if (typeof body.retryable === 'boolean')
|
|
40
|
+
retryable = body.retryable;
|
|
41
|
+
}
|
|
15
42
|
}
|
|
16
43
|
catch {
|
|
17
44
|
// non-JSON error body — keep the HTTP-status defaults
|
|
18
45
|
}
|
|
19
46
|
throw new ClientSyncError(code, message, retryable, {
|
|
20
|
-
|
|
47
|
+
causeKind: 'status',
|
|
21
48
|
httpStatus: response.status,
|
|
22
49
|
});
|
|
23
50
|
}
|
|
@@ -29,47 +56,57 @@ function hostHeaders(options) {
|
|
|
29
56
|
export function httpSyncTransport(syncUrl, options) {
|
|
30
57
|
const doFetch = options?.fetch ?? fetch;
|
|
31
58
|
return async (request) => {
|
|
32
|
-
const response = await doFetch(syncUrl, {
|
|
59
|
+
const response = await transfer('sync request failed', () => doFetch(syncUrl, {
|
|
33
60
|
method: 'POST',
|
|
34
61
|
headers: {
|
|
35
62
|
'Content-Type': SSP2_CONTENT_TYPE,
|
|
36
63
|
...hostHeaders(options),
|
|
37
64
|
},
|
|
38
65
|
body: request.slice().buffer,
|
|
39
|
-
});
|
|
66
|
+
}));
|
|
40
67
|
if (!response.ok)
|
|
41
68
|
await throwHttpError(response);
|
|
42
|
-
return new Uint8Array(await response.arrayBuffer());
|
|
69
|
+
return new Uint8Array(await transfer('response body read failed', () => response.arrayBuffer(), response.status));
|
|
43
70
|
};
|
|
44
71
|
}
|
|
45
72
|
/** POST one registered authoritative operation to `<mount>/operations`. */
|
|
46
73
|
export function httpRemoteOperationTransport(operationsUrl, options) {
|
|
47
74
|
const doFetch = options?.fetch ?? fetch;
|
|
48
75
|
return async (request) => {
|
|
49
|
-
const response = await doFetch(operationsUrl, {
|
|
76
|
+
const response = await transfer('remote operation request failed', () => doFetch(operationsUrl, {
|
|
50
77
|
method: 'POST',
|
|
51
78
|
headers: {
|
|
52
79
|
'Content-Type': 'application/vnd.syncular.operations.v1+json',
|
|
53
80
|
...hostHeaders(options),
|
|
54
81
|
},
|
|
55
82
|
body: request.slice().buffer,
|
|
56
|
-
});
|
|
83
|
+
}));
|
|
57
84
|
if (!response.ok)
|
|
58
85
|
await throwHttpError(response);
|
|
59
|
-
return new Uint8Array(await response.arrayBuffer());
|
|
86
|
+
return new Uint8Array(await transfer('response body read failed', () => response.arrayBuffer(), response.status));
|
|
60
87
|
};
|
|
61
88
|
}
|
|
89
|
+
function socketOperation(message, operation) {
|
|
90
|
+
try {
|
|
91
|
+
return operation();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw new ClientSyncError('sync.transport_failed', message, true, {
|
|
95
|
+
causeKind: 'unknown',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
62
99
|
/** WebSocket connector for registered query snapshots. */
|
|
63
100
|
export function webSocketRemoteOperationConnector(realtimeUrl) {
|
|
64
101
|
return (handlers) => new Promise((resolve, reject) => {
|
|
65
|
-
const socket = new WebSocket(realtimeUrl);
|
|
102
|
+
const socket = socketOperation('realtime socket failed to connect', () => new WebSocket(realtimeUrl));
|
|
66
103
|
let opened = false;
|
|
67
104
|
socket.binaryType = 'arraybuffer';
|
|
68
105
|
socket.onopen = () => {
|
|
69
106
|
opened = true;
|
|
70
107
|
resolve({
|
|
71
|
-
send: (bytes) => socket.send(bytes.slice().buffer),
|
|
72
|
-
close: () => socket.close(),
|
|
108
|
+
send: (bytes) => socketOperation('realtime send failed', () => socket.send(bytes.slice().buffer)),
|
|
109
|
+
close: () => socketOperation('realtime close failed', () => socket.close()),
|
|
73
110
|
});
|
|
74
111
|
};
|
|
75
112
|
socket.onmessage = (event) => {
|
|
@@ -140,29 +177,13 @@ async function readSegmentBody(response, onProgress) {
|
|
|
140
177
|
export function httpSegmentDownloader(segmentsBaseUrl, options) {
|
|
141
178
|
const doFetch = options?.fetch ?? fetch;
|
|
142
179
|
const download = async (url, init, onProgress, signed = false) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
response = await doFetch(url, init);
|
|
150
|
-
if (!response.ok) {
|
|
151
|
-
if (!signed)
|
|
152
|
-
await throwHttpError(response, { path });
|
|
153
|
-
throw new ClientSyncError('sync.transport_failed', 'signed segment request failed; invalidate the descriptor and re-pull', true, { path, httpStatus: response.status });
|
|
154
|
-
}
|
|
155
|
-
return await readSegmentBody(response, onProgress);
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
if (error instanceof ClientSyncError)
|
|
159
|
-
throw error;
|
|
160
|
-
throw new ClientSyncError('sync.transport_failed', 'segment transfer failed', true, {
|
|
161
|
-
path,
|
|
162
|
-
...(response !== undefined ? { httpStatus: response.status } : {}),
|
|
163
|
-
causeMessage: error instanceof Error ? error.message : String(error),
|
|
164
|
-
});
|
|
180
|
+
const response = await transfer('segment transfer failed', () => doFetch(url, init));
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
if (!signed)
|
|
183
|
+
await throwHttpError(response);
|
|
184
|
+
throw new ClientSyncError('sync.transport_failed', 'signed segment request failed; invalidate the descriptor and re-pull', true, { causeKind: 'status', httpStatus: response.status });
|
|
165
185
|
}
|
|
186
|
+
return transfer('segment transfer failed', () => readSegmentBody(response, onProgress), response.status);
|
|
166
187
|
};
|
|
167
188
|
const direct = (request) => download(`${segmentsBaseUrl}/${encodeURIComponent(request.segmentId)}`, {
|
|
168
189
|
headers: {
|
|
@@ -185,28 +206,34 @@ export function httpBlobTransport(blobsBaseUrl, options) {
|
|
|
185
206
|
const blobUrl = (blobId) => `${blobsBaseUrl}/${encodeURIComponent(blobId)}`;
|
|
186
207
|
return {
|
|
187
208
|
upload: async (blobId, bytes, mediaType) => {
|
|
188
|
-
const response = await doFetch(blobUrl(blobId), {
|
|
209
|
+
const response = await transfer('blob upload failed', () => doFetch(blobUrl(blobId), {
|
|
189
210
|
method: 'PUT',
|
|
190
211
|
headers: {
|
|
191
212
|
'Content-Type': mediaType ?? 'application/octet-stream',
|
|
192
213
|
...hostHeaders(options),
|
|
193
214
|
},
|
|
194
215
|
body: bytes.slice().buffer,
|
|
195
|
-
});
|
|
216
|
+
}));
|
|
196
217
|
if (!response.ok)
|
|
197
218
|
await throwHttpError(response);
|
|
198
219
|
},
|
|
199
220
|
download: async (blobId) => {
|
|
200
|
-
const response = await doFetch(blobUrl(blobId), {
|
|
221
|
+
const response = await transfer('blob download failed', () => doFetch(blobUrl(blobId), {
|
|
201
222
|
headers: { ...hostHeaders(options) },
|
|
202
|
-
});
|
|
223
|
+
}));
|
|
203
224
|
if (!response.ok)
|
|
204
|
-
await throwHttpError(response
|
|
225
|
+
await throwHttpError(response, response.status === 404
|
|
226
|
+
? 'blob.not_found'
|
|
227
|
+
: response.status === 403
|
|
228
|
+
? 'blob.forbidden'
|
|
229
|
+
: response.status === 401
|
|
230
|
+
? 'sync.auth_required'
|
|
231
|
+
: 'sync.transport_failed');
|
|
205
232
|
// §5.9.5 always-issue: a JSON body with `url` means presigned delivery;
|
|
206
233
|
// an octet-stream body is inline bytes.
|
|
207
234
|
const contentType = response.headers.get('content-type') ?? '';
|
|
208
235
|
if (contentType.includes('application/json')) {
|
|
209
|
-
const body = (await response.json());
|
|
236
|
+
const body = (await transfer('blob download body read failed', () => response.json(), response.status));
|
|
210
237
|
if (typeof body.url === 'string') {
|
|
211
238
|
return {
|
|
212
239
|
kind: 'url',
|
|
@@ -219,20 +246,20 @@ export function httpBlobTransport(blobsBaseUrl, options) {
|
|
|
219
246
|
}
|
|
220
247
|
return {
|
|
221
248
|
kind: 'bytes',
|
|
222
|
-
bytes: new Uint8Array(await response.arrayBuffer()),
|
|
249
|
+
bytes: new Uint8Array(await transfer('blob download body read failed', () => response.arrayBuffer(), response.status)),
|
|
223
250
|
};
|
|
224
251
|
},
|
|
225
252
|
// §5.9.5: bare GET of the signed URL — no host auth (the URL is the grant).
|
|
226
253
|
fetchUrl: async (url) => {
|
|
227
|
-
const response = await doFetch(url);
|
|
254
|
+
const response = await transfer('signed blob download failed', () => doFetch(url));
|
|
228
255
|
if (!response.ok) {
|
|
229
|
-
throw new ClientSyncError('sync.transport_failed',
|
|
256
|
+
throw new ClientSyncError('sync.transport_failed', 'signed blob download failed; re-request to recover', true, { causeKind: 'status', httpStatus: response.status });
|
|
230
257
|
}
|
|
231
|
-
return new Uint8Array(await response.arrayBuffer());
|
|
258
|
+
return new Uint8Array(await transfer('response body read failed', () => response.arrayBuffer(), response.status));
|
|
232
259
|
},
|
|
233
260
|
// §5.9.3: presigned-upload grant.
|
|
234
261
|
uploadGrant: async (blobId, byteLength, mediaType) => {
|
|
235
|
-
const response = await doFetch(`${blobUrl(blobId)}/upload-grant`, {
|
|
262
|
+
const response = await transfer('blob upload grant request failed', () => doFetch(`${blobUrl(blobId)}/upload-grant`, {
|
|
236
263
|
method: 'POST',
|
|
237
264
|
headers: {
|
|
238
265
|
'Content-Type': 'application/json',
|
|
@@ -242,10 +269,10 @@ export function httpBlobTransport(blobsBaseUrl, options) {
|
|
|
242
269
|
byteLength,
|
|
243
270
|
...(mediaType !== undefined ? { mediaType } : {}),
|
|
244
271
|
}),
|
|
245
|
-
});
|
|
272
|
+
}));
|
|
246
273
|
if (!response.ok)
|
|
247
274
|
await throwHttpError(response);
|
|
248
|
-
const body = (await response.json());
|
|
275
|
+
const body = (await transfer('blob upload grant body read failed', () => response.json(), response.status));
|
|
249
276
|
if (typeof body.url === 'string') {
|
|
250
277
|
return {
|
|
251
278
|
kind: 'url',
|
|
@@ -261,15 +288,15 @@ export function httpBlobTransport(blobsBaseUrl, options) {
|
|
|
261
288
|
},
|
|
262
289
|
// §5.9.3: direct-to-storage PUT — no host auth (the URL is the grant).
|
|
263
290
|
uploadToUrl: async (url, bytes, mediaType) => {
|
|
264
|
-
const response = await doFetch(url, {
|
|
291
|
+
const response = await transfer('signed blob upload failed', () => doFetch(url, {
|
|
265
292
|
method: 'PUT',
|
|
266
293
|
headers: {
|
|
267
294
|
'Content-Type': mediaType ?? 'application/octet-stream',
|
|
268
295
|
},
|
|
269
296
|
body: bytes.slice().buffer,
|
|
270
|
-
});
|
|
297
|
+
}));
|
|
271
298
|
if (!response.ok) {
|
|
272
|
-
throw new ClientSyncError('sync.transport_failed',
|
|
299
|
+
throw new ClientSyncError('sync.transport_failed', 'signed blob upload failed; re-request a grant or stream direct', true, { causeKind: 'status', httpStatus: response.status });
|
|
273
300
|
}
|
|
274
301
|
},
|
|
275
302
|
};
|
|
@@ -277,17 +304,17 @@ export function httpBlobTransport(blobsBaseUrl, options) {
|
|
|
277
304
|
/** WebSocket realtime connector (§8.1): text = control, binary = deltas. */
|
|
278
305
|
export function webSocketRealtimeConnector(realtimeUrl) {
|
|
279
306
|
return (handlers) => new Promise((resolve, reject) => {
|
|
280
|
-
const socket = new WebSocket(realtimeUrl);
|
|
307
|
+
const socket = socketOperation('realtime socket failed to connect', () => new WebSocket(realtimeUrl));
|
|
281
308
|
let opened = false;
|
|
282
309
|
socket.binaryType = 'arraybuffer';
|
|
283
310
|
socket.onopen = () => {
|
|
284
311
|
opened = true;
|
|
285
312
|
resolve({
|
|
286
|
-
send: (text) => socket.send(text),
|
|
313
|
+
send: (text) => socketOperation('realtime send failed', () => socket.send(text)),
|
|
287
314
|
sendBytes: (bytes) => {
|
|
288
|
-
socket.send(bytes.slice().buffer);
|
|
315
|
+
socketOperation('realtime send failed', () => socket.send(bytes.slice().buffer));
|
|
289
316
|
},
|
|
290
|
-
close: () => socket.close(),
|
|
317
|
+
close: () => socketOperation('realtime close failed', () => socket.close()),
|
|
291
318
|
});
|
|
292
319
|
};
|
|
293
320
|
socket.onmessage = (event) => {
|
package/dist/outcomes.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare const OUTCOMES_TABLE = "_syncular_commit_outcomes";
|
|
|
7
7
|
* restart can never turn "rejected" into an inferred success. Conflict payloads
|
|
8
8
|
* deliberately stay local; retention never deletes an unresolved failure.
|
|
9
9
|
*/
|
|
10
|
-
import type
|
|
10
|
+
import { type RejectionDetails, type RowValue } from '@syncular/core';
|
|
11
11
|
import type { ClientDatabase } from './database.js';
|
|
12
12
|
import type { OutboxOperation } from './outbox.js';
|
|
13
13
|
export interface ConflictRecord {
|
|
@@ -97,8 +97,16 @@ export interface ResolveCommitOutcomeInput {
|
|
|
97
97
|
readonly resolution: Exclude<CommitOutcomeResolution, 'active'>;
|
|
98
98
|
readonly replacementClientCommitId?: string;
|
|
99
99
|
}
|
|
100
|
+
export declare function isStoredOperation(value: unknown): value is OutboxOperation;
|
|
100
101
|
export declare function recordCommitOutcome(db: ClientDatabase, outcome: Omit<CommitOutcome, 'sequence' | 'resolution'>): CommitOutcome;
|
|
101
102
|
export declare function commitOutcome(db: ClientDatabase, clientCommitId: string): CommitOutcome | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* §7.5 persisted-journal view: the stored outcome WITHOUT the owner-derived
|
|
105
|
+
* `retainedRows` images. A malformed journal row (invalid status, resolution,
|
|
106
|
+
* results, or operations JSON) raises the same static `sync.local_corrupt` the
|
|
107
|
+
* native sidecar reports, never a raw `SyntaxError`.
|
|
108
|
+
*/
|
|
109
|
+
export declare function persistedCommitOutcome(db: ClientDatabase, clientCommitId: string): CommitOutcome | undefined;
|
|
102
110
|
export declare function listCommitOutcomes(db: ClientDatabase, query?: CommitOutcomeQuery): CommitOutcome[];
|
|
103
111
|
export declare function persistCommitOutcomeResolution(db: ClientDatabase, input: ResolveCommitOutcomeInput, nowMs: number): CommitOutcome | undefined;
|
|
104
112
|
/**
|