
# API

Izpildes API ir viena klase: `Phasis\Engine`. Viss pārējais (`JsValue`, `JsFunction`, `JsObject`, …) ir dzinēja iekšējā daļa un pieejams tikai resursdatora funkcijām, kas sasaista PHP↔JS. Vienīgā cita publiskā saskarne ir patstāvīgais [parsētājsAPI](https://phasis.inline0.dev/lv/docs/parser) (`Phasis\Parser\Parser` un `Phasis\Ast` klases), kas parsē avota kodu, to neizpildot.

## Izveide

```php
use Phasis\Engine;

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

Jauns `Engine` nodrošina pilnu standarta bibliotēku — `Object`, `Array`, `String`, `Number`, `Symbol`, `BigInt`, `Promise`, `RegExp`, `Error` un valodas pamatfunkcijas tiek uzstādītas uzreiz, lai darbotos specifikācija (bez `Function.prototype` nevar izmantot klases, bez `Promise` — async/await, bez `RegExp` — regex literāļus). Viss pārējais — `Math`, `JSON`, `Map`, `Set`, `Date`, `TypedArray` un masīva buferu saime, `Atomics`, `Proxy`, `Reflect`, `console`, `Intl`, vājo atsauču saime, `Temporal`, `ShadowRealm`, Web Platform Pack un Fetch Pack — tiek reģistrēts kā viettura piekļuves metode un materializēts pirmajā nolasīšanas reizē. Globālā mainīgā nolasīšana ir caurspīdīga: `Map`, `typeof Map`, `'Map' in globalThis`, `Object.keys(globalThis).includes('Map')` darbojas, un pēc pirmās nolasīšanas vietturu aizstāj parastu datu deskriptoru.

Norādiet `eager: true`, ja īpaši nepieciešama iepriekšējā uzvedība bez slinkās ielādes — visas iebūvētās funkcijas tiek instalētas izveides laikā, un visa deskriptoru saskarne ir pieejama introspekcijai. Atbilstības testu ietvars (`bin/test262`, `bin/wpt`) izveido dzinējus tūlītējās ielādes režīmā, lai test262/WPT redzētu gaidītās deskriptoru formas.

## eval

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

Parsē un izpilda `$source` kā klasisku skriptu dzinēja globālajā tvērumā, pēc tam darbina notikumu ciklu, līdz tas ir tukšs. Atgriež pēdējās novērtētās izteiksmes vērtību, pārveidotu par PHP (vai `null`, ja programma beidzās bez izteiksmes). Noteikumus skatiet sadaļā [Vērtību pārveidošana](https://phasis.inline0.dev/lv/docs/interop/values).

```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 netiek atrisināts: `eval('Promise.resolve(5)')` atgriež tukšu masīvu. Tā kā `eval()` atgriešanās brīdī notikumu cikls jau ir iztukšots, saglabājiet atrisināto vērtību globālajā mainīgajā un nolasiet to ar otru `eval()`:

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

Ja skaidri izsauktais JS `throw` netiek pārtverts, tas tiek parādīts kā `Phasis\Exceptions\JsThrowable`, kura ziņojums ir `"Name: message"` un kuras rekvizīts `$jsValue` satur izmesto `JsValue`. Dzinēja pašas radītās kļūdas, piemēram, īpašības nolasīšana no `null` vai nedefinēts mainīgais, tiek parādītas kā `Phasis\Exceptions\TypeError`, `ReferenceError` vai `RangeError`. Tās visas ir `Phasis\Exceptions\RuntimeError` atvasinātas klases; parsēšanas kļūdas ir `Phasis\Exceptions\SyntaxError` atvasinātas. Skatiet sadaļu [Izņēmumi](#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
```

Novērtē `$source` kā ES moduli: ir pieejami `import` / `export`, augšējā līmeņa `await` un `import.meta`, un relatīvie specifikatori tiek atrisināti attiecībā pret `$path` (absolūtu faila ceļu). Notikumu cikls darbojas kā `eval()`, un vienmēr tiek atgriezts `null`; rezultātus nolasiet, izmantojot globālos mainīgos vai konsoles buferi.

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

Ielādētājs atrisina tikai relatīvos un absolūtos ceļus; skatiet [Zināmie ierobežojumi](https://phasis.inline0.dev/lv/docs/compatibility/limitations#module-loader).

## execFile

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

Nolasa failu no diska un nodod tā saturu uz `eval()`, tāpēc tas tiek izpildīts kā klasisks skripts neatkarīgi no faila paplašinājuma; fails ar `import` / `export` izraisa SyntaxError. Faila īstais ceļš kļūst par dinamisko `import()` specifikatoru pamatu. Moduļu failiem izmantojiet `evalAsModule()`.

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

Ja failu nevar nolasīt, PHP parāda brīdinājumu, bet `execFile()` izmet vienkāršu `\RuntimeException` ("Cannot read file: …").

## setGlobal

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

Saista globālu mainīgo, kas ir redzams visiem turpmākajiem JS novērtējumiem. PHP vērtības tiek pārveidotas par JS:

| PHP tips | JS tips |
|---|---|
| `null` | `null` |
| `bool` | `boolean` |
| `int` / `float` | `number` |
| `string` | `string` (arī tad, ja tas apzīmē funkciju, piemēram, `'strlen'`) |
| masīvs (saraksts) | `Array` (kopija) |
| masīvs (asociatīvs) | `Object` (kopija) |
| `Closure`, izsaucams objekts, `[$object, 'method']` | `function` |
| jebkurš cits objekts, ģenerators vai resurss | `undefined` |

Masīvi tiek kopēti: izmaiņas, ko tajos veic JS, nav redzamas PHP masīvā. Vienkāršus PHP objektus pagaidām nevar nodot ([18. problēma](https://github.com/inline0/phasis/issues/18)); risinājumus skatiet sadaļā [Koplietojamie objekti](https://phasis.inline0.dev/lv/docs/interop/shared-objects).

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

Iekš JS:

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

## call

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

Izsauc globālu JS funkciju ar PHP argumentiem (pārveidotiem tāpat kā `setGlobal` gadījumā). Atgrieztā vērtība tiek pārveidota atpakaļ par PHP. Izsaucot nedefinētu nosaukumu, tiek izraisīts `Phasis\Exceptions\ReferenceError`; izsaucot globālo mainīgo, kas nav funkcija, tiek izraisīts `Phasis\Exceptions\TypeError`.

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

## Konsoles izvade

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

`console.log`, `console.info`, `console.warn`, `console.error` un `console.debug` neko neizvada. Katrs izsaukums pievieno vienu formatētu rindu dzinēja buferim. `getConsoleLines()` atgriež buferī esošās rindas kā virkņu sarakstu, `getConsoleOutput()` atgriež tās, savienotas ar `\n`, bet `clearConsole()` iztukšo buferi. Starp `eval()` izsaukumiem buferis netiek notīrīts.

```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
```

Iestata izpildlaikā maināmu resursa ierobežojumu. Tāds ir tieši viens:

| Ierobežojums | Noklusējums | Apraksts |
|---|---|---|
| `maxLoopIterations` | `100000` | maksimālais iterāciju skaits katrā koka pastaigas interpretētāja izpildītajā `for` / `while` / `do…while` ciklā |

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

Jebkurš cits nosaukums tiek klusi ignorēts; nav ierobežojumu virkņu garumam, konsoles izvadei vai izpildes laikam.

Cikla ierobežojums vēl netiek piemērots visur. Bytecode VM izpildītie cikli, kas aptver funkciju pamatdaļas un augstākā līmeņa programmas, kuras pieņem bytecode kompilators, netiek skaitīti, tāpēc `while (true) {}` turpina darboties, līdz to aptur PHP ([19. problēma](https://github.com/inline0/phasis/issues/19)). Neuzticieties tam, lai apturētu neuzticamu kodu; izmantojiet PHP `max_execution_time` vai atsevišķu procesu.

Kad ierobežojums sasniegts, kļūdu var uztvert ar JS `try` / `catch` (kā `Error` ar ziņojumu "Maximum loop iterations exceeded"). Ja nekas to neuztver, tā sasniedz PHP kā `Phasis\Exceptions\InternalError`, kas paplašina `Phasis\Exceptions\RuntimeError`, nevis `JsThrowable`.

Dzinējā ir iebūvēti vēl divi griesti, kurus nevar mainīt izpildlaikā: `Phasis\Runtime\CallStack::maxDepth` (4,096 JS kadri) un JsToPhp transpēlēto aizvēruma dziļuma sargs (8,192 PHP kadri). Pārsniedzot jebkuru no tiem, tiek izraisīts "Maximum call stack size exceeded", kas arī sasniedz PHP kā `InternalError`. Tie ir krietni augstāki par jebkuras leģitīmas JS programmas vajadzībām; to palielināšanai jāmaina kods, konfigurācijas iestatījuma tam nav.

## reset

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

Notīra visus lietotāja definētos globālos mainīgos un konsoles buferi, kā arī no jauna izveido standarta bibliotēku un notikumu ciklu.

Dzinējā, kas izveidots noklusējuma slinkajā režīmā, `reset()` pašlaik zaudē visus slinki instalētos iebūvētos objektus: pēc tam `Map`, `JSON`, `Math`, `console`, `fetch` un pārējie nav definēti ([22. problēma](https://github.com/inline0/phasis/issues/22)). Kamēr tas nav novērsts, izveidojiet jaunu `Engine` vai izveidojiet to ar `eager: true`, kur darbojas `reset()`.

## Notikumu cikls

Dzinējam ir JavaScript notikumu cikls ar `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` un `queueMicrotask` globālajā objektā. Mikrouzdevumi (Promise apstrādātāji, `queueMicrotask`) tiek izpildīti starp katru makrouzdevumu; taimeri nostrādā termiņa secībā, izmantojot katras vides uzdevumu rindu.

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

`eval()` un `evalAsModule()` automātiski izsauc `runEventLoop()` pēc sinhronā koda izpildes. Tādēļ skriptā, kas augšējā līmenī ieplāno `setTimeout(cb, 5000)`, tas tiek izpildīts `cb`, pirms `eval()` atgriežas — cikls izmanto **virtuālo laiku**, pārvietojot pulksteni uz nākamo gaidāmo termiņu, nevis gaidot PHP. Atbilst `node script.js` semantikai.

Reāllaika iegulšanai (ilgstoši darbojošos serveros, UI integrācijā, plānošanā pēc reāla pulksteņa) `tickEventLoop()` izpilda vienu iterāciju (izpilda mikrouzdevumus, iedarbina visus pienākušos taimerus) un atgriežas. `pendingTaskCount()` ziņo par gaidošajiem taimeriem; iegultā lietotne var pati vadīt izpildes ciklu:

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

Nekontrolēta `setInterval` darbība ir ierobežota: `runEventLoop()` apstājas pēc 100,000 cikliem un izmet vienkāršu `\RuntimeException` — skaidra PHP kļūda parāda JS koda vainu, nevis ļauj procesam uzkārties. Intervāls paliek ieplānots, tāpēc katrs nākamais `eval()` tajā pašā dzinējā izmet to pašu kļūdu; pēc tam dzinēju izmetiet. Neuztvertās kļūdas taimera atzvanā tiek izvadītas, izmantojot `error_log()`, un cikls turpina izpildi; viens kļūdains taimeris neaptur programmu.

## Fetch āķi

Phasis pēc noklusējuma ir ieslēgts `fetch()`, kurā par transportu tiek izmantots PHP `ext-curl`. Trīs āķi ļauj iegultajai lietotnei nomainīt transportu, ierobežot pieprasījumus vai ieslēgt sīkfailu glabāšanu. Katrs no tiem pieņem vai nu PHP izsaucamu funkciju, vai metodi objektā; pieļaujamo vērtību struktūru skatiet sadaļā [Tīmekļa API](https://phasis.inline0.dev/lv/docs/compatibility/web-apis).

### setFetchTransport

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

Aizstāj noklusējuma CurlTransport. Izsaucamā funkcija saņem pieprasījuma deskriptoru un neobligātu `AbortSignal` JsObject; atgriež atbildes deskriptoru.

```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(),
    ];
});
```

Izplatīts iegulšanas paņēmiens: testos instalēt imitētu transportu, bet produkcijā — īsto curl transportu.

### setFetchPolicy

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

Iepriekšējas pārbaudes politikas āķis. Saņem `Request` JsObject; var izsaukt kļūdu, lai noraidītu, atgriezt modificētu `Request`, lai pārrakstītu, vai atgriezt `null` / neatgriezt vērtību, lai atļautu.

```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
```

Neobligāta sīkfailu krātuve. Pieņem jebkuru objektu, kas nodrošina `get(string $url): string` un `set(string $url, string $header): void` — PHP vai JS pusē.

```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());
```

Ja nav `setCookieJar()`, `fetch` ignorē gan ienākošās `Set-Cookie`, gan izejošās `Cookie` galvenes — pēc noklusējuma **sīkfaili netiek izmantoti**.

## Izņēmumi

| Klase | Paplašina | Kad |
|---|---|---|
| `Phasis\Exceptions\JsThrowable` | `RuntimeError` | nepārprotami JS `throw`, ko nekas neuztvēra |
| `Phasis\Exceptions\TypeError`, `ReferenceError`, `RangeError` | `RuntimeError` | dzinēja paša izraisīta kļūda (piemēram, `null.x` vai nedefinēts mainīgais), ko nekas neuztvēra |
| `Phasis\Exceptions\InternalError` | `RuntimeError` | pārsniegts cikla ierobežojums vai izsaukumu steka limits |
| `Phasis\Exceptions\RuntimeError` | `\RuntimeException` | iepriekš minēto klašu bāzes klase |
| `Phasis\Parser\ParseError` | `Phasis\Exceptions\SyntaxError` (`\RuntimeException`) | avotu nevar parsēt |
| `\RuntimeException` | | `execFile()` nevar nolasīt failu; notikumu cikls sasniedzis 100,000 ciklu ierobežojumu |

Lai uztvertu visas skripta kļūmes, uztveriet `Phasis\Exceptions\RuntimeError` un `Phasis\Exceptions\SyntaxError` vai vienkārši `\RuntimeException`.

Paša PHP resursdatora funkciju izraisītie izņēmumi atšķiras: pašlaik tie nemainīti iziet cauri JS `try` / `catch` un sasniedz `eval()` izsaucēju, saglabājot sākotnējo klasi ([20. problēma](https://github.com/inline0/phasis/issues/20)). Skatiet sadaļu [Resursdatora funkcijas](https://phasis.inline0.dev/lv/docs/interop/host-functions#throwing-back-into-js).

## Dzīves cikls

`Engine` izveide ir ātra (aukstā palaišanā ~dažas ms), un standarta bibliotēka tiek instalēta pēc vajadzības. Vairumā iegulšanas gadījumu procesam pietiek ar vienu ilgstoši darbojošos dzinēju. Vairāku klientu lietošanai piešķiriet katram klientam vai pieprasījumam savu dzinēju, lai globālie mainīgie starp tiem nenoplūstu. Pašlaik uzticamākais veids, kā sākt ar tīru stāvokli, ir izveidot jaunu dzinēju (skatiet [atiestatīšana](#reset)).

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