// memberreconciler reconciles the Firestore membership mirror // (humans/{humanId}.networks) against the authoritative Postgres // network_members table. Safe to run on a cron — only humans whose mirrored // set differs from Postgres are written, so a steady-state run is nearly free. package main import ( "context" "os" "slices" "time" "github.com/flowy-live/llink/internal/utils/flog" "cloud.google.com/go/firestore" "google.golang.org/api/iterator" "github.com/flowy-live/llink/internal/db" "github.com/flowy-live/llink/internal/human" "github.com/flowy-live/llink/internal/network" "github.com/flowy-live/llink/internal/utils" ) func main() { ctx := context.Background() db.Init() defer db.Cleanup() gcpProject := utils.MustGetEnv("GCP_PROJECT") fs, err := firestore.NewClient(ctx, gcpProject) if err != nil { flog.Error("failed to create Firestore client", "error", err) os.Exit(1) } defer fs.Close() humanSvc := human.NewService(db.Pool()) networkSvc := network.NewReader(db.Pool()) started := time.Now() written, scanned, err := reconcile(ctx, fs, humanSvc, networkSvc) if err != nil { flog.Error("reconciliation failed", "error", err, "elapsed", time.Since(started)) os.Exit(1) } flog.Info("reconciliation complete", "humans_scanned", scanned, "humans_written", written, "elapsed", time.Since(started), ) } func reconcile( ctx context.Context, fs *firestore.Client, humanSvc human.Service, networkReader network.Reader, ) (written, scanned int, err error) { humans, err := humanSvc.ListAll(ctx) if err != nil { return 0, 0, err } memberships, err := networkReader.ListAllMemberships(ctx) if err != nil { return 0, 0, err } current, err := snapshotMirror(ctx, fs) if err != nil { return 0, 0, err } bulk := fs.BulkWriter(ctx) for _, h := range humans { scanned++ desired := memberships[h.ID] if desired == nil { desired = []string{} } if sameSet(current[h.ID], desired) { continue } if _, err := bulk.Set(fs.Collection("humans").Doc(h.ID), map[string]any{ "networks": desired, "updated_at": firestore.ServerTimestamp, }, firestore.MergeAll); err != nil { return written, scanned, err } written++ } bulk.End() return written, scanned, nil } // One iterator, N billed reads — returns humanId → mirrored networks. func snapshotMirror(ctx context.Context, fs *firestore.Client) (map[string][]string, error) { out := map[string][]string{} iter := fs.Collection("humans").Documents(ctx) defer iter.Stop() for { doc, err := iter.Next() if err == iterator.Done { break } if err != nil { return nil, err } var data struct { Networks []string `firestore:"networks"` } if err := doc.DataTo(&data); err != nil { flog.Warn("skipping malformed mirror doc", "id", doc.Ref.ID, "error", err) continue } out[doc.Ref.ID] = data.Networks } return out, nil } // Set equality (order- and duplicate-insensitive); Firestore array ops don't // preserve order. func sameSet(a, b []string) bool { if len(a) == 0 && len(b) == 0 { return true } ac := slices.Clone(a) bc := slices.Clone(b) slices.Sort(ac) slices.Sort(bc) ac = slices.Compact(ac) bc = slices.Compact(bc) return slices.Equal(ac, bc) }