surrogate
Version:
Object method hooks made easy
300 lines (228 loc) • 19 kB
Markdown
# Surrogate
Easily create `Pre` and `Post` hooks on JavaScript objects.
## Install
`npm install surrogate`
or
`yarn add surrogate`
## Usage
There are a couple ways to manage Surrogate. The first is to utilize an exposed helper function, `wrapSurrogate`, to wrap objects and register pre and post methods through it.
```typescript
import { wrapSurrogate, Surrogate, NextParameters } from 'surrogate';
const guitar: Surrogate<Guitar> = wrapSurrogate(new Guitar(), options);
guitar.getSurrogate().registerPreHook('play', ({ next }: NextParameters<Guitar>) => {
// do things before running 'play'
next.next();
});
```
Check [examples](./examples) for expanded samples.
### SurrogateOptions
| Option | Type | Default Value | Description |
| ---------------- | ----------------------------- | :-----------: | ---------------------------------------------------------------------------------------------------------------------------------- |
| useSingleton? | boolean | true | Informs Surrogate to operate as a Singleton |
| useContext? | any | T | The context in which to call surrogate handlers. Handler specific contexts take precedence. |
| provide? | any | null | User provided content to pass to handlers and conditionals. Handler specific values take precedence |
| silenceErrors? | boolean \| SilenceErrors | false | Specify if Surrogate error output should be silenced. Accepts boolean or function that will receive the error and return a boolean |
| maintainContext? | boolean \| string \| string[] | false | Maintain context for methods without hooks. Can be a method name or array of method names |
### SurrogateMethods
After wrapping your instance with Surrogate new methods are available, `getSurrogate`, which, when called will return an instance of Surrogate Event Manager that allows management of pre and post methods and `disposeSurrogate` which will restore all methods, deregister hooks and remove the current instance from Surrogate management.
| Method | Parameters | Returns | Description |
| ---------------- | :--------: | --------------------- | ------------------------------------------------------ |
| getSurrogate | n/a | SurrogateEventManager | Provides capabilities for managing method hooks |
| disposeSurrogate | n/a | T | Cleans current instance of Surrogate handlers |
| bypassSurrogate | n/a | T | Allows calling target methods without running handlers |
### SurrogateEventManager
| Method | Parameters | Returns | Description |
| ------------------- | ------------------------------------------------------------------------------ | --------------------- | -------------------------------------------- |
| registerPreHook | (event: string, handler: SurrogateHandlers, options?: SurrogateHandlerOptions) | SurrogateEventManager | Registers a `pre` hook |
| registerPostHook | (event: string, handler: SurrogateHandlers, options?: SurrogateHandlerOptions) | SurrogateEventManager | Registers a `post` hook |
| deregisterPreHooks | (event: string) | SurrogateEventManager | Deregisters `pre` hooks for the given event |
| deregisterPostHooks | (event: string) | SurrogateEventManager | Deregisters `post` hooks for the given event |
| deregisterHooks | n/a | SurrogateEventManager | Removes all hooks for the current instance. |
SurrogateHandlers is any function that accepts a `NextParameters` object which can be used to control flow through pre and post hooks. It may also be the name of a method on the target object.
### CommonParameters
Common parameters passed to all handlers and conditional functions. (runConditions, runOnError, runOnBail)
| Property | Member Of | Type | Description |
| ----------------- | ------------------ | ----------- | -------------------------------------------------------------------------------------- |
| action | ProviderParameters | string | The current target method. |
| correlationId | ProviderParameters | string | Unique identifier for the current hook pipeline |
| instance | ProviderParameters | T | The current unwrapped instance |
| error? | ProviderParameters | Error | An error object, if passed |
| hookType | ProviderParameters | string | Provides the current hook type as a string. |
| receivedArguments | ProviderParameters | any[] | Arguments received from the previous handler. |
| currentArgs | ProviderParameters | any[] | Array of potentially modified arguments passed to original method invoked by surrogate |
| originalArgs | ProviderParameters | any[] | Array of arguments passed to the instance invoked method |
| timeTracker | ProviderParameters | TimeTracker | Provides access to the current time tracker |
| result | ProviderParameters | any | Result of original method invocation if run |
| provide | ProviderParameters | any | User provided content to pass to handlers and conditionals. Default value is `null` |
### NextParameters
`NextParameters` is passed to all hook handlers. In addition to `CommonParameters` you'll receive the current Surrogate wrapped instance and an `INext` object that provides functionality to skip hooks or continue to the next hook for execution.
| Property | Type | Description |
| --------- | --------- | ------------------------------------------------------ |
| surrogate | Surrogate | Provides handler access to surrogate wrapped instance. |
| next | INext | Object that provides flow control capabilities |
#### TimeTracker
| Method | Member Of | Parameters | Return Value | Description |
| ------------------- | :---------: | :--------: | :----------: | --------------------------------------------------- |
| geStartTime | TimeTracker | none | number | Returns the start time of the current pipeline. |
| getTotalDuration | TimeTracker | none | number | Returns the total duration of the current pipeline. |
| getHookStartTime | TimeTracker | none | number | Returns the start time of the current hook. |
| getLastRunDuration | TimeTracker | none | number | Returns the duration of the last hook. |
| getTimeSinceLastRun | TimeTracker | none | number | Returns the duration since the last hook completed. |
#### INext
| Method | Member Of | Parameters | Default Value | Description |
| -------- | :-------: | ------------------------------------ | :-----------: | -------------------------------------------------------------------------------------------- |
| skip | INext | (skipAmount?: number) | 1 | Method that will skip the next 'skipAmount' handlers |
| skipWith | INext | (skipAmount: number, ...args: any[]) | [] | Same as `skip` but will accept any number of arguments, passing to the next executed handler |
| next | INext | (nextOptions?: NextOptions) | n/a | Calling next may advance to the next hook. |
#### Next Options
| Property | Type | Description |
| --------- | ------- | ----------------------------------------------------------------------------------------------------------------------- |
| error? | Error | Passing an Error may result in the error being thrown, depending on supplied handler options |
| using? | any[] | An array of values to pass to the next handler |
| bail? | boolean | Indicates all subsequent handler executions should stop immediately. Target method is not called if bailing in pre hook |
| replace? | any | Replaces the original arguments passed to the target method. |
| bailWith? | any | If bailing, the supplied value should be returned |
### SurrogateHandlerOptions
When registering a hook you may provide any of the following options.
| Property | Type | Default Value | Description |
| -------------- | ------------------------------ | :-----------: | ----------------------------------------------------------------------------------------------------------------------- |
| useNext? | boolean | true | `true` indicates usage of the `INext` object to control flow, otherwise Surrogate makes a determination when to advance |
| noArgs? | boolean | false | Specify that `NextParameters` should NOT be passed to a handler |
| ignoreErrors? | boolean | false | If true and an Error is passed or caught Surrogate will not throw. |
| useContext? | any | T | The context in which to call surrogate handlers. |
| wrapper? | MethodWrappers | sync | Tells Surrogate if it is managing synchronous or asynchronous methods. |
| runConditions? | RunCondition \| RunCondition[] | n/a | Conditions to determine if a handler should be executed. |
| runOnError? | RunOnError \| RunOnError[] | n/a | Functions to run in the event of handler error. Runs regardless of ignoreError |
| runOnBail? | RunOnBail \| RunOnBail[] | n/a | Functions to run in the event of handler bailing. |
| priority? | number | 0 | Used to determine the order in which handlers are executed. Larger numbers have higher priority |
`useContext` option defaults to the current instance. Specifying context should be `surrogate` will allow hooks in the pipeline to trigger additional hooks. This can be extraordinarily useful but has the potential to cause recursive loops.
#### RunCondition
A RunCondition is a function that receives `RunConditionParameters`, which includes [CommonParameters](#-CommonParameters) and returns a boolean indicating if the current handler should be executed(`true`) or skipped(`false`). All run conditions are executed synchronously and all conditions must be true for the handler to execute.
| Property | Member Of | Type | Description |
| --------------------------- | ---------------------- | ------- | --------------------------------------------------- |
| didError | RunConditionParameters | boolean | Indicates if the previous handler passed an Error. |
| valueFromCondition | RunConditionParameters | any | Value passed forward from previous run condition |
| didReceiveFromLastCondition | RunConditionParameters | boolean | Indicates if the previous condition passed a value. |
| passToNextCondition() | RunConditionParameters | any | Function to pass a value to next condition |
#### RunOnError
`RunOnError` is a function that receives `RunOnErrorParameters`, which includes [CommonParameters](#CommonParameters) and the ability to recover from an error.
| Property | Member Of | Type | Description |
| ------------------ | -------------------- | ------- | ------------------------------------------------- |
| error | RunOnErrorParameters | Error | An error object received or caught from a handler |
| recoverFromError() | RunOnErrorParameters | boolean | Function to recover from an error |
#### RunOnBail
`RunOnBail` is a function that receives `RunOnBailParameters`, which includes [CommonParameters](#-CommonParameters) and the ability to recover from a bailing handler.
| Property | Member Of | Type | Description |
| ----------------- | ------------------- | ------- | ------------------------------------------ |
| bailWith() | RunOnBailParameters | any | Function that accepts a value to bail with |
| recoverFromBail() | RunOnBailParameters | boolean | Function to recover from a bailing handler |
---
## Decorators
Perhaps a more convenient way to register hooks is with decorators.
```typescript
import { SurrogateDelegate } from 'surrogate';
()
class Guitar {}
```
`SurrogateDelegate` registers your class and will automatically wrap instances of that class with Surrogate.
It supports all options from [`SurrogateOptions`](#-SurrogateOptions) as well as option `locateWith` which may be provided to assist Surrogate in
locating method decorators for a particular class. Should only be necessary if multiple class decorators are utilized.
```typescript
import { SurrogateDelegate } from 'surrogate';
({
locateWith: Guitar,
})
()
class Guitar {}
```
If you wish to use `Surrogate` [methods](#-SurrogateMethods) inside this instance of your class you must extend `SurrogateMethods` interface.
```typescript
import { SurrogateDelegate, SurrogateMethods } from 'surrogate';
export interface Guitar extends SurrogateMethods<Guitar> {}
()
export class Guitar {}
```
Registering hooks:
```typescript
import { NextParameters, SurrogatePre, SurrogatePost, SurrogateDelegate } from 'surrogate';
()
class Guitar {
(({ next }: NextParameters<Guitar>) => {
console.log(`Tuning guitar`);
next.next();
})
<Guitar>({
handler: () => {
console.log(`Put guitar away`);
},
options: {
useNext: false,
},
})
play() {
console.log(`playing guitar`);
}
}
```
### SurrogateDecoratorOptions
All `Surrogate[Async](Pre|Post)` decorators accept `SurrogateDecoratorOptions` or an array of options and must decorate the intended hooked method.
| Option | Type | Default Value | Description |
| :------: | ---------------------------------------------------- | :-----------: | --------------------------------------------------------------------------------- |
| handler | [SurrogateHandlers](##-SurrogateEventManager) | n/a | This function or array of functions will run before or after the decorated method |
| options? | [SurrogateHandlerOptions](#-SurrogateHandlerOptions) | {} | Options defining `Surrogate` handler behavior |
### NextDecoratorOptions
All `Next[Async](Pre|Post)` decorators accept `NextDecoratorOptions` or an array of options. Any method decorated with Next\* will be registered as a Surrogate handler. Therefore, you must supply the target method the Next\* method will run with.
| Option | Type | Default Value | Description |
| :------: | ---------------------------------------------------- | :-----------: | ----------------------------------------------------------------------- |
| action | keyof T \| string \| (keyof T \| string )[] | n/a | Name of the target decorated method, accepts regexp string for matching |
| options? | [SurrogateHandlerOptions](#-SurrogateHandlerOptions) | {} | Options defining `Surrogate` handler behavior |
```typescript
({
locateWith: Account,
})
({
timestamps: true,
})
class Account extends Model<Account> {
<Account>({
action: ['update', 'save'],
options: {
useNext: false,
},
})
protected async logActions({
instance: model,
originalArgs,
timeTracker,
hookType,
action,
}: NextParameters<Account>) {
const keys = (model.changed() || []) as (keyof Account)[];
const changes = keys.reduce(
(changed, key) => ({
...changed,
[key]: {
current: model.getDataValue(key),
previous: model.previous(key),
},
}),
{},
);
telemetry.trackEvent({
name: `${hookType}|${action}|Logging`,
properties: {
action,
changes,
hookType,
model: model.toJSON(),
arguments: originalArgs,
},
measurements: {
lastRunDuration: timeTracker.getDurationSinceLastRun(),
},
});
}
}
```
## Acknowledgements
Many thanks to [Dale](https://github.com/divmain) for transferring the npm package name.