npm.io
0.1.0 • Published 4h ago

@caspian-vega/nano-rune-binding

Licence
MIT
Version
0.1.0
Deps
0
Size
21 kB
Vulns
0
Weekly
0

@caspian-vega/nano-rune-binding

Two way binding between a nanostore and a Svelte 5 rune, with every write back traceable through a named @nanostores/logger action.

Part of caspian-vega-astro-libs.

Read only interop

If you ONLY reading a store inside a component, use the official @nanostores/svelte-runes package:

pnpm add @nanostores/svelte-runes
<script lang="ts">
    import { useStore } from '@nanostores/svelte-runes';

    import { postCount } from './post.store.ts';

    const count = useStore(postCount);
</script>

<p>{count.current} posts</p>

This rune binding is read-only, by default, but ships code you may actually never need.

Install

npm install @caspian-vega/nano-rune-binding
# or
pnpm add @caspian-vega/nano-rune-binding

Peer dependencies: svelte 5, nanostores. @nanostores/logger is optional and only needed for bindLogging.

Usage

<script lang="ts">
    import { toRune } from '@caspian-vega/nano-rune-binding';

    import { filterStore } from './post.store.ts';

    // Edits to `filter` are pushed back into the store, each one logged as
    // the action `RuneProxy_post-filters`.
    const filter = toRune(filterStore, true, 'post-filters').rune;
</script>

<input bind:value={filter.search} />

Traceable write back

A bind: on a rune is otherwise an anonymous mutation: the store changes and nothing says which component did it. Passing a label to bindLogging wraps the write back in a @nanostores/logger action named RuneProxy_<label>, so every store change coming from a binding is attributed to that label in the logger output, and a change without such an entry came from somewhere else.

sequenceDiagram
  participant U as User input
  participant R as Rune proxy
  participant A as Action RuneProxy_label
  participant S as Store
  participant L as Logger
  U->>R: bind: writes a nested value
  R->>R: Snapshot differs from last synced value
  R->>A: Call with the cloned snapshot
  A->>S: set(value)
  A-->>L: action RuneProxy_label
  S-->>R: Emit, marked as synced, no echo back

Give each binding its own label. Two bindings sharing one label are indistinguishable in the log.

Why the package depends on peer .svelte.js

The entry is a rune module. It is published uncompiled, so the runes are compiled by the consumer's vite-plugin-svelte and share that app's reactivity. The package therefore declares a svelte export condition, and any bundler must run .svelte.js files from node_modules through the Svelte compiler. Astro projects using @astrojs/svelte get this by default.

API

toRune(store, bidirectional?, bindLogging?)

Subscribes to store and mirrors its value into a $state proxy of the same shape.

  • store (StoreContract<T>, required): anything with subscribe, and set for write back.
  • bidirectional (boolean, default false): push rune edits back into the store. Requires store.set.
  • bindLogging (false | string, default false): wrap the write back in a @nanostores/logger action named RuneProxy_<value>. Needs @nanostores/logger installed and a logger attached to the store.
  • Returns: RuneProxy<T> with a rune property holding the proxy and a destroy() method.
  • Registers onDestroy, so it must be called during component initialization.
const proxy = toRune(draftStore, true, 'draft-editor');
proxy.rune.title = 'new title';

Write back is guarded, which is what keeps a store update from bouncing back as a fresh object:

flowchart TD
  S[Store emits value] --> D[deepAssign into rune proxy]
  D --> M[Mark value as synced]
  E[Rune mutated in a component] --> F{Snapshot equals synced value?}
  F -->|yes| G[Skip, the store already holds it]
  F -->|no| H[Clone, mark synced, store.set]
  M --> F

Types

StoreContract
  • subscribe(run: (value: T) => void, invalidate?: () => void): () => void (required)
  • set(value: T): void (optional): required for bidirectional

Constraints

  • The store value must be an object or an array. toRune mirrors into a proxy of that shape.
  • The shape should stay stable. Values are merged with a deep assign that preserves proxy identity, arrays sync by index and are truncated to the source length.
  • Write back clones with structuredClone, so values must be structured cloneable.
  • toRune uses onDestroy and belongs in component initialization, not in a store or a plain module.

License

MIT

Keywords