birdclaw 0.8.1 → 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 (103) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +2 -4
  3. package/scripts/browser-perf.mjs +27 -0
  4. package/src/cli/command-context.ts +17 -0
  5. package/src/cli/register-analysis.ts +500 -0
  6. package/src/cli/register-compose.ts +40 -0
  7. package/src/cli/register-graph.ts +132 -0
  8. package/src/cli/register-inbox.ts +41 -0
  9. package/src/cli/register-storage.ts +106 -0
  10. package/src/cli.ts +30 -750
  11. package/src/components/AccountSwitcher.tsx +7 -15
  12. package/src/components/AvatarChip.tsx +1 -1
  13. package/src/components/AvatarPreload.ts +149 -0
  14. package/src/components/ConversationThread.tsx +4 -0
  15. package/src/components/EmbeddedTweetCard.tsx +4 -0
  16. package/src/components/FloatingPreview.tsx +382 -0
  17. package/src/components/MarkdownCitations.tsx +680 -0
  18. package/src/components/MarkdownViewer.tsx +7 -715
  19. package/src/components/ProfileAnalysisClient.ts +191 -0
  20. package/src/components/ProfileAnalysisStream.tsx +16 -185
  21. package/src/components/ProfilePreview.tsx +41 -81
  22. package/src/components/TimelineCard.tsx +18 -2
  23. package/src/components/TweetArticleCard.tsx +66 -0
  24. package/src/components/TweetRichText.tsx +33 -2
  25. package/src/components/links-controller.ts +162 -0
  26. package/src/components/links-model.ts +198 -0
  27. package/src/components/network-map-controller.ts +84 -0
  28. package/src/components/network-map-model.ts +255 -0
  29. package/src/components/useDebouncedValue.ts +12 -0
  30. package/src/components/useTimelineRouteData.ts +142 -170
  31. package/src/lib/analysis-runtime.ts +238 -0
  32. package/src/lib/api-client.ts +16 -100
  33. package/src/lib/api-contracts.ts +328 -0
  34. package/src/lib/archive-finder.ts +38 -0
  35. package/src/lib/archive-import-plan.ts +102 -0
  36. package/src/lib/archive-import.ts +170 -239
  37. package/src/lib/authored-live.ts +77 -182
  38. package/src/lib/backup.ts +335 -424
  39. package/src/lib/bird.ts +32 -1
  40. package/src/lib/blocks-write.ts +30 -26
  41. package/src/lib/blocks.ts +18 -20
  42. package/src/lib/database-metrics.ts +88 -0
  43. package/src/lib/database-migrations.ts +34 -0
  44. package/src/lib/database-schema.ts +312 -0
  45. package/src/lib/database-writer.ts +69 -0
  46. package/src/lib/db.ts +141 -334
  47. package/src/lib/dm-read-model.ts +533 -0
  48. package/src/lib/dms-live.ts +34 -97
  49. package/src/lib/follow-graph.ts +17 -27
  50. package/src/lib/import-repository.ts +138 -0
  51. package/src/lib/inbox.ts +2 -1
  52. package/src/lib/live-sync-engine.ts +209 -0
  53. package/src/lib/live-transport-gateway.ts +128 -0
  54. package/src/lib/mention-threads-live.ts +90 -176
  55. package/src/lib/mentions-export.ts +1 -1
  56. package/src/lib/mentions-live.ts +57 -225
  57. package/src/lib/moderation-target.ts +15 -4
  58. package/src/lib/moderation-write.ts +1 -1
  59. package/src/lib/mutes-write.ts +30 -26
  60. package/src/lib/openai-response-runtime.ts +251 -0
  61. package/src/lib/paginated-sync.ts +93 -0
  62. package/src/lib/period-digest.ts +116 -304
  63. package/src/lib/profile-analysis.ts +37 -111
  64. package/src/lib/queries.ts +6 -2380
  65. package/src/lib/query-actions.ts +437 -0
  66. package/src/lib/query-client.tsx +47 -0
  67. package/src/lib/query-read-model-shared.ts +52 -0
  68. package/src/lib/query-read-models.ts +5 -0
  69. package/src/lib/query-resource.ts +41 -0
  70. package/src/lib/query-status.ts +164 -0
  71. package/src/lib/research.ts +1 -1
  72. package/src/lib/runtime-services.ts +20 -0
  73. package/src/lib/search-discussion.ts +75 -279
  74. package/src/lib/server-runtime-services.ts +30 -0
  75. package/src/lib/sqlite.ts +53 -14
  76. package/src/lib/streaming-ingestion.ts +240 -0
  77. package/src/lib/sync-cache.ts +6 -1
  78. package/src/lib/sync-plan.ts +175 -0
  79. package/src/lib/timeline-collections-live.ts +83 -256
  80. package/src/lib/timeline-live.ts +86 -235
  81. package/src/lib/timeline-read-model.ts +1191 -0
  82. package/src/lib/tweet-render.ts +78 -0
  83. package/src/lib/tweet-repository.ts +156 -0
  84. package/src/lib/tweet-search-live.ts +63 -166
  85. package/src/lib/types.ts +8 -0
  86. package/src/lib/ui.ts +1 -1
  87. package/src/lib/web-sync.ts +67 -50
  88. package/src/lib/whois.ts +2 -1
  89. package/src/router.tsx +1 -1
  90. package/src/routes/__root.tsx +11 -21
  91. package/src/routes/api/action.tsx +1 -1
  92. package/src/routes/api/conversation.tsx +1 -1
  93. package/src/routes/api/query.tsx +32 -26
  94. package/src/routes/api/status.tsx +6 -2
  95. package/src/routes/api/sync.tsx +5 -2
  96. package/src/routes/blocks.tsx +97 -131
  97. package/src/routes/data-sources.tsx +17 -25
  98. package/src/routes/dms.tsx +168 -167
  99. package/src/routes/inbox.tsx +63 -57
  100. package/src/routes/links.tsx +31 -329
  101. package/src/routes/network-map.tsx +41 -344
  102. package/src/routes/rate-limits.tsx +17 -21
  103. package/vite.config.ts +0 -2
@@ -1,27 +1,26 @@
1
1
  import type { Database } from "./sqlite";
2
2
  import { Effect } from "effect";
3
- import {
4
- getAuthenticatedBirdAccountEffect,
5
- listMentionsViaBirdEffect,
6
- } from "./bird";
7
3
  import type { MentionsDataSource } from "./config";
4
+ import { databaseWriteEffect } from "./database-writer";
8
5
  import { getNativeDb } from "./db";
9
- import { runEffectPromise, tryPromise } from "./effect-runtime";
10
- import { buildMediaJsonFromIncludes, countTweetMedia } from "./media-includes";
6
+ import { runEffectPromise } from "./effect-runtime";
7
+ import { liveTransportGateway } from "./live-transport-gateway";
8
+ import {
9
+ assertLiveAccountMatches,
10
+ resolveLiveSyncAccount,
11
+ type LiveSyncAccount,
12
+ } from "./live-sync-engine";
11
13
  import { serializeMentionItemsAsXurlCompatible } from "./mentions-export";
12
- import { listTimelineItems } from "./queries";
14
+ import { listTimelineItems } from "./timeline-read-model";
13
15
  import { deleteSyncCache, readSyncCache, writeSyncCache } from "./sync-cache";
14
16
  import type {
15
17
  ReplyFilter,
16
- TweetEntities,
17
18
  XurlMentionData,
18
19
  XurlMentionsResponse,
19
20
  XurlMediaItem,
20
21
  XurlMentionUser,
21
22
  } from "./types";
22
- import { upsertTweetAccountEdge } from "./tweet-account-edges";
23
- import { ensureStubProfileForXUser, upsertProfileFromXUser } from "./x-profile";
24
- import { listMentionsViaXurl, lookupUsersByHandles } from "./xurl";
23
+ import { ingestTweetPayload } from "./tweet-repository";
25
24
 
26
25
  export const DEFAULT_MENTIONS_CACHE_TTL_MS = 2 * 60_000;
27
26
  const MIN_XURL_MENTIONS_LIMIT = 5;
@@ -430,71 +429,21 @@ function writeMentionHighWaterId(
430
429
  writeSyncCache(getMentionHighWaterKey({ mode, accountId }), { sinceId }, db);
431
430
  }
432
431
 
433
- function resolveAccount(db: Database, accountId?: string) {
434
- const row = accountId
435
- ? (db
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)
442
- : (db
443
- .prepare(
444
- `
445
- select id, handle, external_user_id
446
- from accounts
447
- order by is_default desc, created_at asc
448
- limit 1
449
- `,
450
- )
451
- .get() as
452
- | { id: string; handle: string; external_user_id: string | null }
453
- | undefined);
454
-
455
- if (!row) {
456
- throw new Error(`Unknown account: ${accountId ?? "default"}`);
457
- }
458
-
459
- return {
460
- accountId: row.id,
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,
467
- };
468
- }
469
-
470
- function verifyBirdAccountMatchesEffect({
471
- accountId,
472
- username,
473
- externalUserId,
474
- }: ReturnType<typeof resolveAccount>) {
432
+ function verifyBirdAccountMatchesEffect(account: LiveSyncAccount) {
475
433
  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
- }
434
+ const authenticated =
435
+ yield* liveTransportGateway.bird.getAuthenticatedAccount();
436
+ return yield* Effect.try({
437
+ try: () =>
438
+ assertLiveAccountMatches({
439
+ source: "bird",
440
+ account,
441
+ liveUsername: authenticated.username,
442
+ liveExternalUserId: authenticated.id,
443
+ }),
444
+ catch: (error) =>
445
+ error instanceof Error ? error : new Error(String(error)),
446
+ });
498
447
  });
499
448
  }
500
449
 
@@ -520,139 +469,20 @@ function findNewestArchiveMentionId(db: Database, accountId: string) {
520
469
  return row?.id;
521
470
  }
522
471
 
523
- function toLocalEntities(tweet: XurlMentionData): TweetEntities {
524
- const raw = tweet.entities;
525
- if (!raw || typeof raw !== "object") {
526
- return {};
527
- }
528
-
529
- const entities = raw as Record<string, unknown>;
530
- const rawMentions = Array.isArray(entities.mentions) ? entities.mentions : [];
531
- const rawUrls = Array.isArray(entities.urls) ? entities.urls : [];
532
-
533
- return {
534
- ...(rawMentions.length
535
- ? {
536
- mentions: rawMentions.map((mention) => {
537
- const value =
538
- mention && typeof mention === "object"
539
- ? (mention as Record<string, unknown>)
540
- : {};
541
- return {
542
- username: String(value.username ?? ""),
543
- id: typeof value.id === "string" ? String(value.id) : undefined,
544
- start: Number(value.start ?? 0),
545
- end: Number(value.end ?? 0),
546
- };
547
- }),
548
- }
549
- : {}),
550
- ...(rawUrls.length
551
- ? {
552
- urls: rawUrls.map((url) => {
553
- const value =
554
- url && typeof url === "object"
555
- ? (url as Record<string, unknown>)
556
- : {};
557
- return {
558
- url: String(value.url ?? ""),
559
- expandedUrl: String(value.expanded_url ?? value.url ?? ""),
560
- displayUrl: String(
561
- value.display_url ?? value.expanded_url ?? value.url ?? "",
562
- ),
563
- start: Number(value.start ?? 0),
564
- end: Number(value.end ?? 0),
565
- };
566
- }),
567
- }
568
- : {}),
569
- };
570
- }
571
-
572
- function replaceTweetFts(db: Database, tweetId: string, text: string) {
573
- db.prepare("delete from tweets_fts where tweet_id = ?").run(tweetId);
574
- db.prepare("insert into tweets_fts (tweet_id, text) values (?, ?)").run(
575
- tweetId,
576
- text,
577
- );
578
- }
579
-
580
472
  function mergeMentionsIntoLocalStore(
581
473
  db: Database,
582
474
  accountId: string,
583
475
  payload: XurlMentionsResponse,
584
476
  source: MentionsDataSource,
585
477
  ) {
586
- const usersById = new Map(
587
- (payload.includes?.users ?? []).map((user) => [user.id, user]),
588
- );
589
- const upsertTweet = db.prepare(
590
- `
591
- insert into tweets (
592
- id, account_id, author_profile_id, kind, text, created_at,
593
- is_replied, reply_to_id, like_count, media_count, bookmarked, liked,
594
- entities_json, media_json, quoted_tweet_id
595
- ) values (?, ?, ?, 'mention', ?, ?, 0, null, ?, ?, 0, 0, ?, ?, null)
596
- on conflict(id) do update set
597
- account_id = tweets.account_id,
598
- author_profile_id = excluded.author_profile_id,
599
- kind = case
600
- when tweets.kind in ('authored', 'home', 'mention') then tweets.kind
601
- when excluded.kind in ('authored', 'home', 'mention') then excluded.kind
602
- else excluded.kind
603
- end,
604
- text = excluded.text,
605
- created_at = excluded.created_at,
606
- like_count = excluded.like_count,
607
- media_count = max(tweets.media_count, excluded.media_count),
608
- entities_json = excluded.entities_json,
609
- media_json = case
610
- when excluded.media_json not in ('', '[]', 'null') then excluded.media_json
611
- else tweets.media_json
612
- end,
613
- is_replied = max(tweets.is_replied, excluded.is_replied),
614
- bookmarked = max(tweets.bookmarked, excluded.bookmarked),
615
- liked = max(tweets.liked, excluded.liked)
616
- `,
617
- );
618
-
619
- const writePayload = db.transaction(() => {
620
- const seenAt = new Date().toISOString();
621
- for (const tweet of payload.data) {
622
- const author =
623
- usersById.get(tweet.author_id) ??
624
- ({
625
- id: tweet.author_id,
626
- username: `user_${tweet.author_id}`,
627
- name: `user_${tweet.author_id}`,
628
- } as const);
629
- const profile = usersById.has(tweet.author_id)
630
- ? upsertProfileFromXUser(db, author)
631
- : ensureStubProfileForXUser(db, tweet.author_id);
632
- upsertTweet.run(
633
- tweet.id,
634
- accountId,
635
- profile.profile.id,
636
- tweet.text,
637
- tweet.created_at,
638
- Number(tweet.public_metrics?.like_count ?? 0),
639
- countTweetMedia(tweet),
640
- JSON.stringify(toLocalEntities(tweet)),
641
- buildMediaJsonFromIncludes(tweet, payload.includes?.media),
642
- );
643
- upsertTweetAccountEdge(db, {
644
- accountId,
645
- tweetId: tweet.id,
646
- kind: "mention",
647
- source,
648
- seenAt,
649
- rawJson: JSON.stringify(tweet),
650
- });
651
- replaceTweetFts(db, tweet.id, tweet.text);
652
- }
478
+ ingestTweetPayload(db, {
479
+ accountId,
480
+ payload,
481
+ kind: "mention",
482
+ edgeKind: "mention",
483
+ replaceSecondaryKind: true,
484
+ source,
653
485
  });
654
-
655
- writePayload();
656
486
  }
657
487
 
658
488
  function shouldReturnFilteredLocalPayload({
@@ -755,7 +585,7 @@ function fetchMentionsViaXurlEffect({
755
585
  startTime,
756
586
  onProgress,
757
587
  }: {
758
- resolvedAccount: ReturnType<typeof resolveAccount>;
588
+ resolvedAccount: LiveSyncAccount;
759
589
  limit: number;
760
590
  all: boolean;
761
591
  parsedMaxPages: number | null;
@@ -767,9 +597,9 @@ function fetchMentionsViaXurlEffect({
767
597
  return Effect.gen(function* () {
768
598
  const accountUserId =
769
599
  resolvedAccount.externalUserId ??
770
- (yield* tryPromise(() =>
771
- lookupUsersByHandles([resolvedAccount.username]),
772
- ).pipe(Effect.map((users) => users[0]?.id)));
600
+ (yield* liveTransportGateway.xurl
601
+ .lookupUsersByHandles([resolvedAccount.username])
602
+ .pipe(Effect.map((users) => users[0]?.id)));
773
603
  if (!accountUserId) {
774
604
  return yield* Effect.fail(
775
605
  new Error(
@@ -782,16 +612,14 @@ function fetchMentionsViaXurlEffect({
782
612
  let nextToken: string | undefined = startPaginationToken;
783
613
  let pageCount = 0;
784
614
  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
- );
615
+ const payload = yield* liveTransportGateway.xurl.listMentions({
616
+ maxResults: limit,
617
+ username: resolvedAccount.username,
618
+ userId: String(accountUserId),
619
+ paginationToken: nextToken,
620
+ ...(sinceId ? { sinceId } : {}),
621
+ ...(startTime ? { startTime } : {}),
622
+ });
795
623
  pages.push(payload);
796
624
  const metaNextToken =
797
625
  typeof payload.meta?.next_token === "string"
@@ -826,7 +654,7 @@ function fetchMentionsViaXurlEffect({
826
654
  }
827
655
 
828
656
  function fetchMentionsViaBirdEffect({ limit }: { limit: number }) {
829
- return listMentionsViaBirdEffect({ maxResults: limit });
657
+ return liveTransportGateway.bird.listMentions({ maxResults: limit });
830
658
  }
831
659
 
832
660
  function isMaxPagesPartial({
@@ -876,7 +704,9 @@ export function syncMentionsEffect({
876
704
  (parsedMaxPages !== null ||
877
705
  Boolean(explicitSinceId || explicitStartTime));
878
706
  const db = yield* trySync(() => getNativeDb());
879
- const resolvedAccount = yield* trySync(() => resolveAccount(db, account));
707
+ const resolvedAccount = yield* trySync(() =>
708
+ resolveLiveSyncAccount(db, account),
709
+ );
880
710
  const cursorShape: MentionScanShape = {
881
711
  endpoint: "mentions",
882
712
  mode: primaryMode,
@@ -966,9 +796,9 @@ export function syncMentionsEffect({
966
796
  cached &&
967
797
  cacheAgeMs <= ttlMs
968
798
  ) {
969
- yield* trySync(() =>
799
+ yield* databaseWriteEffect((writeDb) =>
970
800
  mergeMentionsIntoLocalStore(
971
- db,
801
+ writeDb,
972
802
  resolvedAccount.accountId,
973
803
  cached.value,
974
804
  primaryMode,
@@ -1046,9 +876,9 @@ export function syncMentionsEffect({
1046
876
  }),
1047
877
  );
1048
878
  }
1049
- yield* trySync(() =>
879
+ yield* databaseWriteEffect((writeDb) =>
1050
880
  mergeMentionsIntoLocalStore(
1051
- db,
881
+ writeDb,
1052
882
  resolvedAccount.accountId,
1053
883
  payload,
1054
884
  source,
@@ -1157,7 +987,9 @@ function exportMentionsViaCachedLiveSourceEffect({
1157
987
  const fetchAll = primaryMode === "xurl" && (all || parsedMaxPages !== null);
1158
988
 
1159
989
  const db = yield* trySync(() => getNativeDb());
1160
- const resolvedAccount = yield* trySync(() => resolveAccount(db, account));
990
+ const resolvedAccount = yield* trySync(() =>
991
+ resolveLiveSyncAccount(db, account),
992
+ );
1161
993
  const cacheKey = getMentionsFetchModeKey({
1162
994
  scope: "export",
1163
995
  mode,
@@ -1215,14 +1047,14 @@ function exportMentionsViaCachedLiveSourceEffect({
1215
1047
  );
1216
1048
  }),
1217
1049
  Effect.flatMap((payload) =>
1218
- trySync(() => {
1050
+ databaseWriteEffect((writeDb) => {
1219
1051
  mergeMentionsIntoLocalStore(
1220
- db,
1052
+ writeDb,
1221
1053
  resolvedAccount.accountId,
1222
1054
  payload,
1223
1055
  source,
1224
1056
  );
1225
- writeSyncCache(cacheKey, payload, db);
1057
+ writeSyncCache(cacheKey, payload, writeDb);
1226
1058
  return readFilteredOrRaw(payload);
1227
1059
  }),
1228
1060
  ),
@@ -2,6 +2,7 @@ import type { Database } from "./sqlite";
2
2
  import { Effect } from "effect";
3
3
  import { lookupProfileViaBird } from "./bird-actions";
4
4
  import { getNativeDb } from "./db";
5
+ import { databaseWriteEffect } from "./database-writer";
5
6
  import { runEffectPromise, tryPromise } from "./effect-runtime";
6
7
  import type { ProfileRecord, XurlMentionUser } from "./types";
7
8
  import { getExternalUserId, upsertProfileFromXUser } from "./x-profile";
@@ -105,9 +106,10 @@ export function resolveLocalProfile(
105
106
 
106
107
  export function resolveProfileEffect(
107
108
  query: string,
109
+ providedDb?: Database,
108
110
  ): Effect.Effect<ResolvedModerationProfile, unknown> {
109
111
  return Effect.gen(function* () {
110
- const db = yield* trySync(() => getNativeDb());
112
+ const db = providedDb ?? (yield* trySync(() => getNativeDb()));
111
113
  const normalizedQuery = normalizeProfileQuery(query);
112
114
  if (!normalizedQuery) {
113
115
  return yield* Effect.fail(new Error("Missing profile handle or id"));
@@ -180,7 +182,10 @@ export function resolveProfileEffect(
180
182
  }
181
183
 
182
184
  if (local) {
183
- return yield* trySync(() => upsertProfileFromXUser(db, user));
185
+ return yield* databaseWriteEffect(
186
+ (writeDb) => upsertProfileFromXUser(writeDb, user),
187
+ db,
188
+ );
184
189
  }
185
190
 
186
191
  const username = String(user.username ?? "").replace(/^@/, "");
@@ -189,11 +194,17 @@ export function resolveProfileEffect(
189
194
  resolveLocalProfile(db, username),
190
195
  );
191
196
  if (localByHandle) {
192
- return yield* trySync(() => upsertProfileFromXUser(db, user));
197
+ return yield* databaseWriteEffect(
198
+ (writeDb) => upsertProfileFromXUser(writeDb, user),
199
+ db,
200
+ );
193
201
  }
194
202
  }
195
203
 
196
- return yield* trySync(() => upsertProfileFromXUser(db, user));
204
+ return yield* databaseWriteEffect(
205
+ (writeDb) => upsertProfileFromXUser(writeDb, user),
206
+ db,
207
+ );
197
208
  });
198
209
  }
199
210
 
@@ -53,7 +53,7 @@ export function resolveModerationTargetEffect({
53
53
  return yield* Effect.fail(new Error(selfActionError));
54
54
  }
55
55
 
56
- const resolved = yield* resolveProfileEffect(query);
56
+ const resolved = yield* resolveProfileEffect(query, db);
57
57
  const account = yield* trySync(
58
58
  () =>
59
59
  db
@@ -1,5 +1,6 @@
1
1
  import { Effect } from "effect";
2
2
  import { runModerationAction } from "./actions-transport";
3
+ import { databaseWriteEffect } from "./database-writer";
3
4
  import { runEffectPromise, tryPromise } from "./effect-runtime";
4
5
  import {
5
6
  deleteModerationRow,
@@ -8,14 +9,6 @@ import {
8
9
  writeModerationRow,
9
10
  } from "./moderation-write";
10
11
 
11
- function trySync<T>(try_: () => T) {
12
- return Effect.try({
13
- try: try_,
14
- catch: (error) =>
15
- error instanceof Error ? error : new Error(String(error)),
16
- });
17
- }
18
-
19
12
  export function addMuteEffect(
20
13
  accountId: string,
21
14
  query: string,
@@ -49,14 +42,16 @@ export function addMuteEffect(
49
42
  }
50
43
 
51
44
  const mutedAt = new Date().toISOString();
52
- yield* trySync(() =>
53
- writeModerationRow(
54
- db,
55
- "mutes",
56
- resolvedAccountId,
57
- resolved.profile.id,
58
- mutedAt,
59
- ),
45
+ yield* databaseWriteEffect(
46
+ (writeDb) =>
47
+ writeModerationRow(
48
+ writeDb,
49
+ "mutes",
50
+ resolvedAccountId,
51
+ resolved.profile.id,
52
+ mutedAt,
53
+ ),
54
+ db,
60
55
  );
61
56
 
62
57
  return {
@@ -88,14 +83,16 @@ export function recordMuteEffect(accountId: string, query: string) {
88
83
  });
89
84
 
90
85
  const mutedAt = new Date().toISOString();
91
- yield* trySync(() =>
92
- writeModerationRow(
93
- db,
94
- "mutes",
95
- resolvedAccountId,
96
- resolved.profile.id,
97
- mutedAt,
98
- ),
86
+ yield* databaseWriteEffect(
87
+ (writeDb) =>
88
+ writeModerationRow(
89
+ writeDb,
90
+ "mutes",
91
+ resolvedAccountId,
92
+ resolved.profile.id,
93
+ mutedAt,
94
+ ),
95
+ db,
99
96
  );
100
97
 
101
98
  return {
@@ -144,8 +141,15 @@ export function removeMuteEffect(
144
141
  };
145
142
  }
146
143
 
147
- yield* trySync(() =>
148
- deleteModerationRow(db, "mutes", resolvedAccountId, resolved.profile.id),
144
+ yield* databaseWriteEffect(
145
+ (writeDb) =>
146
+ deleteModerationRow(
147
+ writeDb,
148
+ "mutes",
149
+ resolvedAccountId,
150
+ resolved.profile.id,
151
+ ),
152
+ db,
149
153
  );
150
154
 
151
155
  return {