Migation to monorepo
This commit is contained in:
4
chron-core/cmd/main.go
Normal file
4
chron-core/cmd/main.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
}
|
||||
1
chron-core/errors.go
Normal file
1
chron-core/errors.go
Normal file
@@ -0,0 +1 @@
|
||||
package chroncore
|
||||
8
chron-core/go.mod
Normal file
8
chron-core/go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
module git.michelsen.id/phill/chron/chron-core
|
||||
|
||||
go 1.25.7
|
||||
|
||||
require (
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
)
|
||||
4
chron-core/go.sum
Normal file
4
chron-core/go.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||
24
chron-core/interfaces.go
Normal file
24
chron-core/interfaces.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package chroncore
|
||||
|
||||
import "context"
|
||||
|
||||
type EntryStore interface {
|
||||
Store(ctx context.Context, entry Entry) error
|
||||
Load(ctx context.Context, id EntryID) (Entry, error)
|
||||
Exists(ctx context.Context, id EntryID) (bool, error)
|
||||
Delete(ctx context.Context, id EntryID) error
|
||||
|
||||
StoreBatch(ctx context.Context, entries []Entry) error
|
||||
LoadBatch(ctx context.Context, ids []EntryID) ([]Entry, error)
|
||||
}
|
||||
|
||||
type ReferenceStore interface {
|
||||
Set(ctx context.Context, name string, entryID EntryID) error
|
||||
Get(ctx context.Context, name string) (EntryID, bool, error)
|
||||
Delete(ctx context.Context, name string) error
|
||||
|
||||
List(ctx context.Context, prefix string) (map[string]EntryID, error)
|
||||
|
||||
SetBatch(ctx context.Context, refs map[string]EntryID) error
|
||||
GetBatch(ctx context.Context, names []string) (map[string]EntryID, error)
|
||||
}
|
||||
302
chron-core/ledger.go
Normal file
302
chron-core/ledger.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package chroncore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrHeadNotSet = errors.New("HEAD not set")
|
||||
ErrEntryIDMismatch = errors.New("entry id mismatch")
|
||||
ErrBatchLoadSize = errors.New("LoadBatch returned unexpected number of entries")
|
||||
)
|
||||
|
||||
type Ledger struct {
|
||||
entryStore EntryStore
|
||||
referenceStore ReferenceStore
|
||||
}
|
||||
|
||||
func NewLedger(entryStore EntryStore, referenceStore ReferenceStore) (*Ledger, error) {
|
||||
return &Ledger{
|
||||
entryStore: entryStore,
|
||||
referenceStore: referenceStore,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *Ledger) Add(ctx context.Context, parents []EntryID, payload []byte) (EntryID, error) {
|
||||
ps := normalizeParents(parents)
|
||||
|
||||
ts := time.Now()
|
||||
id := ComputeEntryID(ps, ts, payload)
|
||||
|
||||
e := Entry{
|
||||
EntryID: id,
|
||||
Parents: ps,
|
||||
Timestamp: ts,
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
if err := l.VerifyEntry(e); err != nil {
|
||||
return EntryID{}, err
|
||||
}
|
||||
|
||||
if err := l.entryStore.Store(ctx, e); err != nil {
|
||||
return EntryID{}, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (l *Ledger) Append(ctx context.Context, payload []byte) (EntryID, error) {
|
||||
head, ok, err := l.referenceStore.Get(ctx, "HEAD")
|
||||
if err != nil {
|
||||
return EntryID{}, err
|
||||
}
|
||||
|
||||
var parents []EntryID
|
||||
if ok {
|
||||
parents = []EntryID{head}
|
||||
} else {
|
||||
parents = nil
|
||||
}
|
||||
|
||||
id, err := l.Add(ctx, parents, payload)
|
||||
if err != nil {
|
||||
return EntryID{}, err
|
||||
}
|
||||
|
||||
if err := l.referenceStore.Set(ctx, "HEAD", id); err != nil {
|
||||
return EntryID{}, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (l *Ledger) AppendTo(ctx context.Context, parent EntryID, payload []byte) (EntryID, error) {
|
||||
return l.Add(ctx, []EntryID{parent}, payload)
|
||||
}
|
||||
|
||||
func (l *Ledger) Get(ctx context.Context, id EntryID) (Entry, error) {
|
||||
return l.entryStore.Load(ctx, id)
|
||||
}
|
||||
|
||||
func (l *Ledger) Exists(ctx context.Context, id EntryID) (bool, error) {
|
||||
return l.entryStore.Exists(ctx, id)
|
||||
}
|
||||
|
||||
func (l *Ledger) Verify(ctx context.Context, id EntryID) error {
|
||||
e, err := l.entryStore.Load(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return l.VerifyEntry(e)
|
||||
}
|
||||
|
||||
func (l *Ledger) VerifyEntry(e Entry) error {
|
||||
want := ComputeEntryID(e.Parents, e.Timestamp, e.Payload)
|
||||
if want != e.EntryID {
|
||||
return ErrEntryIDMismatch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Ledger) GetRef(ctx context.Context, name string) (EntryID, bool, error) {
|
||||
return l.referenceStore.Get(ctx, name)
|
||||
}
|
||||
|
||||
func (l *Ledger) SetRef(ctx context.Context, name string, id EntryID) error {
|
||||
return l.referenceStore.Set(ctx, name, id)
|
||||
}
|
||||
|
||||
func (l *Ledger) DeleteRef(ctx context.Context, name string) error {
|
||||
return l.referenceStore.Delete(ctx, name)
|
||||
}
|
||||
|
||||
func (l *Ledger) ListRefs(ctx context.Context, prefix string) (map[string]EntryID, error) {
|
||||
return l.referenceStore.List(ctx, prefix)
|
||||
}
|
||||
|
||||
func (l *Ledger) SetRefs(ctx context.Context, refs map[string]EntryID) error {
|
||||
return l.referenceStore.SetBatch(ctx, refs)
|
||||
}
|
||||
|
||||
func (l *Ledger) GetRefs(ctx context.Context, names []string) (map[string]EntryID, error) {
|
||||
return l.referenceStore.GetBatch(ctx, names)
|
||||
}
|
||||
|
||||
func (l *Ledger) GetHead(ctx context.Context) (EntryID, bool, error) {
|
||||
return l.referenceStore.Get(ctx, "HEAD")
|
||||
}
|
||||
|
||||
func (l *Ledger) SetHead(ctx context.Context, id EntryID) error {
|
||||
return l.referenceStore.Set(ctx, "HEAD", id)
|
||||
}
|
||||
|
||||
func (l *Ledger) GetHeads(ctx context.Context, prefix string) ([]EntryID, error) {
|
||||
m, err := l.referenceStore.List(ctx, prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
seen := make(map[EntryID]struct{}, len(m))
|
||||
for _, id := range m {
|
||||
seen[id] = struct{}{}
|
||||
}
|
||||
|
||||
out := make([]EntryID, 0, len(seen))
|
||||
for id := range seen {
|
||||
out = append(out, id)
|
||||
}
|
||||
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
return bytes.Compare(out[i][:], out[j][:]) < 0
|
||||
})
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (l *Ledger) WalkAncestors(ctx context.Context, start []EntryID, fn func(Entry) bool) error {
|
||||
frontier := dedupeIDs(start)
|
||||
visited := make(map[EntryID]struct{}, 1024)
|
||||
|
||||
for len(frontier) > 0 {
|
||||
batchIDs := make([]EntryID, 0, len(frontier))
|
||||
for _, id := range frontier {
|
||||
if _, ok := visited[id]; ok {
|
||||
continue
|
||||
}
|
||||
visited[id] = struct{}{}
|
||||
batchIDs = append(batchIDs, id)
|
||||
}
|
||||
if len(batchIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
entries, err := l.entryStore.LoadBatch(ctx, batchIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(entries) != len(batchIDs) {
|
||||
return ErrBatchLoadSize
|
||||
}
|
||||
|
||||
next := make([]EntryID, 0, len(batchIDs)*2)
|
||||
|
||||
for _, e := range entries {
|
||||
if err := l.VerifyEntry(e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !fn(e) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, p := range e.Parents {
|
||||
next = append(next, p)
|
||||
}
|
||||
}
|
||||
|
||||
frontier = dedupeIDs(next)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Ledger) IsAncestor(ctx context.Context, ancestor, descendant EntryID) (bool, error) {
|
||||
if ancestor == descendant {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
found := false
|
||||
err := l.WalkAncestors(ctx, []EntryID{descendant}, func(e Entry) bool {
|
||||
if slices.Contains(e.Parents, ancestor) {
|
||||
found = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return found, nil
|
||||
}
|
||||
|
||||
func (l *Ledger) CommonAncestors(ctx context.Context, a, b []EntryID, limit int) ([]EntryID, error) {
|
||||
aSet := make(map[EntryID]struct{}, 1024)
|
||||
|
||||
if err := l.WalkAncestors(ctx, a, func(e Entry) bool {
|
||||
aSet[e.EntryID] = struct{}{}
|
||||
return true
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var out []EntryID
|
||||
seen := make(map[EntryID]struct{}, 64)
|
||||
|
||||
err := l.WalkAncestors(ctx, b, func(e Entry) bool {
|
||||
if _, ok := aSet[e.EntryID]; ok {
|
||||
if _, dup := seen[e.EntryID]; !dup {
|
||||
seen[e.EntryID] = struct{}{}
|
||||
out = append(out, e.EntryID)
|
||||
if limit > 0 && len(out) >= limit {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func normalizeParents(parents []EntryID) []EntryID {
|
||||
if len(parents) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ps := make([]EntryID, len(parents))
|
||||
copy(ps, parents)
|
||||
|
||||
sort.Slice(ps, func(i, j int) bool {
|
||||
return bytes.Compare(ps[i][:], ps[j][:]) < 0
|
||||
})
|
||||
|
||||
out := ps[:0]
|
||||
var last EntryID
|
||||
for i, p := range ps {
|
||||
if i == 0 || p != last {
|
||||
out = append(out, p)
|
||||
last = p
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func dedupeIDs(ids []EntryID) []EntryID {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tmp := make([]EntryID, len(ids))
|
||||
copy(tmp, ids)
|
||||
sort.Slice(tmp, func(i, j int) bool {
|
||||
return bytes.Compare(tmp[i][:], tmp[j][:]) < 0
|
||||
})
|
||||
out := tmp[:0]
|
||||
var last EntryID
|
||||
for i, id := range tmp {
|
||||
if i == 0 || id != last {
|
||||
out = append(out, id)
|
||||
last = id
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
51
chron-core/types.go
Normal file
51
chron-core/types.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package chroncore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"lukechampine.com/blake3"
|
||||
)
|
||||
|
||||
type EntryID [32]byte
|
||||
|
||||
type Entry struct {
|
||||
EntryID EntryID
|
||||
|
||||
Parents []EntryID
|
||||
|
||||
Timestamp time.Time
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func ComputeEntryID(parents []EntryID, ts time.Time, payload []byte) EntryID {
|
||||
h := blake3.New(32, nil)
|
||||
|
||||
ps := make([]EntryID, len(parents))
|
||||
copy(ps, parents)
|
||||
sort.Slice(ps, func(i, j int) bool {
|
||||
return bytes.Compare(ps[i][:], ps[j][:]) < 0
|
||||
})
|
||||
|
||||
var u32 [4]byte
|
||||
binary.LittleEndian.PutUint32(u32[:], uint32(len(ps)))
|
||||
h.Write(u32[:])
|
||||
|
||||
for _, p := range ps {
|
||||
h.Write(p[:])
|
||||
}
|
||||
|
||||
var i64 [8]byte
|
||||
binary.LittleEndian.PutUint64(i64[:], uint64(ts.UTC().UnixNano()))
|
||||
h.Write(i64[:])
|
||||
|
||||
binary.LittleEndian.PutUint32(u32[:], uint32(len(payload)))
|
||||
h.Write(u32[:])
|
||||
h.Write(payload)
|
||||
|
||||
var id EntryID
|
||||
copy(id[:], h.Sum(nil))
|
||||
return id
|
||||
}
|
||||
Reference in New Issue
Block a user