birdclaw 0.8.2 → 0.8.3

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 (89) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +2 -1
  3. package/src/cli/command-context.ts +17 -0
  4. package/src/cli/register-analysis.ts +500 -0
  5. package/src/cli/register-compose.ts +40 -0
  6. package/src/cli/register-graph.ts +132 -0
  7. package/src/cli/register-inbox.ts +41 -0
  8. package/src/cli/register-storage.ts +106 -0
  9. package/src/cli.ts +30 -750
  10. package/src/components/AccountSwitcher.tsx +7 -15
  11. package/src/components/AvatarChip.tsx +1 -1
  12. package/src/components/AvatarPreload.ts +149 -0
  13. package/src/components/MarkdownCitations.tsx +680 -0
  14. package/src/components/MarkdownViewer.tsx +8 -674
  15. package/src/components/ProfileAnalysisClient.ts +191 -0
  16. package/src/components/ProfileAnalysisStream.tsx +16 -185
  17. package/src/components/ProfilePreview.tsx +2 -0
  18. package/src/components/links-controller.ts +162 -0
  19. package/src/components/links-model.ts +198 -0
  20. package/src/components/network-map-controller.ts +84 -0
  21. package/src/components/network-map-model.ts +255 -0
  22. package/src/components/useTimelineRouteData.ts +105 -235
  23. package/src/lib/analysis-runtime.ts +238 -0
  24. package/src/lib/api-client.ts +16 -215
  25. package/src/lib/api-contracts.ts +328 -0
  26. package/src/lib/archive-import-plan.ts +102 -0
  27. package/src/lib/archive-import.ts +170 -239
  28. package/src/lib/authored-live.ts +75 -120
  29. package/src/lib/backup.ts +335 -424
  30. package/src/lib/blocks-write.ts +30 -26
  31. package/src/lib/blocks.ts +18 -20
  32. package/src/lib/database-metrics.ts +88 -0
  33. package/src/lib/database-migrations.ts +34 -0
  34. package/src/lib/database-schema.ts +312 -0
  35. package/src/lib/database-writer.ts +69 -0
  36. package/src/lib/db.ts +84 -330
  37. package/src/lib/dm-read-model.ts +533 -0
  38. package/src/lib/dms-live.ts +34 -97
  39. package/src/lib/follow-graph.ts +17 -27
  40. package/src/lib/import-repository.ts +138 -0
  41. package/src/lib/inbox.ts +2 -1
  42. package/src/lib/live-sync-engine.ts +209 -0
  43. package/src/lib/live-transport-gateway.ts +128 -0
  44. package/src/lib/mention-threads-live.ts +90 -177
  45. package/src/lib/mentions-export.ts +1 -1
  46. package/src/lib/mentions-live.ts +57 -181
  47. package/src/lib/moderation-target.ts +15 -4
  48. package/src/lib/moderation-write.ts +1 -1
  49. package/src/lib/mutes-write.ts +30 -26
  50. package/src/lib/openai-response-runtime.ts +251 -0
  51. package/src/lib/paginated-sync.ts +93 -0
  52. package/src/lib/period-digest.ts +116 -304
  53. package/src/lib/profile-analysis.ts +36 -110
  54. package/src/lib/queries.ts +6 -2381
  55. package/src/lib/query-actions.ts +437 -0
  56. package/src/lib/query-client.tsx +47 -0
  57. package/src/lib/query-read-model-shared.ts +52 -0
  58. package/src/lib/query-read-models.ts +5 -0
  59. package/src/lib/query-resource.ts +41 -0
  60. package/src/lib/query-status.ts +164 -0
  61. package/src/lib/research.ts +1 -1
  62. package/src/lib/runtime-services.ts +20 -0
  63. package/src/lib/search-discussion.ts +75 -279
  64. package/src/lib/server-runtime-services.ts +30 -0
  65. package/src/lib/sqlite.ts +48 -12
  66. package/src/lib/streaming-ingestion.ts +240 -0
  67. package/src/lib/sync-cache.ts +6 -1
  68. package/src/lib/sync-plan.ts +175 -0
  69. package/src/lib/timeline-collections-live.ts +83 -257
  70. package/src/lib/timeline-live.ts +86 -236
  71. package/src/lib/timeline-read-model.ts +1191 -0
  72. package/src/lib/tweet-repository.ts +156 -0
  73. package/src/lib/tweet-search-live.ts +63 -167
  74. package/src/lib/web-sync.ts +67 -50
  75. package/src/lib/whois.ts +2 -1
  76. package/src/routes/__root.tsx +11 -8
  77. package/src/routes/api/action.tsx +1 -1
  78. package/src/routes/api/conversation.tsx +1 -1
  79. package/src/routes/api/query.tsx +32 -26
  80. package/src/routes/api/status.tsx +6 -4
  81. package/src/routes/api/sync.tsx +5 -2
  82. package/src/routes/blocks.tsx +97 -131
  83. package/src/routes/data-sources.tsx +17 -25
  84. package/src/routes/dms.tsx +167 -184
  85. package/src/routes/inbox.tsx +63 -57
  86. package/src/routes/links.tsx +31 -394
  87. package/src/routes/network-map.tsx +41 -344
  88. package/src/routes/rate-limits.tsx +17 -21
  89. package/src/lib/client-cache.ts +0 -109
@@ -12,9 +12,20 @@ import path from "node:path";
12
12
  import { pipeline } from "node:stream/promises";
13
13
  import { promisify } from "node:util";
14
14
  import { Effect } from "effect";
15
+ import {
16
+ ArchiveImportPlan,
17
+ type ArchiveMessageRow,
18
+ type ArchiveProfileRow,
19
+ } from "./archive-import-plan";
15
20
  import { getBirdclawPaths } from "./config";
16
21
  import { getNativeDb } from "./db";
22
+ import { databaseWriteEffect } from "./database-writer";
17
23
  import { runEffectPromise, tryPromise } from "./effect-runtime";
24
+ import { getImportRepository } from "./import-repository";
25
+ import {
26
+ ingestSourcesInBatchesEffect,
27
+ streamAssignedJsonArray,
28
+ } from "./streaming-ingestion";
18
29
  import { safeHttpUrl } from "./url-safety";
19
30
 
20
31
  const execFileAsync = promisify(execFile);
@@ -204,6 +215,56 @@ function readArchiveEntryEffect(
204
215
  return runUnzipEffect(archivePath, ["-p", archivePath, entryPath]);
205
216
  }
206
217
 
218
+ async function* streamArchiveArrayRecords(
219
+ archivePath: string,
220
+ entryPath: string,
221
+ ) {
222
+ const child = spawn("unzip", ["-p", archivePath, entryPath], {
223
+ stdio: ["ignore", "pipe", "pipe"],
224
+ });
225
+ let stderr = "";
226
+ child.stderr.setEncoding("utf8");
227
+ child.stderr.on("data", (chunk) => {
228
+ stderr += String(chunk);
229
+ });
230
+ const exit = new Promise<number | null>((resolve, reject) => {
231
+ child.on("error", reject);
232
+ child.on("close", resolve);
233
+ });
234
+
235
+ try {
236
+ yield* streamAssignedJsonArray(child.stdout);
237
+ const exitCode = await exit;
238
+ if (exitCode !== 0) {
239
+ throw new Error(
240
+ `Failed to extract ${entryPath}: ${
241
+ stderr.trim() || `exit ${String(exitCode)}`
242
+ }`,
243
+ );
244
+ }
245
+ } finally {
246
+ if (!child.killed) child.kill();
247
+ }
248
+ }
249
+
250
+ function processArchiveEntryRecordsEffect(
251
+ archivePath: string,
252
+ entryPath: string,
253
+ processRecord: (record: ArchiveRecord) => void,
254
+ ) {
255
+ return ingestSourcesInBatchesEffect({
256
+ sources: [
257
+ {
258
+ id: entryPath,
259
+ stream: () => streamArchiveArrayRecords(archivePath, entryPath),
260
+ },
261
+ ],
262
+ processBatch: (batch) => {
263
+ for (const record of batch) processRecord(record);
264
+ },
265
+ });
266
+ }
267
+
207
268
  function getFirstEntry(entries: string[], pattern: RegExp) {
208
269
  return entries.find((entry) => pattern.test(normalizeArchivePath(entry)));
209
270
  }
@@ -640,58 +701,14 @@ function extractArchiveMediaFilesEffect(
640
701
  });
641
702
  }
642
703
 
643
- function getArchiveFollowRows(content: string, key: ArchiveFollowKey) {
644
- const rows: Array<{ profileId: string; externalUserId: string }> = [];
645
-
646
- for (const wrapper of parseArchiveArray(content)) {
647
- const item = asRecord(wrapper[key]);
648
- const externalUserId = String(item?.accountId ?? "");
649
- if (!externalUserId) continue;
650
-
651
- rows.push({
652
- profileId: `profile_user_${externalUserId}`,
653
- externalUserId,
654
- });
655
- }
656
-
657
- return rows;
658
- }
659
-
660
- function clearImportedData(db = getNativeDb()) {
661
- db.exec(`
662
- delete from ai_scores;
663
- delete from tweet_actions;
664
- delete from tweet_account_edges;
665
- delete from tweet_collections;
666
- delete from link_occurrences;
667
- delete from url_expansions;
668
- delete from dm_fts;
669
- delete from tweets_fts;
670
- delete from dm_messages;
671
- delete from dm_conversations;
672
- delete from tweets;
673
- delete from profiles;
674
- delete from accounts;
675
- `);
676
- clearAuthoredSyncCursors(db);
677
- }
678
-
679
- function clearAuthoredSyncCursors(db = getNativeDb(), accountId?: string) {
680
- if (accountId) {
681
- db.prepare("delete from sync_cache where cache_key = ?").run(
682
- `authored:xurl:${accountId}:cursor`,
683
- );
684
- return;
685
- }
686
- db.prepare(
687
- "delete from sync_cache where cache_key like 'authored:xurl:%:cursor'",
688
- ).run();
689
- }
690
-
691
- function clearMentionSyncState(db = getNativeDb()) {
692
- db.prepare(
693
- "delete from sync_cache where cache_key like 'mentions:sync:%'",
694
- ).run();
704
+ function getArchiveFollowRow(wrapper: ArchiveRecord, key: ArchiveFollowKey) {
705
+ const item = asRecord(wrapper[key]);
706
+ const externalUserId = String(item?.accountId ?? "");
707
+ if (!externalUserId) return undefined;
708
+ return {
709
+ profileId: `profile_user_${externalUserId}`,
710
+ externalUserId,
711
+ };
695
712
  }
696
713
 
697
714
  function importArchiveInternalEffect(
@@ -767,47 +784,21 @@ function importArchiveInternalEffect(
767
784
  parseArchiveArray(accountContent)[0] ?? null,
768
785
  parseArchiveArray(profileContent)[0] ?? null,
769
786
  );
770
-
771
- const mentionDirectory = new Map<
772
- string,
773
- { handle?: string; displayName?: string }
774
- >();
775
- const tweetRows: Array<{
776
- id: string;
777
- kind: "home" | "like" | "bookmark";
778
- authorProfileId: string;
779
- text: string;
780
- createdAt: string;
781
- isReplied: number;
782
- replyToId: string | null;
783
- likeCount: number;
784
- mediaCount: number;
785
- bookmarked: number;
786
- liked: number;
787
- entitiesJson: string;
788
- mediaJson: string;
789
- quotedTweetId: string | null;
790
- }> = [];
791
- const collectionRows: Array<{
792
- tweetId: string;
793
- kind: "likes" | "bookmarks";
794
- collectedAt: string | null;
795
- source: string;
796
- rawJson: string;
797
- }> = [];
798
- const tweetRowsById = new Map<string, (typeof tweetRows)[number]>();
799
-
800
- function addTweetRow(row: (typeof tweetRows)[number]) {
801
- const existing = tweetRowsById.get(row.id);
802
- if (existing) {
803
- existing.bookmarked = Math.max(existing.bookmarked, row.bookmarked);
804
- existing.liked = Math.max(existing.liked, row.liked);
805
- if (!existing.text && row.text) existing.text = row.text;
806
- return;
807
- }
808
- tweetRows.push(row);
809
- tweetRowsById.set(row.id, row);
810
- }
787
+ const db = getNativeDb();
788
+ const repository = getImportRepository(db);
789
+ const plan = new ArchiveImportPlan();
790
+ const {
791
+ mentionDirectory,
792
+ tweets: tweetRows,
793
+ collections: collectionRows,
794
+ profiles,
795
+ conversations,
796
+ dmMessages,
797
+ followers: followerRows,
798
+ following: followingRows,
799
+ followerIds,
800
+ followingIds,
801
+ } = plan;
811
802
 
812
803
  if (tweetEntries.length > 0) {
813
804
  onProgress({
@@ -817,10 +808,9 @@ function importArchiveInternalEffect(
817
808
  });
818
809
  }
819
810
  for (const [tweetFileIndex, entry] of tweetEntries.entries()) {
820
- const content = yield* readArchiveEntryEffect(archivePath, entry);
821
- for (const wrapper of parseArchiveArray(content)) {
811
+ yield* processArchiveEntryRecordsEffect(archivePath, entry, (wrapper) => {
822
812
  const tweet = asRecord(wrapper.tweet);
823
- if (!tweet) continue;
813
+ if (!tweet) return;
824
814
 
825
815
  for (const mention of asArray<Record<string, unknown>>(
826
816
  asRecord(tweet.entities)?.user_mentions,
@@ -846,7 +836,7 @@ function importArchiveInternalEffect(
846
836
  });
847
837
  }
848
838
 
849
- addTweetRow({
839
+ plan.addTweet({
850
840
  id: String(tweet.id_str ?? tweet.id),
851
841
  kind: "home",
852
842
  authorProfileId: "profile_me",
@@ -866,7 +856,7 @@ function importArchiveInternalEffect(
866
856
  ? String(tweet.quoted_status_id_str)
867
857
  : null,
868
858
  });
869
- }
859
+ });
870
860
  onProgress({
871
861
  kind: "slice-file",
872
862
  slice: "tweets",
@@ -891,12 +881,11 @@ function importArchiveInternalEffect(
891
881
  }
892
882
  const tweetRowsBeforeNotes = tweetRows.length;
893
883
  for (const [noteFileIndex, entry] of noteTweetEntries.entries()) {
894
- const content = yield* readArchiveEntryEffect(archivePath, entry);
895
- for (const wrapper of parseArchiveArray(content)) {
884
+ yield* processArchiveEntryRecordsEffect(archivePath, entry, (wrapper) => {
896
885
  const noteTweet = asRecord(wrapper.noteTweet);
897
- if (!noteTweet) continue;
886
+ if (!noteTweet) return;
898
887
  const core = asRecord(noteTweet.core);
899
- addTweetRow({
888
+ plan.addTweet({
900
889
  id: String(noteTweet.noteTweetId ?? noteTweet.id ?? randomUUID()),
901
890
  kind: "home",
902
891
  authorProfileId: "profile_me",
@@ -912,7 +901,7 @@ function importArchiveInternalEffect(
912
901
  mediaJson: "[]",
913
902
  quotedTweetId: null,
914
903
  });
915
- }
904
+ });
916
905
  onProgress({
917
906
  kind: "slice-file",
918
907
  slice: "noteTweets",
@@ -929,38 +918,7 @@ function importArchiveInternalEffect(
929
918
  }
930
919
  const authoredTweetCount = tweetRows.length;
931
920
 
932
- type MessageRow = {
933
- id: string;
934
- conversationId: string;
935
- senderProfileId: string;
936
- text: string;
937
- createdAt: string;
938
- direction: "inbound" | "outbound";
939
- mediaCount: number;
940
- };
941
-
942
- const profiles = new Map<
943
- string,
944
- {
945
- id: string;
946
- handle: string;
947
- displayName: string;
948
- bio: string;
949
- followersCount: number;
950
- followingCount: number;
951
- publicMetricsJson: string;
952
- avatarHue: number;
953
- avatarUrl: string | null;
954
- location: string | null;
955
- url: string | null;
956
- verifiedType: string | null;
957
- entitiesJson: string;
958
- rawJson: string;
959
- createdAt: string;
960
- }
961
- >();
962
- type ProfileRow =
963
- typeof profiles extends Map<string, infer Value> ? Value : never;
921
+ type ProfileRow = ArchiveProfileRow;
964
922
  const defaultProfileMetadata = {
965
923
  publicMetricsJson: "{}",
966
924
  location: null,
@@ -969,25 +927,6 @@ function importArchiveInternalEffect(
969
927
  entitiesJson: "{}",
970
928
  rawJson: "{}",
971
929
  };
972
- const conversations = new Map<
973
- string,
974
- {
975
- id: string;
976
- title: string;
977
- accountId: string;
978
- participantProfileId: string;
979
- lastMessageAt: string;
980
- unreadCount: number;
981
- needsReply: number;
982
- }
983
- >();
984
- const dmMessages: MessageRow[] = [];
985
- const followerRows: Array<{ profileId: string; externalUserId: string }> =
986
- [];
987
- const followingRows: Array<{ profileId: string; externalUserId: string }> =
988
- [];
989
- const followerIds = new Set<string>();
990
- const followingIds = new Set<string>();
991
930
  type ExistingProfileRow = {
992
931
  id: string;
993
932
  handle: string;
@@ -1006,18 +945,14 @@ function importArchiveInternalEffect(
1006
945
  created_at: string;
1007
946
  };
1008
947
  const existingProfiles = new Map(
1009
- (
1010
- getNativeDb()
1011
- .prepare(
1012
- `
948
+ repository
949
+ .readRows<ExistingProfileRow>(`
1013
950
  select id, handle, display_name, bio, followers_count, following_count,
1014
951
  public_metrics_json, avatar_hue, avatar_url, location, url,
1015
952
  verified_type, entities_json, raw_json, created_at
1016
953
  from profiles
1017
- `,
1018
- )
1019
- .all() as ExistingProfileRow[]
1020
- ).map((profile) => [profile.id, profile]),
954
+ `)
955
+ .map((profile) => [profile.id, profile]),
1021
956
  );
1022
957
  const existingProfilesByHandle = new Map(
1023
958
  [...existingProfiles.values()].map((profile) => [
@@ -1025,11 +960,13 @@ function importArchiveInternalEffect(
1025
960
  profile,
1026
961
  ]),
1027
962
  );
1028
- const existingPrimaryAccount = getNativeDb()
1029
- .prepare("select handle, external_user_id from accounts where id = ?")
1030
- .get("acct_primary") as
1031
- | { handle: string; external_user_id: string | null }
1032
- | undefined;
963
+ const existingPrimaryAccount = repository.readRow<{
964
+ handle: string;
965
+ external_user_id: string | null;
966
+ }>(
967
+ "select handle, external_user_id from accounts where id = ?",
968
+ "acct_primary",
969
+ );
1033
970
  const profileIdAliases = new Map<string, string>();
1034
971
 
1035
972
  type ArchiveProfileTier =
@@ -1363,13 +1300,12 @@ function importArchiveInternalEffect(
1363
1300
  });
1364
1301
  }
1365
1302
  for (const [dmFileIndex, entry] of dmEntries.entries()) {
1366
- const content = yield* readArchiveEntryEffect(archivePath, entry);
1367
- for (const wrapper of parseArchiveArray(content)) {
1303
+ yield* processArchiveEntryRecordsEffect(archivePath, entry, (wrapper) => {
1368
1304
  const dmConversation = asRecord(wrapper.dmConversation);
1369
- if (!dmConversation) continue;
1305
+ if (!dmConversation) return;
1370
1306
 
1371
1307
  const rawConversationId = String(dmConversation.conversationId ?? "");
1372
- if (!rawConversationId) continue;
1308
+ if (!rawConversationId) return;
1373
1309
  const conversationId =
1374
1310
  resolveArchiveDmConversationId(rawConversationId);
1375
1311
  const conversationIdChanged = conversationId !== rawConversationId;
@@ -1516,18 +1452,18 @@ function importArchiveInternalEffect(
1516
1452
  direction:
1517
1453
  senderId === accountPayload.accountId ? "outbound" : "inbound",
1518
1454
  mediaCount: asArray(messageCreate.mediaUrls).length,
1519
- } satisfies MessageRow;
1455
+ } satisfies ArchiveMessageRow;
1520
1456
  })
1521
1457
  .sort((left, right) =>
1522
1458
  compareIsoTimestamp(left.createdAt, right.createdAt),
1523
1459
  );
1524
1460
 
1525
1461
  if (messageEvents.length === 0) {
1526
- continue;
1462
+ return;
1527
1463
  }
1528
1464
 
1529
1465
  const lastMessage = messageEvents.at(-1);
1530
- if (!lastMessage) continue;
1466
+ if (!lastMessage) return;
1531
1467
 
1532
1468
  dmMessages.push(...messageEvents);
1533
1469
  const resolvedParticipantProfileId =
@@ -1544,7 +1480,7 @@ function importArchiveInternalEffect(
1544
1480
  unreadCount: 0,
1545
1481
  needsReply: lastMessage.direction === "inbound" ? 1 : 0,
1546
1482
  });
1547
- }
1483
+ });
1548
1484
  onProgress({
1549
1485
  kind: "slice-file",
1550
1486
  slice: "directMessages",
@@ -1569,11 +1505,10 @@ function importArchiveInternalEffect(
1569
1505
  }
1570
1506
  let likeCount = 0;
1571
1507
  for (const [likeFileIndex, entry] of likeEntries.entries()) {
1572
- const content = yield* readArchiveEntryEffect(archivePath, entry);
1573
- const likes = parseArchiveArray(content);
1574
- for (const like of likes) {
1508
+ yield* processArchiveEntryRecordsEffect(archivePath, entry, (like) => {
1509
+ likeCount += 1;
1575
1510
  const tweet = extractCollectionTweet(like, "like");
1576
- if (!tweet) continue;
1511
+ if (!tweet) return;
1577
1512
  collectionRows.push({
1578
1513
  tweetId: tweet.id,
1579
1514
  kind: "likes",
@@ -1581,7 +1516,7 @@ function importArchiveInternalEffect(
1581
1516
  source: "archive",
1582
1517
  rawJson: JSON.stringify(like),
1583
1518
  });
1584
- addTweetRow({
1519
+ plan.addTweet({
1585
1520
  id: tweet.id,
1586
1521
  kind: "like",
1587
1522
  authorProfileId: "profile_unknown",
@@ -1597,8 +1532,7 @@ function importArchiveInternalEffect(
1597
1532
  mediaJson: "[]",
1598
1533
  quotedTweetId: null,
1599
1534
  });
1600
- }
1601
- likeCount += likes.length;
1535
+ });
1602
1536
  onProgress({
1603
1537
  kind: "slice-file",
1604
1538
  slice: "likes",
@@ -1619,36 +1553,38 @@ function importArchiveInternalEffect(
1619
1553
  }
1620
1554
  let bookmarkCount = 0;
1621
1555
  for (const [bookmarkFileIndex, entry] of bookmarkEntries.entries()) {
1622
- const content = yield* readArchiveEntryEffect(archivePath, entry);
1623
- const bookmarks = parseArchiveArray(content);
1624
- for (const bookmark of bookmarks) {
1625
- const tweet = extractCollectionTweet(bookmark, "bookmark");
1626
- if (!tweet) continue;
1627
- collectionRows.push({
1628
- tweetId: tweet.id,
1629
- kind: "bookmarks",
1630
- collectedAt: tweet.createdAt,
1631
- source: "archive",
1632
- rawJson: JSON.stringify(bookmark),
1633
- });
1634
- addTweetRow({
1635
- id: tweet.id,
1636
- kind: "bookmark",
1637
- authorProfileId: "profile_unknown",
1638
- text: tweet.text,
1639
- createdAt: tweet.createdAt,
1640
- isReplied: 0,
1641
- replyToId: null,
1642
- likeCount: tweet.likeCount,
1643
- mediaCount: 0,
1644
- bookmarked: 1,
1645
- liked: 0,
1646
- entitiesJson: "{}",
1647
- mediaJson: "[]",
1648
- quotedTweetId: null,
1649
- });
1650
- }
1651
- bookmarkCount += bookmarks.length;
1556
+ yield* processArchiveEntryRecordsEffect(
1557
+ archivePath,
1558
+ entry,
1559
+ (bookmark) => {
1560
+ bookmarkCount += 1;
1561
+ const tweet = extractCollectionTweet(bookmark, "bookmark");
1562
+ if (!tweet) return;
1563
+ collectionRows.push({
1564
+ tweetId: tweet.id,
1565
+ kind: "bookmarks",
1566
+ collectedAt: tweet.createdAt,
1567
+ source: "archive",
1568
+ rawJson: JSON.stringify(bookmark),
1569
+ });
1570
+ plan.addTweet({
1571
+ id: tweet.id,
1572
+ kind: "bookmark",
1573
+ authorProfileId: "profile_unknown",
1574
+ text: tweet.text,
1575
+ createdAt: tweet.createdAt,
1576
+ isReplied: 0,
1577
+ replyToId: null,
1578
+ likeCount: tweet.likeCount,
1579
+ mediaCount: 0,
1580
+ bookmarked: 1,
1581
+ liked: 0,
1582
+ entitiesJson: "{}",
1583
+ mediaJson: "[]",
1584
+ quotedTweetId: null,
1585
+ });
1586
+ },
1587
+ );
1652
1588
  onProgress({
1653
1589
  kind: "slice-file",
1654
1590
  slice: "bookmarks",
@@ -1686,12 +1622,12 @@ function importArchiveInternalEffect(
1686
1622
  });
1687
1623
  }
1688
1624
  for (const [followerFileIndex, entry] of followerEntries.entries()) {
1689
- const content = yield* readArchiveEntryEffect(archivePath, entry);
1690
- for (const row of getArchiveFollowRows(content, "follower")) {
1691
- if (followerIds.has(row.externalUserId)) continue;
1625
+ yield* processArchiveEntryRecordsEffect(archivePath, entry, (wrapper) => {
1626
+ const row = getArchiveFollowRow(wrapper, "follower");
1627
+ if (!row || followerIds.has(row.externalUserId)) return;
1692
1628
  followerIds.add(row.externalUserId);
1693
1629
  followerRows.push(row);
1694
- }
1630
+ });
1695
1631
  onProgress({
1696
1632
  kind: "slice-file",
1697
1633
  slice: "followers",
@@ -1715,12 +1651,12 @@ function importArchiveInternalEffect(
1715
1651
  });
1716
1652
  }
1717
1653
  for (const [followingFileIndex, entry] of followingEntries.entries()) {
1718
- const content = yield* readArchiveEntryEffect(archivePath, entry);
1719
- for (const row of getArchiveFollowRows(content, "following")) {
1720
- if (followingIds.has(row.externalUserId)) continue;
1654
+ yield* processArchiveEntryRecordsEffect(archivePath, entry, (wrapper) => {
1655
+ const row = getArchiveFollowRow(wrapper, "following");
1656
+ if (!row || followingIds.has(row.externalUserId)) return;
1721
1657
  followingIds.add(row.externalUserId);
1722
1658
  followingRows.push(row);
1723
- }
1659
+ });
1724
1660
  onProgress({
1725
1661
  kind: "slice-file",
1726
1662
  slice: "following",
@@ -1747,25 +1683,21 @@ function importArchiveInternalEffect(
1747
1683
  if (includeFollowing && followingEntries.length === 0) {
1748
1684
  clearedFollowDirections.add("following");
1749
1685
  }
1750
- const retainedFollowProfiles = getNativeDb()
1751
- .prepare(
1752
- `
1753
- select direction, profile_id, external_user_id, source, null as snapshot_id, null as snapshot_source
1754
- from follow_edges
1755
- union
1756
- select ev.direction, ev.profile_id, ev.external_user_id, null as source, ev.snapshot_id, snap.source as snapshot_source
1757
- from follow_events ev
1758
- left join follow_snapshots snap on snap.id = ev.snapshot_id
1759
- `,
1760
- )
1761
- .all() as Array<{
1686
+ const retainedFollowProfiles = repository.readRows<{
1762
1687
  direction: ArchiveFollowDirection;
1763
1688
  profile_id: string;
1764
1689
  external_user_id: string;
1765
1690
  source: string | null;
1766
1691
  snapshot_id: string | null;
1767
1692
  snapshot_source: string | null;
1768
- }>;
1693
+ }>(`
1694
+ select direction, profile_id, external_user_id, source, null as snapshot_id, null as snapshot_source
1695
+ from follow_edges
1696
+ union
1697
+ select ev.direction, ev.profile_id, ev.external_user_id, null as source, ev.snapshot_id, snap.source as snapshot_source
1698
+ from follow_events ev
1699
+ left join follow_snapshots snap on snap.id = ev.snapshot_id
1700
+ `);
1769
1701
  for (const row of retainedFollowProfiles) {
1770
1702
  const isClearedArchiveRow =
1771
1703
  clearedFollowDirections.has(row.direction) &&
@@ -1803,7 +1735,6 @@ function importArchiveInternalEffect(
1803
1735
  );
1804
1736
  }
1805
1737
 
1806
- const db = getNativeDb();
1807
1738
  const insertAccount = db.prepare(`
1808
1739
  insert into accounts (id, name, handle, external_user_id, transport, is_default, created_at)
1809
1740
  values (?, ?, ?, ?, ?, 1, ?)
@@ -2451,15 +2382,15 @@ function importArchiveInternalEffect(
2451
2382
  onProgress({ kind: "write-progress", phase, processed, total });
2452
2383
  }
2453
2384
  }
2454
- db.transaction(() => {
2385
+ yield* databaseWriteEffect(() => {
2455
2386
  if (!selection) {
2456
- clearImportedData(db);
2457
- clearMentionSyncState(db);
2387
+ repository.clearArchiveImport();
2388
+ repository.clearMentionSyncState();
2458
2389
  }
2459
2390
 
2460
2391
  if (selection) {
2461
2392
  if (includeTweets) {
2462
- clearAuthoredSyncCursors(db, "acct_primary");
2393
+ repository.clearAuthoredSyncCursors("acct_primary");
2463
2394
  demoteSelectedArchiveTweetsWithCollections.run(
2464
2395
  "acct_primary",
2465
2396
  "acct_primary",
@@ -2704,7 +2635,7 @@ function importArchiveInternalEffect(
2704
2635
  } else if (includeFollowing) {
2705
2636
  clearArchiveFollowRows("following");
2706
2637
  }
2707
- })();
2638
+ }, db);
2708
2639
  onProgress({ kind: "done" });
2709
2640
 
2710
2641
  return {