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.
- package/CHANGELOG.md +16 -0
- package/package.json +2 -1
- 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/MarkdownCitations.tsx +680 -0
- package/src/components/MarkdownViewer.tsx +8 -674
- package/src/components/ProfileAnalysisClient.ts +191 -0
- package/src/components/ProfileAnalysisStream.tsx +16 -185
- package/src/components/ProfilePreview.tsx +2 -0
- 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/useTimelineRouteData.ts +105 -235
- package/src/lib/analysis-runtime.ts +238 -0
- package/src/lib/api-client.ts +16 -215
- package/src/lib/api-contracts.ts +328 -0
- package/src/lib/archive-import-plan.ts +102 -0
- package/src/lib/archive-import.ts +170 -239
- package/src/lib/authored-live.ts +75 -120
- package/src/lib/backup.ts +335 -424
- 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 +84 -330
- 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 -177
- package/src/lib/mentions-export.ts +1 -1
- package/src/lib/mentions-live.ts +57 -181
- 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 +36 -110
- package/src/lib/queries.ts +6 -2381
- 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 +48 -12
- 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 -257
- package/src/lib/timeline-live.ts +86 -236
- package/src/lib/timeline-read-model.ts +1191 -0
- package/src/lib/tweet-repository.ts +156 -0
- package/src/lib/tweet-search-live.ts +63 -167
- package/src/lib/web-sync.ts +67 -50
- package/src/lib/whois.ts +2 -1
- package/src/routes/__root.tsx +11 -8
- 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 -4
- 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 +167 -184
- package/src/routes/inbox.tsx +63 -57
- package/src/routes/links.tsx +31 -394
- package/src/routes/network-map.tsx +41 -344
- package/src/routes/rate-limits.tsx +17 -21
- package/src/lib/client-cache.ts +0 -109
package/src/lib/blocks-write.ts
CHANGED
|
@@ -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 addBlockEffect(
|
|
20
13
|
accountId: string,
|
|
21
14
|
query: string,
|
|
@@ -49,14 +42,16 @@ export function addBlockEffect(
|
|
|
49
42
|
}
|
|
50
43
|
|
|
51
44
|
const blockedAt = new Date().toISOString();
|
|
52
|
-
yield*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
yield* databaseWriteEffect(
|
|
46
|
+
(writeDb) =>
|
|
47
|
+
writeModerationRow(
|
|
48
|
+
writeDb,
|
|
49
|
+
"blocks",
|
|
50
|
+
resolvedAccountId,
|
|
51
|
+
resolved.profile.id,
|
|
52
|
+
blockedAt,
|
|
53
|
+
),
|
|
54
|
+
db,
|
|
60
55
|
);
|
|
61
56
|
|
|
62
57
|
return {
|
|
@@ -88,14 +83,16 @@ export function recordBlockEffect(accountId: string, query: string) {
|
|
|
88
83
|
});
|
|
89
84
|
|
|
90
85
|
const blockedAt = new Date().toISOString();
|
|
91
|
-
yield*
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
86
|
+
yield* databaseWriteEffect(
|
|
87
|
+
(writeDb) =>
|
|
88
|
+
writeModerationRow(
|
|
89
|
+
writeDb,
|
|
90
|
+
"blocks",
|
|
91
|
+
resolvedAccountId,
|
|
92
|
+
resolved.profile.id,
|
|
93
|
+
blockedAt,
|
|
94
|
+
),
|
|
95
|
+
db,
|
|
99
96
|
);
|
|
100
97
|
|
|
101
98
|
return {
|
|
@@ -144,8 +141,15 @@ export function removeBlockEffect(
|
|
|
144
141
|
};
|
|
145
142
|
}
|
|
146
143
|
|
|
147
|
-
yield*
|
|
148
|
-
|
|
144
|
+
yield* databaseWriteEffect(
|
|
145
|
+
(writeDb) =>
|
|
146
|
+
deleteModerationRow(
|
|
147
|
+
writeDb,
|
|
148
|
+
"blocks",
|
|
149
|
+
resolvedAccountId,
|
|
150
|
+
resolved.profile.id,
|
|
151
|
+
),
|
|
152
|
+
db,
|
|
149
153
|
);
|
|
150
154
|
|
|
151
155
|
return {
|
package/src/lib/blocks.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
2
|
import type { Database } from "./sqlite";
|
|
3
3
|
import { getNativeDb } from "./db";
|
|
4
|
+
import { databaseWriteEffect } from "./database-writer";
|
|
4
5
|
import { runEffectPromise, tryPromise } from "./effect-runtime";
|
|
5
6
|
import {
|
|
6
7
|
getAccountHandle,
|
|
@@ -322,33 +323,30 @@ export function syncBlocksEffect(accountId: string) {
|
|
|
322
323
|
const page = yield* tryPromise(() =>
|
|
323
324
|
listBlockedUsers(sourceUserId, nextToken ?? undefined),
|
|
324
325
|
).pipe(Effect.mapError(toError));
|
|
325
|
-
yield*
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
);
|
|
336
|
-
}
|
|
326
|
+
const pageProfileIds = yield* databaseWriteEffect((writeDb) => {
|
|
327
|
+
return page.items.map((user) => {
|
|
328
|
+
const resolved = upsertProfileFromXUser(writeDb, user);
|
|
329
|
+
upsertRemoteBlock(
|
|
330
|
+
writeDb,
|
|
331
|
+
resolvedAccountId,
|
|
332
|
+
resolved.profile.id,
|
|
333
|
+
blockedAt,
|
|
334
|
+
);
|
|
335
|
+
return resolved.profile.id;
|
|
337
336
|
});
|
|
338
|
-
|
|
339
|
-
|
|
337
|
+
}, db);
|
|
338
|
+
remoteProfileIds.push(...pageProfileIds);
|
|
340
339
|
nextToken = page.nextToken;
|
|
341
340
|
pageCount += 1;
|
|
342
341
|
} while (nextToken && pageCount < 20);
|
|
343
342
|
|
|
344
343
|
completed = !nextToken;
|
|
345
344
|
if (completed) {
|
|
346
|
-
yield*
|
|
347
|
-
|
|
348
|
-
pruneRemoteBlocks(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
});
|
|
345
|
+
yield* databaseWriteEffect(
|
|
346
|
+
(writeDb) =>
|
|
347
|
+
pruneRemoteBlocks(writeDb, resolvedAccountId, remoteProfileIds),
|
|
348
|
+
db,
|
|
349
|
+
);
|
|
352
350
|
}
|
|
353
351
|
|
|
354
352
|
return {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export type DatabaseConnectionRole = "reader" | "writer";
|
|
2
|
+
|
|
3
|
+
export interface SlowDatabaseStatement {
|
|
4
|
+
durationMs: number;
|
|
5
|
+
role: DatabaseConnectionRole;
|
|
6
|
+
sql: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const SLOW_STATEMENT_MS = 50;
|
|
10
|
+
const MAX_SLOW_STATEMENTS = 10;
|
|
11
|
+
|
|
12
|
+
let readStatements = 0;
|
|
13
|
+
let writeStatements = 0;
|
|
14
|
+
let slowStatements: SlowDatabaseStatement[] = [];
|
|
15
|
+
let queuedWrites = 0;
|
|
16
|
+
let activeWrites = 0;
|
|
17
|
+
let completedWrites = 0;
|
|
18
|
+
let failedWrites = 0;
|
|
19
|
+
let maxQueuedWrites = 0;
|
|
20
|
+
let maxWriteWaitMs = 0;
|
|
21
|
+
|
|
22
|
+
function compactSql(sql: string) {
|
|
23
|
+
return sql.replace(/\s+/g, " ").trim().slice(0, 240);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function recordDatabaseStatement(
|
|
27
|
+
role: DatabaseConnectionRole,
|
|
28
|
+
sql: string,
|
|
29
|
+
durationMs: number,
|
|
30
|
+
) {
|
|
31
|
+
if (role === "reader") {
|
|
32
|
+
readStatements += 1;
|
|
33
|
+
} else {
|
|
34
|
+
writeStatements += 1;
|
|
35
|
+
}
|
|
36
|
+
if (durationMs < SLOW_STATEMENT_MS) return;
|
|
37
|
+
slowStatements = [
|
|
38
|
+
...slowStatements,
|
|
39
|
+
{ durationMs, role, sql: compactSql(sql) },
|
|
40
|
+
].slice(-MAX_SLOW_STATEMENTS);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function recordDatabaseWriteQueued() {
|
|
44
|
+
queuedWrites += 1;
|
|
45
|
+
maxQueuedWrites = Math.max(maxQueuedWrites, queuedWrites);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function recordDatabaseWriteStarted(waitMs: number) {
|
|
49
|
+
queuedWrites = Math.max(0, queuedWrites - 1);
|
|
50
|
+
activeWrites += 1;
|
|
51
|
+
maxWriteWaitMs = Math.max(maxWriteWaitMs, waitMs);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function recordDatabaseWriteCompleted(failed: boolean) {
|
|
55
|
+
activeWrites = Math.max(0, activeWrites - 1);
|
|
56
|
+
completedWrites += 1;
|
|
57
|
+
if (failed) failedWrites += 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getDatabaseRuntimeMetrics() {
|
|
61
|
+
return {
|
|
62
|
+
connections: {
|
|
63
|
+
readStatements,
|
|
64
|
+
writeStatements,
|
|
65
|
+
slowStatements: [...slowStatements],
|
|
66
|
+
},
|
|
67
|
+
writer: {
|
|
68
|
+
active: activeWrites,
|
|
69
|
+
completed: completedWrites,
|
|
70
|
+
failed: failedWrites,
|
|
71
|
+
maxQueued: maxQueuedWrites,
|
|
72
|
+
maxWaitMs: maxWriteWaitMs,
|
|
73
|
+
queued: queuedWrites,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function resetDatabaseRuntimeMetricsForTests() {
|
|
79
|
+
readStatements = 0;
|
|
80
|
+
writeStatements = 0;
|
|
81
|
+
slowStatements = [];
|
|
82
|
+
queuedWrites = 0;
|
|
83
|
+
activeWrites = 0;
|
|
84
|
+
completedWrites = 0;
|
|
85
|
+
failedWrites = 0;
|
|
86
|
+
maxQueuedWrites = 0;
|
|
87
|
+
maxWriteWaitMs = 0;
|
|
88
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Database } from "./sqlite";
|
|
2
|
+
|
|
3
|
+
export interface DatabaseMigration {
|
|
4
|
+
version: number;
|
|
5
|
+
name: string;
|
|
6
|
+
up: (db: Database) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getDatabaseSchemaVersion(db: Database) {
|
|
10
|
+
return Number(db.pragma("user_version", { simple: true }) ?? 0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function runDatabaseMigrations(
|
|
14
|
+
db: Database,
|
|
15
|
+
migrations: readonly DatabaseMigration[],
|
|
16
|
+
) {
|
|
17
|
+
let currentVersion = getDatabaseSchemaVersion(db);
|
|
18
|
+
const pending = [...migrations]
|
|
19
|
+
.filter((migration) => migration.version > currentVersion)
|
|
20
|
+
.sort((left, right) => left.version - right.version);
|
|
21
|
+
|
|
22
|
+
for (const migration of pending) {
|
|
23
|
+
if (migration.version !== currentVersion + 1) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Missing database migration between versions ${String(currentVersion)} and ${String(migration.version)}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
migration.up(db);
|
|
29
|
+
db.pragma(`user_version = ${String(migration.version)}`);
|
|
30
|
+
currentVersion = migration.version;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return currentVersion;
|
|
34
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
export interface AccountsTable {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
handle: string;
|
|
5
|
+
external_user_id: string | null;
|
|
6
|
+
transport: string;
|
|
7
|
+
is_default: number;
|
|
8
|
+
created_at: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ProfilesTable {
|
|
12
|
+
id: string;
|
|
13
|
+
handle: string;
|
|
14
|
+
display_name: string;
|
|
15
|
+
bio: string;
|
|
16
|
+
followers_count: number;
|
|
17
|
+
following_count: number;
|
|
18
|
+
public_metrics_json: string;
|
|
19
|
+
avatar_hue: number;
|
|
20
|
+
avatar_url: string | null;
|
|
21
|
+
location: string | null;
|
|
22
|
+
url: string | null;
|
|
23
|
+
verified_type: string | null;
|
|
24
|
+
entities_json: string;
|
|
25
|
+
raw_json: string;
|
|
26
|
+
created_at: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ProfileAffiliationsTable {
|
|
30
|
+
subject_profile_id: string;
|
|
31
|
+
organization_profile_id: string;
|
|
32
|
+
organization_name: string | null;
|
|
33
|
+
organization_handle: string | null;
|
|
34
|
+
badge_url: string | null;
|
|
35
|
+
url: string | null;
|
|
36
|
+
label: string | null;
|
|
37
|
+
source: string;
|
|
38
|
+
is_active: number;
|
|
39
|
+
first_seen_at: string;
|
|
40
|
+
last_seen_at: string;
|
|
41
|
+
raw_json: string;
|
|
42
|
+
updated_at: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ProfileSnapshotsTable {
|
|
46
|
+
profile_id: string;
|
|
47
|
+
snapshot_hash: string;
|
|
48
|
+
observed_at: string;
|
|
49
|
+
last_seen_at: string;
|
|
50
|
+
source: string;
|
|
51
|
+
handle: string;
|
|
52
|
+
display_name: string;
|
|
53
|
+
bio: string;
|
|
54
|
+
location: string | null;
|
|
55
|
+
url: string | null;
|
|
56
|
+
verified_type: string | null;
|
|
57
|
+
followers_count: number;
|
|
58
|
+
following_count: number;
|
|
59
|
+
affiliations_json: string;
|
|
60
|
+
raw_json: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ProfileBioEntitiesTable {
|
|
64
|
+
profile_id: string;
|
|
65
|
+
kind: string;
|
|
66
|
+
value: string;
|
|
67
|
+
source: string;
|
|
68
|
+
is_active: number;
|
|
69
|
+
first_seen_at: string;
|
|
70
|
+
last_seen_at: string;
|
|
71
|
+
raw_json: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface IdentitySearchIndexTable {
|
|
75
|
+
profile_id: string;
|
|
76
|
+
kind: string;
|
|
77
|
+
value: string;
|
|
78
|
+
normalized_value: string;
|
|
79
|
+
source: string;
|
|
80
|
+
weight: number;
|
|
81
|
+
updated_at: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface TweetsTable {
|
|
85
|
+
id: string;
|
|
86
|
+
account_id: string;
|
|
87
|
+
author_profile_id: string;
|
|
88
|
+
kind: string;
|
|
89
|
+
text: string;
|
|
90
|
+
created_at: string;
|
|
91
|
+
is_replied: number;
|
|
92
|
+
reply_to_id: string | null;
|
|
93
|
+
like_count: number;
|
|
94
|
+
media_count: number;
|
|
95
|
+
bookmarked: number;
|
|
96
|
+
liked: number;
|
|
97
|
+
entities_json: string;
|
|
98
|
+
media_json: string;
|
|
99
|
+
quoted_tweet_id: string | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface TweetCollectionsTable {
|
|
103
|
+
account_id: string;
|
|
104
|
+
tweet_id: string;
|
|
105
|
+
kind: "likes" | "bookmarks";
|
|
106
|
+
collected_at: string | null;
|
|
107
|
+
source: string;
|
|
108
|
+
raw_json: string;
|
|
109
|
+
updated_at: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface TweetAccountEdgesTable {
|
|
113
|
+
account_id: string;
|
|
114
|
+
tweet_id: string;
|
|
115
|
+
kind:
|
|
116
|
+
| "home"
|
|
117
|
+
| "mention"
|
|
118
|
+
| "authored"
|
|
119
|
+
| "search"
|
|
120
|
+
| "profile"
|
|
121
|
+
| "thread_context";
|
|
122
|
+
first_seen_at: string;
|
|
123
|
+
last_seen_at: string;
|
|
124
|
+
seen_count: number;
|
|
125
|
+
source: string;
|
|
126
|
+
raw_json: string;
|
|
127
|
+
updated_at: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface DmConversationsTable {
|
|
131
|
+
id: string;
|
|
132
|
+
account_id: string;
|
|
133
|
+
participant_profile_id: string;
|
|
134
|
+
title: string;
|
|
135
|
+
inbox_kind: "accepted" | "request";
|
|
136
|
+
last_message_at: string;
|
|
137
|
+
unread_count: number;
|
|
138
|
+
needs_reply: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface DmMessagesTable {
|
|
142
|
+
id: string;
|
|
143
|
+
conversation_id: string;
|
|
144
|
+
sender_profile_id: string;
|
|
145
|
+
text: string;
|
|
146
|
+
created_at: string;
|
|
147
|
+
direction: "inbound" | "outbound";
|
|
148
|
+
is_replied: number;
|
|
149
|
+
media_count: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface TweetActionsTable {
|
|
153
|
+
id: string;
|
|
154
|
+
account_id: string;
|
|
155
|
+
tweet_id: string | null;
|
|
156
|
+
kind: string;
|
|
157
|
+
body: string;
|
|
158
|
+
created_at: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface BlocksTable {
|
|
162
|
+
account_id: string;
|
|
163
|
+
profile_id: string;
|
|
164
|
+
source: string;
|
|
165
|
+
created_at: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface MutesTable {
|
|
169
|
+
account_id: string;
|
|
170
|
+
profile_id: string;
|
|
171
|
+
source: string;
|
|
172
|
+
created_at: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface AiScoresTable {
|
|
176
|
+
entity_kind: string;
|
|
177
|
+
entity_id: string;
|
|
178
|
+
model: string;
|
|
179
|
+
score: number;
|
|
180
|
+
summary: string;
|
|
181
|
+
reasoning: string;
|
|
182
|
+
updated_at: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface SyncCacheTable {
|
|
186
|
+
cache_key: string;
|
|
187
|
+
value_json: string;
|
|
188
|
+
updated_at: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface UrlExpansionsTable {
|
|
192
|
+
short_url: string;
|
|
193
|
+
expanded_url: string;
|
|
194
|
+
final_url: string;
|
|
195
|
+
status: string;
|
|
196
|
+
expanded_tweet_id: string | null;
|
|
197
|
+
expanded_handle: string | null;
|
|
198
|
+
title: string | null;
|
|
199
|
+
description: string | null;
|
|
200
|
+
image_url: string | null;
|
|
201
|
+
site_name: string | null;
|
|
202
|
+
error: string | null;
|
|
203
|
+
source: string;
|
|
204
|
+
updated_at: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface LinkOccurrencesTable {
|
|
208
|
+
source_kind: "dm" | "tweet";
|
|
209
|
+
source_id: string;
|
|
210
|
+
source_position: number;
|
|
211
|
+
short_url: string;
|
|
212
|
+
account_id: string | null;
|
|
213
|
+
conversation_id: string | null;
|
|
214
|
+
direction: string | null;
|
|
215
|
+
created_at: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface FollowEdgesTable {
|
|
219
|
+
account_id: string;
|
|
220
|
+
direction: "followers" | "following";
|
|
221
|
+
profile_id: string;
|
|
222
|
+
external_user_id: string;
|
|
223
|
+
source: string;
|
|
224
|
+
current: number;
|
|
225
|
+
first_seen_at: string;
|
|
226
|
+
last_seen_at: string;
|
|
227
|
+
ended_at: string | null;
|
|
228
|
+
updated_at: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface FollowSnapshotsTable {
|
|
232
|
+
id: string;
|
|
233
|
+
account_id: string;
|
|
234
|
+
direction: "followers" | "following";
|
|
235
|
+
source: string;
|
|
236
|
+
status: "complete" | "incomplete";
|
|
237
|
+
page_count: number;
|
|
238
|
+
result_count: number;
|
|
239
|
+
started_at: string;
|
|
240
|
+
completed_at: string;
|
|
241
|
+
raw_meta_json: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface FollowSnapshotMembersTable {
|
|
245
|
+
snapshot_id: string;
|
|
246
|
+
profile_id: string;
|
|
247
|
+
external_user_id: string;
|
|
248
|
+
position: number;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface FollowEventsTable {
|
|
252
|
+
id: string;
|
|
253
|
+
account_id: string;
|
|
254
|
+
direction: "followers" | "following";
|
|
255
|
+
profile_id: string;
|
|
256
|
+
external_user_id: string;
|
|
257
|
+
kind: "started" | "ended";
|
|
258
|
+
event_at: string;
|
|
259
|
+
snapshot_id: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface GeocodedLocationsTable {
|
|
263
|
+
normalized_key: string;
|
|
264
|
+
original: string;
|
|
265
|
+
lat: number;
|
|
266
|
+
lng: number;
|
|
267
|
+
formatted: string | null;
|
|
268
|
+
country_code: string | null;
|
|
269
|
+
confidence: number | null;
|
|
270
|
+
provider: string;
|
|
271
|
+
approx_radius_m: number | null;
|
|
272
|
+
bounds_json: string;
|
|
273
|
+
components_json: string;
|
|
274
|
+
hits: number;
|
|
275
|
+
created_at: string;
|
|
276
|
+
last_used_at: string;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface GeocodedLocationsUnresolvedTable {
|
|
280
|
+
normalized_key: string;
|
|
281
|
+
original: string;
|
|
282
|
+
reason: string;
|
|
283
|
+
last_attempted_at: string;
|
|
284
|
+
ttl_until: string | null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface BirdclawDatabase {
|
|
288
|
+
accounts: AccountsTable;
|
|
289
|
+
profiles: ProfilesTable;
|
|
290
|
+
profile_affiliations: ProfileAffiliationsTable;
|
|
291
|
+
profile_snapshots: ProfileSnapshotsTable;
|
|
292
|
+
profile_bio_entities: ProfileBioEntitiesTable;
|
|
293
|
+
identity_search_index: IdentitySearchIndexTable;
|
|
294
|
+
tweets: TweetsTable;
|
|
295
|
+
tweet_collections: TweetCollectionsTable;
|
|
296
|
+
tweet_account_edges: TweetAccountEdgesTable;
|
|
297
|
+
dm_conversations: DmConversationsTable;
|
|
298
|
+
dm_messages: DmMessagesTable;
|
|
299
|
+
tweet_actions: TweetActionsTable;
|
|
300
|
+
blocks: BlocksTable;
|
|
301
|
+
mutes: MutesTable;
|
|
302
|
+
ai_scores: AiScoresTable;
|
|
303
|
+
sync_cache: SyncCacheTable;
|
|
304
|
+
url_expansions: UrlExpansionsTable;
|
|
305
|
+
link_occurrences: LinkOccurrencesTable;
|
|
306
|
+
follow_edges: FollowEdgesTable;
|
|
307
|
+
follow_snapshots: FollowSnapshotsTable;
|
|
308
|
+
follow_snapshot_members: FollowSnapshotMembersTable;
|
|
309
|
+
follow_events: FollowEventsTable;
|
|
310
|
+
geocoded_locations: GeocodedLocationsTable;
|
|
311
|
+
geocoded_locations_unresolved: GeocodedLocationsUnresolvedTable;
|
|
312
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { Database } from "./sqlite";
|
|
3
|
+
import {
|
|
4
|
+
defaultServerRuntimeServices,
|
|
5
|
+
type ServerRuntimeServices,
|
|
6
|
+
} from "./server-runtime-services";
|
|
7
|
+
import {
|
|
8
|
+
recordDatabaseWriteCompleted,
|
|
9
|
+
recordDatabaseWriteQueued,
|
|
10
|
+
recordDatabaseWriteStarted,
|
|
11
|
+
} from "./database-metrics";
|
|
12
|
+
|
|
13
|
+
let writeTails = new Map<string | object, Promise<void>>();
|
|
14
|
+
|
|
15
|
+
export function enqueueDatabaseWrite<T>(
|
|
16
|
+
write: (db: Database) => T,
|
|
17
|
+
providedDb?: Database,
|
|
18
|
+
runtime: ServerRuntimeServices = defaultServerRuntimeServices,
|
|
19
|
+
): Promise<T> {
|
|
20
|
+
const db = providedDb ?? runtime.getDatabase({ seedDemoData: false });
|
|
21
|
+
const writeIdentity = db.writeIdentity;
|
|
22
|
+
const queuedAt = performance.now();
|
|
23
|
+
recordDatabaseWriteQueued();
|
|
24
|
+
const writeTail = writeTails.get(writeIdentity) ?? Promise.resolve();
|
|
25
|
+
const pending = writeTail.then(() => {
|
|
26
|
+
recordDatabaseWriteStarted(performance.now() - queuedAt);
|
|
27
|
+
try {
|
|
28
|
+
const result = db.transaction(() => write(db))();
|
|
29
|
+
recordDatabaseWriteCompleted(false);
|
|
30
|
+
return result;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
recordDatabaseWriteCompleted(true);
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
const settled = pending.then(
|
|
37
|
+
() => undefined,
|
|
38
|
+
() => undefined,
|
|
39
|
+
);
|
|
40
|
+
writeTails.set(writeIdentity, settled);
|
|
41
|
+
void settled.then(() => {
|
|
42
|
+
if (writeTails.get(writeIdentity) === settled) {
|
|
43
|
+
writeTails.delete(writeIdentity);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return pending;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function databaseWriteEffect<T>(
|
|
50
|
+
write: (db: Database) => T,
|
|
51
|
+
providedDb?: Database,
|
|
52
|
+
runtime: ServerRuntimeServices = defaultServerRuntimeServices,
|
|
53
|
+
) {
|
|
54
|
+
return Effect.tryPromise({
|
|
55
|
+
try: () => enqueueDatabaseWrite(write, providedDb, runtime),
|
|
56
|
+
catch: (error) =>
|
|
57
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function drainDatabaseWrites() {
|
|
62
|
+
while (writeTails.size > 0) {
|
|
63
|
+
await Promise.all(writeTails.values());
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resetDatabaseWriterForTests() {
|
|
68
|
+
writeTails = new Map();
|
|
69
|
+
}
|