/synchronization-atom

A powerful zero-dependency primitive (written in TypeScript) to help build other synchronization primitives like locks, semaphores, events or barriers

Primary LanguageTypeScriptMIT LicenseMIT

npm NPM GitHub issues npm bundle size npm GitHub forks GitHub Repo stars

synchronization-atom

A powerful (& typed) zero-dependency primitive to help build other synchronization primitives including but not limited to: locks, semaphores, events, barriers, channels

This project was heavily inspired by mobx's when

Installation

# pnpm
pnpm add --save synchronization-atom

API

atom

function atom<T>(initialState: T): Atom<T>

Creates and returns an Atom<T> with the given initialState: T.

async atom.conditionallyUpdate

interface Atom<T> {
    ...
    conditionallyUpdate: (
        predicate: (state: T) => boolean,
        nextState: T | ((state: T) => T),
        sideEffect?: (state: T) => void,
        abortSignal?: AbortSignal
    ) => Promise<T>
    ...
}
  • Updates the atom's state to nextState if the current state satisfies the predicate.
  • If the current state does not satisfy the predicate, the call blocks until the predicate is satisfied.
  • If a sideEffect is provided, it is executed atomically as part of the update (i.e. no other update or side-effect will be running simultaneously against the atom).
  • Can be cancelled via an optional AbortSignal as last argument.

async atom.waitFor

interface Atom<T> {
    ...
    waitFor: (
        predicate: (state: T) => boolean,
        reaction?: (state: T) => void,
        abortSignal?: AbortSignal
    ) => Promise<T> | void
    ...
}
  • Blocks until the atom's state satisfies the predicate, unless a reaction is provided.
  • If a reaction is provided, the call returns immediately, and when the predicate is satisfied, the reaction is executed.
  • Can be cancelled via an optional AbortSignal as last argument.

atom.getState

interface Atom<T> {
    ...
    getState: () => T
    ...
}

Returns the current state of the atom.

Usage Examples

Make a lock

import {atom} from 'synchronization-atom';

const lockAtom = atom(false /* is locked */);

async function test(n: number) {
    // aquire lock
    await lockAtom.conditionallyUpdate(
        (locked) => locked === false,
        true
    );

    console.log(n, `aquired lock`);
    await doCrazyAsyncStuffHere();
    console.log(n, `releasing lock`);

    // release lock
    lockAtom.conditionallyUpdate(() => true, false);
}

Promise.all([test(1), test(2), test(3)]);

Make a semaphore

import { atom } from 'synchronization-atom';

const semaphoreAtom = atom(3 /* no. of seats */);

async function test(n: number) {
    // aquire lock
    await semaphoreAtom.conditionallyUpdate(
        (seats) => seats > 0,
        (seats) => seats - 1
    );
    
    console.log(n, `aquired lock`);
    await doCrazyAsyncStuffHere();
    console.log(n, `releasing lock`);
    
    // release lock
    semaphoreAtom.conditionallyUpdate(
        () => true,
        seats => seats + 1
    );
}

Promise.all([test(1), test(2), test(3), test(4), test(5)]);

Make a event

import { atom } from 'synchronization-atom';

const eventAtom = atom(false /* is event set */);

async function test(n: number) {
    console.log(n, `waiting for event`);
    await eventAtom.waitFor((isSet) => isSet === true);

    console.log(n, `running`);
}

Promise.all([test(1), test(2), test(3)]);

console.log(`setting event`);
eventAtom.conditionallyUpdate(() => true, true);

Make a barrier

import { atom } from 'synchronization-atom';

const barrierAtom = atom(3 /* empty seats */);

async function test(n: number) {
    await barrierAtom.conditionallyUpdate(
        () => true,
        (emptySeats) => emptySeats - 1
    );

    console.log(n, `waiting for seats to fill`);
    await barrierAtom.waitFor((emptySeats) => emptySeats < 0);

    console.log(n, `running`);
}

Promise.all([test(1), test(2), test(3), test(4), test(5)]);

Why?

I often use async calls like separate threads or at least like Go routines, as in as long as I'm fetching from DB or API over a network, it is effectively multi-threading (at least in my head).

Sadly I couldn't enjoy the very powerful sync primitives that Python, Java or Go has to offer.

Simultaneously, I noticed different standard libraries of the different languages have a different set of sync primitives but mutexes were at the heart.

I set out to create these primitives for JS while basing them off of a single primitive that is analogous to a mutex, but on parr with the level of expressiveness and ease we come to expect from the JS ecosystem.

synchronization-atom is the result of that effort.

Omran Jamal

License

MIT