edytor 4.0.0 → 6.0.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/README.md +2 -2
- package/dist/block/block.svelte.js +6 -21
- package/dist/block/inlineBlock.svelte.js +4 -6
- package/dist/clipboard/moveText.js +6 -8
- package/dist/cloudflare/DocumentRoom.d.ts +10 -3
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/room/access.d.ts +8 -3
- package/dist/cloudflare/room/access.js +44 -20
- package/dist/cloudflare/room/admission.js +28 -5
- package/dist/cloudflare/room/comments.js +28 -1
- package/dist/cloudflare/room/context.d.ts +106 -109
- package/dist/cloudflare/room/context.js +10 -3
- package/dist/cloudflare/room/forged.d.ts +14 -0
- package/dist/cloudflare/room/forged.js +30 -2
- package/dist/cloudflare/room/frames.d.ts +4 -0
- package/dist/cloudflare/room/frames.js +5 -1
- package/dist/cloudflare/room/purge.js +2 -1
- package/dist/cloudflare/room/replicas.d.ts +4 -2
- package/dist/cloudflare/room/replicas.js +8 -3
- package/dist/cloudflare/room/suggest.d.ts +7 -0
- package/dist/cloudflare/room/suggest.js +143 -0
- package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
- package/dist/cloudflare/routeDocumentSocket.js +5 -3
- package/dist/components/Content.svelte +9 -1
- package/dist/crdt/anchors.js +3 -2
- package/dist/crdt/attribution/attribution.d.ts +3 -0
- package/dist/crdt/attribution/attribution.js +3 -3
- package/dist/crdt/doc/funnel.d.ts +1 -1
- package/dist/crdt/doc/funnel.js +5 -2
- package/dist/crdt/doc/gate.d.ts +1 -1
- package/dist/crdt/doc/history.d.ts +6 -0
- package/dist/crdt/doc/history.js +79 -59
- package/dist/crdt/doc/ops.d.ts +81 -0
- package/dist/crdt/doc/ops.js +1898 -0
- package/dist/crdt/doc/reads.d.ts +2 -2
- package/dist/crdt/doc/reads.js +18 -28
- package/dist/crdt/doc/suggest.d.ts +167 -0
- package/dist/crdt/doc/suggest.js +579 -0
- package/dist/crdt/document.d.ts +10 -1
- package/dist/crdt/document.js +19 -11
- package/dist/crdt/edytor-doc.d.ts +294 -272
- package/dist/crdt/edytor-doc.js +123 -108
- package/dist/crdt/engine-api.d.ts +8 -2
- package/dist/crdt/engine.d.ts +2 -1
- package/dist/crdt/engine.js +3 -1
- package/dist/crdt/flow.d.ts +5 -47
- package/dist/crdt/flow.js +3 -269
- package/dist/crdt/index.d.ts +102 -108
- package/dist/crdt/index.js +3 -0
- package/dist/crdt/operations.d.ts +13 -4
- package/dist/crdt/parts.d.ts +43 -0
- package/dist/crdt/parts.js +44 -0
- package/dist/crdt/placement/display.d.ts +1 -1
- package/dist/crdt/placement/display.js +3 -3
- package/dist/crdt/placement/keys.d.ts +5 -0
- package/dist/crdt/placement/keys.js +5 -0
- package/dist/crdt/placement/resolve.d.ts +1 -1
- package/dist/crdt/placement/resolve.js +2 -1
- package/dist/crdt/protocol.d.ts +1 -1
- package/dist/crdt/protocol.js +1 -1
- package/dist/crdt/protocols/auth.d.ts +11 -1
- package/dist/crdt/protocols/auth.js +17 -1
- package/dist/crdt/protocols/awareness.js +46 -81
- package/dist/crdt/protocols/comments.d.ts +16 -1
- package/dist/crdt/protocols/comments.js +9 -1
- package/dist/crdt/providers/index.d.ts +12 -22
- package/dist/crdt/providers/index.js +3 -1
- package/dist/crdt/providers/indexeddb.d.ts +6 -10
- package/dist/crdt/providers/indexeddb.js +10 -16
- package/dist/crdt/providers/room.d.ts +11 -0
- package/dist/crdt/providers/room.js +26 -12
- package/dist/crdt/providers/websocket.d.ts +8 -12
- package/dist/crdt/providers/websocket.js +17 -19
- package/dist/crdt/purge.d.ts +7 -1
- package/dist/crdt/purge.js +32 -2
- package/dist/crdt/rangeDelete.d.ts +2 -47
- package/dist/crdt/rangeDelete.js +1 -169
- package/dist/crdt/restore.d.ts +2 -1
- package/dist/crdt/restore.js +9 -0
- package/dist/crdt/schema.d.ts +9 -1
- package/dist/crdt/schema.js +13 -3
- package/dist/crdt/structs.d.ts +2 -0
- package/dist/crdt/structs.js +2 -0
- package/dist/crdt/text/index/build.d.ts +125 -0
- package/dist/crdt/text/index/build.js +2895 -0
- package/dist/crdt/text/index/shared.d.ts +2 -6
- package/dist/crdt/text/index/shared.js +19 -19
- package/dist/crdt/text/model.d.ts +10 -0
- package/dist/crdt/text/model.js +10 -4
- package/dist/crdt/text/runs.d.ts +43 -42
- package/dist/crdt/text/runs.js +2 -165
- package/dist/crdt/vendor/yjs/UPSTREAM.md +83 -0
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +6 -3
- package/dist/crdt/vendor/yjs/dts/ynode.d.ts +13 -2
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/Item.js +44 -5
- package/dist/crdt/vendor/yjs/src/ynode.js +29 -7
- package/dist/edytor.svelte.d.ts +1 -1
- package/dist/edytor.svelte.js +100 -75
- package/dist/events/beforeInputCommands.d.ts +2 -2
- package/dist/events/domTextOffset.d.ts +2 -0
- package/dist/events/domTextOffset.js +8 -0
- package/dist/events/nativeInteractiveControl.d.ts +11 -1
- package/dist/events/nativeInteractiveControl.js +41 -1
- package/dist/events/onBeforeInput.js +3 -3
- package/dist/events/onFocus.js +9 -11
- package/dist/export/forms.d.ts +75 -75
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -1
- package/dist/labels.d.ts +8 -0
- package/dist/labels.js +6 -1
- package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
- package/dist/plugins/comments/CommentsController.svelte.js +14 -4
- package/dist/plugins/find/search.js +2 -2
- package/dist/plugins/index.d.ts +1 -0
- package/dist/plugins/index.js +1 -0
- package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
- package/dist/plugins/richtext/richTextOperations.js +26 -32
- package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
- package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
- package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
- package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
- package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
- package/dist/plugins/suggestions/shared.svelte.js +421 -0
- package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
- package/dist/selection/domSelection.d.ts +2 -0
- package/dist/selection/domSelection.js +6 -2
- package/dist/selection/pointer.svelte.js +75 -48
- package/dist/selection/replaceSelection.js +2 -6
- package/dist/selection/selection.svelte.d.ts +3 -3
- package/dist/selection/selection.svelte.js +39 -32
- package/dist/selection/selection.utils.d.ts +14 -0
- package/dist/selection/selection.utils.js +33 -11
- package/dist/session/commands.js +21 -11
- package/dist/session/composition.svelte.d.ts +5 -3
- package/dist/session/composition.svelte.js +21 -17
- package/dist/session/handles.d.ts +4 -17
- package/dist/session/handles.js +14 -44
- package/dist/session/navigation.js +6 -9
- package/dist/session/selection.d.ts +11 -5
- package/dist/session/selection.js +14 -48
- package/dist/session/suggestions.svelte.d.ts +38 -9
- package/dist/session/suggestions.svelte.js +37 -18
- package/dist/session/virtual.d.ts +1 -1
- package/dist/surface/attributes.d.ts +5 -0
- package/dist/surface/attributes.js +2 -2
- package/dist/surface/cells.d.ts +2 -26
- package/dist/surface/cells.js +2 -33
- package/dist/surface/observer.svelte.d.ts +6 -1
- package/dist/surface/observer.svelte.js +143 -111
- package/dist/surface/pin.svelte.d.ts +9 -9
- package/dist/surface/pin.svelte.js +15 -22
- package/dist/surface/projector.svelte.js +10 -16
- package/dist/text/text.svelte.js +14 -19
- package/dist/themes/notion.css +29 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +7 -0
- package/package.json +3 -1
- package/dist/crdt/doc/capability.d.ts +0 -56
- package/dist/crdt/doc/capability.js +0 -211
- package/dist/crdt/doc/content.d.ts +0 -16
- package/dist/crdt/doc/content.js +0 -77
- package/dist/crdt/doc/delete.d.ts +0 -15
- package/dist/crdt/doc/delete.js +0 -84
- package/dist/crdt/doc/layout.d.ts +0 -9
- package/dist/crdt/doc/layout.js +0 -178
- package/dist/crdt/doc/meta.d.ts +0 -19
- package/dist/crdt/doc/meta.js +0 -199
- package/dist/crdt/doc/moves.d.ts +0 -21
- package/dist/crdt/doc/moves.js +0 -274
- package/dist/crdt/doc/split-merge.d.ts +0 -10
- package/dist/crdt/doc/split-merge.js +0 -179
- package/dist/crdt/doc/steps.d.ts +0 -65
- package/dist/crdt/doc/steps.js +0 -311
- package/dist/crdt/text/index/cache.d.ts +0 -16
- package/dist/crdt/text/index/cache.js +0 -175
- package/dist/crdt/text/index/claims.d.ts +0 -18
- package/dist/crdt/text/index/claims.js +0 -225
- package/dist/crdt/text/index/fold.d.ts +0 -21
- package/dist/crdt/text/index/fold.js +0 -656
- package/dist/crdt/text/index/layout.d.ts +0 -24
- package/dist/crdt/text/index/layout.js +0 -271
- package/dist/crdt/text/index/placement.d.ts +0 -47
- package/dist/crdt/text/index/placement.js +0 -741
- package/dist/crdt/text/index/records.d.ts +0 -13
- package/dist/crdt/text/index/records.js +0 -139
- package/dist/crdt/text/index/report.d.ts +0 -18
- package/dist/crdt/text/index/report.js +0 -337
- package/dist/crdt/text/index/state.d.ts +0 -197
- package/dist/crdt/text/index/state.js +0 -116
- package/dist/crdt/text/index/streams.d.ts +0 -25
- package/dist/crdt/text/index/streams.js +0 -248
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import { ATTRIBUTION_ROOT, CONTENT_NODE, isNodeLike, SUGGEST_ROOT, TEXT_DELETES_ROOT } from '../schema.js';
|
|
2
|
+
import { blockNodeOf, registryOf } from '../placement/model.js';
|
|
3
|
+
import { isRowMark, newNode, openRangeCursor, ownTextIds, proposing } from '../text/model.js';
|
|
4
|
+
import { asYDoc, changedUnder, clientsOf, engineOps, firstItem, itemOf, storeOf } from '../structs.js';
|
|
5
|
+
import { nonceOf } from '../rand.js';
|
|
6
|
+
import { id as mint } from '../../utils.js';
|
|
7
|
+
import { effectOf, NOOP, refused } from './plan.js';
|
|
8
|
+
import { keepingReplaced } from '../incarnations.js';
|
|
9
|
+
import { ACTOR_PREFIX, CLIENT_PREFIX, profileOf, sanitizeProfile } from '../attribution/attribution.js';
|
|
10
|
+
import { bindBlockAttribution } from '../attribution/block.js';
|
|
11
|
+
/** What a suggestion refuses, unless on its author's own pending block (`sugg.refused`). */
|
|
12
|
+
const REFUSE = ['mergeBlocks', 'setBlockType', 'patchData', 'formatRange'];
|
|
13
|
+
/**
|
|
14
|
+
* What a decision does with a change of each kind, accepted and rejected:
|
|
15
|
+
* `1` settles its ids (it stays), `2` holds them (its text goes), `3`
|
|
16
|
+
* deletes its block, `4` holds its writer's text and joins its block to the
|
|
17
|
+
* one it was split from.
|
|
18
|
+
*/
|
|
19
|
+
const DECIDE = {
|
|
20
|
+
insert: [1, 2],
|
|
21
|
+
delete: [2, 1],
|
|
22
|
+
block: [1, 3],
|
|
23
|
+
split: [1, 4],
|
|
24
|
+
remove: [3, 1]
|
|
25
|
+
};
|
|
26
|
+
/** The list `map` keeps at `key` (made empty when absent). */
|
|
27
|
+
const listOf = (map, key) => map.get(key) ?? map.set(key, []).get(key);
|
|
28
|
+
/** Append unit `client:clock` to the id triples `ids`, extending the last one when it continues it. */
|
|
29
|
+
const push = (ids, client, clock) => {
|
|
30
|
+
const n = ids.length;
|
|
31
|
+
if (ids[n - 3] === client && ids[n - 2] + ids[n - 1] === clock)
|
|
32
|
+
ids[n - 1] = ids[n - 1] + 1;
|
|
33
|
+
else
|
|
34
|
+
ids.push(client, clock, 1);
|
|
35
|
+
};
|
|
36
|
+
const parts = new WeakMap();
|
|
37
|
+
/**
|
|
38
|
+
* The shared suggestions of the document `facade` is over, built once:
|
|
39
|
+
* from then on, its `toJSON()` leaves the pending ones out (`sugg.json.base`).
|
|
40
|
+
*/
|
|
41
|
+
export const suggestionsOf = (facade) => parts.get(facade.part) ?? parts.set(facade.part, docSuggest(facade)).get(facade.part);
|
|
42
|
+
/**
|
|
43
|
+
* Build the shared suggestions over `document`, a document read without a
|
|
44
|
+
* view (on a server, in a worker): its `toJSON()` leaves the pending ones
|
|
45
|
+
* out from then on, as a view's with the shared suggestions plugin does. A
|
|
46
|
+
* document no one built them over reads their content as plain content.
|
|
47
|
+
*/
|
|
48
|
+
export const adoptSharedSuggestions = (document) => void suggestionsOf(document.facade);
|
|
49
|
+
/** The document's shared suggestions ({@link suggestionsOf}), built over facade `f`. */
|
|
50
|
+
const docSuggest = (f) => {
|
|
51
|
+
const c = f.part;
|
|
52
|
+
const { Y, doc, runsView, view } = c;
|
|
53
|
+
const BA = bindBlockAttribution(Y);
|
|
54
|
+
const ops = engineOps(Y);
|
|
55
|
+
const { idSet } = ops;
|
|
56
|
+
const root = () => doc.get(SUGGEST_ROOT);
|
|
57
|
+
const actorOf = (client) => {
|
|
58
|
+
const bound = doc.get(ATTRIBUTION_ROOT).getAttr(`c/${client}`);
|
|
59
|
+
return typeof bound === 'string' ? bound : undefined;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Run `fn` in a transaction (`tr`) writing as `client` (a suggestion's
|
|
63
|
+
* writer), then as before: the transaction names the writer, so a
|
|
64
|
+
* history replays its step under it (`StackItem.writer`).
|
|
65
|
+
*/
|
|
66
|
+
const as = (client, fn) => f.transact(() => doc.transact((tr) => {
|
|
67
|
+
const local = doc.clientID;
|
|
68
|
+
if (client !== local)
|
|
69
|
+
tr.writer = client;
|
|
70
|
+
doc.clientID = client;
|
|
71
|
+
try {
|
|
72
|
+
return fn(tr);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
doc.clientID = local;
|
|
76
|
+
}
|
|
77
|
+
}));
|
|
78
|
+
/** The writers this replica minted: client ids it writes suggestions under besides its own. */
|
|
79
|
+
const writers = new Set();
|
|
80
|
+
/** Bumped by every change of the records (a commit, or a write of this part); the facade's `version` too. */
|
|
81
|
+
let version = 0;
|
|
82
|
+
const bump = () => {
|
|
83
|
+
version++;
|
|
84
|
+
c.v++;
|
|
85
|
+
};
|
|
86
|
+
let table = null;
|
|
87
|
+
const records = () => {
|
|
88
|
+
if (table?.v === version)
|
|
89
|
+
return table.t;
|
|
90
|
+
const t = { recs: [], byWriter: new Map(), proposing: [] };
|
|
91
|
+
const r = root();
|
|
92
|
+
for (const id of r.attrKeys()) {
|
|
93
|
+
const node = r.getAttr(id);
|
|
94
|
+
const item = isNodeLike(node) ? itemOf(node) : null;
|
|
95
|
+
if (!item)
|
|
96
|
+
continue;
|
|
97
|
+
const writer = item.id.client;
|
|
98
|
+
const attr = (key) => {
|
|
99
|
+
const v = node.getAttr(key);
|
|
100
|
+
return typeof v === 'string' ? v : undefined;
|
|
101
|
+
};
|
|
102
|
+
const rec = {
|
|
103
|
+
id,
|
|
104
|
+
node,
|
|
105
|
+
writer,
|
|
106
|
+
get by() {
|
|
107
|
+
return actorOf(writer);
|
|
108
|
+
},
|
|
109
|
+
via: attr('via'),
|
|
110
|
+
label: attr('lbl'),
|
|
111
|
+
at: Number(node.getAttr('at')) || 0,
|
|
112
|
+
start: item.id.clock,
|
|
113
|
+
settled: idSet(),
|
|
114
|
+
proposed: idSet()
|
|
115
|
+
};
|
|
116
|
+
for (let it = firstItem(node); it !== null; it = it.right) {
|
|
117
|
+
const ids = it.content.ids;
|
|
118
|
+
if (!it.deleted && ids)
|
|
119
|
+
for (let i = 0; i < ids.length; i += 3)
|
|
120
|
+
(it.id.client === writer ? rec.proposed : rec.settled).add(ids[i], ids[i + 1], ids[i + 2]);
|
|
121
|
+
}
|
|
122
|
+
if (rec.proposed.clients.size)
|
|
123
|
+
t.proposing.push(rec);
|
|
124
|
+
t.recs.push(rec);
|
|
125
|
+
listOf(t.byWriter, writer).push(rec);
|
|
126
|
+
}
|
|
127
|
+
for (const recs of t.byWriter.values())
|
|
128
|
+
recs.sort((a, b) => a.start - b.start);
|
|
129
|
+
t.recs.sort((a, b) => a.at - b.at || a.start - b.start);
|
|
130
|
+
table = { v: version, t };
|
|
131
|
+
return t;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* The record whose clocks hold `client:clock` (its writer's latest opened
|
|
135
|
+
* before it), when it is pending there (not settled).
|
|
136
|
+
*/
|
|
137
|
+
const pendingIn = (client, clock) => {
|
|
138
|
+
const rec = records()
|
|
139
|
+
.byWriter.get(client)
|
|
140
|
+
?.findLast((r) => r.start <= clock);
|
|
141
|
+
return rec && !rec.settled.has(client, clock) ? rec : undefined;
|
|
142
|
+
};
|
|
143
|
+
/** The records proposing to delete unit `client:clock` (not settled there). */
|
|
144
|
+
const proposersOf = (client, clock) => records().proposing.filter((r) => r.proposed.has(client, clock) && !r.settled.has(client, clock));
|
|
145
|
+
/** The id of block `b`'s node: what its creation and a proposal of its deletion name. */
|
|
146
|
+
const bornOf = (b) => {
|
|
147
|
+
const node = b === undefined ? null : blockNodeOf(doc, b);
|
|
148
|
+
return node && itemOf(node).id;
|
|
149
|
+
};
|
|
150
|
+
// ── what is pending ─────────────────────────────────────────────────
|
|
151
|
+
/** The work done (`sugg.scale`): blocks read, units walked. */
|
|
152
|
+
const debug = { computes: 0, units: 0 };
|
|
153
|
+
/** The shown units of `b`, in display order (row marks skipped, as the reads skip them). */
|
|
154
|
+
const unitsOf = (b) => {
|
|
155
|
+
const out = [];
|
|
156
|
+
debug.computes++;
|
|
157
|
+
for (const seg of view().own.display(b) ?? [])
|
|
158
|
+
for (const piece of openRangeCursor(seg.text).read(seg.i0, seg.i1)) {
|
|
159
|
+
const { str } = piece.content;
|
|
160
|
+
const arr = str === undefined ? piece.content.getContent?.() : null;
|
|
161
|
+
for (let j = 0; !piece.deleted && j < piece.len; j++)
|
|
162
|
+
if (!arr || !isRowMark(arr[piece.offset + j]))
|
|
163
|
+
out.push({
|
|
164
|
+
client: piece.id.client,
|
|
165
|
+
clock: piece.id.clock + j,
|
|
166
|
+
offset: out.length,
|
|
167
|
+
char: str?.[piece.offset + j] ?? ''
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
debug.units += out.length;
|
|
171
|
+
return out;
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* The block a split-born `b` was split from: the one showing the nearest
|
|
175
|
+
* stream before `b`'s in its text (`null`: none
|
|
176
|
+
* is shown; `undefined`: `b` was not split).
|
|
177
|
+
*/
|
|
178
|
+
const sourceOf = (b) => {
|
|
179
|
+
const { own } = view();
|
|
180
|
+
const s = own.streamOf(b);
|
|
181
|
+
if (s === undefined || s.home === b)
|
|
182
|
+
return undefined;
|
|
183
|
+
const streams = own.streamsIn(s.home);
|
|
184
|
+
for (let i = streams.findIndex((x) => x.block === b) - 1; i >= 0; i--) {
|
|
185
|
+
const owner = own.ownerOf(streams[i].block);
|
|
186
|
+
// Not itself: a block merged into it shows its stream there.
|
|
187
|
+
if (typeof owner === 'string' && owner !== b && c.live(owner))
|
|
188
|
+
return owner;
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
};
|
|
192
|
+
/** `b`'s pending changes, read from the state now (`null`: none). */
|
|
193
|
+
const compute = (b) => {
|
|
194
|
+
const units = unitsOf(b);
|
|
195
|
+
const born = bornOf(b);
|
|
196
|
+
// A split whose source is gone is no longer one: its writer's text is an insertion.
|
|
197
|
+
const split = sourceOf(b);
|
|
198
|
+
const maker = born && split !== null ? pendingIn(born.client, born.clock) : undefined;
|
|
199
|
+
const out = [];
|
|
200
|
+
const runs = new Map();
|
|
201
|
+
/** Its maker's units: shown as the block's change, in one piece. */
|
|
202
|
+
const made = [];
|
|
203
|
+
const add = (rec, kind, u) => {
|
|
204
|
+
const run = runs.get(kind + rec.id);
|
|
205
|
+
if (run && run.offset + run.length === u.offset) {
|
|
206
|
+
run.length++;
|
|
207
|
+
run.text += u.char;
|
|
208
|
+
return push(run.ids, u.client, u.clock);
|
|
209
|
+
}
|
|
210
|
+
if (run)
|
|
211
|
+
out.push(run);
|
|
212
|
+
runs.set(kind + rec.id, {
|
|
213
|
+
suggestion: rec.id,
|
|
214
|
+
kind,
|
|
215
|
+
block: b,
|
|
216
|
+
offset: u.offset,
|
|
217
|
+
length: 1,
|
|
218
|
+
text: u.char,
|
|
219
|
+
ids: [u.client, u.clock, 1]
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
for (const u of units) {
|
|
223
|
+
const rec = pendingIn(u.client, u.clock);
|
|
224
|
+
if (rec && rec === maker)
|
|
225
|
+
push(made, u.client, u.clock);
|
|
226
|
+
else if (rec)
|
|
227
|
+
add(rec, 'insert', u);
|
|
228
|
+
for (const r of proposersOf(u.client, u.clock))
|
|
229
|
+
add(r, 'delete', u);
|
|
230
|
+
}
|
|
231
|
+
out.push(...runs.values());
|
|
232
|
+
out.sort((x, y) => x.offset - y.offset);
|
|
233
|
+
const whole = (rec, kind, ids) => ({
|
|
234
|
+
suggestion: rec.id,
|
|
235
|
+
kind,
|
|
236
|
+
block: b,
|
|
237
|
+
offset: 0,
|
|
238
|
+
length: units.length,
|
|
239
|
+
text: units.map((u) => u.char).join(''),
|
|
240
|
+
ids: [born.client, born.clock, 1, ...ids]
|
|
241
|
+
});
|
|
242
|
+
if (maker)
|
|
243
|
+
out.unshift(whole(maker, split ? 'split' : 'block', made));
|
|
244
|
+
// A record whose id list names the block's node proposes deleting the block.
|
|
245
|
+
for (const r of born ? proposersOf(born.client, born.clock) : [])
|
|
246
|
+
out.push(whole(r, 'remove', []));
|
|
247
|
+
return out.length ? out : null;
|
|
248
|
+
};
|
|
249
|
+
/** Every block the records' writers and proposals are in (a proposal naming a block's node included). */
|
|
250
|
+
const candidates = () => {
|
|
251
|
+
const t = records();
|
|
252
|
+
const out = new Set();
|
|
253
|
+
const texts = new Set();
|
|
254
|
+
const clients = clientsOf(doc);
|
|
255
|
+
/** Note where the structs of `client` from `from` to `to` (exclusive) are. */
|
|
256
|
+
const each = (client, from, to) => {
|
|
257
|
+
const structs = clients.get(client) ?? [];
|
|
258
|
+
// Past what this replica holds: nothing.
|
|
259
|
+
if (from >= storeOf(doc).getClock(client))
|
|
260
|
+
return;
|
|
261
|
+
for (let k = Y.findIndexSS(structs, from); k < structs.length && structs[k].id.clock < to; k++) {
|
|
262
|
+
const st = structs[k];
|
|
263
|
+
const parent = st.parent;
|
|
264
|
+
if (st.deleted || !isNodeLike(parent))
|
|
265
|
+
continue;
|
|
266
|
+
if (parent.name === CONTENT_NODE)
|
|
267
|
+
texts.add(parent);
|
|
268
|
+
else if (st.parentSub && parent === registryOf(doc))
|
|
269
|
+
out.add(st.parentSub);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
for (const [writer, recs] of t.byWriter)
|
|
273
|
+
each(writer, recs[0].start, Infinity);
|
|
274
|
+
for (const rec of t.proposing)
|
|
275
|
+
for (const [client, ranges] of rec.proposed.clients)
|
|
276
|
+
for (const { clock, len } of ranges.getIds())
|
|
277
|
+
each(client, clock, clock + len);
|
|
278
|
+
const { own } = view();
|
|
279
|
+
for (const text of texts) {
|
|
280
|
+
const home = text._item?.parent?._item?.parentSub;
|
|
281
|
+
for (const b of home ? [home, ...own.streamsIn(home).map((s) => s.block)] : []) {
|
|
282
|
+
const owner = own.ownerOf(b);
|
|
283
|
+
if (typeof owner === 'string')
|
|
284
|
+
out.add(owner);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
};
|
|
289
|
+
/** The pending changes per block, read again after any write. */
|
|
290
|
+
let memo = null;
|
|
291
|
+
const pending = () => {
|
|
292
|
+
const v = `${version}.${runsView.version()}`;
|
|
293
|
+
if (memo?.v === v)
|
|
294
|
+
return memo.map;
|
|
295
|
+
const map = new Map();
|
|
296
|
+
for (const b of records().recs.length ? candidates() : []) {
|
|
297
|
+
const p = c.live(b) ? compute(b) : null;
|
|
298
|
+
if (p)
|
|
299
|
+
map.set(b, p);
|
|
300
|
+
}
|
|
301
|
+
memo = { v, map };
|
|
302
|
+
return map;
|
|
303
|
+
};
|
|
304
|
+
const listeners = new Set();
|
|
305
|
+
const onCommit = (tr) => {
|
|
306
|
+
if (!changedUnder(tr, [root()]))
|
|
307
|
+
return;
|
|
308
|
+
bump();
|
|
309
|
+
for (const f of listeners)
|
|
310
|
+
f();
|
|
311
|
+
};
|
|
312
|
+
doc.on('commit', onCommit);
|
|
313
|
+
// ── writes ──────────────────────────────────────────────────────────
|
|
314
|
+
/** A fresh writer this replica writes suggestions under. */
|
|
315
|
+
const writer = () => {
|
|
316
|
+
let w = 0;
|
|
317
|
+
while (!w || w === doc.clientID || storeOf(doc).getClock(w))
|
|
318
|
+
w = (nonceOf(doc) | 0x4000000) >>> 0;
|
|
319
|
+
writers.add(w);
|
|
320
|
+
return w;
|
|
321
|
+
};
|
|
322
|
+
/**
|
|
323
|
+
* Bind writer `w` to this replica's actor, written by `w` itself, once
|
|
324
|
+
* (`sugg.writer.binding`), and the actor's profile when the stored one
|
|
325
|
+
* differs: inside the writer's first transaction.
|
|
326
|
+
*/
|
|
327
|
+
const bind = (w) => {
|
|
328
|
+
const actor = c.config.actor?.();
|
|
329
|
+
const attr = doc.get(ATTRIBUTION_ROOT);
|
|
330
|
+
if (!actor || !writers.has(w) || attr.getAttr(CLIENT_PREFIX + w) !== undefined)
|
|
331
|
+
return;
|
|
332
|
+
attr.setAttr(CLIENT_PREFIX + w, actor.id);
|
|
333
|
+
const next = profileOf(actor);
|
|
334
|
+
const stored = sanitizeProfile(attr.getAttr(ACTOR_PREFIX + actor.id));
|
|
335
|
+
if (stored.name !== next.name || stored.color !== next.color)
|
|
336
|
+
attr.setAttr(ACTOR_PREFIX + actor.id, next);
|
|
337
|
+
};
|
|
338
|
+
/**
|
|
339
|
+
* Open a suggestion written by `writer` (a fresh writer by default): its
|
|
340
|
+
* record, in no history step. Answers its id.
|
|
341
|
+
*/
|
|
342
|
+
const open = (options = {}) => f.transact(() => {
|
|
343
|
+
const w = options.writer ?? writer();
|
|
344
|
+
const id = options.id ?? mint('s');
|
|
345
|
+
const from = storeOf(doc).getClock(w);
|
|
346
|
+
as(w, () => {
|
|
347
|
+
bind(w);
|
|
348
|
+
root().setAttr(id, newNode('suggestion'));
|
|
349
|
+
const rec = root().getAttr(id);
|
|
350
|
+
rec.setAttr('at', options.at ?? Date.now());
|
|
351
|
+
if (options.via)
|
|
352
|
+
rec.setAttr('via', options.via);
|
|
353
|
+
if (options.label)
|
|
354
|
+
rec.setAttr('lbl', options.label);
|
|
355
|
+
});
|
|
356
|
+
// No history step captures the record's creation: its clocks stay its own.
|
|
357
|
+
const own = ownTextIds.get(doc) ?? ownTextIds.set(doc, idSet()).get(doc);
|
|
358
|
+
own.add(w, from, storeOf(doc).getClock(w) - from);
|
|
359
|
+
bump();
|
|
360
|
+
return id;
|
|
361
|
+
});
|
|
362
|
+
const recOf = (id) => records().recs.find((r) => r.id === id);
|
|
363
|
+
/** Two writers of one author (the same writer, or bound to the same actor). */
|
|
364
|
+
const sameAuthor = (a, b) => a === b || (!!actorOf(a) && actorOf(a) === actorOf(b));
|
|
365
|
+
/** `client:clock` is a pending insertion of `w`'s author: its edits are direct. */
|
|
366
|
+
const ownPending = (client, clock, w) => !!pendingIn(client, clock) && sameAuthor(client, w);
|
|
367
|
+
/** Block `b` is a pending insertion of `w`'s author. */
|
|
368
|
+
const ownBlock = (b, w) => {
|
|
369
|
+
const id = bornOf(b);
|
|
370
|
+
return !!id && ownPending(id.client, id.clock, w);
|
|
371
|
+
};
|
|
372
|
+
/** Append id list `ids` (YP31) to record `rec`'s list, as the current client. */
|
|
373
|
+
const entry = (rec, ids) => {
|
|
374
|
+
rec.node.hold(ids);
|
|
375
|
+
bump();
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* The block each step of plan `p` is about: a merge's, the block it
|
|
379
|
+
* merges away; a retype of a child a block deletion of `p` promotes, the
|
|
380
|
+
* deleted block (it follows that deletion: written with it, or read again
|
|
381
|
+
* when the deletion is accepted).
|
|
382
|
+
*/
|
|
383
|
+
const about = (p) => {
|
|
384
|
+
const removed = new Set(p.writes.flatMap((s) => (s.op === 'deleteBlock' ? s.removes : [])));
|
|
385
|
+
return (s) => {
|
|
386
|
+
const up = s.op === 'setBlockType' ? c.positionOf(s.id)?.parent : null;
|
|
387
|
+
return up && removed.has(up) ? up : 'from' in s ? s.from : 'id' in s ? s.id : undefined;
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
/**
|
|
391
|
+
* Write plan `p` as suggestion `id` (a new one of writer `w` when `null`
|
|
392
|
+
* or gone: the view's suggesting mode), by the suggestion's writer, in one frame:
|
|
393
|
+
* insertions are written; a text delete proposes the units that are not
|
|
394
|
+
* its author's pending ones (those go), the engine hiding them until the
|
|
395
|
+
* plan ends so its later steps read the offsets they were prepared with
|
|
396
|
+
* (fork patch YP32: an insertion at a proposed deletion lands after it);
|
|
397
|
+
* a block delete proposes the blocks it removes by their nodes (its
|
|
398
|
+
* author's own go). A plan that moves blocks, or merges, retypes,
|
|
399
|
+
* changes data or formats what is not its author's own pending block, is
|
|
400
|
+
* refused, nothing written (`sugg.refused`). Answers the result, the
|
|
401
|
+
* suggestion and where the plan's caret is now, the proposed text shown.
|
|
402
|
+
*/
|
|
403
|
+
const apply = (id, p, w = recOf(id).writer) => {
|
|
404
|
+
if (!('writes' in p))
|
|
405
|
+
return { result: p };
|
|
406
|
+
const at = about(p);
|
|
407
|
+
const [writes, blocks] = [[], []];
|
|
408
|
+
for (const s of p.writes)
|
|
409
|
+
if (s.op === 'moveBlocks')
|
|
410
|
+
return { result: refused('suggesting') };
|
|
411
|
+
else if (ownBlock(at(s), w) || (!REFUSE.includes(s.op) && s.op !== 'deleteBlock'))
|
|
412
|
+
writes.push(s);
|
|
413
|
+
else if (s.op === 'deleteBlock')
|
|
414
|
+
for (const r of s.removes) {
|
|
415
|
+
const { client, clock } = bornOf(r);
|
|
416
|
+
blocks.push(client, clock, 1);
|
|
417
|
+
}
|
|
418
|
+
// A promoted child's retype follows its deletion: read again when it is accepted.
|
|
419
|
+
else if (s.op !== 'setBlockType' || at(s) === s.id)
|
|
420
|
+
return { result: refused('suggesting') };
|
|
421
|
+
const rec = (id !== null && recOf(id)) || recOf(open({ writer: w }));
|
|
422
|
+
return as(w, (tr) => {
|
|
423
|
+
let result = NOOP;
|
|
424
|
+
let anchor;
|
|
425
|
+
proposing.set(doc, {
|
|
426
|
+
into: rec.node,
|
|
427
|
+
sort: (client, clock) => ownPending(client, clock, w)
|
|
428
|
+
? 1
|
|
429
|
+
: rec.proposed.has(client, clock) && !rec.settled.has(client, clock)
|
|
430
|
+
? 3
|
|
431
|
+
: 2
|
|
432
|
+
});
|
|
433
|
+
try {
|
|
434
|
+
if (writes.length)
|
|
435
|
+
result = f.apply({ ...p, writes, effect: effectOf(writes), version: runsView.version() }, true);
|
|
436
|
+
anchor = p.at ? f.anchorAt(p.at.block, p.at.offset, 'right') : null;
|
|
437
|
+
}
|
|
438
|
+
finally {
|
|
439
|
+
proposing.delete(doc);
|
|
440
|
+
ops.unhide(tr);
|
|
441
|
+
}
|
|
442
|
+
if (blocks.length) {
|
|
443
|
+
entry(rec, blocks);
|
|
444
|
+
result = { status: 'applied', ids: p.ids };
|
|
445
|
+
}
|
|
446
|
+
bump();
|
|
447
|
+
const caret = anchor && f.resolveAnchor(anchor);
|
|
448
|
+
return {
|
|
449
|
+
result,
|
|
450
|
+
id: rec.id,
|
|
451
|
+
...(caret && { at: { block: caret.blockId, offset: caret.offset } })
|
|
452
|
+
};
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
/**
|
|
456
|
+
* Write decision part `part` (an id list) by the current client: settled
|
|
457
|
+
* in its record, or held, the text that goes: its blocks stamped as
|
|
458
|
+
* changed by the decider, as a text delete stamps them.
|
|
459
|
+
*/
|
|
460
|
+
const hold = ({ into, ids, blocks = [] }) => f.transact(() => {
|
|
461
|
+
if (into !== null)
|
|
462
|
+
return entry(recOf(into), ids);
|
|
463
|
+
doc.get(TEXT_DELETES_ROOT).hold(ids);
|
|
464
|
+
const actor = c.config.actor?.();
|
|
465
|
+
for (const b of actor ? blocks : []) {
|
|
466
|
+
const node = blockNodeOf(doc, b);
|
|
467
|
+
if (node)
|
|
468
|
+
BA.stampChange(doc, node, b, actor.id);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
/**
|
|
472
|
+
* What accepting (`accept`) or rejecting `changes` writes, in the order
|
|
473
|
+
* it runs ({@link DECIDE}): the id lists first (what stays settled in its
|
|
474
|
+
* record, the text that goes held by the decider), then the merges, then
|
|
475
|
+
* the block deletes, each reading its arguments when it runs.
|
|
476
|
+
*/
|
|
477
|
+
const decide = (changes, accept) => {
|
|
478
|
+
const lists = new Map();
|
|
479
|
+
const gone = new Set();
|
|
480
|
+
const [merges, deletes] = [[], []];
|
|
481
|
+
for (const { kind, block: b, suggestion, ids } of changes) {
|
|
482
|
+
const act = DECIDE[kind][accept ? 0 : 1];
|
|
483
|
+
if (act === 2)
|
|
484
|
+
gone.add(b);
|
|
485
|
+
if (act === 3)
|
|
486
|
+
deletes.push({ op: 'deleteBlock', calls: () => (c.live(b) ? [[b]] : []) });
|
|
487
|
+
else
|
|
488
|
+
listOf(lists, act === 1 ? suggestion : null).push(...(act === 4 ? ids.slice(3) : ids));
|
|
489
|
+
if (act === 4)
|
|
490
|
+
merges.push({
|
|
491
|
+
op: 'mergeBlocks',
|
|
492
|
+
calls: () => {
|
|
493
|
+
const into = sourceOf(b);
|
|
494
|
+
return into && c.live(b) ? [[b, into]] : [];
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
return [
|
|
499
|
+
...[...lists].map(([into, ids]) => ({
|
|
500
|
+
into,
|
|
501
|
+
ids,
|
|
502
|
+
...(into === null && { blocks: [...gone] })
|
|
503
|
+
})),
|
|
504
|
+
...merges,
|
|
505
|
+
...deletes
|
|
506
|
+
];
|
|
507
|
+
};
|
|
508
|
+
/**
|
|
509
|
+
* Write decision `parts` by client `w` (the current one by default; its
|
|
510
|
+
* author's rejection, by a writer of theirs), in one transaction: the id
|
|
511
|
+
* lists, then each operation through `exec` (the bare document's own
|
|
512
|
+
* operation by default; a view passes its dispatched one).
|
|
513
|
+
*/
|
|
514
|
+
const run = (parts, exec = (op, args) => f[op](...args), w = doc.clientID) => as(w, () => {
|
|
515
|
+
bind(w);
|
|
516
|
+
for (const part of parts)
|
|
517
|
+
if ('op' in part)
|
|
518
|
+
for (const args of part.calls())
|
|
519
|
+
exec(part.op, args);
|
|
520
|
+
else
|
|
521
|
+
hold(part);
|
|
522
|
+
});
|
|
523
|
+
/** Every pending change, in document order. */
|
|
524
|
+
const changes = () => {
|
|
525
|
+
const map = pending();
|
|
526
|
+
return [...map.keys()].sort(c.compare).flatMap((b) => map.get(b));
|
|
527
|
+
};
|
|
528
|
+
/**
|
|
529
|
+
* The document without its pending suggestions (`sugg.json.base`): on a
|
|
530
|
+
* copy, every pending one rejected; `undefined` while none is pending.
|
|
531
|
+
*/
|
|
532
|
+
const base = () => {
|
|
533
|
+
if (root().attrKeys().next().done || !changes().length)
|
|
534
|
+
return;
|
|
535
|
+
const { Y } = c;
|
|
536
|
+
const copy = keepingReplaced(new Y.Doc());
|
|
537
|
+
Y.applyUpdate(copy, Y.encodeStateAsUpdate(asYDoc(doc)));
|
|
538
|
+
const other = c.create(copy, { ...c.config, actor: undefined });
|
|
539
|
+
const all = suggestionsOf(other);
|
|
540
|
+
all.run(all.decide(all.changes(), false));
|
|
541
|
+
const json = other.toJSON({ suggestions: true });
|
|
542
|
+
all.dispose();
|
|
543
|
+
other.dispose();
|
|
544
|
+
copy.destroy();
|
|
545
|
+
return json;
|
|
546
|
+
};
|
|
547
|
+
c.base = base;
|
|
548
|
+
return {
|
|
549
|
+
/** A fresh writer this replica writes suggestions under. */
|
|
550
|
+
writer,
|
|
551
|
+
/** The writers this replica minted (a history tags their steps, `StackItem.writer`). */
|
|
552
|
+
writers,
|
|
553
|
+
open,
|
|
554
|
+
apply,
|
|
555
|
+
decide,
|
|
556
|
+
run,
|
|
557
|
+
/** The suggestions the document holds, oldest first. */
|
|
558
|
+
records: () => records().recs,
|
|
559
|
+
/** Whether the document holds any suggestion record. */
|
|
560
|
+
any: () => !root().attrKeys().next().done,
|
|
561
|
+
/** Every pending change, in document order. */
|
|
562
|
+
changes,
|
|
563
|
+
/** The pending changes of block `b` (`[]`: none). */
|
|
564
|
+
at: (b) => pending().get(b) ?? [],
|
|
565
|
+
/** Bumped by every change of the records. */
|
|
566
|
+
version: () => version,
|
|
567
|
+
/** Be told of every change of the records. Returns the unsubscribe. */
|
|
568
|
+
onChange: (f) => {
|
|
569
|
+
listeners.add(f);
|
|
570
|
+
return () => void listeners.delete(f);
|
|
571
|
+
},
|
|
572
|
+
/** @internal The work done (blocks read, units walked): the scaling rows count it. */
|
|
573
|
+
debug,
|
|
574
|
+
dispose: () => {
|
|
575
|
+
doc.off('commit', onCommit);
|
|
576
|
+
listeners.clear();
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
};
|
package/dist/crdt/document.d.ts
CHANGED
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
*/
|
|
142
142
|
import type { MarkEdge } from './text/marks.js';
|
|
143
143
|
import type { EngineApi, EngineDoc, YDoc, YUndoManager } from './engine-api.js';
|
|
144
|
-
import {
|
|
144
|
+
import type { DocumentOperations } from './operations.js';
|
|
145
145
|
import { type AttributionBinding, type DocumentAttribution } from './attribution/index.js';
|
|
146
146
|
import { DocumentDestroyedError, type BlockRole, type DocFacade, type EdytorDocConfig } from './edytor-doc.js';
|
|
147
147
|
import { Awareness } from './protocols/awareness.js';
|
|
@@ -277,6 +277,8 @@ export type DocumentReadiness = 'pending' | 'local' | 'hydrated';
|
|
|
277
277
|
* does: from the socket's open, so a slow dial never counts).
|
|
278
278
|
*/
|
|
279
279
|
export declare const DEFAULT_READINESS_BOUND = 1000;
|
|
280
|
+
/** What a document's user may do in its room (`EdytorDocument.access`). */
|
|
281
|
+
export type DocumentAccess = 'write' | 'suggest' | 'comment' | 'read';
|
|
280
282
|
/** Internal constructor bag — use {@link createDocument}/{@link loadDocument}/{@link attachDocument}. */
|
|
281
283
|
export type EdytorDocumentInit = {
|
|
282
284
|
doc: YDoc;
|
|
@@ -368,6 +370,13 @@ export declare class EdytorDocument {
|
|
|
368
370
|
*/
|
|
369
371
|
get readiness(): DocumentReadiness;
|
|
370
372
|
get destroyed(): boolean;
|
|
373
|
+
/**
|
|
374
|
+
* What this document's user may do, as its room said as it joined
|
|
375
|
+
* (`'write'` until a room says otherwise): `'suggest'` writes only
|
|
376
|
+
* suggestions, `'comment'` only comments, `'read'` nothing. The plugins
|
|
377
|
+
* read it (the shared suggestions' suggesting mode, the comments).
|
|
378
|
+
*/
|
|
379
|
+
access: DocumentAccess;
|
|
371
380
|
/**
|
|
372
381
|
* `false` while the document carries a schema stamp this build cannot
|
|
373
382
|
* own (a foreign stamp got in despite the transport's inbound refusal):
|
package/dist/crdt/document.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { DEV } from 'esm-env';
|
|
2
2
|
import { Y } from './engine.js';
|
|
3
3
|
import { asEngineDoc } from './structs.js';
|
|
4
|
-
import { DOCUMENT_OPERATIONS } from './operations.js';
|
|
5
4
|
import { propsProxy } from './props.js';
|
|
6
5
|
import { bindAttribution } from './attribution/index.js';
|
|
7
6
|
import { profileOf } from './attribution/attribution.js';
|
|
@@ -192,13 +191,15 @@ export class EdytorDocument {
|
|
|
192
191
|
// with the `SchemaMismatchError` naming the stamp.
|
|
193
192
|
assertWritable: () => assertSchema(asEngineDoc(this.doc), 'document')
|
|
194
193
|
});
|
|
195
|
-
// The document's own operations (`
|
|
196
|
-
// facade's, read through so each
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
194
|
+
// The document's own operations (the facade's `api`, which satisfies
|
|
195
|
+
// `DocumentOperations`): the bound facade's, read through so each
|
|
196
|
+
// call reaches the live binding.
|
|
197
|
+
for (const name in this.facade.api)
|
|
198
|
+
if (name !== 'transact')
|
|
199
|
+
Object.defineProperty(this, name, {
|
|
200
|
+
get: () => this.facade[name],
|
|
201
|
+
configurable: true
|
|
202
|
+
});
|
|
202
203
|
this.awareness = init.awareness ?? new init.awarenessCtor(this.doc);
|
|
203
204
|
this.#ownsAwareness = init.awareness === undefined;
|
|
204
205
|
this.#historyOptions = init.history;
|
|
@@ -261,6 +262,13 @@ export class EdytorDocument {
|
|
|
261
262
|
get destroyed() {
|
|
262
263
|
return this._destroyed;
|
|
263
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* What this document's user may do, as its room said as it joined
|
|
267
|
+
* (`'write'` until a room says otherwise): `'suggest'` writes only
|
|
268
|
+
* suggestions, `'comment'` only comments, `'read'` nothing. The plugins
|
|
269
|
+
* read it (the shared suggestions' suggesting mode, the comments).
|
|
270
|
+
*/
|
|
271
|
+
access = 'write';
|
|
264
272
|
/**
|
|
265
273
|
* `false` while the document carries a schema stamp this build cannot
|
|
266
274
|
* own (a foreign stamp got in despite the transport's inbound refusal):
|
|
@@ -360,8 +368,7 @@ export class EdytorDocument {
|
|
|
360
368
|
// promoted out of a newly island kind displays as a default child; a
|
|
361
369
|
// newly layout kind displays only its items (`layout.*`), a newly
|
|
362
370
|
// table kind only its rows (`table.*`).
|
|
363
|
-
|
|
364
|
-
this.facade.rolesChanged();
|
|
371
|
+
this.facade.rolesChanged(Object.values(config.roles ?? {}).some((role) => role?.void || role?.island || role?.layout || role?.table));
|
|
365
372
|
};
|
|
366
373
|
/**
|
|
367
374
|
* The readiness transition — decide the document's content state. This
|
|
@@ -680,7 +687,8 @@ export class EdytorDocument {
|
|
|
680
687
|
bound = setTimeout(decide, DEFAULT_READINESS_BOUND);
|
|
681
688
|
},
|
|
682
689
|
holdBound: () => clearTimeout(bound),
|
|
683
|
-
attach: (companion) => this.attachSync(companion, opts)
|
|
690
|
+
attach: (companion) => this.attachSync(companion, opts),
|
|
691
|
+
access: (access) => void (this.access = access)
|
|
684
692
|
});
|
|
685
693
|
}
|
|
686
694
|
catch (error) {
|