penambahan web socket

This commit is contained in:
2025-09-18 19:01:22 +07:00
parent 1d053646a9
commit d7bb2eb5bb
15070 changed files with 2402916 additions and 0 deletions

View File

@@ -0,0 +1,298 @@
export type Options = {
/**
Deep changes will not trigger the callback. Only changes to the immediate properties of the original object.
@default false
@example
```
import onChange from 'on-change';
const object = {
a: {
b: false
}
};
let index = 0;
const watchedObject = onChange(object, () => {
console.log('Object changed:', ++index);
}, {isShallow: true});
watchedObject.a.b = true;
// Nothing happens
watchedObject.a = true;
//=> 'Object changed: 1'
```
*/
readonly isShallow?: boolean;
/**
The function receives two arguments to be compared for equality. Should return `true` if the two values are determined to be equal.
@default Object.is
@example
```
import onChange from 'on-change';
const object = {
a: {
b: false
}
};
let index = 0;
const watchedObject = onChange(object, () => {
console.log('Object changed:', ++index);
}, {equals: (a, b) => a === b});
watchedObject.a.b = 0;
// Nothing happens
watchedObject.a = true;
//=> 'Object changed: 1'
```
*/
equals?: (a: unknown, b: unknown) => boolean;
/**
Setting properties as `Symbol` won't trigger the callback.
@default false
*/
readonly ignoreSymbols?: boolean;
/**
Setting properties in this array won't trigger the callback.
@default undefined
*/
readonly ignoreKeys?: ReadonlyArray<string | symbol>;
/**
Setting properties with an underscore as the first character won't trigger the callback.
@default false
*/
readonly ignoreUnderscores?: boolean;
/**
The path will be provided as an array of keys instead of a delimited string.
@default false
*/
readonly pathAsArray?: boolean;
/**
Ignore changes to objects that become detached from the watched object.
@default false
*/
readonly ignoreDetached?: boolean;
/**
Trigger callbacks for each change within specified method calls or all method calls.
@default false
*/
readonly details?: boolean | readonly string[];
/**
The function receives the same arguments and context as the [onChange callback](#onchange). The function is called whenever a change is attempted. Returning true will allow the change to be made and the onChange callback to execute, returning anything else will prevent the change from being made and the onChange callback will not trigger.
@example
```
import onChange from 'on-change';
const object = {a: 0};
let index = 0;
const watchedObject = onChange(object, () => {
console.log('Object changed:', ++index);
}, {onValidate: () => false});
watchedObject.a = true;
// watchedObject.a still equals 0
```
*/
onValidate?: (
this: unknown,
path: string,
value: unknown,
previousValue: unknown,
applyData: ApplyData
) => boolean;
};
export type ApplyData = {
/**
The name of the method that produced the change.
*/
readonly name: string;
/**
The arguments provided to the method that produced the change.
*/
readonly args: unknown[];
/**
The result returned from the method that produced the change.
*/
readonly result: unknown;
};
declare const onChange: {
/**
Watch an object or array for changes. It works recursively, so it will even detect if you modify a deep property like `obj.a.b[0].c = true`.
@param object - Object to watch for changes.
@param onChange - Function that gets called anytime the object changes.
@param [options] - Options for altering the behavior of onChange.
@returns A version of `object` that is watched. It's the exact same object, just with some `Proxy` traps.
@example
```
import onChange from 'on-change';
const object = {
foo: false,
a: {
b: [
{
c: false
}
]
}
};
let index = 0;
const watchedObject = onChange(object, function (path, value, previousValue, applyData) {
console.log('Object changed:', ++index);
console.log('this:', this);
console.log('path:', path);
console.log('value:', value);
console.log('previousValue:', previousValue);
console.log('applyData:', applyData);
});
watchedObject.foo = true;
//=> 'Object changed: 1'
//=> 'this: {
// foo: true,
// a: {
// b: [
// {
// c: false
// }
// ]
// }
// }'
//=> 'path: "foo"'
//=> 'value: true'
//=> 'previousValue: false'
//=> 'applyData: undefined'
watchedObject.a.b[0].c = true;
//=> 'Object changed: 2'
//=> 'this: {
// foo: true,
// a: {
// b: [
// {
// c: true
// }
// ]
// }
// }'
//=> 'path: "a.b.0.c"'
//=> 'value: true'
//=> 'previousValue: false'
//=> 'applyData: undefined'
watchedObject.a.b.push(3);
//=> 'Object changed: 3'
//=> 'this: {
// foo: true,
// a: {
// b: [
// {
// c: true
// },
// 3
// ]
// }
// }'
//=> 'path: "a.b"'
//=> 'value: [{c: true}, 3]'
//=> 'previousValue: [{c: true}]'
//=> 'applyData: {
// name: "push",
// args: [3],
// result: 2,
// }'
// Access the original object
onChange.target(watchedObject).foo = false;
// Callback isn't called
// Unsubscribe
onChange.unsubscribe(watchedObject);
watchedObject.foo = 'bar';
// Callback isn't called
```
*/
<ObjectType extends Record<string, any>>(
object: ObjectType,
onChange: (
this: ObjectType,
path: string,
value: unknown,
previousValue: unknown,
applyData: ApplyData
) => void,
options?: Options & {pathAsArray?: false}
): ObjectType;
// Overload that returns a string array as path when `ignoreSymbols` and `pathAsArray` options are true.
<ObjectType extends Record<string, any>>(
object: ObjectType,
onChange: (
this: ObjectType,
path: string[],
value: unknown,
previousValue: unknown,
applyData: ApplyData
) => void,
options: Options & {ignoreSymbols: true; pathAsArray: true}
): ObjectType;
// Overload that returns an array as path when `pathAsArray` option is true.
<ObjectType extends Record<string, any>>(
object: ObjectType,
onChange: (
this: ObjectType,
path: Array<string | symbol>,
value: unknown,
previousValue: unknown,
applyData: ApplyData
) => void,
options: Options & {pathAsArray: true}
): ObjectType;
/**
@param object - Object that is already being watched for changes.
@returns The original unwatched object.
*/
target<ObjectType extends Record<string, any>>(object: ObjectType): ObjectType;
/**
Cancels all future callbacks on a watched object.
@param object - Object that is already being watched for changes.
@returns The original unwatched object.
*/
unsubscribe<ObjectType extends Record<string, any>>(object: ObjectType): ObjectType;
};
export default onChange;

View File

@@ -0,0 +1,292 @@
/* eslint-disable unicorn/prefer-spread */
import {TARGET, UNSUBSCRIBE, PATH_SEPARATOR} from './lib/constants.js';
import {isBuiltinWithMutableMethods, isBuiltinWithoutMutableMethods} from './lib/is-builtin.js';
import path from './lib/path.js';
import isArray from './lib/is-array.js';
import isSymbol from './lib/is-symbol.js';
import isIterator from './lib/is-iterator.js';
import wrapIterator from './lib/wrap-iterator.js';
import ignoreProperty from './lib/ignore-property.js';
import Cache from './lib/cache.js';
import SmartClone from './lib/smart-clone/smart-clone.js';
const defaultOptions = {
equals: Object.is,
isShallow: false,
pathAsArray: false,
ignoreSymbols: false,
ignoreUnderscores: false,
ignoreDetached: false,
details: false,
};
const onChange = (object, onChange, options = {}) => {
options = {
...defaultOptions,
...options,
};
const proxyTarget = Symbol('ProxyTarget');
const {equals, isShallow, ignoreDetached, details} = options;
const cache = new Cache(equals);
const hasOnValidate = typeof options.onValidate === 'function';
const smartClone = new SmartClone(hasOnValidate);
// eslint-disable-next-line max-params
const validate = (target, property, value, previous, applyData) => !hasOnValidate
|| smartClone.isCloning
|| options.onValidate(path.concat(cache.getPath(target), property), value, previous, applyData) === true;
const handleChangeOnTarget = (target, property, value, previous) => {
if (
!ignoreProperty(cache, options, property)
&& !(ignoreDetached && cache.isDetached(target, object))
) {
handleChange(cache.getPath(target), property, value, previous);
}
};
// eslint-disable-next-line max-params
const handleChange = (changePath, property, value, previous, applyData) => {
if (smartClone.isCloning && smartClone.isPartOfClone(changePath)) {
smartClone.update(changePath, property, previous);
} else {
onChange(path.concat(changePath, property), value, previous, applyData);
}
};
const getProxyTarget = value => value
? (value[proxyTarget] ?? value)
: value;
const prepareValue = (value, target, property, basePath) => {
if (
isBuiltinWithoutMutableMethods(value)
|| property === 'constructor'
|| (isShallow && !SmartClone.isHandledMethod(target, property))
|| ignoreProperty(cache, options, property)
|| cache.isGetInvariant(target, property)
|| (ignoreDetached && cache.isDetached(target, object))
) {
return value;
}
if (basePath === undefined) {
basePath = cache.getPath(target);
}
/*
Check for circular references.
If the value already has a corresponding path/proxy,
and if the path corresponds to one of the parents,
then we are on a circular case, where the child is pointing to their parent.
In this case we return the proxy object with the shortest path.
*/
const childPath = path.concat(basePath, property);
const existingPath = cache.getPath(value);
if (existingPath && isSameObjectTree(childPath, existingPath)) {
// We are on the same object tree but deeper, so we use the parent path.
return cache.getProxy(value, existingPath, handler, proxyTarget);
}
return cache.getProxy(value, childPath, handler, proxyTarget);
};
/*
Returns true if `childPath` is a subpath of `existingPath`
(if childPath starts with existingPath). Otherwise, it returns false.
It also returns false if the 2 paths are identical.
For example:
- childPath = group.layers.0.parent.layers.0.value
- existingPath = group.layers.0.parent
*/
const isSameObjectTree = (childPath, existingPath) => {
if (isSymbol(childPath) || childPath.length <= existingPath.length) {
return false;
}
if (isArray(existingPath) && existingPath.length === 0) {
return false;
}
const childParts = isArray(childPath) ? childPath : childPath.split(PATH_SEPARATOR);
const existingParts = isArray(existingPath) ? existingPath : existingPath.split(PATH_SEPARATOR);
if (childParts.length <= existingParts.length) {
return false;
}
return !(existingParts.some((part, index) => part !== childParts[index]));
};
const handler = {
get(target, property, receiver) {
if (isSymbol(property)) {
if (property === proxyTarget || property === TARGET) {
return target;
}
if (
property === UNSUBSCRIBE
&& !cache.isUnsubscribed
&& cache.getPath(target).length === 0
) {
cache.unsubscribe();
return target;
}
}
const value = isBuiltinWithMutableMethods(target)
? Reflect.get(target, property)
: Reflect.get(target, property, receiver);
return prepareValue(value, target, property);
},
set(target, property, value, receiver) {
value = getProxyTarget(value);
const reflectTarget = target[proxyTarget] ?? target;
const previous = reflectTarget[property];
if (equals(previous, value) && property in target) {
return true;
}
const isValid = validate(target, property, value, previous);
if (
isValid
&& cache.setProperty(reflectTarget, property, value, receiver, previous)
) {
handleChangeOnTarget(target, property, target[property], previous);
return true;
}
return !isValid;
},
defineProperty(target, property, descriptor) {
if (!cache.isSameDescriptor(descriptor, target, property)) {
const previous = target[property];
if (
validate(target, property, descriptor.value, previous)
&& cache.defineProperty(target, property, descriptor, previous)
) {
handleChangeOnTarget(target, property, descriptor.value, previous);
}
}
return true;
},
deleteProperty(target, property) {
if (!Reflect.has(target, property)) {
return true;
}
const previous = Reflect.get(target, property);
const isValid = validate(target, property, undefined, previous);
if (
isValid
&& cache.deleteProperty(target, property, previous)
) {
handleChangeOnTarget(target, property, undefined, previous);
return true;
}
return !isValid;
},
apply(target, thisArg, argumentsList) {
const thisProxyTarget = thisArg[proxyTarget] ?? thisArg;
if (cache.isUnsubscribed) {
return Reflect.apply(target, thisProxyTarget, argumentsList);
}
if (
(details === false
|| (details !== true && !details.includes(target.name)))
&& SmartClone.isHandledType(thisProxyTarget)
) {
let applyPath = path.initial(cache.getPath(target));
const isHandledMethod = SmartClone.isHandledMethod(thisProxyTarget, target.name);
smartClone.start(thisProxyTarget, applyPath, argumentsList);
let result = Reflect.apply(
target,
smartClone.preferredThisArg(target, thisArg, thisProxyTarget),
isHandledMethod
? argumentsList.map(argument => getProxyTarget(argument))
: argumentsList,
);
const isChanged = smartClone.isChanged(thisProxyTarget, equals);
const previous = smartClone.stop();
if (SmartClone.isHandledType(result) && isHandledMethod) {
if (thisArg instanceof Map && target.name === 'get') {
applyPath = path.concat(applyPath, argumentsList[0]);
}
result = cache.getProxy(result, applyPath, handler);
}
if (isChanged) {
const applyData = {
name: target.name,
args: argumentsList,
result,
};
const changePath = smartClone.isCloning
? path.initial(applyPath)
: applyPath;
const property = smartClone.isCloning
? path.last(applyPath)
: '';
if (validate(path.get(object, changePath), property, thisProxyTarget, previous, applyData)) {
handleChange(changePath, property, thisProxyTarget, previous, applyData);
} else {
smartClone.undo(thisProxyTarget);
}
}
if (
(thisArg instanceof Map || thisArg instanceof Set)
&& isIterator(result)
) {
return wrapIterator(result, target, thisArg, applyPath, prepareValue);
}
return result;
}
return Reflect.apply(target, thisArg, argumentsList);
},
};
const proxy = cache.getProxy(object, options.pathAsArray ? [] : '', handler);
onChange = onChange.bind(proxy);
if (hasOnValidate) {
options.onValidate = options.onValidate.bind(proxy);
}
return proxy;
};
onChange.target = proxy => proxy?.[TARGET] ?? proxy;
onChange.unsubscribe = proxy => proxy?.[UNSUBSCRIBE] ?? proxy;
export default onChange;

View File

@@ -0,0 +1,152 @@
import path from './path.js';
/**
@class Cache
@private
*/
export default class Cache {
constructor(equals) {
this._equals = equals;
this._proxyCache = new WeakMap();
this._pathCache = new WeakMap();
this.isUnsubscribed = false;
}
_getDescriptorCache() {
if (this._descriptorCache === undefined) {
this._descriptorCache = new WeakMap();
}
return this._descriptorCache;
}
_getProperties(target) {
const descriptorCache = this._getDescriptorCache();
let properties = descriptorCache.get(target);
if (properties === undefined) {
properties = {};
descriptorCache.set(target, properties);
}
return properties;
}
_getOwnPropertyDescriptor(target, property) {
if (this.isUnsubscribed) {
return Reflect.getOwnPropertyDescriptor(target, property);
}
const properties = this._getProperties(target);
let descriptor = properties[property];
if (descriptor === undefined) {
descriptor = Reflect.getOwnPropertyDescriptor(target, property);
properties[property] = descriptor;
}
return descriptor;
}
getProxy(target, path, handler, proxyTarget) {
if (this.isUnsubscribed) {
return target;
}
const reflectTarget = target[proxyTarget];
const source = reflectTarget ?? target;
this._pathCache.set(source, path);
let proxy = this._proxyCache.get(source);
if (proxy === undefined) {
proxy = reflectTarget === undefined
? new Proxy(target, handler)
: target;
this._proxyCache.set(source, proxy);
}
return proxy;
}
getPath(target) {
return this.isUnsubscribed ? undefined : this._pathCache.get(target);
}
isDetached(target, object) {
return !Object.is(target, path.get(object, this.getPath(target)));
}
defineProperty(target, property, descriptor) {
if (!Reflect.defineProperty(target, property, descriptor)) {
return false;
}
if (!this.isUnsubscribed) {
this._getProperties(target)[property] = descriptor;
}
return true;
}
setProperty(target, property, value, receiver, previous) { // eslint-disable-line max-params
if (!this._equals(previous, value) || !(property in target)) {
const descriptor = this._getOwnPropertyDescriptor(target, property);
if (descriptor !== undefined && 'set' in descriptor) {
return Reflect.set(target, property, value, receiver);
}
return Reflect.set(target, property, value);
}
return true;
}
deleteProperty(target, property, previous) {
if (Reflect.deleteProperty(target, property)) {
if (!this.isUnsubscribed) {
const properties = this._getDescriptorCache().get(target);
if (properties) {
delete properties[property];
this._pathCache.delete(previous);
}
}
return true;
}
return false;
}
isSameDescriptor(a, target, property) {
const b = this._getOwnPropertyDescriptor(target, property);
return a !== undefined
&& b !== undefined
&& Object.is(a.value, b.value)
&& (a.writable || false) === (b.writable || false)
&& (a.enumerable || false) === (b.enumerable || false)
&& (a.configurable || false) === (b.configurable || false)
&& a.get === b.get
&& a.set === b.set;
}
isGetInvariant(target, property) {
const descriptor = this._getOwnPropertyDescriptor(target, property);
return descriptor !== undefined
&& descriptor.configurable !== true
&& descriptor.writable !== true;
}
unsubscribe() {
this._descriptorCache = null;
this._pathCache = null;
this._proxyCache = null;
this.isUnsubscribed = true;
}
}

View File

@@ -0,0 +1,3 @@
export const PATH_SEPARATOR = '.';
export const TARGET = Symbol('target');
export const UNSUBSCRIBE = Symbol('unsubscribe');

View File

@@ -0,0 +1,8 @@
import isSymbol from './is-symbol.js';
export default function ignoreProperty(cache, options, property) {
return cache.isUnsubscribed
|| (options.ignoreSymbols && isSymbol(property))
|| (options.ignoreUnderscores && property.charAt(0) === '_')
|| ('ignoreKeys' in options && options.ignoreKeys.includes(property));
}

View File

@@ -0,0 +1 @@
export default Array.isArray;

View File

@@ -0,0 +1,12 @@
export function isBuiltinWithMutableMethods(value) {
return value instanceof Date
|| value instanceof Set
|| value instanceof Map
|| value instanceof WeakSet
|| value instanceof WeakMap
|| ArrayBuffer.isView(value);
}
export function isBuiltinWithoutMutableMethods(value) {
return (typeof value === 'object' ? value === null : typeof value !== 'function') || value instanceof RegExp;
}

View File

@@ -0,0 +1,3 @@
export default function isIterator(value) {
return typeof value === 'object' && typeof value.next === 'function';
}

View File

@@ -0,0 +1,3 @@
export default function isObject(value) {
return toString.call(value) === '[object Object]';
}

View File

@@ -0,0 +1,3 @@
export default function isSymbol(value) {
return typeof value === 'symbol';
}

View File

@@ -0,0 +1,149 @@
import {PATH_SEPARATOR} from './constants.js';
import isArray from './is-array.js';
import isSymbol from './is-symbol.js';
const path = {
after(path, subPath) {
if (isArray(path)) {
return path.slice(subPath.length);
}
if (subPath === '') {
return path;
}
return path.slice(subPath.length + 1);
},
concat(path, key) {
if (isArray(path)) {
path = [...path];
if (key) {
path.push(key);
}
return path;
}
if (key && key.toString !== undefined) {
if (path !== '') {
path += PATH_SEPARATOR;
}
if (isSymbol(key)) {
return path + key.toString();
}
return path + key;
}
return path;
},
initial(path) {
if (isArray(path)) {
return path.slice(0, -1);
}
if (path === '') {
return path;
}
const index = path.lastIndexOf(PATH_SEPARATOR);
if (index === -1) {
return '';
}
return path.slice(0, index);
},
last(path) {
if (isArray(path)) {
return path.at(-1) ?? '';
}
if (path === '') {
return path;
}
const index = path.lastIndexOf(PATH_SEPARATOR);
if (index === -1) {
return path;
}
return path.slice(index + 1);
},
walk(path, callback) {
if (isArray(path)) {
for (const key of path) {
callback(key);
}
} else if (path !== '') {
let position = 0;
let index = path.indexOf(PATH_SEPARATOR);
if (index === -1) {
callback(path);
} else {
while (position < path.length) {
if (index === -1) {
index = path.length;
}
callback(path.slice(position, index));
position = index + 1;
index = path.indexOf(PATH_SEPARATOR, position);
}
}
}
},
get(object, path) {
this.walk(path, key => {
if (object) {
object = object[key];
}
});
return object;
},
isSubPath(path, subPath) {
if (isArray(path)) {
if (path.length < subPath.length) {
return false;
}
// eslint-disable-next-line unicorn/no-for-loop
for (let i = 0; i < subPath.length; i++) {
if (path[i] !== subPath[i]) {
return false;
}
}
return true;
}
if (path.length < subPath.length) {
return false;
}
if (path === subPath) {
return true;
}
if (path.startsWith(subPath)) {
return path[subPath.length] === PATH_SEPARATOR;
}
return false;
},
isRootPath(path) {
if (isArray(path)) {
return path.length === 0;
}
return path === '';
},
};
export default path;

View File

@@ -0,0 +1,8 @@
import {HANDLED_ARRAY_METHODS} from '../methods/array.js';
import CloneObject from './clone-object.js';
export default class CloneArray extends CloneObject {
static isHandledMethod(name) {
return HANDLED_ARRAY_METHODS.has(name);
}
}

View File

@@ -0,0 +1,11 @@
import CloneObject from './clone-object.js';
export default class CloneDate extends CloneObject {
undo(object) {
object.setTime(this.clone.getTime());
}
isChanged(value, equals) {
return !equals(this.clone.valueOf(), value.valueOf());
}
}

View File

@@ -0,0 +1,20 @@
import {HANDLED_MAP_METHODS} from '../methods/map.js';
import CloneObject from './clone-object.js';
export default class CloneMap extends CloneObject {
static isHandledMethod(name) {
return HANDLED_MAP_METHODS.has(name);
}
undo(object) {
for (const [key, value] of this.clone.entries()) {
object.set(key, value);
}
for (const key of object.keys()) {
if (!this.clone.has(key)) {
object.delete(key);
}
}
}
}

View File

@@ -0,0 +1,115 @@
import path from '../../path.js';
import isArray from '../../is-array.js';
import isObject from '../../is-object.js';
import {MUTABLE_ARRAY_METHODS} from '../methods/array.js';
import {MUTABLE_SET_METHODS} from '../methods/set.js';
import {MUTABLE_MAP_METHODS} from '../methods/map.js';
import {IMMUTABLE_OBJECT_METHODS} from '../methods/object.js';
export default class CloneObject {
constructor(value, path, argumentsList, hasOnValidate) {
this._path = path;
this._isChanged = false;
this._clonedCache = new Set();
this._hasOnValidate = hasOnValidate;
this._changes = hasOnValidate ? [] : null;
this.clone = path === undefined ? value : this._shallowClone(value);
}
static isHandledMethod(name) {
return IMMUTABLE_OBJECT_METHODS.has(name);
}
_shallowClone(value) {
let clone = value;
if (isObject(value)) {
clone = {...value};
} else if (isArray(value) || ArrayBuffer.isView(value)) {
clone = [...value];
} else if (value instanceof Date) {
clone = new Date(value);
} else if (value instanceof Set) {
clone = new Set([...value].map(item => this._shallowClone(item)));
} else if (value instanceof Map) {
clone = new Map();
for (const [key, item] of value.entries()) {
clone.set(key, this._shallowClone(item));
}
}
this._clonedCache.add(clone);
return clone;
}
preferredThisArg(isHandledMethod, name, thisArgument, thisProxyTarget) {
if (isHandledMethod) {
if (isArray(thisProxyTarget)) {
this._onIsChanged = MUTABLE_ARRAY_METHODS[name];
} else if (thisProxyTarget instanceof Set) {
this._onIsChanged = MUTABLE_SET_METHODS[name];
} else if (thisProxyTarget instanceof Map) {
this._onIsChanged = MUTABLE_MAP_METHODS[name];
}
return thisProxyTarget;
}
return thisArgument;
}
update(fullPath, property, value) {
const changePath = path.after(fullPath, this._path);
if (property !== 'length') {
let object = this.clone;
path.walk(changePath, key => {
if (object?.[key]) {
if (!this._clonedCache.has(object[key])) {
object[key] = this._shallowClone(object[key]);
}
object = object[key];
}
});
if (this._hasOnValidate) {
this._changes.push({
path: changePath,
property,
previous: value,
});
}
if (object?.[property]) {
object[property] = value;
}
}
this._isChanged = true;
}
undo(object) {
let change;
for (let index = this._changes.length - 1; index !== -1; index--) {
change = this._changes[index];
path.get(object, change.path)[change.property] = change.previous;
}
}
isChanged(value) {
return this._onIsChanged === undefined
? this._isChanged
: this._onIsChanged(this.clone, value);
}
isPathApplicable(changePath) {
return path.isRootPath(this._path) || path.isSubPath(changePath, this._path);
}
}

View File

@@ -0,0 +1,20 @@
import {HANDLED_SET_METHODS} from '../methods/set.js';
import CloneObject from './clone-object.js';
export default class CloneSet extends CloneObject {
static isHandledMethod(name) {
return HANDLED_SET_METHODS.has(name);
}
undo(object) {
for (const value of this.clone) {
object.add(value);
}
for (const value of object) {
if (!this.clone.has(value)) {
object.delete(value);
}
}
}
}

View File

@@ -0,0 +1,27 @@
import CloneObject from './clone-object.js';
export default class CloneWeakMap extends CloneObject {
constructor(value, path, argumentsList, hasOnValidate) {
super(undefined, path, argumentsList, hasOnValidate);
this._weakKey = argumentsList[0];
this._weakHas = value.has(this._weakKey);
this._weakValue = value.get(this._weakKey);
}
isChanged(value) {
return this._weakValue !== value.get(this._weakKey);
}
undo(object) {
const weakHas = object.has(this._weakKey);
if (this._weakHas && !weakHas) {
object.set(this._weakKey, this._weakValue);
} else if (!this._weakHas && weakHas) {
object.delete(this._weakKey);
} else if (this._weakValue !== object.get(this._weakKey)) {
object.set(this._weakKey, this._weakValue);
}
}
}

View File

@@ -0,0 +1,22 @@
import CloneObject from './clone-object.js';
export default class CloneWeakSet extends CloneObject {
constructor(value, path, argumentsList, hasOnValidate) {
super(undefined, path, argumentsList, hasOnValidate);
this._argument1 = argumentsList[0];
this._weakValue = value.has(this._argument1);
}
isChanged(value) {
return this._weakValue !== value.has(this._argument1);
}
undo(object) {
if (this._weakValue && !object.has(this._argument1)) {
object.add(this._argument1);
} else {
object.delete(this._argument1);
}
}
}

View File

@@ -0,0 +1,3 @@
export default function isDiffArrays(clone, value) {
return clone.length !== value.length || clone.some((item, index) => value[index] !== item);
}

View File

@@ -0,0 +1,3 @@
export default function isDiffCertain() {
return true;
}

View File

@@ -0,0 +1,16 @@
export default function isDiffMaps(clone, value) {
if (clone.size !== value.size) {
return true;
}
let bValue;
for (const [key, aValue] of clone) {
bValue = value.get(key);
if (bValue !== aValue || (bValue === undefined && !value.has(key))) {
return true;
}
}
return false;
}

View File

@@ -0,0 +1,13 @@
export default function isDiffSets(clone, value) {
if (clone.size !== value.size) {
return true;
}
for (const element of clone) {
if (!value.has(element)) {
return true;
}
}
return false;
}

View File

@@ -0,0 +1,31 @@
import isDiffCertain from '../diff/is-diff-certain.js';
import isDiffArrays from '../diff/is-diff-arrays.js';
import {IMMUTABLE_OBJECT_METHODS} from './object.js';
const IMMUTABLE_ARRAY_METHODS = new Set([
'concat',
'includes',
'indexOf',
'join',
'keys',
'lastIndexOf',
]);
export const MUTABLE_ARRAY_METHODS = {
push: isDiffCertain,
pop: isDiffCertain,
shift: isDiffCertain,
unshift: isDiffCertain,
copyWithin: isDiffArrays,
reverse: isDiffArrays,
sort: isDiffArrays,
splice: isDiffArrays,
flat: isDiffArrays,
fill: isDiffArrays,
};
export const HANDLED_ARRAY_METHODS = new Set([
...IMMUTABLE_OBJECT_METHODS,
...IMMUTABLE_ARRAY_METHODS,
...Object.keys(MUTABLE_ARRAY_METHODS),
]);

View File

@@ -0,0 +1,17 @@
import isDiffMaps from '../diff/is-diff-maps.js';
import {IMMUTABLE_SET_METHODS, COLLECTION_ITERATOR_METHODS} from './set.js';
const IMMUTABLE_MAP_METHODS = new Set([...IMMUTABLE_SET_METHODS, 'get']);
export const MUTABLE_MAP_METHODS = {
set: isDiffMaps,
clear: isDiffMaps,
delete: isDiffMaps,
forEach: isDiffMaps,
};
export const HANDLED_MAP_METHODS = new Set([
...IMMUTABLE_MAP_METHODS,
...Object.keys(MUTABLE_MAP_METHODS),
...COLLECTION_ITERATOR_METHODS,
]);

View File

@@ -0,0 +1,8 @@
export const IMMUTABLE_OBJECT_METHODS = new Set([
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'toLocaleString',
'toString',
'valueOf',
]);

View File

@@ -0,0 +1,25 @@
import isDiffSets from '../diff/is-diff-sets.js';
export const COLLECTION_ITERATOR_METHODS = [
'keys',
'values',
'entries',
];
export const IMMUTABLE_SET_METHODS = new Set([
'has',
'toString',
]);
export const MUTABLE_SET_METHODS = {
add: isDiffSets,
clear: isDiffSets,
delete: isDiffSets,
forEach: isDiffSets,
};
export const HANDLED_SET_METHODS = new Set([
...IMMUTABLE_SET_METHODS,
...Object.keys(MUTABLE_SET_METHODS),
...COLLECTION_ITERATOR_METHODS,
]);

View File

@@ -0,0 +1,99 @@
import isArray from '../is-array.js';
import {isBuiltinWithMutableMethods} from '../is-builtin.js';
import isObject from '../is-object.js';
import CloneObject from './clone/clone-object.js';
import CloneArray from './clone/clone-array.js';
import CloneDate from './clone/clone-date.js';
import CloneSet from './clone/clone-set.js';
import CloneMap from './clone/clone-map.js';
import CloneWeakSet from './clone/clone-weakset.js';
import CloneWeakMap from './clone/clone-weakmap.js';
export default class SmartClone {
constructor(hasOnValidate) {
this._stack = [];
this._hasOnValidate = hasOnValidate;
}
static isHandledType(value) {
return isObject(value)
|| isArray(value)
|| isBuiltinWithMutableMethods(value);
}
static isHandledMethod(target, name) {
if (isObject(target)) {
return CloneObject.isHandledMethod(name);
}
if (isArray(target)) {
return CloneArray.isHandledMethod(name);
}
if (target instanceof Set) {
return CloneSet.isHandledMethod(name);
}
if (target instanceof Map) {
return CloneMap.isHandledMethod(name);
}
return isBuiltinWithMutableMethods(target);
}
get isCloning() {
return this._stack.length > 0;
}
start(value, path, argumentsList) {
let CloneClass = CloneObject;
if (isArray(value)) {
CloneClass = CloneArray;
} else if (value instanceof Date) {
CloneClass = CloneDate;
} else if (value instanceof Set) {
CloneClass = CloneSet;
} else if (value instanceof Map) {
CloneClass = CloneMap;
} else if (value instanceof WeakSet) {
CloneClass = CloneWeakSet;
} else if (value instanceof WeakMap) {
CloneClass = CloneWeakMap;
}
this._stack.push(new CloneClass(value, path, argumentsList, this._hasOnValidate));
}
update(fullPath, property, value) {
this._stack.at(-1).update(fullPath, property, value);
}
preferredThisArg(target, thisArgument, thisProxyTarget) {
const {name} = target;
const isHandledMethod = SmartClone.isHandledMethod(thisProxyTarget, name);
return this._stack.at(-1)
.preferredThisArg(isHandledMethod, name, thisArgument, thisProxyTarget);
}
isChanged(isMutable, value, equals) {
return this._stack.at(-1).isChanged(isMutable, value, equals);
}
isPartOfClone(changePath) {
return this._stack.at(-1).isPathApplicable(changePath);
}
undo(object) {
if (this._previousClone !== undefined) {
this._previousClone.undo(object);
}
}
stop() {
this._previousClone = this._stack.pop();
return this._previousClone.clone;
}
}

View File

@@ -0,0 +1,63 @@
import {TARGET} from './constants.js';
// eslint-disable-next-line max-params
export default function wrapIterator(iterator, target, thisArgument, applyPath, prepareValue) {
const originalNext = iterator.next;
if (target.name === 'entries') {
iterator.next = function () {
const result = originalNext.call(this);
if (result.done === false) {
result.value[0] = prepareValue(
result.value[0],
target,
result.value[0],
applyPath,
);
result.value[1] = prepareValue(
result.value[1],
target,
result.value[0],
applyPath,
);
}
return result;
};
} else if (target.name === 'values') {
const keyIterator = thisArgument[TARGET].keys();
iterator.next = function () {
const result = originalNext.call(this);
if (result.done === false) {
result.value = prepareValue(
result.value,
target,
keyIterator.next().value,
applyPath,
);
}
return result;
};
} else {
iterator.next = function () {
const result = originalNext.call(this);
if (result.done === false) {
result.value = prepareValue(
result.value,
target,
result.value,
applyPath,
);
}
return result;
};
}
return iterator;
}

View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,55 @@
{
"name": "on-change",
"version": "5.0.1",
"description": "Watch an object or array for changes",
"license": "MIT",
"repository": "sindresorhus/on-change",
"funding": "https://github.com/sindresorhus/on-change?sponsor=1",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "https://sindresorhus.com"
},
"type": "module",
"exports": {
"types": "./index.d.ts",
"default": "./index.js"
},
"sideEffects": false,
"engines": {
"node": ">=18"
},
"scripts": {
"test": "xo && ava && tsd",
"bench": "karma start karma.bench.conf.cjs"
},
"files": [
"index.js",
"index.d.ts",
"lib"
],
"keywords": [
"on",
"change",
"watch",
"object",
"array",
"changes",
"observe",
"watcher",
"observer",
"proxy",
"proxies",
"es2015",
"event",
"listener"
],
"devDependencies": {
"ava": "^6.0.1",
"display-value": "^2.2.0",
"karma-webpack-bundle": "^1.3.3",
"powerset": "0.0.1",
"tsd": "^0.29.0",
"xo": "^0.56.0"
}
}

View File

@@ -0,0 +1,279 @@
# on-change
> Watch an object or array for changes
It works recursively, so it will even detect if you modify a deep property like `obj.a.b[0].c = true`.
Uses the [`Proxy` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy).
## Install
```sh
npm install on-change
```
## Usage
```js
import onChange from 'on-change';
const object = {
foo: false,
a: {
b: [
{
c: false
}
]
}
};
let index = 0;
const watchedObject = onChange(object, function (path, value, previousValue, applyData) {
console.log('Object changed:', ++index);
console.log('this:', this);
console.log('path:', path);
console.log('value:', value);
console.log('previousValue:', previousValue);
console.log('applyData:', applyData);
});
watchedObject.foo = true;
//=> 'Object changed: 1'
//=> 'this: {
// foo: true,
// a: {
// b: [
// {
// c: false
// }
// ]
// }
// }'
//=> 'path: "foo"'
//=> 'value: true'
//=> 'previousValue: false'
//=> 'applyData: undefined'
watchedObject.a.b[0].c = true;
//=> 'Object changed: 2'
//=> 'this: {
// foo: true,
// a: {
// b: [
// {
// c: true
// }
// ]
// }
// }'
//=> 'path: "a.b.0.c"'
//=> 'value: true'
//=> 'previousValue: false'
//=> 'applyData: undefined'
watchedObject.a.b.push(3);
//=> 'Object changed: 3'
//=> 'this: {
// foo: true,
// a: {
// b: [
// {
// c: true
// },
// 3
// ]
// }
// }'
//=> 'path: "a.b"'
//=> 'value: [{c: true}, 3]'
//=> 'previousValue: [{c: true}]'
//=> 'applyData: {
// name: "push",
// args: [3],
// result: 2,
// }'
// Access the original object
onChange.target(watchedObject).foo = false;
// Callback isn't called
// Unsubscribe
onChange.unsubscribe(watchedObject);
watchedObject.foo = 'bar';
// Callback isn't called
```
## API
### onChange(object, onChange, options?)
Returns a version of `object` that is watched. It's the exact same object, just with some `Proxy` traps.
#### object
Type: `object`
Object to watch for changes.
#### onChange
Type: `Function`
Function that gets called anytime the object changes.
The function receives four arguments:
1. A path to the value that was changed. A change to `c` in the above example would return `a.b.0.c`.
2. The new value at the path.
3. The previous value at the path. Changes in `WeakSets` and `WeakMaps` will return `undefined`.
4. An object with the name of the method that produced the change, the args passed to the method, and the result of the method.
The context (this) is set to the original object passed to `onChange` (with Proxy).
#### options
Type: `object`
Options for altering the behavior of onChange.
##### isShallow
Type: `boolean`\
Default: `false`
Deep changes will not trigger the callback. Only changes to the immediate properties of the original object.
##### equals
Type: `Function`\
Default: [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)
The function receives two arguments to be compared for equality. Should return `true` if the two values are determined to be equal. Useful if you only need a more loose form of equality.
##### ignoreSymbols
Type: `boolean`\
Default: `false`
Setting properties as `Symbol` won't trigger the callback.
##### ignoreKeys
Type: `Array<string | symbol>`\
Default: `undefined`
Setting properties in this array won't trigger the callback.
##### ignoreUnderscores
Type: `boolean`\
Default: `false`
Setting properties with an underscore as the first character won't trigger the callback.
##### pathAsArray
Type: `boolean`\
Default: `false`
The path will be provided as an array of keys instead of a delimited string. Recommended when working with Sets, Maps, or property keys that are Symbols.
##### ignoreDetached
Type: `boolean`\
Default: `false`
Ignore changes to objects that become detached from the watched object.
##### details
Type: `boolean|string[]`\
Default: `false`
Trigger callbacks for each change within specified method calls or all method calls.
##### onValidate
Type: `Function`
The function receives the same arguments and context as the [onChange callback](#onchange). The function is called whenever a change is attempted. Returning true will allow the change to be made and the onChange callback to execute, returning anything else will prevent the change from being made and the onChange callback will not trigger.
### onChange.target(object)
Returns the original unwatched object.
#### object
Type: `object`
Object that is already being watched for changes.
### onChange.unsubscribe(object)
Cancels all future callbacks on a watched object and returns the original unwatched object.
#### object
Type: `object`
Object that is already being watched for changes.
## Use-case
I had some code that was like:
```js
const foo = {
a: 0,
b: 0
};
// …
foo.a = 3;
save(foo);
// …
foo.b = 7;
save(foo);
// …
foo.a = 10;
save(foo);
```
Now it can be simplified to:
```js
const foo = onChange({
a: 0,
b: 0
}, () => save(foo));
// …
foo.a = 3;
// …
foo.b = 7;
// …
foo.a = 10;
```
## Related
- [known](https://github.com/sindresorhus/known) - Allow only access to known object properties *(Uses `Proxy` too)*
- [negative-array](https://github.com/sindresorhus/negative-array) - Negative array index support `array[-1]` *(Uses `Proxy` too)*
- [atama](https://github.com/franciscop/atama) - State manager *(Uses `Proxy` too)*
- [introspected](https://github.com/WebReflection/introspected) - Never-ending Proxy with multiple observers *(Uses `Proxy` too)*
## Maintainers
- [Sindre Sorhus](https://github.com/sindresorhus)
- [Darren Wright](https://github.com/DarrenPaulWright)