Skip to content

Instantly share code, notes, and snippets.

@haruair
Created August 31, 2017 14:40
Show Gist options
  • Save haruair/330b06bfae25ae58883706b53e6ed923 to your computer and use it in GitHub Desktop.
Save haruair/330b06bfae25ae58883706b53e6ed923 to your computer and use it in GitHub Desktop.
event sourcing js practice code
function generatedId() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
)
}
class LocalStorageEventStore {
getStorage() {
var raw = window.localStorage.getItem('event-store')
var data = JSON.parse(raw) || []
data = data.map(v => {
var payload = v.domainEvent.payload
var props = this.convertPayloadToProps(payload)
var event = Object.create(eval(v['@type']).prototype, props)
return new DomainEvent(payload.id, event)
})
return data
}
convertPayloadToProps(payload) {
var props = {}
Object.keys(payload).forEach(key => {
props[key] = {
value: payload[key],
enumerable: true
}
})
return props
}
setStorage(stream) {
var data = stream.map(domainEvent => {
return {
'@type': domainEvent.payload.constructor.name,
domainEvent: domainEvent
}
})
window.localStorage.setItem('event-store', JSON.stringify(data))
}
load(id) {
var data = this.getStorage()
return data.filter(v => v.id == id)
}
append(id, eventStream) {
var stream = eventStream.map(payload => new DomainEvent(id, payload))
var data = this.getStorage()
this.setStorage(data.concat(stream))
}
fetch(criteria, callback) {
this.getStorage().filter(criteria).forEach(event => callback.call(null, event));
}
}
class EventStore {
load(id) {
this.events = this.events || [];
return this.events.filter(v => v.id == id)
}
append(id, eventStream) {
var stream = eventStream.map(payload => new DomainEvent(id, payload));
this.events = this.events || [];
this.events = this.events.concat(stream);
}
fetch(criteria, callback) {
this.events = this.events || [];
this.events.filter(criteria).forEach(event => callback.call(null, event));
}
}
class Repository {
constructor(eventStore, aggregateClass) {
this.eventStore = eventStore
this.aggregateClass = aggregateClass
}
load(id) {
var events = this.eventStore.load(id)
var aggregate = Object.create(this.aggregateClass.prototype)
aggregate.initializeState(events)
return aggregate
}
save(aggregate) {
var eventStream = aggregate.getUncommittedEvents()
this.eventStore.append(aggregate.id, eventStream)
}
}
class DomainEvent {
constructor(id, payload) {
this.id = id
this.payload = payload
}
}
class DomainEventStream {
constructor(events) {
this.events = events
}
}
class OpenCommand {
constructor(id, name) {
this.id = id;
this.name = name;
}
}
class WithdrawCommand {
constructor(id, amount) {
this.id = id;
this.amount = amount;
}
}
class DepositCommand {
constructor(id, amount) {
this.id = id;
this.amount = amount;
}
}
class CloseCommand {
constructor(id) {
this.id = id;
}
}
class OpenedEvent {
constructor(id, name) {
this.id = id;
this.name = name;
}
}
class WithdrawedEvent {
constructor(id, amount) {
this.id = id;
this.amount = amount;
}
}
class DepositedEvent {
constructor(id, amount) {
this.id = id;
this.amount = amount;
}
}
class ClosedEvent {
constructor(id) {
this.id = id;
}
}
class AggregateRoot {
apply(event) {
this.version = this.version !== undefined ? this.version : -1
this.version++
this.handle(event)
this.uncommittedEvents = this.uncommittedEvents || []
this.uncommittedEvents.push(event);
}
getUncommittedEvents() {
var stream = this.uncommittedEvents
delete this.uncommittedEvents
return stream;
}
getVersion() {
return this.version !== undefined ? this.version : -1;
}
handle(event) {
var eventName = event.constructor.name
eventName = 'apply' + eventName.charAt(0).toUpperCase() + eventName.slice(1);
if (!this[eventName]) {
return;
}
this[eventName](event)
}
initializeState(eventStream) {
this.version = this.version !== undefined ? this.version : -1
eventStream.forEach(domainEvent => {
this.version++
this.handle(domainEvent.payload)
})
}
}
class BankAccount extends AggregateRoot {
static open(id, name) {
var bankAccount = new BankAccount;
bankAccount.apply(new OpenedEvent(id, name));
return bankAccount;
}
withdraw(amount) {
if (this.closed) {
throw new Error('Account is already closed.')
}
this.apply(new WithdrawedEvent(this.id, amount))
}
deposit(amount) {
if (this.closed) {
throw new Error('Account is already closed.')
}
this.apply(new DepositedEvent(this.id, amount))
}
close() {
if (!this.closed) {
this.apply(new ClosedEvent(this.id))
}
}
applyOpenedEvent(event) {
this.id = event.id
this.name = event.name
this.balance = 0
this.closed = false
}
applyWithdrawedEvent(event) {
this.balance = this.balance || 0;
this.balance -= event.amount
}
applyDepositedEvent(event) {
this.balance = this.balance || 0;
this.balance += event.amount
}
applyClosedEvent(event) {
this.closed = true
}
}
class CommandHandler {
handle(command) {
var commandName = command.constructor.name;
commandName = 'handle' + commandName.charAt(0).toUpperCase() + commandName.slice(1);
if (!this[commandName]) {
return
}
this[commandName](command)
}
}
class BankAccountCommandHandler extends CommandHandler {
constructor(repository) {
super()
this.repository = repository
}
handleOpenCommand(command) {
var account = BankAccount.open(command.id, command.name)
this.repository.save(account)
}
handleCloseCommand(command) {
var account = this.repository.load(command.id)
account.close()
this.repository.save(account)
}
handleWithdrawCommand(command) {
var account = this.repository.load(command.id)
account.withdraw(command.amount)
this.repository.save(account)
}
handleDepositCommand(command) {
var account = this.repository.load(command.id)
account.deposit(command.amount)
this.repository.save(account)
}
}
class CommandBus {
subscribe(commandHandler) {
this.commandHandlers = this.commandHandlers || []
this.commandHandlers.push(commandHandler)
}
dispatch(command) {
this.commandHandlers = this.commandHandlers || []
this.isDispatching = this.isDispatching !== undefined ? this.isDispatching : false
if (this.isDispatching) {
return
}
this.isDispatching = true
this.commandHandlers.forEach(commandHandler => {
commandHandler.handle(command)
})
this.isDispatching = false
}
}
var eventStore = new LocalStorageEventStore
var repository = new Repository(eventStore, BankAccount)
var bankAccountCommandHandler = new BankAccountCommandHandler(repository)
var commandBus = new CommandBus
var edwardAccountId = generatedId();
var mindyAccountId = generatedId();
commandBus.subscribe(bankAccountCommandHandler)
commandBus.dispatch(new OpenCommand(edwardAccountId, 'Edward'))
commandBus.dispatch(new DepositCommand(edwardAccountId, 1000))
commandBus.dispatch(new DepositCommand(edwardAccountId, 1000))
commandBus.dispatch(new DepositCommand(edwardAccountId, 1000))
commandBus.dispatch(new DepositCommand(edwardAccountId, 1000))
commandBus.dispatch(new WithdrawCommand(edwardAccountId, 10000))
commandBus.dispatch(new CloseCommand(edwardAccountId))
var mindy = BankAccount.open(mindyAccountId, 'mindy')
mindy.deposit(100)
mindy.deposit(100000)
mindy.close()
repository.save(mindy)
console.log(eventStore)
var edwardAccount = repository.load(edwardAccountId)
console.log(edwardAccount)
eventStore.fetch(
v => v.payload instanceof DepositedEvent,
v => console.log(v)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment