Querying Data: Views
An event View is a component that observes event streams and applies a Reducer function to them to generate a data structure that is suitable for querying.
An example View: Users
Create a list of users from user.created
events and provider operations for users.
import {
setEventSourceName,
eventClientOnDatastore,
InMemoryDatastore,
setDataStore,
setEventClient, eventClient, EventicleEvent, EventView, dataStore, registerView
} from '@eventicle/eventiclejs';
class UserLoginView implements EventView {
consumerGroup: string = "user_login_view" (1)
streamsToSubscribe: string[] = ["app.users"] (2)
/**
* View reducer function.
*/
async handleEvent(event: EventicleEvent): Promise<void> {
switch(event.type) { (3)
case "user.register":
// Insert into some data store. Here using the built in datastore abstraction so we can do InMem during testing
await dataStore().createEntity("example", "user-view-record", {
username: event.data.username,
password: event.data.password
})
break;
default:
console.log("UserLoginView ignoring event type " + event.type)
}
}
/*
* An operation on the view. These will most commonly be API driven, and can any for of query.
* They should only read the data, not attempt to mutate it
*/
async checkPassword(username: string, password: string) { (4)
// do a hash/ password comparison
}
async allUsers() { (5)
(await dataStore().findEntity("example", "user-view-record", {})).map(value => {
return value.content
})
}
}
async function execute() {
setEventSourceName('my-cool-service');
setDataStore(new InMemoryDatastore());
setEventClient(eventClientOnDatastore());
let view = new UserLoginView()
await registerView(view) (6)
await eventClient().emit([
{
type: "user.created",
data: {
userName: "Personal Person"
}
}
], "app.users")
console.log(await view.allUsers()) (7)
}
execute()
1 | The consumer group is the same as for Redis Streams/ Kafka. Multiple view instances across processes will share processing. |
2 | 1..n streams to subscribe to. |
3 | View must handle all the potential event types in the stream. Normally this is done by picking out the required types and ignoring any others. |
4 | An operation on a view does not have to be a simple query, it can be more advanced, as with a login check. It does though, always have to be read only to preserve the 1 way data flow semantics of the system |
5 | A simple select all query on the view data structure. |
6 | Register the view with Eventicle for it to attach the streams to the view. Maintain a reference to it for your use to query the view data. |
7 | View will have the user record when this event has been async processed. |
In Memory Views
The persisted nature of event streams means that you can use the event stream as the system persistence Then have your views be purely in memory. This means that they will rebuild each time the application starts, but will require no external data storage.
An example View: Count of Users
Count the user registrations.
import {
setEventSourceName,
eventClientOnDatastore,
InMemoryDatastore,
setDataStore,
setEventClient, eventClient, EventicleEvent, EventView, dataStore, registerView
} from '@eventicle/eventiclejs';
import * as uuid from "uuid";
import {pause} from "@eventicle/eventiclejs/dist/util";
class UserLoginView implements EventView {
consumerGroup: string = "user_count_view" + uuid.v4() (1)
streamsToSubscribe: string[] = ["app.users"]
userRegistrations:number = 0; (2)
async handleEvent(event: EventicleEvent): Promise<void> {
switch(event.type) {
case "user.register":
this.userRegistrations++; (3)
break;
}
}
}
async function execute() {
setEventSourceName('my-cool-service');
setDataStore(new InMemoryDatastore());
setEventClient(eventClientOnDatastore());
await eventClient().emit([
{
type: "user.created",
data: {
userName: "Personal Person"
}
}
], "app.users")
let view = new UserLoginView()
await registerView(view) (5)
await pause(2000) // let the view come up to date
console.log(await view.userRegistrations) (4)
}
execute()
1 | Set the consumer group to be random. This means this instance will fully replay every time it connects to the event stream, and will receive all events, not share with other instances. |
2 | The view state, in this case as simple integer count. |
3 | Implement eventual consistency of the user counter via the event stream reducer in this view. |
4 | Will output 1 , showing that the view observed the event that was emitted before it was connected, due to the full replay of the stream. |
5 | Eventicle will connect the view to the requested event streams. |