@pluv/crdt-loro 0.0.0-canary-20261027014949

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,592 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.mjs";
2
+ import { LoroCounter, LoroDoc, LoroList, LoroMap, LoroMovableList, LoroText, LoroTree, UndoManager, VersionVector, decodeImportBlobMeta, isContainer } from "loro-crdt";
3
+ import { fromUint8Array, toUint8Array } from "js-base64";
4
+ import { AbstractCrdtDocFactory, CrdtSchemaError, assertJsonValue, assertNotOptional, createSchemaNode, flattenObjectShape, fromJSON, isJsonKind, isLoroCrdtKind, isSchemaNode, isUnseedableKind, isYjsCrdtKind, pickUnionOption, unwrapOptional } from "@pluv/crdt";
5
+ // src/counter.ts
6
+ const counter = () => {
7
+ return new LoroCounter();
8
+ };
9
+ // src/schema/hydrate.ts
10
+ const isPlainObject = (value) => {
11
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12
+ };
13
+ const insertIntoList = (list, index, value) => {
14
+ if (isContainer(value)) list.insertContainer(index, value);
15
+ else list.insert(index, value);
16
+ };
17
+ const setOnMap = (map, key, value) => {
18
+ if (isContainer(value)) map.setContainer(key, value);
19
+ else map.set(key, value);
20
+ };
21
+ const getLoroShare = (doc, key, kind) => {
22
+ switch (kind) {
23
+ case "loroList": return doc.getList(key);
24
+ case "loroMap": return doc.getMap(key);
25
+ case "loroText": return doc.getText(key);
26
+ case "loroMovableList": return doc.getMovableList(key);
27
+ case "loroCounter": return doc.getCounter(key);
28
+ case "loroTree": return doc.getTree(key);
29
+ default: throw new CrdtSchemaError(`Cannot bind Loro share for kind "${kind}"`);
30
+ }
31
+ };
32
+ const hydrateValue = (node, json) => {
33
+ const current = unwrapOptional(node);
34
+ if (current.kind === "union" || current.kind === "discriminatedUnion") {
35
+ const option = pickUnionOption(current, json);
36
+ if (!option) throw new CrdtSchemaError(`No ${current.kind} option matched`);
37
+ return hydrateValue(option, json);
38
+ }
39
+ if (current.kind === "intersection") {
40
+ const shape = flattenObjectShape(current);
41
+ return hydrateValue({
42
+ ...current,
43
+ kind: "object",
44
+ shape
45
+ }, json);
46
+ }
47
+ if (current.kind === "object") {
48
+ assertJsonValue(current, json, "JSON object");
49
+ const shape = current.shape;
50
+ const source = json;
51
+ return Object.entries(shape).reduce((acc, [key, field]) => {
52
+ if (!(key in source)) return acc;
53
+ acc[key] = hydrateValue(unwrapOptional(field), source[key]);
54
+ return acc;
55
+ }, {});
56
+ }
57
+ if (current.kind === "array") {
58
+ assertJsonValue(current, json, "JSON array");
59
+ const item = current.item;
60
+ return json.map((entry) => hydrateValue(item, entry));
61
+ }
62
+ if (current.kind === "record") {
63
+ assertJsonValue(current, json, "JSON record");
64
+ const value = current.value;
65
+ return Object.entries(json).reduce((acc, [key, entry]) => {
66
+ acc[key] = hydrateValue(value, entry);
67
+ return acc;
68
+ }, {});
69
+ }
70
+ if (isJsonKind(current.kind)) {
71
+ assertJsonValue(current, json, `JSON ${current.kind}`);
72
+ return json;
73
+ }
74
+ switch (current.kind) {
75
+ case "loroList":
76
+ case "loroMovableList": {
77
+ if (!Array.isArray(json)) throw new CrdtSchemaError(`Expected an array to hydrate ${current.kind}`);
78
+ const item = current.item;
79
+ const list = current.kind === "loroList" ? new LoroList() : new LoroMovableList();
80
+ json.forEach((entry, index) => {
81
+ insertIntoList(list, index, hydrateValue(item, entry));
82
+ });
83
+ return list;
84
+ }
85
+ case "loroMap": {
86
+ if (!isPlainObject(json)) throw new CrdtSchemaError("Expected an object to hydrate loroMap");
87
+ const value = current.value;
88
+ const map = new LoroMap();
89
+ Object.entries(json).forEach(([key, entry]) => {
90
+ setOnMap(map, key, hydrateValue(value, entry));
91
+ });
92
+ return map;
93
+ }
94
+ case "loroText": {
95
+ if (typeof json !== "string") throw new CrdtSchemaError("Expected a string to hydrate loroText");
96
+ const text = new LoroText();
97
+ if (json) text.insert(0, json);
98
+ return text;
99
+ }
100
+ case "loroCounter": {
101
+ if (typeof json !== "number" || !Number.isFinite(json)) throw new CrdtSchemaError("Expected a number to hydrate loroCounter");
102
+ const counter = new LoroCounter();
103
+ if (json !== 0) counter.increment(json);
104
+ return counter;
105
+ }
106
+ default: throw new CrdtSchemaError(`Cannot hydrate schema kind "${current.kind}" from JSON`);
107
+ }
108
+ };
109
+ const fillShare = (share, node, json) => {
110
+ switch (node.kind) {
111
+ case "loroList":
112
+ case "loroMovableList": {
113
+ if (!Array.isArray(json)) throw new CrdtSchemaError(`Expected an array to seed ${node.kind}`);
114
+ const item = node.item;
115
+ const list = share;
116
+ json.forEach((entry, index) => {
117
+ insertIntoList(list, index, hydrateValue(item, entry));
118
+ });
119
+ return;
120
+ }
121
+ case "loroMap": {
122
+ if (!isPlainObject(json)) throw new CrdtSchemaError("Expected an object to seed loroMap");
123
+ const value = node.value;
124
+ const map = share;
125
+ Object.entries(json).forEach(([key, entry]) => {
126
+ setOnMap(map, key, hydrateValue(value, entry));
127
+ });
128
+ return;
129
+ }
130
+ case "loroText": {
131
+ if (typeof json !== "string") throw new CrdtSchemaError("Expected a string to seed loroText");
132
+ const text = share;
133
+ if (json) text.insert(0, json);
134
+ return;
135
+ }
136
+ case "loroCounter": {
137
+ if (typeof json !== "number" || !Number.isFinite(json)) throw new CrdtSchemaError("Expected a number to seed loroCounter");
138
+ const counter = share;
139
+ if (json !== 0) counter.increment(json);
140
+ return;
141
+ }
142
+ default: throw new CrdtSchemaError(`Cannot seed kind "${node.kind}"`);
143
+ }
144
+ };
145
+ const hydrateTopLevel = (doc, schema, seed) => {
146
+ if (seed) {
147
+ const extra = Object.keys(seed).filter((key) => !(key in schema.shape));
148
+ if (extra.length) throw new CrdtSchemaError(`Unexpected seed keys: ${extra.join(", ")}`);
149
+ }
150
+ return Object.entries(schema.shape).reduce((acc, [key, node]) => {
151
+ const share = getLoroShare(doc, key, node.kind);
152
+ const value = seed?.[key];
153
+ if (value !== void 0) {
154
+ if (isUnseedableKind(node.kind)) throw new CrdtSchemaError(`Cannot JSON-seed "${key}" (${node.kind})`);
155
+ fillShare(share, node, value);
156
+ }
157
+ acc[key] = share;
158
+ return acc;
159
+ }, {});
160
+ };
161
+ // src/doc/PendingSpans.ts
162
+ /**
163
+ * Counter ranges from Loro imports that are still waiting on earlier changes.
164
+ */
165
+ var PendingSpans = class {
166
+ _spans = /* @__PURE__ */ new Map();
167
+ get isPending() {
168
+ return this._spans.size > 0;
169
+ }
170
+ record(status) {
171
+ status.pending?.forEach((span, peer) => {
172
+ const key = String(peer);
173
+ const existing = this._spans.get(key) ?? [];
174
+ this._spans.set(key, this._mergeSpans([...existing, span]));
175
+ });
176
+ status.success.forEach((span, peer) => {
177
+ const key = String(peer);
178
+ const existing = this._spans.get(key);
179
+ if (!existing) return;
180
+ const remaining = existing.flatMap((pending) => this._subtractSpan(pending, span));
181
+ if (remaining.length) this._spans.set(key, remaining);
182
+ else this._spans.delete(key);
183
+ });
184
+ }
185
+ _mergeSpans(spans) {
186
+ return spans.filter((span) => span.end > span.start).toSorted((left, right) => left.start - right.start).reduce((merged, span) => {
187
+ const last = merged.at(-1);
188
+ if (!last || span.start > last.end) {
189
+ merged.push({
190
+ start: span.start,
191
+ end: span.end
192
+ });
193
+ return merged;
194
+ }
195
+ last.end = Math.max(last.end, span.end);
196
+ return merged;
197
+ }, []);
198
+ }
199
+ _subtractSpan(span, cover) {
200
+ if (cover.end <= span.start || cover.start >= span.end) return [span];
201
+ const remaining = [];
202
+ if (cover.start > span.start) remaining.push({
203
+ start: span.start,
204
+ end: cover.start
205
+ });
206
+ if (cover.end < span.end) remaining.push({
207
+ start: cover.end,
208
+ end: span.end
209
+ });
210
+ return remaining;
211
+ }
212
+ };
213
+ // src/doc/CrdtLoroDoc.ts
214
+ const MAX_UNDO_STEPS = 100;
215
+ const MERGE_INTERVAL_MS = 1e3;
216
+ const PLUV_ID_FIELD = "__$pluv";
217
+ const clocksFromVector = (clocks) => [...clocks].map(([id, clock]) => [String(id), clock]).sort((left, right) => left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0);
218
+ var CrdtLoroDoc = class {
219
+ value = new LoroDoc();
220
+ #_importOrigin = null;
221
+ #_pendingSpans = new PendingSpans();
222
+ #_schema;
223
+ #_storage;
224
+ #_undoManager = null;
225
+ constructor(schema, seed, hydrate = false) {
226
+ this.#_schema = schema;
227
+ if (hydrate) {
228
+ this.#_storage = hydrateTopLevel(this.value, schema, seed);
229
+ if (Object.keys(schema.shape).length) this.#_setPluvId();
230
+ } else this.#_storage = {};
231
+ this.value.commit();
232
+ }
233
+ applyEncodedState(params) {
234
+ const update = typeof params.update === "string" ? toUint8Array(params.update) : params.update;
235
+ if (!update) return this;
236
+ this.#_import(update, params.origin);
237
+ return this;
238
+ }
239
+ batchApplyEncodedState(params) {
240
+ const filtered = (params.updates ?? []).reduce((acc, update) => {
241
+ if (!update) return acc;
242
+ if (typeof update === "string") {
243
+ acc.push(toUint8Array(update));
244
+ return acc;
245
+ }
246
+ if (typeof update === "object") {
247
+ acc.push(update);
248
+ return acc;
249
+ }
250
+ return acc;
251
+ }, []);
252
+ if (!filtered.length) return this;
253
+ const [first] = filtered;
254
+ if (filtered.length === 1 && first) {
255
+ this.#_import(first, params.origin);
256
+ return this;
257
+ }
258
+ this.#_importOrigin = params.origin ?? null;
259
+ try {
260
+ this.#_pendingSpans.record(this.value.importBatch(filtered));
261
+ } finally {
262
+ this.#_importOrigin = null;
263
+ }
264
+ return this;
265
+ }
266
+ captureUpdate(fn) {
267
+ const from = this.value.oplogVersion();
268
+ try {
269
+ fn();
270
+ this.value.commit();
271
+ const after = this.value.oplogVersion();
272
+ try {
273
+ if (from.compare(after) === 0) return null;
274
+ const update = fromUint8Array(this.value.export({
275
+ mode: "update",
276
+ from
277
+ }));
278
+ return update === "" ? null : update;
279
+ } finally {
280
+ after.free();
281
+ }
282
+ } finally {
283
+ from.free();
284
+ }
285
+ }
286
+ canRedo() {
287
+ if (!this.#_undoManager) return false;
288
+ return this.#_undoManager.canRedo();
289
+ }
290
+ canUndo() {
291
+ if (!this.#_undoManager) return false;
292
+ return this.#_undoManager.canUndo();
293
+ }
294
+ destroy() {}
295
+ get(key) {
296
+ if (typeof key === "undefined") return this.#_storage;
297
+ return this.#_storage[key];
298
+ }
299
+ encodeDiff(stateVector) {
300
+ if (!stateVector) return fromUint8Array(this.value.export({ mode: "update" }));
301
+ const from = VersionVector.decode(toUint8Array(stateVector));
302
+ try {
303
+ return fromUint8Array(this.value.export({
304
+ mode: "update",
305
+ from
306
+ }));
307
+ } finally {
308
+ from.free();
309
+ }
310
+ }
311
+ getEncodedState() {
312
+ return fromUint8Array(this.value.export({ mode: "snapshot" }));
313
+ }
314
+ getStateVector() {
315
+ const version = this.value.oplogVersion();
316
+ try {
317
+ return fromUint8Array(version.encode());
318
+ } finally {
319
+ version.free();
320
+ }
321
+ }
322
+ readStateVersion(encoded) {
323
+ if (!encoded) return [];
324
+ const version = VersionVector.decode(toUint8Array(encoded));
325
+ try {
326
+ return clocksFromVector(version.toJSON());
327
+ } finally {
328
+ version.free();
329
+ }
330
+ }
331
+ readUpdateVersion(update) {
332
+ if (!update) return [];
333
+ const meta = decodeImportBlobMeta(toUint8Array(update), false);
334
+ try {
335
+ return clocksFromVector(meta.partialEndVersionVector.toJSON());
336
+ } finally {
337
+ meta.partialStartVersionVector.free();
338
+ meta.partialEndVersionVector.free();
339
+ }
340
+ }
341
+ containsUpdate(_update) {
342
+ return true;
343
+ }
344
+ hasPending() {
345
+ return this.#_pendingSpans.isPending;
346
+ }
347
+ canDetectPending() {
348
+ return true;
349
+ }
350
+ toJson(type) {
351
+ const serialized = this.value.toJSON();
352
+ if (typeof type === "string") {
353
+ const container = this.#_storage[type];
354
+ if (container) return container instanceof LoroText ? container.toString() : container instanceof LoroCounter ? container.value : container.toJSON();
355
+ return serialized[type] ?? null;
356
+ }
357
+ const { [PLUV_ID_FIELD]: _pluvId, ...json } = serialized;
358
+ return json;
359
+ }
360
+ isDirty() {
361
+ return !!this.value.oplogFrontiers().length;
362
+ }
363
+ isEmpty() {
364
+ const serialized = this.value.toJSON();
365
+ return !serialized || !Object.keys(serialized).length;
366
+ }
367
+ rebuildStorage() {
368
+ if (!!Object.keys(this.#_storage).length) {
369
+ console.warn("Attempted to rebuild storage multiple times");
370
+ return this;
371
+ }
372
+ this.#_storage = Object.entries(this.#_schema.shape).reduce((acc, [key, node]) => {
373
+ Object.assign(acc, { [key]: getLoroShare(this.value, key, node.kind) });
374
+ return acc;
375
+ }, {});
376
+ return this.track();
377
+ }
378
+ redo() {
379
+ this.#_undoManager?.redo();
380
+ return this;
381
+ }
382
+ subscribe(listener) {
383
+ return this.value.subscribe((event) => {
384
+ const from = this.value.frontiersToVV(event.from);
385
+ let update;
386
+ try {
387
+ update = fromUint8Array(this.value.export({
388
+ mode: "update",
389
+ from
390
+ }));
391
+ } finally {
392
+ from.free();
393
+ }
394
+ const origin = event.by === "import" ? this.#_importOrigin ?? (event.origin || null) : event.origin || null;
395
+ listener({
396
+ doc: this,
397
+ local: event.by === "local",
398
+ origin,
399
+ update
400
+ });
401
+ });
402
+ }
403
+ #_import(update, origin) {
404
+ this.#_importOrigin = origin ?? null;
405
+ try {
406
+ this.#_pendingSpans.record(this.value.import(update));
407
+ } finally {
408
+ this.#_importOrigin = null;
409
+ }
410
+ }
411
+ track() {
412
+ if (this.#_undoManager) {
413
+ this.#_undoManager.clear();
414
+ this.#_undoManager.free();
415
+ this.#_undoManager = null;
416
+ }
417
+ this.#_undoManager = new UndoManager(this.value, {
418
+ maxUndoSteps: MAX_UNDO_STEPS,
419
+ mergeInterval: MERGE_INTERVAL_MS
420
+ });
421
+ return this;
422
+ }
423
+ /**
424
+ * @description Unlike Yjs, this method is required to be called after each loro operation.
425
+ * @date January 12, 2025
426
+ */
427
+ transact(fn) {
428
+ fn();
429
+ this.value.commit();
430
+ return this;
431
+ }
432
+ undo() {
433
+ this.#_undoManager?.undo();
434
+ return this;
435
+ }
436
+ #_setPluvId() {
437
+ const text = this.value.getText(PLUV_ID_FIELD);
438
+ const id = typeof crypto !== "undefined" ? crypto.randomUUID() : Math.random().toString();
439
+ text.insert(0, id);
440
+ }
441
+ };
442
+ // src/doc/LoroSchema.ts
443
+ var LoroSchema = class LoroSchema extends AbstractCrdtDocFactory {
444
+ kind = "loro.doc";
445
+ shape;
446
+ _seed;
447
+ constructor(shape, seed) {
448
+ super();
449
+ this.shape = shape;
450
+ this._seed = seed;
451
+ }
452
+ toJSON() {
453
+ return createSchemaNode("loro.doc", { shape: this.shape }).toJSON();
454
+ }
455
+ getEmpty() {
456
+ return new CrdtLoroDoc(this);
457
+ }
458
+ getFactory(seed) {
459
+ return new LoroSchema(this.shape, seed ?? this._seed);
460
+ }
461
+ getInitialized(seed) {
462
+ return new CrdtLoroDoc(this, seed ?? this._seed, true);
463
+ }
464
+ };
465
+ // src/schema/nodes.ts
466
+ const loroList = (item) => {
467
+ assertNotOptional(item, "loro.loroList item");
468
+ return createSchemaNode("loroList", { item });
469
+ };
470
+ const loroMap = (value) => {
471
+ assertNotOptional(value, "loro.loroMap value");
472
+ return createSchemaNode("loroMap", { value });
473
+ };
474
+ const loroText = () => {
475
+ return createSchemaNode("loroText", {});
476
+ };
477
+ const loroMovableList = (item) => {
478
+ assertNotOptional(item, "loro.loroMovableList item");
479
+ return createSchemaNode("loroMovableList", { item });
480
+ };
481
+ const loroCounter = () => {
482
+ return createSchemaNode("loroCounter", {});
483
+ };
484
+ const loroTree = () => {
485
+ return createSchemaNode("loroTree", {});
486
+ };
487
+ // src/schema/fromJSON.ts
488
+ const fromJSON$1 = (ast) => {
489
+ switch (ast.kind) {
490
+ case "loroList": return loroList(fromJSON$1(ast.item));
491
+ case "loroMap": return loroMap(fromJSON$1(ast.value));
492
+ case "loroText": return loroText();
493
+ case "loroMovableList": return loroMovableList(fromJSON$1(ast.item));
494
+ case "loroCounter": return loroCounter();
495
+ case "loroTree": return loroTree();
496
+ default:
497
+ if (isLoroCrdtKind(ast.kind)) throw new CrdtSchemaError(`Unhandled Loro schema kind "${ast.kind}"`);
498
+ return fromJSON(ast);
499
+ }
500
+ };
501
+ // src/schema/schema.ts
502
+ const schema = (shape) => {
503
+ Object.entries(shape).forEach(([key, node]) => {
504
+ if (!isSchemaNode(node)) throw new CrdtSchemaError(`loro.schema field "${key}" must be a schema node`);
505
+ if (isYjsCrdtKind(node.kind)) throw new CrdtSchemaError(`loro.schema field "${key}" cannot be a Yjs node`);
506
+ if (!isLoroCrdtKind(node.kind)) throw new CrdtSchemaError(`loro.schema field "${key}" must be a Loro CRDT node`);
507
+ });
508
+ return new LoroSchema(shape);
509
+ };
510
+ schema.fromJSON = (ast) => {
511
+ if (ast.kind !== "loro.doc") throw new CrdtSchemaError(`Expected kind "loro.doc", received "${ast.kind}"`);
512
+ const shapeAst = ast.shape ?? {};
513
+ const shape = Object.entries(shapeAst).reduce((acc, [key, value]) => {
514
+ acc[key] = fromJSON$1(value);
515
+ return acc;
516
+ }, {});
517
+ return schema(shape);
518
+ };
519
+ // src/doc/doc.ts
520
+ const doc = (_value) => {
521
+ return schema({});
522
+ };
523
+ // src/list.ts
524
+ const list = (value = []) => {
525
+ const container = new LoroList();
526
+ value.forEach((item, i) => {
527
+ if (isContainer(item)) container.insertContainer(i, item);
528
+ else container.insert(i, item);
529
+ });
530
+ return container;
531
+ };
532
+ // src/map.ts
533
+ const map = (value = []) => {
534
+ const container = new LoroMap();
535
+ value.forEach(([key, item]) => {
536
+ if (isContainer(item)) container.setContainer(key, item);
537
+ else container.set(key, item);
538
+ });
539
+ return container;
540
+ };
541
+ // src/movableList.ts
542
+ const movableList = (value = []) => {
543
+ const container = new LoroMovableList();
544
+ value.forEach((item, i) => {
545
+ if (isContainer(item)) container.insertContainer(i, item);
546
+ else container.insert(i, item);
547
+ });
548
+ return container;
549
+ };
550
+ // src/object.ts
551
+ const object = (value) => {
552
+ const container = new LoroMap();
553
+ Object.entries(value).forEach(([key, item]) => {
554
+ if (isContainer(item)) container.setContainer(key, item);
555
+ else container.set(key, item);
556
+ });
557
+ return container;
558
+ };
559
+ // src/text.ts
560
+ const text = (value = "") => {
561
+ const container = new LoroText();
562
+ if (value) container.insert(0, value);
563
+ return container;
564
+ };
565
+ // src/tree.ts
566
+ const tree = () => {
567
+ return new LoroTree();
568
+ };
569
+ // src/loro.ts
570
+ var loro_exports = /* @__PURE__ */ __exportAll({
571
+ CrdtLoroDoc: () => CrdtLoroDoc,
572
+ counter: () => counter,
573
+ doc: () => doc,
574
+ kind: () => kind,
575
+ list: () => list,
576
+ loroCounter: () => loroCounter,
577
+ loroList: () => loroList,
578
+ loroMap: () => loroMap,
579
+ loroMovableList: () => loroMovableList,
580
+ loroText: () => loroText,
581
+ loroTree: () => loroTree,
582
+ map: () => map,
583
+ movableList: () => movableList,
584
+ object: () => object,
585
+ schema: () => schema,
586
+ text: () => text,
587
+ tree: () => tree
588
+ });
589
+ const kind = "loro";
590
+ export { loro_exports as loro };
591
+
592
+ //# sourceMappingURL=browser.mjs.map