Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

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.

RouteVerified file:line owner
Entity position / levelcrates/horizon-mod-vocab/src/host/entities.rs:123,143; crates/horizon-client/src/game/modding/native_entity_refs.rs:675,735
Entity clear_firecrates/horizon-mod-vocab/src/host/entities.rs:744; crates/horizon-client/src/game/modding/native_entity_state.rs:232
Player name / action_barcrates/horizon-mod-vocab/src/host/players.rs:54,302; crates/horizon-client/src/game/modding/native_server.rs:1152,1196
Level block readscrates/horizon-mod-vocab/src/host/levels.rs:190,211,217; crates/horizon-client/src/game/modding/native_level_refs.rs:433,518,530
Lifecycle lookupcrates/horizon-client/src/join_seq/residency/table.rs:24 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:

FixtureExpected result
Live entity at [1,2,3]Position success [1,2,3]
Retain handle across despawn; clear_fireStale; zero mutations
Close/reopen same world, reuse numeric idOld handle Stale; fresh handle succeeds
Taken at admission / during applyBusy / Stale; zero mutation and no retry
Live identity temporarily inaccessibleBusy(Unavailable); no load or fabricated name/block
Entity transfers X to YSame epoch/id resolves Y
Valid loaded air blockSuccessful air value, not refusal
Malformed textInvalidInput with text-form reason; zero packets
Listener writes then stale apply or caught panic/trapRollback; zero committed effects; next built-in action succeeds
Wrong-side writeCompile failure on both targets
No listenersZero 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.