Skip to content

Commit

Permalink
feat(extensions): added trace extension
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewcourtice committed Aug 4, 2021
1 parent 19a6335 commit 2835c45
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 28 deletions.
1 change: 0 additions & 1 deletion extensions/snapshot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"url": "https://github.com/andrewcourtice/harlem/issues"
},
"scripts": {
"dev": "microbundle watch --raw",
"build": "node build.js",
"prepublish": "yarn build"
},
Expand Down
67 changes: 67 additions & 0 deletions extensions/trace/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<p align="center">
<a href="https://harlemjs.com">
<img src="https://raw.githubusercontent.com/andrewcourtice/harlem/main/docs/src/.vuepress/public/assets/images/logo-192.svg" alt="Harlem"/>
</a>
</p>

# 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.

<!-- TOC depthfrom:2 -->

- [Getting started](#getting-started)

<!-- /TOC -->

## 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
```
10 changes: 10 additions & 0 deletions extensions/trace/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const path = require('path');
const build = require('@harlem/build');

(async () => {
const cwd = path.resolve('.');

return build(cwd, 'index', {
globalName: 'HarlemTraceExtension'
});
})();
43 changes: 43 additions & 0 deletions extensions/trace/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@harlem/extension-trace",
"amdName": "harlemTrace",
"version": "1.3.2",
"license": "MIT",
"author": "Andrew Courtice <[email protected]>",
"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"
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import isArray from '../type/is-array';
import isObject from '../type/is-object';
import clone from './clone';

type TraceGate<TValue extends object> = keyof ProxyHandler<TValue>;
type TraceCallback<TValue extends object> = (result: TraceResult<TValue>) => void;

interface TraceOptions<TValue extends object> {
gates: TraceGate<TValue>[];
paths: PropertyKey[];
hasGetGate: boolean;
}

interface TraceResult<TValue extends object> {
gate: TraceGate<TValue>;
paths: PropertyKey[];
oldValue: unknown;
newValue: unknown;
}

type GateMap<TValue extends object = any> = {
[TGate in TraceGate<TValue>]?: (callback: TraceCallback<TValue>, options: TraceOptions<TValue>) => ProxyHandler<TValue>[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) => {
Expand All @@ -46,7 +43,14 @@ const GATE_MAP = {
},
} as GateMap;

function defaultCallback<TValue extends object>(callback: TraceCallback<TValue>, gate: TraceGate<TValue>, paths: PropertyKey[], key: PropertyKey, oldValue: unknown, newValue?: unknown) {
function defaultCallback<TValue extends object>(
callback: TraceCallback<TValue>,
gate: TraceGate<TValue>,
paths: PropertyKey[],
key: PropertyKey,
oldValue: unknown,
newValue?: unknown,
) {
try {
callback({
gate,
Expand Down Expand Up @@ -81,7 +85,7 @@ function deepTrace<TValue extends object>(value: TValue, callback: TraceCallback
return new Proxy(value, handler);
}

export default function trace<TValue extends object>(value: TValue, gates: TraceGate<TValue> | TraceGate<TValue>[], callback: TraceCallback<TValue>): TValue {
function trace<TValue extends object>(value: TValue, gates: TraceGate<TValue> | TraceGate<TValue>[], callback: TraceCallback<TValue>): TValue {
const allGates = ([] as TraceGate<TValue>[]).concat(gates);
const hasGetGate = allGates.includes('get');

Expand All @@ -90,4 +94,46 @@ export default function trace<TValue extends object>(value: TValue, gates: Trace
gates: hasGetGate ? allGates : allGates.concat('get'),
paths: [],
});
}
}

export default function traceExtension<TState extends BaseState>(options?: Partial<Options>) {
const {
autoStart,
} = {
autoStart: false,
...options,
} as Options;

return (store: InternalStore<TState>) => {
const traceCallbacks = new Set<TraceCallback<TState>>();

function startTrace(gates: TraceGate<TState> | TraceGate<TState>[] = '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<TState>): TraceListener {
traceCallbacks.add(callback);

return {
dispose: () => traceCallbacks.delete(callback),
};
}

store.once(EVENTS.store.destroyed, () => traceCallbacks.clear());

if (autoStart) {
startTrace();
}

return {
startTrace,
stopTrace,
onTraceResult,
};
};
}

27 changes: 27 additions & 0 deletions extensions/trace/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export type TraceGate<TValue extends object> = keyof ProxyHandler<TValue>;
export type TraceCallback<TValue extends object> = (result: TraceResult<TValue>) => void;

export type GateMap<TValue extends object = any> = {
[TGate in TraceGate<TValue>]?: (callback: TraceCallback<TValue>, options: TraceOptions<TValue>) => ProxyHandler<TValue>[TGate];
}

export interface TraceOptions<TValue extends object> {
gates: TraceGate<TValue>[];
paths: PropertyKey[];
hasGetGate: boolean;
}

export interface TraceResult<TValue extends object> {
gate: TraceGate<TValue>;
paths: PropertyKey[];
oldValue: unknown;
newValue: unknown;
}

export interface TraceListener {
dispose(): void;
}

export interface Options {
autoStart: boolean;
}
52 changes: 52 additions & 0 deletions extensions/trace/test/trace.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});

});
7 changes: 7 additions & 0 deletions extensions/trace/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"include": [
"src/**/*.ts",
"../../global.d.ts"
]
}
1 change: 0 additions & 1 deletion packages/utilities/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 2835c45

Please sign in to comment.