@@ -11,6 +11,13 @@ const PROJECT_IDS_KEY = 'projectIds';
1111 */
1212let indexQueue : Promise < unknown > = Promise . resolve ( ) ;
1313
14+ /**
15+ * Per-project serialization queues. Keyed by project ID; each entry serializes all
16+ * read-modify-write operations on that project's storage record so concurrent update and delete
17+ * calls cannot interleave and create orphaned or stale records.
18+ */
19+ const projectQueues = new Map < string , Promise < unknown > > ( ) ;
20+
1421/**
1522 * Enqueues `fn` on the index serialization queue and returns a promise that resolves or rejects
1623 * with `fn`'s result. The queue always advances regardless of whether `fn` throws.
@@ -24,6 +31,26 @@ function enqueueIndexOp<T>(fn: () => Promise<T>): Promise<T> {
2431 return result ;
2532}
2633
34+ /**
35+ * Enqueues `fn` on the per-project serialization queue for `id` and returns a promise that resolves
36+ * or rejects with `fn`'s result. Cleans up the queue entry when the operation settles.
37+ *
38+ * @param id - The project UUID whose queue `fn` should join.
39+ * @param fn - The async function to serialize.
40+ * @returns A promise that resolves or rejects with the return value of `fn`.
41+ */
42+ function enqueueProjectOp < T > ( id : string , fn : ( ) => Promise < T > ) : Promise < T > {
43+ const previous = projectQueues . get ( id ) ?? Promise . resolve ( ) ;
44+ const result = previous . then ( fn ) ;
45+ let settled : Promise < void > ;
46+ const cleanup = ( ) => {
47+ if ( projectQueues . get ( id ) === settled ) projectQueues . delete ( id ) ;
48+ } ;
49+ settled = result . then ( cleanup , cleanup ) ;
50+ projectQueues . set ( id , settled ) ;
51+ return result ;
52+ }
53+
2754/**
2855 * Returns the storage key for a project by ID.
2956 *
@@ -201,24 +228,26 @@ export async function updateProjectMetadata(
201228 description : string | undefined ,
202229 analysisWritingSystem ?: string ,
203230) : Promise < InterlinearProject | undefined > {
204- const project = await getProject ( token , id ) ;
205- if ( ! project ) return undefined ;
206- const updated : InterlinearProject = { ...project } ;
207- if ( name === undefined ) {
208- delete updated . name ;
209- } else {
210- updated . name = name ;
211- }
212- if ( description === undefined ) {
213- delete updated . description ;
214- } else {
215- updated . description = description ;
216- }
217- if ( analysisWritingSystem ) {
218- updated . analysisWritingSystem = analysisWritingSystem ;
219- }
220- await papi . storage . writeUserData ( token , projectKey ( id ) , JSON . stringify ( updated ) ) ;
221- return updated ;
231+ return enqueueProjectOp ( id , async ( ) => {
232+ const project = await getProject ( token , id ) ;
233+ if ( ! project ) return undefined ;
234+ const updated : InterlinearProject = { ...project } ;
235+ if ( name === undefined ) {
236+ delete updated . name ;
237+ } else {
238+ updated . name = name ;
239+ }
240+ if ( description === undefined ) {
241+ delete updated . description ;
242+ } else {
243+ updated . description = description ;
244+ }
245+ if ( analysisWritingSystem ) {
246+ updated . analysisWritingSystem = analysisWritingSystem ;
247+ }
248+ await papi . storage . writeUserData ( token , projectKey ( id ) , JSON . stringify ( updated ) ) ;
249+ return updated ;
250+ } ) ;
222251}
223252
224253/**
@@ -231,14 +260,16 @@ export async function updateProjectMetadata(
231260 * @throws If `papi.storage.writeUserData` rejects when updating the index.
232261 */
233262export async function deleteProject ( token : ExecutionToken , id : string ) : Promise < void > {
234- try {
235- await papi . storage . deleteUserData ( token , projectKey ( id ) ) ;
236- } catch ( e ) {
237- if ( ! isNotFound ( e ) ) throw e ;
238- }
239- await enqueueIndexOp ( async ( ) => {
240- const ids = await readIds ( token ) ;
241- const updated = ids . filter ( ( i ) => i !== id ) ;
242- await papi . storage . writeUserData ( token , PROJECT_IDS_KEY , JSON . stringify ( updated ) ) ;
263+ await enqueueProjectOp ( id , async ( ) => {
264+ try {
265+ await papi . storage . deleteUserData ( token , projectKey ( id ) ) ;
266+ } catch ( e ) {
267+ if ( ! isNotFound ( e ) ) throw e ;
268+ }
269+ await enqueueIndexOp ( async ( ) => {
270+ const ids = await readIds ( token ) ;
271+ const updated = ids . filter ( ( i ) => i !== id ) ;
272+ await papi . storage . writeUserData ( token , PROJECT_IDS_KEY , JSON . stringify ( updated ) ) ;
273+ } ) ;
243274 } ) ;
244275}
0 commit comments