| name | rabby-create-store |
| description | 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. |
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 !== && value !== ) {
(persistedState <, >)[key] = value;
}
});
persistedState;
},
() {
.(, 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.