Pluggable data storage system providing persistent storage for robot data with support for custom backend implementations and async operations.
Abstract base class for implementing custom data persistence backends.
/**
* Abstract DataStore interface for persistent storage
* @param robot - Robot instance that owns this datastore
*/
class DataStore {
constructor(robot: Robot);
// Properties
robot: Robot;
// Global data operations
async set(key: string, value: any): Promise<void>;
async get(key: string): Promise<any>;
// Object property operations
async setObject(key: string, objectKey: string, value: any): Promise<void>;
async getObject(key: string, objectKey: string): Promise<any>;
// Array operations
async setArray(key: string, value: any): Promise<void>;
// Abstract implementation methods (must implement in subclasses)
abstract async _set(key: string, value: any, table: string): Promise<void>;
abstract async _get(key: string, table: string): Promise<any>;
}Usage Example - Creating Custom DataStore:
import { DataStore } from "hubot";
class RedisDataStore extends DataStore {
constructor(robot) {
super(robot);
this.redis = new Redis(process.env.REDIS_URL);
}
async _set(key, value, table) {
const redisKey = `hubot:${table}:${key}`;
await this.redis.set(redisKey, JSON.stringify(value));
}
async _get(key, table) {
const redisKey = `hubot:${table}:${key}`;
const value = await this.redis.get(redisKey);
return value ? JSON.parse(value) : null;
}
}
// Use custom datastore
const robot = new Robot("Shell");
robot.datastore = new RedisDataStore(robot);Default in-memory implementation for development and testing.
/**
* In-memory DataStore implementation
* Stores data in memory with global and users tables
*/
class MemoryDataStore extends DataStore {
constructor(robot: Robot);
// Internal storage tables
data: {
global: Record<string, any>;
users: Record<string, any>;
};
}Usage Example:
import { Robot } from "hubot";
const robot = new Robot("Shell");
// MemoryDataStore is used by default
// Store global data
await robot.datastore.set("app-config", {
version: "1.0.0",
features: ["chat", "api"]
});
// Retrieve global data
const config = await robot.datastore.get("app-config");
// Store object properties
await robot.datastore.setObject("user-stats", "total-users", 150);
await robot.datastore.setObject("user-stats", "active-today", 42);
// Retrieve object properties
const totalUsers = await robot.datastore.getObject("user-stats", "total-users");
// Add to arrays
await robot.datastore.setArray("recent-actions", "user-login");
await robot.datastore.setArray("recent-actions", "command-executed");Methods for storing and retrieving global robot data.
/**
* Store global key-value data
* @param key - Storage key
* @param value - Value to store (will be JSON serialized)
*/
async set(key: string, value: any): Promise<void>;
/**
* Retrieve global data by key
* @param key - Storage key
* @returns Stored value or null if not found
*/
async get(key: string): Promise<any>;Usage Examples:
// Store configuration
await robot.datastore.set("api-keys", {
weather: process.env.WEATHER_API_KEY,
translation: process.env.TRANSLATION_API_KEY
});
// Store metrics
await robot.datastore.set("bot-stats", {
started: new Date().toISOString(),
messages_processed: 0,
commands_executed: 0
});
// Retrieve and update
const stats = await robot.datastore.get("bot-stats");
stats.messages_processed += 1;
await robot.datastore.set("bot-stats", stats);Methods for storing nested object properties efficiently.
/**
* Set property within a stored object
* @param key - Parent object key
* @param objectKey - Property key within the object
* @param value - Value to store
*/
async setObject(key: string, objectKey: string, value: any): Promise<void>;
/**
* Get property from within a stored object
* @param key - Parent object key
* @param objectKey - Property key within the object
* @returns Property value or null if not found
*/
async getObject(key: string, objectKey: string): Promise<any>;Usage Examples:
// Store user preferences individually
await robot.datastore.setObject("user-prefs", "user123", {
theme: "dark",
notifications: true,
timezone: "UTC"
});
// Update specific preference
await robot.datastore.setObject("user-prefs", "user123.theme", "light");
// Retrieve specific preference
const theme = await robot.datastore.getObject("user-prefs", "user123.theme");
// Store room configurations
await robot.datastore.setObject("room-config", "general", {
auto_greet: true,
moderators: ["admin1", "admin2"]
});
await robot.datastore.setObject("room-config", "development", {
auto_greet: false,
debug_mode: true
});Methods for appending data to stored arrays.
/**
* Append value to stored array
* @param key - Array key
* @param value - Value to append
*/
async setArray(key: string, value: any): Promise<void>;Usage Examples:
// Log recent activities
await robot.datastore.setArray("activity-log", {
timestamp: new Date().toISOString(),
action: "user-joined",
user: "alice",
room: "general"
});
await robot.datastore.setArray("activity-log", {
timestamp: new Date().toISOString(),
action: "command-executed",
command: "weather",
user: "bob"
});
// Store error logs
await robot.datastore.setArray("error-log", {
timestamp: new Date().toISOString(),
error: error.message,
stack: error.stack
});
// Retrieve full arrays
const activities = await robot.datastore.get("activity-log");
const recentActivities = activities.slice(-10); // Last 10 activitiesHandling DataStore unavailability and connection issues.
/**
* Error class for DataStore unavailable conditions
* Thrown when datastore operations fail due to backend unavailability
*/
class DataStoreUnavailable extends Error {
constructor(message: string);
}Usage Examples:
import { DataStoreUnavailable } from "hubot";
// Graceful error handling
robot.hear(/save data (.+)/i, async (res) => {
const data = res.match[1];
try {
await robot.datastore.set("user-data", data);
res.send("Data saved successfully!");
} catch (error) {
if (error instanceof DataStoreUnavailable) {
res.send("Sorry, data storage is temporarily unavailable.");
robot.logger.error("DataStore unavailable:", error);
} else {
res.send("An error occurred while saving data.");
robot.logger.error("DataStore error:", error);
}
}
});
// Custom DataStore with error handling
class DatabaseDataStore extends DataStore {
async _set(key, value, table) {
try {
await this.database.query(
"INSERT INTO datastore (table_name, key, value) VALUES (?, ?, ?)",
[table, key, JSON.stringify(value)]
);
} catch (error) {
if (error.code === 'CONNECTION_LOST') {
throw new DataStoreUnavailable("Database connection lost");
}
throw error;
}
}
async _get(key, table) {
try {
const result = await this.database.query(
"SELECT value FROM datastore WHERE table_name = ? AND key = ?",
[table, key]
);
return result.length > 0 ? JSON.parse(result[0].value) : null;
} catch (error) {
if (error.code === 'CONNECTION_LOST') {
throw new DataStoreUnavailable("Database connection lost");
}
throw error;
}
}
}DataStore works alongside Brain for comprehensive data persistence.
// Brain uses DataStore for user data persistence
robot.brain.on('save', async (data) => {
try {
await robot.datastore.set('brain-data', data);
robot.logger.info('Brain data saved to datastore');
} catch (error) {
robot.logger.error('Failed to save brain data:', error);
}
});
// Load brain data from DataStore on startup
robot.brain.on('loaded', async () => {
try {
const savedData = await robot.datastore.get('brain-data');
if (savedData) {
robot.brain.mergeData(savedData);
robot.logger.info('Brain data loaded from datastore');
}
} catch (error) {
robot.logger.error('Failed to load brain data:', error);
}
});Examples of implementing DataStore for different persistence backends.
// File-based DataStore
class FileDataStore extends DataStore {
constructor(robot) {
super(robot);
this.dataDir = process.env.HUBOT_DATA_DIR || './data';
this.ensureDirectory();
}
async _set(key, value, table) {
const filePath = path.join(this.dataDir, `${table}_${key}.json`);
await fs.writeFile(filePath, JSON.stringify(value), 'utf8');
}
async _get(key, table) {
const filePath = path.join(this.dataDir, `${table}_${key}.json`);
try {
const data = await fs.readFile(filePath, 'utf8');
return JSON.parse(data);
} catch (error) {
if (error.code === 'ENOENT') return null;
throw error;
}
}
}
// MongoDB DataStore
class MongoDataStore extends DataStore {
constructor(robot) {
super(robot);
this.db = new MongoClient(process.env.MONGODB_URL).db('hubot');
}
async _set(key, value, table) {
await this.db.collection(table).replaceOne(
{ _id: key },
{ _id: key, value: value },
{ upsert: true }
);
}
async _get(key, table) {
const doc = await this.db.collection(table).findOne({ _id: key });
return doc ? doc.value : null;
}
}abstract class DataStore {
robot: Robot;
constructor(robot: Robot);
// Public API
set(key: string, value: any): Promise<void>;
get(key: string): Promise<any>;
setObject(key: string, objectKey: string, value: any): Promise<void>;
getObject(key: string, objectKey: string): Promise<any>;
setArray(key: string, value: any): Promise<void>;
// Abstract methods for implementation
abstract _set(key: string, value: any, table: string): Promise<void>;
abstract _get(key: string, table: string): Promise<any>;
}
interface DataStoreOptions {
timeout?: number;
retries?: number;
[key: string]: any;
}
interface StoredData {
key: string;
value: any;
table: string;
timestamp?: Date;
}