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
# pnpm
pnpm add --save synchronization-atom
function atom<T>(initialState: T): Atom<T>
Creates and returns an Atom<T>
with the given initialState: T
.
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 thepredicate
. - 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.
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 areaction
is provided. - If a
reaction
is provided, the call returns immediately, and when thepredicate
is satisfied, thereaction
is executed. - Can be cancelled via an optional
AbortSignal
as last argument.
interface Atom<T> {
...
getState: () => T
...
}
Returns the current state of the atom.
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)]);
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.
MIT