1 //go:build js && wasm
2 3 package safejs
4 5 import (
6 "fmt"
7 "syscall/js"
8 9 "github.com/hack-pad/safejs/internal/catch"
10 )
11 12 // Value is a safer version of js.Value. Any panic returns an error instead.
13 type Value struct {
14 jsValue js.Value
15 }
16 17 // Safe wraps a js.Value into a safejs.Value.
18 // Ideal for use in libraries where exposed types must match the standard library.
19 func Safe(value js.Value) Value {
20 return Value{
21 jsValue: value,
22 }
23 }
24 25 // Unsafe unwraps a safejs.Value back into its js.Value.
26 // Ideal for use in libraries where exposed types must match the standard library.
27 func Unsafe(value Value) js.Value {
28 return value.jsValue
29 }
30 31 // Null returns the JavaScript value of "null".
32 func Null() Value {
33 return Safe(js.Null())
34 }
35 36 // Undefined returns the JavaScript value of "undefined".
37 func Undefined() Value {
38 return Safe(js.Undefined())
39 }
40 41 func toJSValue(jsValue any) any {
42 switch value := jsValue.(type) {
43 case Value:
44 return value.jsValue
45 case Func:
46 return value.fn
47 case Error:
48 return value.err
49 case map[string]any:
50 newValue := make(map[string]any)
51 for mapKey, mapValue := range value {
52 newValue[mapKey] = toJSValue(mapValue)
53 }
54 return newValue
55 case []any:
56 newValue := make([]any, len(value))
57 for i, arg := range value {
58 newValue[i] = toJSValue(arg)
59 }
60 return newValue
61 default:
62 return jsValue
63 }
64 }
65 66 func toJSValues(args []any) []any {
67 return toJSValue(args).([]any)
68 }
69 70 func toValues(args []js.Value) []Value {
71 newArgs := make([]Value, len(args))
72 for i, arg := range args {
73 newArgs[i] = Safe(arg)
74 }
75 return newArgs
76 }
77 78 // ValueOf returns value as a JavaScript value. See [js.ValueOf] for details.
79 func ValueOf(value any) (Value, error) {
80 jsValue, err := catch.Try(func() js.Value {
81 return js.ValueOf(value)
82 })
83 return Safe(jsValue), err
84 }
85 86 // Bool attempts to convert this value into a boolean, otherwise returns an error.
87 func (v Value) Bool() (bool, error) {
88 return catch.Try(v.jsValue.Bool)
89 }
90 91 // Call does a JavaScript call to the method m of value v with the given arguments.
92 // The arguments are mapped to JavaScript values according to the ValueOf function.
93 // Returns an error if v has no method m, the arguments failed to map to JavaScript values, or the function throws an error.
94 func (v Value) Call(m string, args ...any) (Value, error) {
95 args = toJSValues(args)
96 return catch.Try(func() Value {
97 return Safe(v.jsValue.Call(m, args...))
98 })
99 }
100 101 // Delete deletes the JavaScript property p of value v. Returns an error if v is not a JavaScript object.
102 func (v Value) Delete(p string) error {
103 return catch.TrySideEffect(func() {
104 v.jsValue.Delete(p)
105 })
106 }
107 108 // Equal reports whether v and w are equal according to JavaScript's === operator.
109 func (v Value) Equal(w Value) bool {
110 return v.jsValue.Equal(w.jsValue)
111 }
112 113 // Float returns the value v as a float64. Returns an error if v is not a JavaScript number.
114 func (v Value) Float() (float64, error) {
115 return catch.Try(v.jsValue.Float)
116 }
117 118 // Get returns the JavaScript property p of value v. Returns an error if v is not a JavaScript object.
119 func (v Value) Get(p string) (Value, error) {
120 return catch.Try(func() Value {
121 return Safe(v.jsValue.Get(p))
122 })
123 }
124 125 // Index returns JavaScript index i of value v. Returns an error if v is not a JavaScript object.
126 func (v Value) Index(i int) (Value, error) {
127 return catch.Try(func() Value {
128 return Safe(v.jsValue.Index(i))
129 })
130 }
131 132 // InstanceOf reports whether v is an instance of type t according to JavaScript's instanceof operator.
133 // Returns an error if v is not a constructable type.
134 func (v Value) InstanceOf(t Value) (bool, error) {
135 // Type failures in JS throw "TypeError: Right-hand side of 'instanceof' is not an object"
136 // so catch those cases here.
137 //
138 // A valid type is a function with a field "prototype" which is an object.
139 if t.Type() != TypeFunction {
140 return false, fmt.Errorf("invalid type for instanceof: %v", t.Type())
141 }
142 prototype, err := t.Get("prototype")
143 if err != nil {
144 return false, fmt.Errorf("invalid constructor type for instanceof: %v", err)
145 } else if prototype.Type() != TypeObject {
146 return false, fmt.Errorf("invalid constructor type for instanceof: %v", prototype.Type())
147 }
148 return catch.Try(func() bool {
149 return v.jsValue.InstanceOf(t.jsValue)
150 })
151 }
152 153 // Int returns the value v truncated to an int. Returns an error if v is not a JavaScript number.
154 func (v Value) Int() (int, error) {
155 return catch.Try(v.jsValue.Int)
156 }
157 158 // Invoke does a JavaScript call of the value v with the given arguments.
159 // The arguments get mapped to JavaScript values according to the ValueOf function.
160 // Returns an error if v is not a JavaScript function, the arguments failed to map to JavaScript values, or the function throws an error.
161 func (v Value) Invoke(args ...any) (Value, error) {
162 args = toJSValues(args)
163 return catch.Try(func() Value {
164 return Safe(v.jsValue.Invoke(args...))
165 })
166 }
167 168 // IsNaN reports whether v is the JavaScript value "NaN".
169 func (v Value) IsNaN() bool {
170 return v.jsValue.IsNaN()
171 }
172 173 // IsNull reports whether v is the JavaScript value "null".
174 func (v Value) IsNull() bool {
175 return v.jsValue.IsNull()
176 }
177 178 // IsUndefined reports whether v is the JavaScript value "undefined".
179 func (v Value) IsUndefined() bool {
180 return v.jsValue.IsUndefined()
181 }
182 183 // Length returns the JavaScript property "length" of v.
184 // Returns an error if v is not a JavaScript object.
185 func (v Value) Length() (int, error) {
186 return catch.Try(v.jsValue.Length)
187 }
188 189 // New uses JavaScript's "new" operator with value v as constructor and the given arguments.
190 // The arguments get mapped to JavaScript values according to the ValueOf function.
191 // Returns an error if v is not a JavaScript function, the arguments failed to map to JavaScript values, or the constructor throws an error.
192 func (v Value) New(args ...any) (Value, error) {
193 args = toJSValues(args)
194 return catch.Try(func() Value {
195 return Safe(v.jsValue.New(args...))
196 })
197 }
198 199 // Set sets the JavaScript property p of value v to ValueOf(x).
200 // Returns an error if v is not a JavaScript object or x failed to map to a JavaScript value.
201 func (v Value) Set(p string, x any) error {
202 x = toJSValue(x)
203 return catch.TrySideEffect(func() {
204 v.jsValue.Set(p, x)
205 })
206 }
207 208 // SetIndex sets the JavaScript index i of value v to ValueOf(x).
209 // Returns an error if if v is not a JavaScript object or x failed to map to a JavaScript value.
210 func (v Value) SetIndex(i int, x any) error {
211 x = toJSValue(x)
212 return catch.TrySideEffect(func() {
213 v.jsValue.SetIndex(i, x)
214 })
215 }
216 217 // String returns the value v as a string.
218 // Unlike the other getters, String() does not return an error if v's Type is not TypeString.
219 // Instead, it returns a string of the form "<T>" or "<T: V>" where T is v's type and V is a string representation of v's value.
220 //
221 // Returns an error if v is an invalid type or the string failed to load from the JavaScript runtime.
222 //
223 // NOTE: [syscall/js] takes the stance that String is a special case due to Go's String method convention and avoids panicking.
224 // However, js.String() can still fail in other ways so an error is returned anyway.
225 func (v Value) String() (string, error) {
226 return catch.Try(v.jsValue.String)
227 }
228 229 // Truthy returns the JavaScript "truthiness" of the value v.
230 // In JavaScript, false, 0, "", null, undefined, and NaN are "falsy", and everything else is "truthy".
231 // See https://developer.mozilla.org/en-US/docs/Glossary/Truthy.
232 //
233 // Returns an error if v's type is invalid or if the value fails to load from the JavaScript runtime.
234 func (v Value) Truthy() (bool, error) {
235 return catch.Try(v.jsValue.Truthy)
236 }
237 238 // Type returns the JavaScript type of the value v.
239 // It is similar to JavaScript's typeof operator, except it returns TypeNull instead of TypeObject for null.
240 func (v Value) Type() Type {
241 return Type(v.jsValue.Type())
242 }
243