Skip to content

soul-codes/reffx

Repository files navigation

reffx is a tiny utility for creating reference count-aware effects (reffx) requiring clean-ups ("disposing").

When you create a reffx, the effect is automatically invoked only at "first reference" and is cleaned up only upon the disposal of the "last reference".

Example

import { reffx } from "reffx";

const clock = reffx(() => {
  const intervalId = setInterval(() = console.log("tick"), 1000);
  return () => clearInterval(intervalId);
})

Now suppose we call clock() the first time, this would get the console logging tick every one second.

const disposeClock = clock();

Now we have one active reference to the effect clock. Later, we call clock() again, this would simply leave the current interval (rather than starting another setInterval) because the effect is already active with a ref count of one.

const disposeClock2 = clock(); // nothing happens

In order to stop the clock, we must call both disposeClock() and disposeClock2() to bring the active reference to the effect clock to zero.

Usage

import { reffx } from "reffx";
const fx = reffx(() => {
  /* ... effectful behavior here ... */
  return () => {
    /* ... effect disposer here .. */
  }
})

The reference count-aware effect fx now will keep track of the number of calls. Each call returns a disposer function that must be called to neutralize that reference.

const dispose1 = fx(); // ref count = 1, effect started
const dispose2 = fx(); // ref count = 2, nothing happens
const dispose3 = fx(); // ref count = 3, nothing happens
dispose3(); // ref count = 2, nothing happens
dispose3(); // ref count still = 2 because disposers are idempotent, nothing happens
dispose1(); // ref count = 1, nothing happens
dispose2(); // ref count = 0, effect disposed.

Keyed Reffx

keyedReffx is similar to reffx except it produces a map of effects that are maintained, and the reference count is maintained for each referentially distinct map key.

import { keyedReffx } from "reffx";
const subscribeToTopic = keyedReffx(
  (topicId: string) => /* ... some API calls here ... */);

const disposeTopicA1 = fx("topicA"); // topic A subscription starts
const disposeTopicB1 = fx("topicB"); // topic B subscription starts
const disposeTopicB2 = fx("topicB");
const disposeTopicA2 = fx("topicA");

disposeTopicA1();
disposeTopicB2();
disposeTopicB1(); // topic B subscription cleaned up
disposeTopicA2(); // topic A subscription cleaned up

Reffx with Effect Object

It can be useful for the effect to expose an interface with functionality that can be used as long as the effect is active. In this case, use objectReffx, which now returns a tuple of the effect object and the disposer.

import { objectReffx } from "reffx";
const clock = objectReffx(() => {
  let time = new Date();
  const getTime = () => time;
  const intervalId = setInterval(() => new Date(), 1000);
  return [getTime, () => clearInterval(intervalId)];
});

const [getTime, disposeClock] = clock();
getTime(); // some date object here
disposeClock(); // reduces reference count to clock effect by 1.

A keyed version of objectReffx, called keyedObjectReffx, is also availble.

Effect object decorator.

The tuple of effect object/disposer returned by objectReffx and keyedObjectReffx is the default behavior. It is also possible to pass another argument to these functions that customizes how the effect object and the disposer are combined.

import { objectReffx } from "reffx";
const clock = objectReffx(() => {
  let time = new Date();
  const getTime = () => time;
  const intervalId = setInterval(() => new Date(), 1000);
  return [getTime, () => clearInterval(intervalId)];
}, (getTime, disposeClock) => ({ getTime, disposeClock }));

const clockObject = clock();
clockObject.getTime(); // some date object here
clockObject.disposeClock(); // reduces reference count to clock effect by 1.

Reference-counted subscription

Using the idea of effect object, one can create a reference-counted effect that exposes a subscriber that automatically destroys the effect is no longer used.

import { objectReffx } from "reffx";
import { atomicEvent } from "atomic-event";

const subClock = objectReffx(() => {
  let time = new Date();
  const [sub, pub] = atomicEvent();
  const intervalId = setInterval(() => pub(new Date()), 1000);
  return [sub, () => clearInterval(intervalId)];
}, (sub, disposeClock) => (callback) => {
  const unsub = sub(callback);
  return () => { unsub(); disposeClock(); }
});

const unsubClock1 = subClock(time => console.log(time));
const unsubsubClock2 = clock(time => console.log(time));
unsubClock1(); // clock still ticking
unsubClock2(); // clock disposed

This pattern can be quite useful, so the package provides a shortcut, namely subReffx and keyedSubReffx, that assumes your effect function to return a subscriber/effect disposer pair. It then automatically decorates your effect object in the manner shown above. The decorator itself is also available as the import named asSubscriber.

import { subReffx } from "reffx";
import { atomicEvent } from "atomic-event";

const subClock = subReffx(() => {
  let time = new Date();
  const [sub, pub] = atomicEvent();
  const intervalId = setInterval(() => pub(new Date()), 1000);
  return [sub, () => clearInterval(intervalId)];
});

const unsubClock1 = subClock(time => console.log(time));
const unsubsubClock2 = clock(time => console.log(time));
unsubClock1(); // clock still ticking
unsubClock2(); // clock disposed

Use Cases

Reference count-aware effects can be used to perform effectful behavior such as subscription in a way that different consumers can simply invoke the effect without worrying about creating the same effects. For instance, if different parts of the UI require the same effect at different time, reffx can make sure that the effect only happens once.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published