-
Notifications
You must be signed in to change notification settings - Fork 27
/
collab.js
164 lines (147 loc) · 5.93 KB
/
collab.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import {Plugin, PluginKey} from "prosemirror-state"
class Rebaseable {
constructor(step, inverted, origin) {
this.step = step
this.inverted = inverted
this.origin = origin
}
}
// : ([Rebaseable], [Step], Transform) → [Rebaseable]
// Undo a given set of steps, apply a set of other steps, and then
// redo them.
export function rebaseSteps(steps, over, transform) {
for (let i = steps.length - 1; i >= 0; i--) transform.step(steps[i].inverted)
for (let i = 0; i < over.length; i++) transform.step(over[i])
let result = []
for (let i = 0, mapFrom = steps.length; i < steps.length; i++) {
let mapped = steps[i].step.map(transform.mapping.slice(mapFrom))
mapFrom--
if (mapped && !transform.maybeStep(mapped).failed) {
transform.mapping.setMirror(mapFrom, transform.steps.length - 1)
result.push(new Rebaseable(mapped, mapped.invert(transform.docs[transform.docs.length - 1]), steps[i].origin))
}
}
return result
}
// This state field accumulates changes that have to be sent to the
// central authority in the collaborating group and makes it possible
// to integrate changes made by peers into our local document. It is
// defined by the plugin, and will be available as the `collab` field
// in the resulting editor state.
class CollabState {
constructor(version, unconfirmed) {
// : number
// The version number of the last update received from the central
// authority. Starts at 0 or the value of the `version` property
// in the option object, for the editor's value when the option
// was enabled.
this.version = version
// : [Rebaseable]
// The local steps that havent been successfully sent to the
// server yet.
this.unconfirmed = unconfirmed
}
}
function unconfirmedFrom(transform) {
let result = []
for (let i = 0; i < transform.steps.length; i++)
result.push(new Rebaseable(transform.steps[i],
transform.steps[i].invert(transform.docs[i]),
transform))
return result
}
const collabKey = new PluginKey("collab")
// :: (?Object) → Plugin
//
// Creates a plugin that enables the collaborative editing framework
// for the editor.
//
// config::- An optional set of options
//
// version:: ?number
// The starting version number of the collaborative editing.
// Defaults to 0.
//
// clientID:: ?union<number, string>
// This client's ID, used to distinguish its changes from those of
// other clients. Defaults to a random 32-bit number.
export function collab(config = {}) {
config = {version: config.version || 0,
clientID: config.clientID == null ? Math.floor(Math.random() * 0xFFFFFFFF) : config.clientID}
return new Plugin({
key: collabKey,
state: {
init: () => new CollabState(config.version, []),
apply(tr, collab) {
let newState = tr.getMeta(collabKey)
if (newState)
return newState
if (tr.docChanged)
return new CollabState(collab.version, collab.unconfirmed.concat(unconfirmedFrom(tr)))
return collab
}
},
config,
// This is used to notify the history plugin to not merge steps,
// so that the history can be rebased.
historyPreserveItems: true
})
}
// :: (state: EditorState, steps: [Step], clientIDs: [union<number, string>]) → Transaction
// Create a transaction that represents a set of new steps received from
// the authority. Applying this transaction moves the state forward to
// adjust to the authority's view of the document.
export function receiveTransaction(state, steps, clientIDs) {
// Pushes a set of steps (received from the central authority) into
// the editor state (which should have the collab plugin enabled).
// Will recognize its own changes, and confirm unconfirmed steps as
// appropriate. Remaining unconfirmed steps will be rebased over
// remote steps.
let collabState = collabKey.getState(state)
let version = collabState.version + steps.length
let ourID = collabKey.get(state).spec.config.clientID
// Find out which prefix of the steps originated with us
let ours = 0
while (ours < clientIDs.length && clientIDs[ours] == ourID) ++ours
let unconfirmed = collabState.unconfirmed.slice(ours)
steps = ours ? steps.slice(ours) : steps
// If all steps originated with us, we're done.
if (!steps.length)
return state.tr.setMeta(collabKey, new CollabState(version, unconfirmed))
let nUnconfirmed = unconfirmed.length
let tr = state.tr
if (nUnconfirmed) {
unconfirmed = rebaseSteps(unconfirmed, steps, tr)
} else {
for (let i = 0; i < steps.length; i++) tr.step(steps[i])
unconfirmed = []
}
let newCollabState = new CollabState(version, unconfirmed)
return tr.setMeta("rebased", nUnconfirmed).setMeta("addToHistory", false).setMeta(collabKey, newCollabState)
}
// :: (state: EditorState) → ?{version: number, steps: [Step], clientID: union<number, string>, origins: [Transaction]}
// Provides data describing the editor's unconfirmed steps, which need
// to be sent to the central authority. Returns null when there is
// nothing to send.
//
// `origins` holds the _original_ transactions that produced each
// steps. This can be useful for looking up time stamps and other
// metadata for the steps, but note that the steps may have been
// rebased, whereas the origin transactions are still the old,
// unchanged objects.
export function sendableSteps(state) {
let collabState = collabKey.getState(state)
if (collabState.unconfirmed.length == 0) return null
return {
version: collabState.version,
steps: collabState.unconfirmed.map(s => s.step),
clientID: collabKey.get(state).spec.config.clientID,
get origins() { return this._origins || (this._origins = collabState.unconfirmed.map(s => s.origin)) }
}
}
// :: (EditorState) → number
// Get the version up to which the collab plugin has synced with the
// central authority.
export function getVersion(state) {
return collabKey.getState(state).version
}