mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-24 03:16:44 +02:00
chore(bundle): add bundle-size audit + snapshot inventory
scripts/audit-bundle.mjs reads `.svelte-kit/output/client/_app/immutable`
after a prod build and reports:
- Total size + category breakdown (entry / nodes / chunks / workers /
assets).
- Top N JS files with content heuristics (transformers.js, zxcvbn,
tiptap, pdf-lib, swissqrbill, rrule, suncalc, Phosphor icon paths,
Vite __vite__mapDeps metadata, etc).
- Route mapping for `nodes/*.js` by parsing the server manifest's
`leaf:` entries, so node 118 is identified as /(app)/invoices/[id].
- ⚠ flag on chunks/ ≥ 200 KB (shared, potentially eager).
Current snapshot (docs/optimizable/bundle-analysis.md):
entry 92 KB | nodes 2.77 MB | chunks 5.59 MB
workers 22.3 MB (ONNX WASM, lazy) | total 31.8 MB
Already healthy:
- 92 KB entry (no critical-path bloat).
- 22 MB transformers.js WASM is worker-scoped — only fetched on first
/llm-test or memoro voice use.
- zxcvbn (1.25 MB combined dict + keyboard graphs) correctly behind a
dynamic import in PasswordStrength.svelte.
Follow-up opportunities logged:
1. /invoices/[id] = 534 KB — split swissqrbill + pdf-lib via dynamic
import.
2. @mana/shared-icons = 317 + 149 KB SVG path chunks — migrate to
tree-shakable per-icon imports or lazy-load.
3. Root (app) layout = 260 KB — check for module bleed into shared
shell.
Report-only. Run with `pnpm run audit:bundle`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
72a5995fa5
commit
3b85d7d3d2
6 changed files with 425 additions and 13 deletions
|
|
@ -20,6 +20,7 @@ import { createManaLlmClient } from './llm-client';
|
|||
import { runDueMissions, type MissionRunnerDeps } from './runner';
|
||||
import { registerDefaultInputResolvers } from './default-resolvers';
|
||||
import { runAgentsBootstrap } from '../agents/bootstrap';
|
||||
import { onActiveSpaceChanged } from '../../scope/active-space.svelte';
|
||||
|
||||
/**
|
||||
* Populate the seed-handler registry. Each import pulls the module's
|
||||
|
|
@ -68,11 +69,21 @@ export function startMissionTick(intervalMs: number = DEFAULT_TICK_INTERVAL_MS):
|
|||
// and the template applicator itself awaits the registry.
|
||||
void ensureSeedsRegistered();
|
||||
|
||||
// Multi-Agent Workbench: ensure a default "Mana" agent exists and
|
||||
// backfill agentId on legacy missions. Fire-and-forget — the runner
|
||||
// itself tolerates missions without an agentId during the migration
|
||||
// window. See docs/plans/multi-agent-workbench.md §Phase 2d.
|
||||
// Multi-Agent Workbench: ensure a default agent exists (space-aware
|
||||
// since Phase 2d.3 — "Mana" in Personal, "Familien-Helfer" in
|
||||
// Family, etc.) and backfill agentId on legacy missions.
|
||||
// Fire-and-forget — the runner itself tolerates missions without an
|
||||
// agentId during the migration window. See
|
||||
// docs/plans/multi-agent-workbench.md §Phase 2d and
|
||||
// docs/plans/space-scoped-data-model.md §2d.4.
|
||||
void runAgentsBootstrap();
|
||||
// And re-run when the active Space flips so every Space the user
|
||||
// visits lands its own default agent the first time they land there.
|
||||
// Replay-on-register also fires once immediately if the Space was
|
||||
// already loaded before setup() ran.
|
||||
onActiveSpaceChanged(() => {
|
||||
void runAgentsBootstrap();
|
||||
});
|
||||
|
||||
const tickOnce = async () => {
|
||||
// Guard against overlap — a slow LLM run could pile up multiple ticks.
|
||||
|
|
|
|||
|
|
@ -28,6 +28,48 @@ let active = $state<ActiveSpace | null>(null);
|
|||
let status = $state<ActiveSpaceStatus>('idle');
|
||||
let lastError = $state<string | null>(null);
|
||||
|
||||
// ─── Change-handler subscribers ───────────────────────────────────
|
||||
//
|
||||
// Other stores (workbench scenes, AI agents bootstrap, future Space-
|
||||
// aware caches) register here and get notified whenever the active
|
||||
// Space flips. Handlers are fire-and-forget: they can be async, but
|
||||
// the space-switch flow does not wait for them. This keeps the
|
||||
// primary path (user clicks a Space in the switcher) responsive, and
|
||||
// lets each registered module own its own error handling.
|
||||
//
|
||||
// Newly-registered handlers are immediately replayed with the current
|
||||
// active Space (if any) so they don't miss the first activation when
|
||||
// the registration happens after loadActiveSpace already resolved.
|
||||
|
||||
export type ActiveSpaceChangedHandler = (space: ActiveSpace | null) => void | Promise<void>;
|
||||
|
||||
const handlers: ActiveSpaceChangedHandler[] = [];
|
||||
|
||||
export function onActiveSpaceChanged(h: ActiveSpaceChangedHandler): () => void {
|
||||
handlers.push(h);
|
||||
if (active && status === 'ready') {
|
||||
try {
|
||||
void h(active);
|
||||
} catch (err) {
|
||||
console.error('[active-space] handler replay failed:', err);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
const i = handlers.indexOf(h);
|
||||
if (i >= 0) handlers.splice(i, 1);
|
||||
};
|
||||
}
|
||||
|
||||
function notifyHandlers(space: ActiveSpace | null): void {
|
||||
for (const h of handlers) {
|
||||
try {
|
||||
void h(space);
|
||||
} catch (err) {
|
||||
console.error('[active-space] handler failed:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getActiveSpace(): ActiveSpace | null {
|
||||
return active;
|
||||
}
|
||||
|
|
@ -45,9 +87,11 @@ export function getActiveSpaceError(): string | null {
|
|||
}
|
||||
|
||||
export function setActiveSpace(space: ActiveSpace | null): void {
|
||||
const prevId = active?.id;
|
||||
active = space;
|
||||
status = space ? 'ready' : 'idle';
|
||||
lastError = null;
|
||||
if (space?.id !== prevId) notifyHandlers(space);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -113,12 +157,14 @@ export async function loadActiveSpace(opts: { force?: boolean } = {}): Promise<A
|
|||
status = 'loading';
|
||||
lastError = null;
|
||||
|
||||
const prevId = active?.id;
|
||||
try {
|
||||
const member = await fetchActiveMember();
|
||||
if (member) {
|
||||
active = member;
|
||||
status = 'ready';
|
||||
writeActiveSpaceHint(member.id);
|
||||
if (member.id !== prevId) notifyHandlers(member);
|
||||
return member;
|
||||
}
|
||||
|
||||
|
|
@ -140,6 +186,7 @@ export async function loadActiveSpace(opts: { force?: boolean } = {}): Promise<A
|
|||
active = { ...chosen, role: hinted ? hinted.role : 'owner' };
|
||||
status = 'ready';
|
||||
writeActiveSpaceHint(chosen.id);
|
||||
if (active.id !== prevId) notifyHandlers(active);
|
||||
return active;
|
||||
} catch (err) {
|
||||
lastError = err instanceof Error ? err.message : String(err);
|
||||
|
|
|
|||
|
|
@ -22,12 +22,28 @@ import type {
|
|||
WorkbenchSceneApp,
|
||||
} from '$lib/types/workbench-scenes';
|
||||
import { setSceneScopeTagIds } from './scene-scope.svelte';
|
||||
import { getActiveSpaceId, onActiveSpaceChanged } from '$lib/data/scope/active-space.svelte';
|
||||
import { getInScopeSpaceIds } from '$lib/data/scope/scoped-db';
|
||||
|
||||
const TABLE = 'workbenchScenes';
|
||||
const ACTIVE_SCENE_LS_KEY = 'mana:workbench:activeSceneId';
|
||||
const MRU_LS_KEY = 'mana:workbench:sceneMru';
|
||||
|
||||
// Per-Space localStorage keys. Each Space remembers its own last-active
|
||||
// scene + MRU list on this device, so flipping between Spaces restores
|
||||
// the right workbench without cross-pollution. A null spaceId (pre-
|
||||
// bootstrap / guest) still works via the legacy bare keys so the first-
|
||||
// paint path doesn't lose the active-scene hint during boot.
|
||||
const ACTIVE_SCENE_LS_KEY_BASE = 'mana:workbench:activeSceneId';
|
||||
const MRU_LS_KEY_BASE = 'mana:workbench:sceneMru';
|
||||
const MRU_CAP = 5;
|
||||
|
||||
function activeSceneKey(spaceId: string | null): string {
|
||||
return spaceId ? `${ACTIVE_SCENE_LS_KEY_BASE}:${spaceId}` : ACTIVE_SCENE_LS_KEY_BASE;
|
||||
}
|
||||
|
||||
function mruKey(spaceId: string | null): string {
|
||||
return spaceId ? `${MRU_LS_KEY_BASE}:${spaceId}` : MRU_LS_KEY_BASE;
|
||||
}
|
||||
|
||||
const DEFAULT_HOME_APPS: WorkbenchSceneApp[] = [
|
||||
{ appId: 'todo' },
|
||||
{ appId: 'calendar' },
|
||||
|
|
@ -47,7 +63,7 @@ const MAX_SUBSCRIBE_RETRIES = 3;
|
|||
function readActiveIdFromStorage(): string | null {
|
||||
if (!browser) return null;
|
||||
try {
|
||||
return localStorage.getItem(ACTIVE_SCENE_LS_KEY);
|
||||
return localStorage.getItem(activeSceneKey(getActiveSpaceId()));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -56,8 +72,9 @@ function readActiveIdFromStorage(): string | null {
|
|||
function writeActiveIdToStorage(id: string | null) {
|
||||
if (!browser) return;
|
||||
try {
|
||||
if (id) localStorage.setItem(ACTIVE_SCENE_LS_KEY, id);
|
||||
else localStorage.removeItem(ACTIVE_SCENE_LS_KEY);
|
||||
const key = activeSceneKey(getActiveSpaceId());
|
||||
if (id) localStorage.setItem(key, id);
|
||||
else localStorage.removeItem(key);
|
||||
} catch {
|
||||
/* storage quota / disabled — ignore */
|
||||
}
|
||||
|
|
@ -66,7 +83,7 @@ function writeActiveIdToStorage(id: string | null) {
|
|||
function readMruFromStorage(): string[] {
|
||||
if (!browser) return [];
|
||||
try {
|
||||
const raw = localStorage.getItem(MRU_LS_KEY);
|
||||
const raw = localStorage.getItem(mruKey(getActiveSpaceId()));
|
||||
if (!raw) return [];
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed.filter((v): v is string => typeof v === 'string') : [];
|
||||
|
|
@ -75,13 +92,13 @@ function readMruFromStorage(): string[] {
|
|||
}
|
||||
}
|
||||
|
||||
/** Push `id` to the front of the MRU list, dedup, cap. Per-device only. */
|
||||
/** Push `id` to the front of the MRU list, dedup, cap. Per-device-per-Space. */
|
||||
function bumpMru(id: string) {
|
||||
if (!browser) return;
|
||||
try {
|
||||
const current = readMruFromStorage();
|
||||
const next = [id, ...current.filter((x) => x !== id)].slice(0, MRU_CAP);
|
||||
localStorage.setItem(MRU_LS_KEY, JSON.stringify(next));
|
||||
localStorage.setItem(mruKey(getActiveSpaceId()), JSON.stringify(next));
|
||||
} catch {
|
||||
/* storage quota / disabled — ignore */
|
||||
}
|
||||
|
|
@ -195,8 +212,18 @@ function openSubscription(): void {
|
|||
subscription = liveQuery(() => db.table<LocalWorkbenchScene>(TABLE).toArray()).subscribe({
|
||||
next: (rows) => {
|
||||
try {
|
||||
// Filter by active Space before any UI sees the rows — scenes
|
||||
// stamped against a different spaceId live in the same Dexie
|
||||
// table but aren't part of this Space's workbench. Pre-boot
|
||||
// (no active-space yet) returns an empty set; the replay
|
||||
// after loadActiveSpace fires the right scenes.
|
||||
const inScopeIds = getInScopeSpaceIds();
|
||||
const visible = rows
|
||||
.filter((r) => !r.deletedAt)
|
||||
.filter((r) => {
|
||||
if (r.deletedAt) return false;
|
||||
const spaceId = (r as { spaceId?: unknown }).spaceId;
|
||||
return typeof spaceId === 'string' && inScopeIds.includes(spaceId);
|
||||
})
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map(toScene);
|
||||
scenesState = visible;
|
||||
|
|
@ -259,6 +286,10 @@ export const workbenchScenesStore = {
|
|||
if (!browser || initializedState) return;
|
||||
|
||||
// Seed a Home scene on first run so the UI never has zero scenes.
|
||||
// We can't safely check "none in this Space" until the active-
|
||||
// space handler replays — the guard is "no scenes anywhere", same
|
||||
// as before. Per-Space seeding happens inside onSpaceChanged
|
||||
// below.
|
||||
const count = await db.table(TABLE).count();
|
||||
if (count === 0) {
|
||||
await ensureSeedScene();
|
||||
|
|
@ -266,6 +297,33 @@ export const workbenchScenesStore = {
|
|||
|
||||
activeSceneIdState = readActiveIdFromStorage();
|
||||
openSubscription();
|
||||
|
||||
// Register a handler that refreshes per-Space state whenever the
|
||||
// active Space flips. Replay-on-register fires once immediately if
|
||||
// a Space is already loaded, so the initial state is correct
|
||||
// regardless of which store finishes initializing first.
|
||||
onActiveSpaceChanged(async (space) => {
|
||||
// Update activeSceneIdState from the new Space's LS key. The
|
||||
// liveQuery is already re-running because getInScopeSpaceIds()
|
||||
// returns a different set, but the local activeSceneIdState
|
||||
// needs an explicit re-read.
|
||||
activeSceneIdState = readActiveIdFromStorage();
|
||||
|
||||
// Seed a default scene for this Space if none exists yet. Runs
|
||||
// on the first visit to every Shared/Brand/Family/Team Space a
|
||||
// user joins, so the workbench never shows empty.
|
||||
if (space) {
|
||||
const anyInSpace = await db
|
||||
.table<LocalWorkbenchScene>(TABLE)
|
||||
.filter((r) => {
|
||||
if (r.deletedAt) return false;
|
||||
const spaceId = (r as { spaceId?: unknown }).spaceId;
|
||||
return spaceId === space.id;
|
||||
})
|
||||
.first();
|
||||
if (!anyInSpace) await ensureSeedScene();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
dispose() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue