@postrun/react 0.1.0 → 1.0.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 +59 -5
- package/dist/index.cjs +421 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +483 -124
- package/dist/index.d.ts +483 -124
- package/dist/index.js +417 -118
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.cjs
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
var react = require('react');
|
|
5
5
|
var reactQuery = require('@tanstack/react-query');
|
|
6
6
|
var js = require('@postrun/js');
|
|
7
|
-
var
|
|
7
|
+
var Nango = require('@nangohq/frontend');
|
|
8
8
|
var pWaitFor = require('p-wait-for');
|
|
9
|
+
var pLimit = require('p-limit');
|
|
10
|
+
var pRetry = require('p-retry');
|
|
9
11
|
var axios = require('axios');
|
|
10
12
|
var reactTweet = require('react-tweet');
|
|
11
13
|
var fi = require('react-icons/fi');
|
|
@@ -15,8 +17,10 @@ var lu = require('react-icons/lu');
|
|
|
15
17
|
|
|
16
18
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
17
19
|
|
|
18
|
-
var
|
|
20
|
+
var Nango__default = /*#__PURE__*/_interopDefault(Nango);
|
|
19
21
|
var pWaitFor__default = /*#__PURE__*/_interopDefault(pWaitFor);
|
|
22
|
+
var pLimit__default = /*#__PURE__*/_interopDefault(pLimit);
|
|
23
|
+
var pRetry__default = /*#__PURE__*/_interopDefault(pRetry);
|
|
20
24
|
var axios__default = /*#__PURE__*/_interopDefault(axios);
|
|
21
25
|
var twitterText__default = /*#__PURE__*/_interopDefault(twitterText);
|
|
22
26
|
|
|
@@ -98,7 +102,8 @@ var profileKeys = {
|
|
|
98
102
|
list: (query) => [...profileKeys.lists(), query ?? {}],
|
|
99
103
|
// Nested under lists() so a create/update/delete invalidating lists() also
|
|
100
104
|
// refreshes the infinite cache; distinct tail so the two cache shapes (a
|
|
101
|
-
// single Page vs accumulated pages) never collide on one key.
|
|
105
|
+
// single Page vs accumulated pages) never collide on one key. The filter omits
|
|
106
|
+
// limit/offset — the infinite hook owns pagination, so they never key the cache.
|
|
102
107
|
infinite: (query) => [...profileKeys.lists(), "infinite", query ?? {}],
|
|
103
108
|
details: () => [...profileKeys.all, "detail"],
|
|
104
109
|
detail: (id) => [...profileKeys.details(), id]
|
|
@@ -109,20 +114,28 @@ var postKeys = {
|
|
|
109
114
|
list: (query) => [...postKeys.lists(), query ?? {}],
|
|
110
115
|
// Nested under lists() so a create/update/delete invalidating lists() also
|
|
111
116
|
// refreshes the infinite cache; distinct tail so the two cache shapes (a
|
|
112
|
-
// single Page vs accumulated pages) never collide on one key.
|
|
117
|
+
// single Page vs accumulated pages) never collide on one key. The filter omits
|
|
118
|
+
// limit/offset — the infinite hook owns pagination, so they never key the cache.
|
|
113
119
|
infinite: (query) => [...postKeys.lists(), "infinite", query ?? {}],
|
|
114
120
|
details: () => [...postKeys.all, "detail"],
|
|
115
121
|
detail: (id) => [...postKeys.details(), id]
|
|
116
122
|
};
|
|
117
123
|
var mediaKeys = {
|
|
118
124
|
all: [ROOT, "media"],
|
|
125
|
+
lists: () => [...mediaKeys.all, "list"],
|
|
126
|
+
list: (query) => [...mediaKeys.lists(), query ?? {}],
|
|
127
|
+
// Nested under lists() so an upload/update/delete invalidating lists() also
|
|
128
|
+
// refreshes the infinite cache; distinct tail so the two cache shapes (a
|
|
129
|
+
// single Page vs accumulated pages) never collide on one key. The filter omits
|
|
130
|
+
// limit/offset — the infinite hook owns pagination, so they never key the cache.
|
|
131
|
+
infinite: (query) => [...mediaKeys.lists(), "infinite", query ?? {}],
|
|
119
132
|
details: () => [...mediaKeys.all, "detail"],
|
|
120
133
|
detail: (id) => [...mediaKeys.details(), id]
|
|
121
134
|
};
|
|
122
135
|
var connectionKeys = {
|
|
123
136
|
all: [ROOT, "connections"],
|
|
124
137
|
lists: () => [...connectionKeys.all, "list"],
|
|
125
|
-
list: (profileId) => [...connectionKeys.lists(), profileId],
|
|
138
|
+
list: (profileId, filter) => [...connectionKeys.lists(), profileId, filter ?? {}],
|
|
126
139
|
details: () => [...connectionKeys.all, "detail"],
|
|
127
140
|
detail: (id) => [...connectionKeys.details(), id],
|
|
128
141
|
accounts: (id) => [...connectionKeys.all, "accounts", id]
|
|
@@ -194,36 +207,256 @@ function useDeleteProfile() {
|
|
|
194
207
|
queryClient
|
|
195
208
|
);
|
|
196
209
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
210
|
+
var PENDING = { status: "connected_pending" };
|
|
211
|
+
var CANCELLED = { status: "cancelled" };
|
|
212
|
+
var active = (connection) => ({
|
|
213
|
+
status: "active",
|
|
214
|
+
connection
|
|
215
|
+
});
|
|
216
|
+
var failed = (reason) => ({
|
|
217
|
+
status: "error",
|
|
218
|
+
reason
|
|
219
|
+
});
|
|
220
|
+
function outcomeForAuthError(error) {
|
|
221
|
+
switch (error.type) {
|
|
222
|
+
case "window_closed":
|
|
223
|
+
return CANCELLED;
|
|
224
|
+
case "blocked_by_browser":
|
|
225
|
+
return failed("popup_blocked");
|
|
226
|
+
case "missing_auth_token":
|
|
227
|
+
case "invalid_host_url":
|
|
228
|
+
case "missing_credentials":
|
|
229
|
+
case "connection_test_failed":
|
|
230
|
+
case "missing_connect_session_token":
|
|
231
|
+
case "connection_validation_failed":
|
|
232
|
+
case "resource_capped":
|
|
233
|
+
case "unknown_error":
|
|
234
|
+
return failed("auth_failed");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function grant(authorize) {
|
|
238
|
+
try {
|
|
239
|
+
return { ok: true, connectionId: await authorize() };
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error instanceof Nango.AuthError) {
|
|
242
|
+
return { ok: false, outcome: outcomeForAuthError(error) };
|
|
243
|
+
}
|
|
244
|
+
return { ok: false, outcome: failed("auth_failed") };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function awaitGrantedConnection(deps, nangoConnectionId) {
|
|
248
|
+
try {
|
|
249
|
+
return await pWaitFor__default.default(
|
|
250
|
+
async () => {
|
|
251
|
+
const rows = await deps.listByNangoConnectionId(nangoConnectionId);
|
|
252
|
+
const match = rows[0];
|
|
253
|
+
return match ? pWaitFor__default.default.resolveWith(match) : false;
|
|
254
|
+
},
|
|
255
|
+
{ interval: deps.pollIntervalMs, timeout: deps.pollTimeoutMs }
|
|
256
|
+
);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (error instanceof pWaitFor.TimeoutError) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async function bindPendingConnection(deps, connection) {
|
|
265
|
+
let accounts;
|
|
266
|
+
try {
|
|
267
|
+
accounts = await deps.discoverAccounts(connection.id);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (error instanceof js.PostrunError && error.code === "not_implemented") {
|
|
270
|
+
return PENDING;
|
|
271
|
+
}
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
if (accounts.length === 0) {
|
|
275
|
+
return PENDING;
|
|
276
|
+
}
|
|
277
|
+
const chosen = await deps.chooseAccount(accounts);
|
|
278
|
+
try {
|
|
279
|
+
return active(await deps.selectAccount(connection.id, chosen));
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (error instanceof js.PostrunError) {
|
|
282
|
+
return error.code === "connection_reauth_required" ? failed("reauth_required") : failed("select_failed");
|
|
283
|
+
}
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function runEmbeddedConnect(deps) {
|
|
288
|
+
const granted = await grant(deps.authorize);
|
|
289
|
+
if (!granted.ok) {
|
|
290
|
+
return granted.outcome;
|
|
291
|
+
}
|
|
292
|
+
let connection;
|
|
293
|
+
try {
|
|
294
|
+
connection = await awaitGrantedConnection(deps, granted.connectionId);
|
|
295
|
+
} catch {
|
|
296
|
+
return failed("connection_not_found");
|
|
297
|
+
}
|
|
298
|
+
if (connection === null) {
|
|
299
|
+
return PENDING;
|
|
300
|
+
}
|
|
301
|
+
if (connection.external_account_id !== null) {
|
|
302
|
+
return active(connection);
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
return await bindPendingConnection(deps, connection);
|
|
306
|
+
} catch {
|
|
307
|
+
return failed("select_failed");
|
|
308
|
+
}
|
|
201
309
|
}
|
|
202
310
|
|
|
203
311
|
// src/connections.ts
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
312
|
+
var POLL_INTERVAL_MS = 1500;
|
|
313
|
+
var POLL_TIMEOUT_MS = 15e3;
|
|
314
|
+
function useConnect({
|
|
315
|
+
profileId,
|
|
316
|
+
platform,
|
|
317
|
+
onConnected
|
|
318
|
+
}) {
|
|
319
|
+
const { client } = usePostrun();
|
|
320
|
+
const [state, setState] = react.useState({ phase: "preparing" });
|
|
321
|
+
const [remintNonce, setRemintNonce] = react.useState(0);
|
|
322
|
+
const sessionRef = react.useRef(null);
|
|
323
|
+
const pickRef = react.useRef(null);
|
|
324
|
+
const inFlightRef = react.useRef(false);
|
|
325
|
+
const flowGenRef = react.useRef(0);
|
|
326
|
+
const onConnectedRef = react.useRef(onConnected);
|
|
327
|
+
react.useEffect(() => {
|
|
328
|
+
onConnectedRef.current = onConnected;
|
|
329
|
+
}, [onConnected]);
|
|
330
|
+
const abandonFlow = react.useCallback(() => {
|
|
331
|
+
flowGenRef.current += 1;
|
|
332
|
+
inFlightRef.current = false;
|
|
333
|
+
const pick = pickRef.current;
|
|
334
|
+
pickRef.current = null;
|
|
335
|
+
pick?.reject(new Error("connect flow abandoned"));
|
|
336
|
+
}, []);
|
|
337
|
+
react.useEffect(() => {
|
|
338
|
+
let abandoned = false;
|
|
339
|
+
setState({ phase: "preparing" });
|
|
340
|
+
sessionRef.current = null;
|
|
341
|
+
js.connectionsConnect({ client, path: { id: profileId }, body: { platform } }).then(({ data }) => {
|
|
342
|
+
if (abandoned || !data) return;
|
|
343
|
+
sessionRef.current = {
|
|
344
|
+
token: data.connect_session_token,
|
|
345
|
+
providerConfigKey: data.provider_config_key,
|
|
346
|
+
host: data.nango_host
|
|
347
|
+
};
|
|
348
|
+
setState({ phase: "idle" });
|
|
349
|
+
}).catch(() => {
|
|
350
|
+
if (!abandoned) setState({ phase: "error", reason: "auth_failed" });
|
|
351
|
+
});
|
|
352
|
+
return () => {
|
|
353
|
+
abandoned = true;
|
|
354
|
+
abandonFlow();
|
|
355
|
+
};
|
|
356
|
+
}, [client, profileId, platform, remintNonce, abandonFlow]);
|
|
357
|
+
const start = react.useCallback(() => {
|
|
358
|
+
const session = sessionRef.current;
|
|
359
|
+
if (!session || inFlightRef.current) return;
|
|
360
|
+
inFlightRef.current = true;
|
|
361
|
+
const gen = flowGenRef.current;
|
|
362
|
+
const isCurrent = () => flowGenRef.current === gen;
|
|
363
|
+
setState({ phase: "connecting" });
|
|
364
|
+
void runEmbeddedConnect({
|
|
365
|
+
// Nango lives INSIDE `authorize` so a SYNCHRONOUS throw (invalid host /
|
|
366
|
+
// missing token — the Nango SDK throws `AuthError` synchronously) becomes a
|
|
367
|
+
// promise rejection that `grant()` maps to `auth_failed`, never an uncaught
|
|
368
|
+
// throw escaping the click and wedging `inFlightRef`. Gesture timing still
|
|
369
|
+
// holds: `authorize()` is invoked SYNCHRONOUSLY down the
|
|
370
|
+
// start → runEmbeddedConnect → grant chain (each `await`'s operand is
|
|
371
|
+
// evaluated before it suspends), so `nango.auth()`'s `window.open` fires
|
|
372
|
+
// inside the user gesture, with no `await` before it.
|
|
373
|
+
authorize: async () => {
|
|
374
|
+
const nango = new Nango__default.default({
|
|
375
|
+
host: session.host,
|
|
376
|
+
connectSessionToken: session.token
|
|
377
|
+
});
|
|
378
|
+
const result = await nango.auth(session.providerConfigKey, {
|
|
379
|
+
detectClosedAuthWindow: true
|
|
380
|
+
});
|
|
381
|
+
return result.connectionId;
|
|
382
|
+
},
|
|
383
|
+
chooseAccount: (accounts) => new Promise((resolve, reject) => {
|
|
384
|
+
if (!isCurrent()) {
|
|
385
|
+
reject(new Error("connect flow abandoned"));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
pickRef.current = { resolve, reject };
|
|
389
|
+
setState({ phase: "picking", accounts });
|
|
390
|
+
}),
|
|
391
|
+
listByNangoConnectionId: async (nangoConnectionId) => {
|
|
392
|
+
const { data } = await js.connectionsListByProfile({
|
|
210
393
|
client,
|
|
211
394
|
path: { id: profileId },
|
|
212
|
-
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
|
|
395
|
+
query: { nango_connection_id: nangoConnectionId }
|
|
396
|
+
});
|
|
397
|
+
return data?.data ?? [];
|
|
398
|
+
},
|
|
399
|
+
discoverAccounts: async (connectionId) => {
|
|
400
|
+
const { data } = await js.connectionsListAccounts({
|
|
401
|
+
client,
|
|
402
|
+
path: { id: connectionId }
|
|
403
|
+
});
|
|
404
|
+
return data?.data ?? [];
|
|
405
|
+
},
|
|
406
|
+
selectAccount: async (connectionId, externalAccountId) => {
|
|
407
|
+
const { data } = await js.connectionsSelect({
|
|
408
|
+
client,
|
|
409
|
+
path: { id: connectionId },
|
|
410
|
+
body: { external_account_id: externalAccountId }
|
|
411
|
+
});
|
|
412
|
+
if (!data) throw new Error("select returned no connection");
|
|
413
|
+
return data;
|
|
414
|
+
},
|
|
415
|
+
pollIntervalMs: POLL_INTERVAL_MS,
|
|
416
|
+
pollTimeoutMs: POLL_TIMEOUT_MS
|
|
417
|
+
}).then((outcome) => {
|
|
418
|
+
if (!isCurrent()) return;
|
|
419
|
+
inFlightRef.current = false;
|
|
420
|
+
pickRef.current = null;
|
|
421
|
+
switch (outcome.status) {
|
|
422
|
+
case "active":
|
|
423
|
+
setState({ phase: "active", connection: outcome.connection });
|
|
424
|
+
onConnectedRef.current?.(outcome.connection);
|
|
425
|
+
return;
|
|
426
|
+
case "connected_pending":
|
|
427
|
+
setState({ phase: "connected_pending" });
|
|
428
|
+
return;
|
|
429
|
+
case "cancelled":
|
|
430
|
+
setState({ phase: "cancelled" });
|
|
431
|
+
return;
|
|
432
|
+
case "error":
|
|
433
|
+
setState({ phase: "error", reason: outcome.reason });
|
|
434
|
+
return;
|
|
216
435
|
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
)
|
|
436
|
+
});
|
|
437
|
+
}, [client, profileId]);
|
|
438
|
+
const select = react.useCallback((externalAccountId) => {
|
|
439
|
+
const pick = pickRef.current;
|
|
440
|
+
if (!pick) return;
|
|
441
|
+
pickRef.current = null;
|
|
442
|
+
setState({ phase: "connecting" });
|
|
443
|
+
pick.resolve(externalAccountId);
|
|
444
|
+
}, []);
|
|
445
|
+
const reset = react.useCallback(() => {
|
|
446
|
+
setRemintNonce((n) => n + 1);
|
|
447
|
+
}, []);
|
|
448
|
+
return { state, start, select, reset };
|
|
220
449
|
}
|
|
221
|
-
function useConnections(profileId) {
|
|
450
|
+
function useConnections(profileId, filter) {
|
|
222
451
|
const { client, queryClient } = usePostrun();
|
|
223
452
|
return reactQuery.useQuery(
|
|
224
453
|
{
|
|
225
|
-
queryKey: connectionKeys.list(profileId),
|
|
226
|
-
queryFn: async () => (await js.connectionsListByProfile({
|
|
454
|
+
queryKey: connectionKeys.list(profileId, filter),
|
|
455
|
+
queryFn: async () => (await js.connectionsListByProfile({
|
|
456
|
+
client,
|
|
457
|
+
path: { id: profileId },
|
|
458
|
+
query: filter
|
|
459
|
+
})).data,
|
|
227
460
|
enabled: Boolean(profileId)
|
|
228
461
|
},
|
|
229
462
|
queryClient
|
|
@@ -278,6 +511,17 @@ function useDisconnect() {
|
|
|
278
511
|
queryClient
|
|
279
512
|
);
|
|
280
513
|
}
|
|
514
|
+
|
|
515
|
+
// src/Connect.tsx
|
|
516
|
+
function Connect({
|
|
517
|
+
profileId,
|
|
518
|
+
platform,
|
|
519
|
+
onConnected,
|
|
520
|
+
children
|
|
521
|
+
}) {
|
|
522
|
+
const api = useConnect({ profileId, platform, onConnected });
|
|
523
|
+
return children(api);
|
|
524
|
+
}
|
|
281
525
|
var UploadError = class extends Error {
|
|
282
526
|
status;
|
|
283
527
|
constructor(status, message) {
|
|
@@ -318,17 +562,7 @@ async function uploadBytes(target, file, options = {}) {
|
|
|
318
562
|
}
|
|
319
563
|
|
|
320
564
|
// src/media.ts
|
|
321
|
-
|
|
322
|
-
function inferKind(contentType) {
|
|
323
|
-
if (contentType === "image/gif") return "gif";
|
|
324
|
-
if (contentType.startsWith("image/")) return "image";
|
|
325
|
-
if (contentType.startsWith("video/")) return "video";
|
|
326
|
-
if (DOCUMENT_MIME.test(contentType)) return "document";
|
|
327
|
-
throw new Error(
|
|
328
|
-
`Could not infer media kind from "${contentType}". Pass { kind } explicitly.`
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
async function pollUntilSettled(client, id, signal) {
|
|
565
|
+
async function pollUntilSettled(client, id, signal, onTick) {
|
|
332
566
|
let latest;
|
|
333
567
|
await pWaitFor__default.default(
|
|
334
568
|
async () => {
|
|
@@ -336,6 +570,7 @@ async function pollUntilSettled(client, id, signal) {
|
|
|
336
570
|
throw new DOMException("Upload aborted", "AbortError");
|
|
337
571
|
}
|
|
338
572
|
latest = (await js.mediaGet({ client, path: { id } })).data;
|
|
573
|
+
onTick?.(latest);
|
|
339
574
|
return latest.status === "ready" || latest.status === "failed";
|
|
340
575
|
},
|
|
341
576
|
{ interval: 1500, timeout: 3e5 }
|
|
@@ -345,92 +580,134 @@ async function pollUntilSettled(client, id, signal) {
|
|
|
345
580
|
}
|
|
346
581
|
return latest;
|
|
347
582
|
}
|
|
348
|
-
function
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
body: {
|
|
375
|
-
profile_id: options.profileId,
|
|
376
|
-
kind,
|
|
377
|
-
content_type: contentType,
|
|
378
|
-
targets: options.targets,
|
|
379
|
-
raw: options.raw,
|
|
380
|
-
alt_text: options.altText,
|
|
381
|
-
external_id: options.externalId,
|
|
382
|
-
metadata: options.metadata
|
|
583
|
+
async function runUpload(client, file, options, signal, callbacks) {
|
|
584
|
+
const created = (await js.mediaCreate({
|
|
585
|
+
client,
|
|
586
|
+
body: {
|
|
587
|
+
profile_id: options.profileId,
|
|
588
|
+
kind: options.kind,
|
|
589
|
+
content_type: options.contentType,
|
|
590
|
+
targets: options.targets,
|
|
591
|
+
raw: options.raw,
|
|
592
|
+
alt_text: options.altText,
|
|
593
|
+
external_id: options.externalId,
|
|
594
|
+
metadata: options.metadata
|
|
595
|
+
}
|
|
596
|
+
})).data;
|
|
597
|
+
if (created.upload) {
|
|
598
|
+
const target = created.upload;
|
|
599
|
+
await pRetry__default.default(
|
|
600
|
+
async () => {
|
|
601
|
+
try {
|
|
602
|
+
await uploadBytes(target, file, {
|
|
603
|
+
onProgress: callbacks.onProgress,
|
|
604
|
+
signal
|
|
605
|
+
});
|
|
606
|
+
} catch (uploadError) {
|
|
607
|
+
if (uploadError instanceof UploadError && uploadError.status >= 400 && uploadError.status < 500) {
|
|
608
|
+
throw new pRetry.AbortError(uploadError);
|
|
383
609
|
}
|
|
384
|
-
|
|
385
|
-
if (created.upload) {
|
|
386
|
-
const target = created.upload;
|
|
387
|
-
await pRetry__default.default(
|
|
388
|
-
async () => {
|
|
389
|
-
try {
|
|
390
|
-
await uploadBytes(target, file, {
|
|
391
|
-
onProgress: setProgress,
|
|
392
|
-
signal: controller.signal
|
|
393
|
-
});
|
|
394
|
-
} catch (uploadError) {
|
|
395
|
-
if (uploadError instanceof UploadError && uploadError.status >= 400 && uploadError.status < 500) {
|
|
396
|
-
throw new pRetry.AbortError(uploadError);
|
|
397
|
-
}
|
|
398
|
-
throw uploadError;
|
|
399
|
-
}
|
|
400
|
-
},
|
|
401
|
-
{ retries: 3, signal: controller.signal }
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
setStatus("processing");
|
|
405
|
-
const settled = await pollUntilSettled(
|
|
406
|
-
client,
|
|
407
|
-
created.id,
|
|
408
|
-
controller.signal
|
|
409
|
-
);
|
|
410
|
-
queryClient.setQueryData(mediaKeys.detail(created.id), settled);
|
|
411
|
-
setMedia(settled);
|
|
412
|
-
setStatus(settled.status === "failed" ? "failed" : "ready");
|
|
413
|
-
return settled;
|
|
414
|
-
} catch (caught) {
|
|
415
|
-
setError(caught);
|
|
416
|
-
setStatus("failed");
|
|
417
|
-
throw caught;
|
|
418
|
-
} finally {
|
|
419
|
-
if (abortRef.current === controller) {
|
|
420
|
-
abortRef.current = null;
|
|
610
|
+
throw uploadError;
|
|
421
611
|
}
|
|
612
|
+
},
|
|
613
|
+
{ retries: 3, signal }
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
callbacks.onProcessing();
|
|
617
|
+
return pollUntilSettled(client, created.id, signal, callbacks.onPoll);
|
|
618
|
+
}
|
|
619
|
+
function toFileArray(files) {
|
|
620
|
+
if (files instanceof File) return [files];
|
|
621
|
+
return Array.from(files);
|
|
622
|
+
}
|
|
623
|
+
function useMediaUpload(options) {
|
|
624
|
+
const { client, queryClient } = usePostrun();
|
|
625
|
+
const [items, setItems] = react.useState([]);
|
|
626
|
+
const controllers = react.useRef(/* @__PURE__ */ new Map());
|
|
627
|
+
const limitRef = react.useRef(null);
|
|
628
|
+
if (!limitRef.current) {
|
|
629
|
+
limitRef.current = pLimit__default.default(options?.concurrency ?? 3);
|
|
630
|
+
}
|
|
631
|
+
const patch = react.useCallback(
|
|
632
|
+
(id, changes) => {
|
|
633
|
+
setItems(
|
|
634
|
+
(current) => current.map(
|
|
635
|
+
(item) => item.id === id ? { ...item, ...changes } : item
|
|
636
|
+
)
|
|
637
|
+
);
|
|
638
|
+
},
|
|
639
|
+
[]
|
|
640
|
+
);
|
|
641
|
+
const add = react.useCallback(
|
|
642
|
+
(files, uploadOptions) => {
|
|
643
|
+
const queued = toFileArray(files).map((file) => ({
|
|
644
|
+
id: crypto.randomUUID(),
|
|
645
|
+
file,
|
|
646
|
+
status: "uploading",
|
|
647
|
+
progress: 0,
|
|
648
|
+
media: null,
|
|
649
|
+
error: null
|
|
650
|
+
}));
|
|
651
|
+
setItems((current) => [...current, ...queued]);
|
|
652
|
+
const limit = limitRef.current;
|
|
653
|
+
if (!limit) {
|
|
654
|
+
return Promise.resolve([]);
|
|
422
655
|
}
|
|
656
|
+
const settlements = queued.map((item) => {
|
|
657
|
+
const controller = new AbortController();
|
|
658
|
+
controllers.current.set(item.id, controller);
|
|
659
|
+
return limit(() => {
|
|
660
|
+
if (controller.signal.aborted) {
|
|
661
|
+
throw new DOMException("Upload aborted", "AbortError");
|
|
662
|
+
}
|
|
663
|
+
return runUpload(client, item.file, uploadOptions, controller.signal, {
|
|
664
|
+
onProgress: (progress) => patch(item.id, { progress }),
|
|
665
|
+
onProcessing: () => patch(item.id, { status: "processing" }),
|
|
666
|
+
// Live server progress (stage + percent) each poll tick.
|
|
667
|
+
onPoll: (media) => patch(item.id, { media })
|
|
668
|
+
});
|
|
669
|
+
}).then((settled) => {
|
|
670
|
+
patch(item.id, {
|
|
671
|
+
status: settled.status === "failed" ? "failed" : "ready",
|
|
672
|
+
media: settled,
|
|
673
|
+
progress: 1
|
|
674
|
+
});
|
|
675
|
+
queryClient.setQueryData(mediaKeys.detail(settled.id), settled);
|
|
676
|
+
void queryClient.invalidateQueries({ queryKey: mediaKeys.lists() });
|
|
677
|
+
return settled;
|
|
678
|
+
}).catch((error) => {
|
|
679
|
+
if (controller.signal.aborted) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
patch(item.id, { status: "failed", error });
|
|
683
|
+
return null;
|
|
684
|
+
}).finally(() => {
|
|
685
|
+
controllers.current.delete(item.id);
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
return Promise.all(settlements).then(
|
|
689
|
+
(results) => results.filter((result) => result !== null)
|
|
690
|
+
);
|
|
423
691
|
},
|
|
424
|
-
[client, queryClient]
|
|
692
|
+
[client, queryClient, patch]
|
|
425
693
|
);
|
|
426
|
-
const
|
|
694
|
+
const remove = react.useCallback((id) => {
|
|
695
|
+
controllers.current.get(id)?.abort();
|
|
696
|
+
controllers.current.delete(id);
|
|
697
|
+
setItems((current) => current.filter((item) => item.id !== id));
|
|
698
|
+
}, []);
|
|
427
699
|
const reset = react.useCallback(() => {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
setError(null);
|
|
700
|
+
controllers.current.forEach((controller) => controller.abort());
|
|
701
|
+
controllers.current.clear();
|
|
702
|
+
setItems([]);
|
|
432
703
|
}, []);
|
|
433
|
-
|
|
704
|
+
const ready = items.flatMap(
|
|
705
|
+
(item) => item.status === "ready" && item.media ? [item.media] : []
|
|
706
|
+
);
|
|
707
|
+
const isUploading = items.some(
|
|
708
|
+
(item) => item.status === "uploading" || item.status === "processing"
|
|
709
|
+
);
|
|
710
|
+
return { items, ready, isUploading, add, remove, reset };
|
|
434
711
|
}
|
|
435
712
|
function useMedia(id) {
|
|
436
713
|
const { client, queryClient } = usePostrun();
|
|
@@ -447,12 +724,33 @@ function useMedia(id) {
|
|
|
447
724
|
queryClient
|
|
448
725
|
);
|
|
449
726
|
}
|
|
727
|
+
function useMediaList(query) {
|
|
728
|
+
const { client, queryClient } = usePostrun();
|
|
729
|
+
return reactQuery.useQuery(
|
|
730
|
+
{
|
|
731
|
+
queryKey: mediaKeys.list(query),
|
|
732
|
+
queryFn: async () => (await js.mediaList({ client, query })).data
|
|
733
|
+
},
|
|
734
|
+
queryClient
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
function useMediaInfinite(filters, options) {
|
|
738
|
+
const { client } = usePostrun();
|
|
739
|
+
return useInfiniteList({
|
|
740
|
+
queryKey: mediaKeys.infinite(filters),
|
|
741
|
+
limit: options?.pageSize,
|
|
742
|
+
fetchPage: async ({ limit, offset }) => (await js.mediaList({ client, query: { ...filters, limit, offset } })).data
|
|
743
|
+
});
|
|
744
|
+
}
|
|
450
745
|
function useUpdateMedia() {
|
|
451
746
|
const { client, queryClient } = usePostrun();
|
|
452
747
|
return reactQuery.useMutation(
|
|
453
748
|
{
|
|
454
749
|
mutationFn: async ({ id, ...body }) => (await js.mediaUpdate({ client, path: { id }, body })).data,
|
|
455
|
-
onSuccess: (result, { id }) =>
|
|
750
|
+
onSuccess: (result, { id }) => {
|
|
751
|
+
queryClient.setQueryData(mediaKeys.detail(id), result);
|
|
752
|
+
void queryClient.invalidateQueries({ queryKey: mediaKeys.lists() });
|
|
753
|
+
}
|
|
456
754
|
},
|
|
457
755
|
queryClient
|
|
458
756
|
);
|
|
@@ -462,7 +760,10 @@ function useDeleteMedia() {
|
|
|
462
760
|
return reactQuery.useMutation(
|
|
463
761
|
{
|
|
464
762
|
mutationFn: async (id) => (await js.mediaDelete({ client, path: { id } })).data,
|
|
465
|
-
onSuccess: (_result, id) =>
|
|
763
|
+
onSuccess: (_result, id) => {
|
|
764
|
+
queryClient.removeQueries({ queryKey: mediaKeys.detail(id) });
|
|
765
|
+
void queryClient.invalidateQueries({ queryKey: mediaKeys.lists() });
|
|
766
|
+
}
|
|
466
767
|
},
|
|
467
768
|
queryClient
|
|
468
769
|
);
|
|
@@ -1288,6 +1589,7 @@ function LinkedInPostPreviewImpl({
|
|
|
1288
1589
|
}
|
|
1289
1590
|
var LinkedInPostPreview = react.memo(LinkedInPostPreviewImpl);
|
|
1290
1591
|
|
|
1592
|
+
exports.Connect = Connect;
|
|
1291
1593
|
exports.LinkedInPostPreview = LinkedInPostPreview;
|
|
1292
1594
|
exports.PostrunProvider = PostrunProvider;
|
|
1293
1595
|
exports.UploadError = UploadError;
|
|
@@ -1309,6 +1611,8 @@ exports.useDisconnect = useDisconnect;
|
|
|
1309
1611
|
exports.useDiscoverableAccounts = useDiscoverableAccounts;
|
|
1310
1612
|
exports.useInfiniteList = useInfiniteList;
|
|
1311
1613
|
exports.useMedia = useMedia;
|
|
1614
|
+
exports.useMediaInfinite = useMediaInfinite;
|
|
1615
|
+
exports.useMediaList = useMediaList;
|
|
1312
1616
|
exports.useMediaUpload = useMediaUpload;
|
|
1313
1617
|
exports.usePost = usePost;
|
|
1314
1618
|
exports.usePostrun = usePostrun;
|