The Application Layer
client/src/application/ contains the three pieces of plumbing every feature
relies on. None of them know anything about books or catalogs:
| Module | Responsibility |
|---|---|
agent.ts |
The send-only protocol agent — the app’s single connection to the server |
store.ts |
An IndexedDB-backed key/value store for persistent client state |
query.ts |
The TanStack Query client and the command() helper |
The Client Agent
The client instantiates the same Agent class the server runs — but as a
sender only. It carries no encryption key, no operations, and no database.
Just the shared protocol:
// client/src/application/agent.ts
import { createAgent, generate as genIdx } from "@market/shared"
import store from "./store"
// the client agent only ever sends messages — it carries no encryption key,
// no operations, and no database. Just the shared protocol.
const id = store.ensure("agent", genIdx())
const hub = new URL("/io", location.origin).toString()
export const agent = createAgent({ id, hub, operations: {} })
The agent’s network id (x:…) is generated once and persisted, so the client
keeps a stable identity across reloads while remaining disposable by design.
Persistent State: the IndexedDB Store
store.ts is a small cache-through key/value store on top of idb-keyval.
It hydrates from IndexedDB on boot and pushes writes back, with a guard so
nothing persists before hydration completes:
// client/src/application/store.ts (excerpt)
export const store = {
set(key: string, value: any): void {
cache[key] = value
// only persist after hydration so we don't clobber data before pull()
if (hydrated) {
this.push().then(() => channels.send(key, value))
}
},
// ensure a value exists, returning the existing or newly-set value
ensure<T>(key: string, value: T): T {
if (!cache[key]) this.set(key, value)
return cache[key]
},
// load the cache from IndexedDB
async pull() {
const data = await idb.get(storeKey)
Object.assign(cache, data)
hydrated = true
await this.push()
return cache
},
}
It stores exactly two kinds of things: the agent id and the auth
session (token, username, role). Subscribers can watch keys via the shared
pub/sub channels helper — the same utility the protocol package uses for
agent events.
Boot Sequence
store.pull()hydrates the cache from IndexedDB.- The agent is created with the persisted (or freshly generated) id.
- The Pinia auth store’s
init()restores a persisted session, if any (see State Management). - The router mounts the requested view; guarded routes redirect to
/loginwhen no session exists.
The shell stays intentionally small: views and composables never touch
fetch, IndexedDB, or tokens directly — they compose the three application
modules instead.