@bsv/sdk 1.7.1 → 1.7.3
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/cjs/package.json +1 -1
- package/dist/cjs/src/overlay-tools/LookupResolver.js +156 -76
- package/dist/cjs/src/overlay-tools/LookupResolver.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/overlay-tools/LookupResolver.js +163 -76
- package/dist/esm/src/overlay-tools/LookupResolver.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/overlay-tools/LookupResolver.d.ts +30 -2
- package/dist/types/src/overlay-tools/LookupResolver.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +3 -3
- package/dist/umd/bundle.js.map +1 -1
- package/docs/reference/overlay-tools.md +11 -2
- package/package.json +1 -1
- package/src/overlay-tools/LookupResolver.ts +172 -82
- package/src/overlay-tools/__tests/LookupResolver.test.ts +0 -47
|
@@ -100,6 +100,7 @@ export interface LookupResolverConfig {
|
|
|
100
100
|
slapTrackers?: string[];
|
|
101
101
|
hostOverrides?: Record<string, string[]>;
|
|
102
102
|
additionalHosts?: Record<string, string[]>;
|
|
103
|
+
cache?: CacheOptions;
|
|
103
104
|
}
|
|
104
105
|
```
|
|
105
106
|
|
|
@@ -113,6 +114,14 @@ Map of lookup service names to arrays of hosts to use in addition to resolving v
|
|
|
113
114
|
additionalHosts?: Record<string, string[]>
|
|
114
115
|
```
|
|
115
116
|
|
|
117
|
+
#### Property cache
|
|
118
|
+
|
|
119
|
+
Optional cache tuning.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
cache?: CacheOptions
|
|
123
|
+
```
|
|
124
|
+
|
|
116
125
|
#### Property facilitator
|
|
117
126
|
|
|
118
127
|
The facilitator used to make requests to Overlay Services hosts.
|
|
@@ -315,7 +324,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
|
|
|
315
324
|
export class HTTPSOverlayLookupFacilitator implements OverlayLookupFacilitator {
|
|
316
325
|
fetchClient: typeof fetch;
|
|
317
326
|
allowHTTP: boolean;
|
|
318
|
-
constructor(httpClient =
|
|
327
|
+
constructor(httpClient = defaultFetch, allowHTTP: boolean = false)
|
|
319
328
|
async lookup(url: string, question: LookupQuestion, timeout: number = 5000): Promise<LookupAnswer>
|
|
320
329
|
}
|
|
321
330
|
```
|
|
@@ -327,7 +336,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
|
|
|
327
336
|
---
|
|
328
337
|
### Class: LookupResolver
|
|
329
338
|
|
|
330
|
-
Represents
|
|
339
|
+
Represents a Lookup Resolver.
|
|
331
340
|
|
|
332
341
|
```ts
|
|
333
342
|
export default class LookupResolver {
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Transaction } from '../transaction/index.js'
|
|
2
2
|
import OverlayAdminTokenTemplate from './OverlayAdminTokenTemplate.js'
|
|
3
3
|
|
|
4
|
+
// Only bind window.fetch in the browser
|
|
5
|
+
const defaultFetch = typeof window !== 'undefined' ? fetch.bind(window) : fetch
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* The question asked to the Overlay Services Engine when a consumer of state wishes to look up information.
|
|
6
9
|
*/
|
|
@@ -57,6 +60,16 @@ export const DEFAULT_TESTNET_SLAP_TRACKERS: string[] = [
|
|
|
57
60
|
|
|
58
61
|
const MAX_TRACKER_WAIT_TIME = 5000
|
|
59
62
|
|
|
63
|
+
/** Internal cache options. Kept optional to preserve drop-in compatibility. */
|
|
64
|
+
interface CacheOptions {
|
|
65
|
+
/** How long (ms) a hosts entry is considered fresh. Default 5 minutes. */
|
|
66
|
+
hostsTtlMs?: number
|
|
67
|
+
/** How many distinct services’ hosts to cache before evicting. Default 128. */
|
|
68
|
+
hostsMaxEntries?: number
|
|
69
|
+
/** How long (ms) to keep txId memoization. Default 10 minutes. */
|
|
70
|
+
txMemoTtlMs?: number
|
|
71
|
+
}
|
|
72
|
+
|
|
60
73
|
/** Configuration options for the Lookup resolver. */
|
|
61
74
|
export interface LookupResolverConfig {
|
|
62
75
|
/**
|
|
@@ -74,6 +87,8 @@ export interface LookupResolverConfig {
|
|
|
74
87
|
hostOverrides?: Record<string, string[]>
|
|
75
88
|
/** Map of lookup service names to arrays of hosts to use in addition to resolving via SLAP. */
|
|
76
89
|
additionalHosts?: Record<string, string[]>
|
|
90
|
+
/** Optional cache tuning. */
|
|
91
|
+
cache?: CacheOptions
|
|
77
92
|
}
|
|
78
93
|
|
|
79
94
|
/** Facilitates lookups to URLs that return answers. */
|
|
@@ -96,7 +111,7 @@ export class HTTPSOverlayLookupFacilitator implements OverlayLookupFacilitator {
|
|
|
96
111
|
fetchClient: typeof fetch
|
|
97
112
|
allowHTTP: boolean
|
|
98
113
|
|
|
99
|
-
constructor (httpClient =
|
|
114
|
+
constructor (httpClient = defaultFetch, allowHTTP: boolean = false) {
|
|
100
115
|
this.fetchClient = httpClient
|
|
101
116
|
this.allowHTTP = allowHTTP
|
|
102
117
|
}
|
|
@@ -111,36 +126,35 @@ export class HTTPSOverlayLookupFacilitator implements OverlayLookupFacilitator {
|
|
|
111
126
|
'HTTPS facilitator can only use URLs that start with "https:"'
|
|
112
127
|
)
|
|
113
128
|
}
|
|
114
|
-
const timeoutPromise = new Promise((resolve, reject) =>
|
|
115
|
-
setTimeout(() => reject(new Error('Request timed out')), timeout)
|
|
116
|
-
)
|
|
117
129
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
},
|
|
123
|
-
body: JSON.stringify({
|
|
124
|
-
service: question.service,
|
|
125
|
-
query: question.query
|
|
126
|
-
})
|
|
127
|
-
})
|
|
130
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : undefined
|
|
131
|
+
const timer = setTimeout(() => {
|
|
132
|
+
try { controller?.abort() } catch { /* noop */ }
|
|
133
|
+
}, timeout)
|
|
128
134
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
try {
|
|
136
|
+
const fco: RequestInit = {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: { 'Content-Type': 'application/json' },
|
|
139
|
+
body: JSON.stringify({ service: question.service, query: question.query }),
|
|
140
|
+
signal: controller?.signal
|
|
141
|
+
}
|
|
142
|
+
const response: Response = await this.fetchClient(`${url}/lookup`, fco)
|
|
133
143
|
|
|
134
|
-
|
|
144
|
+
if (!response.ok) throw new Error(`Failed to facilitate lookup (HTTP ${response.status})`)
|
|
135
145
|
return await response.json()
|
|
136
|
-
}
|
|
137
|
-
|
|
146
|
+
} catch (e) {
|
|
147
|
+
// Normalize timeouts to a consistent error message
|
|
148
|
+
if ((e as any)?.name === 'AbortError') throw new Error('Request timed out')
|
|
149
|
+
throw e
|
|
150
|
+
} finally {
|
|
151
|
+
clearTimeout(timer)
|
|
138
152
|
}
|
|
139
153
|
}
|
|
140
154
|
}
|
|
141
155
|
|
|
142
156
|
/**
|
|
143
|
-
* Represents
|
|
157
|
+
* Represents a Lookup Resolver.
|
|
144
158
|
*/
|
|
145
159
|
export default class LookupResolver {
|
|
146
160
|
private readonly facilitator: OverlayLookupFacilitator
|
|
@@ -149,12 +163,30 @@ export default class LookupResolver {
|
|
|
149
163
|
private readonly additionalHosts: Record<string, string[]>
|
|
150
164
|
private readonly networkPreset: 'mainnet' | 'testnet' | 'local'
|
|
151
165
|
|
|
166
|
+
// ---- Caches / memoization ----
|
|
167
|
+
private readonly hostsCache: Map<string, { hosts: string[], expiresAt: number }>
|
|
168
|
+
private readonly hostsInFlight: Map<string, Promise<string[]>>
|
|
169
|
+
private readonly hostsTtlMs: number
|
|
170
|
+
private readonly hostsMaxEntries: number
|
|
171
|
+
|
|
172
|
+
private readonly txMemo: Map<string, { txId: string, expiresAt: number }>
|
|
173
|
+
private readonly txMemoTtlMs: number
|
|
174
|
+
|
|
152
175
|
constructor (config: LookupResolverConfig = {}) {
|
|
153
176
|
this.networkPreset = config.networkPreset ?? 'mainnet'
|
|
154
177
|
this.facilitator = config.facilitator ?? new HTTPSOverlayLookupFacilitator(undefined, this.networkPreset === 'local')
|
|
155
178
|
this.slapTrackers = config.slapTrackers ?? (this.networkPreset === 'mainnet' ? DEFAULT_SLAP_TRACKERS : DEFAULT_TESTNET_SLAP_TRACKERS)
|
|
156
179
|
this.hostOverrides = config.hostOverrides ?? {}
|
|
157
180
|
this.additionalHosts = config.additionalHosts ?? {}
|
|
181
|
+
|
|
182
|
+
// cache tuning
|
|
183
|
+
this.hostsTtlMs = config.cache?.hostsTtlMs ?? 5 * 60 * 1000 // 5 min
|
|
184
|
+
this.hostsMaxEntries = config.cache?.hostsMaxEntries ?? 128
|
|
185
|
+
this.txMemoTtlMs = config.cache?.txMemoTtlMs ?? 10 * 60 * 1000 // 10 min
|
|
186
|
+
|
|
187
|
+
this.hostsCache = new Map()
|
|
188
|
+
this.hostsInFlight = new Map()
|
|
189
|
+
this.txMemo = new Map()
|
|
158
190
|
}
|
|
159
191
|
|
|
160
192
|
/**
|
|
@@ -172,13 +204,13 @@ export default class LookupResolver {
|
|
|
172
204
|
} else if (this.networkPreset === 'local') {
|
|
173
205
|
competentHosts = ['http://localhost:8080']
|
|
174
206
|
} else {
|
|
175
|
-
competentHosts = await this.
|
|
207
|
+
competentHosts = await this.getCompetentHostsCached(question.service)
|
|
176
208
|
}
|
|
177
209
|
if (this.additionalHosts[question.service]?.length > 0) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
210
|
+
// preserve order: resolved hosts first, then additional (unique)
|
|
211
|
+
const extra = this.additionalHosts[question.service]
|
|
212
|
+
const seen = new Set(competentHosts)
|
|
213
|
+
for (const h of extra) if (!seen.has(h)) competentHosts.push(h)
|
|
182
214
|
}
|
|
183
215
|
if (competentHosts.length < 1) {
|
|
184
216
|
throw new Error(
|
|
@@ -186,49 +218,114 @@ export default class LookupResolver {
|
|
|
186
218
|
)
|
|
187
219
|
}
|
|
188
220
|
|
|
189
|
-
//
|
|
221
|
+
// Fire all hosts with per-host timeout, harvest successful output-list responses
|
|
190
222
|
const hostResponses = await Promise.allSettled(
|
|
191
|
-
competentHosts.map(
|
|
192
|
-
|
|
193
|
-
)
|
|
223
|
+
competentHosts.map(async (host) => {
|
|
224
|
+
return await this.facilitator.lookup(host, question, timeout)
|
|
225
|
+
})
|
|
194
226
|
)
|
|
195
227
|
|
|
196
|
-
const
|
|
197
|
-
.filter((result): result is PromiseFulfilledResult<LookupAnswer> => result.status === 'fulfilled')
|
|
198
|
-
.map((result) => result.value)
|
|
228
|
+
const outputsMap = new Map<string, { beef: number[], context?: number[], outputIndex: number }>()
|
|
199
229
|
|
|
200
|
-
|
|
201
|
-
|
|
230
|
+
// Memo key helper for tx parsing
|
|
231
|
+
const beefKey = (beef: number[]): string => {
|
|
232
|
+
if (typeof beef !== 'object') return '' // The invalid BEEF has an empty key.
|
|
233
|
+
// A fast and deterministic key for memoization; avoids large JSON strings
|
|
234
|
+
// since beef is an array of integers, join is safe and compact.
|
|
235
|
+
return beef.join(',')
|
|
202
236
|
}
|
|
203
237
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
238
|
+
for (const result of hostResponses) {
|
|
239
|
+
if (result.status !== 'fulfilled') continue
|
|
240
|
+
const response = result.value
|
|
241
|
+
if (response?.type !== 'output-list' || !Array.isArray(response.outputs)) continue
|
|
207
242
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
for (const output of response.outputs) {
|
|
243
|
+
for (const output of response.outputs) {
|
|
244
|
+
const keyForBeef = beefKey(output.beef)
|
|
245
|
+
let memo = this.txMemo.get(keyForBeef)
|
|
246
|
+
const now = Date.now()
|
|
247
|
+
if (typeof memo !== 'object' || memo === null || memo.expiresAt <= now) {
|
|
214
248
|
try {
|
|
215
|
-
const txId
|
|
216
|
-
|
|
217
|
-
|
|
249
|
+
const txId = Transaction.fromBEEF(output.beef).id('hex')
|
|
250
|
+
memo = { txId, expiresAt: now + this.txMemoTtlMs }
|
|
251
|
+
// prune opportunistically if the map gets too large (cheap heuristic)
|
|
252
|
+
if (this.txMemo.size > 4096) this.evictOldest(this.txMemo)
|
|
253
|
+
this.txMemo.set(keyForBeef, memo)
|
|
218
254
|
} catch {
|
|
219
255
|
continue
|
|
220
256
|
}
|
|
221
257
|
}
|
|
222
|
-
|
|
223
|
-
|
|
258
|
+
|
|
259
|
+
const uniqKey = `${memo.txId}.${output.outputIndex}`
|
|
260
|
+
// last-writer wins is fine here; outputs are identical if uniqKey matches
|
|
261
|
+
outputsMap.set(uniqKey, output)
|
|
224
262
|
}
|
|
225
263
|
}
|
|
226
264
|
return {
|
|
227
265
|
type: 'output-list',
|
|
228
|
-
outputs: Array.from(
|
|
266
|
+
outputs: Array.from(outputsMap.values())
|
|
229
267
|
}
|
|
230
268
|
}
|
|
231
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Cached wrapper for competent host discovery with stale-while-revalidate.
|
|
272
|
+
*/
|
|
273
|
+
private async getCompetentHostsCached (service: string): Promise<string[]> {
|
|
274
|
+
const now = Date.now()
|
|
275
|
+
const cached = this.hostsCache.get(service)
|
|
276
|
+
|
|
277
|
+
// if fresh, return immediately
|
|
278
|
+
if (typeof cached === 'object' && cached.expiresAt > now) {
|
|
279
|
+
return cached.hosts.slice()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// if stale but present, kick off a refresh if not already in-flight and return stale
|
|
283
|
+
if (typeof cached === 'object' && cached.expiresAt <= now) {
|
|
284
|
+
if (!this.hostsInFlight.has(service)) {
|
|
285
|
+
this.hostsInFlight.set(service, this.refreshHosts(service).finally(() => {
|
|
286
|
+
this.hostsInFlight.delete(service)
|
|
287
|
+
}))
|
|
288
|
+
}
|
|
289
|
+
return cached.hosts.slice()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// no cache: coalesce concurrent requests
|
|
293
|
+
if (this.hostsInFlight.has(service)) {
|
|
294
|
+
try {
|
|
295
|
+
const hosts = await this.hostsInFlight.get(service)
|
|
296
|
+
if (typeof hosts !== 'object') {
|
|
297
|
+
throw new Error('Hosts is not defined.')
|
|
298
|
+
}
|
|
299
|
+
return hosts.slice()
|
|
300
|
+
} catch {
|
|
301
|
+
// fall through to a fresh attempt below
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const promise = this.refreshHosts(service).finally(() => {
|
|
306
|
+
this.hostsInFlight.delete(service)
|
|
307
|
+
})
|
|
308
|
+
this.hostsInFlight.set(service, promise)
|
|
309
|
+
const hosts = await promise
|
|
310
|
+
return hosts.slice()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Actually resolves competent hosts from SLAP trackers and updates cache.
|
|
315
|
+
*/
|
|
316
|
+
private async refreshHosts (service: string): Promise<string[]> {
|
|
317
|
+
const hosts = await this.findCompetentHosts(service)
|
|
318
|
+
const expiresAt = Date.now() + this.hostsTtlMs
|
|
319
|
+
|
|
320
|
+
// bounded cache with simple FIFO eviction
|
|
321
|
+
if (!this.hostsCache.has(service) && this.hostsCache.size >= this.hostsMaxEntries) {
|
|
322
|
+
const oldestKey = this.hostsCache.keys().next().value
|
|
323
|
+
if (oldestKey !== undefined) this.hostsCache.delete(oldestKey)
|
|
324
|
+
}
|
|
325
|
+
this.hostsCache.set(service, { hosts, expiresAt })
|
|
326
|
+
return hosts
|
|
327
|
+
}
|
|
328
|
+
|
|
232
329
|
/**
|
|
233
330
|
* Returns a list of competent hosts for a given lookup service.
|
|
234
331
|
* @param service Service for which competent hosts are to be returned
|
|
@@ -237,52 +334,45 @@ export default class LookupResolver {
|
|
|
237
334
|
private async findCompetentHosts (service: string): Promise<string[]> {
|
|
238
335
|
const query: LookupQuestion = {
|
|
239
336
|
service: 'ls_slap',
|
|
240
|
-
query: {
|
|
241
|
-
service
|
|
242
|
-
}
|
|
337
|
+
query: { service }
|
|
243
338
|
}
|
|
244
339
|
|
|
245
|
-
//
|
|
340
|
+
// Query all SLAP trackers; tolerate failures.
|
|
246
341
|
const trackerResponses = await Promise.allSettled(
|
|
247
|
-
this.slapTrackers.map(
|
|
248
|
-
|
|
249
|
-
await this.facilitator.lookup(tracker, query, MAX_TRACKER_WAIT_TIME)
|
|
342
|
+
this.slapTrackers.map(async (tracker) =>
|
|
343
|
+
await this.facilitator.lookup(tracker, query, MAX_TRACKER_WAIT_TIME)
|
|
250
344
|
)
|
|
251
345
|
)
|
|
252
346
|
|
|
253
347
|
const hosts = new Set<string>()
|
|
254
348
|
|
|
255
349
|
for (const result of trackerResponses) {
|
|
256
|
-
if (result.status
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
parsed.topicOrService !== service ||
|
|
269
|
-
parsed.protocol !== 'SLAP'
|
|
270
|
-
) {
|
|
271
|
-
// Invalid advertisement, skip
|
|
272
|
-
continue
|
|
273
|
-
}
|
|
350
|
+
if (result.status !== 'fulfilled') continue
|
|
351
|
+
const answer = result.value
|
|
352
|
+
if (answer.type !== 'output-list') continue
|
|
353
|
+
|
|
354
|
+
for (const output of answer.outputs) {
|
|
355
|
+
try {
|
|
356
|
+
const tx = Transaction.fromBEEF(output.beef)
|
|
357
|
+
const script = tx.outputs[output.outputIndex]?.lockingScript
|
|
358
|
+
if (typeof script !== 'object' || script === null) continue
|
|
359
|
+
const parsed = OverlayAdminTokenTemplate.decode(script)
|
|
360
|
+
if (parsed.topicOrService !== service || parsed.protocol !== 'SLAP') continue
|
|
361
|
+
if (typeof parsed.domain === 'string' && parsed.domain.length > 0) {
|
|
274
362
|
hosts.add(parsed.domain)
|
|
275
|
-
} catch {
|
|
276
|
-
// Invalid output, skip
|
|
277
|
-
continue
|
|
278
363
|
}
|
|
364
|
+
} catch {
|
|
365
|
+
continue
|
|
279
366
|
}
|
|
280
|
-
} else {
|
|
281
|
-
// Log tracker failure and continue
|
|
282
|
-
continue
|
|
283
367
|
}
|
|
284
368
|
}
|
|
285
369
|
|
|
286
370
|
return [...hosts]
|
|
287
371
|
}
|
|
372
|
+
|
|
373
|
+
/** Evict an arbitrary “oldest” entry from a Map (iteration order). */
|
|
374
|
+
private evictOldest<T>(m: Map<string, T>): void {
|
|
375
|
+
const firstKey = m.keys().next().value
|
|
376
|
+
if (firstKey !== undefined) m.delete(firstKey)
|
|
377
|
+
}
|
|
288
378
|
}
|
|
@@ -1522,53 +1522,6 @@ describe('LookupResolver', () => {
|
|
|
1522
1522
|
expect(mockFacilitator.lookup.mock.calls.length).toBe(2)
|
|
1523
1523
|
})
|
|
1524
1524
|
|
|
1525
|
-
it('should handle all hosts failing and throw an error', async () => {
|
|
1526
|
-
const slapHostKey = new PrivateKey(42)
|
|
1527
|
-
const slapWallet = new CompletedProtoWallet(slapHostKey)
|
|
1528
|
-
const slapLib = new OverlayAdminTokenTemplate(slapWallet)
|
|
1529
|
-
const slapScript = await slapLib.lock(
|
|
1530
|
-
'SLAP',
|
|
1531
|
-
'https://slaphost.com',
|
|
1532
|
-
'ls_foo'
|
|
1533
|
-
)
|
|
1534
|
-
const slapTx = new Transaction(
|
|
1535
|
-
1,
|
|
1536
|
-
[],
|
|
1537
|
-
[
|
|
1538
|
-
{
|
|
1539
|
-
lockingScript: slapScript,
|
|
1540
|
-
satoshis: 1
|
|
1541
|
-
}
|
|
1542
|
-
],
|
|
1543
|
-
0
|
|
1544
|
-
)
|
|
1545
|
-
|
|
1546
|
-
// SLAP tracker returns host
|
|
1547
|
-
mockFacilitator.lookup.mockReturnValueOnce({
|
|
1548
|
-
type: 'output-list',
|
|
1549
|
-
outputs: [{ outputIndex: 0, beef: slapTx.toBEEF() }]
|
|
1550
|
-
})
|
|
1551
|
-
|
|
1552
|
-
// Host fails
|
|
1553
|
-
mockFacilitator.lookup.mockImplementationOnce(async () => {
|
|
1554
|
-
throw new Error('Host failed')
|
|
1555
|
-
})
|
|
1556
|
-
|
|
1557
|
-
const r = new LookupResolver({
|
|
1558
|
-
facilitator: mockFacilitator,
|
|
1559
|
-
slapTrackers: ['https://mock.slap']
|
|
1560
|
-
})
|
|
1561
|
-
|
|
1562
|
-
await expect(
|
|
1563
|
-
r.query({
|
|
1564
|
-
service: 'ls_foo',
|
|
1565
|
-
query: { test: 1 }
|
|
1566
|
-
})
|
|
1567
|
-
).rejects.toThrow('No successful responses from any hosts')
|
|
1568
|
-
|
|
1569
|
-
expect(mockFacilitator.lookup.mock.calls.length).toBe(2)
|
|
1570
|
-
})
|
|
1571
|
-
|
|
1572
1525
|
it('should continue to aggregate outputs when some hosts return invalid outputs', async () => {
|
|
1573
1526
|
const slapHostKey1 = new PrivateKey(42)
|
|
1574
1527
|
const slapWallet1 = new CompletedProtoWallet(slapHostKey1)
|