mobx-keystone-loro 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/mobx-keystone-loro.esm.js +957 -0
- package/dist/mobx-keystone-loro.esm.mjs +957 -0
- package/dist/mobx-keystone-loro.umd.js +957 -0
- package/dist/types/binding/LoroTextModel.d.ts +72 -0
- package/dist/types/binding/applyLoroEventToMobx.d.ts +9 -0
- package/dist/types/binding/applyMobxChangeToLoroObject.d.ts +7 -0
- package/dist/types/binding/bindLoroToMobxKeystone.d.ts +33 -0
- package/dist/types/binding/convertJsonToLoroData.d.ts +51 -0
- package/dist/types/binding/convertLoroDataToJson.d.ts +11 -0
- package/dist/types/binding/loroBindingContext.d.ts +39 -0
- package/dist/types/binding/loroSnapshotTracking.d.ts +26 -0
- package/dist/types/binding/moveWithinArray.d.ts +36 -0
- package/dist/types/binding/resolveLoroPath.d.ts +11 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/plainTypes.d.ts +6 -0
- package/dist/types/utils/error.d.ts +4 -0
- package/dist/types/utils/getOrCreateLoroCollectionAtom.d.ts +7 -0
- package/dist/types/utils/isBindableLoroContainer.d.ts +9 -0
- package/package.json +92 -0
- package/src/binding/LoroTextModel.ts +211 -0
- package/src/binding/applyLoroEventToMobx.ts +280 -0
- package/src/binding/applyMobxChangeToLoroObject.ts +182 -0
- package/src/binding/bindLoroToMobxKeystone.ts +353 -0
- package/src/binding/convertJsonToLoroData.ts +315 -0
- package/src/binding/convertLoroDataToJson.ts +68 -0
- package/src/binding/loroBindingContext.ts +46 -0
- package/src/binding/loroSnapshotTracking.ts +36 -0
- package/src/binding/moveWithinArray.ts +112 -0
- package/src/binding/resolveLoroPath.ts +37 -0
- package/src/index.ts +16 -0
- package/src/plainTypes.ts +7 -0
- package/src/utils/error.ts +12 -0
- package/src/utils/getOrCreateLoroCollectionAtom.ts +17 -0
- package/src/utils/isBindableLoroContainer.ts +13 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Delta, LoroText } from 'loro-crdt';
|
|
2
|
+
import { SnapshotOutOf } from 'mobx-keystone';
|
|
3
|
+
export declare const loroTextModelId = "mobx-keystone-loro/LoroTextModel";
|
|
4
|
+
/**
|
|
5
|
+
* Type for the delta stored in the model.
|
|
6
|
+
* Uses Quill delta format: array of operations with insert, delete, retain.
|
|
7
|
+
*/
|
|
8
|
+
export type LoroTextDeltaList = Delta<string>[];
|
|
9
|
+
declare const LoroTextModel_base: import('mobx-keystone')._Model<unknown, {
|
|
10
|
+
/**
|
|
11
|
+
* The current delta representing the rich text content.
|
|
12
|
+
* Uses Quill delta format.
|
|
13
|
+
*/
|
|
14
|
+
deltaList: import('mobx-keystone').OptionalModelProp<import('mobx-keystone').Frozen<LoroTextDeltaList>>;
|
|
15
|
+
}, never, never>;
|
|
16
|
+
/**
|
|
17
|
+
* A mobx-keystone model representing Loro rich text.
|
|
18
|
+
* This model stores the current delta state (Quill format) and syncs with LoroText.
|
|
19
|
+
*/
|
|
20
|
+
export declare class LoroTextModel extends LoroTextModel_base {
|
|
21
|
+
/**
|
|
22
|
+
* Creates a LoroTextModel with initial text content.
|
|
23
|
+
*/
|
|
24
|
+
static withText(text: string): LoroTextModel;
|
|
25
|
+
/**
|
|
26
|
+
* Creates a LoroTextModel with initial delta.
|
|
27
|
+
*/
|
|
28
|
+
static withDelta(delta: LoroTextDeltaList): LoroTextModel;
|
|
29
|
+
/**
|
|
30
|
+
* Atom that gets changed when the associated Loro text changes.
|
|
31
|
+
*/
|
|
32
|
+
loroTextChangedAtom: import('mobx').IAtom;
|
|
33
|
+
/**
|
|
34
|
+
* The LoroText object represented by this mobx-keystone node, if bound.
|
|
35
|
+
* Returns undefined when the model is not part of a bound object tree.
|
|
36
|
+
*/
|
|
37
|
+
get loroText(): LoroText | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Gets the plain text content.
|
|
40
|
+
* This always uses the stored delta, which is kept in sync with Loro.
|
|
41
|
+
*/
|
|
42
|
+
get text(): string;
|
|
43
|
+
/**
|
|
44
|
+
* Gets the current delta (Quill format).
|
|
45
|
+
*/
|
|
46
|
+
get currentDelta(): LoroTextDeltaList;
|
|
47
|
+
/**
|
|
48
|
+
* Converts delta to plain text.
|
|
49
|
+
*/
|
|
50
|
+
private deltaToText;
|
|
51
|
+
/**
|
|
52
|
+
* Sets the delta.
|
|
53
|
+
*/
|
|
54
|
+
setDelta(delta: LoroTextDeltaList): void;
|
|
55
|
+
/**
|
|
56
|
+
* Inserts text at the specified position.
|
|
57
|
+
*/
|
|
58
|
+
insertText(index: number, text: string): void;
|
|
59
|
+
/**
|
|
60
|
+
* Deletes text at the specified range.
|
|
61
|
+
*/
|
|
62
|
+
deleteText(index: number, length: number): void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Type for LoroTextModel for use with tProp.
|
|
66
|
+
*/
|
|
67
|
+
export declare const loroTextModelType: import('mobx-keystone').ModelType<LoroTextModel>;
|
|
68
|
+
/**
|
|
69
|
+
* Checks if a snapshot is a LoroTextModel snapshot.
|
|
70
|
+
*/
|
|
71
|
+
export declare function isLoroTextModelSnapshot(value: unknown): value is SnapshotOutOf<LoroTextModel>;
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ContainerID, LoroDoc, LoroEvent } from 'loro-crdt';
|
|
2
|
+
import { Path } from 'mobx-keystone';
|
|
3
|
+
export type ReconciliationMap = Map<string, object>;
|
|
4
|
+
/**
|
|
5
|
+
* Applies a Loro event directly to the MobX model tree using proper mutations
|
|
6
|
+
* (splice for arrays, property assignment for objects).
|
|
7
|
+
* This is more efficient than converting to patches first.
|
|
8
|
+
*/
|
|
9
|
+
export declare function applyLoroEventToMobx(event: LoroEvent, loroDoc: LoroDoc, boundObject: object, rootPath: Path, reconciliationMap: ReconciliationMap, newlyInsertedContainers: Set<ContainerID>): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DeepChange } from 'mobx-keystone';
|
|
2
|
+
import { BindableLoroContainer } from '../utils/isBindableLoroContainer';
|
|
3
|
+
import { ArrayMoveChange } from './moveWithinArray';
|
|
4
|
+
/**
|
|
5
|
+
* Applies a MobX DeepChange or an ArrayMoveChange to a Loro object.
|
|
6
|
+
*/
|
|
7
|
+
export declare function applyMobxChangeToLoroObject(change: DeepChange | ArrayMoveChange, loroObject: BindableLoroContainer): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { LoroDoc } from 'loro-crdt';
|
|
2
|
+
import { AnyDataModel, AnyModel, AnyStandardType, ModelClass, TypeToData } from 'mobx-keystone';
|
|
3
|
+
import { BindableLoroContainer } from '../utils/isBindableLoroContainer';
|
|
4
|
+
/**
|
|
5
|
+
* Creates a bidirectional binding between a Loro data structure and a mobx-keystone model.
|
|
6
|
+
*/
|
|
7
|
+
export declare function bindLoroToMobxKeystone<TType extends AnyStandardType | ModelClass<AnyModel> | ModelClass<AnyDataModel>>({ loroDoc, loroObject, mobxKeystoneType, }: {
|
|
8
|
+
/**
|
|
9
|
+
* The Loro document.
|
|
10
|
+
*/
|
|
11
|
+
loroDoc: LoroDoc;
|
|
12
|
+
/**
|
|
13
|
+
* The bound Loro data structure.
|
|
14
|
+
*/
|
|
15
|
+
loroObject: BindableLoroContainer;
|
|
16
|
+
/**
|
|
17
|
+
* The mobx-keystone model type.
|
|
18
|
+
*/
|
|
19
|
+
mobxKeystoneType: TType;
|
|
20
|
+
}): {
|
|
21
|
+
/**
|
|
22
|
+
* The bound mobx-keystone instance.
|
|
23
|
+
*/
|
|
24
|
+
boundObject: TypeToData<TType>;
|
|
25
|
+
/**
|
|
26
|
+
* Disposes the binding.
|
|
27
|
+
*/
|
|
28
|
+
dispose: () => void;
|
|
29
|
+
/**
|
|
30
|
+
* The Loro origin string used for binding transactions.
|
|
31
|
+
*/
|
|
32
|
+
loroOrigin: string;
|
|
33
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Delta, LoroMap, LoroMovableList, LoroText } from 'loro-crdt';
|
|
2
|
+
import { PlainArray, PlainObject, PlainValue } from '../plainTypes';
|
|
3
|
+
import { BindableLoroContainer } from '../utils/isBindableLoroContainer';
|
|
4
|
+
type LoroValue = BindableLoroContainer | PlainValue;
|
|
5
|
+
/**
|
|
6
|
+
* Options for applying JSON data to Loro data structures.
|
|
7
|
+
*/
|
|
8
|
+
export interface ApplyJsonToLoroOptions {
|
|
9
|
+
/**
|
|
10
|
+
* The mode to use when applying JSON data to Loro data structures.
|
|
11
|
+
* - `add`: Creates new Loro containers for objects/arrays (default, backwards compatible)
|
|
12
|
+
* - `merge`: Recursively merges values, preserving existing container references where possible
|
|
13
|
+
*/
|
|
14
|
+
mode?: "add" | "merge";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Extracts delta array from a LoroTextModel snapshot's delta field.
|
|
18
|
+
* The delta field is a frozen Delta<string>[] (array of delta operations).
|
|
19
|
+
*/
|
|
20
|
+
export declare function extractTextDeltaFromSnapshot(delta: unknown): Delta<string>[];
|
|
21
|
+
/**
|
|
22
|
+
* Applies delta operations to a LoroText using insert/mark APIs.
|
|
23
|
+
* This works on both attached and detached containers.
|
|
24
|
+
*
|
|
25
|
+
* Strategy: Insert all text first, then apply marks. This avoids mark inheritance
|
|
26
|
+
* issues when inserting at the boundary of a marked region.
|
|
27
|
+
*/
|
|
28
|
+
export declare function applyDeltaToLoroText(text: LoroText, deltas: Delta<string>[]): void;
|
|
29
|
+
/**
|
|
30
|
+
* Converts a plain value to a Loro data structure.
|
|
31
|
+
* Objects are converted to LoroMaps, arrays to LoroMovableLists, primitives are untouched.
|
|
32
|
+
* Frozen values are a special case and they are kept as immutable plain values.
|
|
33
|
+
*/
|
|
34
|
+
export declare function convertJsonToLoroData(v: PlainValue): LoroValue;
|
|
35
|
+
/**
|
|
36
|
+
* Applies a JSON array to a LoroMovableList, using convertJsonToLoroData to convert the values.
|
|
37
|
+
*
|
|
38
|
+
* @param dest The destination LoroMovableList.
|
|
39
|
+
* @param source The source JSON array.
|
|
40
|
+
* @param options Options for applying the JSON data.
|
|
41
|
+
*/
|
|
42
|
+
export declare const applyJsonArrayToLoroMovableList: (dest: LoroMovableList, source: PlainArray, options?: ApplyJsonToLoroOptions) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Applies a JSON object to a LoroMap, using convertJsonToLoroData to convert the values.
|
|
45
|
+
*
|
|
46
|
+
* @param dest The destination LoroMap.
|
|
47
|
+
* @param source The source JSON object.
|
|
48
|
+
* @param options Options for applying the JSON data.
|
|
49
|
+
*/
|
|
50
|
+
export declare const applyJsonObjectToLoroMap: (dest: LoroMap, source: PlainObject, options?: ApplyJsonToLoroOptions) => void;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PlainValue } from '../plainTypes';
|
|
2
|
+
import { BindableLoroContainer } from '../utils/isBindableLoroContainer';
|
|
3
|
+
type LoroValue = BindableLoroContainer | PlainValue;
|
|
4
|
+
/**
|
|
5
|
+
* Converts Loro data to JSON-compatible format for mobx-keystone snapshots.
|
|
6
|
+
*
|
|
7
|
+
* @param value The Loro value to convert
|
|
8
|
+
* @returns JSON-compatible value
|
|
9
|
+
*/
|
|
10
|
+
export declare function convertLoroDataToJson(value: LoroValue): PlainValue;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { LoroDoc } from 'loro-crdt';
|
|
2
|
+
import { AnyType } from 'mobx-keystone';
|
|
3
|
+
import { BindableLoroContainer } from '../utils/isBindableLoroContainer';
|
|
4
|
+
/**
|
|
5
|
+
* Context for the Loro binding, providing access to the Loro document
|
|
6
|
+
* and binding state from within mobx-keystone models.
|
|
7
|
+
*/
|
|
8
|
+
export interface LoroBindingContext {
|
|
9
|
+
/**
|
|
10
|
+
* The Loro document being bound.
|
|
11
|
+
*/
|
|
12
|
+
loroDoc: LoroDoc;
|
|
13
|
+
/**
|
|
14
|
+
* The root Loro object being bound.
|
|
15
|
+
*/
|
|
16
|
+
loroObject: BindableLoroContainer;
|
|
17
|
+
/**
|
|
18
|
+
* The mobx-keystone model type being used.
|
|
19
|
+
*/
|
|
20
|
+
mobxKeystoneType: AnyType;
|
|
21
|
+
/**
|
|
22
|
+
* String used as origin for Loro transactions to identify changes
|
|
23
|
+
* coming from mobx-keystone.
|
|
24
|
+
*/
|
|
25
|
+
loroOrigin: string;
|
|
26
|
+
/**
|
|
27
|
+
* The bound mobx-keystone object (once created).
|
|
28
|
+
*/
|
|
29
|
+
boundObject: unknown;
|
|
30
|
+
/**
|
|
31
|
+
* Whether changes are currently being applied from Loro to mobx-keystone.
|
|
32
|
+
* Used to prevent infinite loops.
|
|
33
|
+
*/
|
|
34
|
+
isApplyingLoroChangesToMobxKeystone: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Context for accessing the Loro binding from within mobx-keystone models.
|
|
38
|
+
*/
|
|
39
|
+
export declare const loroBindingContext: import('mobx-keystone').Context<LoroBindingContext | undefined>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { LoroMap, LoroMovableList } from 'loro-crdt';
|
|
2
|
+
type LoroContainer = LoroMap | LoroMovableList;
|
|
3
|
+
/**
|
|
4
|
+
* WeakMap that tracks which snapshot each Loro container was last synced from.
|
|
5
|
+
* This is used during reconciliation to skip containers that are already up-to-date.
|
|
6
|
+
*
|
|
7
|
+
* The key is the Loro container (LoroMap or LoroMovableList).
|
|
8
|
+
* The value is the snapshot (plain object or array) that was last synced to it.
|
|
9
|
+
*/
|
|
10
|
+
export declare const loroContainerToSnapshot: WeakMap<LoroContainer, unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* Updates the snapshot tracking for a Loro container.
|
|
13
|
+
* Call this after syncing a snapshot to a Loro container.
|
|
14
|
+
*/
|
|
15
|
+
export declare function setLoroContainerSnapshot(container: LoroContainer, snapshot: unknown): void;
|
|
16
|
+
/**
|
|
17
|
+
* Gets the last synced snapshot for a Loro container.
|
|
18
|
+
* Returns undefined if the container has never been synced.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getLoroContainerSnapshot(container: LoroContainer): unknown;
|
|
21
|
+
/**
|
|
22
|
+
* Checks if a Loro container is up-to-date with the given snapshot.
|
|
23
|
+
* Uses reference equality to check if the snapshot is the same.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isLoroContainerUpToDate(container: LoroContainer, snapshot: unknown): boolean;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DeepChange } from 'mobx-keystone';
|
|
2
|
+
/**
|
|
3
|
+
* Synthetic change type for array moves.
|
|
4
|
+
*/
|
|
5
|
+
export interface ArrayMoveChange {
|
|
6
|
+
type: "ArrayMove";
|
|
7
|
+
path: readonly (string | number)[];
|
|
8
|
+
fromIndex: number;
|
|
9
|
+
toIndex: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Moves an item within an array from one index to another.
|
|
13
|
+
*
|
|
14
|
+
* When used on a mobx-keystone array bound to a Loro movable list,
|
|
15
|
+
* this translates to a native Loro move operation for optimal CRDT merging.
|
|
16
|
+
*
|
|
17
|
+
* For unbound arrays, performs a standard splice-based move.
|
|
18
|
+
*
|
|
19
|
+
* @param array The array to move within
|
|
20
|
+
* @param fromIndex The index of the item to move
|
|
21
|
+
* @param toIndex The target index to move the item to
|
|
22
|
+
*/
|
|
23
|
+
export declare function moveWithinArray<T>(array: T[], fromIndex: number, toIndex: number): void;
|
|
24
|
+
/**
|
|
25
|
+
* Check if a change is part of an active move operation and process it.
|
|
26
|
+
* This is called for ArraySplice changes on the target array.
|
|
27
|
+
*
|
|
28
|
+
* @param change The deep change (must be ArraySplice type)
|
|
29
|
+
* @returns The ArrayMoveChange if the move is complete (second splice),
|
|
30
|
+
* undefined if intercepted but not complete (first splice)
|
|
31
|
+
*/
|
|
32
|
+
export declare function processChangeForMove(change: DeepChange): ArrayMoveChange | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Check if we're currently in a move context for a specific array.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isInMoveContextForArray(array: unknown[]): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Path } from 'mobx-keystone';
|
|
2
|
+
import { BindableLoroContainer } from '../utils/isBindableLoroContainer';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves a path within a Loro object structure.
|
|
5
|
+
* Returns the Loro container at the specified path.
|
|
6
|
+
*
|
|
7
|
+
* @param loroObject The root Loro object
|
|
8
|
+
* @param path Array of keys/indices to traverse
|
|
9
|
+
* @returns The Loro container at the path
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveLoroPath(loroObject: BindableLoroContainer, path: Path): unknown;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { bindLoroToMobxKeystone } from './binding/bindLoroToMobxKeystone';
|
|
2
|
+
export type { ApplyJsonToLoroOptions } from './binding/convertJsonToLoroData';
|
|
3
|
+
export { applyJsonArrayToLoroMovableList, applyJsonObjectToLoroMap, convertJsonToLoroData, } from './binding/convertJsonToLoroData';
|
|
4
|
+
export { isLoroTextModelSnapshot, LoroTextModel, loroTextModelType, } from './binding/LoroTextModel';
|
|
5
|
+
export type { LoroBindingContext } from './binding/loroBindingContext';
|
|
6
|
+
export { loroBindingContext } from './binding/loroBindingContext';
|
|
7
|
+
export { moveWithinArray } from './binding/moveWithinArray';
|
|
8
|
+
export { MobxKeystoneLoroError } from './utils/error';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { IAtom } from 'mobx';
|
|
2
|
+
import { BindableLoroContainer } from './isBindableLoroContainer';
|
|
3
|
+
/**
|
|
4
|
+
* Gets or creates a MobX atom for a Loro collection.
|
|
5
|
+
* This is used to track reactivity for computed properties that read from Loro containers.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getOrCreateLoroCollectionAtom(collection: BindableLoroContainer): IAtom;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { LoroMap, LoroMovableList, LoroText } from 'loro-crdt';
|
|
2
|
+
/**
|
|
3
|
+
* A bindable Loro container (Map, MovableList, or Text).
|
|
4
|
+
*/
|
|
5
|
+
export type BindableLoroContainer = LoroMap | LoroMovableList | LoroText;
|
|
6
|
+
/**
|
|
7
|
+
* Checks if a value is a bindable Loro container.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isBindableLoroContainer(value: unknown): value is BindableLoroContainer;
|
package/package.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mobx-keystone-loro",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Loro CRDT bindings for mobx-keystone",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mobx",
|
|
7
|
+
"mobx-keystone",
|
|
8
|
+
"loro",
|
|
9
|
+
"loro-crdt",
|
|
10
|
+
"crdt",
|
|
11
|
+
"state",
|
|
12
|
+
"state-management",
|
|
13
|
+
"reactive",
|
|
14
|
+
"collaborative"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/xaviergonz/mobx-keystone.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/xaviergonz/mobx-keystone/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://mobx-keystone.js.org",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "Javier González Garcés",
|
|
26
|
+
"source": "./src/index.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
"./package.json": "./package.json",
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/types/index.d.ts",
|
|
31
|
+
"script": "./dist/mobx-keystone-loro.umd.js",
|
|
32
|
+
"import": "./dist/mobx-keystone-loro.esm.mjs",
|
|
33
|
+
"require": "./dist/mobx-keystone-loro.umd.js",
|
|
34
|
+
"default": "./dist/mobx-keystone-loro.esm.mjs"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"esmodule": "./dist/mobx-keystone-loro.esm.js",
|
|
38
|
+
"module": "./dist/mobx-keystone-loro.esm.js",
|
|
39
|
+
"jsnext:main": "./dist/mobx-keystone-loro.esm.js",
|
|
40
|
+
"react-native": "./dist/mobx-keystone-loro.umd.js",
|
|
41
|
+
"umd:main": "./dist/mobx-keystone-loro.umd.js",
|
|
42
|
+
"unpkg": "./dist/mobx-keystone-loro.umd.js",
|
|
43
|
+
"jsdelivr": "./dist/mobx-keystone-loro.umd.js",
|
|
44
|
+
"main": "./dist/mobx-keystone-loro.umd.js",
|
|
45
|
+
"types": "./dist/types/index.d.ts",
|
|
46
|
+
"typings": "./dist/types/index.d.ts",
|
|
47
|
+
"sideEffects": false,
|
|
48
|
+
"files": [
|
|
49
|
+
"src",
|
|
50
|
+
"dist",
|
|
51
|
+
"CHANGELOG.md",
|
|
52
|
+
"LICENSE",
|
|
53
|
+
"README.md"
|
|
54
|
+
],
|
|
55
|
+
"scripts": {
|
|
56
|
+
"quick-build": "tsc",
|
|
57
|
+
"quick-build-tests": "tsc -p test",
|
|
58
|
+
"copy-root-files": "shx cp ../../LICENSE .",
|
|
59
|
+
"build": "yarn quick-build && yarn copy-root-files && shx rm -rf dist && vite build && shx cp dist/mobx-keystone-loro.esm.mjs dist/mobx-keystone-loro.esm.js",
|
|
60
|
+
"test": "jest",
|
|
61
|
+
"test:ci": "jest --ci"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"loro-crdt": "^1.10.3",
|
|
65
|
+
"mobx": "^6.0.0 || ^5.0.0 || ^4.0.0",
|
|
66
|
+
"mobx-keystone": "^1.12.0"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@babel/core": "^7.28.5",
|
|
70
|
+
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
|
71
|
+
"@babel/plugin-proposal-decorators": "^7.28.0",
|
|
72
|
+
"@babel/preset-env": "^7.28.5",
|
|
73
|
+
"@babel/preset-typescript": "^7.28.5",
|
|
74
|
+
"@types/jest": "^30.0.0",
|
|
75
|
+
"@types/node": "^25.0.3",
|
|
76
|
+
"babel-jest": "^30.2.0",
|
|
77
|
+
"jest": "^30.2.0",
|
|
78
|
+
"mobx-keystone": "workspace:packages/lib",
|
|
79
|
+
"shx": "^0.4.0",
|
|
80
|
+
"spec.ts": "^1.1.3",
|
|
81
|
+
"ts-jest": "^29.4.6",
|
|
82
|
+
"ts-node": "^10.9.2",
|
|
83
|
+
"typescript": "^5.9.3"
|
|
84
|
+
},
|
|
85
|
+
"dependencies": {
|
|
86
|
+
"nanoid": "^3.3.11",
|
|
87
|
+
"tslib": "^2.8.1"
|
|
88
|
+
},
|
|
89
|
+
"directories": {
|
|
90
|
+
"test": "test"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { type Delta, LoroText } from "loro-crdt"
|
|
2
|
+
import { computed, createAtom } from "mobx"
|
|
3
|
+
import {
|
|
4
|
+
frozen,
|
|
5
|
+
getParentToChildPath,
|
|
6
|
+
getSnapshotModelType,
|
|
7
|
+
Model,
|
|
8
|
+
model,
|
|
9
|
+
modelAction,
|
|
10
|
+
type SnapshotOutOf,
|
|
11
|
+
tProp,
|
|
12
|
+
types,
|
|
13
|
+
} from "mobx-keystone"
|
|
14
|
+
import { getOrCreateLoroCollectionAtom } from "../utils/getOrCreateLoroCollectionAtom"
|
|
15
|
+
import { loroBindingContext } from "./loroBindingContext"
|
|
16
|
+
import { resolveLoroPath } from "./resolveLoroPath"
|
|
17
|
+
|
|
18
|
+
export const loroTextModelId = "mobx-keystone-loro/LoroTextModel"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Type for the delta stored in the model.
|
|
22
|
+
* Uses Quill delta format: array of operations with insert, delete, retain.
|
|
23
|
+
*/
|
|
24
|
+
export type LoroTextDeltaList = Delta<string>[]
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A mobx-keystone model representing Loro rich text.
|
|
28
|
+
* This model stores the current delta state (Quill format) and syncs with LoroText.
|
|
29
|
+
*/
|
|
30
|
+
@model(loroTextModelId)
|
|
31
|
+
export class LoroTextModel extends Model({
|
|
32
|
+
/**
|
|
33
|
+
* The current delta representing the rich text content.
|
|
34
|
+
* Uses Quill delta format.
|
|
35
|
+
*/
|
|
36
|
+
deltaList: tProp(types.frozen(types.unchecked<LoroTextDeltaList>()), () => frozen([])),
|
|
37
|
+
}) {
|
|
38
|
+
/**
|
|
39
|
+
* Creates a LoroTextModel with initial text content.
|
|
40
|
+
*/
|
|
41
|
+
static withText(text: string): LoroTextModel {
|
|
42
|
+
return new LoroTextModel({
|
|
43
|
+
deltaList: frozen([{ insert: text }]),
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a LoroTextModel with initial delta.
|
|
49
|
+
*/
|
|
50
|
+
static withDelta(delta: LoroTextDeltaList): LoroTextModel {
|
|
51
|
+
return new LoroTextModel({
|
|
52
|
+
deltaList: frozen(delta),
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Atom that gets changed when the associated Loro text changes.
|
|
58
|
+
*/
|
|
59
|
+
loroTextChangedAtom = createAtom("loroTextChangedAtom")
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The LoroText object represented by this mobx-keystone node, if bound.
|
|
63
|
+
* Returns undefined when the model is not part of a bound object tree.
|
|
64
|
+
*/
|
|
65
|
+
@computed
|
|
66
|
+
get loroText(): LoroText | undefined {
|
|
67
|
+
// Check if we have a binding context first - return undefined if not bound
|
|
68
|
+
const ctx = loroBindingContext.get(this)
|
|
69
|
+
if (ctx?.boundObject == null) {
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const path = getParentToChildPath(ctx.boundObject, this)
|
|
75
|
+
if (!path) {
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// If this model IS the bound object, the loroObject is the LoroText
|
|
80
|
+
if (path.length === 0) {
|
|
81
|
+
const loroObject = ctx.loroObject
|
|
82
|
+
if (loroObject instanceof LoroText) {
|
|
83
|
+
getOrCreateLoroCollectionAtom(loroObject).reportObserved()
|
|
84
|
+
return loroObject
|
|
85
|
+
}
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Otherwise resolve the path
|
|
90
|
+
const loroObject = resolveLoroPath(ctx.loroObject, path)
|
|
91
|
+
|
|
92
|
+
if (loroObject instanceof LoroText) {
|
|
93
|
+
getOrCreateLoroCollectionAtom(loroObject).reportObserved()
|
|
94
|
+
return loroObject
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Path resolution failed - return undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Gets the plain text content.
|
|
105
|
+
* This always uses the stored delta, which is kept in sync with Loro.
|
|
106
|
+
*/
|
|
107
|
+
@computed
|
|
108
|
+
get text(): string {
|
|
109
|
+
this.loroTextChangedAtom.reportObserved()
|
|
110
|
+
|
|
111
|
+
// Always compute from delta - it's the source of truth for this model
|
|
112
|
+
// The delta is kept in sync with Loro via patches
|
|
113
|
+
return this.deltaToText(this.deltaList.data)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Gets the current delta (Quill format).
|
|
118
|
+
*/
|
|
119
|
+
@computed
|
|
120
|
+
get currentDelta(): LoroTextDeltaList {
|
|
121
|
+
this.loroTextChangedAtom.reportObserved()
|
|
122
|
+
|
|
123
|
+
// Try to get from bound LoroText first
|
|
124
|
+
const loroText = this.loroText
|
|
125
|
+
if (loroText) {
|
|
126
|
+
try {
|
|
127
|
+
return loroText.toDelta()
|
|
128
|
+
} catch {
|
|
129
|
+
// fall back to stored delta
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return this.deltaList.data
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Converts delta to plain text.
|
|
138
|
+
*/
|
|
139
|
+
private deltaToText(delta: LoroTextDeltaList): string {
|
|
140
|
+
let result = ""
|
|
141
|
+
for (const op of delta) {
|
|
142
|
+
if ("insert" in op && typeof op.insert === "string") {
|
|
143
|
+
result += op.insert
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Sets the delta.
|
|
151
|
+
*/
|
|
152
|
+
@modelAction
|
|
153
|
+
setDelta(delta: LoroTextDeltaList): void {
|
|
154
|
+
this.deltaList = frozen(delta)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Inserts text at the specified position.
|
|
159
|
+
*/
|
|
160
|
+
@modelAction
|
|
161
|
+
insertText(index: number, text: string): void {
|
|
162
|
+
const loroText = this.loroText
|
|
163
|
+
if (loroText) {
|
|
164
|
+
loroText.insert(index, text)
|
|
165
|
+
// The binding will handle syncing back
|
|
166
|
+
} else {
|
|
167
|
+
// Fallback: modify delta directly
|
|
168
|
+
const currentText = this.text
|
|
169
|
+
const newText = currentText.slice(0, index) + text + currentText.slice(index)
|
|
170
|
+
this.deltaList = frozen([{ insert: newText }])
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Deletes text at the specified range.
|
|
176
|
+
*/
|
|
177
|
+
@modelAction
|
|
178
|
+
deleteText(index: number, length: number): void {
|
|
179
|
+
const loroText = this.loroText
|
|
180
|
+
if (loroText) {
|
|
181
|
+
loroText.delete(index, length)
|
|
182
|
+
// The binding will handle syncing back
|
|
183
|
+
} else {
|
|
184
|
+
// Fallback: modify delta directly
|
|
185
|
+
const currentText = this.text
|
|
186
|
+
const newText = currentText.slice(0, index) + currentText.slice(index + length)
|
|
187
|
+
this.deltaList = frozen([{ insert: newText }])
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Internal action to update delta from Loro sync.
|
|
193
|
+
* @internal
|
|
194
|
+
*/
|
|
195
|
+
@modelAction
|
|
196
|
+
_updateDeltaFromLoro(delta: LoroTextDeltaList): void {
|
|
197
|
+
this.deltaList = frozen(delta)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Type for LoroTextModel for use with tProp.
|
|
203
|
+
*/
|
|
204
|
+
export const loroTextModelType = types.model(LoroTextModel)
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Checks if a snapshot is a LoroTextModel snapshot.
|
|
208
|
+
*/
|
|
209
|
+
export function isLoroTextModelSnapshot(value: unknown): value is SnapshotOutOf<LoroTextModel> {
|
|
210
|
+
return getSnapshotModelType(value) === loroTextModelId
|
|
211
|
+
}
|