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 🙏

© 2024 – Pkg Stats / Ryan Hefner

collection-sync

v1.1.9

Published

[![npm version](https://badge.fury.io/js/collection-sync.svg)](https://badge.fury.io/js/collection-sync)

Downloads

1

Readme

npm version

Collection Sync

Javascript Library for bi-directional database synchronization between multiple devices or servers. Customizable and completely database agnostic.

See Documentation.

Use cases

Some examples where this mechanism would be useful:

  • Memo app that works offline, and updates remote database when it goes online.
  • Multiplatform app (desktop, mobile, web app) which works offline (in mobile and desktop), and updates remote database when going online. When using the app in a different device, the new data is downloaded, making all devices up to date.

Install

npm install collection-sync

How to use

Make sure your project is using TypeScript.

Import dependencies:

import { SyncItem } from "collection-sync";
import { SynchronizableCollection } from "collection-sync";
import { CollectionSyncMetadata } from "collection-sync";
import { SyncOperation, SyncConflictStrategy } from "collection-sync";
import DocId from "collection-sync/dist/types/DocId";

Create a class that extends SyncItem, and populates its data starting from a document from your database. You must customize the way to extract the ID and date it was last updated.

If we use Mongo's _id, when synchronizing from one database to another it might be impossible to set it, since some database engines auto generate the _id value, hence you must choose how to identify documents using a custom way.

The same applies for updatedAt. If your database engine automatically sets updatedAt when saving a record, then you'll need to have a custom attribute you can set freely.

Synchronization will fail if you try to set the ID and/or updatedAt but your database refuses to leave you set a specific value.

class CustomItem extends SyncItem {
  constructor(doc: any /* e.g. Mongo Document */){
    super(doc.documentId, doc, doc.updatedAt);
  }
}

Then, create a class that extends SynchronizableCollection and implement its abstract methods:

class LocalCollection extends SynchronizableCollection {
  countAll(): number | Promise<number> {
    // Count collection documents.
    return 100;
  }
  async initialize(): Promise<void> {
    // Executes async logic to initialize collection or datastore (open file, create database connection, etc).
  }
  findByIds(ids: DocId[]): SyncItem[] | Promise<SyncItem[]> {
    const docs = [
      { /* Doc from DB */ },
      { /* Doc from DB */ },
      { /* Doc from DB */ }
    ];
    return docs.map(d => new CustomItem(d)); // Convert to CustomItem.
  }
  syncBatch(items: SyncItem[]): SyncItem[] | Promise<SyncItem[]> {
    // Implement batch upsert/delete of records.
  }
  itemsNewerThan(date: Date | undefined, limit: number): SyncItem[] | Promise<SyncItem[]> {
    // Returns a list of items that have updatedAt greater than argument provided.
    // The list MUST be ordered by updatedAt ASC, otherwise an exception will be
    // thrown (no syncing will be executed).
  }
  latestUpdatedItem(): SyncItem | Promise<SyncItem | undefined> | undefined {
    // Gets the highest updateAt date in the collection.
  }
}

Install npm install --save @types/node if you get errors related to missing Node types.

You can also implement several lifecycle hooks for granular control over syncing. See which methods can be overriden from SynchronizableCollection class for details.

All methods allow the use of async/await if needed.

Next, implement a class that communicates with the remote collection (datastore).

In cases where the local collection is a database in a mobile app, you don't want to connect directly to a remote database, but instead you'd have to prepare a backend API to connect to, which provides the operations needed (syncBatch, findByIds, etc). This class must work as a communication layer between the client and that API.

If both collections are inside a private/secure network, then connecting directly to another database would be fine.

class RemoteCollection extends SynchronizableCollection {
  countAll(): number | Promise<number> {
    // Execute some API call to
    // https://your_server.com/api/users/count_all
    // and return its value here.
  }
  async initialize(): Promise<void> {
    // ...
  }
  findByIds(ids: DocId[]): SyncItem[] | Promise<SyncItem[]> {
    // ...
  }
  syncBatch(items: SyncItem[]): SyncItem[] | Promise<SyncItem[]> {
    // ...
  }
  itemsNewerThan(date: Date | undefined, limit: number): SyncItem[] | Promise<SyncItem[]> {
    // ...
  }
  latestUpdatedItem(): SyncItem | Promise<SyncItem | undefined> | undefined {
    // ...
  }
}

Finally, implement a mechanism to store and retrieve two dates (last fetch and post dates).

A persistent storage is recommended.

class MySyncMetadata extends CollectionSyncMetadata{
  setLastFetchAt(d: Date): void {
    // ...
  }
  setLastPostAt(d: Date): void {
    // ...
  }
  getLastFetchAt(): Date | Promise<Date | undefined> | undefined {
    // ...
  }
  getLastPostAt(): Date | Promise<Date | undefined> | undefined {
    // ...
  }
  async initialize(): Promise<void> {
    // ...
  }
}

Note that both classes have a initialize method. Some storage mechanisms require to open a file, create a DB connection, or do some asynchronous logic before beginning to use them. You can put that logic there.

In this example, however, we'll import and use BasicSyncMetadata, which provides an in-memory storage for synchronization metadata. This is the simplest way to get started.

Add a new import to the top of the file:

import { BasicSyncMetadata } from "collection-sync";

Then, create two synchronization metadata managers:

const syncMetadataSlave: CollectionSyncMetadata = new BasicSyncMetadata();
const syncMetadataMaster: CollectionSyncMetadata = new BasicSyncMetadata();

Now, create two collections:

const collectionSlave = new LocalCollection(syncMetadataSlave);
const collectionMaster = new RemoteCollection(syncMetadataMaster);

Since only the slave keeps track of sync metadata, the master doesn't need a CollectionSyncMetadata object. However, since all collections could potentially have a master, it is a required argument.

Attach the parent as master:

collectionSlave.parent = collectionMaster;

Note that both collectionSlave and collectionMaster simply model how your data stores are arranged. The machine where this code is running doesn't actually need to host collectionMaster's data, but since the way to communicate with it was implemented (i.e. RemoteCollection's methods for API communication), we still can modify its data.

If we assume that some data exists in the datastore collectionMaster is pointing at, and the database pointed by collectionSlave is empty, then we can perform a fetch to update collectionSlave:

collectionSlave.sync(SyncOperation.Fetch, 100, { conflictStrategy: SyncConflictStrategy.Force });

See sync method documentation.

See also other specifications related to sync.

When syncing, conflicts might occur, and there are a few strategies to overcome them. A conflict occurs when trying to update a record using a record with an older updatedAt. In general, when synchronizing data collections, older data should be overwritten by newer data, but sometimes this is not the case, and that's when a conflict is generated. See SyncConflictStrategy for details.

Another example

Note: Omitting some steps from the previous example.

const android = new LocalCollection(new MySyncMetadata());
const pc = new LocalCollection(new MySyncMetadata());
const backend = new LocalCollection(new MySyncMetadata());

android.parent = backend;
pc.parent = backend;

// Data that only exists in Android devide is being pushed...
android.sync(SyncOperation.Post, 1000);

// PC device now has data that previously only the Android device had.
pc.sync(SyncOperation.Fetch, 1000);

In practice, you'd want to make your slave collection perform both post and fetch operations during a full sync.

When a conflict is encountered, a suggestion is to ask the user to manually select how to solve them, and then trigger a new synchronization but using a different configuration (e.g. forcing data from the master collection to overwrite slave data).

Current limitations and future work

Locking mechanism

Locking mechanism (to prevent multiple devices from synchronizing at the same time) must be implemented by the user. The addition of acquireLock and releaseLock abstract methods to 'SynchronizableCollection' or Collection have been proposed.

Using it with Vanilla Javascript

Use with vanilla Javascript is not tested. It may not be convenient for development. Typescript is recommended.

Develop

npm run test:watch