model.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895
  1. /* eslint-disable
  2. no-console,
  3. no-return-assign,
  4. n/no-callback-literal,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS101: Remove unnecessary use of Array.from
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * DS103: Rewrite code to no longer use __guard__
  13. * DS104: Avoid inline assignments
  14. * DS204: Change includes calls to have a more natural evaluation order
  15. * DS205: Consider reworking code to avoid use of IIFEs
  16. * DS207: Consider shorter variations of null checks
  17. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  18. */
  19. // The model of all the ops. Responsible for applying & transforming remote deltas
  20. // and managing the storage layer.
  21. //
  22. // Actual storage is handled by the database wrappers in db/*, wrapped by DocCache
  23. let Model
  24. const { EventEmitter } = require('node:events')
  25. const queue = require('./syncqueue')
  26. const types = require('../types')
  27. const Profiler = require('../../Profiler')
  28. const isArray = o => Object.prototype.toString.call(o) === '[object Array]'
  29. // This constructor creates a new Model object. There will be one model object
  30. // per server context.
  31. //
  32. // The model object is responsible for a lot of things:
  33. //
  34. // - It manages the interactions with the database
  35. // - It maintains (in memory) a set of all active documents
  36. // - It calls out to the OT functions when necessary
  37. //
  38. // The model is an event emitter. It emits the following events:
  39. //
  40. // create(docName, data): A document has been created with the specified name & data
  41. module.exports = Model = function (db, options) {
  42. // db can be null if the user doesn't want persistance.
  43. let getOps
  44. if (!(this instanceof Model)) {
  45. return new Model(db, options)
  46. }
  47. const model = this
  48. if (options == null) {
  49. options = {}
  50. }
  51. // This is a cache of 'live' documents.
  52. //
  53. // The cache is a map from docName -> {
  54. // ops:[{op, meta}]
  55. // snapshot
  56. // type
  57. // v
  58. // meta
  59. // eventEmitter
  60. // reapTimer
  61. // committedVersion: v
  62. // snapshotWriteLock: bool to make sure writeSnapshot isn't re-entrant
  63. // dbMeta: database specific data
  64. // opQueue: syncQueue for processing ops
  65. // }
  66. //
  67. // The ops list contains the document's last options.numCachedOps ops. (Or all
  68. // of them if we're using a memory store).
  69. //
  70. // Documents are stored in this set so long as the document has been accessed in
  71. // the last few seconds (options.reapTime) OR at least one client has the document
  72. // open. I don't know if I should keep open (but not being edited) documents live -
  73. // maybe if a client has a document open but the document isn't being edited, I should
  74. // flush it from the cache.
  75. //
  76. // In any case, the API to model is designed such that if we want to change that later
  77. // it should be pretty easy to do so without any external-to-the-model code changes.
  78. const docs = {}
  79. // This is a map from docName -> [callback]. It is used when a document hasn't been
  80. // cached and multiple getSnapshot() / getVersion() requests come in. All requests
  81. // are added to the callback list and called when db.getSnapshot() returns.
  82. //
  83. // callback(error, snapshot data)
  84. const awaitingGetSnapshot = {}
  85. // The time that documents which no clients have open will stay in the cache.
  86. // Should be > 0.
  87. if (options.reapTime == null) {
  88. options.reapTime = 3000
  89. }
  90. // The number of operations the cache holds before reusing the space
  91. if (options.numCachedOps == null) {
  92. options.numCachedOps = 10
  93. }
  94. // This option forces documents to be reaped, even when there's no database backend.
  95. // This is useful when you don't care about persistance and don't want to gradually
  96. // fill memory.
  97. //
  98. // You might want to set reapTime to a day or something.
  99. if (options.forceReaping == null) {
  100. options.forceReaping = false
  101. }
  102. // Until I come up with a better strategy, we'll save a copy of the document snapshot
  103. // to the database every ~20 submitted ops.
  104. if (options.opsBeforeCommit == null) {
  105. options.opsBeforeCommit = 20
  106. }
  107. // It takes some processing time to transform client ops. The server will punt ops back to the
  108. // client to transform if they're too old.
  109. if (options.maximumAge == null) {
  110. options.maximumAge = 40
  111. }
  112. // **** Cache API methods
  113. // Its important that all ops are applied in order. This helper method creates the op submission queue
  114. // for a single document. This contains the logic for transforming & applying ops.
  115. const makeOpQueue = (docName, doc) =>
  116. queue(function (opData, callback) {
  117. if (!(opData.v >= 0)) {
  118. return callback('Version missing')
  119. }
  120. if (opData.v > doc.v) {
  121. return callback('Op at future version')
  122. }
  123. // Punt the transforming work back to the client if the op is too old.
  124. if (opData.v + options.maximumAge < doc.v) {
  125. return callback('Op too old')
  126. }
  127. if (!opData.meta) {
  128. opData.meta = {}
  129. }
  130. opData.meta.ts = Date.now()
  131. // We'll need to transform the op to the current version of the document. This
  132. // calls the callback immediately if opVersion == doc.v.
  133. return getOps(docName, opData.v, doc.v, function (error, ops) {
  134. let snapshot
  135. if (error) {
  136. return callback(error)
  137. }
  138. if (doc.v - opData.v !== ops.length) {
  139. // This should never happen. It indicates that we didn't get all the ops we
  140. // asked for. Its important that the submitted op is correctly transformed.
  141. console.error(
  142. `Could not get old ops in model for document ${docName}`
  143. )
  144. console.error(
  145. `Expected ops ${opData.v} to ${doc.v} and got ${ops.length} ops`
  146. )
  147. return callback('Internal error')
  148. }
  149. if (ops.length > 0) {
  150. try {
  151. const profile = new Profiler('model.transform')
  152. // If there's enough ops, it might be worth spinning this out into a webworker thread.
  153. for (const oldOp of Array.from(ops)) {
  154. // Dup detection works by sending the id(s) the op has been submitted with previously.
  155. // If the id matches, we reject it. The client can also detect the op has been submitted
  156. // already if it sees its own previous id in the ops it sees when it does catchup.
  157. if (
  158. oldOp.meta.source &&
  159. opData.dupIfSource &&
  160. Array.from(opData.dupIfSource).includes(oldOp.meta.source)
  161. ) {
  162. return callback('Op already submitted')
  163. }
  164. opData.op = doc.type.transform(opData.op, oldOp.op, 'left')
  165. opData.v++
  166. }
  167. profile.log('transform', { sync: true }).end()
  168. } catch (error1) {
  169. error = error1
  170. return callback(error.message)
  171. }
  172. }
  173. try {
  174. const profile = new Profiler('model.apply')
  175. snapshot = doc.type.apply(doc.snapshot, opData.op)
  176. profile.log('model.apply', { sync: true }).end()
  177. } catch (error2) {
  178. error = error2
  179. return callback(error.message)
  180. }
  181. if (
  182. options.maxDocLength != null &&
  183. doc.snapshot.length > options.maxDocLength
  184. ) {
  185. return callback('Update takes doc over max doc size')
  186. }
  187. // The op data should be at the current version, and the new document data should be at
  188. // the next version.
  189. //
  190. // This should never happen in practice, but its a nice little check to make sure everything
  191. // is hunky-dory.
  192. if (opData.v !== doc.v) {
  193. // This should never happen.
  194. console.error(
  195. 'Version mismatch detected in model. File a ticket - this is a bug.'
  196. )
  197. console.error(`Expecting ${opData.v} == ${doc.v}`)
  198. return callback('Internal error')
  199. }
  200. // newDocData = {snapshot, type:type.name, v:opVersion + 1, meta:docData.meta}
  201. const writeOp =
  202. (db != null ? db.writeOp : undefined) ||
  203. ((docName, newOpData, callback) => callback())
  204. return writeOp(docName, opData, function (error) {
  205. if (error) {
  206. // The user should probably know about this.
  207. console.warn(`Error writing ops to database: ${error}`)
  208. return callback(error)
  209. }
  210. __guardMethod__(options.stats, 'writeOp', o => o.writeOp())
  211. // This is needed when we emit the 'change' event, below.
  212. const oldSnapshot = doc.snapshot
  213. // All the heavy lifting is now done. Finally, we'll update the cache with the new data
  214. // and (maybe!) save a new document snapshot to the database.
  215. doc.v = opData.v + 1
  216. doc.snapshot = snapshot
  217. doc.ops.push(opData)
  218. if (db && doc.ops.length > options.numCachedOps) {
  219. doc.ops.shift()
  220. }
  221. model.emit('applyOp', docName, opData, snapshot, oldSnapshot)
  222. doc.eventEmitter.emit('op', opData, snapshot, oldSnapshot)
  223. // The callback is called with the version of the document at which the op was applied.
  224. // This is the op.v after transformation, and its doc.v - 1.
  225. callback(null, opData.v)
  226. // I need a decent strategy here for deciding whether or not to save the snapshot.
  227. //
  228. // The 'right' strategy looks something like "Store the snapshot whenever the snapshot
  229. // is smaller than the accumulated op data". For now, I'll just store it every 20
  230. // ops or something. (Configurable with doc.committedVersion)
  231. if (
  232. !doc.snapshotWriteLock &&
  233. doc.committedVersion + options.opsBeforeCommit <= doc.v
  234. ) {
  235. return tryWriteSnapshot(docName, function (error) {
  236. if (error) {
  237. return console.warn(
  238. `Error writing snapshot ${error}. This is nonfatal`
  239. )
  240. }
  241. })
  242. }
  243. })
  244. })
  245. })
  246. // Add the data for the given docName to the cache. The named document shouldn't already
  247. // exist in the doc set.
  248. //
  249. // Returns the new doc.
  250. const add = function (docName, error, data, committedVersion, ops, dbMeta) {
  251. let callback, doc
  252. const callbacks = awaitingGetSnapshot[docName]
  253. delete awaitingGetSnapshot[docName]
  254. if (error) {
  255. if (callbacks) {
  256. for (callback of Array.from(callbacks)) {
  257. callback(error)
  258. }
  259. }
  260. } else {
  261. doc = docs[docName] = {
  262. snapshot: data.snapshot,
  263. v: data.v,
  264. type: data.type,
  265. meta: data.meta,
  266. // Cache of ops
  267. ops: ops || [],
  268. eventEmitter: new EventEmitter(),
  269. // Timer before the document will be invalidated from the cache (if the document has no
  270. // listeners)
  271. reapTimer: null,
  272. // Version of the snapshot thats in the database
  273. committedVersion: committedVersion != null ? committedVersion : data.v,
  274. snapshotWriteLock: false,
  275. dbMeta,
  276. }
  277. doc.opQueue = makeOpQueue(docName, doc)
  278. refreshReapingTimeout(docName)
  279. model.emit('add', docName, data)
  280. if (callbacks) {
  281. for (callback of Array.from(callbacks)) {
  282. callback(null, doc)
  283. }
  284. }
  285. }
  286. return doc
  287. }
  288. // This is a little helper wrapper around db.getOps. It does two things:
  289. //
  290. // - If there's no database set, it returns an error to the callback
  291. // - It adds version numbers to each op returned from the database
  292. // (These can be inferred from context so the DB doesn't store them, but its useful to have them).
  293. const getOpsInternal = function (docName, start, end, callback) {
  294. if (!db) {
  295. return typeof callback === 'function'
  296. ? callback('Document does not exist')
  297. : undefined
  298. }
  299. return db.getOps(docName, start, end, function (error, ops) {
  300. if (error) {
  301. return typeof callback === 'function' ? callback(error) : undefined
  302. }
  303. let v = start
  304. for (const op of Array.from(ops)) {
  305. op.v = v++
  306. }
  307. return typeof callback === 'function' ? callback(null, ops) : undefined
  308. })
  309. }
  310. // Load the named document into the cache. This function is re-entrant.
  311. //
  312. // The callback is called with (error, doc)
  313. const load = function (docName, callback) {
  314. if (docs[docName]) {
  315. // The document is already loaded. Return immediately.
  316. __guardMethod__(options.stats, 'cacheHit', o => o.cacheHit('getSnapshot'))
  317. return callback(null, docs[docName])
  318. }
  319. // We're a memory store. If we don't have it, nobody does.
  320. if (!db) {
  321. return callback('Document does not exist')
  322. }
  323. const callbacks = awaitingGetSnapshot[docName]
  324. // The document is being loaded already. Add ourselves as a callback.
  325. if (callbacks) {
  326. return callbacks.push(callback)
  327. }
  328. __guardMethod__(options.stats, 'cacheMiss', o1 =>
  329. o1.cacheMiss('getSnapshot')
  330. )
  331. // The document isn't loaded and isn't being loaded. Load it.
  332. awaitingGetSnapshot[docName] = [callback]
  333. return db.getSnapshot(docName, function (error, data, dbMeta) {
  334. if (error) {
  335. return add(docName, error)
  336. }
  337. const type = types[data.type]
  338. if (!type) {
  339. console.warn(`Type '${data.type}' missing`)
  340. return callback('Type not found')
  341. }
  342. data.type = type
  343. const committedVersion = data.v
  344. // The server can close without saving the most recent document snapshot.
  345. // In this case, there are extra ops which need to be applied before
  346. // returning the snapshot.
  347. return getOpsInternal(docName, data.v, null, function (error, ops) {
  348. if (error) {
  349. return callback(error)
  350. }
  351. if (ops.length > 0) {
  352. console.log(`Catchup ${docName} ${data.v} -> ${data.v + ops.length}`)
  353. try {
  354. for (const op of Array.from(ops)) {
  355. data.snapshot = type.apply(data.snapshot, op.op)
  356. data.v++
  357. }
  358. } catch (e) {
  359. // This should never happen - it indicates that whats in the
  360. // database is invalid.
  361. console.error(`Op data invalid for ${docName}: ${e.stack}`)
  362. return callback('Op data invalid')
  363. }
  364. }
  365. model.emit('load', docName, data)
  366. return add(docName, error, data, committedVersion, ops, dbMeta)
  367. })
  368. })
  369. }
  370. // This makes sure the cache contains a document. If the doc cache doesn't contain
  371. // a document, it is loaded from the database and stored.
  372. //
  373. // Documents are stored so long as either:
  374. // - They have been accessed within the past #{PERIOD}
  375. // - At least one client has the document open
  376. function refreshReapingTimeout(docName) {
  377. const doc = docs[docName]
  378. if (!doc) {
  379. return
  380. }
  381. // I want to let the clients list be updated before this is called.
  382. return process.nextTick(function () {
  383. // This is an awkward way to find out the number of clients on a document. If this
  384. // causes performance issues, add a numClients field to the document.
  385. //
  386. // The first check is because its possible that between refreshReapingTimeout being called and this
  387. // event being fired, someone called delete() on the document and hence the doc is something else now.
  388. if (
  389. doc === docs[docName] &&
  390. doc.eventEmitter.listeners('op').length === 0 &&
  391. (db || options.forceReaping) &&
  392. doc.opQueue.busy === false
  393. ) {
  394. let reapTimer
  395. clearTimeout(doc.reapTimer)
  396. return (doc.reapTimer = reapTimer =
  397. setTimeout(
  398. () =>
  399. tryWriteSnapshot(docName, function () {
  400. // If the reaping timeout has been refreshed while we're writing the snapshot, or if we're
  401. // in the middle of applying an operation, don't reap.
  402. if (
  403. docs[docName].reapTimer === reapTimer &&
  404. doc.opQueue.busy === false
  405. ) {
  406. return delete docs[docName]
  407. }
  408. }),
  409. options.reapTime
  410. ))
  411. }
  412. })
  413. }
  414. function tryWriteSnapshot(docName, callback) {
  415. if (!db) {
  416. return typeof callback === 'function' ? callback() : undefined
  417. }
  418. const doc = docs[docName]
  419. // The doc is closed
  420. if (!doc) {
  421. return typeof callback === 'function' ? callback() : undefined
  422. }
  423. // The document is already saved.
  424. if (doc.committedVersion === doc.v) {
  425. return typeof callback === 'function' ? callback() : undefined
  426. }
  427. if (doc.snapshotWriteLock) {
  428. return typeof callback === 'function'
  429. ? callback('Another snapshot write is in progress')
  430. : undefined
  431. }
  432. doc.snapshotWriteLock = true
  433. __guardMethod__(options.stats, 'writeSnapshot', o => o.writeSnapshot())
  434. const writeSnapshot =
  435. (db != null ? db.writeSnapshot : undefined) ||
  436. ((docName, docData, dbMeta, callback) => callback())
  437. const data = {
  438. v: doc.v,
  439. meta: doc.meta,
  440. snapshot: doc.snapshot,
  441. // The database doesn't know about object types.
  442. type: doc.type.name,
  443. }
  444. // Commit snapshot.
  445. return writeSnapshot(docName, data, doc.dbMeta, function (error, dbMeta) {
  446. doc.snapshotWriteLock = false
  447. // We have to use data.v here because the version in the doc could
  448. // have been updated between the call to writeSnapshot() and now.
  449. doc.committedVersion = data.v
  450. doc.dbMeta = dbMeta
  451. return typeof callback === 'function' ? callback(error) : undefined
  452. })
  453. }
  454. // *** Model interface methods
  455. // Create a new document.
  456. //
  457. // data should be {snapshot, type, [meta]}. The version of a new document is 0.
  458. this.create = function (docName, type, meta, callback) {
  459. if (typeof meta === 'function') {
  460. ;[meta, callback] = Array.from([{}, meta])
  461. }
  462. if (docName.match(/\//)) {
  463. return typeof callback === 'function'
  464. ? callback('Invalid document name')
  465. : undefined
  466. }
  467. if (docs[docName]) {
  468. return typeof callback === 'function'
  469. ? callback('Document already exists')
  470. : undefined
  471. }
  472. if (typeof type === 'string') {
  473. type = types[type]
  474. }
  475. if (!type) {
  476. return typeof callback === 'function'
  477. ? callback('Type not found')
  478. : undefined
  479. }
  480. const data = {
  481. snapshot: type.create(),
  482. type: type.name,
  483. meta: meta || {},
  484. v: 0,
  485. }
  486. const done = function (error, dbMeta) {
  487. // dbMeta can be used to cache extra state needed by the database to access the document, like an ID or something.
  488. if (error) {
  489. return typeof callback === 'function' ? callback(error) : undefined
  490. }
  491. // From here on we'll store the object version of the type name.
  492. data.type = type
  493. add(docName, null, data, 0, [], dbMeta)
  494. model.emit('create', docName, data)
  495. return typeof callback === 'function' ? callback() : undefined
  496. }
  497. if (db) {
  498. return db.create(docName, data, done)
  499. } else {
  500. return done()
  501. }
  502. }
  503. // Perminantly deletes the specified document.
  504. // If listeners are attached, they are removed.
  505. //
  506. // The callback is called with (error) if there was an error. If error is null / undefined, the
  507. // document was deleted.
  508. //
  509. // WARNING: This isn't well supported throughout the code. (Eg, streaming clients aren't told about the
  510. // deletion. Subsequent op submissions will fail).
  511. this.delete = function (docName, callback) {
  512. const doc = docs[docName]
  513. if (doc) {
  514. clearTimeout(doc.reapTimer)
  515. delete docs[docName]
  516. }
  517. const done = function (error) {
  518. if (!error) {
  519. model.emit('delete', docName)
  520. }
  521. return typeof callback === 'function' ? callback(error) : undefined
  522. }
  523. if (db) {
  524. return db.delete(docName, doc != null ? doc.dbMeta : undefined, done)
  525. } else {
  526. return done(!doc ? 'Document does not exist' : undefined)
  527. }
  528. }
  529. // This gets all operations from [start...end]. (That is, its not inclusive.)
  530. //
  531. // end can be null. This means 'get me all ops from start'.
  532. //
  533. // Each op returned is in the form {op:o, meta:m, v:version}.
  534. //
  535. // Callback is called with (error, [ops])
  536. //
  537. // If the document does not exist, getOps doesn't necessarily return an error. This is because
  538. // its awkward to figure out whether or not the document exists for things
  539. // like the redis database backend. I guess its a bit gross having this inconsistant
  540. // with the other DB calls, but its certainly convenient.
  541. //
  542. // Use getVersion() to determine if a document actually exists, if thats what you're
  543. // after.
  544. this.getOps = getOps = function (docName, start, end, callback) {
  545. // getOps will only use the op cache if its there. It won't fill the op cache in.
  546. if (typeof end === 'function') {
  547. ;[end, callback] = Array.from([null, end])
  548. }
  549. if (!(start >= 0)) {
  550. return callback(new Error('start must be 0+'))
  551. }
  552. const ops = docs[docName] != null ? docs[docName].ops : undefined
  553. if (ops) {
  554. const version = docs[docName].v
  555. // Ops contains an array of ops. The last op in the list is the last op applied
  556. if (end == null) {
  557. end = version
  558. }
  559. start = Math.min(start, end)
  560. if (start === end) {
  561. return callback(null, [])
  562. }
  563. // Base is the version number of the oldest op we have cached
  564. const base = version - ops.length
  565. // If the database is null, we'll trim to the ops we do have and hope thats enough.
  566. if (start >= base || db === null) {
  567. refreshReapingTimeout(docName)
  568. if (options.stats != null) {
  569. options.stats.cacheHit('getOps')
  570. }
  571. return callback(null, ops.slice(start - base, end - base))
  572. }
  573. }
  574. if (options.stats != null) {
  575. options.stats.cacheMiss('getOps')
  576. }
  577. return getOpsInternal(docName, start, end, callback)
  578. }
  579. // Gets the snapshot data for the specified document.
  580. // getSnapshot(docName, callback)
  581. // Callback is called with (error, {v: <version>, type: <type>, snapshot: <snapshot>, meta: <meta>})
  582. this.getSnapshot = (docName, callback) =>
  583. load(docName, (error, doc) =>
  584. callback(
  585. error,
  586. doc
  587. ? { v: doc.v, type: doc.type, snapshot: doc.snapshot, meta: doc.meta }
  588. : undefined
  589. )
  590. )
  591. // Gets the latest version # of the document.
  592. // getVersion(docName, callback)
  593. // callback is called with (error, version).
  594. this.getVersion = (docName, callback) =>
  595. load(docName, (error, doc) =>
  596. callback(error, doc != null ? doc.v : undefined)
  597. )
  598. // Apply an op to the specified document.
  599. // The callback is passed (error, applied version #)
  600. // opData = {op:op, v:v, meta:metadata}
  601. //
  602. // Ops are queued before being applied so that the following code applies op C before op B:
  603. // model.applyOp 'doc', OPA, -> model.applyOp 'doc', OPB
  604. // model.applyOp 'doc', OPC
  605. this.applyOp = (
  606. docName,
  607. opData,
  608. callback // All the logic for this is in makeOpQueue, above.
  609. ) =>
  610. load(docName, function (error, doc) {
  611. if (error) {
  612. return callback(error)
  613. }
  614. return process.nextTick(() =>
  615. doc.opQueue(opData, function (error, newVersion) {
  616. refreshReapingTimeout(docName)
  617. return typeof callback === 'function'
  618. ? callback(error, newVersion)
  619. : undefined
  620. })
  621. )
  622. })
  623. // TODO: store (some) metadata in DB
  624. // TODO: op and meta should be combineable in the op that gets sent
  625. this.applyMetaOp = function (docName, metaOpData, callback) {
  626. const { path, value } = metaOpData.meta
  627. if (!isArray(path)) {
  628. return typeof callback === 'function'
  629. ? callback('path should be an array')
  630. : undefined
  631. }
  632. return load(docName, function (error, doc) {
  633. if (error != null) {
  634. return typeof callback === 'function' ? callback(error) : undefined
  635. } else {
  636. let applied = false
  637. switch (path[0]) {
  638. case 'shout':
  639. doc.eventEmitter.emit('op', metaOpData)
  640. applied = true
  641. break
  642. }
  643. if (applied) {
  644. model.emit('applyMetaOp', docName, path, value)
  645. }
  646. return typeof callback === 'function'
  647. ? callback(null, doc.v)
  648. : undefined
  649. }
  650. })
  651. }
  652. // Listen to all ops from the specified version. If version is in the past, all
  653. // ops since that version are sent immediately to the listener.
  654. //
  655. // The callback is called once the listener is attached, but before any ops have been passed
  656. // to the listener.
  657. //
  658. // This will _not_ edit the document metadata.
  659. //
  660. // If there are any listeners, we don't purge the document from the cache. But be aware, this behaviour
  661. // might change in a future version.
  662. //
  663. // version is the document version at which the document is opened. It can be left out if you want to open
  664. // the document at the most recent version.
  665. //
  666. // listener is called with (opData) each time an op is applied.
  667. //
  668. // callback(error, openedVersion)
  669. this.listen = function (docName, version, listener, callback) {
  670. if (typeof version === 'function') {
  671. ;[version, listener, callback] = Array.from([null, version, listener])
  672. }
  673. return load(docName, function (error, doc) {
  674. if (error) {
  675. return typeof callback === 'function' ? callback(error) : undefined
  676. }
  677. clearTimeout(doc.reapTimer)
  678. if (version != null) {
  679. return getOps(docName, version, null, function (error, data) {
  680. if (error) {
  681. return typeof callback === 'function' ? callback(error) : undefined
  682. }
  683. doc.eventEmitter.on('op', listener)
  684. if (typeof callback === 'function') {
  685. callback(null, version)
  686. }
  687. return (() => {
  688. const result = []
  689. for (const op of Array.from(data)) {
  690. let needle
  691. listener(op)
  692. // The listener may well remove itself during the catchup phase. If this happens, break early.
  693. // This is done in a quite inefficient way. (O(n) where n = #listeners on doc)
  694. if (
  695. ((needle = listener),
  696. !Array.from(doc.eventEmitter.listeners('op')).includes(needle))
  697. ) {
  698. break
  699. } else {
  700. result.push(undefined)
  701. }
  702. }
  703. return result
  704. })()
  705. })
  706. } else {
  707. // Version is null / undefined. Just add the listener.
  708. doc.eventEmitter.on('op', listener)
  709. return typeof callback === 'function'
  710. ? callback(null, doc.v)
  711. : undefined
  712. }
  713. })
  714. }
  715. // Remove a listener for a particular document.
  716. //
  717. // removeListener(docName, listener)
  718. //
  719. // This is synchronous.
  720. this.removeListener = function (docName, listener) {
  721. // The document should already be loaded.
  722. const doc = docs[docName]
  723. if (!doc) {
  724. throw new Error('removeListener called but document not loaded')
  725. }
  726. doc.eventEmitter.removeListener('op', listener)
  727. return refreshReapingTimeout(docName)
  728. }
  729. // Flush saves all snapshot data to the database. I'm not sure whether or not this is actually needed -
  730. // sharejs will happily replay uncommitted ops when documents are re-opened anyway.
  731. this.flush = function (callback) {
  732. if (!db) {
  733. return typeof callback === 'function' ? callback() : undefined
  734. }
  735. let pendingWrites = 0
  736. for (const docName in docs) {
  737. const doc = docs[docName]
  738. if (doc.committedVersion < doc.v) {
  739. pendingWrites++
  740. // I'm hoping writeSnapshot will always happen in another thread.
  741. tryWriteSnapshot(docName, () =>
  742. process.nextTick(function () {
  743. pendingWrites--
  744. if (pendingWrites === 0) {
  745. return typeof callback === 'function' ? callback() : undefined
  746. }
  747. })
  748. )
  749. }
  750. }
  751. // If nothing was queued, terminate immediately.
  752. if (pendingWrites === 0) {
  753. return typeof callback === 'function' ? callback() : undefined
  754. }
  755. }
  756. // Close the database connection. This is needed so nodejs can shut down cleanly.
  757. this.closeDb = function () {
  758. __guardMethod__(db, 'close', o => o.close())
  759. return (db = null)
  760. }
  761. }
  762. // Model inherits from EventEmitter.
  763. Model.prototype = new EventEmitter()
  764. function __guardMethod__(obj, methodName, transform) {
  765. if (
  766. typeof obj !== 'undefined' &&
  767. obj !== null &&
  768. typeof obj[methodName] === 'function'
  769. ) {
  770. return transform(obj, methodName)
  771. } else {
  772. return undefined
  773. }
  774. }