-
-
Notifications
You must be signed in to change notification settings - Fork 1
Simplify device obtain process #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
eb60135
wip
heavyrubberslave d23a8ed
fix: repair device offer queue hand-off, stop race and disabled-devic…
heavyrubberslave 777a9b9
refactor: convert runNextInQueue to async/await
heavyrubberslave aba609f
refactor: clean up deviceManager/deviceProvider leftovers
heavyrubberslave d3a9448
Remove unused imports
heavyrubberslave fad4703
fix: reject stale offers that settle after a revoke/reset (coderabbit)
heavyrubberslave 842582e
refactor: improve naming clarity in deviceManager.ts
heavyrubberslave cbf16ae
refactor: further naming clarity in deviceManager.ts
heavyrubberslave f583d16
Simplify runNextOfferInQueue, eliminate undefined from device offer c…
heavyrubberslave 2b7e237
Extract detected-device offer queue mechanics into DetectedDeviceOffe…
heavyrubberslave 9f5def9
Remove injected acceptor from DetectedDeviceOfferQueue
heavyrubberslave fd66dc9
Make DetectedDeviceOfferQueue.open() a no-op if already open
heavyrubberslave db2b598
Fix cancellation check and extract runOffer() in DetectedDeviceOfferQ…
heavyrubberslave 84ca43b
Extract createAndRegisterDevice() from DeviceProvider.handleDeviceDet…
heavyrubberslave 627183b
Address CodeRabbit PR #99 review threads
heavyrubberslave 040eceb
Fix TOCTOU race parking a revoked disabled device for retry
heavyrubberslave 6b51a0a
Fix CodeRabbit thread 2: discard a queue nobody actually offered to
heavyrubberslave 22f5555
Simplify DetectedDeviceOfferQueue to lazy, self-opening queues
heavyrubberslave 69e18f1
Clean up device offer rejection log messages
heavyrubberslave bc9414f
Fix dropIfRevoked() being unreachable behind the has() reentrancy guard
heavyrubberslave 84964fe
Unify DetectedDeviceOfferQueue on close(), address remaining CodeRabb…
heavyrubberslave 6ab565c
Better error handling for failed handshake
heavyrubberslave 059d8fe
Cancel pending offers before closing connected devices in reset()
heavyrubberslave 0bf02dc
Remove comment
heavyrubberslave File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| import { CancellationToken, sequentialTaskQueueEvents, SequentialTaskQueue } from '@timesplinter/sequential-task-queue'; | ||
| import { AnyDevice } from './device.js'; | ||
| import { DeviceDetectionInfo } from './deviceManager.js'; | ||
| import DeviceOfferRejectedError from './deviceOfferRejectedError.js'; | ||
| import Logger from '../logging/Logger.js'; | ||
| import { logError } from '../util/error.js'; | ||
|
|
||
| export type OfferResult<D extends AnyDevice> = | ||
| | { successful: true, device: D } | ||
| | { successful: false, reason: unknown }; | ||
|
|
||
| type DeviceOffer<D extends AnyDevice> = (cancellationToken: CancellationToken) => Promise<D | DeviceOfferRejectedError>; | ||
|
|
||
| export default class DetectedDeviceOfferQueue | ||
| { | ||
| private readonly queues: Map<string, SequentialTaskQueue> = new Map(); | ||
|
|
||
| private readonly logger: Logger; | ||
|
|
||
| public constructor(logger: Logger) { | ||
| this.logger = logger; | ||
| } | ||
|
|
||
| private getOrCreateQueue(detectionId: string): SequentialTaskQueue | ||
| { | ||
| let queue = this.queues.get(detectionId); | ||
|
|
||
| if (queue !== undefined) { | ||
| return queue; | ||
| } | ||
|
|
||
| queue = new SequentialTaskQueue(); | ||
|
|
||
| queue.on(sequentialTaskQueueEvents.drained, () => { | ||
| // A revoked (closed) queue must survive its own drain - it's kept around deliberately | ||
| // as a tombstone so a late offer can still see it and reject itself. | ||
| if (!queue.isClosed) { | ||
| this.queues.delete(detectionId); | ||
| } | ||
| }); | ||
|
|
||
| this.queues.set(detectionId, queue); | ||
|
|
||
| return queue; | ||
| } | ||
|
|
||
| public offer<D extends AnyDevice>(deviceDetectionInfo: DeviceDetectionInfo, deviceOffer: DeviceOffer<D>): Promise<OfferResult<D>> | ||
| { | ||
| const detectionId = deviceDetectionInfo.detectionId; | ||
| const queue = this.getOrCreateQueue(detectionId); | ||
|
|
||
| if (queue.isClosed) { | ||
| return Promise.resolve({ | ||
| successful: false, | ||
| reason: new DeviceOfferRejectedError('Device is not available anymore for offering'), | ||
| }); | ||
| } | ||
|
|
||
| const task = queue.push((cancellationToken: CancellationToken) => this.runOffer(deviceOffer, cancellationToken)); | ||
|
|
||
| return Promise.resolve(task.then( | ||
| (result: OfferResult<D>): OfferResult<D> => { | ||
| if (result.successful) { | ||
| // Reject every other still-queued offer for this detection id without them | ||
| // ever running, since this device has already been claimed. | ||
| this.close(detectionId, new DeviceOfferRejectedError('Device has been claimed by another provider')); | ||
| } | ||
|
|
||
| return result; | ||
| }, | ||
| // Reached either if the offer was cancelled while still queued (never even starting - | ||
| // our callback above never ran) or if deviceOffer() itself rejected/threw uncaught - | ||
| // translate both the same way. | ||
| (reason: unknown): OfferResult<D> => ({ | ||
| successful: false, | ||
| reason: reason, | ||
| }) | ||
| )); | ||
| } | ||
|
|
||
| private async runOffer<D extends AnyDevice>( | ||
| deviceOffer: DeviceOffer<D>, | ||
| cancellationToken: CancellationToken | ||
| ): Promise<OfferResult<D>> { | ||
| const device = await deviceOffer(cancellationToken); | ||
|
|
||
| if (device instanceof DeviceOfferRejectedError) { | ||
| return { successful: false, reason: device }; | ||
| } | ||
|
|
||
| if (true !== cancellationToken.cancelled) { | ||
| return { successful: true, device }; | ||
| } | ||
|
|
||
| // In case this offer lost the race against another offer: close the device and reject the offer with a meaningful reason. | ||
| try { | ||
| await device.close(); | ||
| } catch (e: unknown) { | ||
| logError(this.logger, `Failed to close device '${device.getDeviceId}' offered after its queue was cleared`, e); | ||
| } | ||
|
|
||
| return { | ||
| successful: false, | ||
| reason: cancellationToken.reason, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * True if a queue currently exists for this detection id - either genuinely active/in-flight, | ||
| * or a closed tombstone left behind by revoke(). Does not distinguish between the two; | ||
| * callers that need "is a fresh announce still blocked by a past revoke" must call | ||
| * dropIfRevoked() first. | ||
| */ | ||
| public has(detectionId: string): boolean | ||
| { | ||
| return this.queues.has(detectionId); | ||
| } | ||
|
|
||
| public dropIfRevoked(detectionId: string): void | ||
| { | ||
| const queue = this.queues.get(detectionId); | ||
|
|
||
| if (queue !== undefined && queue.isClosed) { | ||
| this.queues.delete(detectionId); | ||
| } | ||
| } | ||
|
|
||
| private close(detectionId: string, reason: DeviceOfferRejectedError): void | ||
| { | ||
| const queue = this.queues.get(detectionId); | ||
|
|
||
| if (undefined !== queue) { | ||
| void queue.close(true, reason); | ||
| } | ||
|
|
||
| this.queues.delete(detectionId); | ||
| } | ||
|
|
||
| public revoke(detectionId: string, reason: DeviceOfferRejectedError): void | ||
| { | ||
| const queue = this.getOrCreateQueue(detectionId); | ||
|
|
||
| void queue.close(true, reason); | ||
| } | ||
|
|
||
| public closeAll(reason: DeviceOfferRejectedError): void | ||
| { | ||
| for (const detectionId of this.queues.keys()) { | ||
| this.close(detectionId, reason); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.