assistant-cloud
Version:
Cloud integration for assistant-ui
167 lines (166 loc) • 5.01 kB
JavaScript
// src/AssistantCloudEvents.ts
const FLUSH_SIZE = 20;
const MAX_BATCH_SIZE = 50;
const FLUSH_DELAY_MS = 2e3;
const RETRY_DELAYS_MS = [250, 1e3];
const pendingClearers = /* @__PURE__ */ new WeakMap();
const clearPendingAssistantCloudEvents = (events) => {
pendingClearers.get(events)?.();
};
var AssistantCloudEvents = class {
buffer = [];
timer;
flushing;
retryTimer;
resolveRetryDelay;
bestEffortRequested = false;
generation = 0;
cloud;
isEnabled;
listening = false;
constructor(cloud, isEnabled) {
this.cloud = cloud;
this.isEnabled = isEnabled;
pendingClearers.set(this, () => this.clearPending());
}
track(event) {
if (!this.isEnabled()) return;
this.listen();
this.buffer.push(normalizeEvent(event));
if (this.buffer.length >= FLUSH_SIZE) {
this.flush(true);
return;
}
this.scheduleFlush();
}
listen() {
if (this.listening || typeof window === "undefined" || typeof document === "undefined") return;
this.listening = true;
window.addEventListener("pagehide", this.flushBestEffort);
document.addEventListener("visibilitychange", this.onVisibilityChange);
}
unlisten() {
if (!this.listening) return;
this.listening = false;
window.removeEventListener("pagehide", this.flushBestEffort);
document.removeEventListener("visibilitychange", this.onVisibilityChange);
}
dispose() {
this.unlisten();
this.clearFlushTimer();
this.flushBestEffort();
}
clearPending() {
this.generation++;
this.buffer = [];
this.clearFlushTimer();
this.interruptRetryDelay();
this.unlisten();
}
onVisibilityChange = () => {
if (document.visibilityState === "hidden") this.flushBestEffort();
};
flushBestEffort = () => {
if (this.flushing) {
this.bestEffortRequested = true;
this.interruptRetryDelay();
}
return this.flush(false);
};
flush = async (retryFailures) => {
if (!this.isEnabled()) {
this.clearPending();
return;
}
if (this.flushing) return this.flushing;
this.clearFlushTimer();
const task = this.flushPending(retryFailures);
this.flushing = task;
task.then(() => {
if (this.flushing !== task) return;
this.flushing = void 0;
this.bestEffortRequested = false;
if (this.buffer.length === 0) {
this.clearFlushTimer();
this.unlisten();
} else this.flush(true);
});
return task;
};
async flushPending(retryFailures) {
while (this.buffer.length > 0) {
if (!this.isEnabled()) {
this.buffer = [];
return;
}
const events = this.buffer.splice(0, MAX_BATCH_SIZE);
const generation = this.generation;
for (let attempt = 0;; attempt++) try {
await this.cloud.makeRequest("/events", {
method: "POST",
body: { events },
keepalive: true
});
if (generation !== this.generation) return;
break;
} catch {
if (generation !== this.generation) return;
const delay = retryFailures && !this.bestEffortRequested ? RETRY_DELAYS_MS[attempt] : void 0;
if (delay === void 0) break;
await this.waitForRetry(delay);
if (generation !== this.generation) return;
if (!this.isEnabled()) return this.clearPending();
if (this.bestEffortRequested) break;
}
}
}
waitForRetry(delay) {
return new Promise((resolve) => {
const finish = () => {
this.retryTimer = void 0;
this.resolveRetryDelay = void 0;
resolve();
};
this.resolveRetryDelay = finish;
this.retryTimer = setTimeout(finish, delay);
});
}
interruptRetryDelay() {
if (this.retryTimer !== void 0) clearTimeout(this.retryTimer);
this.resolveRetryDelay?.();
}
scheduleFlush() {
if (this.timer !== void 0) return;
this.timer = setTimeout(() => {
this.timer = void 0;
this.flush(true);
}, FLUSH_DELAY_MS);
}
clearFlushTimer() {
if (this.timer === void 0) return;
clearTimeout(this.timer);
this.timer = void 0;
}
};
const normalizeEvent = (event) => {
const props = normalizeProps(event.props);
return {
kind: event.kind,
...normalizeId(event.thread_id) ? { thread_id: event.thread_id } : {},
...normalizeId(event.message_id) ? { message_id: event.message_id } : {},
...normalizeId(event.run_id) ? { run_id: event.run_id } : {},
...isNonNegativeInteger(event.value) ? { value: event.value } : {},
...props ? { props } : {}
};
};
const normalizeId = (value) => value && value.length <= 48 ? value : void 0;
const isNonNegativeInteger = (value) => value !== void 0 && Number.isInteger(value) && value >= 0;
const normalizeProps = (props) => {
if (!props) return void 0;
const entries = Object.entries(props);
if (entries.some(([, value]) => typeof value === "string" && value.length > 256 || typeof value === "number" && !Number.isFinite(value) || typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")) return;
const value = Object.fromEntries(entries);
return new TextEncoder().encode(JSON.stringify(value)).byteLength <= 1024 ? value : void 0;
};
export { AssistantCloudEvents, clearPendingAssistantCloudEvents };
//# sourceMappingURL=AssistantCloudEvents.js.map