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
package/src/lib/mentions-live.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { Database } from "./sqlite";
|
|
2
|
-
import {
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import {
|
|
4
|
+
getAuthenticatedBirdAccountEffect,
|
|
5
|
+
listMentionsViaBirdEffect,
|
|
6
|
+
} from "./bird";
|
|
3
7
|
import type { MentionsDataSource } from "./config";
|
|
4
8
|
import { getNativeDb } from "./db";
|
|
9
|
+
import { runEffectPromise, tryPromise } from "./effect-runtime";
|
|
5
10
|
import { buildMediaJsonFromIncludes, countTweetMedia } from "./media-includes";
|
|
6
11
|
import { serializeMentionItemsAsXurlCompatible } from "./mentions-export";
|
|
7
12
|
import { listTimelineItems } from "./queries";
|
|
@@ -22,6 +27,38 @@ export const DEFAULT_MENTIONS_CACHE_TTL_MS = 2 * 60_000;
|
|
|
22
27
|
const MIN_XURL_MENTIONS_LIMIT = 5;
|
|
23
28
|
const MAX_XURL_MENTIONS_LIMIT = 100;
|
|
24
29
|
type MentionSyncMode = Exclude<MentionsDataSource, "birdclaw">;
|
|
30
|
+
type MentionLiveSource = Exclude<MentionSyncMode, "auto">;
|
|
31
|
+
export interface MentionsProgress {
|
|
32
|
+
source: "bird" | "xurl" | "cache";
|
|
33
|
+
fetched: number;
|
|
34
|
+
total?: number;
|
|
35
|
+
page?: number;
|
|
36
|
+
maxPages?: number;
|
|
37
|
+
pageSize?: number;
|
|
38
|
+
done: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface SyncMentionsOptions {
|
|
41
|
+
account?: string;
|
|
42
|
+
mode?: string;
|
|
43
|
+
limit?: number;
|
|
44
|
+
maxPages?: number;
|
|
45
|
+
refresh?: boolean;
|
|
46
|
+
cacheTtlMs?: number;
|
|
47
|
+
sinceId?: string;
|
|
48
|
+
startTime?: string;
|
|
49
|
+
onProgress?: (progress: MentionsProgress) => void;
|
|
50
|
+
}
|
|
51
|
+
interface ExportMentionsViaCachedLiveSourceOptions {
|
|
52
|
+
mode: MentionSyncMode;
|
|
53
|
+
account?: string;
|
|
54
|
+
search?: string;
|
|
55
|
+
replyFilter?: ReplyFilter;
|
|
56
|
+
limit?: number;
|
|
57
|
+
all?: boolean;
|
|
58
|
+
maxPages?: number;
|
|
59
|
+
refresh?: boolean;
|
|
60
|
+
cacheTtlMs?: number;
|
|
61
|
+
}
|
|
25
62
|
type MentionScanBoundary =
|
|
26
63
|
| { kind: "auto" }
|
|
27
64
|
| { kind: "since"; sinceId: string }
|
|
@@ -29,7 +66,7 @@ type MentionScanBoundary =
|
|
|
29
66
|
| { kind: "unbounded" };
|
|
30
67
|
interface MentionScanShape {
|
|
31
68
|
endpoint: "mentions";
|
|
32
|
-
mode:
|
|
69
|
+
mode: MentionLiveSource;
|
|
33
70
|
accountId: string;
|
|
34
71
|
pageSize: number;
|
|
35
72
|
boundary: MentionScanBoundary;
|
|
@@ -44,6 +81,17 @@ interface MentionHighWaterValue {
|
|
|
44
81
|
sinceId: string;
|
|
45
82
|
}
|
|
46
83
|
|
|
84
|
+
function toError(error: unknown) {
|
|
85
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function trySync<T>(try_: () => T) {
|
|
89
|
+
return Effect.try({
|
|
90
|
+
try: try_,
|
|
91
|
+
catch: toError,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
47
95
|
function getMentionsFetchModeKey({
|
|
48
96
|
scope,
|
|
49
97
|
mode,
|
|
@@ -133,7 +181,7 @@ function getMentionHighWaterKey({
|
|
|
133
181
|
mode,
|
|
134
182
|
accountId,
|
|
135
183
|
}: {
|
|
136
|
-
mode:
|
|
184
|
+
mode: MentionLiveSource;
|
|
137
185
|
accountId: string;
|
|
138
186
|
}) {
|
|
139
187
|
return `mentions:sync:high-water:v1:mode=${mode}:account=${encodeCacheKeyPart(accountId)}`;
|
|
@@ -234,9 +282,9 @@ function parseMaxPages(value?: number) {
|
|
|
234
282
|
}
|
|
235
283
|
|
|
236
284
|
function parseSyncMode(value?: string): MentionSyncMode {
|
|
237
|
-
const mode = value ?? "
|
|
238
|
-
if (mode !== "bird" && mode !== "xurl") {
|
|
239
|
-
throw new Error("--mode must be bird or xurl");
|
|
285
|
+
const mode = value ?? "auto";
|
|
286
|
+
if (mode !== "auto" && mode !== "bird" && mode !== "xurl") {
|
|
287
|
+
throw new Error("--mode must be auto, bird, or xurl");
|
|
240
288
|
}
|
|
241
289
|
return mode;
|
|
242
290
|
}
|
|
@@ -358,7 +406,7 @@ function getNewestMentionId(payload: XurlMentionsResponse) {
|
|
|
358
406
|
|
|
359
407
|
function readMentionHighWaterId(
|
|
360
408
|
db: Database,
|
|
361
|
-
mode:
|
|
409
|
+
mode: MentionLiveSource,
|
|
362
410
|
accountId: string,
|
|
363
411
|
) {
|
|
364
412
|
const cached = readSyncCache<MentionHighWaterValue>(
|
|
@@ -372,7 +420,7 @@ function readMentionHighWaterId(
|
|
|
372
420
|
|
|
373
421
|
function writeMentionHighWaterId(
|
|
374
422
|
db: Database,
|
|
375
|
-
mode:
|
|
423
|
+
mode: MentionLiveSource,
|
|
376
424
|
accountId: string,
|
|
377
425
|
sinceId: string | undefined,
|
|
378
426
|
) {
|
|
@@ -385,18 +433,24 @@ function writeMentionHighWaterId(
|
|
|
385
433
|
function resolveAccount(db: Database, accountId?: string) {
|
|
386
434
|
const row = accountId
|
|
387
435
|
? (db
|
|
388
|
-
.prepare(
|
|
389
|
-
|
|
436
|
+
.prepare(
|
|
437
|
+
"select id, handle, external_user_id from accounts where id = ?",
|
|
438
|
+
)
|
|
439
|
+
.get(accountId) as
|
|
440
|
+
| { id: string; handle: string; external_user_id: string | null }
|
|
441
|
+
| undefined)
|
|
390
442
|
: (db
|
|
391
443
|
.prepare(
|
|
392
444
|
`
|
|
393
|
-
select id, handle
|
|
445
|
+
select id, handle, external_user_id
|
|
394
446
|
from accounts
|
|
395
447
|
order by is_default desc, created_at asc
|
|
396
448
|
limit 1
|
|
397
449
|
`,
|
|
398
450
|
)
|
|
399
|
-
.get() as
|
|
451
|
+
.get() as
|
|
452
|
+
| { id: string; handle: string; external_user_id: string | null }
|
|
453
|
+
| undefined);
|
|
400
454
|
|
|
401
455
|
if (!row) {
|
|
402
456
|
throw new Error(`Unknown account: ${accountId ?? "default"}`);
|
|
@@ -405,9 +459,45 @@ function resolveAccount(db: Database, accountId?: string) {
|
|
|
405
459
|
return {
|
|
406
460
|
accountId: row.id,
|
|
407
461
|
username: row.handle.replace(/^@/, ""),
|
|
462
|
+
externalUserId:
|
|
463
|
+
typeof row.external_user_id === "string" &&
|
|
464
|
+
row.external_user_id.trim().length > 0
|
|
465
|
+
? row.external_user_id.trim()
|
|
466
|
+
: undefined,
|
|
408
467
|
};
|
|
409
468
|
}
|
|
410
469
|
|
|
470
|
+
function verifyBirdAccountMatchesEffect({
|
|
471
|
+
accountId,
|
|
472
|
+
username,
|
|
473
|
+
externalUserId,
|
|
474
|
+
}: ReturnType<typeof resolveAccount>) {
|
|
475
|
+
return Effect.gen(function* () {
|
|
476
|
+
const authenticated = yield* getAuthenticatedBirdAccountEffect();
|
|
477
|
+
if (
|
|
478
|
+
externalUserId &&
|
|
479
|
+
authenticated.id &&
|
|
480
|
+
authenticated.id === externalUserId
|
|
481
|
+
) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (externalUserId && authenticated.id) {
|
|
485
|
+
return yield* Effect.fail(
|
|
486
|
+
new Error(
|
|
487
|
+
`bird is authenticated as user ${authenticated.id}; refusing to sync into ${accountId} (${externalUserId})`,
|
|
488
|
+
),
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
if (authenticated.username.toLowerCase() !== username.toLowerCase()) {
|
|
492
|
+
return yield* Effect.fail(
|
|
493
|
+
new Error(
|
|
494
|
+
`bird is authenticated as @${authenticated.username}; refusing to sync into ${accountId} (@${username})`,
|
|
495
|
+
),
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
411
501
|
function findNewestArchiveMentionId(db: Database, accountId: string) {
|
|
412
502
|
const row = db
|
|
413
503
|
.prepare(
|
|
@@ -655,7 +745,7 @@ function mergeMentionPayloads(
|
|
|
655
745
|
};
|
|
656
746
|
}
|
|
657
747
|
|
|
658
|
-
|
|
748
|
+
function fetchMentionsViaXurlEffect({
|
|
659
749
|
resolvedAccount,
|
|
660
750
|
limit,
|
|
661
751
|
all,
|
|
@@ -663,6 +753,7 @@ async function fetchMentionsViaXurl({
|
|
|
663
753
|
sinceId,
|
|
664
754
|
startPaginationToken,
|
|
665
755
|
startTime,
|
|
756
|
+
onProgress,
|
|
666
757
|
}: {
|
|
667
758
|
resolvedAccount: ReturnType<typeof resolveAccount>;
|
|
668
759
|
limit: number;
|
|
@@ -671,44 +762,71 @@ async function fetchMentionsViaXurl({
|
|
|
671
762
|
sinceId?: string;
|
|
672
763
|
startPaginationToken?: string;
|
|
673
764
|
startTime?: string;
|
|
765
|
+
onProgress?: (progress: MentionsProgress) => void;
|
|
674
766
|
}) {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
767
|
+
return Effect.gen(function* () {
|
|
768
|
+
const accountUserId =
|
|
769
|
+
resolvedAccount.externalUserId ??
|
|
770
|
+
(yield* tryPromise(() =>
|
|
771
|
+
lookupUsersByHandles([resolvedAccount.username]),
|
|
772
|
+
).pipe(Effect.map((users) => users[0]?.id)));
|
|
773
|
+
if (!accountUserId) {
|
|
774
|
+
return yield* Effect.fail(
|
|
775
|
+
new Error(
|
|
776
|
+
`Could not resolve Twitter user id for @${resolvedAccount.username}`,
|
|
777
|
+
),
|
|
778
|
+
);
|
|
779
|
+
}
|
|
681
780
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
781
|
+
const pages: XurlMentionsResponse[] = [];
|
|
782
|
+
let nextToken: string | undefined = startPaginationToken;
|
|
783
|
+
let pageCount = 0;
|
|
784
|
+
do {
|
|
785
|
+
const payload = yield* tryPromise(() =>
|
|
786
|
+
listMentionsViaXurl({
|
|
787
|
+
maxResults: limit,
|
|
788
|
+
username: resolvedAccount.username,
|
|
789
|
+
userId: String(accountUserId),
|
|
790
|
+
paginationToken: nextToken,
|
|
791
|
+
...(sinceId ? { sinceId } : {}),
|
|
792
|
+
...(startTime ? { startTime } : {}),
|
|
793
|
+
}),
|
|
794
|
+
);
|
|
795
|
+
pages.push(payload);
|
|
796
|
+
const metaNextToken =
|
|
797
|
+
typeof payload.meta?.next_token === "string"
|
|
798
|
+
? payload.meta.next_token
|
|
799
|
+
: undefined;
|
|
800
|
+
nextToken = metaNextToken;
|
|
801
|
+
pageCount += 1;
|
|
802
|
+
const fetched = pages.reduce((sum, item) => sum + item.data.length, 0);
|
|
803
|
+
const done =
|
|
804
|
+
!all ||
|
|
805
|
+
!nextToken ||
|
|
806
|
+
(parsedMaxPages !== null && pageCount >= parsedMaxPages);
|
|
807
|
+
yield* Effect.sync(() =>
|
|
808
|
+
onProgress?.({
|
|
809
|
+
source: "xurl",
|
|
810
|
+
fetched,
|
|
811
|
+
total: parsedMaxPages === null ? undefined : parsedMaxPages * limit,
|
|
812
|
+
page: pageCount,
|
|
813
|
+
maxPages: parsedMaxPages ?? undefined,
|
|
814
|
+
pageSize: limit,
|
|
815
|
+
done,
|
|
816
|
+
}),
|
|
817
|
+
);
|
|
818
|
+
} while (
|
|
819
|
+
all &&
|
|
820
|
+
nextToken &&
|
|
821
|
+
(parsedMaxPages === null || pageCount < parsedMaxPages)
|
|
822
|
+
);
|
|
706
823
|
|
|
707
|
-
|
|
824
|
+
return mergeMentionPayloads(pages);
|
|
825
|
+
});
|
|
708
826
|
}
|
|
709
827
|
|
|
710
|
-
|
|
711
|
-
return
|
|
828
|
+
function fetchMentionsViaBirdEffect({ limit }: { limit: number }) {
|
|
829
|
+
return listMentionsViaBirdEffect({ maxResults: limit });
|
|
712
830
|
}
|
|
713
831
|
|
|
714
832
|
function isMaxPagesPartial({
|
|
@@ -725,7 +843,7 @@ function isMaxPagesPartial({
|
|
|
725
843
|
);
|
|
726
844
|
}
|
|
727
845
|
|
|
728
|
-
export
|
|
846
|
+
export function syncMentionsEffect({
|
|
729
847
|
account,
|
|
730
848
|
mode,
|
|
731
849
|
limit = 20,
|
|
@@ -734,222 +852,290 @@ export async function syncMentions({
|
|
|
734
852
|
cacheTtlMs,
|
|
735
853
|
sinceId,
|
|
736
854
|
startTime,
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
accountId: resolvedAccount.accountId,
|
|
766
|
-
pageSize: limit,
|
|
767
|
-
boundary: getMentionCursorBoundary({
|
|
768
|
-
explicitSinceId,
|
|
769
|
-
explicitStartTime,
|
|
770
|
-
}),
|
|
771
|
-
};
|
|
772
|
-
const cursorKey = getMentionCursorKey(cursorShape);
|
|
773
|
-
const legacyCursorKeys = getLegacyMentionCursorKeys(cursorShape);
|
|
774
|
-
const cursor =
|
|
775
|
-
parsedMode === "xurl"
|
|
776
|
-
? readMentionCursor({
|
|
777
|
-
db,
|
|
778
|
-
shape: cursorShape,
|
|
779
|
-
cursorKey,
|
|
780
|
-
legacyCursorKeys,
|
|
781
|
-
})
|
|
782
|
-
: undefined;
|
|
783
|
-
const startPaginationToken = cursor?.token;
|
|
784
|
-
const cursorSinceId =
|
|
785
|
-
cursor?.boundary?.kind === "since" ? cursor.boundary.sinceId : undefined;
|
|
786
|
-
const cursorStartTime =
|
|
787
|
-
cursor?.boundary?.kind === "start" ? cursor.boundary.startTime : undefined;
|
|
788
|
-
const committedSinceId =
|
|
789
|
-
parsedMode === "xurl" &&
|
|
790
|
-
cursorShape.boundary.kind === "auto" &&
|
|
791
|
-
!startPaginationToken
|
|
792
|
-
? readMentionHighWaterId(db, parsedMode, resolvedAccount.accountId)
|
|
793
|
-
: undefined;
|
|
794
|
-
const seededSinceId =
|
|
795
|
-
parsedMode === "xurl" &&
|
|
796
|
-
!explicitSinceId &&
|
|
797
|
-
!explicitStartTime &&
|
|
798
|
-
!startPaginationToken
|
|
799
|
-
? (committedSinceId ??
|
|
800
|
-
findNewestArchiveMentionId(db, resolvedAccount.accountId))
|
|
801
|
-
: undefined;
|
|
802
|
-
const resolvedSinceId = startPaginationToken
|
|
803
|
-
? cursorSinceId
|
|
804
|
-
: (explicitSinceId ?? seededSinceId);
|
|
805
|
-
const resolvedStartTime = startPaginationToken
|
|
806
|
-
? cursorStartTime
|
|
807
|
-
: !resolvedSinceId
|
|
808
|
-
? explicitStartTime
|
|
809
|
-
: undefined;
|
|
810
|
-
const resultShape: MentionScanShape = {
|
|
811
|
-
endpoint: "mentions",
|
|
812
|
-
mode: parsedMode,
|
|
813
|
-
accountId: resolvedAccount.accountId,
|
|
814
|
-
pageSize: limit,
|
|
815
|
-
boundary: getMentionRequestBoundary({
|
|
816
|
-
sinceId: resolvedSinceId,
|
|
817
|
-
startTime: resolvedStartTime,
|
|
818
|
-
}),
|
|
819
|
-
};
|
|
820
|
-
const resultCacheKey = getMentionResultCacheKey({
|
|
821
|
-
shape: resultShape,
|
|
822
|
-
all: fetchAll,
|
|
823
|
-
maxPages: parsedMaxPages,
|
|
824
|
-
});
|
|
825
|
-
const ttlMs = parseCacheTtlMs(cacheTtlMs);
|
|
826
|
-
const cached = startPaginationToken
|
|
827
|
-
? null
|
|
828
|
-
: readSyncCache<XurlMentionsResponse>(resultCacheKey, db);
|
|
829
|
-
const cachedPaginationToken = getCachedPaginationToken(cached);
|
|
830
|
-
const cacheAgeMs = cached
|
|
831
|
-
? Date.now() - new Date(cached.updatedAt).getTime()
|
|
832
|
-
: Number.POSITIVE_INFINITY;
|
|
833
|
-
|
|
834
|
-
if (
|
|
835
|
-
!startPaginationToken &&
|
|
836
|
-
!cachedPaginationToken &&
|
|
837
|
-
!refresh &&
|
|
838
|
-
cached &&
|
|
839
|
-
cacheAgeMs <= ttlMs
|
|
840
|
-
) {
|
|
841
|
-
mergeMentionsIntoLocalStore(
|
|
842
|
-
db,
|
|
843
|
-
resolvedAccount.accountId,
|
|
844
|
-
cached.value,
|
|
845
|
-
parsedMode,
|
|
846
|
-
);
|
|
847
|
-
return {
|
|
848
|
-
ok: true,
|
|
849
|
-
source: "cache",
|
|
850
|
-
kind: "mentions",
|
|
855
|
+
onProgress,
|
|
856
|
+
}: SyncMentionsOptions) {
|
|
857
|
+
return Effect.gen(function* () {
|
|
858
|
+
const parsedMode = yield* trySync(() => parseSyncMode(mode));
|
|
859
|
+
const primaryMode: MentionLiveSource =
|
|
860
|
+
parsedMode === "auto" ? "xurl" : parsedMode;
|
|
861
|
+
const explicitSinceId = sinceId?.trim() || undefined;
|
|
862
|
+
const explicitStartTime = startTime?.trim() || undefined;
|
|
863
|
+
if (primaryMode === "bird" && (explicitSinceId || explicitStartTime)) {
|
|
864
|
+
return yield* Effect.fail(
|
|
865
|
+
new Error("bird mode does not support --since-id or --start-time"),
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
if (primaryMode === "xurl") {
|
|
869
|
+
yield* trySync(() => assertXurlLimit(limit));
|
|
870
|
+
} else {
|
|
871
|
+
yield* trySync(() => assertBirdLimit(limit));
|
|
872
|
+
}
|
|
873
|
+
const parsedMaxPages = yield* trySync(() => parseMaxPages(maxPages));
|
|
874
|
+
const fetchAll =
|
|
875
|
+
primaryMode === "xurl" &&
|
|
876
|
+
(parsedMaxPages !== null ||
|
|
877
|
+
Boolean(explicitSinceId || explicitStartTime));
|
|
878
|
+
const db = yield* trySync(() => getNativeDb());
|
|
879
|
+
const resolvedAccount = yield* trySync(() => resolveAccount(db, account));
|
|
880
|
+
const cursorShape: MentionScanShape = {
|
|
881
|
+
endpoint: "mentions",
|
|
882
|
+
mode: primaryMode,
|
|
851
883
|
accountId: resolvedAccount.accountId,
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
884
|
+
pageSize: limit,
|
|
885
|
+
boundary: getMentionCursorBoundary({
|
|
886
|
+
explicitSinceId,
|
|
887
|
+
explicitStartTime,
|
|
856
888
|
}),
|
|
857
|
-
payload: cached.value,
|
|
858
889
|
};
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
890
|
+
const cursorKey = getMentionCursorKey(cursorShape);
|
|
891
|
+
const legacyCursorKeys = getLegacyMentionCursorKeys(cursorShape);
|
|
892
|
+
const cursor =
|
|
893
|
+
primaryMode === "xurl"
|
|
894
|
+
? yield* trySync(() =>
|
|
895
|
+
readMentionCursor({
|
|
896
|
+
db,
|
|
897
|
+
shape: cursorShape,
|
|
898
|
+
cursorKey,
|
|
899
|
+
legacyCursorKeys,
|
|
900
|
+
}),
|
|
901
|
+
)
|
|
902
|
+
: undefined;
|
|
903
|
+
const startPaginationToken = cursor?.token;
|
|
904
|
+
const cursorSinceId =
|
|
905
|
+
cursor?.boundary?.kind === "since" ? cursor.boundary.sinceId : undefined;
|
|
906
|
+
const cursorStartTime =
|
|
907
|
+
cursor?.boundary?.kind === "start"
|
|
908
|
+
? cursor.boundary.startTime
|
|
909
|
+
: undefined;
|
|
910
|
+
const committedSinceId =
|
|
911
|
+
primaryMode === "xurl" &&
|
|
912
|
+
cursorShape.boundary.kind === "auto" &&
|
|
913
|
+
!startPaginationToken
|
|
914
|
+
? yield* trySync(() =>
|
|
915
|
+
readMentionHighWaterId(db, primaryMode, resolvedAccount.accountId),
|
|
916
|
+
)
|
|
917
|
+
: undefined;
|
|
918
|
+
const seededSinceId =
|
|
919
|
+
primaryMode === "xurl" &&
|
|
920
|
+
!explicitSinceId &&
|
|
921
|
+
!explicitStartTime &&
|
|
922
|
+
!startPaginationToken
|
|
923
|
+
? (committedSinceId ??
|
|
924
|
+
(yield* trySync(() =>
|
|
925
|
+
findNewestArchiveMentionId(db, resolvedAccount.accountId),
|
|
926
|
+
)))
|
|
927
|
+
: undefined;
|
|
928
|
+
const resolvedSinceId = startPaginationToken
|
|
929
|
+
? cursorSinceId
|
|
930
|
+
: (explicitSinceId ?? seededSinceId);
|
|
931
|
+
const resolvedStartTime = startPaginationToken
|
|
932
|
+
? cursorStartTime
|
|
933
|
+
: !resolvedSinceId
|
|
934
|
+
? explicitStartTime
|
|
935
|
+
: undefined;
|
|
936
|
+
const resultShape: MentionScanShape = {
|
|
937
|
+
endpoint: "mentions",
|
|
938
|
+
mode: primaryMode,
|
|
939
|
+
accountId: resolvedAccount.accountId,
|
|
940
|
+
pageSize: limit,
|
|
941
|
+
boundary: getMentionRequestBoundary({
|
|
942
|
+
sinceId: resolvedSinceId,
|
|
943
|
+
startTime: resolvedStartTime,
|
|
944
|
+
}),
|
|
945
|
+
};
|
|
946
|
+
const resultCacheKey = getMentionResultCacheKey({
|
|
947
|
+
shape: resultShape,
|
|
948
|
+
all: fetchAll,
|
|
949
|
+
maxPages: parsedMaxPages,
|
|
950
|
+
});
|
|
951
|
+
const ttlMs = parseCacheTtlMs(cacheTtlMs);
|
|
952
|
+
const cached = startPaginationToken
|
|
953
|
+
? null
|
|
954
|
+
: yield* trySync(() =>
|
|
955
|
+
readSyncCache<XurlMentionsResponse>(resultCacheKey, db),
|
|
956
|
+
);
|
|
957
|
+
const cachedPaginationToken = getCachedPaginationToken(cached);
|
|
958
|
+
const cacheAgeMs = cached
|
|
959
|
+
? Date.now() - new Date(cached.updatedAt).getTime()
|
|
960
|
+
: Number.POSITIVE_INFINITY;
|
|
872
961
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
db,
|
|
887
|
-
resolvedAccount.accountId,
|
|
888
|
-
payload,
|
|
889
|
-
parsedMode,
|
|
890
|
-
);
|
|
891
|
-
const payloadPaginationToken = getCachedPaginationToken({ value: payload });
|
|
892
|
-
if (parsedMode === "xurl") {
|
|
893
|
-
if (payloadPaginationToken) {
|
|
894
|
-
writeSyncCache(
|
|
895
|
-
cursorKey,
|
|
896
|
-
addMentionCursorState(
|
|
897
|
-
payload,
|
|
898
|
-
getMentionRequestBoundary({
|
|
899
|
-
sinceId: resolvedSinceId,
|
|
900
|
-
startTime: resolvedStartTime,
|
|
901
|
-
}),
|
|
902
|
-
maxNumericTweetId(resolvedSinceId, getNewestMentionId(payload)),
|
|
962
|
+
if (
|
|
963
|
+
!startPaginationToken &&
|
|
964
|
+
!cachedPaginationToken &&
|
|
965
|
+
!refresh &&
|
|
966
|
+
cached &&
|
|
967
|
+
cacheAgeMs <= ttlMs
|
|
968
|
+
) {
|
|
969
|
+
yield* trySync(() =>
|
|
970
|
+
mergeMentionsIntoLocalStore(
|
|
971
|
+
db,
|
|
972
|
+
resolvedAccount.accountId,
|
|
973
|
+
cached.value,
|
|
974
|
+
primaryMode,
|
|
903
975
|
),
|
|
904
|
-
db,
|
|
905
976
|
);
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
977
|
+
yield* Effect.sync(() =>
|
|
978
|
+
onProgress?.({
|
|
979
|
+
source: "cache",
|
|
980
|
+
fetched: cached.value.data.length,
|
|
981
|
+
total: parsedMaxPages === null ? undefined : parsedMaxPages * limit,
|
|
982
|
+
done: true,
|
|
983
|
+
}),
|
|
984
|
+
);
|
|
985
|
+
return {
|
|
986
|
+
ok: true,
|
|
987
|
+
source: "cache",
|
|
988
|
+
kind: "mentions",
|
|
989
|
+
accountId: resolvedAccount.accountId,
|
|
990
|
+
count: cached.value.data.length,
|
|
991
|
+
partial: isMaxPagesPartial({
|
|
992
|
+
payload: cached.value,
|
|
911
993
|
maxPages: parsedMaxPages,
|
|
912
994
|
}),
|
|
913
|
-
|
|
995
|
+
payload: cached.value,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (
|
|
1000
|
+
primaryMode === "xurl" &&
|
|
1001
|
+
!explicitSinceId &&
|
|
1002
|
+
!explicitStartTime &&
|
|
1003
|
+
!startPaginationToken &&
|
|
1004
|
+
!seededSinceId
|
|
1005
|
+
) {
|
|
1006
|
+
console.error(
|
|
1007
|
+
"No local mention baseline found; syncing mentions from the newest page backwards.",
|
|
914
1008
|
);
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
let source: MentionLiveSource = primaryMode;
|
|
1012
|
+
const canFallbackToBird =
|
|
1013
|
+
primaryMode === "xurl" &&
|
|
1014
|
+
parsedMode === "auto" &&
|
|
1015
|
+
!explicitSinceId &&
|
|
1016
|
+
!explicitStartTime &&
|
|
1017
|
+
!startPaginationToken;
|
|
1018
|
+
const payload =
|
|
1019
|
+
primaryMode === "bird"
|
|
1020
|
+
? yield* fetchMentionsViaBirdEffect({ limit })
|
|
1021
|
+
: yield* fetchMentionsViaXurlEffect({
|
|
1022
|
+
resolvedAccount,
|
|
1023
|
+
limit,
|
|
1024
|
+
all: fetchAll,
|
|
1025
|
+
parsedMaxPages,
|
|
1026
|
+
sinceId: resolvedSinceId,
|
|
1027
|
+
startPaginationToken,
|
|
1028
|
+
startTime: resolvedStartTime,
|
|
1029
|
+
onProgress,
|
|
1030
|
+
}).pipe(
|
|
1031
|
+
Effect.catchAll((error) => {
|
|
1032
|
+
if (!canFallbackToBird) return Effect.fail(error);
|
|
1033
|
+
source = "bird";
|
|
1034
|
+
return verifyBirdAccountMatchesEffect(resolvedAccount).pipe(
|
|
1035
|
+
Effect.flatMap(() => fetchMentionsViaBirdEffect({ limit })),
|
|
1036
|
+
);
|
|
1037
|
+
}),
|
|
1038
|
+
);
|
|
1039
|
+
if (source === "bird") {
|
|
1040
|
+
yield* Effect.sync(() =>
|
|
1041
|
+
onProgress?.({
|
|
1042
|
+
source: "bird",
|
|
1043
|
+
fetched: payload.data.length,
|
|
1044
|
+
total: limit,
|
|
1045
|
+
done: true,
|
|
1046
|
+
}),
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
yield* trySync(() =>
|
|
1050
|
+
mergeMentionsIntoLocalStore(
|
|
1051
|
+
db,
|
|
1052
|
+
resolvedAccount.accountId,
|
|
1053
|
+
payload,
|
|
1054
|
+
source,
|
|
1055
|
+
),
|
|
1056
|
+
);
|
|
1057
|
+
const payloadPaginationToken = getCachedPaginationToken({ value: payload });
|
|
1058
|
+
if (source === "xurl") {
|
|
1059
|
+
if (payloadPaginationToken) {
|
|
1060
|
+
yield* trySync(() => {
|
|
1061
|
+
writeSyncCache(
|
|
1062
|
+
cursorKey,
|
|
1063
|
+
addMentionCursorState(
|
|
1064
|
+
payload,
|
|
1065
|
+
getMentionRequestBoundary({
|
|
1066
|
+
sinceId: resolvedSinceId,
|
|
1067
|
+
startTime: resolvedStartTime,
|
|
1068
|
+
}),
|
|
1069
|
+
maxNumericTweetId(resolvedSinceId, getNewestMentionId(payload)),
|
|
1070
|
+
),
|
|
1071
|
+
db,
|
|
1072
|
+
);
|
|
1073
|
+
deleteSyncCache(resultCacheKey, db);
|
|
1074
|
+
deleteSyncCache(
|
|
1075
|
+
getMentionResultCacheKey({
|
|
1076
|
+
shape: cursorShape,
|
|
1077
|
+
all: fetchAll,
|
|
1078
|
+
maxPages: parsedMaxPages,
|
|
1079
|
+
}),
|
|
1080
|
+
db,
|
|
1081
|
+
);
|
|
1082
|
+
for (const legacyKey of cursor?.legacyKeys ?? []) {
|
|
1083
|
+
deleteSyncCache(legacyKey, db);
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
} else {
|
|
1087
|
+
yield* trySync(() => {
|
|
1088
|
+
deleteSyncCache(cursorKey, db);
|
|
1089
|
+
for (const legacyKey of legacyCursorKeys) {
|
|
1090
|
+
deleteSyncCache(legacyKey, db);
|
|
1091
|
+
}
|
|
1092
|
+
if (cursorShape.boundary.kind === "auto") {
|
|
1093
|
+
writeMentionHighWaterId(
|
|
1094
|
+
db,
|
|
1095
|
+
source,
|
|
1096
|
+
resolvedAccount.accountId,
|
|
1097
|
+
maxNumericTweetId(
|
|
1098
|
+
resolvedSinceId,
|
|
1099
|
+
cursor?.pendingNewestId,
|
|
1100
|
+
getNewestMentionId(payload),
|
|
1101
|
+
),
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
934
1105
|
}
|
|
935
1106
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1107
|
+
if (!payloadPaginationToken && !startPaginationToken) {
|
|
1108
|
+
const writeCacheKey =
|
|
1109
|
+
source === primaryMode
|
|
1110
|
+
? resultCacheKey
|
|
1111
|
+
: getMentionResultCacheKey({
|
|
1112
|
+
shape: {
|
|
1113
|
+
...resultShape,
|
|
1114
|
+
mode: source,
|
|
1115
|
+
},
|
|
1116
|
+
all: false,
|
|
1117
|
+
maxPages: null,
|
|
1118
|
+
});
|
|
1119
|
+
yield* trySync(() => writeSyncCache(writeCacheKey, payload, db));
|
|
1120
|
+
}
|
|
940
1121
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1122
|
+
return {
|
|
1123
|
+
ok: true,
|
|
1124
|
+
source,
|
|
1125
|
+
kind: "mentions",
|
|
1126
|
+
accountId: resolvedAccount.accountId,
|
|
1127
|
+
count: payload.data.length,
|
|
1128
|
+
partial: isMaxPagesPartial({ payload, maxPages: parsedMaxPages }),
|
|
1129
|
+
payload,
|
|
1130
|
+
};
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
export function syncMentions(options: SyncMentionsOptions) {
|
|
1135
|
+
return runEffectPromise(syncMentionsEffect(options));
|
|
950
1136
|
}
|
|
951
1137
|
|
|
952
|
-
|
|
1138
|
+
function exportMentionsViaCachedLiveSourceEffect({
|
|
953
1139
|
mode,
|
|
954
1140
|
account,
|
|
955
1141
|
search,
|
|
@@ -959,90 +1145,37 @@ async function exportMentionsViaCachedLiveSource({
|
|
|
959
1145
|
maxPages,
|
|
960
1146
|
refresh = false,
|
|
961
1147
|
cacheTtlMs,
|
|
962
|
-
}: {
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
maxPages?: number;
|
|
970
|
-
refresh?: boolean;
|
|
971
|
-
cacheTtlMs?: number;
|
|
972
|
-
}) {
|
|
973
|
-
if (mode === "xurl") {
|
|
974
|
-
assertXurlLimit(limit);
|
|
975
|
-
} else {
|
|
976
|
-
assertBirdLimit(limit);
|
|
977
|
-
}
|
|
978
|
-
const parsedMaxPages = parseMaxPages(maxPages);
|
|
979
|
-
const fetchAll = mode === "xurl" && (all || parsedMaxPages !== null);
|
|
980
|
-
|
|
981
|
-
const db = getNativeDb();
|
|
982
|
-
const resolvedAccount = resolveAccount(db, account);
|
|
983
|
-
const cacheKey = getMentionsFetchModeKey({
|
|
984
|
-
scope: "export",
|
|
985
|
-
mode,
|
|
986
|
-
accountId: resolvedAccount.accountId,
|
|
987
|
-
pageSize: limit,
|
|
988
|
-
all: fetchAll,
|
|
989
|
-
maxPages: parsedMaxPages,
|
|
990
|
-
sinceId: null,
|
|
991
|
-
startTime: null,
|
|
992
|
-
});
|
|
993
|
-
const ttlMs = parseCacheTtlMs(cacheTtlMs);
|
|
994
|
-
const cached = readSyncCache<XurlMentionsResponse>(cacheKey, db);
|
|
995
|
-
const cacheAgeMs = cached
|
|
996
|
-
? Date.now() - new Date(cached.updatedAt).getTime()
|
|
997
|
-
: Number.POSITIVE_INFINITY;
|
|
998
|
-
|
|
999
|
-
if (!refresh && cached && cacheAgeMs <= ttlMs) {
|
|
1000
|
-
if (
|
|
1001
|
-
shouldReturnFilteredLocalPayload({
|
|
1002
|
-
search,
|
|
1003
|
-
replyFilter,
|
|
1004
|
-
})
|
|
1005
|
-
) {
|
|
1006
|
-
return readLocalXurlCompatiblePayload({
|
|
1007
|
-
accountId: resolvedAccount.accountId,
|
|
1008
|
-
search,
|
|
1009
|
-
replyFilter,
|
|
1010
|
-
limit: fetchAll ? cached.value.data.length : limit,
|
|
1011
|
-
});
|
|
1012
|
-
}
|
|
1013
|
-
return cached.value;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
try {
|
|
1017
|
-
const payload =
|
|
1018
|
-
mode === "bird"
|
|
1019
|
-
? await listMentionsViaBird({ maxResults: limit })
|
|
1020
|
-
: await fetchMentionsViaXurl({
|
|
1021
|
-
resolvedAccount,
|
|
1022
|
-
limit,
|
|
1023
|
-
all: fetchAll,
|
|
1024
|
-
parsedMaxPages,
|
|
1025
|
-
});
|
|
1026
|
-
mergeMentionsIntoLocalStore(db, resolvedAccount.accountId, payload, mode);
|
|
1027
|
-
writeSyncCache(cacheKey, payload, db);
|
|
1028
|
-
|
|
1029
|
-
if (
|
|
1030
|
-
shouldReturnFilteredLocalPayload({
|
|
1031
|
-
search,
|
|
1032
|
-
replyFilter,
|
|
1033
|
-
})
|
|
1034
|
-
) {
|
|
1035
|
-
return readLocalXurlCompatiblePayload({
|
|
1036
|
-
accountId: resolvedAccount.accountId,
|
|
1037
|
-
search,
|
|
1038
|
-
replyFilter,
|
|
1039
|
-
limit: fetchAll ? payload.data.length : limit,
|
|
1040
|
-
});
|
|
1148
|
+
}: ExportMentionsViaCachedLiveSourceOptions) {
|
|
1149
|
+
return Effect.gen(function* () {
|
|
1150
|
+
const primaryMode: MentionLiveSource = mode === "auto" ? "xurl" : mode;
|
|
1151
|
+
if (primaryMode === "xurl") {
|
|
1152
|
+
yield* trySync(() => assertXurlLimit(limit));
|
|
1153
|
+
} else {
|
|
1154
|
+
yield* trySync(() => assertBirdLimit(limit));
|
|
1041
1155
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1156
|
+
const parsedMaxPages = yield* trySync(() => parseMaxPages(maxPages));
|
|
1157
|
+
const fetchAll = primaryMode === "xurl" && (all || parsedMaxPages !== null);
|
|
1158
|
+
|
|
1159
|
+
const db = yield* trySync(() => getNativeDb());
|
|
1160
|
+
const resolvedAccount = yield* trySync(() => resolveAccount(db, account));
|
|
1161
|
+
const cacheKey = getMentionsFetchModeKey({
|
|
1162
|
+
scope: "export",
|
|
1163
|
+
mode,
|
|
1164
|
+
accountId: resolvedAccount.accountId,
|
|
1165
|
+
pageSize: limit,
|
|
1166
|
+
all: fetchAll,
|
|
1167
|
+
maxPages: parsedMaxPages,
|
|
1168
|
+
sinceId: null,
|
|
1169
|
+
startTime: null,
|
|
1170
|
+
});
|
|
1171
|
+
const ttlMs = parseCacheTtlMs(cacheTtlMs);
|
|
1172
|
+
const cached = yield* trySync(() =>
|
|
1173
|
+
readSyncCache<XurlMentionsResponse>(cacheKey, db),
|
|
1174
|
+
);
|
|
1175
|
+
const cacheAgeMs = cached
|
|
1176
|
+
? Date.now() - new Date(cached.updatedAt).getTime()
|
|
1177
|
+
: Number.POSITIVE_INFINITY;
|
|
1178
|
+
const readFilteredOrRaw = (payload: XurlMentionsResponse) => {
|
|
1046
1179
|
if (
|
|
1047
1180
|
shouldReturnFilteredLocalPayload({
|
|
1048
1181
|
search,
|
|
@@ -1053,35 +1186,97 @@ async function exportMentionsViaCachedLiveSource({
|
|
|
1053
1186
|
accountId: resolvedAccount.accountId,
|
|
1054
1187
|
search,
|
|
1055
1188
|
replyFilter,
|
|
1056
|
-
limit: fetchAll ?
|
|
1189
|
+
limit: fetchAll ? payload.data.length : limit,
|
|
1057
1190
|
});
|
|
1058
1191
|
}
|
|
1059
|
-
return
|
|
1192
|
+
return payload;
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
if (!refresh && cached && cacheAgeMs <= ttlMs) {
|
|
1196
|
+
return yield* trySync(() => readFilteredOrRaw(cached.value));
|
|
1060
1197
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1198
|
+
|
|
1199
|
+
let source: MentionLiveSource = primaryMode;
|
|
1200
|
+
const liveResult = yield* (
|
|
1201
|
+
primaryMode === "bird"
|
|
1202
|
+
? fetchMentionsViaBirdEffect({ limit })
|
|
1203
|
+
: fetchMentionsViaXurlEffect({
|
|
1204
|
+
resolvedAccount,
|
|
1205
|
+
limit,
|
|
1206
|
+
all: fetchAll,
|
|
1207
|
+
parsedMaxPages,
|
|
1208
|
+
})
|
|
1209
|
+
).pipe(
|
|
1210
|
+
Effect.catchAll((error) => {
|
|
1211
|
+
if (mode !== "auto" || fetchAll) return Effect.fail(error);
|
|
1212
|
+
source = "bird";
|
|
1213
|
+
return verifyBirdAccountMatchesEffect(resolvedAccount).pipe(
|
|
1214
|
+
Effect.flatMap(() => fetchMentionsViaBirdEffect({ limit })),
|
|
1215
|
+
);
|
|
1216
|
+
}),
|
|
1217
|
+
Effect.flatMap((payload) =>
|
|
1218
|
+
trySync(() => {
|
|
1219
|
+
mergeMentionsIntoLocalStore(
|
|
1220
|
+
db,
|
|
1221
|
+
resolvedAccount.accountId,
|
|
1222
|
+
payload,
|
|
1223
|
+
source,
|
|
1224
|
+
);
|
|
1225
|
+
writeSyncCache(cacheKey, payload, db);
|
|
1226
|
+
return readFilteredOrRaw(payload);
|
|
1227
|
+
}),
|
|
1228
|
+
),
|
|
1229
|
+
Effect.map((payload) => ({ ok: true as const, payload })),
|
|
1230
|
+
Effect.catchAll((error) => {
|
|
1231
|
+
if (!refresh && cached) {
|
|
1232
|
+
return Effect.succeed({ ok: false as const });
|
|
1233
|
+
}
|
|
1234
|
+
return Effect.fail(error);
|
|
1235
|
+
}),
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1238
|
+
if (!liveResult.ok) {
|
|
1239
|
+
if (!cached) {
|
|
1240
|
+
return yield* Effect.fail(
|
|
1241
|
+
new Error("Mention export failed without cache"),
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
return yield* trySync(() => readFilteredOrRaw(cached.value));
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
return liveResult.payload;
|
|
1248
|
+
});
|
|
1063
1249
|
}
|
|
1064
1250
|
|
|
1065
|
-
export
|
|
1066
|
-
options: Omit<
|
|
1067
|
-
Parameters<typeof exportMentionsViaCachedLiveSource>[0],
|
|
1068
|
-
"mode"
|
|
1069
|
-
>,
|
|
1251
|
+
export function exportMentionsViaCachedXurl(
|
|
1252
|
+
options: Omit<ExportMentionsViaCachedLiveSourceOptions, "mode">,
|
|
1070
1253
|
) {
|
|
1071
|
-
return
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1254
|
+
return runEffectPromise(
|
|
1255
|
+
exportMentionsViaCachedLiveSourceEffect({
|
|
1256
|
+
...options,
|
|
1257
|
+
mode: "xurl",
|
|
1258
|
+
}),
|
|
1259
|
+
);
|
|
1075
1260
|
}
|
|
1076
1261
|
|
|
1077
|
-
export
|
|
1078
|
-
options: Omit<
|
|
1079
|
-
Parameters<typeof exportMentionsViaCachedLiveSource>[0],
|
|
1080
|
-
"mode"
|
|
1081
|
-
>,
|
|
1262
|
+
export function exportMentionsViaCachedBird(
|
|
1263
|
+
options: Omit<ExportMentionsViaCachedLiveSourceOptions, "mode">,
|
|
1082
1264
|
) {
|
|
1083
|
-
return
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1265
|
+
return runEffectPromise(
|
|
1266
|
+
exportMentionsViaCachedLiveSourceEffect({
|
|
1267
|
+
...options,
|
|
1268
|
+
mode: "bird",
|
|
1269
|
+
}),
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
export function exportMentionsViaCachedAuto(
|
|
1274
|
+
options: Omit<ExportMentionsViaCachedLiveSourceOptions, "mode">,
|
|
1275
|
+
) {
|
|
1276
|
+
return runEffectPromise(
|
|
1277
|
+
exportMentionsViaCachedLiveSourceEffect({
|
|
1278
|
+
...options,
|
|
1279
|
+
mode: "auto",
|
|
1280
|
+
}),
|
|
1281
|
+
);
|
|
1087
1282
|
}
|