Create Rabby Store
Create persisted UI stores with the background service as the source of truth. Preserve existing service APIs and migrate only the requested domain.
Inspect the Existing Domain
- Read these infrastructure files before changing code:
src/ui/state/createStore/createRabbyStore.ts
src/ui/state/createStore/createExtensionStoreOptions.ts
src/ui/state/createStore/createSyncedBackgroundStorage.ts
src/types/persistedStore.ts
src/background/controller/wallet.ts
src/background/utils/persistStore.ts
- Read
src/ui/state/swap.ts and src/background/service/swap.ts as the reference implementation.
- Locate the current Rematch model, all UI call sites, initialization order, background service fields, and legacy controller methods for the requested domain.
- Check the worktree and preserve unrelated user changes.
- Use
rg to find consumers before renaming, deleting, or changing a public API.
Decide State Ownership
- Put durable user preferences and business state in the background service schema.
- Keep actions, loading flags, errors, open/closed UI state, request results, and derived UI-only values out of persistence unless the task explicitly requires them.
- Use
partialize to exclude every non-persisted field and all functions from UI writes.
- Use
merge to rebuild derived or UI-only state from the hydrated background snapshot when necessary.
- Use plain Zustand or component state instead of
createRabbyStore when the entire store is transient and needs no background persistence or cross-window synchronization.
- Never access
chrome.storage directly from the UI store. Keep the path UI -> wallet -> background controller -> service store.
Implement the Background Source of Truth
Define the persisted shape once with Zod in the domain service:
const featureStoreSchema = z.object({
enabled: z.boolean().default(false),
selectedItem: itemSchema.optional(),
});
export type FeatureServiceStore = z.output<typeof featureStoreSchema>;
const createFeatureStoreTemplate = (): FeatureServiceStore =>
featureStoreSchema.parse({});
Then initialize and patch the service through the shared persistence helpers:
this.store = await createPersistStore({
name: 'feature',
template: createFeatureStoreTemplate(),
schema: featureStoreSchema,
});
patchStore(partials: Partial<FeatureServiceStore>) {
patchPersistStore(this.store, partials);
}
Follow these rules:
- Give every required persisted field a schema default.
- Keep persisted values JSON-serializable.
- Avoid asynchronous refinements and transforms to
Date, Map, class instances, or other Chrome Storage-incompatible values.
- Treat the Zod object as the authoritative persisted keys, defaults, validation, and output type. Do not maintain a duplicate field whitelist.
- Route generic UI writes through the service
patchStore method so the merged full state is validated atomically before commit.
- Preserve domain-specific service methods and controller methods still used by existing callers.
Register the Generic Background Bridge
- Add the service store type to
PersistedStoreMap in src/types/persistedStore.ts.
- Add the new key to the routing in
src/background/controller/wallet.ts for snapshot reads and partial writes.
- Delegate writes to the domain service's
patchStore(partials) method. Do not assign an incoming full UI state directly to the service store.
- Return the service snapshot with the current background origin and revision so the UI can reject stale updates without confusing a Service Worker restart for an old event.
- Keep the generic API shaped like storage:
getStorageSnapshot(key) and setStorageItem(key, partials).
Implement the UI Store
Build the domain store with createRabbyStore and createExtensionStoreOptions:
type FeatureState = FeatureServiceStore & {
transientResult: Result | null;
};
type FeatureActions = {
setEnabled: (enabled: boolean) => void;
};
export type FeatureStore = FeatureState & FeatureActions;
export const useFeatureStore = createRabbyStore<FeatureStore>(
(set) => ({
enabled: false,
selectedItem: undefined,
transientResult: null,
setEnabled(enabled) {
set({ enabled });
},
}),
createExtensionStoreOptions<FeatureStore, 'feature'>({
storageKey: 'feature',
autoHydrate: false,
partialize(state) {
const persistedState: Partial<FeatureStore> = {};
Object.entries(state).forEach(([key, value]) => {
if (key !== 'transientResult' && typeof value !== 'function') {
(persistedState as Record<string, unknown>)[key] = value;
}
});
return persistedState;
},
onError(error) {
console.error('[featureStore]', error);
},
})
);
- Call
set() for normal user actions. It performs the optimistic UI update and queues a partial background write after hydration.
- Do not call
wallet.setStorageItem manually from ordinary setters; the storage adapter owns persistence.
- Do not expose or call a remote-apply method from business code. Hydration, background broadcasts, stale-revision checks, and rollback apply authoritative state internally.
- Return persisted values fetched outside the normal sync channel directly to the caller instead of copying them into the UI store. Use ordinary
set() for UI-only fields excluded by partialize.
- Use
autoHydrate: false when startup depends on another initialization step, then expose an initializer that awaits useFeatureStore.persist.hydrate().
- Otherwise allow automatic hydration.
- Update UI consumers and remove the requested domain's Rematch bindings only after the Zustand replacement covers their behavior.
Preserve Synchronization Invariants
- Send only fields changed by the local UI action.
- Validate
current background state + partials before committing any field.
- Make one accepted background patch produce one persistence write, one revision increment, and one broadcast.
- Include only the changed partials, background origin, and new revision in broadcasts.
- Generate one origin per background runtime. Compare revisions only when two updates have the same origin.
- When the origin changes, fetch and apply a full background snapshot before accepting further partial updates. A partial event alone cannot recover fields changed while the UI was disconnected.
- Ignore remote updates whose revision is not newer than the UI's latest revision within the same origin.
- Apply remote updates without triggering another persistence write.
- Serialize UI writes so rapid local updates keep their order.
- On persistence failure, report the error and restore the authoritative background snapshot.
- Preserve unrelated fields when different windows update different fields.
- Treat concurrent writes to the same field as last-background-arrival-wins. Revisions prevent stale delivery; they do not provide CRDT-style conflict resolution.
Test the Migration
Add or update focused tests alongside the existing store tests. Cover the behaviors relevant to the domain:
- manual and automatic hydration;
- updates queued before hydration;
- optimistic local updates and serialized partial persistence;
- remote updates without writeback loops;
- stale revision rejection;
- Service Worker restart recovery when the origin changes and revisions restart from zero;
- rollback after a rejected persistence request;
- Zod defaults and transformations;
- atomic rejection of invalid patches;
- stripping unknown fields;
- one revision and broadcast per accepted patch;
- two UI contexts receiving a background change.
Run the targeted tests first, then run yarn check. If the user asks for a commit, also load and follow skills/rabby-yarn-v4-commit-check/SKILL.md before committing.
Guardrails
- Migrate only the requested domain; do not convert unrelated Rematch models opportunistically.
- Do not persist action functions, request caches, or UI-only state.
- Do not make the UI send full snapshots for a single-field edit.
- Do not duplicate schema keys in controller or service routing code.
- Do not replace existing service APIs merely to fit the new store abstraction.
- Do not claim partial patches eliminate same-field races.
1---2name: rabby-create-store3description: Create or migrate Rabby UI state stores with Zustand createRabbyStore, including wallet-backed background persistence, partial synchronization, revision handling, Zod-validated service schemas, hydration, rollback, and tests. Use when adding a persisted Rabby UI store, migrating a Rematch model to Zustand, syncing UI state with a background service, or extending the persisted-store infrastructure beyond Swap.4---56# Create Rabby Store78Create persisted UI stores with the background service as the source of truth. Preserve existing service APIs and migrate only the requested domain.910## Inspect the Existing Domain11121. Read these infrastructure files before changing code:13 - `src/ui/state/createStore/createRabbyStore.ts`14 - `src/ui/state/createStore/createExtensionStoreOptions.ts`15 - `src/ui/state/createStore/createSyncedBackgroundStorage.ts`16 - `src/types/persistedStore.ts`17 - `src/background/controller/wallet.ts`18 - `src/background/utils/persistStore.ts`192. Read `src/ui/state/swap.ts` and `src/background/service/swap.ts` as the reference implementation.203. Locate the current Rematch model, all UI call sites, initialization order, background service fields, and legacy controller methods for the requested domain.214. Check the worktree and preserve unrelated user changes.225. Use `rg` to find consumers before renaming, deleting, or changing a public API.2324## Decide State Ownership2526- Put durable user preferences and business state in the background service schema.27- Keep actions, loading flags, errors, open/closed UI state, request results, and derived UI-only values out of persistence unless the task explicitly requires them.28- Use `partialize` to exclude every non-persisted field and all functions from UI writes.29- Use `merge` to rebuild derived or UI-only state from the hydrated background snapshot when necessary.30- Use plain Zustand or component state instead of `createRabbyStore` when the entire store is transient and needs no background persistence or cross-window synchronization.31- Never access `chrome.storage` directly from the UI store. Keep the path `UI -> wallet -> background controller -> service store`.3233## Implement the Background Source of Truth3435Define the persisted shape once with Zod in the domain service:3637```ts38const featureStoreSchema = z.object({39 enabled: z.boolean().default(false),40 selectedItem: itemSchema.optional(),41});4243export type FeatureServiceStore = z.output<typeof featureStoreSchema>;4445const createFeatureStoreTemplate = (): FeatureServiceStore =>46 featureStoreSchema.parse({});47```4849Then initialize and patch the service through the shared persistence helpers:5051```ts52this.store = await createPersistStore({53 name: 'feature',54 template: createFeatureStoreTemplate(),55 schema: featureStoreSchema,56});5758patchStore(partials: Partial<FeatureServiceStore>) {59 patchPersistStore(this.store, partials);60}61```6263Follow these rules:6465- Give every required persisted field a schema default.66- Keep persisted values JSON-serializable.67- Avoid asynchronous refinements and transforms to `Date`, `Map`, class instances, or other Chrome Storage-incompatible values.68- Treat the Zod object as the authoritative persisted keys, defaults, validation, and output type. Do not maintain a duplicate field whitelist.69- Route generic UI writes through the service `patchStore` method so the merged full state is validated atomically before commit.70- Preserve domain-specific service methods and controller methods still used by existing callers.7172## Register the Generic Background Bridge73741. Add the service store type to `PersistedStoreMap` in `src/types/persistedStore.ts`.752. Add the new key to the routing in `src/background/controller/wallet.ts` for snapshot reads and partial writes.763. Delegate writes to the domain service's `patchStore(partials)` method. Do not assign an incoming full UI state directly to the service store.774. Return the service snapshot with the current background origin and revision so the UI can reject stale updates without confusing a Service Worker restart for an old event.785. Keep the generic API shaped like storage: `getStorageSnapshot(key)` and `setStorageItem(key, partials)`.7980## Implement the UI Store8182Build the domain store with `createRabbyStore` and `createExtensionStoreOptions`:8384```ts85type FeatureState = FeatureServiceStore & {86 transientResult: Result | null;87};8889type FeatureActions = {90 setEnabled: (enabled: boolean) => void;91};9293export type FeatureStore = FeatureState & FeatureActions;9495export const useFeatureStore = createRabbyStore<FeatureStore>(96 (set) => ({97 enabled: false,98 selectedItem: undefined,99 transientResult: null,100 setEnabled(enabled) {101 set({ enabled });102 },103 }),104 createExtensionStoreOptions<FeatureStore, 'feature'>({105 storageKey: 'feature',106 autoHydrate: false,107 partialize(state) {108 const persistedState: Partial<FeatureStore> = {};109 Object.entries(state).forEach(([key, value]) => {110 if (key !== 'transientResult' && typeof value !== 'function') {111 (persistedState as Record<string, unknown>)[key] = value;112 }113 });114 return persistedState;115 },116 onError(error) {117 console.error('[featureStore]', error);118 },119 })120);121```122123- Call `set()` for normal user actions. It performs the optimistic UI update and queues a partial background write after hydration.124- Do not call `wallet.setStorageItem` manually from ordinary setters; the storage adapter owns persistence.125- Do not expose or call a remote-apply method from business code. Hydration, background broadcasts, stale-revision checks, and rollback apply authoritative state internally.126- Return persisted values fetched outside the normal sync channel directly to the caller instead of copying them into the UI store. Use ordinary `set()` for UI-only fields excluded by `partialize`.127- Use `autoHydrate: false` when startup depends on another initialization step, then expose an initializer that awaits `useFeatureStore.persist.hydrate()`.128- Otherwise allow automatic hydration.129- Update UI consumers and remove the requested domain's Rematch bindings only after the Zustand replacement covers their behavior.130131## Preserve Synchronization Invariants132133- Send only fields changed by the local UI action.134- Validate `current background state + partials` before committing any field.135- Make one accepted background patch produce one persistence write, one revision increment, and one broadcast.136- Include only the changed partials, background origin, and new revision in broadcasts.137- Generate one origin per background runtime. Compare revisions only when two updates have the same origin.138- When the origin changes, fetch and apply a full background snapshot before accepting further partial updates. A partial event alone cannot recover fields changed while the UI was disconnected.139- Ignore remote updates whose revision is not newer than the UI's latest revision within the same origin.140- Apply remote updates without triggering another persistence write.141- Serialize UI writes so rapid local updates keep their order.142- On persistence failure, report the error and restore the authoritative background snapshot.143- Preserve unrelated fields when different windows update different fields.144- Treat concurrent writes to the same field as last-background-arrival-wins. Revisions prevent stale delivery; they do not provide CRDT-style conflict resolution.145146## Test the Migration147148Add or update focused tests alongside the existing store tests. Cover the behaviors relevant to the domain:149150- manual and automatic hydration;151- updates queued before hydration;152- optimistic local updates and serialized partial persistence;153- remote updates without writeback loops;154- stale revision rejection;155- Service Worker restart recovery when the origin changes and revisions restart from zero;156- rollback after a rejected persistence request;157- Zod defaults and transformations;158- atomic rejection of invalid patches;159- stripping unknown fields;160- one revision and broadcast per accepted patch;161- two UI contexts receiving a background change.162163Run the targeted tests first, then run `yarn check`. If the user asks for a commit, also load and follow `skills/rabby-yarn-v4-commit-check/SKILL.md` before committing.164165## Guardrails166167- Migrate only the requested domain; do not convert unrelated Rematch models opportunistically.168- Do not persist action functions, request caches, or UI-only state.169- Do not make the UI send full snapshots for a single-field edit.170- Do not duplicate schema keys in controller or service routing code.171- Do not replace existing service APIs merely to fit the new store abstraction.172- Do not claim partial patches eliminate same-field races.