@syncular/transport-http 0.0.3-3 → 0.0.3-7
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 +7 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/transport-options.test.ts +191 -0
- package/src/index.ts +103 -28
package/README.md
CHANGED
|
@@ -16,6 +16,13 @@ import { createHttpTransport } from '@syncular/transport-http';
|
|
|
16
16
|
const transport = createHttpTransport({
|
|
17
17
|
baseUrl: 'https://api.example.com',
|
|
18
18
|
getHeaders: () => ({ Authorization: `Bearer ${token}` }),
|
|
19
|
+
authLifecycle: {
|
|
20
|
+
onAuthExpired: ({ operation, status }) => {
|
|
21
|
+
console.warn('Auth expired', operation, status);
|
|
22
|
+
},
|
|
23
|
+
refreshToken: async () => auth.refreshToken(),
|
|
24
|
+
retryWithFreshToken: ({ refreshResult }) => refreshResult,
|
|
25
|
+
},
|
|
19
26
|
});
|
|
20
27
|
```
|
|
21
28
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides typed API clients using openapi-fetch with auto-generated types.
|
|
5
5
|
*/
|
|
6
|
-
import type { SyncTransport } from '@syncular/core';
|
|
6
|
+
import type { SyncAuthLifecycle, SyncTransport } from '@syncular/core';
|
|
7
7
|
import createClient from 'openapi-fetch';
|
|
8
8
|
import type { paths } from './generated/api';
|
|
9
|
-
export type { SyncTransport, SyncTransportBlobs, SyncTransportOptions, } from '@syncular/core';
|
|
9
|
+
export type { SyncAuthErrorContext, SyncAuthLifecycle, SyncAuthOperation, SyncTransport, SyncTransportBlobs, SyncTransportOptions, } from '@syncular/core';
|
|
10
10
|
export declare function unwrap<T>(promise: Promise<{
|
|
11
11
|
data?: T;
|
|
12
12
|
error?: unknown;
|
|
@@ -18,6 +18,8 @@ export interface ClientOptions {
|
|
|
18
18
|
baseUrl: string;
|
|
19
19
|
/** Function to get headers for requests (e.g., for auth tokens) */
|
|
20
20
|
getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
21
|
+
/** Shared auth lifecycle for all transport operations. */
|
|
22
|
+
authLifecycle?: SyncAuthLifecycle;
|
|
21
23
|
/** Custom fetch implementation (defaults to globalThis.fetch) */
|
|
22
24
|
fetch?: typeof globalThis.fetch;
|
|
23
25
|
/**
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAEV,iBAAiB,EAIjB,aAAa,EAGd,MAAM,gBAAgB,CAAC;AAExB,OAAO,YAAY,MAAM,eAAe,CAAC;AACzC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE7C,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AA+BxB,wBAAsB,MAAM,CAAC,CAAC,EAC5B,OAAO,EAAE,OAAO,CAAC;IAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,GAC9C,OAAO,CAAC,CAAC,CAAC,CAMZ;AAmCD,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;AAChE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5E,0DAA0D;IAC1D,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,iEAAiE;IACjE,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC;;;OAGG;IACH,aAAa,CAAC,EAAE,iBAAiB,CAAC;CACnC;AA0BD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,UAAU,CA2BlE;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,mBAAmB,CACjC,eAAe,EAAE,UAAU,GAAG,aAAa,GAC1C,aAAa,CA0Pf"}
|
package/dist/index.js
CHANGED
|
@@ -40,19 +40,16 @@ export async function unwrap(promise) {
|
|
|
40
40
|
}
|
|
41
41
|
return data;
|
|
42
42
|
}
|
|
43
|
-
async function executeWithAuthRetry(execute, options) {
|
|
43
|
+
async function executeWithAuthRetry(execute, options, operation, resolveAuthRetry) {
|
|
44
44
|
const first = await execute(options?.signal);
|
|
45
45
|
if (first.response.status !== 401 && first.response.status !== 403) {
|
|
46
46
|
return first;
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
return first;
|
|
50
|
-
}
|
|
51
|
-
const shouldRetry = await options.onAuthError();
|
|
48
|
+
const shouldRetry = await resolveAuthRetry({ operation, status: first.response.status }, options);
|
|
52
49
|
if (!shouldRetry) {
|
|
53
50
|
return first;
|
|
54
51
|
}
|
|
55
|
-
return execute(options
|
|
52
|
+
return execute(options?.signal);
|
|
56
53
|
}
|
|
57
54
|
function resolveRequestUrl(baseUrl, path) {
|
|
58
55
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
@@ -144,21 +141,51 @@ export function createHttpTransport(clientOrOptions) {
|
|
|
144
141
|
? clientOrOptions
|
|
145
142
|
: createApiClient(clientOrOptions);
|
|
146
143
|
const transportOptions = 'GET' in clientOrOptions ? undefined : clientOrOptions;
|
|
144
|
+
const defaultAuthLifecycle = transportOptions?.authLifecycle;
|
|
145
|
+
let refreshInFlight = null;
|
|
146
|
+
const runRefreshSingleFlight = async (lifecycle, context) => {
|
|
147
|
+
if (!lifecycle.refreshToken)
|
|
148
|
+
return false;
|
|
149
|
+
if (!refreshInFlight) {
|
|
150
|
+
refreshInFlight = Promise.resolve(lifecycle.refreshToken(context))
|
|
151
|
+
.then((result) => Boolean(result))
|
|
152
|
+
.finally(() => {
|
|
153
|
+
refreshInFlight = null;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return refreshInFlight;
|
|
157
|
+
};
|
|
158
|
+
const resolveAuthRetry = async (context, options) => {
|
|
159
|
+
if (options?.onAuthError) {
|
|
160
|
+
return Boolean(await options.onAuthError());
|
|
161
|
+
}
|
|
162
|
+
const lifecycle = options?.authLifecycle ?? defaultAuthLifecycle;
|
|
163
|
+
if (!lifecycle)
|
|
164
|
+
return false;
|
|
165
|
+
await lifecycle.onAuthExpired?.(context);
|
|
166
|
+
const refreshResult = await runRefreshSingleFlight(lifecycle, context);
|
|
167
|
+
if (lifecycle.retryWithFreshToken) {
|
|
168
|
+
return Boolean(await lifecycle.retryWithFreshToken({ ...context, refreshResult }));
|
|
169
|
+
}
|
|
170
|
+
return refreshResult;
|
|
171
|
+
};
|
|
147
172
|
// Create blob operations using the typed OpenAPI client
|
|
148
173
|
const blobs = {
|
|
149
174
|
async initiateUpload(args) {
|
|
150
|
-
const { data, error, response } = await client.POST('/sync/blobs/upload', {
|
|
175
|
+
const { data, error, response } = await executeWithAuthRetry((signal) => client.POST('/sync/blobs/upload', {
|
|
151
176
|
body: args,
|
|
152
|
-
|
|
177
|
+
...(signal ? { signal } : {}),
|
|
178
|
+
}), undefined, 'blobInitiateUpload', resolveAuthRetry);
|
|
153
179
|
if (error || !data) {
|
|
154
180
|
throw new SyncTransportError(`Blob upload init failed: ${getErrorMessage(error) || response.statusText}`, response.status);
|
|
155
181
|
}
|
|
156
182
|
return data;
|
|
157
183
|
},
|
|
158
184
|
async completeUpload(hash) {
|
|
159
|
-
const { data, error } = await client.POST('/sync/blobs/{hash}/complete', {
|
|
185
|
+
const { data, error } = await executeWithAuthRetry((signal) => client.POST('/sync/blobs/{hash}/complete', {
|
|
160
186
|
params: { path: { hash } },
|
|
161
|
-
|
|
187
|
+
...(signal ? { signal } : {}),
|
|
188
|
+
}), undefined, 'blobCompleteUpload', resolveAuthRetry);
|
|
162
189
|
if (error || !data) {
|
|
163
190
|
return {
|
|
164
191
|
ok: false,
|
|
@@ -168,9 +195,10 @@ export function createHttpTransport(clientOrOptions) {
|
|
|
168
195
|
return data;
|
|
169
196
|
},
|
|
170
197
|
async getDownloadUrl(hash) {
|
|
171
|
-
const { data, error, response } = await client.GET('/sync/blobs/{hash}/url', {
|
|
198
|
+
const { data, error, response } = await executeWithAuthRetry((signal) => client.GET('/sync/blobs/{hash}/url', {
|
|
172
199
|
params: { path: { hash } },
|
|
173
|
-
|
|
200
|
+
...(signal ? { signal } : {}),
|
|
201
|
+
}), undefined, 'blobGetDownloadUrl', resolveAuthRetry);
|
|
174
202
|
if (error || !data) {
|
|
175
203
|
throw new SyncTransportError(`Get download URL failed: ${getErrorMessage(error) || response.statusText}`, response.status);
|
|
176
204
|
}
|
|
@@ -185,7 +213,7 @@ export function createHttpTransport(clientOrOptions) {
|
|
|
185
213
|
const { data, error, response } = await executeWithAuthRetry((signal) => client.POST('/sync', {
|
|
186
214
|
body: request,
|
|
187
215
|
...(signal ? { signal } : {}),
|
|
188
|
-
}), transportOptions);
|
|
216
|
+
}), transportOptions, 'sync', resolveAuthRetry);
|
|
189
217
|
if (error || !data) {
|
|
190
218
|
throw new SyncTransportError(`Sync failed: ${getErrorMessage(error) || response.statusText}`, response.status);
|
|
191
219
|
}
|
|
@@ -196,7 +224,7 @@ export function createHttpTransport(clientOrOptions) {
|
|
|
196
224
|
params: { path: { chunkId: request.chunkId } },
|
|
197
225
|
parseAs: 'blob',
|
|
198
226
|
...(signal ? { signal } : {}),
|
|
199
|
-
}), transportOptions);
|
|
227
|
+
}), transportOptions, 'snapshotChunk', resolveAuthRetry);
|
|
200
228
|
if (error || !data) {
|
|
201
229
|
throw new SyncTransportError(`Snapshot chunk download failed: ${getErrorMessage(error) || response.statusText}`, response.status);
|
|
202
230
|
}
|
|
@@ -227,11 +255,13 @@ export function createHttpTransport(clientOrOptions) {
|
|
|
227
255
|
});
|
|
228
256
|
};
|
|
229
257
|
let response = await performRequest(options?.signal);
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
258
|
+
if (response.status === 401 || response.status === 403) {
|
|
259
|
+
const shouldRetry = await resolveAuthRetry({
|
|
260
|
+
operation: 'snapshotChunkStream',
|
|
261
|
+
status: response.status,
|
|
262
|
+
}, options);
|
|
233
263
|
if (shouldRetry) {
|
|
234
|
-
response = await performRequest(options
|
|
264
|
+
response = await performRequest(options?.signal);
|
|
235
265
|
}
|
|
236
266
|
}
|
|
237
267
|
if (!response.ok) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/transport-http",
|
|
3
|
-
"version": "0.0.3-
|
|
3
|
+
"version": "0.0.3-7",
|
|
4
4
|
"description": "HTTP transport for Syncular client-server communication",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"release": "bunx syncular-publish"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@syncular/core": "0.0.3-
|
|
46
|
+
"@syncular/core": "0.0.3-7",
|
|
47
47
|
"openapi-fetch": "^0.17.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
@@ -195,4 +195,195 @@ describe('createHttpTransport SyncTransportOptions', () => {
|
|
|
195
195
|
expect(requestCount).toBe(2);
|
|
196
196
|
expect(authErrorCount).toBe(1);
|
|
197
197
|
});
|
|
198
|
+
|
|
199
|
+
it('supports auth lifecycle callbacks on 401/403', async () => {
|
|
200
|
+
let requestCount = 0;
|
|
201
|
+
let authExpiredCount = 0;
|
|
202
|
+
let refreshCount = 0;
|
|
203
|
+
let retryCount = 0;
|
|
204
|
+
|
|
205
|
+
const transport = createHttpTransport({
|
|
206
|
+
baseUrl: 'http://localhost',
|
|
207
|
+
authLifecycle: {
|
|
208
|
+
onAuthExpired: async (context) => {
|
|
209
|
+
authExpiredCount += 1;
|
|
210
|
+
expect(context.operation).toBe('sync');
|
|
211
|
+
expect(context.status).toBe(401);
|
|
212
|
+
},
|
|
213
|
+
refreshToken: async (context) => {
|
|
214
|
+
refreshCount += 1;
|
|
215
|
+
expect(context.operation).toBe('sync');
|
|
216
|
+
expect(context.status).toBe(401);
|
|
217
|
+
return true;
|
|
218
|
+
},
|
|
219
|
+
retryWithFreshToken: async (context) => {
|
|
220
|
+
retryCount += 1;
|
|
221
|
+
expect(context.operation).toBe('sync');
|
|
222
|
+
expect(context.status).toBe(401);
|
|
223
|
+
expect(context.refreshResult).toBe(true);
|
|
224
|
+
return context.refreshResult;
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
fetch: async () => {
|
|
228
|
+
requestCount += 1;
|
|
229
|
+
if (requestCount === 1) {
|
|
230
|
+
return new Response(JSON.stringify({ error: 'UNAUTHENTICATED' }), {
|
|
231
|
+
status: 401,
|
|
232
|
+
headers: { 'content-type': 'application/json' },
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return new Response(
|
|
236
|
+
JSON.stringify({ ok: true, pull: { ok: true, subscriptions: [] } }),
|
|
237
|
+
{
|
|
238
|
+
status: 200,
|
|
239
|
+
headers: { 'content-type': 'application/json' },
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const response = await transport.sync({
|
|
246
|
+
clientId: 'client-1',
|
|
247
|
+
pull: { limitCommits: 10, subscriptions: [] },
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(response.ok).toBe(true);
|
|
251
|
+
expect(requestCount).toBe(2);
|
|
252
|
+
expect(authExpiredCount).toBe(1);
|
|
253
|
+
expect(refreshCount).toBe(1);
|
|
254
|
+
expect(retryCount).toBe(1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('deduplicates concurrent token refresh requests', async () => {
|
|
258
|
+
let requestCount = 0;
|
|
259
|
+
let refreshCount = 0;
|
|
260
|
+
|
|
261
|
+
const transport = createHttpTransport({
|
|
262
|
+
baseUrl: 'http://localhost',
|
|
263
|
+
authLifecycle: {
|
|
264
|
+
refreshToken: async () => {
|
|
265
|
+
refreshCount += 1;
|
|
266
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
267
|
+
return true;
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
fetch: async () => {
|
|
271
|
+
requestCount += 1;
|
|
272
|
+
if (requestCount <= 2) {
|
|
273
|
+
return new Response(JSON.stringify({ error: 'UNAUTHENTICATED' }), {
|
|
274
|
+
status: 401,
|
|
275
|
+
headers: { 'content-type': 'application/json' },
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return new Response(
|
|
279
|
+
JSON.stringify({ ok: true, pull: { ok: true, subscriptions: [] } }),
|
|
280
|
+
{
|
|
281
|
+
status: 200,
|
|
282
|
+
headers: { 'content-type': 'application/json' },
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const [first, second] = await Promise.all([
|
|
289
|
+
transport.sync({
|
|
290
|
+
clientId: 'client-a',
|
|
291
|
+
pull: { limitCommits: 10, subscriptions: [] },
|
|
292
|
+
}),
|
|
293
|
+
transport.sync({
|
|
294
|
+
clientId: 'client-b',
|
|
295
|
+
pull: { limitCommits: 10, subscriptions: [] },
|
|
296
|
+
}),
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
expect(first.ok).toBe(true);
|
|
300
|
+
expect(second.ok).toBe(true);
|
|
301
|
+
expect(refreshCount).toBe(1);
|
|
302
|
+
expect(requestCount).toBe(4);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('prioritizes per-call onAuthError over shared auth lifecycle', async () => {
|
|
306
|
+
let legacyCount = 0;
|
|
307
|
+
let refreshCount = 0;
|
|
308
|
+
|
|
309
|
+
const transport = createHttpTransport({
|
|
310
|
+
baseUrl: 'http://localhost',
|
|
311
|
+
authLifecycle: {
|
|
312
|
+
refreshToken: async () => {
|
|
313
|
+
refreshCount += 1;
|
|
314
|
+
return true;
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
fetch: async () =>
|
|
318
|
+
new Response(JSON.stringify({ error: 'UNAUTHENTICATED' }), {
|
|
319
|
+
status: 401,
|
|
320
|
+
headers: { 'content-type': 'application/json' },
|
|
321
|
+
}),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await expect(
|
|
325
|
+
transport.sync(
|
|
326
|
+
{
|
|
327
|
+
clientId: 'client-1',
|
|
328
|
+
pull: { limitCommits: 10, subscriptions: [] },
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
onAuthError: async () => {
|
|
332
|
+
legacyCount += 1;
|
|
333
|
+
return false;
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
).rejects.toMatchObject({ status: 401 });
|
|
338
|
+
|
|
339
|
+
expect(legacyCount).toBe(1);
|
|
340
|
+
expect(refreshCount).toBe(0);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('retries streamed snapshot chunk fetch via shared auth lifecycle', async () => {
|
|
344
|
+
let requestCount = 0;
|
|
345
|
+
let refreshCount = 0;
|
|
346
|
+
|
|
347
|
+
const transport = createHttpTransport({
|
|
348
|
+
baseUrl: 'http://localhost',
|
|
349
|
+
authLifecycle: {
|
|
350
|
+
refreshToken: async () => {
|
|
351
|
+
refreshCount += 1;
|
|
352
|
+
return true;
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
fetch: async () => {
|
|
356
|
+
requestCount += 1;
|
|
357
|
+
if (requestCount === 1) {
|
|
358
|
+
return new Response(JSON.stringify({ error: 'UNAUTHENTICATED' }), {
|
|
359
|
+
status: 401,
|
|
360
|
+
headers: { 'content-type': 'application/json' },
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
return new Response(new Uint8Array([5, 4, 3]), {
|
|
364
|
+
status: 200,
|
|
365
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
366
|
+
});
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const stream = await transport.fetchSnapshotChunkStream?.({
|
|
371
|
+
chunkId: 'chunk-shared-auth',
|
|
372
|
+
});
|
|
373
|
+
expect(stream).toBeDefined();
|
|
374
|
+
|
|
375
|
+
const reader = stream!.getReader();
|
|
376
|
+
const collected: number[] = [];
|
|
377
|
+
while (true) {
|
|
378
|
+
const { done, value } = await reader.read();
|
|
379
|
+
if (done) break;
|
|
380
|
+
if (!value) continue;
|
|
381
|
+
collected.push(...value);
|
|
382
|
+
}
|
|
383
|
+
reader.releaseLock();
|
|
384
|
+
|
|
385
|
+
expect(collected).toEqual([5, 4, 3]);
|
|
386
|
+
expect(refreshCount).toBe(1);
|
|
387
|
+
expect(requestCount).toBe(2);
|
|
388
|
+
});
|
|
198
389
|
});
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {
|
|
8
|
+
SyncAuthErrorContext,
|
|
9
|
+
SyncAuthLifecycle,
|
|
10
|
+
SyncAuthOperation,
|
|
8
11
|
SyncCombinedRequest,
|
|
9
12
|
SyncCombinedResponse,
|
|
10
13
|
SyncTransport,
|
|
@@ -16,6 +19,9 @@ import createClient from 'openapi-fetch';
|
|
|
16
19
|
import type { paths } from './generated/api';
|
|
17
20
|
|
|
18
21
|
export type {
|
|
22
|
+
SyncAuthErrorContext,
|
|
23
|
+
SyncAuthLifecycle,
|
|
24
|
+
SyncAuthOperation,
|
|
19
25
|
SyncTransport,
|
|
20
26
|
SyncTransportBlobs,
|
|
21
27
|
SyncTransportOptions,
|
|
@@ -66,24 +72,30 @@ type ApiResult<T> = {
|
|
|
66
72
|
response: Response;
|
|
67
73
|
};
|
|
68
74
|
|
|
75
|
+
type ResolveAuthRetry = (
|
|
76
|
+
context: SyncAuthErrorContext,
|
|
77
|
+
options?: SyncTransportOptions
|
|
78
|
+
) => Promise<boolean>;
|
|
79
|
+
|
|
69
80
|
async function executeWithAuthRetry<T>(
|
|
70
81
|
execute: (signal?: AbortSignal) => Promise<ApiResult<T>>,
|
|
71
|
-
options
|
|
82
|
+
options: SyncTransportOptions | undefined,
|
|
83
|
+
operation: SyncAuthOperation,
|
|
84
|
+
resolveAuthRetry: ResolveAuthRetry
|
|
72
85
|
): Promise<ApiResult<T>> {
|
|
73
86
|
const first = await execute(options?.signal);
|
|
74
87
|
if (first.response.status !== 401 && first.response.status !== 403) {
|
|
75
88
|
return first;
|
|
76
89
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const shouldRetry = await options.onAuthError();
|
|
90
|
+
const shouldRetry = await resolveAuthRetry(
|
|
91
|
+
{ operation, status: first.response.status },
|
|
92
|
+
options
|
|
93
|
+
);
|
|
82
94
|
if (!shouldRetry) {
|
|
83
95
|
return first;
|
|
84
96
|
}
|
|
85
97
|
|
|
86
|
-
return execute(options
|
|
98
|
+
return execute(options?.signal);
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
// Re-export useful types from the generated API
|
|
@@ -95,6 +107,8 @@ export interface ClientOptions {
|
|
|
95
107
|
baseUrl: string;
|
|
96
108
|
/** Function to get headers for requests (e.g., for auth tokens) */
|
|
97
109
|
getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
110
|
+
/** Shared auth lifecycle for all transport operations. */
|
|
111
|
+
authLifecycle?: SyncAuthLifecycle;
|
|
98
112
|
/** Custom fetch implementation (defaults to globalThis.fetch) */
|
|
99
113
|
fetch?: typeof globalThis.fetch;
|
|
100
114
|
/**
|
|
@@ -209,15 +223,58 @@ export function createHttpTransport(
|
|
|
209
223
|
: createApiClient(clientOrOptions);
|
|
210
224
|
const transportOptions =
|
|
211
225
|
'GET' in clientOrOptions ? undefined : clientOrOptions;
|
|
226
|
+
const defaultAuthLifecycle = transportOptions?.authLifecycle;
|
|
227
|
+
|
|
228
|
+
let refreshInFlight: Promise<boolean> | null = null;
|
|
229
|
+
|
|
230
|
+
const runRefreshSingleFlight = async (
|
|
231
|
+
lifecycle: SyncAuthLifecycle,
|
|
232
|
+
context: SyncAuthErrorContext
|
|
233
|
+
): Promise<boolean> => {
|
|
234
|
+
if (!lifecycle.refreshToken) return false;
|
|
235
|
+
|
|
236
|
+
if (!refreshInFlight) {
|
|
237
|
+
refreshInFlight = Promise.resolve(lifecycle.refreshToken(context))
|
|
238
|
+
.then((result) => Boolean(result))
|
|
239
|
+
.finally(() => {
|
|
240
|
+
refreshInFlight = null;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return refreshInFlight;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const resolveAuthRetry: ResolveAuthRetry = async (context, options) => {
|
|
248
|
+
if (options?.onAuthError) {
|
|
249
|
+
return Boolean(await options.onAuthError());
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const lifecycle = options?.authLifecycle ?? defaultAuthLifecycle;
|
|
253
|
+
if (!lifecycle) return false;
|
|
254
|
+
|
|
255
|
+
await lifecycle.onAuthExpired?.(context);
|
|
256
|
+
|
|
257
|
+
const refreshResult = await runRefreshSingleFlight(lifecycle, context);
|
|
258
|
+
if (lifecycle.retryWithFreshToken) {
|
|
259
|
+
return Boolean(
|
|
260
|
+
await lifecycle.retryWithFreshToken({ ...context, refreshResult })
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return refreshResult;
|
|
264
|
+
};
|
|
212
265
|
|
|
213
266
|
// Create blob operations using the typed OpenAPI client
|
|
214
267
|
const blobs: SyncTransportBlobs = {
|
|
215
268
|
async initiateUpload(args) {
|
|
216
|
-
const { data, error, response } = await
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
269
|
+
const { data, error, response } = await executeWithAuthRetry(
|
|
270
|
+
(signal) =>
|
|
271
|
+
client.POST('/sync/blobs/upload', {
|
|
272
|
+
body: args,
|
|
273
|
+
...(signal ? { signal } : {}),
|
|
274
|
+
}),
|
|
275
|
+
undefined,
|
|
276
|
+
'blobInitiateUpload',
|
|
277
|
+
resolveAuthRetry
|
|
221
278
|
);
|
|
222
279
|
|
|
223
280
|
if (error || !data) {
|
|
@@ -231,9 +288,16 @@ export function createHttpTransport(
|
|
|
231
288
|
},
|
|
232
289
|
|
|
233
290
|
async completeUpload(hash) {
|
|
234
|
-
const { data, error } = await
|
|
235
|
-
|
|
236
|
-
|
|
291
|
+
const { data, error } = await executeWithAuthRetry(
|
|
292
|
+
(signal) =>
|
|
293
|
+
client.POST('/sync/blobs/{hash}/complete', {
|
|
294
|
+
params: { path: { hash } },
|
|
295
|
+
...(signal ? { signal } : {}),
|
|
296
|
+
}),
|
|
297
|
+
undefined,
|
|
298
|
+
'blobCompleteUpload',
|
|
299
|
+
resolveAuthRetry
|
|
300
|
+
);
|
|
237
301
|
|
|
238
302
|
if (error || !data) {
|
|
239
303
|
return {
|
|
@@ -246,11 +310,15 @@ export function createHttpTransport(
|
|
|
246
310
|
},
|
|
247
311
|
|
|
248
312
|
async getDownloadUrl(hash) {
|
|
249
|
-
const { data, error, response } = await
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
313
|
+
const { data, error, response } = await executeWithAuthRetry(
|
|
314
|
+
(signal) =>
|
|
315
|
+
client.GET('/sync/blobs/{hash}/url', {
|
|
316
|
+
params: { path: { hash } },
|
|
317
|
+
...(signal ? { signal } : {}),
|
|
318
|
+
}),
|
|
319
|
+
undefined,
|
|
320
|
+
'blobGetDownloadUrl',
|
|
321
|
+
resolveAuthRetry
|
|
254
322
|
);
|
|
255
323
|
|
|
256
324
|
if (error || !data) {
|
|
@@ -278,7 +346,9 @@ export function createHttpTransport(
|
|
|
278
346
|
body: request,
|
|
279
347
|
...(signal ? { signal } : {}),
|
|
280
348
|
}),
|
|
281
|
-
transportOptions
|
|
349
|
+
transportOptions,
|
|
350
|
+
'sync',
|
|
351
|
+
resolveAuthRetry
|
|
282
352
|
);
|
|
283
353
|
|
|
284
354
|
if (error || !data) {
|
|
@@ -302,7 +372,9 @@ export function createHttpTransport(
|
|
|
302
372
|
parseAs: 'blob',
|
|
303
373
|
...(signal ? { signal } : {}),
|
|
304
374
|
}),
|
|
305
|
-
transportOptions
|
|
375
|
+
transportOptions,
|
|
376
|
+
'snapshotChunk',
|
|
377
|
+
resolveAuthRetry
|
|
306
378
|
);
|
|
307
379
|
|
|
308
380
|
if (error || !data) {
|
|
@@ -354,13 +426,16 @@ export function createHttpTransport(
|
|
|
354
426
|
};
|
|
355
427
|
|
|
356
428
|
let response = await performRequest(options?.signal);
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
429
|
+
if (response.status === 401 || response.status === 403) {
|
|
430
|
+
const shouldRetry = await resolveAuthRetry(
|
|
431
|
+
{
|
|
432
|
+
operation: 'snapshotChunkStream',
|
|
433
|
+
status: response.status,
|
|
434
|
+
},
|
|
435
|
+
options
|
|
436
|
+
);
|
|
362
437
|
if (shouldRetry) {
|
|
363
|
-
response = await performRequest(options
|
|
438
|
+
response = await performRequest(options?.signal);
|
|
364
439
|
}
|
|
365
440
|
}
|
|
366
441
|
|