Add the reusable browser shell primitives Phase B views build on: - store.js: observable store (get/set/subscribe) - events.js: job activity adapter — SSE preferred, polling fallback, sharing the event `seq` as cursor so a transport switch drops nothing - api.js: cancellable() (AbortController) + job endpoints; aborted requests reject with code "cancelled" Make the SSE stream generically consumable: emit default `message` events with the type in the JSON payload instead of `event: <type>`, so a browser EventSource receives the open-ended type set (state:*, claimed, …) via onmessage without enumerating it. The `event: done` sentinel and resumable `id:` cursors are unchanged. Tests: frontend/js/tests/ in-browser unit suite (api errors + cancellation, store transitions, routing, SSE→polling fallback) served over the app's static mount and driven by tests/e2e/test_frontend_shell.py, which also asserts asset loading, deep-link + reload restore, JSON-only /api/v1, and a clean console/network. Reuses the installed playwright — no JS toolchain added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
24 lines
745 B
JavaScript
24 lines
745 B
JavaScript
// Shared observable store: one state object, subscribe for changes, set() to
|
|
// patch. Views read state and re-render on notify. Nothing here touches the URL
|
|
// (the router owns navigable state) or the network (the api client owns that) —
|
|
// this is only the in-memory glue between them.
|
|
export function createStore(initial = {}) {
|
|
let state = { ...initial };
|
|
const listeners = new Set();
|
|
|
|
function set(patch) {
|
|
const next = typeof patch === "function" ? patch(state) : patch;
|
|
state = { ...state, ...next };
|
|
for (const listener of listeners) listener(state);
|
|
}
|
|
|
|
return {
|
|
get: () => state,
|
|
set,
|
|
subscribe(listener) {
|
|
listeners.add(listener);
|
|
return () => listeners.delete(listener);
|
|
},
|
|
};
|
|
}
|