Initial Index implementation (see #1).
This commit is contained in:
parent
e6e8ccc855
commit
22caa0ee66
@ -13,6 +13,8 @@ function isNotEmpty (map) {
|
||||
return false
|
||||
}
|
||||
|
||||
const {hasOwnProperty} = Object
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class BufferAlreadyFlushed extends BaseError {
|
||||
@ -21,6 +23,12 @@ export class BufferAlreadyFlushed extends BaseError {
|
||||
}
|
||||
}
|
||||
|
||||
export class DuplicateIndex extends BaseError {
|
||||
constructor (name) {
|
||||
super('there is already an index with the name ' + name)
|
||||
}
|
||||
}
|
||||
|
||||
export class DuplicateItem extends BaseError {
|
||||
constructor (key) {
|
||||
super('there is already a item with the key ' + key)
|
||||
@ -39,6 +47,12 @@ export class InvalidKey extends BaseError {
|
||||
}
|
||||
}
|
||||
|
||||
export class NoSuchIndex extends BaseError {
|
||||
constructor (key) {
|
||||
super('there is no index with the name ' + name)
|
||||
}
|
||||
}
|
||||
|
||||
export class NoSuchItem extends BaseError {
|
||||
constructor (key) {
|
||||
super('there is no item with the key ' + key)
|
||||
@ -53,6 +67,8 @@ export default class Collection extends EventEmitter {
|
||||
|
||||
this._buffer = Object.create(null)
|
||||
this._buffering = 0
|
||||
this._indexes = Object.create(null)
|
||||
this._indexedItems = Object.create(null)
|
||||
this._items = Object.create(null)
|
||||
this._size = 0
|
||||
}
|
||||
@ -73,6 +89,10 @@ export default class Collection extends EventEmitter {
|
||||
return this._items
|
||||
}
|
||||
|
||||
get indexes () {
|
||||
return this._indexedItems
|
||||
}
|
||||
|
||||
get size () {
|
||||
return this._size
|
||||
}
|
||||
@ -157,7 +177,35 @@ export default class Collection extends EventEmitter {
|
||||
}
|
||||
|
||||
has (key) {
|
||||
return Object.hasOwnProperty.call(this._items, key)
|
||||
return hasOwnProperty.call(this._items, key)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Indexes
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
createIndex (name, index) {
|
||||
index._attachCollection(this)
|
||||
|
||||
const {_indexes: indexes} = this
|
||||
if (hasOwnProperty.call(indexes, name)) {
|
||||
throw new DuplicateIndex(name)
|
||||
}
|
||||
indexes[name] = index
|
||||
this._indexedItems[name] = index.itemsByHash
|
||||
}
|
||||
|
||||
deleteIndex (name) {
|
||||
const {_indexes: indexes} = this
|
||||
if (!hasOwnProperty.call(indexes, name)) {
|
||||
throw new NoSuchIndex(name)
|
||||
}
|
||||
|
||||
const index = indexes[name]
|
||||
delete indexes[name]
|
||||
delete this._indexedItems[name]
|
||||
|
||||
index._detachCollection()
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
157
packages/xo-collection/src/index.js
Normal file
157
packages/xo-collection/src/index.js
Normal file
@ -0,0 +1,157 @@
|
||||
import bind from 'lodash.bind'
|
||||
import {BaseError} from 'make-error'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class NotImplemented extends BaseError {
|
||||
constructor (message) {
|
||||
super(message || 'this method is not implemented')
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const clearObject = (object) => {
|
||||
for (let key in object) {
|
||||
delete object[key]
|
||||
}
|
||||
}
|
||||
|
||||
const isEmpty = (object) => {
|
||||
/* eslint no-unused-vars: 0 */
|
||||
for (let key in object) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Index {
|
||||
constructor () {
|
||||
this._itemsByHash = Object.create(null)
|
||||
this._keysToHash = Object.create(null)
|
||||
|
||||
// Bound versions of listeners.
|
||||
this._onAdd = bind(this._onAdd, this)
|
||||
this._onUpdate = bind(this._onUpdate, this)
|
||||
this._onRemove = bind(this._onRemove, this)
|
||||
}
|
||||
|
||||
// This method is used to compute the hash under which an item must
|
||||
// be saved.
|
||||
computeHash (value, key) {
|
||||
throw new NotImplemented('this method must be overridden')
|
||||
}
|
||||
|
||||
// Remove empty items lists.
|
||||
sweep () {
|
||||
const {_itemsByHash: itemsByHash} = this
|
||||
for (let hash in itemsByHash) {
|
||||
if (isEmpty(itemsByHash[hash])) {
|
||||
delete itemsByHash[hash]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
get itemsByHash () {
|
||||
return this._itemsByHash
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
_attachCollection (collection) {
|
||||
// Add existing entries.
|
||||
//
|
||||
// FIXME: I think there may be a race condition if the `add` event
|
||||
// has not been emitted yet.
|
||||
this._onAdd(collection.all)
|
||||
|
||||
collection.on('add', this._onAdd)
|
||||
collection.on('update', this._onUpdate)
|
||||
collection.on('remove', this._onRemove)
|
||||
}
|
||||
|
||||
_detachCollection (collection) {
|
||||
collection.removeListener('add', this._onAdd)
|
||||
collection.removeListener('update', this._onUpdate)
|
||||
collection.removeListener('remove', this._onRemove)
|
||||
|
||||
clearObject(this._hashes)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
_onAdd (items) {
|
||||
const {
|
||||
computeHash,
|
||||
_itemsByHash: itemsByHash,
|
||||
_keysToHash: keysToHash
|
||||
} = this
|
||||
|
||||
for (let key in items) {
|
||||
const value = items[key]
|
||||
|
||||
const hash = computeHash(value, key)
|
||||
|
||||
if (hash != null) {
|
||||
(
|
||||
itemsByHash[hash] ||
|
||||
(itemsByHash[hash] = Object.create(null))
|
||||
)[key] = value
|
||||
|
||||
keysToHash[key] = hash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onUpdate (items) {
|
||||
const {
|
||||
computeHash,
|
||||
_itemsByHash: itemsByHash,
|
||||
_keysToHash: keysToHash
|
||||
} = this
|
||||
|
||||
for (let key in items) {
|
||||
const value = items[key]
|
||||
|
||||
const prev = keysToHash[key]
|
||||
const hash = computeHash(value, key)
|
||||
if (hash === prev) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (prev != null) {
|
||||
delete itemsByHash[prev][key]
|
||||
}
|
||||
|
||||
if (hash != null) {
|
||||
(
|
||||
itemsByHash[hash] ||
|
||||
(itemsByHash[hash] = Object.create(null))
|
||||
)[key] = value
|
||||
|
||||
keysToHash[key] = hash
|
||||
} else {
|
||||
delete keysToHash[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onRemove (items) {
|
||||
const {
|
||||
_itemsByHash: itemsByHash,
|
||||
_keysToHash: keysToHash
|
||||
} = this
|
||||
|
||||
for (let key in items) {
|
||||
const prev = keysToHash[key]
|
||||
if (prev != null) {
|
||||
delete keysToHash[key]
|
||||
delete itemsByHash[prev][key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
165
packages/xo-collection/src/index.spec.js
Normal file
165
packages/xo-collection/src/index.spec.js
Normal file
@ -0,0 +1,165 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import chai, {expect} from 'chai'
|
||||
import dirtyChai from 'dirty-chai'
|
||||
chai.use(dirtyChai)
|
||||
|
||||
import sourceMapSupport from 'source-map-support'
|
||||
sourceMapSupport.install()
|
||||
|
||||
import forEach from 'lodash.foreach'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
import Collection from './collection'
|
||||
import Index from './index'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const waitTicks = (n = 1) => {
|
||||
const {nextTick} = process
|
||||
|
||||
return new Promise(resolve => {
|
||||
(function waitNextTick () {
|
||||
// The first tick is handled by Promise#then()
|
||||
if (--n) {
|
||||
nextTick(waitNextTick)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('Index', function () {
|
||||
let col, byGroup
|
||||
const item1 = {
|
||||
id: '2ccb8a72-dc65-48e4-88fe-45ef541f2cba',
|
||||
group: 'foo'
|
||||
}
|
||||
const item2 = {
|
||||
id: '7d21dc51-4da8-4538-a2e9-dd6f4784eb76',
|
||||
group: 'bar'
|
||||
}
|
||||
const item3 = {
|
||||
id: '668c1274-4442-44a6-b99a-512188e0bb09',
|
||||
group: 'foo'
|
||||
}
|
||||
const item4 = {
|
||||
id: 'd90b7335-e540-4a44-ad22-c4baae9cd0a9'
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
col = new Collection()
|
||||
forEach([item1, item2, item3, item4], item => {
|
||||
col.add(item)
|
||||
})
|
||||
|
||||
byGroup = new Index()
|
||||
byGroup.computeHash = item => item.group
|
||||
|
||||
col.createIndex('byGroup', byGroup)
|
||||
|
||||
return waitTicks()
|
||||
})
|
||||
|
||||
it('works with existing items', function () {
|
||||
expect(col.indexes).to.eql({
|
||||
byGroup: {
|
||||
foo: {
|
||||
[item1.id]: item1,
|
||||
[item3.id]: item3
|
||||
},
|
||||
bar: {
|
||||
[item2.id]: item2
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('works with added items', function () {
|
||||
const item5 = {
|
||||
id: '823b56c4-4b96-4f3a-9533-5d08177167ac',
|
||||
group: 'baz'
|
||||
}
|
||||
|
||||
col.add(item5)
|
||||
|
||||
return waitTicks(2).then(() => {
|
||||
expect(col.indexes).to.eql({
|
||||
byGroup: {
|
||||
foo: {
|
||||
[item1.id]: item1,
|
||||
[item3.id]: item3
|
||||
},
|
||||
bar: {
|
||||
[item2.id]: item2
|
||||
},
|
||||
baz: {
|
||||
[item5.id]: item5
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('works with updated items', function () {
|
||||
const item1bis = {
|
||||
id: item1.id,
|
||||
group: 'bar'
|
||||
}
|
||||
|
||||
col.update(item1bis)
|
||||
|
||||
return waitTicks(2).then(() => {
|
||||
expect(col.indexes).to.eql({
|
||||
byGroup: {
|
||||
foo: {
|
||||
[item3.id]: item3
|
||||
},
|
||||
bar: {
|
||||
[item1.id]: item1bis,
|
||||
[item2.id]: item2
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('works with removed items', function () {
|
||||
col.remove(item2)
|
||||
|
||||
return waitTicks(2).then(() => {
|
||||
expect(col.indexes).to.eql({
|
||||
byGroup: {
|
||||
foo: {
|
||||
[item1.id]: item1,
|
||||
[item3.id]: item3
|
||||
},
|
||||
bar: {}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('#sweep()', function () {
|
||||
it('removes empty items lists', function () {
|
||||
col.remove(item2)
|
||||
|
||||
return waitTicks(2).then(() => {
|
||||
byGroup.sweep()
|
||||
|
||||
expect(col.indexes).to.eql({
|
||||
byGroup: {
|
||||
foo: {
|
||||
[item1.id]: item1,
|
||||
[item3.id]: item3
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user