generated from coulomb/repo-seed
239 lines
5.9 KiB
Markdown
239 lines
5.9 KiB
Markdown
# Tutorial 7: Persistence Patterns
|
||
|
||
This tutorial teaches both **localStorage** and **IndexedDB** persistence patterns for your reactive store.
|
||
|
||
### “Saving Greetings” — Making the store survive reloads
|
||
|
||
---
|
||
|
||
## 🎯 Goal
|
||
|
||
Extend the `helloStore` so that all greetings persist between browser sessions.
|
||
When the page reloads, previously added greeters are restored automatically.
|
||
|
||
We’ll implement this in **two steps**:
|
||
|
||
1. Start with simple **`localStorage`** persistence (fast, synchronous).
|
||
2. Upgrade to **`IndexedDB`** for larger data or structured offline storage.
|
||
|
||
Both variants will keep your TestDrive-UI environment self-contained and testable.
|
||
|
||
---
|
||
|
||
## 🧪 Step 1 — Write the failing tests
|
||
|
||
Create a new file:
|
||
`src/store/hello-store.persistence.test.js`
|
||
|
||
```javascript
|
||
import { helloStore } from "./hello-store.js";
|
||
|
||
describe("helloStore persistence", () => {
|
||
beforeEach(() => {
|
||
// Clean simulated storage
|
||
global.localStorage = {
|
||
store: {},
|
||
getItem(k) { return this.store[k] || null; },
|
||
setItem(k, v) { this.store[k] = v; },
|
||
clear() { this.store = {}; }
|
||
};
|
||
});
|
||
|
||
it("saves state to localStorage after modification", () => {
|
||
helloStore.addGreeter("Persisty");
|
||
const saved = JSON.parse(localStorage.getItem("helloStore"));
|
||
expect(saved.greeters).to.include("Persisty");
|
||
});
|
||
|
||
it("restores saved state when re-initialized", () => {
|
||
localStorage.setItem(
|
||
"helloStore",
|
||
JSON.stringify({ greeters: ["Restored"] })
|
||
);
|
||
const { HelloStore } = await import("./hello-store.js");
|
||
const freshStore = new HelloStore();
|
||
const state = freshStore.getState();
|
||
expect(state.greeters).to.deep.equal(["Restored"]);
|
||
});
|
||
});
|
||
```
|
||
|
||
Run:
|
||
|
||
```bash
|
||
npm test
|
||
```
|
||
|
||
All tests will fail initially — perfect.
|
||
|
||
---
|
||
|
||
## ⚙️ Step 2 — Implement `localStorage` persistence
|
||
|
||
Open `src/store/hello-store.js` and extend it like this:
|
||
|
||
```javascript
|
||
class HelloStore {
|
||
constructor() {
|
||
const saved = localStorage.getItem("helloStore");
|
||
this.state = saved ? JSON.parse(saved) : { greeters: ["World"] };
|
||
this.listeners = new Set();
|
||
}
|
||
|
||
getState() {
|
||
return this.state;
|
||
}
|
||
|
||
_notify() {
|
||
// Save current state before notifying
|
||
localStorage.setItem("helloStore", JSON.stringify(this.state));
|
||
for (const cb of this.listeners) cb(this.state);
|
||
}
|
||
|
||
subscribe(callback) {
|
||
this.listeners.add(callback);
|
||
callback(this.state);
|
||
return () => this.listeners.delete(callback);
|
||
}
|
||
|
||
addGreeter(name) {
|
||
this.state = { ...this.state, greeters: [...this.state.greeters, name] };
|
||
this._notify();
|
||
}
|
||
|
||
renameGreeter(oldName, newName) {
|
||
const updated = this.state.greeters.map((n) =>
|
||
n === oldName ? newName : n
|
||
);
|
||
this.state = { ...this.state, greeters: updated };
|
||
this._notify();
|
||
}
|
||
}
|
||
|
||
export const helloStore = new HelloStore();
|
||
export { HelloStore }; // exported for testing
|
||
```
|
||
|
||
✅ Run `npm test` again — tests should now pass.
|
||
|
||
---
|
||
|
||
## 🌐 Step 3 — Try it live
|
||
|
||
Start your dev server:
|
||
|
||
```bash
|
||
npm run dev
|
||
```
|
||
|
||
1. Add new greeters in the dashboard.
|
||
2. Reload the browser.
|
||
→ The same greeters reappear immediately!
|
||
|
||
---
|
||
|
||
## 🧱 Step 4 — Optional: Switch to IndexedDB for larger data
|
||
|
||
`localStorage` is fine for small JSON objects.
|
||
To persist more complex data or avoid blocking the main thread, you can use **IndexedDB** (via the tiny `idb-keyval` helper or native API).
|
||
|
||
Create `src/store/storage.js`:
|
||
|
||
```javascript
|
||
// Minimal IndexedDB wrapper using idb-keyval style API
|
||
const DB_NAME = "testdrive-ui";
|
||
const STORE_NAME = "hello";
|
||
|
||
export async function saveState(state) {
|
||
return new Promise((resolve, reject) => {
|
||
const req = indexedDB.open(DB_NAME, 1);
|
||
req.onupgradeneeded = () => req.result.createObjectStore(STORE_NAME);
|
||
req.onsuccess = () => {
|
||
const tx = req.result.transaction(STORE_NAME, "readwrite");
|
||
tx.objectStore(STORE_NAME).put(state, "store");
|
||
tx.oncomplete = resolve;
|
||
tx.onerror = reject;
|
||
};
|
||
});
|
||
}
|
||
|
||
export async function loadState() {
|
||
return new Promise((resolve, reject) => {
|
||
const req = indexedDB.open(DB_NAME, 1);
|
||
req.onupgradeneeded = () => req.result.createObjectStore(STORE_NAME);
|
||
req.onsuccess = () => {
|
||
const tx = req.result.transaction(STORE_NAME, "readonly");
|
||
const get = tx.objectStore(STORE_NAME).get("store");
|
||
get.onsuccess = () => resolve(get.result);
|
||
get.onerror = reject;
|
||
};
|
||
});
|
||
}
|
||
```
|
||
|
||
Now update the store’s `_notify()` and constructor to use these async calls:
|
||
|
||
```javascript
|
||
import { saveState, loadState } from "./storage.js";
|
||
|
||
class HelloStore {
|
||
constructor() {
|
||
this.listeners = new Set();
|
||
loadState().then(
|
||
(saved) =>
|
||
(this.state = saved || { greeters: ["World"] }) &&
|
||
this._notify()
|
||
);
|
||
}
|
||
|
||
async _notify() {
|
||
await saveState(this.state);
|
||
for (const cb of this.listeners) cb(this.state);
|
||
}
|
||
}
|
||
```
|
||
|
||
💡 Tip: because IndexedDB operations are async, wrap state changes in `async` methods and adjust your tests with `await`.
|
||
|
||
---
|
||
|
||
## 🧠 Step 5 — Testing IndexedDB logic
|
||
|
||
In Node environments, you can simulate IndexedDB with `fake-indexeddb`:
|
||
|
||
```bash
|
||
npm install --save-dev fake-indexeddb
|
||
```
|
||
|
||
Then in your test setup:
|
||
|
||
```javascript
|
||
import "fake-indexeddb/auto";
|
||
```
|
||
|
||
Your tests now work without a browser.
|
||
|
||
---
|
||
|
||
## ✅ Outcome
|
||
|
||
Your TestDrive-UI environment now supports:
|
||
|
||
| Persistence Layer | Use Case | Pros |
|
||
| ----------------- | ------------------------------------- | ------------------ |
|
||
| **localStorage** | Small, synchronous, quick persistence | Simple & immediate |
|
||
| **IndexedDB** | Large, structured, async persistence | Scalable & robust |
|
||
|
||
---
|
||
|
||
## 🔍 Reflection
|
||
|
||
Persistence transforms your store into a **long-term memory layer**.
|
||
With this, TestDrive-UI crosses into **progressive-web-app territory** — offline-ready, recoverable, and reliable.
|
||
|
||
---
|
||
|
||
Continue to Tutorial 8 on Agent Automation.
|
||
|
||
xxx
|