Created
August 31, 2017 14:40
-
-
Save haruair/330b06bfae25ae58883706b53e6ed923 to your computer and use it in GitHub Desktop.
event sourcing js practice code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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