Skip to content

Commit 9ee1caf

Browse files
authored
feat(wasm): bump go to 1.21 (#3710)
1 parent 7f06641 commit 9ee1caf

File tree

6 files changed

+365
-14
lines changed

6 files changed

+365
-14
lines changed

.github/workflows/wasm.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
build-and-test:
99
strategy:
1010
matrix:
11-
go-version: [1.20.x]
11+
go-version: [1.21.x]
1212
platform: [ubuntu-latest]
1313
runs-on: ${{ matrix.platform }}
1414
steps:
@@ -33,6 +33,5 @@ jobs:
3333
env:
3434
TESTER: true
3535
- name: Run prettier
36-
uses: actionsx/prettier@v2
37-
with:
38-
args: --config wasm/.prettierrc --ignore-path wasm/.prettierignore --check wasm
36+
working-directory: wasm
37+
run: pnpm lint

internal/wasm/roundtrip.go

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Code in this file is edited from go source code at net/http/roundtrip_js.go
6+
7+
//go:build wasm
8+
9+
package wasm
10+
11+
import (
12+
"errors"
13+
"fmt"
14+
"io"
15+
"net/http"
16+
"strconv"
17+
"syscall/js"
18+
19+
_ "unsafe"
20+
)
21+
22+
var uint8Array = js.Global().Get("Uint8Array")
23+
24+
// jsFetchMode is a Request.Header map key that, if present,
25+
// signals that the map entry is actually an option to the Fetch API mode setting.
26+
// Valid values are: "cors", "no-cors", "same-origin", "navigate"
27+
// The default is "same-origin".
28+
//
29+
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters
30+
const jsFetchMode = "js.fetch:mode"
31+
32+
// jsFetchCreds is a Request.Header map key that, if present,
33+
// signals that the map entry is actually an option to the Fetch API credentials setting.
34+
// Valid values are: "omit", "same-origin", "include"
35+
// The default is "same-origin".
36+
//
37+
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters
38+
const jsFetchCreds = "js.fetch:credentials"
39+
40+
// jsFetchRedirect is a Request.Header map key that, if present,
41+
// signals that the map entry is actually an option to the Fetch API redirect setting.
42+
// Valid values are: "follow", "error", "manual"
43+
// The default is "follow".
44+
//
45+
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters
46+
const jsFetchRedirect = "js.fetch:redirect"
47+
48+
var errClosed = errors.New("internal/wasm: reader is closed")
49+
50+
// streamReader implements an io.ReadCloser wrapper for ReadableStream.
51+
// See https://fetch.spec.whatwg.org/#readablestream for more information.
52+
type streamReader struct {
53+
pending []byte
54+
stream js.Value
55+
err error // sticky read error
56+
}
57+
58+
func (r *streamReader) Read(p []byte) (n int, err error) {
59+
if r.err != nil {
60+
return 0, r.err
61+
}
62+
if len(r.pending) == 0 {
63+
var (
64+
bCh = make(chan []byte, 1)
65+
errCh = make(chan error, 1)
66+
)
67+
success := js.FuncOf(func(this js.Value, args []js.Value) any {
68+
result := args[0]
69+
if result.Get("done").Bool() {
70+
errCh <- io.EOF
71+
return nil
72+
}
73+
value := make([]byte, result.Get("value").Get("byteLength").Int())
74+
js.CopyBytesToGo(value, result.Get("value"))
75+
bCh <- value
76+
return nil
77+
})
78+
defer success.Release()
79+
failure := js.FuncOf(func(this js.Value, args []js.Value) any {
80+
// Assumes it's a TypeError. See
81+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError
82+
// for more information on this type. See
83+
// https://streams.spec.whatwg.org/#byob-reader-read for the spec on
84+
// the read method.
85+
errCh <- errors.New(args[0].Get("message").String())
86+
return nil
87+
})
88+
defer failure.Release()
89+
r.stream.Call("read").Call("then", success, failure)
90+
select {
91+
case b := <-bCh:
92+
r.pending = b
93+
case err := <-errCh:
94+
r.err = err
95+
return 0, err
96+
}
97+
}
98+
n = copy(p, r.pending)
99+
r.pending = r.pending[n:]
100+
return n, nil
101+
}
102+
103+
func (r *streamReader) Close() error {
104+
// This ignores any error returned from cancel method. So far, I did not encounter any concrete
105+
// situation where reporting the error is meaningful. Most users ignore error from resp.Body.Close().
106+
// If there's a need to report error here, it can be implemented and tested when that need comes up.
107+
r.stream.Call("cancel")
108+
if r.err == nil {
109+
r.err = errClosed
110+
}
111+
return nil
112+
}
113+
114+
// arrayReader implements an io.ReadCloser wrapper for ArrayBuffer.
115+
// https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer.
116+
type arrayReader struct {
117+
arrayPromise js.Value
118+
pending []byte
119+
read bool
120+
err error // sticky read error
121+
}
122+
123+
func (r *arrayReader) Read(p []byte) (n int, err error) {
124+
if r.err != nil {
125+
return 0, r.err
126+
}
127+
if !r.read {
128+
r.read = true
129+
var (
130+
bCh = make(chan []byte, 1)
131+
errCh = make(chan error, 1)
132+
)
133+
success := js.FuncOf(func(this js.Value, args []js.Value) any {
134+
// Wrap the input ArrayBuffer with a Uint8Array
135+
uint8arrayWrapper := uint8Array.New(args[0])
136+
value := make([]byte, uint8arrayWrapper.Get("byteLength").Int())
137+
js.CopyBytesToGo(value, uint8arrayWrapper)
138+
bCh <- value
139+
return nil
140+
})
141+
defer success.Release()
142+
failure := js.FuncOf(func(this js.Value, args []js.Value) any {
143+
// Assumes it's a TypeError. See
144+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError
145+
// for more information on this type.
146+
// See https://fetch.spec.whatwg.org/#concept-body-consume-body for reasons this might error.
147+
errCh <- errors.New(args[0].Get("message").String())
148+
return nil
149+
})
150+
defer failure.Release()
151+
r.arrayPromise.Call("then", success, failure)
152+
select {
153+
case b := <-bCh:
154+
r.pending = b
155+
case err := <-errCh:
156+
return 0, err
157+
}
158+
}
159+
if len(r.pending) == 0 {
160+
return 0, io.EOF
161+
}
162+
n = copy(p, r.pending)
163+
r.pending = r.pending[n:]
164+
return n, nil
165+
}
166+
167+
func (r *arrayReader) Close() error {
168+
if r.err == nil {
169+
r.err = errClosed
170+
}
171+
return nil
172+
}
173+
174+
type Transport struct {
175+
}
176+
177+
// RoundTrip implements the RoundTripper interface using the WHATWG Fetch API.
178+
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
179+
180+
ac := js.Global().Get("AbortController")
181+
if !ac.IsUndefined() {
182+
// Some browsers that support WASM don't necessarily support
183+
// the AbortController. See
184+
// https://developer.mozilla.org/en-US/docs/Web/API/AbortController#Browser_compatibility.
185+
ac = ac.New()
186+
}
187+
188+
opt := js.Global().Get("Object").New()
189+
// See https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
190+
// for options available.
191+
opt.Set("method", req.Method)
192+
opt.Set("credentials", "same-origin")
193+
if h := req.Header.Get(jsFetchCreds); h != "" {
194+
opt.Set("credentials", h)
195+
req.Header.Del(jsFetchCreds)
196+
}
197+
if h := req.Header.Get(jsFetchMode); h != "" {
198+
opt.Set("mode", h)
199+
req.Header.Del(jsFetchMode)
200+
}
201+
if h := req.Header.Get(jsFetchRedirect); h != "" {
202+
opt.Set("redirect", h)
203+
req.Header.Del(jsFetchRedirect)
204+
}
205+
if !ac.IsUndefined() {
206+
opt.Set("signal", ac.Get("signal"))
207+
}
208+
headers := js.Global().Get("Headers").New()
209+
for key, values := range req.Header {
210+
for _, value := range values {
211+
headers.Call("append", key, value)
212+
}
213+
}
214+
opt.Set("headers", headers)
215+
216+
if req.Body != nil {
217+
// TODO(johanbrandhorst): Stream request body when possible.
218+
// See https://bugs.chromium.org/p/chromium/issues/detail?id=688906 for Blink issue.
219+
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1387483 for Firefox issue.
220+
// See https://github.com/web-platform-tests/wpt/issues/7693 for WHATWG tests issue.
221+
// See https://developer.mozilla.org/en-US/docs/Web/API/Streams_API for more details on the Streams API
222+
// and browser support.
223+
// NOTE(haruyama480): Ensure HTTP/1 fallback exists.
224+
// See https://go.dev/issue/61889 for discussion.
225+
body, err := io.ReadAll(req.Body)
226+
if err != nil {
227+
req.Body.Close() // RoundTrip must always close the body, including on errors.
228+
return nil, err
229+
}
230+
req.Body.Close()
231+
if len(body) != 0 {
232+
buf := uint8Array.New(len(body))
233+
js.CopyBytesToJS(buf, body)
234+
opt.Set("body", buf)
235+
}
236+
}
237+
238+
fetchPromise := js.Global().Call("fetch", req.URL.String(), opt)
239+
var (
240+
respCh = make(chan *http.Response, 1)
241+
errCh = make(chan error, 1)
242+
success, failure js.Func
243+
)
244+
success = js.FuncOf(func(this js.Value, args []js.Value) any {
245+
success.Release()
246+
failure.Release()
247+
248+
result := args[0]
249+
header := http.Header{}
250+
// https://developer.mozilla.org/en-US/docs/Web/API/Headers/entries
251+
headersIt := result.Get("headers").Call("entries")
252+
for {
253+
n := headersIt.Call("next")
254+
if n.Get("done").Bool() {
255+
break
256+
}
257+
pair := n.Get("value")
258+
key, value := pair.Index(0).String(), pair.Index(1).String()
259+
ck := http.CanonicalHeaderKey(key)
260+
header[ck] = append(header[ck], value)
261+
}
262+
263+
contentLength := int64(0)
264+
clHeader := header.Get("Content-Length")
265+
switch {
266+
case clHeader != "":
267+
cl, err := strconv.ParseInt(clHeader, 10, 64)
268+
if err != nil {
269+
errCh <- fmt.Errorf("net/http: ill-formed Content-Length header: %v", err)
270+
return nil
271+
}
272+
if cl < 0 {
273+
// Content-Length values less than 0 are invalid.
274+
// See: https://datatracker.ietf.org/doc/html/rfc2616/#section-14.13
275+
errCh <- fmt.Errorf("net/http: invalid Content-Length header: %q", clHeader)
276+
return nil
277+
}
278+
contentLength = cl
279+
default:
280+
// If the response length is not declared, set it to -1.
281+
contentLength = -1
282+
}
283+
284+
b := result.Get("body")
285+
var body io.ReadCloser
286+
// The body is undefined when the browser does not support streaming response bodies (Firefox),
287+
// and null in certain error cases, i.e. when the request is blocked because of CORS settings.
288+
if !b.IsUndefined() && !b.IsNull() {
289+
body = &streamReader{stream: b.Call("getReader")}
290+
} else {
291+
// Fall back to using ArrayBuffer
292+
// https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer
293+
body = &arrayReader{arrayPromise: result.Call("arrayBuffer")}
294+
}
295+
296+
code := result.Get("status").Int()
297+
respCh <- &http.Response{
298+
Status: fmt.Sprintf("%d %s", code, http.StatusText(code)),
299+
StatusCode: code,
300+
Header: header,
301+
ContentLength: contentLength,
302+
Body: body,
303+
Request: req,
304+
}
305+
306+
return nil
307+
})
308+
failure = js.FuncOf(func(this js.Value, args []js.Value) any {
309+
success.Release()
310+
failure.Release()
311+
312+
err := args[0]
313+
// The error is a JS Error type
314+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
315+
// We can use the toString() method to get a string representation of the error.
316+
errMsg := err.Call("toString").String()
317+
// Errors can optionally contain a cause.
318+
if cause := err.Get("cause"); !cause.IsUndefined() {
319+
// The exact type of the cause is not defined,
320+
// but if it's another error, we can call toString() on it too.
321+
if !cause.Get("toString").IsUndefined() {
322+
errMsg += ": " + cause.Call("toString").String()
323+
} else if cause.Type() == js.TypeString {
324+
errMsg += ": " + cause.String()
325+
}
326+
}
327+
errCh <- fmt.Errorf("net/http: fetch() failed: %s", errMsg)
328+
return nil
329+
})
330+
331+
fetchPromise.Call("then", success, failure)
332+
select {
333+
case <-req.Context().Done():
334+
if !ac.IsUndefined() {
335+
// Abort the Fetch request.
336+
ac.Call("abort")
337+
}
338+
return nil, req.Context().Err()
339+
case resp := <-respCh:
340+
return resp, nil
341+
case err := <-errCh:
342+
return nil, err
343+
}
344+
}

internal/wasm/run.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package wasm
55
import (
66
"bytes"
77
"io"
8+
"net/http"
89

910
"github.com/scaleway/scaleway-cli/v2/internal/core"
1011
"github.com/scaleway/scaleway-cli/v2/internal/namespaces"
@@ -47,6 +48,9 @@ func runCommand(buildInfo *core.BuildInfo, cfg *RunConfig, args []string, stdout
4748
DefaultOrganizationID: cfg.DefaultOrganizationID,
4849
APIUrl: cfg.APIUrl,
4950
},
51+
HTTPClient: &http.Client{
52+
Transport: &Transport{},
53+
},
5054
})
5155

5256
return exitCode

wasm/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"name": "@scaleway-internal/scaleway-cli-wasm",
3-
"version": "0.0.14",
2+
"name": "@scaleway/scaleway-cli-wasm",
3+
"version": "0.0.16",
44
"description": "",
55
"type": "module",
66
"main": "index.js",

0 commit comments

Comments
 (0)