package main import ( "git.k6n.net/mats/platform/catalog" ) // catalogProjectionCache is the cart's per-pod, cold-start-ready catalog // projection cache, backed by platform/catalog.ProjectionStore. It consumes the // catalog.projection_published wire — a full snapshot (begin/chunk/end, framed by // epoch) plus incremental deltas — into its OWN local .bin and serves catalog // facts by SKU (price, tax_class, inventory_tracked, image, …) at add-to-cart / // checkout-reserve decisions. Stock is NOT cached — queried synchronously from // inventory per docs/inventory-shape.md. // // Clustering: the cart runs N replicas. Each pod has its OWN exclusive bus queue // (rabbit.ListenToPattern declares an exclusive server-named queue, so every pod // receives the full stream) and its OWN pod-local .bin. It's a replicated read // cache — no leader, no shared volume; N pods writing one file would corrupt it. // See docs/catalog-projection.md. // // The cart stores the full catalog.Projection (identity transform): its // add-to-cart overlay (projection_overlay.go) consumes nearly every field, so // there's nothing to subset. Deletes ride as tombstones in the store's overlay // (IsDeleted) so a removed SKU isn't re-fetched before the next snapshot folds // the delete into the base. Cold-grace: an SKU not yet in the base/overlay // resolves via the existing PRODUCT_BASE_URL HTTP fetch + overlay. type catalogProjectionCache struct { store *catalog.ProjectionStore[catalog.Projection] } // identityProjection is the cart's mandatory transform — it stores the full // Projection because the overlay uses almost all of it. func identityProjection(p catalog.Projection) catalog.Projection { return p } // newCatalogProjectionCache opens the per-pod store under dir. dir MUST be // pod-local (container fs / an emptyDir in k8s) — never a shared volume. func newCatalogProjectionCache(dir string) (*catalogProjectionCache, error) { s, err := catalog.NewProjectionStore(dir, "cart-projection.bin", identityProjection, nil) if err != nil { return nil, err } return &catalogProjectionCache{store: s}, nil } // HandleFrame feeds one catalog.projection_published event (snapshot frame or // delta) into the store; framing/epoch ordering live in platform/catalog. func (c *catalogProjectionCache) HandleFrame(meta map[string]string, payload []byte) error { return c.store.HandleFrame(meta, payload) } func (c *catalogProjectionCache) Get(sku string) (catalog.Projection, bool) { return c.store.Get(sku) } // IsDeleted reports whether the bus has explicitly deleted sku (overlay // tombstone) — call sites skip the HTTP fallback for it. func (c *catalogProjectionCache) IsDeleted(sku string) bool { return c.store.IsDeleted(sku) } // Len reports cached entries (base + overlay) — debug/log gauge only. func (c *catalogProjectionCache) Len() int { _, base, overlay := c.store.Stats() return base + overlay }