refactor: prepare server for machine sync refactoring
- Clean up machine API endpoints formatting - Update machine-alive to use ephemeral events instead of updates - Prepare types for separated metadata and daemonState - Fix activeAt field name consistency in machine responses 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6b1a3c3e82
commit
d03240061d
@ -13,7 +13,7 @@
|
||||
"migrate": "dotenv -e .env.dev -- prisma migrate dev",
|
||||
"generate": "prisma generate",
|
||||
"postinstall": "prisma generate",
|
||||
"db": "docker run -d -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=handy -v postgres-data:/var/lib/postgresql/data -p 5432:5432 postgres",
|
||||
"db": "docker run -d -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=handy -v $(pwd)/.pgdata:/var/lib/postgresql/data -p 5432:5432 postgres",
|
||||
"redis": "docker run -d -p 6379:6379 redis"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -940,6 +940,93 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
});
|
||||
});
|
||||
|
||||
// Machines
|
||||
|
||||
// POST /v1/machines - Create machine or return existing
|
||||
typed.post('/v1/machines', {
|
||||
preHandler: app.authenticate,
|
||||
schema: {
|
||||
body: z.object({
|
||||
id: z.string(),
|
||||
metadata: z.string() // Encrypted metadata
|
||||
})
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const userId = request.user.id;
|
||||
const { id, metadata } = request.body;
|
||||
|
||||
// Check if machine exists (like sessions do)
|
||||
const machine = await db.machine.findFirst({
|
||||
where: {
|
||||
accountId: userId,
|
||||
id: id
|
||||
}
|
||||
});
|
||||
|
||||
if (machine) {
|
||||
// Machine exists - just return it
|
||||
logger.info({ module: 'machines', machineId: id, userId }, 'Found existing machine');
|
||||
return reply.send({
|
||||
machine: {
|
||||
id: machine.id,
|
||||
metadata: machine.metadata,
|
||||
metadataVersion: machine.metadataVersion,
|
||||
active: machine.active,
|
||||
lastActiveAt: machine.lastActiveAt.getTime(),
|
||||
createdAt: machine.createdAt.getTime(),
|
||||
updatedAt: machine.updatedAt.getTime()
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Create new machine
|
||||
logger.info({ module: 'machines', machineId: id, userId }, 'Creating new machine');
|
||||
|
||||
const newMachine = await db.machine.create({
|
||||
data: {
|
||||
id,
|
||||
accountId: userId,
|
||||
metadata,
|
||||
metadataVersion: 1
|
||||
// active defaults to true in schema
|
||||
// lastActiveAt defaults to now() in schema
|
||||
}
|
||||
});
|
||||
|
||||
// Emit update for new machine
|
||||
const updSeq = await allocateUserSeq(userId);
|
||||
emitUpdateToInterestedClients({
|
||||
event: 'update',
|
||||
userId,
|
||||
payload: {
|
||||
id: randomKeyNaked(),
|
||||
seq: updSeq,
|
||||
body: {
|
||||
t: 'update-machine',
|
||||
id: newMachine.id,
|
||||
metadata: {
|
||||
version: 1,
|
||||
value: metadata
|
||||
}
|
||||
},
|
||||
createdAt: Date.now()
|
||||
}
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
machine: {
|
||||
id: newMachine.id,
|
||||
metadata: newMachine.metadata,
|
||||
metadataVersion: 1,
|
||||
active: newMachine.active,
|
||||
lastActiveAt: newMachine.lastActiveAt.getTime(),
|
||||
createdAt: newMachine.createdAt.getTime(),
|
||||
updatedAt: newMachine.updatedAt.getTime()
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Machines API
|
||||
typed.get('/v1/machines', {
|
||||
preHandler: app.authenticate,
|
||||
@ -957,83 +1044,47 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
metadataVersion: m.metadataVersion,
|
||||
seq: m.seq,
|
||||
active: m.active,
|
||||
lastActiveAt: m.lastActiveAt.getTime(),
|
||||
activeAt: m.lastActiveAt.getTime(),
|
||||
createdAt: m.createdAt.getTime(),
|
||||
updatedAt: m.updatedAt.getTime()
|
||||
}));
|
||||
});
|
||||
|
||||
// POST /v1/machines - Create or update machine
|
||||
typed.post('/v1/machines', {
|
||||
// GET /v1/machines/:id - Get single machine by ID
|
||||
typed.get('/v1/machines/:id', {
|
||||
preHandler: app.authenticate,
|
||||
schema: {
|
||||
body: z.object({
|
||||
id: z.string(),
|
||||
metadata: z.string() // Encrypted metadata
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const userId = request.user.id;
|
||||
const { id, metadata } = request.body;
|
||||
const { id } = request.params;
|
||||
|
||||
logger.info({ module: 'machines', machineId: id, userId, hasMetadata: !!metadata }, 'Creating/updating machine');
|
||||
|
||||
try {
|
||||
const machine = await db.machine.upsert({
|
||||
const machine = await db.machine.findFirst({
|
||||
where: {
|
||||
accountId_id: {
|
||||
accountId: userId,
|
||||
id
|
||||
}
|
||||
},
|
||||
create: {
|
||||
id,
|
||||
accountId: userId,
|
||||
metadata,
|
||||
metadataVersion: 1
|
||||
},
|
||||
update: {
|
||||
metadata,
|
||||
metadataVersion: { increment: 1 },
|
||||
active: true,
|
||||
lastActiveAt: new Date()
|
||||
id: id
|
||||
}
|
||||
});
|
||||
|
||||
// Emit update to all user connections
|
||||
const updSeq = await allocateUserSeq(userId);
|
||||
emitUpdateToInterestedClients({
|
||||
event: 'update',
|
||||
userId,
|
||||
payload: {
|
||||
id: randomKeyNaked(),
|
||||
seq: updSeq,
|
||||
body: {
|
||||
t: 'update-machine',
|
||||
if (!machine) {
|
||||
return reply.code(404).send({ error: 'Machine not found' });
|
||||
}
|
||||
|
||||
return {
|
||||
machine: {
|
||||
id: machine.id,
|
||||
metadata: {
|
||||
version: machine.metadataVersion,
|
||||
value: metadata
|
||||
}
|
||||
},
|
||||
createdAt: Date.now()
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
module: 'machines',
|
||||
machineId: id,
|
||||
userId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
errorStack: error instanceof Error ? error.stack : undefined
|
||||
}, 'Failed to create/update machine');
|
||||
return reply.code(500).send({
|
||||
error: 'Failed to create/update machine',
|
||||
details: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
metadata: machine.metadata,
|
||||
metadataVersion: machine.metadataVersion,
|
||||
seq: machine.seq,
|
||||
active: machine.active,
|
||||
activeAt: machine.lastActiveAt.getTime(),
|
||||
createdAt: machine.createdAt.getTime(),
|
||||
updatedAt: machine.updatedAt.getTime()
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Combined logging endpoint (only when explicitly enabled)
|
||||
@ -1336,49 +1387,45 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
return;
|
||||
}
|
||||
|
||||
const machineId = data.machineId;
|
||||
|
||||
// Update machine lastActiveAt in database
|
||||
const machine = await db.machine.update({
|
||||
// Resolve machine
|
||||
const machine = await db.machine.findUnique({
|
||||
where: {
|
||||
accountId_id: {
|
||||
accountId: userId,
|
||||
id: machineId
|
||||
id: data.machineId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!machine) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update machine lastActiveAt in database
|
||||
const updatedMachine = await db.machine.update({
|
||||
where: {
|
||||
accountId_id: {
|
||||
accountId: userId,
|
||||
id: data.machineId
|
||||
}
|
||||
},
|
||||
data: {
|
||||
lastActiveAt: new Date(t),
|
||||
active: true
|
||||
}
|
||||
}).catch(() => {
|
||||
// Machine might not exist yet, that's ok
|
||||
return null;
|
||||
});
|
||||
})
|
||||
|
||||
// If machine was updated, emit update to all user connections
|
||||
if (machine) {
|
||||
const updSeq = await allocateUserSeq(userId);
|
||||
emitUpdateToInterestedClients({
|
||||
event: 'update',
|
||||
event: 'ephemeral',
|
||||
userId,
|
||||
payload: {
|
||||
id: randomKeyNaked(12),
|
||||
seq: updSeq,
|
||||
body: {
|
||||
t: 'update-machine',
|
||||
id: machine.id,
|
||||
metadata: machine.metadata ? {
|
||||
version: machine.metadataVersion,
|
||||
value: machine.metadata
|
||||
} : undefined,
|
||||
type: 'machine-activity',
|
||||
id: updatedMachine.id,
|
||||
active: true,
|
||||
lastActiveAt: t
|
||||
lastActiveAt: t,
|
||||
},
|
||||
createdAt: Date.now()
|
||||
},
|
||||
recipientFilter: { type: 'all-user-authenticated-connections' }
|
||||
recipientFilter: { type: 'user-scoped-only' }
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
log({ module: 'websocket', level: 'error' }, `Error in machine-alive: ${error}`);
|
||||
}
|
||||
@ -1660,50 +1707,108 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
||||
}
|
||||
});
|
||||
|
||||
// Update machine through socket
|
||||
socket.on('update-machine', async (data: { metadata: string }) => {
|
||||
if (connection.connectionType !== 'machine-scoped') {
|
||||
return; // Only machines can update their own metadata
|
||||
// Machine metadata update with optimistic concurrency control
|
||||
socket.on('machine-update-metadata', async (data: any, callback: (response: any) => void) => {
|
||||
try {
|
||||
const { machineId, metadata, expectedVersion } = data;
|
||||
|
||||
// Validate input
|
||||
if (!machineId || typeof metadata !== 'string' || typeof expectedVersion !== 'number') {
|
||||
if (callback) {
|
||||
callback({ result: 'error', message: 'Invalid parameters' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const machineId = connection.machineId;
|
||||
|
||||
try {
|
||||
const machine = await db.machine.update({
|
||||
// Resolve machine
|
||||
const machine = await db.machine.findFirst({
|
||||
where: {
|
||||
accountId_id: {
|
||||
accountId: userId,
|
||||
id: machineId
|
||||
}
|
||||
});
|
||||
if (!machine) {
|
||||
if (callback) {
|
||||
callback({ result: 'error', message: 'Machine not found' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check version
|
||||
if (machine.metadataVersion !== expectedVersion) {
|
||||
callback({
|
||||
result: 'version-mismatch',
|
||||
version: machine.metadataVersion,
|
||||
metadata: machine.metadata
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Update metadata with atomic version check
|
||||
const { count } = await db.machine.updateMany({
|
||||
where: {
|
||||
accountId: userId,
|
||||
id: machineId,
|
||||
metadataVersion: expectedVersion // Atomic CAS
|
||||
},
|
||||
data: {
|
||||
metadata: data.metadata,
|
||||
metadataVersion: { increment: 1 }
|
||||
metadata: metadata,
|
||||
metadataVersion: expectedVersion + 1
|
||||
// NOT updating active or lastActiveAt here
|
||||
}
|
||||
});
|
||||
|
||||
// Emit to other connections
|
||||
if (count === 0) {
|
||||
// Re-fetch current version
|
||||
const current = await db.machine.findFirst({
|
||||
where: {
|
||||
accountId: userId,
|
||||
id: machineId
|
||||
}
|
||||
});
|
||||
callback({
|
||||
result: 'version-mismatch',
|
||||
version: current?.metadataVersion || 0,
|
||||
metadata: current?.metadata
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate update
|
||||
const updSeq = await allocateUserSeq(userId);
|
||||
const updContent: PrismaJson.UpdateBody = {
|
||||
t: 'update-machine',
|
||||
id: machineId,
|
||||
metadata: {
|
||||
value: metadata,
|
||||
version: expectedVersion + 1
|
||||
}
|
||||
};
|
||||
|
||||
// Emit to all connections
|
||||
emitUpdateToInterestedClients({
|
||||
event: 'update',
|
||||
userId,
|
||||
payload: {
|
||||
id: randomKeyNaked(),
|
||||
id: randomKeyNaked(12),
|
||||
seq: updSeq,
|
||||
body: {
|
||||
t: 'update-machine',
|
||||
id: machineId,
|
||||
metadata: {
|
||||
version: machine.metadataVersion,
|
||||
value: data.metadata
|
||||
}
|
||||
},
|
||||
body: updContent,
|
||||
createdAt: Date.now()
|
||||
},
|
||||
skipSenderConnection: connection
|
||||
recipientFilter: { type: 'all-user-authenticated-connections' }
|
||||
});
|
||||
|
||||
// Send success response with new version
|
||||
callback({
|
||||
result: 'success',
|
||||
version: expectedVersion + 1,
|
||||
metadata: metadata
|
||||
});
|
||||
} catch (error) {
|
||||
log({ module: 'websocket', level: 'error' }, `Error updating machine metadata: ${error}`);
|
||||
log({ module: 'websocket', level: 'error' }, `Error in machine-update-metadata: ${error}`);
|
||||
if (callback) {
|
||||
callback({ result: 'error', message: 'Internal error' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -60,6 +60,14 @@ declare global {
|
||||
value: string | null;
|
||||
version: number;
|
||||
} | null | undefined;
|
||||
} | {
|
||||
t: 'update-machine';
|
||||
id: string;
|
||||
metadata?: {
|
||||
value: string;
|
||||
version: number;
|
||||
};
|
||||
activeAt?: number;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user