diff --git a/CHANGELOG.md b/CHANGELOG.md index 0060be6..c7ecb5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +# [1.5.0](https://github.com/klientjs/core/compare/1.4.0...1.5.0) (2023-06-12) + + +### Features + +* allow listen for changes made on parameters and bag objects ([2efd0ca](https://github.com/klientjs/core/commit/2efd0ca9bd2c5e1645fc6f150e9151fc5646f292)) + # [1.4.0](https://github.com/klientjs/core/compare/1.3.1...1.4.0) (2023-06-11) diff --git a/dist/cjs/index.d.ts b/dist/cjs/index.d.ts index 5aabc53..bc32a90 100644 --- a/dist/cjs/index.d.ts +++ b/dist/cjs/index.d.ts @@ -1,7 +1,7 @@ import Klient from './klient'; export { AxiosError } from 'axios'; export { default as Extensions } from './extensions'; -export { default as Bag } from './services/bag'; +export { default as Bag } from './services/bag/bag'; export { default as Dispatcher } from './services/dispatcher/dispatcher'; export { default as RequestFactory } from './services/request/factory'; export { default as Request } from './services/request/request'; diff --git a/dist/cjs/index.js b/dist/cjs/index.js index d37169a..0ed423f 100644 --- a/dist/cjs/index.js +++ b/dist/cjs/index.js @@ -6,7 +6,7 @@ var axios_1 = require("axios"); Object.defineProperty(exports, "AxiosError", { enumerable: true, get: function () { return axios_1.AxiosError; } }); var extensions_1 = require("./extensions"); Object.defineProperty(exports, "Extensions", { enumerable: true, get: function () { return extensions_1.default; } }); -var bag_1 = require("./services/bag"); +var bag_1 = require("./services/bag/bag"); Object.defineProperty(exports, "Bag", { enumerable: true, get: function () { return bag_1.default; } }); var dispatcher_1 = require("./services/dispatcher/dispatcher"); Object.defineProperty(exports, "Dispatcher", { enumerable: true, get: function () { return dispatcher_1.default; } }); diff --git a/dist/cjs/klient.d.ts b/dist/cjs/klient.d.ts index 1481554..9b8fed8 100644 --- a/dist/cjs/klient.d.ts +++ b/dist/cjs/klient.d.ts @@ -1,4 +1,4 @@ -import Bag from './services/bag'; +import Bag from './services/bag/bag'; import Dispatcher from './services/dispatcher/dispatcher'; import RequestFactory from './services/request/factory'; import type Event from './events/event'; diff --git a/dist/cjs/klient.js b/dist/cjs/klient.js index a81aed5..5f868ee 100644 --- a/dist/cjs/klient.js +++ b/dist/cjs/klient.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const bag_1 = require("./services/bag"); +const bag_1 = require("./services/bag/bag"); const dispatcher_1 = require("./services/dispatcher/dispatcher"); const factory_1 = require("./services/request/factory"); const extensions_1 = require("./extensions"); diff --git a/dist/cjs/services/bag.d.ts b/dist/cjs/services/bag.d.ts deleted file mode 100644 index 6302af3..0000000 --- a/dist/cjs/services/bag.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare type BagItems = Record; -export default class Bag implements BagItems { - [x: string]: unknown; - constructor(items?: BagItems); - get(path: string): any; - set(path: string, value: unknown): this; - merge(...items: BagItems[]): this; - all(): BagItems; -} -export {}; diff --git a/dist/cjs/services/bag.js b/dist/cjs/services/bag.js deleted file mode 100644 index f288227..0000000 --- a/dist/cjs/services/bag.js +++ /dev/null @@ -1,36 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const deepmerge = require("deepmerge"); -const objectPath = require("object-path"); -class Bag { - constructor(items = {}) { - Object.keys(items).forEach((key) => { - this.set(key, items[key]); - }); - } - get(path) { - return objectPath.get(this, path); - } - set(path, value) { - objectPath.set(this, path, value); - return this; - } - merge(...items) { - const nextState = deepmerge.all([this, ...items], { - arrayMerge: (_destinationArray, sourceArray) => sourceArray, - isMergeableObject: (o) => (o === null || o === void 0 ? void 0 : o.constructor) === Array || (o === null || o === void 0 ? void 0 : o.constructor) === Object - }); - Object.keys(nextState).forEach((key) => { - this.set(key, nextState[key]); - }); - return this; - } - all() { - const all = {}; - Object.keys(this).forEach((key) => { - all[key] = this[key]; - }); - return all; - } -} -exports.default = Bag; diff --git a/dist/cjs/services/bag/bag.d.ts b/dist/cjs/services/bag/bag.d.ts new file mode 100644 index 0000000..1a0a8d0 --- /dev/null +++ b/dist/cjs/services/bag/bag.d.ts @@ -0,0 +1,17 @@ +import { Watchable, WatchCallback } from './watch'; +declare type BagItems = Record; +export default class Bag implements BagItems, Watchable { + [x: string]: unknown; + constructor(items?: BagItems); + get watchers(): Record; + deep: boolean; + }[]>; + get(path: string): any; + all(): import("../../toolbox/object").AnyObject; + set(path: string, value: unknown): this; + merge(...items: BagItems[]): this; + watch(path: string, onChange: WatchCallback, deep?: boolean): this; + unwatch(path: string, onChange: WatchCallback): this; +} +export {}; diff --git a/dist/cjs/services/bag/bag.js b/dist/cjs/services/bag/bag.js new file mode 100644 index 0000000..ab2e527 --- /dev/null +++ b/dist/cjs/services/bag/bag.js @@ -0,0 +1,45 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const deepmerge = require("deepmerge"); +const objectPath = require("object-path"); +const object_1 = require("../../toolbox/object"); +const watch_1 = require("./watch"); +class Bag { + constructor(items = {}) { + Object.keys(items).forEach((key) => { + this[key] = items[key]; + }); + } + get watchers() { + return (0, watch_1.getWatchers)(this); + } + get(path) { + return objectPath.get(this.all(), path); + } + all() { + return (0, object_1.softClone)(this); + } + set(path, value) { + const prevState = this.all(); + objectPath.set(this, path, value); + return (0, watch_1.invokeWatchers)(this, this.all(), prevState); + } + merge(...items) { + const prevState = this.all(); + const nextState = deepmerge.all([this.all(), ...items], { + arrayMerge: (_destinationArray, sourceArray) => sourceArray, + isMergeableObject: (o) => (0, object_1.isPlainArray)(o) || (0, object_1.isPlainObject)(o) + }); + Object.keys(nextState).forEach((key) => { + this[key] = nextState[key]; + }); + return (0, watch_1.invokeWatchers)(this, nextState, prevState); + } + watch(path, onChange, deep = false) { + return (0, watch_1.watch)(this, path, onChange, deep); + } + unwatch(path, onChange) { + return (0, watch_1.unwatch)(this, path, onChange); + } +} +exports.default = Bag; diff --git a/dist/cjs/services/bag/watch.d.ts b/dist/cjs/services/bag/watch.d.ts new file mode 100644 index 0000000..ceb0b56 --- /dev/null +++ b/dist/cjs/services/bag/watch.d.ts @@ -0,0 +1,11 @@ +export declare type Watchable = object; +export declare type WatchCallback = (next: T, prev: Z) => void; +declare type WatcherItem = { + callback: WatchCallback; + deep: boolean; +}; +export declare function getWatchers(wachtable: Watchable): Record; +export declare function watch(watchable: T, path: string, onChange: WatchCallback, deep: boolean): T; +export declare function unwatch(watchable: T, path: string, onChange: WatchCallback): T; +export declare function invokeWatchers(watchable: T, next: object, prev: object): T; +export {}; diff --git a/dist/cjs/services/bag/watch.js b/dist/cjs/services/bag/watch.js new file mode 100644 index 0000000..b2dc4b3 --- /dev/null +++ b/dist/cjs/services/bag/watch.js @@ -0,0 +1,87 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.invokeWatchers = exports.unwatch = exports.watch = exports.getWatchers = void 0; +const objectPath = require("object-path"); +const deepDiff = require("deep-diff"); +const instances = {}; +const watchers = {}; +function getInstanceId(wachtable) { + const ids = Object.keys(instances); + let id = null; + for (let i = 0, len = ids.length; i < len; i += 1) { + if (instances[ids[i]] === wachtable) { + id = ids[i]; + break; + } + } + if (id === null) { + id = Math.random().toString(36).substring(2); + instances[id] = wachtable; + } + return id; +} +function getWatchers(wachtable) { + const id = getInstanceId(wachtable); + if (!watchers[id]) { + watchers[id] = {}; + } + return watchers[id]; +} +exports.getWatchers = getWatchers; +function watch(watchable, path, onChange, deep) { + const id = getInstanceId(watchable); + watchers[id] = getWatchers(watchable); + const collection = watchers[id][path] || []; + for (let i = 0, len = collection.length; i < len; i += 1) { + if (collection[i].callback === onChange) { + return watchable; + } + } + collection.push({ callback: onChange, deep }); + watchers[id][path] = collection; + return watchable; +} +exports.watch = watch; +function unwatch(watchable, path, onChange) { + const id = getInstanceId(watchable); + watchers[id] = getWatchers(watchable); + const collection = watchers[id][path] || []; + let index = null; + for (let i = 0, len = collection.length; i < len; i += 1) { + if (collection[i].callback === onChange) { + index = i; + break; + } + } + if (index === null) { + return watchable; + } + collection.splice(index, 1); + watchers[id][path] = collection; + return watchable; +} +exports.unwatch = unwatch; +function invokeWatchers(watchable, next, prev) { + var _a; + const watched = getWatchers(watchable); + const watchedPaths = Object.keys(watched); + if (watchedPaths.length === 0) { + return watchable; + } + const changedPaths = (_a = deepDiff(prev, next)) === null || _a === void 0 ? void 0 : _a.map((diff) => diff.path.join('.')); + const invoked = []; + changedPaths === null || changedPaths === void 0 ? void 0 : changedPaths.forEach((path) => { + watchedPaths.forEach((targetPath) => { + if (!path.startsWith(targetPath)) + return; + watched[targetPath].forEach((watcher) => { + if ((path === targetPath || watcher.deep) && !invoked.includes(watcher)) { + watcher.callback(objectPath.get(next, targetPath), objectPath.get(prev, targetPath)); + invoked.push(watcher); + } + }); + }); + }); + return watchable; +} +exports.invokeWatchers = invokeWatchers; diff --git a/dist/cjs/toolbox/object.d.ts b/dist/cjs/toolbox/object.d.ts new file mode 100644 index 0000000..d2fbefa --- /dev/null +++ b/dist/cjs/toolbox/object.d.ts @@ -0,0 +1,4 @@ +export declare type AnyObject = Record; +export declare function isPlainObject(value: unknown): boolean; +export declare function isPlainArray(value: unknown): boolean; +export declare function softClone(obj: AnyObject): AnyObject; diff --git a/dist/cjs/toolbox/object.js b/dist/cjs/toolbox/object.js new file mode 100644 index 0000000..fbaf02a --- /dev/null +++ b/dist/cjs/toolbox/object.js @@ -0,0 +1,28 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.softClone = exports.isPlainArray = exports.isPlainObject = void 0; +function isPlainObject(value) { + return value !== null && typeof value === 'object' && value.constructor.name === 'Object'; +} +exports.isPlainObject = isPlainObject; +function isPlainArray(value) { + return Array.isArray(value) && value.constructor.name === 'Array'; +} +exports.isPlainArray = isPlainArray; +function softClone(obj) { + const values = {}; + Object.keys(obj).forEach((prop) => { + const value = obj[prop]; + if (isPlainObject(value)) { + values[prop] = softClone(value); + } + else if (isPlainArray(value)) { + values[prop] = value.map((b) => (isPlainObject(b) ? softClone(b) : b)); + } + else { + values[prop] = value; + } + }); + return values; +} +exports.softClone = softClone; diff --git a/dist/esm/index.d.ts b/dist/esm/index.d.ts index 5aabc53..bc32a90 100644 --- a/dist/esm/index.d.ts +++ b/dist/esm/index.d.ts @@ -1,7 +1,7 @@ import Klient from './klient'; export { AxiosError } from 'axios'; export { default as Extensions } from './extensions'; -export { default as Bag } from './services/bag'; +export { default as Bag } from './services/bag/bag'; export { default as Dispatcher } from './services/dispatcher/dispatcher'; export { default as RequestFactory } from './services/request/factory'; export { default as Request } from './services/request/request'; diff --git a/dist/esm/index.js b/dist/esm/index.js index e34119a..973fcf7 100644 --- a/dist/esm/index.js +++ b/dist/esm/index.js @@ -1,7 +1,7 @@ import Klient from './klient'; export { AxiosError } from 'axios'; export { default as Extensions } from './extensions'; -export { default as Bag } from './services/bag'; +export { default as Bag } from './services/bag/bag'; export { default as Dispatcher } from './services/dispatcher/dispatcher'; export { default as RequestFactory } from './services/request/factory'; export { default as Request } from './services/request/request'; diff --git a/dist/esm/klient.d.ts b/dist/esm/klient.d.ts index 1481554..9b8fed8 100644 --- a/dist/esm/klient.d.ts +++ b/dist/esm/klient.d.ts @@ -1,4 +1,4 @@ -import Bag from './services/bag'; +import Bag from './services/bag/bag'; import Dispatcher from './services/dispatcher/dispatcher'; import RequestFactory from './services/request/factory'; import type Event from './events/event'; diff --git a/dist/esm/klient.js b/dist/esm/klient.js index 9f22be2..6260fc7 100644 --- a/dist/esm/klient.js +++ b/dist/esm/klient.js @@ -1,4 +1,4 @@ -import Bag from './services/bag'; +import Bag from './services/bag/bag'; import Dispatcher from './services/dispatcher/dispatcher'; import RequestFactory from './services/request/factory'; import Extensions from './extensions'; diff --git a/dist/esm/services/bag.d.ts b/dist/esm/services/bag.d.ts deleted file mode 100644 index 6302af3..0000000 --- a/dist/esm/services/bag.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare type BagItems = Record; -export default class Bag implements BagItems { - [x: string]: unknown; - constructor(items?: BagItems); - get(path: string): any; - set(path: string, value: unknown): this; - merge(...items: BagItems[]): this; - all(): BagItems; -} -export {}; diff --git a/dist/esm/services/bag.js b/dist/esm/services/bag.js deleted file mode 100644 index cc3d9ca..0000000 --- a/dist/esm/services/bag.js +++ /dev/null @@ -1,33 +0,0 @@ -import * as deepmerge from 'deepmerge'; -import * as objectPath from 'object-path'; -export default class Bag { - constructor(items = {}) { - Object.keys(items).forEach((key) => { - this.set(key, items[key]); - }); - } - get(path) { - return objectPath.get(this, path); - } - set(path, value) { - objectPath.set(this, path, value); - return this; - } - merge(...items) { - const nextState = deepmerge.all([this, ...items], { - arrayMerge: (_destinationArray, sourceArray) => sourceArray, - isMergeableObject: (o) => (o === null || o === void 0 ? void 0 : o.constructor) === Array || (o === null || o === void 0 ? void 0 : o.constructor) === Object - }); - Object.keys(nextState).forEach((key) => { - this.set(key, nextState[key]); - }); - return this; - } - all() { - const all = {}; - Object.keys(this).forEach((key) => { - all[key] = this[key]; - }); - return all; - } -} diff --git a/dist/esm/services/bag/bag.d.ts b/dist/esm/services/bag/bag.d.ts new file mode 100644 index 0000000..1a0a8d0 --- /dev/null +++ b/dist/esm/services/bag/bag.d.ts @@ -0,0 +1,17 @@ +import { Watchable, WatchCallback } from './watch'; +declare type BagItems = Record; +export default class Bag implements BagItems, Watchable { + [x: string]: unknown; + constructor(items?: BagItems); + get watchers(): Record; + deep: boolean; + }[]>; + get(path: string): any; + all(): import("../../toolbox/object").AnyObject; + set(path: string, value: unknown): this; + merge(...items: BagItems[]): this; + watch(path: string, onChange: WatchCallback, deep?: boolean): this; + unwatch(path: string, onChange: WatchCallback): this; +} +export {}; diff --git a/dist/esm/services/bag/bag.js b/dist/esm/services/bag/bag.js new file mode 100644 index 0000000..ceb969c --- /dev/null +++ b/dist/esm/services/bag/bag.js @@ -0,0 +1,42 @@ +import * as deepmerge from 'deepmerge'; +import * as objectPath from 'object-path'; +import { isPlainObject, isPlainArray, softClone } from '../../toolbox/object'; +import { watch, unwatch, invokeWatchers, getWatchers } from './watch'; +export default class Bag { + constructor(items = {}) { + Object.keys(items).forEach((key) => { + this[key] = items[key]; + }); + } + get watchers() { + return getWatchers(this); + } + get(path) { + return objectPath.get(this.all(), path); + } + all() { + return softClone(this); + } + set(path, value) { + const prevState = this.all(); + objectPath.set(this, path, value); + return invokeWatchers(this, this.all(), prevState); + } + merge(...items) { + const prevState = this.all(); + const nextState = deepmerge.all([this.all(), ...items], { + arrayMerge: (_destinationArray, sourceArray) => sourceArray, + isMergeableObject: (o) => isPlainArray(o) || isPlainObject(o) + }); + Object.keys(nextState).forEach((key) => { + this[key] = nextState[key]; + }); + return invokeWatchers(this, nextState, prevState); + } + watch(path, onChange, deep = false) { + return watch(this, path, onChange, deep); + } + unwatch(path, onChange) { + return unwatch(this, path, onChange); + } +} diff --git a/dist/esm/services/bag/watch.d.ts b/dist/esm/services/bag/watch.d.ts new file mode 100644 index 0000000..ceb0b56 --- /dev/null +++ b/dist/esm/services/bag/watch.d.ts @@ -0,0 +1,11 @@ +export declare type Watchable = object; +export declare type WatchCallback = (next: T, prev: Z) => void; +declare type WatcherItem = { + callback: WatchCallback; + deep: boolean; +}; +export declare function getWatchers(wachtable: Watchable): Record; +export declare function watch(watchable: T, path: string, onChange: WatchCallback, deep: boolean): T; +export declare function unwatch(watchable: T, path: string, onChange: WatchCallback): T; +export declare function invokeWatchers(watchable: T, next: object, prev: object): T; +export {}; diff --git a/dist/esm/services/bag/watch.js b/dist/esm/services/bag/watch.js new file mode 100644 index 0000000..d097155 --- /dev/null +++ b/dist/esm/services/bag/watch.js @@ -0,0 +1,80 @@ +import * as objectPath from 'object-path'; +import * as deepDiff from 'deep-diff'; +const instances = {}; +const watchers = {}; +function getInstanceId(wachtable) { + const ids = Object.keys(instances); + let id = null; + for (let i = 0, len = ids.length; i < len; i += 1) { + if (instances[ids[i]] === wachtable) { + id = ids[i]; + break; + } + } + if (id === null) { + id = Math.random().toString(36).substring(2); + instances[id] = wachtable; + } + return id; +} +export function getWatchers(wachtable) { + const id = getInstanceId(wachtable); + if (!watchers[id]) { + watchers[id] = {}; + } + return watchers[id]; +} +export function watch(watchable, path, onChange, deep) { + const id = getInstanceId(watchable); + watchers[id] = getWatchers(watchable); + const collection = watchers[id][path] || []; + for (let i = 0, len = collection.length; i < len; i += 1) { + if (collection[i].callback === onChange) { + return watchable; + } + } + collection.push({ callback: onChange, deep }); + watchers[id][path] = collection; + return watchable; +} +export function unwatch(watchable, path, onChange) { + const id = getInstanceId(watchable); + watchers[id] = getWatchers(watchable); + const collection = watchers[id][path] || []; + let index = null; + for (let i = 0, len = collection.length; i < len; i += 1) { + if (collection[i].callback === onChange) { + index = i; + break; + } + } + if (index === null) { + return watchable; + } + collection.splice(index, 1); + watchers[id][path] = collection; + return watchable; +} +export function invokeWatchers(watchable, next, prev) { + var _a; + const watched = getWatchers(watchable); + const watchedPaths = Object.keys(watched); + if (watchedPaths.length === 0) { + return watchable; + } + const changedPaths = (_a = deepDiff(prev, next)) === null || _a === void 0 ? void 0 : _a.map((diff) => diff.path.join('.')); + const invoked = []; + changedPaths === null || changedPaths === void 0 ? void 0 : changedPaths.forEach((path) => { + watchedPaths.forEach((targetPath) => { + if (!path.startsWith(targetPath)) + return; + watched[targetPath].forEach((watcher) => { + if ((path === targetPath || watcher.deep) && !invoked.includes(watcher)) { + watcher.callback(objectPath.get(next, targetPath), objectPath.get(prev, targetPath)); + invoked.push(watcher); + } + }); + }); + }); + return watchable; +} diff --git a/dist/esm/toolbox/object.d.ts b/dist/esm/toolbox/object.d.ts new file mode 100644 index 0000000..d2fbefa --- /dev/null +++ b/dist/esm/toolbox/object.d.ts @@ -0,0 +1,4 @@ +export declare type AnyObject = Record; +export declare function isPlainObject(value: unknown): boolean; +export declare function isPlainArray(value: unknown): boolean; +export declare function softClone(obj: AnyObject): AnyObject; diff --git a/dist/esm/toolbox/object.js b/dist/esm/toolbox/object.js new file mode 100644 index 0000000..b8286b6 --- /dev/null +++ b/dist/esm/toolbox/object.js @@ -0,0 +1,22 @@ +export function isPlainObject(value) { + return value !== null && typeof value === 'object' && value.constructor.name === 'Object'; +} +export function isPlainArray(value) { + return Array.isArray(value) && value.constructor.name === 'Array'; +} +export function softClone(obj) { + const values = {}; + Object.keys(obj).forEach((prop) => { + const value = obj[prop]; + if (isPlainObject(value)) { + values[prop] = softClone(value); + } + else if (isPlainArray(value)) { + values[prop] = value.map((b) => (isPlainObject(b) ? softClone(b) : b)); + } + else { + values[prop] = value; + } + }); + return values; +} diff --git a/package-lock.json b/package-lock.json index d336a97..d334af5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@klient/core", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@klient/core", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "axios": "^0.27.0", diff --git a/package.json b/package.json index ecdaf26..1085981 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "axios", "sdk" ], - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "main": "./dist/cjs/index.js", "types": "./dist/cjs/index.d.ts",