model.js 28 KB

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