From ad5d696ce6eb72c52ff99137c3476ea0447d9f5a Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 20 Apr 2026 09:50:48 -0600 Subject: [PATCH] refactor: dataset.Add returns View[T] instead of Dataset[T] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Group-managed datasets must never have Init/Sync/Run called on them. Rather than patching with NopSyncer, introduce View[T] — a thin wrapper that exposes only Load(). The compiler now prevents misuse: callers can read values but cannot drive fetch/reload cycles directly. Dataset[T] no longer needs a syncer when owned by a Group; View.reload() delegates to the inner Dataset.reload() for Group.reloadAll(). --- net/dataset/dataset.go | 23 +++++++++++++++++------ net/ipcohort/cmd/check-ip/blacklist.go | 6 +++--- net/ipcohort/cmd/check-ip/main.go | 6 +++--- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/net/dataset/dataset.go b/net/dataset/dataset.go index c14d10a..99dc9e3 100644 --- a/net/dataset/dataset.go +++ b/net/dataset/dataset.go @@ -124,13 +124,24 @@ func NewGroup(syncer httpcache.Syncer) *Group { return &Group{syncer: syncer} } -// Add registers a new dataset in g and returns it. Fetch and reload are driven -// by the Group — call Init/Run/Sync on g, not on the returned Dataset. +// View is the read-only handle returned by Add. It exposes only Load — +// fetch and reload are driven by the owning Group. +type View[T any] struct { + d *Dataset[T] +} + +// Load returns the current value. Returns nil before the Group is initialised. +func (v *View[T]) Load() *T { return v.d.ptr.Load() } + +func (v *View[T]) reload() error { return v.d.reload() } + +// Add registers a new dataset in g and returns a View. Call Load to read the +// current value. Drive updates by calling Init/Sync/Run on the Group. // load is a closure capturing whatever paths or config it needs. -func Add[T any](g *Group, load func() (*T, error)) *Dataset[T] { - d := &Dataset[T]{syncer: httpcache.NopSyncer{}, load: load} - g.members = append(g.members, d) - return d +func Add[T any](g *Group, load func() (*T, error)) *View[T] { + v := &View[T]{d: &Dataset[T]{load: load}} + g.members = append(g.members, v) + return v } // Init fetches once then reloads all registered datasets. diff --git a/net/ipcohort/cmd/check-ip/blacklist.go b/net/ipcohort/cmd/check-ip/blacklist.go index 3c47be4..6ab0ed6 100644 --- a/net/ipcohort/cmd/check-ip/blacklist.go +++ b/net/ipcohort/cmd/check-ip/blacklist.go @@ -82,9 +82,9 @@ func (s *Sources) Fetch() (bool, error) { // outbound may be nil if no paths were configured. func (s *Sources) Datasets() ( g *dataset.Group, - whitelist *dataset.Dataset[ipcohort.Cohort], - inbound *dataset.Dataset[ipcohort.Cohort], - outbound *dataset.Dataset[ipcohort.Cohort], + whitelist *dataset.View[ipcohort.Cohort], + inbound *dataset.View[ipcohort.Cohort], + outbound *dataset.View[ipcohort.Cohort], ) { g = dataset.NewGroup(s) if len(s.whitelistPaths) > 0 { diff --git a/net/ipcohort/cmd/check-ip/main.go b/net/ipcohort/cmd/check-ip/main.go index eb24965..afb23ea 100644 --- a/net/ipcohort/cmd/check-ip/main.go +++ b/net/ipcohort/cmd/check-ip/main.go @@ -185,7 +185,7 @@ func newGeoIPDataset(d *geoip.Downloader, edition, path string) *dataset.Dataset } func containsInbound(ip string, - whitelist, inbound *dataset.Dataset[ipcohort.Cohort], + whitelist, inbound *dataset.View[ipcohort.Cohort], ) bool { if whitelist != nil { if wl := whitelist.Load(); wl != nil && wl.Contains(ip) { @@ -200,7 +200,7 @@ func containsInbound(ip string, } func containsOutbound(ip string, - whitelist, outbound *dataset.Dataset[ipcohort.Cohort], + whitelist, outbound *dataset.View[ipcohort.Cohort], ) bool { if whitelist != nil { if wl := whitelist.Load(); wl != nil && wl.Contains(ip) { @@ -256,7 +256,7 @@ func printGeoInfo(ipStr string, cityDS, asnDS *dataset.Dataset[geoip2.Reader]) { } } -func cohortSize(ds *dataset.Dataset[ipcohort.Cohort]) int { +func cohortSize(ds *dataset.View[ipcohort.Cohort]) int { if ds == nil { return 0 }