@sync0/core
v0.1.2
Published
Framework-agnostic reactive field primitive with local, URL, and server sync
Maintainers
Readme
@sync0/core
Framework-agnostic reactive field primitive. Zero dependencies.
@sync0/core is the engine behind all Sync0 adapters. It gives you a single field() call that can be backed by in-memory state, a URL search param, Web Storage, a cookie, or a live server connection — with no framework code anywhere inside.
Installation
npm install @sync0/coreQuick start
import { field, urlField, storageField, cookieField, serverField } from '@sync0/core';
// In-memory
const count = field(0);
// URL search param (?q=…)
const query = urlField('', 'q');
// localStorage (persists + syncs across tabs)
const theme = storageField('light', 'theme');
// sessionStorage (tab-local)
const draft = sessionField('', 'draft');
// Cookie (sent with requests, SSR-readable)
const locale = cookieField('en', 'locale', { maxAge: 31_536_000 });
// Server — optimistic writes + realtime push
const notes = serverField('', 'notes');All fields share the same interface:
field.get() // read current value
field.set(value) // write
field.subscribe(fn) // returns an unsubscribe function
field.__raw // access underlying engine (e.g. onConflict)Sync strategies
| Strategy | Key option | Persists | Cross-tab | SSR-safe | Sent w/ requests |
|---|---|:---:|:---:|:---:|:---:|
| local | — | ✗ | ✗ | ✓ | ✗ |
| url | key | ✓ (URL) | ✓ | ✓ | ✓ (URL) |
| storage (local) | key | ✓ | ✓ | ✗ | ✗ |
| storage (session) | key | ✗ | ✗ | ✗ | ✗ |
| cookie | key | ✓ | ✗ | ✓ | ✓ |
| server | table | ✓ | ✓ realtime | ✓ | ✓ |
Full field() API
function field<T>(initial: T, strategy?: SyncStrategy<T>): Field<T>
type SyncStrategy<T> =
| { sync: 'local' }
| { sync: 'url'; key: string; codec?: Codec<T> }
| { sync: 'storage'; key: string; driver?: 'local' | 'session'; codec?: Codec<T> }
| { sync: 'cookie'; key: string; maxAge?: number; path?: string;
sameSite?: 'Strict' | 'Lax' | 'None'; secure?: boolean; domain?: string; codec?: Codec<T> }
| { sync: 'server'; table: string; conflict?: ConflictStrategy<T> }Convenience wrappers (same as above with strategy pre-applied)
localField<T>(initial)
urlField<T>(initial, key, options?)
storageField<T>(initial, key, options?)
sessionField<T>(initial, key, options?)
cookieField<T>(initial, key, options?)
serverField<T>(initial, table, options?)Server sync
Register an adapter once at startup, then create server-synced fields anywhere:
import { configureServerAdapter, serverField, onConflict } from '@sync0/core';
configureServerAdapter(myAdapter); // once, before any server fields
const cart = serverField<CartItem[]>([], 'cart_items');
// "Updated elsewhere" notifications
const unsub = onConflict(cart, ({ local, remote }) => {
console.log('conflict', local, remote);
});Writing an adapter
import type { ServerAdapter } from '@sync0/core';
class MyAdapter<T> implements ServerAdapter<T> {
async upsert(table: string, value: T): Promise<T> { /* persist */ }
subscribe(table: string, onChange: (v: T) => void): () => void { /* realtime */ }
}SSR
import { registerField, serialize, hydrate } from '@sync0/core';
// Server: after render, capture values
registerField('query', query);
const snapshot = serialize(); // { query: 'current value' }
// Client: before bootstrap, restore
hydrate(snapshot);Custom codec
Non-string values use JSON + base64url by default. Override per-field:
const page = urlField(1, 'page', {
codec: { encode: String, decode: Number },
});Links
- Full documentation & examples
@sync0/angular— Angular Signal adapter@sync0/react— React hooks adapter@sync0/vue— Vue 3 Ref adapter
