# Async & networking

s2script integrates Promises with the game frame. Timers and network I/O never block the main thread. Native work has finite admission: Promise APIs reject with named capacity errors, and socket sends return a boolean.

## Timers

```ts
import { delay, nextTick, nextFrame, threadSleep, after, every } from '@s2script/sdk/timers';

await nextTick(); // after current microtasks
await nextFrame(); // next game frame
await delay(1000); // ~1s, frame-integrated
await threadSleep(50); // worker-thread sleep; settles on a later game frame
```

`delay`, `nextTick`, `nextFrame`, and `threadSleep` reject on overload. `after` and `every` throw `AsyncQueueFull` instead of returning a timer. Existing timer cancellation is unchanged. `threadSleep` returns `Promise<void>` and settles on a later game frame.

## HTTP

```ts
import { fetch } from '@s2script/sdk/http';

const res = await fetch('https://example.com/api', {
	method: 'POST',
	headers: { 'content-type': 'application/json' },
	body: JSON.stringify({ steamId }),
	timeoutMs: 10_000
});

if (res.ok) {
	const data = await res.json();
}
```

HTTP status errors (4xx/5xx) **resolve** with `ok: false` (web-fetch parity — useful on the join path). Network failures and timeouts reject. Admission rejects with `AsyncQueueFull` or `AsyncPayloadTooLarge` before the request is accepted. A response whose UTF-8 body plus retained headers exceeds the configured `http_body_bytes` limit (default 10 MiB) rejects with `HttpResponseTooLarge`.

## WebSocket

```ts
import { WebSocket } from '@s2script/sdk/ws';

const ws = await WebSocket.connect('wss://example.com/ws');
ws.onMessage((text) => console.log(text));
ws.onClose((code) => console.log('closed', code));
if (!ws.send('hello')) {
	// closed, oversized, or admission full — nothing queued
}
ws.close();
```

Subscribe to `onMessage` synchronously after `await connect` so early frames are not missed. Connections are owner-scoped and closed on plugin unload.

`WebSocket.send` returns `boolean`. `false` means closed, oversized, or full admission — nothing was accepted; retry later or close. `true` means queued, not delivered. Existing callers may ignore the return; structural mocks that returned `void` must return `boolean`. Host `apiVersion` 2 remains compatible.

## TCP / UDP

```ts
import { Net } from '@s2script/sdk/net';

const sock = await Net.connectTcp('127.0.0.1', 9000);
sock.onData((bytes) => console.log(bytes.length));
if (!sock.send('hello')) {
	// closed, oversized, or admission full — nothing queued
}

const udp = await Net.udp();
if (!udp.sendTo('127.0.0.1', 9000, 'ping')) {
	// same contract
}
```

`TcpSocket.send` and `UdpSocket.sendTo` use the same boolean admission as `WebSocket.send`.

## Database

```ts
import { Database } from '@s2script/sdk/db';

const db = await Database.open('myplugin');
await db.execute('CREATE TABLE IF NOT EXISTS boots (id INTEGER PRIMARY KEY)');
const rows = await db.query('SELECT * FROM boots');
await db.close();
```

The API is Promise-based (SQLite today; MySQL/Postgres via the same `Database` seam). Handles are opaque, owner-scoped, and ledgered.

`query` / `execute` reject with `AsyncQueueFull` or `AsyncPayloadTooLarge` before acceptance. A result that exceeds `db_result_rows` / `db_result_bytes` rejects with `DatabaseResultTooLarge`.

## Named errors

Promise APIs reject with an `Error` whose `.name` is one of:

| Name                     | When                                                         |
| ------------------------ | ------------------------------------------------------------ |
| `AsyncQueueFull`         | A count or queue slot is exhausted                           |
| `AsyncPayloadTooLarge`   | An input exceeds the configured item/aggregate byte limit    |
| `AsyncCancelled`         | The producer was cancelled before completion                 |
| `HttpResponseTooLarge`   | HTTP body + retained headers exceed `http_body_bytes`        |
| `DatabaseResultTooLarge` | Query rows/bytes exceed `db_result_rows` / `db_result_bytes` |

Check `err.name`, not a message substring.

## Frame budgets

Each game frame drains a bounded batch (`frame_items`, `frame_bytes`, `frame_poll_items`, `frame_soft_us`). Promise and timer work share that budget with cookie and socket callbacks. The elapsed-time check is **soft** and runs between units: one JavaScript callback, V8 materialization, or microtask checkpoint can exceed it. One oversized delivery can run alone so work still makes progress.

These are not a whole-process RSS bound. Counts and bytes cover application-owned retention only — allocator overhead, SQLite page caches, protocol/TLS/kernel buffers, and V8 objects retained by JavaScript sit outside the partitions.

## Operator configuration

Set `S2SCRIPT_ASYNC_LIMITS_JSON` on the game process **before start**. It accepts a JSON object with any subset of the fields below. Unknown fields, zero values, impossible relations, or overflow invalidate the entire override, print a warning, and use defaults. The policy is read once per process — it survives plugin unload and V8 reinitialization. Example: `{"frame_items":64,"frame_bytes":1048576}`.

Byte units in the table are binary KiB/MiB; the JSON itself uses integers in bytes. These are starting defaults, not measured production bandwidth targets. Tune from rejection and retained-byte metrics during soak. Do not change policy in place while producers from an earlier configuration are still alive.

| Fields                                                            | Defaults              | Scope                                                                                               |
| ----------------------------------------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------- |
| `jobs_global`, `jobs_per_owner`                                   | 512, 64               | Admitted producers and retained completions; includes live sockets                                  |
| `input_bytes`, `owner_input_bytes`, `input_item_bytes`            | 32 MiB, 8 MiB, 8 MiB  | Job input aggregate, owner aggregate, single input                                                  |
| `completion_bytes`, `failure_bytes`                               | 16 MiB, 4096          | Aggregate result reservations; reserved failure allowance per job                                   |
| `sockets_global`, `sockets_per_owner`                             | 128, 16               | Actual socket lifetimes, including shutdown in progress                                             |
| `sqlite_global`, `sqlite_per_owner`                               | 32, 4                 | Actual SQLite actor lifetimes                                                                       |
| `pools_global`, `pools_per_owner`                                 | 16, 4                 | Actual remote pool lifetimes; each pool has at most four backend connections                        |
| `socket_out_items`, `socket_out_bytes`, `outbound_bytes`          | 256, 1 MiB, 16 MiB    | Per-socket queued items/bytes (also per-send ceiling), global outbound bytes                        |
| `inbound_items`, `inbound_bytes`                                  | 2048, 16 MiB          | Aggregate socket data events retained by producers, queues, and dispatch                            |
| `timers_global`, `timers_per_owner`                               | 4096, 512             | Pending and selected timers                                                                         |
| `worker_queue_items`, `sqlite_queue_items`                        | 256, 64               | Waiting jobs in the pool / each SQLite actor                                                        |
| `http_body_bytes`                                                 | 10 MiB                | Final UTF-8 body plus retained response headers                                                     |
| `db_result_rows`, `db_result_bytes`                               | 10,000, 8 MiB         | Rows and normalized columns/cells plus conservative row metadata                                    |
| `cookie_versions`, `cookie_bytes`, `cookie_write_bytes`           | 4096, 4 MiB, 64 KiB   | Cookie outbox: retained versions, aggregate bytes, single write — [Cookies](https://s2script.com/docs/concepts/cookies) |
| `frame_items`, `frame_bytes`, `frame_poll_items`, `frame_soft_us` | 256, 2 MiB, 256, 2000 | Logical deliveries, delivered bytes, source polls, soft elapsed-time stop                           |

Plugin `s2script.config` is unrelated — that is the typed per-plugin overlay. See [Config](https://s2script.com/docs/concepts/config).

## Diagnostics

`__s2_async_stats()` is a native helper (no job). It returns a JSON **string**; parse it:

```ts
const stats = JSON.parse(__s2_async_stats());
```

Gauges are diagnostic and can change concurrently — not one transactional snapshot. Counters persist across isolate reinitialization. `rejected` counts failed partition acquisitions/growth; early argument-validation errors are not partition rejections.

```text
jobs, completion, sockets, sqlite, pools, inbound, outbound, timers:
  { items: number, bytes: number, rejected: number }
queued: { worker, http, db, ws, net: number }
staged: { timers, ws, net, cookies, http, db: number }
cache: { accounts, entries, bytes: number }
frame: null | { items, bytes, polls: number }
timerExamined: number
lastNs: number
maxNs: number
```

`queued` vs `staged` is in-flight vs ready-to-deliver. Duration counters (`lastNs`, `maxNs`) cover the async drain through the callback phase.

Browse modules: [timers](https://s2script.com/docs/api/modules/timers), [http](https://s2script.com/docs/api/modules/http), [ws](https://s2script.com/docs/api/modules/ws), [net](https://s2script.com/docs/api/modules/net), [db](https://s2script.com/docs/api/modules/db).
