mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Fix potential memory leak in Stack App cache
This commit is contained in:
parent
3cdceb99f2
commit
659338dd1e
@ -372,7 +372,7 @@ const AdminAccessTokenExpired = createKnownErrorConstructor(
|
||||
`Admin access token has expired. Please refresh it and try again.${expiredAt ? ` (The access token expired at ${expiredAt.toISOString()}.)`: ""}`,
|
||||
{ expired_at_millis: expiredAt?.getTime() ?? null },
|
||||
] as const,
|
||||
(json: any) => [json.expired_at_millis ?? undefined] as const,
|
||||
(json: any) => [json.expired_at_millis ? new Date(json.expired_at_millis) : undefined] as const,
|
||||
);
|
||||
|
||||
const InvalidProjectForAdminAccessToken = createKnownErrorConstructor(
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { isBrowserLike } from "./env";
|
||||
import { DependenciesMap } from "./maps";
|
||||
import { filterUndefined } from "./objects";
|
||||
import { RateLimitOptions, ReactPromise, pending, rateLimited, resolved, runAsynchronously, wait } from "./promises";
|
||||
@ -183,10 +184,15 @@ class AsyncValueCache<T> {
|
||||
this._store.set(value);
|
||||
}
|
||||
|
||||
private _setAsync(value: Promise<T>): ReactPromise<boolean> {
|
||||
private _setAsync(value: Promise<T>): ReactPromise<void> {
|
||||
if (this._subscriptionsCount === 0 && !isBrowserLike()) {
|
||||
// if we're in a server-like environment, we'd rather cache less aggressively to avoid memory leaks.
|
||||
// hence, if no one is listening to this cache, let's invalidate it
|
||||
this._invalidateCacheSoon();
|
||||
}
|
||||
const promise = pending(value);
|
||||
this._pendingPromise = promise;
|
||||
return pending(this._store.setAsync(promise));
|
||||
return pending(this._store.setAsync(promise).then(() => undefined));
|
||||
}
|
||||
|
||||
private _refetch(cacheStrategy: CacheStrategy): ReactPromise<T> {
|
||||
@ -204,7 +210,7 @@ class AsyncValueCache<T> {
|
||||
this._set(value);
|
||||
}
|
||||
|
||||
forceSetCachedValueAsync(value: Promise<T>): ReactPromise<boolean> {
|
||||
forceSetCachedValueAsync(value: Promise<T>): ReactPromise<void> {
|
||||
return this._setAsync(value);
|
||||
}
|
||||
|
||||
@ -212,13 +218,14 @@ class AsyncValueCache<T> {
|
||||
* If anyone is listening to the cache, refreshes the value, and sets it without invalidating the cache.
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
// note that we do the extra check here to save a request if no one is listening to the cache anyway
|
||||
if (this._subscriptionsCount > 0) {
|
||||
await this.getOrWait("write-only");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the cache, marking it dirty (ie. it will be refreshed on the next read). It will refresh immediately.
|
||||
* Invalidates the cache, marking it dirty (ie. it will be refreshed on the next read). If anyone is listening to the cache, it will refresh immediately.
|
||||
*/
|
||||
async invalidate(): Promise<void> {
|
||||
this._store.setUnavailable();
|
||||
@ -230,6 +237,18 @@ class AsyncValueCache<T> {
|
||||
return this._pendingPromise === undefined;
|
||||
}
|
||||
|
||||
_invalidateCacheSoon(): void {
|
||||
// wait a few seconds; we want to keep the cache up during this time
|
||||
// else we do unnecessary requests if we unsubscribe and then subscribe again immediately
|
||||
const currentRefreshPromiseIndex = ++this._mostRecentRefreshPromiseIndex;
|
||||
runAsynchronously(async () => {
|
||||
await wait(5000);
|
||||
if (this._subscriptionsCount === 0 && currentRefreshPromiseIndex === this._mostRecentRefreshPromiseIndex) {
|
||||
await this.invalidate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onStateChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } {
|
||||
const storeObj = this._store.onChange(callback);
|
||||
|
||||
@ -249,15 +268,7 @@ class AsyncValueCache<T> {
|
||||
hasUnsubscribed = true;
|
||||
storeObj.unsubscribe();
|
||||
if (--this._subscriptionsCount === 0) {
|
||||
const currentRefreshPromiseIndex = ++this._mostRecentRefreshPromiseIndex;
|
||||
runAsynchronously(async () => {
|
||||
// wait a few seconds; we want to keep the cache up during this time
|
||||
// else we do unnecessary requests if we unsubscribe and then subscribe again immediately
|
||||
await wait(5000);
|
||||
if (this._subscriptionsCount === 0 && currentRefreshPromiseIndex === this._mostRecentRefreshPromiseIndex) {
|
||||
await this.invalidate();
|
||||
}
|
||||
});
|
||||
this._invalidateCacheSoon();
|
||||
|
||||
for (const unsubscribe of this._unsubscribers) {
|
||||
unsubscribe();
|
||||
|
||||
@ -184,6 +184,7 @@ export class AsyncStore<T> implements ReadonlyAsyncStore<T> {
|
||||
|
||||
setUnavailable(): void {
|
||||
this._lastSuccessfulUpdate = ++this._updateCounter;
|
||||
this._mostRecentOkValue = undefined;
|
||||
this._isAvailable = false;
|
||||
this._isRejected = false;
|
||||
this._rejectionError = undefined;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user