Add Actions

Outline

In this document we setup the VortexJS Tuple Actions.

Since the Vortex serialisable base class is called a Tuple, Actions are referred to as “Action Tuples”, and name DoSomethingActionTuple.

A Tuple Action represents an action the user has taken, this can be:

  • Clicking a button (TupleGenericAction)
  • Updating data (TupleUpdateAction)
  • Some other action (extend TupleActionABC)

The Action design is ideal for apps where there are many users observing data more than altering it or performing actions against it.

Typically, users can only perform so many updates per a minute. TupleActions takes the approach of having many small, discrete “Actions” that can be sent back to the server as they are performed.

The Observable then ensures that all users watching the data are updated immediately, Keeping all users working with the latest data as TupleActions processed.

This helps avoid issues, such as one users update overwriting another users update. These issues you will get if you’re using the VortexJS TupleLoader for many users.

There are two Angular services that provide support for pushing Tuple Actions to the Client service.

  1. TupleActionPushService, for online only actions.
  2. TupleActionPushOfflineService, for actions that will be stored locally and delivered when the device is next online.

Both these services have the same functional interface, pushAction().

../../_images/LearnAction_DataFlow.png

On the Server service, the TupleActionProcessorProxy class receives all the TupleActions, delegates processing to a TupleActionProcessorDelegateABC class. A delegate can be registered to handle just one type of action, and/or a default delegate can be registered to catch all.

Like the Observable, there is a TupleActionProcessorProxy needed in the Client service that passes actions onto the Server service for processing.

Unlike the Observable, the TupleAction Client proxy passes every action onto the Server service, waits for a response from the Server service then sends that back to the Mobile or Desktop device.

Actions require responses. Callers of the TupleActionPushService will receive a promise which resolve regardless of if the push timed out or failed.

In the case of TupleActionPushOfflineService, a promise is returned and resolved on success of the commit to the database in the Desktop/Mobile device.

The TupleActionPushOfflineService will continually retry until it receives either a success or failure response from the Client service.

Note

The Mobile/Desktop devices don’t and can’t talk directly to the Server service.

../../_images/LearnAction_Response.png

Advantages

  1. Reduces the risk of one update overwriting another.
  2. Atomic changes can more easily be buffered when the device is offline.
  3. Smaller, more immediate results for updates.

Disadvantages

  1. This could lead to higher resource usage and less efficient commits.

Objective

In this document, our plugin will provide the following actions to the user:

  1. Increase or decrease an Int
  2. Toggle capitals of a string

The action will be processed by the Server which will update the table created in Adding a StringInt Table.

This is the order:

  1. Add the Action scaffolding for the project.
  2. Add the Server side Action Processor
  3. Alter the Observable tutorial UI to incorporate buttons and send the actions.

Add Python Tuples

Add File StringCapToggleActionTuple.py

The StringCapToggleActionTuple.py defines a python action tuple.


Create the file peek_plugin_tutorial/_private/tuples/StringCapToggleActionTuple.py and populate it with the following contents.

from vortex.Tuple import addTupleType, TupleField
from vortex.TupleAction import TupleActionABC

from peek_plugin_tutorial._private.PluginNames import tutorialTuplePrefix


@addTupleType
class StringCapToggleActionTuple(TupleActionABC):
    __tupleType__ = tutorialTuplePrefix + "StringCapToggleActionTuple"

    stringIntId = TupleField()

Add File AddIntValueActionTuple.py

The AddIntValueActionTuple.py defines a python action tuple.


Create the file peek_plugin_tutorial/_private/tuples/AddIntValueActionTuple.py and populate it with the following contents.

from vortex.Tuple import addTupleType, TupleField
from vortex.TupleAction import TupleActionABC

from peek_plugin_tutorial._private.PluginNames import tutorialTuplePrefix


@addTupleType
class AddIntValueActionTuple(TupleActionABC):
    __tupleType__ = tutorialTuplePrefix + "AddIntValueActionTuple"

    stringIntId = TupleField()
    offset = TupleField()

Add TypeScript Tuples

Add StringCapToggleActionTuple.ts

The StringCapToggleActionTuple.ts file defines a TypeScript class for our StringCapToggleActionTuple Tuple Action.


Create file peek_plugin_tutorial/plugin-module/_private/tuples/StringCapToggleActionTuple.ts, with contents

import {addTupleType, Tuple, TupleActionABC} from "@synerty/vortexjs";
import {tutorialTuplePrefix} from "../PluginNames";

@addTupleType
export class StringCapToggleActionTuple extends TupleActionABC {
    static readonly tupleName = tutorialTuplePrefix + "StringCapToggleActionTuple";

    stringIntId: number;

    constructor() {
        super(StringCapToggleActionTuple.tupleName)
    }
}

Add AddIntValueActionTuple.ts

The AddIntValueActionTuple.ts file defines a TypeScript class for our AddIntValueActionTuple Tuple Action.


Create file peek_plugin_tutorial/plugin-module/_private/tuples/AddIntValueActionTuple.ts, with contents

import {addTupleType, Tuple, TupleActionABC} from "@synerty/vortexjs";
import {tutorialTuplePrefix} from "../PluginNames";

@addTupleType
export class AddIntValueActionTuple extends TupleActionABC {
    public static readonly tupleName = tutorialTuplePrefix + "AddIntValueActionTuple";

    stringIntId: number;
    offset: number;

    constructor() {
        super(AddIntValueActionTuple.tupleName)
    }
}

Edit File _private/index.ts

The _private/index.ts file will re-export the Tuples in a more standard way. Developers won’t need to know the exact path of the file.


Edit file peek_plugin_tutorial/plugin-module/_private/index.ts, Append the lines:

export {StringCapToggleActionTuple} from "./tuples/StringCapToggleActionTuple";
export {AddIntValueActionTuple} from "./tuples/AddIntValueActionTuple";

Server Service Setup

Add Package controller

The controller python package will contain the classes that provide logic to the plugin, like a brain controlling limbs.

Note

Though the tutorial creates “controllers”, the plugin developer can decide how ever they want to structure this.


Create the peek_plugin_tutorial/_private/server/controller package, with the commands

mkdir peek_plugin_tutorial/_private/server/controller
touch peek_plugin_tutorial/_private/server/controller/__init__.py

Add File MainController.py

The MainController.py will glue everything together. For large plugins there will be multiple sub controllers.

In this example we have everything in MainController.


Create the file peek_plugin_tutorial/_private/server/controller/MainController.py and populate it with the following contents.

import logging

from twisted.internet.defer import Deferred
from vortex.DeferUtil import deferToThreadWrapWithLogger

from vortex.TupleSelector import TupleSelector
from vortex.TupleAction import TupleActionABC
from vortex.handler.TupleActionProcessor import TupleActionProcessorDelegateABC
from vortex.handler.TupleDataObservableHandler import TupleDataObservableHandler

from peek_plugin_tutorial._private.storage.StringIntTuple import StringIntTuple
from peek_plugin_tutorial._private.tuples.StringCapToggleActionTuple import StringCapToggleActionTuple
from peek_plugin_tutorial._private.tuples.AddIntValueActionTuple import AddIntValueActionTuple

logger = logging.getLogger(__name__)


class MainController(TupleActionProcessorDelegateABC):
    def __init__(self, dbSessionCreator, tupleObservable: TupleDataObservableHandler):
        self._dbSessionCreator = dbSessionCreator
        self._tupleObservable = tupleObservable

    def shutdown(self):
        pass

    def processTupleAction(self, tupleAction: TupleActionABC) -> Deferred:

        if isinstance(tupleAction, AddIntValueActionTuple):
            return self._processAddIntValue(tupleAction)

        if isinstance(tupleAction, StringCapToggleActionTuple):
            return self._processCapToggleString(tupleAction)

        raise NotImplementedError(tupleAction.tupleName())

    @deferToThreadWrapWithLogger(logger)
    def _processCapToggleString(self, action: StringCapToggleActionTuple):
        try:
            # Perform update using SQLALchemy
            session = self._dbSessionCreator()
            row = (session.query(StringIntTuple)
                   .filter(StringIntTuple.id == action.stringIntId)
                   .one())

            # Exit early if the string is empty
            if not row.string1:
                logger.debug("string1 for StringIntTuple.id=%s is empty")
                return

            if row.string1[0].isupper():
                row.string1 = row.string1.lower()
                logger.debug("Toggled to lower")
            else:
                row.string1 = row.string1.upper()
                logger.debug("Toggled to upper")

            session.commit()

            # Notify the observer of the update
            # This tuple selector must exactly match what the UI observes
            tupleSelector = TupleSelector(StringIntTuple.tupleName(), {})
            self._tupleObservable.notifyOfTupleUpdate(tupleSelector)

        finally:
            # Always close the session after we create it
            session.close()

    @deferToThreadWrapWithLogger(logger)
    def _processAddIntValue(self, action: AddIntValueActionTuple):
        try:
            # Perform update using SQLALchemy
            session = self._dbSessionCreator()
            row = (session.query(StringIntTuple)
                   .filter(StringIntTuple.id == action.stringIntId)
                   .one())
            row.int1 += action.offset
            session.commit()

            logger.debug("Int changed by %u", action.offset)

            # Notify the observer of the update
            # This tuple selector must exactly match what the UI observes
            tupleSelector = TupleSelector(StringIntTuple.tupleName(), {})
            self._tupleObservable.notifyOfTupleUpdate(tupleSelector)

        finally:
            # Always close the session after we create it
            session.close()

Add File TupleActionProcessor.py

The class in file TupleActionProcessor.py, accepts all tuple actions for this plugin and calls the relevant TupleActionProcessorDelegateABC.


Create the file peek_plugin_tutorial/_private/server/TupleActionProcessor.py and populate it with the following contents.

from vortex.handler.TupleActionProcessor import TupleActionProcessor

from peek_plugin_tutorial._private.PluginNames import tutorialFilt
from peek_plugin_tutorial._private.PluginNames import tutorialActionProcessorName
from .controller.MainController import MainController


def makeTupleActionProcessorHandler(mainController: MainController):
    processor = TupleActionProcessor(
        tupleActionProcessorName=tutorialActionProcessorName,
        additionalFilt=tutorialFilt,
        defaultDelegate=mainController)
    return processor

Edit File ServerEntryHook.py

We need to update ServerEntryHook.py, it will initialise the
MainController and TupleActionProcessor objects.

Edit the file peek_plugin_tutorial/_private/server/ServerEntryHook.py:

  1. Add these imports at the top of the file with the other imports:

    from .TupleActionProcessor import makeTupleActionProcessorHandler
    from .controller.MainController import MainController
    
  2. Add these line just before logger.debug("started") in the start() method:

    mainController = MainController(
        dbSessionCreator=self.dbSessionCreator,
        tupleObservable=tupleObservable)
    
    self._loadedObjects.append(mainController)
    self._loadedObjects.append(makeTupleActionProcessorHandler(mainController))
    

The Action Processor for the Server service is setup now.

Client Service Setup

Add File DeviceTupleProcessorActionProxy.py

The DeviceTupleProcessorActionProxy.py creates the Tuple Action Processoe Proxy. This class is responsible for proxying action tuple data between the devices and the Server.


Create the file peek_plugin_tutorial/_private/client/DeviceTupleProcessorActionProxy.py and populate it with the following contents.

from peek_plugin_base.PeekVortexUtil import peekServerName
from peek_plugin_tutorial._private.PluginNames import tutorialFilt
from peek_plugin_tutorial._private.PluginNames import tutorialActionProcessorName
from vortex.handler.TupleActionProcessorProxy import TupleActionProcessorProxy


def makeTupleActionProcessorProxy():
    return TupleActionProcessorProxy(
                tupleActionProcessorName=tutorialActionProcessorName,
                proxyToVortexName=peekServerName,
                additionalFilt=tutorialFilt)

Edit File ClientEntryHook.py

We need to update ClientEntryHook.py, it will initialise the tuple action proxy object when the Plugin is started.


Edit the file peek_plugin_tutorial/_private/client/ClientEntryHook.py:

  1. Add this import at the top of the file with the other imports:

    from .DeviceTupleProcessorActionProxy import makeTupleActionProcessorProxy
    
  2. Add this line after the docstring in the start() method:

    self._loadedObjects.append(makeTupleActionProcessorProxy())
    

Mobile Service Setup

Now we need to edit the Angular module in the mobile-app and add the providers:

Edit File tutorial.module.ts

Edit the tutorial.module.ts Angular module for the tutorial plugin to add the provider entry for the TupleAction service.


Edit the file peek_plugin_tutorial/_private/mobile-app/tutorial.module.ts:

  1. Add the following imports:

    // Import the required classes from VortexJS
    import {
        TupleActionPushNameService,
        TupleActionPushOfflineService,
        TupleActionPushService
    } from "@synerty/vortexjs";
    
    // Import the names we need for the
    import {
        tutorialActionProcessorName
    } from "@peek/peek_plugin_tutorial/_private";
    
  2. After the imports, add this function

    export function tupleActionPushNameServiceFactory() {
        return new TupleActionPushNameService(
            tutorialActionProcessorName, tutorialFilt);
    }
    
  3. Finally, add this snippet to the providers array in the @NgModule decorator

    TupleActionPushOfflineService, TupleActionPushService, {
        provide: TupleActionPushNameService,
        useFactory: tupleActionPushNameServiceFactory
    },
    

It should look similar to the following:

...

import {
    TupleActionPushNameService,
    TupleActionPushOfflineService,
    TupleActionPushService
} from "@synerty/vortexjs";

import {
    tutorialActionProcessorName
} from "@peek/peek_plugin_tutorial/_private";

...

export function tupleActionPushNameServiceFactory() {
    return new TupleActionPushNameService(
        tutorialActionProcessorName, tutorialFilt);
}


@NgModule({
    ...
    providers: [
        ...
        TupleActionPushOfflineService, TupleActionPushService, {
            provide: TupleActionPushNameService,
            useFactory: tupleActionPushNameServiceFactory
        },
        ...
    ]
})
export class TutorialModule {

}

At this point, all of the Tuple Action setup is done. It’s much easier to work with the tuple action code from here on.

Add Mobile View

Finally, lets add a new component to the mobile screen.

Edit File string-int.component.ts

Edit the file, string-int.component.ts to connect the tuple action to the frontend.


edit the file peek_plugin_tutorial/_private/mobile-app/string-int/string-int.component.ts

  1. Add the following imports:
import {TupleActionPushService} from "@synerty/vortexjs";

import {
    AddIntValueActionTuple,
    StringCapToggleActionTuple
} from "@peek/peek_plugin_tutorial/_private";
  1. Add private actionService: TupleActionPushService to the constructor argument:
constructor(private actionService: TupleActionPushService,
    ...) {
  1. Finally, add the methods to the StringIntComponent class after the constructor:
toggleUpperClicked(item) {
    let action = new StringCapToggleActionTuple();
    action.stringIntId = item.id;
    this.actionService.pushAction(action)
        .then(() => {
            alert('success');

        })
        .catch((err) => {
            alert(err);
        });
}

incrementClicked(item) {
    let action = new AddIntValueActionTuple();
    action.stringIntId = item.id;
    action.offset = 1;
    this.actionService.pushAction(action)
        .then(() => {
            alert('success');

        })
        .catch((err) => {
            alert(err);
        });
}

decrementClicked(item) {
    let action = new AddIntValueActionTuple();
    action.stringIntId = item.id;
    action.offset = -1;
    this.actionService.pushAction(action)
        .then(() => {
            alert('success');

        })
        .catch((err) => {
            alert(err);
        });
}

It should look similar to the following:

...

import {
    AddIntValueActionTuple,
    StringCapToggleActionTuple
} from "@peek/peek_plugin_tutorial/_private";

...

    constructor(private actionService: TupleActionPushService,
                ...) {

    ...

    incrementClicked(item) {
        let action = new AddIntValueActionTuple();
        action.stringIntId = item.id;
        action.offset = 1;
        this.actionService.pushAction(action)
            .then(() => {
                alert('success');

            })
            .catch((err) => {
                alert(err);
            });
    }


    decrementClicked(item) {
        let action = new AddIntValueActionTuple();
        action.stringIntId = item.id;
        action.offset = -1;
        this.actionService.pushAction(action)
            .then(() => {
                alert('success');

            })
            .catch((err) => {
                alert(err);
            });
    }

    mainClicked() {
        this.router.navigate([tutorialBaseUrl]);
    }

}

Edit File string-int.component.mweb.html

Edit the web HTML view file, string-int.component.mweb.html and insert buttons that will change initiate the created tuple actions.


Edit the file peek_plugin_tutorial/_private/mobile-app/string-int/string-int.component.mweb.html and populate it with the following contents:

<div class="container">
    <Button class="btn btn-default" (click)="mainClicked()">Back to Main</Button>

    <table class="table table-striped">
        <thead>
        <tr>
            <th>String</th>
            <th>Int</th>
            <th></th>
        </tr>
        </thead>
        <tbody>
        <tr *ngFor="let item of stringInts">
            <td>{{item.string1}}</td>
            <td>{{item.int1}}</td>
            <td>
                <Button class="btn btn-default" (click)="toggleUpperClicked(item)">
                    Toggle Caps
                </Button>
                <Button class="btn btn-default" (click)="incrementClicked(item)">
                    Increment Int
                </Button>
                <Button class="btn btn-default" (click)="decrementClicked(item)">
                    Decrement Int
                </Button>
            </td>
        </tr>
        </tbody>
    </table>
</div>

Edit File string-int.component.ns.html

Edit the NativeScript XML view file, string-int.component.ns.html and insert buttons that will change initiate the created tuple actions.


Edit the file peek_plugin_tutorial/_private/mobile-app/string-int/string-int.component.ns.html and populate it with the following contents.

<StackLayout class="p-20">
    <Button text="Back to Main" (tap)="mainClicked()"></Button>

    <GridLayout columns="4*, 1*" rows="auto" width="*">
        <Label class="h3" col="0" text="String"></Label>
        <Label class="h3" col="1" text="Int"></Label>
    </GridLayout>

    <ListView [items]="stringInts">
        <template let-item="item" let-i="index" let-odd="odd" let-even="even">
            <StackLayout [class.odd]="odd" [class.even]="even">
                <GridLayout columns="4*, 1*" rows="auto" width="*">
                    <!-- String -->
                    <Label class="h3 peek-field-data-text" row="0" col="0"
                           textWrap="true"
                           [text]="item.string1"></Label>

                    <!-- Int -->
                    <Label class="h3 peek-field-data-text" row="0" col="1"
                           [text]="item.int1"></Label>

                </GridLayout>
                <Button text="Toggle Caps" (tap)="toggleUpperClicked(item)"></Button>
                <Button text="Increment Int" (tap)="incrementClicked(item)"></Button>
                <Button text="Decrement Int" (tap)="decrementClicked(item)"></Button>
            </StackLayout>
        </template>
    </ListView>
</StackLayout>

Testing

  1. Open mobile Peek web app
  2. Tap the Tutorial app icon
  3. Tap the “String Ints” button
  4. Expect to see the string ints data
  5. Select the “Toggle Caps” button
  6. If successful an alert will appear stating “success”. If you receive an error, go back through the “Add Actions” instructions. Restart the server service and retry step five
  7. You will see the data update instantly
  8. Return to step five for buttons “Increment Int” and “Decrement Int”

Offline Observable

The Synerty VortexJS library has an TupleDataOfflineObserverService, once offline storage has been setup, (here Add Offline Storage), the offline observable is a drop in replacement.

When using the offline observable, it will:

  1. Queue a request to observe the data, sending it to the client
  2. Query the SQL db in the browser/mobile device, and return the data for the observer. This provides instant data for the user.

When new data is sent to the the observer (Mobile/Desktop service) from the observable (Client service), the offline observer does two things:

  1. Notifies the subscribers like normal
  2. Stores the data back into the offline db, in the browser / app.

Edit File string-int.component.ts

TupleDataOfflineObserverService is a drop-in replacement for TupleDataObserverService.

Switching to use the offline observer requires two edits to string-int.component.ts.


Edit file peek_plugin_tutorial/_private/mobile-app/string-int/string-int.component.ts.

Add the import for the TupleDataOfflineObserverService:

import TupleDataOfflineObserverService from "@synerty/vortexjs";

Change the type of the tupleDataObserver parameter in the component constructor, EG,

From

constructor(private tupleDataObserver: TupleDataObserverService, ...) {

To

constructor(private tupleDataObserver: TupleDataOfflineObserverService, ...) {

That’s it. Now the String Int data will load on the device, even when the Vortex between the device and the Client service is offline.