@metamask-previews/geolocation-controller 2.0.0-preview-313288d71 → 2.0.0-preview-35f016caa

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 CHANGED
@@ -9,22 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
- - Add `GeolocationApiServiceInvalidateQueriesAction`, `GeolocationApiServiceCacheUpdatedEvent` and `GeolocationApiServiceGranularCacheUpdatedEvent`, also included in `GeolocationApiServiceActions` and `GeolocationApiServiceEvents` ([#10881](https://github.com/MetaMask/core/pull/10881))
13
12
  - Add support for Node.js 26 ([#10981](https://github.com/MetaMask/core/pull/10981))
14
13
 
15
14
  ### Changed
16
15
 
17
- - **BREAKING:** `GeolocationApiService` now extends `BaseDataService` from `@metamask/base-data-service` ([#10881](https://github.com/MetaMask/core/pull/10881))
18
- - `GeolocationApiServiceMessenger` now includes the `GeolocationApiService:invalidateQueries` action and `GeolocationApiService:cacheUpdated` events. Messengers built from `GeolocationApiServiceActions` / `GeolocationApiServiceEvents` pick these up automatically.
19
- - The hand-rolled TTL cache and in-flight deduplication are replaced by the `BaseDataService` query cache. `ttlMs` and `bypassCache` keep their existing behaviour.
20
- - Add dependency `@metamask/base-data-service` ([#10881](https://github.com/MetaMask/core/pull/10881))
21
- - Add dependency `@metamask/superstruct` ([#10881](https://github.com/MetaMask/core/pull/10881))
22
16
  - Bump `@metamask/base-controller` from `^10.0.0` to `^10.1.0` ([#10864](https://github.com/MetaMask/core/pull/10864))
23
17
 
24
- ### Removed
25
-
26
- - **BREAKING:** Remove `onRetry`, `onBreak` and `onDegraded` from `GeolocationApiService` ([#10881](https://github.com/MetaMask/core/pull/10881))
27
-
28
18
  ## [2.0.0]
29
19
 
30
20
  ### Changed
@@ -5,11 +5,13 @@
5
5
  import type { GeolocationApiService } from './geolocation-api-service.js';
6
6
  /**
7
7
  * Returns the geolocation code. Serves from cache when the TTL has not
8
- * expired, otherwise performs a network fetch.
8
+ * expired, otherwise performs a network fetch. Concurrent callers are
9
+ * deduplicated to a single in-flight request.
9
10
  *
10
11
  * @param options - Optional fetch options.
11
- * @param options.bypassCache - When true, ignores any cached result and
12
- * fetches fresh data.
12
+ * @param options.bypassCache - When true, invalidates the TTL cache. If a
13
+ * request is already in-flight it will be reused (deduplication always
14
+ * applies).
13
15
  * @returns An ISO 3166-2 location code (e.g. `US`, `US-NY`, `CA-ON`), or
14
16
  * {@link UNKNOWN_LOCATION} when the API returns an empty or invalid body.
15
17
  */
@@ -20,11 +22,12 @@ export type GeolocationApiServiceFetchGeolocationAction = {
20
22
  /**
21
23
  * Returns the country, region, and timezone for the current client. Serves
22
24
  * from cache when the TTL has not expired, otherwise performs a network
23
- * fetch.
25
+ * fetch. Concurrent callers are deduplicated to a single in-flight request.
24
26
  *
25
27
  * @param options - Optional fetch options.
26
- * @param options.bypassCache - When true, ignores any cached result and
27
- * fetches fresh data.
28
+ * @param options.bypassCache - When true, invalidates the TTL cache. If a
29
+ * request is already in-flight it will be reused (deduplication always
30
+ * applies).
28
31
  * @returns The geolocation data, where each field is `null` when the API
29
32
  * omits it or returns a value that fails validation.
30
33
  */
@@ -1 +1 @@
1
- {"version":3,"file":"geolocation-api-service-method-action-types.d.ts","sourceRoot":"","sources":["../../src/geolocation-api-service/geolocation-api-service-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E;;;;;;;;;GASG;AACH,MAAM,MAAM,2CAA2C,GAAG;IACxD,IAAI,EAAE,wCAAwC,CAAC;IAC/C,OAAO,EAAE,qBAAqB,CAAC,kBAAkB,CAAC,CAAC;CACpD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,+CAA+C,GAAG;IAC5D,IAAI,EAAE,4CAA4C,CAAC;IACnD,OAAO,EAAE,qBAAqB,CAAC,sBAAsB,CAAC,CAAC;CACxD,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,kCAAkC,GAC1C,2CAA2C,GAC3C,+CAA+C,CAAC"}
1
+ {"version":3,"file":"geolocation-api-service-method-action-types.d.ts","sourceRoot":"","sources":["../../src/geolocation-api-service/geolocation-api-service-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,2CAA2C,GAAG;IACxD,IAAI,EAAE,wCAAwC,CAAC;IAC/C,OAAO,EAAE,qBAAqB,CAAC,kBAAkB,CAAC,CAAC;CACpD,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,+CAA+C,GAAG;IAC5D,IAAI,EAAE,4CAA4C,CAAC;IACnD,OAAO,EAAE,qBAAqB,CAAC,sBAAsB,CAAC,CAAC;CACxD,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,kCAAkC,GAC1C,2CAA2C,GAC3C,+CAA+C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"geolocation-api-service-method-action-types.js","sourceRoot":"","sources":["../../src/geolocation-api-service/geolocation-api-service-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * This file is auto generated.\n * Do not edit manually.\n */\n\nimport type { GeolocationApiService } from './geolocation-api-service.js';\n\n/**\n * Returns the geolocation code. Serves from cache when the TTL has not\n * expired, otherwise performs a network fetch.\n *\n * @param options - Optional fetch options.\n * @param options.bypassCache - When true, ignores any cached result and\n * fetches fresh data.\n * @returns An ISO 3166-2 location code (e.g. `US`, `US-NY`, `CA-ON`), or\n * {@link UNKNOWN_LOCATION} when the API returns an empty or invalid body.\n */\nexport type GeolocationApiServiceFetchGeolocationAction = {\n type: `GeolocationApiService:fetchGeolocation`;\n handler: GeolocationApiService['fetchGeolocation'];\n};\n\n/**\n * Returns the country, region, and timezone for the current client. Serves\n * from cache when the TTL has not expired, otherwise performs a network\n * fetch.\n *\n * @param options - Optional fetch options.\n * @param options.bypassCache - When true, ignores any cached result and\n * fetches fresh data.\n * @returns The geolocation data, where each field is `null` when the API\n * omits it or returns a value that fails validation.\n */\nexport type GeolocationApiServiceFetchGeolocationDataAction = {\n type: `GeolocationApiService:fetchGeolocationData`;\n handler: GeolocationApiService['fetchGeolocationData'];\n};\n\n/**\n * Union of all GeolocationApiService action types.\n */\nexport type GeolocationApiServiceMethodActions =\n | GeolocationApiServiceFetchGeolocationAction\n | GeolocationApiServiceFetchGeolocationDataAction;\n"]}
1
+ {"version":3,"file":"geolocation-api-service-method-action-types.js","sourceRoot":"","sources":["../../src/geolocation-api-service/geolocation-api-service-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * This file is auto generated.\n * Do not edit manually.\n */\n\nimport type { GeolocationApiService } from './geolocation-api-service.js';\n\n/**\n * Returns the geolocation code. Serves from cache when the TTL has not\n * expired, otherwise performs a network fetch. Concurrent callers are\n * deduplicated to a single in-flight request.\n *\n * @param options - Optional fetch options.\n * @param options.bypassCache - When true, invalidates the TTL cache. If a\n * request is already in-flight it will be reused (deduplication always\n * applies).\n * @returns An ISO 3166-2 location code (e.g. `US`, `US-NY`, `CA-ON`), or\n * {@link UNKNOWN_LOCATION} when the API returns an empty or invalid body.\n */\nexport type GeolocationApiServiceFetchGeolocationAction = {\n type: `GeolocationApiService:fetchGeolocation`;\n handler: GeolocationApiService['fetchGeolocation'];\n};\n\n/**\n * Returns the country, region, and timezone for the current client. Serves\n * from cache when the TTL has not expired, otherwise performs a network\n * fetch. Concurrent callers are deduplicated to a single in-flight request.\n *\n * @param options - Optional fetch options.\n * @param options.bypassCache - When true, invalidates the TTL cache. If a\n * request is already in-flight it will be reused (deduplication always\n * applies).\n * @returns The geolocation data, where each field is `null` when the API\n * omits it or returns a value that fails validation.\n */\nexport type GeolocationApiServiceFetchGeolocationDataAction = {\n type: `GeolocationApiService:fetchGeolocationData`;\n handler: GeolocationApiService['fetchGeolocationData'];\n};\n\n/**\n * Union of all GeolocationApiService action types.\n */\nexport type GeolocationApiServiceMethodActions =\n | GeolocationApiServiceFetchGeolocationAction\n | GeolocationApiServiceFetchGeolocationDataAction;\n"]}
@@ -1,6 +1,6 @@
1
- import { BaseDataService } from '@metamask/base-data-service';
2
- import type { CreateServicePolicyOptions, DataServiceCacheUpdatedEvent, DataServiceGranularCacheUpdatedEvent, DataServiceInvalidateQueriesAction } from '@metamask/base-data-service';
1
+ import type { CreateServicePolicyOptions, ServicePolicy } from '@metamask/controller-utils';
3
2
  import type { Messenger } from '@metamask/messenger';
3
+ import type { IDisposable } from 'cockatiel';
4
4
  import { Env } from '../types.js';
5
5
  import type { GeolocationApiServiceMethodActions } from './geolocation-api-service-method-action-types.js';
6
6
  /**
@@ -46,32 +46,19 @@ export declare function getUnknownGeolocationData(): GeolocationData;
46
46
  * {@link UNKNOWN_LOCATION} when the country is unknown.
47
47
  */
48
48
  export declare function toLocationCode(data: GeolocationData): string;
49
- /**
50
- * Invalidates cached queries for {@link GeolocationApiService}.
51
- */
52
- export type GeolocationApiServiceInvalidateQueriesAction = DataServiceInvalidateQueriesAction<typeof serviceName>;
53
49
  /**
54
50
  * Actions that {@link GeolocationApiService} exposes to other consumers.
55
51
  */
56
- export type GeolocationApiServiceActions = GeolocationApiServiceMethodActions | GeolocationApiServiceInvalidateQueriesAction;
52
+ export type GeolocationApiServiceActions = GeolocationApiServiceMethodActions;
57
53
  /**
58
54
  * Actions from other messengers that {@link GeolocationApiServiceMessenger}
59
55
  * calls.
60
56
  */
61
57
  type AllowedActions = never;
62
- /**
63
- * Published when {@link GeolocationApiService}'s cache is updated.
64
- */
65
- export type GeolocationApiServiceCacheUpdatedEvent = DataServiceCacheUpdatedEvent<typeof serviceName>;
66
- /**
67
- * Published when a key within {@link GeolocationApiService}'s cache is
68
- * updated.
69
- */
70
- export type GeolocationApiServiceGranularCacheUpdatedEvent = DataServiceGranularCacheUpdatedEvent<typeof serviceName>;
71
58
  /**
72
59
  * Events that {@link GeolocationApiService} exposes to other consumers.
73
60
  */
74
- export type GeolocationApiServiceEvents = GeolocationApiServiceCacheUpdatedEvent | GeolocationApiServiceGranularCacheUpdatedEvent;
61
+ export type GeolocationApiServiceEvents = never;
75
62
  /**
76
63
  * Events from other messengers that {@link GeolocationApiService} subscribes
77
64
  * to.
@@ -86,7 +73,7 @@ export type GeolocationApiServiceMessenger = Messenger<typeof serviceName, Geolo
86
73
  * Options accepted by {@link GeolocationApiService.fetchGeolocation}.
87
74
  */
88
75
  export type FetchGeolocationOptions = {
89
- /** When true, ignores any cached result and fetches fresh data. */
76
+ /** When true, the TTL cache is invalidated so the next request fetches fresh data. */
90
77
  bypassCache?: boolean;
91
78
  };
92
79
  /**
@@ -96,14 +83,20 @@ export type FetchGeolocationOptions = {
96
83
  * Responsibilities:
97
84
  * - HTTP request to the geolocation endpoint (wrapped in a service policy)
98
85
  * - Response validation of the country, region, and timezone fields
86
+ * - TTL-based in-memory cache
87
+ * - Promise deduplication (concurrent callers share a single in-flight request)
99
88
  *
100
89
  * This class is intentionally not a controller: it does not manage UI state.
101
90
  * Its {@link fetchGeolocation} and {@link fetchGeolocationData} methods are
102
91
  * automatically registered on the messenger so that controllers and other
103
92
  * packages can call them directly.
104
93
  */
105
- export declare class GeolocationApiService extends BaseDataService<typeof serviceName, GeolocationApiServiceMessenger> {
94
+ export declare class GeolocationApiService {
106
95
  #private;
96
+ /**
97
+ * The name of the service.
98
+ */
99
+ readonly name: typeof serviceName;
107
100
  /**
108
101
  * Constructs a new {@link GeolocationApiService}.
109
102
  *
@@ -113,8 +106,7 @@ export declare class GeolocationApiService extends BaseDataService<typeof servic
113
106
  * Defaults to PRD.
114
107
  * @param args.fetch - A function that can be used to make an HTTP request.
115
108
  * Defaults to the global fetch.
116
- * @param args.ttlMs - How long a fetched result is served from cache, in
117
- * milliseconds. Defaults to 5 minutes.
109
+ * @param args.ttlMs - Cache TTL in milliseconds. Defaults to 5 minutes.
118
110
  * @param args.policyOptions - Options to pass to `createServicePolicy`, which
119
111
  * is used to wrap each request. See {@link CreateServicePolicyOptions}.
120
112
  */
@@ -125,13 +117,42 @@ export declare class GeolocationApiService extends BaseDataService<typeof servic
125
117
  ttlMs?: number;
126
118
  policyOptions?: CreateServicePolicyOptions;
127
119
  });
120
+ /**
121
+ * Registers a handler that will be called after a request returns a 5xx
122
+ * response, causing a retry.
123
+ *
124
+ * @param listener - The handler to be called.
125
+ * @returns An object that can be used to unregister the handler.
126
+ * @see {@link createServicePolicy}
127
+ */
128
+ onRetry(listener: Parameters<ServicePolicy['onRetry']>[0]): IDisposable;
129
+ /**
130
+ * Registers a handler that will be called after a set number of retry rounds
131
+ * prove that requests to the API endpoint consistently return a 5xx response.
132
+ *
133
+ * @param listener - The handler to be called.
134
+ * @returns An object that can be used to unregister the handler.
135
+ * @see {@link createServicePolicy}
136
+ */
137
+ onBreak(listener: Parameters<ServicePolicy['onBreak']>[0]): IDisposable;
138
+ /**
139
+ * Registers a handler that will be called when requests are consistently
140
+ * failing or when a successful request takes longer than the degraded
141
+ * threshold.
142
+ *
143
+ * @param listener - The handler to be called.
144
+ * @returns An object that can be used to unregister the handler.
145
+ */
146
+ onDegraded(listener: Parameters<ServicePolicy['onDegraded']>[0]): IDisposable;
128
147
  /**
129
148
  * Returns the geolocation code. Serves from cache when the TTL has not
130
- * expired, otherwise performs a network fetch.
149
+ * expired, otherwise performs a network fetch. Concurrent callers are
150
+ * deduplicated to a single in-flight request.
131
151
  *
132
152
  * @param options - Optional fetch options.
133
- * @param options.bypassCache - When true, ignores any cached result and
134
- * fetches fresh data.
153
+ * @param options.bypassCache - When true, invalidates the TTL cache. If a
154
+ * request is already in-flight it will be reused (deduplication always
155
+ * applies).
135
156
  * @returns An ISO 3166-2 location code (e.g. `US`, `US-NY`, `CA-ON`), or
136
157
  * {@link UNKNOWN_LOCATION} when the API returns an empty or invalid body.
137
158
  */
@@ -139,11 +160,12 @@ export declare class GeolocationApiService extends BaseDataService<typeof servic
139
160
  /**
140
161
  * Returns the country, region, and timezone for the current client. Serves
141
162
  * from cache when the TTL has not expired, otherwise performs a network
142
- * fetch.
163
+ * fetch. Concurrent callers are deduplicated to a single in-flight request.
143
164
  *
144
165
  * @param options - Optional fetch options.
145
- * @param options.bypassCache - When true, ignores any cached result and
146
- * fetches fresh data.
166
+ * @param options.bypassCache - When true, invalidates the TTL cache. If a
167
+ * request is already in-flight it will be reused (deduplication always
168
+ * applies).
147
169
  * @returns The geolocation data, where each field is `null` when the API
148
170
  * omits it or returns a value that fails validation.
149
171
  */
@@ -1 +1 @@
1
- {"version":3,"file":"geolocation-api-service.d.ts","sourceRoot":"","sources":["../../src/geolocation-api-service/geolocation-api-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EACV,0BAA0B,EAC1B,4BAA4B,EAC5B,oCAAoC,EACpC,kCAAkC,EACnC,MAAM,6BAA6B,CAAC;AAErC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAQrD,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,KAAK,EAAE,kCAAkC,EAAE,MAAM,kDAAkD,CAAC;AAc3G;;;GAGG;AACH,eAAO,MAAM,WAAW,0BAA0B,CAAC;AAEnD;;;GAGG;AACH,eAAO,MAAM,gBAAgB,YAAY,CAAC;AAE1C;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,yDAAyD;IACzD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,eAAe,CAE3D;AAmBD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAU5D;AASD;;GAEG;AACH,MAAM,MAAM,4CAA4C,GACtD,kCAAkC,CAAC,OAAO,WAAW,CAAC,CAAC;AAEzD;;GAEG;AACH,MAAM,MAAM,4BAA4B,GACpC,kCAAkC,GAClC,4CAA4C,CAAC;AAEjD;;;GAGG;AACH,KAAK,cAAc,GAAG,KAAK,CAAC;AAE5B;;GAEG;AACH,MAAM,MAAM,sCAAsC,GAChD,4BAA4B,CAAC,OAAO,WAAW,CAAC,CAAC;AAEnD;;;GAGG;AACH,MAAM,MAAM,8CAA8C,GACxD,oCAAoC,CAAC,OAAO,WAAW,CAAC,CAAC;AAE3D;;GAEG;AACH,MAAM,MAAM,2BAA2B,GACnC,sCAAsC,GACtC,8CAA8C,CAAC;AAEnD;;;GAGG;AACH,KAAK,aAAa,GAAG,KAAK,CAAC;AAE3B;;;GAGG;AACH,MAAM,MAAM,8BAA8B,GAAG,SAAS,CACpD,OAAO,WAAW,EAClB,4BAA4B,GAAG,cAAc,EAC7C,2BAA2B,GAAG,aAAa,CAC5C,CAAC;AA+EF;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,mEAAmE;IACnE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,qBAAa,qBAAsB,SAAQ,eAAe,CACxD,OAAO,WAAW,EAClB,8BAA8B,CAC/B;;IAOC;;;;;;;;;;;;;OAaG;IACH,YAAY,EACV,SAAS,EACT,GAAa,EACb,KAAK,EAAE,aAAgC,EACvC,KAAK,EACL,aAAkB,GACnB,EAAE;QACD,SAAS,EAAE,8BAA8B,CAAC;QAC1C,GAAG,CAAC,EAAE,GAAG,CAAC;QACV,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,aAAa,CAAC,EAAE,0BAA0B,CAAC;KAC5C,EAUA;IAED;;;;;;;;;OASG;IACG,gBAAgB,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,MAAM,CAAC,CAEzE;IAED;;;;;;;;;;OAUG;IACG,oBAAoB,CACxB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,eAAe,CAAC,CAmC1B;CACF"}
1
+ {"version":3,"file":"geolocation-api-service.d.ts","sourceRoot":"","sources":["../../src/geolocation-api-service/geolocation-api-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,aAAa,EACd,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,KAAK,EAAE,kCAAkC,EAAE,MAAM,kDAAkD,CAAC;AAc3G;;;GAGG;AACH,eAAO,MAAM,WAAW,0BAA0B,CAAC;AAEnD;;;GAGG;AACH,eAAO,MAAM,gBAAgB,YAAY,CAAC;AAE1C;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,yDAAyD;IACzD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,eAAe,CAE3D;AAUD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAU5D;AASD;;GAEG;AACH,MAAM,MAAM,4BAA4B,GAAG,kCAAkC,CAAC;AAE9E;;;GAGG;AACH,KAAK,cAAc,GAAG,KAAK,CAAC;AAE5B;;GAEG;AACH,MAAM,MAAM,2BAA2B,GAAG,KAAK,CAAC;AAEhD;;;GAGG;AACH,KAAK,aAAa,GAAG,KAAK,CAAC;AAE3B;;;GAGG;AACH,MAAM,MAAM,8BAA8B,GAAG,SAAS,CACpD,OAAO,WAAW,EAClB,4BAA4B,GAAG,cAAc,EAC7C,2BAA2B,GAAG,aAAa,CAC5C,CAAC;AA+EF;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,sFAAsF;IACtF,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,qBAAa,qBAAqB;;IAChC;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,OAAO,WAAW,CAAC;IAuBlC;;;;;;;;;;;;OAYG;IACH,YAAY,EACV,SAAS,EACT,GAAa,EACb,KAAK,EAAE,aAAgC,EACvC,KAAK,EACL,aAAkB,GACnB,EAAE;QACD,SAAS,EAAE,8BAA8B,CAAC;QAC1C,GAAG,CAAC,EAAE,GAAG,CAAC;QACV,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,aAAa,CAAC,EAAE,0BAA0B,CAAC;KAC5C,EAYA;IAED;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,EAAE,UAAU,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,WAAW,CAEtE;IAED;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,EAAE,UAAU,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,WAAW,CAEtE;IAED;;;;;;;OAOG;IACH,UAAU,CACR,QAAQ,EAAE,UAAU,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,GACnD,WAAW,CAEb;IAED;;;;;;;;;;;OAWG;IACG,gBAAgB,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,MAAM,CAAC,CAEzE;IAED;;;;;;;;;;;OAWG;IACG,oBAAoB,CACxB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,eAAe,CAAC,CAqB1B;CAoDF"}
@@ -1,6 +1,4 @@
1
- import { BaseDataService } from '@metamask/base-data-service';
2
- import { HttpError } from '@metamask/controller-utils';
3
- import { nullable, object, pattern as matches, string, } from '@metamask/superstruct';
1
+ import { createServicePolicy, HttpError } from '@metamask/controller-utils';
4
2
  import { Env } from '../types.js';
5
3
  const DEFAULT_TTL_MS = 5 * 60 * 1000;
6
4
  const ENDPOINT_PATH = '/v2/geolocation';
@@ -32,14 +30,6 @@ export function getUnknownGeolocationData() {
32
30
  * the United States and Canada (e.g. `US-NY`, `CA-ON`) and returned the country
33
31
  * alone for everywhere else.
34
32
  */
35
- /**
36
- * Describes the validated shape of {@link GeolocationData}.
37
- */
38
- const GeolocationDataStruct = object({
39
- country: nullable(matches(string(), COUNTRY_PATTERN)),
40
- region: nullable(matches(string(), REGION_PATTERN)),
41
- timezone: nullable(matches(string(), TIMEZONE_PATTERN)),
42
- });
43
33
  const REGION_APPENDED_COUNTRIES = new Set(['US', 'CA']);
44
34
  /**
45
35
  * Converts geolocation data to a location code.
@@ -137,16 +127,32 @@ function parseGeolocationResponse(raw) {
137
127
  * Responsibilities:
138
128
  * - HTTP request to the geolocation endpoint (wrapped in a service policy)
139
129
  * - Response validation of the country, region, and timezone fields
130
+ * - TTL-based in-memory cache
131
+ * - Promise deduplication (concurrent callers share a single in-flight request)
140
132
  *
141
133
  * This class is intentionally not a controller: it does not manage UI state.
142
134
  * Its {@link fetchGeolocation} and {@link fetchGeolocationData} methods are
143
135
  * automatically registered on the messenger so that controllers and other
144
136
  * packages can call them directly.
145
137
  */
146
- export class GeolocationApiService extends BaseDataService {
138
+ export class GeolocationApiService {
139
+ /**
140
+ * The name of the service.
141
+ */
142
+ name;
143
+ #messenger;
147
144
  #fetch;
148
145
  #url;
149
146
  #ttlMs;
147
+ /**
148
+ * The policy that wraps each HTTP request.
149
+ *
150
+ * @see {@link createServicePolicy}
151
+ */
152
+ #policy;
153
+ #cachedGeolocation = getUnknownGeolocationData();
154
+ #lastFetchedAt = null;
155
+ #fetchPromise = null;
150
156
  /**
151
157
  * Constructs a new {@link GeolocationApiService}.
152
158
  *
@@ -156,25 +162,61 @@ export class GeolocationApiService extends BaseDataService {
156
162
  * Defaults to PRD.
157
163
  * @param args.fetch - A function that can be used to make an HTTP request.
158
164
  * Defaults to the global fetch.
159
- * @param args.ttlMs - How long a fetched result is served from cache, in
160
- * milliseconds. Defaults to 5 minutes.
165
+ * @param args.ttlMs - Cache TTL in milliseconds. Defaults to 5 minutes.
161
166
  * @param args.policyOptions - Options to pass to `createServicePolicy`, which
162
167
  * is used to wrap each request. See {@link CreateServicePolicyOptions}.
163
168
  */
164
169
  constructor({ messenger, env = Env.PRD, fetch: fetchFunction = globalThis.fetch, ttlMs, policyOptions = {}, }) {
165
- super({ name: serviceName, messenger, policyOptions });
170
+ this.name = serviceName;
171
+ this.#messenger = messenger;
166
172
  this.#url = getGeolocationUrl(env);
167
173
  this.#fetch = fetchFunction;
168
174
  this.#ttlMs = ttlMs ?? DEFAULT_TTL_MS;
169
- this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
175
+ this.#policy = createServicePolicy(policyOptions);
176
+ this.#messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
177
+ }
178
+ /**
179
+ * Registers a handler that will be called after a request returns a 5xx
180
+ * response, causing a retry.
181
+ *
182
+ * @param listener - The handler to be called.
183
+ * @returns An object that can be used to unregister the handler.
184
+ * @see {@link createServicePolicy}
185
+ */
186
+ onRetry(listener) {
187
+ return this.#policy.onRetry(listener);
188
+ }
189
+ /**
190
+ * Registers a handler that will be called after a set number of retry rounds
191
+ * prove that requests to the API endpoint consistently return a 5xx response.
192
+ *
193
+ * @param listener - The handler to be called.
194
+ * @returns An object that can be used to unregister the handler.
195
+ * @see {@link createServicePolicy}
196
+ */
197
+ onBreak(listener) {
198
+ return this.#policy.onBreak(listener);
199
+ }
200
+ /**
201
+ * Registers a handler that will be called when requests are consistently
202
+ * failing or when a successful request takes longer than the degraded
203
+ * threshold.
204
+ *
205
+ * @param listener - The handler to be called.
206
+ * @returns An object that can be used to unregister the handler.
207
+ */
208
+ onDegraded(listener) {
209
+ return this.#policy.onDegraded(listener);
170
210
  }
171
211
  /**
172
212
  * Returns the geolocation code. Serves from cache when the TTL has not
173
- * expired, otherwise performs a network fetch.
213
+ * expired, otherwise performs a network fetch. Concurrent callers are
214
+ * deduplicated to a single in-flight request.
174
215
  *
175
216
  * @param options - Optional fetch options.
176
- * @param options.bypassCache - When true, ignores any cached result and
177
- * fetches fresh data.
217
+ * @param options.bypassCache - When true, invalidates the TTL cache. If a
218
+ * request is already in-flight it will be reused (deduplication always
219
+ * applies).
178
220
  * @returns An ISO 3166-2 location code (e.g. `US`, `US-NY`, `CA-ON`), or
179
221
  * {@link UNKNOWN_LOCATION} when the API returns an empty or invalid body.
180
222
  */
@@ -184,38 +226,68 @@ export class GeolocationApiService extends BaseDataService {
184
226
  /**
185
227
  * Returns the country, region, and timezone for the current client. Serves
186
228
  * from cache when the TTL has not expired, otherwise performs a network
187
- * fetch.
229
+ * fetch. Concurrent callers are deduplicated to a single in-flight request.
188
230
  *
189
231
  * @param options - Optional fetch options.
190
- * @param options.bypassCache - When true, ignores any cached result and
191
- * fetches fresh data.
232
+ * @param options.bypassCache - When true, invalidates the TTL cache. If a
233
+ * request is already in-flight it will be reused (deduplication always
234
+ * applies).
192
235
  * @returns The geolocation data, where each field is `null` when the API
193
236
  * omits it or returns a value that fails validation.
194
237
  */
195
238
  async fetchGeolocationData(options) {
196
- const queryKey = [`${this.name}:fetchGeolocationData`];
197
239
  if (options?.bypassCache) {
198
- await this.invalidateQueries({ queryKey, refetchType: 'none' });
240
+ this.#lastFetchedAt = null;
241
+ }
242
+ if (this.#isCacheValid()) {
243
+ return this.#cachedGeolocation;
199
244
  }
200
- const geolocation = await this.fetchQuery({
201
- queryKey,
202
- staleTime: this.#ttlMs,
203
- responseStruct: GeolocationDataStruct,
204
- gcTime: this.#ttlMs,
205
- queryFn: async () => {
206
- const response = await this.#fetch(this.#url);
207
- if (!response.ok) {
208
- throw new HttpError(response.status, `Geolocation fetch failed: ${response.status}`);
209
- }
210
- return parseGeolocationResponse((await response.text()).trim());
211
- },
245
+ if (this.#fetchPromise) {
246
+ return this.#fetchPromise;
247
+ }
248
+ const promise = this.#performFetch();
249
+ this.#fetchPromise = promise;
250
+ try {
251
+ return await promise;
252
+ }
253
+ finally {
254
+ this.#fetchPromise = null;
255
+ }
256
+ }
257
+ /**
258
+ * Checks whether the cached geolocation is still within the TTL window.
259
+ *
260
+ * @returns True if the cache is valid.
261
+ */
262
+ #isCacheValid() {
263
+ return (this.#lastFetchedAt !== null &&
264
+ Date.now() - this.#lastFetchedAt < this.#ttlMs);
265
+ }
266
+ /**
267
+ * Performs the actual HTTP fetch, wrapped in the service policy for automatic
268
+ * retry and circuit-breaking, and validates the response.
269
+ *
270
+ * @returns The validated geolocation data.
271
+ */
272
+ async #performFetch() {
273
+ const response = await this.#policy.execute(async () => {
274
+ const localResponse = await this.#fetch(this.#url);
275
+ if (!localResponse.ok) {
276
+ throw new HttpError(localResponse.status, `Geolocation fetch failed: ${localResponse.status}`);
277
+ }
278
+ return localResponse;
212
279
  });
213
- // A partially-known result is worth keeping for the whole TTL. A
214
- // fully-unknown one is marked stale so the next call retries it.
215
- if (geolocation.country === null &&
216
- geolocation.region === null &&
217
- geolocation.timezone === null) {
218
- await this.invalidateQueries({ queryKey, refetchType: 'none' });
280
+ const geolocation = parseGeolocationResponse((await response.text()).trim());
281
+ // Cache whenever at least one field resolved. A partially-known result
282
+ // (e.g. timezone without a valid country) is still worth caching so we do
283
+ // not re-fetch it within the TTL window; only a fully-unknown response is
284
+ // left uncached so it can be retried.
285
+ const hasKnownField = geolocation.country !== null ||
286
+ geolocation.region !== null ||
287
+ geolocation.timezone !== null;
288
+ if (hasKnownField) {
289
+ this.#cachedGeolocation = geolocation;
290
+ this.#lastFetchedAt = Date.now();
219
291
  }
220
292
  return geolocation;
221
293
  }
@@ -1 +1 @@
1
- {"version":3,"file":"geolocation-api-service.js","sourceRoot":"","sources":["../../src/geolocation-api-service/geolocation-api-service.ts"],"names":[],"mappings":"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","sourcesContent":["import { BaseDataService } from '@metamask/base-data-service';\nimport type {\n CreateServicePolicyOptions,\n DataServiceCacheUpdatedEvent,\n DataServiceGranularCacheUpdatedEvent,\n DataServiceInvalidateQueriesAction,\n} from '@metamask/base-data-service';\nimport { HttpError } from '@metamask/controller-utils';\nimport type { Messenger } from '@metamask/messenger';\nimport {\n nullable,\n object,\n pattern as matches,\n string,\n} from '@metamask/superstruct';\n\nimport { Env } from '../types.js';\nimport type { GeolocationApiServiceMethodActions } from './geolocation-api-service-method-action-types.js';\n\nconst DEFAULT_TTL_MS = 5 * 60 * 1000;\n\nconst ENDPOINT_PATH = '/v2/geolocation';\n\nconst COUNTRY_PATTERN = /^[A-Z]{2}$/u;\n\nconst REGION_PATTERN = /^[A-Z0-9]{1,3}$/u;\n\nconst TIMEZONE_PATTERN = /^[A-Za-z][A-Za-z0-9_+-]*(?:\\/[A-Za-z0-9_+-]+)*$/u;\n\n// === GENERAL ===\n\n/**\n * The name of the {@link GeolocationApiService}, used to namespace the\n * service's actions and events.\n */\nexport const serviceName = 'GeolocationApiService';\n\n/**\n * Sentinel value used when the geolocation has not been determined yet or when\n * the API returns an empty / invalid response.\n */\nexport const UNKNOWN_LOCATION = 'UNKNOWN';\n\n/**\n * Geolocation details returned by the geolocation API.\n *\n * Each field is `null` when the API omits it or returns a value that fails\n * validation.\n */\nexport type GeolocationData = {\n /** ISO 3166-1 alpha-2 country code (e.g. `US`, `FR`). */\n country: string | null;\n /** ISO 3166-2 subdivision code without the country prefix (e.g. `WA`). */\n region: string | null;\n /** IANA time zone name (e.g. `America/Los_Angeles`). */\n timezone: string | null;\n};\n\n/**\n * Constructs a {@link GeolocationData} with no known fields.\n *\n * @returns Geolocation data where every field is `null`.\n */\nexport function getUnknownGeolocationData(): GeolocationData {\n return { country: null, region: null, timezone: null };\n}\n\n/**\n * Country codes for which the location code includes the region. This mirrors\n * the legacy `v1` geolocation endpoint, which only appended the subdivision for\n * the United States and Canada (e.g. `US-NY`, `CA-ON`) and returned the country\n * alone for everywhere else.\n */\n/**\n * Describes the validated shape of {@link GeolocationData}.\n */\nconst GeolocationDataStruct = object({\n country: nullable(matches(string(), COUNTRY_PATTERN)),\n region: nullable(matches(string(), REGION_PATTERN)),\n timezone: nullable(matches(string(), TIMEZONE_PATTERN)),\n});\n\nconst REGION_APPENDED_COUNTRIES = new Set(['US', 'CA']);\n\n/**\n * Converts geolocation data to a location code.\n *\n * To preserve backwards compatibility with the legacy `v1` endpoint, the region\n * is appended only for {@link REGION_APPENDED_COUNTRIES} (e.g. `US-NY`,\n * `CA-ON`); all other countries return the country code alone (e.g. `FR`), even\n * when a region is known.\n *\n * @param data - The geolocation data to convert.\n * @returns The location code (e.g. `US-NY`, `FR`), or\n * {@link UNKNOWN_LOCATION} when the country is unknown.\n */\nexport function toLocationCode(data: GeolocationData): string {\n if (data.country === null) {\n return UNKNOWN_LOCATION;\n }\n\n if (data.region !== null && REGION_APPENDED_COUNTRIES.has(data.country)) {\n return `${data.country}-${data.region}`;\n }\n\n return data.country;\n}\n\n// === MESSENGER ===\n\nconst MESSENGER_EXPOSED_METHODS = [\n 'fetchGeolocation',\n 'fetchGeolocationData',\n] as const;\n\n/**\n * Invalidates cached queries for {@link GeolocationApiService}.\n */\nexport type GeolocationApiServiceInvalidateQueriesAction =\n DataServiceInvalidateQueriesAction<typeof serviceName>;\n\n/**\n * Actions that {@link GeolocationApiService} exposes to other consumers.\n */\nexport type GeolocationApiServiceActions =\n | GeolocationApiServiceMethodActions\n | GeolocationApiServiceInvalidateQueriesAction;\n\n/**\n * Actions from other messengers that {@link GeolocationApiServiceMessenger}\n * calls.\n */\ntype AllowedActions = never;\n\n/**\n * Published when {@link GeolocationApiService}'s cache is updated.\n */\nexport type GeolocationApiServiceCacheUpdatedEvent =\n DataServiceCacheUpdatedEvent<typeof serviceName>;\n\n/**\n * Published when a key within {@link GeolocationApiService}'s cache is\n * updated.\n */\nexport type GeolocationApiServiceGranularCacheUpdatedEvent =\n DataServiceGranularCacheUpdatedEvent<typeof serviceName>;\n\n/**\n * Events that {@link GeolocationApiService} exposes to other consumers.\n */\nexport type GeolocationApiServiceEvents =\n | GeolocationApiServiceCacheUpdatedEvent\n | GeolocationApiServiceGranularCacheUpdatedEvent;\n\n/**\n * Events from other messengers that {@link GeolocationApiService} subscribes\n * to.\n */\ntype AllowedEvents = never;\n\n/**\n * The messenger restricted to actions and events accessed by\n * {@link GeolocationApiService}.\n */\nexport type GeolocationApiServiceMessenger = Messenger<\n typeof serviceName,\n GeolocationApiServiceActions | AllowedActions,\n GeolocationApiServiceEvents | AllowedEvents\n>;\n\n// === SERVICE DEFINITION ===\n\n/**\n * Returns the base URL for the geolocation API for the given environment.\n *\n * Served by API Platform's `geolocation-api` service, not the legacy\n * Ramps-owned `on-ramp` endpoint this previously pointed to. API Platform has\n * not yet provisioned a dedicated UAT deployment for this service, so UAT\n * temporarily resolves to the production URL until one exists.\n *\n * @param env - The environment to get the URL for.\n * @returns The full URL for the geolocation endpoint.\n */\nfunction getGeolocationUrl(env: Env): string {\n const envPrefix = env === Env.DEV ? 'dev-' : '';\n return `https://geolocation.${envPrefix}api.cx.metamask.io${ENDPOINT_PATH}`;\n}\n\n/**\n * Reads a string field from a parsed response body, keeping it only when it\n * matches the expected format.\n *\n * @param body - The parsed response body.\n * @param field - The name of the field to read.\n * @param pattern - The pattern the field value must match.\n * @returns The trimmed field value, or `null` when it is missing or invalid.\n */\nfunction readValidatedField(\n body: Record<string, unknown>,\n field: string,\n pattern: RegExp,\n): string | null {\n const value = body[field];\n\n if (typeof value !== 'string') {\n return null;\n }\n\n const trimmed = value.trim();\n\n return pattern.test(trimmed) ? trimmed : null;\n}\n\n/**\n * Parses and validates the geolocation API response body.\n *\n * The endpoint is expected to return JSON such as\n * `{\"country\":\"US\",\"region\":\"WA\",\"timezone\":\"America/Los_Angeles\"}`. Anything\n * that cannot be parsed, or any individual field that fails validation, is\n * reported as unknown rather than throwing, so that consumers can keep working\n * with partial or missing data.\n *\n * @param raw - The raw response body.\n * @returns The validated geolocation data.\n */\nfunction parseGeolocationResponse(raw: string): GeolocationData {\n let body: unknown;\n\n try {\n body = JSON.parse(raw);\n } catch {\n return getUnknownGeolocationData();\n }\n\n if (typeof body !== 'object' || body === null || Array.isArray(body)) {\n return getUnknownGeolocationData();\n }\n\n const record = body as Record<string, unknown>;\n\n return {\n country: readValidatedField(record, 'country', COUNTRY_PATTERN),\n region: readValidatedField(record, 'region', REGION_PATTERN),\n timezone: readValidatedField(record, 'timezone', TIMEZONE_PATTERN),\n };\n}\n\n/**\n * Options accepted by {@link GeolocationApiService.fetchGeolocation}.\n */\nexport type FetchGeolocationOptions = {\n /** When true, ignores any cached result and fetches fresh data. */\n bypassCache?: boolean;\n};\n\n/**\n * Low-level data service that fetches geolocation details from the geolocation\n * API.\n *\n * Responsibilities:\n * - HTTP request to the geolocation endpoint (wrapped in a service policy)\n * - Response validation of the country, region, and timezone fields\n *\n * This class is intentionally not a controller: it does not manage UI state.\n * Its {@link fetchGeolocation} and {@link fetchGeolocationData} methods are\n * automatically registered on the messenger so that controllers and other\n * packages can call them directly.\n */\nexport class GeolocationApiService extends BaseDataService<\n typeof serviceName,\n GeolocationApiServiceMessenger\n> {\n readonly #fetch: typeof globalThis.fetch;\n\n readonly #url: string;\n\n readonly #ttlMs: number;\n\n /**\n * Constructs a new {@link GeolocationApiService}.\n *\n * @param args - The constructor arguments.\n * @param args.messenger - The messenger suited for this service.\n * @param args.env - The environment to determine the correct API endpoint.\n * Defaults to PRD.\n * @param args.fetch - A function that can be used to make an HTTP request.\n * Defaults to the global fetch.\n * @param args.ttlMs - How long a fetched result is served from cache, in\n * milliseconds. Defaults to 5 minutes.\n * @param args.policyOptions - Options to pass to `createServicePolicy`, which\n * is used to wrap each request. See {@link CreateServicePolicyOptions}.\n */\n constructor({\n messenger,\n env = Env.PRD,\n fetch: fetchFunction = globalThis.fetch,\n ttlMs,\n policyOptions = {},\n }: {\n messenger: GeolocationApiServiceMessenger;\n env?: Env;\n fetch?: typeof fetch;\n ttlMs?: number;\n policyOptions?: CreateServicePolicyOptions;\n }) {\n super({ name: serviceName, messenger, policyOptions });\n this.#url = getGeolocationUrl(env);\n this.#fetch = fetchFunction;\n this.#ttlMs = ttlMs ?? DEFAULT_TTL_MS;\n\n this.messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n }\n\n /**\n * Returns the geolocation code. Serves from cache when the TTL has not\n * expired, otherwise performs a network fetch.\n *\n * @param options - Optional fetch options.\n * @param options.bypassCache - When true, ignores any cached result and\n * fetches fresh data.\n * @returns An ISO 3166-2 location code (e.g. `US`, `US-NY`, `CA-ON`), or\n * {@link UNKNOWN_LOCATION} when the API returns an empty or invalid body.\n */\n async fetchGeolocation(options?: FetchGeolocationOptions): Promise<string> {\n return toLocationCode(await this.fetchGeolocationData(options));\n }\n\n /**\n * Returns the country, region, and timezone for the current client. Serves\n * from cache when the TTL has not expired, otherwise performs a network\n * fetch.\n *\n * @param options - Optional fetch options.\n * @param options.bypassCache - When true, ignores any cached result and\n * fetches fresh data.\n * @returns The geolocation data, where each field is `null` when the API\n * omits it or returns a value that fails validation.\n */\n async fetchGeolocationData(\n options?: FetchGeolocationOptions,\n ): Promise<GeolocationData> {\n const queryKey = [`${this.name}:fetchGeolocationData`] as const;\n\n if (options?.bypassCache) {\n await this.invalidateQueries({ queryKey, refetchType: 'none' });\n }\n\n const geolocation = await this.fetchQuery({\n queryKey,\n staleTime: this.#ttlMs,\n responseStruct: GeolocationDataStruct,\n gcTime: this.#ttlMs,\n queryFn: async () => {\n const response = await this.#fetch(this.#url);\n if (!response.ok) {\n throw new HttpError(\n response.status,\n `Geolocation fetch failed: ${response.status}`,\n );\n }\n return parseGeolocationResponse((await response.text()).trim());\n },\n });\n\n // A partially-known result is worth keeping for the whole TTL. A\n // fully-unknown one is marked stale so the next call retries it.\n if (\n geolocation.country === null &&\n geolocation.region === null &&\n geolocation.timezone === null\n ) {\n await this.invalidateQueries({ queryKey, refetchType: 'none' });\n }\n\n return geolocation;\n }\n}\n"]}
1
+ {"version":3,"file":"geolocation-api-service.js","sourceRoot":"","sources":["../../src/geolocation-api-service/geolocation-api-service.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n CreateServicePolicyOptions,\n ServicePolicy,\n} from '@metamask/controller-utils';\nimport { createServicePolicy, HttpError } from '@metamask/controller-utils';\nimport type { Messenger } from '@metamask/messenger';\nimport type { IDisposable } from 'cockatiel';\n\nimport { Env } from '../types.js';\nimport type { GeolocationApiServiceMethodActions } from './geolocation-api-service-method-action-types.js';\n\nconst DEFAULT_TTL_MS = 5 * 60 * 1000;\n\nconst ENDPOINT_PATH = '/v2/geolocation';\n\nconst COUNTRY_PATTERN = /^[A-Z]{2}$/u;\n\nconst REGION_PATTERN = /^[A-Z0-9]{1,3}$/u;\n\nconst TIMEZONE_PATTERN = /^[A-Za-z][A-Za-z0-9_+-]*(?:\\/[A-Za-z0-9_+-]+)*$/u;\n\n// === GENERAL ===\n\n/**\n * The name of the {@link GeolocationApiService}, used to namespace the\n * service's actions and events.\n */\nexport const serviceName = 'GeolocationApiService';\n\n/**\n * Sentinel value used when the geolocation has not been determined yet or when\n * the API returns an empty / invalid response.\n */\nexport const UNKNOWN_LOCATION = 'UNKNOWN';\n\n/**\n * Geolocation details returned by the geolocation API.\n *\n * Each field is `null` when the API omits it or returns a value that fails\n * validation.\n */\nexport type GeolocationData = {\n /** ISO 3166-1 alpha-2 country code (e.g. `US`, `FR`). */\n country: string | null;\n /** ISO 3166-2 subdivision code without the country prefix (e.g. `WA`). */\n region: string | null;\n /** IANA time zone name (e.g. `America/Los_Angeles`). */\n timezone: string | null;\n};\n\n/**\n * Constructs a {@link GeolocationData} with no known fields.\n *\n * @returns Geolocation data where every field is `null`.\n */\nexport function getUnknownGeolocationData(): GeolocationData {\n return { country: null, region: null, timezone: null };\n}\n\n/**\n * Country codes for which the location code includes the region. This mirrors\n * the legacy `v1` geolocation endpoint, which only appended the subdivision for\n * the United States and Canada (e.g. `US-NY`, `CA-ON`) and returned the country\n * alone for everywhere else.\n */\nconst REGION_APPENDED_COUNTRIES = new Set(['US', 'CA']);\n\n/**\n * Converts geolocation data to a location code.\n *\n * To preserve backwards compatibility with the legacy `v1` endpoint, the region\n * is appended only for {@link REGION_APPENDED_COUNTRIES} (e.g. `US-NY`,\n * `CA-ON`); all other countries return the country code alone (e.g. `FR`), even\n * when a region is known.\n *\n * @param data - The geolocation data to convert.\n * @returns The location code (e.g. `US-NY`, `FR`), or\n * {@link UNKNOWN_LOCATION} when the country is unknown.\n */\nexport function toLocationCode(data: GeolocationData): string {\n if (data.country === null) {\n return UNKNOWN_LOCATION;\n }\n\n if (data.region !== null && REGION_APPENDED_COUNTRIES.has(data.country)) {\n return `${data.country}-${data.region}`;\n }\n\n return data.country;\n}\n\n// === MESSENGER ===\n\nconst MESSENGER_EXPOSED_METHODS = [\n 'fetchGeolocation',\n 'fetchGeolocationData',\n] as const;\n\n/**\n * Actions that {@link GeolocationApiService} exposes to other consumers.\n */\nexport type GeolocationApiServiceActions = GeolocationApiServiceMethodActions;\n\n/**\n * Actions from other messengers that {@link GeolocationApiServiceMessenger}\n * calls.\n */\ntype AllowedActions = never;\n\n/**\n * Events that {@link GeolocationApiService} exposes to other consumers.\n */\nexport type GeolocationApiServiceEvents = never;\n\n/**\n * Events from other messengers that {@link GeolocationApiService} subscribes\n * to.\n */\ntype AllowedEvents = never;\n\n/**\n * The messenger restricted to actions and events accessed by\n * {@link GeolocationApiService}.\n */\nexport type GeolocationApiServiceMessenger = Messenger<\n typeof serviceName,\n GeolocationApiServiceActions | AllowedActions,\n GeolocationApiServiceEvents | AllowedEvents\n>;\n\n// === SERVICE DEFINITION ===\n\n/**\n * Returns the base URL for the geolocation API for the given environment.\n *\n * Served by API Platform's `geolocation-api` service, not the legacy\n * Ramps-owned `on-ramp` endpoint this previously pointed to. API Platform has\n * not yet provisioned a dedicated UAT deployment for this service, so UAT\n * temporarily resolves to the production URL until one exists.\n *\n * @param env - The environment to get the URL for.\n * @returns The full URL for the geolocation endpoint.\n */\nfunction getGeolocationUrl(env: Env): string {\n const envPrefix = env === Env.DEV ? 'dev-' : '';\n return `https://geolocation.${envPrefix}api.cx.metamask.io${ENDPOINT_PATH}`;\n}\n\n/**\n * Reads a string field from a parsed response body, keeping it only when it\n * matches the expected format.\n *\n * @param body - The parsed response body.\n * @param field - The name of the field to read.\n * @param pattern - The pattern the field value must match.\n * @returns The trimmed field value, or `null` when it is missing or invalid.\n */\nfunction readValidatedField(\n body: Record<string, unknown>,\n field: string,\n pattern: RegExp,\n): string | null {\n const value = body[field];\n\n if (typeof value !== 'string') {\n return null;\n }\n\n const trimmed = value.trim();\n\n return pattern.test(trimmed) ? trimmed : null;\n}\n\n/**\n * Parses and validates the geolocation API response body.\n *\n * The endpoint is expected to return JSON such as\n * `{\"country\":\"US\",\"region\":\"WA\",\"timezone\":\"America/Los_Angeles\"}`. Anything\n * that cannot be parsed, or any individual field that fails validation, is\n * reported as unknown rather than throwing, so that consumers can keep working\n * with partial or missing data.\n *\n * @param raw - The raw response body.\n * @returns The validated geolocation data.\n */\nfunction parseGeolocationResponse(raw: string): GeolocationData {\n let body: unknown;\n\n try {\n body = JSON.parse(raw);\n } catch {\n return getUnknownGeolocationData();\n }\n\n if (typeof body !== 'object' || body === null || Array.isArray(body)) {\n return getUnknownGeolocationData();\n }\n\n const record = body as Record<string, unknown>;\n\n return {\n country: readValidatedField(record, 'country', COUNTRY_PATTERN),\n region: readValidatedField(record, 'region', REGION_PATTERN),\n timezone: readValidatedField(record, 'timezone', TIMEZONE_PATTERN),\n };\n}\n\n/**\n * Options accepted by {@link GeolocationApiService.fetchGeolocation}.\n */\nexport type FetchGeolocationOptions = {\n /** When true, the TTL cache is invalidated so the next request fetches fresh data. */\n bypassCache?: boolean;\n};\n\n/**\n * Low-level data service that fetches geolocation details from the geolocation\n * API.\n *\n * Responsibilities:\n * - HTTP request to the geolocation endpoint (wrapped in a service policy)\n * - Response validation of the country, region, and timezone fields\n * - TTL-based in-memory cache\n * - Promise deduplication (concurrent callers share a single in-flight request)\n *\n * This class is intentionally not a controller: it does not manage UI state.\n * Its {@link fetchGeolocation} and {@link fetchGeolocationData} methods are\n * automatically registered on the messenger so that controllers and other\n * packages can call them directly.\n */\nexport class GeolocationApiService {\n /**\n * The name of the service.\n */\n readonly name: typeof serviceName;\n\n readonly #messenger: GeolocationApiServiceMessenger;\n\n readonly #fetch: typeof globalThis.fetch;\n\n readonly #url: string;\n\n readonly #ttlMs: number;\n\n /**\n * The policy that wraps each HTTP request.\n *\n * @see {@link createServicePolicy}\n */\n readonly #policy: ServicePolicy;\n\n #cachedGeolocation: GeolocationData = getUnknownGeolocationData();\n\n #lastFetchedAt: number | null = null;\n\n #fetchPromise: Promise<GeolocationData> | null = null;\n\n /**\n * Constructs a new {@link GeolocationApiService}.\n *\n * @param args - The constructor arguments.\n * @param args.messenger - The messenger suited for this service.\n * @param args.env - The environment to determine the correct API endpoint.\n * Defaults to PRD.\n * @param args.fetch - A function that can be used to make an HTTP request.\n * Defaults to the global fetch.\n * @param args.ttlMs - Cache TTL in milliseconds. Defaults to 5 minutes.\n * @param args.policyOptions - Options to pass to `createServicePolicy`, which\n * is used to wrap each request. See {@link CreateServicePolicyOptions}.\n */\n constructor({\n messenger,\n env = Env.PRD,\n fetch: fetchFunction = globalThis.fetch,\n ttlMs,\n policyOptions = {},\n }: {\n messenger: GeolocationApiServiceMessenger;\n env?: Env;\n fetch?: typeof fetch;\n ttlMs?: number;\n policyOptions?: CreateServicePolicyOptions;\n }) {\n this.name = serviceName;\n this.#messenger = messenger;\n this.#url = getGeolocationUrl(env);\n this.#fetch = fetchFunction;\n this.#ttlMs = ttlMs ?? DEFAULT_TTL_MS;\n this.#policy = createServicePolicy(policyOptions);\n\n this.#messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n }\n\n /**\n * Registers a handler that will be called after a request returns a 5xx\n * response, causing a retry.\n *\n * @param listener - The handler to be called.\n * @returns An object that can be used to unregister the handler.\n * @see {@link createServicePolicy}\n */\n onRetry(listener: Parameters<ServicePolicy['onRetry']>[0]): IDisposable {\n return this.#policy.onRetry(listener);\n }\n\n /**\n * Registers a handler that will be called after a set number of retry rounds\n * prove that requests to the API endpoint consistently return a 5xx response.\n *\n * @param listener - The handler to be called.\n * @returns An object that can be used to unregister the handler.\n * @see {@link createServicePolicy}\n */\n onBreak(listener: Parameters<ServicePolicy['onBreak']>[0]): IDisposable {\n return this.#policy.onBreak(listener);\n }\n\n /**\n * Registers a handler that will be called when requests are consistently\n * failing or when a successful request takes longer than the degraded\n * threshold.\n *\n * @param listener - The handler to be called.\n * @returns An object that can be used to unregister the handler.\n */\n onDegraded(\n listener: Parameters<ServicePolicy['onDegraded']>[0],\n ): IDisposable {\n return this.#policy.onDegraded(listener);\n }\n\n /**\n * Returns the geolocation code. Serves from cache when the TTL has not\n * expired, otherwise performs a network fetch. Concurrent callers are\n * deduplicated to a single in-flight request.\n *\n * @param options - Optional fetch options.\n * @param options.bypassCache - When true, invalidates the TTL cache. If a\n * request is already in-flight it will be reused (deduplication always\n * applies).\n * @returns An ISO 3166-2 location code (e.g. `US`, `US-NY`, `CA-ON`), or\n * {@link UNKNOWN_LOCATION} when the API returns an empty or invalid body.\n */\n async fetchGeolocation(options?: FetchGeolocationOptions): Promise<string> {\n return toLocationCode(await this.fetchGeolocationData(options));\n }\n\n /**\n * Returns the country, region, and timezone for the current client. Serves\n * from cache when the TTL has not expired, otherwise performs a network\n * fetch. Concurrent callers are deduplicated to a single in-flight request.\n *\n * @param options - Optional fetch options.\n * @param options.bypassCache - When true, invalidates the TTL cache. If a\n * request is already in-flight it will be reused (deduplication always\n * applies).\n * @returns The geolocation data, where each field is `null` when the API\n * omits it or returns a value that fails validation.\n */\n async fetchGeolocationData(\n options?: FetchGeolocationOptions,\n ): Promise<GeolocationData> {\n if (options?.bypassCache) {\n this.#lastFetchedAt = null;\n }\n\n if (this.#isCacheValid()) {\n return this.#cachedGeolocation;\n }\n\n if (this.#fetchPromise) {\n return this.#fetchPromise;\n }\n\n const promise = this.#performFetch();\n this.#fetchPromise = promise;\n\n try {\n return await promise;\n } finally {\n this.#fetchPromise = null;\n }\n }\n\n /**\n * Checks whether the cached geolocation is still within the TTL window.\n *\n * @returns True if the cache is valid.\n */\n #isCacheValid(): boolean {\n return (\n this.#lastFetchedAt !== null &&\n Date.now() - this.#lastFetchedAt < this.#ttlMs\n );\n }\n\n /**\n * Performs the actual HTTP fetch, wrapped in the service policy for automatic\n * retry and circuit-breaking, and validates the response.\n *\n * @returns The validated geolocation data.\n */\n async #performFetch(): Promise<GeolocationData> {\n const response = await this.#policy.execute(async () => {\n const localResponse = await this.#fetch(this.#url);\n if (!localResponse.ok) {\n throw new HttpError(\n localResponse.status,\n `Geolocation fetch failed: ${localResponse.status}`,\n );\n }\n return localResponse;\n });\n\n const geolocation = parseGeolocationResponse(\n (await response.text()).trim(),\n );\n\n // Cache whenever at least one field resolved. A partially-known result\n // (e.g. timezone without a valid country) is still worth caching so we do\n // not re-fetch it within the TTL window; only a fully-unknown response is\n // left uncached so it can be retried.\n const hasKnownField =\n geolocation.country !== null ||\n geolocation.region !== null ||\n geolocation.timezone !== null;\n\n if (hasKnownField) {\n this.#cachedGeolocation = geolocation;\n this.#lastFetchedAt = Date.now();\n }\n\n return geolocation;\n }\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -4,6 +4,6 @@ export type { GeolocationRequestStatus } from './types.js';
4
4
  export { Env } from './types.js';
5
5
  export { GeolocationController, getDefaultGeolocationControllerState, } from './GeolocationController.js';
6
6
  export { GeolocationApiService, getUnknownGeolocationData, toLocationCode, UNKNOWN_LOCATION, } from './geolocation-api-service/geolocation-api-service.js';
7
- export type { GeolocationApiServiceMessenger, GeolocationApiServiceActions, GeolocationApiServiceEvents, GeolocationApiServiceCacheUpdatedEvent, GeolocationApiServiceGranularCacheUpdatedEvent, GeolocationApiServiceInvalidateQueriesAction, FetchGeolocationOptions, GeolocationData, } from './geolocation-api-service/geolocation-api-service.js';
7
+ export type { GeolocationApiServiceMessenger, GeolocationApiServiceActions, GeolocationApiServiceEvents, FetchGeolocationOptions, GeolocationData, } from './geolocation-api-service/geolocation-api-service.js';
8
8
  export type { GeolocationApiServiceFetchGeolocationAction, GeolocationApiServiceFetchGeolocationDataAction, } from './geolocation-api-service/geolocation-api-service-method-action-types.js';
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,0BAA0B,EAC1B,mCAAmC,EACnC,4BAA4B,EAC5B,qCAAqC,EACrC,2BAA2B,EAC3B,8BAA8B,EAC9B,4BAA4B,GAC7B,MAAM,4BAA4B,CAAC;AACpC,YAAY,EACV,yCAAyC,EACzC,6CAA6C,EAC7C,6CAA6C,GAC9C,MAAM,gDAAgD,CAAC;AACxD,YAAY,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EACL,qBAAqB,EACrB,oCAAoC,GACrC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,qBAAqB,EACrB,yBAAyB,EACzB,cAAc,EACd,gBAAgB,GACjB,MAAM,sDAAsD,CAAC;AAC9D,YAAY,EACV,8BAA8B,EAC9B,4BAA4B,EAC5B,2BAA2B,EAC3B,sCAAsC,EACtC,8CAA8C,EAC9C,4CAA4C,EAC5C,uBAAuB,EACvB,eAAe,GAChB,MAAM,sDAAsD,CAAC;AAC9D,YAAY,EACV,2CAA2C,EAC3C,+CAA+C,GAChD,MAAM,0EAA0E,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,0BAA0B,EAC1B,mCAAmC,EACnC,4BAA4B,EAC5B,qCAAqC,EACrC,2BAA2B,EAC3B,8BAA8B,EAC9B,4BAA4B,GAC7B,MAAM,4BAA4B,CAAC;AACpC,YAAY,EACV,yCAAyC,EACzC,6CAA6C,EAC7C,6CAA6C,GAC9C,MAAM,gDAAgD,CAAC;AACxD,YAAY,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EACL,qBAAqB,EACrB,oCAAoC,GACrC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,qBAAqB,EACrB,yBAAyB,EACzB,cAAc,EACd,gBAAgB,GACjB,MAAM,sDAAsD,CAAC;AAC9D,YAAY,EACV,8BAA8B,EAC9B,4BAA4B,EAC5B,2BAA2B,EAC3B,uBAAuB,EACvB,eAAe,GAChB,MAAM,sDAAsD,CAAC;AAC9D,YAAY,EACV,2CAA2C,EAC3C,+CAA+C,GAChD,MAAM,0EAA0E,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EACL,qBAAqB,EACrB,oCAAoC,GACrC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,qBAAqB,EACrB,yBAAyB,EACzB,cAAc,EACd,gBAAgB,GACjB,MAAM,sDAAsD,CAAC","sourcesContent":["export type {\n GeolocationControllerState,\n GeolocationControllerGetStateAction,\n GeolocationControllerActions,\n GeolocationControllerStateChangeEvent,\n GeolocationControllerEvents,\n GeolocationControllerMessenger,\n GeolocationControllerOptions,\n} from './GeolocationController.js';\nexport type {\n GeolocationControllerGetGeolocationAction,\n GeolocationControllerGetGeolocationDataAction,\n GeolocationControllerRefreshGeolocationAction,\n} from './GeolocationController-method-action-types.js';\nexport type { GeolocationRequestStatus } from './types.js';\nexport { Env } from './types.js';\nexport {\n GeolocationController,\n getDefaultGeolocationControllerState,\n} from './GeolocationController.js';\nexport {\n GeolocationApiService,\n getUnknownGeolocationData,\n toLocationCode,\n UNKNOWN_LOCATION,\n} from './geolocation-api-service/geolocation-api-service.js';\nexport type {\n GeolocationApiServiceMessenger,\n GeolocationApiServiceActions,\n GeolocationApiServiceEvents,\n GeolocationApiServiceCacheUpdatedEvent,\n GeolocationApiServiceGranularCacheUpdatedEvent,\n GeolocationApiServiceInvalidateQueriesAction,\n FetchGeolocationOptions,\n GeolocationData,\n} from './geolocation-api-service/geolocation-api-service.js';\nexport type {\n GeolocationApiServiceFetchGeolocationAction,\n GeolocationApiServiceFetchGeolocationDataAction,\n} from './geolocation-api-service/geolocation-api-service-method-action-types.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EACL,qBAAqB,EACrB,oCAAoC,GACrC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,qBAAqB,EACrB,yBAAyB,EACzB,cAAc,EACd,gBAAgB,GACjB,MAAM,sDAAsD,CAAC","sourcesContent":["export type {\n GeolocationControllerState,\n GeolocationControllerGetStateAction,\n GeolocationControllerActions,\n GeolocationControllerStateChangeEvent,\n GeolocationControllerEvents,\n GeolocationControllerMessenger,\n GeolocationControllerOptions,\n} from './GeolocationController.js';\nexport type {\n GeolocationControllerGetGeolocationAction,\n GeolocationControllerGetGeolocationDataAction,\n GeolocationControllerRefreshGeolocationAction,\n} from './GeolocationController-method-action-types.js';\nexport type { GeolocationRequestStatus } from './types.js';\nexport { Env } from './types.js';\nexport {\n GeolocationController,\n getDefaultGeolocationControllerState,\n} from './GeolocationController.js';\nexport {\n GeolocationApiService,\n getUnknownGeolocationData,\n toLocationCode,\n UNKNOWN_LOCATION,\n} from './geolocation-api-service/geolocation-api-service.js';\nexport type {\n GeolocationApiServiceMessenger,\n GeolocationApiServiceActions,\n GeolocationApiServiceEvents,\n FetchGeolocationOptions,\n GeolocationData,\n} from './geolocation-api-service/geolocation-api-service.js';\nexport type {\n GeolocationApiServiceFetchGeolocationAction,\n GeolocationApiServiceFetchGeolocationDataAction,\n} from './geolocation-api-service/geolocation-api-service-method-action-types.js';\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamask-previews/geolocation-controller",
3
- "version": "2.0.0-preview-313288d71",
3
+ "version": "2.0.0-preview-35f016caa",
4
4
  "description": "Centralised geolocation controller with TTL caching and request deduplication",
5
5
  "keywords": [
6
6
  "Ethereum",
@@ -51,10 +51,8 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@metamask/base-controller": "^10.1.0",
54
- "@metamask/base-data-service": "^2.1.0",
55
54
  "@metamask/controller-utils": "^13.0.0",
56
- "@metamask/messenger": "^3.0.0",
57
- "@metamask/superstruct": "^3.4.1"
55
+ "@metamask/messenger": "^3.0.0"
58
56
  },
59
57
  "devDependencies": {
60
58
  "@metamask/auto-changelog": "^6.2.1",