Design: stale handle errors
Provisional answer to #1644: typed stale errors for ordinary handle reads and writes. This question is small enough for an issue comment proposing the answer, as Ari permits; this short page records that proposal for review, not implemented behavior.
From the modding book of arismoko/horizonbfbda6a
On this page
Proposed contract
Reuse the decide, fire, apply outcome vocabulary and Part instance state identity rules. Ordinary reads return owned values or a typed refusal; writes return their operation-specific success or typed refusal. Do not expose decision tokens or apply outcomes as a second transaction API. Replace covered Option/bool methods and their ID helpers together, updating catalog, generated adapters, callers and horizon-machine-sample without compatibility aliases. A valid domain absence remains an optional success value where the operation actually permits absence.
Resolve identity through the existing lifecycle holder and authoritative table: server-wide epoch plus id for entities, no dimension and no second generation. Missing means Stale(reason); Taken at admission means Busy(reason). Found still requires authoritative residency/visibility validation. Recommend temporary inaccessible live state as admission Busy(Unavailable) (reason spelling provisional), never Stale or fabricated defaults. Reads must not load, pin, swap parked state or retry. During apply, a newly Taken or invalidated prerequisite is Stale(reason), following the accepted action note. Transfer preserves entity identity and resolves its current level. World close invalidates old handles permanently; reload needs fresh handles, as the Part note requires.
Use InvalidInput(reason) for malformed operation-owned input, retaining structured text-form reasons for action_bar. Preserve meaningful existing cancellation/refusal semantics rather than classifying every false as stale. Canceled and ListenerFailed remain action outcomes only where dispatch actually occurs. Reads take &dyn Host; writes take &mut dyn ServerHost; catalog side restrictions still govern both targets. Wrong-side writes must fail compilation.
Writes participate in the existing transaction and revalidate before mutation. In lifted passes, retirement and journal emission wait for root commit; abort publishes no packets or effects. Refuse saves during an open action savepoint. No mutable borrow crosses dispatch. Caught native unwinding and wasm traps follow ListenerFailed and rollback policy; process-aborting panics cannot be recovered.
Evidence and route owners
Current owners below were inspected; they establish entry points, not complete migration or a verified transitive route audit. Single-player server, LAN and dedicated server must share authoritative resolution; remote client reads must obey replica visibility without gaining write authority.
| Route | Verified file:line owner |
|---|---|
| Entity position / level | crates/; crates/ |
| Entity clear_fire | crates/; crates/ |
| Player name / action_bar | crates/; crates/ |
| Level block reads | crates/; crates/ |
| Lifecycle lookup | crates/ defines Found, Taken, Missing |
Vanilla 26.3 inspection via /workspace/horizon/tools/rea_vanilla.py method net.minecraft.world.entity.Entity isRemoved shows removal is tested by a non-null removal reason. It does not define Rust retained-handle errors or verify all read/write routes. This proposal changes Horizon lifetime reporting, not vanilla gameplay. The issue asserts no NeoForge counterpart. The public 26.3 Entity Javadoc was inaccessible; NeoForge 26.3 lifetime behavior is unverified. Require public documentation before adopting any claimed counterpart; no NeoForge source or patches were consulted.
Generate API-owned Wire values and typed refusals for native and wasm from one definition. Unsupported capabilities must refuse registration/load with the type and capability named, before first access. Treat input handles, discriminants, positions and text as untrusted; validate with existing authoritative scope and Wire/text bounds, checked arithmetic and operation-specific ranges. Preserve vanilla capacity; introduce no arbitrary new numeric cap. No mod-side revalidation protocol. Unused paths add only O(1) holder validation, with zero listener scans, callback dispatch, encoding or per-holder allocations; measure this before runtime closure.
Acceptance matrix
Proposed implementation tests on native and wasm, not results of this documentation change:
| Fixture | Expected result |
|---|---|
| Live entity at [1,2,3] | Position success [1,2,3] |
| Retain handle across despawn; clear_fire | Stale; zero mutations |
| Close/reopen same world, reuse numeric id | Old handle Stale; fresh handle succeeds |
| Taken at admission / during apply | Busy / Stale; zero mutation and no retry |
| Live identity temporarily inaccessible | Busy(Unavailable); no load or fabricated name/block |
| Entity transfers X to Y | Same epoch/id resolves Y |
| Valid loaded air block | Successful air value, not refusal |
| Malformed text | InvalidInput with text-form reason; zero packets |
| Listener writes then stale apply or caught panic/trap | Rollback; zero committed effects; next built-in action succeeds |
| Wrong-side write | Compile failure on both targets |
| No listeners | Zero dispatch/encoding/all-mod scans |
Proposed implementation stages
File: none. First confirm the unavailable mapping with the world-visibility/residency owner and inventory every ordinary operation and semantic absence/refusal. Then generate the shared result representation and migrate those methods, callers and sample together. Finally exercise real retained handles, transaction failure recovery, native/wasm parity and unused-path cost. Adjacent residency and visibility work supplies validation; it is not evidence that these result surfaces already exist.
Open questions
- Temporary unavailability: recommend Busy with a structured Unavailable reason, preserving admission versus apply semantics. The world-visibility owner must confirm suspended-player and unloaded-level classifications before runtime code; do not infer stale merely from failure to access storage.
- Per-operation refusal mappings: recommend keeping domain absence separate from lifetime failure and retaining structured text errors. Audit each covered method before freezing public signatures and Wire discriminants.
Edit this page on GitHub · Spotted something wrong? Open an issue .