fix(member-work-sync): lock pending report writes
This commit is contained in:
parent
cd70fc09cc
commit
bf1f3b6b02
2 changed files with 63 additions and 52 deletions
|
|
@ -2,6 +2,7 @@ const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const runtimeHelpers = require('./runtimeHelpers.js');
|
const runtimeHelpers = require('./runtimeHelpers.js');
|
||||||
|
const { withFileLockSync } = require('./fileLock.js');
|
||||||
|
|
||||||
const DEFAULT_WAIT_TIMEOUT_MS = 10000;
|
const DEFAULT_WAIT_TIMEOUT_MS = 10000;
|
||||||
const MIN_WAIT_TIMEOUT_MS = 1000;
|
const MIN_WAIT_TIMEOUT_MS = 1000;
|
||||||
|
|
@ -173,25 +174,28 @@ function writePendingReportFile(filePath, data) {
|
||||||
|
|
||||||
function appendPendingReportIntent(context, body, reason) {
|
function appendPendingReportIntent(context, body, reason) {
|
||||||
const filePath = path.join(context.paths.teamDir, '.member-work-sync', 'pending-reports.json');
|
const filePath = path.join(context.paths.teamDir, '.member-work-sync', 'pending-reports.json');
|
||||||
const data = readPendingReportFile(filePath);
|
const { id } = withFileLockSync(filePath, () => {
|
||||||
const request = {
|
const data = readPendingReportFile(filePath);
|
||||||
...body,
|
const request = {
|
||||||
source: 'mcp',
|
...body,
|
||||||
};
|
source: 'mcp',
|
||||||
const id = buildPendingIntentId(request);
|
|
||||||
const current = data.intents[id];
|
|
||||||
if (!current || current.status === 'pending') {
|
|
||||||
data.intents[id] = {
|
|
||||||
id,
|
|
||||||
teamName: body.teamName,
|
|
||||||
memberName: body.memberName,
|
|
||||||
request,
|
|
||||||
reason,
|
|
||||||
status: 'pending',
|
|
||||||
recordedAt: current && current.recordedAt ? current.recordedAt : new Date().toISOString(),
|
|
||||||
};
|
};
|
||||||
writePendingReportFile(filePath, data);
|
const intentId = buildPendingIntentId(request);
|
||||||
}
|
const current = data.intents[intentId];
|
||||||
|
if (!current || current.status === 'pending') {
|
||||||
|
data.intents[intentId] = {
|
||||||
|
id: intentId,
|
||||||
|
teamName: body.teamName,
|
||||||
|
memberName: body.memberName,
|
||||||
|
request,
|
||||||
|
reason,
|
||||||
|
status: 'pending',
|
||||||
|
recordedAt: current && current.recordedAt ? current.recordedAt : new Date().toISOString(),
|
||||||
|
};
|
||||||
|
writePendingReportFile(filePath, data);
|
||||||
|
}
|
||||||
|
return { id: intentId };
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
accepted: false,
|
accepted: false,
|
||||||
pendingValidation: true,
|
pendingValidation: true,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { atomicWriteAsync } from '@main/utils/atomicWrite';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { mkdir, readFile, rename } from 'fs/promises';
|
import { mkdir, readFile, rename } from 'fs/promises';
|
||||||
|
|
||||||
|
import { withFileLock } from '@main/services/team/fileLock';
|
||||||
import type {
|
import type {
|
||||||
MemberWorkSyncReportIntent,
|
MemberWorkSyncReportIntent,
|
||||||
MemberWorkSyncReportRequest,
|
MemberWorkSyncReportRequest,
|
||||||
|
|
@ -106,34 +107,38 @@ export class JsonMemberWorkSyncStore
|
||||||
|
|
||||||
async write(status: MemberWorkSyncStatus): Promise<void> {
|
async write(status: MemberWorkSyncStatus): Promise<void> {
|
||||||
await this.enqueue(status.teamName, async () => {
|
await this.enqueue(status.teamName, async () => {
|
||||||
const existing = await this.readFile(status.teamName);
|
await withFileLock(this.paths.getStatusPath(status.teamName), async () => {
|
||||||
existing.members[normalizeMemberKey(status.memberName)] = status;
|
const existing = await this.readFile(status.teamName);
|
||||||
await mkdir(this.paths.getTeamDir(status.teamName), { recursive: true });
|
existing.members[normalizeMemberKey(status.memberName)] = status;
|
||||||
await atomicWriteAsync(
|
await mkdir(this.paths.getTeamDir(status.teamName), { recursive: true });
|
||||||
this.paths.getStatusPath(status.teamName),
|
await atomicWriteAsync(
|
||||||
JSON.stringify(existing, null, 2)
|
this.paths.getStatusPath(status.teamName),
|
||||||
);
|
JSON.stringify(existing, null, 2)
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async appendPendingReport(request: MemberWorkSyncReportRequest, reason: string): Promise<void> {
|
async appendPendingReport(request: MemberWorkSyncReportRequest, reason: string): Promise<void> {
|
||||||
const id = buildPendingReportIntentId(request);
|
const id = buildPendingReportIntentId(request);
|
||||||
await this.enqueue(request.teamName, async () => {
|
await this.enqueue(request.teamName, async () => {
|
||||||
const existing = await this.readPendingFile(request.teamName);
|
await withFileLock(this.paths.getPendingReportsPath(request.teamName), async () => {
|
||||||
const current = existing.intents[id];
|
const existing = await this.readPendingFile(request.teamName);
|
||||||
if (current && current.status !== 'pending') {
|
const current = existing.intents[id];
|
||||||
return;
|
if (current && current.status !== 'pending') {
|
||||||
}
|
return;
|
||||||
existing.intents[id] = {
|
}
|
||||||
id,
|
existing.intents[id] = {
|
||||||
teamName: request.teamName,
|
id,
|
||||||
memberName: request.memberName,
|
teamName: request.teamName,
|
||||||
request,
|
memberName: request.memberName,
|
||||||
reason: current?.reason ?? reason,
|
request,
|
||||||
status: 'pending',
|
reason: current?.reason ?? reason,
|
||||||
recordedAt: current?.recordedAt ?? new Date().toISOString(),
|
status: 'pending',
|
||||||
};
|
recordedAt: current?.recordedAt ?? new Date().toISOString(),
|
||||||
await this.writePendingFile(request.teamName, existing);
|
};
|
||||||
|
await this.writePendingFile(request.teamName, existing);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,18 +159,20 @@ export class JsonMemberWorkSyncStore
|
||||||
}
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.enqueue(teamName, async () => {
|
await this.enqueue(teamName, async () => {
|
||||||
const existing = await this.readPendingFile(teamName);
|
await withFileLock(this.paths.getPendingReportsPath(teamName), async () => {
|
||||||
const current = existing.intents[id];
|
const existing = await this.readPendingFile(teamName);
|
||||||
if (!current || current.status !== 'pending') {
|
const current = existing.intents[id];
|
||||||
return;
|
if (!current || current.status !== 'pending') {
|
||||||
}
|
return;
|
||||||
existing.intents[id] = {
|
}
|
||||||
...current,
|
existing.intents[id] = {
|
||||||
status: result.status,
|
...current,
|
||||||
resultCode: result.resultCode,
|
status: result.status,
|
||||||
processedAt: result.processedAt,
|
resultCode: result.resultCode,
|
||||||
};
|
processedAt: result.processedAt,
|
||||||
await this.writePendingFile(teamName, existing);
|
};
|
||||||
|
await this.writePendingFile(teamName, existing);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue