[{"data":1,"prerenderedAt":7246},["ShallowReactive",2],{"search-sections-capitan":3,"nav-capitan":1546,"content-tree-capitan":1605,"footer-resources":1632,"content-/v1.0.2/guides/best-practices":3934,"surround-/v1.0.2/guides/best-practices":7243},[4,10,14,20,25,30,35,41,46,51,56,59,64,69,74,79,84,88,93,98,103,108,113,118,123,128,133,138,143,148,153,158,163,168,173,178,183,188,193,198,203,208,213,217,221,225,230,235,240,245,250,255,260,265,270,275,280,285,290,295,300,305,309,313,318,323,328,333,338,342,347,352,357,362,366,371,376,380,385,390,395,400,405,410,415,420,425,430,434,439,444,449,454,459,464,469,474,479,484,489,494,498,503,507,512,517,522,527,532,537,542,547,552,556,560,565,570,575,580,585,590,595,600,605,610,615,620,624,628,633,638,643,647,652,657,662,666,671,676,681,685,690,695,700,703,708,713,718,721,726,731,736,741,745,750,755,760,765,769,774,779,783,788,793,798,803,808,812,817,821,826,831,836,841,845,850,854,858,862,867,871,875,879,883,887,892,896,901,906,911,916,921,926,930,935,940,945,950,955,960,965,970,975,980,984,989,993,997,1001,1005,1009,1013,1017,1021,1025,1029,1034,1039,1043,1048,1053,1056,1061,1066,1071,1076,1079,1083,1088,1092,1097,1102,1107,1111,1116,1121,1126,1130,1135,1139,1143,1147,1152,1157,1162,1167,1171,1175,1179,1184,1189,1194,1199,1203,1208,1212,1217,1222,1227,1232,1237,1242,1247,1252,1257,1262,1267,1272,1277,1282,1287,1292,1296,1301,1306,1311,1316,1321,1326,1331,1335,1340,1345,1349,1354,1359,1364,1368,1373,1378,1383,1388,1393,1398,1403,1408,1413,1418,1423,1427,1431,1435,1439,1443,1448,1453,1457,1462,1466,1470,1475,1479,1484,1489,1494,1499,1504,1508,1513,1518,1523,1528,1533,1538,1542],{"id":5,"title":6,"titles":7,"content":8,"level":9},"/v1.0.2/learn/overview","Overview",[],"In-process event coordination for Go applications",1,{"id":11,"title":6,"titles":12,"content":13,"level":9},"/v1.0.2/learn/overview#overview",[],"Event handling in Go often means choosing between tight coupling or heavyweight infrastructure. Capitan offers a third path: in-process event coordination that stays out of your way. // Define your types\ntype Order struct {\n    ID    string\n    Total float64\n    Items int\n}\n\n// Define a signal\norderCreated := capitan.NewSignal(\"order.created\", \"New order placed\")\n\n// Define unique keys\norderID := capitan.NewStringKey(\"order_id\")\norderKey := capitan.NewKey[Order](\"order\", \"myapp.Order\")\n\n// Hook a listener\ncapitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n    id, ok := orderID.From(e)\n    if !ok {\n        return\n    }\n    order, ok := orderKey.From(e)\n    if !ok {\n        return\n    }\n    fmt.Printf(\"Order %s: $%.2f (%d items)\\n\", id, order.Total, order.Items)\n})\n\n// Emit an event\ncapitan.Emit(ctx, orderCreated,\n    orderID.Field(\"ORDER-123\"),\n    orderKey.Field(Order{ID: \"ORDER-123\", Total: 99.99, Items: 3}),\n) Type-safe, zero dependencies, async by default.",{"id":15,"title":16,"titles":17,"content":18,"level":19},"/v1.0.2/learn/overview#architecture","Architecture",[6],"┌─────────────────────────────────────────────────────────┐\n│                      Capitan                            │\n│                                                         │\n│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐      │\n│  │   Signal A  │  │   Signal B  │  │   Signal C  │      │\n│  │   Worker    │  │   Worker    │  │   Worker    │      │\n│  │  [buffer]   │  │  [buffer]   │  │  [buffer]   │      │\n│  │      │      │  │      │      │  │      │      │      │\n│  │  Listeners  │  │  Listeners  │  │  Listeners  │      │\n│  └─────────────┘  └─────────────┘  └─────────────┘      │\n│                                                         │\n│  ┌─────────────────────────────────────────────────┐    │\n│  │              Observers (cross-cutting)          │    │\n│  └─────────────────────────────────────────────────┘    │\n└─────────────────────────────────────────────────────────┘ Each signal maintains its own buffered channel and worker goroutine. Listeners subscribe to specific signals. Observers span all signals for cross-cutting concerns like logging or metrics.",2,{"id":21,"title":22,"titles":23,"content":24,"level":19},"/v1.0.2/learn/overview#philosophy","Philosophy",[6],"Capitan draws inspiration from Go's slog library: a package-level singleton that enables cross-cutting concerns without explicit wiring. Any package in your application can emit events or observe them. This creates a unified event stream that flows through your entire system. // In your order service\ncapitan.Emit(ctx, orderCreated, orderID.Field(id))\n\n// In your notification service\ncapitan.Hook(orderCreated, sendConfirmationEmail)\n\n// In your analytics service\ncapitan.Observe(recordMetrics) Three independent packages, zero explicit dependencies between them, one coherent event flow.",{"id":26,"title":27,"titles":28,"content":29,"level":19},"/v1.0.2/learn/overview#capabilities","Capabilities",[6],"A unified event stream opens possibilities: aperture - OpenTelemetry observability from capitan events. herald - Message broker bridge for distributed capitan events. ago - Workflow orchestration with capitan event choreography. (Experimental) Persistence - Store events to a database for audit trails, debugging, and replay. Re-emit historical events through the same signal infrastructure. Capitan provides the coordination layer. What you build on top is up to you.",{"id":31,"title":32,"titles":33,"content":34,"level":19},"/v1.0.2/learn/overview#priorities","Priorities",[6],"",{"id":36,"title":37,"titles":38,"content":39,"level":40},"/v1.0.2/learn/overview#type-safety","Type Safety",[6,32],"Fields are typed at compile time. No runtime type assertions, no stringly-typed maps, no interface{} gymnastics. orderID := capitan.NewStringKey(\"order_id\")\ntotal := capitan.NewFloat64Key(\"total\")\n\n// Type-safe field creation\ncapitan.Emit(ctx, signal, orderID.Field(\"ORD-123\"), total.Field(99.99))\n\n// Type-safe extraction\nid, ok := orderID.From(event)  // Returns (string, bool) Custom types work the same way via generics: var orderKey = capitan.NewKey[OrderInfo](\"order\", \"myapp.OrderInfo\")\norder, ok := orderKey.From(event)  // Returns (OrderInfo, bool)",3,{"id":42,"title":43,"titles":44,"content":45,"level":40},"/v1.0.2/learn/overview#reliability","Reliability",[6,32],"Events queue asynchronously with backpressure. Each signal gets its own worker goroutine, created lazily on first emission. Slow listeners on one signal cannot affect others. Listener panics are recovered automatically. One misbehaving handler won't crash your system or prevent other listeners from running. Graceful shutdown drains all pending events before exit: capitan.Shutdown()  // Waits for queues to empty",{"id":47,"title":48,"titles":49,"content":50,"level":40},"/v1.0.2/learn/overview#performance","Performance",[6,32],"Lazy initialization - Workers spawn only when neededEvent pooling - Reduces allocations via sync.PoolPer-signal isolation - No contention between different event typesMinimal locking - Read locks on hot paths, write locks only for registration Events are pooled and reused. Workers process their queues independently. The system scales with your signal count, not your event volume. 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 .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}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 .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 .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}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);}",{"id":52,"title":53,"titles":54,"content":55,"level":9},"/v1.0.2/learn/quickstart","Quickstart",[],"Get started with capitan in minutes",{"id":57,"title":53,"titles":58,"content":34,"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",[53],"Go 1.23 or later.",{"id":65,"title":66,"titles":67,"content":68,"level":19},"/v1.0.2/learn/quickstart#installation","Installation",[53],"go get github.com/zoobz-io/capitan",{"id":70,"title":71,"titles":72,"content":73,"level":19},"/v1.0.2/learn/quickstart#basic-usage","Basic Usage",[53],"package main\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    \"github.com/zoobz-io/capitan\"\n)\n\n// Define signals as package-level variables\nvar orderCreated = capitan.NewSignal(\"order.created\", \"New order placed\")\n\n// Define keys as package-level variables\nvar orderID = capitan.NewStringKey(\"order_id\")\n\nfunc main() {\n    // Hook a listener\n    capitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n        id, ok := orderID.From(e)\n        if !ok {\n            return\n        }\n        fmt.Printf(\"Order received: %s\\n\", id)\n    })\n\n    // Emit an event\n    capitan.Emit(context.Background(), orderCreated, orderID.Field(\"ORDER-123\"))\n\n    // Drain pending events before exit\n    capitan.Shutdown()\n}",{"id":75,"title":76,"titles":77,"content":78,"level":19},"/v1.0.2/learn/quickstart#whats-happening","What's Happening",[53],"NewSignal creates an event type identifierNewStringKey creates a typed key for string fieldsHook registers a listener for a specific signalEmit queues an event for async processingShutdown waits for all pending events to process 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 .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 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 .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 .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}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":80,"title":81,"titles":82,"content":83,"level":9},"/v1.0.2/learn/concepts","Core Concepts",[],"Signals, keys, fields, and listeners - the building blocks of capitan",{"id":85,"title":81,"titles":86,"content":87,"level":9},"/v1.0.2/learn/concepts#core-concepts",[],"Capitan has four primitives: signals, keys, fields, and listeners. Understanding these unlocks the full API.",{"id":89,"title":90,"titles":91,"content":92,"level":19},"/v1.0.2/learn/concepts#signals","Signals",[81],"A signal identifies an event type. Signals route events to listeners. var orderCreated = capitan.NewSignal(\"order.created\", \"New order placed\") The first argument is the name—used for routing and logging. The second is a human-readable description. Define signals as package-level variables. Each signal creates internal state (worker goroutines, registries). Dynamic signal creation leads to unbounded memory growth. // Good: fixed set of signals\nvar (\n    OrderCreated   = capitan.NewSignal(\"order.created\", \"New order placed\")\n    OrderConfirmed = capitan.NewSignal(\"order.confirmed\", \"Order confirmed\")\n    OrderCanceled  = capitan.NewSignal(\"order.canceled\", \"Order canceled\")\n)\n\n// Bad: dynamic signal per user\nsignal := capitan.NewSignal(fmt.Sprintf(\"user.%s.action\", userID), \"...\") Use fields to carry variable data, not signal names.",{"id":94,"title":95,"titles":96,"content":97,"level":19},"/v1.0.2/learn/concepts#keys","Keys",[81],"A key defines a typed field name. Keys are bound to a specific type at compile time. orderID := capitan.NewStringKey(\"order_id\")\ntotal := capitan.NewFloat64Key(\"total\")\ncount := capitan.NewIntKey(\"count\")\nactive := capitan.NewBoolKey(\"active\")",{"id":99,"title":100,"titles":101,"content":102,"level":40},"/v1.0.2/learn/concepts#built-in-key-types","Built-in Key Types",[81,95],"ConstructorTypeExampleNewStringKeystring\"ORDER-123\"NewIntKeyint42NewInt32Keyint3242NewInt64Keyint6442NewUintKeyuint42NewUint32Keyuint3242NewUint64Keyuint6442NewFloat32Keyfloat323.14NewFloat64Keyfloat643.14NewBoolKeybooltrueNewTimeKeytime.Timetime.Now()NewDurationKeytime.Duration5 * time.SecondNewBytesKey[]byte[]byte{0x01, 0x02}NewErrorKeyerrorerrors.New(\"failed\")",{"id":104,"title":105,"titles":106,"content":107,"level":40},"/v1.0.2/learn/concepts#custom-types","Custom Types",[81,95],"Use NewKey[T] for structs or any custom type: type Order struct {\n    ID    string\n    Total float64\n    Items int\n}\n\nvar orderKey = capitan.NewKey[Order](\"order\", \"myapp.Order\") The second argument is the variant—a string that identifies the type. Use namespaced strings to avoid collisions (e.g., \"myapp.Order\" or \"github.com/org/pkg.Type\").",{"id":109,"title":110,"titles":111,"content":112,"level":19},"/v1.0.2/learn/concepts#fields","Fields",[81],"A field is a key-value pair. Create fields from keys: orderID := capitan.NewStringKey(\"order_id\")\ntotal := capitan.NewFloat64Key(\"total\")\n\n// Create fields\nidField := orderID.Field(\"ORDER-123\")\ntotalField := total.Field(99.99) Fields are passed to Emit: capitan.Emit(ctx, orderCreated,\n    orderID.Field(\"ORDER-123\"),\n    total.Field(99.99),\n)",{"id":114,"title":115,"titles":116,"content":117,"level":40},"/v1.0.2/learn/concepts#extracting-values","Extracting Values",[81,110],"Extract typed values from events using From: capitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n    id, ok := orderID.From(e)\n    if !ok {\n        // Field not present or wrong type\n        return\n    }\n    // id is a string\n}) The From method returns (T, bool). The boolean is false if the field is missing or the type doesn't match.",{"id":119,"title":120,"titles":121,"content":122,"level":40},"/v1.0.2/learn/concepts#accessing-all-fields","Accessing All Fields",[81,110],"Get all fields from an event: for _, field := range e.Fields() {\n    fmt.Printf(\"%s: %v\\n\", field.Key().Name(), field.Value())\n} Or get a specific field by key: field := e.Get(orderID)\nif field != nil {\n    // field.Value() returns any\n}",{"id":124,"title":125,"titles":126,"content":127,"level":19},"/v1.0.2/learn/concepts#listeners","Listeners",[81],"A listener handles events for a specific signal. Register with Hook: listener := capitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n    // Handle event\n}) Hook returns a *Listener that can be closed: listener.Close()  // Stop receiving events Multiple listeners can subscribe to the same signal. All receive every event.",{"id":129,"title":130,"titles":131,"content":132,"level":40},"/v1.0.2/learn/concepts#one-time-listeners","One-Time Listeners",[81,125],"For events you only need to handle once (e.g., initialization, first occurrence), use HookOnce: listener := capitan.HookOnce(signal, func(ctx context.Context, e *capitan.Event) {\n    // Runs once, then auto-unregisters\n    fmt.Println(\"First event received!\")\n}) The listener automatically closes after the callback fires. You can also close it early to prevent the callback from firing at all.",{"id":134,"title":135,"titles":136,"content":137,"level":40},"/v1.0.2/learn/concepts#listener-lifecycle","Listener Lifecycle",[81,125],"Listeners receive events asynchronously via the signal's worker goroutineListener panics are recovered automatically—one bad handler won't crash othersClosing a listener removes it from the registry immediatelyWhen the last listener for a signal closes, the worker goroutine exits",{"id":139,"title":140,"titles":141,"content":142,"level":19},"/v1.0.2/learn/concepts#observers","Observers",[81],"An observer receives events from multiple signals. Use Observe for cross-cutting concerns: // Observe all signals\nobserver := capitan.Observe(func(ctx context.Context, e *capitan.Event) {\n    log.Printf(\"[%s] %s\", e.Signal().Name(), e.Signal().Description())\n}) Observers attach to existing signals and any signals created after registration.",{"id":144,"title":145,"titles":146,"content":147,"level":40},"/v1.0.2/learn/concepts#whitelisting-signals","Whitelisting Signals",[81,140],"Observe specific signals only: observer := capitan.Observe(handler, orderCreated, orderConfirmed, orderCanceled) The observer receives only events from the listed signals.",{"id":149,"title":150,"titles":151,"content":152,"level":40},"/v1.0.2/learn/concepts#closing-observers","Closing Observers",[81,140],"observer.Close()  // Removes all underlying listeners",{"id":154,"title":155,"titles":156,"content":157,"level":19},"/v1.0.2/learn/concepts#severity-levels","Severity Levels",[81],"Events have a severity level for filtering and logging: capitan.Debug(ctx, signal, fields...)  // Development, troubleshooting\ncapitan.Info(ctx, signal, fields...)   // Normal operations\ncapitan.Warn(ctx, signal, fields...)   // Warning conditions\ncapitan.Error(ctx, signal, fields...)  // Error conditions Emit dispatches with SeverityInfo and is the most common choice for normal operations. Use the named severity functions when you need explicit control: // These are equivalent\ncapitan.Emit(ctx, orderCreated, fields...)\ncapitan.Info(ctx, orderCreated, fields...) Access severity in listeners: capitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n    switch e.Severity() {\n    case capitan.SeverityError:\n        alertOps(e)\n    case capitan.SeverityWarn:\n        logWarning(e)\n    default:\n        logInfo(e)\n    }\n})",{"id":159,"title":160,"titles":161,"content":162,"level":19},"/v1.0.2/learn/concepts#event-properties","Event Properties",[81],"Events expose metadata: e.Signal()      // Signal - the event type\ne.Timestamp()   // time.Time - when the event was created\ne.Severity()    // Severity - DEBUG, INFO, WARN, ERROR\ne.Context()     // context.Context - passed at emission time\ne.Fields()      // []Field - all fields\ne.Get(key)      // Field - specific field by key",{"id":164,"title":165,"titles":166,"content":167,"level":19},"/v1.0.2/learn/concepts#configuration","Configuration",[81],"Optionally configure the default instance before first use: capitan.Configure(\n    capitan.WithBufferSize(128),\n    capitan.WithPanicHandler(func(sig capitan.Signal, recovered any) {\n        log.Printf(\"Panic on %s: %v\", sig.Name(), recovered)\n    }),\n)",{"id":169,"title":170,"titles":171,"content":172,"level":40},"/v1.0.2/learn/concepts#options","Options",[81,165],"OptionDefaultDescriptionWithBufferSize(n)16Event queue buffer per signalWithPanicHandler(fn)silentCalled when a listener panicsWithSyncMode()offProcess events synchronously (testing)",{"id":174,"title":175,"titles":176,"content":177,"level":40},"/v1.0.2/learn/concepts#multiple-instances","Multiple Instances",[81,165],"Create isolated instances for testing or separation: c := capitan.New(\n    capitan.WithBufferSize(256),\n)\n\nc.Hook(signal, handler)\nc.Emit(ctx, signal, fields...)\nc.Shutdown()",{"id":179,"title":180,"titles":181,"content":182,"level":19},"/v1.0.2/learn/concepts#shutdown-and-drain","Shutdown and Drain",[81],"Gracefully drain pending events before exit: capitan.Shutdown() Shutdown closes all worker goroutines, processing remaining queued events first. Safe to call multiple times.",{"id":184,"title":185,"titles":186,"content":187,"level":40},"/v1.0.2/learn/concepts#drain-without-shutdown","Drain Without Shutdown",[81,180],"To wait for queued events without stopping workers, use Drain: ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\ndefer cancel()\n\nif err := cap.Drain(ctx); err != nil {\n    // Context cancelled before drain completed\n} Drain blocks until all queued events are processed but leaves workers running. Useful in tests and when you need synchronization points without full shutdown.",{"id":189,"title":190,"titles":191,"content":192,"level":40},"/v1.0.2/learn/concepts#checking-shutdown-state","Checking Shutdown State",[81,180],"if cap.IsShutdown() {\n    // Instance has been shut down\n}",{"id":194,"title":195,"titles":196,"content":197,"level":19},"/v1.0.2/learn/concepts#replay","Replay",[81],"Capitan supports replaying historical events from storage. This is useful for debugging, backfilling new listeners, or reproducing issues.",{"id":199,"title":200,"titles":201,"content":202,"level":40},"/v1.0.2/learn/concepts#creating-events-for-replay","Creating Events for Replay",[81,195],"Use NewEvent to construct events from stored data: e := capitan.NewEvent(orderCreated, capitan.SeverityInfo, storedTimestamp,\n    orderID.Field(\"ORD-123\"),\n    total.Field(99.99),\n) Unlike internally emitted events, NewEvent events are not pooled—they can be held safely.",{"id":204,"title":205,"titles":206,"content":207,"level":40},"/v1.0.2/learn/concepts#replaying-events","Replaying Events",[81,195],"Use Replay to re-emit a constructed event: capitan.Replay(ctx, e) Replay events: Preserve their original timestamp and severityAre marked as replays—check with e.IsReplay()Process synchronously in the calling goroutine (bypass worker queues entirely)Are not pooled—safe to hold references after replay completesInvoke all registered listeners, including observers Replaying during normal operation is safe. Replayed events invoke the same listeners as regular events but run synchronously rather than through the async worker queue. This means replay doesn't compete for buffer space and won't experience backpressure.",{"id":209,"title":210,"titles":211,"content":212,"level":40},"/v1.0.2/learn/concepts#detecting-replays","Detecting Replays",[81,195],"Listeners can check if an event is a replay to skip side effects: capitan.Hook(orderConfirmed, func(ctx context.Context, e *capitan.Event) {\n    // Skip external calls during replay\n    if !e.IsReplay() {\n        sendConfirmationEmail(e)\n    }\n\n    // Always update internal state\n    recordOrderConfirmed(e)\n}) See Persistence for a complete replay implementation. 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 .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 .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}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 .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 .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}",{"id":214,"title":16,"titles":215,"content":216,"level":9},"/v1.0.2/learn/architecture",[],"Internal design, workers, buffering, and event flow",{"id":218,"title":16,"titles":219,"content":220,"level":9},"/v1.0.2/learn/architecture#architecture",[],"Understanding capitan's internals helps you reason about performance, ordering, and failure modes.",{"id":222,"title":6,"titles":223,"content":224,"level":19},"/v1.0.2/learn/architecture#overview",[16],"┌─────────────────────────────────────────────────────────────────┐\n│                          Capitan                                │\n│                                                                 │\n│  ┌──────────────────────────────────────────────────────────┐   │\n│  │                      Registry                            │   │\n│  │         Signal → []*Listener mapping                     │   │\n│  └──────────────────────────────────────────────────────────┘   │\n│                                                                 │\n│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐              │\n│  │  Signal A   │  │  Signal B   │  │  Signal C   │    ...       │\n│  │             │  │             │  │             │              │\n│  │ ┌─────────┐ │  │ ┌─────────┐ │  │ ┌─────────┐ │              │\n│  │ │ Worker  │ │  │ │ Worker  │ │  │ │ Worker  │ │              │\n│  │ │ Gorout. │ │  │ │ Gorout. │ │  │ │ Gorout. │ │              │\n│  │ └────┬────┘ │  │ └────┬────┘ │  │ └────┬────┘ │              │\n│  │      │      │  │      │      │  │      │      │              │\n│  │ ┌────▼────┐ │  │ ┌────▼────┐ │  │ ┌────▼────┐ │              │\n│  │ │ Buffer  │ │  │ │ Buffer  │ │  │ │ Buffer  │ │              │\n│  │ │ (chan)  │ │  │ │ (chan)  │ │  │ │ (chan)  │ │              │\n│  │ └─────────┘ │  │ └─────────┘ │  │ └─────────┘ │              │\n│  │             │  │             │  │             │              │\n│  │ Listeners:  │  │ Listeners:  │  │ Listeners:  │              │\n│  │  - handler1 │  │  - handler3 │  │  - handler5 │              │\n│  │  - handler2 │  │  - handler4 │  │             │              │\n│  └─────────────┘  └─────────────┘  └─────────────┘              │\n│                                                                 │\n│  ┌──────────────────────────────────────────────────────────┐   │\n│  │                      Observers                           │   │\n│  │       Attached to all signals (cross-cutting)            │   │\n│  └──────────────────────────────────────────────────────────┘   │\n└─────────────────────────────────────────────────────────────────┘",{"id":226,"title":227,"titles":228,"content":229,"level":19},"/v1.0.2/learn/architecture#per-signal-workers","Per-Signal Workers",[16],"Each signal gets its own worker goroutine and buffered channel. This provides: Isolation - A slow listener on Signal A doesn't block Signal B. Each signal processes independently. Ordering - Events to the same signal process in emission order. The worker processes its queue sequentially. Lazy creation - Workers spawn on first emission, not at signal definition. No upfront cost for unused signals.",{"id":231,"title":232,"titles":233,"content":234,"level":40},"/v1.0.2/learn/architecture#worker-lifecycle","Worker Lifecycle",[16,227],"First Emit to a signal creates the worker goroutineWorker reads from its buffered channel in a loopFor each event, worker invokes all registered listenersWorker exits when:\nThe last listener closes (signal has no subscribers)Shutdown is called (global termination) // Simplified worker loop\nfunc (c *Capitan) processEvents(signal Signal, state *workerState) {\n    for {\n        select {\n        case event := \u003C-state.events:\n            c.processEvent(signal, event)\n        case \u003C-state.done:\n            c.drainEvents(signal, state.events)\n            return\n        case \u003C-c.shutdown:\n            c.drainEvents(signal, state.events)\n            return\n        }\n    }\n}",{"id":236,"title":237,"titles":238,"content":239,"level":19},"/v1.0.2/learn/architecture#buffered-channels","Buffered Channels",[16],"Each signal's worker has a buffered channel (default: 16 events). events: make(chan *Event, c.bufferSize)",{"id":241,"title":242,"titles":243,"content":244,"level":40},"/v1.0.2/learn/architecture#backpressure","Backpressure",[16,237],"When the buffer fills, Emit blocks until space is available. This provides natural backpressure: Slow listeners cause their signal's buffer to fillEmitters to that signal slow downOther signals are unaffected This prevents unbounded memory growth while maintaining throughput for healthy signals.",{"id":246,"title":247,"titles":248,"content":249,"level":40},"/v1.0.2/learn/architecture#tuning-buffer-size","Tuning Buffer Size",[16,237],"capitan.Configure(capitan.WithBufferSize(128)) Larger buffers absorb bursts but use more memory. Smaller buffers apply backpressure sooner.",{"id":251,"title":252,"titles":253,"content":254,"level":19},"/v1.0.2/learn/architecture#event-pooling","Event Pooling",[16],"Events are pooled using sync.Pool to reduce allocations: var eventPool = sync.Pool{\n    New: func() any {\n        return &Event{\n            fields:   make(map[string]Field),\n            severity: SeverityInfo,\n        }\n    },\n}",{"id":256,"title":257,"titles":258,"content":259,"level":40},"/v1.0.2/learn/architecture#pool-lifecycle","Pool Lifecycle",[16,252],"Emit acquires an event from the poolEvent is populated with signal, timestamp, fieldsEvent is queued to the workerWorker processes event, invokes listenersEvent is returned to the pool Events are reused across emissions. Don't hold references to events outside listener scope.",{"id":261,"title":262,"titles":263,"content":264,"level":19},"/v1.0.2/learn/architecture#listener-invocation","Listener Invocation",[16],"When processing an event, the worker: Checks if listener list has changed (via version counter)Copies the listener slice only if version differs (under read lock)Invokes each listener with panic recoveryReturns event to pool",{"id":266,"title":267,"titles":268,"content":269,"level":40},"/v1.0.2/learn/architecture#version-based-caching","Version-Based Caching",[16,262],"Each signal maintains a listener version counter that increments whenever listeners are added or removed. Workers cache the listener slice and only re-copy when the version changes: // workerState caches listeners between events\ntype workerState struct {\n    // ...\n    cachedListeners []*Listener\n    listenerVersion uint64\n}\n\nfunc (c *Capitan) processWorkerEvent(signal Signal, state *workerState, event *Event) {\n    // Skip if context canceled\n    if event.ctx.Err() != nil {\n        eventPool.Put(event)\n        return\n    }\n\n    // Copy listener slice only if version changed\n    c.mu.RLock()\n    if state.listenerVersion != c.listenerVersions[signal] {\n        state.cachedListeners = make([]*Listener, len(c.registry[signal]))\n        copy(state.cachedListeners, c.registry[signal])\n        state.listenerVersion = c.listenerVersions[signal]\n    }\n    listeners := state.cachedListeners\n    c.mu.RUnlock()\n\n    // Invoke with panic recovery\n    for _, listener := range listeners {\n        func() {\n            defer func() {\n                if r := recover(); r != nil && c.panicHandler != nil {\n                    c.panicHandler(signal, r)\n                }\n            }()\n            listener.callback(event.ctx, event)\n        }()\n    }\n\n    eventPool.Put(event)\n} This optimisation eliminates allocations in steady state—listeners typically change rarely compared to event frequency. The version counter is incremented by Hook, Close, and Observe operations. Note: Sync mode (used primarily for testing) copies on every event for simplicity.",{"id":271,"title":272,"titles":273,"content":274,"level":40},"/v1.0.2/learn/architecture#panic-recovery","Panic Recovery",[16,262],"Listener panics are caught and recovered. If a panic handler is configured, it's called with the signal and recovered value. Other listeners still execute.",{"id":276,"title":277,"titles":278,"content":279,"level":19},"/v1.0.2/learn/architecture#context-handling","Context Handling",[16],"Events carry the context passed at emission: capitan.Emit(ctx, signal, fields...) Context is checked at two points: Before queueing - If context is canceled while waiting for buffer space, the event is droppedBefore processing - If context was canceled while queued, the event is skipped This prevents processing stale events from canceled requests.",{"id":281,"title":282,"titles":283,"content":284,"level":19},"/v1.0.2/learn/architecture#locking-strategy","Locking Strategy",[16],"Capitan uses a single sync.RWMutex for the registry: OperationLock TypeHook (register listener)WriteEmit (check/create worker)Read → WriteprocessEvent (version check, conditional copy)ReadClose (unregister listener)WriteStats (read metrics)Read The hot path (Emit with existing worker) uses only a read lock. Write locks occur only during setup and teardown. Event processing holds the read lock briefly for version checking—the actual listener invocation happens outside the lock.",{"id":286,"title":287,"titles":288,"content":289,"level":19},"/v1.0.2/learn/architecture#observer-attachment","Observer Attachment",[16],"Observers maintain a list of underlying listeners—one per signal they observe. When a new signal is first used: Capitan checks for active observersFor each observer (respecting whitelists), creates a listenerAttaches listener to the signal's registry This allows observers registered early to receive events from signals created later.",{"id":291,"title":292,"titles":293,"content":294,"level":19},"/v1.0.2/learn/architecture#shutdown-sequence","Shutdown Sequence",[16],"Shutdown initiates graceful termination: Close the shutdown channel (under write lock)Workers detect closure and drain their queueswg.Wait() blocks until all workers exit func (c *Capitan) Shutdown() {\n    c.shutdownOnce.Do(func() {\n        c.mu.Lock()\n        close(c.shutdown)\n        c.mu.Unlock()\n    })\n    c.wg.Wait()\n} Events queued before shutdown are processed. Events emitted after shutdown may be dropped (workers won't accept new events).",{"id":296,"title":297,"titles":298,"content":299,"level":19},"/v1.0.2/learn/architecture#runtime-metrics","Runtime Metrics",[16],"Stats() provides visibility into internal state: stats := capitan.Stats()\n\nstats.ActiveWorkers   // Number of running worker goroutines\nstats.SignalCount     // Number of registered signals\nstats.DroppedEvents   // Events dropped (no listeners)\nstats.QueueDepths     // Current buffer usage per signal\nstats.ListenerCounts  // Listeners registered per signal\nstats.EmitCounts      // Total emissions per signal\nstats.FieldSchemas    // Field keys from first emission per signal Use these for monitoring and debugging in production.",{"id":301,"title":302,"titles":303,"content":304,"level":19},"/v1.0.2/learn/architecture#concurrency-guarantees","Concurrency Guarantees",[16],"GuaranteeScopeOrdered processingWithin a single signalNo orderingAcross different signalsThread-safeAll public API methodsPanic isolationPer listener invocation Events emitted to the same signal process in order. Events to different signals may interleave arbitrarily. 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 .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 .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}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 .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}",{"id":306,"title":165,"titles":307,"content":308,"level":9},"/v1.0.2/guides/configuration",[],"Instance options, per-signal configuration, and runtime monitoring",{"id":310,"title":165,"titles":311,"content":312,"level":9},"/v1.0.2/guides/configuration#configuration",[],"Capitan provides two levels of configuration: Instance options - Set once at creation (WithBufferSize, WithPanicHandler)Per-signal configuration - Dynamic, hot-reloadable settings per signal (ApplyConfig) See API Reference for complete API documentation.",{"id":314,"title":315,"titles":316,"content":317,"level":19},"/v1.0.2/guides/configuration#configuring-the-default-instance","Configuring the Default Instance",[165],"If you need custom configuration, use Configure before any other capitan calls: func main() {\n    capitan.Configure(\n        capitan.WithBufferSize(128),\n        capitan.WithPanicHandler(logPanic),\n    )\n\n    // Now use the module-level API\n    capitan.Hook(signal, handler)\n    capitan.Emit(ctx, signal, fields...)\n} Configure is optional. If called, it must be called before the first Hook, Emit, Observe, or Shutdown. Once the default instance is created (lazily, on first use), configuration is locked.",{"id":319,"title":320,"titles":321,"content":322,"level":19},"/v1.0.2/guides/configuration#buffer-size","Buffer Size",[165],"Each signal has a buffered channel for queuing events. The default size is 16. capitan.Configure(capitan.WithBufferSize(128))",{"id":324,"title":325,"titles":326,"content":327,"level":40},"/v1.0.2/guides/configuration#when-to-adjust","When to Adjust",[165,320],"Increase buffer size when: Bursts of events are common and listeners process quicklyYou want to absorb temporary spikes without backpressureMemory is plentiful and latency matters more Decrease buffer size when: You want earlier backpressure signalsMemory is constrainedSlow listeners should slow down emitters sooner",{"id":329,"title":330,"titles":331,"content":332,"level":40},"/v1.0.2/guides/configuration#backpressure-behavior","Backpressure Behavior",[165,320],"When a buffer fills, Emit blocks until space is available. This affects only that signal—other signals continue processing normally. // Signal A's buffer is full\ncapitan.Emit(ctx, signalA, fields...)  // Blocks here\n\n// Signal B is unaffected\ncapitan.Emit(ctx, signalB, fields...)  // Proceeds normally Monitor Stats().QueueDepths to detect signals approaching capacity. See Architecture for details on per-signal isolation.",{"id":334,"title":335,"titles":336,"content":337,"level":19},"/v1.0.2/guides/configuration#panic-handler","Panic Handler",[165],"By default, listener panics are recovered silently. Configure a handler to log or alert: capitan.Configure(\n    capitan.WithPanicHandler(func(sig capitan.Signal, recovered any) {\n        log.Printf(\"PANIC in %s listener: %v\", sig.Name(), recovered)\n\n        // Stack trace\n        debug.PrintStack()\n\n        // Alert\n        alerting.Send(\"listener-panic\", map[string]any{\n            \"signal\": sig.Name(),\n            \"panic\":  recovered,\n        })\n    }),\n) The panic handler receives: sig - The signal being processed when the panic occurredrecovered - The value passed to panic() Other listeners for the same event still execute after a panic.",{"id":339,"title":297,"titles":340,"content":341,"level":19},"/v1.0.2/guides/configuration#runtime-metrics",[165],"Stats() returns a snapshot of internal state: stats := capitan.Stats()",{"id":343,"title":344,"titles":345,"content":346,"level":40},"/v1.0.2/guides/configuration#available-metrics","Available Metrics",[165,297],"FieldTypeDescriptionActiveWorkersintRunning worker goroutinesSignalCountintRegistered signalsDroppedEventsuint64Events dropped (no listeners)QueueDepthsmap[Signal]intCurrent buffer usage per signalListenerCountsmap[Signal]intListeners per signalEmitCountsmap[Signal]uint64Total emissions per signalFieldSchemasmap[Signal][]KeyField keys from first emission",{"id":348,"title":349,"titles":350,"content":351,"level":40},"/v1.0.2/guides/configuration#monitoring-in-production","Monitoring in Production",[165,297],"Expose metrics to your monitoring system: func recordMetrics() {\n    stats := capitan.Stats()\n\n    // Gauge: active workers\n    metrics.Gauge(\"capitan_workers_active\", float64(stats.ActiveWorkers))\n\n    // Gauge: dropped events\n    metrics.Gauge(\"capitan_events_dropped_total\", float64(stats.DroppedEvents))\n\n    // Per-signal metrics\n    for signal, depth := range stats.QueueDepths {\n        metrics.Gauge(\"capitan_queue_depth\",\n            float64(depth),\n            \"signal\", signal.Name())\n    }\n\n    for signal, count := range stats.EmitCounts {\n        metrics.Counter(\"capitan_events_emitted_total\",\n            float64(count),\n            \"signal\", signal.Name())\n    }\n}",{"id":353,"title":354,"titles":355,"content":356,"level":40},"/v1.0.2/guides/configuration#health-checks","Health Checks",[165,297],"Use stats for health checks: func healthCheck() error {\n    stats := capitan.Stats()\n\n    // Check for queue buildup\n    for signal, depth := range stats.QueueDepths {\n        if depth > 100 {\n            return fmt.Errorf(\"signal %s queue depth %d exceeds threshold\", signal.Name(), depth)\n        }\n    }\n\n    return nil\n}",{"id":358,"title":359,"titles":360,"content":361,"level":40},"/v1.0.2/guides/configuration#debugging","Debugging",[165,297],"Field schemas help debug unexpected events: stats := capitan.Stats()\nfor signal, keys := range stats.FieldSchemas {\n    fmt.Printf(\"%s fields:\\n\", signal.Name())\n    for _, key := range keys {\n        fmt.Printf(\"  - %s (%s)\\n\", key.Name(), key.Variant())\n    }\n}",{"id":363,"title":175,"titles":364,"content":365,"level":19},"/v1.0.2/guides/configuration#multiple-instances",[165],"Create isolated instances when you need separate configuration: // High-throughput instance\nfast := capitan.New(\n    capitan.WithBufferSize(1024),\n)\n\n// Strict instance with panic logging\nstrict := capitan.New(\n    capitan.WithBufferSize(16),\n    capitan.WithPanicHandler(logAndAlert),\n)\n\n// Each instance has independent workers, buffers, and registries\nfast.Hook(signal, fastHandler)\nstrict.Hook(signal, strictHandler)\n\n// Shutdown each independently\nfast.Shutdown()\nstrict.Shutdown() Use cases for multiple instances: Tenant isolation in multi-tenant systemsSeparate configuration for different subsystemsTesting without affecting the default singleton",{"id":367,"title":368,"titles":369,"content":370,"level":19},"/v1.0.2/guides/configuration#sync-mode","Sync Mode",[165],"For testing, sync mode processes events synchronously: c := capitan.New(capitan.WithSyncMode()) In sync mode: Emit calls listeners directly (no workers, no buffering)Events process in the calling goroutineDeterministic ordering for tests Do not use sync mode in production—it loses isolation and backpressure benefits. See Testing for testing patterns.",{"id":372,"title":373,"titles":374,"content":375,"level":19},"/v1.0.2/guides/configuration#per-signal-configuration","Per-Signal Configuration",[165],"Configure individual signals or groups of signals dynamically with ApplyConfig. Unlike instance options, per-signal config can be changed at runtime.",{"id":377,"title":71,"titles":378,"content":379,"level":40},"/v1.0.2/guides/configuration#basic-usage",[165,373],"cfg := capitan.Config{\n    Signals: map[string]capitan.SignalConfig{\n        \"order.created\": {BufferSize: 64},\n        \"payment.*\":     {MinSeverity: capitan.SeverityWarn},\n    },\n}\ncap.ApplyConfig(cfg)",{"id":381,"title":382,"titles":383,"content":384,"level":40},"/v1.0.2/guides/configuration#signalconfig-options","SignalConfig Options",[165,373],"OptionTypeDefaultDescriptionBufferSizeintinstance defaultEvent queue size for this signalDisabledboolfalseDrop all events for this signalMinSeveritySeveritynoneFilter events below this levelMaxListenersintunlimitedCap listener registrationsDropPolicyDropPolicyblockBehavior when buffer fullRateLimitfloat64unlimitedMax events per secondBurstSizeint1Burst allowance above rate limit",{"id":386,"title":387,"titles":388,"content":389,"level":40},"/v1.0.2/guides/configuration#disabled-signals","Disabled Signals",[165,373],"Disable noisy signals without code changes: cfg := capitan.Config{\n    Signals: map[string]capitan.SignalConfig{\n        \"debug.*\": {Disabled: true},\n    },\n} Disabled signals drop all events silently. Re-enable by applying config without Disabled: true.",{"id":391,"title":392,"titles":393,"content":394,"level":40},"/v1.0.2/guides/configuration#severity-filtering","Severity Filtering",[165,373],"Filter low-priority events in production: cfg := capitan.Config{\n    Signals: map[string]capitan.SignalConfig{\n        \"audit.*\": {MinSeverity: capitan.SeverityInfo},  // Drop DEBUG\n    },\n} Severity order: DEBUG \u003C INFO \u003C WARN \u003C ERROR",{"id":396,"title":397,"titles":398,"content":399,"level":40},"/v1.0.2/guides/configuration#drop-policy","Drop Policy",[165,373],"Choose between backpressure and lossy behavior: cfg := capitan.Config{\n    Signals: map[string]capitan.SignalConfig{\n        // Block emitters when buffer full (default)\n        \"critical.*\": {DropPolicy: capitan.DropPolicyBlock},\n\n        // Drop newest events when buffer full\n        \"metrics.*\": {DropPolicy: capitan.DropPolicyDropNewest, BufferSize: 100},\n    },\n} Use DropPolicyDropNewest for high-volume, loss-tolerant signals like metrics.",{"id":401,"title":402,"titles":403,"content":404,"level":40},"/v1.0.2/guides/configuration#rate-limiting","Rate Limiting",[165,373],"Protect downstream systems from event floods: cfg := capitan.Config{\n    Signals: map[string]capitan.SignalConfig{\n        \"notification.*\": {\n            RateLimit: 10,    // 10 events/second max\n            BurstSize: 50,    // Allow bursts up to 50\n        },\n    },\n} Events exceeding the rate are dropped. Monitor Stats().DroppedEvents.",{"id":406,"title":407,"titles":408,"content":409,"level":40},"/v1.0.2/guides/configuration#glob-patterns","Glob Patterns",[165,373],"Configure signal families with glob patterns: cfg := capitan.Config{\n    Signals: map[string]capitan.SignalConfig{\n        \"order.*\":         {BufferSize: 32},\n        \"order.payment.*\": {BufferSize: 64},   // More specific\n        \"order.created\":   {BufferSize: 128},  // Exact match\n    },\n} Resolution rules (in order): Exact match winsLongest glob pattern wins (more specific)Alphabetical order for ties Supported patterns: * (any chars), ? (single char), [abc] (char class)",{"id":411,"title":412,"titles":413,"content":414,"level":40},"/v1.0.2/guides/configuration#config-replacement","Config Replacement",[165,373],"Each ApplyConfig call replaces the entire configuration: // Config A\ncap.ApplyConfig(capitan.Config{\n    Signals: map[string]capitan.SignalConfig{\n        \"order.*\": {BufferSize: 32},\n    },\n})\n\n// Config B - replaces A entirely\ncap.ApplyConfig(capitan.Config{\n    Signals: map[string]capitan.SignalConfig{\n        \"payment.*\": {BufferSize: 64},\n    },\n})\n\n// Now: order.* has default config, payment.* has BufferSize=64 Signals not in the new config revert to defaults. Only workers with actual config changes are rebuilt.",{"id":416,"title":417,"titles":418,"content":419,"level":40},"/v1.0.2/guides/configuration#hot-reload-with-flux","Hot-Reload with Flux",[165,373],"Flux is a reactive configuration library that watches files for changes and invokes callbacks when they update. Integrate it with capitan for file-based hot-reload: import (\n    \"github.com/zoobz-io/capitan\"\n    \"github.com/zoobz-io/flux\"\n    \"github.com/zoobz-io/flux/file\"\n)\n\nfunc main() {\n    cap := capitan.New()\n\n    // Watch config file, apply on change\n    watcher := file.JSON[capitan.Config](\"/etc/myapp/capitan.json\")\n    flux.New(watcher, func(cfg capitan.Config) {\n        if err := cap.ApplyConfig(cfg); err != nil {\n            log.Printf(\"config error: %v\", err)\n        }\n    })\n\n    // ...\n} Example config file (capitan.json): {\n  \"signals\": {\n    \"order.*\": {\"bufferSize\": 64, \"minSeverity\": \"INFO\"},\n    \"debug.*\": {\"disabled\": true},\n    \"metrics.*\": {\"dropPolicy\": \"drop_newest\", \"rateLimit\": 100}\n  }\n} Changes apply immediately without restart.",{"id":421,"title":422,"titles":423,"content":424,"level":19},"/v1.0.2/guides/configuration#configuration-summary","Configuration Summary",[165],"OptionDefaultProduction GuidanceWithBufferSize(n)16Start with default, increase if queue depths spikeWithPanicHandler(fn)silentAlways configure in production for visibilityWithSyncMode()offTesting onlyApplyConfig(cfg)noneUse for per-signal tuning and hot-reload 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 .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 .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 .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 .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 pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}",{"id":426,"title":427,"titles":428,"content":429,"level":9},"/v1.0.2/guides/context","Context",[],"Request tracing, cancellation, and propagating values through events",{"id":431,"title":427,"titles":432,"content":433,"level":9},"/v1.0.2/guides/context#context",[],"Every event carries the context.Context passed at emission. This enables request tracing, cancellation, and propagating request-scoped values through event flows.",{"id":435,"title":436,"titles":437,"content":438,"level":19},"/v1.0.2/guides/context#accessing-context","Accessing Context",[427],"Listeners receive context as the first argument: capitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n    // ctx is the context passed to Emit\n}) The same context is also available via e.Context().",{"id":440,"title":441,"titles":442,"content":443,"level":19},"/v1.0.2/guides/context#request-tracing","Request Tracing",[427],"Propagate trace IDs through event flows: type ctxKey string\nconst requestIDKey ctxKey = \"request_id\"\n\nfunc HandleRequest(w http.ResponseWriter, r *http.Request) {\n    // Extract or generate request ID\n    requestID := r.Header.Get(\"X-Request-ID\")\n    if requestID == \"\" {\n        requestID = uuid.New().String()\n    }\n\n    // Add to context\n    ctx := context.WithValue(r.Context(), requestIDKey, requestID)\n\n    // Events carry the request ID\n    capitan.Emit(ctx, orderCreated, orderID.Field(\"ORDER-123\"))\n}\n\n// Listener can access the request ID\ncapitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n    requestID, _ := ctx.Value(requestIDKey).(string)\n    log.Printf(\"[%s] Processing order\", requestID)\n})",{"id":445,"title":446,"titles":447,"content":448,"level":40},"/v1.0.2/guides/context#with-opentelemetry","With OpenTelemetry",[427,441],"Propagate spans through events: func HandleRequest(w http.ResponseWriter, r *http.Request) {\n    ctx, span := tracer.Start(r.Context(), \"HandleRequest\")\n    defer span.End()\n\n    // Event carries the trace context\n    capitan.Emit(ctx, orderCreated, orderID.Field(\"ORDER-123\"))\n}\n\ncapitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n    // Continue the trace\n    ctx, span := tracer.Start(ctx, \"ProcessOrder\")\n    defer span.End()\n\n    span.AddEvent(\"order.created\", trace.WithAttributes(\n        attribute.String(\"signal\", e.Signal().Name()),\n    ))\n\n    // Process order...\n}) Events emitted from listeners continue the trace: capitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n    ctx, span := tracer.Start(ctx, \"ProcessOrder\")\n    defer span.End()\n\n    // Downstream event inherits trace context\n    capitan.Emit(ctx, inventoryReserved, fields...)\n})",{"id":450,"title":451,"titles":452,"content":453,"level":19},"/v1.0.2/guides/context#cancellation","Cancellation",[427],"Context cancellation affects event processing at two points.",{"id":455,"title":456,"titles":457,"content":458,"level":40},"/v1.0.2/guides/context#before-queueing","Before Queueing",[427,451],"If the context is canceled while Emit is waiting for buffer space, the event is dropped: ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)\ndefer cancel()\n\n// If buffer is full and timeout expires, event is dropped\ncapitan.Emit(ctx, signal, fields...)",{"id":460,"title":461,"titles":462,"content":463,"level":40},"/v1.0.2/guides/context#before-processing","Before Processing",[427,451],"If the context was canceled while the event was queued, it's skipped: capitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n    // This won't execute if ctx was canceled before processing\n})",{"id":465,"title":466,"titles":467,"content":468,"level":40},"/v1.0.2/guides/context#checking-cancellation-in-listeners","Checking Cancellation in Listeners",[427,451],"For long-running listeners, check context periodically: capitan.Hook(batchProcess, func(ctx context.Context, e *capitan.Event) {\n    items := getItems(e)\n\n    for _, item := range items {\n        // Check if request was canceled\n        if ctx.Err() != nil {\n            log.Printf(\"Processing canceled: %v\", ctx.Err())\n            return\n        }\n\n        processItem(item)\n    }\n})",{"id":470,"title":471,"titles":472,"content":473,"level":19},"/v1.0.2/guides/context#timeouts","Timeouts",[427],"Emit with timeouts to bound queue wait time: func EmitWithTimeout(signal capitan.Signal, fields ...capitan.Field) error {\n    ctx, cancel := context.WithTimeout(context.Background(), time.Second)\n    defer cancel()\n\n    capitan.Emit(ctx, signal, fields...)\n\n    if ctx.Err() != nil {\n        return fmt.Errorf(\"emit timeout: %w\", ctx.Err())\n    }\n    return nil\n} Note: This bounds queue wait time, not listener execution time. Listeners run to completion regardless of context state.",{"id":475,"title":476,"titles":477,"content":478,"level":19},"/v1.0.2/guides/context#request-scoped-values","Request-Scoped Values",[427],"Pass request-scoped data through context: type User struct {\n    ID    string\n    Role  string\n}\n\ntype ctxKey string\nconst userKey ctxKey = \"user\"\n\nfunc AuthMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        user := authenticate(r)\n        ctx := context.WithValue(r.Context(), userKey, user)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n\n// Listener accesses user from context\ncapitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n    user, ok := ctx.Value(userKey).(User)\n    if !ok {\n        log.Println(\"No user in context\")\n        return\n    }\n\n    log.Printf(\"Order created by user %s (role: %s)\", user.ID, user.Role)\n})",{"id":480,"title":481,"titles":482,"content":483,"level":19},"/v1.0.2/guides/context#observer-context-patterns","Observer Context Patterns",[427],"Observers can use context for filtering or enrichment: capitan.Observe(func(ctx context.Context, e *capitan.Event) {\n    entry := map[string]any{\n        \"signal\":    e.Signal().Name(),\n        \"timestamp\": e.Timestamp(),\n    }\n\n    // Enrich with request context if available\n    if requestID, ok := ctx.Value(requestIDKey).(string); ok {\n        entry[\"request_id\"] = requestID\n    }\n\n    if user, ok := ctx.Value(userKey).(User); ok {\n        entry[\"user_id\"] = user.ID\n    }\n\n    logger.Info(entry)\n})",{"id":485,"title":486,"titles":487,"content":488,"level":19},"/v1.0.2/guides/context#context-vs-fields","Context vs Fields",[427],"Use context for request-scoped metadata. Use fields for event-specific data. ContextFieldsRequest ID, trace IDOrder ID, amountAuthenticated userCustomer IDDeadline, cancellationEvent payloadCross-cutting concernsDomain data // Context: request metadata\nctx := context.WithValue(r.Context(), requestIDKey, requestID)\n\n// Fields: event data\ncapitan.Emit(ctx, orderCreated,\n    orderID.Field(\"ORDER-123\"),\n    total.Field(99.99),\n) Listeners that need the request ID get it from context. Listeners that need the order ID get it from fields. 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 .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 .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 .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 .sfm-E, html code.shiki .sfm-E{--shiki-default:var(--shiki-variable)}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 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)}",{"id":490,"title":491,"titles":492,"content":493,"level":9},"/v1.0.2/guides/errors","Errors",[],"Error fields, severity levels, and failure workflows",{"id":495,"title":491,"titles":496,"content":497,"level":9},"/v1.0.2/guides/errors#errors",[],"Capitan provides patterns for handling errors in event-driven systems: error fields, severity levels, and failure workflows.",{"id":499,"title":500,"titles":501,"content":502,"level":19},"/v1.0.2/guides/errors#error-fields","Error Fields",[491],"Use ErrorKey to attach errors to events: var errKey = capitan.NewErrorKey(\"error\")\n\nfunc processPayment(ctx context.Context, orderID string) {\n    err := chargeCustomer(orderID)\n    if err != nil {\n        capitan.Error(ctx, paymentFailed,\n            orderIDKey.Field(orderID),\n            errKey.Field(err),\n        )\n        return\n    }\n\n    capitan.Emit(ctx, paymentProcessed, orderIDKey.Field(orderID))\n} Extract errors in listeners: capitan.Hook(paymentFailed, func(ctx context.Context, e *capitan.Event) {\n    err, ok := errKey.From(e)\n    if ok {\n        log.Printf(\"Payment failed: %v\", err)\n    }\n})",{"id":504,"title":155,"titles":505,"content":506,"level":19},"/v1.0.2/guides/errors#severity-levels",[491],"Use severity to distinguish error conditions: // Normal operation\ncapitan.Emit(ctx, orderCreated, fields...)      // INFO (default)\ncapitan.Info(ctx, orderCreated, fields...)      // Explicit INFO\n\n// Warning conditions\ncapitan.Warn(ctx, lowStock, fields...)          // WARN\n\n// Error conditions\ncapitan.Error(ctx, paymentFailed, fields...)    // ERROR\n\n// Debug information\ncapitan.Debug(ctx, queryExecuted, fields...)    // DEBUG",{"id":508,"title":509,"titles":510,"content":511,"level":40},"/v1.0.2/guides/errors#routing-by-severity","Routing by Severity",[491,155],"Filter events by severity in observers: capitan.Observe(func(ctx context.Context, e *capitan.Event) {\n    switch e.Severity() {\n    case capitan.SeverityError:\n        alertOps(e)\n        logError(e)\n    case capitan.SeverityWarn:\n        logWarning(e)\n    case capitan.SeverityInfo:\n        logInfo(e)\n    case capitan.SeverityDebug:\n        if debugEnabled {\n            logDebug(e)\n        }\n    }\n})",{"id":513,"title":514,"titles":515,"content":516,"level":40},"/v1.0.2/guides/errors#error-only-observer","Error-Only Observer",[491,155],"Observe only error events: capitan.Observe(func(ctx context.Context, e *capitan.Event) {\n    if e.Severity() != capitan.SeverityError {\n        return\n    }\n\n    // Alert on all errors\n    alerting.Send(alerting.Alert{\n        Signal:  e.Signal().Name(),\n        Message: e.Signal().Description(),\n        Fields:  extractFields(e),\n    })\n})",{"id":518,"title":519,"titles":520,"content":521,"level":19},"/v1.0.2/guides/errors#failure-signals","Failure Signals",[491],"Define explicit failure signals for error workflows: var (\n    paymentProcessed = capitan.NewSignal(\"payment.processed\", \"Payment successful\")\n    paymentFailed    = capitan.NewSignal(\"payment.failed\", \"Payment failed\")\n\n    inventoryReserved      = capitan.NewSignal(\"inventory.reserved\", \"Stock reserved\")\n    inventoryReserveFailed = capitan.NewSignal(\"inventory.reserve_failed\", \"Stock reservation failed\")\n)\n\nvar (\n    orderIDKey = capitan.NewStringKey(\"order_id\")\n    errKey     = capitan.NewErrorKey(\"error\")\n    reasonKey  = capitan.NewStringKey(\"reason\")\n)",{"id":523,"title":524,"titles":525,"content":526,"level":40},"/v1.0.2/guides/errors#error-workflows","Error Workflows",[491,519],"Handle failures as workflow branches: // Success path\ncapitan.Hook(paymentProcessed, func(ctx context.Context, e *capitan.Event) {\n    orderID, _ := orderIDKey.From(e)\n    confirmOrder(orderID)\n    capitan.Emit(ctx, orderConfirmed, orderIDKey.Field(orderID))\n})\n\n// Failure path\ncapitan.Hook(paymentFailed, func(ctx context.Context, e *capitan.Event) {\n    orderID, _ := orderIDKey.From(e)\n    err, _ := errKey.From(e)\n\n    // Release reserved inventory\n    releaseStock(orderID)\n    capitan.Emit(ctx, inventoryReleased, orderIDKey.Field(orderID))\n\n    // Cancel order\n    cancelOrder(orderID, err.Error())\n    capitan.Emit(ctx, orderCanceled,\n        orderIDKey.Field(orderID),\n        reasonKey.Field(err.Error()),\n    )\n\n    // Notify customer\n    notifyCustomer(orderID, \"payment_failed\")\n})",{"id":528,"title":529,"titles":530,"content":531,"level":19},"/v1.0.2/guides/errors#retry-patterns","Retry Patterns",[491],"Implement retries with failure events: var (\n    processJob       = capitan.NewSignal(\"job.process\", \"Process job\")\n    jobCompleted     = capitan.NewSignal(\"job.completed\", \"Job completed\")\n    jobFailed        = capitan.NewSignal(\"job.failed\", \"Job failed\")\n    jobRetryExceeded = capitan.NewSignal(\"job.retry_exceeded\", \"Job exceeded retry limit\")\n)\n\nvar (\n    jobIDKey    = capitan.NewStringKey(\"job_id\")\n    attemptKey  = capitan.NewIntKey(\"attempt\")\n    maxRetries  = 3\n)\n\ncapitan.Hook(processJob, func(ctx context.Context, e *capitan.Event) {\n    jobID, _ := jobIDKey.From(e)\n    attempt, _ := attemptKey.From(e)\n    if attempt == 0 {\n        attempt = 1\n    }\n\n    err := doWork(jobID)\n    if err != nil {\n        if attempt >= maxRetries {\n            capitan.Error(ctx, jobRetryExceeded,\n                jobIDKey.Field(jobID),\n                attemptKey.Field(attempt),\n                errKey.Field(err),\n            )\n            return\n        }\n\n        // Schedule retry\n        capitan.Warn(ctx, jobFailed,\n            jobIDKey.Field(jobID),\n            attemptKey.Field(attempt),\n            errKey.Field(err),\n        )\n        return\n    }\n\n    capitan.Emit(ctx, jobCompleted, jobIDKey.Field(jobID))\n})\n\n// Retry handler\ncapitan.Hook(jobFailed, func(ctx context.Context, e *capitan.Event) {\n    jobID, _ := jobIDKey.From(e)\n    attempt, _ := attemptKey.From(e)\n\n    // Exponential backoff\n    delay := time.Duration(attempt*attempt) * time.Second\n    time.AfterFunc(delay, func() {\n        capitan.Emit(context.Background(), processJob,\n            jobIDKey.Field(jobID),\n            attemptKey.Field(attempt+1),\n        )\n    })\n})\n\n// Dead letter handler\ncapitan.Hook(jobRetryExceeded, func(ctx context.Context, e *capitan.Event) {\n    jobID, _ := jobIDKey.From(e)\n    err, _ := errKey.From(e)\n\n    log.Printf(\"Job %s failed permanently: %v\", jobID, err)\n    moveToDeadLetter(jobID)\n    alertOps(jobID, err)\n})",{"id":533,"title":534,"titles":535,"content":536,"level":19},"/v1.0.2/guides/errors#error-aggregation","Error Aggregation",[491],"Aggregate errors for batch reporting: type ErrorAggregator struct {\n    errors []ErrorRecord\n    mu     sync.Mutex\n}\n\ntype ErrorRecord struct {\n    Signal    string\n    Error     error\n    Timestamp time.Time\n    Fields    map[string]any\n}\n\nfunc (a *ErrorAggregator) Handler() capitan.EventCallback {\n    return func(ctx context.Context, e *capitan.Event) {\n        if e.Severity() != capitan.SeverityError {\n            return\n        }\n\n        err, _ := errKey.From(e)\n\n        a.mu.Lock()\n        a.errors = append(a.errors, ErrorRecord{\n            Signal:    e.Signal().Name(),\n            Error:     err,\n            Timestamp: e.Timestamp(),\n            Fields:    extractFields(e),\n        })\n        a.mu.Unlock()\n    }\n}\n\nfunc (a *ErrorAggregator) Flush() []ErrorRecord {\n    a.mu.Lock()\n    defer a.mu.Unlock()\n\n    result := a.errors\n    a.errors = nil\n    return result\n}\n\n// Usage\naggregator := &ErrorAggregator{}\ncapitan.Observe(aggregator.Handler())\n\n// Periodic flush\ngo func() {\n    for range time.Tick(time.Minute) {\n        errors := aggregator.Flush()\n        if len(errors) > 0 {\n            sendErrorReport(errors)\n        }\n    }\n}()",{"id":538,"title":539,"titles":540,"content":541,"level":19},"/v1.0.2/guides/errors#panic-vs-error-events","Panic vs Error Events",[491],"Panics and error events serve different purposes: PanicError EventUnexpected, unrecoverableExpected, handledBug in listener codeBusiness logic failureLogged via panic handlerPart of normal flowListener terminatesListener completes Use panics for programming errors. Use error events for domain failures. See Configuration for panic handler setup and Testing for testing error paths: capitan.Hook(processOrder, func(ctx context.Context, e *capitan.Event) {\n    order, ok := orderKey.From(e)\n    if !ok {\n        // Programming error - this shouldn't happen\n        panic(\"order field missing from processOrder event\")\n    }\n\n    err := validateOrder(order)\n    if err != nil {\n        // Domain error - expected failure case\n        capitan.Error(ctx, orderValidationFailed,\n            orderIDKey.Field(order.ID),\n            errKey.Field(err),\n        )\n        return\n    }\n\n    // Continue processing...\n})",{"id":543,"title":544,"titles":545,"content":546,"level":19},"/v1.0.2/guides/errors#error-context","Error Context",[491],"Include context for debugging: var (\n    errKey       = capitan.NewErrorKey(\"error\")\n    componentKey = capitan.NewStringKey(\"component\")\n    operationKey = capitan.NewStringKey(\"operation\")\n    inputKey     = capitan.NewStringKey(\"input\")\n)\n\nfunc processWithContext(ctx context.Context, input string) {\n    result, err := riskyOperation(input)\n    if err != nil {\n        capitan.Error(ctx, operationFailed,\n            errKey.Field(err),\n            componentKey.Field(\"payment-service\"),\n            operationKey.Field(\"charge\"),\n            inputKey.Field(input),\n        )\n        return\n    }\n    // ...\n} Observers can then log rich error context: capitan.Observe(func(ctx context.Context, e *capitan.Event) {\n    if e.Severity() != capitan.SeverityError {\n        return\n    }\n\n    log.Printf(\"ERROR [%s] %s: %v (component=%s, operation=%s)\",\n        e.Signal().Name(),\n        e.Signal().Description(),\n        errKey.ExtractFromFields(e.Fields()),\n        componentKey.ExtractFromFields(e.Fields()),\n        operationKey.ExtractFromFields(e.Fields()),\n    )\n}) 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 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 .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 .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}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 .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"id":548,"title":549,"titles":550,"content":551,"level":9},"/v1.0.2/guides/testing","Testing",[],"Utilities for testing event-driven code",{"id":553,"title":549,"titles":554,"content":555,"level":9},"/v1.0.2/guides/testing#testing",[],"Capitan provides utilities for testing event-driven code. The testing package offers helpers for capturing events, counting emissions, and recording panics. See Testing Package Reference for complete API documentation.",{"id":557,"title":368,"titles":558,"content":559,"level":19},"/v1.0.2/guides/testing#sync-mode",[549],"By default, capitan processes events asynchronously. For deterministic tests, use sync mode: func TestOrderWorkflow(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n\n    var received string\n    c.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n        if id, ok := orderID.From(e); ok {\n            received = id\n        }\n    })\n\n    c.Emit(context.Background(), orderCreated, orderID.Field(\"ORDER-123\"))\n\n    // No waiting needed - event processed synchronously\n    if received != \"ORDER-123\" {\n        t.Errorf(\"expected ORDER-123, got %s\", received)\n    }\n} In sync mode, Emit calls listeners directly instead of queueing to workers. This eliminates timing dependencies.",{"id":561,"title":562,"titles":563,"content":564,"level":19},"/v1.0.2/guides/testing#event-capture","Event Capture",[549],"The capitantesting.EventCapture helper captures events for assertions. Import the testing package: import capitantesting \"github.com/zoobz-io/capitan/testing\" Use it in tests: import (\n    \"testing\"\n\n    \"github.com/zoobz-io/capitan\"\n    capitantesting \"github.com/zoobz-io/capitan/testing\"\n)\n\nfunc TestEmitsOrderCreated(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n\n    capture := capitantesting.NewEventCapture()\n    c.Hook(orderCreated, capture.Handler())\n\n    // Exercise code that emits events\n    createOrder(\"ORDER-123\", 99.99)\n\n    // Assert captured events\n    events := capture.Events()\n    if len(events) != 1 {\n        t.Fatalf(\"expected 1 event, got %d\", len(events))\n    }\n\n    if events[0].Signal != orderCreated {\n        t.Errorf(\"wrong signal: %v\", events[0].Signal)\n    }\n}",{"id":566,"title":567,"titles":568,"content":569,"level":40},"/v1.0.2/guides/testing#extracting-field-values","Extracting Field Values",[549,562],"Use ExtractFromFields to get typed values from captured events: events := capture.Events()\nid := orderID.ExtractFromFields(events[0].Fields)\ntotal := totalKey.ExtractFromFields(events[0].Fields)\n\nif id != \"ORDER-123\" {\n    t.Errorf(\"expected ORDER-123, got %s\", id)\n}\nif total != 99.99 {\n    t.Errorf(\"expected 99.99, got %f\", total)\n}",{"id":571,"title":572,"titles":573,"content":574,"level":40},"/v1.0.2/guides/testing#waiting-for-events-async-mode","Waiting for Events (Async Mode)",[549,562],"When testing without sync mode, use WaitForCount: func TestAsyncEmission(t *testing.T) {\n    c := capitan.New() // async mode\n    defer c.Shutdown()\n\n    capture := capitantesting.NewEventCapture()\n    c.Hook(orderCreated, capture.Handler())\n\n    c.Emit(context.Background(), orderCreated, orderID.Field(\"ORDER-123\"))\n\n    // Wait for event to be processed\n    if !capture.WaitForCount(1, time.Second) {\n        t.Fatal(\"timeout waiting for event\")\n    }\n\n    events := capture.Events()\n    // ... assertions\n}",{"id":576,"title":577,"titles":578,"content":579,"level":40},"/v1.0.2/guides/testing#using-drain-for-synchronization","Using Drain for Synchronization",[549,562],"Alternatively, use Drain to wait for all queued events to process: func TestAsyncWithDrain(t *testing.T) {\n    c := capitan.New() // async mode\n    defer c.Shutdown()\n\n    var received string\n    c.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n        if id, ok := orderID.From(e); ok {\n            received = id\n        }\n    })\n\n    c.Emit(context.Background(), orderCreated, orderID.Field(\"ORDER-123\"))\n\n    // Wait for all queued events to process\n    ctx, cancel := context.WithTimeout(context.Background(), time.Second)\n    defer cancel()\n    if err := c.Drain(ctx); err != nil {\n        t.Fatal(\"drain timed out\")\n    }\n\n    if received != \"ORDER-123\" {\n        t.Errorf(\"expected ORDER-123, got %s\", received)\n    }\n} Unlike sync mode, Drain preserves async semantics while providing a synchronization point. Unlike Shutdown, it leaves workers running so you can continue emitting events.",{"id":581,"title":582,"titles":583,"content":584,"level":19},"/v1.0.2/guides/testing#event-counter","Event Counter",[549],"For tests that only need to verify event counts: func TestEmitsCorrectCount(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n\n    counter := capitantesting.NewEventCounter()\n    c.Hook(orderCreated, counter.Handler())\n\n    for i := 0; i \u003C 100; i++ {\n        c.Emit(context.Background(), orderCreated, orderID.Field(fmt.Sprintf(\"ORDER-%d\", i)))\n    }\n\n    if counter.Count() != 100 {\n        t.Errorf(\"expected 100 events, got %d\", counter.Count())\n    }\n}",{"id":586,"title":587,"titles":588,"content":589,"level":19},"/v1.0.2/guides/testing#panic-recording","Panic Recording",[549],"Test that panics are recovered and handled: func TestPanicRecovery(t *testing.T) {\n    recorder := capitantesting.NewPanicRecorder()\n    c := capitan.New(\n        capitan.WithSyncMode(),\n        capitan.WithPanicHandler(recorder.Handler()),\n    )\n    defer c.Shutdown()\n\n    c.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n        panic(\"intentional panic\")\n    })\n\n    // Should not crash\n    c.Emit(context.Background(), orderCreated, orderID.Field(\"ORDER-123\"))\n\n    panics := recorder.Panics()\n    if len(panics) != 1 {\n        t.Fatalf(\"expected 1 panic, got %d\", len(panics))\n    }\n\n    if panics[0].Recovered != \"intentional panic\" {\n        t.Errorf(\"wrong panic value: %v\", panics[0].Recovered)\n    }\n}",{"id":591,"title":592,"titles":593,"content":594,"level":19},"/v1.0.2/guides/testing#stats-waiter","Stats Waiter",[549],"For async tests that need to wait for specific conditions: func TestWorkerCreation(t *testing.T) {\n    c := capitan.New()\n    defer c.Shutdown()\n\n    waiter := capitantesting.NewStatsWaiter(c)\n\n    c.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {})\n    c.Emit(context.Background(), orderCreated, orderID.Field(\"ORDER-123\"))\n\n    // Wait for worker to be created\n    if !waiter.WaitForWorkers(1, time.Second) {\n        t.Fatal(\"timeout waiting for worker\")\n    }\n\n    // Wait for queue to drain\n    if !waiter.WaitForEmptyQueues(time.Second) {\n        t.Fatal(\"timeout waiting for queue to drain\")\n    }\n}",{"id":596,"title":597,"titles":598,"content":599,"level":19},"/v1.0.2/guides/testing#isolated-instances","Isolated Instances",[549],"Always create isolated instances for tests to avoid cross-test contamination: func TestFeatureA(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n    // Test with isolated instance\n}\n\nfunc TestFeatureB(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n    // Separate instance, no shared state\n} Avoid using the default singleton (capitan.Emit, capitan.Hook) in tests—instances don't reset between test runs. See Configuration for more on sync mode and instance creation.",{"id":601,"title":602,"titles":603,"content":604,"level":19},"/v1.0.2/guides/testing#testing-observers","Testing Observers",[549],"Capture events from observers the same way: func TestObserverReceivesAllEvents(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n\n    capture := capitantesting.NewEventCapture()\n    c.Observe(capture.Handler())\n\n    c.Emit(context.Background(), orderCreated, orderID.Field(\"ORDER-1\"))\n    c.Emit(context.Background(), orderShipped, orderID.Field(\"ORDER-1\"))\n\n    events := capture.Events()\n    if len(events) != 2 {\n        t.Fatalf(\"expected 2 events, got %d\", len(events))\n    }\n\n    if events[0].Signal != orderCreated {\n        t.Errorf(\"first event should be orderCreated\")\n    }\n    if events[1].Signal != orderShipped {\n        t.Errorf(\"second event should be orderShipped\")\n    }\n}",{"id":606,"title":607,"titles":608,"content":609,"level":19},"/v1.0.2/guides/testing#testing-workflows","Testing Workflows",[549],"For workflow tests, capture at multiple points: func TestOrderWorkflow(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n\n    orderCapture := capitantesting.NewEventCapture()\n    inventoryCapture := capitantesting.NewEventCapture()\n\n    c.Hook(orderCreated, orderCapture.Handler())\n    c.Hook(inventoryReserved, inventoryCapture.Handler())\n\n    // Wire workflow: order.created → inventory.reserved\n    c.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n        id, ok := orderID.From(e)\n        if !ok {\n            return\n        }\n        c.Emit(ctx, inventoryReserved, orderID.Field(id))\n    })\n\n    // Trigger workflow\n    c.Emit(context.Background(), orderCreated, orderID.Field(\"ORDER-123\"))\n\n    // Verify both events fired\n    if orderCapture.Count() != 1 {\n        t.Error(\"order.created not captured\")\n    }\n    if inventoryCapture.Count() != 1 {\n        t.Error(\"inventory.reserved not captured\")\n    }\n}",{"id":611,"title":612,"titles":613,"content":614,"level":19},"/v1.0.2/guides/testing#see-also","See Also",[549],"Testing Package Reference - Complete API documentation for all testing utilitiesConfiguration - Sync mode and instance creation optionsBest Practices - Testing guidelines and recommendations 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 .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 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 .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"id":616,"title":617,"titles":618,"content":619,"level":9},"/v1.0.2/guides/best-practices","Best Practices",[],"Guidelines for using capitan effectively in production",{"id":621,"title":617,"titles":622,"content":623,"level":9},"/v1.0.2/guides/best-practices#best-practices",[],"Guidelines for using capitan effectively in production systems.",{"id":625,"title":626,"titles":627,"content":34,"level":19},"/v1.0.2/guides/best-practices#signal-design","Signal Design",[617],{"id":629,"title":630,"titles":631,"content":632,"level":40},"/v1.0.2/guides/best-practices#define-signals-as-package-constants","Define Signals as Package Constants",[617,626],"Signals create internal state (workers, registries). Define them at package level: // signals.go\npackage orders\n\nimport \"github.com/zoobz-io/capitan\"\n\nvar (\n    OrderCreated   = capitan.NewSignal(\"order.created\", \"New order placed\")\n    OrderConfirmed = capitan.NewSignal(\"order.confirmed\", \"Order confirmed\")\n    OrderShipped   = capitan.NewSignal(\"order.shipped\", \"Order shipped\")\n    OrderCanceled  = capitan.NewSignal(\"order.canceled\", \"Order canceled\")\n) Never create signals dynamically: // Bad: unbounded signal creation\nsignal := capitan.NewSignal(fmt.Sprintf(\"user.%s.action\", userID), \"...\")\n\n// Good: use fields for variable data\ncapitan.Emit(ctx, userAction, userID.Field(id)) Signals are compared by identity, not name. Using different instances with the same name won't match: // Wrong: different signal instances\nvar SignalA = capitan.NewSignal(\"order.created\", \"...\")\nvar SignalB = capitan.NewSignal(\"order.created\", \"...\") // Different instance\n\ncapitan.Hook(SignalA, handler)\ncapitan.Emit(ctx, SignalB, fields...) // Won't match SignalA\n\n// Correct: import and use the same signal\nimport \"myapp/orders\"\ncapitan.Hook(orders.OrderCreated, handler)\ncapitan.Emit(ctx, orders.OrderCreated, fields...)",{"id":634,"title":635,"titles":636,"content":637,"level":40},"/v1.0.2/guides/best-practices#use-hierarchical-naming","Use Hierarchical Naming",[617,626],"Name signals with dot-separated hierarchies: // Domain.action pattern\n\"order.created\"\n\"order.shipped\"\n\"payment.processed\"\n\"payment.failed\"\n\"inventory.reserved\"\n\"inventory.released\" Benefits: Grep-friendly (grep \"order\\.\")Observer whitelisting by prefix (manual filtering)Clear ownership boundaries",{"id":639,"title":640,"titles":641,"content":642,"level":40},"/v1.0.2/guides/best-practices#write-descriptive-descriptions","Write Descriptive Descriptions",[617,626],"The description appears in logs and debugging. Make it human-readable: // Good: describes what happened\ncapitan.NewSignal(\"order.created\", \"New order placed by customer\")\ncapitan.NewSignal(\"payment.failed\", \"Payment processing failed\")\n\n// Bad: repeats the name\ncapitan.NewSignal(\"order.created\", \"order.created\")\ncapitan.NewSignal(\"payment.failed\", \"PaymentFailed\")",{"id":644,"title":645,"titles":646,"content":34,"level":19},"/v1.0.2/guides/best-practices#key-design","Key Design",[617],{"id":648,"title":649,"titles":650,"content":651,"level":40},"/v1.0.2/guides/best-practices#define-keys-alongside-signals","Define Keys Alongside Signals",[617,645],"Keep keys with their signals for discoverability: // orders/signals.go\npackage orders\n\nvar (\n    OrderCreated = capitan.NewSignal(\"order.created\", \"New order placed\")\n    // ... other signals\n)\n\nvar (\n    OrderID    = capitan.NewStringKey(\"order_id\")\n    CustomerID = capitan.NewStringKey(\"customer_id\")\n    Total      = capitan.NewFloat64Key(\"total\")\n)",{"id":653,"title":654,"titles":655,"content":656,"level":40},"/v1.0.2/guides/best-practices#use-consistent-key-names","Use Consistent Key Names",[617,645],"Establish naming conventions across your codebase: // Consistent: snake_case\norderID := capitan.NewStringKey(\"order_id\")\ncustomerID := capitan.NewStringKey(\"customer_id\")\ncreatedAt := capitan.NewTimeKey(\"created_at\")\n\n// Inconsistent: mixed styles\norderID := capitan.NewStringKey(\"orderId\")\ncustomer_id := capitan.NewStringKey(\"customer-id\")",{"id":658,"title":659,"titles":660,"content":661,"level":40},"/v1.0.2/guides/best-practices#prefer-specific-types","Prefer Specific Types",[617,645],"Use the most specific key type: // Good: typed keys\ncount := capitan.NewIntKey(\"count\")\ntotal := capitan.NewFloat64Key(\"total\")\nactive := capitan.NewBoolKey(\"active\")\ncreatedAt := capitan.NewTimeKey(\"created_at\")\n\n// Avoid: stringly-typed\ncount := capitan.NewStringKey(\"count\")  // \"42\" instead of 42",{"id":663,"title":664,"titles":665,"content":34,"level":19},"/v1.0.2/guides/best-practices#lifecycle-management","Lifecycle Management",[617],{"id":667,"title":668,"titles":669,"content":670,"level":40},"/v1.0.2/guides/best-practices#close-listeners-on-shutdown","Close Listeners on Shutdown",[617,664],"In long-running services, close listeners during graceful shutdown: func main() {\n    // Register listeners\n    orderListener := capitan.Hook(orderCreated, handleOrder)\n    paymentListener := capitan.Hook(paymentProcessed, handlePayment)\n    observer := capitan.Observe(logEvent)\n\n    // Setup shutdown handling\n    stop := make(chan os.Signal, 1)\n    signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)\n\n    // Run server...\n    \u003C-stop\n\n    // Close listeners (stops new event delivery)\n    orderListener.Close()\n    paymentListener.Close()\n    observer.Close()\n\n    // Drain pending events\n    capitan.Shutdown()\n}",{"id":672,"title":673,"titles":674,"content":675,"level":40},"/v1.0.2/guides/best-practices#scope-listeners-to-components","Scope Listeners to Components",[617,664],"Close listeners when their owning component stops: type OrderProcessor struct {\n    listener *capitan.Listener\n}\n\nfunc NewOrderProcessor() *OrderProcessor {\n    p := &OrderProcessor{}\n    p.listener = capitan.Hook(orderCreated, p.handle)\n    return p\n}\n\nfunc (p *OrderProcessor) handle(ctx context.Context, e *capitan.Event) {\n    // Process order...\n}\n\nfunc (p *OrderProcessor) Stop() {\n    p.listener.Close()\n}",{"id":677,"title":678,"titles":679,"content":680,"level":40},"/v1.0.2/guides/best-practices#use-isolated-instances-for-modules","Use Isolated Instances for Modules",[617,664],"Separate subsystems can use separate instances: // Billing module\nvar billingCapitan = capitan.New(capitan.WithBufferSize(64))\n\n// Analytics module\nvar analyticsCapitan = capitan.New(capitan.WithBufferSize(256))\n\nfunc ShutdownAll() {\n    billingCapitan.Shutdown()\n    analyticsCapitan.Shutdown()\n}",{"id":682,"title":683,"titles":684,"content":34,"level":19},"/v1.0.2/guides/best-practices#error-handling","Error Handling",[617],{"id":686,"title":687,"titles":688,"content":689,"level":40},"/v1.0.2/guides/best-practices#always-check-field-extraction","Always Check Field Extraction",[617,683],"Fields may be missing or have wrong types: capitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n    orderID, ok := orderIDKey.From(e)\n    if !ok {\n        log.Printf(\"Missing order_id in %s event\", e.Signal().Name())\n        return\n    }\n    // Proceed with orderID...\n})",{"id":691,"title":692,"titles":693,"content":694,"level":40},"/v1.0.2/guides/best-practices#use-severity-appropriately","Use Severity Appropriately",[617,683],"Reserve error severity for actual errors: // Info: normal operations\ncapitan.Emit(ctx, orderCreated, fields...)\n\n// Warn: unusual but handled\ncapitan.Warn(ctx, lowStock, fields...)\n\n// Error: failures requiring attention\ncapitan.Error(ctx, paymentFailed, fields...)\n\n// Debug: development only\ncapitan.Debug(ctx, queryExecuted, fields...)",{"id":696,"title":697,"titles":698,"content":699,"level":40},"/v1.0.2/guides/best-practices#configure-panic-handlers-in-production","Configure Panic Handlers in Production",[617,683],"Never run production without panic visibility: capitan.Configure(\n    capitan.WithPanicHandler(func(sig capitan.Signal, recovered any) {\n        log.Printf(\"PANIC in %s: %v\", sig.Name(), recovered)\n        metrics.Increment(\"capitan_panics_total\", \"signal\", sig.Name())\n\n        // Optional: alert on-call\n        if shouldAlert(sig) {\n            alerting.Page(\"Listener panic\", sig.Name(), recovered)\n        }\n    }),\n)",{"id":701,"title":48,"titles":702,"content":34,"level":19},"/v1.0.2/guides/best-practices#performance",[617],{"id":704,"title":705,"titles":706,"content":707,"level":40},"/v1.0.2/guides/best-practices#dont-block-in-listeners","Don't Block in Listeners",[617,48],"Listeners run in worker goroutines. Blocking affects all events for that signal: // Bad: blocking call in listener\ncapitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n    time.Sleep(5 * time.Second)  // Blocks worker\n    http.Post(url, body)          // Blocking I/O\n})\n\n// Good: offload blocking work\ncapitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n    go func() {\n        // Blocking work in separate goroutine\n        http.Post(url, body)\n    }()\n})\n\n// Better: use a work queue\ncapitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n    workQueue \u003C- extractWork(e)\n})",{"id":709,"title":710,"titles":711,"content":712,"level":40},"/v1.0.2/guides/best-practices#dont-hold-event-references","Don't Hold Event References",[617,48],"Events are pooled and reused. Copy data you need to retain: // Bad: holding event reference\nvar lastEvent *capitan.Event\ncapitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n    lastEvent = e  // Dangerous: event will be reused\n})\n\n// Good: copy needed data\nvar lastOrderID string\ncapitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n    if id, ok := orderIDKey.From(e); ok {\n        lastOrderID = id\n    }\n})\n\n// Also good: use Clone() when you need the full event\nvar lastEvent *capitan.Event\ncapitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n    lastEvent = e.Clone()  // Safe: independent copy\n})",{"id":714,"title":715,"titles":716,"content":717,"level":40},"/v1.0.2/guides/best-practices#size-buffers-appropriately","Size Buffers Appropriately",[617,48],"Monitor queue depths and adjust: // Start with defaults\ncapitan.Configure(capitan.WithBufferSize(16))\n\n// Monitor in production\ngo func() {\n    for range time.Tick(time.Minute) {\n        stats := capitan.Stats()\n        for signal, depth := range stats.QueueDepths {\n            if depth > 10 {\n                log.Printf(\"High queue depth: %s = %d\", signal.Name(), depth)\n            }\n        }\n    }\n}()\n\n// Increase if queues consistently fill\ncapitan.Configure(capitan.WithBufferSize(128))",{"id":719,"title":549,"titles":720,"content":34,"level":19},"/v1.0.2/guides/best-practices#testing",[617],{"id":722,"title":723,"titles":724,"content":725,"level":40},"/v1.0.2/guides/best-practices#use-sync-mode-for-unit-tests","Use Sync Mode for Unit Tests",[617,549],"Eliminates timing dependencies: func TestOrderHandler(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n\n    // Events process synchronously - no waiting needed\n}",{"id":727,"title":728,"titles":729,"content":730,"level":40},"/v1.0.2/guides/best-practices#use-isolated-instances","Use Isolated Instances",[617,549],"Avoid cross-test contamination: func TestA(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n    // ...\n}\n\nfunc TestB(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n    // Separate instance, clean state\n}",{"id":732,"title":733,"titles":734,"content":735,"level":40},"/v1.0.2/guides/best-practices#never-use-default-instance-in-tests","Never Use Default Instance in Tests",[617,549],"The default singleton persists across tests: // Bad: uses shared singleton\nfunc TestBad(t *testing.T) {\n    capitan.Hook(signal, handler)  // Leaks into other tests\n}\n\n// Good: isolated instance\nfunc TestGood(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    c.Hook(signal, handler)\n    defer c.Shutdown()\n} 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 .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}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 .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}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 .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}",{"id":737,"title":738,"titles":739,"content":740,"level":9},"/v1.0.2/guides/persistence","Persistence",[],"Event storage and replay for audit trails and debugging",{"id":742,"title":738,"titles":743,"content":744,"level":9},"/v1.0.2/guides/persistence#persistence",[],"Store events to a database for audit trails, debugging, and replay. An observer captures events; the replay API re-emits them on demand.",{"id":746,"title":747,"titles":748,"content":749,"level":19},"/v1.0.2/guides/persistence#the-pattern","The Pattern",[738],"Emit(ctx, signal, fields...)\n       │\n       ▼\n┌─────────────┐\n│   Capitan   │───▶ Listeners\n└──────┬──────┘\n       │\n ┌─────▼─────┐\n │  Observer │\n │ (persist) │\n └─────┬─────┘\n       │\n ┌─────▼─────┐\n │  Database │\n └─────┬─────┘\n       │\n ┌─────▼─────┐\n │  Replay() │\n └───────────┘",{"id":751,"title":752,"titles":753,"content":754,"level":19},"/v1.0.2/guides/persistence#storage-format","Storage Format",[738],"Capture all event data for reconstruction: type StoredEvent struct {\n    ID        string         `json:\"id\"`\n    Signal    string         `json:\"signal\"`\n    Severity  string         `json:\"severity\"`\n    Timestamp time.Time      `json:\"timestamp\"`\n    Fields    map[string]any `json:\"fields\"`\n}\n\nfunc FromEvent(e *capitan.Event) StoredEvent {\n    fields := make(map[string]any)\n    for _, f := range e.Fields() {\n        fields[f.Key().Name()] = f.Value()\n    }\n    return StoredEvent{\n        ID:        generateID(),\n        Signal:    e.Signal().Name(),\n        Severity:  string(e.Severity()),\n        Timestamp: e.Timestamp(),\n        Fields:    fields,\n    }\n}",{"id":756,"title":757,"titles":758,"content":759,"level":19},"/v1.0.2/guides/persistence#persistence-observer","Persistence Observer",[738],"Attach an observer that writes events to storage: func PersistenceObserver(store *EventStore) capitan.EventCallback {\n    return func(ctx context.Context, e *capitan.Event) {\n        if e.IsReplay() {\n            return // Don't re-persist replayed events\n        }\n        store.Save(ctx, FromEvent(e))\n    }\n}\n\n// Attach at startup\ncapitan.Observe(PersistenceObserver(store))",{"id":761,"title":762,"titles":763,"content":764,"level":40},"/v1.0.2/guides/persistence#selective-persistence","Selective Persistence",[738,757],"Persist only specific signals if storage is constrained: capitan.Observe(PersistenceObserver(store),\n    OrderCreated,\n    OrderConfirmed,\n    PaymentFailed, // Persist failures for debugging\n)",{"id":766,"title":195,"titles":767,"content":768,"level":19},"/v1.0.2/guides/persistence#replay",[738],"Reconstruct and re-emit stored events: func Replay(ctx context.Context, stored StoredEvent, signals map[string]capitan.Signal) {\n    signal, ok := signals[stored.Signal]\n    if !ok {\n        return // Unknown signal\n    }\n\n    fields := decodeFields(stored.Fields)\n    severity := parseSeverity(stored.Severity)\n\n    e := capitan.NewEvent(signal, severity, stored.Timestamp, fields...)\n    capitan.Replay(ctx, e)\n}",{"id":770,"title":771,"titles":772,"content":773,"level":40},"/v1.0.2/guides/persistence#signal-registry","Signal Registry",[738,195],"Map signal names to values for replay: var signals = map[string]capitan.Signal{\n    \"order.created\":   OrderCreated,\n    \"order.confirmed\": OrderConfirmed,\n    \"payment.failed\":  PaymentFailed,\n}",{"id":775,"title":776,"titles":777,"content":778,"level":19},"/v1.0.2/guides/persistence#handling-replays-in-listeners","Handling Replays in Listeners",[738],"Check IsReplay() to skip side effects: capitan.Hook(OrderConfirmed, func(ctx context.Context, e *capitan.Event) {\n    // Skip external calls during replay\n    if !e.IsReplay() {\n        sendConfirmationEmail(e)\n    }\n\n    // Always update internal state\n    recordOrderConfirmed(e)\n})",{"id":780,"title":781,"titles":782,"content":34,"level":19},"/v1.0.2/guides/persistence#considerations","Considerations",[738],{"id":784,"title":785,"titles":786,"content":787,"level":40},"/v1.0.2/guides/persistence#idempotency","Idempotency",[738,781],"Listeners should handle replay safely. Use IsReplay() to guard side effects like emails, payments, or external API calls.",{"id":789,"title":790,"titles":791,"content":792,"level":40},"/v1.0.2/guides/persistence#replay-ordering","Replay Ordering",[738,781],"Events replay in the order you query them. Use timestamp ordering to preserve original sequence.",{"id":794,"title":795,"titles":796,"content":797,"level":40},"/v1.0.2/guides/persistence#storage-growth","Storage Growth",[738,781],"Events accumulate. Implement retention policies: func (s *EventStore) Prune(ctx context.Context, olderThan time.Duration) error {\n    cutoff := time.Now().Add(-olderThan)\n    _, err := s.db.ExecContext(ctx, `DELETE FROM events WHERE timestamp \u003C $1`, cutoff)\n    return err\n}",{"id":799,"title":800,"titles":801,"content":802,"level":40},"/v1.0.2/guides/persistence#replay-vs-normal-processing","Replay vs Normal Processing",[738,781],"AspectNormalReplayProcessingAsync (worker queue)Sync (calling goroutine)PoolingEvents pooled and reusedEvents not pooledIsReplay()falsetruePersistenceStored by observerSkipped (already stored)",{"id":804,"title":805,"titles":806,"content":807,"level":19},"/v1.0.2/guides/persistence#use-cases","Use Cases",[738],"Audit trails — Complete record of system activityDebugging — Replay events to reproduce issuesBackfilling — Populate new listeners with historical dataTesting — Replay production events in test environments",{"id":809,"title":612,"titles":810,"content":811,"level":19},"/v1.0.2/guides/persistence#see-also",[738],"Concepts: Replay — API details for NewEvent, Replay, IsReplayTesting — Testing patterns for replay scenarios 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 .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 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 .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)}",{"id":813,"title":814,"titles":815,"content":816,"level":9},"/v1.0.2/integrations/aperture","aperture",[],"OpenTelemetry observability from capitan events",{"id":818,"title":814,"titles":819,"content":820,"level":9},"/v1.0.2/integrations/aperture#aperture",[],"Transforms capitan events into OpenTelemetry spans, metrics, and logs.",{"id":822,"title":823,"titles":824,"content":825,"level":19},"/v1.0.2/integrations/aperture#the-pipeline","The Pipeline",[814],"// Your application emits events\ncapitan.Emit(ctx, UserCreated, userID.Field(\"usr_123\")) // aperture observes and exports to OTEL\naperture.Attach(capitan.Default(),\n    aperture.WithTracerProvider(provider),\n) Emit(ctx, signal, fields...)\n       │\n       ▼\n┌─────────────┐\n│  aperture   │\n│  observer   │\n└──────┬──────┘\n       │\n       ▼\n┌─────────────┐\n│    OTEL     │\n│  exporter   │\n└─────────────┘",{"id":827,"title":828,"titles":829,"content":830,"level":19},"/v1.0.2/integrations/aperture#what-capitan-provides","What Capitan Provides",[814],"aperture needsCapitan providesSpan namesSignal.Name()Span attributesEvent.Fields() as key-value pairsTimestampsEvent.Timestamp()Severity mappingEvent.Severity() → OTEL severityTrace contextEvent.Context() propagationSelective exportObserve(callback, signals...) whitelist",{"id":832,"title":833,"titles":834,"content":835,"level":19},"/v1.0.2/integrations/aperture#learn-more","Learn More",[814],"aperture repository html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}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);}",{"id":837,"title":838,"titles":839,"content":840,"level":9},"/v1.0.2/integrations/herald","herald",[],"Message broker bridge for distributed capitan events",{"id":842,"title":838,"titles":843,"content":844,"level":9},"/v1.0.2/integrations/herald#herald",[],"Bridges capitan to external message systems like Kafka, NATS, and RabbitMQ.",{"id":846,"title":847,"titles":848,"content":849,"level":19},"/v1.0.2/integrations/herald#the-problem","The Problem",[838],"Capitan coordinates events within a process. Distributed systems need events to flow across service boundaries—consuming from external topics, processing locally, and publishing results back.",{"id":851,"title":823,"titles":852,"content":853,"level":19},"/v1.0.2/integrations/herald#the-pipeline",[838],"h := herald.New(kafkaClient)\n\n// Inbound: external topic → capitan signal\nh.Subscribe(\"tasks.created\", TaskCreated)\n\n// Outbound: capitan signal → external topic\nh.Publish(TaskCompleted, \"tasks.completed\")\n\nh.Start(ctx) External Topic              Capitan                External Topic\n      │                        │                        ▲\n      │    ┌───────────┐       │                        │\n      └───▶│  herald   │───▶ signal ───▶ listeners ─────┘\n           │  bridge   │◀─── signal ◀─── Emit()\n           └───────────┘",{"id":855,"title":828,"titles":856,"content":857,"level":19},"/v1.0.2/integrations/herald#what-capitan-provides",[838],"herald needsCapitan providesEvent routingSignal as topic binding targetInbound emissionEmit(ctx, signal, fields...)Outbound captureHook(signal, callback) for publishingPayload dataEvent.Fields() for serializationTrace propagationEvent.Context() carries trace headers",{"id":859,"title":833,"titles":860,"content":861,"level":19},"/v1.0.2/integrations/herald#learn-more",[838],"herald repository 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 .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}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);}",{"id":863,"title":864,"titles":865,"content":866,"level":9},"/v1.0.2/integrations/ago","ago",[],"Workflow orchestration with capitan event choreography",{"id":868,"title":864,"titles":869,"content":870,"level":9},"/v1.0.2/integrations/ago#ago",[],"Experimental — ago is under active development. API may change. Orchestrates multi-step workflows through capitan event choreography.",{"id":872,"title":847,"titles":873,"content":874,"level":19},"/v1.0.2/integrations/ago#the-problem",[864],"Event-driven workflows require coordination: one signal triggers the next, errors fork to recovery paths, and the full sequence needs visibility. Building this from raw listeners means reimplementing routing, error handling, and tracing for every workflow.",{"id":876,"title":823,"titles":877,"content":878,"level":19},"/v1.0.2/integrations/ago#the-pipeline",[864],"workflow := ago.New(\"task-processing\").\n    On(TaskCreated).\n    Then(ValidateTask, AssignWorker).\n    On(TaskCompleted).\n    Then(NotifyRequester, UpdateMetrics).\n    OnError(TaskFailed).\n    Then(RetryOrEscalate)\n\nworkflow.Attach(capitan.Default()) TaskCreated\n     │\n     ├──▶ ValidateTask\n     └──▶ AssignWorker\n              │\nTaskCompleted ├──▶ NotifyRequester\n              └──▶ UpdateMetrics\n\nTaskFailed ──────▶ RetryOrEscalate",{"id":880,"title":828,"titles":881,"content":882,"level":19},"/v1.0.2/integrations/ago#what-capitan-provides",[864],"ago needsCapitan providesTrigger eventsSignal definitionsEvent routingHook(signal, callback)Workflow branchingMultiple signals for success/error pathsEvent emissionEmit(ctx, signal, fields...)Trace correlationEvent.Context() propagationField forwardingEvent.Fields() passed through chain",{"id":884,"title":833,"titles":885,"content":886,"level":19},"/v1.0.2/integrations/ago#learn-more",[864],"ago repository 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);}",{"id":888,"title":889,"titles":890,"content":891,"level":9},"/v1.0.2/reference/api","API Reference",[],"Complete API reference for the capitan package",{"id":893,"title":889,"titles":894,"content":895,"level":9},"/v1.0.2/reference/api#api-reference",[],"Complete API reference for the github.com/zoobz-io/capitan package.",{"id":897,"title":898,"titles":899,"content":900,"level":19},"/v1.0.2/reference/api#module-level-functions","Module-Level Functions",[889],"These functions operate on the default singleton instance. Optionally use Configure before any other calls to customize options.",{"id":902,"title":903,"titles":904,"content":905,"level":40},"/v1.0.2/reference/api#emit","Emit",[889,898],"func Emit(ctx context.Context, signal Signal, fields ...Field) Dispatches an event with Info severity. The event is queued to the signal's worker goroutine for asynchronous processing. Events are silently dropped if emitted after Shutdown has been called.",{"id":907,"title":908,"titles":909,"content":910,"level":40},"/v1.0.2/reference/api#debug","Debug",[889,898],"func Debug(ctx context.Context, signal Signal, fields ...Field) Dispatches an event with Debug severity.",{"id":912,"title":913,"titles":914,"content":915,"level":40},"/v1.0.2/reference/api#info","Info",[889,898],"func Info(ctx context.Context, signal Signal, fields ...Field) Dispatches an event with Info severity. Equivalent to Emit.",{"id":917,"title":918,"titles":919,"content":920,"level":40},"/v1.0.2/reference/api#warn","Warn",[889,898],"func Warn(ctx context.Context, signal Signal, fields ...Field) Dispatches an event with Warn severity.",{"id":922,"title":923,"titles":924,"content":925,"level":40},"/v1.0.2/reference/api#error","Error",[889,898],"func Error(ctx context.Context, signal Signal, fields ...Field) Dispatches an event with Error severity.",{"id":927,"title":195,"titles":928,"content":929,"level":40},"/v1.0.2/reference/api#replay",[889,898],"func Replay(ctx context.Context, e *Event) Re-emits a historical event. Replay events process synchronously in the calling goroutine, bypass worker queues, and are marked as replays (check with e.IsReplay()).",{"id":931,"title":932,"titles":933,"content":934,"level":40},"/v1.0.2/reference/api#hook","Hook",[889,898],"func Hook(signal Signal, callback EventCallback) *Listener Registers a listener for a signal. Returns a *Listener that can be closed to unregister.",{"id":936,"title":937,"titles":938,"content":939,"level":40},"/v1.0.2/reference/api#hookonce","HookOnce",[889,898],"func HookOnce(signal Signal, callback EventCallback) *Listener Registers a listener that fires only once, then automatically unregisters. Returns a *Listener that can be closed early to prevent the callback from firing.",{"id":941,"title":942,"titles":943,"content":944,"level":40},"/v1.0.2/reference/api#observe","Observe",[889,898],"func Observe(callback EventCallback, signals ...Signal) *Observer Registers an observer. If signals are provided, observes only those signals. If no signals are provided, observes all signals including those created after registration.",{"id":946,"title":947,"titles":948,"content":949,"level":40},"/v1.0.2/reference/api#stats","Stats",[889,898],"func Stats() Stats Returns a snapshot of runtime metrics.",{"id":951,"title":952,"titles":953,"content":954,"level":40},"/v1.0.2/reference/api#configure","Configure",[889,898],"func Configure(opts ...Option) Sets options for the default instance. Must be called before any other capitan function. Configuration is locked after the default instance is created.",{"id":956,"title":957,"titles":958,"content":959,"level":40},"/v1.0.2/reference/api#applyconfig","ApplyConfig",[889,898],"func ApplyConfig(cfg Config) error Applies per-signal configuration to the default instance. Replaces any previous config entirely. See Configuration Guide for details.",{"id":961,"title":962,"titles":963,"content":964,"level":40},"/v1.0.2/reference/api#shutdown","Shutdown",[889,898],"func Shutdown() Drains pending events and stops all worker goroutines. Safe to call multiple times.",{"id":966,"title":967,"titles":968,"content":969,"level":40},"/v1.0.2/reference/api#default","Default",[889,898],"func Default() *Capitan Returns the default singleton instance, creating it if necessary.",{"id":971,"title":972,"titles":973,"content":974,"level":19},"/v1.0.2/reference/api#capitan","Capitan",[889],"The main coordinator type. Create isolated instances with New.",{"id":976,"title":977,"titles":978,"content":979,"level":40},"/v1.0.2/reference/api#new","New",[889,972],"func New(opts ...Option) *Capitan Creates a new instance with the given options. Each instance has independent workers, buffers, and registries.",{"id":981,"title":982,"titles":983,"content":34,"level":40},"/v1.0.2/reference/api#methods","Methods",[889,972],{"id":985,"title":932,"titles":986,"content":987,"level":988},"/v1.0.2/reference/api#hook-1",[889,972,982],"func (c *Capitan) Hook(signal Signal, callback EventCallback) *Listener Registers a listener for a signal on this instance. Returns nil if MaxListeners is configured and the limit is reached.",4,{"id":990,"title":937,"titles":991,"content":992,"level":988},"/v1.0.2/reference/api#hookonce-1",[889,972,982],"func (c *Capitan) HookOnce(signal Signal, callback EventCallback) *Listener Registers a listener that fires only once, then automatically unregisters.",{"id":994,"title":942,"titles":995,"content":996,"level":988},"/v1.0.2/reference/api#observe-1",[889,972,982],"func (c *Capitan) Observe(callback EventCallback, signals ...Signal) *Observer Registers an observer on this instance.",{"id":998,"title":903,"titles":999,"content":1000,"level":988},"/v1.0.2/reference/api#emit-1",[889,972,982],"func (c *Capitan) Emit(ctx context.Context, signal Signal, fields ...Field) Dispatches an event with Info severity on this instance.",{"id":1002,"title":908,"titles":1003,"content":1004,"level":988},"/v1.0.2/reference/api#debug-1",[889,972,982],"func (c *Capitan) Debug(ctx context.Context, signal Signal, fields ...Field) Dispatches an event with Debug severity on this instance.",{"id":1006,"title":913,"titles":1007,"content":1008,"level":988},"/v1.0.2/reference/api#info-1",[889,972,982],"func (c *Capitan) Info(ctx context.Context, signal Signal, fields ...Field) Dispatches an event with Info severity on this instance.",{"id":1010,"title":918,"titles":1011,"content":1012,"level":988},"/v1.0.2/reference/api#warn-1",[889,972,982],"func (c *Capitan) Warn(ctx context.Context, signal Signal, fields ...Field) Dispatches an event with Warn severity on this instance.",{"id":1014,"title":923,"titles":1015,"content":1016,"level":988},"/v1.0.2/reference/api#error-1",[889,972,982],"func (c *Capitan) Error(ctx context.Context, signal Signal, fields ...Field) Dispatches an event with Error severity on this instance.",{"id":1018,"title":195,"titles":1019,"content":1020,"level":988},"/v1.0.2/reference/api#replay-1",[889,972,982],"func (c *Capitan) Replay(ctx context.Context, e *Event) Re-emits a historical event on this instance.",{"id":1022,"title":947,"titles":1023,"content":1024,"level":988},"/v1.0.2/reference/api#stats-1",[889,972,982],"func (c *Capitan) Stats() Stats Returns runtime metrics for this instance.",{"id":1026,"title":962,"titles":1027,"content":1028,"level":988},"/v1.0.2/reference/api#shutdown-1",[889,972,982],"func (c *Capitan) Shutdown() Drains pending events and stops workers for this instance.",{"id":1030,"title":1031,"titles":1032,"content":1033,"level":988},"/v1.0.2/reference/api#isshutdown","IsShutdown",[889,972,982],"func (c *Capitan) IsShutdown() bool Reports whether Shutdown has been called on this instance.",{"id":1035,"title":1036,"titles":1037,"content":1038,"level":988},"/v1.0.2/reference/api#drain","Drain",[889,972,982],"func (c *Capitan) Drain(ctx context.Context) error Blocks until all currently queued events have been processed. Unlike Shutdown, workers remain active after draining. Returns an error if the context is cancelled before drain completes.",{"id":1040,"title":957,"titles":1041,"content":1042,"level":988},"/v1.0.2/reference/api#applyconfig-1",[889,972,982],"func (c *Capitan) ApplyConfig(cfg Config) error Applies per-signal configuration. Replaces any previous config entirely—signals not in the new config revert to defaults. Only rebuilds workers whose resolved config actually changed.",{"id":1044,"title":1045,"titles":1046,"content":1047,"level":19},"/v1.0.2/reference/api#signal","Signal",[889],"Identifies an event type for routing.",{"id":1049,"title":1050,"titles":1051,"content":1052,"level":40},"/v1.0.2/reference/api#newsignal","NewSignal",[889,1045],"func NewSignal(name, description string) Signal Creates a signal with a name (used for routing) and human-readable description.",{"id":1054,"title":982,"titles":1055,"content":34,"level":40},"/v1.0.2/reference/api#methods-1",[889,1045],{"id":1057,"title":1058,"titles":1059,"content":1060,"level":988},"/v1.0.2/reference/api#name","Name",[889,1045,982],"func (s Signal) Name() string Returns the signal's identifier.",{"id":1062,"title":1063,"titles":1064,"content":1065,"level":988},"/v1.0.2/reference/api#description","Description",[889,1045,982],"func (s Signal) Description() string Returns the signal's human-readable description.",{"id":1067,"title":1068,"titles":1069,"content":1070,"level":19},"/v1.0.2/reference/api#event","Event",[889],"Represents a signal emission with typed fields.",{"id":1072,"title":1073,"titles":1074,"content":1075,"level":40},"/v1.0.2/reference/api#newevent","NewEvent",[889,1068],"func NewEvent(signal Signal, severity Severity, timestamp time.Time, fields ...Field) *Event Creates an event for replay. Unlike internally emitted events, these are not pooled and can be held safely.",{"id":1077,"title":982,"titles":1078,"content":34,"level":40},"/v1.0.2/reference/api#methods-2",[889,1068],{"id":1080,"title":1045,"titles":1081,"content":1082,"level":988},"/v1.0.2/reference/api#signal-1",[889,1068,982],"func (e *Event) Signal() Signal Returns the event's signal.",{"id":1084,"title":1085,"titles":1086,"content":1087,"level":988},"/v1.0.2/reference/api#timestamp","Timestamp",[889,1068,982],"func (e *Event) Timestamp() time.Time Returns when the event was created.",{"id":1089,"title":427,"titles":1090,"content":1091,"level":988},"/v1.0.2/reference/api#context",[889,1068,982],"func (e *Event) Context() context.Context Returns the context passed at emission time.",{"id":1093,"title":1094,"titles":1095,"content":1096,"level":988},"/v1.0.2/reference/api#severity","Severity",[889,1068,982],"func (e *Event) Severity() Severity Returns the event's severity level.",{"id":1098,"title":1099,"titles":1100,"content":1101,"level":988},"/v1.0.2/reference/api#isreplay","IsReplay",[889,1068,982],"func (e *Event) IsReplay() bool Returns true if this event was replayed from storage.",{"id":1103,"title":1104,"titles":1105,"content":1106,"level":988},"/v1.0.2/reference/api#get","Get",[889,1068,982],"func (e *Event) Get(key Key) Field Returns the field for the given key, or nil if not present.",{"id":1108,"title":110,"titles":1109,"content":1110,"level":988},"/v1.0.2/reference/api#fields",[889,1068,982],"func (e *Event) Fields() []Field Returns all fields as a defensive copy.",{"id":1112,"title":1113,"titles":1114,"content":1115,"level":988},"/v1.0.2/reference/api#clone","Clone",[889,1068,982],"func (e *Event) Clone() *Event Creates a deep copy of the event that is safe to retain. Use this when you need to store or pass an event beyond the listener callback, as pooled events are recycled after all listeners complete.",{"id":1117,"title":1118,"titles":1119,"content":1120,"level":19},"/v1.0.2/reference/api#listener","Listener",[889],"Active subscription to a signal.",{"id":1122,"title":1123,"titles":1124,"content":1125,"level":40},"/v1.0.2/reference/api#close","Close",[889,1118],"func (l *Listener) Close() Unregisters the listener and stops receiving events. Blocks until all events queued before Close was called have been processed. When the last listener for a signal closes, the worker goroutine exits.",{"id":1127,"title":1036,"titles":1128,"content":1129,"level":40},"/v1.0.2/reference/api#drain-1",[889,1118],"func (l *Listener) Drain(ctx context.Context) error Blocks until all events queued before Drain was called have been processed. Unlike Close, the listener remains active after draining. Returns an error if the context is cancelled before drain completes.",{"id":1131,"title":1132,"titles":1133,"content":1134,"level":19},"/v1.0.2/reference/api#observer","Observer",[889],"Dynamic subscription to multiple signals.",{"id":1136,"title":1123,"titles":1137,"content":1138,"level":40},"/v1.0.2/reference/api#close-1",[889,1132],"func (o *Observer) Close() Unregisters all underlying listeners.",{"id":1140,"title":1036,"titles":1141,"content":1142,"level":40},"/v1.0.2/reference/api#drain-2",[889,1132],"func (o *Observer) Drain(ctx context.Context) error Blocks until all events queued before Drain was called have been processed across all observed signals. Unlike Close, the observer remains active after draining. Returns an error if the context is cancelled before drain completes.",{"id":1144,"title":170,"titles":1145,"content":1146,"level":19},"/v1.0.2/reference/api#options",[889],"Configuration functions for New and Configure. See Configuration Guide for usage guidance.",{"id":1148,"title":1149,"titles":1150,"content":1151,"level":40},"/v1.0.2/reference/api#withbuffersize","WithBufferSize",[889,170],"func WithBufferSize(size int) Option Sets the event queue buffer size per signal. Default: 16.",{"id":1153,"title":1154,"titles":1155,"content":1156,"level":40},"/v1.0.2/reference/api#withpanichandler","WithPanicHandler",[889,170],"func WithPanicHandler(handler PanicHandler) Option Sets the callback invoked when a listener panics. Default: silent recovery.",{"id":1158,"title":1159,"titles":1160,"content":1161,"level":40},"/v1.0.2/reference/api#withsyncmode","WithSyncMode",[889,170],"func WithSyncMode() Option Enables synchronous event processing. For testing only.",{"id":1163,"title":1164,"titles":1165,"content":1166,"level":19},"/v1.0.2/reference/api#callback-types","Callback Types",[889],"type EventCallback func(context.Context, *Event) Handler function for events. type PanicHandler func(signal Signal, recovered any) Handler function for listener panics.",{"id":1168,"title":1094,"titles":1169,"content":1170,"level":19},"/v1.0.2/reference/api#severity-1",[889],"Event severity levels. ConstantValueDescriptionSeverityDebug\"DEBUG\"Development, troubleshootingSeverityInfo\"INFO\"Normal operations (default)SeverityWarn\"WARN\"Warning conditionsSeverityError\"ERROR\"Error conditions",{"id":1172,"title":947,"titles":1173,"content":1174,"level":19},"/v1.0.2/reference/api#stats-2",[889],"Runtime metrics snapshot. type Stats struct {\n    ActiveWorkers  int\n    SignalCount    int\n    DroppedEvents  uint64\n    QueueDepths    map[Signal]int\n    ListenerCounts map[Signal]int\n    EmitCounts     map[Signal]uint64\n    FieldSchemas   map[Signal][]Key\n} FieldDescriptionActiveWorkersRunning worker goroutinesSignalCountNumber of registered signalsDroppedEventsEvents emitted with no listenersQueueDepthsCurrent buffer usage per signalListenerCountsRegistered listeners per signalEmitCountsTotal emissions per signalFieldSchemasField keys from first emission per signal",{"id":1176,"title":1177,"titles":1178,"content":34,"level":19},"/v1.0.2/reference/api#per-signal-configuration-types","Per-Signal Configuration Types",[889],{"id":1180,"title":1181,"titles":1182,"content":1183,"level":40},"/v1.0.2/reference/api#config","Config",[889,1177],"Serializable configuration for per-signal settings. type Config struct {\n    Signals map[string]SignalConfig `json:\"signals\"`\n} The Signals map keys can be exact signal names or glob patterns (*, ?, [...]). See Configuration Guide for resolution rules.",{"id":1185,"title":1186,"titles":1187,"content":1188,"level":40},"/v1.0.2/reference/api#signalconfig","SignalConfig",[889,1177],"Per-signal configuration options. type SignalConfig struct {\n    BufferSize   int        `json:\"bufferSize,omitempty\"`\n    Disabled     bool       `json:\"disabled,omitempty\"`\n    MinSeverity  Severity   `json:\"minSeverity,omitempty\"`\n    MaxListeners int        `json:\"maxListeners,omitempty\"`\n    DropPolicy   DropPolicy `json:\"dropPolicy,omitempty\"`\n    RateLimit    float64    `json:\"rateLimit,omitempty\"`\n    BurstSize    int        `json:\"burstSize,omitempty\"`\n} FieldDefaultDescriptionBufferSizeinstance defaultEvent queue size for this signalDisabledfalseDrop all events for this signalMinSeveritynoneFilter events below this levelMaxListenersunlimitedCap listener registrationsDropPolicyblockBehavior when buffer fullRateLimitunlimitedMax events per secondBurstSize1Burst allowance above rate limit",{"id":1190,"title":1191,"titles":1192,"content":1193,"level":40},"/v1.0.2/reference/api#droppolicy","DropPolicy",[889,1177],"Behavior when the event buffer is full. ConstantValueDescriptionDropPolicyBlock\"block\"Wait for space in buffer (default)DropPolicyDropNewest\"drop_newest\"Drop incoming events if buffer full 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 .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 .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 .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}",{"id":1195,"title":1196,"titles":1197,"content":1198,"level":9},"/v1.0.2/reference/fields","Fields Reference",[],"Complete reference for typed keys and fields",{"id":1200,"title":1196,"titles":1201,"content":1202,"level":9},"/v1.0.2/reference/fields#fields-reference",[],"Complete reference for typed keys and fields in github.com/zoobz-io/capitan.",{"id":1204,"title":1205,"titles":1206,"content":1207,"level":19},"/v1.0.2/reference/fields#key-interface","Key Interface",[1196],"All keys implement this interface: type Key interface {\n    Name() string\n    Variant() Variant\n}",{"id":1209,"title":1210,"titles":1211,"content":34,"level":19},"/v1.0.2/reference/fields#built-in-key-constructors","Built-in Key Constructors",[1196],{"id":1213,"title":1214,"titles":1215,"content":1216,"level":40},"/v1.0.2/reference/fields#newstringkey","NewStringKey",[1196,1210],"func NewStringKey(name string) StringKey Creates a key for string values. Variant: \"string\".",{"id":1218,"title":1219,"titles":1220,"content":1221,"level":40},"/v1.0.2/reference/fields#newintkey","NewIntKey",[1196,1210],"func NewIntKey(name string) IntKey Creates a key for int values. Variant: \"int\".",{"id":1223,"title":1224,"titles":1225,"content":1226,"level":40},"/v1.0.2/reference/fields#newint32key","NewInt32Key",[1196,1210],"func NewInt32Key(name string) Int32Key Creates a key for int32 values. Variant: \"int32\".",{"id":1228,"title":1229,"titles":1230,"content":1231,"level":40},"/v1.0.2/reference/fields#newint64key","NewInt64Key",[1196,1210],"func NewInt64Key(name string) Int64Key Creates a key for int64 values. Variant: \"int64\".",{"id":1233,"title":1234,"titles":1235,"content":1236,"level":40},"/v1.0.2/reference/fields#newuintkey","NewUintKey",[1196,1210],"func NewUintKey(name string) UintKey Creates a key for uint values. Variant: \"uint\".",{"id":1238,"title":1239,"titles":1240,"content":1241,"level":40},"/v1.0.2/reference/fields#newuint32key","NewUint32Key",[1196,1210],"func NewUint32Key(name string) Uint32Key Creates a key for uint32 values. Variant: \"uint32\".",{"id":1243,"title":1244,"titles":1245,"content":1246,"level":40},"/v1.0.2/reference/fields#newuint64key","NewUint64Key",[1196,1210],"func NewUint64Key(name string) Uint64Key Creates a key for uint64 values. Variant: \"uint64\".",{"id":1248,"title":1249,"titles":1250,"content":1251,"level":40},"/v1.0.2/reference/fields#newfloat32key","NewFloat32Key",[1196,1210],"func NewFloat32Key(name string) Float32Key Creates a key for float32 values. Variant: \"float32\".",{"id":1253,"title":1254,"titles":1255,"content":1256,"level":40},"/v1.0.2/reference/fields#newfloat64key","NewFloat64Key",[1196,1210],"func NewFloat64Key(name string) Float64Key Creates a key for float64 values. Variant: \"float64\".",{"id":1258,"title":1259,"titles":1260,"content":1261,"level":40},"/v1.0.2/reference/fields#newboolkey","NewBoolKey",[1196,1210],"func NewBoolKey(name string) BoolKey Creates a key for bool values. Variant: \"bool\".",{"id":1263,"title":1264,"titles":1265,"content":1266,"level":40},"/v1.0.2/reference/fields#newtimekey","NewTimeKey",[1196,1210],"func NewTimeKey(name string) TimeKey Creates a key for time.Time values. Variant: \"time.Time\".",{"id":1268,"title":1269,"titles":1270,"content":1271,"level":40},"/v1.0.2/reference/fields#newdurationkey","NewDurationKey",[1196,1210],"func NewDurationKey(name string) DurationKey Creates a key for time.Duration values. Variant: \"time.Duration\".",{"id":1273,"title":1274,"titles":1275,"content":1276,"level":40},"/v1.0.2/reference/fields#newbyteskey","NewBytesKey",[1196,1210],"func NewBytesKey(name string) BytesKey Creates a key for []byte values. Variant: \"[]byte\".",{"id":1278,"title":1279,"titles":1280,"content":1281,"level":40},"/v1.0.2/reference/fields#newerrorkey","NewErrorKey",[1196,1210],"func NewErrorKey(name string) ErrorKey Creates a key for error values. Variant: \"error\".",{"id":1283,"title":1284,"titles":1285,"content":1286,"level":40},"/v1.0.2/reference/fields#newkey","NewKey",[1196,1210],"func NewKey[T any](name string, variant Variant) GenericKey[T] Creates a key for custom types. Use namespaced variant strings to avoid collisions: type Order struct {\n    ID    string\n    Total float64\n}\n\nvar orderKey = capitan.NewKey[Order](\"order\", \"myapp.Order\")",{"id":1288,"title":1289,"titles":1290,"content":1291,"level":19},"/v1.0.2/reference/fields#generickeyt","GenericKeyT",[1196],"All built-in key types are aliases of GenericKey[T].",{"id":1293,"title":1058,"titles":1294,"content":1295,"level":40},"/v1.0.2/reference/fields#name",[1196,1289],"func (k GenericKey[T]) Name() string Returns the key's semantic identifier.",{"id":1297,"title":1298,"titles":1299,"content":1300,"level":40},"/v1.0.2/reference/fields#variant","Variant",[1196,1289],"func (k GenericKey[T]) Variant() Variant Returns the key's type discriminator.",{"id":1302,"title":1303,"titles":1304,"content":1305,"level":40},"/v1.0.2/reference/fields#field","Field",[1196,1289],"func (k GenericKey[T]) Field(value T) Field Creates a field with the given value for use in Emit.",{"id":1307,"title":1308,"titles":1309,"content":1310,"level":40},"/v1.0.2/reference/fields#from","From",[1196,1289],"func (k GenericKey[T]) From(e *Event) (T, bool) Extracts the typed value from an event. Returns (zero, false) if the field is missing or has the wrong type.",{"id":1312,"title":1313,"titles":1314,"content":1315,"level":40},"/v1.0.2/reference/fields#extractfromfields","ExtractFromFields",[1196,1289],"func (k GenericKey[T]) ExtractFromFields(fields []Field) T Extracts the typed value from a field slice. Returns the zero value if not found. Useful for testing with captured events.",{"id":1317,"title":1318,"titles":1319,"content":1320,"level":19},"/v1.0.2/reference/fields#usage-pattern","Usage Pattern",[1196],"// Define key\norderID := capitan.NewStringKey(\"order_id\")\n\n// Create field for emission\ncapitan.Emit(ctx, signal, orderID.Field(\"ORD-123\"))\n\n// Extract in listener\ncapitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n    id, ok := orderID.From(e)\n    if !ok {\n        return\n    }\n    // Use id...\n})",{"id":1322,"title":1323,"titles":1324,"content":1325,"level":19},"/v1.0.2/reference/fields#field-interface","Field Interface",[1196],"Fields are key-value pairs attached to events: type Field interface {\n    Variant() Variant\n    Key() Key\n    Value() any\n}",{"id":1327,"title":1328,"titles":1329,"content":1330,"level":19},"/v1.0.2/reference/fields#genericfieldt","GenericFieldT",[1196],"Concrete implementation of Field for typed values.",{"id":1332,"title":1298,"titles":1333,"content":1334,"level":40},"/v1.0.2/reference/fields#variant-1",[1196,1328],"func (f GenericField[T]) Variant() Variant Returns the field's type discriminator.",{"id":1336,"title":1337,"titles":1338,"content":1339,"level":40},"/v1.0.2/reference/fields#key","Key",[1196,1328],"func (f GenericField[T]) Key() Key Returns the field's key.",{"id":1341,"title":1342,"titles":1343,"content":1344,"level":40},"/v1.0.2/reference/fields#value","Value",[1196,1328],"func (f GenericField[T]) Value() any Returns the value as any.",{"id":1346,"title":1104,"titles":1347,"content":1348,"level":40},"/v1.0.2/reference/fields#get",[1196,1328],"func (f GenericField[T]) Get() T Returns the typed value.",{"id":1350,"title":1351,"titles":1352,"content":1353,"level":19},"/v1.0.2/reference/fields#variant-constants","Variant Constants",[1196],"Type discriminators for built-in types: ConstantValueVariantString\"string\"VariantInt\"int\"VariantInt32\"int32\"VariantInt64\"int64\"VariantUint\"uint\"VariantUint32\"uint32\"VariantUint64\"uint64\"VariantFloat32\"float32\"VariantFloat64\"float64\"VariantBool\"bool\"VariantTime\"time.Time\"VariantDuration\"time.Duration\"VariantBytes\"[]byte\"VariantError\"error\" Custom types should use namespaced variant strings to avoid collisions: // Good: namespaced\ncapitan.NewKey[Order](\"order\", \"myapp.Order\")\ncapitan.NewKey[User](\"user\", \"github.com/org/pkg.User\")\n\n// Bad: collision risk\ncapitan.NewKey[Order](\"order\", \"Order\")",{"id":1355,"title":1356,"titles":1357,"content":1358,"level":19},"/v1.0.2/reference/fields#extracting-from-field-slices","Extracting from Field Slices",[1196],"When working with captured events in tests, use ExtractFromFields: import capitantesting \"github.com/zoobz-io/capitan/testing\"\n\ncapture := capitantesting.NewEventCapture()\nc.Hook(signal, capture.Handler())\n\n// ... emit events ...\n\nevents := capture.Events()\nid := orderID.ExtractFromFields(events[0].Fields)  // Returns zero value if missing See Testing Guide for more testing patterns. 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 .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 .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 .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}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)}",{"id":1360,"title":1361,"titles":1362,"content":1363,"level":9},"/v1.0.2/reference/testing","Testing Package Reference",[],"Complete reference for the capitan testing package",{"id":1365,"title":1361,"titles":1366,"content":1367,"level":9},"/v1.0.2/reference/testing#testing-package-reference",[],"Complete reference for github.com/zoobz-io/capitan/testing. For usage patterns and testing strategies, see the Testing Guide. import capitantesting \"github.com/zoobz-io/capitan/testing\"",{"id":1369,"title":1370,"titles":1371,"content":1372,"level":19},"/v1.0.2/reference/testing#eventcapture","EventCapture",[1361],"Captures events for testing and verification.",{"id":1374,"title":1375,"titles":1376,"content":1377,"level":40},"/v1.0.2/reference/testing#neweventcapture","NewEventCapture",[1361,1370],"func NewEventCapture() *EventCapture Creates a new capture instance.",{"id":1379,"title":1380,"titles":1381,"content":1382,"level":40},"/v1.0.2/reference/testing#handler","Handler",[1361,1370],"func (ec *EventCapture) Handler() capitan.EventCallback Returns a callback for use with Hook or Observe.",{"id":1384,"title":1385,"titles":1386,"content":1387,"level":40},"/v1.0.2/reference/testing#events","Events",[1361,1370],"func (ec *EventCapture) Events() []CapturedEvent Returns all captured events as a defensive copy.",{"id":1389,"title":1390,"titles":1391,"content":1392,"level":40},"/v1.0.2/reference/testing#count","Count",[1361,1370],"func (ec *EventCapture) Count() int Returns the number of captured events.",{"id":1394,"title":1395,"titles":1396,"content":1397,"level":40},"/v1.0.2/reference/testing#reset","Reset",[1361,1370],"func (ec *EventCapture) Reset() Clears all captured events.",{"id":1399,"title":1400,"titles":1401,"content":1402,"level":40},"/v1.0.2/reference/testing#waitforcount","WaitForCount",[1361,1370],"func (ec *EventCapture) WaitForCount(n int, timeout time.Duration) bool Blocks until the capture has at least n events or timeout expires. Returns true if count was reached.",{"id":1404,"title":1405,"titles":1406,"content":1407,"level":40},"/v1.0.2/reference/testing#capturedevent","CapturedEvent",[1361,1370],"Snapshot of captured event data: type CapturedEvent struct {\n    Signal    capitan.Signal\n    Timestamp time.Time\n    Severity  capitan.Severity\n    Fields    []capitan.Field\n}",{"id":1409,"title":1410,"titles":1411,"content":1412,"level":40},"/v1.0.2/reference/testing#example","Example",[1361,1370],"func TestEmission(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n\n    capture := capitantesting.NewEventCapture()\n    c.Hook(orderCreated, capture.Handler())\n\n    c.Emit(context.Background(), orderCreated, orderID.Field(\"ORD-123\"))\n\n    events := capture.Events()\n    if len(events) != 1 {\n        t.Fatalf(\"expected 1 event, got %d\", len(events))\n    }\n\n    id := orderID.ExtractFromFields(events[0].Fields)\n    if id != \"ORD-123\" {\n        t.Errorf(\"expected ORD-123, got %s\", id)\n    }\n}",{"id":1414,"title":1415,"titles":1416,"content":1417,"level":19},"/v1.0.2/reference/testing#eventcounter","EventCounter",[1361],"Counts events without storing them.",{"id":1419,"title":1420,"titles":1421,"content":1422,"level":40},"/v1.0.2/reference/testing#neweventcounter","NewEventCounter",[1361,1415],"func NewEventCounter() *EventCounter Creates a new counter instance.",{"id":1424,"title":1380,"titles":1425,"content":1426,"level":40},"/v1.0.2/reference/testing#handler-1",[1361,1415],"func (ec *EventCounter) Handler() capitan.EventCallback Returns a callback for use with Hook or Observe.",{"id":1428,"title":1390,"titles":1429,"content":1430,"level":40},"/v1.0.2/reference/testing#count-1",[1361,1415],"func (ec *EventCounter) Count() int64 Returns the current count.",{"id":1432,"title":1395,"titles":1433,"content":1434,"level":40},"/v1.0.2/reference/testing#reset-1",[1361,1415],"func (ec *EventCounter) Reset() Resets the count to zero.",{"id":1436,"title":1400,"titles":1437,"content":1438,"level":40},"/v1.0.2/reference/testing#waitforcount-1",[1361,1415],"func (ec *EventCounter) WaitForCount(n int64, timeout time.Duration) bool Blocks until count reaches n or timeout expires. Returns true if count was reached.",{"id":1440,"title":1410,"titles":1441,"content":1442,"level":40},"/v1.0.2/reference/testing#example-1",[1361,1415],"func TestBulkEmission(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n\n    counter := capitantesting.NewEventCounter()\n    c.Hook(signal, counter.Handler())\n\n    for i := 0; i \u003C 1000; i++ {\n        c.Emit(context.Background(), signal)\n    }\n\n    if counter.Count() != 1000 {\n        t.Errorf(\"expected 1000, got %d\", counter.Count())\n    }\n}",{"id":1444,"title":1445,"titles":1446,"content":1447,"level":19},"/v1.0.2/reference/testing#panicrecorder","PanicRecorder",[1361],"Records panics from listeners.",{"id":1449,"title":1450,"titles":1451,"content":1452,"level":40},"/v1.0.2/reference/testing#newpanicrecorder","NewPanicRecorder",[1361,1445],"func NewPanicRecorder() *PanicRecorder Creates a new recorder instance.",{"id":1454,"title":1380,"titles":1455,"content":1456,"level":40},"/v1.0.2/reference/testing#handler-2",[1361,1445],"func (pr *PanicRecorder) Handler() func(capitan.Signal, any) Returns a panic handler for use with WithPanicHandler.",{"id":1458,"title":1459,"titles":1460,"content":1461,"level":40},"/v1.0.2/reference/testing#panics","Panics",[1361,1445],"func (pr *PanicRecorder) Panics() []PanicRecord Returns all recorded panics as a defensive copy.",{"id":1463,"title":1390,"titles":1464,"content":1465,"level":40},"/v1.0.2/reference/testing#count-2",[1361,1445],"func (pr *PanicRecorder) Count() int Returns the number of recorded panics.",{"id":1467,"title":1395,"titles":1468,"content":1469,"level":40},"/v1.0.2/reference/testing#reset-2",[1361,1445],"func (pr *PanicRecorder) Reset() Clears all recorded panics.",{"id":1471,"title":1472,"titles":1473,"content":1474,"level":40},"/v1.0.2/reference/testing#panicrecord","PanicRecord",[1361,1445],"type PanicRecord struct {\n    Signal    capitan.Signal\n    Recovered any\n    Timestamp time.Time\n}",{"id":1476,"title":1410,"titles":1477,"content":1478,"level":40},"/v1.0.2/reference/testing#example-2",[1361,1445],"func TestPanicRecovery(t *testing.T) {\n    recorder := capitantesting.NewPanicRecorder()\n    c := capitan.New(\n        capitan.WithSyncMode(),\n        capitan.WithPanicHandler(recorder.Handler()),\n    )\n    defer c.Shutdown()\n\n    c.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n        panic(\"test panic\")\n    })\n\n    c.Emit(context.Background(), signal)\n\n    panics := recorder.Panics()\n    if len(panics) != 1 {\n        t.Fatalf(\"expected 1 panic, got %d\", len(panics))\n    }\n    if panics[0].Recovered != \"test panic\" {\n        t.Errorf(\"wrong panic value: %v\", panics[0].Recovered)\n    }\n}",{"id":1480,"title":1481,"titles":1482,"content":1483,"level":19},"/v1.0.2/reference/testing#statswaiter","StatsWaiter",[1361],"Polls stats until conditions are met.",{"id":1485,"title":1486,"titles":1487,"content":1488,"level":40},"/v1.0.2/reference/testing#newstatswaiter","NewStatsWaiter",[1361,1481],"func NewStatsWaiter(c *capitan.Capitan) *StatsWaiter Creates a waiter for the given instance.",{"id":1490,"title":1491,"titles":1492,"content":1493,"level":40},"/v1.0.2/reference/testing#waitforworkers","WaitForWorkers",[1361,1481],"func (sw *StatsWaiter) WaitForWorkers(n int, timeout time.Duration) bool Blocks until at least n workers are active or timeout expires.",{"id":1495,"title":1496,"titles":1497,"content":1498,"level":40},"/v1.0.2/reference/testing#waitforemptyqueues","WaitForEmptyQueues",[1361,1481],"func (sw *StatsWaiter) WaitForEmptyQueues(timeout time.Duration) bool Blocks until all signal queues are empty or timeout expires.",{"id":1500,"title":1501,"titles":1502,"content":1503,"level":40},"/v1.0.2/reference/testing#waitforemitcount","WaitForEmitCount",[1361,1481],"func (sw *StatsWaiter) WaitForEmitCount(signal capitan.Signal, n uint64, timeout time.Duration) bool Blocks until the signal's emit count reaches n or timeout expires.",{"id":1505,"title":1410,"titles":1506,"content":1507,"level":40},"/v1.0.2/reference/testing#example-3",[1361,1481],"func TestAsyncProcessing(t *testing.T) {\n    c := capitan.New() // async mode\n    defer c.Shutdown()\n\n    waiter := capitantesting.NewStatsWaiter(c)\n\n    c.Hook(signal, func(ctx context.Context, e *capitan.Event) {})\n    c.Emit(context.Background(), signal)\n\n    if !waiter.WaitForWorkers(1, time.Second) {\n        t.Fatal(\"worker not created\")\n    }\n\n    if !waiter.WaitForEmptyQueues(time.Second) {\n        t.Fatal(\"queue not drained\")\n    }\n}",{"id":1509,"title":1510,"titles":1511,"content":1512,"level":19},"/v1.0.2/reference/testing#fieldextractor","FieldExtractor",[1361],"Utility for extracting typed values from events.",{"id":1514,"title":1515,"titles":1516,"content":1517,"level":40},"/v1.0.2/reference/testing#newfieldextractor","NewFieldExtractor",[1361,1510],"func NewFieldExtractor() *FieldExtractor Creates a new extractor.",{"id":1519,"title":1520,"titles":1521,"content":1522,"level":40},"/v1.0.2/reference/testing#getstring","GetString",[1361,1510],"func (fe *FieldExtractor) GetString(e *capitan.Event, key capitan.StringKey) string Extracts a string value. Returns empty string if missing.",{"id":1524,"title":1525,"titles":1526,"content":1527,"level":40},"/v1.0.2/reference/testing#getint","GetInt",[1361,1510],"func (fe *FieldExtractor) GetInt(e *capitan.Event, key capitan.IntKey) int Extracts an int value. Returns zero if missing.",{"id":1529,"title":1530,"titles":1531,"content":1532,"level":40},"/v1.0.2/reference/testing#getbool","GetBool",[1361,1510],"func (fe *FieldExtractor) GetBool(e *capitan.Event, key capitan.BoolKey) bool Extracts a bool value. Returns false if missing.",{"id":1534,"title":1535,"titles":1536,"content":1537,"level":40},"/v1.0.2/reference/testing#getfloat64","GetFloat64",[1361,1510],"func (fe *FieldExtractor) GetFloat64(e *capitan.Event, key capitan.Float64Key) float64 Extracts a float64 value. Returns zero if missing.",{"id":1539,"title":1540,"titles":1541,"content":34,"level":19},"/v1.0.2/reference/testing#testcapitan","TestCapitan",[1361],{"id":1543,"title":1540,"titles":1544,"content":1545,"level":40},"/v1.0.2/reference/testing#testcapitan-1",[1361,1540],"func TestCapitan(opts ...capitan.Option) *capitan.Capitan Creates an instance for testing. Equivalent to capitan.New(opts...) but communicates test intent. 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 .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 .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 .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 .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 .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}",[1547],{"title":1548,"path":1549,"stem":1550,"children":1551,"page":1565},"V102","/v1.0.2","v1.0.2",[1552,1566,1583,1594],{"title":1553,"path":1554,"stem":1555,"children":1556,"page":1565},"Learn","/v1.0.2/learn","v1.0.2/1.learn",[1557,1559,1561,1563],{"title":6,"path":5,"stem":1558,"description":8},"v1.0.2/1.learn/1.overview",{"title":53,"path":52,"stem":1560,"description":55},"v1.0.2/1.learn/2.quickstart",{"title":81,"path":80,"stem":1562,"description":83},"v1.0.2/1.learn/3.concepts",{"title":16,"path":214,"stem":1564,"description":216},"v1.0.2/1.learn/4.architecture",false,{"title":1567,"path":1568,"stem":1569,"children":1570,"page":1565},"Guides","/v1.0.2/guides","v1.0.2/2.guides",[1571,1573,1575,1577,1579,1581],{"title":165,"path":306,"stem":1572,"description":308},"v1.0.2/2.guides/1.configuration",{"title":427,"path":426,"stem":1574,"description":429},"v1.0.2/2.guides/2.context",{"title":491,"path":490,"stem":1576,"description":493},"v1.0.2/2.guides/3.errors",{"title":549,"path":548,"stem":1578,"description":551},"v1.0.2/2.guides/4.testing",{"title":617,"path":616,"stem":1580,"description":619},"v1.0.2/2.guides/5.best-practices",{"title":738,"path":737,"stem":1582,"description":740},"v1.0.2/2.guides/6.persistence",{"title":1584,"path":1585,"stem":1586,"children":1587,"page":1565},"Integrations","/v1.0.2/integrations","v1.0.2/3.integrations",[1588,1590,1592],{"title":814,"path":813,"stem":1589,"description":816},"v1.0.2/3.integrations/1.aperture",{"title":838,"path":837,"stem":1591,"description":840},"v1.0.2/3.integrations/2.herald",{"title":864,"path":863,"stem":1593,"description":866},"v1.0.2/3.integrations/3.ago",{"title":1595,"path":1596,"stem":1597,"children":1598,"page":1565},"Reference","/v1.0.2/reference","v1.0.2/4.reference",[1599,1601,1603],{"title":889,"path":888,"stem":1600,"description":891},"v1.0.2/4.reference/1.api",{"title":1196,"path":1195,"stem":1602,"description":1198},"v1.0.2/4.reference/2.fields",{"title":1361,"path":1360,"stem":1604,"description":1363},"v1.0.2/4.reference/3.testing",[1606],{"title":1548,"path":1549,"stem":1550,"children":1607,"page":1565},[1608,1614,1622,1627],{"title":1553,"path":1554,"stem":1555,"children":1609,"page":1565},[1610,1611,1612,1613],{"title":6,"path":5,"stem":1558},{"title":53,"path":52,"stem":1560},{"title":81,"path":80,"stem":1562},{"title":16,"path":214,"stem":1564},{"title":1567,"path":1568,"stem":1569,"children":1615,"page":1565},[1616,1617,1618,1619,1620,1621],{"title":165,"path":306,"stem":1572},{"title":427,"path":426,"stem":1574},{"title":491,"path":490,"stem":1576},{"title":549,"path":548,"stem":1578},{"title":617,"path":616,"stem":1580},{"title":738,"path":737,"stem":1582},{"title":1584,"path":1585,"stem":1586,"children":1623,"page":1565},[1624,1625,1626],{"title":814,"path":813,"stem":1589},{"title":838,"path":837,"stem":1591},{"title":864,"path":863,"stem":1593},{"title":1595,"path":1596,"stem":1597,"children":1628,"page":1565},[1629,1630,1631],{"title":889,"path":888,"stem":1600},{"title":1196,"path":1195,"stem":1602},{"title":1361,"path":1360,"stem":1604},[1633,3017,3444],{"id":1634,"title":1635,"body":1636,"description":34,"extension":3010,"icon":3011,"meta":3012,"navigation":1796,"path":3013,"seo":3014,"stem":3015,"__hash__":3016},"resources/readme.md","README",{"type":1637,"value":1638,"toc":2994},"minimark",[1639,1643,1711,1714,1717,1722,1725,1905,1908,2135,2138,2141,2158,2161,2165,2565,2568,2670,2674,2715,2719,2725,2728,2835,2838,2842,2849,2853,2877,2880,2915,2918,2942,2945,2965,2969,2981,2984,2990],[1640,1641,1642],"h1",{"id":1642},"capitan",[1644,1645,1646,1657,1665,1673,1681,1689,1696,1703],"p",{},[1647,1648,1652],"a",{"href":1649,"rel":1650},"https://github.com/zoobz-io/capitan/actions/workflows/ci.yml",[1651],"nofollow",[1653,1654],"img",{"alt":1655,"src":1656},"CI Status","https://github.com/zoobz-io/capitan/workflows/CI/badge.svg",[1647,1658,1661],{"href":1659,"rel":1660},"https://codecov.io/gh/zoobz-io/capitan",[1651],[1653,1662],{"alt":1663,"src":1664},"codecov","https://codecov.io/gh/zoobz-io/capitan/graph/badge.svg?branch=main",[1647,1666,1669],{"href":1667,"rel":1668},"https://goreportcard.com/report/github.com/zoobz-io/capitan",[1651],[1653,1670],{"alt":1671,"src":1672},"Go Report Card","https://goreportcard.com/badge/github.com/zoobz-io/capitan",[1647,1674,1677],{"href":1675,"rel":1676},"https://github.com/zoobz-io/capitan/security/code-scanning",[1651],[1653,1678],{"alt":1679,"src":1680},"CodeQL","https://github.com/zoobz-io/capitan/workflows/CodeQL/badge.svg",[1647,1682,1685],{"href":1683,"rel":1684},"https://pkg.go.dev/github.com/zoobz-io/capitan",[1651],[1653,1686],{"alt":1687,"src":1688},"Go Reference","https://pkg.go.dev/badge/github.com/zoobz-io/capitan.svg",[1647,1690,1692],{"href":1691},"LICENSE",[1653,1693],{"alt":1694,"src":1695},"License","https://img.shields.io/github/license/zoobz-io/capitan",[1647,1697,1699],{"href":1698},"go.mod",[1653,1700],{"alt":1701,"src":1702},"Go Version","https://img.shields.io/github/go-mod/go-version/zoobz-io/capitan",[1647,1704,1707],{"href":1705,"rel":1706},"https://github.com/zoobz-io/capitan/releases",[1651],[1653,1708],{"alt":1709,"src":1710},"Release","https://img.shields.io/github/v/release/zoobz-io/capitan",[1644,1712,1713],{},"Type-safe event coordination for Go with zero dependencies.",[1644,1715,1716],{},"Emit events with typed fields, hook listeners, and let capitan handle the rest with async processing and backpressure.",[1718,1719,1721],"h2",{"id":1720},"send-a-signal-listen-anywhere","Send a Signal, Listen Anywhere",[1644,1723,1724],{},"Events carry typed fields with compile-time safety.",[1726,1727,1731],"pre",{"className":1728,"code":1729,"language":1730,"meta":34,"style":34},"language-go shiki shiki-themes","// Define typed keys\norderID := capitan.NewStringKey(\"order_id\")\ntotal   := capitan.NewFloat64Key(\"total\")\n\n// Define a signal\norderCreated := capitan.NewSignal(\"order.created\", \"New order placed\")\n\n// Emit with typed fields\ncapitan.Emit(ctx, orderCreated,\n    orderID.Field(\"ORD-123\"),\n    total.Field(99.99),\n)\n","go",[1732,1733,1734,1742,1771,1792,1798,1804,1831,1836,1842,1864,1882,1900],"code",{"__ignoreMap":34},[1735,1736,1738],"span",{"class":1737,"line":9},"line",[1735,1739,1741],{"class":1740},"sLkEo","// Define typed keys\n",[1735,1743,1744,1748,1751,1754,1758,1761,1764,1768],{"class":1737,"line":19},[1735,1745,1747],{"class":1746},"sh8_p","orderID",[1735,1749,1750],{"class":1746}," :=",[1735,1752,1753],{"class":1746}," capitan",[1735,1755,1757],{"class":1756},"sq5bi",".",[1735,1759,1214],{"class":1760},"s5klm",[1735,1762,1763],{"class":1756},"(",[1735,1765,1767],{"class":1766},"sxAnc","\"order_id\"",[1735,1769,1770],{"class":1756},")\n",[1735,1772,1773,1776,1779,1781,1783,1785,1787,1790],{"class":1737,"line":40},[1735,1774,1775],{"class":1746},"total",[1735,1777,1778],{"class":1746},"   :=",[1735,1780,1753],{"class":1746},[1735,1782,1757],{"class":1756},[1735,1784,1254],{"class":1760},[1735,1786,1763],{"class":1756},[1735,1788,1789],{"class":1766},"\"total\"",[1735,1791,1770],{"class":1756},[1735,1793,1794],{"class":1737,"line":988},[1735,1795,1797],{"emptyLinePlaceholder":1796},true,"\n",[1735,1799,1801],{"class":1737,"line":1800},5,[1735,1802,1803],{"class":1740},"// Define a signal\n",[1735,1805,1807,1810,1812,1814,1816,1818,1820,1823,1826,1829],{"class":1737,"line":1806},6,[1735,1808,1809],{"class":1746},"orderCreated",[1735,1811,1750],{"class":1746},[1735,1813,1753],{"class":1746},[1735,1815,1757],{"class":1756},[1735,1817,1050],{"class":1760},[1735,1819,1763],{"class":1756},[1735,1821,1822],{"class":1766},"\"order.created\"",[1735,1824,1825],{"class":1756},",",[1735,1827,1828],{"class":1766}," \"New order placed\"",[1735,1830,1770],{"class":1756},[1735,1832,1834],{"class":1737,"line":1833},7,[1735,1835,1797],{"emptyLinePlaceholder":1796},[1735,1837,1839],{"class":1737,"line":1838},8,[1735,1840,1841],{"class":1740},"// Emit with typed fields\n",[1735,1843,1845,1847,1849,1851,1853,1856,1858,1861],{"class":1737,"line":1844},9,[1735,1846,1642],{"class":1746},[1735,1848,1757],{"class":1756},[1735,1850,903],{"class":1760},[1735,1852,1763],{"class":1756},[1735,1854,1855],{"class":1746},"ctx",[1735,1857,1825],{"class":1756},[1735,1859,1860],{"class":1746}," orderCreated",[1735,1862,1863],{"class":1756},",\n",[1735,1865,1867,1870,1872,1874,1876,1879],{"class":1737,"line":1866},10,[1735,1868,1869],{"class":1746},"    orderID",[1735,1871,1757],{"class":1756},[1735,1873,1303],{"class":1760},[1735,1875,1763],{"class":1756},[1735,1877,1878],{"class":1766},"\"ORD-123\"",[1735,1880,1881],{"class":1756},"),\n",[1735,1883,1885,1888,1890,1892,1894,1898],{"class":1737,"line":1884},11,[1735,1886,1887],{"class":1746},"    total",[1735,1889,1757],{"class":1756},[1735,1891,1303],{"class":1760},[1735,1893,1763],{"class":1756},[1735,1895,1897],{"class":1896},"sMAmT","99.99",[1735,1899,1881],{"class":1756},[1735,1901,1903],{"class":1737,"line":1902},12,[1735,1904,1770],{"class":1756},[1644,1906,1907],{},"Each signal queues to its own worker — isolated, async, backpressure-aware.",[1726,1909,1911],{"className":1728,"code":1910,"language":1730,"meta":34,"style":34},"// Hook a listener — extract typed values directly\ncapitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n    id, _ := orderID.From(e)      // string\n    amount, _ := total.From(e)    // float64\n    process(id, amount)\n})\n\n// Observe all signals — unified visibility across the system\ncapitan.Observe(func(ctx context.Context, e *capitan.Event) {\n    log.Info(\"event\", \"signal\", e.Signal().Name())\n})\n",[1732,1912,1913,1918,1970,1999,2026,2043,2048,2052,2057,2096,2131],{"__ignoreMap":34},[1735,1914,1915],{"class":1737,"line":9},[1735,1916,1917],{"class":1740},"// Hook a listener — extract typed values directly\n",[1735,1919,1920,1922,1924,1926,1928,1930,1932,1936,1938,1941,1945,1947,1949,1951,1954,1958,1960,1962,1964,1967],{"class":1737,"line":19},[1735,1921,1642],{"class":1746},[1735,1923,1757],{"class":1756},[1735,1925,932],{"class":1760},[1735,1927,1763],{"class":1756},[1735,1929,1809],{"class":1746},[1735,1931,1825],{"class":1756},[1735,1933,1935],{"class":1934},"sUt3r"," func",[1735,1937,1763],{"class":1756},[1735,1939,1855],{"class":1940},"sSYET",[1735,1942,1944],{"class":1943},"sYBwO"," context",[1735,1946,1757],{"class":1756},[1735,1948,427],{"class":1943},[1735,1950,1825],{"class":1756},[1735,1952,1953],{"class":1940}," e",[1735,1955,1957],{"class":1956},"sW3Qg"," *",[1735,1959,1642],{"class":1943},[1735,1961,1757],{"class":1756},[1735,1963,1068],{"class":1943},[1735,1965,1966],{"class":1756},")",[1735,1968,1969],{"class":1756}," {\n",[1735,1971,1972,1975,1977,1980,1982,1985,1987,1989,1991,1994,1996],{"class":1737,"line":40},[1735,1973,1974],{"class":1746},"    id",[1735,1976,1825],{"class":1756},[1735,1978,1979],{"class":1746}," _",[1735,1981,1750],{"class":1746},[1735,1983,1984],{"class":1746}," orderID",[1735,1986,1757],{"class":1756},[1735,1988,1308],{"class":1760},[1735,1990,1763],{"class":1756},[1735,1992,1993],{"class":1746},"e",[1735,1995,1966],{"class":1756},[1735,1997,1998],{"class":1740},"      // string\n",[1735,2000,2001,2004,2006,2008,2010,2013,2015,2017,2019,2021,2023],{"class":1737,"line":988},[1735,2002,2003],{"class":1746},"    amount",[1735,2005,1825],{"class":1756},[1735,2007,1979],{"class":1746},[1735,2009,1750],{"class":1746},[1735,2011,2012],{"class":1746}," total",[1735,2014,1757],{"class":1756},[1735,2016,1308],{"class":1760},[1735,2018,1763],{"class":1756},[1735,2020,1993],{"class":1746},[1735,2022,1966],{"class":1756},[1735,2024,2025],{"class":1740},"    // float64\n",[1735,2027,2028,2031,2033,2036,2038,2041],{"class":1737,"line":1800},[1735,2029,2030],{"class":1760},"    process",[1735,2032,1763],{"class":1756},[1735,2034,2035],{"class":1746},"id",[1735,2037,1825],{"class":1756},[1735,2039,2040],{"class":1746}," amount",[1735,2042,1770],{"class":1756},[1735,2044,2045],{"class":1737,"line":1806},[1735,2046,2047],{"class":1756},"})\n",[1735,2049,2050],{"class":1737,"line":1833},[1735,2051,1797],{"emptyLinePlaceholder":1796},[1735,2053,2054],{"class":1737,"line":1838},[1735,2055,2056],{"class":1740},"// Observe all signals — unified visibility across the system\n",[1735,2058,2059,2061,2063,2065,2067,2070,2072,2074,2076,2078,2080,2082,2084,2086,2088,2090,2092,2094],{"class":1737,"line":1844},[1735,2060,1642],{"class":1746},[1735,2062,1757],{"class":1756},[1735,2064,942],{"class":1760},[1735,2066,1763],{"class":1756},[1735,2068,2069],{"class":1934},"func",[1735,2071,1763],{"class":1756},[1735,2073,1855],{"class":1940},[1735,2075,1944],{"class":1943},[1735,2077,1757],{"class":1756},[1735,2079,427],{"class":1943},[1735,2081,1825],{"class":1756},[1735,2083,1953],{"class":1940},[1735,2085,1957],{"class":1956},[1735,2087,1642],{"class":1943},[1735,2089,1757],{"class":1756},[1735,2091,1068],{"class":1943},[1735,2093,1966],{"class":1756},[1735,2095,1969],{"class":1756},[1735,2097,2098,2101,2103,2105,2107,2110,2112,2115,2117,2119,2121,2123,2126,2128],{"class":1737,"line":1866},[1735,2099,2100],{"class":1746},"    log",[1735,2102,1757],{"class":1756},[1735,2104,913],{"class":1760},[1735,2106,1763],{"class":1756},[1735,2108,2109],{"class":1766},"\"event\"",[1735,2111,1825],{"class":1756},[1735,2113,2114],{"class":1766}," \"signal\"",[1735,2116,1825],{"class":1756},[1735,2118,1953],{"class":1746},[1735,2120,1757],{"class":1756},[1735,2122,1045],{"class":1760},[1735,2124,2125],{"class":1756},"().",[1735,2127,1058],{"class":1760},[1735,2129,2130],{"class":1756},"())\n",[1735,2132,2133],{"class":1737,"line":1884},[1735,2134,2047],{"class":1756},[1644,2136,2137],{},"Type-safe at the edges. Async and isolated in between.",[1718,2139,66],{"id":2140},"installation",[1726,2142,2146],{"className":2143,"code":2144,"language":2145,"meta":34,"style":34},"language-bash shiki shiki-themes","go get github.com/zoobz-io/capitan\n","bash",[1732,2147,2148],{"__ignoreMap":34},[1735,2149,2150,2152,2155],{"class":1737,"line":9},[1735,2151,1730],{"class":1760},[1735,2153,2154],{"class":1766}," get",[1735,2156,2157],{"class":1766}," github.com/zoobz-io/capitan\n",[1644,2159,2160],{},"Requires Go 1.24+.",[1718,2162,2164],{"id":2163},"quick-start","Quick Start",[1726,2166,2168],{"className":1728,"code":2167,"language":1730,"meta":34,"style":34},"package main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"github.com/zoobz-io/capitan\"\n)\n\n// Define signals and keys as package-level variables\nvar (\n    orderCreated = capitan.NewSignal(\"order.created\", \"New order placed\")\n    orderID      = capitan.NewStringKey(\"order_id\")\n    total        = capitan.NewFloat64Key(\"total\")\n)\n\nfunc main() {\n    // Hook a listener\n    capitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n        id, _ := orderID.From(e)\n        amount, _ := total.From(e)\n        fmt.Printf(\"Order %s: $%.2f\\n\", id, amount)\n    })\n\n    // Emit an event (async with backpressure)\n    capitan.Emit(context.Background(), orderCreated,\n        orderID.Field(\"ORDER-123\"),\n        total.Field(99.99),\n    )\n\n    // Gracefully drain pending events\n    capitan.Shutdown()\n}\n",[1732,2169,2170,2178,2182,2191,2196,2201,2206,2210,2214,2219,2226,2250,2269,2289,2294,2299,2312,2318,2362,2386,2410,2454,2460,2465,2471,2497,2514,2530,2536,2541,2547,2559],{"__ignoreMap":34},[1735,2171,2172,2175],{"class":1737,"line":9},[1735,2173,2174],{"class":1934},"package",[1735,2176,2177],{"class":1943}," main\n",[1735,2179,2180],{"class":1737,"line":19},[1735,2181,1797],{"emptyLinePlaceholder":1796},[1735,2183,2184,2187],{"class":1737,"line":40},[1735,2185,2186],{"class":1934},"import",[1735,2188,2190],{"class":2189},"soy-K"," (\n",[1735,2192,2193],{"class":1737,"line":988},[1735,2194,2195],{"class":1766},"    \"context\"\n",[1735,2197,2198],{"class":1737,"line":1800},[1735,2199,2200],{"class":1766},"    \"fmt\"\n",[1735,2202,2203],{"class":1737,"line":1806},[1735,2204,2205],{"class":1766},"    \"github.com/zoobz-io/capitan\"\n",[1735,2207,2208],{"class":1737,"line":1833},[1735,2209,1770],{"class":2189},[1735,2211,2212],{"class":1737,"line":1838},[1735,2213,1797],{"emptyLinePlaceholder":1796},[1735,2215,2216],{"class":1737,"line":1844},[1735,2217,2218],{"class":1740},"// Define signals and keys as package-level variables\n",[1735,2220,2221,2224],{"class":1737,"line":1866},[1735,2222,2223],{"class":1934},"var",[1735,2225,2190],{"class":1756},[1735,2227,2228,2231,2234,2236,2238,2240,2242,2244,2246,2248],{"class":1737,"line":1884},[1735,2229,2230],{"class":1746},"    orderCreated",[1735,2232,2233],{"class":1746}," =",[1735,2235,1753],{"class":1746},[1735,2237,1757],{"class":1756},[1735,2239,1050],{"class":1760},[1735,2241,1763],{"class":1756},[1735,2243,1822],{"class":1766},[1735,2245,1825],{"class":1756},[1735,2247,1828],{"class":1766},[1735,2249,1770],{"class":1756},[1735,2251,2252,2254,2257,2259,2261,2263,2265,2267],{"class":1737,"line":1902},[1735,2253,1869],{"class":1746},[1735,2255,2256],{"class":1746},"      =",[1735,2258,1753],{"class":1746},[1735,2260,1757],{"class":1756},[1735,2262,1214],{"class":1760},[1735,2264,1763],{"class":1756},[1735,2266,1767],{"class":1766},[1735,2268,1770],{"class":1756},[1735,2270,2272,2274,2277,2279,2281,2283,2285,2287],{"class":1737,"line":2271},13,[1735,2273,1887],{"class":1746},[1735,2275,2276],{"class":1746},"        =",[1735,2278,1753],{"class":1746},[1735,2280,1757],{"class":1756},[1735,2282,1254],{"class":1760},[1735,2284,1763],{"class":1756},[1735,2286,1789],{"class":1766},[1735,2288,1770],{"class":1756},[1735,2290,2292],{"class":1737,"line":2291},14,[1735,2293,1770],{"class":1756},[1735,2295,2297],{"class":1737,"line":2296},15,[1735,2298,1797],{"emptyLinePlaceholder":1796},[1735,2300,2302,2304,2307,2310],{"class":1737,"line":2301},16,[1735,2303,2069],{"class":1934},[1735,2305,2306],{"class":1760}," main",[1735,2308,2309],{"class":1756},"()",[1735,2311,1969],{"class":1756},[1735,2313,2315],{"class":1737,"line":2314},17,[1735,2316,2317],{"class":1740},"    // Hook a listener\n",[1735,2319,2321,2324,2326,2328,2330,2332,2334,2336,2338,2340,2342,2344,2346,2348,2350,2352,2354,2356,2358,2360],{"class":1737,"line":2320},18,[1735,2322,2323],{"class":1746},"    capitan",[1735,2325,1757],{"class":1756},[1735,2327,932],{"class":1760},[1735,2329,1763],{"class":1756},[1735,2331,1809],{"class":1746},[1735,2333,1825],{"class":1756},[1735,2335,1935],{"class":1934},[1735,2337,1763],{"class":1756},[1735,2339,1855],{"class":1940},[1735,2341,1944],{"class":1943},[1735,2343,1757],{"class":1756},[1735,2345,427],{"class":1943},[1735,2347,1825],{"class":1756},[1735,2349,1953],{"class":1940},[1735,2351,1957],{"class":1956},[1735,2353,1642],{"class":1943},[1735,2355,1757],{"class":1756},[1735,2357,1068],{"class":1943},[1735,2359,1966],{"class":1756},[1735,2361,1969],{"class":1756},[1735,2363,2365,2368,2370,2372,2374,2376,2378,2380,2382,2384],{"class":1737,"line":2364},19,[1735,2366,2367],{"class":1746},"        id",[1735,2369,1825],{"class":1756},[1735,2371,1979],{"class":1746},[1735,2373,1750],{"class":1746},[1735,2375,1984],{"class":1746},[1735,2377,1757],{"class":1756},[1735,2379,1308],{"class":1760},[1735,2381,1763],{"class":1756},[1735,2383,1993],{"class":1746},[1735,2385,1770],{"class":1756},[1735,2387,2389,2392,2394,2396,2398,2400,2402,2404,2406,2408],{"class":1737,"line":2388},20,[1735,2390,2391],{"class":1746},"        amount",[1735,2393,1825],{"class":1756},[1735,2395,1979],{"class":1746},[1735,2397,1750],{"class":1746},[1735,2399,2012],{"class":1746},[1735,2401,1757],{"class":1756},[1735,2403,1308],{"class":1760},[1735,2405,1763],{"class":1756},[1735,2407,1993],{"class":1746},[1735,2409,1770],{"class":1756},[1735,2411,2413,2416,2418,2421,2423,2426,2430,2433,2436,2440,2443,2445,2448,2450,2452],{"class":1737,"line":2412},21,[1735,2414,2415],{"class":1746},"        fmt",[1735,2417,1757],{"class":1756},[1735,2419,2420],{"class":1760},"Printf",[1735,2422,1763],{"class":1756},[1735,2424,2425],{"class":1766},"\"Order ",[1735,2427,2429],{"class":2428},"scyPU","%s",[1735,2431,2432],{"class":1766},": $",[1735,2434,2435],{"class":2428},"%.2f",[1735,2437,2439],{"class":2438},"suWN2","\\n",[1735,2441,2442],{"class":1766},"\"",[1735,2444,1825],{"class":1756},[1735,2446,2447],{"class":1746}," id",[1735,2449,1825],{"class":1756},[1735,2451,2040],{"class":1746},[1735,2453,1770],{"class":1756},[1735,2455,2457],{"class":1737,"line":2456},22,[1735,2458,2459],{"class":1756},"    })\n",[1735,2461,2463],{"class":1737,"line":2462},23,[1735,2464,1797],{"emptyLinePlaceholder":1796},[1735,2466,2468],{"class":1737,"line":2467},24,[1735,2469,2470],{"class":1740},"    // Emit an event (async with backpressure)\n",[1735,2472,2474,2476,2478,2480,2482,2485,2487,2490,2493,2495],{"class":1737,"line":2473},25,[1735,2475,2323],{"class":1746},[1735,2477,1757],{"class":1756},[1735,2479,903],{"class":1760},[1735,2481,1763],{"class":1756},[1735,2483,2484],{"class":1746},"context",[1735,2486,1757],{"class":1756},[1735,2488,2489],{"class":1760},"Background",[1735,2491,2492],{"class":1756},"(),",[1735,2494,1860],{"class":1746},[1735,2496,1863],{"class":1756},[1735,2498,2500,2503,2505,2507,2509,2512],{"class":1737,"line":2499},26,[1735,2501,2502],{"class":1746},"        orderID",[1735,2504,1757],{"class":1756},[1735,2506,1303],{"class":1760},[1735,2508,1763],{"class":1756},[1735,2510,2511],{"class":1766},"\"ORDER-123\"",[1735,2513,1881],{"class":1756},[1735,2515,2517,2520,2522,2524,2526,2528],{"class":1737,"line":2516},27,[1735,2518,2519],{"class":1746},"        total",[1735,2521,1757],{"class":1756},[1735,2523,1303],{"class":1760},[1735,2525,1763],{"class":1756},[1735,2527,1897],{"class":1896},[1735,2529,1881],{"class":1756},[1735,2531,2533],{"class":1737,"line":2532},28,[1735,2534,2535],{"class":1756},"    )\n",[1735,2537,2539],{"class":1737,"line":2538},29,[1735,2540,1797],{"emptyLinePlaceholder":1796},[1735,2542,2544],{"class":1737,"line":2543},30,[1735,2545,2546],{"class":1740},"    // Gracefully drain pending events\n",[1735,2548,2550,2552,2554,2556],{"class":1737,"line":2549},31,[1735,2551,2323],{"class":1746},[1735,2553,1757],{"class":1756},[1735,2555,962],{"class":1760},[1735,2557,2558],{"class":1756},"()\n",[1735,2560,2562],{"class":1737,"line":2561},32,[1735,2563,2564],{"class":1756},"}\n",[1718,2566,27],{"id":2567},"capabilities",[2569,2570,2571,2586],"table",{},[2572,2573,2574],"thead",{},[2575,2576,2577,2581,2583],"tr",{},[2578,2579,2580],"th",{},"Feature",[2578,2582,1063],{},[2578,2584,2585],{},"Docs",[2587,2588,2589,2607,2619,2632,2644,2657],"tbody",{},[2575,2590,2591,2595,2602],{},[2592,2593,2594],"td",{},"Typed Fields",[2592,2596,2597,2598,2601],{},"Built-in keys for primitives; ",[1732,2599,2600],{},"NewKey[T]"," for any custom type",[2592,2603,2604],{},[1647,2605,110],{"href":2606},"docs/reference/fields",[2575,2608,2609,2611,2614],{},[2592,2610,227],{},[2592,2612,2613],{},"Each signal gets its own goroutine and buffered queue",[2592,2615,2616],{},[1647,2617,16],{"href":2618},"docs/learn/architecture",[2575,2620,2621,2623,2626],{},[2592,2622,140],{},[2592,2624,2625],{},"Cross-cutting handlers that see all signals (or a whitelist)",[2592,2627,2628],{},[1647,2629,2631],{"href":2630},"docs/learn/concepts","Concepts",[2575,2633,2634,2636,2639],{},[2592,2635,165],{},[2592,2637,2638],{},"Buffer sizes, rate limits, drop policies, panic handlers",[2592,2640,2641],{},[1647,2642,165],{"href":2643},"docs/guides/configuration",[2575,2645,2646,2649,2652],{},[2592,2647,2648],{},"Graceful Shutdown",[2592,2650,2651],{},"Drain pending events before exit",[2592,2653,2654],{},[1647,2655,617],{"href":2656},"docs/guides/best-practices",[2575,2658,2659,2662,2665],{},[2592,2660,2661],{},"Testing Utilities",[2592,2663,2664],{},"Sync mode, event capture, isolated instances",[2592,2666,2667],{},[1647,2668,549],{"href":2669},"docs/guides/testing",[1718,2671,2673],{"id":2672},"why-capitan","Why capitan?",[2675,2676,2677,2685,2691,2697,2703,2709],"ul",{},[2678,2679,2680,2684],"li",{},[2681,2682,2683],"strong",{},"Type-safe"," — Typed fields with compile-time safety, including custom structs",[2678,2686,2687,2690],{},[2681,2688,2689],{},"Zero dependencies"," — Standard library only",[2678,2692,2693,2696],{},[2681,2694,2695],{},"Async by default"," — Per-signal workers with backpressure",[2678,2698,2699,2702],{},[2681,2700,2701],{},"Isolated"," — Slow listeners don't affect other signals",[2678,2704,2705,2708],{},[2681,2706,2707],{},"Panic-safe"," — Listener panics recovered, system stays running",[2678,2710,2711,2714],{},[2681,2712,2713],{},"Testable"," — Sync mode and capture utilities for deterministic tests",[1718,2716,2718],{"id":2717},"decoupled-coordination","Decoupled Coordination",[1644,2720,2721,2722,1757],{},"Capitan enables a pattern: ",[2681,2723,2724],{},"packages emit, concerned parties listen, services observe",[1644,2726,2727],{},"Your domain packages emit events when meaningful things happen. Other packages hook the signals they care about. Service-level concerns — audit trails, structured logging, metrics collection — observe everything through a unified stream.",[1726,2729,2731],{"className":1728,"code":2730,"language":1730,"meta":34,"style":34},"// In your order package\ncapitan.Emit(ctx, orderCreated, orderID.Field(id), total.Field(amount))\n\n// In your notification package\ncapitan.Hook(orderCreated, sendConfirmationEmail)\n\n// In your audit service\ncapitan.Observe(writeToAuditLog)\n",[1732,2732,2733,2738,2783,2787,2792,2811,2815,2820],{"__ignoreMap":34},[1735,2734,2735],{"class":1737,"line":9},[1735,2736,2737],{"class":1740},"// In your order package\n",[1735,2739,2740,2742,2744,2746,2748,2750,2752,2754,2756,2758,2760,2762,2764,2766,2769,2771,2773,2775,2777,2780],{"class":1737,"line":19},[1735,2741,1642],{"class":1746},[1735,2743,1757],{"class":1756},[1735,2745,903],{"class":1760},[1735,2747,1763],{"class":1756},[1735,2749,1855],{"class":1746},[1735,2751,1825],{"class":1756},[1735,2753,1860],{"class":1746},[1735,2755,1825],{"class":1756},[1735,2757,1984],{"class":1746},[1735,2759,1757],{"class":1756},[1735,2761,1303],{"class":1760},[1735,2763,1763],{"class":1756},[1735,2765,2035],{"class":1746},[1735,2767,2768],{"class":1756},"),",[1735,2770,2012],{"class":1746},[1735,2772,1757],{"class":1756},[1735,2774,1303],{"class":1760},[1735,2776,1763],{"class":1756},[1735,2778,2779],{"class":1746},"amount",[1735,2781,2782],{"class":1756},"))\n",[1735,2784,2785],{"class":1737,"line":40},[1735,2786,1797],{"emptyLinePlaceholder":1796},[1735,2788,2789],{"class":1737,"line":988},[1735,2790,2791],{"class":1740},"// In your notification package\n",[1735,2793,2794,2796,2798,2800,2802,2804,2806,2809],{"class":1737,"line":1800},[1735,2795,1642],{"class":1746},[1735,2797,1757],{"class":1756},[1735,2799,932],{"class":1760},[1735,2801,1763],{"class":1756},[1735,2803,1809],{"class":1746},[1735,2805,1825],{"class":1756},[1735,2807,2808],{"class":1746}," sendConfirmationEmail",[1735,2810,1770],{"class":1756},[1735,2812,2813],{"class":1737,"line":1806},[1735,2814,1797],{"emptyLinePlaceholder":1796},[1735,2816,2817],{"class":1737,"line":1833},[1735,2818,2819],{"class":1740},"// In your audit service\n",[1735,2821,2822,2824,2826,2828,2830,2833],{"class":1737,"line":1838},[1735,2823,1642],{"class":1746},[1735,2825,1757],{"class":1756},[1735,2827,942],{"class":1760},[1735,2829,1763],{"class":1756},[1735,2831,2832],{"class":1746},"writeToAuditLog",[1735,2834,1770],{"class":1756},[1644,2836,2837],{},"Three packages, one event flow, zero direct imports between them.",[1718,2839,2841],{"id":2840},"documentation","Documentation",[1644,2843,2844,2845,2848],{},"Full documentation is available in the ",[1647,2846,2847],{"href":2847},"docs/"," directory:",[2850,2851,1553],"h3",{"id":2852},"learn",[2675,2854,2855,2861,2867,2872],{},[2678,2856,2857,2860],{},[1647,2858,6],{"href":2859},"docs/learn/overview"," — What capitan provides",[2678,2862,2863,2866],{},[1647,2864,53],{"href":2865},"docs/learn/quickstart"," — Get started in minutes",[2678,2868,2869,2871],{},[1647,2870,81],{"href":2630}," — Signals, keys, fields, listeners, observers",[2678,2873,2874,2876],{},[1647,2875,16],{"href":2618}," — Per-signal workers, event pooling, backpressure",[2850,2878,1567],{"id":2879},"guides",[2675,2881,2882,2887,2893,2899,2904,2909],{},[2678,2883,2884,2886],{},[1647,2885,165],{"href":2643}," — Buffer sizes, panic handlers, runtime metrics",[2678,2888,2889,2892],{},[1647,2890,427],{"href":2891},"docs/guides/context"," — Request tracing, cancellation, timeouts",[2678,2894,2895,2898],{},[1647,2896,491],{"href":2897},"docs/guides/errors"," — Error propagation, severity levels, retry patterns",[2678,2900,2901,2903],{},[1647,2902,549],{"href":2669}," — Sync mode, event capture, isolated instances",[2678,2905,2906,2908],{},[1647,2907,617],{"href":2656}," — Signal design, listener lifecycle, performance",[2678,2910,2911,2914],{},[1647,2912,738],{"href":2913},"docs/guides/persistence"," — Event storage and replay",[2850,2916,1584],{"id":2917},"integrations",[2675,2919,2920,2926,2932],{},[2678,2921,2922,2925],{},[1647,2923,814],{"href":2924},"docs/integrations/aperture"," — OpenTelemetry observability",[2678,2927,2928,2931],{},[1647,2929,838],{"href":2930},"docs/integrations/herald"," — Message broker bridge (Kafka, NATS)",[2678,2933,2934,2937,2938],{},[1647,2935,864],{"href":2936},"docs/integrations/ago"," — Workflow orchestration ",[2939,2940,2941],"em",{},"(Experimental)",[2850,2943,1595],{"id":2944},"reference",[2675,2946,2947,2953,2958],{},[2678,2948,2949,2952],{},[1647,2950,889],{"href":2951},"docs/reference/api"," — Complete function and type documentation",[2678,2954,2955,2957],{},[1647,2956,1196],{"href":2606}," — All built-in and custom field types",[2678,2959,2960,2964],{},[1647,2961,2963],{"href":2962},"docs/reference/testing","Testing Reference"," — Test utilities API",[1718,2966,2968],{"id":2967},"contributing","Contributing",[1644,2970,2971,2972,2976,2977,2980],{},"See ",[1647,2973,2975],{"href":2974},"CONTRIBUTING","CONTRIBUTING.md"," for guidelines. Run ",[1732,2978,2979],{},"make help"," for available commands.",[1718,2982,1694],{"id":2983},"license",[1644,2985,2986,2987,2989],{},"MIT License — see ",[1647,2988,1691],{"href":1691}," for details.",[2991,2992,2993],"style",{},"html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}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 .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 .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 .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 .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}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)}",{"title":34,"searchDepth":19,"depth":19,"links":2995},[2996,2997,2998,2999,3000,3001,3002,3008,3009],{"id":1720,"depth":19,"text":1721},{"id":2140,"depth":19,"text":66},{"id":2163,"depth":19,"text":2164},{"id":2567,"depth":19,"text":27},{"id":2672,"depth":19,"text":2673},{"id":2717,"depth":19,"text":2718},{"id":2840,"depth":19,"text":2841,"children":3003},[3004,3005,3006,3007],{"id":2852,"depth":40,"text":1553},{"id":2879,"depth":40,"text":1567},{"id":2917,"depth":40,"text":1584},{"id":2944,"depth":40,"text":1595},{"id":2967,"depth":19,"text":2968},{"id":2983,"depth":19,"text":1694},"md","book-open",{},"/readme",{"title":1635,"description":34},"readme","7s0cSFWpjHaQ0od9Wc1SkKDxJNyRMjrgdgzYKTnt5gc",{"id":3018,"title":3019,"body":3020,"description":34,"extension":3010,"icon":3438,"meta":3439,"navigation":1796,"path":3440,"seo":3441,"stem":3442,"__hash__":3443},"resources/security.md","Security",{"type":1637,"value":3021,"toc":3424},[3022,3026,3030,3033,3072,3076,3079,3083,3088,3091,3130,3134,3137,3186,3190,3216,3220,3223,3227,3230,3320,3324,3327,3356,3360,3363,3382,3386,3400,3404,3407,3412,3415,3421],[1640,3023,3025],{"id":3024},"security-policy","Security Policy",[1718,3027,3029],{"id":3028},"supported-versions","Supported Versions",[1644,3031,3032],{},"We release patches for security vulnerabilities. Which versions are eligible for receiving such patches depends on the CVSS v3.0 Rating:",[2569,3034,3035,3048],{},[2572,3036,3037],{},[2575,3038,3039,3042,3045],{},[2578,3040,3041],{},"Version",[2578,3043,3044],{},"Supported",[2578,3046,3047],{},"Status",[2587,3049,3050,3061],{},[2575,3051,3052,3055,3058],{},[2592,3053,3054],{},"latest",[2592,3056,3057],{},"✅",[2592,3059,3060],{},"Active development",[2575,3062,3063,3066,3069],{},[2592,3064,3065],{},"\u003C latest",[2592,3067,3068],{},"❌",[2592,3070,3071],{},"Security fixes only for critical issues",[1718,3073,3075],{"id":3074},"reporting-a-vulnerability","Reporting a Vulnerability",[1644,3077,3078],{},"We take the security of capitan seriously. If you have discovered a security vulnerability in this project, please report it responsibly.",[2850,3080,3082],{"id":3081},"how-to-report","How to Report",[1644,3084,3085],{},[2681,3086,3087],{},"Please DO NOT report security vulnerabilities through public GitHub issues.",[1644,3089,3090],{},"Instead, please report them via one of the following methods:",[3092,3093,3094,3117],"ol",{},[2678,3095,3096,3099,3100],{},[2681,3097,3098],{},"GitHub Security Advisories"," (Preferred)",[2675,3101,3102,3111,3114],{},[2678,3103,3104,3105,3110],{},"Go to the ",[1647,3106,3109],{"href":3107,"rel":3108},"https://github.com/zoobz-io/capitan/security",[1651],"Security tab"," of this repository",[2678,3112,3113],{},"Click \"Report a vulnerability\"",[2678,3115,3116],{},"Fill out the form with details about the vulnerability",[2678,3118,3119,3122],{},[2681,3120,3121],{},"Email",[2675,3123,3124,3127],{},[2678,3125,3126],{},"Send details to the repository maintainer through GitHub profile contact information",[2678,3128,3129],{},"Use PGP encryption if possible for sensitive details",[2850,3131,3133],{"id":3132},"what-to-include","What to Include",[1644,3135,3136],{},"Please include the following information (as much as you can provide) to help us better understand the nature and scope of the possible issue:",[2675,3138,3139,3145,3151,3157,3163,3168,3174,3180],{},[2678,3140,3141,3144],{},[2681,3142,3143],{},"Type of issue"," (e.g., race condition, deadlock, memory leak, etc.)",[2678,3146,3147,3150],{},[2681,3148,3149],{},"Full paths of source file(s)"," related to the manifestation of the issue",[2678,3152,3153,3156],{},[2681,3154,3155],{},"The location of the affected source code"," (tag/branch/commit or direct URL)",[2678,3158,3159,3162],{},[2681,3160,3161],{},"Any special configuration required"," to reproduce the issue",[2678,3164,3165,3162],{},[2681,3166,3167],{},"Step-by-step instructions",[2678,3169,3170,3173],{},[2681,3171,3172],{},"Proof-of-concept or exploit code"," (if possible)",[2678,3175,3176,3179],{},[2681,3177,3178],{},"Impact of the issue",", including how an attacker might exploit the issue",[2678,3181,3182,3185],{},[2681,3183,3184],{},"Your name and affiliation"," (optional)",[2850,3187,3189],{"id":3188},"what-to-expect","What to Expect",[2675,3191,3192,3198,3204,3210],{},[2678,3193,3194,3197],{},[2681,3195,3196],{},"Acknowledgment",": We will acknowledge receipt of your vulnerability report within 48 hours",[2678,3199,3200,3203],{},[2681,3201,3202],{},"Initial Assessment",": Within 7 days, we will provide an initial assessment of the report",[2678,3205,3206,3209],{},[2681,3207,3208],{},"Resolution Timeline",": We aim to resolve critical issues within 30 days",[2678,3211,3212,3215],{},[2681,3213,3214],{},"Disclosure",": We will coordinate with you on the disclosure timeline",[2850,3217,3219],{"id":3218},"preferred-languages","Preferred Languages",[1644,3221,3222],{},"We prefer all communications to be in English.",[1718,3224,3226],{"id":3225},"security-best-practices","Security Best Practices",[1644,3228,3229],{},"When using capitan in your applications, we recommend:",[3092,3231,3232,3253,3273,3288,3304],{},[2678,3233,3234,3237],{},[2681,3235,3236],{},"Keep Dependencies Updated",[1726,3238,3240],{"className":2143,"code":3239,"language":2145,"meta":34,"style":34},"go get -u github.com/zoobz-io/capitan\n",[1732,3241,3242],{"__ignoreMap":34},[1735,3243,3244,3246,3248,3251],{"class":1737,"line":9},[1735,3245,1730],{"class":1760},[1735,3247,2154],{"class":1766},[1735,3249,3250],{"class":1934}," -u",[1735,3252,2157],{"class":1766},[2678,3254,3255,3258],{},[2681,3256,3257],{},"Resource Management",[2675,3259,3260,3267,3270],{},[2678,3261,3262,3263,3266],{},"Call ",[1732,3264,3265],{},"Shutdown()"," before application exit",[2678,3268,3269],{},"Close listeners when no longer needed",[2678,3271,3272],{},"Don't emit events after shutdown",[2678,3274,3275,3277],{},[2681,3276,683],{},[2675,3278,3279,3282,3285],{},[2678,3280,3281],{},"Implement proper error handling in listeners",[2678,3283,3284],{},"Don't panic in listener callbacks",[2678,3286,3287],{},"Log errors appropriately",[2678,3289,3290,3293],{},[2681,3291,3292],{},"Input Validation",[2675,3294,3295,3298,3301],{},[2678,3296,3297],{},"Validate event fields in listeners",[2678,3299,3300],{},"Use type assertions safely",[2678,3302,3303],{},"Handle missing fields gracefully",[2678,3305,3306,3309],{},[2681,3307,3308],{},"Concurrency Safety",[2675,3310,3311,3314,3317],{},[2678,3312,3313],{},"Don't modify events in listeners (they're meant to be immutable)",[2678,3315,3316],{},"Don't share mutable state across listeners without synchronization",[2678,3318,3319],{},"Be aware that listeners run asynchronously",[1718,3321,3323],{"id":3322},"security-features","Security Features",[1644,3325,3326],{},"capitan includes several built-in security features:",[2675,3328,3329,3334,3339,3345,3350],{},[2678,3330,3331,3333],{},[2681,3332,37],{},": Typed fields prevent type confusion",[2678,3335,3336,3338],{},[2681,3337,272],{},": Listener panics caught and isolated",[2678,3340,3341,3344],{},[2681,3342,3343],{},"Worker Isolation",": Per-signal workers prevent cascading failures",[2678,3346,3347,3349],{},[2681,3348,2648],{},": Pending events processed on shutdown",[2678,3351,3352,3355],{},[2681,3353,3354],{},"Zero Dependencies",": No external dependencies reduce attack surface",[1718,3357,3359],{"id":3358},"automated-security-scanning","Automated Security Scanning",[1644,3361,3362],{},"This project uses:",[2675,3364,3365,3370,3376],{},[2678,3366,3367,3369],{},[2681,3368,1679],{},": GitHub's semantic code analysis for security vulnerabilities",[2678,3371,3372,3375],{},[2681,3373,3374],{},"golangci-lint",": Static analysis including security linters",[2678,3377,3378,3381],{},[2681,3379,3380],{},"Codecov",": Coverage tracking to ensure security-critical code is tested",[1718,3383,3385],{"id":3384},"vulnerability-disclosure-policy","Vulnerability Disclosure Policy",[2675,3387,3388,3391,3394,3397],{},[2678,3389,3390],{},"Security vulnerabilities will be disclosed via GitHub Security Advisories",[2678,3392,3393],{},"We follow a 90-day disclosure timeline for non-critical issues",[2678,3395,3396],{},"Critical vulnerabilities may be disclosed sooner after patches are available",[2678,3398,3399],{},"We will credit reporters who follow responsible disclosure practices",[1718,3401,3403],{"id":3402},"credits","Credits",[1644,3405,3406],{},"We thank the following individuals for responsibly disclosing security issues:",[1644,3408,3409],{},[2939,3410,3411],{},"This list is currently empty. Be the first to help improve our security!",[3413,3414],"hr",{},[1644,3416,3417,3420],{},[2681,3418,3419],{},"Last Updated",": 2024-10-24",[2991,3422,3423],{},"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":34,"searchDepth":19,"depth":19,"links":3425},[3426,3427,3433,3434,3435,3436,3437],{"id":3028,"depth":19,"text":3029},{"id":3074,"depth":19,"text":3075,"children":3428},[3429,3430,3431,3432],{"id":3081,"depth":40,"text":3082},{"id":3132,"depth":40,"text":3133},{"id":3188,"depth":40,"text":3189},{"id":3218,"depth":40,"text":3219},{"id":3225,"depth":19,"text":3226},{"id":3322,"depth":19,"text":3323},{"id":3358,"depth":19,"text":3359},{"id":3384,"depth":19,"text":3385},{"id":3402,"depth":19,"text":3403},"shield",{},"/security",{"title":3019,"description":34},"security","cxnIfe_6JB3lcouK1M8FV-005g-to7mamhK2pvZHYS4",{"id":3445,"title":2968,"body":3446,"description":3454,"extension":3010,"icon":1732,"meta":3930,"navigation":1796,"path":3931,"seo":3932,"stem":2967,"__hash__":3933},"resources/contributing.md",{"type":1637,"value":3447,"toc":3906},[3448,3452,3455,3459,3462,3466,3516,3520,3524,3541,3544,3560,3562,3573,3577,3581,3595,3599,3610,3614,3617,3631,3635,3663,3666,3669,3682,3685,3697,3700,3712,3715,3727,3731,3739,3743,3746,3790,3794,3798,3801,3812,3815,3829,3833,3861,3867,3871,3874,3885,3889,3900,3903],[1640,3449,3451],{"id":3450},"contributing-to-capitan","Contributing to capitan",[1644,3453,3454],{},"Thank you for your interest in contributing to capitan! This guide will help you get started.",[1718,3456,3458],{"id":3457},"code-of-conduct","Code of Conduct",[1644,3460,3461],{},"By participating in this project, you agree to maintain a respectful and inclusive environment for all contributors.",[1718,3463,3465],{"id":3464},"getting-started","Getting Started",[3092,3467,3468,3471,3477,3483,3489,3492,3498,3504,3507,3513],{},[2678,3469,3470],{},"Fork the repository",[2678,3472,3473,3474],{},"Clone your fork: ",[1732,3475,3476],{},"git clone https://github.com/yourusername/capitan.git",[2678,3478,3479,3480,3482],{},"Run ",[1732,3481,2979],{}," to see available commands",[2678,3484,3485,3486],{},"Create a feature branch: ",[1732,3487,3488],{},"git checkout -b feature/your-feature-name",[2678,3490,3491],{},"Make your changes",[2678,3493,3494,3495],{},"Run tests: ",[1732,3496,3497],{},"make test",[2678,3499,3500,3501],{},"Run linters: ",[1732,3502,3503],{},"make lint",[2678,3505,3506],{},"Commit your changes with a descriptive message",[2678,3508,3509,3510],{},"Push to your fork: ",[1732,3511,3512],{},"git push origin feature/your-feature-name",[2678,3514,3515],{},"Create a Pull Request",[1718,3517,3519],{"id":3518},"development-guidelines","Development Guidelines",[2850,3521,3523],{"id":3522},"code-style","Code Style",[2675,3525,3526,3529,3535,3538],{},[2678,3527,3528],{},"Follow standard Go conventions",[2678,3530,3479,3531,3534],{},[1732,3532,3533],{},"go fmt"," before committing",[2678,3536,3537],{},"Add comments for exported functions and types",[2678,3539,3540],{},"Keep functions small and focused",[2850,3542,549],{"id":3543},"testing",[2675,3545,3546,3549,3554,3557],{},[2678,3547,3548],{},"Write tests for new functionality",[2678,3550,3551,3552],{},"Ensure all tests pass: ",[1732,3553,3497],{},[2678,3555,3556],{},"Maintain 1:1 file-to-test ratio",[2678,3558,3559],{},"Aim for 80%+ test coverage",[2850,3561,2841],{"id":2840},[2675,3563,3564,3567,3570],{},[2678,3565,3566],{},"Update README.md for API changes",[2678,3568,3569],{},"Add comments to all exported types",[2678,3571,3572],{},"Keep doc comments clear and concise",[1718,3574,3576],{"id":3575},"types-of-contributions","Types of Contributions",[2850,3578,3580],{"id":3579},"bug-reports","Bug Reports",[2675,3582,3583,3586,3589,3592],{},[2678,3584,3585],{},"Use GitHub Issues",[2678,3587,3588],{},"Include minimal reproduction code",[2678,3590,3591],{},"Describe expected vs actual behavior",[2678,3593,3594],{},"Include Go version and OS",[2850,3596,3598],{"id":3597},"feature-requests","Feature Requests",[2675,3600,3601,3604,3607],{},[2678,3602,3603],{},"Open an issue for discussion first",[2678,3605,3606],{},"Explain the use case",[2678,3608,3609],{},"Consider backwards compatibility",[2850,3611,3613],{"id":3612},"code-contributions","Code Contributions",[1644,3615,3616],{},"All contributions should:",[2675,3618,3619,3622,3625,3628],{},[2678,3620,3621],{},"Include comprehensive tests",[2678,3623,3624],{},"Pass linter checks",[2678,3626,3627],{},"Maintain existing code style",[2678,3629,3630],{},"Update documentation as needed",[1718,3632,3634],{"id":3633},"pull-request-process","Pull Request Process",[3092,3636,3637,3643,3648,3653,3658],{},[2678,3638,3639,3642],{},[2681,3640,3641],{},"Keep PRs focused"," - One feature/fix per PR",[2678,3644,3645],{},[2681,3646,3647],{},"Write descriptive commit messages",[2678,3649,3650],{},[2681,3651,3652],{},"Update tests and documentation",[2678,3654,3655],{},[2681,3656,3657],{},"Ensure CI passes",[2678,3659,3660],{},[2681,3661,3662],{},"Respond to review feedback",[1718,3664,549],{"id":3665},"testing-1",[1644,3667,3668],{},"Run the full test suite:",[1726,3670,3672],{"className":2143,"code":3671,"language":2145,"meta":34,"style":34},"make test\n",[1732,3673,3674],{"__ignoreMap":34},[1735,3675,3676,3679],{"class":1737,"line":9},[1735,3677,3678],{"class":1760},"make",[1735,3680,3681],{"class":1766}," test\n",[1644,3683,3684],{},"Run with coverage:",[1726,3686,3688],{"className":2143,"code":3687,"language":2145,"meta":34,"style":34},"make coverage\n",[1732,3689,3690],{"__ignoreMap":34},[1735,3691,3692,3694],{"class":1737,"line":9},[1735,3693,3678],{"class":1760},[1735,3695,3696],{"class":1766}," coverage\n",[1644,3698,3699],{},"Run linters:",[1726,3701,3703],{"className":2143,"code":3702,"language":2145,"meta":34,"style":34},"make lint\n",[1732,3704,3705],{"__ignoreMap":34},[1735,3706,3707,3709],{"class":1737,"line":9},[1735,3708,3678],{"class":1760},[1735,3710,3711],{"class":1766}," lint\n",[1644,3713,3714],{},"Run full CI simulation:",[1726,3716,3718],{"className":2143,"code":3717,"language":2145,"meta":34,"style":34},"make ci\n",[1732,3719,3720],{"__ignoreMap":34},[1735,3721,3722,3724],{"class":1737,"line":9},[1735,3723,3678],{"class":1760},[1735,3725,3726],{"class":1766}," ci\n",[1718,3728,3730],{"id":3729},"project-structure","Project Structure",[1726,3732,3737],{"className":3733,"code":3735,"language":3736},[3734],"language-text","capitan/\n├── *.go              # Core library files\n├── *_test.go         # Tests (1:1 with source files)\n├── .github/          # GitHub workflows and templates\n├── README.md         # Project documentation\n└── Makefile          # Build and test commands\n","text",[1732,3738,3735],{"__ignoreMap":34},[1718,3740,3742],{"id":3741},"commit-messages","Commit Messages",[1644,3744,3745],{},"Follow conventional commits:",[2675,3747,3748,3754,3760,3766,3772,3778,3784],{},[2678,3749,3750,3753],{},[1732,3751,3752],{},"feat:"," New feature",[2678,3755,3756,3759],{},[1732,3757,3758],{},"fix:"," Bug fix",[2678,3761,3762,3765],{},[1732,3763,3764],{},"docs:"," Documentation changes",[2678,3767,3768,3771],{},[1732,3769,3770],{},"test:"," Test additions/changes",[2678,3773,3774,3777],{},[1732,3775,3776],{},"refactor:"," Code refactoring",[2678,3779,3780,3783],{},[1732,3781,3782],{},"perf:"," Performance improvements",[2678,3785,3786,3789],{},[1732,3787,3788],{},"chore:"," Maintenance tasks",[1718,3791,3793],{"id":3792},"release-process","Release Process",[2850,3795,3797],{"id":3796},"automated-releases","Automated Releases",[1644,3799,3800],{},"This project uses automated release versioning. To create a release:",[3092,3802,3803,3806,3809],{},[2678,3804,3805],{},"Go to Actions → Release → Run workflow",[2678,3807,3808],{},"Leave \"Version override\" empty for automatic version inference",[2678,3810,3811],{},"Click \"Run workflow\"",[1644,3813,3814],{},"The system will:",[2675,3816,3817,3820,3823,3826],{},[2678,3818,3819],{},"Automatically determine the next version from conventional commits",[2678,3821,3822],{},"Create a git tag",[2678,3824,3825],{},"Generate release notes via GoReleaser",[2678,3827,3828],{},"Publish the release to GitHub",[2850,3830,3832],{"id":3831},"commit-conventions-for-versioning","Commit Conventions for Versioning",[2675,3834,3835,3840,3845,3851],{},[2678,3836,3837,3839],{},[1732,3838,3752],{}," new features (minor version: 1.2.0 → 1.3.0)",[2678,3841,3842,3844],{},[1732,3843,3758],{}," bug fixes (patch version: 1.2.0 → 1.2.1)",[2678,3846,3847,3850],{},[1732,3848,3849],{},"feat!:"," breaking changes (major version: 1.2.0 → 2.0.0)",[2678,3852,3853,3855,3856,3855,3858,3860],{},[1732,3854,3764],{},", ",[1732,3857,3770],{},[1732,3859,3788],{}," no version change",[1644,3862,3863,3864],{},"Example: ",[1732,3865,3866],{},"feat(worker): add configurable buffer size",[2850,3868,3870],{"id":3869},"version-preview-on-pull-requests","Version Preview on Pull Requests",[1644,3872,3873],{},"Every PR automatically shows the next version that will be created:",[2675,3875,3876,3879,3882],{},[2678,3877,3878],{},"Check PR comments for \"Version Preview\"",[2678,3880,3881],{},"Updates automatically as you add commits",[2678,3883,3884],{},"Helps verify your commits have the intended effect",[1718,3886,3888],{"id":3887},"questions","Questions?",[2675,3890,3891,3894,3897],{},[2678,3892,3893],{},"Open an issue for questions",[2678,3895,3896],{},"Check existing issues first",[2678,3898,3899],{},"Be patient and respectful",[1644,3901,3902],{},"Thank you for contributing to capitan!",[2991,3904,3905],{},"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":34,"searchDepth":19,"depth":19,"links":3907},[3908,3909,3910,3915,3920,3921,3922,3923,3924,3929],{"id":3457,"depth":19,"text":3458},{"id":3464,"depth":19,"text":3465},{"id":3518,"depth":19,"text":3519,"children":3911},[3912,3913,3914],{"id":3522,"depth":40,"text":3523},{"id":3543,"depth":40,"text":549},{"id":2840,"depth":40,"text":2841},{"id":3575,"depth":19,"text":3576,"children":3916},[3917,3918,3919],{"id":3579,"depth":40,"text":3580},{"id":3597,"depth":40,"text":3598},{"id":3612,"depth":40,"text":3613},{"id":3633,"depth":19,"text":3634},{"id":3665,"depth":19,"text":549},{"id":3729,"depth":19,"text":3730},{"id":3741,"depth":19,"text":3742},{"id":3792,"depth":19,"text":3793,"children":3925},[3926,3927,3928],{"id":3796,"depth":40,"text":3797},{"id":3831,"depth":40,"text":3832},{"id":3869,"depth":40,"text":3870},{"id":3887,"depth":19,"text":3888},{},"/contributing",{"title":2968,"description":3454},"pIKYb0iTJ4mdpN4L33Blk2DAa8mDQguOoNlB5jx5u18",{"id":3935,"title":617,"author":3936,"body":3937,"description":619,"extension":3010,"meta":7235,"navigation":1796,"path":616,"published":7236,"readtime":7237,"seo":7238,"stem":1580,"tags":7239,"updated":7236,"__hash__":7242},"capitan/v1.0.2/2.guides/5.best-practices.md","zoobzio",{"type":1637,"value":3938,"toc":7203},[3939,3942,3944,3947,3950,3953,4095,4098,4193,4196,4381,4384,4387,4427,4430,4444,4447,4450,4546,4549,4552,4555,4685,4688,4691,4806,4809,4812,4928,4931,4934,4937,5180,5183,5186,5405,5408,5411,5531,5534,5537,5540,5676,5679,5682,5818,5821,5824,6010,6013,6016,6019,6297,6300,6303,6581,6584,6587,6813,6815,6818,6821,6901,6904,6907,7048,7051,7054,7200],[1640,3940,617],{"id":3941},"best-practices",[1644,3943,623],{},[1718,3945,626],{"id":3946},"signal-design",[2850,3948,630],{"id":3949},"define-signals-as-package-constants",[1644,3951,3952],{},"Signals create internal state (workers, registries). Define them at package level:",[1726,3954,3956],{"className":1728,"code":3955,"language":1730,"meta":34,"style":34},"// signals.go\npackage orders\n\nimport \"github.com/zoobz-io/capitan\"\n\nvar (\n    OrderCreated   = capitan.NewSignal(\"order.created\", \"New order placed\")\n    OrderConfirmed = capitan.NewSignal(\"order.confirmed\", \"Order confirmed\")\n    OrderShipped   = capitan.NewSignal(\"order.shipped\", \"Order shipped\")\n    OrderCanceled  = capitan.NewSignal(\"order.canceled\", \"Order canceled\")\n)\n",[1732,3957,3958,3963,3970,3974,3981,3985,3991,4015,4040,4065,4091],{"__ignoreMap":34},[1735,3959,3960],{"class":1737,"line":9},[1735,3961,3962],{"class":1740},"// signals.go\n",[1735,3964,3965,3967],{"class":1737,"line":19},[1735,3966,2174],{"class":1934},[1735,3968,3969],{"class":1943}," orders\n",[1735,3971,3972],{"class":1737,"line":40},[1735,3973,1797],{"emptyLinePlaceholder":1796},[1735,3975,3976,3978],{"class":1737,"line":988},[1735,3977,2186],{"class":1934},[1735,3979,3980],{"class":1766}," \"github.com/zoobz-io/capitan\"\n",[1735,3982,3983],{"class":1737,"line":1800},[1735,3984,1797],{"emptyLinePlaceholder":1796},[1735,3986,3987,3989],{"class":1737,"line":1806},[1735,3988,2223],{"class":1934},[1735,3990,2190],{"class":1756},[1735,3992,3993,3996,3999,4001,4003,4005,4007,4009,4011,4013],{"class":1737,"line":1833},[1735,3994,3995],{"class":1746},"    OrderCreated",[1735,3997,3998],{"class":1746},"   =",[1735,4000,1753],{"class":1746},[1735,4002,1757],{"class":1756},[1735,4004,1050],{"class":1760},[1735,4006,1763],{"class":1756},[1735,4008,1822],{"class":1766},[1735,4010,1825],{"class":1756},[1735,4012,1828],{"class":1766},[1735,4014,1770],{"class":1756},[1735,4016,4017,4020,4022,4024,4026,4028,4030,4033,4035,4038],{"class":1737,"line":1838},[1735,4018,4019],{"class":1746},"    OrderConfirmed",[1735,4021,2233],{"class":1746},[1735,4023,1753],{"class":1746},[1735,4025,1757],{"class":1756},[1735,4027,1050],{"class":1760},[1735,4029,1763],{"class":1756},[1735,4031,4032],{"class":1766},"\"order.confirmed\"",[1735,4034,1825],{"class":1756},[1735,4036,4037],{"class":1766}," \"Order confirmed\"",[1735,4039,1770],{"class":1756},[1735,4041,4042,4045,4047,4049,4051,4053,4055,4058,4060,4063],{"class":1737,"line":1844},[1735,4043,4044],{"class":1746},"    OrderShipped",[1735,4046,3998],{"class":1746},[1735,4048,1753],{"class":1746},[1735,4050,1757],{"class":1756},[1735,4052,1050],{"class":1760},[1735,4054,1763],{"class":1756},[1735,4056,4057],{"class":1766},"\"order.shipped\"",[1735,4059,1825],{"class":1756},[1735,4061,4062],{"class":1766}," \"Order shipped\"",[1735,4064,1770],{"class":1756},[1735,4066,4067,4070,4073,4075,4077,4079,4081,4084,4086,4089],{"class":1737,"line":1866},[1735,4068,4069],{"class":1746},"    OrderCanceled",[1735,4071,4072],{"class":1746},"  =",[1735,4074,1753],{"class":1746},[1735,4076,1757],{"class":1756},[1735,4078,1050],{"class":1760},[1735,4080,1763],{"class":1756},[1735,4082,4083],{"class":1766},"\"order.canceled\"",[1735,4085,1825],{"class":1756},[1735,4087,4088],{"class":1766}," \"Order canceled\"",[1735,4090,1770],{"class":1756},[1735,4092,4093],{"class":1737,"line":1884},[1735,4094,1770],{"class":1756},[1644,4096,4097],{},"Never create signals dynamically:",[1726,4099,4101],{"className":1728,"code":4100,"language":1730,"meta":34,"style":34},"// Bad: unbounded signal creation\nsignal := capitan.NewSignal(fmt.Sprintf(\"user.%s.action\", userID), \"...\")\n\n// Good: use fields for variable data\ncapitan.Emit(ctx, userAction, userID.Field(id))\n",[1732,4102,4103,4108,4153,4157,4162],{"__ignoreMap":34},[1735,4104,4105],{"class":1737,"line":9},[1735,4106,4107],{"class":1740},"// Bad: unbounded signal creation\n",[1735,4109,4110,4113,4115,4117,4119,4121,4123,4126,4128,4131,4133,4136,4138,4141,4143,4146,4148,4151],{"class":1737,"line":19},[1735,4111,4112],{"class":1746},"signal",[1735,4114,1750],{"class":1746},[1735,4116,1753],{"class":1746},[1735,4118,1757],{"class":1756},[1735,4120,1050],{"class":1760},[1735,4122,1763],{"class":1756},[1735,4124,4125],{"class":1746},"fmt",[1735,4127,1757],{"class":1756},[1735,4129,4130],{"class":1760},"Sprintf",[1735,4132,1763],{"class":1756},[1735,4134,4135],{"class":1766},"\"user.",[1735,4137,2429],{"class":2428},[1735,4139,4140],{"class":1766},".action\"",[1735,4142,1825],{"class":1756},[1735,4144,4145],{"class":1746}," userID",[1735,4147,2768],{"class":1756},[1735,4149,4150],{"class":1766}," \"...\"",[1735,4152,1770],{"class":1756},[1735,4154,4155],{"class":1737,"line":40},[1735,4156,1797],{"emptyLinePlaceholder":1796},[1735,4158,4159],{"class":1737,"line":988},[1735,4160,4161],{"class":1740},"// Good: use fields for variable data\n",[1735,4163,4164,4166,4168,4170,4172,4174,4176,4179,4181,4183,4185,4187,4189,4191],{"class":1737,"line":1800},[1735,4165,1642],{"class":1746},[1735,4167,1757],{"class":1756},[1735,4169,903],{"class":1760},[1735,4171,1763],{"class":1756},[1735,4173,1855],{"class":1746},[1735,4175,1825],{"class":1756},[1735,4177,4178],{"class":1746}," userAction",[1735,4180,1825],{"class":1756},[1735,4182,4145],{"class":1746},[1735,4184,1757],{"class":1756},[1735,4186,1303],{"class":1760},[1735,4188,1763],{"class":1756},[1735,4190,2035],{"class":1746},[1735,4192,2782],{"class":1756},[1644,4194,4195],{},"Signals are compared by identity, not name. Using different instances with the same name won't match:",[1726,4197,4199],{"className":1728,"code":4198,"language":1730,"meta":34,"style":34},"// Wrong: different signal instances\nvar SignalA = capitan.NewSignal(\"order.created\", \"...\")\nvar SignalB = capitan.NewSignal(\"order.created\", \"...\") // Different instance\n\ncapitan.Hook(SignalA, handler)\ncapitan.Emit(ctx, SignalB, fields...) // Won't match SignalA\n\n// Correct: import and use the same signal\nimport \"myapp/orders\"\ncapitan.Hook(orders.OrderCreated, handler)\ncapitan.Emit(ctx, orders.OrderCreated, fields...)\n",[1732,4200,4201,4206,4231,4259,4263,4283,4312,4316,4321,4328,4352],{"__ignoreMap":34},[1735,4202,4203],{"class":1737,"line":9},[1735,4204,4205],{"class":1740},"// Wrong: different signal instances\n",[1735,4207,4208,4210,4213,4215,4217,4219,4221,4223,4225,4227,4229],{"class":1737,"line":19},[1735,4209,2223],{"class":1934},[1735,4211,4212],{"class":1746}," SignalA",[1735,4214,2233],{"class":1746},[1735,4216,1753],{"class":1746},[1735,4218,1757],{"class":1756},[1735,4220,1050],{"class":1760},[1735,4222,1763],{"class":1756},[1735,4224,1822],{"class":1766},[1735,4226,1825],{"class":1756},[1735,4228,4150],{"class":1766},[1735,4230,1770],{"class":1756},[1735,4232,4233,4235,4238,4240,4242,4244,4246,4248,4250,4252,4254,4256],{"class":1737,"line":40},[1735,4234,2223],{"class":1934},[1735,4236,4237],{"class":1746}," SignalB",[1735,4239,2233],{"class":1746},[1735,4241,1753],{"class":1746},[1735,4243,1757],{"class":1756},[1735,4245,1050],{"class":1760},[1735,4247,1763],{"class":1756},[1735,4249,1822],{"class":1766},[1735,4251,1825],{"class":1756},[1735,4253,4150],{"class":1766},[1735,4255,1966],{"class":1756},[1735,4257,4258],{"class":1740}," // Different instance\n",[1735,4260,4261],{"class":1737,"line":988},[1735,4262,1797],{"emptyLinePlaceholder":1796},[1735,4264,4265,4267,4269,4271,4273,4276,4278,4281],{"class":1737,"line":1800},[1735,4266,1642],{"class":1746},[1735,4268,1757],{"class":1756},[1735,4270,932],{"class":1760},[1735,4272,1763],{"class":1756},[1735,4274,4275],{"class":1746},"SignalA",[1735,4277,1825],{"class":1756},[1735,4279,4280],{"class":1746}," handler",[1735,4282,1770],{"class":1756},[1735,4284,4285,4287,4289,4291,4293,4295,4297,4299,4301,4304,4307,4309],{"class":1737,"line":1806},[1735,4286,1642],{"class":1746},[1735,4288,1757],{"class":1756},[1735,4290,903],{"class":1760},[1735,4292,1763],{"class":1756},[1735,4294,1855],{"class":1746},[1735,4296,1825],{"class":1756},[1735,4298,4237],{"class":1746},[1735,4300,1825],{"class":1756},[1735,4302,4303],{"class":1746}," fields",[1735,4305,4306],{"class":1956},"...",[1735,4308,1966],{"class":1756},[1735,4310,4311],{"class":1740}," // Won't match SignalA\n",[1735,4313,4314],{"class":1737,"line":1833},[1735,4315,1797],{"emptyLinePlaceholder":1796},[1735,4317,4318],{"class":1737,"line":1838},[1735,4319,4320],{"class":1740},"// Correct: import and use the same signal\n",[1735,4322,4323,4325],{"class":1737,"line":1844},[1735,4324,2186],{"class":1934},[1735,4326,4327],{"class":1766}," \"myapp/orders\"\n",[1735,4329,4330,4332,4334,4336,4338,4341,4343,4346,4348,4350],{"class":1737,"line":1866},[1735,4331,1642],{"class":1746},[1735,4333,1757],{"class":1756},[1735,4335,932],{"class":1760},[1735,4337,1763],{"class":1756},[1735,4339,4340],{"class":1746},"orders",[1735,4342,1757],{"class":1756},[1735,4344,4345],{"class":1746},"OrderCreated",[1735,4347,1825],{"class":1756},[1735,4349,4280],{"class":1746},[1735,4351,1770],{"class":1756},[1735,4353,4354,4356,4358,4360,4362,4364,4366,4369,4371,4373,4375,4377,4379],{"class":1737,"line":1884},[1735,4355,1642],{"class":1746},[1735,4357,1757],{"class":1756},[1735,4359,903],{"class":1760},[1735,4361,1763],{"class":1756},[1735,4363,1855],{"class":1746},[1735,4365,1825],{"class":1756},[1735,4367,4368],{"class":1746}," orders",[1735,4370,1757],{"class":1756},[1735,4372,4345],{"class":1746},[1735,4374,1825],{"class":1756},[1735,4376,4303],{"class":1746},[1735,4378,4306],{"class":1956},[1735,4380,1770],{"class":1756},[2850,4382,635],{"id":4383},"use-hierarchical-naming",[1644,4385,4386],{},"Name signals with dot-separated hierarchies:",[1726,4388,4390],{"className":1728,"code":4389,"language":1730,"meta":34,"style":34},"// Domain.action pattern\n\"order.created\"\n\"order.shipped\"\n\"payment.processed\"\n\"payment.failed\"\n\"inventory.reserved\"\n\"inventory.released\"\n",[1732,4391,4392,4397,4402,4407,4412,4417,4422],{"__ignoreMap":34},[1735,4393,4394],{"class":1737,"line":9},[1735,4395,4396],{"class":1740},"// Domain.action pattern\n",[1735,4398,4399],{"class":1737,"line":19},[1735,4400,4401],{"class":1766},"\"order.created\"\n",[1735,4403,4404],{"class":1737,"line":40},[1735,4405,4406],{"class":1766},"\"order.shipped\"\n",[1735,4408,4409],{"class":1737,"line":988},[1735,4410,4411],{"class":1766},"\"payment.processed\"\n",[1735,4413,4414],{"class":1737,"line":1800},[1735,4415,4416],{"class":1766},"\"payment.failed\"\n",[1735,4418,4419],{"class":1737,"line":1806},[1735,4420,4421],{"class":1766},"\"inventory.reserved\"\n",[1735,4423,4424],{"class":1737,"line":1833},[1735,4425,4426],{"class":1766},"\"inventory.released\"\n",[1644,4428,4429],{},"Benefits:",[2675,4431,4432,4438,4441],{},[2678,4433,4434,4435,1966],{},"Grep-friendly (",[1732,4436,4437],{},"grep \"order\\.\"",[2678,4439,4440],{},"Observer whitelisting by prefix (manual filtering)",[2678,4442,4443],{},"Clear ownership boundaries",[2850,4445,640],{"id":4446},"write-descriptive-descriptions",[1644,4448,4449],{},"The description appears in logs and debugging. Make it human-readable:",[1726,4451,4453],{"className":1728,"code":4452,"language":1730,"meta":34,"style":34},"// Good: describes what happened\ncapitan.NewSignal(\"order.created\", \"New order placed by customer\")\ncapitan.NewSignal(\"payment.failed\", \"Payment processing failed\")\n\n// Bad: repeats the name\ncapitan.NewSignal(\"order.created\", \"order.created\")\ncapitan.NewSignal(\"payment.failed\", \"PaymentFailed\")\n",[1732,4454,4455,4460,4479,4499,4503,4508,4527],{"__ignoreMap":34},[1735,4456,4457],{"class":1737,"line":9},[1735,4458,4459],{"class":1740},"// Good: describes what happened\n",[1735,4461,4462,4464,4466,4468,4470,4472,4474,4477],{"class":1737,"line":19},[1735,4463,1642],{"class":1746},[1735,4465,1757],{"class":1756},[1735,4467,1050],{"class":1760},[1735,4469,1763],{"class":1756},[1735,4471,1822],{"class":1766},[1735,4473,1825],{"class":1756},[1735,4475,4476],{"class":1766}," \"New order placed by customer\"",[1735,4478,1770],{"class":1756},[1735,4480,4481,4483,4485,4487,4489,4492,4494,4497],{"class":1737,"line":40},[1735,4482,1642],{"class":1746},[1735,4484,1757],{"class":1756},[1735,4486,1050],{"class":1760},[1735,4488,1763],{"class":1756},[1735,4490,4491],{"class":1766},"\"payment.failed\"",[1735,4493,1825],{"class":1756},[1735,4495,4496],{"class":1766}," \"Payment processing failed\"",[1735,4498,1770],{"class":1756},[1735,4500,4501],{"class":1737,"line":988},[1735,4502,1797],{"emptyLinePlaceholder":1796},[1735,4504,4505],{"class":1737,"line":1800},[1735,4506,4507],{"class":1740},"// Bad: repeats the name\n",[1735,4509,4510,4512,4514,4516,4518,4520,4522,4525],{"class":1737,"line":1806},[1735,4511,1642],{"class":1746},[1735,4513,1757],{"class":1756},[1735,4515,1050],{"class":1760},[1735,4517,1763],{"class":1756},[1735,4519,1822],{"class":1766},[1735,4521,1825],{"class":1756},[1735,4523,4524],{"class":1766}," \"order.created\"",[1735,4526,1770],{"class":1756},[1735,4528,4529,4531,4533,4535,4537,4539,4541,4544],{"class":1737,"line":1833},[1735,4530,1642],{"class":1746},[1735,4532,1757],{"class":1756},[1735,4534,1050],{"class":1760},[1735,4536,1763],{"class":1756},[1735,4538,4491],{"class":1766},[1735,4540,1825],{"class":1756},[1735,4542,4543],{"class":1766}," \"PaymentFailed\"",[1735,4545,1770],{"class":1756},[1718,4547,645],{"id":4548},"key-design",[2850,4550,649],{"id":4551},"define-keys-alongside-signals",[1644,4553,4554],{},"Keep keys with their signals for discoverability:",[1726,4556,4558],{"className":1728,"code":4557,"language":1730,"meta":34,"style":34},"// orders/signals.go\npackage orders\n\nvar (\n    OrderCreated = capitan.NewSignal(\"order.created\", \"New order placed\")\n    // ... other signals\n)\n\nvar (\n    OrderID    = capitan.NewStringKey(\"order_id\")\n    CustomerID = capitan.NewStringKey(\"customer_id\")\n    Total      = capitan.NewFloat64Key(\"total\")\n)\n",[1732,4559,4560,4565,4571,4575,4581,4603,4608,4612,4616,4622,4642,4662,4681],{"__ignoreMap":34},[1735,4561,4562],{"class":1737,"line":9},[1735,4563,4564],{"class":1740},"// orders/signals.go\n",[1735,4566,4567,4569],{"class":1737,"line":19},[1735,4568,2174],{"class":1934},[1735,4570,3969],{"class":1943},[1735,4572,4573],{"class":1737,"line":40},[1735,4574,1797],{"emptyLinePlaceholder":1796},[1735,4576,4577,4579],{"class":1737,"line":988},[1735,4578,2223],{"class":1934},[1735,4580,2190],{"class":1756},[1735,4582,4583,4585,4587,4589,4591,4593,4595,4597,4599,4601],{"class":1737,"line":1800},[1735,4584,3995],{"class":1746},[1735,4586,2233],{"class":1746},[1735,4588,1753],{"class":1746},[1735,4590,1757],{"class":1756},[1735,4592,1050],{"class":1760},[1735,4594,1763],{"class":1756},[1735,4596,1822],{"class":1766},[1735,4598,1825],{"class":1756},[1735,4600,1828],{"class":1766},[1735,4602,1770],{"class":1756},[1735,4604,4605],{"class":1737,"line":1806},[1735,4606,4607],{"class":1740},"    // ... other signals\n",[1735,4609,4610],{"class":1737,"line":1833},[1735,4611,1770],{"class":1756},[1735,4613,4614],{"class":1737,"line":1838},[1735,4615,1797],{"emptyLinePlaceholder":1796},[1735,4617,4618,4620],{"class":1737,"line":1844},[1735,4619,2223],{"class":1934},[1735,4621,2190],{"class":1756},[1735,4623,4624,4627,4630,4632,4634,4636,4638,4640],{"class":1737,"line":1866},[1735,4625,4626],{"class":1746},"    OrderID",[1735,4628,4629],{"class":1746},"    =",[1735,4631,1753],{"class":1746},[1735,4633,1757],{"class":1756},[1735,4635,1214],{"class":1760},[1735,4637,1763],{"class":1756},[1735,4639,1767],{"class":1766},[1735,4641,1770],{"class":1756},[1735,4643,4644,4647,4649,4651,4653,4655,4657,4660],{"class":1737,"line":1884},[1735,4645,4646],{"class":1746},"    CustomerID",[1735,4648,2233],{"class":1746},[1735,4650,1753],{"class":1746},[1735,4652,1757],{"class":1756},[1735,4654,1214],{"class":1760},[1735,4656,1763],{"class":1756},[1735,4658,4659],{"class":1766},"\"customer_id\"",[1735,4661,1770],{"class":1756},[1735,4663,4664,4667,4669,4671,4673,4675,4677,4679],{"class":1737,"line":1902},[1735,4665,4666],{"class":1746},"    Total",[1735,4668,2256],{"class":1746},[1735,4670,1753],{"class":1746},[1735,4672,1757],{"class":1756},[1735,4674,1254],{"class":1760},[1735,4676,1763],{"class":1756},[1735,4678,1789],{"class":1766},[1735,4680,1770],{"class":1756},[1735,4682,4683],{"class":1737,"line":2271},[1735,4684,1770],{"class":1756},[2850,4686,654],{"id":4687},"use-consistent-key-names",[1644,4689,4690],{},"Establish naming conventions across your codebase:",[1726,4692,4694],{"className":1728,"code":4693,"language":1730,"meta":34,"style":34},"// Consistent: snake_case\norderID := capitan.NewStringKey(\"order_id\")\ncustomerID := capitan.NewStringKey(\"customer_id\")\ncreatedAt := capitan.NewTimeKey(\"created_at\")\n\n// Inconsistent: mixed styles\norderID := capitan.NewStringKey(\"orderId\")\ncustomer_id := capitan.NewStringKey(\"customer-id\")\n",[1732,4695,4696,4701,4719,4738,4758,4762,4767,4786],{"__ignoreMap":34},[1735,4697,4698],{"class":1737,"line":9},[1735,4699,4700],{"class":1740},"// Consistent: snake_case\n",[1735,4702,4703,4705,4707,4709,4711,4713,4715,4717],{"class":1737,"line":19},[1735,4704,1747],{"class":1746},[1735,4706,1750],{"class":1746},[1735,4708,1753],{"class":1746},[1735,4710,1757],{"class":1756},[1735,4712,1214],{"class":1760},[1735,4714,1763],{"class":1756},[1735,4716,1767],{"class":1766},[1735,4718,1770],{"class":1756},[1735,4720,4721,4724,4726,4728,4730,4732,4734,4736],{"class":1737,"line":40},[1735,4722,4723],{"class":1746},"customerID",[1735,4725,1750],{"class":1746},[1735,4727,1753],{"class":1746},[1735,4729,1757],{"class":1756},[1735,4731,1214],{"class":1760},[1735,4733,1763],{"class":1756},[1735,4735,4659],{"class":1766},[1735,4737,1770],{"class":1756},[1735,4739,4740,4743,4745,4747,4749,4751,4753,4756],{"class":1737,"line":988},[1735,4741,4742],{"class":1746},"createdAt",[1735,4744,1750],{"class":1746},[1735,4746,1753],{"class":1746},[1735,4748,1757],{"class":1756},[1735,4750,1264],{"class":1760},[1735,4752,1763],{"class":1756},[1735,4754,4755],{"class":1766},"\"created_at\"",[1735,4757,1770],{"class":1756},[1735,4759,4760],{"class":1737,"line":1800},[1735,4761,1797],{"emptyLinePlaceholder":1796},[1735,4763,4764],{"class":1737,"line":1806},[1735,4765,4766],{"class":1740},"// Inconsistent: mixed styles\n",[1735,4768,4769,4771,4773,4775,4777,4779,4781,4784],{"class":1737,"line":1833},[1735,4770,1747],{"class":1746},[1735,4772,1750],{"class":1746},[1735,4774,1753],{"class":1746},[1735,4776,1757],{"class":1756},[1735,4778,1214],{"class":1760},[1735,4780,1763],{"class":1756},[1735,4782,4783],{"class":1766},"\"orderId\"",[1735,4785,1770],{"class":1756},[1735,4787,4788,4791,4793,4795,4797,4799,4801,4804],{"class":1737,"line":1838},[1735,4789,4790],{"class":1746},"customer_id",[1735,4792,1750],{"class":1746},[1735,4794,1753],{"class":1746},[1735,4796,1757],{"class":1756},[1735,4798,1214],{"class":1760},[1735,4800,1763],{"class":1756},[1735,4802,4803],{"class":1766},"\"customer-id\"",[1735,4805,1770],{"class":1756},[2850,4807,659],{"id":4808},"prefer-specific-types",[1644,4810,4811],{},"Use the most specific key type:",[1726,4813,4815],{"className":1728,"code":4814,"language":1730,"meta":34,"style":34},"// Good: typed keys\ncount := capitan.NewIntKey(\"count\")\ntotal := capitan.NewFloat64Key(\"total\")\nactive := capitan.NewBoolKey(\"active\")\ncreatedAt := capitan.NewTimeKey(\"created_at\")\n\n// Avoid: stringly-typed\ncount := capitan.NewStringKey(\"count\")  // \"42\" instead of 42\n",[1732,4816,4817,4822,4842,4860,4880,4898,4902,4907],{"__ignoreMap":34},[1735,4818,4819],{"class":1737,"line":9},[1735,4820,4821],{"class":1740},"// Good: typed keys\n",[1735,4823,4824,4827,4829,4831,4833,4835,4837,4840],{"class":1737,"line":19},[1735,4825,4826],{"class":1746},"count",[1735,4828,1750],{"class":1746},[1735,4830,1753],{"class":1746},[1735,4832,1757],{"class":1756},[1735,4834,1219],{"class":1760},[1735,4836,1763],{"class":1756},[1735,4838,4839],{"class":1766},"\"count\"",[1735,4841,1770],{"class":1756},[1735,4843,4844,4846,4848,4850,4852,4854,4856,4858],{"class":1737,"line":40},[1735,4845,1775],{"class":1746},[1735,4847,1750],{"class":1746},[1735,4849,1753],{"class":1746},[1735,4851,1757],{"class":1756},[1735,4853,1254],{"class":1760},[1735,4855,1763],{"class":1756},[1735,4857,1789],{"class":1766},[1735,4859,1770],{"class":1756},[1735,4861,4862,4865,4867,4869,4871,4873,4875,4878],{"class":1737,"line":988},[1735,4863,4864],{"class":1746},"active",[1735,4866,1750],{"class":1746},[1735,4868,1753],{"class":1746},[1735,4870,1757],{"class":1756},[1735,4872,1259],{"class":1760},[1735,4874,1763],{"class":1756},[1735,4876,4877],{"class":1766},"\"active\"",[1735,4879,1770],{"class":1756},[1735,4881,4882,4884,4886,4888,4890,4892,4894,4896],{"class":1737,"line":1800},[1735,4883,4742],{"class":1746},[1735,4885,1750],{"class":1746},[1735,4887,1753],{"class":1746},[1735,4889,1757],{"class":1756},[1735,4891,1264],{"class":1760},[1735,4893,1763],{"class":1756},[1735,4895,4755],{"class":1766},[1735,4897,1770],{"class":1756},[1735,4899,4900],{"class":1737,"line":1806},[1735,4901,1797],{"emptyLinePlaceholder":1796},[1735,4903,4904],{"class":1737,"line":1833},[1735,4905,4906],{"class":1740},"// Avoid: stringly-typed\n",[1735,4908,4909,4911,4913,4915,4917,4919,4921,4923,4925],{"class":1737,"line":1838},[1735,4910,4826],{"class":1746},[1735,4912,1750],{"class":1746},[1735,4914,1753],{"class":1746},[1735,4916,1757],{"class":1756},[1735,4918,1214],{"class":1760},[1735,4920,1763],{"class":1756},[1735,4922,4839],{"class":1766},[1735,4924,1966],{"class":1756},[1735,4926,4927],{"class":1740},"  // \"42\" instead of 42\n",[1718,4929,664],{"id":4930},"lifecycle-management",[2850,4932,668],{"id":4933},"close-listeners-on-shutdown",[1644,4935,4936],{},"In long-running services, close listeners during graceful shutdown:",[1726,4938,4940],{"className":1728,"code":4939,"language":1730,"meta":34,"style":34},"func main() {\n    // Register listeners\n    orderListener := capitan.Hook(orderCreated, handleOrder)\n    paymentListener := capitan.Hook(paymentProcessed, handlePayment)\n    observer := capitan.Observe(logEvent)\n\n    // Setup shutdown handling\n    stop := make(chan os.Signal, 1)\n    signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)\n\n    // Run server...\n    \u003C-stop\n\n    // Close listeners (stops new event delivery)\n    orderListener.Close()\n    paymentListener.Close()\n    observer.Close()\n\n    // Drain pending events\n    capitan.Shutdown()\n}\n",[1732,4941,4942,4952,4957,4981,5006,5026,5030,5035,5065,5101,5105,5110,5118,5122,5127,5137,5147,5157,5161,5166,5176],{"__ignoreMap":34},[1735,4943,4944,4946,4948,4950],{"class":1737,"line":9},[1735,4945,2069],{"class":1934},[1735,4947,2306],{"class":1760},[1735,4949,2309],{"class":1756},[1735,4951,1969],{"class":1756},[1735,4953,4954],{"class":1737,"line":19},[1735,4955,4956],{"class":1740},"    // Register listeners\n",[1735,4958,4959,4962,4964,4966,4968,4970,4972,4974,4976,4979],{"class":1737,"line":40},[1735,4960,4961],{"class":1746},"    orderListener",[1735,4963,1750],{"class":1746},[1735,4965,1753],{"class":1746},[1735,4967,1757],{"class":1756},[1735,4969,932],{"class":1760},[1735,4971,1763],{"class":1756},[1735,4973,1809],{"class":1746},[1735,4975,1825],{"class":1756},[1735,4977,4978],{"class":1746}," handleOrder",[1735,4980,1770],{"class":1756},[1735,4982,4983,4986,4988,4990,4992,4994,4996,4999,5001,5004],{"class":1737,"line":988},[1735,4984,4985],{"class":1746},"    paymentListener",[1735,4987,1750],{"class":1746},[1735,4989,1753],{"class":1746},[1735,4991,1757],{"class":1756},[1735,4993,932],{"class":1760},[1735,4995,1763],{"class":1756},[1735,4997,4998],{"class":1746},"paymentProcessed",[1735,5000,1825],{"class":1756},[1735,5002,5003],{"class":1746}," handlePayment",[1735,5005,1770],{"class":1756},[1735,5007,5008,5011,5013,5015,5017,5019,5021,5024],{"class":1737,"line":1800},[1735,5009,5010],{"class":1746},"    observer",[1735,5012,1750],{"class":1746},[1735,5014,1753],{"class":1746},[1735,5016,1757],{"class":1756},[1735,5018,942],{"class":1760},[1735,5020,1763],{"class":1756},[1735,5022,5023],{"class":1746},"logEvent",[1735,5025,1770],{"class":1756},[1735,5027,5028],{"class":1737,"line":1806},[1735,5029,1797],{"emptyLinePlaceholder":1796},[1735,5031,5032],{"class":1737,"line":1833},[1735,5033,5034],{"class":1740},"    // Setup shutdown handling\n",[1735,5036,5037,5040,5042,5046,5048,5051,5054,5056,5058,5060,5063],{"class":1737,"line":1838},[1735,5038,5039],{"class":1746},"    stop",[1735,5041,1750],{"class":1746},[1735,5043,5045],{"class":5044},"skxcq"," make",[1735,5047,1763],{"class":1756},[1735,5049,5050],{"class":1934},"chan",[1735,5052,5053],{"class":1943}," os",[1735,5055,1757],{"class":1756},[1735,5057,1045],{"class":1943},[1735,5059,1825],{"class":1756},[1735,5061,5062],{"class":1896}," 1",[1735,5064,1770],{"class":1756},[1735,5066,5067,5070,5072,5075,5077,5080,5082,5085,5087,5090,5092,5094,5096,5099],{"class":1737,"line":1844},[1735,5068,5069],{"class":1746},"    signal",[1735,5071,1757],{"class":1756},[1735,5073,5074],{"class":1760},"Notify",[1735,5076,1763],{"class":1756},[1735,5078,5079],{"class":1746},"stop",[1735,5081,1825],{"class":1756},[1735,5083,5084],{"class":1746}," syscall",[1735,5086,1757],{"class":1756},[1735,5088,5089],{"class":1746},"SIGINT",[1735,5091,1825],{"class":1756},[1735,5093,5084],{"class":1746},[1735,5095,1757],{"class":1756},[1735,5097,5098],{"class":1746},"SIGTERM",[1735,5100,1770],{"class":1756},[1735,5102,5103],{"class":1737,"line":1866},[1735,5104,1797],{"emptyLinePlaceholder":1796},[1735,5106,5107],{"class":1737,"line":1884},[1735,5108,5109],{"class":1740},"    // Run server...\n",[1735,5111,5112,5115],{"class":1737,"line":1902},[1735,5113,5114],{"class":1956},"    \u003C-",[1735,5116,5117],{"class":1746},"stop\n",[1735,5119,5120],{"class":1737,"line":2271},[1735,5121,1797],{"emptyLinePlaceholder":1796},[1735,5123,5124],{"class":1737,"line":2291},[1735,5125,5126],{"class":1740},"    // Close listeners (stops new event delivery)\n",[1735,5128,5129,5131,5133,5135],{"class":1737,"line":2296},[1735,5130,4961],{"class":1746},[1735,5132,1757],{"class":1756},[1735,5134,1123],{"class":1760},[1735,5136,2558],{"class":1756},[1735,5138,5139,5141,5143,5145],{"class":1737,"line":2301},[1735,5140,4985],{"class":1746},[1735,5142,1757],{"class":1756},[1735,5144,1123],{"class":1760},[1735,5146,2558],{"class":1756},[1735,5148,5149,5151,5153,5155],{"class":1737,"line":2314},[1735,5150,5010],{"class":1746},[1735,5152,1757],{"class":1756},[1735,5154,1123],{"class":1760},[1735,5156,2558],{"class":1756},[1735,5158,5159],{"class":1737,"line":2320},[1735,5160,1797],{"emptyLinePlaceholder":1796},[1735,5162,5163],{"class":1737,"line":2364},[1735,5164,5165],{"class":1740},"    // Drain pending events\n",[1735,5167,5168,5170,5172,5174],{"class":1737,"line":2388},[1735,5169,2323],{"class":1746},[1735,5171,1757],{"class":1756},[1735,5173,962],{"class":1760},[1735,5175,2558],{"class":1756},[1735,5177,5178],{"class":1737,"line":2412},[1735,5179,2564],{"class":1756},[2850,5181,673],{"id":5182},"scope-listeners-to-components",[1644,5184,5185],{},"Close listeners when their owning component stops:",[1726,5187,5189],{"className":1728,"code":5188,"language":1730,"meta":34,"style":34},"type OrderProcessor struct {\n    listener *capitan.Listener\n}\n\nfunc NewOrderProcessor() *OrderProcessor {\n    p := &OrderProcessor{}\n    p.listener = capitan.Hook(orderCreated, p.handle)\n    return p\n}\n\nfunc (p *OrderProcessor) handle(ctx context.Context, e *capitan.Event) {\n    // Process order...\n}\n\nfunc (p *OrderProcessor) Stop() {\n    p.listener.Close()\n}\n",[1732,5190,5191,5204,5219,5223,5227,5243,5258,5291,5299,5303,5307,5353,5358,5362,5366,5387,5401],{"__ignoreMap":34},[1735,5192,5193,5196,5199,5202],{"class":1737,"line":9},[1735,5194,5195],{"class":1934},"type",[1735,5197,5198],{"class":1943}," OrderProcessor",[1735,5200,5201],{"class":1934}," struct",[1735,5203,1969],{"class":1756},[1735,5205,5206,5210,5212,5214,5216],{"class":1737,"line":19},[1735,5207,5209],{"class":5208},"sBGCq","    listener",[1735,5211,1957],{"class":1956},[1735,5213,1642],{"class":1943},[1735,5215,1757],{"class":1756},[1735,5217,5218],{"class":1943},"Listener\n",[1735,5220,5221],{"class":1737,"line":40},[1735,5222,2564],{"class":1756},[1735,5224,5225],{"class":1737,"line":988},[1735,5226,1797],{"emptyLinePlaceholder":1796},[1735,5228,5229,5231,5234,5236,5238,5241],{"class":1737,"line":1800},[1735,5230,2069],{"class":1934},[1735,5232,5233],{"class":1760}," NewOrderProcessor",[1735,5235,2309],{"class":1756},[1735,5237,1957],{"class":1956},[1735,5239,5240],{"class":1943},"OrderProcessor",[1735,5242,1969],{"class":1756},[1735,5244,5245,5248,5250,5253,5255],{"class":1737,"line":1806},[1735,5246,5247],{"class":1746},"    p",[1735,5249,1750],{"class":1746},[1735,5251,5252],{"class":1956}," &",[1735,5254,5240],{"class":1943},[1735,5256,5257],{"class":1756},"{}\n",[1735,5259,5260,5262,5264,5267,5269,5271,5273,5275,5277,5279,5281,5284,5286,5289],{"class":1737,"line":1833},[1735,5261,5247],{"class":1746},[1735,5263,1757],{"class":1756},[1735,5265,5266],{"class":1746},"listener",[1735,5268,2233],{"class":1746},[1735,5270,1753],{"class":1746},[1735,5272,1757],{"class":1756},[1735,5274,932],{"class":1760},[1735,5276,1763],{"class":1756},[1735,5278,1809],{"class":1746},[1735,5280,1825],{"class":1756},[1735,5282,5283],{"class":1746}," p",[1735,5285,1757],{"class":1756},[1735,5287,5288],{"class":1746},"handle",[1735,5290,1770],{"class":1756},[1735,5292,5293,5296],{"class":1737,"line":1838},[1735,5294,5295],{"class":1956},"    return",[1735,5297,5298],{"class":1746}," p\n",[1735,5300,5301],{"class":1737,"line":1844},[1735,5302,2564],{"class":1756},[1735,5304,5305],{"class":1737,"line":1866},[1735,5306,1797],{"emptyLinePlaceholder":1796},[1735,5308,5309,5311,5314,5317,5320,5322,5324,5327,5329,5331,5333,5335,5337,5339,5341,5343,5345,5347,5349,5351],{"class":1737,"line":1884},[1735,5310,2069],{"class":1934},[1735,5312,5313],{"class":1756}," (",[1735,5315,5316],{"class":1940},"p ",[1735,5318,5319],{"class":1956},"*",[1735,5321,5240],{"class":1943},[1735,5323,1966],{"class":1756},[1735,5325,5326],{"class":1760}," handle",[1735,5328,1763],{"class":1756},[1735,5330,1855],{"class":1940},[1735,5332,1944],{"class":1943},[1735,5334,1757],{"class":1756},[1735,5336,427],{"class":1943},[1735,5338,1825],{"class":1756},[1735,5340,1953],{"class":1940},[1735,5342,1957],{"class":1956},[1735,5344,1642],{"class":1943},[1735,5346,1757],{"class":1756},[1735,5348,1068],{"class":1943},[1735,5350,1966],{"class":1756},[1735,5352,1969],{"class":1756},[1735,5354,5355],{"class":1737,"line":1902},[1735,5356,5357],{"class":1740},"    // Process order...\n",[1735,5359,5360],{"class":1737,"line":2271},[1735,5361,2564],{"class":1756},[1735,5363,5364],{"class":1737,"line":2291},[1735,5365,1797],{"emptyLinePlaceholder":1796},[1735,5367,5368,5370,5372,5374,5376,5378,5380,5383,5385],{"class":1737,"line":2296},[1735,5369,2069],{"class":1934},[1735,5371,5313],{"class":1756},[1735,5373,5316],{"class":1940},[1735,5375,5319],{"class":1956},[1735,5377,5240],{"class":1943},[1735,5379,1966],{"class":1756},[1735,5381,5382],{"class":1760}," Stop",[1735,5384,2309],{"class":1756},[1735,5386,1969],{"class":1756},[1735,5388,5389,5391,5393,5395,5397,5399],{"class":1737,"line":2301},[1735,5390,5247],{"class":1746},[1735,5392,1757],{"class":1756},[1735,5394,5266],{"class":1746},[1735,5396,1757],{"class":1756},[1735,5398,1123],{"class":1760},[1735,5400,2558],{"class":1756},[1735,5402,5403],{"class":1737,"line":2314},[1735,5404,2564],{"class":1756},[2850,5406,678],{"id":5407},"use-isolated-instances-for-modules",[1644,5409,5410],{},"Separate subsystems can use separate instances:",[1726,5412,5414],{"className":1728,"code":5413,"language":1730,"meta":34,"style":34},"// Billing module\nvar billingCapitan = capitan.New(capitan.WithBufferSize(64))\n\n// Analytics module\nvar analyticsCapitan = capitan.New(capitan.WithBufferSize(256))\n\nfunc ShutdownAll() {\n    billingCapitan.Shutdown()\n    analyticsCapitan.Shutdown()\n}\n",[1732,5415,5416,5421,5451,5455,5460,5490,5494,5505,5516,5527],{"__ignoreMap":34},[1735,5417,5418],{"class":1737,"line":9},[1735,5419,5420],{"class":1740},"// Billing module\n",[1735,5422,5423,5425,5428,5430,5432,5434,5436,5438,5440,5442,5444,5446,5449],{"class":1737,"line":19},[1735,5424,2223],{"class":1934},[1735,5426,5427],{"class":1746}," billingCapitan",[1735,5429,2233],{"class":1746},[1735,5431,1753],{"class":1746},[1735,5433,1757],{"class":1756},[1735,5435,977],{"class":1760},[1735,5437,1763],{"class":1756},[1735,5439,1642],{"class":1746},[1735,5441,1757],{"class":1756},[1735,5443,1149],{"class":1760},[1735,5445,1763],{"class":1756},[1735,5447,5448],{"class":1896},"64",[1735,5450,2782],{"class":1756},[1735,5452,5453],{"class":1737,"line":40},[1735,5454,1797],{"emptyLinePlaceholder":1796},[1735,5456,5457],{"class":1737,"line":988},[1735,5458,5459],{"class":1740},"// Analytics module\n",[1735,5461,5462,5464,5467,5469,5471,5473,5475,5477,5479,5481,5483,5485,5488],{"class":1737,"line":1800},[1735,5463,2223],{"class":1934},[1735,5465,5466],{"class":1746}," analyticsCapitan",[1735,5468,2233],{"class":1746},[1735,5470,1753],{"class":1746},[1735,5472,1757],{"class":1756},[1735,5474,977],{"class":1760},[1735,5476,1763],{"class":1756},[1735,5478,1642],{"class":1746},[1735,5480,1757],{"class":1756},[1735,5482,1149],{"class":1760},[1735,5484,1763],{"class":1756},[1735,5486,5487],{"class":1896},"256",[1735,5489,2782],{"class":1756},[1735,5491,5492],{"class":1737,"line":1806},[1735,5493,1797],{"emptyLinePlaceholder":1796},[1735,5495,5496,5498,5501,5503],{"class":1737,"line":1833},[1735,5497,2069],{"class":1934},[1735,5499,5500],{"class":1760}," ShutdownAll",[1735,5502,2309],{"class":1756},[1735,5504,1969],{"class":1756},[1735,5506,5507,5510,5512,5514],{"class":1737,"line":1838},[1735,5508,5509],{"class":1746},"    billingCapitan",[1735,5511,1757],{"class":1756},[1735,5513,962],{"class":1760},[1735,5515,2558],{"class":1756},[1735,5517,5518,5521,5523,5525],{"class":1737,"line":1844},[1735,5519,5520],{"class":1746},"    analyticsCapitan",[1735,5522,1757],{"class":1756},[1735,5524,962],{"class":1760},[1735,5526,2558],{"class":1756},[1735,5528,5529],{"class":1737,"line":1866},[1735,5530,2564],{"class":1756},[1718,5532,683],{"id":5533},"error-handling",[2850,5535,687],{"id":5536},"always-check-field-extraction",[1644,5538,5539],{},"Fields may be missing or have wrong types:",[1726,5541,5543],{"className":1728,"code":5542,"language":1730,"meta":34,"style":34},"capitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {\n    orderID, ok := orderIDKey.From(e)\n    if !ok {\n        log.Printf(\"Missing order_id in %s event\", e.Signal().Name())\n        return\n    }\n    // Proceed with orderID...\n})\n",[1732,5544,5545,5587,5611,5624,5657,5662,5667,5672],{"__ignoreMap":34},[1735,5546,5547,5549,5551,5553,5555,5557,5559,5561,5563,5565,5567,5569,5571,5573,5575,5577,5579,5581,5583,5585],{"class":1737,"line":9},[1735,5548,1642],{"class":1746},[1735,5550,1757],{"class":1756},[1735,5552,932],{"class":1760},[1735,5554,1763],{"class":1756},[1735,5556,1809],{"class":1746},[1735,5558,1825],{"class":1756},[1735,5560,1935],{"class":1934},[1735,5562,1763],{"class":1756},[1735,5564,1855],{"class":1940},[1735,5566,1944],{"class":1943},[1735,5568,1757],{"class":1756},[1735,5570,427],{"class":1943},[1735,5572,1825],{"class":1756},[1735,5574,1953],{"class":1940},[1735,5576,1957],{"class":1956},[1735,5578,1642],{"class":1943},[1735,5580,1757],{"class":1756},[1735,5582,1068],{"class":1943},[1735,5584,1966],{"class":1756},[1735,5586,1969],{"class":1756},[1735,5588,5589,5591,5593,5596,5598,5601,5603,5605,5607,5609],{"class":1737,"line":19},[1735,5590,1869],{"class":1746},[1735,5592,1825],{"class":1756},[1735,5594,5595],{"class":1746}," ok",[1735,5597,1750],{"class":1746},[1735,5599,5600],{"class":1746}," orderIDKey",[1735,5602,1757],{"class":1756},[1735,5604,1308],{"class":1760},[1735,5606,1763],{"class":1756},[1735,5608,1993],{"class":1746},[1735,5610,1770],{"class":1756},[1735,5612,5613,5616,5619,5622],{"class":1737,"line":40},[1735,5614,5615],{"class":1956},"    if",[1735,5617,5618],{"class":1956}," !",[1735,5620,5621],{"class":1746},"ok",[1735,5623,1969],{"class":1756},[1735,5625,5626,5629,5631,5633,5635,5638,5640,5643,5645,5647,5649,5651,5653,5655],{"class":1737,"line":988},[1735,5627,5628],{"class":1746},"        log",[1735,5630,1757],{"class":1756},[1735,5632,2420],{"class":1760},[1735,5634,1763],{"class":1756},[1735,5636,5637],{"class":1766},"\"Missing order_id in ",[1735,5639,2429],{"class":2428},[1735,5641,5642],{"class":1766}," event\"",[1735,5644,1825],{"class":1756},[1735,5646,1953],{"class":1746},[1735,5648,1757],{"class":1756},[1735,5650,1045],{"class":1760},[1735,5652,2125],{"class":1756},[1735,5654,1058],{"class":1760},[1735,5656,2130],{"class":1756},[1735,5658,5659],{"class":1737,"line":1800},[1735,5660,5661],{"class":1956},"        return\n",[1735,5663,5664],{"class":1737,"line":1806},[1735,5665,5666],{"class":1756},"    }\n",[1735,5668,5669],{"class":1737,"line":1833},[1735,5670,5671],{"class":1740},"    // Proceed with orderID...\n",[1735,5673,5674],{"class":1737,"line":1838},[1735,5675,2047],{"class":1756},[2850,5677,692],{"id":5678},"use-severity-appropriately",[1644,5680,5681],{},"Reserve error severity for actual errors:",[1726,5683,5685],{"className":1728,"code":5684,"language":1730,"meta":34,"style":34},"// Info: normal operations\ncapitan.Emit(ctx, orderCreated, fields...)\n\n// Warn: unusual but handled\ncapitan.Warn(ctx, lowStock, fields...)\n\n// Error: failures requiring attention\ncapitan.Error(ctx, paymentFailed, fields...)\n\n// Debug: development only\ncapitan.Debug(ctx, queryExecuted, fields...)\n",[1732,5686,5687,5692,5716,5720,5725,5750,5754,5759,5784,5788,5793],{"__ignoreMap":34},[1735,5688,5689],{"class":1737,"line":9},[1735,5690,5691],{"class":1740},"// Info: normal operations\n",[1735,5693,5694,5696,5698,5700,5702,5704,5706,5708,5710,5712,5714],{"class":1737,"line":19},[1735,5695,1642],{"class":1746},[1735,5697,1757],{"class":1756},[1735,5699,903],{"class":1760},[1735,5701,1763],{"class":1756},[1735,5703,1855],{"class":1746},[1735,5705,1825],{"class":1756},[1735,5707,1860],{"class":1746},[1735,5709,1825],{"class":1756},[1735,5711,4303],{"class":1746},[1735,5713,4306],{"class":1956},[1735,5715,1770],{"class":1756},[1735,5717,5718],{"class":1737,"line":40},[1735,5719,1797],{"emptyLinePlaceholder":1796},[1735,5721,5722],{"class":1737,"line":988},[1735,5723,5724],{"class":1740},"// Warn: unusual but handled\n",[1735,5726,5727,5729,5731,5733,5735,5737,5739,5742,5744,5746,5748],{"class":1737,"line":1800},[1735,5728,1642],{"class":1746},[1735,5730,1757],{"class":1756},[1735,5732,918],{"class":1760},[1735,5734,1763],{"class":1756},[1735,5736,1855],{"class":1746},[1735,5738,1825],{"class":1756},[1735,5740,5741],{"class":1746}," lowStock",[1735,5743,1825],{"class":1756},[1735,5745,4303],{"class":1746},[1735,5747,4306],{"class":1956},[1735,5749,1770],{"class":1756},[1735,5751,5752],{"class":1737,"line":1806},[1735,5753,1797],{"emptyLinePlaceholder":1796},[1735,5755,5756],{"class":1737,"line":1833},[1735,5757,5758],{"class":1740},"// Error: failures requiring attention\n",[1735,5760,5761,5763,5765,5767,5769,5771,5773,5776,5778,5780,5782],{"class":1737,"line":1838},[1735,5762,1642],{"class":1746},[1735,5764,1757],{"class":1756},[1735,5766,923],{"class":1760},[1735,5768,1763],{"class":1756},[1735,5770,1855],{"class":1746},[1735,5772,1825],{"class":1756},[1735,5774,5775],{"class":1746}," paymentFailed",[1735,5777,1825],{"class":1756},[1735,5779,4303],{"class":1746},[1735,5781,4306],{"class":1956},[1735,5783,1770],{"class":1756},[1735,5785,5786],{"class":1737,"line":1844},[1735,5787,1797],{"emptyLinePlaceholder":1796},[1735,5789,5790],{"class":1737,"line":1866},[1735,5791,5792],{"class":1740},"// Debug: development only\n",[1735,5794,5795,5797,5799,5801,5803,5805,5807,5810,5812,5814,5816],{"class":1737,"line":1884},[1735,5796,1642],{"class":1746},[1735,5798,1757],{"class":1756},[1735,5800,908],{"class":1760},[1735,5802,1763],{"class":1756},[1735,5804,1855],{"class":1746},[1735,5806,1825],{"class":1756},[1735,5808,5809],{"class":1746}," queryExecuted",[1735,5811,1825],{"class":1756},[1735,5813,4303],{"class":1746},[1735,5815,4306],{"class":1956},[1735,5817,1770],{"class":1756},[2850,5819,697],{"id":5820},"configure-panic-handlers-in-production",[1644,5822,5823],{},"Never run production without panic visibility:",[1726,5825,5827],{"className":1728,"code":5826,"language":1730,"meta":34,"style":34},"capitan.Configure(\n    capitan.WithPanicHandler(func(sig capitan.Signal, recovered any) {\n        log.Printf(\"PANIC in %s: %v\", sig.Name(), recovered)\n        metrics.Increment(\"capitan_panics_total\", \"signal\", sig.Name())\n\n        // Optional: alert on-call\n        if shouldAlert(sig) {\n            alerting.Page(\"Listener panic\", sig.Name(), recovered)\n        }\n    }),\n)\n",[1732,5828,5829,5840,5875,5913,5942,5946,5951,5967,5996,6001,6006],{"__ignoreMap":34},[1735,5830,5831,5833,5835,5837],{"class":1737,"line":9},[1735,5832,1642],{"class":1746},[1735,5834,1757],{"class":1756},[1735,5836,952],{"class":1760},[1735,5838,5839],{"class":1756},"(\n",[1735,5841,5842,5844,5846,5848,5850,5852,5854,5857,5859,5861,5863,5865,5868,5871,5873],{"class":1737,"line":19},[1735,5843,2323],{"class":1746},[1735,5845,1757],{"class":1756},[1735,5847,1154],{"class":1760},[1735,5849,1763],{"class":1756},[1735,5851,2069],{"class":1934},[1735,5853,1763],{"class":1756},[1735,5855,5856],{"class":1940},"sig",[1735,5858,1753],{"class":1943},[1735,5860,1757],{"class":1756},[1735,5862,1045],{"class":1943},[1735,5864,1825],{"class":1756},[1735,5866,5867],{"class":1940}," recovered",[1735,5869,5870],{"class":1943}," any",[1735,5872,1966],{"class":1756},[1735,5874,1969],{"class":1756},[1735,5876,5877,5879,5881,5883,5885,5888,5890,5893,5896,5898,5900,5903,5905,5907,5909,5911],{"class":1737,"line":40},[1735,5878,5628],{"class":1746},[1735,5880,1757],{"class":1756},[1735,5882,2420],{"class":1760},[1735,5884,1763],{"class":1756},[1735,5886,5887],{"class":1766},"\"PANIC in ",[1735,5889,2429],{"class":2428},[1735,5891,5892],{"class":1766},": ",[1735,5894,5895],{"class":2428},"%v",[1735,5897,2442],{"class":1766},[1735,5899,1825],{"class":1756},[1735,5901,5902],{"class":1746}," sig",[1735,5904,1757],{"class":1756},[1735,5906,1058],{"class":1760},[1735,5908,2492],{"class":1756},[1735,5910,5867],{"class":1746},[1735,5912,1770],{"class":1756},[1735,5914,5915,5918,5920,5923,5925,5928,5930,5932,5934,5936,5938,5940],{"class":1737,"line":988},[1735,5916,5917],{"class":1746},"        metrics",[1735,5919,1757],{"class":1756},[1735,5921,5922],{"class":1760},"Increment",[1735,5924,1763],{"class":1756},[1735,5926,5927],{"class":1766},"\"capitan_panics_total\"",[1735,5929,1825],{"class":1756},[1735,5931,2114],{"class":1766},[1735,5933,1825],{"class":1756},[1735,5935,5902],{"class":1746},[1735,5937,1757],{"class":1756},[1735,5939,1058],{"class":1760},[1735,5941,2130],{"class":1756},[1735,5943,5944],{"class":1737,"line":1800},[1735,5945,1797],{"emptyLinePlaceholder":1796},[1735,5947,5948],{"class":1737,"line":1806},[1735,5949,5950],{"class":1740},"        // Optional: alert on-call\n",[1735,5952,5953,5956,5959,5961,5963,5965],{"class":1737,"line":1833},[1735,5954,5955],{"class":1956},"        if",[1735,5957,5958],{"class":1760}," shouldAlert",[1735,5960,1763],{"class":1756},[1735,5962,5856],{"class":1746},[1735,5964,1966],{"class":1756},[1735,5966,1969],{"class":1756},[1735,5968,5969,5972,5974,5977,5979,5982,5984,5986,5988,5990,5992,5994],{"class":1737,"line":1838},[1735,5970,5971],{"class":1746},"            alerting",[1735,5973,1757],{"class":1756},[1735,5975,5976],{"class":1760},"Page",[1735,5978,1763],{"class":1756},[1735,5980,5981],{"class":1766},"\"Listener panic\"",[1735,5983,1825],{"class":1756},[1735,5985,5902],{"class":1746},[1735,5987,1757],{"class":1756},[1735,5989,1058],{"class":1760},[1735,5991,2492],{"class":1756},[1735,5993,5867],{"class":1746},[1735,5995,1770],{"class":1756},[1735,5997,5998],{"class":1737,"line":1844},[1735,5999,6000],{"class":1756},"        }\n",[1735,6002,6003],{"class":1737,"line":1866},[1735,6004,6005],{"class":1756},"    }),\n",[1735,6007,6008],{"class":1737,"line":1884},[1735,6009,1770],{"class":1756},[1718,6011,48],{"id":6012},"performance",[2850,6014,705],{"id":6015},"dont-block-in-listeners",[1644,6017,6018],{},"Listeners run in worker goroutines. Blocking affects all events for that signal:",[1726,6020,6022],{"className":1728,"code":6021,"language":1730,"meta":34,"style":34},"// Bad: blocking call in listener\ncapitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n    time.Sleep(5 * time.Second)  // Blocks worker\n    http.Post(url, body)          // Blocking I/O\n})\n\n// Good: offload blocking work\ncapitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n    go func() {\n        // Blocking work in separate goroutine\n        http.Post(url, body)\n    }()\n})\n\n// Better: use a work queue\ncapitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n    workQueue \u003C- extractWork(e)\n})\n",[1732,6023,6024,6029,6071,6101,6126,6130,6134,6139,6181,6192,6197,6216,6221,6225,6229,6234,6276,6293],{"__ignoreMap":34},[1735,6025,6026],{"class":1737,"line":9},[1735,6027,6028],{"class":1740},"// Bad: blocking call in listener\n",[1735,6030,6031,6033,6035,6037,6039,6041,6043,6045,6047,6049,6051,6053,6055,6057,6059,6061,6063,6065,6067,6069],{"class":1737,"line":19},[1735,6032,1642],{"class":1746},[1735,6034,1757],{"class":1756},[1735,6036,932],{"class":1760},[1735,6038,1763],{"class":1756},[1735,6040,4112],{"class":1746},[1735,6042,1825],{"class":1756},[1735,6044,1935],{"class":1934},[1735,6046,1763],{"class":1756},[1735,6048,1855],{"class":1940},[1735,6050,1944],{"class":1943},[1735,6052,1757],{"class":1756},[1735,6054,427],{"class":1943},[1735,6056,1825],{"class":1756},[1735,6058,1953],{"class":1940},[1735,6060,1957],{"class":1956},[1735,6062,1642],{"class":1943},[1735,6064,1757],{"class":1756},[1735,6066,1068],{"class":1943},[1735,6068,1966],{"class":1756},[1735,6070,1969],{"class":1756},[1735,6072,6073,6076,6078,6081,6083,6086,6088,6091,6093,6096,6098],{"class":1737,"line":40},[1735,6074,6075],{"class":1746},"    time",[1735,6077,1757],{"class":1756},[1735,6079,6080],{"class":1760},"Sleep",[1735,6082,1763],{"class":1756},[1735,6084,6085],{"class":1896},"5",[1735,6087,1957],{"class":1746},[1735,6089,6090],{"class":1746}," time",[1735,6092,1757],{"class":1756},[1735,6094,6095],{"class":1746},"Second",[1735,6097,1966],{"class":1756},[1735,6099,6100],{"class":1740},"  // Blocks worker\n",[1735,6102,6103,6106,6108,6111,6113,6116,6118,6121,6123],{"class":1737,"line":988},[1735,6104,6105],{"class":1746},"    http",[1735,6107,1757],{"class":1756},[1735,6109,6110],{"class":1760},"Post",[1735,6112,1763],{"class":1756},[1735,6114,6115],{"class":1746},"url",[1735,6117,1825],{"class":1756},[1735,6119,6120],{"class":1746}," body",[1735,6122,1966],{"class":1756},[1735,6124,6125],{"class":1740},"          // Blocking I/O\n",[1735,6127,6128],{"class":1737,"line":1800},[1735,6129,2047],{"class":1756},[1735,6131,6132],{"class":1737,"line":1806},[1735,6133,1797],{"emptyLinePlaceholder":1796},[1735,6135,6136],{"class":1737,"line":1833},[1735,6137,6138],{"class":1740},"// Good: offload blocking work\n",[1735,6140,6141,6143,6145,6147,6149,6151,6153,6155,6157,6159,6161,6163,6165,6167,6169,6171,6173,6175,6177,6179],{"class":1737,"line":1838},[1735,6142,1642],{"class":1746},[1735,6144,1757],{"class":1756},[1735,6146,932],{"class":1760},[1735,6148,1763],{"class":1756},[1735,6150,4112],{"class":1746},[1735,6152,1825],{"class":1756},[1735,6154,1935],{"class":1934},[1735,6156,1763],{"class":1756},[1735,6158,1855],{"class":1940},[1735,6160,1944],{"class":1943},[1735,6162,1757],{"class":1756},[1735,6164,427],{"class":1943},[1735,6166,1825],{"class":1756},[1735,6168,1953],{"class":1940},[1735,6170,1957],{"class":1956},[1735,6172,1642],{"class":1943},[1735,6174,1757],{"class":1756},[1735,6176,1068],{"class":1943},[1735,6178,1966],{"class":1756},[1735,6180,1969],{"class":1756},[1735,6182,6183,6186,6188,6190],{"class":1737,"line":1844},[1735,6184,6185],{"class":1956},"    go",[1735,6187,1935],{"class":1934},[1735,6189,2309],{"class":1756},[1735,6191,1969],{"class":1756},[1735,6193,6194],{"class":1737,"line":1866},[1735,6195,6196],{"class":1740},"        // Blocking work in separate goroutine\n",[1735,6198,6199,6202,6204,6206,6208,6210,6212,6214],{"class":1737,"line":1884},[1735,6200,6201],{"class":1746},"        http",[1735,6203,1757],{"class":1756},[1735,6205,6110],{"class":1760},[1735,6207,1763],{"class":1756},[1735,6209,6115],{"class":1746},[1735,6211,1825],{"class":1756},[1735,6213,6120],{"class":1746},[1735,6215,1770],{"class":1756},[1735,6217,6218],{"class":1737,"line":1902},[1735,6219,6220],{"class":1756},"    }()\n",[1735,6222,6223],{"class":1737,"line":2271},[1735,6224,2047],{"class":1756},[1735,6226,6227],{"class":1737,"line":2291},[1735,6228,1797],{"emptyLinePlaceholder":1796},[1735,6230,6231],{"class":1737,"line":2296},[1735,6232,6233],{"class":1740},"// Better: use a work queue\n",[1735,6235,6236,6238,6240,6242,6244,6246,6248,6250,6252,6254,6256,6258,6260,6262,6264,6266,6268,6270,6272,6274],{"class":1737,"line":2301},[1735,6237,1642],{"class":1746},[1735,6239,1757],{"class":1756},[1735,6241,932],{"class":1760},[1735,6243,1763],{"class":1756},[1735,6245,4112],{"class":1746},[1735,6247,1825],{"class":1756},[1735,6249,1935],{"class":1934},[1735,6251,1763],{"class":1756},[1735,6253,1855],{"class":1940},[1735,6255,1944],{"class":1943},[1735,6257,1757],{"class":1756},[1735,6259,427],{"class":1943},[1735,6261,1825],{"class":1756},[1735,6263,1953],{"class":1940},[1735,6265,1957],{"class":1956},[1735,6267,1642],{"class":1943},[1735,6269,1757],{"class":1756},[1735,6271,1068],{"class":1943},[1735,6273,1966],{"class":1756},[1735,6275,1969],{"class":1756},[1735,6277,6278,6281,6284,6287,6289,6291],{"class":1737,"line":2314},[1735,6279,6280],{"class":1746},"    workQueue",[1735,6282,6283],{"class":1956}," \u003C-",[1735,6285,6286],{"class":1760}," extractWork",[1735,6288,1763],{"class":1756},[1735,6290,1993],{"class":1746},[1735,6292,1770],{"class":1756},[1735,6294,6295],{"class":1737,"line":2320},[1735,6296,2047],{"class":1756},[2850,6298,710],{"id":6299},"dont-hold-event-references",[1644,6301,6302],{},"Events are pooled and reused. Copy data you need to retain:",[1726,6304,6306],{"className":1728,"code":6305,"language":1730,"meta":34,"style":34},"// Bad: holding event reference\nvar lastEvent *capitan.Event\ncapitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n    lastEvent = e  // Dangerous: event will be reused\n})\n\n// Good: copy needed data\nvar lastOrderID string\ncapitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n    if id, ok := orderIDKey.From(e); ok {\n        lastOrderID = id\n    }\n})\n\n// Also good: use Clone() when you need the full event\nvar lastEvent *capitan.Event\ncapitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {\n    lastEvent = e.Clone()  // Safe: independent copy\n})\n",[1732,6307,6308,6313,6329,6371,6383,6387,6391,6396,6406,6448,6477,6487,6491,6495,6499,6504,6518,6560,6577],{"__ignoreMap":34},[1735,6309,6310],{"class":1737,"line":9},[1735,6311,6312],{"class":1740},"// Bad: holding event reference\n",[1735,6314,6315,6317,6320,6322,6324,6326],{"class":1737,"line":19},[1735,6316,2223],{"class":1934},[1735,6318,6319],{"class":1746}," lastEvent",[1735,6321,1957],{"class":1956},[1735,6323,1642],{"class":1943},[1735,6325,1757],{"class":1756},[1735,6327,6328],{"class":1943},"Event\n",[1735,6330,6331,6333,6335,6337,6339,6341,6343,6345,6347,6349,6351,6353,6355,6357,6359,6361,6363,6365,6367,6369],{"class":1737,"line":40},[1735,6332,1642],{"class":1746},[1735,6334,1757],{"class":1756},[1735,6336,932],{"class":1760},[1735,6338,1763],{"class":1756},[1735,6340,4112],{"class":1746},[1735,6342,1825],{"class":1756},[1735,6344,1935],{"class":1934},[1735,6346,1763],{"class":1756},[1735,6348,1855],{"class":1940},[1735,6350,1944],{"class":1943},[1735,6352,1757],{"class":1756},[1735,6354,427],{"class":1943},[1735,6356,1825],{"class":1756},[1735,6358,1953],{"class":1940},[1735,6360,1957],{"class":1956},[1735,6362,1642],{"class":1943},[1735,6364,1757],{"class":1756},[1735,6366,1068],{"class":1943},[1735,6368,1966],{"class":1756},[1735,6370,1969],{"class":1756},[1735,6372,6373,6376,6378,6380],{"class":1737,"line":988},[1735,6374,6375],{"class":1746},"    lastEvent",[1735,6377,2233],{"class":1746},[1735,6379,1953],{"class":1746},[1735,6381,6382],{"class":1740},"  // Dangerous: event will be reused\n",[1735,6384,6385],{"class":1737,"line":1800},[1735,6386,2047],{"class":1756},[1735,6388,6389],{"class":1737,"line":1806},[1735,6390,1797],{"emptyLinePlaceholder":1796},[1735,6392,6393],{"class":1737,"line":1833},[1735,6394,6395],{"class":1740},"// Good: copy needed data\n",[1735,6397,6398,6400,6403],{"class":1737,"line":1838},[1735,6399,2223],{"class":1934},[1735,6401,6402],{"class":1746}," lastOrderID",[1735,6404,6405],{"class":1943}," string\n",[1735,6407,6408,6410,6412,6414,6416,6418,6420,6422,6424,6426,6428,6430,6432,6434,6436,6438,6440,6442,6444,6446],{"class":1737,"line":1844},[1735,6409,1642],{"class":1746},[1735,6411,1757],{"class":1756},[1735,6413,932],{"class":1760},[1735,6415,1763],{"class":1756},[1735,6417,4112],{"class":1746},[1735,6419,1825],{"class":1756},[1735,6421,1935],{"class":1934},[1735,6423,1763],{"class":1756},[1735,6425,1855],{"class":1940},[1735,6427,1944],{"class":1943},[1735,6429,1757],{"class":1756},[1735,6431,427],{"class":1943},[1735,6433,1825],{"class":1756},[1735,6435,1953],{"class":1940},[1735,6437,1957],{"class":1956},[1735,6439,1642],{"class":1943},[1735,6441,1757],{"class":1756},[1735,6443,1068],{"class":1943},[1735,6445,1966],{"class":1756},[1735,6447,1969],{"class":1756},[1735,6449,6450,6452,6454,6456,6458,6460,6462,6464,6466,6468,6470,6473,6475],{"class":1737,"line":1866},[1735,6451,5615],{"class":1956},[1735,6453,2447],{"class":1746},[1735,6455,1825],{"class":1756},[1735,6457,5595],{"class":1746},[1735,6459,1750],{"class":1746},[1735,6461,5600],{"class":1746},[1735,6463,1757],{"class":1756},[1735,6465,1308],{"class":1760},[1735,6467,1763],{"class":1756},[1735,6469,1993],{"class":1746},[1735,6471,6472],{"class":1756},");",[1735,6474,5595],{"class":1746},[1735,6476,1969],{"class":1756},[1735,6478,6479,6482,6484],{"class":1737,"line":1884},[1735,6480,6481],{"class":1746},"        lastOrderID",[1735,6483,2233],{"class":1746},[1735,6485,6486],{"class":1746}," id\n",[1735,6488,6489],{"class":1737,"line":1902},[1735,6490,5666],{"class":1756},[1735,6492,6493],{"class":1737,"line":2271},[1735,6494,2047],{"class":1756},[1735,6496,6497],{"class":1737,"line":2291},[1735,6498,1797],{"emptyLinePlaceholder":1796},[1735,6500,6501],{"class":1737,"line":2296},[1735,6502,6503],{"class":1740},"// Also good: use Clone() when you need the full event\n",[1735,6505,6506,6508,6510,6512,6514,6516],{"class":1737,"line":2301},[1735,6507,2223],{"class":1934},[1735,6509,6319],{"class":1746},[1735,6511,1957],{"class":1956},[1735,6513,1642],{"class":1943},[1735,6515,1757],{"class":1756},[1735,6517,6328],{"class":1943},[1735,6519,6520,6522,6524,6526,6528,6530,6532,6534,6536,6538,6540,6542,6544,6546,6548,6550,6552,6554,6556,6558],{"class":1737,"line":2314},[1735,6521,1642],{"class":1746},[1735,6523,1757],{"class":1756},[1735,6525,932],{"class":1760},[1735,6527,1763],{"class":1756},[1735,6529,4112],{"class":1746},[1735,6531,1825],{"class":1756},[1735,6533,1935],{"class":1934},[1735,6535,1763],{"class":1756},[1735,6537,1855],{"class":1940},[1735,6539,1944],{"class":1943},[1735,6541,1757],{"class":1756},[1735,6543,427],{"class":1943},[1735,6545,1825],{"class":1756},[1735,6547,1953],{"class":1940},[1735,6549,1957],{"class":1956},[1735,6551,1642],{"class":1943},[1735,6553,1757],{"class":1756},[1735,6555,1068],{"class":1943},[1735,6557,1966],{"class":1756},[1735,6559,1969],{"class":1756},[1735,6561,6562,6564,6566,6568,6570,6572,6574],{"class":1737,"line":2320},[1735,6563,6375],{"class":1746},[1735,6565,2233],{"class":1746},[1735,6567,1953],{"class":1746},[1735,6569,1757],{"class":1756},[1735,6571,1113],{"class":1760},[1735,6573,2309],{"class":1756},[1735,6575,6576],{"class":1740},"  // Safe: independent copy\n",[1735,6578,6579],{"class":1737,"line":2364},[1735,6580,2047],{"class":1756},[2850,6582,715],{"id":6583},"size-buffers-appropriately",[1644,6585,6586],{},"Monitor queue depths and adjust:",[1726,6588,6590],{"className":1728,"code":6589,"language":1730,"meta":34,"style":34},"// Start with defaults\ncapitan.Configure(capitan.WithBufferSize(16))\n\n// Monitor in production\ngo func() {\n    for range time.Tick(time.Minute) {\n        stats := capitan.Stats()\n        for signal, depth := range stats.QueueDepths {\n            if depth > 10 {\n                log.Printf(\"High queue depth: %s = %d\", signal.Name(), depth)\n            }\n        }\n    }\n}()\n\n// Increase if queues consistently fill\ncapitan.Configure(capitan.WithBufferSize(128))\n",[1732,6591,6592,6597,6620,6624,6629,6639,6668,6683,6710,6725,6763,6768,6772,6776,6781,6785,6790],{"__ignoreMap":34},[1735,6593,6594],{"class":1737,"line":9},[1735,6595,6596],{"class":1740},"// Start with defaults\n",[1735,6598,6599,6601,6603,6605,6607,6609,6611,6613,6615,6618],{"class":1737,"line":19},[1735,6600,1642],{"class":1746},[1735,6602,1757],{"class":1756},[1735,6604,952],{"class":1760},[1735,6606,1763],{"class":1756},[1735,6608,1642],{"class":1746},[1735,6610,1757],{"class":1756},[1735,6612,1149],{"class":1760},[1735,6614,1763],{"class":1756},[1735,6616,6617],{"class":1896},"16",[1735,6619,2782],{"class":1756},[1735,6621,6622],{"class":1737,"line":40},[1735,6623,1797],{"emptyLinePlaceholder":1796},[1735,6625,6626],{"class":1737,"line":988},[1735,6627,6628],{"class":1740},"// Monitor in production\n",[1735,6630,6631,6633,6635,6637],{"class":1737,"line":1800},[1735,6632,1730],{"class":1956},[1735,6634,1935],{"class":1934},[1735,6636,2309],{"class":1756},[1735,6638,1969],{"class":1756},[1735,6640,6641,6644,6647,6649,6651,6654,6656,6659,6661,6664,6666],{"class":1737,"line":1806},[1735,6642,6643],{"class":1956},"    for",[1735,6645,6646],{"class":1956}," range",[1735,6648,6090],{"class":1746},[1735,6650,1757],{"class":1756},[1735,6652,6653],{"class":1760},"Tick",[1735,6655,1763],{"class":1756},[1735,6657,6658],{"class":1746},"time",[1735,6660,1757],{"class":1756},[1735,6662,6663],{"class":1746},"Minute",[1735,6665,1966],{"class":1756},[1735,6667,1969],{"class":1756},[1735,6669,6670,6673,6675,6677,6679,6681],{"class":1737,"line":1833},[1735,6671,6672],{"class":1746},"        stats",[1735,6674,1750],{"class":1746},[1735,6676,1753],{"class":1746},[1735,6678,1757],{"class":1756},[1735,6680,947],{"class":1760},[1735,6682,2558],{"class":1756},[1735,6684,6685,6688,6691,6693,6696,6698,6700,6703,6705,6708],{"class":1737,"line":1838},[1735,6686,6687],{"class":1956},"        for",[1735,6689,6690],{"class":1746}," signal",[1735,6692,1825],{"class":1756},[1735,6694,6695],{"class":1746}," depth",[1735,6697,1750],{"class":1746},[1735,6699,6646],{"class":1956},[1735,6701,6702],{"class":1746}," stats",[1735,6704,1757],{"class":1756},[1735,6706,6707],{"class":1746},"QueueDepths",[1735,6709,1969],{"class":1756},[1735,6711,6712,6715,6717,6720,6723],{"class":1737,"line":1844},[1735,6713,6714],{"class":1956},"            if",[1735,6716,6695],{"class":1746},[1735,6718,6719],{"class":1956}," >",[1735,6721,6722],{"class":1896}," 10",[1735,6724,1969],{"class":1756},[1735,6726,6727,6730,6732,6734,6736,6739,6741,6744,6747,6749,6751,6753,6755,6757,6759,6761],{"class":1737,"line":1866},[1735,6728,6729],{"class":1746},"                log",[1735,6731,1757],{"class":1756},[1735,6733,2420],{"class":1760},[1735,6735,1763],{"class":1756},[1735,6737,6738],{"class":1766},"\"High queue depth: ",[1735,6740,2429],{"class":2428},[1735,6742,6743],{"class":1766}," = ",[1735,6745,6746],{"class":2428},"%d",[1735,6748,2442],{"class":1766},[1735,6750,1825],{"class":1756},[1735,6752,6690],{"class":1746},[1735,6754,1757],{"class":1756},[1735,6756,1058],{"class":1760},[1735,6758,2492],{"class":1756},[1735,6760,6695],{"class":1746},[1735,6762,1770],{"class":1756},[1735,6764,6765],{"class":1737,"line":1884},[1735,6766,6767],{"class":1756},"            }\n",[1735,6769,6770],{"class":1737,"line":1902},[1735,6771,6000],{"class":1756},[1735,6773,6774],{"class":1737,"line":2271},[1735,6775,5666],{"class":1756},[1735,6777,6778],{"class":1737,"line":2291},[1735,6779,6780],{"class":1756},"}()\n",[1735,6782,6783],{"class":1737,"line":2296},[1735,6784,1797],{"emptyLinePlaceholder":1796},[1735,6786,6787],{"class":1737,"line":2301},[1735,6788,6789],{"class":1740},"// Increase if queues consistently fill\n",[1735,6791,6792,6794,6796,6798,6800,6802,6804,6806,6808,6811],{"class":1737,"line":2314},[1735,6793,1642],{"class":1746},[1735,6795,1757],{"class":1756},[1735,6797,952],{"class":1760},[1735,6799,1763],{"class":1756},[1735,6801,1642],{"class":1746},[1735,6803,1757],{"class":1756},[1735,6805,1149],{"class":1760},[1735,6807,1763],{"class":1756},[1735,6809,6810],{"class":1896},"128",[1735,6812,2782],{"class":1756},[1718,6814,549],{"id":3543},[2850,6816,723],{"id":6817},"use-sync-mode-for-unit-tests",[1644,6819,6820],{},"Eliminates timing dependencies:",[1726,6822,6824],{"className":1728,"code":6823,"language":1730,"meta":34,"style":34},"func TestOrderHandler(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n\n    // Events process synchronously - no waiting needed\n}\n",[1732,6825,6826,6851,6874,6888,6892,6897],{"__ignoreMap":34},[1735,6827,6828,6830,6833,6835,6838,6840,6842,6844,6847,6849],{"class":1737,"line":9},[1735,6829,2069],{"class":1934},[1735,6831,6832],{"class":1760}," TestOrderHandler",[1735,6834,1763],{"class":1756},[1735,6836,6837],{"class":1940},"t",[1735,6839,1957],{"class":1956},[1735,6841,3543],{"class":1943},[1735,6843,1757],{"class":1756},[1735,6845,6846],{"class":1943},"T",[1735,6848,1966],{"class":1756},[1735,6850,1969],{"class":1756},[1735,6852,6853,6856,6858,6860,6862,6864,6866,6868,6870,6872],{"class":1737,"line":19},[1735,6854,6855],{"class":1746},"    c",[1735,6857,1750],{"class":1746},[1735,6859,1753],{"class":1746},[1735,6861,1757],{"class":1756},[1735,6863,977],{"class":1760},[1735,6865,1763],{"class":1756},[1735,6867,1642],{"class":1746},[1735,6869,1757],{"class":1756},[1735,6871,1159],{"class":1760},[1735,6873,2130],{"class":1756},[1735,6875,6876,6879,6882,6884,6886],{"class":1737,"line":40},[1735,6877,6878],{"class":1956},"    defer",[1735,6880,6881],{"class":1746}," c",[1735,6883,1757],{"class":1756},[1735,6885,962],{"class":1760},[1735,6887,2558],{"class":1756},[1735,6889,6890],{"class":1737,"line":988},[1735,6891,1797],{"emptyLinePlaceholder":1796},[1735,6893,6894],{"class":1737,"line":1800},[1735,6895,6896],{"class":1740},"    // Events process synchronously - no waiting needed\n",[1735,6898,6899],{"class":1737,"line":1806},[1735,6900,2564],{"class":1756},[2850,6902,728],{"id":6903},"use-isolated-instances",[1644,6905,6906],{},"Avoid cross-test contamination:",[1726,6908,6910],{"className":1728,"code":6909,"language":1730,"meta":34,"style":34},"func TestA(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n    // ...\n}\n\nfunc TestB(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n    // Separate instance, clean state\n}\n",[1732,6911,6912,6935,6957,6969,6974,6978,6982,7005,7027,7039,7044],{"__ignoreMap":34},[1735,6913,6914,6916,6919,6921,6923,6925,6927,6929,6931,6933],{"class":1737,"line":9},[1735,6915,2069],{"class":1934},[1735,6917,6918],{"class":1760}," TestA",[1735,6920,1763],{"class":1756},[1735,6922,6837],{"class":1940},[1735,6924,1957],{"class":1956},[1735,6926,3543],{"class":1943},[1735,6928,1757],{"class":1756},[1735,6930,6846],{"class":1943},[1735,6932,1966],{"class":1756},[1735,6934,1969],{"class":1756},[1735,6936,6937,6939,6941,6943,6945,6947,6949,6951,6953,6955],{"class":1737,"line":19},[1735,6938,6855],{"class":1746},[1735,6940,1750],{"class":1746},[1735,6942,1753],{"class":1746},[1735,6944,1757],{"class":1756},[1735,6946,977],{"class":1760},[1735,6948,1763],{"class":1756},[1735,6950,1642],{"class":1746},[1735,6952,1757],{"class":1756},[1735,6954,1159],{"class":1760},[1735,6956,2130],{"class":1756},[1735,6958,6959,6961,6963,6965,6967],{"class":1737,"line":40},[1735,6960,6878],{"class":1956},[1735,6962,6881],{"class":1746},[1735,6964,1757],{"class":1756},[1735,6966,962],{"class":1760},[1735,6968,2558],{"class":1756},[1735,6970,6971],{"class":1737,"line":988},[1735,6972,6973],{"class":1740},"    // ...\n",[1735,6975,6976],{"class":1737,"line":1800},[1735,6977,2564],{"class":1756},[1735,6979,6980],{"class":1737,"line":1806},[1735,6981,1797],{"emptyLinePlaceholder":1796},[1735,6983,6984,6986,6989,6991,6993,6995,6997,6999,7001,7003],{"class":1737,"line":1833},[1735,6985,2069],{"class":1934},[1735,6987,6988],{"class":1760}," TestB",[1735,6990,1763],{"class":1756},[1735,6992,6837],{"class":1940},[1735,6994,1957],{"class":1956},[1735,6996,3543],{"class":1943},[1735,6998,1757],{"class":1756},[1735,7000,6846],{"class":1943},[1735,7002,1966],{"class":1756},[1735,7004,1969],{"class":1756},[1735,7006,7007,7009,7011,7013,7015,7017,7019,7021,7023,7025],{"class":1737,"line":1838},[1735,7008,6855],{"class":1746},[1735,7010,1750],{"class":1746},[1735,7012,1753],{"class":1746},[1735,7014,1757],{"class":1756},[1735,7016,977],{"class":1760},[1735,7018,1763],{"class":1756},[1735,7020,1642],{"class":1746},[1735,7022,1757],{"class":1756},[1735,7024,1159],{"class":1760},[1735,7026,2130],{"class":1756},[1735,7028,7029,7031,7033,7035,7037],{"class":1737,"line":1844},[1735,7030,6878],{"class":1956},[1735,7032,6881],{"class":1746},[1735,7034,1757],{"class":1756},[1735,7036,962],{"class":1760},[1735,7038,2558],{"class":1756},[1735,7040,7041],{"class":1737,"line":1866},[1735,7042,7043],{"class":1740},"    // Separate instance, clean state\n",[1735,7045,7046],{"class":1737,"line":1884},[1735,7047,2564],{"class":1756},[2850,7049,733],{"id":7050},"never-use-default-instance-in-tests",[1644,7052,7053],{},"The default singleton persists across tests:",[1726,7055,7057],{"className":1728,"code":7056,"language":1730,"meta":34,"style":34},"// Bad: uses shared singleton\nfunc TestBad(t *testing.T) {\n    capitan.Hook(signal, handler)  // Leaks into other tests\n}\n\n// Good: isolated instance\nfunc TestGood(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    c.Hook(signal, handler)\n    defer c.Shutdown()\n}\n",[1732,7058,7059,7064,7087,7108,7112,7116,7121,7144,7166,7184,7196],{"__ignoreMap":34},[1735,7060,7061],{"class":1737,"line":9},[1735,7062,7063],{"class":1740},"// Bad: uses shared singleton\n",[1735,7065,7066,7068,7071,7073,7075,7077,7079,7081,7083,7085],{"class":1737,"line":19},[1735,7067,2069],{"class":1934},[1735,7069,7070],{"class":1760}," TestBad",[1735,7072,1763],{"class":1756},[1735,7074,6837],{"class":1940},[1735,7076,1957],{"class":1956},[1735,7078,3543],{"class":1943},[1735,7080,1757],{"class":1756},[1735,7082,6846],{"class":1943},[1735,7084,1966],{"class":1756},[1735,7086,1969],{"class":1756},[1735,7088,7089,7091,7093,7095,7097,7099,7101,7103,7105],{"class":1737,"line":40},[1735,7090,2323],{"class":1746},[1735,7092,1757],{"class":1756},[1735,7094,932],{"class":1760},[1735,7096,1763],{"class":1756},[1735,7098,4112],{"class":1746},[1735,7100,1825],{"class":1756},[1735,7102,4280],{"class":1746},[1735,7104,1966],{"class":1756},[1735,7106,7107],{"class":1740},"  // Leaks into other tests\n",[1735,7109,7110],{"class":1737,"line":988},[1735,7111,2564],{"class":1756},[1735,7113,7114],{"class":1737,"line":1800},[1735,7115,1797],{"emptyLinePlaceholder":1796},[1735,7117,7118],{"class":1737,"line":1806},[1735,7119,7120],{"class":1740},"// Good: isolated instance\n",[1735,7122,7123,7125,7128,7130,7132,7134,7136,7138,7140,7142],{"class":1737,"line":1833},[1735,7124,2069],{"class":1934},[1735,7126,7127],{"class":1760}," TestGood",[1735,7129,1763],{"class":1756},[1735,7131,6837],{"class":1940},[1735,7133,1957],{"class":1956},[1735,7135,3543],{"class":1943},[1735,7137,1757],{"class":1756},[1735,7139,6846],{"class":1943},[1735,7141,1966],{"class":1756},[1735,7143,1969],{"class":1756},[1735,7145,7146,7148,7150,7152,7154,7156,7158,7160,7162,7164],{"class":1737,"line":1838},[1735,7147,6855],{"class":1746},[1735,7149,1750],{"class":1746},[1735,7151,1753],{"class":1746},[1735,7153,1757],{"class":1756},[1735,7155,977],{"class":1760},[1735,7157,1763],{"class":1756},[1735,7159,1642],{"class":1746},[1735,7161,1757],{"class":1756},[1735,7163,1159],{"class":1760},[1735,7165,2130],{"class":1756},[1735,7167,7168,7170,7172,7174,7176,7178,7180,7182],{"class":1737,"line":1844},[1735,7169,6855],{"class":1746},[1735,7171,1757],{"class":1756},[1735,7173,932],{"class":1760},[1735,7175,1763],{"class":1756},[1735,7177,4112],{"class":1746},[1735,7179,1825],{"class":1756},[1735,7181,4280],{"class":1746},[1735,7183,1770],{"class":1756},[1735,7185,7186,7188,7190,7192,7194],{"class":1737,"line":1866},[1735,7187,6878],{"class":1956},[1735,7189,6881],{"class":1746},[1735,7191,1757],{"class":1756},[1735,7193,962],{"class":1760},[1735,7195,2558],{"class":1756},[1735,7197,7198],{"class":1737,"line":1884},[1735,7199,2564],{"class":1756},[2991,7201,7202],{},"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 .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}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 .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}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 .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}",{"title":34,"searchDepth":19,"depth":19,"links":7204},[7205,7210,7215,7220,7225,7230],{"id":3946,"depth":19,"text":626,"children":7206},[7207,7208,7209],{"id":3949,"depth":40,"text":630},{"id":4383,"depth":40,"text":635},{"id":4446,"depth":40,"text":640},{"id":4548,"depth":19,"text":645,"children":7211},[7212,7213,7214],{"id":4551,"depth":40,"text":649},{"id":4687,"depth":40,"text":654},{"id":4808,"depth":40,"text":659},{"id":4930,"depth":19,"text":664,"children":7216},[7217,7218,7219],{"id":4933,"depth":40,"text":668},{"id":5182,"depth":40,"text":673},{"id":5407,"depth":40,"text":678},{"id":5533,"depth":19,"text":683,"children":7221},[7222,7223,7224],{"id":5536,"depth":40,"text":687},{"id":5678,"depth":40,"text":692},{"id":5820,"depth":40,"text":697},{"id":6012,"depth":19,"text":48,"children":7226},[7227,7228,7229],{"id":6015,"depth":40,"text":705},{"id":6299,"depth":40,"text":710},{"id":6583,"depth":40,"text":715},{"id":3543,"depth":19,"text":549,"children":7231},[7232,7233,7234],{"id":6817,"depth":40,"text":723},{"id":6903,"depth":40,"text":728},{"id":7050,"depth":40,"text":733},{},"2025-12-09T00:00:00.000Z",null,{"title":617,"description":619},[617,7240,7241],"Production","Patterns","VHihEhLjFs7iG3pZWNreriqEzD9WT_h1U0NChC104fM",[7244,7245],{"title":549,"path":548,"stem":1578,"description":551,"children":-1},{"title":738,"path":737,"stem":1582,"description":740,"children":-1},1776110885976]