birdclaw 0.5.1 → 0.7.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/CHANGELOG.md +92 -1
- package/README.md +75 -5
- package/package.json +8 -2
- package/scripts/browser-perf.mjs +1 -0
- package/scripts/start-test-server.mjs +16 -3
- package/src/cli.ts +812 -37
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +37 -7
- package/src/components/AvatarChip.tsx +9 -3
- package/src/components/DmWorkspace.tsx +18 -8
- package/src/components/LinkPreviewCard.tsx +40 -18
- package/src/components/MarkdownViewer.tsx +818 -0
- package/src/components/ProfileAnalysisStream.tsx +428 -0
- package/src/components/ProfilePreview.tsx +120 -9
- package/src/components/SavedTimelineView.tsx +30 -8
- package/src/components/SyncNowButton.tsx +60 -25
- package/src/components/ThemeSlider.tsx +55 -50
- package/src/components/TimelineCard.tsx +240 -92
- package/src/components/TimelineRouteFrame.tsx +38 -8
- package/src/components/TweetMediaGrid.tsx +87 -38
- package/src/components/TweetRichText.tsx +45 -17
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +97 -13
- package/src/lib/account-sync-job.ts +666 -0
- package/src/lib/actions-transport.ts +216 -146
- package/src/lib/api-client.ts +128 -53
- package/src/lib/archive-finder.ts +78 -63
- package/src/lib/archive-import.ts +1593 -1291
- package/src/lib/authored-live.ts +262 -204
- package/src/lib/avatar-cache.ts +208 -43
- package/src/lib/backup.ts +1536 -954
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +582 -194
- package/src/lib/blocklist.ts +40 -23
- package/src/lib/blocks-write.ts +129 -80
- package/src/lib/blocks.ts +165 -97
- package/src/lib/bookmark-sync-job.ts +250 -160
- package/src/lib/config.ts +35 -2
- package/src/lib/conversation-surface.ts +79 -48
- package/src/lib/data-sources.ts +219 -0
- package/src/lib/db.ts +95 -4
- package/src/lib/dms-live.ts +720 -66
- package/src/lib/effect-runtime.ts +45 -0
- package/src/lib/follow-graph.ts +224 -180
- package/src/lib/geocoding.ts +296 -0
- package/src/lib/http-effect.ts +222 -0
- package/src/lib/inbox.ts +74 -43
- package/src/lib/link-index.ts +88 -76
- package/src/lib/link-insights.ts +24 -0
- package/src/lib/link-preview-metadata.ts +472 -52
- package/src/lib/location.ts +137 -0
- package/src/lib/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +445 -288
- package/src/lib/mentions-live.ts +549 -354
- package/src/lib/moderation-target.ts +102 -65
- package/src/lib/moderation-write.ts +77 -18
- package/src/lib/mutes-write.ts +129 -80
- package/src/lib/mutes.ts +8 -1
- package/src/lib/network-map.ts +382 -0
- package/src/lib/openai.ts +84 -53
- package/src/lib/period-digest.ts +1317 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- package/src/lib/profile-analysis.ts +1272 -0
- package/src/lib/profile-bio-entities.ts +1 -1
- package/src/lib/profile-hydration.ts +124 -72
- package/src/lib/profile-replies.ts +60 -43
- package/src/lib/profile-resolver.ts +402 -294
- package/src/lib/queries.ts +983 -203
- package/src/lib/research.ts +165 -120
- package/src/lib/search-discussion.ts +1016 -0
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +325 -51
- package/src/lib/tweet-account-edges.ts +2 -0
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/tweet-render.ts +141 -1
- package/src/lib/tweet-search-live.ts +565 -0
- package/src/lib/types.ts +75 -3
- package/src/lib/ui.ts +31 -8
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +222 -149
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl-rate-limits.ts +267 -0
- package/src/lib/xurl.ts +1185 -405
- package/src/routeTree.gen.ts +273 -0
- package/src/routes/__root.tsx +24 -5
- package/src/routes/api/action.tsx +127 -78
- package/src/routes/api/avatar.tsx +39 -30
- package/src/routes/api/blocks.tsx +26 -23
- package/src/routes/api/conversation.tsx +25 -14
- package/src/routes/api/data-sources.tsx +24 -0
- package/src/routes/api/inbox.tsx +27 -21
- package/src/routes/api/link-insights.tsx +31 -25
- package/src/routes/api/link-preview.tsx +25 -21
- package/src/routes/api/network-map.tsx +55 -0
- package/src/routes/api/period-digest.tsx +133 -0
- package/src/routes/api/profile-analysis.tsx +152 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +80 -55
- package/src/routes/api/search-discussion.tsx +169 -0
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +75 -29
- package/src/routes/api/xurl-rate-limits.tsx +24 -0
- package/src/routes/data-sources.tsx +255 -0
- package/src/routes/discuss.tsx +419 -0
- package/src/routes/dms.tsx +95 -28
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/network-map.tsx +1035 -0
- package/src/routes/profile-analyze.tsx +112 -0
- package/src/routes/profiles.$handle.tsx +228 -0
- package/src/routes/rate-limits.tsx +309 -0
- package/src/routes/today.tsx +455 -0
- package/src/styles.css +22 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Cause, Effect, Exit, Option } from "effect";
|
|
2
|
+
|
|
3
|
+
export function tryPromise<A>(
|
|
4
|
+
try_: () => PromiseLike<A>,
|
|
5
|
+
): Effect.Effect<A, unknown> {
|
|
6
|
+
return Effect.tryPromise({
|
|
7
|
+
try: try_,
|
|
8
|
+
catch: (cause) => cause,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function runEffectPromise<A, E>(
|
|
13
|
+
effect: Effect.Effect<A, E>,
|
|
14
|
+
): Promise<A> {
|
|
15
|
+
return Effect.runPromiseExit(effect).then((exit) => {
|
|
16
|
+
if (Exit.isSuccess(exit)) return exit.value;
|
|
17
|
+
throw effectExitError(exit);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function effectExitError<E>(exit: Exit.Exit<unknown, E>): E | Error {
|
|
22
|
+
if (Exit.isSuccess(exit)) {
|
|
23
|
+
return new Error("Effect completed successfully");
|
|
24
|
+
}
|
|
25
|
+
const failure = Cause.failureOption(exit.cause);
|
|
26
|
+
if (Option.isSome(failure)) return failure.value;
|
|
27
|
+
const squashed = Cause.squash(exit.cause);
|
|
28
|
+
return squashed instanceof Error ? squashed : new Error(String(squashed));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function runEffectBackground<A, E>(
|
|
32
|
+
effect: Effect.Effect<A, E>,
|
|
33
|
+
handlers: {
|
|
34
|
+
onSuccess: (value: A) => void;
|
|
35
|
+
onFailure: (error: E | Error) => void;
|
|
36
|
+
},
|
|
37
|
+
) {
|
|
38
|
+
void Effect.runPromiseExit(effect).then((exit) => {
|
|
39
|
+
if (Exit.isSuccess(exit)) {
|
|
40
|
+
handlers.onSuccess(exit.value);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
handlers.onFailure(effectExitError(exit));
|
|
44
|
+
});
|
|
45
|
+
}
|
package/src/lib/follow-graph.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { Effect } from "effect";
|
|
2
3
|
import { getNativeDb } from "./db";
|
|
3
|
-
import {
|
|
4
|
+
import { listFollowUsersViaBirdEffect } from "./bird";
|
|
5
|
+
import { runEffectPromise, tryPromise } from "./effect-runtime";
|
|
4
6
|
import type { Database } from "./sqlite";
|
|
5
7
|
import { readSyncCache, writeSyncCache } from "./sync-cache";
|
|
6
8
|
import type {
|
|
@@ -38,6 +40,17 @@ export interface SyncFollowGraphOptions {
|
|
|
38
40
|
type FollowGraphSyncMode = "auto" | "bird" | "xurl";
|
|
39
41
|
type FollowGraphLiveSource = "bird" | "xurl";
|
|
40
42
|
|
|
43
|
+
function toError(error: unknown) {
|
|
44
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function trySync<T>(try_: () => T) {
|
|
48
|
+
return Effect.try({
|
|
49
|
+
try: try_,
|
|
50
|
+
catch: toError,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
interface ResolvedAccount {
|
|
42
55
|
accountId: string;
|
|
43
56
|
username: string;
|
|
@@ -238,7 +251,7 @@ function mergePages(
|
|
|
238
251
|
};
|
|
239
252
|
}
|
|
240
253
|
|
|
241
|
-
|
|
254
|
+
function fetchFollowGraphViaXurlEffect({
|
|
242
255
|
direction,
|
|
243
256
|
username,
|
|
244
257
|
userId,
|
|
@@ -252,37 +265,41 @@ async function fetchFollowGraphViaXurl({
|
|
|
252
265
|
limit: number;
|
|
253
266
|
maxPages?: number;
|
|
254
267
|
maxResources?: number;
|
|
255
|
-
}):
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
268
|
+
}): Effect.Effect<MergedFollowPayload, unknown> {
|
|
269
|
+
return Effect.gen(function* () {
|
|
270
|
+
const pages: XurlFollowUsersResponse[] = [];
|
|
271
|
+
let nextToken: string | undefined;
|
|
272
|
+
let pageCount = 0;
|
|
273
|
+
let collectedCount = 0;
|
|
260
274
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
275
|
+
do {
|
|
276
|
+
const payload = yield* tryPromise(() =>
|
|
277
|
+
listFollowUsersViaXurl({
|
|
278
|
+
direction,
|
|
279
|
+
username,
|
|
280
|
+
userId,
|
|
281
|
+
maxResults: limit,
|
|
282
|
+
paginationToken: nextToken,
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
pages.push(payload);
|
|
286
|
+
collectedCount += payload.data.length;
|
|
287
|
+
nextToken =
|
|
288
|
+
typeof payload.meta?.next_token === "string"
|
|
289
|
+
? String(payload.meta.next_token)
|
|
290
|
+
: undefined;
|
|
291
|
+
pageCount += 1;
|
|
292
|
+
} while (
|
|
293
|
+
nextToken &&
|
|
294
|
+
(maxPages === undefined || pageCount < maxPages) &&
|
|
295
|
+
(maxResources === undefined || collectedCount < maxResources)
|
|
296
|
+
);
|
|
281
297
|
|
|
282
|
-
|
|
298
|
+
return mergePages(pages, nextToken, maxResources);
|
|
299
|
+
});
|
|
283
300
|
}
|
|
284
301
|
|
|
285
|
-
|
|
302
|
+
function fetchFollowGraphViaBirdEffect({
|
|
286
303
|
direction,
|
|
287
304
|
userId,
|
|
288
305
|
limit,
|
|
@@ -294,32 +311,34 @@ async function fetchFollowGraphViaBird({
|
|
|
294
311
|
limit: number;
|
|
295
312
|
maxPages?: number;
|
|
296
313
|
maxResources?: number;
|
|
297
|
-
}):
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
314
|
+
}): Effect.Effect<MergedFollowPayload, unknown> {
|
|
315
|
+
return Effect.gen(function* () {
|
|
316
|
+
const birdLimit = Math.min(limit, BIRD_FOLLOW_PAGE_LIMIT);
|
|
317
|
+
const cappedMaxPages =
|
|
318
|
+
maxResources === undefined
|
|
319
|
+
? maxPages
|
|
320
|
+
: Math.min(
|
|
321
|
+
maxPages ?? Number.POSITIVE_INFINITY,
|
|
322
|
+
Math.ceil(maxResources / birdLimit),
|
|
323
|
+
);
|
|
324
|
+
const payload = yield* listFollowUsersViaBirdEffect({
|
|
325
|
+
direction,
|
|
326
|
+
userId,
|
|
327
|
+
maxResults: Math.min(birdLimit, maxResources ?? birdLimit),
|
|
328
|
+
all: true,
|
|
329
|
+
maxPages: Number.isFinite(cappedMaxPages) ? cappedMaxPages : undefined,
|
|
330
|
+
});
|
|
331
|
+
return mergePages(
|
|
332
|
+
[payload],
|
|
333
|
+
typeof payload.meta?.next_token === "string"
|
|
334
|
+
? String(payload.meta.next_token)
|
|
335
|
+
: undefined,
|
|
336
|
+
maxResources,
|
|
337
|
+
);
|
|
312
338
|
});
|
|
313
|
-
return mergePages(
|
|
314
|
-
[payload],
|
|
315
|
-
typeof payload.meta?.next_token === "string"
|
|
316
|
-
? String(payload.meta.next_token)
|
|
317
|
-
: undefined,
|
|
318
|
-
maxResources,
|
|
319
|
-
);
|
|
320
339
|
}
|
|
321
340
|
|
|
322
|
-
|
|
341
|
+
function fetchFollowGraphEffect({
|
|
323
342
|
mode,
|
|
324
343
|
direction,
|
|
325
344
|
username,
|
|
@@ -335,49 +354,27 @@ async function fetchFollowGraph({
|
|
|
335
354
|
limit: number;
|
|
336
355
|
maxPages?: number;
|
|
337
356
|
maxResources?: number;
|
|
338
|
-
}):
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
username,
|
|
357
|
-
userId,
|
|
358
|
-
limit,
|
|
359
|
-
maxPages,
|
|
360
|
-
maxResources,
|
|
361
|
-
}),
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
try {
|
|
366
|
-
return {
|
|
367
|
-
source: "bird",
|
|
368
|
-
payload: await fetchFollowGraphViaBird({
|
|
369
|
-
direction,
|
|
370
|
-
userId,
|
|
371
|
-
limit,
|
|
372
|
-
maxPages,
|
|
373
|
-
maxResources,
|
|
374
|
-
}),
|
|
375
|
-
};
|
|
376
|
-
} catch (birdError) {
|
|
377
|
-
try {
|
|
357
|
+
}): Effect.Effect<
|
|
358
|
+
{ source: FollowGraphLiveSource; payload: MergedFollowPayload },
|
|
359
|
+
unknown
|
|
360
|
+
> {
|
|
361
|
+
return Effect.gen(function* () {
|
|
362
|
+
if (mode === "bird") {
|
|
363
|
+
return {
|
|
364
|
+
source: "bird",
|
|
365
|
+
payload: yield* fetchFollowGraphViaBirdEffect({
|
|
366
|
+
direction,
|
|
367
|
+
userId,
|
|
368
|
+
limit,
|
|
369
|
+
maxPages,
|
|
370
|
+
maxResources,
|
|
371
|
+
}),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (mode === "xurl") {
|
|
378
375
|
return {
|
|
379
376
|
source: "xurl",
|
|
380
|
-
payload:
|
|
377
|
+
payload: yield* fetchFollowGraphViaXurlEffect({
|
|
381
378
|
direction,
|
|
382
379
|
username,
|
|
383
380
|
userId,
|
|
@@ -386,14 +383,51 @@ async function fetchFollowGraph({
|
|
|
386
383
|
maxResources,
|
|
387
384
|
}),
|
|
388
385
|
};
|
|
389
|
-
} catch (xurlError) {
|
|
390
|
-
throw new Error(
|
|
391
|
-
`follow graph sync failed via bird and xurl: bird: ${errorMessage(
|
|
392
|
-
birdError,
|
|
393
|
-
)}; xurl: ${errorMessage(xurlError)}`,
|
|
394
|
-
);
|
|
395
386
|
}
|
|
396
|
-
|
|
387
|
+
|
|
388
|
+
const birdResult = yield* fetchFollowGraphViaBirdEffect({
|
|
389
|
+
direction,
|
|
390
|
+
userId,
|
|
391
|
+
limit,
|
|
392
|
+
maxPages,
|
|
393
|
+
maxResources,
|
|
394
|
+
}).pipe(
|
|
395
|
+
Effect.map((payload) => ({ ok: true as const, payload })),
|
|
396
|
+
Effect.catchAll((error) => Effect.succeed({ ok: false as const, error })),
|
|
397
|
+
);
|
|
398
|
+
if (birdResult.ok) {
|
|
399
|
+
return {
|
|
400
|
+
source: "bird",
|
|
401
|
+
payload: birdResult.payload,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const xurlResult = yield* fetchFollowGraphViaXurlEffect({
|
|
406
|
+
direction,
|
|
407
|
+
username,
|
|
408
|
+
userId,
|
|
409
|
+
limit,
|
|
410
|
+
maxPages,
|
|
411
|
+
maxResources,
|
|
412
|
+
}).pipe(
|
|
413
|
+
Effect.map((payload) => ({ ok: true as const, payload })),
|
|
414
|
+
Effect.catchAll((error) => Effect.succeed({ ok: false as const, error })),
|
|
415
|
+
);
|
|
416
|
+
if (xurlResult.ok) {
|
|
417
|
+
return {
|
|
418
|
+
source: "xurl",
|
|
419
|
+
payload: xurlResult.payload,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return yield* Effect.fail(
|
|
424
|
+
new Error(
|
|
425
|
+
`follow graph sync failed via bird and xurl: bird: ${errorMessage(
|
|
426
|
+
birdResult.error,
|
|
427
|
+
)}; xurl: ${errorMessage(xurlResult.error)}`,
|
|
428
|
+
),
|
|
429
|
+
);
|
|
430
|
+
});
|
|
397
431
|
}
|
|
398
432
|
|
|
399
433
|
function getExistingEdges(
|
|
@@ -620,98 +654,108 @@ function errorMessage(error: unknown) {
|
|
|
620
654
|
return error instanceof Error ? error.message : String(error);
|
|
621
655
|
}
|
|
622
656
|
|
|
623
|
-
export
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const cacheKey = buildCacheKey({
|
|
638
|
-
direction: options.direction,
|
|
639
|
-
accountId: account.accountId,
|
|
640
|
-
mode,
|
|
641
|
-
limit,
|
|
642
|
-
maxPages,
|
|
643
|
-
maxResources,
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
if (!options.yes) {
|
|
647
|
-
return makeDryRunResponse({
|
|
648
|
-
db,
|
|
649
|
-
account,
|
|
657
|
+
export function syncFollowGraphEffect(options: SyncFollowGraphOptions) {
|
|
658
|
+
return Effect.gen(function* () {
|
|
659
|
+
const db = yield* trySync(() => getNativeDb());
|
|
660
|
+
const mode = yield* trySync(() => parseMode(options.mode));
|
|
661
|
+
const limit = yield* trySync(() => parseLimit(options.limit));
|
|
662
|
+
const maxPages = yield* trySync(() =>
|
|
663
|
+
parseOptionalPositiveInteger("--max-pages", options.maxPages),
|
|
664
|
+
);
|
|
665
|
+
const maxResources = yield* trySync(() =>
|
|
666
|
+
parseOptionalPositiveInteger("--max-resources", options.maxResources),
|
|
667
|
+
);
|
|
668
|
+
const cacheTtlMs = parseCacheTtlMs(options.cacheTtlMs);
|
|
669
|
+
const account = yield* trySync(() => resolveAccount(db, options.account));
|
|
670
|
+
const cacheKey = buildCacheKey({
|
|
650
671
|
direction: options.direction,
|
|
672
|
+
accountId: account.accountId,
|
|
651
673
|
mode,
|
|
652
674
|
limit,
|
|
653
675
|
maxPages,
|
|
654
676
|
maxResources,
|
|
655
|
-
cacheKey,
|
|
656
|
-
cacheTtlMs,
|
|
657
677
|
});
|
|
658
|
-
}
|
|
659
678
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
679
|
+
if (!options.yes) {
|
|
680
|
+
return yield* trySync(() =>
|
|
681
|
+
makeDryRunResponse({
|
|
682
|
+
db,
|
|
683
|
+
account,
|
|
684
|
+
direction: options.direction,
|
|
685
|
+
mode,
|
|
686
|
+
limit,
|
|
687
|
+
maxPages,
|
|
688
|
+
maxResources,
|
|
689
|
+
cacheKey,
|
|
690
|
+
cacheTtlMs,
|
|
691
|
+
}),
|
|
692
|
+
);
|
|
693
|
+
}
|
|
667
694
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
maxResources,
|
|
678
|
-
});
|
|
679
|
-
const payload = useCache ? cached!.value : liveResult!.payload;
|
|
695
|
+
const cached = yield* trySync(() =>
|
|
696
|
+
readSyncCache<MergedFollowPayload>(cacheKey, db),
|
|
697
|
+
);
|
|
698
|
+
const cacheAgeMs = cached
|
|
699
|
+
? Date.now() - new Date(cached.updatedAt).getTime()
|
|
700
|
+
: Number.POSITIVE_INFINITY;
|
|
701
|
+
const useCache = Boolean(
|
|
702
|
+
!options.refresh && cached && cacheAgeMs <= cacheTtlMs,
|
|
703
|
+
);
|
|
680
704
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
705
|
+
const liveResult = useCache
|
|
706
|
+
? undefined
|
|
707
|
+
: yield* fetchFollowGraphEffect({
|
|
708
|
+
mode,
|
|
709
|
+
direction: options.direction,
|
|
710
|
+
username: account.username,
|
|
711
|
+
userId: account.externalUserId,
|
|
712
|
+
limit,
|
|
713
|
+
maxPages,
|
|
714
|
+
maxResources,
|
|
715
|
+
});
|
|
716
|
+
const payload = useCache ? cached!.value : liveResult!.payload;
|
|
684
717
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
718
|
+
if (!useCache) {
|
|
719
|
+
yield* trySync(() => writeSyncCache(cacheKey, payload, db));
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const mergeResult = yield* trySync(() =>
|
|
723
|
+
mergeFollowPayloadIntoLocalStore({
|
|
724
|
+
db,
|
|
725
|
+
accountId: account.accountId,
|
|
726
|
+
direction: options.direction,
|
|
727
|
+
payload,
|
|
728
|
+
source: useCache ? "cache" : liveResult!.source,
|
|
729
|
+
}),
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
return {
|
|
733
|
+
ok: true,
|
|
734
|
+
dryRun: false,
|
|
735
|
+
source: useCache ? "cache" : liveResult!.source,
|
|
736
|
+
mode,
|
|
737
|
+
direction: options.direction,
|
|
738
|
+
accountId: account.accountId,
|
|
739
|
+
status: mergeResult.status,
|
|
740
|
+
count: mergeResult.count,
|
|
741
|
+
pageCount: mergeResult.pageCount,
|
|
742
|
+
snapshotId: mergeResult.snapshotId,
|
|
743
|
+
partial: mergeResult.status === "incomplete",
|
|
744
|
+
cache: {
|
|
745
|
+
key: cacheKey,
|
|
746
|
+
reused: useCache,
|
|
747
|
+
updatedAt: useCache ? cached?.updatedAt : new Date().toISOString(),
|
|
748
|
+
},
|
|
749
|
+
warning:
|
|
750
|
+
mergeResult.status === "incomplete" && !options.allowPartial
|
|
751
|
+
? "Snapshot is incomplete because a page/resource cap stopped pagination. It was recorded but not used for churn events."
|
|
752
|
+
: undefined,
|
|
753
|
+
};
|
|
691
754
|
});
|
|
755
|
+
}
|
|
692
756
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
dryRun: false,
|
|
696
|
-
source: useCache ? "cache" : liveResult!.source,
|
|
697
|
-
mode,
|
|
698
|
-
direction: options.direction,
|
|
699
|
-
accountId: account.accountId,
|
|
700
|
-
status: mergeResult.status,
|
|
701
|
-
count: mergeResult.count,
|
|
702
|
-
pageCount: mergeResult.pageCount,
|
|
703
|
-
snapshotId: mergeResult.snapshotId,
|
|
704
|
-
partial: mergeResult.status === "incomplete",
|
|
705
|
-
cache: {
|
|
706
|
-
key: cacheKey,
|
|
707
|
-
reused: useCache,
|
|
708
|
-
updatedAt: useCache ? cached?.updatedAt : new Date().toISOString(),
|
|
709
|
-
},
|
|
710
|
-
warning:
|
|
711
|
-
mergeResult.status === "incomplete" && !options.allowPartial
|
|
712
|
-
? "Snapshot is incomplete because a page/resource cap stopped pagination. It was recorded but not used for churn events."
|
|
713
|
-
: undefined,
|
|
714
|
-
};
|
|
757
|
+
export function syncFollowGraph(options: SyncFollowGraphOptions) {
|
|
758
|
+
return runEffectPromise(syncFollowGraphEffect(options));
|
|
715
759
|
}
|
|
716
760
|
|
|
717
761
|
function toGraphProfile(row: Record<string, unknown>): FollowGraphProfile {
|