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  }