@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.jsimport 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; // trueThis 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; // trueTo 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__); // 5nhpc2ufqkfuj588nhjauipcThe 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 equalityEventTarget
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 constructorinstance- The cached instance if one was found by__id__, otherwiseundefinedthis- Thethiscontext 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 logicreturnValue.initialized- Can be set totrueto skip the rest of the initialization processreturnValue.noAssign- Can be set totrueto 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 instanceproperties- The properties object passed to the Instance constructorinstance- 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 modifiedchanges- A Map containing the property changes withfromandtovalues
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 instanceinstance- The instance being accessedproperty- 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 valueInstance.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 instanceinstance- The instance being modifiedproperty- The property name being setpreviousValue- The previous value of the propertyvalue- 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 instanceinstance- The instance that was modifiedproperty- The property name that was setpreviousValue- The previous value of the propertyvalue- 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 instanceinstance- The instance that was modifiedchanges- A Map containing the property changesadded- Array of new values addedremoved- 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 instanceinstance- The instance being modifiedproperty- The property name being deletedpreviousValue- 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 instanceinstance- The instance that was modifiedproperty- The property name that was deletedpreviousValue- 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 instanceinstance- The instance that was modifiedchanges- A Map containing the property changesadded- 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 instanceinstance- The instance being modifiedproperty- The property name being defineddescriptor- 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 instanceinstance- The instance that was modifiedproperty- The property name that was defineddescriptor- The property descriptor that was appliedpreviousValue- The previous value of the propertyvalue- 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 instanceinstance- The instance that was modifiedchanges- A Map containing the property changesadded- Array of new values addedremoved- 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 eventEvents
"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 instanceinstance- The instance that was modifiedchanges- A Map containing all the property changesadded- 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 instanceIf 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 instanceinstance- 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 instanceinstance- The instance that was destroyedchanges- 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:
- Checks the cache first: If an instance with that
__id__exists inInstance.C, it immediately replaces the__ref__object with the actual instance - Creates a backref if not found: If the instance doesn't exist yet, it creates a backref entry in
Instance.Rthat will be resolved later - 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:
- Removes old cache entries: Deletes the instance from the old
__id__in bothInstance.CandInstance.R - Resolves pending backrefs: If there were backrefs waiting for the new
__id__, they are immediately resolved - Updates cache: Adds the instance to the new
__id__inInstance.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 upThis 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__); // falseWhen 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:
pushfillpopshiftunshiftsplicereversesortcopyWithin
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` eventCustom 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 modifiedchanges- A Map containing the property changesadded- Array of items that were addedremoved- Array of items that were removed (empty for append)
collection.append('item1', 'item2'); // Returns new lengthCollection.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 modifiedchanges- A Map containing the property changesadded- Array of items that were addedremoved- Array of items that were removed (empty for prepend)
collection.prepend('item1', 'item2'); // Returns new lengthCollection.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, orundefinedif 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 modifiedchanges- A Map containing the property changesadded- Array of items that were added (empty for removeLast)removed- Array containing the removed item
let item = collection.removeLast(); // Returns removed itemCollection.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, orundefinedif 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 modifiedchanges- A Map containing the property changesadded- Array of items that were added (empty for removeFirst)removed- Array containing the removed item
let item = collection.removeFirst(); // Returns removed itemCollection.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 modifiedchanges- A Map containing the property changesadded- Array of items that were addedremoved- Array of items that were removed
let removed = collection.replace(0, 2, 'new1', 'new2'); // Returns removed itemsCollection.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 modifiedchanges- A Map containing the property changesadded- Array of items that were added (empty for flip)removed- Array of items that were removed (empty for flip)
collection.flip(); // Returns the collectionCollection.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 modifiedchanges- A Map containing the property changesadded- 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 collectionCollection 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" eventStandard 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 eventsThis 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" eventThis 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:
- the current timestamp in milliseconds,
- a crypographically generated random string,
- and an incrementing id within the millisecond.
- 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 ATypegetConstructor
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 constructorisClassOrFunctionConstructor
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
