mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52: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.
|
are experimental and may change without notice.
|
||||||
|
|
||||||
A candidate's `selectedAt` is a token that changes each time the user selects
|
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
|
the subject, even the same one again, so entry counts it as selected then; a
|
||||||
Rail and Transit snapshots give one, `selectedAtMs`: when the user last
|
new `id` under the same token is the same selection (a train's selection
|
||||||
selected the train or vehicle by a click or `focusTrain`/`focusVehicle`. It is
|
moving to another feed's copy of it). The Rail and Transit snapshots give one,
|
||||||
null after a deselection and for one selected only by a hold, since a hold is
|
`selectedAtMs`: when the user last selected the train or vehicle by a click or
|
||||||
not the user's selection.
|
`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
|
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
|
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). */
|
/** Each stops list's frozen copy for a snapshot's `stops` (stopsOf). */
|
||||||
const stopLists = new WeakMap();
|
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
|
* 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) {
|
function stopsOf(list) {
|
||||||
if (!Array.isArray(list)) return NO_STOPS;
|
if (!Array.isArray(list)) return NO_STOPS;
|
||||||
let stops = stopLists.get(list);
|
let stops = stopLists.get(list);
|
||||||
if (!stops) {
|
if (!stops || !sameStops(stops, list)) {
|
||||||
stops = Object.freeze(
|
stops = Object.freeze(
|
||||||
list.map((stop) =>
|
list.map((stop) =>
|
||||||
stop && typeof stop === 'object' ? Object.freeze({ ...stop }) : 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
|
* 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.
|
* null. A train it holds is selected, if it was not already.
|
||||||
* @param {string|null} key
|
* @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) {
|
setCockpitSubject(key) {
|
||||||
const previous = state._trains.get(state._cockpitSubjectKey);
|
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),
|
later.stops.map((stop) => stop.name),
|
||||||
['Delta', 'Echo'],
|
['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) => {
|
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');
|
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) => {
|
test('a subject first seen together with a built-in one comes after it', async (t) => {
|
||||||
manualClock(t, 10_000);
|
manualClock(t, 10_000);
|
||||||
const ride = subjectProvider();
|
const ride = subjectProvider();
|
||||||
|
|||||||
@@ -110,7 +110,10 @@ const BUILT_IN_ENTRY_ORDER = Object.freeze({
|
|||||||
* takes it back. Providers' candidates are ordered most recently selected
|
* takes it back. Providers' candidates are ordered most recently selected
|
||||||
* first, then by priority. A candidate's time is when this controller first
|
* 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
|
* 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
|
* 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
|
* provider, entry is the built-in order alone. Each carries `allowed()`, its
|
||||||
* kind's entry gate.
|
* kind's entry gate.
|
||||||
@@ -123,8 +126,16 @@ export function entryCandidates(nowMs = performance.now()) {
|
|||||||
const list = [];
|
const list = [];
|
||||||
const note = (kind, id, entry, selectedAt = null) => {
|
const note = (kind, id, entry, selectedAt = null) => {
|
||||||
const last = seen.get(kind);
|
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 });
|
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 });
|
list.push({ kind, id, seenAtMs: seen.get(kind).atMs, ...entry });
|
||||||
};
|
};
|
||||||
const aircraft = this.readAircraftInfo();
|
const aircraft = this.readAircraftInfo();
|
||||||
|
|||||||
Reference in new issue
Block a user