Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decide on additionalMethods format #1

Closed
vweevers opened this issue Sep 22, 2019 · 10 comments
Closed

Decide on additionalMethods format #1

vweevers opened this issue Sep 22, 2019 · 10 comments
Labels
discussion Discussion

Comments

@vweevers
Copy link
Member

vweevers commented Sep 22, 2019

Initially we can do:

{
  additionalMethods: {
    approximateSize: true
  }
}

Which is enough to start using that in deferred-leveldown (Level/deferred-leveldown#35) and level-packager levelup. @ralphtheninja WDYT?

Later we can add function signature info. I propose to stick with boolean properties, so that we can describe multiple signatures (e.g. callbacks and promises):

{
  additionalMethods: {
    approximateSize: {
      sync: false,
      callback: true,
      promise: false
    }
  }
}

With that in place, a db wrapper could do things like:

wrapper.prototype.approximateSize = function (..) {
  if (this.db.supports.additionalMethods.approximateSize.promise) {
    return db.approximateSize(..)
  } else {
    // Wrap in promise
  }
}

Even later, we can add a return type to the manifest. Mainly for streams. Not sure what that should look like. Perhaps:

{
  additionalMethods: {
    createWriteStream: {
      sync: false,
      writable: true
    }
  }
}
@vweevers vweevers added the discussion Discussion label Sep 22, 2019
@vweevers
Copy link
Member Author

One thing the manifest currently can't cover is "static" additional methods like destroy() and repair().

@vweevers
Copy link
Member Author

vweevers commented Oct 1, 2019

To use the manifest in encoding-down to automatically encode arguments of approximateSize() etc, we need a way to describe arguments. I made a draft PR of what that might look like: Level/encoding-down#93

@vweevers
Copy link
Member Author

vweevers commented Oct 5, 2019

Combined with level-compose, a plugin pattern starts to emerge:

function myIndexPlugin (db) {
  db.supports.additionalMethods.createIndex = {
    args: [{ type: 'key' }]
  }

  db.createIndex = function (prefix) {
    // prefix is encoded
  }

  return db
}

const factory = compose()
  .use(leveldown)
  .use(myIndexPlugin)
  .use(encode, { keyEncoding: charwise })
  .use(levelup)

const db = factory()

db.createIndex(['my_indexes', 'foo'])

@ralphtheninja
Copy link
Member

The api looks very nice I think.

@vweevers
Copy link
Member Author

vweevers commented Oct 6, 2019

A more complex example, where encoding-down has to decode output of a hypothetical getAll() method. Trying out a format compatible with superstruct (compact, but would require parsing). We'll at some point hit the same problem that the typescript folks had, with return types that depend on options:

function getAllPlugin (db) {
  db.supports.additionalMethods.getAll = {
    args: ['options?', 'callback'],
    yields: ['entry'] // TODO: type depends on options
  }

  db.getAll = function (options, callback) {
    const it = db.iterator(options)

    concat(it, function (err, entries) {
      if (err) return callback(err)
      
      if (options.keys === false) {
        entries = entries.map(e => e.value)
      } else if (options.values === false) {
        entries = entries.map(e => e.key)
      }

      callback(null, entries)
    })
  }

  return db
}

const factory = compose()
  .use(leveldown)
  .use(getAllPlugin)
  .use(encode, { valueEncoding: 'json' })
  .use(levelup)

const db = factory()
const entries = await db.getAll({ lt: 18 })
const values = await db.getAll({ lt: 18, keys: false })

@vweevers
Copy link
Member Author

vweevers commented Oct 6, 2019

Perhaps we should simply stop using that pattern ({ keys: false }), at least in public APIs. On read streams for example, we could hide it like so:

// Yields entries, values or keys
db._internalCreateReadStream = function (options, ..) {
  if (options.keys === false) // ..
}

// Always yields entries
db.createReadStream = function (..) {
  return this._internalCreateReadStream(..)
}

// Always yields values
db.createValueStream = function (..) {
  return this._internalCreateReadStream({ keys: false })
}

@vweevers
Copy link
Member Author

vweevers commented Oct 6, 2019

Although if you swap the layers:

.use(encode, { valueEncoding: 'json' })
.use(getAllPlugin)

Then the problem disappears, because the db.iterator() used by getAll() already does the encoding & decoding. And the manifest doesn't have to describe the signature of getAll().

@vweevers
Copy link
Member Author

vweevers commented Oct 6, 2019

If we want dynamic return types like described above, then the manifest would also have to be dynamic:

{
  args: ['options?', 'callback'],
  yields (options) {
    if (options.keys === false) return ['value']
    if (options.values === false) return ['key']
    return ['entry']
  }
}

Which is not impossible, but...:

Click to expand encoding-down example
function EncodingDown (db, opts) {
  AbstractLevelDOWN.call(this, db.supports)

  // ..

  Object.keys(this.supports.additionalMethods).forEach((m) => {
    var signature = this.supports.additionalMethods[m]

    this[m] = function () {
      var i = Math.min(arguments.length, signature.args.length)
      var args = []
      var opts
      var callback

      while (i--) {
        if (signature.args[i] === 'callback') {
          callback = arguments[i]
        } else if (signature.args[i] === 'options' && !opts && isOptions(arguments[i])) {
          opts = arguments[i]
          args.unshift(this.codec.encodeLtgt(opts))
        } else if (signature.args[i] === 'key') {
          args.unshift(this.codec.encodeKey(arguments[i], opts))
        } else {
          args.unshift(arguments[i])
        }
      }

      const resultType = signature.yields(opts)
      const decode = (result, type) => {
        if (type === 'key') {
          return this.codec.decodeKey(result, opts)
        } else if (type === 'value') {
          return this.codec.decodeValue(result, opts)
        } else if (type === 'entry') {
          return {
            key: this.codec.decodeKey(result.key, opts),
            value: this.codec.decodeValue(result.value, opts)
          }
        } else {
          return result
        }
      }

      args.push(function (err, result) {
        if (err) return callback(err)

        if (Array.isArray(resultType)) {
          result = result.map((r, i) => decode(r, resultType[i]))
        } else {
          result = decode(result, resultType)
        }

        callback(null, result)
      })

      return this.db[m].apply(this.db, args)
    }
  })
}

I don't want to go there. For situations like this we should instead write some abstraction like hooks or perhaps _deserialize* (symmetric with _serialize*) to process keys, values or entries.

@vweevers
Copy link
Member Author

vweevers commented Oct 6, 2019

@ralphtheninja TLDR, I don't want args or return types in the manifest for now. And maybe never.

I'll update Level/encoding-down#93 to simply do DB.prototype.approximateSize = .. like before.

vweevers added a commit to Level/encoding-down that referenced this issue Oct 6, 2019
@ralphtheninja
Copy link
Member

@ralphtheninja TLDR, I don't want args or return types in the manifest for now. And maybe never.

It's a fine line when to stop engineering stuff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion Discussion
Projects
None yet
Development

No branches or pull requests

2 participants