@pluv/crdt-loro 0.16.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.
@@ -0,0 +1,102 @@
1
+ import { AbstractCrdtType, type InferCrdtStorageJson } from "@pluv/crdt";
2
+ import { LoroList } from "loro-crdt";
3
+ import type { CrdtLoroDoc } from "../doc/CrdtLoroDoc";
4
+ import { cloneType, getLoroContainerType, isWrapper } from "../shared";
5
+ import type { InferLoroJson } from "../types";
6
+
7
+ export class CrdtLoroArray<T extends unknown> extends AbstractCrdtType<
8
+ LoroList<T[]>,
9
+ InferLoroJson<T>[]
10
+ > {
11
+ public readonly initialValue: T[] | readonly T[];
12
+
13
+ private _doc: CrdtLoroDoc<any> | null = null;
14
+ private _initialized: boolean = false;
15
+ private _value: LoroList<T[]> = new LoroList();
16
+
17
+ constructor(value: T[] | readonly T[] = []) {
18
+ super();
19
+
20
+ this.initialValue = value.slice();
21
+ }
22
+
23
+ public set doc(doc: CrdtLoroDoc<any>) {
24
+ if (this._doc) throw new Error("Cannot overwrite array doc");
25
+
26
+ this._doc = doc;
27
+ }
28
+
29
+ public get length(): number {
30
+ return this.value.length;
31
+ }
32
+
33
+ public get value(): LoroList<T[]> {
34
+ return this._value;
35
+ }
36
+
37
+ public set value(value: LoroList<T[]>) {
38
+ if (this._initialized) throw new Error("Cannot re-assign array");
39
+
40
+ this._initialized = true;
41
+ this._value = value;
42
+
43
+ cloneType({ source: this, target: this.value });
44
+ }
45
+
46
+ public delete(index: number, length: number = 1): this {
47
+ this._guardInitialized();
48
+
49
+ this.value.delete(index, length);
50
+ this._doc?.value.commit();
51
+
52
+ return this;
53
+ }
54
+
55
+ public insert(index: number, ...items: T[]): this {
56
+ this._guardInitialized();
57
+
58
+ items.forEach((item, i) => {
59
+ if (!(item instanceof AbstractCrdtType)) {
60
+ this.value.insert(index + i, item);
61
+ this._doc?.value.commit();
62
+
63
+ return this;
64
+ }
65
+
66
+ if (!isWrapper(item)) {
67
+ throw new Error("This type is not yet supported");
68
+ }
69
+
70
+ const containerType = getLoroContainerType(item);
71
+ const container = this.value.insertContainer(
72
+ index + i,
73
+ containerType,
74
+ );
75
+
76
+ cloneType({ source: item, target: container as any });
77
+ if (this._doc) item.doc = this._doc;
78
+
79
+ return this;
80
+ });
81
+
82
+ this._doc?.value.commit();
83
+
84
+ return this;
85
+ }
86
+
87
+ public push(...items: T[]): this {
88
+ return this.insert(this.length, ...items);
89
+ }
90
+
91
+ public unshift(...items: T[]): this {
92
+ return this.insert(0, ...items);
93
+ }
94
+
95
+ public toJson(): InferCrdtStorageJson<InferLoroJson<T>>[] {
96
+ return this.value.toJson();
97
+ }
98
+
99
+ private _guardInitialized(): void {
100
+ if (!this._initialized) throw new Error("Array is not yet initialized");
101
+ }
102
+ }
@@ -0,0 +1,7 @@
1
+ import { CrdtLoroArray } from "./CrdtLoroArray";
2
+
3
+ export const array = <T extends unknown>(
4
+ value: T[] | readonly T[] = [],
5
+ ): CrdtLoroArray<T> => {
6
+ return new CrdtLoroArray<T>(value);
7
+ };
@@ -0,0 +1,2 @@
1
+ export { CrdtLoroArray } from "./CrdtLoroArray";
2
+ export { array } from "./array";
@@ -0,0 +1,239 @@
1
+ import type {
2
+ AbstractCrdtType,
3
+ DocApplyEncodedStateParams,
4
+ DocSubscribeCallbackParams,
5
+ InferCrdtStorageJson,
6
+ } from "@pluv/crdt";
7
+ import { AbstractCrdtDoc } from "@pluv/crdt";
8
+ import { fromUint8Array, toUint8Array } from "js-base64";
9
+ import type { Container, LoroEventBatch } from "loro-crdt";
10
+ import { Loro } from "loro-crdt";
11
+ import { CrdtLoroArray } from "../array/CrdtLoroArray";
12
+ import { CrdtLoroMap } from "../map/CrdtLoroMap";
13
+ import { CrdtLoroObject } from "../object/CrdtLoroObject";
14
+ import { CrdtLoroText } from "../text/CrdtLoroText";
15
+
16
+ export class CrdtLoroDoc<
17
+ TStorage extends Record<string, AbstractCrdtType<any, any>>,
18
+ > extends AbstractCrdtDoc<TStorage> {
19
+ public value: Loro = new Loro();
20
+
21
+ private _storage: TStorage;
22
+
23
+ constructor(value: TStorage = {} as TStorage) {
24
+ super();
25
+
26
+ this._storage = Object.entries(value).reduce((acc, [key, node]) => {
27
+ if (node instanceof CrdtLoroArray) {
28
+ const loroList = this.value.getList(key);
29
+
30
+ node.value = loroList;
31
+ node.doc = this;
32
+
33
+ return { ...acc, [key]: node };
34
+ }
35
+
36
+ if (node instanceof CrdtLoroMap || node instanceof CrdtLoroObject) {
37
+ const loroMap = this.value.getMap(key);
38
+
39
+ node.value = loroMap;
40
+ node.doc = this;
41
+
42
+ return { ...acc, [key]: node };
43
+ }
44
+
45
+ if (node instanceof CrdtLoroText) {
46
+ const loroText = this.value.getText(key);
47
+
48
+ node.value = loroText;
49
+ node.doc = this;
50
+
51
+ return { ...acc, [key]: loroText };
52
+ }
53
+
54
+ return acc;
55
+ }, {} as TStorage);
56
+ }
57
+
58
+ public applyEncodedState(params: DocApplyEncodedStateParams): this {
59
+ const update =
60
+ typeof params.update === "string"
61
+ ? toUint8Array(params.update)
62
+ : params.update;
63
+
64
+ if (!update) return this;
65
+
66
+ this.value.import(update);
67
+
68
+ return this;
69
+ }
70
+
71
+ public batchApplyEncodedState(
72
+ updates: readonly (
73
+ | DocApplyEncodedStateParams
74
+ | string
75
+ | null
76
+ | undefined
77
+ )[],
78
+ ): this {
79
+ const _updates = updates.reduce<Uint8Array[]>((acc, item) => {
80
+ if (!item) return acc;
81
+
82
+ if (typeof item === "string") {
83
+ acc.push(toUint8Array(item));
84
+
85
+ return acc;
86
+ }
87
+
88
+ if (typeof item === "object") {
89
+ const update =
90
+ typeof item.update === "string"
91
+ ? toUint8Array(item.update)
92
+ : item.update;
93
+
94
+ if (!update) return acc;
95
+
96
+ acc.push(update);
97
+
98
+ return acc;
99
+ }
100
+
101
+ return acc;
102
+ }, []);
103
+
104
+ if (!_updates.length) return this;
105
+
106
+ if (_updates.length === 1) {
107
+ const update = _updates[0] ?? null;
108
+
109
+ update && this.value.import(update);
110
+
111
+ return this;
112
+ }
113
+
114
+ this.value.importUpdateBatch(_updates);
115
+
116
+ return this;
117
+ }
118
+
119
+ /**
120
+ * TODO
121
+ * @description This method is not yet supported for loro
122
+ */
123
+ public canRedo(): boolean {
124
+ return false;
125
+ }
126
+
127
+ /**
128
+ * TODO
129
+ * @description This method is not yet supported for loro
130
+ */
131
+ public canUndo(): boolean {
132
+ return false;
133
+ }
134
+
135
+ public destroy(): void {
136
+ return;
137
+ }
138
+
139
+ public get(key?: undefined): TStorage;
140
+ public get<TKey extends keyof TStorage>(key: TKey): TStorage[TKey];
141
+ public get<TKey extends keyof TStorage>(
142
+ key?: TKey,
143
+ ): TStorage | TStorage[TKey] {
144
+ if (typeof key === "undefined") return this._storage;
145
+
146
+ return this._storage[key as TKey];
147
+ }
148
+
149
+ public getEncodedState(): string {
150
+ return fromUint8Array(this.value.exportSnapshot());
151
+ }
152
+
153
+ public toJson(): InferCrdtStorageJson<TStorage> {
154
+ return Object.entries(this._storage).reduce(
155
+ (acc, [key, value]) => ({ ...acc, [key]: value.toJson() }),
156
+ {} as InferCrdtStorageJson<TStorage>,
157
+ );
158
+ }
159
+
160
+ public isEmpty(): boolean {
161
+ const serialized = this.value.toJson();
162
+
163
+ return !serialized || !Object.keys(serialized).length;
164
+ }
165
+
166
+ /**
167
+ * TODO
168
+ * @description This method is not yet supported for loro
169
+ */
170
+ public redo(): this {
171
+ throw new Error("This is not yet supported");
172
+ }
173
+
174
+ public subscribe(
175
+ listener: (params: DocSubscribeCallbackParams<TStorage>) => void,
176
+ ): () => void {
177
+ const fn = (event: LoroEventBatch) => {
178
+ const update = fromUint8Array(this.value.exportFrom());
179
+
180
+ listener({
181
+ doc: this,
182
+ local: event.local,
183
+ origin: event.origin ? String(event.origin) : null,
184
+ update,
185
+ });
186
+ };
187
+
188
+ const subscriptionIds = Object.entries(this._storage).reduce(
189
+ (map, [key, crdtType]) => {
190
+ const container = crdtType.value as Container;
191
+ const subscriptionId = container.subscribe(this.value, fn);
192
+
193
+ return map.set(key, subscriptionId);
194
+ },
195
+ new Map<string, number>(),
196
+ );
197
+
198
+ return () => {
199
+ Array.from(subscriptionIds.entries()).forEach(
200
+ ([key, subscriptionId]) => {
201
+ const container = (this._storage[key]?.value ??
202
+ null) as Container | null;
203
+
204
+ if (!container) {
205
+ throw new Error("Storage could not be found");
206
+ }
207
+
208
+ container.unsubscribe(this.value, subscriptionId);
209
+ },
210
+ );
211
+ };
212
+ }
213
+
214
+ /**
215
+ * TODO
216
+ * @description This method doesn't do anything yet.
217
+ */
218
+ public track(): this {
219
+ return this;
220
+ }
221
+
222
+ /**
223
+ * TODO
224
+ * @description This method doesn't do anything yet. The callback will still be executed.
225
+ */
226
+ public transact(fn: () => void): this {
227
+ fn();
228
+
229
+ return this;
230
+ }
231
+
232
+ /**
233
+ * TODO
234
+ * @description This method is not yet supported for loro
235
+ */
236
+ public undo(): this {
237
+ throw new Error("This is not yet supported");
238
+ }
239
+ }
@@ -0,0 +1,65 @@
1
+ import type { AbstractCrdtType } from "@pluv/crdt";
2
+ import { AbstractCrdtDocFactory } from "@pluv/crdt";
3
+ import { CrdtLoroArray } from "../array";
4
+ import { CrdtLoroMap } from "../map";
5
+ import { CrdtLoroObject } from "../object";
6
+ import { CrdtLoroText } from "../text";
7
+ import { CrdtLoroDoc } from "./CrdtLoroDoc";
8
+
9
+ export class CrdtLoroDocFactory<
10
+ TStorage extends Record<string, AbstractCrdtType<any, any>>,
11
+ > extends AbstractCrdtDocFactory<TStorage> {
12
+ private _initialStorage: () => TStorage;
13
+
14
+ constructor(initialStorage: () => TStorage = () => ({}) as TStorage) {
15
+ super();
16
+
17
+ this._initialStorage = initialStorage;
18
+ }
19
+
20
+ public getEmpty(): CrdtLoroDoc<TStorage> {
21
+ return new CrdtLoroDoc<TStorage>();
22
+ }
23
+
24
+ public getFactory(
25
+ initialStorage?: (() => TStorage) | undefined,
26
+ ): CrdtLoroDocFactory<TStorage> {
27
+ return new CrdtLoroDocFactory<TStorage>(
28
+ initialStorage ?? this._initialStorage,
29
+ );
30
+ }
31
+
32
+ public getFresh(): CrdtLoroDoc<TStorage> {
33
+ const storage = this._initialStorage();
34
+
35
+ return new CrdtLoroDoc<TStorage>(
36
+ Object.entries(storage).reduce((acc, [key, node]) => {
37
+ if (node instanceof CrdtLoroArray) {
38
+ return { ...acc, [key]: new CrdtLoroArray([]) };
39
+ }
40
+
41
+ if (node instanceof CrdtLoroMap) {
42
+ return { ...acc, [key]: new CrdtLoroMap([]) };
43
+ }
44
+
45
+ if (node instanceof CrdtLoroObject) {
46
+ return { ...acc, [key]: new CrdtLoroObject({}) };
47
+ }
48
+
49
+ if (node instanceof CrdtLoroText) {
50
+ return { ...acc, [key]: new CrdtLoroText("") };
51
+ }
52
+
53
+ return acc;
54
+ }, {} as TStorage),
55
+ );
56
+ }
57
+
58
+ public getInitialized(
59
+ initialStorage?: () => TStorage,
60
+ ): CrdtLoroDoc<TStorage> {
61
+ return new CrdtLoroDoc<TStorage>(
62
+ initialStorage?.() ?? this._initialStorage(),
63
+ );
64
+ }
65
+ }
package/src/doc/doc.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { AbstractCrdtType } from "@pluv/crdt";
2
+ import { CrdtLoroDocFactory } from "./CrdtLoroDocFactory";
3
+
4
+ export const doc = <
5
+ TStorage extends Record<string, AbstractCrdtType<any, any>>,
6
+ >(
7
+ value: () => TStorage = () => ({}) as TStorage,
8
+ ): CrdtLoroDocFactory<TStorage> => {
9
+ return new CrdtLoroDocFactory<TStorage>(value);
10
+ };
@@ -0,0 +1,3 @@
1
+ export { CrdtLoroDoc } from "./CrdtLoroDoc";
2
+ export { CrdtLoroDocFactory } from "./CrdtLoroDocFactory";
3
+ export { doc } from "./doc";
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * as loro from "./loro";
package/src/loro.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { CrdtLoroArray, array } from "./array";
2
+ export { CrdtLoroDoc, doc } from "./doc";
3
+ export { CrdtLoroMap, map } from "./map";
4
+ export { CrdtLoroObject, object } from "./object";
5
+ export { CrdtLoroText, text } from "./text";
6
+ export type { InferLoroJson, InferLoroType } from "./types";
@@ -0,0 +1,90 @@
1
+ import type { InferCrdtStorageJson } from "@pluv/crdt";
2
+ import { AbstractCrdtType } from "@pluv/crdt";
3
+ import { LoroMap } from "loro-crdt";
4
+ import type { CrdtLoroDoc } from "../doc/CrdtLoroDoc";
5
+ import { cloneType, getLoroContainerType, isWrapper } from "../shared";
6
+ import type { InferLoroJson } from "../types";
7
+
8
+ export class CrdtLoroMap<T extends unknown> extends AbstractCrdtType<
9
+ LoroMap<Record<string, T>>,
10
+ Record<string, InferLoroJson<T>>
11
+ > {
12
+ public readonly initialValue: readonly (readonly [key: string, value: T])[];
13
+
14
+ private _doc: CrdtLoroDoc<any> | null = null;
15
+ private _initialized: boolean = false;
16
+ private _value: LoroMap<Record<string, T>> = new LoroMap();
17
+
18
+ constructor(value: readonly (readonly [key: string, value: T])[] = []) {
19
+ super();
20
+
21
+ this.initialValue = value.map(
22
+ ([k, v]) => [k, v] as [key: string, value: T],
23
+ );
24
+ }
25
+
26
+ public set doc(doc: CrdtLoroDoc<any>) {
27
+ if (this._doc) throw new Error("Cannot overwrite array doc");
28
+
29
+ this._doc = doc;
30
+ }
31
+
32
+ public get size(): number {
33
+ return this.value.size;
34
+ }
35
+
36
+ public get value(): LoroMap<Record<string, T>> {
37
+ return this._value;
38
+ }
39
+
40
+ public set value(value: LoroMap<Record<string, T>>) {
41
+ if (this._initialized) throw new Error("Cannot re-assign map");
42
+
43
+ this._initialized = true;
44
+ this._value = value;
45
+
46
+ cloneType({ source: this, target: this.value });
47
+ }
48
+
49
+ public delete(prop: string): this {
50
+ this._guardInitialized();
51
+
52
+ this.value.delete(prop);
53
+ this._doc?.value.commit();
54
+
55
+ return this;
56
+ }
57
+
58
+ public set(prop: string, value: T): this {
59
+ this._guardInitialized();
60
+
61
+ if (!(value instanceof AbstractCrdtType)) {
62
+ this.value.set(prop, value);
63
+ this._doc?.value.commit();
64
+
65
+ return this;
66
+ }
67
+
68
+ if (!isWrapper(value)) {
69
+ throw new Error("This type is not yet supported");
70
+ }
71
+
72
+ const containerType = getLoroContainerType(value);
73
+ const container = this.value.setContainer(prop, containerType);
74
+
75
+ cloneType({ source: value, target: container as any });
76
+ if (this._doc) value.doc = this._doc;
77
+
78
+ this._doc?.value.commit();
79
+
80
+ return this;
81
+ }
82
+
83
+ public toJson(): InferCrdtStorageJson<Record<string, InferLoroJson<T>>> {
84
+ return this.value.toJson();
85
+ }
86
+
87
+ private _guardInitialized(): void {
88
+ if (!this._initialized) throw new Error("Array is not yet initialized");
89
+ }
90
+ }
@@ -0,0 +1,2 @@
1
+ export { CrdtLoroMap } from "./CrdtLoroMap";
2
+ export { map } from "./map";
package/src/map/map.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { CrdtLoroMap } from "./CrdtLoroMap";
2
+
3
+ export const map = <T extends unknown>(
4
+ value: readonly (readonly [key: string, value: T])[] = [],
5
+ ): CrdtLoroMap<T> => {
6
+ return new CrdtLoroMap<T>(value);
7
+ };
@@ -0,0 +1,79 @@
1
+ import { AbstractCrdtType, type InferCrdtStorageJson } from "@pluv/crdt";
2
+ import { LoroMap } from "loro-crdt";
3
+ import type { CrdtLoroDoc } from "../doc/CrdtLoroDoc";
4
+ import { cloneType, getLoroContainerType, isWrapper } from "../shared";
5
+ import type { InferLoroJson } from "../types";
6
+
7
+ export class CrdtLoroObject<
8
+ T extends Record<string, any>,
9
+ > extends AbstractCrdtType<LoroMap<T>, InferLoroJson<T>> {
10
+ public readonly initialValue: readonly (readonly [key: string, value: T])[];
11
+
12
+ private _doc: CrdtLoroDoc<any> | null = null;
13
+ private _initialized: boolean = false;
14
+ private _value: LoroMap<T> = new LoroMap();
15
+
16
+ constructor(value: T) {
17
+ super();
18
+
19
+ this.initialValue = Object.entries(value).map(
20
+ ([k, v]) => [k, v] as [key: string, value: T],
21
+ );
22
+ }
23
+
24
+ public set doc(doc: CrdtLoroDoc<any>) {
25
+ if (this._doc) throw new Error("Cannot overwrite array doc");
26
+
27
+ this._doc = doc;
28
+ }
29
+
30
+ public get size(): number {
31
+ return this.value.size;
32
+ }
33
+
34
+ public get value(): LoroMap<T> {
35
+ return this._value;
36
+ }
37
+
38
+ public set value(value: LoroMap<T>) {
39
+ if (this._initialized) throw new Error("Cannot re-assign map");
40
+
41
+ this._initialized = true;
42
+ this._value = value;
43
+
44
+ cloneType({ source: this, target: this.value });
45
+ }
46
+
47
+ public set(prop: string, value: T): this {
48
+ this._guardInitialized();
49
+
50
+ if (!(value instanceof AbstractCrdtType)) {
51
+ this.value.set(prop, value);
52
+ this._doc?.value.commit();
53
+
54
+ return this;
55
+ }
56
+
57
+ if (!isWrapper(value)) {
58
+ throw new Error("This type is not yet supported");
59
+ }
60
+
61
+ const containerType = getLoroContainerType(value);
62
+ const container = this.value.setContainer(prop, containerType);
63
+
64
+ cloneType({ source: value, target: container as any });
65
+ if (this._doc) value.doc = this._doc;
66
+
67
+ this._doc?.value.commit();
68
+
69
+ return this;
70
+ }
71
+
72
+ public toJson(): InferCrdtStorageJson<InferLoroJson<T>> {
73
+ return this.value.toJson() as InferCrdtStorageJson<InferLoroJson<T>>;
74
+ }
75
+
76
+ private _guardInitialized(): void {
77
+ if (!this._initialized) throw new Error("Array is not yet initialized");
78
+ }
79
+ }
@@ -0,0 +1,2 @@
1
+ export { CrdtLoroObject } from "./CrdtLoroObject";
2
+ export { object } from "./object";
@@ -0,0 +1,7 @@
1
+ import { CrdtLoroObject } from "./CrdtLoroObject";
2
+
3
+ export const object = <T extends Record<string, any>>(
4
+ value: T,
5
+ ): CrdtLoroObject<T> => {
6
+ return new CrdtLoroObject<T>(value);
7
+ };