@atproto/sync
Version:
atproto sync library
316 lines • 10.9 kB
JavaScript
import { createDeferrable, wait } from '@atproto/common';
import { parseToAtprotoDocument, } from '@atproto/identity';
import { RepoVerificationError, cborToLexRecord, formatDataKey, parseDataKey, readCar, readCarWithRoot, verifyProofs, } from '@atproto/repo';
import { AtUri } from '@atproto/syntax';
import { Subscription } from '@atproto/xrpc-server';
import { com } from '../lexicons/index.js';
import { didAndSeqForEvt } from '../util.js';
export class Firehose {
constructor(opts) {
this.opts = opts;
this.matchCollection = null;
this.destoryDefer = createDeferrable();
this.abortController = new AbortController();
if (this.opts.getCursor && this.opts.runner) {
throw new Error('Must set only `getCursor` or `runner`');
}
if (opts.filterCollections) {
const exact = new Set();
const prefixes = [];
for (const pattern of opts.filterCollections) {
if (pattern.endsWith('.*')) {
prefixes.push(pattern.slice(0, -2));
}
else {
exact.add(pattern);
}
}
this.matchCollection = (col) => {
if (exact.has(col))
return true;
for (const prefix of prefixes) {
if (col.startsWith(prefix))
return true;
}
return false;
};
}
this.sub = new Subscription({
...opts,
service: opts.service ?? 'wss://bsky.network',
method: com.atproto.sync.subscribeRepos.$lxm,
signal: this.abortController.signal,
getParams: async () => {
let cursor;
if (this.opts.runner) {
cursor = await this.opts.runner.getCursor();
}
else if (this.opts.getCursor) {
cursor = await this.opts.getCursor();
}
return cursor === undefined ? undefined : { cursor };
},
validate: (value) => {
const result = com.atproto.sync.subscribeRepos.$message.safeParse(value);
if (result.success) {
return result.value;
}
else {
this.opts.onError(new FirehoseValidationError(result.reason, value));
}
},
});
}
async start() {
try {
for await (const evt of this.sub) {
if (this.opts.runner) {
const parsed = didAndSeqForEvt(evt);
if (!parsed) {
continue;
}
this.opts.runner.trackEvent(parsed.did, parsed.seq, async () => {
const parsed = await this.parseEvt(evt);
for (const write of parsed) {
try {
await this.opts.handleEvent(write);
}
catch (err) {
this.opts.onError(new FirehoseHandlerError(err, write));
}
}
});
}
else {
await this.processEvt(evt);
}
}
}
catch (err) {
if (err && err['name'] === 'AbortError') {
this.destoryDefer.resolve();
return;
}
this.opts.onError(new FirehoseSubscriptionError(err));
await wait(this.opts.subscriptionReconnectDelay ?? 3000);
return this.start();
}
}
async parseEvt(evt) {
try {
if (com.atproto.sync.subscribeRepos.commit.$isTypeOf(evt)) {
if (this.opts.excludeCommit)
return [];
return this.opts.unauthenticatedCommits
? await parseCommitUnauthenticated(evt, this.matchCollection)
: await parseCommitAuthenticated(this.opts.idResolver, evt, this.matchCollection);
}
else if (com.atproto.sync.subscribeRepos.account.$isTypeOf(evt)) {
if (this.opts.excludeAccount)
return [];
const parsed = parseAccount(evt);
return parsed ? [parsed] : [];
}
else if (com.atproto.sync.subscribeRepos.identity.$isTypeOf(evt)) {
if (this.opts.excludeIdentity)
return [];
const parsed = await parseIdentity(this.opts.idResolver, evt, this.opts.unauthenticatedHandles);
return parsed ? [parsed] : [];
}
else if (com.atproto.sync.subscribeRepos.sync.$isTypeOf(evt)) {
if (this.opts.excludeSync)
return [];
const parsed = await parseSync(evt);
return parsed ? [parsed] : [];
}
else {
return [];
}
}
catch (err) {
this.opts.onError(new FirehoseParseError(err, evt));
return [];
}
}
async processEvt(evt) {
const parsed = await this.parseEvt(evt);
for (const write of parsed) {
try {
await this.opts.handleEvent(write);
}
catch (err) {
this.opts.onError(new FirehoseHandlerError(err, write));
}
}
}
async destroy() {
this.abortController.abort();
await this.destoryDefer.complete;
}
}
export const parseCommitAuthenticated = async (idResolver, evt, matchCollection, forceKeyRefresh = false) => {
const did = evt.repo;
const ops = maybeFilterOps(evt.ops, matchCollection);
if (ops.length === 0) {
return [];
}
const claims = ops.map((op) => {
const { collection, rkey } = parseDataKey(op.path);
return {
collection,
rkey,
cid: op.action === 'delete' ? null : op.cid,
};
});
const key = await idResolver.did.resolveAtprotoKey(did, forceKeyRefresh);
const verifiedCids = {};
try {
const results = await verifyProofs(evt.blocks, claims, did, key);
results.verified.forEach((op) => {
const path = formatDataKey(op.collection, op.rkey);
verifiedCids[path] = op.cid;
});
}
catch (err) {
if (err instanceof RepoVerificationError && !forceKeyRefresh) {
return parseCommitAuthenticated(idResolver, evt, matchCollection, true);
}
throw err;
}
const verifiedOps = ops.filter((op) => {
const verifiedCid = verifiedCids[op.path];
if (op.action === 'delete') {
return verifiedCid === null;
}
else {
return (op.cid != null && verifiedCid != null && verifiedCid.equals(op.cid));
}
});
return formatCommitOps(evt, verifiedOps, {
skipCidVerification: true, // already checked via verifyProofs()
});
};
export const parseCommitUnauthenticated = async (evt, matchCollection) => {
const ops = maybeFilterOps(evt.ops, matchCollection);
return formatCommitOps(evt, ops);
};
const maybeFilterOps = (ops, matchCollection) => {
if (!matchCollection)
return ops;
return ops.filter((op) => {
const { collection } = parseDataKey(op.path);
return matchCollection(collection);
});
};
const formatCommitOps = async (evt, ops, options) => {
const car = await readCar(evt.blocks, options);
const evts = [];
for (const op of ops) {
const uri = AtUri.make(evt.repo, op.path);
const meta = {
seq: evt.seq,
time: evt.time,
commit: evt.commit,
blocks: car.blocks,
rev: evt.rev,
uri,
did: uri.did,
collection: uri.collection,
rkey: uri.rkey,
};
if (op.action === 'create' || op.action === 'update') {
if (!op.cid)
continue;
const recordBytes = car.blocks.get(op.cid);
if (!recordBytes)
continue;
const record = cborToLexRecord(recordBytes);
evts.push({
...meta,
event: op.action,
cid: op.cid,
record,
});
}
if (op.action === 'delete') {
evts.push({
...meta,
event: 'delete',
});
}
}
return evts;
};
export const parseSync = async (evt) => {
const car = await readCarWithRoot(evt.blocks);
return {
event: 'sync',
seq: evt.seq,
time: evt.time,
did: evt.did,
cid: car.root,
rev: evt.rev,
blocks: car.blocks,
};
};
export const parseIdentity = async (idResolver, evt, unauthenticated = false) => {
const res = await idResolver.did.resolve(evt.did);
const handle = res && !unauthenticated
? await verifyHandle(idResolver, evt.did, res)
: undefined;
return {
event: 'identity',
seq: evt.seq,
time: evt.time,
did: evt.did,
handle,
didDocument: res ?? undefined,
};
};
const verifyHandle = async (idResolver, did, didDoc) => {
const { handle } = parseToAtprotoDocument(didDoc);
if (!handle) {
return undefined;
}
const res = await idResolver.handle.resolve(handle);
return res === did ? handle : undefined;
};
export const parseAccount = (evt) => {
if (evt.status && !isValidStatus(evt.status))
return;
return {
event: 'account',
seq: evt.seq,
time: evt.time,
did: evt.did,
active: evt.active,
status: evt.status,
};
};
const isValidStatus = (str) => {
return ['takendown', 'suspended', 'deleted', 'deactivated'].includes(str);
};
export class FirehoseValidationError extends Error {
constructor(err, value) {
super('error in firehose event lexicon validation', { cause: err });
this.value = value;
}
}
export class FirehoseParseError extends Error {
constructor(err, event) {
super('error in parsing and authenticating firehose event', { cause: err });
this.event = event;
}
}
export class FirehoseSubscriptionError extends Error {
constructor(err) {
super('error on firehose subscription', { cause: err });
}
}
export class FirehoseHandlerError extends Error {
constructor(err, event) {
super('error in firehose event handler', { cause: err });
this.event = event;
}
}
//# sourceMappingURL=index.js.map