Skip to content
Open
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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dist/
.astro/
playwright-report/
test-results/
pnpm-lock.yaml

packages/hyperdb/dist/
packages/hyperdb-doc/dist/
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@

<!-- intent-skills:start -->

## Skill Loading

Before substantial work:

- Skill check: run `pnpm dlx @tanstack/intent@latest list`, or use skills already listed in context.
- Skill guidance: if one local skill clearly matches the task, run `pnpm dlx @tanstack/intent@latest load <package>#<skill>` and follow the returned `SKILL.md`.
- Monorepos: when working across packages, run the skill check from the workspace root and prefer the local skill for the package being changed.
- Multiple matches: prefer the most specific local skill for the package or concern you are changing; load additional skills only when the task spans multiple packages or concerns.
<!-- intent-skills:end -->

When you do any changes to packages/hyperdb or packages/hyperdb-devtool,
make sure that you covered it at packages/hyperdb-doc.

make sure that you covered it at packages/hyperdb-doc and root README.md.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
47 changes: 24 additions & 23 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
Github:

1. Setup description, tags, auto-releaser
1. DONE Setup description, tags, auto-releaser

Codesandbox:
stackblitz:

1. Add demo
1. DONE Add demo

Devtool:

Expand All @@ -13,29 +13,30 @@ Devtool:

Doc:

1. MIGRATE TO DOCUSAURUS
1. Fix npm install/doc links
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
1. DONE Remove wa-sqlite from hyperdb-lib. Move wa-sqlite support to recipe
1. Add screenshot of code to the home page
1. Add demo page with devtool
1. Name package is @hyperdb/hyperdb
1. Create @hyperdb/sync
1. Remove SyncDB from doc
1. Mention tanstack/db, ....
1. Generate icon, favicon
1. Check all links
1. Update "start here" links
1. Ask make beaturfil deign using frontend design skill
1. On index page - embed stackblitz
1. Maybe rmeove "$" restriction?
1. Add quick guide to past to llm
1. Create new repo and setup release flow with changeset
1. Beatiful design
1. Release and record codesandbox video to put it to landing page
1. adopt styling of trpc
1. move devtool to separate package
1. how to isolate devtool styling?
1. Add quick guide to paste to llm or skill
1. DONE Fix npm install/doc links
1. DONE Remove wa-sqlite from hyperdb-lib. Move wa-sqlite support to recipe
1. DONE Add screenshot of code to the home page
1. DONE Add demo page with devtool
1. DONE Name package is @hyperdb/hyperdb
1. DONE Remove SyncDB from doc
1. DONE Generate icon, favicon
1. DONE Check all links
1. DONE Update "start here" links
1. DONE Ask make beaturfil deign using frontend design skill
1. DONE Create new repo and setup release flow with changeset or skill
1. DONE Beatiful design
1. DONE Release and make codesandbox screenshot to put it to landing page
1. DONE adopt styling of trpc
1. DONE move devtool to separate package

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
3 changes: 2 additions & 1 deletion packages/hyperdb-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"@will-be-done/hyperdb-devtool": "workspace:*",
"react": "19.2.7",
"react-dom": "19.2.7",
"tailwindcss": "^4.3.1"
"tailwindcss": "^4.3.1",
"wa-sqlite": "^1.0.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
Expand Down
102 changes: 85 additions & 17 deletions packages/hyperdb-demo/src/BenchmarkApp.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import {
useDispatch as useHyperdbDispatch,
useSyncSelector,
useAsyncDispatch as useHyperdbDispatch,
useAsyncSelector,
} from "@will-be-done/hyperdb/react";
import {
clearWorkload,
EMPTY_DASHBOARD_SNAPSHOT,
generateWorkload,
getDashboardSnapshot,
toggleTaskDone,
} from "./db";
import { LIST_PAGE_SIZE, useBenchmarkState } from "./useBenchmarkState";
import type { ClearWorkloadResult, WorkloadResult } from "./workload";
import { getStoredMode, setStoredMode, type StoreMode } from "./store-mode";
import { usePersistence } from "./persistence-context";

const numberFormatter = new Intl.NumberFormat("en-US");
const durationFormatter = new Intl.NumberFormat("en-US", {
Expand Down Expand Up @@ -57,34 +60,79 @@ export function BenchmarkApp() {
isWorking,
setIsWorking,
} = benchmarkState;
const dashboard = useSyncSelector({
selector: getDashboardSnapshot,
args: {
taskLimit,
projectLimit,
selectedProjectId: benchmarkState.selectedProjectId,
},
});
const dashboard =
useAsyncSelector({
selector: getDashboardSnapshot,
args: {
taskLimit,
projectLimit,
selectedProjectId: benchmarkState.selectedProjectId,
},
}) ?? EMPTY_DASHBOARD_SNAPSHOT;

const storeMode = getStoredMode();
const persistence = usePersistence();
const handleStoreModeChange = (
event: React.ChangeEvent<HTMLSelectElement>,
) => {
const nextMode = event.currentTarget.value as StoreMode;
if (nextMode === storeMode) return;
// The store is created once at startup, so swapping tiers means a reload.
// The choice is remembered in localStorage and applied on the next boot.
setStoredMode(nextMode);
window.location.reload();
};

const queuedTasks = projectCount * tasksPerProject;
const visibleTaskCount = dashboard.selectedProject
? Math.min(taskLimit, dashboard.selectedTaskCount)
: 0;
const visibleProjectCount = Math.min(projectLimit, dashboard.totalProjects);
const directDriver =
storeMode === "idb" || storeMode === "idb-inmem"
? "IndexedDB"
: "WA-SQLite OPFS";
const hybrid = storeMode === "idb-inmem" || storeMode === "wa-sqlite-inmem";
const storageStatus = 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,
workload: () => WorkloadResult | ClearWorkloadResult,
workload: () => Promise<WorkloadResult | ClearWorkloadResult>,
) => {
setIsWorking(true);

requestAnimationFrame(() => {
const startedAt = performance.now();
const result = workload();
const durationMs = performance.now() - startedAt;
void (async () => {
const startedAt = performance.now();
try {
const result = await workload();
const durationMs = performance.now() - startedAt;

setLastRun({ label, durationMs, result });
setIsWorking(false);
setLastRun({ label, durationMs, result });
} catch (error) {
console.error(`Failed to run ${label}`, error);
} finally {
setIsWorking(false);
}
})();
});
};

Expand Down Expand Up @@ -130,6 +178,26 @@ export function BenchmarkApp() {
</h1>
</div>

<div className="flex min-w-[200px] flex-col justify-center gap-2 border-t border-line bg-base/60 p-6 text-left sm:border-l sm:border-t-0">
<span className={LABEL}>Driver</span>
<select
value={storeMode}
onChange={handleStoreModeChange}
className="h-10 w-full cursor-pointer rounded-md border border-line bg-base px-3 font-mono text-sm text-ink outline-none transition focus-visible:border-signal/60 focus-visible:ring-2 focus-visible:ring-signal/20"
>
<option value="idb">IndexedDB</option>
<option value="idb-inmem">IndexedDB + in-memory</option>
<option value="wa-sqlite">WA-SQLite OPFS</option>
<option value="wa-sqlite-inmem">WA-SQLite OPFS + in-memory</option>
</select>
<div className="flex items-center gap-2">
<span
className={`size-2 shrink-0 rounded-full ${storageStatus.dot}`}
/>
<p className="text-xs text-faint">{storageStatus.text}</p>
</div>
</div>
Comment on lines +181 to +199

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing accessible label for the driver select.

The <select> element lacks an associated label. Other form controls in this component use a <label> wrapper pattern for accessibility.

Proposed fix
-        <div className="flex min-w-[200px] flex-col justify-center gap-2 border-t border-line bg-base/60 p-6 text-left sm:border-l sm:border-t-0">
-          <span className={LABEL}>Driver</span>
+        <label className="flex min-w-[200px] flex-col justify-center gap-2 border-t border-line bg-base/60 p-6 text-left sm:border-l sm:border-t-0">
+          <span className={LABEL} aria-hidden="true">Driver</span>
           <select
+            aria-label="Driver"
             value={storeMode}

Or simply wrap the span-and-select portion in a <label> element.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/hyperdb-demo/src/BenchmarkApp.tsx` around lines 181 - 199, The
select element with value={storeMode} and onChange={handleStoreModeChange} lacks
an associated accessible label for screen readers and accessibility tools. Wrap
both the span element containing "Driver" text and the select element in a
<label> element to properly associate the label with the form control, following
the same pattern used for other form controls in the component.


<div className="relative flex min-w-[220px] flex-col justify-center gap-2 border-t border-line bg-base/60 p-6 text-left sm:border-l sm:border-t-0 sm:text-right">
<div className="flex items-center gap-2 sm:justify-end">
<span
Expand Down Expand Up @@ -311,7 +379,7 @@ export function BenchmarkApp() {
</div>
<button
type="button"
onClick={() => dispatch(toggleTaskDone({ task }))}
onClick={() => void dispatch(toggleTaskDone({ task }))}
className={`cursor-pointer rounded-full border px-3 py-0.5 font-display text-[10px] font-semibold uppercase tracking-wider transition hover:brightness-125 ${
STATUS_STYLES[task.status] ?? STATUS_STYLES.todo
}`}
Expand Down
34 changes: 30 additions & 4 deletions packages/hyperdb-demo/src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,18 @@ import {
import { createWorkloadRows, type DashboardSnapshot } from "./workload";
import type { ExtractSchema } from "@will-be-done/hyperdb";

const action = createAction();
const selector = createSelector();
const action = createAction({
trace: {
enabled: true,
startOn: "load",
},
});
const selector = createSelector({
trace: {
enabled: true,
startOn: "load",
},
});

export const projectsTable = defineTable("projects", {
id: v.string(),
Expand All @@ -21,7 +31,8 @@ export const projectsTable = defineTable("projects", {
createdAt: v.number(),
})
.index("byCreatedAt", ["createdAt"])
.index("byName", ["name"]);
.index("byName", ["name"])
.index("byIds", ["id"]); // btree over id → enables a full-table scan for hydration
export type Project = ExtractSchema<typeof projectsTable>;

export const tasksTable = defineTable("tasks", {
Expand All @@ -36,7 +47,8 @@ export const tasksTable = defineTable("tasks", {
})
.index("byCreatedAt", ["createdAt"])
.index("byProjectPosition", ["projectId", "position"])
.index("byStatus", ["status"]);
.index("byStatus", ["status"])
.index("byIds", ["id"]); // btree over id → enables a full-table scan for hydration
export type Task = ExtractSchema<typeof tasksTable>;
export type TaskStatus = Task["status"];

Expand Down Expand Up @@ -129,6 +141,20 @@ export const EMPTY_TASK_STATS: TaskStats = {
done: 0,
};

export const EMPTY_DASHBOARD_SNAPSHOT: DashboardSnapshot = {
projects: [],
selectedProject: null,
selectedTasks: [],
selectedTaskCount: 0,
projectTaskCountsById: {},
projectNamesById: {},
totalProjects: 0,
totalTasks: 0,
todoTasks: 0,
doingTasks: 0,
doneTasks: 0,
};

export const getDashboardSnapshot = selector({
name: "getDashboardSnapshot",
args: {
Expand Down
31 changes: 7 additions & 24 deletions packages/hyperdb-demo/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,20 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BptreeInmemDriver } from "@will-be-done/hyperdb/drivers/inmemory";
import { DB, SubscribableDB, execSync } from "@will-be-done/hyperdb";
import { DBProvider } from "@will-be-done/hyperdb/react";
import App from "./App.tsx";
import {
projectsTable,
projectTaskStatsTable,
tasksTable,
taskStatsTable,
} from "./db.ts";
import { initStore } from "./stores.ts";
import { getStoredMode } from "./store-mode.ts";
import { PersistenceProvider } from "./persistence-context.tsx";
import "./index.css";
import { installTaskStatsHooks } from "./count-hook.ts";

const baseDb = new DB(new BptreeInmemDriver(), {
freezeArgs: false,
freezeRows: false,
});
execSync(
baseDb.loadTables([
projectsTable,
tasksTable,
taskStatsTable,
projectTaskStatsTable,
]),
);
const db = new SubscribableDB(baseDb);
installTaskStatsHooks(db);
const { db, persistence } = await initStore(getStoredMode());

createRoot(document.getElementById("root")!).render(
<StrictMode>
<DBProvider value={db}>
<App />
<PersistenceProvider value={persistence}>
<App />
</PersistenceProvider>
</DBProvider>
</StrictMode>,
);
21 changes: 21 additions & 0 deletions packages/hyperdb-demo/src/persistence-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createContext, useContext, useSyncExternalStore } from "react";
import type {
PersistenceMonitor,
PersistenceSnapshot,
} from "./persistence-monitor";

const PersistenceContext = createContext<PersistenceMonitor | null>(null);

export const PersistenceProvider = PersistenceContext.Provider;

const noopSubscribe = () => () => {};
const getNull = () => null;

/** Live persistence state, or `null` in memory mode. */
export function usePersistence(): PersistenceSnapshot | null {
const monitor = useContext(PersistenceContext);
return useSyncExternalStore(
monitor?.subscribe ?? noopSubscribe,
monitor?.getSnapshot ?? getNull,
);
}
Loading
Loading