
# API

API eksekusi terdiri dari satu kelas: `Phasis\Engine`. Semua yang lain (`JsValue`, `JsFunction`, `JsObject`, …) bersifat internal bagi engine dan hanya diekspos ke fungsi host yang menjembatani PHP↔JS. Satu-satunya permukaan publik lainnya adalah [parser mandiri API](https://phasis.inline0.dev/id/docs/parser) (kelas `Phasis\Parser\Parser` dan `Phasis\Ast`), yang mengurai sumber tanpa mengeksekusinya.

## Konstruksi

```php
use Phasis\Engine;

$engine = new Engine();                 // lazy mode (default)
$engine = new Engine(eager: true);      // install every built-in up front
```

`Engine` baru mengekspos seluruh pustaka standar — `Object`, `Array`, `String`, `Number`, `Symbol`, `BigInt`, `Promise`, `RegExp`, `Error` serta intrinsik bahasa inti yang dipasang secara langsung agar spesifikasi berfungsi (tanpa `Function.prototype` Anda tidak dapat memiliki kelas, tanpa `Promise` tidak ada async/await, dan tanpa `RegExp` tidak ada literal regex). Semua yang lain — `Math`, `JSON`, `Map`, `Set`, `Date`, `TypedArray` dan keluarga array-buffer, `Atomics`, `Proxy`, `Reflect`, `console`, `Intl`, keluarga weak-ref, `Temporal`, `ShadowRealm`, Web Platform Pack, dan Fetch Pack — didaftarkan sebagai accessor placeholder dan diwujudkan saat pertama kali dibaca. Pembacaan global berlangsung transparan: `Map`, `typeof Map`, `'Map' in globalThis`, `Object.keys(globalThis).includes('Map')` semuanya berfungsi, dan setelah pembacaan pertama placeholder diganti dengan deskriptor data biasa.

Teruskan `eager: true` jika Anda benar-benar membutuhkan perilaku sebelum lazy loading — setiap built-in dipasang saat konstruksi dengan seluruh permukaan deskriptor terlihat saat introspeksi. Harness kesesuaian (`bin/test262`, `bin/wpt`) membuat engine dalam mode eager agar test262/WPT melihat bentuk deskriptor yang diharapkan.

## eval

```php
public function eval(string $source): mixed
```

Mengurai dan mengeksekusi `$source` sebagai skrip klasik dalam cakupan global engine, lalu menjalankan event loop hingga kosong. Mengembalikan nilai ekspresi terakhir yang dievaluasi, dikonversi ke PHP (atau `null` jika program tidak memiliki ekspresi penutup). Lihat [Konversi nilai](https://phasis.inline0.dev/id/docs/interop/values) untuk aturannya.

```php
$result = $engine->eval('1 + 2');           // 3
$result = $engine->eval('JSON.stringify({a: 1})');  // '{"a":1}'
$result = $engine->eval('[1,2,3].map(x => x * 2)'); // [2, 4, 6] (PHP array)
```

Promise tidak di-unpack: `eval('Promise.resolve(5)')` mengembalikan array kosong. Karena event loop sudah selesai berjalan saat `eval()` mengembalikan hasil, simpan nilai yang telah diselesaikan dalam global dan baca dengan `eval()` kedua:

```php
$engine->eval('let out; (async () => { out = await Promise.resolve(9); })();');
$engine->eval('out');   // 9
```

JS `throw` eksplisit yang tidak ditangkap akan muncul sebagai `Phasis\Exceptions\JsThrowable`, dengan pesan `"Name: message"` dan properti `$jsValue` yang menyimpan `JsValue` yang dilempar. Error yang dihasilkan engine sendiri, seperti membaca properti dari `null` atau variabel yang tidak didefinisikan, muncul sebagai `Phasis\Exceptions\TypeError`, `ReferenceError`, atau `RangeError`. Semua ini memperluas `Phasis\Exceptions\RuntimeError`; error penguraian memperluas `Phasis\Exceptions\SyntaxError`. Lihat [Pengecualian](#exceptions).

```php
try {
    $engine->eval('throw new Error("boom")');
} catch (\Phasis\Exceptions\JsThrowable $e) {
    echo $e->getMessage();
    // Error: boom
}
```

## evalAsModule

```php
public function evalAsModule(string $source, ?string $path = null): mixed
```

Mengevaluasi `$source` sebagai modul ES: `import` / `export`, `await` tingkat teratas dan `import.meta` tersedia, sementara specifier relatif diselesaikan terhadap `$path` (jalur file absolut). Menjalankan event loop seperti `eval()` dan selalu mengembalikan `null`; baca hasil melalui global atau buffer console.

```php
$path = __DIR__ . '/entry.mjs';
$engine->evalAsModule(file_get_contents($path), $path);
```

Pemuat hanya menyelesaikan jalur relatif dan absolut; lihat [Batasan yang diketahui](https://phasis.inline0.dev/id/docs/compatibility/limitations#module-loader).

## execFile

```php
public function execFile(string $path): mixed
```

Membaca file dari disk dan meneruskan isinya ke `eval()`, sehingga file dijalankan sebagai skrip klasik apa pun ekstensinya; file dengan `import` / `export` gagal dengan SyntaxError. Jalur asli file menjadi basis bagi specifier `import()` dinamis. Gunakan `evalAsModule()` untuk file modul.

```php
$engine->execFile(__DIR__ . '/script.js');
```

Jika file tidak dapat dibaca, PHP mengeluarkan peringatan dan `execFile()` melempar `\RuntimeException` biasa ("Cannot read file: …").

## setGlobal

```php
public function setGlobal(string $name, mixed $value): void
```

Mengikat variabel global yang terlihat dalam semua evaluasi JS berikutnya. Nilai PHP dikonversi menjadi JS:

| Tipe PHP | Tipe JS |
|---|---|
| `null` | `null` |
| `bool` | `boolean` |
| `int` / `float` | `number` |
| `string` | `string` (bahkan jika namanya merujuk ke fungsi, seperti `'strlen'`) |
| array (daftar) | `Array` (salinan) |
| array (asosiatif) | `Object` (salinan) |
| `Closure`, objek yang dapat dipanggil, `[$object, 'method']` | `function` |
| objek, generator, atau resource lainnya | `undefined` |

Array disalin: perubahan yang dibuat JS pada array tersebut tidak terlihat di array PHP. Objek PHP biasa belum dapat diteruskan ([issue #18](https://github.com/inline0/phasis/issues/18)); lihat [Objek bersama](https://phasis.inline0.dev/id/docs/interop/shared-objects) untuk solusinya.

```php
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->setGlobal('log', fn(string $msg) => error_log($msg));
```

Di dalam JS:

```javascript
console.log(config.version);   // "1.0"
log('hello');                  // calls the PHP closure
```

## call

```php
public function call(string $name, mixed ...$args): mixed
```

Memanggil fungsi global JS dengan argumen PHP (dikonversi seperti pada `setGlobal`). Nilai yang dikembalikan dikonversi kembali ke PHP. Memanggil nama yang tidak didefinisikan akan melempar `Phasis\Exceptions\ReferenceError`; memanggil global yang bukan fungsi akan melempar `Phasis\Exceptions\TypeError`.

```php
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3);   // 5
```

## Keluaran console

```php
public function getConsoleOutput(): string
public function getConsoleLines(): array
public function clearConsole(): void
```

`console.log`, `console.info`, `console.warn`, `console.error`, dan `console.debug` tidak mencetak apa pun. Setiap pemanggilan menambahkan satu baris berformat ke buffer pada engine. `getConsoleLines()` mengembalikan baris dalam buffer sebagai daftar string, `getConsoleOutput()` mengembalikannya dengan `\n` sebagai pemisah, dan `clearConsole()` mengosongkan buffer. Buffer tidak dihapus di antara pemanggilan `eval()`.

```php
$engine->eval('console.log("a", 1); console.warn({ b: 2 })');
$engine->getConsoleLines();    // ['a 1', '{ b: 2 }']
echo $engine->getConsoleOutput();
// a 1
// { b: 2 }
$engine->clearConsole();
```

## setLimit

```php
public function setLimit(string $limit, int $value): void
```

Menetapkan batas sumber daya yang dapat diubah saat runtime. Hanya ada satu:

| Batas | Default | Deskripsi |
|---|---|---|
| `maxLoopIterations` | `100000` | jumlah maksimum iterasi per pelaksanaan loop `for` / `while` / `do…while` oleh interpreter tree-walking |

```php
$engine->setLimit('maxLoopIterations', 1_000_000);
```

Nama lain apa pun diabaikan tanpa pemberitahuan; tidak ada batas untuk panjang string, keluaran console, atau waktu dinding.

Batas loop belum diterapkan di semua tempat. Loop yang berjalan pada VM bytecode, yang mencakup isi fungsi dan program tingkat teratas yang diterima compiler bytecode, tidak pernah dihitung, sehingga `while (true) {}` berjalan sampai PHP menghentikannya ([issue #19](https://github.com/inline0/phasis/issues/19)). Jangan mengandalkannya untuk menghentikan kode yang tidak tepercaya; gunakan PHP milik `max_execution_time` atau proses terpisah.

Saat batas tercapai, error dapat ditangkap oleh JS `try` / `catch` (sebagai `Error` dengan pesan "Maximum loop iterations exceeded"). Jika tidak ada yang menangkapnya, error sampai ke PHP sebagai `Phasis\Exceptions\InternalError`, yang memperluas `Phasis\Exceptions\RuntimeError`, bukan `JsThrowable`.

Dua batas tambahan tertanam dalam engine dan tidak dapat diubah saat runtime: `Phasis\Runtime\CallStack::maxDepth` (4,096 frame JS) dan pengaman kedalaman closure transpiled JsToPhp (8,192 frame PHP). Keduanya melempar "Maximum call stack size exceeded" saat terlampaui, dan error itu juga sampai ke PHP sebagai `InternalError`. Batas ini jauh di atas kebutuhan program JS yang sah — menaikkannya memerlukan perubahan kode, bukan pengaturan konfigurasi.

## reset

```php
public function reset(): void
```

Menghapus semua global yang ditentukan pengguna dan buffer console, serta membangun ulang pustaka standar dan event loop.

Pada engine yang dibuat dalam mode lazy default, `reset()` saat ini menghilangkan semua built-in yang dipasang secara lazy: setelahnya `Map`, `JSON`, `Math`, `console`, `fetch`, dan lainnya tidak terdefinisi ([issue #22](https://github.com/inline0/phasis/issues/22)). Sampai masalah ini diperbaiki, buat `Engine` baru atau buat dengan `eager: true`, yang membuat `reset()` berfungsi.

## Event loop

Engine menyertakan event loop JavaScript dengan `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`, dan `queueMicrotask` pada objek global. Microtask (penangan Promise, `queueMicrotask`) dikuras di antara setiap macrotask; timer berjalan berdasarkan urutan tenggat melalui antrean tugas per-realm.

```php
public function runEventLoop(): void
public function tickEventLoop(): bool
public function pendingTaskCount(): int
public function getEventLoop(): \Phasis\Runtime\EventLoop
```

`eval()` dan `evalAsModule()` memanggil `runEventLoop()` secara otomatis setelah kode sinkron selesai. Jadi, skrip yang menjadwalkan `setTimeout(cb, 5000)` di tingkat teratas berjalan `cb` sebelum `eval()` kembali — loop menggunakan **waktu virtual**, memajukan jamnya ke tenggat berikutnya yang tertunda alih-alih menunggu PHP. Sesuai dengan semantik `node script.js`.

Untuk penyematan waktu nyata (server berumur panjang, integrasi UI, penjadwalan berdasarkan jam nyata), `tickEventLoop()` menjalankan satu iterasi (menguras microtask, menjalankan timer yang waktunya tiba) lalu kembali. `pendingTaskCount()` melaporkan timer yang masih tertunda; embedder dapat menjalankan pump-nya sendiri:

```php
while ($engine->pendingTaskCount() > 0) {
    $engine->tickEventLoop();
    usleep(1000);
}
```

`setInterval` yang berjalan tanpa henti dibatasi: `runEventLoop()` membatasi hingga 100,000 siklus lalu melempar `\RuntimeException` biasa — kesalahan pada kode JS muncul sebagai error PHP yang jelas, bukan membuat proses macet. Interval tetap terjadwal, jadi setiap `eval()` berikutnya pada engine yang sama akan melempar error yang sama; buang engine tersebut setelahnya. Error yang tidak tertangkap dalam callback timer dikeluarkan melalui `error_log()` dan loop terus menguras antrean; satu timer yang bermasalah tidak menghentikan program.

## Hook fetch

Phasis menyertakan `fetch()` yang aktif secara default, dengan `ext-curl` milik PHP sebagai transport yang mendasarinya. Tiga hook memungkinkan embedder mengganti transport, membatasi permintaan, atau mengaktifkan penyimpanan cookie. Ketiganya menerima callable PHP atau metode pada objek; lihat [API Web](https://phasis.inline0.dev/id/docs/compatibility/web-apis) untuk kontrak bentuk nilai.

### setFetchTransport

```php
public function setFetchTransport(callable $transport): void
```

Ganti CurlTransport default. Callable menerima deskriptor permintaan dan JsObject `AbortSignal` opsional; mengembalikan deskriptor respons.

```php
// Use Guzzle as the transport instead of curl
$client = new GuzzleHttp\Client();
$engine->setFetchTransport(function (array $req, $signal) use ($client) {
    $r = $client->request($req['method'], $req['url'], [
        'headers' => $req['headers'],
        'body' => $req['body'],
        'http_errors' => false,
    ]);
    return [
        'status' => $r->getStatusCode(),
        'statusText' => $r->getReasonPhrase(),
        'headers' => array_map(
            fn ($name, $values) => array_map(fn ($v) => [$name, $v], $values),
            array_keys($r->getHeaders()),
            $r->getHeaders(),
        ),
        'body' => (string) $r->getBody(),
    ];
});
```

Pola penyematan yang umum: pasang transport tiruan dalam pengujian, lalu transport curl sungguhan di produksi.

### setFetchPolicy

```php
public function setFetchPolicy(callable $hook): void
```

Hook kebijakan pra-penerbangan. Menerima JsObject `Request`; dapat melempar error untuk menolak, mengembalikan `Request` yang diubah untuk menulis ulang, atau mengembalikan `null`/tanpa nilai untuk mengizinkan.

```php
// Block non-HTTPS and inject auth on a specific host
$engine->setFetchPolicy(function ($req) {
    $url = $req->get('url')->value;
    if (str_starts_with($url, 'http://')) {
        throw new \RuntimeException('HTTPS only');
    }
    if (str_contains($url, 'api.example.com')) {
        $req->get('headers')->call('set', 'Authorization', 'Bearer ...');
    }
    return null;
});
```

### setCookieJar

```php
public function setCookieJar(mixed $jar): void
```

Penyimpanan cookie opsional. Menerima objek apa pun yang mengekspos `get(string $url): string` dan `set(string $url, string $header): void` — di sisi PHP atau sisi JS.

```php
class SimpleJar
{
    private array $cookies = [];
    public function get(string $url): string { return implode('; ', $this->cookies); }
    public function set(string $url, string $header): void { $this->cookies[] = $header; }
}
$engine->setCookieJar(new SimpleJar());
```

Tanpa `setCookieJar()`, `fetch` mengabaikan header `Set-Cookie` masuk dan `Cookie` keluar — default-nya **tanpa cookie**.

## Exception

| Class | Extends | Kapan |
|---|---|---|
| `Phasis\Exceptions\JsThrowable` | `RuntimeError` | JS `throw` eksplisit yang tidak ditangkap apa pun |
| `Phasis\Exceptions\TypeError`, `ReferenceError`, `RangeError` | `RuntimeError` | error yang dibuat engine sendiri (misalnya `null.x` atau variabel yang tidak terdefinisi) dan tidak ditangkap apa pun |
| `Phasis\Exceptions\InternalError` | `RuntimeError` | batas loop atau batas tumpukan pemanggilan terlampaui |
| `Phasis\Exceptions\RuntimeError` | `\RuntimeException` | kelas dasar untuk yang di atas |
| `Phasis\Parser\ParseError` | `Phasis\Exceptions\SyntaxError` (`\RuntimeException`) | sumber tidak dapat diuraikan |
| `\RuntimeException` | | `execFile()` tidak dapat membaca file; event loop mencapai batas 100,000 siklusnya |

Untuk menangkap setiap kegagalan skrip, tangkap `Phasis\Exceptions\RuntimeError` dan `Phasis\Exceptions\SyntaxError`, atau cukup tangkap `\RuntimeException`.

Exception yang dilempar oleh fungsi host PHP Anda sendiri berbeda: saat ini exception tersebut melewati JS `try` / `catch` dan sampai ke pemanggil `eval()` tanpa perubahan, dengan kelas aslinya ([masalah #20](https://github.com/inline0/phasis/issues/20)). Lihat [Fungsi host](https://phasis.inline0.dev/id/docs/interop/host-functions#throwing-back-into-js).

## Siklus hidup

`Engine` murah untuk dibuat (~beberapa ms saat cold start) dan pustaka standarnya dipasang secara lazy. Untuk sebagian besar skenario penyematan, satu engine berumur panjang per proses sudah memadai. Untuk penggunaan multi-tenant, berikan setiap tenant atau permintaan engine sendiri agar global tidak bocor di antara mereka. Membuat engine baru adalah cara andal untuk memulai dengan kondisi bersih saat ini (lihat [reset](#reset)).

```php
foreach ($requests as $req) {
    $engine = new Engine();
    $engine->setGlobal('request', ['path' => $req->path, 'query' => $req->query]);
    $engine->execFile($req->scriptPath);
}
```
