diff --git a/.flowconfig b/.flowconfig index bc83640d..b3deece7 100644 --- a/.flowconfig +++ b/.flowconfig @@ -16,8 +16,7 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]* suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError -unsafe.enable_getters_and_setters=true munge_underscores=true [version] -^0.60.0 +^0.66.0 diff --git a/package.json b/package.json index f79590c8..971c3995 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "eslint-plugin-prettier": "2.6.0", "eslint-plugin-react": "7.6.1", "eslint-plugin-relay": "0.0.21", - "flow-bin": "^0.60.1", + "flow-bin": "^0.66.0", "glob": "^7.1.1", "istanbul-api": "^1.1.0", "istanbul-lib-coverage": "^1.0.0", diff --git a/packages/metro-cache/src/PersistedMapStore.js b/packages/metro-cache/src/PersistedMapStore.js new file mode 100644 index 00000000..4c44dcd2 --- /dev/null +++ b/packages/metro-cache/src/PersistedMapStore.js @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +const fs = require('fs'); +const serializer = require('jest-serializer'); + +export type Options = {| + path: string, + writeDelay: ?number, +|}; + +class PersistedMapStore { + _map: ?Map; + _path: string; + _store: () => void; + _timeout: ?TimeoutID; + _writeDelay: number; + + constructor(options: Options) { + this._path = options.path; + this._writeDelay = options.writeDelay || 5000; + + this._store = this._store.bind(this); + this._timeout = null; + this._map = null; + } + + get(key: Buffer): mixed { + this._getMap(); + + if (this._map) { + return this._map.get(key.toString('hex')); + } + + return null; + } + + set(key: Buffer, value: mixed) { + this._getMap(); + + if (this._map) { + this._map.set(key.toString('hex'), value); + } + + if (!this._timeout) { + this._timeout = setTimeout(this._store, this._writeDelay); + } + } + + _getMap() { + if (!this._map) { + if (fs.existsSync(this._path)) { + this._map = serializer.readFileSync(this._path); + } else { + this._map = new Map(); + } + } + } + + _store() { + serializer.writeFileSync(this._path, this._map); + this._timeout = null; + } +} + +module.exports = PersistedMapStore; diff --git a/packages/metro-cache/src/__tests__/PersistedMapStore-test.js b/packages/metro-cache/src/__tests__/PersistedMapStore-test.js new file mode 100644 index 00000000..bf9e5597 --- /dev/null +++ b/packages/metro-cache/src/__tests__/PersistedMapStore-test.js @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails oncall+javascript_foundation + * @format + */ + +'use strict'; + +describe('PersistedMapStore', () => { + const key1 = Buffer.from('foo'); + const key2 = Buffer.from('bar'); + let now; + let serializer; + let fs; + let PersistedMapStore; + + function advance(time) { + now += time; + jest.advanceTimersByTime(time); + } + + Date.now = () => now; + + beforeEach(() => { + jest + .resetModules() + .resetAllMocks() + .useFakeTimers(); + + jest.mock('fs', () => ({ + existsSync: jest.fn(), + })); + + jest.mock('jest-serializer', () => ({ + readFileSync: jest.fn(), + writeFileSync: jest.fn(), + })); + + fs = require('fs'); + serializer = require('jest-serializer'); + PersistedMapStore = require('../PersistedMapStore'); + + now = 0; + }); + + it('ensures that the persisted map file is checked first', () => { + const store = new PersistedMapStore({path: '/foo'}); + + fs.existsSync.mockReturnValue(false); + store.get(key1); + + expect(fs.existsSync).toHaveBeenCalledTimes(1); + expect(serializer.readFileSync).not.toBeCalled(); + }); + + it('loads the file when it exists', () => { + const store = new PersistedMapStore({path: '/foo'}); + + fs.existsSync.mockReturnValue(true); + serializer.readFileSync.mockReturnValue(new Map()); + store.get(key1); + + expect(fs.existsSync).toHaveBeenCalledTimes(1); + expect(serializer.readFileSync).toHaveBeenCalledTimes(1); + expect(serializer.readFileSync.mock.calls[0]).toEqual(['/foo']); + }); + + it('throws if the file is invalid', () => { + const store = new PersistedMapStore({path: '/foo'}); + + fs.existsSync.mockReturnValue(true); + serializer.readFileSync.mockImplementation(() => { + throw new Error(); + }); + expect(() => store.get(key1)).toThrow(); + }); + + it('deserializes and serializes correctly from/to disk', () => { + let file; + + fs.existsSync.mockReturnValue(false); + serializer.readFileSync.mockImplementation(() => file); + serializer.writeFileSync.mockImplementation((_, data) => (file = data)); + + const store1 = new PersistedMapStore({path: '/foo'}); + + store1.set(key1, 'value1'); + store1.set(key2, 123456); + + // Force throttle to kick in and perform the file storage. + advance(7500); + fs.existsSync.mockReturnValue(true); + + const store2 = new PersistedMapStore({path: '/foo'}); + + expect(store2.get(key1)).toBe('value1'); + expect(store2.get(key2)).toBe(123456); + }); + + it('ensures that the throttling is working correctly', () => { + const store1 = new PersistedMapStore({ + path: '/foo', + writeDelay: 1234, + }); + + // Triggers the write, multiple times (only one write should happen). + store1.set(key1, 'foo'); + store1.set(key1, 'bar'); + store1.set(key1, 'baz'); + + advance(1233); + expect(serializer.writeFileSync).toHaveBeenCalledTimes(0); + + advance(1); + expect(serializer.writeFileSync).toHaveBeenCalledTimes(1); + }); +}); diff --git a/yarn.lock b/yarn.lock index 42a4e367..9f9197d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1436,7 +1436,7 @@ babylon@7.0.0-beta.36: version "7.0.0-beta.36" resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.36.tgz#3a3683ba6a9a1e02b0aa507c8e63435e39305b9e" -babylon@^7.0.0-beta, babylon@7.0.0-beta.38: +babylon@7.0.0-beta.38, babylon@^7.0.0-beta: version "7.0.0-beta.38" resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.38.tgz#9b3a33e571a47464a2d20cb9dd5a570f00e3f996" @@ -2527,9 +2527,9 @@ flat-cache@^1.2.1: graceful-fs "^4.1.2" write "^0.2.1" -flow-bin@^0.60.1: - version "0.60.1" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.60.1.tgz#0f4fa7b49be2a916f18cd946fc4a51e32ffe4b48" +flow-bin@^0.66.0: + version "0.66.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.66.0.tgz#a96dde7015dc3343fd552a7b4963c02be705ca26" for-in@^1.0.1: version "1.0.2"