birdclaw 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/README.md +55 -5
- package/bin/birdclaw.mjs +50 -11
- package/package.json +9 -7
- package/public/birdclaw-mark.png +0 -0
- package/scripts/browser-perf.mjs +400 -0
- package/scripts/build-docs-site.mjs +940 -0
- package/scripts/docs-site-assets.mjs +311 -0
- package/scripts/run-vitest.mjs +21 -0
- package/scripts/sanitize-node-options.mjs +23 -0
- package/scripts/start-test-server.mjs +42 -0
- package/src/cli.ts +422 -14
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +29 -9
- package/src/components/AvatarChip.tsx +9 -3
- package/src/components/BrandMark.tsx +67 -0
- package/src/components/ConversationThread.tsx +11 -10
- package/src/components/DmWorkspace.tsx +39 -14
- package/src/components/FeedState.tsx +147 -0
- package/src/components/InboxCard.tsx +14 -2
- package/src/components/LinkPreviewCard.tsx +40 -18
- package/src/components/MarkdownViewer.tsx +452 -0
- package/src/components/SavedTimelineView.tsx +64 -56
- package/src/components/SyncNowButton.tsx +137 -0
- package/src/components/ThemeSlider.tsx +49 -93
- package/src/components/TimelineCard.tsx +364 -136
- package/src/components/TimelineRouteFrame.tsx +170 -0
- package/src/components/TweetMediaGrid.tsx +170 -24
- package/src/components/TweetRichText.tsx +28 -13
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +153 -0
- package/src/lib/account-sync-job.ts +654 -0
- package/src/lib/actions-transport.ts +216 -146
- package/src/lib/api-client.ts +304 -0
- package/src/lib/archive-finder.ts +72 -53
- package/src/lib/archive-import.ts +1377 -1298
- package/src/lib/authored-live.ts +261 -204
- package/src/lib/avatar-cache.ts +159 -44
- package/src/lib/backup.ts +1532 -951
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +549 -194
- package/src/lib/blocklist.ts +40 -23
- package/src/lib/blocks-write.ts +129 -80
- package/src/lib/blocks.ts +165 -97
- package/src/lib/bookmark-sync-job.ts +250 -160
- package/src/lib/conversation-surface.ts +205 -0
- package/src/lib/db.ts +35 -3
- package/src/lib/dms-live.ts +720 -66
- package/src/lib/effect-runtime.ts +45 -0
- package/src/lib/follow-graph.ts +224 -180
- package/src/lib/http-effect.ts +222 -0
- package/src/lib/inbox.ts +74 -43
- package/src/lib/link-index.ts +88 -76
- package/src/lib/link-insights.ts +24 -0
- package/src/lib/link-preview-metadata.ts +472 -52
- package/src/lib/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +352 -288
- package/src/lib/mentions-live.ts +390 -342
- package/src/lib/moderation-target.ts +102 -65
- package/src/lib/moderation-write.ts +77 -18
- package/src/lib/mutes-write.ts +129 -80
- package/src/lib/mutes.ts +8 -1
- package/src/lib/openai.ts +84 -53
- package/src/lib/period-digest.ts +953 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- package/src/lib/profile-hydration.ts +124 -72
- package/src/lib/profile-replies.ts +60 -43
- package/src/lib/profile-resolver.ts +402 -294
- package/src/lib/queries.ts +1024 -189
- package/src/lib/research.ts +165 -120
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +60 -39
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/types.ts +38 -1
- package/src/lib/ui.ts +41 -10
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +511 -0
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl.ts +681 -411
- package/src/routeTree.gen.ts +63 -0
- package/src/routes/__root.tsx +25 -5
- package/src/routes/api/action.tsx +127 -78
- package/src/routes/api/avatar.tsx +39 -30
- package/src/routes/api/blocks.tsx +26 -23
- package/src/routes/api/conversation.tsx +25 -14
- package/src/routes/api/inbox.tsx +27 -21
- package/src/routes/api/link-insights.tsx +31 -25
- package/src/routes/api/link-preview.tsx +25 -21
- package/src/routes/api/period-digest.tsx +123 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +79 -55
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +105 -0
- package/src/routes/dms.tsx +195 -55
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/index.tsx +21 -127
- package/src/routes/links.tsx +50 -5
- package/src/routes/mentions.tsx +21 -127
- package/src/routes/today.tsx +441 -0
- package/src/styles.css +74 -11
- package/vite.config.ts +8 -0
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
|
|
1
3
|
import { getNativeDb } from "./db";
|
|
2
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
lookupProfileViaBirdEffect,
|
|
6
|
+
lookupProfilesViaBirdEffect,
|
|
7
|
+
} from "./bird";
|
|
8
|
+
import { runEffectPromise, tryPromise } from "./effect-runtime";
|
|
3
9
|
import { readSyncCache, writeSyncCache } from "./sync-cache";
|
|
4
10
|
import {
|
|
5
|
-
|
|
11
|
+
hydrateProfileAffiliationOrganizationsEffect,
|
|
6
12
|
type ProfileAffiliationHydrationResult,
|
|
7
13
|
} from "./profile-affiliation-hydration";
|
|
8
14
|
import type { ProfileRecord, XurlMentionUser } from "./types";
|
|
@@ -22,6 +28,17 @@ interface CachedProfileLookup {
|
|
|
22
28
|
error?: string;
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
function toError(error: unknown) {
|
|
32
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function trySync<T>(try_: () => T) {
|
|
36
|
+
return Effect.try({
|
|
37
|
+
try: try_,
|
|
38
|
+
catch: toError,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
25
42
|
export interface ResolveProfilesOptions {
|
|
26
43
|
refresh?: boolean;
|
|
27
44
|
maxAgeMs?: number;
|
|
@@ -130,384 +147,467 @@ function writeProfileLookupCache(
|
|
|
130
147
|
writeSyncCache(cacheKeyForUserId(externalUserId), value);
|
|
131
148
|
}
|
|
132
149
|
|
|
133
|
-
function updateConversationTitles(profile: ProfileRecord) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
`
|
|
150
|
+
function updateConversationTitles(profile: ProfileRecord, db = getNativeDb()) {
|
|
151
|
+
db.prepare(
|
|
152
|
+
`
|
|
137
153
|
update dm_conversations
|
|
138
154
|
set title = ?
|
|
139
155
|
where participant_profile_id = ?
|
|
140
156
|
`,
|
|
141
|
-
|
|
142
|
-
.run(profile.displayName || profile.handle, profile.id);
|
|
157
|
+
).run(profile.displayName || profile.handle, profile.id);
|
|
143
158
|
}
|
|
144
159
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
160
|
+
function lookupViaXurlEffect(externalUserId: string) {
|
|
161
|
+
return Effect.gen(function* () {
|
|
162
|
+
const [user] = yield* tryPromise(() => lookupUsersByIds([externalUserId]));
|
|
163
|
+
return user ?? null;
|
|
164
|
+
});
|
|
148
165
|
}
|
|
149
166
|
|
|
150
167
|
function normalizeHandle(value: string) {
|
|
151
168
|
return value.trim().replace(/^@/, "").toLowerCase();
|
|
152
169
|
}
|
|
153
170
|
|
|
154
|
-
|
|
171
|
+
function fetchProfileUserEffect(
|
|
155
172
|
externalUserId: string,
|
|
156
173
|
xurlFallback: boolean,
|
|
157
|
-
):
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
174
|
+
): Effect.Effect<CachedProfileLookup, never> {
|
|
175
|
+
return Effect.gen(function* () {
|
|
176
|
+
const birdResult = yield* lookupProfileViaBirdEffect(externalUserId).pipe(
|
|
177
|
+
Effect.map((user) => ({ ok: true as const, user })),
|
|
178
|
+
Effect.catchAll((error) => Effect.succeed({ ok: false as const, error })),
|
|
179
|
+
);
|
|
180
|
+
if (birdResult.ok) {
|
|
181
|
+
const birdUser = birdResult.user;
|
|
182
|
+
if (birdUser) {
|
|
183
|
+
return { status: "hit", source: "bird", user: birdUser };
|
|
184
|
+
}
|
|
185
|
+
} else if (!xurlFallback) {
|
|
165
186
|
return {
|
|
166
187
|
status: "error",
|
|
167
188
|
source: "bird",
|
|
168
|
-
error:
|
|
189
|
+
error:
|
|
190
|
+
birdResult.error instanceof Error
|
|
191
|
+
? birdResult.error.message
|
|
192
|
+
: String(birdResult.error),
|
|
169
193
|
};
|
|
170
194
|
}
|
|
171
|
-
}
|
|
172
195
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
196
|
+
if (!xurlFallback) {
|
|
197
|
+
return { status: "miss", source: "bird" };
|
|
198
|
+
}
|
|
176
199
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
200
|
+
const xurlResult = yield* lookupViaXurlEffect(externalUserId).pipe(
|
|
201
|
+
Effect.map((user) => ({ ok: true as const, user })),
|
|
202
|
+
Effect.catchAll((error) => Effect.succeed({ ok: false as const, error })),
|
|
203
|
+
);
|
|
204
|
+
if (xurlResult.ok) {
|
|
205
|
+
const xurlUser = xurlResult.user;
|
|
206
|
+
return xurlUser
|
|
207
|
+
? { status: "hit", source: "xurl", user: xurlUser }
|
|
208
|
+
: { status: "miss", source: "xurl" };
|
|
209
|
+
}
|
|
183
210
|
return {
|
|
184
211
|
status: "error",
|
|
185
212
|
source: "xurl",
|
|
186
|
-
error:
|
|
213
|
+
error:
|
|
214
|
+
xurlResult.error instanceof Error
|
|
215
|
+
? xurlResult.error.message
|
|
216
|
+
: String(xurlResult.error),
|
|
187
217
|
};
|
|
188
|
-
}
|
|
218
|
+
});
|
|
189
219
|
}
|
|
190
220
|
|
|
191
|
-
|
|
221
|
+
function fetchProfileUsersEffect(
|
|
192
222
|
externalUserIds: string[],
|
|
193
223
|
xurlFallback: boolean,
|
|
194
224
|
) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
})
|
|
225
|
+
return Effect.gen(function* () {
|
|
226
|
+
const uniqueIds = Array.from(new Set(externalUserIds));
|
|
227
|
+
const results = new Map<string, CachedProfileLookup>();
|
|
228
|
+
let unresolved = uniqueIds;
|
|
229
|
+
|
|
230
|
+
const birdResult = yield* lookupProfilesViaBirdEffect(uniqueIds).pipe(
|
|
231
|
+
Effect.map((items) => ({ ok: true as const, items })),
|
|
232
|
+
Effect.catchAll((error) => Effect.succeed({ ok: false as const, error })),
|
|
233
|
+
);
|
|
234
|
+
if (birdResult.ok) {
|
|
235
|
+
const birdResults = birdResult.items;
|
|
236
|
+
for (const result of birdResults) {
|
|
237
|
+
const externalUserId = result.target;
|
|
238
|
+
if (result.user) {
|
|
239
|
+
results.set(externalUserId, {
|
|
240
|
+
status: "hit",
|
|
241
|
+
source: "bird",
|
|
242
|
+
user: result.user,
|
|
243
|
+
});
|
|
244
|
+
} else if (result.error && !xurlFallback) {
|
|
245
|
+
results.set(externalUserId, {
|
|
246
|
+
status: "error",
|
|
247
|
+
source: "bird",
|
|
248
|
+
error: result.error,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
215
251
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
} catch (error) {
|
|
219
|
-
if (!xurlFallback) {
|
|
252
|
+
unresolved = uniqueIds.filter((id) => !results.has(id));
|
|
253
|
+
} else if (!xurlFallback) {
|
|
220
254
|
for (const externalUserId of uniqueIds) {
|
|
221
255
|
results.set(externalUserId, {
|
|
222
256
|
status: "error",
|
|
223
257
|
source: "bird",
|
|
224
|
-
error:
|
|
258
|
+
error:
|
|
259
|
+
birdResult.error instanceof Error
|
|
260
|
+
? birdResult.error.message
|
|
261
|
+
: String(birdResult.error),
|
|
225
262
|
});
|
|
226
263
|
}
|
|
227
264
|
return results;
|
|
228
265
|
}
|
|
229
|
-
}
|
|
230
266
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
267
|
+
if (unresolved.length === 0) {
|
|
268
|
+
return results;
|
|
269
|
+
}
|
|
270
|
+
if (!xurlFallback) {
|
|
271
|
+
for (const externalUserId of unresolved) {
|
|
272
|
+
results.set(externalUserId, { status: "miss", source: "bird" });
|
|
273
|
+
}
|
|
274
|
+
return results;
|
|
237
275
|
}
|
|
238
|
-
return results;
|
|
239
|
-
}
|
|
240
276
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
277
|
+
const xurlResult = yield* tryPromise(() =>
|
|
278
|
+
lookupUsersByIds(unresolved),
|
|
279
|
+
).pipe(
|
|
280
|
+
Effect.map((users) => ({ ok: true as const, users })),
|
|
281
|
+
Effect.catchAll((error) => Effect.succeed({ ok: false as const, error })),
|
|
282
|
+
);
|
|
283
|
+
if (xurlResult.ok) {
|
|
284
|
+
const xurlUsers = xurlResult.users;
|
|
285
|
+
const usersById = new Map(
|
|
286
|
+
xurlUsers.map((user) => [String(user.id), user]),
|
|
251
287
|
);
|
|
288
|
+
for (const externalUserId of unresolved) {
|
|
289
|
+
const user = usersById.get(externalUserId);
|
|
290
|
+
results.set(
|
|
291
|
+
externalUserId,
|
|
292
|
+
user
|
|
293
|
+
? { status: "hit", source: "xurl", user }
|
|
294
|
+
: { status: "miss", source: "xurl" },
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
for (const externalUserId of unresolved) {
|
|
299
|
+
results.set(externalUserId, {
|
|
300
|
+
status: "error",
|
|
301
|
+
source: "xurl",
|
|
302
|
+
error:
|
|
303
|
+
xurlResult.error instanceof Error
|
|
304
|
+
? xurlResult.error.message
|
|
305
|
+
: String(xurlResult.error),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
252
308
|
}
|
|
253
|
-
} catch (error) {
|
|
254
|
-
for (const externalUserId of unresolved) {
|
|
255
|
-
results.set(externalUserId, {
|
|
256
|
-
status: "error",
|
|
257
|
-
source: "xurl",
|
|
258
|
-
error: error instanceof Error ? error.message : String(error),
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
309
|
|
|
263
|
-
|
|
310
|
+
return results;
|
|
311
|
+
});
|
|
264
312
|
}
|
|
265
313
|
|
|
266
|
-
export
|
|
314
|
+
export function resolveProfilesForIdsEffect(
|
|
267
315
|
profileIds: string[],
|
|
268
316
|
options: ResolveProfilesOptions = {},
|
|
269
|
-
):
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
options.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
localProfile &&
|
|
297
|
-
!options.refresh &&
|
|
298
|
-
!isPlaceholderProfile(localProfile)
|
|
299
|
-
) {
|
|
300
|
-
ordered.push({
|
|
301
|
-
kind: "ready",
|
|
302
|
-
result: {
|
|
303
|
-
profileId,
|
|
304
|
-
externalUserId,
|
|
305
|
-
status: "hit",
|
|
306
|
-
source: "local",
|
|
307
|
-
profile: localProfile,
|
|
308
|
-
},
|
|
309
|
-
});
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
317
|
+
): Effect.Effect<ProfileResolveResult[], unknown> {
|
|
318
|
+
return Effect.gen(function* () {
|
|
319
|
+
const db = yield* trySync(() => getNativeDb());
|
|
320
|
+
const maxAgeMs = options.maxAgeMs ?? PROFILE_CACHE_TTL_MS;
|
|
321
|
+
const negativeMaxAgeMs =
|
|
322
|
+
options.negativeMaxAgeMs ?? PROFILE_NEGATIVE_CACHE_TTL_MS;
|
|
323
|
+
const xurlFallback = options.xurlFallback ?? true;
|
|
324
|
+
const ordered: Array<
|
|
325
|
+
| { kind: "ready"; result: ProfileResolveResult }
|
|
326
|
+
| { kind: "pending"; profileId: string; externalUserId: string }
|
|
327
|
+
> = [];
|
|
328
|
+
|
|
329
|
+
for (const profileId of Array.from(new Set(profileIds))) {
|
|
330
|
+
const externalUserId = getExternalUserId(profileId);
|
|
331
|
+
if (!externalUserId) {
|
|
332
|
+
ordered.push({
|
|
333
|
+
kind: "ready",
|
|
334
|
+
result: {
|
|
335
|
+
profileId,
|
|
336
|
+
externalUserId: null,
|
|
337
|
+
status: "miss",
|
|
338
|
+
source: "local",
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
312
343
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (isFresh(cached.updatedAt, maxAge)) {
|
|
320
|
-
if (cached.value.status === "hit" && cached.value.user) {
|
|
321
|
-
const resolved = upsertProfileFromXUser(
|
|
322
|
-
getNativeDb(),
|
|
323
|
-
cached.value.user,
|
|
324
|
-
);
|
|
325
|
-
updateConversationTitles(resolved.profile);
|
|
326
|
-
ordered.push({
|
|
327
|
-
kind: "ready",
|
|
328
|
-
result: {
|
|
329
|
-
profileId: resolved.profile.id,
|
|
330
|
-
externalUserId,
|
|
331
|
-
status: "hit",
|
|
332
|
-
source: "cache",
|
|
333
|
-
profile: resolved.profile,
|
|
334
|
-
},
|
|
335
|
-
});
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
344
|
+
const localProfile = yield* trySync(() => getProfile(profileId));
|
|
345
|
+
if (
|
|
346
|
+
localProfile &&
|
|
347
|
+
!options.refresh &&
|
|
348
|
+
!isPlaceholderProfile(localProfile)
|
|
349
|
+
) {
|
|
338
350
|
ordered.push({
|
|
339
351
|
kind: "ready",
|
|
340
352
|
result: {
|
|
341
353
|
profileId,
|
|
342
354
|
externalUserId,
|
|
343
|
-
status:
|
|
344
|
-
source: "
|
|
345
|
-
|
|
355
|
+
status: "hit",
|
|
356
|
+
source: "local",
|
|
357
|
+
profile: localProfile,
|
|
346
358
|
},
|
|
347
359
|
});
|
|
348
360
|
continue;
|
|
349
361
|
}
|
|
350
|
-
}
|
|
351
362
|
|
|
352
|
-
|
|
353
|
-
|
|
363
|
+
const cached = yield* trySync(() =>
|
|
364
|
+
readSyncCache<CachedProfileLookup>(cacheKeyForUserId(externalUserId)),
|
|
365
|
+
);
|
|
366
|
+
if (cached && !options.refresh) {
|
|
367
|
+
const maxAge =
|
|
368
|
+
cached.value.status === "hit" ? maxAgeMs : negativeMaxAgeMs;
|
|
369
|
+
if (isFresh(cached.updatedAt, maxAge)) {
|
|
370
|
+
if (cached.value.status === "hit" && cached.value.user) {
|
|
371
|
+
const resolved = yield* trySync(() => {
|
|
372
|
+
const resolved = upsertProfileFromXUser(db, cached.value.user!);
|
|
373
|
+
updateConversationTitles(resolved.profile, db);
|
|
374
|
+
return resolved;
|
|
375
|
+
});
|
|
376
|
+
ordered.push({
|
|
377
|
+
kind: "ready",
|
|
378
|
+
result: {
|
|
379
|
+
profileId: resolved.profile.id,
|
|
380
|
+
externalUserId,
|
|
381
|
+
status: "hit",
|
|
382
|
+
source: "cache",
|
|
383
|
+
profile: resolved.profile,
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
ordered.push({
|
|
389
|
+
kind: "ready",
|
|
390
|
+
result: {
|
|
391
|
+
profileId,
|
|
392
|
+
externalUserId,
|
|
393
|
+
status: cached.value.status,
|
|
394
|
+
source: "negative-cache",
|
|
395
|
+
error: cached.value.error,
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
354
401
|
|
|
355
|
-
|
|
356
|
-
item.kind === "pending" ? [item.externalUserId] : [],
|
|
357
|
-
);
|
|
358
|
-
const fetchedByExternalId =
|
|
359
|
-
pendingExternalIds.length > 1
|
|
360
|
-
? await fetchProfileUsers(pendingExternalIds, xurlFallback)
|
|
361
|
-
: new Map<string, CachedProfileLookup>();
|
|
362
|
-
|
|
363
|
-
const results: ProfileResolveResult[] = [];
|
|
364
|
-
for (const item of ordered) {
|
|
365
|
-
if (item.kind === "ready") {
|
|
366
|
-
results.push(item.result);
|
|
367
|
-
continue;
|
|
402
|
+
ordered.push({ kind: "pending", profileId, externalUserId });
|
|
368
403
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
404
|
+
|
|
405
|
+
const pendingExternalIds = ordered.flatMap((item) =>
|
|
406
|
+
item.kind === "pending" ? [item.externalUserId] : [],
|
|
407
|
+
);
|
|
408
|
+
const fetchedByExternalId =
|
|
409
|
+
pendingExternalIds.length > 1
|
|
410
|
+
? yield* fetchProfileUsersEffect(pendingExternalIds, xurlFallback)
|
|
411
|
+
: new Map<string, CachedProfileLookup>();
|
|
412
|
+
|
|
413
|
+
const results: ProfileResolveResult[] = [];
|
|
414
|
+
for (const item of ordered) {
|
|
415
|
+
if (item.kind === "ready") {
|
|
416
|
+
results.push(item.result);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const fetched =
|
|
420
|
+
fetchedByExternalId.get(item.externalUserId) ??
|
|
421
|
+
(yield* fetchProfileUserEffect(item.externalUserId, xurlFallback));
|
|
422
|
+
yield* trySync(() =>
|
|
423
|
+
writeProfileLookupCache(item.externalUserId, fetched),
|
|
378
424
|
);
|
|
379
|
-
|
|
425
|
+
if (fetched.status === "hit" && fetched.user) {
|
|
426
|
+
const resolved = yield* trySync(() =>
|
|
427
|
+
upsertProfileFromXUser(db, fetched.user!),
|
|
428
|
+
);
|
|
429
|
+
const affiliationHydration =
|
|
430
|
+
yield* hydrateProfileAffiliationOrganizationsEffect(
|
|
431
|
+
db,
|
|
432
|
+
resolved.profile.id,
|
|
433
|
+
);
|
|
434
|
+
yield* trySync(() => updateConversationTitles(resolved.profile, db));
|
|
435
|
+
results.push({
|
|
436
|
+
profileId: resolved.profile.id,
|
|
437
|
+
externalUserId: item.externalUserId,
|
|
438
|
+
status: "hit",
|
|
439
|
+
source: fetched.source,
|
|
440
|
+
profile: resolved.profile,
|
|
441
|
+
...(affiliationHydration.checked > 0 ? { affiliationHydration } : {}),
|
|
442
|
+
});
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
380
445
|
results.push({
|
|
381
|
-
profileId:
|
|
446
|
+
profileId: item.profileId,
|
|
382
447
|
externalUserId: item.externalUserId,
|
|
383
|
-
status:
|
|
448
|
+
status: fetched.status,
|
|
384
449
|
source: fetched.source,
|
|
385
|
-
|
|
386
|
-
...(affiliationHydration.checked > 0 ? { affiliationHydration } : {}),
|
|
450
|
+
error: fetched.error,
|
|
387
451
|
});
|
|
388
|
-
continue;
|
|
389
452
|
}
|
|
390
|
-
results.push({
|
|
391
|
-
profileId: item.profileId,
|
|
392
|
-
externalUserId: item.externalUserId,
|
|
393
|
-
status: fetched.status,
|
|
394
|
-
source: fetched.source,
|
|
395
|
-
error: fetched.error,
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
453
|
|
|
399
|
-
|
|
454
|
+
return results;
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function resolveProfilesForIds(
|
|
459
|
+
profileIds: string[],
|
|
460
|
+
options: ResolveProfilesOptions = {},
|
|
461
|
+
): Promise<ProfileResolveResult[]> {
|
|
462
|
+
return runEffectPromise(resolveProfilesForIdsEffect(profileIds, options));
|
|
400
463
|
}
|
|
401
464
|
|
|
402
|
-
export
|
|
465
|
+
export function resolveProfilesForHandlesEffect(
|
|
403
466
|
handles: string[],
|
|
404
467
|
options: Pick<ResolveProfilesOptions, "xurlFallback"> = {},
|
|
405
|
-
):
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
468
|
+
): Effect.Effect<HandleProfileResolveResult[], unknown> {
|
|
469
|
+
return Effect.gen(function* () {
|
|
470
|
+
const db = yield* trySync(() => getNativeDb());
|
|
471
|
+
const xurlFallback = options.xurlFallback ?? true;
|
|
472
|
+
const targets = Array.from(
|
|
473
|
+
new Set(
|
|
474
|
+
handles.map(normalizeHandle).filter((handle) => handle.length > 0),
|
|
475
|
+
),
|
|
476
|
+
);
|
|
477
|
+
if (targets.length === 0) {
|
|
478
|
+
return [];
|
|
479
|
+
}
|
|
413
480
|
|
|
414
|
-
|
|
415
|
-
|
|
481
|
+
const results = new Map<string, HandleProfileResolveResult>();
|
|
482
|
+
let unresolved = targets;
|
|
416
483
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
484
|
+
const birdResult = yield* lookupProfilesViaBirdEffect(targets).pipe(
|
|
485
|
+
Effect.map((items) => ({ ok: true as const, items })),
|
|
486
|
+
Effect.catchAll((error) => Effect.succeed({ ok: false as const, error })),
|
|
487
|
+
);
|
|
488
|
+
if (birdResult.ok) {
|
|
489
|
+
const birdResults = birdResult.items;
|
|
490
|
+
for (const item of birdResults) {
|
|
491
|
+
const handle = normalizeHandle(item.target);
|
|
492
|
+
if (item.user) {
|
|
493
|
+
const resolved = yield* trySync(() => {
|
|
494
|
+
const resolved = upsertProfileFromXUser(db, item.user!);
|
|
495
|
+
updateConversationTitles(resolved.profile, db);
|
|
496
|
+
return resolved;
|
|
497
|
+
});
|
|
498
|
+
results.set(handle, {
|
|
499
|
+
handle,
|
|
500
|
+
status: "hit",
|
|
501
|
+
source: "bird",
|
|
502
|
+
profile: resolved.profile,
|
|
503
|
+
});
|
|
504
|
+
} else if (item.error && !xurlFallback) {
|
|
505
|
+
results.set(handle, {
|
|
506
|
+
handle,
|
|
507
|
+
status: "error",
|
|
508
|
+
source: "bird",
|
|
509
|
+
error: item.error,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
437
512
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
} catch (error) {
|
|
441
|
-
if (!xurlFallback) {
|
|
513
|
+
unresolved = targets.filter((handle) => !results.has(handle));
|
|
514
|
+
} else if (!xurlFallback) {
|
|
442
515
|
for (const handle of targets) {
|
|
443
516
|
results.set(handle, {
|
|
444
517
|
handle,
|
|
445
518
|
status: "error",
|
|
446
519
|
source: "bird",
|
|
447
|
-
error:
|
|
520
|
+
error:
|
|
521
|
+
birdResult.error instanceof Error
|
|
522
|
+
? birdResult.error.message
|
|
523
|
+
: String(birdResult.error),
|
|
448
524
|
});
|
|
449
525
|
}
|
|
450
526
|
unresolved = [];
|
|
451
527
|
}
|
|
452
|
-
}
|
|
453
528
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
529
|
+
if (unresolved.length > 0 && xurlFallback) {
|
|
530
|
+
const xurlResult = yield* tryPromise(() =>
|
|
531
|
+
lookupUsersByHandles(unresolved),
|
|
532
|
+
).pipe(
|
|
533
|
+
Effect.map((users) => ({ ok: true as const, users })),
|
|
534
|
+
Effect.catchAll((error) =>
|
|
535
|
+
Effect.succeed({ ok: false as const, error }),
|
|
536
|
+
),
|
|
462
537
|
);
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
538
|
+
if (xurlResult.ok) {
|
|
539
|
+
const users = xurlResult.users;
|
|
540
|
+
const usersByHandle = new Map(
|
|
541
|
+
users.map((user) => [
|
|
542
|
+
normalizeHandle(String(user.username ?? "")),
|
|
543
|
+
user,
|
|
544
|
+
]),
|
|
545
|
+
);
|
|
546
|
+
for (const handle of unresolved) {
|
|
547
|
+
const user = usersByHandle.get(handle);
|
|
548
|
+
if (user) {
|
|
549
|
+
const resolved = yield* trySync(() => {
|
|
550
|
+
const resolved = upsertProfileFromXUser(db, user);
|
|
551
|
+
updateConversationTitles(resolved.profile, db);
|
|
552
|
+
return resolved;
|
|
553
|
+
});
|
|
554
|
+
results.set(handle, {
|
|
555
|
+
handle,
|
|
556
|
+
status: "hit",
|
|
557
|
+
source: "xurl",
|
|
558
|
+
profile: resolved.profile,
|
|
559
|
+
});
|
|
560
|
+
} else {
|
|
561
|
+
results.set(handle, {
|
|
562
|
+
handle,
|
|
563
|
+
status: "miss",
|
|
564
|
+
source: "xurl",
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
for (const handle of unresolved) {
|
|
475
570
|
results.set(handle, {
|
|
476
571
|
handle,
|
|
477
|
-
status: "
|
|
572
|
+
status: "error",
|
|
478
573
|
source: "xurl",
|
|
574
|
+
error:
|
|
575
|
+
xurlResult.error instanceof Error
|
|
576
|
+
? xurlResult.error.message
|
|
577
|
+
: String(xurlResult.error),
|
|
479
578
|
});
|
|
480
579
|
}
|
|
481
580
|
}
|
|
482
|
-
} catch (error) {
|
|
483
|
-
for (const handle of unresolved) {
|
|
484
|
-
results.set(handle, {
|
|
485
|
-
handle,
|
|
486
|
-
status: "error",
|
|
487
|
-
source: "xurl",
|
|
488
|
-
error: error instanceof Error ? error.message : String(error),
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
581
|
}
|
|
492
|
-
}
|
|
493
582
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
583
|
+
return targets.map(
|
|
584
|
+
(handle) =>
|
|
585
|
+
results.get(handle) ?? {
|
|
586
|
+
handle,
|
|
587
|
+
status: "miss",
|
|
588
|
+
source: "bird",
|
|
589
|
+
},
|
|
590
|
+
);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export function resolveProfilesForHandles(
|
|
595
|
+
handles: string[],
|
|
596
|
+
options: Pick<ResolveProfilesOptions, "xurlFallback"> = {},
|
|
597
|
+
): Promise<HandleProfileResolveResult[]> {
|
|
598
|
+
return runEffectPromise(resolveProfilesForHandlesEffect(handles, options));
|
|
502
599
|
}
|
|
503
600
|
|
|
504
|
-
export
|
|
601
|
+
export function resolvePlaceholderProfilesEffect(
|
|
505
602
|
options: ResolveProfilesOptions & { limit?: number } = {},
|
|
506
603
|
) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
604
|
+
return Effect.gen(function* () {
|
|
605
|
+
const db = yield* trySync(() => getNativeDb());
|
|
606
|
+
const rows = yield* trySync(
|
|
607
|
+
() =>
|
|
608
|
+
db
|
|
609
|
+
.prepare(
|
|
610
|
+
`
|
|
511
611
|
select id
|
|
512
612
|
from profiles
|
|
513
613
|
where id like 'profile_user_%'
|
|
@@ -520,20 +620,28 @@ export async function resolvePlaceholderProfiles(
|
|
|
520
620
|
order by id asc
|
|
521
621
|
limit ?
|
|
522
622
|
`,
|
|
523
|
-
|
|
524
|
-
|
|
623
|
+
)
|
|
624
|
+
.all(options.limit ?? 500) as Array<{ id: string }>,
|
|
625
|
+
);
|
|
525
626
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
627
|
+
const results = yield* resolveProfilesForIdsEffect(
|
|
628
|
+
rows.map((row) => row.id),
|
|
629
|
+
options,
|
|
630
|
+
);
|
|
631
|
+
return {
|
|
632
|
+
ok: true,
|
|
633
|
+
requestedProfiles: rows.length,
|
|
634
|
+
hydratedProfiles: results.filter((result) => result.status === "hit")
|
|
635
|
+
.length,
|
|
636
|
+
results,
|
|
637
|
+
};
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export function resolvePlaceholderProfiles(
|
|
642
|
+
options: ResolveProfilesOptions & { limit?: number } = {},
|
|
643
|
+
) {
|
|
644
|
+
return runEffectPromise(resolvePlaceholderProfilesEffect(options));
|
|
537
645
|
}
|
|
538
646
|
|
|
539
647
|
export const __test__ = {
|