@webreflection/utils
Each utility can be loaded from a CDN via either https://esm.run/@webreflection/utils/UTILITY or https://cdn.jsdelivr.net/npm/@webreflection/utils/src/UTILITY.js.
This document describes each utility separately.
accessor
Wrap a { get, set } descriptor as a single synchronous function. Argument
count selects the operation: call with no arguments to read, or with exactly
one argument to write.
The descriptor may be any object or class instance that defines or inherits
both methods. Extra properties are allowed. get takes no parameters and
returns a value. set takes exactly one value and may return void. The
returned accessor is synchronous: both ref() and ref(value) return T.
This mirrors (value = x) where assignment yields the assigned value. Writes
are ref(value) since assignment syntax cannot be expressed via property
descriptors alone.
get and set are invoked with a this context. When the accessor is called
standalone (ref()), that context is the descriptor object passed to
accessor. When it is assigned to a host and called as a property
(host.ref()), the context is the host instead. The same descriptor can
therefore target either its own backing fields or those on another object,
depending on how the returned function is used.
import accessor from '@webreflection/utils/accessor';
// Standalone: `this` in get/set is the descriptor object itself.
const value = accessor({
value: 42,
get() {
return this.value;
},
set(next) {
this.value = next;
},
});
value(); // 42
value(43); // 43
value(); // 43
When the accessor is installed on a host object, get and set see that host
as this instead:
const object = Object.defineProperty({ _: 42 }, 'value', {
enumerable: true,
writable: true,
value: accessor({
get() {
return this._;
},
set(next) {
this._ = next;
},
}),
});
object.value(); // 42
object.value(43); // 43
object.value(); // 43
object._; // 43
In TypeScript, annotate the expected context on get / set with @this in
JSDoc, or with an explicit this parameter in .d.ts consumers. The context
type can differ between the two patterns above: a standalone accessor usually
types this as the descriptor (or a shape it includes), while a host property
accessor types this as the host object.
all
A Promise.all companion with one extra convenience: when called with a
single object literal, it resolves each value and returns an object with the
same keys.
import all from '@webreflection/utils/all';
const user = await all({
name: fetchName(),
age: fetchAge()
});
// { name: 'Ada', age: 36 }
This preserves the shape and names of object-literal work, avoiding the
positional array juggling required by Promise.all. For arrays, or for two or
more arguments, it behaves like Promise.all and resolves to an array.
To await a single promise, use await (or Promise.resolve) directly — all
is for resolving many values at once, not a substitute for awaiting one promise.
ascii
An extremely small string to Uint8Array converter for known ASCII-compatible
content. It does not validate or encode Unicode code points; it simply stores
each string unit as its 0-255 char code.
This is meant for niche cases where the input is already constrained, such as ISO date strings, plain-English global names or method names, and other small ad-hoc values.
import { encode, decode } from '@webreflection/utils/ascii';
console.log(decode(encode('ASCII')));
// ASCII
Please note that decoding also fails for inputs bigger than about 64K bytes, or
whatever argument limit your runtime has for String.fromCharCode.
async-accessor
Wrap a { get, set } descriptor as a single async function. Argument count
selects the operation: call with no arguments to read, or with exactly one
argument to write.
The descriptor may be any object or class instance that defines or inherits
both methods. Extra properties are allowed. get takes no parameters and may
return a value or a promise. set takes exactly one value; its return value
is ignored and may be sync or async. The returned accessor is always async:
both await ref() and await ref(value) resolve to T.
This mirrors (value = x) where assignment yields the assigned value. Writes
are await ref(value) since assignment syntax cannot be expressed via
property descriptors alone.
get and set are invoked with a this context. When the accessor is called
standalone (await ref()), that context is the descriptor object passed to
asyncAccessor. When it is assigned to a host and called as a property
(await host.ref()), the context is the host instead. The same descriptor can
therefore target either its own backing fields or those on another object,
depending on how the returned function is used.
import asyncAccessor from '@webreflection/utils/async-accessor';
// Standalone: `this` in get/set is the descriptor object itself.
const value = asyncAccessor({
value: 42,
async get() {
return this.value;
},
async set(next) {
this.value = next;
},
});
await value(); // 42
await value(43); // 43
await value(); // 43
When the accessor is installed on a host object, get and set see that host
as this instead:
const object = Object.defineProperty({ _: 42 }, 'value', {
enumerable: true,
writable: true,
value: asyncAccessor({
async get() {
return this._;
},
async set(next) {
this._ = next;
},
}),
});
await object.value(); // 42
await object.value(43); // 43
await object.value(); // 43
object._; // 43
In TypeScript, annotate the expected context on get / set with @this in
JSDoc, or with an explicit this parameter in .d.ts consumers. The context
type can differ between the two patterns above: a standalone accessor usually
types this as the descriptor (or a shape it includes), while a host property
accessor types this as the host object.
base64
A small async wrapper around Uint8Array.prototype.toBase64() and
Uint8Array.fromBase64() for encoding and decoding binary data as strings.
Optional compression is applied through CompressionStream and
DecompressionStream before or after the base64 step.
// if a polyfill is required for older browsers
import '@ungap/base64';
// this module base64 utility
import { encode, decode } from '@webreflection/utils/base64';
const encoded = await encode('Hello, world!');
const decoded = await decode(encoded);
console.log(decoded);
// Hello, world!
Pass the same format to both sides when the payload should be compressed
first. Supported formats are brotli, gzip, deflate, deflate-raw, and
zstd as mentioned on MDN.
const compressed = await encode('Hello, world!', { format: 'deflate' });
const decompressed = await decode(compressed, { format: 'deflate' });
console.log(decompressed);
// Hello, world!
By default, decode() returns a UTF-8 string. Pass { buffer: true } to get
the raw ArrayBuffer instead.
const buffer = await decode(compressed, { format: 'deflate', buffer: true });
console.log(new Uint8Array(buffer));
// Uint8Array(13) [ 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33 ]
encode() accepts any BlobPart, so strings, typed arrays, and ArrayBuffer
values all work. Both helpers also accept alphabet: 'base64url' and other
options forwarded to the native base64 APIs, such as omitPadding on encode and
lastChunkHandling on decode.
Use stream() when the decoded (and optionally decompressed) payload should stay
a Response / readable stream instead of being buffered into a string or
ArrayBuffer. The format option still drives DecompressionStream; type
sets the response Content-Type (default application/octet-stream).
import { encode, stream } from '@webreflection/utils/base64';
const compressed = await encode('Hello, world!', { format: 'deflate' });
const response = await stream(compressed, { format: 'deflate' });
console.log(await response.text());
// Hello, world!
Transformer is a TransformStream that turns base64 text chunks into
Uint8Array chunks. Pipe a stream of base64 through it when decoding should
happen incrementally rather than in one shot:
import { encode, Transformer } from '@webreflection/utils/base64';
const encoded = await encode('Hello, world!');
const { body } = await fetch(`data:application/octet-stream,${encoded}`);
const response = new Response(body.pipeThrough(new Transformer));
console.log(await response.text());
// Hello, world!
Both stream and Transformer are also available as standalone subpaths:
@webreflection/utils/base64/stream and @webreflection/utils/base64/transformer.
bound-once
This is equivalent to bound, except each bound method is created only once. It is useful when bound method identity must be preserved across multiple calls.
This variant uses sticky to ensure that weakly referenced targets always produce the same bound method within the same realm.
bound-key
This utility binds one or more functions to a context key and caches the result
per key. It is useful when the same logic should run with different this
values — especially in DOM code — without passing that context through every
call.
Pass one or more functions; it returns a matching array of factories. Each
factory accepts a key, binds the function’s this to that key, and reuses the
same bound function when the key is seen again.
import boundKey from '@webreflection/utils/bound-key';
function greet() {
return `Hello ${this}!`;
}
const [bound] = boundKey(greet);
const world = bound('world');
world(); // 'Hello world!'
bound('world') === world; // true — cached per key
A typical DOM use case:
function handle() {
this.classList.toggle('active');
}
const [boundHandle] = boundKey(handle);
for (const el of document.querySelectorAll('.item'))
el.addEventListener('click', boundHandle(el));
Unlike bound and bound-once, which bind methods on an object target, bound-key binds arbitrary functions to any key and keeps one cached bound function per key.
bound
This utility provides an object-destructuring syntax shortcut for binding methods.
import bound from '@webreflection/utils/bound';
const { all, resolve } = bound(Promise);
all([1, 2, 3]);
resolve(4);
The bound-once variant ensures that repeated accesses, such as boundOnce(Promise).all, always return the same bound method.
cache
A temporal Map subclass for short-lived memoization. It keeps newly added
entries only until its scheduled cleanup runs, making it useful to reuse
expensive work for repeated access to the same key without keeping the value
around as a long-term cache.
import Cache from '@webreflection/utils/cache';
const users = new Cache;
const loadUser = id => users.getOrInsertComputed(
id,
id => fetch(`/users/${id}`).then(response => response.json())
);
When the constructor delay is omitted, 0, or less than 0, cleanup is
queued as a microtask, so same-tick lookups can share the stored value and the
map clears itself before the next task. Pass a positive delay, such as
new Cache(100), to keep entries until a timer removes them instead.
Use getOrInsert(key, value) or getOrInsertComputed(key, callback) when the
value should only be stored if missing. Use put(key, value) for the faster
cache.get(key) ?? cache.put(key, value) pattern when duplicate queue entries
are acceptable.
caller-of
Borrow any function or method so the caller passes this explicitly as the
first argument. It is a one-liner over Function.prototype.call.bind:
import callerOf from '@webreflection/utils/caller-of';
const hasOwn = callerOf(Object.prototype.hasOwnProperty);
hasOwn({ a: 1 }, 'a'); // true
const toString = callerOf(Object.prototype.toString);
toString([]); // '[object Array]'
Unlike bound, which binds methods to a fixed host object, caller-of
keeps this free: each call supplies a different thisArg. That is useful for
safe borrowed natives (hasOwnProperty, toString, …) without touching the
target’s prototype chain, and without allocating a new bound function per host.
// equivalent
const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
class
Upgrade an existing instance to a real subclass without invoking the base
constructor as super(). Pass any constructor Base to get a bridge whose
.prototype is the same object as Base.prototype; then class Sub extends
custom(Base) works like a normal class, while new Sub(target) only runs
Object.setPrototypeOf(target, new.target.prototype) and returns that same
target.
This is the same idea as
custom-function/factory,
with tighter TypeScript generics so the bridge and InstanceType stay aligned.
import custom from '@webreflection/utils/class';
class Strong extends custom(String) {
constructor(value) {
super(new String(value));
}
}
const strong = new Strong('hello');
strong instanceof String; // true
strong instanceof Strong; // true
String(strong); // "hello"
Typical use: bases whose constructor is awkward or wrong to re-run on an object
you already have — Function (would imply eval), DOM element constructors,
boxed primitives, and so on. You keep the original identity and engine shape,
and still get instanceof, methods, getters, super, and private fields.
import custom from '@webreflection/utils/class';
const Div = custom(HTMLDivElement);
class MyDiv extends Div {
constructor(...childNodes) {
super(document.createElement('div'));
this.append(...childNodes);
}
}
document.body.appendChild(
new MyDiv(
new MyDiv('A'),
new MyDiv('B', 'C'),
),
);
Signature: custom(Base) => Bridge, where new Sub(target) returns
target typed as InstanceType<typeof Base> after the prototype swap.
dom-fragment is built on this pattern.
content
A tiny factory builder for turning markup strings into DocumentFragment
instances, where each factory parses in the context of a specific element. The
parsing context is just an element, so any namespace reachable through
createElementNS works: HTML, SVG, MathML, and so on.
import content from '@webreflection/utils/content';
const parse = content({
html: document.createElement('template'),
svg: document.createElementNS('http://www.w3.org/2000/svg', 'svg')
});
const fragment = parse.html('<div>Hello</div>');
document.body.append(fragment);
The passed object maps free-form names to the element whose contents define the
parsing context for that name. The returned object exposes the same keys, each
being a value => DocumentFragment factory. A single shared Range is reused
across all factories, re-selecting node contents only when the active context
changes, so repeated parsing within the same context stays cheap.
Unlike most DOM utilities here, content also accepts an optional second
document argument. This is handy in SSR projects where there is no global
document, but one can be created with linkedom, jsdom, or similar and
passed in so the same parsing logic works on the server.
import { parseHTML } from 'linkedom';
import content from '@webreflection/utils/content';
const { document } = parseHTML('<html><body></body></html>');
const parse = content({
html: document.createElement('template')
}, document);
const fragment = parse.html('<div>Hello</div>');
When omitted, the second argument defaults to globalThis.document. For the
common HTML and SVG contexts in the browser, see dom-content.
dedent
Strip common leading indentation from multiline strings. The utility finds the first non-empty line, measures its leading whitespace, and removes that same indentation from every line while leaving everything else unchanged.
It works both as a tagged template and as a plain function on strings. In tag form, interpolations are joined first via plain-tag, then dedented.
import dedent from '@webreflection/utils/dedent';
console.log(dedent`
Hello,
world!
`);
// Hello,
// world!
console.log(dedent(`
Hello,
world!
`));
// Hello,
// world!
Use the tag form when the string is written inline in source code and should lose the surrounding indentation. Use the function form when the input is already a string variable.
devtools
Short selectors for DOM queries, mirroring the helpers available in browser
DevTools. $ and $$ wrap querySelector and querySelectorAll; $x
evaluates an XPath expression and returns matching nodes as an array.
import { $, $$, $x } from '@webreflection/utils/devtools';
const title = $('h1');
const links = $$('a[href]');
const items = $x('//li[@data-id]');
Each helper accepts an optional root node. When omitted, it defaults to
document, so queries can be scoped to any Document, DocumentFragment, or
Element.
dom-content
A ready-made content instance for the two most common contexts,
exposing html and svg factories backed by a <template> element and an
<svg> element respectively.
import { html, svg } from '@webreflection/utils/dom-content';
const layout = html('<section><h1>Title</h1></section>');
const icon = svg('<circle cx="10" cy="10" r="5" />');
Each helper parses its markup string in the matching context and returns a
DocumentFragment ready to be inserted into the DOM. This module relies on the
global document, so it is browser-oriented. For SSR, or when a specific
Document or additional parsing contexts are required, use
content directly and pass the server-side document as its second
argument.
dom-diff
Reconcile a live list of DOM nodes against a desired future list, anchored to a
pin node that marks where the list ends in the parent. Entries may be ordinary
ChildNodes or persistent fragments from dom-fragment.
import diff from '@webreflection/utils/dom-diff';
const parent = document.querySelector('#list');
const pin = document.createComment('');
parent.append(pin);
let nodes = [];
// insert a, b, c before the pin
nodes = diff(nodes, [a, b, c], pin);
// reorder and drop b
nodes = diff(nodes, [c, a], pin);
// clear the list
nodes = diff(nodes, [], pin);
The pin can also be bound once so every later call remembers where to differ:
const update = diff.bind(pin);
nodes = update(nodes, [a, b, c]);
nodes = update(nodes, [c, a]);
nodes = update(nodes, []);
Persistent fragments count as a single list entry while their children occupy a
range in the parent. Diff places them via valueOf() and then advances the pin
to the fragment’s start marker, so siblings still line up correctly:
import diff from '@webreflection/utils/dom-diff';
import Fragment from '@webreflection/utils/dom-fragment';
const pin = document.body.appendChild(document.createComment(''));
const group = new Fragment(document.createDocumentFragment());
group.append(document.createElement('div'), document.createElement('div'));
let nodes = [];
nodes = diff(nodes, [hr1, group, hr2], pin);
nodes = diff(nodes, [group, hr2], pin); // drop hr1, keep the group range
Signature: diff(current, future, pin?) => future — pass pin each time, or
omit it after diff.bind(pin).
- nodes present in
currentbut missing fromfutureare removed — skipped entirely whencurrentandfutureare the same array reference (in-place mutate / reorder only needs placement) - nodes in
futureare walked right-to-left and placed so each ends up immediately before the pin (then the pin advances), producing the future order as consecutive siblings ending at the original pin - for a persistent fragment (
nodeType === 11), placement usespin.before(node.valueOf())and the pin becomesnode.firstChild(the start comment); for a normal node, the pin becomes that node - when the pin is connected and a normal node already lives under the same
parent,
parentNode.moveBeforeis preferred so state is preserved; otherwisepin.before(...)inserts or re-homes the entry (including fragments) - returns
futureso the caller can keep that array as the nextcurrent
Requires a DOM parent on pin.parentNode, and a pin that supports before
(any ChildNode). Useful for keyed list updates where you already hold node
identity — including multi-node groups via dom-fragment — and
only need remove / insert / reorder.
dom-fragment
A DocumentFragment subclass (via class) that keeps a live range in
the tree instead of dissolving on insert. Construct with an existing fragment;
super swaps its prototype onto the subclass without re-running
DocumentFragment as a constructor, so the same object becomes
instanceof Fragment while remaining a real fragment.
The instance wraps content between stable <> / </> comment markers
(firstChild / lastChild). Once placed in a parent, that range can be moved,
removed, or replaced as one unit. Designed as a single entry in
dom-diff lists.
import Fragment from '@webreflection/utils/dom-fragment';
const pin = document.body.appendChild(document.createComment(''));
const fragment = document.createDocumentFragment();
fragment.append(
document.createElement('li'),
document.createElement('li'),
);
// guards fragment childNodes via surrounding comments
const group = new Fragment(document.createDocumentFragment());
// markers + children land before `pin`; `group` remains the handle
pin.before(group);
group.remove(); // drops everything from the start marker through the end marker
Signature: new Fragment(documentFragment).
The constructed instance is the same fragment object, with:
firstChild/lastChild— comment markers that bound the live rangeparentNode— the parent of the start marker when the range is in the treebefore(...nodes)— insert before the start markerremove()— delete the range (markers and everything between) from the parentreplaceWith(node)— replace the whole range with a single nodevalueOf()— if children are live under a parent, gather the range back into the fragment and return it, so a later insert moves the group again
Use this when a list item is logically one entry but renders as several nodes
(a view “block”, a row with multiple cells, and so on). Pair it with
dom-diff so the group participates in reconcile like any other
node. Requires a DOM (document, Range, comments).
dom-observer
Shared browser helper that runs one document-wide MutationObserver and lets
any number of subscribers react to added or removed nodes. The first import in
the realm (via sticky) starts observing document with
{ childList: true, subtree: true } and patches Element.prototype.attachShadow
so every new shadow root is observed too. Later copies of the module — for
example after re-bundling — reuse the same subscribers and shadows and skip
setup, so there is only one observer and one attachShadow patch per realm.
Requires a DOM (document, MutationObserver, Element). In Node, provide one
(e.g. linkedom) or do not import this entry.
import { subscribers, shadows } from '@webreflection/utils/dom-observer';
subscribers.add(mutations => {
for (const { addedNodes, removedNodes } of mutations) {
// react to tree changes, including inside shadow roots
}
});
// later
const host = document.querySelector('my-element');
const root = shadows.get(host); // ShadowRoot attached after this module loaded
Exports:
subscribers— aSetof(mutations: MutationRecord[]) => voidcallbacks. Add your listener once; every mutation batch is forwarded to all subscribersshadows— aWeakMapfrom hostNodeto theShadowRootcreated via the patchedattachShadow, including closed roots that are otherwise unreachable
Use this when several features need the same add/remove notifications without
each spinning up its own observer or attachShadow patch. Prefer importing it
as early as possible so shadow roots attached before the first sticky install are
not missed. dom-signals is built on top of this module.
dom-signals
Browser companion to signals: the same minimal core, plus explicit
subscribe / unsubscribe helpers that bind a callback to both a DOM node and
a signal.
Built on dom-observer: importing this entry registers one
subscriber on the shared observer (and therefore inherits its sticky
once-per-realm document-wide watch plus attachShadow patch). Associations are
stored in a WeakMap, so a node that becomes unreachable can be garbage
collected without leaving signal subscriptions behind. Subtree walks also follow
shadow roots recorded in shadows, so nodes inside open or closed shadow DOM
pause and resume with their host.
import {
signal,
computed,
batch,
subscribe,
unsubscribe,
} from '@webreflection/utils/dom-signals';
const text = signal('test');
const num = signal(0);
// note: mandatory tracking of signals relevant for this computed
const label = computed(() => `${text.value} ${num.value}`, [text, num]);
const app = document.querySelector('#app');
const sync = subscribe(app, label, () => {
app.textContent = label.value;
});
// apply the returned callback directly
// if the dom node is already live
if (app.isConnected) sync();
batch(() => {
text.value = 'test2';
num.value = 1;
});
// later, drop the association explicitly if the node stays in the tree
unsubscribe(app, label, sync);
subscribe(node, signal, callback) registers callback on signal immediately
and remembers the pair on node. unsubscribe removes that pair and deletes
the callback from the signal.
Lifecycle is automatic when nodes move in or out of the tree (light DOM or shadow):
- disconnected — the observer walks the removed subtree and
deletes every associated callback from its signal, so detached UI stops reacting and does not keep signals alive - reconnected — the observer walks the added subtree,
adds those callbacks again, and invokes each one once so the node refreshes against the current value
You still choose what to subscribe; the observer only keeps those explicit
bindings in sync with DOM attachment. Prefer this entry over importing
signals alone when the app runs in the browser and updates should
follow node lifetime. For custom add/remove handling without signals, use
dom-observer directly. For arbitrary objects or symbols (no DOM
lifecycle), use ref-signals. For object-shaped reactive state
with property syntax, use state-signals (its own
subscribe / unsubscribe take a property key). For environments without a
document, use signals or ref-signals directly.
empty
Frozen, shared empty references for code that needs a guaranteed-empty array, plain object, or null-prototype object without allocating a new one each time.
import { array, object, nil } from '@webreflection/utils/empty';
const defaults = { ...object, theme: 'light' };
const items = [...array, 'new'];
array— a frozen, shared empty array (readonly never[])object— a frozen, shared empty object (Readonly<Record<string, never>>)nil— a frozen, shared empty object with anullprototype
fetch
A drop-in fetch proxy that keeps the usual Promise<Response> behavior while
also exposing Response fields directly as thenables on the returned promise.
Promise methods (then, catch, finally) forward to the underlying promise,
so await fetch(url) and fetch(url).then(...) still yield a Response.
Body and clone methods (text, json, arrayBuffer, blob, bytes,
formData, clone) are invoked automatically when accessed: if response.ok
is false they reject with that Response, otherwise they resolve to the method
result. Other Response properties (status, ok, headers, …) resolve to
the property value.
import fetch from '@webreflection/utils/fetch';
// still a normal Response promise
const ok = await fetch(location.href).then(r => r.ok);
// Response properties as thenables
const status = await fetch(location.href).status;
// body readers are auto-invoked (no `.text()` call)
const html = await fetch(location.href).text;
// non-OK responses reject with the Response when reading a body
try {
await fetch('/missing').json;
} catch (response) {
console.log(response.status); // 404
}
global
A lazily trapped view of globalThis for pages that must keep using native
constructors, prototypes, and utilities even when other scripts try to replace
or pollute them.
Import this module as early as possible — ideally before any third-party code
runs — then read the globals you need through it instead of from globalThis
directly. Each property is snapshotted on first access. Non-null objects are
trapped recursively, so once Object.prototype.toString (or any other nested
reference) is retrieved, later reassignment or prototype pollution on the live
global no longer affects the trapped copy.
import global from '@webreflection/utils/global';
const { Object: { prototype: { toString } } } = global;
// later, a hostile script mutates the live global
Object.prototype.toString = function () { return 'polluted'; };
globalThis.Object = function Object() {};
toString.call([]); // still the native result
toString !== Object.prototype.toString; // true — trapped copy is reliable
globalThis.Object !== global.Object; // true — constructor was trapped first
This is deliberately niche: it does not sandbox code, block execution, or trap every global up front. It only protects the specific properties already read through this export, at the time they were first read. Destructure or access everything you rely on early, and load this module before untrusted scripts when that guarantee matters.
has-own
A quick and simple polyfill for Object.hasOwn() on older browsers. When the
native method is available, it is used directly; otherwise it falls back to
Object.prototype.hasOwnProperty.call.
import hasOwn from '@webreflection/utils/has-own';
console.log(hasOwn({ a: 1 }, 'a'));
// true
id
A tiny factory for unique int32 identifiers. Each call to the returned function
yields the next value, and the counter wraps automatically from 2 ** 31 - 1 to
-2 ** 31 so it can roundtrip forever without growing past signed 32-bit range.
import id from '@webreflection/utils/id';
const next = id();
console.log(next()); // 0
console.log(next()); // 1
const roundtrip = id(2 ** 31 - 1);
console.log(roundtrip()); // 2147483647
console.log(roundtrip()); // -2147483648
Pass an optional starting value when the first issued id should not be 0.
instance-of
Return the first constructor in a list that matches ref via instanceof, so
switch/case can dispatch on types without the switch (true) workaround.
import instanceOf from '@webreflection/utils/instance-of';
const types = [Response, Request, Headers];
const kind = instanceOf(ref, types);
switch (kind) {
case Response:
// ...
break;
case Request:
// ...
break;
case Headers:
// ...
break;
case Array:
// arrays that did not match any listed constructor
break;
case Object:
// plain objects that did not match any listed constructor
break;
}
ref must be a non-null object (typeof ref === 'object' && ref !== null).
Constructors are checked in list order; the first match wins. When none match,
the result is Array for arrays and Object otherwise, so both defaults can
be handled explicitly in the same switch.
iterable
Ensures an object can be consumed by for...of, spread, Array.from, and
other iterable-aware APIs.
import iterable from '@webreflection/utils/iterable';
const query = iterable({ page: 1, perPage: 20 });
console.log([...query]);
// [['page', 1], ['perPage', 20]]
If the object already defines or inherits Symbol.iterator, it is returned
unchanged. Otherwise, the same object receives a configurable own
Symbol.iterator method that yields Object.entries(ref).
json-callback
Return a callback’s source string for JSON payloads. Arrow functions and
function declarations or expressions are left as-is; method shorthand is
normalized to named function form so the result can be revived later.
import toJSONCallback from '@webreflection/utils/json-callback';
console.log(toJSONCallback(() => {}));
// () => {}
console.log(toJSONCallback({ f() {} }.f));
// function f() {}
json-storage
A small Map like facade over localStorage by default, or sessionStorage
when requested. Values are serialized with JSON.stringify on write and parsed
with JSON.parse on read, so callers can store structured data without
manually converting every value.
import JSONStorage from '@webreflection/utils/json-storage';
const preferences = new JSONStorage;
preferences.set('theme', { dark: true });
console.log(preferences.get('theme').dark);
// true
The API follows familiar Map names where they make sense: get, set,
has, delete, clear, entries, keys, values, and default iteration.
Missing keys return undefined, while delete(key) reports whether the key was
present.
const cart = new JSONStorage(JSONStorage.SESSION);
const items = cart.getOrInsert('items', []);
items.push('book');
cart.set('items', items);
for (const [key, value] of cart) {
console.log(key, value);
}
Use getOrInsert(key, value) to create a value only when the key is absent, or
getOrInsertComputed(key, callback) when the initial value should be computed
from the key. A second constructor argument can replace the native JSON API as
long as it provides compatible parse(source) and stringify(value) methods.
Store JSON-serializable values that stringify turns into a string. null is
fine (JSON.stringify(null) is "null"). If stringify returns null or
undefined (as default JSON does for undefined), set / put remove the
key instead of writing — so a later get is undefined and has is false.
libwabt
A drop-in bootstrap of the original
libwabt.js from the WebAssembly Binary
Toolkit. No npm package tracks the latest demo build, so this wrapper
re-packages the files WABT already publishes after its Emscripten demo build.
The WASM binary is shipped as a compressed base64 payload and decoded at
runtime, so there is nothing extra to fetch, land on disk, or compile by hand.
The default export is async: it instantiates the module and returns the same
parseWat / readWasm API as upstream. From there WAT can be compiled to WASM,
WASM can be printed back as WAT, modules can be validated, and the resulting
binary can be instantiated for tests.
import libwabt from '@webreflection/utils/libwabt';
const wabt = await libwabt();
const wat = wabt.parseWat(
'add.wat',
`(module
(func (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add))`
);
wat.validate();
const { buffer } = wat.toBinary({ log: true, write_debug_names: true });
const { instance: { exports } } = await WebAssembly.instantiate(buffer);
exports.add(1, 2); // 3
wat.destroy();
const wasm = wabt.readWasm(buffer, { readDebugNames: true });
console.log(wasm.toText({ foldExprs: false, inlineExport: false }));
wasm.destroy();
Any extra argument is forwarded to the upstream loader except locateFile,
which this wrapper owns so the inlined binary is used. Call destroy() on each
parsed module when it is no longer needed. For the common parse / validate /
instantiate loop, wat-tag binds features, init, and import
once and returns a template tag that also disposes via using or
[Symbol.dispose](). Upstream files keep their original
Apache 2.0 license and
credits.
map
A native Map subclass with one extra method: put(key, value). It stores the
entry like set, but returns the value instead of the map reference itself.
import Map from '@webreflection/utils/map';
const map = new Map;
const value = map.put('theme', { dark: true });
console.log(value.dark);
// true
Use set when chaining on the map is needed; use put when the stored value
should flow directly into the next expression.
next-resolver
The former next-resolver
package, brought into this collection because that module depended on
@webreflection/utils as a whole — a heavy extra dependency for such a small
helper — and size needed to stay constrained. The logic now sits next to
id and with-resolvers, which it already used.
It simplifies the repeated unique-ID + promise dance: next() yields a unique
identifier and a promise; resolve(id, value) (or resolve(id, null, error)
to reject) settles that promise later.
import nextResolver from '@webreflection/utils/next-resolver';
const [next, resolve] = nextResolver();
// next unique identifier and its promise
const [id, promise] = next();
// pass the promise around, hold the id ...
// ... so that whenever it's done:
if (condition) {
resolve(id, value);
// or reject via
resolve(id, null, new Error('reason'));
}
The default nextResolver() accepts an optional callback that will receive a
unique identifier that can be used to return something else:
// use strings instead of numbers as IDs
const [next, resolve] = nextResolver(String);
const [id, promise] = next();
typeof id; // string
// use any Map key variant
const [next, resolve] = nextResolver(id => {
// make it stronger (not really useful)
return `${id}-${crypto.randomUUID()}`;
// make it a unique ref
return { id };
});
If the returned value is still awaited, the next id will be passed to create
a new identifier (so don’t put too much logic within the id creation; it’s
already granted to be unique per that logic/session).
This is mostly needed to brand async operations, so a prefix is often enough:
const [next, resolve] = nextResolver(id => `my-logic-${id}`);
plain-tag
Transform a generic tagged template function into a plain string by interpolating the static parts and values, without any special handling or escaping.
import plainTag from '@webreflection/utils/plain-tag';
console.log(plainTag`Hello, ${'world'}!`);
// Hello, world!
python-tag
Bootstrap a Pyodide or
MicroPython
WASM interpreter and return a runner that executes Python with the least
ceremony. The default export is async: it dynamically imports the module URL,
prefers loadMicroPython when present, otherwise calls loadPyodide, and
forwards any extra arguments to that loader. The resolved value is the runner;
the underlying instance is also available as .interpreter.
import pythonTag from '@webreflection/utils/python-tag';
// Pyodide (default module URL is https://esm.run/pyodide)
const py = await pythonTag();
// or pin a CDN build / use MicroPython instead
// const py = await pythonTag('https://cdn.jsdelivr.net/pyodide/v0.28.0/full/pyodide.mjs');
// const py = await pythonTag(micropythonURL, { url: wasmURL });
Code is always dedented after plain-tag interpolation.
The last expression’s value is returned (same as Pyodide’s runPython /
runPythonAsync).
Template literal (async, default)
Tagged use runs via runPythonAsync, so top-level await works and the result
is a promise:
const result = await py`
print('Hello, world!')
1 + 2
`;
// 3
String + optional options
Pass a code string, and optionally an options object shaped like
Pyodide’s runPython options
(globals, locals, filename, …). This utility also accepts sync: true,
which selects synchronous runPython instead of runPythonAsync and is
stripped before the call. MicroPython’s WASM runner takes the code only, so
fields like globals are ignored; sync remains handled here.
await py(`print(123)`);
// async via runPythonAsync
py(`print(123)`, { sync: true });
// sync via runPython
const globals = py.interpreter.toPy({ test: true });
await py(`'test' in globals() and globals()['test']`, { globals });
// true
Bound options tag
Pass only the options object to get a template tag that reuses those options on
every invoke (including a persistent sync: true):
const syncPy = py({ sync: true });
syncPy`1 + 2`;
// 3 (not a Promise)
const withGlobals = py({ globals });
await withGlobals`'test' in globals()`;
// true
ref-id
Assigns a unique int32 identifier to any WeakMap-compatible key (object or
symbol). The same reference always gets the same id; different references get
different ids. Values come from id, so they stay in signed 32-bit range
and wrap forever without growing past Int32Array limits.
import refId from '@webreflection/utils/ref-id';
const a = {};
const b = {};
console.log(refId(a)); // e.g. 0
console.log(refId(a)); // same id as above
console.log(refId(b)); // a different id
console.log(refId(globalThis) === refId(globalThis)); // true
Ids are held weakly: when a key is garbage-collected, its id can be reused for a later key. Use this when you need a compact numeric handle for an object or symbol without attaching an own property.
ref-signals
Companion to signals: the same minimal core, plus explicit
subscribe / unsubscribe helpers that bind a callback to both a
WeakMap-compatible key (WeakKey: object or symbol) and a signal.
Associations are tracked by ref-id and cleaned up through a
FinalizationRegistry: when the ref becomes unreachable and is garbage
collected, every callback registered for that ref is deleted from its signal.
There is no reconnect path — once the ref is gone, the subscriptions are gone.
import {
signal,
computed,
batch,
subscribe,
unsubscribe,
} from '@webreflection/utils/ref-signals';
const num = signal(0);
const ref = { value: num.value };
const sync = subscribe(ref, num, () => {
ref.value = num.value;
});
num.value = 1;
// sync ran; ref.value === 1
// drop the association explicitly while the ref is still live
unsubscribe(ref, num, sync);
subscribe(ref, signal, callback) registers callback on signal immediately
and remembers the pair under ref. unsubscribe removes that pair and deletes
the callback from the signal. Prefer this entry over importing
signals alone when subscriptions should die with an arbitrary
object or symbol. For DOM nodes that should also pause and resume with
attachment, use dom-signals instead. For object-shaped reactive
state with property syntax, use state-signals (its own
subscribe / unsubscribe take a property key).
registry
A Map subclass that validates keys and values before storing them. By default,
keys are permanent: setting the same key twice throws a TypeError, and
deleting an existing key also throws so it cannot be re-appended later. Pass
unique: false when replacement and deletion should behave like a regular
Map.
import Registry from '@webreflection/utils/registry';
const registry = new Registry(null, {
key: value => typeof value === 'string',
value: value => typeof value === 'function'
});
registry.set('ready', () => true);
console.log(registry.get('ready')());
// true
Both validators receive the candidate value and should return whether it is
allowed. In TypeScript-aware editors, type-predicate validators also define the
resulting Registry<Key, Value> shape, so key controls the map key type and
value controls the stored value type.
const mutable = new Registry(
[
['answer', 41],
['answer', 42]
],
{
key: value => value === 'answer',
value: value => Number.isInteger(value),
unique: false
}
);
console.log(mutable.get('answer'));
// 42
console.log(mutable.delete('answer'));
// true
Initial iterable entries are validated with the same rules used by set(), so
invalid keys, invalid values, or duplicate keys fail during construction. With
the default unique: true behavior, only missing keys can be passed to
delete() without throwing, in which case it returns false like Map.
set
A native Set subclass with one extra method: put(value). It stores the entry
like add, but returns the value instead of the set reference itself.
import Set from '@webreflection/utils/set';
const set = new Set;
const value = set.put('ready');
console.log(value);
// 'ready'
Use add when chaining on the set is needed; use put when the stored value
should flow directly into the next expression.
shared-array-buffer
This utility provides an unobtrusive SAB (SharedArrayBuffer) shim based on the default ArrayBuffer, with grow(length) and growable additions.
This class can be used to simulate SAB capabilities.
The module exports both SharedArrayBuffer and native. The native boolean indicates whether the returned constructor is the platform implementation or the shim.
signals
A minimalistic, explicit signals implementation. There is no automatic
dependency tracking: every computed and effect takes the list of signals it
depends on. That keeps the runtime small and the dataflow obvious at the call
site.
signal(value) always creates a non-eager signal: writes notify subscribers
only when the new value is not Object.is-equal to the previous one.
Same-value writes are no-ops, so dependents (including computed and
effect) do not re-run. When every write should notify instead — useful for
instrumentation, or whenever identity equality is the wrong gate — construct
an eager signal explicitly with new Signal(value, true).
import {
Signal,
signal,
computed,
batch,
effect,
dispose,
} from '@webreflection/utils/signals';
const a = signal(1);
const b = signal(2);
const c = computed(() => a.value + b.value, [a, b]);
c.value; // 3
a.value = 3;
c.value; // 5
a.value = 3; // Object.is-equal — no notify, c does not recompute
batch(() => {
a.value = 4;
batch(() => {
b.value = 5;
});
});
c.value; // 9 — recomputed once after the outer batch
// eager only via the class — notify on every write, even when unchanged
const ticks = new Signal(0, true);
ticks.value = 0; // still notifies
const stop = effect(() => {
const id = setInterval(() => console.log(c.value), 1000);
return () => clearInterval(id); // runs before the next effect, and on stop()
}, [c]);
a.value = 5; // previous interval cleared, effect runs again
stop(); // unsubscribe and run the last cleanup
c[dispose]();
a[dispose]();
b[dispose]();
API surface:
signal(value)— readable and writable.value; creates a non-eager signal so subscribers run when the new value is notObject.is-equal to the previous one (or once after an outerbatch)new Signal(value, eager)— same.valueAPI; passtruefor an eager signal that notifies on every write. Prefersignal(value)unless you need that behaviorcomputed(fn, signals)/Computed— readonly.value, recomputed when any listed signal notifies; the dependency list is mandatory. A same-value write on a source does not recompute. When a source does notify, dependents of the computed still run even iffn’s result is unchanged (Computeduses an eager parent signal internally — an implementation detail)batch(fn)— coalesces nested updates so dependent work runs once afterwardeffect(fn, signals)— runsfnimmediately and again when any listed signal notifies;fnmay return a cleanup, which runs before the nextfnand again when the returned dispose function unsubscribesdispose— re-exported from@webreflection/utils/patch/dispose; callref[dispose]()to clear subscribers (Signal) or detach from sources (Computed)
Nested batch calls rely on Set.prototype.union. Engines that do not ship it
yet need a one-time import of @webreflection/utils/patch/set-union before
using signals (or dom-signals / ref-signals /
state-signals).
Signal#add / Signal#delete are available when a custom subscriber is needed.
For DOM nodes that should react while attached and drop listeners when removed
(or resume when reinserted), use dom-signals. For raw
add/remove notifications without signal bindings, use dom-observer.
For arbitrary objects or symbols whose subscriptions should end when the key is
collected, use ref-signals. For plain objects whose fields should
read and write like ordinary properties while staying reactive, use
state-signals.
state-signals
Companion to signals: the same minimal core, plus helpers that turn
a plain object into reactive state, and explicit subscribe / unsubscribe
helpers that bind a callback to a state key (like dom-signals
and ref-signals, but keyed by property name instead of a DOM
node or WeakKey).
Each data property becomes a signal-backed accessor — reads return the current
value, writes update the underlying signal — so call sites can use ordinary
property syntax instead of .value.
import {
signal,
computed,
create,
update,
raw,
subscribe,
unsubscribe,
dispose,
Signal,
Computed,
} from '@webreflection/utils/state-signals';
const count = signal(0);
const label = computed(() => `n=${count.value}`, [count]);
const state = create({
count,
label,
name: 'John',
get whole() {
return `${this.name} is ${this.count}`;
},
});
state.whole; // 'John is 0'
state.name; // 'John'
const sync = subscribe(state, 'count', () => {
// react to state.count changes
});
update(state, { count: 1, name: 'Jane' });
state.whole; // 'Jane is 1'
state.label; // 'n=1'
unsubscribe(state, 'count', sync);
raw(state, 'count') instanceof Signal; // true
raw(state, 'whole') instanceof Computed; // true
// tear down signals create() allocated (not ones reused from the input)
state[dispose]();
// or, where Symbol.dispose is available, let `using` dispose at block end:
// {
// using scoped = create({ name: 'temp' });
// scoped.name; // 'temp'
// }
API surface beyond signals:
create(object)— returns aStatewith the same keys (Created<T>in TypeScript). Plain values become new signals; existingSignalorComputedinstances are reused (Computedproperties have no setter). TS unwraps those fields to their.valuetypes, and the result is aStatesoraw/update/subscribe/disposeaccept it without casts. Own getter-only accessors become lazyComputedvalues that depend on every signal already present when first read (typically all data properties). Accessors that define bothgetandsetare left unchangedupdate(state, partial)— assigns the partial ontostateinside onebatch, so dependents run once after all listed keys are writtenraw(state, key)— returns the underlyingSignalorComputedfor that key (touching the key first so lazy getter-only computeds initialize). Use this when you need the signal instance itself (for example wiring a field through dom-signals or ref-signals)subscribe(state, key, callback)— registerscallbackon the underlying signal forkey(viaraw) and returns that callbackunsubscribe(state, key, callback)— removes that callback from the underlying signal forkey; returns whether it was presentdispose— same re-export as signals; callstate[dispose]()to dispose every signal or computed thatcreateallocated for that state (reused inputSignal/Computedinstances are left alone). Because the method is keyed bySymbol.disposewhen the engine provides it, a state can also be disposed automatically via the ECMAScriptusingdeclaration
Prefer this entry when object-shaped state and property syntax are a better fit
than holding individual signal references. Nested batch still needs
Set.prototype.union (or @webreflection/utils/patch/set-union) as with
signals.
sticky
Based on Symbol.for(name), this utility helps modules that might be embedded multiple times across projects avoid conflicts in their internal logic. It preserves the assumption that a module is imported only once per application.
import sticky from '@webreflection/utils/sticky';
// will be created and discarded ASAP
// if embedded multiple times
const computed = new WeakMap;
// module will always point at the very first computed
const [module, known] = sticky(
'@my-project/known-references',
ref => {
// ensure this reference is processed only once in this realm
if (computed.has(ref)) return computed.get(ref);
// compute the value once, then reuse it on future calls
const costlyComputation = somethingNeededOnce(ref);
computed.set(ref, costlyComputation);
return costlyComputation;
},
);
if (known) console.warn('embedded multiple times');
export default module;
Because the sticky logic is intentionally simple, using a “first come, first served” global symbol lookup, avoid storing sensitive values there directly when secrecy or module-level isolation matters.
wat-tag
A libwabt wrapper that returns an async template tag after options
are bound once. Pass features, init, and import (plus optional binary
and name) to the default export; tagged WAT is then dedented after
plain-tag interpolation, parsed, validated, and instantiated.
import watTag from '@webreflection/utils/wat-tag';
const wat = watTag({
import: {
my_namespace: {
imported_func: arg => console.log(arg),
},
},
});
const result = await wat`
(module
(func $i (import "my_namespace" "imported_func") (param i32))
(func (export "exported_func")
i32.const 42
call $i))
`;
result.instance.exports.exported_func();
Each invoke returns the WebAssembly.instantiate result (module and
instance) plus a dispose method that calls WABT’s module.destroy(). Use
using where Symbol.dispose is available, or dispose explicitly:
import dispose from '@webreflection/utils/patch/dispose';
{
using result = await wat`
(module
(func (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add))
`;
result.instance.exports.add(1, 2); // 3
}
// or destroy the parsed module by hand:
const result = await wat`
(module (func (export "nop")))
`;
result.instance.exports.nop();
result[dispose]();
init is forwarded to libwabt when the tag is created. features
go to parseWat / validate, binary to toBinary, import to
WebAssembly.instantiate, and name defaults to 'test.wast'.
weak
A convenience entry that re-exports both weakmap and weakset when both are needed from a single import.
import { WeakMap, WeakSet } from '@webreflection/utils/weak';
const map = new WeakMap;
const set = new WeakSet;
weakmap
A native WeakMap subclass with one extra method: put(key, value). It stores
the entry like set, but returns the value instead of the map reference itself.
Also available as @webreflection/utils/weak-map — the dash is the only
difference; both paths resolve to the same module.
import WeakMap from '@webreflection/utils/weakmap';
// or: import WeakMap from '@webreflection/utils/weak-map';
const map = new WeakMap;
const key = {};
const value = map.put(key, { ready: true });
console.log(value.ready);
// true
Use set when chaining on the map is needed; use put when the stored value
should flow directly into the next expression. Keys must be objects or symbols,
like the native WeakMap.
weakset
A native WeakSet subclass with one extra method: put(value). It stores the
entry like add, but returns the value instead of the set reference itself.
Also available as @webreflection/utils/weak-set — the dash is the only
difference; both paths resolve to the same module.
import WeakSet from '@webreflection/utils/weakset';
// or: import WeakSet from '@webreflection/utils/weak-set';
const set = new WeakSet;
const item = {};
const value = set.put(item);
console.log(value === item);
// true
Use add when chaining on the set is needed; use put when the stored value
should flow directly into the next expression. Values must be objects or
symbols, like the native WeakSet.
with-resolvers
This utility returns a self-bound Promise.withResolvers() implementation that also works on older Android WebView runtimes.
import withResolvers from '@webreflection/utils/with-resolvers';
const { promise, resolve, reject } = withResolvers();
setTimeout(resolve, 0, 42);
export default promise;