this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: tessera-geotessera-jsoo implementation plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+281
+281
docs/plans/2026-03-06-tessera-geotessera-jsoo.md
··· 1 + # tessera-geotessera-jsoo Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** A thin js_of_ocaml wrapper providing browser-compatible synchronous HTTP fetch for `tessera-geotessera`, enabling tile fetching in web workers. 6 + 7 + **Architecture:** The core `tessera-geotessera` is parameterised over `fetch:(string -> string)`. This package provides a synchronous XHR-based fetch (works in web workers) and a convenience `fetch_mosaic` that wires it in. Playwright tests verify it works in a real browser. 8 + 9 + **Tech Stack:** OCaml, js_of_ocaml, js_of_ocaml-ppx, dune, Playwright 10 + 11 + --- 12 + 13 + ### Task 1: Project scaffold 14 + 15 + **Files:** 16 + - Create: `tessera-geotessera-jsoo/dune-project` 17 + - Create: `tessera-geotessera-jsoo/lib/dune` 18 + - Create: `tessera-geotessera-jsoo/lib/geotessera_jsoo.mli` 19 + - Create: `tessera-geotessera-jsoo/lib/geotessera_jsoo.ml` 20 + 21 + **Step 1: Create dune-project** 22 + 23 + ``` 24 + tessera-geotessera-jsoo/dune-project 25 + ``` 26 + 27 + ```dune 28 + (lang dune 3.17) 29 + (name tessera-geotessera-jsoo) 30 + (generate_opam_files true) 31 + (license ISC) 32 + (package 33 + (name tessera-geotessera-jsoo) 34 + (synopsis "Browser fetch for tessera-geotessera") 35 + (description "Synchronous XHR-based fetch for GeoTessera tile retrieval in web workers.") 36 + (depends 37 + (ocaml (>= 5.2)) 38 + (tessera-geotessera (>= 0.1)) 39 + (js_of_ocaml (>= 5.0)) 40 + (js_of_ocaml-ppx (>= 5.0)))) 41 + ``` 42 + 43 + **Step 2: Create lib/dune** 44 + 45 + ```dune 46 + (library 47 + (name geotessera_jsoo) 48 + (public_name tessera-geotessera-jsoo) 49 + (libraries tessera-geotessera js_of_ocaml) 50 + (preprocess (pps js_of_ocaml-ppx))) 51 + ``` 52 + 53 + **Step 3: Create lib/geotessera_jsoo.mli** 54 + 55 + ```ocaml 56 + (** Browser-compatible fetch for GeoTessera tiles. 57 + 58 + Provides synchronous HTTP GET via XMLHttpRequest for use in web workers, 59 + and a convenience wrapper around {!Geotessera.fetch_mosaic_sync}. *) 60 + 61 + val fetch : string -> string 62 + (** Synchronous HTTP GET via XMLHttpRequest. 63 + Returns the response body as a string. 64 + @raise Failure on non-200 status or if response cannot be read. *) 65 + 66 + val fetch_mosaic : ?base_url:string -> ?version:string -> year:int -> 67 + Geotessera.bbox -> Linalg.mat * int * int 68 + (** Fetch and assemble tiles for a bounding box using browser XHR. 69 + Convenience wrapper around {!Geotessera.fetch_mosaic_sync} 70 + with {!fetch} as the fetch function. *) 71 + ``` 72 + 73 + **Step 4: Create lib/geotessera_jsoo.ml** 74 + 75 + ```ocaml 76 + open Js_of_ocaml 77 + 78 + let fetch url = 79 + let x = XmlHttpRequest.create () in 80 + x##.responseType := Js.string "arraybuffer"; 81 + x##_open (Js.string "GET") (Js.string url) Js._false; 82 + x##send Js.null; 83 + match x##.status with 84 + | 200 -> 85 + Js.Opt.case 86 + (File.CoerceTo.arrayBuffer x##.response) 87 + (fun () -> failwith ("Failed to read response from " ^ url)) 88 + (fun b -> Typed_array.String.of_arrayBuffer b) 89 + | code -> 90 + failwith (Printf.sprintf "HTTP %d fetching %s" code url) 91 + 92 + let fetch_mosaic ?base_url ?version ~year bbox = 93 + Geotessera.fetch_mosaic_sync ~fetch ?base_url ?version ~year bbox 94 + ``` 95 + 96 + **Step 5: Build** 97 + 98 + Run: `cd ~/workspace/mono && opam exec -- dune build -p tessera-geotessera-jsoo` 99 + Expected: Build succeeds with no errors. 100 + 101 + **Step 6: Commit** 102 + 103 + ``` 104 + git add tessera-geotessera-jsoo/ 105 + git commit -m "tessera-geotessera-jsoo: scaffold with sync XHR fetch" 106 + ``` 107 + 108 + --- 109 + 110 + ### Task 2: Playwright browser test 111 + 112 + **Files:** 113 + - Create: `tessera-geotessera-jsoo/test/dune` 114 + - Create: `tessera-geotessera-jsoo/test/test_browser.ml` 115 + - Create: `tessera-geotessera-jsoo/test/test_browser.html` 116 + - Create: `tessera-geotessera-jsoo/test/run_playwright.sh` 117 + 118 + **Context:** The test compiles an OCaml program to JS, serves it with a local HTTP server, and uses Playwright to verify it works in a real browser. The test fetches a single scales.npy file from dl2.geotessera.org (small, CORS-enabled, no auth). 119 + 120 + **Step 1: Create test/dune** 121 + 122 + This builds the test as a JS executable (not a normal test — Playwright runs it separately). 123 + 124 + ```dune 125 + (executable 126 + (name test_browser) 127 + (modes js) 128 + (libraries tessera-geotessera-jsoo tessera-geotessera tessera-linalg tessera-npy js_of_ocaml) 129 + (preprocess (pps js_of_ocaml-ppx))) 130 + ``` 131 + 132 + **Step 2: Create test/test_browser.ml** 133 + 134 + This runs in the browser and writes results to the DOM. 135 + 136 + ```ocaml 137 + open Js_of_ocaml 138 + 139 + let set_result id text = 140 + let doc = Dom_html.document in 141 + Js.Opt.iter (doc##getElementById (Js.string id)) (fun el -> 142 + el##.textContent := Js.some (Js.string text)) 143 + 144 + let () = 145 + try 146 + (* Test 1: fetch a single scales file *) 147 + let url = "https://dl2.geotessera.org/v1/global_0.1_degree_representation/2024/grid_0.15_52.05/grid_0.15_52.05_scales.npy" in 148 + let data = Geotessera_jsoo.fetch url in 149 + let len = String.length data in 150 + set_result "fetch-result" 151 + (if len > 100 then Printf.sprintf "OK: %d bytes" len 152 + else Printf.sprintf "FAIL: only %d bytes" len); 153 + 154 + (* Test 2: parse the fetched npy *) 155 + let npy = Npy.of_string data |> Result.get_ok in 156 + let shape = Npy.shape npy in 157 + set_result "parse-result" 158 + (Printf.sprintf "OK: shape %s" 159 + (String.concat "x" (Array.to_list (Array.map string_of_int shape)))); 160 + 161 + (* Test 3: fetch_mosaic on a single-tile bbox *) 162 + let bbox = Geotessera.{ min_lon = 0.1; min_lat = 52.0; max_lon = 0.2; max_lat = 52.1 } in 163 + let mosaic, h, w = Geotessera_jsoo.fetch_mosaic ~year:2024 bbox in 164 + set_result "mosaic-result" 165 + (Printf.sprintf "OK: %d rows x %d cols, %dx%d" mosaic.rows mosaic.cols h w); 166 + 167 + set_result "status" "ALL PASSED" 168 + with exn -> 169 + set_result "status" (Printf.sprintf "FAILED: %s" (Printexc.to_string exn)) 170 + ``` 171 + 172 + **Step 3: Create test/test_browser.html** 173 + 174 + ```html 175 + <!DOCTYPE html> 176 + <html> 177 + <head><title>tessera-geotessera-jsoo test</title></head> 178 + <body> 179 + <h1>tessera-geotessera-jsoo Browser Tests</h1> 180 + <p>Fetch: <span id="fetch-result">pending...</span></p> 181 + <p>Parse: <span id="parse-result">pending...</span></p> 182 + <p>Mosaic: <span id="mosaic-result">pending...</span></p> 183 + <p>Status: <span id="status">running...</span></p> 184 + <script src="test_browser.bc.js"></script> 185 + </body> 186 + </html> 187 + ``` 188 + 189 + **Step 4: Create test/run_playwright.sh** 190 + 191 + This script builds, serves, and runs the Playwright test. It can be run manually or from CI. 192 + 193 + ```bash 194 + #!/usr/bin/env bash 195 + set -euo pipefail 196 + 197 + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 198 + MONO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" 199 + TEST_BUILD="$MONO_DIR/_build/default/tessera-geotessera-jsoo/test" 200 + 201 + # Build the JS test 202 + echo "Building test JS..." 203 + cd "$MONO_DIR" 204 + opam exec -- dune build tessera-geotessera-jsoo/test/test_browser.bc.js 205 + 206 + # Copy HTML to build dir 207 + cp "$SCRIPT_DIR/test_browser.html" "$TEST_BUILD/" 208 + 209 + # Start local server 210 + echo "Starting local server..." 211 + cd "$TEST_BUILD" 212 + python3 -m http.server 8765 & 213 + SERVER_PID=$! 214 + trap "kill $SERVER_PID 2>/dev/null || true" EXIT 215 + 216 + # Wait for server 217 + sleep 1 218 + 219 + echo "Server running on http://localhost:8765" 220 + echo "Run Playwright tests against http://localhost:8765/test_browser.html" 221 + echo "Server PID: $SERVER_PID (will be killed on exit)" 222 + 223 + # If called with --wait, keep server running for manual Playwright testing 224 + if [[ "${1:-}" == "--wait" ]]; then 225 + echo "Waiting... Press Ctrl+C to stop." 226 + wait $SERVER_PID 227 + fi 228 + ``` 229 + 230 + **Step 5: Build the JS test** 231 + 232 + Run: `cd ~/workspace/mono && opam exec -- dune build tessera-geotessera-jsoo/test/test_browser.bc.js` 233 + Expected: Build succeeds, produces `_build/default/tessera-geotessera-jsoo/test/test_browser.bc.js`. 234 + 235 + **Step 6: Run with Playwright** 236 + 237 + 1. Start the server: 238 + ```bash 239 + chmod +x tessera-geotessera-jsoo/test/run_playwright.sh 240 + ./tessera-geotessera-jsoo/test/run_playwright.sh --wait & 241 + ``` 242 + 243 + 2. Use Playwright MCP to: 244 + - Navigate to `http://localhost:8765/test_browser.html` 245 + - Wait for `#status` to not contain "running..." 246 + - Assert `#status` text is "ALL PASSED" 247 + - Assert `#fetch-result` starts with "OK:" 248 + - Assert `#parse-result` starts with "OK:" 249 + - Assert `#mosaic-result` starts with "OK:" 250 + 251 + 3. Kill the server. 252 + 253 + **Step 7: Commit** 254 + 255 + ``` 256 + git add tessera-geotessera-jsoo/test/ 257 + git commit -m "tessera-geotessera-jsoo: add Playwright browser test" 258 + ``` 259 + 260 + --- 261 + 262 + ### Task 3: Documentation and opam 263 + 264 + **Files:** 265 + - Modify: `tessera-geotessera-jsoo/dune-project` (if needed) 266 + 267 + **Step 1: Generate opam file** 268 + 269 + Run: `cd ~/workspace/mono && opam exec -- dune build tessera-geotessera-jsoo/tessera-geotessera-jsoo.opam` 270 + Expected: opam file generated. 271 + 272 + **Step 2: Verify the opam file looks correct** 273 + 274 + Read the generated opam file. It should list `tessera-geotessera`, `js_of_ocaml`, and `js_of_ocaml-ppx` as dependencies. 275 + 276 + **Step 3: Commit** 277 + 278 + ``` 279 + git add tessera-geotessera-jsoo/tessera-geotessera-jsoo.opam 280 + git commit -m "tessera-geotessera-jsoo: generate opam file" 281 + ```