spot-sdk-js
Version:
Develop applications and payloads for Spot using the unofficial Boston Dynamics Spot Node.js SDK.
613 lines (498 loc) • 18.5 kB
JavaScript
const {BaseClient, common_lease_errors, common_header_errors, error_factory} = require('./common');
const {ResponseError, ValueError} = require('./exceptions');
const lease_service_grpc_pb = require('../bosdyn/api/lease_service_grpc_pb');
const lease_pb = require('../bosdyn/api/lease_pb');
const {cloneDeep} = require('lodash');
const _RESOURCE_BODY = 'body';
class LeaseResponseError extends ResponseError {
constructor(msg){
super(msg);
this.name = 'LeaseResponseError';
}
};
class InvalidLeaseError extends LeaseResponseError {
constructor(msg){
super(msg);
this.name = 'InvalidLeaseError';
}
};
class DisplacedLeaseError extends LeaseResponseError {
constructor(msg){
super(msg);
this.name = 'DisplacedLeaseError';
}
};
class InvalidResourceError extends LeaseResponseError {
constructor(msg){
super(msg);
this.name = 'InvalidResourceError';
}
};
class NotAuthoritativeServiceError extends LeaseResponseError {
constructor(msg){
super(msg);
this.name = 'NotAuthoritativeServiceError';
}
};
class ResourceAlreadyClaimedError extends LeaseResponseError {
constructor(msg){
super(msg);
this.name = 'ResourceAlreadyClaimedError';
}
};
class RevokedLeaseError extends LeaseResponseError {
constructor(msg){
super(msg);
this.name = 'RevokedLeaseError';
}
};
class UnmanagedResourceError extends LeaseResponseError {
constructor(msg){
super(msg);
this.name = 'UnmanagedResourceError';
}
};
class WrongEpochError extends LeaseResponseError {
constructor(msg){
super(msg);
this.name = 'WrongEpochError';
}
};
class NotActiveLeaseError extends LeaseResponseError {
constructor(msg){
super(msg);
this.name = 'NotActiveLeaseError';
}
};
class NoSuchLease extends Error {
constructor(resource){
super(`No lease for resource "${resource}"`);
this.name = 'NoSuchLease';
this.resource = resource;
}
toString(){
return `No lease for resource "${this.resource}"`;
}
};
class LeaseNotOwnedByWallet extends Error {
constructor(resource, lease_state){
super(`Lease on "${this.resource}" has state (${this.lease_state?.lease_status ? this.lease_state.lease_status : '<unknown>'})`);
this.name = 'LeaseNotOwnedByWallet';
this.resource = resource;
this.lease_state = lease_state;
}
toString(){
let state;
try{
state = this.lease_state.lease_status;
}catch(e){
state = "<unknown>";
}
return `Lease on "${this.resource}" has state (${state})`;
}
};
const _ACQUIRE_LEASE_STATUS_TO_ERROR = {
STATUS_OK: [null, null],
STATUS_RESOURCE_ALREADY_CLAIMED: [ResourceAlreadyClaimedError, 'Use TakeLease method to forcefully grab the already claimed lease.'],
STATUS_INVALID_RESOURCE: [InvalidResourceError, 'Resource is not known to the LeaseService.'],
STATUS_NOT_AUTHORITATIVE_SERVICE: [NotAuthoritativeServiceError, 'LeaseService is not authoritative so Acquire should not work.']
}
const _TAKE_LEASE_STATUS_TO_ERROR = {
STATUS_OK: [null, null],
STATUS_INVALID_RESOURCE: [InvalidResourceError, 'Resource is not known to the LeaseService.'],
STATUS_NOT_AUTHORITATIVE_SERVICE: [NotAuthoritativeServiceError, 'LeaseService is not authoritative so Acquire should not work.']
}
const _RETURN_LEASE_STATUS_TO_ERROR = {
STATUS_OK: [null, null],
STATUS_INVALID_RESOURCE: [InvalidResourceError, 'Resource is not known to the LeaseService.'],
STATUS_NOT_ACTIVE_LEASE: [NotActiveLeaseError, 'Lease is not the active lease.'],
STATUS_NOT_AUTHORITATIVE_SERVICE: [NotAuthoritativeServiceError, 'LeaseService is not authoritative so Acquire should not work.']
}
class Lease {
static CompareResult = {
SAME: 1,
SUPER_LEASE: 2,
SUB_LEASE: 3,
OLDER: 4,
NEWER: 5,
DIFFERENT_RESOURCES: 6,
DIFFERENT_EPOCHS: 7
}
constructor(lease_proto){
if(!this.is_valid_proto(lease_proto)) throw new ValueError(`invalid lease_proto: ${lease_proto}`);
this.lease_proto = lease_proto;
}
compare(other_lease){
if (this.lease_proto.getResource() != other_lease.lease_proto.getResource()) return this.CompareResult.DIFFERENT_RESOURCES;
if (this.lease_proto.getEpoch() != other_lease.lease_proto.getEpoch()) return this.CompareResult.DIFFERENT_EPOCHS;
const sequence_size = this.lease_proto.getSequenceList().length;
const other_sequence_size = other_lease.lease_proto.getSequenceList().length;
const common_sequence_size = Math.min(sequence_size, other_sequence_size);
for(let i = 0; i < common_sequence_size; i++){
const sequence_num = this.lease_proto.getSequenceList()[i];
const other_sequence_num = other_lease.lease_proto.getSequenceList()[i];
if(sequence_num < other_sequence_num){
return this.CompareResult.OLDER;
}else if(sequence_num > other_sequence_num){
return this.CompareResult.NEWER;
}
}
if(sequence_size < other_sequence_size){
return this.CompareResult.SUPER_LEASE;
}else if(sequence_size > other_sequence_size){
return this.CompareResult.SUB_LEASE;
}
return this.CompareResult.SAME;
}
create_newer(){
let incr_lease_proto = new lease_pb.Lease();
incr_lease_proto = cloneDeep(this.lease_proto);
incr_lease_proto.setSequenceList(incr_lease_proto.getSequenceList()[incr_lease_proto.getSequenceList().length - 1] = this.lease_proto.sequence[this.lease_proto.sequence.length - 1] + 1);
return new Lease(incr_lease_proto);
}
create_sublease(){
let sub_lease_proto = new lease_pb.Lease();
sub_lease_proto = cloneDeep(this.lease_proto);
sub_lease_proto.addSequence(0);
return new Lease(sub_lease_proto);
}
static is_valid_proto(lease_proto){
return lease_proto && lease_proto.getResource() && lease_proto.getSequenceList();
}
};
class LeaseState {
static Status = {
STATUS_UNOWNED: 0,
STATUS_REVOKED: 1,
STATUS_SELF_OWNER: 2,
STATUS_OTHER_OWNER: 3,
STATUS_NOT_MANAGED: 4
}
constructor(lease_status, lease_owner = null, lease = null, lease_current = null, client_name = null){
this.lease_status = lease_status;
this.lease_owner = lease_owner;
this.lease_original = lease;
this.client_name = client_name;
if(lease_current){
this.lease_current = lease_current;
}else if(lease){
this.lease_current = this.lease_original.create_sublease();
}else{
this.lease_current = null;
}
}
create_newer(){
if(!this.lease_current) return this;
return new LeaseState(this.lease_status, this.lease_owner, this.lease_original, this.lease_current.create_newer());
}
update_from_lease_use_result(lease_use_result){
if(lease_use_result.getStatus() == lease_pb.LeaseUseResult.Status.STATUS_OLDER){
if(this.lease_current){
const attempted_lease = new Lease(lease_use_result.getAttemptedLease());
if(attempted_lease.compare(this.lease_current) == Lease.CompareResult.SAME){
return new LeaseState(LeaseState.Status.STATUS_OTHER_OWNER, lease_use_result.getOwner());
}
}
}else if(lease_use_result.getStatus() == lease_pb.LeaseUseResult.Status.STATUS_WRONG_EPOCH){
if(this.lease_current){
const attempted_lease = new Lease(lease_use_result.getAttemptedLease());
if(attempted_lease.compare(this.lease_current) == Lease.CompareResult.SAME){
return new LeaseState(LeaseState.Status.STATUS_UNOWNED);
}
}
}else if(lease_use_result.getStatus() == lease_pb.LeaseUseResult.Status.STATUS_REVOKED){
if(this.lease_current){
const attempted_lease = new Lease(lease_use_result.getAttemptedLease());
if(attempted_lease.compare(this.lease_current) == Lease.CompareResult.SAME){
return new LeaseState(LeaseState.Status.STATUS_REVOKED);
}
}
}
return this;
}
};
class LeaseWallet {
constructor(){
this._lease_state_map = {};
// this._lock = threading.Lock();
this.client_name = null;
}
add(lease){
this._lease_state_map[lease.getLease().getResource()] = new LeaseState(LeaseState.Status.STATUS_SELF_OWNER, null, lease, this.client_name);
}
remove(lease){
delete this._lease_state_map[lease.getLease().getResource()];
}
advance(resource=_RESOURCE_BODY){
const lease_state = this._get_owned_lease_state_locked(resource);
const new_lease = lease_state.create_newer();
this._lease_state_map[resource] = new_lease;
return new_lease.lease_current;
}
get_lease(resource=_RESOURCE_BODY){
return this._get_owned_lease_state_locked(resource).lease_current;
}
get_lease_state(resource=_RESOURCE_BODY){
return this._get_lease_state_locked(resource);
}
_get_lease_state_locked(resource){
try{
return this._lease_state_map[resource];
}catch(e){
throw new NoSuchLease(resource);
}
}
_get_owned_lease_state_locked(resource){
const lease_state = this._get_lease_state_locked(resource);
if(lease_state.lease_status != LeaseState.Status.STATUS_SELF_OWNER) throw new LeaseNotOwnedByWallet(resource, lease_state);
return lease_state;
}
on_lease_use_result(lease_use_result, resource = null){
resource = resource || lease_use_result.getAttemptedLease().getResource();
const lease_state = this._lease_state_map[resource];
if(!lease_state) return;
const new_lease_state = lease_state.update_from_lease_use_result(lease_use_result);
this._lease_state_map[resource] = new_lease_state;
}
set_client_name(client_name){
this.client_name = client_name;
}
};
class LeaseClient extends BaseClient {
static default_service_name = 'lease';
static service_type = 'bosdyn.api.LeaseService';
constructor(lease_wallet = null){
super(lease_service_grpc_pb.LeaseServiceClient);
this.lease_wallet = lease_wallet;
}
async acquire(resource=_RESOURCE_BODY, args){
const req = LeaseClient._make_acquire_request(resource);
return await this.call(this._stub.acquireLease, req, this._handle_acquire_success, this._handle_acquire_errors, args);
}
acquire_async(resource=_RESOURCE_BODY, args){
const req = LeaseClient._make_acquire_request(resource);
return this.call_async(this._stub.acquireLease, req, this._handle_acquire_success, this._handle_acquire_errors, args);
}
async take(resource=_RESOURCE_BODY, args){
const req = LeaseClient._make_take_request(resource);
return await this.call(this._stub.takeLease, req, this._handle_acquire_success, this._handle_take_errors, args);
}
take_async(resource=_RESOURCE_BODY, args){
const req = LeaseClient._make_take_request(resource);
return this.call_async(this._stub.takeLease, req, this._handle_acquire_success, this._handle_take_errors, args);
}
async return_lease(lease, args){
if(this.lease_wallet) this.lease_wallet.remove(lease);
const req = LeaseClient._make_return_request(lease);
return await this.call(this._stub.returnLease, req, null, this._handle_return_errors, args);
}
return_lease_async(lease, args){
if(this.lease_wallet) this.lease_wallet.remove(lease);
const req = LeaseClient._make_return_request(lease);
return this.call_async(this._stub.returnLease, req, null, this._handle_return_errors, args);
}
async retain_lease(lease, args){
const req = LeaseClient._make_retain_request(lease);
return await this.call(this._stub.retainLease, req, null, common_lease_errors, args);
}
retain_lease_async(lease, args){
const req = LeaseClient._make_retain_request(lease);
return this.call_async(this._stub.retainLease, req, null, common_lease_errors, args);
}
async list_leases(args){
const req = LeaseClient._make_list_leases_request();
return await this.call(this._stub.listLeases, req, this._list_leases_success, common_header_errors, args);
}
list_leases_async(args){
const req = LeaseClient._make_list_leases_request();
return this.call_async(this._stub.listLeases, req, this._list_leases_success, common_header_errors, args);
}
static _make_acquire_request(resource){
return new lease_pb.AcquireLeaseRequest().setResource(resource);
}
_handle_acquire_success(response){
const lease = new Lease(response.getLease());
if(this.lease_wallet) this.lease_wallet.add(lease);
return lease;
}
_handle_acquire_errors(response){
return error_factory(response, response.status, Object.keys(lease_pb.AcquireLeaseResponse.Status), _ACQUIRE_LEASE_STATUS_TO_ERROR);
}
static _make_take_request(resource){
return new lease_pb.TakeLeaseRequest().setResource(resource);
}
_handle_take_errors(response){
return error_factory(response, response.status, Object.keys(lease_pb.TakeLeaseResponse.Status), _TAKE_LEASE_STATUS_TO_ERROR);
}
static _make_return_request(lease){
return new lease_pb.ReturnLeaseRequest().setLease(lease.getLease());
}
_handle_return_errors(response){
return error_factory(response, response.status, Object.keys(lease_pb.ReturnLeaseResponse.Status), _RETURN_LEASE_STATUS_TO_ERROR);
}
static _make_retain_request(lease){
return new lease_pb.RetainLeaseRequest().setLease(lease.getLease());
}
static _make_list_leases_request(include_full_lease_info){
return new lease_pb.ListLeasesRequest().setIncludeFullLeaseInfo(include_full_lease_info);
}
_list_leases_success(response){
return response.getResources();
}
};
class LeaseWalletRequestProcessor {
constructor(lease_wallet, resource_list = null){
this.lease_wallet = lease_wallet;
this.resource_list = resource_list || [_RESOURCE_BODY];
this.logger = console;
}
mutate(request){
const [multiple_leases, skip_mutation] = this.get_lease_state(request);
if(skip_mutation) return;
if(multiple_leases && this.resource_list.length <= 1){
//pass
}else if(!multiple_leases && this.resource_list.length > 1){
this.logger.error('[LEASE] LeaseWalletRequestProcessor assigned multiple leases, but request only wants one.');
}
if(multiple_leases){
for(const resource of this.resource_list){
const lease = this.lease_wallet.advance(resource);
request.leases.add(lease.getLease());
}
}else{
const lease = this.lease_wallet.advance(this.resource_list[0]);
request.lease = lease.getLease();
}
}
static get_lease_state(request){
let skip_mutation = false;
let multiple_leases = null;
let isCatchFirst = false;
try{
skip_mutation = request.hasLease();
}catch(e){
let isCatchSecond = false;
isCatchFirst = true;
try{
skip_mutation = request.getLease().length > 0;
}catch(e){
isCatchSecond = true;
skip_mutation = true;
}
if(!isCatchSecond) multiple_leases = true;
}
if(!isCatchFirst) multiple_leases = false;
return [multiple_leases, skip_mutation]
}
};
class LeaseWalletResponseProcessor {
constructor(lease_wallet){
this.lease_wallet = lease_wallet;
}
mutate(response){
let lease_use_results = null;
try{
lease_use_results = [response.getLeaseUseResult()];
}catch(e){
try{
lease_use_results = response.getLeaseUseResult();
}catch(e){
return;
}
}
for(const result of lease_use_results){
this.lease_wallet.on_lease_use_result(result);
}
}
};
function add_lease_wallet_processors(client, lease_wallet, resource_list = null){
client.request_processors.push(new LeaseWalletRequestProcessor(lease_wallet, resource_list));
client.response_processors.push(new LeaseWalletResponseProcessor(lease_wallet));
}
class LeaseKeepAlive {
constructor(lease_client, lease_wallet = null, resource = _RESOURCE_BODY, rpc_interval_seconds = 2, keep_running_cb = null){
if(!lease_client) throw new ValueError("lease_client must be set");
this._lease_client = lease_client;
if(!lease_wallet) lease_wallet = lease_client.lease_wallet;
if(!lease_wallet) throw new ValueError("lease_wallet must be set");
this._lease_wallet = lease_wallet;
if(!resource) throw new ValueError("resource must be set");
this._resource = resource;
if(rpc_interval_seconds <= 0.0) throw new ValueError(`rpc_interval_seconds must be > 0, was ${rpc_interval_seconds}`);
this._rpc_interval_seconds = rpc_interval_seconds;
this.logger = console;
this._keep_running = keep_running_cb || (() => true);
// this._end_check_in_signal = threading.Event()
/*this._thread = threading.Thread(target=this._periodic_check_in);
this._thread.daemon = true;
this._thread.start();*/
}
shutdown(){
this.logger.debug('Shutting down');
this._end_periodic_check_in();
this.wait_until_done();
}
is_alive(){
return false; //this._thread.is_alive();
}
get lease_wallet(){
return this._lease_wallet;
}
wait_until_done(){
// this._thread.join();
}
_end_periodic_check_in(){
// this.logger.debug('Stopping check-in');
// this._end_check_in_signal.set();
}
_ok(){
this.logger.debug('Check-in successful');
}
_check_in(){
var lease = this._lease_wallet.get_lease(this._resource);
if(!lease) return null;
return this._lease_client.retain_lease(lease);
}
_periodic_check_in(){
this.logger.info('Starting lease check-in');
while(true){
const exec_start = Date.now();
if(!this._keep_running()) break;
let isCatch = false;
try{
this._check_in();
}catch(e){
isCatch = true;
this.logger.warn(`Generic exception during check-in:\n${e}\n (resuming check-in)`);
}
if(!isCatch){
this._ok();
}
const exec_seconds = Date.now() - exec_start;
if(this._end_check_in_signal.wait(this._rpc_interval_seconds - exec_seconds)) break;
}
this.logger.info('Lease check-in stopped');
}
};
module.exports = {
LeaseResponseError,
InvalidLeaseError,
DisplacedLeaseError,
InvalidResourceError,
NotAuthoritativeServiceError,
ResourceAlreadyClaimedError,
RevokedLeaseError,
UnmanagedResourceError,
WrongEpochError,
NotActiveLeaseError,
NoSuchLease,
LeaseNotOwnedByWallet,
Lease,
LeaseState,
LeaseWallet,
LeaseClient,
LeaseWalletRequestProcessor,
LeaseWalletResponseProcessor,
add_lease_wallet_processors,
LeaseKeepAlive
};