In-progress rework of JavaScript World APIs.#114
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a first-pass event system for the JavaScript World APIs (world lifecycle, entity lifecycle/property changes, and collisions) and wires emissions into runtime/world loading and entity management, with a broad NUnit test suite for the new behavior.
Changes:
- Introduces core event primitives (
Events,IEventEmitter,ObserverLimits) and registersEventsinto the JS runtime. - Adds lifecycle emissions for
World(load/ready/error) andEntity(spawn/destroy + property change events). - Adds collision event bridging via a Unity
MonoBehaviourand extensive JavaScript-handler unit tests.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs | Attaches/removes CollisionEmitter for mesh entities and preview clones. |
| Assets/Runtime/StraightFour/Entity/Base/Scripts/CollisionEmitter.cs | New Unity collision/trigger bridge intended to emit JS collision events. |
| Assets/Runtime/Runtime/Scripts/WebVerseRuntime.cs | Emits World lifecycle events during world load success/failure. |
| Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldLifecycle.Tests.cs | New tests for World event registration/unregistration and lifecycle semantics. |
| Assets/Runtime/Handlers/JavascriptHandler/Tests/PropertyChangeEvents.Tests.cs | New tests for entity property-change event mechanics. |
| Assets/Runtime/Handlers/JavascriptHandler/Tests/ObserverLimits.Tests.cs | New tests for global observer limit tracking/enforcement. |
| Assets/Runtime/Handlers/JavascriptHandler/Tests/NetworkingEvents.Tests.cs | New tests for HTTP static events and IEventEmitter pattern for networking APIs. |
| Assets/Runtime/Handlers/JavascriptHandler/Tests/MultipleListeners.Tests.cs | New tests for listener ordering and multi-listener behavior. |
| Assets/Runtime/Handlers/JavascriptHandler/Tests/LegacyCoexistence.Tests.cs | New regression tests ensuring event system coexists with legacy callback paths. |
| Assets/Runtime/Handlers/JavascriptHandler/Tests/InputEvents.Tests.cs | New tests for Input static event methods. |
| Assets/Runtime/Handlers/JavascriptHandler/Tests/Events.Tests.cs | New tests for event constants and validation. |
| Assets/Runtime/Handlers/JavascriptHandler/Tests/EventEmitter.Tests.cs | New tests for default interface method behavior on IEventEmitter. |
| Assets/Runtime/Handlers/JavascriptHandler/Tests/EntityLifecycle.Tests.cs | New tests for entity spawn/destroy semantics and disposal behavior. |
| Assets/Runtime/Handlers/JavascriptHandler/Tests/EntityEventEmitter.Tests.cs | New tests for BaseEntity implementing IEventEmitter. |
| Assets/Runtime/Handlers/JavascriptHandler/Tests/CollisionEvents.Tests.cs | New tests for collision event naming and argument passing mechanics. |
| Assets/Runtime/Handlers/JavascriptHandler/Scripts/JavascriptHandler.cs | Exposes Engine getter and registers JS-facing Events constants object. |
| Assets/Runtime/Handlers/JavascriptHandler/APIs/WorldBrowserUtilities/Scripts/World.cs | Adds static World event system (on/off/once/Emit/dispose + debug). |
| Assets/Runtime/Handlers/JavascriptHandler/APIs/Networking/Scripts/WebSocket.cs | Implements IEventEmitter on WebSocket and adds event disposal. |
| Assets/Runtime/Handlers/JavascriptHandler/APIs/Networking/Scripts/MQTTClient.cs | Implements IEventEmitter on MQTTClient and adds event disposal. |
| Assets/Runtime/Handlers/JavascriptHandler/APIs/Networking/Scripts/HTTPNetworking.cs | Adds static HTTP event system (on/off/once/Emit/dispose). |
| Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs | Adds static Input event system (on/off/once/Emit/dispose). |
| Assets/Runtime/Handlers/JavascriptHandler/APIs/Entity/Scripts/EntityAPIHelper.cs | Emits entity spawn upon mapping registration. |
| Assets/Runtime/Handlers/JavascriptHandler/APIs/Entity/Scripts/BaseEntity.cs | Implements IEventEmitter, emits property-change/destroy events, adds delete guard. |
| Assets/Runtime/Handlers/JavascriptHandler/APIs/Core/Scripts/IEventEmitter.cs | New default-method event emitter interface + global observer limits. |
| Assets/Runtime/Handlers/JavascriptHandler/APIs/Core/Scripts/Events.cs | New canonical event name constants + validation registry. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (result && Listeners.ContainsKey(Core.Events.Entity.Scale)) | ||
| { | ||
| ((Core.IEventEmitter)this).Emit(Core.Events.Entity.Scale); | ||
| } |
There was a problem hiding this comment.
Same lazy-init/allocation issue: this Listeners.ContainsKey(...) call will allocate the listeners dictionary for entities that never use events. Consider checking the backing field (or a helper) before emitting scale events.
| if (result && Listeners.ContainsKey(Core.Events.Entity.Visibility)) | ||
| { | ||
| ((Core.IEventEmitter)this).Emit(Core.Events.Entity.Visibility); | ||
| } |
There was a problem hiding this comment.
Same lazy-init/allocation issue: emitting visibility events currently forces Listeners initialization even when unused. Prefer a non-allocating listener check before emitting.
| /// <summary> | ||
| /// Guard flag to prevent re-entrant Delete() calls from destroy listeners. | ||
| /// </summary> |
There was a problem hiding this comment.
The XML doc comments are malformed here: a second /// <summary> block starts inside the existing Delete method documentation. This will produce invalid XML docs / warnings; merge the guard-flag description into the existing summary/remarks instead of starting a new <summary>.
| // Emit destroy event — listeners can still access entity properties | ||
| ((Core.IEventEmitter)this).Emit(Core.Events.Entity.Destroy); | ||
|
|
||
| // Clean up all event listeners and mark as disposed | ||
| DisposeEvents(); | ||
|
|
||
| // Deregister from entity mapping (fixes pre-existing leak where | ||
| // RemoveEntityMapping was never called during entity destruction) | ||
| EntityAPIHelper.RemoveEntityMapping(internalEntity); | ||
|
|
||
| return internalEntity.Delete(synchronizeChange); |
There was a problem hiding this comment.
_isDeleting is set to true but never reset. If internalEntity.Delete(...) returns false (or throws), future Delete() calls will always be skipped. Reset the flag in a finally (or only set it once deletion is guaranteed) so the entity can recover from failure paths.
| // Emit destroy event — listeners can still access entity properties | |
| ((Core.IEventEmitter)this).Emit(Core.Events.Entity.Destroy); | |
| // Clean up all event listeners and mark as disposed | |
| DisposeEvents(); | |
| // Deregister from entity mapping (fixes pre-existing leak where | |
| // RemoveEntityMapping was never called during entity destruction) | |
| EntityAPIHelper.RemoveEntityMapping(internalEntity); | |
| return internalEntity.Delete(synchronizeChange); | |
| bool deleted = false; | |
| try | |
| { | |
| // Emit destroy event — listeners can still access entity properties | |
| ((Core.IEventEmitter)this).Emit(Core.Events.Entity.Destroy); | |
| // Clean up all event listeners and mark as disposed | |
| DisposeEvents(); | |
| // Deregister from entity mapping (fixes pre-existing leak where | |
| // RemoveEntityMapping was never called during entity destruction) | |
| EntityAPIHelper.RemoveEntityMapping(internalEntity); | |
| deleted = internalEntity.Delete(synchronizeChange); | |
| return deleted; | |
| } | |
| finally | |
| { | |
| if (deleted == false) | |
| { | |
| _isDeleting = false; | |
| } | |
| } |
| public static int CurrentCount { get; internal set; } = 0; | ||
|
|
There was a problem hiding this comment.
ObserverLimits.CurrentCount has an internal set, but the new JavaScript handler tests set this value directly from a separate test assembly. Without InternalsVisibleTo("FiveSQD.WebVerse.Handlers.Javascript.Tests"), those tests won’t compile; consider adding the attribute in the runtime assembly rather than making this setter public.
| bool result = internalEntity.SetEulerRotation(new UnityEngine.Vector3(eulerRotation.x, eulerRotation.y, eulerRotation.z), | ||
| local, synchronizeChange); | ||
|
|
||
| if (result && Listeners.ContainsKey(Core.Events.Entity.Rotation)) |
There was a problem hiding this comment.
Same lazy-init/allocation issue: Listeners.ContainsKey(...) will allocate the Listeners dictionary even when no listeners exist. Prefer a non-allocating check before emitting rotation events.
| if (result && Listeners.ContainsKey(Core.Events.Entity.Rotation)) | |
| if (result) |
| // Clean up all event listeners and mark as disposed | ||
| DisposeEvents(); | ||
|
|
||
| // Deregister from entity mapping (fixes pre-existing leak where | ||
| // RemoveEntityMapping was never called during entity destruction) |
There was a problem hiding this comment.
This method disposes the event system and removes the entity mapping before confirming that internalEntity.Delete(...) succeeded. If deletion fails, JS-side references will be orphaned (disposed + unmapped) while the internal entity may still exist. Consider only disposing/removing the mapping after a successful delete (or roll back on failure).
| /// <summary> | ||
| /// Reset the counter (for testing or world unload). | ||
| /// </summary> | ||
| internal static void Reset() { CurrentCount = 0; } | ||
| } |
There was a problem hiding this comment.
ObserverLimits.Reset() is internal, but the new JavaScript handler tests call it from a separate test assembly. Add InternalsVisibleTo("FiveSQD.WebVerse.Handlers.Javascript.Tests") (preferred) or provide a public test hook so the tests compile.
| // Performance guard: only proceed if entity has listeners for this collision event | ||
| // This avoids Jint overhead for entities without collision handlers | ||
| if (!ownerPublic.Listeners.ContainsKey(eventName)) return; | ||
|
|
There was a problem hiding this comment.
This “performance guard” still forces ownerPublic.Listeners lazy initialization, which allocates the listeners dictionary even when the entity has no collision listeners. That undermines the goal of avoiding overhead for non-listening entities. Consider adding a non-allocating listener check API on the JS BaseEntity (or IEventEmitter) and using that here.
| internal static void Emit(string eventName, params Jint.Native.JsValue[] args) | ||
| { | ||
| if (string.IsNullOrEmpty(eventName) || !_listeners.ContainsKey(eventName)) return; | ||
| if (!_emittingEvents.Add(eventName)) return; |
There was a problem hiding this comment.
HTTPNetworking.Emit(...) / DisposeAllHTTPListeners() are internal, but the new tests call them from the separate JavaScript test assembly. This will not compile without InternalsVisibleTo("FiveSQD.WebVerse.Handlers.Javascript.Tests") on the runtime assembly (preferable to making these public, which would expose them to JS).
No description provided.