generated from coulomb/repo-seed
doc: Tutorials for new users
This commit is contained in:
238
tutorials/Tutorial 7 Persistence Patterns.md
Normal file
238
tutorials/Tutorial 7 Persistence Patterns.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user