@gibs/quotes 1.13.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/LICENSE +15 -0
- package/README.md +45 -0
- package/dist/cache.d.ts +73 -0
- package/dist/cache.js +137 -0
- package/dist/carriers/calibration.d.ts +115 -0
- package/dist/carriers/calibration.js +83 -0
- package/dist/carriers/lifi.d.ts +63 -0
- package/dist/carriers/lifi.js +217 -0
- package/dist/carriers/near-intents.d.ts +91 -0
- package/dist/carriers/near-intents.js +249 -0
- package/dist/carriers/price-catalogue.d.ts +186 -0
- package/dist/carriers/price-catalogue.js +191 -0
- package/dist/carriers/relay.d.ts +97 -0
- package/dist/carriers/relay.js +242 -0
- package/dist/chain-lists.d.ts +144 -0
- package/dist/chain-lists.js +169 -0
- package/dist/client.d.ts +102 -0
- package/dist/client.js +402 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +18 -0
- package/dist/quote-guards.d.ts +128 -0
- package/dist/quote-guards.js +240 -0
- package/dist/quote-refusals.d.ts +180 -0
- package/dist/quote-refusals.js +284 -0
- package/dist/quote-service-contract.d.ts +2240 -0
- package/dist/quote-service-contract.js +247 -0
- package/dist/search/capabilities.d.ts +68 -0
- package/dist/search/capabilities.js +28 -0
- package/dist/search/enumerate.d.ts +162 -0
- package/dist/search/enumerate.js +313 -0
- package/dist/search/index.d.ts +17 -0
- package/dist/search/index.js +17 -0
- package/dist/search/serve.d.ts +178 -0
- package/dist/search/serve.js +142 -0
- package/dist/search/waves.d.ts +481 -0
- package/dist/search/waves.js +939 -0
- package/dist/single-flight.d.ts +90 -0
- package/dist/single-flight.js +142 -0
- package/dist/types.d.ts +502 -0
- package/dist/types.js +1 -0
- package/dist/upstream.d.ts +126 -0
- package/dist/upstream.js +343 -0
- package/llms.txt +217 -0
- package/package.json +142 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type DisplayProviderSlot } from './quote-service-contract.js';
|
|
2
|
+
/**
|
|
3
|
+
* The bucket-and-amount-aware in-flight coordinator `docs/quote-service.md`'s
|
|
4
|
+
* "Cache" section describes: "identical concurrent requests wait on one
|
|
5
|
+
* upstream call. An `estimate-ok` request joins an in-flight call for another
|
|
6
|
+
* amount in the same bucket and gets a scaled answer."
|
|
7
|
+
*
|
|
8
|
+
* WHY THIS IS SEPARATE FROM `cache.ts`'S OWN `joinOrRun`. `AnswerCache.joinOrRun`
|
|
9
|
+
* only ever dedups CALLERS OF THE IDENTICAL KEY, and a key already collapses
|
|
10
|
+
* every amount in a two-significant-figure bucket (`displayQuoteCacheKey`) —
|
|
11
|
+
* so it already gives an `estimate-ok` ask sharing behaviour for free. What it
|
|
12
|
+
* cannot do is the ONE THING this module exists for: telling an `exact` ask
|
|
13
|
+
* apart from an `estimate-ok` one sharing the same key, so that an `exact`
|
|
14
|
+
* ask NEVER receives another asker's answer for a different literal amount —
|
|
15
|
+
* `docs/quote-service.md`'s rule, "`precision: 'exact'` never takes a scaled
|
|
16
|
+
* answer" — while an `estimate-ok` ask still gets to share, scaled to its own
|
|
17
|
+
* amount. This module keys its own two in-flight maps directly on
|
|
18
|
+
* `(bucketKey, precision, askedAmount)` rather than reusing `AnswerCache`'s
|
|
19
|
+
* single, coarser key space.
|
|
20
|
+
*/
|
|
21
|
+
/** Whether an ask may join an in-flight call for a different literal amount in the same bucket. */
|
|
22
|
+
export type SingleFlightPrecision = 'estimate-ok' | 'exact';
|
|
23
|
+
/** One caller's own request entering single-flight de-duplication. */
|
|
24
|
+
export type SingleFlightRequest = {
|
|
25
|
+
/** The bucket-level key this ask maps to — a {@link displayQuoteCacheKey} result. */
|
|
26
|
+
readonly bucketKey: string;
|
|
27
|
+
/** This caller's own literal amount, in the asked token's base units. */
|
|
28
|
+
readonly askedAmount: bigint;
|
|
29
|
+
/** `'exact'` only ever shares a call with an identical literal amount; `'estimate-ok'` shares any call in the bucket. */
|
|
30
|
+
readonly precision: SingleFlightPrecision;
|
|
31
|
+
};
|
|
32
|
+
/** What running the shared upstream call produces: the raw answer, and the literal amount it was actually asked for. */
|
|
33
|
+
export type SingleFlightOutcome = {
|
|
34
|
+
readonly answer: DisplayProviderSlot;
|
|
35
|
+
/** The amount `answer` is a genuine, un-scaled reply to — the FIRST caller's own amount, since only the first caller's `run` is ever invoked. */
|
|
36
|
+
readonly askedAmount: bigint;
|
|
37
|
+
};
|
|
38
|
+
/** What {@link SingleFlightGroup.ask} hands back to one caller. */
|
|
39
|
+
export type SingleFlightResult = {
|
|
40
|
+
/** This caller's own answer — scaled to its own amount when it joined a call for a different one. */
|
|
41
|
+
readonly answer: DisplayProviderSlot;
|
|
42
|
+
/** Whether this caller joined another's in-flight call rather than starting its own. */
|
|
43
|
+
readonly joined: boolean;
|
|
44
|
+
};
|
|
45
|
+
/** Runs the shared upstream call for a key, if nobody else is already running one. */
|
|
46
|
+
export type SingleFlightRun = () => Promise<SingleFlightOutcome>;
|
|
47
|
+
/**
|
|
48
|
+
* Thrown when `run` does not settle before {@link SingleFlightConfig.timeoutMs}.
|
|
49
|
+
* Every caller sharing that call — the one that started it and every joiner —
|
|
50
|
+
* rejects with this.
|
|
51
|
+
*
|
|
52
|
+
* A CALLER'S OWN ABORT NEVER REACHES THIS FAR. `SingleFlightGroup.ask` takes
|
|
53
|
+
* no `AbortSignal`, by design: `docs/quote-service.md`'s rule, "a client
|
|
54
|
+
* disconnect never cancels the shared call," is upheld structurally, by never
|
|
55
|
+
* giving a caller a channel to cancel it, rather than by remembering to
|
|
56
|
+
* ignore one. Only this module's OWN timeout — never a caller — ends a call
|
|
57
|
+
* that runs too long.
|
|
58
|
+
*/
|
|
59
|
+
export declare class SingleFlightTimeoutError extends Error {
|
|
60
|
+
readonly key: string;
|
|
61
|
+
readonly timeoutMs: number;
|
|
62
|
+
constructor(key: string, timeoutMs: number);
|
|
63
|
+
}
|
|
64
|
+
/** The bucket-and-amount-aware in-flight coordinator. */
|
|
65
|
+
export type SingleFlightGroup = {
|
|
66
|
+
/**
|
|
67
|
+
* Asks for `request`, sharing an already in-flight call when this
|
|
68
|
+
* request's own precision allows it, or starting `run` when it does not.
|
|
69
|
+
*
|
|
70
|
+
* @param request - this caller's own bucket key, amount and precision
|
|
71
|
+
* @param run - the upstream call to make, if nobody joinable is already running one
|
|
72
|
+
* @returns this caller's own answer, and whether it joined another's call
|
|
73
|
+
* @throws {SingleFlightTimeoutError} when the shared call does not settle within the timeout
|
|
74
|
+
*/
|
|
75
|
+
readonly ask: (request: SingleFlightRequest, run: SingleFlightRun) => Promise<SingleFlightResult>;
|
|
76
|
+
};
|
|
77
|
+
/** How a host builds a {@link SingleFlightGroup}. */
|
|
78
|
+
export type SingleFlightConfig = {
|
|
79
|
+
/** How long a shared call may run before every caller sharing it rejects with {@link SingleFlightTimeoutError}. Defaults to {@link DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS}. */
|
|
80
|
+
readonly timeoutMs?: number;
|
|
81
|
+
};
|
|
82
|
+
/** The 15 s default timeout `docs/quote-service.md`'s "Cache" section names: "own 15 s timeout." */
|
|
83
|
+
export declare const DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 15000;
|
|
84
|
+
/**
|
|
85
|
+
* Builds a fresh, empty {@link SingleFlightGroup}.
|
|
86
|
+
*
|
|
87
|
+
* @param config - see {@link SingleFlightConfig}
|
|
88
|
+
* @returns the group
|
|
89
|
+
*/
|
|
90
|
+
export declare const createSingleFlightGroup: (config?: SingleFlightConfig) => SingleFlightGroup;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { scaleToAskedAmount } from './quote-guards.js';
|
|
2
|
+
import { decodeBaseUnitsAmount, encodeBaseUnitsAmount, toEstimatedDelivery, } from './quote-service-contract.js';
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when `run` does not settle before {@link SingleFlightConfig.timeoutMs}.
|
|
5
|
+
* Every caller sharing that call — the one that started it and every joiner —
|
|
6
|
+
* rejects with this.
|
|
7
|
+
*
|
|
8
|
+
* A CALLER'S OWN ABORT NEVER REACHES THIS FAR. `SingleFlightGroup.ask` takes
|
|
9
|
+
* no `AbortSignal`, by design: `docs/quote-service.md`'s rule, "a client
|
|
10
|
+
* disconnect never cancels the shared call," is upheld structurally, by never
|
|
11
|
+
* giving a caller a channel to cancel it, rather than by remembering to
|
|
12
|
+
* ignore one. Only this module's OWN timeout — never a caller — ends a call
|
|
13
|
+
* that runs too long.
|
|
14
|
+
*/
|
|
15
|
+
export class SingleFlightTimeoutError extends Error {
|
|
16
|
+
key;
|
|
17
|
+
timeoutMs;
|
|
18
|
+
constructor(key, timeoutMs) {
|
|
19
|
+
super(`no answer for "${key}" within ${timeoutMs}ms`);
|
|
20
|
+
this.key = key;
|
|
21
|
+
this.timeoutMs = timeoutMs;
|
|
22
|
+
this.name = 'SingleFlightTimeoutError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** The 15 s default timeout `docs/quote-service.md`'s "Cache" section names: "own 15 s timeout." */
|
|
26
|
+
export const DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 15_000;
|
|
27
|
+
const scheduleTimer = (callback, delayMs) => {
|
|
28
|
+
const handle = setTimeout(callback, Math.max(0, delayMs));
|
|
29
|
+
if (typeof handle === 'object' && handle !== null && 'unref' in handle) {
|
|
30
|
+
handle.unref();
|
|
31
|
+
}
|
|
32
|
+
return () => clearTimeout(handle);
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Turns the shared call's own outcome into ONE joiner's own answer: unchanged
|
|
36
|
+
* when the joiner's amount matches the amount actually asked upstream (a
|
|
37
|
+
* genuine, un-scaled reply for that joiner too), or scaled via
|
|
38
|
+
* {@link scaleToAskedAmount} and re-labelled `basis: 'scaled-from-nearby-amount'`
|
|
39
|
+
* otherwise. A refusal or a `pending` slot carries no figure to scale, so it
|
|
40
|
+
* passes through unchanged either way — only `cache` is set to `'joined'` on
|
|
41
|
+
* a `quoted` answer, since only that status carries the field.
|
|
42
|
+
*
|
|
43
|
+
* @param outcome - the shared call's own outcome
|
|
44
|
+
* @param joinerAskedAmount - this joiner's own literal amount
|
|
45
|
+
* @returns this joiner's own answer
|
|
46
|
+
*/
|
|
47
|
+
const answerForJoiner = (outcome, joinerAskedAmount) => {
|
|
48
|
+
const { answer } = outcome;
|
|
49
|
+
if (answer.status !== 'quoted')
|
|
50
|
+
return answer;
|
|
51
|
+
if (outcome.askedAmount === joinerAskedAmount) {
|
|
52
|
+
return { ...answer, cache: 'joined' };
|
|
53
|
+
}
|
|
54
|
+
const rawExpected = decodeBaseUnitsAmount(answer.expectedAmount);
|
|
55
|
+
const scaledExpected = scaleToAskedAmount(rawExpected, joinerAskedAmount, outcome.askedAmount);
|
|
56
|
+
const scaledMinimum = answer.minimumAmount === undefined
|
|
57
|
+
? undefined
|
|
58
|
+
: scaleToAskedAmount(decodeBaseUnitsAmount(answer.minimumAmount), joinerAskedAmount, outcome.askedAmount);
|
|
59
|
+
return {
|
|
60
|
+
status: 'quoted',
|
|
61
|
+
basis: 'scaled-from-nearby-amount',
|
|
62
|
+
askedAmount: encodeBaseUnitsAmount(joinerAskedAmount),
|
|
63
|
+
quotedAt: answer.quotedAt,
|
|
64
|
+
ageMs: answer.ageMs,
|
|
65
|
+
cache: 'joined',
|
|
66
|
+
expectedAmount: toEstimatedDelivery(scaledExpected),
|
|
67
|
+
...(scaledMinimum === undefined ? {} : { minimumAmount: toEstimatedDelivery(scaledMinimum) }),
|
|
68
|
+
scaledFrom: { askedAmount: answer.askedAmount, expectedAmount: answer.expectedAmount },
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Builds a fresh, empty {@link SingleFlightGroup}.
|
|
73
|
+
*
|
|
74
|
+
* @param config - see {@link SingleFlightConfig}
|
|
75
|
+
* @returns the group
|
|
76
|
+
*/
|
|
77
|
+
export const createSingleFlightGroup = (config = {}) => {
|
|
78
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
79
|
+
// `estimate-ok` asks share ONE call per bucket, regardless of literal
|
|
80
|
+
// amount — matching "joins an in-flight call for another amount in the
|
|
81
|
+
// same bucket." `exact` asks share only with an identical literal amount,
|
|
82
|
+
// in their OWN map, so they never inherit an estimate-ok call's amount.
|
|
83
|
+
const estimateInFlightByBucket = new Map();
|
|
84
|
+
const exactInFlightByAmountKey = new Map();
|
|
85
|
+
const runWithTimeout = (key, run) => new Promise((resolve, reject) => {
|
|
86
|
+
let settled = false;
|
|
87
|
+
const cancelTimer = scheduleTimer(() => {
|
|
88
|
+
if (settled)
|
|
89
|
+
return;
|
|
90
|
+
settled = true;
|
|
91
|
+
reject(new SingleFlightTimeoutError(key, timeoutMs));
|
|
92
|
+
}, timeoutMs);
|
|
93
|
+
run().then((outcome) => {
|
|
94
|
+
if (settled)
|
|
95
|
+
return;
|
|
96
|
+
settled = true;
|
|
97
|
+
cancelTimer();
|
|
98
|
+
resolve(outcome);
|
|
99
|
+
}, (error) => {
|
|
100
|
+
if (settled)
|
|
101
|
+
return;
|
|
102
|
+
settled = true;
|
|
103
|
+
cancelTimer();
|
|
104
|
+
reject(error);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
ask: async (request, run) => {
|
|
109
|
+
if (request.precision === 'exact') {
|
|
110
|
+
const amountKey = `${request.bucketKey}#exact#${request.askedAmount.toString()}`;
|
|
111
|
+
const existing = exactInFlightByAmountKey.get(amountKey);
|
|
112
|
+
if (existing !== undefined) {
|
|
113
|
+
const outcome = await existing;
|
|
114
|
+
return { answer: answerForJoiner(outcome, request.askedAmount), joined: true };
|
|
115
|
+
}
|
|
116
|
+
const promise = runWithTimeout(amountKey, run);
|
|
117
|
+
exactInFlightByAmountKey.set(amountKey, promise);
|
|
118
|
+
try {
|
|
119
|
+
const outcome = await promise;
|
|
120
|
+
return { answer: outcome.answer, joined: false };
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
exactInFlightByAmountKey.delete(amountKey);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const existing = estimateInFlightByBucket.get(request.bucketKey);
|
|
127
|
+
if (existing !== undefined) {
|
|
128
|
+
const outcome = await existing;
|
|
129
|
+
return { answer: answerForJoiner(outcome, request.askedAmount), joined: true };
|
|
130
|
+
}
|
|
131
|
+
const promise = runWithTimeout(request.bucketKey, run);
|
|
132
|
+
estimateInFlightByBucket.set(request.bucketKey, promise);
|
|
133
|
+
try {
|
|
134
|
+
const outcome = await promise;
|
|
135
|
+
return { answer: outcome.answer, joined: false };
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
estimateInFlightByBucket.delete(request.bucketKey);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
};
|