npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@schematize/instance.js

v0.2.10

Published

An extensible javascript library for creating instances

Downloads

641

Readme

Instance.js

An extensible base class for instances.

Features

  • Small - The Instance library is < 4.5KB minified
  • Event-driven architecture - Built-in EventTarget interface with custom events
  • Instance caching - Automatic caching with unique IDs and reference management
  • Proxy support - Optional Proxy-based property interception for "magic" capabilities
  • Collection class - Extended Array with event-driven methods and proxied operations
  • Serialization - JSON serialization with reference preservation
  • Memory management - Proper cleanup and garbage collection support

Installation

npm install @schematize/instance.js
import Instance from '@schematize/instance.js/src/Instance.mjs';
import Collection from '@schematize/instance.js/src/Collection.mjs';
// import utilities, etc.=
import findType from '@schematize/instance.js/src/utils/findType.mjs';
import set from '@schematize/instance.js/src/Instance/set.mjs';
import EventTarget from '@schematize/instance.js/src/EventTarget.mjs';

Instance

To create an instance simply use the new javascript keyword syntax:

let instance = new Instance({});

You can also call Instance without the "new" keywork:

let instance = Instance({});

When initializing a new instance you can pass initial properties to be set on the instance:

let instance = new Instance({
  property1: `value`,
});

__type__

You can also pass the special __type__ property to indicate the "type" of instance this should be (or what constructor function's prototype this instance should inherit from):

class AType {}
let instance = new Instance({
  __type__: AType,
});
instance instanceof AType; // true

This woks for any "constructor function":

function AnotherType () {}
let instance = new Instance({
  __type__: AnotherType,
});
instance instanceof AnotherType; // true

__type__ is a special property that essentially equates to instance.__proto__.constructor (or Object.getPrototypeOf(instance).constructor). It is settable just like any other property, but when changed it will trigger a change in the __proto__ as well.

Whenever you provide a __type__ in the initial properties when calling Instance({ __type__: AType }), internally Instance will ensure that the instance parameter is truely an instanceof the __type__. This happens by using Object.create and in some cases Object.setPrototypeOf.

"extending" Instance

Passing the __type__ as an initial property is fine, but it's not always ideal. Using Instance as a "base" class can be a powerful pattern.

To extend Instance as a base class using ES6 class syntax:

class CoolType extends Instance {
  constructor(properties = {}) {
    properties.__type__ = properties.__type__ || CoolType;
    return super(properties);
  }
}
let instance = new CoolType();
instance instanceof CoolType; // true
instance instanceof Instance; // true

To extend Instance as a base class using ES5 function constructor syntax:

NeatType = function NeatType (properties = {}) {
  properties.__type__ = properties.__type__ || NeatType;
  return Instance(properties, this);
  // or 
  // return Instance.call(this, properties);
};
NeatType.prototype = Object.create(Instance.prototype);
NeatType.prototype.constructor = NeatType;
let instance = new NeatType();
instance instanceof NeatType; // true
instance instanceof Instance; // true

__id__

Every instance is given an automatic __id__ property when created.

let instance = new Instance();
console.log(instance.__id__); // 5nhpc2ufqkfuj588nhjauipc

The value of the __id__ property is up to 128 bytes. It is generated using the sid function (described later) and should be universally unique. The __id__ identifies the instance and can be used for multiple use cases including:

  • Object equality
  • references to instances in cache
  • Serialization and deserialization and maintaining referencial integrity
  • identification in data stores
  • and many more...

You can initialize an instance with an __id__:

let instance = new Instance({
  __id__: `ABC123`,
});

If an instance with that same __id__ has already been instantiated, you will receive back an exact reference to the instance that was previously created. In other words, the two instances will have object equality.

let instance1 = new Instance({
  __id__: `ABC123`,
});
// ...later
let instance2 = new Instance({
  __id__: `ABC123`,
});
instance1 === instance2; // true - object equality

EventTarget

Each instance is build with methods that adhere to the EventTarget Interface. SEE: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget. More specifically Instance.prototype gets the following instance methods:

  • addEventListener (alias "on")
  • removeEventListener (alias "off")
  • dispatchEvent (alias "dispatch" and "trigger")

Instance.prototype is an EventTarget, meaning all of these methods are available on the Instance.prototype object and therefore available to any instance that inherits from Instance.prototype.

EventTarget function

Calling EventTarget directly on an object will set the event target methods on that object.

let obj = {};
EventTarget(obj);
// obj.addEventListener is now defined
// obj.removeEventListener
// obj.dispatEvent
// etc...

Instance.prototype.addEventListener (on)

To add an event listener to an instance, you should use the EventTarget.prototype.addEventListener function. Since Instance.prototype implements EventTarget you can additionally use Instance.prototype.addEventListener.

Parameters

  • name (string): The event name to listen and handle.
  • fn (Function): The "handler" function or "callback" to call when the event occurs.
  • instance (Object): The instance or object on which events would be dispatched.

Usage

If the instance is an "instanceof" Instance, you can call it from the instance method:

let instance = new Instance({
  //...
});
instance.addEventListener(`change`, (ev) => {
  // ev
});
// or
// instance.on(`change`, (ev) => {});

If the object is not an "instanceof" Instance, you can use the prototype method and pass the object as a third parameter (the this parameter):

let obj = {};
Instance.prototype.addEventListener(`change`, (ev) => (
 // ev
), obj);
// or
// Instance.prototype.addEventListener.call(obj, (ev) => {});
// or
// Instance.prototype.on(`change`, (ev) => {}, obj);
// or
// Instance.prototype.on.call(obj, (ev) => {});
// or 
// EventTarget.prototype.addEventListener(`change`, (ev) => {}, obj);
// or 
// EventTarget.prototype.addEventListener.call(obj, (ev) => {});
// or 
// EventTarget.prototype.on(`change`, (ev) => {}, obj);
// or
// EventTarget.prototype.on.call(obj, (ev) => {});

Similarly, you can simply import the on function directly and use it without working through prototype methods:

import on from '@schematize/instance.js/src/EventTarget/on.mjs';
//...
let obj = {};
on('change', (ev) => {}, obj);

Instance.prototype.removeEventListener (off)

To remove an event listener from an instance, you should use the EventTarget.prototype.removeEventListener function. Since Instance.prototype implements EventTarget you can additionally use Instance.prototype.removeEventListener.

Parameters

  • name (string): The event name to stop listening or handling.
  • fn (Function): The "handler" function or "callback" to remove. Object equality is used to determine the function to remove.
  • instance (Object): The instance or object on which holds the "handler" or "callback" function.

Usage

If the instance is an "instanceof" Instance, you can call it from the instance method:

// ...handler function created and addEventListener/on was called previously
let instance = new Instance({
  //...
});
instance.removeEventListener(`change`, handler);
// or
// instance.off(`change`, handler);

If the object is not an "instanceof" Instance, you can use the prototype method and pass the object as a third parameter (the this parameter):

// ...handler function created and addEventListener/on was called previously
let obj = {};
Instance.prototype.removeEventListener(`change`, handler, obj);
// or
// Instance.prototype.removeEventListener.call(obj, handler);
// or
// Instance.prototype.off(`change`, handler, obj);
// or
// Instance.prototype.off.call(obj, handler);
// or 
// EventTarget.prototype.removeEventListener(`change`, handler, obj);
// or 
// EventTarget.prototype.removeEventListener.call(obj, handler);
// or 
// EventTarget.prototype.off(`change`, handler, obj);
// or
// EventTarget.prototype.off.call(obj, handler);

Similarly, you can simply import the off function directly and use it without working through prototype methods:

import off from '@schematize/instance.js/src/EventTarget/off.mjs';
// ...handler function created and addEventListener/on was called previously
let obj = {};
off('change', handler, obj);

Instance.prototype.dispatchEvent (dispatch) (trigger)

To dispatch an event for an instance, you should use the EventTarget.prototype.dispatchEvent function. Since Instance.prototype implements EventTarget you can additionally use Instance.prototype.dispatchEvent.

Parameters

  • evt (Object): The event to dispatch.
    • evt.type (string): The event type or name. This corresponds to the "name" on addEventListener and removeEventListener.
    • evt.detail (Object): Optional. An object with the additional details about the event, context, etc.
    • evt.returnValue (mixed): Optional. A standard for returning something from an event handler back to the code that dispatched the event.
  • instance (Object): The instance or object on which to find event handlers and dispatch the event.

As you can see, the first parameter is the event object which contains a "type" property and a "detail" property. This was modeled after the browser CustomEvent interface. (SEE: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent). However, be aware that you do not have to use CustomEvent for building the evt parameter. While you can use CustomEvent, all that dispatchEvent requires from evt is that it contains a "type" property. In my testing, CustomEvent is extremely performance intensive so I avoid it. Please also not that CustomEvent inherits from the Event interface which has multiple other properties and methods like "cancelable", "bubbles", "currentTarget", "stopPropogation()", "preventDefault()", etc. which are not supported at this point.

On that note, one of the important choices I've made with support for these capabilities relates to peformance. dispatchEvent is the backbone through which message passing and reactivity is achieved in multiple dependent packages. It is the single biggest bottleneck across all platforms. For this reason performance in dispatchEvent is absolutely critical. In order to maintain the best performance possible, I have intentionally kept the dispatchEvent function as lean as possible and used as many tricks as possible to squeeze out performance. This means sometimes not supporting some features or functionality.

Catpure/Bubbling

Browsers implement the W3C specification dispatching events. (SEE: https://www.quirksmode.org/js/events_order.html). When an event is dispatched, it first goes from the root element "window" and/or "document" and makes it's way to the top most event target. This is called the "capture" phase. Then after dispatching on the "target" element, it makes it's way back down to the "document". This is called the bubbling phase.

For dispatching events on instances, I have made the choice to support a slightly different model, but one that gives you just as much flexibility while also ensuring that dispatchEvent is performant.

First, let's establish what "capturing" or "bubbling" would look like in an "instance" world. Mainly what it looks like is looking through the instance's prototype chain to deteremine whether to capture all events for a particular instance, or it's __type__ or all Instances. Each of these can be valuable.

Traversing prototype chains for every instance even when there may not be listeners attached to that prototype in the chain can get expensive when it comes to performance. The less we have to loop the better.

__dispatch__

For this reason in order to decide what order (bubble, capture, other) and which prototypes in the chain on which to dispatch the events (instance, instance.__proto__, Instance.prototype, etc.), you have the __dispatch__ special property. This property should be an array.

By default Instance.prototype.__dispatch__ is set to [ Instance.prototype ]. This means that by default the event will be dispatched on any listeners on the instance as well as the Instance.prototype. For example, if you fire a "change" event on the instance, any "change" listeners on instance would be handled as well as any "change" listener that were added to the Instance.prototype object. This means you can listen to events for ALL instances of Instance.prototype by attaching listeners to Instance.prototype:

Instance.prototype.on(`change`, (ev) => {
  // fires for all "change" events across all instances that have Instance.prototype in their prototype chain...
});

In order to switch around the order and which prototypes you want to dispatch, you can overwrite the Instance.__dispatch__ array or hide the __dispatch__ instance by defining a property on Instance.__dispatch__ or any other prototype in the prototype chain. You can also define a getter/setter property the dynamically adjusts based on the instance or that simply gets the full prototype chain.

Usage

If the instance is an "instanceof" Instance, you can call it from the instance method:

// ...handler function created and addEventListener/on was called previously
let instance = new Instance({
  //...
});
instance.dispatchEvent({
  type: `change`,
  detail: {
    instance: instance,
    property: `property1`,
    // ...
  },
});
// or
// instance.dispatch({...});
// or
// instance.trigger({...});

If the object is not an "instanceof" Instance, you can use the prototype method and pass the object as a third parameter (the this parameter):

// ...handler function created and addEventListener/on was called previously
let obj = {};
Instance.prototype.dispatchEvent({
  type: `change`,
  detail: {
    instance: instance,
    property: `property1`,
    // ...
  },
}, obj);
// or
// Instance.prototype.dispatchEvent.call(obj, handler);
// or
// Instance.prototype.dispatch(`change`, handler, obj);
// or
// Instance.prototype.dispatch.call(obj, handler);
// or
// Instance.prototype.trigger(`change`, handler, obj);
// or
// Instance.prototype.trigger.call(obj, handler);
// or 
// EventTarget.prototype.dispatchEvent(`change`, handler, obj);
// or 
// EventTarget.prototype.dispatchEvent.call(obj, handler);
// or 
// EventTarget.prototype.dispatch(`change`, handler, obj);
// or
// EventTarget.prototype.dispatch.call(obj, handler);
// or 
// EventTarget.prototype.trigger(`change`, handler, obj);
// or
// EventTarget.prototype.trigger.call(obj, handler);

Similarly, you can simply import the dispatch function directly and use it without working through prototype methods:

import dispatch from '@schematize/instance.js/src/EventTarget/dispatch.mjs';
// ...handler function created and addEventListener/on was called previously
let obj = {};
dispatch({
  type: `change`,
  detail: {
    instance: instance,
    property: `property1`,
    // ...
  },
}, obj);

Instance events

With that in mind, let's go back and talk about what events are fired when you create a new Instance or call the Instance function constructor as a result of constructing an instance of a Type that extends Instance in some way.

Events

"beforeInstance" event

The beforeInstance event is fired at the very beginning of the Instance constructor, before any instance creation logic occurs. This allows listeners to intercept and potentially modify the instance creation process.

Event Detail Properties:

  • properties - The properties object passed to the Instance constructor
  • instance - The cached instance if one was found by __id__, otherwise undefined
  • this - The this context passed to the Instance constructor

Return Value:

  • returnValue.instance - Can be set to provide a custom instance to use instead of creating a new one. This is especially helpful when wanting to implement your own cache or instance provider.
  • returnValue.__type__ - Can be set to override the type determination logic
  • returnValue.initialized - Can be set to true to skip the rest of the initialization process
  • returnValue.noAssign - Can be set to true to prevent property assignment
Instance.prototype.on(`beforeInstance`, (evt) => {
  const { properties, instance } = evt.detail;
  
  // Custom logic to modify instance creation
  if (properties?.customType) {
    evt.returnValue.__type__ = CustomClass;
  }
  
  // Skip initialization if already handled
  if (instance && instance.fullyInitialized) {
    evt.returnValue.initialized = true;
  }
});
"instance" event

The instance event is fired after the instance has been created, cached, and all internal properties (__type__, __id__) have been set, but before properties are assigned.

Event Detail Properties:

  • __type__ - The type/constructor that was used to create the instance
  • properties - The properties object passed to the Instance constructor
  • instance - The newly created instance
Instance.prototype.on(`instance`, (evt) => {
  const { __type__, instance, properties } = evt.detail;
  
  // Perform any post-creation setup
  if (__type__ === CustomClass) {
    instance.setupCustomFeatures();
  }
});
"change" event

The change event is fired whenever properties on an instance are modified. This includes special handling for __type__ and __id__ properties.

Event Detail Properties:

  • instance - The instance that was modified
  • changes - A Map containing the property changes with from and to values
Instance.prototype.on(`change`, (evt) => {
  const { instance, changes } = evt.detail;
  
  for (const [propertyName, change] of changes) {
    console.log(`${propertyName} changed from ${change.from} to ${change.to}`);
    
    // Special handling for __type__ changes
    if (propertyName === `__type__`) {
      // Instance prototype chain is automatically updated
    }
    
    // Special handling for __id__ changes  
    if (propertyName === `__id__`) {
      // Cache and backrefs are automatically updated
    }
  }
});

Instance.prototype methods

Instance.prototype.get

To get a value and also trigger any listeners who might want to modify the value returned from a "get", you should use the Instance.prototype.get function.

Parameters

  • name (string): The property name.
  • instance (Object): The instance on which to get the value of the property.

Usage

If the instance is an "instanceof" Instance, you can call it from the instance method:

let instance = new Instance({
  //...
});
instance.get(`property`);

If the object is not an "instanceof" Instance, you can use the prototype method and pass the object as a second parameter (the this parameter):

let obj = {};
Instance.prototype.get(`property`, obj);
// or 
// Instance.prototype.get.call(obj, `property`);

Similarly, you can simply import the get function directly and use it without working through the Instance.prototype method:

import get from '@schematize/instance.js/src/Instance/get.mjs';
//...
let obj = {};
get('property', obj);

Events

"get" event

The get function fires a "get" event before returning the property value. This allows listeners to intercept and potentially modify what gets returned.

Event Detail Properties:

  • __type__ - The constructor function of the instance
  • instance - The instance being accessed
  • property - The property name being retrieved

Return Value:

Listeners can set event.returnValue to override what gets returned from the get function. If no returnValue is set, the function returns the actual property value from the instance.

instance.on('get', (event) => {
  if (event.detail.property === 'sensitive') {
    event.returnValue = '[REDACTED]';
  }
});

let value = instance.get('sensitive'); // Returns '[REDACTED]' instead of actual value

Instance.prototype.set

To set a value and also trigger any listeners who might want to know when a value was changed on an instance, you should use the Instance.prototype.set function.

Parameters

  • name (string): The property name.
  • value (mixed): The value to set.
  • instance (Object): The instance on which to set the value of the property.

Usage

If the instance is an "instanceof" Instance, you can call it from the instance method:

let instance = new Instance({
  //...
});
instance.set(`property`, `value`);

If the object is not an "instanceof" Instance, you can use the prototype method and pass the object as the third parameter (the this parameter):

let obj = {};
Instance.prototype.set(`property`, `value`, obj);
// or 
// Instance.prototype.set.call(obj, `property`, `value`);

Similarly, you can simply import the set function directly and use it without working through the Instance.prototype method:

import set from '@schematize/instance.js/src/Instance/set.mjs';
//...
let obj = {};
set(`property`, obj, `value`);

Events

"set" event

The set function fires a "set" event before setting the property value. This allows listeners to intercept and potentially modify the value being set.

Event Detail Properties:

  • __type__ - The constructor function of the instance
  • instance - The instance being modified
  • property - The property name being set
  • previousValue - The previous value of the property
  • value - The new value being set

Return Value: Listeners can set event.returnValue.value to override what value gets set on the property. If no returnValue.value is set, the function uses the original value.

instance.on('set', (event) => {
  if (event.detail.property === 'password') {
    event.returnValue = { value: '[ENCRYPTED]' };
  }
});

instance.set('password', 'plaintext'); // Actually sets '[ENCRYPTED]'
"modify" event

The set function fires a "modify" event after the property has been set. This allows listeners to react to completed property changes.

Event Detail Properties:

  • __type__ - The constructor function of the instance
  • instance - The instance that was modified
  • property - The property name that was set
  • previousValue - The previous value of the property
  • value - The new value that was set
"change" event

The set function fires a "change" event when the property value actually changes (previousValue !== value). This provides a higher-level notification of changes.

Event Detail Properties:

  • __type__ - The constructor function of the instance
  • instance - The instance that was modified
  • changes - A Map containing the property changes
  • added - Array of new values added
  • removed - Array of previous values removed

Instance.prototype.deleteProperty

To delete a property and notify any listeners who might want to know when a property was deleted from an instance, you should use the Instance.prototype.deleteProperty function.

Parameters

  • name (string): The property name.
  • instance (Object): The instance on which to delete the property.

Usage

If the instance is an "instanceof" Instance, you can call it from the instance method:

let instance = new Instance({
  //...
});
instance.deleteProperty(`property`);

If the object is not an "instanceof" Instance, you can use the prototype method and pass the object as the second parameter (the this parameter):

let obj = {};
Instance.prototype.deleteProperty(`property`, obj);
// or 
// Instance.prototype.deleteProperty.call(obj, `property`);

Similarly, you can simply import the deleteProperty function directly and use it without working through the Instance.prototype method:

import deleteProperty from '@schematize/instance.js/src/Instance/deleteProperty.mjs';
//...
let obj = {};
deleteProperty(`property`, obj);

Events

"deleteProperty" event

The deleteProperty function fires a "deleteProperty" event before deleting the property. This allows listeners to intercept and potentially modify the deletion behavior.

Event Detail Properties:

  • __type__ - The constructor function of the instance
  • instance - The instance being modified
  • property - The property name being deleted
  • previousValue - The value that will be deleted

Return Value: Listeners can set event.returnValue.instance to change which instance the property is deleted from, or event.returnValue.property to change which property gets deleted.

instance.on('deleteProperty', (event) => {
  if (event.detail.property === 'important') {
    event.returnValue = { property: 'backup_' + event.detail.property };
  }
});

instance.deleteProperty('important'); // Actually deletes 'backup_important'
"modify" event

The deleteProperty function fires a "modify" event after the property has been deleted. This allows listeners to react to completed property deletions.

Event Detail Properties:

  • __type__ - The constructor function of the instance
  • instance - The instance that was modified
  • property - The property name that was deleted
  • previousValue - The value that was deleted
"change" event

The deleteProperty function fires a "change" event after the property deletion. This provides a higher-level notification of the change.

Event Detail Properties:

  • __type__ - The constructor function of the instance
  • instance - The instance that was modified
  • changes - A Map containing the property changes
  • added - Array of new values added (empty for deletions)
  • removed - Array of previous values removed

Instance.prototype.defineProperty

To define a property with a descriptor and trigger event listeners, you should use the Instance.prototype.defineProperty function. This is similar to Object.defineProperty but with event support.

Parameters

  • name (string): The property name to define.
  • descriptor (Object): The property descriptor object (see Object.defineProperty).
  • instance (Object): The instance on which to define the property.

Usage

If the instance is an "instanceof" Instance, you can call it from the instance method:

let instance = new Instance({
  //...
});
instance.defineProperty('computed', {
  get() { return this.value * 2; },
  enumerable: true
});

If the object is not an "instanceof" Instance, you can use the prototype method and pass the object as the third parameter (the this parameter):

let obj = {};
Instance.prototype.defineProperty('computed', {
  get() { return this.value * 2; },
  enumerable: true
}, obj);
// or 
// Instance.prototype.defineProperty.call(obj, 'computed', descriptor);

Similarly, you can simply import the defineProperty function directly and use it without working through the Instance.prototype method:

import defineProperty from '@schematize/instance.js/src/Instance/defineProperty.mjs';
//...
let obj = {};
defineProperty('computed', descriptor, obj);

Events

"defineProperty" event

The defineProperty function fires a "defineProperty" event before defining the property. This allows listeners to intercept and potentially modify the property definition.

Event Detail Properties:

  • __type__ - The constructor function of the instance
  • instance - The instance being modified
  • property - The property name being defined
  • descriptor - The property descriptor object

Return Value: Listeners can set event.returnValue.instance to change which instance the property is defined on, event.returnValue.property to change which property gets defined, or event.returnValue.descriptor to modify the property descriptor.

instance.on('defineProperty', (event) => {
  if (event.detail.property === 'sensitive') {
    event.returnValue = { 
      descriptor: { 
        get() { return '[PROTECTED]'; },
        enumerable: false 
      } 
    };
  }
});

instance.defineProperty('sensitive', { value: 'secret' }); // Actually creates a getter that returns '[PROTECTED]'
"modify" event

The defineProperty function fires a "modify" event after the property has been defined. This allows listeners to react to completed property definitions.

Event Detail Properties:

  • __type__ - The constructor function of the instance
  • instance - The instance that was modified
  • property - The property name that was defined
  • descriptor - The property descriptor that was applied
  • previousValue - The previous value of the property
  • value - The new value of the property
"change" event

The defineProperty function fires a "change" event when the property value actually changes (previousValue !== value). This provides a higher-level notification of changes.

Event Detail Properties:

  • __type__ - The constructor function of the instance
  • instance - The instance that was modified
  • changes - A Map containing the property changes
  • added - Array of new values added
  • removed - Array of previous values removed

Instance.prototype.assign

To assign multiple properties at once and trigger event listeners, you should use the Instance.prototype.assign function. This is similar to Object.assign but with event support and the ability to skip individual properties.

Parameters

  • properties (Object): The properties object to assign to the instance.
  • instance (Object): The instance on which to assign the properties.
  • options (Object): Optional configuration object.
    • options.s (Object): Properties to skip during assignment (skipped properties).
    • options.c (boolean): Skip the "change" event (skipChange).

Usage

If the instance is an "instanceof" Instance, you can call it from the instance method:

let instance = new Instance({
  //...
});
instance.assign({
  name: 'John',
  age: 30,
  city: 'New York'
});

If the object is not an "instanceof" Instance, you can use the prototype method and pass the object as the second parameter (the this parameter):

let obj = {};
Instance.prototype.assign({
  name: 'Jane',
  age: 25
}, obj);
// or 
// Instance.prototype.assign.call(obj, properties);

Similarly, you can simply import the assign function directly and use it without working through the Instance.prototype method:

import assign from '@schematize/instance.js/src/Instance/assign.mjs';
//...
let obj = {};
assign(properties, obj);

Options

You can skip specific properties during assignment:

instance.assign({
  name: 'John',
  age: 30,
  __type__: SomeOtherType  // This will be skipped
}, {
  s: { __type__: 1 }  // Skip the __type__ property
});

You can also skip the "change" event:

instance.assign(properties, instance, { c: 1 }); // Skip change event

Events

"change" event

The assign function fires a single "change" event after all properties have been assigned (if any values actually changed). This provides a higher-level notification of multiple changes at once.

Note: Individual "set", "modify" events are still fired for each property during assignment, but only one "change" event is fired at the end if any values changed.

Event Detail Properties:

  • __type__ - The constructor function of the instance
  • instance - The instance that was modified
  • changes - A Map containing all the property changes
  • added - Array of new values added (empty for assign)
  • removed - Array of previous values removed (empty for assign)
instance.on('change', (event) => {
  console.log('Multiple properties changed:', event.detail.changes);
  // Logs: Map { 'name' => { to: 'John', from: 'Jane' }, 'age' => { to: 30, from: 25 } }
});

instance.assign({
  name: 'John',
  age: 30
});

Instance.prototype.destroy

To properly destroy an instance and clean up memory, you should use the Instance.prototype.destroy function. This removes all properties from the instance and allows the garbage collector to free memory.

Note: The destroy function removes all properties from the instance, including __type__ and __id__. The instance object itself remains but is completely empty. This in combination with event listeners can remove references that keep the instance from being garbage collected.

Note: Internally Instance will listen to events fired out of this function and remove the Instance from Cache's and other Backref lists.

Parameters

  • instance (Object): The instance to destroy.

Usage

If the instance is an "instanceof" Instance, you can call it from the instance method:

let instance = new Instance({
  name: 'John',
  age: 30
});
instance.destroy();
// All properties are now removed from the instance

If the object is not an "instanceof" Instance, you can use the prototype method and pass the object as the first parameter (the this parameter):

let obj = { name: 'Jane', age: 25 };
Instance.prototype.destroy(obj);
// or 
// Instance.prototype.destroy.call(obj);

Similarly, you can simply import the destroy function directly and use it without working through the Instance.prototype method:

import destroy from '@schematize/instance.js/src/Instance/destroy.mjs';
//...
let obj = { name: 'Jane', age: 25 };
destroy(obj);

Events

"destroy" event

The destroy function fires a "destroy" event before destroying the instance. This allows listeners to perform cleanup operations before the instance is destroyed.

Event Detail Properties:

  • __type__ - The constructor function of the instance
  • instance - The instance being destroyed
"change" event

The destroy function fires a "change" event after all properties have been removed. This provides a higher-level notification of the destruction.

Event Detail Properties:

  • __type__ - The constructor function of the instance
  • instance - The instance that was destroyed
  • changes - A Map containing all the property changes (all properties removed)
  • added - Array of new values added (empty for destroy)
  • removed - Array of all previous values that were removed
instance.on('destroy', (event) => {
  console.log('Instance is being destroyed:', event.detail.instance);
  // Perform cleanup operations here
});

instance.on('change', (event) => {
  console.log('All properties removed:', event.detail.removed);
  // Logs: ['John', 30, 'someId', Instance]
});

instance.destroy();

Instance Cache

The Instance system maintains a global cache of all instances using their __id__ as the key. This cache is accessible through Instance.C and serves several important purposes:

  • Prevents duplicate instances: If you try to create an instance with an existing __id__, the cached instance is returned instead of creating a new one
  • Enables reference resolution: When deserializing objects with __ref__ properties (described later), the cache allows the system to resolve these references to actual instances
  • Performance optimization: Avoids recreating expensive objects that already exist
// Access the cache directly
console.log(Instance.C); // Shows all cached instances by __id__

// Check if an instance exists in cache
const existingInstance = Instance.C['some-id'];
if (existingInstance) {
  console.log('Instance already exists:', existingInstance);
}

// Create an instance with a specific __id__ to retrieve from cache
const instance1 = Instance({ __id__: 'my-unique-id', name: 'First' });
const instance2 = Instance({ __id__: 'my-unique-id', name: 'Second' }); // Returns instance1, not a new instance

console.log(instance1 === instance2); // true
console.log(instance1.name); // 'First' (not 'Second')

The cache is automatically managed - instances are added when created and can be removed when destroyed (though the destroy cleanup is currently commented out in the code).

Backrefs

The backref system handles deferred reference resolution when instances are not yet available in the cache. This is particularly useful during deserialization or when instances are loaded asynchronously.

How Backrefs Work

When you set a property to an object with a __ref__ property (instead of __id__), the system:

  1. Checks the cache first: If an instance with that __id__ exists in Instance.C, it immediately replaces the __ref__ object with the actual instance
  2. Creates a backref if not found: If the instance doesn't exist yet, it creates a backref entry in Instance.R that will be resolved later
  3. Resolves backrefs when instance appears: When an instance with the matching __id__ is eventually created or loaded, all backrefs are automatically resolved

Accessing the Backref Cache

The global backref cache is accessible through Instance.R:

// Access the backref cache directly
console.log(Instance.R); // Shows all pending backrefs by __id__

// Check if there are pending backrefs for an ID
const pendingBackrefs = Instance.R['some-id'];
if (pendingBackrefs) {
  console.log('Pending backrefs:', pendingBackrefs);
  // Each backref is [object, propertyName] that needs to be resolved
}

Using ref for Deferred References

// Create a reference object with __ref__ instead of __id__
const userRef = { __ref__: 'user-123' };

// Set it as a property - this creates a backref if user-123 doesn't exist yet
instance.set('owner', userRef);

// Later, when the actual user instance is created/loaded
const actualUser = Instance({ __id__: 'user-123', name: 'John' });

// The backref is automatically resolved - instance.owner now points to actualUser
console.log(instance.owner === actualUser); // true
console.log(instance.owner.name); // 'John'

Backref Resolution During id Changes

When an instance's __id__ changes, the system automatically:

  1. Removes old cache entries: Deletes the instance from the old __id__ in both Instance.C and Instance.R
  2. Resolves pending backrefs: If there were backrefs waiting for the new __id__, they are immediately resolved
  3. Updates cache: Adds the instance to the new __id__ in Instance.C
// Change an instance's __id__
instance.set('__id__', 'new-id');

// Any objects with { __ref__: 'new-id' } will now be resolved to this instance
// The old 'old-id' cache entries are cleaned up

This system ensures that references are maintained correctly even when instances are loaded out of order or when IDs change.

Proxy

When using Instance.js you can optionally wrap instances in JavaScript Proxies to provide transparent property access with automatic event dispatching. This creates a "magic" experience where you work with instances using normal object syntax, but all operations trigger events and can be intercepted.

Enabling Proxy Support

Proxy support is disabled by default for performance reasons. To enable it, set the global variable before loading the library:

// Enable proxy support before importing the library
globalThis.__proxy__ = true;

// Now import and use the library
import Instance from '@schematize/instance.js';

// All instances will now be wrapped in proxies
const instance = Instance({ name: 'John' });

The "Magic" Experience

With proxies enabled, you can use normal object syntax and everything "just works":

// Normal property access - triggers "get" events
console.log(instance.name); // 'John'

// Normal property assignment - triggers "set" events  
instance.name = 'Jane';

// Normal property deletion - triggers "deleteProperty" events
delete instance.age;

// Normal property definition - triggers "defineProperty" events
Object.defineProperty(instance, 'id', { value: 123 });

Under the hood, these operations automatically dispatch events and can be intercepted, allowing for:

  • Change detection: Know exactly when and how properties change
  • Behavior modification: Intercept and modify property operations like get, set, deleteProperty, etc.
  • Validation: Add custom validation logic
  • Logging: Track all property access and modifications

Proxy Target References

When an instance is wrapped in a proxy, two special properties are created:

  • __proxyTarget__: References the original instance (the proxy target)
  • __proxy__: References the proxy wrapper
const instance = Instance({ name: 'John' });

// Access the original instance (bypasses proxy traps)
const original = instance.__proxyTarget__;
original.name = 'Jane'; // No events fired

// Access the proxy (triggers events)
const proxy = instance.__proxy__;
proxy.name = 'Bob'; // Fires "set" event

// Object equality should use the proxy
console.log(instance === instance.__proxy__); // true
console.log(instance === instance.__proxyTarget__); // false

When to Use Proxy vs Direct Access

Use the proxy (normal property access) when you want:

  • Event dispatching
  • Change detection
  • Behavior interception
  • Normal application logic

Use __proxyTarget__ when you need:

  • Performance-critical operations
  • Bypassing event dispatching
  • Direct property manipulation
  • Internal library operations

Performance Considerations

Proxies add overhead to every property access, assignment, and deletion. This is why they're disabled by default (globalThis.__proxy__ is undefined by default):

The performance impact is most noticeable in:

  • High-frequency property access
  • Large-scale data processing
  • Performance-critical applications

You must make the judgement call based on your coding style and your specific needs.

Collection

Collection extends the primitive JavaScript Array with event-driven functionality. It provides all standard Array methods plus custom methods for array manipulation, all with event support.

Creating Collections

import Collection from '@schematize/instance.js/src/Collection.mjs';

Create empty collection:

let collection = new Collection();

Without new keyword:

let collection = Collection();

Create with initial data:

let collection2 = new Collection(['item1', 'item2']);

// Create empty collection with properties:

let collection3 = new Collection([], { name: 'My Collection' });

Collection EventTarget

One of the biggest goals of the "Collection" constructor is to essentially make an "observable" array. In order to achieve this, Collections must be able to dispatch events when items in the array are modified or after an operation occurs.

To achieve this, Collection also implements the EventTarget Interface, and utilizes the same conventions like __dispatch__, etc. For more information about how EventTarget works, please see EventTarget above. By default Collection.prototype.__dispatch__ is set to [ Collection.prototype ] which means you can listen to events for ALL instances of Collection.prototype by attaching listeners to Collection.prototype:

Collection.prototype.on(`change`, (ev) => {
  // fires for all "change" events across all Collection instances...
});

Standard Array Methods

Because Collection inherits from Array, it has all of the same native methods as Array. collection.forEach, collection.map, etc. SEE: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array.

Overwritten Array Methods

Arrays natively (as of this writing) only have 9 methods that modify the array in place (or mutate the array rather than simply return a copy of the array). In order to preserve backwards compatibility with these methods while still dispatching events when these arrays change, I have overwritten these mutuating methods so that they dispatch the "change" event after performing the operation. These are the methods that are overwritten:

  • push
  • fill
  • pop
  • shift
  • unshift
  • splice
  • reverse
  • sort
  • copyWithin
collection.push(`new item`);    // Fires `change` event
collection.fill(1, 2, `item`);  // Fires `change` event
collection.pop();               // Fires `change` event
collection.shift();             // Fires `change` event
collection.unshift(`item`);     // Fires `change` event
collection.splice(0, 1, `new`); // Fires `change` event
collection.reverse();           // Fires `change` event
collection.sort();              // Fires `change` event
collection.copyWithin(0, 1, 2); // Fires `change` event

Custom Collection Methods

In order to precisely control when and how collections are updated and be able to interject in that behavior, I've introduce equivilent methods that fire even more events and capture more metadata about what operation has taken place.

Collection.prototype.append

Append items to the end of the collection. This is equivalent to Array.prototype.push but with event support.

Usage

If an "instanceof" Collection, you can call the method directly:

let collection = new Collection([]);
collection.append(`item`);

If not not an "instanceof" Collection, you can use the prototype method. Unlike the instance methods, you must call the function with .call or .apply in order to properly set the this value to the array-like object. This is because many of the Collection methods have "rest" parameters:

let arr = [];
Collection.prototype.append.call(arr, `item`);

As with every method, you can simply import the function and use it directly (again making sure to call it with .call or .apply):

import append from '@schematize/instance.js/src/Collection/append.mjs';
//...
let arr = [];
append.call(arr, 'item');

Parameters

  • ...items (any): Items to append to the collection.

Returns

  • number: The new length of the collection.

Events

Note: During append operations, individual "set" and "modify" events may fire for each index being set and for the "length" property. A single "change" event fires after the complete operation.

"change" event

The append function fires a "change" event after items have been appended.

Event Detail Properties:

  • instance - The collection that was modified
  • changes - A Map containing the property changes
  • added - Array of items that were added
  • removed - Array of items that were removed (empty for append)
collection.append('item1', 'item2'); // Returns new length

Collection.prototype.prepend

Prepend items to the beginning of the collection. This is equivalent to Array.prototype.unshift but with event support.

Usage

If an "instanceof" Collection, you can call the method directly:

let collection = new Collection([]);
collection.prepend(`item`);

If not not an "instanceof" Collection, you can use the prototype method. Unlike the instance methods, you must call the function with .call or .apply in order to properly set the this value to the array-like object. This is because many of the Collection methods have "rest" parameters:

let arr = [];
Collection.prototype.prepend.call(arr, `item`);

As with every method, you can simply import the function and use it directly (again making sure to call it with .call or .apply):

import prepend from '@schematize/instance.js/src/Collection/prepend.mjs';
//...
let arr = [];
prepend.call(arr, 'item');

Parameters

  • ...items (any): Items to prepend to the collection.

Returns

  • number: The new length of the collection.

Events

Note: During prepend operations, individual "set", "modify", and "deleteProperty" events may fire for each index being modified and for the "length" property. A single "change" event fires after the complete operation.

"change" event

The prepend function fires a "change" event after items have been prepended.

Event Detail Properties:

  • instance - The collection that was modified
  • changes - A Map containing the property changes
  • added - Array of items that were added
  • removed - Array of items that were removed (empty for prepend)
collection.prepend('item1', 'item2'); // Returns new length

Collection.prototype.removeLast

Remove the last item from the collection. This is equivalent to Array.prototype.pop but with event support.

Usage

If an "instanceof" Collection, you can call the method directly:

let collection = new Collection([]);
collection.removeLast();

If not not an "instanceof" Collection, you can use the prototype method. Unlike the instance methods, you must call the function with .call or .apply in order to properly set the this value to the array-like object. This is because many of the Collection methods have "rest" parameters:

let arr = [];
Collection.prototype.removeLast.call(arr);

As with every method, you can simply import the function and use it directly (again making sure to call it with .call or .apply):

import removeLast from '@schematize/instance.js/src/Collection/removeLast.mjs';
//...
let arr = [];
removeLast.call(arr);

Parameters

None.

Returns

  • any: The removed item, or undefined if the collection was empty.

Events

Note: During removeLast operations, individual "deleteProperty" and "modify" events may fire for the index being removed and for the "length" property. A single "change" event fires after the complete operation.

"change" event

The removeLast function fires a "change" event after the last item has been removed.

Event Detail Properties:

  • instance - The collection that was modified
  • changes - A Map containing the property changes
  • added - Array of items that were added (empty for removeLast)
  • removed - Array containing the removed item
let item = collection.removeLast(); // Returns removed item

Collection.prototype.removeFirst

Remove the first item from the collection. This is equivalent to Array.prototype.shift but with event support.

Usage

If an "instanceof" Collection, you can call the method directly:

let collection = new Collection([]);
collection.removeFirst();

If not not an "instanceof" Collection, you can use the prototype method. Unlike the instance methods, you must call the function with .call or .apply in order to properly set the this value to the array-like object. This is because many of the Collection methods have "rest" parameters:

let arr = [];
Collection.prototype.removeFirst.call(arr);

As with every method, you can simply import the function and use it directly (again making sure to call it with .call or .apply):

import removeFirst from '@schematize/instance.js/src/Collection/removeFirst.mjs';
//...
let arr = [];
removeFirst.call(arr);

Parameters

None.

Returns

  • any: The removed item, or undefined if the collection was empty.

Events

Note: During removeFirst operations, individual "set", "modify", and "deleteProperty" events may fire for each index being shifted and for the "length" property. A single "change" event fires after the complete operation.

"change" event

The removeFirst function fires a "change" event after the first item has been removed.

Event Detail Properties:

  • instance - The collection that was modified
  • changes - A Map containing the property changes
  • added - Array of items that were added (empty for removeFirst)
  • removed - Array containing the removed item
let item = collection.removeFirst(); // Returns removed item

Collection.prototype.replace

Replace items in the collection. This is equivalent to Array.prototype.splice but with event support.

Usage

If an "instanceof" Collection, you can call the method directly:

let collection = new Collection([]);
collection.replace(0, 1, `newItem`);

If not not an "instanceof" Collection, you can use the prototype method. Unlike the instance methods, you must call the function with .call or .apply in order to properly set the this value to the array-like object. This is because many of the Collection methods have "rest" parameters:

let arr = [];
Collection.prototype.replace.call(arr, 0, 1, `newItem`);

As with every method, you can simply import the function and use it directly (again making sure to call it with .call or .apply):

import replace from '@schematize/instance.js/src/Collection/replace.mjs';
//...
let arr = [];
replace.call(arr, 0, 1, 'newItem');

Parameters

  • start (number): The index at which to start replacing.
  • deleteCount (number): The number of items to remove.
  • ...items (any): Items to insert.

Returns

  • Array: An array containing the removed items.

Events

Note: During replace operations, individual "set", "modify", and "deleteProperty" events may fire for each index being modified and for the "length" property. A single "change" event fires after the complete operation.

"change" event

The replace function fires a "change" event after items have been replaced.

Event Detail Properties:

  • instance - The collection that was modified
  • changes - A Map containing the property changes
  • added - Array of items that were added
  • removed - Array of items that were removed
let removed = collection.replace(0, 2, 'new1', 'new2'); // Returns removed items

Collection.prototype.flip

Reverse the order of items in the collection. This is equivalent to Array.prototype.reverse but with event support.

Usage

If an "instanceof" Collection, you can call the method directly:

let collection = new Collection([]);
collection.flip();

If not not an "instanceof" Collection, you can use the prototype method. Unlike the instance methods, you must call the function with .call or .apply in order to properly set the this value to the array-like object. This is because many of the Collection methods have "rest" parameters:

let arr = [];
Collection.prototype.flip.call(arr);

As with every method, you can simply import the function and use it directly (again making sure to call it with .call or .apply):

import flip from '@schematize/instance.js/src/Collection/flip.mjs';
//...
let arr = [];
flip.call(arr);

Parameters

None.

Returns

  • Collection: The collection itself (for chaining).

Events

Note: During flip operations, individual "set" and "modify" events may fire for each index being swapped. A single "change" event fires after the complete operation.

"change" event

The flip function fires a "change" event after the collection has been reversed.

Event Detail Properties:

  • instance - The collection that was modified
  • changes - A Map containing the property changes
  • added - Array of items that were added (empty for flip)
  • removed - Array of items that were removed (empty for flip)
collection.flip(); // Returns the collection

Collection.prototype.order

Sort the items in the collection. This is equivalent to Array.prototype.sort but with event support.

Usage

If an "instanceof" Collection, you can call the method directly:

let collection = new Collection([]);
collection.order((a, b) => a - b);

If not not an "instanceof" Collection, you can use the prototype method. Unlike the instance methods, you must call the function with .call or .apply in order to properly set the this value to the array-like object. This is because many of the Collection methods have "rest" parameters:

let arr = [];
Collection.prototype.order.call(arr, (a, b) => a - b);

As with every method, you can simply import the function and use it directly (again making sure to call it with .call or .apply):

import order from '@schematize/instance.js/src/Collection/order.mjs';
//...
let arr = [];
order.call(obj, (a, b) => a - b);

Parameters

  • comparefn (function): Optional comparison function for sorting.

Returns

  • Collection: The collection itself (for chaining).

Events

Note: During order operations, individual "set", "modify", and "deleteProperty" events may fire for each index being reordered. A single "change" event fires after the complete operation.

"change" event

The order function fires a "change" event after the collection has been sorted (only if changes were made).

Event Detail Properties:

  • instance - The collection that was modified
  • changes - A Map containing the property changes
  • added - Array of items that were added (empty for order)
  • removed - Array of items that were removed (empty for order)
collection.order((a, b) => a - b); // Returns the collection

Collection as a Proxy

When globalThis.__proxy__ is enabled, Collection instances are also wrapped in proxies, providing the same "magic" behavior for array operations.

Magic Array Operations

With proxy support enabled, you can use normal array syntax and all operations trigger events:

// Enable proxy support
globalThis.__proxy__ = true;
import { Collection } from '@schematize/instance.js';

const collection = new Collection(['a', 'b', 'c']);

// Normal index access - triggers "get" events
console.log(collection[0]); // 'a'

// Normal index assignment - triggers "set" events
collection[1] = 'x'; // Fires "set" and "change" events

// Normal index deletion - triggers "deleteProperty" events
delete collection[2]; // Fires "deleteProperty" and "change" events

// Normal array methods work with automatic event dispatching
collection.push('d'); // Fires "change" event
collection.pop(); // Fires "change" event

Standard Array Method Integration

The proxy system captures JavaScript's internal index access, assignment, and deletion operations that occur within standard array methods:

// These operations internally trigger proxy traps:
collection.splice(1, 2, 'new1', 'new2'); // Internal index access/assignment triggers events
collection.sort(); // Internal index swapping triggers events
collection.reverse(); // Internal index swapping triggers events
collection.fill('x', 1, 3); // Internal index assignment triggers events

This means that even when using standard array methods, you get full event coverage for all the underlying property operations, providing complete observability into array changes.

Proxy Target Access

Collections also have the same proxy target references:

// Access the original array (bypasses proxy traps)
const original = collection.__proxyTarget__;
original[0] = 'direct'; // No events fired

// Access the proxy (triggers events)
const proxy = collection.__proxy__;
proxy[0] = 'magic'; // Fires "set" event

This gives you the flexibility to choose between performance (direct access) and features (event dispatching) based on your needs.

Utility Functions

sid - Super ID

A sid is composed of:

  1. the current timestamp in milliseconds,
  2. a crypographically generated random string,
  3. and an incrementing id within the millisecond.
  4. Math.random.

In theory it is universally unique and has been tested, but more tests could be done to be sure.

findType

Find the __type__ of an instance. Returns the instance's __type__ property if it exists, otherwise returns the constructor from the instance's prototype.

import findType from '@schematize/instance.js/src/utils/findType.mjs';

let instance = new Instance();
let type = findType(instance); // Returns Instance constructor

let AType = {};
let obj = {
  __type__: AType,
};
findType(obj); // returns AType

getConstructor

Get the constructor function from an instance's prototype chain.

import getConstructor from '@schematize/instance.js/src/utils/getConstructor.mjs';

let instance = new Instance();
let constructor = getConstructor(instance); // Returns Instance constructor

class SweetClass {}
let instance = new SweetClass();
let constructor = getConstructor(SweetClass); // Returns SweetClass class constructor

isClassOrFunctionConstructor

Check if a value is a class or function constructor (has a prototype property).

import isClassOrFunctionConstructor from '@schematize/instance.js/src/utils/isClassOrFunctionConstructor.mjs';

isClassOrFunctionConstructor(Instance); // true
isClassOrFunctionConstructor(class {}); // true
isClassOrFunctionConstructor(function () {}); // true
isClassOrFunctionConstructor(() => {}); // false (arrow function)
isClassOrFunctionConstructor({}); // false (object)

Browser Support

Instance.js works in all modern browsers and Node.js environments that support:

  • ES6 Classes
  • Proxy (optional)
  • Map/Set
  • Symbol

License

MIT