Skip to content

Commit 02b4532

Browse files
Stamp the links a merge moves onto its target
1 parent d66aad4 commit 02b4532

2 files changed

Lines changed: 67 additions & 18 deletions

File tree

src/__tests__/store/analysisSlice.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2512,6 +2512,45 @@ describe('analysis timestamps', () => {
25122512
expect(approved.link).toMatchObject({ createdAt: SECOND_WRITE, updatedAt: SECOND_WRITE });
25132513
});
25142514

2515+
it('stamps every link a merge moves onto the target', () => {
2516+
const store = createAnalysisStore();
2517+
store.dispatch(writeGloss('tok-1', 'cat', 'feline'));
2518+
store.dispatch(writeGloss('tok-2', 'cat', 'tomcat'));
2519+
const source = approvedPair(store.getState().analysis, 'tok-1');
2520+
const target = approvedPair(store.getState().analysis, 'tok-2');
2521+
2522+
setClock(SECOND_WRITE);
2523+
store.dispatch(
2524+
mergeAnalysisInto({
2525+
sourceAnalysisId: source.analysis?.id ?? '',
2526+
targetAnalysisId: target.analysis?.id ?? '',
2527+
}),
2528+
);
2529+
2530+
const moved = approvedPair(store.getState().analysis, 'tok-1');
2531+
expect(moved.link).toMatchObject({ createdAt: FIRST_WRITE, updatedAt: SECOND_WRITE });
2532+
});
2533+
2534+
it('leaves the merge target payload and its own links dated by their content', () => {
2535+
const store = createAnalysisStore();
2536+
store.dispatch(writeGloss('tok-1', 'cat', 'feline'));
2537+
store.dispatch(writeGloss('tok-2', 'cat', 'tomcat'));
2538+
const source = approvedPair(store.getState().analysis, 'tok-1');
2539+
const target = approvedPair(store.getState().analysis, 'tok-2');
2540+
2541+
setClock(SECOND_WRITE);
2542+
store.dispatch(
2543+
mergeAnalysisInto({
2544+
sourceAnalysisId: source.analysis?.id ?? '',
2545+
targetAnalysisId: target.analysis?.id ?? '',
2546+
}),
2547+
);
2548+
2549+
const survivor = approvedPair(store.getState().analysis, 'tok-2');
2550+
expect(survivor.analysis).toMatchObject({ createdAt: FIRST_WRITE, updatedAt: FIRST_WRITE });
2551+
expect(survivor.link).toMatchObject({ createdAt: FIRST_WRITE, updatedAt: FIRST_WRITE });
2552+
});
2553+
25152554
it('stamps a morpheme gloss edit on the payload and the link', () => {
25162555
const store = createAnalysisStore();
25172556
store.dispatch(writeMorphemes('tok-1', 'cats', ['cat', 's'], 'en'));

src/store/analysisSlice.ts

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -891,25 +891,35 @@ const analysisSlice = createSlice({
891891
* usage count becomes the sum of the two and the source's tokens end up analyzed as the target
892892
* rather than stranded with nothing.
893893
*
894-
* Only the links move: no write is aimed at what the target says, so neither it nor the moved
895-
* links are re-stamped. No-ops when either id resolves to no payload, or when both name the
896-
* same record.
894+
* Only the links move, and only they are stamped: a moved link's token comes to say something
895+
* different, while no write is aimed at the target's own content, so its timestamps keep
896+
* reporting the age of the record. No-ops when either id resolves to no payload, or when both
897+
* name the same record.
897898
*/
898-
mergeAnalysisInto(
899-
state,
900-
action: PayloadAction<{ sourceAnalysisId: string; targetAnalysisId: string }>,
901-
) {
902-
const { sourceAnalysisId, targetAnalysisId } = action.payload;
903-
if (sourceAnalysisId === targetAnalysisId) return;
904-
const has = (id: string) => state.analysis.tokenAnalyses.some((ta) => ta.id === id);
905-
if (!has(sourceAnalysisId) || !has(targetAnalysisId)) return;
906-
907-
state.analysis.tokenAnalysisLinks.forEach((l) => {
908-
if (l.analysisId === sourceAnalysisId) l.analysisId = targetAnalysisId;
909-
});
910-
state.analysis.tokenAnalyses = state.analysis.tokenAnalyses.filter(
911-
(ta) => ta.id !== sourceAnalysisId,
912-
);
899+
mergeAnalysisInto: {
900+
/** Reads the clock before the action reaches the reducer, keeping the reducer pure. */
901+
prepare(arg: { sourceAnalysisId: string; targetAnalysisId: string }) {
902+
return { payload: { ...arg, now: nowIso() } };
903+
},
904+
reducer(
905+
state,
906+
action: PayloadAction<{ sourceAnalysisId: string; targetAnalysisId: string; now: string }>,
907+
) {
908+
const { sourceAnalysisId, targetAnalysisId, now } = action.payload;
909+
if (sourceAnalysisId === targetAnalysisId) return;
910+
const has = (id: string) => state.analysis.tokenAnalyses.some((ta) => ta.id === id);
911+
if (!has(sourceAnalysisId) || !has(targetAnalysisId)) return;
912+
913+
state.analysis.tokenAnalysisLinks.forEach((l) => {
914+
if (l.analysisId === sourceAnalysisId) {
915+
l.analysisId = targetAnalysisId;
916+
l.updatedAt = now;
917+
}
918+
});
919+
state.analysis.tokenAnalyses = state.analysis.tokenAnalyses.filter(
920+
(ta) => ta.id !== sourceAnalysisId,
921+
);
922+
},
913923
},
914924
/**
915925
* Approves a shared `TokenAnalysis` payload for a token — the persisted half of accepting a

0 commit comments

Comments
 (0)