github.com/0chain/gosdk@v1.17.11/wasmsdk/jsbridge/webworker.go (about) 1 //go:build js && wasm 2 // +build js,wasm 3 4 package jsbridge 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "strconv" 11 "sync" 12 "syscall/js" 13 14 "github.com/google/uuid" 15 "github.com/hack-pad/go-webworkers/worker" 16 "github.com/hack-pad/safejs" 17 ) 18 19 const ( 20 MsgTypeAuth = "auth" 21 MsgTypeAuthRsp = "auth_rsp" 22 MsgTypeUpload = "upload" 23 MsgTypeUpdateWallet = "update_wallet" 24 ) 25 26 type WasmWebWorker struct { 27 // Name specifies an identifying name for the DedicatedWorkerGlobalScope representing the scope of the worker, which is mainly useful for debugging purposes. 28 // If this is not specified, `Start` will create a UUIDv4 for it and populate back. 29 Name string 30 31 // Path is the path of the WASM to run as the Web Worker. 32 // This can be a relative path on the server, or an abosolute URL. 33 Path string 34 35 // Args holds command line arguments, including the WASM as Args[0]. 36 // If the Args field is empty or nil, Run uses {Path}. 37 Args []string 38 39 // Env specifies the environment of the process. 40 // Each entry is of the form "key=value". 41 // If Env is nil, the new Web Worker uses the current context's 42 // environment. 43 // If Env contains duplicate environment keys, only the last 44 // value in the slice for each duplicate key is used. 45 Env []string 46 worker *worker.Worker 47 48 // For subscribing to events 49 ctx context.Context 50 cancelContext context.CancelFunc 51 subscribers map[string]chan worker.MessageEvent 52 numberOfSubs int 53 subMutex sync.Mutex 54 55 //isTerminated bool 56 isTerminated bool 57 } 58 59 var ( 60 workers = make(map[string]*WasmWebWorker) 61 gZauthServer string 62 ) 63 64 func NewWasmWebWorker(blobberID, blobberURL, clientID, clientKey, peerPublicKey, publicKey, privateKey, mnemonic string, isSplit bool) (*WasmWebWorker, bool, error) { 65 created := false 66 _, ok := workers[blobberID] 67 if ok { 68 return workers[blobberID], created, nil 69 } 70 71 fmt.Println("New wasm web worker, zauth server:", gZauthServer) 72 w := &WasmWebWorker{ 73 Name: blobberURL, 74 Env: []string{"BLOBBER_URL=" + blobberURL, 75 "CLIENT_ID=" + clientID, 76 "CLIENT_KEY=" + clientKey, 77 "PEER_PUBLIC_KEY=" + peerPublicKey, 78 "PRIVATE_KEY=" + privateKey, 79 "MODE=worker", 80 "PUBLIC_KEY=" + publicKey, 81 "IS_SPLIT=" + strconv.FormatBool(isSplit), 82 "MNEMONIC=" + mnemonic, 83 "ZAUTH_SERVER=" + gZauthServer}, 84 Path: "zcn.wasm", 85 subscribers: make(map[string]chan worker.MessageEvent), 86 } 87 88 if err := w.Start(); err != nil { 89 return nil, created, err 90 } 91 workers[blobberID] = w 92 created = true 93 return w, created, nil 94 } 95 96 func GetWorker(blobberID string) *WasmWebWorker { 97 return workers[blobberID] 98 } 99 100 func RemoveWorker(blobberID string) { 101 worker, ok := workers[blobberID] 102 if ok { 103 worker.subMutex.Lock() 104 if worker.numberOfSubs == 0 { 105 worker.Terminate() 106 delete(workers, blobberID) 107 worker.isTerminated = true 108 } 109 worker.subMutex.Unlock() 110 } 111 } 112 113 // pass a buffered channel to subscribe to events so that the caller is not blocked 114 func (ww *WasmWebWorker) SubscribeToEvents(remotePath string, ch chan worker.MessageEvent) error { 115 if ch == nil { 116 return errors.New("channel is nil") 117 } 118 ww.subMutex.Lock() 119 if ww.isTerminated { 120 ww.subMutex.Unlock() 121 return errors.New("worker is terminated") 122 } 123 ww.subscribers[remotePath] = ch 124 ww.numberOfSubs++ 125 //start the worker listener if there are subscribers 126 if ww.numberOfSubs == 1 { 127 ctx, cancel := context.WithCancel(context.Background()) 128 ww.ctx = ctx 129 ww.cancelContext = cancel 130 eventChan, err := ww.Listen(ctx) 131 if err != nil { 132 ww.subMutex.Unlock() 133 return err 134 } 135 go ww.ListenForEvents(eventChan) 136 } 137 ww.subMutex.Unlock() 138 return nil 139 } 140 141 func (ww *WasmWebWorker) UnsubscribeToEvents(remotePath string) { 142 ww.subMutex.Lock() 143 ch, ok := ww.subscribers[remotePath] 144 if ok { 145 close(ch) 146 delete(ww.subscribers, remotePath) 147 ww.numberOfSubs-- 148 //stop the worker listener if there are no subscribers 149 if ww.numberOfSubs == 0 { 150 ww.cancelContext() 151 } 152 } 153 ww.subMutex.Unlock() 154 } 155 156 func (ww *WasmWebWorker) ListenForEvents(eventChan <-chan worker.MessageEvent) { 157 for { 158 select { 159 case <-ww.ctx.Done(): 160 return 161 case event, ok := <-eventChan: 162 if !ok { 163 return 164 } 165 //get remote path from the event 166 data, err := event.Data() 167 // if above throws an error, pass it to all the subscribers 168 if err != nil { 169 ww.removeAllSubscribers() 170 return 171 } 172 remotePathObject, err := data.Get("remotePath") 173 if err != nil { 174 ww.removeAllSubscribers() 175 return 176 } 177 remotePath, _ := remotePathObject.String() 178 if remotePath == "" { 179 ww.removeAllSubscribers() 180 return 181 } 182 ww.subMutex.Lock() 183 ch, ok := ww.subscribers[remotePath] 184 if ok { 185 ch <- event 186 } 187 ww.subMutex.Unlock() 188 } 189 } 190 } 191 192 func (ww *WasmWebWorker) removeAllSubscribers() { 193 ww.subMutex.Lock() 194 for path, ch := range ww.subscribers { 195 close(ch) 196 delete(ww.subscribers, path) 197 ww.numberOfSubs-- 198 } 199 ww.cancelContext() 200 ww.subMutex.Unlock() 201 } 202 203 func (ww *WasmWebWorker) Start() error { 204 workerJS, err := buildWorkerJS(ww.Args, ww.Env, ww.Path) 205 if err != nil { 206 return err 207 } 208 209 if ww.Name == "" { 210 ww.Name = uuid.New().String() 211 } 212 213 wk, err := worker.NewFromScript(workerJS, worker.Options{Name: ww.Name}) 214 if err != nil { 215 return err 216 } 217 218 ww.worker = wk 219 220 return nil 221 } 222 223 // PostMessage sends data in a message to the worker, optionally transferring ownership of all items in transfers. 224 func (ww *WasmWebWorker) PostMessage(data safejs.Value, transfers []safejs.Value) error { 225 return ww.worker.PostMessage(data, transfers) 226 } 227 228 // Terminate immediately terminates the Worker. 229 func (ww *WasmWebWorker) Terminate() { 230 ww.worker.Terminate() 231 } 232 233 // Listen sends message events on a channel for events fired by self.postMessage() calls inside the Worker's global scope. 234 // Stops the listener and closes the channel when ctx is canceled. 235 func (ww *WasmWebWorker) Listen(ctx context.Context) (<-chan worker.MessageEvent, error) { 236 return ww.worker.Listen(ctx) 237 } 238 239 func SetZauthServer(zauthServer string) { 240 gZauthServer = zauthServer 241 } 242 243 type PostWorker interface { 244 PostMessage(data safejs.Value, transferables []safejs.Value) error 245 } 246 247 func PostMessage(w PostWorker, msgType string, data map[string]string) error { 248 msgTypeUint8Array := js.Global().Get("Uint8Array").New(len(msgType)) 249 js.CopyBytesToJS(msgTypeUint8Array, []byte(msgType)) 250 251 obj := js.Global().Get("Object").New() 252 obj.Set("msgType", msgTypeUint8Array) 253 254 for k, v := range data { 255 if k == "msgType" { 256 return errors.New("msgType is key word reserved") 257 } 258 259 dataUint8Array := js.Global().Get("Uint8Array").New(len(v)) 260 js.CopyBytesToJS(dataUint8Array, []byte(v)) 261 obj.Set(k, dataUint8Array) 262 } 263 264 return w.PostMessage(safejs.Safe(obj), nil) 265 } 266 267 func GetMsgType(event worker.MessageEvent) (string, *safejs.Value, error) { 268 data, err := event.Data() 269 if err != nil { 270 return "", nil, err 271 } 272 273 mt, err := data.Get("msgType") 274 if err != nil { 275 return "", nil, err 276 } 277 msgTypeLen, err := mt.Length() 278 if err != nil { 279 return "", nil, err 280 } 281 282 mstType := make([]byte, msgTypeLen) 283 safejs.CopyBytesToGo(mstType, mt) 284 285 return string(mstType), &data, nil 286 } 287 288 func SetMsgType(data *js.Value, msgType string) { 289 msgTypeUint8Array := js.Global().Get("Uint8Array").New(len(msgType)) 290 js.CopyBytesToJS(msgTypeUint8Array, []byte(msgType)) 291 data.Set("msgType", msgTypeUint8Array) 292 } 293 294 func ParseEventDataField(data *safejs.Value, field string) (string, error) { 295 fieldUint8Array, err := data.Get(field) 296 if err != nil { 297 return "", err 298 } 299 fieldLen, err := fieldUint8Array.Length() 300 if err != nil { 301 return "", err 302 } 303 304 fieldData := make([]byte, fieldLen) 305 safejs.CopyBytesToGo(fieldData, fieldUint8Array) 306 307 return string(fieldData), nil 308 } 309 310 func PostMessageToAllWorkers(msgType string, data map[string]string) error { 311 for id, worker := range workers { 312 fmt.Println("post message to worker", id) 313 err := PostMessage(worker, msgType, data) 314 if err != nil { 315 return fmt.Errorf("failed to post message to worker: %s, err: %v", id, err) 316 } 317 } 318 319 return nil 320 }