Add ActivityManager and integrate activity tracking functionality
All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good
- Introduced the ActivityManager class to handle object activity tracking, including viewing and editing events. - Implemented methods for setting, clearing, and retrieving activities, as well as subscribing to activity updates via websockets. - Replaced the LockManager with ActivityManager in SocketUser and SocketManager classes to streamline activity management. - Added comprehensive tests for ActivityManager to ensure correct functionality and error handling. - Removed LockManager and its associated tests to eliminate redundancy in functionality.
This commit is contained in:
parent
e80022513f
commit
49226290b6
258
src/activity/__tests__/activitymanager.test.js
Normal file
258
src/activity/__tests__/activitymanager.test.js
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../database/nats.js', () => ({
|
||||||
|
natsServer: {
|
||||||
|
publish: jest.fn().mockResolvedValue({ success: true }),
|
||||||
|
subscribe: jest.fn().mockResolvedValue({ success: true }),
|
||||||
|
removeSubscription: jest.fn().mockResolvedValue({ success: true })
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../database/redis.js', () => ({
|
||||||
|
redisServer: {
|
||||||
|
setKey: jest.fn().mockResolvedValue(undefined),
|
||||||
|
getKey: jest.fn().mockResolvedValue(null),
|
||||||
|
deleteKey: jest.fn().mockResolvedValue(undefined),
|
||||||
|
getKeysByPattern: jest.fn().mockResolvedValue([])
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../database/database.js', () => ({
|
||||||
|
getObject: jest.fn().mockResolvedValue({
|
||||||
|
_id: 'user-123',
|
||||||
|
username: 'testuser',
|
||||||
|
name: 'Test User',
|
||||||
|
profileImage: { _id: 'file-123' }
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../database/utils.js', () => ({
|
||||||
|
expandObjectIds: jest.fn(value => value)
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../database/schemas/management/user.schema.js', () => ({
|
||||||
|
userModel: {}
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('log4js', () => ({
|
||||||
|
default: {
|
||||||
|
getLogger: () => ({
|
||||||
|
level: 'info',
|
||||||
|
debug: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
trace: jest.fn(),
|
||||||
|
info: jest.fn()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../config.js', () => ({
|
||||||
|
loadConfig: jest.fn(() => ({
|
||||||
|
server: {
|
||||||
|
logLevel: 'info'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { ActivityManager } = await import('../activitymanager.js');
|
||||||
|
const { natsServer } = await import('../../database/nats.js');
|
||||||
|
const { redisServer } = await import('../../database/redis.js');
|
||||||
|
|
||||||
|
describe('ActivityManager', () => {
|
||||||
|
let mockSocketClient;
|
||||||
|
let activityManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
mockSocketClient = {
|
||||||
|
socketId: 'test-socket-id',
|
||||||
|
socket: {
|
||||||
|
emit: jest.fn()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
activityManager = new ActivityManager(mockSocketClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setActivity', () => {
|
||||||
|
it('should set activity in Redis and publish via NATS', async () => {
|
||||||
|
const testObject = {
|
||||||
|
_id: 'test-id-123',
|
||||||
|
type: 'printer',
|
||||||
|
user: 'user-123',
|
||||||
|
mode: 'viewing'
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await activityManager.setActivity(testObject);
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
success: true,
|
||||||
|
activity: expect.objectContaining({
|
||||||
|
_id: 'test-id-123',
|
||||||
|
type: 'printer',
|
||||||
|
mode: 'viewing'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(redisServer.setKey).toHaveBeenCalledWith(
|
||||||
|
'activity:printers:test-id-123:user-123',
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: 'test-id-123',
|
||||||
|
type: 'printer',
|
||||||
|
user: 'user-123',
|
||||||
|
mode: 'viewing'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(natsServer.publish).toHaveBeenCalledWith(
|
||||||
|
'activity.printers.test-id-123',
|
||||||
|
expect.objectContaining({
|
||||||
|
mode: 'viewing'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject editing when another user is already editing', async () => {
|
||||||
|
redisServer.getKeysByPattern.mockResolvedValueOnce([
|
||||||
|
'activity:printers:test-id-123:user-456'
|
||||||
|
]);
|
||||||
|
redisServer.getKey.mockResolvedValueOnce({
|
||||||
|
_id: 'test-id-123',
|
||||||
|
type: 'printer',
|
||||||
|
user: 'user-456',
|
||||||
|
mode: 'editing'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await activityManager.setActivity({
|
||||||
|
_id: 'test-id-123',
|
||||||
|
type: 'printer',
|
||||||
|
user: 'user-123',
|
||||||
|
mode: 'editing'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
success: false,
|
||||||
|
conflict: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(redisServer.setKey).not.toHaveBeenCalled();
|
||||||
|
expect(natsServer.publish).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearActivity', () => {
|
||||||
|
it('should clear activity from Redis and publish via NATS', async () => {
|
||||||
|
const testObject = {
|
||||||
|
_id: 'test-id-123',
|
||||||
|
type: 'printer',
|
||||||
|
user: 'user-123'
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await activityManager.clearActivity(testObject);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(redisServer.deleteKey).toHaveBeenCalledWith(
|
||||||
|
'activity:printers:test-id-123:user-123'
|
||||||
|
);
|
||||||
|
expect(natsServer.publish).toHaveBeenCalledWith(
|
||||||
|
'activity.printers.test-id-123',
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: 'test-id-123',
|
||||||
|
type: 'printer',
|
||||||
|
user: 'user-123',
|
||||||
|
mode: null
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getObjectActivities', () => {
|
||||||
|
it('should return hydrated activities for an object', async () => {
|
||||||
|
redisServer.getKeysByPattern.mockResolvedValueOnce([
|
||||||
|
'activity:printers:test-id-123:user-123'
|
||||||
|
]);
|
||||||
|
redisServer.getKey.mockResolvedValueOnce({
|
||||||
|
_id: 'test-id-123',
|
||||||
|
type: 'printer',
|
||||||
|
user: 'user-123',
|
||||||
|
mode: 'editing'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await activityManager.getObjectActivities({
|
||||||
|
_id: 'test-id-123',
|
||||||
|
type: 'printer'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
mode: 'editing'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('subscribeToObjectActivity', () => {
|
||||||
|
it('should subscribe to NATS activity updates', async () => {
|
||||||
|
const result = await activityManager.subscribeToObjectActivity(
|
||||||
|
'test-id-123',
|
||||||
|
'printer'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(natsServer.subscribe).toHaveBeenCalledWith(
|
||||||
|
'activity.printers.test-id-123',
|
||||||
|
'test-socket-id',
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not create duplicate subscriptions for the same object', async () => {
|
||||||
|
await activityManager.subscribeToObjectActivity('test-id-123', 'printer');
|
||||||
|
await activityManager.subscribeToObjectActivity('test-id-123', 'printer');
|
||||||
|
|
||||||
|
expect(natsServer.subscribe).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit activity updates with the full activities list', async () => {
|
||||||
|
let natsCallback;
|
||||||
|
natsServer.subscribe.mockImplementation(async (_subject, _owner, callback) => {
|
||||||
|
natsCallback = callback;
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
redisServer.getKeysByPattern.mockResolvedValue([
|
||||||
|
'activity:printers:test-id-123:user-123'
|
||||||
|
]);
|
||||||
|
redisServer.getKey.mockResolvedValue({
|
||||||
|
_id: 'test-id-123',
|
||||||
|
type: 'printer',
|
||||||
|
user: 'user-123',
|
||||||
|
mode: 'editing'
|
||||||
|
});
|
||||||
|
|
||||||
|
await activityManager.subscribeToObjectActivity('test-id-123', 'printer');
|
||||||
|
await natsCallback('activity.printers.test-id-123', {
|
||||||
|
_id: 'test-id-123',
|
||||||
|
type: 'printer',
|
||||||
|
user: 'user-123',
|
||||||
|
mode: 'editing'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockSocketClient.socket.emit).toHaveBeenCalledWith(
|
||||||
|
'activityUpdate',
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: 'test-id-123',
|
||||||
|
objectType: 'printer',
|
||||||
|
activities: [
|
||||||
|
expect.objectContaining({
|
||||||
|
mode: 'editing'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
262
src/activity/activitymanager.js
Normal file
262
src/activity/activitymanager.js
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
import log4js from 'log4js';
|
||||||
|
import { loadConfig } from '../config.js';
|
||||||
|
import { natsServer } from '../database/nats.js';
|
||||||
|
import { redisServer } from '../database/redis.js';
|
||||||
|
import { getObject } from '../database/database.js';
|
||||||
|
import { expandObjectIds } from '../database/utils.js';
|
||||||
|
import { userModel } from '../database/schemas/management/user.schema.js';
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
const logger = log4js.getLogger('Activity Manager');
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
const getSubscriptionKey = (subject, owner) => `${subject}:${owner}`;
|
||||||
|
|
||||||
|
const normalizeId = value => {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof value === 'string' ? value : value.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeType = type => {
|
||||||
|
if (!type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof type === 'string' ? type : type.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActivityRedisKey = (type, id, userId) =>
|
||||||
|
`activity:${normalizeType(type)}s:${normalizeId(id)}:${normalizeId(userId)}`;
|
||||||
|
|
||||||
|
const getActivityPattern = (type, id) =>
|
||||||
|
`activity:${normalizeType(type)}s:${normalizeId(id)}:*`;
|
||||||
|
|
||||||
|
const getActivitySubject = (type, id) =>
|
||||||
|
`activity.${normalizeType(type)}s.${normalizeId(id)}`;
|
||||||
|
|
||||||
|
async function hydrateActivity(activity) {
|
||||||
|
const expanded = expandObjectIds({ ...activity });
|
||||||
|
const userId = expanded.user?._id || expanded.user;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
const user = await getObject({
|
||||||
|
model: userModel,
|
||||||
|
id: userId.toString(),
|
||||||
|
populate: ['profileImage'],
|
||||||
|
cached: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
expanded.user = expandObjectIds(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ActivityManager tracks object viewing/editing activity and broadcasts events via websockets.
|
||||||
|
*/
|
||||||
|
export class ActivityManager {
|
||||||
|
constructor(socketClient) {
|
||||||
|
this.socketClient = socketClient;
|
||||||
|
this.subscriptions = new Set();
|
||||||
|
this.objectSubscriptions = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
emitActivityUpdate(id, objectType, activity, activities) {
|
||||||
|
this.socketClient.socket.emit('activityUpdate', {
|
||||||
|
_id: normalizeId(id),
|
||||||
|
objectType: normalizeType(objectType),
|
||||||
|
activity,
|
||||||
|
activities
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getObjectActivities(object) {
|
||||||
|
const objectId = normalizeId(object?._id);
|
||||||
|
const objectType = normalizeType(object?.type);
|
||||||
|
|
||||||
|
logger.debug('Getting activities for object:', objectId);
|
||||||
|
try {
|
||||||
|
const pattern = getActivityPattern(objectType, objectId);
|
||||||
|
const keys = await redisServer.getKeysByPattern(pattern);
|
||||||
|
const activities = [];
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const storedActivity = await redisServer.getKey(key);
|
||||||
|
if (storedActivity?.mode) {
|
||||||
|
activities.push(await hydrateActivity(storedActivity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return activities;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`Error getting activities for object ${objectId}:`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setActivity(object) {
|
||||||
|
const objectId = normalizeId(object?._id);
|
||||||
|
const objectType = normalizeType(object?.type);
|
||||||
|
const userId = normalizeId(object?.user);
|
||||||
|
|
||||||
|
logger.debug('Setting activity:', objectId, object.mode);
|
||||||
|
try {
|
||||||
|
if (object.mode === 'editing') {
|
||||||
|
const pattern = getActivityPattern(objectType, objectId);
|
||||||
|
const keys = await redisServer.getKeysByPattern(pattern);
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const storedActivity = await redisServer.getKey(key);
|
||||||
|
const activityUserId = normalizeId(storedActivity?.user);
|
||||||
|
|
||||||
|
if (
|
||||||
|
storedActivity?.mode === 'editing' &&
|
||||||
|
activityUserId &&
|
||||||
|
activityUserId !== userId
|
||||||
|
) {
|
||||||
|
logger.info(
|
||||||
|
`Rejected editing activity for user ${userId} on object ${objectId}; already being edited by ${activityUserId}`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
conflict: true,
|
||||||
|
activity: await hydrateActivity(storedActivity)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const redisKey = getActivityRedisKey(objectType, objectId, userId);
|
||||||
|
const activityPayload = {
|
||||||
|
_id: objectId,
|
||||||
|
type: objectType,
|
||||||
|
user: userId,
|
||||||
|
mode: object.mode
|
||||||
|
};
|
||||||
|
|
||||||
|
await redisServer.setKey(redisKey, activityPayload);
|
||||||
|
|
||||||
|
const hydratedPayload = await hydrateActivity(activityPayload);
|
||||||
|
const subject = getActivitySubject(objectType, objectId);
|
||||||
|
await natsServer.publish(subject, hydratedPayload);
|
||||||
|
logger.info(`Activity event published for id: ${objectId}`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
activity: hydratedPayload
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Error setting activity for object ${objectId}:`, err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearActivity(object) {
|
||||||
|
const objectId = normalizeId(object?._id);
|
||||||
|
const objectType = normalizeType(object?.type);
|
||||||
|
const userId = normalizeId(object?.user);
|
||||||
|
const redisKey = getActivityRedisKey(objectType, objectId, userId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug('Clearing activity:', objectId);
|
||||||
|
await redisServer.deleteKey(redisKey);
|
||||||
|
|
||||||
|
const clearedPayload = {
|
||||||
|
_id: objectId,
|
||||||
|
type: objectType,
|
||||||
|
user: userId,
|
||||||
|
mode: null
|
||||||
|
};
|
||||||
|
const subject = getActivitySubject(objectType, objectId);
|
||||||
|
await natsServer.publish(subject, clearedPayload);
|
||||||
|
logger.info(`Cleared activity and published event: ${objectId}`);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Error clearing activity for object ${objectId}:`, err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async subscribeToObjectActivity(id, objectType) {
|
||||||
|
const objectId = normalizeId(id);
|
||||||
|
const normalizedType = normalizeType(objectType);
|
||||||
|
const subject = getActivitySubject(normalizedType, objectId);
|
||||||
|
const owner = this.socketClient.socketId;
|
||||||
|
const subscriptionKey = getSubscriptionKey(subject, owner);
|
||||||
|
const objectKey = `${normalizedType}:${objectId}`;
|
||||||
|
|
||||||
|
logger.debug('Subscribing to object activity...', objectId, normalizedType);
|
||||||
|
|
||||||
|
if (!this.subscriptions.has(subscriptionKey)) {
|
||||||
|
await natsServer.subscribe(subject, owner, async (_subject, value) => {
|
||||||
|
const expandedValue = expandObjectIds(value);
|
||||||
|
const hydratedValue = value?.mode
|
||||||
|
? await hydrateActivity(expandedValue)
|
||||||
|
: expandedValue;
|
||||||
|
const activities = await this.getObjectActivities({
|
||||||
|
_id: objectId,
|
||||||
|
type: normalizedType
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.trace('Activity update event:', objectId);
|
||||||
|
this.emitActivityUpdate(
|
||||||
|
objectId,
|
||||||
|
normalizedType,
|
||||||
|
hydratedValue,
|
||||||
|
activities
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.subscriptions.add(subscriptionKey);
|
||||||
|
this.objectSubscriptions.set(objectKey, subscriptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const activities = await this.getObjectActivities({
|
||||||
|
_id: objectId,
|
||||||
|
type: normalizedType
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, activities };
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeObjectActivityListener(id, objectType) {
|
||||||
|
const objectId = normalizeId(id);
|
||||||
|
const normalizedType = normalizeType(objectType);
|
||||||
|
const subject = getActivitySubject(normalizedType, objectId);
|
||||||
|
const owner = this.socketClient.socketId;
|
||||||
|
const subscriptionKey = getSubscriptionKey(subject, owner);
|
||||||
|
const objectKey = `${normalizedType}:${objectId}`;
|
||||||
|
|
||||||
|
await natsServer.removeSubscription(subject, owner);
|
||||||
|
this.subscriptions.delete(subscriptionKey);
|
||||||
|
this.objectSubscriptions.delete(objectKey);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeAllListeners() {
|
||||||
|
logger.debug('Removing all activity listeners...');
|
||||||
|
const removePromises = Array.from(this.subscriptions).map(
|
||||||
|
subscriptionKey => {
|
||||||
|
const separatorIndex = subscriptionKey.indexOf(':');
|
||||||
|
const subject = subscriptionKey.slice(0, separatorIndex);
|
||||||
|
const owner = subscriptionKey.slice(separatorIndex + 1);
|
||||||
|
return natsServer.removeSubscription(subject, owner);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(removePromises);
|
||||||
|
this.subscriptions.clear();
|
||||||
|
this.objectSubscriptions.clear();
|
||||||
|
logger.debug(`Removed ${removePromises.length} activity listener(s)`);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,226 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../database/nats.js', () => ({
|
|
||||||
natsServer: {
|
|
||||||
publish: jest.fn().mockResolvedValue({ success: true }),
|
|
||||||
subscribe: jest.fn().mockResolvedValue({ success: true }),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../database/redis.js', () => ({
|
|
||||||
redisServer: {
|
|
||||||
setKey: jest.fn().mockResolvedValue(undefined),
|
|
||||||
getKey: jest.fn().mockResolvedValue(null),
|
|
||||||
deleteKey: jest.fn().mockResolvedValue(undefined),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
|
||||||
default: {
|
|
||||||
getLogger: () => ({
|
|
||||||
level: 'info',
|
|
||||||
debug: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
trace: jest.fn(),
|
|
||||||
info: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../config.js', () => ({
|
|
||||||
loadConfig: jest.fn(() => ({
|
|
||||||
server: {
|
|
||||||
logLevel: 'info',
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { LockManager } = await import('../lockmanager.js');
|
|
||||||
const { natsServer } = await import('../../database/nats.js');
|
|
||||||
const { redisServer } = await import('../../database/redis.js');
|
|
||||||
|
|
||||||
describe('LockManager', () => {
|
|
||||||
let mockSocketClient;
|
|
||||||
let lockManager;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
mockSocketClient = {
|
|
||||||
id: 'test-socket-id',
|
|
||||||
socket: {
|
|
||||||
emit: jest.fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
lockManager = new LockManager(mockSocketClient);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('lockObject', () => {
|
|
||||||
it('should lock an object and publish via NATS', async () => {
|
|
||||||
const testObject = {
|
|
||||||
_id: 'test-id-123',
|
|
||||||
type: 'product',
|
|
||||||
user: 'user-123',
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await lockManager.lockObject(testObject);
|
|
||||||
|
|
||||||
expect(result).toBe(true);
|
|
||||||
expect(redisServer.setKey).toHaveBeenCalledWith(
|
|
||||||
'locks:products:test-id-123',
|
|
||||||
expect.objectContaining({
|
|
||||||
_id: 'test-id-123',
|
|
||||||
type: 'product',
|
|
||||||
user: 'user-123',
|
|
||||||
locked: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(natsServer.publish).toHaveBeenCalledWith(
|
|
||||||
'locks.products.test-id-123',
|
|
||||||
expect.objectContaining({
|
|
||||||
locked: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle errors when locking fails', async () => {
|
|
||||||
const testObject = {
|
|
||||||
_id: 'test-id-123',
|
|
||||||
type: 'product',
|
|
||||||
};
|
|
||||||
|
|
||||||
redisServer.setKey.mockRejectedValueOnce(new Error('Redis error'));
|
|
||||||
|
|
||||||
await expect(lockManager.lockObject(testObject)).rejects.toThrow('Redis error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('unlockObject', () => {
|
|
||||||
it('should unlock an object when user matches', async () => {
|
|
||||||
const testObject = {
|
|
||||||
_id: 'test-id-123',
|
|
||||||
type: 'product',
|
|
||||||
user: 'user-123',
|
|
||||||
};
|
|
||||||
|
|
||||||
redisServer.getKey.mockResolvedValueOnce({
|
|
||||||
_id: 'test-id-123',
|
|
||||||
type: 'product',
|
|
||||||
user: 'user-123',
|
|
||||||
locked: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await lockManager.unlockObject(testObject);
|
|
||||||
|
|
||||||
expect(result).toBe(true);
|
|
||||||
expect(redisServer.deleteKey).toHaveBeenCalledWith('locks:products:test-id-123');
|
|
||||||
expect(natsServer.publish).toHaveBeenCalledWith(
|
|
||||||
'locks.products.test-id-123',
|
|
||||||
expect.objectContaining({
|
|
||||||
_id: 'test-id-123',
|
|
||||||
type: 'product',
|
|
||||||
locked: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not unlock when user does not match', async () => {
|
|
||||||
const testObject = {
|
|
||||||
_id: 'test-id-123',
|
|
||||||
type: 'product',
|
|
||||||
user: 'user-123',
|
|
||||||
};
|
|
||||||
|
|
||||||
redisServer.getKey.mockResolvedValueOnce({
|
|
||||||
_id: 'test-id-123',
|
|
||||||
type: 'product',
|
|
||||||
user: 'different-user',
|
|
||||||
locked: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await lockManager.unlockObject(testObject);
|
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
|
||||||
expect(redisServer.deleteKey).not.toHaveBeenCalled();
|
|
||||||
expect(natsServer.publish).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle errors when unlocking fails', async () => {
|
|
||||||
const testObject = {
|
|
||||||
_id: 'test-id-123',
|
|
||||||
type: 'product',
|
|
||||||
user: 'user-123',
|
|
||||||
};
|
|
||||||
|
|
||||||
redisServer.getKey.mockRejectedValueOnce(new Error('Redis error'));
|
|
||||||
|
|
||||||
await expect(lockManager.unlockObject(testObject)).rejects.toThrow('Redis error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getObjectLock', () => {
|
|
||||||
it('should return locked status when object is locked', async () => {
|
|
||||||
const testObject = {
|
|
||||||
_id: 'test-id-123',
|
|
||||||
type: 'product',
|
|
||||||
};
|
|
||||||
|
|
||||||
redisServer.getKey.mockResolvedValueOnce({
|
|
||||||
_id: 'test-id-123',
|
|
||||||
type: 'product',
|
|
||||||
user: 'user-123',
|
|
||||||
locked: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await lockManager.getObjectLock(testObject);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
_id: 'test-id-123',
|
|
||||||
type: 'product',
|
|
||||||
user: 'user-123',
|
|
||||||
locked: true,
|
|
||||||
});
|
|
||||||
expect(redisServer.getKey).toHaveBeenCalledWith('locks:products:test-id-123');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return unlocked status when object is not locked', async () => {
|
|
||||||
const testObject = {
|
|
||||||
_id: 'test-id-123',
|
|
||||||
type: 'product',
|
|
||||||
};
|
|
||||||
|
|
||||||
redisServer.getKey.mockResolvedValueOnce(null);
|
|
||||||
|
|
||||||
const result = await lockManager.getObjectLock(testObject);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
_id: 'test-id-123',
|
|
||||||
locked: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle errors when getting lock status fails', async () => {
|
|
||||||
const testObject = {
|
|
||||||
_id: 'test-id-123',
|
|
||||||
type: 'product',
|
|
||||||
};
|
|
||||||
|
|
||||||
redisServer.getKey.mockRejectedValueOnce(new Error('Redis error'));
|
|
||||||
|
|
||||||
await expect(lockManager.getObjectLock(testObject)).rejects.toThrow('Redis error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('setupLocksListeners', () => {
|
|
||||||
it('should subscribe to NATS lock changes', () => {
|
|
||||||
expect(natsServer.subscribe).toHaveBeenCalledWith(
|
|
||||||
'locks.>',
|
|
||||||
'test-socket-id',
|
|
||||||
expect.any(Function)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
import { natsServer } from '../database/nats.js';
|
|
||||||
import { redisServer } from '../database/redis.js';
|
|
||||||
import log4js from 'log4js';
|
|
||||||
import { loadConfig } from '../config.js';
|
|
||||||
const config = loadConfig();
|
|
||||||
|
|
||||||
// Setup logger
|
|
||||||
const logger = log4js.getLogger('Lock Manager');
|
|
||||||
logger.level = config.server.logLevel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LockManager handles distributed locking and broadcasts lock events via websockets.
|
|
||||||
*/
|
|
||||||
export class LockManager {
|
|
||||||
constructor(socketClient) {
|
|
||||||
this.socketClient = socketClient;
|
|
||||||
this.setupLocksListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
async lockObject(object) {
|
|
||||||
// Persist lock in Redis and publish via NATS
|
|
||||||
logger.debug('Locking object:', object._id);
|
|
||||||
try {
|
|
||||||
const redisKey = `locks:${object.type}s:${object._id}`;
|
|
||||||
const lockPayload = {
|
|
||||||
...object,
|
|
||||||
locked: true
|
|
||||||
};
|
|
||||||
await redisServer.setKey(redisKey, lockPayload);
|
|
||||||
const subject = `locks.${object.type}s.${object._id}`;
|
|
||||||
await natsServer.publish(subject, lockPayload);
|
|
||||||
logger.info(`Lock event published for id: ${object._id}`);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Error locking object ${object._id}:`, err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async unlockObject(object) {
|
|
||||||
// Remove lock from Redis (if owned by user) and publish via NATS
|
|
||||||
const redisKey = `locks:${object.type}s:${object._id}`;
|
|
||||||
try {
|
|
||||||
logger.debug('Checking user can unlock:', object._id);
|
|
||||||
|
|
||||||
const lockEvent = await redisServer.getKey(redisKey);
|
|
||||||
|
|
||||||
if (lockEvent?.user === object.user) {
|
|
||||||
logger.debug('Unlocking object:', object._id);
|
|
||||||
await redisServer.deleteKey(redisKey);
|
|
||||||
const subject = `locks.${object.type}s.${object._id}`;
|
|
||||||
await natsServer.publish(subject, {
|
|
||||||
_id: object._id,
|
|
||||||
type: object.type,
|
|
||||||
locked: false
|
|
||||||
});
|
|
||||||
logger.info(`Unlocked object and published event: ${object._id}`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Error unlocking object ${object._id}:`, err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getObjectLock(object) {
|
|
||||||
// Get the current lock status of an object
|
|
||||||
logger.info('Getting lock status for object:', object._id);
|
|
||||||
try {
|
|
||||||
const lockKey = `locks:${object.type}s:${object._id}`;
|
|
||||||
const lockValue = await redisServer.getKey(lockKey);
|
|
||||||
|
|
||||||
if (lockValue) {
|
|
||||||
// Object is locked
|
|
||||||
logger.debug(`Object ${object._id} is locked`);
|
|
||||||
return {
|
|
||||||
...lockValue,
|
|
||||||
locked: true
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Object is not locked
|
|
||||||
logger.debug(`Object ${object._id} is not locked`);
|
|
||||||
return {
|
|
||||||
_id: object._id,
|
|
||||||
locked: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Error getting lock status for object ${object._id}:`, err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupLocksListeners() {
|
|
||||||
// Subscribe to NATS subject for lock changes and emit via socket
|
|
||||||
const subject = 'locks.>';
|
|
||||||
natsServer
|
|
||||||
.subscribe(subject, this.socketClient.id, (_subject, value) => {
|
|
||||||
// Expected subjects: locks.{type}s.{id}
|
|
||||||
const parts = _subject.split('.');
|
|
||||||
const last = parts[parts.length - 1];
|
|
||||||
const id = last;
|
|
||||||
const payload =
|
|
||||||
typeof value === 'object'
|
|
||||||
? value
|
|
||||||
: { _id: id, locked: !!value?.locked };
|
|
||||||
logger.debug('Lock event received:', _subject);
|
|
||||||
this.socketClient.socket.emit('lockUpdate', payload);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
logger.info('Subscribed to NATS for lock changes.');
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
logger.error('Failed to subscribe to NATS lock changes:', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -40,8 +40,8 @@ jest.unstable_mockModule('../../database/redis.js', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.unstable_mockModule('../../lock/lockmanager.js', () => ({
|
jest.unstable_mockModule('../../activity/activitymanager.js', () => ({
|
||||||
LockManager: jest.fn(),
|
ActivityManager: jest.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.unstable_mockModule('../../templates/templatemanager.js', () => ({
|
jest.unstable_mockModule('../../templates/templatemanager.js', () => ({
|
||||||
|
|||||||
@ -18,11 +18,14 @@ jest.unstable_mockModule('../../utils.js', () => ({
|
|||||||
generateHostOTP: jest.fn()
|
generateHostOTP: jest.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.unstable_mockModule('../../lock/lockmanager.js', () => ({
|
jest.unstable_mockModule('../../activity/activitymanager.js', () => ({
|
||||||
LockManager: jest.fn().mockImplementation(() => ({
|
ActivityManager: jest.fn().mockImplementation(() => ({
|
||||||
lockObject: jest.fn(),
|
setActivity: jest.fn(),
|
||||||
unlockObject: jest.fn(),
|
clearActivity: jest.fn(),
|
||||||
getObjectLock: jest.fn()
|
getObjectActivities: jest.fn(),
|
||||||
|
subscribeToObjectActivity: jest.fn(),
|
||||||
|
removeObjectActivityListener: jest.fn(),
|
||||||
|
removeAllListeners: jest.fn()
|
||||||
}))
|
}))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -128,7 +131,10 @@ describe('SocketUser', () => {
|
|||||||
'authenticate',
|
'authenticate',
|
||||||
expect.any(Function)
|
expect.any(Function)
|
||||||
);
|
);
|
||||||
expect(mockSocket.on).toHaveBeenCalledWith('lock', expect.any(Function));
|
expect(mockSocket.on).toHaveBeenCalledWith(
|
||||||
|
'setActivity',
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
expect(mockSocket.on).toHaveBeenCalledWith(
|
expect(mockSocket.on).toHaveBeenCalledWith(
|
||||||
'disconnect',
|
'disconnect',
|
||||||
expect.any(Function)
|
expect.any(Function)
|
||||||
@ -183,26 +189,26 @@ describe('SocketUser', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('lock event handlers', () => {
|
describe('activity event handlers', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
socketUser.user = { _id: 'user-id' };
|
socketUser.user = { _id: 'user-id' };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handleLockEvent should call lockManager.lockObject', async () => {
|
it('handleSetActivityEvent should call activityManager.setActivity', async () => {
|
||||||
const data = { _id: 'obj-1', type: 'printer' };
|
const data = { _id: 'obj-1', type: 'printer', mode: 'viewing' };
|
||||||
await socketUser.handleLockEvent(data);
|
await socketUser.handleSetActivityEvent(data);
|
||||||
|
|
||||||
expect(socketUser.lockManager.lockObject).toHaveBeenCalledWith({
|
expect(socketUser.activityManager.setActivity).toHaveBeenCalledWith({
|
||||||
...data,
|
...data,
|
||||||
user: 'user-id'
|
user: 'user-id'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handleUnlockEvent should call lockManager.unlockObject', async () => {
|
it('handleClearActivityEvent should call activityManager.clearActivity', async () => {
|
||||||
const data = { _id: 'obj-1' };
|
const data = { _id: 'obj-1', type: 'printer' };
|
||||||
await socketUser.handleUnlockEvent(data);
|
await socketUser.handleClearActivityEvent(data);
|
||||||
|
|
||||||
expect(socketUser.lockManager.unlockObject).toHaveBeenCalledWith({
|
expect(socketUser.activityManager.clearActivity).toHaveBeenCalledWith({
|
||||||
...data,
|
...data,
|
||||||
user: 'user-id'
|
user: 'user-id'
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import log4js from 'log4js';
|
|||||||
// Load configuration
|
// Load configuration
|
||||||
import { loadConfig } from '../config.js';
|
import { loadConfig } from '../config.js';
|
||||||
import { SocketUser } from './socketuser.js';
|
import { SocketUser } from './socketuser.js';
|
||||||
import { LockManager } from '../lock/lockmanager.js';
|
|
||||||
import { UpdateManager } from '../updates/updatemanager.js';
|
import { UpdateManager } from '../updates/updatemanager.js';
|
||||||
import { TemplateManager } from '../templates/templatemanager.js';
|
import { TemplateManager } from '../templates/templatemanager.js';
|
||||||
import { SocketHost } from './sockethost.js';
|
import { SocketHost } from './sockethost.js';
|
||||||
@ -53,7 +52,7 @@ export class SocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addUser(socket) {
|
async addUser(socket) {
|
||||||
const socketUser = new SocketUser(socket, this, this.lockManager);
|
const socketUser = new SocketUser(socket, this);
|
||||||
this.socketUsers.set(socketUser.id, socketUser);
|
this.socketUsers.set(socketUser.id, socketUser);
|
||||||
logger.info('External user connected. Socket ID:', socket.id);
|
logger.info('External user connected. Socket ID:', socket.id);
|
||||||
// Handle disconnection
|
// Handle disconnection
|
||||||
@ -64,7 +63,7 @@ export class SocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addHost(socket) {
|
async addHost(socket) {
|
||||||
const socketHost = new SocketHost(socket, this, this.lockManager);
|
const socketHost = new SocketHost(socket, this);
|
||||||
this.socketHosts.set(socketHost.id, socketHost);
|
this.socketHosts.set(socketHost.id, socketHost);
|
||||||
logger.info('External host connected. Socket ID:', socket.id);
|
logger.info('External host connected. Socket ID:', socket.id);
|
||||||
// Handle disconnection
|
// Handle disconnection
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import log4js from 'log4js';
|
|||||||
import { loadConfig } from '../config.js';
|
import { loadConfig } from '../config.js';
|
||||||
import { createAuthMiddleware, KeycloakAuth } from '../auth/auth.js';
|
import { createAuthMiddleware, KeycloakAuth } from '../auth/auth.js';
|
||||||
import { generateHostOTP } from '../utils.js';
|
import { generateHostOTP } from '../utils.js';
|
||||||
import { LockManager } from '../lock/lockmanager.js';
|
import { ActivityManager } from '../activity/activitymanager.js';
|
||||||
import { UpdateManager } from '../updates/updatemanager.js';
|
import { UpdateManager } from '../updates/updatemanager.js';
|
||||||
import { ActionManager } from '../actions/actionmanager.js';
|
import { ActionManager } from '../actions/actionmanager.js';
|
||||||
import { EventManager } from '../events/eventmanager.js';
|
import { EventManager } from '../events/eventmanager.js';
|
||||||
@ -25,7 +25,8 @@ export class SocketUser {
|
|||||||
this.id = null;
|
this.id = null;
|
||||||
this.user = null;
|
this.user = null;
|
||||||
this.socketManager = socketManager;
|
this.socketManager = socketManager;
|
||||||
this.lockManager = new LockManager(this);
|
this.activityManager = new ActivityManager(this);
|
||||||
|
this.trackedActivities = new Set();
|
||||||
this.updateManager = new UpdateManager(this);
|
this.updateManager = new UpdateManager(this);
|
||||||
this.actionManager = new ActionManager(this);
|
this.actionManager = new ActionManager(this);
|
||||||
this.eventManager = new EventManager(this);
|
this.eventManager = new EventManager(this);
|
||||||
@ -41,9 +42,17 @@ export class SocketUser {
|
|||||||
setupSocketEventHandlers() {
|
setupSocketEventHandlers() {
|
||||||
this.socket.use(createAuthMiddleware(this));
|
this.socket.use(createAuthMiddleware(this));
|
||||||
this.socket.on('authenticate', this.handleAuthenticateEvent.bind(this));
|
this.socket.on('authenticate', this.handleAuthenticateEvent.bind(this));
|
||||||
this.socket.on('lock', this.handleLockEvent.bind(this));
|
this.socket.on('setActivity', this.handleSetActivityEvent.bind(this));
|
||||||
this.socket.on('unlock', this.handleUnlockEvent.bind(this));
|
this.socket.on('clearActivity', this.handleClearActivityEvent.bind(this));
|
||||||
this.socket.on('getLock', this.handleGetLockEvent.bind(this));
|
this.socket.on('getActivities', this.handleGetActivitiesEvent.bind(this));
|
||||||
|
this.socket.on(
|
||||||
|
'subscribeToObjectActivity',
|
||||||
|
this.handleSubscribeToObjectActivityEvent.bind(this)
|
||||||
|
);
|
||||||
|
this.socket.on(
|
||||||
|
'unsubscribeObjectActivity',
|
||||||
|
this.handleUnsubscribeObjectActivityEvent.bind(this)
|
||||||
|
);
|
||||||
this.socket.on(
|
this.socket.on(
|
||||||
'subscribeToObjectTypeUpdate',
|
'subscribeToObjectTypeUpdate',
|
||||||
this.handleSubscribeToObjectTypeUpdateEvent.bind(this)
|
this.handleSubscribeToObjectTypeUpdateEvent.bind(this)
|
||||||
@ -113,6 +122,7 @@ export class SocketUser {
|
|||||||
logger.info('User authenticated and valid.');
|
logger.info('User authenticated and valid.');
|
||||||
|
|
||||||
this.user = result.user;
|
this.user = result.user;
|
||||||
|
console.log('user', this.user);
|
||||||
this.id = this.user._id.toString();
|
this.id = this.user._id.toString();
|
||||||
this.authenticated = true;
|
this.authenticated = true;
|
||||||
await this.notificationManager.subscribe();
|
await this.notificationManager.subscribe();
|
||||||
@ -123,61 +133,115 @@ export class SocketUser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleLockEvent(data) {
|
trackActivity(type, id) {
|
||||||
// data: { _id: string, params?: object }
|
this.trackedActivities.add(`${type}|${id}`);
|
||||||
if (!data || !data._id) {
|
}
|
||||||
this.socket.emit('lock_result', {
|
|
||||||
|
untrackActivity(type, id) {
|
||||||
|
this.trackedActivities.delete(`${type}|${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSetActivityEvent(data, callback) {
|
||||||
|
const respond = result => {
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback(result);
|
||||||
|
} else {
|
||||||
|
this.socket.emit('setActivity_result', result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!data || !data._id || !data.type || !data.mode) {
|
||||||
|
respond({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Invalid lock event data'
|
error: 'Invalid setActivity event data'
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
data = { ...data, user: this.user._id.toString() };
|
data = { ...data, user: this.user._id.toString() };
|
||||||
try {
|
try {
|
||||||
await this.lockManager.lockObject(data);
|
const result = await this.activityManager.setActivity(data);
|
||||||
|
if (result?.success === false) {
|
||||||
|
respond(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.trackActivity(data.type, data._id);
|
||||||
|
respond(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Lock event error:', err);
|
logger.error('Set activity event error:', err);
|
||||||
this.socket.emit('lock_result', { success: false, error: err.message });
|
respond({
|
||||||
|
success: false,
|
||||||
|
error: err.message
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleUnlockEvent(data) {
|
async handleClearActivityEvent(data) {
|
||||||
// data: { _id: string }
|
if (!data || !data._id || !data.type) {
|
||||||
if (!data || !data._id) {
|
this.socket.emit('clearActivity_result', {
|
||||||
this.socket.emit('unlock_result', {
|
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Invalid unlock event data'
|
error: 'Invalid clearActivity event data'
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
data = { ...data, user: this.user._id.toString() };
|
data = { ...data, user: this.user._id.toString() };
|
||||||
try {
|
try {
|
||||||
await this.lockManager.unlockObject(data);
|
await this.activityManager.clearActivity(data);
|
||||||
|
this.untrackActivity(data.type, data._id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Unlock event error:', err);
|
logger.error('Clear activity event error:', err);
|
||||||
this.socket.emit('unlock_result', { success: false, error: err.message });
|
this.socket.emit('clearActivity_result', {
|
||||||
|
success: false,
|
||||||
|
error: err.message
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleGetLockEvent(data, callback) {
|
async handleGetActivitiesEvent(data, callback) {
|
||||||
// data: { _id: string }
|
if (!data || !data._id || !data.type) {
|
||||||
if (!data || !data._id) {
|
|
||||||
callback({
|
callback({
|
||||||
error: 'Invalid getLock event data'
|
error: 'Invalid getActivities event data'
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const lockEvent = await this.lockManager.getObjectLock(data);
|
const activities = await this.activityManager.getObjectActivities(data);
|
||||||
callback(lockEvent);
|
callback({ activities });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('GetLock event error:', err);
|
logger.error('GetActivities event error:', err);
|
||||||
callback({
|
callback({
|
||||||
error: err.message
|
error: err.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleSubscribeToObjectActivityEvent(data, callback) {
|
||||||
|
if (!data || !data._id || !data.objectType) {
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid subscribeToObjectActivity event data'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.activityManager.subscribeToObjectActivity(
|
||||||
|
data._id,
|
||||||
|
data.objectType
|
||||||
|
);
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleUnsubscribeObjectActivityEvent(data) {
|
||||||
|
await this.activityManager.removeObjectActivityListener(
|
||||||
|
data._id,
|
||||||
|
data.objectType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async handleSubscribeToObjectTypeUpdateEvent(data, callback) {
|
async handleSubscribeToObjectTypeUpdateEvent(data, callback) {
|
||||||
await this.updateManager.subscribeToObjectNew(data.objectType, data.filter);
|
await this.updateManager.subscribeToObjectNew(data.objectType, data.filter);
|
||||||
await this.updateManager.subscribeToObjectDelete(
|
await this.updateManager.subscribeToObjectDelete(
|
||||||
@ -305,6 +369,26 @@ export class SocketUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleDisconnect() {
|
async handleDisconnect() {
|
||||||
|
if (this.user?._id) {
|
||||||
|
const userId = this.user._id.toString();
|
||||||
|
for (const activityKey of this.trackedActivities) {
|
||||||
|
const separatorIndex = activityKey.indexOf('|');
|
||||||
|
const type = activityKey.slice(0, separatorIndex);
|
||||||
|
const id = activityKey.slice(separatorIndex + 1);
|
||||||
|
try {
|
||||||
|
await this.activityManager.clearActivity({
|
||||||
|
_id: id,
|
||||||
|
type,
|
||||||
|
user: userId
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error clearing activity on disconnect:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.trackedActivities.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.activityManager.removeAllListeners();
|
||||||
await this.actionManager.removeAllListeners();
|
await this.actionManager.removeAllListeners();
|
||||||
await this.updateManager.removeAllListeners();
|
await this.updateManager.removeAllListeners();
|
||||||
await this.eventManager.removeAllListeners();
|
await this.eventManager.removeAllListeners();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user