diff --git a/CHANGELOG.md b/CHANGELOG.md index c787f7373..89703a0de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,18 @@ Changelog _Note: Gaps between patch versions are faulty, broken or test releases._ +## v3.??.?? (2022-??-??) + +#### :boom: Breaking Change + +* Now to clone and freeze server responses is used Proxy API if it supported `core/request/reponse` + +#### :rocket: New Feature + +* Added a common function to clone `core/object/proxy-clone` +* Added a common function to create a read-only view `core/object/proxy-readonly` +* Added a new bunch of constants `support` to check a runtime for features support `core/const` + ## v3.74.7 (2022-01-31) #### :bug: Bug Fix diff --git a/src/core/const/CHANGELOG.md b/src/core/const/CHANGELOG.md new file mode 100644 index 000000000..274e37dda --- /dev/null +++ b/src/core/const/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v3.??.?? (2022-??-??) + +#### :rocket: New Feature + +* Added a new bunch of constants `support` to check a runtime for features support diff --git a/src/core/const/props.ts b/src/core/const/props.ts index 967d76eaf..c9aa732d9 100644 --- a/src/core/const/props.ts +++ b/src/core/const/props.ts @@ -16,5 +16,6 @@ export const defProp = Object.freeze({ export const defReadonlyProp = Object.freeze({ configurable: true, enumerable: true, + writable: false, value: undefined }); diff --git a/src/core/const/support.ts b/src/core/const/support.ts new file mode 100644 index 000000000..b95edc8f9 --- /dev/null +++ b/src/core/const/support.ts @@ -0,0 +1,28 @@ +/*! + * V4Fire Core + * https://github.com/V4Fire/Core + * + * Released under the MIT license + * https://github.com/V4Fire/Core/blob/master/LICENSE + */ + +/** + * True if the runtime supports Proxy objects + */ +export const proxy = (() => { + try { + const obj = new Proxy({a: 1}, { + defineProperty(target: typeof obj, key: string, desc: PropertyDescriptor): boolean { + return Reflect.defineProperty(target, key, desc); + } + }); + + obj.a = 2; + + return Object.keys(obj).toString() === 'a'; + + } catch { + return false; + } +})(); + diff --git a/src/core/data/modules/base.ts b/src/core/data/modules/base.ts index 1b0f584e7..80946a8cb 100644 --- a/src/core/data/modules/base.ts +++ b/src/core/data/modules/base.ts @@ -9,6 +9,7 @@ import { EventEmitter2 as EventEmitter } from 'eventemitter2'; import symbolGenerator from 'core/symbol'; +import { readonly } from 'core/object/proxy-readonly'; import { deprecate } from 'core/functools'; import { concatURLs } from 'core/url'; @@ -370,7 +371,7 @@ export default abstract class Provider extends ParamsProvider implements IProvid } }); - res.data = Object.freeze(composition); + res.data = readonly(composition); return res; }), @@ -487,6 +488,9 @@ export default abstract class Provider extends ParamsProvider implements IProvid /** * Sets a readonly value by the specified key to the current provider + * + * @param key + * @param val */ protected setReadonlyParam(key: string, val: unknown): void { Object.defineProperty(this, key, { diff --git a/src/core/object/proxy-clone/CHANGELOG.md b/src/core/object/proxy-clone/CHANGELOG.md index 51d724091..a106d0713 100644 --- a/src/core/object/proxy-clone/CHANGELOG.md +++ b/src/core/object/proxy-clone/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :rocket: New Feature + +* Added a common function to clone + ## v3.73.2 (2021-12-28) #### :bug: Bug Fix diff --git a/src/core/object/proxy-clone/README.md b/src/core/object/proxy-clone/README.md index 2225be5b5..b59680d3d 100644 --- a/src/core/object/proxy-clone/README.md +++ b/src/core/object/proxy-clone/README.md @@ -15,7 +15,8 @@ const original = { } }; -const clone = proxyClone(original); +const + clone = proxyClone(original); clone.user.name = 'Jack'; clone.user.skills.push('boxing'); @@ -37,3 +38,33 @@ Because the process of cloning uses native Proxy objects, there are a few limita 2. `Object.isExtensible` always returns a value from the original object. 3. If the original object prevents extensions, then a cloned object will also prevent these extensions. 4. You can't redefine a property descriptor if it contains `configurable: false` attribute in the original object. + +## Common clone + +The module also exports a common implementation to clone objects. If the runtime supports Proxy API, it will be used. + +```js +import { clone } from 'core/object/proxy-clone'; + +const original = { + user: { + name: 'Bob', + age: 56, + skills: ['singing', 'dancing', 'programming'] + } +}; + +const + clone = clone(original); + +clone.user.name = 'Jack'; +clone.user.skills.push('boxing'); + +console.log(clone.user.name !== original.user.name); + +// ['singing', 'dancing', 'programming', 'boxing'] +console.log(clone.user.skills); + +// ['singing', 'dancing', 'programming'] +console.log(original.user.skills); +``` diff --git a/src/core/object/proxy-clone/index.ts b/src/core/object/proxy-clone/index.ts index 2eeb327c9..6e3d5015e 100644 --- a/src/core/object/proxy-clone/index.ts +++ b/src/core/object/proxy-clone/index.ts @@ -11,11 +11,23 @@ * @packageDocumentation */ +import * as support from 'core/const/support'; + import { unimplement } from 'core/functools/implementation'; import { toOriginalObject, NULL } from 'core/object/proxy-clone/const'; import { resolveTarget, getRawValueFromStore, Descriptor } from 'core/object/proxy-clone/helpers'; +/** + * Returns a clone of the specified object. + * If the runtime supports Proxy, it will be used to clone. + * + * @param obj + */ +export function clone(obj: T): T { + return support.proxy ? proxyClone(obj) : Object.fastClone(obj, {freezable: false}); +} + /** * Returns a clone of the specified object. * The function uses a Proxy object to create a clone. The process of cloning is a lazy operation. @@ -31,7 +43,7 @@ export default function proxyClone(obj: T): T { return obj; } - if (typeof Proxy !== 'function') { + if (!support.proxy) { unimplement({ name: 'proxyClone', type: 'function', diff --git a/src/core/object/proxy-readonly/CHANGELOG.md b/src/core/object/proxy-readonly/CHANGELOG.md index 14a92b484..74de05d80 100644 --- a/src/core/object/proxy-readonly/CHANGELOG.md +++ b/src/core/object/proxy-readonly/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :rocket: New Feature + +* Added a common function to create a read-only view + ## v3.73.0 (2021-12-14) #### :rocket: New Feature diff --git a/src/core/object/proxy-readonly/README.md b/src/core/object/proxy-readonly/README.md index 181393a8c..c53b936db 100644 --- a/src/core/object/proxy-readonly/README.md +++ b/src/core/object/proxy-readonly/README.md @@ -15,26 +15,27 @@ const original = { } }; -const clone = proxyReadonly(original); +const + readonly = proxyReadonly(original); try { - clone.user.name = 'Jack'; + readonly.user.name = 'Jack'; } catch (err) { console.log(err); } try { - clone.user.skills.push('boxing'); + readonly.user.skills.push('boxing'); } catch (err) { console.log(err); } -console.log(clone.user.name === original.user.name); +console.log(readonly.user.name === original.user.name); // ['singing', 'dancing', 'programming'] -console.log(clone.user.skills); +console.log(readonly.user.skills); // ['singing', 'dancing', 'programming'] console.log(original.user.skills); @@ -45,3 +46,28 @@ console.log(original.user.skills); Because the process of cloning uses native Proxy objects, there are a few limitations: 1. You can't use `Object.preventExtension` at a clone object because it should be applied to the original object. + +## Common readonly + +The module also exports a common implementation to make objects read-only. If the runtime supports Proxy API, it will be used. + +```js +import { readonly } from 'core/object/proxy-readonly'; + +const original = { + a: 1 +}; + +const + readonly = proxyReadonly(original); + +try { + readonly.a++; + +} catch (err) { + console.log(err); +} + +// 1 +console.log(readonly.a); +``` diff --git a/src/core/object/proxy-readonly/index.ts b/src/core/object/proxy-readonly/index.ts index 421529bdb..c5cee58bb 100644 --- a/src/core/object/proxy-readonly/index.ts +++ b/src/core/object/proxy-readonly/index.ts @@ -11,9 +11,21 @@ * @packageDocumentation */ +import * as support from 'core/const/support'; + import { unimplement } from 'core/functools/implementation'; import { READONLY, PROXY } from 'core/prelude/types/const'; +/** + * Returns a read-only view of the specified object. + * If the runtime supports Proxy, it will be used to create a view. + * + * @param obj + */ +export function readonly(obj: T): T { + return support.proxy ? proxyReadonly(obj) : Object.freeze(obj); +} + /** * Returns a read-only view of the specified object. * The function uses a Proxy object to create a view. @@ -28,7 +40,7 @@ export default function proxyReadonly(obj: T): Readonly { return obj; } - if (typeof Proxy !== 'function') { + if (!support.proxy) { unimplement({ name: 'proxyReadonly', type: 'function', diff --git a/src/core/object/watch/engines/index.ts b/src/core/object/watch/engines/index.ts index 1d38adb2e..5580f5d60 100644 --- a/src/core/object/watch/engines/index.ts +++ b/src/core/object/watch/engines/index.ts @@ -6,7 +6,9 @@ * https://github.com/V4Fire/Core/blob/master/LICENSE */ +import * as support from 'core/const/support'; + import * as proxyEngine from 'core/object/watch/engines/proxy'; import * as accEngine from 'core/object/watch/engines/accessors'; -export default typeof Proxy === 'function' ? proxyEngine : accEngine; +export default support.proxy ? proxyEngine : accEngine; diff --git a/src/core/prelude/object/clone/index.ts b/src/core/prelude/object/clone/index.ts index 1a8253015..8c2551a7e 100644 --- a/src/core/prelude/object/clone/index.ts +++ b/src/core/prelude/object/clone/index.ts @@ -31,6 +31,10 @@ extend(Object, 'fastClone', (obj, opts?: FastCloneOptions) => { const p = opts ?? {}; + if (Object.isFrozen(obj) && p.freezable !== false) { + return obj; + } + let clone; @@ -181,18 +185,19 @@ export function createSerializer( return (key, value) => { if (init && value === base) { - return objRef; - } + value = objRef; - if (!init) { - init = true; - } + } else { + if (!init) { + init = true; + } - if (replacer) { - return replacer(key, value); + if (replacer) { + value = replacer(key, value); + } } - return value; + return Object.unwrapProxy(value); }; } diff --git a/src/core/prelude/object/clone/spec.js b/src/core/prelude/object/clone/spec.js index bf091538b..75d60b060 100644 --- a/src/core/prelude/object/clone/spec.js +++ b/src/core/prelude/object/clone/spec.js @@ -80,7 +80,7 @@ describe('core/prelude/object/clone/fastClone', () => { obj = Object.freeze({a: 1, b: 2}), clone = Object.fastClone(obj, {freezable: true}); - expect(clone).not.toBe(obj); + expect(clone).toBe(obj); expect(clone).toEqual(obj); expect(Object.isFrozen(clone)).toBeTrue(); }); diff --git a/src/core/prelude/object/compare/index.ts b/src/core/prelude/object/compare/index.ts index 34a81697d..98f43c932 100644 --- a/src/core/prelude/object/compare/index.ts +++ b/src/core/prelude/object/compare/index.ts @@ -15,6 +15,9 @@ extend(Object, 'fastCompare', function fastCompare(a: unknown, b: unknown): bool return (b) => Object.fastCompare(a, b); } + a = Object.unwrapProxy(a); + b = Object.unwrapProxy(b); + const isEqual = a === b; @@ -63,18 +66,19 @@ extend(Object, 'fastCompare', function fastCompare(a: unknown, b: unknown): bool if (cantJSONCompare) { if ((isMap || isSet)) { const + setA = Object.cast>(a), setB = Object.cast>(b); - if (a.size !== setB.size) { + if (setA.size !== setB.size) { return false; } - if (a.size === 0) { + if (setA.size === 0) { return true; } const - aIter = a.entries(), + aIter = setA.entries(), bIter = setB.entries(); for (let aEl = aIter.next(), bEl = bIter.next(); !aEl.done; aEl = aIter.next(), bEl = bIter.next()) { @@ -98,12 +102,12 @@ extend(Object, 'fastCompare', function fastCompare(a: unknown, b: unknown): bool length2; if (isArr) { - length1 = a.length; + length1 = objA['length']; length2 = objB['length']; } else if (isMap || isSet) { - length1 = a.size; - length2 = objA['size']; + length1 = objA['size']; + length2 = objB['size']; } else { length1 = objA['length'] ?? Object.keys(objA).length; @@ -146,7 +150,7 @@ export function createSerializer( return (key, value) => { if (value == null) { init = true; - return value; + return Object.unwrapProxy(value); } const @@ -173,9 +177,9 @@ export function createSerializer( } if (isObj && (value instanceof Map || value instanceof Set)) { - return [...value.entries()]; + return [...Object.unwrapProxy(value).entries()]; } - return value; + return Object.unwrapProxy(value); }; } diff --git a/src/core/prelude/types/index.ts b/src/core/prelude/types/index.ts index bf4a11cc4..7c74b5b52 100644 --- a/src/core/prelude/types/index.ts +++ b/src/core/prelude/types/index.ts @@ -171,8 +171,14 @@ extend(Object, 'isPromiseLike', (value) => { /** @see [[ObjectConstructor.isProxy]] */ extend(Object, 'isProxy', (value) => value?.[PROXY] != null); -/** @see [[ObjectConstructor.isProxy]] */ -extend(Object, 'unwrapProxy', (value) => value?.[PROXY] ?? value); +/** @see [[ObjectConstructor.unwrapProxy]] */ +extend(Object, 'unwrapProxy', (value) => { + while (value?.[PROXY] && value[PROXY] !== value) { + value = value[PROXY]; + } + + return value; +}); /** @see [[ObjectConstructor.isMap]] */ extend(Object, 'isMap', (value) => value instanceof Map); diff --git a/src/core/request/response/CHANGELOG.md b/src/core/request/response/CHANGELOG.md index 88e3bc985..220d80098 100644 --- a/src/core/request/response/CHANGELOG.md +++ b/src/core/request/response/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :boom: Breaking Change + +* Now to clone and freeze server responses is used Proxy API if it supported + ## v3.72.0 (2021-12-10) #### :rocket: New Feature diff --git a/src/core/request/response/index.ts b/src/core/request/response/index.ts index 53c22e22d..6e63684d5 100644 --- a/src/core/request/response/index.ts +++ b/src/core/request/response/index.ts @@ -14,6 +14,9 @@ import Range from 'core/range'; import AbortablePromise from 'core/promise/abortable'; +import { readonly } from 'core/object/proxy-readonly'; +import { clone } from 'core/object/proxy-clone'; + import { IS_NODE } from 'core/env'; import { once } from 'core/functools'; import { convertIfDate } from 'core/json'; @@ -218,10 +221,10 @@ export default class Response< Object.defineProperty(data, 'valueOf', { configurable: true, - value: () => Object.fastClone(originalData, {freezable: false}) + value: () => clone(originalData) }); - Object.freeze(data); + data = readonly(data); } return data;