@urbit/http-api: ensure acks are sent if timers are throttled

Strips the debounce mechanic, instead preferring to send acks when 20 or
more events have not been acked
This commit is contained in:
Liam Fitzgerald 2021-07-20 12:19:31 +10:00
parent 22d0d9557f
commit 86c3d156dd
No known key found for this signature in database
GPG Key ID: D390E12C61D1CFFB

View File

@ -164,14 +164,18 @@ export class Urbit {
/** /**
* Initializes the SSE pipe for the appropriate channel. * Initializes the SSE pipe for the appropriate channel.
*/ */
eventSource(): Promise<void> { async eventSource(): Promise<void> {
if(this.sseClientInitialized) {
return Promise.resolve();
}
this.sseClientInitialized = true;
if(this.lastEventId === 0) { if(this.lastEventId === 0) {
// Can't receive events until the channel is open // Can't receive events until the channel is open,
this.skipDebounce = true; // so poke and open then
return this.poke({ app: 'hood', mark: 'helm-hi', json: 'Opening API channel' }).then(() => {}); await this.poke({ app: 'hood', mark: 'helm-hi', json: 'Opening API channel' });
return;
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!this.sseClientInitialized) {
const sseOptions: SSEOptions = { const sseOptions: SSEOptions = {
headers: {} headers: {}
}; };
@ -201,14 +205,13 @@ export class Urbit {
console.log('Received SSE: ', event); console.log('Received SSE: ', event);
} }
if (!event.id) return; if (!event.id) return;
this.ack(Number(event.id)); this.lastEventId = parseInt(event.id, 10);
if (event.data && JSON.parse(event.data)) { if((this.lastEventId - this.lastAcknowledgedEventId) > 20) {
this.ack(this.lastEventId);
const data: any = JSON.parse(event.data); }
if (data.response === 'diff') { if (event.data && JSON.parse(event.data)) {
this.clearQueue(); const data: any = JSON.parse(event.data);
}
if (data.response === 'poke' && this.outstandingPokes.has(data.id)) { if (data.response === 'poke' && this.outstandingPokes.has(data.id)) {
const funcs = this.outstandingPokes.get(data.id); const funcs = this.outstandingPokes.get(data.id);
@ -221,8 +224,8 @@ export class Urbit {
console.error('Invalid poke response', data); console.error('Invalid poke response', data);
} }
this.outstandingPokes.delete(data.id); this.outstandingPokes.delete(data.id);
} else if (data.response === 'subscribe' || } else if (data.response === 'subscribe'
(data.response === 'poke' && this.outstandingSubscriptions.has(data.id))) { && this.outstandingSubscriptions.has(data.id)) {
const funcs = this.outstandingSubscriptions.get(data.id); const funcs = this.outstandingSubscriptions.get(data.id);
if (data.hasOwnProperty('err')) { if (data.hasOwnProperty('err')) {
console.error(data.err); console.error(data.err);
@ -231,7 +234,11 @@ export class Urbit {
} }
} else if (data.response === 'diff' && this.outstandingSubscriptions.has(data.id)) { } else if (data.response === 'diff' && this.outstandingSubscriptions.has(data.id)) {
const funcs = this.outstandingSubscriptions.get(data.id); const funcs = this.outstandingSubscriptions.get(data.id);
funcs.event(data.json); try {
funcs.event(data.json);
} catch (e) {
console.error('Failed to call subscription event callback', e);
}
} else if (data.response === 'quit' && this.outstandingSubscriptions.has(data.id)) { } else if (data.response === 'quit' && this.outstandingSubscriptions.has(data.id)) {
const funcs = this.outstandingSubscriptions.get(data.id); const funcs = this.outstandingSubscriptions.get(data.id);
funcs.quit(data); funcs.quit(data);
@ -257,10 +264,7 @@ export class Urbit {
}, },
}); });
this.sseClientInitialized = true; })
}
resolve();
});
} }
/** /**
@ -272,12 +276,6 @@ export class Urbit {
this.abort.abort(); this.abort.abort();
this.abort = new AbortController(); this.abort = new AbortController();
this.uid = `${Math.floor(Date.now() / 1000)}-${hexString(6)}`; this.uid = `${Math.floor(Date.now() / 1000)}-${hexString(6)}`;
if(this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
this.calm = true;
this.outstandingJSON = [];
this.lastEventId = 0; this.lastEventId = 0;
this.lastAcknowledgedEventId = 0; this.lastAcknowledgedEventId = 0;
this.outstandingSubscriptions = new Map(); this.outstandingSubscriptions = new Map();
@ -299,6 +297,7 @@ export class Urbit {
* @param eventId The event to acknowledge. * @param eventId The event to acknowledge.
*/ */
private async ack(eventId: number): Promise<number | void> { private async ack(eventId: number): Promise<number | void> {
this.lastAcknowledgedEventId = eventId;
const message: Message = { const message: Message = {
action: 'ack', action: 'ack',
'event-id': eventId 'event-id': eventId
@ -307,90 +306,18 @@ export class Urbit {
return eventId; return eventId;
} }
/** private async sendJSONtoChannel(...json: Message[]): Promise<void> {
* This is a wrapper method that can be used to send any action with data. const response = await fetch(this.channelUrl, {
* ...this.fetchOptions,
* Every message sent has some common parameters, like method, headers, and data method: 'PUT',
* structure, so this method exists to prevent duplication. body: JSON.stringify(json)
*
* @param action The action to send
* @param data The data to send with the action
*
* @returns void | number If successful, returns the number of the message that was sent
*/
// async sendMessage(action: Action, data?: object): Promise<number | void> {
// const id = this.getEventId();
// if (this.verbose) {
// console.log(`Sending message ${id}:`, action, data,);
// }
// const message: Message = { id, action, ...data };
// await this.sendJSONtoChannel(message);
// return id;
// }
private outstandingJSON: Message[] = [];
private debounceTimer: NodeJS.Timeout = null;
private debounceInterval = 10;
private skipDebounce = false;
private calm = true;
private sendJSONtoChannel(json: Message): Promise<boolean | void> {
this.outstandingJSON.push(json);
return this.processQueue();
}
private processQueue(): Promise<boolean | void> {
return new Promise(async (resolve, reject) => {
const process = async () => {
if (this.calm) {
if (this.outstandingJSON.length === 0) resolve(true);
this.calm = false; // We are now occupied
const json = this.outstandingJSON;
const body = JSON.stringify(json);
this.outstandingJSON = [];
if (body === '[]') {
this.calm = true;
return resolve(false);
}
try {
const response = await fetch(this.channelUrl, {
...this.fetchOptions,
method: 'PUT',
body
});
if(!response.ok) {
throw new Error('failed to PUT');
}
} catch (error) {
console.log(error);
json.forEach(failed => this.outstandingJSON.push(failed));
if (this.onError) {
this.onError(error);
} else {
throw error;
}
}
this.calm = true;
if (!this.sseClientInitialized) {
this.eventSource().then(resolve); // We can open the channel for subscriptions once we've sent data over it
}
resolve(true);
} else {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(process, this.debounceInterval);
resolve(false);
}
}
if(this.skipDebounce) {
process();
this.skipDebounce = false;
}
this.debounceTimer = setTimeout(process, this.debounceInterval);
}); });
if(!response.ok) {
throw new Error('Failed to PUT channel');
}
if(!this.sseClientInitialized) {
await this.eventSource();
}
} }
/** /**
@ -433,24 +360,6 @@ export class Urbit {
}); });
} }
// resetDebounceTimer() {
// if (this.debounceTimer) {
// clearTimeout(this.debounceTimer);
// this.debounceTimer = null;
// }
// this.calm = false;
// this.debounceTimer = setTimeout(() => {
// this.calm = true;
// }, this.debounceInterval);
// }
clearQueue() {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
/** /**
* Pokes a ship with data. * Pokes a ship with data.
* *