@yogasoft/tidal-state
v1.0.0-17
Published
An object-oriented reactive state management library.
Downloads
50
Readme
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
- Structure shareable state objects with stateful properties.
- Share states and create state relationships with linking.
- Configure common services and links by bootstrapping at runtime.
- Subscribe to real-time views of the current application state.
- Fully typed classes and interfaces.
- Many additional utility features.
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.