@byrding/core
v0.10.0
Published
Vanilla JS core — Proxy reactivity, subscription map, store registry
Readme
@byrding/core
Framework-agnostic reactivity engine. App code does not call this package directly — it's the substrate for @byrding/react and @byrding/vue. Use this reference if you're writing a new adapter or debugging.
AI agents — if you're generating consumer code, see the consumer agent guidance. If you're implementing a new adapter, see the refactor agent guidance.
Install
npm install @byrding/core
# or
npx jsr add @byrding/corecreateStore(id, definition)
function createStore<T>(
id: string,
definition: (new () => T) | (() => T),
): CoreStore<T>Register (or retrieve) a store. First registration for a given id wins; subsequent calls return the same singleton and the definition argument is discarded.
Returns a CoreStore<T>:
interface CoreStore<T> {
/** Live flat merged object — state getters/setters, computed getters, action functions. */
store: T
/** Register a subscriber. Returns an unsubscribe function. */
subscribe(
componentId: string,
keyPaths: string[],
callback: () => void,
): () => void
/** Cached snapshot of raw state — new reference on each mutation. */
getSnapshot(): Partial<T>
}Class vs closure dispatch
Internally, createStore detects the definition style:
/^\s*class\s/match on the function source → class strategy:_proxyis anew Proxy(_raw); actions bound to the proxy.- Anything else → closure factory strategy: each state property on the returned instance is redefined with
Object.defineProperty(inst, key, { get, set })that reads/writes a separate_rawvalues map and calls_notify.
Actions, getters, and state fields are then all bound/exposed on the flat merged store.
generateComponentId()
function generateComponentId(): stringReturns byrding_1, byrding_2, … — a process-unique opaque ID. Used by adapters to distinguish subscribers in the subscribe/notify map.
subscribe(store, componentId, keyPaths, callback)
function subscribe(
store: StoreInstance,
componentId: string,
keyPaths: string[],
callback: () => void,
): () => voidLow-level subscribe. Adapters call this from their own hook/composable. keyPaths may be either a list of dot paths or ['*'] (wildcard).
notify(store, keyPath)
function notify(store: StoreInstance, rawKeyPath: string): voidLow-level notify. Normalises keyPath (strips .<number> array indices and trailing .length), then fires callbacks for:
- exact match on
keyPath - every ancestor path (
a.b.c→a.b→a) - the
*wildcard bucket
Each callback runs at most once per call, even if it matches multiple paths.
storeRegistry
const storeRegistry: Map<string, StoreInstance>The module-level singleton map keyed by store id. This is what enables cross-framework sharing — both adapters touch the same map. Do not delete entries manually in production code; it breaks reactivity on any live subscriber.
Types
interface StoreInstance {
id: string
_raw: Record<string, unknown>
_proxy: Record<string, unknown>
_stateKeys: string[]
_actionKeys: string[]
_computedKeys: string[]
_getterFns: Record<string, () => unknown>
_actionFns: Record<string, (...args: unknown[]) => unknown>
_updateMap: Map<string, Set<string>>
_callbackMap: Map<string, () => void>
_notify: (keyPath: string) => void
}Helpers
function classify(instance: object): {
stateKeys: string[]
actionKeys: string[]
computedKeys: string[]
}
function createReactiveState(
target: Record<string, unknown>,
notify: (keyPath: string) => void,
prefix?: string,
): Record<string, unknown>
function normaliseKeyPath(keyPath: string): stringclassifywalks the prototype chain to separate state, getters, and methods usinggetOwnPropertyDescriptors(never a live read — that would trigger getters).createReactiveStatebuilds theProxyused for class-style stores.normaliseKeyPathcollapsesitems.0→itemsanditems.length→items.
