mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 07:12:21 +00:00
fix(cockpit): a train handed to another feed's copy keeps its selection time, so it never takes entry back on its own; snapshot stops are copied again when a provider updates its list in place; setCockpitSubject's return is documented as it is
This commit is contained in:
1 parent
09f3380203
commit
0584292405
5 files changed
+91
-10
No files matched your search
+6
-5
@@ -317,11 +317,12 @@ Rail and Transit read APIs (`getSelectedTrain`, `getSelectedVehicle`,
|
||||
are experimental and may change without notice.
|
||||
|
||||
A candidate's `selectedAt` is a token that changes each time the user selects
|
||||
the subject, even the same one again, so entry counts it as selected then. The
|
||||
Rail and Transit snapshots give one, `selectedAtMs`: when the user last
|
||||
selected the train or vehicle by a click or `focusTrain`/`focusVehicle`. It is
|
||||
null after a deselection and for one selected only by a hold, since a hold is
|
||||
not the user's selection.
|
||||
the subject, even the same one again, so entry counts it as selected then; a
|
||||
new `id` under the same token is the same selection (a train's selection
|
||||
moving to another feed's copy of it). The Rail and Transit snapshots give one,
|
||||
`selectedAtMs`: when the user last selected the train or vehicle by a click or
|
||||
`focusTrain`/`focusVehicle`. It is null after a deselection and for one
|
||||
selected only by a hold, since a hold is not the user's selection.
|
||||
|
||||
A frame with `heightM: null` stands on the shared floor, or where it last
|
||||
stood while that floor is cold. If no height has been known since entry, the
|
||||
|
||||
@@ -29,14 +29,39 @@ const NO_STOPS = Object.freeze([]);
|
||||
/** Each stops list's frozen copy for a snapshot's `stops` (stopsOf). */
|
||||
const stopLists = new WeakMap();
|
||||
|
||||
/** Whether `copy` still holds each of `list`'s stops, field by field. */
|
||||
function sameStops(copy, list) {
|
||||
if (copy.length !== list.length) return false;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const stop = list[i];
|
||||
const kept = copy[i];
|
||||
if (
|
||||
!stop ||
|
||||
typeof stop !== 'object' ||
|
||||
!kept ||
|
||||
typeof kept !== 'object'
|
||||
) {
|
||||
if (!Object.is(stop, kept)) return false;
|
||||
continue;
|
||||
}
|
||||
const names = Object.keys(stop);
|
||||
if (names.length !== Object.keys(kept).length) return false;
|
||||
for (const name of names)
|
||||
if (!Object.is(stop[name], kept[name])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A trip's stops as a snapshot hands them out: a frozen array of frozen
|
||||
* copies, made once per list the stops provider returns.
|
||||
* copies, kept per list the stops provider returns while that list still
|
||||
* holds the same stops, so a provider that updates its list in place is
|
||||
* copied again.
|
||||
*/
|
||||
function stopsOf(list) {
|
||||
if (!Array.isArray(list)) return NO_STOPS;
|
||||
let stops = stopLists.get(list);
|
||||
if (!stops) {
|
||||
if (!stops || !sameStops(stops, list)) {
|
||||
stops = Object.freeze(
|
||||
list.map((stop) =>
|
||||
stop && typeof stop === 'object' ? Object.freeze({ ...stop }) : stop,
|
||||
@@ -258,7 +283,9 @@ export function createRailCockpitSubject({
|
||||
* Hold the train `key` for a first-person view, or release the hold with
|
||||
* null. A train it holds is selected, if it was not already.
|
||||
* @param {string|null} key
|
||||
* @returns {boolean} False for a train the layer does not show.
|
||||
* @returns {boolean} False for a key the layer does not list. A listed
|
||||
* train is held even when its category is off or another feed's copy
|
||||
* is drawn: the held copy is then the one drawn. Releasing returns true.
|
||||
*/
|
||||
setCockpitSubject(key) {
|
||||
const previous = state._trains.get(state._cockpitSubjectKey);
|
||||
|
||||
@@ -158,6 +158,13 @@ test('a snapshot read again while nothing changed is the one read before', async
|
||||
later.stops.map((stop) => stop.name),
|
||||
['Delta', 'Echo'],
|
||||
);
|
||||
// A provider that updates its list in place: the stops are copied again.
|
||||
list[0].state = 'passed';
|
||||
app.run(STOP_REFRESH_MS);
|
||||
const updated = app.layer.getSelectedTrain();
|
||||
assert.notEqual(updated.stops, later.stops);
|
||||
assert.equal(updated.stops[0].state, 'passed');
|
||||
assert.equal(app.layer.getSelectedTrain().stops, updated.stops);
|
||||
});
|
||||
|
||||
test('a snapshot’s line is a frozen copy: writing to it leaves the layer’s line', async (t) => {
|
||||
|
||||
@@ -1049,6 +1049,41 @@ test('the built-in kinds keep their order; a subject takes entry only when selec
|
||||
assert.equal(cockpit.resolveEntryKind(), 'aircraft');
|
||||
});
|
||||
|
||||
test('a new id under the same selectedAt is not a new selection', async (t) => {
|
||||
const clock = manualClock(t, 10_000);
|
||||
const ride = subjectProvider();
|
||||
ride.subject.candidate = { id: 'a1', name: 'Example 12', selectedAt: 1 };
|
||||
const { cockpit, trackAircraft } = fixture(t, { providers: [ride] });
|
||||
cockpit.syncEntry();
|
||||
assert.equal(cockpit.resolveEntryKind(), 'example-kind');
|
||||
// An aircraft tracked after it takes entry.
|
||||
clock.advance(500);
|
||||
trackAircraft(TRACKED_AIRCRAFT);
|
||||
cockpit.syncEntry();
|
||||
assert.equal(cockpit.resolveEntryKind(), 'aircraft');
|
||||
// The selection moves to another copy of the same subject: same token.
|
||||
clock.advance(500);
|
||||
ride.subject.candidate = { id: 'b1', name: 'Example 12', selectedAt: 1 };
|
||||
cockpit.syncEntry();
|
||||
assert.equal(cockpit.resolveEntryKind(), 'aircraft');
|
||||
// A new token is a new selection.
|
||||
clock.advance(500);
|
||||
ride.subject.candidate = { id: 'b1', name: 'Example 12', selectedAt: 2 };
|
||||
cockpit.syncEntry();
|
||||
assert.equal(cockpit.resolveEntryKind(), 'example-kind');
|
||||
// Without a token, a new id is a new selection.
|
||||
clock.advance(500);
|
||||
trackAircraft(null);
|
||||
cockpit.syncEntry();
|
||||
trackAircraft(TRACKED_AIRCRAFT);
|
||||
cockpit.syncEntry();
|
||||
assert.equal(cockpit.resolveEntryKind(), 'aircraft');
|
||||
clock.advance(500);
|
||||
ride.subject.candidate = { id: 'c1', name: 'Example 3' };
|
||||
cockpit.syncEntry();
|
||||
assert.equal(cockpit.resolveEntryKind(), 'example-kind');
|
||||
});
|
||||
|
||||
test('a subject first seen together with a built-in one comes after it', async (t) => {
|
||||
manualClock(t, 10_000);
|
||||
const ride = subjectProvider();
|
||||
|
||||
@@ -110,7 +110,10 @@ const BUILT_IN_ENTRY_ORDER = Object.freeze({
|
||||
* takes it back. Providers' candidates are ordered most recently selected
|
||||
* first, then by priority. A candidate's time is when this controller first
|
||||
* saw it (it looks on its entry cadence and on tracking changes), or saw it
|
||||
* selected again (a provider candidate whose `selectedAt` changes). One
|
||||
* selected again: another subject of a built-in kind, a provider candidate
|
||||
* whose `selectedAt` changes, or one without a `selectedAt` whose id changes.
|
||||
* A new id under the same `selectedAt` is the same selection, as when a
|
||||
* train's selection moves to another feed's copy of it. One
|
||||
* first seen together with a built-in candidate comes after it. With no
|
||||
* provider, entry is the built-in order alone. Each carries `allowed()`, its
|
||||
* kind's entry gate.
|
||||
@@ -123,8 +126,16 @@ export function entryCandidates(nowMs = performance.now()) {
|
||||
const list = [];
|
||||
const note = (kind, id, entry, selectedAt = null) => {
|
||||
const last = seen.get(kind);
|
||||
if (!last || last.id !== id || last.selectedAt !== selectedAt)
|
||||
// A provider's token says when its subject was selected: a new id under
|
||||
// the same token (a train handed to another feed's copy of it) is not a
|
||||
// new selection. Without a token, a new id is.
|
||||
if (
|
||||
!last ||
|
||||
last.selectedAt !== selectedAt ||
|
||||
(selectedAt === null && last.id !== id)
|
||||
)
|
||||
seen.set(kind, { id, selectedAt, atMs: nowMs });
|
||||
else if (last.id !== id) seen.set(kind, { ...last, id });
|
||||
list.push({ kind, id, seenAtMs: seen.get(kind).atMs, ...entry });
|
||||
};
|
||||
const aircraft = this.readAircraftInfo();
|
||||
|
||||
Reference in new issue
Block a user