An in-memory database with sync capabilities. Zero runtime dependencies. Bun-first.
| Package | Description |
|---|---|
@graphdb/core |
Core database: collections, CRUD, queries, indexes, listeners, syncers |
@graphdb/types |
Shared TypeScript type definitions |
bun add @graphdb/coreimport { GraphDB } from '@graphdb/core';
const db = GraphDB();
db.createCollection<{ name: string; age: number }>('users', {
indexes: ['name'],
});
const users = db.getCollection<{ name: string; age: number }>('users')!;
const id = await users.create({ name: 'Alex', age: 29 });
const doc = users.read(id);updateAtrenamed toupdatedAt- Timestamps are epoch ms (
number), notDateobjects query()always returnsDoc<T>[](nevernullor single doc)- New
findOne()returnsDoc<T> | null - 0 runtime deps: removed
uuid(usescrypto.randomUUID()) anddate-fns - Listener payloads:
on('create', ({ doc }) => ...),on('update', ({ before, after, patch }) => ...) - Fix: skip edge cases (
skip:0valid,skip >= lengthreturns[]) - Fix: query order is filter -> sort -> skip -> limit
- Fix: multi-field sort evaluates keys in order, first non-zero decides
- Fix: top-level RegExp in where clause works:
{ name: /re/i } - Fix: async/sync no more
new Promise(async ...)anti-pattern; sync errors surface and revert - Populate validates every doc has
_id; duplicates overwrite (last wins) - Map/Set for listeners: O(1) unsubscribe, no array allocations
const db = GraphDB();
db.createCollection<T>(name, options?); // options: { indexes?, syncers? }
db.getCollection<T>(name); // Collection<T> | null
db.listCollections(); // string[]
db.removeCollection(name); // booleancol.read(id); // Doc<T> | null
col.query(where, options?); // Doc<T>[] (always array)
col.findOne(where); // Doc<T> | null
col.create(doc); // Promise<string> (the _id)
col.update(id, patch); // Promise<Doc<T>>
col.remove(id); // Promise<RemoveResult>
col.populate(docs); // void (bulk load)
col.count(where?); // number
col.exists(id); // boolean
col.clear(); // void
col.updateMany(where, patch); // Promise<Doc<T>[]>
col.removeMany(where); // Promise<RemoveResult[]>// Collection events with typed payloads
col.on('create', ({ doc }) => ...);
col.on('update', ({ before, after, patch }) => ...);
col.on('remove', ({ doc }) => ...);
col.on('populate', ({ count }) => ...);
col.on('syncError', ({ op, error, docId? }) => ...);
// Per-document listener
const cancel = col.listen(id, (payload) => ...);
cancel(); // unsubscribe// Primitive equality
col.query({ name: 'Alex' });
// RegExp
col.query({ name: /^al/i });
// Operators
col.query({ age: { gt: 20, lte: 40 } });
col.query({ name: { includes: 'lex' } });
col.query({ name: { startsWith: 'Al' } });
col.query({ name: { match: /regex/ } });
col.query({ status: { in: ['active', 'pending'] } });
// Combined
col.query({ age: { gt: 18 }, lastName: 'Doe' });db.createCollection<User>('users', {
indexes: ['name', 'age'], // hash equality indexes
});Indexes accelerate equality ({ name: 'X' }), { eq }, and { in: [] } lookups. Other operators (gt, regex, includes, etc.) fall through to full evaluation on candidate docs.
db.createCollection<User>('users', {
syncers: {
create: async (doc) => { /* POST to backend; return true/false */ },
update: async (doc) => { /* PUT to backend; return true/false */ },
remove: async (id) => { /* DELETE from backend; return true/false */ },
},
});Optimistic write + revert on sync failure. Sync errors are thrown and emitted via syncError event.
bun install
bun run test # run all tests
bun run build # build all packages
bun run typecheck # typecheck all packagesbun changeset # create a changeset
bun run version-packages # bump versions + changelogs
bun run release # publish to npmMIT