aboutsummaryrefslogtreecommitdiff
path: root/object
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-06-07 19:21:17 +0000
committerGravatar Runxi Yu2026-06-07 19:21:26 +0000
commit2e22099f4bf1a78c4a481d98cb47a7e1975ea65f (patch)
treea6081c8c0b4ad676a8a6a26ba98af1632e8c7316 /object
parentinternal/mru: Add (diff)
signatureNo signature
object/store/mix: Add
Diffstat (limited to 'object')
-rw-r--r--object/store/mix/bytes.go52
-rw-r--r--object/store/mix/doc.go4
-rw-r--r--object/store/mix/header.go52
-rw-r--r--object/store/mix/mix.go51
-rw-r--r--object/store/mix/reader.go53
5 files changed, 212 insertions, 0 deletions
diff --git a/object/store/mix/bytes.go b/object/store/mix/bytes.go
new file mode 100644
index 00000000..2b4d3819
--- /dev/null
+++ b/object/store/mix/bytes.go
@@ -0,0 +1,52 @@
+package mix
+
+import (
+ "errors"
+ "fmt"
+
+ "lindenii.org/go/furgit/object/id"
+ "lindenii.org/go/furgit/object/store"
+ "lindenii.org/go/furgit/object/typ"
+)
+
+// ReadBytesFull reads a full serialized object
+// from the most-recently-used backend that has it.
+func (mix *Mix) ReadBytesFull(id id.ObjectID) ([]byte, error) {
+ for _, backend := range mix.order.Keys() {
+ full, err := backend.ReadBytesFull(id)
+ if err == nil {
+ mix.order.Touch(backend)
+
+ return full, nil
+ }
+
+ if errors.Is(err, store.ErrObjectNotFound) {
+ continue
+ }
+
+ return nil, fmt.Errorf("object/store/mix: read bytes full: %w", err)
+ }
+
+ return nil, store.ErrObjectNotFound
+}
+
+// ReadBytesContent reads an object's type and content bytes
+// from the most-recently-used backend that has it.
+func (mix *Mix) ReadBytesContent(id id.ObjectID) (typ.Type, []byte, error) {
+ for _, backend := range mix.order.Keys() {
+ ty, content, err := backend.ReadBytesContent(id)
+ if err == nil {
+ mix.order.Touch(backend)
+
+ return ty, content, nil
+ }
+
+ if errors.Is(err, store.ErrObjectNotFound) {
+ continue
+ }
+
+ return typ.TypeUnknown, nil, fmt.Errorf("object/store/mix: read bytes content: %w", err)
+ }
+
+ return typ.TypeUnknown, nil, store.ErrObjectNotFound
+}
diff --git a/object/store/mix/doc.go b/object/store/mix/doc.go
new file mode 100644
index 00000000..8de84d30
--- /dev/null
+++ b/object/store/mix/doc.go
@@ -0,0 +1,4 @@
+// Package mix provides a wrapper object storage backend
+// that queries multiple read-only backends
+// with a most-recently-used preference.
+package mix
diff --git a/object/store/mix/header.go b/object/store/mix/header.go
new file mode 100644
index 00000000..13dda9af
--- /dev/null
+++ b/object/store/mix/header.go
@@ -0,0 +1,52 @@
+package mix
+
+import (
+ "errors"
+ "fmt"
+
+ "lindenii.org/go/furgit/object/id"
+ "lindenii.org/go/furgit/object/store"
+ "lindenii.org/go/furgit/object/typ"
+)
+
+// ReadHeader reads object header data
+// from the most-recently-used backend that has it.
+func (mix *Mix) ReadHeader(id id.ObjectID) (typ.Type, uint64, error) {
+ for _, backend := range mix.order.Keys() {
+ ty, size, err := backend.ReadHeader(id)
+ if err == nil {
+ mix.order.Touch(backend)
+
+ return ty, size, nil
+ }
+
+ if errors.Is(err, store.ErrObjectNotFound) {
+ continue
+ }
+
+ return typ.TypeUnknown, 0, fmt.Errorf("object/store/mix: read header: %w", err)
+ }
+
+ return typ.TypeUnknown, 0, store.ErrObjectNotFound
+}
+
+// ReadSize reads object content length
+// from the most-recently-used backend that has it.
+func (mix *Mix) ReadSize(id id.ObjectID) (uint64, error) {
+ for _, backend := range mix.order.Keys() {
+ size, err := backend.ReadSize(id)
+ if err == nil {
+ mix.order.Touch(backend)
+
+ return size, nil
+ }
+
+ if errors.Is(err, store.ErrObjectNotFound) {
+ continue
+ }
+
+ return 0, fmt.Errorf("object/store/mix: read size: %w", err)
+ }
+
+ return 0, store.ErrObjectNotFound
+}
diff --git a/object/store/mix/mix.go b/object/store/mix/mix.go
new file mode 100644
index 00000000..2e8e926b
--- /dev/null
+++ b/object/store/mix/mix.go
@@ -0,0 +1,51 @@
+package mix
+
+import (
+ "errors"
+
+ "lindenii.org/go/furgit/internal/mru"
+ "lindenii.org/go/furgit/object/store"
+)
+
+// Mix queries multiple object databases
+// with a most-recently-used backend preference.
+//
+// Labels: Close-Caller.
+type Mix struct {
+ order *mru.Order[store.ObjectReader]
+}
+
+var _ store.ObjectReader = (*Mix)(nil)
+
+// New creates a Mix from backends.
+//
+// The provided backends must be non-nil and distinct.
+//
+// Labels: Deps-Borrowed, Life-Parent.
+func New(backends ...store.ObjectReader) *Mix {
+ present := make(map[store.ObjectReader]struct{}, len(backends))
+ for _, backend := range backends {
+ present[backend] = struct{}{}
+ }
+
+ order := mru.New[store.ObjectReader]()
+ order.Sync(present)
+
+ return &Mix{
+ order: order,
+ }
+}
+
+// Refresh forwards refresh calls to all backends.
+func (mix *Mix) Refresh() error {
+ var errs []error
+
+ for _, backend := range mix.order.Keys() {
+ err := backend.Refresh()
+ if err != nil {
+ errs = append(errs, err)
+ }
+ }
+
+ return errors.Join(errs...)
+}
diff --git a/object/store/mix/reader.go b/object/store/mix/reader.go
new file mode 100644
index 00000000..f9edc1a4
--- /dev/null
+++ b/object/store/mix/reader.go
@@ -0,0 +1,53 @@
+package mix
+
+import (
+ "errors"
+ "fmt"
+ "io"
+
+ "lindenii.org/go/furgit/object/id"
+ "lindenii.org/go/furgit/object/store"
+ "lindenii.org/go/furgit/object/typ"
+)
+
+// ReadReaderFull reads a full serialized object stream
+// from the most-recently-used backend that has it.
+func (mix *Mix) ReadReaderFull(id id.ObjectID) (io.ReadCloser, error) {
+ for _, backend := range mix.order.Keys() {
+ reader, err := backend.ReadReaderFull(id)
+ if err == nil {
+ mix.order.Touch(backend)
+
+ return reader, nil
+ }
+
+ if errors.Is(err, store.ErrObjectNotFound) {
+ continue
+ }
+
+ return nil, fmt.Errorf("object/store/mix: read reader full: %w", err)
+ }
+
+ return nil, store.ErrObjectNotFound
+}
+
+// ReadReaderContent reads an object's type, declared content length,
+// and content stream from the most-recently-used backend that has it.
+func (mix *Mix) ReadReaderContent(id id.ObjectID) (typ.Type, uint64, io.ReadCloser, error) {
+ for _, backend := range mix.order.Keys() {
+ ty, size, reader, err := backend.ReadReaderContent(id)
+ if err == nil {
+ mix.order.Touch(backend)
+
+ return ty, size, reader, nil
+ }
+
+ if errors.Is(err, store.ErrObjectNotFound) {
+ continue
+ }
+
+ return typ.TypeUnknown, 0, nil, fmt.Errorf("object/store/mix: read reader content: %w", err)
+ }
+
+ return typ.TypeUnknown, 0, nil, store.ErrObjectNotFound
+}