@live-change/prosemirror-service 0.2.47 → 0.2.49
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/index.js +173 -4
- package/model.js +104 -9
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const app = App.app()
|
|
|
4
4
|
const definition = require('./definition.js')
|
|
5
5
|
const config = definition.config
|
|
6
6
|
|
|
7
|
-
const { Document, StepsBucket, schemas } = require("./model.js")
|
|
7
|
+
const { Document, StepsBucket, schemas, getDocument } = require("./model.js")
|
|
8
8
|
|
|
9
9
|
const { testLatency } = config
|
|
10
10
|
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
@@ -49,6 +49,53 @@ definition.view({
|
|
|
49
49
|
}
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
+
definition.view({
|
|
53
|
+
name: 'snapshot',
|
|
54
|
+
properties: {
|
|
55
|
+
document: {
|
|
56
|
+
type: Document,
|
|
57
|
+
validation: ['nonEmpty']
|
|
58
|
+
},
|
|
59
|
+
version: {
|
|
60
|
+
type: Number
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
async daoPath({ document, version }, { client, context }) {
|
|
64
|
+
return Snapshot.path( App.encodeIdentifier([props.document,version.toFixed().padStart(10, '0')]) )
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
definition.view({
|
|
69
|
+
name: 'snapshots',
|
|
70
|
+
properties: {
|
|
71
|
+
document: {
|
|
72
|
+
type: Document,
|
|
73
|
+
validation: ['nonEmpty']
|
|
74
|
+
},
|
|
75
|
+
...App.rangeProperties
|
|
76
|
+
},
|
|
77
|
+
async daoPath({ document, version }, { client, context }) {
|
|
78
|
+
return Snapshot.indexRangePath( [document], App.extractRange(props) )
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
definition.view({
|
|
83
|
+
name: 'snapshots',
|
|
84
|
+
properties: {
|
|
85
|
+
document: {
|
|
86
|
+
type: Document,
|
|
87
|
+
validation: ['nonEmpty']
|
|
88
|
+
},
|
|
89
|
+
version: {
|
|
90
|
+
type: Number
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
async daoPath({ document, version }, { client, context }) {
|
|
94
|
+
Snapshot.limitedRangePath([props.document], { limit: 10 })
|
|
95
|
+
return Snapshot.path( App.encodeIdentifier([props.document,version.toFixed().padStart(10, '0')]) )
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
52
99
|
definition.action({
|
|
53
100
|
name: 'createDocument',
|
|
54
101
|
waitForEvents: true,
|
|
@@ -78,6 +125,48 @@ definition.action({
|
|
|
78
125
|
type: 'documentCreated',
|
|
79
126
|
document, documentType: type, purpose, content, lastModified: new Date(), created: new Date()
|
|
80
127
|
})
|
|
128
|
+
return {
|
|
129
|
+
id: document,
|
|
130
|
+
type, purpose, content, version: 0
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
definition.action({
|
|
136
|
+
name: 'createDocumentIfNotExists',
|
|
137
|
+
waitForEvents: true,
|
|
138
|
+
properties: {
|
|
139
|
+
document: {
|
|
140
|
+
type: String,
|
|
141
|
+
validation: ['nonEmpty']
|
|
142
|
+
},
|
|
143
|
+
type: {
|
|
144
|
+
type: String,
|
|
145
|
+
validation: ['nonEmpty']
|
|
146
|
+
},
|
|
147
|
+
purpose: {
|
|
148
|
+
type: String,
|
|
149
|
+
validation: ['nonEmpty']
|
|
150
|
+
},
|
|
151
|
+
content: {
|
|
152
|
+
type: Object
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
async execute({ document, type, purpose, content }, { client, service }, emit) {
|
|
156
|
+
if(testLatency) await sleep(testLatency)
|
|
157
|
+
if(!schemas[type]) throw new Error(`schema not found for document type ${type}`)
|
|
158
|
+
const documentData = await Document.get(document)
|
|
159
|
+
if(documentData) {
|
|
160
|
+
return documentData
|
|
161
|
+
}
|
|
162
|
+
emit({
|
|
163
|
+
type: 'documentCreated',
|
|
164
|
+
document, documentType: type, purpose, content, lastModified: new Date(), created: new Date()
|
|
165
|
+
})
|
|
166
|
+
return {
|
|
167
|
+
id: document,
|
|
168
|
+
type, purpose, content, version: 0
|
|
169
|
+
}
|
|
81
170
|
}
|
|
82
171
|
})
|
|
83
172
|
|
|
@@ -103,17 +192,32 @@ definition.action({
|
|
|
103
192
|
window: {
|
|
104
193
|
type: String,
|
|
105
194
|
validation: ['nonEmpty']
|
|
195
|
+
},
|
|
196
|
+
continuation: {
|
|
197
|
+
type: Boolean
|
|
106
198
|
}
|
|
107
199
|
},
|
|
108
200
|
queuedBy: (command) => command.client.document,
|
|
109
|
-
async execute({ document, type, version, steps, window }, { client, service }, emit) {
|
|
201
|
+
async execute({ document, type, version, steps, window, continuation }, { client, service }, emit) {
|
|
110
202
|
if(testLatency) await sleep(testLatency)
|
|
111
203
|
if(!schemas[type]) throw new Error(`schema not found for document type ${type}`)
|
|
112
|
-
const documentData = await
|
|
204
|
+
const documentData = await getDocument(document, type)
|
|
113
205
|
if(!documentData) throw new Error('document not found')
|
|
114
|
-
if(
|
|
206
|
+
if(documentData.version != version) return 'ignored'
|
|
115
207
|
const [sessionOrUserType, sessionOrUser] =
|
|
116
208
|
client.user ? ['user_User', client.user] : ['session_Session', client.session]
|
|
209
|
+
if(continuation) {
|
|
210
|
+
//console.log("DOC DATA", documentData)
|
|
211
|
+
//console.log("CONTINUATION", documentData.lastStepsBucket, sessionOrUserType, sessionOrUser)
|
|
212
|
+
if(!documentData.lastStepsBucket
|
|
213
|
+
|| documentData.lastStepsBucket.sessionOrUserType != sessionOrUserType
|
|
214
|
+
|| documentData.lastStepsBucket.sessionOrUser != sessionOrUser
|
|
215
|
+
|| documentData.lastStepsBucket.window != window) {
|
|
216
|
+
console.log("CONTINUATION IGNORED!!")
|
|
217
|
+
return [] // ignore, client will rebase
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
117
221
|
emit({
|
|
118
222
|
type: 'documentEdited',
|
|
119
223
|
document, documentType: type, version, steps, window,
|
|
@@ -125,4 +229,69 @@ definition.action({
|
|
|
125
229
|
}
|
|
126
230
|
})
|
|
127
231
|
|
|
232
|
+
definition.action({
|
|
233
|
+
name: 'takeSnapshot',
|
|
234
|
+
waitForEvents: true,
|
|
235
|
+
properties: {
|
|
236
|
+
document: {
|
|
237
|
+
type: String,
|
|
238
|
+
validation: ['nonEmpty']
|
|
239
|
+
},
|
|
240
|
+
type: {
|
|
241
|
+
type: String,
|
|
242
|
+
validation: ['nonEmpty']
|
|
243
|
+
},
|
|
244
|
+
version: {
|
|
245
|
+
type: Number
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
queuedBy: (command) => command.client.document,
|
|
249
|
+
async execute({ document, type, version }, { client, service }, emit) {
|
|
250
|
+
if(!schemas[type]) throw new Error(`schema not found for document type ${type}`)
|
|
251
|
+
const documentData = await getDocument(document, type)
|
|
252
|
+
if(!documentData) throw new Error('document not found')
|
|
253
|
+
if(typeof version != 'number') version = documentData.version
|
|
254
|
+
const snapshot = App.encodeIdentifier([document, version.toFixed().padStart(10, '0')])
|
|
255
|
+
emit({
|
|
256
|
+
type: 'snapshotTaken',
|
|
257
|
+
snapshot,
|
|
258
|
+
document, documentType: type, version
|
|
259
|
+
})
|
|
260
|
+
return snapshot
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
definition.trigger({
|
|
265
|
+
name: 'takeSnapshot',
|
|
266
|
+
waitForEvents: true,
|
|
267
|
+
properties: {
|
|
268
|
+
document: {
|
|
269
|
+
type: String,
|
|
270
|
+
validation: ['nonEmpty']
|
|
271
|
+
},
|
|
272
|
+
type: {
|
|
273
|
+
type: String,
|
|
274
|
+
validation: ['nonEmpty']
|
|
275
|
+
},
|
|
276
|
+
version: {
|
|
277
|
+
type: Number
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
queuedBy: (command) => command.client.document,
|
|
281
|
+
async execute({ document, type, version }, { client, service }, emit) {
|
|
282
|
+
if(!schemas[type]) throw new Error(`schema not found for document type ${type}`)
|
|
283
|
+
const documentData = await getDocument(document, type)
|
|
284
|
+
if(!documentData) throw new Error('document not found')
|
|
285
|
+
if(typeof version != 'number') version = documentData.version
|
|
286
|
+
const snapshot = App.encodeIdentifier([document, version.toFixed().padStart(10, '0')])
|
|
287
|
+
emit({
|
|
288
|
+
type: 'snapshotTaken',
|
|
289
|
+
snapshot,
|
|
290
|
+
document, documentType: type, version
|
|
291
|
+
})
|
|
292
|
+
return snapshot
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
|
|
128
297
|
module.exports = definition
|
package/model.js
CHANGED
|
@@ -6,6 +6,8 @@ const LRU = require('lru-cache')
|
|
|
6
6
|
const { Schema } = require('prosemirror-model')
|
|
7
7
|
const { Step } = require('prosemirror-transform')
|
|
8
8
|
|
|
9
|
+
const { snapshotAfterSteps = 230 } = config
|
|
10
|
+
|
|
9
11
|
const Document = definition.model({
|
|
10
12
|
name: 'Document',
|
|
11
13
|
propertyOfAny: {
|
|
@@ -53,6 +55,29 @@ const StepsBucket = definition.model({
|
|
|
53
55
|
}
|
|
54
56
|
})
|
|
55
57
|
|
|
58
|
+
const Snapshot = definition.model({
|
|
59
|
+
name: 'Snapshot',
|
|
60
|
+
properties: {
|
|
61
|
+
document: {
|
|
62
|
+
type: String
|
|
63
|
+
},
|
|
64
|
+
version: {
|
|
65
|
+
type: Number
|
|
66
|
+
},
|
|
67
|
+
content: {
|
|
68
|
+
type: Object
|
|
69
|
+
},
|
|
70
|
+
timestamp: {
|
|
71
|
+
type: Date
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
indexes: {
|
|
75
|
+
list: {
|
|
76
|
+
property: ['document', 'version']
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
56
81
|
const schemas = {}
|
|
57
82
|
for(const typeName in config.documentTypes) {
|
|
58
83
|
const spec = config.documentTypes[typeName]
|
|
@@ -75,10 +100,16 @@ async function getDocument(documentId, documentType) {
|
|
|
75
100
|
if(!documentData) {
|
|
76
101
|
return null
|
|
77
102
|
}
|
|
103
|
+
const [lastStepsBucket, lastSnapshot] = await Promise.all([
|
|
104
|
+
StepsBucket.rangeGet([documentId], { reverse: true, limit: 1 }).then(x => x?.[0] ?? null),
|
|
105
|
+
Snapshot.rangeGet([documentId], { reverse: true, limit: 1 }).then(x => x?.[0] ?? null)
|
|
106
|
+
])
|
|
78
107
|
document = {
|
|
79
108
|
type: documentData.type,
|
|
80
109
|
content: schema.nodeFromJSON(documentData.content),
|
|
81
110
|
version: documentData.version,
|
|
111
|
+
lastStepsBucket,
|
|
112
|
+
lastSnapshot,
|
|
82
113
|
schema
|
|
83
114
|
}
|
|
84
115
|
openDocuments.set(documentId, document)
|
|
@@ -90,7 +121,10 @@ async function getDocument(documentId, documentType) {
|
|
|
90
121
|
definition.event({
|
|
91
122
|
name: "documentCreated",
|
|
92
123
|
async execute({ document, documentType, purpose, content, created, lastModified }) {
|
|
93
|
-
|
|
124
|
+
const version = 0
|
|
125
|
+
await Document.create({ id: document, type: documentType, purpose, content, created, lastModified, version })
|
|
126
|
+
await Snapshot.create({ id: App.encodeIdentifier([document, version.toFixed().padStart(10, '0')]),
|
|
127
|
+
document, version, content, timestamp: lastModified })
|
|
94
128
|
}
|
|
95
129
|
})
|
|
96
130
|
|
|
@@ -105,19 +139,80 @@ definition.event({
|
|
|
105
139
|
openDocument.content = step.apply(openDocument.content).doc
|
|
106
140
|
openDocument.version ++
|
|
107
141
|
}
|
|
108
|
-
|
|
142
|
+
const bucket = {
|
|
143
|
+
id: App.encodeIdentifier([document, openDocument.version.toFixed().padStart(10, '0')]),
|
|
144
|
+
window, sessionOrUserType, sessionOrUser, timestamp: new Date(),
|
|
145
|
+
steps
|
|
146
|
+
}
|
|
147
|
+
//console.log("DOC EDITED", bucket)
|
|
148
|
+
const content = openDocument.content.toJSON()
|
|
149
|
+
openDocument.lastStepsBucket = bucket
|
|
150
|
+
const promises = [
|
|
109
151
|
Document.update(document, {
|
|
110
|
-
content
|
|
152
|
+
content,
|
|
111
153
|
version: openDocument.version,
|
|
112
154
|
lastModified: timestamp
|
|
113
155
|
}),
|
|
114
|
-
StepsBucket.create(
|
|
156
|
+
StepsBucket.create(bucket)
|
|
157
|
+
]
|
|
158
|
+
if(openDocument.lastSnapshot.version < openDocument.version - snapshotAfterSteps) {
|
|
159
|
+
openDocument.lastSnapshot = {
|
|
115
160
|
id: App.encodeIdentifier([document, openDocument.version.toFixed().padStart(10, '0')]),
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
161
|
+
document, version: openDocument.version, content, timestamp
|
|
162
|
+
}
|
|
163
|
+
promises.push(Snapshot.create(openDocument.lastSnapshot))
|
|
164
|
+
}
|
|
165
|
+
await Promise.all(promises)
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
async function readVersion(document, documentType, version) {
|
|
170
|
+
const schema = schemas[documentType]
|
|
171
|
+
const snapshot = await Snapshot.rangePath([document], {
|
|
172
|
+
reverse: true, limit: 1, lte: version.toFixed().padStart(10, '0')
|
|
173
|
+
})?.[0]
|
|
174
|
+
if(!snapshot) throw 'not_found'
|
|
175
|
+
let content = schema.nodeFromJSON(snapshot.content)
|
|
176
|
+
let current = snapshot.version
|
|
177
|
+
while(current < version) {
|
|
178
|
+
const stepsBuckets = await StepsBucket.rangePath([document], {
|
|
179
|
+
gt: current.toFixed().padStart(10, '0')
|
|
180
|
+
})
|
|
181
|
+
for(const stepsBucket of stepsBuckets) {
|
|
182
|
+
for(const stepJson of stepsBucket.steps) {
|
|
183
|
+
const step = Step.fromJSON(schemas[snapshot.type], stepJson)
|
|
184
|
+
content = step.apply(content).doc
|
|
185
|
+
current ++
|
|
186
|
+
if(current == version) return {
|
|
187
|
+
content,
|
|
188
|
+
timestamp: stepsBuckets.timestamp
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
definition.event({
|
|
196
|
+
name: "snapshotTaken",
|
|
197
|
+
async execute({ snapshot: id, document, documentType, version }) {
|
|
198
|
+
const openDocument = await getDocument(document, documentType)
|
|
199
|
+
if(!openDocument) throw new Error('critical error - document not found') /// impossible
|
|
200
|
+
if(openDocument.version < version)
|
|
201
|
+
throw new Error('critical error - document version is lower than snapshot version') /// impossible
|
|
202
|
+
const existing = Snapshot.get(id)
|
|
203
|
+
if(existing) return id
|
|
204
|
+
if(openDocument.version == version) {
|
|
205
|
+
const content = openDocument.content
|
|
206
|
+
const snapshot = { id, document, version, content, timestamp: openDocument.lastModified }
|
|
207
|
+
await Snapshot.create(snapshot)
|
|
208
|
+
if(version > openDocument.lastSnapshot.version) openDocument.lastSnapshot = snapshot
|
|
209
|
+
} else {
|
|
210
|
+
const { content, timestamp } = readVersion(document, documentType, version)
|
|
211
|
+
const snapshot = { id, document, version, content, timestamp }
|
|
212
|
+
await Snapshot.create(snapshot)
|
|
213
|
+
if(version > openDocument.lastSnapshot.version) openDocument.lastSnapshot = snapshot
|
|
214
|
+
}
|
|
120
215
|
}
|
|
121
216
|
})
|
|
122
217
|
|
|
123
|
-
module.exports = { Document, StepsBucket, schemas }
|
|
218
|
+
module.exports = { Document, StepsBucket, schemas, getDocument, readVersion }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@live-change/prosemirror-service",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.49",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -28,5 +28,5 @@
|
|
|
28
28
|
"progress-stream": "^2.0.0",
|
|
29
29
|
"prosemirror-model": "^1.18.1"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "f95b94a51d134dda63ab6f8a42b16a9b26c5dbfb"
|
|
32
32
|
}
|