Add Delta Bundler client to JS remote debugger

Reviewed By: jeanlauliac

Differential Revision: D5899825

fbshipit-source-id: 9c5815ef234507034016becaabae754129b96c71
This commit is contained in:
Rafael Oleza 2017-09-29 05:38:12 -07:00 committed by Facebook Github Bot
parent 3849765ac1
commit ace7273538
3 changed files with 204 additions and 1 deletions

View File

@ -0,0 +1,124 @@
/**
* Copyright (c) 2015-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.
*
* @format
*/
/**
* This file is a copy of the reference `DeltaPatcher`, located in
* metro-bundler. The reason to not reuse that file is that in this context
* we cannot have flow annotations or CJS syntax (since this file is directly)
* injected into a static HTML page.
*
* TODO: Find a simple and lightweight way to compile `DeltaPatcher` to avoid
* having this duplicated file.
*/
(function(global) {
'use strict';
/**
* This is a reference client for the Delta Bundler: it maintains cached the
* last patched bundle delta and it's capable of applying new Deltas received
* from the Bundler.
*/
class DeltaPatcher {
constructor() {
this._lastBundle = {
pre: new Map(),
post: new Map(),
modules: new Map(),
};
this._initialized = false;
this._lastNumModifiedFiles = 0;
this._lastModifiedDate = new Date();
}
static get(id) {
let deltaPatcher = this._deltaPatchers.get(id);
if (!deltaPatcher) {
deltaPatcher = new DeltaPatcher();
this._deltaPatchers.set(id, deltaPatcher);
}
return deltaPatcher;
}
/**
* Applies a Delta Bundle to the current bundle.
*/
applyDelta(deltaBundle) {
// Make sure that the first received delta is a fresh one.
if (!this._initialized && !deltaBundle.reset) {
throw new Error(
'DeltaPatcher should receive a fresh Delta when being initialized',
);
}
this._initialized = true;
// Reset the current delta when we receive a fresh delta.
if (deltaBundle.reset) {
this._lastBundle = {
pre: new Map(),
post: new Map(),
modules: new Map(),
};
}
this._lastNumModifiedFiles =
deltaBundle.pre.size + deltaBundle.post.size + deltaBundle.delta.size;
if (this._lastNumModifiedFiles > 0) {
this._lastModifiedDate = new Date();
}
this._patchMap(this._lastBundle.pre, deltaBundle.pre);
this._patchMap(this._lastBundle.post, deltaBundle.post);
this._patchMap(this._lastBundle.modules, deltaBundle.delta);
return this;
}
/**
* Returns the number of modified files in the last received Delta. This is
* currently used to populate the `X-Metro-Files-Changed-Count` HTTP header
* when metro serves the whole JS bundle, and can potentially be removed once
* we only send the actual deltas to clients.
*/
getLastNumModifiedFiles() {
return this._lastNumModifiedFiles;
}
getLastModifiedDate() {
return this._lastModifiedDate;
}
getAllModules() {
return [].concat(
Array.from(this._lastBundle.pre.values()),
Array.from(this._lastBundle.modules.values()),
Array.from(this._lastBundle.post.values()),
);
}
_patchMap(original, patch) {
for (const [key, value] of patch.entries()) {
if (value == null) {
original.delete(key);
} else {
original.set(key, value);
}
}
}
}
DeltaPatcher._deltaPatchers = new Map();
global.DeltaPatcher = DeltaPatcher;
})(window);

View File

@ -0,0 +1,69 @@
/**
* Copyright (c) 2015-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
* @format
*/
/* global Blob, URL: true */
(function(global) {
'use strict';
let cachedBundleUrls = new Map();
/**
* Converts the passed delta URL into an URL object containing already the
* whole JS bundle Blob.
*/
async function deltaUrlToBlobUrl(deltaUrl) {
let cachedBundle = cachedBundleUrls.get(deltaUrl);
const deltaBundleId = cachedBundle
? `&deltaBundleId=${cachedBundle.id}`
: '';
const data = await fetch(deltaUrl + deltaBundleId);
const bundle = await data.json();
const deltaPatcher = global.DeltaPatcher.get(bundle.id).applyDelta({
pre: new Map(bundle.pre),
post: new Map(bundle.post),
delta: new Map(bundle.delta),
reset: bundle.reset,
});
// If nothing changed, avoid recreating a bundle blob by reusing the
// previous one.
if (deltaPatcher.getLastNumModifiedFiles() === 0 && cachedBundle) {
return cachedBundle.url;
}
// Clean up the previous bundle URL to not leak memory.
if (cachedBundle) {
URL.revokeObjectURL(cachedBundle.url);
}
const blobContent = deltaPatcher.getAllModules();
// Build the blob with the whole JS bundle.
const blob = new Blob(blobContent, {
type: 'application/javascript',
});
const bundleUrl = URL.createObjectURL(blob);
cachedBundleUrls.set(deltaUrl, {
id: bundle.id,
url: bundleUrl,
});
return bundleUrl;
}
global.deltaUrlToBlobUrl = deltaUrlToBlobUrl;
})(window || {});

View File

@ -12,6 +12,8 @@
<meta charset=utf-8>
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<title>React Native Debugger</title>
<script src="/debugger-ui/DeltaPatcher.js"></script>
<script src="/debugger-ui/deltaUrlToBlobUrl.js"></script>
<script>
/* eslint-env browser */
'use strict';
@ -150,7 +152,7 @@
Page.setState({status: {type: 'connecting'}});
};
ws.onmessage = function(message) {
ws.onmessage = async function(message) {
if (!message.data) {
return;
}
@ -176,6 +178,11 @@
} else if (object.method === '$disconnected') {
shutdownJSRuntime();
Page.setState({status: {type: 'disconnected'}});
} else if (object.method === 'executeApplicationScript') {
worker.postMessage({
...object,
url: await getBlobUrl(object.url),
});
} else {
// Otherwise, pass through to the worker.
worker.postMessage(object);
@ -198,6 +205,9 @@
connectToDebuggerProxy();
async function getBlobUrl(url) {
return await window.deltaUrlToBlobUrl(url.replace('.bundle', '.delta'));
}
})();
</script>
<style type="text/css">