@thi.ng/atom
Advanced tools
Comparing version 0.9.0 to 0.9.1
@@ -6,7 +6,8 @@ import * as api from "@thi.ng/api/api"; | ||
export declare type ViewTransform<T> = (x: any) => T; | ||
export declare type InterceptorFn = (state: any, e: Event) => any; | ||
export declare type InterceptorPredicate = (state: any, e: Event) => boolean; | ||
export declare type InterceptorFn = (state: any, e: Event, fx?: any) => any; | ||
export declare type InterceptorPredicate = (state: any, e: Event, fx?: any) => boolean; | ||
export declare type SideEffect = (x: any) => void; | ||
export declare type EventDef = Interceptor | Interceptor[] | InterceptorFn | InterceptorFn[]; | ||
export declare type EffectDef = [SideEffect, number]; | ||
export declare type EventDef = Interceptor | InterceptorFn | (Interceptor | InterceptorFn)[]; | ||
export declare type EffectDef = SideEffect | [SideEffect, number]; | ||
export declare type EffectPriority = [string, number]; | ||
export interface ReadonlyAtom<T> extends api.IDeref<T>, api.IWatch<T> { | ||
@@ -13,0 +14,0 @@ } |
@@ -6,2 +6,10 @@ # Change Log | ||
<a name="0.9.1"></a> | ||
## [0.9.1](https://github.com/thi-ng/umbrella/compare/@thi.ng/atom@0.9.0...@thi.ng/atom@0.9.1) (2018-03-07) | ||
**Note:** Version bump only for package @thi.ng/atom | ||
<a name="0.9.0"></a> | ||
@@ -8,0 +16,0 @@ # [0.9.0](https://github.com/thi-ng/umbrella/compare/@thi.ng/atom@0.8.0...@thi.ng/atom@0.9.0) (2018-03-07) |
import { IObjectOf } from "@thi.ng/api/api"; | ||
import { DCons } from "@thi.ng/dcons"; | ||
import * as api from "./api"; | ||
/** | ||
* Batched event processor for using composable interceptors for event handling | ||
* and side effects to execute the result of handled events. | ||
* | ||
* In this model an event handler is an array of objects with `pre` and/or `post` | ||
* keys and functions attached to each key. These functions are called interceptors, | ||
* since each intercepts the processing of an event and can contribute their | ||
* own side effects. The outcome of this setup is a more aspect-oriented, composable | ||
* approach to event handling and allows to inject common, re-usable behaviors | ||
* for multiple event types (tracing, validation, undo/redo triggers etc.) | ||
* | ||
* The overall approach of this type of event processing is heavily based on the | ||
* pattern initially pioneered by @Day8/re-frame, with the following differences: | ||
* | ||
* - standalone implementation (no assumptions about surrounding context/framework) | ||
* - manual trigger of event queue processing | ||
* - supports event cancellation | ||
* - side effect collection (multiple side effects for same effect type per frame) | ||
* - side effect priorities (to better control execution order) | ||
* - dynamic addition/removal of handlers & effects | ||
* | ||
*/ | ||
export declare class EventBus { | ||
@@ -10,3 +31,3 @@ state: api.IAtom<any>; | ||
effects: IObjectOf<api.SideEffect>; | ||
priorites: DCons<[number, string]>; | ||
priorities: api.EffectPriority[]; | ||
constructor(state: api.IAtom<any>, handlers?: IObjectOf<api.EventDef>, effects?: IObjectOf<api.EffectDef>); | ||
@@ -17,8 +38,71 @@ addHandler(id: string, spec: api.EventDef): void; | ||
addEffects(specs: IObjectOf<api.EffectDef>): void; | ||
removeHandler(id: string): void; | ||
removeHandlers(ids: string[]): void; | ||
removeEffect(id: string): void; | ||
removeEffects(ids: string[]): void; | ||
/** | ||
* Adds given event to event queue to be processed | ||
* by `processQueue()` later on. | ||
* | ||
* @param e | ||
*/ | ||
dispatch(e: api.Event): void; | ||
/** | ||
* Adds given event to whatever is the current | ||
* event queue. If triggered via the `FX_DISPATCH_NOW` | ||
* side effect the event will still be executed | ||
* in the currently active batch. If called from | ||
* elsewhere, the result is the same as calling | ||
* `dispatch()`. | ||
* | ||
* @param e | ||
*/ | ||
dispatchNow(e: api.Event): void; | ||
/** | ||
* Triggers processing of current event queue and | ||
* returns `true` if the any of the processed events | ||
* caused a state change. | ||
* | ||
* If an event handler triggers the `FX_DISPATCH_NOW` | ||
* side effect, the new event will be added to the | ||
* currently processed batch and therefore executed | ||
* in the same frame. Also see `dispatchNow()`. | ||
*/ | ||
processQueue(): boolean; | ||
/** | ||
* Processes a single event using the configured handler/interceptor chain. | ||
* Logs warning message and skips processing if no handler | ||
* is available for the event. | ||
* | ||
* This function processes the array of interceptors in bi-directional | ||
* order. First any `pre` interceptors are processed in | ||
* forward order. Then `post` interceptors are processed in reverse. | ||
* | ||
* Each interceptor can return a result object of side effects, | ||
* which are being merged and collected for `processEffects()`. | ||
* | ||
* Any interceptor can trigger zero or more known side effects, | ||
* each (side effect) will be collected in an array to support | ||
* multiple invocations of the same effect type per frame. If no | ||
* side effects are requested, an interceptor can return `undefined`. | ||
* | ||
* Processing of the current event stops immediatedly, if an | ||
* interceptor includes the `FX_CANCEL` side effect. However, the | ||
* results interceptors (incl. the one which cancelled) are kept and | ||
* processed further as usual. | ||
* | ||
* @param fx | ||
* @param e | ||
*/ | ||
protected processEvent(fx: any, e: api.Event): void; | ||
/** | ||
* Takes a collection of side effects generated during | ||
* event processing and applies them in order of configured | ||
* priorities. | ||
* | ||
* @param fx | ||
*/ | ||
protected processEffects(fx: any): void; | ||
protected mergeEffects(fx: any, ret: any): void; | ||
protected insertPriority(p: api.EffectPriority): void; | ||
} |
138
event-bus.js
@@ -5,4 +5,25 @@ "use strict"; | ||
const is_function_1 = require("@thi.ng/checks/is-function"); | ||
const dcons_1 = require("@thi.ng/dcons"); | ||
const api = require("./api"); | ||
/** | ||
* Batched event processor for using composable interceptors for event handling | ||
* and side effects to execute the result of handled events. | ||
* | ||
* In this model an event handler is an array of objects with `pre` and/or `post` | ||
* keys and functions attached to each key. These functions are called interceptors, | ||
* since each intercepts the processing of an event and can contribute their | ||
* own side effects. The outcome of this setup is a more aspect-oriented, composable | ||
* approach to event handling and allows to inject common, re-usable behaviors | ||
* for multiple event types (tracing, validation, undo/redo triggers etc.) | ||
* | ||
* The overall approach of this type of event processing is heavily based on the | ||
* pattern initially pioneered by @Day8/re-frame, with the following differences: | ||
* | ||
* - standalone implementation (no assumptions about surrounding context/framework) | ||
* - manual trigger of event queue processing | ||
* - supports event cancellation | ||
* - side effect collection (multiple side effects for same effect type per frame) | ||
* - side effect priorities (to better control execution order) | ||
* - dynamic addition/removal of handlers & effects | ||
* | ||
*/ | ||
class EventBus { | ||
@@ -14,3 +35,3 @@ constructor(state, handlers, effects) { | ||
this.eventQueue = []; | ||
this.priorites = new dcons_1.DCons(); | ||
this.priorities = []; | ||
this.addEffect(api.FX_STATE, (x) => this.state.reset(x), -1000); | ||
@@ -43,15 +64,78 @@ this.addEffect(api.FX_DISPATCH, (e) => this.dispatch(e), -999); | ||
this.effects[id] = fx; | ||
this.priorites.insertSorted([priority, id], (a, b) => a[0] - b[0]); | ||
const p = [id, priority]; | ||
const priors = this.priorities; | ||
for (let i = 0; i < priors.length; i++) { | ||
if (p[1] < priors[i][1]) { | ||
priors.splice(i, 0, p); | ||
return; | ||
} | ||
} | ||
priors.push(p); | ||
} | ||
addEffects(specs) { | ||
for (let id in specs) { | ||
this.addEffect(id, specs[id][0], specs[id][1]); | ||
const fx = specs[id]; | ||
if (is_array_1.isArray(fx)) { | ||
this.addEffect(id, fx[0], fx[1]); | ||
} | ||
else { | ||
this.addEffect(id, fx); | ||
} | ||
} | ||
} | ||
removeHandler(id) { | ||
delete this.handlers[id]; | ||
} | ||
removeHandlers(ids) { | ||
for (let id of ids) { | ||
this.removeHandler(id); | ||
} | ||
} | ||
removeEffect(id) { | ||
delete this.effects[id]; | ||
const p = this.priorities; | ||
for (let i = p.length - 1; i >= 0; i--) { | ||
if (id === p[i][0]) { | ||
p.splice(i, 1); | ||
return; | ||
} | ||
} | ||
} | ||
removeEffects(ids) { | ||
for (let id of ids) { | ||
this.removeEffect(id); | ||
} | ||
} | ||
/** | ||
* Adds given event to event queue to be processed | ||
* by `processQueue()` later on. | ||
* | ||
* @param e | ||
*/ | ||
dispatch(e) { | ||
this.eventQueue.push(e); | ||
} | ||
/** | ||
* Adds given event to whatever is the current | ||
* event queue. If triggered via the `FX_DISPATCH_NOW` | ||
* side effect the event will still be executed | ||
* in the currently active batch. If called from | ||
* elsewhere, the result is the same as calling | ||
* `dispatch()`. | ||
* | ||
* @param e | ||
*/ | ||
dispatchNow(e) { | ||
(this.currQueue || this.eventQueue).push(e); | ||
} | ||
/** | ||
* Triggers processing of current event queue and | ||
* returns `true` if the any of the processed events | ||
* caused a state change. | ||
* | ||
* If an event handler triggers the `FX_DISPATCH_NOW` | ||
* side effect, the new event will be added to the | ||
* currently processed batch and therefore executed | ||
* in the same frame. Also see `dispatchNow()`. | ||
*/ | ||
processQueue() { | ||
@@ -72,4 +156,33 @@ if (this.eventQueue.length > 0) { | ||
} | ||
/** | ||
* Processes a single event using the configured handler/interceptor chain. | ||
* Logs warning message and skips processing if no handler | ||
* is available for the event. | ||
* | ||
* This function processes the array of interceptors in bi-directional | ||
* order. First any `pre` interceptors are processed in | ||
* forward order. Then `post` interceptors are processed in reverse. | ||
* | ||
* Each interceptor can return a result object of side effects, | ||
* which are being merged and collected for `processEffects()`. | ||
* | ||
* Any interceptor can trigger zero or more known side effects, | ||
* each (side effect) will be collected in an array to support | ||
* multiple invocations of the same effect type per frame. If no | ||
* side effects are requested, an interceptor can return `undefined`. | ||
* | ||
* Processing of the current event stops immediatedly, if an | ||
* interceptor includes the `FX_CANCEL` side effect. However, the | ||
* results interceptors (incl. the one which cancelled) are kept and | ||
* processed further as usual. | ||
* | ||
* @param fx | ||
* @param e | ||
*/ | ||
processEvent(fx, e) { | ||
const iceps = this.handlers[e[0]]; | ||
if (!iceps) { | ||
console.warn(`missing handler for event type: ${e[0]}`); | ||
return; | ||
} | ||
const n = iceps.length - 1; | ||
@@ -80,3 +193,3 @@ let hasPost = false; | ||
if (icep.pre) { | ||
this.mergeEffects(fx, icep.pre(fx.state, e)); | ||
this.mergeEffects(fx, icep.pre(fx.state, e, fx)); | ||
} | ||
@@ -91,10 +204,17 @@ hasPost = hasPost || !!icep.post; | ||
if (icep.post) { | ||
this.mergeEffects(fx, icep.post(fx.state, e)); | ||
this.mergeEffects(fx, icep.post(fx.state, e, fx)); | ||
} | ||
} | ||
} | ||
/** | ||
* Takes a collection of side effects generated during | ||
* event processing and applies them in order of configured | ||
* priorities. | ||
* | ||
* @param fx | ||
*/ | ||
processEffects(fx) { | ||
const effects = this.effects; | ||
for (let p of this.priorites) { | ||
const id = p[1]; | ||
for (let p of this.priorities) { | ||
const id = p[0]; | ||
const val = fx[id]; | ||
@@ -130,3 +250,5 @@ if (val !== undefined) { | ||
} | ||
insertPriority(p) { | ||
} | ||
} | ||
exports.EventBus = EventBus; |
@@ -1,5 +0,52 @@ | ||
import { InterceptorFn, InterceptorPredicate } from "./api"; | ||
export declare const trace: (_: any, e: any) => void; | ||
export declare const ensurePred: (pred: InterceptorPredicate, err?: InterceptorFn) => (state: any, e: any) => any; | ||
export declare const ensureLessThan: (max: number, err: InterceptorFn) => (state: any, e: any) => any; | ||
export declare const ensureGreaterThan: (min: number, err: InterceptorFn) => (state: any, e: any) => any; | ||
import { Event, InterceptorFn, InterceptorPredicate, Path } from "./api"; | ||
/** | ||
* Debug interceptor to log the current event to the console. | ||
*/ | ||
export declare function trace(_: any, e: any): void; | ||
/** | ||
* Higher-order interceptor for validation purposes. | ||
* Takes a predicate function and an optional interceptor function, | ||
* which will only be called if the predicate fails for a given event. | ||
* By default the `FX_CANCEL` side effect is triggered if the predicate | ||
* failed, thus ensuring the actual event handler for the failed event | ||
* will not be executed anymore. However, this can be overridden using | ||
* the error interceptor's result, which is merged into the result of | ||
* this interceptor. | ||
* | ||
* Note: For this interceptor to work as expected, it needs to be provided | ||
* BEFORE the main handler in the interceptor list for a given event, i.e. | ||
* | ||
* ``` | ||
* [ | ||
* ensurePred((state, e) => false), | ||
* (state, e) => console.log("no one never calls me") | ||
* ] | ||
* ``` | ||
* | ||
* @param pred predicate applied to given state & event | ||
* @param err interceptor triggered on predicate failure | ||
*/ | ||
export declare function ensurePred(pred: InterceptorPredicate, err?: InterceptorFn): (state: any, e: any, fx: any) => any; | ||
/** | ||
* Specialization of `ensurePred()` to ensure a state value is less than given max. | ||
* The optional `path` fn is used to extract or produce the path for the state | ||
* value to be validated. If omitted, the event's payload item is interpreted as | ||
* the value path. | ||
* | ||
* For example, without a provided `path` function and for an event | ||
* of this form: `["event-id", "foo.bar"]`, the term `"foo.bar"` would be | ||
* interpreted as path. | ||
* | ||
* If the event has this shape: `["event-id", ["foo.bar", 23]]`, we must provide | ||
* `(e) => e[1][0]` as path function to extract `"foo.bar"` from the event. | ||
* | ||
* @param path path extractor | ||
*/ | ||
export declare function ensureLessThan(max: number, path?: (e: Event) => Path, err?: InterceptorFn): (state: any, e: any, fx: any) => any; | ||
/** | ||
* Specialization of `ensurePred()` to ensure a state value is greater than given min. | ||
* See `ensureLessThan()` for further details. | ||
* | ||
* @param path path extractor | ||
*/ | ||
export declare function ensureGreaterThan(min: number, path?: (e: Event) => Path, err?: InterceptorFn): (state: any, e: any, fx: any) => any; |
@@ -5,20 +5,68 @@ "use strict"; | ||
const path_1 = require("./path"); | ||
/////////////////////////////////////////////////////////////////////// | ||
// re-usable interceptors | ||
// this one simply logs the current event | ||
exports.trace = (_, e) => console.log("event:", e); | ||
// higher-order interceptor for validation purposes | ||
// takes a predicate function and an optional interceptor function | ||
// which will be called if the predicate fails for a given event | ||
// by default the FX_CANCEL side effect is triggered if there is a failure | ||
// ensuring the actual event handler for the failed event will not be | ||
// executed anymore | ||
exports.ensurePred = (pred, err) => (state, e) => { | ||
if (!pred(state, e)) { | ||
return Object.assign({ [api_1.FX_CANCEL]: true }, (err ? err(state, e) : null)); | ||
} | ||
}; | ||
// specialization of `ensurePred()` to ensure a value less than given max | ||
exports.ensureLessThan = (max, err) => exports.ensurePred((state, e) => path_1.getIn(state, e[1]) < max, err); | ||
// specialization of `ensurePred()` to ensure a value greater than given min | ||
exports.ensureGreaterThan = (min, err) => exports.ensurePred((state, e) => path_1.getIn(state, e[1]) > min, err); | ||
/** | ||
* Debug interceptor to log the current event to the console. | ||
*/ | ||
function trace(_, e) { | ||
console.log("event:", e); | ||
} | ||
exports.trace = trace; | ||
/** | ||
* Higher-order interceptor for validation purposes. | ||
* Takes a predicate function and an optional interceptor function, | ||
* which will only be called if the predicate fails for a given event. | ||
* By default the `FX_CANCEL` side effect is triggered if the predicate | ||
* failed, thus ensuring the actual event handler for the failed event | ||
* will not be executed anymore. However, this can be overridden using | ||
* the error interceptor's result, which is merged into the result of | ||
* this interceptor. | ||
* | ||
* Note: For this interceptor to work as expected, it needs to be provided | ||
* BEFORE the main handler in the interceptor list for a given event, i.e. | ||
* | ||
* ``` | ||
* [ | ||
* ensurePred((state, e) => false), | ||
* (state, e) => console.log("no one never calls me") | ||
* ] | ||
* ``` | ||
* | ||
* @param pred predicate applied to given state & event | ||
* @param err interceptor triggered on predicate failure | ||
*/ | ||
function ensurePred(pred, err) { | ||
return (state, e, fx) => { | ||
if (!pred(state, e, fx)) { | ||
return Object.assign({ [api_1.FX_CANCEL]: true }, (err ? err(state, e, fx) : null)); | ||
} | ||
}; | ||
} | ||
exports.ensurePred = ensurePred; | ||
/** | ||
* Specialization of `ensurePred()` to ensure a state value is less than given max. | ||
* The optional `path` fn is used to extract or produce the path for the state | ||
* value to be validated. If omitted, the event's payload item is interpreted as | ||
* the value path. | ||
* | ||
* For example, without a provided `path` function and for an event | ||
* of this form: `["event-id", "foo.bar"]`, the term `"foo.bar"` would be | ||
* interpreted as path. | ||
* | ||
* If the event has this shape: `["event-id", ["foo.bar", 23]]`, we must provide | ||
* `(e) => e[1][0]` as path function to extract `"foo.bar"` from the event. | ||
* | ||
* @param path path extractor | ||
*/ | ||
function ensureLessThan(max, path, err) { | ||
return ensurePred((state, e) => path_1.getIn(state, path ? path(e) : e[1]) < max, err); | ||
} | ||
exports.ensureLessThan = ensureLessThan; | ||
/** | ||
* Specialization of `ensurePred()` to ensure a state value is greater than given min. | ||
* See `ensureLessThan()` for further details. | ||
* | ||
* @param path path extractor | ||
*/ | ||
function ensureGreaterThan(min, path, err) { | ||
return ensurePred((state, e) => path_1.getIn(state, path ? path(e) : e[1]) > min, err); | ||
} | ||
exports.ensureGreaterThan = ensureGreaterThan; |
{ | ||
"name": "@thi.ng/atom", | ||
"version": "0.9.0", | ||
"version": "0.9.1", | ||
"description": "Mutable wrapper for a immutable values", | ||
@@ -29,4 +29,3 @@ "main": "./index.js", | ||
"dependencies": { | ||
"@thi.ng/api": "^2.0.3", | ||
"@thi.ng/dcons": "0.1.13" | ||
"@thi.ng/api": "^2.0.3" | ||
}, | ||
@@ -33,0 +32,0 @@ "keywords": [ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
77709
1
1459
- Removed@thi.ng/dcons@0.1.13
- Removed@thi.ng/dcons@0.1.13(transitive)