hibernot
Version:
Solution for hibernation using dynamic pings to your free tier backend services
204 lines (190 loc) • 7.57 kB
text/typescript
// Custom error class for Hibernot-specific errors.
// Use this for all errors thrown by the Hibernot library, so consumers can distinguish them from other errors.
export class HibernotError extends Error {
constructor(message: string) {
super(message);
this.name = 'HibernotError';
}
}
// Type definition for the configuration object required by Hibernot.
// - inactivityLimitMs: Time in milliseconds to wait before triggering keepAliveFn after inactivity.
// - keepAliveFn: An async function to be called when inactivityLimitMs is reached.
// - instanceName: Optional identifier for logging/debugging.
// - maxRetryAttempts: Optional number of times to retry keepAliveFn on failure.
type HibernotConfig = {
inactivityLimit: number; // ms
keepAliveFn: () => Promise<void>;
instanceName?: string;
maxRetryAttempts?: number;
};
/**
* Hibernot
*
* Main class that manages inactivity detection and keep-alive logic.
*
* Usage: Instantiate with a config object, then use the middleware() in your Express app.
*
* This class is designed to help keep your service "warm" by calling a keep-alive function
* after a period of inactivity, with optional retry logic.
*/
export class Hibernot {
// Tracks the number of times registerActivity() has been called (i.e., API hits).
private activityCount = 1;
// Stores the timestamp (ms since epoch) of the last activity.
private lastActivityTimestamp = Date.now();
// Holds the reference to the current inactivity timer (Node.js Timeout object).
private inactivityTimeout: NodeJS.Timeout | null = null;
// Stores the configuration options for this instance.
private config: HibernotConfig;
/**
* Constructor
* Validates and sets up configuration, then starts the inactivity timer.
*
* @param config - HibernotConfig object (see type above)
*/
constructor(config: HibernotConfig) {
// Validate inactivityLimitMs: must be a positive number.
if (typeof config.inactivityLimit !== 'number' || config.inactivityLimit <= 0) {
throw new HibernotError('inactivityLimit must be a positive number');
}
// Validate keepAliveFn: must be a function.
if (!config.keepAliveFn || typeof config.keepAliveFn !== 'function') {
throw new HibernotError('keepAliveFn must be a valid async function');
}
// Validate maxRetryAttempts: if provided, must be a non-negative number.
if (config.maxRetryAttempts !== undefined && (typeof config.maxRetryAttempts !== 'number' || config.maxRetryAttempts < 0)) {
throw new HibernotError('maxRetryAttempts must be a non-negative number');
}
// Set config, providing a default for maxRetryAttempts if not specified.
this.config = {
...config,
maxRetryAttempts: config.maxRetryAttempts !== undefined ? config.maxRetryAttempts : 3,
};
// Start the inactivity timer.
this.start();
}
/**
* Express-style middleware generator.
*
* Use this in your Express app to automatically register activity on each request.
* Example: app.use(hibernotInstance.middleware());
*/
public middleware() {
return (req: any, res: any, next: () => void) => {
this.registerActivity();
next();
};
}
/**
* Registers an activity (e.g., API hit).
* Increments the activity counter, updates the last activity timestamp, and resets the inactivity timer.
* Call this manually if not using the middleware.
*/
public registerActivity() {
this.activityCount++;
this.lastActivityTimestamp = Date.now();
this.resetInactivityTimeout();
}
/**
* Starts (or restarts) the inactivity timer.
* Call this if you want to manually restart the inactivity detection logic.
*/
public start() {
this.resetInactivityTimeout();
}
/**
* Resets the activity counter to zero.
* Useful for monitoring or testing purposes.
*/
public resetActivityCount() {
this.activityCount = 0;
}
/**
* (Private) Resets the inactivity timer.
* If the timer expires, checks if inactivityLimitMs has passed since last activity.
* If so, logs a message, calls keepAliveFn (with retries), and registers a new activity.
* Always resets the timer at the end (recursively).
*
* Dev note: If you want to change the inactivity logic, modify this method.
*/
private async resetInactivityTimeout() {
if (this.inactivityTimeout) {
clearTimeout(this.inactivityTimeout);
}
this.inactivityTimeout = setTimeout(async () => {
try {
const msSinceLastActivity = Date.now() - this.lastActivityTimestamp;
if (msSinceLastActivity >= this.config.inactivityLimit) {
console.log(
`[${this.config.instanceName || 'Hibernot'}] No activity for ${
this.config.inactivityLimit / 1000
} seconds. Executing keepAliveFn...`
);
await this.executeKeepAliveWithRetries();
// After keepAliveFn, register a new activity to reset the inactivity window.
this.registerActivity();
}
} catch (err) {
// Log error if keepAliveFn fails after all retries.
console.error(`[${this.config.instanceName || 'Hibernot'}] Keep-alive function failed after retries:`, err);
} finally {
// Always reset the timer for the next inactivity window.
this.resetInactivityTimeout();
}
}, this.config.inactivityLimit);
}
/**
* (Private) Executes keepAliveFn, retrying up to maxRetryAttempts times if it fails.
* Waits 1 second between retries.
* Throws the last error if all retries fail.
*
* Dev note: To change retry logic or backoff, edit this method.
*/
private async executeKeepAliveWithRetries() {
let lastError: Error | null = null;
const maxAttempts = this.config.maxRetryAttempts ?? 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await this.config.keepAliveFn();
return; // Success, exit function.
} catch (err) {
lastError = err as Error;
console.warn(
`[${this.config.instanceName || 'Hibernot'}] Keep-alive attempt ${attempt} failed:`,
err
);
// Wait 1 second before next retry, unless this was the last attempt.
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
// All retries failed, throw the last error.
throw lastError || new HibernotError('keepAliveFn failed after all retries');
}
/**
* Returns statistics about the Hibernot instance:
* - activityCount: number of registered activities (API hits)
* - lastActivityTimestamp: timestamp of last activity
* - instanceName: instance name (or 'Unnamed' if not set)
*
* Dev note: Extend this if you want to expose more metrics.
*/
public getStats() {
return {
activityCount: this.activityCount,
lastActivityTimestamp: this.lastActivityTimestamp,
instanceName: this.config.instanceName || 'Unnamed',
};
}
/**
* Stops the inactivity timer, preventing further keepAliveFn calls.
* Call this if you want to disable inactivity detection (e.g., during shutdown).
*/
public stop() {
if (this.inactivityTimeout) {
clearTimeout(this.inactivityTimeout);
this.inactivityTimeout = null;
}
}
}