### Run Example (Bash) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Command to compile and run the Go example program that demonstrates Chronicle projections with SQLite. ```bash go run examples/6_projections/main.go ``` -------------------------------- ### Go SQL Transactor Example Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Provides an example skeleton for a SQL-based `Transactor` in Go. It demonstrates how to manage a database transaction, including starting, committing, and rolling back operations using `sql.Tx`. ```go package main import ( "context" "database/sql" "log" "os" "github.com/deluxeowl/chronicle/event" "github.com/deluxeowl/chronicle/version" ) // Mock implementations for demonstration type MockDB struct{} func (m *MockDB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) { log.Println("BeginTx called") // In a real scenario, this would return a valid *sql.Tx return nil, nil } type MockSQLEventLog struct { db *MockDB } // WithinTx demonstrates transaction management for a SQL-based event log. // It starts a transaction, executes a provided function within that transaction, // and then commits or rolls back based on the function's result. func (m *MockSQLEventLog) WithinTx(ctx context.Context, fn func(ctx context.Context, tx *sql.Tx) error) error { tx, err := m.db.BeginTx(ctx, nil) if err != nil { return err // Failed to start transaction } // Rollback is a no-op if the transaction is committed // In a real implementation, tx would be a valid *sql.Tx and defer tx.Rollback() would be used. if err := fn(ctx, tx); err != nil { // The function failed, so we'll rollback // In a real implementation: tx.Rollback() return err } // Success // In a real implementation: return tx.Commit() return nil } func main() { // Example usage (requires a real DB connection and implementation) log.Println("This is a mock example. A real DB connection is needed.") // Mocking the necessary components for the example to compile mockSQLEventLog := &MockSQLEventLog{ db: &MockDB{}, } ctx := context.Background() _ = mockSQLEventLog.WithinTx(ctx, func(ctx context.Context, tx *sql.Tx) error { log.Println("Executing function within transaction...") // Simulate some database operation using tx return nil }) } ``` -------------------------------- ### Install Chronicle Go Library Source: https://github.com/deluxeowl/chronicle/blob/main/README.md This snippet shows how to install the Chronicle library using the Go package manager. It's a prerequisite for using the toolkit in your Go projects. ```Shell go get github.com/DeluxeOwl/chronicle ``` -------------------------------- ### Implement Get Method for Custom Repository in Go Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Implements the `Get` method to load an aggregate. It utilizes the `aggregate.ReadAndLoadFromStore` helper to fetch and apply events from the event log to a new aggregate instance. ```Go func (r *CustomRepository[...]) Get(ctx context.Context, id TID) (R, error) { root := r.createRoot() // Create a new, empty aggregate instance. // This helper does all the heavy lifting of loading. err := aggregate.ReadAndLoadFromStore( ctx, root, r.eventlog, r.registry, r.serde, r.transformers, id, version.SelectFromBeginning, // Load all events ) if err != nil { var empty R // return zero value for the root return empty, fmt.Errorf("custom repo get: %w", err) } return root, nil } ``` -------------------------------- ### Create In-Memory Event Log Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Initializes a simple in-memory event log. This implementation stores all events in memory and is suitable for quickstarts, examples, and testing. However, it is not persistent and data is lost upon application restart. ```go eventlog.NewMemory() ``` -------------------------------- ### SQL Query for Reading Events Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Provides an example SQL query for reading event history from a database table. This query selects version, event name, and data for a specific log ID, ordered by version, suitable for an event.Reader implementation. ```SQL SELECT version, event_name, data FROM my_events_table WHERE log_id = ? AND version >= ? ORDER BY version ASC; ``` -------------------------------- ### Create New Empty Account Aggregate (Go) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Provides a constructor function NewEmpty() that returns a new, uninitialized Account aggregate, which is useful for starting new aggregate instances. ```Go func NewEmpty() *Account { return new(Account) } ``` -------------------------------- ### Go Chronicle Custom Snapshot Strategy Example Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Provides an example of a custom snapshot strategy in Go Chronicle. The custom function determines when to take a snapshot based on the aggregate's balance, specifically when it's a multiple of 250. ```Go aggregate.SnapStrategyFor[*account.Account]().Custom( func(ctx context.Context, root *account.Account, previousVersion, newVersion version.Version, committedEvents aggregate.CommittedEvents[account.AccountEvent]) bool { return true // always snapshot }), ``` ```Go func CustomSnapshot( ctx context.Context, root *Account, _, _ version.Version, _ aggregate.CommittedEvents[AccountEvent], ) bool { return root.balance%250 == 0 // Only snapshot if the balance is a multiple of 250 } ``` -------------------------------- ### Go Appender: Conflict Handling Example Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Illustrates how to handle version conflicts within the `AppendEvents` method. If the actual version from the database does not match the expected version, the transaction should be aborted and a `version.ConflictError` returned. ```Go if actualVersion != expectedVersion { return version.Zero, version.NewConflictError(expectedVersion, actualVersion) } ``` -------------------------------- ### Example Event Transformer Usage (Go) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Demonstrates the use of event transformers in Go for modifying events before serialization and after deserialization. Transformers can be used for encryption, compression, or up-casting event schemas. The order of application is crucial: write order is sequential, while read order is reversed. ```go package main import ( "fmt" "github.com/deluxeowl/chronicle/examples/internal/account" "github.com/deluxeowl/chronicle/examples/internal/crypto" "github.com/deluxeowl/chronicle/examples/internal/transformer" "github.com/deluxeowl/chronicle/pkg/eventlog" "github.com/deluxeowl/chronicle/pkg/stream" ) func main() { // Example: Encryption transformer key := []byte("my-secret-key") encryptTransformer := crypto.NewAESEncryptTransformer(key) // Example: Compression transformer compressTransformer := transformer.NewGzipTransformer() // Create an event log with multiple transformers // Order for writing: encrypt -> compress // Order for reading: decompress -> decrypt log, err := eventlog.NewMemoryWithTransformers( stream.NewMemoryStreamManager(), encryptTransformer, compressTransformer, ) if err != nil { panic(err) } fmt.Println("Event log with transformers created successfully.") // Further usage would involve appending and reading events... } ``` -------------------------------- ### Aggregate Versioning and Event Committing (Go) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Explains how the repository returns the new aggregate version and committed events after saving. It clarifies that the aggregate's version increments with each recorded event, starting from 0. ```Go ctx := context.Background() version, committedEvents, err := accountRepo.Save(ctx, acc) if err != nil { panic(err) } // The repository returns the new version of the aggregate, the list of committed events, and an error if one occurred. The version is also updated on the aggregate instance itself and can be accessed via `acc.Version()` (this is handled by `aggregate.Base`) // An aggregate starts at version 0. The version is incremented for each new event that is recorded. fmt.Printf("version: %d\n", version) for _, ev := range committedEvents { litter.Dump(ev) } ``` -------------------------------- ### Go: Handling Conflict Errors by Retrying Source: https://github.com/deluxeowl/chronicle/blob/main/README.md This Go code illustrates the process of handling a conflict error by re-loading the aggregate to get the latest state, re-applying the command, and attempting to save again. This is a common strategy for resolving optimistic concurrency conflicts. ```Go // 💥 Oh no! A conflict error occurred! //... //... try to withdraw again accUserA, _ = accountRepo.Get(ctx, accID) _, err = accUserA.WithdrawMoney(100) if err != nil { panic(err) } fmt.Println("User A tries to withdraw $100 and save...") version, _, err := accountRepo.Save(ctx, accUserA) if err != nil { panic(err) } fmt.Printf("User A saved successfully! Version is %d and balance $%d\n", version, accUserA.Balance()) // User A saved successfully! Version is 4 and balance $50 ``` -------------------------------- ### Go: Initial Account Deposit and Save Source: https://github.com/deluxeowl/chronicle/blob/main/README.md This Go code snippet demonstrates opening a new account, depositing money, and saving the initial state to the repository. It shows how the account version increments after events are applied and saved. ```Go acc, _ := account.Open(accID, time.Now(), "John Smith") _ = acc.DepositMoney(200) // balance: 200 _, _, _ = accountRepo.Save(ctx, acc) fmt.Printf("Initial account saved. Balance: 200, Version: %d\n\n", acc.Version()) ``` -------------------------------- ### Initialize Chronicle Repository (Go) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Demonstrates initializing the Chronicle transactional repository. It sets up an SQLite event log and hooks up the `accountProcessor` to handle transactional aggregate processing. ```go accountRepo, err := chronicle.NewTransactionalRepository( sqliteLog, accountMaker, nil, aggregate.NewProcessorChain( accountProcessor, ), ) ``` -------------------------------- ### Initialize Memory Event Log and Repository (Go) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Sets up the main application flow by creating an in-memory event log and an event-sourced repository for the Account aggregate. It handles potential errors during repository creation. ```Go package main import ( "context" "fmt" "time" "github.com/DeluxeOwl/chronicle" "github.com/DeluxeOwl/chronicle/eventlog" "github.com/DeluxeOwl/chronicle/examples/internal/account" "github.com/sanity-io/litter" ) func main() { // Create a memory event log memoryEventLog := eventlog.NewMemory() //... accountRepo, err := chronicle.NewEventSourcedRepository( memoryEventLog, // The event log account.NewEmpty, // The constructor for our aggregate nil, // This is an optional parameter called "transformers" ) if err != nil { panic(err) } // Create an account acc, err := account.Open(AccountID("123"), time.Now(), "John Smith") if err != nil { panic(err) } // Deposit some money err = acc.DepositMoney(200) if err != nil { panic(err) } // Withdraw some money _, err = acc.WithdrawMoney(50) if err != nil { panic(err) } ctx := context.Background() version, committedEvents, err := accountRepo.Save(ctx, acc) if err != nil { panic(err) } fmt.Printf("version: %d\n", version) for _, ev := range committedEvents { litter.Dump(ev) } } ``` -------------------------------- ### Shell: Running Project Tasks Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Lists available tasks for the Chronicle project using the `task --list` command and demonstrates the workflow for running linters and tests after making changes. ```Shell task --list # After making changes: task lint task test ``` -------------------------------- ### Initialize Repository with CryptoTransformer Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Demonstrates initializing an event-sourced repository with the CryptoTransformer, enabling it to process events for encryption and decryption. ```go func main() { memoryEventLog := eventlog.NewMemory() // A 256-bit key (32 bytes) encryptionKey := []byte("a-very-secret-32-byte-key-123456") cryptoTransformer := account.NewCryptoTransformer(encryptionKey) // ... accountRepo, err := chronicle.NewEventSourcedRepository( memoryEventLog, account.NewEmpty, []event.Transformer[account.AccountEvent]{cryptoTransformer}, ) if err != nil { panic(err) } } ``` -------------------------------- ### Open and Save Accounts (Go) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Shows how to open new accounts for 'Alice' and 'Bob' using the `accountv2.Open` function and then save these accounts using the `accountRepo.Save` method. This process triggers the event handling and projection updates. ```go // Alice's account accA, _ := accountv2.Open(accountv2.AccountID("alice-account-01"), timeProvider, "Alice") _ = accA.DepositMoney(100) _ = accA.DepositMoney(50) _, _, err = accountRepo.Save(ctx, accA) // Bob's account accB, _ := accountv2.Open(accountv2.AccountID("bob-account-02"), timeProvider, "Bob") _ = accB.DepositMoney(200) _, _, err = accountRepo.Save(ctx, accB) ``` -------------------------------- ### Poller Goroutine - Select Oldest Record - Go Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Illustrates the start of the poller goroutine, which periodically checks the outbox table. It begins a transaction and selects the oldest record from the 'outbox_account_events' table to be processed. ```Go go func() { ticker := time.NewTicker(100 * time.Millisecond) for { select { case <-ctx.Done(): return case <-ticker.C: // 1. Begin transaction tx, err := db.BeginTx(ctx, nil) // ... // 2. Select the oldest record row := tx.QueryRowContext(ctx, "SELECT id, aggregate_id, event_name, payload FROM outbox_account_events ORDER BY id LIMIT 1") // ... scan row // 3. Publish the message to the bus ``` -------------------------------- ### Integrating TimeProvider with Snapshotting and Account Opening (Go) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Shows the correct integration of the time provider with the event-sourced repository that includes snapshotting, and also with the account opening function. ```go accountRepo, err := chronicle.NewEventSourcedRepositoryWithSnapshots( baseRepo, accountSnapshotStore, &accountv2.Snapshotter{ TimeProvider: timeProvider, // ⚠️ The same timeProvider }, aggregate.SnapStrategyFor[*accountv2.Account]().EveryNEvents(3), ) acc, err := accountv2.Open(accID, timeProvider, "John Smith") // ⚠️ The same timeProvider // ... // Print the events for ev := range memoryEventLog.ReadAllEvents(ctx, version.SelectFromBeginning) { fmt.Println(ev.EventName() + " " + string(ev.Data())) } ``` -------------------------------- ### Go: Loading Account State for Concurrent Access Source: https://github.com/deluxeowl/chronicle/blob/main/README.md This Go code illustrates loading the same account state twice, simulating two users accessing it concurrently. It prints the version and balance for each loaded account, setting the stage for a potential conflict. ```Go accUserA, _ := accountRepo.Get(ctx, accID) fmt.Printf("User A loads account. Version: %d, Balance: %d\n", accUserA.Version(), accUserA.Balance()) // User A loads account. Version: 2, Balance: 200 accUserB, _ := accountRepo.Get(ctx, accID) fmt.Printf("User B loads account. Version: %d, Balance: %d\n\n", accUserB.Version(), accUserB.Balance()) // User B loads account. Version: 2, Balance: 200 ``` -------------------------------- ### Open Account with Event Metadata Generation (Go) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Demonstrates opening an account and recording the 'accountOpened' event, utilizing a meta-generator to create event metadata. Includes a check to prevent account opening on Sundays. ```go func Open(id AccountID, timeProvider timeutils.TimeProvider, holderName string) (*Account, error) { makeEmptyAccount := NewEmptyMaker(timeProvider) // Create the maker with the dependencies a := makeEmptyAccount() currentTime := a.timeProvider.Now() if currentTime.Weekday() == time.Sunday { return nil, errors.New("sorry, you can't open an account on Sunday ¯\\_(ツ)_/¯") } if err := a.recordThat(&accountOpened{ ID: id, OpenedAt: currentTime, HolderName: holderName, EventMetadata: a.metaGenerator.NewEventMeta(), // We're using the generator to generate the metadata }); err != nil { return nil, fmt.Errorf("open account: %w", err) } return a, nil } ``` -------------------------------- ### SQL Pessimistic Locking Example Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Illustrates a typical SQL transaction using pessimistic locking with `FOR UPDATE`. This method locks a row when it's read, preventing other transactions from modifying it until the current transaction is committed or rolled back, thereby avoiding race conditions. ```SQL BEGIN TRANSACTION; SELECT balance FROM accounts WHERE id = 'acc-123' FOR UPDATE; -- Application logic checks balance UPDATE accounts SET balance = balance - 50 WHERE id = 'acc-123'; COMMIT; ``` -------------------------------- ### Initialize Chronicle Repository with Snapshots in Go Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Initializes a Chronicle repository with snapshotting capabilities. This involves creating a snapshot store (e.g., `snapshotstore.NewMemoryStore`) and configuring a snapshot strategy (e.g., `aggregate.SnapStrategyFor[*account.Account]().EveryNEvents(3)`). ```Go package main import ( "context" "fmt" "time" "github.com/DeluxeOwl/chronicle" "github.com/DeluxeOwl/chronicle/eventlog" "github.com/DeluxeOwl/chronicle/examples/internal/account" "github.com/DeluxeOwl/chronicle/snapshotstore" ) func main() { memoryEventLog := eventlog.NewMemory() baseRepo, _ := chronicle.NewEventSourcedRepository( memoryEventLog, account.NewEmpty, nil, ) accountSnapshotStore := snapshotstore.NewMemoryStore( func() *account.Snapshot { return new(account.Snapshot) }, ) // ... accountRepo, err := chronicle.NewEventSourcedRepositoryWithSnapshots( baseRepo, accountSnapshotStore, &account.Snapshotter{}, aggregate.SnapStrategyFor[*account.Account]().EveryNEvents(3), ) if err != nil { panic(err) } } ``` -------------------------------- ### Go Appender: Preparing Records for Insertion Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Shows how to use the `events.ToRecords` helper function to prepare `RawEvents` for insertion. This function takes the log ID and current version, returning a slice of `*event.Record` structs with correctly assigned sequential versions. ```Go // If actual version is 5, these records will be for versions 6, 7, ... recordsToInsert := events.ToRecords(id, actualVersion) ``` -------------------------------- ### Implement Custom Retry for Repository Save in Go Source: https://github.com/deluxeowl/chronicle/blob/main/README.md This Go code defines a `SaverWithRetry` struct that wraps an existing `aggregate.Saver`. It implements a custom `Save` method using `retry.DoWithData` to automatically retry operations up to 3 times, specifically retrying on `version.ConflictError`. The example also shows how to integrate this retry mechanism into a `FusedRepo`. ```Go type SaveResult struct { Version version.Version CommittedEvents aggregate.CommittedEvents[account.AccountEvent] } type SaverWithRetry struct { saver aggregate.Saver[account.AccountID, account.AccountEvent, *account.Account] } func (s *SaverWithRetry) Save(ctx context.Context, root *account.Account) (version.Version, aggregate.CommittedEvents[account.AccountEvent], error) { result, err := retry.DoWithData( func() (SaveResult, error) { version, committedEvents, err := s.saver.Save(ctx, root) if err != nil { return SaveResult{}, err } return SaveResult{ Version: version, CommittedEvents: committedEvents, }, }, retry.Attempts(3), retry.Context(ctx), retry.RetryIf(func(err error) bool { // Only retry on ConflictErr or specific errors var conflictErr *version.ConflictError return errors.As(err, &conflictErr) }), ) if err != nil { var zero version.Version var zeroCE aggregate.CommittedEvents[account.AccountEvent] return zero, zeroCE, err } return result.Version, result.CommittedEvents, nil } ``` ```Go accountRepo, _ := chronicle.NewEventSourcedRepository( memoryEventLog, account.NewEmpty, nil, ) repoWithRetry := &aggregate.FusedRepo[account.AccountID, account.AccountEvent, *account.Account]{ AggregateLoader: accountRepo, VersionedGetter: accountRepo, Getter: accountRepo, Saver: &SaverWithRetry{ saver: accountRepo, }, } ``` -------------------------------- ### Generate Repository Mock with moq Source: https://github.com/deluxeowl/chronicle/blob/main/README.md This Go code snippet demonstrates how to use the `go:generate` directive with the `moq` library to create mock implementations for the `Repository` interface in the `aggregate` package. It specifies the package for the mock, skips ensuring dependencies, removes the mock file if it exists, and defines the output file name. ```go // in aggregate/repository.go //go:generate go run github.com/matryer/moq@latest -pkg aggregate_test -skip-ensure -rm -out repository_mock_test.go . Repository ``` -------------------------------- ### Create Constructor for Custom Repository in Go Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Provides a constructor function for the custom repository. It initializes the repository with dependencies and registers the aggregate's events using the provided registry. ```Go func NewCustomRepository[...]( eventLog event.Log, createRoot func() R, // ... ) (*CustomRepository[...], error) { repo := &CustomRepository[...]{ eventlog: eventLog, createRoot: createRoot, registry: event.NewRegistry[E](), serde: serde.NewJSONBinary(), // Default to JSON, you can also change this. } err := repo.registry.RegisterEvents(createRoot()) if err != nil { return nil, fmt.Errorf("new custom repository: %w", err) } return repo, nil } ``` -------------------------------- ### Create In-Memory Snapshot Store Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Initializes an in-memory snapshot store that stores snapshots in a map. This is perfect for testing but is limited to single-process use and data is lost on restart. ```go snapshotstore.NewMemoryStore(...) ``` -------------------------------- ### Shell: Clone Repository and Enter Devbox Shell Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Provides the shell commands to clone the Chronicle project repository and then enter the development environment managed by Devbox. This ensures all necessary tools and dependencies are available. ```Shell git clone https://github.com/DeluxeOwl/chronicle cd chronicle devbox shell ``` -------------------------------- ### Run Event Log Benchmarks Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Executes Go tests for the event log package, focusing on benchmark performance and memory allocation. It uses the '-bench=.' flag to run all benchmarks and '-benchmem' to include memory statistics. ```sh task bench-eventlogs task: [bench-eventlogs] go test ./eventlog -bench=. -benchmem -run=^$ ./... goos: darwin goarch: arm64 pkg: github.com/DeluxeOwl/chronicle/eventlog cpu: Apple M3 Pro ``` -------------------------------- ### Using Global and Specific Transformers with AnyTransformerToTyped (Go) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Demonstrates how to combine a specific `CryptoTransformer` with a global `LoggingTransformer` for an account repository. It uses `event.AnyTransformerToTyped` to adapt the generic logger to the specific aggregate's transformer type. ```go cryptoTransformer := account.NewCryptoTransformer(deletedKey) loggingTransformer := &LoggingTransformer{} forgottenRepo, _ := chronicle.NewEventSourcedRepository( memoryEventLog, account.NewEmpty, []event.Transformer[account.AccountEvent]{ cryptoTransformer, event.AnyTransformerToTyped[account.AccountEvent](loggingTransformer), }, ) ``` -------------------------------- ### Integrate Outbox Processor in Repository - Go Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Demonstrates how to integrate the AccountOutboxProcessor into the TransactionalRepository in the main function. It shows the creation of the repository with the outbox processor added to the processor chain. ```Go func main() { db, err := sql.Open("sqlite3", "file:memdb1?mode=memory&cache=shared") // ... pubsub := examplehelper.NewPubSubMemory[OutboxMessage]() outboxProcessor, err := accountv2.NewAccountOutboxProcessor(db) // ... sqliteLog, err := eventlog.NewSqlite(db) // ... repo, err := chronicle.NewTransactionalRepository( sqliteLog, accountMaker, nil, aggregate.NewProcessorChain(outboxProcessor), // The outbox processor ) // ... } ``` -------------------------------- ### Implement TimeProvider Interface Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Defines the `TimeProvider` interface with a `Now()` method for retrieving the current time. It includes a `RealTimeProvider` implementation that uses `time.Now()` and a mockable interface for testing. ```Go package timeutils import "time" //go:generate go run github.com/matryer/moq@latest -pkg timeutils -skip-ensure -rm -out now_mock.go . TimeProvider type TimeProvider interface { Now() time.Time } var RealTimeProvider = sync.OnceValue(func() *realTimeProvider { return &realTimeProvider{} }) type realTimeProvider struct{} func (r *realTimeProvider) Now() time.Time { return time.Now() ``` -------------------------------- ### Recreating Account from Snapshot with TimeProvider (Go) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Illustrates fixing a compiler error by providing the necessary time provider dependency to the Snapshotter when recreating an account from a snapshot. ```go type Snapshotter struct { TimeProvider timeutils.TimeProvider } func (s *Snapshotter) FromSnapshot(snap *Snapshot) (*Account, error) { // Recreate the aggregate from the snapshot's data acc := NewEmptyMaker(s.TimeProvider)() // ✅ Create the maker and call it acc.id = snap.ID() // ... } ``` -------------------------------- ### Mocking Time for Account Operations (Go) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Demonstrates using a mocked time provider to set a future date (year 2100) for testing account opening and event sourcing scenarios. ```go func main() { memoryEventLog := eventlog.NewMemory() // We're using a mock time provider, generating the current time but setting the year to 2100 timeProvider := &timeutils.TimeProviderMock{ NowFunc: func() time.Time { now := time.Now() futureTime := now.AddDate(2100-now.Year(), 0, 0) return futureTime }, } accountMaker := accountv2.NewEmptyMaker(timeProvider) // Create the maker baseRepo, _ := chronicle.NewEventSourcedRepository( memoryEventLog, accountMaker, // Pass it into the repo nil, ) // ... } ``` -------------------------------- ### Iterating Through Database Rows Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Demonstrates how to iterate through database rows returned by a query, similar to how the `database/sql` package handles results. It shows the pattern for scanning rows and yielding records. ```Go for rows.Next() { // ... if err := rows.Scan(...) { // ... } if !yield(record, nil) { // ... } } ``` -------------------------------- ### Create PostgreSQL Event Log Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Initializes a persistent event log using PostgreSQL. This robust, production-ready implementation is ideal for distributed, high-concurrency applications. It uses database triggers for optimistic concurrency control, ensuring version consistency. ```go eventlog.NewPostgres(db) ``` -------------------------------- ### Go Chronicle Account Save and Load with Snapshot Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Demonstrates saving an account aggregate with uncommitted events, triggering a snapshot due to the `EveryNEvents` strategy. It then shows how to reload the account from the snapshot, which is faster as it avoids replaying all events. ```Go ctx := context.Background() accID := account.AccountID("snap-123") acc, err := account.Open(accID, time.Now(), "John Smith") // version 1 if err != nil { panic(err) } _ = acc.DepositMoney(100) // version 2 _ = acc.DepositMoney(100) // version 3 // Saving the aggregate with 3 uncommitted events. // The new version will be 3. // Since 3 >= 3 (our N), the strategy will trigger a snapshot. _, _, err = accountRepo.Save(ctx, acc) if err != nil { panic(err) } // The repository loads the snapshot at version 3. Then, it will ask the event log for events for "snap-123" starting from version 4. Since there are none, loading is complete, and very fast. reloadedAcc, err := accountRepo.Get(ctx, accID) if err != nil { panic(err) } fmt.Printf("Loaded account from snapshot. Version: %d\n", reloadedAcc.Version()) // Loaded account from snapshot. Version: 3 ``` -------------------------------- ### Create PostgreSQL Snapshot Store Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Initializes a persistent snapshot store using PostgreSQL. It stores snapshots in a table using an atomic `INSERT ... ON CONFLICT DO UPDATE` statement, making it suitable for durable snapshots. ```go snapshotstore.NewPostgresStore(...) ``` -------------------------------- ### Create SQLite Event Log Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Initializes a persistent event log using SQLite. This implementation leverages SQL databases for persistence and is suitable for single-server deployments. It requires managing scaling, which might be a consideration for very high event volumes. ```go eventlog.NewSqlite(db) ``` -------------------------------- ### Recording Money Withdrawal/Deposit with Event Metadata (Go) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Shows how to record 'moneyWithdrawn' and 'moneyDeposited' events, both incorporating generated event metadata via the meta-generator. ```go err := a.recordThat(&moneyWithdrawn{ Amount: amount, EventMetadata: a.metaGenerator.NewEventMeta(), }) // ... return a.recordThat(&moneyDeposited{ Amount: amount, EventMetadata: a.metaGenerator.NewEventMeta(), }) ``` -------------------------------- ### Update Account Constructor with TimeProvider Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Modifies the `Account` struct to include `timeProvider` and `metaGenerator` dependencies. The `Open` function signature is updated to accept a `timeProvider` instead of a direct `time.Time` value, facilitating testing. ```Go type Account struct { aggregate.Base // ... // technical dependencies timeProvider timeutils.TimeProvider metaGenerator *shared.EventMetaGenerator } func Open(id AccountID, timeProvider timeutils.TimeProvider, holderName string) (*Account, error) { // ... } ``` -------------------------------- ### Subscriber Goroutine - Go Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Sets up a subscriber goroutine that listens for messages on a channel and prints received events. This is part of the background process that consumes messages published from the outbox. ```Go go func() { fmt.Println("\nSubscriber started. Waiting for events...") for msg := range subCh { fmt.Printf( "-> Subscriber received: %s for aggregate %s\n", msg.EventName, msg.AggregateID, ) wg.Done() } }() ``` -------------------------------- ### Implement Account Snapshotter in Go Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Implements the `aggregate.Snapshotter` interface for the `Account` aggregate. The `Snapshotter` provides methods to convert an `Account` aggregate to a `Snapshot` (`ToSnapshot`) and to reconstruct an `Account` from a `Snapshot` (`FromSnapshot`). ```Go package account type Snapshotter struct{} func (s *Snapshotter) ToSnapshot(acc *Account) (*Snapshot, error) { return &Snapshot{ AccountID: acc.ID(), // Important: save the aggregate's id OpenedAt: acc.openedAt, Balance: acc.balance, HolderName: acc.holderName, AggregateVersion: acc.Version(), // Important: save the aggregate's version }, nil } func (s *Snapshotter) FromSnapshot(snap *Snapshot) (*Account, error) { // Recreate the aggregate from the snapshot's data acc := NewEmpty() acc.id = snap.ID() acc.openedAt = snap.OpenedAt acc.balance = snap.Balance acc.holderName = snap.HolderName // ⚠️ The repository will set the correct version on the aggregate's Base return acc, nil } ``` -------------------------------- ### Create Outbox Table - Go Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Defines the constructor for the AccountOutboxProcessor, which creates the 'outbox_account_events' table if it doesn't exist. This table stores events to be sent to external systems. ```Go func NewAccountOutboxProcessor(db *sql.DB) (*AccountOutboxProcessor, error) { _, err := db.Exec(""" CREATE TABLE IF NOT EXISTS outbox_account_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, aggregate_id TEXT NOT NULL, event_name TEXT NOT NULL, payload BLOB NOT NULL ); """) if err != nil { return nil, fmt.Errorf("new account outbox processor: could not create table: %w", err) } return &AccountOutboxProcessor{}, nil } ``` -------------------------------- ### Go: Custom LoggingSaver with FusedRepo Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Demonstrates creating a custom `LoggingSaver` that adds logging before saving an aggregate. It then shows how to use `aggregate.FusedRepo` to combine this custom saver with a base repository's loading capabilities. ```Go package main import ( "context" "log" "github.com/DeluxeOwl/chronicle/aggregate" "github.com/DeluxeOwl/chronicle/event" "github.com/DeluxeOwl/chronicle/version" ) // Assuming ID, Root, and event.Any are defined elsewhere type ID string type Root[TID ID, E event.Any] interface { ID() TID } // A custom Saver that adds logging before saving. type LoggingSaver[TID ID, E event.Any, R Root[TID, E]] struct { // The "real" saver saver aggregate.Saver[TID, E, R] } func (s *LoggingSaver[TID, E, R]) Save(ctx context.Context, root R) (version.Version, aggregate.CommittedEvents[E], error) { log.Printf("Attempting to save aggregate %s", root.ID()) return s.saver.Save(ctx, root) } func main() { // Placeholder for baseRepo initialization // baseRepo, _ := chronicle.NewEventSourcedRepository(...) // Create a new repository that uses the standard Get but our custom Save. // loggingRepo := &aggregate.FusedRepo[...]{ // AggregateLoader: baseRepo, // VersionedGetter: baseRepo, // Getter: baseRepo, // Saver: &LoggingSaver[...]{saver: baseRepo}, // } } ``` -------------------------------- ### Update Empty Account Constructor Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Adjusts the `NewEmpty` constructor to accept a `timeutils.TimeProvider` and initialize the `Account` struct with this dependency, ensuring that the necessary technical dependencies are available when creating new `Account` instances. ```Go // From this func NewEmpty() *Account { return new(Account) } // To this func NewEmptyMaker(timeProvider timeutils.TimeProvider) func() *Account { return func() *Account { return &Account{ timeProvider: timeProvider, ``` -------------------------------- ### Initialize Account Projection Table (Go) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md This function initializes the `projection_accounts` table in the SQLite database if it doesn't already exist. It defines `account_id` as the primary key and `holder_name` as a non-nullable text field. ```go func NewAccountsWithNameProcessor(db *sql.DB) (*AccountsWithNameProcessor, error) { _, err := db.Exec(` CREATE TABLE IF NOT EXISTS projection_accounts ( account_id TEXT PRIMARY KEY, holder_name TEXT NOT NULL ); `) if err != nil { return nil, fmt.Errorf("new accounts with name processor: %w", err) } return &AccountsWithNameProcessor{}, nil } ``` -------------------------------- ### Go GlobalReader Interface for Global Event Log Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Defines the `GlobalReader` interface, an extension of `GlobalLog`, for building system-wide projections. It includes the `ReadAllEvents` method, which requires a backing store capable of assigning a globally monotonic ID. ```Go type GlobalReader interface { ReadAllEvents(ctx context.Context, globalSelector version.Selector) GlobalRecords } ``` -------------------------------- ### Implement CryptoTransformer for Write Source: https://github.com/deluxeowl/chronicle/blob/main/README.md The CryptoTransformer's TransformForWrite method encrypts the holderName for accountOpened events and encodes it to base64 before writing. ```go type CryptoTransformer struct { key []byte } func NewCryptoTransformer(key []byte) *CryptoTransformer { return &CryptoTransformer{ key: key, } } func (t *CryptoTransformer) TransformForWrite(ctx context.Context, event AccountEvent) (AccountEvent, error) { if opened, isOpened := event.(*accountOpened); isOpened { fmt.Println("Received \"accountOpened\" event") encryptedName, err := encrypt([]byte(opened.HolderName), t.key) if err != nil { return nil, fmt.Errorf("failed to encrypt holder name: %w", err) } opened.HolderName = base64.StdEncoding.EncodeToString(encryptedName) fmt.Printf("Holder name after encryption and encoding: %s\n", opened.HolderName) } return event, nil } ``` -------------------------------- ### Create Pebble Event Log Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Initializes a persistent, file-based event log using Pebble, a key-value store from Cockroach Labs. This is useful for persistence without the overhead of a full database server. It is intended for single-process use, as concurrent writes from multiple processes can lead to corruption. ```go eventlog.NewPebble(db) ``` -------------------------------- ### Implement Encryption and Decryption Helpers Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Provides helper functions for encrypting and decrypting byte slices using a provided key. These are essential for the crypto transformer. ```go package account func encrypt(plaintext []byte, key []byte) ([]byte, error) { // ... } func decrypt(ciphertext []byte, key []byte) ([]byte, error) { // ... } ``` -------------------------------- ### Create Event Metadata Generator Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Provides a constructor `NewEventMetaGenerator` that takes a `TimeProvider` and returns an `EventMetaGenerator`. The generator creates `EventMetadata` with a unique ID (using uuidv7) and the current timestamp. ```Go func NewEventMetaGenerator(provider timeutils.TimeProvider) *EventMetaGenerator { return &EventMetaGenerator{ gen: func() EventMetadata { now := provider.Now() return EventMetadata{ // The uuidv7 contains the timestamp EventID: uuid.Must(uuid.NewV7AtTime(now)).String(), // Or just same a simple timestamp OccuredAt: now, } }, } } type EventMetaGenerator struct { gen func() EventMetadata } func (gen *EventMetaGenerator) NewEventMeta() EventMetadata { return gen.gen() ``` -------------------------------- ### Simulate GDPR Request with Key Deletion (Go) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Simulates a GDPR 'right to be forgotten' request by deleting the encryption key and creating a new repository. Attempting to access data with the deleted key results in an error, demonstrating data unreadability. ```go fmt.Println("!!!! Simulating GDPR request: Deleting the encryption key. !!!!") deletedKey := []byte("a-very-deleted-key-1234567891234") cryptoTransformer := account.NewCryptoTransformer(deletedKey) forgottenRepo, _ := chronicle.NewEventSourcedRepository( memoryEventLog, account.NewEmpty, []event.Transformer[account.AccountEvent]{cryptoTransformer}, ) _, err := forgottenRepo.Get(context.Background(), accID) if err != nil { fmt.Printf("Success! The data is unreadable. Error: %v\n", err) } ``` -------------------------------- ### Go: GlobalLog Interface for System-Wide Event Ordering Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Defines the GlobalLog interface, which extends the base Log interface with GlobalReader capabilities. This allows reading all events across aggregates in their global commit order, essential for system-wide projections and audit trails. ```Go // event/event_log.go // GlobalLog extends a standard Log with the ability to read all events across // all aggregates, in the global order they were committed. type GlobalLog interface { Log GlobalReader } // GlobalReader defines the contract for reading the global stream. type GlobalReader interface { ReadAllEvents(ctx context.Context, globalSelector version.Selector) GlobalRecords } ``` -------------------------------- ### Benchmark Reading All Events Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Assesses the performance of reading all events from the global event log with different total event counts (1000 and 10000) across various storage backends. It reports ns/op, B/op, and allocs/op. ```text BenchmarkReadAllEvents/Total1000_Events/memory-12 7255 146797 ns/op 302663 B/op 1026 allocs/op BenchmarkReadAllEvents/Total1000_Events/pebble-12 1285 906482 ns/op 450500 B/op 9022 allocs/op BenchmarkReadAllEvents/Total1000_Events/sqlite-12 565 2125557 ns/op 488207 B/op 16710 allocs/op BenchmarkReadAllEvents/Total1000_Events/postgres-12 1366 921196 ns/op 328453 B/op 14711 allocs/op BenchmarkReadAllEvents/Total10000_Events/memory-12 753 1399182 ns/op 3715825 B/op 10036 allocs/op BenchmarkReadAllEvents/Total10000_Events/pebble-12 124 9021583 ns/op 4634662 B/op 90037 allocs/op BenchmarkReadAllEvents/Total10000_Events/sqlite-12 57 21337020 ns/op 5088790 B/op 186170 allocs/op BenchmarkReadAllEvents/Total10000_Events/postgres-12 198 6017864 ns/op 3489125 B/op 166171 allocs/op ``` -------------------------------- ### Query Projection Table (SQL) Source: https://github.com/deluxeowl/chronicle/blob/main/README.md Executes a SQL query against the `projection_accounts` table to retrieve the `account_id` and `holder_name` for all accounts. This demonstrates how to access the data stored in the projection. ```sql SELECT account_id, holder_name FROM projection_accounts ```