Skip to content

Commit 681cbdb

Browse files
committed
Refactor: Implement MetadataManager for Sync Service #7643
1 parent 7707226 commit 681cbdb

3 files changed

Lines changed: 106 additions & 55 deletions

File tree

ai/mcp/server/github-workflow/services/SyncService.mjs

Lines changed: 12 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,18 @@
1-
import aiConfig from '../config.mjs';
2-
import Base from '../../../../../src/core/Base.mjs';
3-
import fs from 'fs/promises';
4-
import logger from '../logger.mjs';
5-
import path from 'path';
6-
import IssueSyncer from './sync/IssueSyncer.mjs';
7-
import ReleaseSyncer from './sync/ReleaseSyncer.mjs';
8-
9-
const issueSyncConfig = aiConfig.issueSync;
1+
import aiConfig from '../config.mjs';
2+
import Base from '../../../../../src/core/Base.mjs';
3+
import logger from '../logger.mjs';
4+
import IssueSyncer from './sync/IssueSyncer.mjs';
5+
import MetadataManager from './sync/MetadataManager.mjs';
6+
import ReleaseSyncer from './sync/ReleaseSyncer.mjs';
107

118
/**
129
* Orchestrates the bi-directional synchronization of GitHub issues and releases with local Markdown files.
1310
*
1411
* This service is the core engine for the GitHub sync workflow. Its primary responsibilities include:
15-
* - **State Management:** It maintains a persistent state via a `.sync-metadata.json` file to track
16-
* the last sync time and the status of each item, enabling efficient delta-based updates.
1712
* - **Orchestration:** It calls specialized syncer modules (`IssueSyncer`, `ReleaseSyncer`) in the
1813
* correct order to ensure data integrity and minimize conflicts (e.g., push-then-pull).
19-
* - **Metadata Management:** It loads the metadata at the start of a sync and saves the updated
20-
* metadata, including new cache objects from the syncers, at the end.
14+
* - **Metadata Management:** It uses the `MetadataManager` to load metadata at the start of a sync
15+
* and save the updated metadata at the end.
2116
*
2217
* The main entry point is the `runFullSync` method, which executes the entire orchestration sequence.
2318
* @class Neo.ai.mcp.server.github-workflow.SyncService
@@ -43,20 +38,20 @@ class SyncService extends Base {
4338
*
4439
* This method orchestrates the entire bi-directional sync workflow in a specific order
4540
* to ensure data integrity and minimize conflicts:
46-
* 1. Loads the persistent metadata from the last sync.
41+
* 1. Loads the persistent metadata from the last sync via `MetadataManager`.
4742
* 2. Fetches and caches GitHub release data via `ReleaseSyncer`.
4843
* 3. **Pushes** any local issue changes to GitHub via `IssueSyncer`.
4944
* 4. **Pulls** the latest issue changes from GitHub via `IssueSyncer`.
5045
* 5. Syncs release notes into local Markdown files via `ReleaseSyncer`.
51-
* 6. Saves the updated metadata (including new release and issue data) to disk.
46+
* 6. Saves the updated, pruned metadata to disk via `MetadataManager`.
5247
*
5348
* @returns {Promise<object>} A comprehensive object containing detailed statistics and timing
5449
* information about all operations performed during the sync.
5550
*/
5651
async runFullSync() {
5752
const startTime = new Date();
5853

59-
const metadata = await this.#loadMetadata();
54+
const metadata = await MetadataManager.load();
6055

6156
// Fetch releases first, as they are needed for issue archiving
6257
await ReleaseSyncer.fetchAndCacheReleases(metadata);
@@ -89,7 +84,7 @@ class SyncService extends Base {
8984
newMetadata.releasesLastFetched = new Date().toISOString();
9085

9186
// 6. Save metadata
92-
await this.#saveMetadata(newMetadata);
87+
await MetadataManager.save(newMetadata);
9388

9489
const endTime = new Date();
9590
const durationMs = endTime - startTime;
@@ -121,41 +116,6 @@ class SyncService extends Base {
121116
timing
122117
};
123118
}
124-
125-
/**
126-
* Loads the synchronization metadata file from disk. If the file doesn't exist,
127-
* it returns a default empty metadata object.
128-
* @returns {Promise<object>} The parsed metadata object.
129-
* @throws {Error} If reading the file fails for reasons other than not existing.
130-
* @private
131-
*/
132-
async #loadMetadata() {
133-
try {
134-
const data = await fs.readFile(issueSyncConfig.metadataFile, 'utf-8');
135-
return JSON.parse(data);
136-
} catch (error) {
137-
if (error.code === 'ENOENT') {
138-
return {
139-
lastSync: null,
140-
issues : {}
141-
};
142-
}
143-
throw error;
144-
}
145-
}
146-
147-
/**
148-
* Saves the provided metadata object to the configured metadata file on disk,
149-
* ensuring the directory exists.
150-
* @param {object} metadata - The metadata object to serialize and save.
151-
* @returns {Promise<void>}
152-
* @private
153-
*/
154-
async #saveMetadata(metadata) {
155-
const dir = path.dirname(issueSyncConfig.metadataFile);
156-
await fs.mkdir(dir, { recursive: true });
157-
await fs.writeFile(issueSyncConfig.metadataFile, JSON.stringify(metadata, null, 2), 'utf-8');
158-
}
159119
}
160120

161121
export default Neo.setupClass(SyncService);
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import aiConfig from '../../config.mjs';
2+
import Base from '../../../../../../src/core/Base.mjs';
3+
import fs from 'fs/promises';
4+
import path from 'path';
5+
6+
const issueSyncConfig = aiConfig.issueSync;
7+
8+
/**
9+
* Manages loading, saving, and pruning of the .sync-metadata.json file.
10+
* @class Neo.ai.mcp.server.github-workflow.services.sync.MetadataManager
11+
* @extends Neo.core.Base
12+
* @singleton
13+
*/
14+
class MetadataManager extends Base {
15+
static config = {
16+
/**
17+
* @member {String} className='Neo.ai.mcp.server.github-workflow.services.sync.MetadataManager'
18+
* @protected
19+
*/
20+
className: 'Neo.ai.mcp.server.github-workflow.services.sync.MetadataManager',
21+
/**
22+
* @member {Boolean} singleton=true
23+
* @protected
24+
*/
25+
singleton: true
26+
}
27+
28+
/**
29+
* Loads the synchronization metadata file from disk.
30+
* If the file doesn't exist, it returns a default empty metadata object.
31+
* @returns {Promise<object>} The parsed metadata object.
32+
* @throws {Error} If reading the file fails for reasons other than not existing.
33+
*/
34+
async load() {
35+
try {
36+
const data = await fs.readFile(issueSyncConfig.metadataFile, 'utf-8');
37+
return JSON.parse(data);
38+
} catch (error) {
39+
if (error.code === 'ENOENT') {
40+
return {
41+
lastSync: null,
42+
issues: {},
43+
releases: {}
44+
};
45+
} else {
46+
throw error;
47+
}
48+
}
49+
}
50+
51+
/**
52+
* Saves the provided metadata object to the configured metadata file on disk,
53+
* ensuring the directory exists. This method also prunes the data to save only
54+
* essential fields for change detection.
55+
* @param {object} metadata - The metadata object to serialize and save.
56+
* @returns {Promise<void>}
57+
*/
58+
async save(metadata) {
59+
const prunedMetadata = {
60+
lastSync : metadata.lastSync,
61+
releasesLastFetched: metadata.releasesLastFetched,
62+
pushFailures : metadata.pushFailures || [],
63+
issues : {},
64+
releases : {}
65+
};
66+
67+
// Prune issues
68+
for (const [key, value] of Object.entries(metadata.issues)) {
69+
prunedMetadata.issues[key] = {
70+
state : value.state,
71+
path : value.path,
72+
updated : value.updated,
73+
contentHash: value.contentHash
74+
};
75+
}
76+
77+
// Prune releases
78+
for (const [key, value] of Object.entries(metadata.releases)) {
79+
prunedMetadata.releases[key] = {
80+
publishedAt: value.publishedAt,
81+
contentHash: value.contentHash
82+
};
83+
}
84+
85+
const dir = path.dirname(issueSyncConfig.metadataFile);
86+
await fs.mkdir(dir, { recursive: true });
87+
await fs.writeFile(issueSyncConfig.metadataFile, JSON.stringify(prunedMetadata, null, 2), 'utf-8');
88+
}
89+
}
90+
91+
export default Neo.setupClass(MetadataManager);

ai/mcp/server/github-workflow/services/sync/ReleaseSyncer.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@ class ReleaseSyncer extends Base {
9595
// Phase 2: Full fetch with early exit
9696
logger.info('Fetching releases from GitHub via GraphQL...');
9797

98-
let allReleases = [];
99-
let hasNextPage = true;
100-
let cursor = null;
98+
let allReleases = [];
99+
let hasNextPage = true;
100+
let cursor = null;
101101
const maxReleases = issueSyncConfig.maxReleases;
102102
const startDate = new Date(issueSyncConfig.syncStartDate);
103103

0 commit comments

Comments
 (0)