Skip to content

Pool

Pool does the housekeeping of managing a limited set of resources which are shared by ‘users’. All resources in the Pool are meant to be the same kind of object.

An example is an audio sketch driven by TensorFlow. We might want to allocate a sound oscillator per detected human body. A naive implementation would be to make an oscillator for each detected body. However, because poses appear/disappear unpredictably, it’s a lot of extra work to maintain the binding between pose and oscillator.

Instead, we might use the Pool to allocate oscillators to poses. This will allow us to limit resources and clean up automatically if they haven’t been used for a while.

Basic usage

Resources can be added manually with addResource(), or automatically by providing a generate() function in the Pool options. They can then be accessed via a user key. This is meant to associated with a single ‘user’ of a resource.

To access a resource value, call useValue() with a userKey. The pool will try to allocate a resource to this particular ‘user’ if it can, or return the same resource the ‘user’ was allocated last time.

First set up the pool with options .

import { Pool } from 'https://unpkg.com/ixfx/dist/data.js';
const pool = Pool.create({
capacity: 5,
// Remove 'users' if they haven't been seen after one second
userExpireAfterMs: 1000,
// If the capacity is reached, reuse resources
fullPolicy: `evictOldestUser` // or could be 'error'
});

Add resources to the pool

for (const o of allResources) {
pool.addResource(o);
}

Now the pool is primed with resources and ready to be used.

To request a resource you need to supply an id to associate the resource’s ‘user’ with. For example, if you want to each pointer to have a resource associated with it, you might use the pointer’s id property. That way, if the same pointer requests a resource it will be given back the resource it had before.

The basic idea is that a user has exclusive control over a resource until it ‘releases’ it, or the Pool claims it back in the event of the pool being depleted.

// Borrow from the pool
const v = pool.useValue(userKey);
// Use value...
// Release it back
pool.release(userKey); // where the value of userKey is the same as for .useValue

Do not hold on to the reference to a pool value. Ideally you request it from the Pool whenever it’s used, or at least in the context of a short-running function. This is because the Pool might want to claim it back.

In-action

Manual resources

For example, if we are want to associate sound oscillators with TensorFlow poses. Since pose data has an id property, we can use that as the ‘user key’.

import { Pool } from 'https://unpkg.com/ixfx/dist/data.js';
const oscillators = [/* not shown*/]
const pool = Pool.create({
capacity: oscillators.length,
// Remove 'users' if they haven't been seen after one second
userExpireAfterMs: 1000,
// If the capacity is reached, re-use the oscillator from the
// last-seen pose
fullPolicy: `evictOldestUser`
});
// Add a bunch of pre-made oscillators as resources
for (const osc of oscillators) {
pool.addResource(osc);
}

When we get a chunk of data (which can contain one or more poses), we get an associated oscillator for each pose. If a pose with the same id previously used a resource, it will get that back again. That way we get a consistent mapping of poses and oscillators.

// New poses come in
const onData = (poses) => {
for (const pose of poses) {
// Get an oscillator for it this this pose
const osc = pool.useValue(pose.id);
// Do something with the resource...
osc.filterCutoff = ...
}
}

Dynamic resources

In this example, we will generate and dispose Pool resources on-demand. The Pool will create them (with the generate() function we provide) and automatically free them when they are unused. In this way, the Pool acts as a way of mapping keys to ad-hoc resources.

First we create the Pool, providing generate and free functions which are responsible for creating and destroying resources. Here HTML elements are the resources being managed.

import { Pool } from 'https://unpkg.com/ixfx/dist/data.js';
const pool = Pool.create({
capacity: 10,
userExpireAfterMs: 1000,
resourcesWithoutUserExpireAfterMs: 10000,
fullPolicy: `evictOldestUser`,
// Generate a new resource (in this example, a HTML element)
generate: () => {
const el = document.createElement(`DIV`);
el.classList.add(`pool-item`);
document.getElementById(`items`)?.append(el);
return el;
},
/**
* Delete the HTML element when the resource is freed
* @param {HTMLElement} el
*/
free:(el) => {
el.remove();
}
});

Now we can use resources from the pool, for example assigning a HTML element per key down. In this case, we’re associating the letter pressed with a resource.

const useState = () => {
const { keysDown } = state;
for (const key of keysDown) {
// Allocate a HTML element for each key held down
const el = pool.useValue(key);
// Set the text of the element to be the key
el.innerText = key;
}
};

This pattern is implemented in the demo below. Resources are allocated based on the letter. So if you press the same letter, you won’t see it twice, since it’s resource stays alive. If you type lots of different characters, older ones will disappear. And the Pool cleans up automatically if a resource hasn’t been used for a short time.

Advanced

Creating

Create a Pool and provide some options

const pool = Pool.create({
capacity: 3
})

Overview of options, all of which are optional.

optioninfo
capacityMaximum number of resources. Defaults to 0, no limit
capacityPerResourceMaximum number of users per resource. Defaults to 0, no limit
debugIf true, additional logging will be printed
freeA function that takes a single value. Call when a resource is removed from pool. Meant for cleaning up a value, where necessary.
fullPolicy’error’ (throws an error when pool is full) or ‘evictOldestUser’, removing oldest user of a resource. Defaults to ‘error’
generateA function that returns a value. Used for generating resources on demand
resourcesWithoutUsersExpireAfterMsIf provided, an unused resource will be removed after this period
userExpireAfterMsIf provided, a user will be marked as expired if it hasn’t been updated

Accessing resources

A resource can be accessed by a user key, returning a PoolUser instance.

const u = pool.use(key);

As described earlier, the user key is some unique reference for ‘owner’ of that pool resource. The idea is that if the same logical owner accesses the resource again, it always is using the same key. As far as the Pool is concerned, a different key means a different user, thus allocating a different resource.

When using resources managed by the Pool, it is important that all access to them happens via the Pool. Don’t cache references to resources, always access them via use() or useValue().

The PoolUser instance returned by use() has a disposed event handler. This allows you to be notified if you have lost ownership of a resource. It is also called if the resource itself has been cleaned up.

const u = pool.use(key);
u.addEventListener(`disposed`, evt => {
const { data, reason } = evt;
// 'reason' is a string describing why it was disposed
// 'data' is the data of the resource
// You might do some clean up
})

Resources can be manually released:

pool.release(userKey);

When releasing, the resource is freed for use under a different key. If there are no more users of a resource and the Pool option resourcesWithoutUsersExpireAfterMs is set, the resource will be freed.