@live-change/prosemirror-service 0.2.48 → 0.2.50

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 +117 -4
  2. package/model.js +97 -7
  3. package/package.json +4 -4
package/index.js CHANGED
@@ -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,
@@ -160,11 +207,12 @@ definition.action({
160
207
  const [sessionOrUserType, sessionOrUser] =
161
208
  client.user ? ['user_User', client.user] : ['session_Session', client.session]
162
209
  if(continuation) {
163
- console.log("DOC DATA", documentData)
164
- console.log("CONTINUATION", documentData.lastStepsBucket, sessionOrUserType, sessionOrUser)
210
+ //console.log("DOC DATA", documentData)
211
+ //console.log("CONTINUATION", documentData.lastStepsBucket, sessionOrUserType, sessionOrUser)
165
212
  if(!documentData.lastStepsBucket
166
- || documentData.lastStepsBucket.sessionOrUserType != sessionOrUserType
167
- || documentData.lastStepsBucket.sessionOrUser != sessionOrUser) {
213
+ || documentData.lastStepsBucket.sessionOrUserType != sessionOrUserType
214
+ || documentData.lastStepsBucket.sessionOrUser != sessionOrUser
215
+ || documentData.lastStepsBucket.window != window) {
168
216
  console.log("CONTINUATION IGNORED!!")
169
217
  return [] // ignore, client will rebase
170
218
  }
@@ -181,4 +229,69 @@ definition.action({
181
229
  }
182
230
  })
183
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
+
184
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,12 +100,16 @@ async function getDocument(documentId, documentType) {
75
100
  if(!documentData) {
76
101
  return null
77
102
  }
78
- const lastStepsBucket = await StepsBucket.rangePath([documentId], { reverse: true, limit: 1 })?.[0] ?? null
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
+ ])
79
107
  document = {
80
108
  type: documentData.type,
81
109
  content: schema.nodeFromJSON(documentData.content),
82
110
  version: documentData.version,
83
111
  lastStepsBucket,
112
+ lastSnapshot,
84
113
  schema
85
114
  }
86
115
  openDocuments.set(documentId, document)
@@ -92,7 +121,10 @@ async function getDocument(documentId, documentType) {
92
121
  definition.event({
93
122
  name: "documentCreated",
94
123
  async execute({ document, documentType, purpose, content, created, lastModified }) {
95
- 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 })
96
128
  }
97
129
  })
98
130
 
@@ -112,17 +144,75 @@ definition.event({
112
144
  window, sessionOrUserType, sessionOrUser, timestamp: new Date(),
113
145
  steps
114
146
  }
115
- console.log("DOC EDITED", bucket)
147
+ //console.log("DOC EDITED", bucket)
148
+ const content = openDocument.content.toJSON()
116
149
  openDocument.lastStepsBucket = bucket
117
- await Promise.all([
150
+ const promises = [
118
151
  Document.update(document, {
119
- content: openDocument.content.toJSON(),
152
+ content,
120
153
  version: openDocument.version,
121
154
  lastModified: timestamp
122
155
  }),
123
156
  StepsBucket.create(bucket)
124
- ])
157
+ ]
158
+ if(openDocument.lastSnapshot.version < openDocument.version - snapshotAfterSteps) {
159
+ openDocument.lastSnapshot = {
160
+ id: App.encodeIdentifier([document, openDocument.version.toFixed().padStart(10, '0')]),
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
+ }
125
215
  }
126
216
  })
127
217
 
128
- module.exports = { Document, StepsBucket, schemas, getDocument }
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.48",
3
+ "version": "0.2.50",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -21,12 +21,12 @@
21
21
  "url": "https://www.viamage.com/"
22
22
  },
23
23
  "dependencies": {
24
- "@live-change/framework": "0.6.12",
25
- "@live-change/relations-plugin": "0.6.12",
24
+ "@live-change/framework": "0.6.14",
25
+ "@live-change/relations-plugin": "0.6.14",
26
26
  "lru-cache": "^7.12.0",
27
27
  "pluralize": "8.0.0",
28
28
  "progress-stream": "^2.0.0",
29
29
  "prosemirror-model": "^1.18.1"
30
30
  },
31
- "gitHead": "cb09b064a6756221f1b773fe79e5b61de87c5ca5"
31
+ "gitHead": "8d09f0f01547edd2c1321a8d7341e33aff4c562d"
32
32
  }