139 lines
5.2 KiB
JavaScript
139 lines
5.2 KiB
JavaScript
"use strict";
|
|
/**
|
|
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
* SPDX-License-Identifier: MIT
|
|
*
|
|
* Generic throttled flush controller.
|
|
*
|
|
* A pure scheduling primitive that manages timer-based throttling,
|
|
* mutex-guarded flushing, and reflush-on-conflict. Contains no
|
|
* business logic — the actual flush work is provided via a callback.
|
|
*/
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.FlushController = void 0;
|
|
const reply_dispatcher_types_1 = require("./reply-dispatcher-types.js");
|
|
// ---------------------------------------------------------------------------
|
|
// FlushController
|
|
// ---------------------------------------------------------------------------
|
|
class FlushController {
|
|
doFlush;
|
|
flushInProgress = false;
|
|
flushResolvers = [];
|
|
needsReflush = false;
|
|
pendingFlushTimer = null;
|
|
lastUpdateTime = 0;
|
|
isCompleted = false;
|
|
constructor(doFlush) {
|
|
this.doFlush = doFlush;
|
|
}
|
|
/** Mark the controller as completed — no more flushes after current one. */
|
|
complete() {
|
|
this.isCompleted = true;
|
|
}
|
|
/** Cancel any pending deferred flush timer. */
|
|
cancelPendingFlush() {
|
|
if (this.pendingFlushTimer) {
|
|
clearTimeout(this.pendingFlushTimer);
|
|
this.pendingFlushTimer = null;
|
|
}
|
|
}
|
|
/** Wait for any in-progress flush to finish. */
|
|
waitForFlush() {
|
|
if (!this.flushInProgress)
|
|
return Promise.resolve();
|
|
return new Promise((resolve) => this.flushResolvers.push(resolve));
|
|
}
|
|
/**
|
|
* Execute a flush (mutex-guarded, with reflush on conflict).
|
|
*
|
|
* If a flush is already in progress, marks needsReflush so a
|
|
* follow-up flush fires immediately after the current one completes.
|
|
*/
|
|
async flush() {
|
|
if (!this.cardMessageReady() || this.flushInProgress || this.isCompleted) {
|
|
if (this.flushInProgress && !this.isCompleted)
|
|
this.needsReflush = true;
|
|
return;
|
|
}
|
|
this.flushInProgress = true;
|
|
this.needsReflush = false;
|
|
// Update timestamp BEFORE the API call to prevent concurrent callers
|
|
// from also entering the flush (race condition fix).
|
|
this.lastUpdateTime = Date.now();
|
|
try {
|
|
await this.doFlush();
|
|
this.lastUpdateTime = Date.now();
|
|
}
|
|
finally {
|
|
this.flushInProgress = false;
|
|
const resolvers = this.flushResolvers;
|
|
this.flushResolvers = [];
|
|
for (const resolve of resolvers)
|
|
resolve();
|
|
// If events arrived while the API call was in flight,
|
|
// schedule an immediate follow-up flush.
|
|
if (this.needsReflush && !this.isCompleted && !this.pendingFlushTimer) {
|
|
this.needsReflush = false;
|
|
this.pendingFlushTimer = setTimeout(() => {
|
|
this.pendingFlushTimer = null;
|
|
void this.flush();
|
|
}, 0);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Throttled update entry point.
|
|
*
|
|
* @param throttleMs - Minimum interval between flushes (varies by
|
|
* CardKit vs IM patch mode). Passed in by the caller so this
|
|
* controller remains business-logic-free.
|
|
*/
|
|
async throttledUpdate(throttleMs) {
|
|
if (!this.cardMessageReady())
|
|
return;
|
|
const now = Date.now();
|
|
const elapsed = now - this.lastUpdateTime;
|
|
if (elapsed >= throttleMs) {
|
|
this.cancelPendingFlush();
|
|
if (elapsed > reply_dispatcher_types_1.THROTTLE_CONSTANTS.LONG_GAP_THRESHOLD_MS) {
|
|
// After a long gap, batch briefly so the first visible update
|
|
// contains meaningful text rather than just 1-2 characters.
|
|
this.lastUpdateTime = now;
|
|
this.pendingFlushTimer = setTimeout(() => {
|
|
this.pendingFlushTimer = null;
|
|
void this.flush();
|
|
}, reply_dispatcher_types_1.THROTTLE_CONSTANTS.BATCH_AFTER_GAP_MS);
|
|
}
|
|
else {
|
|
await this.flush();
|
|
}
|
|
}
|
|
else if (!this.pendingFlushTimer) {
|
|
// Inside throttle window — schedule a deferred flush
|
|
const delay = throttleMs - elapsed;
|
|
this.pendingFlushTimer = setTimeout(() => {
|
|
this.pendingFlushTimer = null;
|
|
void this.flush();
|
|
}, delay);
|
|
}
|
|
}
|
|
// ------------------------------------------------------------------
|
|
// Internal
|
|
// ------------------------------------------------------------------
|
|
/** Overridable gate: subclasses / consumers can set via setCardMessageReady. */
|
|
_cardMessageReady = false;
|
|
cardMessageReady() {
|
|
return this._cardMessageReady;
|
|
}
|
|
setCardMessageReady(ready) {
|
|
this._cardMessageReady = ready;
|
|
if (ready) {
|
|
// Initialize the timestamp so the first throttledUpdate sees a
|
|
// small elapsed time (matching original behavior where
|
|
// lastCardUpdateTime = Date.now() was set during card creation).
|
|
this.lastUpdateTime = Date.now();
|
|
}
|
|
}
|
|
}
|
|
exports.FlushController = FlushController;
|