Skip to content

Commit

Permalink
Add the initial worker API (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnStarich authored Mar 5, 2023
1 parent 4f9b361 commit c84fdb4
Show file tree
Hide file tree
Showing 10 changed files with 636 additions and 26 deletions.
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Env file defines useful environment variables for editors.
GOOS=js
GOARCH=wasm
Empty file added .gitignore
Empty file.
7 changes: 2 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ module github.com/hack-pad/go-webworkers
go 1.19

require (
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.0 // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/tools v0.5.0 // indirect
github.com/hack-pad/safejs v0.1.1
github.com/pkg/errors v0.9.1
)
11 changes: 4 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8=
github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
40 changes: 40 additions & 0 deletions worker/message_event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//go:build js && wasm

package worker

import (
"github.com/hack-pad/safejs"
"github.com/pkg/errors"
)

// MessageEvent is received from the channel returned by Listen().
// Represents a JS MessageEvent.
type MessageEvent struct {
data safejs.Value
err error
target *messagePort
}

// Data returns this event's data or a parse error
func (e MessageEvent) Data() (safejs.Value, error) {
return e.data, errors.Wrapf(e.err, "failed to parse MessageEvent %+v", e.data)
}

func parseMessageEvent(v safejs.Value) MessageEvent {
value, err := v.Get("target")
if err != nil {
return MessageEvent{err: err}
}
target, err := wrapMessagePort(value)
if err != nil {
return MessageEvent{err: err}
}
data, err := v.Get("data")
if err != nil {
return MessageEvent{err: err}
}
return MessageEvent{
data: data,
target: target,
}
}
98 changes: 98 additions & 0 deletions worker/message_port.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//go:build js && wasm

package worker

import (
"context"
"fmt"

"github.com/hack-pad/safejs"
)

type messagePort struct {
jsMessagePort safejs.Value
}

func wrapMessagePort(v safejs.Value) (*messagePort, error) {
someMethod, err := v.Get("postMessage")
if err != nil {
return nil, err
}
if truthy, err := someMethod.Truthy(); err != nil || !truthy {
return nil, fmt.Errorf("invalid MessagePort value: postMessage is not a function")
}
return &messagePort{v}, nil
}

func (p *messagePort) PostMessage(data safejs.Value, transfers []safejs.Value) error {
args := append([]any{data}, toJSSlice(transfers))
_, err := p.jsMessagePort.Call("postMessage", args...)
return err
}

func toJSSlice[Type any](slice []Type) []any {
newSlice := make([]any, len(slice))
for i := range slice {
newSlice[i] = slice[i]
}
return newSlice
}

func (p *messagePort) Listen(ctx context.Context) (_ <-chan MessageEvent, err error) {
ctx, cancel := context.WithCancel(ctx)
defer func() {
if err != nil {
cancel()
}
}()

events := make(chan MessageEvent)
messageHandler, err := nonBlocking(func(args []safejs.Value) {
events <- parseMessageEvent(args[0])
})
if err != nil {
return nil, err
}
errorHandler, err := nonBlocking(func(args []safejs.Value) {
events <- parseMessageEvent(args[0])
})
if err != nil {
return nil, err
}

go func() {
<-ctx.Done()
_, err := p.jsMessagePort.Call("removeEventListener", "message", messageHandler)
if err == nil {
messageHandler.Release()
}
_, err = p.jsMessagePort.Call("removeEventListener", "messageerror", errorHandler)
if err == nil {
errorHandler.Release()
}
close(events)
}()
_, err = p.jsMessagePort.Call("addEventListener", "message", messageHandler)
if err != nil {
return nil, err
}
_, err = p.jsMessagePort.Call("addEventListener", "messageerror", errorHandler)
if err != nil {
return nil, err
}
if start, err := p.jsMessagePort.Get("start"); err == nil {
if truthy, err := start.Truthy(); err == nil && truthy {
if _, err := p.jsMessagePort.Call("start"); err != nil {
return nil, err
}
}
}
return events, nil
}

func nonBlocking(fn func(args []safejs.Value)) (safejs.Func, error) {
return safejs.FuncOf(func(_ safejs.Value, args []safejs.Value) any {
go fn(args)
return nil
})
}
66 changes: 66 additions & 0 deletions worker/self.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//go:build js && wasm

package worker

import (
"context"

"github.com/hack-pad/safejs"
)

// GlobalSelf represents the global scope, named "self", in the context of using Workers.
// Supports sending and receiving messages via PostMessage() and Listen().
type GlobalSelf struct {
self safejs.Value
port *messagePort
}

// Self returns the global "self"
func Self() (*GlobalSelf, error) {
self, err := safejs.Global().Get("self")
if err != nil {
return nil, err
}
port, err := wrapMessagePort(self)
if err != nil {
return nil, err
}
return &GlobalSelf{
self: self,
port: port,
}, nil
}

// PostMessage sends data in a message to the main thread that spawned it,
// optionally transferring ownership of all items in transfers.
//
// The data may be any value handled by the "structured clone algorithm", which includes cyclical references.
//
// Transfers is an optional array of Transferable objects to transfer ownership of.
// If the ownership of an object is transferred, it becomes unusable in the context it was sent from and becomes available only to the worker it was sent to.
// Transferable objects are instances of classes like ArrayBuffer, MessagePort or ImageBitmap objects that can be transferred.
// null is not an acceptable value for transfer.
func (s *GlobalSelf) PostMessage(message safejs.Value, transfers []safejs.Value) error {
return s.port.PostMessage(message, transfers)
}

// Listen sends message events on a channel for events fired by worker.postMessage() calls inside the main thread's global scope.
// Stops the listener and closes the channel when ctx is canceled.
func (s *GlobalSelf) Listen(ctx context.Context) (<-chan MessageEvent, error) {
return s.port.Listen(ctx)
}

// Close discards any tasks queued in the global scope's event loop, effectively closing this particular scope.
func (s *GlobalSelf) Close() error {
_, err := s.self.Call("close")
return err
}

// Name returns the name that the Worker was (optionally) given when it was created.
func (s *GlobalSelf) Name() (string, error) {
name, err := s.self.Get("name")
if err != nil {
return "", err
}
return name.String()
}
35 changes: 35 additions & 0 deletions worker/self_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//go:build js && wasm

package worker

import (
"testing"

"github.com/hack-pad/safejs"
)

func TestSelf(t *testing.T) {
t.Parallel()
self, err := Self()
if err != nil {
t.Fatal(err)
}
if !self.self.Equal(safejs.MustGetGlobal("self")) {
t.Error("self is not equal to the global self")
}
}

func TestSelfName(t *testing.T) {
t.Parallel()
self, err := Self()
if err != nil {
t.Fatal(err)
}
name, err := self.Name()
if err != nil {
t.Fatal(err)
}
if name != "" {
t.Errorf("Expected %q, got %q", "", name)
}
}
101 changes: 93 additions & 8 deletions worker/worker.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,101 @@
//go:build js && wasm
// +build js,wasm

// Package worker provides a Web Workers driver for Go code compiled to WebAssembly.
package worker

import "errors"
import (
"context"

// Worker is a Web Worker, which represents a background task that can be created via script.
// Workers can send messages back to its creator.
type Worker struct{}
"github.com/hack-pad/safejs"
)

// NewWorker returns a new Worker
func NewWorker() (*Worker, error) {
return nil, errors.New("not implemented")
var (
jsWorker = safejs.MustGetGlobal("Worker")
jsURL = safejs.MustGetGlobal("URL")
jsBlob = safejs.MustGetGlobal("Blob")
)

// Worker is a Web Worker, which represents a background task created via a script.
// Use Listen() and PostMessage() to communicate with the worker.
type Worker struct {
worker safejs.Value
port *messagePort
}

// Options contains optional configuration for new Workers
type Options struct {
// Name specifies an identifying name for the DedicatedWorkerGlobalScope representing the scope of the worker, which is mainly useful for debugging purposes.
Name string
}

func (w Options) toJSValue() (safejs.Value, error) {
options := make(map[string]any)
if w.Name != "" {
options["name"] = w.Name
}
return safejs.ValueOf(options)
}

// New starts a worker with the given script's URL and returns it
func New(url string, options Options) (*Worker, error) {
jsOptions, err := options.toJSValue()
if err != nil {
return nil, err
}
worker, err := jsWorker.New(url, jsOptions)
if err != nil {
return nil, err
}
port, err := wrapMessagePort(worker)
if err != nil {
return nil, err
}
return &Worker{
port: port,
worker: worker,
}, nil
}

// NewFromScript is like New, but starts the worker with the given script (in JavaScript)
func NewFromScript(jsScript string, options Options) (*Worker, error) {
blob, err := jsBlob.New([]any{jsScript}, map[string]any{
"type": "text/javascript",
})
if err != nil {
return nil, err
}
objectURL, err := jsURL.Call("createObjectURL", blob)
if err != nil {
return nil, err
}
objectURLStr, err := objectURL.String()
if err != nil {
return nil, err
}
return New(objectURLStr, options)
}

// Terminate immediately terminates the Worker.
// This does not offer the worker an opportunity to finish its operations; it is stopped at once.
func (w *Worker) Terminate() error {
_, err := w.worker.Call("terminate")
return err
}

// PostMessage sends data in a message to the worker, optionally transferring ownership of all items in transfers.
//
// The data may be any value handled by the "structured clone algorithm", which includes cyclical references.
//
// Transfers is an optional array of Transferable objects to transfer ownership of.
// If the ownership of an object is transferred, it becomes unusable in the context it was sent from and becomes available only to the worker it was sent to.
// Transferable objects are instances of classes like ArrayBuffer, MessagePort or ImageBitmap objects that can be transferred.
// null is not an acceptable value for transfer.
func (w *Worker) PostMessage(data safejs.Value, transfers []safejs.Value) error {
return w.port.PostMessage(data, transfers)
}

// Listen sends message events on a channel for events fired by self.postMessage() calls inside the Worker's global scope.
// Stops the listener and closes the channel when ctx is canceled.
func (w *Worker) Listen(ctx context.Context) (<-chan MessageEvent, error) {
return w.port.Listen(ctx)
}
Loading

0 comments on commit c84fdb4

Please sign in to comment.