services/api/src/modules/media/media-claims.service.js
Overview
This module coordinates atomic media claim and deletion handling for files stored in ImageKit and attached to Listing records. It exists to keep file state consistent when a listing adds media, removes media, or loses media references during concurrent writes. The service works with Listing, MediaFileClaim, and deleteAsset from media.service.js.
It is not an HTTP route by itself. Instead, listings.routes.js imports its helpers to protect media writes and deletions at the listing layer. The code comments and the integration tests show that the module is used to handle race conditions, retries, and shared-file preservation.
[!IMPORTANT] The service treats file deletion as a two-phase operation. A file only reaches
ImageKitafter possession is confirmed with aholder, and only if no otherListingstill references it.
How it works
flowchart TD
AttachFlow[claimListingMediaFiles]
DeleteFlow[deleteListingFilesSafely]
Claims[MediaFileClaim]
Listings[Listing]
ImageKit[deleteAsset]
Refs[referencedElsewhere]
Acquire[acquireForDeletion]
Transition[transition]
Release[releaseListingMediaClaims]
AttachFlow --> Claims
AttachFlow --> Refs
Refs --> Listings
AttachFlow --> Release
DeleteFlow --> Acquire
Acquire --> Claims
DeleteFlow --> Refs
Refs --> Listings
DeleteFlow --> Transition
Transition --> Claims
DeleteFlow --> ImageKit
The attachment path creates MediaFileClaim records before a listing write finishes. If the write cannot be trusted, the caller can release the created claims. The deletion path first acquires possession for every file, checks whether any other listing still references those file IDs, and only then calls deleteAsset for files that are exclusive to the current listing.
Key components
{
"title": "listingImageKitFileIds",
"content": "[badge:public] Extracts unique ImageKit file IDs from a Listing. It reads media items with provider imagekit and sourceDocuments entries with fileId.",
"type": "function",
"filePath": "services/api/src/modules/media/media-claims.service.js",
"badges": [
"public"
]
}
{
"title": "releaseListingMediaClaims",
"content": "[badge:public] Removes active MediaFileClaim records for the given listing and file IDs. It never touches deleting or deleted states.",
"type": "function",
"filePath": "services/api/src/modules/media/media-claims.service.js",
"badges": [
"public"
]
}
{
"title": "claimListingMediaFiles",
"content": "[badge:public] Creates active claims for new file IDs before a Listing write. It returns the claims created by this call and throws MEDIA_IN_USE when a file is already used elsewhere.",
"type": "function",
"filePath": "services/api/src/modules/media/media-claims.service.js",
"badges": [
"public"
]
}
{
"title": "deleteListingFilesSafely",
"content": "[badge:public] Deletes exclusive ImageKit files with possession checks and rollback support. It returns deleted and preserved file IDs, and optionally inProgress.",
"type": "function",
"filePath": "services/api/src/modules/media/media-claims.service.js",
"badges": [
"public"
]
}
Data & fields
[
{
"name": "listingId",
"type": "string | ObjectId",
"required": true,
"description": "Listing target used by claim and deletion operations."
},
{
"name": "fileIds",
"type": "string[]",
"required": true,
"description": "ImageKit file identifiers processed by the service. Empty values are removed, and duplicates are deduplicated."
},
{
"name": "hooks",
"type": "object | null",
"description": "Test-only pause hooks. The service calls delete:afterSharedCheck when present."
},
{
"name": "report",
"type": "boolean",
"description": "When true, deleteListingFilesSafely returns inProgress together with deleted and preserved."
},
{
"name": "holder",
"type": "ObjectId",
"description": "Operation token used to confirm deletion possession."
}
]
[
{
"name": "MediaFileClaim",
"type": "object",
"required": true,
"description": "MongoDB claim document for one ImageKit file.",
"children": [
{
"name": "_id",
"type": "string",
"required": true,
"description": "Equals the fileId."
},
{
"name": "listingId",
"type": "ObjectId",
"required": true,
"description": "Listing that owns the claim."
},
{
"name": "state",
"type": "string",
"required": true,
"description": "Lifecycle state. The code uses active, deleting, erasing, and deleted."
},
{
"name": "holder",
"type": "ObjectId",
"description": "Per-operation possession token used during deletion."
}
]
}
]
media[].provider— onlyimagekitmedia are counted bylistingImageKitFileIds.media[].fileId— used for claim and cross-listing reference checks.sourceDocuments[].fileId— included in claim, reference, and delete checks.
active -> deleting -> erasing -> deleted
This state chain is the core claim lifecycle described in the comments and enforced by the update logic. The module also uses a temporary holder value to keep one deletion operation from taking another operation’s claim.
Behaviors
mediaInUseError— Builds a409error with codeMEDIA_IN_USEand the message about a file already being used in another publication.step— Runs a named hook whenhooks[name]is a function. It exists only for tests.unique— Removes falsy values, stringifies file IDs, and deduplicates them.toObjectId— Converts a value intomongoose.Types.ObjectId.listingImageKitFileIds[badge:public] — Returns unique ImageKit file IDs frommediaandsourceDocuments.referencedElsewhere— Returns aSetwith file IDs still referenced by otherListingdocuments.releaseListingMediaClaims[badge:public] — Deletes onlyactiveclaims for the given listing and file IDs.claimListingMediaFiles[badge:public] — Createsactiveclaims for new file IDs, checks cross-listing references, and rolls back created claims onMEDIA_IN_USE.acquireForDeletion— Tries to move or create a claim intodeletingwith aholder. It returnsacquired,already_deleted,in_progress, orheld_elsewhere.transition— Updates a claim only when_id,listingId,state, andholderall match. It returns a boolean.deleteListingFilesSafely[badge:public] — Deletes files that belong only to the current listing, preserves shared or contested files, and optionally reportsinProgress.
Important implementation details
claimListingMediaFiles first creates claims and then checks for shared references. If any newly claimed file is already referenced elsewhere, it throws MEDIA_IN_USE and releases only the claims created by that call. The duplicate-key branch treats an existing active claim from the same listing as idempotent.
deleteListingFilesSafely uses a two-phase model. First, it acquires claims for all files without deleting anything. Then it checks whether any acquired file is referenced by another listing. After that, it moves each exclusive file from deleting to erasing, calls deleteAsset, and finally writes the tombstone state deleted.
The module also handles partial failures. If deleteAsset fails for some files, it keeps their claims in deleting, collects failure metadata, and throws an aggregate error with code IMAGEKIT_PARTIAL_DELETE_FAILURE.
Workflows
Attachment workflow
- The caller collects new file IDs.
claimListingMediaFilesinsertsMediaFileClaimrecords inactive.- The function checks whether another listing already references those IDs.
- If conflict exists, the function releases the newly created claims.
- The caller continues only after the listing write is safe.
Deletion workflow
- The caller sends the target listing and the file IDs to delete.
deleteListingFilesSafelyacquires claims with a privateholder.- The function checks whether any file is still shared.
- Shared or contested files are preserved.
- Exclusive files move to
erasing, thendeleteAssetruns. - Successful deletions end in
deleted. - Failures revert to
deletingfor a later retry.
Gotchas & edge cases
[!WARNING]
claimListingMediaFilesonly releases claims created in the current call when it seesMEDIA_IN_USE. Existing claims from other states are not touched.
[!CAUTION]
deleteListingFilesSafelycan returninProgresswhen another operation from the same listing already owns the file. That file is preserved in the result, but its physical deletion is not confirmed.
[!IMPORTANT] MongoDB errors during phase 1 are propagated before any deletion happens. ImageKit errors are accumulated and reported after all files are attempted.
[!NOTE] The
hooksargument exists only for tests. The code uses it to pause atdelete:afterSharedCheck.
Related
services/api/src/modules/listings/listings.routes.jsusesclaimListingMediaFiles,deleteListingFilesSafely,listingImageKitFileIds, andreleaseListingMediaClaimsinside listing write and deletion paths.services/api/src/modules/media/media.service.jsprovidesdeleteAsset, which performs the physical ImageKit deletion.services/api/src/models/Listing.jsis queried to detect whether other listings still reference a file.services/api/src/models/MediaFileClaim.jsstores the claim state andholderdata used by this module.services/api/integration/mongo/e5-3-b2-claim-possession.mongo.jsandservices/api/integration/mongo/e6-3-c3-adversarial.mongo.jsexercise the possession rules, retries, and blocking states under concurrent conditions.