document-model 6.0.0-dev.105 → 6.0.0-dev.106
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.
- package/dist/index.d.ts +81 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +216 -0
- package/dist/index.js.map +1 -0
- package/dist/{src/core/node.d.ts → node.d.mts} +25 -23
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +151 -0
- package/dist/node.mjs.map +1 -0
- package/package.json +11 -15
- package/dist/src/core/actions.d.ts +0 -87
- package/dist/src/core/actions.d.ts.map +0 -1
- package/dist/src/core/actions.js +0 -187
- package/dist/src/core/actions.js.map +0 -1
- package/dist/src/core/controller.d.ts +0 -30
- package/dist/src/core/controller.d.ts.map +0 -1
- package/dist/src/core/controller.js +0 -60
- package/dist/src/core/controller.js.map +0 -1
- package/dist/src/core/crypto.d.ts +0 -8
- package/dist/src/core/crypto.d.ts.map +0 -1
- package/dist/src/core/crypto.js +0 -68
- package/dist/src/core/crypto.js.map +0 -1
- package/dist/src/core/documents.d.ts +0 -147
- package/dist/src/core/documents.d.ts.map +0 -1
- package/dist/src/core/documents.js +0 -845
- package/dist/src/core/documents.js.map +0 -1
- package/dist/src/core/errors.d.ts +0 -21
- package/dist/src/core/errors.d.ts.map +0 -1
- package/dist/src/core/errors.js +0 -46
- package/dist/src/core/errors.js.map +0 -1
- package/dist/src/core/files.d.ts +0 -27
- package/dist/src/core/files.d.ts.map +0 -1
- package/dist/src/core/files.js +0 -90
- package/dist/src/core/files.js.map +0 -1
- package/dist/src/core/header.d.ts +0 -63
- package/dist/src/core/header.d.ts.map +0 -1
- package/dist/src/core/header.js +0 -173
- package/dist/src/core/header.js.map +0 -1
- package/dist/src/core/index.d.ts +0 -16
- package/dist/src/core/index.d.ts.map +0 -1
- package/dist/src/core/index.js +0 -16
- package/dist/src/core/index.js.map +0 -1
- package/dist/src/core/logger-types.d.ts +0 -12
- package/dist/src/core/logger-types.d.ts.map +0 -1
- package/dist/src/core/logger-types.js +0 -2
- package/dist/src/core/logger-types.js.map +0 -1
- package/dist/src/core/logger.d.ts +0 -27
- package/dist/src/core/logger.d.ts.map +0 -1
- package/dist/src/core/logger.js +0 -127
- package/dist/src/core/logger.js.map +0 -1
- package/dist/src/core/node.d.ts.map +0 -1
- package/dist/src/core/node.js +0 -155
- package/dist/src/core/node.js.map +0 -1
- package/dist/src/core/operations.d.ts +0 -50
- package/dist/src/core/operations.d.ts.map +0 -1
- package/dist/src/core/operations.js +0 -126
- package/dist/src/core/operations.js.map +0 -1
- package/dist/src/core/ph-types.d.ts +0 -2
- package/dist/src/core/ph-types.d.ts.map +0 -1
- package/dist/src/core/ph-types.js +0 -2
- package/dist/src/core/ph-types.js.map +0 -1
- package/dist/src/core/reducer.d.ts +0 -63
- package/dist/src/core/reducer.d.ts.map +0 -1
- package/dist/src/core/reducer.js +0 -446
- package/dist/src/core/reducer.js.map +0 -1
- package/dist/src/core/schemas.d.ts +0 -75
- package/dist/src/core/schemas.d.ts.map +0 -1
- package/dist/src/core/schemas.js +0 -116
- package/dist/src/core/schemas.js.map +0 -1
- package/dist/src/core/state.d.ts +0 -26
- package/dist/src/core/state.d.ts.map +0 -1
- package/dist/src/core/state.js +0 -56
- package/dist/src/core/state.js.map +0 -1
- package/dist/src/core/types.d.ts +0 -2
- package/dist/src/core/types.d.ts.map +0 -1
- package/dist/src/core/types.js +0 -2
- package/dist/src/core/types.js.map +0 -1
- package/dist/src/core/utils.d.ts +0 -6
- package/dist/src/core/utils.d.ts.map +0 -1
- package/dist/src/core/utils.js +0 -15
- package/dist/src/core/utils.js.map +0 -1
- package/dist/src/core/validation.d.ts +0 -4
- package/dist/src/core/validation.d.ts.map +0 -1
- package/dist/src/core/validation.js +0 -27
- package/dist/src/core/validation.js.map +0 -1
- package/dist/src/document-model/actions.d.ts +0 -164
- package/dist/src/document-model/actions.d.ts.map +0 -1
- package/dist/src/document-model/actions.js +0 -111
- package/dist/src/document-model/actions.js.map +0 -1
- package/dist/src/document-model/constants.d.ts +0 -6
- package/dist/src/document-model/constants.d.ts.map +0 -1
- package/dist/src/document-model/constants.js +0 -584
- package/dist/src/document-model/constants.js.map +0 -1
- package/dist/src/document-model/controller.d.ts +0 -5
- package/dist/src/document-model/controller.d.ts.map +0 -1
- package/dist/src/document-model/controller.js +0 -5
- package/dist/src/document-model/controller.js.map +0 -1
- package/dist/src/document-model/document-schema.d.ts +0 -69
- package/dist/src/document-model/document-schema.d.ts.map +0 -1
- package/dist/src/document-model/document-schema.js +0 -43
- package/dist/src/document-model/document-schema.js.map +0 -1
- package/dist/src/document-model/document-type.d.ts +0 -2
- package/dist/src/document-model/document-type.d.ts.map +0 -1
- package/dist/src/document-model/document-type.js +0 -2
- package/dist/src/document-model/document-type.js.map +0 -1
- package/dist/src/document-model/files.d.ts +0 -5
- package/dist/src/document-model/files.d.ts.map +0 -1
- package/dist/src/document-model/files.js +0 -9
- package/dist/src/document-model/files.js.map +0 -1
- package/dist/src/document-model/index.d.ts +0 -12
- package/dist/src/document-model/index.d.ts.map +0 -1
- package/dist/src/document-model/index.js +0 -12
- package/dist/src/document-model/index.js.map +0 -1
- package/dist/src/document-model/module.d.ts +0 -3
- package/dist/src/document-model/module.d.ts.map +0 -1
- package/dist/src/document-model/module.js +0 -24
- package/dist/src/document-model/module.js.map +0 -1
- package/dist/src/document-model/reducers.d.ts +0 -11
- package/dist/src/document-model/reducers.d.ts.map +0 -1
- package/dist/src/document-model/reducers.js +0 -618
- package/dist/src/document-model/reducers.js.map +0 -1
- package/dist/src/document-model/schemas.d.ts +0 -189
- package/dist/src/document-model/schemas.d.ts.map +0 -1
- package/dist/src/document-model/schemas.js +0 -388
- package/dist/src/document-model/schemas.js.map +0 -1
- package/dist/src/document-model/state.d.ts +0 -15
- package/dist/src/document-model/state.d.ts.map +0 -1
- package/dist/src/document-model/state.js +0 -75
- package/dist/src/document-model/state.js.map +0 -1
- package/dist/src/document-model/types.d.ts +0 -584
- package/dist/src/document-model/types.d.ts.map +0 -1
- package/dist/src/document-model/types.js +0 -2
- package/dist/src/document-model/types.js.map +0 -1
- package/dist/src/document-model/validation.d.ts +0 -32
- package/dist/src/document-model/validation.d.ts.map +0 -1
- package/dist/src/document-model/validation.js +0 -166
- package/dist/src/document-model/validation.js.map +0 -1
- package/dist/src/index.d.ts +0 -6
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -3
- package/dist/src/index.js.map +0 -1
- package/dist/test/document/crypto.test.d.ts +0 -2
- package/dist/test/document/crypto.test.d.ts.map +0 -1
- package/dist/test/document/crypto.test.js +0 -192
- package/dist/test/document/crypto.test.js.map +0 -1
- package/dist/test/document/event-vs-command.test.d.ts +0 -2
- package/dist/test/document/event-vs-command.test.d.ts.map +0 -1
- package/dist/test/document/event-vs-command.test.js +0 -202
- package/dist/test/document/event-vs-command.test.js.map +0 -1
- package/dist/test/document/local.test.d.ts +0 -2
- package/dist/test/document/local.test.d.ts.map +0 -1
- package/dist/test/document/local.test.js +0 -226
- package/dist/test/document/local.test.js.map +0 -1
- package/dist/test/document/operation-id.test.d.ts +0 -2
- package/dist/test/document/operation-id.test.d.ts.map +0 -1
- package/dist/test/document/operation-id.test.js +0 -118
- package/dist/test/document/operation-id.test.js.map +0 -1
- package/dist/test/document/prune.test.d.ts +0 -2
- package/dist/test/document/prune.test.d.ts.map +0 -1
- package/dist/test/document/prune.test.js +0 -159
- package/dist/test/document/prune.test.js.map +0 -1
- package/dist/test/document/reducer.test.d.ts +0 -2
- package/dist/test/document/reducer.test.d.ts.map +0 -1
- package/dist/test/document/reducer.test.js +0 -284
- package/dist/test/document/reducer.test.js.map +0 -1
- package/dist/test/document/skip-operations.test.d.ts +0 -2
- package/dist/test/document/skip-operations.test.d.ts.map +0 -1
- package/dist/test/document/skip-operations.test.js +0 -495
- package/dist/test/document/skip-operations.test.js.map +0 -1
- package/dist/test/document/undo-redo-v2.test.d.ts +0 -2
- package/dist/test/document/undo-redo-v2.test.d.ts.map +0 -1
- package/dist/test/document/undo-redo-v2.test.js +0 -207
- package/dist/test/document/undo-redo-v2.test.js.map +0 -1
- package/dist/test/document/undo-redo.test.d.ts +0 -2
- package/dist/test/document/undo-redo.test.d.ts.map +0 -1
- package/dist/test/document/undo-redo.test.js +0 -413
- package/dist/test/document/undo-redo.test.js.map +0 -1
- package/dist/test/document/utils.test.d.ts +0 -2
- package/dist/test/document/utils.test.d.ts.map +0 -1
- package/dist/test/document/utils.test.js +0 -172
- package/dist/test/document/utils.test.js.map +0 -1
- package/dist/test/document-helpers/addUndo.test.d.ts +0 -2
- package/dist/test/document-helpers/addUndo.test.d.ts.map +0 -1
- package/dist/test/document-helpers/addUndo.test.js +0 -120
- package/dist/test/document-helpers/addUndo.test.js.map +0 -1
- package/dist/test/document-helpers/attachBranch.test.d.ts +0 -2
- package/dist/test/document-helpers/attachBranch.test.d.ts.map +0 -1
- package/dist/test/document-helpers/attachBranch.test.js +0 -364
- package/dist/test/document-helpers/attachBranch.test.js.map +0 -1
- package/dist/test/document-helpers/checkCleanedOperationsIntegrity.test.d.ts +0 -2
- package/dist/test/document-helpers/checkCleanedOperationsIntegrity.test.d.ts.map +0 -1
- package/dist/test/document-helpers/checkCleanedOperationsIntegrity.test.js +0 -252
- package/dist/test/document-helpers/checkCleanedOperationsIntegrity.test.js.map +0 -1
- package/dist/test/document-helpers/conflictResolution.test.d.ts +0 -2
- package/dist/test/document-helpers/conflictResolution.test.d.ts.map +0 -1
- package/dist/test/document-helpers/conflictResolution.test.js +0 -109
- package/dist/test/document-helpers/conflictResolution.test.js.map +0 -1
- package/dist/test/document-helpers/filterDuplicatedOperations.test.d.ts +0 -2
- package/dist/test/document-helpers/filterDuplicatedOperations.test.d.ts.map +0 -1
- package/dist/test/document-helpers/filterDuplicatedOperations.test.js +0 -126
- package/dist/test/document-helpers/filterDuplicatedOperations.test.js.map +0 -1
- package/dist/test/document-helpers/garbageCollect.test.d.ts +0 -2
- package/dist/test/document-helpers/garbageCollect.test.d.ts.map +0 -1
- package/dist/test/document-helpers/garbageCollect.test.js +0 -136
- package/dist/test/document-helpers/garbageCollect.test.js.map +0 -1
- package/dist/test/document-helpers/groupOperationsByScope.test.d.ts +0 -2
- package/dist/test/document-helpers/groupOperationsByScope.test.d.ts.map +0 -1
- package/dist/test/document-helpers/groupOperationsByScope.test.js +0 -102
- package/dist/test/document-helpers/groupOperationsByScope.test.js.map +0 -1
- package/dist/test/document-helpers/headerRevision.test.d.ts +0 -2
- package/dist/test/document-helpers/headerRevision.test.d.ts.map +0 -1
- package/dist/test/document-helpers/headerRevision.test.js +0 -203
- package/dist/test/document-helpers/headerRevision.test.js.map +0 -1
- package/dist/test/document-helpers/merge.test.d.ts +0 -2
- package/dist/test/document-helpers/merge.test.d.ts.map +0 -1
- package/dist/test/document-helpers/merge.test.js +0 -906
- package/dist/test/document-helpers/merge.test.js.map +0 -1
- package/dist/test/document-helpers/nextSkipNumber.test.d.ts +0 -2
- package/dist/test/document-helpers/nextSkipNumber.test.d.ts.map +0 -1
- package/dist/test/document-helpers/nextSkipNumber.test.js +0 -135
- package/dist/test/document-helpers/nextSkipNumber.test.js.map +0 -1
- package/dist/test/document-helpers/prepareOperations.test.d.ts +0 -2
- package/dist/test/document-helpers/prepareOperations.test.d.ts.map +0 -1
- package/dist/test/document-helpers/prepareOperations.test.js +0 -304
- package/dist/test/document-helpers/prepareOperations.test.js.map +0 -1
- package/dist/test/document-helpers/removeExistingOperations.test.d.ts +0 -2
- package/dist/test/document-helpers/removeExistingOperations.test.d.ts.map +0 -1
- package/dist/test/document-helpers/removeExistingOperations.test.js +0 -177
- package/dist/test/document-helpers/removeExistingOperations.test.js.map +0 -1
- package/dist/test/document-helpers/reshuffleByTimestamp.test.d.ts +0 -2
- package/dist/test/document-helpers/reshuffleByTimestamp.test.d.ts.map +0 -1
- package/dist/test/document-helpers/reshuffleByTimestamp.test.js +0 -148
- package/dist/test/document-helpers/reshuffleByTimestamp.test.js.map +0 -1
- package/dist/test/document-helpers/reshuffleByTimestampAndIndex.test.d.ts +0 -2
- package/dist/test/document-helpers/reshuffleByTimestampAndIndex.test.d.ts.map +0 -1
- package/dist/test/document-helpers/reshuffleByTimestampAndIndex.test.js +0 -200
- package/dist/test/document-helpers/reshuffleByTimestampAndIndex.test.js.map +0 -1
- package/dist/test/document-helpers/skipHeaderOperations.test.d.ts +0 -2
- package/dist/test/document-helpers/skipHeaderOperations.test.d.ts.map +0 -1
- package/dist/test/document-helpers/skipHeaderOperations.test.js +0 -72
- package/dist/test/document-helpers/skipHeaderOperations.test.js.map +0 -1
- package/dist/test/document-helpers/sortOperations.test.d.ts +0 -2
- package/dist/test/document-helpers/sortOperations.test.d.ts.map +0 -1
- package/dist/test/document-helpers/sortOperations.test.js +0 -101
- package/dist/test/document-helpers/sortOperations.test.js.map +0 -1
- package/dist/test/document-helpers/split.test.d.ts +0 -2
- package/dist/test/document-helpers/split.test.d.ts.map +0 -1
- package/dist/test/document-helpers/split.test.js +0 -121
- package/dist/test/document-helpers/split.test.js.map +0 -1
- package/dist/test/document-helpers/utils.d.ts +0 -15
- package/dist/test/document-helpers/utils.d.ts.map +0 -1
- package/dist/test/document-helpers/utils.js +0 -61
- package/dist/test/document-helpers/utils.js.map +0 -1
- package/dist/test/document-model/replay.test.d.ts +0 -2
- package/dist/test/document-model/replay.test.d.ts.map +0 -1
- package/dist/test/document-model/replay.test.js +0 -139
- package/dist/test/document-model/replay.test.js.map +0 -1
- package/dist/test/document-model/skip-operations.test.d.ts +0 -2
- package/dist/test/document-model/skip-operations.test.d.ts.map +0 -1
- package/dist/test/document-model/skip-operations.test.js +0 -214
- package/dist/test/document-model/skip-operations.test.js.map +0 -1
- package/dist/test/document-model/validation.test.d.ts +0 -2
- package/dist/test/document-model/validation.test.d.ts.map +0 -1
- package/dist/test/document-model/validation.test.js +0 -451
- package/dist/test/document-model/validation.test.js.map +0 -1
- package/dist/test/document-model/versioning.test.d.ts +0 -2
- package/dist/test/document-model/versioning.test.d.ts.map +0 -1
- package/dist/test/document-model/versioning.test.js +0 -93
- package/dist/test/document-model/versioning.test.js.map +0 -1
- package/dist/test/document-model/zip.test.d.ts +0 -2
- package/dist/test/document-model/zip.test.d.ts.map +0 -1
- package/dist/test/document-model/zip.test.js +0 -79
- package/dist/test/document-model/zip.test.js.map +0 -1
- package/dist/test/helpers.d.ts +0 -79
- package/dist/test/helpers.d.ts.map +0 -1
- package/dist/test/helpers.js +0 -121
- package/dist/test/helpers.js.map +0 -1
- package/dist/test/index.d.ts +0 -4
- package/dist/test/index.d.ts.map +0 -1
- package/dist/test/index.js +0 -3
- package/dist/test/index.js.map +0 -1
- package/dist/test/types.d.ts +0 -6
- package/dist/test/types.d.ts.map +0 -1
- package/dist/test/types.js +0 -2
- package/dist/test/types.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/dist/vitest.config.d.ts +0 -3
- package/dist/vitest.config.d.ts.map +0 -1
- package/dist/vitest.config.js +0 -7
- 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
|