share workers, prevent REPL crosstalk

pull/2272/head
Richard Harris 7 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 { export default class Compiler {
constructor(version) { constructor(version) {
this.worker = new Worker('/workers/compiler.js'); if (!workers.has(version)) {
this.worker.postMessage({ type: 'init', 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.handlers = new Map();
this.worker.onmessage = event => { this.worker.addEventListener('message', event => {
const handler = this.handlers.get(event.data.id); 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) { compile(component, options) {
return new Promise(fulfil => { return new Promise(fulfil => {
const id = this.uid++; const id = uid++;
this.handlers.set(id, fulfil); this.handlers.set(id, fulfil);

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

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

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

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

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

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

@ -1,149 +1,105 @@
(function (){ (function() {
const importCache = {}; const import_cache = {};
function fetchImport(id) { function fetch_import(id) {
return new Promise((fulfil, reject) => { return new Promise((fulfil, reject) => {
curl([`https://bundle.run/${id}`]).then(module => { curl([`https://bundle.run/${id}`]).then(module => {
importCache[id] = module; import_cache[id] = module;
fulfil(module); fulfil(module);
}, err => { }, err => {
console.error(err.stack); console.error(err.stack);
reject(new Error(`Error loading ${id} from bundle.run`)); 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") { function fetch_imports(imports, progress_func) {
let { script } = ev.data.args; const missing_imports = imports.filter(x => !import_cache[x]);
try { let pending_imports = missing_imports.length;
eval(script);
sendOk(); if (missing_imports.length) {
} catch (e) { let promise = Promise.all(
sendError(e.message, e.stack); 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") { function handle_message(ev) {
let { props } = ev.data.args let { action, cmd_id } = ev.data;
const send_message = (payload) => parent.postMessage( { ...payload }, ev.origin);
if (!window.component) { const send_reply = (payload) => send_message({ ...payload, cmd_id });
// TODO can this happen? const send_ok = () => send_reply({ action: 'cmd_ok' });
console.warn('no component to bind to'); const send_error = (message, stack) => send_reply({ action: 'cmd_error', message, stack });
sendOk();
return; if (action === 'eval') {
} try {
const { script } = ev.data.args;
try { eval(script);
props.forEach(prop => { send_ok();
// TODO should there be a public API for binding? } catch (e) {
// e.g. `component.$watch(prop, handler)`? send_error(e.message, e.stack);
// (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;
} }
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; if (action === 'catch_clicks') {
try {
event.preventDefault(); const top_origin = ev.origin;
document.body.addEventListener('click', event => {
if (el.href.startsWith(topOrigin)) { if (event.which !== 1) return;
const url = new URL(el.href); if (event.metaKey || event.ctrlKey || event.shiftKey) return;
if (url.hash[0] === '#') { if (event.defaultPrevented) return;
window.location.hash = url.hash;
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'); window.open(el.href, '_blank');
}); });
sendOk(); send_ok();
} catch(e) { } catch(e) {
sendError(e.message, e.stack); send_error(e.message, e.stack);
}
} }
}
if (action == "fetch_imports") { if (action === 'fetch_imports') {
let { imports, import_map } = ev.data.args; const { imports, import_map } = ev.data.args;
fetchImports(imports, (remaining) => { fetch_imports(imports, (remaining) => {
sendMessage({action: "fetch_progress", args: { remaining }}); send_message({action: 'fetch_progress', args: { remaining }});
}) })
.then(() => { .then(() => {
imports.forEach(x=> { imports.forEach(x=> {
const module = importCache[x]; const module = import_cache[x];
const name = import_map.get(x); const name = import_map.get(x);
window[name] = module; 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; if (event.data.components.length === 0) return;
await ready; await ready;
const result = await bundle(event.data.components); const result = await bundle(event.data);
if (result) { if (result) {
postMessage(result); postMessage(result);
} }
@ -33,7 +33,7 @@ self.addEventListener('message', async event => {
} }
}); });
const commonCompilerOptions = { const common_options = {
dev: true, dev: true,
}; };
@ -42,8 +42,6 @@ let cached = {
ssr: {} ssr: {}
}; };
let currentToken;
const is_svelte_module = id => id === 'svelte' || id.startsWith('svelte/'); const is_svelte_module = id => id === 'svelte' || id.startsWith('svelte/');
const cache = new Map(); const cache = new Map();
@ -60,7 +58,7 @@ function fetch_if_uncached(url) {
return cache.get(url); return cache.get(url);
} }
async function getBundle(mode, cache, lookup) { async function get_bundle(mode, cache, lookup) {
let bundle; let bundle;
const all_warnings = []; const all_warnings = [];
@ -107,7 +105,7 @@ async function getBundle(mode, cache, lookup) {
format: 'esm', format: 'esm',
name, name,
filename: name + '.svelte' filename: name + '.svelte'
}, commonCompilerOptions)); }, common_options));
new_cache[id] = { code, result }; 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 }; return { bundle, cache: new_cache, error: null, warnings: all_warnings };
} }
async function bundle(components) { async function bundle({ id, components }) {
// console.clear(); // console.clear();
console.log(`running Svelte compiler version %c${svelte.VERSION}`, 'font-weight: bold'); console.log(`running Svelte compiler version %c${svelte.VERSION}`, 'font-weight: bold');
const token = currentToken = {};
const lookup = {}; const lookup = {};
components.forEach(component => { components.forEach(component => {
const path = `./${component.name}.${component.type}`; const path = `./${component.name}.${component.type}`;
@ -154,16 +150,11 @@ async function bundle(components) {
let error; let error;
try { try {
dom = await getBundle('dom', cached.dom, lookup); dom = await get_bundle('dom', cached.dom, lookup);
if (dom.error) { if (dom.error) {
throw dom.error; throw dom.error;
} }
if (token !== currentToken) {
console.error(`aborted`);
return;
}
cached.dom = dom.cache; cached.dom = dom.cache;
let uid = 1; let uid = 1;
@ -180,10 +171,8 @@ async function bundle(components) {
sourcemap: true sourcemap: true
})).output[0]; })).output[0];
if (token !== currentToken) return;
const ssr = false // TODO how can we do SSR? const ssr = false // TODO how can we do SSR?
? await getBundle('ssr', cached.ssr, lookup) ? await get_bundle('ssr', cached.ssr, lookup)
: null; : null;
if (ssr) { if (ssr) {
@ -193,8 +182,6 @@ async function bundle(components) {
} }
} }
if (token !== currentToken) return;
const ssr_result = ssr const ssr_result = ssr
? (await ssr.bundle.generate({ ? (await ssr.bundle.generate({
format: 'iife', format: 'iife',
@ -206,6 +193,7 @@ async function bundle(components) {
: null; : null;
return { return {
id,
imports: dom_result.imports, imports: dom_result.imports,
import_map, import_map,
dom: dom_result, dom: dom_result,
@ -218,6 +206,7 @@ async function bundle(components) {
delete e.toString; delete e.toString;
return { return {
id,
imports: [], imports: [],
import_map, import_map,
dom: null, dom: null,

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

Loading…
Cancel
Save