From 2835c4581faa023f4c38b3e8e4414452d9131132 Mon Sep 17 00:00:00 2001 From: Andrew Courtice Date: Wed, 4 Aug 2021 15:19:17 +1000 Subject: [PATCH] feat(extensions): added trace extension --- extensions/snapshot/package.json | 1 - extensions/trace/README.md | 67 +++++++++++++ extensions/trace/build.js | 10 ++ extensions/trace/package.json | 43 ++++++++ .../trace.ts => extensions/trace/src/index.ts | 98 ++++++++++++++----- extensions/trace/src/types.ts | 27 +++++ extensions/trace/test/trace.test.ts | 52 ++++++++++ extensions/trace/tsconfig.json | 7 ++ packages/utilities/src/index.ts | 1 - 9 files changed, 278 insertions(+), 28 deletions(-) create mode 100644 extensions/trace/README.md create mode 100644 extensions/trace/build.js create mode 100644 extensions/trace/package.json rename packages/utilities/src/object/trace.ts => extensions/trace/src/index.ts (52%) create mode 100644 extensions/trace/src/types.ts create mode 100644 extensions/trace/test/trace.test.ts create mode 100644 extensions/trace/tsconfig.json diff --git a/extensions/snapshot/package.json b/extensions/snapshot/package.json index 12a193a8..620b7e35 100644 --- a/extensions/snapshot/package.json +++ b/extensions/snapshot/package.json @@ -27,7 +27,6 @@ "url": "https://github.com/andrewcourtice/harlem/issues" }, "scripts": { - "dev": "microbundle watch --raw", "build": "node build.js", "prepublish": "yarn build" }, diff --git a/extensions/trace/README.md b/extensions/trace/README.md new file mode 100644 index 00000000..95d7e2df --- /dev/null +++ b/extensions/trace/README.md @@ -0,0 +1,67 @@ +

+ + Harlem + +

+ +# Harlem Snapshot Plugin + +![npm](https://img.shields.io/npm/v/@harlem/plugin-snapshot) + +This is the official Harlem plugin for taking state snapshots and applying them when convenient. + + + +- [Getting started](#getting-started) + + + +## Getting started + +Before installing the snapshot plugin make sure you have installed `@harlem/core`. + +1. Install `@harlem/plugin-snapshot`: +``` +npm install @harlem/plugin-snapshot +``` +Or if you're using Yarn: +``` +yarn add @harlem/plugin-snapshot +``` + +2. Create an instance of the plugin and register it with Harlem: +```typescript +import App from './app.vue'; + +import harlem from '@harlem/core'; +import createSnapshotPlugin from '@harlem/plugin-snapshot'; + +createApp(App) + .use(harlem, { + plugins: [ + createSnapshotPlugin() + ] + }) + .mount('#app'); +``` + +3. Call the snapshot method with the name of the store you wish to snapshot: +```typescript +import { + snapshot +} from '@harlem/plugin-snapshot'; + +export default function() { + const snap = snapshot('my-store'); + + // ... +} +``` + +4. Apply the snapshot: +```typescript +const snap = snapshot('my-store'); + +snap.apply(); // Apply the snapshot over the top of current state +snap.apply(true) // Replace state with the current snapshot +``` \ No newline at end of file diff --git a/extensions/trace/build.js b/extensions/trace/build.js new file mode 100644 index 00000000..a49d96fa --- /dev/null +++ b/extensions/trace/build.js @@ -0,0 +1,10 @@ +const path = require('path'); +const build = require('@harlem/build'); + +(async () => { + const cwd = path.resolve('.'); + + return build(cwd, 'index', { + globalName: 'HarlemTraceExtension' + }); +})(); \ No newline at end of file diff --git a/extensions/trace/package.json b/extensions/trace/package.json new file mode 100644 index 00000000..387f11cd --- /dev/null +++ b/extensions/trace/package.json @@ -0,0 +1,43 @@ +{ + "name": "@harlem/extension-trace", + "amdName": "harlemTrace", + "version": "1.3.2", + "license": "MIT", + "author": "Andrew Courtice ", + "description": "The official trace extension for Harlem", + "homepage": "https://harlemjs.com", + "main": "dist/index.cjs.js", + "module": "dist/index.bundler.esm.js", + "unpkg": "dist/index.min.js", + "types": "dist/index.d.ts", + "source": "src/index.ts", + "keywords": [ + "vue", + "state", + "harlem", + "extension", + "trace" + ], + "repository": { + "type": "git", + "url": "https://github.com/andrewcourtice/harlem.git", + "directory": "extensions/trace" + }, + "bugs": { + "url": "https://github.com/andrewcourtice/harlem/issues" + }, + "scripts": { + "build": "node build.js", + "prepublish": "yarn build" + }, + "peerDependencies": { + "@harlem/core": "^1.1.2" + }, + "dependencies": { + "@harlem/utilities": "^1.3.2" + }, + "devDependencies": { + "@harlem/core": "^1.3.2", + "@harlem/testing": "^1.3.2" + } +} diff --git a/packages/utilities/src/object/trace.ts b/extensions/trace/src/index.ts similarity index 52% rename from packages/utilities/src/object/trace.ts rename to extensions/trace/src/index.ts index b090c3fa..da2c084f 100644 --- a/packages/utilities/src/object/trace.ts +++ b/extensions/trace/src/index.ts @@ -1,26 +1,23 @@ -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]; -} +import { + EVENTS, + BaseState, + InternalStore, +} from '@harlem/core'; + +import { + clone, + isArray, + isObject, +} from '@harlem/utilities'; + +import type { + GateMap, + Options, + TraceCallback, + TraceGate, + TraceListener, + TraceOptions, +} from './types'; const GATE_MAP = { get: (callback, { hasGetGate, gates, paths }) => (target, prop, receiver) => { @@ -46,7 +43,14 @@ const GATE_MAP = { }, } as GateMap; -function defaultCallback(callback: TraceCallback, gate: TraceGate, paths: PropertyKey[], key: PropertyKey, oldValue: unknown, newValue?: unknown) { +function defaultCallback( + callback: TraceCallback, + gate: TraceGate, + paths: PropertyKey[], + key: PropertyKey, + oldValue: unknown, + newValue?: unknown, +) { try { callback({ gate, @@ -81,7 +85,7 @@ function deepTrace(value: TValue, callback: TraceCallback return new Proxy(value, handler); } -export default function trace(value: TValue, gates: TraceGate | TraceGate[], callback: TraceCallback): TValue { +function trace(value: TValue, gates: TraceGate | TraceGate[], callback: TraceCallback): TValue { const allGates = ([] as TraceGate[]).concat(gates); const hasGetGate = allGates.includes('get'); @@ -90,4 +94,46 @@ export default function trace(value: TValue, gates: Trace gates: hasGetGate ? allGates : allGates.concat('get'), paths: [], }); -} \ No newline at end of file +} + +export default function traceExtension(options?: Partial) { + const { + autoStart, + } = { + autoStart: false, + ...options, + } as Options; + + return (store: InternalStore) => { + const traceCallbacks = new Set>(); + + function startTrace(gates: TraceGate | TraceGate[] = 'set') { + store.provider('write', state => trace(state, gates, result => traceCallbacks.forEach(callback => callback(result)))); + } + + function stopTrace() { + store.provider('write', state => state); + } + + function onTraceResult(callback: TraceCallback): TraceListener { + traceCallbacks.add(callback); + + return { + dispose: () => traceCallbacks.delete(callback), + }; + } + + store.once(EVENTS.store.destroyed, () => traceCallbacks.clear()); + + if (autoStart) { + startTrace(); + } + + return { + startTrace, + stopTrace, + onTraceResult, + }; + }; +} + diff --git a/extensions/trace/src/types.ts b/extensions/trace/src/types.ts new file mode 100644 index 00000000..bc745fa1 --- /dev/null +++ b/extensions/trace/src/types.ts @@ -0,0 +1,27 @@ +export type TraceGate = keyof ProxyHandler; +export type TraceCallback = (result: TraceResult) => void; + +export type GateMap = { + [TGate in TraceGate]?: (callback: TraceCallback, options: TraceOptions) => ProxyHandler[TGate]; +} + +export interface TraceOptions { + gates: TraceGate[]; + paths: PropertyKey[]; + hasGetGate: boolean; +} + +export interface TraceResult { + gate: TraceGate; + paths: PropertyKey[]; + oldValue: unknown; + newValue: unknown; +} + +export interface TraceListener { + dispose(): void; +} + +export interface Options { + autoStart: boolean; +} \ No newline at end of file diff --git a/extensions/trace/test/trace.test.ts b/extensions/trace/test/trace.test.ts new file mode 100644 index 00000000..ef2fa2f6 --- /dev/null +++ b/extensions/trace/test/trace.test.ts @@ -0,0 +1,52 @@ +import { + getStore, + bootstrap, +} from '@harlem/testing'; + +import traceExtension from '../src'; + +describe('Trace Extension', () => { + + const getInstance = () => getStore({ + extensions: [ + traceExtension(), + ], + }); + + let instance = getInstance(); + + beforeAll(() => bootstrap()); + beforeEach(() => instance = getInstance()); + afterEach(() => instance.store.destroy()); + + test('Run a trace', () => { + const { + store, + setUserID, + setUserDetails, + } = instance; + + const { + startTrace, + stopTrace, + onTraceResult, + } = store; + + const callback = jest.fn(); + + onTraceResult(callback); + startTrace(); + + setUserID(5); + setUserDetails({ + firstName: 'John', + lastName: 'Smith', + age: 35, + }); + + stopTrace(); + + expect(callback).toHaveBeenCalledTimes(4); + }); + +}); \ No newline at end of file diff --git a/extensions/trace/tsconfig.json b/extensions/trace/tsconfig.json new file mode 100644 index 00000000..7d5ec66e --- /dev/null +++ b/extensions/trace/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "src/**/*.ts", + "../../global.d.ts" + ] +} \ No newline at end of file diff --git a/packages/utilities/src/index.ts b/packages/utilities/src/index.ts index 99699d0b..9d992f40 100644 --- a/packages/utilities/src/index.ts +++ b/packages/utilities/src/index.ts @@ -2,7 +2,6 @@ export { default as Task } from './task/task'; 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';