Observable state

white-label-model

Own object, array, or Map state with synchronous change notifications, deep mutation observation, and optional whole-state runtime validation.

Install and requirements

  • Node.js ^22.18.0 or >=24.11.0
  • npm >=11
  • 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.on('change', state => console.log(state));
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 emit change and a mutate event containing operation, path, oldValue, newValue, and the complete current state.

Writes using __proto__, constructor, or prototype are rejected to avoid prototype-pollution paths.

model.on('mutate', mutation => {
    console.log(mutation.path);
});
model.get().user.preferences.theme = 'dark';

Events and mediator integration

Accepted non-silent explicit mutations emit change first, followed by the matching operation event: set, update, push, delete, or clear.

Assign name and an EventEmitter-compatible mediator to relay local events as model:<name>:<event>. The package does not import Mediator directly.

Core API

new Model(data?, validator?)

Create an observable root state container.

ParameterTypeDefaultDescription
dataobject | unknown[] | MapInitial root state; defaults to {}.
validator(state: unknown) => booleanOptional 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

get() / get(key)

Read the complete observable root or one object property, array index, or Map entry.

ParameterTypeDefaultDescription
keyunknownOptional 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.

set(data, silent?)

Replace the complete root state with another supported root shape.

ParameterTypeDefaultDescription
dataunknownReplacement root state.
silentbooleanfalseSuppress 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.

ParameterTypeDefaultDescription
partialobjectFields merged into an object root.
keyunknownExisting array index or Map key.
valueunknownReplacement or shallow-merge value.
silentbooleanfalseSuppress emitted 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.

ParameterTypeDefaultDescription
valueOrValuesunknown | unknown[]Value or values appended to an array root.
keyunknownMap key.
valueunknownMap 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.

ParameterTypeDefaultDescription
keyPropertyKey | number | unknownMember to remove.
silentbooleanfalseSuppress emitted 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 listeners.

Returns
The same Model instance after cleanup.
Runtime
Browser + Node.js

Current compatibility surface

These methods are publicly declared today but are not part of the recommended state-management workflow. They are documented here so the reference matches the shipped API exactly.

serviceGet()

Compatibility placeholder; performs no I/O in the current implementation.

Returns
Promise resolving to {}.
Runtime
Browser + Node.js

servicePatch()

Compatibility placeholder; performs no I/O in the current implementation.

Returns
Promise resolving to {}.
Runtime
Browser + Node.js

servicePost()

Compatibility placeholder; performs no I/O in the current implementation.

Returns
Promise resolving to {}.
Runtime
Browser + Node.js

servicePut()

Compatibility placeholder; performs no I/O in the current implementation.

Returns
Promise resolving to {}.
Runtime
Browser + Node.js

Inherited utility methods

Model currently inherits these public utility methods. Application code normally does not need them for ordinary Model usage.

isMap(value)

Return whether a value is a native Map, including cross-realm Maps.

Returns
boolean.
Runtime
Browser + Node.js

isFinite(value)

Accept finite numeric values without coercion.

Returns
boolean.
Runtime
Browser + Node.js

isPlainObject(value)

Return whether a value is an ordinary or null-prototype object rather than a class instance.

Returns
boolean.
Runtime
Browser + Node.js

pullAt(array, index)

Remove one array member in place.

Returns
The same array.
Runtime
Browser + Node.js

extend(object1, object2)

Create a safe shallow merge while blocking prototype-pollution keys.

Returns
A new merged object.
Runtime
Browser + Node.js

message(messages, data)

Emit local events and optional namespaced mediator events.

Returns
false for falsey data; true after emitting requested events.
Runtime
Browser + Node.js

TypeScript

Model<T> describes the supported root state and provides typed get() root access.

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.