docs(architecture): state-mgmt patterns, zddcMode dispatcher, polyfill gaps
Three additive sections in ARCHITECTURE.md:
1. Promote a recommended state-management pattern. Three patterns coexist
in the codebase (direct mutation, store pub-sub, Proxy-reactive); the
recommendation for new tools is direct mutation + explicit re-render —
it is the boring pick, debuggable, and what 5 of 7 IIFE-pattern tools
already use. Reactive is appropriate when one state property drives
≥3 independent UI regions (transmittal's mode/published/locked).
2. Document the zddcMode dispatcher contract used by the unified
tables.html bundle that hosts both the form renderer and the table
view. Standalone form/dist/form.html intentionally has no zddcMode
set; undefined means "form mode" by back-compat.
3. List zddc-source.js known gaps so callers don't fall into them:
- recursive directory removal not implemented (HTTP backend has no
recursive-DELETE endpoint; tools that rename non-empty dirs by
copy+remove will leak the source dir)
- no truncate semantics on writes (whole-file replacement only)
- directory listings re-fetched per traversal (no client-side cache)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bf5ea7aa4f
commit
346cbba688
1 changed files with 47 additions and 6 deletions
|
|
@ -278,17 +278,29 @@ main.js ← Initialization (depends on all modules)
|
|||
|
||||
### State Management
|
||||
|
||||
Tools manage state in one of two patterns:
|
||||
Three patterns coexist. **For new tools, prefer the first one** — direct mutation on `window.app` with explicit re-render. It's debuggable, it's the most common pattern in this codebase (archive, mdedit, browse, form, tables), and it doesn't hide control flow.
|
||||
|
||||
**1. Direct state on `window.app`** (archive, classifier, mdedit)
|
||||
**1. Direct mutation on `window.app` + explicit re-render** *(recommended for new tools)*
|
||||
|
||||
```javascript
|
||||
window.app = { files: [], selectedFolders: new Set(), modules: {}, ... };
|
||||
// Mutate then re-render:
|
||||
window.app.files.push(newFile);
|
||||
window.app.modules.table.render();
|
||||
```
|
||||
|
||||
State is read directly; mutations trigger explicit re-render calls. Classifier additionally layers a small pub-sub on top via `store.js` (`store.on('files', render)`).
|
||||
State is read directly. Mutations trigger explicit `render()` calls — no auto-tracking, no surprise updates. Used by archive, mdedit, browse, form, tables, landing.
|
||||
|
||||
**2. Proxy-based reactive state** (transmittal)
|
||||
**2. Pub-sub store on top of #1** (classifier)
|
||||
|
||||
```javascript
|
||||
store.set('files', newFiles);
|
||||
store.on('files', render);
|
||||
```
|
||||
|
||||
Adds a tiny `store.on(key, fn)` / `store.notify(key)` layer in `classifier/js/store.js`. Justification: classifier has multiple independent panels (file list, spreadsheet, validation pane) that all need to react to the same state changes; calling three separate `render*()` functions from every mutation site would invite forgetting one.
|
||||
|
||||
**3. Proxy-based reactive state** (transmittal)
|
||||
|
||||
```javascript
|
||||
const state = createReactiveState({ mode: 'edit', published: false });
|
||||
|
|
@ -296,7 +308,26 @@ state.subscribe((prop, newVal) => { /* auto-update UI */ });
|
|||
state.mode = 'view'; // Proxy notifies all subscribers automatically
|
||||
```
|
||||
|
||||
Use reactive state when the same property drives multiple independent UI elements. Use direct state when the data flow is simple and unidirectional.
|
||||
Used by transmittal because a single state change (e.g. `mode`) drives ≥3 independent UI regions (header chrome, body editability, action toolbar). Reactive shines when the cross-cutting wiring would otherwise be tedious. **Don't reach for this pattern unless you have at least three subscribers per state property.**
|
||||
|
||||
### `zddcMode` dispatcher (form / tables unified bundle)
|
||||
|
||||
The form and tables tools share a single compiled bundle (`tables/dist/tables.html`, also `//go:embed`d into `zddc-server` at `zddc/internal/handler/tables.html`). One window, two views. The bundle holds both `window.tablesApp` and `window.formApp`; whichever app paints is decided by a single global:
|
||||
|
||||
```javascript
|
||||
// Set by the server-injected context (or absent for standalone form.html):
|
||||
window.zddcMode = 'form' // → form renderer paints; tables app no-ops
|
||||
window.zddcMode = 'table' // → tables app paints; form app no-ops
|
||||
window.zddcMode = undefined // → standalone form.html, treated as 'form'
|
||||
```
|
||||
|
||||
Each app's `main.js` checks `window.zddcMode` first and returns early when it's not their mode (see `form/js/main.js:10`, `tables/js/mode.js`). Rules for adding a third mode:
|
||||
|
||||
1. Set `window.zddcMode = '<new>'` in `tables/js/context.js` based on server context shape.
|
||||
2. Add the new app's main module with the same early-return guard.
|
||||
3. Keep the standalone-fallback rule consistent: undefined `zddcMode` should still mean "the lightest, most common mode for this bundle's standalone HTML."
|
||||
|
||||
Standalone `form/dist/form.html` uses this contract too — it has no `zddcMode` set, so form's main runs unconditionally and renders either the schema (when injected) or a friendly empty-state welcome (`form/js/main.js renderStandaloneWelcome`).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -671,7 +702,17 @@ zddc-server exposes write methods on the same URL space as GET. Each method maps
|
|||
|
||||
Writes use `WriteAtomic` (temp file → fsync → rename) for partial-write safety. Move uses `os.Rename` for same-FS atomicity. Body size capped by `--max-write-bytes` (default 256 MiB). Reserved hidden segments (`.`-prefixed, `_app`, `_template`) are 404'd uniformly with the read path. Every write logs a structured `file_write` event (op, path, email, status, bytes) into the same audit stream as access logs.
|
||||
|
||||
Browser clients reach the API through `shared/zddc-source.js` — an FS Access API polyfill (`HttpDirectoryHandle`, `HttpFileHandle`) that lets tools written against `showDirectoryPicker()` work unchanged when served by zddc-server. classifier, mdedit, and transmittal auto-detect HTTP mode at startup, build a polyfill handle for `location.pathname`'s directory, and skip the file picker entirely. A 403 on the initial listing surfaces a "no permission to list this directory" message instead of the welcome screen.
|
||||
Browser clients reach the API through `shared/zddc-source.js` — an FS Access API polyfill (`HttpDirectoryHandle`, `HttpFileHandle`) that lets tools written against `showDirectoryPicker()` work unchanged when served by zddc-server. classifier, mdedit, transmittal, and browse auto-detect HTTP mode at startup, build a polyfill handle for `location.pathname`'s directory, and skip the file picker entirely. A 403 on the initial listing surfaces a "no permission to list this directory" message instead of the welcome screen.
|
||||
|
||||
#### `zddc-source.js` known gaps
|
||||
|
||||
The polyfill covers the FS Access surface tools actually use. A few corners are intentionally unimplemented — note them when adding new tool features:
|
||||
|
||||
- **Recursive directory removal is not implemented.** `HttpDirectoryHandle.removeEntry(name, { recursive: true })` is a no-op against the server because there is no recursive-DELETE endpoint. Tools that rename a non-empty directory by copy + remove (the FS-Access idiom) will leave the source directory orphaned in HTTP mode. Detect this case and either guard the operation or implement server-side `POST X-ZDDC-Op: move` for the directory.
|
||||
- **Writes have no truncate semantics.** Each PUT replaces the whole file. There's no `FileSystemWritableFileStream.truncate(size)` analogue; partial-write support means partial-overwrite-via-streaming is the polyfill's only write path.
|
||||
- **Directory listings are not cached on the client side.** Cache mode does cache file responses (and persists `.zddc-listing.<json|html>` sidecars on the *server* side), but the polyfill itself re-fetches `?json=1` listings on every traversal. Tools that re-enter the same directory many times in quick succession should cache results in tool state.
|
||||
|
||||
These are deliberate scope decisions, not bugs. Lift any of them only when a concrete tool feature pays for the implementation cost.
|
||||
|
||||
### Why the tool-rooted view matters for third-party containment
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue