mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 10:12:34 +00:00
Implement M1 authoring loop: library, editor, live preview, persistence
M1 MVP from docs/IMPLEMENTATION-PLAN.md, with the /alignment pass applied. - core: Snippet model + factory; prepareSpecForRender (copy-not-mutate); per-theme Vega chart config - state/orchestration: SnippetStore with debounced auto-save; IndexedDB adapter + read-time migration; startup hydration + write-through persistence - ui: SnippetLibrary, SpecEditor (Monaco edcore.main — full editor features, JSON-only languages), LivePreview - build: Monaco/Vega manual chunks; raised PWA precache ceiling - alignment: flush a valid draft on snippet switch (+regression tests); TODO breadcrumbs for the preview render race and the window.confirm delete - housekeeping: gitignore .claude/projects/
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Persistence & auto-save wiring (docs/architecture/01 §5).
|
||||
*
|
||||
* Bridges the pure SnippetStore to the IndexedDB adapter via startup
|
||||
* subscribers — the store stays browser-free, and all reads/writes funnel
|
||||
* through `infrastructure/`. Two subscribers:
|
||||
*
|
||||
* 1. Debounced auto-save: editor keystrokes (`draftText`) settle into a
|
||||
* `commitDraft()` after a pause (spec §03B).
|
||||
* 2. Write-through: any change to the `snippets` array is diffed against the
|
||||
* previous snapshot and persisted/deleted. Because commits and structural
|
||||
* edits produce fresh object references, a reference diff catches exactly
|
||||
* what changed.
|
||||
*/
|
||||
|
||||
import { deleteSnippet, saveSnippet } from '../infrastructure/snippet-store';
|
||||
import { useSnippetStore } from '../stores/SnippetStore';
|
||||
|
||||
/** Delay before a settled edit is committed to the stored draft (spec §03B). */
|
||||
export const AUTOSAVE_DEBOUNCE_MS = 400;
|
||||
|
||||
type Unsubscribe = () => void;
|
||||
|
||||
/** Debounce `commitDraft` on editor-buffer changes. */
|
||||
function wireDraftAutoSave(): Unsubscribe {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
return useSnippetStore.subscribe((s, prev) => {
|
||||
if (s.draftText === prev.draftText) return;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => useSnippetStore.getState().commitDraft(), AUTOSAVE_DEBOUNCE_MS);
|
||||
});
|
||||
}
|
||||
|
||||
/** Persist snippet upserts and deletions whenever the array changes. */
|
||||
function wireWriteThrough(): Unsubscribe {
|
||||
let prevSnippets = useSnippetStore.getState().snippets;
|
||||
return useSnippetStore.subscribe((s) => {
|
||||
const next = s.snippets;
|
||||
if (next === prevSnippets) return;
|
||||
const prev = prevSnippets;
|
||||
prevSnippets = next;
|
||||
|
||||
for (const old of prev) {
|
||||
if (!next.some((n) => n.id === old.id)) void deleteSnippet(old.id);
|
||||
}
|
||||
for (const n of next) {
|
||||
const old = prev.find((p) => p.id === n.id);
|
||||
if (old !== n) void saveSnippet(n); // new, or a changed reference
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Wire all persistence subscribers. Returns a teardown that detaches them. */
|
||||
export function wirePersistence(): Unsubscribe {
|
||||
const unsubs = [wireDraftAutoSave(), wireWriteThrough()];
|
||||
return () => unsubs.forEach((u) => u());
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* App startup orchestration.
|
||||
*
|
||||
* Loads the snippet library from IndexedDB into the store, seeds a sample
|
||||
* snippet on first run (spec §02 → "On first run … seed one sample bar-chart
|
||||
* snippet"), then wires the persistence subscribers. Called once from main.tsx;
|
||||
* the UI renders immediately and fills in when hydration completes.
|
||||
*/
|
||||
|
||||
import { createSnippet } from '@core/snippet';
|
||||
import { loadSnippets, saveSnippet } from '../infrastructure/snippet-store';
|
||||
import { useSnippetStore } from '../stores/SnippetStore';
|
||||
import { wirePersistence } from './persistence';
|
||||
|
||||
let started = false;
|
||||
|
||||
export async function initApp(): Promise<void> {
|
||||
if (started) return; // idempotent — guard against double-invocation (StrictMode)
|
||||
started = true;
|
||||
|
||||
let snippets = await loadSnippets();
|
||||
if (snippets.length === 0) {
|
||||
const sample = createSnippet();
|
||||
await saveSnippet(sample); // ensure the seed survives even before the first edit
|
||||
snippets = [sample];
|
||||
}
|
||||
|
||||
useSnippetStore.getState().hydrate(snippets);
|
||||
|
||||
// Wire persistence AFTER hydrate so write-through's baseline is the loaded set
|
||||
// — otherwise it would redundantly re-save every snippet on each startup.
|
||||
wirePersistence();
|
||||
}
|
||||
Reference in New Issue
Block a user