loro-crdt 1.4.6 → 1.5.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 CHANGED
@@ -1,5 +1,159 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8dfcad4: # New Hooks: `pre-commit` and `first-commit-from-peer`
8
+
9
+ ## `doc.subscribePreCommit(listener)`
10
+
11
+ The `pre-commit` hook enables users to modify commit options before any commit is processed.
12
+
13
+ This hook is particularly useful because `doc.commit()` is often invoked implicitly in various methods such as `doc.import`, `doc.export`, `doc.checkout`, and `doc.exportJsonUpdates`. Without this hook, users attempting to add custom messages to each commit might miss these implicit commit triggers.
14
+
15
+ ```ts
16
+ const doc = new LoroDoc();
17
+ doc.setPeerId(0);
18
+ doc.subscribePreCommit((e) => {
19
+ e.modifier.setMessage("test").setTimestamp(Date.now());
20
+ });
21
+ doc.getList("list").insert(0, 100);
22
+ doc.commit();
23
+ expect(doc.getChangeAt({ peer: "0", counter: 0 }).message).toBe("test");
24
+ ```
25
+
26
+ ### Advanced Example: Creating a Merkle DAG
27
+
28
+ By combining `doc.subscribePreCommit` with `doc.exportJsonInIdSpan`, you can implement advanced features like representing Loro's editing history as a Merkle DAG:
29
+
30
+ ```ts
31
+ const doc = new LoroDoc();
32
+ doc.setPeerId(0);
33
+ doc.subscribePreCommit((e) => {
34
+ const changes = doc.exportJsonInIdSpan(e.changeMeta);
35
+ expect(changes).toHaveLength(1);
36
+ const hash = crypto.createHash("sha256");
37
+ const change = {
38
+ ...changes[0],
39
+ deps: changes[0].deps.map((d) => {
40
+ const depChange = doc.getChangeAt(idStrToId(d));
41
+ return depChange.message;
42
+ }),
43
+ };
44
+ hash.update(JSON.stringify(change));
45
+ const sha256Hash = hash.digest("hex");
46
+ e.modifier.setMessage(sha256Hash);
47
+ });
48
+
49
+ console.log(change); // The output is shown below
50
+ doc.getList("list").insert(0, 100);
51
+ doc.commit();
52
+ // Change 0
53
+ // {
54
+ // id: '0@0',
55
+ // timestamp: 0,
56
+ // deps: [],
57
+ // lamport: 0,
58
+ // msg: undefined,
59
+ // ops: [
60
+ // {
61
+ // container: 'cid:root-list:List',
62
+ // content: { type: 'insert', pos: 0, value: [100] },
63
+ // counter: 0
64
+ // }
65
+ // ]
66
+ // }
67
+
68
+ doc.getList("list").insert(0, 200);
69
+ doc.commit();
70
+ // Change 1
71
+ // {
72
+ // id: '1@0',
73
+ // timestamp: 0,
74
+ // deps: [
75
+ // '2af99cf93869173984bcf6b1ce5412610b0413d027a5511a8f720a02a4432853'
76
+ // ],
77
+ // lamport: 1,
78
+ // msg: undefined,
79
+ // ops: [
80
+ // {
81
+ // container: 'cid:root-list:List',
82
+ // content: { type: 'insert', pos: 0, value: [200] },
83
+ // counter: 1
84
+ // }
85
+ // ]
86
+ // }
87
+
88
+ expect(doc.getChangeAt({ peer: "0", counter: 0 }).message).toBe(
89
+ "2af99cf93869173984bcf6b1ce5412610b0413d027a5511a8f720a02a4432853",
90
+ );
91
+ expect(doc.getChangeAt({ peer: "0", counter: 1 }).message).toBe(
92
+ "aedbb442c554ecf59090e0e8339df1d8febf647f25cc37c67be0c6e27071d37f",
93
+ );
94
+ ```
95
+
96
+ ## `doc.subscribeFirstCommitFromPeer(listener)`
97
+
98
+ The `first-commit-from-peer` event triggers when a peer performs operations on the document for the first time.
99
+ This provides an ideal point to associate peer information (such as author identity) with the document.
100
+
101
+ ```ts
102
+ const doc = new LoroDoc();
103
+ doc.setPeerId(0);
104
+ doc.subscribeFirstCommitFromPeer((e) => {
105
+ doc.getMap("users").set(e.peer, "user-" + e.peer);
106
+ });
107
+ doc.getList("list").insert(0, 100);
108
+ doc.commit();
109
+ expect(doc.getMap("users").get("0")).toBe("user-0");
110
+ ```
111
+
112
+ - a997885: # `EphemeralStore`: An Alternative to Awareness
113
+
114
+ Awareness is commonly used as a state-based CRDT for handling ephemeral states in real-time collaboration scenarios, such as cursor positions and application component highlights. As application complexity grows, Awareness may be set in multiple places, from cursor positions to user presence. However, the current version of Awareness doesn't support partial state updates, which means even minor mouse movements require synchronizing the entire Awareness state.
115
+
116
+ ```ts
117
+ awareness.setLocalState({
118
+ ...awareness.getLocalState(),
119
+ x: 167,
120
+ });
121
+ ```
122
+
123
+ Since Awareness is primarily used in real-time collaboration scenarios where consistency requirements are relatively low, we can make it more flexible. We've introduced `EphemeralStore` as an alternative to `Awareness`. Think of it as a simple key-value store that uses timestamp-based last-write-wins for conflict resolution. You can choose the appropriate granularity for your key-value pairs based on your application's needs, and only modified key-value pairs are synchronized.
124
+
125
+ ## Examples
126
+
127
+ ```ts
128
+ import {
129
+ EphemeralStore,
130
+ EphemeralListener,
131
+ EphemeralStoreEvent,
132
+ } from "loro-crdt";
133
+
134
+ const store = new EphemeralStore();
135
+ // Set ephemeral data
136
+ store.set("loro-prosemirror", {
137
+ anchor: ...,
138
+ focus: ...,
139
+ user: "Alice"
140
+ });
141
+ store.set("online-users", ["Alice", "Bob"]);
142
+
143
+ expect(storeB.get("online-users")).toEqual(["Alice", "Bob"]);
144
+ // Encode only the data for `loro-prosemirror`
145
+ const encoded = store.encode("loro-prosemirror")
146
+
147
+ store.subscribe((e: EphemeralStoreEvent) => {
148
+ // Listen to changes from `local`, `remote`, or `timeout` events
149
+ });
150
+ ```
151
+
152
+ ### Patch Changes
153
+
154
+ - 742842f: fix: apply multiple styles via text delta at the end "\n" char #692
155
+ - 4cb7ae3: feat: get ops from current txn as json #676
156
+
3
157
  ## 1.4.6
4
158
 
5
159
  ### Patch Changes
package/base64/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export * from "./loro_wasm";
2
2
  export type * from "./loro_wasm";
3
- import { AwarenessWasm, PeerID, Container, ContainerID, ContainerType, LoroCounter, LoroDoc, LoroList, LoroMap, LoroText, LoroTree, OpId, Value, AwarenessListener } from "./loro_wasm";
3
+ import { AwarenessWasm, EphemeralStoreWasm, PeerID, Container, ContainerID, ContainerType, LoroCounter, LoroDoc, LoroList, LoroMap, LoroText, LoroTree, OpId, Value, AwarenessListener, EphemeralListener, EphemeralLocalListener } from "./loro_wasm";
4
4
  /**
5
5
  * @deprecated Please use LoroDoc
6
6
  */
@@ -46,10 +46,12 @@ export declare function getType<T>(value: T): T extends LoroText ? "Text" : T ex
46
46
  export declare function newContainerID(id: OpId, type: ContainerType): ContainerID;
47
47
  export declare function newRootContainerID(name: string, type: ContainerType): ContainerID;
48
48
  /**
49
+ * @deprecated Please use `EphemeralStore` instead.
50
+ *
49
51
  * Awareness is a structure that allows to track the ephemeral state of the peers.
50
52
  *
51
53
  * If we don't receive a state update from a peer within the timeout, we will remove their state.
52
- * The timeout is in milliseconds. This can be used to handle the off-line state of a peer.
54
+ * The timeout is in milliseconds. This can be used to handle the offline state of a peer.
53
55
  */
54
56
  export declare class Awareness<T extends Value = Value> {
55
57
  inner: AwarenessWasm<T>;
@@ -70,3 +72,51 @@ export declare class Awareness<T extends Value = Value> {
70
72
  destroy(): void;
71
73
  private startTimerIfNotEmpty;
72
74
  }
75
+ /**
76
+ * EphemeralStore is a structure that allows to track the ephemeral state of the peers.
77
+ *
78
+ * It can be used to synchronize cursor positions, selections, and the names of the peers.
79
+ * Each entry uses timestamp-based LWW (Last-Write-Wins) for conflict resolution.
80
+ *
81
+ * If we don't receive a state update from a peer within the timeout, we will remove their state.
82
+ * The timeout is in milliseconds. This can be used to handle the offline state of a peer.
83
+ *
84
+ * @example
85
+ *
86
+ * ```ts
87
+ * const store = new EphemeralStore();
88
+ * const store2 = new EphemeralStore();
89
+ * // Subscribe to local updates
90
+ * store.subscribeLocalUpdates((data)=>{
91
+ * store2.apply(data);
92
+ * })
93
+ * // Subscribe to all updates
94
+ * store2.subscribe((event)=>{
95
+ * console.log("event: ", event);
96
+ * })
97
+ * // Set a value
98
+ * store.set("key", "value");
99
+ * // Encode the value
100
+ * const encoded = store.encode("key");
101
+ * // Apply the encoded value
102
+ * store2.apply(encoded);
103
+ * ```
104
+ */
105
+ export declare class EphemeralStore<T extends Value = Value> {
106
+ inner: EphemeralStoreWasm<T>;
107
+ private timer;
108
+ private timeout;
109
+ constructor(timeout?: number);
110
+ apply(bytes: Uint8Array): void;
111
+ set(key: string, value: T): void;
112
+ get(key: string): T | undefined;
113
+ getAllStates(): Record<string, T>;
114
+ encode(key: string): Uint8Array;
115
+ encodeAll(): Uint8Array;
116
+ keys(): string[];
117
+ destroy(): void;
118
+ subscribe(listener: EphemeralListener): () => void;
119
+ subscribeLocalUpdates(listener: EphemeralLocalListener): () => void;
120
+ private startTimerIfNotEmpty;
121
+ }
122
+ export declare function idStrToId(idStr: `${number}@${PeerID}`): OpId;