2026 03 11_graphstore_vs_client_storage_manager_note - mark-ik/graphshell GitHub Wiki
Date: 2026-03-11
Status: Active architecture note
Purpose: Define the clean architectural seam between Graphshell app-state durability (GraphStore) and a future Servo-compatible web-origin storage authority (ClientStorageManager).
Related:
SUBSYSTEM_STORAGE.md2026-03-08_unified_storage_architecture_plan.md2026-03-11_client_storage_manager_implementation_plan.mdstorage_and_persistence_integrity_spec.md../../research/2026-03-04_standards_alignment_report.md
Graphshell already has a substantial persistence subsystem centered on GraphStore.
That subsystem is responsible for Graphshell's own durable app state:
- graph WAL and snapshots
- workspace layout persistence
- archive persistence
- recovery, integrity, and encryption health
That is not the same problem as browser client storage for web-origin APIs such as IndexedDB, localStorage, Cache API, OPFS, or future bucket-aware storage clients.
The WHATWG Storage Standard applies to the latter problem. Its canonical model
is storage-key-scoped and hierarchical: storage shed -> storage shelf -> storage
bucket -> storage bottle. A future Servo-facing ClientStorageManager should
implement that model. GraphStore should not be forced into it.
| Concern | GraphStore |
Future ClientStorageManager
|
|---|---|---|
| Primary scope | Graphshell app durability | Web-origin client storage |
| Authority key | Graphshell graph/workspace identity | WHATWG storage key |
| Main data classes | Graph, layouts, archives, settings payloads | Buckets, bottles, endpoint storage roots, quota/persistence metadata |
| Canonical model | WAL + snapshots + archives | Shed + shelf + bucket + bottle |
| Durability contract | App recovery and integrity | Site data lifecycle and storage API coordination |
| Clearing semantics | Clear graph / layouts / archives | Clear site data / delete bucket / async purge |
| Session model | App/workbench session persistence | Local vs session storage split |
| Standard target | Internal Graphshell contracts | WHATWG Storage Standard |
Key rule:
GraphStore is the persistence authority for Graphshell-owned application
state. ClientStorageManager would be the storage authority for browser-origin
storage clients. They may share lower-level crypto/path/diagnostic utilities,
but they are not the same subsystem surface and should not share a conceptual
model.
Compatibility rule:
Graphshell should treat Servo's storage-spec work as the canonical target model for browser-origin storage. Any Graphshell-layer client-storage code must be Servo-compatible first and should avoid inventing a rival hierarchy or metadata model that would later need to be translated back into Servo terms.
ClientStorageManager is the in-memory authority for origin-scoped storage
metadata and policy. It should own:
- storage-key to shelf resolution
- bucket metadata and bucket mode (
best-effort/persistent) - bottle metadata per registered storage endpoint
- session-vs-local storage separation
- private browsing isolation
- usage/quota accounting hooks
- site-data clearing and asynchronous deletion scheduling
- endpoint-neutral lookup of physical storage roots
It should not directly embed every storage client's data model. The bottle's map is a high-level abstraction over a given endpoint's stored data; endpoint implementations remain responsible for their own internal representation.
Graphshell should prefer one of two implementation postures:
- a thin host-facing adapter over Servo's eventual storage authority, or
- a staging layer that uses Servo-compatible concepts, naming, and ownership so it can collapse into Servo's implementation later without semantic churn.
It should not pursue a third, independent browser-storage architecture.
The manager should load all authoritative metadata into memory during startup, then persist metadata changes through a pluggable backend. This keeps spec-level operations synchronous or cheaply asynchronous at the runtime boundary, while still allowing different physical storage implementations.
Suggested internal state:
local_shed: StorageShedsession_sheds: HashMap<TraversableId, StorageShed>private_local_sheds: HashMap<PrivateScopeId, StorageShed>registered_endpoints: HashMap<StorageIdentifier, EndpointDescriptor>pending_deletions: Vec<PendingBucketDeletion>
The backend should persist metadata and allocate physical bucket roots, but it should not be the authority for the live hierarchy. The manager is the authority; the backend is a persistence and storage-allocation service.
Above that, Graphshell itself still has a legitimate host/runtime role for backend orchestration, especially where Servo and Wry need interoperability policy rather than shared physical storage formats.
Graphshell should define a small host-side orchestration layer above browser storage authority. A useful name is StorageInteropCoordinator.
This layer is not a third storage authority. It does not own storage keys, shelves, buckets, bottles, quota, or bucket lifecycle semantics. Instead it owns backend orchestration and compatibility policy for browser runtimes.
Recommended responsibilities:
- map nodes/panes/views to browser storage contexts or profile identities
- decide whether a backend switch is shared-context, cloned-context, or isolated-fallback
- route explicit user commands such as "clear site data for current node" or "reload in Wry" to the correct backend authority
- mediate Wry profile/session handling so it stays conceptually compatible with Servo, even if the physical data formats differ
- expose diagnostics about backend-specific storage continuity limits
Recommended non-responsibilities:
- owning the Storage Standard hierarchy
- redefining storage-key semantics
- persisting canonical bucket metadata separately from Servo-compatible storage
- silently merging Servo and Wry physical storage stores
Within Graphshell's current architecture, ClientStorageManager should live on
the Verso / browser-runtime side of the system, not in GraphStore, and not in
the reducer-owned graph domain.
Recommended module layout:
mods/native/verso/client_storage/
mod.rs // public facade + runtime wiring entrypoints
manager.rs // ClientStorageManagerImpl; in-memory authority
types.rs // storage-key, shed/shelf/bucket/bottle types
backend.rs // ClientStorageBackend trait + persistence contracts
endpoint.rs // StorageEndpointClient trait + endpoint descriptors
quota.rs // usage/quota estimation and policy hooks
deletion.rs // async bucket/site-data deletion queue
private_scope.rs // private browsing scope isolation helpers
session.rs // traversable/session shed ownership helpers
diagnostics.rs // storage-manager diagnostic channel emission
bridge.rs // thin Servo/Verso integration boundary
app/storage_interop/
mod.rs // host-facing orchestration facade
coordinator.rs // StorageInteropCoordinator
context_map.rs // node/pane/backend -> storage context mapping
transition_policy.rs // Servo <-> Wry transition decisions
wry_profile.rs // Wry-specific profile/session helpers
commands.rs // explicit user-facing clear/reload/forget actions
If Graphshell later completes the planned core/host split, only the pure value
types from types.rs are candidates for extraction into a host-shared crate.
The manager, backend, deletion queue, and Servo bridge remain host/runtime code.
The storage_interop layer is also host/runtime code.
ClientStorageManager should follow the same authority separation Graphshell
already uses elsewhere:
- GraphWorkspace / reducer domain: does not own browser site-data metadata and does not mutate storage shelves/buckets/bottles.
-
AppServices / runtime instances: owns the live
ClientStorageManagerhandle, just as it owns runtime services such as embedder state. - Verso / EmbedderCore bridge: obtains bottle handles or storage lookups from the manager, but does not become the authority for shelf/bucket metadata.
- StorageInteropCoordinator: runtime-owned Graphshell policy layer that can coordinate Servo/Wry backend transitions and user-facing compound actions, but does not become the owner of storage truth.
This keeps site-data policy where it belongs: runtime-owned, not graph-owned.
Suggested concrete type inventory:
pub struct StorageKey {
pub origin: ImmutableOrigin,
pub partition: Option<StoragePartitionKey>,
}
pub enum StorageScope {
Local,
Session(TraversableId),
Private(PrivateScopeId),
}
pub struct StorageShed {
pub shelves: HashMap<StorageKey, StorageShelf>,
}
pub struct StorageShelf {
pub key: StorageKey,
pub bucket_map: HashMap<BucketName, StorageBucket>,
}
pub struct StorageBucket {
pub name: BucketName,
pub mode: BucketMode,
pub generation: BucketGeneration,
pub bottle_map: HashMap<StorageIdentifier, StorageBottle>,
pub root: PhysicalBucketRoot,
}
pub struct StorageBottle {
pub endpoint: StorageIdentifier,
pub quota_hint: Option<u64>,
pub proxy_generation: BucketGeneration,
}
pub struct EndpointDescriptor {
pub identifier: StorageIdentifier,
pub storage_types: StorageTypeSet,
pub quota_hint: Option<u64>,
}
pub struct PendingBucketDeletion {
pub locator: BucketLocator,
pub generation: BucketGeneration,
pub ticket: DeletionTicket,
}Notes:
-
StorageKeymust be future-proofed for partitioning; do not hard-code origin-only semantics into naming. -
BucketGenerationis the simplest way to make async deletion safe: a new bucket with the same logical name can exist while an older generation is still being deleted. -
PhysicalBucketRootis implementation-defined and backend-owned, but the manager keeps the authoritative mapping.
-
GraphStoreowns Graphshell app durability only. -
ClientStorageManagerImplowns all in-memory shelf/bucket/bottle metadata. -
ClientStorageBackendowns durable metadata persistence and physical bucket root allocation/deletion only. -
StorageEndpointClientimplementations own endpoint-specific bottle usage, but not bucket creation, persistence mode, quota policy, or site clearing. - Session sheds are owned by the manager and keyed by
TraversableId; endpoint clients must not retain authority after the traversable is closed. - Private scope sheds are isolated from durable local sheds and must be torn down wholesale when the private scope ends.
- Servo callbacks and embedder code may request storage handles, but must not mutate the hierarchy except through manager APIs.
- The deletion worker may delete physical roots asynchronously, but must not change logical metadata except through manager-owned state transitions.
-
StorageInteropCoordinatormay map nodes/panes/backends to storage contexts and invoke explicit clear/reload actions, but it must not become the source of truth for storage-key, bucket, or quota metadata.
Recommended boot sequence:
- App startup constructs
AppServices. - Verso runtime initializes
EmbedderCore. -
ClientStorageBackend::load_metadata()loads persisted storage metadata. -
ClientStorageManagerImpl::from_snapshot(...)reconstructs in-memory sheds, shelves, buckets, and endpoint descriptors. - Registered storage endpoints are attached.
- Deletion queue resumes any orphaned pending deletions.
- Runtime stores the manager handle in
AppServices.
Request path:
- Servo/Verso asks for a bottle using
(scope, storage key, bucket, endpoint). - Manager resolves or creates the shelf and bucket in memory.
- Manager asks backend to ensure the bucket root exists if needed.
- Manager returns a bottle handle to the endpoint client.
- Endpoint client performs its own API-specific storage operations under that bottle/root.
Deletion path:
- Manager marks the bucket generation as deleted in memory.
- Manager persists updated metadata.
- Backend schedules physical deletion for the old generation.
- New bucket generation may be created immediately with the same logical name.
Graphshell interop path:
- A node/pane is associated with a backend runtime and a storage context id.
- If the user requests reload in another backend,
StorageInteropCoordinatorresolves the transition policy. - The coordinator chooses one of: shared logical context, cloned compatibility context, or isolated fallback context.
- The target backend is started with the selected context binding and any explicit continuity warnings/diagnostics.
The manager should be optimized for cheap reads from in-memory metadata.
Recommended posture:
-
ClientStorageManagerImplbehindArc<RwLock<...>>or an equivalent runtime ownership model. - Read-side resolution of shelves/buckets/bottles should avoid synchronous disk lookups.
- Metadata persistence and physical directory deletion happen off the hot path.
- Endpoint clients should receive stable handles containing logical identity and physical root information, not mutable references into the manager's internal maps.
This matches the Zulip design discussion: spec-facing lookups should resolve against already-loaded in-memory hierarchy, not by blocking on a database for every bottle request.
Recommended diagnostic families for this future module:
client_storage.metadata.loadedclient_storage.metadata.persist_failedclient_storage.bucket.createdclient_storage.bucket.deletion_scheduledclient_storage.bucket.deletion_failedclient_storage.quota.estimate_failedclient_storage.scope.private_destroyedclient_storage.session_shed.cleared
Important failure rule:
metadata failure and payload failure must be distinguished. A broken IndexedDB database file is not the same category of fault as a corrupted bucket registry. The manager owns registry integrity; endpoint clients own endpoint-local data integrity.
For Graphshell interop, add a third category:
- interop continuity failure — the browser backend switch could not preserve storage continuity exactly, so Graphshell had to fall back to a cloned or isolated context. This is not a registry corruption and not an endpoint payload corruption; it is a host-level compatibility limitation.
Graphshell nodes are references to content and browsing contexts. They are not, by default, the owners of the underlying browser site data.
Rules:
- Deleting a node does not automatically delete site data.
- Site data is owned by the browser storage authority (
ClientStorageManagerfor Servo-backed contexts; backend-specific profile manager for Wry-backed contexts). - Graphshell may expose explicit compound actions such as:
- delete node only
- clear site data only
- delete node and clear site data
- If Graphshell wants automatic cleanup heuristics, they must operate on storage-key or context reference policy, not on the assumption that one node owns one site's data.
This is a policy hierarchy, not an ownership hierarchy:
- browser storage authority owns storage truth
- Graphshell owns reference truth
- cross-effects happen only through explicit policy
Graphshell should assume that Servo and Wry will often have different physical storage implementations and on-disk formats.
Therefore compatibility should be defined at the policy level, not by assuming binary-compatible stores.
Recommended transition classes:
| Transition class | Meaning | When to use |
|---|---|---|
| Shared logical context | Same logical storage context id is preserved across backends | Only when semantics and implementation support it safely |
| Cloned compatibility context | Relevant state is copied or approximated into a backend-specific context | When continuity is desired but exact store sharing is unsafe |
| Isolated fallback context | New backend starts with an isolated context/profile | When compatibility is uncertain or unsupported |
Recommended default posture:
- Servo is the canonical target for browser-origin storage semantics.
- Wry fallback should be treated as a peer runtime with its own profile/store.
- Graphshell should not promise exact physical storage sharing between Servo and Wry unless proven safe for a given data class.
Likely continuity classes by default:
- cookies / permissions: maybe clonable, backend-dependent
- localStorage / sessionStorage: maybe clonable, not assumed shareable
- IndexedDB / Cache API / OPFS / service workers: not assumed shareable; default to isolated or explicitly migrated contexts
User-facing commands should make the policy explicit:
- reload in Servo
- reload in Wry
- reload in isolated fallback context
- clear site data for current storage context
- forget Wry fallback profile
pub trait ClientStorageManager {
fn obtain_local_shelf(
&self,
key: &StorageKey,
scope: StorageScope,
) -> Result<StorageShelfHandle, StorageError>;
fn obtain_session_shelf(
&self,
traversable: TraversableId,
key: &StorageKey,
) -> Result<StorageShelfHandle, StorageError>;
fn obtain_bottle(
&self,
scope: StorageScope,
key: &StorageKey,
bucket: BucketName,
endpoint: StorageIdentifier,
) -> Result<StorageBottleHandle, StorageError>;
fn create_bucket(
&mut self,
scope: StorageScope,
key: &StorageKey,
name: BucketName,
) -> Result<BucketDescriptor, StorageError>;
fn delete_bucket_async(
&mut self,
scope: StorageScope,
key: &StorageKey,
name: BucketName,
) -> Result<DeletionTicket, StorageError>;
fn set_bucket_persistence(
&mut self,
scope: StorageScope,
key: &StorageKey,
name: BucketName,
mode: BucketMode,
) -> Result<(), StorageError>;
fn estimate_usage(
&self,
scope: StorageScope,
key: &StorageKey,
) -> Result<StorageEstimate, StorageError>;
fn clear_storage_key(
&mut self,
scope: StorageScope,
key: &StorageKey,
) -> Result<DeletionTicket, StorageError>;
}
pub trait ClientStorageBackend {
fn load_metadata(&self) -> Result<ClientStorageSnapshot, StorageError>;
fn persist_metadata(
&self,
snapshot: &ClientStorageSnapshot,
) -> Result<(), StorageError>;
fn ensure_bucket_root(
&self,
locator: &BucketLocator,
) -> Result<PhysicalBucketRoot, StorageError>;
fn schedule_bucket_root_deletion(
&self,
root: &PhysicalBucketRoot,
generation: BucketGeneration,
) -> Result<DeletionTicket, StorageError>;
fn estimate_bucket_usage(
&self,
locator: &BucketLocator,
) -> Result<u64, StorageError>;
}
pub trait StorageEndpointClient {
fn identifier(&self) -> StorageIdentifier;
fn storage_types(&self) -> StorageTypeSet;
fn open_bottle(
&self,
bottle: &StorageBottleHandle,
) -> Result<Box<dyn EndpointStorageHandle>, StorageError>;
}Interface notes:
-
ClientStorageManagerowns the spec hierarchy and policy. -
ClientStorageBackendis pluggable and implementation-defined. -
StorageEndpointClientlets IndexedDB, localStorage, Cache API, OPFS, and future clients consume the same hierarchy without each inventing separate registry logic.
Concrete assignment:
-
manager.rsownsClientStorageManagerImpl. -
backend.rsowns the trait plus the default backend adapter. -
endpoint.rsowns endpoint registration and endpoint-facing handles. -
bridge.rsowns the only Servo/Verso-facing integration seam.
- Use
storage key, notorigin, in public API and metadata naming. - Persist bucket/shelf/bottle metadata; named buckets must survive restart.
- Load metadata into memory during initialization.
- Support multiple buckets from the start, even if only
defaultis fully wired initially. - Treat async deletion as first-class; deletion should not require all existing endpoint handles to close first.
- Keep session storage separate from local durable storage.
- Do not fold Graphshell app durability into the Storage Standard hierarchy.
- Keep Graphshell's browser-storage orchestration Servo-compatible first; avoid creating a rival browser-storage model.
- Treat node deletion and site-data deletion as distinct operations unless an explicit compound policy says otherwise.
- Treat Servo/Wry continuity as a host-level compatibility policy problem, not as proof of shared physical storage.
For Graphshell documentation and future Servo-facing work, use this split:
-
GraphStoreremains the owner of Graphshell app durability. -
ClientStorageManageris the reserved name for future WHATWG-style browser client storage coordination. - Shared helpers are acceptable below that seam, but not a merged conceptual model.