@fluidframework/tree 2.114.0 → 2.116.0-416006

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 (242) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/api-report/tree.alpha.api.md +108 -39
  3. package/dist/core/index.d.ts +1 -1
  4. package/dist/core/index.d.ts.map +1 -1
  5. package/dist/core/index.js +3 -2
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/rebase/index.d.ts +1 -1
  8. package/dist/core/rebase/index.d.ts.map +1 -1
  9. package/dist/core/rebase/index.js +2 -1
  10. package/dist/core/rebase/index.js.map +1 -1
  11. package/dist/core/rebase/types.d.ts +110 -0
  12. package/dist/core/rebase/types.d.ts.map +1 -1
  13. package/dist/core/rebase/types.js +47 -1
  14. package/dist/core/rebase/types.js.map +1 -1
  15. package/dist/entrypoints/alpha.d.ts +1 -1
  16. package/dist/entrypoints/alpha.d.ts.map +1 -1
  17. package/dist/entrypoints/alpha.js +6 -4
  18. package/dist/entrypoints/alpha.js.map +1 -1
  19. package/dist/feature-libraries/chunked-forest/codec/codecs.d.ts +9 -6
  20. package/dist/feature-libraries/chunked-forest/codec/codecs.d.ts.map +1 -1
  21. package/dist/feature-libraries/chunked-forest/codec/codecs.js +16 -13
  22. package/dist/feature-libraries/chunked-forest/codec/codecs.js.map +1 -1
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +6 -5
  26. package/dist/index.js.map +1 -1
  27. package/dist/packageVersion.d.ts +1 -1
  28. package/dist/packageVersion.d.ts.map +1 -1
  29. package/dist/packageVersion.js +1 -1
  30. package/dist/packageVersion.js.map +1 -1
  31. package/dist/shared-tree/sharedTree.d.ts +5 -0
  32. package/dist/shared-tree/sharedTree.d.ts.map +1 -1
  33. package/dist/shared-tree/sharedTree.js +1 -1
  34. package/dist/shared-tree/sharedTree.js.map +1 -1
  35. package/dist/shared-tree/treeCheckout.d.ts +5 -0
  36. package/dist/shared-tree/treeCheckout.d.ts.map +1 -1
  37. package/dist/shared-tree/treeCheckout.js +58 -12
  38. package/dist/shared-tree/treeCheckout.js.map +1 -1
  39. package/dist/shared-tree-core/branch.d.ts +1 -0
  40. package/dist/shared-tree-core/branch.d.ts.map +1 -1
  41. package/dist/shared-tree-core/branch.js.map +1 -1
  42. package/dist/shared-tree-core/sharedTreeCore.js +1 -1
  43. package/dist/shared-tree-core/sharedTreeCore.js.map +1 -1
  44. package/dist/simple-tree/api/index.d.ts +1 -1
  45. package/dist/simple-tree/api/index.d.ts.map +1 -1
  46. package/dist/simple-tree/api/index.js +2 -3
  47. package/dist/simple-tree/api/index.js.map +1 -1
  48. package/dist/simple-tree/api/schemaFactoryAlpha.d.ts +3 -3
  49. package/dist/simple-tree/api/schemaFactoryAlpha.d.ts.map +1 -1
  50. package/dist/simple-tree/api/schemaStatics.d.ts +6 -7
  51. package/dist/simple-tree/api/schemaStatics.d.ts.map +1 -1
  52. package/dist/simple-tree/api/schemaStatics.js.map +1 -1
  53. package/dist/simple-tree/api/treeNodeApi.d.ts +20 -24
  54. package/dist/simple-tree/api/treeNodeApi.d.ts.map +1 -1
  55. package/dist/simple-tree/api/treeNodeApi.js +48 -49
  56. package/dist/simple-tree/api/treeNodeApi.js.map +1 -1
  57. package/dist/simple-tree/core/treeNode.d.ts +5 -6
  58. package/dist/simple-tree/core/treeNode.d.ts.map +1 -1
  59. package/dist/simple-tree/core/treeNode.js +2 -4
  60. package/dist/simple-tree/core/treeNode.js.map +1 -1
  61. package/dist/simple-tree/core/withType.d.ts +20 -7
  62. package/dist/simple-tree/core/withType.d.ts.map +1 -1
  63. package/dist/simple-tree/core/withType.js +4 -7
  64. package/dist/simple-tree/core/withType.js.map +1 -1
  65. package/dist/simple-tree/index.d.ts +2 -2
  66. package/dist/simple-tree/index.d.ts.map +1 -1
  67. package/dist/simple-tree/index.js +3 -4
  68. package/dist/simple-tree/index.js.map +1 -1
  69. package/dist/simple-tree/leafNodeSchema.d.ts +6 -1
  70. package/dist/simple-tree/leafNodeSchema.d.ts.map +1 -1
  71. package/dist/simple-tree/leafNodeSchema.js +0 -1
  72. package/dist/simple-tree/leafNodeSchema.js.map +1 -1
  73. package/dist/simple-tree/node-kinds/array/arrayNode.d.ts +32 -3
  74. package/dist/simple-tree/node-kinds/array/arrayNode.d.ts.map +1 -1
  75. package/dist/simple-tree/node-kinds/array/arrayNode.js +91 -10
  76. package/dist/simple-tree/node-kinds/array/arrayNode.js.map +1 -1
  77. package/dist/simple-tree/node-kinds/map/mapNode.d.ts +77 -0
  78. package/dist/simple-tree/node-kinds/map/mapNode.d.ts.map +1 -1
  79. package/dist/simple-tree/node-kinds/map/mapNode.js +17 -1
  80. package/dist/simple-tree/node-kinds/map/mapNode.js.map +1 -1
  81. package/dist/simple-tree/node-kinds/object/objectNode.js +2 -2
  82. package/dist/simple-tree/node-kinds/object/objectNode.js.map +1 -1
  83. package/dist/simple-tree/node-kinds/record/recordNode.js +2 -2
  84. package/dist/simple-tree/node-kinds/record/recordNode.js.map +1 -1
  85. package/dist/text/codePointUtils.d.ts +2 -2
  86. package/dist/text/codePointUtils.js +2 -2
  87. package/dist/text/codePointUtils.js.map +1 -1
  88. package/dist/text/index.d.ts +3 -3
  89. package/dist/text/index.d.ts.map +1 -1
  90. package/dist/text/index.js +4 -4
  91. package/dist/text/index.js.map +1 -1
  92. package/dist/text/textDomain.d.ts +18 -18
  93. package/dist/text/textDomain.d.ts.map +1 -1
  94. package/dist/text/textDomain.js +8 -8
  95. package/dist/text/textDomain.js.map +1 -1
  96. package/dist/text/textDomainFormatted.d.ts +46 -42
  97. package/dist/text/textDomainFormatted.d.ts.map +1 -1
  98. package/dist/text/textDomainFormatted.js +32 -28
  99. package/dist/text/textDomainFormatted.js.map +1 -1
  100. package/dist/text/textDomainFormattedDefault.d.ts +18 -18
  101. package/dist/text/textDomainFormattedDefault.d.ts.map +1 -1
  102. package/dist/text/textDomainFormattedDefault.js +14 -17
  103. package/dist/text/textDomainFormattedDefault.js.map +1 -1
  104. package/dist/util/compressedIds.d.ts +22 -2
  105. package/dist/util/compressedIds.d.ts.map +1 -1
  106. package/dist/util/compressedIds.js +22 -1
  107. package/dist/util/compressedIds.js.map +1 -1
  108. package/lib/core/index.d.ts +1 -1
  109. package/lib/core/index.d.ts.map +1 -1
  110. package/lib/core/index.js +1 -1
  111. package/lib/core/index.js.map +1 -1
  112. package/lib/core/rebase/index.d.ts +1 -1
  113. package/lib/core/rebase/index.d.ts.map +1 -1
  114. package/lib/core/rebase/index.js +1 -1
  115. package/lib/core/rebase/index.js.map +1 -1
  116. package/lib/core/rebase/types.d.ts +110 -0
  117. package/lib/core/rebase/types.d.ts.map +1 -1
  118. package/lib/core/rebase/types.js +46 -0
  119. package/lib/core/rebase/types.js.map +1 -1
  120. package/lib/entrypoints/alpha.d.ts +1 -1
  121. package/lib/entrypoints/alpha.d.ts.map +1 -1
  122. package/lib/entrypoints/alpha.js +1 -1
  123. package/lib/entrypoints/alpha.js.map +1 -1
  124. package/lib/feature-libraries/chunked-forest/codec/codecs.d.ts +9 -6
  125. package/lib/feature-libraries/chunked-forest/codec/codecs.d.ts.map +1 -1
  126. package/lib/feature-libraries/chunked-forest/codec/codecs.js +16 -13
  127. package/lib/feature-libraries/chunked-forest/codec/codecs.js.map +1 -1
  128. package/lib/index.d.ts +3 -3
  129. package/lib/index.d.ts.map +1 -1
  130. package/lib/index.js +2 -2
  131. package/lib/index.js.map +1 -1
  132. package/lib/packageVersion.d.ts +1 -1
  133. package/lib/packageVersion.d.ts.map +1 -1
  134. package/lib/packageVersion.js +1 -1
  135. package/lib/packageVersion.js.map +1 -1
  136. package/lib/shared-tree/sharedTree.d.ts +5 -0
  137. package/lib/shared-tree/sharedTree.d.ts.map +1 -1
  138. package/lib/shared-tree/sharedTree.js +1 -1
  139. package/lib/shared-tree/sharedTree.js.map +1 -1
  140. package/lib/shared-tree/treeCheckout.d.ts +5 -0
  141. package/lib/shared-tree/treeCheckout.d.ts.map +1 -1
  142. package/lib/shared-tree/treeCheckout.js +60 -14
  143. package/lib/shared-tree/treeCheckout.js.map +1 -1
  144. package/lib/shared-tree-core/branch.d.ts +1 -0
  145. package/lib/shared-tree-core/branch.d.ts.map +1 -1
  146. package/lib/shared-tree-core/branch.js.map +1 -1
  147. package/lib/shared-tree-core/sharedTreeCore.js +1 -1
  148. package/lib/shared-tree-core/sharedTreeCore.js.map +1 -1
  149. package/lib/simple-tree/api/index.d.ts +1 -1
  150. package/lib/simple-tree/api/index.d.ts.map +1 -1
  151. package/lib/simple-tree/api/index.js +1 -1
  152. package/lib/simple-tree/api/index.js.map +1 -1
  153. package/lib/simple-tree/api/schemaFactoryAlpha.d.ts +3 -3
  154. package/lib/simple-tree/api/schemaFactoryAlpha.d.ts.map +1 -1
  155. package/lib/simple-tree/api/schemaStatics.d.ts +6 -7
  156. package/lib/simple-tree/api/schemaStatics.d.ts.map +1 -1
  157. package/lib/simple-tree/api/schemaStatics.js.map +1 -1
  158. package/lib/simple-tree/api/treeNodeApi.d.ts +20 -24
  159. package/lib/simple-tree/api/treeNodeApi.d.ts.map +1 -1
  160. package/lib/simple-tree/api/treeNodeApi.js +46 -46
  161. package/lib/simple-tree/api/treeNodeApi.js.map +1 -1
  162. package/lib/simple-tree/core/treeNode.d.ts +5 -6
  163. package/lib/simple-tree/core/treeNode.d.ts.map +1 -1
  164. package/lib/simple-tree/core/treeNode.js +2 -4
  165. package/lib/simple-tree/core/treeNode.js.map +1 -1
  166. package/lib/simple-tree/core/withType.d.ts +20 -7
  167. package/lib/simple-tree/core/withType.d.ts.map +1 -1
  168. package/lib/simple-tree/core/withType.js +4 -7
  169. package/lib/simple-tree/core/withType.js.map +1 -1
  170. package/lib/simple-tree/index.d.ts +2 -2
  171. package/lib/simple-tree/index.d.ts.map +1 -1
  172. package/lib/simple-tree/index.js +1 -1
  173. package/lib/simple-tree/index.js.map +1 -1
  174. package/lib/simple-tree/leafNodeSchema.d.ts +6 -1
  175. package/lib/simple-tree/leafNodeSchema.d.ts.map +1 -1
  176. package/lib/simple-tree/leafNodeSchema.js +0 -1
  177. package/lib/simple-tree/leafNodeSchema.js.map +1 -1
  178. package/lib/simple-tree/node-kinds/array/arrayNode.d.ts +32 -3
  179. package/lib/simple-tree/node-kinds/array/arrayNode.d.ts.map +1 -1
  180. package/lib/simple-tree/node-kinds/array/arrayNode.js +93 -12
  181. package/lib/simple-tree/node-kinds/array/arrayNode.js.map +1 -1
  182. package/lib/simple-tree/node-kinds/map/mapNode.d.ts +77 -0
  183. package/lib/simple-tree/node-kinds/map/mapNode.d.ts.map +1 -1
  184. package/lib/simple-tree/node-kinds/map/mapNode.js +18 -2
  185. package/lib/simple-tree/node-kinds/map/mapNode.js.map +1 -1
  186. package/lib/simple-tree/node-kinds/object/objectNode.js +3 -3
  187. package/lib/simple-tree/node-kinds/object/objectNode.js.map +1 -1
  188. package/lib/simple-tree/node-kinds/record/recordNode.js +3 -3
  189. package/lib/simple-tree/node-kinds/record/recordNode.js.map +1 -1
  190. package/lib/text/codePointUtils.d.ts +2 -2
  191. package/lib/text/codePointUtils.js +2 -2
  192. package/lib/text/codePointUtils.js.map +1 -1
  193. package/lib/text/index.d.ts +3 -3
  194. package/lib/text/index.d.ts.map +1 -1
  195. package/lib/text/index.js +3 -3
  196. package/lib/text/index.js.map +1 -1
  197. package/lib/text/textDomain.d.ts +18 -18
  198. package/lib/text/textDomain.d.ts.map +1 -1
  199. package/lib/text/textDomain.js +7 -7
  200. package/lib/text/textDomain.js.map +1 -1
  201. package/lib/text/textDomainFormatted.d.ts +46 -42
  202. package/lib/text/textDomainFormatted.d.ts.map +1 -1
  203. package/lib/text/textDomainFormatted.js +31 -27
  204. package/lib/text/textDomainFormatted.js.map +1 -1
  205. package/lib/text/textDomainFormattedDefault.d.ts +18 -18
  206. package/lib/text/textDomainFormattedDefault.d.ts.map +1 -1
  207. package/lib/text/textDomainFormattedDefault.js +14 -17
  208. package/lib/text/textDomainFormattedDefault.js.map +1 -1
  209. package/lib/util/compressedIds.d.ts +22 -2
  210. package/lib/util/compressedIds.d.ts.map +1 -1
  211. package/lib/util/compressedIds.js +22 -1
  212. package/lib/util/compressedIds.js.map +1 -1
  213. package/package.json +29 -22
  214. package/src/core/index.ts +2 -0
  215. package/src/core/rebase/index.ts +2 -0
  216. package/src/core/rebase/types.ts +113 -0
  217. package/src/entrypoints/alpha.ts +10 -6
  218. package/src/feature-libraries/chunked-forest/codec/codecs.ts +17 -14
  219. package/src/index.ts +7 -4
  220. package/src/packageVersion.ts +1 -1
  221. package/src/shared-tree/sharedTree.ts +6 -1
  222. package/src/shared-tree/treeCheckout.ts +81 -14
  223. package/src/shared-tree-core/branch.ts +1 -0
  224. package/src/shared-tree-core/sharedTreeCore.ts +1 -1
  225. package/src/simple-tree/api/index.ts +0 -1
  226. package/src/simple-tree/api/schemaStatics.ts +2 -2
  227. package/src/simple-tree/api/treeNodeApi.ts +64 -48
  228. package/src/simple-tree/core/treeNode.ts +13 -10
  229. package/src/simple-tree/core/withType.ts +20 -7
  230. package/src/simple-tree/index.ts +1 -2
  231. package/src/simple-tree/leafNodeSchema.ts +6 -1
  232. package/src/simple-tree/node-kinds/array/arrayNode.ts +109 -14
  233. package/src/simple-tree/node-kinds/map/mapNode.ts +113 -3
  234. package/src/simple-tree/node-kinds/object/objectNode.ts +3 -3
  235. package/src/simple-tree/node-kinds/record/recordNode.ts +3 -3
  236. package/src/text/README.md +1 -1
  237. package/src/text/codePointUtils.ts +2 -2
  238. package/src/text/index.ts +3 -3
  239. package/src/text/textDomain.ts +21 -21
  240. package/src/text/textDomainFormatted.ts +56 -52
  241. package/src/text/textDomainFormattedDefault.ts +14 -23
  242. package/src/util/compressedIds.ts +36 -3
@@ -3,10 +3,10 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { Lazy, oob, fail, assert } from "@fluidframework/core-utils/internal";
6
+ import { Lazy, oob, fail, assert, clamp } from "@fluidframework/core-utils/internal";
7
7
  import { UsageError } from "@fluidframework/telemetry-utils/internal";
8
8
 
9
- import { EmptyKey, ObjectNodeStoredSchema } from "../../../core/index.js";
9
+ import { EmptyKey, ObjectNodeStoredSchema, type DeltaMark } from "../../../core/index.js";
10
10
  import type {
11
11
  FlexibleFieldContent,
12
12
  FlexTreeNode,
@@ -23,7 +23,7 @@ import type { NodeSchemaOptionsAlpha, System_Unsafe } from "../../api/index.js";
23
23
  import {
24
24
  CompatibilityLevel,
25
25
  type WithType,
26
- // eslint-disable-next-line import-x/no-deprecated
26
+ // eslint-disable-next-line import-x/no-deprecated -- Required to implement the deprecated typeNameSymbol API.
27
27
  typeNameSymbol,
28
28
  NodeKind,
29
29
  type TreeNode,
@@ -1534,7 +1534,7 @@ export function arraySchema<
1534
1534
  public static readonly persistedMetadata: JsonCompatibleReadOnlyObject | undefined =
1535
1535
  persistedMetadata;
1536
1536
 
1537
- // eslint-disable-next-line import-x/no-deprecated
1537
+ // eslint-disable-next-line import-x/no-deprecated -- Required to implement the deprecated typeNameSymbol API.
1538
1538
  public get [typeNameSymbol](): TName {
1539
1539
  return identifier;
1540
1540
  }
@@ -1668,11 +1668,21 @@ export interface ArrayPlaceAnchor {
1668
1668
  * The current index within the array that this anchor refers to.
1669
1669
  * @remarks
1670
1670
  * This value is updated as the array is edited in a way that depends on the specific anchor implementation.
1671
- * This index may take on a value from 0 to the length of the array (inclusive).
1671
+ * This index is a value from 0 to the current length of the array (inclusive).
1672
1672
  * If used as the index to insert content into the array, this means it can point to any location in the array,
1673
1673
  * including just after the last child.
1674
+ * @throws A {@link @fluidframework/telemetry-utils#UsageError} if the anchor has been {@link ArrayPlaceAnchor.dispose | disposed}.
1674
1675
  */
1675
1676
  get index(): number;
1677
+
1678
+ /**
1679
+ * Stop tracking this anchor and release any resources it holds.
1680
+ * @remarks
1681
+ * Call this when the anchor is no longer needed (for example when a tracked cursor position is discarded).
1682
+ * Interacting with an anchor (including reading its properties) after it has been disposed is invalid and will throw.
1683
+ * Calling `dispose` more than once has no effect.
1684
+ */
1685
+ dispose(): void;
1676
1686
  }
1677
1687
 
1678
1688
  /**
@@ -1690,9 +1700,29 @@ export interface ArrayPlaceAnchor {
1690
1700
  * This is intended to track a location that might be used for an insertion point (for example in a text editor): future changes to its details should
1691
1701
  * make it behave better for such uses.
1692
1702
  *
1693
- * The current implementation is known to behave particularly poorly if the child which was at the original anchor point's index is removed
1694
- * (jumps to the end of the array): this behavior is subject to change.
1703
+ * In rare cases the tracked index cannot be updated precisely and the anchor falls back to best-effort behavior:
1704
+ * it keeps reporting a valid in-bounds index, but that index may no longer correspond to the same logical position
1705
+ * as before the change (this can happen when an incremental delta is unavailable for a change, such as during a
1706
+ * schema change). Consumers that require an accurate position across such changes should re-derive it from their
1707
+ * own state rather than relying solely on this anchor.
1695
1708
  * @privateRemarks
1709
+ * The index is maintained incrementally from the shallow (insert/remove) delta delivered by the array node's
1710
+ * `childrenChangedAfterBatch` event: inserts and removes before the anchor point shift it, while edits after it
1711
+ * (or a removal of the span it sits between) leave it in place. This keeps the anchor pinned to the gap between
1712
+ * children even when the child originally at its index is removed.
1713
+ *
1714
+ * The best-effort behavior noted in the public remarks is the no-delta path: when the composed delta is unavailable
1715
+ * (see {@link NodeChangedDataDelta.delta}), the exact shift is unknown so the index is only clamped back into range.
1716
+ * This is intentionally weak rather than reintroducing a per-edit O(n) snapshot of the array to diff old-vs-new
1717
+ * (which would defeat the point of delta-based tracking) for a case that is rare and, for this alpha API, acceptable
1718
+ * to degrade. A stronger fallback could instead let a consumer that already maintains the array contents re-seed the
1719
+ * anchor's position on this path, so it lives with the consumer that has the data instead of being duplicated here.
1720
+ *
1721
+ * TODO: The no-delta case above is one of several places whose behavior is limited by changes for which a composed
1722
+ * delta cannot be produced (see {@link NodeChangedDataDelta.delta}). The underlying eventing limitation should be
1723
+ * fixed so a delta is always available; when adding new code constrained by it, note it here (or track it centrally)
1724
+ * so the full set of affected call sites is known and can be prioritized.
1725
+ *
1696
1726
  * When stabilized, this should probably become a method on {@link (TreeArrayNode:interface)}.
1697
1727
  * Future versions of this should use rebaser / changeset logic to do a better job of tracking a location across removals or reinsertion.
1698
1728
  * How this would work, especially for unhydrated nodes is not yet clear.
@@ -1702,17 +1732,82 @@ export function createArrayInsertionAnchor(
1702
1732
  node: TreeArrayNode,
1703
1733
  currentIndex: number,
1704
1734
  ): ArrayPlaceAnchor {
1735
+ // An out-of-range or non-integer index is a usage error. An index equal to the array length is valid
1736
+ // (it points just past the last child).
1705
1737
  const field = getInnerNode(node).getBoxed(EmptyKey);
1706
- const child = field.boxedAt(currentIndex);
1738
+ validateIndex(currentIndex, field, "createArrayInsertionAnchor", /* allowOnePastEnd */ true);
1739
+ let trackedIndex = currentIndex;
1740
+
1741
+ const kernel = getKernel(node);
1742
+ let off: (() => void) | undefined = kernel.events.on(
1743
+ "childrenChangedAfterBatch",
1744
+ ({ fieldMarks }) => {
1745
+ const marks = fieldMarks.get(EmptyKey);
1746
+ // This event fires after the change is applied, so the field reflects the post-edit length.
1747
+ const length = getInnerNode(node).getBoxed(EmptyKey).length;
1748
+ if (marks === undefined) {
1749
+ // No-delta path: the composed delta is unavailable, so the exact shift is unknown. Keep the
1750
+ // index valid by clamping it to the current length (best-effort). See this function's
1751
+ // @privateRemarks for when this happens and why the fallback is intentionally weak.
1752
+ trackedIndex = clamp(trackedIndex, 0, length);
1753
+ return;
1754
+ }
1755
+ // Clamp defensively so a malformed or partial delta can never push the tracked index outside the
1756
+ // valid insertion range while the anchor is live (adjustIndexForArrayDelta has no final clamp).
1757
+ trackedIndex = clamp(adjustIndexForArrayDelta(trackedIndex, marks), 0, length);
1758
+ },
1759
+ );
1760
+
1707
1761
  return {
1708
1762
  get index() {
1709
- if (child === undefined) {
1710
- return field.length;
1711
- }
1712
- if (child.parentField.parent !== field) {
1713
- return field.length;
1763
+ if (off === undefined) {
1764
+ throw new UsageError("Cannot read the index of a disposed ArrayPlaceAnchor.");
1714
1765
  }
1715
- return child.parentField.index;
1766
+ return trackedIndex;
1767
+ },
1768
+ dispose() {
1769
+ off?.();
1770
+ off = undefined;
1716
1771
  },
1717
1772
  };
1718
1773
  }
1774
+
1775
+ /**
1776
+ * Compute the new index of an {@link ArrayPlaceAnchor} after applying a shallow array delta.
1777
+ * @remarks
1778
+ * The marks cover the array's sequence field in order. `readPosition` walks the pre-edit array as the marks are
1779
+ * consumed while `index` stays fixed at the anchor's pre-edit location; comparing the two (both in pre-edit
1780
+ * coordinates) shifts `newIndex` to keep the anchor in the same gap between children as content is inserted or
1781
+ * removed around it. Each mark is a removal (`detach`), an insertion (`attach`), or a retain (neither); a mark may
1782
+ * carry both `detach` and `attach`, which is handled as a removal followed by an insertion. The per-mark behavior is
1783
+ * commented inline below.
1784
+ */
1785
+ function adjustIndexForArrayDelta(index: number, marks: readonly DeltaMark[]): number {
1786
+ let readPosition = 0;
1787
+ let newIndex = index;
1788
+ for (const mark of marks) {
1789
+ // A removal consumes pre-edit content: if it lies entirely before the anchor it pulls the anchor left,
1790
+ // and if it contains the anchor's position the anchor collapses to the start of the removed span.
1791
+ if (mark.detach !== undefined) {
1792
+ const removeEnd = readPosition + mark.count;
1793
+ if (removeEnd <= index) {
1794
+ newIndex -= mark.count;
1795
+ } else if (readPosition < index) {
1796
+ newIndex -= index - readPosition;
1797
+ }
1798
+ readPosition += mark.count;
1799
+ }
1800
+ if (mark.attach !== undefined) {
1801
+ // Content inserted at or before the anchor pushes it right; inserting exactly at the anchor leaves the
1802
+ // anchor after the new content (insertion-point behavior). Inserted content is new, so it does not
1803
+ // advance the pre-edit read position.
1804
+ if (readPosition <= index) {
1805
+ newIndex += mark.count;
1806
+ }
1807
+ } else if (mark.detach === undefined) {
1808
+ // Retain: existing content that was neither inserted nor removed.
1809
+ readPosition += mark.count;
1810
+ }
1811
+ }
1812
+ return newIndex;
1813
+ }
@@ -22,14 +22,18 @@ import {
22
22
  type JsonCompatibleReadOnlyObject,
23
23
  type RestrictiveStringRecord,
24
24
  } from "../../../util/index.js";
25
- import type { NodeSchemaOptionsAlpha } from "../../api/index.js";
25
+ import type {
26
+ NodeSchemaOptionsAlpha,
27
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Referenced by TSDoc {@link} in this file.
28
+ RunTransactionParamsAlpha,
29
+ } from "../../api/index.js";
26
30
  import {
27
31
  CompatibilityLevel,
28
32
  getKernel,
29
33
  type InnerNode,
30
34
  NodeKind,
31
35
  type TreeNodeSchema,
32
- // eslint-disable-next-line import-x/no-deprecated
36
+ // eslint-disable-next-line import-x/no-deprecated -- Required to implement the deprecated typeNameSymbol API.
33
37
  typeNameSymbol,
34
38
  type TreeNode,
35
39
  typeSchemaSymbol,
@@ -180,6 +184,90 @@ export interface TreeMapNodeAlpha<T extends ImplicitAllowedTypes = ImplicitAllow
180
184
  * even if some of those elements were not yet in the map when the edit was authored.
181
185
  */
182
186
  clear(): void;
187
+ /**
188
+ * Returns the value at `key`, first inserting `fallbackValue` if this map has no entry for `key`.
189
+ *
190
+ * @param key - The key of the element to return or insert at.
191
+ * @param fallbackValue - The value to insert if `key` has no entry.
192
+ * @returns The value at `key`, which may be the provided `fallbackValue`
193
+ * if the map had no previous entry for `key`.
194
+ *
195
+ * @remarks
196
+ * The check for the presence of an existing entry with the given `key` is performed at the time the edit is authored.
197
+ * This has implications for the merge semantics of this operation:
198
+ *
199
+ * - If no entry is present for `key` at authoring time (thus leading to an insert)
200
+ * while a peer concurrently inserts a value for the same `key`,
201
+ * then the two inserts will race and the one that is sequenced second will overwrite the value inserted by the one
202
+ * that is sequenced first.
203
+ *
204
+ * - If an entry is present for `key` at authoring time (thus leading to no insert)
205
+ * while a peer concurrently deletes the entry for `key`,
206
+ * then the map will end up with no entry for `key` no matter how the two edits are sequenced.
207
+ *
208
+ * - If an entry is present for `key` at authoring time (thus leading to no insert)
209
+ * while a peer concurrently inserts a new entry for `key`,
210
+ * then the map will end up with the entry set by the peer no matter how the two edits are sequenced.
211
+ *
212
+ * These last two points mean that, upon sequencing of an edit made with this API,
213
+ * there is no guarantee that the entry for the given key will be the current one (if any) or the fallback one.
214
+ * If such a guarantee is important, then consider using {@link RunTransactionParamsAlpha.preconditions}
215
+ * to ensure the edit only applies when appropriate.
216
+ *
217
+ * This API is **not** equivalent to the following alternative:
218
+ *
219
+ * ```typescript
220
+ * map.set(key, map.get(key) ?? fallbackValue);
221
+ * ```
222
+ *
223
+ * They differ in the following ways:
224
+ *
225
+ * - This API treats entries containing `null` values as populated.
226
+ * By contrast, the above alternative's usage of `??` means that a `null` entry is treated as equivalent to a missing entry.
227
+ *
228
+ * - This API only inserts/sets the entry for the given `key` when there is no current one.
229
+ * By contrast, the above alternative always performs an insert.
230
+ * That insert will throw an error for non-leaf types (objects, maps, arrays, and records) in cases where the entry
231
+ * is already populated, because inserting a node that was already inserted is not supported.
232
+ * It will always succeed for leaf types (number, string, boolean, null, and handle) since a new node is created
233
+ * from the value.
234
+ * Note that even in the cases where it does not throw,
235
+ * the `set` operation has the potential to overwrite entries that are concurrently inserted even when the map had
236
+ * an entry for `key`, which is not the case with this API.
237
+ *
238
+ * Avoid using this API to write a default value into the document when the application could instead return that
239
+ * default when reading a missing entry, which is much less costly.
240
+ * A more appropriate use of this API is when the value to be optionally inserted varies with the key.
241
+ */
242
+ getOrInsert(
243
+ key: string,
244
+ fallbackValue: InsertableTreeNodeFromImplicitAllowedTypes<T>,
245
+ ): TreeNodeFromImplicitAllowedTypes<T>;
246
+
247
+ /**
248
+ * Returns the value at `key`, first inserting the value produced by `callback` if this map has no entry for `key`.
249
+ *
250
+ * @param key - The key of the element to return or insert at.
251
+ * @param callback - Invoked with `key` to produce the value to insert if `key` has no entry.
252
+ * Not invoked if an entry is present.
253
+ * @returns The value at `key`, which may be the value produced by `callback`
254
+ * if the map had no previous entry for `key`.
255
+ *
256
+ * @remarks
257
+ * This API is equivalent to {@link TreeMapNodeAlpha.getOrInsert} except that the fallback value is computed lazily:
258
+ * `callback` is only invoked when the map has no entry for `key`.
259
+ * Prefer this API over {@link TreeMapNodeAlpha.getOrInsert} when producing the fallback value is expensive.
260
+ *
261
+ * The check for the presence of an existing entry with the given `key` is performed at the time the edit is authored.
262
+ * See {@link TreeMapNodeAlpha.getOrInsert} for the implications this has on the merge semantics of this operation.
263
+ *
264
+ * If `callback` throws, no edit is made and the error is propagated to the caller.
265
+ * If `callback` sets an entry for `key` in this map, that entry is overwritten with the value `callback` returned.
266
+ */
267
+ getOrInsertComputed(
268
+ key: string,
269
+ callback: (key: string) => InsertableTreeNodeFromImplicitAllowedTypes<T>,
270
+ ): TreeNodeFromImplicitAllowedTypes<T>;
183
271
  }
184
272
 
185
273
  // TreeMapNode is invariant over schema type, so for this handler to work with all schema, the only possible type for the schema is `any`.
@@ -261,6 +349,28 @@ abstract class CustomMapNodeBase<const T extends ImplicitAllowedTypes> extends T
261
349
  this.editor(key).set(mapTree, field.length === 0);
262
350
  return this;
263
351
  }
352
+ public getOrInsert(
353
+ key: string,
354
+ fallbackValue: InsertableTreeNodeFromImplicitAllowedTypes<T>,
355
+ ): TreeNodeFromImplicitAllowedTypes<T> {
356
+ const existing = this.get(key);
357
+ if (existing !== undefined) {
358
+ return existing;
359
+ }
360
+ this.set(key, fallbackValue);
361
+ return this.get(key);
362
+ }
363
+ public getOrInsertComputed(
364
+ key: string,
365
+ callback: (key: string) => InsertableTreeNodeFromImplicitAllowedTypes<T>,
366
+ ): TreeNodeFromImplicitAllowedTypes<T> {
367
+ const existing = this.get(key);
368
+ if (existing !== undefined) {
369
+ return existing;
370
+ }
371
+ this.set(key, callback(key));
372
+ return this.get(key);
373
+ }
264
374
  public get size(): number {
265
375
  return count(this.innerNode.keys());
266
376
  }
@@ -375,7 +485,7 @@ export function mapSchema<
375
485
  public static readonly persistedMetadata: JsonCompatibleReadOnlyObject | undefined =
376
486
  persistedMetadata;
377
487
 
378
- // eslint-disable-next-line import-x/no-deprecated
488
+ // eslint-disable-next-line import-x/no-deprecated -- Required to implement the deprecated typeNameSymbol API.
379
489
  public get [typeNameSymbol](): TName {
380
490
  return identifier;
381
491
  }
@@ -32,7 +32,7 @@ import {
32
32
  type TreeNodeSchema,
33
33
  NodeKind,
34
34
  type WithType,
35
- // eslint-disable-next-line import-x/no-deprecated
35
+ // eslint-disable-next-line import-x/no-deprecated -- Required to handle the deprecated typeNameSymbol API.
36
36
  typeNameSymbol,
37
37
  typeSchemaSymbol,
38
38
  type InternalTreeNode,
@@ -335,7 +335,7 @@ function createProxyHandler(
335
335
  if (propertyKey === typeSchemaSymbol) {
336
336
  return schema;
337
337
  }
338
- // eslint-disable-next-line import-x/no-deprecated
338
+ // eslint-disable-next-line import-x/no-deprecated -- Required to handle the deprecated typeNameSymbol API.
339
339
  if (propertyKey === typeNameSymbol) {
340
340
  return schema.identifier;
341
341
  }
@@ -633,7 +633,7 @@ export function objectSchema<
633
633
  public static readonly persistedMetadata: JsonCompatibleReadOnlyObject | undefined =
634
634
  nodeOptions.persistedMetadata;
635
635
 
636
- // eslint-disable-next-line import-x/no-deprecated
636
+ // eslint-disable-next-line import-x/no-deprecated -- Required to implement the deprecated typeNameSymbol API.
637
637
  public get [typeNameSymbol](): TName {
638
638
  return identifier;
639
639
  }
@@ -17,7 +17,7 @@ import type { NodeSchemaOptionsAlpha } from "../../api/index.js";
17
17
  import {
18
18
  type TreeNodeSchema,
19
19
  NodeKind,
20
- // eslint-disable-next-line import-x/no-deprecated
20
+ // eslint-disable-next-line import-x/no-deprecated -- Required to handle the deprecated typeNameSymbol API.
21
21
  typeNameSymbol,
22
22
  typeSchemaSymbol,
23
23
  type UnhydratedFlexTreeNode,
@@ -78,7 +78,7 @@ function createRecordNodeProxy(
78
78
  case typeSchemaSymbol: {
79
79
  return schema;
80
80
  }
81
- // eslint-disable-next-line import-x/no-deprecated
81
+ // eslint-disable-next-line import-x/no-deprecated -- Required to handle the deprecated typeNameSymbol API.
82
82
  case typeNameSymbol: {
83
83
  return schema.identifier;
84
84
  }
@@ -370,7 +370,7 @@ export function recordSchema<
370
370
  public static readonly persistedMetadata: JsonCompatibleReadOnlyObject | undefined =
371
371
  persistedMetadata;
372
372
 
373
- // eslint-disable-next-line import-x/no-deprecated
373
+ // eslint-disable-next-line import-x/no-deprecated -- Required to implement the deprecated typeNameSymbol API.
374
374
  public get [typeNameSymbol](): TName {
375
375
  return identifier;
376
376
  }
@@ -5,4 +5,4 @@ A collection of text related types, schema and utilities for working with text b
5
5
  ## Status
6
6
 
7
7
  Very early work in progress.
8
- See doc comment on `TextAsTree` for more details.
8
+ See doc comment on `PlainText` for more details.
@@ -9,7 +9,7 @@ import { UsageError } from "@fluidframework/telemetry-utils/internal";
9
9
  * Returns the number of Unicode code points in `value`.
10
10
  * @remarks
11
11
  * Use this to translate a JavaScript string length (which is in UTF-16 code units) into
12
- * the atom/code-point space used by {@link TextAsTree}.
12
+ * the atom/code-point space used by {@link PlainText} and {@link FormattedText}.
13
13
  *
14
14
  * @example
15
15
  * ```typescript
@@ -33,7 +33,7 @@ export function codePointCount(value: string): number {
33
33
  * Returns the number of UTF-16 code units occupied by the first `count` Unicode code points in `value`,
34
34
  * starting at UTF-16 index `start`.
35
35
  * @remarks
36
- * Use this to translate {@link TextAsTree}-space counts (code points) into JavaScript string indices (UTF-16).
36
+ * Use this to translate {@link PlainText}-space counts (code points) into JavaScript string indices (UTF-16).
37
37
  * One code point outside the Basic Multilingual Plane (e.g. most emoji) occupies two UTF-16 code units.
38
38
  *
39
39
  * Validates that the requested `count` code points are fully consumable from `start`; silent truncation
package/src/text/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- export { TextAsTree } from "./textDomain.js";
7
- export { FormattedTextAsTree } from "./textDomainFormatted.js";
8
- export { FormattedTextAsTreeDefault } from "./textDomainFormattedDefault.js";
6
+ export { PlainText } from "./textDomain.js";
7
+ export { FormattedText } from "./textDomainFormatted.js";
8
+ export { FormattedTextDefault } from "./textDomainFormattedDefault.js";
9
9
  export { codePointCount, utf16LengthForCodePoints } from "./codePointUtils.js";
@@ -63,7 +63,7 @@ class TextNode
63
63
  extends sf.object("Text", {
64
64
  content: SchemaFactory.required([() => StringArray], { key: EmptyKey }),
65
65
  })
66
- implements TextAsTree.Members, IExposedMethods
66
+ implements PlainText.Members, IExposedMethods
67
67
  {
68
68
  public static [exposeMethodsSymbol](methods: ExposedMethods): void {
69
69
  methods.exposeMethod(
@@ -174,7 +174,7 @@ class TextNode
174
174
  }
175
175
 
176
176
  public onCharactersChanged(
177
- callback: (ops: readonly TextAsTree.TextOp[] | undefined) => void,
177
+ callback: (ops: readonly PlainText.TextOp[] | undefined) => void,
178
178
  ): () => void {
179
179
  return TreeAlpha.on(this.content, "nodeChanged", ({ delta }) =>
180
180
  processCharactersChangedDelta(delta, (i) => this.content[i], callback),
@@ -224,7 +224,7 @@ class StringArray extends sf.array("StringArray", SchemaFactory.string) {
224
224
  }
225
225
 
226
226
  /**
227
- * Processes an array-node delta into a {@link TextAsTree.TextOp}[] and calls `callback`.
227
+ * Processes an array-node delta into a {@link PlainText.TextOp}[] and calls `callback`.
228
228
  * @remarks
229
229
  * Shared by both the plain `onCharactersChanged` (from `nodeChanged`) and formatted `onContentChanged`
230
230
  * (from `treeChanged`) implementations.
@@ -240,14 +240,14 @@ class StringArray extends sf.array("StringArray", SchemaFactory.string) {
240
240
  export function processCharactersChangedDelta(
241
241
  delta: readonly (ArrayNodeDeltaOp | ArrayNodeTreeChangedDeltaOp)[] | undefined,
242
242
  getCharacter: (index: number) => string | undefined,
243
- callback: (ops: readonly TextAsTree.TextOp[] | undefined) => void,
243
+ callback: (ops: readonly PlainText.TextOp[] | undefined) => void,
244
244
  ): void {
245
245
  if (delta === undefined) {
246
246
  callback(undefined);
247
247
  return;
248
248
  }
249
249
  let readPosition = 0;
250
- const ops: TextAsTree.TextOp[] = [];
250
+ const ops: PlainText.TextOp[] = [];
251
251
  for (const op of delta) {
252
252
  if (op.type === "retain") {
253
253
  // `subtreeChanged` is only present on retain ops from `treeChanged` deltas.
@@ -342,7 +342,7 @@ export function processCharactersChangedDelta(
342
342
  * in addition to implementing them for text.
343
343
  * @alpha
344
344
  */
345
- export namespace TextAsTree {
345
+ export namespace PlainText {
346
346
  /**
347
347
  * A retain op in a character-level delta — a span of unchanged characters that the consumer should skip over.
348
348
  * @sealed
@@ -360,8 +360,8 @@ export namespace TextAsTree {
360
360
  /**
361
361
  * Whether at least one character in the retained range had a deep change.
362
362
  * @remarks
363
- * Present only on retain ops delivered by {@link @fluidframework/tree#FormattedTextAsTree.Members.onContentChanged};
364
- * always absent on retain ops delivered by {@link TextAsTree.Members.onCharactersChanged}.
363
+ * Present only on retain ops delivered by {@link @fluidframework/tree#FormattedText.Members.onContentChanged};
364
+ * always absent on retain ops delivered by {@link PlainText.Members.onCharactersChanged}.
365
365
  * When present, `true` indicates the retained range contained a formatting property update
366
366
  * or an atom content edit; `false` indicates no deep change.
367
367
  */
@@ -414,7 +414,7 @@ export namespace TextAsTree {
414
414
  */
415
415
  export interface Statics {
416
416
  /**
417
- * Construct a {@link TextAsTree.(Tree:type)} from a string, where each character (as defined by iterating over the string) becomes a single character in the text node.
417
+ * Construct a {@link PlainText.(Tree:type)} from a string, where each character (as defined by iterating over the string) becomes a single character in the text node.
418
418
  * This combines pairs of utf-16 surrogate code units into single characters as appropriate.
419
419
  */
420
420
  fromString(value: string): Tree;
@@ -433,8 +433,8 @@ export namespace TextAsTree {
433
433
  * (which often operates on something in between unicode code points and grapheme clusters)
434
434
  * and navigation/selection (which typically uses grapheme clusters).
435
435
  *
436
- * @see {@link TextAsTree.Statics.fromString} for construction.
437
- * @see {@link TextAsTree.(Tree:type)} for schema.
436
+ * @see {@link PlainText.Statics.fromString} for construction.
437
+ * @see {@link PlainText.(Tree:type)} for schema.
438
438
  * @sealed
439
439
  * @alpha
440
440
  */
@@ -447,15 +447,15 @@ export namespace TextAsTree {
447
447
  characters(): Iterable<string>;
448
448
 
449
449
  /**
450
- * Optimized way to get a copy of the {@link TextAsTree.Members.characters} in an array.
450
+ * Optimized way to get a copy of the {@link PlainText.Members.characters} in an array.
451
451
  */
452
452
  charactersCopy(): string[];
453
453
 
454
454
  /**
455
455
  * Gets the number of characters currently in the text.
456
456
  * @remarks
457
- * The length of {@link TextAsTree.Members.characters}.
458
- * This is not the length of the string returned by {@link TextAsTree.Members.fullString},
457
+ * The length of {@link PlainText.Members.characters}.
458
+ * This is not the length of the string returned by {@link PlainText.Members.fullString},
459
459
  * as that string may contain characters which are made up of multiple UTF-16 code units.
460
460
  */
461
461
  characterCount(): number;
@@ -469,7 +469,7 @@ export namespace TextAsTree {
469
469
  * Insert a range of characters into the string based on character index.
470
470
  * @remarks
471
471
  * See {@link (TreeArrayNode:interface).insertAt} for more details on the behavior.
472
- * See {@link TextAsTree.Statics.fromString} for how the `additionalCharacters` string is broken into characters.
472
+ * See {@link PlainText.Statics.fromString} for how the `additionalCharacters` string is broken into characters.
473
473
  * @privateRemarks
474
474
  * If we provide ways to customize character boundaries, that could be handled here by taking in an Iterable<string> instead of a string.
475
475
  * Doing this currently would enable insertion of text with different character boundaries than the existing text,
@@ -488,13 +488,13 @@ export namespace TextAsTree {
488
488
 
489
489
  /**
490
490
  * Subscribe to shallow character-level changes on this text node — inserts and removes only.
491
- * @param callback - Called after each change with a sequence of {@link TextAsTree.TextOp}s describing what changed,
491
+ * @param callback - Called after each change with a sequence of {@link PlainText.TextOp}s describing what changed,
492
492
  * or `undefined` when a delta could not be computed (e.g. during a schema upgrade).
493
493
  * @returns A cleanup function that unsubscribes the callback when called.
494
494
  * @remarks
495
495
  * Only fires on shallow changes — inserts and removes.
496
496
  * It does not fire on deep changes such as formatting property updates on existing characters.
497
- * For formatted text, use {@link @fluidframework/tree#FormattedTextAsTree.Members.onContentChanged} to also receive deep changes.
497
+ * For formatted text, use {@link @fluidframework/tree#FormattedText.Members.onContentChanged} to also receive deep changes.
498
498
  *
499
499
  * All counts in the delivered ops are in Unicode code points, not UTF-16 code units.
500
500
  * For characters outside the Basic Multilingual Plane (e.g. emoji), one code point
@@ -504,17 +504,17 @@ export namespace TextAsTree {
504
504
  }
505
505
 
506
506
  /**
507
- * Schema for a {@link TextAsTree.(Tree:variable)} node.
507
+ * Schema for a {@link PlainText.(Tree:variable)} node.
508
508
  * @remarks
509
- * See {@link TextAsTree.Statics} for static APIs on this schema, including construction.
509
+ * See {@link PlainText.Statics} for static APIs on this schema, including construction.
510
510
  * @alpha
511
511
  */
512
512
  export const Tree = eraseSchemaDetails<Members, Statics>()(TextNode);
513
513
 
514
514
  /**
515
- * Node for the {@link TextAsTree.(Tree:type)} schema exposing the {@link TextAsTree.Members} API.
515
+ * Node for the {@link PlainText.(Tree:type)} schema exposing the {@link PlainText.Members} API.
516
516
  * @remarks
517
- * Create using {@link TextAsTree.Statics.fromString}.
517
+ * Create using {@link PlainText.Statics.fromString}.
518
518
  * @alpha
519
519
  */
520
520
  export type Tree = Members & TreeNode & WithType<"com.fluidframework.text.Text">;