@dxos/react-ui-editor 0.3.8-main.c3b295a → 0.3.8-main.c66298b
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/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts +16 -0
- package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts +6 -0
- package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts +7 -0
- package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-plugin/index.d.ts +3 -0
- package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-plugin/plugin.d.ts +25 -0
- package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge.stories.d.ts +8 -0
- package/dist/types/src/automerge/automerge.stories.d.ts.map +1 -0
- package/dist/types/src/automerge/demo.d.ts +25 -0
- package/dist/types/src/automerge/demo.d.ts.map +1 -0
- package/dist/types/stories/html/code-mirror.stories.d.ts.map +1 -1
- package/package.json +34 -29
- package/src/automerge/automerge-plugin/PatchSemaphore.ts +73 -0
- package/src/automerge/automerge-plugin/amToCodemirror.ts +104 -0
- package/src/automerge/automerge-plugin/codeMirrorToAm.ts +37 -0
- package/src/automerge/automerge-plugin/index.ts +6 -0
- package/src/automerge/automerge-plugin/plugin.ts +103 -0
- package/src/automerge/automerge.stories.tsx +104 -0
- package/src/automerge/demo.ts +107 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { next as automerge } from '@automerge/automerge';
|
|
2
|
+
import { type EditorView } from '@codemirror/view';
|
|
3
|
+
import { type Field } from './plugin';
|
|
4
|
+
import { type Peer } from '../demo';
|
|
5
|
+
type Doc<T> = automerge.Doc<T>;
|
|
6
|
+
type Heads = automerge.Heads;
|
|
7
|
+
type ChangeFn = (atHeads: Heads, change: (doc: Doc<unknown>) => void) => Heads | undefined;
|
|
8
|
+
export declare class PatchSemaphore {
|
|
9
|
+
_field: Field;
|
|
10
|
+
_inReconcile: boolean;
|
|
11
|
+
_queue: Array<ChangeFn>;
|
|
12
|
+
constructor(field?: Field);
|
|
13
|
+
reconcile: (handle: Peer, view: EditorView) => void;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=PatchSemaphore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PatchSemaphore.d.ts","sourceRoot":"","sources":["../../../../../src/automerge/automerge-plugin/PatchSemaphore.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,IAAI,IAAI,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAAE,KAAK,KAAK,EAA8E,MAAM,UAAU,CAAC;AAClH,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAEpC,KAAK,GAAG,CAAC,CAAC,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC/B,KAAK,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC;AAE7B,KAAK,QAAQ,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,KAAK,KAAK,GAAG,SAAS,CAAC;AAE3F,qBAAa,cAAc;IACzB,MAAM,EAAG,KAAK,CAAC;IACf,YAAY,UAAS;IACrB,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAM;gBAEjB,KAAK,CAAC,EAAE,KAAK;IAMzB,SAAS,WAAY,IAAI,QAAQ,UAAU,UA2CzC;CACH"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Patch, type Prop } from '@automerge/automerge';
|
|
2
|
+
import { type EditorSelection } from '@codemirror/state';
|
|
3
|
+
import { type EditorView } from '@codemirror/view';
|
|
4
|
+
declare const _default: (view: EditorView, selection: EditorSelection, target: Prop[], patches: Patch[]) => void;
|
|
5
|
+
export default _default;
|
|
6
|
+
//# sourceMappingURL=amToCodemirror.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"amToCodemirror.d.ts","sourceRoot":"","sources":["../../../../../src/automerge/automerge-plugin/amToCodemirror.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,KAAK,EACV,KAAK,IAAI,EAGV,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAA8B,KAAK,eAAe,EAAoB,MAAM,mBAAmB,CAAC;AACvG,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;+BAI7B,UAAU,aAAa,eAAe,UAAU,IAAI,EAAE,WAAW,KAAK,EAAE;AAA9F,wBAgBE"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Heads } from '@automerge/automerge';
|
|
2
|
+
import { type EditorState, type Transaction } from '@codemirror/state';
|
|
3
|
+
import { type Field } from './plugin';
|
|
4
|
+
import { type Peer } from '../demo';
|
|
5
|
+
declare const _default: (field: Field, handle: Peer, transactions: Transaction[], state: EditorState) => Heads | undefined;
|
|
6
|
+
export default _default;
|
|
7
|
+
//# sourceMappingURL=codeMirrorToAm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codeMirrorToAm.d.ts","sourceRoot":"","sources":["../../../../../src/automerge/automerge-plugin/codeMirrorToAm.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,KAAK,WAAW,EAAa,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAElF,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;+CAEE,IAAI,gBAAgB,WAAW,EAAE,SAAS,WAAW,KAAG,KAAK,GAAG,SAAS;AAA/G,wBA0BE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/automerge/automerge-plugin/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as automerge from '@automerge/automerge';
|
|
2
|
+
import { type Doc, type Heads, type Prop } from '@automerge/automerge';
|
|
3
|
+
import { type EditorState, type Extension, StateEffect, StateField, type Transaction, type TransactionSpec } from '@codemirror/state';
|
|
4
|
+
import { type EditorView } from '@codemirror/view';
|
|
5
|
+
import { type Peer } from '../demo';
|
|
6
|
+
export type Value = {
|
|
7
|
+
lastHeads: Heads;
|
|
8
|
+
path: Prop[];
|
|
9
|
+
unreconciledTransactions: Transaction[];
|
|
10
|
+
};
|
|
11
|
+
type UpdateHeads = {
|
|
12
|
+
newHeads: Heads;
|
|
13
|
+
};
|
|
14
|
+
export declare const effectType: import("@codemirror/state").StateEffectType<UpdateHeads>;
|
|
15
|
+
export declare const updateHeads: (newHeads: automerge.next.Heads) => StateEffect<UpdateHeads>;
|
|
16
|
+
export declare const getLastHeads: (state: EditorState, field: Field) => automerge.next.Heads;
|
|
17
|
+
export declare const getPath: (state: EditorState, field: Field) => Prop[];
|
|
18
|
+
export type Field = StateField<Value>;
|
|
19
|
+
export declare const plugin: <T>(doc: automerge.next.Doc<T>, path: Prop[]) => Extension;
|
|
20
|
+
export declare const reconcileAnnotationType: import("@codemirror/state").AnnotationType<unknown>;
|
|
21
|
+
export declare const isReconcileTx: (tr: Transaction) => boolean;
|
|
22
|
+
export declare const makeReconcile: (tr: TransactionSpec) => void;
|
|
23
|
+
export declare const reconcile: (handle: Peer, view: EditorView) => void;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../../../src/automerge/automerge-plugin/plugin.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACvE,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,SAAS,EAEd,WAAW,EACX,UAAU,EACV,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAEpC,MAAM,MAAM,KAAK,GAAG;IAClB,SAAS,EAAE,KAAK,CAAC;IACjB,IAAI,EAAE,IAAI,EAAE,CAAC;IACb,wBAAwB,EAAE,WAAW,EAAE,CAAC;CACzC,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,QAAQ,EAAE,KAAK,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,UAAU,0DAAsC,CAAC;AAE9D,eAAO,MAAM,WAAW,sCAAsB,YAAY,WAAW,CAAgC,CAAC;AAEtG,eAAO,MAAM,YAAY,UAAW,WAAW,uCAAsD,CAAC;AAEtG,eAAO,MAAM,OAAO,UAAW,WAAW,mBAAiB,IAAI,EAA6B,CAAC;AAE7F,MAAM,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;AAMtC,eAAO,MAAM,MAAM,wCAA0B,IAAI,EAAE,KAAG,SAiCrD,CAAC;AAEF,eAAO,MAAM,uBAAuB,qDAA+B,CAAC;AAEpE,eAAO,MAAM,aAAa,OAAQ,WAAW,KAAG,OAAmD,CAAC;AAEpG,eAAO,MAAM,aAAa,OAAQ,eAAe,SAchD,CAAC;AAEF,eAAO,MAAM,SAAS,WAAY,IAAI,QAAQ,UAAU,SAEvD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"automerge.stories.d.ts","sourceRoot":"","sources":["../../../../src/automerge/automerge.stories.tsx"],"names":[],"mappings":";;;AAiGA,wBAEE;AAEF,eAAO,MAAM,WAAW;;CAEvB,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Doc, next as automerge, type ChangeOptions, type ChangeFn, type Heads } from '@automerge/automerge';
|
|
2
|
+
import { Event } from '@dxos/async';
|
|
3
|
+
export declare class Peer {
|
|
4
|
+
changeEvent: Event<void>;
|
|
5
|
+
storage: Map<string, Uint8Array>;
|
|
6
|
+
stats: {
|
|
7
|
+
messagesSent: number;
|
|
8
|
+
messagesReceived: number;
|
|
9
|
+
bytesSent: number;
|
|
10
|
+
bytesReceived: number;
|
|
11
|
+
snapshotBytes: number;
|
|
12
|
+
incrementalBytes: number;
|
|
13
|
+
};
|
|
14
|
+
doc: Doc<{
|
|
15
|
+
text: string;
|
|
16
|
+
}>;
|
|
17
|
+
change(options: string | ChangeOptions<any> | ChangeFn<any>, callback?: ChangeFn<any>): void;
|
|
18
|
+
changeAt(scope: Heads, options: string | ChangeOptions<any> | ChangeFn<any>, callback?: ChangeFn<any>): automerge.Heads | null;
|
|
19
|
+
replicate(): {
|
|
20
|
+
readable: ReadableStream<Uint8Array>;
|
|
21
|
+
writable: WritableStream<Uint8Array>;
|
|
22
|
+
};
|
|
23
|
+
private _handleChange;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=demo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../../../../src/automerge/demo.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,GAAG,EACR,IAAI,IAAI,SAAS,EACjB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,KAAK,EAEX,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,qBAAa,IAAI;IACf,WAAW,cAAe;IAE1B,OAAO,0BAAiC;IACxC,KAAK;;;;;;;MAOH;IAEF,GAAG,EAAG,GAAG,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAE5B,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC;IAKrF,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC;IAOrG,SAAS,IAAI;QAAE,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;QAAC,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,CAAA;KAAE;IAgD3F,OAAO,CAAC,aAAa;CAgBtB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"code-mirror.stories.d.ts","sourceRoot":"","sources":["../../../../stories/html/code-mirror.stories.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"code-mirror.stories.d.ts","sourceRoot":"","sources":["../../../../stories/html/code-mirror.stories.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAKhD,OAAO,EAAE,MAAM,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;;;;AAS5D,wBAEE;AAoEF,eAAO,MAAM,OAAO,EAAE,QAAQ,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,CAoBjF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-editor",
|
|
3
|
-
"version": "0.3.8-main.
|
|
3
|
+
"version": "0.3.8-main.c66298b",
|
|
4
4
|
"description": "Document editing experience within a DXOS shell.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -13,20 +13,20 @@
|
|
|
13
13
|
"src"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@codemirror/autocomplete": "^6.
|
|
17
|
-
"@codemirror/commands": "^6.
|
|
18
|
-
"@codemirror/lang-markdown": "^6.
|
|
19
|
-
"@codemirror/language": "^6.
|
|
20
|
-
"@codemirror/language-data": "^6.1
|
|
21
|
-
"@codemirror/lint": "^6.2
|
|
22
|
-
"@codemirror/search": "^6.
|
|
23
|
-
"@codemirror/state": "^6.
|
|
24
|
-
"@codemirror/theme-one-dark": "^6.1.
|
|
25
|
-
"@codemirror/view": "^6.
|
|
16
|
+
"@codemirror/autocomplete": "^6.11.0",
|
|
17
|
+
"@codemirror/commands": "^6.3.0",
|
|
18
|
+
"@codemirror/lang-markdown": "^6.2.2",
|
|
19
|
+
"@codemirror/language": "^6.9.2",
|
|
20
|
+
"@codemirror/language-data": "^6.3.1",
|
|
21
|
+
"@codemirror/lint": "^6.4.2",
|
|
22
|
+
"@codemirror/search": "^6.5.4",
|
|
23
|
+
"@codemirror/state": "^6.3.1",
|
|
24
|
+
"@codemirror/theme-one-dark": "^6.1.2",
|
|
25
|
+
"@codemirror/view": "^6.22.0",
|
|
26
26
|
"@fluentui/react-tabster": "^9.12.0",
|
|
27
|
-
"@lezer/common": "^1.
|
|
28
|
-
"@lezer/highlight": "^1.
|
|
29
|
-
"@lezer/markdown": "^1.
|
|
27
|
+
"@lezer/common": "^1.1.1",
|
|
28
|
+
"@lezer/highlight": "^1.2.0",
|
|
29
|
+
"@lezer/markdown": "^1.1.1",
|
|
30
30
|
"@replit/codemirror-vim": "^6.0.14",
|
|
31
31
|
"@tiptap/core": "2.0.0-beta.220",
|
|
32
32
|
"@tiptap/extension-collaboration": "2.0.0-beta.220",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@tiptap/pm": "2.0.0-beta.220",
|
|
38
38
|
"@tiptap/react": "2.0.0-beta.220",
|
|
39
39
|
"@tiptap/starter-kit": "2.0.0-beta.220",
|
|
40
|
-
"codemirror": "^6.
|
|
40
|
+
"codemirror": "^6.65.7",
|
|
41
41
|
"lib0": "^0.2.65",
|
|
42
42
|
"lodash.get": "^4.4.2",
|
|
43
43
|
"prosemirror-model": "^1.19.0",
|
|
@@ -47,31 +47,36 @@
|
|
|
47
47
|
"y-prosemirror": "~1.0.20",
|
|
48
48
|
"y-protocols": "^1.0.5",
|
|
49
49
|
"yjs": "^13.6.8",
|
|
50
|
-
"@dxos/debug": "0.3.8-main.
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/react-
|
|
55
|
-
"@dxos/react-
|
|
56
|
-
"@dxos/react-ui": "0.3.8-main.
|
|
57
|
-
"@dxos/util": "0.3.8-main.
|
|
58
|
-
"@dxos/text-model": "0.3.8-main.
|
|
50
|
+
"@dxos/debug": "0.3.8-main.c66298b",
|
|
51
|
+
"@dxos/protocols": "0.3.8-main.c66298b",
|
|
52
|
+
"@dxos/display-name": "0.3.8-main.c66298b",
|
|
53
|
+
"@dxos/log": "0.3.8-main.c66298b",
|
|
54
|
+
"@dxos/react-ui": "0.3.8-main.c66298b",
|
|
55
|
+
"@dxos/react-async": "0.3.8-main.c66298b",
|
|
56
|
+
"@dxos/react-ui-theme": "0.3.8-main.c66298b",
|
|
57
|
+
"@dxos/util": "0.3.8-main.c66298b",
|
|
58
|
+
"@dxos/text-model": "0.3.8-main.c66298b"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
+
"@automerge/automerge": "^2.1.7",
|
|
62
|
+
"@automerge/automerge-codemirror": "^0.0.7",
|
|
63
|
+
"@automerge/automerge-repo": "^1.0.17",
|
|
64
|
+
"@codemirror/basic-setup": "^0.20.0",
|
|
65
|
+
"@types/codemirror": "^5.60.14",
|
|
61
66
|
"@types/lodash.get": "^4.4.7",
|
|
62
67
|
"@types/react": "^18.0.21",
|
|
63
68
|
"@types/react-dom": "^18.0.6",
|
|
64
69
|
"react": "^18.2.0",
|
|
65
70
|
"react-dom": "^18.2.0",
|
|
66
|
-
"@dxos/config": "0.3.8-main.
|
|
67
|
-
"@dxos/echo-schema": "0.3.8-main.
|
|
68
|
-
"@dxos/echo-typegen": "0.3.8-main.
|
|
69
|
-
"@dxos/react-client": "0.3.8-main.
|
|
71
|
+
"@dxos/config": "0.3.8-main.c66298b",
|
|
72
|
+
"@dxos/echo-schema": "0.3.8-main.c66298b",
|
|
73
|
+
"@dxos/echo-typegen": "0.3.8-main.c66298b",
|
|
74
|
+
"@dxos/react-client": "0.3.8-main.c66298b"
|
|
70
75
|
},
|
|
71
76
|
"peerDependencies": {
|
|
72
77
|
"react": "^18.2.0",
|
|
73
78
|
"react-dom": "^18.2.0",
|
|
74
|
-
"@dxos/react-client": "0.3.8-main.
|
|
79
|
+
"@dxos/react-client": "0.3.8-main.c66298b"
|
|
75
80
|
},
|
|
76
81
|
"publishConfig": {
|
|
77
82
|
"access": "public"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { next as automerge } from '@automerge/automerge';
|
|
6
|
+
import { type EditorView } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
import amToCodemirror from './amToCodemirror';
|
|
9
|
+
import codeMirrorToAm from './codeMirrorToAm';
|
|
10
|
+
import { type Field, isReconcileTx, getPath, reconcileAnnotationType, updateHeads, getLastHeads } from './plugin';
|
|
11
|
+
import { type Peer } from '../demo';
|
|
12
|
+
|
|
13
|
+
type Doc<T> = automerge.Doc<T>;
|
|
14
|
+
type Heads = automerge.Heads;
|
|
15
|
+
|
|
16
|
+
type ChangeFn = (atHeads: Heads, change: (doc: Doc<unknown>) => void) => Heads | undefined;
|
|
17
|
+
|
|
18
|
+
export class PatchSemaphore {
|
|
19
|
+
_field!: Field;
|
|
20
|
+
_inReconcile = false;
|
|
21
|
+
_queue: Array<ChangeFn> = [];
|
|
22
|
+
|
|
23
|
+
constructor(field?: Field) {
|
|
24
|
+
if (field !== undefined) {
|
|
25
|
+
this._field = field;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
reconcile = (handle: Peer, view: EditorView) => {
|
|
30
|
+
if (!this._inReconcile) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this._inReconcile = true;
|
|
34
|
+
|
|
35
|
+
const path = getPath(view.state, this._field);
|
|
36
|
+
const oldHeads = getLastHeads(view.state, this._field);
|
|
37
|
+
let selection = view.state.selection;
|
|
38
|
+
|
|
39
|
+
const transactions = view.state.field(this._field).unreconciledTransactions.filter((tx) => !isReconcileTx(tx));
|
|
40
|
+
|
|
41
|
+
// First undo all the unreconciled transactions
|
|
42
|
+
const toInvert = transactions.slice().reverse();
|
|
43
|
+
for (const tx of toInvert) {
|
|
44
|
+
const inverted = tx.changes.invert(tx.startState.doc);
|
|
45
|
+
selection = selection.map(inverted);
|
|
46
|
+
view.dispatch({
|
|
47
|
+
changes: inverted,
|
|
48
|
+
annotations: reconcileAnnotationType.of(true),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// now apply the unreconciled transactions to the document
|
|
53
|
+
let newHeads = codeMirrorToAm(this._field, handle, transactions, view.state);
|
|
54
|
+
|
|
55
|
+
// NOTE: null and undefined each come from automerge and repo respectively
|
|
56
|
+
if (newHeads === null || newHeads === undefined) {
|
|
57
|
+
// TODO: @alexjg this is the call that's resetting the editor state on click
|
|
58
|
+
newHeads = automerge.getHeads(handle.doc);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// now get the diff between the updated state of the document and the heads
|
|
62
|
+
// and apply that to the codemirror doc
|
|
63
|
+
const diff = automerge.equals(oldHeads, newHeads) ? [] : automerge.diff(handle.doc, oldHeads, newHeads);
|
|
64
|
+
amToCodemirror(view, selection, path, diff);
|
|
65
|
+
|
|
66
|
+
view.dispatch({
|
|
67
|
+
effects: updateHeads(newHeads),
|
|
68
|
+
annotations: reconcileAnnotationType.of({}),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this._inReconcile = false;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type DelPatch,
|
|
7
|
+
type InsertPatch,
|
|
8
|
+
type Patch,
|
|
9
|
+
type Prop,
|
|
10
|
+
type PutPatch,
|
|
11
|
+
type SpliceTextPatch,
|
|
12
|
+
} from '@automerge/automerge';
|
|
13
|
+
import { ChangeSet, type ChangeSpec, type EditorSelection, type EditorState } from '@codemirror/state';
|
|
14
|
+
import { type EditorView } from '@codemirror/view';
|
|
15
|
+
|
|
16
|
+
import { reconcileAnnotationType } from './plugin';
|
|
17
|
+
|
|
18
|
+
export default (view: EditorView, selection: EditorSelection, target: Prop[], patches: Patch[]) => {
|
|
19
|
+
for (const patch of patches) {
|
|
20
|
+
const changeSpec = handlePatch(patch, target, view.state);
|
|
21
|
+
if (changeSpec != null) {
|
|
22
|
+
const changeSet = ChangeSet.of(changeSpec, view.state.doc.length, '\n');
|
|
23
|
+
selection = selection.map(changeSet, 1);
|
|
24
|
+
view.dispatch({
|
|
25
|
+
changes: changeSet,
|
|
26
|
+
annotations: reconcileAnnotationType.of({}),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
view.dispatch({
|
|
31
|
+
selection,
|
|
32
|
+
annotations: reconcileAnnotationType.of({}),
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handlePatch = (patch: Patch, target: Prop[], state: EditorState): ChangeSpec | null => {
|
|
37
|
+
if (patch.action === 'insert') {
|
|
38
|
+
return handleInsert(target, patch);
|
|
39
|
+
} else if (patch.action === 'splice') {
|
|
40
|
+
return handleSplice(target, patch);
|
|
41
|
+
} else if (patch.action === 'del') {
|
|
42
|
+
return handleDel(target, patch);
|
|
43
|
+
} else if (patch.action === 'put') {
|
|
44
|
+
return handlePut(target, patch, state);
|
|
45
|
+
} else {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleInsert = (target: Prop[], patch: InsertPatch): Array<ChangeSpec> => {
|
|
51
|
+
const index = charPath(target, patch.path);
|
|
52
|
+
if (index == null) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const text = patch.values.map((v) => (v ? v.toString() : '')).join('');
|
|
56
|
+
return [{ from: index, to: index, insert: text }];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleSplice = (target: Prop[], patch: SpliceTextPatch): Array<ChangeSpec> => {
|
|
60
|
+
const index = charPath(target, patch.path);
|
|
61
|
+
if (index == null) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
return [{ from: index, insert: patch.value }];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleDel = (target: Prop[], patch: DelPatch): Array<ChangeSpec> => {
|
|
68
|
+
const index = charPath(target, patch.path);
|
|
69
|
+
if (index == null) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const length = patch.length || 1;
|
|
73
|
+
return [{ from: index, to: index + length }];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handlePut = (target: Prop[], patch: PutPatch, state: EditorState): Array<ChangeSpec> => {
|
|
77
|
+
const index = charPath(target, [...patch.path, 0]);
|
|
78
|
+
if (index == null) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const length = state.doc.length;
|
|
82
|
+
if (typeof patch.value !== 'string') {
|
|
83
|
+
return []; // TODO(dmaretskyi): How to handle non string values?
|
|
84
|
+
}
|
|
85
|
+
return [{ from: 0, to: length, insert: patch.value as any }];
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// If the path of the patch is of the form [path, <index>] then we know this is
|
|
89
|
+
// a path to a character within the sequence given by path
|
|
90
|
+
const charPath = (textPath: Prop[], candidatePath: Prop[]): number | null => {
|
|
91
|
+
if (candidatePath.length !== textPath.length + 1) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
for (let i = 0; i < textPath.length; i++) {
|
|
95
|
+
if (textPath[i] !== candidatePath[i]) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const index = candidatePath[candidatePath.length - 1];
|
|
100
|
+
if (typeof index === 'number') {
|
|
101
|
+
return index;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { next as am, type Heads } from '@automerge/automerge';
|
|
6
|
+
import { type EditorState, type Text, type Transaction } from '@codemirror/state';
|
|
7
|
+
|
|
8
|
+
import { type Field } from './plugin';
|
|
9
|
+
import { type Peer } from '../demo';
|
|
10
|
+
|
|
11
|
+
export default (field: Field, handle: Peer, transactions: Transaction[], state: EditorState): Heads | undefined => {
|
|
12
|
+
const { lastHeads, path } = state.field(field);
|
|
13
|
+
|
|
14
|
+
// We don't want to call `automerge.updateAt` if there are no changes.
|
|
15
|
+
// Otherwise later on `automerge.diff` will return empty patches that result in a no-op but still mess up the selection.
|
|
16
|
+
let hasChanges = false;
|
|
17
|
+
for (const tr of transactions) {
|
|
18
|
+
if (tr.changes.length) {
|
|
19
|
+
tr.changes.iterChanges(() => {
|
|
20
|
+
hasChanges = true;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!hasChanges) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const newHeads = handle.changeAt(lastHeads, (doc: am.Doc<unknown>) => {
|
|
30
|
+
for (const tr of transactions) {
|
|
31
|
+
tr.changes.iterChanges((fromA: number, toA: number, _fromB: number, _toB: number, inserted: Text) => {
|
|
32
|
+
am.splice(doc, path, fromA, toA - fromA, inserted.toString());
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return newHeads ?? undefined;
|
|
37
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as automerge from '@automerge/automerge';
|
|
6
|
+
import { type Doc, type Heads, type Prop } from '@automerge/automerge';
|
|
7
|
+
import {
|
|
8
|
+
Annotation,
|
|
9
|
+
type EditorState,
|
|
10
|
+
type Extension,
|
|
11
|
+
Facet,
|
|
12
|
+
StateEffect,
|
|
13
|
+
StateField,
|
|
14
|
+
type Transaction,
|
|
15
|
+
type TransactionSpec,
|
|
16
|
+
} from '@codemirror/state';
|
|
17
|
+
import { type EditorView } from '@codemirror/view';
|
|
18
|
+
|
|
19
|
+
import { PatchSemaphore } from './PatchSemaphore';
|
|
20
|
+
import { type Peer } from '../demo';
|
|
21
|
+
|
|
22
|
+
export type Value = {
|
|
23
|
+
lastHeads: Heads;
|
|
24
|
+
path: Prop[];
|
|
25
|
+
unreconciledTransactions: Transaction[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type UpdateHeads = {
|
|
29
|
+
newHeads: Heads;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const effectType = StateEffect.define<UpdateHeads>({});
|
|
33
|
+
|
|
34
|
+
export const updateHeads = (newHeads: Heads): StateEffect<UpdateHeads> => effectType.of({ newHeads });
|
|
35
|
+
|
|
36
|
+
export const getLastHeads = (state: EditorState, field: Field): Heads => state.field(field).lastHeads;
|
|
37
|
+
|
|
38
|
+
export const getPath = (state: EditorState, field: Field): Prop[] => state.field(field).path;
|
|
39
|
+
|
|
40
|
+
export type Field = StateField<Value>;
|
|
41
|
+
|
|
42
|
+
const semaphoreFacet = Facet.define<PatchSemaphore, PatchSemaphore>({
|
|
43
|
+
combine: (values) => values.at(-1)!, // Take last.
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const plugin = <T>(doc: Doc<T>, path: Prop[]): Extension => {
|
|
47
|
+
const stateField: StateField<Value> = StateField.define({
|
|
48
|
+
create: () => ({
|
|
49
|
+
lastHeads: automerge.getHeads(doc),
|
|
50
|
+
unreconciledTransactions: [],
|
|
51
|
+
path: path.slice(),
|
|
52
|
+
}),
|
|
53
|
+
update: (value: Value, tr: Transaction) => {
|
|
54
|
+
const result: Value = {
|
|
55
|
+
lastHeads: value.lastHeads,
|
|
56
|
+
unreconciledTransactions: value.unreconciledTransactions.slice(),
|
|
57
|
+
path: path.slice(),
|
|
58
|
+
};
|
|
59
|
+
let clearUnreconciled = false;
|
|
60
|
+
for (const effect of tr.effects) {
|
|
61
|
+
if (effect.is(effectType)) {
|
|
62
|
+
result.lastHeads = effect.value.newHeads;
|
|
63
|
+
clearUnreconciled = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (clearUnreconciled) {
|
|
67
|
+
result.unreconciledTransactions = [];
|
|
68
|
+
} else {
|
|
69
|
+
if (!isReconcileTx(tr)) {
|
|
70
|
+
result.unreconciledTransactions.push(tr);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const semaphore = new PatchSemaphore(stateField);
|
|
77
|
+
|
|
78
|
+
return [stateField, semaphoreFacet.of(semaphore)];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const reconcileAnnotationType = Annotation.define<unknown>();
|
|
82
|
+
|
|
83
|
+
export const isReconcileTx = (tr: Transaction): boolean => !!tr.annotation(reconcileAnnotationType);
|
|
84
|
+
|
|
85
|
+
export const makeReconcile = (tr: TransactionSpec) => {
|
|
86
|
+
if (tr.annotations != null) {
|
|
87
|
+
if (tr.annotations instanceof Array) {
|
|
88
|
+
tr.annotations = [...tr.annotations, reconcileAnnotationType.of({})];
|
|
89
|
+
} else {
|
|
90
|
+
tr.annotations = [tr.annotations, reconcileAnnotationType.of({})];
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
tr.annotations = [reconcileAnnotationType.of({})];
|
|
94
|
+
}
|
|
95
|
+
// return {
|
|
96
|
+
// ...tr,
|
|
97
|
+
// annotations: reconcileAnnotationType.of({})
|
|
98
|
+
// }
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const reconcile = (handle: Peer, view: EditorView) => {
|
|
102
|
+
view.state.facet(semaphoreFacet).reconcile(handle, view);
|
|
103
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Prop, next as automerge } from '@automerge/automerge';
|
|
6
|
+
import { basicSetup } from '@codemirror/basic-setup';
|
|
7
|
+
import { EditorView } from '@codemirror/view';
|
|
8
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
9
|
+
|
|
10
|
+
import { plugin as amgPlugin } from './automerge-plugin';
|
|
11
|
+
import { reconcile } from './automerge-plugin/plugin';
|
|
12
|
+
import { Peer } from './demo';
|
|
13
|
+
|
|
14
|
+
type EditorProps = {
|
|
15
|
+
handle: Peer;
|
|
16
|
+
path: Prop[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const Editor = ({ handle, path }: EditorProps) => {
|
|
20
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
21
|
+
const editorRoot = useRef<EditorView>();
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const doc = handle.doc;
|
|
25
|
+
const source = doc.text; // this should use path
|
|
26
|
+
const plugin = amgPlugin(doc, path);
|
|
27
|
+
const view = (editorRoot.current = new EditorView({
|
|
28
|
+
doc: source,
|
|
29
|
+
extensions: [basicSetup, plugin],
|
|
30
|
+
dispatch: (transaction) => {
|
|
31
|
+
view.update([transaction]);
|
|
32
|
+
reconcile(handle, view);
|
|
33
|
+
},
|
|
34
|
+
parent: containerRef.current as any,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const handleChange = () => {
|
|
38
|
+
reconcile(handle, view);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
handle.changeEvent.on(handleChange);
|
|
42
|
+
|
|
43
|
+
return () => {
|
|
44
|
+
handle.changeEvent.off(handleChange);
|
|
45
|
+
view.destroy();
|
|
46
|
+
};
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
return <div className='codemirror-editor' ref={containerRef} onKeyDown={(evt) => evt.stopPropagation()} />;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const Story = () => {
|
|
53
|
+
const [object1, setObject1] = useState<Peer | null>(null);
|
|
54
|
+
const [object2, setObject2] = useState<Peer | null>(null);
|
|
55
|
+
const [stats1, setStats1] = useState<any>({});
|
|
56
|
+
const [stats2, setStats2] = useState<any>({});
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const object1 = new Peer();
|
|
60
|
+
object1.doc = automerge.from({ text: 'Hello world!' });
|
|
61
|
+
|
|
62
|
+
const object2 = new Peer();
|
|
63
|
+
object2.doc = automerge.init();
|
|
64
|
+
|
|
65
|
+
const r1 = object1.replicate();
|
|
66
|
+
const r2 = object2.replicate();
|
|
67
|
+
|
|
68
|
+
void r1.readable.pipeTo(r2.writable);
|
|
69
|
+
void r2.readable.pipeTo(r1.writable);
|
|
70
|
+
|
|
71
|
+
setObject1(object1);
|
|
72
|
+
setObject2(object2);
|
|
73
|
+
|
|
74
|
+
setInterval(() => {
|
|
75
|
+
setStats1({ ...object1.stats });
|
|
76
|
+
setStats2({ ...object2.stats });
|
|
77
|
+
}, 500);
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
if (!object1 || !object2) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', width: '100vw' }}>
|
|
86
|
+
<div>
|
|
87
|
+
<Editor handle={object1} path={['text']} />
|
|
88
|
+
<div style={{ whiteSpace: 'pre' }}>{JSON.stringify(stats1, null, 2)}</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div>
|
|
91
|
+
<Editor handle={object2} path={['text']} />
|
|
92
|
+
<div style={{ whiteSpace: 'pre' }}>{JSON.stringify(stats2, null, 2)}</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export default {
|
|
99
|
+
title: 'Automerge',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const EditorStory = {
|
|
103
|
+
render: () => <Story />,
|
|
104
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type Doc,
|
|
7
|
+
next as automerge,
|
|
8
|
+
type ChangeOptions,
|
|
9
|
+
type ChangeFn,
|
|
10
|
+
type Heads,
|
|
11
|
+
type SyncState,
|
|
12
|
+
} from '@automerge/automerge';
|
|
13
|
+
|
|
14
|
+
import { Event } from '@dxos/async';
|
|
15
|
+
|
|
16
|
+
export class Peer {
|
|
17
|
+
changeEvent = new Event();
|
|
18
|
+
|
|
19
|
+
storage = new Map<string, Uint8Array>();
|
|
20
|
+
stats = {
|
|
21
|
+
messagesSent: 0,
|
|
22
|
+
messagesReceived: 0,
|
|
23
|
+
bytesSent: 0,
|
|
24
|
+
bytesReceived: 0,
|
|
25
|
+
snapshotBytes: 0,
|
|
26
|
+
incrementalBytes: 0,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
doc!: Doc<{ text: string }>;
|
|
30
|
+
|
|
31
|
+
change(options: string | ChangeOptions<any> | ChangeFn<any>, callback?: ChangeFn<any>) {
|
|
32
|
+
this.doc = automerge.change(this.doc, options, callback);
|
|
33
|
+
this._handleChange();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
changeAt(scope: Heads, options: string | ChangeOptions<any> | ChangeFn<any>, callback?: ChangeFn<any>) {
|
|
37
|
+
const { newDoc, newHeads } = automerge.changeAt(this.doc, scope, options, callback);
|
|
38
|
+
this.doc = newDoc;
|
|
39
|
+
this._handleChange();
|
|
40
|
+
return newHeads;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
replicate(): { readable: ReadableStream<Uint8Array>; writable: WritableStream<Uint8Array> } {
|
|
44
|
+
let syncState: SyncState = automerge.initSyncState();
|
|
45
|
+
|
|
46
|
+
let unsub: (() => void) | undefined;
|
|
47
|
+
const readable = new ReadableStream({
|
|
48
|
+
start: (controller) => {
|
|
49
|
+
unsub = this.changeEvent.on(() => {
|
|
50
|
+
const [newState, msg] = automerge.generateSyncMessage(this.doc, syncState);
|
|
51
|
+
syncState = newState;
|
|
52
|
+
|
|
53
|
+
if (msg) {
|
|
54
|
+
this.stats.messagesSent++;
|
|
55
|
+
this.stats.bytesSent += msg.byteLength;
|
|
56
|
+
controller.enqueue(msg);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
pull: (controller) => {
|
|
61
|
+
const [newState, msg] = automerge.generateSyncMessage(this.doc, syncState);
|
|
62
|
+
syncState = newState;
|
|
63
|
+
|
|
64
|
+
if (msg) {
|
|
65
|
+
this.stats.messagesSent++;
|
|
66
|
+
this.stats.bytesSent += msg.byteLength;
|
|
67
|
+
controller.enqueue(msg);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
cancel: () => {
|
|
71
|
+
unsub?.();
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const writable = new WritableStream({
|
|
76
|
+
write: (chunk) => {
|
|
77
|
+
this.stats.messagesReceived++;
|
|
78
|
+
this.stats.bytesReceived += chunk.byteLength;
|
|
79
|
+
|
|
80
|
+
const [newDoc, newState] = automerge.receiveSyncMessage(this.doc, syncState, chunk);
|
|
81
|
+
this.doc = newDoc;
|
|
82
|
+
syncState = newState;
|
|
83
|
+
|
|
84
|
+
this._handleChange();
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return { readable, writable };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private _handleChange() {
|
|
92
|
+
// TODO(dmaretskyi): Save since + debounce.
|
|
93
|
+
|
|
94
|
+
if (this.stats.incrementalBytes > 1024 && this.stats.incrementalBytes > this.stats.snapshotBytes) {
|
|
95
|
+
const snapshot = automerge.save(this.doc);
|
|
96
|
+
this.stats.snapshotBytes = snapshot.byteLength;
|
|
97
|
+
this.stats.incrementalBytes = 0;
|
|
98
|
+
console.log('snapshot save', snapshot.byteLength);
|
|
99
|
+
} else {
|
|
100
|
+
const chunk = automerge.saveIncremental(this.doc);
|
|
101
|
+
this.stats.incrementalBytes += chunk.length;
|
|
102
|
+
console.log('incremental save', chunk.length);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.changeEvent.emit();
|
|
106
|
+
}
|
|
107
|
+
}
|