@atproto/bsky 0.0.124 → 0.0.125

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.
Files changed (113) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/api/app/bsky/notification/listNotifications.d.ts +7 -0
  3. package/dist/api/app/bsky/notification/listNotifications.d.ts.map +1 -1
  4. package/dist/api/app/bsky/notification/listNotifications.js +21 -5
  5. package/dist/api/app/bsky/notification/listNotifications.js.map +1 -1
  6. package/dist/config.d.ts +6 -0
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +24 -15
  9. package/dist/config.js.map +1 -1
  10. package/dist/context.d.ts +6 -1
  11. package/dist/context.d.ts.map +1 -1
  12. package/dist/context.js +6 -0
  13. package/dist/context.js.map +1 -1
  14. package/dist/data-plane/client/hosts.d.ts +37 -0
  15. package/dist/data-plane/client/hosts.d.ts.map +1 -0
  16. package/dist/data-plane/client/hosts.js +106 -0
  17. package/dist/data-plane/client/hosts.js.map +1 -0
  18. package/dist/data-plane/client/index.d.ts +13 -0
  19. package/dist/data-plane/client/index.d.ts.map +1 -0
  20. package/dist/data-plane/client/index.js +133 -0
  21. package/dist/data-plane/client/index.js.map +1 -0
  22. package/dist/data-plane/{client.d.ts → client/util.d.ts} +3 -10
  23. package/dist/data-plane/client/util.d.ts.map +1 -0
  24. package/dist/data-plane/client/util.js +85 -0
  25. package/dist/data-plane/client/util.js.map +1 -0
  26. package/dist/data-plane/server/db/pagination.d.ts +69 -9
  27. package/dist/data-plane/server/db/pagination.d.ts.map +1 -1
  28. package/dist/data-plane/server/db/pagination.js +114 -14
  29. package/dist/data-plane/server/db/pagination.js.map +1 -1
  30. package/dist/data-plane/server/routes/notifs.d.ts.map +1 -1
  31. package/dist/data-plane/server/routes/notifs.js +3 -5
  32. package/dist/data-plane/server/routes/notifs.js.map +1 -1
  33. package/dist/etcd.d.ts +25 -0
  34. package/dist/etcd.d.ts.map +1 -0
  35. package/dist/etcd.js +109 -0
  36. package/dist/etcd.js.map +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +14 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/lexicon/index.d.ts +6 -0
  41. package/dist/lexicon/index.d.ts.map +1 -1
  42. package/dist/lexicon/index.js +12 -0
  43. package/dist/lexicon/index.js.map +1 -1
  44. package/dist/lexicon/lexicons.d.ts +304 -156
  45. package/dist/lexicon/lexicons.d.ts.map +1 -1
  46. package/dist/lexicon/lexicons.js +168 -80
  47. package/dist/lexicon/lexicons.js.map +1 -1
  48. package/dist/lexicon/types/app/bsky/embed/video.d.ts +1 -0
  49. package/dist/lexicon/types/app/bsky/embed/video.d.ts.map +1 -1
  50. package/dist/lexicon/types/app/bsky/embed/video.js.map +1 -1
  51. package/dist/lexicon/types/com/atproto/identity/defs.d.ts +17 -0
  52. package/dist/lexicon/types/com/atproto/identity/defs.d.ts.map +1 -0
  53. package/dist/lexicon/types/com/atproto/identity/defs.js +16 -0
  54. package/dist/lexicon/types/com/atproto/identity/defs.js.map +1 -0
  55. package/dist/lexicon/types/com/atproto/identity/refreshIdentity.d.ts +39 -0
  56. package/dist/lexicon/types/com/atproto/identity/refreshIdentity.d.ts.map +1 -0
  57. package/dist/lexicon/types/com/atproto/identity/refreshIdentity.js +7 -0
  58. package/dist/lexicon/types/com/atproto/identity/refreshIdentity.js.map +1 -0
  59. package/dist/lexicon/types/com/atproto/identity/resolveDid.d.ts +40 -0
  60. package/dist/lexicon/types/com/atproto/identity/resolveDid.d.ts.map +1 -0
  61. package/dist/lexicon/types/com/atproto/identity/resolveDid.js +7 -0
  62. package/dist/lexicon/types/com/atproto/identity/resolveDid.js.map +1 -0
  63. package/dist/lexicon/types/com/atproto/identity/resolveHandle.d.ts +1 -0
  64. package/dist/lexicon/types/com/atproto/identity/resolveHandle.d.ts.map +1 -1
  65. package/dist/lexicon/types/com/atproto/identity/resolveIdentity.d.ts +36 -0
  66. package/dist/lexicon/types/com/atproto/identity/resolveIdentity.d.ts.map +1 -0
  67. package/dist/lexicon/types/com/atproto/identity/resolveIdentity.js +7 -0
  68. package/dist/lexicon/types/com/atproto/identity/resolveIdentity.js.map +1 -0
  69. package/dist/lexicon/types/com/atproto/repo/listRecords.d.ts +0 -4
  70. package/dist/lexicon/types/com/atproto/repo/listRecords.d.ts.map +1 -1
  71. package/dist/lexicon/types/com/atproto/repo/listRecords.js.map +1 -1
  72. package/dist/lexicon/types/com/atproto/sync/getRecord.d.ts +0 -2
  73. package/dist/lexicon/types/com/atproto/sync/getRecord.d.ts.map +1 -1
  74. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.d.ts +1 -30
  75. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.d.ts.map +1 -1
  76. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.js +0 -27
  77. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.js.map +1 -1
  78. package/dist/logger.d.ts +1 -0
  79. package/dist/logger.d.ts.map +1 -1
  80. package/dist/logger.js +2 -1
  81. package/dist/logger.js.map +1 -1
  82. package/package.json +16 -15
  83. package/src/api/app/bsky/notification/listNotifications.ts +28 -6
  84. package/src/config.ts +45 -15
  85. package/src/context.ts +12 -1
  86. package/src/data-plane/client/hosts.ts +103 -0
  87. package/src/data-plane/client/index.ts +123 -0
  88. package/src/data-plane/client/util.ts +66 -0
  89. package/src/data-plane/server/db/pagination.ts +158 -35
  90. package/src/data-plane/server/routes/notifs.ts +4 -9
  91. package/src/etcd.ts +90 -0
  92. package/src/index.ts +26 -2
  93. package/src/lexicon/index.ts +36 -0
  94. package/src/lexicon/lexicons.ts +183 -83
  95. package/src/lexicon/types/app/bsky/embed/video.ts +1 -0
  96. package/src/lexicon/types/com/atproto/identity/defs.ts +30 -0
  97. package/src/lexicon/types/com/atproto/identity/refreshIdentity.ts +52 -0
  98. package/src/lexicon/types/com/atproto/identity/resolveDid.ts +52 -0
  99. package/src/lexicon/types/com/atproto/identity/resolveHandle.ts +1 -0
  100. package/src/lexicon/types/com/atproto/identity/resolveIdentity.ts +48 -0
  101. package/src/lexicon/types/com/atproto/repo/listRecords.ts +0 -4
  102. package/src/lexicon/types/com/atproto/sync/getRecord.ts +0 -2
  103. package/src/lexicon/types/com/atproto/sync/subscribeRepos.ts +0 -59
  104. package/src/logger.ts +2 -0
  105. package/tests/etcd.test.ts +301 -0
  106. package/tests/views/__snapshots__/notifications.test.ts.snap +3 -3
  107. package/tests/views/notifications.test.ts +190 -10
  108. package/tsconfig.build.tsbuildinfo +1 -1
  109. package/tsconfig.tests.tsbuildinfo +1 -1
  110. package/dist/data-plane/client.d.ts.map +0 -1
  111. package/dist/data-plane/client.js +0 -156
  112. package/dist/data-plane/client.js.map +0 -1
  113. package/src/data-plane/client.ts +0 -154
@@ -2,8 +2,8 @@ import { sql } from 'kysely'
2
2
  import { InvalidRequestError } from '@atproto/xrpc-server'
3
3
  import { AnyQb, DbRef } from './util'
4
4
 
5
- export type Cursor = { primary: string; secondary: string }
6
- export type LabeledResult = {
5
+ type KeysetCursor = { primary: string; secondary: string }
6
+ type KeysetLabeledResult = {
7
7
  primary: string | number
8
8
  secondary: string | number
9
9
  }
@@ -22,14 +22,14 @@ export type LabeledResult = {
22
22
  * Result -*-> LabeledResult <-*-> Cursor <--> packed/string cursor
23
23
  * ↳ SQL Condition
24
24
  */
25
- export abstract class GenericKeyset<R, LR extends LabeledResult> {
25
+ export abstract class GenericKeyset<R, LR extends KeysetLabeledResult> {
26
26
  constructor(
27
27
  public primary: DbRef,
28
28
  public secondary: DbRef,
29
29
  ) {}
30
30
  abstract labelResult(result: R): LR
31
- abstract labeledResultToCursor(labeled: LR): Cursor
32
- abstract cursorToLabeledResult(cursor: Cursor): LR
31
+ abstract labeledResultToCursor(labeled: LR): KeysetCursor
32
+ abstract cursorToLabeledResult(cursor: KeysetCursor): LR
33
33
  packFromResult(results: R | R[]): string | undefined {
34
34
  const result = Array.isArray(results) ? results.at(-1) : results
35
35
  if (!result) return
@@ -45,11 +45,11 @@ export abstract class GenericKeyset<R, LR extends LabeledResult> {
45
45
  if (!cursor) return
46
46
  return this.cursorToLabeledResult(cursor)
47
47
  }
48
- packCursor(cursor?: Cursor): string | undefined {
48
+ packCursor(cursor?: KeysetCursor): string | undefined {
49
49
  if (!cursor) return
50
50
  return `${cursor.primary}__${cursor.secondary}`
51
51
  }
52
- unpackCursor(cursorStr?: string): Cursor | undefined {
52
+ unpackCursor(cursorStr?: string): KeysetCursor | undefined {
53
53
  if (!cursorStr) return
54
54
  const result = cursorStr.split('__')
55
55
  const [primary, secondary, ...others] = result
@@ -79,10 +79,43 @@ export abstract class GenericKeyset<R, LR extends LabeledResult> {
79
79
  }
80
80
  }
81
81
  }
82
+ paginate<QB extends AnyQb>(
83
+ qb: QB,
84
+ opts: {
85
+ limit?: number
86
+ cursor?: string
87
+ direction?: 'asc' | 'desc'
88
+ tryIndex?: boolean
89
+ // By default, pg does nullsFirst
90
+ nullsLast?: boolean
91
+ },
92
+ ): QB {
93
+ const { limit, cursor, direction = 'desc', tryIndex, nullsLast } = opts
94
+ const keysetSql = this.getSql(this.unpack(cursor), direction, tryIndex)
95
+ return qb
96
+ .if(!!limit, (q) => q.limit(limit as number))
97
+ .if(!nullsLast, (q) =>
98
+ q.orderBy(this.primary, direction).orderBy(this.secondary, direction),
99
+ )
100
+ .if(!!nullsLast, (q) =>
101
+ q
102
+ .orderBy(
103
+ direction === 'asc'
104
+ ? sql`${this.primary} asc nulls last`
105
+ : sql`${this.primary} desc nulls last`,
106
+ )
107
+ .orderBy(
108
+ direction === 'asc'
109
+ ? sql`${this.secondary} asc nulls last`
110
+ : sql`${this.secondary} desc nulls last`,
111
+ ),
112
+ )
113
+ .if(!!keysetSql, (qb) => (keysetSql ? qb.where(keysetSql) : qb)) as QB
114
+ }
82
115
  }
83
116
 
84
117
  type SortAtCidResult = { sortAt: string; cid: string }
85
- type TimeCidLabeledResult = Cursor
118
+ type TimeCidLabeledResult = KeysetCursor
86
119
 
87
120
  export class TimeCidKeyset<
88
121
  TimeCidResult = SortAtCidResult,
@@ -97,7 +130,7 @@ export class TimeCidKeyset<
97
130
  secondary: labeled.secondary,
98
131
  }
99
132
  }
100
- cursorToLabeledResult(cursor: Cursor) {
133
+ cursorToLabeledResult(cursor: KeysetCursor) {
101
134
  const primaryDate = new Date(parseInt(cursor.primary, 10))
102
135
  if (isNaN(primaryDate.getTime())) {
103
136
  throw new InvalidRequestError('Malformed cursor')
@@ -127,6 +160,9 @@ export class IndexedAtDidKeyset extends TimeCidKeyset<{
127
160
  }
128
161
  }
129
162
 
163
+ /**
164
+ * This is being deprecated. Use {@link GenericKeyset#paginate} instead.
165
+ */
130
166
  export const paginate = <
131
167
  QB extends AnyQb,
132
168
  K extends GenericKeyset<unknown, any>,
@@ -142,32 +178,119 @@ export const paginate = <
142
178
  nullsLast?: boolean
143
179
  },
144
180
  ): QB => {
145
- const {
146
- limit,
147
- cursor,
148
- keyset,
149
- direction = 'desc',
150
- tryIndex,
151
- nullsLast,
152
- } = opts
153
- const keysetSql = keyset.getSql(keyset.unpack(cursor), direction, tryIndex)
154
- return qb
155
- .if(!!limit, (q) => q.limit(limit as number))
156
- .if(!nullsLast, (q) =>
157
- q.orderBy(keyset.primary, direction).orderBy(keyset.secondary, direction),
158
- )
159
- .if(!!nullsLast, (q) =>
160
- q
161
- .orderBy(
162
- direction === 'asc'
163
- ? sql`${keyset.primary} asc nulls last`
164
- : sql`${keyset.primary} desc nulls last`,
165
- )
166
- .orderBy(
181
+ return opts.keyset.paginate(qb, opts)
182
+ }
183
+
184
+ type SingleKeyCursor = {
185
+ primary: string
186
+ }
187
+
188
+ type SingleKeyLabeledResult = {
189
+ primary: string | number
190
+ }
191
+
192
+ /**
193
+ * GenericSingleKey is similar to {@link GenericKeyset} but for a single key cursor.
194
+ */
195
+ export abstract class GenericSingleKey<R, LR extends SingleKeyLabeledResult> {
196
+ constructor(public primary: DbRef) {}
197
+ abstract labelResult(result: R): LR
198
+ abstract labeledResultToCursor(labeled: LR): SingleKeyCursor
199
+ abstract cursorToLabeledResult(cursor: SingleKeyCursor): LR
200
+ packFromResult(results: R | R[]): string | undefined {
201
+ const result = Array.isArray(results) ? results.at(-1) : results
202
+ if (!result) return
203
+ return this.pack(this.labelResult(result))
204
+ }
205
+ pack(labeled?: LR): string | undefined {
206
+ if (!labeled) return
207
+ const cursor = this.labeledResultToCursor(labeled)
208
+ return this.packCursor(cursor)
209
+ }
210
+ unpack(cursorStr?: string): LR | undefined {
211
+ const cursor = this.unpackCursor(cursorStr)
212
+ if (!cursor) return
213
+ return this.cursorToLabeledResult(cursor)
214
+ }
215
+ packCursor(cursor?: SingleKeyCursor): string | undefined {
216
+ if (!cursor) return
217
+ return cursor.primary
218
+ }
219
+ unpackCursor(cursorStr?: string): SingleKeyCursor | undefined {
220
+ if (!cursorStr) return
221
+ const result = cursorStr.split('__')
222
+ const [primary, ...others] = result
223
+ if (!primary || others.length > 0) {
224
+ throw new InvalidRequestError('Malformed cursor')
225
+ }
226
+ return {
227
+ primary,
228
+ }
229
+ }
230
+ getSql(labeled?: LR, direction?: 'asc' | 'desc') {
231
+ if (labeled === undefined) return
232
+ if (direction === 'asc') {
233
+ return sql`${this.primary} > ${labeled.primary}`
234
+ }
235
+ return sql`${this.primary} < ${labeled.primary}`
236
+ }
237
+ paginate<QB extends AnyQb>(
238
+ qb: QB,
239
+ opts: {
240
+ limit?: number
241
+ cursor?: string
242
+ direction?: 'asc' | 'desc'
243
+ // By default, pg does nullsFirst
244
+ nullsLast?: boolean
245
+ },
246
+ ): QB {
247
+ const { limit, cursor, direction = 'desc', nullsLast } = opts
248
+ const keySql = this.getSql(this.unpack(cursor), direction)
249
+ return qb
250
+ .if(!!limit, (q) => q.limit(limit as number))
251
+ .if(!nullsLast, (q) => q.orderBy(this.primary, direction))
252
+ .if(!!nullsLast, (q) =>
253
+ q.orderBy(
167
254
  direction === 'asc'
168
- ? sql`${keyset.secondary} asc nulls last`
169
- : sql`${keyset.secondary} desc nulls last`,
255
+ ? sql`${this.primary} asc nulls last`
256
+ : sql`${this.primary} desc nulls last`,
170
257
  ),
171
- )
172
- .if(!!keysetSql, (qb) => (keysetSql ? qb.where(keysetSql) : qb)) as QB
258
+ )
259
+ .if(!!keySql, (qb) => (keySql ? qb.where(keySql) : qb)) as QB
260
+ }
261
+ }
262
+
263
+ type SortAtResult = { sortAt: string }
264
+ type TimeLabeledResult = SingleKeyCursor
265
+
266
+ export class IsoTimeKey<TimeResult = SortAtResult> extends GenericSingleKey<
267
+ TimeResult,
268
+ TimeLabeledResult
269
+ > {
270
+ labelResult(result: TimeResult): TimeLabeledResult
271
+ labelResult<TimeResult extends SortAtResult>(result: TimeResult) {
272
+ return { primary: result.sortAt }
273
+ }
274
+ labeledResultToCursor(labeled: TimeLabeledResult) {
275
+ return {
276
+ primary: new Date(labeled.primary).toISOString(),
277
+ }
278
+ }
279
+ cursorToLabeledResult(cursor: SingleKeyCursor) {
280
+ const primaryDate = new Date(cursor.primary)
281
+ if (isNaN(primaryDate.getTime())) {
282
+ throw new InvalidRequestError('Malformed cursor')
283
+ }
284
+ return {
285
+ primary: primaryDate.toISOString(),
286
+ }
287
+ }
288
+ }
289
+
290
+ export class IsoSortAtKey extends IsoTimeKey<{
291
+ sortAt: string
292
+ }> {
293
+ labelResult(result: { sortAt: string }) {
294
+ return { primary: result.sortAt }
295
+ }
173
296
  }
@@ -3,7 +3,7 @@ import { ServiceImpl } from '@connectrpc/connect'
3
3
  import { sql } from 'kysely'
4
4
  import { Service } from '../../../proto/bsky_connect'
5
5
  import { Database } from '../db'
6
- import { TimeCidKeyset, paginate } from '../db/pagination'
6
+ import { IsoSortAtKey } from '../db/pagination'
7
7
  import { countAll, notSoftDeletedClause } from '../db/util'
8
8
 
9
9
  export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
@@ -41,15 +41,10 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
41
41
  ])
42
42
  .select(priorityFollowQb.as('priority'))
43
43
 
44
- const keyset = new TimeCidKeyset(
45
- ref('notif.sortAt'),
46
- ref('notif.recordCid'),
47
- )
48
- builder = paginate(builder, {
44
+ const key = new IsoSortAtKey(ref('notif.sortAt'))
45
+ builder = key.paginate(builder, {
49
46
  cursor,
50
47
  limit,
51
- keyset,
52
- tryIndex: true,
53
48
  })
54
49
 
55
50
  const notifsRes = await builder.execute()
@@ -63,7 +58,7 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
63
58
  }))
64
59
  return {
65
60
  notifications,
66
- cursor: keyset.packFromResult(notifsRes),
61
+ cursor: key.packFromResult(notifsRes),
67
62
  }
68
63
  },
69
64
 
package/src/etcd.ts ADDED
@@ -0,0 +1,90 @@
1
+ import { once } from 'node:events'
2
+ import { Etcd3, Watcher } from 'etcd3'
3
+
4
+ /**
5
+ * A reactive map based on the keys and values stored within etcd under a given prefix.
6
+ */
7
+ export class EtcdMap {
8
+ inner = new Map<string, VersionedValue>()
9
+ watcher: Watcher
10
+ connecting: Promise<void> | undefined
11
+ handlers: ((self: EtcdMap) => void)[] = []
12
+
13
+ constructor(
14
+ private etcd: Etcd3,
15
+ private prefix = '',
16
+ ) {
17
+ this.watcher = etcd.watch().prefix(prefix).watcher()
18
+ this.connecting = connectWatcher(this.watcher)
19
+ }
20
+
21
+ async connect() {
22
+ this.watcher.on('put', (kv) => {
23
+ const key = kv.key.toString()
24
+ const value = kv.value.toString()
25
+ const rev = revToInt(kv.mod_revision)
26
+ this.apply(key, { value, rev })
27
+ })
28
+ this.watcher.on('delete', (kv) => {
29
+ const key = kv.key.toString()
30
+ const value = null
31
+ const rev = revToInt(kv.mod_revision)
32
+ this.apply(key, { value, rev })
33
+ })
34
+ await this.connecting?.finally(() => {
35
+ this.connecting = undefined
36
+ })
37
+ const { kvs } = await this.etcd.getAll().prefix(this.prefix).exec()
38
+ for (const kv of kvs) {
39
+ const key = kv.key.toString()
40
+ const value = kv.value.toString()
41
+ const rev = revToInt(kv.mod_revision)
42
+ this.apply(key, { value, rev })
43
+ }
44
+ }
45
+
46
+ get(key: string) {
47
+ return this.inner.get(key)?.value ?? null
48
+ }
49
+
50
+ *values() {
51
+ for (const { value } of this.inner.values()) {
52
+ if (value !== null) {
53
+ yield value
54
+ }
55
+ }
56
+ }
57
+
58
+ onUpdate(handler: (self: EtcdMap) => void) {
59
+ this.handlers.push(handler)
60
+ }
61
+
62
+ private update() {
63
+ for (const handler of this.handlers) {
64
+ handler(this)
65
+ }
66
+ }
67
+
68
+ private apply(key, vv: VersionedValue) {
69
+ const curr = this.inner.get(key)
70
+ if (curr && curr.rev > vv.rev) return
71
+ this.inner.set(key, vv)
72
+ this.update()
73
+ }
74
+ }
75
+
76
+ function revToInt(rev: string) {
77
+ return parseInt(rev, 10)
78
+ }
79
+
80
+ async function connectWatcher(watcher: Watcher) {
81
+ await Promise.race([
82
+ once(watcher, 'connected'),
83
+ once(watcher, 'error').then((err) => Promise.reject(err)),
84
+ ])
85
+ }
86
+
87
+ type VersionedValue = {
88
+ rev: number
89
+ value: string | null
90
+ }
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ import http from 'node:http'
3
3
  import { AddressInfo } from 'node:net'
4
4
  import compression from 'compression'
5
5
  import cors from 'cors'
6
+ import { Etcd3 } from 'etcd3'
6
7
  import express from 'express'
7
8
  import { HttpTerminator, createHttpTerminator } from 'http-terminator'
8
9
  import { AtpAgent } from '@atproto/api'
@@ -16,7 +17,11 @@ import { authWithApiKey as bsyncAuth, createBsyncClient } from './bsync'
16
17
  import { ServerConfig } from './config'
17
18
  import { AppContext } from './context'
18
19
  import { authWithApiKey as courierAuth, createCourierClient } from './courier'
19
- import { createDataPlaneClient } from './data-plane/client'
20
+ import {
21
+ BasicHostList,
22
+ EtcdHostList,
23
+ createDataPlaneClient,
24
+ } from './data-plane/client'
20
25
  import * as error from './error'
21
26
  import { FeatureGates } from './feature-gates'
22
27
  import { Hydrator } from './hydration/hydrator'
@@ -98,7 +103,20 @@ export class BskyAppView {
98
103
  )
99
104
  }
100
105
 
101
- const dataplane = createDataPlaneClient(config.dataplaneUrls, {
106
+ const etcd = config.etcdHosts.length
107
+ ? new Etcd3({ hosts: config.etcdHosts })
108
+ : undefined
109
+
110
+ const dataplaneHostList =
111
+ etcd && config.dataplaneUrlsEtcdKeyPrefix
112
+ ? new EtcdHostList(
113
+ etcd,
114
+ config.dataplaneUrlsEtcdKeyPrefix,
115
+ config.dataplaneUrls,
116
+ )
117
+ : new BasicHostList(config.dataplaneUrls)
118
+
119
+ const dataplane = createDataPlaneClient(dataplaneHostList, {
102
120
  httpVersion: config.dataplaneHttpVersion,
103
121
  rejectUnauthorized: !config.dataplaneIgnoreBadTls,
104
122
  })
@@ -147,7 +165,9 @@ export class BskyAppView {
147
165
 
148
166
  const ctx = new AppContext({
149
167
  cfg: config,
168
+ etcd,
150
169
  dataplane,
170
+ dataplaneHostList,
151
171
  searchAgent,
152
172
  suggestionsAgent,
153
173
  topicsAgent,
@@ -184,6 +204,9 @@ export class BskyAppView {
184
204
  }
185
205
 
186
206
  async start(): Promise<http.Server> {
207
+ if (this.ctx.dataplaneHostList instanceof EtcdHostList) {
208
+ await this.ctx.dataplaneHostList.connect()
209
+ }
187
210
  await this.ctx.featureGates.start()
188
211
  const server = this.app.listen(this.ctx.cfg.port)
189
212
  this.server = server
@@ -198,6 +221,7 @@ export class BskyAppView {
198
221
  async destroy(): Promise<void> {
199
222
  await this.terminator?.terminate()
200
223
  this.ctx.featureGates.destroy()
224
+ await this.ctx.etcd?.close()
201
225
  }
202
226
  }
203
227
 
@@ -24,8 +24,11 @@ import * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/u
24
24
  import * as ComAtprotoAdminUpdateAccountPassword from './types/com/atproto/admin/updateAccountPassword.js'
25
25
  import * as ComAtprotoAdminUpdateSubjectStatus from './types/com/atproto/admin/updateSubjectStatus.js'
26
26
  import * as ComAtprotoIdentityGetRecommendedDidCredentials from './types/com/atproto/identity/getRecommendedDidCredentials.js'
27
+ import * as ComAtprotoIdentityRefreshIdentity from './types/com/atproto/identity/refreshIdentity.js'
27
28
  import * as ComAtprotoIdentityRequestPlcOperationSignature from './types/com/atproto/identity/requestPlcOperationSignature.js'
29
+ import * as ComAtprotoIdentityResolveDid from './types/com/atproto/identity/resolveDid.js'
28
30
  import * as ComAtprotoIdentityResolveHandle from './types/com/atproto/identity/resolveHandle.js'
31
+ import * as ComAtprotoIdentityResolveIdentity from './types/com/atproto/identity/resolveIdentity.js'
29
32
  import * as ComAtprotoIdentitySignPlcOperation from './types/com/atproto/identity/signPlcOperation.js'
30
33
  import * as ComAtprotoIdentitySubmitPlcOperation from './types/com/atproto/identity/submitPlcOperation.js'
31
34
  import * as ComAtprotoIdentityUpdateHandle from './types/com/atproto/identity/updateHandle.js'
@@ -436,6 +439,17 @@ export class ComAtprotoIdentityNS {
436
439
  return this._server.xrpc.method(nsid, cfg)
437
440
  }
438
441
 
442
+ refreshIdentity<AV extends AuthVerifier>(
443
+ cfg: ConfigOf<
444
+ AV,
445
+ ComAtprotoIdentityRefreshIdentity.Handler<ExtractAuth<AV>>,
446
+ ComAtprotoIdentityRefreshIdentity.HandlerReqCtx<ExtractAuth<AV>>
447
+ >,
448
+ ) {
449
+ const nsid = 'com.atproto.identity.refreshIdentity' // @ts-ignore
450
+ return this._server.xrpc.method(nsid, cfg)
451
+ }
452
+
439
453
  requestPlcOperationSignature<AV extends AuthVerifier>(
440
454
  cfg: ConfigOf<
441
455
  AV,
@@ -449,6 +463,17 @@ export class ComAtprotoIdentityNS {
449
463
  return this._server.xrpc.method(nsid, cfg)
450
464
  }
451
465
 
466
+ resolveDid<AV extends AuthVerifier>(
467
+ cfg: ConfigOf<
468
+ AV,
469
+ ComAtprotoIdentityResolveDid.Handler<ExtractAuth<AV>>,
470
+ ComAtprotoIdentityResolveDid.HandlerReqCtx<ExtractAuth<AV>>
471
+ >,
472
+ ) {
473
+ const nsid = 'com.atproto.identity.resolveDid' // @ts-ignore
474
+ return this._server.xrpc.method(nsid, cfg)
475
+ }
476
+
452
477
  resolveHandle<AV extends AuthVerifier>(
453
478
  cfg: ConfigOf<
454
479
  AV,
@@ -460,6 +485,17 @@ export class ComAtprotoIdentityNS {
460
485
  return this._server.xrpc.method(nsid, cfg)
461
486
  }
462
487
 
488
+ resolveIdentity<AV extends AuthVerifier>(
489
+ cfg: ConfigOf<
490
+ AV,
491
+ ComAtprotoIdentityResolveIdentity.Handler<ExtractAuth<AV>>,
492
+ ComAtprotoIdentityResolveIdentity.HandlerReqCtx<ExtractAuth<AV>>
493
+ >,
494
+ ) {
495
+ const nsid = 'com.atproto.identity.resolveIdentity' // @ts-ignore
496
+ return this._server.xrpc.method(nsid, cfg)
497
+ }
498
+
463
499
  signPlcOperation<AV extends AuthVerifier>(
464
500
  cfg: ConfigOf<
465
501
  AV,