Files
testdrive-jsui/tutorials/Tutorial 7 Persistence Patterns.md

239 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
Well 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 stores `_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