@metamask-previews/ramps-controller 2.0.0-preview-749d0638 → 2.0.0-preview-e69ede40

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.
@@ -1,4 +1,17 @@
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 _RampsController_instances, _RampsController_requestCacheTTL, _RampsController_requestCacheMaxSize, _RampsController_pendingRequests, _RampsController_removeRequestState, _RampsController_updateRequestState;
1
13
  import { BaseController } from "@metamask/base-controller";
14
+ import { DEFAULT_REQUEST_CACHE_TTL, DEFAULT_REQUEST_CACHE_MAX_SIZE, createCacheKey, isCacheExpired, createLoadingState, createSuccessState, createErrorState } from "./RequestCache.mjs";
2
15
  // === GENERAL ===
3
16
  /**
4
17
  * The name of the {@link RampsController}, used to namespace the
@@ -16,6 +29,12 @@ const rampsControllerMetadata = {
16
29
  includeInStateLogs: true,
17
30
  usedInUi: true,
18
31
  },
32
+ requests: {
33
+ persist: false,
34
+ includeInDebugSnapshot: true,
35
+ includeInStateLogs: false,
36
+ usedInUi: true,
37
+ },
19
38
  };
20
39
  /**
21
40
  * Constructs the default {@link RampsController} state. This allows
@@ -28,6 +47,7 @@ const rampsControllerMetadata = {
28
47
  export function getDefaultRampsControllerState() {
29
48
  return {
30
49
  geolocation: null,
50
+ requests: {},
31
51
  };
32
52
  }
33
53
  // === CONTROLLER DEFINITION ===
@@ -42,8 +62,10 @@ export class RampsController extends BaseController {
42
62
  * @param args.messenger - The messenger suited for this controller.
43
63
  * @param args.state - The desired state with which to initialize this
44
64
  * controller. Missing properties will be filled in with defaults.
65
+ * @param args.requestCacheTTL - Time to live for cached requests in milliseconds.
66
+ * @param args.requestCacheMaxSize - Maximum number of entries in the request cache.
45
67
  */
46
- constructor({ messenger, state = {}, }) {
68
+ constructor({ messenger, state = {}, requestCacheTTL = DEFAULT_REQUEST_CACHE_TTL, requestCacheMaxSize = DEFAULT_REQUEST_CACHE_MAX_SIZE, }) {
47
69
  super({
48
70
  messenger,
49
71
  metadata: rampsControllerMetadata,
@@ -51,19 +73,163 @@ export class RampsController extends BaseController {
51
73
  state: {
52
74
  ...getDefaultRampsControllerState(),
53
75
  ...state,
76
+ // Always reset requests cache on initialization (non-persisted)
77
+ requests: {},
54
78
  },
55
79
  });
80
+ _RampsController_instances.add(this);
81
+ /**
82
+ * Default TTL for cached requests.
83
+ */
84
+ _RampsController_requestCacheTTL.set(this, void 0);
85
+ /**
86
+ * Maximum number of entries in the request cache.
87
+ */
88
+ _RampsController_requestCacheMaxSize.set(this, void 0);
89
+ /**
90
+ * Map of pending requests for deduplication.
91
+ * Key is the cache key, value is the pending request with abort controller.
92
+ */
93
+ _RampsController_pendingRequests.set(this, new Map());
94
+ __classPrivateFieldSet(this, _RampsController_requestCacheTTL, requestCacheTTL, "f");
95
+ __classPrivateFieldSet(this, _RampsController_requestCacheMaxSize, requestCacheMaxSize, "f");
96
+ }
97
+ /**
98
+ * Executes a request with caching and deduplication.
99
+ *
100
+ * If a request with the same cache key is already in flight, returns the
101
+ * existing promise. If valid cached data exists, returns it without making
102
+ * a new request.
103
+ *
104
+ * @param cacheKey - Unique identifier for this request.
105
+ * @param fetcher - Function that performs the actual fetch. Receives an AbortSignal.
106
+ * @param options - Options for cache behavior.
107
+ * @returns The result of the request.
108
+ */
109
+ async executeRequest(cacheKey, fetcher, options) {
110
+ const ttl = options?.ttl ?? __classPrivateFieldGet(this, _RampsController_requestCacheTTL, "f");
111
+ // Check for existing pending request - join it instead of making a duplicate
112
+ const pending = __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").get(cacheKey);
113
+ if (pending) {
114
+ return pending.promise;
115
+ }
116
+ // Check cache validity (unless force refresh)
117
+ if (!options?.forceRefresh) {
118
+ const cached = this.state.requests[cacheKey];
119
+ if (cached && !isCacheExpired(cached, ttl)) {
120
+ return cached.data;
121
+ }
122
+ }
123
+ // Create abort controller for this request
124
+ const abortController = new AbortController();
125
+ const lastFetchedAt = Date.now();
126
+ // Update state to loading
127
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, createLoadingState());
128
+ // Create the fetch promise
129
+ const promise = (async () => {
130
+ try {
131
+ const data = await fetcher(abortController.signal);
132
+ // Don't update state if aborted
133
+ if (abortController.signal.aborted) {
134
+ throw new Error('Request was aborted');
135
+ }
136
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, createSuccessState(data, lastFetchedAt));
137
+ return data;
138
+ }
139
+ catch (error) {
140
+ // Don't update state if aborted
141
+ if (abortController.signal.aborted) {
142
+ throw error;
143
+ }
144
+ const errorMessage = error?.message;
145
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, createErrorState(errorMessage ?? 'Unknown error', lastFetchedAt));
146
+ throw error;
147
+ }
148
+ finally {
149
+ // Only delete if this is still our entry (not replaced by a new request)
150
+ const currentPending = __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").get(cacheKey);
151
+ if (currentPending?.abortController === abortController) {
152
+ __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").delete(cacheKey);
153
+ }
154
+ }
155
+ })();
156
+ // Store pending request for deduplication
157
+ __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").set(cacheKey, { promise, abortController });
158
+ return promise;
159
+ }
160
+ /**
161
+ * Aborts a pending request if one exists.
162
+ *
163
+ * @param cacheKey - The cache key of the request to abort.
164
+ * @returns True if a request was aborted.
165
+ */
166
+ abortRequest(cacheKey) {
167
+ const pending = __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").get(cacheKey);
168
+ if (pending) {
169
+ pending.abortController.abort();
170
+ __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").delete(cacheKey);
171
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_removeRequestState).call(this, cacheKey);
172
+ return true;
173
+ }
174
+ return false;
175
+ }
176
+ /**
177
+ * Gets the state of a specific cached request.
178
+ *
179
+ * @param cacheKey - The cache key to look up.
180
+ * @returns The request state, or undefined if not cached.
181
+ */
182
+ getRequestState(cacheKey) {
183
+ return this.state.requests[cacheKey];
56
184
  }
57
185
  /**
58
186
  * Updates the user's geolocation.
59
187
  * This method calls the RampsService to get the geolocation
60
188
  * and stores the result in state.
189
+ *
190
+ * @param options - Options for cache behavior.
191
+ * @returns The geolocation string.
61
192
  */
62
- async updateGeolocation() {
63
- const geolocation = await this.messenger.call('RampsService:getGeolocation');
193
+ async updateGeolocation(options) {
194
+ const cacheKey = createCacheKey('updateGeolocation', []);
195
+ const geolocation = await this.executeRequest(cacheKey, async () => {
196
+ const result = await this.messenger.call('RampsService:getGeolocation');
197
+ return result;
198
+ }, options);
64
199
  this.update((state) => {
65
200
  state.geolocation = geolocation;
66
201
  });
202
+ return geolocation;
67
203
  }
68
204
  }
205
+ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheMaxSize = new WeakMap(), _RampsController_pendingRequests = new WeakMap(), _RampsController_instances = new WeakSet(), _RampsController_removeRequestState = function _RampsController_removeRequestState(cacheKey) {
206
+ this.update((state) => {
207
+ const requests = state.requests;
208
+ delete requests[cacheKey];
209
+ });
210
+ }, _RampsController_updateRequestState = function _RampsController_updateRequestState(cacheKey, requestState) {
211
+ const maxSize = __classPrivateFieldGet(this, _RampsController_requestCacheMaxSize, "f");
212
+ this.update((state) => {
213
+ const requests = state.requests;
214
+ requests[cacheKey] = requestState;
215
+ // Evict oldest entries if cache exceeds max size
216
+ const keys = Object.keys(requests);
217
+ if (keys.length > maxSize) {
218
+ // Sort by timestamp (oldest first)
219
+ const sortedKeys = keys.sort((a, b) => {
220
+ const aTime = requests[a]?.timestamp ?? 0;
221
+ const bTime = requests[b]?.timestamp ?? 0;
222
+ return aTime - bTime;
223
+ });
224
+ // Remove oldest entries until we're under the limit
225
+ const entriesToRemove = keys.length - maxSize;
226
+ for (let i = 0; i < entriesToRemove; i++) {
227
+ const keyToRemove = sortedKeys[i];
228
+ if (keyToRemove) {
229
+ delete requests[keyToRemove];
230
+ }
231
+ }
232
+ }
233
+ });
234
+ };
69
235
  //# sourceMappingURL=RampsController.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"RampsController.mjs","sourceRoot":"","sources":["../src/RampsController.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,kCAAkC;AAK3D,kBAAkB;AAElB;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,iBAAiB,CAAC;AAchD;;GAEG;AACH,MAAM,uBAAuB,GAAG;IAC9B,WAAW,EAAE;QACX,OAAO,EAAE,IAAI;QACb,sBAAsB,EAAE,IAAI;QAC5B,kBAAkB,EAAE,IAAI;QACxB,QAAQ,EAAE,IAAI;KACf;CAC4C,CAAC;AAEhD;;;;;;;GAOG;AACH,MAAM,UAAU,8BAA8B;IAC5C,OAAO;QACL,WAAW,EAAE,IAAI;KAClB,CAAC;AACJ,CAAC;AAkDD,gCAAgC;AAEhC;;GAEG;AACH,MAAM,OAAO,eAAgB,SAAQ,cAIpC;IACC;;;;;;;OAOG;IACH,YAAY,EACV,SAAS,EACT,KAAK,GAAG,EAAE,GAIX;QACC,KAAK,CAAC;YACJ,SAAS;YACT,QAAQ,EAAE,uBAAuB;YACjC,IAAI,EAAE,cAAc;YACpB,KAAK,EAAE;gBACL,GAAG,8BAA8B,EAAE;gBACnC,GAAG,KAAK;aACT;SACF,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,iBAAiB;QACrB,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAC3C,6BAA6B,CAC9B,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;YACpB,KAAK,CAAC,WAAW,GAAG,WAAW,CAAC;QAClC,CAAC,CAAC,CAAC;IACL,CAAC;CACF","sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type { Messenger } from '@metamask/messenger';\n\nimport type { RampsServiceGetGeolocationAction } from './RampsService-method-action-types';\n\n// === GENERAL ===\n\n/**\n * The name of the {@link RampsController}, used to namespace the\n * controller's actions and events and to namespace the controller's state data\n * when composed with other controllers.\n */\nexport const controllerName = 'RampsController';\n\n// === STATE ===\n\n/**\n * Describes the shape of the state object for {@link RampsController}.\n */\nexport type RampsControllerState = {\n /**\n * The user's country code determined by geolocation.\n */\n geolocation: string | null;\n};\n\n/**\n * The metadata for each property in {@link RampsControllerState}.\n */\nconst rampsControllerMetadata = {\n geolocation: {\n persist: true,\n includeInDebugSnapshot: true,\n includeInStateLogs: true,\n usedInUi: true,\n },\n} satisfies StateMetadata<RampsControllerState>;\n\n/**\n * Constructs the default {@link RampsController} state. This allows\n * consumers to provide a partial state object when initializing the controller\n * and also helps in constructing complete state objects for this controller in\n * tests.\n *\n * @returns The default {@link RampsController} state.\n */\nexport function getDefaultRampsControllerState(): RampsControllerState {\n return {\n geolocation: null,\n };\n}\n\n// === MESSENGER ===\n\n/**\n * Retrieves the state of the {@link RampsController}.\n */\nexport type RampsControllerGetStateAction = ControllerGetStateAction<\n typeof controllerName,\n RampsControllerState\n>;\n\n/**\n * Actions that {@link RampsControllerMessenger} exposes to other consumers.\n */\nexport type RampsControllerActions = RampsControllerGetStateAction;\n\n/**\n * Actions from other messengers that {@link RampsController} calls.\n */\ntype AllowedActions = RampsServiceGetGeolocationAction;\n\n/**\n * Published when the state of {@link RampsController} changes.\n */\nexport type RampsControllerStateChangeEvent = ControllerStateChangeEvent<\n typeof controllerName,\n RampsControllerState\n>;\n\n/**\n * Events that {@link RampsControllerMessenger} exposes to other consumers.\n */\nexport type RampsControllerEvents = RampsControllerStateChangeEvent;\n\n/**\n * Events from other messengers that {@link RampsController} subscribes to.\n */\ntype AllowedEvents = never;\n\n/**\n * The messenger restricted to actions and events accessed by\n * {@link RampsController}.\n */\nexport type RampsControllerMessenger = Messenger<\n typeof controllerName,\n RampsControllerActions | AllowedActions,\n RampsControllerEvents | AllowedEvents\n>;\n\n// === CONTROLLER DEFINITION ===\n\n/**\n * Manages cryptocurrency on/off ramps functionality.\n */\nexport class RampsController extends BaseController<\n typeof controllerName,\n RampsControllerState,\n RampsControllerMessenger\n> {\n /**\n * Constructs a new {@link RampsController}.\n *\n * @param args - The constructor arguments.\n * @param args.messenger - The messenger suited for this controller.\n * @param args.state - The desired state with which to initialize this\n * controller. Missing properties will be filled in with defaults.\n */\n constructor({\n messenger,\n state = {},\n }: {\n messenger: RampsControllerMessenger;\n state?: Partial<RampsControllerState>;\n }) {\n super({\n messenger,\n metadata: rampsControllerMetadata,\n name: controllerName,\n state: {\n ...getDefaultRampsControllerState(),\n ...state,\n },\n });\n }\n\n /**\n * Updates the user's geolocation.\n * This method calls the RampsService to get the geolocation\n * and stores the result in state.\n */\n async updateGeolocation(): Promise<void> {\n const geolocation = await this.messenger.call(\n 'RampsService:getGeolocation',\n );\n\n this.update((state) => {\n state.geolocation = geolocation;\n });\n }\n}\n"]}
1
+ {"version":3,"file":"RampsController.mjs","sourceRoot":"","sources":["../src/RampsController.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type { Messenger } from '@metamask/messenger';\nimport type { Json } from '@metamask/utils';\n\nimport type { RampsServiceGetGeolocationAction } from './RampsService-method-action-types';\nimport type {\n RequestCache as RequestCacheType,\n RequestState,\n ExecuteRequestOptions,\n PendingRequest,\n} from './RequestCache';\nimport {\n DEFAULT_REQUEST_CACHE_TTL,\n DEFAULT_REQUEST_CACHE_MAX_SIZE,\n createCacheKey,\n isCacheExpired,\n createLoadingState,\n createSuccessState,\n createErrorState,\n} from './RequestCache';\n\n// === GENERAL ===\n\n/**\n * The name of the {@link RampsController}, used to namespace the\n * controller's actions and events and to namespace the controller's state data\n * when composed with other controllers.\n */\nexport const controllerName = 'RampsController';\n\n// === STATE ===\n\n/**\n * Describes the shape of the state object for {@link RampsController}.\n */\nexport type RampsControllerState = {\n /**\n * The user's country code determined by geolocation.\n */\n geolocation: string | null;\n /**\n * Cache of request states, keyed by cache key.\n * This stores loading, success, and error states for API requests.\n */\n requests: RequestCacheType;\n};\n\n/**\n * The metadata for each property in {@link RampsControllerState}.\n */\nconst rampsControllerMetadata = {\n geolocation: {\n persist: true,\n includeInDebugSnapshot: true,\n includeInStateLogs: true,\n usedInUi: true,\n },\n requests: {\n persist: false,\n includeInDebugSnapshot: true,\n includeInStateLogs: false,\n usedInUi: true,\n },\n} satisfies StateMetadata<RampsControllerState>;\n\n/**\n * Constructs the default {@link RampsController} state. This allows\n * consumers to provide a partial state object when initializing the controller\n * and also helps in constructing complete state objects for this controller in\n * tests.\n *\n * @returns The default {@link RampsController} state.\n */\nexport function getDefaultRampsControllerState(): RampsControllerState {\n return {\n geolocation: null,\n requests: {},\n };\n}\n\n// === MESSENGER ===\n\n/**\n * Retrieves the state of the {@link RampsController}.\n */\nexport type RampsControllerGetStateAction = ControllerGetStateAction<\n typeof controllerName,\n RampsControllerState\n>;\n\n/**\n * Actions that {@link RampsControllerMessenger} exposes to other consumers.\n */\nexport type RampsControllerActions = RampsControllerGetStateAction;\n\n/**\n * Actions from other messengers that {@link RampsController} calls.\n */\ntype AllowedActions = RampsServiceGetGeolocationAction;\n\n/**\n * Published when the state of {@link RampsController} changes.\n */\nexport type RampsControllerStateChangeEvent = ControllerStateChangeEvent<\n typeof controllerName,\n RampsControllerState\n>;\n\n/**\n * Events that {@link RampsControllerMessenger} exposes to other consumers.\n */\nexport type RampsControllerEvents = RampsControllerStateChangeEvent;\n\n/**\n * Events from other messengers that {@link RampsController} subscribes to.\n */\ntype AllowedEvents = never;\n\n/**\n * The messenger restricted to actions and events accessed by\n * {@link RampsController}.\n */\nexport type RampsControllerMessenger = Messenger<\n typeof controllerName,\n RampsControllerActions | AllowedActions,\n RampsControllerEvents | AllowedEvents\n>;\n\n/**\n * Configuration options for the RampsController.\n */\nexport type RampsControllerOptions = {\n /** The messenger suited for this controller. */\n messenger: RampsControllerMessenger;\n /** The desired state with which to initialize this controller. */\n state?: Partial<RampsControllerState>;\n /** Time to live for cached requests in milliseconds. Defaults to 15 minutes. */\n requestCacheTTL?: number;\n /** Maximum number of entries in the request cache. Defaults to 250. */\n requestCacheMaxSize?: number;\n};\n\n// === CONTROLLER DEFINITION ===\n\n/**\n * Manages cryptocurrency on/off ramps functionality.\n */\nexport class RampsController extends BaseController<\n typeof controllerName,\n RampsControllerState,\n RampsControllerMessenger\n> {\n /**\n * Default TTL for cached requests.\n */\n readonly #requestCacheTTL: number;\n\n /**\n * Maximum number of entries in the request cache.\n */\n readonly #requestCacheMaxSize: number;\n\n /**\n * Map of pending requests for deduplication.\n * Key is the cache key, value is the pending request with abort controller.\n */\n readonly #pendingRequests: Map<string, PendingRequest> = new Map();\n\n /**\n * Constructs a new {@link RampsController}.\n *\n * @param args - The constructor arguments.\n * @param args.messenger - The messenger suited for this controller.\n * @param args.state - The desired state with which to initialize this\n * controller. Missing properties will be filled in with defaults.\n * @param args.requestCacheTTL - Time to live for cached requests in milliseconds.\n * @param args.requestCacheMaxSize - Maximum number of entries in the request cache.\n */\n constructor({\n messenger,\n state = {},\n requestCacheTTL = DEFAULT_REQUEST_CACHE_TTL,\n requestCacheMaxSize = DEFAULT_REQUEST_CACHE_MAX_SIZE,\n }: RampsControllerOptions) {\n super({\n messenger,\n metadata: rampsControllerMetadata,\n name: controllerName,\n state: {\n ...getDefaultRampsControllerState(),\n ...state,\n // Always reset requests cache on initialization (non-persisted)\n requests: {},\n },\n });\n\n this.#requestCacheTTL = requestCacheTTL;\n this.#requestCacheMaxSize = requestCacheMaxSize;\n }\n\n /**\n * Executes a request with caching and deduplication.\n *\n * If a request with the same cache key is already in flight, returns the\n * existing promise. If valid cached data exists, returns it without making\n * a new request.\n *\n * @param cacheKey - Unique identifier for this request.\n * @param fetcher - Function that performs the actual fetch. Receives an AbortSignal.\n * @param options - Options for cache behavior.\n * @returns The result of the request.\n */\n async executeRequest<TResult>(\n cacheKey: string,\n fetcher: (signal: AbortSignal) => Promise<TResult>,\n options?: ExecuteRequestOptions,\n ): Promise<TResult> {\n const ttl = options?.ttl ?? this.#requestCacheTTL;\n\n // Check for existing pending request - join it instead of making a duplicate\n const pending = this.#pendingRequests.get(cacheKey);\n if (pending) {\n return pending.promise as Promise<TResult>;\n }\n\n // Check cache validity (unless force refresh)\n if (!options?.forceRefresh) {\n const cached = this.state.requests[cacheKey];\n if (cached && !isCacheExpired(cached, ttl)) {\n return cached.data as TResult;\n }\n }\n\n // Create abort controller for this request\n const abortController = new AbortController();\n const lastFetchedAt = Date.now();\n\n // Update state to loading\n this.#updateRequestState(cacheKey, createLoadingState());\n\n // Create the fetch promise\n const promise = (async (): Promise<TResult> => {\n try {\n const data = await fetcher(abortController.signal);\n\n // Don't update state if aborted\n if (abortController.signal.aborted) {\n throw new Error('Request was aborted');\n }\n\n this.#updateRequestState(\n cacheKey,\n createSuccessState(data as Json, lastFetchedAt),\n );\n return data;\n } catch (error) {\n // Don't update state if aborted\n if (abortController.signal.aborted) {\n throw error;\n }\n\n const errorMessage = (error as Error)?.message;\n\n this.#updateRequestState(\n cacheKey,\n createErrorState(errorMessage ?? 'Unknown error', lastFetchedAt),\n );\n throw error;\n } finally {\n // Only delete if this is still our entry (not replaced by a new request)\n const currentPending = this.#pendingRequests.get(cacheKey);\n if (currentPending?.abortController === abortController) {\n this.#pendingRequests.delete(cacheKey);\n }\n }\n })();\n\n // Store pending request for deduplication\n this.#pendingRequests.set(cacheKey, { promise, abortController });\n\n return promise;\n }\n\n /**\n * Aborts a pending request if one exists.\n *\n * @param cacheKey - The cache key of the request to abort.\n * @returns True if a request was aborted.\n */\n abortRequest(cacheKey: string): boolean {\n const pending = this.#pendingRequests.get(cacheKey);\n if (pending) {\n pending.abortController.abort();\n this.#pendingRequests.delete(cacheKey);\n this.#removeRequestState(cacheKey);\n return true;\n }\n return false;\n }\n\n /**\n * Removes a request state from the cache.\n *\n * @param cacheKey - The cache key to remove.\n */\n #removeRequestState(cacheKey: string): void {\n this.update((state) => {\n const requests = state.requests as unknown as Record<\n string,\n RequestState | undefined\n >;\n delete requests[cacheKey];\n });\n }\n\n /**\n * Gets the state of a specific cached request.\n *\n * @param cacheKey - The cache key to look up.\n * @returns The request state, or undefined if not cached.\n */\n getRequestState(cacheKey: string): RequestState | undefined {\n return this.state.requests[cacheKey];\n }\n\n /**\n * Updates the state for a specific request.\n *\n * @param cacheKey - The cache key.\n * @param requestState - The new state for the request.\n */\n #updateRequestState(cacheKey: string, requestState: RequestState): void {\n const maxSize = this.#requestCacheMaxSize;\n\n this.update((state) => {\n const requests = state.requests as unknown as Record<\n string,\n RequestState | undefined\n >;\n requests[cacheKey] = requestState;\n\n // Evict oldest entries if cache exceeds max size\n const keys = Object.keys(requests);\n\n if (keys.length > maxSize) {\n // Sort by timestamp (oldest first)\n const sortedKeys = keys.sort((a, b) => {\n const aTime = requests[a]?.timestamp ?? 0;\n const bTime = requests[b]?.timestamp ?? 0;\n return aTime - bTime;\n });\n\n // Remove oldest entries until we're under the limit\n const entriesToRemove = keys.length - maxSize;\n for (let i = 0; i < entriesToRemove; i++) {\n const keyToRemove = sortedKeys[i];\n if (keyToRemove) {\n delete requests[keyToRemove];\n }\n }\n }\n });\n }\n\n /**\n * Updates the user's geolocation.\n * This method calls the RampsService to get the geolocation\n * and stores the result in state.\n *\n * @param options - Options for cache behavior.\n * @returns The geolocation string.\n */\n async updateGeolocation(options?: ExecuteRequestOptions): Promise<string> {\n const cacheKey = createCacheKey('updateGeolocation', []);\n\n const geolocation = await this.executeRequest(\n cacheKey,\n async () => {\n const result = await this.messenger.call('RampsService:getGeolocation');\n return result;\n },\n options,\n );\n\n this.update((state) => {\n state.geolocation = geolocation;\n });\n\n return geolocation;\n }\n}\n"]}
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createErrorState = exports.createSuccessState = exports.createLoadingState = exports.isCacheExpired = exports.createCacheKey = exports.DEFAULT_REQUEST_CACHE_MAX_SIZE = exports.DEFAULT_REQUEST_CACHE_TTL = exports.RequestStatus = void 0;
4
+ /**
5
+ * Status of a cached request.
6
+ */
7
+ var RequestStatus;
8
+ (function (RequestStatus) {
9
+ RequestStatus["IDLE"] = "idle";
10
+ RequestStatus["LOADING"] = "loading";
11
+ RequestStatus["SUCCESS"] = "success";
12
+ RequestStatus["ERROR"] = "error";
13
+ })(RequestStatus || (exports.RequestStatus = RequestStatus = {}));
14
+ /**
15
+ * Default TTL for cached requests in milliseconds (15 minutes).
16
+ */
17
+ exports.DEFAULT_REQUEST_CACHE_TTL = 15 * 60 * 1000;
18
+ /**
19
+ * Default maximum number of entries in the request cache.
20
+ */
21
+ exports.DEFAULT_REQUEST_CACHE_MAX_SIZE = 250;
22
+ /**
23
+ * Creates a cache key from a method name and parameters.
24
+ *
25
+ * @param method - The method name.
26
+ * @param params - The parameters passed to the method.
27
+ * @returns A unique cache key string.
28
+ */
29
+ function createCacheKey(method, params) {
30
+ return `${method}:${JSON.stringify(params)}`;
31
+ }
32
+ exports.createCacheKey = createCacheKey;
33
+ /**
34
+ * Checks if a cached request has expired based on TTL.
35
+ *
36
+ * @param requestState - The cached request state.
37
+ * @param ttl - Time to live in milliseconds.
38
+ * @returns True if the cache entry has expired.
39
+ */
40
+ function isCacheExpired(requestState, ttl = exports.DEFAULT_REQUEST_CACHE_TTL) {
41
+ if (requestState.status !== RequestStatus.SUCCESS) {
42
+ return true;
43
+ }
44
+ const now = Date.now();
45
+ return now - requestState.timestamp > ttl;
46
+ }
47
+ exports.isCacheExpired = isCacheExpired;
48
+ /**
49
+ * Creates an initial loading state for a request.
50
+ *
51
+ * @returns A new RequestState in loading status.
52
+ */
53
+ function createLoadingState() {
54
+ const now = Date.now();
55
+ return {
56
+ status: RequestStatus.LOADING,
57
+ data: null,
58
+ error: null,
59
+ timestamp: now,
60
+ lastFetchedAt: now,
61
+ };
62
+ }
63
+ exports.createLoadingState = createLoadingState;
64
+ /**
65
+ * Creates a success state for a request.
66
+ *
67
+ * @param data - The data returned by the request.
68
+ * @param lastFetchedAt - When the fetch started.
69
+ * @returns A new RequestState in success status.
70
+ */
71
+ function createSuccessState(data, lastFetchedAt) {
72
+ return {
73
+ status: RequestStatus.SUCCESS,
74
+ data,
75
+ error: null,
76
+ timestamp: Date.now(),
77
+ lastFetchedAt,
78
+ };
79
+ }
80
+ exports.createSuccessState = createSuccessState;
81
+ /**
82
+ * Creates an error state for a request.
83
+ *
84
+ * @param error - The error message.
85
+ * @param lastFetchedAt - When the fetch started.
86
+ * @returns A new RequestState in error status.
87
+ */
88
+ function createErrorState(error, lastFetchedAt) {
89
+ return {
90
+ status: RequestStatus.ERROR,
91
+ data: null,
92
+ error,
93
+ timestamp: Date.now(),
94
+ lastFetchedAt,
95
+ };
96
+ }
97
+ exports.createErrorState = createErrorState;
98
+ //# sourceMappingURL=RequestCache.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RequestCache.cjs","sourceRoot":"","sources":["../src/RequestCache.ts"],"names":[],"mappings":";;;AAEA;;GAEG;AACH,IAAY,aAKX;AALD,WAAY,aAAa;IACvB,8BAAa,CAAA;IACb,oCAAmB,CAAA;IACnB,oCAAmB,CAAA;IACnB,gCAAe,CAAA;AACjB,CAAC,EALW,aAAa,6BAAb,aAAa,QAKxB;AAwBD;;GAEG;AACU,QAAA,yBAAyB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAExD;;GAEG;AACU,QAAA,8BAA8B,GAAG,GAAG,CAAC;AAElD;;;;;;GAMG;AACH,SAAgB,cAAc,CAAC,MAAc,EAAE,MAAiB;IAC9D,OAAO,GAAG,MAAM,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;AAC/C,CAAC;AAFD,wCAEC;AAED;;;;;;GAMG;AACH,SAAgB,cAAc,CAC5B,YAA0B,EAC1B,MAAc,iCAAyB;IAEvC,IAAI,YAAY,CAAC,MAAM,KAAK,aAAa,CAAC,OAAO,EAAE,CAAC;QAClD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,OAAO,GAAG,GAAG,YAAY,CAAC,SAAS,GAAG,GAAG,CAAC;AAC5C,CAAC;AATD,wCASC;AAED;;;;GAIG;AACH,SAAgB,kBAAkB;IAChC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,OAAO;QACL,MAAM,EAAE,aAAa,CAAC,OAAO;QAC7B,IAAI,EAAE,IAAI;QACV,KAAK,EAAE,IAAI;QACX,SAAS,EAAE,GAAG;QACd,aAAa,EAAE,GAAG;KACnB,CAAC;AACJ,CAAC;AATD,gDASC;AAED;;;;;;GAMG;AACH,SAAgB,kBAAkB,CAChC,IAAU,EACV,aAAqB;IAErB,OAAO;QACL,MAAM,EAAE,aAAa,CAAC,OAAO;QAC7B,IAAI;QACJ,KAAK,EAAE,IAAI;QACX,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,aAAa;KACd,CAAC;AACJ,CAAC;AAXD,gDAWC;AAED;;;;;;GAMG;AACH,SAAgB,gBAAgB,CAC9B,KAAa,EACb,aAAqB;IAErB,OAAO;QACL,MAAM,EAAE,aAAa,CAAC,KAAK;QAC3B,IAAI,EAAE,IAAI;QACV,KAAK;QACL,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,aAAa;KACd,CAAC;AACJ,CAAC;AAXD,4CAWC","sourcesContent":["import type { Json } from '@metamask/utils';\n\n/**\n * Status of a cached request.\n */\nexport enum RequestStatus {\n IDLE = 'idle',\n LOADING = 'loading',\n SUCCESS = 'success',\n ERROR = 'error',\n}\n\n/**\n * State of a single cached request.\n * All properties must be JSON-serializable to satisfy StateConstraint.\n */\nexport type RequestState = {\n /** Current status of the request */\n status: `${RequestStatus}`;\n /** The data returned by the request, if successful */\n data: Json | null;\n /** Error message if the request failed */\n error: string | null;\n /** Timestamp when the request completed (for TTL calculation) */\n timestamp: number;\n /** Timestamp when the fetch started */\n lastFetchedAt: number;\n};\n\n/**\n * Cache of request states, keyed by cache key.\n */\nexport type RequestCache = Record<string, RequestState>;\n\n/**\n * Default TTL for cached requests in milliseconds (15 minutes).\n */\nexport const DEFAULT_REQUEST_CACHE_TTL = 15 * 60 * 1000;\n\n/**\n * Default maximum number of entries in the request cache.\n */\nexport const DEFAULT_REQUEST_CACHE_MAX_SIZE = 250;\n\n/**\n * Creates a cache key from a method name and parameters.\n *\n * @param method - The method name.\n * @param params - The parameters passed to the method.\n * @returns A unique cache key string.\n */\nexport function createCacheKey(method: string, params: unknown[]): string {\n return `${method}:${JSON.stringify(params)}`;\n}\n\n/**\n * Checks if a cached request has expired based on TTL.\n *\n * @param requestState - The cached request state.\n * @param ttl - Time to live in milliseconds.\n * @returns True if the cache entry has expired.\n */\nexport function isCacheExpired(\n requestState: RequestState,\n ttl: number = DEFAULT_REQUEST_CACHE_TTL,\n): boolean {\n if (requestState.status !== RequestStatus.SUCCESS) {\n return true;\n }\n const now = Date.now();\n return now - requestState.timestamp > ttl;\n}\n\n/**\n * Creates an initial loading state for a request.\n *\n * @returns A new RequestState in loading status.\n */\nexport function createLoadingState(): RequestState {\n const now = Date.now();\n return {\n status: RequestStatus.LOADING,\n data: null,\n error: null,\n timestamp: now,\n lastFetchedAt: now,\n };\n}\n\n/**\n * Creates a success state for a request.\n *\n * @param data - The data returned by the request.\n * @param lastFetchedAt - When the fetch started.\n * @returns A new RequestState in success status.\n */\nexport function createSuccessState(\n data: Json,\n lastFetchedAt: number,\n): RequestState {\n return {\n status: RequestStatus.SUCCESS,\n data,\n error: null,\n timestamp: Date.now(),\n lastFetchedAt,\n };\n}\n\n/**\n * Creates an error state for a request.\n *\n * @param error - The error message.\n * @param lastFetchedAt - When the fetch started.\n * @returns A new RequestState in error status.\n */\nexport function createErrorState(\n error: string,\n lastFetchedAt: number,\n): RequestState {\n return {\n status: RequestStatus.ERROR,\n data: null,\n error,\n timestamp: Date.now(),\n lastFetchedAt,\n };\n}\n\n/**\n * Options for executing a cached request.\n */\nexport type ExecuteRequestOptions = {\n /** Force a refresh even if cached data exists */\n forceRefresh?: boolean;\n /** Custom TTL for this request in milliseconds */\n ttl?: number;\n};\n\n/**\n * Represents a pending request with its promise and abort controller.\n */\nexport type PendingRequest<TResult = unknown> = {\n promise: Promise<TResult>;\n abortController: AbortController;\n};\n"]}
@@ -0,0 +1,93 @@
1
+ import type { Json } from "@metamask/utils";
2
+ /**
3
+ * Status of a cached request.
4
+ */
5
+ export declare enum RequestStatus {
6
+ IDLE = "idle",
7
+ LOADING = "loading",
8
+ SUCCESS = "success",
9
+ ERROR = "error"
10
+ }
11
+ /**
12
+ * State of a single cached request.
13
+ * All properties must be JSON-serializable to satisfy StateConstraint.
14
+ */
15
+ export type RequestState = {
16
+ /** Current status of the request */
17
+ status: `${RequestStatus}`;
18
+ /** The data returned by the request, if successful */
19
+ data: Json | null;
20
+ /** Error message if the request failed */
21
+ error: string | null;
22
+ /** Timestamp when the request completed (for TTL calculation) */
23
+ timestamp: number;
24
+ /** Timestamp when the fetch started */
25
+ lastFetchedAt: number;
26
+ };
27
+ /**
28
+ * Cache of request states, keyed by cache key.
29
+ */
30
+ export type RequestCache = Record<string, RequestState>;
31
+ /**
32
+ * Default TTL for cached requests in milliseconds (15 minutes).
33
+ */
34
+ export declare const DEFAULT_REQUEST_CACHE_TTL: number;
35
+ /**
36
+ * Default maximum number of entries in the request cache.
37
+ */
38
+ export declare const DEFAULT_REQUEST_CACHE_MAX_SIZE = 250;
39
+ /**
40
+ * Creates a cache key from a method name and parameters.
41
+ *
42
+ * @param method - The method name.
43
+ * @param params - The parameters passed to the method.
44
+ * @returns A unique cache key string.
45
+ */
46
+ export declare function createCacheKey(method: string, params: unknown[]): string;
47
+ /**
48
+ * Checks if a cached request has expired based on TTL.
49
+ *
50
+ * @param requestState - The cached request state.
51
+ * @param ttl - Time to live in milliseconds.
52
+ * @returns True if the cache entry has expired.
53
+ */
54
+ export declare function isCacheExpired(requestState: RequestState, ttl?: number): boolean;
55
+ /**
56
+ * Creates an initial loading state for a request.
57
+ *
58
+ * @returns A new RequestState in loading status.
59
+ */
60
+ export declare function createLoadingState(): RequestState;
61
+ /**
62
+ * Creates a success state for a request.
63
+ *
64
+ * @param data - The data returned by the request.
65
+ * @param lastFetchedAt - When the fetch started.
66
+ * @returns A new RequestState in success status.
67
+ */
68
+ export declare function createSuccessState(data: Json, lastFetchedAt: number): RequestState;
69
+ /**
70
+ * Creates an error state for a request.
71
+ *
72
+ * @param error - The error message.
73
+ * @param lastFetchedAt - When the fetch started.
74
+ * @returns A new RequestState in error status.
75
+ */
76
+ export declare function createErrorState(error: string, lastFetchedAt: number): RequestState;
77
+ /**
78
+ * Options for executing a cached request.
79
+ */
80
+ export type ExecuteRequestOptions = {
81
+ /** Force a refresh even if cached data exists */
82
+ forceRefresh?: boolean;
83
+ /** Custom TTL for this request in milliseconds */
84
+ ttl?: number;
85
+ };
86
+ /**
87
+ * Represents a pending request with its promise and abort controller.
88
+ */
89
+ export type PendingRequest<TResult = unknown> = {
90
+ promise: Promise<TResult>;
91
+ abortController: AbortController;
92
+ };
93
+ //# sourceMappingURL=RequestCache.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RequestCache.d.cts","sourceRoot":"","sources":["../src/RequestCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,wBAAwB;AAE5C;;GAEG;AACH,oBAAY,aAAa;IACvB,IAAI,SAAS;IACb,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,KAAK,UAAU;CAChB;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,oCAAoC;IACpC,MAAM,EAAE,GAAG,aAAa,EAAE,CAAC;IAC3B,sDAAsD;IACtD,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,0CAA0C;IAC1C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;AAExD;;GAEG;AACH,eAAO,MAAM,yBAAyB,QAAiB,CAAC;AAExD;;GAEG;AACH,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAElD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CAExE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,YAAY,EAAE,YAAY,EAC1B,GAAG,GAAE,MAAkC,GACtC,OAAO,CAMT;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,YAAY,CASjD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,EACV,aAAa,EAAE,MAAM,GACpB,YAAY,CAQd;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,YAAY,CAQd;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,iDAAiD;IACjD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kDAAkD;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,cAAc,CAAC,OAAO,GAAG,OAAO,IAAI;IAC9C,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1B,eAAe,EAAE,eAAe,CAAC;CAClC,CAAC"}
@@ -0,0 +1,93 @@
1
+ import type { Json } from "@metamask/utils";
2
+ /**
3
+ * Status of a cached request.
4
+ */
5
+ export declare enum RequestStatus {
6
+ IDLE = "idle",
7
+ LOADING = "loading",
8
+ SUCCESS = "success",
9
+ ERROR = "error"
10
+ }
11
+ /**
12
+ * State of a single cached request.
13
+ * All properties must be JSON-serializable to satisfy StateConstraint.
14
+ */
15
+ export type RequestState = {
16
+ /** Current status of the request */
17
+ status: `${RequestStatus}`;
18
+ /** The data returned by the request, if successful */
19
+ data: Json | null;
20
+ /** Error message if the request failed */
21
+ error: string | null;
22
+ /** Timestamp when the request completed (for TTL calculation) */
23
+ timestamp: number;
24
+ /** Timestamp when the fetch started */
25
+ lastFetchedAt: number;
26
+ };
27
+ /**
28
+ * Cache of request states, keyed by cache key.
29
+ */
30
+ export type RequestCache = Record<string, RequestState>;
31
+ /**
32
+ * Default TTL for cached requests in milliseconds (15 minutes).
33
+ */
34
+ export declare const DEFAULT_REQUEST_CACHE_TTL: number;
35
+ /**
36
+ * Default maximum number of entries in the request cache.
37
+ */
38
+ export declare const DEFAULT_REQUEST_CACHE_MAX_SIZE = 250;
39
+ /**
40
+ * Creates a cache key from a method name and parameters.
41
+ *
42
+ * @param method - The method name.
43
+ * @param params - The parameters passed to the method.
44
+ * @returns A unique cache key string.
45
+ */
46
+ export declare function createCacheKey(method: string, params: unknown[]): string;
47
+ /**
48
+ * Checks if a cached request has expired based on TTL.
49
+ *
50
+ * @param requestState - The cached request state.
51
+ * @param ttl - Time to live in milliseconds.
52
+ * @returns True if the cache entry has expired.
53
+ */
54
+ export declare function isCacheExpired(requestState: RequestState, ttl?: number): boolean;
55
+ /**
56
+ * Creates an initial loading state for a request.
57
+ *
58
+ * @returns A new RequestState in loading status.
59
+ */
60
+ export declare function createLoadingState(): RequestState;
61
+ /**
62
+ * Creates a success state for a request.
63
+ *
64
+ * @param data - The data returned by the request.
65
+ * @param lastFetchedAt - When the fetch started.
66
+ * @returns A new RequestState in success status.
67
+ */
68
+ export declare function createSuccessState(data: Json, lastFetchedAt: number): RequestState;
69
+ /**
70
+ * Creates an error state for a request.
71
+ *
72
+ * @param error - The error message.
73
+ * @param lastFetchedAt - When the fetch started.
74
+ * @returns A new RequestState in error status.
75
+ */
76
+ export declare function createErrorState(error: string, lastFetchedAt: number): RequestState;
77
+ /**
78
+ * Options for executing a cached request.
79
+ */
80
+ export type ExecuteRequestOptions = {
81
+ /** Force a refresh even if cached data exists */
82
+ forceRefresh?: boolean;
83
+ /** Custom TTL for this request in milliseconds */
84
+ ttl?: number;
85
+ };
86
+ /**
87
+ * Represents a pending request with its promise and abort controller.
88
+ */
89
+ export type PendingRequest<TResult = unknown> = {
90
+ promise: Promise<TResult>;
91
+ abortController: AbortController;
92
+ };
93
+ //# sourceMappingURL=RequestCache.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RequestCache.d.mts","sourceRoot":"","sources":["../src/RequestCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,wBAAwB;AAE5C;;GAEG;AACH,oBAAY,aAAa;IACvB,IAAI,SAAS;IACb,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,KAAK,UAAU;CAChB;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,oCAAoC;IACpC,MAAM,EAAE,GAAG,aAAa,EAAE,CAAC;IAC3B,sDAAsD;IACtD,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,0CAA0C;IAC1C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;AAExD;;GAEG;AACH,eAAO,MAAM,yBAAyB,QAAiB,CAAC;AAExD;;GAEG;AACH,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAElD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CAExE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,YAAY,EAAE,YAAY,EAC1B,GAAG,GAAE,MAAkC,GACtC,OAAO,CAMT;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,YAAY,CASjD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,EACV,aAAa,EAAE,MAAM,GACpB,YAAY,CAQd;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,YAAY,CAQd;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,iDAAiD;IACjD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kDAAkD;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,cAAc,CAAC,OAAO,GAAG,OAAO,IAAI;IAC9C,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1B,eAAe,EAAE,eAAe,CAAC;CAClC,CAAC"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Status of a cached request.
3
+ */
4
+ export var RequestStatus;
5
+ (function (RequestStatus) {
6
+ RequestStatus["IDLE"] = "idle";
7
+ RequestStatus["LOADING"] = "loading";
8
+ RequestStatus["SUCCESS"] = "success";
9
+ RequestStatus["ERROR"] = "error";
10
+ })(RequestStatus || (RequestStatus = {}));
11
+ /**
12
+ * Default TTL for cached requests in milliseconds (15 minutes).
13
+ */
14
+ export const DEFAULT_REQUEST_CACHE_TTL = 15 * 60 * 1000;
15
+ /**
16
+ * Default maximum number of entries in the request cache.
17
+ */
18
+ export const DEFAULT_REQUEST_CACHE_MAX_SIZE = 250;
19
+ /**
20
+ * Creates a cache key from a method name and parameters.
21
+ *
22
+ * @param method - The method name.
23
+ * @param params - The parameters passed to the method.
24
+ * @returns A unique cache key string.
25
+ */
26
+ export function createCacheKey(method, params) {
27
+ return `${method}:${JSON.stringify(params)}`;
28
+ }
29
+ /**
30
+ * Checks if a cached request has expired based on TTL.
31
+ *
32
+ * @param requestState - The cached request state.
33
+ * @param ttl - Time to live in milliseconds.
34
+ * @returns True if the cache entry has expired.
35
+ */
36
+ export function isCacheExpired(requestState, ttl = DEFAULT_REQUEST_CACHE_TTL) {
37
+ if (requestState.status !== RequestStatus.SUCCESS) {
38
+ return true;
39
+ }
40
+ const now = Date.now();
41
+ return now - requestState.timestamp > ttl;
42
+ }
43
+ /**
44
+ * Creates an initial loading state for a request.
45
+ *
46
+ * @returns A new RequestState in loading status.
47
+ */
48
+ export function createLoadingState() {
49
+ const now = Date.now();
50
+ return {
51
+ status: RequestStatus.LOADING,
52
+ data: null,
53
+ error: null,
54
+ timestamp: now,
55
+ lastFetchedAt: now,
56
+ };
57
+ }
58
+ /**
59
+ * Creates a success state for a request.
60
+ *
61
+ * @param data - The data returned by the request.
62
+ * @param lastFetchedAt - When the fetch started.
63
+ * @returns A new RequestState in success status.
64
+ */
65
+ export function createSuccessState(data, lastFetchedAt) {
66
+ return {
67
+ status: RequestStatus.SUCCESS,
68
+ data,
69
+ error: null,
70
+ timestamp: Date.now(),
71
+ lastFetchedAt,
72
+ };
73
+ }
74
+ /**
75
+ * Creates an error state for a request.
76
+ *
77
+ * @param error - The error message.
78
+ * @param lastFetchedAt - When the fetch started.
79
+ * @returns A new RequestState in error status.
80
+ */
81
+ export function createErrorState(error, lastFetchedAt) {
82
+ return {
83
+ status: RequestStatus.ERROR,
84
+ data: null,
85
+ error,
86
+ timestamp: Date.now(),
87
+ lastFetchedAt,
88
+ };
89
+ }
90
+ //# sourceMappingURL=RequestCache.mjs.map