@hatk/hatk 0.0.1-alpha.7 → 0.0.1-alpha.70
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/dist/adapter.d.ts +19 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +108 -0
- package/dist/backfill.d.ts +21 -2
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +116 -43
- package/dist/car.d.ts +42 -10
- package/dist/car.d.ts.map +1 -1
- package/dist/car.js +154 -14
- package/dist/cli.js +243 -1043
- package/dist/config.d.ts +32 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +41 -9
- package/dist/database/adapter-factory.d.ts +6 -0
- package/dist/database/adapter-factory.d.ts.map +1 -0
- package/dist/database/adapter-factory.js +20 -0
- package/dist/database/adapters/duckdb-search.d.ts +12 -0
- package/dist/database/adapters/duckdb-search.d.ts.map +1 -0
- package/dist/database/adapters/duckdb-search.js +27 -0
- package/dist/database/adapters/duckdb.d.ts +25 -0
- package/dist/database/adapters/duckdb.d.ts.map +1 -0
- package/dist/database/adapters/duckdb.js +161 -0
- package/dist/database/adapters/sqlite-search.d.ts +23 -0
- package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
- package/dist/database/adapters/sqlite-search.js +74 -0
- package/dist/database/adapters/sqlite.d.ts +18 -0
- package/dist/database/adapters/sqlite.d.ts.map +1 -0
- package/dist/database/adapters/sqlite.js +88 -0
- package/dist/{db.d.ts → database/db.d.ts} +57 -6
- package/dist/database/db.d.ts.map +1 -0
- package/dist/{db.js → database/db.js} +755 -552
- package/dist/database/dialect.d.ts +45 -0
- package/dist/database/dialect.d.ts.map +1 -0
- package/dist/database/dialect.js +72 -0
- package/dist/{fts.d.ts → database/fts.d.ts} +7 -0
- package/dist/database/fts.d.ts.map +1 -0
- package/dist/{fts.js → database/fts.js} +116 -32
- package/dist/database/index.d.ts +7 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +6 -0
- package/dist/database/ports.d.ts +50 -0
- package/dist/database/ports.d.ts.map +1 -0
- package/dist/database/ports.js +1 -0
- package/dist/{schema.d.ts → database/schema.d.ts} +14 -3
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/{schema.js → database/schema.js} +81 -41
- package/dist/dev-entry.d.ts +8 -0
- package/dist/dev-entry.d.ts.map +1 -0
- package/dist/dev-entry.js +119 -0
- package/dist/feeds.d.ts +12 -8
- package/dist/feeds.d.ts.map +1 -1
- package/dist/feeds.js +51 -6
- package/dist/hooks.d.ts +85 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +163 -0
- package/dist/hydrate.d.ts +7 -6
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +4 -16
- package/dist/indexer.d.ts +43 -0
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +246 -38
- package/dist/labels.d.ts +36 -0
- package/dist/labels.d.ts.map +1 -1
- package/dist/labels.js +71 -6
- package/dist/lexicon-resolve.d.ts.map +1 -1
- package/dist/lexicon-resolve.js +27 -112
- package/dist/lexicons/com/atproto/label/defs.json +75 -0
- package/dist/lexicons/com/atproto/moderation/defs.json +30 -0
- package/dist/lexicons/com/atproto/repo/strongRef.json +21 -0
- package/dist/lexicons/dev/hatk/applyWrites.json +87 -0
- package/dist/lexicons/dev/hatk/createRecord.json +40 -0
- package/dist/lexicons/dev/hatk/createReport.json +48 -0
- package/dist/lexicons/dev/hatk/deleteRecord.json +25 -0
- package/dist/lexicons/dev/hatk/describeCollections.json +41 -0
- package/dist/lexicons/dev/hatk/describeFeeds.json +29 -0
- package/dist/lexicons/dev/hatk/describeLabels.json +45 -0
- package/dist/lexicons/dev/hatk/getFeed.json +30 -0
- package/dist/lexicons/dev/hatk/getPreferences.json +19 -0
- package/dist/lexicons/dev/hatk/getRecord.json +26 -0
- package/dist/lexicons/dev/hatk/getRecords.json +32 -0
- package/dist/lexicons/dev/hatk/putPreference.json +28 -0
- package/dist/lexicons/dev/hatk/putRecord.json +41 -0
- package/dist/lexicons/dev/hatk/searchRecords.json +32 -0
- package/dist/lexicons/dev/hatk/uploadBlob.json +23 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +29 -0
- package/dist/main.js +148 -68
- package/dist/mst.d.ts +18 -1
- package/dist/mst.d.ts.map +1 -1
- package/dist/mst.js +19 -8
- package/dist/oauth/db.d.ts +3 -1
- package/dist/oauth/db.d.ts.map +1 -1
- package/dist/oauth/db.js +56 -19
- package/dist/oauth/discovery.d.ts.map +1 -1
- package/dist/oauth/discovery.js +13 -1
- package/dist/oauth/server.d.ts +38 -0
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +228 -31
- package/dist/oauth/session.d.ts +11 -0
- package/dist/oauth/session.d.ts.map +1 -0
- package/dist/oauth/session.js +65 -0
- package/dist/opengraph.d.ts +10 -0
- package/dist/opengraph.d.ts.map +1 -1
- package/dist/opengraph.js +80 -40
- package/dist/pds-proxy.d.ts +60 -0
- package/dist/pds-proxy.d.ts.map +1 -0
- package/dist/pds-proxy.js +277 -0
- package/dist/private-collections.d.ts +8 -0
- package/dist/private-collections.d.ts.map +1 -0
- package/dist/private-collections.js +22 -0
- package/dist/push.d.ts +34 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/push.js +186 -0
- package/dist/renderer.d.ts +27 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +46 -0
- package/dist/resolve-hatk.d.ts +6 -0
- package/dist/resolve-hatk.d.ts.map +1 -0
- package/dist/resolve-hatk.js +20 -0
- package/dist/response.d.ts +16 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +69 -0
- package/dist/scanner.d.ts +21 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +88 -0
- package/dist/seed.d.ts +19 -0
- package/dist/seed.d.ts.map +1 -1
- package/dist/seed.js +43 -4
- package/dist/server-init.d.ts +8 -0
- package/dist/server-init.d.ts.map +1 -0
- package/dist/server-init.js +62 -0
- package/dist/server.d.ts +26 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +729 -638
- package/dist/setup.d.ts +28 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +50 -3
- package/dist/templates/feed.tpl +14 -0
- package/dist/templates/hook.tpl +5 -0
- package/dist/templates/label.tpl +15 -0
- package/dist/templates/og.tpl +17 -0
- package/dist/templates/seed.tpl +11 -0
- package/dist/templates/setup.tpl +5 -0
- package/dist/templates/test-feed.tpl +19 -0
- package/dist/templates/test-xrpc.tpl +19 -0
- package/dist/templates/xrpc.tpl +41 -0
- package/dist/test.d.ts +1 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +41 -32
- package/dist/views.js +1 -1
- package/dist/vite-plugin.d.ts +1 -1
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +254 -66
- package/dist/xrpc.d.ts +75 -11
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +189 -39
- package/package.json +17 -9
- package/public/admin-auth.js +271 -306
- package/public/admin.html +177 -61
- package/dist/db.d.ts.map +0 -1
- package/dist/fts.d.ts.map +0 -1
- package/dist/oauth/hooks.d.ts +0 -10
- package/dist/oauth/hooks.d.ts.map +0 -1
- package/dist/oauth/hooks.js +0 -40
- package/dist/schema.d.ts.map +0 -1
- package/dist/test-browser.d.ts +0 -14
- package/dist/test-browser.d.ts.map +0 -1
- package/dist/test-browser.js +0 -26
package/dist/indexer.js
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import { cborDecode } from "./cbor.js";
|
|
2
2
|
import { parseCarFrame } from "./car.js";
|
|
3
|
-
import {
|
|
3
|
+
import { isPrivateCollection } from "./private-collections.js";
|
|
4
|
+
import { insertRecord, deleteRecord, setCursor, setRepoStatus, getRepoRetryInfo, listAllRepoStatuses, getDatabasePort, updateRepoHandle, } from "./database/db.js";
|
|
4
5
|
import { backfillRepo } from "./backfill.js";
|
|
5
|
-
import { rebuildAllIndexes } from "./fts.js";
|
|
6
|
+
import { rebuildAllIndexes } from "./database/fts.js";
|
|
6
7
|
import { log, emit, timer } from "./logger.js";
|
|
7
8
|
import { runLabelRules } from "./labels.js";
|
|
8
|
-
import {
|
|
9
|
+
import { fireOnCommitHooks } from "./hooks.js";
|
|
10
|
+
import { getLexiconArray } from "./database/schema.js";
|
|
9
11
|
import { validateRecord } from '@bigmoves/lexicon';
|
|
10
12
|
let buffer = [];
|
|
11
13
|
let flushTimer = null;
|
|
12
14
|
let lastSeq = null;
|
|
15
|
+
let lastPersistedSeq = null;
|
|
16
|
+
let cursorCheckpointTimer = null;
|
|
13
17
|
const BATCH_SIZE = 100;
|
|
14
18
|
const FLUSH_INTERVAL_MS = 500;
|
|
19
|
+
const CURSOR_CHECKPOINT_INTERVAL_MS = 5_000;
|
|
15
20
|
let writesSinceRebuild = 0;
|
|
16
21
|
let ftsRebuildInterval = 500;
|
|
17
22
|
// Event buffer for DIDs mid-backfill
|
|
18
23
|
const pendingBuffers = new Map();
|
|
19
24
|
// Track in-flight backfills to avoid duplicates
|
|
20
25
|
const backfillInFlight = new Set();
|
|
21
|
-
const
|
|
26
|
+
const backfillPromises = new Map();
|
|
27
|
+
const pendingReschedule = new Set();
|
|
22
28
|
// In-memory cache of repo status to avoid flooding the DB read queue
|
|
23
29
|
const repoStatusCache = new Map();
|
|
24
30
|
// Set by startIndexer
|
|
@@ -27,6 +33,13 @@ let indexerSignalCollections;
|
|
|
27
33
|
let indexerPinnedRepos = null;
|
|
28
34
|
let indexerFetchTimeout;
|
|
29
35
|
let indexerMaxRetries;
|
|
36
|
+
let indexerPlcUrl;
|
|
37
|
+
let maxConcurrentBackfills = 3;
|
|
38
|
+
/**
|
|
39
|
+
* Flush the write buffer — insert all buffered records, update the relay cursor,
|
|
40
|
+
* run label rules on inserted records, and trigger FTS rebuilds when the write
|
|
41
|
+
* threshold is reached. Emits a wide event with batch stats.
|
|
42
|
+
*/
|
|
30
43
|
async function flushBuffer() {
|
|
31
44
|
if (buffer.length === 0)
|
|
32
45
|
return;
|
|
@@ -47,8 +60,10 @@ async function flushBuffer() {
|
|
|
47
60
|
}
|
|
48
61
|
}
|
|
49
62
|
if (lastSeq !== null) {
|
|
63
|
+
const seq = lastSeq;
|
|
50
64
|
try {
|
|
51
|
-
await setCursor('relay', String(
|
|
65
|
+
await setCursor('relay', String(seq));
|
|
66
|
+
lastPersistedSeq = seq;
|
|
52
67
|
}
|
|
53
68
|
catch (err) {
|
|
54
69
|
cursorError = err.message;
|
|
@@ -64,6 +79,14 @@ async function flushBuffer() {
|
|
|
64
79
|
value: item.record,
|
|
65
80
|
}).catch(() => { });
|
|
66
81
|
}
|
|
82
|
+
// Fire on-commit hooks for inserted records (async, non-blocking)
|
|
83
|
+
fireOnCommitHooks(inserted.map((item) => ({
|
|
84
|
+
action: 'create',
|
|
85
|
+
collection: item.collection,
|
|
86
|
+
uri: item.uri,
|
|
87
|
+
authorDid: item.authorDid,
|
|
88
|
+
record: item.record,
|
|
89
|
+
})));
|
|
67
90
|
// Aggregate collection counts and unique DIDs for wide event
|
|
68
91
|
const collections = {};
|
|
69
92
|
const dids = new Set();
|
|
@@ -86,9 +109,14 @@ async function flushBuffer() {
|
|
|
86
109
|
writesSinceRebuild += batch.length;
|
|
87
110
|
if (writesSinceRebuild >= ftsRebuildInterval) {
|
|
88
111
|
writesSinceRebuild = 0;
|
|
89
|
-
|
|
112
|
+
// Skip periodic full rebuild for SQLite — it uses incremental FTS updates
|
|
113
|
+
const port = getDatabasePort();
|
|
114
|
+
if (port.dialect !== 'sqlite') {
|
|
115
|
+
rebuildAllIndexes([...indexerCollections]).catch(() => { });
|
|
116
|
+
}
|
|
90
117
|
}
|
|
91
118
|
}
|
|
119
|
+
/** Schedule a flush after FLUSH_INTERVAL_MS if one isn't already pending. */
|
|
92
120
|
function scheduleFlush() {
|
|
93
121
|
if (flushTimer)
|
|
94
122
|
return;
|
|
@@ -97,6 +125,7 @@ function scheduleFlush() {
|
|
|
97
125
|
await flushBuffer();
|
|
98
126
|
}, FLUSH_INTERVAL_MS);
|
|
99
127
|
}
|
|
128
|
+
/** Add a record to the write buffer. Flushes immediately if BATCH_SIZE is reached. */
|
|
100
129
|
function bufferWrite(item) {
|
|
101
130
|
buffer.push(item);
|
|
102
131
|
if (buffer.length >= BATCH_SIZE) {
|
|
@@ -110,11 +139,79 @@ function bufferWrite(item) {
|
|
|
110
139
|
scheduleFlush();
|
|
111
140
|
}
|
|
112
141
|
}
|
|
142
|
+
/** Record the latest firehose sequence seen. Source of truth for cursor persistence and reconnect resume. */
|
|
143
|
+
export function noteSeq(seq) {
|
|
144
|
+
lastSeq = seq;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Persist the latest firehose sequence when it has advanced past the stored
|
|
148
|
+
* cursor. Runs on a timer independent of the write path: an app whose
|
|
149
|
+
* collections rarely appear on the firehose never flushes a batch, so the
|
|
150
|
+
* flush-path cursor write alone leaves the stored cursor frozen — and a frozen
|
|
151
|
+
* cursor makes the relay replay its whole retention window at line rate
|
|
152
|
+
* (~135 GB/hour observed against bsky.network) on every boot and reconnect.
|
|
153
|
+
*/
|
|
154
|
+
export async function checkpointCursor() {
|
|
155
|
+
if (lastSeq === null || lastSeq === lastPersistedSeq)
|
|
156
|
+
return;
|
|
157
|
+
const seq = lastSeq;
|
|
158
|
+
try {
|
|
159
|
+
await setCursor('relay', String(seq));
|
|
160
|
+
lastPersistedSeq = seq;
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
emit('indexer', 'cursor_checkpoint_error', { cursor_seq: seq, error: err.message });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The cursor a reconnect should resume from: the latest seq this process has
|
|
168
|
+
* seen, falling back to the boot-time cursor before any message has arrived.
|
|
169
|
+
* Reusing the boot-time cursor on every reconnect would replay everything
|
|
170
|
+
* received since the process started.
|
|
171
|
+
*/
|
|
172
|
+
export function resumeCursor(liveSeq, bootCursor) {
|
|
173
|
+
return liveSeq !== null ? String(liveSeq) : bootCursor;
|
|
174
|
+
}
|
|
175
|
+
/** Reset module cursor state between tests. */
|
|
176
|
+
export function _resetCursorStateForTests() {
|
|
177
|
+
lastSeq = null;
|
|
178
|
+
lastPersistedSeq = null;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Auto-backfill a DID's repo when first seen on the firehose.
|
|
182
|
+
*
|
|
183
|
+
* Fetches the full repo via CAR export, inserts all records, then replays any
|
|
184
|
+
* firehose events that arrived during the backfill. Concurrency is capped at
|
|
185
|
+
* `maxConcurrentBackfills`. Failed backfills retry with exponential delay up
|
|
186
|
+
* to `maxRetries`.
|
|
187
|
+
*/
|
|
188
|
+
/** Wait for a DID's backfill to complete if one is in flight. */
|
|
189
|
+
export function awaitBackfill(did) {
|
|
190
|
+
const entry = backfillPromises.get(did);
|
|
191
|
+
return entry ? entry.promise : Promise.resolve();
|
|
192
|
+
}
|
|
113
193
|
export async function triggerAutoBackfill(did, attempt = 0) {
|
|
114
194
|
if (backfillInFlight.has(did))
|
|
115
195
|
return;
|
|
196
|
+
if (backfillInFlight.size >= maxConcurrentBackfills) {
|
|
197
|
+
if (!pendingReschedule.has(did)) {
|
|
198
|
+
pendingReschedule.add(did);
|
|
199
|
+
setTimeout(() => {
|
|
200
|
+
pendingReschedule.delete(did);
|
|
201
|
+
triggerAutoBackfill(did, attempt);
|
|
202
|
+
}, 10_000);
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
116
206
|
backfillInFlight.add(did);
|
|
117
207
|
pendingBuffers.set(did, []);
|
|
208
|
+
if (!backfillPromises.has(did)) {
|
|
209
|
+
let resolveBackfill;
|
|
210
|
+
const promise = new Promise((r) => {
|
|
211
|
+
resolveBackfill = r;
|
|
212
|
+
});
|
|
213
|
+
backfillPromises.set(did, { promise, resolve: resolveBackfill });
|
|
214
|
+
}
|
|
118
215
|
if (attempt === 0)
|
|
119
216
|
await setRepoStatus(did, 'pending');
|
|
120
217
|
const elapsed = timer();
|
|
@@ -154,6 +251,12 @@ export async function triggerAutoBackfill(did, attempt = 0) {
|
|
|
154
251
|
error,
|
|
155
252
|
retry_count: currentRetryCount,
|
|
156
253
|
});
|
|
254
|
+
// Resolve awaiting callers (e.g. on-login hooks)
|
|
255
|
+
const entry = backfillPromises.get(did);
|
|
256
|
+
if (entry) {
|
|
257
|
+
entry.resolve();
|
|
258
|
+
backfillPromises.delete(did);
|
|
259
|
+
}
|
|
157
260
|
if (status === 'error' && currentRetryCount < indexerMaxRetries) {
|
|
158
261
|
const delaySecs = Math.min(currentRetryCount * 60, 3600);
|
|
159
262
|
const delayMs = Math.max(delaySecs, 60) * 1000;
|
|
@@ -162,7 +265,7 @@ export async function triggerAutoBackfill(did, attempt = 0) {
|
|
|
162
265
|
}, delayMs);
|
|
163
266
|
}
|
|
164
267
|
}
|
|
165
|
-
|
|
268
|
+
/** Emit a memory diagnostics wide event every 30s for observability. */
|
|
166
269
|
function startMemoryDiagnostics() {
|
|
167
270
|
setInterval(() => {
|
|
168
271
|
const mem = process.memoryUsage();
|
|
@@ -184,6 +287,16 @@ function startMemoryDiagnostics() {
|
|
|
184
287
|
});
|
|
185
288
|
}, 30_000);
|
|
186
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Connect to the AT Protocol relay firehose and begin indexing.
|
|
292
|
+
*
|
|
293
|
+
* Opens a WebSocket to `subscribeRepos`, processes commit messages synchronously
|
|
294
|
+
* on the event loop to minimize backpressure, and batches writes through
|
|
295
|
+
* {@link flushBuffer}. New DIDs trigger auto-backfill via {@link triggerAutoBackfill}.
|
|
296
|
+
* Reconnects automatically on disconnect after a 3s delay.
|
|
297
|
+
*
|
|
298
|
+
* @returns The WebSocket connection (for shutdown coordination)
|
|
299
|
+
*/
|
|
187
300
|
export async function startIndexer(opts) {
|
|
188
301
|
const { relayUrl, collections, cursor, fetchTimeout } = opts;
|
|
189
302
|
if (opts.ftsRebuildInterval != null)
|
|
@@ -193,6 +306,8 @@ export async function startIndexer(opts) {
|
|
|
193
306
|
indexerPinnedRepos = opts.pinnedRepos || null;
|
|
194
307
|
indexerFetchTimeout = fetchTimeout;
|
|
195
308
|
indexerMaxRetries = opts.maxRetries;
|
|
309
|
+
indexerPlcUrl = opts.plcUrl;
|
|
310
|
+
maxConcurrentBackfills = opts.parallelism ?? 3;
|
|
196
311
|
// Pre-populate repo status cache from DB so non-signal updates
|
|
197
312
|
// (e.g. profile changes) are processed for already-tracked DIDs
|
|
198
313
|
if (repoStatusCache.size === 0) {
|
|
@@ -202,7 +317,14 @@ export async function startIndexer(opts) {
|
|
|
202
317
|
}
|
|
203
318
|
log(`[indexer] Warmed repo status cache with ${statuses.length} entries`);
|
|
204
319
|
}
|
|
205
|
-
startMemoryDiagnostics()
|
|
320
|
+
// startMemoryDiagnostics()
|
|
321
|
+
// Checkpoint the cursor on a timer regardless of write activity (see
|
|
322
|
+
// checkpointCursor). Guarded so reconnects don't stack intervals; unref'd so
|
|
323
|
+
// it never keeps the process alive.
|
|
324
|
+
if (!cursorCheckpointTimer) {
|
|
325
|
+
cursorCheckpointTimer = setInterval(() => void checkpointCursor(), CURSOR_CHECKPOINT_INTERVAL_MS);
|
|
326
|
+
cursorCheckpointTimer.unref?.();
|
|
327
|
+
}
|
|
206
328
|
let wsUrl = `${relayUrl}/xrpc/com.atproto.sync.subscribeRepos`;
|
|
207
329
|
if (cursor) {
|
|
208
330
|
wsUrl += `?cursor=${cursor}`;
|
|
@@ -220,27 +342,95 @@ export async function startIndexer(opts) {
|
|
|
220
342
|
const bytes = new Uint8Array(event.data);
|
|
221
343
|
processMessage(bytes, collections);
|
|
222
344
|
}
|
|
223
|
-
catch {
|
|
224
|
-
|
|
345
|
+
catch (err) {
|
|
346
|
+
emit('indexer', 'decode_error', { error: err instanceof Error ? err.message : String(err) });
|
|
225
347
|
}
|
|
226
348
|
});
|
|
227
349
|
ws.addEventListener('open', () => log('[indexer] Connected to relay'));
|
|
228
350
|
ws.addEventListener('close', () => {
|
|
229
351
|
log('[indexer] Disconnected, reconnecting in 3s...');
|
|
230
|
-
setTimeout(() => startIndexer(opts), 3000);
|
|
352
|
+
setTimeout(() => startIndexer({ ...opts, cursor: resumeCursor(lastSeq, opts.cursor) }), 3000);
|
|
231
353
|
});
|
|
232
354
|
return ws;
|
|
233
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Handle a `#identity` firehose event for a DID. The `handle` field on the
|
|
358
|
+
* event is optional per the lexicon, and some emitters omit it (signalling
|
|
359
|
+
* "re-resolve"). When absent, we re-resolve from the PLC directory so handle
|
|
360
|
+
* renames propagate even when the relay payload is sparse.
|
|
361
|
+
*
|
|
362
|
+
* Only updates DIDs we already track (present in repoStatusCache) to avoid
|
|
363
|
+
* writing rows for the entire network.
|
|
364
|
+
*/
|
|
365
|
+
async function handleIdentityEvent(did, payloadHandle) {
|
|
366
|
+
if (!repoStatusCache.has(did))
|
|
367
|
+
return;
|
|
368
|
+
let handle = payloadHandle;
|
|
369
|
+
const payloadHadHandle = handle !== undefined;
|
|
370
|
+
if (!handle) {
|
|
371
|
+
try {
|
|
372
|
+
// Bound the PLC fetch so a slow plc.directory can't pile up unbounded
|
|
373
|
+
// promises during an identity-event burst (fire-and-forget caller).
|
|
374
|
+
const res = await fetch(`${indexerPlcUrl}/${did}`, {
|
|
375
|
+
signal: AbortSignal.timeout(indexerFetchTimeout * 1000),
|
|
376
|
+
});
|
|
377
|
+
if (res.ok) {
|
|
378
|
+
const doc = (await res.json());
|
|
379
|
+
// First at:// entry is the canonical handle (per @atproto/identity convention)
|
|
380
|
+
const aka = doc.alsoKnownAs?.find((u) => u.startsWith('at://'));
|
|
381
|
+
handle = aka ? aka.slice('at://'.length) : undefined;
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
emit('indexer', 'identity_resolve_error', { did, status: res.status });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
emit('indexer', 'identity_resolve_error', {
|
|
389
|
+
did,
|
|
390
|
+
error: err instanceof Error ? err.message : String(err),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (!handle) {
|
|
395
|
+
emit('indexer', 'identity_no_handle', { did, payload_had_handle: payloadHadHandle });
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
await updateRepoHandle(did, handle);
|
|
400
|
+
emit('indexer', 'identity_handle_update', { did, handle, payload_had_handle: payloadHadHandle });
|
|
401
|
+
}
|
|
402
|
+
catch (err) {
|
|
403
|
+
emit('indexer', 'identity_update_error', {
|
|
404
|
+
did,
|
|
405
|
+
handle,
|
|
406
|
+
error: err instanceof Error ? err.message : String(err),
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Process a single firehose message. Decodes the CBOR header/body, filters
|
|
412
|
+
* for relevant collections, validates records against lexicons, and routes
|
|
413
|
+
* writes to the buffer (or pending buffer if the DID is mid-backfill).
|
|
414
|
+
*/
|
|
234
415
|
function processMessage(bytes, collections) {
|
|
235
416
|
const header = cborDecode(bytes, 0);
|
|
236
417
|
const body = cborDecode(bytes, header.offset);
|
|
418
|
+
// Handle identity events (handle changes). Fire-and-forget — keeps
|
|
419
|
+
// processMessage synchronous so the WS event loop drains without backpressure.
|
|
420
|
+
if (header.value.t === '#identity') {
|
|
421
|
+
const did = typeof body.value.did === 'string' ? body.value.did : undefined;
|
|
422
|
+
const handle = typeof body.value.handle === 'string' ? body.value.handle : undefined;
|
|
423
|
+
if (did)
|
|
424
|
+
handleIdentityEvent(did, handle);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
237
427
|
if (header.value.op !== 1 || header.value.t !== '#commit')
|
|
238
428
|
return;
|
|
239
429
|
if (!body.value.blocks || !body.value.ops)
|
|
240
430
|
return;
|
|
241
431
|
// Track sequence number for cursor
|
|
242
432
|
if (body.value.seq)
|
|
243
|
-
|
|
433
|
+
noteSeq(body.value.seq);
|
|
244
434
|
const did = body.value.repo;
|
|
245
435
|
if (!did)
|
|
246
436
|
return;
|
|
@@ -248,7 +438,13 @@ function processMessage(bytes, collections) {
|
|
|
248
438
|
if (indexerPinnedRepos && !indexerPinnedRepos.has(did))
|
|
249
439
|
return;
|
|
250
440
|
// Check if any ops in this commit are for collections we care about
|
|
251
|
-
|
|
441
|
+
// Private collections are AppView-authoritative: nothing on the network may
|
|
442
|
+
// write or delete their rows, so firehose ops naming them are spoofed by
|
|
443
|
+
// definition and must be dropped.
|
|
444
|
+
const relevantOps = body.value.ops.filter((op) => {
|
|
445
|
+
const collection = op.path.split('/')[0];
|
|
446
|
+
return collections.has(collection) && !isPrivateCollection(collection);
|
|
447
|
+
});
|
|
252
448
|
if (relevantOps.length === 0)
|
|
253
449
|
return;
|
|
254
450
|
// Copy blocks out of the original buffer before it can be GC'd
|
|
@@ -264,7 +460,7 @@ function processMessage(bytes, collections) {
|
|
|
264
460
|
repoStatusCache.set(did, 'unknown');
|
|
265
461
|
}
|
|
266
462
|
if (hasSignalOp && (!indexerPinnedRepos || indexerPinnedRepos.has(did))) {
|
|
267
|
-
if (repoStatus === null && backfillInFlight.size <
|
|
463
|
+
if (repoStatus === null && backfillInFlight.size < maxConcurrentBackfills) {
|
|
268
464
|
repoStatusCache.set(did, 'pending');
|
|
269
465
|
triggerAutoBackfill(did);
|
|
270
466
|
}
|
|
@@ -283,34 +479,46 @@ function processMessage(bytes, collections) {
|
|
|
283
479
|
const uri = `at://${did}/${op.path}`;
|
|
284
480
|
if (op.action === 'delete') {
|
|
285
481
|
deleteRecord(collection, uri);
|
|
482
|
+
fireOnCommitHooks([
|
|
483
|
+
{
|
|
484
|
+
action: 'delete',
|
|
485
|
+
collection,
|
|
486
|
+
uri,
|
|
487
|
+
authorDid: did,
|
|
488
|
+
record: null,
|
|
489
|
+
},
|
|
490
|
+
]);
|
|
286
491
|
continue;
|
|
287
492
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
493
|
+
const opCid = typeof op.cid === 'string' ? op.cid : op.cid?.$link;
|
|
494
|
+
if (!opCid)
|
|
495
|
+
continue;
|
|
496
|
+
const data = blocks.get(opCid);
|
|
497
|
+
if (!data)
|
|
498
|
+
continue;
|
|
499
|
+
try {
|
|
500
|
+
const { value: record } = cborDecode(data);
|
|
501
|
+
if (record?.$type === collection) {
|
|
502
|
+
const validationError = validateRecord(getLexiconArray(), collection, record);
|
|
503
|
+
if (validationError) {
|
|
504
|
+
emit('indexer', 'validation_skip', {
|
|
505
|
+
uri,
|
|
506
|
+
collection,
|
|
507
|
+
path: validationError.path,
|
|
508
|
+
error: validationError.message,
|
|
509
|
+
});
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const item = { collection, uri, cid: opCid, authorDid: did, record };
|
|
513
|
+
// If DID is mid-backfill, buffer instead of writing directly
|
|
514
|
+
if (pendingBuffers.has(did)) {
|
|
515
|
+
pendingBuffers.get(did).push(item);
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
bufferWrite(item);
|
|
311
519
|
}
|
|
312
520
|
}
|
|
313
|
-
catch { }
|
|
314
521
|
}
|
|
522
|
+
catch { }
|
|
315
523
|
}
|
|
316
524
|
}
|
package/dist/labels.d.ts
CHANGED
|
@@ -13,7 +13,36 @@ export interface LabelRuleContext {
|
|
|
13
13
|
value: Record<string, any>;
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
+
export interface LabelModule {
|
|
17
|
+
definition?: LabelDefinition;
|
|
18
|
+
evaluate?: (ctx: LabelRuleContext) => Promise<string[]>;
|
|
19
|
+
}
|
|
20
|
+
export declare function defineLabel(module: LabelModule): {
|
|
21
|
+
definition?: LabelDefinition;
|
|
22
|
+
evaluate?: (ctx: LabelRuleContext) => Promise<string[]>;
|
|
23
|
+
__type: "labels";
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Discover and load label rule modules from the `labels/` directory.
|
|
27
|
+
*
|
|
28
|
+
* Each module should default-export an object with an optional `definition`
|
|
29
|
+
* (label metadata like severity and blur behavior) and an optional `evaluate`
|
|
30
|
+
* function that returns label values to apply to a record.
|
|
31
|
+
*
|
|
32
|
+
* @param labelsDir - Absolute path to the `labels/` directory
|
|
33
|
+
*/
|
|
16
34
|
export declare function initLabels(labelsDir: string): Promise<void>;
|
|
35
|
+
/** Clear all registered label definitions and rules (for hot-reload). */
|
|
36
|
+
export declare function clearLabels(): void;
|
|
37
|
+
/** Register a single label module from a scanned server/ module. */
|
|
38
|
+
export declare function registerLabelModule(name: string, labelMod: {
|
|
39
|
+
definition?: LabelDefinition;
|
|
40
|
+
evaluate?: (ctx: LabelRuleContext) => Promise<string[]>;
|
|
41
|
+
}): void;
|
|
42
|
+
/**
|
|
43
|
+
* Evaluate all loaded label rules against a record and persist any resulting labels.
|
|
44
|
+
* Called after each record is indexed. Rule errors are logged but never block indexing.
|
|
45
|
+
*/
|
|
17
46
|
export declare function runLabelRules(record: {
|
|
18
47
|
uri: string;
|
|
19
48
|
cid: string;
|
|
@@ -21,9 +50,16 @@ export declare function runLabelRules(record: {
|
|
|
21
50
|
collection: string;
|
|
22
51
|
value: Record<string, any>;
|
|
23
52
|
}): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Re-evaluate all label rules against every existing record in the given collections.
|
|
55
|
+
* Used by `/admin/rescan-labels` to apply new or updated rules retroactively.
|
|
56
|
+
*
|
|
57
|
+
* @returns Count of records scanned and new labels applied
|
|
58
|
+
*/
|
|
24
59
|
export declare function rescanLabels(collections: string[]): Promise<{
|
|
25
60
|
scanned: number;
|
|
26
61
|
labeled: number;
|
|
27
62
|
}>;
|
|
63
|
+
/** Return all label definitions discovered during {@link initLabels}. */
|
|
28
64
|
export declare function getLabelDefinitions(): LabelDefinition[];
|
|
29
65
|
//# sourceMappingURL=labels.d.ts.map
|
package/dist/labels.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"labels.d.ts","sourceRoot":"","sources":["../src/labels.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"labels.d.ts","sourceRoot":"","sources":["../src/labels.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAIlD,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;QACtD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KACtD,CAAA;IACD,MAAM,EAAE;QACN,GAAG,EAAE,MAAM,CAAA;QACX,GAAG,EAAE,MAAM,CAAA;QACX,GAAG,EAAE,MAAM,CAAA;QACX,UAAU,EAAE,MAAM,CAAA;QAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KAC3B,CAAA;CACF;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;CACxD;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW;iBAJhC,eAAe;eACjB,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC;;EAKxD;AAYD;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmCjE;AAED,yEAAyE;AACzE,wBAAgB,WAAW,IAAI,IAAI,CAGlC;AAED,oEAAoE;AACpE,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE;IAAE,UAAU,CAAC,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;CAAE,GAClG,IAAI,CAON;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAC3B,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBhB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAuCvG;AAED,yEAAyE;AACzE,wBAAgB,mBAAmB,IAAI,eAAe,EAAE,CAEvD"}
|
package/dist/labels.js
CHANGED
|
@@ -6,13 +6,53 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* Label system for applying moderation labels to records as they are indexed.
|
|
11
|
+
*
|
|
12
|
+
* Place label modules in the `labels/` directory. Each module default-exports
|
|
13
|
+
* an object with a `definition` (label metadata) and/or an `evaluate` function
|
|
14
|
+
* (rule that returns label values for a given record).
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // labels/nsfw.ts
|
|
19
|
+
* import type { LabelRuleContext } from '@hatk/hatk/labels'
|
|
20
|
+
*
|
|
21
|
+
* export default {
|
|
22
|
+
* definition: {
|
|
23
|
+
* identifier: 'nsfw',
|
|
24
|
+
* severity: 'alert',
|
|
25
|
+
* blurs: 'media',
|
|
26
|
+
* defaultSetting: 'warn',
|
|
27
|
+
* locales: [{ lang: 'en', name: 'NSFW', description: 'Not safe for work' }],
|
|
28
|
+
* },
|
|
29
|
+
*
|
|
30
|
+
* async evaluate(ctx: LabelRuleContext): Promise<string[]> {
|
|
31
|
+
* if (ctx.record.value.nsfw === true) return ['nsfw']
|
|
32
|
+
* return []
|
|
33
|
+
* },
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
9
37
|
import { resolve } from 'node:path';
|
|
10
38
|
import { readdirSync } from 'node:fs';
|
|
11
|
-
import { querySQL, runSQL, insertLabels, getSchema } from "./db.js";
|
|
39
|
+
import { querySQL, runSQL, insertLabels, getSchema } from "./database/db.js";
|
|
12
40
|
import { log, emit } from "./logger.js";
|
|
41
|
+
export function defineLabel(module) {
|
|
42
|
+
return { __type: 'labels', ...module };
|
|
43
|
+
}
|
|
13
44
|
const rules = [];
|
|
14
45
|
let labelDefs = [];
|
|
15
46
|
let labelSrc = 'self';
|
|
47
|
+
/**
|
|
48
|
+
* Discover and load label rule modules from the `labels/` directory.
|
|
49
|
+
*
|
|
50
|
+
* Each module should default-export an object with an optional `definition`
|
|
51
|
+
* (label metadata like severity and blur behavior) and an optional `evaluate`
|
|
52
|
+
* function that returns label values to apply to a record.
|
|
53
|
+
*
|
|
54
|
+
* @param labelsDir - Absolute path to the `labels/` directory
|
|
55
|
+
*/
|
|
16
56
|
export async function initLabels(labelsDir) {
|
|
17
57
|
let files;
|
|
18
58
|
try {
|
|
@@ -26,7 +66,7 @@ export async function initLabels(labelsDir) {
|
|
|
26
66
|
for (const file of files) {
|
|
27
67
|
const name = file.replace(/\.(ts|js)$/, '');
|
|
28
68
|
const scriptPath = resolve(labelsDir, file);
|
|
29
|
-
const mod = await import(__rewriteRelativeImportExtension(scriptPath));
|
|
69
|
+
const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${scriptPath}?t=${Date.now()}`));
|
|
30
70
|
const handler = mod.default;
|
|
31
71
|
if (handler.definition) {
|
|
32
72
|
labelDefs.push(handler.definition);
|
|
@@ -45,6 +85,24 @@ export async function initLabels(labelsDir) {
|
|
|
45
85
|
log(`[labels] ${labelDefs.length} label definitions loaded`);
|
|
46
86
|
}
|
|
47
87
|
}
|
|
88
|
+
/** Clear all registered label definitions and rules (for hot-reload). */
|
|
89
|
+
export function clearLabels() {
|
|
90
|
+
labelDefs.length = 0;
|
|
91
|
+
rules.length = 0;
|
|
92
|
+
}
|
|
93
|
+
/** Register a single label module from a scanned server/ module. */
|
|
94
|
+
export function registerLabelModule(name, labelMod) {
|
|
95
|
+
if (labelMod.definition) {
|
|
96
|
+
labelDefs.push(labelMod.definition);
|
|
97
|
+
}
|
|
98
|
+
if (labelMod.evaluate) {
|
|
99
|
+
rules.push({ name, evaluate: labelMod.evaluate });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Evaluate all loaded label rules against a record and persist any resulting labels.
|
|
104
|
+
* Called after each record is indexed. Rule errors are logged but never block indexing.
|
|
105
|
+
*/
|
|
48
106
|
export async function runLabelRules(record) {
|
|
49
107
|
if (rules.length === 0)
|
|
50
108
|
return;
|
|
@@ -69,15 +127,21 @@ export async function runLabelRules(record) {
|
|
|
69
127
|
emit('labels', 'applied', { count: allLabels.length, uri: record.uri, vals: allLabels.map((l) => l.val) });
|
|
70
128
|
}
|
|
71
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Re-evaluate all label rules against every existing record in the given collections.
|
|
132
|
+
* Used by `/admin/rescan-labels` to apply new or updated rules retroactively.
|
|
133
|
+
*
|
|
134
|
+
* @returns Count of records scanned and new labels applied
|
|
135
|
+
*/
|
|
72
136
|
export async function rescanLabels(collections) {
|
|
73
|
-
const beforeRows = await querySQL(`SELECT COUNT(*) as count FROM _labels`);
|
|
137
|
+
const beforeRows = (await querySQL(`SELECT COUNT(*) as count FROM _labels`));
|
|
74
138
|
const beforeCount = Number(beforeRows[0]?.count || 0);
|
|
75
139
|
let scanned = 0;
|
|
76
140
|
for (const collection of collections) {
|
|
77
141
|
const schema = getSchema(collection);
|
|
78
142
|
if (!schema)
|
|
79
143
|
continue;
|
|
80
|
-
const rows = await querySQL(`SELECT * FROM ${schema.tableName}`);
|
|
144
|
+
const rows = (await querySQL(`SELECT * FROM ${schema.tableName}`));
|
|
81
145
|
for (const row of rows) {
|
|
82
146
|
scanned++;
|
|
83
147
|
const value = {};
|
|
@@ -85,7 +149,7 @@ export async function rescanLabels(collections) {
|
|
|
85
149
|
let v = row[col.name];
|
|
86
150
|
if (v === null || v === undefined)
|
|
87
151
|
continue;
|
|
88
|
-
if (col.
|
|
152
|
+
if (col.isJson && typeof v === 'string') {
|
|
89
153
|
try {
|
|
90
154
|
v = JSON.parse(v);
|
|
91
155
|
}
|
|
@@ -102,10 +166,11 @@ export async function rescanLabels(collections) {
|
|
|
102
166
|
});
|
|
103
167
|
}
|
|
104
168
|
}
|
|
105
|
-
const afterRows = await querySQL(`SELECT COUNT(*) as count FROM _labels`);
|
|
169
|
+
const afterRows = (await querySQL(`SELECT COUNT(*) as count FROM _labels`));
|
|
106
170
|
const afterCount = Number(afterRows[0]?.count || 0);
|
|
107
171
|
return { scanned, labeled: afterCount - beforeCount };
|
|
108
172
|
}
|
|
173
|
+
/** Return all label definitions discovered during {@link initLabels}. */
|
|
109
174
|
export function getLabelDefinitions() {
|
|
110
175
|
return labelDefs;
|
|
111
176
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lexicon-resolve.d.ts","sourceRoot":"","sources":["../src/lexicon-resolve.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"lexicon-resolve.d.ts","sourceRoot":"","sources":["../src/lexicon-resolve.ts"],"names":[],"mappings":"AAoFA,UAAU,OAAO;IACf,OAAO,EAAE,MAAM,CAAA;IACf,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB;AA2FD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAgChF"}
|