birdclaw 0.5.0 → 0.6.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +55 -5
  3. package/bin/birdclaw.mjs +50 -11
  4. package/package.json +9 -7
  5. package/public/birdclaw-mark.png +0 -0
  6. package/scripts/browser-perf.mjs +400 -0
  7. package/scripts/build-docs-site.mjs +940 -0
  8. package/scripts/docs-site-assets.mjs +311 -0
  9. package/scripts/run-vitest.mjs +21 -0
  10. package/scripts/sanitize-node-options.mjs +23 -0
  11. package/scripts/start-test-server.mjs +42 -0
  12. package/src/cli.ts +422 -14
  13. package/src/components/AccountSwitcher.tsx +186 -0
  14. package/src/components/AppNav.tsx +29 -9
  15. package/src/components/AvatarChip.tsx +9 -3
  16. package/src/components/BrandMark.tsx +67 -0
  17. package/src/components/ConversationThread.tsx +11 -10
  18. package/src/components/DmWorkspace.tsx +39 -14
  19. package/src/components/FeedState.tsx +147 -0
  20. package/src/components/InboxCard.tsx +14 -2
  21. package/src/components/LinkPreviewCard.tsx +40 -18
  22. package/src/components/MarkdownViewer.tsx +452 -0
  23. package/src/components/SavedTimelineView.tsx +64 -56
  24. package/src/components/SyncNowButton.tsx +137 -0
  25. package/src/components/ThemeSlider.tsx +49 -93
  26. package/src/components/TimelineCard.tsx +364 -136
  27. package/src/components/TimelineRouteFrame.tsx +170 -0
  28. package/src/components/TweetMediaGrid.tsx +170 -24
  29. package/src/components/TweetRichText.tsx +28 -13
  30. package/src/components/account-selection.ts +64 -0
  31. package/src/components/useTimelineRouteData.ts +153 -0
  32. package/src/lib/account-sync-job.ts +654 -0
  33. package/src/lib/actions-transport.ts +216 -146
  34. package/src/lib/api-client.ts +304 -0
  35. package/src/lib/archive-finder.ts +72 -53
  36. package/src/lib/archive-import.ts +1377 -1298
  37. package/src/lib/authored-live.ts +261 -204
  38. package/src/lib/avatar-cache.ts +159 -44
  39. package/src/lib/backup.ts +1532 -951
  40. package/src/lib/bird-actions.ts +139 -57
  41. package/src/lib/bird-command.ts +101 -28
  42. package/src/lib/bird.ts +549 -194
  43. package/src/lib/blocklist.ts +40 -23
  44. package/src/lib/blocks-write.ts +129 -80
  45. package/src/lib/blocks.ts +165 -97
  46. package/src/lib/bookmark-sync-job.ts +250 -160
  47. package/src/lib/conversation-surface.ts +205 -0
  48. package/src/lib/db.ts +35 -3
  49. package/src/lib/dms-live.ts +720 -66
  50. package/src/lib/effect-runtime.ts +45 -0
  51. package/src/lib/follow-graph.ts +224 -180
  52. package/src/lib/http-effect.ts +222 -0
  53. package/src/lib/inbox.ts +74 -43
  54. package/src/lib/link-index.ts +88 -76
  55. package/src/lib/link-insights.ts +24 -0
  56. package/src/lib/link-preview-metadata.ts +472 -52
  57. package/src/lib/media-fetch.ts +286 -213
  58. package/src/lib/mention-threads-live.ts +352 -288
  59. package/src/lib/mentions-live.ts +390 -342
  60. package/src/lib/moderation-target.ts +102 -65
  61. package/src/lib/moderation-write.ts +77 -18
  62. package/src/lib/mutes-write.ts +129 -80
  63. package/src/lib/mutes.ts +8 -1
  64. package/src/lib/openai.ts +84 -53
  65. package/src/lib/period-digest.ts +953 -0
  66. package/src/lib/profile-affiliation-hydration.ts +93 -54
  67. package/src/lib/profile-hydration.ts +124 -72
  68. package/src/lib/profile-replies.ts +60 -43
  69. package/src/lib/profile-resolver.ts +402 -294
  70. package/src/lib/queries.ts +1024 -189
  71. package/src/lib/research.ts +165 -120
  72. package/src/lib/sqlite.ts +1 -0
  73. package/src/lib/timeline-collections-live.ts +204 -167
  74. package/src/lib/timeline-live.ts +60 -39
  75. package/src/lib/tweet-lookup.ts +30 -19
  76. package/src/lib/types.ts +38 -1
  77. package/src/lib/ui.ts +41 -10
  78. package/src/lib/url-expansion.ts +226 -55
  79. package/src/lib/url-safety.ts +220 -0
  80. package/src/lib/web-sync.ts +511 -0
  81. package/src/lib/whois.ts +166 -147
  82. package/src/lib/x-web.ts +102 -71
  83. package/src/lib/xurl.ts +681 -411
  84. package/src/routeTree.gen.ts +63 -0
  85. package/src/routes/__root.tsx +25 -5
  86. package/src/routes/api/action.tsx +127 -78
  87. package/src/routes/api/avatar.tsx +39 -30
  88. package/src/routes/api/blocks.tsx +26 -23
  89. package/src/routes/api/conversation.tsx +25 -14
  90. package/src/routes/api/inbox.tsx +27 -21
  91. package/src/routes/api/link-insights.tsx +31 -25
  92. package/src/routes/api/link-preview.tsx +25 -21
  93. package/src/routes/api/period-digest.tsx +123 -0
  94. package/src/routes/api/profile-hydrate.tsx +31 -28
  95. package/src/routes/api/query.tsx +79 -55
  96. package/src/routes/api/status.tsx +18 -10
  97. package/src/routes/api/sync.tsx +105 -0
  98. package/src/routes/dms.tsx +195 -55
  99. package/src/routes/inbox.tsx +32 -19
  100. package/src/routes/index.tsx +21 -127
  101. package/src/routes/links.tsx +50 -5
  102. package/src/routes/mentions.tsx +21 -127
  103. package/src/routes/today.tsx +441 -0
  104. package/src/styles.css +74 -11
  105. package/vite.config.ts +8 -0
@@ -1,5 +1,7 @@
1
1
  import type { Database } from "./sqlite";
2
+ import { Effect } from "effect";
2
3
  import { getNativeDb } from "./db";
4
+ import { runEffectPromise, tryPromise } from "./effect-runtime";
3
5
  import { readSyncCache, writeSyncCache } from "./sync-cache";
4
6
  import type {
5
7
  TweetEntities,
@@ -26,6 +28,15 @@ import {
26
28
 
27
29
  export type AuthoredSyncMode = "xurl";
28
30
 
31
+ export interface SyncAuthoredTweetsOptions {
32
+ account?: string;
33
+ mode?: AuthoredSyncMode;
34
+ limit?: number;
35
+ maxPages?: number;
36
+ sinceId?: string;
37
+ untilId?: string;
38
+ }
39
+
29
40
  export class AuthoredSyncError extends Error {
30
41
  constructor(
31
42
  message: string,
@@ -108,6 +119,17 @@ const AUTHORED_MEDIA_FIELDS = [
108
119
  "alt_text",
109
120
  ];
110
121
 
122
+ function toError(error: unknown) {
123
+ return error instanceof Error ? error : new Error(String(error));
124
+ }
125
+
126
+ function trySync<T>(try_: () => T) {
127
+ return Effect.try({
128
+ try: try_,
129
+ catch: toError,
130
+ });
131
+ }
132
+
111
133
  function assertXurlLimit(limit: number) {
112
134
  if (
113
135
  !Number.isFinite(limit) ||
@@ -413,57 +435,65 @@ function userFromAuthenticatedPayload(
413
435
  };
414
436
  }
415
437
 
416
- async function resolveAuthoredIdentity({
438
+ function resolveAuthoredIdentityEffect({
417
439
  account,
418
440
  db,
419
441
  }: {
420
442
  account?: string;
421
443
  db: Database;
422
444
  }) {
423
- const status = await getTransportStatus();
424
- if (status.availableTransport !== "xurl") {
425
- throw new AuthoredSyncError(status.statusText, 4);
426
- }
445
+ return Effect.gen(function* () {
446
+ const status = yield* tryPromise(() => getTransportStatus());
447
+ if (status.availableTransport !== "xurl") {
448
+ return yield* Effect.fail(new AuthoredSyncError(status.statusText, 4));
449
+ }
427
450
 
428
- const resolvedAccount = resolveAccount(db, account);
429
- if (resolvedAccount.externalUserId) {
430
- return {
431
- ...resolvedAccount,
432
- userId: resolvedAccount.externalUserId,
433
- authenticatedUser: undefined,
434
- };
435
- }
451
+ const resolvedAccount = yield* trySync(() => resolveAccount(db, account));
452
+ if (resolvedAccount.externalUserId) {
453
+ return {
454
+ ...resolvedAccount,
455
+ userId: resolvedAccount.externalUserId,
456
+ authenticatedUser: undefined,
457
+ };
458
+ }
436
459
 
437
- const authenticated = await lookupAuthenticatedUser();
438
- const authenticatedUser = userFromAuthenticatedPayload(authenticated);
439
- if (!authenticatedUser?.id) {
440
- throw new AuthoredSyncError(
441
- "Could not resolve authenticated Twitter user id",
442
- 4,
443
- );
444
- }
460
+ const authenticated = yield* tryPromise(() => lookupAuthenticatedUser());
461
+ const authenticatedUser = userFromAuthenticatedPayload(authenticated);
462
+ if (!authenticatedUser?.id) {
463
+ return yield* Effect.fail(
464
+ new AuthoredSyncError(
465
+ "Could not resolve authenticated Twitter user id",
466
+ 4,
467
+ ),
468
+ );
469
+ }
445
470
 
446
- if (
447
- normalizeUsername(authenticatedUser.username) !==
448
- normalizeUsername(resolvedAccount.username)
449
- ) {
450
- throw new AuthoredSyncError(
451
- `xurl is authenticated as @${authenticatedUser.username}, but selected account ${resolvedAccount.accountId} is @${resolvedAccount.username}. Link the account external_user_id or switch xurl login before syncing authored tweets.`,
452
- 4,
453
- );
454
- }
471
+ if (
472
+ normalizeUsername(authenticatedUser.username) !==
473
+ normalizeUsername(resolvedAccount.username)
474
+ ) {
475
+ return yield* Effect.fail(
476
+ new AuthoredSyncError(
477
+ `xurl is authenticated as @${authenticatedUser.username}, but selected account ${resolvedAccount.accountId} is @${resolvedAccount.username}. Link the account external_user_id or switch xurl login before syncing authored tweets.`,
478
+ 4,
479
+ ),
480
+ );
481
+ }
455
482
 
456
- persistAccountExternalUserId(
457
- db,
458
- resolvedAccount.accountId,
459
- authenticatedUser.id,
460
- );
483
+ yield* trySync(() =>
484
+ persistAccountExternalUserId(
485
+ db,
486
+ resolvedAccount.accountId,
487
+ authenticatedUser.id,
488
+ ),
489
+ );
461
490
 
462
- return {
463
- ...resolvedAccount,
464
- userId: authenticatedUser.id,
465
- authenticatedUser,
466
- };
491
+ return {
492
+ ...resolvedAccount,
493
+ userId: authenticatedUser.id,
494
+ authenticatedUser,
495
+ };
496
+ });
467
497
  }
468
498
 
469
499
  function toFallbackUser({
@@ -887,188 +917,215 @@ function buildResult({
887
917
  };
888
918
  }
889
919
 
890
- export async function syncAuthoredTweets({
920
+ export function syncAuthoredTweetsEffect({
891
921
  account,
892
922
  mode = "xurl",
893
923
  limit = DEFAULT_LIMIT,
894
924
  maxPages,
895
925
  sinceId,
896
926
  untilId,
897
- }: {
898
- account?: string;
899
- mode?: AuthoredSyncMode;
900
- limit?: number;
901
- maxPages?: number;
902
- sinceId?: string;
903
- untilId?: string;
904
- }) {
905
- if (mode !== "xurl") {
906
- throw new Error("authored sync only supports --mode xurl");
907
- }
927
+ }: SyncAuthoredTweetsOptions) {
928
+ return Effect.gen(function* () {
929
+ if (mode !== "xurl") {
930
+ return yield* Effect.fail(
931
+ new Error("authored sync only supports --mode xurl"),
932
+ );
933
+ }
908
934
 
909
- const pageLimit = assertXurlLimit(limit);
910
- const parsedMaxPages = parseMaxPages(maxPages);
911
- const db = getNativeDb();
912
- const identity = await resolveAuthoredIdentity({ account, db });
913
- const cursor = readAuthoredCursor(db, identity.accountId);
914
- const usePersistedForward =
915
- sinceId === undefined && !untilId && cursor.state === "pending-forward";
916
- const usePersistedUntil =
917
- sinceId === undefined &&
918
- Boolean(untilId) &&
919
- cursor.state === "pending-until" &&
920
- cursor.untilId === untilId;
921
- const shouldSeedFromArchive =
922
- !usePersistedForward &&
923
- !cursor.sinceId &&
924
- sinceId === undefined &&
925
- !untilId;
926
- const archiveSinceSeed = shouldSeedFromArchive
927
- ? findArchiveAuthoredSinceSeed(db, identity.accountId)
928
- : null;
929
- if (shouldSeedFromArchive && !archiveSinceSeed) {
930
- console.error(
931
- "birdclaw sync authored: no archive baseline found; starting a full backwards scan",
935
+ const pageLimit = yield* trySync(() => assertXurlLimit(limit));
936
+ const parsedMaxPages = yield* trySync(() => parseMaxPages(maxPages));
937
+ const db = yield* trySync(() => getNativeDb());
938
+ const identity = yield* resolveAuthoredIdentityEffect({ account, db });
939
+ const cursor = yield* trySync(() =>
940
+ readAuthoredCursor(db, identity.accountId),
932
941
  );
933
- }
934
- const persistedUntilSinceId: string | null = usePersistedUntil
935
- ? (("requestedSinceId" in cursor
936
- ? cursor.requestedSinceId
937
- : cursor.sinceId) ?? null)
938
- : null;
939
- const effectiveSinceId: string | null =
940
- sinceId ??
941
- archiveSinceSeed ??
942
- (untilId ? persistedUntilSinceId : cursor.sinceId) ??
943
- null;
944
- let nextToken = usePersistedForward
945
- ? cursor.token
946
- : usePersistedUntil
942
+ const usePersistedForward =
943
+ sinceId === undefined && !untilId && cursor.state === "pending-forward";
944
+ const usePersistedUntil =
945
+ sinceId === undefined &&
946
+ Boolean(untilId) &&
947
+ cursor.state === "pending-until" &&
948
+ cursor.untilId === untilId;
949
+ const shouldSeedFromArchive =
950
+ !usePersistedForward &&
951
+ !cursor.sinceId &&
952
+ sinceId === undefined &&
953
+ !untilId;
954
+ const archiveSinceSeed = shouldSeedFromArchive
955
+ ? yield* trySync(() =>
956
+ findArchiveAuthoredSinceSeed(db, identity.accountId),
957
+ )
958
+ : null;
959
+ if (shouldSeedFromArchive && !archiveSinceSeed) {
960
+ console.error(
961
+ "birdclaw sync authored: no archive baseline found; starting a full backwards scan",
962
+ );
963
+ }
964
+ const persistedUntilSinceId: string | null = usePersistedUntil
965
+ ? (("requestedSinceId" in cursor
966
+ ? cursor.requestedSinceId
967
+ : cursor.sinceId) ?? null)
968
+ : null;
969
+ const effectiveSinceId: string | null =
970
+ sinceId ??
971
+ archiveSinceSeed ??
972
+ (untilId ? persistedUntilSinceId : cursor.sinceId) ??
973
+ null;
974
+ let nextToken = usePersistedForward
947
975
  ? cursor.token
948
- : undefined;
949
- let newestSeenId = usePersistedForward
950
- ? maxTweetId(cursor.sinceId, cursor.pendingNewestId)
951
- : cursor.sinceId;
952
- const pages: XurlUserTweetsResponse[] = [];
953
- const sourceUser = toFallbackUser({
954
- userId: identity.userId,
955
- username: identity.username,
956
- authenticatedUser: identity.authenticatedUser,
957
- });
958
- let pageCount = 0;
959
-
960
- while (parsedMaxPages === null || pageCount < parsedMaxPages) {
961
- let page: XurlUserTweetsResponse;
962
- try {
963
- page = await listUserTweets(identity.userId, {
964
- maxResults: pageLimit,
965
- paginationToken: nextToken,
966
- excludeRetweets: false,
967
- sinceId: effectiveSinceId ?? undefined,
968
- untilId,
969
- tweetFields: AUTHORED_TWEET_FIELDS,
970
- expansions: AUTHORED_EXPANSIONS,
971
- userFields: AUTHORED_USER_FIELDS,
972
- mediaFields: AUTHORED_MEDIA_FIELDS,
973
- auth: "oauth2",
974
- });
975
- } catch (error) {
976
- if (pages.length === 0) {
977
- throw error;
976
+ : usePersistedUntil
977
+ ? cursor.token
978
+ : undefined;
979
+ let newestSeenId = usePersistedForward
980
+ ? maxTweetId(cursor.sinceId, cursor.pendingNewestId)
981
+ : cursor.sinceId;
982
+ const pages: XurlUserTweetsResponse[] = [];
983
+ const sourceUser = toFallbackUser({
984
+ userId: identity.userId,
985
+ username: identity.username,
986
+ authenticatedUser: identity.authenticatedUser,
987
+ });
988
+ let pageCount = 0;
989
+
990
+ while (parsedMaxPages === null || pageCount < parsedMaxPages) {
991
+ const fetched = yield* tryPromise(() =>
992
+ listUserTweets(identity.userId, {
993
+ maxResults: pageLimit,
994
+ paginationToken: nextToken,
995
+ excludeRetweets: false,
996
+ sinceId: effectiveSinceId ?? undefined,
997
+ untilId,
998
+ tweetFields: AUTHORED_TWEET_FIELDS,
999
+ expansions: AUTHORED_EXPANSIONS,
1000
+ userFields: AUTHORED_USER_FIELDS,
1001
+ mediaFields: AUTHORED_MEDIA_FIELDS,
1002
+ auth: "oauth2",
1003
+ }),
1004
+ ).pipe(
1005
+ Effect.map((page) => ({ ok: true as const, page })),
1006
+ Effect.catchAll((error) =>
1007
+ Effect.succeed({ ok: false as const, error }),
1008
+ ),
1009
+ );
1010
+
1011
+ if (!fetched.ok) {
1012
+ if (pages.length === 0) {
1013
+ return yield* Effect.fail(fetched.error);
1014
+ }
1015
+ const payload = mergePages({
1016
+ pages,
1017
+ userId: identity.userId,
1018
+ nextToken: nextToken ?? null,
1019
+ });
1020
+ return buildResult({
1021
+ accountId: identity.accountId,
1022
+ userId: identity.userId,
1023
+ effectiveSinceId,
1024
+ nextSinceId: untilId ? cursor.sinceId : effectiveSinceId,
1025
+ nextToken: nextToken ?? null,
1026
+ pageCount,
1027
+ payload,
1028
+ partial: true,
1029
+ error: formatError(fetched.error),
1030
+ });
978
1031
  }
979
- const payload = mergePages({
980
- pages,
981
- userId: identity.userId,
982
- nextToken: nextToken ?? null,
983
- });
984
- return buildResult({
985
- accountId: identity.accountId,
1032
+
1033
+ const page = fetched.page;
1034
+ const pagePayload = mergePages({
1035
+ pages: [page],
986
1036
  userId: identity.userId,
987
- effectiveSinceId,
988
- nextSinceId: untilId ? cursor.sinceId : effectiveSinceId,
989
- nextToken: nextToken ?? null,
990
- pageCount,
991
- payload,
992
- partial: true,
993
- error: formatError(error),
1037
+ nextToken: page.nextToken,
994
1038
  });
995
- }
1039
+ yield* trySync(() =>
1040
+ mergeAuthoredPayloadIntoLocalStore({
1041
+ db,
1042
+ accountId: identity.accountId,
1043
+ payload: pagePayload,
1044
+ sourceUser,
1045
+ }),
1046
+ );
1047
+ pages.push(page);
1048
+ pageCount += 1;
1049
+ newestSeenId = maxTweetId(newestSeenId, pagePayload.meta.newest_id);
1050
+ nextToken = page.nextToken ?? undefined;
996
1051
 
997
- const pagePayload = mergePages({
998
- pages: [page],
999
- userId: identity.userId,
1000
- nextToken: page.nextToken,
1001
- });
1002
- mergeAuthoredPayloadIntoLocalStore({
1003
- db,
1004
- accountId: identity.accountId,
1005
- payload: pagePayload,
1006
- sourceUser,
1007
- });
1008
- pages.push(page);
1009
- pageCount += 1;
1010
- newestSeenId = maxTweetId(newestSeenId, pagePayload.meta.newest_id);
1011
- nextToken = page.nextToken ?? undefined;
1012
-
1013
- if (nextToken && untilId) {
1014
- writePendingUntilCursor(db, identity.accountId, {
1015
- sinceId: cursor.sinceId,
1016
- token: nextToken,
1017
- untilId,
1018
- requestedSinceId: effectiveSinceId,
1019
- });
1020
- } else if (nextToken) {
1021
- writePendingForwardCursor(db, identity.accountId, {
1022
- sinceId: effectiveSinceId,
1023
- token: nextToken,
1024
- pendingNewestId: newestSeenId,
1025
- });
1052
+ const pendingToken = nextToken;
1053
+ if (pendingToken && untilId) {
1054
+ yield* trySync(() =>
1055
+ writePendingUntilCursor(db, identity.accountId, {
1056
+ sinceId: cursor.sinceId,
1057
+ token: pendingToken,
1058
+ untilId,
1059
+ requestedSinceId: effectiveSinceId,
1060
+ }),
1061
+ );
1062
+ } else if (pendingToken) {
1063
+ yield* trySync(() =>
1064
+ writePendingForwardCursor(db, identity.accountId, {
1065
+ sinceId: effectiveSinceId,
1066
+ token: pendingToken,
1067
+ pendingNewestId: newestSeenId,
1068
+ }),
1069
+ );
1070
+ }
1071
+
1072
+ if (!nextToken) {
1073
+ break;
1074
+ }
1026
1075
  }
1027
1076
 
1028
- if (!nextToken) {
1029
- break;
1077
+ const capped = Boolean(nextToken);
1078
+ const nextSinceId = untilId
1079
+ ? cursor.sinceId
1080
+ : capped
1081
+ ? effectiveSinceId
1082
+ : maxTweetId(newestSeenId, effectiveSinceId, cursor.sinceId);
1083
+ if (untilId && capped && nextToken) {
1084
+ yield* trySync(() =>
1085
+ writePendingUntilCursor(db, identity.accountId, {
1086
+ sinceId: cursor.sinceId,
1087
+ token: nextToken,
1088
+ untilId,
1089
+ requestedSinceId: effectiveSinceId,
1090
+ }),
1091
+ );
1092
+ } else if (untilId) {
1093
+ yield* trySync(() =>
1094
+ writeCommittedCursor(db, identity.accountId, cursor.sinceId),
1095
+ );
1096
+ } else if (capped && nextToken) {
1097
+ yield* trySync(() =>
1098
+ writePendingForwardCursor(db, identity.accountId, {
1099
+ sinceId: nextSinceId,
1100
+ token: nextToken,
1101
+ pendingNewestId: newestSeenId,
1102
+ }),
1103
+ );
1104
+ } else {
1105
+ yield* trySync(() =>
1106
+ writeCommittedCursor(db, identity.accountId, nextSinceId),
1107
+ );
1030
1108
  }
1031
- }
1032
1109
 
1033
- const capped = Boolean(nextToken);
1034
- const nextSinceId = untilId
1035
- ? cursor.sinceId
1036
- : capped
1037
- ? effectiveSinceId
1038
- : maxTweetId(newestSeenId, effectiveSinceId, cursor.sinceId);
1039
- if (untilId && capped && nextToken) {
1040
- writePendingUntilCursor(db, identity.accountId, {
1041
- sinceId: cursor.sinceId,
1042
- token: nextToken,
1043
- untilId,
1044
- requestedSinceId: effectiveSinceId,
1110
+ const payload = mergePages({
1111
+ pages,
1112
+ userId: identity.userId,
1113
+ nextToken: nextToken ?? null,
1045
1114
  });
1046
- } else if (untilId) {
1047
- writeCommittedCursor(db, identity.accountId, cursor.sinceId);
1048
- } else if (capped && nextToken) {
1049
- writePendingForwardCursor(db, identity.accountId, {
1050
- sinceId: nextSinceId,
1051
- token: nextToken,
1052
- pendingNewestId: newestSeenId,
1115
+ return buildResult({
1116
+ accountId: identity.accountId,
1117
+ userId: identity.userId,
1118
+ effectiveSinceId,
1119
+ nextSinceId,
1120
+ nextToken: nextToken ?? null,
1121
+ pageCount,
1122
+ payload,
1123
+ partial: capped,
1124
+ ...(capped ? { error: "max pages reached before sync completed" } : {}),
1053
1125
  });
1054
- } else {
1055
- writeCommittedCursor(db, identity.accountId, nextSinceId);
1056
- }
1057
-
1058
- const payload = mergePages({
1059
- pages,
1060
- userId: identity.userId,
1061
- nextToken: nextToken ?? null,
1062
- });
1063
- return buildResult({
1064
- accountId: identity.accountId,
1065
- userId: identity.userId,
1066
- effectiveSinceId,
1067
- nextSinceId,
1068
- nextToken: nextToken ?? null,
1069
- pageCount,
1070
- payload,
1071
- partial: capped,
1072
- ...(capped ? { error: "max pages reached before sync completed" } : {}),
1073
1126
  });
1074
1127
  }
1128
+
1129
+ export function syncAuthoredTweets(options: SyncAuthoredTweetsOptions) {
1130
+ return runEffectPromise(syncAuthoredTweetsEffect(options));
1131
+ }