1.0.0-17 • Published 3 years ago

@yogasoft/tidal-state v1.0.0-17

Weekly downloads
-
License
MIT
Repository
-
Last release
3 years ago

Tidal State

An object-oriented reactive state management library.

Framework agnostic: works on its own or side-by-side with any framework.

Built 100% in TypeScript. Import into any JS or TS project (UI/Node). ES5 compatible.

Highlights

Installation

npm i @yogasoft/tidal-state

All classes/types are indexed at the root package level. Most likely you will want to selectively import types as needed:

import { TidalApplication, StateNavigator } from '@yogasoft/tidal-state'

Learn

Begin by learning about Properties, or jump straight into State Management.

Properties

The Tidal* properties form the basis of a StateVessel, defining the properties to be exposed together in the application's state pool. They are wrappers around a value and extend an RxJS Observable. There are three main types, each of which have a read-only and a read-write (RW) version:

  • TidalPrimitive
  • TidalObject
  • TidalFunction

The most commonly used are typically TidalPrimitive and TidalObject. TidalPrimitive takes as a value any JS primitive or non-object, non-function type (typeof value !== 'object' && typeof value !== 'function'). TidalObject takes any object type, while TidalFunction takes any Function type.

The read-write version of each property has the same name followed by an 'RW'. See When to Use Read-Only vs Read-Write Properties.

The Tidal* property constructor takes the initial value/event for the property. Non-function properties can also take a provider function to generate their initial value.

Properties in a Class

class MyClass {
  private readonly myString = new TidalPrimitive("my string") // Read
  private readonly myBool = new TidalPrimitive(true) // Read
  private readonly myBoolRW = new TidalPrimitiveRW(true) // Read-Write

  private readonly myFn = new TidalFunction(() => false) // Read
  private readonly myFnRW = new TidalFunctionRW(() => "") // Read-Write

  private readonly myObj = new TidalObject({prop: ""}) // Read

  // Non-function properties can also take a provider function instead of a concrete value.
  private readonly myObjRW = new TidalObjectRW(() => { return {prop: true} }) // Read-Write
}

Within a class, Tidal* properties should typically be defined as readonly, since the reference to the property wrapper itself will never change, only its contained value. Generally, they should also be marked private, since properties are shared through State Management, not directly. Some frameworks like Angular may not allow this if you want to access/subscribe to the property in the component's html template. If this is the case, you can use a getter method to expose the property's value if you want to keep the property safely encapsulated. Also, see: Properties Are Not Transfer Objects

Getting and Setting Property Values

Read-write property values are updated with a call to set() and can also be reset to their initial value with a call to reset(). Values can be retrieved synchronously with get() or by using RxJS features (since all properties extend Observable).

const myProp = new TidalPrimitiveRW("initial value")

// Will have output 3 total events after all below calls:
// 1: "initial value"
// 2: "new value"
// 3: "initial value"
myProp.subscribe(next => console.log(next))

// Get initial value.
console.log(myProp.get()) // "initial value"

// Set new value.
myProp.set("new value")
console.log(myProp.get()) // "new value"

// Most function calls on properties can be chained as well. 
// Reset value, followed by get value.
console.log(myProp.reset().get()) // "initial value"

Read-only properties do not have set() or reset() functions, but must have their values changed via a StateNavigator. See Modifying Read Only Properties for more.

When to Use Read-Only vs Read-Write Properties

Generally, Tidal* properties should be read-only (any property without an RW suffix) by default. Make a property writeable only if all consumers/agents outside of a particular StateVessel should be allowed to modify the value of the property. Since a shared StateVessel exposes its properties to potentially many consumers, think carefully about whether you want any agent outside of the StateVessel's own StateNavigator to modify and propagate events through a particular property. This is a design decision related to Separation of Concerns for your particular application.

Any StateVessel can always modify its own property values through its associated StateNavigator. See: Modifying Read Only Properties.

Local Properties

Local* properties (LocalPrimitive, LocalObject, and LocalFunction) behave exactly as Tidal* properties, except that they are never pulled into a StateVessel upon initialization. They can be used to leverage the same utility benefits as Tidal* properties, but without exposing properties to the state pool. Local* properties are always Read-Write, since they are only ever used by the local state.

class MyClass {
  private readonly sharedProperty = new TidalPrimitive("shared")
  private readonly localProperty = new LocalPrimitive("not shared")
}

Properties Are Not Transfer Objects

Tidal* and Local* properties should never be passed around outside of their local context. This would be the equivalent of taking an RxJS Observable and passing it around, rather than subscribing and passing its value. Tidal* and Local* properties are event emitters, so the event/value which they emit should be handled by consumers, not the emitter (property) itself.

If you find yourself wanting to directly share a Tidal* property wrapper outside of its own class/object, then what you likely want to do is expose and access it using state navigators. Within its local context, the property value/event should be accessed through normal means (see: Getting and Setting Property Values).

Special Property Types

Several additional property types exist that provide utility functionality:

Toggleable Properties
  • ToggleableBoolean
  • ToggleablePrimitive
  • ToggleableObject
  • ToggleableFunction
const toggleableBool = new ToggleableBoolean(true)
console.log(toggleableBool.get()) // "true"
console.log(toggleableBool.toggle()) // "false"
console.log(toggleableBool.reset().get()) // "true"

const toggleableString = new ToggleablePrimitive("string A", "string B")
// Outputs 3 events:
// 1: "string A"
// 2: "string B"
// 3: "string A"
toggleableString.subscribe(next => console.log(next))

toggleableString.toggle()
toggleableString.reset()
Service Related:
  • ServiceCallStatus
  • LocalServiceCallStatus
  • TidalExecutor

The service related properties are used by TidalService types (see: Services), but can be used independently as well. LocalServiceCallStatus is a local variant of ServiceCallStatus which is writable and intended only for local state purposes.

State Management

The tidal-state library, beyond providing some useful stateful property wrappers, is an application-wide (global) state management library. It supports the use of several common design patterns in your application, such as Service-Orientation and Service Locator, while filling a similar niche to global dependency injection, and can eliminate the need for a dependency injection framework in most cases.

Below are the various parts which together provide state management features in tidal-state.

State Navigator

A StateNavigator is the primary agent for a tidal-state application. Each StateVessel can have a navigator that is associated with it and used in the state vessel's local context. Think of the navigator as the vessel's captain, which largely controls the vessel's activities and interactions with other state vessels. Without a StateNavigator, a StateVessel is not known to the state pool, and therefore is not available for interactions with other states. There are two exceptions to this: Headless States and Services.

A navigator is initialized with a static call to TidalApplication#initNavigator with the name of its associated StateVessel:

const myStateName = 'MY_STATE'
const myNavigator = TidalApplication.initNavigator(myStateName)

The navigator is responsible for initializing its vessel in the state pool with:

myNavigator.initOwnState({prop: new TidalPrimitive(true)})

You also retrieve other states from the state pool, if they have provided a link, with the StateNavigator#getState method:

interface OtherState extends StateVessel {
  // ... props
}
const otherStateVessel: OtherState = myNavigator.getState('OTHER_STATE_NAME')

Typically, a StateNavigator will be initialized when its surrounding class/component is constructed:

interface MyState extends StateVessel {
  prop1: TidalPrimitive<boolean>
}

const myStateName = 'MY_STATE'

class MyStateClass implements MyState {
  [index: string]: any

  private readonly prop1 = new TidalPrimitive(false)

  otherProp = 12345

  private readonly navigator: StateNavigator<MyState>

  constructor() {
    // Initialize the navigator
    this.navigator = TidalApplication.initNavigator(myStateName)
    // Initialize the navigator's StateVessel (the current class)
    this.navigator.initOwnState(this)
  }
}

Note, it is perfectly fine to initialize a StateVessel from an object with more than only Tidal* properties. Any properties or functions that aren't wrapped in a Tidal* property are filtered out when the StateVessel is initialized.

Child States

A StateNavigator can have one or more child states associated with it. A child state is a StateVessel in the state pool which is owned by the navigator, similar to its own StateVessel, so it always has access to it. The difference from a regular state vessel is that a child state does not have its own navigator and therefore cannot create links to other states.

// Assumes navigator initialized normally
let myNavigator: StateNavigator<MyState>

myNavigator.initChildState('MY_CHILD_STATE', {childProp: new TidalPrimitive(true)})
myNavigator.getState('MY_CHILD_STATE') // Works fine

let otherNavigator: StateNavigator<OtherState>
otherNavigator.getState('MY_CHILD_STATE') // Error! No link!

Child states are useful for example if you want to manage a child component/class's state from its parent when there is no need for other states to access the child. In a certain sense they behave like a Headless State which has one and only one link to its parent.

Modifying Read Only Properties

A navigator is required in order to update the values of a Read-Only Tidal* property. There are utility methods on StateNavigator for doing this, such as set() and reset().

class MyStateClass implements MyState {
  private readonly readOnlyProp = new TidalPrimitive('initial value')
  private readonly navigator: StateNavigator<MyState>
  
  constructor() {
    this.navigator = TidalApplication.initNavigator('MY_STATE_NAME')
    this.doIt()
  }

  public doIt() {
    console.log(this.readOnlyProp.get()) // 'initial value'

    this.navigator.set(this.readOnlyProp, 'new value')
    console.log(this.readOnlyProp.get()) // 'new value'
  
    this.navigator.reset(this.readOnlyProp)
    console.log(this.readOnlyProp.get()) // 'initial value'
  }
}
Reset State

Navigators also can reset an entire StateVessel with resetState():

// Assume navigator is initialized per normal
let navigator: StateNavigator<MyState>

navigator.resetState('SOME_STATE')

Note, if 'SOME_STATE' contains read-only (not *RW) properties, then calling resetState from a navigator that does not own it will throw an Error. Only the state owner can modify the values of any read-only properties, therefore resetting the entire state is only allowed if the owner is the caller.

Best Practices

Const or Enums for State Names

Use const or enum to store state names, since they act like developer-friendly keys to access and manage states in application logic. The state name is also the name that shows up in logs or errors when an issue happens related to interacting with a state, so name them something that a developer will understand, rather than using randomized or dynamic strings for example. When using an enum, use string-based Typescript enums.

const myStateA = 'COMPONENT_A'
const myService = 'SERVICE_A'

enum WidgetStateName {
  WIDGET_1 = 'WIDGET_1',
  WIDGET_2 = 'WIDGET_2'
}

enum ServiceName {
  SERVICE_1 = 'SERVICE_1',
  SERVICE_2 = 'SERVICE_2'
}
// Initialize a state with an enum.
TidalApplication.headlessStateBuilder(WidgetStateName.WIDGET_1, { widgetProp: new TidalPrimitive("") }).init()

// Using const or enum for state names makes subsequent interactions reliable across the application.
let navigator: StateNavigator<MyState>
const widgetOneState = navigator.getState(WidgetStateName.WIDGET_1)
StateVessel Interfaces

It is recommended to use an interface to define a StateVessel that is shared in the state pool.

// All StateVessel interfaces should extend the StateVessel type
interface MyState extends StateVessel {
  readonly myPrimitive: TidalPrimitive<null | string>
  readonly myObject: TidalObject<{prop1: string}>  
}
class MyClass implements MyState {
  [index: string]: AnyTidalProperty // Type which contains all Tidal* property types.

  private readonly myPrimitive = new TidalPrimitive(null)
  private readonly myObject = new TidalObject({prop1: "some string"})
}
Class Index

Note, currently an index declaration is required either in the implementing class or the StateVessel interface. If the class is expected to have more than just Tidal* properties, then simply use any for the index value:

class MyClass implements MyState {
  [index: string]: any // Simply use 'any' for more complex classes.

  private readonly myPrimitive = new TidalPrimitive(null)
  private readonly myObject = new TidalObject({prop1: "some string"})

  otherProp = 123
  
  myFn(): void {}
}

Linking

Linking is how a state relationship graph is constructed for your application. State vessels that have provided a link to another vessel can be accessed by that other vessel. Links can be created dynamically at runtime or configured and bootstrapped at application initialization for convenience when dealing with static or long-lived links. See Configuration Bootstrap

When a link is initialized it is permanently added as a "transient" link which means that the link is added any time both sides of the link relationship are initialized in the state pool. The link can be removed by calling unlink() from the side A state navigator of an A -> B link. See: Unlinking and LinkStatus

const myStateName = 'MY_STATE_NAME'
const myNavigator = TidalApplication.initNavigator(myStateName)

const otherStateName = 'MY_OTHER_STATE'
myNavigator.link(otherStateName) // Can pass in a single name, or an array of names.

// Check the link status with a LinkStatus object.
const linkStatus = TidalApplication.getLinkStatus(myStateName, otherStateName)
// The link exist in transient form
linkStatus.exists.get() // true
// Call the isActiveCheck to see if the link exists (in transient form) and is currently active.
linkStatus.isActiveCheck.get()() // false

// The state with otherStateName does not yet exist yet, so here we build and initialize it.
TidalApplication.headlessStateBuilder(otherStateName, { prop: new TidalPrimitive(false) }).init()

// Now that both sides of the link exist, the link is active
linkStatus.isActiveCheck.get()() // true

Note, the above example is only for illustrative purposes, since in a real-world case there would be no reason to link from a navigator to a headless state, since a headless state never has a navigator and would never have any need to access and use properties from another state. However, linking from a headless state to another state is more practical and can be done with the builder:

const builder = TidalApplication.headlessStateBuilder(myStateName, { prop: new TidalPrimitive(false) })
builder.withLinks([otherStateName1, otherStateName2]).init()

When 'otherStateName1' or 'otherStateName2' are initialized separately (or if they were already present), then a link from 'myStateName' will automatically be created to them.

Unlinking and LinkStatus

When a link is created it exists as a separate entity that represents a 'transient' link whose status can be monitored with a LinkStatus object, shown previously in the linking example. When both sides of an A -> B link exist, then the actual link between them is created. In order to unlink two states, regardless of the current status of the link, simple call StateNavigator#unlink:

// Assumes navigator created, states linked, and LinkStatus retrieved for link A -> B.
let linkStatusAtoB: LinkStatus
let stateANavigator: StateNavigator<MyState>

stateANavigator.unlink(sideBStateName)

linkStatusAtoB.exists.get() // false
linkStatus.isActiveCheck.get()() // false

Note, if an attempt is made to create a LinkStatus object prior to creating the link, then an error is thrown. However, any LinkStatus created prior to unlinking can still be used.

Services

A ServiceVessel contains one or more TidalService and/or TidalExecutor properties. TidalService properties are a special type of TidalObject which take a service call function in their constructor (TidalServiceFn). This function must return an Observable that emits any type of object event (as opposed to emitting a primitive or function type), such as a TidalObject.

Service vessels are intended to contain common logic such as network calls that are likely used by many agents/vessels in the application. They can handle ephemeral service calls with a single consumer, or ongoing service calls with multiple consumers monitoring some shared service. Service vessels are automatically linked to any consumer upon retrieval so will likely have a larger relationship graph than StateVessels, which may have none or only a few links.

Services can be initialized the same as any StateVessel (from its own constructor with a navigator), but for non-ephemeral or app-wide services it is simplest to bootstrap the service.

Service Example
//// Setup service interfaces and class ////
interface ServiceResponse {
  prop1: string
}
// ServiceVessel interfaces extend the ServiceVessel type
interface MyServiceVessel extends ServiceVessel {
  readonly serviceCallObs: TidalService<ServiceResponse>
  readonly serviceCallTidal: TidalService<ServiceResponse>
}
class MyServiceClass implements MyServiceVessel {
  [index: string]: AnyServiceVesselProperty // Type for all valid ServiceVessel properties

  // TidalServices take a function that returns an Observable and can accept any number of args.
  private readonly serviceCallObs = new TidalService((arg1: string) => Observable.of({prop1: arg1}))
  // All Tidal* and Local* properties are Observables, so those can also be returned by the function.
  private readonly serviceCallTidal = new TidalService(() => new TidalObject({prop1: "my prop1 val"}))

  // TidalExecutors are also valid ServiceVessels properties
  private readonly serviceExecutor = new TidalExecutor()
}

//// Initialize and use the service ////
const myServiceName = 'MY_SERVICE_NAME'
const myNavigator = TidalApplication.initNavigator('MY_STATE')

// Initialize with the navigator
myNavigator.initService(myServiceName)

// Retreive the service later on, typically from another location/navigator
const serviceVessel = TidalApplication.initNavigator('OTHER_STATE').getService<MyServiceVessel>(myServiceName)

// Use the executor of a TidalService property to execute it.
serviceVessel.serviceCallObs.executor.execute('my other string')
console.log(myService.serviceCallObs.get().prop1) // "my other string"

// Emitted after below .execute() call:
// "my prop1 val"
serviceVessel.serviceCallTidal.subscribe((next: ServiceResponse) => console.log(next.prop1))
serviceVessel.serviceCallTidal.executor.execute()

It is also possible to bootstrap/initialize a service at the beginning of the application lifecycle. See: Configuration Bootstrap

Each TidalService property has an associated ServiceCallStatus and TidalExecutor property. The ServiceCallStatus provides real-time insight into the status of the provided ServiceCallFn, while the TidalExecutor provides the ability to execute the service function:

const status: ServiceCallStatus = serviceVessel.service.status
console.log(status.get()) // 'INIT'

serviceVessel.service.executor.execute()
console.log(status.get()) // 'PROCESSING' - Status so long as the observable has not emitted yet.

... // After successful call, event emitted.
console.log(status.get()) // 'SUCCESS'

If the execution fails, then the status will be ServiceCallStatusEnum.FAILURE, and any error response emmitted by the Observable in the service function will be available in the 'error' TidalObject property:

serviceVessel.service.execute() // Assume this execution fails, Observable emits an error
console.log(serviceVessel.service.status.get()) // 'FAILURE'
console.log(serviceVessel.service.error.get()) // '{ error: 'Some error msg.' }' Note: error format not controlled by tidal-state.
Resetting Services

It is useful to be able to reset a TidalService property or ServiceVessel. Often there will be a case where after a certain action is taken, the property should no longer emit the latest result from a network call, so should be reset to the default (which is always null for a TidalService). The approach to doing this differs depending on whether this applies to the entire ServiceVessel (potentially containing multiple TidalService and TidalExecutor properties) or only a single TidalService property, but since TidalService properties are read-only, you will have to expose this functionality through one or more TidalExecutor properties that can be called by service consumers.

class MyServiceImpl implements MyService {
    [index: string]: any
    
    private readonly serviceCall = new TidalService(() => new TidalObject({prop: ''}))
    private readonly resetServiceCall = new TidalExecutor()
    private readonly navigator: StateNavigator<MyService>
    
    constructor() {
      this.navigator = TidalApplication.initNavigator(serviceName)
      this.navigator.initOwnService(this)

      // Configure the execution behavior of the reset executor to reset the TidalService property.
      this.resetServiceCall.onExecute((...args: any[]) => {
        // All TidalService properties are read-only, therefore the state's navigator must reset it.
        this.navigator.reset(this.serviceCall)
      })
    }
}

// ... initialize the ServiceVessel in the state pool (either through another StateNavigator or Config Bootstrap)

// ... elsewhere, retrieve the service (not shown)
let myService: MyService

// Call the reset executor
myService.resetServiceCall.execute()

When the above 'resetServiceCall' TidalExecutor is executed, then it will reset the 'serviceCall' TidalService property. This pattern is obviously very flexible, but also potentially dangerous since the executor could expose any internal action to callers. Therefore, be very concise and specific in what you setup your TidalExecutor to do when following this pattern.

Single Execution Service Calls

If a service call needs to isolate a TidalService property call to a single, synchronous request/response pair, then a TidalService executor can be called through the TidalExecutor#withCallback function. This function takes an ExecutionCallback function and returns a SingleExecution object that has an execute function that performs the same actions as the normal TidalExecutor#execute call, but upon completion of the single request, the appropriate function in the ExecutionCallback is called.

type ServiceResponse = {prop: string}
const service: TidalService<ServiceResponse> // Assumes initialized properly

const singleExecution: SingleExecution = service.executor.withCallback({
  onSuccess: (response: ServiceResponse) => {
    // Do something with the response
    console.log(response)
  },
  onFailure: (error: any) => console.log('Error!', error) // Optional onFailure function
})

singleExecution.execute('anyArg') // onSuccess will be executed after the service function completes

Configuration Bootstrap

Configuration for links and services can be initialized at application start-up:

// Configure links with LinkConfig objects
const linkConfig1: LinkConfig = {
    sideAName: 'STATE_1',
    sideBLinks: [{
      sideBName: 'STATE_2',
    }]
  }
const linkConfig2: LinkConfig {
    sideAName: 'STATE_3',
    sideBLinks: [{
      sideBName: 'STATE_2',
      isTwoWayLink: true // Optional 'isTwoWayLink' property will create two links (A -> B and B -> A)
    }]
  }

// Configure services with ServiceVesselConfig objects
const service1Config: ServiceVesselConfig = {
  serviceName: 'SERVICE_1',
    provider: () => {
      return {
        serviceProp: new TidalService(() => new TidalObject({prop: false}))
      }
    }
  }
const service2Config: ServiceVesselConfig = {
  serviceName: 'SERVICE_2',
    provider: () => {
      return {
        serviceProp: new TidalService(() => Observable.of({prop: ''})) // RxJS Observable
      }
    }
  }

// Populate the final BootstrapConfig object.
const config: BootstrapConfig = {
  services: [service1Config, service2Config],
  links: [linkConfig1, linkConfig2]
}

// Initialize it somewhere early in the application lifecycle
TidalApplication.bootstrapConfig(config)

Following the above configuration and initialization, links and service can be interacted with as normal throughout the application's lifecycle. See: Linking and Services.

Self-Initializing Service

Sometimes for more complex ServiceVessel use cases the service will have its own internal StateNavigator. If this is the case and the navigator is initializing the service vessel state itself with a call to StateNavigator#initOwnService, then the configuration bootstrapper must be informed of this.

For self-initializing services, set the optional property 'isSelfInitializing' to true:

const serviceName = 'MY_COMPLEX_SERVICE'

class MyComplexServiceClass implements MyComplexService {
  // ... service properties here
  private readonly navigator: StateNavigator<MyComplexService>

  constructor() {
    this.navigator = TidalApplication.initNavigator(serviceName)
    // Self-initializing services will make this call
    this.navigator.initOwnService(this)
  } 
}
const serviceConfig: ServiceVesselConfig = {
  serviceName: serviceName,
  provider: () => new MyService(),
  isSelfInitializing: true
}

Note that a self-initializing service must call StateNavigator#initOwnService, rather than StateNavigator#initOwnState.

Headless States

Most states will likely need a navigator if the state will be dynamically interacting with other states in the state pool. However, in some cases a state behaves more like a shared object or data store that has no agency of its own. In this case, it is often simpler to use a headless state:

interface MyHeadlessState extends StateVessel {
  readonly prop1: TidalPrimitive<string>
  readonly prop2: TidalPrimitiveRW<boolean>
}

const stateSourceObj = {
  prop1: new TidalPrimitive('my string'),
  prop2: new TidalPrimitiveRW(false)
} as MyHeadlessState

const myStateName = 'HEADLESS_NAME'
const builder: HeadlessStateBuilder = TidalApplication.headlessStateBuilder(myStateName, stateSourceObj)
const newStateVessel = builder.init()

Note, the only way any StateNavigator will have access to the new state is to initialize it with links. The above example would not be accessible to any other state's StateNavigator, because there are no links. To initialize with links, use the HeadlessStateBuilder#withLinks method:

const builder = TidalApplication.headlessStateBuilder('MY_STATE', {prop: new TidalPrimitive(false)})
builder.withLinks(['OTHER_STATE_1', 'OTHER_STATE_2']).init()

Now, 'OTHER_STATE_1' and 'OTHER_STATE_2' may retrieve 'MY_STATE' as normal with the StateNavigator#getState method. See: State Navigator.

Important Note: A headless state cannot be destroyed by a StateNavigator if there is no link from the headless state to the navigator. If you do not want a headless state to persist for the lifetime of the application, make sure at least one state vessel with a StateNavigator is provided a link to the headless state, so that it can destroy it. See: Safely Destroying and Cleaning Up (Resetting) State

Transient States

Transient states provide a hook into state initialization so that interactions with a state can be setup prior to the state being initialized and known to the state pool. If you attempt to retrieve a state with StateNavigator#getState and the state does not exist you will get an Error. If it is possible that a state may not exist when you setup interactions with it, then consider using a TransientState instead.

A TransientState is initialized through a StateNavigator:

// Assumes initialized as normal
interface SomeOtherState extends StateVessel {
  prop: TidalPrimitive<string>
}
let navigator: StateNavigator<MyState>

const transientState = navigator.initTransientState<SomeOtherState>('SOME_OTHER_STATE')

After being initialized a TransientState immediately provides insight into when its target StateVessel exsits in the state pool through the stateExists property, which is a TidalPrimitive:

const exists: TidalPrimitive<boolean> = transientState.stateExists
console.log(exists.get()) // 'true' or 'false'

In order to setup asynchronous state interactions, you must call onInit() and/or onDestroy(). The onInit() method allows you to setup behavior for when the target StateVessel is initialized, while the onDestroy() method sets up behavior for when the StateVessel is destroyed:

transientState.onInit((state: SomeOtherState) => {
  // Setup a subscription on a property after the state is initialized in the state pool
  state.prop.subscribe(next => console.log(next))
})
transientState.onDestroy((state: SomeOtherState) => {
  console.log('State being destroyed! Prop value was: ' + state.prop.get())
})

Transient states are useful for setting up reliable state interactions even if another state is ephemeral, or only sometimes exists.

Safely Destroying and Cleaning Up State

All state managed in tidal-state is setup for easy destruction and clean-up when it goes out of scope or is no longer used. Properly cleaning up your state ensures you only keep your application state alive so long as it is needed and protect against things like memory leaks. Some states might persist for as long as the application is running, others may be reset or destroyed immediately after being consumed once, while most are likely somewhere in between.

All objects in tidal-state have a destroy() method that takes care of clean-up tasks specifically related to the object on which destroy is called. For example, for any Tidal* property or their derivatives, calling destroy() will call complete() and unsubscribe() on the internal BehaviorSubject, any ownership association on the property will be cleared, and any initial value reference will be cleared.

const tidalProp = new TidalPrimitive('initial string')

tidalProp.subscribe(
  next => console.log(next), // Immediately outputs: 'initial string'
  error => {},
  complete => console.log('Completed!')
)

tidalProp.destroy() // 'Completed!'

The only way a StateVessel can be destroyed and removed from the state pool is through a StateNavigator. Simply call StateNavigator#destroyState:

const SOME_STATE_NAME = 'SOME_STATE_NAME'
let navigator: StateNavigator<MyState> // Assumes navigator initialized as normal

navigator.destroyState(SOME_STATE_NAME)

After the above call the StateVessel with name 'SOME_STATE_NAME' will no longer exist in the state pool.

It is possible to destroy a navigator's own StateVessel (MyState above) through this same method, but this will not fully cleanup the StateNavigator and other associated items. To fully cleanup a StateNavigator, instead call StateNavigator#destroy, which will:

  • Destroy all child states created by this navigator.
  • Destroy all TransientStates created by this navigator.
  • Destroy all ServiceVessels created by this navigator.
  • Call unlink for all links which were created from this StateVessel via this navigator.
  • Remove any references to this StateNavigator so that it frees up memory and can be re-created at any time.
  • Finally, destroy the StateVessel owned by this navigator.
navigator.destroy()

If you are using a framework that has a destroy hook like Angular's ngOnDestroy which gets called when a component goes out of scope, then that is the place to call StateNavigator#destroy for the navigator used within your component.

Resetting State

Sometimes a state does not need to be totally destroyed or removed from the state pool, but merely reset. As shown in the Properties section, individual properties can be reset. A StateNavigator can also reset an entire StateVessel (assuming all properties are RW, OR it owns the StateVessel). See: State Navigator.

It is also useful to be able to reset services. See: Resetting Services.

Seascape

Seascape is a feature of tidal-state which enables an app to view a snapshot of the current state of the entire application. It provides a summary of all states and their links so that an overall picture of the application state graph can be constructed.

There are two types of Seascape views: SeascapeView and TransientSeascapeView. The SeascapeView shows states which are currently present in the state pool and their active links, while the TransientSeascapeView shows all transient states that have been initialized and all transient links that exist.

Each of the views is emitted as an event from a special type of TidalObject, the Seascape and TransientSeascape types:

const seaScape: Seascape = TidalApplication.getSeascape()
const seaScapeView: SeascapeView = seaScape.get() // Do something with the view!

const transientSeascape: TransientSeascape = TidalApplication.getTransientSeascape()
transientSeascape.subscribe((next: TransientSeascapeView) => {
  // Do something with the view!
  console.log(next)
})

SeascapeView Interfaces

/**
 * Static representation of the current sea scape.
 * Emitted as an event by {@link Seascape}.
 */
export interface SeascapeView {
  /**
   * Map of state vessel name to its {@link StateScape} object.
   */
  readonly stateVessels: ReadonlyMap<string, StateScape>
  /**
   * Map of service vessel name to its {@link StateScape} object.
   */
  readonly serviceVessels: ReadonlyMap<string, StateScape>
}

export interface StateScape {
  /**
   * Name of the {@link StateVessel} represented by this {@link StateScape}.
   */
  readonly stateName: string

  /**
   * If this is a normal state, the state owner will be the same as {@link StateScape#stateName}.
   * If this is a child state, the owner will be some other state.
   * If this state was created by the tidal-state library, is a {@link ServiceVessel}, or is a
   * headless state (a state without a navigator), then this will be null.
   */
  readonly stateOwner: string | null

  /**
   * Set of state names that represent side B of link A -> B, where A is the
   * stateName for this {@link StateScape}.
   */
  readonly outgoingLinks: ReadonlySet<string>

  /**
   * Set of state names that represent side A of link A -> B, where A is another
   * {@link StateVessel}'s name, and B is the state represented by this {@link StateScape}.
   */
  readonly incomingLinks: ReadonlySet<string>
}

TransientSeascapeView Interfaces

/**
 * Static representation of the current transient sea scape.
 * Emitted as an event by {@link TransientSeascape}.
 *
 * The transient application state is an indication of states or links which may or may not
 * currently exist, but which have been established as potential links or tracked as potential
 * states by one or more agents ({@link StateNavigator}s).
 */
export interface TransientSeascapeView {
  /**
   * Map of transient state name to its {@link TransientStateScape} object.
   */
  readonly transientStates: ReadonlyMap<string, TransientStateScape>
  /**
   * Map of side A state name to the array of {@link LinkPair}s for all links A -> B where B is any
   * other state that side A has a one-way link to.
   */
  readonly transientLinks: ReadonlyMap<string, LinkPair[]>
}

export interface TransientStateScape {
  /**
   * The name of the {@link StateVessel} represented by this {@link TransientState}.
   */
  readonly stateName: string
  /**
   * The name of the owner of this {@link TransientState}, or null if created by the
   * tidal-state library.
   */
  readonly transientStateOwner: string
}

export interface LinkPair {
  readonly sideAName: string,
  readonly sideBName: string
}

Various Util Functions

Property Methods

TBD..

Other Functions

TBD..

Release History

1.0.0 (Pre-Release)

  • Significant refactor and additions to state management features.
  • Linking.
  • Config bootstrap.
  • Seascape.
  • Renamed various features and streamlined API.

0.4.0 (Released)

  • Add withCallback to TidalExecutor and onExecuteWithCallback for calling an executor with callback behavior.

0.3.0 (Released)

  • New TidalNavigator#getTransientState method now returns the new TransientState type. The onInit and onDestroy methods allow setting up interactions with StateObjects immediately after they're initialized in the state pool, or just before they are destroyed and removed. TransientStates can be used to setup state interactions regardless of whether the state has been initialized or not.
  • Add new TidalService type for representing service calls as non-finite event streams. Includes internal ServiceCallStatus property for tracking service call state and TidalExecutor for synchronously triggering event propagation.
  • Added new TidalApplication#initFreeState method (now headlessStateBuilder), which initializes a "free" state in the application state pool, which is not owned by any caller, and whose access is therefore fully determined by the required StateAccessRule that is passed in. This can be used for semi-global states or states which have a longer lifetime than their consumers.
  • Added resetAllLocalProperties(obj) util method. Resets all Local* properties contained in the passed in object.
  • Add ability to chain two Tidal or Local property to eachother with new sync() method. This causes value updates to propagate between them.
  • All destroy method calls now call Subject#unsubscribe() internally on all Tidal* properties, preventing any access to a tidal property value after it has been destroyed.
  • Reworked TidalFunction* generic types so a function type is used now instead of only the function return type.
  • ServiceCallStatus is now a shareable property, extending TidalPrimitive. The previous version is now called LocalServiceCallStatus, which still extends LocalPrimitive, and is therefore RW, but never is part of a shared StateObject.
  • Added orElseGet method to all Tidal and Local properties. Returns passed in value if the current value of the property is null or undefined.
  • Fixed TidalNavigator#resetState(stateId) method to only attempt resets on Tidal* properties as expected.
  • Removal of all functionality which was deprecated in v0.2.0.

0.3.x (Patches)

  • Update TidalExecutor to extend TidalObject now, which emits execution args as its event.

0.2.0

  • Published library is now fully ES5 backwards compatible.
  • Addition of Local property types. Local properties behave exactly the same as the normal Tidal type of properties, except that they are never pulled into the state pool (they're excluded from the StateObject), and thus are only available to the class in which they are defined. This allows fine-grained control over which properties are shareable and which are not, within a single class's state.
  • Streamlined state and navigator initialization. The 'TidalApplication#initNavigator' function followed by 'TidalNavigator#initState' is now the primary way of initializing a state object. Child states can be initialized with 'TidalNavigator#initChildState'. Both 'TidalApplication#initDetachedNavigator' and 'TidalApplication#initState' are now deprecated.
  • Added new 'destroy' method to TidalNavigator, which destroys the navigator, along with all of its owned states, including any child states created by the navigator.
  • Added isAnyReadOnlyProperty, isAnyReadWriteProperty, and isAnyLocalProperty util functions.
  • Fixed type handling on TidalObject/RW patch method. It is no longer possible to patch when the object is currently null.
  • Added new Toggleable* util properties. Convenient properties that take a primary and secondary value and can be toggled with .toggle(). Including the basic ToggleableBoolean, which just takes the initial value.
  • Add 'resetState' method to TidalNavigator objects, providing a simple way to reset all properties in any StateObject. Note, if any non-owner attempts to reset a state with read-only properties, then an error will be throw and none of the target StateObject's properties will be reset.
  • Added ServiceCallStatus util property. Easily track the status of a service call in various phases (init, processing, failure, success).
  • Added 'set' convenience method to TidalNavigator - a shorthand way for state owners to update a RO property value.

0.1.0

  • Initial stable release with core features.
1.0.0-17

3 years ago

1.0.0-16

3 years ago

1.0.0-12

3 years ago

1.0.0-13

3 years ago

1.0.0-14

3 years ago

1.0.0-15

3 years ago

1.0.0-9

4 years ago

1.0.0-10

4 years ago

1.0.0-11

4 years ago

1.0.0-8

4 years ago

1.0.0-7

4 years ago

1.0.0-6

4 years ago

1.0.0-5

4 years ago

1.0.0-4

4 years ago

1.0.0-3

4 years ago

1.0.0-2

4 years ago

1.0.0-1

4 years ago

1.0.0-0

4 years ago

0.4.0

4 years ago

0.4.0-1

4 years ago

0.2.0

5 years ago

0.0.27

5 years ago

0.0.26

5 years ago

0.0.25

5 years ago

0.0.24

5 years ago

0.0.23

5 years ago

0.0.22

5 years ago

0.0.21

5 years ago

0.0.20

5 years ago

0.0.19

5 years ago

0.0.18

5 years ago

0.0.17

5 years ago

0.0.16

5 years ago

0.0.15

5 years ago

0.0.14

5 years ago

0.0.13

5 years ago

0.0.12

5 years ago

0.0.11

5 years ago

0.0.10

5 years ago

0.0.9

5 years ago

0.0.8

5 years ago

0.0.7

5 years ago

0.0.6

5 years ago

0.0.5

5 years ago

0.0.4

5 years ago

0.0.3

5 years ago

0.0.2

5 years ago

0.0.1

5 years ago