mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-06 14:07:51 +00:00
perf(dashboard): load usage and storage once when the dashboard opens
Home and Usage both load the monthly usage and the storage space when the dashboard opens, whichever tab it opens on, so every open sent /metering/usage and /df twice. Overlapping loads now share one request. Nothing is cached: a load after the shared one settles fetches again.
This commit is contained in:
1 parent
dc9ce4fffd
commit
eef34ce4c8
5 files changed
+121
-4
No files matched your search
@@ -20,6 +20,7 @@
|
||||
import UIWindowSaveAccount from '../UIWindowSaveAccount.js';
|
||||
import { formatCredits, formatDollarsFromMicrocents, usageIsCredits } from './credits.js';
|
||||
import { addonCreditsRemaining, monthlyUsageOrThrow, usageBudget } from './usageBudget.js';
|
||||
import { getMonthlyUsage, getStorageSpace } from './usageRequests.js';
|
||||
import { appIconAttrs } from '../../helpers/appIcon.js';
|
||||
import { filterLaunchableApps } from '../../helpers/launchAppsList.js';
|
||||
import { isOrgSeat, orgSeatTeamName } from './orgSeat.js';
|
||||
@@ -613,7 +614,7 @@ const TabHome = {
|
||||
|
||||
// Load storage data
|
||||
try {
|
||||
const res = await puter.fs.space();
|
||||
const res = await getStorageSpace();
|
||||
// Guard capacity 0 — 0/0 would render literally as "NaN%".
|
||||
let usage_percentage = res.capacity
|
||||
? ((res.used / res.capacity) * 100).toFixed(0)
|
||||
@@ -644,7 +645,7 @@ const TabHome = {
|
||||
|
||||
// Load monthly usage data
|
||||
try {
|
||||
const res = monthlyUsageOrThrow(await puter.auth.getMonthlyUsage());
|
||||
const res = monthlyUsageOrThrow(await getMonthlyUsage());
|
||||
const budget = usageBudget(res.usage, res.allowanceInfo);
|
||||
const addonLeft = addonCreditsRemaining(res.allowanceInfo);
|
||||
// The server reports credits (already scaled) or raw amounts
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { formatCredits, formatDollarsFromMicrocents, usageIsCredits } from './credits.js';
|
||||
import { isOrgSeat } from './orgSeat.js';
|
||||
import { addonCreditsRemaining, monthlyUsageOrThrow, usageBudget } from './usageBudget.js';
|
||||
import { getMonthlyUsage, getStorageSpace } from './usageRequests.js';
|
||||
|
||||
// Whether the server reported credits — decides whether the usage surfaces
|
||||
// render credits or fall back to dollars.
|
||||
@@ -302,7 +303,7 @@ function renderUsageTable () {
|
||||
}
|
||||
|
||||
async function update_usage_details ($el_window) {
|
||||
const monthlyUsagePromise = puter.auth.getMonthlyUsage().then(monthlyUsageOrThrow).then(res => {
|
||||
const monthlyUsagePromise = getMonthlyUsage().then(monthlyUsageOrThrow).then(res => {
|
||||
const budget = usageBudget(res.usage, res.allowanceInfo);
|
||||
const addonLeft = addonCreditsRemaining(res.allowanceInfo);
|
||||
// The server reports credits (already scaled) or raw amounts (no
|
||||
@@ -372,7 +373,7 @@ async function update_usage_details ($el_window) {
|
||||
}
|
||||
});
|
||||
|
||||
const spacePromise = puter.fs.space().then(res => {
|
||||
const spacePromise = getStorageSpace().then(res => {
|
||||
// Guard capacity 0 — otherwise 0/0 renders literally as "NaN%".
|
||||
let usage_percentage = res.capacity ? (res.used / res.capacity * 100).toFixed(0) : '0';
|
||||
usage_percentage = usage_percentage > 100 ? 100 : usage_percentage;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import shareInFlight from '../../helpers/shareInFlight.js';
|
||||
|
||||
// Home and Usage both show these and both load them when the dashboard
|
||||
// opens; a load overlapping the other's shares its request.
|
||||
export const getMonthlyUsage = shareInFlight(() => puter.auth.getMonthlyUsage());
|
||||
export const getStorageSpace = shareInFlight(() => puter.fs.space());
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Wraps an async call so overlapping callers share one call: until the
|
||||
* first settles, every caller gets its promise. The next call after it
|
||||
* settles starts afresh, so nothing is cached.
|
||||
*
|
||||
* @template T
|
||||
* @param {() => Promise<T>} fn
|
||||
* @returns {() => Promise<T>}
|
||||
*/
|
||||
export default function shareInFlight (fn) {
|
||||
let pending = null;
|
||||
return () => {
|
||||
pending ??= Promise.resolve().then(fn).finally(() => {
|
||||
pending = null;
|
||||
});
|
||||
return pending;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import shareInFlight from './shareInFlight.js';
|
||||
|
||||
describe('shareInFlight', () => {
|
||||
it('gives overlapping callers the same call', async () => {
|
||||
const fn = vi.fn(async () => 'answer');
|
||||
const shared = shareInFlight(fn);
|
||||
const [a, b] = await Promise.all([shared(), shared()]);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(a).toBe('answer');
|
||||
expect(b).toBe('answer');
|
||||
});
|
||||
|
||||
it('calls again once the previous call has settled', async () => {
|
||||
const fn = vi.fn(async () => 'answer');
|
||||
const shared = shareInFlight(fn);
|
||||
await shared();
|
||||
await shared();
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('shares a rejection, then lets the next call retry', async () => {
|
||||
const fn = vi.fn()
|
||||
.mockRejectedValueOnce(new Error('boom'))
|
||||
.mockResolvedValueOnce('answer');
|
||||
const shared = shareInFlight(fn);
|
||||
const first = shared();
|
||||
const second = shared();
|
||||
await expect(first).rejects.toThrow('boom');
|
||||
await expect(second).rejects.toThrow('boom');
|
||||
await expect(shared()).resolves.toBe('answer');
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user