From cb0e6b50481755b32a3ee2adc52186b25698fdbb Mon Sep 17 00:00:00 2001 From: load1n9 <49134864+load1n9@users.noreply.github.com> Date: Sat, 13 Jun 2026 16:28:51 -0400 Subject: [PATCH] feat(ext/workers): pass SharedArrayBuffer through postMessage --- Cargo.lock | 283 ++++++++++-------- Cargo.toml | 32 +- crates/cli/src/repl.rs | 1 + crates/runtime/src/event_loop.rs | 22 +- .../broadcast_channel/broadcast_channel.ts | 6 + crates/runtime/src/ext/web/dom_exception.ts | 6 +- crates/runtime/src/ext/web/mod.rs | 33 +- .../runtime/src/ext/web/structured_clone.ts | 101 ++++++- crates/runtime/src/ext/workers/mod.rs | 130 +++++++- crates/runtime/src/ext/workers/worker.ts | 20 +- .../runtime/src/ext/workers/worker_global.ts | 17 +- crates/runtime/src/recommended.rs | 13 +- examples/workers/sab_atomics.ts | 21 ++ examples/workers/sab_atomics.worker.ts | 10 + examples/workers/sab_parallel_sum.ts | 42 +++ examples/workers/sab_parallel_sum.worker.ts | 19 ++ examples/workers/sab_shared_memory.ts | 20 ++ examples/workers/sab_shared_memory.worker.ts | 12 + tests/js/structured_clone_sab.test.ts | 172 +++++++++++ tests/js/workers/sab.worker.ts | 73 +++++ tests/js/workers/shared_array_buffer.test.ts | 142 +++++++++ types/internals.d.ts | 32 ++ 22 files changed, 1020 insertions(+), 187 deletions(-) create mode 100644 examples/workers/sab_atomics.ts create mode 100644 examples/workers/sab_atomics.worker.ts create mode 100644 examples/workers/sab_parallel_sum.ts create mode 100644 examples/workers/sab_parallel_sum.worker.ts create mode 100644 examples/workers/sab_shared_memory.ts create mode 100644 examples/workers/sab_shared_memory.worker.ts create mode 100644 tests/js/structured_clone_sab.test.ts create mode 100644 tests/js/workers/sab.worker.ts create mode 100644 tests/js/workers/shared_array_buffer.test.ts diff --git a/Cargo.lock b/Cargo.lock index 3fcb60ca..36c10d0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,7 +115,7 @@ dependencies = [ [[package]] name = "andromeda" -version = "0.1.13" +version = "0.1.14" dependencies = [ "andromeda-core", "andromeda-runtime", @@ -167,7 +167,7 @@ dependencies = [ [[package]] name = "andromeda-core" -version = "0.1.13" +version = "0.1.14" dependencies = [ "anyhow", "anymap", @@ -191,7 +191,7 @@ dependencies = [ [[package]] name = "andromeda-runtime" -version = "0.1.13" +version = "0.1.14" dependencies = [ "andromeda-core", "anyhow", @@ -590,6 +590,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.5.1" @@ -664,9 +673,9 @@ dependencies = [ [[package]] name = "calendrical_calculations" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7" +checksum = "5abbd6eeda6885048d357edc66748eea6e0268e3dd11f326fff5bd248d779c26" dependencies = [ "core_maths", "displaydoc", @@ -917,6 +926,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "convert_case" version = "0.10.0" @@ -1180,6 +1195,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cursor-icon" version = "1.2.0" @@ -1321,8 +1345,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", ] [[package]] @@ -2201,7 +2236,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", - "equivalent", "foldhash 0.2.0", ] @@ -2227,9 +2261,9 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ "hashbrown 0.16.1", ] @@ -2370,6 +2404,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.8.1" @@ -2476,9 +2519,9 @@ dependencies = [ [[package]] name = "icu_calendar" -version = "2.1.1" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e" +checksum = "a2b2acc6263f494f1df50685b53ff8e57869e47d5c6fe39c23d518ae9a4f3e45" dependencies = [ "calendrical_calculations", "displaydoc", @@ -2492,18 +2535,19 @@ dependencies = [ [[package]] name = "icu_calendar_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d" +checksum = "118577bcf3a0fa7c6ac0a7d6e951814da84ee56b9b1f68fb4d8d10b08cefaf4d" [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -2511,9 +2555,9 @@ dependencies = [ [[package]] name = "icu_locale" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60" +checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" dependencies = [ "icu_collections", "icu_locale_core", @@ -2526,9 +2570,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -2540,15 +2584,15 @@ dependencies = [ [[package]] name = "icu_locale_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c5f1d16b4c3a2642d3a719f18f6b06070ab0aef246a6418130c955ae08aa831" +checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993" [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2560,15 +2604,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -2580,15 +2624,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -3447,12 +3491,12 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nova_vm" version = "1.0.0" -source = "git+https://github.com/trynova/nova?rev=a82b0408533bc93f857aa2ee5daee4f39f62dc6f#a82b0408533bc93f857aa2ee5daee4f39f62dc6f" +source = "git+https://github.com/trynova/nova?rev=bece61acd71a8a980d83cad2a0c3cd3e56614229#bece61acd71a8a980d83cad2a0c3cd3e56614229" dependencies = [ "ahash", "ecmascript_atomics", "fast-float", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "lexical", "num-bigint", "num-traits", @@ -3921,9 +3965,9 @@ dependencies = [ [[package]] name = "oxc-miette" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a7ba54c704edefead1f44e9ef09c43e5cfae666bdc33516b066011f0e6ebf7" +checksum = "4356a61f2ed4c9b3610245215fbf48970eb277126919f87db9d0efa93a74245c" dependencies = [ "backtrace", "backtrace-ext", @@ -3942,9 +3986,9 @@ dependencies = [ [[package]] name = "oxc-miette-derive" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4faecb54d0971f948fbc1918df69b26007e6f279a204793669542e1e8b75eb3" +checksum = "b237422b014f8f8fff75bb9379e697d13f8d57551a22c88bebb39f073c1bf696" dependencies = [ "proc-macro2", "quote", @@ -3953,9 +3997,9 @@ dependencies = [ [[package]] name = "oxc_allocator" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff805b88789451a080b3c4d49fa0ebcd02dc6c0e370ed7a37ef954fbaf79915f" +checksum = "7cce9493fc18c7f2b9274baba258555d88cc1fab3ac3c4b293433b4f85ad097b" dependencies = [ "allocator-api2", "hashbrown 0.16.1", @@ -3965,9 +4009,9 @@ dependencies = [ [[package]] name = "oxc_ast" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addc03b644cd9f26996bb32883f5cf4f4e46a51d20f5fbdbf675c14b29d38e95" +checksum = "29366258930c55e2578e231995d2079cba12793429454fa892f01d985821a554" dependencies = [ "bitflags 2.10.0", "oxc_allocator", @@ -3982,9 +4026,9 @@ dependencies = [ [[package]] name = "oxc_ast_macros" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5950f9746248c26af04811e6db0523d354080637995be1dcc1c6bd3fca893bb2" +checksum = "617bf2f55d04db8d6fea9583569c7e4d9052297f76f2f8ae31b1f4ef8bcfd98e" dependencies = [ "phf 0.13.1", "proc-macro2", @@ -3994,9 +4038,9 @@ dependencies = [ [[package]] name = "oxc_ast_visit" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31da485219d7ca6810872ce84fbcc7d11d8492145012603ead79beaf1476dc92" +checksum = "4344952280d3e8cbfed93da2775c460bbded12f388404daa662dc0ee731e051f" dependencies = [ "oxc_allocator", "oxc_ast", @@ -4006,9 +4050,9 @@ dependencies = [ [[package]] name = "oxc_codegen" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8af47790edfd7cc2d35ff47b70a1746c73388cc498c7f470a9cdc35f89375c" +checksum = "fb16cc717d020bff40a1ef4da006da23b24990c277eec39586a454110c2ad628" dependencies = [ "bitflags 2.10.0", "cow-utils", @@ -4027,9 +4071,9 @@ dependencies = [ [[package]] name = "oxc_compat" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3103453f49b58f20dfb5d0d7be109c44975b436ad056fdb046db03e971ee9f64" +checksum = "31b7f19adf1f6b67312fc3ac1003d995c55b51fc6a77be17fc597139300aab24" dependencies = [ "cow-utils", "oxc-browserslist", @@ -4040,18 +4084,18 @@ dependencies = [ [[package]] name = "oxc_data_structures" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "623bffc9732a0d39f248a2e7655d6d1704201790e5a8777aa188a678f1746fe8" +checksum = "a3a309fcc491b31039bd2a77d8517278c198f566c284e9a18977dab801c05681" dependencies = [ "ropey", ] [[package]] name = "oxc_diagnostics" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c612203fb402e998169c3e152a9fc8e736faafea0f13287c92144d4b8bc7b55" +checksum = "a1c0f18571aac10db23d1ab681108102ac735c50142c5418ec8272e1d861219f" dependencies = [ "cow-utils", "oxc-miette", @@ -4060,9 +4104,9 @@ dependencies = [ [[package]] name = "oxc_ecmascript" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04c62e45b93f4257f5ca6d00f441e669ad52d98d36332394abe9f5527cf461d6" +checksum = "4eaddc891449b4c7d8720714d6939c99fc531054c8f7decba9ffdd1c70a7b67b" dependencies = [ "cow-utils", "num-bigint", @@ -4076,9 +4120,9 @@ dependencies = [ [[package]] name = "oxc_estree" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8794e3fbcd834e8ae4246dbd3121f9ee82c6ae60bc92615a276d42b6b62a2341" +checksum = "8dd0f39cc6f2014fc1a60a563903c6c6c88f856772d44f390fe876a575bd7c97" [[package]] name = "oxc_index" @@ -4092,9 +4136,9 @@ dependencies = [ [[package]] name = "oxc_mangler" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c902734a4b51a797bb6f3083bcf29c67db48433443521055f37ad13f48a55c81" +checksum = "2c5678dafbd00203db1cecd0e7304f62d80d817e46d8481d24fd99d74527af16" dependencies = [ "itertools 0.14.0", "oxc_allocator", @@ -4109,9 +4153,9 @@ dependencies = [ [[package]] name = "oxc_minifier" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e2b27bbd36243d7d583f561c9de84d1dfa0a55e28672c51e058a11882076e79" +checksum = "64dbaa7ee2c376f0d0783ee90e2682c6d7a51d16cc253db5f6974f52deb84022" dependencies = [ "cow-utils", "itoa", @@ -4135,9 +4179,9 @@ dependencies = [ [[package]] name = "oxc_parser" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "041125897019b72d23e6549d95985fe379354cf004e69cb811803109375fa91b" +checksum = "ecf347b9ba5fd251f215f0c44602fbec98c01ea4cf13ae2682167f33d8a8d0b4" dependencies = [ "bitflags 2.10.0", "cow-utils", @@ -4158,9 +4202,9 @@ dependencies = [ [[package]] name = "oxc_regular_expression" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "405e9515c3ae4c7227b3596219ec256dd883cb403db3a0d1c10146f82a894c93" +checksum = "922016d2def4d0a2b17c907bda16d6eb20516622ae818eb8662f69b353ba9f20" dependencies = [ "bitflags 2.10.0", "oxc_allocator", @@ -4174,9 +4218,9 @@ dependencies = [ [[package]] name = "oxc_semantic" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebb0597a0132e69aaecb010753b7450ffaf46cf45a389a7babe0e5e5825a911c" +checksum = "4cb5b9082935c4b0e076bc9c2add9f335fed1be4e6ae5e580747a33808348318" dependencies = [ "itertools 0.14.0", "memchr", @@ -4207,9 +4251,9 @@ dependencies = [ [[package]] name = "oxc_span" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "894327633e5dcaef8baf34815d68100297f9776e20371502458ea3c42b8a710b" +checksum = "9b4413a552b443c777dd2782bc49e719a20cb36434c9b196e9021259028ca74c" dependencies = [ "compact_str", "oxc-miette", @@ -4221,9 +4265,9 @@ dependencies = [ [[package]] name = "oxc_str" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e0b900b4f66db7d5b46a454532464861f675d03e16994040484d2c04151490" +checksum = "321abe830f84ab9c13ac43eadb625f7e8ccddab6a150732d1e5bf9dde043ef4f" dependencies = [ "compact_str", "hashbrown 0.16.1", @@ -4233,9 +4277,9 @@ dependencies = [ [[package]] name = "oxc_syntax" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5edd0173b4667e5a1775b5d37e06a78c796fab18ee095739186831f2c54400" +checksum = "11919498c468e21e0688d6c99e37c15f2825a64cfa2f9a0c99d6f767076011d8" dependencies = [ "bitflags 2.10.0", "cow-utils", @@ -4252,9 +4296,9 @@ dependencies = [ [[package]] name = "oxc_transformer" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a216c0a1291fcb42f6be51ce32d928921cf2a6e232e43e6339c8e48d0e4048f" +checksum = "80da5fe1be7e026fb17d3b948712d5d53b6257fa9806c851f405b842dcf761b8" dependencies = [ "base64", "compact_str", @@ -4281,9 +4325,9 @@ dependencies = [ [[package]] name = "oxc_traverse" -version = "0.122.0" +version = "0.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1d4f7d8539ccc032bf20a837b075a301a7846c6ded266a7a1889f0cfcae038" +checksum = "d8cbebcd5777218246cd65c540c4792600ba2bf3467671c068f6dc4da963cdd2" dependencies = [ "itoa", "oxc_allocator", @@ -4986,9 +5030,9 @@ dependencies = [ [[package]] name = "reedline" -version = "0.46.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9e7c532bfc2759bc8a28902c04e8b993fc13ebd085ee4292eb1b230fa9beef" +checksum = "201e8e0160cbe7bb5eb2caccf281e178e77fac95115ab31a2c29edc5593603c8" dependencies = [ "chrono", "crossterm", @@ -4998,7 +5042,6 @@ dependencies = [ "serde", "strip-ansi-escapes", "strum", - "strum_macros", "thiserror 2.0.18", "unicase", "unicode-segmentation", @@ -5186,7 +5229,7 @@ dependencies = [ "bitflags 2.10.0", "fallible-iterator", "fallible-streaming-iterator", - "hashlink 0.11.0", + "hashlink 0.11.1", "libsqlite3-sys", "smallvec", "sqlite-wasm-rs", @@ -5455,9 +5498,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -5540,13 +5583,13 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", - "digest", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -5557,7 +5600,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -5677,7 +5720,7 @@ dependencies = [ [[package]] name = "small_string" version = "1.0.0" -source = "git+https://github.com/trynova/nova?rev=a82b0408533bc93f857aa2ee5daee4f39f62dc6f#a82b0408533bc93f857aa2ee5daee4f39f62dc6f" +source = "git+https://github.com/trynova/nova?rev=bece61acd71a8a980d83cad2a0c3cd3e56614229#bece61acd71a8a980d83cad2a0c3cd3e56614229" dependencies = [ "wtf8", ] @@ -5796,9 +5839,9 @@ dependencies = [ [[package]] name = "sonic-rs" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16b39f36452a4fa6f14a481b02e9ba5c6842c6a44d0545da967eca122f2eed9" +checksum = "d971cc77a245ccf1756dbd1a87c3e7f709c0191464096510d43eec056d0f2c4f" dependencies = [ "ahash", "bumpalo", @@ -5911,20 +5954,22 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.3" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", "syn 2.0.117", ] @@ -5957,9 +6002,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "swash" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842f3cd369c2ba38966204f983eaa5e54a8e84a7d7159ed36ade2b6c335aae64" +checksum = "d1804632b66a35ca2b1d277eb0a138e10f46cb365b9a6d297e876b69ef79de43" dependencies = [ "skrifa", "yazi", @@ -6185,14 +6230,15 @@ dependencies = [ [[package]] name = "temporal_rs" -version = "0.2.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "654b89203889c8b90b96d275d1296c56df5f0ffee127213730c1b9895b69b33e" +checksum = "9a902a45282e5175186b21d355efc92564601efe6e2d92818dc9e333d50bd4de" dependencies = [ + "calendrical_calculations", "core_maths", "iana-time-zone", "icu_calendar", - "icu_locale", + "icu_locale_core", "ixdtf", "num-traits", "timezone_provider", @@ -6363,9 +6409,9 @@ dependencies = [ [[package]] name = "timezone_provider" -version = "0.2.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "838a6ce9f08d07a637682a7dda441b97dc34e4aaf0dbfd9ca3d7eaf6fe1e8495" +checksum = "c48f9b04628a2b813051e4dfe97c65281e49625eabd09ec343190e31e399a8c2" dependencies = [ "combine", "jiff-tzdb", @@ -6414,9 +6460,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "serde_core", @@ -6768,9 +6814,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "tzif" @@ -8154,9 +8200,9 @@ checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -8165,9 +8211,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -8251,20 +8297,21 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "serde", "yoke", @@ -8274,9 +8321,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 231ec440..26141528 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } @@ -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" @@ -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" diff --git a/crates/cli/src/repl.rs b/crates/cli/src/repl.rs index d4c4a092..01ed510f 100644 --- a/crates/cli/src/repl.rs +++ b/crates/cli/src/repl.rs @@ -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; diff --git a/crates/runtime/src/event_loop.rs b/crates/runtime/src/event_loop.rs index 2eaf79d0..a44fabf7 100644 --- a/crates/runtime/src/event_loop.rs +++ b/crates/runtime/src/event_loop.rs @@ -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; @@ -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, + }, /// 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. @@ -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, + }, /// Worker-side close request: drives the runtime's event loop to exit. WorkerSelfClose, /// Posted by the parent-side forwarder thread when the worker has diff --git a/crates/runtime/src/ext/broadcast_channel/broadcast_channel.ts b/crates/runtime/src/ext/broadcast_channel/broadcast_channel.ts index 732c22f3..f7c9acb9 100644 --- a/crates/runtime/src/ext/broadcast_channel/broadcast_channel.ts +++ b/crates/runtime/src/ext/broadcast_channel/broadcast_channel.ts @@ -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, diff --git a/crates/runtime/src/ext/web/dom_exception.ts b/crates/runtime/src/ext/web/dom_exception.ts index 380daae8..8464003a 100644 --- a/crates/runtime/src/ext/web/dom_exception.ts +++ b/crates/runtime/src/ext/web/dom_exception.ts @@ -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" @@ -51,8 +53,6 @@ const DOMExceptionCode: Record = { 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; @@ -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); } } diff --git a/crates/runtime/src/ext/web/mod.rs b/crates/runtime/src/ext/web/mod.rs index 9ff99504..fa2d83c4 100644 --- a/crates/runtime/src/ext/web/mod.rs +++ b/crates/runtime/src/ext/web/mod.rs @@ -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}, }; @@ -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![ @@ -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, diff --git a/crates/runtime/src/ext/web/structured_clone.ts b/crates/runtime/src/ext/web/structured_clone.ts index f8543c7d..2939b178 100644 --- a/crates/runtime/src/ext/web/structured_clone.ts +++ b/crates/runtime/src/ext/web/structured_clone.ts @@ -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 { @@ -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, @@ -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; @@ -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(); @@ -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( @@ -370,6 +428,11 @@ function structuredClone( 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"); } @@ -381,7 +444,7 @@ function structuredClone( } 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++) { @@ -399,10 +462,10 @@ function structuredClone( } } - 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") { @@ -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"); } @@ -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); }; diff --git a/crates/runtime/src/ext/workers/mod.rs b/crates/runtime/src/ext/workers/mod.rs index cf3c69b0..6f2f127d 100644 --- a/crates/runtime/src/ext/workers/mod.rs +++ b/crates/runtime/src/ext/workers/mod.rs @@ -16,8 +16,9 @@ use andromeda_core::{ }; use nova_vm::{ ecmascript::{ - Agent, ArgumentsList, ExceptionType, Function, HostDefined, InternalMethods, JsResult, - PropertyKey, String as NovaString, Value, parse_module, parse_script, script_evaluation, + Agent, ArgumentsList, Array, ExceptionType, Function, HostDefined, InternalMethods, + JsResult, PropertyKey, SharedArrayBuffer, SharedDataBlock, String as NovaString, Value, + parse_module, parse_script, script_evaluation, }, engine::{Bindable, GcScope}, }; @@ -36,15 +37,27 @@ struct WorkerInitData { use crate::RuntimeMacroTask; -#[derive(Debug)] pub(crate) enum WorkerInbound { - Message(String), + Message(String, Vec), Terminate, } -#[derive(Debug)] +// Manual impl: `SharedDataBlock` is not `Debug`. +impl std::fmt::Debug for WorkerInbound { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Message(payload, blocks) => f + .debug_tuple("Message") + .field(payload) + .field(&format_args!("{} shared block(s)", blocks.len())) + .finish(), + Self::Terminate => f.write_str("Terminate"), + } + } +} + pub(crate) enum WorkerOutbound { - Message(String), + Message(String, Vec), MessageError(String), UncaughtError { message: String, @@ -54,6 +67,31 @@ pub(crate) enum WorkerOutbound { }, } +impl std::fmt::Debug for WorkerOutbound { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Message(payload, blocks) => f + .debug_tuple("Message") + .field(payload) + .field(&format_args!("{} shared block(s)", blocks.len())) + .finish(), + Self::MessageError(reason) => f.debug_tuple("MessageError").field(reason).finish(), + Self::UncaughtError { + message, + filename, + lineno, + colno, + } => f + .debug_struct("UncaughtError") + .field("message", message) + .field("filename", filename) + .field("lineno", lineno) + .field("colno", colno) + .finish(), + } + } +} + pub struct WorkerRecord { pub id: u32, pub name: String, @@ -234,8 +272,12 @@ impl WorkersExt { std::thread::spawn(move || { while let Ok(msg) = rx_outbound.recv() { let task = match msg { - WorkerOutbound::Message(payload) => { - RuntimeMacroTask::WorkerDeliverMessage { worker_id, payload } + WorkerOutbound::Message(payload, blocks) => { + RuntimeMacroTask::WorkerDeliverMessage { + worker_id, + payload, + blocks, + } } WorkerOutbound::MessageError(reason) => { RuntimeMacroTask::WorkerDeliverMessageError { worker_id, reason } @@ -292,6 +334,34 @@ impl WorkersExt { Ok(Value::from_f64(agent, worker_id as f64, gc).unbind()) } + /// Collect the SharedDataBlocks of the SharedArrayBuffer arguments a + fn collect_shared_blocks<'gc>( + agent: &mut Agent, + args: &ArgumentsList, + start: usize, + gc: &GcScope<'gc, '_>, + ) -> JsResult<'gc, Vec> { + let mut blocks = Vec::with_capacity(args.as_slice().len().saturating_sub(start)); + for value in &args.as_slice()[start.min(args.as_slice().len())..] { + match value { + Value::SharedArrayBuffer(sab) => { + blocks.push(sab.get_data_block(agent).clone()); + } + _ => { + return Err(agent + .throw_exception( + ExceptionType::TypeError, + "worker postMessage shared values must be SharedArrayBuffers" + .to_string(), + gc.nogc(), + ) + .unbind()); + } + } + } + Ok(blocks) + } + pub fn op_worker_post_to_worker<'gc>( agent: &mut Agent, _this: Value, @@ -304,13 +374,16 @@ impl WorkersExt { .as_str(agent) .expect("worker payload is not valid UTF-8") .to_string(); + let blocks = Self::collect_shared_blocks(agent, &args, 2, &gc)?; Self::with_resources(agent, |res| { let workers = res.workers.lock().unwrap(); if let Some(record) = workers.get(&id) && !record.terminate_flag.load(Ordering::Acquire) { - let _ = record.tx_inbound.send(WorkerInbound::Message(payload)); + let _ = record + .tx_inbound + .send(WorkerInbound::Message(payload, blocks)); } }); @@ -328,10 +401,11 @@ impl WorkersExt { .as_str(agent) .expect("worker payload is not valid UTF-8") .to_string(); + let blocks = Self::collect_shared_blocks(agent, &args, 1, &gc)?; Self::with_resources(agent, |res| { if let Some(tx) = res.outbound_to_parent.lock().unwrap().as_ref() { - let _ = tx.send(WorkerOutbound::Message(payload)); + let _ = tx.send(WorkerOutbound::Message(payload, blocks)); } }); @@ -440,8 +514,8 @@ fn run_worker_thread( let mut sent_close = false; while let Ok(msg) = rx_inbound.recv() { let (task, is_close) = match msg { - WorkerInbound::Message(payload) => ( - RuntimeMacroTask::WorkerSelfDeliverMessage { payload }, + WorkerInbound::Message(payload, blocks) => ( + RuntimeMacroTask::WorkerSelfDeliverMessage { payload, blocks }, false, ), WorkerInbound::Terminate => (RuntimeMacroTask::WorkerSelfClose, true), @@ -643,6 +717,11 @@ pub fn worker_recommended_extensions() -> Vec { /// Resolve a function stashed on `globalThis` by property name and call /// it with the given string arguments. Errors are silently dropped. +/// +/// `shared_blocks` carries the SharedDataBlocks of any SharedArrayBuffers +/// in the message; new SharedArrayBuffer objects are minted from them +/// inside the realm (Values cannot be created outside `run_in_realm`) and +/// appended as a trailing JS Array argument. fn call_global_function( agent: &mut nova_vm::ecmascript::GcAgent, realm_root: &nova_vm::ecmascript::RealmRoot, @@ -650,6 +729,7 @@ fn call_global_function( prefix_args_f64: &[f64], prefix_args_str: &[&str], payload_args: Vec, + shared_blocks: Vec, ) { let _ = call_global_function_capturing_err( agent, @@ -658,6 +738,7 @@ fn call_global_function( prefix_args_f64, prefix_args_str, payload_args, + shared_blocks, ); } @@ -668,6 +749,7 @@ fn call_global_function_capturing_err( prefix_args_f64: &[f64], prefix_args_str: &[&str], payload_args: Vec, + shared_blocks: Vec, ) -> Option { agent.run_in_realm(realm_root, |agent, mut gc| -> Option { let global_obj = agent.current_realm(gc.nogc()).global_object(agent).unbind(); @@ -688,6 +770,26 @@ fn call_global_function_capturing_err( for s in payload_args.into_iter() { js_args.push(Value::from_string(agent, s, gc.nogc()).unbind()); } + if !shared_blocks.is_empty() { + // Mint a new SharedArrayBuffer per block in this agent — same + // backing memory, fresh wrapper objects — and pass them as one + // trailing Array argument. If the JS call throws, the wrappers + // are ordinary garbage-collectable objects; the blocks free + // with them. The deserializer rewraps each element again via + // op_structured_clone_new_sab (spec requires a fresh object + // per deserialization); these intermediates are short-lived + // holders, which is intentional. + let sab_values: Vec = shared_blocks + .into_iter() + .map(|block| { + Value::SharedArrayBuffer( + SharedArrayBuffer::new_from_data_block(agent, block, gc.nogc()).unbind(), + ) + }) + .collect(); + let sab_array = Array::from_slice(agent, sab_values.as_slice(), gc.nogc()); + js_args.push(Value::from(sab_array).unbind()); + } let call_result = func .call(agent, Value::Undefined, &mut js_args[..], gc.reborrow()) @@ -712,6 +814,7 @@ pub fn dispatch_parent_event( worker_id: u32, kind: &'static str, payload_args: Vec, + shared_blocks: Vec, ) { call_global_function( agent, @@ -720,6 +823,7 @@ pub fn dispatch_parent_event( &[worker_id as f64], &[kind], payload_args, + shared_blocks, ); } @@ -729,6 +833,7 @@ pub fn dispatch_self_event( host_data: &HostData, kind: &'static str, payload_args: Vec, + shared_blocks: Vec, ) { let err_msg = call_global_function_capturing_err( agent, @@ -737,6 +842,7 @@ pub fn dispatch_self_event( &[], &[kind], payload_args, + shared_blocks, ); if let Some(msg) = err_msg { // We are inside the worker — forward to the parent as ErrorEvent. diff --git a/crates/runtime/src/ext/workers/worker.ts b/crates/runtime/src/ext/workers/worker.ts index 2bdfec70..e78d33c7 100644 --- a/crates/runtime/src/ext/workers/worker.ts +++ b/crates/runtime/src/ext/workers/worker.ts @@ -126,11 +126,9 @@ class Worker { transfer = transferOrOptions.transfer; } - const serialized = (globalThis as any).__andromeda_structured_serialize( - message, - transfer, - ); - __andromeda__.op_worker_post_to_worker(this.#id, serialized); + const { json, sharedValues } = (globalThis as any) + .__andromeda_structured_serialize(message, transfer); + __andromeda__.op_worker_post_to_worker(this.#id, json, ...sharedValues); } terminate(): void { @@ -218,7 +216,7 @@ class Worker { worker_id: number, kind: string, arg1?: string, - arg2?: string, + arg2?: unknown, arg3?: string, arg4?: string, ): void { @@ -232,19 +230,23 @@ class Worker { if (kind === "message") { let data: unknown; try { - data = (globalThis as any).__andromeda_structured_deserialize(arg1 ?? ""); + data = (globalThis as any).__andromeda_structured_deserialize( + arg1 ?? "", + [], + (arg2 as unknown[]) ?? [], + ); } catch (_e) { worker.dispatchEvent((globalThis as any).__andromeda_make_message_event("messageerror", null)); return; } worker.dispatchEvent((globalThis as any).__andromeda_make_message_event("message", data)); } else if (kind === "messageerror") { - worker.dispatchEvent((globalThis as any).__andromeda_make_message_event("messageerror", arg1 ?? "")); + worker.dispatchEvent((globalThis as any).__andromeda_make_message_event("messageerror", null)); } else if (kind === "error") { worker.dispatchEvent( (globalThis as any).__andromeda_make_error_event( arg1 ?? "", - arg2 ?? "", + (arg2 as string) ?? "", Number(arg3 ?? 0) | 0, Number(arg4 ?? 0) | 0, ), diff --git a/crates/runtime/src/ext/workers/worker_global.ts b/crates/runtime/src/ext/workers/worker_global.ts index fcd303d9..582d0420 100644 --- a/crates/runtime/src/ext/workers/worker_global.ts +++ b/crates/runtime/src/ext/workers/worker_global.ts @@ -9,11 +9,16 @@ const selfEventTarget = new EventTarget(); (globalThis as any).__andromeda_dispatch_self_event = function( kind: string, arg1?: string, + arg2?: unknown, ): void { if (kind === "message") { let data: unknown; try { - data = (globalThis as any).__andromeda_structured_deserialize(arg1 ?? ""); + data = (globalThis as any).__andromeda_structured_deserialize( + arg1 ?? "", + [], + (arg2 as unknown[]) ?? [], + ); } catch (_e) { selfEventTarget.dispatchEvent((globalThis as any).__andromeda_make_message_event("messageerror", null)); return; @@ -21,7 +26,7 @@ const selfEventTarget = new EventTarget(); selfEventTarget.dispatchEvent((globalThis as any).__andromeda_make_message_event("message", data)); } else if (kind === "messageerror") { selfEventTarget.dispatchEvent( - (globalThis as any).__andromeda_make_message_event("messageerror", arg1 ?? ""), + (globalThis as any).__andromeda_make_message_event("messageerror", null), ); } }; @@ -43,11 +48,9 @@ const selfEventTarget = new EventTarget(); ) { transfer = transferOrOptions.transfer; } - const serialized = (globalThis as any).__andromeda_structured_serialize( - message, - transfer, - ); - __andromeda__.op_worker_post_to_parent(serialized); + const { json, sharedValues } = (globalThis as any) + .__andromeda_structured_serialize(message, transfer); + __andromeda__.op_worker_post_to_parent(json, ...sharedValues); } function workerClose(): void { diff --git a/crates/runtime/src/recommended.rs b/crates/runtime/src/recommended.rs index 9be4a67d..28ca84f2 100644 --- a/crates/runtime/src/recommended.rs +++ b/crates/runtime/src/recommended.rs @@ -247,13 +247,18 @@ pub fn recommended_eventloop_handler( println!("Aborted lock request {} for '{}'", lock_id, name); // TODO: Implement actual abort logic that cancels the associated promise } - RuntimeMacroTask::WorkerDeliverMessage { worker_id, payload } => { + RuntimeMacroTask::WorkerDeliverMessage { + worker_id, + payload, + blocks, + } => { crate::ext::workers::dispatch_parent_event( agent, realm_root, worker_id, "message", vec![payload], + blocks, ); } RuntimeMacroTask::WorkerDeliverMessageError { worker_id, reason } => { @@ -263,6 +268,7 @@ pub fn recommended_eventloop_handler( worker_id, "messageerror", vec![reason], + vec![], ); } RuntimeMacroTask::WorkerDeliverError { @@ -278,15 +284,17 @@ pub fn recommended_eventloop_handler( worker_id, "error", vec![message, filename, lineno.to_string(), colno.to_string()], + vec![], ); } - RuntimeMacroTask::WorkerSelfDeliverMessage { payload } => { + RuntimeMacroTask::WorkerSelfDeliverMessage { payload, blocks } => { crate::ext::workers::dispatch_self_event( agent, realm_root, host_data, "message", vec![payload], + blocks, ); } RuntimeMacroTask::WorkerForwarderClosed { worker_id } => { @@ -304,6 +312,7 @@ pub fn recommended_eventloop_handler( worker_id, "__cleanup__", vec![], + vec![], ); } RuntimeMacroTask::WorkerSelfClose => { diff --git a/examples/workers/sab_atomics.ts b/examples/workers/sab_atomics.ts new file mode 100644 index 00000000..538ca145 --- /dev/null +++ b/examples/workers/sab_atomics.ts @@ -0,0 +1,21 @@ +const sab = new SharedArrayBuffer(8); +const flag = new Int32Array(sab); // flag[0] = signal slot, flag[1] = answer + +const worker = new Worker( + new URL("./sab_atomics.worker.ts", import.meta.url), + { type: "module" }, +); + +worker.onmessage = (event: MessageEvent) => { + const msg = event.data as { state: string }; + if (msg.state === "waiting") { + console.log("worker is parked in Atomics.wait — waking it up…"); + Atomics.store(flag, 0, 1); + Atomics.notify(flag, 0); + } else if (msg.state === "woken") { + console.log("worker woke up and computed:", Atomics.load(flag, 1)); + worker.terminate(); + } +}; + +worker.postMessage(sab); diff --git a/examples/workers/sab_atomics.worker.ts b/examples/workers/sab_atomics.worker.ts new file mode 100644 index 00000000..5b16d96a --- /dev/null +++ b/examples/workers/sab_atomics.worker.ts @@ -0,0 +1,10 @@ +self.onmessage = (event: MessageEvent) => { + const flag = new Int32Array(event.data as SharedArrayBuffer); + self.postMessage({ state: "waiting" }); + const result = Atomics.wait(flag, 0, 0, 5000); + + console.log("worker Atomics.wait returned:", result); + + Atomics.store(flag, 1, Atomics.load(flag, 0) * 21); + self.postMessage({ state: "woken" }); +}; diff --git a/examples/workers/sab_parallel_sum.ts b/examples/workers/sab_parallel_sum.ts new file mode 100644 index 00000000..8941adec --- /dev/null +++ b/examples/workers/sab_parallel_sum.ts @@ -0,0 +1,42 @@ + +const N = 1_000_000; +const WORKERS = 4; + +const data = new Int32Array(new SharedArrayBuffer(N * 4)); +for (let i = 0; i < N; i++) data[i] = i % 10; + +// One result slot per worker. +const results = new Int32Array(new SharedArrayBuffer(WORKERS * 4)); + +let expected = 0; +for (let i = 0; i < N; i++) expected += i % 10; +console.log(`summing ${N} integers across ${WORKERS} workers…`); + +const chunk = Math.ceil(N / WORKERS); +let done = 0; + +for (let w = 0; w < WORKERS; w++) { + const worker = new Worker( + new URL("./sab_parallel_sum.worker.ts", import.meta.url), + { type: "module" }, + ); + worker.onmessage = () => { + worker.terminate(); + done += 1; + if (done === WORKERS) { + let total = 0; + for (let i = 0; i < WORKERS; i++) { + console.log(`worker ${i} partial sum: ${Atomics.load(results, i)}`); + total += Atomics.load(results, i); + } + console.log(`total: ${total} (expected ${expected})`, total === expected ? "✓" : "✗"); + } + }; + worker.postMessage({ + data: data.buffer, + results: results.buffer, + index: w, + start: w * chunk, + end: Math.min((w + 1) * chunk, N), + }); +} diff --git a/examples/workers/sab_parallel_sum.worker.ts b/examples/workers/sab_parallel_sum.worker.ts new file mode 100644 index 00000000..9e7b0aba --- /dev/null +++ b/examples/workers/sab_parallel_sum.worker.ts @@ -0,0 +1,19 @@ +interface Job { + data: SharedArrayBuffer; + results: SharedArrayBuffer; + index: number; + start: number; + end: number; +} + +self.onmessage = (event: MessageEvent) => { + const job = event.data as Job; + const data = new Int32Array(job.data); + const results = new Int32Array(job.results); + + let sum = 0; + for (let i = job.start; i < job.end; i++) sum += data[i]; + + Atomics.store(results, job.index, sum); + self.postMessage("done"); +}; diff --git a/examples/workers/sab_shared_memory.ts b/examples/workers/sab_shared_memory.ts new file mode 100644 index 00000000..a5cd8b16 --- /dev/null +++ b/examples/workers/sab_shared_memory.ts @@ -0,0 +1,20 @@ +const sab = new SharedArrayBuffer(16); +const view = new Int32Array(sab); + +const worker = new Worker( + new URL("./sab_shared_memory.worker.ts", import.meta.url), + { type: "module" }, +); + +view[0] = 42; + +worker.onmessage = () => { + console.log("parent reads what the worker wrote:", view[1], view[2], view[3]); + + view[0] = 1000; + console.log("shared memory works: no bytes were copied!"); + worker.terminate(); +}; + +console.log("parent wrote 42 into slot 0, posting the buffer to the worker…"); +worker.postMessage(sab); diff --git a/examples/workers/sab_shared_memory.worker.ts b/examples/workers/sab_shared_memory.worker.ts new file mode 100644 index 00000000..848818cd --- /dev/null +++ b/examples/workers/sab_shared_memory.worker.ts @@ -0,0 +1,12 @@ +self.onmessage = (event: MessageEvent) => { + const sab = event.data as SharedArrayBuffer; + const view = new Int32Array(sab); + + console.log("worker reads what the parent wrote:", view[0]); + + view[1] = 7; + view[2] = 8; + view[3] = 9; + + self.postMessage("done"); +}; diff --git a/tests/js/structured_clone_sab.test.ts b/tests/js/structured_clone_sab.test.ts new file mode 100644 index 00000000..001c60af --- /dev/null +++ b/tests/js/structured_clone_sab.test.ts @@ -0,0 +1,172 @@ +// deno-lint-ignore-file no-explicit-any +let passed = 0; +let total = 0; + +function check(name: string, cond: boolean) { + total += 1; + if (cond) { + passed += 1; + } else { + throw new Error(`FAIL ${name}`); + } +} + +{ + const sab = new SharedArrayBuffer(16); + (sab as any).expando = "x"; + const clone = structuredClone(sab); + check("basic: instance", clone instanceof SharedArrayBuffer); + check("basic: new wrapper", clone !== sab); + check("basic: byteLength", clone.byteLength === 16); + check("basic: no expando", (clone as any).expando === undefined); + + const a = new Int32Array(sab); + const b = new Int32Array(clone); + a[0] = 42; + check("basic: write through original visible in clone", b[0] === 42); + b[1] = 7; + check("basic: write through clone visible in original", a[1] === 7); +} + +{ + const sab = new SharedArrayBuffer(16); + const view = new Uint8Array(sab, 4, 8); + const out = structuredClone({ a: sab, view }); + check("graph: buffer cloned", out.a instanceof SharedArrayBuffer); + check("graph: view byteOffset", out.view.byteOffset === 4); + check("graph: view length", out.view.length === 8); + check("graph: view.buffer identity", out.view.buffer === out.a); + const original = new Uint8Array(sab); + out.view[0] = 99; // writes at byte offset 4 + check("graph: shared through view", original[4] === 99); +} + +{ + const sab = new SharedArrayBuffer(16); + const ta = structuredClone(new Int32Array(sab)); + check("bare view: length", ta.length === 4); + const orig = new Int32Array(sab); + ta[2] = 1234; + check("bare view: shared", orig[2] === 1234); + + const dv = structuredClone(new DataView(sab, 2, 4)); + check("bare DataView: byteOffset", dv.byteOffset === 2); + check("bare DataView: byteLength", dv.byteLength === 4); + const bytes = new Uint8Array(sab); + bytes[2] = 0xAB; + check("bare DataView: shared", dv.getUint8(0) === 0xAB); +} + +{ + const sab = new SharedArrayBuffer(8); + const out = structuredClone([sab, sab]); + check("dup: same wrapper", out[0] === out[1]); + check("dup: new wrapper", out[0] !== sab); +} + +{ + const z1 = new SharedArrayBuffer(0); + const z2 = new SharedArrayBuffer(0); + const out = structuredClone([z1, z2]); + check("zero: byteLength", out[0].byteLength === 0); + check("zero: distinct objects stay distinct", out[0] !== out[1]); +} + +{ + const sab = new SharedArrayBuffer(8, { maxByteLength: 32 }); + const clone = structuredClone(sab); + check("growable: flag", clone.growable === true); + check("growable: maxByteLength", clone.maxByteLength === 32); + sab.grow(16); + check("growable: grow visible through clone", clone.byteLength === 16); +} + +{ + const sab = new SharedArrayBuffer(8); + let threw = false; + let code = 0; + try { + structuredClone(sab, { transfer: [sab as any] }); + } catch (e) { + threw = e instanceof DOMException && e.name === "DataCloneError"; + code = (e as any).code; + } + check("transfer: SAB in transfer list throws DataCloneError", threw); + check("transfer: DataCloneError code", code === 25); +} + +{ + const sab = new SharedArrayBuffer(8); + const impostor = { buffer: sab, byteOffset: 0, byteLength: 8, extra: 1 }; + const out = structuredClone(impostor); + check("impostor: stays a plain object", out.extra === 1); + check("impostor: buffer cloned as SAB", out.buffer instanceof SharedArrayBuffer); + check("impostor: byteOffset survives", out.byteOffset === 0); +} + +{ + const ab = new ArrayBuffer(4); + new Uint8Array(ab)[0] = 5; + const sab = new SharedArrayBuffer(4); + const out = structuredClone({ a: ab, b: sab }, { transfer: [ab] }); + check("mixed: AB arrives", new Uint8Array(out.a)[0] === 5); + check("mixed: SAB is shared", out.b instanceof SharedArrayBuffer); + new Uint8Array(out.b)[0] = 9; + check("mixed: SAB shares block", new Uint8Array(sab)[0] === 9); +} + +function messagePortRoundTrip(): Promise { + return new Promise((resolve, reject) => { + const { port1, port2 } = new MessageChannel(); + const sab = new SharedArrayBuffer(8); + port2.onmessage = (event: MessageEvent) => { + try { + const got = event.data as SharedArrayBuffer; + check("port: instance", got instanceof SharedArrayBuffer); + check("port: new wrapper", got !== sab); + new Int32Array(got)[0] = 11; + check("port: shared", new Int32Array(sab)[0] === 11); + port1.close(); + resolve(); + } catch (e) { + reject(e); + } + }; + port1.postMessage(sab); + }); +} + +function broadcastChannelRoundTrip(): Promise { + return new Promise((resolve, reject) => { + const rx = new BroadcastChannel("sab-test"); + const tx = new BroadcastChannel("sab-test"); + const sab = new SharedArrayBuffer(8); + rx.onmessage = (event: MessageEvent) => { + try { + const got = event.data as SharedArrayBuffer; + check("broadcast: instance", got instanceof SharedArrayBuffer); + check("broadcast: new wrapper", got !== sab); + new Int32Array(got)[0] = 21; + check("broadcast: shared", new Int32Array(sab)[0] === 21); + rx.close(); + tx.close(); + resolve(); + } catch (e) { + rx.close(); + tx.close(); + reject(e); + } + }; + tx.postMessage(sab); + }); +} + +messagePortRoundTrip() + .then(broadcastChannelRoundTrip) + .then(() => { + console.log(`structured_clone_sab: ${passed}/${total} passed`); + }) + .catch((e) => { + console.log(`FAIL async: ${e}`); + throw e; + }); diff --git a/tests/js/workers/sab.worker.ts b/tests/js/workers/sab.worker.ts new file mode 100644 index 00000000..f65f164d --- /dev/null +++ b/tests/js/workers/sab.worker.ts @@ -0,0 +1,73 @@ +// deno-lint-ignore-file no-explicit-any +self.onmessage = (event: MessageEvent) => { + const msg = event.data as any; + switch (msg.cmd) { + case "fill": { + const ia = new Int32Array(msg.sab); + for (let i = 0; i < ia.length; i++) ia[i] = (i + 1) * 10; + self.postMessage({ cmd: "filled" }); + break; + } + case "create": { + const sab = new SharedArrayBuffer(msg.byteLength); + const ia = new Int32Array(sab); + ia[0] = 7; + self.postMessage({ cmd: "created", sab }); + break; + } + case "view": { + const view = msg.view as Int32Array; + view[0] = 555; + self.postMessage({ + cmd: "viewdone", + byteOffset: view.byteOffset, + length: view.length, + }); + break; + } + case "grow": { + (msg.sab as SharedArrayBuffer).grow(msg.to); + self.postMessage({ cmd: "grown" }); + break; + } + case "dup": { + self.postMessage({ + cmd: "dupresult", + same: msg.a === msg.b, + instance: msg.a instanceof SharedArrayBuffer, + }); + break; + } + case "zero": { + self.postMessage({ + cmd: "zeroresult", + byteLength: (msg.sab as SharedArrayBuffer).byteLength, + }); + break; + } + case "wait": { + const ia = new Int32Array(msg.sab); + self.postMessage({ cmd: "waiting" }); + const result = Atomics.wait(ia, 0, 0, 5000); + self.postMessage({ cmd: "woken", result, value: Atomics.load(ia, 0) }); + break; + } + case "messageerror": { + (globalThis as any).__andromeda__.op_worker_post_messageerror_to_parent( + "deliberate messageerror", + ); + break; + } + case "corrupt": { + (globalThis as any).__andromeda__.op_worker_post_to_parent( + JSON.stringify({ + root: { type: "SharedArrayBuffer", id: 0, sharedIndex: 0 }, + transferList: 0, + }), + ); + break; + } + default: + throw new Error(`unknown command: ${msg && msg.cmd}`); + } +}; diff --git a/tests/js/workers/shared_array_buffer.test.ts b/tests/js/workers/shared_array_buffer.test.ts new file mode 100644 index 00000000..7883aca5 --- /dev/null +++ b/tests/js/workers/shared_array_buffer.test.ts @@ -0,0 +1,142 @@ +// deno-lint-ignore-file no-explicit-any +const worker = new Worker( + new URL("./sab.worker.ts", import.meta.url), + { type: "module" }, +); + +let passed = 0; +let total = 0; + +function check(name: string, cond: boolean) { + total += 1; + if (cond) { + passed += 1; + } else { + console.log(`FAIL ${name}`); + } +} + +function send(msg: unknown): Promise { + return new Promise((resolve) => { + worker.onmessage = (event: MessageEvent) => resolve(event.data); + worker.postMessage(msg); + }); +} + +async function main() { + { + const sab = new SharedArrayBuffer(16); + const reply = await send({ cmd: "fill", sab }); + check("fill: ack", reply.cmd === "filled"); + const ia = new Int32Array(sab); + check( + "fill: writes visible in main", + ia[0] === 10 && ia[1] === 20 && ia[2] === 30 && ia[3] === 40, + ); + } + + { + const reply = await send({ cmd: "create", byteLength: 8 }); + check("create: instance", reply.sab instanceof SharedArrayBuffer); + const ia = new Int32Array(reply.sab); + check("create: worker write visible", ia[0] === 7); + ia[1] = 8; + } + + { + const sab = new SharedArrayBuffer(32); + const view = new Int32Array(sab, 8, 4); + const reply = await send({ cmd: "view", view }); + check("view: byteOffset", reply.byteOffset === 8); + check("view: length", reply.length === 4); + check("view: shared", new Int32Array(sab, 8, 4)[0] === 555); + } + + { + const sab = new SharedArrayBuffer(8, { maxByteLength: 64 }); + const reply = await send({ cmd: "grow", sab, to: 32 }); + check("grow: ack", reply.cmd === "grown"); + check("grow: visible in main", sab.byteLength === 32); + check("grow: still growable", sab.growable === true); + } + + { + const sab = new SharedArrayBuffer(8); + const reply = await send({ cmd: "dup", a: sab, b: sab }); + check("dup: same identity", reply.same === true); + check("dup: instance", reply.instance === true); + } + + { + const sab = new SharedArrayBuffer(0); + const reply = await send({ cmd: "zero", sab }); + check("zero: byteLength", reply.byteLength === 0); + } + { + const sab = new SharedArrayBuffer(8); + const ia = new Int32Array(sab); + const woken = new Promise((resolve) => { + worker.onmessage = (event: MessageEvent) => { + const msg = event.data as any; + if (msg.cmd === "waiting") { + Atomics.store(ia, 0, 1); + Atomics.notify(ia, 0); + } else if (msg.cmd === "woken") { + resolve(msg); + } + }; + }); + worker.postMessage({ cmd: "wait", sab }); + const msg = await woken; + check( + "atomics: worker woke", + msg.result === "ok" || msg.result === "not-equal", + ); + check("atomics: value", msg.value === 1); + } + + { + const me = new Promise((resolve) => { + worker.onmessageerror = (event: MessageEvent) => resolve(event); + }); + worker.postMessage({ cmd: "messageerror" }); + const event = await me; + check("messageerror: data is null", (event as any).data === null); + worker.onmessageerror = null; + } + + { + const me = new Promise((resolve) => { + worker.onmessageerror = (event: MessageEvent) => resolve(event); + }); + worker.postMessage({ cmd: "corrupt" }); + const event = await me; + check("corrupt: messageerror data is null", (event as any).data === null); + worker.onmessageerror = null; + const sab = new SharedArrayBuffer(16); + const reply = await send({ cmd: "fill", sab }); + check("corrupt: runtime continues", reply.cmd === "filled"); + check("corrupt: memory still shares", new Int32Array(sab)[0] === 10); + } + + { + const sab = new SharedArrayBuffer(8); + worker.postMessage({ cmd: "fill", sab }); + worker.terminate(); + const ia = new Int32Array(sab); + ia[0] = 1; + check("terminate: original SAB usable", ia[0] === 1); + } + + if (passed !== total) { + throw new Error(`shared_array_buffer: ${passed}/${total} passed`); + } + console.log(`shared_array_buffer: ${passed}/${total} passed`); +} + +worker.onerror = (event: ErrorEvent) => { + worker.terminate(); + throw new Error(`worker reported error: ${event.message}`); +}; + +main(); diff --git a/types/internals.d.ts b/types/internals.d.ts index 94e82c67..aa8db7fc 100644 --- a/types/internals.d.ts +++ b/types/internals.d.ts @@ -1308,6 +1308,38 @@ declare namespace __andromeda__ { export function internal_battery_info(): string; + /** + * The `op_structured_clone_new_sab` function mints a new SharedArrayBuffer + * object sharing the data block of the given SharedArrayBuffer. Used by + * structured deserialization (a serialized SAB deserializes to a NEW + * object backed by the SAME shared memory). + */ + export function op_structured_clone_new_sab( + sab: SharedArrayBuffer, + ): SharedArrayBuffer; + + /** + * The `op_worker_post_to_worker` function delivers a structured-clone + * JSON payload to a worker. SharedArrayBuffers collected by the + * serializer are spread as trailing arguments (in sharedIndex order). + */ + export function op_worker_post_to_worker( + workerId: number, + payload: string, + ...sharedValues: SharedArrayBuffer[] + ): void; + + /** + * The `op_worker_post_to_parent` function delivers a structured-clone + * JSON payload from a worker to its parent. SharedArrayBuffers collected + * by the serializer are spread as trailing arguments (in sharedIndex + * order). + */ + export function op_worker_post_to_parent( + payload: string, + ...sharedValues: SharedArrayBuffer[] + ): void; + /** * The `internal_add_signal_listener` function adds a signal listener for the specified signal. * The signal can be a string like "SIGINT", "SIGTERM", etc.