@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.
Files changed (3) hide show
  1. package/index.js +173 -4
  2. package/model.js +104 -9
  3. 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 Document.get(document)
204
+ const documentData = await getDocument(document, type)
113
205
  if(!documentData) throw new Error('document not found')
114
- if(document.version > version) return 'ignored'
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
- await Document.create({ id: document, type: documentType, purpose, content, created, lastModified, version: 0 })
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
- await Promise.all([
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: openDocument.content.toJSON(),
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
- window, sessionOrUserType, sessionOrUser, timestamp: new Date(),
117
- steps
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.47",
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": "710dc205163bf09d6b9469822101e9dfc11b74cb"
31
+ "gitHead": "f95b94a51d134dda63ab6f8a42b16a9b26c5dbfb"
32
32
  }