Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,17 @@ state libraries start to strain:
same schema, selectors, and actions run against a persistent store on the
server (SQLite today, pg/mongodb in future). The runtime reads only the rows a
selector touches instead of hydrating the whole dataset into memory.
- **Lazy persistent reads when you need them.** `HybridDB` pairs a persistent
primary store with an in-memory cache: reads check memory first, fall through
to persistent storage on a miss, then cache the covered index range for next
time. Cache fills, write-through mutations, and transactions are serialized
per HybridDB instance so async selectors and actions do not overlap against
the in-memory cache tier.
- **Synchronous on the frontend.** Against the in-memory driver, selectors and
actions execute **synchronously** (no `await`, no microtask hop), so a click
updates the store and the UI in the same tick.
updates the store and the UI in the same tick. `useAsyncSelector` keeps this
fast path when a run completes from memory, then promotes to async only if a
command yields a promise.
- **JavaScript selectors and actions.** Selectors and actions are ordinary JS: loops,
conditionals, function calls. You get fast indexed lookups underneath, not a
query language to learn.
Expand Down Expand Up @@ -64,7 +72,11 @@ npm install @will-be-done/hyperdb
```

The React devtool ships separately. It traces every selector run and mutation
into a browsable call tree, so you can see which index a slow view scanned:
into a browsable call tree, so you can see which index a slow view scanned. For
HybridDB reads, select nodes are labeled `in-mem` or `persist` to show whether
the returned rows came from the memory cache or the primary persistent store.
When you switch traces, the active detail tab stays selected so comparison stays
focused:

```bash
npm install @will-be-done/hyperdb-devtool
Expand Down
10 changes: 10 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ Devtool:

1. resetcss
1. fix tabl selection (like call tree) not persisted to local storage
1. for hybrid db - mark whic data was loaded from cahce and which from persistent storage

Doc:

1. Make font be local
1. Maybe reframe it and remove mention about sync nature? amybe even async by default?
1. review index.mdx, start/\*(execpt why), index.md, in-memory-persistence.md
1. Add performance compare doc
Expand Down Expand Up @@ -40,3 +42,11 @@ Others:
1. Understand when normalisation happen. Does it happened in-mem? Indexeddb? When validation happen?
1. intent skills css tanstack support
1. On release - cp readme.md to hyperdb/readme.md

DB:

1. start readonly transaction for one selector, if not cached data appeared for hybrid db, and reuse it for other selectors too. Also, don't wait commit to finish for readonly txes. It also means that now beginTx() will accpes modes - readonly | readwrite
2. (? maybe) CoW if new cloned btree appeared. But it still will be locked by idb transaction
3. Allow disable/enable logging of sqlite/idb. Disabled by default
4. Renamer upsert -> put ?
5. Is there any bulk insert for indexeddb?
10 changes: 5 additions & 5 deletions packages/hyperdb-demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<!-- <link rel="preconnect" href="https://fonts.googleapis.com" /> -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<!-- <link -->
<!-- href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" -->
<!-- rel="stylesheet" -->
<!-- /> -->
<title>HyperDB · Demo</title>
</head>
<body>
Expand Down
45 changes: 27 additions & 18 deletions packages/hyperdb-demo/src/BenchmarkApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,29 +89,37 @@ export function BenchmarkApp() {
: 0;
const visibleProjectCount = Math.min(projectLimit, dashboard.totalProjects);
const directDriver =
storeMode === "idb" || storeMode === "idb-inmem"
storeMode === "idb" ||
storeMode === "idb-inmem" ||
storeMode === "idb-hybrid"
? "IndexedDB"
: "WA-SQLite OPFS";
const hybrid = storeMode === "idb-inmem" || storeMode === "wa-sqlite-inmem";
const storageStatus = hybrid
const runtimeHybrid = storeMode === "idb-hybrid";
const storageStatus = runtimeHybrid
? {
dot:
persistence?.draining || persistence?.pendingBatches
? "animate-blip bg-amber"
: "bg-green",
text:
persistence?.draining || persistence?.pendingBatches
? `Saving... ${formatNumber(persistence.pendingOps)} ops queued`
: persistence?.lastDurationMs != null
? `Saved ${formatNumber(
persistence.lastOpCount ?? 0,
)} ops in ${formatDuration(persistence.lastDurationMs)} ms`
: `${directDriver} mirrored behind in-memory reads/writes.`,
}
: {
dot: "bg-green",
text: `Direct async ${directDriver} driver; changes survive reloads.`,
};
text: "HybridDB uses IndexedDB as primary storage with an in-memory cache.",
}
: hybrid
? {
dot:
persistence?.draining || persistence?.pendingBatches
? "animate-blip bg-amber"
: "bg-green",
text:
persistence?.draining || persistence?.pendingBatches
? `Saving... ${formatNumber(persistence.pendingOps)} ops queued`
: persistence?.lastDurationMs != null
? `Saved ${formatNumber(
persistence.lastOpCount ?? 0,
)} ops in ${formatDuration(persistence.lastDurationMs)} ms`
: `${directDriver} mirrored behind in-memory reads/writes.`,
}
: {
dot: "bg-green",
text: `Direct async ${directDriver} driver; changes survive reloads.`,
};

const runMeasured = (
label: string,
Expand Down Expand Up @@ -187,6 +195,7 @@ export function BenchmarkApp() {
>
<option value="idb">IndexedDB</option>
<option value="idb-inmem">IndexedDB + in-memory</option>
<option value="idb-hybrid">IndexedDB + HybridDB cache</option>
<option value="wa-sqlite">WA-SQLite OPFS</option>
<option value="wa-sqlite-inmem">WA-SQLite OPFS + in-memory</option>
</select>
Expand Down
8 changes: 7 additions & 1 deletion packages/hyperdb-demo/src/store-mode.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
export type StoreMode = "idb" | "idb-inmem" | "wa-sqlite" | "wa-sqlite-inmem";
export type StoreMode =
| "idb"
| "idb-inmem"
| "idb-hybrid"
| "wa-sqlite"
| "wa-sqlite-inmem";

const STORAGE_KEY = "hyperdb-demo-mode";
const modes = new Set<StoreMode>([
"idb",
"idb-inmem",
"idb-hybrid",
"wa-sqlite",
"wa-sqlite-inmem",
]);
Expand Down
37 changes: 34 additions & 3 deletions packages/hyperdb-demo/src/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "@will-be-done/hyperdb/drivers/sqlite";
import {
DB,
HybridDB,
SubscribableDB,
asyncDispatch,
createAction,
Expand Down Expand Up @@ -166,23 +167,33 @@ class WaSQLiteWorkerDB implements AsyncSQLiteDB {
}

function getBackend(mode: StoreMode): PersistentBackend {
return mode === "idb" || mode === "idb-inmem" ? "idb" : "wa-sqlite";
return mode === "idb" || mode === "idb-inmem" || mode === "idb-hybrid"
? "idb"
: "wa-sqlite";
}

function isHybridMode(mode: StoreMode): boolean {
return mode === "idb-inmem" || mode === "wa-sqlite-inmem";
}

function isRuntimeHybridMode(mode: StoreMode): boolean {
return mode === "idb-hybrid";
}

async function createPersistentDriver(mode: StoreMode) {
const backend = getBackend(mode);
const hybrid = isHybridMode(mode);
const hybrid = isHybridMode(mode) || isRuntimeHybridMode(mode);

if (backend === "idb") {
return {
driver: await openIndexedDBDriver(
hybrid ? IDB_HYBRID_NAME : IDB_DIRECT_NAME,
),
dbName: hybrid ? "demo-idb-inmem:persistent" : "demo-idb",
dbName: isRuntimeHybridMode(mode)
? "demo-idb-hybrid:primary"
: hybrid
? "demo-idb-inmem:persistent"
: "demo-idb",
};
}

Expand Down Expand Up @@ -363,6 +374,26 @@ export type InitResult = {
};

export async function initStore(mode: StoreMode): Promise<InitResult> {
if (isRuntimeHybridMode(mode)) {
const { driver, dbName } = await createPersistentDriver(mode);
const primaryDB = new DB(driver, {
freezeArgs: false,
freezeRows: false,
dbName,
});
const cacheDB = new DB(new BptreeInmemDriver(), {
freezeArgs: false,
freezeRows: false,
dbName: "demo-idb-hybrid:cache",
});
const db = new SubscribableDB(new HybridDB(primaryDB, cacheDB));

await execAsync(db.loadTables(ALL_TABLES));
installTaskStatsHooks(db);

return { db, persistence: null };
}

if (isHybridMode(mode)) {
const memDB = createMemDb(mode);
const { driver, dbName } = await createPersistentDriver(mode);
Expand Down
3 changes: 2 additions & 1 deletion packages/hyperdb-devtool/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# @will-be-done/hyperdb-devtool

React devtools for HyperDB.
React devtools for HyperDB. The trace details tab stays selected as you switch
between traces, making it easier to compare call trees, queries, and mutations.

```tsx
import { HyperDBDevtools } from "@will-be-done/hyperdb-devtool/react";
Expand Down
Loading
Loading