Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 6 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,17 @@
# Edge Python Host

Official JS modules for [Edge Python](https://edgepython.com) that expose host APIs (DOM, ) to Python scripts. Each capability is a plain ESM that registers with `createWorker` via `mainThreadModules` — no `.wasm`, no Rust, no custom embedder.
Official JS modules for [Edge Python](https://edgepython.com) exposing host APIs (DOM, network, storage) to Python scripts. Each capability is a plain ESM registered with `createWorker` via `mainThreadModules` — no `.wasm`, no Rust, no custom embedder.

## Layout

```
edge-python-host/
├── dom/
│ ├── src/
│ ├── web/
│ ├── tests/
│ └── README.md
├── network/
│ ├── src/
│ ├── web/
│ ├── tests/
│ └── README.md
├── storage/
│ ├── src/
│ ├── web/
│ ├── tests/
│ └── README.md
└── static/
dom/ — src/, web/, tests/, README.md
network/ — src/, web/, tests/, README.md
storage/ — src/, web/, tests/, README.md
static/
```

Each top-level folder is one capability.
One folder per capability.

## Usage

Expand Down
27 changes: 12 additions & 15 deletions dom/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Edge Python DOM

DOM access for Edge Python, shipped as a plain ESM module. Python scripts see `dom` as an ordinary module — full surface coverage: queries, mutation, events, forms, files, observers, animations, layout, media, SVG, and modern platform APIs (dialog, fullscreen, pointer lock).
DOM access shipped as a plain ESM module. Scripts see `dom` as ordinary — full surface: queries, mutation, events, forms, files, observers, animations, layout, media, SVG, dialog, fullscreen, pointer lock.

```python
from dom import query, set_text, bind_event
Expand Down Expand Up @@ -29,7 +29,7 @@ async def main():
</script>
```

The engine runs in a Web Worker; `dom` handlers run on the page's main thread (where `document` lives) via the runtime's deferred host-call mechanism. Python sees every call as synchronous.
Engine runs in a Web Worker; `dom` handlers run on the page's main thread (where `document` lives) via the runtime's deferred host-call mechanism. Python sees every call as synchronous.

## Quick start

Expand All @@ -43,14 +43,11 @@ Open <http://127.0.0.1:8080/dom/web/>. No build step.

## Testing

A smoke test loads the demo in headless Chromium, clicks through a few interactions, and fails if any console error fires.
Headless Chromium smoke test loads the demo, clicks through, fails on console error:

```bash
# Deno setup
curl -fsSL https://deno.land/install.sh | sh
source ~/.bashrc

#Cache the browser binary
# Setup
curl -fsSL https://deno.land/install.sh | sh && source ~/.bashrc
deno run -A npm:playwright install chromium

# Run
Expand All @@ -60,14 +57,14 @@ deno test --allow-all tests/dom.test.js

## API

**Conventions.**
**Conventions:**

- Handles are opaque integers — store them, pass them, never compute on them.
- Handles are opaque integers — store, pass, never compute on them.
- Multi-result queries (`query_all`, `children`) return CSV strings of handles.
- Structured returns (`rect`, `validity`, `form_data`, `bbox`, event payloads) are JSON strings.
- All async results (events, FileReader, animation finishes, observer entries) arrive through `receive()`.
- Async results (events, FileReader, animation finishes, observer entries) arrive via `receive()`.

**Parsing JSON returns.** Edge Python has no stdlib `json` — declare one in your `packages.json` (e.g. `{ "imports": { "json": "https://runtime.edgepython.com/lib/json.py" } }`) to get `json.loads`. For simple dispatch by event tag you can skip it and substring-match the raw payload (`'"msg":"click"' in ev`) as shown in `web/palette.py`. Examples below assume `json` is mapped where they use it.
**Parsing JSON returns.** The runtime auto-registers `json` — `from json import loads, dumps` works with zero setup. Examples below assume it.

### Selection and traversal

Expand Down Expand Up @@ -245,19 +242,19 @@ append_child(query("svg"), circle)

## How it works

The module is a factory `(ctx) => handlers`. `src/state.js` opens a fresh closure per `createWorker` with the handle tables (`nodes`, `bindings`, `files`, observers, animations) and the `alloc` / `node` / `allocList` helpers. Eight handler slices (`tree`, `style`, `events`, `forms`, `observers`, `animations`, `media`, `platform`) each return an object literal of named handlers that close over the shared state; `src/index.js` composes them with `Object.assign`. Async callbacks (event listeners, `FileReader`, animation `finished`, observer entries) call `ctx.pushEvent(jsonDetail)` to wake a paused `receive()` in the script.
Factory `(ctx) => handlers`. `src/state.js` opens a fresh closure per `createWorker` with handle tables (`nodes`, `bindings`, `files`, observers, animations) and `alloc` / `node` / `allocList` helpers. Eight handler slices (`tree`, `style`, `events`, `forms`, `observers`, `animations`, `media`, `platform`) each return an object literal of named handlers closing over the shared state; `src/index.js` composes them with `Object.assign`. Async callbacks call `ctx.pushEvent(jsonDetail)` to wake a paused `receive()`.

Adding a handler is one entry in one slice. Nothing else changes.

## Performance

Per-handler cost is one `postMessage` round-trip between the Worker (engine) and the main thread (handlers): ~0.1–0.4 ms in modern browsers. Plenty of headroom for UI-rate workloads events, mutations, layoutat hundreds of ops per frame.
Per-handler cost is one `postMessage` round-trip (~0.1–0.4 ms in modern browsers) — plenty for UI-rate workloads (events, mutations, layout) at hundreds of ops/frame.

Bad fit: tight per-frame loops with thousands of fine-grained ops, or pixel-precise renders. Pair with a `<canvas>` capability for the framebuffer path.

## Distribution

This repo serves only the JS sources. `compiler_lib.wasm` and the Edge Python runtime both come from `runtime.edgepython.com` at page load — no vendored copy here, no build step.
JS sources only `compiler_lib.wasm` and the runtime load from `runtime.edgepython.com` at page load. No vendored copy, no build step.

## License

Expand Down
48 changes: 26 additions & 22 deletions network/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# Edge Python Network

HTTP, WebSocket, and Server-Sent Events for Edge Python, shipped as a plain ESM module. Python scripts see `network` as an ordinary module.
HTTP, WebSocket, and SSE shipped as a plain ESM module. Scripts see `network` as ordinary.

```python
from network import fetch_json, ws_open, ws_send

import json

# HTTP — yields and resumes when the response arrives. Composes with gather / with_timeout.
data = fetch_json("https://api.example.com/users")

Expand All @@ -13,9 +15,9 @@ sock = ws_open("wss://example.com/socket", "msg")
ws_send(sock, "hello")
async def main():
while True:
ev = receive()
if '"type":"message"' in ev:
print(ev)
ev = json.loads(receive())
if ev["type"] == "message":
print(ev["data"])
```

## Setup
Expand Down Expand Up @@ -47,11 +49,11 @@ Open <http://127.0.0.1:8080/network/web/>. No build step.

### Conventions

- **HTTP handlers are yielding host calls.** They return a Promise on the JS side and the runtime parks the coro in `WaitingHostCall` until it resolves — Python sees a sync-looking call that suspends. `gather()`, `with_timeout()`, and `run()` work over them automatically without `await`/`receive()` boilerplate.
- **WebSocket and SSE use the push-event pattern** (same as `dom`'s `bind_event`). Connections are opened with a `msg` tag; every wire event arrives in Python via `receive()` carrying that tag.
- **Handles are integer IDs** — store them, pass them, never compute on them.
- **Options are JSON strings** — `fetch(url, '{"method":"POST","body":"..."}')`. Mirrors `bind_event` and `animate` in `dom`.
- **All response bodies arrive as strings.** For JSON, parse with `json.loads` (declare the module in `packages.json` if you haven't yet — see [the dom README](../dom/README.md#api) for the import map snippet).
- **HTTP handlers yield.** They return a Promise on the JS side; the runtime parks the coro in `WaitingHostCall` until it resolves — Python sees a sync-looking call that suspends. `gather()`, `with_timeout()`, and `run()` work over them with no `await`/`receive()`.
- **WebSocket/SSE use push-events** (like `dom`'s `bind_event`). Connections open with a `msg` tag; every wire event arrives via `receive()` as JSON.
- **Handles are integer IDs.**
- **Options are JSON strings** — `fetch(url, '{"method":"POST","body":"..."}')`.
- **Response bodies are strings.** Parse JSON with `json.loads` — `json` is auto-registered by the runtime.

### HTTP

Expand Down Expand Up @@ -98,18 +100,19 @@ except TimeoutError:
### WebSocket

```python
import json
from network import ws_open, ws_send, ws_close, ws_state

sock = ws_open("wss://example.com/socket", "ws")

async def main():
while True:
ev = receive()
if '"type":"open"' in ev:
ev = json.loads(receive())
if ev["type"] == "open":
ws_send(sock, "hello")
elif '"type":"message"' in ev:
print(ev) # {"msg":"ws","type":"message","data":"..."}
elif '"type":"close"' in ev:
elif ev["type"] == "message":
print(ev["data"])
elif ev["type"] == "close":
return

run(main())
Expand All @@ -122,16 +125,17 @@ Payload `type` values: `open`, `message`, `close`, `error`. `message` carries `d
### Server-Sent Events

```python
import json
from network import sse_open, sse_close

stream = sse_open("/events", "sse")

async def main():
while True:
ev = receive()
if '"type":"message"' in ev:
print(ev) # {"msg":"sse","type":"message","data":"...","event_id":"..."}
elif '"type":"error"' in ev:
ev = json.loads(receive())
if ev["type"] == "message":
print(ev["data"])
elif ev["type"] == "error":
sse_close(stream)
return

Expand All @@ -142,17 +146,17 @@ run(main())

## How it works

`network/src/index.js` is a factory `(ctx) => handlers`, the same shape `dom` uses. Three handler slices (`http`, `ws`, `sse`) close over a shared `state` (handle tables for in-flight requests, sockets, SSE sources) and are merged with `Object.assign`.
`src/index.js` is a factory `(ctx) => handlers` (same shape as `dom`). Three slices (`http`, `ws`, `sse`) close over a shared `state` (handle tables for in-flight requests, sockets, SSE sources) and merge with `Object.assign`.

The HTTP slice returns **async handlers** (`async (url) => { ... return body; }`). The runtime detects the returned Promise and parks the calling coro in `WaitingHostCall` until it resolves — equivalent to how `sleep()` parks until a deadline. WS/SSE slices return synchronous handlers that wire DOM-style listeners into `ctx.pushEvent`, mirroring `bind_event` exactly.
HTTP slice returns async handlers (`async (url) => { ... return body; }`); the runtime detects the Promise and parks in `WaitingHostCall` until resolved — same shape as `sleep()`. WS/SSE slices return sync handlers wiring DOM-style listeners into `ctx.pushEvent`.

## Performance

Per-handler cost is one `postMessage` round-trip per call. HTTP handlers add the network latency on top (the dominant cost). For pipelines that do many small same-host requests, prefer one larger request over many small ones — same advice as in plain JS.
Per-handler cost is one `postMessage` round-trip per call; HTTP adds network latency on top. For many small same-host requests, prefer one larger request — same advice as plain JS.

## Distribution

This repo serves only the JS sources. `compiler_lib.wasm` and the Edge Python runtime both come from `runtime.edgepython.com` at page load — no vendored copy here, no build step.
JS sources only `compiler_lib.wasm` and the runtime load from `runtime.edgepython.com`. No build step.

## License

Expand Down
16 changes: 8 additions & 8 deletions storage/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Edge Python Storage

Persistent client-side storage for Edge Python — `localStorage`, `sessionStorage`, and `IndexedDB`. Shipped as a plain ESM module that registers with `createWorker`.
Persistent client-side storage — `localStorage`, `sessionStorage`, `IndexedDB`. Plain ESM module registered with `createWorker`.

```python
from storage import local_set, local_get, idb_open, idb_put, idb_get
Expand Down Expand Up @@ -44,11 +44,11 @@ Open <http://127.0.0.1:8080/storage/web/>. No build step.

### Conventions

- **Key-value handlers are sync.** `localStorage` and `sessionStorage` are blocking by spec; handlers return strings or `None`. No `await`, no `receive()`.
- **IndexedDB handlers are async (yielding host calls).** They return a Promise on the JS side; the runtime parks the calling coro in `WaitingHostCall` until it resolves. Python sees a sync-looking call that suspends, identical to `fetch()` in [`network/`](../network/README.md).
- **Values cross as JSON strings.** For `idb_put` / `idb_get`, encode/decode with `json.dumps` / `json.loads`. Storing structured objects directly would require crossing arbitrary types over the worker boundary — JSON is the same trade-off `dom`'s `animate` and `bind_event` make for options.
- **Key listings are JSON arrays, not CSV.** `local_keys()` / `session_keys()` / `idb_keys(...)` return a JSON-array string (because keys can contain commas). Parse with `json.loads`.
- **Handles are integer IDs** for IndexedDB; `local_*` / `session_*` need no handle (the global stores are addressed directly).
- **KV handlers are sync.** `localStorage` / `sessionStorage` are blocking by spec; handlers return strings or `None`. No `await`, no `receive()`.
- **IndexedDB handlers yield.** They return a Promise on the JS side; the runtime parks the coro in `WaitingHostCall` until resolved — same shape as `fetch()` in [`network/`](../network/README.md).
- **Values cross as JSON strings.** Encode with `json.dumps`, decode with `json.loads`. Same trade-off `dom`'s `animate` and `bind_event` make for options.
- **Key listings are JSON arrays** (keys can contain commas). Parse with `json.loads`.
- **Handles are integer IDs** for IndexedDB; `local_*` / `session_*` address global stores directly (no handle).

### localStorage / sessionStorage

Expand Down Expand Up @@ -105,9 +105,9 @@ except TimeoutError:

## How it works

`storage/src/index.js` is a factory `() => handlers`, the same shape `dom` and `network` use. Two handler slices (`kv`, `idb`) close over a shared `state` (just a handle table for open IDBDatabase instances) and are merged with `Object.assign`.
`src/index.js` is a factory `() => handlers` (same shape as `dom`, `network`). Two slices (`kv`, `idb`) close over a shared `state` (a handle table for open `IDBDatabase` instances) and merge with `Object.assign`.

The KV slice returns **synchronous handlers** that call `localStorage` / `sessionStorage` directly. The IDB slice returns **async handlers** that promisify each native `IDBRequest`; the runtime detects the Promise return and parks the coro until it resolves.
KV slice returns sync handlers calling `localStorage` / `sessionStorage` directly. IDB slice returns async handlers that promisify native `IDBRequest`s; runtime detects the Promise and parks until resolved.

## License

Expand Down