| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895 |
- /* eslint-disable
- no-console,
- no-return-assign,
- n/no-callback-literal,
- */
- // TODO: This file was created by bulk-decaffeinate.
- // Fix any style issues and re-enable lint.
- /*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS103: Rewrite code to no longer use __guard__
- * DS104: Avoid inline assignments
- * DS204: Change includes calls to have a more natural evaluation order
- * DS205: Consider reworking code to avoid use of IIFEs
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
- // The model of all the ops. Responsible for applying & transforming remote deltas
- // and managing the storage layer.
- //
- // Actual storage is handled by the database wrappers in db/*, wrapped by DocCache
- let Model
- const { EventEmitter } = require('node:events')
- const queue = require('./syncqueue')
- const types = require('../types')
- const Profiler = require('../../Profiler')
- const isArray = o => Object.prototype.toString.call(o) === '[object Array]'
- // This constructor creates a new Model object. There will be one model object
- // per server context.
- //
- // The model object is responsible for a lot of things:
- //
- // - It manages the interactions with the database
- // - It maintains (in memory) a set of all active documents
- // - It calls out to the OT functions when necessary
- //
- // The model is an event emitter. It emits the following events:
- //
- // create(docName, data): A document has been created with the specified name & data
- module.exports = Model = function (db, options) {
- // db can be null if the user doesn't want persistance.
- let getOps
- if (!(this instanceof Model)) {
- return new Model(db, options)
- }
- const model = this
- if (options == null) {
- options = {}
- }
- // This is a cache of 'live' documents.
- //
- // The cache is a map from docName -> {
- // ops:[{op, meta}]
- // snapshot
- // type
- // v
- // meta
- // eventEmitter
- // reapTimer
- // committedVersion: v
- // snapshotWriteLock: bool to make sure writeSnapshot isn't re-entrant
- // dbMeta: database specific data
- // opQueue: syncQueue for processing ops
- // }
- //
- // The ops list contains the document's last options.numCachedOps ops. (Or all
- // of them if we're using a memory store).
- //
- // Documents are stored in this set so long as the document has been accessed in
- // the last few seconds (options.reapTime) OR at least one client has the document
- // open. I don't know if I should keep open (but not being edited) documents live -
- // maybe if a client has a document open but the document isn't being edited, I should
- // flush it from the cache.
- //
- // In any case, the API to model is designed such that if we want to change that later
- // it should be pretty easy to do so without any external-to-the-model code changes.
- const docs = {}
- // This is a map from docName -> [callback]. It is used when a document hasn't been
- // cached and multiple getSnapshot() / getVersion() requests come in. All requests
- // are added to the callback list and called when db.getSnapshot() returns.
- //
- // callback(error, snapshot data)
- const awaitingGetSnapshot = {}
- // The time that documents which no clients have open will stay in the cache.
- // Should be > 0.
- if (options.reapTime == null) {
- options.reapTime = 3000
- }
- // The number of operations the cache holds before reusing the space
- if (options.numCachedOps == null) {
- options.numCachedOps = 10
- }
- // This option forces documents to be reaped, even when there's no database backend.
- // This is useful when you don't care about persistance and don't want to gradually
- // fill memory.
- //
- // You might want to set reapTime to a day or something.
- if (options.forceReaping == null) {
- options.forceReaping = false
- }
- // Until I come up with a better strategy, we'll save a copy of the document snapshot
- // to the database every ~20 submitted ops.
- if (options.opsBeforeCommit == null) {
- options.opsBeforeCommit = 20
- }
- // It takes some processing time to transform client ops. The server will punt ops back to the
- // client to transform if they're too old.
- if (options.maximumAge == null) {
- options.maximumAge = 40
- }
- // **** Cache API methods
- // Its important that all ops are applied in order. This helper method creates the op submission queue
- // for a single document. This contains the logic for transforming & applying ops.
- const makeOpQueue = (docName, doc) =>
- queue(function (opData, callback) {
- if (!(opData.v >= 0)) {
- return callback('Version missing')
- }
- if (opData.v > doc.v) {
- return callback('Op at future version')
- }
- // Punt the transforming work back to the client if the op is too old.
- if (opData.v + options.maximumAge < doc.v) {
- return callback('Op too old')
- }
- if (!opData.meta) {
- opData.meta = {}
- }
- opData.meta.ts = Date.now()
- // We'll need to transform the op to the current version of the document. This
- // calls the callback immediately if opVersion == doc.v.
- return getOps(docName, opData.v, doc.v, function (error, ops) {
- let snapshot
- if (error) {
- return callback(error)
- }
- if (doc.v - opData.v !== ops.length) {
- // This should never happen. It indicates that we didn't get all the ops we
- // asked for. Its important that the submitted op is correctly transformed.
- console.error(
- `Could not get old ops in model for document ${docName}`
- )
- console.error(
- `Expected ops ${opData.v} to ${doc.v} and got ${ops.length} ops`
- )
- return callback('Internal error')
- }
- if (ops.length > 0) {
- try {
- const profile = new Profiler('model.transform')
- // If there's enough ops, it might be worth spinning this out into a webworker thread.
- for (const oldOp of Array.from(ops)) {
- // Dup detection works by sending the id(s) the op has been submitted with previously.
- // If the id matches, we reject it. The client can also detect the op has been submitted
- // already if it sees its own previous id in the ops it sees when it does catchup.
- if (
- oldOp.meta.source &&
- opData.dupIfSource &&
- Array.from(opData.dupIfSource).includes(oldOp.meta.source)
- ) {
- return callback('Op already submitted')
- }
- opData.op = doc.type.transform(opData.op, oldOp.op, 'left')
- opData.v++
- }
- profile.log('transform', { sync: true }).end()
- } catch (error1) {
- error = error1
- return callback(error.message)
- }
- }
- try {
- const profile = new Profiler('model.apply')
- snapshot = doc.type.apply(doc.snapshot, opData.op)
- profile.log('model.apply', { sync: true }).end()
- } catch (error2) {
- error = error2
- return callback(error.message)
- }
- if (
- options.maxDocLength != null &&
- doc.snapshot.length > options.maxDocLength
- ) {
- return callback('Update takes doc over max doc size')
- }
- // The op data should be at the current version, and the new document data should be at
- // the next version.
- //
- // This should never happen in practice, but its a nice little check to make sure everything
- // is hunky-dory.
- if (opData.v !== doc.v) {
- // This should never happen.
- console.error(
- 'Version mismatch detected in model. File a ticket - this is a bug.'
- )
- console.error(`Expecting ${opData.v} == ${doc.v}`)
- return callback('Internal error')
- }
- // newDocData = {snapshot, type:type.name, v:opVersion + 1, meta:docData.meta}
- const writeOp =
- (db != null ? db.writeOp : undefined) ||
- ((docName, newOpData, callback) => callback())
- return writeOp(docName, opData, function (error) {
- if (error) {
- // The user should probably know about this.
- console.warn(`Error writing ops to database: ${error}`)
- return callback(error)
- }
- __guardMethod__(options.stats, 'writeOp', o => o.writeOp())
- // This is needed when we emit the 'change' event, below.
- const oldSnapshot = doc.snapshot
- // All the heavy lifting is now done. Finally, we'll update the cache with the new data
- // and (maybe!) save a new document snapshot to the database.
- doc.v = opData.v + 1
- doc.snapshot = snapshot
- doc.ops.push(opData)
- if (db && doc.ops.length > options.numCachedOps) {
- doc.ops.shift()
- }
- model.emit('applyOp', docName, opData, snapshot, oldSnapshot)
- doc.eventEmitter.emit('op', opData, snapshot, oldSnapshot)
- // The callback is called with the version of the document at which the op was applied.
- // This is the op.v after transformation, and its doc.v - 1.
- callback(null, opData.v)
- // I need a decent strategy here for deciding whether or not to save the snapshot.
- //
- // The 'right' strategy looks something like "Store the snapshot whenever the snapshot
- // is smaller than the accumulated op data". For now, I'll just store it every 20
- // ops or something. (Configurable with doc.committedVersion)
- if (
- !doc.snapshotWriteLock &&
- doc.committedVersion + options.opsBeforeCommit <= doc.v
- ) {
- return tryWriteSnapshot(docName, function (error) {
- if (error) {
- return console.warn(
- `Error writing snapshot ${error}. This is nonfatal`
- )
- }
- })
- }
- })
- })
- })
- // Add the data for the given docName to the cache. The named document shouldn't already
- // exist in the doc set.
- //
- // Returns the new doc.
- const add = function (docName, error, data, committedVersion, ops, dbMeta) {
- let callback, doc
- const callbacks = awaitingGetSnapshot[docName]
- delete awaitingGetSnapshot[docName]
- if (error) {
- if (callbacks) {
- for (callback of Array.from(callbacks)) {
- callback(error)
- }
- }
- } else {
- doc = docs[docName] = {
- snapshot: data.snapshot,
- v: data.v,
- type: data.type,
- meta: data.meta,
- // Cache of ops
- ops: ops || [],
- eventEmitter: new EventEmitter(),
- // Timer before the document will be invalidated from the cache (if the document has no
- // listeners)
- reapTimer: null,
- // Version of the snapshot thats in the database
- committedVersion: committedVersion != null ? committedVersion : data.v,
- snapshotWriteLock: false,
- dbMeta,
- }
- doc.opQueue = makeOpQueue(docName, doc)
- refreshReapingTimeout(docName)
- model.emit('add', docName, data)
- if (callbacks) {
- for (callback of Array.from(callbacks)) {
- callback(null, doc)
- }
- }
- }
- return doc
- }
- // This is a little helper wrapper around db.getOps. It does two things:
- //
- // - If there's no database set, it returns an error to the callback
- // - It adds version numbers to each op returned from the database
- // (These can be inferred from context so the DB doesn't store them, but its useful to have them).
- const getOpsInternal = function (docName, start, end, callback) {
- if (!db) {
- return typeof callback === 'function'
- ? callback('Document does not exist')
- : undefined
- }
- return db.getOps(docName, start, end, function (error, ops) {
- if (error) {
- return typeof callback === 'function' ? callback(error) : undefined
- }
- let v = start
- for (const op of Array.from(ops)) {
- op.v = v++
- }
- return typeof callback === 'function' ? callback(null, ops) : undefined
- })
- }
- // Load the named document into the cache. This function is re-entrant.
- //
- // The callback is called with (error, doc)
- const load = function (docName, callback) {
- if (docs[docName]) {
- // The document is already loaded. Return immediately.
- __guardMethod__(options.stats, 'cacheHit', o => o.cacheHit('getSnapshot'))
- return callback(null, docs[docName])
- }
- // We're a memory store. If we don't have it, nobody does.
- if (!db) {
- return callback('Document does not exist')
- }
- const callbacks = awaitingGetSnapshot[docName]
- // The document is being loaded already. Add ourselves as a callback.
- if (callbacks) {
- return callbacks.push(callback)
- }
- __guardMethod__(options.stats, 'cacheMiss', o1 =>
- o1.cacheMiss('getSnapshot')
- )
- // The document isn't loaded and isn't being loaded. Load it.
- awaitingGetSnapshot[docName] = [callback]
- return db.getSnapshot(docName, function (error, data, dbMeta) {
- if (error) {
- return add(docName, error)
- }
- const type = types[data.type]
- if (!type) {
- console.warn(`Type '${data.type}' missing`)
- return callback('Type not found')
- }
- data.type = type
- const committedVersion = data.v
- // The server can close without saving the most recent document snapshot.
- // In this case, there are extra ops which need to be applied before
- // returning the snapshot.
- return getOpsInternal(docName, data.v, null, function (error, ops) {
- if (error) {
- return callback(error)
- }
- if (ops.length > 0) {
- console.log(`Catchup ${docName} ${data.v} -> ${data.v + ops.length}`)
- try {
- for (const op of Array.from(ops)) {
- data.snapshot = type.apply(data.snapshot, op.op)
- data.v++
- }
- } catch (e) {
- // This should never happen - it indicates that whats in the
- // database is invalid.
- console.error(`Op data invalid for ${docName}: ${e.stack}`)
- return callback('Op data invalid')
- }
- }
- model.emit('load', docName, data)
- return add(docName, error, data, committedVersion, ops, dbMeta)
- })
- })
- }
- // This makes sure the cache contains a document. If the doc cache doesn't contain
- // a document, it is loaded from the database and stored.
- //
- // Documents are stored so long as either:
- // - They have been accessed within the past #{PERIOD}
- // - At least one client has the document open
- function refreshReapingTimeout(docName) {
- const doc = docs[docName]
- if (!doc) {
- return
- }
- // I want to let the clients list be updated before this is called.
- return process.nextTick(function () {
- // This is an awkward way to find out the number of clients on a document. If this
- // causes performance issues, add a numClients field to the document.
- //
- // The first check is because its possible that between refreshReapingTimeout being called and this
- // event being fired, someone called delete() on the document and hence the doc is something else now.
- if (
- doc === docs[docName] &&
- doc.eventEmitter.listeners('op').length === 0 &&
- (db || options.forceReaping) &&
- doc.opQueue.busy === false
- ) {
- let reapTimer
- clearTimeout(doc.reapTimer)
- return (doc.reapTimer = reapTimer =
- setTimeout(
- () =>
- tryWriteSnapshot(docName, function () {
- // If the reaping timeout has been refreshed while we're writing the snapshot, or if we're
- // in the middle of applying an operation, don't reap.
- if (
- docs[docName].reapTimer === reapTimer &&
- doc.opQueue.busy === false
- ) {
- return delete docs[docName]
- }
- }),
- options.reapTime
- ))
- }
- })
- }
- function tryWriteSnapshot(docName, callback) {
- if (!db) {
- return typeof callback === 'function' ? callback() : undefined
- }
- const doc = docs[docName]
- // The doc is closed
- if (!doc) {
- return typeof callback === 'function' ? callback() : undefined
- }
- // The document is already saved.
- if (doc.committedVersion === doc.v) {
- return typeof callback === 'function' ? callback() : undefined
- }
- if (doc.snapshotWriteLock) {
- return typeof callback === 'function'
- ? callback('Another snapshot write is in progress')
- : undefined
- }
- doc.snapshotWriteLock = true
- __guardMethod__(options.stats, 'writeSnapshot', o => o.writeSnapshot())
- const writeSnapshot =
- (db != null ? db.writeSnapshot : undefined) ||
- ((docName, docData, dbMeta, callback) => callback())
- const data = {
- v: doc.v,
- meta: doc.meta,
- snapshot: doc.snapshot,
- // The database doesn't know about object types.
- type: doc.type.name,
- }
- // Commit snapshot.
- return writeSnapshot(docName, data, doc.dbMeta, function (error, dbMeta) {
- doc.snapshotWriteLock = false
- // We have to use data.v here because the version in the doc could
- // have been updated between the call to writeSnapshot() and now.
- doc.committedVersion = data.v
- doc.dbMeta = dbMeta
- return typeof callback === 'function' ? callback(error) : undefined
- })
- }
- // *** Model interface methods
- // Create a new document.
- //
- // data should be {snapshot, type, [meta]}. The version of a new document is 0.
- this.create = function (docName, type, meta, callback) {
- if (typeof meta === 'function') {
- ;[meta, callback] = Array.from([{}, meta])
- }
- if (docName.match(/\//)) {
- return typeof callback === 'function'
- ? callback('Invalid document name')
- : undefined
- }
- if (docs[docName]) {
- return typeof callback === 'function'
- ? callback('Document already exists')
- : undefined
- }
- if (typeof type === 'string') {
- type = types[type]
- }
- if (!type) {
- return typeof callback === 'function'
- ? callback('Type not found')
- : undefined
- }
- const data = {
- snapshot: type.create(),
- type: type.name,
- meta: meta || {},
- v: 0,
- }
- const done = function (error, dbMeta) {
- // dbMeta can be used to cache extra state needed by the database to access the document, like an ID or something.
- if (error) {
- return typeof callback === 'function' ? callback(error) : undefined
- }
- // From here on we'll store the object version of the type name.
- data.type = type
- add(docName, null, data, 0, [], dbMeta)
- model.emit('create', docName, data)
- return typeof callback === 'function' ? callback() : undefined
- }
- if (db) {
- return db.create(docName, data, done)
- } else {
- return done()
- }
- }
- // Perminantly deletes the specified document.
- // If listeners are attached, they are removed.
- //
- // The callback is called with (error) if there was an error. If error is null / undefined, the
- // document was deleted.
- //
- // WARNING: This isn't well supported throughout the code. (Eg, streaming clients aren't told about the
- // deletion. Subsequent op submissions will fail).
- this.delete = function (docName, callback) {
- const doc = docs[docName]
- if (doc) {
- clearTimeout(doc.reapTimer)
- delete docs[docName]
- }
- const done = function (error) {
- if (!error) {
- model.emit('delete', docName)
- }
- return typeof callback === 'function' ? callback(error) : undefined
- }
- if (db) {
- return db.delete(docName, doc != null ? doc.dbMeta : undefined, done)
- } else {
- return done(!doc ? 'Document does not exist' : undefined)
- }
- }
- // This gets all operations from [start...end]. (That is, its not inclusive.)
- //
- // end can be null. This means 'get me all ops from start'.
- //
- // Each op returned is in the form {op:o, meta:m, v:version}.
- //
- // Callback is called with (error, [ops])
- //
- // If the document does not exist, getOps doesn't necessarily return an error. This is because
- // its awkward to figure out whether or not the document exists for things
- // like the redis database backend. I guess its a bit gross having this inconsistant
- // with the other DB calls, but its certainly convenient.
- //
- // Use getVersion() to determine if a document actually exists, if thats what you're
- // after.
- this.getOps = getOps = function (docName, start, end, callback) {
- // getOps will only use the op cache if its there. It won't fill the op cache in.
- if (typeof end === 'function') {
- ;[end, callback] = Array.from([null, end])
- }
- if (!(start >= 0)) {
- return callback(new Error('start must be 0+'))
- }
- const ops = docs[docName] != null ? docs[docName].ops : undefined
- if (ops) {
- const version = docs[docName].v
- // Ops contains an array of ops. The last op in the list is the last op applied
- if (end == null) {
- end = version
- }
- start = Math.min(start, end)
- if (start === end) {
- return callback(null, [])
- }
- // Base is the version number of the oldest op we have cached
- const base = version - ops.length
- // If the database is null, we'll trim to the ops we do have and hope thats enough.
- if (start >= base || db === null) {
- refreshReapingTimeout(docName)
- if (options.stats != null) {
- options.stats.cacheHit('getOps')
- }
- return callback(null, ops.slice(start - base, end - base))
- }
- }
- if (options.stats != null) {
- options.stats.cacheMiss('getOps')
- }
- return getOpsInternal(docName, start, end, callback)
- }
- // Gets the snapshot data for the specified document.
- // getSnapshot(docName, callback)
- // Callback is called with (error, {v: <version>, type: <type>, snapshot: <snapshot>, meta: <meta>})
- this.getSnapshot = (docName, callback) =>
- load(docName, (error, doc) =>
- callback(
- error,
- doc
- ? { v: doc.v, type: doc.type, snapshot: doc.snapshot, meta: doc.meta }
- : undefined
- )
- )
- // Gets the latest version # of the document.
- // getVersion(docName, callback)
- // callback is called with (error, version).
- this.getVersion = (docName, callback) =>
- load(docName, (error, doc) =>
- callback(error, doc != null ? doc.v : undefined)
- )
- // Apply an op to the specified document.
- // The callback is passed (error, applied version #)
- // opData = {op:op, v:v, meta:metadata}
- //
- // Ops are queued before being applied so that the following code applies op C before op B:
- // model.applyOp 'doc', OPA, -> model.applyOp 'doc', OPB
- // model.applyOp 'doc', OPC
- this.applyOp = (
- docName,
- opData,
- callback // All the logic for this is in makeOpQueue, above.
- ) =>
- load(docName, function (error, doc) {
- if (error) {
- return callback(error)
- }
- return process.nextTick(() =>
- doc.opQueue(opData, function (error, newVersion) {
- refreshReapingTimeout(docName)
- return typeof callback === 'function'
- ? callback(error, newVersion)
- : undefined
- })
- )
- })
- // TODO: store (some) metadata in DB
- // TODO: op and meta should be combineable in the op that gets sent
- this.applyMetaOp = function (docName, metaOpData, callback) {
- const { path, value } = metaOpData.meta
- if (!isArray(path)) {
- return typeof callback === 'function'
- ? callback('path should be an array')
- : undefined
- }
- return load(docName, function (error, doc) {
- if (error != null) {
- return typeof callback === 'function' ? callback(error) : undefined
- } else {
- let applied = false
- switch (path[0]) {
- case 'shout':
- doc.eventEmitter.emit('op', metaOpData)
- applied = true
- break
- }
- if (applied) {
- model.emit('applyMetaOp', docName, path, value)
- }
- return typeof callback === 'function'
- ? callback(null, doc.v)
- : undefined
- }
- })
- }
- // Listen to all ops from the specified version. If version is in the past, all
- // ops since that version are sent immediately to the listener.
- //
- // The callback is called once the listener is attached, but before any ops have been passed
- // to the listener.
- //
- // This will _not_ edit the document metadata.
- //
- // If there are any listeners, we don't purge the document from the cache. But be aware, this behaviour
- // might change in a future version.
- //
- // version is the document version at which the document is opened. It can be left out if you want to open
- // the document at the most recent version.
- //
- // listener is called with (opData) each time an op is applied.
- //
- // callback(error, openedVersion)
- this.listen = function (docName, version, listener, callback) {
- if (typeof version === 'function') {
- ;[version, listener, callback] = Array.from([null, version, listener])
- }
- return load(docName, function (error, doc) {
- if (error) {
- return typeof callback === 'function' ? callback(error) : undefined
- }
- clearTimeout(doc.reapTimer)
- if (version != null) {
- return getOps(docName, version, null, function (error, data) {
- if (error) {
- return typeof callback === 'function' ? callback(error) : undefined
- }
- doc.eventEmitter.on('op', listener)
- if (typeof callback === 'function') {
- callback(null, version)
- }
- return (() => {
- const result = []
- for (const op of Array.from(data)) {
- let needle
- listener(op)
- // The listener may well remove itself during the catchup phase. If this happens, break early.
- // This is done in a quite inefficient way. (O(n) where n = #listeners on doc)
- if (
- ((needle = listener),
- !Array.from(doc.eventEmitter.listeners('op')).includes(needle))
- ) {
- break
- } else {
- result.push(undefined)
- }
- }
- return result
- })()
- })
- } else {
- // Version is null / undefined. Just add the listener.
- doc.eventEmitter.on('op', listener)
- return typeof callback === 'function'
- ? callback(null, doc.v)
- : undefined
- }
- })
- }
- // Remove a listener for a particular document.
- //
- // removeListener(docName, listener)
- //
- // This is synchronous.
- this.removeListener = function (docName, listener) {
- // The document should already be loaded.
- const doc = docs[docName]
- if (!doc) {
- throw new Error('removeListener called but document not loaded')
- }
- doc.eventEmitter.removeListener('op', listener)
- return refreshReapingTimeout(docName)
- }
- // Flush saves all snapshot data to the database. I'm not sure whether or not this is actually needed -
- // sharejs will happily replay uncommitted ops when documents are re-opened anyway.
- this.flush = function (callback) {
- if (!db) {
- return typeof callback === 'function' ? callback() : undefined
- }
- let pendingWrites = 0
- for (const docName in docs) {
- const doc = docs[docName]
- if (doc.committedVersion < doc.v) {
- pendingWrites++
- // I'm hoping writeSnapshot will always happen in another thread.
- tryWriteSnapshot(docName, () =>
- process.nextTick(function () {
- pendingWrites--
- if (pendingWrites === 0) {
- return typeof callback === 'function' ? callback() : undefined
- }
- })
- )
- }
- }
- // If nothing was queued, terminate immediately.
- if (pendingWrites === 0) {
- return typeof callback === 'function' ? callback() : undefined
- }
- }
- // Close the database connection. This is needed so nodejs can shut down cleanly.
- this.closeDb = function () {
- __guardMethod__(db, 'close', o => o.close())
- return (db = null)
- }
- }
- // Model inherits from EventEmitter.
- Model.prototype = new EventEmitter()
- function __guardMethod__(obj, methodName, transform) {
- if (
- typeof obj !== 'undefined' &&
- obj !== null &&
- typeof obj[methodName] === 'function'
- ) {
- return transform(obj, methodName)
- } else {
- return undefined
- }
- }
|