UNPKG

react-native

Version:

A framework for building native apps using React

343 lines (300 loc) • 9.59 kB
/** * Copyright (c) 2016-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @flow */ 'use strict'; const crypto = require('crypto'); const imurmurhash = require('imurmurhash'); const invariant = require('invariant'); const jsonStableStringify = require('json-stable-stringify'); const path = require('path'); const request = require('request'); import type {Options as TransformOptions} from '../JSTransformer/worker/worker'; import type {CachedResult} from './TransformCache'; type FetchResultURIs = ( keys: Array<string>, callback: (error?: Error, results?: Map<string, string>) => void, ) => mixed; type StoreResults = ( resultsByKey: Map<string, CachedResult>, callback: (error?: Error) => void, ) => mixed; type FetchProps = { filePath: string, sourceCode: string, transformCacheKey: string, transformOptions: TransformOptions, }; type FetchCallback = (error?: Error, resultURI?: ?CachedResult) => mixed; type FetchURICallback = (error?: Error, resultURI?: ?string) => mixed; type ProcessBatch<TItem, TResult> = ( batch: Array<TItem>, callback: (error?: Error, orderedResults?: Array<TResult>) => mixed, ) => mixed; type BatchProcessorOptions = { maximumDelayMs: number, maximumItems: number, concurrency: number, }; /** * We batch keys together trying to make a smaller amount of queries. For that * we wait a small moment before starting to fetch. We limit also the number of * keys we try to fetch at once, so if we already have that many keys pending, * we can start fetching right away. */ class BatchProcessor<TItem, TResult> { _options: BatchProcessorOptions; _processBatch: ProcessBatch<TItem, TResult>; _queue: Array<{ item: TItem, callback: (error?: Error, result?: TResult) => mixed, }>; _timeoutHandle: ?number; _currentProcessCount: number; constructor( options: BatchProcessorOptions, processBatch: ProcessBatch<TItem, TResult>, ) { this._options = options; this._processBatch = processBatch; this._queue = []; this._timeoutHandle = null; this._currentProcessCount = 0; (this: any)._processQueue = this._processQueue.bind(this); } _processQueue() { this._timeoutHandle = null; while ( this._queue.length > 0 && this._currentProcessCount < this._options.concurrency ) { this._currentProcessCount++; const jobs = this._queue.splice(0, this._options.maximumItems); const items = jobs.map(job => job.item); this._processBatch(items, (error, results) => { invariant( results == null || results.length === items.length, 'Not enough results returned.', ); for (let i = 0; i < items.length; ++i) { jobs[i].callback(error, results && results[i]); } this._currentProcessCount--; this._processQueueOnceReady(); }); } } _processQueueOnceReady() { if (this._queue.length >= this._options.maximumItems) { clearTimeout(this._timeoutHandle); process.nextTick(this._processQueue); return; } if (this._timeoutHandle == null) { this._timeoutHandle = setTimeout( this._processQueue, this._options.maximumDelayMs, ); } } queue( item: TItem, callback: (error?: Error, result?: TResult) => mixed, ) { this._queue.push({item, callback}); this._processQueueOnceReady(); } } type URI = string; /** * We aggregate the requests to do a single request for many keys. It also * ensures we do a single request at a time to avoid pressuring the I/O. */ class KeyURIFetcher { _fetchResultURIs: FetchResultURIs; _batchProcessor: BatchProcessor<string, ?URI>; _processKeys( keys: Array<string>, callback: (error?: Error, keyURIs: Array<?URI>) => mixed, ) { this._fetchResultURIs(keys, (error, URIsByKey) => { const URIs = keys.map(key => URIsByKey && URIsByKey.get(key)); callback(error, URIs); }); } fetch(key: string, callback: FetchURICallback) { this._batchProcessor.queue(key, callback); } constructor(fetchResultURIs: FetchResultURIs) { this._fetchResultURIs = fetchResultURIs; this._batchProcessor = new BatchProcessor({ maximumDelayMs: 10, maximumItems: 500, concurrency: 25, }, this._processKeys.bind(this)); } } class KeyResultStore { _storeResults: StoreResults; _batchProcessor: BatchProcessor<{key: string, result: CachedResult}, void>; _processResults( keyResults: Array<{key: string, result: CachedResult}>, callback: (error?: Error) => mixed, ) { const resultsByKey = new Map( keyResults.map(pair => [pair.key, pair.result]), ); this._storeResults(resultsByKey, error => { callback(error); }); } store(key: string, result: CachedResult) { this._batchProcessor.queue({key, result}, () => {}); } constructor(storeResults: StoreResults) { this._storeResults = storeResults; this._batchProcessor = new BatchProcessor({ maximumDelayMs: 1000, maximumItems: 100, concurrency: 10, }, this._processResults.bind(this)); } } function validateCachedResult(cachedResult: mixed): ?CachedResult { if ( cachedResult != null && typeof cachedResult === 'object' && typeof cachedResult.code === 'string' && Array.isArray(cachedResult.dependencies) && cachedResult.dependencies.every(dep => typeof dep === 'string') && Array.isArray(cachedResult.dependencyOffsets) && cachedResult.dependencyOffsets.every(offset => typeof offset === 'number') ) { return (cachedResult: any); } return undefined; } /** * The transform options contain absolute paths. This can contain, for * example, the username if someone works their home directory (very likely). * We need to get rid of this user-and-machine-dependent data for the global * cache, otherwise nobody would share the same cache keys. */ function globalizeTransformOptions( options: TransformOptions, ): TransformOptions { const {transform} = options; if (transform == null) { return options; } return { ...options, transform: { ...transform, projectRoots: transform.projectRoots.map(p => { return path.relative(path.join(__dirname, '../../../../..'), p); }), }, }; } /** * One can enable the global cache by calling configure() from a custom CLI * script. Eventually we may make it more flexible. */ class GlobalTransformCache { _fetcher: KeyURIFetcher; _store: ?KeyResultStore; static _global: ?GlobalTransformCache; constructor( fetchResultURIs: FetchResultURIs, storeResults?: StoreResults, ) { this._fetcher = new KeyURIFetcher(fetchResultURIs); if (storeResults != null) { this._store = new KeyResultStore(storeResults); } } /** * Return a key for identifying uniquely a source file. */ static keyOf(props: FetchProps) { const stableOptions = globalizeTransformOptions(props.transformOptions); const digest = crypto.createHash('sha1').update([ jsonStableStringify(stableOptions), props.transformCacheKey, imurmurhash(props.sourceCode).result().toString(), ].join('$')).digest('hex'); return `${digest}-${path.basename(props.filePath)}`; } /** * We may want to improve that logic to return a stream instead of the whole * blob of transformed results. However the results are generally only a few * megabytes each. */ _fetchFromURI(uri: string, callback: FetchCallback) { request.get({uri, json: true}, (error, response, unvalidatedResult) => { if (error != null) { callback(error); return; } if (response.statusCode !== 200) { callback(new Error( `Unexpected HTTP status code: ${response.statusCode}`, )); return; } const result = validateCachedResult(unvalidatedResult); if (result == null) { callback(new Error('Invalid result returned by server.')); return; } callback(undefined, result); }); } fetch(props: FetchProps, callback: FetchCallback) { this._fetcher.fetch(GlobalTransformCache.keyOf(props), (error, uri) => { if (error != null) { callback(error); } else { if (uri == null) { callback(); return; } this._fetchFromURI(uri, callback); } }); } store(props: FetchProps, result: CachedResult) { if (this._store != null) { this._store.store(GlobalTransformCache.keyOf(props), result); } } /** * For using the global cache one needs to have some kind of central key-value * store that gets prefilled using keyOf() and the transformed results. The * fetching function should provide a mapping of keys to URIs. The files * referred by these URIs contains the transform results. Using URIs instead * of returning the content directly allows for independent fetching of each * result. */ static configure( fetchResultURIs: FetchResultURIs, storeResults?: StoreResults, ) { GlobalTransformCache._global = new GlobalTransformCache( fetchResultURIs, storeResults, ); } static get() { return GlobalTransformCache._global; } } GlobalTransformCache._global = null; module.exports = GlobalTransformCache;