edytor 3.0.0 → 5.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 +23 -12
- package/dist/cloudflare/DocumentRoom.js +10 -6
- 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 +139 -129
- package/dist/cloudflare/room/context.js +11 -4
- 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.d.ts +6 -3
- package/dist/cloudflare/room/purge.js +45 -19
- 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/room/updates.d.ts +3 -4
- package/dist/cloudflare/room/updates.js +6 -8
- 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 +34 -12
- package/dist/crdt/attribution/attribution.js +59 -21
- package/dist/crdt/attribution/block.d.ts +3 -3
- package/dist/crdt/attribution/block.js +7 -8
- package/dist/crdt/attribution/index.d.ts +1 -1
- package/dist/crdt/attribution/index.js +1 -1
- package/dist/crdt/data.d.ts +62 -31
- package/dist/crdt/data.js +211 -103
- package/dist/crdt/doc/capability.js +9 -13
- package/dist/crdt/doc/delete.d.ts +1 -1
- package/dist/crdt/doc/delete.js +13 -30
- package/dist/crdt/doc/funnel.d.ts +1 -1
- package/dist/crdt/doc/funnel.js +34 -26
- package/dist/crdt/doc/gate.d.ts +3 -3
- package/dist/crdt/doc/history.d.ts +31 -9
- package/dist/crdt/doc/history.js +204 -73
- package/dist/crdt/doc/layout.js +16 -27
- package/dist/crdt/doc/meta.js +17 -23
- package/dist/crdt/doc/moves.d.ts +1 -1
- package/dist/crdt/doc/moves.js +52 -32
- package/dist/crdt/doc/reads.d.ts +2 -2
- package/dist/crdt/doc/reads.js +20 -30
- package/dist/crdt/doc/seed.js +2 -2
- package/dist/crdt/doc/split-merge.js +12 -15
- package/dist/crdt/doc/steps.d.ts +25 -13
- package/dist/crdt/doc/steps.js +55 -125
- package/dist/crdt/doc/suggest.d.ts +167 -0
- package/dist/crdt/doc/suggest.js +579 -0
- package/dist/crdt/doc/types.d.ts +21 -9
- package/dist/crdt/document.d.ts +26 -6
- package/dist/crdt/document.js +40 -15
- package/dist/crdt/edytor-doc.d.ts +410 -390
- package/dist/crdt/edytor-doc.js +143 -95
- package/dist/crdt/engine-api.d.ts +39 -1
- package/dist/crdt/engine.d.ts +2 -2
- package/dist/crdt/engine.js +4 -5
- package/dist/crdt/flow.d.ts +2 -2
- package/dist/crdt/flow.js +29 -37
- package/dist/crdt/index.d.ts +134 -127
- package/dist/crdt/index.js +4 -1
- package/dist/crdt/operations.d.ts +12 -3
- package/dist/crdt/parts.d.ts +43 -0
- package/dist/crdt/parts.js +44 -0
- package/dist/crdt/placement/display.d.ts +6 -31
- package/dist/crdt/placement/display.js +41 -146
- package/dist/crdt/placement/keys.d.ts +63 -0
- package/dist/crdt/placement/keys.js +95 -0
- package/dist/crdt/placement/model.d.ts +119 -118
- package/dist/crdt/placement/model.js +242 -142
- package/dist/crdt/placement/resolve.d.ts +123 -35
- package/dist/crdt/placement/resolve.js +188 -153
- package/dist/crdt/placement/slots.d.ts +134 -0
- package/dist/crdt/placement/slots.js +152 -0
- 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/comments.d.ts +16 -1
- package/dist/crdt/protocols/comments.js +9 -1
- package/dist/crdt/providers/index.d.ts +6 -4
- package/dist/crdt/providers/index.js +3 -1
- package/dist/crdt/providers/room.d.ts +16 -2
- package/dist/crdt/providers/room.js +64 -17
- package/dist/crdt/providers/websocket.d.ts +6 -4
- package/dist/crdt/providers/websocket.js +14 -7
- package/dist/crdt/purge.d.ts +24 -9
- package/dist/crdt/purge.js +266 -126
- package/dist/crdt/restore.d.ts +4 -3
- package/dist/crdt/restore.js +27 -34
- package/dist/crdt/schema.d.ts +30 -21
- package/dist/crdt/schema.js +39 -23
- package/dist/crdt/storage.d.ts +1 -1
- package/dist/crdt/storage.js +1 -1
- package/dist/crdt/structs.d.ts +17 -15
- package/dist/crdt/structs.js +10 -22
- package/dist/crdt/text/index/cache.js +4 -0
- package/dist/crdt/text/index/claims.js +13 -23
- package/dist/crdt/text/index/fold.d.ts +2 -3
- package/dist/crdt/text/index/fold.js +194 -44
- package/dist/crdt/text/index/layout.d.ts +2 -4
- package/dist/crdt/text/index/layout.js +14 -30
- package/dist/crdt/text/index/placement.d.ts +26 -18
- package/dist/crdt/text/index/placement.js +420 -157
- package/dist/crdt/text/index/records.d.ts +3 -3
- package/dist/crdt/text/index/records.js +24 -31
- package/dist/crdt/text/index/shared.d.ts +11 -3
- package/dist/crdt/text/index/shared.js +29 -17
- package/dist/crdt/text/index/state.d.ts +21 -17
- package/dist/crdt/text/index/state.js +15 -17
- package/dist/crdt/text/index/streams.d.ts +10 -5
- package/dist/crdt/text/index/streams.js +78 -27
- package/dist/crdt/text/items.d.ts +10 -3
- package/dist/crdt/text/items.js +7 -0
- package/dist/crdt/text/model.d.ts +87 -62
- package/dist/crdt/text/model.js +91 -169
- package/dist/crdt/text/ranges.d.ts +1 -1
- package/dist/crdt/text/ranges.js +3 -3
- package/dist/crdt/text/rows.d.ts +13 -7
- package/dist/crdt/text/rows.js +24 -7
- package/dist/crdt/text/runs.d.ts +51 -42
- package/dist/crdt/text/runs.js +24 -7
- package/dist/crdt/text/streams.d.ts +26 -13
- package/dist/crdt/text/streams.js +41 -14
- package/dist/crdt/vendor/yjs/UPSTREAM.md +651 -21
- package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
- package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +197 -10
- package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
- package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
- package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
- package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
- package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
- package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
- package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
- package/dist/crdt/vendor/yjs/dts/ynode.d.ts +54 -7
- package/dist/crdt/vendor/yjs/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
- package/dist/crdt/vendor/yjs/src/structs/Item.js +649 -51
- package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
- package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
- package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
- package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
- package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
- package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
- package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
- package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
- package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
- package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
- package/dist/crdt/vendor/yjs/src/ynode.js +199 -12
- package/dist/edytor.svelte.d.ts +1 -1
- package/dist/edytor.svelte.js +103 -75
- package/dist/events/beforeInputCommands.d.ts +2 -2
- package/dist/events/beforeInputCommands.js +2 -2
- 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/events/onPageHide.d.ts +3 -0
- package/dist/events/onPageHide.js +16 -0
- package/dist/export/forms.d.ts +50 -50
- 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/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 +28 -3
- 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 +11 -37
- 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 +130 -83
- package/dist/surface/pin.svelte.d.ts +9 -9
- package/dist/surface/pin.svelte.js +15 -22
- package/dist/surface/projector.svelte.d.ts +15 -3
- package/dist/surface/projector.svelte.js +67 -22
- package/dist/text/text.svelte.js +14 -19
- package/dist/themes/notion.css +29 -0
- package/package.json +4 -2
- package/dist/crdt/placement/rank.d.ts +0 -87
- package/dist/crdt/placement/rank.js +0 -332
- package/dist/crdt/placement/source.d.ts +0 -43
- package/dist/crdt/placement/source.js +0 -66
- package/dist/crdt/text/delete-index.d.ts +0 -129
- package/dist/crdt/text/delete-index.js +0 -365
- package/dist/crdt/text/deletes.d.ts +0 -76
- package/dist/crdt/text/deletes.js +0 -362
- package/dist/crdt/text/index/anchored.d.ts +0 -17
- package/dist/crdt/text/index/anchored.js +0 -197
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type PurgeReport } from '../../crdt/purge.js';
|
|
2
2
|
import { type RoomContext } from './context.js';
|
|
3
3
|
export declare class RoomPurge {
|
|
4
4
|
#private;
|
|
@@ -8,8 +8,11 @@ export declare class RoomPurge {
|
|
|
8
8
|
get days(): number | null;
|
|
9
9
|
/** A change was stored at `at`: the purge tick runs a day later at the latest. */
|
|
10
10
|
noteChange(at: number): void;
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* The purge task (`AttachedDocument.purge`): its report, `horizon` the
|
|
13
|
+
* epoch it purged to (`null`: only ended sessions' deletes).
|
|
14
|
+
*/
|
|
12
15
|
tick(): (PurgeReport & {
|
|
13
|
-
horizon: number;
|
|
16
|
+
horizon: number | null;
|
|
14
17
|
}) | null;
|
|
15
18
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The purge (`room.purge.*`): the daily tick records an epoch (the state
|
|
3
3
|
* vector) when the document changed, and purges what was deleted before
|
|
4
|
-
* the horizon — the newest epoch at least `purgeAfterDays` old —
|
|
5
|
-
*
|
|
4
|
+
* the horizon — the newest epoch at least `purgeAfterDays` old — and what
|
|
5
|
+
* the sessions whose history ended deleted (`hist.release.end`), as the
|
|
6
|
+
* room's own transaction (`crdt/purge.ts`), then compacts. With no epoch
|
|
7
|
+
* due, a tick still purges the ended sessions' deletes.
|
|
6
8
|
*/
|
|
7
9
|
import { Y } from '../../crdt/engine.js';
|
|
10
|
+
import { releasesOf } from '../../crdt/purge.js';
|
|
8
11
|
import { asEngineDoc } from '../../crdt/structs.js';
|
|
9
12
|
import { PURGE_ORIGIN, noTimers } from './shared.js';
|
|
10
13
|
import { DEFAULT_RETENTION_DAYS } from '../history.js';
|
|
@@ -27,7 +30,10 @@ export class RoomPurge {
|
|
|
27
30
|
if (this.days !== null)
|
|
28
31
|
this.room.scheduler.schedule('purge', at + DAY, 'earlier');
|
|
29
32
|
}
|
|
30
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* The purge task (`AttachedDocument.purge`): its report, `horizon` the
|
|
35
|
+
* epoch it purged to (`null`: only ended sessions' deletes).
|
|
36
|
+
*/
|
|
31
37
|
tick() {
|
|
32
38
|
const room = this.room;
|
|
33
39
|
const { scheduler, sql, tables } = room;
|
|
@@ -61,6 +67,10 @@ export class RoomPurge {
|
|
|
61
67
|
if (horizon !== undefined && horizon.at > purged) {
|
|
62
68
|
result = this.#purgeTo({ at: horizon.at, sv: new Uint8Array(horizon.sv) });
|
|
63
69
|
}
|
|
70
|
+
else if (releasesOf(asEngineDoc(doc)).size > 0) {
|
|
71
|
+
// No epoch is due: the ended sessions' deletes go all the same.
|
|
72
|
+
result = this.#purgeTo(null);
|
|
73
|
+
}
|
|
64
74
|
// The next epoch to pass the horizon, or the next change, arms it again.
|
|
65
75
|
const next = sql
|
|
66
76
|
.exec(`SELECT at FROM ${tables.epochs} WHERE at > ? ORDER BY at LIMIT 1`, Math.max(purged, horizon?.at ?? -1))
|
|
@@ -72,18 +82,27 @@ export class RoomPurge {
|
|
|
72
82
|
return result;
|
|
73
83
|
});
|
|
74
84
|
}
|
|
75
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Purge what was deleted before `horizon` and what ended sessions deleted
|
|
87
|
+
* (`null`: only those), as the room's own transaction, then compact. A
|
|
88
|
+
* purge of ended sessions' deletes that finds none left compacts nothing
|
|
89
|
+
* and reports `null`.
|
|
90
|
+
*/
|
|
76
91
|
#purgeTo(horizon) {
|
|
77
92
|
const room = this.room;
|
|
78
93
|
const doc = room.requireDoc();
|
|
79
94
|
// A restore past the horizon can no longer be undone (`room.history.undo`).
|
|
80
|
-
|
|
95
|
+
if (horizon !== null)
|
|
96
|
+
room.history.expireRestore(horizon.at);
|
|
81
97
|
room.history.closeSlotIfPast();
|
|
82
98
|
let report;
|
|
99
|
+
let wrote = true;
|
|
83
100
|
room.transacting = true;
|
|
84
101
|
try {
|
|
85
102
|
room.facade.transact(() => {
|
|
86
103
|
report = crdt.doc.purge(asEngineDoc(doc), room.facade, horizon);
|
|
104
|
+
const tr = asEngineDoc(doc)._transaction;
|
|
105
|
+
wrote = !tr.insertSet.isEmpty() || !tr.deleteSet.isEmpty();
|
|
87
106
|
}, PURGE_ORIGIN);
|
|
88
107
|
}
|
|
89
108
|
finally {
|
|
@@ -94,30 +113,37 @@ export class RoomPurge {
|
|
|
94
113
|
room.storage.heal();
|
|
95
114
|
throw error;
|
|
96
115
|
}
|
|
97
|
-
|
|
98
|
-
room.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
counters.runs++;
|
|
104
|
-
counters.horizon = horizon.at;
|
|
105
|
-
for (const key of [
|
|
116
|
+
if (horizon !== null)
|
|
117
|
+
room.ctx.storage.transactionSync(() => {
|
|
118
|
+
room.sql.exec(`INSERT OR REPLACE INTO ${room.tables.meta} (key, value) VALUES ('purged', ?)`, horizon.at);
|
|
119
|
+
room.sql.exec(`DELETE FROM ${room.tables.epochs} WHERE at < ?`, horizon.at);
|
|
120
|
+
});
|
|
121
|
+
const keys = [
|
|
106
122
|
'removed',
|
|
107
123
|
'emptied',
|
|
108
124
|
'marks',
|
|
109
125
|
'records',
|
|
110
|
-
'candidates',
|
|
111
126
|
'claims',
|
|
112
|
-
'merged'
|
|
113
|
-
|
|
127
|
+
'merged',
|
|
128
|
+
'released',
|
|
129
|
+
'suggestions'
|
|
130
|
+
];
|
|
131
|
+
// Nothing an ended session deleted was left: no compaction, no run.
|
|
132
|
+
if (horizon === null && !wrote)
|
|
133
|
+
return null;
|
|
134
|
+
room.storage.compact();
|
|
135
|
+
const counters = room.counters.purge;
|
|
136
|
+
counters.runs++;
|
|
137
|
+
if (horizon !== null)
|
|
138
|
+
counters.horizon = horizon.at;
|
|
139
|
+
for (const key of keys)
|
|
114
140
|
counters[key] += report[key];
|
|
115
141
|
room.log({
|
|
116
142
|
edytor: 'purge',
|
|
117
|
-
horizon: horizon
|
|
143
|
+
horizon: horizon?.at ?? null,
|
|
118
144
|
bytes: room.counters.compaction.lastBytes,
|
|
119
145
|
...report
|
|
120
146
|
});
|
|
121
|
-
return { ...report, horizon: horizon
|
|
147
|
+
return { ...report, horizon: horizon?.at ?? null };
|
|
122
148
|
}
|
|
123
149
|
}
|
|
@@ -28,14 +28,16 @@ export declare class ReplicaRegistry {
|
|
|
28
28
|
* stripped from its frame (the rest of the frame is applied: never
|
|
29
29
|
* refused whole). Written: its user's ids, and its own replica (the
|
|
30
30
|
* dialed or bound one), which claims an unowned id or a fresh one, as
|
|
31
|
-
* `bind` would. A socket with no replica claims fresh ids it writes
|
|
31
|
+
* `bind` would. A socket with no replica claims fresh ids it writes, and
|
|
32
|
+
* any socket a fresh id that binds itself to its user in the frame
|
|
33
|
+
* (`bound`: a suggestion's writer).
|
|
32
34
|
* Any other id is relayed — another replica's structs, which a restore
|
|
33
35
|
* from a lagging snapshot may have lost: kept, and left unowned (`''`,
|
|
34
36
|
* claimable only by its own dial), when the room holds none of that id's
|
|
35
37
|
* clocks and no user owns it; stripped otherwise (logged `replica`).
|
|
36
38
|
* Ownership never moves to a relayer.
|
|
37
39
|
*/
|
|
38
|
-
attribute({ user, replica }: Attachment, writers: Set<number>, sv: Map<number, number>, orphans: Set<number>): Set<number>;
|
|
40
|
+
attribute({ user, replica }: Attachment, writers: Set<number>, sv: Map<number, number>, orphans: Set<number>, bound?: Set<number>): Set<number>;
|
|
39
41
|
/**
|
|
40
42
|
* Bind the sender's replicas whose `c/<n>` a frame set to another actor
|
|
41
43
|
* to its verified user (`room.attribution.trust`): one room write after the frame's, which
|
|
@@ -75,14 +75,16 @@ export class ReplicaRegistry {
|
|
|
75
75
|
* stripped from its frame (the rest of the frame is applied: never
|
|
76
76
|
* refused whole). Written: its user's ids, and its own replica (the
|
|
77
77
|
* dialed or bound one), which claims an unowned id or a fresh one, as
|
|
78
|
-
* `bind` would. A socket with no replica claims fresh ids it writes
|
|
78
|
+
* `bind` would. A socket with no replica claims fresh ids it writes, and
|
|
79
|
+
* any socket a fresh id that binds itself to its user in the frame
|
|
80
|
+
* (`bound`: a suggestion's writer).
|
|
79
81
|
* Any other id is relayed — another replica's structs, which a restore
|
|
80
82
|
* from a lagging snapshot may have lost: kept, and left unowned (`''`,
|
|
81
83
|
* claimable only by its own dial), when the room holds none of that id's
|
|
82
84
|
* clocks and no user owns it; stripped otherwise (logged `replica`).
|
|
83
85
|
* Ownership never moves to a relayer.
|
|
84
86
|
*/
|
|
85
|
-
attribute({ user, replica }, writers, sv, orphans) {
|
|
87
|
+
attribute({ user, replica }, writers, sv, orphans, bound = new Set()) {
|
|
86
88
|
const room = this.room;
|
|
87
89
|
const claimed = [];
|
|
88
90
|
const relayed = [];
|
|
@@ -93,7 +95,10 @@ export class ReplicaRegistry {
|
|
|
93
95
|
continue;
|
|
94
96
|
const held = (sv.get(client) ?? 0) > 0;
|
|
95
97
|
const fresh = owner === undefined && !held;
|
|
96
|
-
|
|
98
|
+
// A fresh id the frame binds to its user by itself (a suggestion's writer) is the user's.
|
|
99
|
+
if (client === replica
|
|
100
|
+
? fresh || owner === ''
|
|
101
|
+
: (replica === null || bound.has(client)) && fresh) {
|
|
97
102
|
claimed.push({ replica: client, user });
|
|
98
103
|
if (owner === '')
|
|
99
104
|
orphans.add(client);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { YDoc } from '../../crdt/index.js';
|
|
2
|
+
import type { Decoded } from './context.js';
|
|
3
|
+
/** What a frame of a suggest-only socket may not write: the clients to strip, the deletes to drop. */
|
|
4
|
+
export declare const suggestOnly: (doc: YDoc, { structs, ds }: Decoded, skip: Set<number>, owns: (client: number) => boolean, replica: number | null) => {
|
|
5
|
+
strip: Set<number>;
|
|
6
|
+
deletes: Decoded["ds"];
|
|
7
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The suggest-only rule (`room.access.suggest`, `sugg.room.suggest`): what a
|
|
3
|
+
* frame of a socket whose user may only suggest may write. Every struct must
|
|
4
|
+
* be a suggestion's, under a client id of that user that is not the socket's
|
|
5
|
+
* own replica (its writes are suggestion writers'):
|
|
6
|
+
*
|
|
7
|
+
* - a suggestion record (an attr of the root `suggest`), and what a record
|
|
8
|
+
* of its user holds (its attrs, its writer's id lists and operations);
|
|
9
|
+
* - its own binding and profile (the attribution root: the trust rules
|
|
10
|
+
* check them);
|
|
11
|
+
* - text in a block's backing text (an insertion; a split's boundary; a
|
|
12
|
+
* merge's claim of a block its user made; a mark next to its user's own
|
|
13
|
+
* text), and an atom's attrs of an atom its user made;
|
|
14
|
+
* - a block its user made (its registry entry, attrs, children's slots) and
|
|
15
|
+
* a home slot naming such a block in any block's sequence;
|
|
16
|
+
* - a hold record naming only its user's units (deleting its own text).
|
|
17
|
+
*
|
|
18
|
+
* Anything else strips the client from the frame, as a forged mark does, and
|
|
19
|
+
* a delete of an item another user wrote is dropped. Residuals: a mark both of
|
|
20
|
+
* whose ends touch its user's text covers what lies between them, and its user
|
|
21
|
+
* may delete its own accepted text directly (the room reads no record's
|
|
22
|
+
* decisions). Pure reads of the room document and the frame.
|
|
23
|
+
*/
|
|
24
|
+
import { Y } from '../../crdt/engine.js';
|
|
25
|
+
import { ATTRIBUTION_ROOT, BLOCK_NODE, CONTENT_NODE, INLINE_NODE, REGISTRY_KEY, SUGGEST_ROOT, TEXT_DELETES_ROOT } from '../../crdt/schema.js';
|
|
26
|
+
import { isId } from './forged.js';
|
|
27
|
+
import { heldClock, runsOf, storedStruct, structAt } from './updates.js';
|
|
28
|
+
/** What a frame of a suggest-only socket may not write: the clients to strip, the deletes to drop. */
|
|
29
|
+
export const suggestOnly = (doc, { structs, ds }, skip, owns, replica) => {
|
|
30
|
+
const frame = runsOf(structs);
|
|
31
|
+
const strip = new Set();
|
|
32
|
+
/** The struct at `id`, the room's or the frame's. */
|
|
33
|
+
const at = (id) => (storedStruct(doc, id.client, id.clock) ?? structAt(frame.get(id.client) ?? [], id.clock));
|
|
34
|
+
/**
|
|
35
|
+
* Where the item at `id` sits: its map key (`null` in a sequence) and its
|
|
36
|
+
* parent, read from the item or, for an encoded one that names neither,
|
|
37
|
+
* from an origin of it (left, else right: one sequence, or one key).
|
|
38
|
+
*/
|
|
39
|
+
const placeIn = (id) => {
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
for (let next = id; next !== null;) {
|
|
42
|
+
const held = storedStruct(doc, next.client, next.clock);
|
|
43
|
+
if (held)
|
|
44
|
+
return held instanceof Y.Item ? { key: held.parentSub, parent: held.parent } : null;
|
|
45
|
+
const item = structAt(frame.get(next.client) ?? [], next.clock);
|
|
46
|
+
if (!(item instanceof Y.Item) || seen.has(item))
|
|
47
|
+
return null;
|
|
48
|
+
seen.add(item);
|
|
49
|
+
if (item.parent !== null || item.parentSub !== null)
|
|
50
|
+
return { key: item.parentSub, parent: item.parent };
|
|
51
|
+
next = item.origin ?? item.rightOrigin;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
/** The node (and its item) a place's parent names: a root's name, or a nested node's. */
|
|
56
|
+
const nodeOf = (parent) => {
|
|
57
|
+
if (typeof parent === 'string')
|
|
58
|
+
return { root: parent };
|
|
59
|
+
if (isId(parent)) {
|
|
60
|
+
const item = at(parent);
|
|
61
|
+
return { name: item?.content?.type?.name, item: item?.id };
|
|
62
|
+
}
|
|
63
|
+
const node = parent;
|
|
64
|
+
if (!node)
|
|
65
|
+
return {};
|
|
66
|
+
if (!node._item) {
|
|
67
|
+
const root = [...doc.share].find(([, type]) => type === parent)?.[0];
|
|
68
|
+
return { root };
|
|
69
|
+
}
|
|
70
|
+
return { name: node.name, item: node._item.id };
|
|
71
|
+
};
|
|
72
|
+
const registry = () => doc.share.get(REGISTRY_KEY)?._map;
|
|
73
|
+
/** A block `id` whose node a client of this user made (in the room or the frame). */
|
|
74
|
+
const ownBlock = (id) => {
|
|
75
|
+
if (typeof id !== 'string')
|
|
76
|
+
return false;
|
|
77
|
+
const stored = registry()?.get(id);
|
|
78
|
+
if (stored)
|
|
79
|
+
return owns(stored.id.client);
|
|
80
|
+
return structs.some((s) => s instanceof Y.Item &&
|
|
81
|
+
owns(s.id.client) &&
|
|
82
|
+
placeIn(s.id)?.key === id &&
|
|
83
|
+
nodeOf(placeIn(s.id)?.parent).root === REGISTRY_KEY);
|
|
84
|
+
};
|
|
85
|
+
const allowed = (s) => {
|
|
86
|
+
const place = placeIn(s.id);
|
|
87
|
+
if (place === null)
|
|
88
|
+
return false;
|
|
89
|
+
const parent = nodeOf(place.parent);
|
|
90
|
+
const value = s.content.getContent?.()[0];
|
|
91
|
+
if (parent.root === SUGGEST_ROOT || parent.root === ATTRIBUTION_ROOT)
|
|
92
|
+
return true;
|
|
93
|
+
if (parent.name === 'suggestion')
|
|
94
|
+
return !!parent.item && owns(parent.item.client);
|
|
95
|
+
if (parent.root === TEXT_DELETES_ROOT) {
|
|
96
|
+
const ids = s.content.ids ?? [];
|
|
97
|
+
return ids.every((v, i) => i % 3 !== 0 || owns(v));
|
|
98
|
+
}
|
|
99
|
+
if (parent.name === CONTENT_NODE) {
|
|
100
|
+
// A mark (a format item: its key and value).
|
|
101
|
+
if (s.content.key !== undefined) {
|
|
102
|
+
const { origin, rightOrigin } = s;
|
|
103
|
+
const near = [origin, rightOrigin];
|
|
104
|
+
return near.some((id) => isId(id) && owns(id.client));
|
|
105
|
+
}
|
|
106
|
+
return value?.m === undefined || ownBlock(value.m);
|
|
107
|
+
}
|
|
108
|
+
if (parent.name === INLINE_NODE)
|
|
109
|
+
return !!parent.item && owns(parent.item.client);
|
|
110
|
+
if (parent.root === REGISTRY_KEY) {
|
|
111
|
+
// A top-level home slot of its own block, or its own block's entry (never another's).
|
|
112
|
+
if (place.key === null)
|
|
113
|
+
return ownBlock(value?.b);
|
|
114
|
+
const stored = registry()?.get(place.key);
|
|
115
|
+
return !stored || owns(stored.id.client);
|
|
116
|
+
}
|
|
117
|
+
if (parent.name === BLOCK_NODE) {
|
|
118
|
+
if (s.content.priority !== undefined)
|
|
119
|
+
return false;
|
|
120
|
+
if (place.key === null && value?.b !== undefined)
|
|
121
|
+
return ownBlock(value.b);
|
|
122
|
+
return !!parent.item && owns(parent.item.client);
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
};
|
|
126
|
+
for (const s of structs) {
|
|
127
|
+
const { client, clock } = s.id;
|
|
128
|
+
if (!(s instanceof Y.Item) || skip.has(client) || strip.has(client))
|
|
129
|
+
continue;
|
|
130
|
+
if (storedStruct(doc, client, clock) !== null)
|
|
131
|
+
continue;
|
|
132
|
+
if (client === replica || !owns(client) || !allowed(s))
|
|
133
|
+
strip.add(client);
|
|
134
|
+
}
|
|
135
|
+
// Deletes of what another user wrote are dropped.
|
|
136
|
+
const deletes = Y.createIdSet();
|
|
137
|
+
for (const [client, ranges] of ds.clients)
|
|
138
|
+
if (!owns(client))
|
|
139
|
+
for (const { clock, len } of ranges.getIds())
|
|
140
|
+
if (clock < heldClock(doc, client))
|
|
141
|
+
deletes.add(client, clock, len);
|
|
142
|
+
return { strip, deletes };
|
|
143
|
+
};
|
|
@@ -77,10 +77,9 @@ export declare const heldDeletes: (doc: YDoc, ds: Decoded["ds"]) => Decoded["ds"
|
|
|
77
77
|
export declare const withoutPending: <T>(doc: YDoc, read: () => T) => T;
|
|
78
78
|
/**
|
|
79
79
|
* The live document as one update: its healed state — the engine
|
|
80
|
-
* merged what each keystroke wrote and collected deleted content
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* holds exactly what the stored records hold, collected.
|
|
80
|
+
* merged what each keystroke wrote and collected deleted content —,
|
|
81
|
+
* without what waits (`withoutPending`). Memory never runs ahead of
|
|
82
|
+
* storage, so it holds exactly what the stored records hold, collected.
|
|
84
83
|
*/
|
|
85
84
|
export declare const liveState: (doc: YDoc) => Uint8Array;
|
|
86
85
|
/** A fresh room document (`prepareRoomDoc`'s collection rules). */
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as encoding from 'lib0-v14/encoding';
|
|
7
7
|
import { Y } from '../../crdt/engine.js';
|
|
8
|
-
import {
|
|
8
|
+
import { keepFromCollection, structAt as storeStructAt } from '../../crdt/structs.js';
|
|
9
9
|
import { ROOM_ORIGIN } from './shared.js';
|
|
10
10
|
import { crdt } from './context.js';
|
|
11
11
|
export const stateVector = (doc) => Y.decodeStateVector(Y.encodeStateVector(doc));
|
|
@@ -295,19 +295,17 @@ export const withoutPending = (doc, read) => {
|
|
|
295
295
|
};
|
|
296
296
|
/**
|
|
297
297
|
* The live document as one update: its healed state — the engine
|
|
298
|
-
* merged what each keystroke wrote and collected deleted content
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* holds exactly what the stored records hold, collected.
|
|
298
|
+
* merged what each keystroke wrote and collected deleted content —,
|
|
299
|
+
* without what waits (`withoutPending`). Memory never runs ahead of
|
|
300
|
+
* storage, so it holds exactly what the stored records hold, collected.
|
|
302
301
|
*/
|
|
303
302
|
export const liveState = (doc) => withoutPending(doc, () => Y.encodeStateAsUpdate(doc));
|
|
304
303
|
/**
|
|
305
304
|
* A room document's collection rules, before any update applies: it keeps
|
|
306
|
-
*
|
|
307
|
-
*
|
|
305
|
+
* the content the last restore deleted while its undo stands (`keep`,
|
|
306
|
+
* `room.history.undo`).
|
|
308
307
|
*/
|
|
309
308
|
const prepareRoomDoc = (doc, keep) => {
|
|
310
|
-
crdt.doc.keepCopies(asEngineDoc(doc));
|
|
311
309
|
keepFromCollection(doc, (it) => !(keep()?.intersects(it.id.client, it.id.clock, it.length) ?? false));
|
|
312
310
|
};
|
|
313
311
|
/** A fresh room document (`prepareRoomDoc`'s collection rules). */
|
|
@@ -4,15 +4,24 @@ export type DocumentNamespace = {
|
|
|
4
4
|
fetch(request: Request): Promise<Response>;
|
|
5
5
|
};
|
|
6
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* What a user may do in a document: `read`; `comment` (read, and comment:
|
|
9
|
+
* the room writes a thread's anchor); `suggest` (read, comment, and write
|
|
10
|
+
* suggestions, which a writer accepts or rejects); `write` (everything,
|
|
11
|
+
* deciding suggestions included).
|
|
12
|
+
*/
|
|
13
|
+
export type DocumentAccessLevel = 'read' | 'comment' | 'suggest' | 'write';
|
|
7
14
|
/**
|
|
8
15
|
* What the host verified. `userId` owns every Yjs client id the socket
|
|
9
16
|
* writes under; `replica` is the client's `doc.clientID` (read it from a
|
|
10
17
|
* query parameter with {@link requestedReplica}; omitted, the socket's
|
|
11
|
-
* first presence entry binds it); `
|
|
18
|
+
* first presence entry binds it); `access` what the user may do (default
|
|
19
|
+
* `write`; `readOnly: true` is `access: 'read'`).
|
|
12
20
|
*/
|
|
13
21
|
export type DocumentIdentity = {
|
|
14
22
|
userId: string;
|
|
15
23
|
replica?: number | null;
|
|
24
|
+
access?: DocumentAccessLevel;
|
|
16
25
|
readOnly?: boolean;
|
|
17
26
|
/**
|
|
18
27
|
* When the credential expires, ms since the epoch (a token's `exp` ×
|
|
@@ -119,13 +119,15 @@ const authorized = async (request, documentId, authorize, refuse, options) => {
|
|
|
119
119
|
return refuse(CLOSE.expired, 'expired');
|
|
120
120
|
return identity;
|
|
121
121
|
};
|
|
122
|
+
/** What `identity` may do: its `access`, else `read` for `readOnly`, else `write`. */
|
|
123
|
+
const accessOf = (identity) => identity.access ?? (identity.readOnly ? 'read' : 'write');
|
|
122
124
|
/** A refusal as an HTTP status (a probe, a history request). */
|
|
123
125
|
const statusOf = (code, reason) => new Response(reason, { status: PROBE_STATUS[code] ?? 403 });
|
|
124
126
|
/** The verified identity as the headers the room trusts. */
|
|
125
127
|
const identityHeaders = (identity) => {
|
|
126
128
|
const headers = new Headers({
|
|
127
129
|
[IDENTITY_HEADERS.user]: encodeURIComponent(identity.userId),
|
|
128
|
-
[IDENTITY_HEADERS.access]: identity
|
|
130
|
+
[IDENTITY_HEADERS.access]: accessOf(identity)
|
|
129
131
|
});
|
|
130
132
|
const replica = identity.replica ?? null;
|
|
131
133
|
if (replica !== null)
|
|
@@ -173,7 +175,7 @@ async function routeHistory(request, rooms, documentId, authorize, options) {
|
|
|
173
175
|
const identity = await authorized(request, documentId, authorize, statusOf, options);
|
|
174
176
|
if (identity instanceof Response)
|
|
175
177
|
return identity;
|
|
176
|
-
if ((op === 'restore' || op === 'undo') && identity
|
|
178
|
+
if ((op === 'restore' || op === 'undo') && accessOf(identity) !== 'write') {
|
|
177
179
|
return new Response('read-only', { status: 403 });
|
|
178
180
|
}
|
|
179
181
|
const headers = identityHeaders(identity);
|
|
@@ -247,7 +249,7 @@ async function routeComments(request, rooms, documentId, authorize, options) {
|
|
|
247
249
|
const identity = await authorized(request, documentId, authorize, statusOf, options);
|
|
248
250
|
if (identity instanceof Response)
|
|
249
251
|
return identity;
|
|
250
|
-
if (op === 'post' && identity
|
|
252
|
+
if (op === 'post' && accessOf(identity) === 'read')
|
|
251
253
|
return new Response('read-only', { status: 403 });
|
|
252
254
|
let body = null;
|
|
253
255
|
if (op === 'post') {
|
|
@@ -43,7 +43,15 @@
|
|
|
43
43
|
const placeholder = $derived(edytor.placeholderAt(id));
|
|
44
44
|
|
|
45
45
|
/** An empty content's one text (`partsOf`: a content starts with a text). */
|
|
46
|
-
const EMPTY: Segment = {
|
|
46
|
+
const EMPTY: Segment = {
|
|
47
|
+
kind: 'text',
|
|
48
|
+
key: START,
|
|
49
|
+
text: '',
|
|
50
|
+
runs: [],
|
|
51
|
+
segOrd: 0,
|
|
52
|
+
start: 0,
|
|
53
|
+
end: 0
|
|
54
|
+
};
|
|
47
55
|
|
|
48
56
|
/** Whether this view renders an atom of `part`'s kind (its record has a snippet). */
|
|
49
57
|
const rendersAtom = (part: AtomPart) => Boolean(edytor.inlineBlocks.get(part.type)?.snippet);
|
package/dist/crdt/anchors.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { bySlot } from './placement/display.js';
|
|
2
|
+
/** The children list's own order (`bySlot`): rank, then id. */
|
|
3
|
+
const before = (a, b) => bySlot(a, b) < 0;
|
|
3
4
|
/**
|
|
4
5
|
* Where an endpoint whose block `dead` vanished lands: a displayable stop next
|
|
5
6
|
* to the slot `dead` (or its topmost dead ancestor) occupied. `dead === null`
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
* `u/<actorId>` → profile `{name?, color?}` — one record per
|
|
18
18
|
* stable application actor, referenced by id.
|
|
19
19
|
*
|
|
20
|
-
* Published
|
|
21
|
-
* transaction
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* Published with the replica's first write: inside the first local
|
|
21
|
+
* transaction that writes under its client id (the engine's
|
|
22
|
+
* `beforeCommit`, fork patch YP30), so a replica that only reads writes
|
|
23
|
+
* nothing, and every struct it writes travels with its binding. Readable
|
|
24
|
+
* through `actors`/`actorOf`. These records also travel inside `encode()`
|
|
25
|
+
* output, so `loadDocument` restores the dictionary without
|
|
26
|
+
* republishing.
|
|
24
27
|
*
|
|
25
28
|
* LEGACY RECORDS — documents saved by early builds carry `a/` per-edit
|
|
26
29
|
* records under the same root. They are plain replicated state, preserved
|
|
@@ -35,11 +38,14 @@ import { type BlockAttribution } from './block.js';
|
|
|
35
38
|
/** Reserved replicated root for all attribution metadata (`SCHEMA.roots.attribution`). */
|
|
36
39
|
export { ATTRIBUTION_ROOT };
|
|
37
40
|
/**
|
|
38
|
-
* Origin of
|
|
39
|
-
*
|
|
40
|
-
* out of undo capture; tooling classifies
|
|
41
|
+
* Origin of the profile writes this module performs on its own
|
|
42
|
+
* (`setProfile`; the binding joins the transaction of the replica's first
|
|
43
|
+
* write, whatever its origin). Kept out of undo capture; tooling classifies
|
|
44
|
+
* it as bookkeeping.
|
|
41
45
|
*/
|
|
42
46
|
export declare const ATTRIBUTION_ORIGIN: unique symbol;
|
|
47
|
+
export declare const ACTOR_PREFIX = "u/";
|
|
48
|
+
export declare const CLIENT_PREFIX = "c/";
|
|
43
49
|
/** The stable application-level author identity (mirrors `DocumentActor`). */
|
|
44
50
|
export type AttributionActor = {
|
|
45
51
|
id: string;
|
|
@@ -57,14 +63,23 @@ export type ActorProfile = {
|
|
|
57
63
|
* records identically and stay correct as old replicas append records.
|
|
58
64
|
*/
|
|
59
65
|
export type DocumentAttribution = {
|
|
60
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* actorId → profile — the replicated actor dictionary (`u/` records): the
|
|
68
|
+
* actors whose replicas wrote, each published with its replica's first
|
|
69
|
+
* write (or `setProfile`).
|
|
70
|
+
*/
|
|
61
71
|
readonly actors: ReadonlyMap<string, ActorProfile>;
|
|
62
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Resolve an item's inserting replica (clientID) to an actor id (`c/`
|
|
74
|
+
* records). A replica's binding is written with its first write, so every
|
|
75
|
+
* client id that wrote a struct resolves; one that never wrote does not.
|
|
76
|
+
*/
|
|
63
77
|
actorOf(client: number): string | undefined;
|
|
64
78
|
/**
|
|
65
|
-
* Re-publish the local actor's profile record (`u/<actorId>` — LWW)
|
|
66
|
-
*
|
|
67
|
-
*
|
|
79
|
+
* Re-publish the local actor's profile record (`u/<actorId>` — LWW), a
|
|
80
|
+
* write of this replica (its binding goes with it, as with any first
|
|
81
|
+
* write). Attribution is never relabeled: historical records keep the
|
|
82
|
+
* stable actor id; only the resolvable profile changes.
|
|
68
83
|
*/
|
|
69
84
|
setProfile(profile: ActorProfile): void;
|
|
70
85
|
/**
|
|
@@ -94,6 +109,13 @@ export type AttributionController = {
|
|
|
94
109
|
type AttachOptions = {
|
|
95
110
|
actor: AttributionActor;
|
|
96
111
|
};
|
|
112
|
+
/**
|
|
113
|
+
* The actor `doc`'s own replica writes for: its binding (`c/<clientID>`),
|
|
114
|
+
* else, before its first write, the actor its attribution controller was
|
|
115
|
+
* attached with (`undefined`: a raw doc). The provider's `saved` reads it.
|
|
116
|
+
*/
|
|
117
|
+
export declare const writerOf: (doc: EngineDoc) => string | undefined;
|
|
118
|
+
export declare const sanitizeProfile: (value: unknown) => ActorProfile;
|
|
97
119
|
/** The display profile of `actor`: its name and color, those it has. */
|
|
98
120
|
export declare const profileOf: (actor: AttributionActor) => ActorProfile;
|
|
99
121
|
/**
|