fix(cockpit): an application subject's wait for the floor is bounded, a late asynchronous hold is released, and a hold never counts as the user's selection

- A subject with no height at all waits at most
  COCKPIT_GROUND_WAIT_TIMEOUT_MS for the floor, its HUD updating and
  reading ACQUIRING SURFACE; then the view ends and the provider gets its
  subject back (there is no height of its own to fall back to).
- A hold that returns a promise is refused as before and released again
  once it settles, unless that subject is being ridden by then; release
  must tolerate a second call.
- Rail and Transit stamp selectedAtMs only for a user's selection (a click,
  focusTrain or focusVehicle); a deselection clears it, and a train or
  vehicle selected only by a hold reads null.
This commit is contained in:
Denis Groshev committed 2026-11-12 12:31:04 +01:00
1 parent 1e4060f6ba
commit 4c33552105
13 files changed
+239 -28

No files matched your search

+19 -5
View File
@@ -170,8 +170,10 @@ function refusal(provider, kinds) {
* - `candidate()`, `isEntryAllowed(candidate)` (optional), `hold(id)`,
* `read(nowMs, ctx)`, `hud(frame, ctx)` and `release(id, {restore})`, all
* synchronous (createCockpitSubjectCaller). `isEntryAllowed` allows entry
* only by returning `true`. A `hold` that throws is followed by
* `release(id, {restore: false})`.
* only by returning `true`. A `hold` that throws or returns a promise
* refuses entry and is followed by `release(id, {restore: false})`, and by
* another once that promise settles; `release` must therefore be harmless
* when called again for a subject it already gave back.
*
* A provider that is not so is left out with one warning naming it.
* @param {unknown} list
@@ -501,7 +503,9 @@ export function cockpitSubjectHudView(raw) {
* Provider methods are synchronous: the Cockpit calls them on its render
* cadence and uses the answer at once. One that returns a promise (an async
* function, say) breaks that contract: the promise is refused as a throw
* would be (its rejection, if any, is swallowed), with one warning.
* would be (its rejection, if any, is swallowed), with one warning, and
* `onSettled`, when given, runs once the promise has settled, so a caller
* can undo work that lands late.
* @param {{warn?: (message: string, error: unknown) => void}} [options]
*/
export function createCockpitSubjectCaller({
@@ -517,7 +521,13 @@ export function createCockpitSubjectCaller({
error,
);
};
return function call(provider, method, args = [], fallback = null) {
return function call(
provider,
method,
args = [],
fallback = null,
onSettled = null,
) {
let result;
try {
result = provider[method](...args);
@@ -530,7 +540,11 @@ export function createCockpitSubjectCaller({
(typeof result === 'object' || typeof result === 'function') &&
typeof result.then === 'function'
) {
Promise.resolve(result).catch(() => {});
// Its work may still land: `onSettled` runs once it has, either way.
const settled = () => {
if (typeof onSettled === 'function') onSettled();
};
Promise.resolve(result).then(settled, settled);
warnOnce(
provider,
method,