share workers, prevent REPL crosstalk

pull/2272/head
Richard Harris 6 years ago
parent 9f0630c3fb
commit 795ca0c291

@ -0,0 +1,44 @@
const workers = new Map();
let uid = 1;
export default class Bundler {
constructor(version) {
if (!workers.has(version)) {
const worker = new Worker('/workers/bundler.js');
worker.postMessage({ type: 'init', version });
workers.set(version, worker);
}
this.worker = workers.get(version);
this.handlers = new Map();
this.worker.addEventListener('message', event => {
const handler = this.handlers.get(event.data.id);
if (handler) { // if no handler, was meant for a different REPL
handler(event.data);
this.handlers.delete(event.data.id);
}
});
}
bundle(components) {
return new Promise(fulfil => {
const id = uid++;
this.handlers.set(id, fulfil);
this.worker.postMessage({
id,
type: 'bundle',
components
});
});
}
destroy() {
this.worker.terminate();
}
}

@ -1,21 +1,32 @@
const workers = new Map();
let uid = 1;
export default class Compiler {
constructor(version) {
this.worker = new Worker('/workers/compiler.js');
this.worker.postMessage({ type: 'init', version });
if (!workers.has(version)) {
const worker = new Worker('/workers/compiler.js');
worker.postMessage({ type: 'init', version });
workers.set(version, worker);
}
this.worker = workers.get(version);
this.uid = 1;
this.handlers = new Map();
this.worker.onmessage = event => {
this.worker.addEventListener('message', event => {
const handler = this.handlers.get(event.data.id);
handler(event.data.result);
this.handlers.delete(event.data.id);
};
if (handler) { // if no handler, was meant for a different REPL
handler(event.data.result);
this.handlers.delete(event.data.id);
}
});
}
compile(component, options) {
return new Promise(fulfil => {
const id = this.uid++;
const id = uid++;
this.handlers.set(id, fulfil);

@ -1,12 +1,13 @@
let uid = 1;
export default class ReplProxy {
constructor(iframe, handlers) {
this.iframe = iframe;
this.handlers = handlers;
this.cmdId = 1;
this.pendingCmds = new Map();
this.pending_cmds = new Map();
this.handle_event = e => this.handleReplMessage(e);
this.handle_event = e => this.handle_repl_message(e);
window.addEventListener('message', this.handle_event, false);
}
@ -14,63 +15,61 @@ export default class ReplProxy {
window.removeEventListener('message', this.handle_event);
}
iframeCommand(command, args) {
iframe_command(action, args) {
return new Promise((resolve, reject) => {
this.cmdId += 1;
this.pendingCmds.set(this.cmdId, { resolve, reject });
this.iframe.contentWindow.postMessage({
action: command,
cmdId: this.cmdId,
args
}, '*');
const cmd_id = uid++;
this.pending_cmds.set(cmd_id, { resolve, reject });
this.iframe.contentWindow.postMessage({ action, cmd_id, args }, '*');
});
}
handleCommandMessage(cmdData) {
let action = cmdData.action;
let id = cmdData.cmdId;
let handler = this.pendingCmds.get(id);
handle_command_message(cmd_data) {
let action = cmd_data.action;
let id = cmd_data.cmd_id;
let handler = this.pending_cmds.get(id);
if (handler) {
this.pendingCmds.delete(id);
if (action === 'cmdError') {
let { message, stack } = cmdData;
this.pending_cmds.delete(id);
if (action === 'cmd_error') {
let { message, stack } = cmd_data;
let e = new Error(message);
e.stack = stack;
console.log('repl cmd fail');
handler.reject(e)
}
if (action === 'cmdOk') {
handler.resolve(cmdData.args)
if (action === 'cmd_ok') {
handler.resolve(cmd_data.args)
}
} else {
console.error('command not found', id, cmdData, [...this.pendingCmds.keys()]);
console.error('command not found', id, cmd_data, [...this.pending_cmds.keys()]);
}
}
handleReplMessage(event) {
handle_repl_message(event) {
if (event.source !== this.iframe.contentWindow) return;
const { action, args } = event.data;
if (action === 'cmdError' || action === 'cmdOk') {
this.handleCommandMessage(event.data);
if (action === 'cmd_error' || action === 'cmd_ok') {
this.handle_command_message(event.data);
}
if (action === 'fetch_progress') {
this.handlers.onFetchProgress(args.remaining)
this.handlers.on_fetch_progress(args.remaining)
}
}
eval(script) {
return this.iframeCommand('eval', { script });
return this.iframe_command('eval', { script });
}
handleLinks() {
return this.iframeCommand('catch_clicks', {});
handle_links() {
return this.iframe_command('catch_clicks', {});
}
fetchImports(imports, import_map) {
return this.iframeCommand('fetch_imports', { imports, import_map })
fetch_imports(imports, import_map) {
return this.iframe_command('fetch_imports', { imports, import_map })
}
}

@ -26,13 +26,13 @@
onMount(() => {
proxy = new ReplProxy(iframe, {
onFetchProgress: progress => {
on_fetch_progress: progress => {
pending_imports = progress;
}
});
iframe.addEventListener('load', () => {
proxy.handleLinks();
proxy.handle_links();
ready = true;
});
@ -49,7 +49,7 @@
const token = current_token = {};
try {
await proxy.fetchImports($bundle.imports, $bundle.import_map);
await proxy.fetch_imports($bundle.imports, $bundle.import_map);
if (token !== current_token) return;
await proxy.eval(`

@ -9,9 +9,9 @@
const { register_output } = getContext('REPL');
export let version;
export let sourceErrorLoc;
export let runtimeError;
export let embedded;
export let sourceErrorLoc = null;
export let runtimeError = null;
export let embedded = false;
let foo; // TODO workaround for https://github.com/sveltejs/svelte/issues/2122
@ -39,12 +39,7 @@
}
});
let compiler;
onMount(() => {
compiler = new Compiler(version);
return () => compiler.destroy();
});
const compiler = process.browser && new Compiler(version);
// refs
let viewer;

@ -4,8 +4,8 @@
import Repl from '../../components/Repl/index.svelte';
export let version = 'beta';
export let gist;
export let example;
export let gist = null;
export let example = null;
let repl;
let name = 'loading...';

@ -7,6 +7,7 @@
import ModuleEditor from './Input/ModuleEditor.svelte';
import Output from './Output/index.svelte';
import InputOutputToggle from './InputOutputToggle.svelte';
import Bundler from './Bundler.js';
export let version = 'beta'; // TODO change this to latest when the time comes
export let embedded = false;
@ -69,8 +70,11 @@
let module_editor;
let output;
function rebundle() {
workers.bundler.postMessage({ type: 'bundle', components: $components });
let current_token;
async function rebundle() {
const token = current_token = {};
const result = await bundler.bundle($components);
if (result && token === current_token) bundle.set(result);
}
setContext('REPL', {
@ -145,30 +149,7 @@
let width = typeof window !== 'undefined' ? window.innerWidth : 300;
let show_output = false;
onMount(async () => {
workers = {
bundler: new Worker('/workers/bundler.js')
};
workers.bundler.postMessage({ type: 'init', version });
workers.bundler.onmessage = event => {
bundle.set(event.data);
};
return () => {
workers.bundler.terminate();
};
});
$: if ($bundle && $bundle.error && $selected) {
sourceErrorLoc = $bundle.error.filename === `${$selected.name}.${$selected.type}`
? $bundle.error.start
: null;
}
$: if (workers && $components) {
workers.bundler.postMessage({ type: 'bundle', components: $components });
}
const bundler = process.browser && new Bundler(version);
$: if (output && $selected) {
output.update($selected, $compile_options);

@ -4,7 +4,6 @@
import Nav from '../components/TopNav.svelte';
export let segment;
export let path;
</script>
<InlineSvg />

@ -1,149 +1,105 @@
(function (){
const importCache = {};
function fetchImport(id) {
return new Promise((fulfil, reject) => {
curl([`https://bundle.run/${id}`]).then(module => {
importCache[id] = module;
fulfil(module);
}, err => {
console.error(err.stack);
reject(new Error(`Error loading ${id} from bundle.run`));
(function() {
const import_cache = {};
function fetch_import(id) {
return new Promise((fulfil, reject) => {
curl([`https://bundle.run/${id}`]).then(module => {
import_cache[id] = module;
fulfil(module);
}, err => {
console.error(err.stack);
reject(new Error(`Error loading ${id} from bundle.run`));
});
});
});
}
function fetchImports(imports, progressFunc) {
const missingImports = imports.filter(x => !importCache[x]);
let pendingImports = missingImports.length;
if (missingImports.length) {
let promise = Promise.all(
missingImports.map(id => fetchImport(id).then(() => {
pendingImports -= 1;
if (progressFunc) progressFunc(pendingImports);
}))
);
return promise
} else {
return Promise.resolve();
}
}
function handleMessage(ev) {
let { action, cmdId } = ev.data;
const sendMessage = (payload) => parent.postMessage( { ...payload }, ev.origin);
const sendReply = (payload) => sendMessage({ ...payload, cmdId })
const sendOk = () => sendReply({ action: "cmdOk" });
const sendError = (message, stack) => sendReply({ action: "cmdError", message, stack })
if (action == "eval") {
let { script } = ev.data.args;
try {
eval(script);
sendOk();
} catch (e) {
sendError(e.message, e.stack);
function fetch_imports(imports, progress_func) {
const missing_imports = imports.filter(x => !import_cache[x]);
let pending_imports = missing_imports.length;
if (missing_imports.length) {
let promise = Promise.all(
missing_imports.map(id => fetch_import(id).then(() => {
pending_imports -= 1;
if (progress_func) progress_func(pending_imports);
}))
);
return promise;
} else {
return Promise.resolve();
}
}
if (action == "bind_props") {
let { props } = ev.data.args
if (!window.component) {
// TODO can this happen?
console.warn('no component to bind to');
sendOk();
return;
}
try {
props.forEach(prop => {
// TODO should there be a public API for binding?
// e.g. `component.$watch(prop, handler)`?
// (answer: probably)
window.component.$$.bound[prop] = value => {
sendMessage({ action:"prop_update", args: { prop, value } })
};
});
sendOk();
} catch (e) {
sendError(e.message, e.stack);
}
}
if (action == "set_prop") {
try {
if (!window.component) {
return;
function handle_message(ev) {
let { action, cmd_id } = ev.data;
const send_message = (payload) => parent.postMessage( { ...payload }, ev.origin);
const send_reply = (payload) => send_message({ ...payload, cmd_id });
const send_ok = () => send_reply({ action: 'cmd_ok' });
const send_error = (message, stack) => send_reply({ action: 'cmd_error', message, stack });
if (action === 'eval') {
try {
const { script } = ev.data.args;
eval(script);
send_ok();
} catch (e) {
send_error(e.message, e.stack);
}
let { prop, value } = ev.data.args;
component[prop] = value;
sendOk();
} catch (e) {
sendError(e.message, e.stack);
}
}
if (action == "catch_clicks") {
try {
let topOrigin = ev.origin;
document.body.addEventListener('click', event => {
if (event.which !== 1) return;
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
if (event.defaultPrevented) return;
// ensure target is a link
let el = event.target;
while (el && el.nodeName !== 'A') el = el.parentNode;
if (!el || el.nodeName !== 'A') return;
if (el.hasAttribute('download') || el.getAttribute('rel') === 'external' || el.target) return;
event.preventDefault();
if (el.href.startsWith(topOrigin)) {
const url = new URL(el.href);
if (url.hash[0] === '#') {
window.location.hash = url.hash;
return;
if (action === 'catch_clicks') {
try {
const top_origin = ev.origin;
document.body.addEventListener('click', event => {
if (event.which !== 1) return;
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
if (event.defaultPrevented) return;
// ensure target is a link
let el = event.target;
while (el && el.nodeName !== 'A') el = el.parentNode;
if (!el || el.nodeName !== 'A') return;
if (el.hasAttribute('download') || el.getAttribute('rel') === 'external' || el.target) return;
event.preventDefault();
if (el.href.startsWith(top_origin)) {
const url = new URL(el.href);
if (url.hash[0] === '#') {
window.location.hash = url.hash;
return;
}
}
}
window.open(el.href, '_blank');
});
sendOk();
} catch(e) {
sendError(e.message, e.stack);
window.open(el.href, '_blank');
});
send_ok();
} catch(e) {
send_error(e.message, e.stack);
}
}
}
if (action == "fetch_imports") {
let { imports, import_map } = ev.data.args;
fetchImports(imports, (remaining) => {
sendMessage({action: "fetch_progress", args: { remaining }});
})
.then(() => {
imports.forEach(x=> {
const module = importCache[x];
const name = import_map.get(x);
window[name] = module;
if (action === 'fetch_imports') {
const { imports, import_map } = ev.data.args;
fetch_imports(imports, (remaining) => {
send_message({action: 'fetch_progress', args: { remaining }});
})
.then(() => {
imports.forEach(x=> {
const module = import_cache[x];
const name = import_map.get(x);
window[name] = module;
});
send_ok();
})
.catch(e => {
send_error(e.message, e.stack);
});
sendOk();
})
.catch(e => {
sendError(e.message, e.stack);
})
}
}
}
window.addEventListener("message", handleMessage, false)
console.log("repl-runner initialized");
window.addEventListener('message', handle_message, false);
})();

@ -24,7 +24,7 @@ self.addEventListener('message', async event => {
if (event.data.components.length === 0) return;
await ready;
const result = await bundle(event.data.components);
const result = await bundle(event.data);
if (result) {
postMessage(result);
}
@ -33,7 +33,7 @@ self.addEventListener('message', async event => {
}
});
const commonCompilerOptions = {
const common_options = {
dev: true,
};
@ -42,8 +42,6 @@ let cached = {
ssr: {}
};
let currentToken;
const is_svelte_module = id => id === 'svelte' || id.startsWith('svelte/');
const cache = new Map();
@ -60,7 +58,7 @@ function fetch_if_uncached(url) {
return cache.get(url);
}
async function getBundle(mode, cache, lookup) {
async function get_bundle(mode, cache, lookup) {
let bundle;
const all_warnings = [];
@ -107,7 +105,7 @@ async function getBundle(mode, cache, lookup) {
format: 'esm',
name,
filename: name + '.svelte'
}, commonCompilerOptions));
}, common_options));
new_cache[id] = { code, result };
@ -137,12 +135,10 @@ async function getBundle(mode, cache, lookup) {
return { bundle, cache: new_cache, error: null, warnings: all_warnings };
}
async function bundle(components) {
async function bundle({ id, components }) {
// console.clear();
console.log(`running Svelte compiler version %c${svelte.VERSION}`, 'font-weight: bold');
const token = currentToken = {};
const lookup = {};
components.forEach(component => {
const path = `./${component.name}.${component.type}`;
@ -154,16 +150,11 @@ async function bundle(components) {
let error;
try {
dom = await getBundle('dom', cached.dom, lookup);
dom = await get_bundle('dom', cached.dom, lookup);
if (dom.error) {
throw dom.error;
}
if (token !== currentToken) {
console.error(`aborted`);
return;
}
cached.dom = dom.cache;
let uid = 1;
@ -180,10 +171,8 @@ async function bundle(components) {
sourcemap: true
})).output[0];
if (token !== currentToken) return;
const ssr = false // TODO how can we do SSR?
? await getBundle('ssr', cached.ssr, lookup)
? await get_bundle('ssr', cached.ssr, lookup)
: null;
if (ssr) {
@ -193,8 +182,6 @@ async function bundle(components) {
}
}
if (token !== currentToken) return;
const ssr_result = ssr
? (await ssr.bundle.generate({
format: 'iife',
@ -206,6 +193,7 @@ async function bundle(components) {
: null;
return {
id,
imports: dom_result.imports,
import_map,
dom: dom_result,
@ -218,6 +206,7 @@ async function bundle(components) {
delete e.toString;
return {
id,
imports: [],
import_map,
dom: null,

@ -20,20 +20,19 @@ self.addEventListener('message', async event => {
await ready;
postMessage(compile(event.data));
break;
}
});
const commonCompilerOptions = {
const common_options = {
dev: false,
css: false
};
function compile({ id, source, options, entry }) {
function compile({ id, source, options }) {
try {
const { js, css, stats, vars } = svelte.compile(
const { js, css } = svelte.compile(
source,
Object.assign({}, commonCompilerOptions, options)
Object.assign({}, common_options, options)
);
return {

Loading…
Cancel
Save