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

@interop/social-core

v0.8.1

Published

Platform-neutral contacts data model, normalization, import-merge, and last-write-wins rules shared by Freewallet mobile and web

Readme

Freewallet Social Core (@interop/social-core)

Node.js CI NPM Version

Platform-neutral contacts data model, normalization, import-merge, and last-write-wins rules shared by Freewallet mobile and web.

Table of Contents

Background

Freewallet has two replicas of the same contacts feed: a React Native mobile wallet and a web app. They sync through a shared encrypted store, so they only converge if they agree, byte for byte, on three things:

  • the payload shapes written into each sync envelope (ContactData, the head payload, the revision payload),
  • the import-merge rules that decide how a re-imported address book folds into existing contacts, and
  • the last-write-wins rule that settles concurrent edits to the same contact.

This package is the single source of truth for all three. It is pure, dependency-free, and isomorphic (browser, Node.js, React Native): no storage, no crypto, no platform APIs. Each replica keeps its own storage layer (SQLite on mobile, its own store on web) and calls into this package for the shared decisions, so the two implementations cannot drift.

Security

This package never touches secrets, key material, or the network. It operates only on already-decrypted, in-memory plaintext contact data. The runtime type guards (isContactData, isContactHeadPayload, isContactRevisionPayload) exist so that one replica can validate an envelope body written by the other implementation the moment it decrypts it, before trusting its shape.

Install

  • Node.js 24+ is recommended.

PNPM

To install via PNPM:

pnpm install @interop/social-core

Development

To install locally (for development):

git clone https://github.com/interop-alliance/social-core.git
cd social-core
pnpm install

Usage

Settle concurrent edits to the same contact with the shared last-write-wins rule:

import { remotePayloadWins } from '@interop/social-core'

const takeRemote = remotePayloadWins(
  { updatedAt: remote.updatedAt, deviceId: remote.deviceId },
  { updatedAt: local.updatedAt, deviceId: local.deviceId }
)

Normalize a source-mapped contact before storing it:

import { normalizeContact } from '@interop/social-core'

const contact = normalizeContact({
  nativeId: entry.id,
  givenName: entry.firstName,
  familyName: entry.lastName,
  phoneNumbers: entry.phones // [{ label, number }]
})
// `contact` is `ContactData`, or `null` when there is nothing worth importing.

Plan a re-runnable address-book import against the contacts already stored:

import { planImportMerge } from '@interop/social-core'

const { inserts, overwrites, skips, stale } = planImportMerge(
  existingRows,
  incoming
)
// Apply inserts/overwrites inside your own storage transaction; an overwrite
// must keep the row's `updatedAt === createdAt` so it stays import-refreshable.
// A contact whose source id churned (Android re-linking its aggregate ids, for
// example) is matched by content -- identical content (ignoring the churn-prone
// id metadata), a shared DID alone, or name plus a shared phone or email -- so
// it rebinds to its row instead of duplicating it. `stale` lists the imported rows
// this batch never matched; the planner never deletes, so it is yours to offer
// as cleanup or ignore.

Upgrade a stored contact to the current shape when you load it:

import { upgradeContactData } from '@interop/social-core'

const contact = upgradeContactData(headPayload.contact)
// Idempotent: a contact already in the current shape is returned unchanged,
// so it is safe to apply on every load. As well as the postal-address
// spellings, this re-normalizes every entry label, which repairs a row stored
// before iOS's `_$!<Home>!$_` label wrapper was stripped.

Modules

| Module | Exports | Purpose | | -------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | types | ContactData, ContactAction, ContactHeadPayload, ContactRevisionPayload | The shared contact shape and the decrypted sync-envelope payload shapes. | | constants | CONTACTS_COLLECTION, CONTACTS_HISTORY_COLLECTION, and their specs | The wire-format collection ids and id-derivation both replicas agree on. | | lww | remotePayloadWins | The last-write-wins tiebreak for a mutable head document. | | normalize | normalizeLabel, normalizeContact, ContactInput | Pure normalization of a source-mapped partial contact into ContactData. | | merge | planImportMerge | The pure insert / overwrite / skip / stale planner for a re-runnable import, with a content fallback for churned source ids. | | validate | isContactData, isContactHeadPayload, isContactRevisionPayload | Runtime guards for a payload one replica decrypts from the other. | | upgrade | upgradeContactData, upgradeContactHeadPayload, upgradeContactRevisionPayload | Read-path upgrade of a stored contact to the current shape. | | dids | getDids, setDids, isDidUrl, unmangleDidUrl | The DIDs a contact carries: one predicate shared by the read and write sides of an edit. | | seedContacts | selfContact, SELF_CONTACT_NAME, isUnlinkedSeedTwin | The self-contact a new wallet seeds itself with, and the rule a pull absorbs another replica's copy of a seed by. | | display | initialsFor, secondaryLineFor, contactMatchesQuery, compareContactsByName, ACTION_LABELS, snapshotLines | The presentation rules both wallets render a contact, a contact list, and a revision with. | | buildContact | buildContact, ContactFormRow | The headless half of an edit form: assembling form state plus the loaded contact into the ContactData to store. |

Contribute

PRs accepted. See CONTRIBUTING.md for editor setup (Prettier, ESLint, and EditorConfig) and how it maps to CI.

If editing the Readme, please conform to the standard-readme specification.

License

MIT License © 2026 Interop Alliance.