diff --git a/.babelrc b/.babelrc index d55ba80..f8e1bc4 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,10 @@ { + "plugins": [ + "transform-flow-strip-types" + ], "presets": [ "es2015", "stage-0" - ] + ], + "retainLines": true } diff --git a/.eslintrc b/.eslintrc index 96e8507..8520976 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,10 +4,19 @@ "mocha": true, }, "extends": "airbnb-base", + "parser": "babel-eslint", + "plugins": [ + "flowtype" + ], "rules": { "import/no-extraneous-dependencies": ["error", { "devDependencies": ["**/*Spec.js"] }], "indent": [2, 4], + }, + "settings": { + "flowtype": { + "onlyFilesWithFlowAnnotation": true + } } } diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 0000000..5c5395b --- /dev/null +++ b/.flowconfig @@ -0,0 +1,2 @@ +[options] +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue diff --git a/package.json b/package.json index a3948fb..f90a8dc 100644 --- a/package.json +++ b/package.json @@ -39,12 +39,16 @@ "devDependencies": { "babel-cli": "6.22.2", "babel-core": "6.22.1", + "babel-eslint": "7.1.1", + "babel-plugin-transform-flow-strip-types": "6.22.0", "babel-preset-es2015": "6.22.0", "babel-preset-stage-0": "6.22.0", "eslint": "3.14.1", "eslint-config-airbnb-base": "11.0.1", + "eslint-plugin-flowtype": "2.30.3", "eslint-plugin-import": "2.2.0", "expect": "1.20.2", + "flow-bin": "0.41.0", "mocha": "3.2.0" } } diff --git a/src/applyPatch.js b/src/applyPatch.js index 3f1633f..76809c7 100644 --- a/src/applyPatch.js +++ b/src/applyPatch.js @@ -1,3 +1,4 @@ +// @flow import cloneDeep from 'lodash.clonedeep'; import get from 'lodash.get'; import has from 'lodash.has'; @@ -5,10 +6,14 @@ import set from 'lodash.set'; import unset from 'lodash.unset'; import createRejecter from './createRejecter'; -export default function applyPatch(patch, data, resolver) { +import type { Patch } from './createPatch'; + +export type Resolver = (payload: Object, value: mixed, reject: () => void) => void; + +export default function applyPatch(patch: Patch, data: Object, resolver: Resolver) { const output = cloneDeep(data); - patch.forEach((payload) => { + patch.forEach((payload: Object) : void => { const path = payload.path.slice(1).split('/'); switch (payload.op) { diff --git a/src/buildMergePatch.js b/src/buildMergePatch.js index 3f61919..40e8ce8 100644 --- a/src/buildMergePatch.js +++ b/src/buildMergePatch.js @@ -1,19 +1,25 @@ +// @flow import createRejecter from './createRejecter'; -export default function buildMergePatch(currentBranchPatch, targetBranchPatch, resolver) { - const currentBranchPatchByPaths = currentBranchPatch.reduce((indexed, payload) => ({ - ...indexed, - [payload.path]: payload, - }), {}); +export default function buildMergePatch( + currentBranchPatch: Object, + targetBranchPatch: Object, + resolver: ?Function, +) { + const currentBranchPatchByPaths: Object = currentBranchPatch.reduce( + (indexed: Object, payload: Object) => ({ + ...indexed, + [payload.path]: payload, + }), {}); - return targetBranchPatch.filter((payload) => { - const conflictPayload = currentBranchPatchByPaths[payload.path]; + return targetBranchPatch.filter((payload: Object) => { + const conflictPayload: Object = currentBranchPatchByPaths[payload.path]; if (!resolver || !conflictPayload) { return true; } - const rejecter = createRejecter(); + const rejecter: Object = createRejecter(); resolver(payload, conflictPayload, rejecter.reject); return !rejecter.rejected; diff --git a/src/computeHash.js b/src/computeHash.js index 12694be..4f16169 100644 --- a/src/computeHash.js +++ b/src/computeHash.js @@ -1,11 +1,13 @@ +// @flow /* eslint-disable no-bitwise */ import Rusha from 'rusha'; const rusha = new Rusha(); -export const EMPTY_HASH = '0000000000000000000000000000000000000000'; +export type Hash = string; +export const EMPTY_HASH: Hash = '0000000000000000000000000000000000000000'; -export default function computeHash(input) { +export default function computeHash(input: mixed): Hash { const hash = rusha.digest(typeof input === 'object' ? JSON.stringify(input) : input); return hash; } diff --git a/src/createCompressedHashStore.js b/src/createCompressedHashStore.js new file mode 100644 index 0000000..5af5b76 --- /dev/null +++ b/src/createCompressedHashStore.js @@ -0,0 +1,42 @@ +// @flow +import memoize from 'lodash.memoize'; +import type { HashStore } from './createHashStore'; +import type { Hash } from './computeHash'; +import compressObject from './compressObject'; +import expandObject from './expandObject'; + +export type CompressedHashStore = HashStore; + +export default function createCompressedHashStore(store: HashStore): CompressedHashStore { + const { + write, + ...baseStore + }: { + write: (data: Object) => Hash, + } = store; + + const compressedHashStore = { + ...baseStore, + write(data: Object, refHash: Hash): string { + if (!refHash) { + return write(data); + } + + const parentData = compressedHashStore.read(refHash); + + if (typeof data !== typeof parentData) { + return write(data); + } + + return write(compressObject(parentData, data, `$$ref:${refHash}`)); + }, + + read(hash) { + return expandObject(store.read(hash), compressedHashStore); + }, + }; + + compressedHashStore.read = memoize(compressedHashStore.read); + + return compressedHashStore; +} diff --git a/src/createCompressedStoreSpec.js b/src/createCompressedHashStoreSpec.js similarity index 78% rename from src/createCompressedStoreSpec.js rename to src/createCompressedHashStoreSpec.js index 7ee0e1d..a85d642 100644 --- a/src/createCompressedStoreSpec.js +++ b/src/createCompressedHashStoreSpec.js @@ -1,9 +1,9 @@ import expect from 'expect'; -import createCompressedStore from './createCompressedStore'; +import createCompressedHashStore from './createCompressedHashStore'; -describe('createCompressedStore()', () => { +describe('createCompressedHashStore()', () => { let store; - let compressedStore; + let compressedHashStore; beforeEach(() => { store = { @@ -12,7 +12,7 @@ describe('createCompressedStore()', () => { toJSON: expect.createSpy(), write: expect.createSpy(), }; - compressedStore = createCompressedStore(store); + compressedHashStore = createCompressedHashStore(store); }); it('should first compress data and then give it to its store when write() is called', () => { @@ -21,7 +21,7 @@ describe('createCompressedStore()', () => { }); store.write.andReturn('b4a'); - const hash = compressedStore.write({ + const hash = compressedHashStore.write({ foo: 'bar', foo2: 'bar2', }, 'ae3'); @@ -48,7 +48,7 @@ describe('createCompressedStore()', () => { }; }).andCallThrough(); - const data = compressedStore.read('b4a'); + const data = compressedHashStore.read('b4a'); expect(store.read).toHaveBeenCalledWith('b4a'); expect(store.read).toHaveBeenCalledWith('ae3'); @@ -61,12 +61,12 @@ describe('createCompressedStore()', () => { it('should call store.keys() when keys() is called', () => { store.keys.andReturn(['foo']); - expect(compressedStore.keys()).toEqual(['foo']); + expect(compressedHashStore.keys()).toEqual(['foo']); }); it('should call store.toJSON() when toJSON() is called', () => { store.toJSON.andReturn(['foo']); - expect(compressedStore.toJSON()).toEqual(['foo']); + expect(compressedHashStore.toJSON()).toEqual(['foo']); }); }); diff --git a/src/createCompressedStore.js b/src/createCompressedStore.js deleted file mode 100644 index 8010a8d..0000000 --- a/src/createCompressedStore.js +++ /dev/null @@ -1,45 +0,0 @@ -import memoize from 'lodash.memoize'; -import compressObject from './compressObject'; -import expandObject from './expandObject'; - -export default function createCompressedStore(store) { - const compressedStore = { - keys() { - return store.keys(); - }, - - write(data, refHash) { - if (!refHash) { - return store.write(data); - } - - const parentData = compressedStore.read(refHash); - - if (typeof data !== typeof parentData) { - return store.write(data); - } - - return store.write(compressObject(parentData, data, `$$ref:${refHash}`)); - }, - - read(hash) { - return expandObject(store.read(hash), compressedStore); - }, - - subscribe(subscriber) { - return store.subscribe(subscriber); - }, - - toJSON() { - return store.toJSON(); - }, - - unsubscribe(subscriber) { - return store.unsubscribe(subscriber); - }, - }; - - compressedStore.read = memoize(compressedStore.read); - - return compressedStore; -} diff --git a/src/createHashStore.js b/src/createHashStore.js new file mode 100644 index 0000000..36cc6e7 --- /dev/null +++ b/src/createHashStore.js @@ -0,0 +1,24 @@ +// @flow +import computeHash from './computeHash'; +import type { Hash } from './computeHash'; +import type { Store } from './createStore'; + +export type HashStore = Store; + +export default function createHashStore(store: Store): Store { + const { + write, + ...baseStore + } = store; + + return { + ...baseStore, + write(data: Object): Hash { + const hash: Hash = computeHash(data); + write(hash, data); + + return hash; + }, + + }; +} diff --git a/src/createPatch.js b/src/createPatch.js index 83b2a41..3883ded 100644 --- a/src/createPatch.js +++ b/src/createPatch.js @@ -1,13 +1,22 @@ +// @flow import get from 'lodash.get'; import has from 'lodash.has'; import forEachDeep from './forEachDeep'; -export default function createPatch(left, right) { +type Operation = + { op: 'add', path: string, value: any } | + { op: 'replace', path: string, value: any } | + { op: 'remove', path: string } + ; + +export type Patch = Array; + +export default function createPatch(left: Object, right: Object) { const paths = []; const patch = []; - forEachDeep(right, (value, key, path) => { - const absolutePath = `/${path.join('/')}`; + forEachDeep(right, (value: any, key: string, path: Array) => { + const absolutePath: string = `/${path.join('/')}`; paths.push(absolutePath); if (typeof value === 'object') { @@ -34,8 +43,8 @@ export default function createPatch(left, right) { }); }); - forEachDeep(left, (value, key, path) => { - const absolutePath = `/${path.join('/')}`; + forEachDeep(left, (value: any, key: string, path: Array) => { + const absolutePath: string = `/${path.join('/')}`; if (paths.includes(absolutePath)) { return; @@ -47,7 +56,7 @@ export default function createPatch(left, right) { }); }); - return patch.sort((a, b) => { + return patch.sort((a: Operation, b: Operation) => { if (a.op < b.op) { return -1; } diff --git a/src/createRejecter.js b/src/createRejecter.js index 67d589a..1eb095a 100644 --- a/src/createRejecter.js +++ b/src/createRejecter.js @@ -1,12 +1,14 @@ +// @flow export default function createRejecter() { - let rejected = false; + let rejected: boolean = false; return { + // $FlowIssue - get/set properties not yet supported get rejected() { return rejected; }, - reject() { + reject(): void { rejected = true; }, }; diff --git a/src/createRepository.js b/src/createRepository.js index a2d232d..73073ec 100644 --- a/src/createRepository.js +++ b/src/createRepository.js @@ -1,21 +1,40 @@ -import { EventEmitter } from 'events'; +// @flow +import events from 'events'; import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; import applyPatch from './applyPatch'; import buildMergePatch from './buildMergePatch'; import createPatch from './createPatch'; import createStore from './createStore'; -import createCompressedStore from './createCompressedStore'; +import createHashStore from './createHashStore'; +import createCompressedHashStore from './createCompressedHashStore'; import ensureSnapshot from './ensureSnapshot'; import findReferenceCommitHash from './findReferenceCommitHash'; import { EMPTY_HASH } from './computeHash'; -export default function createRepository(snapshot) { - const emitter = new EventEmitter(); - - let commits = {}; - let trees = {}; - let refs = { +import type { CompressedHashStore } from './createCompressedHashStore'; +import type { Hash } from './computeHash'; +import type { HashStore } from './createHashStore'; +import type { Patch } from './createPatch'; +import type { Snapshot } from './ensureSnapshot'; +import type { Storage, Store } from './createStore'; + +type Commit = { + author: string, + date: string, + message: string, + treeHash: Hash, + parent: Hash, +}; +type Resolver = (payload: Object, value: mixed, reject: () => void) => void; +type Subscriber = (payload: Object) => void; + +export default function createRepository(snapshot: Snapshot): Object { + const emitter = new events.EventEmitter(); + + let commits: Object = {}; + let trees: Object = {}; + let refs: Object = { branch: { value: 'master', }, @@ -33,64 +52,98 @@ export default function createRepository(snapshot) { }; } - const commitStore = createStore(commits); - const refStore = createStore(refs); - const treeStore = createCompressedStore(createStore(trees)); + const commitStore: HashStore = createHashStore(createStore(commits)); + const refStore: Store = createStore(refs); + const treeStore: CompressedHashStore = createCompressedHashStore(createHashStore(createStore(trees))); - function getCurrentBranch() { + function getCurrentBranch(): string { return refStore.read('branch').value; } - function moveHead(branch, commitHash) { - const previousHeads = refStore.read('heads'); - refStore.write({ + function moveDetachedHead(commitHash: Hash): void { + refStore.write('detached', { + head: commitHash, + }); + } + + function moveHead(branch: string, commitHash: Hash): void { + const previousHeads: Object = refStore.read('heads'); + refStore.write('heads', { ...previousHeads, [branch]: commitHash, - }, 'heads'); + }); } - function hasHead(branch) { + function hasDetachedHead(): boolean { + return refStore.has('detached'); + } + + function hasHead(branch: string): boolean { return !!refStore.read('heads')[branch]; } - function getHead(branch) { + function getDetachedHead(): Hash { + return refStore.read('detached').head; + } + + function getHead(branch: string): Hash { return refStore.read('heads')[branch]; } - function removeHead(branch) { + function removeDetachedHead(): void { + refStore.remove('detached'); + } + + function removeHead(branch: string): void { const previousHeads = refStore.read('heads'); delete previousHeads[branch]; - refStore.write({ ...previousHeads }, 'heads'); + refStore.write('heads', { ...previousHeads }); } - function updateBranch(branch) { - refStore.write({ + function updateBranch(branch: string): void { + refStore.write('branch', { value: branch, - }, 'branch'); + }); } - refStore.subscribe(() => emitter.emit('write', { - head: getHead(getCurrentBranch()), - })); + refStore.subscribe(() => { + emitter.emit('write', { + head: hasDetachedHead() ? getDetachedHead() : getHead(getCurrentBranch()), + }); + }); const repository = { + // $FlowIssue - get/set properties not yet supported get branch() { + if (hasDetachedHead()) { + throw new Error(`You are in detached mode on ${getDetachedHead()} `); + } + return getCurrentBranch(); }, + // $FlowIssue - get/set properties not yet supported get branches() { return Object.keys(refStore.read('heads')); }, + // $FlowIssue - get/set properties not yet supported + get detached() { + return hasDetachedHead(); + }, + + // $FlowIssue - get/set properties not yet supported get head() { - return getHead(repository.branch); + return hasDetachedHead() ? getDetachedHead() : getHead(repository.branch); }, - get log() { + // $FlowIssue - get/set properties not yet supported + get log(): Storage { return commitStore.toJSON(); }, + // $FlowIssue - get/set properties not yet supported get tree() { if (repository.head === EMPTY_HASH) { throw new Error("There isn't a tree yet. You must do your first commit for that."); @@ -100,11 +153,12 @@ export default function createRepository(snapshot) { return cloneDeep(treeStore.read(commit.treeHash)); }, - apply(patch, resolver) { + apply(patch: Patch, resolver: ?Function): Object { + // $FlowIssue - get/set properties not yet supported return applyPatch(patch, repository.tree, resolver); }, - commit(author, message, tree) { + commit(author: string, message: string, tree: Object): Hash { if (typeof author !== 'string' || author.length === 0) { throw new Error('Author is mandatory'); } @@ -113,8 +167,9 @@ export default function createRepository(snapshot) { throw new Error('Message is mandatory'); } - let lastTreeHash = null; + let lastTreeHash: ?Hash = null; if (repository.head !== EMPTY_HASH) { + // $FlowIssue - get/set properties not yet supported lastTreeHash = commitStore.read(repository.head).treeHash; if (!tree || isEqual(treeStore.read(lastTreeHash), tree)) { @@ -122,37 +177,52 @@ export default function createRepository(snapshot) { } } - const treeHash = treeStore.write(cloneDeep(tree), lastTreeHash); - const commitHash = commitStore.write({ + const treeHash: Hash = treeStore.write(cloneDeep(tree), lastTreeHash); + const commitHash: Hash = commitStore.write({ author, date: (new Date()).toISOString(), message, treeHash, + // $FlowIssue - get/set properties not yet supported parent: repository.head, }); - moveHead(repository.branch, commitHash); + if (hasDetachedHead()) { + moveDetachedHead(commitHash); + } else { + // $FlowIssue - get/set properties not yet supported + moveHead(repository.branch, commitHash); + } return commitHash; }, - checkout(nextBranch, create = false) { + checkout(target: string | Hash, create: boolean = false): void { + if (commitStore.has(target)) { + moveDetachedHead(target); + + return; + } + if (create) { - if (hasHead(nextBranch)) { - throw new Error(`Branch ${nextBranch} already exists.`); + if (hasHead(target)) { + throw new Error(`Branch ${target} already exists.`); } - moveHead(nextBranch, repository.head); - } else if (!hasHead(nextBranch)) { - throw new Error(`Branch ${nextBranch} does not exists.`); + // $FlowIssue - get/set properties not yet supported + moveHead(target, repository.head); + } else if (!hasHead(target)) { + throw new Error(`Branch ${target} does not exists.`); } - updateBranch(nextBranch); + updateBranch(target); - return repository; + if (hasDetachedHead()) { + removeDetachedHead(); + } }, - deleteBranch(branch) { + deleteBranch(branch: string): void { if (!hasHead(branch)) { throw new Error(`Branch ${branch} doesn't exist`); } @@ -168,9 +238,20 @@ export default function createRepository(snapshot) { removeHead(branch); }, - diff(left, right) { - const leftCommit = commitStore.read(getHead(left) || left); - const rightCommit = commitStore.read(getHead(right) || right); + diff(left: string | Hash, right: string | Hash): Patch { + if (!commitStore.has(left) && !hasHead(left)) { + throw new Error(`Branch ${left} doesn't exist`); + } + + if (!commitStore.has(right) && !hasHead(right)) { + throw new Error(`Branch ${right} doesn't exist`); + } + + const leftHead: Hash = commitStore.has(left) ? left : getHead(left); + const rightHead: Hash = commitStore.has(right) ? right : getHead(right); + + const leftCommit: Commit = commitStore.read(leftHead); + const rightCommit: Commit = commitStore.read(rightHead); return createPatch( treeStore.read(leftCommit.treeHash), @@ -178,42 +259,48 @@ export default function createRepository(snapshot) { ); }, - merge(author, branch, resolver) { - if (!hasHead(branch)) { - throw new Error(`Branch ${branch} doesn't exist`); + merge(author: string, target: string | Hash, resolver: ?Resolver): Hash { + if (!commitStore.has(target) && !hasHead(target)) { + throw new Error(`Branch ${target} doesn't exist`); } - const refCommitHash = findReferenceCommitHash( + const targetHead: Hash = commitStore.has(target) ? target : getHead(target); + + const refCommitHash: Hash = findReferenceCommitHash( + // $FlowIssue - get/set properties not yet supported repository.head, - getHead(branch), + targetHead, commitStore, ); - const mergePatch = buildMergePatch( + const mergePatch: Patch = buildMergePatch( + // $FlowIssue - get/set properties not yet supported repository.diff(refCommitHash, repository.head), - repository.diff(refCommitHash, getHead(branch)), + // $FlowIssue - get/set properties not yet supported + repository.diff(refCommitHash, targetHead), resolver, ); return repository.commit( author, - `Merge of ${branch} into ${repository.branch}`, + // $FlowIssue - get/set properties not yet supported + `Merge of ${target} into ${repository.branch}`, repository.apply(mergePatch), ); }, - revert(author, commitHash, resolver) { + revert(author: string, commitHash: Hash, resolver: ?Resolver): Hash { if (typeof author !== 'string' || author.length === 0) { throw new Error('Author is mandatory'); } - const commit = commitStore.read(commitHash); + const commit: Commit = commitStore.read(commitHash); if (commit.parent === EMPTY_HASH) { throw new Error("You can't revert the first commit."); } - const patch = repository.diff(commitHash, commit.parent); + const patch: Patch = repository.diff(commitHash, commit.parent); return repository.commit( author, @@ -222,15 +309,15 @@ export default function createRepository(snapshot) { ); }, - subscribe(subscriber) { + subscribe(subscriber: Subscriber): void { emitter.on('write', subscriber); }, - unsubscribe(subscriber) { + unsubscribe(subscriber: Subscriber): void { emitter.removeListener('write', subscriber); }, - toJSON() { + toJSON(): Snapshot { return { commits: commitStore.toJSON(), refs: refStore.toJSON(), diff --git a/src/createRepositorySpec.js b/src/createRepositorySpec.js index 8847f6a..315595e 100644 --- a/src/createRepositorySpec.js +++ b/src/createRepositorySpec.js @@ -100,6 +100,70 @@ describe('createRepository()', () => { expect(repository.log[lastCommitHash].parent).toEqual(firstCommitHash); }); + it('should expose a checkout() method to checkout a commit in detached mode', () => { + repository.commit('robin', 'I commit', { + foo: 'bar', + child: { + bar: 'foo', + }, + }); + + const secondCommitHash = repository.commit('robin', 'I commit', { + foo: 'bar2', + child: { + bar: 'foo2', + }, + }); + + const thirdCommitHash = repository.commit('robin', 'I commit', { + foo: 'bar3', + child: { + bar: 'foo3', + }, + }); + + repository.checkout(secondCommitHash); + + expect(repository.tree).toEqual({ + foo: 'bar2', + child: { + bar: 'foo2', + }, + }); + expect(repository.head).toBe(secondCommitHash); + expect(repository.detached).toBe(true); + + const fourthCommitHash = repository.commit('robin', 'I commit', { + foo: 'bar4', + child: { + bar: 'foo4', + }, + }); + + expect(repository.log[fourthCommitHash].parent).toEqual(secondCommitHash); + + repository.checkout('master'); + + expect(repository.branch).toBe('master'); + expect(repository.detached).toBe(false); + expect(repository.tree).toEqual({ + foo: 'bar3', + child: { + bar: 'foo3', + }, + }); + expect(repository.head).toBe(thirdCommitHash); + + const lastCommitHash = repository.commit('robin', 'I commit', { + foo: 'bar4', + child: { + bar: 'foo4', + }, + }); + + expect(repository.log[lastCommitHash].parent).toEqual(thirdCommitHash); + }); + it('should trigger an error when trying to checkout an unknown branch', () => { expect(() => repository.checkout('dev')).toThrow(/Branch dev does not exists./); }); diff --git a/src/createStore.js b/src/createStore.js index a71f36d..eedfe06 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -1,43 +1,58 @@ -import { EventEmitter } from 'events'; -import computeHash from './computeHash'; +// @flow +import events from 'events'; -export default function createStore(snapshot = {}) { - const emitter = new EventEmitter(); - const storage = snapshot; +export type StoreSubscriber = (key: string) => void; +export type Storage = { + [k: string]: Object, +}; +export type Store = Object; + +export default function createStore(snapshot: Storage): Store { + const emitter = new events.EventEmitter(); + const storage: Storage = snapshot || {}; const store = { - keys() { - return Object.keys(storage); + has(key: string): boolean { + return !!storage[key]; }, - write(data, forcedHash) { - const hash = forcedHash || computeHash(data); - storage[hash] = data; + keys(): Array { + return Object.keys(storage); + }, - emitter.emit('write', hash); + write(key: string, data: Object): void { + storage[key] = data; - return hash; + emitter.emit('write', key); }, - read(hash) { - const entry = storage[hash]; + read(key: string): Object { + const entry = storage[key]; if (!entry) { - throw new Error(`Entry ${hash} not found.`); + throw new Error(`Entry ${key} not found.`); } return { ...entry }; }, - subscribe(subscriber) { + remove(key: string): void { + if (!store.has(key)) { + throw new Error(`Entry ${key} not found.`); + } + + delete storage[key]; + }, + + subscribe(subscriber: StoreSubscriber): void { emitter.on('write', subscriber); }, - toJSON() { + toJSON(): Object { return { ...storage }; }, - unsubscribe(subscriber) { + unsubscribe(subscriber: StoreSubscriber): void { emitter.removeListener('write', subscriber); }, }; diff --git a/src/createStoreSpec.js b/src/createStoreSpec.js index 70efe2e..b683224 100644 --- a/src/createStoreSpec.js +++ b/src/createStoreSpec.js @@ -8,40 +8,40 @@ describe('createStore()', () => { store = createStore(); }); - it('should write data and return its key when write() is called', () => { - const hash = store.write({ hello: 'world' }); - expect(hash).toBe(store.keys()[0]); + it('should write data when write() is called', () => { + store.write('foo', { hello: 'world' }); + expect('foo').toBe(store.keys()[0]); }); it('should return data when read() is called with a valid key', () => { - const hash = store.write({ hello: 'world' }); - expect(store.read(hash)).toEqual({ hello: 'world' }); + store.write('foo', { hello: 'world' }); + expect(store.read('foo')).toEqual({ hello: 'world' }); }); it('should throw an error when read() is called with an invalid key', () => { - store.write({ hello: 'world' }); + store.write('foo', { hello: 'world' }); expect(() => store.read('wrong')).toThrow(/Entry wrong not found/); }); it('should return all keys when keys() is called', () => { - const hash1 = store.write({ hello: 'world' }); - const hash2 = store.write({ hello2: 'world2' }); + store.write('foo', { hello: 'world' }); + store.write('bar', { hello2: 'world2' }); expect(store.keys()).toEqual([ - hash1, - hash2, + 'foo', + 'bar', ]); }); it('should return all store content when toJSON() is called', () => { - const hash1 = store.write({ hello: 'world' }); - const hash2 = store.write({ hello2: 'world2' }); + store.write('foo', { hello: 'world' }); + store.write('bar', { hello2: 'world2' }); expect(store.toJSON()).toEqual({ - [hash1]: { + foo: { hello: 'world', }, - [hash2]: { + bar: { hello2: 'world2', }, }); @@ -59,13 +59,6 @@ describe('createStore()', () => { }); }); - it('should write data with the given hash if provided', () => { - const hash = store.write({ hello: 'world' }, 'forcedHash'); - expect(hash).toBe('forcedHash'); - expect(store.keys()[0]).toBe('forcedHash'); - expect(store.read('forcedHash')).toEqual({ hello: 'world' }); - }); - it('should notify any subscriber when something is written into the store', () => { const subscriber1 = expect.createSpy(); store.subscribe(subscriber1); @@ -73,15 +66,21 @@ describe('createStore()', () => { const subscriber2 = expect.createSpy(); store.subscribe(subscriber2); - const hash = store.write({ hello: 'world' }); - expect(subscriber1).toHaveBeenCalledWith(hash); - expect(subscriber2).toHaveBeenCalledWith(hash); + store.write('foo', { hello: 'world' }); + expect(subscriber1).toHaveBeenCalledWith('foo'); + expect(subscriber2).toHaveBeenCalledWith('foo'); store.unsubscribe(subscriber1); - const hash2 = store.write({ hello: 'earth' }); + store.write('bar', { hello: 'earth' }); expect(subscriber1.calls.length).toBe(1); - expect(subscriber2).toHaveBeenCalledWith(hash2); + expect(subscriber2).toHaveBeenCalledWith('bar'); expect(subscriber2.calls.length).toBe(2); }); + + it('should test if a hash exists when has() is called', () => { + store.write('foo', { hello: 'world' }); + expect(store.has('foo')).toBe(true); + expect(store.has('bar')).toBe(false); + }); }); diff --git a/src/ensureSnapshot.js b/src/ensureSnapshot.js index ec4c69c..1f4ec04 100644 --- a/src/ensureSnapshot.js +++ b/src/ensureSnapshot.js @@ -1,4 +1,26 @@ -export default function ensureSnapshot(snapshot) { +// @flow +import type { Hash } from './computeHash'; +import type { Storage } from './createStore'; + +type Refs = { + branch: { + value: string, + }, + detached?: { + head: Hash, + }, + heads: { + [k: string]: Hash, + } +}; + +export type Snapshot = { + refs: Refs, + commits: Storage, + trees: Storage, +}; + +export default function ensureSnapshot(snapshot: Snapshot): boolean { if (!snapshot.refs) { return false; } diff --git a/src/expandObject.js b/src/expandObject.js index 631ee35..31417bb 100644 --- a/src/expandObject.js +++ b/src/expandObject.js @@ -1,26 +1,29 @@ +// @flow import cloneDeep from 'lodash.clonedeep'; import set from 'lodash.set'; import get from 'lodash.get'; import memoize from 'lodash.memoize'; import forEachDeep from './forEachDeep'; -export default function expandObject(object, compressedStore) { - const output = cloneDeep(object); - const read = memoize(compressedStore.read); +import type { Hash } from './computeHash'; - forEachDeep(output, (value, key, path) => { +export default function expandObject(object: Object, compressedStore: Object): Object { + const output: Object = cloneDeep(object); + const read: (key: string) => Object = memoize(compressedStore.read); + + forEachDeep(output, (value: mixed, key: string, path: string) => { if (typeof value !== 'string') { return; } - const matches = value.match(/\$\$ref:(.+)/); + const matches: ?Array = value.match(/\$\$ref:(.+)/); if (!matches) { return; } - const refHash = matches[1]; - const refValue = get(read(refHash), path); + const refHash: Hash = matches[1]; + const refValue: mixed = get(read(refHash), path); set(output, path, refValue); }); diff --git a/src/findReferenceCommitHash.js b/src/findReferenceCommitHash.js index a33e4bb..e14f165 100644 --- a/src/findReferenceCommitHash.js +++ b/src/findReferenceCommitHash.js @@ -1,7 +1,11 @@ +// @flow import intersection from 'lodash.intersection'; import { EMPTY_HASH } from './computeHash'; -function buildHistory(head, commitStore) { +import type { Hash } from './computeHash'; +import type { HashStore } from './createHashStore'; + +function buildHistory(head: Hash, commitStore: HashStore) { const history = []; let currentHead = head; @@ -13,7 +17,11 @@ function buildHistory(head, commitStore) { return history; } -export default function findReferenceCommitHash(leftHead, rightHead, commitStore) { +export default function findReferenceCommitHash( + leftHead: Hash, + rightHead: Hash, + commitStore: HashStore, +): Hash { return intersection( buildHistory(leftHead, commitStore), buildHistory(rightHead, commitStore), diff --git a/yarn.lock b/yarn.lock index 1cef3d3..a8b7ed8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -176,6 +176,16 @@ babel-core@6.22.1, babel-core@^6.22.0, babel-core@^6.22.1: slash "^1.0.0" source-map "^0.5.0" +babel-eslint@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.1.1.tgz#8a6a884f085aa7060af69cfc77341c2f99370fb2" + dependencies: + babel-code-frame "^6.16.0" + babel-traverse "^6.15.0" + babel-types "^6.15.0" + babylon "^6.13.0" + lodash.pickby "^4.6.0" + babel-generator@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.22.0.tgz#d642bf4961911a8adc7c692b0c9297f325cda805" @@ -354,6 +364,10 @@ babel-plugin-syntax-export-extensions@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721" +babel-plugin-syntax-flow@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d" + babel-plugin-syntax-function-bind@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz#48c495f177bdf31a981e732f55adc0bdd2601f46" @@ -599,6 +613,13 @@ babel-plugin-transform-export-extensions@^6.22.0: babel-plugin-syntax-export-extensions "^6.8.0" babel-runtime "^6.22.0" +babel-plugin-transform-flow-strip-types@6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf" + dependencies: + babel-plugin-syntax-flow "^6.18.0" + babel-runtime "^6.22.0" + babel-plugin-transform-function-bind@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz#c6fb8e96ac296a310b8cf8ea401462407ddf6a97" @@ -727,7 +748,7 @@ babel-template@^6.22.0: babylon "^6.11.0" lodash "^4.2.0" -babel-traverse@^6.22.0, babel-traverse@^6.22.1: +babel-traverse@^6.15.0, babel-traverse@^6.22.0, babel-traverse@^6.22.1: version "6.22.1" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.22.1.tgz#3b95cd6b7427d6f1f757704908f2fc9748a5f59f" dependencies: @@ -741,7 +762,7 @@ babel-traverse@^6.22.0, babel-traverse@^6.22.1: invariant "^2.2.0" lodash "^4.2.0" -babel-types@^6.19.0, babel-types@^6.22.0: +babel-types@^6.15.0, babel-types@^6.19.0, babel-types@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.22.0.tgz#2a447e8d0ea25d2512409e4175479fd78cc8b1db" dependencies: @@ -750,7 +771,7 @@ babel-types@^6.19.0, babel-types@^6.22.0: lodash "^4.2.0" to-fast-properties "^1.0.1" -babylon@^6.11.0, babylon@^6.15.0: +babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0: version "6.15.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e" @@ -1101,6 +1122,12 @@ eslint-module-utils@^2.0.0: debug "2.2.0" pkg-dir "^1.0.0" +eslint-plugin-flowtype@2.30.3: + version "2.30.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.30.3.tgz#57835d2c0ed388da7a2725803ec32af2f437c301" + dependencies: + lodash "^4.15.0" + eslint-plugin-import@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.2.0.tgz#72ba306fad305d67c4816348a4699a4229ac8b4e" @@ -1282,6 +1309,10 @@ flat-cache@^1.2.1: graceful-fs "^4.1.2" write "^0.2.1" +flow-bin@0.41.0: + version "0.41.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.41.0.tgz#8badac9a19da45004997e599bd316518db489b2e" + for-in@^0.1.5: version "0.1.6" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8" @@ -1874,6 +1905,10 @@ lodash.memoize@4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" +lodash.pickby@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz#7dea21d8c18d7703a27c704c15d3b84a67e33aff" + lodash.set@4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" @@ -1886,7 +1921,7 @@ lodash.zipobject@4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/lodash.zipobject/-/lodash.zipobject-4.1.3.tgz#b399f5aba8ff62a746f6979bf20b214f964dbef8" -lodash@^4.0.0, lodash@^4.2.0, lodash@^4.3.0: +lodash@^4.0.0, lodash@^4.15.0, lodash@^4.2.0, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"