@metamask-previews/bridge-status-controller 73.1.0-preview-e12e4a38f → 74.0.0-preview-4f9d41d
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 +9 -1
- package/dist/bridge-status-controller.cjs +167 -21
- package/dist/bridge-status-controller.cjs.map +1 -1
- package/dist/bridge-status-controller.d.cts +6 -1
- package/dist/bridge-status-controller.d.cts.map +1 -1
- package/dist/bridge-status-controller.d.mts +6 -1
- package/dist/bridge-status-controller.d.mts.map +1 -1
- package/dist/bridge-status-controller.mjs +167 -21
- package/dist/bridge-status-controller.mjs.map +1 -1
- package/dist/constants.cjs +1 -0
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts.map +1 -1
- package/dist/constants.d.mts.map +1 -1
- package/dist/constants.mjs +1 -0
- package/dist/constants.mjs.map +1 -1
- package/dist/index.cjs +12 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -1
- package/dist/quote-status-manager/constants.cjs +181 -0
- package/dist/quote-status-manager/constants.cjs.map +1 -0
- package/dist/quote-status-manager/constants.d.cts +137 -0
- package/dist/quote-status-manager/constants.d.cts.map +1 -0
- package/dist/quote-status-manager/constants.d.mts +137 -0
- package/dist/quote-status-manager/constants.d.mts.map +1 -0
- package/dist/quote-status-manager/constants.mjs +178 -0
- package/dist/quote-status-manager/constants.mjs.map +1 -0
- package/dist/quote-status-manager/errors.cjs +51 -0
- package/dist/quote-status-manager/errors.cjs.map +1 -0
- package/dist/quote-status-manager/errors.d.cts +45 -0
- package/dist/quote-status-manager/errors.d.cts.map +1 -0
- package/dist/quote-status-manager/errors.d.mts +45 -0
- package/dist/quote-status-manager/errors.d.mts.map +1 -0
- package/dist/quote-status-manager/errors.mjs +46 -0
- package/dist/quote-status-manager/errors.mjs.map +1 -0
- package/dist/quote-status-manager/quote-status-api-service.cjs +241 -0
- package/dist/quote-status-manager/quote-status-api-service.cjs.map +1 -0
- package/dist/quote-status-manager/quote-status-api-service.d.cts +93 -0
- package/dist/quote-status-manager/quote-status-api-service.d.cts.map +1 -0
- package/dist/quote-status-manager/quote-status-api-service.d.mts +93 -0
- package/dist/quote-status-manager/quote-status-api-service.d.mts.map +1 -0
- package/dist/quote-status-manager/quote-status-api-service.mjs +237 -0
- package/dist/quote-status-manager/quote-status-api-service.mjs.map +1 -0
- package/dist/quote-status-manager/quote-status-entry-store.cjs +271 -0
- package/dist/quote-status-manager/quote-status-entry-store.cjs.map +1 -0
- package/dist/quote-status-manager/quote-status-entry-store.d.cts +144 -0
- package/dist/quote-status-manager/quote-status-entry-store.d.cts.map +1 -0
- package/dist/quote-status-manager/quote-status-entry-store.d.mts +144 -0
- package/dist/quote-status-manager/quote-status-entry-store.d.mts.map +1 -0
- package/dist/quote-status-manager/quote-status-entry-store.mjs +267 -0
- package/dist/quote-status-manager/quote-status-entry-store.mjs.map +1 -0
- package/dist/quote-status-manager/quote-status-get-with-retry-outcome.cjs +29 -0
- package/dist/quote-status-manager/quote-status-get-with-retry-outcome.cjs.map +1 -0
- package/dist/quote-status-manager/quote-status-get-with-retry-outcome.d.cts +32 -0
- package/dist/quote-status-manager/quote-status-get-with-retry-outcome.d.cts.map +1 -0
- package/dist/quote-status-manager/quote-status-get-with-retry-outcome.d.mts +32 -0
- package/dist/quote-status-manager/quote-status-get-with-retry-outcome.d.mts.map +1 -0
- package/dist/quote-status-manager/quote-status-get-with-retry-outcome.mjs +25 -0
- package/dist/quote-status-manager/quote-status-get-with-retry-outcome.mjs.map +1 -0
- package/dist/quote-status-manager/quote-status-state-fsm.cjs +107 -0
- package/dist/quote-status-manager/quote-status-state-fsm.cjs.map +1 -0
- package/dist/quote-status-manager/quote-status-state-fsm.d.cts +57 -0
- package/dist/quote-status-manager/quote-status-state-fsm.d.cts.map +1 -0
- package/dist/quote-status-manager/quote-status-state-fsm.d.mts +57 -0
- package/dist/quote-status-manager/quote-status-state-fsm.d.mts.map +1 -0
- package/dist/quote-status-manager/quote-status-state-fsm.mjs +103 -0
- package/dist/quote-status-manager/quote-status-state-fsm.mjs.map +1 -0
- package/dist/quote-status-manager/quote-status-update-with-retry-outcome.cjs +27 -0
- package/dist/quote-status-manager/quote-status-update-with-retry-outcome.cjs.map +1 -0
- package/dist/quote-status-manager/quote-status-update-with-retry-outcome.d.cts +30 -0
- package/dist/quote-status-manager/quote-status-update-with-retry-outcome.d.cts.map +1 -0
- package/dist/quote-status-manager/quote-status-update-with-retry-outcome.d.mts +30 -0
- package/dist/quote-status-manager/quote-status-update-with-retry-outcome.d.mts.map +1 -0
- package/dist/quote-status-manager/quote-status-update-with-retry-outcome.mjs +23 -0
- package/dist/quote-status-manager/quote-status-update-with-retry-outcome.mjs.map +1 -0
- package/dist/quote-status-manager/quotes-status-manager.cjs +477 -0
- package/dist/quote-status-manager/quotes-status-manager.cjs.map +1 -0
- package/dist/quote-status-manager/quotes-status-manager.d.cts +124 -0
- package/dist/quote-status-manager/quotes-status-manager.d.cts.map +1 -0
- package/dist/quote-status-manager/quotes-status-manager.d.mts +124 -0
- package/dist/quote-status-manager/quotes-status-manager.d.mts.map +1 -0
- package/dist/quote-status-manager/quotes-status-manager.mjs +473 -0
- package/dist/quote-status-manager/quotes-status-manager.mjs.map +1 -0
- package/dist/quote-status-manager/types.cjs +8 -0
- package/dist/quote-status-manager/types.cjs.map +1 -0
- package/dist/quote-status-manager/types.d.cts +145 -0
- package/dist/quote-status-manager/types.d.cts.map +1 -0
- package/dist/quote-status-manager/types.d.mts +145 -0
- package/dist/quote-status-manager/types.d.mts.map +1 -0
- package/dist/quote-status-manager/types.mjs +6 -0
- package/dist/quote-status-manager/types.mjs.map +1 -0
- package/dist/quote-status-manager/utils.cjs +11 -0
- package/dist/quote-status-manager/utils.cjs.map +1 -0
- package/dist/quote-status-manager/utils.d.cts +7 -0
- package/dist/quote-status-manager/utils.d.cts.map +1 -0
- package/dist/quote-status-manager/utils.d.mts +7 -0
- package/dist/quote-status-manager/utils.d.mts.map +1 -0
- package/dist/quote-status-manager/utils.mjs +7 -0
- package/dist/quote-status-manager/utils.mjs.map +1 -0
- package/dist/quote-status-manager/validators.cjs +44 -0
- package/dist/quote-status-manager/validators.cjs.map +1 -0
- package/dist/quote-status-manager/validators.d.cts +172 -0
- package/dist/quote-status-manager/validators.d.cts.map +1 -0
- package/dist/quote-status-manager/validators.d.mts +172 -0
- package/dist/quote-status-manager/validators.d.mts.map +1 -0
- package/dist/quote-status-manager/validators.mjs +39 -0
- package/dist/quote-status-manager/validators.mjs.map +1 -0
- package/dist/types.cjs +2 -0
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +15 -3
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.mts +15 -3
- package/dist/types.d.mts.map +1 -1
- package/dist/types.mjs +2 -0
- package/dist/types.mjs.map +1 -1
- package/dist/utils/history.cjs +1 -0
- package/dist/utils/history.cjs.map +1 -1
- package/dist/utils/history.d.cts.map +1 -1
- package/dist/utils/history.d.mts.map +1 -1
- package/dist/utils/history.mjs +1 -0
- package/dist/utils/history.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
var _QuoteStatusApiService_messenger, _QuoteStatusApiService_clientId, _QuoteStatusApiService_clientProduct, _QuoteStatusApiService_clientVersion, _QuoteStatusApiService_apiBaseUrl, _QuoteStatusApiService_onError;
|
|
13
|
+
import { getClientHeaders } from "@metamask/bridge-controller";
|
|
14
|
+
import { StructError } from "@metamask/superstruct";
|
|
15
|
+
import { BridgeClientId } from "../types.mjs";
|
|
16
|
+
import { getJwt } from "../utils/authentication.mjs";
|
|
17
|
+
import { QuoteStatusBackendStatus, QuoteStatusUpdateRetryableBackendTypes, QuoteStatusFetchWithRetryOutcomeType } from "./constants.mjs";
|
|
18
|
+
import { QuoteStatusGetError, QuoteStatusUpdateError } from "./errors.mjs";
|
|
19
|
+
import { QuoteStatusGetWithRetryOutcome } from "./quote-status-get-with-retry-outcome.mjs";
|
|
20
|
+
import { QuoteStatusUpdateWithRetryOutcome } from "./quote-status-update-with-retry-outcome.mjs";
|
|
21
|
+
import { sleep } from "./utils.mjs";
|
|
22
|
+
import { validateQuoteStatusGetResponse, validateQuoteStatusUpdateResponse } from "./validators.mjs";
|
|
23
|
+
/**
|
|
24
|
+
* Service responsible for calling bridge quote status update APIs.
|
|
25
|
+
*
|
|
26
|
+
* It performs authentication, sends properly-scoped client headers, and validates
|
|
27
|
+
* error responses so callers can handle known update-status failures.
|
|
28
|
+
*/
|
|
29
|
+
export class QuoteStatusApiService {
|
|
30
|
+
/**
|
|
31
|
+
* Creates an API service for quote status update requests.
|
|
32
|
+
*
|
|
33
|
+
* @param options - Service dependencies and request configuration.
|
|
34
|
+
* @param options.messenger - Messenger used to retrieve the authentication token.
|
|
35
|
+
* @param options.clientId - Bridge client identifier used for request headers.
|
|
36
|
+
* @param options.clientProduct - Product name sent in client product headers.
|
|
37
|
+
* @param options.clientVersion - Optional client version sent in headers.
|
|
38
|
+
* @param options.apiBaseUrl - Base URL for the quote status API.
|
|
39
|
+
* @param options.onError - Optional callback for unexpected response-shape errors.
|
|
40
|
+
*/
|
|
41
|
+
constructor({ messenger, clientId, clientProduct, clientVersion, apiBaseUrl, onError, }) {
|
|
42
|
+
_QuoteStatusApiService_messenger.set(this, void 0);
|
|
43
|
+
_QuoteStatusApiService_clientId.set(this, void 0);
|
|
44
|
+
_QuoteStatusApiService_clientProduct.set(this, void 0);
|
|
45
|
+
_QuoteStatusApiService_clientVersion.set(this, void 0);
|
|
46
|
+
_QuoteStatusApiService_apiBaseUrl.set(this, void 0);
|
|
47
|
+
_QuoteStatusApiService_onError.set(this, void 0);
|
|
48
|
+
__classPrivateFieldSet(this, _QuoteStatusApiService_messenger, messenger, "f");
|
|
49
|
+
__classPrivateFieldSet(this, _QuoteStatusApiService_clientId, clientId, "f");
|
|
50
|
+
__classPrivateFieldSet(this, _QuoteStatusApiService_clientProduct, clientProduct, "f");
|
|
51
|
+
__classPrivateFieldSet(this, _QuoteStatusApiService_clientVersion, clientVersion, "f");
|
|
52
|
+
__classPrivateFieldSet(this, _QuoteStatusApiService_apiBaseUrl, apiBaseUrl, "f");
|
|
53
|
+
__classPrivateFieldSet(this, _QuoteStatusApiService_onError, onError, "f");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Updates a quote status in the bridge quote-status API.
|
|
57
|
+
*
|
|
58
|
+
* The endpoint returns no payload on success (`2xx`) and a structured payload
|
|
59
|
+
* on non-`2xx` responses. This method returns `null` for successful updates and
|
|
60
|
+
* the validated error payload for unsuccessful updates.
|
|
61
|
+
*
|
|
62
|
+
* @param data - Request payload identifying quote and target status transition.
|
|
63
|
+
* @param data.quoteId - Unique quote identifier to update.
|
|
64
|
+
* @param data.srcTxHash - Source transaction hash associated with the quote.
|
|
65
|
+
* @param data.newStatus - Target quote status to persist.
|
|
66
|
+
* @param signal - Optional abort signal for canceling the request.
|
|
67
|
+
* @returns `null` for `2xx` responses, or a validated error response for non-`2xx`.
|
|
68
|
+
* @throws If the non-`2xx` response body does not match the expected schema.
|
|
69
|
+
*/
|
|
70
|
+
async updateQuoteStatus(data, signal) {
|
|
71
|
+
const jwt = await getJwt(__classPrivateFieldGet(this, _QuoteStatusApiService_messenger, "f"));
|
|
72
|
+
// This method uses `globalThis.fetch` and reads the raw
|
|
73
|
+
// `Response` (including JSON on non-2xx). Wrappers like `handleFetch` that
|
|
74
|
+
// throw on non-2xx would prevent typed error handling in callers.
|
|
75
|
+
const res = await globalThis.fetch(`${__classPrivateFieldGet(this, _QuoteStatusApiService_apiBaseUrl, "f")}/quote/updateStatus`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
'x-metamask-clientproduct': __classPrivateFieldGet(this, _QuoteStatusApiService_clientProduct, "f"),
|
|
80
|
+
...(__classPrivateFieldGet(this, _QuoteStatusApiService_clientVersion, "f")
|
|
81
|
+
? { 'x-metamask-clientversion': __classPrivateFieldGet(this, _QuoteStatusApiService_clientVersion, "f") }
|
|
82
|
+
: {}),
|
|
83
|
+
...getClientHeaders({
|
|
84
|
+
clientId: __classPrivateFieldGet(this, _QuoteStatusApiService_clientId, "f"),
|
|
85
|
+
jwt,
|
|
86
|
+
}),
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify(data),
|
|
89
|
+
signal,
|
|
90
|
+
});
|
|
91
|
+
if (res.ok) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const responseData = await res.json();
|
|
95
|
+
try {
|
|
96
|
+
validateQuoteStatusUpdateResponse(responseData);
|
|
97
|
+
return responseData;
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
__classPrivateFieldGet(this, _QuoteStatusApiService_onError, "f")?.call(this, new QuoteStatusUpdateError('unexpected response shape from quote/updateStatus', { quoteId: data.quoteId }));
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async getQuoteStatus(data, signal) {
|
|
105
|
+
const jwt = await getJwt(__classPrivateFieldGet(this, _QuoteStatusApiService_messenger, "f"));
|
|
106
|
+
// This method uses `globalThis.fetch` and reads the raw
|
|
107
|
+
// `Response` (including JSON on non-2xx). Wrappers like `handleFetch` that
|
|
108
|
+
// throw on non-2xx would prevent typed error handling in callers.
|
|
109
|
+
const res = await globalThis.fetch(`${__classPrivateFieldGet(this, _QuoteStatusApiService_apiBaseUrl, "f")}/getQuoteStatus?quoteId=${data.quoteId}`, {
|
|
110
|
+
method: 'GET',
|
|
111
|
+
headers: {
|
|
112
|
+
'Content-Type': 'application/json',
|
|
113
|
+
'x-metamask-clientproduct': __classPrivateFieldGet(this, _QuoteStatusApiService_clientProduct, "f"),
|
|
114
|
+
...(__classPrivateFieldGet(this, _QuoteStatusApiService_clientVersion, "f")
|
|
115
|
+
? { 'x-metamask-clientversion': __classPrivateFieldGet(this, _QuoteStatusApiService_clientVersion, "f") }
|
|
116
|
+
: {}),
|
|
117
|
+
...getClientHeaders({
|
|
118
|
+
clientId: __classPrivateFieldGet(this, _QuoteStatusApiService_clientId, "f"),
|
|
119
|
+
jwt,
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
signal,
|
|
123
|
+
});
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
// 5xx errors are transient (server-side); 4xx errors are client-side and
|
|
126
|
+
// non-retryable. The `retryable` flag lets `getQuoteStatusWithRetry`
|
|
127
|
+
// distinguish the two and only exit early for permanent failures.
|
|
128
|
+
const retryable = res.status >= 500;
|
|
129
|
+
const error = new QuoteStatusGetError(`request error to getQuoteStatus [${res.status}: ${res.statusText}]`, { quoteId: data.quoteId }, retryable);
|
|
130
|
+
__classPrivateFieldGet(this, _QuoteStatusApiService_onError, "f")?.call(this, error);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
const responseData = await res.json();
|
|
134
|
+
try {
|
|
135
|
+
validateQuoteStatusGetResponse(responseData);
|
|
136
|
+
return responseData;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error instanceof StructError) {
|
|
140
|
+
const validationFailures = [];
|
|
141
|
+
for (const { path } of error.failures()) {
|
|
142
|
+
const aggregatorId = responseData?.submittedTx?.bridge ??
|
|
143
|
+
'unknown';
|
|
144
|
+
const pathString = path?.join('.') || 'unknown';
|
|
145
|
+
validationFailures.push([aggregatorId, pathString].join('|'));
|
|
146
|
+
}
|
|
147
|
+
const validationError = new QuoteStatusGetError('unexpected response shape from getQuoteStatus', { quoteId: data.quoteId, validationFailures });
|
|
148
|
+
__classPrivateFieldGet(this, _QuoteStatusApiService_onError, "f")?.call(this, validationError);
|
|
149
|
+
throw validationError;
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Updates a quote status, retrying on transient backend failures.
|
|
156
|
+
*
|
|
157
|
+
* Wraps {@link updateQuoteStatus} in a bounded retry loop. A request is only
|
|
158
|
+
* retried when the backend returns an error whose type is in
|
|
159
|
+
* {@link QuoteStatusUpdateRetryableBackendTypes}; any other error response
|
|
160
|
+
* resolves immediately as non-retryable. Retries are spaced by
|
|
161
|
+
* `options.delayMsBetweenRetries`, and both the abort signal and the optional
|
|
162
|
+
* `options.retry` predicate are checked before each attempt so an
|
|
163
|
+
* in-flight or pending retry can be cancelled (e.g. when the entry's status
|
|
164
|
+
* changed while sleeping between retries).
|
|
165
|
+
*
|
|
166
|
+
* @param data - Request payload identifying the quote and target transition.
|
|
167
|
+
* @param data.quoteId - Unique quote identifier to update.
|
|
168
|
+
* @param data.srcTxHash - Source transaction hash associated with the quote.
|
|
169
|
+
* @param data.newStatus - Target quote status to persist.
|
|
170
|
+
* @param options - Retry configuration.
|
|
171
|
+
* @param options.maxRetries - Maximum number of retries after the initial attempt.
|
|
172
|
+
* @param options.delayMsBetweenRetries - Delay in milliseconds between attempts.
|
|
173
|
+
* @param options.retry - Optional predicate checked before each attempt;
|
|
174
|
+
* when it returns `false` the loop stops early and resolves as `Interrupted`.
|
|
175
|
+
* @param signal - Optional abort signal for canceling the request and its retries.
|
|
176
|
+
* @returns An outcome describing how the update resolved:
|
|
177
|
+
* `Accepted` when the backend accepted the update, `NonRetryable` for a
|
|
178
|
+
* non-retryable error response, `Interrupted` when aborted, or
|
|
179
|
+
* `RetryableExhausted` when all retries were used up on retryable errors.
|
|
180
|
+
* @throws If a request rejects for a reason other than the abort signal (e.g.
|
|
181
|
+
* an unexpected response shape from {@link updateQuoteStatus}).
|
|
182
|
+
*/
|
|
183
|
+
async updateQuoteStatusWithRetry(data, options, signal) {
|
|
184
|
+
for (let attempt = 0; attempt <= options.maxRetries; attempt += 1) {
|
|
185
|
+
if (attempt > 0) {
|
|
186
|
+
await sleep(options.delayMsBetweenRetries);
|
|
187
|
+
}
|
|
188
|
+
if (signal?.aborted || options.retry?.() === false) {
|
|
189
|
+
return new QuoteStatusUpdateWithRetryOutcome(QuoteStatusFetchWithRetryOutcomeType.Interrupted);
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const response = await this.updateQuoteStatus(data, signal);
|
|
193
|
+
if (response === null) {
|
|
194
|
+
return new QuoteStatusUpdateWithRetryOutcome(QuoteStatusFetchWithRetryOutcomeType.Accepted);
|
|
195
|
+
}
|
|
196
|
+
if (!QuoteStatusUpdateRetryableBackendTypes.includes(response.type)) {
|
|
197
|
+
return new QuoteStatusUpdateWithRetryOutcome(QuoteStatusFetchWithRetryOutcomeType.NonRetryable, response);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (signal?.aborted) {
|
|
202
|
+
return new QuoteStatusUpdateWithRetryOutcome(QuoteStatusFetchWithRetryOutcomeType.Interrupted);
|
|
203
|
+
}
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return new QuoteStatusUpdateWithRetryOutcome(QuoteStatusFetchWithRetryOutcomeType.RetryableExhausted);
|
|
208
|
+
}
|
|
209
|
+
async getQuoteStatusWithRetry(data, options, signal) {
|
|
210
|
+
for (let attempt = 0; attempt <= options.maxRetries; attempt += 1) {
|
|
211
|
+
if (attempt > 0) {
|
|
212
|
+
await sleep(options.delayMsBetweenRetries);
|
|
213
|
+
}
|
|
214
|
+
if (signal?.aborted) {
|
|
215
|
+
return new QuoteStatusGetWithRetryOutcome(QuoteStatusFetchWithRetryOutcomeType.Interrupted);
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const response = await this.getQuoteStatus(data, signal);
|
|
219
|
+
return new QuoteStatusGetWithRetryOutcome(QuoteStatusFetchWithRetryOutcomeType.Accepted, response);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (signal?.aborted) {
|
|
223
|
+
return new QuoteStatusGetWithRetryOutcome(QuoteStatusFetchWithRetryOutcomeType.Interrupted);
|
|
224
|
+
}
|
|
225
|
+
// Only short-circuit for non-retryable errors (e.g. 4xx, validation
|
|
226
|
+
// failures). Retryable errors (e.g. 5xx) fall through so the loop can
|
|
227
|
+
// attempt the next retry instead of returning NonRetryable immediately.
|
|
228
|
+
if (error instanceof QuoteStatusGetError && !error.retryable) {
|
|
229
|
+
return new QuoteStatusGetWithRetryOutcome(QuoteStatusFetchWithRetryOutcomeType.NonRetryable, undefined, error);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return new QuoteStatusGetWithRetryOutcome(QuoteStatusFetchWithRetryOutcomeType.RetryableExhausted);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
_QuoteStatusApiService_messenger = new WeakMap(), _QuoteStatusApiService_clientId = new WeakMap(), _QuoteStatusApiService_clientProduct = new WeakMap(), _QuoteStatusApiService_clientVersion = new WeakMap(), _QuoteStatusApiService_apiBaseUrl = new WeakMap(), _QuoteStatusApiService_onError = new WeakMap();
|
|
237
|
+
//# sourceMappingURL=quote-status-api-service.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-status-api-service.mjs","sourceRoot":"","sources":["../../src/quote-status-manager/quote-status-api-service.ts"],"names":[],"mappings":"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","sourcesContent":["import { getClientHeaders } from '@metamask/bridge-controller';\nimport { StructError } from '@metamask/superstruct';\n\nimport { BridgeClientId, BridgeStatusControllerMessenger } from '../types';\nimport { getJwt } from '../utils/authentication';\nimport {\n QuoteStatusBackendStatus,\n QuoteStatusUpdateRetryableBackendTypes,\n QuoteStatusFetchWithRetryOutcomeType,\n} from './constants';\nimport { QuoteStatusGetError, QuoteStatusUpdateError } from './errors';\nimport { QuoteStatusGetWithRetryOutcome } from './quote-status-get-with-retry-outcome';\nimport { QuoteStatusUpdateWithRetryOutcome } from './quote-status-update-with-retry-outcome';\nimport {\n QuoteStatusApiServiceOptions,\n QuoteStatusGetResponse,\n QuoteStatusUpdateResponse,\n} from './types';\nimport { sleep } from './utils';\nimport {\n validateQuoteStatusGetResponse,\n validateQuoteStatusUpdateResponse,\n} from './validators';\n\n/**\n * Service responsible for calling bridge quote status update APIs.\n *\n * It performs authentication, sends properly-scoped client headers, and validates\n * error responses so callers can handle known update-status failures.\n */\nexport class QuoteStatusApiService {\n readonly #messenger: BridgeStatusControllerMessenger;\n\n readonly #clientId: BridgeClientId;\n\n readonly #clientProduct: string;\n\n readonly #clientVersion: string | undefined;\n\n readonly #apiBaseUrl: string;\n\n readonly #onError: ((error: QuoteStatusUpdateError) => void) | undefined;\n\n /**\n * Creates an API service for quote status update requests.\n *\n * @param options - Service dependencies and request configuration.\n * @param options.messenger - Messenger used to retrieve the authentication token.\n * @param options.clientId - Bridge client identifier used for request headers.\n * @param options.clientProduct - Product name sent in client product headers.\n * @param options.clientVersion - Optional client version sent in headers.\n * @param options.apiBaseUrl - Base URL for the quote status API.\n * @param options.onError - Optional callback for unexpected response-shape errors.\n */\n constructor({\n messenger,\n clientId,\n clientProduct,\n clientVersion,\n apiBaseUrl,\n onError,\n }: QuoteStatusApiServiceOptions) {\n this.#messenger = messenger;\n this.#clientId = clientId;\n this.#clientProduct = clientProduct;\n this.#clientVersion = clientVersion;\n this.#apiBaseUrl = apiBaseUrl;\n this.#onError = onError;\n }\n\n /**\n * Updates a quote status in the bridge quote-status API.\n *\n * The endpoint returns no payload on success (`2xx`) and a structured payload\n * on non-`2xx` responses. This method returns `null` for successful updates and\n * the validated error payload for unsuccessful updates.\n *\n * @param data - Request payload identifying quote and target status transition.\n * @param data.quoteId - Unique quote identifier to update.\n * @param data.srcTxHash - Source transaction hash associated with the quote.\n * @param data.newStatus - Target quote status to persist.\n * @param signal - Optional abort signal for canceling the request.\n * @returns `null` for `2xx` responses, or a validated error response for non-`2xx`.\n * @throws If the non-`2xx` response body does not match the expected schema.\n */\n async updateQuoteStatus(\n data: {\n quoteId: string;\n srcTxHash: string;\n newStatus: QuoteStatusBackendStatus;\n },\n signal?: AbortSignal,\n ): Promise<QuoteStatusUpdateResponse | null> {\n const jwt = await getJwt(this.#messenger);\n\n // This method uses `globalThis.fetch` and reads the raw\n // `Response` (including JSON on non-2xx). Wrappers like `handleFetch` that\n // throw on non-2xx would prevent typed error handling in callers.\n const res = await globalThis.fetch(\n `${this.#apiBaseUrl}/quote/updateStatus`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-metamask-clientproduct': this.#clientProduct,\n ...(this.#clientVersion\n ? { 'x-metamask-clientversion': this.#clientVersion }\n : {}),\n ...getClientHeaders({\n clientId: this.#clientId,\n jwt,\n }),\n },\n body: JSON.stringify(data),\n signal,\n },\n );\n\n if (res.ok) {\n return null;\n }\n\n const responseData = await res.json();\n\n try {\n validateQuoteStatusUpdateResponse(responseData);\n return responseData;\n } catch (error) {\n this.#onError?.(\n new QuoteStatusUpdateError(\n 'unexpected response shape from quote/updateStatus',\n { quoteId: data.quoteId },\n ),\n );\n throw error;\n }\n }\n\n async getQuoteStatus(\n data: {\n quoteId: string;\n },\n signal?: AbortSignal,\n ): Promise<QuoteStatusGetResponse> {\n const jwt = await getJwt(this.#messenger);\n\n // This method uses `globalThis.fetch` and reads the raw\n // `Response` (including JSON on non-2xx). Wrappers like `handleFetch` that\n // throw on non-2xx would prevent typed error handling in callers.\n const res = await globalThis.fetch(\n `${this.#apiBaseUrl}/getQuoteStatus?quoteId=${data.quoteId}`,\n {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n 'x-metamask-clientproduct': this.#clientProduct,\n ...(this.#clientVersion\n ? { 'x-metamask-clientversion': this.#clientVersion }\n : {}),\n ...getClientHeaders({\n clientId: this.#clientId,\n jwt,\n }),\n },\n signal,\n },\n );\n\n if (!res.ok) {\n // 5xx errors are transient (server-side); 4xx errors are client-side and\n // non-retryable. The `retryable` flag lets `getQuoteStatusWithRetry`\n // distinguish the two and only exit early for permanent failures.\n const retryable = res.status >= 500;\n const error = new QuoteStatusGetError(\n `request error to getQuoteStatus [${res.status}: ${res.statusText}]`,\n { quoteId: data.quoteId },\n retryable,\n );\n this.#onError?.(error);\n throw error;\n }\n\n const responseData = await res.json();\n\n try {\n validateQuoteStatusGetResponse(responseData);\n return responseData;\n } catch (error) {\n if (error instanceof StructError) {\n const validationFailures = [];\n\n for (const { path } of error.failures()) {\n const aggregatorId =\n (responseData as QuoteStatusGetResponse)?.submittedTx?.bridge ??\n ('unknown' as string);\n const pathString = path?.join('.') || 'unknown';\n validationFailures.push([aggregatorId, pathString].join('|'));\n }\n\n const validationError = new QuoteStatusGetError(\n 'unexpected response shape from getQuoteStatus',\n { quoteId: data.quoteId, validationFailures },\n );\n\n this.#onError?.(validationError);\n\n throw validationError;\n }\n\n throw error;\n }\n }\n\n /**\n * Updates a quote status, retrying on transient backend failures.\n *\n * Wraps {@link updateQuoteStatus} in a bounded retry loop. A request is only\n * retried when the backend returns an error whose type is in\n * {@link QuoteStatusUpdateRetryableBackendTypes}; any other error response\n * resolves immediately as non-retryable. Retries are spaced by\n * `options.delayMsBetweenRetries`, and both the abort signal and the optional\n * `options.retry` predicate are checked before each attempt so an\n * in-flight or pending retry can be cancelled (e.g. when the entry's status\n * changed while sleeping between retries).\n *\n * @param data - Request payload identifying the quote and target transition.\n * @param data.quoteId - Unique quote identifier to update.\n * @param data.srcTxHash - Source transaction hash associated with the quote.\n * @param data.newStatus - Target quote status to persist.\n * @param options - Retry configuration.\n * @param options.maxRetries - Maximum number of retries after the initial attempt.\n * @param options.delayMsBetweenRetries - Delay in milliseconds between attempts.\n * @param options.retry - Optional predicate checked before each attempt;\n * when it returns `false` the loop stops early and resolves as `Interrupted`.\n * @param signal - Optional abort signal for canceling the request and its retries.\n * @returns An outcome describing how the update resolved:\n * `Accepted` when the backend accepted the update, `NonRetryable` for a\n * non-retryable error response, `Interrupted` when aborted, or\n * `RetryableExhausted` when all retries were used up on retryable errors.\n * @throws If a request rejects for a reason other than the abort signal (e.g.\n * an unexpected response shape from {@link updateQuoteStatus}).\n */\n async updateQuoteStatusWithRetry(\n data: {\n quoteId: string;\n srcTxHash: string;\n newStatus: QuoteStatusBackendStatus;\n },\n options: {\n maxRetries: number;\n delayMsBetweenRetries: number;\n retry?: () => boolean;\n },\n signal?: AbortSignal,\n ): Promise<QuoteStatusUpdateWithRetryOutcome> {\n for (let attempt = 0; attempt <= options.maxRetries; attempt += 1) {\n if (attempt > 0) {\n await sleep(options.delayMsBetweenRetries);\n }\n\n if (signal?.aborted || options.retry?.() === false) {\n return new QuoteStatusUpdateWithRetryOutcome(\n QuoteStatusFetchWithRetryOutcomeType.Interrupted,\n );\n }\n\n try {\n const response = await this.updateQuoteStatus(data, signal);\n\n if (response === null) {\n return new QuoteStatusUpdateWithRetryOutcome(\n QuoteStatusFetchWithRetryOutcomeType.Accepted,\n );\n }\n\n if (!QuoteStatusUpdateRetryableBackendTypes.includes(response.type)) {\n return new QuoteStatusUpdateWithRetryOutcome(\n QuoteStatusFetchWithRetryOutcomeType.NonRetryable,\n response,\n );\n }\n } catch (error) {\n if (signal?.aborted) {\n return new QuoteStatusUpdateWithRetryOutcome(\n QuoteStatusFetchWithRetryOutcomeType.Interrupted,\n );\n }\n\n throw error;\n }\n }\n\n return new QuoteStatusUpdateWithRetryOutcome(\n QuoteStatusFetchWithRetryOutcomeType.RetryableExhausted,\n );\n }\n\n async getQuoteStatusWithRetry(\n data: {\n quoteId: string;\n },\n options: {\n maxRetries: number;\n delayMsBetweenRetries: number;\n },\n signal?: AbortSignal,\n ): Promise<QuoteStatusGetWithRetryOutcome> {\n for (let attempt = 0; attempt <= options.maxRetries; attempt += 1) {\n if (attempt > 0) {\n await sleep(options.delayMsBetweenRetries);\n }\n\n if (signal?.aborted) {\n return new QuoteStatusGetWithRetryOutcome(\n QuoteStatusFetchWithRetryOutcomeType.Interrupted,\n );\n }\n\n try {\n const response = await this.getQuoteStatus(data, signal);\n\n return new QuoteStatusGetWithRetryOutcome(\n QuoteStatusFetchWithRetryOutcomeType.Accepted,\n response,\n );\n } catch (error) {\n if (signal?.aborted) {\n return new QuoteStatusGetWithRetryOutcome(\n QuoteStatusFetchWithRetryOutcomeType.Interrupted,\n );\n }\n\n // Only short-circuit for non-retryable errors (e.g. 4xx, validation\n // failures). Retryable errors (e.g. 5xx) fall through so the loop can\n // attempt the next retry instead of returning NonRetryable immediately.\n if (error instanceof QuoteStatusGetError && !error.retryable) {\n return new QuoteStatusGetWithRetryOutcome(\n QuoteStatusFetchWithRetryOutcomeType.NonRetryable,\n undefined,\n error,\n );\n }\n }\n }\n\n return new QuoteStatusGetWithRetryOutcome(\n QuoteStatusFetchWithRetryOutcomeType.RetryableExhausted,\n );\n }\n}\n"]}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
3
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
4
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
5
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
6
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
7
|
+
};
|
|
8
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
9
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
10
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
11
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
12
|
+
};
|
|
13
|
+
var _QuoteStatusEntryStore_instances, _QuoteStatusEntryStore_items, _QuoteStatusEntryStore_entryTtlMs, _QuoteStatusEntryStore_onPersistUpdates, _QuoteStatusEntryStore_subscribeToStatusUpdates, _QuoteStatusEntryStore_persistToState;
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.QuoteStatusEntryStore = void 0;
|
|
16
|
+
const constants_1 = require("./constants.cjs");
|
|
17
|
+
const quote_status_state_fsm_1 = require("./quote-status-state-fsm.cjs");
|
|
18
|
+
/**
|
|
19
|
+
* In-memory store for quote status update entries.
|
|
20
|
+
*
|
|
21
|
+
* The store deduplicates entries by key, tracks timestamps used for retries and
|
|
22
|
+
* TTL eviction, and persists a cloned snapshot on each mutating operation.
|
|
23
|
+
*/
|
|
24
|
+
class QuoteStatusEntryStore {
|
|
25
|
+
/**
|
|
26
|
+
* Creates a deterministic key for persisted quote status entries.
|
|
27
|
+
*
|
|
28
|
+
* @param entry - Entry identity fields.
|
|
29
|
+
* @param entry.quoteId - Quote identifier.
|
|
30
|
+
* @param entry.srcTxHash - Source transaction hash.
|
|
31
|
+
* @returns Stable key in `${quoteId}:${srcTxHash}` format.
|
|
32
|
+
*/
|
|
33
|
+
static hash(entry) {
|
|
34
|
+
return `${entry.quoteId}:${entry.srcTxHash}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates a quote status entry store.
|
|
38
|
+
*
|
|
39
|
+
* @param options - Store dependencies and retention configuration.
|
|
40
|
+
* @param options.onPersistUpdates - Callback invoked with cloned snapshot updates.
|
|
41
|
+
* @param options.entryTtlMs - Entry time-to-live in milliseconds.
|
|
42
|
+
* @param options.initial - Optional initial persisted entries used to seed the store.
|
|
43
|
+
*/
|
|
44
|
+
constructor({ onPersistUpdates, entryTtlMs, initial, }) {
|
|
45
|
+
_QuoteStatusEntryStore_instances.add(this);
|
|
46
|
+
_QuoteStatusEntryStore_items.set(this, void 0);
|
|
47
|
+
_QuoteStatusEntryStore_entryTtlMs.set(this, void 0);
|
|
48
|
+
_QuoteStatusEntryStore_onPersistUpdates.set(this, void 0);
|
|
49
|
+
__classPrivateFieldSet(this, _QuoteStatusEntryStore_onPersistUpdates, onPersistUpdates, "f");
|
|
50
|
+
__classPrivateFieldSet(this, _QuoteStatusEntryStore_entryTtlMs, entryTtlMs, "f");
|
|
51
|
+
__classPrivateFieldSet(this, _QuoteStatusEntryStore_items, new Map(Object.entries(initial ?? {}).map(([key, entry]) => {
|
|
52
|
+
const quoteStatusStateFsm = new quote_status_state_fsm_1.QuoteStatusStateFsm(entry.status);
|
|
53
|
+
return [
|
|
54
|
+
key,
|
|
55
|
+
// Entries from `initialDeferredUpdates` come from Immer-managed controller
|
|
56
|
+
// state, which deep-freezes all nested objects. Cloning each entry here
|
|
57
|
+
// ensures the in-memory queue holds mutable objects so that mutations
|
|
58
|
+
// (e.g. updating `status` or `lastAttemptAt`) work correctly without
|
|
59
|
+
// throwing a "read only property" error.
|
|
60
|
+
{ ...entry, status: quoteStatusStateFsm },
|
|
61
|
+
];
|
|
62
|
+
})), "f");
|
|
63
|
+
// Subscribe each seeded FSM after the map (and `#onPersistUpdates`) are set
|
|
64
|
+
// so that any later state transition persists the updated snapshot.
|
|
65
|
+
for (const entry of __classPrivateFieldGet(this, _QuoteStatusEntryStore_items, "f").values()) {
|
|
66
|
+
__classPrivateFieldGet(this, _QuoteStatusEntryStore_instances, "m", _QuoteStatusEntryStore_subscribeToStatusUpdates).call(this, entry);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Adds a new entry when the key is not already present.
|
|
71
|
+
*
|
|
72
|
+
* If an entry already exists, only a missing `txMetaId` is backfilled to avoid
|
|
73
|
+
* resetting timestamps and creating duplicate submission updates.
|
|
74
|
+
*
|
|
75
|
+
* @param key - Unique map key for the entry.
|
|
76
|
+
* @param value - Entry payload without internal timestamps.
|
|
77
|
+
* @param value.quoteId - Quote identifier.
|
|
78
|
+
* @param value.srcTxHash - Source transaction hash.
|
|
79
|
+
* @param value.status - Latest quote status.
|
|
80
|
+
* @param value.txMetaId - Optional transaction metadata identifier.
|
|
81
|
+
* @returns The tracked entry (the existing one when the key is already
|
|
82
|
+
* present, otherwise the newly created entry).
|
|
83
|
+
*/
|
|
84
|
+
put(key, value) {
|
|
85
|
+
// If an entry for this key is already in the queue
|
|
86
|
+
// do not overwrite it. Re-enqueueing would reset state and
|
|
87
|
+
// could cause duplicate SUBMITTED events.
|
|
88
|
+
const existing = __classPrivateFieldGet(this, _QuoteStatusEntryStore_items, "f").get(key);
|
|
89
|
+
if (existing) {
|
|
90
|
+
if (!existing.txMetaId && value.txMetaId) {
|
|
91
|
+
existing.txMetaId = value.txMetaId;
|
|
92
|
+
__classPrivateFieldGet(this, _QuoteStatusEntryStore_instances, "m", _QuoteStatusEntryStore_persistToState).call(this);
|
|
93
|
+
}
|
|
94
|
+
return existing;
|
|
95
|
+
}
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
const entry = {
|
|
98
|
+
...value,
|
|
99
|
+
createdAt: now,
|
|
100
|
+
lastAttemptAt: now,
|
|
101
|
+
};
|
|
102
|
+
__classPrivateFieldGet(this, _QuoteStatusEntryStore_items, "f").set(key, entry);
|
|
103
|
+
__classPrivateFieldGet(this, _QuoteStatusEntryStore_instances, "m", _QuoteStatusEntryStore_subscribeToStatusUpdates).call(this, entry);
|
|
104
|
+
__classPrivateFieldGet(this, _QuoteStatusEntryStore_instances, "m", _QuoteStatusEntryStore_persistToState).call(this);
|
|
105
|
+
return entry;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Returns an iterator over all tracked entries.
|
|
109
|
+
*
|
|
110
|
+
* Stale entries are transitioned to {@link QuoteStatusState.Expired} first (but
|
|
111
|
+
* kept in the store), so the iterator reflects their up-to-date terminal state.
|
|
112
|
+
*
|
|
113
|
+
* @returns An iterator over the currently tracked entries.
|
|
114
|
+
*/
|
|
115
|
+
values() {
|
|
116
|
+
this.expireStaleEntries();
|
|
117
|
+
return __classPrivateFieldGet(this, _QuoteStatusEntryStore_items, "f").values();
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Returns every tracked entry matching the provided quote identifier.
|
|
121
|
+
*
|
|
122
|
+
* Stale entries are transitioned to {@link QuoteStatusState.Expired} first so
|
|
123
|
+
* callers observe their up-to-date terminal state.
|
|
124
|
+
*
|
|
125
|
+
* @param quoteId - Quote identifier to match.
|
|
126
|
+
* @returns The matching entries (empty when none exist).
|
|
127
|
+
*/
|
|
128
|
+
getByQuoteId(quoteId) {
|
|
129
|
+
this.expireStaleEntries();
|
|
130
|
+
const matches = [];
|
|
131
|
+
for (const entry of __classPrivateFieldGet(this, _QuoteStatusEntryStore_items, "f").values()) {
|
|
132
|
+
if (entry.quoteId === quoteId) {
|
|
133
|
+
matches.push(entry);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return matches;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Number of entries currently tracked by the store.
|
|
140
|
+
*
|
|
141
|
+
* Note that this includes terminal entries (`Completed`/`Expired`), which are
|
|
142
|
+
* retained rather than evicted; call {@link expireStaleEntries} (or
|
|
143
|
+
* {@link values}) first when an up-to-date view of entry states is required.
|
|
144
|
+
*
|
|
145
|
+
* @returns The count of tracked entries.
|
|
146
|
+
*/
|
|
147
|
+
get size() {
|
|
148
|
+
return __classPrivateFieldGet(this, _QuoteStatusEntryStore_items, "f").size;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Looks up an entry by its transaction metadata identifier.
|
|
152
|
+
*
|
|
153
|
+
* If the matching entry has exceeded its TTL it is transitioned to
|
|
154
|
+
* {@link QuoteStatusState.Expired} (but kept) before being returned.
|
|
155
|
+
*
|
|
156
|
+
* @param txMetaId - Transaction metadata identifier to search for.
|
|
157
|
+
* @returns The matching entry, or `null` if none exists.
|
|
158
|
+
*/
|
|
159
|
+
getByTxMetaId(txMetaId) {
|
|
160
|
+
for (const entry of __classPrivateFieldGet(this, _QuoteStatusEntryStore_items, "f").values()) {
|
|
161
|
+
if (entry.txMetaId === txMetaId) {
|
|
162
|
+
this.expireEntryIfStale(entry);
|
|
163
|
+
return entry;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Retrieves an entry by key.
|
|
170
|
+
*
|
|
171
|
+
* If the entry has exceeded its TTL it is transitioned to
|
|
172
|
+
* {@link QuoteStatusState.Expired} (but kept) before being returned.
|
|
173
|
+
*
|
|
174
|
+
* @param key - Unique map key for the entry.
|
|
175
|
+
* @returns The matching entry, or `null` if absent.
|
|
176
|
+
*/
|
|
177
|
+
get(key) {
|
|
178
|
+
const entry = __classPrivateFieldGet(this, _QuoteStatusEntryStore_items, "f").get(key);
|
|
179
|
+
if (!entry) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
this.expireEntryIfStale(entry);
|
|
183
|
+
return entry;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Persists the current snapshot after an in-place mutation of a tracked entry.
|
|
187
|
+
*
|
|
188
|
+
* Used when fields such as `lastAttemptAt` are mutated directly on an entry
|
|
189
|
+
* the caller already holds. No-ops if the entry is no longer tracked (e.g. the
|
|
190
|
+
* store was cleared in the meantime), so persistence only reflects live entries.
|
|
191
|
+
*
|
|
192
|
+
* @param entry - The mutated entry to persist.
|
|
193
|
+
*/
|
|
194
|
+
update(entry) {
|
|
195
|
+
if (!__classPrivateFieldGet(this, _QuoteStatusEntryStore_items, "f").has(QuoteStatusEntryStore.hash(entry))) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
__classPrivateFieldGet(this, _QuoteStatusEntryStore_instances, "m", _QuoteStatusEntryStore_persistToState).call(this);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Transitions every stale entry to {@link QuoteStatusState.Expired}, keeping it
|
|
202
|
+
* in the store.
|
|
203
|
+
*
|
|
204
|
+
* Expired entries are retained so that later interactions with the same quote
|
|
205
|
+
* (e.g. a duplicate `reportSubmitted`) can be recognized and rejected. Each
|
|
206
|
+
* transition persists the updated snapshot via the entry's FSM subscription;
|
|
207
|
+
* already-terminal entries cannot transition and are left untouched.
|
|
208
|
+
*/
|
|
209
|
+
expireStaleEntries() {
|
|
210
|
+
for (const entry of __classPrivateFieldGet(this, _QuoteStatusEntryStore_items, "f").values()) {
|
|
211
|
+
if (this.entryHasExpired(entry)) {
|
|
212
|
+
entry.status.transitionTo(constants_1.QuoteStatusState.Expired);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Determines whether an entry has outlived the configured TTL.
|
|
218
|
+
*
|
|
219
|
+
* Expiry is measured from the entry's `createdAt` timestamp, not its last
|
|
220
|
+
* attempt, so an entry's total lifetime is bounded regardless of retries.
|
|
221
|
+
*
|
|
222
|
+
* @param entry - Entry to check.
|
|
223
|
+
* @returns `true` if the entry's age exceeds the TTL.
|
|
224
|
+
*/
|
|
225
|
+
entryHasExpired(entry) {
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
return now - entry.createdAt > __classPrivateFieldGet(this, _QuoteStatusEntryStore_entryTtlMs, "f");
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Transitions an entry to {@link QuoteStatusState.Expired} (keeping it) if it
|
|
231
|
+
* has exceeded its TTL.
|
|
232
|
+
*
|
|
233
|
+
* The transition persists the updated snapshot via the entry's FSM
|
|
234
|
+
* subscription. Already-terminal entries cannot transition and are left
|
|
235
|
+
* untouched.
|
|
236
|
+
*
|
|
237
|
+
* @param entry - Entry to check and possibly expire.
|
|
238
|
+
* @returns `true` if the entry was stale, otherwise `false`.
|
|
239
|
+
*/
|
|
240
|
+
expireEntryIfStale(entry) {
|
|
241
|
+
if (this.entryHasExpired(entry)) {
|
|
242
|
+
entry.status.transitionTo(constants_1.QuoteStatusState.Expired);
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Removes every entry from the store.
|
|
249
|
+
*
|
|
250
|
+
* Detaches each entry's FSM listeners before clearing to prevent leaks and
|
|
251
|
+
* stale persist callbacks. Does not emit a persistence update; callers are
|
|
252
|
+
* expected to use this during teardown.
|
|
253
|
+
*/
|
|
254
|
+
clear() {
|
|
255
|
+
for (const entry of __classPrivateFieldGet(this, _QuoteStatusEntryStore_items, "f").values()) {
|
|
256
|
+
entry.status.removeAllListeners();
|
|
257
|
+
}
|
|
258
|
+
__classPrivateFieldGet(this, _QuoteStatusEntryStore_items, "f").clear();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
exports.QuoteStatusEntryStore = QuoteStatusEntryStore;
|
|
262
|
+
_QuoteStatusEntryStore_items = new WeakMap(), _QuoteStatusEntryStore_entryTtlMs = new WeakMap(), _QuoteStatusEntryStore_onPersistUpdates = new WeakMap(), _QuoteStatusEntryStore_instances = new WeakSet(), _QuoteStatusEntryStore_subscribeToStatusUpdates = function _QuoteStatusEntryStore_subscribeToStatusUpdates(entry) {
|
|
263
|
+
entry.status.onStateUpdate(() => __classPrivateFieldGet(this, _QuoteStatusEntryStore_instances, "m", _QuoteStatusEntryStore_persistToState).call(this));
|
|
264
|
+
}, _QuoteStatusEntryStore_persistToState = function _QuoteStatusEntryStore_persistToState() {
|
|
265
|
+
const cloned = {};
|
|
266
|
+
for (const [key, entry] of __classPrivateFieldGet(this, _QuoteStatusEntryStore_items, "f")) {
|
|
267
|
+
cloned[key] = { ...entry, status: entry.status.state };
|
|
268
|
+
}
|
|
269
|
+
__classPrivateFieldGet(this, _QuoteStatusEntryStore_onPersistUpdates, "f").call(this, cloned);
|
|
270
|
+
};
|
|
271
|
+
//# sourceMappingURL=quote-status-entry-store.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-status-entry-store.cjs","sourceRoot":"","sources":["../../src/quote-status-manager/quote-status-entry-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { QuoteStatusState } from './constants';\nimport { QuoteStatusStateFsm } from './quote-status-state-fsm';\nimport {\n QuoteStatusEntryStoreOptions,\n QuoteStatusPersistEntry,\n QuoteStatusRuntimeEntry,\n} from './types';\n\n/**\n * In-memory store for quote status update entries.\n *\n * The store deduplicates entries by key, tracks timestamps used for retries and\n * TTL eviction, and persists a cloned snapshot on each mutating operation.\n */\nexport class QuoteStatusEntryStore {\n readonly #items: Map<string, QuoteStatusRuntimeEntry>;\n\n readonly #entryTtlMs: number;\n\n /**\n * Creates a deterministic key for persisted quote status entries.\n *\n * @param entry - Entry identity fields.\n * @param entry.quoteId - Quote identifier.\n * @param entry.srcTxHash - Source transaction hash.\n * @returns Stable key in `${quoteId}:${srcTxHash}` format.\n */\n static hash(\n entry: Pick<QuoteStatusPersistEntry, 'quoteId' | 'srcTxHash'>,\n ): string {\n return `${entry.quoteId}:${entry.srcTxHash}`;\n }\n\n readonly #onPersistUpdates: (\n updates: Record<string, QuoteStatusPersistEntry>,\n ) => void;\n\n /**\n * Creates a quote status entry store.\n *\n * @param options - Store dependencies and retention configuration.\n * @param options.onPersistUpdates - Callback invoked with cloned snapshot updates.\n * @param options.entryTtlMs - Entry time-to-live in milliseconds.\n * @param options.initial - Optional initial persisted entries used to seed the store.\n */\n constructor({\n onPersistUpdates,\n entryTtlMs,\n initial,\n }: QuoteStatusEntryStoreOptions) {\n this.#onPersistUpdates = onPersistUpdates;\n this.#entryTtlMs = entryTtlMs;\n this.#items = new Map(\n Object.entries(initial ?? {}).map(([key, entry]) => {\n const quoteStatusStateFsm = new QuoteStatusStateFsm(entry.status);\n\n return [\n key,\n // Entries from `initialDeferredUpdates` come from Immer-managed controller\n // state, which deep-freezes all nested objects. Cloning each entry here\n // ensures the in-memory queue holds mutable objects so that mutations\n // (e.g. updating `status` or `lastAttemptAt`) work correctly without\n // throwing a \"read only property\" error.\n { ...entry, status: quoteStatusStateFsm },\n ];\n }),\n );\n\n // Subscribe each seeded FSM after the map (and `#onPersistUpdates`) are set\n // so that any later state transition persists the updated snapshot.\n for (const entry of this.#items.values()) {\n this.#subscribeToStatusUpdates(entry);\n }\n }\n\n /**\n * Subscribes the entry's FSM so that every state transition persists the\n * current snapshot. Kept in one place so seeded and runtime-added entries\n * behave identically.\n *\n * @param entry - Entry whose FSM should trigger persistence on transition.\n */\n #subscribeToStatusUpdates(entry: QuoteStatusRuntimeEntry): void {\n entry.status.onStateUpdate(() => this.#persistToState());\n }\n\n /**\n * Adds a new entry when the key is not already present.\n *\n * If an entry already exists, only a missing `txMetaId` is backfilled to avoid\n * resetting timestamps and creating duplicate submission updates.\n *\n * @param key - Unique map key for the entry.\n * @param value - Entry payload without internal timestamps.\n * @param value.quoteId - Quote identifier.\n * @param value.srcTxHash - Source transaction hash.\n * @param value.status - Latest quote status.\n * @param value.txMetaId - Optional transaction metadata identifier.\n * @returns The tracked entry (the existing one when the key is already\n * present, otherwise the newly created entry).\n */\n put(\n key: string,\n value: Omit<QuoteStatusRuntimeEntry, 'createdAt' | 'lastAttemptAt'>,\n ): QuoteStatusRuntimeEntry {\n // If an entry for this key is already in the queue\n // do not overwrite it. Re-enqueueing would reset state and\n // could cause duplicate SUBMITTED events.\n const existing = this.#items.get(key);\n if (existing) {\n if (!existing.txMetaId && value.txMetaId) {\n existing.txMetaId = value.txMetaId;\n this.#persistToState();\n }\n return existing;\n }\n\n const now = Date.now();\n const entry: QuoteStatusRuntimeEntry = {\n ...value,\n createdAt: now,\n lastAttemptAt: now,\n };\n this.#items.set(key, entry);\n this.#subscribeToStatusUpdates(entry);\n this.#persistToState();\n return entry;\n }\n\n /**\n * Returns an iterator over all tracked entries.\n *\n * Stale entries are transitioned to {@link QuoteStatusState.Expired} first (but\n * kept in the store), so the iterator reflects their up-to-date terminal state.\n *\n * @returns An iterator over the currently tracked entries.\n */\n values(): IterableIterator<QuoteStatusRuntimeEntry> {\n this.expireStaleEntries();\n return this.#items.values();\n }\n\n /**\n * Returns every tracked entry matching the provided quote identifier.\n *\n * Stale entries are transitioned to {@link QuoteStatusState.Expired} first so\n * callers observe their up-to-date terminal state.\n *\n * @param quoteId - Quote identifier to match.\n * @returns The matching entries (empty when none exist).\n */\n getByQuoteId(quoteId: string): QuoteStatusRuntimeEntry[] {\n this.expireStaleEntries();\n\n const matches: QuoteStatusRuntimeEntry[] = [];\n for (const entry of this.#items.values()) {\n if (entry.quoteId === quoteId) {\n matches.push(entry);\n }\n }\n\n return matches;\n }\n\n /**\n * Number of entries currently tracked by the store.\n *\n * Note that this includes terminal entries (`Completed`/`Expired`), which are\n * retained rather than evicted; call {@link expireStaleEntries} (or\n * {@link values}) first when an up-to-date view of entry states is required.\n *\n * @returns The count of tracked entries.\n */\n get size(): number {\n return this.#items.size;\n }\n\n /**\n * Looks up an entry by its transaction metadata identifier.\n *\n * If the matching entry has exceeded its TTL it is transitioned to\n * {@link QuoteStatusState.Expired} (but kept) before being returned.\n *\n * @param txMetaId - Transaction metadata identifier to search for.\n * @returns The matching entry, or `null` if none exists.\n */\n getByTxMetaId(txMetaId: string): QuoteStatusRuntimeEntry | null {\n for (const entry of this.#items.values()) {\n if (entry.txMetaId === txMetaId) {\n this.expireEntryIfStale(entry);\n return entry;\n }\n }\n\n return null;\n }\n\n /**\n * Retrieves an entry by key.\n *\n * If the entry has exceeded its TTL it is transitioned to\n * {@link QuoteStatusState.Expired} (but kept) before being returned.\n *\n * @param key - Unique map key for the entry.\n * @returns The matching entry, or `null` if absent.\n */\n get(key: string): QuoteStatusRuntimeEntry | null {\n const entry = this.#items.get(key);\n if (!entry) {\n return null;\n }\n\n this.expireEntryIfStale(entry);\n return entry;\n }\n\n /**\n * Persists the current snapshot after an in-place mutation of a tracked entry.\n *\n * Used when fields such as `lastAttemptAt` are mutated directly on an entry\n * the caller already holds. No-ops if the entry is no longer tracked (e.g. the\n * store was cleared in the meantime), so persistence only reflects live entries.\n *\n * @param entry - The mutated entry to persist.\n */\n update(entry: QuoteStatusRuntimeEntry): void {\n if (!this.#items.has(QuoteStatusEntryStore.hash(entry))) {\n return;\n }\n\n this.#persistToState();\n }\n\n /**\n * Emits a cloned, serializable snapshot of all tracked entries via the\n * `onPersistUpdates` callback.\n *\n * Each entry's FSM is flattened to its plain `status` value so the snapshot\n * contains no class instances and is safe to store in controller state.\n */\n #persistToState(): void {\n const cloned: Record<string, QuoteStatusPersistEntry> = {};\n for (const [key, entry] of this.#items) {\n cloned[key] = { ...entry, status: entry.status.state };\n }\n this.#onPersistUpdates(cloned);\n }\n\n /**\n * Transitions every stale entry to {@link QuoteStatusState.Expired}, keeping it\n * in the store.\n *\n * Expired entries are retained so that later interactions with the same quote\n * (e.g. a duplicate `reportSubmitted`) can be recognized and rejected. Each\n * transition persists the updated snapshot via the entry's FSM subscription;\n * already-terminal entries cannot transition and are left untouched.\n */\n expireStaleEntries(): void {\n for (const entry of this.#items.values()) {\n if (this.entryHasExpired(entry)) {\n entry.status.transitionTo(QuoteStatusState.Expired);\n }\n }\n }\n\n /**\n * Determines whether an entry has outlived the configured TTL.\n *\n * Expiry is measured from the entry's `createdAt` timestamp, not its last\n * attempt, so an entry's total lifetime is bounded regardless of retries.\n *\n * @param entry - Entry to check.\n * @returns `true` if the entry's age exceeds the TTL.\n */\n entryHasExpired(entry: QuoteStatusRuntimeEntry): boolean {\n const now = Date.now();\n return now - entry.createdAt > this.#entryTtlMs;\n }\n\n /**\n * Transitions an entry to {@link QuoteStatusState.Expired} (keeping it) if it\n * has exceeded its TTL.\n *\n * The transition persists the updated snapshot via the entry's FSM\n * subscription. Already-terminal entries cannot transition and are left\n * untouched.\n *\n * @param entry - Entry to check and possibly expire.\n * @returns `true` if the entry was stale, otherwise `false`.\n */\n expireEntryIfStale(entry: QuoteStatusRuntimeEntry): boolean {\n if (this.entryHasExpired(entry)) {\n entry.status.transitionTo(QuoteStatusState.Expired);\n return true;\n }\n\n return false;\n }\n\n /**\n * Removes every entry from the store.\n *\n * Detaches each entry's FSM listeners before clearing to prevent leaks and\n * stale persist callbacks. Does not emit a persistence update; callers are\n * expected to use this during teardown.\n */\n clear(): void {\n for (const entry of this.#items.values()) {\n entry.status.removeAllListeners();\n }\n\n this.#items.clear();\n }\n}\n"]}
|