Repository navigation
19 Timing and storage
Three small APIs on the ful global that the components use themselves and that are exposed for your own code: rate-limiting a function, dropping the outcome of work that newer work has superseded, and putting something in browser storage with a version on it.
ful.Timing carries three functions.
const [search, abort] = ful.Timing.debounce(300, (needle) => runSearch(needle));sleep(ms) resolves after the given milliseconds.
debounce(ms, fn, { immediate }) collapses a burst of calls into one. By default the call happens on the trailing edge, after the burst has stopped; immediate: true fires the leading edge instead, so the first call goes through and the rest of the burst is swallowed.
throttle(ms, fn, { leading, trailing }) lets a call through at most once per interval. Both edges are on by default, and either can be turned off: { leading: false } waits out the first interval before the first call, { trailing: false } drops the call a burst would otherwise leave pending at the end.
Both return [wrapped, abort]. Calling abort() cancels whatever is pending without disabling the function, so the wrapped function keeps working afterwards. That is what you want when a component is detached and re-attached rather than destroyed.
ful-select throttles its own search at 400ms, which is the built-in example of the shape.
Asynchronous work overlaps: a list asks for page 2 and then for page 3 before page 2 answers, and whichever answer arrives last would win. ful.Claims makes the last request win instead. One Claims stands for one contended resource, and each piece of work takes a claim on it before it starts:
#loads = new ful.Claims();
async load(page) {
const claim = this.#loads.take(); // supersedes every earlier claim
const rows = await this.#fetch(page);
if (claim.stale) {
return; // a newer load started meanwhile: this answer owns nothing
}
this.#paint(rows);
}| call | effect |
|---|---|
take() |
starts a new generation, superseding every earlier claim, and answers the claim on it |
hold() |
answers a claim on the current generation without superseding anything: work that joins what is running, and that a later take() must still detach |
invalidate() |
supersedes every claim without taking one: a hide that ends every pending show |
claim.stale |
true once a later take() or invalidate() superseded the claim |
Ask stale after every await and before every effect: painting, storing state, dispatching an event, throwing towards a caller. Nothing is cancelled: a fetch in flight still resolves, its outcome is just dropped. Hold one Claims per resource rather than per component, so work that contends separately (a dropdown's searches and the labels of an assigned value, in ful-select) does not cancel each other.
ful.LocalStorage and ful.SessionStorage wrap the platform's two stores with the same four calls, serializing through JSON.
ful.LocalStorage.save('draft', { subject: 'hello' });
ful.LocalStorage.load('draft'); // the object, or undefined
ful.LocalStorage.remove('draft');
ful.LocalStorage.pop('draft'); // load and remove in one callThree details that matter in practice. A miss answers undefined, not null: storage is the one place in the library where that is the empty answer, since it is the platform's own. Reads never throw: where storage is unreachable at all, which happens with blocked cookies and in some embedded and private contexts, a read is a miss rather than a failure, and so is a stored value that no longer parses, which is dropped on the way out. Writes do throw, on a full quota, deliberately: silently losing what you asked to persist is worse than a rejected write you can see.
The methods are bound to nothing, so destructuring them keeps them working:
const { save, load } = ful.SessionStorage;ful.VersionedLocalStorage and ful.VersionedSessionStorage add a revision beside the value.
ful.VersionedLocalStorage.save('countries', 'v7', entries);
ful.VersionedLocalStorage.load('countries', 'v7'); // the entries
ful.VersionedLocalStorage.load('countries', 'v8'); // undefined: evictedA load under a revision that does not match what was stored evicts the entry and answers undefined. That is the whole mechanism behind ful-select's revision attribute: a remote vocabulary is cached across page loads, and changing the revision on the server invalidates every client's copy without anyone having to clear anything. A select can also leave the attribute valueless and take the revision from the registry, so the page states its build once rather than in every element: see ful-select.
The revision is an opaque string, so a deployment id, a content hash or a date all work equally well.
ftl
httpc
ful
- Getting started
- Fields
- Forms
- Inputs
- Select
- Files
- Temporal inputs
- Tables
- Disclosures
- Navigation
- Async sections
- Timing and storage
- Theming
Extending
Reference
Project