@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
237 lines (236 loc) • 7.01 kB
JavaScript
import { fileURLToPath as __fileURLToPath } from 'url';
import { dirname as __pathDirname } from 'path';
const __filename = __fileURLToPath(import.meta.url);
const __dirname = __pathDirname(__filename);
import { EventEmitter } from "events";
import { logger } from "../../core/monitoring/logger.js";
import { LinearSyncEngine } from "./sync.js";
import { AsyncMutex } from "../../core/utils/async-mutex.js";
class LinearSyncManager extends EventEmitter {
syncEngine;
syncTimer;
pendingSyncTimer;
config;
lastSyncTime = 0;
syncMutex;
taskStore;
constructor(taskStore, authManager, config, projectRoot) {
super();
this.taskStore = taskStore;
this.syncMutex = new AsyncMutex(3e5);
this.config = {
...config,
autoSyncInterval: config.autoSyncInterval || 15,
syncOnTaskChange: config.syncOnTaskChange !== false,
syncOnSessionStart: config.syncOnSessionStart !== false,
syncOnSessionEnd: config.syncOnSessionEnd !== false,
debounceInterval: config.debounceInterval || 5e3
// 5 seconds
};
this.syncEngine = new LinearSyncEngine(
taskStore,
authManager,
config,
projectRoot
);
this.setupEventListeners();
this.setupPeriodicSync();
}
/**
* Setup event listeners for automatic sync triggers
*/
setupEventListeners() {
if (this.config.syncOnTaskChange && this.taskStore) {
this.taskStore.on("sync:needed", (changeType) => {
logger.debug(`Task change detected: ${changeType}`);
this.scheduleDebouncedSync();
});
this.taskStore.on("task:created", (task) => {
logger.debug(`Task created: ${task.title}`);
});
this.taskStore.on("task:completed", (task) => {
logger.debug(`Task completed: ${task.title}`);
});
logger.info("Task change sync enabled via EventEmitter");
}
}
/**
* Setup periodic sync timer
*/
setupPeriodicSync() {
if (!this.config.autoSync || !this.config.autoSyncInterval) {
return;
}
if (this.syncTimer) {
clearInterval(this.syncTimer);
}
const intervalMs = this.config.autoSyncInterval * 60 * 1e3;
this.syncTimer = setInterval(() => {
this.performSync("periodic");
}, intervalMs);
logger.info(
`Periodic Linear sync enabled: every ${this.config.autoSyncInterval} minutes`
);
}
/**
* Schedule a debounced sync to avoid too frequent syncs
*/
scheduleDebouncedSync() {
if (!this.config.enabled) return;
if (this.pendingSyncTimer) {
clearTimeout(this.pendingSyncTimer);
}
this.pendingSyncTimer = setTimeout(() => {
this.performSync("task-change");
}, this.config.debounceInterval);
}
/**
* Perform a sync operation
* Uses mutex to prevent concurrent sync operations (thread-safe)
*/
async performSync(trigger) {
if (!this.config.enabled) {
return {
success: false,
synced: { toLinear: 0, fromLinear: 0, updated: 0 },
conflicts: [],
errors: ["Sync is disabled"]
};
}
const release = this.syncMutex.tryAcquire(`linear-sync-${trigger}`);
if (!release) {
logger.warn(`Linear sync already in progress, skipping ${trigger} sync`);
return {
success: false,
synced: { toLinear: 0, fromLinear: 0, updated: 0 },
conflicts: [],
errors: ["Sync already in progress"]
};
}
try {
const now = Date.now();
const timeSinceLastSync = now - this.lastSyncTime;
const minInterval = 1e4;
if (trigger !== "manual" && timeSinceLastSync < minInterval) {
logger.debug(
`Skipping ${trigger} sync, too soon since last sync (${timeSinceLastSync}ms ago)`
);
return {
success: false,
synced: { toLinear: 0, fromLinear: 0, updated: 0 },
conflicts: [],
errors: [
`Too soon since last sync (wait ${minInterval - timeSinceLastSync}ms)`
]
};
}
this.emit("sync:started", { trigger });
logger.info(`Starting Linear sync (trigger: ${trigger})`);
const result = await this.syncEngine.sync();
this.lastSyncTime = now;
if (result.success) {
logger.info(
`Linear sync completed: ${result.synced.toLinear} to Linear, ${result.synced.fromLinear} from Linear, ${result.synced.updated} updated`
);
this.emit("sync:completed", { trigger, result });
} else {
logger.error(`Linear sync failed: ${result.errors.join(", ")}`);
this.emit("sync:failed", { trigger, result });
}
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(`Linear sync error: ${errorMessage}`);
const result = {
success: false,
synced: { toLinear: 0, fromLinear: 0, updated: 0 },
conflicts: [],
errors: [errorMessage]
};
this.emit("sync:failed", { trigger, result, error });
return result;
} finally {
release();
}
}
/**
* Sync on session start
*/
async syncOnStart() {
if (this.config.syncOnSessionStart) {
return await this.performSync("session-start");
}
return null;
}
/**
* Sync on session end
*/
async syncOnEnd() {
if (this.config.syncOnSessionEnd) {
return await this.performSync("session-end");
}
return null;
}
/**
* Update sync configuration
*/
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
this.syncEngine.updateConfig(newConfig);
if (newConfig.autoSyncInterval !== void 0 || newConfig.autoSync !== void 0) {
this.setupPeriodicSync();
}
}
/**
* Get sync status
*/
getStatus() {
const nextSyncTime = this.config.autoSync && this.config.autoSyncInterval ? this.lastSyncTime + this.config.autoSyncInterval * 60 * 1e3 : null;
return {
enabled: this.config.enabled,
syncInProgress: this.syncMutex.isLocked(),
lastSyncTime: this.lastSyncTime,
nextSyncTime,
config: this.config
};
}
/**
* Stop all sync activities
*/
stop() {
if (this.syncTimer) {
clearInterval(this.syncTimer);
this.syncTimer = void 0;
}
if (this.pendingSyncTimer) {
clearTimeout(this.pendingSyncTimer);
this.pendingSyncTimer = void 0;
}
this.removeAllListeners();
logger.info("Linear sync manager stopped");
}
/**
* Force an immediate sync
*/
async forceSync() {
return await this.performSync("manual");
}
}
const DEFAULT_SYNC_MANAGER_CONFIG = {
enabled: true,
direction: "bidirectional",
autoSync: true,
autoSyncInterval: 15,
// minutes
conflictResolution: "newest_wins",
syncOnTaskChange: true,
syncOnSessionStart: true,
syncOnSessionEnd: true,
debounceInterval: 5e3
// 5 seconds
};
export {
DEFAULT_SYNC_MANAGER_CONFIG,
LinearSyncManager
};
//# sourceMappingURL=sync-manager.js.map