com.wallstop-studios.dxmessaging 3.0.1 → 3.1.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 (81) hide show
  1. package/CHANGELOG.md +211 -2
  2. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs +69 -62
  3. package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +3 -3
  4. package/Editor/Analyzers/Microsoft.CodeAnalysis.dll.meta +3 -3
  5. package/Editor/Analyzers/System.Collections.Immutable.dll.meta +3 -3
  6. package/Editor/Analyzers/System.Reflection.Metadata.dll.meta +3 -3
  7. package/Editor/Analyzers/System.Runtime.CompilerServices.Unsafe.dll.meta +3 -3
  8. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll +0 -0
  9. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll.meta +15 -2
  10. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
  11. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll.meta +2 -2
  12. package/Editor/AssemblyInfo.cs.meta +9 -1
  13. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs +24 -15
  14. package/Editor/CustomEditors/MessagingComponentEditor.cs.meta +9 -1
  15. package/Editor/DxMessagingEditorIdle.cs +62 -0
  16. package/{Runtime/Core/Internal/TypedDispatchLinkIndex.cs.meta → Editor/DxMessagingEditorIdle.cs.meta} +1 -1
  17. package/Editor/DxMessagingEditorInitializer.cs +112 -15
  18. package/Editor/DxMessagingEditorInitializer.cs.meta +9 -1
  19. package/Editor/DxMessagingEditorLog.cs +32 -0
  20. package/Editor/DxMessagingEditorLog.cs.meta +11 -0
  21. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs +135 -12
  22. package/Editor/Settings/DxMessagingSettings.cs +92 -31
  23. package/Editor/Settings/DxMessagingSettings.cs.meta +9 -1
  24. package/Editor/Settings/DxMessagingSettingsProvider.cs.meta +9 -1
  25. package/Editor/SetupCscRsp.cs +339 -173
  26. package/Editor/SetupCscRsp.cs.meta +9 -1
  27. package/Editor/Testing/MessagingComponentEditorHarness.cs +1 -1
  28. package/Editor/Testing/MessagingComponentEditorHarness.cs.meta +9 -1
  29. package/README.md +17 -18
  30. package/Runtime/AssemblyInfo.cs.meta +9 -1
  31. package/Runtime/Core/Attributes/DxAutoConstructorAttribute.cs.meta +9 -1
  32. package/Runtime/Core/Attributes/DxBroadcastMessageAttribute.cs.meta +9 -1
  33. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs +2 -4
  34. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs.meta +9 -1
  35. package/Runtime/Core/Attributes/DxTargetedMessageAttribute.cs.meta +9 -1
  36. package/Runtime/Core/Attributes/DxUntargetedMessageAttribute.cs.meta +9 -1
  37. package/Runtime/Core/Attributes/Il2CppSetOptionAttribute.cs +56 -0
  38. package/Runtime/Core/Attributes/Il2CppSetOptionAttribute.cs.meta +11 -0
  39. package/Runtime/Core/DataStructure/CyclicBuffer.cs +44 -26
  40. package/Runtime/Core/DataStructure/CyclicBuffer.cs.meta +9 -1
  41. package/Runtime/Core/Diagnostics/MessageEmissionData.cs.meta +9 -1
  42. package/Runtime/Core/Diagnostics/MessageRegistrationData.cs.meta +9 -1
  43. package/Runtime/Core/Diagnostics/MessageRegistrationType.cs.meta +9 -1
  44. package/Runtime/Core/Extensions/EnumExtensions.cs +6 -5
  45. package/Runtime/Core/Extensions/EnumExtensions.cs.meta +9 -1
  46. package/Runtime/Core/Extensions/IListExtensions.cs.meta +9 -1
  47. package/Runtime/Core/Extensions/MessageExtensions.cs +0 -60
  48. package/Runtime/Core/Helper/MessageCache.cs.meta +9 -1
  49. package/Runtime/Core/Helper/MessageHelperIndexer.cs.meta +9 -1
  50. package/Runtime/Core/InstanceId.cs +25 -1
  51. package/Runtime/Core/Internal/DxUnsafe.cs +60 -0
  52. package/Runtime/Core/Internal/DxUnsafe.cs.meta +11 -0
  53. package/Runtime/Core/Internal/FlatDispatch.cs +198 -0
  54. package/Runtime/Core/Internal/FlatDispatch.cs.meta +11 -0
  55. package/Runtime/Core/Internal/TypedSlots.cs +5 -21
  56. package/Runtime/Core/MessageBus/IMessageBus.cs +12 -12
  57. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs +1 -0
  58. package/Runtime/Core/MessageBus/Internal/BusSlots.cs +7 -6
  59. package/Runtime/Core/MessageBus/MessageBus.cs +2313 -2936
  60. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs +187 -14
  61. package/Runtime/Core/MessageHandler.cs +1023 -1143
  62. package/Runtime/Core/MessageRegistrationToken.cs +425 -47
  63. package/Runtime/Core/Messages/GlobalStringMessage.cs.meta +9 -1
  64. package/Runtime/Core/Messages/ReflexiveMessage.cs.meta +9 -1
  65. package/Runtime/Core/Messages/StringMessage.cs.meta +9 -1
  66. package/Runtime/Unity/Integrations/Reflex/AssemblyInfo.cs.meta +9 -1
  67. package/Runtime/Unity/Integrations/VContainer/AssemblyInfo.cs.meta +9 -1
  68. package/Runtime/Unity/Integrations/Zenject/AssemblyInfo.cs.meta +9 -1
  69. package/Runtime/Unity/MessageAwareComponent.cs +46 -1
  70. package/Runtime/Unity/MessagingComponent.cs +43 -10
  71. package/Runtime/WallstopStudios.DxMessaging.asmdef +1 -1
  72. package/Samples~/DI/README.md +7 -7
  73. package/SourceGenerators/Directory.Build.props +50 -3
  74. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs +96 -63
  75. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs +745 -87
  76. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs.meta +9 -1
  77. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj +39 -46
  78. package/SourceGenerators/global.json +7 -0
  79. package/SourceGenerators/global.json.meta +7 -0
  80. package/package.json +27 -40
  81. package/Runtime/Core/Internal/TypedDispatchLinkIndex.cs +0 -51
package/CHANGELOG.md CHANGED
@@ -7,6 +7,215 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.1.0]
11
+
12
+ ### Added
13
+
14
+ - Deregistration-speed benchmarks (`DeregistrationFlood_1000Types_Cold` and
15
+ `DeregistrationFlood_1000Types_WarmJit`): the teardown mirror of the existing
16
+ registration floods. Each stages 1000 live registrations untimed, then times
17
+ `MessageRegistrationToken.UnregisterAll()` (the production deregistration path).
18
+ Both are wall-clock (latency) rows alongside the registration floods in the
19
+ rendered dispatch tables (closes the deregistration ask in GitHub issue #31).
20
+ - `MessageAwareComponent.ReregisterOnEnableAfterRelease`: opt-in virtual property
21
+ (default `false`, preserving existing behavior). When overridden to return
22
+ `true`, a component whose registration token was released (for example via
23
+ `MessagingComponent.Release`) re-creates its token and replays
24
+ `RegisterMessageHandlers` the next time it is enabled, instead of staying
25
+ permanently unregistered. The replay runs exactly once per release; plain
26
+ enable/disable cycles without a release never re-stage registrations.
27
+
28
+ ### Changed
29
+
30
+ - The Unity object identity backing `InstanceId` (the dispatch key) is now read
31
+ through a single version-gated source, `InstanceId.StableId`. On Unity 6.4+ it
32
+ uses the non-deprecated `EntityId.ToULong(...)` accessor and keeps its low 32
33
+ bits -- exactly the value the legacy `GetInstanceID()` returned (verified across
34
+ `GameObject`, `Component`, and `ScriptableObject`); older Unity keeps
35
+ `GetInstanceID()`. This keeps the package compiling on Unity 6.5+, where
36
+ `GetInstanceID()` becomes a compile error, and removes its deprecation warning on
37
+ Unity 6.4+. The 32-bit dispatch key, equality, and hashing are unchanged. Closes
38
+ GitHub issue #208.
39
+ - `MessagingComponent.ToggleMessageHandler(false)` is no longer silently ignored
40
+ while `emitMessagesWhenDisabled` is true: explicit toggle calls now always win,
41
+ in both directions. Instead, the Unity enable/disable lifecycle itself now skips
42
+ the handler toggle while `emitMessagesWhenDisabled` is true, so disabling the
43
+ component still keeps emission alive (the flag's documented purpose) AND an
44
+ explicit `ToggleMessageHandler(false)` is no longer reverted by a later
45
+ enable/disable cycle. Previously the veto made explicit deactivation requests
46
+ silent no-ops and `OnEnable` force-reactivated the handler. One consequence:
47
+ setting the flag while the handler is lifecycle-deactivated (disabled with the
48
+ flag clear) means a later enable no longer auto-reactivates - call
49
+ `ToggleMessageHandler(true)` to resume.
50
+
51
+ - Removed the internal per-handler dispatch-link machinery (the ten
52
+ `*DispatchLink` wrapper classes, their lazily-populated slot array, and
53
+ the outer-generation guard) plus the vestigial non-global prefreeze
54
+ descriptors that the flattened dispatch had already stopped consuming:
55
+ snapshots for non-global slots no longer build per-handler bucket entry
56
+ arrays at all (they dispatch exclusively through the resolved flat
57
+ delegate arrays and keep count-only buckets for the legacy "found any
58
+ handlers" reporting), which removes one pooled array rent/fill/return
59
+ per priority bucket from every snapshot rebuild. No public API or
60
+ dispatch semantics change (verified emission-for-emission against the
61
+ previous implementation, including global accept-all mid-emission
62
+ mutation ordering, trim-then-re-register staleness, reset-mid-dispatch,
63
+ and zero steady-state allocations).
64
+ - Each emission now consults a cached per-(bus, message-type, kind) dispatch
65
+ plan instead of re-resolving interceptor, global accept-all, and
66
+ post-processor sinks with multiple type-cache lookups per emit. When the
67
+ plan shows none of those features are present (the common case), a fast
68
+ emit lane runs only the handle phase: one plan fetch, one validity check,
69
+ snapshot acquisition, and the flat dispatch loop. Plans are invalidated by
70
+ a single bus-wide version stamp that every registration, deregistration,
71
+ interceptor/global/post mutation, sweep/Trim, `ResetState`, and runtime
72
+ settings reload bumps; mutations performed by handlers mid-emission are
73
+ re-detected at phase boundaries, so frozen-snapshot semantics, mid-emission
74
+ registration gating, interceptor re-targeting, and mid-dispatch reset
75
+ behavior are unchanged (verified emission-for-emission against the previous
76
+ implementation). Diagnostics mode and `MessagingDebug.enabled` are still
77
+ read live on every emission. Out-of-Unity rig measurements (directional):
78
+ one-handler throughput up roughly 1.5-1.7x per kind and four-handler
79
+ fan-out up ~35%, with feature-heavy paths unchanged within noise and zero
80
+ steady-state allocations.
81
+ - The internal flat-dispatch shape assertion (`DebugAssertFlatShape`) moved
82
+ from `DEBUG` builds to the opt-in `DXMESSAGING_INTERNAL_CHECKS` scripting
83
+ define; it cost a type test per dispatch on Editor hot paths.
84
+ - On IL2CPP players, the dispatch hot loops (flat snapshot walks, global
85
+ accept-all bucket walks, and global entry invokers) now opt out of the
86
+ generated per-iteration null and array-bounds checks via a vendored
87
+ internal `Unity.IL2CPP.CompilerServices.Il2CppSetOptionAttribute`. The
88
+ elided checks guard invariants the snapshot builder already guarantees
89
+ (frozen arrays, non-null handler/invoker pairs, `count` never exceeding
90
+ the array length), all pinned by tests; rig/diagnostic builds keep the
91
+ `DXMESSAGING_INTERNAL_CHECKS` shape assertions. No behavior change under
92
+ Mono or in the editor.
93
+ - On IL2CPP players, the per-emit AOT untyped-dispatch bridge registration
94
+ (`EnsureAot*Bridge<T>`, IL2CPP-only) now runs when a message type's dispatch
95
+ plan is first built on a bus (the first typed emit) instead of on every emit,
96
+ removing a generic-static class-initialization check and a non-inlined call
97
+ from the steady-state IL2CPP dispatch prologue. The registration is latched
98
+ process-globally, so it actually executes once per type; the guarded call site
99
+ is simply reached at first plan build rather than on every emit. The bridge is
100
+ rooted before the first untyped dispatch of a type by either any registration
101
+ of that type or its first typed emit, so the untyped-dispatch contract is
102
+ unchanged (a never-touched type still throws the same missing-bridge error).
103
+ No behavior change under Mono or in the editor (the calls compile out there).
104
+ - Dispatch for every message kind (untargeted, targeted, and broadcast;
105
+ handle and post-process phases) now resolves handlers to flat, pooled
106
+ delegate arrays at snapshot-build time instead of walking per-handler
107
+ dictionaries and dispatch links per message. Measured on Editor PlayMode
108
+ Mono x64 versus the previous release: one untargeted handler 17.5M to
109
+ 22.1M emits/sec, four untargeted handlers 3.9M to 20.0M, one targeted
110
+ listener 11.4M to 15.7M, sixteen targeted listeners 0.73M to 8.7M, one
111
+ broadcast handler 8.0M to 15.6M, four post-processors 3.3M to 12.6M -
112
+ all with zero steady-state allocations, an 8% faster cold registration
113
+ flood, and unchanged dispatch semantics (snapshot freezing, priority and
114
+ registration order, mid-emission registration visibility). One deliberate
115
+ refinement: a handler that deactivates itself mid-emission now skips its
116
+ own remaining delegates in that emission for every kind (the active check
117
+ runs per delegate instead of once per handler), matching the documented
118
+ immediate-deactivation semantics. Mid-emission registration gating is
119
+ also now uniform: a delegate registered during an emission never fires in
120
+ that emission, for every kind and every registration shape. Previously a
121
+ handler registering a different-shaped delegate for the same type on its
122
+ own MessageHandler could fire it in the same emission, depending on
123
+ unrelated handler counts.
124
+
125
+ ### Fixed
126
+
127
+ - Domain-reload advisory warnings are now re-evaluated after deferred Editor
128
+ settings creation/migration, so an initial passive load with no settings asset
129
+ cannot permanently skip an unsuppressed warning for that editor domain.
130
+ - Token cleanup now clears token-local diagnostics and stale teardown state:
131
+ `UnregisterAll()`, `Dispose()`, final-handle removal, released
132
+ `MessagingComponent` tokens, and disposed registration leases no longer
133
+ retain metadata, call counts, emission history, or stale deregistration
134
+ closures that could report old registrations or over-deregister later.
135
+ Failed `Enable()` replays now roll back partial registrations before
136
+ throwing the original failure; failed active `RetargetMessageBus()` replays
137
+ roll back partial new-bus registrations and restore previous-bus
138
+ registrations that are not still live behind a failed rollback cleanup.
139
+ Deregistration actions that throw before cleanup remain retryable instead
140
+ of being forgotten, including through owning registration leases and
141
+ `MessagingComponent.Release()` retries. `ActivateOnBuild` failures now
142
+ clean up the partially built lease before throwing again; if cleanup cannot
143
+ complete, `MessageRegistrationBuildException` exposes the retryable lease
144
+ so callers can dispose it after resolving the cleanup failure.
145
+ - Interceptors registered through a `MessageRegistrationToken` bound to a
146
+ custom bus (`MessageRegistrationToken.Create(handler, customBus)`) now land
147
+ on that bus. `RegisterUntargetedInterceptor`, `RegisterTargetedInterceptor`,
148
+ and `RegisterBroadcastInterceptor` previously dropped the token's bus and
149
+ registered on the handler's default (typically global) bus, so they never
150
+ saw custom-bus emissions and silently intercepted global traffic instead.
151
+ The fix also covers registrations staged while disabled (they activate on
152
+ the token's bus at `Enable()` time) and `RetargetMessageBus`, which now
153
+ re-routes interceptors along with every other registration kind.
154
+ - Registering or deregistering a handler mid-emission and then emitting the
155
+ same message type reentrant-style from inside a handler no longer corrupts
156
+ the in-flight dispatch.
157
+ The nested emission's snapshot rebuild previously released the pooled
158
+ arrays the outer emission was still iterating
159
+ (`NullReferenceException` / `ArgumentOutOfRangeException`, or silent
160
+ cross-dispatch aliasing at deeper nesting). Displaced snapshots are now
161
+ released only after the outermost dispatch exits, each emission keeps its
162
+ own frozen-cache identity across nested emissions, and the broadcast
163
+ priority walk tolerates nested membership churn.
164
+ - A `MessageRegistrationLease` whose `OnActivate` callback throws no longer
165
+ wedges its registrations. The lease previously recorded itself as inactive
166
+ while the registrations stayed live, so `Deactivate()` and `Dispose()`
167
+ silently refused to release them. The lease now marks itself active before
168
+ invoking the callback: the exception still propagates, and standard
169
+ `Deactivate()`/`Dispose()` teardown fully releases the registrations.
170
+ - Calling `DxMessagingStaticState.Reset` (or resetting a bus) from inside a
171
+ message handler no longer crashes the in-flight emission. Targeted and
172
+ broadcast dispatch previously returned the active emission's pooled snapshot
173
+ arrays mid-iteration (`NullReferenceException` /
174
+ `ArgumentOutOfRangeException`); the teardown is now deferred until the
175
+ outermost dispatch exits, mirroring the existing `Trim` deferral, and the
176
+ remaining handlers in that emission short-circuit cleanly.
177
+ - Equal-priority handlers now always dispatch in live registration order. The
178
+ documented "same priority uses registration order" contract previously broke
179
+ after remove/re-register churn (a new handler could dispatch in a removed
180
+ handler's old position), after `Disable()`/`Enable()` cycles (replay order
181
+ permuted), and across components (a newly created component could dispatch
182
+ in a destroyed component's old position). Handler caches, bus-side priority
183
+ buckets, and token replay now preserve insertion order explicitly.
184
+ - Targeted and broadcast post-processors now follow an interceptor-rewritten
185
+ target/source. When an interceptor redirects a message via its
186
+ `ref InstanceId` parameter, post-processors registered for the rewritten id
187
+ run (previously the broadcast path never re-resolved the rewritten source,
188
+ and the targeted path preferred a stale pre-interceptor snapshot, so the
189
+ rewritten id's post-processors were silently skipped).
190
+ - `DiagnosticsTarget` gating now matches its documented semantics. Player
191
+ builds previously enabled diagnostics for the `Editor` flag and ignored the
192
+ `Runtime` flag, and the Editor enabled diagnostics when only `Runtime` was
193
+ set. `Editor` now enables diagnostics only inside the Unity Editor,
194
+ `Runtime` only in player/runtime builds, and `All` in both, exactly as the
195
+ diagnostics guide describes.
196
+ - Analyzer payload builds are now reproducible under the pinned
197
+ `SourceGenerators/global.json` SDK. The shipped analyzer and source-generator
198
+ DLLs no longer embed source revision or PDB metadata, CI double-builds them
199
+ into temporary payload directories before comparing against
200
+ `Editor/Analyzers`, and Unity runner maintenance now verifies or repairs the
201
+ full active editor matrix outside ordinary test jobs.
202
+ - IL2CPP builds now root untyped dispatch bridges for concrete
203
+ source-visible message types without changing the public API. The source
204
+ generator emits IL2CPP-only AOT bridge registration for attributed messages
205
+ and manual `IUntargetedMessage` / `ITargetedMessage` /
206
+ `IBroadcastMessage` implementations, while open generic definitions are
207
+ skipped until a closed generic type is used through the typed registration
208
+ path. Untyped dispatch also keeps separate per-kind delegate caches, so a
209
+ message type that participates in more than one dispatch kind can no longer
210
+ reuse the wrong cached delegate shape.
211
+ - Unity projects no longer keep stale DxMessaging analyzer entries in `csc.rsp`; the setup script removes package-cache analyzer registrations so Unity loads the shipped analyzer and source generator once through the `RoslynAnalyzer`-labeled plugin copy. The base-call ignore sidecar's `-additionalfile:` entry is also re-synchronized after deferred sidecar regeneration, so first-load `OnValidate` writes and Inspector ignore-list edits repair missing `csc.rsp` wiring during the same editor session.
212
+ - Provider-backed emit helpers now route sourced, targeted, and untargeted messages through the resolved `IMessageBus`, so custom bus and DI-provider callers no longer fall back to the global bus for interface-shaped message dispatch.
213
+ - Standalone and IL2CPP player builds now compile. The dispatch hot path performs reinterpret casts that previously used `System.Runtime.CompilerServices.Unsafe`; the Unity Editor supplies that type, but player builds under the .NET Standard 2.0 profile do not, so editmode and playmode passed while standalone IL2CPP failed to build with `CS0103: The name 'Unsafe' does not exist in the current context`. Those calls now route through Unity's built-in `UnsafeUtility` (in `UnityEngine.CoreModule`), which resolves identically in the Editor and every player scripting backend. The change preserves the existing zero-allocation dispatch behavior and adds no package dependency or shipped assembly.
214
+ - Shipped source generators now compile against Unity 2021-compatible Roslyn 3.8 APIs and use the classic `ISourceGenerator` entry point, preventing Unity 2021.3 analyzer-host load failures (`CS8032`) while preserving the generated message-id and auto-constructor output for newer Unity editors.
215
+ - `MessageRegistrationToken.RemoveRegistration(handle)` now compiles cleanly on Unity 2021 while preserving the existing behavior of removing the active deregistration, staged registration, metadata, and diagnostic call-count entries.
216
+ - Unity 2021.3 no longer aborts compilation with _Multiple precompiled assemblies with the same name_ (`PrecompiledAssemblyException`). The shipped Roslyn analyzer and source-generator DLLs are now excluded from every build platform (the Editor included) and activated solely by the `RoslynAnalyzer` asset label, so Unity treats them as compiler analyzers rather than managed precompiled assemblies that collide with the copy placed in the consuming project's `Assets/`. Generated message-id and auto-constructor output is unchanged.
217
+ - The dependency-injection sample README (`Samples~/DI`) now links to the real `Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs`; the previous link dropped the `VContainer/` folder and resolved to a missing file.
218
+
10
219
  ## [3.0.1]
11
220
 
12
221
  ### Added
@@ -44,7 +253,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
44
253
  - `RegisterGlobalAcceptAll` (`HandleGlobalUntargeted`/`HandleGlobalTargeted`/`HandleGlobalBroadcast`) is intentionally NOT covered by this fix. The bus's global accept-all dispatch path prefreezes lazily per-entry inside the dispatch loop, so a sibling `MessageHandler` that removes another's global registration mid-emit causes the removed handler to be skipped on the in-flight emission. The behavior is pinned by `MutationPostProcessorAcrossHandlersTests.RemoveOtherGlobalAcceptAllAcrossHandlersDuringDispatch`; if a future change introduces upfront global-handler prefreeze, that test must be updated to expect the snapshot semantics that the per-kind paths already provide.
45
254
  - `DxMessagingStaticState.Reset` is now race-safe against deferred deregistrations. Previously, when a message-aware component was destroyed but its disable callback had not yet run (Unity defers Object.Destroy to end of frame) and Reset ran in between, the deferred token teardown would log spurious "Received over-deregistration of {type} for {handler}" errors against the user's Unity console. The bus now stamps each captured deregister closure with a generation counter and silently no-ops closures captured before a Reset. Applied uniformly across every register entry point (untargeted, targeted, broadcast, GlobalAcceptAll, and all three interceptor kinds). The same race-safety guarantee is now propagated to user-installed custom global buses via `MessageBus.BumpResetGeneration()`, which `DxMessagingStaticState.Reset` invokes on the active global bus when it differs from the built-in default; the custom bus's sinks are intentionally left intact to avoid clobbering state the user installed it to preserve. User code is unaffected except that previously-spurious error logs disappear.
46
255
  - `MessageRegistrationToken.RemoveRegistration(handle)` no longer leaks the staged registration entry, so a `Disable()`/`Enable()` cycle after `RemoveRegistration` no longer silently re-registers the removed handler. The fix also drops the matching metadata and call-count entries so diagnostic mode does not accumulate stale handles.
47
- - Resolved [issue #204](https://github.com/wallstop/DxMessaging/issues/204) (build artifacts and orphaned `.meta` files leaking into the npm tarball) and prevented its regression: `scripts/validate-npm-meta.js` now runs `validateNoBuildArtifactsInTarball` (rejects `bin/`, `obj/`, `*.pdb`, `*.tmp`, `*.csproj.user`, `.vs/`, `.idea/`, `*.suo`, and `*.DotSettings.user` paths in the tarball) and `validatePublishedFilesArePairedWithMetas` (every shipped Unity-relevant file has its `.meta` neighbour and every shipped directory has its directory `.meta`), wired into `prepack` and the `validate-npm-meta` workflow so the next publish cannot reintroduce the regression.
256
+ - Resolved [issue #204](https://github.com/Ambiguous-Interactive/DxMessaging/issues/204) (build artifacts and orphaned `.meta` files leaking into the npm tarball) and prevented its regression: `scripts/validate-npm-meta.js` validates real `npm pack --json --dry-run --ignore-scripts` output (and release tarballs via `--tarball`) by rejecting `bin/`, `obj/`, `*.pdb`, `*.tmp`, `*.csproj.user`, `.vs/`, `.idea/`, `*.suo`, `*.user`, and `*.DotSettings.user` paths, while enforcing `.meta` pairing for shipped Unity files and directories. The guard now runs in `validate:all`, pre-push hooks, and the release workflow.
48
257
 
49
258
  ## [2.2.0]
50
259
 
@@ -90,7 +299,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
90
299
  - Wiki synchronization workflow that automatically syncs documentation to GitHub Wiki
91
300
  - Documentation validation workflow that runs on pull requests and pushes
92
301
  - MkDocs build validation in pre-push hooks
93
- - Searchable documentation site at <https://wallstop.github.io/DxMessaging/>
302
+ - Searchable documentation site at <https://ambiguous-interactive.github.io/DxMessaging/>
94
303
  - Theme-aware Mermaid diagrams with automatic light/dark mode switching for GitHub Pages
95
304
  - User-visible error messages when Mermaid diagrams fail to render
96
305
 
@@ -340,10 +340,11 @@ namespace DxMessaging.Editor.Analyzers
340
340
 
341
341
  LoadFromDisk();
342
342
 
343
- // AssetDatabase isn't fully ready inside the static ctor; defer the first scan one
344
- // editor tick so settings load doesn't fight a transitional asset-import state.
345
- EditorApplication.delayCall += SafeRescanFromCallback;
346
- AssemblyReloadEvents.afterAssemblyReload += SafeRescanFromCallback;
343
+ // AssetDatabase isn't fully ready inside the static ctor; defer the first scan
344
+ // until Unity is idle so settings load doesn't fight a transitional asset-import
345
+ // state.
346
+ ScheduleRescanWhenIdle();
347
+ AssemblyReloadEvents.afterAssemblyReload += ScheduleRescanWhenIdle;
347
348
  CompilationPipeline.assemblyCompilationFinished += OnAssemblyCompilationFinished;
348
349
  if (!_logEntriesDisabled)
349
350
  {
@@ -352,8 +353,9 @@ namespace DxMessaging.Editor.Analyzers
352
353
  }
353
354
  catch (Exception ex)
354
355
  {
355
- Debug.LogWarning(
356
- $"[DxMessaging] DxMessagingConsoleHarvester failed to initialize: {ex.Message}"
356
+ DxMessagingEditorLog.LogWarning(
357
+ "DxMessagingConsoleHarvester failed to initialize.",
358
+ ex
357
359
  );
358
360
  _logEntriesDisabled = true;
359
361
  IsAvailable = false;
@@ -428,7 +430,7 @@ namespace DxMessaging.Editor.Analyzers
428
430
  }
429
431
  catch (Exception ex)
430
432
  {
431
- LogOnce("scanner", $"BaseCallTypeScanner.Scan threw: {ex.Message}");
433
+ LogExceptionOnce("scanner", "BaseCallTypeScanner.Scan threw.", ex);
432
434
  scannerEntries = new Dictionary<string, BaseCallReportEntry>(
433
435
  StringComparer.Ordinal
434
436
  );
@@ -492,7 +494,7 @@ namespace DxMessaging.Editor.Analyzers
492
494
  }
493
495
  catch (Exception ex)
494
496
  {
495
- LogOnce("aggregate", $"Snapshot merge failed: {ex.Message}");
497
+ LogExceptionOnce("aggregate", "Snapshot merge failed.", ex);
496
498
  // Fall through with the scanner-only snapshot; partial data is better than
497
499
  // wiping the snapshot when the bridge half misbehaves.
498
500
  }
@@ -616,7 +618,7 @@ namespace DxMessaging.Editor.Analyzers
616
618
  }
617
619
  catch (Exception ex)
618
620
  {
619
- LogOnce("getcount", $"GetCount invocation failed: {ex.Message}");
621
+ LogExceptionOnce("getcount", "GetCount invocation failed.", ex);
620
622
  return new Dictionary<string, ParsedTypeReport>(StringComparer.Ordinal);
621
623
  }
622
624
 
@@ -633,7 +635,7 @@ namespace DxMessaging.Editor.Analyzers
633
635
  }
634
636
  catch (Exception ex)
635
637
  {
636
- LogOnce("start", $"StartGettingEntries invocation failed: {ex.Message}");
638
+ LogExceptionOnce("start", "StartGettingEntries invocation failed.", ex);
637
639
  return new Dictionary<string, ParsedTypeReport>(StringComparer.Ordinal);
638
640
  }
639
641
 
@@ -652,8 +654,13 @@ namespace DxMessaging.Editor.Analyzers
652
654
  {
653
655
  harvestedCount = (int)_getCount.Invoke(null, null);
654
656
  }
655
- catch
657
+ catch (Exception ex)
656
658
  {
659
+ LogExceptionOnce(
660
+ "getcount-after-start",
661
+ "GetCount after StartGettingEntries failed.",
662
+ ex
663
+ );
657
664
  // Retain the previous count.
658
665
  }
659
666
  }
@@ -670,9 +677,10 @@ namespace DxMessaging.Editor.Analyzers
670
677
  }
671
678
  catch (Exception ex)
672
679
  {
673
- LogOnce(
680
+ LogExceptionOnce(
674
681
  "getentry",
675
- $"GetEntryInternal invocation failed at index {j}: {ex.Message}"
682
+ $"GetEntryInternal invocation failed at index {j}.",
683
+ ex
676
684
  );
677
685
  continue;
678
686
  }
@@ -684,9 +692,10 @@ namespace DxMessaging.Editor.Analyzers
684
692
  }
685
693
  catch (Exception ex)
686
694
  {
687
- LogOnce(
695
+ LogExceptionOnce(
688
696
  "getmessage",
689
- $"LogEntry.message read failed at index {j}: {ex.Message}"
697
+ $"LogEntry.message read failed at index {j}.",
698
+ ex
690
699
  );
691
700
  continue;
692
701
  }
@@ -699,7 +708,7 @@ namespace DxMessaging.Editor.Analyzers
699
708
  }
700
709
  catch (Exception ex)
701
710
  {
702
- LogOnce("harvest", $"Harvest loop failed: {ex.Message}");
711
+ LogExceptionOnce("harvest", "Harvest loop failed.", ex);
703
712
  }
704
713
  finally
705
714
  {
@@ -709,7 +718,7 @@ namespace DxMessaging.Editor.Analyzers
709
718
  }
710
719
  catch (Exception ex)
711
720
  {
712
- LogOnce("end", $"EndGettingEntries invocation failed: {ex.Message}");
721
+ LogExceptionOnce("end", "EndGettingEntries invocation failed.", ex);
713
722
  }
714
723
  }
715
724
 
@@ -720,9 +729,10 @@ namespace DxMessaging.Editor.Analyzers
720
729
  }
721
730
  catch (Exception ex)
722
731
  {
723
- LogOnce(
732
+ LogExceptionOnce(
724
733
  "aggregate-logentries",
725
- $"Aggregating LogEntries lines failed: {ex.Message}"
734
+ "Aggregating LogEntries lines failed.",
735
+ ex
726
736
  );
727
737
  return new Dictionary<string, ParsedTypeReport>(StringComparer.Ordinal);
728
738
  }
@@ -770,11 +780,7 @@ namespace DxMessaging.Editor.Analyzers
770
780
  // LogEntries is unavailable, Tick is not registered, so we fall back to delayCall.
771
781
  if (_logEntriesDisabled)
772
782
  {
773
- if (!_rescanScheduled)
774
- {
775
- _rescanScheduled = true;
776
- EditorApplication.delayCall += DrainScheduledRescan;
777
- }
783
+ ScheduleRescanWhenIdle();
778
784
  return;
779
785
  }
780
786
  _lastSeenCount = -1;
@@ -816,7 +822,7 @@ namespace DxMessaging.Editor.Analyzers
816
822
  }
817
823
  catch (Exception ex)
818
824
  {
819
- LogOnce("tick-count", $"GetCount during Tick failed: {ex.Message}");
825
+ LogExceptionOnce("tick-count", "GetCount during Tick failed.", ex);
820
826
  return;
821
827
  }
822
828
 
@@ -827,7 +833,7 @@ namespace DxMessaging.Editor.Analyzers
827
833
  }
828
834
  catch (Exception ex)
829
835
  {
830
- LogOnce("tick", $"Tick failed: {ex.Message}");
836
+ LogExceptionOnce("tick", "Tick failed.", ex);
831
837
  }
832
838
  }
833
839
 
@@ -841,27 +847,13 @@ namespace DxMessaging.Editor.Analyzers
841
847
  // are still compiling; the compiler holds its log-buffer lock and our reflection
842
848
  // call blocks waiting for it. Combined with AssetDatabase touches inside RescanNow,
843
849
  // this caused permanent script-compilation freezes on Unity startup.
844
- //
845
- // S4: when the legacy console-bridge is OFF, we don't need to parse CompilerMessage
846
- // payloads at all; the IL-reflection scanner is the sole data source and it runs
847
- // off the AssemblyReloadEvents.afterAssemblyReload hook that fires once per build,
848
- // not per-assembly. Bail out early so a 30-assembly build doesn't burn CPU running
849
- // the regex-heavy parser 30 times for output we'll never read. Read the setting once
850
- // up-front so the gate decision is consistent for the whole callback (settings can
851
- // be edited concurrently by the Project Settings page on the main thread).
852
- DxMessagingSettings settingsForGate = TryLoadSettings();
853
- bool bridgeEnabled = settingsForGate != null && settingsForGate._useConsoleBridge;
854
- if (!bridgeEnabled)
855
- {
856
- return;
857
- }
858
-
859
850
  // We DO parse the per-assembly CompilerMessage payload here (cheap, pure-CPU work,
860
- // no AssetDatabase / LogEntries contact) and stash it in the cross-thread channel.
861
- // DrainScheduledRescan (on a delayCall) folds the channel into the live snapshot
862
- // once the compile burst is complete. This is the primary data path on Unity 2021,
863
- // where Roslyn-analyzer warnings DO arrive in CompilerMessage[] but do NOT reliably
864
- // appear in the LogEntries store.
851
+ // no AssetDatabase / LogEntries contact) and stash it in the cross-thread channel
852
+ // without checking settings here. The settings gate requires AssetDatabase access, so
853
+ // RescanNow applies it later after DrainScheduledRescan has requeued until Unity is
854
+ // idle; if the bridge is disabled, RescanNow clears this pending buffer. This is the
855
+ // primary bridge data path on Unity 2021, where Roslyn-analyzer warnings DO arrive in
856
+ // CompilerMessage[] but do NOT reliably appear in the LogEntries store.
865
857
  try
866
858
  {
867
859
  if (!string.IsNullOrEmpty(assemblyPath) && messages != null)
@@ -899,17 +891,21 @@ namespace DxMessaging.Editor.Analyzers
899
891
  }
900
892
  catch (Exception ex)
901
893
  {
902
- LogOnce(
894
+ LogExceptionOnce(
903
895
  "compilation-parse",
904
- $"Failed to parse CompilerMessage payload for {assemblyPath}: {ex.Message}"
896
+ $"Failed to parse CompilerMessage payload for {assemblyPath}.",
897
+ ex
905
898
  );
906
899
  }
907
900
 
908
- // Fix: schedule a single delayCall. delayCall fires AFTER the current event chain
909
- // unwinds and AFTER `EditorApplication.isCompiling` flips back to false. Multiple
910
- // delayCall registrations from the same compile burst are debounced by the
911
- // _rescanScheduled latch; only one deferred RescanNow runs per build.
912
- if (_rescanScheduled)
901
+ // Schedule a single delayed drain. The callback rechecks editor state and requeues if
902
+ // Unity is still compiling/updating, so this never drops the final rescan.
903
+ ScheduleRescanWhenIdle();
904
+ }
905
+
906
+ internal static void ScheduleRescanWhenIdle()
907
+ {
908
+ if (!IsAvailable || _rescanScheduled)
913
909
  {
914
910
  return;
915
911
  }
@@ -942,7 +938,7 @@ namespace DxMessaging.Editor.Analyzers
942
938
  }
943
939
  catch (Exception ex)
944
940
  {
945
- LogOnce("rescan-callback", $"RescanNow callback threw: {ex.Message}");
941
+ LogExceptionOnce("rescan-callback", "RescanNow callback threw.", ex);
946
942
  }
947
943
  }
948
944
 
@@ -971,7 +967,7 @@ namespace DxMessaging.Editor.Analyzers
971
967
  }
972
968
  catch (Exception ex)
973
969
  {
974
- LogOnce("settings", $"Could not load DxMessagingSettings: {ex.Message}");
970
+ LogExceptionOnce("settings", "Could not load DxMessagingSettings.", ex);
975
971
  return null;
976
972
  }
977
973
  }
@@ -984,9 +980,10 @@ namespace DxMessaging.Editor.Analyzers
984
980
  }
985
981
  catch (Exception ex)
986
982
  {
987
- LogOnce(
983
+ LogExceptionOnce(
988
984
  $"resolve-{name}",
989
- $"Failed to resolve static method '{name}' on {type.FullName}: {ex.Message}"
985
+ $"Failed to resolve static method '{name}' on {type.FullName}.",
986
+ ex
990
987
  );
991
988
  return null;
992
989
  }
@@ -1000,9 +997,10 @@ namespace DxMessaging.Editor.Analyzers
1000
997
  }
1001
998
  catch (Exception ex)
1002
999
  {
1003
- LogOnce(
1000
+ LogExceptionOnce(
1004
1001
  $"resolve-field-{name}",
1005
- $"Failed to resolve instance field '{name}' on {type.FullName}: {ex.Message}"
1002
+ $"Failed to resolve instance field '{name}' on {type.FullName}.",
1003
+ ex
1006
1004
  );
1007
1005
  return null;
1008
1006
  }
@@ -1017,6 +1015,15 @@ namespace DxMessaging.Editor.Analyzers
1017
1015
  Debug.LogWarning($"[DxMessaging] {message}");
1018
1016
  }
1019
1017
 
1018
+ private static void LogExceptionOnce(string key, string message, Exception exception)
1019
+ {
1020
+ if (!AlreadyWarned.Add(key))
1021
+ {
1022
+ return;
1023
+ }
1024
+ DxMessagingEditorLog.LogWarning(message, exception);
1025
+ }
1026
+
1020
1027
  private static void RaiseReportUpdated()
1021
1028
  {
1022
1029
  try
@@ -1025,7 +1032,7 @@ namespace DxMessaging.Editor.Analyzers
1025
1032
  }
1026
1033
  catch (Exception ex)
1027
1034
  {
1028
- Debug.LogWarning($"[DxMessaging] ReportUpdated subscriber threw: {ex.Message}");
1035
+ LogExceptionOnce("report-updated", "ReportUpdated subscriber threw.", ex);
1029
1036
  }
1030
1037
  }
1031
1038
 
@@ -1057,7 +1064,7 @@ namespace DxMessaging.Editor.Analyzers
1057
1064
  }
1058
1065
  catch (Exception ex)
1059
1066
  {
1060
- LogOnce("persist", $"Failed to persist analyzer diagnostics report: {ex.Message}");
1067
+ LogExceptionOnce("persist", "Failed to persist analyzer diagnostics report.", ex);
1061
1068
  }
1062
1069
  }
1063
1070
 
@@ -1097,7 +1104,7 @@ namespace DxMessaging.Editor.Analyzers
1097
1104
  }
1098
1105
  catch (Exception ex)
1099
1106
  {
1100
- LogOnce("load", $"Failed to load analyzer diagnostics report: {ex.Message}");
1107
+ LogExceptionOnce("load", "Failed to load analyzer diagnostics report.", ex);
1101
1108
  }
1102
1109
  }
1103
1110
 
@@ -16,21 +16,21 @@ PluginImporter:
16
16
  second:
17
17
  enabled: 0
18
18
  settings:
19
- Exclude Editor: 0
19
+ Exclude Editor: 1
20
20
  Exclude Linux64: 1
21
21
  Exclude OSXUniversal: 1
22
22
  Exclude WebGL: 1
23
23
  Exclude Win: 1
24
24
  Exclude Win64: 1
25
25
  - first:
26
- Any:
26
+ Any:
27
27
  second:
28
28
  enabled: 0
29
29
  settings: {}
30
30
  - first:
31
31
  Editor: Editor
32
32
  second:
33
- enabled: 1
33
+ enabled: 0
34
34
  settings:
35
35
  DefaultValueInitialized: true
36
36
  - first:
@@ -16,21 +16,21 @@ PluginImporter:
16
16
  second:
17
17
  enabled: 0
18
18
  settings:
19
- Exclude Editor: 0
19
+ Exclude Editor: 1
20
20
  Exclude Linux64: 1
21
21
  Exclude OSXUniversal: 1
22
22
  Exclude WebGL: 1
23
23
  Exclude Win: 1
24
24
  Exclude Win64: 1
25
25
  - first:
26
- Any:
26
+ Any:
27
27
  second:
28
28
  enabled: 0
29
29
  settings: {}
30
30
  - first:
31
31
  Editor: Editor
32
32
  second:
33
- enabled: 1
33
+ enabled: 0
34
34
  settings:
35
35
  DefaultValueInitialized: true
36
36
  - first:
@@ -16,21 +16,21 @@ PluginImporter:
16
16
  second:
17
17
  enabled: 0
18
18
  settings:
19
- Exclude Editor: 0
19
+ Exclude Editor: 1
20
20
  Exclude Linux64: 1
21
21
  Exclude OSXUniversal: 1
22
22
  Exclude WebGL: 1
23
23
  Exclude Win: 1
24
24
  Exclude Win64: 1
25
25
  - first:
26
- Any:
26
+ Any:
27
27
  second:
28
28
  enabled: 0
29
29
  settings: {}
30
30
  - first:
31
31
  Editor: Editor
32
32
  second:
33
- enabled: 1
33
+ enabled: 0
34
34
  settings:
35
35
  DefaultValueInitialized: true
36
36
  - first:
@@ -16,21 +16,21 @@ PluginImporter:
16
16
  second:
17
17
  enabled: 0
18
18
  settings:
19
- Exclude Editor: 0
19
+ Exclude Editor: 1
20
20
  Exclude Linux64: 1
21
21
  Exclude OSXUniversal: 1
22
22
  Exclude WebGL: 1
23
23
  Exclude Win: 1
24
24
  Exclude Win64: 1
25
25
  - first:
26
- Any:
26
+ Any:
27
27
  second:
28
28
  enabled: 0
29
29
  settings: {}
30
30
  - first:
31
31
  Editor: Editor
32
32
  second:
33
- enabled: 1
33
+ enabled: 0
34
34
  settings:
35
35
  DefaultValueInitialized: true
36
36
  - first: