blob: eac21410a88ffb904a83651d6f907e70c1c70eb5 [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// `package:dartpad` provides a client for launching and interacting with
/// a _Web Worker_ running a development environment with Dart SDK.
library;
import 'dart:async';
import 'dart:convert';
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'package:web/web.dart' as web;
import 'src/util/message_port_channel.dart';
import 'src/worker_client.dart';
export 'src/dartpad_config.dart' show DartPadConfig;
export 'src/exceptions.dart' hide rethrowAsDartPadException;
export 'src/sandbox.dart' show Sandbox;
export 'src/worker_client.dart'
show HotReloadCompiler, LanguageServer, Workspace;
/// A client for interacting with a DartPad Web Worker.
final class DartPad extends WorkerClient {
final web.Worker _worker;
final String _blobUrl;
DartPad._(super.channel, this._worker, this._blobUrl);
/// Create a _Web Worker_ running a DartPad development environment.
///
/// The [assetBaseUrl] should point to the directory containing
/// `worker.loader.js` and `worker.wasm`.
/// The [sdkLocation] should point to the directory containing the `sdk.tar`
/// to load (relative to the worker script, or an absolute URL).
/// Defaults to `./dart/`.
static Future<DartPad> create({
required Uri assetBaseUrl,
required Uri sdkLocation,
Uri? pubHostedUrl,
}) async {
if (!assetBaseUrl.path.endsWith('/')) {
assetBaseUrl = assetBaseUrl.replace(path: '${assetBaseUrl.path}/');
}
sdkLocation = assetBaseUrl.resolveUri(sdkLocation);
if (!sdkLocation.path.endsWith('/')) {
sdkLocation = sdkLocation.replace(path: '${sdkLocation.path}/');
}
var workerScript = assetBaseUrl.resolve('worker.loader.js');
// Since we workerScript might be on a different origin we cannot just
// create from it. So we must create a blob and start the worker from this
// blob. We also cannot inject querystring parameters on a blob.
// So we must inject these directly into the global scope, for the worker
// to pick up.
//
// If in the future we want to make a SharedWorker, we have two options:
// (A) We ask the user host a dartpad-worker.js file that imports
// our worker script. This file must be hosted on the users origin.
// Then the user will have a SharedWorker, that shared for instances
// of their origin. And querystring can be used for parameterization.
// Ensuring that there is one SharedWorker per set of parameters.
// (B) We host a dartpad-worker.html page, which we then embed into the
// users page as a hidden iframe. Inside this iframe we create a
// SharedWorker and we can again use querystring parameters.
// This gives a single SharedWorker across all origins, and would allow
// a persisted PUB_CACHE to be shared across dartpads everywhere.
// Again, we could have a SharedWorker for each set of parameters.
//
// For now we don't support launching a SharedWorker, but in the future we
// explore supporting a SharedWorker strategy. Option (B) would be rather
// attractive, if we could use Origin-Private-File-System (OPFS) to retain
// the PUB_CACHE. Granted to actually use OPFS with sync I/O, we can't use
// SharedWorkers, so we'd have to make a SharedWorker that owns a
// dedicated worker and forwards the port to this worker. The overhead of
// this is probably not bad, it's just (B) does become a fairly complex
// setup.
final blobUrl = web.URL.createObjectURL(
web.Blob(
[
[
'import \'$workerScript\';',
'self.assetBaseUrl = ${jsonEncode(assetBaseUrl.toString())};',
'self.sdkLocation = ${jsonEncode(sdkLocation.toString())};',
'self.pubHostedUrl = ${jsonEncode(pubHostedUrl?.toString())};',
'',
].join('\n').toJS,
].toJS,
web.BlobPropertyBag(type: 'application/javascript'),
),
);
final worker = web.Worker(
blobUrl.toJS,
web.WorkerOptions(name: 'dartpad-worker', type: 'module'),
);
worker.addEventListener(
'error',
(web.Event event) {
web.console.error('Unhandled error from worker'.toJS, event);
}.toJS,
);
worker.onmessage = (web.MessageEvent event) {
final data = event.data as JSObject;
final action = data.getProperty<JSString>('action'.toJS).toDart;
if (action == 'error') {
final m = data.getProperty<JSString>('message'.toJS).toDart;
throw StateError('Failed to start worker: $m');
}
}.toJS;
final web.MessageChannel(:port1, :port2) = web.MessageChannel();
worker.postMessage({'action': 'connect'}.jsify(), [port2].toJS);
return DartPad._(messagePortChannel(port1).cast(), worker, blobUrl);
}
/// Terminates the underlying Web Worker.
@override
Future<void> dispose() async {
try {
await super.dispose();
_worker.terminate();
} finally {
web.URL.revokeObjectURL(_blobUrl);
}
}
}