Annotation has InReplyTo field that points to a non existing annotation

WebViewer Version: 8.3.0

Do you have an issue with a specific file(s)? N/A
Can you reproduce using one of our samples or online demos? No
Are you using the WebViewer server? No
Does the issue only happen on certain browsers? No
Is your issue related to a front-end framework? No
Is your issue related to annotations? Yes

Please give a brief summary of your issue:

I am have a page that uses pdftron realtime collaboration outlined in the following links:

Our realtime collaboration setup utilizes firebase as it’s backend. Every change to annotation in the pdf triggers exporting the annotation to xfdf format and saving it our SQL database.

My issue is that for some reason, one of our users managed to create an annotation that has a InReplyTo value but that value does not point to an existing annotation. I am under the impression that an annotation is not supposed to have an InReplyTo value that does not link to another annoation. This problematic annotation is preventing our “Save to SQL database” function from proceeding. I know a simple null check would prevent this issue. But I would like to ask how did this happen and how can we prevent it from occurring again?

Here is the annotation in question extracted from annotationManager.getAnnotationsList():

{
        "fR": true,
        "Subject": "Note",
        "uD": 18,
        "Dx": 337.79,
        "Ex": 313.06999999999994,
        "kt": 31,
        "it": 31,
        "Rotation": 0,
        "Wd": null,
        "BL": 0,
        "Ax": true,
        "EL": false,
        "FL": false,
        "Hi": {
            "trn-mention": "{\"contents\":\"igone this.\",\"ids\":[]}"
        },
        "xx": true,
        "zL": false,
        "sD": false,
        "yx": true,
        "AL": false,
        "rD": true,
        "NoZoom": true,
        "NoRotate": true,
        "GL": true,
        "Jl": {
            "R": 255,
            "G": 255,
            "B": 0,
            "A": 1
        },
        "VL": "Annotation Author Name",
        "HD": "2022-02-13T08:35:30.000Z",
        "cy": "e89570db-6040-eb9a-fe43-ad55bae3c0d6",
        "VD": false,
        "On": false,
        "LM": false,
        "jN": {},
        "hk": [],
        "oi": null,
        "hC": null,
        "mI": null,
        "aq": [],
        "Bn": {},
        "An": {},
        "DL": "772d1055-ec80-2b57-04a9-566a78fd7fb5",
        "HL": null,
        "ak": null,
        "bk": null,
        "cN": 1,
        "i_": null,
        "gM": "2022-02-13T08:35:30.000Z",
        "wy": null,
        "_xsi:type": "Sticky",
        "Icon": "Comment",
        "Cx": "None",
        "KL": "Review",
        "isImporting": false,
        "eb": {},
        "CB": "337.790,497.850,368.790,528.850",
        "z8": false,
        "Invisible": false,
        "Hidden": false,
        "NoView": false,
        "ReadOnly": false,
        "Locked": false,
        "ToggleNoView": false,
        "LockedContents": false,
        "op": "D:20220213190530+10'30'",
        "Lv": 1644741330000,
        "nI": "igone this.",
        "OS": "igone this.",
        "Zaa": "igone this.",
        "lI": null,
        "kI": "D:20220213190530+10'30'",
        "PS": 1644741330000,
        "statemodel": "Review",
        "AB": {
            "x1": 337.79,
            "y1": 313.06999999999994,
            "x2": 368.79,
            "y2": 344.06999999999994
        },
        "ToolName": "AnnotationCreateSticky",
        "authorId": "49"
    }

Here is the annotationChanged event function:

      annotManager.addEventListener('annotationChanged', async (annotations, type, { imported }) => {
        if (imported)
          return;

        const xfdf = await annotManager.exportAnnotCommand();

        const processAnnotations = () => {

          annotations.forEach((annotation) => {
            if (type === 'add') {

              let parentAuthorId = null;
              if (annotation.InReplyTo) {
                parentAuthorId = annotManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
              }

              if (authorId) {
                annotation.authorId = authorId;
                annotation.setCustomData('userId', authorId);
                annotation.setCustomData('fromENT', 1);
              }

              server.createAnnotation(annotation.Id, {
                authorId: authorId,
                parentAuthorId: parentAuthorId,
                xfdf: xfdf
              });

            } else if (type === 'modify') {

              let parentAuthorId = null;

              if (annotation.InReplyTo) {
                parentAuthorId = annotManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
              } else {
                if (authorId != annotation.authorId)
                  return;
              }

              server.updateAnnotation(annotation.Id, {
                authorId: authorId,
                parentAuthorId: parentAuthorId,
                xfdf: xfdf
              });

            } else if (type === 'delete') {
              server.deleteAnnotation(annotation.Id);
            }

          });

        }

        processAnnotations();
        self.saveAnnotations();

      });

And here is the saveAnnotations function (the code shown here filters out annotations with Rejected and Cancelled status):

self.saveAnnotations = function () {

...
    let annotList = annotManager.getAnnotationsList().sort((a, b) => a.DateModified - b.DateModified);

    annotList.forEach(function (annotation) {

      let parentAnnot = null;
      if (annotation.InReplyTo) {
        parentAnnot = annotManager.getAnnotationById(annotation.InReplyTo);

        if (typeof annotation.getState === "function") {
          if (annotation.getState() == "Accepted"
            || annotation.getState() == "Rejected"
            || annotation.getState() == "Completed"
            || annotation.getState() == "Cancelled")
            parentAnnot.mainState = annotation.getState();
        } else
          parentAnnot.mainState = "Completed";
      }

    });

...
    var filteredAnnotList = annotList.filter(function (annotation) {

      if (annotation.mainState !== undefined) {
        return annotation.mainState !== "Rejected" && annotation.mainState !== "Cancelled";
      } else {
        let parentAnnot = null;
        if (annotation.InReplyTo) {
          parentAnnot = annotManager.getAnnotationById(annotation.InReplyTo); //parentAnnot is null when this annotation is process
          return parentAnnot.mainState !== "Rejected" && parentAnnot.mainState !== "Cancelled";  //an error is thrown in this line due to parentAnnot being null
        } else {
          return true;
        }
      }

    });
...
// Save to database code here

Please describe your issue and provide steps to reproduce it:
(The more descriptive your answer, the faster we are able to help you)
N/A

Please provide a link to a minimal sample where the issue is reproducible:
N/A

Hello, I’m Ron, an automated tech support bot :robot:

While you wait for one of our customer support representatives to get back to you, please check out some of these documentation pages:

Guides:APIs:Forums:

Hi @adrianm,

Thanks for reaching out.

Looking over your code I dont see any obvious issues, as it seems to mostly follow the sample we provide. You are indeed correct that if the InReplyTo property is present it should point to an annotation. I think potentially what happened here is that the parent annotation was deleted, and for some reason the reply was not. However, I’ve run through various scenarios and was not able to reproduce this as deleting the parent always removes the reply, or if the reply has the flag set NoDelete to true, it removes its reference from the original parent annotation.

Could this potentially be an issue with a parent annotation that was not saved correctly in your server due to a connectivity issue? Going over the source code on my end I can’t see how we would end up with a reply to a non-existing annotation. If you are able to find out the repro steps to end up in this state we can look at this further.

As a sidenote, we recently released a new realtime collaboration module that offers a lot more features and functionality. You can read more about it here:

Best Regards,

Armando Bollain
Software Developer
PDFTron Systems, Inc.
www.pdftron.com