Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 165 additions & 118 deletions Cargo.lock

Large diffs are not rendered by default.

32 changes: 16 additions & 16 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ authors = ["the Andromeda team"]
edition = "2024"
license = "Mozilla Public License 2.0"
repository = "https://github.com/tryandromeda/andromeda"
version = "0.1.13"
version = "0.1.14"

[workspace.dependencies]
andromeda-core = { path = "crates/core" }
Expand Down Expand Up @@ -44,22 +44,22 @@ libsui = "0.14.0"
log = "0.4.29"
lru = "0.18.0"
lsp-types = "0.97.0"
nova_vm = { git = "https://github.com/trynova/nova", rev = "a82b0408533bc93f857aa2ee5daee4f39f62dc6f" }
nova_vm = { git = "https://github.com/trynova/nova", rev = "bece61acd71a8a980d83cad2a0c3cd3e56614229" }
nu-ansi-term = "0.50.3"
owo-colors = "4.3.0"
oxc_codegen = "0.122.0"
oxc_ast = "0.122.0"
oxc_minifier = "0.122.0"
oxc_mangler = "0.122.0"
oxc_allocator = "0.122.0"
oxc_diagnostics = "0.122.0"
oxc-miette = { version = "2.7.0", features = ["fancy"] }
oxc_parser = "0.122.0"
oxc_semantic = "0.122.0"
oxc_span = "0.122.0"
oxc_transformer = "0.122.0"
oxc_codegen = "0.124.0"
oxc_ast = "0.124.0"
oxc_minifier = "0.124.0"
oxc_mangler = "0.124.0"
oxc_allocator = "0.124.0"
oxc_diagnostics = "0.124.0"
oxc-miette = { version = "2.7.1", features = ["fancy"] }
oxc_parser = "0.124.0"
oxc_semantic = "0.124.0"
oxc_span = "0.124.0"
oxc_transformer = "0.124.0"
rand = "0.10.1"
reedline = "0.46.0"
reedline = "0.48.0"
regex = "1.12.3"
rustls = "0.23.40"
rustls-pemfile = "2.2.0"
Expand All @@ -72,11 +72,11 @@ rusqlite = { version = "0.39.0", features = [
] }
saffron = "0.1.0"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
serde_json = "1.0.150"
serde_yaml = "0.9.34-deprecated"
socket2 = "0.6.3"

swash = "0.2.7"
swash = "0.2.8"
trust-dns-resolver = "0.23.2"
signal-hook = "0.4.4"
thiserror = "2.0.18"
Expand Down
1 change: 1 addition & 0 deletions crates/cli/src/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ pub fn run_repl_with_config(
Ok(Signal::CtrlD) | Ok(Signal::CtrlC) => {
std::process::exit(0);
}
Ok(_) => continue,
Err(err) => {
println!("Error reading input: {err}");
continue;
Expand Down
22 changes: 18 additions & 4 deletions crates/runtime/src/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use crate::ext::{LockMode, cron::CronId, interval::IntervalId, timeout::TimeoutId};
use nova_vm::{ecmascript::Value, engine::Global};
use nova_vm::{
ecmascript::{SharedDataBlock, Value},
engine::Global,
};
use tokio::net::TcpStream;
use tokio_rustls::client::TlsStream;

Expand Down Expand Up @@ -43,7 +46,14 @@ pub enum RuntimeMacroTask {
AbortLockRequest { name: String, lock_id: u64 },
/// Deliver a structured-clone serialized message from a worker thread to
/// the parent's `Worker` instance. JS-side dispatches `message` event.
WorkerDeliverMessage { worker_id: u32, payload: String },
/// `blocks` carries the SharedDataBlocks of any SharedArrayBuffers in
/// the message (in `sharedIndex` order); the receiving agent mints new
/// SharedArrayBuffer objects from them at dispatch time.
WorkerDeliverMessage {
worker_id: u32,
payload: String,
blocks: Vec<SharedDataBlock>,
},
/// Deliver a `messageerror` event to the parent's `Worker` instance.
WorkerDeliverMessageError { worker_id: u32, reason: String },
/// Deliver an `error` (ErrorEvent) to the parent's `Worker` instance.
Expand All @@ -55,8 +65,12 @@ pub enum RuntimeMacroTask {
colno: u32,
},
/// Deliver a parent-posted message into the worker realm; dispatches
/// `message` event on `self` (DedicatedWorkerGlobalScope).
WorkerSelfDeliverMessage { payload: String },
/// `message` event on `self` (DedicatedWorkerGlobalScope). See
/// [`RuntimeMacroTask::WorkerDeliverMessage`] for `blocks`.
WorkerSelfDeliverMessage {
payload: String,
blocks: Vec<SharedDataBlock>,
},
/// Worker-side close request: drives the runtime's event loop to exit.
WorkerSelfClose,
/// Posted by the parent-side forwarder thread when the worker has
Expand Down
6 changes: 6 additions & 0 deletions crates/runtime/src/ext/broadcast_channel/broadcast_channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,12 @@ class BroadcastChannel {
queueMicrotask(() => {
if (!this._closed && rid !== null) {
try {
// NOTE: op_broadcast_send is currently a same-process placeholder.
// When it is actually implemented, SharedArrayBuffers in the
// message must either be carried as a shared-data-block side
// payload (legal within the in-process agent cluster — see
// ext/workers for the pattern) or rejected with a DataCloneError.
// Passing only the cloned value silently drops SAB sharing.
__andromeda__.op_broadcast_send(
rid,
this.name,
Expand Down
6 changes: 3 additions & 3 deletions crates/runtime/src/ext/web/dom_exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

// deno-lint-ignore-file no-explicit-any

type DOMExceptionName =
| "IndexSizeError"
| "HierarchyRequestError"
Expand Down Expand Up @@ -51,8 +53,6 @@ const DOMExceptionCode: Record<DOMExceptionName, number> = {
DataCloneError: 25,
};
class DOMException extends Error {
override readonly name: DOMExceptionName;
readonly code: number;
static readonly INDEX_SIZE_ERR = 1;
static readonly HIERARCHY_REQUEST_ERR = 3;
static readonly WRONG_DOCUMENT_ERR = 4;
Expand Down Expand Up @@ -82,7 +82,7 @@ class DOMException extends Error {
) {
super(message);
this.name = name;
this.code = DOMExceptionCode[name] || 0;
(this as any).code = DOMExceptionCode[name] || 0;
Object.setPrototypeOf(this, new.target.prototype);
}
}
Expand Down
33 changes: 32 additions & 1 deletion crates/runtime/src/ext/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use andromeda_core::{Extension, ExtensionOp};
use nova_vm::{
ecmascript::{Agent, ArgumentsList, ExceptionType, JsResult, Value},
ecmascript::{Agent, ArgumentsList, ExceptionType, JsResult, SharedArrayBuffer, Value},
engine::{Bindable, GcScope, NoGcScope},
};

Expand Down Expand Up @@ -58,6 +58,12 @@ impl WebExt {
0,
false,
),
ExtensionOp::new(
"op_structured_clone_new_sab",
Self::op_structured_clone_new_sab,
1,
false,
),
],
storage: None,
files: vec![
Expand All @@ -73,6 +79,31 @@ impl WebExt {
}
}

/// Mint a new SharedArrayBuffer object sharing the data block of the
/// SharedArrayBuffer passed as the first argument.
pub fn op_structured_clone_new_sab<'gc>(
agent: &mut Agent,
_this: Value,
args: ArgumentsList,
gc: GcScope<'gc, '_>,
) -> JsResult<'gc, Value<'gc>> {
let gc = gc.into_nogc();
match args.get(0) {
Value::SharedArrayBuffer(sab) => {
let block = sab.get_data_block(agent).clone();
let new_sab = SharedArrayBuffer::new_from_data_block(agent, block, gc);
Ok(Value::SharedArrayBuffer(new_sab).unbind())
}
_ => Err(agent
.throw_exception(
ExceptionType::TypeError,
"op_structured_clone_new_sab expects a SharedArrayBuffer".to_string(),
gc,
)
.unbind()),
}
}

pub fn internal_btoa<'gc>(
agent: &mut Agent,
_this: Value,
Expand Down
101 changes: 86 additions & 15 deletions crates/runtime/src/ext/web/structured_clone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,47 @@ function isTransferable(value: any): value is Transferable {
}

/**
* Serialize a value to JSON representation for structured cloning
* Check if a value is a SharedArrayBuffer.
*/
function isSharedArrayBuffer(value: any): value is SharedArrayBuffer {
return value instanceof SharedArrayBuffer;
}

/**
* Check if a value is a typed array or DataView backed by a
* SharedArrayBuffer.
*/
function isSharedArrayBufferView(value: any): boolean {
return (
value !== null &&
typeof value === "object" &&
isSharedArrayBuffer(value.buffer) &&
typeof value.byteOffset === "number" &&
typeof value.byteLength === "number" &&
(typeof value.BYTES_PER_ELEMENT === "number" ||
typeof value.getInt8 === "function")
);
}

/**
* Check if a view value is a DataView. Only called on values already
* known to be views
*/
function isDataViewLike(value: any): boolean {
return (
value instanceof DataView || typeof value.BYTES_PER_ELEMENT !== "number"
);
}

/**
* Serialize a value to JSON representation for structured cloning.
*/
function structuredSerialize(
value: any,
transferList: Transferable[] = [],
): string {
): { json: string; sharedValues: any[] } {
const memory = new Map();
const sharedValues: any[] = [];
const transferSet = new Set(transferList);

function serializeInternal(val: any): any {
Expand Down Expand Up @@ -123,9 +157,15 @@ function structuredSerialize(
}
}

if (ArrayBuffer.isView(val)) {
if (isSharedArrayBuffer(val)) {
const sharedIndex = sharedValues.length;
sharedValues.push(val);
return { type: "SharedArrayBuffer", id, sharedIndex };
}

if (ArrayBuffer.isView(val) || isSharedArrayBufferView(val)) {
const buffer = serializeInternal(val.buffer);
if (val instanceof DataView) {
if (isDataViewLike(val)) {
return {
type: "DataView",
id,
Expand Down Expand Up @@ -198,10 +238,13 @@ function structuredSerialize(

try {
const serialized = serializeInternal(value);
return JSON.stringify({
root: serialized,
transferList: transferList.length,
});
return {
json: JSON.stringify({
root: serialized,
transferList: transferList.length,
}),
sharedValues,
};
} catch (error) {
if (error instanceof Error && error.name === "DataCloneError") {
throw error;
Expand All @@ -211,11 +254,12 @@ function structuredSerialize(
}

/**
* Deserialize a JSON representation back to JavaScript values
* Deserialize a JSON representation back to JavaScript values.
*/
function structuredDeserialize(
serializedData: string,
transferredValues: any[] = [],
sharedValues: any[] = [],
): any {
const data = JSON.parse(serializedData);
const memory = new Map();
Expand Down Expand Up @@ -284,6 +328,20 @@ function structuredDeserialize(
}
}
break;
case "SharedArrayBuffer": {
const sharedIndex = serialized.sharedIndex;
if (
typeof sharedIndex !== "number" ||
sharedIndex < 0 ||
sharedIndex >= sharedValues.length
) {
throw createDataCloneError("Missing shared SharedArrayBuffer");
}
result = __andromeda__.op_structured_clone_new_sab(
sharedValues[sharedIndex],
);
break;
}
case "DataView": {
const buffer = deserializeInternal(serialized.buffer);
result = new DataView(
Expand Down Expand Up @@ -370,6 +428,11 @@ function structuredClone<T = any>(
const transferList = options.transfer || [];

for (const transferable of transferList) {
if (isSharedArrayBuffer(transferable)) {
throw createDataCloneError(
"SharedArrayBuffer objects cannot be transferred",
);
}
if (!isTransferable(transferable)) {
throw createDataCloneError("Value in transfer list is not transferable");
}
Expand All @@ -381,7 +444,7 @@ function structuredClone<T = any>(
}
try {
if (transferList.length > 0) {
const serialized = structuredSerialize(value, transferList);
const { json, sharedValues } = structuredSerialize(value, transferList);

const transferredValues: any[] = [];
for (let i = 0; i < transferList.length; i++) {
Expand All @@ -399,10 +462,10 @@ function structuredClone<T = any>(
}
}

return structuredDeserialize(serialized, transferredValues) as T;
return structuredDeserialize(json, transferredValues, sharedValues) as T;
} else {
const serialized = structuredSerialize(value);
return structuredDeserialize(serialized) as T;
const { json, sharedValues } = structuredSerialize(value);
return structuredDeserialize(json, [], sharedValues) as T;
}
} catch (error) {
if (error instanceof Error && error.name === "DataCloneError") {
Expand All @@ -419,8 +482,13 @@ globalThis.structuredClone = structuredClone;
(globalThis as any).__andromeda_structured_serialize = function(
value: any,
transfer: Transferable[] = [],
): string {
): { json: string; sharedValues: any[] } {
for (const t of transfer) {
if (isSharedArrayBuffer(t)) {
throw createDataCloneError(
"SharedArrayBuffer objects cannot be transferred",
);
}
if (!isTransferable(t)) {
throw createDataCloneError("Value in transfer list is not transferable");
}
Expand All @@ -429,15 +497,18 @@ globalThis.structuredClone = structuredClone;
if (transferSet.size !== transfer.length) {
throw createDataCloneError("Transfer list contains duplicate values");
}
// NOTE: ArrayBuffer transfer is currently a copy (the validated list is
// not forwarded); SharedArrayBuffers are still collected and shared.
return structuredSerialize(value, []);
};

(globalThis as any).__andromeda_structured_deserialize = function(
payload: string,
transferredValues: any[] = [],
sharedValues: any[] = [],
): any {
if (typeof payload !== "string" || payload.length === 0) {
return undefined;
}
return structuredDeserialize(payload, transferredValues);
return structuredDeserialize(payload, transferredValues, sharedValues);
};
Loading
Loading