Limit the app container to 1 CPU and 2 GB, and stop stale caches
CI / check (push) Canceled after 0s

The app container is now created with --cpus 1 --memory 2048m: the heavy
work happens in the computers' own containers, so the orchestrator does
not need more. Both are installer flags (--cpus, --memory), kept in
install.conf, and part of the idempotency check, so an existing install
picks them up on its next update.

The env and app-setting caches lived in module variables, and bunext
bundles pages and API routes separately. A save cleared only its own
copy, so a saved provider key never reached the page that lists models
until the process restarted. They now share one slot per name on
globalThis, behind a symbol from the global registry, with the pattern
extracted into a tested helper.
This commit is contained in:
2026-10-22 15:16:51 +01:00
parent 06210f00a5
commit e1faa2141e
7 changed files with 172 additions and 10 deletions
@@ -1,14 +1,25 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_APP_SETTINGS } from "@/db/types/db";
import globalCache from "@/src/utils/global-cache";
let cache: Map<string, string> | null = null;
/**
* The cache lives on `globalThis`, behind a registered symbol, rather than
* in a module variable: the page and API bundles each carry their own copy
* of this module, so a module-scoped cache would let a save clear its own
* copy while another page kept serving a stale one until the next restart.
*/
const cache = globalCache<Map<string, string>>({
name: "cobble.grabAppSettings.cache",
});
/**
* Every install-wide setting, cached per process (like `grabAllEnvs`).
* `saveAppSetting` and `deleteAppSetting` clear it.
*/
export default async function grabAppSettings() {
if (cache) return cache;
const cached = cache.get();
if (cached) return cached;
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_APP_SETTINGS>({
sql: `SELECT key, value FROM app_settings`,
@@ -21,7 +32,7 @@ export default async function grabAppSettings() {
if (row.key) map.set(row.key, row.value || "");
}
cache = map;
cache.set(map);
return map;
}
@@ -30,5 +41,5 @@ export default async function grabAppSettings() {
* Drops the cache, so the next read sees the database.
*/
export function clearAppSettingsCache() {
cache = null;
cache.clear();
}
+21 -5
View File
@@ -2,6 +2,7 @@ import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_ENVS } from "@/db/types/db";
import type { EnvParadigm } from "@/src/dict/env-dict";
import { decrypt } from "@/src/utils/crypt";
import globalCache from "@/src/utils/global-cache";
export type DecryptedEnv = {
id: number;
@@ -16,7 +17,16 @@ export type DecryptedEnv = {
expose_to_computers: boolean;
};
let cache: Promise<DecryptedEnv[]> | undefined;
/**
* The cache lives on `globalThis`, behind a registered symbol, rather than
* in a module variable: the page and API bundles each carry their own copy
* of this module, so a module-scoped cache would let `save-env` clear its
* own copy while the page kept serving a stale one until the next restart
* (a saved API key never reached the models shown in the app).
*/
const cache = globalCache<Promise<DecryptedEnv[]>>({
name: "cobble.grabAllEnvs.cache",
});
/**
* Every env, decrypted, cached in this process until `clearEnvCache` (every
@@ -24,16 +34,22 @@ let cache: Promise<DecryptedEnv[]> | undefined;
* encryption key changed) reads as empty.
*/
export default function grabAllEnvs(): Promise<DecryptedEnv[]> {
cache ||= load().catch((error) => {
cache = undefined;
const cached = cache.get();
if (cached) return cached;
const loading = load().catch((error: unknown) => {
cache.clear();
throw error;
});
return cache;
cache.set(loading);
return loading;
}
export function clearEnvCache() {
cache = undefined;
cache.clear();
}
async function load() {