@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/src/index.ts CHANGED
@@ -5,10 +5,8 @@
5
5
  */
6
6
 
7
7
  import type {
8
- SyncPullRequest,
9
- SyncPullResponse,
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/pull', { body: { ... } });
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 pull(
248
- request: SyncPullRequest,
271
+ async sync(
272
+ request: SyncCombinedRequest,
249
273
  transportOptions?: SyncTransportOptions
250
- ): Promise<SyncPullResponse> {
274
+ ): Promise<SyncCombinedResponse> {
251
275
  const { data, error, response } = await executeWithAuthRetry(
252
276
  (signal) =>
253
- client.POST('/sync/pull', {
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
- `Pull failed: ${getErrorMessage(error) || response.statusText}`,
286
+ `Sync failed: ${getErrorMessage(error) || response.statusText}`,
263
287
  response.status
264
288
  );
265
289
  }
266
290
 
267
- return data as SyncPullResponse;
291
+ return data as SyncCombinedResponse;
268
292
  },
269
293
 
270
- async push(
271
- request: SyncPushRequest,
294
+ async fetchSnapshotChunk(
295
+ request: { chunkId: string },
272
296
  transportOptions?: SyncTransportOptions
273
- ): Promise<SyncPushResponse> {
297
+ ): Promise<Uint8Array> {
274
298
  const { data, error, response } = await executeWithAuthRetry(
275
299
  (signal) =>
276
- client.POST('/sync/push', {
277
- body: request,
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
- `Push failed: ${getErrorMessage(error) || response.statusText}`,
310
+ `Snapshot chunk download failed: ${getErrorMessage(error) || response.statusText}`,
286
311
  response.status
287
312
  );
288
313
  }
289
314
 
290
- return data as SyncPushResponse;
315
+ return new Uint8Array(await (data as Blob).arrayBuffer());
291
316
  },
292
317
 
293
- async fetchSnapshotChunk(
318
+ async fetchSnapshotChunkStream(
294
319
  request: { chunkId: string },
295
- transportOptions?: SyncTransportOptions
296
- ): Promise<Uint8Array> {
297
- const { data, error, response } = await executeWithAuthRetry(
298
- (signal) =>
299
- client.GET('/sync/snapshot-chunks/{chunkId}', {
300
- params: { path: { chunkId: request.chunkId } },
301
- parseAs: 'blob',
302
- ...(signal ? { signal } : {}),
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
- if (error || !data) {
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: ${getErrorMessage(error) || response.statusText}`,
378
+ `Snapshot chunk download failed: ${reason}`,
310
379
  response.status
311
380
  );
312
381
  }
313
382
 
314
- return new Uint8Array(await (data as Blob).arrayBuffer());
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