@pylonsync/loro 0.22.39 → 0.22.41

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/src/server.ts ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * A row's live CRDT document in a server function (an action), through
3
+ * `ctx.crdt`:
4
+ *
5
+ * import { openLiveDoc } from "@pylonsync/loro/server";
6
+ *
7
+ * const live = await openLiveDoc(ctx, "Doc", id);
8
+ * await live.edit((doc) => doc.getMap("row").set("status", "done"));
9
+ *
10
+ * `edit` pulls in what others wrote since the doc was opened, runs the
11
+ * change, and sends only this function's ops. Everyone editing sees them as
12
+ * they land. For a ProseMirror field, `editProseMirror` in
13
+ * `@pylonsync/loro/prosemirror` does the same with a ProseMirror document.
14
+ *
15
+ * Each `openLiveDoc` is a new Loro peer, so two functions editing one row
16
+ * at once merge like two people do.
17
+ */
18
+ import { LoroDoc } from "loro-crdt";
19
+ import { exportOwnOps, ownCounter } from "./outbox";
20
+
21
+ /** The byte operations `ctx.crdt` provides (`@pylonsync/functions` `Crdt`). */
22
+ export interface CrdtOps {
23
+ snapshot(entity: string, id: string): Promise<Uint8Array | null>;
24
+ since(entity: string, id: string, version: Uint8Array): Promise<Uint8Array | null>;
25
+ apply(entity: string, id: string, update: Uint8Array): Promise<void>;
26
+ }
27
+
28
+ export interface LiveDocContext {
29
+ crdt: CrdtOps & { unsafe: CrdtOps };
30
+ }
31
+
32
+ export interface OpenLiveDocOptions {
33
+ /** Server trust even under strict function policies (`ctx.crdt.unsafe`). */
34
+ unsafe?: boolean;
35
+ }
36
+
37
+ export class LiveDoc {
38
+ readonly doc: LoroDoc;
39
+ readonly entity: string;
40
+ readonly id: string;
41
+ private readonly ops: CrdtOps;
42
+ /** This peer's op counter the server has merged (exclusive end). */
43
+ private confirmed = 0;
44
+
45
+ private constructor(ops: CrdtOps, entity: string, id: string, doc: LoroDoc) {
46
+ this.ops = ops;
47
+ this.entity = entity;
48
+ this.id = id;
49
+ this.doc = doc;
50
+ }
51
+
52
+ /** Open the row's document. Throws NOT_FOUND when there is none to open. */
53
+ static async open(ctx: LiveDocContext, entity: string, id: string, options: OpenLiveDocOptions = {}): Promise<LiveDoc> {
54
+ const ops = options.unsafe ? ctx.crdt.unsafe : ctx.crdt;
55
+ const snapshot = await ops.snapshot(entity, id);
56
+ if (!snapshot) {
57
+ throw Object.assign(new Error(`${entity} ${id} has no document to open`), { code: "NOT_FOUND" });
58
+ }
59
+ const doc = new LoroDoc();
60
+ doc.import(snapshot);
61
+ return new LiveDoc(ops, entity, id, doc);
62
+ }
63
+
64
+ /** Pull in what others wrote since this doc last heard from the server. */
65
+ async refresh(): Promise<void> {
66
+ const update = await this.ops.since(this.entity, this.id, this.doc.oplogVersion().encode());
67
+ if (update && update.length > 0) this.doc.import(update);
68
+ }
69
+
70
+ /**
71
+ * Send this function's ops the server has not merged yet. Commits pending
72
+ * changes first. Returns false when there was nothing to send.
73
+ */
74
+ async commit(): Promise<boolean> {
75
+ this.doc.commit();
76
+ const target = ownCounter(this.doc);
77
+ if (target <= this.confirmed) return false;
78
+ await this.ops.apply(this.entity, this.id, exportOwnOps(this.doc, this.confirmed));
79
+ this.confirmed = target;
80
+ return true;
81
+ }
82
+
83
+ /** Refresh, run `change` on the doc, and send the result. */
84
+ async edit(change: (doc: LoroDoc) => void | Promise<void>): Promise<void> {
85
+ await this.refresh();
86
+ await change(this.doc);
87
+ await this.commit();
88
+ }
89
+ }
90
+
91
+ /** See {@link LiveDoc.open}. */
92
+ export function openLiveDoc(
93
+ ctx: LiveDocContext,
94
+ entity: string,
95
+ id: string,
96
+ options: OpenLiveDocOptions = {},
97
+ ): Promise<LiveDoc> {
98
+ return LiveDoc.open(ctx, entity, id, options);
99
+ }