micro-sync
v0.0.6
Published
Local-first optimistic sync ORM for TypeScript.
Readme
micro-sync
Local-first optimistic sync ORM for TypeScript.
Overview
- Define models with Zod.
- Read locally and sync explicitly.
- Load relations per query.
- Retry pending writes with an optional sync engine.
Install
npm i zod micro-syncQuick Start
import z from "zod";
import { belongsTo, createIndexedDbStorage, createSyncOrm, defineModel, hasMany } from "micro-sync";
const users = defineModel("users", {
primaryKey: "id",
schema: z.object({
id: z.string(),
name: z.string(),
}),
sync: {
query: async (query) => {
// Fetch rows from your backend using the ORM query shape.
return [];
},
mutation: async ({ action, query }) => {
// Push insert / update / delete operations to your backend.
console.log(action, query);
},
},
});
const posts = defineModel("posts", {
primaryKey: "id",
schema: z.object({
id: z.string(),
userId: z.string(),
title: z.string(),
}),
relations: {
user: belongsTo(users, "userId", "id"),
},
});
users.relations = {
posts: hasMany(posts, "id", "userId"),
};
const db = createSyncOrm({
storage: createIndexedDbStorage(),
models: {
users,
posts,
},
});
const usersRepo = db.model("users");
await usersRepo.insert({ values: { id: "1", name: "Ada" } });
await usersRepo.sync({ where: { id: "1" } });
const localUsers = await usersRepo.select({ orderBy: [{ field: "name", direction: "asc" }] });Sync Behavior
insert, update, and delete try to sync immediately when online.
If sync fails, the mutation stays in _pendingSync and can be retried later.
Sync Engine
const db = createSyncOrm({
storage,
models: { users },
engine: { intervalMs: 5000 },
});
await db.model("users").insert({ values: { id: "1", name: "Ada" } });
db.stopEngine();
db.startEngine();
await db.retryPendingSync();The default network detector uses browser online / offline events.
You can provide a custom networkDetector in engine when needed.
If you omit engine, no background retry loop starts automatically.
Sync Status
Every repo exposes a snapshot and subscription API for UI state:
const status = usersRepo.getSyncStatus();
const unsubscribe = usersRepo.subscribeSyncStatus((nextStatus) => {
console.log(nextStatus.syncing, nextStatus.pendingCount, nextStatus.lastError);
});The snapshot is framework-agnostic and includes:
onlineengineRunningsyncingpendingCountlastErrorlastSyncedAt
Use it to drive loading, error, offline, and dirty UI states.
Relations
Define relations on the model and load them per query:
const usersWithPosts = await usersRepo.select({
relations: {
posts: {
orderBy: [{ field: "title", direction: "asc" }],
},
},
});For mutation results, use returning:
const insertedPosts = await postsRepo.insert({
values: {
id: "p3",
userId: "1",
title: "Later",
},
returning: {
fields: ["id", "title"],
relations: {
user: true,
},
},
});returning.fields projects model fields in returned local rows.
returning.relations loads relation graph on returned local rows.
returning only shapes returned local rows. It does not perform nested relation mutations.
Query Operators
- Equality:
where: { id: "1" } - Not equal:
where: { status: { ne: "archived" } } - Greater / less than:
where: { age: { gt: 18, lte: 30 } } - List membership:
where: { id: { in: ["1", "2"] } } - Like:
where: { name: { like: "Ada%" } } - Case-insensitive like:
where: { name: { ilike: "ada%" } } - Fuzzy like:
where: { name: { fzlike: "ada" } }
API Surface
defineModel(name, { primaryKey, schema, sync, relations })hasMany(model, localKey, foreignKey)hasOne(model, localKey, foreignKey)belongsTo(model, localKey, foreignKey)createSyncOrm({ storage, models, engine? })createSyncEngine(config)createNetworkDetector()createIndexedDbStorage()db.model(name)repo.getSyncStatus()/repo.subscribeSyncStatus(listener)db.startEngine()/db.stopEngine()/db.retryPendingSync()
Notes
- Each model owns its own
synchooks. - Remote data wins during sync merges.
- Pending local writes stay in
_pendingSyncso they can be retried later.
Vue Adapter
Import from adapter subpath:
import { useRepo, useSyncOrm } from "micro-sync/adapters/vue";Initialize ORM and engine in composable context:
const sync = useSyncOrm({
storage,
models: {
users,
posts,
},
});
// Recommended
const usersRepo = sync.useRepo("users");
await usersRepo.select();
console.log(usersRepo.rows.value);Use model repo manually with Vue refs:
const postsRepo = useRepo(sync, "posts");
await postsRepo.sync();
console.log(postsRepo.rows.value);
console.log(postsRepo.status.value.syncing);Notes:
useSyncOrmstarts engine by default and stops on scope dispose.useRepowraps repo result arrays in shallow ref:values.useRepowraps sync status snapshot in ref:status.- Typo alias exists:
useSycnOrm.
Vue SSR
You can initialize the useSyncOrm composable at the root of application
and provide the instance globally. Then inject anywhere you want.
Nuxt
// plugins/micro-sync.ts
export default defineNuxtPlugin(() => {
const sync = useSyncOrm({...})
return {
provide: { sync },
}
})<!-- Usage -->
<script setup lang="ts">
const { $sync } = useNuxtApp();
const usersRepo = $sync.useRepo("users");
</script>