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.
- package/CHANGELOG.md +25 -0
- package/package.json +2 -4
- package/scripts/browser-perf.mjs +27 -0
- package/src/cli/command-context.ts +17 -0
- package/src/cli/register-analysis.ts +500 -0
- package/src/cli/register-compose.ts +40 -0
- package/src/cli/register-graph.ts +132 -0
- package/src/cli/register-inbox.ts +41 -0
- package/src/cli/register-storage.ts +106 -0
- package/src/cli.ts +30 -750
- package/src/components/AccountSwitcher.tsx +7 -15
- package/src/components/AvatarChip.tsx +1 -1
- package/src/components/AvatarPreload.ts +149 -0
- package/src/components/ConversationThread.tsx +4 -0
- package/src/components/EmbeddedTweetCard.tsx +4 -0
- package/src/components/FloatingPreview.tsx +382 -0
- package/src/components/MarkdownCitations.tsx +680 -0
- package/src/components/MarkdownViewer.tsx +7 -715
- package/src/components/ProfileAnalysisClient.ts +191 -0
- package/src/components/ProfileAnalysisStream.tsx +16 -185
- package/src/components/ProfilePreview.tsx +41 -81
- package/src/components/TimelineCard.tsx +18 -2
- package/src/components/TweetArticleCard.tsx +66 -0
- package/src/components/TweetRichText.tsx +33 -2
- package/src/components/links-controller.ts +162 -0
- package/src/components/links-model.ts +198 -0
- package/src/components/network-map-controller.ts +84 -0
- package/src/components/network-map-model.ts +255 -0
- package/src/components/useDebouncedValue.ts +12 -0
- package/src/components/useTimelineRouteData.ts +142 -170
- package/src/lib/analysis-runtime.ts +238 -0
- package/src/lib/api-client.ts +16 -100
- package/src/lib/api-contracts.ts +328 -0
- package/src/lib/archive-finder.ts +38 -0
- package/src/lib/archive-import-plan.ts +102 -0
- package/src/lib/archive-import.ts +170 -239
- package/src/lib/authored-live.ts +77 -182
- package/src/lib/backup.ts +335 -424
- package/src/lib/bird.ts +32 -1
- package/src/lib/blocks-write.ts +30 -26
- package/src/lib/blocks.ts +18 -20
- package/src/lib/database-metrics.ts +88 -0
- package/src/lib/database-migrations.ts +34 -0
- package/src/lib/database-schema.ts +312 -0
- package/src/lib/database-writer.ts +69 -0
- package/src/lib/db.ts +141 -334
- package/src/lib/dm-read-model.ts +533 -0
- package/src/lib/dms-live.ts +34 -97
- package/src/lib/follow-graph.ts +17 -27
- package/src/lib/import-repository.ts +138 -0
- package/src/lib/inbox.ts +2 -1
- package/src/lib/live-sync-engine.ts +209 -0
- package/src/lib/live-transport-gateway.ts +128 -0
- package/src/lib/mention-threads-live.ts +90 -176
- package/src/lib/mentions-export.ts +1 -1
- package/src/lib/mentions-live.ts +57 -225
- package/src/lib/moderation-target.ts +15 -4
- package/src/lib/moderation-write.ts +1 -1
- package/src/lib/mutes-write.ts +30 -26
- package/src/lib/openai-response-runtime.ts +251 -0
- package/src/lib/paginated-sync.ts +93 -0
- package/src/lib/period-digest.ts +116 -304
- package/src/lib/profile-analysis.ts +37 -111
- package/src/lib/queries.ts +6 -2380
- package/src/lib/query-actions.ts +437 -0
- package/src/lib/query-client.tsx +47 -0
- package/src/lib/query-read-model-shared.ts +52 -0
- package/src/lib/query-read-models.ts +5 -0
- package/src/lib/query-resource.ts +41 -0
- package/src/lib/query-status.ts +164 -0
- package/src/lib/research.ts +1 -1
- package/src/lib/runtime-services.ts +20 -0
- package/src/lib/search-discussion.ts +75 -279
- package/src/lib/server-runtime-services.ts +30 -0
- package/src/lib/sqlite.ts +53 -14
- package/src/lib/streaming-ingestion.ts +240 -0
- package/src/lib/sync-cache.ts +6 -1
- package/src/lib/sync-plan.ts +175 -0
- package/src/lib/timeline-collections-live.ts +83 -256
- package/src/lib/timeline-live.ts +86 -235
- package/src/lib/timeline-read-model.ts +1191 -0
- package/src/lib/tweet-render.ts +78 -0
- package/src/lib/tweet-repository.ts +156 -0
- package/src/lib/tweet-search-live.ts +63 -166
- package/src/lib/types.ts +8 -0
- package/src/lib/ui.ts +1 -1
- package/src/lib/web-sync.ts +67 -50
- package/src/lib/whois.ts +2 -1
- package/src/router.tsx +1 -1
- package/src/routes/__root.tsx +11 -21
- package/src/routes/api/action.tsx +1 -1
- package/src/routes/api/conversation.tsx +1 -1
- package/src/routes/api/query.tsx +32 -26
- package/src/routes/api/status.tsx +6 -2
- package/src/routes/api/sync.tsx +5 -2
- package/src/routes/blocks.tsx +97 -131
- package/src/routes/data-sources.tsx +17 -25
- package/src/routes/dms.tsx +168 -167
- package/src/routes/inbox.tsx +63 -57
- package/src/routes/links.tsx +31 -329
- package/src/routes/network-map.tsx +41 -344
- package/src/routes/rate-limits.tsx +17 -21
- package/vite.config.ts +0 -2
package/src/lib/db.ts
CHANGED
|
@@ -1,322 +1,25 @@
|
|
|
1
|
-
import NativeSqliteDatabase, {
|
|
1
|
+
import NativeSqliteDatabase, {
|
|
2
|
+
type Database,
|
|
3
|
+
SQLITE_BUSY_TIMEOUT_MS,
|
|
4
|
+
} from "./sqlite";
|
|
2
5
|
import { Kysely, SqliteDialect } from "kysely";
|
|
3
6
|
import { ensureBirdclawDirs, getBirdclawPaths } from "./config";
|
|
7
|
+
import {
|
|
8
|
+
type DatabaseMigration,
|
|
9
|
+
runDatabaseMigrations,
|
|
10
|
+
} from "./database-migrations";
|
|
4
11
|
import { seedDemoData } from "./seed";
|
|
12
|
+
import {
|
|
13
|
+
type DatabaseConnectionRole,
|
|
14
|
+
recordDatabaseStatement,
|
|
15
|
+
} from "./database-metrics";
|
|
5
16
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
name: string;
|
|
9
|
-
handle: string;
|
|
10
|
-
external_user_id: string | null;
|
|
11
|
-
transport: string;
|
|
12
|
-
is_default: number;
|
|
13
|
-
created_at: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ProfilesTable {
|
|
17
|
-
id: string;
|
|
18
|
-
handle: string;
|
|
19
|
-
display_name: string;
|
|
20
|
-
bio: string;
|
|
21
|
-
followers_count: number;
|
|
22
|
-
following_count: number;
|
|
23
|
-
public_metrics_json: string;
|
|
24
|
-
avatar_hue: number;
|
|
25
|
-
avatar_url: string | null;
|
|
26
|
-
location: string | null;
|
|
27
|
-
url: string | null;
|
|
28
|
-
verified_type: string | null;
|
|
29
|
-
entities_json: string;
|
|
30
|
-
raw_json: string;
|
|
31
|
-
created_at: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface ProfileAffiliationsTable {
|
|
35
|
-
subject_profile_id: string;
|
|
36
|
-
organization_profile_id: string;
|
|
37
|
-
organization_name: string | null;
|
|
38
|
-
organization_handle: string | null;
|
|
39
|
-
badge_url: string | null;
|
|
40
|
-
url: string | null;
|
|
41
|
-
label: string | null;
|
|
42
|
-
source: string;
|
|
43
|
-
is_active: number;
|
|
44
|
-
first_seen_at: string;
|
|
45
|
-
last_seen_at: string;
|
|
46
|
-
raw_json: string;
|
|
47
|
-
updated_at: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface ProfileSnapshotsTable {
|
|
51
|
-
profile_id: string;
|
|
52
|
-
snapshot_hash: string;
|
|
53
|
-
observed_at: string;
|
|
54
|
-
last_seen_at: string;
|
|
55
|
-
source: string;
|
|
56
|
-
handle: string;
|
|
57
|
-
display_name: string;
|
|
58
|
-
bio: string;
|
|
59
|
-
location: string | null;
|
|
60
|
-
url: string | null;
|
|
61
|
-
verified_type: string | null;
|
|
62
|
-
followers_count: number;
|
|
63
|
-
following_count: number;
|
|
64
|
-
affiliations_json: string;
|
|
65
|
-
raw_json: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface ProfileBioEntitiesTable {
|
|
69
|
-
profile_id: string;
|
|
70
|
-
kind: string;
|
|
71
|
-
value: string;
|
|
72
|
-
source: string;
|
|
73
|
-
is_active: number;
|
|
74
|
-
first_seen_at: string;
|
|
75
|
-
last_seen_at: string;
|
|
76
|
-
raw_json: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface IdentitySearchIndexTable {
|
|
80
|
-
profile_id: string;
|
|
81
|
-
kind: string;
|
|
82
|
-
value: string;
|
|
83
|
-
normalized_value: string;
|
|
84
|
-
source: string;
|
|
85
|
-
weight: number;
|
|
86
|
-
updated_at: string;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export interface TweetsTable {
|
|
90
|
-
id: string;
|
|
91
|
-
account_id: string;
|
|
92
|
-
author_profile_id: string;
|
|
93
|
-
kind: string;
|
|
94
|
-
text: string;
|
|
95
|
-
created_at: string;
|
|
96
|
-
is_replied: number;
|
|
97
|
-
reply_to_id: string | null;
|
|
98
|
-
like_count: number;
|
|
99
|
-
media_count: number;
|
|
100
|
-
bookmarked: number;
|
|
101
|
-
liked: number;
|
|
102
|
-
entities_json: string;
|
|
103
|
-
media_json: string;
|
|
104
|
-
quoted_tweet_id: string | null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export interface TweetCollectionsTable {
|
|
108
|
-
account_id: string;
|
|
109
|
-
tweet_id: string;
|
|
110
|
-
kind: "likes" | "bookmarks";
|
|
111
|
-
collected_at: string | null;
|
|
112
|
-
source: string;
|
|
113
|
-
raw_json: string;
|
|
114
|
-
updated_at: string;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export interface TweetAccountEdgesTable {
|
|
118
|
-
account_id: string;
|
|
119
|
-
tweet_id: string;
|
|
120
|
-
kind:
|
|
121
|
-
| "home"
|
|
122
|
-
| "mention"
|
|
123
|
-
| "authored"
|
|
124
|
-
| "search"
|
|
125
|
-
| "profile"
|
|
126
|
-
| "thread_context";
|
|
127
|
-
first_seen_at: string;
|
|
128
|
-
last_seen_at: string;
|
|
129
|
-
seen_count: number;
|
|
130
|
-
source: string;
|
|
131
|
-
raw_json: string;
|
|
132
|
-
updated_at: string;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export interface DmConversationsTable {
|
|
136
|
-
id: string;
|
|
137
|
-
account_id: string;
|
|
138
|
-
participant_profile_id: string;
|
|
139
|
-
title: string;
|
|
140
|
-
inbox_kind: "accepted" | "request";
|
|
141
|
-
last_message_at: string;
|
|
142
|
-
unread_count: number;
|
|
143
|
-
needs_reply: number;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export interface DmMessagesTable {
|
|
147
|
-
id: string;
|
|
148
|
-
conversation_id: string;
|
|
149
|
-
sender_profile_id: string;
|
|
150
|
-
text: string;
|
|
151
|
-
created_at: string;
|
|
152
|
-
direction: "inbound" | "outbound";
|
|
153
|
-
is_replied: number;
|
|
154
|
-
media_count: number;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export interface TweetActionsTable {
|
|
158
|
-
id: string;
|
|
159
|
-
account_id: string;
|
|
160
|
-
tweet_id: string | null;
|
|
161
|
-
kind: string;
|
|
162
|
-
body: string;
|
|
163
|
-
created_at: string;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export interface BlocksTable {
|
|
167
|
-
account_id: string;
|
|
168
|
-
profile_id: string;
|
|
169
|
-
source: string;
|
|
170
|
-
created_at: string;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export interface MutesTable {
|
|
174
|
-
account_id: string;
|
|
175
|
-
profile_id: string;
|
|
176
|
-
source: string;
|
|
177
|
-
created_at: string;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export interface AiScoresTable {
|
|
181
|
-
entity_kind: string;
|
|
182
|
-
entity_id: string;
|
|
183
|
-
model: string;
|
|
184
|
-
score: number;
|
|
185
|
-
summary: string;
|
|
186
|
-
reasoning: string;
|
|
187
|
-
updated_at: string;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export interface SyncCacheTable {
|
|
191
|
-
cache_key: string;
|
|
192
|
-
value_json: string;
|
|
193
|
-
updated_at: string;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export interface UrlExpansionsTable {
|
|
197
|
-
short_url: string;
|
|
198
|
-
expanded_url: string;
|
|
199
|
-
final_url: string;
|
|
200
|
-
status: string;
|
|
201
|
-
expanded_tweet_id: string | null;
|
|
202
|
-
expanded_handle: string | null;
|
|
203
|
-
title: string | null;
|
|
204
|
-
description: string | null;
|
|
205
|
-
image_url: string | null;
|
|
206
|
-
site_name: string | null;
|
|
207
|
-
error: string | null;
|
|
208
|
-
source: string;
|
|
209
|
-
updated_at: string;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export interface LinkOccurrencesTable {
|
|
213
|
-
source_kind: "dm" | "tweet";
|
|
214
|
-
source_id: string;
|
|
215
|
-
source_position: number;
|
|
216
|
-
short_url: string;
|
|
217
|
-
account_id: string | null;
|
|
218
|
-
conversation_id: string | null;
|
|
219
|
-
direction: string | null;
|
|
220
|
-
created_at: string;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export interface FollowEdgesTable {
|
|
224
|
-
account_id: string;
|
|
225
|
-
direction: "followers" | "following";
|
|
226
|
-
profile_id: string;
|
|
227
|
-
external_user_id: string;
|
|
228
|
-
source: string;
|
|
229
|
-
current: number;
|
|
230
|
-
first_seen_at: string;
|
|
231
|
-
last_seen_at: string;
|
|
232
|
-
ended_at: string | null;
|
|
233
|
-
updated_at: string;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export interface FollowSnapshotsTable {
|
|
237
|
-
id: string;
|
|
238
|
-
account_id: string;
|
|
239
|
-
direction: "followers" | "following";
|
|
240
|
-
source: string;
|
|
241
|
-
status: "complete" | "incomplete";
|
|
242
|
-
page_count: number;
|
|
243
|
-
result_count: number;
|
|
244
|
-
started_at: string;
|
|
245
|
-
completed_at: string;
|
|
246
|
-
raw_meta_json: string;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export interface FollowSnapshotMembersTable {
|
|
250
|
-
snapshot_id: string;
|
|
251
|
-
profile_id: string;
|
|
252
|
-
external_user_id: string;
|
|
253
|
-
position: number;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export interface FollowEventsTable {
|
|
257
|
-
id: string;
|
|
258
|
-
account_id: string;
|
|
259
|
-
direction: "followers" | "following";
|
|
260
|
-
profile_id: string;
|
|
261
|
-
external_user_id: string;
|
|
262
|
-
kind: "started" | "ended";
|
|
263
|
-
event_at: string;
|
|
264
|
-
snapshot_id: string;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export interface GeocodedLocationsTable {
|
|
268
|
-
normalized_key: string;
|
|
269
|
-
original: string;
|
|
270
|
-
lat: number;
|
|
271
|
-
lng: number;
|
|
272
|
-
formatted: string | null;
|
|
273
|
-
country_code: string | null;
|
|
274
|
-
confidence: number | null;
|
|
275
|
-
provider: string;
|
|
276
|
-
approx_radius_m: number | null;
|
|
277
|
-
bounds_json: string;
|
|
278
|
-
components_json: string;
|
|
279
|
-
hits: number;
|
|
280
|
-
created_at: string;
|
|
281
|
-
last_used_at: string;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export interface GeocodedLocationsUnresolvedTable {
|
|
285
|
-
normalized_key: string;
|
|
286
|
-
original: string;
|
|
287
|
-
reason: string;
|
|
288
|
-
last_attempted_at: string;
|
|
289
|
-
ttl_until: string | null;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export interface BirdclawDatabase {
|
|
293
|
-
accounts: AccountsTable;
|
|
294
|
-
profiles: ProfilesTable;
|
|
295
|
-
profile_affiliations: ProfileAffiliationsTable;
|
|
296
|
-
profile_snapshots: ProfileSnapshotsTable;
|
|
297
|
-
profile_bio_entities: ProfileBioEntitiesTable;
|
|
298
|
-
identity_search_index: IdentitySearchIndexTable;
|
|
299
|
-
tweets: TweetsTable;
|
|
300
|
-
tweet_collections: TweetCollectionsTable;
|
|
301
|
-
tweet_account_edges: TweetAccountEdgesTable;
|
|
302
|
-
dm_conversations: DmConversationsTable;
|
|
303
|
-
dm_messages: DmMessagesTable;
|
|
304
|
-
tweet_actions: TweetActionsTable;
|
|
305
|
-
blocks: BlocksTable;
|
|
306
|
-
mutes: MutesTable;
|
|
307
|
-
ai_scores: AiScoresTable;
|
|
308
|
-
sync_cache: SyncCacheTable;
|
|
309
|
-
url_expansions: UrlExpansionsTable;
|
|
310
|
-
link_occurrences: LinkOccurrencesTable;
|
|
311
|
-
follow_edges: FollowEdgesTable;
|
|
312
|
-
follow_snapshots: FollowSnapshotsTable;
|
|
313
|
-
follow_snapshot_members: FollowSnapshotMembersTable;
|
|
314
|
-
follow_events: FollowEventsTable;
|
|
315
|
-
geocoded_locations: GeocodedLocationsTable;
|
|
316
|
-
geocoded_locations_unresolved: GeocodedLocationsUnresolvedTable;
|
|
317
|
-
}
|
|
17
|
+
import type { BirdclawDatabase } from "./database-schema";
|
|
18
|
+
export * from "./database-schema";
|
|
318
19
|
|
|
319
20
|
let nativeDb: Database | undefined;
|
|
21
|
+
let readDbs: Database[] = [];
|
|
22
|
+
let readDbIndex = 0;
|
|
320
23
|
let kyselyDb: Kysely<BirdclawDatabase> | undefined;
|
|
321
24
|
let demoSeedAttempted = false;
|
|
322
25
|
|
|
@@ -326,7 +29,7 @@ export interface InitDatabaseOptions {
|
|
|
326
29
|
|
|
327
30
|
const BASE_SCHEMA_SQL = `
|
|
328
31
|
pragma journal_mode = wal;
|
|
329
|
-
pragma busy_timeout =
|
|
32
|
+
pragma busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS};
|
|
330
33
|
pragma foreign_keys = on;
|
|
331
34
|
|
|
332
35
|
create table if not exists accounts (
|
|
@@ -948,6 +651,34 @@ function ensureFollowGraphTables(db: Database) {
|
|
|
948
651
|
}
|
|
949
652
|
|
|
950
653
|
function backfillTweetCollections(db: Database) {
|
|
654
|
+
const missingKinds = (
|
|
655
|
+
[
|
|
656
|
+
["likes", "liked"],
|
|
657
|
+
["bookmarks", "bookmarked"],
|
|
658
|
+
] as const
|
|
659
|
+
).filter(([, column]) =>
|
|
660
|
+
db
|
|
661
|
+
.prepare(
|
|
662
|
+
`
|
|
663
|
+
select 1
|
|
664
|
+
from tweets as tweet
|
|
665
|
+
where tweet.${column} = 1
|
|
666
|
+
and not exists (
|
|
667
|
+
select 1
|
|
668
|
+
from tweet_collections as collection
|
|
669
|
+
where collection.account_id = tweet.account_id
|
|
670
|
+
and collection.tweet_id = tweet.id
|
|
671
|
+
and collection.kind = ?
|
|
672
|
+
)
|
|
673
|
+
limit 1
|
|
674
|
+
`,
|
|
675
|
+
)
|
|
676
|
+
.get(column === "liked" ? "likes" : "bookmarks"),
|
|
677
|
+
);
|
|
678
|
+
if (missingKinds.length === 0) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
951
682
|
const now = new Date().toISOString();
|
|
952
683
|
const insert = db.prepare(`
|
|
953
684
|
insert or ignore into tweet_collections (
|
|
@@ -963,12 +694,34 @@ function backfillTweetCollections(db: Database) {
|
|
|
963
694
|
`);
|
|
964
695
|
|
|
965
696
|
db.transaction(() => {
|
|
966
|
-
|
|
967
|
-
|
|
697
|
+
for (const [kind] of missingKinds) {
|
|
698
|
+
insert.run(kind, now, kind);
|
|
699
|
+
}
|
|
968
700
|
})();
|
|
969
701
|
}
|
|
970
702
|
|
|
971
703
|
function backfillTweetAccountEdges(db: Database) {
|
|
704
|
+
const missing = db
|
|
705
|
+
.prepare(
|
|
706
|
+
`
|
|
707
|
+
select 1
|
|
708
|
+
from tweets as tweet
|
|
709
|
+
where tweet.kind in ('home', 'mention')
|
|
710
|
+
and not exists (
|
|
711
|
+
select 1
|
|
712
|
+
from tweet_account_edges as edge
|
|
713
|
+
where edge.account_id = tweet.account_id
|
|
714
|
+
and edge.tweet_id = tweet.id
|
|
715
|
+
and edge.kind = tweet.kind
|
|
716
|
+
)
|
|
717
|
+
limit 1
|
|
718
|
+
`,
|
|
719
|
+
)
|
|
720
|
+
.get();
|
|
721
|
+
if (!missing) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
972
725
|
const now = new Date().toISOString();
|
|
973
726
|
db.prepare(`
|
|
974
727
|
insert or ignore into tweet_account_edges (
|
|
@@ -994,6 +747,31 @@ function ensureSchemaIndexes(db: Database) {
|
|
|
994
747
|
db.exec(INDEX_SQL);
|
|
995
748
|
}
|
|
996
749
|
|
|
750
|
+
const DATABASE_MIGRATIONS: readonly DatabaseMigration[] = [
|
|
751
|
+
{
|
|
752
|
+
version: 1,
|
|
753
|
+
name: "canonical schema baseline",
|
|
754
|
+
up: (db) => {
|
|
755
|
+
db.exec(BASE_SCHEMA_SQL);
|
|
756
|
+
ensureAccountExternalUserIdColumn(db);
|
|
757
|
+
ensureDmConversationInboxColumns(db);
|
|
758
|
+
ensureTweetMetadataColumns(db);
|
|
759
|
+
ensureProfileAvatarColumns(db);
|
|
760
|
+
ensureTweetCollectionsTable(db);
|
|
761
|
+
ensureTweetAccountEdgesTable(db);
|
|
762
|
+
ensureProfileAffiliationsTable(db);
|
|
763
|
+
ensureProfileSnapshotsTable(db);
|
|
764
|
+
ensureProfileBioEntitiesTable(db);
|
|
765
|
+
ensureIdentitySearchIndexTable(db);
|
|
766
|
+
ensureLinkIndexTables(db);
|
|
767
|
+
ensureFollowGraphTables(db);
|
|
768
|
+
ensureSchemaIndexes(db);
|
|
769
|
+
backfillTweetCollections(db);
|
|
770
|
+
backfillTweetAccountEdges(db);
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
];
|
|
774
|
+
|
|
997
775
|
function ensureDemoData(db: Database) {
|
|
998
776
|
if (demoSeedAttempted) {
|
|
999
777
|
return;
|
|
@@ -1010,26 +788,14 @@ function initDatabase(options: InitDatabaseOptions = {}) {
|
|
|
1010
788
|
|
|
1011
789
|
if (!nativeDb) {
|
|
1012
790
|
const { dbPath } = getBirdclawPaths();
|
|
1013
|
-
nativeDb =
|
|
1014
|
-
nativeDb.exec(
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
ensureTweetCollectionsTable(nativeDb);
|
|
1020
|
-
ensureTweetAccountEdgesTable(nativeDb);
|
|
1021
|
-
ensureProfileAffiliationsTable(nativeDb);
|
|
1022
|
-
ensureProfileSnapshotsTable(nativeDb);
|
|
1023
|
-
ensureProfileBioEntitiesTable(nativeDb);
|
|
1024
|
-
ensureIdentitySearchIndexTable(nativeDb);
|
|
1025
|
-
ensureLinkIndexTables(nativeDb);
|
|
1026
|
-
ensureFollowGraphTables(nativeDb);
|
|
1027
|
-
ensureSchemaIndexes(nativeDb);
|
|
791
|
+
nativeDb = createDatabaseConnection(dbPath, "writer");
|
|
792
|
+
nativeDb.exec(`
|
|
793
|
+
pragma busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS};
|
|
794
|
+
pragma foreign_keys = on;
|
|
795
|
+
`);
|
|
796
|
+
runDatabaseMigrations(nativeDb, DATABASE_MIGRATIONS);
|
|
1028
797
|
if (options.seedDemoData !== false) {
|
|
1029
798
|
ensureDemoData(nativeDb);
|
|
1030
|
-
} else {
|
|
1031
|
-
backfillTweetCollections(nativeDb);
|
|
1032
|
-
backfillTweetAccountEdges(nativeDb);
|
|
1033
799
|
}
|
|
1034
800
|
} else if (options.seedDemoData !== false) {
|
|
1035
801
|
ensureDemoData(nativeDb);
|
|
@@ -1044,11 +810,44 @@ function initDatabase(options: InitDatabaseOptions = {}) {
|
|
|
1044
810
|
}
|
|
1045
811
|
}
|
|
1046
812
|
|
|
813
|
+
function createDatabaseConnection(
|
|
814
|
+
dbPath: string,
|
|
815
|
+
role: DatabaseConnectionRole,
|
|
816
|
+
options: { readonly?: boolean } = {},
|
|
817
|
+
) {
|
|
818
|
+
return new NativeSqliteDatabase(dbPath, {
|
|
819
|
+
...options,
|
|
820
|
+
onStatement: (sql, durationMs) =>
|
|
821
|
+
recordDatabaseStatement(role, sql, durationMs),
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
1047
825
|
export function getNativeDb(options: InitDatabaseOptions = {}) {
|
|
1048
826
|
initDatabase(options);
|
|
1049
827
|
return nativeDb as Database;
|
|
1050
828
|
}
|
|
1051
829
|
|
|
830
|
+
export function getReadDb(options: InitDatabaseOptions = {}) {
|
|
831
|
+
initDatabase(options);
|
|
832
|
+
if (readDbs.length === 0) {
|
|
833
|
+
const { dbPath } = getBirdclawPaths();
|
|
834
|
+
readDbs = Array.from({ length: 2 }, () => {
|
|
835
|
+
const db = createDatabaseConnection(dbPath, "reader", {
|
|
836
|
+
readonly: true,
|
|
837
|
+
});
|
|
838
|
+
db.exec(`
|
|
839
|
+
pragma busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS};
|
|
840
|
+
pragma foreign_keys = on;
|
|
841
|
+
pragma query_only = on;
|
|
842
|
+
`);
|
|
843
|
+
return db;
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
const db = readDbs[readDbIndex % readDbs.length] as Database;
|
|
847
|
+
readDbIndex = (readDbIndex + 1) % readDbs.length;
|
|
848
|
+
return db;
|
|
849
|
+
}
|
|
850
|
+
|
|
1052
851
|
export function getDb() {
|
|
1053
852
|
initDatabase();
|
|
1054
853
|
return kyselyDb as Kysely<BirdclawDatabase>;
|
|
@@ -1057,10 +856,14 @@ export function getDb() {
|
|
|
1057
856
|
export async function closeDatabase() {
|
|
1058
857
|
const db = kyselyDb;
|
|
1059
858
|
const native = nativeDb;
|
|
859
|
+
const readers = readDbs;
|
|
1060
860
|
kyselyDb = undefined;
|
|
1061
861
|
nativeDb = undefined;
|
|
862
|
+
readDbs = [];
|
|
863
|
+
readDbIndex = 0;
|
|
1062
864
|
demoSeedAttempted = false;
|
|
1063
865
|
|
|
866
|
+
for (const reader of readers) reader.close();
|
|
1064
867
|
if (db) {
|
|
1065
868
|
await db.destroy();
|
|
1066
869
|
} else {
|
|
@@ -1071,9 +874,13 @@ export async function closeDatabase() {
|
|
|
1071
874
|
export function resetDatabaseForTests() {
|
|
1072
875
|
const db = kyselyDb;
|
|
1073
876
|
const native = nativeDb;
|
|
877
|
+
const readers = readDbs;
|
|
1074
878
|
kyselyDb = undefined;
|
|
1075
879
|
nativeDb = undefined;
|
|
880
|
+
readDbs = [];
|
|
881
|
+
readDbIndex = 0;
|
|
1076
882
|
demoSeedAttempted = false;
|
|
883
|
+
for (const reader of readers) reader.close();
|
|
1077
884
|
if (db) {
|
|
1078
885
|
void db.destroy();
|
|
1079
886
|
} else {
|