@metamask-previews/perps-controller 22.1.0-preview-543acb9 → 22.1.0-preview-3a0f39d23
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
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Bump `uuid` from `^14.0.0` to `^14.0.1` ([#11179](https://github.com/MetaMask/core/pull/11179))
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Prevent HyperLiquid `userFills` from going silent after a WebSocket reconnect when an in-flight subscribe races with `restoreSubscriptions` (same pending + generation pattern TWAP already uses) ([#11160](https://github.com/MetaMask/core/pull/11160))
|
|
17
|
+
- Without this, live fills can keep attaching to a dead pre-reconnect listener (MetaMask/metamask-extension#47287).
|
|
18
|
+
|
|
10
19
|
## [22.1.0]
|
|
11
20
|
|
|
12
21
|
### Added
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HyperLiquidSubscriptionService.d.ts","sourceRoot":"","sources":["../../src/services/HyperLiquidSubscriptionService.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAMV,mBAAmB,EAKnB,gBAAgB,EAMjB,MAAM,qBAAqB,CAAC;AAO7B,OAAO,KAAK,EAEV,0BAA0B,EAC3B,MAAM,+BAA+B,CAAC;AAGvC,OAAO,KAAK,EAEV,QAAQ,EACR,SAAS,EACT,KAAK,EAEL,SAAS,EACT,qBAAqB,EACrB,wBAAwB,EACxB,yBAAyB,EACzB,qBAAqB,EACrB,yBAAyB,EACzB,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,EAGxB,yBAAyB,EAG1B,MAAM,mBAAmB,CAAC;AAgC3B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAE9E;;;GAGG;AACH,KAAK,kBAAkB,GAAG,IAAI,CAC5B,wBAAwB,EACxB,qBAAqB,GAAG,2BAA2B,CACpD,CAAC;AA0BF;;;GAGG;AACH,qBAAa,8BAA8B;;
|
|
1
|
+
{"version":3,"file":"HyperLiquidSubscriptionService.d.ts","sourceRoot":"","sources":["../../src/services/HyperLiquidSubscriptionService.ts"],"names":[],"mappings":"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"}
|
|
@@ -92,6 +92,20 @@ export class HyperLiquidSubscriptionService {
|
|
|
92
92
|
#pendingBboPromises = new Map();
|
|
93
93
|
// Order fill subscriptions keyed by accountId (normalized: undefined -> 'default')
|
|
94
94
|
#orderFillSubscriptions = new Map();
|
|
95
|
+
// In-flight userFills subscribe calls, so concurrent subscribers / reconnect
|
|
96
|
+
// share one socket open instead of racing two opens onto the same map key.
|
|
97
|
+
#pendingOrderFillSubscriptions = new Map();
|
|
98
|
+
// Bumped per account by teardown and reconnect so an in-flight open for
|
|
99
|
+
// that account discards its subscription instead of rehydrating a cleared
|
|
100
|
+
// map (same race TWAP already guards; without it live fills can stick to a
|
|
101
|
+
// dead pre-reconnect listener — MetaMask/metamask-extension#47287).
|
|
102
|
+
#orderFillSubscriptionGenerations = new Map();
|
|
103
|
+
#orderFillGeneration(normalizedAccountId) {
|
|
104
|
+
return this.#orderFillSubscriptionGenerations.get(normalizedAccountId) ?? 0;
|
|
105
|
+
}
|
|
106
|
+
#bumpOrderFillGeneration(normalizedAccountId) {
|
|
107
|
+
this.#orderFillSubscriptionGenerations.set(normalizedAccountId, this.#orderFillGeneration(normalizedAccountId) + 1);
|
|
108
|
+
}
|
|
95
109
|
// TWAP subscriptions keyed by accountId (normalized: undefined -> 'default')
|
|
96
110
|
#twapOrderSubscriptions = new Map();
|
|
97
111
|
// In-flight TWAP subscribe calls, so concurrent subscribers share one socket
|
|
@@ -1954,6 +1968,10 @@ export class HyperLiquidSubscriptionService {
|
|
|
1954
1968
|
});
|
|
1955
1969
|
this.#orderFillSubscriptions.delete(normalizedAccountId);
|
|
1956
1970
|
}
|
|
1971
|
+
// Invalidate any open still in flight for this account: without this
|
|
1972
|
+
// its continuation installs a live socket nobody is listening to.
|
|
1973
|
+
this.#bumpOrderFillGeneration(normalizedAccountId);
|
|
1974
|
+
this.#pendingOrderFillSubscriptions.delete(normalizedAccountId);
|
|
1957
1975
|
}
|
|
1958
1976
|
};
|
|
1959
1977
|
}
|
|
@@ -1971,19 +1989,71 @@ export class HyperLiquidSubscriptionService {
|
|
|
1971
1989
|
if (this.#orderFillSubscriptions.has(normalizedAccountId)) {
|
|
1972
1990
|
return;
|
|
1973
1991
|
}
|
|
1974
|
-
|
|
1992
|
+
// Another caller is already opening this account's socket. Await it so
|
|
1993
|
+
// subscribe + reconnect do not race two userFills opens onto one key.
|
|
1994
|
+
// The owner of the pending open logs its failure, so swallow it here and
|
|
1995
|
+
// fall through to open our own. Re-read after each wait: several waiters
|
|
1996
|
+
// resume together and only the first should start the replacement open.
|
|
1997
|
+
let pending = this.#pendingOrderFillSubscriptions.get(normalizedAccountId);
|
|
1998
|
+
while (pending) {
|
|
1999
|
+
await pending.catch(() => undefined);
|
|
2000
|
+
if (this.#orderFillSubscriptions.has(normalizedAccountId)) {
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
pending = this.#pendingOrderFillSubscriptions.get(normalizedAccountId);
|
|
2004
|
+
}
|
|
2005
|
+
// A discarded open resolves without registering; if the last subscriber
|
|
2006
|
+
// left meanwhile, opening now would cache frames nobody is listening to.
|
|
2007
|
+
const subscribers = this.#orderFillSubscribers.get(normalizedAccountId);
|
|
2008
|
+
if (!subscribers || subscribers.size === 0) {
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
// Captured once here, spanning the whole open including the client
|
|
2012
|
+
// handshake. Capturing inside the open would let a bump that lands during
|
|
2013
|
+
// the handshake be adopted as the baseline and silently forgotten.
|
|
2014
|
+
const startGeneration = this.#orderFillGeneration(normalizedAccountId);
|
|
2015
|
+
const subscriptionPromise = this.#createOrderFillSubscription(normalizedAccountId, accountId, startGeneration);
|
|
2016
|
+
this.#pendingOrderFillSubscriptions.set(normalizedAccountId, subscriptionPromise);
|
|
2017
|
+
try {
|
|
2018
|
+
await subscriptionPromise;
|
|
2019
|
+
}
|
|
2020
|
+
finally {
|
|
2021
|
+
// Delete by identity: a reconnect may have cleared the map and installed
|
|
2022
|
+
// a newer open behind this one, which must not be evicted here.
|
|
2023
|
+
if (this.#pendingOrderFillSubscriptions.get(normalizedAccountId) ===
|
|
2024
|
+
subscriptionPromise) {
|
|
2025
|
+
this.#pendingOrderFillSubscriptions.delete(normalizedAccountId);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Open one userFills subscription and register it for teardown.
|
|
2031
|
+
*
|
|
2032
|
+
* @param normalizedAccountId - Map key for this account ('default' when unset).
|
|
2033
|
+
* @param accountId - Optional CAIP account ID to resolve the address from.
|
|
2034
|
+
* @param startGeneration - This account's generation when the open began.
|
|
2035
|
+
* @returns A promise that resolves once the subscription is registered.
|
|
2036
|
+
*/
|
|
2037
|
+
async #createOrderFillSubscription(normalizedAccountId, accountId, startGeneration) {
|
|
2038
|
+
// Bootstrap the client inline rather than recursing: a recursive call
|
|
2039
|
+
// would re-read the generation after the handshake and adopt a bump that
|
|
2040
|
+
// landed during it as its own baseline.
|
|
2041
|
+
let subscriptionClient = this.#clientService.getSubscriptionClient();
|
|
1975
2042
|
if (!subscriptionClient) {
|
|
1976
2043
|
await this.#clientService.ensureSubscriptionClient(this.#walletService.createWalletAdapter());
|
|
1977
|
-
|
|
1978
|
-
if (!
|
|
2044
|
+
subscriptionClient = this.#clientService.getSubscriptionClient();
|
|
2045
|
+
if (!subscriptionClient) {
|
|
1979
2046
|
throw new Error('SubscriptionClient not available');
|
|
1980
2047
|
}
|
|
1981
|
-
await this.#ensureOrderFillISubscription(accountId);
|
|
1982
|
-
return;
|
|
1983
2048
|
}
|
|
1984
2049
|
const userAddress = await this.#walletService.getUserAddressWithDefault(accountId);
|
|
1985
2050
|
// userFills returns a Promise<ISubscription>, need to await it
|
|
1986
2051
|
const subscription = await subscriptionClient.userFills({ user: userAddress }, (data) => {
|
|
2052
|
+
// A frame queued before an unsubscribe or reconnect must not mutate
|
|
2053
|
+
// the replacement's cache or reach its subscribers.
|
|
2054
|
+
if (startGeneration !== this.#orderFillGeneration(normalizedAccountId)) {
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
1987
2057
|
// Build a Map for O(1) lookup instead of O(n) find per fill
|
|
1988
2058
|
const orderMap = new Map();
|
|
1989
2059
|
if (this.#cachedOrders) {
|
|
@@ -2046,6 +2116,17 @@ export class HyperLiquidSubscriptionService {
|
|
|
2046
2116
|
});
|
|
2047
2117
|
}
|
|
2048
2118
|
});
|
|
2119
|
+
// Teardown, clearAll, or a reconnect may have run while this open was in
|
|
2120
|
+
// flight. Rehydrating the map here would leave a socket with no
|
|
2121
|
+
// subscribers, or clobber the live replacement — so drop ours instead.
|
|
2122
|
+
const subscribers = this.#orderFillSubscribers.get(normalizedAccountId);
|
|
2123
|
+
if (startGeneration !== this.#orderFillGeneration(normalizedAccountId) ||
|
|
2124
|
+
this.#orderFillSubscriptions.has(normalizedAccountId) ||
|
|
2125
|
+
!subscribers ||
|
|
2126
|
+
subscribers.size === 0) {
|
|
2127
|
+
subscription.unsubscribe().catch(() => undefined);
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2049
2130
|
this.#orderFillSubscriptions.set(normalizedAccountId, subscription);
|
|
2050
2131
|
}
|
|
2051
2132
|
/**
|
|
@@ -3543,8 +3624,19 @@ export class HyperLiquidSubscriptionService {
|
|
|
3543
3624
|
}
|
|
3544
3625
|
// Re-establish order fill subscriptions if there are fill subscribers
|
|
3545
3626
|
if (this.#orderFillSubscribers.size > 0) {
|
|
3546
|
-
//
|
|
3627
|
+
// Existing subscription references are dead after reconnection. Bump
|
|
3628
|
+
// each account's generation first so a pre-reconnect open still in
|
|
3629
|
+
// flight discards itself instead of occupying the map ahead of its
|
|
3630
|
+
// replacement (MetaMask/metamask-extension#47287).
|
|
3631
|
+
for (const normalizedAccountId of new Set([
|
|
3632
|
+
...this.#orderFillSubscribers.keys(),
|
|
3633
|
+
...this.#orderFillSubscriptions.keys(),
|
|
3634
|
+
...this.#pendingOrderFillSubscriptions.keys(),
|
|
3635
|
+
])) {
|
|
3636
|
+
this.#bumpOrderFillGeneration(normalizedAccountId);
|
|
3637
|
+
}
|
|
3547
3638
|
this.#orderFillSubscriptions.clear();
|
|
3639
|
+
this.#pendingOrderFillSubscriptions.clear();
|
|
3548
3640
|
// Re-establish subscriptions for all accountIds with subscribers
|
|
3549
3641
|
// Note: normalizedAccountId is 'default' for undefined, need to convert back
|
|
3550
3642
|
const normalizedAccountIds = Array.from(this.#orderFillSubscribers.keys());
|
|
@@ -3716,6 +3808,15 @@ export class HyperLiquidSubscriptionService {
|
|
|
3716
3808
|
])) {
|
|
3717
3809
|
this.#bumpTwapOrderGeneration(normalizedAccountId);
|
|
3718
3810
|
}
|
|
3811
|
+
// Same for order fills: bump before subscriber keys are cleared so an
|
|
3812
|
+
// in-flight userFills open cannot rehydrate after clearAll.
|
|
3813
|
+
for (const normalizedAccountId of new Set([
|
|
3814
|
+
...this.#orderFillSubscribers.keys(),
|
|
3815
|
+
...this.#orderFillSubscriptions.keys(),
|
|
3816
|
+
...this.#pendingOrderFillSubscriptions.keys(),
|
|
3817
|
+
])) {
|
|
3818
|
+
this.#bumpOrderFillGeneration(normalizedAccountId);
|
|
3819
|
+
}
|
|
3719
3820
|
// Clear all local subscriber collections
|
|
3720
3821
|
this.#priceSubscribers.clear();
|
|
3721
3822
|
this.#positionSubscribers.clear();
|
|
@@ -3726,13 +3827,14 @@ export class HyperLiquidSubscriptionService {
|
|
|
3726
3827
|
this.#accountSubscribers.clear();
|
|
3727
3828
|
this.#marketDataSubscribers.clear();
|
|
3728
3829
|
this.#orderBookSubscribers.clear();
|
|
3729
|
-
// Clear order fill subscriptions
|
|
3830
|
+
// Clear order fill subscriptions; generations were bumped above.
|
|
3730
3831
|
this.#orderFillSubscriptions.forEach((subscription) => {
|
|
3731
3832
|
subscription.unsubscribe().catch(() => {
|
|
3732
3833
|
// Ignore errors during cleanup
|
|
3733
3834
|
});
|
|
3734
3835
|
});
|
|
3735
3836
|
this.#orderFillSubscriptions.clear();
|
|
3837
|
+
this.#pendingOrderFillSubscriptions.clear();
|
|
3736
3838
|
// Clear TWAP subscriptions; generations were bumped above, before the
|
|
3737
3839
|
// subscriber keys naming those accounts were cleared.
|
|
3738
3840
|
this.#cachedTwapOrders.clear();
|