Skip to content
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

Add async cancellable scheduling function #2321

Merged
merged 6 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 65 additions & 66 deletions web/frontend/src/components/encoder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,102 +5,101 @@ import { grpc } from '@improbable-eng/grpc-web';
import { Client, encoderApi, ResponseStream, robotApi, type ServiceError } from '@viamrobotics/sdk';
import { displayError } from '../lib/error';
import { rcLogConditionally } from '../lib/log';
import { scheduleAsyncPoll } from '../lib/schedule';

const props = defineProps<{
name: string
client: Client
statusStream: ResponseStream<robotApi.StreamStatusResponse> | null
}>();

let properties = $ref<encoderApi.GetPropertiesResponse.AsObject | undefined>();
let properties = $ref<encoderApi.GetPropertiesResponse.AsObject>();
let positionTicks = $ref(0);
let positionDegrees = $ref(0);

let refreshId = -1;
let cancelPoll: (() => void) | undefined;

const refresh = () => {
const req = new encoderApi.GetPositionRequest();
req.setName(props.name);
const getProperties = () => new Promise<encoderApi.GetPropertiesResponse.AsObject>((resolve, reject) => {
const request = new encoderApi.GetPropertiesRequest();
request.setName(props.name);

rcLogConditionally(req);
props.client.encoderService.getPosition(
req,
new grpc.Metadata(),
(error: ServiceError | null, resp: encoderApi.GetPositionResponse | null) => {
if (error) {
return displayError(error as ServiceError);
}
rcLogConditionally(request);
props.client.encoderService.getProperties(request, new grpc.Metadata(), (error, response) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a good candidate for a promisification helper in a future PR

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think as a next step I'd like to start moving these out into an api folder that promisifies all these callback apis. Quarantine zones FTW!

if (error) {
return reject((error as ServiceError).message);
}

resolve(response!.toObject());
});
});

const getPosition = () => new Promise<number>((resolve, reject) => {
const request = new encoderApi.GetPositionRequest();
request.setName(props.name);
rcLogConditionally(request);
props.client.encoderService.getPosition(request, new grpc.Metadata(), (error, resp) => {
if (error) {
return reject(new Error((error as ServiceError).message));
}

resolve(resp!.toObject().value);
});
});

const getPositionDegrees = () => new Promise<number>((resolve, reject) => {
const request = new encoderApi.GetPositionRequest();
request.setPositionType(2);
rcLogConditionally(request);

props.client.encoderService.getPosition(request, new grpc.Metadata(), (error, resp) => {
if (error) {
return reject(new Error((error as ServiceError).message));
}

resolve(resp!.toObject().value);
});
});

const refresh = async () => {
try {
positionTicks = await getPosition();

positionTicks = resp!.toObject().value;
if (properties?.angleDegreesSupported) {
positionDegrees = await getPositionDegrees();
}
);

if (properties!.angleDegreesSupported) {
req.setPositionType(2);
rcLogConditionally(req);
props.client.encoderService.getPosition(
req,
new grpc.Metadata(),
(error: ServiceError | null, resp: encoderApi.GetPositionResponse | null) => {
if (error) {
return displayError(error as ServiceError);
}

positionDegrees = resp!.toObject().value;
}
);
} catch (error) {
displayError(error as ServiceError);
}

refreshId = window.setTimeout(refresh, 500);
cancelPoll = scheduleAsyncPoll(refresh, 500);
};

const reset = () => {
const req = new encoderApi.ResetPositionRequest();
req.setName(props.name);

rcLogConditionally(req);
props.client.encoderService.resetPosition(
req,
new grpc.Metadata(),
(error: ServiceError | null) => {
if (error) {
return displayError(error as ServiceError);
}
const request = new encoderApi.ResetPositionRequest();
request.setName(props.name);

rcLogConditionally(request);

props.client.encoderService.resetPosition(request, new grpc.Metadata(), (error) => {
if (error) {
return displayError(error as ServiceError);
}
);
});
};

onMounted(() => {
onMounted(async () => {
try {
const req = new encoderApi.GetPropertiesRequest();
req.setName(props.name);

rcLogConditionally(req);
props.client.encoderService.getProperties(
req,
new grpc.Metadata(),
(error: ServiceError | null, resp: encoderApi.GetPropertiesResponse | null) => {
if (error) {
if (error.message === 'Response closed without headers') {
refreshId = window.setTimeout(refresh, 500);
return;
}

return displayError(error as ServiceError);
}
properties = resp!.toObject();
}
);
refreshId = window.setTimeout(refresh, 500);
properties = await getProperties();
refresh();
} catch (error) {
displayError(error as ServiceError);
}

props.statusStream?.on('end', () => clearTimeout(refreshId));
props.statusStream?.on('end', () => cancelPoll?.());
});

onUnmounted(() => {
clearTimeout(refreshId);
cancelPoll?.();
});

</script>
Expand Down
28 changes: 28 additions & 0 deletions web/frontend/src/lib/schedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export const scheduleAsyncPoll = (callback: () => Promise<void>, interval: number) => {
let cancelled = false;
let timeoutId = -1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC the typescript types allow undefined for clearTimeout (and it's certainly allowed at runtime) so I softly prefer that to a -1 magic value for "not set"


const refreshAndScheduleNext = async () => {
await callback();

// eslint-disable-next-line no-use-before-define
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happily enough, the common JS config will remove the need for this override by configuring the no-use-before-define rule to only complain about actual errors

scheduleNext();
};

const scheduleNext = () => {
if (cancelled) {
return;
}

timeoutId = window.setTimeout(refreshAndScheduleNext, interval);
Comment on lines +13 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rather than an early return, purely stylistic

Suggested change
if (cancelled) {
return;
}
timeoutId = window.setTimeout(refreshAndScheduleNext, interval);
if (!cancelled) {
timeoutId = window.setTimeout(refreshAndScheduleNext, interval);
}

};

const cancel = () => {
cancelled = true;
window.clearTimeout(timeoutId);
};

scheduleNext();

return cancel;
};