mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 07:12:21 +00:00
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:
1 parent
1e4060f6ba
commit
4c33552105
13 files changed
+239
-28
No files matched your search
@@ -306,3 +306,18 @@ for a provider placing a train along its line. The frame, HUD view and
|
||||
candidate fields are documented in `src/cockpitSubjectProviders.js`; the Rail
|
||||
and Transit read APIs in `src/layers/rail/cockpitSubject.js` and
|
||||
`src/layers/transit/cockpitSubject.js`.
|
||||
|
||||
A candidate's `selectedAt` is a token that changes each time the user selects
|
||||
the subject, even the same one again, so entry takes it first. 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
|
||||
view waits, showing ACQUIRING SURFACE, for at most five seconds
|
||||
(`COCKPIT_GROUND_WAIT_TIMEOUT_MS`), then ends and releases the subject. A
|
||||
`hold` that throws or returns a promise refuses entry and is followed by
|
||||
`release(id, { restore: false })`, again once a returned promise settles, so
|
||||
`release` must be harmless when called more than once.
|
||||
@@ -2011,7 +2011,9 @@ older subject's.
|
||||
|
||||
A frame places the eye `eye.upM` above `heightM` (the shared floor where the
|
||||
frame gives none, or where it last stood while that floor is cold; with
|
||||
neither the camera waits where it is), slews toward `headingDeg` at the provider's rate, and may
|
||||
neither, the camera stays where it is and the HUD reads ACQUIRING SURFACE for
|
||||
at most `COCKPIT_GROUND_WAIT_TIMEOUT_MS`, after which the view ends and the
|
||||
subject is released: no height stands in for a floor never read), slews toward `headingDeg` at the provider's rate, and may
|
||||
turn off the floor clamps (`groundClamp: false`, for a subject inside a
|
||||
structure) or keep the anchor exactly on the subject (`anchor: 'exact'`, for a
|
||||
layer that already moves it smoothly); `cameraIntervalMs: 0` updates the
|
||||
@@ -2034,7 +2036,8 @@ The Rail layer offers a read API for such a view: `getSelectedTrain()` and
|
||||
`getCockpitSubject()` (a snapshot of the train: where it is along its line,
|
||||
which way it runs (`pathBinding.js` learns it), its rail height, speed as
|
||||
reported or derived, position source, feed credit and stops, and
|
||||
`selectedAtMs`, when the user last selected it, even again),
|
||||
`selectedAtMs`, when the user last selected it by a click or `focusTrain`,
|
||||
even again; null after a deselection and for a train only a hold selected),
|
||||
`setCockpitSubject(key)` (its marker and card hidden, the selection kept, its
|
||||
feed held, clicks and Escape left to the view) and `focusTrain(key)`; its pure
|
||||
path module is exported as `@gods-eye-view/core/layers/rail/paths`. Transit
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -17,8 +17,9 @@ import { displayMotion } from '../transit/movement.js';
|
||||
* view that waits for it holds it again if the feed lists it again.
|
||||
* `focusTrain(key)` selects a train and hands it the camera, as a click does.
|
||||
* A snapshot's `selectedAtMs` is when the user last selected the selected
|
||||
* train, even again (null for any other), for a view's entry to tell a
|
||||
* re-selection.
|
||||
* train (a click, or `focusTrain`), even again, for a view's entry to tell a
|
||||
* re-selection. It is null for any other train, after a deselection, and for
|
||||
* a train selected only by a hold: a hold is not a user's selection.
|
||||
*
|
||||
* @param {object} context The layer's state, parts, services and timetable.
|
||||
* @returns {{methods: object}}
|
||||
@@ -181,7 +182,11 @@ export function createRailCockpitSubject({
|
||||
}
|
||||
const entry = state._trains.get(key);
|
||||
if (!entry) return false;
|
||||
if (state._selectedKey !== key) parts.selection.selectTrain(key);
|
||||
if (state._selectedKey !== key) {
|
||||
parts.selection.selectTrain(key);
|
||||
// A hold is not a selection the user made (selectedAtMs).
|
||||
state._selectedAtMs = null;
|
||||
}
|
||||
state._cockpitSubjectKey = key;
|
||||
state._cockpitLostFeedId = null;
|
||||
if (previous && previous !== entry) parts.rendering.paint(previous);
|
||||
|
||||
@@ -196,6 +196,36 @@ test('a held train its feed drops leaves the feed polled until the view lets go'
|
||||
assert.equal(app.state()._trains.size, 0);
|
||||
});
|
||||
|
||||
test('selectedAtMs is the user’s selection time, never a hold’s or another train’s', async (t) => {
|
||||
const app = await onTheLine(t);
|
||||
assert.equal(app.layer.focusTrain(T1), true);
|
||||
const first = app.layer.getSelectedTrain().selectedAtMs;
|
||||
assert.ok(Number.isFinite(first), 'a click or focusTrain stamps it');
|
||||
// Selected again: a later stamp.
|
||||
app.run(1000);
|
||||
app.layer.focusTrain(T1);
|
||||
assert.ok(app.layer.getSelectedTrain().selectedAtMs > first);
|
||||
// A hold of another train is not the user's selection.
|
||||
app.layer.setCockpitSubject(T2);
|
||||
assert.equal(app.layer.getCockpitSubject().key, T2);
|
||||
assert.equal(app.layer.getCockpitSubject().selectedAtMs, null);
|
||||
app.layer.setCockpitSubject(null);
|
||||
// Nor after a deselection (Escape), then a hold.
|
||||
app.layer.focusTrain(T1);
|
||||
globalThis.document.dispatchEvent(
|
||||
Object.assign(new Event('keydown'), { key: 'Escape' }),
|
||||
);
|
||||
assert.equal(app.state()._selectedKey, null);
|
||||
assert.equal(app.state()._selectedAtMs, null, 'deselection clears it');
|
||||
app.layer.setCockpitSubject(T2);
|
||||
assert.equal(app.layer.getCockpitSubject().selectedAtMs, null);
|
||||
// Releasing twice is harmless: the selection stays.
|
||||
assert.equal(app.layer.setCockpitSubject(null), true);
|
||||
assert.equal(app.layer.setCockpitSubject(null), true);
|
||||
assert.equal(app.state()._selectedKey, T2);
|
||||
assert.equal(app.layer.getCockpitSubject(), null);
|
||||
});
|
||||
|
||||
test('turning the layer off ends the hold', async (t) => {
|
||||
const app = await onTheLine(t);
|
||||
app.layer.setCockpitSubject(T1);
|
||||
|
||||
@@ -127,6 +127,7 @@ export function createSelection({ state, services, parts }) {
|
||||
const hadSelection = state._selectedKey !== null;
|
||||
parts.follow.release({ untrack });
|
||||
state._selectedKey = null;
|
||||
state._selectedAtMs = null;
|
||||
state._selectedCardText = null;
|
||||
if (hadSelection) {
|
||||
parts.rendering.paintAll();
|
||||
|
||||
@@ -17,8 +17,10 @@ import { displayMotion } from './movement.js';
|
||||
* change the selection. The hold ends with `setCockpitSubject(null)`, and on
|
||||
* its own when the vehicle is removed or the layer is turned off.
|
||||
* `focusVehicle(key)` selects a vehicle, as a click does. A snapshot's
|
||||
* `selectedAtMs` is when the user last selected the selected vehicle, even
|
||||
* again (null for any other).
|
||||
* `selectedAtMs` is when the user last selected the selected vehicle (a
|
||||
* click, or `focusVehicle`), even again. It is null for any other vehicle,
|
||||
* after a deselection, and for a vehicle selected only by a hold: a hold is
|
||||
* not a user's selection.
|
||||
*
|
||||
* @param {object} context The layer's state, parts and services.
|
||||
* @returns {{methods: object}}
|
||||
@@ -165,7 +167,7 @@ export function createTransitCockpitSubject({ state, parts, services }) {
|
||||
*/
|
||||
focusVehicle(key) {
|
||||
if (!state._vehicles.get(key)?.marker) return false;
|
||||
parts.selection.selectVehicle(key);
|
||||
parts.selection.selectVehicle(key, { byUser: true });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4196,6 +4196,32 @@ test('the selected vehicle reads as a snapshot of its place, motion and source',
|
||||
assert.ok(Number.isFinite(view.position.latitude));
|
||||
});
|
||||
|
||||
test('selectedAtMs is the user’s selection time; a hold and a deselection leave none', async (t) => {
|
||||
const app = harness(t);
|
||||
const key = await busRunningNorth(app);
|
||||
// A vehicle only a hold selected reports none.
|
||||
assert.equal(app.layer.setCockpitSubject(key), true);
|
||||
assert.equal(app.layer.getCockpitSubject().selectedAtMs, null);
|
||||
app.layer.setCockpitSubject(null);
|
||||
assert.equal(app.layer.setCockpitSubject(null), true, 'twice is harmless');
|
||||
assert.equal(app.state()._selectedKey, key);
|
||||
// The user's selection stamps it, again on a re-selection.
|
||||
assert.equal(app.layer.focusVehicle(key), true);
|
||||
const first = app.layer.getSelectedVehicle().selectedAtMs;
|
||||
assert.ok(Number.isFinite(first));
|
||||
app.run(1000);
|
||||
app.layer.focusVehicle(key);
|
||||
assert.ok(app.layer.getSelectedVehicle().selectedAtMs > first);
|
||||
// Deselected, then held: none.
|
||||
globalThis.document.dispatchEvent(
|
||||
Object.assign(new Event('keydown'), { key: 'Escape' }),
|
||||
);
|
||||
assert.equal(app.state()._selectedKey, null);
|
||||
assert.equal(app.state()._selectedAtMs, null);
|
||||
app.layer.setCockpitSubject(key);
|
||||
assert.equal(app.layer.getCockpitSubject().selectedAtMs, null);
|
||||
});
|
||||
|
||||
test('a held vehicle hides, keeps moving out of view, and lets go when it goes', async (t) => {
|
||||
const app = harness(t);
|
||||
const key = await busRunningNorth(app);
|
||||
|
||||
@@ -97,6 +97,7 @@ export function createSelection({ state, services, parts }) {
|
||||
: null;
|
||||
parts.trails.clear();
|
||||
state._selectedKey = null;
|
||||
state._selectedAtMs = null;
|
||||
state._selectedCardText = null;
|
||||
state._detectRevision += 1;
|
||||
// Cleared first: the styling path reads the selection from state.
|
||||
@@ -104,12 +105,18 @@ export function createSelection({ state, services, parts }) {
|
||||
state._overlayHost.clearSource(TRANSIT_SELECTED_OVERLAY_SOURCE_ID);
|
||||
}
|
||||
|
||||
function selectVehicle(key) {
|
||||
/**
|
||||
* Select a vehicle. `byUser` marks a selection the user made (a click, or
|
||||
* focusVehicle), even of the selected vehicle again: its time is the
|
||||
* snapshot's `selectedAtMs` (cockpitSubject.js). A first-person view's hold
|
||||
* selects without it.
|
||||
*/
|
||||
function selectVehicle(key, { byUser = false } = {}) {
|
||||
clearSelection();
|
||||
const entry = state._vehicles.get(key);
|
||||
if (!entry?.marker) return;
|
||||
state._selectedKey = key;
|
||||
state._selectedAtMs = Date.now();
|
||||
if (byUser) state._selectedAtMs = Date.now();
|
||||
state._detectRevision += 1;
|
||||
parts.rendering.paintSelected(entry, true);
|
||||
parts.trails.select(entry);
|
||||
@@ -190,11 +197,11 @@ export function createSelection({ state, services, parts }) {
|
||||
typeof primitiveId === 'string' &&
|
||||
state._vehicles.has(primitiveId)
|
||||
) {
|
||||
selectVehicle(primitiveId);
|
||||
selectVehicle(primitiveId, { byUser: true });
|
||||
return;
|
||||
}
|
||||
if (typeof picked.id === 'string' && state._vehicles.has(picked.id)) {
|
||||
selectVehicle(picked.id);
|
||||
selectVehicle(picked.id, { byUser: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
+24
-3
@@ -349,12 +349,33 @@ function updateSubject(nowMs) {
|
||||
}
|
||||
if (!Number.isFinite(heightM)) {
|
||||
// Nothing to stand on yet, from the first frame: the camera stays where
|
||||
// it is while the floor is acquired, as the built-in views wait for the
|
||||
// surface, rather than standing on the ellipsoid (kilometres underground
|
||||
// on a plateau).
|
||||
// it is while the floor is acquired, its HUD saying so, rather than
|
||||
// standing on the ellipsoid (kilometres underground on a plateau). The
|
||||
// wait is bounded as the built-in views' is. Unlike a parked aircraft,
|
||||
// whose fallback is its own reported altitude, such a subject has no
|
||||
// height at all to fall back to: any number would be a guess, so the
|
||||
// view ends and the provider gets its subject back.
|
||||
if (!this.surfaceWaitStartedMs) this.surfaceWaitStartedMs = nowMs;
|
||||
if (
|
||||
cockpitSurfaceWaitExpired(
|
||||
nowMs,
|
||||
this.surfaceWaitStartedMs,
|
||||
COCKPIT_GROUND_WAIT_TIMEOUT_MS,
|
||||
)
|
||||
) {
|
||||
this.exit({ restoreTracking: false });
|
||||
return;
|
||||
}
|
||||
this.surfaceAcquiring = true;
|
||||
if (
|
||||
cockpitUiUpdateDue(nowMs, this.lastHudUpdateMs, COCKPIT_HUD_UPDATE_MS)
|
||||
) {
|
||||
this.lastHudUpdateMs = nowMs;
|
||||
this.updateHud(this.subjectInfo(frame), nowMs);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.surfaceWaitStartedMs = 0;
|
||||
this.surfaceAcquiring = false;
|
||||
this.subjectHeightM = heightM;
|
||||
const target = Cesium.Cartesian3.fromDegrees(
|
||||
|
||||
@@ -183,9 +183,12 @@ function updateSubjectHud(info, nowMs, forceContext) {
|
||||
view.trackSource = null;
|
||||
view.kicker = hud.kicker || provider.labels.kicker;
|
||||
view.callsign = hud.title || info.name || '';
|
||||
view.meta.text = hud.meta.text;
|
||||
view.meta.tone = hud.meta.tone;
|
||||
view.meta.emergency = hud.meta.tone === 'alert';
|
||||
// While the floor under a subject with no height is acquired
|
||||
// (cockpitCamera.js), the meta line says so in place of the provider's.
|
||||
const acquiring = this.surfaceAcquiring === true;
|
||||
view.meta.text = acquiring ? t('ACQUIRING SURFACE') : hud.meta.text;
|
||||
view.meta.tone = acquiring ? 'warning' : hud.meta.tone;
|
||||
view.meta.emergency = !acquiring && hud.meta.tone === 'alert';
|
||||
const speed = hud.speed?.value ?? null;
|
||||
view.speed = hud.speed
|
||||
? {
|
||||
|
||||
@@ -15,6 +15,7 @@ import CockpitView from './views/CockpitView.vue';
|
||||
import ContextView from './views/ContextView.vue';
|
||||
import { cockpitSubjectProviders } from '../cockpitSubjectProviders.js';
|
||||
import { getLocale, setLocale } from '../i18n/index.js';
|
||||
import { COCKPIT_GROUND_WAIT_TIMEOUT_MS } from './cockpitPresentation.js';
|
||||
import { vesselCockpitInfo } from '../layers/vessels/cockpitSubject.js';
|
||||
|
||||
const LAT = 47;
|
||||
@@ -528,18 +529,28 @@ test('a frame with no height and a cold floor never stands the eye on the ellips
|
||||
const clock = manualClock(t, 10_000);
|
||||
const ride = subjectProvider();
|
||||
ride.subject.frame = { ...ride.subject.frame, heightM: null };
|
||||
const { cockpit, shots, setFloor } = fixture(t, { providers: [ride] });
|
||||
// Nothing to stand on from the first frame: the camera waits where it is.
|
||||
const { $, cockpit, shots, setFloor } = fixture(t, { providers: [ride] });
|
||||
// Nothing to stand on from the first frame: the camera waits where it is,
|
||||
// its HUD saying so and still read from the provider.
|
||||
setFloor(undefined);
|
||||
cockpit.syncEntry();
|
||||
assert.equal(cockpit.enter(), true);
|
||||
const before = shots.length;
|
||||
const huds = () => ride.calls.filter(([name]) => name === 'hud').length;
|
||||
const hudsBefore = huds();
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
clock.advance(16);
|
||||
clock.advance(200);
|
||||
cockpit.update();
|
||||
}
|
||||
await settle();
|
||||
assert.equal(shots.length, before, 'no camera placed');
|
||||
assert.equal(cockpit.active, true, 'the view waits');
|
||||
assert.ok(huds() > hudsBefore, 'the HUD is not frozen');
|
||||
assert.equal($('#cockpit-aircraft-meta').textContent, 'ACQUIRING SURFACE');
|
||||
assert.equal(
|
||||
$('#cockpit-aircraft-meta').getAttribute('data-tone'),
|
||||
'warning',
|
||||
);
|
||||
// The floor arrives: the eye stands on it.
|
||||
setFloor(FLOOR_M);
|
||||
clock.advance(16);
|
||||
@@ -561,6 +572,60 @@ test('a frame with no height and a cold floor never stands the eye on the ellips
|
||||
assert.ok(Math.abs(shotHeight(shots) - (FLOOR_M + 3.5)) < 0.01);
|
||||
});
|
||||
|
||||
test('a floor that never comes ends the view after the bounded wait, releasing the subject', async (t) => {
|
||||
const clock = manualClock(t, 10_000);
|
||||
const ride = subjectProvider();
|
||||
ride.subject.frame = { ...ride.subject.frame, heightM: null };
|
||||
const { $, cockpit, shots, setFloor } = fixture(t, { providers: [ride] });
|
||||
setFloor(undefined);
|
||||
cockpit.syncEntry();
|
||||
assert.equal(cockpit.enter(), true);
|
||||
const before = shots.length;
|
||||
// Acquiring, the HUD updating, for just under the wait...
|
||||
for (let elapsed = 0; elapsed < COCKPIT_GROUND_WAIT_TIMEOUT_MS - 250;) {
|
||||
clock.advance(250);
|
||||
elapsed += 250;
|
||||
cockpit.update();
|
||||
}
|
||||
await settle();
|
||||
assert.equal(cockpit.active, true);
|
||||
assert.equal($('#cockpit-aircraft-meta').textContent, 'ACQUIRING SURFACE');
|
||||
// ...then no guessed height: the view ends and the subject goes back.
|
||||
clock.advance(500);
|
||||
cockpit.update();
|
||||
assert.equal(cockpit.active, false);
|
||||
assert.equal(shots.length, before, 'never placed on a guess');
|
||||
assert.deepEqual(lastRelease(ride.calls), ['release', 'a1', false]);
|
||||
});
|
||||
|
||||
test('an asynchronous hold that lands after entry was refused is released again', async (t) => {
|
||||
let heldId = null;
|
||||
const releases = [];
|
||||
const ride = subjectProvider({
|
||||
hold: async (id) => {
|
||||
await Promise.resolve();
|
||||
heldId = id;
|
||||
return true;
|
||||
},
|
||||
release: (id, options) => {
|
||||
releases.push([id, options.restore]);
|
||||
heldId = null;
|
||||
},
|
||||
});
|
||||
const { cockpit, warned } = fixture(t, { providers: [ride] });
|
||||
cockpit.syncEntry();
|
||||
assert.equal(cockpit.enter(), false, 'refused at once');
|
||||
assert.deepEqual(releases, [['a1', false]]);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(heldId, null, 'the late hold is let go');
|
||||
assert.deepEqual(releases, [
|
||||
['a1', false],
|
||||
['a1', false],
|
||||
]);
|
||||
assert.equal(cockpit.active, false);
|
||||
assert.match(warned[0], /hold\(\) returned a promise/);
|
||||
});
|
||||
|
||||
test('a provider’s labels, entry face and look follow a change of language', async (t) => {
|
||||
t.after(() => setLocale('en'));
|
||||
setLocale('en');
|
||||
|
||||
@@ -399,10 +399,29 @@ function enterSubject(provider) {
|
||||
if (!candidate || !this.isSubjectEntryAllowed(provider, candidate))
|
||||
return false;
|
||||
const failed = {};
|
||||
const held = this.callSubject(provider, 'hold', [candidate.id], failed);
|
||||
const release = () =>
|
||||
this.callSubject(provider, 'release', [candidate.id, { restore: false }]);
|
||||
// An asynchronous hold may land after it was refused: it is released
|
||||
// again once it settles, unless this subject is being ridden by then.
|
||||
const releaseLate = () => {
|
||||
if (
|
||||
this.active &&
|
||||
this.subjectProvider === provider &&
|
||||
this.subjectId === candidate.id
|
||||
)
|
||||
return;
|
||||
release();
|
||||
};
|
||||
const held = this.callSubject(
|
||||
provider,
|
||||
'hold',
|
||||
[candidate.id],
|
||||
failed,
|
||||
releaseLate,
|
||||
);
|
||||
if (held === failed) {
|
||||
// A hold that threw (or broke the contract) may have half happened.
|
||||
this.callSubject(provider, 'release', [candidate.id, { restore: false }]);
|
||||
release();
|
||||
return false;
|
||||
}
|
||||
if (held !== true) return false;
|
||||
|
||||
Reference in new issue
Block a user