From 338a1e4f6a63eca1b0bdf48a808b9913e786a911 Mon Sep 17 00:00:00 2001 From: REllEK-IO Date: Thu, 19 Oct 2023 11:37:19 -0700 Subject: [PATCH] Debounce helper methodCreators. --- src/concepts/counter/qualities/add.quality.ts | 3 - .../debounceAsyncNextActionNode.quality.ts | 24 +++ .../debounceNextActionNode.quality.ts | 22 +++ .../asyncDebounceAddOne.strategy.ts | 33 ++++ .../strategies/debounceAddOne.strategy.ts | 32 ++++ src/index.ts | 9 +- src/model/actionController.ts | 17 +- src/model/debounceAction.ts | 165 ++++++++++++++++++ src/model/method.ts | 143 ++++++++------- src/model/stagePlanner.ts | 1 + src/test/actionController.test.ts | 45 ----- src/test/debounceMethods.test.ts | 103 +++++++++++ src/test/methodHelpers.test.ts | 58 ++++++ 13 files changed, 538 insertions(+), 117 deletions(-) create mode 100644 src/concepts/experiment/qualities/debounceAsyncNextActionNode.quality.ts create mode 100644 src/concepts/experiment/qualities/debounceNextActionNode.quality.ts create mode 100644 src/concepts/experiment/strategies/asyncDebounceAddOne.strategy.ts create mode 100644 src/concepts/experiment/strategies/debounceAddOne.strategy.ts create mode 100644 src/model/debounceAction.ts create mode 100644 src/test/debounceMethods.test.ts create mode 100644 src/test/methodHelpers.test.ts diff --git a/src/concepts/counter/qualities/add.quality.ts b/src/concepts/counter/qualities/add.quality.ts index 3c02818..5eeb3ae 100644 --- a/src/concepts/counter/qualities/add.quality.ts +++ b/src/concepts/counter/qualities/add.quality.ts @@ -1,11 +1,8 @@ -import { map, Subject } from 'rxjs'; import { Action, ActionType, prepareActionCreator } from '../../../model/action'; import { defaultMethodCreator, Method, MethodCreator } from '../../../model/concept'; -import { strategySuccess } from '../../../model/actionStrategy'; import { Counter } from '../counter.concept'; import { createQuality } from '../../../model/concept'; import { counterSelectCount } from '../counter.selector'; -import { axiumConclude } from '../../axium/qualities/conclude.quality'; export const counterAddType: ActionType = 'Counter Add'; diff --git a/src/concepts/experiment/qualities/debounceAsyncNextActionNode.quality.ts b/src/concepts/experiment/qualities/debounceAsyncNextActionNode.quality.ts new file mode 100644 index 0000000..5c0f2c7 --- /dev/null +++ b/src/concepts/experiment/qualities/debounceAsyncNextActionNode.quality.ts @@ -0,0 +1,24 @@ +import { MethodCreator, defaultReducer } from '../../../model/concept'; +import { prepareActionCreator } from '../../../model/action'; +import { createQuality } from '../../../model/concept'; +import { createAsyncMethodDebounce } from '../../../model/method'; +import { strategySuccess } from '../../../model/actionStrategy'; + +export const experimentAsyncDebounceNextActionNodeType = 'Experiment will debounce incoming actions within set duration asynchronously'; +export const experimentAsyncDebounceNextActionNode = prepareActionCreator(experimentAsyncDebounceNextActionNodeType); + +export const experimentDebounceNextActionNodeMethodCreator: MethodCreator = () => createAsyncMethodDebounce((controller, action) => { + setTimeout(() => { + if (action.strategy) { + controller.fire(strategySuccess(action.strategy)); + } else { + controller.fire(action); + } + }, 50); +}, 500); + +export const asyncDebounceNextActionNodeQuality = createQuality( + experimentAsyncDebounceNextActionNodeType, + defaultReducer, + experimentDebounceNextActionNodeMethodCreator +); diff --git a/src/concepts/experiment/qualities/debounceNextActionNode.quality.ts b/src/concepts/experiment/qualities/debounceNextActionNode.quality.ts new file mode 100644 index 0000000..eac0e3b --- /dev/null +++ b/src/concepts/experiment/qualities/debounceNextActionNode.quality.ts @@ -0,0 +1,22 @@ +import { MethodCreator, defaultReducer } from '../../../model/concept'; +import { prepareActionCreator } from '../../../model/action'; +import { createQuality } from '../../../model/concept'; +import { createMethodDebounce } from '../../../model/method'; +import { strategySuccess } from '../../../model/actionStrategy'; + +export const experimentDebounceNextActionNodeType = 'Experiment will debounce incoming actions within set duration'; +export const experimentDebounceNextActionNode = prepareActionCreator(experimentDebounceNextActionNodeType); + +export const experimentDebounceNextActionNodeMethodCreator: MethodCreator = () => createMethodDebounce((action) => { + if (action.strategy) { + return strategySuccess(action.strategy); + } else { + return action; + } +}, 500); + +export const debounceNextActionNodeQuality = createQuality( + experimentDebounceNextActionNodeType, + defaultReducer, + experimentDebounceNextActionNodeMethodCreator +); diff --git a/src/concepts/experiment/strategies/asyncDebounceAddOne.strategy.ts b/src/concepts/experiment/strategies/asyncDebounceAddOne.strategy.ts new file mode 100644 index 0000000..8a56328 --- /dev/null +++ b/src/concepts/experiment/strategies/asyncDebounceAddOne.strategy.ts @@ -0,0 +1,33 @@ +import { createStrategy, ActionStrategy, ActionStrategyParameters, createActionNode } from '../../../model/actionStrategy'; +import { counterAdd } from '../../counter/qualities/add.quality'; +import { experimentAsyncDebounceNextActionNode } from '../qualities/debounceAsyncNextActionNode.quality'; +import { experimentDebounceNextActionNode } from '../qualities/debounceNextActionNode.quality'; + +export const experimentAsyncDebounceAddOneTopic = 'Async debounce add one'; +export function experimentAsyncDebounceAddOneStrategy(): ActionStrategy { + const stepTwo = createActionNode(counterAdd(), { + successNode: null, + successNotes: { + preposition: '', + denoter: 'One;', + }, + failureNode: null, + agreement: 1000, + }); + const stepOne = createActionNode(experimentAsyncDebounceNextActionNode(), { + successNode: stepTwo, + successNotes: { + preposition: '', + denoter: 'One;', + }, + failureNode: null, + agreement: 1000, + }); + + const params: ActionStrategyParameters = { + topic: experimentAsyncDebounceAddOneTopic, + initialNode: stepOne, + }; + + return createStrategy(params); +} \ No newline at end of file diff --git a/src/concepts/experiment/strategies/debounceAddOne.strategy.ts b/src/concepts/experiment/strategies/debounceAddOne.strategy.ts new file mode 100644 index 0000000..ff57eea --- /dev/null +++ b/src/concepts/experiment/strategies/debounceAddOne.strategy.ts @@ -0,0 +1,32 @@ +import { createStrategy, ActionStrategy, ActionStrategyParameters, createActionNode } from '../../../model/actionStrategy'; +import { counterAdd } from '../../counter/qualities/add.quality'; +import { experimentDebounceNextActionNode } from '../qualities/debounceNextActionNode.quality'; + +export const experimentDebounceAddOneTopic = 'Debounce add one'; +export function experimentDebounceAddOneStrategy(): ActionStrategy { + const stepTwo = createActionNode(counterAdd(), { + successNode: null, + successNotes: { + preposition: '', + denoter: 'One;', + }, + failureNode: null, + agreement: 1000, + }); + const stepOne = createActionNode(experimentDebounceNextActionNode(), { + successNode: stepTwo, + successNotes: { + preposition: '', + denoter: 'One;', + }, + failureNode: null, + agreement: 1000, + }); + + const params: ActionStrategyParameters = { + topic: experimentDebounceAddOneTopic, + initialNode: stepOne, + }; + + return createStrategy(params); +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 1ee77ce..25ab32c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,8 +27,15 @@ export { createMethod, createAsyncMethod, createMethodWithConcepts, - createAsyncMethodWithConcepts + createAsyncMethodWithConcepts, + createMethodDebounce, + createAsyncMethodDebounce, + createMethodDebounceWithConcepts, + createAsyncMethodDebounceWithConcepts } from './model/method'; +export { + debounceAction +} from './model/debounceAction'; export type { Action, ActionType } from './model/action'; export { primeAction, createAction, getSemaphore, prepareActionCreator, prepareActionWithPayloadCreator } from './model/action'; export { createConcept, createQuality, defaultReducer, defaultMethodCreator } from './model/concept'; diff --git a/src/model/actionController.ts b/src/model/actionController.ts index 961dc26..f07b30f 100644 --- a/src/model/actionController.ts +++ b/src/model/actionController.ts @@ -47,15 +47,24 @@ export class ActionController extends Subject { this.timer.unref(); } let nextAction; + let end = true; + // Logically Determined axiumConclude + if (action.semaphore[3] === 3) { + end = false; + } if (action.strategy) { nextAction = action; - } else if (action.semaphore[3] !== 1) { + // Logically Determined axiumConclude + } else if (action.semaphore[3] === 3) { + nextAction = action; + // Logically Determined axiumBadAction + } else if (!action.strategy && action.semaphore[3] !== 1) { const conclude = axiumConclude(); nextAction = { ...action, ...conclude }; - } else { + } else { nextAction = action; } const { observers } = this; @@ -63,7 +72,9 @@ export class ActionController extends Subject { for (let i = 0; i < len; i++) { observers[i].next(nextAction); } - this.complete(); + if (end) { + this.complete(); + } } } } diff --git a/src/model/debounceAction.ts b/src/model/debounceAction.ts new file mode 100644 index 0000000..7739208 --- /dev/null +++ b/src/model/debounceAction.ts @@ -0,0 +1,165 @@ +import { + MonoTypeOperatorFunction, + Observable, + OperatorFunction, + SchedulerAction, + SchedulerLike, + Subscriber, + Subscription, + asyncScheduler +} from 'rxjs'; +import { Action } from './action'; +import { axiumConclude } from '../concepts/axium/qualities/conclude.quality'; + +function hasLift(source: any): source is { lift: InstanceType['lift'] } { + return typeof source?.lift === 'function'; +} + +function operate( + init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void +): OperatorFunction { + return (source: Observable) => { + if (hasLift(source)) { + // eslint-disable-next-line consistent-return + return source.lift(function (this: Subscriber, liftedSource: Observable) { + try { + return init(liftedSource, this); + } catch (err) { + this.error(err); + } + }); + } + throw new TypeError('Unable to lift unknown Observable type'); + }; +} + +class OperatorSubscriber extends Subscriber { + constructor( + destination: Subscriber, + onNext?: (value: T) => void, + onComplete?: () => void, + onError?: (err: any) => void, + private onFinalize?: () => void, + private shouldUnsubscribe?: () => boolean + ) { + super(destination); + this._next = onNext + ? function (this: OperatorSubscriber, value: T) { + try { + onNext(value); + } catch (err) { + destination.error(err); + } + } + : super._next; + this._error = onError + ? function (this: OperatorSubscriber, err: any) { + try { + onError(err); + } catch (error) { + // Send any errors that occur down stream. + destination.error(error); + } finally { + // Ensure finalization. + this.unsubscribe(); + } + } + : super._error; + this._complete = onComplete + ? function (this: OperatorSubscriber) { + try { + onComplete(); + } catch (err) { + // Send any errors that occur down stream. + destination.error(err); + } finally { + // Ensure finalization. + this.unsubscribe(); + } + } + : super._complete; + } + + unsubscribe() { + if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) { + const { closed } = this; + super.unsubscribe(); + !closed && this.onFinalize?.(); + } + } +} + +function createOperatorSubscriber( + destination: Subscriber, + onNext?: (value: T) => void, + onComplete?: () => void, + onError?: (err: any) => void, + onFinalize?: () => void +): Subscriber { + return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize); +} + +/** + * This will prevent all actions for the specified duration, but will still emit actions as axiumConclude + * Thus this needs to be taken into account in the Method using debounceAction if implemented directly. + * But will be handled automatically in actionControllers and associated debounce createMethods. + */ +export function debounceAction(dueTime: number, scheduler: SchedulerLike = asyncScheduler): MonoTypeOperatorFunction { + return operate((source, subscriber) => { + let activeTask: Subscription | null = null; + let lastValue: Action | null = null; + let lastTime: number | null = null; + + const emit = () => { + if (activeTask) { + activeTask.unsubscribe(); + activeTask = null; + const value = lastValue!; + lastValue = null; + subscriber.next(value); + } + }; + function emitWhenIdle(this: SchedulerAction) { + const targetTime = lastTime! + dueTime; + const now = scheduler.now(); + if (now < targetTime) { + activeTask = this.schedule(undefined, targetTime - now); + subscriber.add(activeTask); + return; + } + + emit(); + } + + source.subscribe( + createOperatorSubscriber( + subscriber, + (value: Action) => { + lastValue = value; + lastTime = scheduler.now(); + if (!activeTask) { + activeTask = scheduler.schedule(emitWhenIdle, dueTime); + subscriber.add(activeTask); + } else { + // All this code just to place this code block. + const conclude = axiumConclude(); + subscriber.next( + { + ...value, + ...conclude, + } + ); + } + }, + () => { + emit(); + subscriber.complete(); + }, + undefined, + () => { + lastValue = activeTask = null; + } + ) + ); + }); +} \ No newline at end of file diff --git a/src/model/method.ts b/src/model/method.ts index 6cdac79..65f1161 100644 --- a/src/model/method.ts +++ b/src/model/method.ts @@ -1,10 +1,10 @@ -import { Observable, Subject, debounceTime, map, switchMap, withLatestFrom } from 'rxjs'; +import { Observable, Subject, map, switchMap, withLatestFrom } from 'rxjs'; import { Concept } from './concept'; import { UnifiedSubject } from './stagePlanner'; import { ActionController, createActionController$ } from './actionController'; import { ActionStrategy } from './actionStrategy'; import { KeyedSelector } from './selector'; -import { createExperimentConcept, createExperimentState } from '../concepts/experiment/experiment.concept'; +import { debounceAction } from './debounceAction'; export type ActionType = string; const axiumConcludeType: ActionType = 'Conclude'; @@ -20,6 +20,9 @@ type Action = { }; type Method = Observable; + + + export const createMethod = (method: (action: Action) => Action): [Method, Subject] => { const defaultSubject = new Subject(); @@ -78,66 +81,76 @@ export const createAsyncMethodWithConcepts = ); return [defaultMethod, defaultSubject]; }; -// export const createMethodDebounce = -// (method: (action: Action) => Action, duration: number): [Method, Subject] => { -// const defaultSubject = new Subject(); -// const defaultMethod: Method = defaultSubject.pipe( -// debounceTime(duration), -// map((action: Action) => { -// const methodAction = method(action); -// if (methodAction.strategy) { -// return methodAction; -// } -// return { -// ...action, -// type: axiumConcludeType -// }; -// }), -// ); -// return [defaultMethod, defaultSubject]; -// }; -// export const createMethodDebounceWithConcepts = -// (methodWithConcepts: (action: Action, concepts: Concept[]) => Action, concepts$: UnifiedSubject, duration: number) -// : [Method, Subject] => { -// const defaultSubject = new Subject(); -// const defaultMethod: Method = defaultSubject.pipe( -// debounceTime(duration), -// withLatestFrom(concepts$ as UnifiedSubject), -// map(([act, concepts] : [Action, Concept[]]) => { -// const methodAction = methodWithConcepts(act, concepts); -// if (methodAction.strategy) { -// return methodAction; -// } -// return { -// ...act, -// type: axiumConcludeType -// }; -// }), -// ); -// return [defaultMethod, defaultSubject]; -// }; -// export const createAsyncMethodDebounce = -// (asyncMethod: (controller: ActionController, action: Action) => void, duration: number): [Method, Subject] => { -// const defaultSubject = new Subject(); -// const defaultMethod: Method = defaultSubject.pipe( -// debounceTime(duration), -// switchMap(act => createActionController$(act, (controller: ActionController, action: Action) => { -// asyncMethod(controller, action); -// })), -// ); -// return [defaultMethod, defaultSubject]; -// }; -// export const createAsyncMethodDebounceWithConcepts = -// (asyncMethodWithConcepts: (controller: ActionController, action: Action, concepts: Concept[]) => -// void, concepts$: UnifiedSubject, duration: number): [Method, Subject] => { -// const defaultSubject = new Subject(); -// const defaultMethod: Method = defaultSubject.pipe( -// debounceTime(duration), -// withLatestFrom(concepts$ as UnifiedSubject), -// switchMap(([act, concepts] : [Action, Concept[]]) => -// createActionController$(act, (controller: ActionController, action: Action) => { -// asyncMethodWithConcepts(controller, action, concepts); -// })), -// ); -// return [defaultMethod, defaultSubject]; -// }; \ No newline at end of file +export const createMethodDebounce = + (method: (action: Action) => Action, duration: number): [Method, Subject] => { + const defaultSubject = new Subject(); + const defaultMethod: Method = defaultSubject.pipe( + debounceAction(duration), + map((action: Action) => { + // Logically Determined axiumConclude + if (action.semaphore[3] !== 3) { + const methodAction = method(action); + if (methodAction.strategy) { + return methodAction; + } + return { + ...action, + type: axiumConcludeType + }; + } else { + return action; + } + }), + ); + return [defaultMethod, defaultSubject]; + }; +export const createMethodDebounceWithConcepts = + (methodWithConcepts: (action: Action, concepts: Concept[]) => Action, concepts$: UnifiedSubject, duration: number) + : [Method, Subject] => { + const defaultSubject = new Subject(); + const defaultMethod: Method = defaultSubject.pipe( + debounceAction(duration), + withLatestFrom(concepts$ as UnifiedSubject), + map(([act, concepts] : [Action, Concept[]]) => { + // Logically Determined axiumConclude + if (act.semaphore[3] !== 3) { + const methodAction = methodWithConcepts(act, concepts); + if (methodAction.strategy) { + return methodAction; + } + return { + ...act, + type: axiumConcludeType + }; + } else { + return act; + } + }), + ); + return [defaultMethod, defaultSubject]; + }; +export const createAsyncMethodDebounce = + (asyncMethod: (controller: ActionController, action: Action) => void, duration: number): [Method, Subject] => { + const defaultSubject = new Subject(); + const defaultMethod: Method = defaultSubject.pipe( + debounceAction(duration), + switchMap(act => createActionController$(act, (controller: ActionController, action: Action) => { + asyncMethod(controller, action); + })), + ); + return [defaultMethod, defaultSubject]; + }; +export const createAsyncMethodDebounceWithConcepts = + (asyncMethodWithConcepts: (controller: ActionController, action: Action, concepts: Concept[]) => + void, concepts$: UnifiedSubject, duration: number): [Method, Subject] => { + const defaultSubject = new Subject(); + const defaultMethod: Method = defaultSubject.pipe( + debounceAction(duration), + withLatestFrom(concepts$ as UnifiedSubject), + switchMap(([act, concepts] : [Action, Concept[]]) => + createActionController$(act, (controller: ActionController, action: Action) => { + asyncMethodWithConcepts(controller, action, concepts); + })), + ); + return [defaultMethod, defaultSubject]; + }; \ No newline at end of file diff --git a/src/model/stagePlanner.ts b/src/model/stagePlanner.ts index 543f574..d1690b5 100644 --- a/src/model/stagePlanner.ts +++ b/src/model/stagePlanner.ts @@ -221,6 +221,7 @@ export class UnifiedSubject extends Subject { let run = true; [stageDelimiter, goodAction] = handleStageDelimiter(plan, action, stageDelimiter, options); [stageDelimiter, run] = handleRun(value, stageDelimiter, plan, action, options); + // console.log('HIT', action, goodAction, run); this.stageDelimiters.set(key, stageDelimiter); if (goodAction && run) { const action$ = axiumState.action$ as Subject; diff --git a/src/test/actionController.test.ts b/src/test/actionController.test.ts index 67c5820..39967c3 100644 --- a/src/test/actionController.test.ts +++ b/src/test/actionController.test.ts @@ -64,48 +64,3 @@ test('ActionController createActionController$ Mock Strategy', (done) => { done(); }); }); - -test('ActionController async Method Test', (done) => { - const experiment = createExperimentConcept(createExperimentState(), [timerEmitActionQuality, mockToTrueQuality]); - const axium = createAxium('Experiment async method creator', [experiment]); - const plan = axium.stage('timed mock to true', [ - (_, dispatch) => { - dispatch(strategyBegin(timedMockToTrue()), { - iterateStage: true - }); - }, - (concepts, _) => { - const experimentState = selectState(concepts, experimentName); - if (experimentState.mock) { - expect(experimentState.mock).toBe(true); - plan.conclude(); - done(); - } - } - ]); -}); - -test('ActionController async Method Test with Concepts', (done) => { - const experiment = createExperimentConcept(createExperimentState(), [timerEmitActionWithConceptsQuality, mockToTrueQuality]); - const axium = createAxium('Experiment async method creator with Concepts', [experiment]); - const plan = axium.stage('timed mock to true', [ - (_, dispatch) => { - dispatch(strategyBegin(timedMockToTrueWithConcepts()), { - iterateStage: true - }); - }, - (concepts, _) => { - const lastStrategy = selectSlice(concepts, axiumSelectLastStrategy); - const experimentState = selectState(concepts, experimentName); - if (lastStrategy === timedMockToTrueWithConceptsTopic) { - const data = selectSlice(concepts, axiumSelectLastStrategyData); - if (data) { - expect(data.mock).toBe(false); - expect(experimentState.mock).toBe(true); - plan.conclude(); - done(); - } - } - } - ]); -}); \ No newline at end of file diff --git a/src/test/debounceMethods.test.ts b/src/test/debounceMethods.test.ts new file mode 100644 index 0000000..0f899b3 --- /dev/null +++ b/src/test/debounceMethods.test.ts @@ -0,0 +1,103 @@ +import { Counter, counterName, createCounterConcept } from '../concepts/counter/counter.concept'; +import { createExperimentConcept, createExperimentState } from '../concepts/experiment/experiment.concept'; +import { asyncDebounceNextActionNodeQuality } from '../concepts/experiment/qualities/debounceAsyncNextActionNode.quality'; +import { debounceNextActionNodeQuality } from '../concepts/experiment/qualities/debounceNextActionNode.quality'; +import { experimentAsyncDebounceAddOneStrategy } from '../concepts/experiment/strategies/asyncDebounceAddOne.strategy'; +import { experimentDebounceAddOneStrategy } from '../concepts/experiment/strategies/debounceAddOne.strategy'; +import { strategyBegin } from '../model/actionStrategy'; +import { createAxium } from '../model/axium'; +import { selectState } from '../model/selector'; + +test('Debounce method prevent excess count', (done) => { + const experiment = createExperimentConcept(createExperimentState(), [debounceNextActionNodeQuality]); + const axium = createAxium('Experiment async method creator with Concepts', [createCounterConcept(), experiment]); + const plan = axium.stage('timed mock to true', [ + (_, dispatch) => { + dispatch(strategyBegin(experimentDebounceAddOneStrategy()), { + iterateStage: true + }); + }, + (_, dispatch) => { + dispatch(strategyBegin(experimentDebounceAddOneStrategy()), { + iterateStage: true + }); + }, + (_, dispatch) => { + dispatch(strategyBegin(experimentDebounceAddOneStrategy()), { + iterateStage: true + }); + }, + (concepts, _) => { + const counterState = selectState(concepts, counterName); + console.log('Debounce HIT 4', counterState); + if (counterState.count === 1) { + console.log('Final Debounce HIT 4', counterState); + expect(counterState.count).toBe(1); + plan.conclude(); + done(); + } + } + ]); +}); + +test('Async debounce method prevent excess count', (done) => { + const experiment = createExperimentConcept(createExperimentState(), [asyncDebounceNextActionNodeQuality]); + const axium = createAxium('Experiment async method creator with Concepts', [createCounterConcept(), experiment]); + const plan = axium.stage('timed mock to true', [ + (_, dispatch) => { + dispatch(strategyBegin(experimentAsyncDebounceAddOneStrategy()), { + iterateStage: true + }); + }, + (_, dispatch) => { + dispatch(strategyBegin(experimentAsyncDebounceAddOneStrategy()), { + iterateStage: true + }); + }, + (_, dispatch) => { + dispatch(strategyBegin(experimentAsyncDebounceAddOneStrategy()), { + iterateStage: true + }); + }, + (concepts, _) => { + const counterState = selectState(concepts, counterName); + console.log('Async Debounce HIT 4', counterState); + if (counterState.count === 1) { + console.log('Async Debounce HIT 4', counterState); + expect(counterState.count).toBe(1); + plan.conclude(); + } + } + ]); + setTimeout(() => { + const secondPlan = axium.stage('timed mock to true', [ + (_, dispatch) => { + dispatch(strategyBegin(experimentAsyncDebounceAddOneStrategy()), { + iterateStage: true + }); + }, + (_, dispatch) => { + dispatch(strategyBegin(experimentAsyncDebounceAddOneStrategy()), { + iterateStage: true + }); + }, + (_, dispatch) => { + dispatch(strategyBegin(experimentAsyncDebounceAddOneStrategy()), { + iterateStage: true + }); + }, + (concepts, _) => { + const counterState = selectState(concepts, counterName); + console.log('Async Plan 2 Debounce HIT 4', counterState); + if (counterState.count === 2) { + console.log('Async Plan 2 Debounce HIT 4', counterState); + expect(counterState.count).toBe(2); + secondPlan.conclude(); + setTimeout(() => { + done(); + }, 500); + } + } + ]); + }, 600); +}); \ No newline at end of file diff --git a/src/test/methodHelpers.test.ts b/src/test/methodHelpers.test.ts new file mode 100644 index 0000000..07b5caa --- /dev/null +++ b/src/test/methodHelpers.test.ts @@ -0,0 +1,58 @@ +import { axiumSelectLastStrategy, axiumSelectLastStrategyData } from '../concepts/axium/axium.selector'; +import { ExperimentState, createExperimentConcept, createExperimentState, experimentName } from '../concepts/experiment/experiment.concept'; +import { mockToTrueQuality } from '../concepts/experiment/qualities/mockTrue.quality'; +import { timerEmitActionQuality } from '../concepts/experiment/qualities/timerEmitAction.quality'; +import { timerEmitActionWithConceptsQuality } from '../concepts/experiment/qualities/timerEmitActionWithConcepts.quality'; +import { timedMockToTrue } from '../concepts/experiment/strategies/timedMockToTrue.strategy'; +import { + timedMockToTrueWithConcepts, + timedMockToTrueWithConceptsTopic +} from '../concepts/experiment/strategies/timedMockToTrueWithConcepts.strategy'; +import { strategyBegin } from '../model/actionStrategy'; +import { createAxium } from '../model/axium'; +import { selectSlice, selectState } from '../model/selector'; + +test('ActionController async Method Test', (done) => { + const experiment = createExperimentConcept(createExperimentState(), [timerEmitActionQuality, mockToTrueQuality]); + const axium = createAxium('Experiment async method creator', [experiment]); + const plan = axium.stage('timed mock to true', [ + (_, dispatch) => { + dispatch(strategyBegin(timedMockToTrue()), { + iterateStage: true + }); + }, + (concepts, _) => { + const experimentState = selectState(concepts, experimentName); + if (experimentState.mock) { + expect(experimentState.mock).toBe(true); + plan.conclude(); + done(); + } + } + ]); +}); + +test('ActionController async Method Test with Concepts', (done) => { + const experiment = createExperimentConcept(createExperimentState(), [timerEmitActionWithConceptsQuality, mockToTrueQuality]); + const axium = createAxium('Experiment async method creator with Concepts', [experiment]); + const plan = axium.stage('timed mock to true', [ + (_, dispatch) => { + dispatch(strategyBegin(timedMockToTrueWithConcepts()), { + iterateStage: true + }); + }, + (concepts, _) => { + const lastStrategy = selectSlice(concepts, axiumSelectLastStrategy); + const experimentState = selectState(concepts, experimentName); + if (lastStrategy === timedMockToTrueWithConceptsTopic) { + const data = selectSlice(concepts, axiumSelectLastStrategyData); + if (data) { + expect(data.mock).toBe(false); + expect(experimentState.mock).toBe(true); + plan.conclude(); + done(); + } + } + } + ]); +});