diff --git a/README.md b/README.md index 6938882..626be4c 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ const axium = createAxium('ownershipTest', [ createCounterConcept(), createExperimentConcept(createExperimentActionQueState(), [checkInQuality], [experimentActionQuePrinciple]) ], true, true); -const staged = axium.stage( +const plan = axium.stage( 'Testing Ownership Staging', [ (cpts, dispatch) => { const axiumState = cpts[0].state as AxiumState; diff --git a/Stage.md b/Stage.md index 40718a5..5aa9286 100644 --- a/Stage.md +++ b/Stage.md @@ -1,21 +1,28 @@ ## Stage ### Abstract -This is derived from the newly created UnifiedSubject to handle the main point of vulnerability that a recursive machine carries. As the main point of dispatching new actions in the system would traditionally be informed via the subscription to listen to state changes. This Design Pattern allows one to safely dispatch in a tightly patterned subscription. This design pattern watches each stage for the potential of a runaway configuration which would normally prevent this machine from halting. But since the Unified Turing Machine was created to be halting complete. The UnifiedSubject internally watches each stage of your application independently and the actions that it dispatches via the supplied dispatch function. If a similar action is dispatched in rapid Succession denoted by its type and no debounce option. That Stage will close and be added to the axium's badPlans property. +This is derived from the newly created UnifiedSubject to handle the main point of vulnerability that a recursive machine carries. As the main point of dispatching new actions in the system would traditionally be informed via the subscription to listen to state changes. This Design Pattern allows one to safely dispatch in a tightly patterned subscription. This design pattern watches each stage for the potential of a runaway configuration which would normally prevent this machine from halting. But since the Unified Turing Machine was created to be halting complete. The UnifiedSubject internally watches each stage of your application independently and the actions that it dispatches via the supplied dispatch function. If a similar action is dispatched in rapid Succession denoted by its type and no debounce option. That plan will conclude and be added to the axium's badPlans property. Once attached to the badPlan property, it would be possible to reinitialize said stage via your concept's principle utilizing the stage's title. But places that burden of responsibility on the developer. As the scope of a Unified Turing Machine is to be designed to specification and halt appropriately. We accept failure as likewise the ability to halt. -"You stage a plan and a plan has multiple stages." +*"You stage a plan and a plan has multiple stages."* ## Working with the Stage Paradigm +The added benefit of the creation of a plan to control the flow of actions. Allows the ability to create a series of stages to handle how the dispatch would be handled within a subscription, but with the added benefit of iterating through each stage your plan. A typical plan would typically be composed of an initialization, main run time, and likewise the ability to conclude. 3 acts if you will. ```typescript // Multiple Stages are a Plan export type Plan = { title: string; - stages: Staging[], stage: number; + stages: Staging[], stageFailed: number; } - +``` +* title - Title of your plan, used to determine your plan within badPlans stored on the axium if they fail. +* stage - This is your stage index, starting at 0. +* stages - Is an array of function that will be called upon each notification depending on the current stage index. +* stageFailed - Is a fail safe mechanism to prevent action overflow due to branch prediction errors. +### Stage Options +```typescript export type dispatchOptions = { runOnce?: boolean; debounce?: number; @@ -27,88 +34,111 @@ export type dispatchOptions = { }, } +``` +* dispatchOptions +* runOnce - If enabled on the dispatch options, this will permit only one dispatch of that action within its stage. +* debounce - Required to prevent the stage to be considered bad if rerunning the same action within the same stage, specific use case is tracking some position over time. If on is part of options, this will only come into play after that action is first dispatched. +* incrementStage - Will increment to the next stage index, this should be your default option for dispatching actions or strategies to prevent action overflow. +* setStage - This will set the stage to a specific stage index, useful if some strategy failed and the staging needs to be reset to prepare for that strategy again. This will always override iterateStage. +* on - Simple handler that will prevent dispatch until the selected value is set to what is expected. Keep in mind this should also be occupied by a debounce, as this dispatch will run on each successful state update. This should be utilized alongside iterateStage, setStage, or debounce to prevent action overflow. + +### Internals +```typescript export type Dispatcher = (action: Action, options: dispatchOptions) => void; export type Staging = ( concepts: Concept[], dispatch: (action: Action, options: dispatchOptions) => void ) => void; -export type Stage = (id: number) => () => void; - export class UnifiedSubject extends Subject { stage(title: string, stages: Staging[]) {} } ``` -The added benefit of the creation of staging to control the flow of actions, is also the ability to create an additional abstraction to handle how the dispatch would be handled within a subscription, but with the added benefit of iterating through each step of a stage your applications. That these steps would typically be an initialization, a main run time, and likewise the ability to close. 3 acts if you will. - -* dispatchOptions -* runOnce - If enabled on the dispatch options, this will permit only one dispatch of that action within its stage. -* debounce - Required to prevent the stage to be considered bad if rerunning the same action within the same step, specific use case is tracking some position over time. -* setStep - This will set the stage to a specific step, useful if some strategy failed and the staging needs to be reset to prepare for that strategy again. -* incrementStep - Will increment the current step of the stage, this should be your default option for dispatching actions or strategies to prevent action overflow. -* on - Simple handler that will prevent dispatch until the selected value is set to what is expected. Keep in mind this should also be occupied by a debounce, as this dispatch will run on each successful state update. +* Dispatcher - This is the supplied dispatch function that is made available each stage. +* Staging - The interface that you will be interacting with when setting up your stages, noting placement of concepts and the dispatch function. +* UnifiedSubject - This is a specialized subject for utilized within STRX to allow for this staging paradigm. This is made available via the createAxium function and likewise within your principles via the concept$ property. Note that your plan will be an array of functions even with just one stage. ## Example - ```typescript let runCount = 0; - -// Sets logging to True -const axium = createAxium([createCounterConcept()], true); - -const staged = axium.stage('Stage DispatchOptions Test', -[ - (concepts, dispatch) => { +const axium = createAxium('axiumStageDispatchOptionsTest', [createCounterConcept()], true); +const sub = axium.subscribe((concepts) => { + const axiumState = concepts[0].state as AxiumState; + if (axiumState.badPlans.length > 0) { + const badPlan = axiumState.badPlans[0]; const counter = selectState(concepts, counterName); - console.log('Stage 1 ', counter, runCount); - // Sets count to 1 - dispatch(counterAdd(), { - iterateStep: true - }); + console.log('Stage Ran Away, badPlans.length: ', axiumState.badPlans.length, 'Count: ', counter.count); + plan.conclude(); + sub.unsubscribe(); + expect(badPlan.stageFailed).toBe(2); + expect(counter.count).toBe(2); + setTimeout(() => {done();}, 500); + } +}); +const plan = axium.stage('Stage DispatchOptions Test', + [ + (concepts, dispatch) => { + const counter = selectState(concepts, counterName); + console.log('Stage 1 ', counter, runCount); + dispatch(counterAdd(), { + iterateStage: true + }); }, (concepts, dispatch) => { - runCount++; - const counter = selectState(concepts, counterName); - console.log('Stage 2 ', counter, runCount); - // Sets count to 2 and only runs once per state update - dispatch(counterAdd(), { + runCount++; + const counter = selectState(concepts, counterName); + console.log('Stage 2 ', counter, runCount); + // Sets count to 2 and only runs once per state update + dispatch(counterAdd(), { runOnce: true - }); - // Will wait until count is set to 2, then set the Stage Explicitly to the third Step counting from 0. - dispatch(counterAdd(), { - setStep: 2, - debounce: 0, + }); + // Will wait until count is set to 2, then set the Stage Explicitly to the third Step counting from 0. + dispatch(counterAdd(), { + setStage: 2, on: { - selector: counterSelectCount, - expected: 2 - } - }); - // } + selector: counterSelectCount, + expected: 2 + }, + // Requires debounce, because the previous action is of the same type, but runs only once. + debounce: 1 + }); + // } }, (concepts, dispatch) => { - runCount++; - const counter = selectState(concepts, counterName); - console.log('Stage 3 ', counter, runCount); - // Will cause an action overflow forcing the stage to close and add itself to bad Stages - dispatch(counterSubtract(), { + runCount++; + const counter = selectState(concepts, counterName); + console.log('Should run twice, Stage 3 ', counter, runCount); + // Will cause an action overflow forcing the current stage to conclude and add the plan to badPlans + dispatch(counterSubtract(), { // Enabling will cause this test to timeout via the subscription watching for badPlans to never be ran. // debounce: 500 // This demonstrates the fault resistance of the Stage paradigm, despite STRX's recursive functionality. - }); - } -]); - -const sub = axium.subscribe((concepts) => { - const axiumState = concepts[0].state as AxiumState; - // This will run once the last step of the stage we created overflows, this is for demonstration purposes only. - if (axiumState.badPlans.length > 0) { - const badPlan = axiumState.badPlans[0]; - const counter = selectState(concepts, counterName); - console.log('Stage Ran Away, badPlans.length: ', axiumState.badPlans.length, 'Count: ', counter.count); - expect(badPlan.stepFailed).toBe(2); - expect(counter.count).toBe(2); - sub.unsubscribe(); + }); + // This dispatch will be invalidated and never dispatched due to the effect of action overflow of the above. + dispatch(counterAdd(), {}); + console.log( + 'Will also run twice. 1st will be before "Stage Ran Away,"', + 'and after "Should run twice." The 2nd will be final console log output.' + ); } -}); + ]); ``` +To prevent action overflow, each stage is paying attention to consecutive actions of the same type. In an action overflow state, sequentially the overflow will call the same dispatch before called the next dispatch even if within the same stage. Keep in mind behind the scenes during a STRX runtime, there will be multiple strategies running concurrently. Observe the runCount specified in this example. Please look to the STRX's tests folder. -As well that STRX is designed to be run primarily through the loaded concepts and their associated principles. To prevent unexpected behaviors in your own principles. Please utilize the supplied KeyedSelector for axium's open property to begin the stage of your concepts. \ No newline at end of file +## Stage within your Principle +STRX is designed to be ran primarily through its loaded concepts and their associated principles. To prevent unexpected behaviors in your own principles. Please utilize the supplied KeyedSelector for axium's open property to begin the stage of your concepts. +```typescript +const plan = concept$.stage('Principle Stage Example', [ + (___, dispatch) => { + dispatch(someAction(), { + iterateStage: true, + on: { + selector: axiumSelectOpen, + expected: true + }, + }); + }, + (concepts, dispatch) => { + // Your principle's run time logic. + } +]); +``` diff --git a/package.json b/package.json index 5658a46..23e81f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@phuire/strx", - "version": "0.0.24", + "version": "0.0.25", "description": "Unified Turing Machine", "main": "dist/index.js", "module": "dist/index.mjs", diff --git a/src/model/unifiedSubject.ts b/src/model/unifiedSubject.ts index 57749ea..7855c94 100644 --- a/src/model/unifiedSubject.ts +++ b/src/model/unifiedSubject.ts @@ -14,8 +14,8 @@ export type Plan = { export type dispatchOptions = { runOnce?: boolean; - iterateStep?: boolean; - setStep?: number; + iterateStage?: boolean; + setStage?: number; on?: { selector: KeyedSelector, // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -29,7 +29,6 @@ export type Staging = ( concepts: Concept[], dispatch: (action: Action, options: dispatchOptions) => void ) => void; -// export type Stage = (id: number) => () => void; export type StageDelimiter = { stage: number, prevActions: ActionType[], @@ -61,6 +60,16 @@ const handleRun = ]; } } else { + const unionExpiration: number[] = []; + stageDelimiter.prevActions = stageDelimiter.prevActions.filter((at, i) => { + if (at !== action.type && stageDelimiter.unionExpiration[i] !== action.expiration) { + unionExpiration.push(unionExpiration[i]); + return true; + } else { + return false; + } + }); + stageDelimiter.unionExpiration = unionExpiration; return [ stageDelimiter, false ]; @@ -134,11 +143,11 @@ export class UnifiedSubject extends Subject { this.currentStages.set(this.stageId, {title, stages, stage: 0, stageFailed: -1}); const stageId = this.stageId; this.stageId++; - const close = () => { + const conclude = () => { this.currentStages.delete(stageId); }; return { - close: close.bind(this) + conclude: conclude.bind(this) }; } @@ -178,12 +187,12 @@ export class UnifiedSubject extends Subject { } this.stageDelimiters.set(key, stageDelimiter); if (!debounce && run) { - if (options?.setStep) { - plan.stage = options.setStep; - } - if (options?.iterateStep) { + if (options?.iterateStage) { plan.stage += 1; } + if (options?.setStage) { + plan.stage = options.setStage; + } // Horrifying // Keep in place, this prevents branch prediction from creating ghost actions if there is an action overflow. if (plan.stageFailed === -1) { @@ -193,7 +202,7 @@ export class UnifiedSubject extends Subject { } else if ( options?.runOnce === undefined && (options.on === undefined || - (options.on && (!options.debounce && (options.iterateStep === undefined || options.setStep === plan.stage))) + (options.on && (!options.debounce && (options.iterateStage === undefined || options.setStage === plan.stage))) )) { plan.stageFailed = plan.stage; plan.stage = plan.stages.length; diff --git a/src/test/addConcepts.test.ts b/src/test/addConcepts.test.ts index a91442b..73ecea7 100644 --- a/src/test/addConcepts.test.ts +++ b/src/test/addConcepts.test.ts @@ -9,14 +9,14 @@ import { countingTopic } from '../concepts/counter/strategies/counting.strategy' test('Axium add Concepts Strategy Test', (done) => { const axium = createAxium('axiumAddConceptTest',[], true, true); - const staged = axium.stage('Add Concepts Stage',[ + const plan = axium.stage('Add Concepts Stage',[ (concepts, dispatch) => { dispatch( strategyBegin( addConceptsToAddQueThenBlockStrategy(concepts,[createCounterConcept()]) ), { - iterateStep: true + iterateStage: true } ); }, @@ -25,7 +25,7 @@ test('Axium add Concepts Strategy Test', (done) => { if (concepts[1].name === counterName) { exists = true; dispatch(strategyBegin(countingStrategy()), { - iterateStep: true + iterateStage: true }); } expect(exists).toBe(true); @@ -36,7 +36,7 @@ test('Axium add Concepts Strategy Test', (done) => { const counter = selectState(concepts, counterName); expect(counter.count).toBe(1); setTimeout(() => {done();}, 500); - staged.close(); + plan.conclude(); } } ]); diff --git a/src/test/ownership.test.ts b/src/test/ownership.test.ts index 99a7350..6e51867 100644 --- a/src/test/ownership.test.ts +++ b/src/test/ownership.test.ts @@ -26,7 +26,7 @@ test('Ownership Test', (done) => { createCounterConcept(), createExperimentConcept(createExperimentActionQueState(), [checkInQuality], [experimentActionQuePrinciple]) ], true, true); - const staged = axium.stage( + const plan = axium.stage( 'Testing Ownership Staging', [ (cpts, dispatch) => { const axiumState = cpts[0].state as AxiumState; @@ -38,7 +38,7 @@ test('Ownership Test', (done) => { // This will place a counting strategy in the experiment actionQue to be later dispatched. // Via its principle, to simulate an action moving off premise. dispatch(strategyBegin(puntCountingStrategy()), { - iterateStep: true + iterateStage: true }); } }, @@ -47,7 +47,7 @@ test('Ownership Test', (done) => { // Will be ran after both counting strategies conclude. const ownership = selectState(cpts, ownershipName); console.log('Stage 2', ownership.ownershipLedger, ownership.pendingActions); - dispatch(counterSetCount(createPayload({newCount: 1000}), undefined, 7000), { iterateStep: true}); + dispatch(counterSetCount(createPayload({newCount: 1000}), undefined, 7000), { iterateStage: true}); }, (cpts, dispatch) => { const ownership = selectState(cpts, ownershipName); @@ -55,7 +55,7 @@ test('Ownership Test', (done) => { const counter = selectState(cpts, counterName); console.log('Count: ', counter.count); dispatch(strategyBegin(experimentPrimedCountingStrategy(cpts)), { - iterateStep: true + iterateStage: true }); }, (cpts, dispatch) => { @@ -70,7 +70,7 @@ test('Ownership Test', (done) => { expect(counter.count).toBe(3); // Comment in if testing the halting ability of log and setCount stage is commented out. // setTimeout(() => {done();}, 1000); - staged.close(); + plan.conclude(); } else if ( (axiumState.lastStrategy === experimentCountingTopic || axiumState.lastStrategy === experimentPrimedCountingTopic) && diff --git a/src/test/removeConcepts.test.ts b/src/test/removeConcepts.test.ts index 5030f0b..a0e5dcc 100644 --- a/src/test/removeConcepts.test.ts +++ b/src/test/removeConcepts.test.ts @@ -11,13 +11,13 @@ import { AxiumState } from '../concepts/axium/axium.concept'; test('Axium remove Concepts Strategy Test', (done) => { const axium = createAxium('axiumRemoveConceptsTest', [createCounterConcept()], true, true); - const staged = axium.stage('Remove Concepts Stage',[ + const plan = axium.stage('Remove Concepts Stage',[ (concepts, dispatch) => { dispatch( strategyBegin( addConceptsToRemovalQueThenBlockStrategy(concepts,[createCounterConcept()]) ), { - iterateStep: true + iterateStage: true } ); }, @@ -33,7 +33,7 @@ test('Axium remove Concepts Strategy Test', (done) => { }); expect(exists).toBe(false); setTimeout(() => {done();}, 500); - staged.close(); + plan.conclude(); } } ]); diff --git a/src/test/stageDisptachOptions.test.ts b/src/test/stageDispatchOptions.test.ts similarity index 79% rename from src/test/stageDisptachOptions.test.ts rename to src/test/stageDispatchOptions.test.ts index a0dfc78..9e7e7d7 100644 --- a/src/test/stageDisptachOptions.test.ts +++ b/src/test/stageDispatchOptions.test.ts @@ -15,20 +15,20 @@ test('Axium Stage Dispatch Options Test', (done) => { const badPlan = axiumState.badPlans[0]; const counter = selectState(concepts, counterName); console.log('Stage Ran Away, badPlans.length: ', axiumState.badPlans.length, 'Count: ', counter.count); - staged.close(); + plan.conclude(); sub.unsubscribe(); expect(badPlan.stageFailed).toBe(2); expect(counter.count).toBe(2); setTimeout(() => {done();}, 500); } }); - const staged = axium.stage('Stage DispatchOptions Test', + const plan = axium.stage('Stage DispatchOptions Test', [ (concepts, dispatch) => { const counter = selectState(concepts, counterName); console.log('Stage 1 ', counter, runCount); dispatch(counterAdd(), { - iterateStep: true + iterateStage: true }); }, (concepts, dispatch) => { runCount++; @@ -40,24 +40,31 @@ test('Axium Stage Dispatch Options Test', (done) => { }); // Will wait until count is set to 2, then set the Stage Explicitly to the third Step counting from 0. dispatch(counterAdd(), { - setStep: 2, - debounce: 0, + setStage: 2, on: { selector: counterSelectCount, expected: 2 - } + }, + // Requires debounce, because the previous action is of the same type, but runs only once. + debounce: 1 }); // } }, (concepts, dispatch) => { runCount++; const counter = selectState(concepts, counterName); console.log('Should run twice, Stage 3 ', counter, runCount); - // Will cause an action overflow forcing the stage to close and add itself to bad Stages + // Will cause an action overflow forcing the stage to close and add itself to badPlans dispatch(counterSubtract(), { // Enabling will cause this test to timeout via the subscription watching for badPlans to never be ran. // debounce: 500 // This demonstrates the fault resistance of the Stage paradigm, despite STRX's recursive functionality. }); + // This dispatch will be invalidated and never dispatched due to the effect of action overflow of the above. + dispatch(counterAdd(), {}); + console.log( + 'Will also run twice. 1st will be before "Stage Ran Away,"', + 'and after "Should run twice." The 2nd will be final console log output.' + ); } ]); }); \ No newline at end of file diff --git a/src/test/stagedPrinciple.test.ts b/src/test/stagedPrinciple.test.ts index dfb3ad8..6321c1f 100644 --- a/src/test/stagedPrinciple.test.ts +++ b/src/test/stagedPrinciple.test.ts @@ -26,15 +26,14 @@ const experimentMockToTrueQuality = createQuality(experimentMockToTrueType, expe test('Axium Principle Stage', (done) => { const experimentPrinciple: PrincipleFunction = (_: Subscriber, __: Concept[], concept$: UnifiedSubject) => { - const stage = concept$.stage('Experiment Principle', [ + const plan = concept$.stage('Experiment Principle', [ (___, dispatch) => { dispatch(experimentMockToTrue(), { - iterateStep: true, + iterateStage: true, on: { selector: axiumSelectOpen, expected: true }, - debounce: 1 }); }, (concepts) => { @@ -42,7 +41,7 @@ test('Axium Principle Stage', (done) => { if (experimentState.mock) { expect(experimentState.mock).toBe(true); setTimeout(() => done(), 1000); - stage.close(); + plan.conclude(); } } ]); diff --git a/src/test/strategy.test.ts b/src/test/strategy.test.ts index 4dd60eb..dbab7a7 100644 --- a/src/test/strategy.test.ts +++ b/src/test/strategy.test.ts @@ -8,11 +8,11 @@ import { countingTopic } from '../concepts/counter/strategies/counting.strategy' test('Axium Counting Strategy Test', (done) => { const axium = createAxium('axiumStrategyTest', [createCounterConcept()], true, true); - const staged = axium.stage('Counting Strategy Stage', + const plan = axium.stage('Counting Strategy Stage', [ (_, dispatch) => { dispatch(strategyBegin(countingStrategy()), { - iterateStep: true + iterateStage: true }); }, (concepts) => { const axiumState = concepts[0].state as AxiumState; @@ -20,7 +20,7 @@ test('Axium Counting Strategy Test', (done) => { const counter = selectState(concepts, counterName); expect(counter.count).toBe(1); setTimeout(() => {done();}, 500); - staged.close(); + plan.conclude(); } } ]);