forked from Yara724/api
114 lines
3.2 KiB
TypeScript
114 lines
3.2 KiB
TypeScript
/**
|
|
* Regression: concurrent capturePart writes must not clobber sibling slots.
|
|
*
|
|
* capturePartV2 used to `$set` the entire `media.damagedParts` array from a
|
|
* stale read. Parallel uploads (common while Fanavaran attachment submit keeps
|
|
* the HTTP request open) made the last writer win — Fanavaran still saw each
|
|
* file on disk and could return errors, while Mongo was missing captures.
|
|
*
|
|
* Required strategy: per-index `$set` (`media.damagedParts.N`), matching
|
|
* `media.carAngles.<key>`.
|
|
*/
|
|
describe("capture-part media.damagedParts write strategies", () => {
|
|
type Row = { path?: string; fileName?: string; name?: string };
|
|
type Claim = { media: { damagedParts: Row[] } };
|
|
|
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
/** Legacy (buggy) strategy: replace the entire array from a stale read. */
|
|
async function writeWholeArray(
|
|
store: { claim: Claim },
|
|
index: number,
|
|
capture: Row,
|
|
readDelayMs: number,
|
|
) {
|
|
const snapshot = structuredClone(store.claim);
|
|
await sleep(readDelayMs);
|
|
const next = snapshot.media.damagedParts.map((row) => ({ ...row }));
|
|
while (next.length <= index) next.push({});
|
|
next[index] = { ...next[index], ...capture };
|
|
store.claim = {
|
|
...store.claim,
|
|
media: { ...store.claim.media, damagedParts: next },
|
|
};
|
|
}
|
|
|
|
/** Required strategy: set only the target index (Mongo $set media.damagedParts.N). */
|
|
async function writeSingleIndex(
|
|
store: { claim: Claim },
|
|
index: number,
|
|
capture: Row,
|
|
readDelayMs: number,
|
|
) {
|
|
await sleep(readDelayMs);
|
|
const next = store.claim.media.damagedParts.map((row) => ({ ...row }));
|
|
while (next.length <= index) next.push({});
|
|
next[index] = { ...next[index], ...capture };
|
|
store.claim.media.damagedParts[index] = next[index];
|
|
}
|
|
|
|
it("documents that whole-array replace loses a concurrent capture", async () => {
|
|
const store: { claim: Claim } = {
|
|
claim: {
|
|
media: {
|
|
damagedParts: [{ name: "hood" }, { name: "front_bumper" }],
|
|
},
|
|
},
|
|
};
|
|
|
|
await Promise.all([
|
|
writeWholeArray(
|
|
store,
|
|
0,
|
|
{ path: "files/claim-captures/hood.jpg", fileName: "hood.jpg" },
|
|
30,
|
|
),
|
|
writeWholeArray(
|
|
store,
|
|
1,
|
|
{
|
|
path: "files/claim-captures/bumper.jpg",
|
|
fileName: "bumper.jpg",
|
|
},
|
|
10,
|
|
),
|
|
]);
|
|
|
|
expect(store.claim.media.damagedParts.map((r) => r.path).filter(Boolean))
|
|
.toHaveLength(1);
|
|
});
|
|
|
|
it("keeps both concurrent captures with per-index writes", async () => {
|
|
const store: { claim: Claim } = {
|
|
claim: {
|
|
media: {
|
|
damagedParts: [{ name: "hood" }, { name: "front_bumper" }],
|
|
},
|
|
},
|
|
};
|
|
|
|
await Promise.all([
|
|
writeSingleIndex(
|
|
store,
|
|
0,
|
|
{ path: "files/claim-captures/hood.jpg", fileName: "hood.jpg" },
|
|
30,
|
|
),
|
|
writeSingleIndex(
|
|
store,
|
|
1,
|
|
{
|
|
path: "files/claim-captures/bumper.jpg",
|
|
fileName: "bumper.jpg",
|
|
},
|
|
10,
|
|
),
|
|
]);
|
|
|
|
expect(store.claim.media.damagedParts.map((r) => r.path)).toEqual([
|
|
"files/claim-captures/hood.jpg",
|
|
"files/claim-captures/bumper.jpg",
|
|
]);
|
|
});
|
|
});
|