@syncular/transport-http 0.0.1 → 0.0.2-126
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 +32 -0
- package/dist/generated/api.d.ts +198 -184
- package/dist/generated/api.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -14
- package/dist/index.js.map +1 -1
- package/package.json +28 -6
- package/src/__tests__/transport-options.test.ts +85 -62
- package/src/generated/api.ts +198 -184
- package/src/index.ts +106 -32
package/src/index.ts
CHANGED
|
@@ -5,10 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
SyncPushRequest,
|
|
11
|
-
SyncPushResponse,
|
|
8
|
+
SyncCombinedRequest,
|
|
9
|
+
SyncCombinedResponse,
|
|
12
10
|
SyncTransport,
|
|
13
11
|
SyncTransportBlobs,
|
|
14
12
|
SyncTransportOptions,
|
|
@@ -106,6 +104,30 @@ export interface ClientOptions {
|
|
|
106
104
|
transportPath?: SyncTransportPath;
|
|
107
105
|
}
|
|
108
106
|
|
|
107
|
+
function resolveRequestUrl(baseUrl: string, path: string): string {
|
|
108
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
109
|
+
const isAbsolute = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(baseUrl);
|
|
110
|
+
if (isAbsolute) {
|
|
111
|
+
return new URL(normalizedPath, baseUrl).toString();
|
|
112
|
+
}
|
|
113
|
+
if (typeof location === 'undefined') {
|
|
114
|
+
return `${baseUrl.replace(/\/$/, '')}${normalizedPath}`;
|
|
115
|
+
}
|
|
116
|
+
return new URL(
|
|
117
|
+
`${baseUrl.replace(/\/$/, '')}${normalizedPath}`,
|
|
118
|
+
location.origin
|
|
119
|
+
).toString();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function bytesToReadableStream(bytes: Uint8Array): ReadableStream<Uint8Array> {
|
|
123
|
+
return new ReadableStream<Uint8Array>({
|
|
124
|
+
start(controller) {
|
|
125
|
+
controller.enqueue(bytes);
|
|
126
|
+
controller.close();
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
109
131
|
/**
|
|
110
132
|
* Create a typed API client for the full Syncular API.
|
|
111
133
|
*
|
|
@@ -119,7 +141,7 @@ export interface ClientOptions {
|
|
|
119
141
|
* });
|
|
120
142
|
*
|
|
121
143
|
* // Sync endpoints
|
|
122
|
-
* const { data } = await client.POST('/sync
|
|
144
|
+
* const { data } = await client.POST('/sync', { body: { clientId: 'c1', pull: { ... } } });
|
|
123
145
|
*
|
|
124
146
|
* // Console endpoints
|
|
125
147
|
* const { data: stats } = await client.GET('/console/stats');
|
|
@@ -185,6 +207,8 @@ export function createHttpTransport(
|
|
|
185
207
|
'GET' in clientOrOptions
|
|
186
208
|
? clientOrOptions
|
|
187
209
|
: createApiClient(clientOrOptions);
|
|
210
|
+
const transportOptions =
|
|
211
|
+
'GET' in clientOrOptions ? undefined : clientOrOptions;
|
|
188
212
|
|
|
189
213
|
// Create blob operations using the typed OpenAPI client
|
|
190
214
|
const blobs: SyncTransportBlobs = {
|
|
@@ -244,13 +268,13 @@ export function createHttpTransport(
|
|
|
244
268
|
};
|
|
245
269
|
|
|
246
270
|
return {
|
|
247
|
-
async
|
|
248
|
-
request:
|
|
271
|
+
async sync(
|
|
272
|
+
request: SyncCombinedRequest,
|
|
249
273
|
transportOptions?: SyncTransportOptions
|
|
250
|
-
): Promise<
|
|
274
|
+
): Promise<SyncCombinedResponse> {
|
|
251
275
|
const { data, error, response } = await executeWithAuthRetry(
|
|
252
276
|
(signal) =>
|
|
253
|
-
client.POST('/sync
|
|
277
|
+
client.POST('/sync', {
|
|
254
278
|
body: request,
|
|
255
279
|
...(signal ? { signal } : {}),
|
|
256
280
|
}),
|
|
@@ -259,22 +283,23 @@ export function createHttpTransport(
|
|
|
259
283
|
|
|
260
284
|
if (error || !data) {
|
|
261
285
|
throw new SyncTransportError(
|
|
262
|
-
`
|
|
286
|
+
`Sync failed: ${getErrorMessage(error) || response.statusText}`,
|
|
263
287
|
response.status
|
|
264
288
|
);
|
|
265
289
|
}
|
|
266
290
|
|
|
267
|
-
return data as
|
|
291
|
+
return data as SyncCombinedResponse;
|
|
268
292
|
},
|
|
269
293
|
|
|
270
|
-
async
|
|
271
|
-
request:
|
|
294
|
+
async fetchSnapshotChunk(
|
|
295
|
+
request: { chunkId: string },
|
|
272
296
|
transportOptions?: SyncTransportOptions
|
|
273
|
-
): Promise<
|
|
297
|
+
): Promise<Uint8Array> {
|
|
274
298
|
const { data, error, response } = await executeWithAuthRetry(
|
|
275
299
|
(signal) =>
|
|
276
|
-
client.
|
|
277
|
-
|
|
300
|
+
client.GET('/sync/snapshot-chunks/{chunkId}', {
|
|
301
|
+
params: { path: { chunkId: request.chunkId } },
|
|
302
|
+
parseAs: 'blob',
|
|
278
303
|
...(signal ? { signal } : {}),
|
|
279
304
|
}),
|
|
280
305
|
transportOptions
|
|
@@ -282,36 +307,85 @@ export function createHttpTransport(
|
|
|
282
307
|
|
|
283
308
|
if (error || !data) {
|
|
284
309
|
throw new SyncTransportError(
|
|
285
|
-
`
|
|
310
|
+
`Snapshot chunk download failed: ${getErrorMessage(error) || response.statusText}`,
|
|
286
311
|
response.status
|
|
287
312
|
);
|
|
288
313
|
}
|
|
289
314
|
|
|
290
|
-
return data as
|
|
315
|
+
return new Uint8Array(await (data as Blob).arrayBuffer());
|
|
291
316
|
},
|
|
292
317
|
|
|
293
|
-
async
|
|
318
|
+
async fetchSnapshotChunkStream(
|
|
294
319
|
request: { chunkId: string },
|
|
295
|
-
|
|
296
|
-
): Promise<Uint8Array
|
|
297
|
-
|
|
298
|
-
(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
transportOptions
|
|
320
|
+
options?: SyncTransportOptions
|
|
321
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
322
|
+
if (!transportOptions) {
|
|
323
|
+
const bytes = await this.fetchSnapshotChunk(request, options);
|
|
324
|
+
return bytesToReadableStream(bytes);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const fetchImpl = transportOptions.fetch ?? globalThis.fetch;
|
|
328
|
+
const requestUrl = resolveRequestUrl(
|
|
329
|
+
transportOptions.baseUrl,
|
|
330
|
+
`/sync/snapshot-chunks/${encodeURIComponent(request.chunkId)}`
|
|
305
331
|
);
|
|
306
332
|
|
|
307
|
-
|
|
333
|
+
const performRequest = async (
|
|
334
|
+
signal?: AbortSignal
|
|
335
|
+
): Promise<Response> => {
|
|
336
|
+
const headers = new Headers();
|
|
337
|
+
const extraHeaders = await transportOptions.getHeaders?.();
|
|
338
|
+
if (extraHeaders) {
|
|
339
|
+
for (const [key, value] of Object.entries(extraHeaders)) {
|
|
340
|
+
headers.set(key, value);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (!headers.has('x-syncular-transport-path')) {
|
|
344
|
+
headers.set(
|
|
345
|
+
'x-syncular-transport-path',
|
|
346
|
+
transportOptions.transportPath ?? 'direct'
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return fetchImpl(requestUrl, {
|
|
350
|
+
method: 'GET',
|
|
351
|
+
headers,
|
|
352
|
+
...(signal ? { signal } : {}),
|
|
353
|
+
});
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
let response = await performRequest(options?.signal);
|
|
357
|
+
if (
|
|
358
|
+
(response.status === 401 || response.status === 403) &&
|
|
359
|
+
options?.onAuthError
|
|
360
|
+
) {
|
|
361
|
+
const shouldRetry = await options.onAuthError();
|
|
362
|
+
if (shouldRetry) {
|
|
363
|
+
response = await performRequest(options.signal);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!response.ok) {
|
|
368
|
+
let reason = response.statusText || 'Request failed';
|
|
369
|
+
try {
|
|
370
|
+
const maybeJson = (await response.json()) as
|
|
371
|
+
| { error?: unknown; message?: unknown }
|
|
372
|
+
| undefined;
|
|
373
|
+
reason = getErrorMessage(maybeJson) || reason;
|
|
374
|
+
} catch {
|
|
375
|
+
// ignore parse failures
|
|
376
|
+
}
|
|
308
377
|
throw new SyncTransportError(
|
|
309
|
-
`Snapshot chunk download failed: ${
|
|
378
|
+
`Snapshot chunk download failed: ${reason}`,
|
|
310
379
|
response.status
|
|
311
380
|
);
|
|
312
381
|
}
|
|
313
382
|
|
|
314
|
-
|
|
383
|
+
if (!response.body) {
|
|
384
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
385
|
+
return bytesToReadableStream(bytes);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return response.body as ReadableStream<Uint8Array>;
|
|
315
389
|
},
|
|
316
390
|
|
|
317
391
|
// Include blob operations
|