@mepuka/skygent 0.2.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/README.md +59 -0
- package/index.ts +146 -0
- package/package.json +56 -0
- package/src/cli/app.ts +75 -0
- package/src/cli/config-command.ts +140 -0
- package/src/cli/config.ts +91 -0
- package/src/cli/derive.ts +205 -0
- package/src/cli/doc/annotation.ts +36 -0
- package/src/cli/doc/filter.ts +69 -0
- package/src/cli/doc/index.ts +9 -0
- package/src/cli/doc/post.ts +155 -0
- package/src/cli/doc/primitives.ts +25 -0
- package/src/cli/doc/render.ts +18 -0
- package/src/cli/doc/table.ts +114 -0
- package/src/cli/doc/thread.ts +46 -0
- package/src/cli/doc/tree.ts +126 -0
- package/src/cli/errors.ts +59 -0
- package/src/cli/exit-codes.ts +52 -0
- package/src/cli/feed.ts +177 -0
- package/src/cli/filter-dsl.ts +1411 -0
- package/src/cli/filter-errors.ts +208 -0
- package/src/cli/filter-help.ts +70 -0
- package/src/cli/filter-input.ts +54 -0
- package/src/cli/filter.ts +435 -0
- package/src/cli/graph.ts +472 -0
- package/src/cli/help.ts +14 -0
- package/src/cli/interval.ts +35 -0
- package/src/cli/jetstream.ts +173 -0
- package/src/cli/layers.ts +180 -0
- package/src/cli/logging.ts +136 -0
- package/src/cli/output-format.ts +26 -0
- package/src/cli/output.ts +82 -0
- package/src/cli/parse.ts +80 -0
- package/src/cli/post.ts +193 -0
- package/src/cli/preferences.ts +11 -0
- package/src/cli/query-fields.ts +247 -0
- package/src/cli/query.ts +415 -0
- package/src/cli/range.ts +44 -0
- package/src/cli/search.ts +465 -0
- package/src/cli/shared-options.ts +169 -0
- package/src/cli/shared.ts +20 -0
- package/src/cli/store-errors.ts +80 -0
- package/src/cli/store-tree.ts +392 -0
- package/src/cli/store.ts +395 -0
- package/src/cli/sync-factory.ts +107 -0
- package/src/cli/sync.ts +366 -0
- package/src/cli/view-thread.ts +196 -0
- package/src/cli/view.ts +47 -0
- package/src/cli/watch.ts +344 -0
- package/src/db/migrations/store-catalog/001_init.ts +14 -0
- package/src/db/migrations/store-index/001_init.ts +34 -0
- package/src/db/migrations/store-index/002_event_log.ts +24 -0
- package/src/db/migrations/store-index/003_fts_and_derived.ts +52 -0
- package/src/db/migrations/store-index/004_query_indexes.ts +9 -0
- package/src/db/migrations/store-index/005_post_lang.ts +15 -0
- package/src/db/migrations/store-index/006_has_embed.ts +10 -0
- package/src/db/migrations/store-index/007_event_seq_and_checkpoints.ts +68 -0
- package/src/domain/bsky.ts +467 -0
- package/src/domain/config.ts +11 -0
- package/src/domain/credentials.ts +6 -0
- package/src/domain/defaults.ts +8 -0
- package/src/domain/derivation.ts +55 -0
- package/src/domain/errors.ts +71 -0
- package/src/domain/events.ts +55 -0
- package/src/domain/extract.ts +64 -0
- package/src/domain/filter-describe.ts +551 -0
- package/src/domain/filter-explain.ts +9 -0
- package/src/domain/filter.ts +797 -0
- package/src/domain/format.ts +91 -0
- package/src/domain/index.ts +13 -0
- package/src/domain/indexes.ts +17 -0
- package/src/domain/policies.ts +16 -0
- package/src/domain/post.ts +88 -0
- package/src/domain/primitives.ts +50 -0
- package/src/domain/raw.ts +140 -0
- package/src/domain/store.ts +103 -0
- package/src/domain/sync.ts +211 -0
- package/src/domain/text-width.ts +56 -0
- package/src/services/app-config.ts +278 -0
- package/src/services/bsky-client.ts +2113 -0
- package/src/services/credential-store.ts +408 -0
- package/src/services/derivation-engine.ts +502 -0
- package/src/services/derivation-settings.ts +61 -0
- package/src/services/derivation-validator.ts +68 -0
- package/src/services/filter-compiler.ts +269 -0
- package/src/services/filter-library.ts +371 -0
- package/src/services/filter-runtime.ts +821 -0
- package/src/services/filter-settings.ts +30 -0
- package/src/services/identity-resolver.ts +563 -0
- package/src/services/jetstream-sync.ts +636 -0
- package/src/services/lineage-store.ts +89 -0
- package/src/services/link-validator.ts +244 -0
- package/src/services/output-manager.ts +274 -0
- package/src/services/post-parser.ts +62 -0
- package/src/services/profile-resolver.ts +223 -0
- package/src/services/resource-monitor.ts +106 -0
- package/src/services/shared.ts +69 -0
- package/src/services/store-cleaner.ts +43 -0
- package/src/services/store-commit.ts +168 -0
- package/src/services/store-db.ts +248 -0
- package/src/services/store-event-log.ts +285 -0
- package/src/services/store-index-sql.ts +289 -0
- package/src/services/store-index.ts +1152 -0
- package/src/services/store-keys.ts +4 -0
- package/src/services/store-manager.ts +358 -0
- package/src/services/store-stats.ts +522 -0
- package/src/services/store-writer.ts +200 -0
- package/src/services/sync-checkpoint-store.ts +169 -0
- package/src/services/sync-engine.ts +547 -0
- package/src/services/sync-reporter.ts +16 -0
- package/src/services/sync-settings.ts +72 -0
- package/src/services/trending-topics.ts +226 -0
- package/src/services/view-checkpoint-store.ts +238 -0
- package/src/typeclass/chunk.ts +84 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Config, Context, Effect, Layer } from "effect";
|
|
2
|
+
import { validatePositive } from "./shared.js";
|
|
3
|
+
|
|
4
|
+
export type FilterSettingsValue = {
|
|
5
|
+
readonly concurrency: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export class FilterSettings extends Context.Tag("@skygent/FilterSettings")<
|
|
9
|
+
FilterSettings,
|
|
10
|
+
FilterSettingsValue
|
|
11
|
+
>() {
|
|
12
|
+
static readonly layer = Layer.effect(
|
|
13
|
+
FilterSettings,
|
|
14
|
+
Effect.gen(function* () {
|
|
15
|
+
const concurrency = yield* Config.integer("SKYGENT_FILTER_CONCURRENCY").pipe(
|
|
16
|
+
Config.withDefault(10)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const concurrencyError = validatePositive(
|
|
20
|
+
"SKYGENT_FILTER_CONCURRENCY",
|
|
21
|
+
concurrency
|
|
22
|
+
);
|
|
23
|
+
if (concurrencyError) {
|
|
24
|
+
return yield* concurrencyError;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return FilterSettings.of({ concurrency });
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity Resolver Service
|
|
3
|
+
*
|
|
4
|
+
* Resolves Bluesky handles to DIDs (Decentralized Identifiers) and vice versa.
|
|
5
|
+
* Provides identity resolution with multiple caching layers for performance:
|
|
6
|
+
* - Persistent key-value store cache for long-term caching
|
|
7
|
+
* - In-memory request cache for short-term deduplication
|
|
8
|
+
*
|
|
9
|
+
* Supports various resolution strategies:
|
|
10
|
+
* - Direct handle resolution via resolveHandle API
|
|
11
|
+
* - Identity resolution via resolveIdentity API (with verification)
|
|
12
|
+
* - Profile-based resolution for handles from DIDs
|
|
13
|
+
*
|
|
14
|
+
* Handles edge cases like deactivated DIDs, invalid handles, and not-found errors
|
|
15
|
+
* with appropriate caching to avoid repeated failed requests.
|
|
16
|
+
*
|
|
17
|
+
* @module services/identity-resolver
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as KeyValueStore from "@effect/platform/KeyValueStore";
|
|
21
|
+
import {
|
|
22
|
+
Cache,
|
|
23
|
+
Clock,
|
|
24
|
+
Config,
|
|
25
|
+
Context,
|
|
26
|
+
Duration,
|
|
27
|
+
Effect,
|
|
28
|
+
Layer,
|
|
29
|
+
Option,
|
|
30
|
+
ParseResult,
|
|
31
|
+
Schema
|
|
32
|
+
} from "effect";
|
|
33
|
+
import { IdentityInfo } from "../domain/bsky.js";
|
|
34
|
+
import { BskyError } from "../domain/errors.js";
|
|
35
|
+
import { Did, Handle } from "../domain/primitives.js";
|
|
36
|
+
import { formatSchemaError, messageFromCause } from "./shared.js";
|
|
37
|
+
import { BskyClient } from "./bsky-client.js";
|
|
38
|
+
|
|
39
|
+
const cachePrefixHandle = "cache/identity/handle/";
|
|
40
|
+
const cachePrefixDid = "cache/identity/did/";
|
|
41
|
+
|
|
42
|
+
const normalizeHandleInput = (value: string) => {
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
const normalized = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
45
|
+
return normalized.toLowerCase();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const isHandleInvalid = (handle: Handle) => handle === "handle.invalid";
|
|
49
|
+
|
|
50
|
+
const toIdentityError = (message: string, operation?: string) => (cause: unknown) =>
|
|
51
|
+
BskyError.make({
|
|
52
|
+
message: messageFromCause(message, cause),
|
|
53
|
+
cause,
|
|
54
|
+
...(operation ? { operation } : {})
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const decodeHandle = (value: string) =>
|
|
58
|
+
Schema.decodeUnknown(Handle)(normalizeHandleInput(value)).pipe(
|
|
59
|
+
Effect.mapError((error) =>
|
|
60
|
+
BskyError.make({
|
|
61
|
+
message: `Invalid handle: ${formatSchemaError(error)}`,
|
|
62
|
+
cause: { handle: value },
|
|
63
|
+
operation: "identityDecodeHandle"
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const decodeDid = (value: string) =>
|
|
69
|
+
Schema.decodeUnknown(Did)(value).pipe(
|
|
70
|
+
Effect.mapError((error) =>
|
|
71
|
+
BskyError.make({
|
|
72
|
+
message: `Invalid DID: ${formatSchemaError(error)}`,
|
|
73
|
+
cause: { did: value },
|
|
74
|
+
operation: "identityDecodeDid"
|
|
75
|
+
})
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
class IdentityCacheEntry extends Schema.Class<IdentityCacheEntry>("IdentityCacheEntry")({
|
|
80
|
+
did: Schema.optional(Did),
|
|
81
|
+
handle: Schema.optional(Handle),
|
|
82
|
+
verified: Schema.Boolean,
|
|
83
|
+
status: Schema.Literal("resolved", "not_found", "deactivated", "invalid"),
|
|
84
|
+
source: Schema.Literal("resolveHandle", "getProfiles", "resolveIdentity"),
|
|
85
|
+
checkedAt: Schema.DateFromString
|
|
86
|
+
}) {}
|
|
87
|
+
|
|
88
|
+
type IdentityStatus = IdentityCacheEntry["status"];
|
|
89
|
+
|
|
90
|
+
type CacheStores = {
|
|
91
|
+
readonly handleStore: KeyValueStore.SchemaStore<IdentityCacheEntry, never>;
|
|
92
|
+
readonly didStore: KeyValueStore.SchemaStore<IdentityCacheEntry, never>;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const cacheKey = (value: string) => encodeURIComponent(value);
|
|
96
|
+
|
|
97
|
+
const makeCacheStores = (kv: KeyValueStore.KeyValueStore): CacheStores => {
|
|
98
|
+
const store = kv.forSchema(IdentityCacheEntry);
|
|
99
|
+
return {
|
|
100
|
+
handleStore: KeyValueStore.prefix(store, cachePrefixHandle),
|
|
101
|
+
didStore: KeyValueStore.prefix(store, cachePrefixDid)
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const entryStatusError = (
|
|
106
|
+
status: IdentityStatus,
|
|
107
|
+
identifier: string,
|
|
108
|
+
kind: "handle" | "did"
|
|
109
|
+
) => {
|
|
110
|
+
if (status === "not_found") {
|
|
111
|
+
return BskyError.make({
|
|
112
|
+
message:
|
|
113
|
+
kind === "handle"
|
|
114
|
+
? `Handle not found: ${identifier}`
|
|
115
|
+
: `DID not found: ${identifier}`,
|
|
116
|
+
error: kind === "handle" ? "HandleNotFound" : "DidNotFound",
|
|
117
|
+
operation: kind === "handle" ? "resolveDid" : "resolveHandle"
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (status === "deactivated") {
|
|
121
|
+
return BskyError.make({
|
|
122
|
+
message: `DID deactivated: ${identifier}`,
|
|
123
|
+
error: "DidDeactivated",
|
|
124
|
+
operation: "resolveHandle"
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return BskyError.make({
|
|
128
|
+
message: `Handle invalid for ${identifier}`,
|
|
129
|
+
error: "HandleInvalid",
|
|
130
|
+
operation: kind === "handle" ? "resolveDid" : "resolveHandle"
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Service for resolving Bluesky identities (handles <-> DIDs).
|
|
136
|
+
*
|
|
137
|
+
* Provides bidirectional resolution between handles and DIDs with comprehensive
|
|
138
|
+
* caching. Lookup methods check cache only, while resolve methods will fetch
|
|
139
|
+
* from the network if not cached.
|
|
140
|
+
*/
|
|
141
|
+
export class IdentityResolver extends Context.Tag("@skygent/IdentityResolver")<
|
|
142
|
+
IdentityResolver,
|
|
143
|
+
{
|
|
144
|
+
/**
|
|
145
|
+
* Looks up a DID from cache by handle (cache-only, no network request).
|
|
146
|
+
*
|
|
147
|
+
* @param handle - The handle to look up (e.g., "alice.bsky.social")
|
|
148
|
+
* @returns Effect resolving to Option of DID, or BskyError on cache failure
|
|
149
|
+
*/
|
|
150
|
+
readonly lookupDid: (handle: string) => Effect.Effect<Option.Option<Did>, BskyError>;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Looks up a handle from cache by DID (cache-only, no network request).
|
|
154
|
+
*
|
|
155
|
+
* @param did - The DID to look up (e.g., "did:plc:...")
|
|
156
|
+
* @returns Effect resolving to Option of Handle, or BskyError on cache failure
|
|
157
|
+
*/
|
|
158
|
+
readonly lookupHandle: (did: string) => Effect.Effect<Option.Option<Handle>, BskyError>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Resolves a handle to a DID, fetching from network if not cached.
|
|
162
|
+
*
|
|
163
|
+
* @param handle - The handle to resolve
|
|
164
|
+
* @returns Effect resolving to DID, or BskyError on resolution failure
|
|
165
|
+
*/
|
|
166
|
+
readonly resolveDid: (handle: string) => Effect.Effect<Did, BskyError>;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Resolves a DID to a handle, fetching from network if not cached.
|
|
170
|
+
*
|
|
171
|
+
* @param did - The DID to resolve
|
|
172
|
+
* @returns Effect resolving to Handle, or BskyError on resolution failure
|
|
173
|
+
*/
|
|
174
|
+
readonly resolveHandle: (did: string) => Effect.Effect<Handle, BskyError>;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Resolves an identity (handle or DID) to full identity info with verification.
|
|
178
|
+
*
|
|
179
|
+
* @param identifier - The handle or DID to resolve
|
|
180
|
+
* @returns Effect resolving to IdentityInfo with did and handle, or BskyError
|
|
181
|
+
*/
|
|
182
|
+
readonly resolveIdentity: (
|
|
183
|
+
identifier: string
|
|
184
|
+
) => Effect.Effect<IdentityInfo, BskyError>;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Manually caches a profile's identity information.
|
|
188
|
+
*
|
|
189
|
+
* @param input - Object containing did, handle, and optional verified flag and source
|
|
190
|
+
* @returns Effect resolving to void, or BskyError on cache failure
|
|
191
|
+
*/
|
|
192
|
+
readonly cacheProfile: (input: {
|
|
193
|
+
readonly did: Did;
|
|
194
|
+
readonly handle: Handle;
|
|
195
|
+
readonly verified?: boolean;
|
|
196
|
+
readonly source?: IdentityCacheEntry["source"];
|
|
197
|
+
}) => Effect.Effect<void, BskyError>;
|
|
198
|
+
}
|
|
199
|
+
>() {
|
|
200
|
+
static readonly layer = Layer.effect(
|
|
201
|
+
IdentityResolver,
|
|
202
|
+
Effect.gen(function* () {
|
|
203
|
+
const bsky = yield* BskyClient;
|
|
204
|
+
const kv = yield* KeyValueStore.KeyValueStore;
|
|
205
|
+
const { handleStore, didStore } = makeCacheStores(kv);
|
|
206
|
+
|
|
207
|
+
const cacheTtl = yield* Config.duration("SKYGENT_IDENTITY_CACHE_TTL").pipe(
|
|
208
|
+
Config.withDefault(Duration.hours(24))
|
|
209
|
+
);
|
|
210
|
+
const failureTtl = yield* Config.duration("SKYGENT_IDENTITY_FAILURE_TTL").pipe(
|
|
211
|
+
Config.withDefault(Duration.minutes(5))
|
|
212
|
+
);
|
|
213
|
+
const strict = yield* Config.boolean("SKYGENT_IDENTITY_STRICT").pipe(
|
|
214
|
+
Config.withDefault(false)
|
|
215
|
+
);
|
|
216
|
+
const requestCapacity = yield* Config.integer(
|
|
217
|
+
"SKYGENT_IDENTITY_REQUEST_CACHE_CAPACITY"
|
|
218
|
+
).pipe(Config.withDefault(5000));
|
|
219
|
+
|
|
220
|
+
const successTtlMs = Duration.toMillis(cacheTtl);
|
|
221
|
+
const failureTtlMs = Duration.toMillis(failureTtl);
|
|
222
|
+
|
|
223
|
+
const entryTtl = (entry: IdentityCacheEntry) =>
|
|
224
|
+
entry.status === "resolved" ? cacheTtl : failureTtl;
|
|
225
|
+
|
|
226
|
+
const isFresh = (entry: IdentityCacheEntry, now: number) => {
|
|
227
|
+
const ttlMs = Duration.toMillis(entryTtl(entry));
|
|
228
|
+
return ttlMs > 0 && now - entry.checkedAt.getTime() < ttlMs;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const shouldPersist = (entry: IdentityCacheEntry) =>
|
|
232
|
+
Duration.toMillis(entryTtl(entry)) > 0;
|
|
233
|
+
|
|
234
|
+
const readEntry = (
|
|
235
|
+
store: KeyValueStore.SchemaStore<IdentityCacheEntry, never>,
|
|
236
|
+
key: string,
|
|
237
|
+
now: number
|
|
238
|
+
) =>
|
|
239
|
+
store.get(key).pipe(
|
|
240
|
+
Effect.catchAll((error) =>
|
|
241
|
+
ParseResult.isParseError(error)
|
|
242
|
+
? Effect.succeed(Option.none())
|
|
243
|
+
: Effect.fail(
|
|
244
|
+
toIdentityError("Identity cache read failed", "identityCacheRead")(
|
|
245
|
+
error
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
),
|
|
249
|
+
Effect.map((cached) =>
|
|
250
|
+
Option.filter(cached, (entry) =>
|
|
251
|
+
isFresh(entry, now) && (!strict || entry.verified)
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const writeEntry = (
|
|
257
|
+
store: KeyValueStore.SchemaStore<IdentityCacheEntry, never>,
|
|
258
|
+
key: string,
|
|
259
|
+
entry: IdentityCacheEntry
|
|
260
|
+
) =>
|
|
261
|
+
store
|
|
262
|
+
.set(key, entry)
|
|
263
|
+
.pipe(
|
|
264
|
+
Effect.mapError(
|
|
265
|
+
toIdentityError("Identity cache write failed", "identityCacheWrite")
|
|
266
|
+
)
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const writeResolvedEntry = (entry: IdentityCacheEntry) => {
|
|
270
|
+
if (!shouldPersist(entry)) {
|
|
271
|
+
return Effect.void;
|
|
272
|
+
}
|
|
273
|
+
const effects: Array<Effect.Effect<void, BskyError>> = [];
|
|
274
|
+
if (entry.handle) {
|
|
275
|
+
effects.push(
|
|
276
|
+
writeEntry(handleStore, cacheKey(entry.handle), entry)
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
if (entry.did) {
|
|
280
|
+
effects.push(writeEntry(didStore, cacheKey(entry.did), entry));
|
|
281
|
+
}
|
|
282
|
+
if (effects.length === 0) {
|
|
283
|
+
return Effect.void;
|
|
284
|
+
}
|
|
285
|
+
return Effect.all(effects, { discard: true });
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const cacheProfile = Effect.fn("IdentityResolver.cacheProfile")(
|
|
289
|
+
(input: {
|
|
290
|
+
readonly did: Did;
|
|
291
|
+
readonly handle: Handle;
|
|
292
|
+
readonly verified?: boolean;
|
|
293
|
+
readonly source?: IdentityCacheEntry["source"];
|
|
294
|
+
}) =>
|
|
295
|
+
Effect.gen(function* () {
|
|
296
|
+
const now = yield* Clock.currentTimeMillis;
|
|
297
|
+
const entry = IdentityCacheEntry.make({
|
|
298
|
+
did: input.did,
|
|
299
|
+
handle: input.handle,
|
|
300
|
+
verified: input.verified ?? false,
|
|
301
|
+
status: "resolved",
|
|
302
|
+
source: input.source ?? "getProfiles",
|
|
303
|
+
checkedAt: new Date(now)
|
|
304
|
+
});
|
|
305
|
+
yield* writeResolvedEntry(entry);
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const writeNegativeEntry = (
|
|
310
|
+
entry: IdentityCacheEntry,
|
|
311
|
+
target: "handle" | "did",
|
|
312
|
+
key: string
|
|
313
|
+
) => {
|
|
314
|
+
if (!shouldPersist(entry)) {
|
|
315
|
+
return Effect.void;
|
|
316
|
+
}
|
|
317
|
+
const store = target === "handle" ? handleStore : didStore;
|
|
318
|
+
return writeEntry(store, cacheKey(key), entry).pipe(Effect.asVoid);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const resolveDidFromCache = (handle: Handle) =>
|
|
322
|
+
Effect.gen(function* () {
|
|
323
|
+
const now = yield* Clock.currentTimeMillis;
|
|
324
|
+
const cached = yield* readEntry(handleStore, cacheKey(handle), now);
|
|
325
|
+
if (Option.isNone(cached)) {
|
|
326
|
+
return Option.none<Did>();
|
|
327
|
+
}
|
|
328
|
+
const entry = cached.value;
|
|
329
|
+
if (entry.status === "resolved") {
|
|
330
|
+
return entry.did ? Option.some(entry.did) : Option.none<Did>();
|
|
331
|
+
}
|
|
332
|
+
const identifier =
|
|
333
|
+
entry.status === "deactivated" && entry.did ? entry.did : handle;
|
|
334
|
+
const kind = entry.status === "deactivated" ? "did" : "handle";
|
|
335
|
+
return yield* entryStatusError(entry.status, identifier, kind);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const resolveHandleFromCache = (did: Did) =>
|
|
339
|
+
Effect.gen(function* () {
|
|
340
|
+
const now = yield* Clock.currentTimeMillis;
|
|
341
|
+
const cached = yield* readEntry(didStore, cacheKey(did), now);
|
|
342
|
+
if (Option.isNone(cached)) {
|
|
343
|
+
return Option.none<Handle>();
|
|
344
|
+
}
|
|
345
|
+
const entry = cached.value;
|
|
346
|
+
if (entry.status === "resolved") {
|
|
347
|
+
return entry.handle ? Option.some(entry.handle) : Option.none<Handle>();
|
|
348
|
+
}
|
|
349
|
+
return yield* entryStatusError(entry.status, did, "did");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const resolveViaResolveIdentity = (identifier: string) =>
|
|
353
|
+
Effect.gen(function* () {
|
|
354
|
+
const info = yield* bsky.resolveIdentity(identifier);
|
|
355
|
+
const now = yield* Clock.currentTimeMillis;
|
|
356
|
+
const invalid = isHandleInvalid(info.handle);
|
|
357
|
+
const entry = IdentityCacheEntry.make({
|
|
358
|
+
did: info.did,
|
|
359
|
+
...(invalid ? {} : { handle: info.handle }),
|
|
360
|
+
verified: !invalid,
|
|
361
|
+
status: invalid ? "invalid" : "resolved",
|
|
362
|
+
source: "resolveIdentity",
|
|
363
|
+
checkedAt: new Date(now)
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (invalid) {
|
|
367
|
+
const key = identifier.startsWith("did:") ? info.did : identifier;
|
|
368
|
+
const target = identifier.startsWith("did:") ? "did" : "handle";
|
|
369
|
+
yield* writeNegativeEntry(entry, target, key);
|
|
370
|
+
return yield* entryStatusError("invalid", identifier, target);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
yield* writeResolvedEntry(entry);
|
|
374
|
+
return info;
|
|
375
|
+
}).pipe(
|
|
376
|
+
Effect.catchTag("BskyError", (error) =>
|
|
377
|
+
Effect.gen(function* () {
|
|
378
|
+
if (
|
|
379
|
+
error.error === "HandleNotFound" ||
|
|
380
|
+
error.error === "DidNotFound" ||
|
|
381
|
+
error.error === "DidDeactivated"
|
|
382
|
+
) {
|
|
383
|
+
const now = yield* Clock.currentTimeMillis;
|
|
384
|
+
const isDid = identifier.startsWith("did:");
|
|
385
|
+
const status =
|
|
386
|
+
error.error === "DidDeactivated" ? "deactivated" : "not_found";
|
|
387
|
+
const entry = IdentityCacheEntry.make({
|
|
388
|
+
...(isDid ? { did: identifier as Did } : { handle: identifier as Handle }),
|
|
389
|
+
verified: false,
|
|
390
|
+
status,
|
|
391
|
+
source: "resolveIdentity",
|
|
392
|
+
checkedAt: new Date(now)
|
|
393
|
+
});
|
|
394
|
+
yield* writeNegativeEntry(entry, isDid ? "did" : "handle", identifier);
|
|
395
|
+
}
|
|
396
|
+
return yield* error;
|
|
397
|
+
})
|
|
398
|
+
)
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const resolveDidUncached = (handle: Handle) =>
|
|
402
|
+
Effect.gen(function* () {
|
|
403
|
+
const cached = yield* resolveDidFromCache(handle);
|
|
404
|
+
if (Option.isSome(cached)) {
|
|
405
|
+
return cached.value;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (strict) {
|
|
409
|
+
const info = yield* resolveViaResolveIdentity(handle);
|
|
410
|
+
return info.did;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const did = yield* bsky.resolveHandle(handle).pipe(
|
|
414
|
+
Effect.catchTag("BskyError", (error) =>
|
|
415
|
+
Effect.gen(function* () {
|
|
416
|
+
if (error.error === "HandleNotFound") {
|
|
417
|
+
const now = yield* Clock.currentTimeMillis;
|
|
418
|
+
const entry = IdentityCacheEntry.make({
|
|
419
|
+
handle,
|
|
420
|
+
verified: false,
|
|
421
|
+
status: "not_found",
|
|
422
|
+
source: "resolveHandle",
|
|
423
|
+
checkedAt: new Date(now)
|
|
424
|
+
});
|
|
425
|
+
yield* writeNegativeEntry(entry, "handle", handle);
|
|
426
|
+
}
|
|
427
|
+
return yield* error;
|
|
428
|
+
})
|
|
429
|
+
)
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
const now = yield* Clock.currentTimeMillis;
|
|
433
|
+
const entry = IdentityCacheEntry.make({
|
|
434
|
+
did,
|
|
435
|
+
handle,
|
|
436
|
+
verified: false,
|
|
437
|
+
status: "resolved",
|
|
438
|
+
source: "resolveHandle",
|
|
439
|
+
checkedAt: new Date(now)
|
|
440
|
+
});
|
|
441
|
+
yield* writeResolvedEntry(entry);
|
|
442
|
+
return did;
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const resolveHandleUncached = (did: Did) =>
|
|
446
|
+
Effect.gen(function* () {
|
|
447
|
+
const cached = yield* resolveHandleFromCache(did);
|
|
448
|
+
if (Option.isSome(cached)) {
|
|
449
|
+
return cached.value;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (strict) {
|
|
453
|
+
const info = yield* resolveViaResolveIdentity(did);
|
|
454
|
+
return info.handle;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const profiles = yield* bsky.getProfiles([did]);
|
|
458
|
+
const profile = profiles[0];
|
|
459
|
+
if (!profile) {
|
|
460
|
+
const error = BskyError.make({
|
|
461
|
+
message: `Profile not found for DID ${did}`,
|
|
462
|
+
error: "ProfileNotFound",
|
|
463
|
+
operation: "getProfiles"
|
|
464
|
+
});
|
|
465
|
+
const now = yield* Clock.currentTimeMillis;
|
|
466
|
+
const entry = IdentityCacheEntry.make({
|
|
467
|
+
did,
|
|
468
|
+
verified: false,
|
|
469
|
+
status: "not_found",
|
|
470
|
+
source: "getProfiles",
|
|
471
|
+
checkedAt: new Date(now)
|
|
472
|
+
});
|
|
473
|
+
yield* writeNegativeEntry(entry, "did", did);
|
|
474
|
+
return yield* error;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const now = yield* Clock.currentTimeMillis;
|
|
478
|
+
const entry = IdentityCacheEntry.make({
|
|
479
|
+
did,
|
|
480
|
+
handle: profile.handle,
|
|
481
|
+
verified: false,
|
|
482
|
+
status: "resolved",
|
|
483
|
+
source: "getProfiles",
|
|
484
|
+
checkedAt: new Date(now)
|
|
485
|
+
});
|
|
486
|
+
yield* writeResolvedEntry(entry);
|
|
487
|
+
return profile.handle;
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const makeRequestCache = <K, V>(
|
|
491
|
+
lookup: (key: K) => Effect.Effect<V, BskyError>
|
|
492
|
+
) => {
|
|
493
|
+
if (requestCapacity <= 0 || (successTtlMs <= 0 && failureTtlMs <= 0)) {
|
|
494
|
+
return Effect.succeed(Option.none<Cache.Cache<K, V, BskyError>>());
|
|
495
|
+
}
|
|
496
|
+
return Cache.makeWith({
|
|
497
|
+
capacity: requestCapacity,
|
|
498
|
+
lookup,
|
|
499
|
+
timeToLive: (exit) =>
|
|
500
|
+
exit._tag === "Failure" ? failureTtl : cacheTtl
|
|
501
|
+
}).pipe(Effect.map(Option.some));
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const resolveDidCache = yield* makeRequestCache(resolveDidUncached);
|
|
505
|
+
const resolveHandleCache = yield* makeRequestCache(resolveHandleUncached);
|
|
506
|
+
|
|
507
|
+
const resolveDid = Effect.fn("IdentityResolver.resolveDid")((handle: string) =>
|
|
508
|
+
Effect.gen(function* () {
|
|
509
|
+
const normalized = yield* decodeHandle(handle);
|
|
510
|
+
const effect = Option.match(resolveDidCache, {
|
|
511
|
+
onNone: () => resolveDidUncached(normalized),
|
|
512
|
+
onSome: (cache) => cache.get(normalized)
|
|
513
|
+
});
|
|
514
|
+
return yield* effect;
|
|
515
|
+
})
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const lookupDid = Effect.fn("IdentityResolver.lookupDid")((handle: string) =>
|
|
519
|
+
Effect.gen(function* () {
|
|
520
|
+
const normalized = yield* decodeHandle(handle);
|
|
521
|
+
return yield* resolveDidFromCache(normalized);
|
|
522
|
+
})
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
const resolveHandle = Effect.fn("IdentityResolver.resolveHandle")((did: string) =>
|
|
526
|
+
Effect.gen(function* () {
|
|
527
|
+
const normalized = yield* decodeDid(did);
|
|
528
|
+
const effect = Option.match(resolveHandleCache, {
|
|
529
|
+
onNone: () => resolveHandleUncached(normalized),
|
|
530
|
+
onSome: (cache) => cache.get(normalized)
|
|
531
|
+
});
|
|
532
|
+
return yield* effect;
|
|
533
|
+
})
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const lookupHandle = Effect.fn("IdentityResolver.lookupHandle")((did: string) =>
|
|
537
|
+
Effect.gen(function* () {
|
|
538
|
+
const normalized = yield* decodeDid(did);
|
|
539
|
+
return yield* resolveHandleFromCache(normalized);
|
|
540
|
+
})
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
const resolveIdentity = Effect.fn("IdentityResolver.resolveIdentity")(
|
|
544
|
+
(identifier: string) =>
|
|
545
|
+
Effect.gen(function* () {
|
|
546
|
+
const normalized = identifier.startsWith("did:")
|
|
547
|
+
? yield* decodeDid(identifier)
|
|
548
|
+
: yield* decodeHandle(identifier);
|
|
549
|
+
return yield* resolveViaResolveIdentity(normalized);
|
|
550
|
+
})
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
return IdentityResolver.of({
|
|
554
|
+
lookupDid,
|
|
555
|
+
lookupHandle,
|
|
556
|
+
resolveDid,
|
|
557
|
+
resolveHandle,
|
|
558
|
+
resolveIdentity,
|
|
559
|
+
cacheProfile
|
|
560
|
+
});
|
|
561
|
+
})
|
|
562
|
+
);
|
|
563
|
+
}
|