@syncular/client 0.30.15 → 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 +37 -1
- 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/index.d.ts +1 -1
- package/dist/index.js +0 -11
- 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.d.ts +18 -0
- package/dist/worker-entry.js +45 -46
- 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 +11 -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/index.ts +4 -5
- 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 +54 -38
- package/src/worker-host.ts +10 -1
- package/src/worker-protocol.ts +2 -1
package/src/diagnostics.ts
CHANGED
|
@@ -153,6 +153,8 @@ export interface DiagnosticQueryFailure {
|
|
|
153
153
|
| 'client.storage_corrupt'
|
|
154
154
|
| 'client.storage_full'
|
|
155
155
|
| 'client.storage_io'
|
|
156
|
+
| 'client.storage_busy'
|
|
157
|
+
| 'sync.local_corrupt'
|
|
156
158
|
| 'client.query_failed';
|
|
157
159
|
/** SQLite extended result code, when the driver exposed one. */
|
|
158
160
|
readonly sqliteCode?: number;
|
package/src/errors.ts
CHANGED
|
@@ -120,6 +120,7 @@ export function classifySqliteFailure(error: unknown): {
|
|
|
120
120
|
| typeof STORAGE_CORRUPT_CODE
|
|
121
121
|
| typeof STORAGE_FULL_CODE
|
|
122
122
|
| typeof STORAGE_IO_CODE
|
|
123
|
+
| typeof STORAGE_BUSY_CODE
|
|
123
124
|
| undefined;
|
|
124
125
|
readonly error: unknown;
|
|
125
126
|
} {
|
|
@@ -151,7 +152,8 @@ export function classifySqliteFailure(error: unknown): {
|
|
|
151
152
|
code:
|
|
152
153
|
error.code === STORAGE_FULL_CODE ||
|
|
153
154
|
error.code === STORAGE_IO_CODE ||
|
|
154
|
-
error.code === STORAGE_CORRUPT_CODE
|
|
155
|
+
error.code === STORAGE_CORRUPT_CODE ||
|
|
156
|
+
error.code === STORAGE_BUSY_CODE
|
|
155
157
|
? error.code
|
|
156
158
|
: undefined,
|
|
157
159
|
error: retained,
|
|
@@ -179,7 +181,9 @@ export function classifySqliteFailure(error: unknown): {
|
|
|
179
181
|
? STORAGE_IO_CODE
|
|
180
182
|
: primary === 11 || primary === 26
|
|
181
183
|
? STORAGE_CORRUPT_CODE
|
|
182
|
-
:
|
|
184
|
+
: primary === 5 || primary === 6
|
|
185
|
+
? STORAGE_BUSY_CODE
|
|
186
|
+
: undefined;
|
|
183
187
|
if (code === undefined) return { sqliteCode, code, error };
|
|
184
188
|
const rollback =
|
|
185
189
|
error instanceof Error && 'rollbackError' in error
|
|
@@ -191,8 +195,10 @@ export function classifySqliteFailure(error: unknown): {
|
|
|
191
195
|
? 'local SQLite storage is full'
|
|
192
196
|
: primary === 10
|
|
193
197
|
? 'local SQLite storage I/O failed'
|
|
194
|
-
:
|
|
195
|
-
|
|
198
|
+
: code === STORAGE_BUSY_CODE
|
|
199
|
+
? 'local SQLite storage is busy'
|
|
200
|
+
: 'local SQLite storage is corrupt',
|
|
201
|
+
code === STORAGE_BUSY_CODE,
|
|
196
202
|
{
|
|
197
203
|
sqliteCode,
|
|
198
204
|
sqliteMessage: error instanceof Error ? error.message : String(error),
|
package/src/failed-overlay.ts
CHANGED
|
@@ -185,19 +185,6 @@ function retainedRows(
|
|
|
185
185
|
);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
function retainedTable(
|
|
189
|
-
schema: CompiledClientSchema,
|
|
190
|
-
name: string,
|
|
191
|
-
): CompiledClientTable {
|
|
192
|
-
const table = schema.tables.get(name);
|
|
193
|
-
if (!table)
|
|
194
|
-
throw new ClientSyncError(
|
|
195
|
-
'sync.unknown_table',
|
|
196
|
-
'retained table is unknown',
|
|
197
|
-
);
|
|
198
|
-
return table;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
188
|
export function restoreFailedBases(
|
|
202
189
|
db: ClientDatabase,
|
|
203
190
|
schema: CompiledClientSchema,
|
|
@@ -208,7 +195,11 @@ export function restoreFailedBases(
|
|
|
208
195
|
const key = JSON.stringify([row.tbl, row.id]);
|
|
209
196
|
if (seen.has(key)) continue;
|
|
210
197
|
seen.add(key);
|
|
211
|
-
|
|
198
|
+
// §7.4.4: a retained row whose table the current schema removed has no
|
|
199
|
+
// mirror to project; its persisted intent and outcome stay available, only
|
|
200
|
+
// this projection is skipped.
|
|
201
|
+
const table = schema.tables.get(row.tbl as string);
|
|
202
|
+
if (table === undefined) continue;
|
|
212
203
|
if (row.base === null) deleteLocalRow(db, table, row.id as string, false);
|
|
213
204
|
else {
|
|
214
205
|
const values: Record<string, JsonRowValue> = JSON.parse(String(row.base));
|
|
@@ -245,7 +236,11 @@ export function failedOverlayCommits(
|
|
|
245
236
|
const id = row.commit_id as string;
|
|
246
237
|
let operation: OutboxOperation = JSON.parse(row.op as string);
|
|
247
238
|
if (schema && operation.op === 'upsert') {
|
|
248
|
-
|
|
239
|
+
// §7.4.4: the table this retained row belongs to is gone; the persisted
|
|
240
|
+
// outcome and intent stay available, only the removed-table projection is
|
|
241
|
+
// skipped.
|
|
242
|
+
const table = schema.tables.get(operation.table);
|
|
243
|
+
if (table === undefined) continue;
|
|
249
244
|
if (acknowledged) {
|
|
250
245
|
if (
|
|
251
246
|
!db.query(
|
package/src/http.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';
|
|
8
8
|
import { SSP2_CONTENT_TYPE } from './content-type';
|
|
@@ -15,27 +15,60 @@ import type {
|
|
|
15
15
|
SyncTransport,
|
|
16
16
|
} from './transport';
|
|
17
17
|
|
|
18
|
+
async function transfer<T>(
|
|
19
|
+
message: string,
|
|
20
|
+
operation: () => Promise<T>,
|
|
21
|
+
httpStatus?: number,
|
|
22
|
+
): Promise<T> {
|
|
23
|
+
try {
|
|
24
|
+
return await operation();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
// Classify only known exception kinds. Injected errors, including
|
|
27
|
+
// ClientSyncError, are untrusted and never become the returned cause.
|
|
28
|
+
const causeKind =
|
|
29
|
+
error instanceof DOMException && error.name === 'TimeoutError'
|
|
30
|
+
? 'timeout'
|
|
31
|
+
: error instanceof DOMException && error.name === 'AbortError'
|
|
32
|
+
? 'aborted'
|
|
33
|
+
: httpStatus !== undefined
|
|
34
|
+
? 'body'
|
|
35
|
+
: error instanceof TypeError
|
|
36
|
+
? 'network'
|
|
37
|
+
: 'unknown';
|
|
38
|
+
throw new ClientSyncError('sync.transport_failed', message, true, {
|
|
39
|
+
causeKind,
|
|
40
|
+
...(httpStatus !== undefined ? { httpStatus } : {}),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
18
45
|
async function throwHttpError(
|
|
19
46
|
response: Response,
|
|
20
|
-
|
|
47
|
+
defaultCode = 'sync.transport_failed',
|
|
21
48
|
): Promise<never> {
|
|
22
|
-
let code =
|
|
49
|
+
let code = defaultCode;
|
|
23
50
|
let message = 'HTTP request failed';
|
|
24
|
-
let retryable =
|
|
51
|
+
let retryable =
|
|
52
|
+
response.status >= 500 ||
|
|
53
|
+
response.status === 429 ||
|
|
54
|
+
defaultCode === 'sync.auth_required';
|
|
25
55
|
try {
|
|
26
56
|
const body = (await response.json()) as {
|
|
27
57
|
code?: string;
|
|
28
58
|
message?: string;
|
|
29
59
|
retryable?: boolean;
|
|
30
60
|
};
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
61
|
+
// Only the blob download endpoint can establish absence.
|
|
62
|
+
if (body.code !== 'blob.not_found' || defaultCode === 'blob.not_found') {
|
|
63
|
+
if (typeof body.code === 'string') code = body.code;
|
|
64
|
+
if (typeof body.message === 'string') message = body.message;
|
|
65
|
+
if (typeof body.retryable === 'boolean') retryable = body.retryable;
|
|
66
|
+
}
|
|
34
67
|
} catch {
|
|
35
68
|
// non-JSON error body — keep the HTTP-status defaults
|
|
36
69
|
}
|
|
37
70
|
throw new ClientSyncError(code, message, retryable, {
|
|
38
|
-
|
|
71
|
+
causeKind: 'status',
|
|
39
72
|
httpStatus: response.status,
|
|
40
73
|
});
|
|
41
74
|
}
|
|
@@ -66,16 +99,24 @@ export function httpSyncTransport(
|
|
|
66
99
|
): SyncTransport {
|
|
67
100
|
const doFetch = options?.fetch ?? fetch;
|
|
68
101
|
return async (request) => {
|
|
69
|
-
const response = await
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
102
|
+
const response = await transfer('sync request failed', () =>
|
|
103
|
+
doFetch(syncUrl, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: {
|
|
106
|
+
'Content-Type': SSP2_CONTENT_TYPE,
|
|
107
|
+
...hostHeaders(options),
|
|
108
|
+
},
|
|
109
|
+
body: request.slice().buffer as ArrayBuffer,
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
77
112
|
if (!response.ok) await throwHttpError(response);
|
|
78
|
-
return new Uint8Array(
|
|
113
|
+
return new Uint8Array(
|
|
114
|
+
await transfer(
|
|
115
|
+
'response body read failed',
|
|
116
|
+
() => response.arrayBuffer(),
|
|
117
|
+
response.status,
|
|
118
|
+
),
|
|
119
|
+
);
|
|
79
120
|
};
|
|
80
121
|
}
|
|
81
122
|
|
|
@@ -86,33 +127,58 @@ export function httpRemoteOperationTransport(
|
|
|
86
127
|
): RemoteOperationTransport {
|
|
87
128
|
const doFetch = options?.fetch ?? fetch;
|
|
88
129
|
return async (request) => {
|
|
89
|
-
const response = await
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
130
|
+
const response = await transfer('remote operation request failed', () =>
|
|
131
|
+
doFetch(operationsUrl, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: {
|
|
134
|
+
'Content-Type': 'application/vnd.syncular.operations.v1+json',
|
|
135
|
+
...hostHeaders(options),
|
|
136
|
+
},
|
|
137
|
+
body: request.slice().buffer as ArrayBuffer,
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
97
140
|
if (!response.ok) await throwHttpError(response);
|
|
98
|
-
return new Uint8Array(
|
|
141
|
+
return new Uint8Array(
|
|
142
|
+
await transfer(
|
|
143
|
+
'response body read failed',
|
|
144
|
+
() => response.arrayBuffer(),
|
|
145
|
+
response.status,
|
|
146
|
+
),
|
|
147
|
+
);
|
|
99
148
|
};
|
|
100
149
|
}
|
|
101
150
|
|
|
151
|
+
function socketOperation<T>(message: string, operation: () => T): T {
|
|
152
|
+
try {
|
|
153
|
+
return operation();
|
|
154
|
+
} catch {
|
|
155
|
+
throw new ClientSyncError('sync.transport_failed', message, true, {
|
|
156
|
+
causeKind: 'unknown',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
102
161
|
/** WebSocket connector for registered query snapshots. */
|
|
103
162
|
export function webSocketRemoteOperationConnector(
|
|
104
163
|
realtimeUrl: string,
|
|
105
164
|
): RemoteOperationRealtimeConnector {
|
|
106
165
|
return (handlers) =>
|
|
107
166
|
new Promise((resolve, reject) => {
|
|
108
|
-
const socket =
|
|
167
|
+
const socket = socketOperation(
|
|
168
|
+
'realtime socket failed to connect',
|
|
169
|
+
() => new WebSocket(realtimeUrl),
|
|
170
|
+
);
|
|
109
171
|
let opened = false;
|
|
110
172
|
socket.binaryType = 'arraybuffer';
|
|
111
173
|
socket.onopen = () => {
|
|
112
174
|
opened = true;
|
|
113
175
|
resolve({
|
|
114
|
-
send: (bytes) =>
|
|
115
|
-
|
|
176
|
+
send: (bytes) =>
|
|
177
|
+
socketOperation('realtime send failed', () =>
|
|
178
|
+
socket.send(bytes.slice().buffer as ArrayBuffer),
|
|
179
|
+
),
|
|
180
|
+
close: () =>
|
|
181
|
+
socketOperation('realtime close failed', () => socket.close()),
|
|
116
182
|
});
|
|
117
183
|
};
|
|
118
184
|
socket.onmessage = (event) => {
|
|
@@ -204,36 +270,23 @@ export function httpSegmentDownloader(
|
|
|
204
270
|
onProgress?: (bytesReceived: number) => void,
|
|
205
271
|
signed = false,
|
|
206
272
|
) => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
response = await doFetch(url, init);
|
|
214
|
-
if (!response.ok) {
|
|
215
|
-
if (!signed) await throwHttpError(response, { path });
|
|
216
|
-
throw new ClientSyncError(
|
|
217
|
-
'sync.transport_failed',
|
|
218
|
-
'signed segment request failed; invalidate the descriptor and re-pull',
|
|
219
|
-
true,
|
|
220
|
-
{ path, httpStatus: response.status },
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
return await readSegmentBody(response, onProgress);
|
|
224
|
-
} catch (error) {
|
|
225
|
-
if (error instanceof ClientSyncError) throw error;
|
|
273
|
+
const response = await transfer('segment transfer failed', () =>
|
|
274
|
+
doFetch(url, init),
|
|
275
|
+
);
|
|
276
|
+
if (!response.ok) {
|
|
277
|
+
if (!signed) await throwHttpError(response);
|
|
226
278
|
throw new ClientSyncError(
|
|
227
279
|
'sync.transport_failed',
|
|
228
|
-
'segment
|
|
280
|
+
'signed segment request failed; invalidate the descriptor and re-pull',
|
|
229
281
|
true,
|
|
230
|
-
{
|
|
231
|
-
path,
|
|
232
|
-
...(response !== undefined ? { httpStatus: response.status } : {}),
|
|
233
|
-
causeMessage: error instanceof Error ? error.message : String(error),
|
|
234
|
-
},
|
|
282
|
+
{ causeKind: 'status', httpStatus: response.status },
|
|
235
283
|
);
|
|
236
284
|
}
|
|
285
|
+
return transfer(
|
|
286
|
+
'segment transfer failed',
|
|
287
|
+
() => readSegmentBody(response, onProgress),
|
|
288
|
+
response.status,
|
|
289
|
+
);
|
|
237
290
|
};
|
|
238
291
|
const direct = (request: {
|
|
239
292
|
readonly segmentId: string;
|
|
@@ -273,26 +326,44 @@ export function httpBlobTransport(
|
|
|
273
326
|
`${blobsBaseUrl}/${encodeURIComponent(blobId)}`;
|
|
274
327
|
return {
|
|
275
328
|
upload: async (blobId, bytes, mediaType) => {
|
|
276
|
-
const response = await
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
329
|
+
const response = await transfer('blob upload failed', () =>
|
|
330
|
+
doFetch(blobUrl(blobId), {
|
|
331
|
+
method: 'PUT',
|
|
332
|
+
headers: {
|
|
333
|
+
'Content-Type': mediaType ?? 'application/octet-stream',
|
|
334
|
+
...hostHeaders(options),
|
|
335
|
+
},
|
|
336
|
+
body: bytes.slice().buffer as ArrayBuffer,
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
284
339
|
if (!response.ok) await throwHttpError(response);
|
|
285
340
|
},
|
|
286
341
|
download: async (blobId) => {
|
|
287
|
-
const response = await
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
342
|
+
const response = await transfer('blob download failed', () =>
|
|
343
|
+
doFetch(blobUrl(blobId), {
|
|
344
|
+
headers: { ...hostHeaders(options) },
|
|
345
|
+
}),
|
|
346
|
+
);
|
|
347
|
+
if (!response.ok)
|
|
348
|
+
await throwHttpError(
|
|
349
|
+
response,
|
|
350
|
+
response.status === 404
|
|
351
|
+
? 'blob.not_found'
|
|
352
|
+
: response.status === 403
|
|
353
|
+
? 'blob.forbidden'
|
|
354
|
+
: response.status === 401
|
|
355
|
+
? 'sync.auth_required'
|
|
356
|
+
: 'sync.transport_failed',
|
|
357
|
+
);
|
|
291
358
|
// §5.9.5 always-issue: a JSON body with `url` means presigned delivery;
|
|
292
359
|
// an octet-stream body is inline bytes.
|
|
293
360
|
const contentType = response.headers.get('content-type') ?? '';
|
|
294
361
|
if (contentType.includes('application/json')) {
|
|
295
|
-
const body = (await
|
|
362
|
+
const body = (await transfer(
|
|
363
|
+
'blob download body read failed',
|
|
364
|
+
() => response.json(),
|
|
365
|
+
response.status,
|
|
366
|
+
)) as {
|
|
296
367
|
url?: string;
|
|
297
368
|
urlExpiresAtMs?: number;
|
|
298
369
|
};
|
|
@@ -308,36 +379,57 @@ export function httpBlobTransport(
|
|
|
308
379
|
}
|
|
309
380
|
return {
|
|
310
381
|
kind: 'bytes',
|
|
311
|
-
bytes: new Uint8Array(
|
|
382
|
+
bytes: new Uint8Array(
|
|
383
|
+
await transfer(
|
|
384
|
+
'blob download body read failed',
|
|
385
|
+
() => response.arrayBuffer(),
|
|
386
|
+
response.status,
|
|
387
|
+
),
|
|
388
|
+
),
|
|
312
389
|
};
|
|
313
390
|
},
|
|
314
391
|
// §5.9.5: bare GET of the signed URL — no host auth (the URL is the grant).
|
|
315
392
|
fetchUrl: async (url) => {
|
|
316
|
-
const response = await
|
|
393
|
+
const response = await transfer('signed blob download failed', () =>
|
|
394
|
+
doFetch(url),
|
|
395
|
+
);
|
|
317
396
|
if (!response.ok) {
|
|
318
397
|
throw new ClientSyncError(
|
|
319
398
|
'sync.transport_failed',
|
|
320
|
-
|
|
399
|
+
'signed blob download failed; re-request to recover',
|
|
321
400
|
true,
|
|
401
|
+
{ causeKind: 'status', httpStatus: response.status },
|
|
322
402
|
);
|
|
323
403
|
}
|
|
324
|
-
return new Uint8Array(
|
|
404
|
+
return new Uint8Array(
|
|
405
|
+
await transfer(
|
|
406
|
+
'response body read failed',
|
|
407
|
+
() => response.arrayBuffer(),
|
|
408
|
+
response.status,
|
|
409
|
+
),
|
|
410
|
+
);
|
|
325
411
|
},
|
|
326
412
|
// §5.9.3: presigned-upload grant.
|
|
327
413
|
uploadGrant: async (blobId, byteLength, mediaType) => {
|
|
328
|
-
const response = await
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
414
|
+
const response = await transfer('blob upload grant request failed', () =>
|
|
415
|
+
doFetch(`${blobUrl(blobId)}/upload-grant`, {
|
|
416
|
+
method: 'POST',
|
|
417
|
+
headers: {
|
|
418
|
+
'Content-Type': 'application/json',
|
|
419
|
+
...hostHeaders(options),
|
|
420
|
+
},
|
|
421
|
+
body: JSON.stringify({
|
|
422
|
+
byteLength,
|
|
423
|
+
...(mediaType !== undefined ? { mediaType } : {}),
|
|
424
|
+
}),
|
|
337
425
|
}),
|
|
338
|
-
|
|
426
|
+
);
|
|
339
427
|
if (!response.ok) await throwHttpError(response);
|
|
340
|
-
const body = (await
|
|
428
|
+
const body = (await transfer(
|
|
429
|
+
'blob upload grant body read failed',
|
|
430
|
+
() => response.json(),
|
|
431
|
+
response.status,
|
|
432
|
+
)) as {
|
|
341
433
|
url?: string;
|
|
342
434
|
urlExpiresAtMs?: number;
|
|
343
435
|
present?: boolean;
|
|
@@ -356,18 +448,21 @@ export function httpBlobTransport(
|
|
|
356
448
|
},
|
|
357
449
|
// §5.9.3: direct-to-storage PUT — no host auth (the URL is the grant).
|
|
358
450
|
uploadToUrl: async (url, bytes, mediaType) => {
|
|
359
|
-
const response = await
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
451
|
+
const response = await transfer('signed blob upload failed', () =>
|
|
452
|
+
doFetch(url, {
|
|
453
|
+
method: 'PUT',
|
|
454
|
+
headers: {
|
|
455
|
+
'Content-Type': mediaType ?? 'application/octet-stream',
|
|
456
|
+
},
|
|
457
|
+
body: bytes.slice().buffer as ArrayBuffer,
|
|
458
|
+
}),
|
|
459
|
+
);
|
|
366
460
|
if (!response.ok) {
|
|
367
461
|
throw new ClientSyncError(
|
|
368
462
|
'sync.transport_failed',
|
|
369
|
-
|
|
463
|
+
'signed blob upload failed; re-request a grant or stream direct',
|
|
370
464
|
true,
|
|
465
|
+
{ causeKind: 'status', httpStatus: response.status },
|
|
371
466
|
);
|
|
372
467
|
}
|
|
373
468
|
},
|
|
@@ -380,17 +475,24 @@ export function webSocketRealtimeConnector(
|
|
|
380
475
|
): RealtimeConnector {
|
|
381
476
|
return (handlers) =>
|
|
382
477
|
new Promise((resolve, reject) => {
|
|
383
|
-
const socket =
|
|
478
|
+
const socket = socketOperation(
|
|
479
|
+
'realtime socket failed to connect',
|
|
480
|
+
() => new WebSocket(realtimeUrl),
|
|
481
|
+
);
|
|
384
482
|
let opened = false;
|
|
385
483
|
socket.binaryType = 'arraybuffer';
|
|
386
484
|
socket.onopen = () => {
|
|
387
485
|
opened = true;
|
|
388
486
|
resolve({
|
|
389
|
-
send: (text) =>
|
|
487
|
+
send: (text) =>
|
|
488
|
+
socketOperation('realtime send failed', () => socket.send(text)),
|
|
390
489
|
sendBytes: (bytes) => {
|
|
391
|
-
|
|
490
|
+
socketOperation('realtime send failed', () =>
|
|
491
|
+
socket.send(bytes.slice().buffer as ArrayBuffer),
|
|
492
|
+
);
|
|
392
493
|
},
|
|
393
|
-
close: () =>
|
|
494
|
+
close: () =>
|
|
495
|
+
socketOperation('realtime close failed', () => socket.close()),
|
|
394
496
|
});
|
|
395
497
|
};
|
|
396
498
|
socket.onmessage = (event) => {
|
package/src/index.ts
CHANGED
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
* (`worker-host`) and the RPC protocol types are root exports — they
|
|
9
9
|
* import no SQLite.
|
|
10
10
|
*/
|
|
11
|
-
export {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
type AuthoritySnapshot,
|
|
11
|
+
export type {
|
|
12
|
+
AuthorityReadPolicy,
|
|
13
|
+
AuthorityReadDeclaration,
|
|
14
|
+
AuthoritySnapshot,
|
|
16
15
|
} from './authority';
|
|
17
16
|
export * from './apply';
|
|
18
17
|
export * from './availability';
|