WebAssembly'den eşzamansız web API'lerini kullanma

Ingvar Stepanyan
Ingvar Stepanyan

Web'deki I/O API'leri eşzamansız olmakla birlikte çoğu sistem dilinde eşzamanlıdır. Zaman WebAssembly'de kod derlemek için bir tür API'yi birbirine bağlamanız gerekir. Bu köprü de Eşzamansız. Bu gönderide, Asyncify'ın ne zaman ve nasıl kullanılacağını ve arka planda nasıl çalıştığını öğreneceksiniz.

Sistem dillerinde G/Ç

C dilinde basit bir örnekle başlayacağım. Kullanıcının adını bir dosyadan okumak ve selamlamak istediğinizi söyleyin bir "Hello, (username)!" (Merhaba, (kullanıcı adı)) mesajıyla mesaj:

#include <stdio.h>

int main() {
    FILE *stream = fopen("name.txt", "r");
    char name[20+1];
    size_t len = fread(&name, 1, 20, stream);
    name[len] = '\0';
    fclose(stream);
    printf("Hello, %s!\n", name);
    return 0;
}

Bu örnek çok fazla bir işlev sunmasa da, bir uygulamada bulabileceğiniz bir şeyi zaten göstermektedir dış dünyadan gelen bazı girdileri okur, onları dahili olarak işler ve dış dünyaya dönüştürür. Dış dünyayla bu tür etkileşimin tümü birkaç adımda gerçekleşir. genellikle giriş-çıkış işlevleri olarak adlandırılan, G/Ç olarak da kısaltılan fonksiyonlar vardır.

C'deki adı okumak için en az iki önemli G/Ç çağrısına ihtiyacınız vardır: fopen, dosyayı açmak ve İçindeki verileri okumak için fread. Verileri aldıktan sonra başka bir G/Ç işlevini kullanabilirsiniz printf düğmesini de kullanabilirsiniz.

Bu fonksiyonlar ilk bakışta oldukça basit görünür ve her şey için iki kez düşünmeniz gerekmez kullanılan araçlardır. Ancak ortama bağlı olarak bir sürü şey olur:

  • Giriş dosyası yerel bir sürücüde bulunuyorsa, uygulamanın dosyayı bulmak, izinleri kontrol etmek, okumak için açmak ve ardından İstenen bayt sayısı alınana kadar blok bazında okuma. Bu süreç oldukça yavaş olabilir. değişiklik gösterir.
  • Veya giriş dosyası, eklenmiş bir ağ konumunda bulunuyor olabilir. Bu durumda, yığın da artık dahil olacak ve böylece karmaşıklığı, gecikmeyi ve potansiyel her işlem için yeniden denenir.
  • Son olarak, printf ürününün bile bir şeyleri konsola yazdıracağı garanti edilmez ve bu nedenle bir dosyaya veya ağ konumuna eklemeye çalışın. Bu durumda, yukarıdaki adımların aynısı uygulanması gerekir.

Kısacası, G/Ç yavaş olabilir ve belirli bir çağrının ne kadar süreceğini tahmin edemezsiniz. koda hızlıca göz atın. Bu işlem çalışırken uygulamanızın tamamı donmuş olarak görünür ve kullanıcıya yanıt vermeyi bırakır.

Bu da C veya C++ ile sınırlı değildir. Çoğu sistem dili, tüm G/Ç'yi şu biçimde sunar: senkronize API'ler. Örneğin, örneği Rust'a çevirirseniz API daha basit görünebilir ancak aynı ilkeler geçerlidir. Bir arama yapar ve eşzamanlı olarak sonucu döndürmesini beklersiniz. tüm pahalı işlemleri gerçekleştirir ve sonunda sonucu tek bir çağrı:

fn main() {
    let s = std::fs::read_to_string("name.txt");
    println!("Hello, {}!", s);
}

Peki bu örneklerden herhangi birini WebAssembly'de derleyip web'de neler var? Örnek vermek gerekirse "dosya okuma" işlemi şu dile nasıl çevrilir? Bu bazı depolama alanındaki verileri okumanız gerekir.

Web'in eşzamansız modeli

Web'de, bellek içi depolama (JS) gibi, eşleme yapabileceğiniz çeşitli depolama seçenekleri mevcuttur. nesneler) localStorage, IndexedDB, sunucu tarafı depolama, ve yeni bir File System Access API.

Ancak bu API'lerden yalnızca ikisi (bellek içi depolama ve localStorage) kullanılabilir. senkronize edilmiştir. Her ikisi de depolayabileceğiniz öğeler ve süreler konusunda en sınırlayıcı seçeneklerdir. Tümü diğer seçenekler yalnızca eşzamansız API'ler sağlar.

Bu, web’de kod yürütmenin temel özelliklerinden biridir. eşzamansız olması gerekir.

Bunun nedeni, web'in geçmişte tek iş parçacıklı olmasıdır ve kullanıcı arayüzüne dokunan tüm kullanıcı kodları kullanıcı arayüzü ile aynı iş parçacığında çalışması gerekir. Çözüm, kalite yönetimi gibi diğer önemli görevlerle etkinlik işlemeyi ve oluşturma sürecini kolaylaştırmak için tasarlanmıştır. Herhangi bir JavaScript veya WebAssembly'nin "dosya okuma" işlemi başlatabilmesi için diğer her şeyi (sekmenin tamamı, milisaniyeler ile birkaç saniye arasında sona erene kadar tarayıcının tamamı üzerine gelir.

Bunun yerine, kodun yalnızca bir G/Ç işlemini yürütülecek bir geri çağırmayla birlikte programlamasına izin verilir kontrol edin. Bu tür geri çağırma işlevleri, tarayıcının etkinlik döngüsünün bir parçası olarak yürütülür. olmeyeceğim ancak etkinlik döngüsünün arka planında nasıl işlediğini öğrenmek isterseniz çıkış yapmak Görevler, mikro görevler, sıralar ve programlar ayrıntılı bir şekilde açıklıyor.

Kısacası, tarayıcı tüm kod parçalarını sonsuz bir döngü içinde çalıştırır. sıradan tek tek alıyorduk. Bir etkinlik tetiklendiğinde, tarayıcı ve sonraki döngü yinelemesinde sıradan çıkarılıp yürütülür. Bu mekanizma yalnızca tek kullanımlık fonksiyonlar kullanılırken eş zamanlılık simülasyonu ve çok sayıda paralel işlem takip edebilirsiniz.

Bu mekanizmayla ilgili unutulmaması gereken önemli nokta, özel JavaScript'inizin (veya WebAssembly) kodu yürütülür, etkinlik döngüsü engellenir ve bu işlem devam ederken her tür harici işleyici, etkinlik, G/Ç gibi öğeler içerir. G/Ç sonuçlarını geri almanın tek yolu, geri çağırmasını sağlayın, kodunuzu yürütme işlemini tamamlayın ve kontrolü tarayıcıya geri verin. Böylece, beklemeye gerek yoktur. G/Ç tamamlandığında, işleyiciniz bu görevlerden biri olur ve yürütülür.

Örneğin, yukarıdaki örnekleri modern JavaScript'te yeniden yazmak ve bir uzak URL'den eklemek istiyorsanız, Getirme API'sini ve eş zamansız bekleme söz dizimini kullanırsınız:

async function main() {
  let response = await fetch("name.txt");
  let name = await response.text();
  console.log("Hello, %s!", name);
}

Eşzamanlı görünse de her await aslında geri aramalar:

function main() {
  return fetch("name.txt")
    .then(response => response.text())
    .then(name => console.log("Hello, %s!", name));
}

Biraz daha açık olan bu sadeleştirilmiş örnekte, bir istek başlatılmıştır ve ilk geri çağırma ile yanıtlara abone olunmaktadır. Tarayıcı ilk yanıtı aldıktan sonra (yalnızca HTTP üstbilgiler. Zaman uyumsuz olarak bu geri çağırmayı çağırır. Geri çağırma, gövdeyi response.text() ve başka bir geri çağırma ile sonuca abone olur. Son olarak, fetch tümünü alırsa son geri çağırma işlevini çağırır ve "Hello, (username)!" (Merhaba, (kullanıcı adı)) değerini döndürür. değerini konsolu.

Bu adımların eşzamansız doğası sayesinde, orijinal işlev kontrolü önceki aracını kullanın ve kullanıcı arayüzünün tamamını duyarlı ve kullanılabilir durumda bırakın. oluşturma ve kaydırma gibi diğer görevleri içerecek şekilde yapılandırılacak.

Son bir örnek olarak, uygulamanın belirli bir süre içinde belirli bir süre beklemesini sağlayan "uyku" gibi basit API'ler bile sayısı da bir G/Ç işlemi biçimidir:

#include <stdio.h>
#include <unistd.h>
// ...
printf("A\n");
sleep(1);
printf("B\n");

Elbette, bunu çok basit bir şekilde çevirebilir ve mevcut ileti dizisini engelleyebilirsiniz. süre dolana kadar:

console.log("A");
for (let start = Date.now(); Date.now() - start < 1000;);
console.log("B");

Aslında, Emscripten, yeni bir API için benzersiz bir deneyim "uyku", ancak bu çok verimsizdir, tüm kullanıcı arayüzünü engeller ve başka hiçbir etkinliğin bu arada. Genellikle üretim kodunda bunu yapmayın.

Daha ziyade, "uyku"nun daha deyimsel bir versiyonu JavaScript'te setTimeout() çağrılacaktır ve bir işleyiciyle abone olma:

console.log("A");
setTimeout(() => {
    console.log("B");
}, 1000);

Tüm bu örneklerin ve API'lerin ortak noktası nedir? Her durumda, orijinal metindeki deyimsel kod sistem dili, G/Ç için engelleme API'si kullanırken web için eşdeğer bir örnekte eşzamansız API'yi kullanmayı tercih edin. Web'de derlerken bu ikisi arasında bir şekilde dönüşüm gerçekleştirmeniz gerekir kullanıyorsanız ve WebAssembly'nin henüz bunu yapmak için yerleşik bir yeteneği yok.

Asyncify ile boşluğu kapatma

Asyncify burada devreye girer. Eş zamansız senkronizasyon, Emscripten tarafından desteklenen ve tüm programın duraklatılmasını sağlayan ve daha sonra devam ettirebilirsiniz.

Çağrı grafiği
bir JavaScript açıklaması -> WebAssembly -> web API -> Asyncify&#39;ın bağlandığı eşzamansız görev çağrısı
eşzamansız görevin sonucu tekrar WebAssembly&#39;ye

Emscripten ile C / C++ dilinde kullanım

Son örnekte eşzamansız bir uyku modu uygulamak için Asyncify uygulamasını kullanmak isterseniz şunu yapabilirsiniz: bu şekildedir:

#include <stdio.h>
#include <emscripten.h>

EM_JS(void, async_sleep, (int seconds), {
    Asyncify.handleSleep(wakeUp => {
        setTimeout(wakeUp, seconds * 1000);
    });
});

puts("A");
async_sleep(1);
puts("B");

EM_JS bir makrosu tarafından sağlanan JavaScript snippet'lerinin C işlevleri gibi tanımlanmasına olanak tanır. İçinde, bir işlev kullanın Asyncify.handleSleep() Bu, Emscripten'e programı askıya almasını söyler ve olması gereken bir wakeUp() işleyici sağlar eşzamansız işlem bittikten sonra çağrılacaktır. Yukarıdaki örnekte işleyici, setTimeout() ancak geri çağırmaların kabul edildiği başka bir bağlamda da kullanılabilir. Son olarak da async_sleep() öğesini, normal sleep() veya diğer herhangi bir eşzamanlı API gibi istediğiniz yere çağırın.

Bu tür bir kodu derlerken Emscripten'e Eşzamansız özelliğini etkinleştirmesini söylemeniz gerekir. Bu işlemi şu tarihe kadar yapın: -s ASYNCIFY ve -s ASYNCIFY_IMPORTS=[func1, func2] başarılı şekilde eşzamansız olabilecek fonksiyonların dizi benzeri bir listesidir.

emcc -O2 \
    -s ASYNCIFY \
    -s ASYNCIFY_IMPORTS=[async_sleep] \
    ...

Bu, Emscripten'e bu fonksiyonlara yapılan çağrıların kaydedilip geri yüklenmesi gerekebileceğini gerekir. Bu nedenle derleyici, bu tür çağrıların etrafına destekleyici kod ekler.

Şimdi bu kodu tarayıcıda yürüttüğünüzde beklediğiniz gibi sorunsuz bir çıkış günlüğü göreceksiniz A'dan sonra kısa bir gecikmenin ardından B geliyor.

A
B

Aşağıdaki değerlerden Eş zamansız işlevleri de kullanabilirsiniz. Ne? tek yapmanız gereken, handleSleep() sonucunu döndürmek ve sonucu wakeUp() geri arama. Örneğin, bir dosyadan okumak yerine uzaktan kumandadan bir sayı getirmek isterseniz bir istekte bulunmak, C kodunu askıya almak ve yanıt gövdesi alındığında devam ettirilir. Bunların tümü çağrı eşzamanlıymış gibi sorunsuz bir şekilde yapılır.

EM_JS(int, get_answer, (), {
     return Asyncify.handleSleep(wakeUp => {
        fetch("answer.txt")
            .then(response => response.text())
            .then(text => wakeUp(Number(text)));
    });
});
puts("Getting answer...");
int answer = get_answer();
printf("Answer is %d\n", answer);

Hatta fetch() gibi Promise tabanlı API'lerde Asyncify'ı JavaScript'in async-await özelliğini kullanmanızı öneririz. Bunun için Asyncify.handleSleep(), Asyncify.handleAsync() numaralı telefonu arayın. Böylece toplantı için wakeUp() geri çağırması için, async JavaScript işlevi iletebilir ve await ile return değerlerini kullanabilirsiniz kodu daha doğal ve eşzamanlı görünür hale getirirken senkronize edilir.

EM_JS(int, get_answer, (), {
     return Asyncify.handleAsync(async () => {
        let response = await fetch("answer.txt");
        let text = await response.text();
        return Number(text);
    });
});

int answer = get_answer();

Karmaşık değerler bekleniyor

Ancak bu örnekte yalnızca sayılarla sınırlanmıştır. Orijinal reklam öğesini uygulamak isterseniz ne olur? nasıl bir kullanıcı adı aranır? Bunu da yapabilirsiniz!

Emscripten, Embind dönüşümleri yönetmek için JavaScript ve C++ değerleri arasında geçiş yapın. Asyncify desteği de vardır. harici Promise öğelerinde await() öğesini çağırabilirsiniz. Bu çağrı, eşzamansız beklemedeki await gibi davranır JavaScript kodu:

val fetch = val::global("fetch");
val response = fetch(std::string("answer.txt")).await();
val text = response.call<val>("text").await();
auto answer = text.as<std::string>();

Bu yöntemi kullanırken ASYNCIFY_IMPORTS öğesini derleme bayrağı olarak iletmeniz bile gerekmez. varsayılan olarak zaten eklenmiştir.

Tamam, tüm bunlar Emscripten'de harika çalışıyor. Peki ya diğer araç zincirleri ve diğer diller?

Diğer dillerden kullanım

Rust kodunuzun bir yerinde, web'de eşzamansız API. Bunu da yapabilirsiniz!

Öncelikle, böyle bir işlevi extern bloğu (veya seçtiğiniz bir blok) üzerinden normal içe aktarma işlemi olarak tanımlamanız gerekir dilin yabancı fonksiyonlar için söz dizimi).

extern {
    fn get_answer() -> i32;
}

println!("Getting answer...");
let answer = get_answer();
println!("Answer is {}", answer);

Ardından kodunuzu WebAssembly'de derleyin:

cargo build --target wasm32-unknown-unknown

Şimdi, WebAssembly dosyasını, yığını depolamak/geri yüklemek için kodla uygulamanız gerekir. C / için C++ ile birlikte, Emscripten bunu bizim yerimize yapar; ancak bu komut burada kullanılmıyor; bu nedenle, süreç biraz daha manueldir.

Neyse ki Asyncify dönüşümünün kendisi araç zincirinden bağımsızdır. İsteğe bağlı olarak WebAssembly dosyaları, hangi derleyici tarafından üretildiğine bakılmaksızın. Dönüştürme işlemi ayrı olarak sağlanır. Binaryen'deki wasm-opt optimize edicinin bir parçası olarak araç zinciri ve şu şekilde çağrılabilir:

wasm-opt -O2 --asyncify \
      --pass-arg=asyncify-imports@env.get_answer \
      [...]

Dönüştürmeyi etkinleştirmek için --asyncify yönergesini iletin ve ardından virgülle ayrılmış bir değer sağlamak için --pass-arg=… kullanın. program durumunun askıya alınması ve daha sonra devam ettirilmesi gereken eşzamansız işlevlerin listesi.

Geriye yalnızca bunu yapacak destekleyici çalışma zamanı kodu sağlamakla kalırsınız; askıya alın ve devam ettirin WebAssembly kodu. Yine C / C++ durumunda bu Emscripten tarafından dahil edilir ancak şimdi özel JavaScript birleştirici kodudur. Yeni bir kitaplık oluşturduk sırf bu yüzden.

Bu sürümü GitHub'da şu adreste bulabilirsiniz: https://github.com/GoogleChromeLabs/asyncify or npm (asyncify-wasm adı).

Standart bir WebAssembly örneklendirmesini simüle eder. API'de oturum açın. Tek diğeri ise normal bir WebAssembly API'sinde eşzamanlı işlevleri yalnızca içe aktarma işlemleri gerçekleştirirken, Asyncify sarmalayıcı altında, eşzamansız içe aktarmaları da sağlayabilirsiniz:

const { instance } = await Asyncify.instantiateStreaming(fetch('app.wasm'), {
    env: {
        async get_answer() {
            let response = await fetch("answer.txt");
            let text = await response.text();
            return Number(text);
        }
    }
});

await instance.exports.main();

Yukarıdaki örnekte get_answer() gibi eşzamansız bir işlevi WebAssembly tarafında, kitaplık döndürülen Promise öğesini algılar, çalışmasını sağlama, taahhüdün tamamlanmasına abone olma ve daha sonra, sorun çözüldükten sonra çağrı yığınını ve durumunu sorunsuz bir şekilde geri yükleyebilir ve hiçbir şey olmamış gibi yürütmeye devam edebilir.

Modüldeki herhangi bir işlev eşzamansız bir çağrı yapabileceği için tüm dışa aktarma işlemleri potansiyel olarak eşzamansız olduğu için de sarmalanırlar. Yukarıdaki örnekte, yürütmenin gerçekten ne zaman gerçekleşeceğini bilmek için instance.exports.main() sonucunun await tamamlandı.

Tüm bunlar arka planda nasıl işliyor?

Asyncify, ASYNCIFY_IMPORTS işlevlerinden birine çağrı algıladığında eşzamansız işlemi, çağrı yığını ve diğer geçici ayarlar da dahil olmak üzere uygulamanın tüm durumunu kaydeder. yerel olarak çalışır ve daha sonra, bu işlem tamamlandığında tüm bellek ile çağrı yığınını aynı yerden ve program hiç durdurulmamış gibi devam eder.

Bu, daha önce gösterdiğim JavaScript'teki eş zamansız bekleme özelliğine çok benziyor, ancak JavaScript bir, dilden herhangi bir özel söz dizimi veya çalışma zamanı desteği gerektirmez ve derleme sırasında düz eşzamanlı işlevleri dönüştürerek çalışır.

Daha önce gösterilen eşzamansız uyku örneğini derlerken:

puts("A");
async_sleep(1);
puts("B");

Asyncify, bu kodu alıp kabaca aşağıdakine benzer şekilde dönüştürür (sözde kod, gerçek kod daha karmaşık bir konudur):

if (mode == NORMAL_EXECUTION) {
    puts("A");
    async_sleep(1);
    saveLocals();
    mode = UNWINDING;
    return;
}
if (mode == REWINDING) {
    restoreLocals();
    mode = NORMAL_EXECUTION;
}
puts("B");

Başlangıçta mode, NORMAL_EXECUTION olarak ayarlanır. Buna karşılık, bu tür dönüştürülen kod ilk kez yürütüldüğünde, yalnızca async_sleep() öncesindeki kısım değerlendirilir. Bu işlemin ardından eşzamansız işlem planlandığında, Asyncify tüm yerel verileri kaydeder ve her işlevden en üste kadar dönüyor. Bu şekilde, kontrolü tarayıcıya geri etkinlik döngüsü oluşturabilirsiniz.

Ardından, async_sleep() sorunu çözüldüğünde Asyncify destek kodu, mode değerini REWINDING olarak değiştirir ve fonksiyonu tekrar çağırın. Bu seferki "normal yürütme" dal atlandı - çünkü zaten atlandı iş son kez çalışıyor ve "A"yı yazdırmak istiyorum ve bunun yerine doğrudan "geri sarma" görebilirsiniz. Erişim sağlandığında depolanan tüm yereller geri yüklenir ve mod tekrar şu şekilde değiştirilir: "normal" ve sanki kod hiç durdurulmamış gibi devam eder.

Dönüşüm maliyetleri

Ne yazık ki eş zamanlı olmayan dönüşüm tamamen ücretsiz değildir çünkü tüm yerel öğeleri saklayıp geri yüklemeye, farklı modları kullanabilirsiniz. Yalnızca komutta eşzamansız olarak işaretlenmiş işlevleri değiştirmeye çalışır satır ve potansiyel arayanlardan herhangi birini içerir ancak kod boyutu ek yükü, sıkıştırmadan önce yaklaşık% 50'ye varan oranda olabilir.

Kodu gösteren bir grafik
ince ayar yapılmış koşullarda neredeyse% 0&#39;dan en kötü koşullarda% 100&#39;ün üzerine çıkacak şekilde, çeşitli karşılaştırmalarda boyut genel giderleri
vakalar

Bu ideal bir çözüm değildir ancak çoğu durumda alternatifin işlevsellikte olmadığı birçok durumda kabul edilebilir ya da orijinal kodda önemli yeniden yazımlar yapmak zorunda kalabilirsiniz.

Daha da yükseğe çıkmamak için son derlemelerde her zaman optimizasyonları etkinleştirdiğinizden emin olun. Şunları yapabilirsiniz: Asyncify'a özel optimizasyon seçenekleri kullanarak genel giderleri dönüşümleri yalnızca belirtilen işlevlerle ve/veya yalnızca doğrudan işlev çağrılarıyla sınırlayın. Ayrıca bir de düşük bir maliyete sahiptir, ancak eşzamansız çağrıların kendisiyle sınırlıdır. Ancak, elde edilen maliyetin ne kadar yüksek olduğuna baksa da bu genellikle göz ardı edilebilir bir miktardır.

Gerçek demolar

Basit örnekleri incelediğinize göre şimdi daha karmaşık senaryolara geçeceğim.

Makalenin başında belirtildiği gibi, web'deki depolama seçeneklerinden biri, eşzamansız File System Access API. Şunlara erişim sağlar: gerçek bir ana bilgisayar dosya sistemi kullanır.

Öte yandan, WASI adlı bir fiili standart vardır. WebAssembly I/O için konsolda ve sunucu tarafında. Bu proje yaşam döngüsü boyunca her tür dosya sistemini ve diğer işlemleri geleneksel bir ortamda senkronize edilir.

Birini başkasıyla eşleyebiliyor olsaydınız ne olurdu? Daha sonra, herhangi bir kaynak dilindeki herhangi bir uygulamayı derleyebilirsiniz WASI hedefini destekleyen herhangi bir araç zinciriyle çalıştırma ve web'deki bir korumalı alanda çalıştırma bu uygulamanın gerçek kullanıcı dosyaları üzerinde çalışmasına olanak tanıyor. Asyncify ile tam da bunu yapabilirsiniz.

Bu demoda Rust coreutils kasasını bir WASI'ye birkaç küçük yama, Asyncify dönüşümü aracılığıyla iletildi ve eşzamansız olarak uygulanır WASI'den alınan bağlamalar JavaScript tarafındaki File System Access API'ye. Şununla birleştirildiğinde: Xterm.js terminal bileşenidir. Bu, gerçek bir terminal gibi gerçek kullanıcı dosyaları üzerinde çalışma.

https://wasi.rreverser.com/ adresinden canlı yayına göz atın.

Eş zamansız kullanım alanları yalnızca zamanlayıcılarla ve dosya sistemleriyle sınırlı değildir. Daha ileri gidip internette daha fazla niş API kullanıyor.

Örneğin, Asyncify'ın yardımıyla, aynı zamanda libusb—muhtemelen birlikte çalışmak için kullanılan en popüler yerel kitaplık USB cihazlar (bu tür cihazlara eşzamansız erişim sağlayan bir WebUSB API'ye) hale getirecektir. Eşlenip derlendikten sonra seçilen verilere göre çalıştırılacak standart libusb testleri ve örnekler aldım. bir web sayfasının korumalı alanında

libusb ekran görüntüsü
bağlı Canon kamerayla ilgili bilgilerin gösterildiği bir web sayfasındaki hata ayıklama çıkışı

Muhtemelen bu başka bir blog yayınının hikayesidir.

Bu örnekler, Asyncify'ın boşluğu doldurmak ve tüm bunları platformlar arası erişim, korumalı alan oluşturma ve daha iyi bir araç deneyimi gibi pek çok farklı türde uygulamayı hem de işlevsellikten ödün vermeden.