Install and requirements
- Node.js ^22.18.0 or >=24.11.0
- npm, Yarn, or pnpm for installation
- Native EventTarget, CustomEvent, AbortController, and AbortSignal.any support
- Native Proxy support
- Native Map support when using Map state
import {Model} from 'white-label-model';
const {Model} = require('white-label-model');
State model
Model accepts a plain object, array, or Map as its root state. Mutations are synchronous: when a mutating call returns, accepted state is already visible through get() and listeners have already run.
Object update() operations are shallow merges. Array and Map update() operations address one existing member. clear() keeps the current root shape.
const profile = new Model({name: 'Ada'});
profile.addEventListener('change', event => console.log(event.detail));
profile.update({name: 'Grace'});
Runtime validation
The validator is optional. It receives the complete proposed state before construction or an explicit set/update/push/delete mutation is committed. Return true to accept and false to reject.
TypeScript checks source code at development time; the validator checks actual runtime values. Direct nested writes through get() are observable but do not invoke the whole-state validator. clear() and destroy() intentionally bypass it so cleanup cannot be blocked.
const user = new Model({name: 'Ada'}, value =>
typeof value === 'object' && value !== null &&
typeof value.name === 'string'
);
user.set({name: 'Grace'}); // true
user.set({name: 42}); // false
Deep observation
Nested objects, arrays, and Maps are proxied lazily as accessed. Direct writes dispatch change and a mutate CustomEvent whose detail contains operation, path, oldValue, newValue, and the complete current state.
Writes using __proto__, constructor, or prototype are rejected to avoid prototype-pollution paths. A proxy retained from a replaced or cleared root can still mutate that detached object, but it no longer publishes Model events for the current root. If a retained proxy becomes stale only because its object moved to a new array index or remains reachable through another live alias, later mutations publish using a current path; truly detached objects remain silent.
model.addEventListener('mutate', event => {
console.log(event.detail.path);
});
model.get().user.preferences.theme = 'dark';
Events and mediator integration
Accepted non-silent explicit mutations dispatch change first, followed by the matching operation CustomEvent: set, update, push, delete, or clear. Payloads are carried in CustomEvent.detail.
Model uses the native EventTarget listener contract for local events. Assign name and an EventTarget-compatible mediator to relay local events as model:<name>:<event>; the original payload is carried in CustomEvent.detail at both boundaries. The package does not import Mediator directly.
Serverless and request-scoped state
Create mutable Model instances per request or invocation unless shared process-wide state is explicitly intended. Serverless hosts can reuse a warm process, so module-level mutable state can leak data between requests.
Keep persistence outside Model. Load data from the application-owned database, cache, API, or storage layer, validate it at the trust boundary when needed, apply it to the request Model, then discard or destroy that request-owned state.
Core API
new Model(data?, validator?)
Create an observable root state container.
| Parameter | Type | Default | Description |
|---|
data | object | unknown[] | Map | — | Initial root state; defaults to {}. |
validator | (state: unknown) => boolean | — | Optional whole-state runtime acceptance check. |
- Returns
- A Model instance.
- Throws / rejects
- TypeError when the initial root shape is unsupported or initial validation fails.
- Runtime
- Browser + Node.js
initialize()
Start the lifecycle for chaining and composition.
- Returns
- The same Model instance.
- Runtime
- Browser + Node.js
addEventListener(type, callback, options?)
Subscribe to Model events with native EventTarget semantics; Model payloads are available through CustomEvent.detail.
| Parameter | Type | Default | Description |
|---|
type | string | — | Model event name. |
callback | EventListener | EventListenerObject | — | Listener invoked synchronously during dispatch. |
options | boolean | AddEventListenerOptions | — | Native capture, once, passive, and signal options. |
- Returns
- undefined, matching EventTarget.
- Runtime
- Browser + Node.js
removeEventListener(type, callback, options?)
Release a Model subscription using EventTarget listener matching semantics.
- Returns
- undefined, matching EventTarget.
- Runtime
- Browser + Node.js
dispatchEvent(event)
Synchronously dispatch an application-supplied Event or CustomEvent through the native EventTarget contract.
| Parameter | Type | Default | Description |
|---|
event | Event | CustomEvent | — | Event to dispatch; put payload data in CustomEvent.detail. |
- Returns
- Native EventTarget cancellation result.
- Runtime
- Browser + Node.js
get() / get(key)
Read the complete observable root or one object property, array index, or Map entry.
| Parameter | Type | Default | Description |
|---|
key | unknown | — | Optional property/index/Map key. |
- Returns
- The root state, one resolved value, or undefined when a keyed lookup does not resolve.
- Runtime
- Browser + Node.js
- The returned root is observable. Direct nested writes are tracked but do not run the whole-state validator.
- Proxies retained from a previous root stop publishing Model events after set(), clear(), or destroy() replaces the current root.
set(data, silent?)
Replace the complete root state with another supported root shape.
| Parameter | Type | Default | Description |
|---|
data | unknown | — | Replacement root state. |
silent | boolean | false | Suppress change/set events when true. |
- Returns
- true when accepted; false for unsupported or validator-rejected data.
- Events / side effects
- change → set unless silent.
- Runtime
- Browser + Node.js
update(partial, silent?) / update(key, value, silent?)
Shallow-merge an object root or replace/shallow-merge one existing array or Map member.
| Parameter | Type | Default | Description |
|---|
partial | object | — | Fields merged into an object root. |
key | unknown | — | Existing array index or Map key. |
value | unknown | — | Replacement or shallow-merge value. |
silent | boolean | false | Suppress dispatched events. |
- Returns
- true when accepted; false for an invalid call, missing member, or validator rejection.
- Events / side effects
- change → update unless silent.
- Runtime
- Browser + Node.js
- Object updates are shallow, not recursive.
- Prototype-pollution keys are blocked from merge input.
push(valueOrValues, silent?) / push(key, value, silent?)
Append array values or insert Map entries.
| Parameter | Type | Default | Description |
|---|
valueOrValues | unknown | unknown[] | — | Value or values appended to an array root. |
key | unknown | — | Map key. |
value | unknown | — | Map value. |
- Returns
- true when accepted; false for invalid calls, object roots, or validator rejection.
- Events / side effects
- change → push unless silent.
- Runtime
- Browser + Node.js
delete(key, silent?)
Delete one object property, array index, or Map entry.
| Parameter | Type | Default | Description |
|---|
key | PropertyKey | number | unknown | — | Member to remove. |
silent | boolean | false | Suppress dispatched events. |
- Returns
- true when removed; false when the member is missing/invalid or validation rejects the resulting state.
- Events / side effects
- change → delete unless silent.
- Runtime
- Browser + Node.js
clear(silent?)
Reset the root to an empty value of the same shape.
- Returns
- true.
- Events / side effects
- change → clear unless silent.
- Runtime
- Browser + Node.js
- Validation is intentionally bypassed so cleanup cannot be blocked.
destroy()
Clear state silently and release Model-owned listeners.
- Returns
- The same Model instance after cleanup.
- Runtime
- Browser + Node.js
TypeScript
Model<T> describes the supported root state and narrows known Model event CustomEvent.detail values.
TypeScript does not validate runtime data; add the optional validator when an external trust boundary needs enforcement.
Security and trust boundaries
Prototype-pollution keys are blocked from direct object writes and safe merge paths.
Validate API, storage, decoded JSON, or user-controlled data before treating it as trusted application state.
Design boundaries
Model does not own rendering, routing, networking, persistence, or application-wide event orchestration.
Resolve asynchronous I/O first, then apply the result with a synchronous Model mutation.