@fluidframework/merge-tree 2.22.1 → 2.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/mergeTree.d.ts.map +1 -1
  3. package/dist/mergeTree.js +12 -9
  4. package/dist/mergeTree.js.map +1 -1
  5. package/dist/mergeTreeNodes.d.ts.map +1 -1
  6. package/dist/mergeTreeNodes.js +0 -2
  7. package/dist/mergeTreeNodes.js.map +1 -1
  8. package/dist/partialLengths.d.ts.map +1 -1
  9. package/dist/partialLengths.js +3 -4
  10. package/dist/partialLengths.js.map +1 -1
  11. package/dist/segmentInfos.d.ts +10 -18
  12. package/dist/segmentInfos.d.ts.map +1 -1
  13. package/dist/segmentInfos.js +22 -3
  14. package/dist/segmentInfos.js.map +1 -1
  15. package/dist/snapshotLoader.d.ts.map +1 -1
  16. package/dist/snapshotLoader.js +0 -2
  17. package/dist/snapshotLoader.js.map +1 -1
  18. package/dist/test/client.obliterateFarm.spec.d.ts.map +1 -1
  19. package/dist/test/client.obliterateFarm.spec.js +12 -0
  20. package/dist/test/client.obliterateFarm.spec.js.map +1 -1
  21. package/dist/test/obliterate.concurrent.spec.js +1 -1
  22. package/dist/test/obliterate.concurrent.spec.js.map +1 -1
  23. package/dist/test/obliterateOperations.d.ts +1 -0
  24. package/dist/test/obliterateOperations.d.ts.map +1 -1
  25. package/dist/test/obliterateOperations.js +35 -13
  26. package/dist/test/obliterateOperations.js.map +1 -1
  27. package/lib/mergeTree.d.ts.map +1 -1
  28. package/lib/mergeTree.js +13 -10
  29. package/lib/mergeTree.js.map +1 -1
  30. package/lib/mergeTreeNodes.d.ts.map +1 -1
  31. package/lib/mergeTreeNodes.js +0 -2
  32. package/lib/mergeTreeNodes.js.map +1 -1
  33. package/lib/partialLengths.d.ts.map +1 -1
  34. package/lib/partialLengths.js +4 -5
  35. package/lib/partialLengths.js.map +1 -1
  36. package/lib/segmentInfos.d.ts +10 -18
  37. package/lib/segmentInfos.d.ts.map +1 -1
  38. package/lib/segmentInfos.js +20 -2
  39. package/lib/segmentInfos.js.map +1 -1
  40. package/lib/snapshotLoader.d.ts.map +1 -1
  41. package/lib/snapshotLoader.js +0 -2
  42. package/lib/snapshotLoader.js.map +1 -1
  43. package/lib/test/client.obliterateFarm.spec.d.ts.map +1 -1
  44. package/lib/test/client.obliterateFarm.spec.js +13 -1
  45. package/lib/test/client.obliterateFarm.spec.js.map +1 -1
  46. package/lib/test/obliterate.concurrent.spec.js +1 -1
  47. package/lib/test/obliterate.concurrent.spec.js.map +1 -1
  48. package/lib/test/obliterateOperations.d.ts +1 -0
  49. package/lib/test/obliterateOperations.d.ts.map +1 -1
  50. package/lib/test/obliterateOperations.js +33 -12
  51. package/lib/test/obliterateOperations.js.map +1 -1
  52. package/package.json +17 -17
  53. package/src/mergeTree.ts +14 -9
  54. package/src/mergeTreeNodes.ts +0 -2
  55. package/src/partialLengths.ts +12 -5
  56. package/src/segmentInfos.ts +23 -21
  57. package/src/snapshotLoader.ts +0 -2
@@ -8,6 +8,7 @@ import { type TestOperation } from "./mergeTreeOperationRunner.js";
8
8
  import type { TestClient } from "./testClient.js";
9
9
  export declare const insertField: TestOperation;
10
10
  export declare const obliterateField: TestOperation;
11
+ export declare const obliterateFieldZeroLength: TestOperation;
11
12
  export declare const insertAvoidField: TestOperation;
12
13
  export declare const removeWithField: TestOperation;
13
14
  export declare const annotateWithField: TestOperation;
@@ -1 +1 @@
1
- {"version":3,"file":"obliterateOperations.d.ts","sourceRoot":"","sources":["../../src/test/obliterateOperations.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAC;AAGpE,OAAO,KAAK,EAAuB,YAAY,EAAE,MAAM,WAAW,CAAC;AAGnE,OAAO,EAA8B,KAAK,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAuElD,eAAO,MAAM,WAAW,EAAE,aAUzB,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,aA6C7B,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,aAY9B,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,aAc7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,aAc/B,CAAC;AAEF,eAAO,MAAM,uBAAuB,WAC3B,UAAU,UACV,OAAO,KACb,YAAY,GAAG,SAQjB,CAAC"}
1
+ {"version":3,"file":"obliterateOperations.d.ts","sourceRoot":"","sources":["../../src/test/obliterateOperations.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAC;AAGpE,OAAO,KAAK,EAAuB,YAAY,EAAE,MAAM,WAAW,CAAC;AAGnE,OAAO,EAA8B,KAAK,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAuElD,eAAO,MAAM,WAAW,EAAE,aAUzB,CAAC;AAoBF,eAAO,MAAM,eAAe,EAAE,aAkC7B,CAAC;AAEF,eAAO,MAAM,yBAAyB,EAAE,aAgCvC,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,aAY9B,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,aAc7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,aAc/B,CAAC;AAEF,eAAO,MAAM,uBAAuB,WAC3B,UAAU,UACV,OAAO,KACb,YAAY,GAAG,SAQjB,CAAC"}
@@ -51,6 +51,15 @@ export const insertField = (client, opStart, opEnd, random) => {
51
51
  return client.insertTextLocal(opStart, `{${numberText}}`);
52
52
  }
53
53
  };
54
+ const obliterateHelper = (client, startPos, endPos, random) => {
55
+ const obliterateOp = client.obliterateRangeLocal({ pos: startPos, side: Side.After }, { pos: endPos, side: Side.Before });
56
+ const insertOp = insertFieldText(client, startPos + 1, random);
57
+ assert(insertOp !== undefined, "Insert op should not be undefined");
58
+ // TODO: AB#31001: We should be able to sometimes use group ops here rather than submit two separate ops,
59
+ // but this causes failures which likely indicate there are bugs with the intersection of obliterate and grouped batching.
60
+ // const op = createGroupOp(obliterateOp, insertOp);
61
+ return [obliterateOp, insertOp];
62
+ };
54
63
  export const obliterateField = (client, opStart, opEnd, random) => {
55
64
  const fieldEndpoints = getFieldEndpoints(client, opStart,
56
65
  // the operation runner generates endpoints with client length, but this model only supports up to client length - 1.
@@ -59,18 +68,30 @@ export const obliterateField = (client, opStart, opEnd, random) => {
59
68
  if (fieldEndpoints !== undefined) {
60
69
  const { startPos, endPos } = fieldEndpoints;
61
70
  // Obliterate text between the separators, but avoid the case where the obliterate range is zero length.
62
- if (endPos - startPos > 1) {
63
- const obliterateOp = client.obliterateRangeLocal({ pos: startPos, side: Side.After }, { pos: endPos, side: Side.Before });
64
- const insertOp = insertFieldText(client, startPos + 1, random);
65
- assert(insertOp !== undefined, "Insert op should not be undefined");
66
- // TODO: AB#31001: We should be able to sometimes use group ops here rather than submit two separate ops,
67
- // but this causes failures which likely indicate there are bugs with the intersection of obliterate and grouped batching.
68
- // const op = createGroupOp(obliterateOp, insertOp);
69
- return [obliterateOp, insertOp];
70
- }
71
- else {
72
- return;
73
- }
71
+ return endPos - startPos > 1
72
+ ? obliterateHelper(client, startPos, endPos, random)
73
+ : undefined;
74
+ }
75
+ if (opEnd >= client.getLength()) {
76
+ endISP = { pos: client.getLength() - 1, side: Side.After };
77
+ }
78
+ if (!client.getText(opStart, opEnd).includes("{")) {
79
+ // Avoid issuing obliterates that might contain multiple fields.
80
+ // Otherwise we may end up with field characters that look like they're outside of the field,
81
+ // since one of these obliterates can wipe out the field including the `{}` delimiters, but
82
+ // a "field replace" obliterate + insert can win and insert the numerical characters.
83
+ return client.obliterateRangeLocal({ pos: opStart, side: Side.Before }, endISP ?? { pos: opEnd, side: Side.After });
84
+ }
85
+ };
86
+ export const obliterateFieldZeroLength = (client, opStart, opEnd, random) => {
87
+ const fieldEndpoints = getFieldEndpoints(client, opStart,
88
+ // the operation runner generates endpoints with client length, but this model only supports up to client length - 1.
89
+ Math.min(opEnd, client.getLength() - 1));
90
+ let endISP;
91
+ if (fieldEndpoints !== undefined) {
92
+ const { startPos, endPos } = fieldEndpoints;
93
+ // Obliterate text between the separators, including the case where the obliterate range is zero length.
94
+ return obliterateHelper(client, startPos, endPos, random);
74
95
  }
75
96
  if (opEnd >= client.getLength()) {
76
97
  endISP = { pos: client.getLength() - 1, side: Side.After };
@@ -1 +1 @@
1
- {"version":3,"file":"obliterateOperations.js","sourceRoot":"","sources":["../../src/test/obliterateOperations.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\n/* eslint-disable @typescript-eslint/no-non-null-assertion */\n\nimport { strict as assert } from \"node:assert\";\n\nimport type { IRandom } from \"@fluid-private/stochastic-test-utils\";\n\n// import { createGroupOp } from \"../opBuilder.js\";\nimport type { IMergeTreeInsertMsg, IMergeTreeOp } from \"../ops.js\";\nimport { InteriorSequencePlace, Side } from \"../sequencePlace.js\";\n\nimport { annotateRange, removeRange, type TestOperation } from \"./mergeTreeOperationRunner.js\";\nimport type { TestClient } from \"./testClient.js\";\n\nconst posInField = (\n\tclient: TestClient,\n\tpos: number,\n): { startPos: number; endPos: number } | undefined => {\n\tconst isFieldCharacter = (char: string): boolean =>\n\t\tNumber.isInteger(Number(char)) || char === \"{\" || char === \"}\";\n\tif (pos >= client.getLength() || !isFieldCharacter(client.getText(pos, pos + 1))) {\n\t\t// pos is not within a field.\n\t\treturn undefined;\n\t}\n\n\tlet startPos = pos;\n\tlet endPos = pos;\n\t// To find the start and end separators, walk backwards and forwards until the desired character is found.\n\twhile (startPos > 0 && client.getText(startPos, startPos + 1) !== \"{\") {\n\t\tconst text = client.getText(endPos, endPos + 1);\n\t\tassert(\n\t\t\tNumber.isInteger(Number(text)) || text === \"}\",\n\t\t\t\"Non-integer character found within a field\",\n\t\t);\n\t\tstartPos--;\n\t}\n\n\twhile (endPos < client.getLength() && client.getText(endPos, endPos + 1) !== \"}\") {\n\t\tconst text = client.getText(endPos, endPos + 1);\n\t\tassert(\n\t\t\tNumber.isInteger(Number(text)) || text === \"{\",\n\t\t\t\"Non-integer character found within a field\",\n\t\t);\n\t\tendPos++;\n\t}\n\n\tassert(client.getText(startPos, startPos + 1) === \"{\", \"Start separator not found\");\n\tassert(\n\t\tendPos < client.getLength() && client.getText(endPos, endPos + 1) === \"}\",\n\t\t\"End separator not found\",\n\t);\n\n\treturn { startPos, endPos };\n};\n\nconst getFieldEndpoints = (\n\tclient: TestClient,\n\tstart: number,\n\tend: number,\n): { startPos: number; endPos: number } | undefined => {\n\tconst startField = posInField(client, start);\n\tconst endField = posInField(client, end);\n\n\tif (startField === undefined && endField === undefined) {\n\t\treturn undefined;\n\t}\n\treturn startField ?? endField;\n};\n\nconst generateFieldText = (client: TestClient, random: IRandom): string => {\n\tconst chunkLength = random.integer(1, 10);\n\treturn (client.longClientId!.codePointAt(0)! % 10).toString().repeat(chunkLength);\n};\n\nconst insertFieldText = (\n\tclient: TestClient,\n\topStart: number,\n\trandom: IRandom,\n): IMergeTreeInsertMsg | undefined => {\n\tconst text = generateFieldText(client, random);\n\treturn client.insertTextLocal(opStart, text);\n};\n\nexport const insertField: TestOperation = (\n\tclient: TestClient,\n\topStart: number,\n\topEnd: number,\n\trandom: IRandom,\n) => {\n\tconst numberText = generateFieldText(client, random);\n\tif (posInField(client, opStart) === undefined) {\n\t\treturn client.insertTextLocal(opStart, `{${numberText}}`);\n\t}\n};\n\nexport const obliterateField: TestOperation = (\n\tclient: TestClient,\n\topStart: number,\n\topEnd: number,\n\trandom: IRandom,\n) => {\n\tconst fieldEndpoints = getFieldEndpoints(\n\t\tclient,\n\t\topStart,\n\t\t// the operation runner generates endpoints with client length, but this model only supports up to client length - 1.\n\t\tMath.min(opEnd, client.getLength() - 1),\n\t);\n\n\tlet endISP: InteriorSequencePlace | undefined;\n\tif (fieldEndpoints !== undefined) {\n\t\tconst { startPos, endPos } = fieldEndpoints;\n\t\t// Obliterate text between the separators, but avoid the case where the obliterate range is zero length.\n\t\tif (endPos - startPos > 1) {\n\t\t\tconst obliterateOp = client.obliterateRangeLocal(\n\t\t\t\t{ pos: startPos, side: Side.After },\n\t\t\t\t{ pos: endPos, side: Side.Before },\n\t\t\t);\n\t\t\tconst insertOp = insertFieldText(client, startPos + 1, random);\n\t\t\tassert(insertOp !== undefined, \"Insert op should not be undefined\");\n\t\t\t// TODO: AB#31001: We should be able to sometimes use group ops here rather than submit two separate ops,\n\t\t\t// but this causes failures which likely indicate there are bugs with the intersection of obliterate and grouped batching.\n\t\t\t// const op = createGroupOp(obliterateOp, insertOp);\n\t\t\treturn [obliterateOp, insertOp];\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t}\n\tif (opEnd >= client.getLength()) {\n\t\tendISP = { pos: client.getLength() - 1, side: Side.After };\n\t}\n\tif (!client.getText(opStart, opEnd).includes(\"{\")) {\n\t\t// Avoid issuing obliterates that might contain multiple fields.\n\t\t// Otherwise we may end up with field characters that look like they're outside of the field,\n\t\t// since one of these obliterates can wipe out the field including the `{}` delimiters, but\n\t\t// a \"field replace\" obliterate + insert can win and insert the numerical characters.\n\t\treturn client.obliterateRangeLocal(\n\t\t\t{ pos: opStart, side: Side.Before },\n\t\t\tendISP ?? { pos: opEnd, side: Side.After },\n\t\t);\n\t}\n};\n\nexport const insertAvoidField: TestOperation = (\n\tclient: TestClient,\n\topStart: number,\n\topEnd: number,\n\trandom: IRandom,\n) => {\n\tlet start = opStart;\n\tconst endpoints = posInField(client, opStart);\n\tif (endpoints !== undefined) {\n\t\tstart = endpoints.startPos;\n\t}\n\treturn client.insertTextLocal(start, client.longClientId!.repeat(random.integer(1, 3)));\n};\n\nexport const removeWithField: TestOperation = (\n\tclient: TestClient,\n\topStart: number,\n\topEnd: number,\n\trandom: IRandom,\n) => {\n\tlet start = opStart;\n\tlet end = opEnd;\n\tconst fieldEndpoints = getFieldEndpoints(client, opStart, opEnd);\n\tif (fieldEndpoints !== undefined) {\n\t\tstart = fieldEndpoints.startPos;\n\t\tend = fieldEndpoints.endPos + 1;\n\t}\n\treturn removeRange(client, start, end, random);\n};\n\nexport const annotateWithField: TestOperation = (\n\tclient: TestClient,\n\topStart: number,\n\topEnd: number,\n\trandom: IRandom,\n) => {\n\tlet start = opStart;\n\tlet end = opEnd;\n\tconst fieldEndpoints = getFieldEndpoints(client, opStart, opEnd);\n\tif (fieldEndpoints !== undefined) {\n\t\tstart = fieldEndpoints.startPos;\n\t\tend = fieldEndpoints.endPos + 1;\n\t}\n\treturn annotateRange(client, start, end, random);\n};\n\nexport const generateInsertWithField = (\n\tclient: TestClient,\n\trandom: IRandom,\n): IMergeTreeOp | undefined => {\n\tconst text = client.longClientId!.repeat(random.integer(1, 3));\n\tlet pos = random.integer(0, client.getLength());\n\tconst endpoints = posInField(client, pos);\n\tif (endpoints !== undefined) {\n\t\tpos = 0;\n\t}\n\treturn client.insertTextLocal(pos, text);\n};\n"]}
1
+ {"version":3,"file":"obliterateOperations.js","sourceRoot":"","sources":["../../src/test/obliterateOperations.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\n/* eslint-disable @typescript-eslint/no-non-null-assertion */\n\nimport { strict as assert } from \"node:assert\";\n\nimport type { IRandom } from \"@fluid-private/stochastic-test-utils\";\n\n// import { createGroupOp } from \"../opBuilder.js\";\nimport type { IMergeTreeInsertMsg, IMergeTreeOp } from \"../ops.js\";\nimport { InteriorSequencePlace, Side } from \"../sequencePlace.js\";\n\nimport { annotateRange, removeRange, type TestOperation } from \"./mergeTreeOperationRunner.js\";\nimport type { TestClient } from \"./testClient.js\";\n\nconst posInField = (\n\tclient: TestClient,\n\tpos: number,\n): { startPos: number; endPos: number } | undefined => {\n\tconst isFieldCharacter = (char: string): boolean =>\n\t\tNumber.isInteger(Number(char)) || char === \"{\" || char === \"}\";\n\tif (pos >= client.getLength() || !isFieldCharacter(client.getText(pos, pos + 1))) {\n\t\t// pos is not within a field.\n\t\treturn undefined;\n\t}\n\n\tlet startPos = pos;\n\tlet endPos = pos;\n\t// To find the start and end separators, walk backwards and forwards until the desired character is found.\n\twhile (startPos > 0 && client.getText(startPos, startPos + 1) !== \"{\") {\n\t\tconst text = client.getText(endPos, endPos + 1);\n\t\tassert(\n\t\t\tNumber.isInteger(Number(text)) || text === \"}\",\n\t\t\t\"Non-integer character found within a field\",\n\t\t);\n\t\tstartPos--;\n\t}\n\n\twhile (endPos < client.getLength() && client.getText(endPos, endPos + 1) !== \"}\") {\n\t\tconst text = client.getText(endPos, endPos + 1);\n\t\tassert(\n\t\t\tNumber.isInteger(Number(text)) || text === \"{\",\n\t\t\t\"Non-integer character found within a field\",\n\t\t);\n\t\tendPos++;\n\t}\n\n\tassert(client.getText(startPos, startPos + 1) === \"{\", \"Start separator not found\");\n\tassert(\n\t\tendPos < client.getLength() && client.getText(endPos, endPos + 1) === \"}\",\n\t\t\"End separator not found\",\n\t);\n\n\treturn { startPos, endPos };\n};\n\nconst getFieldEndpoints = (\n\tclient: TestClient,\n\tstart: number,\n\tend: number,\n): { startPos: number; endPos: number } | undefined => {\n\tconst startField = posInField(client, start);\n\tconst endField = posInField(client, end);\n\n\tif (startField === undefined && endField === undefined) {\n\t\treturn undefined;\n\t}\n\treturn startField ?? endField;\n};\n\nconst generateFieldText = (client: TestClient, random: IRandom): string => {\n\tconst chunkLength = random.integer(1, 10);\n\treturn (client.longClientId!.codePointAt(0)! % 10).toString().repeat(chunkLength);\n};\n\nconst insertFieldText = (\n\tclient: TestClient,\n\topStart: number,\n\trandom: IRandom,\n): IMergeTreeInsertMsg | undefined => {\n\tconst text = generateFieldText(client, random);\n\treturn client.insertTextLocal(opStart, text);\n};\n\nexport const insertField: TestOperation = (\n\tclient: TestClient,\n\topStart: number,\n\topEnd: number,\n\trandom: IRandom,\n) => {\n\tconst numberText = generateFieldText(client, random);\n\tif (posInField(client, opStart) === undefined) {\n\t\treturn client.insertTextLocal(opStart, `{${numberText}}`);\n\t}\n};\n\nconst obliterateHelper = (\n\tclient: TestClient,\n\tstartPos: number,\n\tendPos: number,\n\trandom: IRandom,\n): IMergeTreeOp[] => {\n\tconst obliterateOp = client.obliterateRangeLocal(\n\t\t{ pos: startPos, side: Side.After },\n\t\t{ pos: endPos, side: Side.Before },\n\t);\n\tconst insertOp = insertFieldText(client, startPos + 1, random);\n\tassert(insertOp !== undefined, \"Insert op should not be undefined\");\n\t// TODO: AB#31001: We should be able to sometimes use group ops here rather than submit two separate ops,\n\t// but this causes failures which likely indicate there are bugs with the intersection of obliterate and grouped batching.\n\t// const op = createGroupOp(obliterateOp, insertOp);\n\treturn [obliterateOp, insertOp];\n};\n\nexport const obliterateField: TestOperation = (\n\tclient: TestClient,\n\topStart: number,\n\topEnd: number,\n\trandom: IRandom,\n) => {\n\tconst fieldEndpoints = getFieldEndpoints(\n\t\tclient,\n\t\topStart,\n\t\t// the operation runner generates endpoints with client length, but this model only supports up to client length - 1.\n\t\tMath.min(opEnd, client.getLength() - 1),\n\t);\n\n\tlet endISP: InteriorSequencePlace | undefined;\n\tif (fieldEndpoints !== undefined) {\n\t\tconst { startPos, endPos } = fieldEndpoints;\n\t\t// Obliterate text between the separators, but avoid the case where the obliterate range is zero length.\n\t\treturn endPos - startPos > 1\n\t\t\t? obliterateHelper(client, startPos, endPos, random)\n\t\t\t: undefined;\n\t}\n\tif (opEnd >= client.getLength()) {\n\t\tendISP = { pos: client.getLength() - 1, side: Side.After };\n\t}\n\tif (!client.getText(opStart, opEnd).includes(\"{\")) {\n\t\t// Avoid issuing obliterates that might contain multiple fields.\n\t\t// Otherwise we may end up with field characters that look like they're outside of the field,\n\t\t// since one of these obliterates can wipe out the field including the `{}` delimiters, but\n\t\t// a \"field replace\" obliterate + insert can win and insert the numerical characters.\n\t\treturn client.obliterateRangeLocal(\n\t\t\t{ pos: opStart, side: Side.Before },\n\t\t\tendISP ?? { pos: opEnd, side: Side.After },\n\t\t);\n\t}\n};\n\nexport const obliterateFieldZeroLength: TestOperation = (\n\tclient: TestClient,\n\topStart: number,\n\topEnd: number,\n\trandom: IRandom,\n) => {\n\tconst fieldEndpoints = getFieldEndpoints(\n\t\tclient,\n\t\topStart,\n\t\t// the operation runner generates endpoints with client length, but this model only supports up to client length - 1.\n\t\tMath.min(opEnd, client.getLength() - 1),\n\t);\n\n\tlet endISP: InteriorSequencePlace | undefined;\n\tif (fieldEndpoints !== undefined) {\n\t\tconst { startPos, endPos } = fieldEndpoints;\n\t\t// Obliterate text between the separators, including the case where the obliterate range is zero length.\n\t\treturn obliterateHelper(client, startPos, endPos, random);\n\t}\n\tif (opEnd >= client.getLength()) {\n\t\tendISP = { pos: client.getLength() - 1, side: Side.After };\n\t}\n\tif (!client.getText(opStart, opEnd).includes(\"{\")) {\n\t\t// Avoid issuing obliterates that might contain multiple fields.\n\t\t// Otherwise we may end up with field characters that look like they're outside of the field,\n\t\t// since one of these obliterates can wipe out the field including the `{}` delimiters, but\n\t\t// a \"field replace\" obliterate + insert can win and insert the numerical characters.\n\t\treturn client.obliterateRangeLocal(\n\t\t\t{ pos: opStart, side: Side.Before },\n\t\t\tendISP ?? { pos: opEnd, side: Side.After },\n\t\t);\n\t}\n};\n\nexport const insertAvoidField: TestOperation = (\n\tclient: TestClient,\n\topStart: number,\n\topEnd: number,\n\trandom: IRandom,\n) => {\n\tlet start = opStart;\n\tconst endpoints = posInField(client, opStart);\n\tif (endpoints !== undefined) {\n\t\tstart = endpoints.startPos;\n\t}\n\treturn client.insertTextLocal(start, client.longClientId!.repeat(random.integer(1, 3)));\n};\n\nexport const removeWithField: TestOperation = (\n\tclient: TestClient,\n\topStart: number,\n\topEnd: number,\n\trandom: IRandom,\n) => {\n\tlet start = opStart;\n\tlet end = opEnd;\n\tconst fieldEndpoints = getFieldEndpoints(client, opStart, opEnd);\n\tif (fieldEndpoints !== undefined) {\n\t\tstart = fieldEndpoints.startPos;\n\t\tend = fieldEndpoints.endPos + 1;\n\t}\n\treturn removeRange(client, start, end, random);\n};\n\nexport const annotateWithField: TestOperation = (\n\tclient: TestClient,\n\topStart: number,\n\topEnd: number,\n\trandom: IRandom,\n) => {\n\tlet start = opStart;\n\tlet end = opEnd;\n\tconst fieldEndpoints = getFieldEndpoints(client, opStart, opEnd);\n\tif (fieldEndpoints !== undefined) {\n\t\tstart = fieldEndpoints.startPos;\n\t\tend = fieldEndpoints.endPos + 1;\n\t}\n\treturn annotateRange(client, start, end, random);\n};\n\nexport const generateInsertWithField = (\n\tclient: TestClient,\n\trandom: IRandom,\n): IMergeTreeOp | undefined => {\n\tconst text = client.longClientId!.repeat(random.integer(1, 3));\n\tlet pos = random.integer(0, client.getLength());\n\tconst endpoints = posInField(client, pos);\n\tif (endpoints !== undefined) {\n\t\tpos = 0;\n\t}\n\treturn client.insertTextLocal(pos, text);\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/merge-tree",
3
- "version": "2.22.1",
3
+ "version": "2.23.0",
4
4
  "description": "Merge tree",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -81,30 +81,30 @@
81
81
  "temp-directory": "nyc/.nyc_output"
82
82
  },
83
83
  "dependencies": {
84
- "@fluid-internal/client-utils": "~2.22.1",
85
- "@fluidframework/container-definitions": "~2.22.1",
86
- "@fluidframework/core-interfaces": "~2.22.1",
87
- "@fluidframework/core-utils": "~2.22.1",
88
- "@fluidframework/datastore-definitions": "~2.22.1",
89
- "@fluidframework/driver-definitions": "~2.22.1",
90
- "@fluidframework/runtime-definitions": "~2.22.1",
91
- "@fluidframework/runtime-utils": "~2.22.1",
92
- "@fluidframework/shared-object-base": "~2.22.1",
93
- "@fluidframework/telemetry-utils": "~2.22.1"
84
+ "@fluid-internal/client-utils": "~2.23.0",
85
+ "@fluidframework/container-definitions": "~2.23.0",
86
+ "@fluidframework/core-interfaces": "~2.23.0",
87
+ "@fluidframework/core-utils": "~2.23.0",
88
+ "@fluidframework/datastore-definitions": "~2.23.0",
89
+ "@fluidframework/driver-definitions": "~2.23.0",
90
+ "@fluidframework/runtime-definitions": "~2.23.0",
91
+ "@fluidframework/runtime-utils": "~2.23.0",
92
+ "@fluidframework/shared-object-base": "~2.23.0",
93
+ "@fluidframework/telemetry-utils": "~2.23.0"
94
94
  },
95
95
  "devDependencies": {
96
96
  "@arethetypeswrong/cli": "^0.17.1",
97
97
  "@biomejs/biome": "~1.9.3",
98
- "@fluid-internal/mocha-test-setup": "~2.22.1",
99
- "@fluid-private/stochastic-test-utils": "~2.22.1",
100
- "@fluid-private/test-pairwise-generator": "~2.22.1",
98
+ "@fluid-internal/mocha-test-setup": "~2.23.0",
99
+ "@fluid-private/stochastic-test-utils": "~2.23.0",
100
+ "@fluid-private/test-pairwise-generator": "~2.23.0",
101
101
  "@fluid-tools/benchmark": "^0.50.0",
102
- "@fluid-tools/build-cli": "^0.51.0",
102
+ "@fluid-tools/build-cli": "^0.54.0",
103
103
  "@fluidframework/build-common": "^2.0.3",
104
- "@fluidframework/build-tools": "^0.51.0",
104
+ "@fluidframework/build-tools": "^0.54.0",
105
105
  "@fluidframework/eslint-config-fluid": "^5.7.3",
106
106
  "@fluidframework/merge-tree-previous": "npm:@fluidframework/merge-tree@2.22.0",
107
- "@fluidframework/test-runtime-utils": "~2.22.1",
107
+ "@fluidframework/test-runtime-utils": "~2.23.0",
108
108
  "@microsoft/api-extractor": "7.47.8",
109
109
  "@types/diff": "^3.5.1",
110
110
  "@types/mocha": "^10.0.10",
package/src/mergeTree.ts CHANGED
@@ -95,6 +95,7 @@ import {
95
95
  removeRemovalInfo,
96
96
  toMoveInfo,
97
97
  toRemovalInfo,
98
+ wasMovedOnInsert,
98
99
  type IInsertionInfo,
99
100
  type IMoveInfo,
100
101
  type IRemovalInfo,
@@ -1083,7 +1084,14 @@ export class MergeTree {
1083
1084
  if (!this.collabWindow.collaborating || this.collabWindow.clientId === clientId) {
1084
1085
  if (node.isLeaf()) {
1085
1086
  return this.localNetLength(node, refSeq, localSeq);
1086
- } else if (localSeq === undefined) {
1087
+ } else if (
1088
+ localSeq === undefined ||
1089
+ // All changes are visible. Small note on why we allow refSeq >= this.collabWindow.currentSeq rather than just equality:
1090
+ // merge-tree eventing occurs before the collab window is updated to account for whatever op it is processing, and we want
1091
+ // to support resolving positions from within the event handler which account for that op. e.g. undo-redo relies on this
1092
+ // behavior with local references.
1093
+ (localSeq === this.collabWindow.localSeq && refSeq >= this.collabWindow.currentSeq)
1094
+ ) {
1087
1095
  // Local client sees all segments, even when collaborating
1088
1096
  return node.cachedLength;
1089
1097
  } else {
@@ -1178,6 +1186,8 @@ export class MergeTree {
1178
1186
  */
1179
1187
  public referencePositionToLocalPosition(
1180
1188
  refPos: ReferencePosition,
1189
+ // Note: this is not `this.collabWindow.currentSeq` because we want to support resolving local reference positions to positions
1190
+ // from within event handlers, and the collab window's sequence numbers are not updated in time in all of those cases.
1181
1191
  refSeq = Number.MAX_SAFE_INTEGER,
1182
1192
  clientId = this.collabWindow.clientId,
1183
1193
  localSeq: number | undefined = this.collabWindow.localSeq,
@@ -1675,12 +1685,11 @@ export class MergeTree {
1675
1685
  movedSeq: oldest.seq,
1676
1686
  movedSeqs,
1677
1687
  localMovedSeq: oldestUnacked?.localSeq,
1678
- wasMovedOnInsert: oldest.seq !== UnassignedSequenceNumber,
1679
1688
  };
1680
1689
  } else {
1681
1690
  assert(
1682
1691
  oldestUnacked !== undefined,
1683
- "Expected local obliterate to be defined if newestAcked is not equal to newest",
1692
+ 0xb55 /* Expected local obliterate to be defined if newestAcked is not equal to newest */,
1684
1693
  );
1685
1694
  // There's a pending local obliterate for this range, so it will be marked as obliterated by us. However,
1686
1695
  // all other clients are under the impression that the most recent acked obliterate won the right to insert
@@ -1690,7 +1699,6 @@ export class MergeTree {
1690
1699
  movedSeq: oldestUnacked.seq,
1691
1700
  movedSeqs: [oldestUnacked.seq],
1692
1701
  localMovedSeq: oldestUnacked.localSeq,
1693
- wasMovedOnInsert: false,
1694
1702
  };
1695
1703
  }
1696
1704
 
@@ -2141,8 +2149,6 @@ export class MergeTree {
2141
2149
  movedSeq: seq,
2142
2150
  localMovedSeq: localSeq,
2143
2151
  movedSeqs: [seq],
2144
- wasMovedOnInsert:
2145
- segment.seq === UnassignedSequenceNumber && seq !== UnassignedSequenceNumber,
2146
2152
  });
2147
2153
 
2148
2154
  const existingRemoval = toRemovalInfo(movedSeg);
@@ -2158,16 +2164,15 @@ export class MergeTree {
2158
2164
  }
2159
2165
  } else {
2160
2166
  if (existingMoveInfo.movedSeq === UnassignedSequenceNumber) {
2161
- // Should not need explicit set here, but this should be implied:
2162
2167
  assert(
2163
- !existingMoveInfo.wasMovedOnInsert,
2168
+ !wasMovedOnInsert(segment),
2164
2169
  0xab4 /* Local obliterate cannot have removed a segment as soon as it was inserted */,
2165
2170
  );
2166
2171
  assert(
2167
2172
  seq !== UnassignedSequenceNumber,
2168
2173
  0xab5 /* Cannot obliterate the same segment locally twice */,
2169
2174
  );
2170
- existingMoveInfo.wasMovedOnInsert = segment.seq === UnassignedSequenceNumber;
2175
+
2171
2176
  // we moved this locally, but someone else moved it first
2172
2177
  // so put them at the head of the list
2173
2178
  // The list isn't ordered, but we keep the first move at the head
@@ -375,7 +375,6 @@ export abstract class BaseSegment implements ISegment {
375
375
  overwriteInfo<IMoveInfo>(seg, {
376
376
  movedSeq: this.movedSeq,
377
377
  movedSeqs: [...this.movedSeqs],
378
- wasMovedOnInsert: this.wasMovedOnInsert,
379
378
  movedClientIds: [...this.movedClientIds],
380
379
  });
381
380
  }
@@ -441,7 +440,6 @@ export abstract class BaseSegment implements ISegment {
441
440
  movedSeq: this.movedSeq,
442
441
  movedSeqs: [...this.movedSeqs],
443
442
  localMovedSeq: this.localMovedSeq,
444
- wasMovedOnInsert: this.wasMovedOnInsert,
445
443
  });
446
444
  }
447
445
 
@@ -14,7 +14,12 @@ import {
14
14
  seqLTE,
15
15
  type MergeBlock,
16
16
  } from "./mergeTreeNodes.js";
17
- import { toRemovalInfo, toMoveInfo, assertInserted } from "./segmentInfos.js";
17
+ import {
18
+ toRemovalInfo,
19
+ toMoveInfo,
20
+ assertInserted,
21
+ wasMovedOnInsert,
22
+ } from "./segmentInfos.js";
18
23
  import { SortedSet } from "./sortedSet.js";
19
24
 
20
25
  class PartialSequenceLengthsSet extends SortedSet<PartialSequenceLength> {
@@ -472,8 +477,7 @@ export class PartialSequenceLengths {
472
477
  if (child.isLeaf()) {
473
478
  // Leaf segment
474
479
  const segment = child;
475
- const moveInfo = toMoveInfo(segment);
476
- if (moveInfo?.wasMovedOnInsert) {
480
+ if (wasMovedOnInsert(segment)) {
477
481
  PartialSequenceLengths.accountForMoveOnInsert(
478
482
  combinedPartialLengths,
479
483
  segment,
@@ -511,7 +515,10 @@ export class PartialSequenceLengths {
511
515
  ): void {
512
516
  assertInserted(segment);
513
517
  const moveInfo = toMoveInfo(segment);
514
- assert(moveInfo?.wasMovedOnInsert === true, 0xab7 /* Segment was not moved on insert */);
518
+ assert(
519
+ moveInfo !== undefined && wasMovedOnInsert(segment),
520
+ 0xab7 /* Segment was not moved on insert */,
521
+ );
515
522
  if (moveInfo.movedSeq <= collabWindow.minSeq) {
516
523
  // This segment was obliterated as soon as it was inserted, and everyone was aware of the obliterate.
517
524
  // Thus every single client treats this segment as length 0 from every perspective, and no adjustments
@@ -843,7 +850,7 @@ export class PartialSequenceLengths {
843
850
  segment.seq !== undefined &&
844
851
  moveInfo &&
845
852
  moveInfo.movedSeq < segment.seq &&
846
- moveInfo.wasMovedOnInsert
853
+ wasMovedOnInsert(segment)
847
854
  ) {
848
855
  this.addClientAdjustment(clientId, moveInfo.movedSeq, segment.cachedLength);
849
856
  failIncrementalPropagation = true;
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { assert, isObject } from "@fluidframework/core-utils/internal";
7
7
 
8
+ import { UnassignedSequenceNumber } from "./constants.js";
8
9
  import { ISegmentInternal, ISegmentPrivate, MergeBlock } from "./mergeTreeNodes.js";
9
10
  import type { ReferencePosition } from "./referencePositions.js";
10
11
 
@@ -296,31 +297,12 @@ export interface IMoveInfo {
296
297
  * list have all issued concurrent ops to move the segment.
297
298
  */
298
299
  movedClientIds: number[];
299
-
300
- /**
301
- * If this segment was inserted into a concurrently moved range and
302
- * the move op was sequenced before the insertion op. In this case,
303
- * the segment is visible only to the inserting client
304
- *
305
- * `wasMovedOnInsert` only applies for acked obliterates. That is, if
306
- * a segment inserted by a remote client is moved on insertion by a local
307
- * and unacked obliterate, we do not consider it as having been moved
308
- * on insert
309
- *
310
- * If a segment is moved on insertion, its length is only ever visible to
311
- * the client that inserted the segment. This is relevant in partial length
312
- * calculations
313
- *
314
- * @privateRemarks
315
- * TODO:AB#29553: This property is not persisted in the summary, but it should be.
316
- */
317
- wasMovedOnInsert: boolean;
318
300
  }
301
+
319
302
  export const toMoveInfo = (segmentLike: unknown): IMoveInfo | undefined =>
320
303
  hasProp(segmentLike, "movedClientIds", "array") &&
321
304
  hasProp(segmentLike, "movedSeq", "number") &&
322
- hasProp(segmentLike, "movedSeqs", "array") &&
323
- hasProp(segmentLike, "wasMovedOnInsert", "boolean")
305
+ hasProp(segmentLike, "movedSeqs", "array")
324
306
  ? segmentLike
325
307
  : undefined;
326
308
 
@@ -334,6 +316,26 @@ export const toMoveInfo = (segmentLike: unknown): IMoveInfo | undefined =>
334
316
  export const isMoved = (segmentLike: unknown): segmentLike is IMoveInfo =>
335
317
  toMoveInfo(segmentLike) !== undefined;
336
318
 
319
+ /**
320
+ * Returns whether this segment was marked moved as soon as its insertion was acked.
321
+ *
322
+ * This can happen when an an insert occurs concurrent to an obliterate over the range the segment was inserted into,
323
+ * and the obliterate was sequenced first.
324
+ *
325
+ * When this happens, the segment is only ever visible to the client that inserted the segment
326
+ * (and only until that client has seen the obliterate which removed their segment).
327
+ */
328
+ export function wasMovedOnInsert(segment: IInsertionInfo & ISegmentPrivate): boolean {
329
+ const moveInfo = toMoveInfo(segment);
330
+ const movedSeq = moveInfo?.movedSeq;
331
+ if (movedSeq === undefined || movedSeq === UnassignedSequenceNumber) {
332
+ return false;
333
+ }
334
+
335
+ const insertSeq = segment.seq;
336
+ return insertSeq === UnassignedSequenceNumber || insertSeq > movedSeq;
337
+ }
338
+
337
339
  /**
338
340
  * Asserts that the segment has move info. Usage of this function should not produce a user facing error.
339
341
  *
@@ -140,8 +140,6 @@ export class SnapshotLoader {
140
140
  movedClientIds: spec.movedClientIds.map((id) =>
141
141
  this.client.getOrAddShortClientId(id),
142
142
  ),
143
- // TODO:AB#29553: This property should be derived from segment data, not hard-coded.
144
- wasMovedOnInsert: false,
145
143
  });
146
144
  }
147
145