From a343a8a235d9c3cf4f39b75c1cd2a81acfaa90c7 Mon Sep 17 00:00:00 2001 From: Andrew Courtice Date: Wed, 4 Aug 2021 12:08:41 +1000 Subject: [PATCH] feat(utilities): added trace function --- .eslintrc.js | 1 + packages/utilities/src/index.ts | 11 ++- packages/utilities/src/{ => object}/clone.ts | 4 +- .../utilities/src/{ => object}/overwrite.ts | 0 packages/utilities/src/object/trace.ts | 93 +++++++++++++++++++ packages/utilities/src/{ => task}/task.ts | 2 +- packages/utilities/src/{ => type}/get-type.ts | 2 +- packages/utilities/src/type/is-array.ts | 5 + packages/utilities/src/type/is-object.ts | 5 + packages/utilities/test/clone.test.ts | 2 +- packages/utilities/test/overwrite.test.ts | 2 +- 11 files changed, 118 insertions(+), 9 deletions(-) rename packages/utilities/src/{ => object}/clone.ts (96%) rename packages/utilities/src/{ => object}/overwrite.ts (100%) create mode 100644 packages/utilities/src/object/trace.ts rename packages/utilities/src/{ => task}/task.ts (99%) rename packages/utilities/src/{ => type}/get-type.ts (90%) create mode 100644 packages/utilities/src/type/is-array.ts create mode 100644 packages/utilities/src/type/is-object.ts diff --git a/.eslintrc.js b/.eslintrc.js index 21acd09d..8eab0fcb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -53,5 +53,6 @@ module.exports = { '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/ban-types': 'warn', }, }; \ No newline at end of file diff --git a/packages/utilities/src/index.ts b/packages/utilities/src/index.ts index 1a0b0c40..99699d0b 100644 --- a/packages/utilities/src/index.ts +++ b/packages/utilities/src/index.ts @@ -1,6 +1,11 @@ -export { default as Task } from './task'; +export { default as Task } from './task/task'; -export { default as clone } from './clone'; -export { default as overwrite } from './overwrite'; +export { default as clone } from './object/clone'; +export { default as overwrite } from './object/overwrite'; +export { default as trace } from './object/trace'; + +export { default as getType } from './type/get-type'; +export { default as isArray } from './type/is-array'; +export { default as isObject } from './type/is-object'; export * from './types'; \ No newline at end of file diff --git a/packages/utilities/src/clone.ts b/packages/utilities/src/object/clone.ts similarity index 96% rename from packages/utilities/src/clone.ts rename to packages/utilities/src/object/clone.ts index 5ffec71f..3847df3c 100644 --- a/packages/utilities/src/clone.ts +++ b/packages/utilities/src/object/clone.ts @@ -1,9 +1,9 @@ -import getType from './get-type'; +import getType from '../type/get-type'; import type { Constructable, RuntimeType, -} from './types'; +} from '../types'; function cloneIdentity(input: unknown): unknown { return input; diff --git a/packages/utilities/src/overwrite.ts b/packages/utilities/src/object/overwrite.ts similarity index 100% rename from packages/utilities/src/overwrite.ts rename to packages/utilities/src/object/overwrite.ts diff --git a/packages/utilities/src/object/trace.ts b/packages/utilities/src/object/trace.ts new file mode 100644 index 00000000..b090c3fa --- /dev/null +++ b/packages/utilities/src/object/trace.ts @@ -0,0 +1,93 @@ +import isArray from '../type/is-array'; +import isObject from '../type/is-object'; +import clone from './clone'; + +type TraceGate = keyof ProxyHandler; +type TraceCallback = (result: TraceResult) => void; + +interface TraceOptions { + gates: TraceGate[]; + paths: PropertyKey[]; + hasGetGate: boolean; +} + +interface TraceResult { + gate: TraceGate; + paths: PropertyKey[]; + oldValue: unknown; + newValue: unknown; +} + +type GateMap = { + [TGate in TraceGate]?: (callback: TraceCallback, options: TraceOptions) => ProxyHandler[TGate]; +} + +const GATE_MAP = { + get: (callback, { hasGetGate, gates, paths }) => (target, prop, receiver) => { + const value = Reflect.get(target, prop, receiver); + + if (hasGetGate) { + defaultCallback(callback, 'get', paths, prop, value, value); + } + + return deepTrace(value, callback, { + gates, + hasGetGate, + paths: paths.concat(prop), + }); + }, + set: (callback, { paths }) => (target, prop, value, receiver) => { + defaultCallback(callback, 'set', paths, prop, target[prop], value); + return Reflect.set(target, prop, value, receiver); + }, + deleteProperty: (callback, { paths }) => (target, prop) => { + defaultCallback(callback, 'deleteProperty', paths, prop, target[prop]); + return Reflect.deleteProperty(target, prop); + }, +} as GateMap; + +function defaultCallback(callback: TraceCallback, gate: TraceGate, paths: PropertyKey[], key: PropertyKey, oldValue: unknown, newValue?: unknown) { + try { + callback({ + gate, + newValue, + paths: paths.concat(key), + oldValue: newValue === oldValue ? oldValue : clone(oldValue), + }); + } catch { + console.warn('Trace callback failed'); + } +} + +function deepTrace(value: TValue, callback: TraceCallback, options: TraceOptions): TValue { + if (!isObject(value) && !isArray(value)) { + return value; + } + + const { + gates, + } = options; + + const handler = gates.reduce((output, gate) => { + const gateHandler = (GATE_MAP as GateMap)[gate]; + + if (gateHandler) { + output[gate] = gateHandler(callback, options)?.bind(output) as any; + } + + return output; + }, {} as ProxyHandler); + + return new Proxy(value, handler); +} + +export default function trace(value: TValue, gates: TraceGate | TraceGate[], callback: TraceCallback): TValue { + const allGates = ([] as TraceGate[]).concat(gates); + const hasGetGate = allGates.includes('get'); + + return deepTrace(value, callback, { + hasGetGate, + gates: hasGetGate ? allGates : allGates.concat('get'), + paths: [], + }); +} \ No newline at end of file diff --git a/packages/utilities/src/task.ts b/packages/utilities/src/task/task.ts similarity index 99% rename from packages/utilities/src/task.ts rename to packages/utilities/src/task/task.ts index 5088ac38..cf8bb50f 100644 --- a/packages/utilities/src/task.ts +++ b/packages/utilities/src/task/task.ts @@ -2,7 +2,7 @@ import type { Product, TaskAbortCallback, TaskExecutor, -} from './types'; +} from '../types'; function safeRun(bodyInvokee: Product, finallyInvokee: Product): Product { return (...args: any[]) => { diff --git a/packages/utilities/src/get-type.ts b/packages/utilities/src/type/get-type.ts similarity index 90% rename from packages/utilities/src/get-type.ts rename to packages/utilities/src/type/get-type.ts index ad909ed7..0919ec2e 100644 --- a/packages/utilities/src/get-type.ts +++ b/packages/utilities/src/type/get-type.ts @@ -1,6 +1,6 @@ import type { RuntimeType, -} from './types'; +} from '../types'; export default function getType(input: unknown): RuntimeType { return Object.prototype.toString.call(input).slice(8, -1).toLowerCase() as RuntimeType; diff --git a/packages/utilities/src/type/is-array.ts b/packages/utilities/src/type/is-array.ts new file mode 100644 index 00000000..f8b11cd2 --- /dev/null +++ b/packages/utilities/src/type/is-array.ts @@ -0,0 +1,5 @@ +import getType from './get-type'; + +export default function isArray(value: unknown): value is unknown[] { + return getType(value) === 'array'; +} \ No newline at end of file diff --git a/packages/utilities/src/type/is-object.ts b/packages/utilities/src/type/is-object.ts new file mode 100644 index 00000000..2425723a --- /dev/null +++ b/packages/utilities/src/type/is-object.ts @@ -0,0 +1,5 @@ +import getType from './get-type'; + +export default function isObject(value: unknown): value is object { + return getType(value) === 'object'; +} \ No newline at end of file diff --git a/packages/utilities/test/clone.test.ts b/packages/utilities/test/clone.test.ts index 57fb24a3..812dd2b1 100644 --- a/packages/utilities/test/clone.test.ts +++ b/packages/utilities/test/clone.test.ts @@ -1,4 +1,4 @@ -import clone from '../src/clone'; +import clone from '../src/object/clone'; function getSimpleTypes(): Record { return { diff --git a/packages/utilities/test/overwrite.test.ts b/packages/utilities/test/overwrite.test.ts index a9431bdb..2bc4634a 100644 --- a/packages/utilities/test/overwrite.test.ts +++ b/packages/utilities/test/overwrite.test.ts @@ -1,4 +1,4 @@ -import overwrite from '../src/overwrite'; +import overwrite from '../src/object/overwrite'; describe('Utilities', () => {