@fluidframework/sequence 1.4.0-121020 → 2.0.0-dev-rc.1.0.0.224419

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 (349) hide show
  1. package/.eslintrc.js +9 -11
  2. package/.mocharc.js +12 -0
  3. package/CHANGELOG.md +449 -0
  4. package/README.md +364 -183
  5. package/api-extractor-lint.json +4 -0
  6. package/api-extractor.json +2 -2
  7. package/api-report/sequence.api.md +741 -0
  8. package/dist/{defaultMap.js → defaultMap.cjs} +29 -22
  9. package/dist/defaultMap.cjs.map +1 -0
  10. package/dist/defaultMap.d.ts +7 -6
  11. package/dist/defaultMap.d.ts.map +1 -1
  12. package/dist/defaultMapInterfaces.cjs +7 -0
  13. package/dist/defaultMapInterfaces.cjs.map +1 -0
  14. package/dist/defaultMapInterfaces.d.ts +44 -12
  15. package/dist/defaultMapInterfaces.d.ts.map +1 -1
  16. package/dist/index.cjs +60 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.d.ts +14 -12
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/intervalCollection.cjs +1159 -0
  21. package/dist/intervalCollection.cjs.map +1 -0
  22. package/dist/intervalCollection.d.ts +461 -162
  23. package/dist/intervalCollection.d.ts.map +1 -1
  24. package/dist/intervalIndex/endpointInRangeIndex.cjs +66 -0
  25. package/dist/intervalIndex/endpointInRangeIndex.cjs.map +1 -0
  26. package/dist/intervalIndex/endpointInRangeIndex.d.ts +34 -0
  27. package/dist/intervalIndex/endpointInRangeIndex.d.ts.map +1 -0
  28. package/dist/intervalIndex/endpointIndex.cjs +47 -0
  29. package/dist/intervalIndex/endpointIndex.cjs.map +1 -0
  30. package/dist/intervalIndex/endpointIndex.d.ts +38 -0
  31. package/dist/intervalIndex/endpointIndex.d.ts.map +1 -0
  32. package/dist/intervalIndex/idIntervalIndex.cjs +44 -0
  33. package/dist/intervalIndex/idIntervalIndex.cjs.map +1 -0
  34. package/dist/intervalIndex/idIntervalIndex.d.ts +18 -0
  35. package/dist/intervalIndex/idIntervalIndex.d.ts.map +1 -0
  36. package/dist/intervalIndex/index.cjs +24 -0
  37. package/dist/intervalIndex/index.cjs.map +1 -0
  38. package/dist/intervalIndex/index.d.ts +13 -0
  39. package/dist/intervalIndex/index.d.ts.map +1 -0
  40. package/dist/{defaultMapInterfaces.js → intervalIndex/intervalIndex.cjs} +1 -1
  41. package/dist/intervalIndex/intervalIndex.cjs.map +1 -0
  42. package/dist/intervalIndex/intervalIndex.d.ts +30 -0
  43. package/dist/intervalIndex/intervalIndex.d.ts.map +1 -0
  44. package/dist/intervalIndex/intervalIndexUtils.cjs +22 -0
  45. package/dist/intervalIndex/intervalIndexUtils.cjs.map +1 -0
  46. package/dist/intervalIndex/intervalIndexUtils.d.ts +17 -0
  47. package/dist/intervalIndex/intervalIndexUtils.d.ts.map +1 -0
  48. package/dist/intervalIndex/overlappingIntervalsIndex.cjs +116 -0
  49. package/dist/intervalIndex/overlappingIntervalsIndex.cjs.map +1 -0
  50. package/dist/intervalIndex/overlappingIntervalsIndex.d.ts +44 -0
  51. package/dist/intervalIndex/overlappingIntervalsIndex.d.ts.map +1 -0
  52. package/dist/intervalIndex/overlappingSequenceIntervalsIndex.cjs +41 -0
  53. package/dist/intervalIndex/overlappingSequenceIntervalsIndex.cjs.map +1 -0
  54. package/dist/intervalIndex/overlappingSequenceIntervalsIndex.d.ts +11 -0
  55. package/dist/intervalIndex/overlappingSequenceIntervalsIndex.d.ts.map +1 -0
  56. package/dist/intervalIndex/sequenceIntervalIndexes.cjs +7 -0
  57. package/dist/intervalIndex/sequenceIntervalIndexes.cjs.map +1 -0
  58. package/dist/intervalIndex/sequenceIntervalIndexes.d.ts +35 -0
  59. package/dist/intervalIndex/sequenceIntervalIndexes.d.ts.map +1 -0
  60. package/dist/intervalIndex/startpointInRangeIndex.cjs +66 -0
  61. package/dist/intervalIndex/startpointInRangeIndex.cjs.map +1 -0
  62. package/dist/intervalIndex/startpointInRangeIndex.d.ts +34 -0
  63. package/dist/intervalIndex/startpointInRangeIndex.d.ts.map +1 -0
  64. package/dist/intervalTree.cjs +80 -0
  65. package/dist/intervalTree.cjs.map +1 -0
  66. package/dist/intervalTree.d.ts +24 -0
  67. package/dist/intervalTree.d.ts.map +1 -0
  68. package/dist/intervals/index.cjs +23 -0
  69. package/dist/intervals/index.cjs.map +1 -0
  70. package/dist/intervals/index.d.ts +8 -0
  71. package/dist/intervals/index.d.ts.map +1 -0
  72. package/dist/intervals/interval.cjs +181 -0
  73. package/dist/intervals/interval.cjs.map +1 -0
  74. package/dist/intervals/interval.d.ts +84 -0
  75. package/dist/intervals/interval.d.ts.map +1 -0
  76. package/dist/intervals/intervalUtils.cjs +83 -0
  77. package/dist/intervals/intervalUtils.cjs.map +1 -0
  78. package/dist/intervals/intervalUtils.d.ts +230 -0
  79. package/dist/intervals/intervalUtils.d.ts.map +1 -0
  80. package/dist/intervals/sequenceInterval.cjs +378 -0
  81. package/dist/intervals/sequenceInterval.cjs.map +1 -0
  82. package/dist/intervals/sequenceInterval.d.ts +137 -0
  83. package/dist/intervals/sequenceInterval.d.ts.map +1 -0
  84. package/dist/{localValues.js → localValues.cjs} +1 -1
  85. package/dist/localValues.cjs.map +1 -0
  86. package/dist/localValues.d.ts +2 -1
  87. package/dist/localValues.d.ts.map +1 -1
  88. package/dist/{packageVersion.js → packageVersion.cjs} +2 -2
  89. package/dist/packageVersion.cjs.map +1 -0
  90. package/dist/packageVersion.d.ts +1 -1
  91. package/dist/packageVersion.d.ts.map +1 -1
  92. package/dist/revertibles.cjs +425 -0
  93. package/dist/revertibles.cjs.map +1 -0
  94. package/dist/revertibles.d.ts +86 -0
  95. package/dist/revertibles.d.ts.map +1 -0
  96. package/dist/sequence-alpha.d.ts +1315 -0
  97. package/dist/sequence-beta.d.ts +244 -0
  98. package/dist/sequence-public.d.ts +244 -0
  99. package/dist/sequence-untrimmed.d.ts +1803 -0
  100. package/dist/{sequence.js → sequence.cjs} +226 -156
  101. package/dist/sequence.cjs.map +1 -0
  102. package/dist/sequence.d.ts +125 -48
  103. package/dist/sequence.d.ts.map +1 -1
  104. package/dist/{sequenceDeltaEvent.js → sequenceDeltaEvent.cjs} +18 -8
  105. package/dist/sequenceDeltaEvent.cjs.map +1 -0
  106. package/dist/sequenceDeltaEvent.d.ts +24 -7
  107. package/dist/sequenceDeltaEvent.d.ts.map +1 -1
  108. package/dist/sequenceFactory.cjs +55 -0
  109. package/dist/sequenceFactory.cjs.map +1 -0
  110. package/dist/sequenceFactory.d.ts +3 -89
  111. package/dist/sequenceFactory.d.ts.map +1 -1
  112. package/dist/{sharedIntervalCollection.js → sharedIntervalCollection.cjs} +17 -22
  113. package/dist/sharedIntervalCollection.cjs.map +1 -0
  114. package/dist/sharedIntervalCollection.d.ts +12 -12
  115. package/dist/sharedIntervalCollection.d.ts.map +1 -1
  116. package/dist/{sharedSequence.js → sharedSequence.cjs} +29 -22
  117. package/dist/sharedSequence.cjs.map +1 -0
  118. package/dist/sharedSequence.d.ts +14 -2
  119. package/dist/sharedSequence.d.ts.map +1 -1
  120. package/dist/sharedString.cjs +286 -0
  121. package/dist/sharedString.cjs.map +1 -0
  122. package/dist/sharedString.d.ts +58 -22
  123. package/dist/sharedString.d.ts.map +1 -1
  124. package/dist/tsdoc-metadata.json +11 -0
  125. package/lib/{defaultMap.d.ts → defaultMap.d.mts} +7 -6
  126. package/lib/defaultMap.d.mts.map +1 -0
  127. package/lib/{defaultMap.js → defaultMap.mjs} +28 -21
  128. package/lib/defaultMap.mjs.map +1 -0
  129. package/lib/{defaultMapInterfaces.d.ts → defaultMapInterfaces.d.mts} +44 -12
  130. package/lib/defaultMapInterfaces.d.mts.map +1 -0
  131. package/lib/defaultMapInterfaces.mjs +6 -0
  132. package/lib/defaultMapInterfaces.mjs.map +1 -0
  133. package/lib/index.d.mts +17 -0
  134. package/lib/index.d.mts.map +1 -0
  135. package/lib/index.mjs +16 -0
  136. package/lib/index.mjs.map +1 -0
  137. package/lib/intervalCollection.d.mts +569 -0
  138. package/lib/intervalCollection.d.mts.map +1 -0
  139. package/lib/intervalCollection.mjs +1144 -0
  140. package/lib/intervalCollection.mjs.map +1 -0
  141. package/lib/intervalIndex/endpointInRangeIndex.d.mts +34 -0
  142. package/lib/intervalIndex/endpointInRangeIndex.d.mts.map +1 -0
  143. package/lib/intervalIndex/endpointInRangeIndex.mjs +61 -0
  144. package/lib/intervalIndex/endpointInRangeIndex.mjs.map +1 -0
  145. package/lib/intervalIndex/endpointIndex.d.mts +38 -0
  146. package/lib/intervalIndex/endpointIndex.d.mts.map +1 -0
  147. package/lib/intervalIndex/endpointIndex.mjs +42 -0
  148. package/lib/intervalIndex/endpointIndex.mjs.map +1 -0
  149. package/lib/intervalIndex/idIntervalIndex.d.mts +18 -0
  150. package/lib/intervalIndex/idIntervalIndex.d.mts.map +1 -0
  151. package/lib/intervalIndex/idIntervalIndex.mjs +40 -0
  152. package/lib/intervalIndex/idIntervalIndex.mjs.map +1 -0
  153. package/lib/intervalIndex/index.d.mts +13 -0
  154. package/lib/intervalIndex/index.d.mts.map +1 -0
  155. package/lib/intervalIndex/index.mjs +11 -0
  156. package/lib/intervalIndex/index.mjs.map +1 -0
  157. package/lib/intervalIndex/intervalIndex.d.mts +30 -0
  158. package/lib/intervalIndex/intervalIndex.d.mts.map +1 -0
  159. package/lib/{defaultMapInterfaces.js → intervalIndex/intervalIndex.mjs} +1 -1
  160. package/lib/intervalIndex/intervalIndex.mjs.map +1 -0
  161. package/lib/intervalIndex/intervalIndexUtils.d.mts +17 -0
  162. package/lib/intervalIndex/intervalIndexUtils.d.mts.map +1 -0
  163. package/lib/intervalIndex/intervalIndexUtils.mjs +18 -0
  164. package/lib/intervalIndex/intervalIndexUtils.mjs.map +1 -0
  165. package/lib/intervalIndex/overlappingIntervalsIndex.d.mts +44 -0
  166. package/lib/intervalIndex/overlappingIntervalsIndex.d.mts.map +1 -0
  167. package/lib/intervalIndex/overlappingIntervalsIndex.mjs +111 -0
  168. package/lib/intervalIndex/overlappingIntervalsIndex.mjs.map +1 -0
  169. package/lib/intervalIndex/overlappingSequenceIntervalsIndex.d.mts +11 -0
  170. package/lib/intervalIndex/overlappingSequenceIntervalsIndex.d.mts.map +1 -0
  171. package/lib/intervalIndex/overlappingSequenceIntervalsIndex.mjs +37 -0
  172. package/lib/intervalIndex/overlappingSequenceIntervalsIndex.mjs.map +1 -0
  173. package/lib/intervalIndex/sequenceIntervalIndexes.d.mts +35 -0
  174. package/lib/intervalIndex/sequenceIntervalIndexes.d.mts.map +1 -0
  175. package/lib/intervalIndex/sequenceIntervalIndexes.mjs +6 -0
  176. package/lib/intervalIndex/sequenceIntervalIndexes.mjs.map +1 -0
  177. package/lib/intervalIndex/startpointInRangeIndex.d.mts +34 -0
  178. package/lib/intervalIndex/startpointInRangeIndex.d.mts.map +1 -0
  179. package/lib/intervalIndex/startpointInRangeIndex.mjs +61 -0
  180. package/lib/intervalIndex/startpointInRangeIndex.mjs.map +1 -0
  181. package/lib/intervalTree.d.mts +24 -0
  182. package/lib/intervalTree.d.mts.map +1 -0
  183. package/lib/intervalTree.mjs +76 -0
  184. package/lib/intervalTree.mjs.map +1 -0
  185. package/lib/intervals/index.d.mts +8 -0
  186. package/lib/intervals/index.d.mts.map +1 -0
  187. package/lib/intervals/index.mjs +8 -0
  188. package/lib/intervals/index.mjs.map +1 -0
  189. package/lib/intervals/interval.d.mts +84 -0
  190. package/lib/intervals/interval.d.mts.map +1 -0
  191. package/lib/intervals/interval.mjs +176 -0
  192. package/lib/intervals/interval.mjs.map +1 -0
  193. package/lib/intervals/intervalUtils.d.mts +230 -0
  194. package/lib/intervals/intervalUtils.d.mts.map +1 -0
  195. package/lib/intervals/intervalUtils.mjs +77 -0
  196. package/lib/intervals/intervalUtils.mjs.map +1 -0
  197. package/lib/intervals/sequenceInterval.d.mts +137 -0
  198. package/lib/intervals/sequenceInterval.d.mts.map +1 -0
  199. package/lib/intervals/sequenceInterval.mjs +370 -0
  200. package/lib/intervals/sequenceInterval.mjs.map +1 -0
  201. package/lib/{localValues.d.ts → localValues.d.mts} +3 -2
  202. package/lib/localValues.d.mts.map +1 -0
  203. package/lib/{localValues.js → localValues.mjs} +2 -2
  204. package/lib/localValues.mjs.map +1 -0
  205. package/lib/{packageVersion.d.ts → packageVersion.d.mts} +1 -1
  206. package/lib/{packageVersion.d.ts.map → packageVersion.d.mts.map} +1 -1
  207. package/lib/{packageVersion.js → packageVersion.mjs} +2 -2
  208. package/lib/packageVersion.mjs.map +1 -0
  209. package/lib/revertibles.d.mts +86 -0
  210. package/lib/revertibles.d.mts.map +1 -0
  211. package/lib/revertibles.mjs +416 -0
  212. package/lib/revertibles.mjs.map +1 -0
  213. package/lib/sequence-alpha.d.mts +1315 -0
  214. package/lib/sequence-beta.d.mts +244 -0
  215. package/lib/sequence-public.d.mts +244 -0
  216. package/lib/sequence-untrimmed.d.mts +1803 -0
  217. package/lib/{sequence.d.ts → sequence.d.mts} +127 -50
  218. package/lib/sequence.d.mts.map +1 -0
  219. package/lib/{sequence.js → sequence.mjs} +225 -152
  220. package/lib/sequence.mjs.map +1 -0
  221. package/lib/{sequenceDeltaEvent.d.ts → sequenceDeltaEvent.d.mts} +24 -7
  222. package/lib/sequenceDeltaEvent.d.mts.map +1 -0
  223. package/lib/{sequenceDeltaEvent.js → sequenceDeltaEvent.mjs} +20 -8
  224. package/lib/sequenceDeltaEvent.mjs.map +1 -0
  225. package/lib/sequenceFactory.d.mts +22 -0
  226. package/lib/sequenceFactory.d.mts.map +1 -0
  227. package/lib/sequenceFactory.mjs +51 -0
  228. package/lib/sequenceFactory.mjs.map +1 -0
  229. package/lib/{sharedIntervalCollection.d.ts → sharedIntervalCollection.d.mts} +12 -12
  230. package/lib/sharedIntervalCollection.d.mts.map +1 -0
  231. package/lib/{sharedIntervalCollection.js → sharedIntervalCollection.mjs} +16 -21
  232. package/lib/sharedIntervalCollection.mjs.map +1 -0
  233. package/lib/{sharedSequence.d.ts → sharedSequence.d.mts} +15 -3
  234. package/lib/sharedSequence.d.mts.map +1 -0
  235. package/lib/{sharedSequence.js → sharedSequence.mjs} +30 -23
  236. package/lib/sharedSequence.mjs.map +1 -0
  237. package/lib/{sharedString.d.ts → sharedString.d.mts} +60 -24
  238. package/lib/sharedString.d.mts.map +1 -0
  239. package/lib/sharedString.mjs +281 -0
  240. package/lib/sharedString.mjs.map +1 -0
  241. package/package.json +146 -75
  242. package/prettier.config.cjs +8 -0
  243. package/sequence.test-files.tar +0 -0
  244. package/src/defaultMap.ts +417 -403
  245. package/src/defaultMapInterfaces.ts +157 -117
  246. package/src/index.ts +86 -26
  247. package/src/intervalCollection.ts +2043 -1563
  248. package/src/intervalIndex/endpointInRangeIndex.ts +116 -0
  249. package/src/intervalIndex/endpointIndex.ts +91 -0
  250. package/src/intervalIndex/idIntervalIndex.ts +64 -0
  251. package/src/intervalIndex/index.ts +25 -0
  252. package/src/intervalIndex/intervalIndex.ts +32 -0
  253. package/src/intervalIndex/intervalIndexUtils.ts +27 -0
  254. package/src/intervalIndex/overlappingIntervalsIndex.ts +187 -0
  255. package/src/intervalIndex/overlappingSequenceIntervalsIndex.ts +80 -0
  256. package/src/intervalIndex/sequenceIntervalIndexes.ts +34 -0
  257. package/src/intervalIndex/startpointInRangeIndex.ts +114 -0
  258. package/src/intervalTree.ts +98 -0
  259. package/src/intervals/index.ts +25 -0
  260. package/src/intervals/interval.ts +238 -0
  261. package/src/intervals/intervalUtils.ts +288 -0
  262. package/src/intervals/sequenceInterval.ts +616 -0
  263. package/src/localValues.ts +68 -73
  264. package/src/packageVersion.ts +1 -1
  265. package/src/revertibles.ts +693 -0
  266. package/src/sequence.ts +845 -690
  267. package/src/sequenceDeltaEvent.ts +164 -131
  268. package/src/sequenceFactory.ts +58 -214
  269. package/src/sharedIntervalCollection.ts +161 -152
  270. package/src/sharedSequence.ts +181 -167
  271. package/src/sharedString.ts +390 -234
  272. package/tsc-multi.test.json +10 -0
  273. package/tsconfig.json +11 -13
  274. package/.editorconfig +0 -7
  275. package/.vscode/launch.json +0 -15
  276. package/dist/defaultMap.js.map +0 -1
  277. package/dist/defaultMapInterfaces.js.map +0 -1
  278. package/dist/index.js +0 -44
  279. package/dist/index.js.map +0 -1
  280. package/dist/intervalCollection.js +0 -1250
  281. package/dist/intervalCollection.js.map +0 -1
  282. package/dist/localValues.js.map +0 -1
  283. package/dist/packageVersion.js.map +0 -1
  284. package/dist/sequence.js.map +0 -1
  285. package/dist/sequenceDeltaEvent.js.map +0 -1
  286. package/dist/sequenceFactory.js +0 -192
  287. package/dist/sequenceFactory.js.map +0 -1
  288. package/dist/sharedIntervalCollection.js.map +0 -1
  289. package/dist/sharedNumberSequence.d.ts +0 -50
  290. package/dist/sharedNumberSequence.d.ts.map +0 -1
  291. package/dist/sharedNumberSequence.js +0 -61
  292. package/dist/sharedNumberSequence.js.map +0 -1
  293. package/dist/sharedObjectSequence.d.ts +0 -50
  294. package/dist/sharedObjectSequence.d.ts.map +0 -1
  295. package/dist/sharedObjectSequence.js +0 -61
  296. package/dist/sharedObjectSequence.js.map +0 -1
  297. package/dist/sharedSequence.js.map +0 -1
  298. package/dist/sharedString.js +0 -187
  299. package/dist/sharedString.js.map +0 -1
  300. package/dist/sparsematrix.d.ts +0 -139
  301. package/dist/sparsematrix.d.ts.map +0 -1
  302. package/dist/sparsematrix.js +0 -332
  303. package/dist/sparsematrix.js.map +0 -1
  304. package/lib/defaultMap.d.ts.map +0 -1
  305. package/lib/defaultMap.js.map +0 -1
  306. package/lib/defaultMapInterfaces.d.ts.map +0 -1
  307. package/lib/defaultMapInterfaces.js.map +0 -1
  308. package/lib/index.d.ts +0 -27
  309. package/lib/index.d.ts.map +0 -1
  310. package/lib/index.js +0 -26
  311. package/lib/index.js.map +0 -1
  312. package/lib/intervalCollection.d.ts +0 -270
  313. package/lib/intervalCollection.d.ts.map +0 -1
  314. package/lib/intervalCollection.js +0 -1238
  315. package/lib/intervalCollection.js.map +0 -1
  316. package/lib/localValues.d.ts.map +0 -1
  317. package/lib/localValues.js.map +0 -1
  318. package/lib/packageVersion.js.map +0 -1
  319. package/lib/sequence.d.ts.map +0 -1
  320. package/lib/sequence.js.map +0 -1
  321. package/lib/sequenceDeltaEvent.d.ts.map +0 -1
  322. package/lib/sequenceDeltaEvent.js.map +0 -1
  323. package/lib/sequenceFactory.d.ts +0 -108
  324. package/lib/sequenceFactory.d.ts.map +0 -1
  325. package/lib/sequenceFactory.js +0 -186
  326. package/lib/sequenceFactory.js.map +0 -1
  327. package/lib/sharedIntervalCollection.d.ts.map +0 -1
  328. package/lib/sharedIntervalCollection.js.map +0 -1
  329. package/lib/sharedNumberSequence.d.ts +0 -50
  330. package/lib/sharedNumberSequence.d.ts.map +0 -1
  331. package/lib/sharedNumberSequence.js +0 -57
  332. package/lib/sharedNumberSequence.js.map +0 -1
  333. package/lib/sharedObjectSequence.d.ts +0 -50
  334. package/lib/sharedObjectSequence.d.ts.map +0 -1
  335. package/lib/sharedObjectSequence.js +0 -57
  336. package/lib/sharedObjectSequence.js.map +0 -1
  337. package/lib/sharedSequence.d.ts.map +0 -1
  338. package/lib/sharedSequence.js.map +0 -1
  339. package/lib/sharedString.d.ts.map +0 -1
  340. package/lib/sharedString.js +0 -183
  341. package/lib/sharedString.js.map +0 -1
  342. package/lib/sparsematrix.d.ts +0 -139
  343. package/lib/sparsematrix.d.ts.map +0 -1
  344. package/lib/sparsematrix.js +0 -323
  345. package/lib/sparsematrix.js.map +0 -1
  346. package/src/sharedNumberSequence.ts +0 -62
  347. package/src/sharedObjectSequence.ts +0 -62
  348. package/src/sparsematrix.ts +0 -421
  349. package/tsconfig.esnext.json +0 -7
package/src/sequence.ts CHANGED
@@ -2,62 +2,63 @@
2
2
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
- import { Deferred, bufferToString, assert } from "@fluidframework/common-utils";
6
- import { ChildLogger } from "@fluidframework/telemetry-utils";
7
- import {
8
- ISequencedDocumentMessage,
9
- MessageType,
10
- } from "@fluidframework/protocol-definitions";
5
+
6
+ import { assert, Deferred } from "@fluidframework/core-utils";
7
+ import { bufferToString } from "@fluid-internal/client-utils";
8
+ import { LoggingError, createChildLogger } from "@fluidframework/telemetry-utils";
9
+ import { ISequencedDocumentMessage, MessageType } from "@fluidframework/protocol-definitions";
11
10
  import {
12
- IChannelAttributes,
13
- IFluidDataStoreRuntime,
14
- IChannelStorageService,
11
+ IChannelAttributes,
12
+ IFluidDataStoreRuntime,
13
+ IChannelStorageService,
15
14
  } from "@fluidframework/datastore-definitions";
16
15
  import {
17
- Client,
18
- createAnnotateRangeOp,
19
- createGroupOp,
20
- createInsertOp,
21
- createRemoveRangeOp,
22
- ICombiningOp,
23
- IJSONSegment,
24
- IMergeTreeAnnotateMsg,
25
- IMergeTreeDeltaOp,
26
- IMergeTreeGroupMsg,
27
- IMergeTreeOp,
28
- IMergeTreeRemoveMsg,
29
- IRelativePosition,
30
- ISegment,
31
- ISegmentAction,
32
- LocalReference,
33
- LocalReferencePosition,
34
- matchProperties,
35
- MergeTreeDeltaType,
36
- PropertySet,
37
- RangeStackMap,
38
- ReferencePosition,
39
- ReferenceType,
40
- SegmentGroup,
16
+ // eslint-disable-next-line import/no-deprecated
17
+ Client,
18
+ createAnnotateRangeOp,
19
+ // eslint-disable-next-line import/no-deprecated
20
+ createGroupOp,
21
+ createInsertOp,
22
+ createRemoveRangeOp,
23
+ IJSONSegment,
24
+ IMergeTreeAnnotateMsg,
25
+ IMergeTreeDeltaOp,
26
+ IMergeTreeGroupMsg,
27
+ IMergeTreeOp,
28
+ IMergeTreeRemoveMsg,
29
+ IRelativePosition,
30
+ ISegment,
31
+ ISegmentAction,
32
+ LocalReferencePosition,
33
+ matchProperties,
34
+ MergeTreeDeltaType,
35
+ PropertySet,
36
+ ReferencePosition,
37
+ ReferenceType,
38
+ MergeTreeRevertibleDriver,
39
+ SegmentGroup,
40
+ IMergeTreeObliterateMsg,
41
+ createObliterateRangeOp,
42
+ SlidingPreference,
41
43
  } from "@fluidframework/merge-tree";
42
44
  import { ObjectStoragePartition, SummaryTreeBuilder } from "@fluidframework/runtime-utils";
43
45
  import {
44
- IFluidSerializer,
45
- makeHandlesSerializable,
46
- parseHandles,
47
- SharedObject,
48
- ISharedObjectEvents,
49
- SummarySerializer,
46
+ IFluidSerializer,
47
+ makeHandlesSerializable,
48
+ parseHandles,
49
+ SharedObject,
50
+ ISharedObjectEvents,
50
51
  } from "@fluidframework/shared-object-base";
51
- import { IEventThisPlaceHolder } from "@fluidframework/common-definitions";
52
+ import { IEventThisPlaceHolder } from "@fluidframework/core-interfaces";
52
53
  import { ISummaryTreeWithStats, ITelemetryContext } from "@fluidframework/runtime-definitions";
53
-
54
+ import { DefaultMap, IMapOperation } from "./defaultMap";
55
+ import { IMapMessageLocalMetadata, IValueChanged } from "./defaultMapInterfaces";
56
+ import { SequenceInterval } from "./intervals";
54
57
  import {
55
- IntervalCollection,
56
- SequenceInterval,
57
- SequenceIntervalCollectionValueType,
58
+ IIntervalCollection,
59
+ IntervalCollection,
60
+ SequenceIntervalCollectionValueType,
58
61
  } from "./intervalCollection";
59
- import { DefaultMap } from "./defaultMap";
60
- import { IMapMessageLocalMetadata, IValueChanged } from "./defaultMapInterfaces";
61
62
  import { SequenceDeltaEvent, SequenceMaintenanceEvent } from "./sequenceDeltaEvent";
62
63
  import { ISharedIntervalCollection } from "./sharedIntervalCollection";
63
64
 
@@ -67,7 +68,7 @@ const contentPath = "content";
67
68
  /**
68
69
  * Events emitted in response to changes to the sequence data.
69
70
  *
70
- * @remarks
71
+ * @remarks
71
72
  *
72
73
  * The following is the list of events emitted.
73
74
  *
@@ -96,652 +97,806 @@ const contentPath = "content";
96
97
  * - `event` - Various information on the segments that were modified.
97
98
  *
98
99
  * - `target` - The sequence itself.
100
+ * @alpha
99
101
  */
100
102
  export interface ISharedSegmentSequenceEvents extends ISharedObjectEvents {
101
- (event: "createIntervalCollection",
102
- listener: (label: string, local: boolean, target: IEventThisPlaceHolder) => void);
103
- (event: "sequenceDelta", listener: (event: SequenceDeltaEvent, target: IEventThisPlaceHolder) => void);
104
- (event: "maintenance",
105
- listener: (event: SequenceMaintenanceEvent, target: IEventThisPlaceHolder) => void);
103
+ (
104
+ event: "createIntervalCollection",
105
+ listener: (label: string, local: boolean, target: IEventThisPlaceHolder) => void,
106
+ ): void;
107
+ (
108
+ event: "sequenceDelta",
109
+ listener: (event: SequenceDeltaEvent, target: IEventThisPlaceHolder) => void,
110
+ ): void;
111
+ (
112
+ event: "maintenance",
113
+ listener: (event: SequenceMaintenanceEvent, target: IEventThisPlaceHolder) => void,
114
+ ): void;
106
115
  }
107
116
 
117
+ /**
118
+ * @alpha
119
+ */
108
120
  export abstract class SharedSegmentSequence<T extends ISegment>
109
- extends SharedObject<ISharedSegmentSequenceEvents>
110
- implements ISharedIntervalCollection<SequenceInterval> {
111
- get loaded(): Promise<void> {
112
- return this.loadedDeferred.promise;
113
- }
114
-
115
- private static createOpsFromDelta(event: SequenceDeltaEvent): IMergeTreeDeltaOp[] {
116
- const ops: IMergeTreeDeltaOp[] = [];
117
- for (const r of event.ranges) {
118
- switch (event.deltaOperation) {
119
- case MergeTreeDeltaType.ANNOTATE: {
120
- const lastAnnotate = ops[ops.length - 1] as IMergeTreeAnnotateMsg;
121
- const props = {};
122
- for (const key of Object.keys(r.propertyDeltas)) {
123
- props[key] = r.segment.properties?.[key] ?? null;
124
- }
125
- if (lastAnnotate && lastAnnotate.pos2 === r.position &&
126
- matchProperties(lastAnnotate.props, props)) {
127
- lastAnnotate.pos2 += r.segment.cachedLength;
128
- } else {
129
- ops.push(createAnnotateRangeOp(
130
- r.position,
131
- r.position + r.segment.cachedLength,
132
- props,
133
- undefined));
134
- }
135
- break;
136
- }
137
-
138
- case MergeTreeDeltaType.INSERT:
139
- ops.push(createInsertOp(
140
- r.position,
141
- r.segment.clone().toJSONObject()));
142
- break;
143
-
144
- case MergeTreeDeltaType.REMOVE: {
145
- const lastRem = ops[ops.length - 1] as IMergeTreeRemoveMsg;
146
- if (lastRem?.pos1 === r.position) {
147
- lastRem.pos2 += r.segment.cachedLength;
148
- } else {
149
- ops.push(createRemoveRangeOp(
150
- r.position,
151
- r.position + r.segment.cachedLength));
152
- }
153
- break;
154
- }
155
-
156
- default:
157
- }
158
- }
159
- return ops;
160
- }
161
-
162
- protected client: Client;
163
- // Deferred that triggers once the object is loaded
164
- protected loadedDeferred = new Deferred<void>();
165
- // cache out going ops created when partial loading
166
- private readonly loadedDeferredOutgoingOps:
167
- [IMergeTreeOp, SegmentGroup | SegmentGroup[]][] = [];
168
- // cache incoming ops that arrive when partial loading
169
- private deferIncomingOps = true;
170
- private readonly loadedDeferredIncomingOps: ISequencedDocumentMessage[] = [];
171
-
172
- private messagesSinceMSNChange: ISequencedDocumentMessage[] = [];
173
- private readonly intervalCollections: DefaultMap<IntervalCollection<SequenceInterval>>;
174
- constructor(
175
- private readonly dataStoreRuntime: IFluidDataStoreRuntime,
176
- public id: string,
177
- attributes: IChannelAttributes,
178
- public readonly segmentFromSpec: (spec: IJSONSegment) => ISegment,
179
- ) {
180
- super(id, dataStoreRuntime, attributes, "fluid_sequence_");
181
-
182
- this.loadedDeferred.promise.catch((error) => {
183
- this.logger.sendErrorEvent({ eventName: "SequenceLoadFailed" }, error);
184
- });
185
-
186
- this.client = new Client(
187
- segmentFromSpec,
188
- ChildLogger.create(this.logger, "SharedSegmentSequence.MergeTreeClient"),
189
- dataStoreRuntime.options);
190
-
191
- super.on("newListener", (event) => {
192
- switch (event) {
193
- case "sequenceDelta":
194
- if (!this.client.mergeTreeDeltaCallback) {
195
- this.client.mergeTreeDeltaCallback = (opArgs, deltaArgs) => {
196
- this.emit("sequenceDelta", new SequenceDeltaEvent(opArgs, deltaArgs, this.client), this);
197
- };
198
- }
199
- break;
200
- case "maintenance":
201
- if (!this.client.mergeTreeMaintenanceCallback) {
202
- this.client.mergeTreeMaintenanceCallback = (args, opArgs) => {
203
- this.emit("maintenance", new SequenceMaintenanceEvent(opArgs, args, this.client), this);
204
- };
205
- }
206
- break;
207
- default:
208
- }
209
- });
210
- super.on("removeListener", (event: string | symbol) => {
211
- switch (event) {
212
- case "sequenceDelta":
213
- if (super.listenerCount(event) === 0) {
214
- this.client.mergeTreeDeltaCallback = undefined;
215
- }
216
- break;
217
- case "maintenance":
218
- if (super.listenerCount(event) === 0) {
219
- this.client.mergeTreeMaintenanceCallback = undefined;
220
- }
221
- break;
222
- default:
223
- break;
224
- }
225
- });
226
-
227
- this.intervalCollections = new DefaultMap(
228
- this.serializer,
229
- this.handle,
230
- (op, localOpMetadata) => this.submitLocalMessage(op, localOpMetadata),
231
- new SequenceIntervalCollectionValueType(),
232
- );
233
- }
234
-
235
- /**
236
- * @param start - The inclusive start of the range to remove
237
- * @param end - The exclusive end of the range to remove
238
- */
239
- public removeRange(start: number, end: number): IMergeTreeRemoveMsg {
240
- const removeOp = this.client.removeRangeLocal(start, end);
241
- if (removeOp) {
242
- this.submitSequenceMessage(removeOp);
243
- }
244
- return removeOp;
245
- }
246
-
247
- public groupOperation(groupOp: IMergeTreeGroupMsg) {
248
- this.client.localTransaction(groupOp);
249
- this.submitSequenceMessage(groupOp);
250
- }
251
-
252
- public getContainingSegment(pos: number) {
253
- return this.client.getContainingSegment<T>(pos);
254
- }
255
-
256
- /**
257
- * Returns the length of the current sequence for the client
258
- */
259
- public getLength() {
260
- return this.client.getLength();
261
- }
262
-
263
- /**
264
- * Returns the current position of a segment, and -1 if the segment
265
- * does not exist in this sequence
266
- * @param segment - The segment to get the position of
267
- */
268
- public getPosition(segment: ISegment): number {
269
- return this.client.getPosition(segment);
270
- }
271
-
272
- /**
273
- * Annotates the range with the provided properties
274
- *
275
- * @param start - The inclusive start position of the range to annotate
276
- * @param end - The exclusive end position of the range to annotate
277
- * @param props - The properties to annotate the range with
278
- * @param combiningOp - Optional. Specifies how to combine values for the property, such as "incr" for increment.
279
- *
280
- */
281
- public annotateRange(
282
- start: number,
283
- end: number,
284
- props: PropertySet,
285
- combiningOp?: ICombiningOp) {
286
- const annotateOp =
287
- this.client.annotateRangeLocal(start, end, props, combiningOp);
288
- if (annotateOp) {
289
- this.submitSequenceMessage(annotateOp);
290
- }
291
- }
292
-
293
- public getPropertiesAtPosition(pos: number) {
294
- return this.client.getPropertiesAtPosition(pos);
295
- }
296
-
297
- public getRangeExtentsOfPosition(pos: number) {
298
- return this.client.getRangeExtentsOfPosition(pos);
299
- }
300
-
301
- /**
302
- * @deprecated - use createLocalReferencePosition
303
- */
304
- public createPositionReference(
305
- segment: T,
306
- offset: number,
307
- refType: ReferenceType): LocalReference {
308
- const lref = new LocalReference(this.client, segment, offset, refType);
309
- if (refType !== ReferenceType.Transient) {
310
- this.addLocalReference(lref);
311
- }
312
- return lref;
313
- }
314
-
315
- public createLocalReferencePosition(
316
- segment: T,
317
- offset: number,
318
- refType: ReferenceType,
319
- properties: PropertySet | undefined): LocalReferencePosition {
320
- return this.client.createLocalReferencePosition(
321
- segment,
322
- offset,
323
- refType,
324
- properties);
325
- }
326
-
327
- /**
328
- * @deprecated - use localReferencePositionToPosition
329
- */
330
- public localRefToPos(localRef: LocalReference) {
331
- return this.client.localReferencePositionToPosition(localRef);
332
- }
333
-
334
- public localReferencePositionToPosition(lref: ReferencePosition): number {
335
- return this.client.localReferencePositionToPosition(lref);
336
- }
337
-
338
- /**
339
- * Resolves a remote client's position against the local sequence
340
- * and returns the remote client's position relative to the local
341
- * sequence. The client ref seq must be above the minimum sequence number
342
- * or the return value will be undefined.
343
- * Generally this method is used in conjunction with signals which provide
344
- * point in time values for the below parameters, and is useful for things
345
- * like displaying user position. It should not be used with persisted values
346
- * as persisted values will quickly become invalid as the remoteClientRefSeq
347
- * moves below the minimum sequence number
348
- * @param remoteClientPosition - The remote client's position to resolve
349
- * @param remoteClientRefSeq - The reference sequence number of the remote client
350
- * @param remoteClientId - The client id of the remote client
351
- */
352
- public resolveRemoteClientPosition(
353
- remoteClientPosition: number,
354
- remoteClientRefSeq: number,
355
- remoteClientId: string): number {
356
- return this.client.resolveRemoteClientPosition(
357
- remoteClientPosition,
358
- remoteClientRefSeq,
359
- remoteClientId);
360
- }
361
-
362
- public submitSequenceMessage(message: IMergeTreeOp) {
363
- if (!this.isAttached()) {
364
- return;
365
- }
366
- const translated = makeHandlesSerializable(message, this.serializer, this.handle);
367
- const metadata = this.client.peekPendingSegmentGroups(
368
- message.type === MergeTreeDeltaType.GROUP ? message.ops.length : 1);
369
-
370
- // if loading isn't complete, we need to cache
371
- // local ops until loading is complete, and then
372
- // they will be resent
373
- if (!this.loadedDeferred.isCompleted) {
374
- this.loadedDeferredOutgoingOps.push([translated, metadata]);
375
- } else {
376
- this.submitLocalMessage(translated, metadata);
377
- }
378
- }
379
-
380
- /**
381
- * @deprecated - use createLocalReferencePosition
382
- */
383
- public addLocalReference(lref: LocalReference) {
384
- return this.client.addLocalReference(lref);
385
- }
386
-
387
- /**
388
- * @deprecated - use removeLocalReferencePosition
389
- */
390
- public removeLocalReference(lref: LocalReference) {
391
- return this.client.removeLocalReferencePosition(lref);
392
- }
393
-
394
- public removeLocalReferencePosition(lref: LocalReferencePosition) {
395
- return this.client.removeLocalReferencePosition(lref);
396
- }
397
-
398
- /**
399
- * Given a position specified relative to a marker id, lookup the marker
400
- * and convert the position to a character position.
401
- * @param relativePos - Id of marker (may be indirect) and whether position is before or after marker.
402
- */
403
- public posFromRelativePos(relativePos: IRelativePosition) {
404
- return this.client.posFromRelativePos(relativePos);
405
- }
406
-
407
- /**
408
- * Walk the underlying segments of the sequence.
409
- * The walked segments may extend beyond the range
410
- * if the segments cross the ranges start or end boundaries.
411
- * Set split range to true to ensure only segments within the
412
- * range are walked.
413
- *
414
- * @param handler - The function to handle each segment
415
- * @param start - Optional. The start of range walk.
416
- * @param end - Optional. The end of range walk
417
- * @param accum - Optional. An object that will be passed to the handler for accumulation
418
- * @param splitRange - Optional. Splits boundary segments on the range boundaries
419
- */
420
- public walkSegments<TClientData>(
421
- handler: ISegmentAction<TClientData>,
422
- start?: number, end?: number, accum?: TClientData,
423
- splitRange: boolean = false) {
424
- return this.client.walkSegments<TClientData>(handler, start, end, accum, splitRange);
425
- }
426
-
427
- public getStackContext(startPos: number, rangeLabels: string[]): RangeStackMap {
428
- return this.client.getStackContext(startPos, rangeLabels);
429
- }
430
-
431
- public getCurrentSeq() {
432
- return this.client.getCurrentSeq();
433
- }
434
-
435
- public insertAtReferencePosition(pos: ReferencePosition, segment: T) {
436
- const insertOp = this.client.insertAtReferencePositionLocal(pos, segment);
437
- if (insertOp) {
438
- this.submitSequenceMessage(insertOp);
439
- }
440
- }
441
-
442
- /**
443
- * @deprecated - IntervalCollections are created on a first-write wins basis, and concurrent creates
444
- * are supported. Use `getIntervalCollection` instead.
445
- */
446
- public async waitIntervalCollection(
447
- label: string,
448
- ): Promise<IntervalCollection<SequenceInterval>> {
449
- return this.intervalCollections.get(label);
450
- }
451
-
452
- public getIntervalCollection(label: string): IntervalCollection<SequenceInterval> {
453
- return this.intervalCollections.get(label);
454
- }
455
-
456
- /**
457
- * @returns an iterable object that enumerates the IntervalCollection labels
458
- * Usage:
459
- * const iter = this.getIntervalCollectionKeys();
460
- * for (key of iter)
461
- * const collection = this.getIntervalCollection(key);
462
- * ...
463
- */
464
- public getIntervalCollectionLabels(): IterableIterator<string> {
465
- return this.intervalCollections.keys();
466
- }
467
-
468
- protected summarizeCore(
469
- serializer: IFluidSerializer,
470
- telemetryContext?: ITelemetryContext,
471
- ): ISummaryTreeWithStats {
472
- const builder = new SummaryTreeBuilder();
473
-
474
- // conditionally write the interval collection blob
475
- // only if it has entries
476
- if (this.intervalCollections.size > 0) {
477
- builder.addBlob(snapshotFileName, this.intervalCollections.serialize(serializer));
478
- }
479
-
480
- builder.addWithStats(contentPath, this.summarizeMergeTree(serializer));
481
-
482
- return builder.getSummaryTree();
483
- }
484
-
485
- /**
486
- * Runs serializer over the GC data for this SharedMatrix.
487
- * All the IFluidHandle's represent routes to other objects.
488
- */
489
- protected processGCDataCore(serializer: SummarySerializer) {
490
- if (this.intervalCollections.size > 0) {
491
- this.intervalCollections.serialize(serializer);
492
- }
493
-
494
- this.client.serializeGCData(this.handle, serializer);
495
- }
496
-
497
- /**
498
- * Replace the range specified from start to end with the provided segment
499
- * This is done by inserting the segment at the end of the range, followed
500
- * by removing the contents of the range
501
- * For a zero or reverse range (start \>= end), insert at end do not remove anything
502
- * @param start - The start of the range to replace
503
- * @param end - The end of the range to replace
504
- * @param segment - The segment that will replace the range
505
- */
506
- protected replaceRange(start: number, end: number, segment: ISegment) {
507
- // Insert at the max end of the range when start > end, but still remove the range later
508
- const insertIndex: number = Math.max(start, end);
509
-
510
- // Insert first, so local references can slide to the inserted seg if any
511
- const insert = this.client.insertSegmentLocal(insertIndex, segment);
512
- if (insert) {
513
- if (start < end) {
514
- const remove = this.client.removeRangeLocal(start, end);
515
- this.submitSequenceMessage(createGroupOp(insert, remove));
516
- } else {
517
- this.submitSequenceMessage(insert);
518
- }
519
- }
520
- }
521
-
522
- protected onConnect() {
523
- // Update merge tree collaboration information with new client ID and then resend pending ops
524
- this.client.startOrUpdateCollaboration(this.runtime.clientId);
525
- }
526
-
527
- protected onDisconnect() { }
528
-
529
- protected reSubmitCore(content: any, localOpMetadata: unknown) {
530
- if (!this.intervalCollections.tryResubmitMessage(content, localOpMetadata as IMapMessageLocalMetadata)) {
531
- this.submitSequenceMessage(
532
- this.client.regeneratePendingOp(
533
- content as IMergeTreeOp,
534
- localOpMetadata as SegmentGroup | SegmentGroup[]));
535
- }
536
- }
537
-
538
- /**
539
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
540
- */
541
- protected async loadCore(storage: IChannelStorageService) {
542
- if (await storage.contains(snapshotFileName)) {
543
- const blob = await storage.readBlob(snapshotFileName);
544
- const header = bufferToString(blob, "utf8");
545
- this.intervalCollections.populate(header);
546
- }
547
-
548
- try {
549
- // this will load the header, and return a promise
550
- // that will resolve when the body is loaded
551
- // and the catchup ops are available.
552
- const { catchupOpsP } = await this.client.load(
553
- this.runtime,
554
- new ObjectStoragePartition(storage, contentPath),
555
- this.serializer);
556
-
557
- // setup a promise to process the
558
- // catch up ops, and finishing the loading process
559
- const loadCatchUpOps = catchupOpsP
560
- .then((msgs) => {
561
- msgs.forEach((m) => {
562
- const collabWindow = this.client.getCollabWindow();
563
- if (m.minimumSequenceNumber < collabWindow.minSeq
564
- || m.referenceSequenceNumber < collabWindow.minSeq
565
- || m.sequenceNumber <= collabWindow.minSeq
566
- || m.sequenceNumber <= collabWindow.currentSeq) {
567
- throw new Error(`Invalid catchup operations in snapshot: ${JSON.stringify({
568
- op: {
569
- seq: m.sequenceNumber,
570
- minSeq: m.minimumSequenceNumber,
571
- refSeq: m.referenceSequenceNumber,
572
- },
573
- collabWindow: {
574
- seq: collabWindow.currentSeq,
575
- minSeq: collabWindow.minSeq,
576
- },
577
- })}`);
578
- }
579
- this.processMergeTreeMsg(m);
580
- });
581
- this.loadFinished();
582
- })
583
- .catch((error) => {
584
- this.loadFinished(error);
585
- });
586
- if (this.dataStoreRuntime.options?.sequenceInitializeFromHeaderOnly !== true) {
587
- // if we not doing partial load, await the catch up ops,
588
- // and the finalization of the load
589
- await loadCatchUpOps;
590
- }
591
- } catch (error) {
592
- this.loadFinished(error);
593
- }
594
- }
595
-
596
- protected processCore(message: ISequencedDocumentMessage, local: boolean, localOpMetadata: unknown) {
597
- // if loading isn't complete, we need to cache all
598
- // incoming ops to be applied after loading is complete
599
- if (this.deferIncomingOps) {
600
- assert(!local, 0x072 /* "Unexpected local op when loading not finished" */);
601
- this.loadedDeferredIncomingOps.push(message);
602
- } else {
603
- assert(message.type === MessageType.Operation, 0x073 /* "Sequence message not operation" */);
604
-
605
- const handled = this.intervalCollections.tryProcessMessage(
606
- message.contents,
607
- local,
608
- message,
609
- localOpMetadata,
610
- );
611
-
612
- if (!handled) {
613
- this.processMergeTreeMsg(message, local);
614
- }
615
- }
616
- }
617
-
618
- protected didAttach() {
619
- // If we are not local, and we've attached we need to start generating and sending ops
620
- // so start collaboration and provide a default client id incase we are not connected
621
- if (this.isAttached()) {
622
- this.client.startOrUpdateCollaboration(this.runtime.clientId ?? "attached");
623
- }
624
- }
625
-
626
- protected initializeLocalCore() {
627
- super.initializeLocalCore();
628
- this.loadFinished();
629
- }
630
-
631
- /**
632
- * {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
633
- */
634
- protected applyStashedOp(content: any): unknown {
635
- return this.client.applyStashedOp(content);
636
- }
637
-
638
- private summarizeMergeTree(serializer: IFluidSerializer): ISummaryTreeWithStats {
639
- // Are we fully loaded? If not, things will go south
640
- assert(this.loadedDeferred.isCompleted, 0x074 /* "Snapshot called when not fully loaded" */);
641
- const minSeq = this.runtime.deltaManager.minimumSequenceNumber;
642
-
643
- this.processMinSequenceNumberChanged(minSeq);
644
-
645
- this.messagesSinceMSNChange.forEach((m) => { m.minimumSequenceNumber = minSeq; });
646
-
647
- return this.client.summarize(this.runtime, this.handle, serializer, this.messagesSinceMSNChange);
648
- }
649
-
650
- private processMergeTreeMsg(rawMessage: ISequencedDocumentMessage, local?: boolean) {
651
- const message = parseHandles(rawMessage, this.serializer);
652
-
653
- const ops: IMergeTreeDeltaOp[] = [];
654
- function transformOps(event: SequenceDeltaEvent) {
655
- ops.push(...SharedSegmentSequence.createOpsFromDelta(event));
656
- }
657
- const needsTransformation = message.referenceSequenceNumber !== message.sequenceNumber - 1;
658
- let stashMessage: Readonly<ISequencedDocumentMessage> = message;
659
- if (this.runtime.options?.newMergeTreeSnapshotFormat !== true) {
660
- if (needsTransformation) {
661
- this.on("sequenceDelta", transformOps);
662
- }
663
- }
664
-
665
- this.client.applyMsg(message, local);
666
-
667
- if (this.runtime.options?.newMergeTreeSnapshotFormat !== true) {
668
- if (needsTransformation) {
669
- this.removeListener("sequenceDelta", transformOps);
670
- // shallow clone the message as we only overwrite top level properties,
671
- // like referenceSequenceNumber and content only
672
- stashMessage = {
673
- ...message,
674
- referenceSequenceNumber: stashMessage.sequenceNumber - 1,
675
- contents: ops.length !== 1 ? createGroupOp(...ops) : ops[0],
676
- };
677
- }
678
-
679
- this.messagesSinceMSNChange.push(stashMessage);
680
-
681
- // Do GC every once in a while...
682
- if (this.messagesSinceMSNChange.length > 20
683
- && this.messagesSinceMSNChange[20].sequenceNumber < message.minimumSequenceNumber) {
684
- this.processMinSequenceNumberChanged(message.minimumSequenceNumber);
685
- }
686
- }
687
- }
688
-
689
- private processMinSequenceNumberChanged(minSeq: number) {
690
- let index = 0;
691
- for (; index < this.messagesSinceMSNChange.length; index++) {
692
- if (this.messagesSinceMSNChange[index].sequenceNumber > minSeq) {
693
- break;
694
- }
695
- }
696
- if (index !== 0) {
697
- this.messagesSinceMSNChange = this.messagesSinceMSNChange.slice(index);
698
- }
699
- }
700
-
701
- private loadFinished(error?: any) {
702
- if (!this.loadedDeferred.isCompleted) {
703
- // Initialize the interval collections
704
- this.initializeIntervalCollections();
705
- if (error) {
706
- this.loadedDeferred.reject(error);
707
- throw error;
708
- } else {
709
- // it is important this series remains synchronous
710
- // first we stop deferring incoming ops, and apply then all
711
- this.deferIncomingOps = false;
712
- for (const message of this.loadedDeferredIncomingOps) {
713
- this.processCore(message, false, undefined);
714
- }
715
- this.loadedDeferredIncomingOps.length = 0;
716
-
717
- // then resolve the loaded promise
718
- // and resubmit all the outstanding ops, as the snapshot
719
- // is fully loaded, and all outstanding ops are applied
720
- this.loadedDeferred.resolve();
721
-
722
- for (const [messageContent, opMetadata] of this.loadedDeferredOutgoingOps) {
723
- this.reSubmitCore(messageContent, opMetadata);
724
- }
725
- this.loadedDeferredOutgoingOps.length = 0;
726
- }
727
- }
728
- }
729
-
730
- private initializeIntervalCollections() {
731
- // Listen and initialize new SharedIntervalCollections
732
- this.intervalCollections.eventEmitter.on("create", ({ key, previousValue }: IValueChanged, local: boolean) => {
733
- const intervalCollection = this.intervalCollections.get(key);
734
- if (!intervalCollection.attached) {
735
- intervalCollection.attachGraph(this.client, key);
736
- }
737
- assert(previousValue === undefined, 0x2c1 /* "Creating an interval collection that already exists?" */);
738
- this.emit("createIntervalCollection", key, local, this);
739
- });
740
-
741
- // Initialize existing SharedIntervalCollections
742
- for (const key of this.intervalCollections.keys()) {
743
- const intervalCollection = this.intervalCollections.get(key);
744
- intervalCollection.attachGraph(this.client, key);
745
- }
746
- }
121
+ extends SharedObject<ISharedSegmentSequenceEvents>
122
+ implements ISharedIntervalCollection<SequenceInterval>, MergeTreeRevertibleDriver
123
+ {
124
+ get loaded(): Promise<void> {
125
+ return this.loadedDeferred.promise;
126
+ }
127
+
128
+ /**
129
+ * This is a safeguard to avoid problematic reentrancy of local ops. This type of scenario occurs if the user of SharedString subscribes
130
+ * to the `sequenceDelta` event and uses the callback for a local op to submit further local ops.
131
+ * Historically (before 2.0.0-internal.6.1.0), doing so would result in eventual consistency issues or a corrupted document.
132
+ * These issues were fixed in #16815 which makes such reentrancy no different from applying the ops in order but not from within the change events,
133
+ * but there is still little test coverage for reentrant scenarios.
134
+ * Additionally, applications submitting ops from inside change events need to take extreme care that their data models also support reentrancy.
135
+ * Since this is likely not the case, by default SharedString throws when encountering reentrant ops.
136
+ *
137
+ * An application using SharedString which explicitly wants to opt in to allowing reentrancy anyway can set `sharedStringPreventReentrancy`
138
+ * on the data store options to `false`.
139
+ */
140
+ protected guardReentrancy: <TRet>(callback: () => TRet) => TRet;
141
+
142
+ private static createOpsFromDelta(event: SequenceDeltaEvent): IMergeTreeDeltaOp[] {
143
+ const ops: IMergeTreeDeltaOp[] = [];
144
+ for (const r of event.ranges) {
145
+ switch (event.deltaOperation) {
146
+ case MergeTreeDeltaType.ANNOTATE: {
147
+ const lastAnnotate = ops[ops.length - 1] as IMergeTreeAnnotateMsg;
148
+ const props: PropertySet = {};
149
+ for (const key of Object.keys(r.propertyDeltas)) {
150
+ props[key] = r.segment.properties?.[key] ?? null;
151
+ }
152
+ if (
153
+ lastAnnotate &&
154
+ lastAnnotate.pos2 === r.position &&
155
+ matchProperties(lastAnnotate.props, props)
156
+ ) {
157
+ lastAnnotate.pos2 += r.segment.cachedLength;
158
+ } else {
159
+ ops.push(
160
+ createAnnotateRangeOp(
161
+ r.position,
162
+ r.position + r.segment.cachedLength,
163
+ props,
164
+ ),
165
+ );
166
+ }
167
+ break;
168
+ }
169
+
170
+ case MergeTreeDeltaType.INSERT:
171
+ ops.push(createInsertOp(r.position, r.segment.clone().toJSONObject()));
172
+ break;
173
+
174
+ case MergeTreeDeltaType.REMOVE: {
175
+ const lastRem = ops[ops.length - 1] as IMergeTreeRemoveMsg;
176
+ if (lastRem?.pos1 === r.position) {
177
+ assert(
178
+ lastRem.pos2 !== undefined,
179
+ 0x3ff /* pos2 should not be undefined here */,
180
+ );
181
+ lastRem.pos2 += r.segment.cachedLength;
182
+ } else {
183
+ ops.push(
184
+ createRemoveRangeOp(r.position, r.position + r.segment.cachedLength),
185
+ );
186
+ }
187
+ break;
188
+ }
189
+
190
+ case MergeTreeDeltaType.OBLITERATE: {
191
+ const lastRem = ops[ops.length - 1] as IMergeTreeObliterateMsg;
192
+ if (lastRem?.pos1 === r.position) {
193
+ assert(
194
+ lastRem.pos2 !== undefined,
195
+ 0x874 /* pos2 should not be undefined here */,
196
+ );
197
+ lastRem.pos2 += r.segment.cachedLength;
198
+ } else {
199
+ ops.push(
200
+ createObliterateRangeOp(
201
+ r.position,
202
+ r.position + r.segment.cachedLength,
203
+ ),
204
+ );
205
+ }
206
+ break;
207
+ }
208
+
209
+ default:
210
+ }
211
+ }
212
+ return ops;
213
+ }
214
+
215
+ // eslint-disable-next-line import/no-deprecated
216
+ protected client: Client;
217
+ /** `Deferred` that triggers once the object is loaded */
218
+ protected loadedDeferred = new Deferred<void>();
219
+ // cache out going ops created when partial loading
220
+ private readonly loadedDeferredOutgoingOps: [IMergeTreeOp, SegmentGroup | SegmentGroup[]][] =
221
+ [];
222
+ // cache incoming ops that arrive when partial loading
223
+ private deferIncomingOps = true;
224
+ private readonly loadedDeferredIncomingOps: ISequencedDocumentMessage[] = [];
225
+
226
+ private messagesSinceMSNChange: ISequencedDocumentMessage[] = [];
227
+ private readonly intervalCollections: DefaultMap<IntervalCollection<SequenceInterval>>;
228
+ constructor(
229
+ private readonly dataStoreRuntime: IFluidDataStoreRuntime,
230
+ public id: string,
231
+ attributes: IChannelAttributes,
232
+ public readonly segmentFromSpec: (spec: IJSONSegment) => ISegment,
233
+ ) {
234
+ super(id, dataStoreRuntime, attributes, "fluid_sequence_");
235
+
236
+ this.guardReentrancy =
237
+ dataStoreRuntime.options.sharedStringPreventReentrancy ?? true
238
+ ? ensureNoReentrancy
239
+ : createReentrancyDetector((depth) => {
240
+ if (totalReentrancyLogs > 0) {
241
+ totalReentrancyLogs--;
242
+ this.logger.sendTelemetryEvent(
243
+ { eventName: "LocalOpReentry", depth },
244
+ new LoggingError(reentrancyErrorMessage),
245
+ );
246
+ }
247
+ });
248
+
249
+ this.loadedDeferred.promise.catch((error) => {
250
+ this.logger.sendErrorEvent({ eventName: "SequenceLoadFailed" }, error);
251
+ });
252
+
253
+ // eslint-disable-next-line import/no-deprecated
254
+ this.client = new Client(
255
+ segmentFromSpec,
256
+ createChildLogger({
257
+ logger: this.logger,
258
+ namespace: "SharedSegmentSequence.MergeTreeClient",
259
+ }),
260
+ dataStoreRuntime.options,
261
+ );
262
+
263
+ this.client.prependListener("delta", (opArgs, deltaArgs) => {
264
+ const event = new SequenceDeltaEvent(opArgs, deltaArgs, this.client);
265
+ if (opArgs.stashed !== true && event.isLocal) {
266
+ this.submitSequenceMessage(opArgs.op);
267
+ }
268
+ this.emit("sequenceDelta", event, this);
269
+ });
270
+
271
+ this.client.on("maintenance", (args, opArgs) => {
272
+ this.emit("maintenance", new SequenceMaintenanceEvent(opArgs, args, this.client), this);
273
+ });
274
+
275
+ this.intervalCollections = new DefaultMap(
276
+ this.serializer,
277
+ this.handle,
278
+ (op, localOpMetadata) => this.submitLocalMessage(op, localOpMetadata),
279
+ new SequenceIntervalCollectionValueType(),
280
+ dataStoreRuntime.options,
281
+ );
282
+ }
283
+
284
+ /**
285
+ * @param start - The inclusive start of the range to remove
286
+ * @param end - The exclusive end of the range to remove
287
+ */
288
+ public removeRange(start: number, end: number): void {
289
+ this.guardReentrancy(() => this.client.removeRangeLocal(start, end));
290
+ }
291
+
292
+ /**
293
+ * Obliterate is similar to remove, but differs in that segments concurrently
294
+ * inserted into an obliterated range will also be removed
295
+ *
296
+ * @param start - The inclusive start of the range to obliterate
297
+ * @param end - The exclusive end of the range to obliterate
298
+ */
299
+ public obliterateRange(start: number, end: number): void {
300
+ this.guardReentrancy(() => this.client.obliterateRangeLocal(start, end));
301
+ }
302
+
303
+ /**
304
+ * @deprecated The ability to create group ops will be removed in an upcoming
305
+ * release, as group ops are redundant with the native batching capabilities
306
+ * of the runtime
307
+ */
308
+ public groupOperation(groupOp: IMergeTreeGroupMsg) {
309
+ this.guardReentrancy(() => this.client.localTransaction(groupOp));
310
+ }
311
+
312
+ /**
313
+ * Finds the segment information (i.e. segment + offset) corresponding to a character position in the SharedString.
314
+ * If the position is past the end of the string, `segment` and `offset` on the returned object may be undefined.
315
+ * @param pos - Character position (index) into the current local view of the SharedString.
316
+ */
317
+ public getContainingSegment(pos: number): {
318
+ segment: T | undefined;
319
+ offset: number | undefined;
320
+ } {
321
+ return this.client.getContainingSegment<T>(pos);
322
+ }
323
+
324
+ /**
325
+ * Returns the length of the current sequence for the client
326
+ */
327
+ public getLength() {
328
+ return this.client.getLength();
329
+ }
330
+
331
+ /**
332
+ * Returns the current position of a segment, and -1 if the segment
333
+ * does not exist in this sequence
334
+ * @param segment - The segment to get the position of
335
+ */
336
+ public getPosition(segment: ISegment): number {
337
+ return this.client.getPosition(segment);
338
+ }
339
+
340
+ /**
341
+ * Annotates the range with the provided properties
342
+ *
343
+ * @param start - The inclusive start position of the range to annotate
344
+ * @param end - The exclusive end position of the range to annotate
345
+ * @param props - The properties to annotate the range with
346
+ *
347
+ */
348
+ public annotateRange(start: number, end: number, props: PropertySet): void {
349
+ this.guardReentrancy(() => this.client.annotateRangeLocal(start, end, props));
350
+ }
351
+
352
+ public getPropertiesAtPosition(pos: number) {
353
+ return this.client.getPropertiesAtPosition(pos);
354
+ }
355
+
356
+ public getRangeExtentsOfPosition(pos: number) {
357
+ return this.client.getRangeExtentsOfPosition(pos);
358
+ }
359
+
360
+ /**
361
+ * Creates a `LocalReferencePosition` on this SharedString. If the refType does not include
362
+ * ReferenceType.Transient, the returned reference will be added to the localRefs on the provided segment.
363
+ * @param segment - Segment to add the local reference on
364
+ * @param offset - Offset on the segment at which to place the local reference
365
+ * @param refType - ReferenceType for the created local reference
366
+ * @param properties - PropertySet to place on the created local reference
367
+ */
368
+ public createLocalReferencePosition(
369
+ segment: T,
370
+ offset: number,
371
+ refType: ReferenceType,
372
+ properties: PropertySet | undefined,
373
+ slidingPreference?: SlidingPreference,
374
+ canSlideToEndpoint?: boolean,
375
+ ): LocalReferencePosition {
376
+ return this.client.createLocalReferencePosition(
377
+ segment,
378
+ offset,
379
+ refType,
380
+ properties,
381
+ slidingPreference,
382
+ canSlideToEndpoint,
383
+ );
384
+ }
385
+
386
+ /**
387
+ * Resolves a `ReferencePosition` into a character position using this client's perspective.
388
+ */
389
+ public localReferencePositionToPosition(lref: ReferencePosition): number {
390
+ return this.client.localReferencePositionToPosition(lref);
391
+ }
392
+
393
+ /**
394
+ * Removes a `LocalReferencePosition` from this SharedString.
395
+ */
396
+ public removeLocalReferencePosition(lref: LocalReferencePosition) {
397
+ return this.client.removeLocalReferencePosition(lref);
398
+ }
399
+
400
+ /**
401
+ * Resolves a remote client's position against the local sequence
402
+ * and returns the remote client's position relative to the local
403
+ * sequence. The client ref seq must be above the minimum sequence number
404
+ * or the return value will be undefined.
405
+ * Generally this method is used in conjunction with signals which provide
406
+ * point in time values for the below parameters, and is useful for things
407
+ * like displaying user position. It should not be used with persisted values
408
+ * as persisted values will quickly become invalid as the remoteClientRefSeq
409
+ * moves below the minimum sequence number
410
+ * @param remoteClientPosition - The remote client's position to resolve
411
+ * @param remoteClientRefSeq - The reference sequence number of the remote client
412
+ * @param remoteClientId - The client id of the remote client
413
+ */
414
+ public resolveRemoteClientPosition(
415
+ remoteClientPosition: number,
416
+ remoteClientRefSeq: number,
417
+ remoteClientId: string,
418
+ ): number | undefined {
419
+ return this.client.resolveRemoteClientPosition(
420
+ remoteClientPosition,
421
+ remoteClientRefSeq,
422
+ remoteClientId,
423
+ );
424
+ }
425
+
426
+ private submitSequenceMessage(message: IMergeTreeOp) {
427
+ if (!this.isAttached()) {
428
+ return;
429
+ }
430
+ const translated = makeHandlesSerializable(message, this.serializer, this.handle);
431
+ const metadata = this.client.peekPendingSegmentGroups(
432
+ message.type === MergeTreeDeltaType.GROUP ? message.ops.length : 1,
433
+ );
434
+
435
+ // if loading isn't complete, we need to cache
436
+ // local ops until loading is complete, and then
437
+ // they will be present
438
+ if (!this.loadedDeferred.isCompleted) {
439
+ this.loadedDeferredOutgoingOps.push(metadata ? [translated, metadata] : translated);
440
+ } else {
441
+ this.submitLocalMessage(translated, metadata);
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Given a position specified relative to a marker id, lookup the marker
447
+ * and convert the position to a character position.
448
+ * @param relativePos - Id of marker (may be indirect) and whether position is before or after marker.
449
+ */
450
+ public posFromRelativePos(relativePos: IRelativePosition) {
451
+ return this.client.posFromRelativePos(relativePos);
452
+ }
453
+
454
+ /**
455
+ * Walk the underlying segments of the sequence.
456
+ * The walked segments may extend beyond the range if the segments cross the
457
+ * ranges start or end boundaries.
458
+ *
459
+ * Set split range to true to ensure only segments within the range are walked.
460
+ *
461
+ * @param handler - The function to handle each segment. Traversal ends if
462
+ * this function returns true.
463
+ * @param start - Optional. The start of range walk.
464
+ * @param end - Optional. The end of range walk
465
+ * @param accum - Optional. An object that will be passed to the handler for accumulation
466
+ * @param splitRange - Optional. Splits boundary segments on the range boundaries
467
+ */
468
+ public walkSegments<TClientData>(
469
+ handler: ISegmentAction<TClientData>,
470
+ start?: number,
471
+ end?: number,
472
+ accum?: TClientData,
473
+ splitRange: boolean = false,
474
+ ): void {
475
+ this.client.walkSegments(handler, start, end, accum as TClientData, splitRange);
476
+ }
477
+
478
+ /**
479
+ * @returns The most recent sequence number which has been acked by the server and processed by this
480
+ * SharedSegmentSequence.
481
+ */
482
+ public getCurrentSeq() {
483
+ return this.client.getCurrentSeq();
484
+ }
485
+
486
+ /**
487
+ * Inserts a segment directly before a `ReferencePosition`.
488
+ * @param refPos - The reference position to insert the segment at
489
+ * @param segment - The segment to insert
490
+ */
491
+ public insertAtReferencePosition(pos: ReferencePosition, segment: T): void {
492
+ this.guardReentrancy(() => this.client.insertAtReferencePositionLocal(pos, segment));
493
+ }
494
+ /**
495
+ * Inserts a segment
496
+ * @param start - The position to insert the segment at
497
+ * @param spec - The segment to inserts spec
498
+ */
499
+ public insertFromSpec(pos: number, spec: IJSONSegment): void {
500
+ const segment = this.segmentFromSpec(spec);
501
+ this.guardReentrancy(() => this.client.insertSegmentLocal(pos, segment));
502
+ }
503
+
504
+ /**
505
+ * Retrieves the interval collection keyed on `label`. If no such interval collection exists,
506
+ * creates one.
507
+ */
508
+ public getIntervalCollection(label: string): IIntervalCollection<SequenceInterval> {
509
+ return this.intervalCollections.get(label);
510
+ }
511
+
512
+ /**
513
+ * @returns An iterable object that enumerates the IntervalCollection labels.
514
+ *
515
+ * @example
516
+ *
517
+ * ```typescript
518
+ * const iter = this.getIntervalCollectionKeys();
519
+ * for (key of iter)
520
+ * const collection = this.getIntervalCollection(key);
521
+ * ...
522
+ * ```
523
+ */
524
+ public getIntervalCollectionLabels(): IterableIterator<string> {
525
+ return this.intervalCollections.keys();
526
+ }
527
+
528
+ /**
529
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
530
+ */
531
+ protected summarizeCore(
532
+ serializer: IFluidSerializer,
533
+ telemetryContext?: ITelemetryContext,
534
+ ): ISummaryTreeWithStats {
535
+ const builder = new SummaryTreeBuilder();
536
+
537
+ // conditionally write the interval collection blob
538
+ // only if it has entries
539
+ if (this.intervalCollections.size > 0) {
540
+ builder.addBlob(snapshotFileName, this.intervalCollections.serialize(serializer));
541
+ }
542
+
543
+ builder.addWithStats(contentPath, this.summarizeMergeTree(serializer));
544
+
545
+ return builder.getSummaryTree();
546
+ }
547
+
548
+ /**
549
+ * Runs serializer over the GC data for this SharedMatrix.
550
+ * All the IFluidHandle's represent routes to other objects.
551
+ */
552
+ protected processGCDataCore(serializer: IFluidSerializer) {
553
+ if (this.intervalCollections.size > 0) {
554
+ this.intervalCollections.serialize(serializer);
555
+ }
556
+
557
+ this.client.serializeGCData(this.handle, serializer);
558
+ }
559
+
560
+ /**
561
+ * Replace the range specified from start to end with the provided segment
562
+ * This is done by inserting the segment at the end of the range, followed
563
+ * by removing the contents of the range
564
+ * For a zero or reverse range (start \>= end), insert at end do not remove anything
565
+ * @param start - The start of the range to replace
566
+ * @param end - The end of the range to replace
567
+ * @param segment - The segment that will replace the range
568
+ */
569
+ protected replaceRange(start: number, end: number, segment: ISegment): void {
570
+ // Insert at the max end of the range when start > end, but still remove the range later
571
+ const insertIndex: number = Math.max(start, end);
572
+
573
+ // Insert first, so local references can slide to the inserted seg if any
574
+ const insert = this.client.insertSegmentLocal(insertIndex, segment);
575
+ if (insert) {
576
+ if (start < end) {
577
+ this.client.removeRangeLocal(start, end);
578
+ }
579
+ }
580
+ }
581
+
582
+ /**
583
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.onConnect}
584
+ */
585
+ protected onConnect() {
586
+ // Update merge tree collaboration information with new client ID and then resend pending ops
587
+ this.client.startOrUpdateCollaboration(this.runtime.clientId);
588
+ }
589
+
590
+ /**
591
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.onDisconnect}
592
+ */
593
+ protected onDisconnect() {}
594
+
595
+ /**
596
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.reSubmitCore}
597
+ */
598
+ protected reSubmitCore(content: any, localOpMetadata: unknown) {
599
+ if (
600
+ !this.intervalCollections.tryResubmitMessage(
601
+ content,
602
+ localOpMetadata as IMapMessageLocalMetadata,
603
+ )
604
+ ) {
605
+ this.submitSequenceMessage(
606
+ this.client.regeneratePendingOp(
607
+ content as IMergeTreeOp,
608
+ localOpMetadata as SegmentGroup | SegmentGroup[],
609
+ ),
610
+ );
611
+ }
612
+ }
613
+
614
+ /**
615
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
616
+ */
617
+ protected async loadCore(storage: IChannelStorageService) {
618
+ if (await storage.contains(snapshotFileName)) {
619
+ const blob = await storage.readBlob(snapshotFileName);
620
+ const header = bufferToString(blob, "utf8");
621
+ this.intervalCollections.populate(header);
622
+ }
623
+
624
+ try {
625
+ // this will load the header, and return a promise
626
+ // that will resolve when the body is loaded
627
+ // and the catchup ops are available.
628
+ const { catchupOpsP } = await this.client.load(
629
+ this.runtime,
630
+ new ObjectStoragePartition(storage, contentPath),
631
+ this.serializer,
632
+ );
633
+
634
+ // setup a promise to process the
635
+ // catch up ops, and finishing the loading process
636
+ const loadCatchUpOps = catchupOpsP
637
+ .then((msgs) => {
638
+ msgs.forEach((m) => {
639
+ const collabWindow = this.client.getCollabWindow();
640
+ if (
641
+ m.minimumSequenceNumber < collabWindow.minSeq ||
642
+ m.referenceSequenceNumber < collabWindow.minSeq ||
643
+ m.sequenceNumber <= collabWindow.minSeq ||
644
+ // sequenceNumber could be the same if messages are part of a grouped batch
645
+ m.sequenceNumber < collabWindow.currentSeq
646
+ ) {
647
+ throw new Error(
648
+ `Invalid catchup operations in snapshot: ${JSON.stringify({
649
+ op: {
650
+ seq: m.sequenceNumber,
651
+ minSeq: m.minimumSequenceNumber,
652
+ refSeq: m.referenceSequenceNumber,
653
+ },
654
+ collabWindow: {
655
+ seq: collabWindow.currentSeq,
656
+ minSeq: collabWindow.minSeq,
657
+ },
658
+ })}`,
659
+ );
660
+ }
661
+ this.processMergeTreeMsg(m);
662
+ });
663
+ this.loadFinished();
664
+ })
665
+ .catch((error) => {
666
+ this.loadFinished(error);
667
+ });
668
+ if (this.dataStoreRuntime.options?.sequenceInitializeFromHeaderOnly !== true) {
669
+ // if we not doing partial load, await the catch up ops,
670
+ // and the finalization of the load
671
+ await loadCatchUpOps;
672
+ }
673
+ } catch (error) {
674
+ this.loadFinished(error);
675
+ }
676
+ }
677
+
678
+ /**
679
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.processCore}
680
+ */
681
+ protected processCore(
682
+ message: ISequencedDocumentMessage,
683
+ local: boolean,
684
+ localOpMetadata: unknown,
685
+ ) {
686
+ // if loading isn't complete, we need to cache all
687
+ // incoming ops to be applied after loading is complete
688
+ if (this.deferIncomingOps) {
689
+ assert(!local, 0x072 /* "Unexpected local op when loading not finished" */);
690
+ this.loadedDeferredIncomingOps.push(message);
691
+ } else {
692
+ assert(
693
+ message.type === MessageType.Operation,
694
+ 0x073 /* "Sequence message not operation" */,
695
+ );
696
+
697
+ const handled = this.intervalCollections.tryProcessMessage(
698
+ message.contents as IMapOperation,
699
+ local,
700
+ message,
701
+ localOpMetadata,
702
+ );
703
+
704
+ if (!handled) {
705
+ this.processMergeTreeMsg(message, local);
706
+ }
707
+ }
708
+ }
709
+
710
+ /**
711
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.didAttach}
712
+ */
713
+ protected didAttach() {
714
+ // If we are not local, and we've attached we need to start generating and sending ops
715
+ // so start collaboration and provide a default client id incase we are not connected
716
+ if (this.isAttached()) {
717
+ this.client.startOrUpdateCollaboration(this.runtime.clientId ?? "attached");
718
+ }
719
+ }
720
+
721
+ /**
722
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.initializeLocalCore}
723
+ */
724
+ protected initializeLocalCore() {
725
+ super.initializeLocalCore();
726
+ this.loadFinished();
727
+ }
728
+
729
+ /**
730
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
731
+ */
732
+ protected applyStashedOp(content: any): unknown {
733
+ return this.client.applyStashedOp(parseHandles(content, this.serializer));
734
+ }
735
+
736
+ private summarizeMergeTree(serializer: IFluidSerializer): ISummaryTreeWithStats {
737
+ // Are we fully loaded? If not, things will go south
738
+ assert(
739
+ this.loadedDeferred.isCompleted,
740
+ 0x074 /* "Snapshot called when not fully loaded" */,
741
+ );
742
+ const minSeq = this.runtime.deltaManager.minimumSequenceNumber;
743
+
744
+ this.processMinSequenceNumberChanged(minSeq);
745
+
746
+ this.messagesSinceMSNChange.forEach((m) => {
747
+ m.minimumSequenceNumber = minSeq;
748
+ });
749
+
750
+ return this.client.summarize(
751
+ this.runtime,
752
+ this.handle,
753
+ serializer,
754
+ this.messagesSinceMSNChange,
755
+ );
756
+ }
757
+
758
+ private processMergeTreeMsg(rawMessage: ISequencedDocumentMessage, local?: boolean) {
759
+ const message = parseHandles(rawMessage, this.serializer);
760
+
761
+ const ops: IMergeTreeDeltaOp[] = [];
762
+ function transformOps(event: SequenceDeltaEvent) {
763
+ ops.push(...SharedSegmentSequence.createOpsFromDelta(event));
764
+ }
765
+ const needsTransformation = message.referenceSequenceNumber !== message.sequenceNumber - 1;
766
+ let stashMessage: Readonly<ISequencedDocumentMessage> = message;
767
+ if (this.runtime.options?.newMergeTreeSnapshotFormat !== true) {
768
+ if (needsTransformation) {
769
+ this.on("sequenceDelta", transformOps);
770
+ }
771
+ }
772
+
773
+ this.client.applyMsg(message, local);
774
+
775
+ if (this.runtime.options?.newMergeTreeSnapshotFormat !== true) {
776
+ if (needsTransformation) {
777
+ this.removeListener("sequenceDelta", transformOps);
778
+ // shallow clone the message as we only overwrite top level properties,
779
+ // like referenceSequenceNumber and content only
780
+ stashMessage = {
781
+ ...message,
782
+ referenceSequenceNumber: stashMessage.sequenceNumber - 1,
783
+ // eslint-disable-next-line import/no-deprecated
784
+ contents: ops.length !== 1 ? createGroupOp(...ops) : ops[0],
785
+ };
786
+ }
787
+
788
+ this.messagesSinceMSNChange.push(stashMessage);
789
+
790
+ // Do GC every once in a while...
791
+ if (
792
+ this.messagesSinceMSNChange.length > 20 &&
793
+ this.messagesSinceMSNChange[20].sequenceNumber < message.minimumSequenceNumber
794
+ ) {
795
+ this.processMinSequenceNumberChanged(message.minimumSequenceNumber);
796
+ }
797
+ }
798
+ }
799
+
800
+ private processMinSequenceNumberChanged(minSeq: number) {
801
+ let index = 0;
802
+ for (; index < this.messagesSinceMSNChange.length; index++) {
803
+ if (this.messagesSinceMSNChange[index].sequenceNumber > minSeq) {
804
+ break;
805
+ }
806
+ }
807
+ if (index !== 0) {
808
+ this.messagesSinceMSNChange = this.messagesSinceMSNChange.slice(index);
809
+ }
810
+ }
811
+
812
+ private loadFinished(error?: any) {
813
+ if (!this.loadedDeferred.isCompleted) {
814
+ // Initialize the interval collections
815
+ this.initializeIntervalCollections();
816
+ if (error) {
817
+ this.loadedDeferred.reject(error);
818
+ throw error;
819
+ } else {
820
+ // it is important this series remains synchronous
821
+ // first we stop deferring incoming ops, and apply then all
822
+ this.deferIncomingOps = false;
823
+ for (const message of this.loadedDeferredIncomingOps) {
824
+ this.processCore(message, false, undefined);
825
+ }
826
+ this.loadedDeferredIncomingOps.length = 0;
827
+
828
+ // then resolve the loaded promise
829
+ // and resubmit all the outstanding ops, as the snapshot
830
+ // is fully loaded, and all outstanding ops are applied
831
+ this.loadedDeferred.resolve();
832
+
833
+ for (const [messageContent, opMetadata] of this.loadedDeferredOutgoingOps) {
834
+ this.reSubmitCore(messageContent, opMetadata);
835
+ }
836
+ this.loadedDeferredOutgoingOps.length = 0;
837
+ }
838
+ }
839
+ }
840
+
841
+ private initializeIntervalCollections() {
842
+ // Listen and initialize new SharedIntervalCollections
843
+ this.intervalCollections.eventEmitter.on(
844
+ "create",
845
+ ({ key, previousValue }: IValueChanged, local: boolean) => {
846
+ const intervalCollection = this.intervalCollections.get(key);
847
+ if (!intervalCollection.attached) {
848
+ intervalCollection.attachGraph(this.client, key);
849
+ }
850
+ assert(
851
+ previousValue === undefined,
852
+ 0x2c1 /* "Creating an interval collection that already exists?" */,
853
+ );
854
+ this.emit("createIntervalCollection", key, local, this);
855
+ },
856
+ );
857
+
858
+ // Initialize existing SharedIntervalCollections
859
+ for (const key of this.intervalCollections.keys()) {
860
+ const intervalCollection = this.intervalCollections.get(key);
861
+ intervalCollection.attachGraph(this.client, key);
862
+ }
863
+ }
864
+ }
865
+
866
+ function createReentrancyDetector(
867
+ onReentrancy: (depth: number) => void,
868
+ ): <T>(callback: () => T) => T {
869
+ let depth = 0;
870
+ function detectReentrancy<T>(callback: () => T): T {
871
+ if (depth > 0) {
872
+ onReentrancy(depth);
873
+ }
874
+ depth++;
875
+ try {
876
+ return callback();
877
+ } finally {
878
+ depth--;
879
+ }
880
+ }
881
+
882
+ return detectReentrancy;
747
883
  }
884
+
885
+ /**
886
+ * Apps which generate reentrant behavior may do so at a high frequency.
887
+ * Logging even per-SharedSegmentSequence instance might be too noisy, and having a few logs from a session
888
+ * is likely enough.
889
+ */
890
+ let totalReentrancyLogs = 3;
891
+
892
+ /**
893
+ * Resets the reentrancy log counter. Test-only API.
894
+ */
895
+ export function resetReentrancyLogCounter() {
896
+ totalReentrancyLogs = 3;
897
+ }
898
+
899
+ const reentrancyErrorMessage = "Reentrancy detected in sequence local ops";
900
+ const ensureNoReentrancy = createReentrancyDetector(() => {
901
+ throw new LoggingError(reentrancyErrorMessage);
902
+ });