[{"data":1,"prerenderedAt":8034},["ShallowReactive",2],{"search-sections-flux":3,"nav-flux":1409,"content-tree-flux":1464,"footer-resources":1489,"content-/v1.0.2/cookbook/custom-watcher":4353,"surround-/v1.0.2/cookbook/custom-watcher":8031},[4,10,14,20,25,30,35,40,45,50,55,59,64,69,74,79,84,89,94,99,104,109,113,117,121,126,132,137,142,147,152,157,162,167,172,177,182,187,192,196,201,205,210,214,219,224,228,233,238,242,247,252,257,261,266,271,276,281,286,290,295,299,303,308,313,318,323,328,333,338,343,348,352,356,360,364,369,374,379,384,389,394,399,404,409,414,418,423,427,432,436,441,446,451,456,461,466,471,476,481,486,491,496,501,505,510,514,518,523,528,533,537,542,547,550,555,560,564,569,574,577,582,587,592,597,601,606,611,616,620,625,629,634,639,644,648,653,658,663,668,672,677,681,685,690,695,700,705,710,715,719,724,728,733,737,742,746,751,756,761,766,770,775,780,785,790,795,800,804,809,813,817,821,826,831,835,841,846,850,855,860,865,870,875,880,885,889,894,898,903,908,913,918,923,927,932,937,942,947,952,957,962,967,972,977,982,987,992,997,1002,1007,1012,1017,1022,1027,1032,1037,1042,1047,1052,1057,1062,1067,1072,1077,1082,1086,1089,1094,1099,1103,1108,1112,1117,1121,1125,1129,1134,1139,1144,1148,1153,1158,1163,1168,1173,1176,1181,1186,1191,1196,1201,1205,1210,1214,1219,1224,1229,1233,1238,1242,1247,1251,1255,1260,1265,1269,1272,1276,1281,1285,1288,1292,1297,1301,1304,1308,1313,1317,1320,1324,1329,1333,1338,1341,1346,1350,1355,1359,1362,1366,1371,1375,1378,1383,1387,1392,1397,1401,1405],{"id":5,"title":6,"titles":7,"content":8,"level":9},"/v1.0.2/overview","Overview",[],"Reactive configuration synchronization for Go applications",1,{"id":11,"title":6,"titles":12,"content":13,"level":9},"/v1.0.2/overview#overview",[],"Configuration management in Go usually means one of two extremes: reload-on-restart or complex orchestration. Flux offers a third path: reactive configuration that updates automatically when sources change. type Config struct {\n    Port int    `json:\"port\"`\n    Host string `json:\"host\"`\n}\n\nfunc (c Config) Validate() error {\n    if c.Port \u003C 1 || c.Port > 65535 {\n        return errors.New(\"port must be between 1 and 65535\")\n    }\n    if c.Host == \"\" {\n        return errors.New(\"host is required\")\n    }\n    return nil\n}\n\ncapacitor := flux.New[Config](\n    file.New(\"/etc/myapp/config.json\"),\n    func(ctx context.Context, prev, curr Config) error {\n        log.Printf(\"config changed: port %d -> %d\", prev.Port, curr.Port)\n        return app.Reconfigure(curr)\n    },\n)\n\nif err := capacitor.Start(ctx); err != nil {\n    log.Printf(\"initial config failed: %v\", err)\n} File changes trigger automatic reload. Invalid configs are rejected. Previous valid config is retained on failure.",{"id":15,"title":16,"titles":17,"content":18,"level":19},"/v1.0.2/overview#core-concepts","Core Concepts",[6],"Capacitor - The central type. Watches a source, deserializes data, validates it, and delivers changes to your callback. Watcher - Abstraction for data sources. Implementations exist for files, Redis, Consul, etcd, NATS, Kubernetes, ZooKeeper, and Firestore. State Machine - Tracks configuration health: Loading, Healthy, Degraded, or Empty. Codec - Handles deserialization. JSON by default, YAML available.",2,{"id":21,"title":22,"titles":23,"content":24,"level":19},"/v1.0.2/overview#the-pipeline","The Pipeline",[6],"Every configuration change flows through: Source → Deserialize → Validate → Callback Source emits raw bytes when data changesDeserialize converts bytes to your struct (JSON/YAML)Validate calls your struct's Validate() methodCallback receives both previous and current values If any step fails, the previous valid configuration is retained.",{"id":26,"title":27,"titles":28,"content":29,"level":19},"/v1.0.2/overview#state-machine","State Machine",[6],"┌─────────┐   success   ┌─────────┐\n│ Loading │────────────▶│ Healthy │◀──┐\n└─────────┘             └─────────┘   │\n     │                       │        │\n     │ failure          failure│   success\n     ▼                       ▼        │\n┌─────────┐             ┌─────────┐───┘\n│  Empty  │             │Degraded │\n└─────────┘             └─────────┘ StateHas ConfigErrorMeaningLoadingNoNoWaiting for first valueHealthyYesNoValid config activeDegradedYesYesUpdate failed, previous config retainedEmptyNoYesNo valid config ever obtained",{"id":31,"title":32,"titles":33,"content":34,"level":19},"/v1.0.2/overview#providers","Providers",[6],"The core package is zero-dependency. Providers live in pkg/ with isolated dependencies: ProviderBackendWatch Mechanismpkg/fileLocal filesystemfsnotifypkg/redisRedisKeyspace notificationspkg/consulConsul KVBlocking queriespkg/etcdetcdWatch APIpkg/natsNATS JetStreamKV Watchpkg/kubernetesConfigMap/SecretWatch APIpkg/zookeeperZooKeeperNode watchpkg/firestoreFirestoreRealtime listeners",{"id":36,"title":37,"titles":38,"content":39,"level":19},"/v1.0.2/overview#observability","Observability",[6],"Flux emits capitan signals for all state transitions and errors: capitan.Hook(flux.CapacitorStateChanged, func(ctx context.Context, e *capitan.Event) {\n    old, _ := flux.KeyOldState.From(e)\n    new, _ := flux.KeyNewState.From(e)\n    log.Printf(\"state: %s -> %s\", old, new)\n})",{"id":41,"title":42,"titles":43,"content":44,"level":19},"/v1.0.2/overview#when-to-use-flux","When to Use Flux",[6],"Good fit: Feature flags that change at runtimeDatabase connection pool sizingRate limits and quotasService discovery endpointsA/B test configurations Not ideal: Secrets that require rotation workflowsConfiguration that requires coordinated rolloutStatic config that never changes",{"id":46,"title":47,"titles":48,"content":49,"level":19},"/v1.0.2/overview#next-steps","Next Steps",[6],"Quickstart - Get running in 5 minutesCore Concepts - Understand the primitivesProviders Guide - Choose your data source html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":51,"title":52,"titles":53,"content":54,"level":9},"/v1.0.2/learn/quickstart","Quickstart",[],"Get started with flux in 5 minutes",{"id":56,"title":52,"titles":57,"content":58,"level":9},"/v1.0.2/learn/quickstart#quickstart",[],"",{"id":60,"title":61,"titles":62,"content":63,"level":19},"/v1.0.2/learn/quickstart#requirements","Requirements",[52],"Go 1.24 or later.",{"id":65,"title":66,"titles":67,"content":68,"level":19},"/v1.0.2/learn/quickstart#installation","Installation",[52],"go get github.com/zoobz-io/flux\ngo get github.com/zoobz-io/flux/file  # For file watching",{"id":70,"title":71,"titles":72,"content":73,"level":19},"/v1.0.2/learn/quickstart#basic-example","Basic Example",[52],"Watch a JSON config file and reload on changes: package main\n\nimport (\n    \"context\"\n    \"errors\"\n    \"log\"\n    \"os\"\n    \"os/signal\"\n\n    \"github.com/zoobz-io/flux\"\n    \"github.com/zoobz-io/flux/file\"\n)\n\ntype Config struct {\n    Port     int    `json:\"port\"`\n    Host     string `json:\"host\"`\n    LogLevel string `json:\"log_level\"`\n}\n\nfunc (c Config) Validate() error {\n    if c.Port \u003C 1 || c.Port > 65535 {\n        return errors.New(\"port must be between 1 and 65535\")\n    }\n    if c.Host == \"\" {\n        return errors.New(\"host is required\")\n    }\n    switch c.LogLevel {\n    case \"debug\", \"info\", \"warn\", \"error\":\n        // valid\n    default:\n        return errors.New(\"log_level must be one of: debug, info, warn, error\")\n    }\n    return nil\n}\n\nfunc main() {\n    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)\n    defer cancel()\n\n    capacitor := flux.New[Config](\n        file.New(\"/etc/myapp/config.json\"),\n        func(ctx context.Context, prev, curr Config) error {\n            log.Printf(\"config updated: port=%d, host=%s\", curr.Port, curr.Host)\n            return nil\n        },\n    )\n\n    if err := capacitor.Start(ctx); err != nil {\n        log.Printf(\"initial config failed: %v\", err)\n    }\n\n    log.Printf(\"state: %s\", capacitor.State())\n\n    \u003C-ctx.Done()\n}",{"id":75,"title":76,"titles":77,"content":78,"level":19},"/v1.0.2/learn/quickstart#config-file","Config File",[52],"{\n    \"port\": 8080,\n    \"host\": \"localhost\",\n    \"log_level\": \"info\"\n}",{"id":80,"title":81,"titles":82,"content":83,"level":19},"/v1.0.2/learn/quickstart#run-it","Run It",[52],"go run main.go\n# Output: config updated: port=8080, host=localhost\n# Output: state: healthy\n\n# Edit config.json in another terminal\n# Output: config updated: port=9090, host=localhost",{"id":85,"title":86,"titles":87,"content":88,"level":19},"/v1.0.2/learn/quickstart#validation","Validation",[52],"Configuration types must implement the Validator interface: type Validator interface {\n    Validate() error\n} Your Validate() method has full control over validation logic: type Config struct {\n    Port     int    `json:\"port\"`\n    Host     string `json:\"host\"`\n    LogLevel string `json:\"log_level\"`\n}\n\nfunc (c Config) Validate() error {\n    if c.Port \u003C 1 || c.Port > 65535 {\n        return fmt.Errorf(\"port must be between 1 and 65535, got %d\", c.Port)\n    }\n    if c.Host == \"\" {\n        return errors.New(\"host is required\")\n    }\n    return nil\n} For complex validation, you can use libraries like go-playground/validator inside your Validate() method. Invalid configs are rejected. The previous valid config is retained.",{"id":90,"title":91,"titles":92,"content":93,"level":19},"/v1.0.2/learn/quickstart#accessing-current-config","Accessing Current Config",[52],"cfg, ok := capacitor.Current()\nif !ok {\n    // No valid config yet (Loading or Empty state)\n    return\n}\nfmt.Printf(\"using port %d\\n\", cfg.Port)",{"id":95,"title":96,"titles":97,"content":98,"level":19},"/v1.0.2/learn/quickstart#error-handling","Error Handling",[52],"// Check last error\nif err := capacitor.LastError(); err != nil {\n    log.Printf(\"config error: %v\", err)\n}\n\n// Check state\nswitch capacitor.State() {\ncase flux.StateHealthy:\n    // All good\ncase flux.StateDegraded:\n    // Update failed, using previous config\ncase flux.StateEmpty:\n    // No valid config ever loaded\n}",{"id":100,"title":101,"titles":102,"content":103,"level":19},"/v1.0.2/learn/quickstart#yaml-support","YAML Support",[52],"capacitor := flux.New[Config](\n    file.New(\"/etc/myapp/config.yaml\"),\n    callback,\n).Codec(flux.YAMLCodec{})",{"id":105,"title":106,"titles":107,"content":108,"level":19},"/v1.0.2/learn/quickstart#debouncing","Debouncing",[52],"Rapid changes are coalesced: capacitor := flux.New[Config](\n    watcher,\n    callback,\n).Debounce(200*time.Millisecond) // Default: 100ms",{"id":110,"title":47,"titles":111,"content":112,"level":19},"/v1.0.2/learn/quickstart#next-steps",[52],"Core Concepts - Understand Capacitor, Watcher, StateProviders Guide - Redis, Consul, etcd, and moreTesting Guide - Sync mode for deterministic tests html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}",{"id":114,"title":16,"titles":115,"content":116,"level":9},"/v1.0.2/learn/concepts",[],"Capacitor, Watcher, State, and Codec - the building blocks of flux",{"id":118,"title":16,"titles":119,"content":120,"level":9},"/v1.0.2/learn/concepts#core-concepts",[],"Flux has five primitives: Capacitor, Watcher, Validator, State, and Codec.",{"id":122,"title":123,"titles":124,"content":125,"level":19},"/v1.0.2/learn/concepts#capacitor","Capacitor",[16],"The central type. A Capacitor watches a source, processes changes, and delivers valid configurations to your callback. capacitor := flux.New[Config](\n    watcher,                                          // Data source\n    func(ctx context.Context, prev, curr Config) error {  // Callback\n        return app.Reconfigure(curr)\n    },\n) The generic parameter [Config] must implement the Validator interface. Flux deserializes to this type and calls Validate() before invoking your callback.",{"id":127,"title":128,"titles":129,"content":130,"level":131},"/v1.0.2/learn/concepts#callback-signature","Callback Signature",[16,123],"func(ctx context.Context, prev, curr T) error ctx - Context for cancellation and deadlinesprev - Previous valid config (zero value on first load)curr - New valid configReturn nil to accept, error to reject The callback only runs after successful deserialization and validation. If it returns an error, the Capacitor enters Degraded state and retains the previous config.",3,{"id":133,"title":134,"titles":135,"content":136,"level":131},"/v1.0.2/learn/concepts#lifecycle","Lifecycle",[16,123],"// Create\ncapacitor := flux.New[Config](watcher, callback)\n\n// Start watching (blocks until first value processed)\nerr := capacitor.Start(ctx)\n\n// Access current config\ncfg, ok := capacitor.Current()\n\n// Check health\nstate := capacitor.State()\nlastErr := capacitor.LastError() Start() blocks until the first configuration is processed. It returns any error from initial load but continues watching in the background.",{"id":138,"title":139,"titles":140,"content":141,"level":19},"/v1.0.2/learn/concepts#watcher","Watcher",[16],"A Watcher observes a data source and emits raw bytes when changes occur. type Watcher interface {\n    Watch(ctx context.Context) (\u003C-chan []byte, error)\n} Contract: Emit current value immediately upon Watch() being calledEmit subsequent values when the source changesClose the channel when context is cancelled",{"id":143,"title":144,"titles":145,"content":146,"level":131},"/v1.0.2/learn/concepts#built-in-watchers","Built-in Watchers",[16,139],"ChannelWatcher - Wraps an existing channel (useful for testing): ch := make(chan []byte, 1)\nch \u003C- []byte(`{\"port\": 8080}`)\n\nwatcher := flux.NewChannelWatcher(ch)         // Async (uses internal goroutine)\nwatcher := flux.NewSyncChannelWatcher(ch)     // Sync (for deterministic tests)",{"id":148,"title":149,"titles":150,"content":151,"level":131},"/v1.0.2/learn/concepts#provider-watchers","Provider Watchers",[16,139],"Each provider in pkg/ implements Watcher for a specific backend: import \"github.com/zoobz-io/flux/file\"\nimport \"github.com/zoobz-io/flux/redis\"\nimport \"github.com/zoobz-io/flux/consul\"\n\nfile.New(\"/etc/myapp/config.json\")\nredis.New(client, \"myapp:config\")\nconsul.New(client, \"myapp/config\") See Providers Guide for all options.",{"id":153,"title":154,"titles":155,"content":156,"level":19},"/v1.0.2/learn/concepts#validator","Validator",[16],"Configuration types must implement the Validator interface: type Validator interface {\n    Validate() error\n} This gives you full control over validation logic: type Config struct {\n    Port int    `json:\"port\"`\n    Host string `json:\"host\"`\n}\n\nfunc (c Config) Validate() error {\n    if c.Port \u003C 1 || c.Port > 65535 {\n        return fmt.Errorf(\"port must be between 1 and 65535\")\n    }\n    if c.Host == \"\" {\n        return errors.New(\"host is required\")\n    }\n    return nil\n} For struct tag validation, you can integrate libraries like go-playground/validator within your Validate() method: import \"github.com/go-playground/validator/v10\"\n\nvar validate = validator.New()\n\nfunc (c Config) Validate() error {\n    return validate.Struct(c)\n}",{"id":158,"title":159,"titles":160,"content":161,"level":19},"/v1.0.2/learn/concepts#state","State",[16],"Capacitor maintains one of four states: const (\n    StateLoading  State = iota  // Waiting for first value\n    StateHealthy                // Valid config active\n    StateDegraded               // Update failed, previous retained\n    StateEmpty                  // No valid config ever obtained\n)",{"id":163,"title":164,"titles":165,"content":166,"level":131},"/v1.0.2/learn/concepts#transitions","Transitions",[16,159],"┌─────────┐   success   ┌─────────┐\n│ Loading │────────────▶│ Healthy │◀──┐\n└─────────┘             └─────────┘   │\n     │                       │        │\n     │ failure          failure│   success\n     ▼                       ▼        │\n┌─────────┐             ┌─────────┐───┘\n│  Empty  │             │Degraded │\n└─────────┘             └─────────┘ Loading → Healthy: First config loaded successfullyLoading → Empty: First config failed (parse, validation, or callback error)Healthy → Degraded: Update failed, previous config retainedDegraded → Healthy: Valid config arrived, recovered",{"id":168,"title":169,"titles":170,"content":171,"level":131},"/v1.0.2/learn/concepts#checking-state","Checking State",[16,159],"switch capacitor.State() {\ncase flux.StateHealthy:\n    cfg, _ := capacitor.Current()\n    // Use cfg\ncase flux.StateDegraded:\n    cfg, _ := capacitor.Current()  // Previous valid config\n    log.Printf(\"degraded: %v\", capacitor.LastError())\ncase flux.StateEmpty:\n    log.Fatalf(\"no config: %v\", capacitor.LastError())\n}",{"id":173,"title":174,"titles":175,"content":176,"level":19},"/v1.0.2/learn/concepts#codec","Codec",[16],"A Codec handles deserialization from raw bytes to your struct. type Codec interface {\n    Unmarshal(data []byte, v any) error\n}",{"id":178,"title":179,"titles":180,"content":181,"level":131},"/v1.0.2/learn/concepts#built-in-codecs","Built-in Codecs",[16,174],"JSONCodec (default): capacitor := flux.New[Config](watcher, callback)\n// Uses JSON by default YAMLCodec: capacitor := flux.New[Config](\n    watcher,\n    callback,\n).Codec(flux.YAMLCodec{})",{"id":183,"title":184,"titles":185,"content":186,"level":131},"/v1.0.2/learn/concepts#custom-codec","Custom Codec",[16,174],"type TOMLCodec struct{}\n\nfunc (TOMLCodec) Unmarshal(data []byte, v any) error {\n    return toml.Unmarshal(data, v)\n}\n\ncapacitor := flux.New[Config](\n    watcher,\n    callback,\n).Codec(TOMLCodec{})",{"id":188,"title":189,"titles":190,"content":191,"level":19},"/v1.0.2/learn/concepts#options","Options",[16],"Capacitor accepts pipeline options as constructor arguments and instance configuration via chainable methods: capacitor := flux.New[Config](\n    watcher,\n    callback,\n    // Pipeline options (retry, backoff, circuit breaker, etc.)\n    flux.WithRetry[Config](3),\n    flux.WithCircuitBreaker[Config](5, 30*time.Second),\n).Debounce(200*time.Millisecond).  // Instance configuration\n  Codec(flux.YAMLCodec{}).          // Use YAML instead of JSON\n  SyncMode().                       // Disable async (for testing)\n  Clock(fakeClock)                  // Custom clock (for testing)",{"id":193,"title":47,"titles":194,"content":195,"level":19},"/v1.0.2/learn/concepts#next-steps",[16],"Architecture - Internals and event flowTesting Guide - Sync mode and fake clocksState Management - Recovery patterns html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sfm-E, html code.shiki .sfm-E{--shiki-default:var(--shiki-variable)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}",{"id":197,"title":198,"titles":199,"content":200,"level":9},"/v1.0.2/learn/architecture","Architecture",[],"Internal design, pipeline flow, and debouncing",{"id":202,"title":198,"titles":203,"content":204,"level":9},"/v1.0.2/learn/architecture#architecture",[],"Understanding flux internals helps you reason about behavior, timing, and failure modes.",{"id":206,"title":207,"titles":208,"content":209,"level":19},"/v1.0.2/learn/architecture#pipeline-overview","Pipeline Overview",[198],"┌──────────┐    ┌───────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐\n│  Watcher │───▶│  Debounce │───▶│  Codec   │───▶│ Validate │───▶│ Callback │\n│          │    │           │    │          │    │          │    │          │\n│ []byte   │    │ coalesce  │    │ unmarshal│    │ struct   │    │ prev,curr│\n└──────────┘    └───────────┘    └──────────┘    └──────────┘    └──────────┘\n                                       │              │               │\n                                       ▼              ▼               ▼\n                                   Transform     Validation       Apply\n                                   Failed        Failed           Failed\n                                   (signal)      (signal)         (signal)",{"id":211,"title":212,"titles":213,"content":58,"level":19},"/v1.0.2/learn/architecture#event-flow","Event Flow",[198],{"id":215,"title":216,"titles":217,"content":218,"level":131},"/v1.0.2/learn/architecture#start-sequence","Start Sequence",[198,212],"err := capacitor.Start(ctx) Start() calls watcher.Watch(ctx)Watcher emits current value immediatelyFirst value processed synchronously (no debounce)State transitions: Loading → Healthy (or Empty on failure)Start() returns (error if initial load failed)Background goroutine begins watching for changes",{"id":220,"title":221,"titles":222,"content":223,"level":131},"/v1.0.2/learn/architecture#change-sequence","Change Sequence",[198,212],"Watcher emits new valueDebounce timer starts/resetsAfter debounce duration, value is processedCodec unmarshals bytes to structValidator checks struct tagsCallback receives (prev, curr)State updated, signals emitted",{"id":225,"title":106,"titles":226,"content":227,"level":19},"/v1.0.2/learn/architecture#debouncing",[198],"Rapid changes are coalesced to prevent thrashing: Time:     0ms    10ms   20ms   30ms   100ms  200ms\nEvents:   E1     E2     E3     E4\nDebounce: [--100ms--]   [--100ms--]   [--100ms--]→ Process E4 Only the last value is processed after the debounce window. // Default: 100ms\nflux.New[Config](watcher, callback).Debounce(200*time.Millisecond)",{"id":229,"title":230,"titles":231,"content":232,"level":131},"/v1.0.2/learn/architecture#first-value-exception","First Value Exception",[198,106],"The first value from Start() is processed immediately without debouncing. This ensures initial configuration loads without delay.",{"id":234,"title":235,"titles":236,"content":237,"level":19},"/v1.0.2/learn/architecture#goroutine-model","Goroutine Model",[198],"Main Goroutine                    Watch Goroutine\n──────────────                    ───────────────\ncapacitor.Start(ctx)\n    │\n    ├─▶ watcher.Watch(ctx)\n    │       │\n    │       └─▶ returns \u003C-chan []byte\n    │\n    ├─▶ receive first value\n    ├─▶ process synchronously\n    ├─▶ spawn watch goroutine ─────▶ for { select { ... } }\n    │                                     │\n    └─▶ return                            ├─▶ receive value\n                                          ├─▶ debounce\n                                          └─▶ process A single background goroutine handles all subsequent changes. The debounce timer runs within this goroutine.",{"id":239,"title":27,"titles":240,"content":241,"level":19},"/v1.0.2/learn/architecture#state-machine",[198],"State is stored atomically and accessed lock-free: type Capacitor[T any] struct {\n    state     atomic.Int32\n    current   atomic.Pointer[T]\n    lastError atomic.Pointer[error]\n    // ...\n}",{"id":243,"title":244,"titles":245,"content":246,"level":131},"/v1.0.2/learn/architecture#thread-safety","Thread Safety",[198,27],"State() - Always safe to callCurrent() - Always safe to callLastError() - Always safe to callStart() - Call once only (enforced by mutex)Process() - Sync mode only, single-threaded",{"id":248,"title":249,"titles":250,"content":251,"level":19},"/v1.0.2/learn/architecture#signal-emission","Signal Emission",[198],"Flux emits capitan signals at key points: SignalWhenCapacitorStartedStart() calledCapacitorChangeReceivedWatcher emits valueCapacitorTransformFailedCodec unmarshal failedCapacitorValidationFailedStruct validation failedCapacitorApplyFailedCallback returned errorCapacitorApplySucceededCallback succeededCapacitorStateChangedState transition occurredCapacitorStoppedWatch goroutine exited",{"id":253,"title":254,"titles":255,"content":256,"level":131},"/v1.0.2/learn/architecture#field-keys","Field Keys",[198,249],"Signals carry typed fields: flux.KeyError     // Error message (string)\nflux.KeyOldState  // Previous state (string)\nflux.KeyNewState  // Current state (string)\nflux.KeyState     // State at stop (string)\nflux.KeyDebounce  // Debounce duration (time.Duration)",{"id":258,"title":259,"titles":260,"content":58,"level":19},"/v1.0.2/learn/architecture#failure-handling","Failure Handling",[198],{"id":262,"title":263,"titles":264,"content":265,"level":131},"/v1.0.2/learn/architecture#unmarshal-failure","Unmarshal Failure",[198,259],"Value arrives → Codec.Unmarshal fails → CapacitorTransformFailed emitted\n                                      → State: Degraded (or Empty)\n                                      → Previous config retained",{"id":267,"title":268,"titles":269,"content":270,"level":131},"/v1.0.2/learn/architecture#validation-failure","Validation Failure",[198,259],"Struct created → validate.Struct fails → CapacitorValidationFailed emitted\n                                       → State: Degraded (or Empty)\n                                       → Previous config retained",{"id":272,"title":273,"titles":274,"content":275,"level":131},"/v1.0.2/learn/architecture#callback-failure","Callback Failure",[198,259],"Valid struct → callback returns error → CapacitorApplyFailed emitted\n                                      → State: Degraded (or Empty)\n                                      → Previous config retained In all cases, the Capacitor continues watching. A subsequent valid value recovers the system.",{"id":277,"title":278,"titles":279,"content":280,"level":19},"/v1.0.2/learn/architecture#sync-mode","Sync Mode",[198],"For testing, sync mode disables the background goroutine: capacitor := flux.New[Config](\n    watcher,\n    callback,\n).SyncMode()\n\n// Start processes first value only\ncapacitor.Start(ctx)\n\n// Manually process subsequent values\ncapacitor.Process(ctx)  // Process next available value This makes tests deterministic - no timing dependencies.",{"id":282,"title":283,"titles":284,"content":285,"level":19},"/v1.0.2/learn/architecture#clock-abstraction","Clock Abstraction",[198],"Debouncing uses clockz for time operations: fake := clockz.NewFakeClock()\n\ncapacitor := flux.New[Config](\n    watcher,\n    callback,\n).Clock(fake)\n\n// Advance time manually in tests\nfake.Advance(100 * time.Millisecond)",{"id":287,"title":47,"titles":288,"content":289,"level":19},"/v1.0.2/learn/architecture#next-steps",[198],"Testing Guide - Sync mode and fake clocksAPI Reference - Complete API documentation html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}",{"id":291,"title":292,"titles":293,"content":294,"level":9},"/v1.0.2/guides/testing","Testing",[],"Sync mode, fake clocks, and deterministic testing patterns",{"id":296,"title":292,"titles":297,"content":298,"level":9},"/v1.0.2/guides/testing#testing",[],"Flux provides two mechanisms for deterministic testing: Sync Mode - Manual, step-by-step processingFake Clock - Controlled time for debounce testing",{"id":300,"title":278,"titles":301,"content":302,"level":19},"/v1.0.2/guides/testing#sync-mode",[292],"Sync mode disables async processing. You control exactly when values are processed.",{"id":304,"title":305,"titles":306,"content":307,"level":131},"/v1.0.2/guides/testing#basic-usage","Basic Usage",[292,278],"func TestConfigValidation(t *testing.T) {\n    ch := make(chan []byte, 2)\n    ch \u003C- []byte(`{\"port\": 8080}`)\n    ch \u003C- []byte(`{\"port\": -1}`)  // Invalid\n\n    var applied Config\n\n    capacitor := flux.New[Config](\n        flux.NewSyncChannelWatcher(ch),  // Use sync watcher for deterministic tests\n        func(ctx context.Context, prev, curr Config) error {\n            applied = curr\n            return nil\n        },\n    ).SyncMode()\n\n    ctx := context.Background()\n\n    // Initial load - processed synchronously\n    err := capacitor.Start(ctx)\n    require.NoError(t, err)\n    assert.Equal(t, 8080, applied.Port)\n    assert.Equal(t, flux.StateHealthy, capacitor.State())\n\n    // Second value - must call Process() explicitly\n    capacitor.Process(ctx)\n    assert.Equal(t, 8080, applied.Port)  // Unchanged - validation failed\n    assert.Equal(t, flux.StateDegraded, capacitor.State())\n}",{"id":309,"title":310,"titles":311,"content":312,"level":131},"/v1.0.2/guides/testing#key-points","Key Points",[292,278],"Use NewSyncChannelWatcher with SyncMode() for fully deterministic testsStart() processes the first value synchronouslyProcess(ctx) processes the next available valueProcess() returns true if a value was availableNo background goroutines, no timing dependencies",{"id":314,"title":315,"titles":316,"content":317,"level":131},"/v1.0.2/guides/testing#testing-state-transitions","Testing State Transitions",[292,278],"func TestStateTransitions(t *testing.T) {\n    ch := make(chan []byte, 3)\n    ch \u003C- []byte(`{\"valid\": true}`)   // Healthy\n    ch \u003C- []byte(`{\"valid\": false}`)  // Degraded (callback fails)\n    ch \u003C- []byte(`{\"valid\": true}`)   // Recovery\n\n    capacitor := flux.New[Config](\n        flux.NewSyncChannelWatcher(ch),\n        func(ctx context.Context, prev, curr Config) error {\n            if !curr.Valid {\n                return errors.New(\"invalid config\")\n            }\n            return nil\n        },\n    ).SyncMode()\n\n    ctx := context.Background()\n\n    capacitor.Start(ctx)\n    assert.Equal(t, flux.StateHealthy, capacitor.State())\n\n    capacitor.Process(ctx)\n    assert.Equal(t, flux.StateDegraded, capacitor.State())\n    assert.Error(t, capacitor.LastError())\n\n    capacitor.Process(ctx)\n    assert.Equal(t, flux.StateHealthy, capacitor.State())\n    assert.NoError(t, capacitor.LastError())\n}",{"id":319,"title":320,"titles":321,"content":322,"level":19},"/v1.0.2/guides/testing#fake-clock","Fake Clock",[292],"For testing debounce behavior, use a fake clock: import \"github.com/zoobz-io/clockz\"\n\nfunc TestDebouncing(t *testing.T) {\n    fake := clockz.NewFakeClock()\n\n    ch := make(chan []byte, 3)\n\n    var callCount int\n    capacitor := flux.New[Config](\n        flux.NewSyncChannelWatcher(ch),\n        func(ctx context.Context, prev, curr Config) error {\n            callCount++\n            return nil\n        },\n    ).Clock(fake).Debounce(100*time.Millisecond)\n\n    ctx := context.Background()\n\n    // Send initial value\n    ch \u003C- []byte(`{\"port\": 8080}`)\n    capacitor.Start(ctx)\n    assert.Equal(t, 1, callCount)  // Initial processed immediately\n\n    // Send rapid updates\n    ch \u003C- []byte(`{\"port\": 8081}`)\n    ch \u003C- []byte(`{\"port\": 8082}`)\n    ch \u003C- []byte(`{\"port\": 8083}`)\n\n    // Advance time past debounce\n    fake.Advance(150 * time.Millisecond)\n\n    // Only last value processed\n    assert.Equal(t, 2, callCount)\n}",{"id":324,"title":325,"titles":326,"content":327,"level":19},"/v1.0.2/guides/testing#testing-signals","Testing Signals",[292],"Use capitan's testing helpers to verify signal emission: import capitantesting \"github.com/zoobz-io/capitan/testing\"\n\nfunc TestSignalEmission(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n\n    capture := capitantesting.NewEventCapture()\n    c.Observe(capture.Observer())\n\n    ch := make(chan []byte, 1)\n    ch \u003C- []byte(`{\"port\": \"invalid\"}`)  // Wrong type\n\n    capacitor := flux.New[Config](\n        flux.NewSyncChannelWatcher(ch),\n        func(ctx context.Context, prev, curr Config) error { return nil },\n    ).SyncMode()\n\n    capacitor.Start(context.Background())\n\n    // Verify transform failed signal\n    events := capture.Events()\n    assert.Contains(t, events, flux.CapacitorTransformFailed)\n}",{"id":329,"title":330,"titles":331,"content":332,"level":19},"/v1.0.2/guides/testing#testing-prevcurr-values","Testing Prev/Curr Values",[292],"The callback receives both previous and current values: func TestPrevCurrValues(t *testing.T) {\n    ch := make(chan []byte, 2)\n    ch \u003C- []byte(`{\"version\": 1}`)\n    ch \u003C- []byte(`{\"version\": 2}`)\n\n    var prevVersions, currVersions []int\n\n    capacitor := flux.New[Config](\n        flux.NewSyncChannelWatcher(ch),\n        func(ctx context.Context, prev, curr Config) error {\n            prevVersions = append(prevVersions, prev.Version)\n            currVersions = append(currVersions, curr.Version)\n            return nil\n        },\n    ).SyncMode()\n\n    ctx := context.Background()\n    capacitor.Start(ctx)\n    capacitor.Process(ctx)\n\n    assert.Equal(t, []int{0, 1}, prevVersions)  // Zero value, then 1\n    assert.Equal(t, []int{1, 2}, currVersions)\n}",{"id":334,"title":335,"titles":336,"content":337,"level":19},"/v1.0.2/guides/testing#testing-validation-errors","Testing Validation Errors",[292],"type Config struct {\n    Port int `json:\"port\" validate:\"min=1,max=65535\"`\n}\n\nfunc TestValidationError(t *testing.T) {\n    ch := make(chan []byte, 1)\n    ch \u003C- []byte(`{\"port\": 99999}`)  // Exceeds max\n\n    capacitor := flux.New[Config](\n        flux.NewSyncChannelWatcher(ch),\n        func(ctx context.Context, prev, curr Config) error { return nil },\n    ).SyncMode()\n\n    err := capacitor.Start(context.Background())\n\n    assert.Error(t, err)\n    assert.Contains(t, err.Error(), \"validation failed\")\n    assert.Equal(t, flux.StateEmpty, capacitor.State())\n}",{"id":339,"title":340,"titles":341,"content":342,"level":19},"/v1.0.2/guides/testing#testing-parse-errors","Testing Parse Errors",[292],"func TestParseError(t *testing.T) {\n    ch := make(chan []byte, 1)\n    ch \u003C- []byte(`{invalid json}`)\n\n    capacitor := flux.New[Config](\n        flux.NewSyncChannelWatcher(ch),\n        func(ctx context.Context, prev, curr Config) error { return nil },\n    ).SyncMode()\n\n    err := capacitor.Start(context.Background())\n\n    assert.Error(t, err)\n    assert.Contains(t, err.Error(), \"unmarshal failed\")\n    assert.Equal(t, flux.StateEmpty, capacitor.State())\n}",{"id":344,"title":345,"titles":346,"content":347,"level":19},"/v1.0.2/guides/testing#integration-testing-with-providers","Integration Testing with Providers",[292],"Provider packages include testcontainers-based tests. Run them: # Requires Docker\ngo test ./pkg/redis/...\ngo test ./pkg/consul/...\ngo test ./pkg/etcd/... For your own integration tests, follow the same pattern: func TestRedisIntegration(t *testing.T) {\n    if testing.Short() {\n        t.Skip(\"skipping integration test\")\n    }\n\n    // Set up testcontainer\n    ctx := context.Background()\n    container, _ := setupRedis(t)\n    client := connectRedis(container)\n\n    // Test your configuration flow\n    capacitor := flux.New[Config](\n        redis.New(client, \"test:config\"),\n        func(ctx context.Context, prev, curr Config) error { return nil },\n    )\n\n    client.Set(ctx, \"test:config\", `{\"port\": 8080}`, 0)\n\n    err := capacitor.Start(ctx)\n    assert.NoError(t, err)\n}",{"id":349,"title":47,"titles":350,"content":351,"level":19},"/v1.0.2/guides/testing#next-steps",[292],"Providers Guide - Provider-specific detailsArchitecture - How sync mode works internally html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}",{"id":353,"title":32,"titles":354,"content":355,"level":9},"/v1.0.2/guides/providers",[],"Overview of all flux data source providers",{"id":357,"title":32,"titles":358,"content":359,"level":9},"/v1.0.2/guides/providers#providers",[],"Flux providers live in pkg/ with isolated dependencies. Each implements the Watcher interface for a specific backend.",{"id":361,"title":66,"titles":362,"content":363,"level":19},"/v1.0.2/guides/providers#installation",[32],"Install only the providers you need: go get github.com/zoobz-io/flux/file\ngo get github.com/zoobz-io/flux/redis\ngo get github.com/zoobz-io/flux/consul\n# etc.",{"id":365,"title":366,"titles":367,"content":368,"level":19},"/v1.0.2/guides/providers#provider-overview","Provider Overview",[32],"ProviderBackendWatch MechanismUse Casepkg/fileLocal FSfsnotifyLocal config filespkg/redisRedisKeyspace notificationsShared config, feature flagspkg/consulConsul KVBlocking queriesService configurationpkg/etcdetcdWatch APIDistributed configpkg/natsNATS JetStreamKV WatchCloud-native messagingpkg/kubernetesConfigMap/SecretWatch APIK8s-native appspkg/zookeeperZooKeeperNode watchLegacy systemspkg/firestoreFirestoreRealtime listenersGCP applications",{"id":370,"title":371,"titles":372,"content":373,"level":19},"/v1.0.2/guides/providers#file","File",[32],"Watch local filesystem files using fsnotify. import \"github.com/zoobz-io/flux/file\"\n\ncapacitor := flux.New[Config](\n    file.New(\"/etc/myapp/config.json\"),\n    callback,\n) Best for: Local development, single-instance deployments, config files managed by CM tools.",{"id":375,"title":376,"titles":377,"content":378,"level":19},"/v1.0.2/guides/providers#redis","Redis",[32],"Watch Redis keys using keyspace notifications. import (\n    \"github.com/redis/go-redis/v9\"\n    fluxredis \"github.com/zoobz-io/flux/redis\"\n)\n\nclient := redis.NewClient(&redis.Options{Addr: \"localhost:6379\"})\n\ncapacitor := flux.New[Config](\n    fluxredis.New(client, \"myapp:config\"),\n    callback,\n) Requires keyspace notifications enabled: redis-cli CONFIG SET notify-keyspace-events KEA Best for: Feature flags, shared configuration across instances.",{"id":380,"title":381,"titles":382,"content":383,"level":19},"/v1.0.2/guides/providers#consul","Consul",[32],"Watch Consul KV using blocking queries. import (\n    \"github.com/hashicorp/consul/api\"\n    fluxconsul \"github.com/zoobz-io/flux/consul\"\n)\n\nclient, _ := api.NewClient(api.DefaultConfig())\n\ncapacitor := flux.New[Config](\n    fluxconsul.New(client, \"myapp/config\"),\n    callback,\n) Best for: HashiCorp ecosystem, service mesh configurations.",{"id":385,"title":386,"titles":387,"content":388,"level":19},"/v1.0.2/guides/providers#etcd","etcd",[32],"Watch etcd keys using the native Watch API. import (\n    clientv3 \"go.etcd.io/etcd/client/v3\"\n    fluxetcd \"github.com/zoobz-io/flux/etcd\"\n)\n\nclient, _ := clientv3.New(clientv3.Config{\n    Endpoints: []string{\"localhost:2379\"},\n})\n\ncapacitor := flux.New[Config](\n    fluxetcd.New(client, \"/myapp/config\"),\n    callback,\n) Best for: Kubernetes ecosystem, distributed systems requiring strong consistency.",{"id":390,"title":391,"titles":392,"content":393,"level":19},"/v1.0.2/guides/providers#nats","NATS",[32],"Watch NATS JetStream KV buckets. import (\n    \"github.com/nats-io/nats.go\"\n    \"github.com/nats-io/nats.go/jetstream\"\n    fluxnats \"github.com/zoobz-io/flux/nats\"\n)\n\nnc, _ := nats.Connect(\"nats://localhost:4222\")\njs, _ := jetstream.New(nc)\nkv, _ := js.KeyValue(ctx, \"config\")\n\ncapacitor := flux.New[Config](\n    fluxnats.New(kv, \"myapp\"),\n    callback,\n) Best for: Cloud-native apps, microservices using NATS for messaging.",{"id":395,"title":396,"titles":397,"content":398,"level":19},"/v1.0.2/guides/providers#kubernetes","Kubernetes",[32],"Watch ConfigMaps or Secrets. import (\n    \"k8s.io/client-go/kubernetes\"\n    \"k8s.io/client-go/rest\"\n    fluxk8s \"github.com/zoobz-io/flux/kubernetes\"\n)\n\nconfig, _ := rest.InClusterConfig()\nclient, _ := kubernetes.NewForConfig(config)\n\n// ConfigMap (default)\ncapacitor := flux.New[Config](\n    fluxk8s.New(client, \"default\", \"myapp-config\", \"config.json\"),\n    callback,\n)\n\n// Secret\ncapacitor := flux.New[Config](\n    fluxk8s.New(client, \"default\", \"myapp-secret\", \"config.json\",\n        fluxk8s.WithResourceType(fluxk8s.Secret),\n    ),\n    callback,\n) Best for: Kubernetes-native applications, GitOps workflows.",{"id":400,"title":401,"titles":402,"content":403,"level":19},"/v1.0.2/guides/providers#zookeeper","ZooKeeper",[32],"Watch ZooKeeper nodes. import (\n    \"github.com/go-zookeeper/zk\"\n    fluxzk \"github.com/zoobz-io/flux/zookeeper\"\n)\n\nconn, _, _ := zk.Connect([]string{\"localhost:2181\"}, 5*time.Second)\n\ncapacitor := flux.New[Config](\n    fluxzk.New(conn, \"/config/myapp\"),\n    callback,\n) Best for: Legacy systems, Kafka ecosystem, existing ZK infrastructure.",{"id":405,"title":406,"titles":407,"content":408,"level":19},"/v1.0.2/guides/providers#firestore","Firestore",[32],"Watch Firestore documents using realtime listeners. import (\n    \"cloud.google.com/go/firestore\"\n    fluxfs \"github.com/zoobz-io/flux/firestore\"\n)\n\nclient, _ := firestore.NewClient(ctx, \"my-project\")\n\ncapacitor := flux.New[Config](\n    fluxfs.New(client, \"config\", \"myapp\"),\n    callback,\n) Document structure: By default expects a data field containing JSON: {\"data\": \"{\\\"port\\\": 8080}\"} Use helper functions: fluxfs.CreateDocument(ctx, client, \"config\", \"myapp\", []byte(`{\"port\": 8080}`))\nfluxfs.UpdateDocument(ctx, client, \"config\", \"myapp\", []byte(`{\"port\": 9090}`)) Best for: GCP applications, serverless deployments on Cloud Run/Functions.",{"id":410,"title":411,"titles":412,"content":413,"level":19},"/v1.0.2/guides/providers#choosing-a-provider","Choosing a Provider",[32],"RequirementRecommendedSimple, single instancefileShared across instancesredis, consul, etcdKubernetes-nativekubernetesGCP/FirebasefirestoreNATS messagingnatsExisting ZK infrastructurezookeeper",{"id":415,"title":47,"titles":416,"content":417,"level":19},"/v1.0.2/guides/providers#next-steps",[32],"Custom Watcher - Build your own providerMulti-Source - Combine multiple providers html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}",{"id":419,"title":420,"titles":421,"content":422,"level":9},"/v1.0.2/guides/state","State Management",[],"State transitions, recovery patterns, and graceful degradation",{"id":424,"title":420,"titles":425,"content":426,"level":9},"/v1.0.2/guides/state#state-management",[],"Flux maintains a state machine that tracks configuration health. This guide covers state transitions, failure handling, and recovery patterns.",{"id":428,"title":429,"titles":430,"content":431,"level":19},"/v1.0.2/guides/state#state-overview","State Overview",[420],"StateHas ConfigHas ErrorMeaningLoadingNoNoWaiting for first valueHealthyYesNoValid config activeDegradedYesYesUpdate failed, previous retainedEmptyNoYesNo valid config ever obtained",{"id":433,"title":169,"titles":434,"content":435,"level":19},"/v1.0.2/guides/state#checking-state",[420],"switch capacitor.State() {\ncase flux.StateLoading:\n    log.Println(\"waiting for configuration...\")\n\ncase flux.StateHealthy:\n    cfg, _ := capacitor.Current()\n    log.Printf(\"using config: %+v\", cfg)\n\ncase flux.StateDegraded:\n    cfg, _ := capacitor.Current()\n    err := capacitor.LastError()\n    log.Printf(\"using fallback config, error: %v\", err)\n\ncase flux.StateEmpty:\n    err := capacitor.LastError()\n    log.Fatalf(\"no valid config: %v\", err)\n}",{"id":437,"title":438,"titles":439,"content":440,"level":19},"/v1.0.2/guides/state#initial-load-failure","Initial Load Failure",[420],"When the first configuration fails: err := capacitor.Start(ctx)\nif err != nil {\n    // State is Empty\n    // Capacitor continues watching for valid config\n\n    switch capacitor.State() {\n    case flux.StateEmpty:\n        // Option 1: Fatal\n        log.Fatalf(\"cannot start without config: %v\", err)\n\n        // Option 2: Use defaults\n        useDefaults()\n        log.Printf(\"using defaults, waiting for config: %v\", err)\n\n        // Option 3: Wait for recovery\n        go waitForHealthy(capacitor)\n    }\n}",{"id":442,"title":443,"titles":444,"content":445,"level":131},"/v1.0.2/guides/state#wait-for-recovery","Wait for Recovery",[420,438],"func waitForHealthy[T any](c *flux.Capacitor[T]) {\n    ticker := time.NewTicker(5 * time.Second)\n    defer ticker.Stop()\n\n    for range ticker.C {\n        if c.State() == flux.StateHealthy {\n            log.Println(\"configuration recovered\")\n            return\n        }\n        log.Printf(\"waiting for valid config: %v\", c.LastError())\n    }\n}",{"id":447,"title":448,"titles":449,"content":450,"level":19},"/v1.0.2/guides/state#degraded-state","Degraded State",[420],"When an update fails but previous config exists: capitan.Hook(flux.CapacitorStateChanged, func(ctx context.Context, e *capitan.Event) {\n    newState, _ := flux.KeyNewState.From(e)\n\n    if newState == flux.StateDegraded.String() {\n        alerting.Send(alert{\n            Severity: \"warning\",\n            Message:  \"config update failed, using previous version\",\n        })\n    }\n})",{"id":452,"title":453,"titles":454,"content":455,"level":131},"/v1.0.2/guides/state#monitoring-degraded-duration","Monitoring Degraded Duration",[420,448],"func monitorState[T any](c *flux.Capacitor[T]) {\n    ticker := time.NewTicker(10 * time.Second)\n    defer ticker.Stop()\n\n    for range ticker.C {\n        state := c.State()\n        metrics.Gauge(\"config_state\", stateToNumber(state))\n\n        if state == flux.StateDegraded {\n            metrics.Increment(\"config_degraded_seconds\", 10)\n        }\n    }\n}\n\nfunc stateToNumber(s flux.State) float64 {\n    switch s {\n    case flux.StateHealthy:  return 1\n    case flux.StateDegraded: return 0.5\n    case flux.StateEmpty:    return 0\n    default:                 return -1\n    }\n}",{"id":457,"title":458,"titles":459,"content":460,"level":19},"/v1.0.2/guides/state#automatic-recovery","Automatic Recovery",[420],"Recovery happens automatically when a valid config arrives: capitan.Hook(flux.CapacitorStateChanged, func(ctx context.Context, e *capitan.Event) {\n    old, _ := flux.KeyOldState.From(e)\n    new, _ := flux.KeyNewState.From(e)\n\n    if old == flux.StateDegraded.String() && new == flux.StateHealthy.String() {\n        log.Println(\"configuration recovered from degraded state\")\n        alerting.Resolve(\"config_degraded\")\n    }\n})",{"id":462,"title":463,"titles":464,"content":465,"level":19},"/v1.0.2/guides/state#error-classification","Error Classification",[420],"Different signals for different error types: // Parse errors (JSON/YAML syntax)\ncapitan.Hook(flux.CapacitorTransformFailed, func(ctx context.Context, e *capitan.Event) {\n    err, _ := flux.KeyError.From(e)\n    log.Printf(\"config parse error: %s\", err)\n    metrics.Increment(\"config_parse_errors\")\n})\n\n// Validation errors (Validate() method returned error)\ncapitan.Hook(flux.CapacitorValidationFailed, func(ctx context.Context, e *capitan.Event) {\n    err, _ := flux.KeyError.From(e)\n    log.Printf(\"config validation error: %s\", err)\n    metrics.Increment(\"config_validation_errors\")\n})\n\n// Callback errors (application-level)\ncapitan.Hook(flux.CapacitorApplyFailed, func(ctx context.Context, e *capitan.Event) {\n    err, _ := flux.KeyError.From(e)\n    log.Printf(\"config apply error: %s\", err)\n    metrics.Increment(\"config_apply_errors\")\n})",{"id":467,"title":468,"titles":469,"content":470,"level":19},"/v1.0.2/guides/state#graceful-degradation","Graceful Degradation",[420],"Design your application to handle degraded state: type Application struct {\n    capacitor *flux.Capacitor[Config]\n}\n\nfunc (a *Application) Config() Config {\n    cfg, ok := a.capacitor.Current()\n    if !ok {\n        // No valid config - return safe defaults\n        return Config{\n            MaxConnections: 10,\n            Timeout:        30 * time.Second,\n        }\n    }\n    return cfg\n}\n\nfunc (a *Application) IsHealthy() bool {\n    return a.capacitor.State() == flux.StateHealthy\n}",{"id":472,"title":473,"titles":474,"content":475,"level":19},"/v1.0.2/guides/state#health-check-endpoint","Health Check Endpoint",[420],"func (a *Application) HealthHandler(w http.ResponseWriter, r *http.Request) {\n    state := a.capacitor.State()\n\n    response := struct {\n        Status string `json:\"status\"`\n        Config string `json:\"config\"`\n        Error  string `json:\"error,omitempty\"`\n    }{\n        Status: state.String(),\n    }\n\n    switch state {\n    case flux.StateHealthy:\n        w.WriteHeader(http.StatusOK)\n        response.Config = \"current\"\n\n    case flux.StateDegraded:\n        w.WriteHeader(http.StatusOK)  // Still serving\n        response.Config = \"previous\"\n        if err := a.capacitor.LastError(); err != nil {\n            response.Error = err.Error()\n        }\n\n    case flux.StateEmpty:\n        w.WriteHeader(http.StatusServiceUnavailable)\n        response.Config = \"none\"\n        if err := a.capacitor.LastError(); err != nil {\n            response.Error = err.Error()\n        }\n\n    default:\n        w.WriteHeader(http.StatusServiceUnavailable)\n        response.Config = \"loading\"\n    }\n\n    json.NewEncoder(w).Encode(response)\n}",{"id":477,"title":478,"titles":479,"content":480,"level":19},"/v1.0.2/guides/state#error-history","Error History",[420],"Track recent errors with ErrorHistorySize: capacitor := flux.New[Config](\n    watcher,\n    callback,\n).ErrorHistorySize(10)  // Keep last 10 errors\n\ncapacitor.Start(ctx)\n\n// After some failures...\nfor _, err := range capacitor.ErrorHistory() {\n    log.Printf(\"error: %v\", err)\n} When enabled, ErrorHistory() returns errors oldest-first. Use LastError() for just the most recent error regardless of history setting.",{"id":482,"title":483,"titles":484,"content":485,"level":19},"/v1.0.2/guides/state#circuit-breaker","Circuit Breaker",[420],"Protect against cascading failures with WithCircuitBreaker: capacitor := flux.New[Config](\n    watcher,\n    callback,\n    flux.WithCircuitBreaker[Config](5, 30*time.Second),\n) After 5 consecutive failures, the circuit opens and rejects further processing until 30 seconds have passed. The circuit breaker has three states: Closed: Normal operation, requests pass throughOpen: After threshold failures, requests are rejected immediatelyHalf-Open: After recovery timeout, one request is allowed to test recovery The circuit breaker protects the entire pipeline. Any success resets the failure counter and closes the circuit.",{"id":487,"title":488,"titles":489,"content":490,"level":19},"/v1.0.2/guides/state#metrics-integration","Metrics Integration",[420],"Integrate with your metrics system via Metrics: type MyMetrics struct{}\n\nfunc (m *MyMetrics) OnStateChange(from, to flux.State) {\n    metrics.Gauge(\"config_state\", stateToNumber(to))\n    metrics.Increment(\"config_state_changes\")\n}\n\nfunc (m *MyMetrics) OnProcessSuccess(duration time.Duration) {\n    metrics.Histogram(\"config_process_duration_ms\", duration.Milliseconds())\n    metrics.Increment(\"config_applies_total\")\n}\n\nfunc (m *MyMetrics) OnProcessFailure(stage string, duration time.Duration) {\n    metrics.Increment(\"config_failures_total\", \"stage\", stage)\n}\n\nfunc (m *MyMetrics) OnChangeReceived() {\n    metrics.Increment(\"config_changes_received_total\")\n}\n\n// Usage\ncapacitor := flux.New[Config](\n    watcher,\n    callback,\n).Metrics(&MyMetrics{}) Stages passed to OnProcessFailure: \"unmarshal\", \"validate\", \"pipeline\".",{"id":492,"title":493,"titles":494,"content":495,"level":19},"/v1.0.2/guides/state#shutdown-handling","Shutdown Handling",[420],"Handle graceful shutdown with OnStop: capacitor := flux.New[Config](\n    watcher,\n    callback,\n).OnStop(func(finalState flux.State) {\n    log.Printf(\"capacitor stopped in state: %s\", finalState)\n    metrics.Gauge(\"config_state\", -1)  // Mark as stopped\n}) The callback is invoked when the watch goroutine exits (context cancelled).",{"id":497,"title":498,"titles":499,"content":500,"level":19},"/v1.0.2/guides/state#state-history","State History",[420],"Track all transitions for debugging: type StateChange struct {\n    Timestamp time.Time\n    From      string\n    To        string\n}\n\nvar stateHistory []StateChange\nvar mu sync.Mutex\n\ncapitan.Hook(flux.CapacitorStateChanged, func(ctx context.Context, e *capitan.Event) {\n    old, _ := flux.KeyOldState.From(e)\n    new, _ := flux.KeyNewState.From(e)\n\n    mu.Lock()\n    stateHistory = append(stateHistory, StateChange{\n        Timestamp: time.Now(),\n        From:      old,\n        To:        new,\n    })\n    mu.Unlock()\n})",{"id":502,"title":47,"titles":503,"content":504,"level":19},"/v1.0.2/guides/state#next-steps",[420],"Best Practices - Production patternsTesting Guide - Testing state transitions html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"id":506,"title":507,"titles":508,"content":509,"level":9},"/v1.0.2/guides/best-practices","Best Practices",[],"Guidelines for using flux effectively in production",{"id":511,"title":507,"titles":512,"content":513,"level":9},"/v1.0.2/guides/best-practices#best-practices",[],"Guidelines for using flux effectively in production systems.",{"id":515,"title":516,"titles":517,"content":58,"level":19},"/v1.0.2/guides/best-practices#configuration-design","Configuration Design",[507],{"id":519,"title":520,"titles":521,"content":522,"level":131},"/v1.0.2/guides/best-practices#keep-configs-small-and-focused","Keep Configs Small and Focused",[507,516],"// Good: Focused configuration\ntype FeatureFlags struct {\n    EnableNewCheckout bool `json:\"enable_new_checkout\"`\n    EnableDarkMode    bool `json:\"enable_dark_mode\"`\n    MaxUploadSize     int  `json:\"max_upload_size\" validate:\"min=1\"`\n}\n\n// Avoid: Kitchen sink configuration\ntype EverythingConfig struct {\n    Database    DatabaseConfig\n    Cache       CacheConfig\n    Features    FeatureFlags\n    Logging     LoggingConfig\n    Secrets     SecretsConfig  // Don't mix secrets with config\n    // ...50 more fields\n}",{"id":524,"title":525,"titles":526,"content":527,"level":131},"/v1.0.2/guides/best-practices#implement-thorough-validation","Implement Thorough Validation",[507,516],"Always implement comprehensive Validate() methods: type Config struct {\n    Port        int           `json:\"port\"`\n    Host        string        `json:\"host\"`\n    Timeout     time.Duration `json:\"timeout\"`\n    LogLevel    string        `json:\"log_level\"`\n    MaxRetries  int           `json:\"max_retries\"`\n    Concurrency int           `json:\"concurrency\"`\n}\n\nfunc (c Config) Validate() error {\n    if c.Port \u003C 1 || c.Port > 65535 {\n        return fmt.Errorf(\"port must be between 1 and 65535\")\n    }\n    if c.Host == \"\" {\n        return errors.New(\"host is required\")\n    }\n    if c.Timeout \u003C time.Second || c.Timeout > 5*time.Minute {\n        return errors.New(\"timeout must be between 1s and 5m\")\n    }\n    switch c.LogLevel {\n    case \"debug\", \"info\", \"warn\", \"error\":\n    default:\n        return errors.New(\"log_level must be one of: debug, info, warn, error\")\n    }\n    if c.MaxRetries \u003C 0 || c.MaxRetries > 10 {\n        return errors.New(\"max_retries must be between 0 and 10\")\n    }\n    if c.Concurrency \u003C 1 || c.Concurrency > 1000 {\n        return errors.New(\"concurrency must be between 1 and 1000\")\n    }\n    return nil\n}",{"id":529,"title":530,"titles":531,"content":532,"level":131},"/v1.0.2/guides/best-practices#validate-business-rules-in-callback","Validate Business Rules in Callback",[507,516],"For rules that span multiple fields: capacitor := flux.New[Config](\n    watcher,\n    func(ctx context.Context, prev, curr Config) error {\n        // Cross-field validation\n        if curr.MinConnections > curr.MaxConnections {\n            return errors.New(\"min_connections cannot exceed max_connections\")\n        }\n\n        // Safe transitions\n        if prev.MaxConnections > 0 && curr.MaxConnections \u003C prev.MaxConnections/2 {\n            return errors.New(\"cannot reduce max_connections by more than 50%\")\n        }\n\n        return app.Apply(curr)\n    },\n)",{"id":534,"title":535,"titles":536,"content":58,"level":19},"/v1.0.2/guides/best-practices#callback-design","Callback Design",[507],{"id":538,"title":539,"titles":540,"content":541,"level":131},"/v1.0.2/guides/best-practices#keep-callbacks-fast","Keep Callbacks Fast",[507,535],"The callback blocks the processing goroutine. Avoid slow operations: // Good: Quick application\nfunc(ctx context.Context, prev, curr Config) error {\n    app.config.Store(&curr)\n    return nil\n}\n\n// Avoid: Slow operations in callback\nfunc(ctx context.Context, prev, curr Config) error {\n    db.Migrate(curr.Schema)        // Slow\n    http.Post(webhookURL, curr)    // Network I/O\n    time.Sleep(time.Second)        // Never do this\n    return nil\n} For slow operations, trigger them asynchronously: func(ctx context.Context, prev, curr Config) error {\n    app.config.Store(&curr)\n\n    // Trigger async if needed\n    if prev.Schema != curr.Schema {\n        go app.scheduleMigration(curr.Schema)\n    }\n\n    return nil\n}",{"id":543,"title":544,"titles":545,"content":546,"level":131},"/v1.0.2/guides/best-practices#handle-prev-value-correctly","Handle Prev Value Correctly",[507,535],"On first load, prev is the zero value: func(ctx context.Context, prev, curr Config) error {\n    if prev.Port == 0 {\n        // First load - initialize\n        return app.Initialize(curr)\n    }\n\n    // Subsequent loads - update\n    if prev.Port != curr.Port {\n        return app.RestartServer(curr.Port)\n    }\n\n    return app.UpdateConfig(curr)\n}",{"id":548,"title":96,"titles":549,"content":58,"level":19},"/v1.0.2/guides/best-practices#error-handling",[507],{"id":551,"title":552,"titles":553,"content":554,"level":131},"/v1.0.2/guides/best-practices#dont-swallow-errors","Don't Swallow Errors",[507,96],"Return errors from callback to trigger Degraded state: // Good: Return errors\nfunc(ctx context.Context, prev, curr Config) error {\n    if err := app.Apply(curr); err != nil {\n        return fmt.Errorf(\"failed to apply config: %w\", err)\n    }\n    return nil\n}\n\n// Avoid: Swallowing errors\nfunc(ctx context.Context, prev, curr Config) error {\n    if err := app.Apply(curr); err != nil {\n        log.Printf(\"error: %v\", err)  // Logged but not returned\n    }\n    return nil  // State shows Healthy but config wasn't applied\n}",{"id":556,"title":557,"titles":558,"content":559,"level":131},"/v1.0.2/guides/best-practices#monitor-all-error-types","Monitor All Error Types",[507,96],"func setupMonitoring() {\n    capitan.Hook(flux.CapacitorTransformFailed, logAndAlert)\n    capitan.Hook(flux.CapacitorValidationFailed, logAndAlert)\n    capitan.Hook(flux.CapacitorApplyFailed, logAndAlert)\n}\n\nfunc logAndAlert(ctx context.Context, e *capitan.Event) {\n    err, _ := flux.KeyError.From(e)\n    log.Printf(\"[%s] %s\", e.Signal.Name(), err)\n    metrics.Increment(\"config_errors\", \"signal\", e.Signal.Name())\n}",{"id":561,"title":562,"titles":563,"content":58,"level":19},"/v1.0.2/guides/best-practices#debounce-tuning","Debounce Tuning",[507],{"id":565,"title":566,"titles":567,"content":568,"level":131},"/v1.0.2/guides/best-practices#match-your-use-case","Match Your Use Case",[507,562],"// Fast iteration (development)\nflux.New[Config](watcher, callback).Debounce(50 * time.Millisecond)\n\n// Normal operation (production)\nflux.New[Config](watcher, callback).Debounce(100 * time.Millisecond)  // Default\n\n// Expensive operations\nflux.New[Config](watcher, callback).Debounce(500 * time.Millisecond)\n\n// Very expensive (database migrations)\nflux.New[Config](watcher, callback).Debounce(5 * time.Second)",{"id":570,"title":571,"titles":572,"content":573,"level":131},"/v1.0.2/guides/best-practices#consider-source-characteristics","Consider Source Characteristics",[507,562],"SourceRecommended DebounceFile (editor save)100-200msRedis/Consul50-100msKubernetes ConfigMap100-500msDatabase200-1000ms",{"id":575,"title":292,"titles":576,"content":58,"level":19},"/v1.0.2/guides/best-practices#testing",[507],{"id":578,"title":579,"titles":580,"content":581,"level":131},"/v1.0.2/guides/best-practices#always-use-sync-mode-in-unit-tests","Always Use Sync Mode in Unit Tests",[507,292],"func TestConfig(t *testing.T) {\n    capacitor := flux.New[Config](\n        flux.NewSyncChannelWatcher(ch),\n        callback,\n    ).SyncMode()  // Deterministic\n}",{"id":583,"title":584,"titles":585,"content":586,"level":131},"/v1.0.2/guides/best-practices#test-error-paths","Test Error Paths",[507,292],"func TestInvalidConfig(t *testing.T) {\n    ch := make(chan []byte, 1)\n    ch \u003C- []byte(`{\"port\": -1}`)  // Invalid\n\n    capacitor := flux.New[Config](\n        flux.NewSyncChannelWatcher(ch),\n        callback,\n    ).SyncMode()\n\n    err := capacitor.Start(ctx)\n    assert.Error(t, err)\n    assert.Equal(t, flux.StateEmpty, capacitor.State())\n}",{"id":588,"title":589,"titles":590,"content":591,"level":131},"/v1.0.2/guides/best-practices#test-recovery","Test Recovery",[507,292],"func TestRecovery(t *testing.T) {\n    ch := make(chan []byte, 3)\n    ch \u003C- []byte(`{\"port\": 8080}`)  // Valid\n    ch \u003C- []byte(`{\"port\": -1}`)    // Invalid\n    ch \u003C- []byte(`{\"port\": 9090}`)  // Recovery\n\n    // ... verify state transitions\n}",{"id":593,"title":594,"titles":595,"content":596,"level":19},"/v1.0.2/guides/best-practices#production-checklist","Production Checklist",[507],"Validation tags on all config fields Business rule validation in callback Error monitoring via capitan hooks Health endpoint exposing state Metrics for state and errors Alerting on Degraded/Empty states Graceful degradation with defaults Integration tests with real providers Documentation of config schema",{"id":598,"title":599,"titles":600,"content":58,"level":19},"/v1.0.2/guides/best-practices#anti-patterns","Anti-Patterns",[507],{"id":602,"title":603,"titles":604,"content":605,"level":131},"/v1.0.2/guides/best-practices#dont-create-capacitors-dynamically","Don't Create Capacitors Dynamically",[507,599],"// Bad: Creates unbounded goroutines\nfunc handleRequest(key string) {\n    capacitor := flux.New[Config](\n        redis.New(client, key),\n        callback,\n    )\n    capacitor.Start(ctx)\n}",{"id":607,"title":608,"titles":609,"content":610,"level":131},"/v1.0.2/guides/best-practices#dont-ignore-initial-errors","Don't Ignore Initial Errors",[507,599],"// Bad: Ignoring initial error\ncapacitor.Start(ctx)  // Error discarded\n\n// Good: Handle initial error\nif err := capacitor.Start(ctx); err != nil {\n    // Decide: fatal, defaults, or wait\n}",{"id":612,"title":613,"titles":614,"content":615,"level":131},"/v1.0.2/guides/best-practices#dont-mix-secrets-with-config","Don't Mix Secrets with Config",[507,599],"// Bad: Secrets in config\ntype Config struct {\n    Port      int    `json:\"port\"`\n    APISecret string `json:\"api_secret\"`  // Use secret manager\n}\n\n// Good: Separate concerns\ntype Config struct {\n    Port int `json:\"port\"`\n}\n// Use dedicated secret manager for sensitive data",{"id":617,"title":47,"titles":618,"content":619,"level":19},"/v1.0.2/guides/best-practices#next-steps",[507],"State Management - Recovery patternsTesting Guide - Comprehensive testing html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"id":621,"title":622,"titles":623,"content":624,"level":9},"/v1.0.2/cookbook/file-config","File Configuration",[],"Complete file-based configuration example with observability",{"id":626,"title":622,"titles":627,"content":628,"level":9},"/v1.0.2/cookbook/file-config#file-configuration",[],"Recipe: Complete file-based configuration with observability, error handling, and graceful shutdown.",{"id":630,"title":631,"titles":632,"content":633,"level":19},"/v1.0.2/cookbook/file-config#the-scenario","The Scenario",[622],"A web service that reads configuration from a YAML file. When the file changes, the service reloads without restart.",{"id":635,"title":636,"titles":637,"content":638,"level":19},"/v1.0.2/cookbook/file-config#project-structure","Project Structure",[622],"myapp/\n├── main.go\n├── config.yaml\n└── go.mod",{"id":640,"title":641,"titles":642,"content":643,"level":19},"/v1.0.2/cookbook/file-config#configuration-type","Configuration Type",[622],"// config.go\npackage main\n\nimport \"time\"\n\ntype Config struct {\n    Server   ServerConfig   `yaml:\"server\"`\n    Database DatabaseConfig `yaml:\"database\"`\n    Features FeatureFlags   `yaml:\"features\"`\n}\n\nfunc (c Config) Validate() error {\n    if err := c.Server.Validate(); err != nil {\n        return fmt.Errorf(\"server: %w\", err)\n    }\n    if err := c.Database.Validate(); err != nil {\n        return fmt.Errorf(\"database: %w\", err)\n    }\n    return nil\n}\n\ntype ServerConfig struct {\n    Port         int           `yaml:\"port\"`\n    ReadTimeout  time.Duration `yaml:\"read_timeout\"`\n    WriteTimeout time.Duration `yaml:\"write_timeout\"`\n}\n\nfunc (s ServerConfig) Validate() error {\n    if s.Port \u003C 1 || s.Port > 65535 {\n        return errors.New(\"port must be between 1 and 65535\")\n    }\n    if s.ReadTimeout \u003C time.Second {\n        return errors.New(\"read_timeout must be at least 1s\")\n    }\n    if s.WriteTimeout \u003C time.Second {\n        return errors.New(\"write_timeout must be at least 1s\")\n    }\n    return nil\n}\n\ntype DatabaseConfig struct {\n    URL             string `yaml:\"url\"`\n    MaxConnections  int    `yaml:\"max_connections\"`\n    IdleConnections int    `yaml:\"idle_connections\"`\n}\n\nfunc (d DatabaseConfig) Validate() error {\n    if d.URL == \"\" {\n        return errors.New(\"url is required\")\n    }\n    if d.MaxConnections \u003C 1 {\n        return errors.New(\"max_connections must be at least 1\")\n    }\n    if d.IdleConnections \u003C 0 {\n        return errors.New(\"idle_connections must be non-negative\")\n    }\n    return nil\n}\n\ntype FeatureFlags struct {\n    EnableMetrics bool `yaml:\"enable_metrics\"`\n    EnableTracing bool `yaml:\"enable_tracing\"`\n    DebugMode     bool `yaml:\"debug_mode\"`\n}",{"id":645,"title":76,"titles":646,"content":647,"level":19},"/v1.0.2/cookbook/file-config#config-file",[622],"# config.yaml\nserver:\n  port: 8080\n  read_timeout: 30s\n  write_timeout: 30s\n\ndatabase:\n  url: postgres://localhost:5432/myapp\n  max_connections: 100\n  idle_connections: 10\n\nfeatures:\n  enable_metrics: true\n  enable_tracing: false\n  debug_mode: false",{"id":649,"title":650,"titles":651,"content":652,"level":19},"/v1.0.2/cookbook/file-config#main-application","Main Application",[622],"// main.go\npackage main\n\nimport (\n    \"context\"\n    \"encoding/json\"\n    \"fmt\"\n    \"log\"\n    \"net/http\"\n    \"os\"\n    \"os/signal\"\n    \"sync/atomic\"\n    \"time\"\n\n    \"github.com/zoobz-io/capitan\"\n    \"github.com/zoobz-io/flux\"\n    \"github.com/zoobz-io/flux/file\"\n)\n\ntype Application struct {\n    config    atomic.Pointer[Config]\n    capacitor *flux.Capacitor[Config]\n}\n\nfunc (a *Application) Config() Config {\n    return *a.config.Load()\n}\n\nfunc (a *Application) UpdateConfig(ctx context.Context, prev, curr Config) error {\n    // Validate business rules\n    if curr.Database.IdleConnections > curr.Database.MaxConnections {\n        return fmt.Errorf(\"idle_connections cannot exceed max_connections\")\n    }\n\n    log.Printf(\"config updated: port=%d, max_conn=%d, debug=%v\",\n        curr.Server.Port,\n        curr.Database.MaxConnections,\n        curr.Features.DebugMode,\n    )\n\n    a.config.Store(&curr)\n    return nil\n}\n\nfunc main() {\n    app := &Application{}\n\n    // Set up observability\n    setupObservability()\n\n    // Create capacitor\n    app.capacitor = flux.New[Config](\n        file.New(\"config.yaml\"),\n        app.UpdateConfig,\n    ).Codec(flux.YAMLCodec{}).Debounce(100*time.Millisecond)\n\n    // Start watching\n    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)\n    defer cancel()\n\n    log.Println(\"starting configuration watcher...\")\n\n    if err := app.capacitor.Start(ctx); err != nil {\n        log.Printf(\"WARNING: initial config failed: %v\", err)\n        log.Println(\"continuing to watch for valid configuration...\")\n    }\n\n    logState(app.capacitor)\n\n    // Start HTTP server\n    server := startServer(app)\n\n    // Wait for shutdown\n    \u003C-ctx.Done()\n\n    log.Println(\"shutting down...\")\n    shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)\n    defer shutdownCancel()\n    server.Shutdown(shutdownCtx)\n}\n\nfunc setupObservability() {\n    capitan.Hook(flux.CapacitorStateChanged, func(ctx context.Context, e *capitan.Event) {\n        oldState, _ := flux.KeyOldState.From(e)\n        newState, _ := flux.KeyNewState.From(e)\n        log.Printf(\"config state: %s -> %s\", oldState, newState)\n    })\n\n    capitan.Hook(flux.CapacitorChangeReceived, func(ctx context.Context, e *capitan.Event) {\n        log.Println(\"config file change detected\")\n    })\n\n    capitan.Hook(flux.CapacitorValidationFailed, func(ctx context.Context, e *capitan.Event) {\n        errMsg, _ := flux.KeyError.From(e)\n        log.Printf(\"config validation failed: %s\", errMsg)\n    })\n\n    capitan.Hook(flux.CapacitorTransformFailed, func(ctx context.Context, e *capitan.Event) {\n        errMsg, _ := flux.KeyError.From(e)\n        log.Printf(\"config parse failed: %s\", errMsg)\n    })\n\n    capitan.Hook(flux.CapacitorApplyFailed, func(ctx context.Context, e *capitan.Event) {\n        errMsg, _ := flux.KeyError.From(e)\n        log.Printf(\"config apply failed: %s\", errMsg)\n    })\n}\n\nfunc logState[T any](c *flux.Capacitor[T]) {\n    switch c.State() {\n    case flux.StateHealthy:\n        log.Println(\"config status: HEALTHY\")\n    case flux.StateDegraded:\n        log.Printf(\"config status: DEGRADED (error: %v)\", c.LastError())\n    case flux.StateEmpty:\n        log.Printf(\"config status: EMPTY (error: %v)\", c.LastError())\n    case flux.StateLoading:\n        log.Println(\"config status: LOADING\")\n    }\n}\n\nfunc startServer(app *Application) *http.Server {\n    mux := http.NewServeMux()\n\n    mux.HandleFunc(\"/health\", func(w http.ResponseWriter, r *http.Request) {\n        state := app.capacitor.State()\n        response := map[string]interface{}{\n            \"status\": state.String(),\n        }\n\n        if state == flux.StateHealthy || state == flux.StateDegraded {\n            w.WriteHeader(http.StatusOK)\n        } else {\n            w.WriteHeader(http.StatusServiceUnavailable)\n        }\n\n        if err := app.capacitor.LastError(); err != nil {\n            response[\"error\"] = err.Error()\n        }\n\n        json.NewEncoder(w).Encode(response)\n    })\n\n    mux.HandleFunc(\"/config\", func(w http.ResponseWriter, r *http.Request) {\n        cfg := app.Config()\n        json.NewEncoder(w).Encode(cfg)\n    })\n\n    cfg := app.Config()\n    server := &http.Server{\n        Addr:         fmt.Sprintf(\":%d\", cfg.Server.Port),\n        Handler:      mux,\n        ReadTimeout:  cfg.Server.ReadTimeout,\n        WriteTimeout: cfg.Server.WriteTimeout,\n    }\n\n    go func() {\n        log.Printf(\"server listening on :%d\", cfg.Server.Port)\n        if err := server.ListenAndServe(); err != http.ErrServerClosed {\n            log.Printf(\"server error: %v\", err)\n        }\n    }()\n\n    return server\n}",{"id":654,"title":655,"titles":656,"content":657,"level":19},"/v1.0.2/cookbook/file-config#running","Running",[622],"go run .\n# Output:\n# starting configuration watcher...\n# config file change detected\n# config updated: port=8080, max_conn=100, debug=false\n# config state: loading -> healthy\n# config status: HEALTHY\n# server listening on :8080",{"id":659,"title":660,"titles":661,"content":662,"level":19},"/v1.0.2/cookbook/file-config#testing-changes","Testing Changes",[622],"# Valid change\nsed -i 's/port: 8080/port: 9090/' config.yaml\n# Output:\n# config file change detected\n# config updated: port=9090, max_conn=100, debug=false\n\n# Invalid change\nsed -i 's/port: 9090/port: 99999/' config.yaml\n# Output:\n# config file change detected\n# config validation failed: ...\n# config state: healthy -> degraded\n\n# Recovery\nsed -i 's/port: 99999/port: 8080/' config.yaml\n# Output:\n# config file change detected\n# config updated: port=8080, max_conn=100, debug=false\n# config state: degraded -> healthy",{"id":664,"title":665,"titles":666,"content":667,"level":19},"/v1.0.2/cookbook/file-config#unit-testing","Unit Testing",[622],"func TestConfigReload(t *testing.T) {\n    ch := make(chan []byte, 2)\n\n    ch \u003C- []byte(`\nserver:\n  port: 8080\n  read_timeout: 30s\n  write_timeout: 30s\ndatabase:\n  url: postgres://localhost/test\n  max_connections: 10\n  idle_connections: 2\nfeatures:\n  enable_metrics: false\n`)\n\n    ch \u003C- []byte(`\nserver:\n  port: 9090\n  read_timeout: 60s\n  write_timeout: 60s\ndatabase:\n  url: postgres://localhost/test\n  max_connections: 20\n  idle_connections: 5\nfeatures:\n  enable_metrics: true\n`)\n\n    var applied Config\n\n    capacitor := flux.New[Config](\n        flux.NewSyncChannelWatcher(ch),\n        func(ctx context.Context, prev, curr Config) error {\n            applied = curr\n            return nil\n        },\n    ).Codec(flux.YAMLCodec{}).SyncMode()\n\n    ctx := context.Background()\n\n    err := capacitor.Start(ctx)\n    require.NoError(t, err)\n    assert.Equal(t, 8080, applied.Server.Port)\n\n    capacitor.Process(ctx)\n    assert.Equal(t, 9090, applied.Server.Port)\n    assert.True(t, applied.Features.EnableMetrics)\n}",{"id":669,"title":47,"titles":670,"content":671,"level":19},"/v1.0.2/cookbook/file-config#next-steps",[622],"Multi-Source - Combine multiple config sourcesCustom Watcher - Build your own provider html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"id":673,"title":674,"titles":675,"content":676,"level":9},"/v1.0.2/cookbook/multi-source","Multi-Source Configuration",[],"Combine multiple configuration sources with Compose",{"id":678,"title":674,"titles":679,"content":680,"level":9},"/v1.0.2/cookbook/multi-source#multi-source-configuration",[],"Recipe: Combine multiple configuration sources with priority-based merging.",{"id":682,"title":631,"titles":683,"content":684,"level":19},"/v1.0.2/cookbook/multi-source#the-scenario",[674],"An application needs configuration from multiple sources: Defaults - Hardcoded fallback valuesFile - Local config fileRemote - Consul/etcd for dynamic overrides Later sources override earlier ones.",{"id":686,"title":687,"titles":688,"content":689,"level":19},"/v1.0.2/cookbook/multi-source#the-pattern","The Pattern",[674],"┌──────────┐   ┌──────────┐   ┌──────────┐\n│ Defaults │   │   File   │   │  Remote  │\n└────┬─────┘   └────┬─────┘   └────┬─────┘\n     │              │              │\n     ▼              ▼              ▼\n   [T]            [T]            [T]\n     │              │              │\n     └──────────────┼──────────────┘\n                    │\n                    ▼\n              ┌──────────┐\n              │  Reducer │\n              └────┬─────┘\n                   │\n                   ▼\n                 [T]",{"id":691,"title":692,"titles":693,"content":694,"level":19},"/v1.0.2/cookbook/multi-source#using-compose","Using Compose",[674],"type Config struct {\n    Port        int           `json:\"port\"`\n    Timeout     time.Duration `json:\"timeout\"`\n    MaxRetries  int           `json:\"max_retries\"`\n    FeatureFlag bool          `json:\"feature_flag\"`\n}\n\nfunc (c Config) Validate() error {\n    if c.Port \u003C 1 || c.Port > 65535 {\n        return errors.New(\"port must be between 1 and 65535\")\n    }\n    if c.MaxRetries \u003C 0 {\n        return errors.New(\"max_retries must be non-negative\")\n    }\n    return nil\n}\n\ncapacitor := flux.Compose[Config](\n    // Reducer: merge sources with priority\n    func(ctx context.Context, prev, curr []Config) (Config, error) {\n        // curr[0] = defaults, curr[1] = file, curr[2] = remote\n        merged := curr[0]\n\n        // File overrides defaults\n        if curr[1].Port != 0 {\n            merged.Port = curr[1].Port\n        }\n        if curr[1].Timeout != 0 {\n            merged.Timeout = curr[1].Timeout\n        }\n        if curr[1].MaxRetries != 0 {\n            merged.MaxRetries = curr[1].MaxRetries\n        }\n\n        // Remote overrides file\n        if curr[2].Port != 0 {\n            merged.Port = curr[2].Port\n        }\n        if curr[2].FeatureFlag {\n            merged.FeatureFlag = curr[2].FeatureFlag\n        }\n\n        return merged, nil\n    },\n    // Sources in priority order (lowest to highest)\n    []flux.Watcher{\n        defaultsWatcher,\n        file.New(\"config.json\"),\n        consul.New(client, \"myapp/config\"),\n    },\n)",{"id":696,"title":697,"titles":698,"content":699,"level":19},"/v1.0.2/cookbook/multi-source#complete-example","Complete Example",[674],"package main\n\nimport (\n    \"context\"\n    \"log\"\n    \"os\"\n    \"os/signal\"\n    \"time\"\n\n    \"github.com/hashicorp/consul/api\"\n    \"github.com/zoobz-io/flux\"\n    \"github.com/zoobz-io/flux/consul\"\n    \"github.com/zoobz-io/flux/file\"\n)\n\ntype Config struct {\n    Port        int           `json:\"port\"`\n    Timeout     time.Duration `json:\"timeout\"`\n    MaxRetries  int           `json:\"max_retries\"`\n    FeatureFlag bool          `json:\"feature_flag\"`\n}\n\nfunc (c Config) Validate() error {\n    if c.Port \u003C 1 || c.Port > 65535 {\n        return errors.New(\"port must be between 1 and 65535\")\n    }\n    if c.Timeout \u003C time.Second {\n        return errors.New(\"timeout must be at least 1s\")\n    }\n    if c.MaxRetries \u003C 0 || c.MaxRetries > 10 {\n        return errors.New(\"max_retries must be between 0 and 10\")\n    }\n    return nil\n}\n\nfunc main() {\n    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)\n    defer cancel()\n\n    // Consul client\n    consulClient, _ := api.NewClient(api.DefaultConfig())\n\n    // Defaults via channel watcher\n    defaultsCh := make(chan []byte, 1)\n    defaultsCh \u003C- []byte(`{\n        \"port\": 8080,\n        \"timeout\": \"30s\",\n        \"max_retries\": 3,\n        \"feature_flag\": false\n    }`)\n\n    capacitor := flux.Compose[Config](\n        mergeConfigs,\n        []flux.Watcher{\n            flux.NewChannelWatcher(defaultsCh),\n            file.New(\"config.json\"),\n            consul.New(consulClient, \"myapp/config\"),\n        },\n    )\n\n    if err := capacitor.Start(ctx); err != nil {\n        log.Fatalf(\"failed to start: %v\", err)\n    }\n\n    cfg, _ := capacitor.Current()\n    log.Printf(\"merged config: port=%d, timeout=%s, feature=%v\",\n        cfg.Port, cfg.Timeout, cfg.FeatureFlag)\n\n    \u003C-ctx.Done()\n}\n\nfunc mergeConfigs(ctx context.Context, prev, curr []Config) (Config, error) {\n    // Start with defaults\n    merged := curr[0]\n\n    // Apply file config (non-zero values only)\n    fileConfig := curr[1]\n    if fileConfig.Port != 0 {\n        merged.Port = fileConfig.Port\n    }\n    if fileConfig.Timeout != 0 {\n        merged.Timeout = fileConfig.Timeout\n    }\n    if fileConfig.MaxRetries != 0 {\n        merged.MaxRetries = fileConfig.MaxRetries\n    }\n\n    // Apply remote config (highest priority)\n    remoteConfig := curr[2]\n    if remoteConfig.Port != 0 {\n        merged.Port = remoteConfig.Port\n    }\n    // Feature flags always from remote\n    merged.FeatureFlag = remoteConfig.FeatureFlag\n\n    return merged, nil\n}",{"id":701,"title":702,"titles":703,"content":704,"level":19},"/v1.0.2/cookbook/multi-source#detecting-changes","Detecting Changes",[674],"The reducer receives both previous and current slices: func mergeConfigs(ctx context.Context, prev, curr []Config) (Config, error) {\n    merged := merge(curr...)\n\n    // prev is nil on initial load\n    if prev != nil {\n        // Check what changed\n        if prev[2].FeatureFlag != curr[2].FeatureFlag {\n            log.Printf(\"feature flag changed via remote\")\n        }\n    }\n\n    return merged, nil\n}",{"id":706,"title":707,"titles":708,"content":709,"level":19},"/v1.0.2/cookbook/multi-source#handling-source-errors","Handling Source Errors",[674],"Individual source failures don't block other sources: // Check which sources had errors\nerrs := capacitor.SourceErrors()\nfor _, se := range errs {\n    log.Printf(\"source %d error: %v\", se.Index, se.Error)\n}\n\n// Overall state\nswitch capacitor.State() {\ncase flux.StateHealthy:\n    // All sources valid\ncase flux.StateDegraded:\n    // Some source failed, using previous merged config\ncase flux.StateEmpty:\n    // Never got valid config from all sources\n}",{"id":711,"title":712,"titles":713,"content":714,"level":19},"/v1.0.2/cookbook/multi-source#partial-source-updates","Partial Source Updates",[674],"Each source updates independently. When any source changes, all sources are re-merged: Time 0:  defaults=✓  file=✓  remote=✓  → merge all → Config\nTime 1:  defaults=✓  file=✓  remote=✓* → merge all → Config (remote changed)\nTime 2:  defaults=✓  file=✓* remote=✓  → merge all → Config (file changed)",{"id":716,"title":292,"titles":717,"content":718,"level":19},"/v1.0.2/cookbook/multi-source#testing",[674],"func TestCompose(t *testing.T) {\n    defaultsCh := make(chan []byte, 1)\n    fileCh := make(chan []byte, 1)\n    remoteCh := make(chan []byte, 1)\n\n    defaultsCh \u003C- []byte(`{\"port\": 8080}`)\n    fileCh \u003C- []byte(`{\"port\": 9090}`)\n    remoteCh \u003C- []byte(`{\"feature_flag\": true}`)\n\n    var merged Config\n\n    capacitor := flux.Compose[Config](\n        func(ctx context.Context, prev, curr []Config) (Config, error) {\n            m := curr[0]\n            if curr[1].Port != 0 {\n                m.Port = curr[1].Port\n            }\n            m.FeatureFlag = curr[2].FeatureFlag\n            return m, nil\n        },\n        []flux.Watcher{\n            flux.NewSyncChannelWatcher(defaultsCh),\n            flux.NewSyncChannelWatcher(fileCh),\n            flux.NewSyncChannelWatcher(remoteCh),\n        },\n    ).SyncMode()\n\n    err := capacitor.Start(context.Background())\n    require.NoError(t, err)\n\n    merged, _ = capacitor.Current()\n    assert.Equal(t, 9090, merged.Port)         // From file\n    assert.True(t, merged.FeatureFlag)         // From remote\n}",{"id":720,"title":721,"titles":722,"content":723,"level":19},"/v1.0.2/cookbook/multi-source#use-cases","Use Cases",[674],"PatternSourcesPriorityDefaults + FileChannel, FileFile winsLocal + RemoteFile, Consul/etcdRemote winsEnvironment LayersDev, Staging, ProdLater winsFeature FlagsBase, OverridesOverrides win",{"id":725,"title":47,"titles":726,"content":727,"level":19},"/v1.0.2/cookbook/multi-source#next-steps",[674],"Custom Watcher - Build your own sourceProviders Guide - All available providers html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}",{"id":729,"title":730,"titles":731,"content":732,"level":9},"/v1.0.2/cookbook/custom-watcher","Custom Watcher",[],"Build your own configuration source watcher",{"id":734,"title":730,"titles":735,"content":736,"level":9},"/v1.0.2/cookbook/custom-watcher#custom-watcher",[],"Recipe: Build a custom watcher for configuration sources not covered by built-in providers.",{"id":738,"title":739,"titles":740,"content":741,"level":19},"/v1.0.2/cookbook/custom-watcher#the-interface","The Interface",[730],"type Watcher interface {\n    Watch(ctx context.Context) (\u003C-chan []byte, error)\n}",{"id":743,"title":61,"titles":744,"content":745,"level":19},"/v1.0.2/cookbook/custom-watcher#requirements",[730],"Emit current value immediately - First emission enables initial loadEmit on change - Subsequent emissions trigger the pipelineClose channel on shutdown - When context is cancelledReturn error if watching cannot start - Invalid config, connection failure",{"id":747,"title":748,"titles":749,"content":750,"level":19},"/v1.0.2/cookbook/custom-watcher#example-http-polling-watcher","Example: HTTP Polling Watcher",[730],"Poll an HTTP endpoint at intervals: package httpconfig\n\nimport (\n    \"bytes\"\n    \"context\"\n    \"fmt\"\n    \"io\"\n    \"net/http\"\n    \"time\"\n)\n\ntype Watcher struct {\n    url      string\n    interval time.Duration\n    client   *http.Client\n}\n\nfunc New(url string, interval time.Duration) *Watcher {\n    return &Watcher{\n        url:      url,\n        interval: interval,\n        client:   &http.Client{Timeout: 10 * time.Second},\n    }\n}\n\nfunc (w *Watcher) Watch(ctx context.Context) (\u003C-chan []byte, error) {\n    // Fetch initial value to validate endpoint\n    initial, err := w.fetch(ctx)\n    if err != nil {\n        return nil, fmt.Errorf(\"initial fetch failed: %w\", err)\n    }\n\n    out := make(chan []byte)\n\n    go func() {\n        defer close(out)\n\n        // Emit initial value\n        select {\n        case out \u003C- initial:\n        case \u003C-ctx.Done():\n            return\n        }\n\n        current := initial\n        ticker := time.NewTicker(w.interval)\n        defer ticker.Stop()\n\n        for {\n            select {\n            case \u003C-ctx.Done():\n                return\n            case \u003C-ticker.C:\n                next, err := w.fetch(ctx)\n                if err != nil {\n                    continue // Skip failed fetches\n                }\n                if !bytes.Equal(current, next) {\n                    current = next\n                    select {\n                    case out \u003C- current:\n                    case \u003C-ctx.Done():\n                        return\n                    }\n                }\n            }\n        }\n    }()\n\n    return out, nil\n}\n\nfunc (w *Watcher) fetch(ctx context.Context) ([]byte, error) {\n    req, err := http.NewRequestWithContext(ctx, \"GET\", w.url, nil)\n    if err != nil {\n        return nil, err\n    }\n\n    resp, err := w.client.Do(req)\n    if err != nil {\n        return nil, err\n    }\n    defer resp.Body.Close()\n\n    if resp.StatusCode != http.StatusOK {\n        return nil, fmt.Errorf(\"unexpected status: %d\", resp.StatusCode)\n    }\n\n    return io.ReadAll(resp.Body)\n}",{"id":752,"title":753,"titles":754,"content":755,"level":131},"/v1.0.2/cookbook/custom-watcher#usage","Usage",[730,748],"type Config struct {\n    Port int `json:\"port\"`\n}\n\nfunc (c Config) Validate() error {\n    if c.Port \u003C 1 || c.Port > 65535 {\n        return errors.New(\"port must be between 1 and 65535\")\n    }\n    return nil\n}\n\ncapacitor := flux.New[Config](\n    httpconfig.New(\"https://config.example.com/app.json\", 30*time.Second),\n    func(ctx context.Context, prev, curr Config) error {\n        return app.Apply(curr)\n    },\n)",{"id":757,"title":758,"titles":759,"content":760,"level":19},"/v1.0.2/cookbook/custom-watcher#example-aws-parameter-store-watcher","Example: AWS Parameter Store Watcher",[730],"Watch AWS SSM Parameter Store: package ssm\n\nimport (\n    \"context\"\n    \"time\"\n\n    \"github.com/aws/aws-sdk-go-v2/service/ssm\"\n)\n\ntype Watcher struct {\n    client   *ssm.Client\n    name     string\n    interval time.Duration\n}\n\nfunc New(client *ssm.Client, name string, interval time.Duration) *Watcher {\n    return &Watcher{\n        client:   client,\n        name:     name,\n        interval: interval,\n    }\n}\n\nfunc (w *Watcher) Watch(ctx context.Context) (\u003C-chan []byte, error) {\n    initial, err := w.fetch(ctx)\n    if err != nil {\n        return nil, err\n    }\n\n    out := make(chan []byte)\n\n    go func() {\n        defer close(out)\n\n        select {\n        case out \u003C- initial:\n        case \u003C-ctx.Done():\n            return\n        }\n\n        current := string(initial)\n        ticker := time.NewTicker(w.interval)\n        defer ticker.Stop()\n\n        for {\n            select {\n            case \u003C-ctx.Done():\n                return\n            case \u003C-ticker.C:\n                next, err := w.fetch(ctx)\n                if err != nil {\n                    continue\n                }\n                if string(next) != current {\n                    current = string(next)\n                    select {\n                    case out \u003C- next:\n                    case \u003C-ctx.Done():\n                        return\n                    }\n                }\n            }\n        }\n    }()\n\n    return out, nil\n}\n\nfunc (w *Watcher) fetch(ctx context.Context) ([]byte, error) {\n    out, err := w.client.GetParameter(ctx, &ssm.GetParameterInput{\n        Name:           &w.name,\n        WithDecryption: aws.Bool(true),\n    })\n    if err != nil {\n        return nil, err\n    }\n    return []byte(*out.Parameter.Value), nil\n}",{"id":762,"title":763,"titles":764,"content":765,"level":19},"/v1.0.2/cookbook/custom-watcher#example-grpc-streaming-watcher","Example: gRPC Streaming Watcher",[730],"Watch a gRPC streaming endpoint: package grpcconfig\n\nimport (\n    \"context\"\n\n    pb \"myapp/proto/config\"\n)\n\ntype Watcher struct {\n    client pb.ConfigServiceClient\n    key    string\n}\n\nfunc New(client pb.ConfigServiceClient, key string) *Watcher {\n    return &Watcher{client: client, key: key}\n}\n\nfunc (w *Watcher) Watch(ctx context.Context) (\u003C-chan []byte, error) {\n    stream, err := w.client.WatchConfig(ctx, &pb.WatchRequest{Key: w.key})\n    if err != nil {\n        return nil, err\n    }\n\n    out := make(chan []byte)\n\n    go func() {\n        defer close(out)\n\n        for {\n            resp, err := stream.Recv()\n            if err != nil {\n                return\n            }\n\n            select {\n            case out \u003C- resp.Value:\n            case \u003C-ctx.Done():\n                return\n            }\n        }\n    }()\n\n    return out, nil\n}",{"id":767,"title":768,"titles":769,"content":58,"level":19},"/v1.0.2/cookbook/custom-watcher#common-patterns","Common Patterns",[730],{"id":771,"title":772,"titles":773,"content":774,"level":131},"/v1.0.2/cookbook/custom-watcher#deduplication","Deduplication",[730,768],"Only emit when value changes: if !bytes.Equal(current, next) {\n    current = next\n    out \u003C- current\n}",{"id":776,"title":777,"titles":778,"content":779,"level":131},"/v1.0.2/cookbook/custom-watcher#backoff-on-error","Backoff on Error",[730,768],"Don't spam a failing source: backoff := time.Second\nmaxBackoff := time.Minute\n\nfor {\n    data, err := fetch(ctx)\n    if err != nil {\n        time.Sleep(backoff)\n        backoff = min(backoff*2, maxBackoff)\n        continue\n    }\n    backoff = time.Second  // Reset on success\n    // ...\n}",{"id":781,"title":782,"titles":783,"content":784,"level":131},"/v1.0.2/cookbook/custom-watcher#graceful-send","Graceful Send",[730,768],"Always respect context during channel sends: select {\ncase out \u003C- value:\ncase \u003C-ctx.Done():\n    return\n}",{"id":786,"title":787,"titles":788,"content":789,"level":131},"/v1.0.2/cookbook/custom-watcher#options-pattern","Options Pattern",[730,768],"type Option func(*Watcher)\n\nfunc WithInterval(d time.Duration) Option {\n    return func(w *Watcher) {\n        w.interval = d\n    }\n}\n\nfunc WithClient(c *http.Client) Option {\n    return func(w *Watcher) {\n        w.client = c\n    }\n}\n\nfunc New(url string, opts ...Option) *Watcher {\n    w := &Watcher{\n        url:      url,\n        interval: 30 * time.Second,\n        client:   http.DefaultClient,\n    }\n    for _, opt := range opts {\n        opt(w)\n    }\n    return w\n}",{"id":791,"title":792,"titles":793,"content":794,"level":19},"/v1.0.2/cookbook/custom-watcher#testing-custom-watchers","Testing Custom Watchers",[730],"func TestHTTPWatcher(t *testing.T) {\n    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        w.Write([]byte(`{\"port\": 8080}`))\n    }))\n    defer server.Close()\n\n    watcher := httpconfig.New(server.URL, 10*time.Millisecond)\n\n    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)\n    defer cancel()\n\n    ch, err := watcher.Watch(ctx)\n    require.NoError(t, err)\n\n    // Should receive initial value\n    val := \u003C-ch\n    assert.Equal(t, `{\"port\": 8080}`, string(val))\n}",{"id":796,"title":797,"titles":798,"content":799,"level":19},"/v1.0.2/cookbook/custom-watcher#contributing-a-provider","Contributing a Provider",[730],"If your watcher is generally useful, consider contributing it to pkg/: Create pkg/yourprovider/Add go.mod with dependenciesImplement Watcher interfaceAdd tests using testcontainers if applicableAdd README.md with usage examplesUpdate go.work to include new module",{"id":801,"title":47,"titles":802,"content":803,"level":19},"/v1.0.2/cookbook/custom-watcher#next-steps",[730],"Providers Guide - Existing providers for referenceArchitecture - How watchers integrate with Capacitor html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"id":805,"title":806,"titles":807,"content":808,"level":9},"/v1.0.2/reference/api","API Reference",[],"Complete API reference for the flux package",{"id":810,"title":806,"titles":811,"content":812,"level":9},"/v1.0.2/reference/api#api-reference",[],"Complete API reference for github.com/zoobz-io/flux.",{"id":814,"title":815,"titles":816,"content":58,"level":19},"/v1.0.2/reference/api#types","Types",[806],{"id":818,"title":139,"titles":819,"content":820,"level":131},"/v1.0.2/reference/api#watcher",[806,815],"Interface for configuration sources. type Watcher interface {\n    Watch(ctx context.Context) (\u003C-chan []byte, error)\n} Implementations must emit the current value immediately upon Watch() being called, then emit subsequent values when the source changes. The channel closes when context is cancelled.",{"id":822,"title":823,"titles":824,"content":825,"level":131},"/v1.0.2/reference/api#request","Request",[806,815],"Carries configuration data through the processing pipeline. type Request[T Validator] struct {\n    Previous T      // Last successfully applied configuration\n    Current  T      // Newly parsed and validated configuration\n    Raw      []byte // Original bytes from watcher\n}",{"id":827,"title":828,"titles":829,"content":830,"level":131},"/v1.0.2/reference/api#reducer","Reducer",[806,815],"Merges multiple configuration sources into a single configuration. type Reducer[T Validator] func(ctx context.Context, prev, curr []T) (T, error) Receives the previous merged values (nil on first call) and the current parsed values from each source in the same order as the sources were provided.",{"id":832,"title":123,"titles":833,"content":834,"level":131},"/v1.0.2/reference/api#capacitor",[806,815],"Watches a single source for configuration changes. type Capacitor[T Validator] struct {\n    // unexported fields\n}",{"id":836,"title":837,"titles":838,"content":839,"level":840},"/v1.0.2/reference/api#new","New",[806,815,123],"func New[T Validator](\n    watcher Watcher,\n    fn func(ctx context.Context, prev, curr T) error,\n    opts ...Option[T],\n) *Capacitor[T] Creates a Capacitor that watches a source for configuration changes. The type T must implement the Validator interface. The watcher emits raw bytes, which are unmarshaled to type T using the configured codec (JSON by default). Validation is performed by calling T.Validate(). On success, the callback is invoked with the previous and current configuration values.",4,{"id":842,"title":843,"titles":844,"content":845,"level":840},"/v1.0.2/reference/api#start","Start",[806,815,123],"func (c *Capacitor[T]) Start(ctx context.Context) error Begins watching. Blocks until the first value is processed, then continues asynchronously. Returns error if initial load fails (but continues watching). Can only be called once.",{"id":847,"title":159,"titles":848,"content":849,"level":840},"/v1.0.2/reference/api#state",[806,815,123],"func (c *Capacitor[T]) State() State Returns the current state: StateLoading, StateHealthy, StateDegraded, or StateEmpty.",{"id":851,"title":852,"titles":853,"content":854,"level":840},"/v1.0.2/reference/api#current","Current",[806,815,123],"func (c *Capacitor[T]) Current() (T, bool) Returns the current valid configuration and true, or zero value and false if none exists.",{"id":856,"title":857,"titles":858,"content":859,"level":840},"/v1.0.2/reference/api#lasterror","LastError",[806,815,123],"func (c *Capacitor[T]) LastError() error Returns the last error, or nil if no error occurred.",{"id":861,"title":862,"titles":863,"content":864,"level":840},"/v1.0.2/reference/api#errorhistory","ErrorHistory",[806,815,123],"func (c *Capacitor[T]) ErrorHistory() []error Returns recent errors (oldest first) if error history is enabled via ErrorHistorySize. Returns nil if not enabled.",{"id":866,"title":867,"titles":868,"content":869,"level":840},"/v1.0.2/reference/api#process","Process",[806,815,123],"func (c *Capacitor[T]) Process(ctx context.Context) bool Manually processes the next value. Only available in sync mode. Returns true if a value was processed.",{"id":871,"title":872,"titles":873,"content":874,"level":131},"/v1.0.2/reference/api#compositecapacitor","CompositeCapacitor",[806,815],"Watches multiple sources and merges their configurations. type CompositeCapacitor[T Validator] struct {\n    // unexported fields\n}",{"id":876,"title":877,"titles":878,"content":879,"level":840},"/v1.0.2/reference/api#compose","Compose",[806,815,872],"func Compose[T Validator](\n    reducer Reducer[T],\n    sources []Watcher,\n    opts ...Option[T],\n) *CompositeCapacitor[T] Creates a CompositeCapacitor. The type T must implement the Validator interface. Each source emits raw bytes, unmarshaled and validated to type T. The reducer receives previous and current slices (in source order) and returns the merged configuration.",{"id":881,"title":882,"titles":883,"content":884,"level":840},"/v1.0.2/reference/api#methods","Methods",[806,815,872],"CompositeCapacitor has the same methods as Capacitor: Start, State, Current, LastError, ErrorHistory, Process. Additional method: func (c *CompositeCapacitor[T]) SourceErrors() []SourceError Returns errors from individual sources for granular diagnostics.",{"id":886,"title":159,"titles":887,"content":888,"level":131},"/v1.0.2/reference/api#state-1",[806,815],"Configuration health state. type State int\n\nconst (\n    StateLoading  State = iota  // Waiting for first value\n    StateHealthy                // Valid config active\n    StateDegraded               // Update failed, previous retained\n    StateEmpty                  // No valid config ever obtained\n)",{"id":890,"title":891,"titles":892,"content":893,"level":840},"/v1.0.2/reference/api#string","String",[806,815,159],"func (s State) String() string Returns string representation: \"loading\", \"healthy\", \"degraded\", or \"empty\".",{"id":895,"title":174,"titles":896,"content":897,"level":131},"/v1.0.2/reference/api#codec",[806,815],"Interface for configuration deserialization. type Codec interface {\n    Unmarshal(data []byte, v any) error\n    ContentType() string\n}",{"id":899,"title":900,"titles":901,"content":902,"level":131},"/v1.0.2/reference/api#jsoncodec","JSONCodec",[806,815],"JSON deserialization codec (default). type JSONCodec struct{}\n\nfunc (JSONCodec) Unmarshal(data []byte, v any) error",{"id":904,"title":905,"titles":906,"content":907,"level":131},"/v1.0.2/reference/api#yamlcodec","YAMLCodec",[806,815],"YAML deserialization codec. type YAMLCodec struct{}\n\nfunc (YAMLCodec) Unmarshal(data []byte, v any) error",{"id":909,"title":910,"titles":911,"content":912,"level":131},"/v1.0.2/reference/api#sourceerror","SourceError",[806,815],"Error from a specific source in CompositeCapacitor. type SourceError struct {\n    Index int\n    Error error\n}",{"id":914,"title":915,"titles":916,"content":917,"level":19},"/v1.0.2/reference/api#instance-configuration","Instance Configuration",[806],"Instance configuration methods are chainable and configure the Capacitor or CompositeCapacitor behavior. These methods can be called on the returned instance after calling New() or Compose().",{"id":919,"title":920,"titles":921,"content":922,"level":131},"/v1.0.2/reference/api#debounce","Debounce",[806,915],"func (c *Capacitor[T]) Debounce(d time.Duration) *Capacitor[T] Sets debounce duration. Changes within this duration are coalesced. Default: 100ms.",{"id":924,"title":174,"titles":925,"content":926,"level":131},"/v1.0.2/reference/api#codec-1",[806,915],"func (c *Capacitor[T]) Codec(codec Codec) *Capacitor[T] Sets the codec for deserialization. Default: JSONCodec{}.",{"id":928,"title":929,"titles":930,"content":931,"level":131},"/v1.0.2/reference/api#syncmode","SyncMode",[806,915],"func (c *Capacitor[T]) SyncMode() *Capacitor[T] Enables synchronous processing for testing. Disables async goroutines and debouncing.",{"id":933,"title":934,"titles":935,"content":936,"level":131},"/v1.0.2/reference/api#clock","Clock",[806,915],"func (c *Capacitor[T]) Clock(clock clockz.Clock) *Capacitor[T] Sets a custom clock for time operations. Use with clockz.FakeClock for testing debounce behavior.",{"id":938,"title":939,"titles":940,"content":941,"level":131},"/v1.0.2/reference/api#startuptimeout","StartupTimeout",[806,915],"func (c *Capacitor[T]) StartupTimeout(d time.Duration) *Capacitor[T] Sets the maximum duration to wait for the initial configuration value from the watcher. If the watcher fails to emit within this duration, Start() returns an error. Default: no timeout (wait indefinitely).",{"id":943,"title":944,"titles":945,"content":946,"level":131},"/v1.0.2/reference/api#metrics","Metrics",[806,915],"func (c *Capacitor[T]) Metrics(provider MetricsProvider) *Capacitor[T] Sets a metrics provider for observability integration. The provider receives callbacks on state changes, processing success/failure, and change events. See MetricsProvider.",{"id":948,"title":949,"titles":950,"content":951,"level":131},"/v1.0.2/reference/api#onstop","OnStop",[806,915],"func (c *Capacitor[T]) OnStop(fn func(State)) *Capacitor[T] Sets a callback invoked when the capacitor stops watching. The callback receives the final state. Useful for graceful shutdown scenarios.",{"id":953,"title":954,"titles":955,"content":956,"level":131},"/v1.0.2/reference/api#errorhistorysize","ErrorHistorySize",[806,915],"func (c *Capacitor[T]) ErrorHistorySize(n int) *Capacitor[T] Sets the number of recent errors to retain. When set, ErrorHistory() returns up to this many recent errors (oldest first). Use 0 (default) to only track the most recent error via LastError().",{"id":958,"title":959,"titles":960,"content":961,"level":19},"/v1.0.2/reference/api#pipeline-options","Pipeline Options",[806],"Pipeline options wrap the processing pipeline with middleware at the boundary level. These are powered by pipz.",{"id":963,"title":964,"titles":965,"content":966,"level":131},"/v1.0.2/reference/api#withretry","WithRetry",[806,959],"func WithRetry[T Validator](maxAttempts int) Option[T] Wraps the pipeline with retry logic. Failed operations are retried immediately up to maxAttempts times.",{"id":968,"title":969,"titles":970,"content":971,"level":131},"/v1.0.2/reference/api#withbackoff","WithBackoff",[806,959],"func WithBackoff[T Validator](maxAttempts int, baseDelay time.Duration) Option[T] Wraps the pipeline with exponential backoff retry logic. Failed operations are retried with increasing delays: baseDelay, 2*baseDelay, 4*baseDelay, etc.",{"id":973,"title":974,"titles":975,"content":976,"level":131},"/v1.0.2/reference/api#withtimeout","WithTimeout",[806,959],"func WithTimeout[T Validator](d time.Duration) Option[T] Wraps the pipeline with a timeout. If processing takes longer than the specified duration, the operation fails with a timeout error.",{"id":978,"title":979,"titles":980,"content":981,"level":131},"/v1.0.2/reference/api#withfallback","WithFallback",[806,959],"func WithFallback[T Validator](fallbacks ...pipz.Chainable[*Request[T]]) Option[T] Wraps the pipeline with fallback processors. If the primary pipeline (including the callback) fails, each fallback is tried in order until one succeeds.",{"id":983,"title":984,"titles":985,"content":986,"level":131},"/v1.0.2/reference/api#withcircuitbreaker","WithCircuitBreaker",[806,959],"func WithCircuitBreaker[T Validator](failures int, recovery time.Duration) Option[T] Wraps the pipeline with circuit breaker protection. After failures consecutive failures, the circuit opens and rejects further requests until recovery time has passed. The circuit breaker has three states: Closed: Normal operation, requests pass throughOpen: After threshold failures, requests are rejected immediatelyHalf-Open: After recovery timeout, one request is allowed to test recovery",{"id":988,"title":989,"titles":990,"content":991,"level":131},"/v1.0.2/reference/api#witherrorhandler","WithErrorHandler",[806,959],"func WithErrorHandler[T Validator](handler pipz.Chainable[*pipz.Error[*Request[T]]]) Option[T] Adds error observation to the pipeline. Errors are passed to the handler for logging, metrics, or alerting, but the error still propagates. Use this for observability, not recovery.",{"id":993,"title":994,"titles":995,"content":996,"level":131},"/v1.0.2/reference/api#withpipeline","WithPipeline",[806,959],"func WithPipeline[T Validator](identity pipz.Identity) Option[T] Wraps the entire processing pipeline with a pipz.Pipeline for correlated tracing. Each Process() call generates a unique execution ID, while the pipeline ID remains stable (derived from the identity). Use pipz.ExecutionIDFromContext and pipz.PipelineIDFromContext in middleware or signal handlers to extract correlation IDs for observability. This option should typically be applied last (outermost) to ensure all nested processors have access to the correlation context. Example: var configPipelineID = pipz.NewIdentity(\"myapp:config\", \"Configuration pipeline\")\n\ncapacitor := flux.New[Config](\n    watcher,\n    callback,\n    flux.WithRetry[Config](3),\n    flux.WithPipeline[Config](configPipelineID),\n)",{"id":998,"title":999,"titles":1000,"content":1001,"level":131},"/v1.0.2/reference/api#withmiddleware","WithMiddleware",[806,959],"func WithMiddleware[T Validator](processors ...pipz.Chainable[*Request[T]]) Option[T] Wraps the pipeline with a sequence of processors. Processors execute in order, with the wrapped pipeline (callback) last. Use the Use* functions to create processors for common patterns, or provide custom pipz.Chainable implementations directly. Example: var (\n    logID    = pipz.NewIdentity(\"myapp:log\", \"Logs config changes\")\n    enrichID = pipz.NewIdentity(\"myapp:enrich\", \"Enriches config\")\n    markID   = pipz.NewIdentity(\"myapp:mark\", \"Marks config\")\n)\n\nflux.New[Config](\n    watcher,\n    callback,\n    flux.WithMiddleware(\n        flux.UseEffect[Config](logID, logFn),\n        flux.UseApply[Config](enrichID, enrichFn),\n        flux.UseRateLimit[Config](10, 5,\n            flux.UseTransform[Config](markID, markFn),\n        ),\n    ),\n    flux.WithCircuitBreaker[Config](5, 30*time.Second),\n)",{"id":1003,"title":1004,"titles":1005,"content":1006,"level":19},"/v1.0.2/reference/api#middleware-processors","Middleware Processors",[806],"These functions create processors for use inside WithMiddleware. They transform or observe the request as it flows through the pipeline.",{"id":1008,"title":1009,"titles":1010,"content":1011,"level":131},"/v1.0.2/reference/api#adapters","Adapters",[806,1004],"Adapters convert functions into pipz processors.",{"id":1013,"title":1014,"titles":1015,"content":1016,"level":840},"/v1.0.2/reference/api#usetransform","UseTransform",[806,1004,1009],"func UseTransform[T Validator](identity pipz.Identity, fn func(context.Context, *Request[T]) *Request[T]) pipz.Chainable[*Request[T]] Creates a processor that transforms the request. Cannot fail. Use for pure transformations that always succeed.",{"id":1018,"title":1019,"titles":1020,"content":1021,"level":840},"/v1.0.2/reference/api#useapply","UseApply",[806,1004,1009],"func UseApply[T Validator](identity pipz.Identity, fn func(context.Context, *Request[T]) (*Request[T], error)) pipz.Chainable[*Request[T]] Creates a processor that can transform the request and fail. Use for operations like enrichment, validation, or transformation that may produce errors.",{"id":1023,"title":1024,"titles":1025,"content":1026,"level":840},"/v1.0.2/reference/api#useeffect","UseEffect",[806,1004,1009],"func UseEffect[T Validator](identity pipz.Identity, fn func(context.Context, *Request[T]) error) pipz.Chainable[*Request[T]] Creates a processor that performs a side effect. The request passes through unchanged. Use for logging, metrics, or notifications.",{"id":1028,"title":1029,"titles":1030,"content":1031,"level":840},"/v1.0.2/reference/api#usemutate","UseMutate",[806,1004,1009],"func UseMutate[T Validator](identity pipz.Identity, transformer func(context.Context, *Request[T]) *Request[T], condition func(context.Context, *Request[T]) bool) pipz.Chainable[*Request[T]] Creates a processor that conditionally transforms the request. The transformer is only applied if the condition returns true.",{"id":1033,"title":1034,"titles":1035,"content":1036,"level":840},"/v1.0.2/reference/api#useenrich","UseEnrich",[806,1004,1009],"func UseEnrich[T Validator](identity pipz.Identity, fn func(context.Context, *Request[T]) (*Request[T], error)) pipz.Chainable[*Request[T]] Creates a processor that attempts optional enhancement. If the enrichment fails, the error is logged but processing continues with the original request. Use for non-critical enhancements.",{"id":1038,"title":1039,"titles":1040,"content":1041,"level":131},"/v1.0.2/reference/api#wrapping-processors","Wrapping Processors",[806,1004],"These wrap another processor with reliability logic.",{"id":1043,"title":1044,"titles":1045,"content":1046,"level":840},"/v1.0.2/reference/api#useretry","UseRetry",[806,1004,1039],"func UseRetry[T Validator](maxAttempts int, processor pipz.Chainable[*Request[T]]) pipz.Chainable[*Request[T]] Wraps a processor with retry logic. Failed operations are retried immediately up to maxAttempts times.",{"id":1048,"title":1049,"titles":1050,"content":1051,"level":840},"/v1.0.2/reference/api#usebackoff","UseBackoff",[806,1004,1039],"func UseBackoff[T Validator](maxAttempts int, baseDelay time.Duration, processor pipz.Chainable[*Request[T]]) pipz.Chainable[*Request[T]] Wraps a processor with exponential backoff retry logic. Failed operations are retried with increasing delays.",{"id":1053,"title":1054,"titles":1055,"content":1056,"level":840},"/v1.0.2/reference/api#usetimeout","UseTimeout",[806,1004,1039],"func UseTimeout[T Validator](d time.Duration, processor pipz.Chainable[*Request[T]]) pipz.Chainable[*Request[T]] Wraps a processor with a deadline. If processing takes longer than the specified duration, the operation fails.",{"id":1058,"title":1059,"titles":1060,"content":1061,"level":840},"/v1.0.2/reference/api#usefallback","UseFallback",[806,1004,1039],"func UseFallback[T Validator](primary pipz.Chainable[*Request[T]], fallbacks ...pipz.Chainable[*Request[T]]) pipz.Chainable[*Request[T]] Wraps a processor with fallback alternatives. If the primary fails, each fallback is tried in order.",{"id":1063,"title":1064,"titles":1065,"content":1066,"level":840},"/v1.0.2/reference/api#usefilter","UseFilter",[806,1004,1039],"func UseFilter[T Validator](identity pipz.Identity, condition func(context.Context, *Request[T]) bool, processor pipz.Chainable[*Request[T]]) pipz.Chainable[*Request[T]] Wraps a processor with a condition. If the condition returns false, the request passes through unchanged.",{"id":1068,"title":1069,"titles":1070,"content":1071,"level":840},"/v1.0.2/reference/api#useratelimit","UseRateLimit",[806,1004,1039],"func UseRateLimit[T Validator](rate float64, burst int, processor pipz.Chainable[*Request[T]]) pipz.Chainable[*Request[T]] Wraps a processor with rate limiting. Uses a token bucket algorithm with the specified rate (tokens per second) and burst size. When tokens are exhausted, requests wait for availability.",{"id":1073,"title":1074,"titles":1075,"content":1076,"level":19},"/v1.0.2/reference/api#metricsprovider","MetricsProvider",[806],"Interface for metrics integration. type MetricsProvider interface {\n    OnStateChange(from, to State)\n    OnProcessSuccess(duration time.Duration)\n    OnProcessFailure(stage string, duration time.Duration)\n    OnChangeReceived()\n} OnStateChange - Called on state transitionsOnProcessSuccess - Called after successful processing with durationOnProcessFailure - Called on failure with stage name (\"unmarshal\", \"validate\", \"pipeline\", or \"reducer\" for Compose) and durationOnChangeReceived - Called when raw data arrives from watcher",{"id":1078,"title":1079,"titles":1080,"content":1081,"level":131},"/v1.0.2/reference/api#noopmetricsprovider","NoOpMetricsProvider",[806,1074],"type NoOpMetricsProvider struct{} A no-op implementation for testing or when metrics are not needed.",{"id":1083,"title":154,"titles":1084,"content":1085,"level":19},"/v1.0.2/reference/api#validator",[806],"Interface that configuration types must implement. type Validator interface {\n    Validate() error\n} Return nil for valid configuration, or an error describing the validation failure.",{"id":1087,"title":144,"titles":1088,"content":58,"level":19},"/v1.0.2/reference/api#built-in-watchers",[806],{"id":1090,"title":1091,"titles":1092,"content":1093,"level":131},"/v1.0.2/reference/api#newchannelwatcher","NewChannelWatcher",[806,144],"func NewChannelWatcher(ch \u003C-chan []byte) *ChannelWatcher Creates a watcher that forwards values from the given channel through an internal goroutine.",{"id":1095,"title":1096,"titles":1097,"content":1098,"level":131},"/v1.0.2/reference/api#newsyncchannelwatcher","NewSyncChannelWatcher",[806,144],"func NewSyncChannelWatcher(ch \u003C-chan []byte) *ChannelWatcher Creates a watcher that returns the source channel directly without an intermediate goroutine. Use with SyncMode() for deterministic testing.",{"id":1100,"title":1101,"titles":1102,"content":58,"level":19},"/v1.0.2/reference/api#constants","Constants",[806],{"id":1104,"title":1105,"titles":1106,"content":1107,"level":131},"/v1.0.2/reference/api#defaultdebounce","DefaultDebounce",[806,1101],"const DefaultDebounce = 100 * time.Millisecond Default debounce duration for change processing.",{"id":1109,"title":47,"titles":1110,"content":1111,"level":19},"/v1.0.2/reference/api#next-steps",[806],"Fields Reference - Signals and keys for observabilityProviders Reference - Provider-specific API html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sfm-E, html code.shiki .sfm-E{--shiki-default:var(--shiki-variable)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}",{"id":1113,"title":1114,"titles":1115,"content":1116,"level":9},"/v1.0.2/reference/fields","Fields Reference",[],"Signals and keys for flux observability",{"id":1118,"title":1114,"titles":1119,"content":1120,"level":9},"/v1.0.2/reference/fields#fields-reference",[],"Flux emits capitan signals at key points in the configuration lifecycle. This reference covers all signals and field keys.",{"id":1122,"title":1123,"titles":1124,"content":58,"level":19},"/v1.0.2/reference/fields#signals","Signals",[1114],{"id":1126,"title":1127,"titles":1128,"content":58,"level":131},"/v1.0.2/reference/fields#lifecycle-signals","Lifecycle Signals",[1114,1123],{"id":1130,"title":1131,"titles":1132,"content":1133,"level":840},"/v1.0.2/reference/fields#capacitorstarted","CapacitorStarted",[1114,1123,1127],"Emitted when Start() is called. var CapacitorStarted = capitan.NewSignal(\n    \"flux.capacitor.started\",\n    \"Capacitor watching started\",\n) Fields: KeyDebounce - Configured debounce duration",{"id":1135,"title":1136,"titles":1137,"content":1138,"level":840},"/v1.0.2/reference/fields#capacitorstopped","CapacitorStopped",[1114,1123,1127],"Emitted when the watch goroutine exits. var CapacitorStopped = capitan.NewSignal(\n    \"flux.capacitor.stopped\",\n    \"Capacitor watching stopped\",\n) Fields: KeyState - Final state at shutdown",{"id":1140,"title":1141,"titles":1142,"content":1143,"level":840},"/v1.0.2/reference/fields#capacitorstatechanged","CapacitorStateChanged",[1114,1123,1127],"Emitted on state transitions. var CapacitorStateChanged = capitan.NewSignal(\n    \"flux.capacitor.state.changed\",\n    \"Capacitor state transition\",\n) Fields: KeyOldState - Previous stateKeyNewState - New state",{"id":1145,"title":1146,"titles":1147,"content":58,"level":131},"/v1.0.2/reference/fields#processing-signals","Processing Signals",[1114,1123],{"id":1149,"title":1150,"titles":1151,"content":1152,"level":840},"/v1.0.2/reference/fields#capacitorchangereceived","CapacitorChangeReceived",[1114,1123,1146],"Emitted when raw data arrives from the watcher. var CapacitorChangeReceived = capitan.NewSignal(\n    \"flux.capacitor.change.received\",\n    \"Raw change received from watcher\",\n) Fields: None",{"id":1154,"title":1155,"titles":1156,"content":1157,"level":840},"/v1.0.2/reference/fields#capacitortransformfailed","CapacitorTransformFailed",[1114,1123,1146],"Emitted when codec unmarshal fails. var CapacitorTransformFailed = capitan.NewSignal(\n    \"flux.capacitor.transform.failed\",\n    \"Transform function failed\",\n) Fields: KeyError - Error message",{"id":1159,"title":1160,"titles":1161,"content":1162,"level":840},"/v1.0.2/reference/fields#capacitorvalidationfailed","CapacitorValidationFailed",[1114,1123,1146],"Emitted when struct validation fails. var CapacitorValidationFailed = capitan.NewSignal(\n    \"flux.capacitor.validation.failed\",\n    \"Validation failed\",\n) Fields: KeyError - Validation error message",{"id":1164,"title":1165,"titles":1166,"content":1167,"level":840},"/v1.0.2/reference/fields#capacitorapplyfailed","CapacitorApplyFailed",[1114,1123,1146],"Emitted when the callback returns an error. var CapacitorApplyFailed = capitan.NewSignal(\n    \"flux.capacitor.apply.failed\",\n    \"Apply function failed\",\n) Fields: KeyError - Error message",{"id":1169,"title":1170,"titles":1171,"content":1172,"level":840},"/v1.0.2/reference/fields#capacitorapplysucceeded","CapacitorApplySucceeded",[1114,1123,1146],"Emitted when configuration is successfully applied. var CapacitorApplySucceeded = capitan.NewSignal(\n    \"flux.capacitor.apply.succeeded\",\n    \"Config applied successfully\",\n) Fields: None",{"id":1174,"title":254,"titles":1175,"content":58,"level":19},"/v1.0.2/reference/fields#field-keys",[1114],{"id":1177,"title":1178,"titles":1179,"content":1180,"level":131},"/v1.0.2/reference/fields#keystate","KeyState",[1114,254],"Current state of the Capacitor. var KeyState = capitan.NewStringKey(\"state\") Type: stringValues: \"loading\", \"healthy\", \"degraded\", \"empty\"",{"id":1182,"title":1183,"titles":1184,"content":1185,"level":131},"/v1.0.2/reference/fields#keyoldstate","KeyOldState",[1114,254],"Previous state before a transition. var KeyOldState = capitan.NewStringKey(\"old_state\") Type: string",{"id":1187,"title":1188,"titles":1189,"content":1190,"level":131},"/v1.0.2/reference/fields#keynewstate","KeyNewState",[1114,254],"New state after a transition. var KeyNewState = capitan.NewStringKey(\"new_state\") Type: string",{"id":1192,"title":1193,"titles":1194,"content":1195,"level":131},"/v1.0.2/reference/fields#keyerror","KeyError",[1114,254],"Error message when an operation fails. var KeyError = capitan.NewStringKey(\"error\") Type: string",{"id":1197,"title":1198,"titles":1199,"content":1200,"level":131},"/v1.0.2/reference/fields#keydebounce","KeyDebounce",[1114,254],"Configured debounce duration. var KeyDebounce = capitan.NewDurationKey(\"debounce\") Type: time.Duration",{"id":1202,"title":1203,"titles":1204,"content":58,"level":19},"/v1.0.2/reference/fields#usage-examples","Usage Examples",[1114],{"id":1206,"title":1207,"titles":1208,"content":1209,"level":131},"/v1.0.2/reference/fields#logging-all-errors","Logging All Errors",[1114,1203],"func setupErrorLogging() {\n    handler := func(ctx context.Context, e *capitan.Event) {\n        errMsg, _ := flux.KeyError.From(e)\n        log.Printf(\"[%s] %s\", e.Signal.Name(), errMsg)\n    }\n\n    capitan.Hook(flux.CapacitorTransformFailed, handler)\n    capitan.Hook(flux.CapacitorValidationFailed, handler)\n    capitan.Hook(flux.CapacitorApplyFailed, handler)\n}",{"id":1211,"title":944,"titles":1212,"content":1213,"level":131},"/v1.0.2/reference/fields#metrics",[1114,1203],"func setupMetrics() {\n    capitan.Hook(flux.CapacitorStateChanged, func(ctx context.Context, e *capitan.Event) {\n        newState, _ := flux.KeyNewState.From(e)\n        metrics.Gauge(\"config_state\", stateToNumber(newState))\n    })\n\n    capitan.Hook(flux.CapacitorApplySucceeded, func(ctx context.Context, e *capitan.Event) {\n        metrics.Increment(\"config_applies_total\")\n    })\n\n    capitan.Hook(flux.CapacitorApplyFailed, func(ctx context.Context, e *capitan.Event) {\n        metrics.Increment(\"config_apply_failures_total\")\n    })\n}",{"id":1215,"title":1216,"titles":1217,"content":1218,"level":131},"/v1.0.2/reference/fields#state-transition-tracking","State Transition Tracking",[1114,1203],"capitan.Hook(flux.CapacitorStateChanged, func(ctx context.Context, e *capitan.Event) {\n    old, _ := flux.KeyOldState.From(e)\n    new, _ := flux.KeyNewState.From(e)\n\n    log.Printf(\"state transition: %s -> %s\", old, new)\n\n    if new == flux.StateDegraded.String() {\n        alerting.Send(\"config update failed\")\n    }\n\n    if old == flux.StateDegraded.String() && new == flux.StateHealthy.String() {\n        alerting.Resolve(\"config update failed\")\n    }\n})",{"id":1220,"title":1221,"titles":1222,"content":1223,"level":131},"/v1.0.2/reference/fields#change-detection","Change Detection",[1114,1203],"capitan.Hook(flux.CapacitorChangeReceived, func(ctx context.Context, e *capitan.Event) {\n    log.Println(\"configuration change detected\")\n    metrics.Increment(\"config_changes_received_total\")\n})",{"id":1225,"title":1226,"titles":1227,"content":1228,"level":19},"/v1.0.2/reference/fields#signal-flow","Signal Flow",[1114],"Typical signal sequence for a successful update: CapacitorChangeReceived\n    ↓\nCapacitorApplySucceeded\n    ↓\nCapacitorStateChanged (if state changed) For a failed update: CapacitorChangeReceived\n    ↓\nCapacitorTransformFailed / CapacitorValidationFailed / CapacitorApplyFailed\n    ↓\nCapacitorStateChanged (healthy -> degraded, or loading -> empty)",{"id":1230,"title":47,"titles":1231,"content":1232,"level":19},"/v1.0.2/reference/fields#next-steps",[1114],"API Reference - Core types and functionsProviders Reference - Provider-specific details html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}",{"id":1234,"title":1235,"titles":1236,"content":1237,"level":9},"/v1.0.2/reference/providers","Providers Reference",[],"API reference for all flux provider packages",{"id":1239,"title":1235,"titles":1240,"content":1241,"level":9},"/v1.0.2/reference/providers#providers-reference",[],"API reference for all provider packages in github.com/zoobz-io/flux/.",{"id":1243,"title":1244,"titles":1245,"content":1246,"level":19},"/v1.0.2/reference/providers#pkgfile","pkg/file",[1235],"File watcher using fsnotify. import \"github.com/zoobz-io/flux/file\"",{"id":1248,"title":837,"titles":1249,"content":1250,"level":131},"/v1.0.2/reference/providers#new",[1235,1244],"func New(path string, opts ...Option) *Watcher Creates a watcher for the given file path.",{"id":1252,"title":189,"titles":1253,"content":1254,"level":131},"/v1.0.2/reference/providers#options",[1235,1244],"None currently defined.",{"id":1256,"title":1257,"titles":1258,"content":1259,"level":131},"/v1.0.2/reference/providers#example","Example",[1235,1244],"capacitor := flux.New[Config](\n    file.New(\"/etc/myapp/config.json\"),\n    callback,\n)",{"id":1261,"title":1262,"titles":1263,"content":1264,"level":19},"/v1.0.2/reference/providers#pkgredis","pkg/redis",[1235],"Redis watcher using keyspace notifications. import \"github.com/zoobz-io/flux/redis\"",{"id":1266,"title":837,"titles":1267,"content":1268,"level":131},"/v1.0.2/reference/providers#new-1",[1235,1262],"func New(client *redis.Client, key string, opts ...Option) *Watcher Creates a watcher for the given Redis key. Requires keyspace notifications enabled: redis-cli CONFIG SET notify-keyspace-events KEA",{"id":1270,"title":189,"titles":1271,"content":1254,"level":131},"/v1.0.2/reference/providers#options-1",[1235,1262],{"id":1273,"title":1257,"titles":1274,"content":1275,"level":131},"/v1.0.2/reference/providers#example-1",[1235,1262],"client := redis.NewClient(&redis.Options{Addr: \"localhost:6379\"})\n\ncapacitor := flux.New[Config](\n    fluxredis.New(client, \"myapp:config\"),\n    callback,\n)",{"id":1277,"title":1278,"titles":1279,"content":1280,"level":19},"/v1.0.2/reference/providers#pkgconsul","pkg/consul",[1235],"Consul KV watcher using blocking queries. import \"github.com/zoobz-io/flux/consul\"",{"id":1282,"title":837,"titles":1283,"content":1284,"level":131},"/v1.0.2/reference/providers#new-2",[1235,1278],"func New(client *api.Client, key string, opts ...Option) *Watcher Creates a watcher for the given Consul KV key.",{"id":1286,"title":189,"titles":1287,"content":1254,"level":131},"/v1.0.2/reference/providers#options-2",[1235,1278],{"id":1289,"title":1257,"titles":1290,"content":1291,"level":131},"/v1.0.2/reference/providers#example-2",[1235,1278],"client, _ := api.NewClient(api.DefaultConfig())\n\ncapacitor := flux.New[Config](\n    consul.New(client, \"myapp/config\"),\n    callback,\n)",{"id":1293,"title":1294,"titles":1295,"content":1296,"level":19},"/v1.0.2/reference/providers#pkgetcd","pkg/etcd",[1235],"etcd watcher using the native Watch API. import \"github.com/zoobz-io/flux/etcd\"",{"id":1298,"title":837,"titles":1299,"content":1300,"level":131},"/v1.0.2/reference/providers#new-3",[1235,1294],"func New(client *clientv3.Client, key string, opts ...Option) *Watcher Creates a watcher for the given etcd key.",{"id":1302,"title":189,"titles":1303,"content":1254,"level":131},"/v1.0.2/reference/providers#options-3",[1235,1294],{"id":1305,"title":1257,"titles":1306,"content":1307,"level":131},"/v1.0.2/reference/providers#example-3",[1235,1294],"client, _ := clientv3.New(clientv3.Config{\n    Endpoints: []string{\"localhost:2379\"},\n})\n\ncapacitor := flux.New[Config](\n    etcd.New(client, \"/myapp/config\"),\n    callback,\n)",{"id":1309,"title":1310,"titles":1311,"content":1312,"level":19},"/v1.0.2/reference/providers#pkgnats","pkg/nats",[1235],"NATS JetStream KV watcher. import \"github.com/zoobz-io/flux/nats\"",{"id":1314,"title":837,"titles":1315,"content":1316,"level":131},"/v1.0.2/reference/providers#new-4",[1235,1310],"func New(kv jetstream.KeyValue, key string, opts ...Option) *Watcher Creates a watcher for the given NATS KV key.",{"id":1318,"title":189,"titles":1319,"content":1254,"level":131},"/v1.0.2/reference/providers#options-4",[1235,1310],{"id":1321,"title":1257,"titles":1322,"content":1323,"level":131},"/v1.0.2/reference/providers#example-4",[1235,1310],"nc, _ := nats.Connect(\"nats://localhost:4222\")\njs, _ := jetstream.New(nc)\nkv, _ := js.KeyValue(ctx, \"config\")\n\ncapacitor := flux.New[Config](\n    fluxnats.New(kv, \"myapp\"),\n    callback,\n)",{"id":1325,"title":1326,"titles":1327,"content":1328,"level":19},"/v1.0.2/reference/providers#pkgkubernetes","pkg/kubernetes",[1235],"Kubernetes ConfigMap/Secret watcher. import \"github.com/zoobz-io/flux/kubernetes\"",{"id":1330,"title":837,"titles":1331,"content":1332,"level":131},"/v1.0.2/reference/providers#new-5",[1235,1326],"func New(client kubernetes.Interface, namespace, name, key string, opts ...Option) *Watcher Creates a watcher for a ConfigMap or Secret. The key specifies which data key within the resource to watch.",{"id":1334,"title":1335,"titles":1336,"content":1337,"level":131},"/v1.0.2/reference/providers#resourcetype","ResourceType",[1235,1326],"type ResourceType int\n\nconst (\n    ConfigMap ResourceType = iota\n    Secret\n)",{"id":1339,"title":189,"titles":1340,"content":58,"level":131},"/v1.0.2/reference/providers#options-5",[1235,1326],{"id":1342,"title":1343,"titles":1344,"content":1345,"level":840},"/v1.0.2/reference/providers#withresourcetype","WithResourceType",[1235,1326,189],"func WithResourceType(rt ResourceType) Option Sets the resource type. Default: ConfigMap.",{"id":1347,"title":1257,"titles":1348,"content":1349,"level":131},"/v1.0.2/reference/providers#example-5",[1235,1326],"config, _ := rest.InClusterConfig()\nclient, _ := kubernetes.NewForConfig(config)\n\n// ConfigMap\ncapacitor := flux.New[Config](\n    k8s.New(client, \"default\", \"myapp-config\", \"config.json\"),\n    callback,\n)\n\n// Secret\ncapacitor := flux.New[Config](\n    k8s.New(client, \"default\", \"myapp-secret\", \"config.json\",\n        k8s.WithResourceType(k8s.Secret),\n    ),\n    callback,\n)",{"id":1351,"title":1352,"titles":1353,"content":1354,"level":19},"/v1.0.2/reference/providers#pkgzookeeper","pkg/zookeeper",[1235],"ZooKeeper node watcher. import \"github.com/zoobz-io/flux/zookeeper\"",{"id":1356,"title":837,"titles":1357,"content":1358,"level":131},"/v1.0.2/reference/providers#new-6",[1235,1352],"func New(conn *zk.Conn, path string, opts ...Option) *Watcher Creates a watcher for the given ZooKeeper node path.",{"id":1360,"title":189,"titles":1361,"content":1254,"level":131},"/v1.0.2/reference/providers#options-6",[1235,1352],{"id":1363,"title":1257,"titles":1364,"content":1365,"level":131},"/v1.0.2/reference/providers#example-6",[1235,1352],"conn, _, _ := zk.Connect([]string{\"localhost:2181\"}, 5*time.Second)\n\ncapacitor := flux.New[Config](\n    zookeeper.New(conn, \"/config/myapp\"),\n    callback,\n)",{"id":1367,"title":1368,"titles":1369,"content":1370,"level":19},"/v1.0.2/reference/providers#pkgfirestore","pkg/firestore",[1235],"Firestore document watcher using realtime listeners. import \"github.com/zoobz-io/flux/firestore\"",{"id":1372,"title":837,"titles":1373,"content":1374,"level":131},"/v1.0.2/reference/providers#new-7",[1235,1368],"func New(client *firestore.Client, collection, document string, opts ...Option) *Watcher Creates a watcher for the given Firestore document.",{"id":1376,"title":189,"titles":1377,"content":58,"level":131},"/v1.0.2/reference/providers#options-7",[1235,1368],{"id":1379,"title":1380,"titles":1381,"content":1382,"level":840},"/v1.0.2/reference/providers#withfield","WithField",[1235,1368,189],"func WithField(field string) Option Sets a specific field to extract. Default: extracts \"data\" field.",{"id":1384,"title":1385,"titles":1386,"content":58,"level":131},"/v1.0.2/reference/providers#helper-functions","Helper Functions",[1235,1368],{"id":1388,"title":1389,"titles":1390,"content":1391,"level":840},"/v1.0.2/reference/providers#createdocument","CreateDocument",[1235,1368,1385],"func CreateDocument(ctx context.Context, client *firestore.Client, collection, document string, data []byte) error Creates a document with the expected structure.",{"id":1393,"title":1394,"titles":1395,"content":1396,"level":840},"/v1.0.2/reference/providers#updatedocument","UpdateDocument",[1235,1368,1385],"func UpdateDocument(ctx context.Context, client *firestore.Client, collection, document string, data []byte) error Updates a document's data field.",{"id":1398,"title":1257,"titles":1399,"content":1400,"level":131},"/v1.0.2/reference/providers#example-7",[1235,1368],"client, _ := firestore.NewClient(ctx, \"my-project\")\n\ncapacitor := flux.New[Config](\n    fluxfs.New(client, \"config\", \"myapp\"),\n    callback,\n)\n\n// Or with specific field\ncapacitor := flux.New[Config](\n    fluxfs.New(client, \"config\", \"myapp\", fluxfs.WithField(\"settings\")),\n    callback,\n) Document structure: By default expects a data field: {\"data\": \"{\\\"port\\\": 8080}\"}",{"id":1402,"title":768,"titles":1403,"content":1404,"level":19},"/v1.0.2/reference/providers#common-patterns",[1235],"All providers implement the same interface: type Watcher interface {\n    Watch(ctx context.Context) (\u003C-chan []byte, error)\n} All providers: Emit current value immediately on Watch()Emit subsequent values on changeClose channel when context is cancelledReturn error if watching cannot start",{"id":1406,"title":47,"titles":1407,"content":1408,"level":19},"/v1.0.2/reference/providers#next-steps",[1235],"Providers Guide - Usage guidanceCustom Watcher - Build your own html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sfm-E, html code.shiki .sfm-E{--shiki-default:var(--shiki-variable)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}",[1410],{"title":1411,"path":1412,"stem":1413,"children":1414,"page":1428},"V102","/v1.0.2","v1.0.2",[1415,1417,1429,1442,1453],{"title":6,"path":5,"stem":1416,"description":8},"v1.0.2/1.overview",{"title":1418,"path":1419,"stem":1420,"children":1421,"page":1428},"Learn","/v1.0.2/learn","v1.0.2/2.learn",[1422,1424,1426],{"title":52,"path":51,"stem":1423,"description":54},"v1.0.2/2.learn/1.quickstart",{"title":16,"path":114,"stem":1425,"description":116},"v1.0.2/2.learn/2.concepts",{"title":198,"path":197,"stem":1427,"description":200},"v1.0.2/2.learn/3.architecture",false,{"title":1430,"path":1431,"stem":1432,"children":1433,"page":1428},"Guides","/v1.0.2/guides","v1.0.2/3.guides",[1434,1436,1438,1440],{"title":292,"path":291,"stem":1435,"description":294},"v1.0.2/3.guides/1.testing",{"title":32,"path":353,"stem":1437,"description":355},"v1.0.2/3.guides/2.providers",{"title":420,"path":419,"stem":1439,"description":422},"v1.0.2/3.guides/3.state",{"title":507,"path":506,"stem":1441,"description":509},"v1.0.2/3.guides/4.best-practices",{"title":1443,"path":1444,"stem":1445,"children":1446,"page":1428},"Cookbook","/v1.0.2/cookbook","v1.0.2/4.cookbook",[1447,1449,1451],{"title":622,"path":621,"stem":1448,"description":624},"v1.0.2/4.cookbook/1.file-config",{"title":674,"path":673,"stem":1450,"description":676},"v1.0.2/4.cookbook/2.multi-source",{"title":730,"path":729,"stem":1452,"description":732},"v1.0.2/4.cookbook/3.custom-watcher",{"title":1454,"path":1455,"stem":1456,"children":1457,"page":1428},"Reference","/v1.0.2/reference","v1.0.2/5.reference",[1458,1460,1462],{"title":806,"path":805,"stem":1459,"description":808},"v1.0.2/5.reference/1.api",{"title":1114,"path":1113,"stem":1461,"description":1116},"v1.0.2/5.reference/2.fields",{"title":1235,"path":1234,"stem":1463,"description":1237},"v1.0.2/5.reference/3.providers",[1465],{"title":1411,"path":1412,"stem":1413,"children":1466,"page":1428},[1467,1468,1473,1479,1484],{"title":6,"path":5,"stem":1416},{"title":1418,"path":1419,"stem":1420,"children":1469,"page":1428},[1470,1471,1472],{"title":52,"path":51,"stem":1423},{"title":16,"path":114,"stem":1425},{"title":198,"path":197,"stem":1427},{"title":1430,"path":1431,"stem":1432,"children":1474,"page":1428},[1475,1476,1477,1478],{"title":292,"path":291,"stem":1435},{"title":32,"path":353,"stem":1437},{"title":420,"path":419,"stem":1439},{"title":507,"path":506,"stem":1441},{"title":1443,"path":1444,"stem":1445,"children":1480,"page":1428},[1481,1482,1483],{"title":622,"path":621,"stem":1448},{"title":674,"path":673,"stem":1450},{"title":730,"path":729,"stem":1452},{"title":1454,"path":1455,"stem":1456,"children":1485,"page":1428},[1486,1487,1488],{"title":806,"path":805,"stem":1459},{"title":1114,"path":1113,"stem":1461},{"title":1235,"path":1234,"stem":1463},[1490,3444,3869],{"id":1491,"title":1492,"body":1493,"description":58,"extension":3437,"icon":3438,"meta":3439,"navigation":1644,"path":3440,"seo":3441,"stem":3442,"__hash__":3443},"resources/readme.md","README",{"type":1494,"value":1495,"toc":3421},"minimark",[1496,1500,1568,1571,1574,1579,1582,1917,1920,2131,2134,2296,2299,2303,2320,2323,2327,2910,2914,3034,3038,3083,3087,3093,3096,3284,3287,3291,3299,3303,3321,3324,3347,3350,3370,3373,3392,3396,3408,3411,3417],[1497,1498,1499],"h1",{"id":1499},"flux",[1501,1502,1503,1514,1522,1530,1538,1546,1553,1560],"p",{},[1504,1505,1509],"a",{"href":1506,"rel":1507},"https://github.com/zoobz-io/flux/actions/workflows/ci.yml",[1508],"nofollow",[1510,1511],"img",{"alt":1512,"src":1513},"CI Status","https://github.com/zoobz-io/flux/workflows/CI/badge.svg",[1504,1515,1518],{"href":1516,"rel":1517},"https://codecov.io/gh/zoobz-io/flux",[1508],[1510,1519],{"alt":1520,"src":1521},"codecov","https://codecov.io/gh/zoobz-io/flux/graph/badge.svg?branch=main",[1504,1523,1526],{"href":1524,"rel":1525},"https://goreportcard.com/report/github.com/zoobz-io/flux",[1508],[1510,1527],{"alt":1528,"src":1529},"Go Report Card","https://goreportcard.com/badge/github.com/zoobz-io/flux",[1504,1531,1534],{"href":1532,"rel":1533},"https://github.com/zoobz-io/flux/security/code-scanning",[1508],[1510,1535],{"alt":1536,"src":1537},"CodeQL","https://github.com/zoobz-io/flux/workflows/CodeQL/badge.svg",[1504,1539,1542],{"href":1540,"rel":1541},"https://pkg.go.dev/github.com/zoobz-io/flux",[1508],[1510,1543],{"alt":1544,"src":1545},"Go Reference","https://pkg.go.dev/badge/github.com/zoobz-io/flux.svg",[1504,1547,1549],{"href":1548},"LICENSE",[1510,1550],{"alt":1551,"src":1552},"License","https://img.shields.io/github/license/zoobz-io/flux",[1504,1554,1556],{"href":1555},"go.mod",[1510,1557],{"alt":1558,"src":1559},"Go Version","https://img.shields.io/github/go-mod/go-version/zoobz-io/flux",[1504,1561,1564],{"href":1562,"rel":1563},"https://github.com/zoobz-io/flux/releases",[1508],[1510,1565],{"alt":1566,"src":1567},"Release","https://img.shields.io/github/v/release/zoobz-io/flux",[1501,1569,1570],{},"Reactive configuration synchronization for Go.",[1501,1572,1573],{},"Watch external sources, validate changes, and apply them safely with automatic rollback on failure.",[1575,1576,1578],"h2",{"id":1577},"watch-validate-apply","Watch, Validate, Apply",[1501,1580,1581],{},"A capacitor watches a source, validates incoming data, and calls you back when it changes.",[1583,1584,1588],"pre",{"className":1585,"code":1586,"language":1587,"meta":58,"style":58},"language-go shiki shiki-themes","type Config struct {\n    Port int    `json:\"port\"`\n    Host string `json:\"host\"`\n}\n\nfunc (c Config) Validate() error {\n    if c.Port \u003C 1 || c.Port > 65535 {\n        return errors.New(\"invalid port\")\n    }\n    return nil\n}\n\ncapacitor := flux.New[Config](\n    file.New(\"/etc/app/config.json\"),\n    func(ctx context.Context, prev, curr Config) error {\n        log.Printf(\"Port changed: %d -> %d\", prev.Port, curr.Port)\n        return reconfigureServer(curr)\n    },\n)\n","go",[1589,1590,1591,1610,1623,1634,1639,1646,1677,1718,1740,1746,1755,1760,1765,1789,1807,1845,1891,1906,1912],"code",{"__ignoreMap":58},[1592,1593,1595,1599,1603,1606],"span",{"class":1594,"line":9},"line",[1592,1596,1598],{"class":1597},"sUt3r","type",[1592,1600,1602],{"class":1601},"sYBwO"," Config",[1592,1604,1605],{"class":1597}," struct",[1592,1607,1609],{"class":1608},"sq5bi"," {\n",[1592,1611,1612,1616,1619],{"class":1594,"line":19},[1592,1613,1615],{"class":1614},"sBGCq","    Port",[1592,1617,1618],{"class":1601}," int",[1592,1620,1622],{"class":1621},"sxAnc","    `json:\"port\"`\n",[1592,1624,1625,1628,1631],{"class":1594,"line":131},[1592,1626,1627],{"class":1614},"    Host",[1592,1629,1630],{"class":1601}," string",[1592,1632,1633],{"class":1621}," `json:\"host\"`\n",[1592,1635,1636],{"class":1594,"line":840},[1592,1637,1638],{"class":1608},"}\n",[1592,1640,1642],{"class":1594,"line":1641},5,[1592,1643,1645],{"emptyLinePlaceholder":1644},true,"\n",[1592,1647,1649,1652,1655,1659,1662,1665,1669,1672,1675],{"class":1594,"line":1648},6,[1592,1650,1651],{"class":1597},"func",[1592,1653,1654],{"class":1608}," (",[1592,1656,1658],{"class":1657},"sSYET","c ",[1592,1660,1661],{"class":1601},"Config",[1592,1663,1664],{"class":1608},")",[1592,1666,1668],{"class":1667},"s5klm"," Validate",[1592,1670,1671],{"class":1608},"()",[1592,1673,1674],{"class":1601}," error",[1592,1676,1609],{"class":1608},[1592,1678,1680,1684,1688,1691,1694,1697,1701,1704,1706,1708,1710,1713,1716],{"class":1594,"line":1679},7,[1592,1681,1683],{"class":1682},"sW3Qg","    if",[1592,1685,1687],{"class":1686},"sh8_p"," c",[1592,1689,1690],{"class":1608},".",[1592,1692,1693],{"class":1686},"Port",[1592,1695,1696],{"class":1682}," \u003C",[1592,1698,1700],{"class":1699},"sMAmT"," 1",[1592,1702,1703],{"class":1682}," ||",[1592,1705,1687],{"class":1686},[1592,1707,1690],{"class":1608},[1592,1709,1693],{"class":1686},[1592,1711,1712],{"class":1682}," >",[1592,1714,1715],{"class":1699}," 65535",[1592,1717,1609],{"class":1608},[1592,1719,1721,1724,1727,1729,1731,1734,1737],{"class":1594,"line":1720},8,[1592,1722,1723],{"class":1682},"        return",[1592,1725,1726],{"class":1686}," errors",[1592,1728,1690],{"class":1608},[1592,1730,837],{"class":1667},[1592,1732,1733],{"class":1608},"(",[1592,1735,1736],{"class":1621},"\"invalid port\"",[1592,1738,1739],{"class":1608},")\n",[1592,1741,1743],{"class":1594,"line":1742},9,[1592,1744,1745],{"class":1608},"    }\n",[1592,1747,1749,1752],{"class":1594,"line":1748},10,[1592,1750,1751],{"class":1682},"    return",[1592,1753,1754],{"class":1597}," nil\n",[1592,1756,1758],{"class":1594,"line":1757},11,[1592,1759,1638],{"class":1608},[1592,1761,1763],{"class":1594,"line":1762},12,[1592,1764,1645],{"emptyLinePlaceholder":1644},[1592,1766,1768,1771,1774,1777,1779,1781,1784,1786],{"class":1594,"line":1767},13,[1592,1769,1770],{"class":1686},"capacitor",[1592,1772,1773],{"class":1686}," :=",[1592,1775,1776],{"class":1686}," flux",[1592,1778,1690],{"class":1608},[1592,1780,837],{"class":1667},[1592,1782,1783],{"class":1608},"[",[1592,1785,1661],{"class":1601},[1592,1787,1788],{"class":1608},"](\n",[1592,1790,1792,1795,1797,1799,1801,1804],{"class":1594,"line":1791},14,[1592,1793,1794],{"class":1686},"    file",[1592,1796,1690],{"class":1608},[1592,1798,837],{"class":1667},[1592,1800,1733],{"class":1608},[1592,1802,1803],{"class":1621},"\"/etc/app/config.json\"",[1592,1805,1806],{"class":1608},"),\n",[1592,1808,1810,1813,1815,1818,1821,1823,1826,1829,1832,1834,1837,1839,1841,1843],{"class":1594,"line":1809},15,[1592,1811,1812],{"class":1597},"    func",[1592,1814,1733],{"class":1608},[1592,1816,1817],{"class":1657},"ctx",[1592,1819,1820],{"class":1601}," context",[1592,1822,1690],{"class":1608},[1592,1824,1825],{"class":1601},"Context",[1592,1827,1828],{"class":1608},",",[1592,1830,1831],{"class":1657}," prev",[1592,1833,1828],{"class":1608},[1592,1835,1836],{"class":1657}," curr",[1592,1838,1602],{"class":1601},[1592,1840,1664],{"class":1608},[1592,1842,1674],{"class":1601},[1592,1844,1609],{"class":1608},[1592,1846,1848,1851,1853,1856,1858,1861,1865,1868,1870,1873,1875,1877,1879,1881,1883,1885,1887,1889],{"class":1594,"line":1847},16,[1592,1849,1850],{"class":1686},"        log",[1592,1852,1690],{"class":1608},[1592,1854,1855],{"class":1667},"Printf",[1592,1857,1733],{"class":1608},[1592,1859,1860],{"class":1621},"\"Port changed: ",[1592,1862,1864],{"class":1863},"scyPU","%d",[1592,1866,1867],{"class":1621}," -> ",[1592,1869,1864],{"class":1863},[1592,1871,1872],{"class":1621},"\"",[1592,1874,1828],{"class":1608},[1592,1876,1831],{"class":1686},[1592,1878,1690],{"class":1608},[1592,1880,1693],{"class":1686},[1592,1882,1828],{"class":1608},[1592,1884,1836],{"class":1686},[1592,1886,1690],{"class":1608},[1592,1888,1693],{"class":1686},[1592,1890,1739],{"class":1608},[1592,1892,1894,1896,1899,1901,1904],{"class":1594,"line":1893},17,[1592,1895,1723],{"class":1682},[1592,1897,1898],{"class":1667}," reconfigureServer",[1592,1900,1733],{"class":1608},[1592,1902,1903],{"class":1686},"curr",[1592,1905,1739],{"class":1608},[1592,1907,1909],{"class":1594,"line":1908},18,[1592,1910,1911],{"class":1608},"    },\n",[1592,1913,1915],{"class":1594,"line":1914},19,[1592,1916,1739],{"class":1608},[1501,1918,1919],{},"Start once, react forever. Invalid config is rejected, previous valid config is retained.",[1583,1921,1923],{"className":1585,"code":1922,"language":1587,"meta":58,"style":58},"if err := capacitor.Start(ctx); err != nil {\n    log.Fatal(\"Initial load failed:\", err)\n}\n\n// State machine tracks health\ncapacitor.State()     // Loading -> Healthy -> Degraded -> Empty\n\n// Current config always available\nif cfg, ok := capacitor.Current(); ok {\n    server.Listen(cfg.Host, cfg.Port)\n}\n\n// Last error for observability\nif err := capacitor.LastError(); err != nil {\n    metrics.RecordConfigError(err)\n}\n",[1589,1924,1925,1959,1980,1984,1988,1994,2007,2011,2016,2043,2073,2077,2081,2086,2110,2127],{"__ignoreMap":58},[1592,1926,1927,1930,1933,1935,1938,1940,1942,1944,1946,1949,1951,1954,1957],{"class":1594,"line":9},[1592,1928,1929],{"class":1682},"if",[1592,1931,1932],{"class":1686}," err",[1592,1934,1773],{"class":1686},[1592,1936,1937],{"class":1686}," capacitor",[1592,1939,1690],{"class":1608},[1592,1941,843],{"class":1667},[1592,1943,1733],{"class":1608},[1592,1945,1817],{"class":1686},[1592,1947,1948],{"class":1608},");",[1592,1950,1932],{"class":1686},[1592,1952,1953],{"class":1682}," !=",[1592,1955,1956],{"class":1597}," nil",[1592,1958,1609],{"class":1608},[1592,1960,1961,1964,1966,1969,1971,1974,1976,1978],{"class":1594,"line":19},[1592,1962,1963],{"class":1686},"    log",[1592,1965,1690],{"class":1608},[1592,1967,1968],{"class":1667},"Fatal",[1592,1970,1733],{"class":1608},[1592,1972,1973],{"class":1621},"\"Initial load failed:\"",[1592,1975,1828],{"class":1608},[1592,1977,1932],{"class":1686},[1592,1979,1739],{"class":1608},[1592,1981,1982],{"class":1594,"line":131},[1592,1983,1638],{"class":1608},[1592,1985,1986],{"class":1594,"line":840},[1592,1987,1645],{"emptyLinePlaceholder":1644},[1592,1989,1990],{"class":1594,"line":1641},[1592,1991,1993],{"class":1992},"sLkEo","// State machine tracks health\n",[1592,1995,1996,1998,2000,2002,2004],{"class":1594,"line":1648},[1592,1997,1770],{"class":1686},[1592,1999,1690],{"class":1608},[1592,2001,159],{"class":1667},[1592,2003,1671],{"class":1608},[1592,2005,2006],{"class":1992},"     // Loading -> Healthy -> Degraded -> Empty\n",[1592,2008,2009],{"class":1594,"line":1679},[1592,2010,1645],{"emptyLinePlaceholder":1644},[1592,2012,2013],{"class":1594,"line":1720},[1592,2014,2015],{"class":1992},"// Current config always available\n",[1592,2017,2018,2020,2023,2025,2028,2030,2032,2034,2036,2039,2041],{"class":1594,"line":1742},[1592,2019,1929],{"class":1682},[1592,2021,2022],{"class":1686}," cfg",[1592,2024,1828],{"class":1608},[1592,2026,2027],{"class":1686}," ok",[1592,2029,1773],{"class":1686},[1592,2031,1937],{"class":1686},[1592,2033,1690],{"class":1608},[1592,2035,852],{"class":1667},[1592,2037,2038],{"class":1608},"();",[1592,2040,2027],{"class":1686},[1592,2042,1609],{"class":1608},[1592,2044,2045,2048,2050,2053,2055,2058,2060,2063,2065,2067,2069,2071],{"class":1594,"line":1748},[1592,2046,2047],{"class":1686},"    server",[1592,2049,1690],{"class":1608},[1592,2051,2052],{"class":1667},"Listen",[1592,2054,1733],{"class":1608},[1592,2056,2057],{"class":1686},"cfg",[1592,2059,1690],{"class":1608},[1592,2061,2062],{"class":1686},"Host",[1592,2064,1828],{"class":1608},[1592,2066,2022],{"class":1686},[1592,2068,1690],{"class":1608},[1592,2070,1693],{"class":1686},[1592,2072,1739],{"class":1608},[1592,2074,2075],{"class":1594,"line":1757},[1592,2076,1638],{"class":1608},[1592,2078,2079],{"class":1594,"line":1762},[1592,2080,1645],{"emptyLinePlaceholder":1644},[1592,2082,2083],{"class":1594,"line":1767},[1592,2084,2085],{"class":1992},"// Last error for observability\n",[1592,2087,2088,2090,2092,2094,2096,2098,2100,2102,2104,2106,2108],{"class":1594,"line":1791},[1592,2089,1929],{"class":1682},[1592,2091,1932],{"class":1686},[1592,2093,1773],{"class":1686},[1592,2095,1937],{"class":1686},[1592,2097,1690],{"class":1608},[1592,2099,857],{"class":1667},[1592,2101,2038],{"class":1608},[1592,2103,1932],{"class":1686},[1592,2105,1953],{"class":1682},[1592,2107,1956],{"class":1597},[1592,2109,1609],{"class":1608},[1592,2111,2112,2115,2117,2120,2122,2125],{"class":1594,"line":1809},[1592,2113,2114],{"class":1686},"    metrics",[1592,2116,1690],{"class":1608},[1592,2118,2119],{"class":1667},"RecordConfigError",[1592,2121,1733],{"class":1608},[1592,2123,2124],{"class":1686},"err",[1592,2126,1739],{"class":1608},[1592,2128,2129],{"class":1594,"line":1847},[1592,2130,1638],{"class":1608},[1501,2132,2133],{},"Multiple sources? Compose them with a reducer.",[1583,2135,2137],{"className":1585,"code":2136,"language":1587,"meta":58,"style":58},"capacitor := flux.Compose[Config](\n    func(ctx context.Context, prev, curr []Config) (Config, error) {\n        // Merge defaults, file config, and environment overrides\n        return mergeConfigs(curr[0], curr[1], curr[2]), nil\n    },\n    file.New(\"/etc/app/defaults.json\"),\n    file.New(\"/etc/app/config.json\"),\n    env.New(\"APP_\"),\n)\n",[1589,2138,2139,2157,2198,2203,2243,2247,2262,2276,2292],{"__ignoreMap":58},[1592,2140,2141,2143,2145,2147,2149,2151,2153,2155],{"class":1594,"line":9},[1592,2142,1770],{"class":1686},[1592,2144,1773],{"class":1686},[1592,2146,1776],{"class":1686},[1592,2148,1690],{"class":1608},[1592,2150,877],{"class":1667},[1592,2152,1783],{"class":1608},[1592,2154,1661],{"class":1601},[1592,2156,1788],{"class":1608},[1592,2158,2159,2161,2163,2165,2167,2169,2171,2173,2175,2177,2179,2182,2184,2186,2188,2190,2192,2194,2196],{"class":1594,"line":19},[1592,2160,1812],{"class":1597},[1592,2162,1733],{"class":1608},[1592,2164,1817],{"class":1657},[1592,2166,1820],{"class":1601},[1592,2168,1690],{"class":1608},[1592,2170,1825],{"class":1601},[1592,2172,1828],{"class":1608},[1592,2174,1831],{"class":1657},[1592,2176,1828],{"class":1608},[1592,2178,1836],{"class":1657},[1592,2180,2181],{"class":1608}," []",[1592,2183,1661],{"class":1601},[1592,2185,1664],{"class":1608},[1592,2187,1654],{"class":1608},[1592,2189,1661],{"class":1601},[1592,2191,1828],{"class":1608},[1592,2193,1674],{"class":1601},[1592,2195,1664],{"class":1608},[1592,2197,1609],{"class":1608},[1592,2199,2200],{"class":1594,"line":131},[1592,2201,2202],{"class":1992},"        // Merge defaults, file config, and environment overrides\n",[1592,2204,2205,2207,2210,2212,2214,2216,2219,2222,2224,2226,2229,2231,2233,2235,2238,2241],{"class":1594,"line":840},[1592,2206,1723],{"class":1682},[1592,2208,2209],{"class":1667}," mergeConfigs",[1592,2211,1733],{"class":1608},[1592,2213,1903],{"class":1686},[1592,2215,1783],{"class":1608},[1592,2217,2218],{"class":1699},"0",[1592,2220,2221],{"class":1608},"],",[1592,2223,1836],{"class":1686},[1592,2225,1783],{"class":1608},[1592,2227,2228],{"class":1699},"1",[1592,2230,2221],{"class":1608},[1592,2232,1836],{"class":1686},[1592,2234,1783],{"class":1608},[1592,2236,2237],{"class":1699},"2",[1592,2239,2240],{"class":1608},"]),",[1592,2242,1754],{"class":1597},[1592,2244,2245],{"class":1594,"line":1641},[1592,2246,1911],{"class":1608},[1592,2248,2249,2251,2253,2255,2257,2260],{"class":1594,"line":1648},[1592,2250,1794],{"class":1686},[1592,2252,1690],{"class":1608},[1592,2254,837],{"class":1667},[1592,2256,1733],{"class":1608},[1592,2258,2259],{"class":1621},"\"/etc/app/defaults.json\"",[1592,2261,1806],{"class":1608},[1592,2263,2264,2266,2268,2270,2272,2274],{"class":1594,"line":1679},[1592,2265,1794],{"class":1686},[1592,2267,1690],{"class":1608},[1592,2269,837],{"class":1667},[1592,2271,1733],{"class":1608},[1592,2273,1803],{"class":1621},[1592,2275,1806],{"class":1608},[1592,2277,2278,2281,2283,2285,2287,2290],{"class":1594,"line":1720},[1592,2279,2280],{"class":1686},"    env",[1592,2282,1690],{"class":1608},[1592,2284,837],{"class":1667},[1592,2286,1733],{"class":1608},[1592,2288,2289],{"class":1621},"\"APP_\"",[1592,2291,1806],{"class":1608},[1592,2293,2294],{"class":1594,"line":1742},[1592,2295,1739],{"class":1608},[1501,2297,2298],{},"Same guarantees: validate first, reject invalid, retain previous, call back with valid.",[1575,2300,2302],{"id":2301},"install","Install",[1583,2304,2308],{"className":2305,"code":2306,"language":2307,"meta":58,"style":58},"language-bash shiki shiki-themes","go get github.com/zoobz-io/flux\n","bash",[1589,2309,2310],{"__ignoreMap":58},[1592,2311,2312,2314,2317],{"class":1594,"line":9},[1592,2313,1587],{"class":1667},[1592,2315,2316],{"class":1621}," get",[1592,2318,2319],{"class":1621}," github.com/zoobz-io/flux\n",[1501,2321,2322],{},"Requires Go 1.24+.",[1575,2324,2326],{"id":2325},"quick-start","Quick Start",[1583,2328,2330],{"className":1585,"code":2329,"language":1587,"meta":58,"style":58},"package main\n\nimport (\n    \"context\"\n    \"errors\"\n    \"log\"\n\n    \"github.com/zoobz-io/flux\"\n    \"github.com/zoobz-io/flux/file\"\n)\n\ntype Config struct {\n    Port int    `json:\"port\"`\n    Host string `json:\"host\"`\n}\n\nfunc (c Config) Validate() error {\n    if c.Port \u003C 1 || c.Port > 65535 {\n        return errors.New(\"port must be between 1 and 65535\")\n    }\n    if c.Host == \"\" {\n        return errors.New(\"host is required\")\n    }\n    return nil\n}\n\nfunc main() {\n    ctx := context.Background()\n\n    capacitor := flux.New[Config](\n        file.New(\"/etc/myapp/config.json\"),\n        func(ctx context.Context, prev, curr Config) error {\n            log.Printf(\"Config updated: %+v\", curr)\n            return nil\n        },\n    )\n\n    if err := capacitor.Start(ctx); err != nil {\n        log.Fatalf(\"Initial load failed: %v\", err)\n    }\n\n    log.Printf(\"State: %s\", capacitor.State())\n\n    if cfg, ok := capacitor.Current(); ok {\n        log.Printf(\"Listening on %s:%d\", cfg.Host, cfg.Port)\n    }\n\n    // Capacitor watches in background until context is cancelled\n    \u003C-ctx.Done()\n}\n",[1589,2331,2332,2340,2344,2353,2358,2363,2368,2372,2377,2382,2386,2390,2400,2408,2416,2420,2424,2444,2472,2489,2494,2513,2531,2536,2543,2548,2553,2565,2583,2588,2608,2625,2657,2683,2691,2697,2703,2708,2737,2763,2768,2773,2803,2808,2833,2874,2879,2884,2890,2905],{"__ignoreMap":58},[1592,2333,2334,2337],{"class":1594,"line":9},[1592,2335,2336],{"class":1597},"package",[1592,2338,2339],{"class":1601}," main\n",[1592,2341,2342],{"class":1594,"line":19},[1592,2343,1645],{"emptyLinePlaceholder":1644},[1592,2345,2346,2349],{"class":1594,"line":131},[1592,2347,2348],{"class":1597},"import",[1592,2350,2352],{"class":2351},"soy-K"," (\n",[1592,2354,2355],{"class":1594,"line":840},[1592,2356,2357],{"class":1621},"    \"context\"\n",[1592,2359,2360],{"class":1594,"line":1641},[1592,2361,2362],{"class":1621},"    \"errors\"\n",[1592,2364,2365],{"class":1594,"line":1648},[1592,2366,2367],{"class":1621},"    \"log\"\n",[1592,2369,2370],{"class":1594,"line":1679},[1592,2371,1645],{"emptyLinePlaceholder":1644},[1592,2373,2374],{"class":1594,"line":1720},[1592,2375,2376],{"class":1621},"    \"github.com/zoobz-io/flux\"\n",[1592,2378,2379],{"class":1594,"line":1742},[1592,2380,2381],{"class":1621},"    \"github.com/zoobz-io/flux/file\"\n",[1592,2383,2384],{"class":1594,"line":1748},[1592,2385,1739],{"class":2351},[1592,2387,2388],{"class":1594,"line":1757},[1592,2389,1645],{"emptyLinePlaceholder":1644},[1592,2391,2392,2394,2396,2398],{"class":1594,"line":1762},[1592,2393,1598],{"class":1597},[1592,2395,1602],{"class":1601},[1592,2397,1605],{"class":1597},[1592,2399,1609],{"class":1608},[1592,2401,2402,2404,2406],{"class":1594,"line":1767},[1592,2403,1615],{"class":1614},[1592,2405,1618],{"class":1601},[1592,2407,1622],{"class":1621},[1592,2409,2410,2412,2414],{"class":1594,"line":1791},[1592,2411,1627],{"class":1614},[1592,2413,1630],{"class":1601},[1592,2415,1633],{"class":1621},[1592,2417,2418],{"class":1594,"line":1809},[1592,2419,1638],{"class":1608},[1592,2421,2422],{"class":1594,"line":1847},[1592,2423,1645],{"emptyLinePlaceholder":1644},[1592,2425,2426,2428,2430,2432,2434,2436,2438,2440,2442],{"class":1594,"line":1893},[1592,2427,1651],{"class":1597},[1592,2429,1654],{"class":1608},[1592,2431,1658],{"class":1657},[1592,2433,1661],{"class":1601},[1592,2435,1664],{"class":1608},[1592,2437,1668],{"class":1667},[1592,2439,1671],{"class":1608},[1592,2441,1674],{"class":1601},[1592,2443,1609],{"class":1608},[1592,2445,2446,2448,2450,2452,2454,2456,2458,2460,2462,2464,2466,2468,2470],{"class":1594,"line":1908},[1592,2447,1683],{"class":1682},[1592,2449,1687],{"class":1686},[1592,2451,1690],{"class":1608},[1592,2453,1693],{"class":1686},[1592,2455,1696],{"class":1682},[1592,2457,1700],{"class":1699},[1592,2459,1703],{"class":1682},[1592,2461,1687],{"class":1686},[1592,2463,1690],{"class":1608},[1592,2465,1693],{"class":1686},[1592,2467,1712],{"class":1682},[1592,2469,1715],{"class":1699},[1592,2471,1609],{"class":1608},[1592,2473,2474,2476,2478,2480,2482,2484,2487],{"class":1594,"line":1914},[1592,2475,1723],{"class":1682},[1592,2477,1726],{"class":1686},[1592,2479,1690],{"class":1608},[1592,2481,837],{"class":1667},[1592,2483,1733],{"class":1608},[1592,2485,2486],{"class":1621},"\"port must be between 1 and 65535\"",[1592,2488,1739],{"class":1608},[1592,2490,2492],{"class":1594,"line":2491},20,[1592,2493,1745],{"class":1608},[1592,2495,2497,2499,2501,2503,2505,2508,2511],{"class":1594,"line":2496},21,[1592,2498,1683],{"class":1682},[1592,2500,1687],{"class":1686},[1592,2502,1690],{"class":1608},[1592,2504,2062],{"class":1686},[1592,2506,2507],{"class":1682}," ==",[1592,2509,2510],{"class":1621}," \"\"",[1592,2512,1609],{"class":1608},[1592,2514,2516,2518,2520,2522,2524,2526,2529],{"class":1594,"line":2515},22,[1592,2517,1723],{"class":1682},[1592,2519,1726],{"class":1686},[1592,2521,1690],{"class":1608},[1592,2523,837],{"class":1667},[1592,2525,1733],{"class":1608},[1592,2527,2528],{"class":1621},"\"host is required\"",[1592,2530,1739],{"class":1608},[1592,2532,2534],{"class":1594,"line":2533},23,[1592,2535,1745],{"class":1608},[1592,2537,2539,2541],{"class":1594,"line":2538},24,[1592,2540,1751],{"class":1682},[1592,2542,1754],{"class":1597},[1592,2544,2546],{"class":1594,"line":2545},25,[1592,2547,1638],{"class":1608},[1592,2549,2551],{"class":1594,"line":2550},26,[1592,2552,1645],{"emptyLinePlaceholder":1644},[1592,2554,2556,2558,2561,2563],{"class":1594,"line":2555},27,[1592,2557,1651],{"class":1597},[1592,2559,2560],{"class":1667}," main",[1592,2562,1671],{"class":1608},[1592,2564,1609],{"class":1608},[1592,2566,2568,2571,2573,2575,2577,2580],{"class":1594,"line":2567},28,[1592,2569,2570],{"class":1686},"    ctx",[1592,2572,1773],{"class":1686},[1592,2574,1820],{"class":1686},[1592,2576,1690],{"class":1608},[1592,2578,2579],{"class":1667},"Background",[1592,2581,2582],{"class":1608},"()\n",[1592,2584,2586],{"class":1594,"line":2585},29,[1592,2587,1645],{"emptyLinePlaceholder":1644},[1592,2589,2591,2594,2596,2598,2600,2602,2604,2606],{"class":1594,"line":2590},30,[1592,2592,2593],{"class":1686},"    capacitor",[1592,2595,1773],{"class":1686},[1592,2597,1776],{"class":1686},[1592,2599,1690],{"class":1608},[1592,2601,837],{"class":1667},[1592,2603,1783],{"class":1608},[1592,2605,1661],{"class":1601},[1592,2607,1788],{"class":1608},[1592,2609,2611,2614,2616,2618,2620,2623],{"class":1594,"line":2610},31,[1592,2612,2613],{"class":1686},"        file",[1592,2615,1690],{"class":1608},[1592,2617,837],{"class":1667},[1592,2619,1733],{"class":1608},[1592,2621,2622],{"class":1621},"\"/etc/myapp/config.json\"",[1592,2624,1806],{"class":1608},[1592,2626,2628,2631,2633,2635,2637,2639,2641,2643,2645,2647,2649,2651,2653,2655],{"class":1594,"line":2627},32,[1592,2629,2630],{"class":1597},"        func",[1592,2632,1733],{"class":1608},[1592,2634,1817],{"class":1657},[1592,2636,1820],{"class":1601},[1592,2638,1690],{"class":1608},[1592,2640,1825],{"class":1601},[1592,2642,1828],{"class":1608},[1592,2644,1831],{"class":1657},[1592,2646,1828],{"class":1608},[1592,2648,1836],{"class":1657},[1592,2650,1602],{"class":1601},[1592,2652,1664],{"class":1608},[1592,2654,1674],{"class":1601},[1592,2656,1609],{"class":1608},[1592,2658,2660,2663,2665,2667,2669,2672,2675,2677,2679,2681],{"class":1594,"line":2659},33,[1592,2661,2662],{"class":1686},"            log",[1592,2664,1690],{"class":1608},[1592,2666,1855],{"class":1667},[1592,2668,1733],{"class":1608},[1592,2670,2671],{"class":1621},"\"Config updated: ",[1592,2673,2674],{"class":1863},"%+v",[1592,2676,1872],{"class":1621},[1592,2678,1828],{"class":1608},[1592,2680,1836],{"class":1686},[1592,2682,1739],{"class":1608},[1592,2684,2686,2689],{"class":1594,"line":2685},34,[1592,2687,2688],{"class":1682},"            return",[1592,2690,1754],{"class":1597},[1592,2692,2694],{"class":1594,"line":2693},35,[1592,2695,2696],{"class":1608},"        },\n",[1592,2698,2700],{"class":1594,"line":2699},36,[1592,2701,2702],{"class":1608},"    )\n",[1592,2704,2706],{"class":1594,"line":2705},37,[1592,2707,1645],{"emptyLinePlaceholder":1644},[1592,2709,2711,2713,2715,2717,2719,2721,2723,2725,2727,2729,2731,2733,2735],{"class":1594,"line":2710},38,[1592,2712,1683],{"class":1682},[1592,2714,1932],{"class":1686},[1592,2716,1773],{"class":1686},[1592,2718,1937],{"class":1686},[1592,2720,1690],{"class":1608},[1592,2722,843],{"class":1667},[1592,2724,1733],{"class":1608},[1592,2726,1817],{"class":1686},[1592,2728,1948],{"class":1608},[1592,2730,1932],{"class":1686},[1592,2732,1953],{"class":1682},[1592,2734,1956],{"class":1597},[1592,2736,1609],{"class":1608},[1592,2738,2740,2742,2744,2747,2749,2752,2755,2757,2759,2761],{"class":1594,"line":2739},39,[1592,2741,1850],{"class":1686},[1592,2743,1690],{"class":1608},[1592,2745,2746],{"class":1667},"Fatalf",[1592,2748,1733],{"class":1608},[1592,2750,2751],{"class":1621},"\"Initial load failed: ",[1592,2753,2754],{"class":1863},"%v",[1592,2756,1872],{"class":1621},[1592,2758,1828],{"class":1608},[1592,2760,1932],{"class":1686},[1592,2762,1739],{"class":1608},[1592,2764,2766],{"class":1594,"line":2765},40,[1592,2767,1745],{"class":1608},[1592,2769,2771],{"class":1594,"line":2770},41,[1592,2772,1645],{"emptyLinePlaceholder":1644},[1592,2774,2776,2778,2780,2782,2784,2787,2790,2792,2794,2796,2798,2800],{"class":1594,"line":2775},42,[1592,2777,1963],{"class":1686},[1592,2779,1690],{"class":1608},[1592,2781,1855],{"class":1667},[1592,2783,1733],{"class":1608},[1592,2785,2786],{"class":1621},"\"State: ",[1592,2788,2789],{"class":1863},"%s",[1592,2791,1872],{"class":1621},[1592,2793,1828],{"class":1608},[1592,2795,1937],{"class":1686},[1592,2797,1690],{"class":1608},[1592,2799,159],{"class":1667},[1592,2801,2802],{"class":1608},"())\n",[1592,2804,2806],{"class":1594,"line":2805},43,[1592,2807,1645],{"emptyLinePlaceholder":1644},[1592,2809,2811,2813,2815,2817,2819,2821,2823,2825,2827,2829,2831],{"class":1594,"line":2810},44,[1592,2812,1683],{"class":1682},[1592,2814,2022],{"class":1686},[1592,2816,1828],{"class":1608},[1592,2818,2027],{"class":1686},[1592,2820,1773],{"class":1686},[1592,2822,1937],{"class":1686},[1592,2824,1690],{"class":1608},[1592,2826,852],{"class":1667},[1592,2828,2038],{"class":1608},[1592,2830,2027],{"class":1686},[1592,2832,1609],{"class":1608},[1592,2834,2836,2838,2840,2842,2844,2847,2849,2852,2854,2856,2858,2860,2862,2864,2866,2868,2870,2872],{"class":1594,"line":2835},45,[1592,2837,1850],{"class":1686},[1592,2839,1690],{"class":1608},[1592,2841,1855],{"class":1667},[1592,2843,1733],{"class":1608},[1592,2845,2846],{"class":1621},"\"Listening on ",[1592,2848,2789],{"class":1863},[1592,2850,2851],{"class":1621},":",[1592,2853,1864],{"class":1863},[1592,2855,1872],{"class":1621},[1592,2857,1828],{"class":1608},[1592,2859,2022],{"class":1686},[1592,2861,1690],{"class":1608},[1592,2863,2062],{"class":1686},[1592,2865,1828],{"class":1608},[1592,2867,2022],{"class":1686},[1592,2869,1690],{"class":1608},[1592,2871,1693],{"class":1686},[1592,2873,1739],{"class":1608},[1592,2875,2877],{"class":1594,"line":2876},46,[1592,2878,1745],{"class":1608},[1592,2880,2882],{"class":1594,"line":2881},47,[1592,2883,1645],{"emptyLinePlaceholder":1644},[1592,2885,2887],{"class":1594,"line":2886},48,[1592,2888,2889],{"class":1992},"    // Capacitor watches in background until context is cancelled\n",[1592,2891,2893,2896,2898,2900,2903],{"class":1594,"line":2892},49,[1592,2894,2895],{"class":1682},"    \u003C-",[1592,2897,1817],{"class":1686},[1592,2899,1690],{"class":1608},[1592,2901,2902],{"class":1667},"Done",[1592,2904,2582],{"class":1608},[1592,2906,2908],{"class":1594,"line":2907},50,[1592,2909,1638],{"class":1608},[1575,2911,2913],{"id":2912},"capabilities","Capabilities",[2915,2916,2917,2933],"table",{},[2918,2919,2920],"thead",{},[2921,2922,2923,2927,2930],"tr",{},[2924,2925,2926],"th",{},"Feature",[2924,2928,2929],{},"Description",[2924,2931,2932],{},"Docs",[2934,2935,2936,2950,2964,2977,2990,3002,3021],"tbody",{},[2921,2937,2938,2941,2944],{},[2939,2940,27],"td",{},[2939,2942,2943],{},"Loading, Healthy, Degraded, Empty with clear transitions",[2939,2945,2946],{},[1504,2947,2949],{"href":2948},"docs/learn/concepts","Concepts",[2921,2951,2952,2955,2958],{},[2939,2953,2954],{},"Multi-Source Composition",[2939,2956,2957],{},"Merge configs from multiple watchers with custom reducers",[2939,2959,2960],{},[1504,2961,2963],{"href":2962},"docs/cookbook/multi-source","Multi-Source",[2921,2965,2966,2969,2972],{},[2939,2967,2968],{},"Pluggable Providers",[2939,2970,2971],{},"File, Redis, Consul, etcd, NATS, Kubernetes, ZooKeeper, Firestore",[2939,2973,2974],{},[1504,2975,32],{"href":2976},"docs/guides/providers",[2921,2978,2979,2982,2985],{},[2939,2980,2981],{},"Validation Pipeline",[2939,2983,2984],{},"Type-safe validation with automatic rejection and rollback",[2939,2986,2987],{},[1504,2988,198],{"href":2989},"docs/learn/architecture",[2921,2991,2992,2994,2997],{},[2939,2993,106],{},[2939,2995,2996],{},"Configurable delay to batch rapid changes",[2939,2998,2999],{},[1504,3000,507],{"href":3001},"docs/guides/best-practices",[2921,3003,3004,3007,3015],{},[2939,3005,3006],{},"Signal Observability",[2939,3008,3009,3010],{},"State changes and errors via ",[1504,3011,3014],{"href":3012,"rel":3013},"https://github.com/zoobz-io/capitan",[1508],"capitan",[2939,3016,3017],{},[1504,3018,3020],{"href":3019},"docs/reference/fields","Fields",[2921,3022,3023,3026,3029],{},[2939,3024,3025],{},"Testing Utilities",[2939,3027,3028],{},"Sync mode and channel watchers for deterministic tests",[2939,3030,3031],{},[1504,3032,292],{"href":3033},"docs/guides/testing",[1575,3035,3037],{"id":3036},"why-flux","Why flux?",[3039,3040,3041,3049,3055,3061,3067,3077],"ul",{},[3042,3043,3044,3048],"li",{},[3045,3046,3047],"strong",{},"Safe by default"," — Invalid config rejected, previous retained, callback only sees valid data",[3042,3050,3051,3054],{},[3045,3052,3053],{},"Four-state machine"," — Loading, Healthy, Degraded, Empty with clear transitions",[3042,3056,3057,3060],{},[3045,3058,3059],{},"Multi-source composition"," — Merge configs from files, Redis, Kubernetes, environment",[3042,3062,3063,3066],{},[3045,3064,3065],{},"Pluggable providers"," — File, Redis, Consul, etcd, NATS, Kubernetes, ZooKeeper, Firestore",[3042,3068,3069,3072,3073,3076],{},[3045,3070,3071],{},"Observable"," — ",[1504,3074,3014],{"href":3012,"rel":3075},[1508]," signals for state changes and failures",[3042,3078,3079,3082],{},[3045,3080,3081],{},"Testable"," — Sync mode and channel watchers for deterministic tests",[1575,3084,3086],{"id":3085},"configuration-as-a-service","Configuration as a Service",[1501,3088,3089,3090,1690],{},"Flux enables a pattern: ",[3045,3091,3092],{},"define once, update anywhere, validate always",[1501,3094,3095],{},"Your configuration lives in external sources — files, Redis, Kubernetes ConfigMaps. Flux watches, validates, and delivers changes. Your application just reacts.",[1583,3097,3099],{"className":1585,"code":3098,"language":1587,"meta":58,"style":58},"// In your infrastructure\nconfigMap := &corev1.ConfigMap{\n    Data: map[string]string{\n        \"config\": `{\"port\": 8080, \"host\": \"0.0.0.0\"}`,\n    },\n}\n\n// In your application\ncapacitor := flux.New[Config](\n    kubernetes.New(client, \"default\", \"app-config\", \"config\"),\n    func(ctx context.Context, prev, curr Config) error {\n        return server.Reconfigure(curr)\n    },\n)\n",[1589,3100,3101,3106,3127,3149,3162,3166,3170,3174,3179,3197,3228,3258,3276,3280],{"__ignoreMap":58},[1592,3102,3103],{"class":1594,"line":9},[1592,3104,3105],{"class":1992},"// In your infrastructure\n",[1592,3107,3108,3111,3113,3116,3119,3121,3124],{"class":1594,"line":19},[1592,3109,3110],{"class":1686},"configMap",[1592,3112,1773],{"class":1686},[1592,3114,3115],{"class":1682}," &",[1592,3117,3118],{"class":1601},"corev1",[1592,3120,1690],{"class":1608},[1592,3122,3123],{"class":1601},"ConfigMap",[1592,3125,3126],{"class":1608},"{\n",[1592,3128,3129,3132,3134,3137,3139,3142,3145,3147],{"class":1594,"line":131},[1592,3130,3131],{"class":1614},"    Data",[1592,3133,2851],{"class":1608},[1592,3135,3136],{"class":1597}," map",[1592,3138,1783],{"class":1608},[1592,3140,3141],{"class":1601},"string",[1592,3143,3144],{"class":1608},"]",[1592,3146,3141],{"class":1601},[1592,3148,3126],{"class":1608},[1592,3150,3151,3154,3156,3159],{"class":1594,"line":840},[1592,3152,3153],{"class":1621},"        \"config\"",[1592,3155,2851],{"class":1608},[1592,3157,3158],{"class":1621}," `{\"port\": 8080, \"host\": \"0.0.0.0\"}`",[1592,3160,3161],{"class":1608},",\n",[1592,3163,3164],{"class":1594,"line":1641},[1592,3165,1911],{"class":1608},[1592,3167,3168],{"class":1594,"line":1648},[1592,3169,1638],{"class":1608},[1592,3171,3172],{"class":1594,"line":1679},[1592,3173,1645],{"emptyLinePlaceholder":1644},[1592,3175,3176],{"class":1594,"line":1720},[1592,3177,3178],{"class":1992},"// In your application\n",[1592,3180,3181,3183,3185,3187,3189,3191,3193,3195],{"class":1594,"line":1742},[1592,3182,1770],{"class":1686},[1592,3184,1773],{"class":1686},[1592,3186,1776],{"class":1686},[1592,3188,1690],{"class":1608},[1592,3190,837],{"class":1667},[1592,3192,1783],{"class":1608},[1592,3194,1661],{"class":1601},[1592,3196,1788],{"class":1608},[1592,3198,3199,3202,3204,3206,3208,3211,3213,3216,3218,3221,3223,3226],{"class":1594,"line":1748},[1592,3200,3201],{"class":1686},"    kubernetes",[1592,3203,1690],{"class":1608},[1592,3205,837],{"class":1667},[1592,3207,1733],{"class":1608},[1592,3209,3210],{"class":1686},"client",[1592,3212,1828],{"class":1608},[1592,3214,3215],{"class":1621}," \"default\"",[1592,3217,1828],{"class":1608},[1592,3219,3220],{"class":1621}," \"app-config\"",[1592,3222,1828],{"class":1608},[1592,3224,3225],{"class":1621}," \"config\"",[1592,3227,1806],{"class":1608},[1592,3229,3230,3232,3234,3236,3238,3240,3242,3244,3246,3248,3250,3252,3254,3256],{"class":1594,"line":1757},[1592,3231,1812],{"class":1597},[1592,3233,1733],{"class":1608},[1592,3235,1817],{"class":1657},[1592,3237,1820],{"class":1601},[1592,3239,1690],{"class":1608},[1592,3241,1825],{"class":1601},[1592,3243,1828],{"class":1608},[1592,3245,1831],{"class":1657},[1592,3247,1828],{"class":1608},[1592,3249,1836],{"class":1657},[1592,3251,1602],{"class":1601},[1592,3253,1664],{"class":1608},[1592,3255,1674],{"class":1601},[1592,3257,1609],{"class":1608},[1592,3259,3260,3262,3265,3267,3270,3272,3274],{"class":1594,"line":1762},[1592,3261,1723],{"class":1682},[1592,3263,3264],{"class":1686}," server",[1592,3266,1690],{"class":1608},[1592,3268,3269],{"class":1667},"Reconfigure",[1592,3271,1733],{"class":1608},[1592,3273,1903],{"class":1686},[1592,3275,1739],{"class":1608},[1592,3277,3278],{"class":1594,"line":1767},[1592,3279,1911],{"class":1608},[1592,3281,3282],{"class":1594,"line":1791},[1592,3283,1739],{"class":1608},[1501,3285,3286],{},"Update the ConfigMap, flux delivers the change. Invalid update? Rejected. Previous config retained. Your application never sees bad data.",[1575,3288,3290],{"id":3289},"documentation","Documentation",[3039,3292,3293],{},[3042,3294,3295,3298],{},[1504,3296,6],{"href":3297},"docs/overview"," — Design philosophy and architecture",[3300,3301,1418],"h3",{"id":3302},"learn",[3039,3304,3305,3311,3316],{},[3042,3306,3307,3310],{},[1504,3308,52],{"href":3309},"docs/learn/quickstart"," — Get started in minutes",[3042,3312,3313,3315],{},[1504,3314,16],{"href":2948}," — Capacitor, watchers, state machine, validation",[3042,3317,3318,3320],{},[1504,3319,198],{"href":2989}," — Processing pipeline, debouncing, error handling",[3300,3322,1430],{"id":3323},"guides",[3039,3325,3326,3331,3336,3342],{},[3042,3327,3328,3330],{},[1504,3329,292],{"href":3033}," — Sync mode, channel watchers, deterministic tests",[3042,3332,3333,3335],{},[1504,3334,32],{"href":2976}," — Configuring file, Redis, Kubernetes, and other watchers",[3042,3337,3338,3341],{},[1504,3339,420],{"href":3340},"docs/guides/state"," — State transitions, error recovery, circuit breakers",[3042,3343,3344,3346],{},[1504,3345,507],{"href":3001}," — Validation design, graceful degradation, observability",[3300,3348,1443],{"id":3349},"cookbook",[3039,3351,3352,3359,3364],{},[3042,3353,3354,3358],{},[1504,3355,3357],{"href":3356},"docs/cookbook/file-config","File Config"," — Hot-reloading configuration files",[3042,3360,3361,3363],{},[1504,3362,2963],{"href":2962}," — Merging defaults, files, and environment",[3042,3365,3366,3369],{},[1504,3367,730],{"href":3368},"docs/cookbook/custom-watcher"," — Building watchers for custom sources",[3300,3371,1454],{"id":3372},"reference",[3039,3374,3375,3381,3386],{},[3042,3376,3377,3380],{},[1504,3378,806],{"href":3379},"docs/reference/api"," — Complete function and type documentation",[3042,3382,3383,3385],{},[1504,3384,1114],{"href":3019}," — Capitan signal fields",[3042,3387,3388,3391],{},[1504,3389,1235],{"href":3390},"docs/reference/providers"," — All provider packages and options",[1575,3393,3395],{"id":3394},"contributing","Contributing",[1501,3397,3398,3399,3403,3404,3407],{},"See ",[1504,3400,3402],{"href":3401},"CONTRIBUTING","CONTRIBUTING.md"," for guidelines. Run ",[1589,3405,3406],{},"make help"," for available commands.",[1575,3409,1551],{"id":3410},"license",[1501,3412,3413,3414,3416],{},"MIT License — see ",[1504,3415,1548],{"href":1548}," for details.",[3418,3419,3420],"style",{},"html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}",{"title":58,"searchDepth":19,"depth":19,"links":3422},[3423,3424,3425,3426,3427,3428,3429,3435,3436],{"id":1577,"depth":19,"text":1578},{"id":2301,"depth":19,"text":2302},{"id":2325,"depth":19,"text":2326},{"id":2912,"depth":19,"text":2913},{"id":3036,"depth":19,"text":3037},{"id":3085,"depth":19,"text":3086},{"id":3289,"depth":19,"text":3290,"children":3430},[3431,3432,3433,3434],{"id":3302,"depth":131,"text":1418},{"id":3323,"depth":131,"text":1430},{"id":3349,"depth":131,"text":1443},{"id":3372,"depth":131,"text":1454},{"id":3394,"depth":19,"text":3395},{"id":3410,"depth":19,"text":1551},"md","book-open",{},"/readme",{"title":1492,"description":58},"readme","Ck3s779waop3NTkSRGI3SuKU-BmYBa3jS_y3A6SdaY8",{"id":3445,"title":3446,"body":3447,"description":58,"extension":3437,"icon":3863,"meta":3864,"navigation":1644,"path":3865,"seo":3866,"stem":3867,"__hash__":3868},"resources/security.md","Security",{"type":1494,"value":3448,"toc":3849},[3449,3453,3457,3460,3499,3503,3506,3510,3515,3518,3557,3561,3564,3613,3617,3643,3647,3650,3654,3657,3742,3746,3749,3780,3784,3787,3806,3810,3824,3828,3831,3837,3840,3846],[1497,3450,3452],{"id":3451},"security-policy","Security Policy",[1575,3454,3456],{"id":3455},"supported-versions","Supported Versions",[1501,3458,3459],{},"We release patches for security vulnerabilities. Which versions are eligible for receiving such patches depends on the CVSS v3.0 Rating:",[2915,3461,3462,3475],{},[2918,3463,3464],{},[2921,3465,3466,3469,3472],{},[2924,3467,3468],{},"Version",[2924,3470,3471],{},"Supported",[2924,3473,3474],{},"Status",[2934,3476,3477,3488],{},[2921,3478,3479,3482,3485],{},[2939,3480,3481],{},"latest",[2939,3483,3484],{},"Yes",[2939,3486,3487],{},"Active development",[2921,3489,3490,3493,3496],{},[2939,3491,3492],{},"\u003C latest",[2939,3494,3495],{},"No",[2939,3497,3498],{},"Security fixes only for critical issues",[1575,3500,3502],{"id":3501},"reporting-a-vulnerability","Reporting a Vulnerability",[1501,3504,3505],{},"We take the security of flux seriously. If you have discovered a security vulnerability in this project, please report it responsibly.",[3300,3507,3509],{"id":3508},"how-to-report","How to Report",[1501,3511,3512],{},[3045,3513,3514],{},"Please DO NOT report security vulnerabilities through public GitHub issues.",[1501,3516,3517],{},"Instead, please report them via one of the following methods:",[3519,3520,3521,3544],"ol",{},[3042,3522,3523,3526,3527],{},[3045,3524,3525],{},"GitHub Security Advisories"," (Preferred)",[3039,3528,3529,3538,3541],{},[3042,3530,3531,3532,3537],{},"Go to the ",[1504,3533,3536],{"href":3534,"rel":3535},"https://github.com/zoobz-io/flux/security",[1508],"Security tab"," of this repository",[3042,3539,3540],{},"Click \"Report a vulnerability\"",[3042,3542,3543],{},"Fill out the form with details about the vulnerability",[3042,3545,3546,3549],{},[3045,3547,3548],{},"Email",[3039,3550,3551,3554],{},[3042,3552,3553],{},"Send details to the repository maintainer through GitHub profile contact information",[3042,3555,3556],{},"Use PGP encryption if possible for sensitive details",[3300,3558,3560],{"id":3559},"what-to-include","What to Include",[1501,3562,3563],{},"Please include the following information (as much as you can provide) to help us better understand the nature and scope of the possible issue:",[3039,3565,3566,3572,3578,3584,3590,3595,3601,3607],{},[3042,3567,3568,3571],{},[3045,3569,3570],{},"Type of issue"," (e.g., race condition, deadlock, memory leak, etc.)",[3042,3573,3574,3577],{},[3045,3575,3576],{},"Full paths of source file(s)"," related to the manifestation of the issue",[3042,3579,3580,3583],{},[3045,3581,3582],{},"The location of the affected source code"," (tag/branch/commit or direct URL)",[3042,3585,3586,3589],{},[3045,3587,3588],{},"Any special configuration required"," to reproduce the issue",[3042,3591,3592,3589],{},[3045,3593,3594],{},"Step-by-step instructions",[3042,3596,3597,3600],{},[3045,3598,3599],{},"Proof-of-concept or exploit code"," (if possible)",[3042,3602,3603,3606],{},[3045,3604,3605],{},"Impact of the issue",", including how an attacker might exploit the issue",[3042,3608,3609,3612],{},[3045,3610,3611],{},"Your name and affiliation"," (optional)",[3300,3614,3616],{"id":3615},"what-to-expect","What to Expect",[3039,3618,3619,3625,3631,3637],{},[3042,3620,3621,3624],{},[3045,3622,3623],{},"Acknowledgment",": We will acknowledge receipt of your vulnerability report within 48 hours",[3042,3626,3627,3630],{},[3045,3628,3629],{},"Initial Assessment",": Within 7 days, we will provide an initial assessment of the report",[3042,3632,3633,3636],{},[3045,3634,3635],{},"Resolution Timeline",": We aim to resolve critical issues within 30 days",[3042,3638,3639,3642],{},[3045,3640,3641],{},"Disclosure",": We will coordinate with you on the disclosure timeline",[3300,3644,3646],{"id":3645},"preferred-languages","Preferred Languages",[1501,3648,3649],{},"We prefer all communications to be in English.",[1575,3651,3653],{"id":3652},"security-best-practices","Security Best Practices",[1501,3655,3656],{},"When using flux in your applications, we recommend:",[3519,3658,3659,3680,3696,3711,3727],{},[3042,3660,3661,3664],{},[3045,3662,3663],{},"Keep Dependencies Updated",[1583,3665,3667],{"className":2305,"code":3666,"language":2307,"meta":58,"style":58},"go get -u github.com/zoobz-io/flux\n",[1589,3668,3669],{"__ignoreMap":58},[1592,3670,3671,3673,3675,3678],{"class":1594,"line":9},[1592,3672,1587],{"class":1667},[1592,3674,2316],{"class":1621},[1592,3676,3677],{"class":1597}," -u",[1592,3679,2319],{"class":1621},[3042,3681,3682,3685],{},[3045,3683,3684],{},"Validate Configuration",[3039,3686,3687,3690,3693],{},[3042,3688,3689],{},"Always implement thorough validation functions",[3042,3691,3692],{},"Check for security-sensitive fields (ports, URLs, credentials)",[3042,3694,3695],{},"Reject configuration that could compromise security",[3042,3697,3698,3700],{},[3045,3699,96],{},[3039,3701,3702,3705,3708],{},[3042,3703,3704],{},"Implement proper error handling in transform/validate/apply functions",[3042,3706,3707],{},"Log validation failures for audit purposes",[3042,3709,3710],{},"Don't expose sensitive config details in error messages",[3042,3712,3713,3716],{},[3045,3714,3715],{},"File Permissions",[3039,3717,3718,3721,3724],{},[3042,3719,3720],{},"Ensure config files have appropriate permissions",[3042,3722,3723],{},"Don't store secrets in plain config files",[3042,3725,3726],{},"Use environment variables or secret managers for sensitive data",[3042,3728,3729,3731],{},[3045,3730,420],{},[3039,3732,3733,3736,3739],{},[3042,3734,3735],{},"Monitor Degraded state for potential issues",[3042,3737,3738],{},"Alert on repeated validation failures",[3042,3740,3741],{},"Review configuration changes in audit logs",[1575,3743,3745],{"id":3744},"security-features","Security Features",[1501,3747,3748],{},"flux includes several built-in security features:",[3039,3750,3751,3757,3763,3769,3775],{},[3042,3752,3753,3756],{},[3045,3754,3755],{},"Validation Before Apply",": Invalid config never reaches application",[3042,3758,3759,3762],{},[3045,3760,3761],{},"State Isolation",": Failed updates don't corrupt current config",[3042,3764,3765,3768],{},[3045,3766,3767],{},"Rollback Protection",": Previous valid config retained on failure",[3042,3770,3771,3774],{},[3045,3772,3773],{},"Capitan Signals",": Full audit trail of configuration changes",[3042,3776,3777,3779],{},[3045,3778,106],{},": Prevents rapid config thrashing",[1575,3781,3783],{"id":3782},"automated-security-scanning","Automated Security Scanning",[1501,3785,3786],{},"This project uses:",[3039,3788,3789,3794,3800],{},[3042,3790,3791,3793],{},[3045,3792,1536],{},": GitHub's semantic code analysis for security vulnerabilities",[3042,3795,3796,3799],{},[3045,3797,3798],{},"golangci-lint",": Static analysis including security linters",[3042,3801,3802,3805],{},[3045,3803,3804],{},"Codecov",": Coverage tracking to ensure security-critical code is tested",[1575,3807,3809],{"id":3808},"vulnerability-disclosure-policy","Vulnerability Disclosure Policy",[3039,3811,3812,3815,3818,3821],{},[3042,3813,3814],{},"Security vulnerabilities will be disclosed via GitHub Security Advisories",[3042,3816,3817],{},"We follow a 90-day disclosure timeline for non-critical issues",[3042,3819,3820],{},"Critical vulnerabilities may be disclosed sooner after patches are available",[3042,3822,3823],{},"We will credit reporters who follow responsible disclosure practices",[1575,3825,3827],{"id":3826},"credits","Credits",[1501,3829,3830],{},"We thank the following individuals for responsibly disclosing security issues:",[1501,3832,3833],{},[3834,3835,3836],"em",{},"This list is currently empty. Be the first to help improve our security!",[3838,3839],"hr",{},[1501,3841,3842,3845],{},[3045,3843,3844],{},"Last Updated",": 2024-12-03",[3418,3847,3848],{},"html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":58,"searchDepth":19,"depth":19,"links":3850},[3851,3852,3858,3859,3860,3861,3862],{"id":3455,"depth":19,"text":3456},{"id":3501,"depth":19,"text":3502,"children":3853},[3854,3855,3856,3857],{"id":3508,"depth":131,"text":3509},{"id":3559,"depth":131,"text":3560},{"id":3615,"depth":131,"text":3616},{"id":3645,"depth":131,"text":3646},{"id":3652,"depth":19,"text":3653},{"id":3744,"depth":19,"text":3745},{"id":3782,"depth":19,"text":3783},{"id":3808,"depth":19,"text":3809},{"id":3826,"depth":19,"text":3827},"shield",{},"/security",{"title":3446,"description":58},"security","q3Gc5sYSFErkz8wMF6cjMHd8CxxFA17BlZQJurl4SbI",{"id":3870,"title":3395,"body":3871,"description":3879,"extension":3437,"icon":1589,"meta":4349,"navigation":1644,"path":4350,"seo":4351,"stem":3394,"__hash__":4352},"resources/contributing.md",{"type":1494,"value":3872,"toc":4325},[3873,3877,3880,3884,3887,3891,3935,3939,3943,3961,3964,3980,3982,3993,3997,4001,4015,4019,4030,4034,4037,4051,4055,4083,4086,4089,4102,4105,4117,4120,4132,4135,4147,4150,4158,4162,4165,4209,4213,4217,4220,4231,4234,4248,4252,4280,4286,4290,4293,4304,4308,4319,4322],[1497,3874,3876],{"id":3875},"contributing-to-flux","Contributing to flux",[1501,3878,3879],{},"Thank you for your interest in contributing to flux! This guide will help you get started.",[1575,3881,3883],{"id":3882},"code-of-conduct","Code of Conduct",[1501,3885,3886],{},"By participating in this project, you agree to maintain a respectful and inclusive environment for all contributors.",[1575,3888,3890],{"id":3889},"getting-started","Getting Started",[3519,3892,3893,3896,3902,3908,3911,3917,3923,3926,3932],{},[3042,3894,3895],{},"Fork the repository",[3042,3897,3898,3899],{},"Clone your fork: ",[1589,3900,3901],{},"git clone https://github.com/yourusername/flux.git",[3042,3903,3904,3905],{},"Create a feature branch: ",[1589,3906,3907],{},"git checkout -b feature/your-feature-name",[3042,3909,3910],{},"Make your changes",[3042,3912,3913,3914],{},"Run tests: ",[1589,3915,3916],{},"make test",[3042,3918,3919,3920],{},"Run linters: ",[1589,3921,3922],{},"make lint",[3042,3924,3925],{},"Commit your changes with a descriptive message",[3042,3927,3928,3929],{},"Push to your fork: ",[1589,3930,3931],{},"git push origin feature/your-feature-name",[3042,3933,3934],{},"Create a Pull Request",[1575,3936,3938],{"id":3937},"development-guidelines","Development Guidelines",[3300,3940,3942],{"id":3941},"code-style","Code Style",[3039,3944,3945,3948,3955,3958],{},[3042,3946,3947],{},"Follow standard Go conventions",[3042,3949,3950,3951,3954],{},"Run ",[1589,3952,3953],{},"go fmt"," before committing",[3042,3956,3957],{},"Add comments for exported functions and types",[3042,3959,3960],{},"Keep functions small and focused",[3300,3962,292],{"id":3963},"testing",[3039,3965,3966,3969,3974,3977],{},[3042,3967,3968],{},"Write tests for new functionality",[3042,3970,3971,3972],{},"Ensure all tests pass: ",[1589,3973,3916],{},[3042,3975,3976],{},"Maintain 1:1 file-to-test ratio",[3042,3978,3979],{},"Aim for 80%+ test coverage",[3300,3981,3290],{"id":3289},[3039,3983,3984,3987,3990],{},[3042,3985,3986],{},"Update README.md for API changes",[3042,3988,3989],{},"Add comments to all exported types",[3042,3991,3992],{},"Keep doc comments clear and concise",[1575,3994,3996],{"id":3995},"types-of-contributions","Types of Contributions",[3300,3998,4000],{"id":3999},"bug-reports","Bug Reports",[3039,4002,4003,4006,4009,4012],{},[3042,4004,4005],{},"Use GitHub Issues",[3042,4007,4008],{},"Include minimal reproduction code",[3042,4010,4011],{},"Describe expected vs actual behavior",[3042,4013,4014],{},"Include Go version and OS",[3300,4016,4018],{"id":4017},"feature-requests","Feature Requests",[3039,4020,4021,4024,4027],{},[3042,4022,4023],{},"Open an issue for discussion first",[3042,4025,4026],{},"Explain the use case",[3042,4028,4029],{},"Consider backwards compatibility",[3300,4031,4033],{"id":4032},"code-contributions","Code Contributions",[1501,4035,4036],{},"All contributions should:",[3039,4038,4039,4042,4045,4048],{},[3042,4040,4041],{},"Include comprehensive tests",[3042,4043,4044],{},"Pass linter checks",[3042,4046,4047],{},"Maintain existing code style",[3042,4049,4050],{},"Update documentation as needed",[1575,4052,4054],{"id":4053},"pull-request-process","Pull Request Process",[3519,4056,4057,4063,4068,4073,4078],{},[3042,4058,4059,4062],{},[3045,4060,4061],{},"Keep PRs focused"," - One feature/fix per PR",[3042,4064,4065],{},[3045,4066,4067],{},"Write descriptive commit messages",[3042,4069,4070],{},[3045,4071,4072],{},"Update tests and documentation",[3042,4074,4075],{},[3045,4076,4077],{},"Ensure CI passes",[3042,4079,4080],{},[3045,4081,4082],{},"Respond to review feedback",[1575,4084,292],{"id":4085},"testing-1",[1501,4087,4088],{},"Run the full test suite:",[1583,4090,4092],{"className":2305,"code":4091,"language":2307,"meta":58,"style":58},"make test\n",[1589,4093,4094],{"__ignoreMap":58},[1592,4095,4096,4099],{"class":1594,"line":9},[1592,4097,4098],{"class":1667},"make",[1592,4100,4101],{"class":1621}," test\n",[1501,4103,4104],{},"Run with coverage:",[1583,4106,4108],{"className":2305,"code":4107,"language":2307,"meta":58,"style":58},"make coverage\n",[1589,4109,4110],{"__ignoreMap":58},[1592,4111,4112,4114],{"class":1594,"line":9},[1592,4113,4098],{"class":1667},[1592,4115,4116],{"class":1621}," coverage\n",[1501,4118,4119],{},"Run linters:",[1583,4121,4123],{"className":2305,"code":4122,"language":2307,"meta":58,"style":58},"make lint\n",[1589,4124,4125],{"__ignoreMap":58},[1592,4126,4127,4129],{"class":1594,"line":9},[1592,4128,4098],{"class":1667},[1592,4130,4131],{"class":1621}," lint\n",[1501,4133,4134],{},"Run full CI simulation:",[1583,4136,4138],{"className":2305,"code":4137,"language":2307,"meta":58,"style":58},"make ci\n",[1589,4139,4140],{"__ignoreMap":58},[1592,4141,4142,4144],{"class":1594,"line":9},[1592,4143,4098],{"class":1667},[1592,4145,4146],{"class":1621}," ci\n",[1575,4148,636],{"id":4149},"project-structure",[1583,4151,4156],{"className":4152,"code":4154,"language":4155},[4153],"language-text","flux/\n├── *.go              # Core library files\n├── *_test.go         # Tests (1:1 with source files)\n├── .github/          # GitHub workflows and templates\n├── docs/             # Documentation\n├── examples/         # Example applications\n├── testing/          # Integration tests and benchmarks\n├── README.md         # Project documentation\n└── Makefile          # Build and test commands\n","text",[1589,4157,4154],{"__ignoreMap":58},[1575,4159,4161],{"id":4160},"commit-messages","Commit Messages",[1501,4163,4164],{},"Follow conventional commits:",[3039,4166,4167,4173,4179,4185,4191,4197,4203],{},[3042,4168,4169,4172],{},[1589,4170,4171],{},"feat:"," New feature",[3042,4174,4175,4178],{},[1589,4176,4177],{},"fix:"," Bug fix",[3042,4180,4181,4184],{},[1589,4182,4183],{},"docs:"," Documentation changes",[3042,4186,4187,4190],{},[1589,4188,4189],{},"test:"," Test additions/changes",[3042,4192,4193,4196],{},[1589,4194,4195],{},"refactor:"," Code refactoring",[3042,4198,4199,4202],{},[1589,4200,4201],{},"perf:"," Performance improvements",[3042,4204,4205,4208],{},[1589,4206,4207],{},"chore:"," Maintenance tasks",[1575,4210,4212],{"id":4211},"release-process","Release Process",[3300,4214,4216],{"id":4215},"automated-releases","Automated Releases",[1501,4218,4219],{},"This project uses automated release versioning. To create a release:",[3519,4221,4222,4225,4228],{},[3042,4223,4224],{},"Go to Actions > Release > Run workflow",[3042,4226,4227],{},"Leave \"Version override\" empty for automatic version inference",[3042,4229,4230],{},"Click \"Run workflow\"",[1501,4232,4233],{},"The system will:",[3039,4235,4236,4239,4242,4245],{},[3042,4237,4238],{},"Automatically determine the next version from conventional commits",[3042,4240,4241],{},"Create a git tag",[3042,4243,4244],{},"Generate release notes via GoReleaser",[3042,4246,4247],{},"Publish the release to GitHub",[3300,4249,4251],{"id":4250},"commit-conventions-for-versioning","Commit Conventions for Versioning",[3039,4253,4254,4259,4264,4270],{},[3042,4255,4256,4258],{},[1589,4257,4171],{}," new features (minor version: 1.2.0 > 1.3.0)",[3042,4260,4261,4263],{},[1589,4262,4177],{}," bug fixes (patch version: 1.2.0 > 1.2.1)",[3042,4265,4266,4269],{},[1589,4267,4268],{},"feat!:"," breaking changes (major version: 1.2.0 > 2.0.0)",[3042,4271,4272,4274,4275,4274,4277,4279],{},[1589,4273,4183],{},", ",[1589,4276,4189],{},[1589,4278,4207],{}," no version change",[1501,4281,4282,4283],{},"Example: ",[1589,4284,4285],{},"feat(capacitor): add retry option",[3300,4287,4289],{"id":4288},"version-preview-on-pull-requests","Version Preview on Pull Requests",[1501,4291,4292],{},"Every PR automatically shows the next version that will be created:",[3039,4294,4295,4298,4301],{},[3042,4296,4297],{},"Check PR comments for \"Version Preview\"",[3042,4299,4300],{},"Updates automatically as you add commits",[3042,4302,4303],{},"Helps verify your commits have the intended effect",[1575,4305,4307],{"id":4306},"questions","Questions?",[3039,4309,4310,4313,4316],{},[3042,4311,4312],{},"Open an issue for questions",[3042,4314,4315],{},"Check existing issues first",[3042,4317,4318],{},"Be patient and respectful",[1501,4320,4321],{},"Thank you for contributing to flux!",[3418,4323,4324],{},"html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":58,"searchDepth":19,"depth":19,"links":4326},[4327,4328,4329,4334,4339,4340,4341,4342,4343,4348],{"id":3882,"depth":19,"text":3883},{"id":3889,"depth":19,"text":3890},{"id":3937,"depth":19,"text":3938,"children":4330},[4331,4332,4333],{"id":3941,"depth":131,"text":3942},{"id":3963,"depth":131,"text":292},{"id":3289,"depth":131,"text":3290},{"id":3995,"depth":19,"text":3996,"children":4335},[4336,4337,4338],{"id":3999,"depth":131,"text":4000},{"id":4017,"depth":131,"text":4018},{"id":4032,"depth":131,"text":4033},{"id":4053,"depth":19,"text":4054},{"id":4085,"depth":19,"text":292},{"id":4149,"depth":19,"text":636},{"id":4160,"depth":19,"text":4161},{"id":4211,"depth":19,"text":4212,"children":4344},[4345,4346,4347],{"id":4215,"depth":131,"text":4216},{"id":4250,"depth":131,"text":4251},{"id":4288,"depth":131,"text":4289},{"id":4306,"depth":19,"text":4307},{},"/contributing",{"title":3395,"description":3879},"xTR1eV3-iDK4IuFNWXMXjU-jZiFoGx687_-1DHDmDmE",{"id":4354,"title":730,"author":4355,"body":4356,"description":732,"extension":3437,"meta":8024,"navigation":1644,"path":729,"published":8025,"readtime":8026,"seo":8027,"stem":1452,"tags":8028,"updated":8025,"__hash__":8030},"flux/v1.0.2/4.cookbook/3.custom-watcher.md","zoobzio",{"type":1494,"value":4357,"toc":8006},[4358,4361,4365,4368,4425,4428,4454,4457,4460,5541,5544,5760,5763,5766,6584,6587,6590,7042,7045,7048,7051,7102,7105,7108,7257,7260,7263,7312,7315,7618,7621,7941,7944,7950,7985,7988,8003],[1497,4359,730],{"id":4360},"custom-watcher",[1501,4362,4363],{},[3834,4364,736],{},[1575,4366,739],{"id":4367},"the-interface",[1583,4369,4371],{"className":1585,"code":4370,"language":1587,"meta":58,"style":58},"type Watcher interface {\n    Watch(ctx context.Context) (\u003C-chan []byte, error)\n}\n",[1589,4372,4373,4385,4421],{"__ignoreMap":58},[1592,4374,4375,4377,4380,4383],{"class":1594,"line":9},[1592,4376,1598],{"class":1597},[1592,4378,4379],{"class":1601}," Watcher",[1592,4381,4382],{"class":1597}," interface",[1592,4384,1609],{"class":1608},[1592,4386,4387,4390,4392,4394,4396,4398,4400,4402,4404,4407,4410,4412,4415,4417,4419],{"class":1594,"line":19},[1592,4388,4389],{"class":1667},"    Watch",[1592,4391,1733],{"class":1608},[1592,4393,1817],{"class":1657},[1592,4395,1820],{"class":1601},[1592,4397,1690],{"class":1608},[1592,4399,1825],{"class":1601},[1592,4401,1664],{"class":1608},[1592,4403,1654],{"class":1608},[1592,4405,4406],{"class":1682},"\u003C-",[1592,4408,4409],{"class":1597},"chan",[1592,4411,2181],{"class":1608},[1592,4413,4414],{"class":1601},"byte",[1592,4416,1828],{"class":1608},[1592,4418,1674],{"class":1601},[1592,4420,1739],{"class":1608},[1592,4422,4423],{"class":1594,"line":131},[1592,4424,1638],{"class":1608},[1575,4426,61],{"id":4427},"requirements",[3519,4429,4430,4436,4442,4448],{},[3042,4431,4432,4435],{},[3045,4433,4434],{},"Emit current value immediately"," - First emission enables initial load",[3042,4437,4438,4441],{},[3045,4439,4440],{},"Emit on change"," - Subsequent emissions trigger the pipeline",[3042,4443,4444,4447],{},[3045,4445,4446],{},"Close channel on shutdown"," - When context is cancelled",[3042,4449,4450,4453],{},[3045,4451,4452],{},"Return error if watching cannot start"," - Invalid config, connection failure",[1575,4455,748],{"id":4456},"example-http-polling-watcher",[1501,4458,4459],{},"Poll an HTTP endpoint at intervals:",[1583,4461,4463],{"className":1585,"code":4462,"language":1587,"meta":58,"style":58},"package httpconfig\n\nimport (\n    \"bytes\"\n    \"context\"\n    \"fmt\"\n    \"io\"\n    \"net/http\"\n    \"time\"\n)\n\ntype Watcher struct {\n    url      string\n    interval time.Duration\n    client   *http.Client\n}\n\nfunc New(url string, interval time.Duration) *Watcher {\n    return &Watcher{\n        url:      url,\n        interval: interval,\n        client:   &http.Client{Timeout: 10 * time.Second},\n    }\n}\n\nfunc (w *Watcher) Watch(ctx context.Context) (\u003C-chan []byte, error) {\n    // Fetch initial value to validate endpoint\n    initial, err := w.fetch(ctx)\n    if err != nil {\n        return nil, fmt.Errorf(\"initial fetch failed: %w\", err)\n    }\n\n    out := make(chan []byte)\n\n    go func() {\n        defer close(out)\n\n        // Emit initial value\n        select {\n        case out \u003C- initial:\n        case \u003C-ctx.Done():\n            return\n        }\n\n        current := initial\n        ticker := time.NewTicker(w.interval)\n        defer ticker.Stop()\n\n        for {\n            select {\n            case \u003C-ctx.Done():\n                return\n            case \u003C-ticker.C:\n                next, err := w.fetch(ctx)\n                if err != nil {\n                    continue // Skip failed fetches\n                }\n                if !bytes.Equal(current, next) {\n                    current = next\n                    select {\n                    case out \u003C- current:\n                    case \u003C-ctx.Done():\n                        return\n                    }\n                }\n            }\n        }\n    }()\n\n    return out, nil\n}\n\nfunc (w *Watcher) fetch(ctx context.Context) ([]byte, error) {\n    req, err := http.NewRequestWithContext(ctx, \"GET\", w.url, nil)\n    if err != nil {\n        return nil, err\n    }\n\n    resp, err := w.client.Do(req)\n    if err != nil {\n        return nil, err\n    }\n    defer resp.Body.Close()\n\n    if resp.StatusCode != http.StatusOK {\n        return nil, fmt.Errorf(\"unexpected status: %d\", resp.StatusCode)\n    }\n\n    return io.ReadAll(resp.Body)\n}\n",[1589,4464,4465,4472,4476,4482,4487,4491,4496,4501,4506,4511,4515,4519,4529,4537,4550,4566,4570,4574,4609,4619,4631,4642,4682,4686,4690,4694,4743,4748,4773,4785,4817,4821,4825,4846,4850,4862,4877,4881,4886,4893,4910,4925,4930,4935,4939,4949,4975,4989,4993,5000,5007,5023,5029,5046,5070,5084,5093,5099,5129,5141,5149,5164,5179,5185,5191,5196,5202,5207,5213,5218,5229,5234,5239,5282,5325,5338,5350,5355,5360,5390,5403,5414,5419,5440,5445,5468,5502,5507,5512,5536],{"__ignoreMap":58},[1592,4466,4467,4469],{"class":1594,"line":9},[1592,4468,2336],{"class":1597},[1592,4470,4471],{"class":1601}," httpconfig\n",[1592,4473,4474],{"class":1594,"line":19},[1592,4475,1645],{"emptyLinePlaceholder":1644},[1592,4477,4478,4480],{"class":1594,"line":131},[1592,4479,2348],{"class":1597},[1592,4481,2352],{"class":2351},[1592,4483,4484],{"class":1594,"line":840},[1592,4485,4486],{"class":1621},"    \"bytes\"\n",[1592,4488,4489],{"class":1594,"line":1641},[1592,4490,2357],{"class":1621},[1592,4492,4493],{"class":1594,"line":1648},[1592,4494,4495],{"class":1621},"    \"fmt\"\n",[1592,4497,4498],{"class":1594,"line":1679},[1592,4499,4500],{"class":1621},"    \"io\"\n",[1592,4502,4503],{"class":1594,"line":1720},[1592,4504,4505],{"class":1621},"    \"net/http\"\n",[1592,4507,4508],{"class":1594,"line":1742},[1592,4509,4510],{"class":1621},"    \"time\"\n",[1592,4512,4513],{"class":1594,"line":1748},[1592,4514,1739],{"class":2351},[1592,4516,4517],{"class":1594,"line":1757},[1592,4518,1645],{"emptyLinePlaceholder":1644},[1592,4520,4521,4523,4525,4527],{"class":1594,"line":1762},[1592,4522,1598],{"class":1597},[1592,4524,4379],{"class":1601},[1592,4526,1605],{"class":1597},[1592,4528,1609],{"class":1608},[1592,4530,4531,4534],{"class":1594,"line":1767},[1592,4532,4533],{"class":1614},"    url",[1592,4535,4536],{"class":1601},"      string\n",[1592,4538,4539,4542,4545,4547],{"class":1594,"line":1791},[1592,4540,4541],{"class":1614},"    interval",[1592,4543,4544],{"class":1601}," time",[1592,4546,1690],{"class":1608},[1592,4548,4549],{"class":1601},"Duration\n",[1592,4551,4552,4555,4558,4561,4563],{"class":1594,"line":1809},[1592,4553,4554],{"class":1614},"    client",[1592,4556,4557],{"class":1682},"   *",[1592,4559,4560],{"class":1601},"http",[1592,4562,1690],{"class":1608},[1592,4564,4565],{"class":1601},"Client\n",[1592,4567,4568],{"class":1594,"line":1847},[1592,4569,1638],{"class":1608},[1592,4571,4572],{"class":1594,"line":1893},[1592,4573,1645],{"emptyLinePlaceholder":1644},[1592,4575,4576,4578,4581,4583,4586,4588,4590,4593,4595,4597,4600,4602,4605,4607],{"class":1594,"line":1908},[1592,4577,1651],{"class":1597},[1592,4579,4580],{"class":1667}," New",[1592,4582,1733],{"class":1608},[1592,4584,4585],{"class":1657},"url",[1592,4587,1630],{"class":1601},[1592,4589,1828],{"class":1608},[1592,4591,4592],{"class":1657}," interval",[1592,4594,4544],{"class":1601},[1592,4596,1690],{"class":1608},[1592,4598,4599],{"class":1601},"Duration",[1592,4601,1664],{"class":1608},[1592,4603,4604],{"class":1682}," *",[1592,4606,139],{"class":1601},[1592,4608,1609],{"class":1608},[1592,4610,4611,4613,4615,4617],{"class":1594,"line":1914},[1592,4612,1751],{"class":1682},[1592,4614,3115],{"class":1682},[1592,4616,139],{"class":1601},[1592,4618,3126],{"class":1608},[1592,4620,4621,4624,4626,4629],{"class":1594,"line":2491},[1592,4622,4623],{"class":1614},"        url",[1592,4625,2851],{"class":1608},[1592,4627,4628],{"class":1686},"      url",[1592,4630,3161],{"class":1608},[1592,4632,4633,4636,4638,4640],{"class":1594,"line":2496},[1592,4634,4635],{"class":1614},"        interval",[1592,4637,2851],{"class":1608},[1592,4639,4592],{"class":1686},[1592,4641,3161],{"class":1608},[1592,4643,4644,4647,4649,4652,4654,4656,4659,4662,4665,4667,4670,4672,4674,4676,4679],{"class":1594,"line":2515},[1592,4645,4646],{"class":1614},"        client",[1592,4648,2851],{"class":1608},[1592,4650,4651],{"class":1682},"   &",[1592,4653,4560],{"class":1601},[1592,4655,1690],{"class":1608},[1592,4657,4658],{"class":1601},"Client",[1592,4660,4661],{"class":1608},"{",[1592,4663,4664],{"class":1614},"Timeout",[1592,4666,2851],{"class":1608},[1592,4668,4669],{"class":1699}," 10",[1592,4671,4604],{"class":1686},[1592,4673,4544],{"class":1686},[1592,4675,1690],{"class":1608},[1592,4677,4678],{"class":1686},"Second",[1592,4680,4681],{"class":1608},"},\n",[1592,4683,4684],{"class":1594,"line":2533},[1592,4685,1745],{"class":1608},[1592,4687,4688],{"class":1594,"line":2538},[1592,4689,1638],{"class":1608},[1592,4691,4692],{"class":1594,"line":2545},[1592,4693,1645],{"emptyLinePlaceholder":1644},[1592,4695,4696,4698,4700,4703,4706,4708,4710,4713,4715,4717,4719,4721,4723,4725,4727,4729,4731,4733,4735,4737,4739,4741],{"class":1594,"line":2550},[1592,4697,1651],{"class":1597},[1592,4699,1654],{"class":1608},[1592,4701,4702],{"class":1657},"w ",[1592,4704,4705],{"class":1682},"*",[1592,4707,139],{"class":1601},[1592,4709,1664],{"class":1608},[1592,4711,4712],{"class":1667}," Watch",[1592,4714,1733],{"class":1608},[1592,4716,1817],{"class":1657},[1592,4718,1820],{"class":1601},[1592,4720,1690],{"class":1608},[1592,4722,1825],{"class":1601},[1592,4724,1664],{"class":1608},[1592,4726,1654],{"class":1608},[1592,4728,4406],{"class":1682},[1592,4730,4409],{"class":1597},[1592,4732,2181],{"class":1608},[1592,4734,4414],{"class":1601},[1592,4736,1828],{"class":1608},[1592,4738,1674],{"class":1601},[1592,4740,1664],{"class":1608},[1592,4742,1609],{"class":1608},[1592,4744,4745],{"class":1594,"line":2555},[1592,4746,4747],{"class":1992},"    // Fetch initial value to validate endpoint\n",[1592,4749,4750,4753,4755,4757,4759,4762,4764,4767,4769,4771],{"class":1594,"line":2567},[1592,4751,4752],{"class":1686},"    initial",[1592,4754,1828],{"class":1608},[1592,4756,1932],{"class":1686},[1592,4758,1773],{"class":1686},[1592,4760,4761],{"class":1686}," w",[1592,4763,1690],{"class":1608},[1592,4765,4766],{"class":1667},"fetch",[1592,4768,1733],{"class":1608},[1592,4770,1817],{"class":1686},[1592,4772,1739],{"class":1608},[1592,4774,4775,4777,4779,4781,4783],{"class":1594,"line":2585},[1592,4776,1683],{"class":1682},[1592,4778,1932],{"class":1686},[1592,4780,1953],{"class":1682},[1592,4782,1956],{"class":1597},[1592,4784,1609],{"class":1608},[1592,4786,4787,4789,4791,4793,4796,4798,4801,4803,4806,4809,4811,4813,4815],{"class":1594,"line":2590},[1592,4788,1723],{"class":1682},[1592,4790,1956],{"class":1597},[1592,4792,1828],{"class":1608},[1592,4794,4795],{"class":1686}," fmt",[1592,4797,1690],{"class":1608},[1592,4799,4800],{"class":1667},"Errorf",[1592,4802,1733],{"class":1608},[1592,4804,4805],{"class":1621},"\"initial fetch failed: ",[1592,4807,4808],{"class":1863},"%w",[1592,4810,1872],{"class":1621},[1592,4812,1828],{"class":1608},[1592,4814,1932],{"class":1686},[1592,4816,1739],{"class":1608},[1592,4818,4819],{"class":1594,"line":2610},[1592,4820,1745],{"class":1608},[1592,4822,4823],{"class":1594,"line":2627},[1592,4824,1645],{"emptyLinePlaceholder":1644},[1592,4826,4827,4830,4832,4836,4838,4840,4842,4844],{"class":1594,"line":2659},[1592,4828,4829],{"class":1686},"    out",[1592,4831,1773],{"class":1686},[1592,4833,4835],{"class":4834},"skxcq"," make",[1592,4837,1733],{"class":1608},[1592,4839,4409],{"class":1597},[1592,4841,2181],{"class":1608},[1592,4843,4414],{"class":1601},[1592,4845,1739],{"class":1608},[1592,4847,4848],{"class":1594,"line":2685},[1592,4849,1645],{"emptyLinePlaceholder":1644},[1592,4851,4852,4855,4858,4860],{"class":1594,"line":2693},[1592,4853,4854],{"class":1682},"    go",[1592,4856,4857],{"class":1597}," func",[1592,4859,1671],{"class":1608},[1592,4861,1609],{"class":1608},[1592,4863,4864,4867,4870,4872,4875],{"class":1594,"line":2699},[1592,4865,4866],{"class":1682},"        defer",[1592,4868,4869],{"class":4834}," close",[1592,4871,1733],{"class":1608},[1592,4873,4874],{"class":1686},"out",[1592,4876,1739],{"class":1608},[1592,4878,4879],{"class":1594,"line":2705},[1592,4880,1645],{"emptyLinePlaceholder":1644},[1592,4882,4883],{"class":1594,"line":2710},[1592,4884,4885],{"class":1992},"        // Emit initial value\n",[1592,4887,4888,4891],{"class":1594,"line":2739},[1592,4889,4890],{"class":1682},"        select",[1592,4892,1609],{"class":1608},[1592,4894,4895,4898,4901,4904,4907],{"class":1594,"line":2765},[1592,4896,4897],{"class":1682},"        case",[1592,4899,4900],{"class":1686}," out",[1592,4902,4903],{"class":1682}," \u003C-",[1592,4905,4906],{"class":1686}," initial",[1592,4908,4909],{"class":1608},":\n",[1592,4911,4912,4914,4916,4918,4920,4922],{"class":1594,"line":2770},[1592,4913,4897],{"class":1682},[1592,4915,4903],{"class":1682},[1592,4917,1817],{"class":1686},[1592,4919,1690],{"class":1608},[1592,4921,2902],{"class":1667},[1592,4923,4924],{"class":1608},"():\n",[1592,4926,4927],{"class":1594,"line":2775},[1592,4928,4929],{"class":1682},"            return\n",[1592,4931,4932],{"class":1594,"line":2805},[1592,4933,4934],{"class":1608},"        }\n",[1592,4936,4937],{"class":1594,"line":2810},[1592,4938,1645],{"emptyLinePlaceholder":1644},[1592,4940,4941,4944,4946],{"class":1594,"line":2835},[1592,4942,4943],{"class":1686},"        current",[1592,4945,1773],{"class":1686},[1592,4947,4948],{"class":1686}," initial\n",[1592,4950,4951,4954,4956,4958,4960,4963,4965,4968,4970,4973],{"class":1594,"line":2876},[1592,4952,4953],{"class":1686},"        ticker",[1592,4955,1773],{"class":1686},[1592,4957,4544],{"class":1686},[1592,4959,1690],{"class":1608},[1592,4961,4962],{"class":1667},"NewTicker",[1592,4964,1733],{"class":1608},[1592,4966,4967],{"class":1686},"w",[1592,4969,1690],{"class":1608},[1592,4971,4972],{"class":1686},"interval",[1592,4974,1739],{"class":1608},[1592,4976,4977,4979,4982,4984,4987],{"class":1594,"line":2881},[1592,4978,4866],{"class":1682},[1592,4980,4981],{"class":1686}," ticker",[1592,4983,1690],{"class":1608},[1592,4985,4986],{"class":1667},"Stop",[1592,4988,2582],{"class":1608},[1592,4990,4991],{"class":1594,"line":2886},[1592,4992,1645],{"emptyLinePlaceholder":1644},[1592,4994,4995,4998],{"class":1594,"line":2892},[1592,4996,4997],{"class":1682},"        for",[1592,4999,1609],{"class":1608},[1592,5001,5002,5005],{"class":1594,"line":2907},[1592,5003,5004],{"class":1682},"            select",[1592,5006,1609],{"class":1608},[1592,5008,5010,5013,5015,5017,5019,5021],{"class":1594,"line":5009},51,[1592,5011,5012],{"class":1682},"            case",[1592,5014,4903],{"class":1682},[1592,5016,1817],{"class":1686},[1592,5018,1690],{"class":1608},[1592,5020,2902],{"class":1667},[1592,5022,4924],{"class":1608},[1592,5024,5026],{"class":1594,"line":5025},52,[1592,5027,5028],{"class":1682},"                return\n",[1592,5030,5032,5034,5036,5039,5041,5044],{"class":1594,"line":5031},53,[1592,5033,5012],{"class":1682},[1592,5035,4903],{"class":1682},[1592,5037,5038],{"class":1686},"ticker",[1592,5040,1690],{"class":1608},[1592,5042,5043],{"class":1686},"C",[1592,5045,4909],{"class":1608},[1592,5047,5049,5052,5054,5056,5058,5060,5062,5064,5066,5068],{"class":1594,"line":5048},54,[1592,5050,5051],{"class":1686},"                next",[1592,5053,1828],{"class":1608},[1592,5055,1932],{"class":1686},[1592,5057,1773],{"class":1686},[1592,5059,4761],{"class":1686},[1592,5061,1690],{"class":1608},[1592,5063,4766],{"class":1667},[1592,5065,1733],{"class":1608},[1592,5067,1817],{"class":1686},[1592,5069,1739],{"class":1608},[1592,5071,5073,5076,5078,5080,5082],{"class":1594,"line":5072},55,[1592,5074,5075],{"class":1682},"                if",[1592,5077,1932],{"class":1686},[1592,5079,1953],{"class":1682},[1592,5081,1956],{"class":1597},[1592,5083,1609],{"class":1608},[1592,5085,5087,5090],{"class":1594,"line":5086},56,[1592,5088,5089],{"class":1682},"                    continue",[1592,5091,5092],{"class":1992}," // Skip failed fetches\n",[1592,5094,5096],{"class":1594,"line":5095},57,[1592,5097,5098],{"class":1608},"                }\n",[1592,5100,5102,5104,5107,5110,5112,5115,5117,5120,5122,5125,5127],{"class":1594,"line":5101},58,[1592,5103,5075],{"class":1682},[1592,5105,5106],{"class":1682}," !",[1592,5108,5109],{"class":1686},"bytes",[1592,5111,1690],{"class":1608},[1592,5113,5114],{"class":1667},"Equal",[1592,5116,1733],{"class":1608},[1592,5118,5119],{"class":1686},"current",[1592,5121,1828],{"class":1608},[1592,5123,5124],{"class":1686}," next",[1592,5126,1664],{"class":1608},[1592,5128,1609],{"class":1608},[1592,5130,5132,5135,5138],{"class":1594,"line":5131},59,[1592,5133,5134],{"class":1686},"                    current",[1592,5136,5137],{"class":1686}," =",[1592,5139,5140],{"class":1686}," next\n",[1592,5142,5144,5147],{"class":1594,"line":5143},60,[1592,5145,5146],{"class":1682},"                    select",[1592,5148,1609],{"class":1608},[1592,5150,5152,5155,5157,5159,5162],{"class":1594,"line":5151},61,[1592,5153,5154],{"class":1682},"                    case",[1592,5156,4900],{"class":1686},[1592,5158,4903],{"class":1682},[1592,5160,5161],{"class":1686}," current",[1592,5163,4909],{"class":1608},[1592,5165,5167,5169,5171,5173,5175,5177],{"class":1594,"line":5166},62,[1592,5168,5154],{"class":1682},[1592,5170,4903],{"class":1682},[1592,5172,1817],{"class":1686},[1592,5174,1690],{"class":1608},[1592,5176,2902],{"class":1667},[1592,5178,4924],{"class":1608},[1592,5180,5182],{"class":1594,"line":5181},63,[1592,5183,5184],{"class":1682},"                        return\n",[1592,5186,5188],{"class":1594,"line":5187},64,[1592,5189,5190],{"class":1608},"                    }\n",[1592,5192,5194],{"class":1594,"line":5193},65,[1592,5195,5098],{"class":1608},[1592,5197,5199],{"class":1594,"line":5198},66,[1592,5200,5201],{"class":1608},"            }\n",[1592,5203,5205],{"class":1594,"line":5204},67,[1592,5206,4934],{"class":1608},[1592,5208,5210],{"class":1594,"line":5209},68,[1592,5211,5212],{"class":1608},"    }()\n",[1592,5214,5216],{"class":1594,"line":5215},69,[1592,5217,1645],{"emptyLinePlaceholder":1644},[1592,5219,5221,5223,5225,5227],{"class":1594,"line":5220},70,[1592,5222,1751],{"class":1682},[1592,5224,4900],{"class":1686},[1592,5226,1828],{"class":1608},[1592,5228,1754],{"class":1597},[1592,5230,5232],{"class":1594,"line":5231},71,[1592,5233,1638],{"class":1608},[1592,5235,5237],{"class":1594,"line":5236},72,[1592,5238,1645],{"emptyLinePlaceholder":1644},[1592,5240,5242,5244,5246,5248,5250,5252,5254,5257,5259,5261,5263,5265,5267,5269,5272,5274,5276,5278,5280],{"class":1594,"line":5241},73,[1592,5243,1651],{"class":1597},[1592,5245,1654],{"class":1608},[1592,5247,4702],{"class":1657},[1592,5249,4705],{"class":1682},[1592,5251,139],{"class":1601},[1592,5253,1664],{"class":1608},[1592,5255,5256],{"class":1667}," fetch",[1592,5258,1733],{"class":1608},[1592,5260,1817],{"class":1657},[1592,5262,1820],{"class":1601},[1592,5264,1690],{"class":1608},[1592,5266,1825],{"class":1601},[1592,5268,1664],{"class":1608},[1592,5270,5271],{"class":1608}," ([]",[1592,5273,4414],{"class":1601},[1592,5275,1828],{"class":1608},[1592,5277,1674],{"class":1601},[1592,5279,1664],{"class":1608},[1592,5281,1609],{"class":1608},[1592,5283,5285,5288,5290,5292,5294,5297,5299,5302,5304,5306,5308,5311,5313,5315,5317,5319,5321,5323],{"class":1594,"line":5284},74,[1592,5286,5287],{"class":1686},"    req",[1592,5289,1828],{"class":1608},[1592,5291,1932],{"class":1686},[1592,5293,1773],{"class":1686},[1592,5295,5296],{"class":1686}," http",[1592,5298,1690],{"class":1608},[1592,5300,5301],{"class":1667},"NewRequestWithContext",[1592,5303,1733],{"class":1608},[1592,5305,1817],{"class":1686},[1592,5307,1828],{"class":1608},[1592,5309,5310],{"class":1621}," \"GET\"",[1592,5312,1828],{"class":1608},[1592,5314,4761],{"class":1686},[1592,5316,1690],{"class":1608},[1592,5318,4585],{"class":1686},[1592,5320,1828],{"class":1608},[1592,5322,1956],{"class":1597},[1592,5324,1739],{"class":1608},[1592,5326,5328,5330,5332,5334,5336],{"class":1594,"line":5327},75,[1592,5329,1683],{"class":1682},[1592,5331,1932],{"class":1686},[1592,5333,1953],{"class":1682},[1592,5335,1956],{"class":1597},[1592,5337,1609],{"class":1608},[1592,5339,5341,5343,5345,5347],{"class":1594,"line":5340},76,[1592,5342,1723],{"class":1682},[1592,5344,1956],{"class":1597},[1592,5346,1828],{"class":1608},[1592,5348,5349],{"class":1686}," err\n",[1592,5351,5353],{"class":1594,"line":5352},77,[1592,5354,1745],{"class":1608},[1592,5356,5358],{"class":1594,"line":5357},78,[1592,5359,1645],{"emptyLinePlaceholder":1644},[1592,5361,5363,5366,5368,5370,5372,5374,5376,5378,5380,5383,5385,5388],{"class":1594,"line":5362},79,[1592,5364,5365],{"class":1686},"    resp",[1592,5367,1828],{"class":1608},[1592,5369,1932],{"class":1686},[1592,5371,1773],{"class":1686},[1592,5373,4761],{"class":1686},[1592,5375,1690],{"class":1608},[1592,5377,3210],{"class":1686},[1592,5379,1690],{"class":1608},[1592,5381,5382],{"class":1667},"Do",[1592,5384,1733],{"class":1608},[1592,5386,5387],{"class":1686},"req",[1592,5389,1739],{"class":1608},[1592,5391,5393,5395,5397,5399,5401],{"class":1594,"line":5392},80,[1592,5394,1683],{"class":1682},[1592,5396,1932],{"class":1686},[1592,5398,1953],{"class":1682},[1592,5400,1956],{"class":1597},[1592,5402,1609],{"class":1608},[1592,5404,5406,5408,5410,5412],{"class":1594,"line":5405},81,[1592,5407,1723],{"class":1682},[1592,5409,1956],{"class":1597},[1592,5411,1828],{"class":1608},[1592,5413,5349],{"class":1686},[1592,5415,5417],{"class":1594,"line":5416},82,[1592,5418,1745],{"class":1608},[1592,5420,5422,5425,5428,5430,5433,5435,5438],{"class":1594,"line":5421},83,[1592,5423,5424],{"class":1682},"    defer",[1592,5426,5427],{"class":1686}," resp",[1592,5429,1690],{"class":1608},[1592,5431,5432],{"class":1686},"Body",[1592,5434,1690],{"class":1608},[1592,5436,5437],{"class":1667},"Close",[1592,5439,2582],{"class":1608},[1592,5441,5443],{"class":1594,"line":5442},84,[1592,5444,1645],{"emptyLinePlaceholder":1644},[1592,5446,5448,5450,5452,5454,5457,5459,5461,5463,5466],{"class":1594,"line":5447},85,[1592,5449,1683],{"class":1682},[1592,5451,5427],{"class":1686},[1592,5453,1690],{"class":1608},[1592,5455,5456],{"class":1686},"StatusCode",[1592,5458,1953],{"class":1682},[1592,5460,5296],{"class":1686},[1592,5462,1690],{"class":1608},[1592,5464,5465],{"class":1686},"StatusOK",[1592,5467,1609],{"class":1608},[1592,5469,5471,5473,5475,5477,5479,5481,5483,5485,5488,5490,5492,5494,5496,5498,5500],{"class":1594,"line":5470},86,[1592,5472,1723],{"class":1682},[1592,5474,1956],{"class":1597},[1592,5476,1828],{"class":1608},[1592,5478,4795],{"class":1686},[1592,5480,1690],{"class":1608},[1592,5482,4800],{"class":1667},[1592,5484,1733],{"class":1608},[1592,5486,5487],{"class":1621},"\"unexpected status: ",[1592,5489,1864],{"class":1863},[1592,5491,1872],{"class":1621},[1592,5493,1828],{"class":1608},[1592,5495,5427],{"class":1686},[1592,5497,1690],{"class":1608},[1592,5499,5456],{"class":1686},[1592,5501,1739],{"class":1608},[1592,5503,5505],{"class":1594,"line":5504},87,[1592,5506,1745],{"class":1608},[1592,5508,5510],{"class":1594,"line":5509},88,[1592,5511,1645],{"emptyLinePlaceholder":1644},[1592,5513,5515,5517,5520,5522,5525,5527,5530,5532,5534],{"class":1594,"line":5514},89,[1592,5516,1751],{"class":1682},[1592,5518,5519],{"class":1686}," io",[1592,5521,1690],{"class":1608},[1592,5523,5524],{"class":1667},"ReadAll",[1592,5526,1733],{"class":1608},[1592,5528,5529],{"class":1686},"resp",[1592,5531,1690],{"class":1608},[1592,5533,5432],{"class":1686},[1592,5535,1739],{"class":1608},[1592,5537,5539],{"class":1594,"line":5538},90,[1592,5540,1638],{"class":1608},[3300,5542,753],{"id":5543},"usage",[1583,5545,5547],{"className":1585,"code":5546,"language":1587,"meta":58,"style":58},"type Config struct {\n    Port int `json:\"port\"`\n}\n\nfunc (c Config) Validate() error {\n    if c.Port \u003C 1 || c.Port > 65535 {\n        return errors.New(\"port must be between 1 and 65535\")\n    }\n    return nil\n}\n\ncapacitor := flux.New[Config](\n    httpconfig.New(\"https://config.example.com/app.json\", 30*time.Second),\n    func(ctx context.Context, prev, curr Config) error {\n        return app.Apply(curr)\n    },\n)\n",[1589,5548,5549,5559,5568,5572,5576,5596,5624,5640,5644,5650,5654,5658,5676,5704,5734,5752,5756],{"__ignoreMap":58},[1592,5550,5551,5553,5555,5557],{"class":1594,"line":9},[1592,5552,1598],{"class":1597},[1592,5554,1602],{"class":1601},[1592,5556,1605],{"class":1597},[1592,5558,1609],{"class":1608},[1592,5560,5561,5563,5565],{"class":1594,"line":19},[1592,5562,1615],{"class":1614},[1592,5564,1618],{"class":1601},[1592,5566,5567],{"class":1621}," `json:\"port\"`\n",[1592,5569,5570],{"class":1594,"line":131},[1592,5571,1638],{"class":1608},[1592,5573,5574],{"class":1594,"line":840},[1592,5575,1645],{"emptyLinePlaceholder":1644},[1592,5577,5578,5580,5582,5584,5586,5588,5590,5592,5594],{"class":1594,"line":1641},[1592,5579,1651],{"class":1597},[1592,5581,1654],{"class":1608},[1592,5583,1658],{"class":1657},[1592,5585,1661],{"class":1601},[1592,5587,1664],{"class":1608},[1592,5589,1668],{"class":1667},[1592,5591,1671],{"class":1608},[1592,5593,1674],{"class":1601},[1592,5595,1609],{"class":1608},[1592,5597,5598,5600,5602,5604,5606,5608,5610,5612,5614,5616,5618,5620,5622],{"class":1594,"line":1648},[1592,5599,1683],{"class":1682},[1592,5601,1687],{"class":1686},[1592,5603,1690],{"class":1608},[1592,5605,1693],{"class":1686},[1592,5607,1696],{"class":1682},[1592,5609,1700],{"class":1699},[1592,5611,1703],{"class":1682},[1592,5613,1687],{"class":1686},[1592,5615,1690],{"class":1608},[1592,5617,1693],{"class":1686},[1592,5619,1712],{"class":1682},[1592,5621,1715],{"class":1699},[1592,5623,1609],{"class":1608},[1592,5625,5626,5628,5630,5632,5634,5636,5638],{"class":1594,"line":1679},[1592,5627,1723],{"class":1682},[1592,5629,1726],{"class":1686},[1592,5631,1690],{"class":1608},[1592,5633,837],{"class":1667},[1592,5635,1733],{"class":1608},[1592,5637,2486],{"class":1621},[1592,5639,1739],{"class":1608},[1592,5641,5642],{"class":1594,"line":1720},[1592,5643,1745],{"class":1608},[1592,5645,5646,5648],{"class":1594,"line":1742},[1592,5647,1751],{"class":1682},[1592,5649,1754],{"class":1597},[1592,5651,5652],{"class":1594,"line":1748},[1592,5653,1638],{"class":1608},[1592,5655,5656],{"class":1594,"line":1757},[1592,5657,1645],{"emptyLinePlaceholder":1644},[1592,5659,5660,5662,5664,5666,5668,5670,5672,5674],{"class":1594,"line":1762},[1592,5661,1770],{"class":1686},[1592,5663,1773],{"class":1686},[1592,5665,1776],{"class":1686},[1592,5667,1690],{"class":1608},[1592,5669,837],{"class":1667},[1592,5671,1783],{"class":1608},[1592,5673,1661],{"class":1601},[1592,5675,1788],{"class":1608},[1592,5677,5678,5681,5683,5685,5687,5690,5692,5695,5698,5700,5702],{"class":1594,"line":1767},[1592,5679,5680],{"class":1686},"    httpconfig",[1592,5682,1690],{"class":1608},[1592,5684,837],{"class":1667},[1592,5686,1733],{"class":1608},[1592,5688,5689],{"class":1621},"\"https://config.example.com/app.json\"",[1592,5691,1828],{"class":1608},[1592,5693,5694],{"class":1699}," 30",[1592,5696,5697],{"class":1686},"*time",[1592,5699,1690],{"class":1608},[1592,5701,4678],{"class":1686},[1592,5703,1806],{"class":1608},[1592,5705,5706,5708,5710,5712,5714,5716,5718,5720,5722,5724,5726,5728,5730,5732],{"class":1594,"line":1791},[1592,5707,1812],{"class":1597},[1592,5709,1733],{"class":1608},[1592,5711,1817],{"class":1657},[1592,5713,1820],{"class":1601},[1592,5715,1690],{"class":1608},[1592,5717,1825],{"class":1601},[1592,5719,1828],{"class":1608},[1592,5721,1831],{"class":1657},[1592,5723,1828],{"class":1608},[1592,5725,1836],{"class":1657},[1592,5727,1602],{"class":1601},[1592,5729,1664],{"class":1608},[1592,5731,1674],{"class":1601},[1592,5733,1609],{"class":1608},[1592,5735,5736,5738,5741,5743,5746,5748,5750],{"class":1594,"line":1809},[1592,5737,1723],{"class":1682},[1592,5739,5740],{"class":1686}," app",[1592,5742,1690],{"class":1608},[1592,5744,5745],{"class":1667},"Apply",[1592,5747,1733],{"class":1608},[1592,5749,1903],{"class":1686},[1592,5751,1739],{"class":1608},[1592,5753,5754],{"class":1594,"line":1847},[1592,5755,1911],{"class":1608},[1592,5757,5758],{"class":1594,"line":1893},[1592,5759,1739],{"class":1608},[1575,5761,758],{"id":5762},"example-aws-parameter-store-watcher",[1501,5764,5765],{},"Watch AWS SSM Parameter Store:",[1583,5767,5769],{"className":1585,"code":5768,"language":1587,"meta":58,"style":58},"package ssm\n\nimport (\n    \"context\"\n    \"time\"\n\n    \"github.com/aws/aws-sdk-go-v2/service/ssm\"\n)\n\ntype Watcher struct {\n    client   *ssm.Client\n    name     string\n    interval time.Duration\n}\n\nfunc New(client *ssm.Client, name string, interval time.Duration) *Watcher {\n    return &Watcher{\n        client:   client,\n        name:     name,\n        interval: interval,\n    }\n}\n\nfunc (w *Watcher) Watch(ctx context.Context) (\u003C-chan []byte, error) {\n    initial, err := w.fetch(ctx)\n    if err != nil {\n        return nil, err\n    }\n\n    out := make(chan []byte)\n\n    go func() {\n        defer close(out)\n\n        select {\n        case out \u003C- initial:\n        case \u003C-ctx.Done():\n            return\n        }\n\n        current := string(initial)\n        ticker := time.NewTicker(w.interval)\n        defer ticker.Stop()\n\n        for {\n            select {\n            case \u003C-ctx.Done():\n                return\n            case \u003C-ticker.C:\n                next, err := w.fetch(ctx)\n                if err != nil {\n                    continue\n                }\n                if string(next) != current {\n                    current = string(next)\n                    select {\n                    case out \u003C- next:\n                    case \u003C-ctx.Done():\n                        return\n                    }\n                }\n            }\n        }\n    }()\n\n    return out, nil\n}\n\nfunc (w *Watcher) fetch(ctx context.Context) ([]byte, error) {\n    out, err := w.client.GetParameter(ctx, &ssm.GetParameterInput{\n        Name:           &w.name,\n        WithDecryption: aws.Bool(true),\n    })\n    if err != nil {\n        return nil, err\n    }\n    return []byte(*out.Parameter.Value), nil\n}\n",[1589,5770,5771,5778,5782,5788,5792,5796,5800,5805,5809,5813,5823,5836,5844,5854,5858,5862,5905,5915,5926,5938,5948,5952,5956,5960,6006,6028,6040,6050,6054,6058,6076,6080,6090,6102,6106,6112,6124,6138,6142,6146,6150,6165,6187,6199,6203,6209,6215,6229,6233,6247,6269,6281,6286,6290,6309,6323,6329,6341,6355,6359,6363,6367,6371,6375,6379,6383,6393,6397,6401,6441,6479,6498,6520,6525,6537,6547,6551,6580],{"__ignoreMap":58},[1592,5772,5773,5775],{"class":1594,"line":9},[1592,5774,2336],{"class":1597},[1592,5776,5777],{"class":1601}," ssm\n",[1592,5779,5780],{"class":1594,"line":19},[1592,5781,1645],{"emptyLinePlaceholder":1644},[1592,5783,5784,5786],{"class":1594,"line":131},[1592,5785,2348],{"class":1597},[1592,5787,2352],{"class":2351},[1592,5789,5790],{"class":1594,"line":840},[1592,5791,2357],{"class":1621},[1592,5793,5794],{"class":1594,"line":1641},[1592,5795,4510],{"class":1621},[1592,5797,5798],{"class":1594,"line":1648},[1592,5799,1645],{"emptyLinePlaceholder":1644},[1592,5801,5802],{"class":1594,"line":1679},[1592,5803,5804],{"class":1621},"    \"github.com/aws/aws-sdk-go-v2/service/ssm\"\n",[1592,5806,5807],{"class":1594,"line":1720},[1592,5808,1739],{"class":2351},[1592,5810,5811],{"class":1594,"line":1742},[1592,5812,1645],{"emptyLinePlaceholder":1644},[1592,5814,5815,5817,5819,5821],{"class":1594,"line":1748},[1592,5816,1598],{"class":1597},[1592,5818,4379],{"class":1601},[1592,5820,1605],{"class":1597},[1592,5822,1609],{"class":1608},[1592,5824,5825,5827,5829,5832,5834],{"class":1594,"line":1757},[1592,5826,4554],{"class":1614},[1592,5828,4557],{"class":1682},[1592,5830,5831],{"class":1601},"ssm",[1592,5833,1690],{"class":1608},[1592,5835,4565],{"class":1601},[1592,5837,5838,5841],{"class":1594,"line":1762},[1592,5839,5840],{"class":1614},"    name",[1592,5842,5843],{"class":1601},"     string\n",[1592,5845,5846,5848,5850,5852],{"class":1594,"line":1767},[1592,5847,4541],{"class":1614},[1592,5849,4544],{"class":1601},[1592,5851,1690],{"class":1608},[1592,5853,4549],{"class":1601},[1592,5855,5856],{"class":1594,"line":1791},[1592,5857,1638],{"class":1608},[1592,5859,5860],{"class":1594,"line":1809},[1592,5861,1645],{"emptyLinePlaceholder":1644},[1592,5863,5864,5866,5868,5870,5872,5874,5876,5878,5880,5882,5885,5887,5889,5891,5893,5895,5897,5899,5901,5903],{"class":1594,"line":1847},[1592,5865,1651],{"class":1597},[1592,5867,4580],{"class":1667},[1592,5869,1733],{"class":1608},[1592,5871,3210],{"class":1657},[1592,5873,4604],{"class":1682},[1592,5875,5831],{"class":1601},[1592,5877,1690],{"class":1608},[1592,5879,4658],{"class":1601},[1592,5881,1828],{"class":1608},[1592,5883,5884],{"class":1657}," name",[1592,5886,1630],{"class":1601},[1592,5888,1828],{"class":1608},[1592,5890,4592],{"class":1657},[1592,5892,4544],{"class":1601},[1592,5894,1690],{"class":1608},[1592,5896,4599],{"class":1601},[1592,5898,1664],{"class":1608},[1592,5900,4604],{"class":1682},[1592,5902,139],{"class":1601},[1592,5904,1609],{"class":1608},[1592,5906,5907,5909,5911,5913],{"class":1594,"line":1893},[1592,5908,1751],{"class":1682},[1592,5910,3115],{"class":1682},[1592,5912,139],{"class":1601},[1592,5914,3126],{"class":1608},[1592,5916,5917,5919,5921,5924],{"class":1594,"line":1908},[1592,5918,4646],{"class":1614},[1592,5920,2851],{"class":1608},[1592,5922,5923],{"class":1686},"   client",[1592,5925,3161],{"class":1608},[1592,5927,5928,5931,5933,5936],{"class":1594,"line":1914},[1592,5929,5930],{"class":1614},"        name",[1592,5932,2851],{"class":1608},[1592,5934,5935],{"class":1686},"     name",[1592,5937,3161],{"class":1608},[1592,5939,5940,5942,5944,5946],{"class":1594,"line":2491},[1592,5941,4635],{"class":1614},[1592,5943,2851],{"class":1608},[1592,5945,4592],{"class":1686},[1592,5947,3161],{"class":1608},[1592,5949,5950],{"class":1594,"line":2496},[1592,5951,1745],{"class":1608},[1592,5953,5954],{"class":1594,"line":2515},[1592,5955,1638],{"class":1608},[1592,5957,5958],{"class":1594,"line":2533},[1592,5959,1645],{"emptyLinePlaceholder":1644},[1592,5961,5962,5964,5966,5968,5970,5972,5974,5976,5978,5980,5982,5984,5986,5988,5990,5992,5994,5996,5998,6000,6002,6004],{"class":1594,"line":2538},[1592,5963,1651],{"class":1597},[1592,5965,1654],{"class":1608},[1592,5967,4702],{"class":1657},[1592,5969,4705],{"class":1682},[1592,5971,139],{"class":1601},[1592,5973,1664],{"class":1608},[1592,5975,4712],{"class":1667},[1592,5977,1733],{"class":1608},[1592,5979,1817],{"class":1657},[1592,5981,1820],{"class":1601},[1592,5983,1690],{"class":1608},[1592,5985,1825],{"class":1601},[1592,5987,1664],{"class":1608},[1592,5989,1654],{"class":1608},[1592,5991,4406],{"class":1682},[1592,5993,4409],{"class":1597},[1592,5995,2181],{"class":1608},[1592,5997,4414],{"class":1601},[1592,5999,1828],{"class":1608},[1592,6001,1674],{"class":1601},[1592,6003,1664],{"class":1608},[1592,6005,1609],{"class":1608},[1592,6007,6008,6010,6012,6014,6016,6018,6020,6022,6024,6026],{"class":1594,"line":2545},[1592,6009,4752],{"class":1686},[1592,6011,1828],{"class":1608},[1592,6013,1932],{"class":1686},[1592,6015,1773],{"class":1686},[1592,6017,4761],{"class":1686},[1592,6019,1690],{"class":1608},[1592,6021,4766],{"class":1667},[1592,6023,1733],{"class":1608},[1592,6025,1817],{"class":1686},[1592,6027,1739],{"class":1608},[1592,6029,6030,6032,6034,6036,6038],{"class":1594,"line":2550},[1592,6031,1683],{"class":1682},[1592,6033,1932],{"class":1686},[1592,6035,1953],{"class":1682},[1592,6037,1956],{"class":1597},[1592,6039,1609],{"class":1608},[1592,6041,6042,6044,6046,6048],{"class":1594,"line":2555},[1592,6043,1723],{"class":1682},[1592,6045,1956],{"class":1597},[1592,6047,1828],{"class":1608},[1592,6049,5349],{"class":1686},[1592,6051,6052],{"class":1594,"line":2567},[1592,6053,1745],{"class":1608},[1592,6055,6056],{"class":1594,"line":2585},[1592,6057,1645],{"emptyLinePlaceholder":1644},[1592,6059,6060,6062,6064,6066,6068,6070,6072,6074],{"class":1594,"line":2590},[1592,6061,4829],{"class":1686},[1592,6063,1773],{"class":1686},[1592,6065,4835],{"class":4834},[1592,6067,1733],{"class":1608},[1592,6069,4409],{"class":1597},[1592,6071,2181],{"class":1608},[1592,6073,4414],{"class":1601},[1592,6075,1739],{"class":1608},[1592,6077,6078],{"class":1594,"line":2610},[1592,6079,1645],{"emptyLinePlaceholder":1644},[1592,6081,6082,6084,6086,6088],{"class":1594,"line":2627},[1592,6083,4854],{"class":1682},[1592,6085,4857],{"class":1597},[1592,6087,1671],{"class":1608},[1592,6089,1609],{"class":1608},[1592,6091,6092,6094,6096,6098,6100],{"class":1594,"line":2659},[1592,6093,4866],{"class":1682},[1592,6095,4869],{"class":4834},[1592,6097,1733],{"class":1608},[1592,6099,4874],{"class":1686},[1592,6101,1739],{"class":1608},[1592,6103,6104],{"class":1594,"line":2685},[1592,6105,1645],{"emptyLinePlaceholder":1644},[1592,6107,6108,6110],{"class":1594,"line":2693},[1592,6109,4890],{"class":1682},[1592,6111,1609],{"class":1608},[1592,6113,6114,6116,6118,6120,6122],{"class":1594,"line":2699},[1592,6115,4897],{"class":1682},[1592,6117,4900],{"class":1686},[1592,6119,4903],{"class":1682},[1592,6121,4906],{"class":1686},[1592,6123,4909],{"class":1608},[1592,6125,6126,6128,6130,6132,6134,6136],{"class":1594,"line":2705},[1592,6127,4897],{"class":1682},[1592,6129,4903],{"class":1682},[1592,6131,1817],{"class":1686},[1592,6133,1690],{"class":1608},[1592,6135,2902],{"class":1667},[1592,6137,4924],{"class":1608},[1592,6139,6140],{"class":1594,"line":2710},[1592,6141,4929],{"class":1682},[1592,6143,6144],{"class":1594,"line":2739},[1592,6145,4934],{"class":1608},[1592,6147,6148],{"class":1594,"line":2765},[1592,6149,1645],{"emptyLinePlaceholder":1644},[1592,6151,6152,6154,6156,6158,6160,6163],{"class":1594,"line":2770},[1592,6153,4943],{"class":1686},[1592,6155,1773],{"class":1686},[1592,6157,1630],{"class":1601},[1592,6159,1733],{"class":1608},[1592,6161,6162],{"class":1686},"initial",[1592,6164,1739],{"class":1608},[1592,6166,6167,6169,6171,6173,6175,6177,6179,6181,6183,6185],{"class":1594,"line":2775},[1592,6168,4953],{"class":1686},[1592,6170,1773],{"class":1686},[1592,6172,4544],{"class":1686},[1592,6174,1690],{"class":1608},[1592,6176,4962],{"class":1667},[1592,6178,1733],{"class":1608},[1592,6180,4967],{"class":1686},[1592,6182,1690],{"class":1608},[1592,6184,4972],{"class":1686},[1592,6186,1739],{"class":1608},[1592,6188,6189,6191,6193,6195,6197],{"class":1594,"line":2805},[1592,6190,4866],{"class":1682},[1592,6192,4981],{"class":1686},[1592,6194,1690],{"class":1608},[1592,6196,4986],{"class":1667},[1592,6198,2582],{"class":1608},[1592,6200,6201],{"class":1594,"line":2810},[1592,6202,1645],{"emptyLinePlaceholder":1644},[1592,6204,6205,6207],{"class":1594,"line":2835},[1592,6206,4997],{"class":1682},[1592,6208,1609],{"class":1608},[1592,6210,6211,6213],{"class":1594,"line":2876},[1592,6212,5004],{"class":1682},[1592,6214,1609],{"class":1608},[1592,6216,6217,6219,6221,6223,6225,6227],{"class":1594,"line":2881},[1592,6218,5012],{"class":1682},[1592,6220,4903],{"class":1682},[1592,6222,1817],{"class":1686},[1592,6224,1690],{"class":1608},[1592,6226,2902],{"class":1667},[1592,6228,4924],{"class":1608},[1592,6230,6231],{"class":1594,"line":2886},[1592,6232,5028],{"class":1682},[1592,6234,6235,6237,6239,6241,6243,6245],{"class":1594,"line":2892},[1592,6236,5012],{"class":1682},[1592,6238,4903],{"class":1682},[1592,6240,5038],{"class":1686},[1592,6242,1690],{"class":1608},[1592,6244,5043],{"class":1686},[1592,6246,4909],{"class":1608},[1592,6248,6249,6251,6253,6255,6257,6259,6261,6263,6265,6267],{"class":1594,"line":2907},[1592,6250,5051],{"class":1686},[1592,6252,1828],{"class":1608},[1592,6254,1932],{"class":1686},[1592,6256,1773],{"class":1686},[1592,6258,4761],{"class":1686},[1592,6260,1690],{"class":1608},[1592,6262,4766],{"class":1667},[1592,6264,1733],{"class":1608},[1592,6266,1817],{"class":1686},[1592,6268,1739],{"class":1608},[1592,6270,6271,6273,6275,6277,6279],{"class":1594,"line":5009},[1592,6272,5075],{"class":1682},[1592,6274,1932],{"class":1686},[1592,6276,1953],{"class":1682},[1592,6278,1956],{"class":1597},[1592,6280,1609],{"class":1608},[1592,6282,6283],{"class":1594,"line":5025},[1592,6284,6285],{"class":1682},"                    continue\n",[1592,6287,6288],{"class":1594,"line":5031},[1592,6289,5098],{"class":1608},[1592,6291,6292,6294,6296,6298,6301,6303,6305,6307],{"class":1594,"line":5048},[1592,6293,5075],{"class":1682},[1592,6295,1630],{"class":1601},[1592,6297,1733],{"class":1608},[1592,6299,6300],{"class":1686},"next",[1592,6302,1664],{"class":1608},[1592,6304,1953],{"class":1682},[1592,6306,5161],{"class":1686},[1592,6308,1609],{"class":1608},[1592,6310,6311,6313,6315,6317,6319,6321],{"class":1594,"line":5072},[1592,6312,5134],{"class":1686},[1592,6314,5137],{"class":1686},[1592,6316,1630],{"class":1601},[1592,6318,1733],{"class":1608},[1592,6320,6300],{"class":1686},[1592,6322,1739],{"class":1608},[1592,6324,6325,6327],{"class":1594,"line":5086},[1592,6326,5146],{"class":1682},[1592,6328,1609],{"class":1608},[1592,6330,6331,6333,6335,6337,6339],{"class":1594,"line":5095},[1592,6332,5154],{"class":1682},[1592,6334,4900],{"class":1686},[1592,6336,4903],{"class":1682},[1592,6338,5124],{"class":1686},[1592,6340,4909],{"class":1608},[1592,6342,6343,6345,6347,6349,6351,6353],{"class":1594,"line":5101},[1592,6344,5154],{"class":1682},[1592,6346,4903],{"class":1682},[1592,6348,1817],{"class":1686},[1592,6350,1690],{"class":1608},[1592,6352,2902],{"class":1667},[1592,6354,4924],{"class":1608},[1592,6356,6357],{"class":1594,"line":5131},[1592,6358,5184],{"class":1682},[1592,6360,6361],{"class":1594,"line":5143},[1592,6362,5190],{"class":1608},[1592,6364,6365],{"class":1594,"line":5151},[1592,6366,5098],{"class":1608},[1592,6368,6369],{"class":1594,"line":5166},[1592,6370,5201],{"class":1608},[1592,6372,6373],{"class":1594,"line":5181},[1592,6374,4934],{"class":1608},[1592,6376,6377],{"class":1594,"line":5187},[1592,6378,5212],{"class":1608},[1592,6380,6381],{"class":1594,"line":5193},[1592,6382,1645],{"emptyLinePlaceholder":1644},[1592,6384,6385,6387,6389,6391],{"class":1594,"line":5198},[1592,6386,1751],{"class":1682},[1592,6388,4900],{"class":1686},[1592,6390,1828],{"class":1608},[1592,6392,1754],{"class":1597},[1592,6394,6395],{"class":1594,"line":5204},[1592,6396,1638],{"class":1608},[1592,6398,6399],{"class":1594,"line":5209},[1592,6400,1645],{"emptyLinePlaceholder":1644},[1592,6402,6403,6405,6407,6409,6411,6413,6415,6417,6419,6421,6423,6425,6427,6429,6431,6433,6435,6437,6439],{"class":1594,"line":5215},[1592,6404,1651],{"class":1597},[1592,6406,1654],{"class":1608},[1592,6408,4702],{"class":1657},[1592,6410,4705],{"class":1682},[1592,6412,139],{"class":1601},[1592,6414,1664],{"class":1608},[1592,6416,5256],{"class":1667},[1592,6418,1733],{"class":1608},[1592,6420,1817],{"class":1657},[1592,6422,1820],{"class":1601},[1592,6424,1690],{"class":1608},[1592,6426,1825],{"class":1601},[1592,6428,1664],{"class":1608},[1592,6430,5271],{"class":1608},[1592,6432,4414],{"class":1601},[1592,6434,1828],{"class":1608},[1592,6436,1674],{"class":1601},[1592,6438,1664],{"class":1608},[1592,6440,1609],{"class":1608},[1592,6442,6443,6445,6447,6449,6451,6453,6455,6457,6459,6462,6464,6466,6468,6470,6472,6474,6477],{"class":1594,"line":5220},[1592,6444,4829],{"class":1686},[1592,6446,1828],{"class":1608},[1592,6448,1932],{"class":1686},[1592,6450,1773],{"class":1686},[1592,6452,4761],{"class":1686},[1592,6454,1690],{"class":1608},[1592,6456,3210],{"class":1686},[1592,6458,1690],{"class":1608},[1592,6460,6461],{"class":1667},"GetParameter",[1592,6463,1733],{"class":1608},[1592,6465,1817],{"class":1686},[1592,6467,1828],{"class":1608},[1592,6469,3115],{"class":1682},[1592,6471,5831],{"class":1601},[1592,6473,1690],{"class":1608},[1592,6475,6476],{"class":1601},"GetParameterInput",[1592,6478,3126],{"class":1608},[1592,6480,6481,6484,6486,6489,6491,6493,6496],{"class":1594,"line":5231},[1592,6482,6483],{"class":1614},"        Name",[1592,6485,2851],{"class":1608},[1592,6487,6488],{"class":1682},"           &",[1592,6490,4967],{"class":1686},[1592,6492,1690],{"class":1608},[1592,6494,6495],{"class":1686},"name",[1592,6497,3161],{"class":1608},[1592,6499,6500,6503,6505,6508,6510,6513,6515,6518],{"class":1594,"line":5236},[1592,6501,6502],{"class":1614},"        WithDecryption",[1592,6504,2851],{"class":1608},[1592,6506,6507],{"class":1686}," aws",[1592,6509,1690],{"class":1608},[1592,6511,6512],{"class":1667},"Bool",[1592,6514,1733],{"class":1608},[1592,6516,6517],{"class":1597},"true",[1592,6519,1806],{"class":1608},[1592,6521,6522],{"class":1594,"line":5241},[1592,6523,6524],{"class":1608},"    })\n",[1592,6526,6527,6529,6531,6533,6535],{"class":1594,"line":5284},[1592,6528,1683],{"class":1682},[1592,6530,1932],{"class":1686},[1592,6532,1953],{"class":1682},[1592,6534,1956],{"class":1597},[1592,6536,1609],{"class":1608},[1592,6538,6539,6541,6543,6545],{"class":1594,"line":5327},[1592,6540,1723],{"class":1682},[1592,6542,1956],{"class":1597},[1592,6544,1828],{"class":1608},[1592,6546,5349],{"class":1686},[1592,6548,6549],{"class":1594,"line":5340},[1592,6550,1745],{"class":1608},[1592,6552,6553,6555,6557,6559,6561,6563,6565,6567,6570,6572,6575,6578],{"class":1594,"line":5352},[1592,6554,1751],{"class":1682},[1592,6556,2181],{"class":1608},[1592,6558,4414],{"class":1601},[1592,6560,1733],{"class":1608},[1592,6562,4705],{"class":1682},[1592,6564,4874],{"class":1686},[1592,6566,1690],{"class":1608},[1592,6568,6569],{"class":1686},"Parameter",[1592,6571,1690],{"class":1608},[1592,6573,6574],{"class":1686},"Value",[1592,6576,6577],{"class":1608},"),",[1592,6579,1754],{"class":1597},[1592,6581,6582],{"class":1594,"line":5357},[1592,6583,1638],{"class":1608},[1575,6585,763],{"id":6586},"example-grpc-streaming-watcher",[1501,6588,6589],{},"Watch a gRPC streaming endpoint:",[1583,6591,6593],{"className":1585,"code":6592,"language":1587,"meta":58,"style":58},"package grpcconfig\n\nimport (\n    \"context\"\n\n    pb \"myapp/proto/config\"\n)\n\ntype Watcher struct {\n    client pb.ConfigServiceClient\n    key    string\n}\n\nfunc New(client pb.ConfigServiceClient, key string) *Watcher {\n    return &Watcher{client: client, key: key}\n}\n\nfunc (w *Watcher) Watch(ctx context.Context) (\u003C-chan []byte, error) {\n    stream, err := w.client.WatchConfig(ctx, &pb.WatchRequest{Key: w.key})\n    if err != nil {\n        return nil, err\n    }\n\n    out := make(chan []byte)\n\n    go func() {\n        defer close(out)\n\n        for {\n            resp, err := stream.Recv()\n            if err != nil {\n                return\n            }\n\n            select {\n            case out \u003C- resp.Value:\n            case \u003C-ctx.Done():\n                return\n            }\n        }\n    }()\n\n    return out, nil\n}\n",[1589,6594,6595,6602,6606,6612,6616,6620,6628,6632,6636,6646,6658,6666,6670,6674,6706,6733,6737,6741,6787,6842,6854,6864,6868,6872,6890,6894,6904,6916,6920,6926,6947,6960,6964,6968,6972,6978,6994,7008,7012,7016,7020,7024,7028,7038],{"__ignoreMap":58},[1592,6596,6597,6599],{"class":1594,"line":9},[1592,6598,2336],{"class":1597},[1592,6600,6601],{"class":1601}," grpcconfig\n",[1592,6603,6604],{"class":1594,"line":19},[1592,6605,1645],{"emptyLinePlaceholder":1644},[1592,6607,6608,6610],{"class":1594,"line":131},[1592,6609,2348],{"class":1597},[1592,6611,2352],{"class":2351},[1592,6613,6614],{"class":1594,"line":840},[1592,6615,2357],{"class":1621},[1592,6617,6618],{"class":1594,"line":1641},[1592,6619,1645],{"emptyLinePlaceholder":1644},[1592,6621,6622,6625],{"class":1594,"line":1648},[1592,6623,6624],{"class":1657},"    pb",[1592,6626,6627],{"class":1621}," \"myapp/proto/config\"\n",[1592,6629,6630],{"class":1594,"line":1679},[1592,6631,1739],{"class":2351},[1592,6633,6634],{"class":1594,"line":1720},[1592,6635,1645],{"emptyLinePlaceholder":1644},[1592,6637,6638,6640,6642,6644],{"class":1594,"line":1742},[1592,6639,1598],{"class":1597},[1592,6641,4379],{"class":1601},[1592,6643,1605],{"class":1597},[1592,6645,1609],{"class":1608},[1592,6647,6648,6650,6653,6655],{"class":1594,"line":1748},[1592,6649,4554],{"class":1614},[1592,6651,6652],{"class":1601}," pb",[1592,6654,1690],{"class":1608},[1592,6656,6657],{"class":1601},"ConfigServiceClient\n",[1592,6659,6660,6663],{"class":1594,"line":1757},[1592,6661,6662],{"class":1614},"    key",[1592,6664,6665],{"class":1601},"    string\n",[1592,6667,6668],{"class":1594,"line":1762},[1592,6669,1638],{"class":1608},[1592,6671,6672],{"class":1594,"line":1767},[1592,6673,1645],{"emptyLinePlaceholder":1644},[1592,6675,6676,6678,6680,6682,6684,6686,6688,6691,6693,6696,6698,6700,6702,6704],{"class":1594,"line":1791},[1592,6677,1651],{"class":1597},[1592,6679,4580],{"class":1667},[1592,6681,1733],{"class":1608},[1592,6683,3210],{"class":1657},[1592,6685,6652],{"class":1601},[1592,6687,1690],{"class":1608},[1592,6689,6690],{"class":1601},"ConfigServiceClient",[1592,6692,1828],{"class":1608},[1592,6694,6695],{"class":1657}," key",[1592,6697,1630],{"class":1601},[1592,6699,1664],{"class":1608},[1592,6701,4604],{"class":1682},[1592,6703,139],{"class":1601},[1592,6705,1609],{"class":1608},[1592,6707,6708,6710,6712,6714,6716,6718,6720,6723,6725,6727,6729,6731],{"class":1594,"line":1809},[1592,6709,1751],{"class":1682},[1592,6711,3115],{"class":1682},[1592,6713,139],{"class":1601},[1592,6715,4661],{"class":1608},[1592,6717,3210],{"class":1614},[1592,6719,2851],{"class":1608},[1592,6721,6722],{"class":1686}," client",[1592,6724,1828],{"class":1608},[1592,6726,6695],{"class":1614},[1592,6728,2851],{"class":1608},[1592,6730,6695],{"class":1686},[1592,6732,1638],{"class":1608},[1592,6734,6735],{"class":1594,"line":1847},[1592,6736,1638],{"class":1608},[1592,6738,6739],{"class":1594,"line":1893},[1592,6740,1645],{"emptyLinePlaceholder":1644},[1592,6742,6743,6745,6747,6749,6751,6753,6755,6757,6759,6761,6763,6765,6767,6769,6771,6773,6775,6777,6779,6781,6783,6785],{"class":1594,"line":1908},[1592,6744,1651],{"class":1597},[1592,6746,1654],{"class":1608},[1592,6748,4702],{"class":1657},[1592,6750,4705],{"class":1682},[1592,6752,139],{"class":1601},[1592,6754,1664],{"class":1608},[1592,6756,4712],{"class":1667},[1592,6758,1733],{"class":1608},[1592,6760,1817],{"class":1657},[1592,6762,1820],{"class":1601},[1592,6764,1690],{"class":1608},[1592,6766,1825],{"class":1601},[1592,6768,1664],{"class":1608},[1592,6770,1654],{"class":1608},[1592,6772,4406],{"class":1682},[1592,6774,4409],{"class":1597},[1592,6776,2181],{"class":1608},[1592,6778,4414],{"class":1601},[1592,6780,1828],{"class":1608},[1592,6782,1674],{"class":1601},[1592,6784,1664],{"class":1608},[1592,6786,1609],{"class":1608},[1592,6788,6789,6792,6794,6796,6798,6800,6802,6804,6806,6809,6811,6813,6815,6817,6820,6822,6825,6827,6830,6832,6834,6836,6839],{"class":1594,"line":1914},[1592,6790,6791],{"class":1686},"    stream",[1592,6793,1828],{"class":1608},[1592,6795,1932],{"class":1686},[1592,6797,1773],{"class":1686},[1592,6799,4761],{"class":1686},[1592,6801,1690],{"class":1608},[1592,6803,3210],{"class":1686},[1592,6805,1690],{"class":1608},[1592,6807,6808],{"class":1667},"WatchConfig",[1592,6810,1733],{"class":1608},[1592,6812,1817],{"class":1686},[1592,6814,1828],{"class":1608},[1592,6816,3115],{"class":1682},[1592,6818,6819],{"class":1601},"pb",[1592,6821,1690],{"class":1608},[1592,6823,6824],{"class":1601},"WatchRequest",[1592,6826,4661],{"class":1608},[1592,6828,6829],{"class":1614},"Key",[1592,6831,2851],{"class":1608},[1592,6833,4761],{"class":1686},[1592,6835,1690],{"class":1608},[1592,6837,6838],{"class":1686},"key",[1592,6840,6841],{"class":1608},"})\n",[1592,6843,6844,6846,6848,6850,6852],{"class":1594,"line":2491},[1592,6845,1683],{"class":1682},[1592,6847,1932],{"class":1686},[1592,6849,1953],{"class":1682},[1592,6851,1956],{"class":1597},[1592,6853,1609],{"class":1608},[1592,6855,6856,6858,6860,6862],{"class":1594,"line":2496},[1592,6857,1723],{"class":1682},[1592,6859,1956],{"class":1597},[1592,6861,1828],{"class":1608},[1592,6863,5349],{"class":1686},[1592,6865,6866],{"class":1594,"line":2515},[1592,6867,1745],{"class":1608},[1592,6869,6870],{"class":1594,"line":2533},[1592,6871,1645],{"emptyLinePlaceholder":1644},[1592,6873,6874,6876,6878,6880,6882,6884,6886,6888],{"class":1594,"line":2538},[1592,6875,4829],{"class":1686},[1592,6877,1773],{"class":1686},[1592,6879,4835],{"class":4834},[1592,6881,1733],{"class":1608},[1592,6883,4409],{"class":1597},[1592,6885,2181],{"class":1608},[1592,6887,4414],{"class":1601},[1592,6889,1739],{"class":1608},[1592,6891,6892],{"class":1594,"line":2545},[1592,6893,1645],{"emptyLinePlaceholder":1644},[1592,6895,6896,6898,6900,6902],{"class":1594,"line":2550},[1592,6897,4854],{"class":1682},[1592,6899,4857],{"class":1597},[1592,6901,1671],{"class":1608},[1592,6903,1609],{"class":1608},[1592,6905,6906,6908,6910,6912,6914],{"class":1594,"line":2555},[1592,6907,4866],{"class":1682},[1592,6909,4869],{"class":4834},[1592,6911,1733],{"class":1608},[1592,6913,4874],{"class":1686},[1592,6915,1739],{"class":1608},[1592,6917,6918],{"class":1594,"line":2567},[1592,6919,1645],{"emptyLinePlaceholder":1644},[1592,6921,6922,6924],{"class":1594,"line":2585},[1592,6923,4997],{"class":1682},[1592,6925,1609],{"class":1608},[1592,6927,6928,6931,6933,6935,6937,6940,6942,6945],{"class":1594,"line":2590},[1592,6929,6930],{"class":1686},"            resp",[1592,6932,1828],{"class":1608},[1592,6934,1932],{"class":1686},[1592,6936,1773],{"class":1686},[1592,6938,6939],{"class":1686}," stream",[1592,6941,1690],{"class":1608},[1592,6943,6944],{"class":1667},"Recv",[1592,6946,2582],{"class":1608},[1592,6948,6949,6952,6954,6956,6958],{"class":1594,"line":2610},[1592,6950,6951],{"class":1682},"            if",[1592,6953,1932],{"class":1686},[1592,6955,1953],{"class":1682},[1592,6957,1956],{"class":1597},[1592,6959,1609],{"class":1608},[1592,6961,6962],{"class":1594,"line":2627},[1592,6963,5028],{"class":1682},[1592,6965,6966],{"class":1594,"line":2659},[1592,6967,5201],{"class":1608},[1592,6969,6970],{"class":1594,"line":2685},[1592,6971,1645],{"emptyLinePlaceholder":1644},[1592,6973,6974,6976],{"class":1594,"line":2693},[1592,6975,5004],{"class":1682},[1592,6977,1609],{"class":1608},[1592,6979,6980,6982,6984,6986,6988,6990,6992],{"class":1594,"line":2699},[1592,6981,5012],{"class":1682},[1592,6983,4900],{"class":1686},[1592,6985,4903],{"class":1682},[1592,6987,5427],{"class":1686},[1592,6989,1690],{"class":1608},[1592,6991,6574],{"class":1686},[1592,6993,4909],{"class":1608},[1592,6995,6996,6998,7000,7002,7004,7006],{"class":1594,"line":2705},[1592,6997,5012],{"class":1682},[1592,6999,4903],{"class":1682},[1592,7001,1817],{"class":1686},[1592,7003,1690],{"class":1608},[1592,7005,2902],{"class":1667},[1592,7007,4924],{"class":1608},[1592,7009,7010],{"class":1594,"line":2710},[1592,7011,5028],{"class":1682},[1592,7013,7014],{"class":1594,"line":2739},[1592,7015,5201],{"class":1608},[1592,7017,7018],{"class":1594,"line":2765},[1592,7019,4934],{"class":1608},[1592,7021,7022],{"class":1594,"line":2770},[1592,7023,5212],{"class":1608},[1592,7025,7026],{"class":1594,"line":2775},[1592,7027,1645],{"emptyLinePlaceholder":1644},[1592,7029,7030,7032,7034,7036],{"class":1594,"line":2805},[1592,7031,1751],{"class":1682},[1592,7033,4900],{"class":1686},[1592,7035,1828],{"class":1608},[1592,7037,1754],{"class":1597},[1592,7039,7040],{"class":1594,"line":2810},[1592,7041,1638],{"class":1608},[1575,7043,768],{"id":7044},"common-patterns",[3300,7046,772],{"id":7047},"deduplication",[1501,7049,7050],{},"Only emit when value changes:",[1583,7052,7054],{"className":1585,"code":7053,"language":1587,"meta":58,"style":58},"if !bytes.Equal(current, next) {\n    current = next\n    out \u003C- current\n}\n",[1589,7055,7056,7080,7089,7098],{"__ignoreMap":58},[1592,7057,7058,7060,7062,7064,7066,7068,7070,7072,7074,7076,7078],{"class":1594,"line":9},[1592,7059,1929],{"class":1682},[1592,7061,5106],{"class":1682},[1592,7063,5109],{"class":1686},[1592,7065,1690],{"class":1608},[1592,7067,5114],{"class":1667},[1592,7069,1733],{"class":1608},[1592,7071,5119],{"class":1686},[1592,7073,1828],{"class":1608},[1592,7075,5124],{"class":1686},[1592,7077,1664],{"class":1608},[1592,7079,1609],{"class":1608},[1592,7081,7082,7085,7087],{"class":1594,"line":19},[1592,7083,7084],{"class":1686},"    current",[1592,7086,5137],{"class":1686},[1592,7088,5140],{"class":1686},[1592,7090,7091,7093,7095],{"class":1594,"line":131},[1592,7092,4829],{"class":1686},[1592,7094,4903],{"class":1682},[1592,7096,7097],{"class":1686}," current\n",[1592,7099,7100],{"class":1594,"line":840},[1592,7101,1638],{"class":1608},[3300,7103,777],{"id":7104},"backoff-on-error",[1501,7106,7107],{},"Don't spam a failing source:",[1583,7109,7111],{"className":1585,"code":7110,"language":1587,"meta":58,"style":58},"backoff := time.Second\nmaxBackoff := time.Minute\n\nfor {\n    data, err := fetch(ctx)\n    if err != nil {\n        time.Sleep(backoff)\n        backoff = min(backoff*2, maxBackoff)\n        continue\n    }\n    backoff = time.Second  // Reset on success\n    // ...\n}\n",[1589,7112,7113,7127,7141,7145,7152,7171,7183,7199,7223,7228,7232,7248,7253],{"__ignoreMap":58},[1592,7114,7115,7118,7120,7122,7124],{"class":1594,"line":9},[1592,7116,7117],{"class":1686},"backoff",[1592,7119,1773],{"class":1686},[1592,7121,4544],{"class":1686},[1592,7123,1690],{"class":1608},[1592,7125,7126],{"class":1686},"Second\n",[1592,7128,7129,7132,7134,7136,7138],{"class":1594,"line":19},[1592,7130,7131],{"class":1686},"maxBackoff",[1592,7133,1773],{"class":1686},[1592,7135,4544],{"class":1686},[1592,7137,1690],{"class":1608},[1592,7139,7140],{"class":1686},"Minute\n",[1592,7142,7143],{"class":1594,"line":131},[1592,7144,1645],{"emptyLinePlaceholder":1644},[1592,7146,7147,7150],{"class":1594,"line":840},[1592,7148,7149],{"class":1682},"for",[1592,7151,1609],{"class":1608},[1592,7153,7154,7157,7159,7161,7163,7165,7167,7169],{"class":1594,"line":1641},[1592,7155,7156],{"class":1686},"    data",[1592,7158,1828],{"class":1608},[1592,7160,1932],{"class":1686},[1592,7162,1773],{"class":1686},[1592,7164,5256],{"class":1667},[1592,7166,1733],{"class":1608},[1592,7168,1817],{"class":1686},[1592,7170,1739],{"class":1608},[1592,7172,7173,7175,7177,7179,7181],{"class":1594,"line":1648},[1592,7174,1683],{"class":1682},[1592,7176,1932],{"class":1686},[1592,7178,1953],{"class":1682},[1592,7180,1956],{"class":1597},[1592,7182,1609],{"class":1608},[1592,7184,7185,7188,7190,7193,7195,7197],{"class":1594,"line":1679},[1592,7186,7187],{"class":1686},"        time",[1592,7189,1690],{"class":1608},[1592,7191,7192],{"class":1667},"Sleep",[1592,7194,1733],{"class":1608},[1592,7196,7117],{"class":1686},[1592,7198,1739],{"class":1608},[1592,7200,7201,7204,7206,7209,7211,7214,7216,7218,7221],{"class":1594,"line":1720},[1592,7202,7203],{"class":1686},"        backoff",[1592,7205,5137],{"class":1686},[1592,7207,7208],{"class":4834}," min",[1592,7210,1733],{"class":1608},[1592,7212,7213],{"class":1686},"backoff*",[1592,7215,2237],{"class":1699},[1592,7217,1828],{"class":1608},[1592,7219,7220],{"class":1686}," maxBackoff",[1592,7222,1739],{"class":1608},[1592,7224,7225],{"class":1594,"line":1742},[1592,7226,7227],{"class":1682},"        continue\n",[1592,7229,7230],{"class":1594,"line":1748},[1592,7231,1745],{"class":1608},[1592,7233,7234,7237,7239,7241,7243,7245],{"class":1594,"line":1757},[1592,7235,7236],{"class":1686},"    backoff",[1592,7238,5137],{"class":1686},[1592,7240,4544],{"class":1686},[1592,7242,1690],{"class":1608},[1592,7244,4678],{"class":1686},[1592,7246,7247],{"class":1992},"  // Reset on success\n",[1592,7249,7250],{"class":1594,"line":1762},[1592,7251,7252],{"class":1992},"    // ...\n",[1592,7254,7255],{"class":1594,"line":1767},[1592,7256,1638],{"class":1608},[3300,7258,782],{"id":7259},"graceful-send",[1501,7261,7262],{},"Always respect context during channel sends:",[1583,7264,7266],{"className":1585,"code":7265,"language":1587,"meta":58,"style":58},"select {\ncase out \u003C- value:\ncase \u003C-ctx.Done():\n    return\n}\n",[1589,7267,7268,7275,7289,7303,7308],{"__ignoreMap":58},[1592,7269,7270,7273],{"class":1594,"line":9},[1592,7271,7272],{"class":1682},"select",[1592,7274,1609],{"class":1608},[1592,7276,7277,7280,7282,7284,7287],{"class":1594,"line":19},[1592,7278,7279],{"class":1682},"case",[1592,7281,4900],{"class":1686},[1592,7283,4903],{"class":1682},[1592,7285,7286],{"class":1614}," value",[1592,7288,4909],{"class":1608},[1592,7290,7291,7293,7295,7297,7299,7301],{"class":1594,"line":131},[1592,7292,7279],{"class":1682},[1592,7294,4903],{"class":1682},[1592,7296,1817],{"class":1686},[1592,7298,1690],{"class":1608},[1592,7300,2902],{"class":1667},[1592,7302,4924],{"class":1608},[1592,7304,7305],{"class":1594,"line":840},[1592,7306,7307],{"class":1682},"    return\n",[1592,7309,7310],{"class":1594,"line":1641},[1592,7311,1638],{"class":1608},[3300,7313,787],{"id":7314},"options-pattern",[1583,7316,7318],{"className":1585,"code":7317,"language":1587,"meta":58,"style":58},"type Option func(*Watcher)\n\nfunc WithInterval(d time.Duration) Option {\n    return func(w *Watcher) {\n        w.interval = d\n    }\n}\n\nfunc WithClient(c *http.Client) Option {\n    return func(w *Watcher) {\n        w.client = c\n    }\n}\n\nfunc New(url string, opts ...Option) *Watcher {\n    w := &Watcher{\n        url:      url,\n        interval: 30 * time.Second,\n        client:   http.DefaultClient,\n    }\n    for _, opt := range opts {\n        opt(w)\n    }\n    return w\n}\n",[1589,7319,7320,7337,7341,7365,7383,7397,7401,7405,7409,7435,7453,7466,7470,7474,7478,7509,7522,7532,7550,7566,7570,7592,7603,7607,7614],{"__ignoreMap":58},[1592,7321,7322,7324,7327,7329,7331,7333,7335],{"class":1594,"line":9},[1592,7323,1598],{"class":1597},[1592,7325,7326],{"class":1601}," Option",[1592,7328,4857],{"class":1597},[1592,7330,1733],{"class":1608},[1592,7332,4705],{"class":1682},[1592,7334,139],{"class":1601},[1592,7336,1739],{"class":1608},[1592,7338,7339],{"class":1594,"line":19},[1592,7340,1645],{"emptyLinePlaceholder":1644},[1592,7342,7343,7345,7348,7350,7353,7355,7357,7359,7361,7363],{"class":1594,"line":131},[1592,7344,1651],{"class":1597},[1592,7346,7347],{"class":1667}," WithInterval",[1592,7349,1733],{"class":1608},[1592,7351,7352],{"class":1657},"d",[1592,7354,4544],{"class":1601},[1592,7356,1690],{"class":1608},[1592,7358,4599],{"class":1601},[1592,7360,1664],{"class":1608},[1592,7362,7326],{"class":1601},[1592,7364,1609],{"class":1608},[1592,7366,7367,7369,7371,7373,7375,7377,7379,7381],{"class":1594,"line":840},[1592,7368,1751],{"class":1682},[1592,7370,4857],{"class":1597},[1592,7372,1733],{"class":1608},[1592,7374,4967],{"class":1657},[1592,7376,4604],{"class":1682},[1592,7378,139],{"class":1601},[1592,7380,1664],{"class":1608},[1592,7382,1609],{"class":1608},[1592,7384,7385,7388,7390,7392,7394],{"class":1594,"line":1641},[1592,7386,7387],{"class":1686},"        w",[1592,7389,1690],{"class":1608},[1592,7391,4972],{"class":1686},[1592,7393,5137],{"class":1686},[1592,7395,7396],{"class":1686}," d\n",[1592,7398,7399],{"class":1594,"line":1648},[1592,7400,1745],{"class":1608},[1592,7402,7403],{"class":1594,"line":1679},[1592,7404,1638],{"class":1608},[1592,7406,7407],{"class":1594,"line":1720},[1592,7408,1645],{"emptyLinePlaceholder":1644},[1592,7410,7411,7413,7416,7418,7421,7423,7425,7427,7429,7431,7433],{"class":1594,"line":1742},[1592,7412,1651],{"class":1597},[1592,7414,7415],{"class":1667}," WithClient",[1592,7417,1733],{"class":1608},[1592,7419,7420],{"class":1657},"c",[1592,7422,4604],{"class":1682},[1592,7424,4560],{"class":1601},[1592,7426,1690],{"class":1608},[1592,7428,4658],{"class":1601},[1592,7430,1664],{"class":1608},[1592,7432,7326],{"class":1601},[1592,7434,1609],{"class":1608},[1592,7436,7437,7439,7441,7443,7445,7447,7449,7451],{"class":1594,"line":1748},[1592,7438,1751],{"class":1682},[1592,7440,4857],{"class":1597},[1592,7442,1733],{"class":1608},[1592,7444,4967],{"class":1657},[1592,7446,4604],{"class":1682},[1592,7448,139],{"class":1601},[1592,7450,1664],{"class":1608},[1592,7452,1609],{"class":1608},[1592,7454,7455,7457,7459,7461,7463],{"class":1594,"line":1757},[1592,7456,7387],{"class":1686},[1592,7458,1690],{"class":1608},[1592,7460,3210],{"class":1686},[1592,7462,5137],{"class":1686},[1592,7464,7465],{"class":1686}," c\n",[1592,7467,7468],{"class":1594,"line":1762},[1592,7469,1745],{"class":1608},[1592,7471,7472],{"class":1594,"line":1767},[1592,7473,1638],{"class":1608},[1592,7475,7476],{"class":1594,"line":1791},[1592,7477,1645],{"emptyLinePlaceholder":1644},[1592,7479,7480,7482,7484,7486,7488,7490,7492,7495,7498,7501,7503,7505,7507],{"class":1594,"line":1809},[1592,7481,1651],{"class":1597},[1592,7483,4580],{"class":1667},[1592,7485,1733],{"class":1608},[1592,7487,4585],{"class":1657},[1592,7489,1630],{"class":1601},[1592,7491,1828],{"class":1608},[1592,7493,7494],{"class":1657}," opts",[1592,7496,7497],{"class":1682}," ...",[1592,7499,7500],{"class":1601},"Option",[1592,7502,1664],{"class":1608},[1592,7504,4604],{"class":1682},[1592,7506,139],{"class":1601},[1592,7508,1609],{"class":1608},[1592,7510,7511,7514,7516,7518,7520],{"class":1594,"line":1847},[1592,7512,7513],{"class":1686},"    w",[1592,7515,1773],{"class":1686},[1592,7517,3115],{"class":1682},[1592,7519,139],{"class":1601},[1592,7521,3126],{"class":1608},[1592,7523,7524,7526,7528,7530],{"class":1594,"line":1893},[1592,7525,4623],{"class":1614},[1592,7527,2851],{"class":1608},[1592,7529,4628],{"class":1686},[1592,7531,3161],{"class":1608},[1592,7533,7534,7536,7538,7540,7542,7544,7546,7548],{"class":1594,"line":1908},[1592,7535,4635],{"class":1614},[1592,7537,2851],{"class":1608},[1592,7539,5694],{"class":1699},[1592,7541,4604],{"class":1686},[1592,7543,4544],{"class":1686},[1592,7545,1690],{"class":1608},[1592,7547,4678],{"class":1686},[1592,7549,3161],{"class":1608},[1592,7551,7552,7554,7556,7559,7561,7564],{"class":1594,"line":1914},[1592,7553,4646],{"class":1614},[1592,7555,2851],{"class":1608},[1592,7557,7558],{"class":1686},"   http",[1592,7560,1690],{"class":1608},[1592,7562,7563],{"class":1686},"DefaultClient",[1592,7565,3161],{"class":1608},[1592,7567,7568],{"class":1594,"line":2491},[1592,7569,1745],{"class":1608},[1592,7571,7572,7575,7578,7580,7583,7585,7588,7590],{"class":1594,"line":2496},[1592,7573,7574],{"class":1682},"    for",[1592,7576,7577],{"class":1686}," _",[1592,7579,1828],{"class":1608},[1592,7581,7582],{"class":1686}," opt",[1592,7584,1773],{"class":1686},[1592,7586,7587],{"class":1682}," range",[1592,7589,7494],{"class":1686},[1592,7591,1609],{"class":1608},[1592,7593,7594,7597,7599,7601],{"class":1594,"line":2515},[1592,7595,7596],{"class":1667},"        opt",[1592,7598,1733],{"class":1608},[1592,7600,4967],{"class":1686},[1592,7602,1739],{"class":1608},[1592,7604,7605],{"class":1594,"line":2533},[1592,7606,1745],{"class":1608},[1592,7608,7609,7611],{"class":1594,"line":2538},[1592,7610,1751],{"class":1682},[1592,7612,7613],{"class":1686}," w\n",[1592,7615,7616],{"class":1594,"line":2545},[1592,7617,1638],{"class":1608},[1575,7619,792],{"id":7620},"testing-custom-watchers",[1583,7622,7624],{"className":1585,"code":7623,"language":1587,"meta":58,"style":58},"func TestHTTPWatcher(t *testing.T) {\n    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        w.Write([]byte(`{\"port\": 8080}`))\n    }))\n    defer server.Close()\n\n    watcher := httpconfig.New(server.URL, 10*time.Millisecond)\n\n    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)\n    defer cancel()\n\n    ch, err := watcher.Watch(ctx)\n    require.NoError(t, err)\n\n    // Should receive initial value\n    val := \u003C-ch\n    assert.Equal(t, `{\"port\": 8080}`, string(val))\n}\n",[1589,7625,7626,7651,7706,7728,7733,7745,7749,7786,7790,7830,7838,7842,7867,7887,7891,7896,7908,7937],{"__ignoreMap":58},[1592,7627,7628,7630,7633,7635,7638,7640,7642,7644,7647,7649],{"class":1594,"line":9},[1592,7629,1651],{"class":1597},[1592,7631,7632],{"class":1667}," TestHTTPWatcher",[1592,7634,1733],{"class":1608},[1592,7636,7637],{"class":1657},"t",[1592,7639,4604],{"class":1682},[1592,7641,3963],{"class":1601},[1592,7643,1690],{"class":1608},[1592,7645,7646],{"class":1601},"T",[1592,7648,1664],{"class":1608},[1592,7650,1609],{"class":1608},[1592,7652,7653,7655,7657,7660,7662,7665,7667,7669,7671,7674,7676,7678,7680,7682,7684,7686,7689,7691,7694,7696,7698,7700,7702,7704],{"class":1594,"line":19},[1592,7654,2047],{"class":1686},[1592,7656,1773],{"class":1686},[1592,7658,7659],{"class":1686}," httptest",[1592,7661,1690],{"class":1608},[1592,7663,7664],{"class":1667},"NewServer",[1592,7666,1733],{"class":1608},[1592,7668,4560],{"class":1686},[1592,7670,1690],{"class":1608},[1592,7672,7673],{"class":1667},"HandlerFunc",[1592,7675,1733],{"class":1608},[1592,7677,1651],{"class":1597},[1592,7679,1733],{"class":1608},[1592,7681,4967],{"class":1657},[1592,7683,5296],{"class":1601},[1592,7685,1690],{"class":1608},[1592,7687,7688],{"class":1601},"ResponseWriter",[1592,7690,1828],{"class":1608},[1592,7692,7693],{"class":1657}," r",[1592,7695,4604],{"class":1682},[1592,7697,4560],{"class":1601},[1592,7699,1690],{"class":1608},[1592,7701,823],{"class":1601},[1592,7703,1664],{"class":1608},[1592,7705,1609],{"class":1608},[1592,7707,7708,7710,7712,7715,7718,7720,7722,7725],{"class":1594,"line":131},[1592,7709,7387],{"class":1686},[1592,7711,1690],{"class":1608},[1592,7713,7714],{"class":1667},"Write",[1592,7716,7717],{"class":1608},"([]",[1592,7719,4414],{"class":1601},[1592,7721,1733],{"class":1608},[1592,7723,7724],{"class":1621},"`{\"port\": 8080}`",[1592,7726,7727],{"class":1608},"))\n",[1592,7729,7730],{"class":1594,"line":840},[1592,7731,7732],{"class":1608},"    }))\n",[1592,7734,7735,7737,7739,7741,7743],{"class":1594,"line":1641},[1592,7736,5424],{"class":1682},[1592,7738,3264],{"class":1686},[1592,7740,1690],{"class":1608},[1592,7742,5437],{"class":1667},[1592,7744,2582],{"class":1608},[1592,7746,7747],{"class":1594,"line":1648},[1592,7748,1645],{"emptyLinePlaceholder":1644},[1592,7750,7751,7754,7756,7759,7761,7763,7765,7768,7770,7773,7775,7777,7779,7781,7784],{"class":1594,"line":1679},[1592,7752,7753],{"class":1686},"    watcher",[1592,7755,1773],{"class":1686},[1592,7757,7758],{"class":1686}," httpconfig",[1592,7760,1690],{"class":1608},[1592,7762,837],{"class":1667},[1592,7764,1733],{"class":1608},[1592,7766,7767],{"class":1686},"server",[1592,7769,1690],{"class":1608},[1592,7771,7772],{"class":1686},"URL",[1592,7774,1828],{"class":1608},[1592,7776,4669],{"class":1699},[1592,7778,5697],{"class":1686},[1592,7780,1690],{"class":1608},[1592,7782,7783],{"class":1686},"Millisecond",[1592,7785,1739],{"class":1608},[1592,7787,7788],{"class":1594,"line":1720},[1592,7789,1645],{"emptyLinePlaceholder":1644},[1592,7791,7792,7794,7796,7799,7801,7803,7805,7807,7809,7812,7814,7816,7819,7822,7824,7826,7828],{"class":1594,"line":1742},[1592,7793,2570],{"class":1686},[1592,7795,1828],{"class":1608},[1592,7797,7798],{"class":1686}," cancel",[1592,7800,1773],{"class":1686},[1592,7802,1820],{"class":1686},[1592,7804,1690],{"class":1608},[1592,7806,974],{"class":1667},[1592,7808,1733],{"class":1608},[1592,7810,7811],{"class":1686},"context",[1592,7813,1690],{"class":1608},[1592,7815,2579],{"class":1667},[1592,7817,7818],{"class":1608},"(),",[1592,7820,7821],{"class":1699}," 100",[1592,7823,5697],{"class":1686},[1592,7825,1690],{"class":1608},[1592,7827,7783],{"class":1686},[1592,7829,1739],{"class":1608},[1592,7831,7832,7834,7836],{"class":1594,"line":1748},[1592,7833,5424],{"class":1682},[1592,7835,7798],{"class":1667},[1592,7837,2582],{"class":1608},[1592,7839,7840],{"class":1594,"line":1757},[1592,7841,1645],{"emptyLinePlaceholder":1644},[1592,7843,7844,7847,7849,7851,7853,7856,7858,7861,7863,7865],{"class":1594,"line":1762},[1592,7845,7846],{"class":1686},"    ch",[1592,7848,1828],{"class":1608},[1592,7850,1932],{"class":1686},[1592,7852,1773],{"class":1686},[1592,7854,7855],{"class":1686}," watcher",[1592,7857,1690],{"class":1608},[1592,7859,7860],{"class":1667},"Watch",[1592,7862,1733],{"class":1608},[1592,7864,1817],{"class":1686},[1592,7866,1739],{"class":1608},[1592,7868,7869,7872,7874,7877,7879,7881,7883,7885],{"class":1594,"line":1767},[1592,7870,7871],{"class":1686},"    require",[1592,7873,1690],{"class":1608},[1592,7875,7876],{"class":1667},"NoError",[1592,7878,1733],{"class":1608},[1592,7880,7637],{"class":1686},[1592,7882,1828],{"class":1608},[1592,7884,1932],{"class":1686},[1592,7886,1739],{"class":1608},[1592,7888,7889],{"class":1594,"line":1791},[1592,7890,1645],{"emptyLinePlaceholder":1644},[1592,7892,7893],{"class":1594,"line":1809},[1592,7894,7895],{"class":1992},"    // Should receive initial value\n",[1592,7897,7898,7901,7903,7905],{"class":1594,"line":1847},[1592,7899,7900],{"class":1686},"    val",[1592,7902,1773],{"class":1686},[1592,7904,4903],{"class":1682},[1592,7906,7907],{"class":1686},"ch\n",[1592,7909,7910,7913,7915,7917,7919,7921,7923,7926,7928,7930,7932,7935],{"class":1594,"line":1893},[1592,7911,7912],{"class":1686},"    assert",[1592,7914,1690],{"class":1608},[1592,7916,5114],{"class":1667},[1592,7918,1733],{"class":1608},[1592,7920,7637],{"class":1686},[1592,7922,1828],{"class":1608},[1592,7924,7925],{"class":1621}," `{\"port\": 8080}`",[1592,7927,1828],{"class":1608},[1592,7929,1630],{"class":1601},[1592,7931,1733],{"class":1608},[1592,7933,7934],{"class":1686},"val",[1592,7936,7727],{"class":1608},[1592,7938,7939],{"class":1594,"line":1908},[1592,7940,1638],{"class":1608},[1575,7942,797],{"id":7943},"contributing-a-provider",[1501,7945,7946,7947,2851],{},"If your watcher is generally useful, consider contributing it to ",[1589,7948,7949],{},"pkg/",[3519,7951,7952,7958,7964,7969,7972,7978],{},[3042,7953,7954,7955],{},"Create ",[1589,7956,7957],{},"pkg/yourprovider/",[3042,7959,7960,7961,7963],{},"Add ",[1589,7962,1555],{}," with dependencies",[3042,7965,7966,7967,4382],{},"Implement ",[1589,7968,139],{},[3042,7970,7971],{},"Add tests using testcontainers if applicable",[3042,7973,7960,7974,7977],{},[1589,7975,7976],{},"README.md"," with usage examples",[3042,7979,7980,7981,7984],{},"Update ",[1589,7982,7983],{},"go.work"," to include new module",[1575,7986,47],{"id":7987},"next-steps",[3039,7989,7990,7997],{},[3042,7991,7992,7996],{},[1504,7993,7995],{"href":7994},"../guides/providers","Providers Guide"," - Existing providers for reference",[3042,7998,7999,8002],{},[1504,8000,198],{"href":8001},"../learn/architecture"," - How watchers integrate with Capacitor",[3418,8004,8005],{},"html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"title":58,"searchDepth":19,"depth":19,"links":8007},[8008,8009,8010,8013,8014,8015,8021,8022,8023],{"id":4367,"depth":19,"text":739},{"id":4427,"depth":19,"text":61},{"id":4456,"depth":19,"text":748,"children":8011},[8012],{"id":5543,"depth":131,"text":753},{"id":5762,"depth":19,"text":758},{"id":6586,"depth":19,"text":763},{"id":7044,"depth":19,"text":768,"children":8016},[8017,8018,8019,8020],{"id":7047,"depth":131,"text":772},{"id":7104,"depth":131,"text":777},{"id":7259,"depth":131,"text":782},{"id":7314,"depth":131,"text":787},{"id":7620,"depth":19,"text":792},{"id":7943,"depth":19,"text":797},{"id":7987,"depth":19,"text":47},{},"2025-12-11T00:00:00.000Z",null,{"title":730,"description":732},[1443,8029,139],"Custom","tvK3UYXwl9WJtNDdskCVuu9kS2sajeQCuXbk-1lnB2A",[8032,8033],{"title":674,"path":673,"stem":1450,"description":676,"children":-1},{"title":806,"path":805,"stem":1459,"description":808,"children":-1},1776191582072]