Skip to content

Commit 86463bf

Browse files
committed
WIP
1 parent c907fda commit 86463bf

15 files changed

Lines changed: 523 additions & 691 deletions

File tree

cmp/compiler/package.json

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,6 @@
8686
"import": "./build/plugin/webpack.mjs",
8787
"require": "./build/plugin/webpack.cjs"
8888
},
89-
"./rollup": {
90-
"types": "./build/plugin/rollup.d.ts",
91-
"import": "./build/plugin/rollup.mjs",
92-
"require": "./build/plugin/rollup.cjs"
93-
},
94-
"./esbuild": {
95-
"types": "./build/plugin/esbuild.d.ts",
96-
"import": "./build/plugin/esbuild.mjs",
97-
"require": "./build/plugin/esbuild.cjs"
98-
},
9989
"./turbopack-loader": {
10090
"types": "./build/plugin/turbopack-loader.d.ts",
10191
"import": "./build/plugin/turbopack-loader.mjs",
Lines changed: 104 additions & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
11
import fs from "fs/promises";
22
import path from "path";
33
import lockfile from "proper-lockfile";
4-
import type {
5-
MetadataConfig,
6-
MetadataSchema,
7-
TranslationEntry,
8-
} from "../types";
9-
import { getMetadataPath as getMetadataPathUtil } from "../utils/path-helpers";
10-
import { withTimeout, DEFAULT_TIMEOUTS } from "../utils/timeout";
4+
import type { MetadataSchema, TranslationEntry } from "../types";
5+
import { DEFAULT_TIMEOUTS, withTimeout } from "../utils/timeout";
116

12-
/**
13-
* Default metadata schema
14-
*/
157
export function createEmptyMetadata(): MetadataSchema {
168
return {
179
version: "0.1",
@@ -23,190 +15,122 @@ export function createEmptyMetadata(): MetadataSchema {
2315
};
2416
}
2517

26-
// TODO (AleksandrSl 24/11/2025): Probably remove and use path util as is
27-
/**
28-
* Get the path to the metadata file
29-
*/
30-
export function getMetadataPath(
31-
config: MetadataConfig,
32-
filename?: string,
33-
): string {
34-
return getMetadataPathUtil(config, filename);
18+
export function loadMetadata(path: string) {
19+
return new MetadataManager(path).loadMetadata();
3520
}
3621

37-
/**
38-
* Load metadata from disk
39-
* Creates empty metadata if file doesn't exist
40-
* Times out after 15 seconds to prevent indefinite hangs
41-
*/
42-
export async function loadMetadata(
43-
config: MetadataConfig,
44-
filename?: string,
45-
): Promise<MetadataSchema> {
46-
const metadataPath = getMetadataPath(config, filename);
47-
48-
try {
49-
const content = await withTimeout(
50-
fs.readFile(metadataPath, "utf-8"),
51-
DEFAULT_TIMEOUTS.METADATA,
52-
"Load metadata",
53-
);
54-
return JSON.parse(content) as MetadataSchema;
55-
} catch (error: any) {
56-
if (error.code === "ENOENT") {
57-
// File doesn't exist, create new metadata
58-
return createEmptyMetadata();
22+
export class MetadataManager {
23+
constructor(private readonly filePath: string) {}
24+
25+
/**
26+
* Load metadata from disk
27+
* Creates empty metadata if file doesn't exist
28+
* Times out after 15 seconds to prevent indefinite hangs
29+
*/
30+
async loadMetadata(): Promise<MetadataSchema> {
31+
try {
32+
const content = await withTimeout(
33+
fs.readFile(this.filePath, "utf-8"),
34+
DEFAULT_TIMEOUTS.METADATA,
35+
"Load metadata",
36+
);
37+
return JSON.parse(content) as MetadataSchema;
38+
} catch (error: any) {
39+
if (error.code === "ENOENT") {
40+
// File doesn't exist, create new metadata
41+
return createEmptyMetadata();
42+
}
43+
throw error;
5944
}
60-
throw error;
6145
}
62-
}
63-
64-
/**
65-
* Save metadata to disk
66-
* Times out after 15 seconds to prevent indefinite hangs
67-
*/
68-
export async function saveMetadata(
69-
config: MetadataConfig,
70-
metadata: MetadataSchema,
71-
filename?: string,
72-
): Promise<void> {
73-
const metadataPath = getMetadataPath(config, filename);
74-
await withTimeout(
75-
fs.mkdir(path.dirname(metadataPath), { recursive: true }),
76-
DEFAULT_TIMEOUTS.FILE_IO,
77-
"Create metadata directory",
78-
);
79-
80-
metadata.stats = {
81-
totalEntries: Object.keys(metadata.entries).length,
82-
lastUpdated: new Date().toISOString(),
83-
};
84-
85-
await withTimeout(
86-
fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8"),
87-
DEFAULT_TIMEOUTS.METADATA,
88-
"Save metadata",
89-
);
90-
}
9146

92-
/**
93-
* Thread-safe save operation that atomically updates metadata with new entries
94-
* Uses file locking to prevent concurrent write corruption
95-
*
96-
* @param config - Metadata configuration
97-
* @param entries - Translation entries to add/update
98-
* @param filename - Optional custom metadata filename
99-
* @returns The updated metadata schema
100-
*/
101-
export async function saveMetadataWithEntries(
102-
config: MetadataConfig,
103-
entries: TranslationEntry[],
104-
filename?: string,
105-
): Promise<MetadataSchema> {
106-
const metadataPath = getMetadataPath(config, filename);
107-
const lockDir = path.dirname(metadataPath);
47+
/**
48+
* Save metadata to disk
49+
* Times out after 15 seconds to prevent indefinite hangs
50+
*/
51+
private async saveMetadata(metadata: MetadataSchema): Promise<void> {
52+
await withTimeout(
53+
fs.mkdir(path.dirname(this.filePath), { recursive: true }),
54+
DEFAULT_TIMEOUTS.FILE_IO,
55+
"Create metadata directory",
56+
);
10857

109-
// Ensure directory exists before locking
110-
await fs.mkdir(lockDir, { recursive: true });
58+
metadata.stats = {
59+
totalEntries: Object.keys(metadata.entries).length,
60+
lastUpdated: new Date().toISOString(),
61+
};
11162

112-
// Create lock file if it doesn't exist (lockfile needs a file to lock)
113-
try {
114-
await fs.access(metadataPath);
115-
} catch {
116-
await fs.writeFile(
117-
metadataPath,
118-
JSON.stringify(createEmptyMetadata(), null, 2),
119-
"utf-8",
63+
await withTimeout(
64+
fs.writeFile(this.filePath, JSON.stringify(metadata, null, 2), "utf-8"),
65+
DEFAULT_TIMEOUTS.METADATA,
66+
"Save metadata",
12067
);
12168
}
12269

123-
// Acquire lock with retry options
124-
const release = await lockfile.lock(metadataPath, {
125-
retries: {
126-
retries: 10,
127-
minTimeout: 50,
128-
maxTimeout: 1000,
129-
},
130-
stale: 2000, // Consider lock stale after 5 seconds
131-
});
132-
133-
try {
134-
// Re-load metadata inside lock to get latest state
135-
const currentMetadata = await loadMetadata(config, filename);
136-
137-
// Apply updates
138-
const updatedMetadata = upsertEntries(currentMetadata, entries);
139-
140-
// Save
141-
await saveMetadata(config, updatedMetadata, filename);
70+
/**
71+
* Thread-safe save operation that atomically updates metadata with new entries
72+
* Uses file locking to prevent concurrent write corruption
73+
*
74+
* @param entries - Translation entries to add/update
75+
* @returns The updated metadata schema
76+
*/
77+
async saveMetadataWithEntries(
78+
entries: TranslationEntry[],
79+
): Promise<MetadataSchema> {
80+
const lockDir = path.dirname(this.filePath);
81+
82+
// Ensure directory exists before locking
83+
await fs.mkdir(lockDir, { recursive: true });
84+
85+
// Create lock file if it doesn't exist (lockfile needs a file to lock)
86+
try {
87+
await fs.access(this.filePath);
88+
} catch {
89+
// TODO (AleksandrSl 10/12/2025): Should I use another file as a lock?
90+
await fs.writeFile(
91+
this.filePath,
92+
JSON.stringify(createEmptyMetadata(), null, 2),
93+
"utf-8",
94+
);
95+
}
14296

143-
return updatedMetadata;
144-
} finally {
145-
// Always release lock
146-
await release();
97+
// Acquire lock with retry options
98+
const release = await lockfile.lock(this.filePath, {
99+
retries: {
100+
retries: 10,
101+
minTimeout: 50,
102+
maxTimeout: 1000,
103+
},
104+
stale: 2000, // Consider lock stale after 5 seconds
105+
});
106+
107+
try {
108+
// Re-load metadata inside lock to get latest state
109+
const currentMetadata = await this.loadMetadata();
110+
for (const entry of entries) {
111+
currentMetadata.entries[entry.hash] = entry;
112+
}
113+
await this.saveMetadata(currentMetadata);
114+
return currentMetadata;
115+
} finally {
116+
await release();
117+
}
147118
}
148-
}
149-
150-
/**
151-
* Add or update a translation entry in metadata
152-
*/
153-
export function upsertEntry(
154-
metadata: MetadataSchema,
155-
entry: TranslationEntry,
156-
): MetadataSchema {
157-
metadata.entries[entry.hash] = entry;
158119

159-
return metadata;
160-
}
161-
162-
/**
163-
* Batch add multiple entries
164-
*/
165-
export function upsertEntries(
166-
metadata: MetadataSchema,
167-
entries: TranslationEntry[],
168-
): MetadataSchema {
169-
let result = metadata;
170-
for (const entry of entries) {
171-
result = upsertEntry(result, entry);
120+
/**
121+
* Get an entry by hash
122+
*/
123+
getEntry(
124+
metadata: MetadataSchema,
125+
hash: string,
126+
): TranslationEntry | undefined {
127+
return metadata.entries[hash];
172128
}
173-
return result;
174-
}
175-
176-
/**
177-
* Get an entry by hash
178-
*/
179-
export function getEntry(
180-
metadata: MetadataSchema,
181-
hash: string,
182-
): TranslationEntry | undefined {
183-
return metadata.entries[hash];
184-
}
185-
186-
/**
187-
* Check if an entry exists
188-
*/
189-
export function hasEntry(metadata: MetadataSchema, hash: string): boolean {
190-
return hash in metadata.entries;
191-
}
192-
193-
/**
194-
* Remove entries by hash
195-
*/
196-
export function removeEntries(
197-
metadata: MetadataSchema,
198-
hashesToRemove: Set<string>,
199-
): MetadataSchema {
200-
const filtered: Record<string, TranslationEntry> = {};
201129

202-
for (const [hash, entry] of Object.entries(metadata.entries)) {
203-
if (!hashesToRemove.has(hash)) {
204-
filtered[hash] = entry;
205-
}
130+
/**
131+
* Check if an entry exists
132+
*/
133+
hasEntry(metadata: MetadataSchema, hash: string): boolean {
134+
return hash in metadata.entries;
206135
}
207-
208-
return {
209-
...metadata,
210-
entries: filtered,
211-
};
212136
}

0 commit comments

Comments
 (0)