com.wallstop-studios.dxmessaging 3.2.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs +8 -8
  3. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs +171 -13
  4. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs +391 -96
  5. package/Editor/CustomEditors/MessageAwareComponentInspectorView.cs +279 -0
  6. package/Editor/CustomEditors/MessageAwareComponentInspectorView.cs.meta +11 -0
  7. package/Editor/DxMessagingEditorPalette.cs +66 -0
  8. package/Editor/DxMessagingEditorPalette.cs.meta +11 -0
  9. package/Editor/DxMessagingEditorTheme.cs +200 -0
  10. package/Editor/DxMessagingEditorTheme.cs.meta +11 -0
  11. package/Editor/Icons/dxmessaging-icon-256.png +0 -0
  12. package/Editor/Icons/dxmessaging-icon-256.png.meta +156 -0
  13. package/Editor/Icons/dxmessaging-icon-32.png +0 -0
  14. package/Editor/Icons/dxmessaging-icon-32.png.meta +156 -0
  15. package/Editor/Icons/dxmessaging-icon-48.png +0 -0
  16. package/Editor/Icons/dxmessaging-icon-48.png.meta +156 -0
  17. package/Editor/Icons.meta +8 -0
  18. package/Editor/Settings/DxMessagingSettingsProvider.cs +368 -33
  19. package/Editor/Testing/MessagingComponentEditorHarness.cs +11 -4
  20. package/Editor/Theme/DxMessagingTheme.uss +255 -0
  21. package/Editor/Theme/DxMessagingTheme.uss.meta +11 -0
  22. package/Editor/Theme/DxTokens.uss +90 -0
  23. package/Editor/Theme/DxTokens.uss.meta +11 -0
  24. package/Editor/Theme.meta +8 -0
  25. package/Editor/Windows/DxMessagingFlowGraphWindow.cs +5513 -0
  26. package/Editor/Windows/DxMessagingFlowGraphWindow.cs.meta +11 -0
  27. package/Editor/Windows/DxMessagingMessageMonitorWindow.cs +2288 -0
  28. package/Editor/Windows/DxMessagingMessageMonitorWindow.cs.meta +11 -0
  29. package/Editor/Windows.meta +8 -0
  30. package/README.md +84 -35
  31. package/Runtime/AssemblyInfo.cs +1 -0
  32. package/Runtime/Core/Diagnostics/MessageEmissionData.cs +29 -1
  33. package/Runtime/Core/Extensions/MessageExtensions.cs +4 -5
  34. package/Runtime/Core/Helper/MessageCache.cs +56 -23
  35. package/Runtime/Core/Internal/FlatDispatch.cs +32 -0
  36. package/Runtime/Core/Internal/TypedSlots.cs +3 -7
  37. package/Runtime/Core/MessageBus/MessageBus.cs +577 -142
  38. package/Runtime/Core/MessageBus/RegistrationLog.cs +25 -12
  39. package/Runtime/Core/MessageHandler.cs +774 -786
  40. package/Runtime/Core/MessageRegistrationHandle.cs +19 -5
  41. package/Runtime/Core/MessageRegistrationToken.cs +1080 -520
  42. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingExerciser.cs +173 -0
  43. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingExerciser.cs.meta +11 -0
  44. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingExerciser.unity +424 -0
  45. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingExerciser.unity.meta +7 -0
  46. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingReceiver.cs +185 -0
  47. package/Samples~/Diagnostics Tooling Exerciser/DiagnosticsToolingReceiver.cs.meta +11 -0
  48. package/Samples~/Diagnostics Tooling Exerciser/Messages.cs +46 -0
  49. package/Samples~/Diagnostics Tooling Exerciser/Messages.cs.meta +11 -0
  50. package/Samples~/Diagnostics Tooling Exerciser/README.md +53 -0
  51. package/Samples~/Diagnostics Tooling Exerciser/README.md.meta +7 -0
  52. package/Samples~/Diagnostics Tooling Exerciser/WallstopStudios.DxMessaging.DiagnosticsToolingExerciser.Sample.asmdef +13 -0
  53. package/Samples~/Diagnostics Tooling Exerciser/WallstopStudios.DxMessaging.DiagnosticsToolingExerciser.Sample.asmdef.meta +7 -0
  54. package/Samples~/Diagnostics Tooling Exerciser.meta +8 -0
  55. package/Samples~/Mini Combat/README.md +1 -1
  56. package/Samples~/Mini Combat/Walkthrough.md +3 -3
  57. package/docs/images/DxMessaging-banner.svg +79 -0
  58. package/docs/images/DxMessaging-banner.svg.meta +53 -0
  59. package/docs/images.meta +8 -0
  60. package/docs.meta +8 -0
  61. package/package.json +13 -4
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 2ca6a4e3bf77409a9be9f081ef85983d
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -0,0 +1,8 @@
1
+ fileFormatVersion: 2
2
+ guid: 51d5e91f8bea443da9aa3ce912e88410
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant:
package/README.md CHANGED
@@ -1,31 +1,37 @@
1
- # DxMessaging for Unity
1
+ # DxMessaging
2
2
 
3
3
  <p align="center">
4
- <img src="docs/images/DxMessaging-banner.svg" alt="DxMessaging - Type-safe messaging system for Unity" width="800"/>
4
+ <img src="docs/images/DxMessaging-banner.svg" alt="DxMessaging - Decoupled, simple systems for Unity" width="800"/>
5
5
  </p>
6
6
 
7
7
  <p align="center">
8
8
  <a href="https://ambiguous-interactive.github.io/DxMessaging/">
9
- <img src="https://img.shields.io/badge/Full_Documentation-Visit_the_Docs_Site-2ea44f?style=for-the-badge" alt="Full Documentation" />
9
+ <img src="https://img.shields.io/badge/Docs-ambiguous--interactive.github.io-f4a836?style=for-the-badge" alt="Documentation" />
10
10
  </a>
11
11
  </p>
12
12
 
13
13
  [![Unity](https://img.shields.io/badge/Unity-2021.3+-black.svg)](https://unity.com/releases/editor/archive)<br/>
14
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md)<br/>
14
+ [![License: MIT](https://img.shields.io/badge/License-MIT-f4a836.svg)](LICENSE.md)<br/>
15
15
  [![openupm](https://img.shields.io/npm/v/com.wallstop-studios.dxmessaging?label=openupm&registry_uri=https://package.openupm.com)](https://openupm.com/packages/com.wallstop-studios.dxmessaging/)<br/>
16
16
  [![Version](https://img.shields.io/npm/v/com.wallstop-studios.dxmessaging.svg)](https://www.npmjs.com/package/com.wallstop-studios.dxmessaging)<br/>
17
- [![Performance: Standalone (IL2CPP)](<https://img.shields.io/badge/Performance-Standalone%20(IL2CPP)-blueviolet.svg>)](docs/architecture/performance.md)<br/>
17
+ [![Performance: Standalone (IL2CPP)](<https://img.shields.io/badge/Performance-Standalone%20(IL2CPP)-f4a836.svg>)](docs/architecture/performance.md)<br/>
18
18
  [![Markdown Link Validity](https://github.com/Ambiguous-Interactive/DxMessaging/actions/workflows/markdown-link-validity.yml/badge.svg)](https://github.com/Ambiguous-Interactive/DxMessaging/actions/workflows/markdown-link-validity.yml)
19
19
 
20
- > **🤖 AI Assistance Disclosure:**
20
+ > **AI Assistance Disclosure:**
21
21
  >
22
22
  > This project has been actively developed and maintained by human authors for over a decade. Recent versions have utilized AI assistance for documentation, test coverage, and performance optimizations.
23
23
 
24
- **DxMessaging is a type-safe messaging system** that replaces sprawling C# events, brittle UnityEvents, and global static event buses with an observable and lifecycle-managed communication pattern.
24
+ **DxMessaging is a synchronous, type-safe Unity message bus for decoupled, simple systems.** It replaces sprawling C# events, brittle UnityEvents, and global static event buses with explicit message contracts, lifecycle-managed registrations, and editor-visible routing.
25
25
 
26
- Think of it as: A messaging system designed for decoupled game systems.
26
+ Use it when one system needs to announce a fact, command a target, or broadcast from a source without keeping a direct scene reference to the receiver.
27
27
 
28
- Need install instructions? Try [OpenUPM](https://openupm.com/packages/com.wallstop-studios.dxmessaging/) (recommended) or see the [Install Guide](docs/getting-started/install.md) for Git URLs, scoped registries, and more.
28
+ ```bash
29
+ openupm add com.wallstop-studios.dxmessaging
30
+ ```
31
+
32
+ The core taxonomy is small: **Untargeted** announcements, **Targeted** commands, and **Broadcast** facts from a source. Diagnostics are built around that model: Message Monitor shows recent emissions and filters, Flow Graph shows registration topology and trace evidence, and the Inspector overlay catches `MessageAwareComponent` lifecycle mistakes.
33
+
34
+ See the [Install Guide](docs/getting-started/install.md) for Git URL, scoped registry, and release tarball options.
29
35
 
30
36
  ## Table of Contents
31
37
 
@@ -605,38 +611,79 @@ void OnDamage(ref TookDamage msg) {
605
611
  - Block messages during cutscenes
606
612
  - Log/audit sensitive actions
607
613
 
608
- ### Built-in Inspector Diagnostics
614
+ ### Built-in Editor Diagnostics
609
615
 
610
616
  **The problem with normal events:** "Which event fired? When? Who handled it? In what order?" = unknown
611
617
 
612
- **DxMessaging solution:** Click any `MessageAwareComponent` in the Inspector:
613
-
614
- #### Message History (last 50)
615
-
616
- - `[12:34:56.123] HealthChanged`
617
- - amount: 25
618
- - priority: 0
619
- - handlers: 3
620
- - `[12:34:55.987] ItemAdded`
621
- - itemId: 42, count: 1
622
- - priority: 5
623
- - handlers: 2
624
-
625
- ##### Active Registrations
626
-
627
- - [x] HealthChanged (priority: 0, called: 847 times)
628
- - [x] ItemAdded (priority: 5, called: 23 times)
629
- - [x] TookDamage (priority: 10, called: 1,203 times)
618
+ **DxMessaging solution:** turn on diagnostics in the Inspector or Project Settings, then use the
619
+ dedicated Unity editor tools under **Tools > Wallstop Studios > DxMessaging**.
620
+
621
+ #### Message Monitor
622
+
623
+ - Recent global-bus emissions, most-recent first
624
+ - Message type, context, and stack trace details
625
+ - Plain text filtering, `type:` / `message:` / `context:` / `stack:` field
626
+ filters, an active-filter summary with Clear, and Copy JSON export
627
+ - Visible message-type lanes showing per-filter entry counts, context breadth,
628
+ and message-volume share, plus context lanes showing context volume and
629
+ message breadth, with row-level Filter shortcuts and exact quoted context
630
+ shortcuts
631
+ - Loaded-scene `MessagingComponent` diagnostics, including listener counts,
632
+ registrations, calls, local messages, and provider warnings
633
+
634
+ #### Flow Graph
635
+
636
+ - Component nodes, message-type nodes, and registration edges
637
+ - Route-map route-kind mix, call shares, widest-message target-component
638
+ fan-out, most-routed target, inactive routed-target, hottest-route, and
639
+ no-call route summaries, recent traced-route coverage, busiest traced-route,
640
+ traced-message, and traced-target share, visible message lanes grouped by
641
+ message type, visible target lanes grouped by target component, visible trace
642
+ route-kind lanes grouped by traced registration kind, visible trace message
643
+ lanes grouped by traced message type, visible trace target lanes grouped by
644
+ traced target component, visible trace-id lanes grouped by positive trace id,
645
+ visible trace context lanes grouped by normalized context, visible trace
646
+ context volume and share, visible trace-id
647
+ breadth, visible trace-target/path concentration, selected
648
+ component/message route-health and busiest traced-route details, selected
649
+ component busiest traced-message details, selected message busiest
650
+ traced-target details, selected
651
+ component/message/route visible traced-share details, and selected
652
+ component/message/route trace context volume, trace context delivery,
653
+ busiest-context-share, trace-id breadth, selected component trace-message,
654
+ selected message trace-target, busiest-path, and busiest-path-share details
655
+ - Recent global/listener emission evidence
656
+ - Exact recent traced delivery counts per registration edge
657
+ - Recent trace-path/context aggregates, including busiest trace context share,
658
+ busiest trace message/target/path shares, visible flow corridors by
659
+ message/target pair, visible trace route-kind lanes by traced registration
660
+ kind, visible trace message lanes by traced message type, visible trace target
661
+ lanes by traced target component, visible trace context lanes by normalized
662
+ context, visible trace-id lanes by positive trace id, widest visible trace id
663
+ by path count, and exact trace-id export arrays when diagnostics capture token
664
+ delivery records with positive trace ids
665
+
666
+ #### Inspector integration
667
+
668
+ Click any `MessagingComponent` in the Inspector for component-local diagnostics
669
+ and any `MessageAwareComponent` subclass for base-call warning surfaces.
630
670
 
631
671
  #### Real-world debugging scenarios
632
672
 
633
- - "Did my message fire?" -> Check history, see timestamp
634
- - "Why didn't my handler run?" -> Check registrations, see if it's active
635
- - "What's firing too often?" -> Sort by call count
636
- - "What's the execution order?" -> Sort by priority
673
+ - "Did my message fire?" -> Check Message Monitor history.
674
+ - "Why didn't my handler run?" -> Check registrations and Flow Graph edges.
675
+ - "What's firing too often?" -> Compare call counts in component and route summaries.
676
+ - "Which source/target context was involved?" -> Check Message Monitor context
677
+ and Flow Graph trace paths.
637
678
 
638
679
  **No more:** Setting 50 breakpoints and stepping through code for 30 minutes.
639
680
 
681
+ See [Diagnostics](docs/guides/diagnostics.md) for the current editor tooling and
682
+ trace-path semantics. The
683
+ [Diagnostics Tooling Exerciser](Samples~/Diagnostics%20Tooling%20Exerciser/README.md)
684
+ sample imports a deterministic scene that drives Message Monitor, Flow Graph,
685
+ Inspector diagnostics, and Project Settings together.
686
+
640
687
  ### Local Bus Islands for Testing
641
688
 
642
689
  **The problem with normal events:** Global static events contaminate tests. Mock complexity. Flaky tests.
@@ -654,8 +701,9 @@ public void TestAchievementSystem() {
654
701
  // Test in isolation
655
702
  _ = token.RegisterBroadcastWithoutSource<EnemyKilled>(achievements.OnKill);
656
703
 
704
+ var gameObject = new GameObject();
657
705
  var msg = new EnemyKilled("Boss");
658
- msg.EmitGameObjectBroadcast(enemy, testBus); // Only this test sees it
706
+ msg.EmitGameObjectBroadcast(gameObject, testBus); // Only this test sees it
659
707
 
660
708
  Assert.IsTrue(achievements.Unlocked("BossSlayer"));
661
709
  }
@@ -693,7 +741,7 @@ public void TestAchievementSystem() {
693
741
 
694
742
  - [Unity Integration](docs/guides/unity-integration.md) -- MessagingComponent deep dive
695
743
  - [Targeting & Context](docs/concepts/targeting-and-context.md) -- GameObject vs Component
696
- - [Diagnostics](docs/guides/diagnostics.md) -- Inspector tools and debugging
744
+ - [Diagnostics](docs/guides/diagnostics.md) -- editor tools, traces, and debugging
697
745
  - [Memory Reclamation](docs/guides/memory-reclamation.md) -- idle eviction, explicit Trim, and tuning the runtime settings asset
698
746
 
699
747
  Important: Inheritance with MessageAwareComponent
@@ -872,6 +920,7 @@ Import samples from Package Manager:
872
920
 
873
921
  - **[Mini Combat](Samples~/Mini%20Combat/README.md)** -- Simple combat with Heal/Damage messages
874
922
  - **[UI Buttons + Inspector](Samples~/UI%20Buttons%20%2B%20Inspector/README.md)** -- Interactive diagnostics demo
923
+ - **[Diagnostics Tooling Exerciser](Samples~/Diagnostics%20Tooling%20Exerciser/README.md)** -- Deterministic scene for Message Monitor, Flow Graph, Inspector diagnostics, and Project Settings
875
924
 
876
925
  ## Requirements
877
926
 
@@ -908,7 +957,7 @@ DxMessaging publishes machine-readable context for AI agents through [llms.txt](
908
957
 
909
958
  - **[llms.txt](llms.txt)** -- Complete project overview, API reference, and context in a single file
910
959
  - **[Repository Guidelines](.llm/context.md)** -- Coding standards and development workflows
911
- - **[AI Agent Skills](.llm/skills/)** -- 157+ specialized skill documents covering documentation, testing, GitHub Actions, and more
960
+ - **[AI Agent Skills](.llm/skills/)** -- 159+ specialized skill documents covering documentation, testing, GitHub Actions, and more
912
961
 
913
962
  The `llms.txt` file is automatically updated via CI/CD to stay current with project changes. It includes:
914
963
 
@@ -14,3 +14,4 @@ using System.Runtime.CompilerServices;
14
14
  [assembly: InternalsVisibleTo("WallstopStudios.DxMessaging.Tests.Runtime.Zenject")]
15
15
  [assembly: InternalsVisibleTo("WallstopStudios.DxMessaging.Tests.Editor")]
16
16
  [assembly: InternalsVisibleTo("WallstopStudios.DxMessaging.Tests.Editor.Allocations")]
17
+ [assembly: InternalsVisibleTo("WallstopStudios.DxMessaging.Tests.00.Runtime.Benchmarks")]
@@ -17,7 +17,9 @@ namespace DxMessaging.Core.Diagnostics
17
17
  /// that excludes DxMessaging internals for easier debugging.
18
18
  ///
19
19
  /// The <see cref="context"/> contains the relevant <see cref="InstanceId"/> for targeted/broadcast messages
20
- /// (target or source respectively) and is null for untargeted messages.
20
+ /// (target or source respectively) and is null for untargeted messages. Runtime records emitted by a
21
+ /// <see cref="MessageBus.MessageBus"/> also carry a <see cref="traceId"/> that token-local delivery records can
22
+ /// use to join a bus emission to the registrations that observed it.
21
23
  /// </remarks>
22
24
  public readonly struct MessageEmissionData
23
25
  {
@@ -33,15 +35,41 @@ namespace DxMessaging.Core.Diagnostics
33
35
  /// <summary>Trimmed stack trace captured at the emission site.</summary>
34
36
  public readonly string stackTrace;
35
37
 
38
+ /// <summary>
39
+ /// Dispatch trace identifier shared by bus-side emission records and token-side delivery records.
40
+ /// </summary>
41
+ public readonly long traceId;
42
+
43
+ /// <summary>
44
+ /// Registration handle that observed this message; default for bus-side emission records.
45
+ /// </summary>
46
+ public readonly MessageRegistrationHandle registrationHandle;
47
+
36
48
  /// <summary>
37
49
  /// Creates a new diagnostic record for an emitted message.
38
50
  /// </summary>
39
51
  /// <param name="message">The message that was emitted.</param>
40
52
  /// <param name="context">Target or source depending on message category; null for untargeted.</param>
41
53
  public MessageEmissionData(IMessage message, InstanceId? context = null)
54
+ : this(message, context, traceId: 0, registrationHandle: default) { }
55
+
56
+ internal MessageEmissionData(IMessage message, long traceId)
57
+ : this(message, context: null, traceId, registrationHandle: default) { }
58
+
59
+ internal MessageEmissionData(IMessage message, InstanceId? context, long traceId)
60
+ : this(message, context, traceId, registrationHandle: default) { }
61
+
62
+ internal MessageEmissionData(
63
+ IMessage message,
64
+ InstanceId? context,
65
+ long traceId,
66
+ MessageRegistrationHandle registrationHandle
67
+ )
42
68
  {
43
69
  this.message = message;
44
70
  this.context = context;
71
+ this.traceId = traceId;
72
+ this.registrationHandle = registrationHandle;
45
73
  stackTrace = GetAccurateStackTrace();
46
74
  }
47
75
 
@@ -23,10 +23,9 @@ namespace DxMessaging.Core.Extensions
23
23
  /// var heal = new Heal(10);
24
24
  /// heal.EmitTargeted(target);
25
25
  ///
26
- /// // Broadcast (from source)
27
- /// var source = (DxMessaging.Core.InstanceId)gameObject;
26
+ /// // Broadcast (from this GameObject)
28
27
  /// var dmg = new TookDamage(5);
29
- /// dmg.EmitBroadcast(source);
28
+ /// dmg.EmitGameObjectBroadcast(gameObject);
30
29
  ///
31
30
  /// // Unity conveniences
32
31
  /// var hello = new StringMessage("Hello");
@@ -334,11 +333,11 @@ namespace DxMessaging.Core.Extensions
334
333
  /// public readonly partial struct TookDamage { public readonly int amount; }
335
334
  ///
336
335
  /// var dmg = new TookDamage(5);
337
- /// dmg.EmitFrom((InstanceId)gameObject); // OK: from GameObject
338
336
  /// dmg.EmitGameObjectBroadcast(gameObject); // explicit, preferred in Unity code
337
+ /// dmg.EmitFrom(gameObject); // shorthand from the same GameObject
339
338
  ///
340
339
  /// // Pitfall: identifies the Component as source (won't match GO-source registrations)
341
- /// dmg.EmitFrom((InstanceId)this);
340
+ /// dmg.EmitComponentBroadcast(this);
342
341
  /// </code>
343
342
  /// </example>
344
343
  public static void EmitFrom<TMessage>(
@@ -1,5 +1,6 @@
1
1
  namespace DxMessaging.Core.Helper
2
2
  {
3
+ using System;
3
4
  using System.Collections;
4
5
  using System.Collections.Generic;
5
6
  using System.Runtime.CompilerServices;
@@ -8,7 +9,7 @@ namespace DxMessaging.Core.Helper
8
9
  /// Sparse, type-indexed cache keyed by message type for fast lookups without dictionaries.
9
10
  /// </summary>
10
11
  /// <remarks>
11
- /// Internally maintains a list indexed by a compact, per-message-type integer (assigned via
12
+ /// Internally maintains a sparse array indexed by a compact, per-message-type integer (assigned via
12
13
  /// <see cref="MessageHelperIndexer{TMessage}"/>). Used heavily by the bus to store handlers and interceptors
13
14
  /// with minimal overhead.
14
15
  /// </remarks>
@@ -39,8 +40,8 @@ namespace DxMessaging.Core.Helper
39
40
  /// <returns><c>true</c> if another non-null value exists; otherwise <c>false</c>.</returns>
40
41
  public bool MoveNext()
41
42
  {
42
- List<TValue> values = _cache._values;
43
- int count = values.Count;
43
+ TValue[] values = _cache._values;
44
+ int count = _cache._count;
44
45
  while (++_index < count)
45
46
  {
46
47
  _current = values[_index];
@@ -74,7 +75,8 @@ namespace DxMessaging.Core.Helper
74
75
  public void Dispose() { }
75
76
  }
76
77
 
77
- private readonly List<TValue> _values = new();
78
+ private TValue[] _values = Array.Empty<TValue>();
79
+ private int _count;
78
80
 
79
81
  /// <summary>
80
82
  /// Retrieves the value associated with <typeparamref name="TMessage"/>, creating one if needed.
@@ -89,7 +91,7 @@ namespace DxMessaging.Core.Helper
89
91
  int index = MessageHelperIndexer<TMessage>.SequentialId;
90
92
  if (0 <= index)
91
93
  {
92
- FillToIndex(index);
94
+ EnsureIndex(index);
93
95
  value = _values[index];
94
96
  if (value != null)
95
97
  {
@@ -101,11 +103,9 @@ namespace DxMessaging.Core.Helper
101
103
  }
102
104
  else
103
105
  {
104
- index = MessageHelperIndexer.TotalMessages++;
105
- MessageHelperIndexer<TMessage>.SequentialId = index;
106
- FillToIndex(index - 1);
106
+ index = AssignIndex<TMessage>();
107
107
  value = new TValue();
108
- _values.Add(value);
108
+ _values[index] = value;
109
109
  }
110
110
 
111
111
  return value;
@@ -123,15 +123,13 @@ namespace DxMessaging.Core.Helper
123
123
  int index = MessageHelperIndexer<TMessage>.SequentialId;
124
124
  if (0 <= index)
125
125
  {
126
- FillToIndex(index);
126
+ EnsureIndex(index);
127
127
  _values[index] = value;
128
128
  return;
129
129
  }
130
130
 
131
- index = MessageHelperIndexer.TotalMessages++;
132
- MessageHelperIndexer<TMessage>.SequentialId = index;
133
- FillToIndex(index - 1);
134
- _values.Add(value);
131
+ index = AssignIndex<TMessage>();
132
+ _values[index] = value;
135
133
  }
136
134
 
137
135
  /// <summary>
@@ -145,7 +143,7 @@ namespace DxMessaging.Core.Helper
145
143
  where TMessage : IMessage
146
144
  {
147
145
  int index = MessageHelperIndexer<TMessage>.SequentialId;
148
- if (0 <= index && index < _values.Count)
146
+ if (0 <= index && index < _values.Length)
149
147
  {
150
148
  value = _values[index];
151
149
  return value != null;
@@ -164,7 +162,7 @@ namespace DxMessaging.Core.Helper
164
162
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
165
163
  internal bool TryGetValueAtIndex(int index, out TValue value)
166
164
  {
167
- if (0 <= index && index < _values.Count)
165
+ if (0 <= index && index < _values.Length)
168
166
  {
169
167
  value = _values[index];
170
168
  return value != null;
@@ -183,7 +181,7 @@ namespace DxMessaging.Core.Helper
183
181
  where TMessage : IMessage
184
182
  {
185
183
  int index = MessageHelperIndexer<TMessage>.SequentialId;
186
- if (0 <= index && index < _values.Count)
184
+ if (0 <= index && index < _values.Length)
187
185
  {
188
186
  _values[index] = null;
189
187
  }
@@ -196,7 +194,7 @@ namespace DxMessaging.Core.Helper
196
194
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
197
195
  internal void RemoveAtIndex(int index)
198
196
  {
199
- if (0 <= index && index < _values.Count)
197
+ if (0 <= index && index < _values.Length)
200
198
  {
201
199
  _values[index] = null;
202
200
  }
@@ -224,17 +222,52 @@ namespace DxMessaging.Core.Helper
224
222
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
225
223
  internal void Clear()
226
224
  {
227
- _values.Clear();
225
+ _values = Array.Empty<TValue>();
226
+ _count = 0;
227
+ }
228
+
229
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
230
+ private int AssignIndex<TMessage>()
231
+ where TMessage : IMessage
232
+ {
233
+ int index = MessageHelperIndexer.TotalMessages;
234
+ EnsureIndex(index);
235
+ MessageHelperIndexer<TMessage>.SequentialId = index;
236
+ MessageHelperIndexer.TotalMessages = index + 1;
237
+ return index;
228
238
  }
229
239
 
230
240
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
231
- private void FillToIndex(int index)
241
+ private void EnsureIndex(int index)
232
242
  {
233
- int count = _values.Count;
234
- for (int i = count; i <= index; ++i)
243
+ if (index < 0)
244
+ {
245
+ throw new InvalidOperationException("Message type indices cannot be negative.");
246
+ }
247
+
248
+ if (index < _values.Length)
249
+ {
250
+ if (_count <= index)
251
+ {
252
+ _count = index + 1;
253
+ }
254
+ return;
255
+ }
256
+
257
+ int capacity = _values.Length == 0 ? 4 : _values.Length;
258
+ while (capacity <= index)
235
259
  {
236
- _values.Add(null);
260
+ if (capacity > int.MaxValue >> 1)
261
+ {
262
+ throw new InvalidOperationException(
263
+ "The message type index exceeds the cache's supported capacity."
264
+ );
265
+ }
266
+ capacity <<= 1;
237
267
  }
268
+
269
+ Array.Resize(ref _values, capacity);
270
+ _count = index + 1;
238
271
  }
239
272
  }
240
273
  }
@@ -68,6 +68,23 @@ namespace DxMessaging.Core.Internal
68
68
  /// </summary>
69
69
  internal abstract class FlatDispatchArray
70
70
  {
71
+ internal abstract int Count { get; }
72
+
73
+ /// <summary>
74
+ /// Physical length of the currently rented entry array. Exposed only for internal
75
+ /// storage benchmarks; dispatch continues to use <see cref="Count"/> as its bound.
76
+ /// </summary>
77
+ internal abstract int Capacity { get; }
78
+
79
+ /// <summary>
80
+ /// Number of released holders currently retained by this holder shape's pool. Released
81
+ /// holders have already returned their rented arrays and hold <c>Array.Empty</c>,
82
+ /// so this is empty-holder pool topology, not retained array-memory evidence. Array-pool
83
+ /// retained bytes require external memory measurement. This benchmark telemetry does not
84
+ /// participate in rent/release decisions.
85
+ /// </summary>
86
+ internal abstract int EmptyHolderPoolCount { get; }
87
+
71
88
  internal abstract void Release();
72
89
  }
73
90
 
@@ -108,6 +125,21 @@ namespace DxMessaging.Core.Internal
108
125
  internal TEntry[] entries = Array.Empty<TEntry>();
109
126
  internal int count;
110
127
 
128
+ internal override int Count => count;
129
+
130
+ internal override int Capacity => entries.Length;
131
+
132
+ internal override int EmptyHolderPoolCount
133
+ {
134
+ get
135
+ {
136
+ lock (HolderPoolLock)
137
+ {
138
+ return HolderPool.Count;
139
+ }
140
+ }
141
+ }
142
+
111
143
  // True while the holder is parked in (or eligible for) the pool;
112
144
  // false while it is owned by a live DispatchSnapshot. Guards the
113
145
  // rent/release lifecycle against double-release and rent-of-live.
@@ -28,13 +28,9 @@ namespace DxMessaging.Core.Internal
28
28
  /// surface picks up no new members from the interface retrofit.
29
29
  /// </para>
30
30
  /// <para>
31
- /// Deliberately a thin, marker-style surface: only the five members that
32
- /// staged dispatch (<see cref="Version"/>, <see cref="LastSeenVersion"/>,
33
- /// <see cref="LastSeenEmissionId"/>) and eviction
34
- /// (<see cref="IsEmpty"/>, <see cref="Reset"/>) require. The
35
- /// <c>entries</c> dictionary and <c>cache</c> list are NOT exposed
36
- /// because their generic shape is the very thing this interface erases;
37
- /// dispatchers that need the typed cache down-cast at the call site.
31
+ /// Deliberately a thin erased lifecycle surface for staged dispatch and eviction.
32
+ /// Generic keys, values, snapshot lists, and representation diagnostics are not
33
+ /// exposed; dispatchers that need typed entries down-cast at the call site.
38
34
  /// </para>
39
35
  /// </remarks>
40
36
  internal interface IHandlerActionCache