@fluidframework/tree 2.93.0 → 2.101.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 (273) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/INCREMENTAL_SUMMARY.md +89 -0
  3. package/README.md +6 -0
  4. package/api-report/tree.alpha.api.md +6 -1
  5. package/api-report/tree.beta.api.md +3 -1
  6. package/api-report/tree.legacy.beta.api.md +3 -1
  7. package/dist/core/change-family/changeFamily.d.ts +23 -0
  8. package/dist/core/change-family/changeFamily.d.ts.map +1 -1
  9. package/dist/core/change-family/changeFamily.js.map +1 -1
  10. package/dist/core/tree/detachedFieldIndexCodecV1.d.ts.map +1 -1
  11. package/dist/core/tree/detachedFieldIndexCodecV1.js +2 -0
  12. package/dist/core/tree/detachedFieldIndexCodecV1.js.map +1 -1
  13. package/dist/core/tree/detachedFieldIndexCodecV2.d.ts.map +1 -1
  14. package/dist/core/tree/detachedFieldIndexCodecV2.js +2 -0
  15. package/dist/core/tree/detachedFieldIndexCodecV2.js.map +1 -1
  16. package/dist/feature-libraries/chunked-forest/basicChunk.d.ts +25 -1
  17. package/dist/feature-libraries/chunked-forest/basicChunk.d.ts.map +1 -1
  18. package/dist/feature-libraries/chunked-forest/basicChunk.js +71 -18
  19. package/dist/feature-libraries/chunked-forest/basicChunk.js.map +1 -1
  20. package/dist/feature-libraries/chunked-forest/codec/chunkDecoding.d.ts +13 -4
  21. package/dist/feature-libraries/chunked-forest/codec/chunkDecoding.d.ts.map +1 -1
  22. package/dist/feature-libraries/chunked-forest/codec/chunkDecoding.js +31 -4
  23. package/dist/feature-libraries/chunked-forest/codec/chunkDecoding.js.map +1 -1
  24. package/dist/feature-libraries/chunked-forest/codec/codecs.d.ts +27 -0
  25. package/dist/feature-libraries/chunked-forest/codec/codecs.d.ts.map +1 -1
  26. package/dist/feature-libraries/chunked-forest/codec/codecs.js +5 -2
  27. package/dist/feature-libraries/chunked-forest/codec/codecs.js.map +1 -1
  28. package/dist/feature-libraries/chunked-forest/codec/compressedEncode.d.ts +10 -2
  29. package/dist/feature-libraries/chunked-forest/codec/compressedEncode.d.ts.map +1 -1
  30. package/dist/feature-libraries/chunked-forest/codec/compressedEncode.js +7 -2
  31. package/dist/feature-libraries/chunked-forest/codec/compressedEncode.js.map +1 -1
  32. package/dist/feature-libraries/chunked-forest/codec/format/formatGeneric.d.ts +1 -2
  33. package/dist/feature-libraries/chunked-forest/codec/format/formatGeneric.d.ts.map +1 -1
  34. package/dist/feature-libraries/chunked-forest/codec/format/formatGeneric.js +0 -1
  35. package/dist/feature-libraries/chunked-forest/codec/format/formatGeneric.js.map +1 -1
  36. package/dist/feature-libraries/chunked-forest/codec/format/index.d.ts +1 -1
  37. package/dist/feature-libraries/chunked-forest/codec/format/index.d.ts.map +1 -1
  38. package/dist/feature-libraries/chunked-forest/codec/format/index.js +2 -1
  39. package/dist/feature-libraries/chunked-forest/codec/format/index.js.map +1 -1
  40. package/dist/feature-libraries/chunked-forest/codec/format/versions.d.ts +10 -2
  41. package/dist/feature-libraries/chunked-forest/codec/format/versions.d.ts.map +1 -1
  42. package/dist/feature-libraries/chunked-forest/codec/format/versions.js +15 -1
  43. package/dist/feature-libraries/chunked-forest/codec/format/versions.js.map +1 -1
  44. package/dist/feature-libraries/chunked-forest/codec/nodeEncoder.d.ts.map +1 -1
  45. package/dist/feature-libraries/chunked-forest/codec/nodeEncoder.js +9 -1
  46. package/dist/feature-libraries/chunked-forest/codec/nodeEncoder.js.map +1 -1
  47. package/dist/feature-libraries/chunked-forest/codec/schemaBasedEncode.d.ts +3 -3
  48. package/dist/feature-libraries/chunked-forest/codec/schemaBasedEncode.d.ts.map +1 -1
  49. package/dist/feature-libraries/chunked-forest/codec/schemaBasedEncode.js +8 -8
  50. package/dist/feature-libraries/chunked-forest/codec/schemaBasedEncode.js.map +1 -1
  51. package/dist/feature-libraries/chunked-forest/uniformChunk.d.ts +6 -1
  52. package/dist/feature-libraries/chunked-forest/uniformChunk.d.ts.map +1 -1
  53. package/dist/feature-libraries/chunked-forest/uniformChunk.js +25 -1
  54. package/dist/feature-libraries/chunked-forest/uniformChunk.js.map +1 -1
  55. package/dist/feature-libraries/forest-summary/incrementalSummaryBuilder.d.ts +19 -0
  56. package/dist/feature-libraries/forest-summary/incrementalSummaryBuilder.d.ts.map +1 -1
  57. package/dist/feature-libraries/forest-summary/incrementalSummaryBuilder.js +76 -22
  58. package/dist/feature-libraries/forest-summary/incrementalSummaryBuilder.js.map +1 -1
  59. package/dist/feature-libraries/modular-schema/genericFieldKindFormat.d.ts +2 -14
  60. package/dist/feature-libraries/modular-schema/genericFieldKindFormat.d.ts.map +1 -1
  61. package/dist/feature-libraries/modular-schema/genericFieldKindFormat.js +1 -17
  62. package/dist/feature-libraries/modular-schema/genericFieldKindFormat.js.map +1 -1
  63. package/dist/feature-libraries/modular-schema/modularChangeCodecV1.d.ts.map +1 -1
  64. package/dist/feature-libraries/modular-schema/modularChangeCodecV1.js +4 -0
  65. package/dist/feature-libraries/modular-schema/modularChangeCodecV1.js.map +1 -1
  66. package/dist/packageVersion.d.ts +1 -1
  67. package/dist/packageVersion.d.ts.map +1 -1
  68. package/dist/packageVersion.js +1 -1
  69. package/dist/packageVersion.js.map +1 -1
  70. package/dist/shared-tree/independentView.d.ts.map +1 -1
  71. package/dist/shared-tree/independentView.js +2 -0
  72. package/dist/shared-tree/independentView.js.map +1 -1
  73. package/dist/shared-tree/sharedTree.d.ts +35 -1
  74. package/dist/shared-tree/sharedTree.d.ts.map +1 -1
  75. package/dist/shared-tree/sharedTree.js +6 -0
  76. package/dist/shared-tree/sharedTree.js.map +1 -1
  77. package/dist/shared-tree/sharedTreeChangeCodecs.js +1 -0
  78. package/dist/shared-tree/sharedTreeChangeCodecs.js.map +1 -1
  79. package/dist/shared-tree/treeAlpha.d.ts.map +1 -1
  80. package/dist/shared-tree/treeAlpha.js +2 -0
  81. package/dist/shared-tree/treeAlpha.js.map +1 -1
  82. package/dist/shared-tree/treeCheckout.d.ts +1 -1
  83. package/dist/shared-tree/treeCheckout.d.ts.map +1 -1
  84. package/dist/shared-tree/treeCheckout.js +2 -0
  85. package/dist/shared-tree/treeCheckout.js.map +1 -1
  86. package/dist/shared-tree-core/editManagerCodecs.d.ts +3 -0
  87. package/dist/shared-tree-core/editManagerCodecs.d.ts.map +1 -1
  88. package/dist/shared-tree-core/editManagerCodecs.js.map +1 -1
  89. package/dist/shared-tree-core/editManagerCodecsCommons.d.ts +14 -0
  90. package/dist/shared-tree-core/editManagerCodecsCommons.d.ts.map +1 -1
  91. package/dist/shared-tree-core/editManagerCodecsCommons.js +14 -0
  92. package/dist/shared-tree-core/editManagerCodecsCommons.js.map +1 -1
  93. package/dist/shared-tree-core/editManagerCodecsV1toV4.d.ts +3 -0
  94. package/dist/shared-tree-core/editManagerCodecsV1toV4.d.ts.map +1 -1
  95. package/dist/shared-tree-core/editManagerCodecsV1toV4.js.map +1 -1
  96. package/dist/shared-tree-core/editManagerCodecsVSharedBranches.d.ts +3 -0
  97. package/dist/shared-tree-core/editManagerCodecsVSharedBranches.d.ts.map +1 -1
  98. package/dist/shared-tree-core/editManagerCodecsVSharedBranches.js.map +1 -1
  99. package/dist/shared-tree-core/editManagerSummarizer.d.ts +9 -1
  100. package/dist/shared-tree-core/editManagerSummarizer.d.ts.map +1 -1
  101. package/dist/shared-tree-core/editManagerSummarizer.js +20 -5
  102. package/dist/shared-tree-core/editManagerSummarizer.js.map +1 -1
  103. package/dist/shared-tree-core/messageCodecV1ToV4.d.ts.map +1 -1
  104. package/dist/shared-tree-core/messageCodecV1ToV4.js +4 -0
  105. package/dist/shared-tree-core/messageCodecV1ToV4.js.map +1 -1
  106. package/dist/shared-tree-core/messageCodecVSharedBranches.d.ts.map +1 -1
  107. package/dist/shared-tree-core/messageCodecVSharedBranches.js +4 -0
  108. package/dist/shared-tree-core/messageCodecVSharedBranches.js.map +1 -1
  109. package/dist/shared-tree-core/sharedTreeCore.d.ts +4 -0
  110. package/dist/shared-tree-core/sharedTreeCore.d.ts.map +1 -1
  111. package/dist/shared-tree-core/sharedTreeCore.js +1 -1
  112. package/dist/shared-tree-core/sharedTreeCore.js.map +1 -1
  113. package/dist/simple-tree/api/schemaFactory.d.ts +3 -3
  114. package/dist/simple-tree/api/schemaFactory.js +3 -3
  115. package/dist/simple-tree/api/schemaFactory.js.map +1 -1
  116. package/dist/simple-tree/api/schemaFactoryAlpha.d.ts +17 -1
  117. package/dist/simple-tree/api/schemaFactoryAlpha.d.ts.map +1 -1
  118. package/dist/simple-tree/api/schemaFactoryAlpha.js +9 -0
  119. package/dist/simple-tree/api/schemaFactoryAlpha.js.map +1 -1
  120. package/dist/tableSchema.d.ts.map +1 -1
  121. package/dist/tableSchema.js +102 -20
  122. package/dist/tableSchema.js.map +1 -1
  123. package/docs/user-facing/isolated-declarations.md +147 -0
  124. package/lib/core/change-family/changeFamily.d.ts +23 -0
  125. package/lib/core/change-family/changeFamily.d.ts.map +1 -1
  126. package/lib/core/change-family/changeFamily.js.map +1 -1
  127. package/lib/core/tree/detachedFieldIndexCodecV1.d.ts.map +1 -1
  128. package/lib/core/tree/detachedFieldIndexCodecV1.js +2 -0
  129. package/lib/core/tree/detachedFieldIndexCodecV1.js.map +1 -1
  130. package/lib/core/tree/detachedFieldIndexCodecV2.d.ts.map +1 -1
  131. package/lib/core/tree/detachedFieldIndexCodecV2.js +2 -0
  132. package/lib/core/tree/detachedFieldIndexCodecV2.js.map +1 -1
  133. package/lib/feature-libraries/chunked-forest/basicChunk.d.ts +25 -1
  134. package/lib/feature-libraries/chunked-forest/basicChunk.d.ts.map +1 -1
  135. package/lib/feature-libraries/chunked-forest/basicChunk.js +72 -19
  136. package/lib/feature-libraries/chunked-forest/basicChunk.js.map +1 -1
  137. package/lib/feature-libraries/chunked-forest/codec/chunkDecoding.d.ts +13 -4
  138. package/lib/feature-libraries/chunked-forest/codec/chunkDecoding.d.ts.map +1 -1
  139. package/lib/feature-libraries/chunked-forest/codec/chunkDecoding.js +32 -5
  140. package/lib/feature-libraries/chunked-forest/codec/chunkDecoding.js.map +1 -1
  141. package/lib/feature-libraries/chunked-forest/codec/codecs.d.ts +27 -0
  142. package/lib/feature-libraries/chunked-forest/codec/codecs.d.ts.map +1 -1
  143. package/lib/feature-libraries/chunked-forest/codec/codecs.js +6 -3
  144. package/lib/feature-libraries/chunked-forest/codec/codecs.js.map +1 -1
  145. package/lib/feature-libraries/chunked-forest/codec/compressedEncode.d.ts +10 -2
  146. package/lib/feature-libraries/chunked-forest/codec/compressedEncode.d.ts.map +1 -1
  147. package/lib/feature-libraries/chunked-forest/codec/compressedEncode.js +8 -3
  148. package/lib/feature-libraries/chunked-forest/codec/compressedEncode.js.map +1 -1
  149. package/lib/feature-libraries/chunked-forest/codec/format/formatGeneric.d.ts +1 -2
  150. package/lib/feature-libraries/chunked-forest/codec/format/formatGeneric.d.ts.map +1 -1
  151. package/lib/feature-libraries/chunked-forest/codec/format/formatGeneric.js +0 -1
  152. package/lib/feature-libraries/chunked-forest/codec/format/formatGeneric.js.map +1 -1
  153. package/lib/feature-libraries/chunked-forest/codec/format/index.d.ts +1 -1
  154. package/lib/feature-libraries/chunked-forest/codec/format/index.d.ts.map +1 -1
  155. package/lib/feature-libraries/chunked-forest/codec/format/index.js +1 -1
  156. package/lib/feature-libraries/chunked-forest/codec/format/index.js.map +1 -1
  157. package/lib/feature-libraries/chunked-forest/codec/format/versions.d.ts +10 -2
  158. package/lib/feature-libraries/chunked-forest/codec/format/versions.d.ts.map +1 -1
  159. package/lib/feature-libraries/chunked-forest/codec/format/versions.js +13 -0
  160. package/lib/feature-libraries/chunked-forest/codec/format/versions.js.map +1 -1
  161. package/lib/feature-libraries/chunked-forest/codec/nodeEncoder.d.ts.map +1 -1
  162. package/lib/feature-libraries/chunked-forest/codec/nodeEncoder.js +9 -1
  163. package/lib/feature-libraries/chunked-forest/codec/nodeEncoder.js.map +1 -1
  164. package/lib/feature-libraries/chunked-forest/codec/schemaBasedEncode.d.ts +3 -3
  165. package/lib/feature-libraries/chunked-forest/codec/schemaBasedEncode.d.ts.map +1 -1
  166. package/lib/feature-libraries/chunked-forest/codec/schemaBasedEncode.js +8 -8
  167. package/lib/feature-libraries/chunked-forest/codec/schemaBasedEncode.js.map +1 -1
  168. package/lib/feature-libraries/chunked-forest/uniformChunk.d.ts +6 -1
  169. package/lib/feature-libraries/chunked-forest/uniformChunk.d.ts.map +1 -1
  170. package/lib/feature-libraries/chunked-forest/uniformChunk.js +26 -2
  171. package/lib/feature-libraries/chunked-forest/uniformChunk.js.map +1 -1
  172. package/lib/feature-libraries/forest-summary/incrementalSummaryBuilder.d.ts +19 -0
  173. package/lib/feature-libraries/forest-summary/incrementalSummaryBuilder.d.ts.map +1 -1
  174. package/lib/feature-libraries/forest-summary/incrementalSummaryBuilder.js +76 -22
  175. package/lib/feature-libraries/forest-summary/incrementalSummaryBuilder.js.map +1 -1
  176. package/lib/feature-libraries/modular-schema/genericFieldKindFormat.d.ts +2 -14
  177. package/lib/feature-libraries/modular-schema/genericFieldKindFormat.d.ts.map +1 -1
  178. package/lib/feature-libraries/modular-schema/genericFieldKindFormat.js +1 -17
  179. package/lib/feature-libraries/modular-schema/genericFieldKindFormat.js.map +1 -1
  180. package/lib/feature-libraries/modular-schema/modularChangeCodecV1.d.ts.map +1 -1
  181. package/lib/feature-libraries/modular-schema/modularChangeCodecV1.js +4 -0
  182. package/lib/feature-libraries/modular-schema/modularChangeCodecV1.js.map +1 -1
  183. package/lib/packageVersion.d.ts +1 -1
  184. package/lib/packageVersion.d.ts.map +1 -1
  185. package/lib/packageVersion.js +1 -1
  186. package/lib/packageVersion.js.map +1 -1
  187. package/lib/shared-tree/independentView.d.ts.map +1 -1
  188. package/lib/shared-tree/independentView.js +2 -0
  189. package/lib/shared-tree/independentView.js.map +1 -1
  190. package/lib/shared-tree/sharedTree.d.ts +35 -1
  191. package/lib/shared-tree/sharedTree.d.ts.map +1 -1
  192. package/lib/shared-tree/sharedTree.js +6 -0
  193. package/lib/shared-tree/sharedTree.js.map +1 -1
  194. package/lib/shared-tree/sharedTreeChangeCodecs.js +1 -0
  195. package/lib/shared-tree/sharedTreeChangeCodecs.js.map +1 -1
  196. package/lib/shared-tree/treeAlpha.d.ts.map +1 -1
  197. package/lib/shared-tree/treeAlpha.js +2 -0
  198. package/lib/shared-tree/treeAlpha.js.map +1 -1
  199. package/lib/shared-tree/treeCheckout.d.ts +1 -1
  200. package/lib/shared-tree/treeCheckout.d.ts.map +1 -1
  201. package/lib/shared-tree/treeCheckout.js +2 -0
  202. package/lib/shared-tree/treeCheckout.js.map +1 -1
  203. package/lib/shared-tree-core/editManagerCodecs.d.ts +3 -0
  204. package/lib/shared-tree-core/editManagerCodecs.d.ts.map +1 -1
  205. package/lib/shared-tree-core/editManagerCodecs.js.map +1 -1
  206. package/lib/shared-tree-core/editManagerCodecsCommons.d.ts +14 -0
  207. package/lib/shared-tree-core/editManagerCodecsCommons.d.ts.map +1 -1
  208. package/lib/shared-tree-core/editManagerCodecsCommons.js +14 -0
  209. package/lib/shared-tree-core/editManagerCodecsCommons.js.map +1 -1
  210. package/lib/shared-tree-core/editManagerCodecsV1toV4.d.ts +3 -0
  211. package/lib/shared-tree-core/editManagerCodecsV1toV4.d.ts.map +1 -1
  212. package/lib/shared-tree-core/editManagerCodecsV1toV4.js.map +1 -1
  213. package/lib/shared-tree-core/editManagerCodecsVSharedBranches.d.ts +3 -0
  214. package/lib/shared-tree-core/editManagerCodecsVSharedBranches.d.ts.map +1 -1
  215. package/lib/shared-tree-core/editManagerCodecsVSharedBranches.js.map +1 -1
  216. package/lib/shared-tree-core/editManagerSummarizer.d.ts +9 -1
  217. package/lib/shared-tree-core/editManagerSummarizer.d.ts.map +1 -1
  218. package/lib/shared-tree-core/editManagerSummarizer.js +20 -5
  219. package/lib/shared-tree-core/editManagerSummarizer.js.map +1 -1
  220. package/lib/shared-tree-core/messageCodecV1ToV4.d.ts.map +1 -1
  221. package/lib/shared-tree-core/messageCodecV1ToV4.js +4 -0
  222. package/lib/shared-tree-core/messageCodecV1ToV4.js.map +1 -1
  223. package/lib/shared-tree-core/messageCodecVSharedBranches.d.ts.map +1 -1
  224. package/lib/shared-tree-core/messageCodecVSharedBranches.js +4 -0
  225. package/lib/shared-tree-core/messageCodecVSharedBranches.js.map +1 -1
  226. package/lib/shared-tree-core/sharedTreeCore.d.ts +4 -0
  227. package/lib/shared-tree-core/sharedTreeCore.d.ts.map +1 -1
  228. package/lib/shared-tree-core/sharedTreeCore.js +1 -1
  229. package/lib/shared-tree-core/sharedTreeCore.js.map +1 -1
  230. package/lib/simple-tree/api/schemaFactory.d.ts +3 -3
  231. package/lib/simple-tree/api/schemaFactory.js +3 -3
  232. package/lib/simple-tree/api/schemaFactory.js.map +1 -1
  233. package/lib/simple-tree/api/schemaFactoryAlpha.d.ts +17 -1
  234. package/lib/simple-tree/api/schemaFactoryAlpha.d.ts.map +1 -1
  235. package/lib/simple-tree/api/schemaFactoryAlpha.js +9 -0
  236. package/lib/simple-tree/api/schemaFactoryAlpha.js.map +1 -1
  237. package/lib/tableSchema.d.ts.map +1 -1
  238. package/lib/tableSchema.js +103 -21
  239. package/lib/tableSchema.js.map +1 -1
  240. package/package.json +24 -24
  241. package/src/core/change-family/changeFamily.ts +25 -0
  242. package/src/core/tree/detachedFieldIndexCodecV1.ts +2 -0
  243. package/src/core/tree/detachedFieldIndexCodecV2.ts +2 -0
  244. package/src/feature-libraries/chunked-forest/basicChunk.ts +76 -20
  245. package/src/feature-libraries/chunked-forest/codec/chunkDecoding.ts +61 -12
  246. package/src/feature-libraries/chunked-forest/codec/codecs.ts +34 -1
  247. package/src/feature-libraries/chunked-forest/codec/compressedEncode.ts +9 -3
  248. package/src/feature-libraries/chunked-forest/codec/format/formatGeneric.ts +0 -1
  249. package/src/feature-libraries/chunked-forest/codec/format/index.ts +1 -0
  250. package/src/feature-libraries/chunked-forest/codec/format/versions.ts +15 -0
  251. package/src/feature-libraries/chunked-forest/codec/nodeEncoder.ts +9 -1
  252. package/src/feature-libraries/chunked-forest/codec/schemaBasedEncode.ts +9 -1
  253. package/src/feature-libraries/chunked-forest/uniformChunk.ts +32 -2
  254. package/src/feature-libraries/forest-summary/incrementalSummaryBuilder.ts +116 -31
  255. package/src/feature-libraries/modular-schema/genericFieldKindFormat.ts +3 -21
  256. package/src/feature-libraries/modular-schema/modularChangeCodecV1.ts +4 -0
  257. package/src/packageVersion.ts +1 -1
  258. package/src/shared-tree/independentView.ts +2 -0
  259. package/src/shared-tree/sharedTree.ts +41 -1
  260. package/src/shared-tree/sharedTreeChangeCodecs.ts +1 -0
  261. package/src/shared-tree/treeAlpha.ts +2 -0
  262. package/src/shared-tree/treeCheckout.ts +2 -0
  263. package/src/shared-tree-core/editManagerCodecs.ts +3 -0
  264. package/src/shared-tree-core/editManagerCodecsCommons.ts +29 -0
  265. package/src/shared-tree-core/editManagerCodecsV1toV4.ts +3 -0
  266. package/src/shared-tree-core/editManagerCodecsVSharedBranches.ts +3 -0
  267. package/src/shared-tree-core/editManagerSummarizer.ts +17 -5
  268. package/src/shared-tree-core/messageCodecV1ToV4.ts +4 -0
  269. package/src/shared-tree-core/messageCodecVSharedBranches.ts +5 -1
  270. package/src/shared-tree-core/sharedTreeCore.ts +8 -1
  271. package/src/simple-tree/api/schemaFactory.ts +3 -3
  272. package/src/simple-tree/api/schemaFactoryAlpha.ts +34 -3
  273. package/src/tableSchema.ts +134 -35
@@ -7,6 +7,8 @@ import type { IIdCompressor, SessionId } from "@fluidframework/id-compressor";
7
7
 
8
8
  import type { ICodecFamily, IJsonCodec } from "../../codec/index.js";
9
9
  import type { SchemaAndPolicy } from "../../core/index.js";
10
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Referenced by doc comments
11
+ import type { FieldBatchEncodingContext } from "../../feature-libraries/index.js";
10
12
  import type { JsonCompatibleReadOnly } from "../../util/index.js";
11
13
  import type { ChangeRebaser, RevisionTag, TaggedChange } from "../rebase/index.js";
12
14
 
@@ -25,6 +27,29 @@ export interface ChangeEncodingContext {
25
27
  readonly revision: RevisionTag | undefined;
26
28
  readonly idCompressor: IIdCompressor;
27
29
  readonly schema?: SchemaAndPolicy;
30
+ /**
31
+ * `true` when this context is encoding to or decoding from a summary blob.
32
+ * `false` when this context is for an op (or any other non-summary path,
33
+ * including utility encoders that aren't tied to persistence).
34
+ *
35
+ * @remarks
36
+ * Used to gate decode-time recovery behavior — for example, healing of
37
+ * unresolvable identifier IDs — that should only run when loading a
38
+ * (possibly broken) attach-summary blob, never when applying ops.
39
+ */
40
+ readonly isSummary: boolean;
41
+ /**
42
+ * If `true`, identifier values that the local id-compressor cannot resolve
43
+ * during decode are healed into deterministic stable UUIDs instead of
44
+ * throwing. See {@link FieldBatchEncodingContext.healUnresolvableIdentifiersOnDecode}.
45
+ * Only takes effect when `isSummary` is also `true`.
46
+ */
47
+ readonly healUnresolvableIdentifiersOnDecode?: boolean;
48
+ /**
49
+ * The SharedTree's shared-object id, used as input to the deterministic
50
+ * UUID derivation when {@link healUnresolvableIdentifiersOnDecode} triggers.
51
+ */
52
+ readonly sharedObjectId?: string;
28
53
  }
29
54
 
30
55
  export type ChangeFamilyCodec<TChange> = IJsonCodec<
@@ -61,6 +61,8 @@ class MajorCodec implements IJsonCodec<Major, EncodedRevisionTag> {
61
61
  originatorId: this.revisionTagCodec.localSessionId,
62
62
  idCompressor: this.idCompressor,
63
63
  revision: undefined,
64
+ // DetachedFieldIndex codecs are only used by the summarizer.
65
+ isSummary: true,
64
66
  });
65
67
  }
66
68
  }
@@ -49,6 +49,8 @@ class MajorCodec implements IJsonCodec<Major> {
49
49
  originatorId: this.revisionTagCodec.localSessionId,
50
50
  idCompressor: this.idCompressor,
51
51
  revision: undefined,
52
+ // DetachedFieldIndex codecs are only used by the summarizer.
53
+ isSummary: true,
52
54
  });
53
55
  }
54
56
  }
@@ -3,7 +3,7 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { assert, oob, fail } from "@fluidframework/core-utils/internal";
6
+ import { assert, oob, fail, debugAssert } from "@fluidframework/core-utils/internal";
7
7
 
8
8
  import {
9
9
  CursorLocationType,
@@ -166,10 +166,19 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
166
166
  if (this.nestedCursor !== undefined) {
167
167
  return this.nestedCursor.mode;
168
168
  }
169
- // Compute the number of nodes deep the current depth is.
170
- // We want the floor of the result, which can computed using a bitwise shift assuming the depth is less than 2^31, which seems safe.
171
- // eslint-disable-next-line no-bitwise
172
- const halfHeight = (this.siblingStack.length + 1) >> 1;
169
+ this.assertChunkStacksMatchNodeDepth();
170
+ return this.siblingStack.length % 2 === 0
171
+ ? CursorLocationType.Fields
172
+ : CursorLocationType.Nodes;
173
+ }
174
+
175
+ /**
176
+ * Asserts that the node-only stacks (`indexOfChunkStack` and `indexWithinChunkStack`) are in sync with `siblingStack`.
177
+ * Since `siblingStack` interleaves field and node levels while the node-only stacks are pushed/popped only on node-level transitions,
178
+ * their length should always equal the number of node levels traversed.
179
+ */
180
+ private assertChunkStacksMatchNodeDepth(): void {
181
+ const halfHeight = this.getNodeOnlyHeightFromHeight();
173
182
  assert(
174
183
  this.indexOfChunkStack.length === halfHeight,
175
184
  0x51c /* unexpected indexOfChunkStack */,
@@ -178,9 +187,6 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
178
187
  this.indexWithinChunkStack.length === halfHeight,
179
188
  0x51d /* unexpected indexWithinChunkStack */,
180
189
  );
181
- return this.siblingStack.length % 2 === 0
182
- ? CursorLocationType.Fields
183
- : CursorLocationType.Nodes;
184
190
  }
185
191
 
186
192
  public getFieldKey(): FieldKey {
@@ -203,9 +209,32 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
203
209
  return this.indexStack[height] ?? oob();
204
210
  }
205
211
 
206
- private getStackedNode(height: number): BasicChunk {
207
- const index = this.getStackedNodeIndex(height);
208
- return (this.siblingStack[height] as readonly TreeChunk[])[index] as BasicChunk;
212
+ private getStackedChunkIndex(height: number): number {
213
+ assert(height % 2 === 1, 0xcf3 /* must be node height */);
214
+ assert(height >= 0, 0xcf4 /* must not be above root */);
215
+ return this.indexOfChunkStack[this.getNodeOnlyHeightFromHeight(height)] ?? oob();
216
+ }
217
+
218
+ private getStackedChunk(height: number): BasicChunk {
219
+ const index = this.getStackedChunkIndex(height);
220
+ const chunk = (this.siblingStack[height] as readonly TreeChunk[])[index];
221
+ debugAssert(() => chunk instanceof BasicChunk || "only basic chunks are expected");
222
+ return chunk as BasicChunk;
223
+ }
224
+
225
+ /**
226
+ * Converts a {@link height}, which contains field and node levels, into the corresponding depth/index
227
+ * for the node-only stacks ({@link indexOfChunkStack} and {@link indexWithinChunkStack}), which are
228
+ * only pushed on node-level transitions.
229
+ *
230
+ * @param height - A depth in {@link siblingStack} to convert. Defaults to {@link siblingStack}'s
231
+ * current length, which gives the current depth of the node-only stacks.
232
+ * @returns `floor(height / 2)` — the number of node levels at or below the given stack height.
233
+ */
234
+ private getNodeOnlyHeightFromHeight(height: number = this.siblingStack.length): number {
235
+ // The bitwise shift computes the floor, which is valid assuming the depth is less than 2^31, which seems safe.
236
+ // eslint-disable-next-line no-bitwise
237
+ return height >> 1;
209
238
  }
210
239
 
211
240
  public getFieldLength(): number {
@@ -322,6 +351,11 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
322
351
  assert(this.mode === CursorLocationType.Nodes, 0x528 /* must be in nodes mode */);
323
352
  this.siblingStack.push(this.siblings);
324
353
  this.indexStack.push(this.index);
354
+ // Save the chunk array position of the current node. When siblings contain
355
+ // multi node chunks, the flat node index diverges from the array position,
356
+ // so getField needs this to locate the parent in the sibling array.
357
+ this.indexOfChunkStack.push(this.indexOfChunk);
358
+ this.indexWithinChunkStack.push(this.indexWithinChunk);
325
359
 
326
360
  // For fields, siblings are only used for key lookup and
327
361
  // nextField and which has arbitrary iteration order,
@@ -330,6 +364,7 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
330
364
  // at the cost of an allocation here.
331
365
  this.index = 0;
332
366
  this.siblings = [key];
367
+ this.assertChunkStacksMatchNodeDepth();
333
368
  }
334
369
 
335
370
  public nextField(): boolean {
@@ -355,8 +390,11 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
355
390
 
356
391
  this.siblingStack.push(this.siblings);
357
392
  this.indexStack.push(this.index);
393
+ this.indexOfChunkStack.push(this.indexOfChunk);
394
+ this.indexWithinChunkStack.push(this.indexWithinChunk);
358
395
  this.index = 0;
359
396
  this.siblings = [...fields.keys()]; // TODO: avoid this copy
397
+ this.assertChunkStacksMatchNodeDepth();
360
398
  return true;
361
399
  }
362
400
 
@@ -422,12 +460,11 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
422
460
  }
423
461
  this.siblingStack.push(this.siblings);
424
462
  this.indexStack.push(this.index);
425
- this.indexOfChunkStack.push(this.indexOfChunk);
426
- this.indexWithinChunkStack.push(this.indexWithinChunk);
427
463
  this.index = 0;
428
464
  this.siblings = siblings;
429
465
  this.indexOfChunk = 0;
430
466
  this.indexWithinChunk = 0;
467
+ this.assertChunkStacksMatchNodeDepth();
431
468
  this.initNestedCursor();
432
469
  return true;
433
470
  }
@@ -486,6 +523,12 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
486
523
  this.siblings =
487
524
  this.siblingStack.pop() ?? fail(0xaf0 /* Unexpected siblingStack.length */);
488
525
  this.index = this.indexStack.pop() ?? fail(0xaf1 /* Unexpected indexStack.length */);
526
+ this.indexOfChunk =
527
+ this.indexOfChunkStack.pop() ?? fail(0xcf5 /* Unexpected indexOfChunkStack.length */);
528
+ this.indexWithinChunk =
529
+ this.indexWithinChunkStack.pop() ??
530
+ fail(0xcf6 /* Unexpected indexWithinChunkStack.length */);
531
+ this.assertChunkStacksMatchNodeDepth();
489
532
  }
490
533
 
491
534
  public exitNode(): void {
@@ -502,18 +545,27 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
502
545
  this.siblings =
503
546
  this.siblingStack.pop() ?? fail(0xaf2 /* Unexpected siblingStack.length */);
504
547
  this.index = this.indexStack.pop() ?? fail(0xaf3 /* Unexpected indexStack.length */);
505
- this.indexOfChunk =
506
- this.indexOfChunkStack.pop() ?? fail(0xaf4 /* Unexpected indexOfChunkStack.length */);
507
- this.indexWithinChunk =
508
- this.indexWithinChunkStack.pop() ??
509
- fail(0xaf5 /* Unexpected indexWithinChunkStack.length */);
548
+ // At the Fields level these aren't semantically used, but reset for consistent state
549
+ // (so a fully-iterated cursor matches a fresh cursor at the same logical position).
550
+ this.indexOfChunk = 0;
551
+ this.indexWithinChunk = 0;
552
+ this.assertChunkStacksMatchNodeDepth();
510
553
  }
511
554
 
512
555
  private getNode(): BasicChunk {
513
556
  assert(this.mode === CursorLocationType.Nodes, 0x52f /* can only get node when in node */);
514
- return (this.siblings as TreeChunk[])[this.index] as BasicChunk;
557
+ const chunk = (this.siblings as TreeChunk[])[this.indexOfChunk];
558
+ debugAssert(() => chunk instanceof BasicChunk || "only basic chunks are expected");
559
+ return chunk as BasicChunk;
515
560
  }
516
561
 
562
+ /**
563
+ * Resolves the chunks that make up the field the cursor is currently in. At the root, this is
564
+ * {@link root} directly. Otherwise, the cursor must be in {@link CursorLocationType.Fields} mode,
565
+ * and the result is looked up on the parent node using the current field key.
566
+ *
567
+ * @returns The chunks that make up the field the cursor is currently in.
568
+ */
517
569
  private getField(): readonly TreeChunk[] {
518
570
  if (this.siblingStack.length === 0) {
519
571
  return this.root;
@@ -522,7 +574,11 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor
522
574
  this.mode === CursorLocationType.Fields,
523
575
  0x530 /* can only get field when in fields */,
524
576
  );
525
- const parent = this.getStackedNode(this.indexStack.length - 1);
577
+ // The parent node is the `BasicChunk` in the node array at the top of
578
+ // `siblingStack` while we are in `CursorLocationType.Fields` mode. We need the parent
579
+ // since a field's chunks are stored on the parent node's `BasicChunk.fields` map, not on
580
+ // the cursor itself.
581
+ const parent = this.getStackedChunk(this.siblingStack.length - 1);
526
582
  const key: FieldKey = this.getFieldKey();
527
583
  const field = parent.fields.get(key) ?? [];
528
584
  return field;
@@ -9,6 +9,7 @@ import type {
9
9
  OpSpaceCompressedId,
10
10
  SessionId,
11
11
  } from "@fluidframework/id-compressor";
12
+ import { v5 as uuidV5 } from "uuid";
12
13
 
13
14
  import { DiscriminatedUnionDispatcher } from "../../../codec/index.js";
14
15
  import type {
@@ -37,7 +38,8 @@ import {
37
38
  decode as genericDecode,
38
39
  readStreamIdentifier,
39
40
  } from "./chunkDecodingGeneric.js";
40
- import type { IncrementalDecoder } from "./codecs.js";
41
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Referenced by doc comments
42
+ import type { FieldBatchEncodingContext, IncrementalDecoder } from "./codecs.js";
41
43
  import {
42
44
  type EncodedAnyShape,
43
45
  type EncodedChunkShapeV1OrV2,
@@ -49,8 +51,8 @@ import {
49
51
  type EncodedNestedArrayShape,
50
52
  type EncodedNodeShape,
51
53
  type EncodedValueShape,
52
- FieldBatchFormatVersion,
53
54
  SpecialField,
55
+ supportsIncrementalEncoding,
54
56
  } from "./format/index.js";
55
57
 
56
58
  export interface IdDecodingContext {
@@ -59,13 +61,32 @@ export interface IdDecodingContext {
59
61
  * The creator of any local Ids to be decoded.
60
62
  */
61
63
  originatorId: SessionId;
64
+ /**
65
+ * {@inheritdoc FieldBatchEncodingContext.isSummary}
66
+ */
67
+ isSummary: boolean;
68
+ /**
69
+ * See {@link FieldBatchEncodingContext.healUnresolvableIdentifiersOnDecode}.
70
+ */
71
+ healUnresolvableIdentifiersOnDecode?: boolean;
72
+ /**
73
+ * See {@link FieldBatchEncodingContext.sharedObjectId}.
74
+ */
75
+ sharedObjectId?: string;
62
76
  }
77
+
78
+ /**
79
+ * Random v4 UUID generated as a namespace for the "heal an unresolvable identifier into a stable UUID"
80
+ * path in {@link readValue}. This scheme requires consensus across all clients to function.
81
+ */
82
+ const healingNamespace = "f8a89df3-6882-400f-b913-4c1f6f0157bd";
83
+
63
84
  /**
64
85
  * Decode `chunk` into a TreeChunk.
65
86
  */
66
87
  export function decode(
67
88
  chunk: EncodedFieldBatchV1OrV2,
68
- idDecodingContext: { idCompressor: IIdCompressor; originatorId: SessionId },
89
+ idDecodingContext: IdDecodingContext,
69
90
  incrementalDecoder?: IncrementalDecoder,
70
91
  ): TreeChunk[] {
71
92
  return genericDecode(
@@ -126,15 +147,43 @@ export function readValue(
126
147
  typeof streamValue === "number" || typeof streamValue === "string",
127
148
  0x997 /* identifier must be string or number. */,
128
149
  );
150
+ if (typeof streamValue === "string") {
151
+ return streamValue;
152
+ }
129
153
  const idCompressor = idDecodingContext.idCompressor;
130
- return typeof streamValue === "number"
131
- ? idCompressor.decompress(
132
- idCompressor.normalizeToSessionSpace(
133
- streamValue as OpSpaceCompressedId,
134
- idDecodingContext.originatorId,
135
- ),
136
- )
137
- : streamValue;
154
+ // OpSpaceCompressedIds are negative, and require a session-id to compute their value.
155
+ // Due to a bug, we have some special casing for them (see below).
156
+ // TODO: isFinalId should probably be exported from id-compressor and that could be used to do the narrowing here.
157
+ if (idDecodingContext.isSummary === true && streamValue < 0) {
158
+ if (
159
+ idDecodingContext.healUnresolvableIdentifiersOnDecode === true &&
160
+ idDecodingContext.sharedObjectId !== undefined
161
+ ) {
162
+ // Documents written before the encode-side fix for non-finalized identifier
163
+ // values can persist negative op-space IDs that are no
164
+ // longer resolvable once the originating session's local state has been stripped.
165
+ // When loading such a summary with the heal-on-decode option on, synthesize a deterministic
166
+ // stable UUID so all readers of the same blob agree on the resulting value.
167
+ //
168
+ // The heal path is intentionally restricted to summary loads — an
169
+ // unresolvable ID encountered while applying an op should still surface as
170
+ // an error, since it indicates a real bug rather than a recoverable state.
171
+ return uuidV5(
172
+ `${idDecodingContext.sharedObjectId}|${streamValue}`,
173
+ healingNamespace,
174
+ );
175
+ }
176
+ // See `SharedTreeOptionsBeta.healUnresolvableIdentifiersOnDecode` for details on this error.
177
+ throw new Error(
178
+ "Summary could not be loaded due incorrectly encoded identifier. See SharedTreeOptionsBeta.healUnresolvableIdentifiersOnDecode for mitigation.",
179
+ );
180
+ }
181
+ return idCompressor.decompress(
182
+ idCompressor.normalizeToSessionSpace(
183
+ streamValue as OpSpaceCompressedId,
184
+ idDecodingContext.originatorId,
185
+ ),
186
+ );
138
187
  } else {
139
188
  // EncodedCounter case:
140
189
  unreachableCase(shape, "decoding values as deltas is not yet supported");
@@ -256,7 +305,7 @@ export class IncrementalChunkDecoder implements ChunkDecoder {
256
305
 
257
306
  const chunkDecoder = (batch: EncodedFieldBatchV2): TreeChunk => {
258
307
  assert(
259
- batch.version >= FieldBatchFormatVersion.v2,
308
+ supportsIncrementalEncoding(batch.version),
260
309
  0xc9f /* Unsupported FieldBatchFormatVersion for incremental chunks; must be v2 or higher */,
261
310
  );
262
311
  const context = new DecoderContext(
@@ -30,6 +30,7 @@ import {
30
30
  EncodedFieldBatchV1,
31
31
  EncodedFieldBatchV2,
32
32
  FieldBatchFormatVersion,
33
+ supportsIncrementalEncoding,
33
34
  type EncodedFieldBatchV1OrV2,
34
35
  } from "./format/index.js";
35
36
  import type { IncrementalEncodingPolicy } from "./incrementalEncodingPolicy.js";
@@ -105,6 +106,33 @@ export interface FieldBatchEncodingContext {
105
106
  * This will be defined if incremental encoding is supported and enabled.
106
107
  */
107
108
  readonly incrementalEncoderDecoder?: IncrementalEncoderDecoder;
109
+ /**
110
+ * `true` when encoding to or decoding from a summary blob. `false` for
111
+ * op-stream encode/decode paths and for utility encoders that are not
112
+ * tied to a persisted document. Healing behavior is gated on this flag.
113
+ */
114
+ readonly isSummary: boolean;
115
+ /**
116
+ * If `true`, when an op-space compressed ID encountered while decoding
117
+ * cannot be resolved by the local id-compressor (e.g. the attach-summary
118
+ * blob's originator session state was stripped), a deterministic stable
119
+ * UUID derived from `sharedObjectId` is returned instead of throwing.
120
+ * @remarks
121
+ * Off by default. Used only to recover documents whose attach summary was
122
+ * written with non-finalized op-space IDs before the encode-side fix
123
+ * shipped. Only takes effect when `isSummary` is also `true`.
124
+ * See {@link SharedTreeOptionsBeta.healUnresolvableIdentifiersOnDecode}.
125
+ */
126
+ readonly healUnresolvableIdentifiersOnDecode?: boolean;
127
+ /**
128
+ * The SharedTree's shared-object id, used as input to the deterministic
129
+ * UUID derivation when `healUnresolvableIdentifiersOnDecode` triggers. Required
130
+ * for that path; ignored otherwise.
131
+ * @remarks
132
+ * This allows us to ensure that multiple attaches,
133
+ * in the same or different documents, with the same session offsets, get different UUIDs.
134
+ */
135
+ readonly sharedObjectId?: string;
108
136
  }
109
137
  /**
110
138
  * @remarks
@@ -124,6 +152,7 @@ function makeFieldBatchCodecForVersion(
124
152
  fieldBatch: FieldBatch,
125
153
  idCompressor: IIdCompressor,
126
154
  incrementalEncoder: IncrementalEncoder | undefined,
155
+ isSummary: boolean,
127
156
  ) => EncodedFieldBatchV1OrV2,
128
157
  encodedFieldBatchType: TSchema,
129
158
  ): CodecAndSchema<FieldBatch, FieldBatchEncodingContext> {
@@ -147,7 +176,7 @@ function makeFieldBatchCodecForVersion(
147
176
  }
148
177
  case TreeCompressionStrategy.CompressedIncremental: {
149
178
  assert(
150
- version >= FieldBatchFormatVersion.v2,
179
+ supportsIncrementalEncoding(version),
151
180
  0xca0 /* Unsupported FieldBatchFormatVersion for incremental encoding; must be v2 or higher */,
152
181
  );
153
182
  // Incremental encoding is only supported for CompressedIncremental.
@@ -166,6 +195,7 @@ function makeFieldBatchCodecForVersion(
166
195
  data,
167
196
  context.idCompressor,
168
197
  incrementalEncoder,
198
+ context.isSummary,
169
199
  );
170
200
  }
171
201
 
@@ -189,6 +219,9 @@ function makeFieldBatchCodecForVersion(
189
219
  {
190
220
  idCompressor: context.idCompressor,
191
221
  originatorId: context.originatorId,
222
+ isSummary: context.isSummary,
223
+ healUnresolvableIdentifiersOnDecode: context.healUnresolvableIdentifiersOnDecode,
224
+ sharedObjectId: context.sharedObjectId,
192
225
  },
193
226
  context.incrementalEncoderDecoder,
194
227
  ).map((chunk) => chunk.cursor());
@@ -26,7 +26,8 @@ import {
26
26
  Shape as ShapeGeneric,
27
27
  updateShapesAndIdentifiersEncoding,
28
28
  } from "./chunkEncodingGeneric.js";
29
- import type { IncrementalEncoder } from "./codecs.js";
29
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Referenced by doc comments
30
+ import type { FieldBatchEncodingContext, IncrementalEncoder } from "./codecs.js";
30
31
  import type { FieldBatch } from "./fieldBatch.js";
31
32
  import {
32
33
  type EncodedAnyShape,
@@ -36,8 +37,9 @@ import {
36
37
  type EncodedFieldBatchV1OrV2,
37
38
  type EncodedNestedArrayShape,
38
39
  type EncodedValueShape,
39
- FieldBatchFormatVersion,
40
+ type FieldBatchFormatVersion,
40
41
  SpecialField,
42
+ supportsIncrementalEncoding,
41
43
  } from "./format/index.js";
42
44
 
43
45
  /**
@@ -461,7 +463,7 @@ export const incrementalFieldEncoder: FieldEncoder = {
461
463
  0xc88 /* incremental encoder must be defined to use incrementalFieldEncoder */,
462
464
  );
463
465
  assert(
464
- context.version >= FieldBatchFormatVersion.v2,
466
+ supportsIncrementalEncoding(context.version),
465
467
  0xca1 /* Unsupported FieldBatchFormatVersion for incremental encoding; must be v2 or higher */,
466
468
  );
467
469
 
@@ -534,6 +536,10 @@ export class EncoderContext implements NodeEncodeBuilder, FieldEncodeBuilder {
534
536
  */
535
537
  public readonly incrementalEncoder: IncrementalEncoder | undefined,
536
538
  public readonly version: FieldBatchFormatVersion,
539
+ /**
540
+ * See {@link FieldBatchEncodingContext.isSummary}.
541
+ */
542
+ public readonly isSummary: boolean,
537
543
  ) {}
538
544
 
539
545
  public nodeEncoderFromSchema(schemaName: TreeNodeSchemaIdentifier): NodeEncoder {
@@ -28,7 +28,6 @@ export const Count = Type.Number({ multipleOf: 1, minimum: 0 });
28
28
 
29
29
  const EncodedFieldBatchBase = Type.Object(
30
30
  {
31
- version: Type.Number(),
32
31
  identifiers: Type.Array(Type.String()),
33
32
  /**
34
33
  * Top level array is list of field from batch.
@@ -20,6 +20,7 @@ export {
20
20
  FieldBatchFormatVersion,
21
21
  EncodedFieldBatchV1,
22
22
  EncodedFieldBatchV2,
23
+ supportsIncrementalEncoding,
23
24
  type EncodedFieldBatchV1OrV2,
24
25
  type EncodedFieldBatchV1AndV2,
25
26
  type EncodedChunkShapeV1OrV2,
@@ -31,6 +31,21 @@ export const FieldBatchFormatVersion = strictEnum("FieldBatchFormatVersion", {
31
31
  v2: 2,
32
32
  });
33
33
 
34
+ /**
35
+ * Whether the given format version supports incremental chunk encoding.
36
+ *
37
+ * @remarks
38
+ * This helper should be used for comparison since experimental versions
39
+ * can be a string.
40
+ */
41
+ export function supportsIncrementalEncoding(version: FieldBatchFormatVersion): boolean {
42
+ if (version === FieldBatchFormatVersion.v1) {
43
+ return false;
44
+ }
45
+
46
+ return true;
47
+ }
48
+
34
49
  /**
35
50
  * Encoded {@link FieldBatch} using V1 format.
36
51
  * @remarks
@@ -77,7 +77,15 @@ export class NodeShapeBasedEncoder
77
77
  if (isStableId(cursor.value)) {
78
78
  const sessionSpaceCompressedId = context.idCompressor.tryRecompress(cursor.value);
79
79
  if (sessionSpaceCompressedId !== undefined) {
80
- return context.idCompressor.normalizeToOpSpace(sessionSpaceCompressedId);
80
+ const opSpaceId = context.idCompressor.normalizeToOpSpace(sessionSpaceCompressedId);
81
+ // Summaries can only contain finalized op-space ids unless they also include the originator's session id somewhere.
82
+ // This is not the case for forest summaries at the time of writing, so non-finalized ids are instead written using
83
+ // their long form (by falling through to the original cursor value).
84
+ // A scenario where such ids can appear in the summary is in the attach summary of a tree being attached to an already-attached container.
85
+ // TODO: isFinalId should probably be exported from id-compressor and that could be used to do the narrowing here.
86
+ if (!context.isSummary || opSpaceId >= 0) {
87
+ return opSpaceId;
88
+ }
81
89
  }
82
90
  }
83
91
  }
@@ -54,6 +54,8 @@ export function schemaCompressedEncodeV1(
54
54
  policy: SchemaPolicy,
55
55
  fieldBatch: FieldBatch,
56
56
  idCompressor: IIdCompressor,
57
+ _incrementalEncoder: IncrementalEncoder | undefined,
58
+ isSummary: boolean,
57
59
  ): EncodedFieldBatchV1 {
58
60
  const encoded: EncodedFieldBatchV1OrV2 = schemaCompressedEncode(
59
61
  schema,
@@ -62,6 +64,7 @@ export function schemaCompressedEncodeV1(
62
64
  idCompressor,
63
65
  undefined /* incrementalEncoder */,
64
66
  brand(FieldBatchFormatVersion.v1),
67
+ isSummary,
65
68
  );
66
69
  // Since incrementalEncoder was not provided, no V2 features should be used, and this cast should be safe.
67
70
  return encoded as EncodedFieldBatchV1;
@@ -78,6 +81,7 @@ export function schemaCompressedEncodeV2(
78
81
  fieldBatch: FieldBatch,
79
82
  idCompressor: IIdCompressor,
80
83
  incrementalEncoder: IncrementalEncoder | undefined,
84
+ isSummary: boolean,
81
85
  ): EncodedFieldBatchV2 {
82
86
  return schemaCompressedEncode(
83
87
  schema,
@@ -86,6 +90,7 @@ export function schemaCompressedEncodeV2(
86
90
  idCompressor,
87
91
  incrementalEncoder,
88
92
  brand(FieldBatchFormatVersion.v2),
93
+ isSummary,
89
94
  );
90
95
  }
91
96
 
@@ -106,10 +111,11 @@ function schemaCompressedEncode(
106
111
  idCompressor: IIdCompressor,
107
112
  incrementalEncoder: IncrementalEncoder | undefined,
108
113
  version: FieldBatchFormatVersion,
114
+ isSummary: boolean,
109
115
  ): EncodedFieldBatchV1OrV2 {
110
116
  return compressedEncode(
111
117
  fieldBatch,
112
- buildContext(schema, policy, idCompressor, incrementalEncoder, version),
118
+ buildContext(schema, policy, idCompressor, incrementalEncoder, version, isSummary),
113
119
  );
114
120
  }
115
121
 
@@ -119,6 +125,7 @@ export function buildContext(
119
125
  idCompressor: IIdCompressor,
120
126
  incrementalEncoder: IncrementalEncoder | undefined,
121
127
  version: FieldBatchFormatVersion,
128
+ isSummary: boolean,
122
129
  ): EncoderContext {
123
130
  const context: EncoderContext = new EncoderContext(
124
131
  (fieldBuilder: FieldEncodeBuilder, schemaName: TreeNodeSchemaIdentifier) =>
@@ -129,6 +136,7 @@ export function buildContext(
129
136
  idCompressor,
130
137
  incrementalEncoder,
131
138
  version,
139
+ isSummary,
132
140
  );
133
141
  return context;
134
142
  }
@@ -20,7 +20,7 @@ import {
20
20
  cursorChunk,
21
21
  dummyRoot,
22
22
  } from "../../core/index.js";
23
- import { ReferenceCountedBase, hasSome } from "../../util/index.js";
23
+ import { ReferenceCountedBase, getOrCreate, hasSome } from "../../util/index.js";
24
24
  import { SynchronousCursor, prefixFieldPath, prefixPath } from "../treeCursorUtils.js";
25
25
 
26
26
  /**
@@ -83,6 +83,23 @@ export class UniformChunk extends ReferenceCountedBase implements TreeChunk {
83
83
  */
84
84
  export type FieldShape = readonly [FieldKey, TreeShape, number];
85
85
 
86
+ /**
87
+ * Maximum topLevelLength value (exclusive) for which {@link TreeShape.withTopLevelLength}
88
+ * caches the resulting {@link ChunkShape}. Values at or above this threshold always
89
+ * create a new instance to prevent unbounded cache growth.
90
+ *
91
+ * @remarks
92
+ * This value is an estimation of the general size needed to cover current workflows,
93
+ * not a researched constant, and is safe to tune as workloads change.
94
+ *
95
+ * Raising this value captures more chunk sizes in the cache, at the cost of
96
+ * each `TreeShape` retaining up to `chunkShapeCacheLimit - 1` cached entries for the
97
+ * lifetime of the shape. Lowering it reduces memory held per `TreeShape` but forces
98
+ * small chunks, where the relative cost of rebuilding `positions` is highest, to pay
99
+ * the construction cost on every call.
100
+ */
101
+ const chunkShapeCacheLimit = 8;
102
+
86
103
  /**
87
104
  * The "shape" of a tree.
88
105
  * Does not contain the actual values from the tree, but describes everything else,
@@ -110,7 +127,13 @@ export class TreeShape {
110
127
  public readonly mayContainCompressedIds: boolean;
111
128
 
112
129
  /**
113
- *
130
+ * Cache for ChunkShape instances created by {@link withTopLevelLength}.
131
+ * `topLevelLength` is always a positive integer (enforced by the {@link ChunkShape} constructor),
132
+ * so the cache only ever holds entries for values in `1..chunkShapeCacheLimit - 1` to prevent unbounded growth.
133
+ */
134
+ private readonly chunkShapeCache: Map<number, ChunkShape> = new Map();
135
+
136
+ /**
114
137
  * @param type - {@link TreeNodeSchemaIdentifier} used to compare shapes.
115
138
  * @param hasValue - whether or not the TreeShape has a value.
116
139
  * @param fieldsArray - an array of {@link FieldShape} values, which contains a TreeShape for each FieldKey.
@@ -179,6 +202,13 @@ export class TreeShape {
179
202
  }
180
203
 
181
204
  public withTopLevelLength(topLevelLength: number): ChunkShape {
205
+ if (topLevelLength < chunkShapeCacheLimit) {
206
+ return getOrCreate(
207
+ this.chunkShapeCache,
208
+ topLevelLength,
209
+ () => new ChunkShape(this, topLevelLength),
210
+ );
211
+ }
182
212
  return new ChunkShape(this, topLevelLength);
183
213
  }
184
214
  }