@ixo/oracle-runtime-workers 0.29.0 → 0.30.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/package.json +2 -2
- package/src/plugins/hub-actions/hub-actions.plugin.ts +73 -24
- package/src/plugins/ixo-chain/balances.ts +141 -0
- package/src/plugins/ixo-chain/blocksync.ts +11 -6
- package/src/plugins/ixo-chain/denoms.ts +84 -36
- package/src/plugins/ixo-chain/http.ts +300 -63
- package/src/plugins/ixo-chain/ixo-chain.plugin.ts +182 -301
- package/src/plugins/ixo-chain/kinds/chain/account.ts +516 -0
- package/src/plugins/ixo-chain/kinds/chain/claims.ts +287 -0
- package/src/plugins/ixo-chain/kinds/chain/economics.ts +237 -0
- package/src/plugins/ixo-chain/kinds/chain/governance.ts +455 -0
- package/src/plugins/ixo-chain/kinds/chain/grants.ts +682 -0
- package/src/plugins/ixo-chain/kinds/chain/index.ts +23 -0
- package/src/plugins/ixo-chain/kinds/chain/liquid-staking.ts +279 -0
- package/src/plugins/ixo-chain/kinds/chain/shared.ts +286 -0
- package/src/plugins/ixo-chain/kinds/chain/staking.ts +220 -0
- package/src/plugins/ixo-chain/kinds/coins.ts +112 -0
- package/src/plugins/ixo-chain/kinds/core.ts +246 -0
- package/src/plugins/ixo-chain/kinds/indexed/agent-deposits.ts +124 -0
- package/src/plugins/ixo-chain/kinds/indexed/dao-proposals.ts +379 -0
- package/src/plugins/ixo-chain/kinds/indexed/daos.ts +319 -0
- package/src/plugins/ixo-chain/kinds/indexed/disputes.ts +159 -0
- package/src/plugins/ixo-chain/kinds/indexed/evaluations.ts +172 -0
- package/src/plugins/ixo-chain/kinds/indexed/index.ts +21 -0
- package/src/plugins/ixo-chain/kinds/indexed/liquid-stake-history.ts +160 -0
- package/src/plugins/ixo-chain/kinds/indexed/member-budgets.ts +175 -0
- package/src/plugins/ixo-chain/kinds/indexed/shared.ts +211 -0
- package/src/plugins/ixo-chain/kinds/indexed/token-history.ts +205 -0
- package/src/plugins/ixo-chain/kinds/registry.ts +43 -0
- package/src/plugins/ixo-chain/kinds/types.ts +156 -0
- package/src/plugins/ixo-chain/lookup.ts +470 -0
- package/src/plugins/ixo-chain/overview.ts +1060 -0
- package/src/plugins/ixo-chain/queries.ts +170 -138
- package/src/plugins/ixo-chain/rest.ts +129 -54
- package/src/plugins/ixo-chain/summaries.ts +3 -2
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `chain_lookup` executor. Every request is planned first; one its kind
|
|
3
|
+
* refuses answers `invalid_input` on its own and is never read. The
|
|
4
|
+
* Blocksync plans of a call become ONE GraphQL document (aliases `r0…r7`
|
|
5
|
+
* by request position); the chain plans run their REST reads and denom
|
|
6
|
+
* lookups at once. Every read takes one of the user's permits
|
|
7
|
+
* (`USER_READ_PERMITS` in `http.ts`, shared by all of the user's calls), so
|
|
8
|
+
* a call never has more in flight; all of it runs inside the call's 20 s
|
|
9
|
+
* budget and the turn's abort. Each request answers on its own, in request
|
|
10
|
+
* order; the answer is cut to {@link MAX_RESULT_CHARS}.
|
|
11
|
+
*/
|
|
12
|
+
import type { z } from 'zod';
|
|
13
|
+
import type { UserIdentity } from '../../core/user-identity';
|
|
14
|
+
import {
|
|
15
|
+
blocksyncLookup,
|
|
16
|
+
STATEMENT_TIMEOUT,
|
|
17
|
+
TRANSIENT_ERROR,
|
|
18
|
+
type AliasedField,
|
|
19
|
+
type BlocksyncAnswer,
|
|
20
|
+
type GraphqlError,
|
|
21
|
+
} from './blocksync';
|
|
22
|
+
import {
|
|
23
|
+
denomDisplay,
|
|
24
|
+
knownDisplay,
|
|
25
|
+
unreadDisplay,
|
|
26
|
+
type IbcDenomTable,
|
|
27
|
+
type TokenDisplay,
|
|
28
|
+
} from './denoms';
|
|
29
|
+
import {
|
|
30
|
+
throwIfAborted,
|
|
31
|
+
tooLarge,
|
|
32
|
+
unavailable,
|
|
33
|
+
type ChainCall,
|
|
34
|
+
type Failure,
|
|
35
|
+
type Outcome,
|
|
36
|
+
type Source,
|
|
37
|
+
} from './http';
|
|
38
|
+
import type {
|
|
39
|
+
KindContext,
|
|
40
|
+
KindDef,
|
|
41
|
+
KindPlan,
|
|
42
|
+
KindRestOptions,
|
|
43
|
+
KindResult,
|
|
44
|
+
} from './kinds/types';
|
|
45
|
+
import { firstOf, type Subject } from './queries';
|
|
46
|
+
import { restJson, type Absent } from './rest';
|
|
47
|
+
import { isRecord } from './summaries';
|
|
48
|
+
|
|
49
|
+
/** Largest answer handed to the model, in JSON characters. */
|
|
50
|
+
export const MAX_RESULT_CHARS = 12_000;
|
|
51
|
+
/**
|
|
52
|
+
* Distinct denoms one request looks up to show its bank coins; any further
|
|
53
|
+
* denom is shown unread (base units, `metadataUnavailable`).
|
|
54
|
+
*/
|
|
55
|
+
export const MAX_DISPLAY_DENOMS = 20;
|
|
56
|
+
|
|
57
|
+
/** One request: its kind and the arguments the kind validates. */
|
|
58
|
+
export interface KindRequest {
|
|
59
|
+
kind: KindDef;
|
|
60
|
+
args?: unknown;
|
|
61
|
+
/**
|
|
62
|
+
* The rows a list kind reads (1–20), used as given instead of the
|
|
63
|
+
* request's `args.first` scaled for the call.
|
|
64
|
+
*/
|
|
65
|
+
first?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** What a call's requests are read with besides the call itself. */
|
|
69
|
+
export interface LookupOptions {
|
|
70
|
+
/**
|
|
71
|
+
* The IBC table bank coins are shown with: the network's built-in
|
|
72
|
+
* entries with the operator's `IXO_CHAIN_IBC_DENOMS` merged over them.
|
|
73
|
+
*/
|
|
74
|
+
ibcDenoms: IbcDenomTable;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type LookupResult =
|
|
78
|
+
| {
|
|
79
|
+
kind: string;
|
|
80
|
+
source: Source;
|
|
81
|
+
ok: true;
|
|
82
|
+
subject?: Subject;
|
|
83
|
+
first?: number;
|
|
84
|
+
data: unknown;
|
|
85
|
+
truncated?: boolean;
|
|
86
|
+
}
|
|
87
|
+
| ({ kind: string; source: Source; ok: false } & Failure);
|
|
88
|
+
|
|
89
|
+
const LEFT_OUT = tooLarge(
|
|
90
|
+
'Left out to keep the answer small; ask for it on its own.',
|
|
91
|
+
);
|
|
92
|
+
const NO_ANSWER = unavailable('blocksync', 'no answer for this request', true);
|
|
93
|
+
const NO_BLOCKSYNC = unavailable(
|
|
94
|
+
'blocksync',
|
|
95
|
+
'no valid BLOCKSYNC_GRAPHQL_URL is configured',
|
|
96
|
+
false,
|
|
97
|
+
);
|
|
98
|
+
const KIND_FAILED: Failure = {
|
|
99
|
+
reason: 'unavailable',
|
|
100
|
+
detail: 'This request failed inside the oracle; asking again will not help.',
|
|
101
|
+
retryable: false,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/* ── contexts ─────────────────────────────────────────────────────────── */
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The kind contexts of one call. A REST read is `restJson` over the call
|
|
108
|
+
* (each attempt under one of the user's permits); a denom that needs a
|
|
109
|
+
* lookup is looked up once per call (and through `denoms.ts`'s per-isolate
|
|
110
|
+
* cache), and each request looks up at most {@link MAX_DISPLAY_DENOMS}
|
|
111
|
+
* distinct denoms. Returns each request's context for the rows it reads.
|
|
112
|
+
*/
|
|
113
|
+
export function callContexts(
|
|
114
|
+
call: ChainCall,
|
|
115
|
+
identity: UserIdentity,
|
|
116
|
+
options: LookupOptions,
|
|
117
|
+
): (first: number) => KindContext {
|
|
118
|
+
function rest<S extends z.ZodType>(
|
|
119
|
+
path: string,
|
|
120
|
+
schema: S,
|
|
121
|
+
opts: KindRestOptions & { noneOn?: undefined },
|
|
122
|
+
): Promise<Outcome<z.infer<S>>>;
|
|
123
|
+
function rest<S extends z.ZodType>(
|
|
124
|
+
path: string,
|
|
125
|
+
schema: S,
|
|
126
|
+
opts: KindRestOptions & { noneOn: readonly number[] },
|
|
127
|
+
): Promise<Outcome<z.infer<S>> | Absent>;
|
|
128
|
+
function rest<S extends z.ZodType>(
|
|
129
|
+
path: string,
|
|
130
|
+
schema: S,
|
|
131
|
+
opts: KindRestOptions,
|
|
132
|
+
): Promise<Outcome<z.infer<S>> | Absent> {
|
|
133
|
+
return restJson(call, path, schema, opts);
|
|
134
|
+
}
|
|
135
|
+
const lookups = new Map<string, Promise<TokenDisplay>>();
|
|
136
|
+
const lookUp = (denom: string): Promise<TokenDisplay> => {
|
|
137
|
+
const running = lookups.get(denom);
|
|
138
|
+
if (running) return running;
|
|
139
|
+
const pending = denomDisplay(call, denom, options.ibcDenoms);
|
|
140
|
+
lookups.set(denom, pending);
|
|
141
|
+
return pending;
|
|
142
|
+
};
|
|
143
|
+
return (first) => {
|
|
144
|
+
const asked = new Set<string>();
|
|
145
|
+
return {
|
|
146
|
+
call,
|
|
147
|
+
identity,
|
|
148
|
+
first,
|
|
149
|
+
rest,
|
|
150
|
+
display(denom) {
|
|
151
|
+
const known = knownDisplay(denom, options.ibcDenoms);
|
|
152
|
+
if (known) return Promise.resolve(known);
|
|
153
|
+
if (!asked.has(denom)) {
|
|
154
|
+
if (asked.size >= MAX_DISPLAY_DENOMS)
|
|
155
|
+
return Promise.resolve(unreadDisplay(denom));
|
|
156
|
+
asked.add(denom);
|
|
157
|
+
}
|
|
158
|
+
return lookUp(denom);
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The request's own `first` when it is a whole number of at least 1 (its kind's schema checks the rest). */
|
|
165
|
+
function requestedFirst(args: unknown): number | undefined {
|
|
166
|
+
const value = isRecord(args) ? args.first : undefined;
|
|
167
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 1
|
|
168
|
+
? value
|
|
169
|
+
: undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* ── one request ──────────────────────────────────────────────────────── */
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A kind that throws for anything but the turn's abort has a bug: its
|
|
176
|
+
* request fails on its own (logged with the kind and the error's class
|
|
177
|
+
* only: a message may carry request data) and the others still answer.
|
|
178
|
+
*/
|
|
179
|
+
function kindFailed(call: ChainCall, kind: string, error: unknown): KindResult {
|
|
180
|
+
throwIfAborted(call.signal);
|
|
181
|
+
// eslint-disable-next-line no-console -- console IS the logger on Workers
|
|
182
|
+
console.error(
|
|
183
|
+
`[ixo-chain] kind ${kind} threw: ${error instanceof Error ? error.name : 'non-error value'}`,
|
|
184
|
+
);
|
|
185
|
+
return { ok: false, ...KIND_FAILED };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
type BlocksyncPlan = Extract<KindPlan, { source: 'blocksync' }>;
|
|
189
|
+
type ChainPlan = Extract<KindPlan, { source: 'chain' }>;
|
|
190
|
+
|
|
191
|
+
/** A request as its plan (and, for Blocksync, its document field), or its refusal. */
|
|
192
|
+
type Planned =
|
|
193
|
+
| { plan: ChainPlan }
|
|
194
|
+
| { plan: BlocksyncPlan; field: AliasedField }
|
|
195
|
+
| { refused: KindResult };
|
|
196
|
+
|
|
197
|
+
function planOf(
|
|
198
|
+
call: ChainCall,
|
|
199
|
+
request: KindRequest,
|
|
200
|
+
ctx: KindContext,
|
|
201
|
+
alias: string,
|
|
202
|
+
): Planned {
|
|
203
|
+
try {
|
|
204
|
+
const plan = request.kind.plan(request.args, ctx);
|
|
205
|
+
if ('invalid' in plan)
|
|
206
|
+
return {
|
|
207
|
+
refused: {
|
|
208
|
+
ok: false,
|
|
209
|
+
reason: 'invalid_input',
|
|
210
|
+
detail: plan.invalid,
|
|
211
|
+
retryable: false,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
if (plan.source === 'chain') return { plan };
|
|
215
|
+
const field = plan.field(alias);
|
|
216
|
+
// A field under another alias would break every request of the document.
|
|
217
|
+
if (!field.startsWith(`${alias}: `))
|
|
218
|
+
throw new Error(`its field is not aliased ${alias}`);
|
|
219
|
+
return { plan, field: { alias, field } };
|
|
220
|
+
} catch (error) {
|
|
221
|
+
return { refused: kindFailed(call, request.kind.name, error) };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function settle(
|
|
226
|
+
call: ChainCall,
|
|
227
|
+
kind: string,
|
|
228
|
+
answer: () => KindResult | Promise<KindResult>,
|
|
229
|
+
): Promise<KindResult> {
|
|
230
|
+
try {
|
|
231
|
+
return await answer();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
return kindFailed(call, kind, error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The failure the document's errors give one alias, if any. */
|
|
238
|
+
function aliasFailure(alias: string, errors: GraphqlError[]): Failure | null {
|
|
239
|
+
const mine = errors.filter(
|
|
240
|
+
(error) => error.path.length === 0 || error.path[0] === alias,
|
|
241
|
+
);
|
|
242
|
+
if (!mine.length) return null;
|
|
243
|
+
if (mine.some((error) => STATEMENT_TIMEOUT.test(error.message)))
|
|
244
|
+
return {
|
|
245
|
+
reason: 'index_timeout',
|
|
246
|
+
detail:
|
|
247
|
+
'The index could not answer in time; ask for fewer rows or a narrower filter.',
|
|
248
|
+
retryable: false,
|
|
249
|
+
};
|
|
250
|
+
if (mine.some((error) => TRANSIENT_ERROR.test(error.message)))
|
|
251
|
+
return unavailable('blocksync', 'a transient database error', true);
|
|
252
|
+
const message = mine[0]?.message ?? '';
|
|
253
|
+
return {
|
|
254
|
+
reason: 'unavailable',
|
|
255
|
+
detail: `Blocksync could not answer this request: ${message.length > 160 ? `${message.slice(0, 159)}…` : message}`,
|
|
256
|
+
retryable: false,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** One alias's answer from the document, or why it has none. */
|
|
261
|
+
function aliasAnswer(
|
|
262
|
+
read: Outcome<BlocksyncAnswer>,
|
|
263
|
+
alias: string,
|
|
264
|
+
plan: BlocksyncPlan,
|
|
265
|
+
): KindResult | Promise<KindResult> {
|
|
266
|
+
if (!read.ok) return { ok: false, ...read.failure };
|
|
267
|
+
const failure = aliasFailure(alias, read.value.errors);
|
|
268
|
+
if (failure) return { ok: false, ...failure };
|
|
269
|
+
// An alias the answer does not carry was not answered, which says
|
|
270
|
+
// nothing about whether the record exists.
|
|
271
|
+
const { data } = read.value;
|
|
272
|
+
if (!data || !(alias in data)) return { ok: false, ...NO_ANSWER };
|
|
273
|
+
return plan.shape(data[alias]);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function resultOf(
|
|
277
|
+
kind: string,
|
|
278
|
+
source: Source,
|
|
279
|
+
result: KindResult,
|
|
280
|
+
): LookupResult {
|
|
281
|
+
if (!result.ok)
|
|
282
|
+
return {
|
|
283
|
+
kind,
|
|
284
|
+
source,
|
|
285
|
+
ok: false,
|
|
286
|
+
reason: result.reason,
|
|
287
|
+
detail: result.detail,
|
|
288
|
+
retryable: result.retryable,
|
|
289
|
+
};
|
|
290
|
+
return {
|
|
291
|
+
kind,
|
|
292
|
+
source,
|
|
293
|
+
ok: true,
|
|
294
|
+
...(result.subject ? { subject: result.subject } : {}),
|
|
295
|
+
...(result.first !== undefined ? { first: result.first } : {}),
|
|
296
|
+
data: result.data,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** The alias of the request at `index`. */
|
|
301
|
+
const aliasOf = (index: number) => `r${index}`;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Every request's answer, in request order. Only the turn's abort is
|
|
305
|
+
* thrown.
|
|
306
|
+
*/
|
|
307
|
+
export async function runRequests(
|
|
308
|
+
call: ChainCall,
|
|
309
|
+
identity: UserIdentity,
|
|
310
|
+
requests: readonly KindRequest[],
|
|
311
|
+
options: LookupOptions,
|
|
312
|
+
): Promise<LookupResult[]> {
|
|
313
|
+
const contextFor = callContexts(call, identity, options);
|
|
314
|
+
const planned = requests.map((request, index) => ({
|
|
315
|
+
kind: request.kind,
|
|
316
|
+
planned: planOf(
|
|
317
|
+
call,
|
|
318
|
+
request,
|
|
319
|
+
contextFor(
|
|
320
|
+
request.first ?? firstOf(requestedFirst(request.args), requests.length),
|
|
321
|
+
),
|
|
322
|
+
aliasOf(index),
|
|
323
|
+
),
|
|
324
|
+
}));
|
|
325
|
+
const fields = planned.flatMap(({ planned: entry }) =>
|
|
326
|
+
'field' in entry ? [entry.field] : [],
|
|
327
|
+
);
|
|
328
|
+
const document: Promise<Outcome<BlocksyncAnswer>> = !fields.length
|
|
329
|
+
? Promise.resolve({ ok: true, value: { data: null, errors: [] } })
|
|
330
|
+
: call.blocksyncUrl
|
|
331
|
+
? blocksyncLookup(call, fields)
|
|
332
|
+
: Promise.resolve({ ok: false, failure: NO_BLOCKSYNC });
|
|
333
|
+
return Promise.all(
|
|
334
|
+
planned.map(async ({ kind, planned: entry }) => {
|
|
335
|
+
if ('refused' in entry)
|
|
336
|
+
return resultOf(kind.name, kind.source, entry.refused);
|
|
337
|
+
if (!('field' in entry)) {
|
|
338
|
+
const { plan } = entry;
|
|
339
|
+
return resultOf(
|
|
340
|
+
kind.name,
|
|
341
|
+
'chain',
|
|
342
|
+
await settle(call, kind.name, () => plan.run()),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const { plan, field } = entry;
|
|
346
|
+
const read = await document;
|
|
347
|
+
return resultOf(
|
|
348
|
+
kind.name,
|
|
349
|
+
'blocksync',
|
|
350
|
+
await settle(call, kind.name, () =>
|
|
351
|
+
aliasAnswer(read, field.alias, plan),
|
|
352
|
+
),
|
|
353
|
+
);
|
|
354
|
+
}),
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* ── size ─────────────────────────────────────────────────────────────── */
|
|
359
|
+
|
|
360
|
+
const size = (value: unknown) => JSON.stringify(value).length;
|
|
361
|
+
|
|
362
|
+
/** The largest array (by JSON size) with more than one element inside `value`. */
|
|
363
|
+
function largestArray(
|
|
364
|
+
value: unknown,
|
|
365
|
+
): { list: unknown[]; size: number } | null {
|
|
366
|
+
let best: { list: unknown[]; size: number } | null = null;
|
|
367
|
+
const visit = (node: unknown) => {
|
|
368
|
+
if (Array.isArray(node)) {
|
|
369
|
+
if (node.length > 1) {
|
|
370
|
+
const bytes = size(node);
|
|
371
|
+
if (!best || bytes > best.size) best = { list: node, size: bytes };
|
|
372
|
+
}
|
|
373
|
+
node.forEach(visit);
|
|
374
|
+
} else if (isRecord(node)) Object.values(node).forEach(visit);
|
|
375
|
+
};
|
|
376
|
+
visit(value);
|
|
377
|
+
return best;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Halves the largest list inside `within` (keeping its first rows) until
|
|
382
|
+
* `holder` serialises within `budget` or no list is left to halve; marks
|
|
383
|
+
* `holder` `truncated` when anything was cut.
|
|
384
|
+
*/
|
|
385
|
+
export function shrink(
|
|
386
|
+
holder: { truncated?: boolean },
|
|
387
|
+
within: unknown,
|
|
388
|
+
budget: number,
|
|
389
|
+
): void {
|
|
390
|
+
while (size(holder) > budget) {
|
|
391
|
+
const found = largestArray(within);
|
|
392
|
+
if (!found) return;
|
|
393
|
+
found.list.length = Math.ceil(found.list.length / 2);
|
|
394
|
+
holder.truncated = true;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Fits `parts` (the results inside `whole`) into what
|
|
400
|
+
* {@link MAX_RESULT_CHARS} leaves after the envelope: each part gets an
|
|
401
|
+
* equal share, smallest first, and a share a part does not use passes to
|
|
402
|
+
* the larger parts after it. A cut re-measures only its own part.
|
|
403
|
+
*/
|
|
404
|
+
function shareOut(
|
|
405
|
+
whole: unknown,
|
|
406
|
+
parts: { data: unknown; truncated?: boolean }[],
|
|
407
|
+
): void {
|
|
408
|
+
const sizes = new Map(parts.map((part) => [part, size(part)]));
|
|
409
|
+
let left =
|
|
410
|
+
MAX_RESULT_CHARS -
|
|
411
|
+
(size(whole) - [...sizes.values()].reduce((sum, n) => sum + n, 0));
|
|
412
|
+
const bySize = [...parts].sort(
|
|
413
|
+
(a, b) => (sizes.get(a) ?? 0) - (sizes.get(b) ?? 0),
|
|
414
|
+
);
|
|
415
|
+
bySize.forEach((part, index) => {
|
|
416
|
+
shrink(part, part.data, Math.floor(left / (bySize.length - index)));
|
|
417
|
+
left -= size(part);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Cuts `results` (inside `whole`) until `whole` fits
|
|
423
|
+
* {@link MAX_RESULT_CHARS}: lists are halved within each result's share;
|
|
424
|
+
* a result that cannot be cut to its share leaves the largest answers out,
|
|
425
|
+
* one at a time, as `too_large`.
|
|
426
|
+
*/
|
|
427
|
+
export function fitResults(whole: unknown, results: LookupResult[]): void {
|
|
428
|
+
shareOut(
|
|
429
|
+
whole,
|
|
430
|
+
results.flatMap((result) => (result.ok ? [result] : [])),
|
|
431
|
+
);
|
|
432
|
+
while (size(whole) > MAX_RESULT_CHARS) {
|
|
433
|
+
let largest = -1;
|
|
434
|
+
results.forEach((result, index) => {
|
|
435
|
+
if (result.ok && (largest < 0 || size(result) > size(results[largest])))
|
|
436
|
+
largest = index;
|
|
437
|
+
});
|
|
438
|
+
const evicted = results[largest];
|
|
439
|
+
if (!evicted) break;
|
|
440
|
+
results[largest] = {
|
|
441
|
+
kind: evicted.kind,
|
|
442
|
+
source: evicted.source,
|
|
443
|
+
ok: false,
|
|
444
|
+
...LEFT_OUT,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** `chain_lookup`'s answer: every request's result in order, cut to size. */
|
|
450
|
+
export async function chainLookup(
|
|
451
|
+
call: ChainCall,
|
|
452
|
+
identity: UserIdentity,
|
|
453
|
+
requests: readonly KindRequest[],
|
|
454
|
+
options: LookupOptions,
|
|
455
|
+
): Promise<{
|
|
456
|
+
ok: true;
|
|
457
|
+
network: ChainCall['network'];
|
|
458
|
+
fetchedAt: string;
|
|
459
|
+
results: LookupResult[];
|
|
460
|
+
}> {
|
|
461
|
+
const results = await runRequests(call, identity, requests, options);
|
|
462
|
+
const whole = {
|
|
463
|
+
ok: true as const,
|
|
464
|
+
network: call.network,
|
|
465
|
+
fetchedAt: new Date().toISOString(),
|
|
466
|
+
results,
|
|
467
|
+
};
|
|
468
|
+
fitResults(whole, results);
|
|
469
|
+
return whole;
|
|
470
|
+
}
|