mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-26 00:30:01 +00:00
* perf(workers): skip hot-reload lookups for files no worker is bound to
The fs.write.file / fs.remove.node / fs.move.node listeners listed every
workers.puter.* row of the file's owner on every event, for every user.
The event's FSEntry already carries the worker rows bound to it, so a file
with none returns before any I/O; a bound file reads just its own rows,
batched, from the primary.
* perf(apps): read the uid alias only after a direct lookup misses
Every AppDriver uid lookup fired an uncached system-KV GetItem for the
merge alias in parallel with the direct read, even when the direct read
hit. The alias is now read only on a miss.
* refactor(apps): one app view with the launch gates, built by AppService
AppDriver's serializer and its private-app / hosted-backing gates move to
AppService.views, built on a pure toAppView that is the one place
index_url is withheld. AppDriver, the subdomains driver's embedded apps and
the GUI shell view all use it. Visibility of protected apps
(filterReadable) moves with it.
Subdomain embeds (associated_app, app_owner) used to return index_url with
no gate; they now withhold it from a viewer who may not open a private app
or whose hosted backing is gone, and carry privateAccess, godmode and
feedback_enabled like a direct app read.
* refactor(apps): one toAppSummary; batch the recommended and taskbar lookups
SuggestedApps, RecommendedApps, /get-launch-apps and the taskbar each built
their own launch summary; they now share toAppSummary (the extra
feedback_enabled / external flags appear on the lists that lacked them).
getRecommendedApps awaited getByName once per recommended app (29) and
getTaskbarItems did the same per taskbar entry on every whoami; both now
make one batched call per key through the new AppStore.getByNames, which
keeps getByName's case-insensitive and old-name matching. The taskbar's
raw UPDATE of the user row goes through UserStore.update, and
/get-launch-apps checks hosted backings in one batch and reaches the
stores without casts.
* refactor(apps): app routes read through AppService; /delete-site calls the subdomains driver
AppController used the global driversContainers through a getter whose
boot-order comment no longer held; it uses the injected drivers and
AppService. GET /apps/:name looked each name up, read it again through the
driver and resolved private access a second time, emitting
app.privateAccess.resolveLaunch twice per private app; it now batches the
names and resolves once. POST /query/app did a lookup, a driver read and a
raw app_filetype_association SELECT per selector (up to 200, serially); it
now makes one batched lookup per key, one grant check and one filetype read.
The driver's read shape is a pure toAppReadView both use.
HostingController's /delete-site re-implemented the subdomains driver's
delete (protected check, delete, subdomain.delete event); it keeps its
owner-scoped 404 and calls the driver. Hosting and Desktop controllers drop
constructors that discarded the drivers argument, and trivial getters.
* refactor(drivers): one per-method policy validator and resolver; drop @Driver
Four policy blocks (rateLimit, concurrent, requireSubscription,
requireReputation) share the { default, methods } shape but each had its
own validator and resolver. They become validatePerMethod(value, label,
policy) and resolvePerMethod(cfg, method); error messages are unchanged.
The @Driver class decorator and the prototype keys it wrote had no users
outside its own tests; drivers declare their fields, and resolveDriverMeta
reads only those. DriverController takes the driver name from the meta it
already resolved instead of probing the prototype.
resolveDriverMethodRateLimit/Concurrent stay as deprecated aliases for the
email extension.
* refactor(drivers): shared actor guards and crud-q concurrency policy
The apps, subdomains, workers and notifications drivers each carried a
private #requireActor, two carried the same #requireVerified, and apps and
subdomains the same owner/app_owner write predicate. These become
requireContextActor, requireContextUserActor, assertActorEmailVerified and
actorOwnsRow in core/actor.ts; each driver keeps its own error for the
cases that differed (system actors still read apps; notifications still
answer 403 for an actor without a user). The identical crud-q concurrency
block is one CRUD_CONCURRENT constant.
The metering, installedApps and appTelemetry extensions re-checked an actor
their routes already require; those checks and the tests for them go.
* refactor(extensions): store methods instead of raw SQL in OSS extensions
installedApps and appTelemetry queried user_to_app_permissions directly
and metering read apps by name with its own SELECT. PermissionStore gains
listAppsGrantedByUser, listAppAuthenticatedUsers (with the email-share
check folded in) and countAppUsers; metering uses AppStore.getByName.
* refactor(server): one layer builder and one lifecycle hook runner
PuterServer built each of its five layers with its own copy of the same
loop (built-ins, then extension additions, into the layer and its export
container), and ran each lifecycle hook with five near-identical loops per
hook. Both are now one helper each: #instantiateLayer and
#runHook(order, hook). Construction order, the args each layer receives,
and hook order (start and prepare bottom-up, shutdown top-down) are
unchanged.
* fix(workers): tear down a deleted source's worker after its FK nulls root_dir_id
135 lines
3.9 KiB
TypeScript
135 lines
3.9 KiB
TypeScript
/*
|
|
* 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 type { AppIconHostConfig } from './appIcon.js';
|
|
import { toAppSummary, type AppRow } from './appView.js';
|
|
|
|
interface TaskbarEntry {
|
|
name?: string;
|
|
id?: number;
|
|
uid?: string;
|
|
type?: string;
|
|
}
|
|
|
|
interface TaskbarOptions {
|
|
iconSize?: number;
|
|
noIcons?: boolean;
|
|
}
|
|
|
|
interface TaskbarDeps {
|
|
apiBaseUrl?: string;
|
|
config?: AppIconHostConfig;
|
|
stores: {
|
|
app: {
|
|
getByNames: (names: string[]) => Promise<Map<string, AppRow>>;
|
|
getByUids: (uids: string[]) => Promise<Map<string, AppRow>>;
|
|
getByIds: (ids: number[]) => Promise<Map<number, AppRow>>;
|
|
};
|
|
user: {
|
|
update: (
|
|
id: number,
|
|
patch: Record<string, unknown>,
|
|
) => Promise<unknown>;
|
|
};
|
|
};
|
|
}
|
|
|
|
const DEFAULT_TASKBAR_ITEMS: TaskbarEntry[] = [
|
|
{ name: 'app-center', type: 'app' },
|
|
{ name: 'builder', type: 'app' },
|
|
{ name: 'dev-center', type: 'app' },
|
|
{ name: 'editor', type: 'app' },
|
|
{ name: 'code', type: 'app' },
|
|
{ name: 'camera', type: 'app' },
|
|
{ name: 'recorder', type: 'app' },
|
|
{ name: 'browser', type: 'app' },
|
|
];
|
|
|
|
export async function getTaskbarItems(
|
|
user: Record<string, unknown>,
|
|
deps: TaskbarDeps,
|
|
options: TaskbarOptions = {},
|
|
): Promise<Array<Record<string, unknown>>> {
|
|
let raw: TaskbarEntry[];
|
|
|
|
if (!user.taskbar_items) {
|
|
raw = DEFAULT_TASKBAR_ITEMS;
|
|
await deps.stores.user.update(user.id as number, {
|
|
taskbar_items: JSON.stringify(raw),
|
|
});
|
|
} else {
|
|
try {
|
|
raw =
|
|
typeof user.taskbar_items === 'string'
|
|
? JSON.parse(user.taskbar_items as string)
|
|
: (user.taskbar_items as TaskbarEntry[]);
|
|
} catch {
|
|
raw = [];
|
|
}
|
|
}
|
|
|
|
const entries = raw.filter(
|
|
(entry) => entry.type === 'app' && entry.name !== 'explorer',
|
|
);
|
|
const [byName, byUid, byId] = await Promise.all([
|
|
deps.stores.app.getByNames(
|
|
entries.flatMap((entry) => (entry.name ? [entry.name] : [])),
|
|
),
|
|
deps.stores.app.getByUids(
|
|
entries.flatMap((entry) =>
|
|
!entry.name && entry.uid ? [entry.uid] : [],
|
|
),
|
|
),
|
|
deps.stores.app.getByIds(
|
|
entries.flatMap((entry) =>
|
|
!entry.name && !entry.uid && entry.id ? [entry.id] : [],
|
|
),
|
|
),
|
|
]);
|
|
|
|
const items: Array<Record<string, unknown>> = [];
|
|
for (const entry of entries) {
|
|
const app = entry.name
|
|
? byName.get(entry.name)
|
|
: entry.uid
|
|
? byUid.get(entry.uid)
|
|
: entry.id
|
|
? byId.get(entry.id)
|
|
: undefined;
|
|
if (!app) continue;
|
|
|
|
const item: Record<string, unknown> = {
|
|
uid: app.uid,
|
|
...toAppSummary(app, {
|
|
apiBaseUrl: deps.apiBaseUrl,
|
|
config: deps.config,
|
|
iconSize: options.iconSize,
|
|
}),
|
|
description: app.description,
|
|
};
|
|
if (options.noIcons) {
|
|
delete item.icon;
|
|
delete item.iconCdnUrl;
|
|
}
|
|
items.push(item);
|
|
}
|
|
|
|
return items;
|
|
}
|