npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pinia-plugin-synced

v0.1.4

Published

Synchronize explicitly opted-in Pinia stores and actions across same-origin tabs.

Downloads

6,214

Readme

pinia-plugin-synced

Synchronize selected Pinia stores and actions across same-origin tabs, windows, and iframes. One context is elected as the leader: actions run there, and committed state is replicated to every participating Pinia.

Playground

Install

pnpm add pinia-plugin-synced pinia vue

Usage

Create one runtime for each Pinia. Use the same namespace in every context that should synchronize:

// main.ts
import { createPinia, defineStore } from 'pinia'
import { createSyncedPiniaPlugin } from 'pinia-plugin-synced'
import { ref } from 'vue'

const pinia = createPinia()
const synced = createSyncedPiniaPlugin({
  // You can use leadership to control and specify the role / behavior of the runtime.
  // By default it's `follower-preferred`, which means no stealing, no takeover, if no leader is present, it will be leader
  // Or otherwise, `leader-only` to take leadership once when the runtime joins, or `follower-only` to never become leader.
  //
  // leadership: 'follower-preferred', <- if not specified, defaults to 'follower-preferred'
  namespace: 'my-app:messages',
})

pinia.use(synced.plugin)

// stores/messages.ts
export const useMessagesStore = defineStore('messages', () => {
  const messages = ref<string[]>([])

  async function send(message: string) {
    messages.value.push(message)
  }

  return { messages, send }
}, {
  synced: {
    actions: ['send'],
    state: true,
  },
})

Calling send() in any context returns a Promise and executes the action in the elected leader. Direct mutations and $patch() calls are sent to the leader as full-state proposals.

Leadership modes

leadership option controls which role the runtime takes when joins.

| Mode | Behavior | | --- | --- | | follower-preferred | Default. If a leader is present, it follows. If no leader is present, it becomes the leader. | | follower-only | Never becomes leader. If no leader would ever be present, this runtime remains a follower and does not take leadership. | | leader-only | Becomes leader once when joins. If a leader is already present, it remains a follower and does not take leadership. |

[!WARNING]

leader-only takeover is a best-effort failover, not a transactional handoff: it does not cancel actions or external side effects already executing in the previous leader. If the current committed state cannot be received within callTimeout, the runtime reports the error through onError, remains a follower, and does not retry the forced takeover.

Resource cleanup

Dispose the runtime when its owning page or window ends:

synced.dispose()

Constraints

  • Everything should be async: Synchronized actions are asynchronous.
  • State changes and completed action records queued before the next commit task are committed together. Only the latest state of each dirty store is serialized. Per-store revisions prevent an unchanged store from being patched again when another store or only the action history changed.
  • State snapshots use structuredClone by default, so values such as Map, Set, and Date keep their types across contexts. Custom serialize and deserialize functions can replace the default when an application needs another state format.
  • Serialized state, action arguments, and action results must support structuredClone because the transport uses structured cloning.
  • We do not offer CRDT merging: direct state proposals use last-arriving-wins semantics. This package does not provide CRDT merging.
  • We do not guarantee application-level idempotency: action RPCs stay deduplicated for the full RPC timeout, but external side effects still need idempotency keys.
  • We do not persist data: synced plugin is not [pinia-plugin-persistedstate], all states will be lost once every tabs/windows/iframes closes. If you need persistence, use it with another plugin.
  • Keep Pinia single: synced plugin belongs to exactly one Pinia. Every context that may become leader must instantiate the synchronized stores it serves.

Use a backend, SharedWorker, or another durable owner when state must cross origins, synchronize across devices, or survive after every browser context closes. If needed, consider linking them with the API linkChannel offered by eventa

Runtime API

synced.participantId // unique ID of this runtime
synced.isLeader() // true if this runtime is the elected leader
synced.getLeaderId() // unique ID of the elected leader runtime
synced.getParticipantCount() // number of runtimes in the synchronization domain

Use with pinia-plugin-persistedstate

// main.ts
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

import { createPinia } from 'pinia'
import { createSyncedPiniaPlugin } from 'pinia-plugin-synced'

const pinia = createPinia()

const synced = createSyncedPiniaPlugin({
  namespace: 'my-app:messages',
})

// Order matters
pinia.use(piniaPluginPersistedstate)
pinia.use(synced.plugin)

// stores/messages.ts
export const useMessagesStore = defineStore('messages', () => {
  const messages = ref<string[]>([])

  async function send(message: string) {
    messages.value.push(message)
  }

  return { messages, send }
}, {
  persist: true,
  synced: {
    actions: ['send'],
    state: true,
  },
})

Development

pnpm install
pnpm test
pnpm build
pnpm --dir playground dev

License

MIT