@atproto/bsky 0.0.124 → 0.0.126
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 +22 -0
- package/dist/api/app/bsky/notification/listNotifications.d.ts +7 -0
- package/dist/api/app/bsky/notification/listNotifications.d.ts.map +1 -1
- package/dist/api/app/bsky/notification/listNotifications.js +21 -5
- package/dist/api/app/bsky/notification/listNotifications.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +24 -15
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +6 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +6 -0
- package/dist/context.js.map +1 -1
- package/dist/data-plane/client/hosts.d.ts +37 -0
- package/dist/data-plane/client/hosts.d.ts.map +1 -0
- package/dist/data-plane/client/hosts.js +106 -0
- package/dist/data-plane/client/hosts.js.map +1 -0
- package/dist/data-plane/client/index.d.ts +13 -0
- package/dist/data-plane/client/index.d.ts.map +1 -0
- package/dist/data-plane/client/index.js +133 -0
- package/dist/data-plane/client/index.js.map +1 -0
- package/dist/data-plane/{client.d.ts → client/util.d.ts} +3 -10
- package/dist/data-plane/client/util.d.ts.map +1 -0
- package/dist/data-plane/client/util.js +85 -0
- package/dist/data-plane/client/util.js.map +1 -0
- package/dist/data-plane/server/db/pagination.d.ts +69 -9
- package/dist/data-plane/server/db/pagination.d.ts.map +1 -1
- package/dist/data-plane/server/db/pagination.js +114 -14
- package/dist/data-plane/server/db/pagination.js.map +1 -1
- package/dist/data-plane/server/routes/notifs.d.ts.map +1 -1
- package/dist/data-plane/server/routes/notifs.js +3 -5
- package/dist/data-plane/server/routes/notifs.js.map +1 -1
- package/dist/data-plane/server/subscription.d.ts.map +1 -1
- package/dist/data-plane/server/subscription.js +6 -0
- package/dist/data-plane/server/subscription.js.map +1 -1
- package/dist/etcd.d.ts +25 -0
- package/dist/etcd.d.ts.map +1 -0
- package/dist/etcd.js +109 -0
- package/dist/etcd.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/lexicon/index.d.ts +6 -0
- package/dist/lexicon/index.d.ts.map +1 -1
- package/dist/lexicon/index.js +12 -0
- package/dist/lexicon/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +304 -156
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +168 -80
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/embed/video.d.ts +1 -0
- package/dist/lexicon/types/app/bsky/embed/video.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/embed/video.js.map +1 -1
- package/dist/lexicon/types/com/atproto/identity/defs.d.ts +17 -0
- package/dist/lexicon/types/com/atproto/identity/defs.d.ts.map +1 -0
- package/dist/lexicon/types/com/atproto/identity/defs.js +16 -0
- package/dist/lexicon/types/com/atproto/identity/defs.js.map +1 -0
- package/dist/lexicon/types/com/atproto/identity/refreshIdentity.d.ts +39 -0
- package/dist/lexicon/types/com/atproto/identity/refreshIdentity.d.ts.map +1 -0
- package/dist/lexicon/types/com/atproto/identity/refreshIdentity.js +7 -0
- package/dist/lexicon/types/com/atproto/identity/refreshIdentity.js.map +1 -0
- package/dist/lexicon/types/com/atproto/identity/resolveDid.d.ts +40 -0
- package/dist/lexicon/types/com/atproto/identity/resolveDid.d.ts.map +1 -0
- package/dist/lexicon/types/com/atproto/identity/resolveDid.js +7 -0
- package/dist/lexicon/types/com/atproto/identity/resolveDid.js.map +1 -0
- package/dist/lexicon/types/com/atproto/identity/resolveHandle.d.ts +1 -0
- package/dist/lexicon/types/com/atproto/identity/resolveHandle.d.ts.map +1 -1
- package/dist/lexicon/types/com/atproto/identity/resolveIdentity.d.ts +36 -0
- package/dist/lexicon/types/com/atproto/identity/resolveIdentity.d.ts.map +1 -0
- package/dist/lexicon/types/com/atproto/identity/resolveIdentity.js +7 -0
- package/dist/lexicon/types/com/atproto/identity/resolveIdentity.js.map +1 -0
- package/dist/lexicon/types/com/atproto/repo/listRecords.d.ts +0 -4
- package/dist/lexicon/types/com/atproto/repo/listRecords.d.ts.map +1 -1
- package/dist/lexicon/types/com/atproto/repo/listRecords.js.map +1 -1
- package/dist/lexicon/types/com/atproto/sync/getRecord.d.ts +0 -2
- package/dist/lexicon/types/com/atproto/sync/getRecord.d.ts.map +1 -1
- package/dist/lexicon/types/com/atproto/sync/subscribeRepos.d.ts +1 -30
- package/dist/lexicon/types/com/atproto/sync/subscribeRepos.d.ts.map +1 -1
- package/dist/lexicon/types/com/atproto/sync/subscribeRepos.js +0 -27
- package/dist/lexicon/types/com/atproto/sync/subscribeRepos.js.map +1 -1
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +2 -1
- package/dist/logger.js.map +1 -1
- package/package.json +16 -15
- package/src/api/app/bsky/notification/listNotifications.ts +28 -6
- package/src/config.ts +45 -15
- package/src/context.ts +12 -1
- package/src/data-plane/client/hosts.ts +103 -0
- package/src/data-plane/client/index.ts +123 -0
- package/src/data-plane/client/util.ts +66 -0
- package/src/data-plane/server/db/pagination.ts +158 -35
- package/src/data-plane/server/routes/notifs.ts +4 -9
- package/src/data-plane/server/subscription.ts +7 -2
- package/src/etcd.ts +90 -0
- package/src/index.ts +26 -2
- package/src/lexicon/index.ts +36 -0
- package/src/lexicon/lexicons.ts +183 -83
- package/src/lexicon/types/app/bsky/embed/video.ts +1 -0
- package/src/lexicon/types/com/atproto/identity/defs.ts +30 -0
- package/src/lexicon/types/com/atproto/identity/refreshIdentity.ts +52 -0
- package/src/lexicon/types/com/atproto/identity/resolveDid.ts +52 -0
- package/src/lexicon/types/com/atproto/identity/resolveHandle.ts +1 -0
- package/src/lexicon/types/com/atproto/identity/resolveIdentity.ts +48 -0
- package/src/lexicon/types/com/atproto/repo/listRecords.ts +0 -4
- package/src/lexicon/types/com/atproto/sync/getRecord.ts +0 -2
- package/src/lexicon/types/com/atproto/sync/subscribeRepos.ts +0 -59
- package/src/logger.ts +2 -0
- package/tests/etcd.test.ts +301 -0
- package/tests/views/__snapshots__/notifications.test.ts.snap +3 -3
- package/tests/views/notifications.test.ts +190 -10
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.tests.tsbuildinfo +1 -1
- package/dist/data-plane/client.d.ts.map +0 -1
- package/dist/data-plane/client.js +0 -156
- package/dist/data-plane/client.js.map +0 -1
- package/src/data-plane/client.ts +0 -154
package/src/config.ts
CHANGED
|
@@ -10,7 +10,9 @@ export interface ServerConfigValues {
|
|
|
10
10
|
alternateAudienceDids: string[]
|
|
11
11
|
entrywayJwtPublicKeyHex?: string
|
|
12
12
|
// external services
|
|
13
|
+
etcdHosts: string[]
|
|
13
14
|
dataplaneUrls: string[]
|
|
15
|
+
dataplaneUrlsEtcdKeyPrefix?: string
|
|
14
16
|
dataplaneHttpVersion?: '1.1' | '2'
|
|
15
17
|
dataplaneIgnoreBadTls?: boolean
|
|
16
18
|
bsyncUrl: string
|
|
@@ -47,6 +49,8 @@ export interface ServerConfigValues {
|
|
|
47
49
|
bigThreadUris: Set<string>
|
|
48
50
|
bigThreadDepth?: number
|
|
49
51
|
maxThreadDepth?: number
|
|
52
|
+
// notifications
|
|
53
|
+
notificationsDelayMs?: number
|
|
50
54
|
// client config
|
|
51
55
|
clientCheckEmailConfirmed?: boolean
|
|
52
56
|
topicsEnabled?: boolean
|
|
@@ -74,15 +78,15 @@ export class ServerConfig {
|
|
|
74
78
|
const envPort = parseInt(process.env.BSKY_PORT || '', 10)
|
|
75
79
|
const port = isNaN(envPort) ? 2584 : envPort
|
|
76
80
|
const didPlcUrl = process.env.BSKY_DID_PLC_URL || 'http://localhost:2582'
|
|
77
|
-
const alternateAudienceDids = process.env.BSKY_ALT_AUDIENCE_DIDS
|
|
78
|
-
? process.env.BSKY_ALT_AUDIENCE_DIDS.split(',')
|
|
79
|
-
: []
|
|
81
|
+
const alternateAudienceDids = envList(process.env.BSKY_ALT_AUDIENCE_DIDS)
|
|
80
82
|
const entrywayJwtPublicKeyHex =
|
|
81
83
|
process.env.BSKY_ENTRYWAY_JWT_PUBLIC_KEY_HEX || undefined
|
|
82
|
-
const handleResolveNameservers =
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const handleResolveNameservers = envList(
|
|
85
|
+
process.env.BSKY_HANDLE_RESOLVE_NAMESERVERS,
|
|
86
|
+
)
|
|
85
87
|
const cdnUrl = process.env.BSKY_CDN_URL || process.env.BSKY_IMG_URI_ENDPOINT
|
|
88
|
+
const etcdHosts =
|
|
89
|
+
overrides?.etcdHosts ?? envList(process.env.BSKY_ETCD_HOSTS)
|
|
86
90
|
// e.g. https://video.invalid/watch/%s/%s/playlist.m3u8
|
|
87
91
|
const videoPlaylistUrlPattern = process.env.BSKY_VIDEO_PLAYLIST_URL_PATTERN
|
|
88
92
|
// e.g. https://video.invalid/watch/%s/%s/thumbnail.jpg
|
|
@@ -97,16 +101,25 @@ export class ServerConfig {
|
|
|
97
101
|
const suggestionsApiKey = process.env.BSKY_SUGGESTIONS_API_KEY || undefined
|
|
98
102
|
const topicsUrl = process.env.BSKY_TOPICS_URL || undefined
|
|
99
103
|
const topicsApiKey = process.env.BSKY_TOPICS_API_KEY
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
const dataplaneUrls =
|
|
105
|
+
overrides?.dataplaneUrls ?? envList(process.env.BSKY_DATAPLANE_URLS)
|
|
106
|
+
const dataplaneUrlsEtcdKeyPrefix =
|
|
107
|
+
process.env.BSKY_DATAPLANE_URLS_ETCD_KEY_PREFIX || undefined
|
|
104
108
|
const dataplaneHttpVersion = process.env.BSKY_DATAPLANE_HTTP_VERSION || '2'
|
|
105
109
|
const dataplaneIgnoreBadTls =
|
|
106
110
|
process.env.BSKY_DATAPLANE_IGNORE_BAD_TLS === 'true'
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
assert(
|
|
112
|
+
!dataplaneUrlsEtcdKeyPrefix || etcdHosts.length,
|
|
113
|
+
'etcd prefix for dataplane urls may only be configured when there are etcd hosts',
|
|
114
|
+
)
|
|
115
|
+
assert(
|
|
116
|
+
dataplaneUrls.length || dataplaneUrlsEtcdKeyPrefix,
|
|
117
|
+
'dataplane urls are not configured directly nor with etcd',
|
|
118
|
+
)
|
|
119
|
+
assert(dataplaneHttpVersion === '1.1' || dataplaneHttpVersion === '2')
|
|
120
|
+
const labelsFromIssuerDids = envList(
|
|
121
|
+
process.env.BSKY_LABELS_FROM_ISSUER_DIDS,
|
|
122
|
+
)
|
|
110
123
|
const bsyncUrl = process.env.BSKY_BSYNC_URL || undefined
|
|
111
124
|
assert(bsyncUrl)
|
|
112
125
|
const bsyncApiKey = process.env.BSKY_BSYNC_API_KEY || undefined
|
|
@@ -133,8 +146,6 @@ export class ServerConfig {
|
|
|
133
146
|
)
|
|
134
147
|
const modServiceDid = process.env.MOD_SERVICE_DID
|
|
135
148
|
assert(modServiceDid)
|
|
136
|
-
assert(dataplaneUrls.length)
|
|
137
|
-
assert(dataplaneHttpVersion === '1.1' || dataplaneHttpVersion === '2')
|
|
138
149
|
const statsigKey =
|
|
139
150
|
process.env.NODE_ENV === 'test'
|
|
140
151
|
? 'secret-key'
|
|
@@ -161,6 +172,10 @@ export class ServerConfig {
|
|
|
161
172
|
? parseInt(process.env.BSKY_MAX_THREAD_DEPTH || '', 10)
|
|
162
173
|
: undefined
|
|
163
174
|
|
|
175
|
+
const notificationsDelayMs = process.env.BSKY_NOTIFICATIONS_DELAY_MS
|
|
176
|
+
? parseInt(process.env.BSKY_NOTIFICATIONS_DELAY_MS || '', 10)
|
|
177
|
+
: 0
|
|
178
|
+
|
|
164
179
|
const disableSsrfProtection = process.env.BSKY_DISABLE_SSRF_PROTECTION
|
|
165
180
|
? process.env.BSKY_DISABLE_SSRF_PROTECTION === 'true'
|
|
166
181
|
: debugMode
|
|
@@ -185,7 +200,9 @@ export class ServerConfig {
|
|
|
185
200
|
serverDid,
|
|
186
201
|
alternateAudienceDids,
|
|
187
202
|
entrywayJwtPublicKeyHex,
|
|
203
|
+
etcdHosts,
|
|
188
204
|
dataplaneUrls,
|
|
205
|
+
dataplaneUrlsEtcdKeyPrefix,
|
|
189
206
|
dataplaneHttpVersion,
|
|
190
207
|
dataplaneIgnoreBadTls,
|
|
191
208
|
searchUrl,
|
|
@@ -220,6 +237,7 @@ export class ServerConfig {
|
|
|
220
237
|
bigThreadUris,
|
|
221
238
|
bigThreadDepth,
|
|
222
239
|
maxThreadDepth,
|
|
240
|
+
notificationsDelayMs,
|
|
223
241
|
disableSsrfProtection,
|
|
224
242
|
proxyAllowHTTP2,
|
|
225
243
|
proxyHeadersTimeout,
|
|
@@ -267,6 +285,14 @@ export class ServerConfig {
|
|
|
267
285
|
return this.cfg.entrywayJwtPublicKeyHex
|
|
268
286
|
}
|
|
269
287
|
|
|
288
|
+
get etcdHosts() {
|
|
289
|
+
return this.cfg.etcdHosts
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
get dataplaneUrlsEtcdKeyPrefix() {
|
|
293
|
+
return this.cfg.dataplaneUrlsEtcdKeyPrefix
|
|
294
|
+
}
|
|
295
|
+
|
|
270
296
|
get dataplaneUrls() {
|
|
271
297
|
return this.cfg.dataplaneUrls
|
|
272
298
|
}
|
|
@@ -407,6 +433,10 @@ export class ServerConfig {
|
|
|
407
433
|
return this.cfg.maxThreadDepth
|
|
408
434
|
}
|
|
409
435
|
|
|
436
|
+
get notificationsDelayMs() {
|
|
437
|
+
return this.cfg.notificationsDelayMs ?? 0
|
|
438
|
+
}
|
|
439
|
+
|
|
410
440
|
get disableSsrfProtection(): boolean {
|
|
411
441
|
return this.cfg.disableSsrfProtection ?? false
|
|
412
442
|
}
|
package/src/context.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as plc from '@did-plc/lib'
|
|
2
|
+
import { Etcd3 } from 'etcd3'
|
|
2
3
|
import express from 'express'
|
|
3
4
|
import { Dispatcher } from 'undici'
|
|
4
5
|
import { AtpAgent } from '@atproto/api'
|
|
@@ -8,7 +9,7 @@ import { AuthVerifier } from './auth-verifier'
|
|
|
8
9
|
import { BsyncClient } from './bsync'
|
|
9
10
|
import { ServerConfig } from './config'
|
|
10
11
|
import { CourierClient } from './courier'
|
|
11
|
-
import { DataPlaneClient } from './data-plane/client'
|
|
12
|
+
import { DataPlaneClient, HostList } from './data-plane/client'
|
|
12
13
|
import { FeatureGates } from './feature-gates'
|
|
13
14
|
import { Hydrator } from './hydration/hydrator'
|
|
14
15
|
import { httpLogger as log } from './logger'
|
|
@@ -23,7 +24,9 @@ export class AppContext {
|
|
|
23
24
|
constructor(
|
|
24
25
|
private opts: {
|
|
25
26
|
cfg: ServerConfig
|
|
27
|
+
etcd: Etcd3 | undefined
|
|
26
28
|
dataplane: DataPlaneClient
|
|
29
|
+
dataplaneHostList: HostList
|
|
27
30
|
searchAgent: AtpAgent | undefined
|
|
28
31
|
suggestionsAgent: AtpAgent | undefined
|
|
29
32
|
topicsAgent: AtpAgent | undefined
|
|
@@ -43,10 +46,18 @@ export class AppContext {
|
|
|
43
46
|
return this.opts.cfg
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
get etcd() {
|
|
50
|
+
return this.opts.etcd
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
get dataplane(): DataPlaneClient {
|
|
47
54
|
return this.opts.dataplane
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
get dataplaneHostList(): HostList {
|
|
58
|
+
return this.opts.dataplaneHostList
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
get searchAgent(): AtpAgent | undefined {
|
|
51
62
|
return this.opts.searchAgent
|
|
52
63
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Etcd3 } from 'etcd3'
|
|
2
|
+
import { EtcdMap } from '../../etcd'
|
|
3
|
+
import { dataplaneLogger as logger } from '../../logger'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Interface for a reactive list of hosts, i.e. for use with the dataplane client.
|
|
7
|
+
*/
|
|
8
|
+
export interface HostList {
|
|
9
|
+
get: () => Iterable<string>
|
|
10
|
+
onUpdate(handler: HostListHandler): void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type HostListHandler = (hosts: Iterable<string>) => void
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Maintains a reactive HostList based on a simple setter.
|
|
17
|
+
*/
|
|
18
|
+
export class BasicHostList implements HostList {
|
|
19
|
+
private hosts: Iterable<string>
|
|
20
|
+
private handlers: HostListHandler[] = []
|
|
21
|
+
|
|
22
|
+
constructor(hosts: Iterable<string>) {
|
|
23
|
+
this.hosts = hosts
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get() {
|
|
27
|
+
return this.hosts
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
set(hosts: Iterable<string>) {
|
|
31
|
+
this.hosts = hosts
|
|
32
|
+
this.update()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private update() {
|
|
36
|
+
for (const handler of this.handlers) {
|
|
37
|
+
handler(this.hosts)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onUpdate(handler: HostListHandler) {
|
|
42
|
+
this.handlers.push(handler)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Maintains a reactive HostList based on etcd key values under a given key prefix.
|
|
48
|
+
* When fallback is provided, ensures that this fallback is used whenever no hosts are available.
|
|
49
|
+
*/
|
|
50
|
+
export class EtcdHostList implements HostList {
|
|
51
|
+
private kv: EtcdMap
|
|
52
|
+
private inner = new BasicHostList(new Set())
|
|
53
|
+
private fallback: Set<string>
|
|
54
|
+
|
|
55
|
+
constructor(etcd: Etcd3, prefix: string, fallback?: string[]) {
|
|
56
|
+
this.fallback = new Set(fallback)
|
|
57
|
+
this.kv = new EtcdMap(etcd, prefix)
|
|
58
|
+
this.update() // init fallback if necessary
|
|
59
|
+
this.kv.watcher.on('connected', (res) => {
|
|
60
|
+
logger.warn(
|
|
61
|
+
{ watcherId: this.kv.watcher.id, header: res.header },
|
|
62
|
+
'EtcdHostList connected',
|
|
63
|
+
)
|
|
64
|
+
})
|
|
65
|
+
this.kv.watcher.on('disconnected', (err) => {
|
|
66
|
+
logger.warn(
|
|
67
|
+
{ watcherId: this.kv.watcher.id, err },
|
|
68
|
+
'EtcdHostList disconnected',
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
this.kv.watcher.on('error', (err) => {
|
|
72
|
+
logger.error({ watcherId: this.kv.watcher.id, err }, 'EtcdHostList error')
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async connect() {
|
|
77
|
+
await this.kv.connect()
|
|
78
|
+
this.update()
|
|
79
|
+
this.kv.onUpdate(() => this.update())
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get() {
|
|
83
|
+
return this.inner.get()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private update() {
|
|
87
|
+
const hosts = new Set<string>()
|
|
88
|
+
for (const host of this.kv.values()) {
|
|
89
|
+
if (URL.canParse(host)) {
|
|
90
|
+
hosts.add(host)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (hosts.size) {
|
|
94
|
+
this.inner.set(hosts)
|
|
95
|
+
} else if (this.fallback.size) {
|
|
96
|
+
this.inner.set(this.fallback)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
onUpdate(handler: HostListHandler) {
|
|
101
|
+
this.inner.onUpdate(handler)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { randomInt } from 'node:crypto'
|
|
3
|
+
import {
|
|
4
|
+
Code,
|
|
5
|
+
ConnectError,
|
|
6
|
+
PromiseClient,
|
|
7
|
+
createPromiseClient,
|
|
8
|
+
makeAnyClient,
|
|
9
|
+
} from '@connectrpc/connect'
|
|
10
|
+
import { createGrpcTransport } from '@connectrpc/connect-node'
|
|
11
|
+
import { Service } from '../../proto/bsky_connect'
|
|
12
|
+
import { HostList } from './hosts'
|
|
13
|
+
|
|
14
|
+
export * from './hosts'
|
|
15
|
+
export * from './util'
|
|
16
|
+
|
|
17
|
+
export type DataPlaneClient = PromiseClient<typeof Service>
|
|
18
|
+
type HttpVersion = '1.1' | '2'
|
|
19
|
+
const MAX_RETRIES = 3
|
|
20
|
+
|
|
21
|
+
export const createDataPlaneClient = (
|
|
22
|
+
hostList: HostList,
|
|
23
|
+
opts: { httpVersion?: HttpVersion; rejectUnauthorized?: boolean },
|
|
24
|
+
) => {
|
|
25
|
+
const clients = new DataPlaneClients(hostList, opts)
|
|
26
|
+
return makeAnyClient(Service, (method) => {
|
|
27
|
+
return async (...args) => {
|
|
28
|
+
let tries = 0
|
|
29
|
+
let error: unknown
|
|
30
|
+
let remainingClients = clients.get()
|
|
31
|
+
while (tries < MAX_RETRIES) {
|
|
32
|
+
const client = randomElement(remainingClients)
|
|
33
|
+
assert(client, 'no clients available')
|
|
34
|
+
try {
|
|
35
|
+
return await client[method.localName](...args)
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (
|
|
38
|
+
err instanceof ConnectError &&
|
|
39
|
+
(err.code === Code.Unavailable || err.code === Code.Aborted)
|
|
40
|
+
) {
|
|
41
|
+
tries++
|
|
42
|
+
error = err
|
|
43
|
+
remainingClients = getRemainingClients(remainingClients, client)
|
|
44
|
+
} else {
|
|
45
|
+
throw err
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
assert(error)
|
|
50
|
+
throw error
|
|
51
|
+
}
|
|
52
|
+
}) as DataPlaneClient
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { Code }
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Uses a reactive HostList in order to maintain a pool of DataPlaneClients.
|
|
59
|
+
* Each DataPlaneClient is cached per host so that it maintains connections
|
|
60
|
+
* and other internal state when the underlying HostList is updated.
|
|
61
|
+
*/
|
|
62
|
+
class DataPlaneClients {
|
|
63
|
+
private clients: DataPlaneClient[] = []
|
|
64
|
+
private clientsByHost = new Map<string, DataPlaneClient>()
|
|
65
|
+
|
|
66
|
+
constructor(
|
|
67
|
+
private hostList: HostList,
|
|
68
|
+
private clientOpts: {
|
|
69
|
+
httpVersion?: HttpVersion
|
|
70
|
+
rejectUnauthorized?: boolean
|
|
71
|
+
},
|
|
72
|
+
) {
|
|
73
|
+
this.refresh()
|
|
74
|
+
this.hostList.onUpdate(() => this.refresh())
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get(): readonly DataPlaneClient[] {
|
|
78
|
+
return this.clients
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private refresh() {
|
|
82
|
+
this.clients = []
|
|
83
|
+
for (const host of this.hostList.get()) {
|
|
84
|
+
let client = this.clientsByHost.get(host)
|
|
85
|
+
if (!client) {
|
|
86
|
+
client = this.createClient(host)
|
|
87
|
+
this.clientsByHost.set(host, client)
|
|
88
|
+
}
|
|
89
|
+
this.clients.push(client)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private createClient(host: string) {
|
|
94
|
+
return createBaseClient(host, this.clientOpts)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const createBaseClient = (
|
|
99
|
+
baseUrl: string,
|
|
100
|
+
opts: { httpVersion?: HttpVersion; rejectUnauthorized?: boolean },
|
|
101
|
+
): DataPlaneClient => {
|
|
102
|
+
const { httpVersion = '2', rejectUnauthorized = true } = opts
|
|
103
|
+
const transport = createGrpcTransport({
|
|
104
|
+
baseUrl,
|
|
105
|
+
httpVersion,
|
|
106
|
+
acceptCompression: [],
|
|
107
|
+
nodeOptions: { rejectUnauthorized },
|
|
108
|
+
})
|
|
109
|
+
return createPromiseClient(Service, transport)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const getRemainingClients = (
|
|
113
|
+
clients: readonly DataPlaneClient[],
|
|
114
|
+
lastClient: DataPlaneClient,
|
|
115
|
+
) => {
|
|
116
|
+
if (clients.length < 2) return clients // no clients to choose from
|
|
117
|
+
return clients.filter((c) => c !== lastClient)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const randomElement = <T>(arr: readonly T[]): T | undefined => {
|
|
121
|
+
if (arr.length === 0) return
|
|
122
|
+
return arr[randomInt(arr.length)]
|
|
123
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Code, ConnectError } from '@connectrpc/connect'
|
|
2
|
+
import * as ui8 from 'uint8arrays'
|
|
3
|
+
import { getDidKeyFromMultibase } from '@atproto/identity'
|
|
4
|
+
|
|
5
|
+
export const isDataplaneError = (
|
|
6
|
+
err: unknown,
|
|
7
|
+
code?: Code,
|
|
8
|
+
): err is ConnectError => {
|
|
9
|
+
if (err instanceof ConnectError) {
|
|
10
|
+
return !code || err.code === code
|
|
11
|
+
}
|
|
12
|
+
return false
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const unpackIdentityServices = (servicesBytes: Uint8Array) => {
|
|
16
|
+
const servicesStr = ui8.toString(servicesBytes, 'utf8')
|
|
17
|
+
if (!servicesStr) return {}
|
|
18
|
+
return JSON.parse(servicesStr) as UnpackedServices
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const unpackIdentityKeys = (keysBytes: Uint8Array) => {
|
|
22
|
+
const keysStr = ui8.toString(keysBytes, 'utf8')
|
|
23
|
+
if (!keysStr) return {}
|
|
24
|
+
return JSON.parse(keysStr) as UnpackedKeys
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const getServiceEndpoint = (
|
|
28
|
+
services: UnpackedServices,
|
|
29
|
+
opts: { id: string; type: string },
|
|
30
|
+
) => {
|
|
31
|
+
const endpoint =
|
|
32
|
+
services[opts.id] &&
|
|
33
|
+
services[opts.id].Type === opts.type &&
|
|
34
|
+
validateUrl(services[opts.id].URL)
|
|
35
|
+
return endpoint || undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const getKeyAsDidKey = (keys: UnpackedKeys, opts: { id: string }) => {
|
|
39
|
+
const key =
|
|
40
|
+
keys[opts.id] &&
|
|
41
|
+
getDidKeyFromMultibase({
|
|
42
|
+
type: keys[opts.id].Type,
|
|
43
|
+
publicKeyMultibase: keys[opts.id].PublicKeyMultibase,
|
|
44
|
+
})
|
|
45
|
+
return key || undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type UnpackedServices = Record<string, { Type: string; URL: string }>
|
|
49
|
+
|
|
50
|
+
type UnpackedKeys = Record<string, { Type: string; PublicKeyMultibase: string }>
|
|
51
|
+
|
|
52
|
+
const validateUrl = (urlStr: string): string | undefined => {
|
|
53
|
+
let url
|
|
54
|
+
try {
|
|
55
|
+
url = new URL(urlStr)
|
|
56
|
+
} catch {
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
60
|
+
return undefined
|
|
61
|
+
} else if (!url.hostname) {
|
|
62
|
+
return undefined
|
|
63
|
+
} else {
|
|
64
|
+
return urlStr
|
|
65
|
+
}
|
|
66
|
+
}
|