Globe Runtime
A minimalist runtime designed to leverage the JavaScript ecosystem and its tools to accelerate development in Dart. Globe Runtime enables seamless communication between Dart and JavaScript by utilizing Dart FFI, V8, Rust, and Deno extensions.
🚀 Quick Start
Installation
Add Globe Runtime to your pubspec.yaml
:
dependencies:
globe_runtime: ^1.0.7
Basic Usage
Here's a simple example that calls a JavaScript function from Dart:
import 'dart:async';
import 'dart:convert';
import 'package:globe_runtime/globe_runtime.dart';
// Create a module from a JavaScript file
final module = FileModule(
name: 'MyModule',
filePath: 'lib/my_module.js',
);
// Call a JavaScript function
Future<String> callJsFunction(String functionName, {List<FFIConvertible> args = const []}) async {
final completer = Completer<String>();
module.callFunction(
functionName,
args: args,
onData: (data) {
if (data.hasError()) {
completer.completeError(data.error);
} else {
completer.complete(utf8.decode(data.data));
}
return true; // Unregister callback
},
);
return completer.future;
}
void main() async {
// Register the module
await module.register();
// Call JavaScript function
final result = await callJsFunction('greet', args: ['World'.toFFIType]);
print(result); // Output: Hello, World!
}
And your JavaScript module (lib/my_module.js
):
const sdk = {
init: function () {
return {};
},
functions: {
greet: function (_, name, callbackId) {
const greeting = `Hello, ${name}!`;
const result = new TextEncoder().encode(greeting);
Dart.send_value(callbackId, result);
},
},
};
export default sdk;
📚 Core Concepts
What is Globe Runtime?
Globe Runtime is a bridge that allows you to call JavaScript code from Dart applications. Unlike full-fledged JavaScript runtimes like Deno or Node.js, Globe Runtime is specifically designed to integrate Dart with JavaScript tools and libraries efficiently.
Key Features
- 🔗 Seamless Dart-JavaScript Interop: Call JavaScript functions directly from Dart
- 📦 JavaScript Ecosystem Access: Use any JavaScript library or npm package
- ⚡ Lightweight: Minimal overhead compared to full JavaScript runtimes
- 🔄 Bidirectional Communication: Send data both ways with proper type conversion
- 📡 Network Capabilities: Built-in
fetch
API support - 🔄 Streaming Support: Handle real-time data streams
- 🔧 Multiple Module Types: File-based, remote, or inline modules
How It Works
Globe Runtime embeds V8 within Rust and exposes key APIs through Dart FFI:
- Module Registration: Load JavaScript code into the runtime
- Function Calls: Execute JavaScript functions from Dart
- Data Exchange: Convert between Dart and JavaScript types automatically
- Callback Handling: Manage asynchronous responses and streams
🛠️ Module Types
Globe Runtime supports three types of modules:
1. FileModule
Load JavaScript code from a local file:
final module = FileModule(
name: 'MyModule',
filePath: 'lib/my_module.js',
);
2. RemoteModule
Load JavaScript code from a remote URL:
final module = RemoteModule(
name: 'RemoteModule',
url: 'https://example.com/module.js',
);
3. InlinedModule
Embed JavaScript code directly in your Dart code:
final module = InlinedModule(
name: 'InlineModule',
sourceCode: '''
const sdk = {
init: function () { return {}; },
functions: {
hello: function (_, callbackId) {
const result = new TextEncoder().encode('Hello from inline!');
Dart.send_value(callbackId, result);
},
},
};
export default sdk;
''',
);
📊 Data Types & Conversion
Globe Runtime automatically converts between Dart and JavaScript types:
Supported Types
Dart Type | JavaScript Type | FFI Type |
---|---|---|
String |
string |
FFIString |
int |
number |
FFIInt |
double |
number |
FFIDouble |
bool |
boolean |
FFIBool |
List<int> |
Uint8Array |
FFIBytes |
Map , List , Set |
object |
FFIJsonPayload |
Type Conversion Examples
// Basic types
module.callFunction('process', args: [
'Hello'.toFFIType, // String
42.toFFIType, // int
3.14.toFFIType, // double
true.toFFIType, // bool
]);
// Complex objects (automatically serialized as JSON)
final user = {
'name': 'John',
'age': 30,
'hobbies': ['coding', 'reading'],
};
module.callFunction('processUser', args: [user.toFFIType]);
🔄 Asynchronous Operations
Promise-based Functions
Handle JavaScript promises and async/await:
Future<Map<String, dynamic>> fetchData(String url) async {
final completer = Completer<Map<String, dynamic>>();
module.callFunction(
'fetchData',
args: [url.toFFIType],
onData: (data) {
if (data.hasError()) {
completer.completeError(data.error);
} else {
// Unpack JSON data
final result = Map<String, dynamic>.from(data.data.unpack());
completer.complete(result);
}
return true;
},
);
return completer.future;
}
JavaScript side:
const sdk = {
init: function () {
return {};
},
functions: {
fetchData: async function (_, url, callbackId) {
try {
const response = await fetch(url);
const data = await response.json();
const encoded = JsonPayload.encode(data);
if (!encoded) {
Dart.send_error(callbackId, "Failed to encode response");
return;
}
Dart.send_value(callbackId, encoded);
} catch (err) {
Dart.send_error(callbackId, `Fetch failed: ${err.message}`);
}
},
},
};
export default sdk;
Streaming Data
Handle real-time data streams:
Stream<String> streamData(String url) {
final streamController = StreamController<String>();
module.callFunction(
'streamData',
args: [url.toFFIType],
onData: (data) {
if (data.hasError()) {
streamController.addError(data.error);
return true;
}
if (data.hasData()) {
final chunk = utf8.decode(data.data);
streamController.add(chunk);
}
if (data.done) {
streamController.close();
return true;
}
return false; // Keep listening for more data
},
);
return streamController.stream;
}
JavaScript streaming:
const sdk = {
init: function () {
return {};
},
functions: {
streamData: async function (_, url, callbackId) {
try {
const response = await fetch(url);
for await (const chunk of response.body.values()) {
Dart.stream_value(callbackId, chunk);
}
Dart.stream_value_end(callbackId);
} catch (err) {
Dart.send_error(callbackId, `Stream failed: ${err.message}`);
}
},
},
};
export default sdk;
🏗️ Advanced Patterns
Module Initialization with Arguments
Pass initialization arguments to your JavaScript modules:
// Register with arguments
await module.register(args: [
'api_key_123'.toFFIType,
'production'.toFFIType,
]);
JavaScript module with initialization:
const sdk = {
init: function (apiKey, environment) {
return { apiKey, environment };
},
functions: {
makeRequest: function (state, endpoint, callbackId) {
const headers = {
Authorization: `Bearer ${state.apiKey}`,
"X-Environment": state.environment,
};
fetch(endpoint, { headers })
.then((response) => response.json())
.then((data) => {
const encoded = JsonPayload.encode(data);
Dart.send_value(callbackId, encoded);
})
.catch((err) => {
Dart.send_error(callbackId, err.message);
});
},
},
};
export default sdk;
Error Handling
Comprehensive error handling patterns:
Future<T> safeCall<T>(String functionName, {List<FFIConvertible> args = const []}) async {
final completer = Completer<T>();
try {
module.callFunction(
functionName,
args: args,
onData: (data) {
if (data.hasError()) {
completer.completeError(GlobeRuntimeException(data.error));
} else {
try {
final result = data.data.unpack();
completer.complete(result);
} catch (e) {
completer.completeError(DataParsingException(e.toString()));
}
}
return true;
},
);
} catch (e) {
completer.completeError(FunctionCallException(e.toString()));
}
return completer.future;
}
// Custom exception classes
class GlobeRuntimeException implements Exception {
final String message;
GlobeRuntimeException(this.message);
@override
String toString() => 'GlobeRuntimeException: $message';
}
class DataParsingException implements Exception {
final String message;
DataParsingException(this.message);
@override
String toString() => 'DataParsingException: $message';
}
class FunctionCallException implements Exception {
final String message;
FunctionCallException(this.message);
@override
String toString() => 'FunctionCallException: $message';
}
🔧 JavaScript Module Structure
Every JavaScript module must follow this structure:
const sdk = {
// Initialize the module and return state
init: function (...args) {
// args are the arguments passed from Dart during registration
return {
// Return any state you want to persist
config: args[0],
environment: args[1],
};
},
// Define your functions
functions: {
// Function signature: (state, ...args, callbackId)
myFunction: function (state, arg1, arg2, callbackId) {
// state: The object returned from init()
// arg1, arg2: Arguments passed from Dart
// callbackId: Unique identifier for this call
try {
// Your logic here
const result = processData(arg1, arg2);
// Send result back to Dart
const encoded = JsonPayload.encode(result);
Dart.send_value(callbackId, encoded);
} catch (error) {
// Send error back to Dart
Dart.send_error(callbackId, error.message);
}
},
// Async function example
asyncFunction: async function (state, url, callbackId) {
try {
const response = await fetch(url);
const data = await response.json();
const encoded = JsonPayload.encode(data);
Dart.send_value(callbackId, encoded);
} catch (error) {
Dart.send_error(callbackId, error.message);
}
},
},
};
export default sdk;
Available JavaScript APIs
In your JavaScript modules, you have access to:
Dart.send_value(callbackId, data)
: Send data back to DartDart.send_error(callbackId, error)
: Send error back to DartDart.stream_value(callbackId, chunk)
: Send streaming dataDart.stream_value_end(callbackId)
: End streamingJsonPayload.encode(data)
: Encode data as JSON payloadfetch()
: Make HTTP requestsTextEncoder
/TextDecoder
: Text encoding utilities
📦 Working with NPM Packages
Globe Runtime supports using NPM packages through a bundling approach. This allows you to use any JavaScript library in your Dart applications.
Why Bundle Instead of FileModule?
When using NPM packages, you cannot use FileModule
directly because:
- Module Resolution: Globe Runtime's internal module resolver expects proper
file://
URLs, but npm packages use different import mechanisms - Dependency Management: NPM packages have their own dependencies that need to be resolved and bundled together
- Browser Environment: Globe Runtime runs in a browser-like environment, not Node.js, so packages need to be bundled for browser compatibility
- Import/Export Compatibility: ES modules and CommonJS modules need to be properly transformed for the runtime environment
The bundling approach ensures all dependencies are included and properly formatted for Globe Runtime.
Approach 1: Using esbuild (JavaScript - Simpler)
Step 1: Create package.json
{
"name": "my_module",
"version": "1.0.0",
"type": "module",
"dependencies": {
"lodash": "^4.17.21"
},
"devDependencies": {
"esbuild": "^0.23.0"
},
"scripts": {
"build": "node build.mjs"
}
}
Step 2: Create build script (build.mjs)
import * as esbuild from 'esbuild';
import { writeFileSync } from 'fs';
import { resolve } from 'path';
import pkg from './package.json' with { type: 'json' };
const dartFileName = `${pkg.name}_source.dart`;
// Bundle the JavaScript using esbuild
const result = await esbuild.build({
entryPoints: ['lib/my_module.js'],
bundle: true,
minify: true,
format: 'esm',
platform: 'browser',
write: false,
});
// Get the bundled source code
const jsSource = result.outputFiles[0].text;
// Write the source code into the .dart file
const dartFileContent = `// GENERATED FILE — DO NOT MODIFY BY HAND
const packageVersion = '${pkg.version}';
const packageSource = r'''
${jsSource}
''';
`;
writeFileSync(resolve(`lib/${dartFileName}`), dartFileContent);
console.log(`✅ Created lib/${dartFileName}`);
Step 3: Create JavaScript module (lib/my_module.js)
import { sum } from "lodash";
const sdk = {
init: function () {
return {};
},
functions: {
addNumbers: function (_, numbers, callbackId) {
try {
const result = sum(numbers);
const encoded = JsonPayload.encode(result);
Dart.send_value(callbackId, encoded);
} catch (error) {
Dart.send_error(callbackId, error.message);
}
},
},
};
export default sdk;
Step 4: Install dependencies and build
# Install NPM packages
npm install
# Build the module (creates lib/my_module_source.dart)
npm run build
Step 5: Use in Dart
import 'dart:async';
import 'package:globe_runtime/globe_runtime.dart';
import 'my_module_source.dart';
class MyModule {
final Module _module;
MyModule._(this._module);
static Future<MyModule> create() async {
final module = InlinedModule(
name: 'MyModule',
sourceCode: packageSource,
);
await module.register();
return MyModule._(module);
}
Future<num> addNumbers(List<num> numbers) async {
final completer = Completer<num>();
_module.callFunction(
'addNumbers',
args: [numbers.toFFIType],
onData: (data) {
if (data.hasError()) {
completer.completeError(data.error);
} else {
final result = data.data.unpack() as num;
completer.complete(result);
}
return true;
},
);
return completer.future;
}
}
// Usage
void main() async {
final module = await MyModule.create();
final result = await module.addNumbers([1, 2, 3, 4, 5]);
print('Sum: $result'); // Output: Sum: 15
}
Approach 2: Using tsup (TypeScript - Recommended)
Step 1: Create package.json
{
"name": "my_module",
"version": "1.0.0",
"type": "module",
"dependencies": {
"lodash": "^4.17.21"
},
"devDependencies": {
"@globe/runtime_types": "https://gitpkg.now.sh/invertase/globe_runtime/packages/globe_runtime_ts?main",
"tsup": "^8.3.6",
"typescript": "^5.8.3"
},
"scripts": {
"build": "tsup"
}
}
Step 2: Create tsup.config.ts
import { defineConfig } from "tsup";
import { version, name } from "./package.json";
import { writeFileSync, readFileSync } from "fs";
import { resolve } from "path";
const outputFileName = `${name}_v${version}`;
const dartFileName = `${name}_source.dart`;
export default defineConfig({
entry: {
[outputFileName]: `lib/${name}.ts`,
},
onSuccess: async () => {
const actualFile = resolve(`dist/${outputFileName}.js`);
const dartFile = resolve(`lib/${dartFileName}`);
const jsSource = readFileSync(actualFile, "utf8");
writeFileSync(
dartFile,
`// GENERATED FILE — DO NOT MODIFY BY HAND
const packageVersion = '${version}';
const packageSource = r'''
${jsSource}
''';
`
);
console.log(`✅ Created lib/${dartFileName}`);
},
format: ["esm"],
minify: true,
bundle: true,
treeshake: true,
clean: true,
noExternal: [/.*/],
platform: "browser",
});
Step 3: Create TypeScript module (lib/my_module.ts)
import { sum } from "lodash";
const sdk = {
init: function () {
return {};
},
functions: {
addNumbers: function (_, numbers, callbackId) {
try {
const result = sum(numbers);
const encoded = JsonPayload.encode(result);
Dart.send_value(callbackId, encoded);
} catch (error) {
Dart.send_error(callbackId, error.message);
}
},
},
};
export default sdk;
Step 4: Install dependencies and build
# Install NPM packages
npm install
# Build the module (creates lib/my_module_source.dart)
npm run build
Step 5: Use in Dart
import 'dart:async';
import 'package:globe_runtime/globe_runtime.dart';
import 'my_module_source.dart';
class MyModule {
final Module _module;
MyModule._(this._module);
static Future<MyModule> create() async {
final module = InlinedModule(
name: 'MyModule',
sourceCode: packageSource,
);
await module.register();
return MyModule._(module);
}
Future<num> addNumbers(List<num> numbers) async {
final completer = Completer<num>();
_module.callFunction(
'addNumbers',
args: [numbers.toFFIType],
onData: (data) {
if (data.hasError()) {
completer.completeError(data.error);
} else {
final result = data.data.unpack() as num;
completer.complete(result);
}
return true;
},
);
return completer.future;
}
}
// Usage
void main() async {
final module = await MyModule.create();
final result = await module.addNumbers([1, 2, 3, 4, 5]);
print('Sum: $result'); // Output: Sum: 15
}
Why This Approach Works
- Bundling:
tsup
bundles all NPM dependencies into a single JavaScript file - TypeScript Support: Full type safety and IntelliSense
- No external dependencies: The bundled file contains everything needed
- InlinedModule: Uses the bundled code directly, avoiding file path and module resolution issues
- Proper type casting: Helper functions ensure Dart types are correctly cast from JavaScript objects
- Tree-shaking: Importing specific functions reduces bundle size
🚨 Error Handling & Debugging
Common Issues
- Module not found: Ensure the JavaScript file exists and follows the correct structure
- Function not found: Check that the function is exported in the
functions
object - Type conversion errors: Verify that you're using supported data types
- Memory leaks: Always return
true
fromonData
callbacks when done
Debugging Tips
// Enable verbose logging
void debugModule(Module module) async {
print('Module name: ${module.name}');
print('Module ready: ${module.isReady}');
print('Runtime version: ${GlobeRuntime.instance.version}');
final source = await module.source;
print('Module source preview: ${source.substring(0, 200)}...');
}
// Safe function calling with timeout
Future<T> callWithTimeout<T>(
Module module,
String function,
List<FFIConvertible> args, {
Duration timeout = const Duration(seconds: 30),
}) async {
return await module.callFunction(function, args: args, onData: (data) {
// Handle response
return true;
}).timeout(timeout);
}
🔒 Security Considerations
- Input Validation: Always validate data before passing to JavaScript
- Error Handling: Never expose sensitive information in error messages
- Module Sources: Be careful with remote modules from untrusted sources
- Memory Management: Dispose of modules when no longer needed
📈 Performance Tips
- Reuse Modules: Register modules once and reuse them
- Batch Operations: Group related function calls
- Streaming: Use streaming for large datasets
- Memory Cleanup: Dispose of the runtime when done
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.