技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
tinybase【数据与研究】TinyBase is a reactive in-memory data store for local-first JavaScript and TypeScript apps, with tables, key-value data, queries, persistence and CRDT sync. Use when a user asks to build an offline-capable or local-first app, keep app state in a TinyBase store, persist it to localStorage, IndexedDB, SQLite or Postgres, sync data between devices or ta…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
TinyBase keeps an app's data in memory as tables (table → row → cell) and key-value pairs, and notifies listeners about exactly what changed. Around that core it offers optional schemas, queries, indexes, relationships and metrics, Persisters that load and save the store (browser storage, files, SQLite, PostgreSQL and more), and Synchronizers that merge a CRDT-based MergeableStore between clients and servers. It has no runtime dependencies; the store module alone is about 7 kB gzipped. It is a client-side library, not a hosted database.
This skill targets TinyBase v10 (September 2026).
npm install tinybase # add `ws` for a Node sync server, `react` (19) for ui-react
# or generate a complete app (asks for framework, persistence and sync type)
npm create tinybase@latest
npm create tinybase@latest -- --list-options # machine-readable list of options
Core modules come from the tinybase root (createStore, createMergeableStore, createQueries, createRelationships, createIndexes, createMetrics, createCheckpoints). Every persister, synchronizer and UI binding has its own subpath, for example tinybase/persisters/persister-indexed-db.
import { createStore } from "tinybase";
const store = createStore().setTablesSchema({
todos: {
text: { type: "string" },
done: { type: "boolean", default: false },
priority: { type: "number", default: 0 },
categoryId: { type: "string" },
},
categories: {
name: { type: "string" },
color: { type: "string" },
},
});
store.setRow("categories", "c1", { name: "Work", color: "#3b82f6" });
store.setRow("todos", "t1", { text: "Build app", priority: 1, categoryId: "c1" });
store.setRow("todos", "t2", { text: "Ship it", priority: 2, categoryId: "c1" });
store.getRow("todos", "t2"); // { text: "Ship it", priority: 2, categoryId: "c1", done: false }
store.setValue("theme", "dark"); // key-value data lives next to the tables
// Listeners fire per table, row or cell; null is a wildcard
const listenerId = store.addRowListener("todos", null, (store, tableId, rowId) => {
console.log(`Todo ${rowId} changed:`, store.getRow(tableId, rowId));
});
store.setCell("todos", "t1", "done", true); // logs: Todo t1 changed: { ... done: true }
store.delListener(listenerId);
import { createQueries, createRelationships } from "tinybase";
const queries = createQueries(store);
queries.setQueryDefinition("activeTodos", "todos", ({ select, join, where }) => {
select("text");
select("priority");
select("categories", "name").as("category"); // column from the joined table
join("categories", "categoryId");
where("done", false);
});
queries.getResultTable("activeTodos");
// { t2: { text: "Ship it", priority: 2, category: "Work" } }
// Sorting and paging happen when reading, not in the definition
queries.getResultSortedRowIds("activeTodos", "priority", true, 0, 20); // descending, first 20
const relationships = createRelationships(store);
relationships.setRelationshipDefinition("todoCategory", "todos", "categories", "categoryId");
relationships.getRemoteRowId("todoCategory", "t1"); // "c1"
relationships.getLocalRowIds("todoCategory", "c1"); // ["t1", "t2"]
The query builder provides select, join, where, group and having. Results are reactive: queries.addResultTableListener("activeTodos", ...) fires when the underlying rows change.
import { createLocalPersister } from "tinybase/persisters/persister-browser"; // localStorage
import { createIndexedDbPersister } from "tinybase/persisters/persister-indexed-db";
const persister = createIndexedDbPersister(store, "todo-app");
await persister.startAutoPersisting(); // loads first, then saves on every change
// Seed the store only when storage is empty: [tables, values]
// await persister.startAutoPersisting([{ categories: { c1: { name: "Work" } } }, {}]);
await persister.destroy(); // on teardown
Other persisters follow the same pattern: persister-file (Node), persister-sqlite-node (node:sqlite), persister-better-sqlite3, persister-sqlite-wasm, persister-pg, persister-postgres, persister-pglite, persister-libsql, persister-supabase, persister-expo-sqlite. Database persisters store either one JSON blob or map tables to real database tables.
import { createStore, createQueries } from "tinybase";
import { createIndexedDbPersister } from "tinybase/persisters/persister-indexed-db";
import {
Provider, useCell, useCreatePersister, useCreateQueries, useCreateStore,
useResultSortedRowIds, useSetCellCallback,
} from "tinybase/ui-react";
export function App() {
const store = useCreateStore(() => createStore());
const queries = useCreateQueries(store, (store) =>
createQueries(store).setQueryDefinition("activeTodos", "todos", ({ select, where }) => {
select("text");
select("priority");
where("done", false);
}),
);
useCreatePersister(
store,
(store) => createIndexedDbPersister(store, "todo-app"),
[],
(persister) => persister.startAutoPersisting(),
);
return (
<Provider store={store} queries={queries}>
<TodoList />
</Provider>
);
}
function TodoList() {
const ids = useResultSortedRowIds("activeTodos", "priority", true);
return <ul>{ids.map((id) => <TodoItem key={id} id={id} />)}</ul>;
}
function TodoItem({ id }: { id: string }) {
const text = useCell("todos", id, "text") as string; // re-renders only when this cell changes
const done = useCell("todos", id, "done");
const toggle = useSetCellCallback("todos", id, "done", () => (cell) => !cell);
return (
<li onClick={toggle} style={{ textDecoration: done ? "line-through" : "none" }}>{text}</li>
);
}
Solid and Svelte bindings live in tinybase/ui-solid and tinybase/ui-svelte.
Sync needs a MergeableStore on every side. createWsSynchronizer returns a promise and does not start by itself:
import { createMergeableStore } from "tinybase";
import { createWsSynchronizer } from "tinybase/synchronizers/synchronizer-ws-client";
const store = createMergeableStore();
const synchronizer = await createWsSynchronizer(store, new WebSocket("wss://sync.northwind.io/shopping-list"));
await synchronizer.startSync();
// later: await synchronizer.destroy();
The URL path (/shopping-list) is the room: clients on the same path share data. For tabs of one browser use createBroadcastChannelSynchronizer(store, "todo-app") from tinybase/synchronizers/synchronizer-broadcast-channel — no server needed.
User prompt: "Build a todo list in React that works offline and keeps its data when I refresh the page."
Use the React component above with createIndexedDbPersister(store, "todo-app"), and add todos with a row callback:
import { useAddRowCallback } from "tinybase/ui-react";
function NewTodo() {
const addTodo = useAddRowCallback("todos", (text: string) => ({ text, done: false, priority: 1 }));
return <input placeholder="What needs doing?" onKeyDown={(e) => {
if (e.key === "Enter") { addTodo(e.currentTarget.value); e.currentTarget.value = ""; }
}} />;
}
Typing "Renew passport" and pressing Enter adds a row with a generated id; the list re-renders, and after a reload the item is still there because startAutoPersisting() loaded it from the todo-app IndexedDB database before saving resumed.
User prompt: "I need two clients to edit the same list and see each other's changes, with the server keeping a copy."
// server.mjs — npm install tinybase ws
import { WebSocketServer } from "ws";
import { createMergeableStore } from "tinybase";
import { createFilePersister } from "tinybase/persisters/persister-file";
import { createWsServer } from "tinybase/synchronizers/synchronizer-ws-server";
const server = createWsServer(
new WebSocketServer({ port: 8048 }),
(pathId) => createFilePersister(createMergeableStore(), `./data-${pathId.replace(/[^\w-]/g, "_")}.json`),
);
server.addClientIdsListener(null, (server, pathId) =>
console.log(`${pathId}: ${server.getClientIds(pathId).length} client(s)`),
);
// client.mjs
import { WebSocket } from "ws";
import { createMergeableStore } from "tinybase";
import { createWsSynchronizer } from "tinybase/synchronizers/synchronizer-ws-client";
const connect = async () => {
const store = createMergeableStore();
const synchronizer = await createWsSynchronizer(store, new WebSocket("ws://localhost:8048/shopping-list"));
await synchronizer.startSync();
return { store, synchronizer };
};
const phone = await connect();
const laptop = await connect();
phone.store.setRow("items", "milk", { name: "Oat milk", bought: false });
laptop.store.setRow("items", "eggs", { name: "Eggs", bought: false });
laptop.store.setCell("items", "milk", "bought", true);
await new Promise((resolve) => setTimeout(resolve, 300));
console.log(JSON.stringify(phone.store.getTable("items")));
await phone.synchronizer.destroy(); // closes the socket; without it the script never exits
await laptop.synchronizer.destroy();
node server.mjs logs shopping-list: 2 client(s) once both stores are connected; node client.mjs prints both rows on the phone store, with the laptop's edit merged: {"milk":{"name":"Oat milk","bought":true},"eggs":{"name":"Eggs","bought":false}}. The server writes data-shopping-list.json, so a client that connects after a restart receives the same list.
startAutoSave() on an empty store overwrites what is in storage. Use startAutoPersisting(), or await persister.load() followed by startAutoSave(), never the reverse.createMergeableStore() — passing a plain store to a synchronizer fails with the error tinybase:0 (codes are listed at tinybase.org/guides/error-codes). A MergeableStore carries CRDT metadata, so use a plain store when nothing is ever merged.await and startSync() — createWsSynchronizer resolves to a synchronizer that is not yet syncing. Call destroy() on unmount; otherwise sockets leak and React strict mode syncs twice.reconnecting-websocket in browsers and re-run synchronizer.load() then save() on open.order — sort with getResultSortedRowIds / useResultSortedRowIds. queries.setQueryDefinition builders that call order() throw.useCell and useValue re-render on one cell; useTable re-renders on any change in the table.createLocalPersister is localStorage (a few MB, synchronous); use the IndexedDB, OPFS or SQLite-WASM persisters for larger browser data.persister-sqlite3, persister-cr-sqlite-wasm and persister-electric-sql modules are gone. Use persister-sqlite-node (node:sqlite) or persister-better-sqlite3; persister-libsql now needs @libsql/client 0.18+.