com.wallstop-studios.dxmessaging 3.1.0 → 3.2.0

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 (85) hide show
  1. package/CHANGELOG.md +286 -0
  2. package/Editor/Analyzers/BaseCallLogMessageParser.cs.meta +3 -3
  3. package/Editor/Analyzers/BaseCallReportAggregator.cs.meta +3 -3
  4. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs.meta +3 -3
  5. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs.meta +3 -3
  6. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs.meta +3 -3
  7. package/Editor/DxMessagingMenu.cs.meta +3 -3
  8. package/Editor/DxMessagingSceneBuildProcessor.cs.meta +3 -3
  9. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs +2 -3
  10. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs.meta +3 -3
  11. package/Editor/SetupCscRsp.cs +232 -163
  12. package/README.md +1 -1
  13. package/Runtime/Analyzers.meta +8 -0
  14. package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs.meta +3 -3
  15. package/Runtime/Core/DxMessagingStaticState.cs.meta +3 -3
  16. package/Runtime/Core/Extensions/MessageBusExtensions.cs.meta +3 -3
  17. package/Runtime/Core/Extensions/MessageExtensions.cs.meta +3 -3
  18. package/Runtime/Core/IMessage.cs.meta +3 -3
  19. package/Runtime/Core/InstanceId.cs.meta +3 -3
  20. package/Runtime/Core/Internal/TypedSlots.cs +42 -0
  21. package/Runtime/Core/MessageBus/DiagnosticsTarget.cs.meta +3 -3
  22. package/Runtime/Core/MessageBus/GlobalMessageBusProvider.cs.meta +3 -3
  23. package/Runtime/Core/MessageBus/IMessageBus.cs +63 -28
  24. package/Runtime/Core/MessageBus/IMessageBus.cs.meta +3 -3
  25. package/Runtime/Core/MessageBus/IMessageBusProvider.cs.meta +3 -3
  26. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs.meta +3 -3
  27. package/Runtime/Core/MessageBus/MessageBus.cs +548 -474
  28. package/Runtime/Core/MessageBus/MessageBus.cs.meta +3 -3
  29. package/Runtime/Core/MessageBus/MessageBusRebindMode.cs.meta +3 -3
  30. package/Runtime/Core/MessageBus/MessageBusRegistration.cs +207 -0
  31. package/Runtime/Core/MessageBus/MessageBusRegistration.cs.meta +11 -0
  32. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs.meta +3 -3
  33. package/Runtime/Core/MessageBus/MessagingRegistration.cs.meta +3 -3
  34. package/Runtime/Core/MessageBus/RegistrationLog.cs.meta +3 -3
  35. package/Runtime/Core/MessageHandler.cs +1210 -394
  36. package/Runtime/Core/MessageHandler.cs.meta +3 -3
  37. package/Runtime/Core/MessageRegistrationHandle.cs.meta +3 -3
  38. package/Runtime/Core/MessageRegistrationToken.cs +1296 -819
  39. package/Runtime/Core/MessageRegistrationToken.cs.meta +3 -3
  40. package/Runtime/Core/Messages/IBroadcastMessage.cs.meta +3 -3
  41. package/Runtime/Core/Messages/ITargetedMessage.cs.meta +3 -3
  42. package/Runtime/Core/Messages/IUntargetedMessage.cs.meta +3 -3
  43. package/Runtime/Core/Messages/SourcedStringMessage.cs.meta +3 -3
  44. package/Runtime/Core/MessagingDebug.cs.meta +3 -3
  45. package/Runtime/Unity/CurrentGlobalMessageBusProvider.cs.meta +3 -3
  46. package/Runtime/Unity/DxMessagingRuntimeInitializer.cs.meta +3 -3
  47. package/Runtime/Unity/InitialGlobalMessageBusProvider.cs.meta +3 -3
  48. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs.meta +3 -3
  49. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs.meta +3 -3
  50. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs.meta +3 -3
  51. package/Runtime/Unity/MessageAwareComponent.cs.meta +3 -3
  52. package/Runtime/Unity/MessageBusProviderHandle.cs.meta +3 -3
  53. package/Runtime/Unity/MessagingComponent.cs.meta +3 -3
  54. package/Runtime/Unity/MessagingComponentInstaller.cs.meta +3 -3
  55. package/Runtime/Unity/ScriptableMessageBusProvider.cs.meta +3 -3
  56. package/Samples~/DI/Reflex/SampleInstaller.cs.meta +3 -3
  57. package/Samples~/DI/VContainer/SampleLifetimeScope.cs.meta +3 -3
  58. package/Samples~/DI/Zenject/SampleInstaller.cs.meta +3 -3
  59. package/Samples~/Mini Combat/Boot.cs.meta +3 -3
  60. package/Samples~/Mini Combat/Enemy.cs.meta +3 -3
  61. package/Samples~/Mini Combat/Messages.cs.meta +3 -3
  62. package/Samples~/Mini Combat/Player.cs.meta +3 -3
  63. package/Samples~/Mini Combat/UIOverlay.cs.meta +3 -3
  64. package/Samples~/UI Buttons + Inspector/DiagnosticsEnabler.cs.meta +3 -3
  65. package/Samples~/UI Buttons + Inspector/Messages.cs.meta +3 -3
  66. package/Samples~/UI Buttons + Inspector/MessagingObserver.cs.meta +3 -3
  67. package/Samples~/UI Buttons + Inspector/UIButtonEmitter.cs.meta +3 -3
  68. package/SourceGenerators/Directory.Build.props +5 -5
  69. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs.meta +3 -3
  70. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj +12 -11
  71. package/package.json +6 -3
  72. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.CSharp.dll +0 -0
  73. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +0 -0
  74. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.dll +0 -0
  75. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.dll.meta +0 -0
  76. /package/{Editor → Runtime}/Analyzers/System.Collections.Immutable.dll +0 -0
  77. /package/{Editor → Runtime}/Analyzers/System.Collections.Immutable.dll.meta +0 -0
  78. /package/{Editor → Runtime}/Analyzers/System.Reflection.Metadata.dll +0 -0
  79. /package/{Editor → Runtime}/Analyzers/System.Reflection.Metadata.dll.meta +0 -0
  80. /package/{Editor → Runtime}/Analyzers/System.Runtime.CompilerServices.Unsafe.dll +0 -0
  81. /package/{Editor → Runtime}/Analyzers/System.Runtime.CompilerServices.Unsafe.dll.meta +0 -0
  82. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll +0 -0
  83. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll.meta +0 -0
  84. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
  85. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll.meta +0 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,292 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.2.0]
11
+
12
+ ### Added
13
+
14
+ - Three per-kind **marginal registration** benchmark scenarios
15
+ (`UntargetedRegistration_Marginal`, `TargetedRegistration_Marginal`,
16
+ `BroadcastRegistration_Marginal`) that measure the GC-allocation cost of an additional
17
+ same-type registration on an already-warm bus -- the surface the registration
18
+ allocation reductions target -- so the published performance tables now show per-kind
19
+ registration cost (allocation count + bytes), not just dispatch throughput. Each
20
+ registers 1000 more handlers of one warmed message type using distinct, pre-built
21
+ handler delegates, so the measured window captures only the registration machinery
22
+ (never the handler delegate, and never a same-handler refcount bump). They are
23
+ wall-clock (zero-throughput) rows whose allocation columns populate on the
24
+ profiler-bearing in-editor PlayMode/Mono leg and read `n/a` on the published Standalone
25
+ IL2CPP Release leg (which strips the profiler), exactly like the existing
26
+ registration-flood rows.
27
+ - Benchmarks now report the total allocated BYTES per measurement batch (`gcAllocatedBytes`)
28
+ alongside the existing managed-allocation CALL count (`gcAllocations`). Bytes are
29
+ measured from a before/after delta of the live Unity
30
+ `ProfilerRecorder(ProfilerCategory.Memory, "GC Allocated In Frame").CurrentValue`
31
+ -- a within-frame `GC.Alloc`-hook byte accumulator that SUMS allocation-hook bytes
32
+ rather than measuring a heap-size difference, so it is exact (verified: 100 x
33
+ `byte[10000]` reads 1,003,200 bytes, run-to-run identical) and immune to mid-window
34
+ collections (a heavy-churn region that swung a `GC.GetTotalMemory` delta to -133 MB
35
+ read a stable 8,000,000 bytes here). The byte counter is profiler-dependent
36
+ like the count probe: in the editor and development players it is functional, and on
37
+ the published non-development Standalone IL2CPP Release leg -- where the profiler is
38
+ stripped -- BOTH metrics report the `Unmeasured` sentinel (rendered `n/a`), never a
39
+ fabricated `0`. Bytes are INFORMATIONAL -- the perf-delta PR comment renders byte
40
+ deltas goodness-signed (`N fewer bytes` / `N more bytes`) -- while the regression gate
41
+ stays on the allocation COUNT, which remains the canonical signal (Unity's own
42
+ Performance Testing package likewise reads the alloc-CALL count, not bytes). The
43
+ cross-library comparison tables now also surface this byte metric as a dedicated
44
+ GC-allocated-bytes matrix, and the Unity `SendMessage` comparison boxes its value
45
+ payload on every dispatch (as any real `SendMessage(value)` call must) so its
46
+ allocation columns show that real per-call cost instead of a misleading zero. New API:
47
+ `AllocationProbe.MeasureWithBytes` returning `AllocationProbe.AllocationSample`,
48
+ `AllocationProbe.Window.SampleBytes()`/`SampleBoth()`,
49
+ `AllocationProbe.BytesFunctional`, `BenchmarkMeasurement.GcAllocatedBytes`,
50
+ `ColdLatencyMeasurement.MedianGcAllocatedBytes`, and
51
+ `MinimumMeasurement<T>.GcAllocatedBytes`.
52
+ - New "What's New in 3.x" documentation page (Getting Started) that summarizes
53
+ the user-visible improvements across the 3.x line -- faster zero-allocation
54
+ dispatch, the base-call analyzer and inspector overlay, project-wide settings,
55
+ memory-reclamation controls, `ReregisterOnEnableAfterRelease`, the clarified
56
+ `ToggleMessageHandler` semantics, Unity 6.4+/6.5 compatibility, and the
57
+ dependency-injection helpers -- with links to the authoritative changelog and
58
+ the migration guide.
59
+
60
+ ### Fixed
61
+
62
+ - The token-creation allocation guard no longer flakes in a warm editor. The
63
+ diagnostics-lazy win (token `Create` allocating 7 managed objects instead of 11) was
64
+ guarded by an absolute `GC.Alloc` recorder COUNT budget, but a warm, long-lived editor
65
+ domain attributes background allocations to whatever measurement window is open --
66
+ measured on the host editor, the minimum over 64 windows was ~19 allocations for that
67
+ 7-allocation operation (median ~51), well above any budget tight enough to catch the
68
+ revert -- so the guard false-failed run-to-run. A measure-first probe also disproved the
69
+ assumption that the swing came from the `DxPools` collection pools: the steady refcount
70
+ registration path does not rent from them at all (hits = misses = 0), so the noise is
71
+ pure background-editor `GC.Alloc` pollution that no pool pre-warm can remove. The guard
72
+ is now a DETERMINISTIC state assertion -- after `Create` with diagnostics off, the lazy
73
+ `_callCounts`/`_emissionBuffer` backing fields must still be `null` -- which uses no
74
+ allocation probe, never flakes, and runs in the per-PR EditMode correctness leg rather
75
+ than the weekly perf-gated Allocation scope.
76
+ - Two more memory-reclamation/registration allocation guards no longer flake in a warm
77
+ editor. The forced-trim guard measured a `GC.Alloc` COUNT budget over a 32-trim window
78
+ and the dirty-target reuse guard a per-batch count budget; both false-failed run-to-run
79
+ on the same warm-editor ambient-noise floor that the token-creation guard hit. The
80
+ forced-trim guard is now a DETERMINISTIC assertion on the exact `IMessageBus.TrimResult`
81
+ eviction counts (the first force-trim reclaims; every subsequent one is an idempotent
82
+ no-op that evicts nothing and leaves the live slot count stable) plus a `DxPools`
83
+ Misses-flat check (repeated no-op trims rent no fresh pooled collection); the
84
+ dirty-target guard now relies solely on its existing exact pool Hits/Misses assertions
85
+ (renting the warmed collection, never allocating a fresh one). Neither uses an allocation
86
+ probe, so neither flakes. The two deterministic registration STORAGE structural guards
87
+ (the by-value metadata parameter and the staging-function map value type) were also moved
88
+ from the weekly perf-gated Allocation suite into the per-PR EditMode correctness leg, so
89
+ they protect every PR. Internal test/quality change only; no public API or runtime
90
+ behavior change.
91
+ - Benchmark and library-comparison allocation reporting is now honest. The harness
92
+ measured allocations with `GC.GetAllocatedBytesForCurrentThread()`, which returns
93
+ `0` for every allocation under Unity's Boehm GC (verified: a forced 1 MB array
94
+ allocation read back a `0`-byte delta), so the published "allocated bytes" column
95
+ was a vacuous `0` for every technology -- hiding real per-operation allocations
96
+ (Unity `SendMessage`, for example, boxes its value-type payload once per call).
97
+ The metric is now a COUNT of managed allocations from the reliable `GC.Alloc`
98
+ profiler recorder (new `AllocationProbe`), renamed `gcAllocations`, and reports an
99
+ `Unmeasured` sentinel (rendered `n/a`) -- never a fabricated `0` -- when no probe
100
+ is available on a backend. The perf-doc/PR-comment renderers, the regression gate,
101
+ and the methodology docs were updated accordingly; the doc's allocation column
102
+ reads `n/a` until the next CI run repopulates real counts.
103
+ - Source generators (`[DxUntargetedMessage]`, `[DxTargetedMessage]`,
104
+ `[DxBroadcastMessage]`, `[DxAutoConstructor]`) now work in projects that do not use
105
+ assembly definitions. The generator and analyzer DLLs previously shipped under the
106
+ editor-only `Editor/Analyzers/` folder, which Unity scopes to the package's editor
107
+ assembly and assemblies that reference it -- so a consumer's runtime code in
108
+ `Assembly-CSharp` (or a runtime asmdef referencing only the runtime assembly) never
109
+ received the generator, and `[Dx*Message]` types failed to implement their generated
110
+ interface with cryptic `CS0315`/`CS0452` errors. The labeled DLLs now ship under
111
+ `Runtime/Analyzers/` (governed by the all-platforms runtime assembly), so Unity
112
+ applies the generator to the DxMessaging runtime assembly and every assembly that
113
+ references it, including the predefined `Assembly-CSharp`. No assembly definition is
114
+ required. Closes GitHub issue #229.
115
+ - Documentation no longer teaches the obsolete `IMessageBus.GlobalDiagnosticsMode`
116
+ API. The reference, patterns, glossary, and migration guides now use
117
+ `IMessageBus.GlobalDiagnosticsTargets` with the `DiagnosticsTarget` flags enum
118
+ (matching the canonical diagnostics guide), and the API reference no longer shows a
119
+ write to the read-only `IMessageBus.DiagnosticsMode` property. A source-derived
120
+ drift-guard (`DocsObsoleteApiReferenceTests`) now fails the build if any published
121
+ doc references a member marked `[Obsolete]` in the runtime, so this cannot regress.
122
+
123
+ ### Changed
124
+
125
+ - Registration now allocates one fewer managed object per handler in the common case. The
126
+ registration token stored every handle's de-registration in a per-handle holder object,
127
+ but the common case is EXACTLY ONE de-registration per handle, so the single
128
+ de-registration delegate is now stored INLINE in the token's de-registration map (with no
129
+ holder object); a holder is allocated only when a rare second de-registration accumulates
130
+ on the same handle (the re-entrant retarget-recovery replay). The ordering,
131
+ partial-failure, and rollback-baseline semantics are unchanged (pinned by the existing
132
+ `PendingDeregistrationStorageTests` and the full re-entrancy suite), and a new
133
+ `RegistrationStorageStructuralGuardTests.DeregistrationsStoreInlineActionNotPerHandleHolder`
134
+ deterministically guards the inline storage against regression. Internal change only; no
135
+ public API or behavior change.
136
+ - **BREAKING (v4):** the `IMessageBus` registration contract now returns an opaque,
137
+ zero-allocation handle instead of a deregistration delegate. The 14
138
+ `IMessageBus.Register*<T>` methods return a new `readonly struct`
139
+ `DxMessaging.Core.MessageBus.MessageBusRegistration` (previously `System.Action`), and a
140
+ new `void IMessageBus.Deregister<T>(in MessageBusRegistration registration) where T : IMessage`
141
+ undoes a registration (call it with the same `T` you registered with; for
142
+ `RegisterGlobalAcceptAll`, any `T : IMessage` works since the global slot is not
143
+ type-keyed). This removes the per-registration bus-side deregistration **closure** (the
144
+ prior `Action` plus its display class) from the handler registration path -- roughly
145
+ **two fewer managed allocations per registration** -- by packing the deregistration
146
+ snapshot into the returned value handle and re-expressing the bus's deregistration logic
147
+ against it (behaviour and the four reentrancy invariants -- generation guard, identity
148
+ liveness / over-deregistration, token idempotency, no counter underflow -- are unchanged).
149
+ Custom `IMessageBus` implementers that wrap `MessageBus` (the
150
+ `DelegatingMessageBus`-style extension point) just forward the new members; from-scratch
151
+ implementers mint handles via the public `MessageBusRegistration(long, object)`
152
+ constructor and read them back in their own `Deregister<T>`.
153
+ **The Unity-facing surface is unaffected:** `MessageRegistrationToken` and
154
+ `MessageAwareComponent` keep their existing public shapes. The `MessageHandler.Register*`
155
+ facades were narrowed from `public` to `internal` (they now return the internal
156
+ `HandlerDeregistration`, still implicitly an `Action`) as part of this bus<->handler
157
+ boundary rework -- see the dedicated entry below; only code that called those facades
158
+ **directly** is affected. The larger per-registration allocation reduction (collapsing the
159
+ token/handler closures onto a single per-handle `Registration` object) landed on top of this
160
+ contract change -- see the entries below.
161
+ - The published performance report no longer prints columns of `n/a`. The
162
+ Standalone IL2CPP leg runs in a Release player whose stripped profiler cannot
163
+ measure GC allocations or bytes, so the renderer now OMITS a memory column from a
164
+ per-scope dispatch table when every row is unmeasured (the Standalone table is
165
+ throughput-only), omits a whole cross-library memory matrix when no leg measured
166
+ that metric, and drops the unmeasured allocation/byte segment from each per-PR
167
+ delta cell -- instead of filling them with a wall of `n/a`. The real allocation
168
+ and byte numbers still publish from the in-editor PlayMode (Mono) leg, and `n/a`
169
+ now appears only as a genuine per-row or per-library cell (a metric measured for
170
+ the scope in general but missing for that one entry).
171
+ - Registration allocates less. Each registration token's diagnostics-only
172
+ call-count and emission-history collections are now created lazily instead of
173
+ eagerly, so a token whose owner never enables diagnostics (the default) no longer
174
+ pays for them -- token creation drops from 11 to 7 managed allocations. The
175
+ per-registration metadata is now passed by value instead of through a closure
176
+ factory that was invoked immediately, removing one delegate allocation per handler
177
+ registration. Steady-state dispatch stays allocation-free, and diagnostics, the
178
+ inspector overlay, and all lifecycle behavior are unchanged.
179
+ - Registering a handler OR a post-processor with the convenience `Action<T>` /
180
+ `Action<InstanceId, T>` overloads now allocates one fewer closure per registration.
181
+ This covers the handler registers (`RegisterUntargeted`, `RegisterTargeted`,
182
+ `RegisterTargetedWithoutTargeting`, the sourced-broadcast register, and
183
+ `RegisterBroadcastWithoutSource`) and the matching post-processor registers
184
+ (`RegisterTargetedPostProcessor`, `RegisterTargetedWithoutTargetingPostProcessor`, the
185
+ GameObject/Component/source broadcast post-processors, and
186
+ `RegisterBroadcastWithoutSourcePostProcessor`). Each such registration previously built
187
+ two delegates -- a diagnostics-augmented `Action` wrapper plus a separate by-ref
188
+ `FastHandler` adapter for the flat-dispatch path -- and now folds diagnostics into a
189
+ single by-ref `FastHandler` closure that is the dispatch target directly (also removing a
190
+ per-dispatch indirection). The public API is unchanged; the optimization is internal and
191
+ verified by differential allocation guards that pin the `Action` registration cost to the
192
+ already-optimal `FastHandler` registration cost. Diagnostics, deduplication, and dispatch
193
+ ordering are preserved.
194
+ - Every registration kind now allocates about two fewer managed objects. The token used to
195
+ wrap each staged registration in a per-registration parameterless `Action` (a delegate plus
196
+ its display class) whose only job was to re-bundle the handle, the staging function, and the
197
+ de-registration bookkeeping; the token now stores each staging function directly and pairs it
198
+ with its handle in the replay queue, so that wrapper -- and the closure `InternalRegister`
199
+ needed to build it -- is gone. Measured cold-registration floor (FastHandler, diagnostics off):
200
+ untargeted drops from 14.69 to 12.69 managed allocations per registration (a clean -2.00), with
201
+ the same ~2-allocation reduction across targeted, broadcast, without-targeting/source, and
202
+ post-processor registrations (~12% fewer registration allocations overall). The de-registration
203
+ replay, rollback-on-failure, re-entrancy, and equal-priority registration-order semantics are
204
+ unchanged; the public API is unchanged. Pinned structurally by a deterministic guard that the
205
+ token stores the staging function (not an `Action` wrapper).
206
+ - Every active registration now allocates about two fewer managed objects again. The token
207
+ tracked each handle's live de-registration in a per-handle holder that eagerly allocated a
208
+ `List` (plus its backing array) to hold what is almost always a single de-registration; the
209
+ holder now keeps that one de-registration inline and only allocates an overflow list on the
210
+ rare second de-registration for the same handle. Measured cold-registration floor (FastHandler,
211
+ diagnostics off): untargeted drops from 13.29 to 11.29 managed allocations per registration (a
212
+ clean -2.00), with the same ~2-allocation reduction across targeted and broadcast. The
213
+ insertion-order, partial-failure-retryable, and rollback-baseline de-registration semantics are
214
+ unchanged (verified by a differential simulation across every count/failure/start-index
215
+ combination); the public API is unchanged.
216
+ - Each handler/post-processor registration now allocates one fewer managed object by replacing
217
+ its per-registration de-registration **closure** with a per-handle **object**. The typed
218
+ handler used to hand back a captured parameterless `Action` (a delegate plus the display class
219
+ holding the slot, cache key, priority, and generation it closed over) as the thing the token
220
+ would later invoke to tear the registration down; that closure is now a single per-handle
221
+ `HandlerDeregistration` object whose `Deregister()` instance method carries the same captured
222
+ state as plain fields (and converts implicitly to `Action`, so the token's replay/rollback
223
+ machinery is untouched). To let one non-generic object tear down a generic
224
+ `HandlerActionCache<TU>` without re-introducing a per-handler-delegate-type closure, the erased
225
+ `IHandlerActionCache` gained three non-generic operations (`ContainsEntry`, `BumpVersion`,
226
+ `DeregisterEntry`). The de-registration **order** (generation guard, slot-version liveness,
227
+ keyed-vs-scalar split, identity/over-de-registration check, version bump, bus de-register,
228
+ refcount decrement) is a faithful re-expression of the prior closure body, pinned by the 19
229
+ arbitrary-order `MixedOrderDeregistrationTests` and the full re-entrancy suite. Because these
230
+ `MessageHandler.Register*` facades now return the internal `HandlerDeregistration` (still
231
+ implicitly an `Action`), they were narrowed from `public` to `internal` -- this is part of the
232
+ v4 bus<->handler boundary rework and does NOT touch the Unity-facing surface
233
+ (`MessageRegistrationToken`, `MessageAwareComponent`); only code calling
234
+ `MessageHandler.Register*` **directly** is affected.
235
+ - Every registration kind now allocates roughly half as many managed objects again by
236
+ collapsing the token's per-handle staging closures into one unified per-handle object.
237
+ Each registration previously staged a `Func<handle, HandlerDeregistration>` (a delegate plus
238
+ its display class capturing the target/source, user handler, and priority) whose nested
239
+ `AugmentedHandler` local function became a _second_ delegate (the diagnostics-augmented flat
240
+ invoker). Both are now a single per-handle `Registration` object stored in `_registrations`:
241
+ its fields hold the captured staging state, and its diagnostics-augmented invoker is an
242
+ _instance method_ bound to the object -- so `MessageHandler` still receives a `FastHandler<T>`
243
+ delegate and the hot dispatch path stays delegate-based (no virtual/interface call per
244
+ dispatch). A `Register()` method runs a kind-switch to call the matching
245
+ `MessageHandler.Register*` and reproduce the exact prior staging body per kind. Because the
246
+ constrained `MessageHandler.Register*<T>` calls require `T : ITargetedMessage` /
247
+ `IUntargetedMessage` / `IBroadcastMessage` (which a single `Registration<T> where T : IMessage`
248
+ cannot satisfy), the object is realized as three constraint-family generic subclasses plus one
249
+ non-generic global-accept-all subclass over a non-generic `Registration` base -- still a
250
+ unified per-handle object with a kind-switch, not a per-method subclass explosion. Measured
251
+ marginal cost (cold, FastHandler, diagnostics off) drops from ~9.3 to ~4.6 managed allocations
252
+ per registration (about half). The equal-priority registration-order dispatch, idempotent
253
+ double-deregister, partial-failure rollback, generation/slot-version guards, refcount handlers,
254
+ and diagnostics call-counts/emission semantics are all unchanged (pinned by the 19
255
+ `MixedOrderDeregistrationTests` and the full re-entrancy suite); the public API is unchanged.
256
+ Pinned structurally by a deterministic guard that `_registrations` stores the unified
257
+ `Registration` object, not a `Func`/`Action` wrapper.
258
+ - Deregistration is now O(1) on the steady path. The v4 bus `Deregister<T>` had been
259
+ re-resolving the sink from the registration method (a `switch` plus `ScalarSinkForMethod` /
260
+ `ContextSinkForMethod`) and re-walking the whole sink -> type -> [context ->] priority ->
261
+ handler chain on every call -- even though the leaf handler-cache is already captured on the
262
+ registration handle. It now operates on that captured cache DIRECTLY, deferring the
263
+ re-resolution to a cold fallback taken only when the handler is not found there (the rare
264
+ post-sweep or over-deregistration case, which the fallback classifies as a silent no-op vs an
265
+ error). This removes the per-deregistration re-resolution that had regressed the
266
+ deregistration-heavy teardown path, with no change to dispatch, registration allocations, the
267
+ generation/sweep-staleness/over-deregistration guards, or the public API; the throw-safe
268
+ ordering (the handler-cache mutation stays after the throwing `IMessageBus.Deregister`) is
269
+ unchanged. A stale over-deregistration whose `(type, priority)` slot was reused by a different
270
+ handler now leaves the live registration completely untouched (new
271
+ `StaleOverDeregistrationTests` regression coverage). The token's de-registration store also
272
+ records the common first-registration in a single dictionary operation (`TryAdd`) instead of a
273
+ probe-then-insert pair.
274
+ - The bug-report issue template now offers the package version as a dropdown of
275
+ released versions (with an `Other` fallback) instead of a free-text field, so
276
+ reports carry an exact, valid version. The list is generated from
277
+ `package.json`, `CHANGELOG.md`, and git tags, kept in sync by
278
+ `npm run check:issue-template-versions` (gated in `validate:all`), and
279
+ self-heals on the default branch via the `Update Issue Template Versions`
280
+ workflow. Closes GitHub issue #230.
281
+ - The Roslyn source generator no longer copies its DLLs into the consumer's
282
+ `Assets/Plugins/Editor/` folder on editor load; it (with its pinned Roslyn dependency
283
+ DLLs) ships ready-to-use under the package's `Runtime/Analyzers/` folder
284
+ (RoslynAnalyzer-labeled, excluded from player builds). Projects upgrading from an
285
+ earlier version have the redundant in-project copy removed automatically during asset
286
+ import, before script compilation, so the package's copy and the old in-project copy
287
+ never both run the generator (which would otherwise duplicate generated members). No
288
+ manual action is required: the cleanup only removes the
289
+ `Assets/Plugins/Editor/WallstopStudios.DxMessaging` folder when it contains the
290
+ first-party source-generator DLL plus exact known legacy analyzer/dependency DLL names
291
+ the package created, and leaves any foreign DLL, foreign `.meta`, subfolder, or
292
+ other content untouched. If a mixed or incomplete legacy payload is detected, the
293
+ editor logs one warning with manual cleanup guidance instead of deleting the folder
294
+ silently.
295
+
10
296
  ## [3.1.0]
11
297
 
12
298
  ### Added
@@ -6,6 +6,6 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -6,6 +6,6 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -6,6 +6,6 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -6,6 +6,6 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -6,6 +6,6 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -6,6 +6,6 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -6,6 +6,6 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -66,9 +66,8 @@ namespace DxMessaging.Editor.Settings
66
66
 
67
67
  /// <summary>
68
68
  /// Regenerates the sidecar text file from the supplied settings asset. Writes only when
69
- /// the on-disk content differs from what would be written, matching the
70
- /// <c>FilesDiffer</c>-style policy used elsewhere in this Editor assembly to avoid
71
- /// AssetDatabase churn during domain reload.
69
+ /// the on-disk content differs from what would be written, to avoid AssetDatabase churn
70
+ /// during domain reload.
72
71
  /// </summary>
73
72
  /// <param name="settings">The settings asset. May be <c>null</c> -- no-op in that case.</param>
74
73
  /// <remarks>
@@ -6,6 +6,6 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant: