document-model 6.0.0-dev.105 → 6.0.0-dev.107

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 (289) hide show
  1. package/dist/index.d.ts +81 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +216 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/{src/core/node.d.ts → node.d.mts} +25 -23
  6. package/dist/node.d.mts.map +1 -0
  7. package/dist/node.mjs +151 -0
  8. package/dist/node.mjs.map +1 -0
  9. package/package.json +11 -15
  10. package/dist/src/core/actions.d.ts +0 -87
  11. package/dist/src/core/actions.d.ts.map +0 -1
  12. package/dist/src/core/actions.js +0 -187
  13. package/dist/src/core/actions.js.map +0 -1
  14. package/dist/src/core/controller.d.ts +0 -30
  15. package/dist/src/core/controller.d.ts.map +0 -1
  16. package/dist/src/core/controller.js +0 -60
  17. package/dist/src/core/controller.js.map +0 -1
  18. package/dist/src/core/crypto.d.ts +0 -8
  19. package/dist/src/core/crypto.d.ts.map +0 -1
  20. package/dist/src/core/crypto.js +0 -68
  21. package/dist/src/core/crypto.js.map +0 -1
  22. package/dist/src/core/documents.d.ts +0 -147
  23. package/dist/src/core/documents.d.ts.map +0 -1
  24. package/dist/src/core/documents.js +0 -845
  25. package/dist/src/core/documents.js.map +0 -1
  26. package/dist/src/core/errors.d.ts +0 -21
  27. package/dist/src/core/errors.d.ts.map +0 -1
  28. package/dist/src/core/errors.js +0 -46
  29. package/dist/src/core/errors.js.map +0 -1
  30. package/dist/src/core/files.d.ts +0 -27
  31. package/dist/src/core/files.d.ts.map +0 -1
  32. package/dist/src/core/files.js +0 -90
  33. package/dist/src/core/files.js.map +0 -1
  34. package/dist/src/core/header.d.ts +0 -63
  35. package/dist/src/core/header.d.ts.map +0 -1
  36. package/dist/src/core/header.js +0 -173
  37. package/dist/src/core/header.js.map +0 -1
  38. package/dist/src/core/index.d.ts +0 -16
  39. package/dist/src/core/index.d.ts.map +0 -1
  40. package/dist/src/core/index.js +0 -16
  41. package/dist/src/core/index.js.map +0 -1
  42. package/dist/src/core/logger-types.d.ts +0 -12
  43. package/dist/src/core/logger-types.d.ts.map +0 -1
  44. package/dist/src/core/logger-types.js +0 -2
  45. package/dist/src/core/logger-types.js.map +0 -1
  46. package/dist/src/core/logger.d.ts +0 -27
  47. package/dist/src/core/logger.d.ts.map +0 -1
  48. package/dist/src/core/logger.js +0 -127
  49. package/dist/src/core/logger.js.map +0 -1
  50. package/dist/src/core/node.d.ts.map +0 -1
  51. package/dist/src/core/node.js +0 -155
  52. package/dist/src/core/node.js.map +0 -1
  53. package/dist/src/core/operations.d.ts +0 -50
  54. package/dist/src/core/operations.d.ts.map +0 -1
  55. package/dist/src/core/operations.js +0 -126
  56. package/dist/src/core/operations.js.map +0 -1
  57. package/dist/src/core/ph-types.d.ts +0 -2
  58. package/dist/src/core/ph-types.d.ts.map +0 -1
  59. package/dist/src/core/ph-types.js +0 -2
  60. package/dist/src/core/ph-types.js.map +0 -1
  61. package/dist/src/core/reducer.d.ts +0 -63
  62. package/dist/src/core/reducer.d.ts.map +0 -1
  63. package/dist/src/core/reducer.js +0 -446
  64. package/dist/src/core/reducer.js.map +0 -1
  65. package/dist/src/core/schemas.d.ts +0 -75
  66. package/dist/src/core/schemas.d.ts.map +0 -1
  67. package/dist/src/core/schemas.js +0 -116
  68. package/dist/src/core/schemas.js.map +0 -1
  69. package/dist/src/core/state.d.ts +0 -26
  70. package/dist/src/core/state.d.ts.map +0 -1
  71. package/dist/src/core/state.js +0 -56
  72. package/dist/src/core/state.js.map +0 -1
  73. package/dist/src/core/types.d.ts +0 -2
  74. package/dist/src/core/types.d.ts.map +0 -1
  75. package/dist/src/core/types.js +0 -2
  76. package/dist/src/core/types.js.map +0 -1
  77. package/dist/src/core/utils.d.ts +0 -6
  78. package/dist/src/core/utils.d.ts.map +0 -1
  79. package/dist/src/core/utils.js +0 -15
  80. package/dist/src/core/utils.js.map +0 -1
  81. package/dist/src/core/validation.d.ts +0 -4
  82. package/dist/src/core/validation.d.ts.map +0 -1
  83. package/dist/src/core/validation.js +0 -27
  84. package/dist/src/core/validation.js.map +0 -1
  85. package/dist/src/document-model/actions.d.ts +0 -164
  86. package/dist/src/document-model/actions.d.ts.map +0 -1
  87. package/dist/src/document-model/actions.js +0 -111
  88. package/dist/src/document-model/actions.js.map +0 -1
  89. package/dist/src/document-model/constants.d.ts +0 -6
  90. package/dist/src/document-model/constants.d.ts.map +0 -1
  91. package/dist/src/document-model/constants.js +0 -584
  92. package/dist/src/document-model/constants.js.map +0 -1
  93. package/dist/src/document-model/controller.d.ts +0 -5
  94. package/dist/src/document-model/controller.d.ts.map +0 -1
  95. package/dist/src/document-model/controller.js +0 -5
  96. package/dist/src/document-model/controller.js.map +0 -1
  97. package/dist/src/document-model/document-schema.d.ts +0 -69
  98. package/dist/src/document-model/document-schema.d.ts.map +0 -1
  99. package/dist/src/document-model/document-schema.js +0 -43
  100. package/dist/src/document-model/document-schema.js.map +0 -1
  101. package/dist/src/document-model/document-type.d.ts +0 -2
  102. package/dist/src/document-model/document-type.d.ts.map +0 -1
  103. package/dist/src/document-model/document-type.js +0 -2
  104. package/dist/src/document-model/document-type.js.map +0 -1
  105. package/dist/src/document-model/files.d.ts +0 -5
  106. package/dist/src/document-model/files.d.ts.map +0 -1
  107. package/dist/src/document-model/files.js +0 -9
  108. package/dist/src/document-model/files.js.map +0 -1
  109. package/dist/src/document-model/index.d.ts +0 -12
  110. package/dist/src/document-model/index.d.ts.map +0 -1
  111. package/dist/src/document-model/index.js +0 -12
  112. package/dist/src/document-model/index.js.map +0 -1
  113. package/dist/src/document-model/module.d.ts +0 -3
  114. package/dist/src/document-model/module.d.ts.map +0 -1
  115. package/dist/src/document-model/module.js +0 -24
  116. package/dist/src/document-model/module.js.map +0 -1
  117. package/dist/src/document-model/reducers.d.ts +0 -11
  118. package/dist/src/document-model/reducers.d.ts.map +0 -1
  119. package/dist/src/document-model/reducers.js +0 -618
  120. package/dist/src/document-model/reducers.js.map +0 -1
  121. package/dist/src/document-model/schemas.d.ts +0 -189
  122. package/dist/src/document-model/schemas.d.ts.map +0 -1
  123. package/dist/src/document-model/schemas.js +0 -388
  124. package/dist/src/document-model/schemas.js.map +0 -1
  125. package/dist/src/document-model/state.d.ts +0 -15
  126. package/dist/src/document-model/state.d.ts.map +0 -1
  127. package/dist/src/document-model/state.js +0 -75
  128. package/dist/src/document-model/state.js.map +0 -1
  129. package/dist/src/document-model/types.d.ts +0 -584
  130. package/dist/src/document-model/types.d.ts.map +0 -1
  131. package/dist/src/document-model/types.js +0 -2
  132. package/dist/src/document-model/types.js.map +0 -1
  133. package/dist/src/document-model/validation.d.ts +0 -32
  134. package/dist/src/document-model/validation.d.ts.map +0 -1
  135. package/dist/src/document-model/validation.js +0 -166
  136. package/dist/src/document-model/validation.js.map +0 -1
  137. package/dist/src/index.d.ts +0 -6
  138. package/dist/src/index.d.ts.map +0 -1
  139. package/dist/src/index.js +0 -3
  140. package/dist/src/index.js.map +0 -1
  141. package/dist/test/document/crypto.test.d.ts +0 -2
  142. package/dist/test/document/crypto.test.d.ts.map +0 -1
  143. package/dist/test/document/crypto.test.js +0 -192
  144. package/dist/test/document/crypto.test.js.map +0 -1
  145. package/dist/test/document/event-vs-command.test.d.ts +0 -2
  146. package/dist/test/document/event-vs-command.test.d.ts.map +0 -1
  147. package/dist/test/document/event-vs-command.test.js +0 -202
  148. package/dist/test/document/event-vs-command.test.js.map +0 -1
  149. package/dist/test/document/local.test.d.ts +0 -2
  150. package/dist/test/document/local.test.d.ts.map +0 -1
  151. package/dist/test/document/local.test.js +0 -226
  152. package/dist/test/document/local.test.js.map +0 -1
  153. package/dist/test/document/operation-id.test.d.ts +0 -2
  154. package/dist/test/document/operation-id.test.d.ts.map +0 -1
  155. package/dist/test/document/operation-id.test.js +0 -118
  156. package/dist/test/document/operation-id.test.js.map +0 -1
  157. package/dist/test/document/prune.test.d.ts +0 -2
  158. package/dist/test/document/prune.test.d.ts.map +0 -1
  159. package/dist/test/document/prune.test.js +0 -159
  160. package/dist/test/document/prune.test.js.map +0 -1
  161. package/dist/test/document/reducer.test.d.ts +0 -2
  162. package/dist/test/document/reducer.test.d.ts.map +0 -1
  163. package/dist/test/document/reducer.test.js +0 -284
  164. package/dist/test/document/reducer.test.js.map +0 -1
  165. package/dist/test/document/skip-operations.test.d.ts +0 -2
  166. package/dist/test/document/skip-operations.test.d.ts.map +0 -1
  167. package/dist/test/document/skip-operations.test.js +0 -495
  168. package/dist/test/document/skip-operations.test.js.map +0 -1
  169. package/dist/test/document/undo-redo-v2.test.d.ts +0 -2
  170. package/dist/test/document/undo-redo-v2.test.d.ts.map +0 -1
  171. package/dist/test/document/undo-redo-v2.test.js +0 -207
  172. package/dist/test/document/undo-redo-v2.test.js.map +0 -1
  173. package/dist/test/document/undo-redo.test.d.ts +0 -2
  174. package/dist/test/document/undo-redo.test.d.ts.map +0 -1
  175. package/dist/test/document/undo-redo.test.js +0 -413
  176. package/dist/test/document/undo-redo.test.js.map +0 -1
  177. package/dist/test/document/utils.test.d.ts +0 -2
  178. package/dist/test/document/utils.test.d.ts.map +0 -1
  179. package/dist/test/document/utils.test.js +0 -172
  180. package/dist/test/document/utils.test.js.map +0 -1
  181. package/dist/test/document-helpers/addUndo.test.d.ts +0 -2
  182. package/dist/test/document-helpers/addUndo.test.d.ts.map +0 -1
  183. package/dist/test/document-helpers/addUndo.test.js +0 -120
  184. package/dist/test/document-helpers/addUndo.test.js.map +0 -1
  185. package/dist/test/document-helpers/attachBranch.test.d.ts +0 -2
  186. package/dist/test/document-helpers/attachBranch.test.d.ts.map +0 -1
  187. package/dist/test/document-helpers/attachBranch.test.js +0 -364
  188. package/dist/test/document-helpers/attachBranch.test.js.map +0 -1
  189. package/dist/test/document-helpers/checkCleanedOperationsIntegrity.test.d.ts +0 -2
  190. package/dist/test/document-helpers/checkCleanedOperationsIntegrity.test.d.ts.map +0 -1
  191. package/dist/test/document-helpers/checkCleanedOperationsIntegrity.test.js +0 -252
  192. package/dist/test/document-helpers/checkCleanedOperationsIntegrity.test.js.map +0 -1
  193. package/dist/test/document-helpers/conflictResolution.test.d.ts +0 -2
  194. package/dist/test/document-helpers/conflictResolution.test.d.ts.map +0 -1
  195. package/dist/test/document-helpers/conflictResolution.test.js +0 -109
  196. package/dist/test/document-helpers/conflictResolution.test.js.map +0 -1
  197. package/dist/test/document-helpers/filterDuplicatedOperations.test.d.ts +0 -2
  198. package/dist/test/document-helpers/filterDuplicatedOperations.test.d.ts.map +0 -1
  199. package/dist/test/document-helpers/filterDuplicatedOperations.test.js +0 -126
  200. package/dist/test/document-helpers/filterDuplicatedOperations.test.js.map +0 -1
  201. package/dist/test/document-helpers/garbageCollect.test.d.ts +0 -2
  202. package/dist/test/document-helpers/garbageCollect.test.d.ts.map +0 -1
  203. package/dist/test/document-helpers/garbageCollect.test.js +0 -136
  204. package/dist/test/document-helpers/garbageCollect.test.js.map +0 -1
  205. package/dist/test/document-helpers/groupOperationsByScope.test.d.ts +0 -2
  206. package/dist/test/document-helpers/groupOperationsByScope.test.d.ts.map +0 -1
  207. package/dist/test/document-helpers/groupOperationsByScope.test.js +0 -102
  208. package/dist/test/document-helpers/groupOperationsByScope.test.js.map +0 -1
  209. package/dist/test/document-helpers/headerRevision.test.d.ts +0 -2
  210. package/dist/test/document-helpers/headerRevision.test.d.ts.map +0 -1
  211. package/dist/test/document-helpers/headerRevision.test.js +0 -203
  212. package/dist/test/document-helpers/headerRevision.test.js.map +0 -1
  213. package/dist/test/document-helpers/merge.test.d.ts +0 -2
  214. package/dist/test/document-helpers/merge.test.d.ts.map +0 -1
  215. package/dist/test/document-helpers/merge.test.js +0 -906
  216. package/dist/test/document-helpers/merge.test.js.map +0 -1
  217. package/dist/test/document-helpers/nextSkipNumber.test.d.ts +0 -2
  218. package/dist/test/document-helpers/nextSkipNumber.test.d.ts.map +0 -1
  219. package/dist/test/document-helpers/nextSkipNumber.test.js +0 -135
  220. package/dist/test/document-helpers/nextSkipNumber.test.js.map +0 -1
  221. package/dist/test/document-helpers/prepareOperations.test.d.ts +0 -2
  222. package/dist/test/document-helpers/prepareOperations.test.d.ts.map +0 -1
  223. package/dist/test/document-helpers/prepareOperations.test.js +0 -304
  224. package/dist/test/document-helpers/prepareOperations.test.js.map +0 -1
  225. package/dist/test/document-helpers/removeExistingOperations.test.d.ts +0 -2
  226. package/dist/test/document-helpers/removeExistingOperations.test.d.ts.map +0 -1
  227. package/dist/test/document-helpers/removeExistingOperations.test.js +0 -177
  228. package/dist/test/document-helpers/removeExistingOperations.test.js.map +0 -1
  229. package/dist/test/document-helpers/reshuffleByTimestamp.test.d.ts +0 -2
  230. package/dist/test/document-helpers/reshuffleByTimestamp.test.d.ts.map +0 -1
  231. package/dist/test/document-helpers/reshuffleByTimestamp.test.js +0 -148
  232. package/dist/test/document-helpers/reshuffleByTimestamp.test.js.map +0 -1
  233. package/dist/test/document-helpers/reshuffleByTimestampAndIndex.test.d.ts +0 -2
  234. package/dist/test/document-helpers/reshuffleByTimestampAndIndex.test.d.ts.map +0 -1
  235. package/dist/test/document-helpers/reshuffleByTimestampAndIndex.test.js +0 -200
  236. package/dist/test/document-helpers/reshuffleByTimestampAndIndex.test.js.map +0 -1
  237. package/dist/test/document-helpers/skipHeaderOperations.test.d.ts +0 -2
  238. package/dist/test/document-helpers/skipHeaderOperations.test.d.ts.map +0 -1
  239. package/dist/test/document-helpers/skipHeaderOperations.test.js +0 -72
  240. package/dist/test/document-helpers/skipHeaderOperations.test.js.map +0 -1
  241. package/dist/test/document-helpers/sortOperations.test.d.ts +0 -2
  242. package/dist/test/document-helpers/sortOperations.test.d.ts.map +0 -1
  243. package/dist/test/document-helpers/sortOperations.test.js +0 -101
  244. package/dist/test/document-helpers/sortOperations.test.js.map +0 -1
  245. package/dist/test/document-helpers/split.test.d.ts +0 -2
  246. package/dist/test/document-helpers/split.test.d.ts.map +0 -1
  247. package/dist/test/document-helpers/split.test.js +0 -121
  248. package/dist/test/document-helpers/split.test.js.map +0 -1
  249. package/dist/test/document-helpers/utils.d.ts +0 -15
  250. package/dist/test/document-helpers/utils.d.ts.map +0 -1
  251. package/dist/test/document-helpers/utils.js +0 -61
  252. package/dist/test/document-helpers/utils.js.map +0 -1
  253. package/dist/test/document-model/replay.test.d.ts +0 -2
  254. package/dist/test/document-model/replay.test.d.ts.map +0 -1
  255. package/dist/test/document-model/replay.test.js +0 -139
  256. package/dist/test/document-model/replay.test.js.map +0 -1
  257. package/dist/test/document-model/skip-operations.test.d.ts +0 -2
  258. package/dist/test/document-model/skip-operations.test.d.ts.map +0 -1
  259. package/dist/test/document-model/skip-operations.test.js +0 -214
  260. package/dist/test/document-model/skip-operations.test.js.map +0 -1
  261. package/dist/test/document-model/validation.test.d.ts +0 -2
  262. package/dist/test/document-model/validation.test.d.ts.map +0 -1
  263. package/dist/test/document-model/validation.test.js +0 -451
  264. package/dist/test/document-model/validation.test.js.map +0 -1
  265. package/dist/test/document-model/versioning.test.d.ts +0 -2
  266. package/dist/test/document-model/versioning.test.d.ts.map +0 -1
  267. package/dist/test/document-model/versioning.test.js +0 -93
  268. package/dist/test/document-model/versioning.test.js.map +0 -1
  269. package/dist/test/document-model/zip.test.d.ts +0 -2
  270. package/dist/test/document-model/zip.test.d.ts.map +0 -1
  271. package/dist/test/document-model/zip.test.js +0 -79
  272. package/dist/test/document-model/zip.test.js.map +0 -1
  273. package/dist/test/helpers.d.ts +0 -79
  274. package/dist/test/helpers.d.ts.map +0 -1
  275. package/dist/test/helpers.js +0 -121
  276. package/dist/test/helpers.js.map +0 -1
  277. package/dist/test/index.d.ts +0 -4
  278. package/dist/test/index.d.ts.map +0 -1
  279. package/dist/test/index.js +0 -3
  280. package/dist/test/index.js.map +0 -1
  281. package/dist/test/types.d.ts +0 -6
  282. package/dist/test/types.d.ts.map +0 -1
  283. package/dist/test/types.js +0 -2
  284. package/dist/test/types.js.map +0 -1
  285. package/dist/tsconfig.tsbuildinfo +0 -1
  286. package/dist/vitest.config.d.ts +0 -3
  287. package/dist/vitest.config.d.ts.map +0 -1
  288. package/dist/vitest.config.js +0 -7
  289. package/dist/vitest.config.js.map +0 -1
@@ -1,845 +0,0 @@
1
- import stringifyJson, { stringify } from "safe-stable-stringify";
2
- import { hashBrowser } from "./crypto.js";
3
- import { HashMismatchError } from "./errors.js";
4
- import { createPresignedHeader } from "./header.js";
5
- import { generateId } from "./utils.js";
6
- export function isNoopOperation(op) {
7
- return (op.type === "NOOP" &&
8
- op.skip !== undefined &&
9
- op.skip > 0 &&
10
- op.hash !== undefined);
11
- }
12
- export function isUndoRedo(action) {
13
- return ["UNDO", "REDO"].includes(action.type);
14
- }
15
- export function isUndo(action) {
16
- return action.type === "UNDO";
17
- }
18
- export function isDocumentAction(action) {
19
- return ["SET_NAME", "UNDO", "REDO", "PRUNE", "LOAD_STATE"].includes(action.type);
20
- }
21
- /**
22
- * Important note: it is the responsibility of the caller to set the document type
23
- * on the header.
24
- */
25
- export function baseCreateDocument(createState, initialState) {
26
- const state = createState(initialState);
27
- const header = createPresignedHeader();
28
- const phDocument = {
29
- header,
30
- state,
31
- initialState: state,
32
- operations: { global: [], local: [] },
33
- clipboard: [],
34
- };
35
- return phDocument;
36
- }
37
- export function hashDocumentStateForScope(document, scope = "global") {
38
- const stateString = stringifyJson(document.state[scope] || "");
39
- return hashBrowser(stateString);
40
- }
41
- export function readOnly(value) {
42
- return Object.freeze(value);
43
- }
44
- /**
45
- * Maps skipped operations in an array of operations.
46
- * Skipped operations are operations that are ignored during processing.
47
- * @param operations - The array of operations to map.
48
- * @param skippedHeadOperations - The number of operations to skip at the head of the array of operations.
49
- * @returns An array of mapped operations with ignore flag indicating if the operation is skipped.
50
- * @throws Error if the operation index is invalid and there are missing operations.
51
- */
52
- export function mapSkippedOperations(operations, skippedHeadOperations) {
53
- const ops = [...operations];
54
- let skipped = skippedHeadOperations || 0;
55
- let latestOpIndex = ops.length > 0 ? ops[ops.length - 1].index : 0;
56
- const scopeOpsWithIgnore = [];
57
- for (const operation of ops.reverse()) {
58
- if (skipped > 0) {
59
- const operationsDiff = latestOpIndex - operation.index;
60
- skipped -= operationsDiff;
61
- }
62
- if (skipped < 0) {
63
- throw new Error("Invalid operation index, missing operations");
64
- }
65
- const mappedOp = {
66
- ignore: skipped > 0,
67
- operation,
68
- };
69
- // here we add 1 to the skip number because we want to get the number of
70
- // operations that we want to move the pointer back to get the latest valid operation
71
- // operation.skip = 1 means that we want to move the pointer back 2 operations to get to the latest valid operation
72
- const operationSkip = operation.skip > 0 ? operation.skip + 1 : 0;
73
- if (operationSkip > 0 && operationSkip > skipped) {
74
- const skipDiff = operationSkip - skipped;
75
- skipped = skipped + skipDiff;
76
- }
77
- latestOpIndex = operation.index;
78
- scopeOpsWithIgnore.push(mappedOp);
79
- }
80
- return scopeOpsWithIgnore.reverse();
81
- }
82
- /**
83
- * V2 version of mapSkippedOperations for protocol version 2+.
84
- * In V2, all NOOPs have skip=1 and consecutive NOOPs form chains.
85
- * N consecutive NOOPs at any point skip N preceding content operations.
86
- *
87
- * Algorithm: Process from end to start
88
- * - When hitting a NOOP: increment chain length, mark as ignored
89
- * - When hitting a non-NOOP:
90
- * - If chain > 0: decrement chain, mark as ignored (this op was undone)
91
- * - If chain == 0: mark as not ignored (apply this op)
92
- */
93
- export function mapSkippedOperationsV2(operations) {
94
- const ops = [...operations];
95
- const result = [];
96
- let noopChainLength = 0;
97
- for (let i = ops.length - 1; i >= 0; i--) {
98
- const operation = ops[i];
99
- const isNoop = operation.action.type === "NOOP";
100
- if (isNoop) {
101
- noopChainLength++;
102
- result.unshift({ ignore: true, operation });
103
- }
104
- else if (noopChainLength > 0) {
105
- noopChainLength--;
106
- result.unshift({ ignore: true, operation });
107
- }
108
- else {
109
- result.unshift({ ignore: false, operation });
110
- }
111
- }
112
- return result;
113
- }
114
- /**
115
- * V2 garbage collect that returns only operations that should be applied for state.
116
- * Uses the V2 model where consecutive NOOPs form chains.
117
- * Unlike V1 garbageCollect, this preserves ALL operations but marks which to apply.
118
- */
119
- export function garbageCollectV2(sortedOperations) {
120
- const result = [];
121
- let noopChainLength = 0;
122
- for (let i = sortedOperations.length - 1; i >= 0; i--) {
123
- const op = sortedOperations[i];
124
- // Check if this is a NOOP operation
125
- const isNoop = "action" in op &&
126
- op.action.type === "NOOP" &&
127
- op.skip > 0;
128
- if (isNoop) {
129
- noopChainLength++;
130
- // Include the NOOP in result (for operation history)
131
- result.unshift(op);
132
- }
133
- else if (noopChainLength > 0) {
134
- noopChainLength--;
135
- // Skip this operation - it was undone
136
- }
137
- else {
138
- // Include this operation
139
- result.unshift(op);
140
- }
141
- }
142
- return result;
143
- }
144
- // Flattens the mapped operations (with ignore flag) from all scopes into
145
- // a single array and sorts them by timestamp
146
- export function sortMappedOperations(operations) {
147
- return Object.values(operations)
148
- .flatMap((array) => array)
149
- .sort((a, b) => new Date(a.operation.timestampUtcMs).getTime() -
150
- new Date(b.operation.timestampUtcMs).getTime());
151
- }
152
- // Default createState function that just returns the state as-is
153
- const defaultCreateState = (state) => {
154
- return state;
155
- };
156
- // Runs the operations on the initial data using the
157
- // provided document reducer.
158
- // This rebuilds the document according to the provided actions.
159
- export function replayDocument(initialState, operations, reducer, header, dispatch, skipHeaderOperations = {}, options) {
160
- const { checkHashes = true, reuseOperationResultingState, operationResultingStateParser = parseResultingState, skipIndexValidation, } = options || {};
161
- let documentState = initialState;
162
- const operationsToReplay = [];
163
- // Initialize with all scopes found in operations, plus global and local for backward compatibility
164
- const allScopes = new Set([...Object.keys(operations), "global", "local"]);
165
- const initialOperations = {};
166
- for (const scope of allScopes) {
167
- initialOperations[scope] = [];
168
- }
169
- // if operation resulting state is to be used then
170
- // looks for the last operation with state of each
171
- // scope to use it as the starting point and only
172
- // replay operations that follow it
173
- if (reuseOperationResultingState) {
174
- for (const [scope, scopeOperations] of Object.entries(operations)) {
175
- if (!scopeOperations) {
176
- continue;
177
- }
178
- const index = scopeOperations.findLastIndex((s) => !!s.resultingState);
179
- if (index < 0) {
180
- operationsToReplay.push(...scopeOperations);
181
- continue;
182
- }
183
- const opWithState = scopeOperations[index];
184
- if (!opWithState || !opWithState.resultingState)
185
- continue;
186
- try {
187
- const scopeState = operationResultingStateParser(opWithState.resultingState);
188
- documentState = {
189
- ...documentState,
190
- // TODO how to deal with attachments?
191
- [scope]: scopeState,
192
- };
193
- const scopeInitialOps = initialOperations[scope];
194
- if (scopeInitialOps) {
195
- scopeInitialOps.push(...scopeOperations.slice(0, index + 1));
196
- }
197
- operationsToReplay.push(...scopeOperations.slice(index + 1));
198
- }
199
- catch {
200
- /* if parsing fails then keeps replays all scope operations */
201
- operationsToReplay.push(...scopeOperations);
202
- }
203
- }
204
- }
205
- else {
206
- operationsToReplay.push(...Object.values(operations).flatMap((ops) => ops || []));
207
- }
208
- // builds a new document using the provided header (no generated header)
209
- const document = {
210
- header,
211
- state: defaultCreateState(documentState),
212
- initialState,
213
- operations: initialOperations,
214
- clipboard: [],
215
- };
216
- let result = document;
217
- // if there are operations left without resulting state
218
- // then replays them
219
- if (operationsToReplay.length) {
220
- result = operationsToReplay.reduce((document, operation) => {
221
- const doc = reducer(document, operation.action, dispatch, {
222
- ignoreSkipOperations: true,
223
- checkHashes,
224
- skipIndexValidation,
225
- replayOptions: {
226
- operation,
227
- },
228
- });
229
- return doc;
230
- }, document);
231
- }
232
- // if not then updates the document header according
233
- // to the latest operation of each scope
234
- else {
235
- for (const scopeOperations of Object.values(initialOperations)) {
236
- if (!scopeOperations) {
237
- continue;
238
- }
239
- const lastOperation = scopeOperations.at(-1);
240
- if (lastOperation) {
241
- result = updateHeaderRevision(result, lastOperation.action.scope, lastOperation.timestampUtcMs);
242
- }
243
- }
244
- }
245
- // if hash generation was skipped then checks if the hash
246
- // of each scope matches the hash of last operation
247
- if (!checkHashes) {
248
- for (const scope of Object.keys(result.state)) {
249
- for (let i = operationsToReplay.length - 1; i >= 0; i--) {
250
- const operation = operationsToReplay[i];
251
- if (operation.action.scope !== scope) {
252
- continue;
253
- }
254
- if (operation.hash !== hashDocumentStateForScope(result, scope)) {
255
- throw new HashMismatchError(scope, result, operation);
256
- }
257
- else {
258
- break;
259
- }
260
- }
261
- }
262
- }
263
- // reuses operation timestamp if provided
264
- // Initialize with all scopes from both result.operations and input operations
265
- const allResultScopes = new Set([
266
- ...Object.keys(result.operations),
267
- ...Object.keys(operations),
268
- "global",
269
- "local",
270
- ]);
271
- const initialResultOperations = {};
272
- for (const scope of allResultScopes) {
273
- initialResultOperations[scope] = [];
274
- }
275
- // Iterate over all scopes (not just result.operations) to preserve empty scopes
276
- const resultOperations = Array.from(allResultScopes).reduce((acc, scope) => {
277
- const scopeOps = result.operations[scope] || [];
278
- return {
279
- ...acc,
280
- [scope]: [
281
- ...scopeOps.map((operation, index) => {
282
- return {
283
- ...operation,
284
- timestamp: operations[scope]?.[index]?.timestampUtcMs ??
285
- operation.timestampUtcMs,
286
- };
287
- }),
288
- ],
289
- };
290
- }, initialResultOperations);
291
- // gets the last modified timestamp from the latest operation
292
- const lastModified = header
293
- ? header.lastModifiedAtUtcIso
294
- : Object.values(resultOperations).reduce((acc, curr) => {
295
- if (!curr) {
296
- return acc;
297
- }
298
- const operation = curr.at(-1);
299
- if (operation) {
300
- if (operation.timestampUtcMs > acc) {
301
- return operation.timestampUtcMs;
302
- }
303
- }
304
- return acc;
305
- }, document.header.lastModifiedAtUtcIso);
306
- if (header) {
307
- result.header = {
308
- ...header,
309
- revision: result.header.revision,
310
- lastModifiedAtUtcIso: lastModified,
311
- };
312
- }
313
- return {
314
- ...result,
315
- operations: resultOperations,
316
- };
317
- }
318
- export function parseResultingState(state) {
319
- const stateType = typeof state;
320
- if (stateType === "string") {
321
- return JSON.parse(state);
322
- }
323
- else if (stateType === "object") {
324
- return state;
325
- }
326
- else {
327
- throw new Error(`Providing resulting state is of type: ${stateType}`);
328
- }
329
- }
330
- export var IntegrityIssueType;
331
- (function (IntegrityIssueType) {
332
- IntegrityIssueType["UNEXPECTED_INDEX"] = "UNEXPECTED_INDEX";
333
- })(IntegrityIssueType || (IntegrityIssueType = {}));
334
- export var IntegrityIssueSubType;
335
- (function (IntegrityIssueSubType) {
336
- IntegrityIssueSubType["DUPLICATED_INDEX"] = "DUPLICATED_INDEX";
337
- IntegrityIssueSubType["MISSING_INDEX"] = "MISSING_INDEX";
338
- })(IntegrityIssueSubType || (IntegrityIssueSubType = {}));
339
- export function checkCleanedOperationsIntegrity(sortedOperations) {
340
- const result = [];
341
- // 1:1 1
342
- // 0:0 0 -> 1:0 1 -> 2:0 -> 3:0 -> 4:0 -> 5:0
343
- // 0:0 0 -> 2:1 1 -> 3:0 -> 4:0 -> 5:0
344
- // 0:0 0 -> 3:2 1 -> 4:0 -> 5:0
345
- // 0:0 0 -> 3:2 1 -> 5:1
346
- // 0:3 (expected 0, got -3)
347
- // 1:2 (expected 0, got -1)
348
- // 0:0 -> 1:1
349
- // 0:0 -> 2:2
350
- // 0:0 -> 3:2 -> 5:2
351
- let currentIndex = -1;
352
- for (const nextOperation of sortedOperations) {
353
- const nextIndex = nextOperation.index - nextOperation.skip;
354
- if (nextIndex !== currentIndex + 1) {
355
- result.push({
356
- operation: {
357
- index: nextOperation.index,
358
- skip: nextOperation.skip,
359
- },
360
- issue: IntegrityIssueType.UNEXPECTED_INDEX,
361
- category: nextIndex > currentIndex + 1
362
- ? IntegrityIssueSubType.MISSING_INDEX
363
- : IntegrityIssueSubType.DUPLICATED_INDEX,
364
- message: `Expected index ${currentIndex + 1} with skip 0 or equivalent, got index ${nextOperation.index} with skip ${nextOperation.skip}`,
365
- });
366
- }
367
- currentIndex = nextOperation.index;
368
- }
369
- return result;
370
- }
371
- // [] -> []
372
- // [0:0] -> [0:0]
373
- // 0:0 1:0 2:0 => 0:0 1:0 2:0, removals 0, no issues
374
- // 0:0 1:1 2:0 => 1:1 2:0, removals 1, no issues
375
- // 0:0 1:1 2:0 3:1 => 1:1 3:1, removals 2, no issues
376
- // 0:0 1:1 2:0 3:3 => 3:3
377
- // 1:1 2:0 3:0 => 1:1 2:0 3:0, removals 0, no issues
378
- // 1:0 0:0 2:0 => 2:0, removals 2, issues [UNEXPECTED_INDEX, INDEX_OUT_OF_ORDER]
379
- // 0:0 1:0 2:0 => 0:0 1:0 2:0, removals 0, no issues
380
- // 0:0 1:0 2:0 => 0:0 1:0 2:0, removals 0, no issues
381
- // 0:0 1:0 2:0 => 0:0 1:0 2:0, removals 0, no issues
382
- export function garbageCollect(sortedOperations) {
383
- const result = [];
384
- let i = sortedOperations.length - 1;
385
- while (i > -1) {
386
- result.unshift(sortedOperations[i]);
387
- const skipUntil = (sortedOperations[i]?.index || 0) - (sortedOperations[i]?.skip || 0) - 1;
388
- let j = i - 1;
389
- while (j > -1 && (sortedOperations[j]?.index || 0) > skipUntil) {
390
- j--;
391
- }
392
- i = j;
393
- }
394
- return result;
395
- }
396
- export function addUndo(sortedOperations) {
397
- const operationsCopy = [...sortedOperations];
398
- const latestOperation = operationsCopy[operationsCopy.length - 1];
399
- if (!latestOperation)
400
- return operationsCopy;
401
- if (latestOperation.action.type === "NOOP") {
402
- operationsCopy.push({
403
- ...latestOperation,
404
- index: latestOperation.index,
405
- skip: nextSkipNumber(sortedOperations),
406
- action: {
407
- ...latestOperation.action,
408
- // TODO: this will break the signature...
409
- id: generateId(),
410
- timestampUtcMs: new Date().toISOString(),
411
- type: "NOOP",
412
- },
413
- });
414
- }
415
- else {
416
- operationsCopy.push({
417
- id: generateId(),
418
- timestampUtcMs: new Date().toISOString(),
419
- index: latestOperation.index + 1,
420
- skip: 1,
421
- hash: latestOperation.hash,
422
- action: {
423
- id: generateId(),
424
- timestampUtcMs: new Date().toISOString(),
425
- type: "NOOP",
426
- input: {},
427
- scope: latestOperation.action.scope,
428
- },
429
- });
430
- }
431
- return operationsCopy;
432
- }
433
- // [0:0 2:0 1:0 3:3 3:1] => [0:0 1:0 2:0 3:1 3:3]
434
- // Sort by index _and_ skip number
435
- export function sortOperations(operations) {
436
- return operations
437
- .slice()
438
- .sort((a, b) => a.skip - b.skip)
439
- .sort((a, b) => a.index - b.index);
440
- }
441
- // [0:0, 1:0, 2:0, A3:0, A4:0, A5:0] + [0:0, 1:0, 2:0, B3:0, B4:2, B5:0]
442
- // GC => [0:0, 1:0, 2:0, A3:0, A4:0, A5:0] + [0:0, 1:0, B4:2, B5:0]
443
- // Split => [0:0, 1:0] + [2:0, A3:0, A4:0, A5:0] + [B4:2, B5:0]
444
- // Reshuffle(6:4) => [6:4, 7:0, 8:0, 9:0, 10:0, 11:0]
445
- // merge => [0:0, 1:0, 6:4, 7:0, 8:0, 9:0, 10:0, 11:0]
446
- export function reshuffleByTimestamp(startIndex, opsA, opsB) {
447
- return [...opsA, ...opsB]
448
- .sort((a, b) => {
449
- const timestampDiff = new Date(a.timestampUtcMs || "").getTime() -
450
- new Date(b.timestampUtcMs || "").getTime();
451
- if (timestampDiff !== 0) {
452
- return timestampDiff;
453
- }
454
- return (a.id || "").localeCompare(b.id || "");
455
- })
456
- .map((op, i) => ({
457
- ...op,
458
- index: startIndex.index + i,
459
- skip: i === 0 ? startIndex.skip : 0,
460
- }));
461
- }
462
- export function reshuffleByTimestampAndIndex(startIndex, opsA, opsB) {
463
- return [...opsA, ...opsB]
464
- .sort((a, b) => {
465
- const indexDiff = a.index - b.index;
466
- if (indexDiff !== 0) {
467
- return indexDiff;
468
- }
469
- const timestampDiff = new Date(a.timestampUtcMs || "").getTime() -
470
- new Date(b.timestampUtcMs || "").getTime();
471
- if (timestampDiff !== 0) {
472
- return timestampDiff;
473
- }
474
- return (a.id || "").localeCompare(b.id || "");
475
- })
476
- .map((op, i) => ({
477
- ...op,
478
- index: startIndex.index + i,
479
- skip: i === 0 ? startIndex.skip : 0,
480
- }));
481
- }
482
- // TODO: implement better operation equality function
483
- export function operationsAreEqual(op1, op2) {
484
- const a = op1;
485
- const b = op2;
486
- const aComparable = {
487
- index: a.index,
488
- skip: a.skip,
489
- type: a.type ?? null,
490
- scope: a.scope ?? null,
491
- input: a.input ?? null,
492
- };
493
- const bComparable = {
494
- index: b.index,
495
- skip: b.skip,
496
- type: b.type ?? null,
497
- scope: b.scope ?? null,
498
- input: b.input ?? null,
499
- };
500
- return stringify(aComparable) === stringify(bComparable);
501
- }
502
- // [T0:0 T1:0 T2:0 T3:0] + [B4:0 B5:0] = [T0:0 T1:0 T2:0 T3:0 B4:0 B5:0]
503
- // [T0:0 T1:0 T2:0 T3:0] + [B3:0 B4:0] = [T0:0 T1:0 T2:0 B3:0 B4:0]
504
- // [T0:0 T1:0 T2:0 T3:0] + [B2:0 B3:0] = [T0:0 T1:0 B2:0 B3:0]
505
- // [T0:0 T1:0 T2:0 T3:0] + [B4:0 B4:2] = [T0:0 T1:0 T2:0 T3:0 B4:0 B4:2]
506
- // [T0:0 T1:0 T2:0 T3:0] + [B3:0 B3:2] = [T0:0 T1:0 T2:0 B3:0 B3:2]
507
- // [T0:0 T1:0 T2:0 T3:0] + [B2:3 B3:0] = [T0:0 T1:0 B2:3 B3:0]
508
- export function attachBranch(trunk, newBranch) {
509
- const trunkCopy = garbageCollect(sortOperations(trunk.slice()));
510
- const newOperations = garbageCollect(sortOperations(newBranch.slice()));
511
- if (trunkCopy.length < 1) {
512
- return [newOperations, []];
513
- }
514
- const result = [];
515
- let enteredBranch = false;
516
- while (newOperations.length > 0) {
517
- const newOperationCandidate = newOperations[0];
518
- let nextTrunkOperation = trunkCopy.shift();
519
- while (nextTrunkOperation &&
520
- precedes(nextTrunkOperation, newOperationCandidate)) {
521
- result.push(nextTrunkOperation);
522
- nextTrunkOperation = trunkCopy.shift();
523
- }
524
- if (!nextTrunkOperation) {
525
- enteredBranch = true;
526
- }
527
- else if (!enteredBranch) {
528
- if (operationsAreEqual(nextTrunkOperation, newOperationCandidate)) {
529
- newOperations.shift();
530
- result.push(nextTrunkOperation);
531
- }
532
- else {
533
- trunkCopy.unshift(nextTrunkOperation);
534
- enteredBranch = true;
535
- }
536
- }
537
- if (enteredBranch) {
538
- let nextAppend = newOperations.shift();
539
- while (nextAppend) {
540
- result.push(nextAppend);
541
- nextAppend = newOperations.shift();
542
- }
543
- }
544
- }
545
- if (!enteredBranch) {
546
- let nextAppend = trunkCopy.shift();
547
- while (nextAppend) {
548
- result.push(nextAppend);
549
- nextAppend = trunkCopy.shift();
550
- }
551
- }
552
- return [garbageCollect(result), trunkCopy];
553
- }
554
- export function precedes(op1, op2) {
555
- return (op1.index < op2.index ||
556
- (op1.index === op2.index && op1.id === op2.id && op1.skip < op2.skip));
557
- }
558
- export function split(sortedTargetOperations, sortedMergeOperations) {
559
- const commonOperations = [];
560
- const targetDiffOperations = [];
561
- const mergeDiffOperations = [];
562
- // get bigger array length
563
- const maxLength = Math.max(sortedTargetOperations.length, sortedMergeOperations.length);
564
- let splitHappened = false;
565
- for (let i = 0; i < maxLength; i++) {
566
- const targetOperation = sortedTargetOperations[i];
567
- const mergeOperation = sortedMergeOperations[i];
568
- if (targetOperation && mergeOperation) {
569
- if (!splitHappened &&
570
- operationsAreEqual(targetOperation, mergeOperation)) {
571
- commonOperations.push(targetOperation);
572
- }
573
- else {
574
- splitHappened = true;
575
- targetDiffOperations.push(targetOperation);
576
- mergeDiffOperations.push(mergeOperation);
577
- }
578
- }
579
- else if (targetOperation) {
580
- targetDiffOperations.push(targetOperation);
581
- }
582
- else if (mergeOperation) {
583
- mergeDiffOperations.push(mergeOperation);
584
- }
585
- }
586
- return [commonOperations, targetDiffOperations, mergeDiffOperations];
587
- }
588
- // [0:0, 1:0, 2:0, A3:0, A4:0, A5:0] + [0:0, 1:0, 2:0, B3:0, B4:2, B5:0]
589
- // GC => [0:0, 1:0, 2:0, A3:0, A4:0, A5:0] + [0:0, 1:0, B4:2, B5:0]
590
- // Split => [0:0, 1:0] + [2:0, A3:0, A4:0, A5:0] + [B4:2, B5:0]
591
- // Reshuffle(6:4) => [6:4, 7:0, 8:0, 9:0, 10:0, 11:0]
592
- // merge => [0:0, 1:0, 6:4, 7:0, 8:0, 9:0, 10:0, 11:0]
593
- export function merge(sortedTargetOperations, sortedMergeOperations, reshuffle) {
594
- const [_commonOperations, _targetOperations, _mergeOperations] = split(garbageCollect(sortedTargetOperations), garbageCollect(sortedMergeOperations));
595
- const maxCommonIndex = getMaxIndex(_commonOperations);
596
- const nextIndex = 1 +
597
- Math.max(maxCommonIndex, getMaxIndex(_targetOperations), getMaxIndex(_mergeOperations));
598
- const filteredMergeOperations = filterDuplicatedOperations(_mergeOperations, _targetOperations);
599
- const newOperationHistory = reshuffle({
600
- index: nextIndex,
601
- skip: nextIndex - (maxCommonIndex + 1),
602
- }, _targetOperations, filteredMergeOperations);
603
- return _commonOperations.concat(newOperationHistory);
604
- }
605
- function getMaxIndex(sortedOperations) {
606
- const lastElement = sortedOperations[sortedOperations.length - 1];
607
- if (!lastElement) {
608
- return -1;
609
- }
610
- return lastElement.index;
611
- }
612
- // [] => -1
613
- // [0:0] => -1
614
- // [0:0 1:0] => 1
615
- // [0:0 1:1] => -1
616
- // [1:1] => -1
617
- // [0:0 1:0 2:0] => 1
618
- // [0:0 1:0 2:0 2:1] => 2
619
- // [0:0 1:0 2:0 2:1 2:2] => -1
620
- // [0:0 1:1 2:0] => 2
621
- // [0:0 1:1 2:2] => -1
622
- // [0:0 1:1 2:0 3:0] => 1
623
- // [0:0 1:1 2:0 3:1] => 3
624
- // [0:0 1:1 2:0 3:3] => -1
625
- // [50:50 100:50 150:50 151:0 152:0 153:0 154:3] => 53
626
- export function nextSkipNumber(sortedOperations) {
627
- if (sortedOperations.length < 1) {
628
- return -1;
629
- }
630
- const cleanedOperations = garbageCollect(sortedOperations);
631
- let nextSkip = (cleanedOperations[cleanedOperations.length - 1]?.skip || 0) + 1;
632
- if (cleanedOperations.length > 1) {
633
- nextSkip += cleanedOperations[cleanedOperations.length - 2]?.skip || 0;
634
- }
635
- return (cleanedOperations[cleanedOperations.length - 1]?.index || -1) <
636
- nextSkip
637
- ? -1
638
- : nextSkip;
639
- }
640
- export function checkOperationsIntegrity(operations) {
641
- return checkCleanedOperationsIntegrity(garbageCollect(sortOperations(operations)));
642
- }
643
- export function groupOperationsByScope(operations) {
644
- const result = operations.reduce((acc, operation) => {
645
- if (!acc[operation.action.scope]) {
646
- acc[operation.action.scope] = [];
647
- }
648
- acc[operation.action.scope]?.push(operation);
649
- return acc;
650
- }, {});
651
- return result;
652
- }
653
- export function prepareOperations(operationsHistory, newOperations) {
654
- const result = {
655
- integrityIssues: [],
656
- validOperations: [],
657
- invalidOperations: [],
658
- duplicatedOperations: [],
659
- };
660
- const sortedOperationsHistory = sortOperations(operationsHistory);
661
- const sortedOperations = sortOperations(newOperations);
662
- const integrityErrors = checkCleanedOperationsIntegrity([
663
- ...sortedOperationsHistory,
664
- ...sortedOperations,
665
- ]);
666
- const missingIndexErrors = integrityErrors.filter((integrityIssue) => integrityIssue.category === IntegrityIssueSubType.MISSING_INDEX);
667
- // get the integrity error with the lowest index operation
668
- const firstMissingIndexOperation = [...missingIndexErrors]
669
- .sort((a, b) => b.operation.index - a.operation.index)
670
- .pop()?.operation;
671
- for (const newOperation of sortedOperations) {
672
- // Operation is missing index or it follows an operation that is missing index
673
- if (firstMissingIndexOperation &&
674
- newOperation.index >= firstMissingIndexOperation.index) {
675
- result.invalidOperations.push(newOperation);
676
- continue;
677
- }
678
- // check if operation is duplicated
679
- const isDuplicatedOperation = integrityErrors.some((integrityError) => {
680
- return (integrityError.operation.index === newOperation.index &&
681
- integrityError.operation.skip === newOperation.skip &&
682
- integrityError.category === IntegrityIssueSubType.DUPLICATED_INDEX);
683
- });
684
- // add to duplicated operations if it is duplicated
685
- if (isDuplicatedOperation) {
686
- result.duplicatedOperations.push(newOperation);
687
- continue;
688
- }
689
- // otherwise, add to valid operations
690
- result.validOperations.push(newOperation);
691
- }
692
- result.integrityIssues.push(...integrityErrors);
693
- return result;
694
- }
695
- export function removeExistingOperations(newOperations, operationsHistory) {
696
- return newOperations.filter((newOperation) => {
697
- return !operationsHistory.some((historyOperation) => {
698
- return ((newOperation.action.type === "NOOP" &&
699
- newOperation.skip === 0 &&
700
- newOperation.index === historyOperation.index) ||
701
- (newOperation.index === historyOperation.index &&
702
- newOperation.skip === historyOperation.skip &&
703
- newOperation.action.scope === historyOperation.action.scope &&
704
- newOperation.hash === historyOperation.hash &&
705
- newOperation.action.type === historyOperation.action.type));
706
- });
707
- });
708
- }
709
- /**
710
- * Skips header operations and returns the remaining operations.
711
- *
712
- * @param operations - The array of operations.
713
- * @param skipHeaderOperation - The skip header operation index.
714
- * @returns The remaining operations after skipping header operations.
715
- */
716
- export function skipHeaderOperations(operations, skipHeaderOperation) {
717
- const lastOperation = sortOperations(operations).at(-1);
718
- const lastIndex = lastOperation?.index ?? -1;
719
- const nextIndex = lastIndex + 1;
720
- const skipOperationIndex = {
721
- ...skipHeaderOperation,
722
- index: skipHeaderOperation.index ?? nextIndex,
723
- };
724
- if (skipOperationIndex.index < lastIndex) {
725
- throw new Error(`The skip header operation index must be greater than or equal to ${lastIndex}`);
726
- }
727
- const clearedOperations = garbageCollect(sortOperations([...operations, skipOperationIndex]));
728
- return clearedOperations.slice(0, -1); //clearedOperation ? [clearedOperation as TOpIndex] : [];
729
- }
730
- export function garbageCollectDocumentOperations(documentOperations) {
731
- const clearedOperations = Object.entries(documentOperations).reduce((acc, entry) => {
732
- const [scope, ops] = entry;
733
- if (!ops) {
734
- return acc;
735
- }
736
- return {
737
- ...acc,
738
- [scope]: garbageCollect(sortOperations(ops)),
739
- };
740
- }, {});
741
- return clearedOperations;
742
- }
743
- /**
744
- * Filters out duplicated operations from the target operations array based on their IDs.
745
- * If an operation has an ID, it is considered duplicated if there is another operation in the source operations array with the same ID.
746
- * If an operation does not have an ID, it is considered unique and will not be filtered out.
747
- * @param targetOperations - The array of target operations to filter.
748
- * @param sourceOperations - The array of source operations to compare against.
749
- * @returns An array of operations with duplicates filtered out.
750
- */
751
- export function filterDuplicatedOperations(targetOperations, sourceOperations) {
752
- return targetOperations.filter((op) => {
753
- if (op.id) {
754
- return !sourceOperations.some((targetOp) => targetOp.id === op.id);
755
- }
756
- return true;
757
- });
758
- }
759
- export function filterDocumentOperationsResultingState(documentOperations) {
760
- if (!documentOperations) {
761
- return {};
762
- }
763
- const entries = Object.entries(documentOperations);
764
- return entries.reduce((acc, [scope, operations]) => {
765
- if (!operations) {
766
- return acc;
767
- }
768
- return {
769
- ...acc,
770
- [scope]: operations.map((op) => {
771
- const { resultingState, ...restProps } = op;
772
- return restProps;
773
- }),
774
- };
775
- }, {});
776
- }
777
- /**
778
- * Calculates the difference between two arrays of operations.
779
- * Returns an array of operations that are present in `clearedOperationsA` but not in `clearedOperationsB`.
780
- *
781
- * @template TOp - The type of the operations.
782
- * @param {TOp[]} clearedOperationsA - The first array of operations.
783
- * @param {TOp[]} clearedOperationsB - The second array of operations.
784
- * @returns {TOp[]} - The difference between the two arrays of operations.
785
- */
786
- export function diffOperations(clearedOperationsA, clearedOperationsB) {
787
- return clearedOperationsA.filter((operationA) => !clearedOperationsB.some((operationB) => operationA.index === operationB.index));
788
- }
789
- // Returns the timestamp of the latest operation by index (and skip as tiebreaker),
790
- // falling back to the document header's lastModifiedAtUtcIso
791
- export function getDocumentLastModified(document) {
792
- let latest;
793
- for (const ops of Object.values(document.operations)) {
794
- if (!ops)
795
- continue;
796
- for (const op of ops) {
797
- if (!latest ||
798
- op.index > latest.index ||
799
- (op.index === latest.index && op.skip > latest.skip)) {
800
- latest = op;
801
- }
802
- }
803
- }
804
- return latest?.timestampUtcMs || document.header.lastModifiedAtUtcIso;
805
- }
806
- /**
807
- * Gets the next revision number based on the provided scope.
808
- *
809
- * @param state The current state of the document.
810
- * @param scope The scope of the operation.
811
- * @returns The next revision number.
812
- */
813
- function getNextRevision(document, scope) {
814
- const scopeOperations = document.operations[scope];
815
- const maxIndex = scopeOperations?.at(-1)?.index ?? -1;
816
- return maxIndex + 1;
817
- }
818
- /**
819
- * Updates the document header with the latest revision number and
820
- * date of last modification.
821
- *
822
- * @param document The current state of the document.
823
- * @param scope The scope of the operation.
824
- * @param lastModifiedTimestamp Optional timestamp to use directly, avoiding a scan of all operations.
825
- * @returns The updated document state.
826
- */
827
- export function updateHeaderRevision(document, scope, lastModifiedTimestamp) {
828
- const newTimestamp = lastModifiedTimestamp ?? getDocumentLastModified(document);
829
- const currentTimestamp = document.header.lastModifiedAtUtcIso;
830
- const header = {
831
- ...document.header,
832
- revision: {
833
- ...document.header.revision,
834
- [scope]: getNextRevision(document, scope),
835
- },
836
- lastModifiedAtUtcIso: !currentTimestamp || newTimestamp > currentTimestamp
837
- ? newTimestamp
838
- : currentTimestamp,
839
- };
840
- return {
841
- ...document,
842
- header,
843
- };
844
- }
845
- //# sourceMappingURL=documents.js.map