Skip to content

Commit ee5b80d

Browse files
authored
🐛 dedupe concurrent initial auth requests (#1437)
1 parent e012e9e commit ee5b80d

2 files changed

Lines changed: 57 additions & 11 deletions

File tree

packages/filesystem/auth.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,38 @@ describe("AuthVerify", () => {
6868
).resolves.toEqual(["new-access", "new-access", "new-access"]);
6969
expect(fetchMock).toHaveBeenCalledTimes(1);
7070
});
71+
72+
it("concurrent initial token verification should share one auth request and save once", async () => {
73+
vi.useFakeTimers();
74+
const createSpy = vi.spyOn(chrome.tabs, "create").mockImplementation(() => Promise.resolve({ id: 1 }) as any);
75+
const originalGet = (chrome.tabs as any).get;
76+
(chrome.tabs as any).get = vi.fn().mockRejectedValue(new Error("closed"));
77+
const saveSpy = vi.spyOn(LocalStorageDAO.prototype, "saveValue");
78+
const fetchMock = vi.fn().mockResolvedValue({
79+
json: vi.fn().mockResolvedValue({
80+
code: 0,
81+
data: {
82+
token: {
83+
access_token: "initial-access",
84+
refresh_token: "initial-refresh",
85+
},
86+
},
87+
}),
88+
} as unknown as Response);
89+
vi.stubGlobal("fetch", fetchMock);
90+
91+
try {
92+
const auth = Promise.all([AuthVerify("onedrive"), AuthVerify("onedrive"), AuthVerify("onedrive")]);
93+
await vi.advanceTimersByTimeAsync(1000);
94+
95+
await expect(auth).resolves.toEqual(["initial-access", "initial-access", "initial-access"]);
96+
expect(createSpy).toHaveBeenCalledTimes(1);
97+
expect(fetchMock).toHaveBeenCalledTimes(1);
98+
expect(saveSpy).toHaveBeenCalledTimes(1);
99+
expect(saveSpy).toHaveBeenCalledWith(key, expect.objectContaining({ accessToken: "initial-access" }));
100+
} finally {
101+
(chrome.tabs as any).get = originalGet;
102+
vi.useRealTimers();
103+
}
104+
});
71105
});

packages/filesystem/auth.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export type Token = {
7474
createtime: number;
7575
};
7676
const refreshTokenPromises: Partial<Record<NetDiskType, Promise<string>>> = {};
77+
const authTokenPromises: Partial<Record<NetDiskType, Promise<Token>>> = {};
7778

7879
function refreshAccessToken(
7980
netDiskType: NetDiskType,
@@ -126,19 +127,30 @@ export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) {
126127
}
127128
// token不存在,或者没有accessToken,重新获取
128129
if (!token || !token.accessToken) {
129-
// 强制重新获取token
130-
await NetDisk(netDiskType);
131-
const resp = await GetNetDiskToken(netDiskType);
132-
if (resp.code !== 0) {
133-
throw new WarpTokenError(new Error(resp.msg));
130+
if (!authTokenPromises[netDiskType]) {
131+
const authPromise = (async () => {
132+
// 强制重新获取token
133+
await NetDisk(netDiskType);
134+
const resp = await GetNetDiskToken(netDiskType);
135+
if (resp.code !== 0) {
136+
throw new WarpTokenError(new Error(resp.msg));
137+
}
138+
const newToken = {
139+
accessToken: resp.data.token.access_token,
140+
refreshToken: resp.data.token.refresh_token,
141+
createtime: Date.now(),
142+
};
143+
await localStorageDAO.saveValue(key, newToken);
144+
return newToken;
145+
})().finally(() => {
146+
if (authTokenPromises[netDiskType] === authPromise) {
147+
delete authTokenPromises[netDiskType];
148+
}
149+
});
150+
authTokenPromises[netDiskType] = authPromise;
134151
}
135-
token = {
136-
accessToken: resp.data.token.access_token,
137-
refreshToken: resp.data.token.refresh_token,
138-
createtime: Date.now(),
139-
};
152+
token = await authTokenPromises[netDiskType];
140153
invalid = false;
141-
await localStorageDAO.saveValue(key, token);
142154
}
143155
// token未过期(一小时内)及有效则保留,不用刷新token
144156
const unexpired = Date.now() < token.createtime + 3600000;

0 commit comments

Comments
 (0)