@atproto/sync 0.1.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 +12 -0
- package/LICENSE.txt +7 -0
- package/README.md +93 -0
- package/dist/events.d.ts +49 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +3 -0
- package/dist/events.js.map +1 -0
- package/dist/firehose/index.d.ts +50 -0
- package/dist/firehose/index.d.ts.map +1 -0
- package/dist/firehose/index.js +309 -0
- package/dist/firehose/index.js.map +1 -0
- package/dist/firehose/lexicons.d.ts +118 -0
- package/dist/firehose/lexicons.d.ts.map +1 -0
- package/dist/firehose/lexicons.js +265 -0
- package/dist/firehose/lexicons.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/runner/consecutive-list.d.ts +27 -0
- package/dist/runner/consecutive-list.d.ts.map +1 -0
- package/dist/runner/consecutive-list.js +68 -0
- package/dist/runner/consecutive-list.js.map +1 -0
- package/dist/runner/index.d.ts +4 -0
- package/dist/runner/index.d.ts.map +1 -0
- package/dist/runner/index.js +20 -0
- package/dist/runner/index.js.map +1 -0
- package/dist/runner/memory-runner.d.ts +24 -0
- package/dist/runner/memory-runner.d.ts.map +1 -0
- package/dist/runner/memory-runner.js +92 -0
- package/dist/runner/memory-runner.js.map +1 -0
- package/dist/runner/types.d.ts +5 -0
- package/dist/runner/types.d.ts.map +1 -0
- package/dist/runner/types.js +3 -0
- package/dist/runner/types.js.map +1 -0
- package/dist/util.d.ts +6 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +13 -0
- package/dist/util.js.map +1 -0
- package/jest.config.js +8 -0
- package/package.json +37 -0
- package/src/events.ts +61 -0
- package/src/firehose/index.ts +357 -0
- package/src/firehose/lexicons.ts +407 -0
- package/src/index.ts +3 -0
- package/src/runner/consecutive-list.ts +44 -0
- package/src/runner/index.ts +3 -0
- package/src/runner/memory-runner.ts +72 -0
- package/src/runner/types.ts +8 -0
- package/src/util.ts +10 -0
- package/tests/firehose.test.ts +180 -0
- package/tests/runner.test.ts +122 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MemoryRunner = exports.ConsecutiveList = void 0;
|
|
7
|
+
const p_queue_1 = __importDefault(require("p-queue"));
|
|
8
|
+
const consecutive_list_1 = require("./consecutive-list");
|
|
9
|
+
Object.defineProperty(exports, "ConsecutiveList", { enumerable: true, get: function () { return consecutive_list_1.ConsecutiveList; } });
|
|
10
|
+
// A queue with arbitrarily many partitions, each processing work sequentially.
|
|
11
|
+
// Partitions are created lazily and taken out of memory when they go idle.
|
|
12
|
+
class MemoryRunner {
|
|
13
|
+
constructor(opts = {}) {
|
|
14
|
+
Object.defineProperty(this, "opts", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: opts
|
|
19
|
+
});
|
|
20
|
+
Object.defineProperty(this, "consecutive", {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: true,
|
|
23
|
+
writable: true,
|
|
24
|
+
value: new consecutive_list_1.ConsecutiveList()
|
|
25
|
+
});
|
|
26
|
+
Object.defineProperty(this, "mainQueue", {
|
|
27
|
+
enumerable: true,
|
|
28
|
+
configurable: true,
|
|
29
|
+
writable: true,
|
|
30
|
+
value: void 0
|
|
31
|
+
});
|
|
32
|
+
Object.defineProperty(this, "partitions", {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
writable: true,
|
|
36
|
+
value: new Map()
|
|
37
|
+
});
|
|
38
|
+
Object.defineProperty(this, "cursor", {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
writable: true,
|
|
42
|
+
value: void 0
|
|
43
|
+
});
|
|
44
|
+
this.mainQueue = new p_queue_1.default({ concurrency: opts.concurrency ?? Infinity });
|
|
45
|
+
this.cursor = opts.startCursor;
|
|
46
|
+
}
|
|
47
|
+
getCursor() {
|
|
48
|
+
return this.cursor;
|
|
49
|
+
}
|
|
50
|
+
async addTask(partitionId, task) {
|
|
51
|
+
if (this.mainQueue.isPaused)
|
|
52
|
+
return;
|
|
53
|
+
return this.mainQueue.add(() => {
|
|
54
|
+
return this.getPartition(partitionId).add(task);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
getPartition(partitionId) {
|
|
58
|
+
let partition = this.partitions.get(partitionId);
|
|
59
|
+
if (!partition) {
|
|
60
|
+
partition = new p_queue_1.default({ concurrency: 1 });
|
|
61
|
+
partition.once('idle', () => this.partitions.delete(partitionId));
|
|
62
|
+
this.partitions.set(partitionId, partition);
|
|
63
|
+
}
|
|
64
|
+
return partition;
|
|
65
|
+
}
|
|
66
|
+
async trackEvent(did, seq, handler) {
|
|
67
|
+
if (this.mainQueue.isPaused)
|
|
68
|
+
return;
|
|
69
|
+
const item = this.consecutive.push(seq);
|
|
70
|
+
await this.addTask(did, async () => {
|
|
71
|
+
await handler();
|
|
72
|
+
const latest = item.complete().at(-1);
|
|
73
|
+
if (latest !== undefined) {
|
|
74
|
+
this.cursor = latest;
|
|
75
|
+
if (this.opts.setCursor) {
|
|
76
|
+
await this.opts.setCursor(this.cursor);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async processAll() {
|
|
82
|
+
await this.mainQueue.onIdle();
|
|
83
|
+
}
|
|
84
|
+
async destroy() {
|
|
85
|
+
this.mainQueue.pause();
|
|
86
|
+
this.mainQueue.clear();
|
|
87
|
+
this.partitions.forEach((p) => p.clear());
|
|
88
|
+
await this.mainQueue.onIdle();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.MemoryRunner = MemoryRunner;
|
|
92
|
+
//# sourceMappingURL=memory-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-runner.js","sourceRoot":"","sources":["../../src/runner/memory-runner.ts"],"names":[],"mappings":";;;;;;AAAA,sDAA4B;AAC5B,yDAAoD;AAG3C,gGAHA,kCAAe,OAGA;AAQxB,+EAA+E;AAC/E,2EAA2E;AAC3E,MAAa,YAAY;IAMvB,YAAmB,OAA4B,EAAE;QAArC;;;;mBAAO,IAAI;WAA0B;QALjD;;;;mBAAc,IAAI,kCAAe,EAAU;WAAA;QAC3C;;;;;WAAiB;QACjB;;;;mBAAa,IAAI,GAAG,EAAkB;WAAA;QACtC;;;;;WAA0B;QAGxB,IAAI,CAAC,SAAS,GAAG,IAAI,iBAAM,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,QAAQ,EAAE,CAAC,CAAA;QAC1E,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,WAAW,CAAA;IAChC,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,MAAM,CAAA;IACpB,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,WAAmB,EAAE,IAAyB;QAC1D,IAAI,IAAI,CAAC,SAAS,CAAC,QAAQ;YAAE,OAAM;QACnC,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE;YAC7B,OAAO,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACjD,CAAC,CAAC,CAAA;IACJ,CAAC;IAEO,YAAY,CAAC,WAAmB;QACtC,IAAI,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QAChD,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,SAAS,GAAG,IAAI,iBAAM,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,CAAA;YAC1C,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAA;YACjE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;QAC7C,CAAC;QACD,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,GAAW,EAAE,GAAW,EAAE,OAA4B;QACrE,IAAI,IAAI,CAAC,SAAS,CAAC,QAAQ;YAAE,OAAM;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACvC,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,IAAI,EAAE;YACjC,MAAM,OAAO,EAAE,CAAA;YACf,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;YACrC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;gBACpB,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;oBACxB,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;gBACxC,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,UAAU;QACd,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAA;IAC/B,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAA;QACtB,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAA;QACtB,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAA;QACzC,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAA;IAC/B,CAAC;CACF;AAzDD,oCAyDC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/runner/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,SAAS,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IACxC,UAAU,CACR,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAC3B,OAAO,CAAC,IAAI,CAAC,CAAA;CACjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/runner/types.ts"],"names":[],"mappings":""}
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAEhF,eAAO,MAAM,eAAe,QACrB,SAAS,KACb;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,SAKjC,CAAA"}
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.didAndSeqForEvt = void 0;
|
|
4
|
+
const lexicons_1 = require("./firehose/lexicons");
|
|
5
|
+
const didAndSeqForEvt = (evt) => {
|
|
6
|
+
if ((0, lexicons_1.isCommit)(evt))
|
|
7
|
+
return { seq: evt.seq, did: evt.repo };
|
|
8
|
+
else if ((0, lexicons_1.isAccount)(evt) || (0, lexicons_1.isIdentity)(evt))
|
|
9
|
+
return { seq: evt.seq, did: evt.did };
|
|
10
|
+
return undefined;
|
|
11
|
+
};
|
|
12
|
+
exports.didAndSeqForEvt = didAndSeqForEvt;
|
|
13
|
+
//# sourceMappingURL=util.js.map
|
package/dist/util.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;;AAAA,kDAAgF;AAEzE,MAAM,eAAe,GAAG,CAC7B,GAAc,EAC4B,EAAE;IAC5C,IAAI,IAAA,mBAAQ,EAAC,GAAG,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,CAAA;SACpD,IAAI,IAAA,oBAAS,EAAC,GAAG,CAAC,IAAI,IAAA,qBAAU,EAAC,GAAG,CAAC;QACxC,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAA;IACvC,OAAO,SAAS,CAAA;AAClB,CAAC,CAAA;AAPY,QAAA,eAAe,mBAO3B"}
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
displayName: 'Sync',
|
|
4
|
+
transform: { '^.+\\.(t|j)s$': '@swc/jest' },
|
|
5
|
+
transformIgnorePatterns: [`<rootDir>/node_modules/(?!get-port)`],
|
|
6
|
+
testTimeout: 60000,
|
|
7
|
+
setupFiles: ['<rootDir>/../../jest.setup.ts'],
|
|
8
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atproto/sync",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "atproto sync library",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"atproto",
|
|
8
|
+
"sync",
|
|
9
|
+
"firehose",
|
|
10
|
+
"relay"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://atproto.com",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/bluesky-social/atproto",
|
|
16
|
+
"directory": "packages/sync"
|
|
17
|
+
},
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"multiformats": "^9.9.0",
|
|
22
|
+
"p-queue": "^6.6.2",
|
|
23
|
+
"@atproto/common": "^0.4.1",
|
|
24
|
+
"@atproto/identity": "^0.4.1",
|
|
25
|
+
"@atproto/lexicon": "^0.4.1",
|
|
26
|
+
"@atproto/repo": "^0.5.0",
|
|
27
|
+
"@atproto/syntax": "^0.3.0",
|
|
28
|
+
"@atproto/xrpc-server": "^0.6.3"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"jest": "^28.1.2"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc --build tsconfig.build.json",
|
|
35
|
+
"test": "../dev-infra/with-test-redis-and-db.sh jest"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { DidDocument } from '@atproto/identity'
|
|
2
|
+
import type { RepoRecord } from '@atproto/lexicon'
|
|
3
|
+
import { BlockMap } from '@atproto/repo'
|
|
4
|
+
import { AtUri } from '@atproto/syntax'
|
|
5
|
+
import type { CID } from 'multiformats/cid'
|
|
6
|
+
|
|
7
|
+
export type Event = CommitEvt | IdentityEvt | AccountEvt
|
|
8
|
+
|
|
9
|
+
export type CommitMeta = {
|
|
10
|
+
seq: number
|
|
11
|
+
time: string
|
|
12
|
+
commit: CID
|
|
13
|
+
blocks: BlockMap
|
|
14
|
+
rev: string
|
|
15
|
+
uri: AtUri
|
|
16
|
+
did: string
|
|
17
|
+
collection: string
|
|
18
|
+
rkey: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CommitEvt = Create | Update | Delete
|
|
22
|
+
|
|
23
|
+
export type Create = CommitMeta & {
|
|
24
|
+
event: 'create'
|
|
25
|
+
record: RepoRecord
|
|
26
|
+
cid: CID
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type Update = CommitMeta & {
|
|
30
|
+
event: 'update'
|
|
31
|
+
record: RepoRecord
|
|
32
|
+
cid: CID
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type Delete = CommitMeta & {
|
|
36
|
+
event: 'delete'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type IdentityEvt = {
|
|
40
|
+
seq: number
|
|
41
|
+
time: string
|
|
42
|
+
event: 'identity'
|
|
43
|
+
did: string
|
|
44
|
+
handle?: string
|
|
45
|
+
didDocument?: DidDocument
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type AccountEvt = {
|
|
49
|
+
seq: number
|
|
50
|
+
time: string
|
|
51
|
+
event: 'account'
|
|
52
|
+
did: string
|
|
53
|
+
active: boolean
|
|
54
|
+
status?: AccountStatus
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type AccountStatus =
|
|
58
|
+
| 'takendown'
|
|
59
|
+
| 'suspended'
|
|
60
|
+
| 'deleted'
|
|
61
|
+
| 'deactivated'
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { createDeferrable, Deferrable, wait } from '@atproto/common'
|
|
2
|
+
import {
|
|
3
|
+
IdResolver,
|
|
4
|
+
parseToAtprotoDocument,
|
|
5
|
+
DidDocument,
|
|
6
|
+
} from '@atproto/identity'
|
|
7
|
+
import {
|
|
8
|
+
cborToLexRecord,
|
|
9
|
+
formatDataKey,
|
|
10
|
+
parseDataKey,
|
|
11
|
+
readCar,
|
|
12
|
+
RepoVerificationError,
|
|
13
|
+
verifyProofs,
|
|
14
|
+
} from '@atproto/repo'
|
|
15
|
+
import { AtUri } from '@atproto/syntax'
|
|
16
|
+
import { Subscription } from '@atproto/xrpc-server'
|
|
17
|
+
import {
|
|
18
|
+
type Account,
|
|
19
|
+
type Commit,
|
|
20
|
+
type Identity,
|
|
21
|
+
type RepoEvent,
|
|
22
|
+
RepoOp,
|
|
23
|
+
isAccount,
|
|
24
|
+
isCommit,
|
|
25
|
+
isIdentity,
|
|
26
|
+
isValidRepoEvent,
|
|
27
|
+
} from './lexicons'
|
|
28
|
+
import {
|
|
29
|
+
Event,
|
|
30
|
+
CommitMeta,
|
|
31
|
+
CommitEvt,
|
|
32
|
+
AccountEvt,
|
|
33
|
+
AccountStatus,
|
|
34
|
+
IdentityEvt,
|
|
35
|
+
} from '../events'
|
|
36
|
+
import { CID } from 'multiformats/cid'
|
|
37
|
+
import { EventRunner } from '../runner'
|
|
38
|
+
import { didAndSeqForEvt } from '../util'
|
|
39
|
+
|
|
40
|
+
export type FirehoseOptions = {
|
|
41
|
+
idResolver: IdResolver
|
|
42
|
+
|
|
43
|
+
handleEvent: (evt: Event) => Awaited<void>
|
|
44
|
+
onError: (err: Error) => void
|
|
45
|
+
getCursor?: () => Awaited<number | undefined>
|
|
46
|
+
|
|
47
|
+
runner?: EventRunner // should only set getCursor *or* runner
|
|
48
|
+
|
|
49
|
+
service?: string
|
|
50
|
+
subscriptionReconnectDelay?: number
|
|
51
|
+
|
|
52
|
+
unauthenticatedCommits?: boolean
|
|
53
|
+
unauthenticatedHandles?: boolean
|
|
54
|
+
|
|
55
|
+
filterCollections?: string[]
|
|
56
|
+
excludeIdentity?: boolean
|
|
57
|
+
excludeAccount?: boolean
|
|
58
|
+
excludeCommit?: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class Firehose {
|
|
62
|
+
private sub: Subscription<RepoEvent>
|
|
63
|
+
private abortController: AbortController
|
|
64
|
+
private destoryDefer: Deferrable
|
|
65
|
+
|
|
66
|
+
constructor(public opts: FirehoseOptions) {
|
|
67
|
+
this.destoryDefer = createDeferrable()
|
|
68
|
+
this.abortController = new AbortController()
|
|
69
|
+
if (this.opts.getCursor && this.opts.runner) {
|
|
70
|
+
throw new Error('Must set only `getCursor` or `runner`')
|
|
71
|
+
}
|
|
72
|
+
this.sub = new Subscription({
|
|
73
|
+
service: opts.service ?? 'wss://bsky.network',
|
|
74
|
+
method: 'com.atproto.sync.subscribeRepos',
|
|
75
|
+
signal: this.abortController.signal,
|
|
76
|
+
getParams: async () => {
|
|
77
|
+
const getCursorFn = () =>
|
|
78
|
+
this.opts.runner?.getCursor() ?? this.opts.getCursor
|
|
79
|
+
if (!getCursorFn) {
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
const cursor = await getCursorFn()
|
|
83
|
+
return { cursor }
|
|
84
|
+
},
|
|
85
|
+
validate: (value: unknown) => {
|
|
86
|
+
try {
|
|
87
|
+
return isValidRepoEvent(value)
|
|
88
|
+
} catch (err) {
|
|
89
|
+
this.opts.onError(new FirehoseValidationError(err, value))
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async start() {
|
|
96
|
+
try {
|
|
97
|
+
for await (const evt of this.sub) {
|
|
98
|
+
if (this.opts.runner) {
|
|
99
|
+
const parsed = didAndSeqForEvt(evt)
|
|
100
|
+
if (!parsed) {
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
this.opts.runner.trackEvent(parsed.did, parsed.seq, async () => {
|
|
104
|
+
const parsed = await this.parseEvt(evt)
|
|
105
|
+
for (const write of parsed) {
|
|
106
|
+
try {
|
|
107
|
+
await this.opts.handleEvent(write)
|
|
108
|
+
} catch (err) {
|
|
109
|
+
this.opts.onError(new FirehoseHandlerError(err, write))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
} else {
|
|
114
|
+
await this.processEvt(evt)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err && err['name'] === 'AbortError') {
|
|
119
|
+
this.destoryDefer.resolve()
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
this.opts.onError(new FirehoseSubscriptionError(err))
|
|
123
|
+
await wait(this.opts.subscriptionReconnectDelay ?? 3000)
|
|
124
|
+
return this.start()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private async parseEvt(evt: RepoEvent): Promise<Event[]> {
|
|
129
|
+
try {
|
|
130
|
+
if (isCommit(evt) && !this.opts.excludeCommit) {
|
|
131
|
+
return this.opts.unauthenticatedCommits
|
|
132
|
+
? await parseCommitUnauthenticated(evt, this.opts.filterCollections)
|
|
133
|
+
: await parseCommitAuthenticated(
|
|
134
|
+
this.opts.idResolver,
|
|
135
|
+
evt,
|
|
136
|
+
this.opts.filterCollections,
|
|
137
|
+
)
|
|
138
|
+
} else if (isAccount(evt) && !this.opts.excludeAccount) {
|
|
139
|
+
const parsed = parseAccount(evt)
|
|
140
|
+
return parsed ? [parsed] : []
|
|
141
|
+
} else if (isIdentity(evt) && !this.opts.excludeIdentity) {
|
|
142
|
+
const parsed = await parseIdentity(
|
|
143
|
+
this.opts.idResolver,
|
|
144
|
+
evt,
|
|
145
|
+
this.opts.unauthenticatedHandles,
|
|
146
|
+
)
|
|
147
|
+
return parsed ? [parsed] : []
|
|
148
|
+
} else {
|
|
149
|
+
return []
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
this.opts.onError(new FirehoseParseError(err, evt))
|
|
153
|
+
return []
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private async processEvt(evt: RepoEvent) {
|
|
158
|
+
const parsed = await this.parseEvt(evt)
|
|
159
|
+
for (const write of parsed) {
|
|
160
|
+
try {
|
|
161
|
+
await this.opts.handleEvent(write)
|
|
162
|
+
} catch (err) {
|
|
163
|
+
this.opts.onError(new FirehoseHandlerError(err, write))
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async destroy(): Promise<void> {
|
|
169
|
+
this.abortController.abort()
|
|
170
|
+
await this.destoryDefer.complete
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const parseCommitAuthenticated = async (
|
|
175
|
+
idResolver: IdResolver,
|
|
176
|
+
evt: Commit,
|
|
177
|
+
filterCollections?: string[],
|
|
178
|
+
forceKeyRefresh = false,
|
|
179
|
+
): Promise<CommitEvt[]> => {
|
|
180
|
+
const did = evt.repo
|
|
181
|
+
const key = await idResolver.did.resolveAtprotoKey(did, forceKeyRefresh)
|
|
182
|
+
const claims = maybeFilterOps(evt.ops, filterCollections).map((op) => {
|
|
183
|
+
const { collection, rkey } = parseDataKey(op.path)
|
|
184
|
+
return {
|
|
185
|
+
collection,
|
|
186
|
+
rkey,
|
|
187
|
+
cid: op.action === 'delete' ? null : op.cid,
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
const verifiedCids: Record<string, CID | null> = {}
|
|
191
|
+
try {
|
|
192
|
+
const results = await verifyProofs(evt.blocks, claims, did, key)
|
|
193
|
+
results.verified.forEach((op) => {
|
|
194
|
+
const path = formatDataKey(op.collection, op.rkey)
|
|
195
|
+
verifiedCids[path] = op.cid
|
|
196
|
+
})
|
|
197
|
+
} catch (err) {
|
|
198
|
+
if (err instanceof RepoVerificationError && !forceKeyRefresh) {
|
|
199
|
+
return parseCommitAuthenticated(idResolver, evt, filterCollections, true)
|
|
200
|
+
}
|
|
201
|
+
throw err
|
|
202
|
+
}
|
|
203
|
+
const verifiedOps: RepoOp[] = evt.ops.filter((op) => {
|
|
204
|
+
if (op.action === 'delete') {
|
|
205
|
+
return verifiedCids[op.path] === null
|
|
206
|
+
} else {
|
|
207
|
+
return op.cid !== null && op.cid.equals(verifiedCids[op.path])
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
return formatCommitOps(evt, verifiedOps)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const parseCommitUnauthenticated = async (
|
|
214
|
+
evt: Commit,
|
|
215
|
+
filterCollections?: string[],
|
|
216
|
+
): Promise<CommitEvt[]> => {
|
|
217
|
+
const ops = maybeFilterOps(evt.ops, filterCollections)
|
|
218
|
+
return formatCommitOps(evt, ops)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const maybeFilterOps = (
|
|
222
|
+
ops: RepoOp[],
|
|
223
|
+
filterCollections?: string[],
|
|
224
|
+
): RepoOp[] => {
|
|
225
|
+
if (!filterCollections) return ops
|
|
226
|
+
return ops.filter((op) => {
|
|
227
|
+
const { collection } = parseDataKey(op.path)
|
|
228
|
+
return filterCollections.includes(collection)
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const formatCommitOps = async (evt: Commit, ops: RepoOp[]) => {
|
|
233
|
+
const car = await readCar(evt.blocks)
|
|
234
|
+
|
|
235
|
+
const evts: CommitEvt[] = []
|
|
236
|
+
|
|
237
|
+
for (const op of ops) {
|
|
238
|
+
const uri = AtUri.make(evt.repo, op.path)
|
|
239
|
+
|
|
240
|
+
const meta: CommitMeta = {
|
|
241
|
+
seq: evt.seq,
|
|
242
|
+
time: evt.time,
|
|
243
|
+
commit: evt.commit,
|
|
244
|
+
blocks: car.blocks,
|
|
245
|
+
rev: evt.rev,
|
|
246
|
+
uri,
|
|
247
|
+
did: uri.host,
|
|
248
|
+
collection: uri.collection,
|
|
249
|
+
rkey: uri.rkey,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (op.action === 'create' || op.action === 'update') {
|
|
253
|
+
if (!op.cid) continue
|
|
254
|
+
const recordBytes = car.blocks.get(op.cid)
|
|
255
|
+
if (!recordBytes) continue
|
|
256
|
+
const record = cborToLexRecord(recordBytes)
|
|
257
|
+
evts.push({
|
|
258
|
+
...meta,
|
|
259
|
+
event: op.action as 'create' | 'update',
|
|
260
|
+
cid: op.cid,
|
|
261
|
+
record,
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (op.action === 'delete') {
|
|
266
|
+
evts.push({
|
|
267
|
+
...meta,
|
|
268
|
+
event: 'delete',
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return evts
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export const parseIdentity = async (
|
|
277
|
+
idResolver: IdResolver,
|
|
278
|
+
evt: Identity,
|
|
279
|
+
unauthenticated = false,
|
|
280
|
+
): Promise<IdentityEvt | null> => {
|
|
281
|
+
const res = await idResolver.did.resolve(evt.did)
|
|
282
|
+
const handle =
|
|
283
|
+
res && !unauthenticated
|
|
284
|
+
? await verifyHandle(idResolver, evt.did, res)
|
|
285
|
+
: undefined
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
event: 'identity',
|
|
289
|
+
seq: evt.seq,
|
|
290
|
+
time: evt.time,
|
|
291
|
+
did: evt.did,
|
|
292
|
+
handle,
|
|
293
|
+
didDocument: res ?? undefined,
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const verifyHandle = async (
|
|
298
|
+
idResolver: IdResolver,
|
|
299
|
+
did: string,
|
|
300
|
+
didDoc: DidDocument,
|
|
301
|
+
): Promise<string | undefined> => {
|
|
302
|
+
const { handle } = parseToAtprotoDocument(didDoc)
|
|
303
|
+
if (!handle) {
|
|
304
|
+
return undefined
|
|
305
|
+
}
|
|
306
|
+
const res = await idResolver.handle.resolve(handle)
|
|
307
|
+
return res === did ? handle : undefined
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export const parseAccount = (evt: Account): AccountEvt | undefined => {
|
|
311
|
+
if (evt.status && !isValidStatus(evt.status)) return
|
|
312
|
+
return {
|
|
313
|
+
event: 'account',
|
|
314
|
+
seq: evt.seq,
|
|
315
|
+
time: evt.time,
|
|
316
|
+
did: evt.did,
|
|
317
|
+
active: evt.active,
|
|
318
|
+
status: evt.status as AccountStatus | undefined,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const isValidStatus = (str: string): str is AccountStatus => {
|
|
323
|
+
return ['takendown', 'suspended', 'deleted', 'deactivated'].includes(str)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export class FirehoseValidationError extends Error {
|
|
327
|
+
constructor(
|
|
328
|
+
err: unknown,
|
|
329
|
+
public value: unknown,
|
|
330
|
+
) {
|
|
331
|
+
super('error in firehose event lexicon validation', { cause: err })
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export class FirehoseParseError extends Error {
|
|
336
|
+
constructor(
|
|
337
|
+
err: unknown,
|
|
338
|
+
public event: RepoEvent,
|
|
339
|
+
) {
|
|
340
|
+
super('error in parsing and authenticating firehose event', { cause: err })
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export class FirehoseSubscriptionError extends Error {
|
|
345
|
+
constructor(err: unknown) {
|
|
346
|
+
super('error on firehose subscription', { cause: err })
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export class FirehoseHandlerError extends Error {
|
|
351
|
+
constructor(
|
|
352
|
+
err: unknown,
|
|
353
|
+
public event: Event,
|
|
354
|
+
) {
|
|
355
|
+
super('error in firehose event handler', { cause: err })
|
|
356
|
+
}
|
|
357
|
+
}
|