Skip to content

Commit c84fdb4

Browse files
authored
Add the initial worker API (#3)
1 parent 4f9b361 commit c84fdb4

10 files changed

Lines changed: 636 additions & 26 deletions

File tree

.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Env file defines useful environment variables for editors.
2+
GOOS=js
3+
GOARCH=wasm

.gitignore

Whitespace-only changes.

go.mod

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ module github.com/hack-pad/go-webworkers
33
go 1.19
44

55
require (
6-
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
7-
github.com/hack-pad/safejs v0.1.0 // indirect
8-
golang.org/x/mod v0.7.0 // indirect
9-
golang.org/x/sys v0.4.0 // indirect
10-
golang.org/x/tools v0.5.0 // indirect
6+
github.com/hack-pad/safejs v0.1.1
7+
github.com/pkg/errors v0.9.1
118
)

go.sum

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
2-
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
3-
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
4-
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
1+
github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8=
2+
github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
3+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
4+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
55
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
6-
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
76
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
8-
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
97
golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=
10-
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=

worker/message_event.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//go:build js && wasm
2+
3+
package worker
4+
5+
import (
6+
"github.com/hack-pad/safejs"
7+
"github.com/pkg/errors"
8+
)
9+
10+
// MessageEvent is received from the channel returned by Listen().
11+
// Represents a JS MessageEvent.
12+
type MessageEvent struct {
13+
data safejs.Value
14+
err error
15+
target *messagePort
16+
}
17+
18+
// Data returns this event's data or a parse error
19+
func (e MessageEvent) Data() (safejs.Value, error) {
20+
return e.data, errors.Wrapf(e.err, "failed to parse MessageEvent %+v", e.data)
21+
}
22+
23+
func parseMessageEvent(v safejs.Value) MessageEvent {
24+
value, err := v.Get("target")
25+
if err != nil {
26+
return MessageEvent{err: err}
27+
}
28+
target, err := wrapMessagePort(value)
29+
if err != nil {
30+
return MessageEvent{err: err}
31+
}
32+
data, err := v.Get("data")
33+
if err != nil {
34+
return MessageEvent{err: err}
35+
}
36+
return MessageEvent{
37+
data: data,
38+
target: target,
39+
}
40+
}

worker/message_port.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//go:build js && wasm
2+
3+
package worker
4+
5+
import (
6+
"context"
7+
"fmt"
8+
9+
"github.com/hack-pad/safejs"
10+
)
11+
12+
type messagePort struct {
13+
jsMessagePort safejs.Value
14+
}
15+
16+
func wrapMessagePort(v safejs.Value) (*messagePort, error) {
17+
someMethod, err := v.Get("postMessage")
18+
if err != nil {
19+
return nil, err
20+
}
21+
if truthy, err := someMethod.Truthy(); err != nil || !truthy {
22+
return nil, fmt.Errorf("invalid MessagePort value: postMessage is not a function")
23+
}
24+
return &messagePort{v}, nil
25+
}
26+
27+
func (p *messagePort) PostMessage(data safejs.Value, transfers []safejs.Value) error {
28+
args := append([]any{data}, toJSSlice(transfers))
29+
_, err := p.jsMessagePort.Call("postMessage", args...)
30+
return err
31+
}
32+
33+
func toJSSlice[Type any](slice []Type) []any {
34+
newSlice := make([]any, len(slice))
35+
for i := range slice {
36+
newSlice[i] = slice[i]
37+
}
38+
return newSlice
39+
}
40+
41+
func (p *messagePort) Listen(ctx context.Context) (_ <-chan MessageEvent, err error) {
42+
ctx, cancel := context.WithCancel(ctx)
43+
defer func() {
44+
if err != nil {
45+
cancel()
46+
}
47+
}()
48+
49+
events := make(chan MessageEvent)
50+
messageHandler, err := nonBlocking(func(args []safejs.Value) {
51+
events <- parseMessageEvent(args[0])
52+
})
53+
if err != nil {
54+
return nil, err
55+
}
56+
errorHandler, err := nonBlocking(func(args []safejs.Value) {
57+
events <- parseMessageEvent(args[0])
58+
})
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
go func() {
64+
<-ctx.Done()
65+
_, err := p.jsMessagePort.Call("removeEventListener", "message", messageHandler)
66+
if err == nil {
67+
messageHandler.Release()
68+
}
69+
_, err = p.jsMessagePort.Call("removeEventListener", "messageerror", errorHandler)
70+
if err == nil {
71+
errorHandler.Release()
72+
}
73+
close(events)
74+
}()
75+
_, err = p.jsMessagePort.Call("addEventListener", "message", messageHandler)
76+
if err != nil {
77+
return nil, err
78+
}
79+
_, err = p.jsMessagePort.Call("addEventListener", "messageerror", errorHandler)
80+
if err != nil {
81+
return nil, err
82+
}
83+
if start, err := p.jsMessagePort.Get("start"); err == nil {
84+
if truthy, err := start.Truthy(); err == nil && truthy {
85+
if _, err := p.jsMessagePort.Call("start"); err != nil {
86+
return nil, err
87+
}
88+
}
89+
}
90+
return events, nil
91+
}
92+
93+
func nonBlocking(fn func(args []safejs.Value)) (safejs.Func, error) {
94+
return safejs.FuncOf(func(_ safejs.Value, args []safejs.Value) any {
95+
go fn(args)
96+
return nil
97+
})
98+
}

worker/self.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//go:build js && wasm
2+
3+
package worker
4+
5+
import (
6+
"context"
7+
8+
"github.com/hack-pad/safejs"
9+
)
10+
11+
// GlobalSelf represents the global scope, named "self", in the context of using Workers.
12+
// Supports sending and receiving messages via PostMessage() and Listen().
13+
type GlobalSelf struct {
14+
self safejs.Value
15+
port *messagePort
16+
}
17+
18+
// Self returns the global "self"
19+
func Self() (*GlobalSelf, error) {
20+
self, err := safejs.Global().Get("self")
21+
if err != nil {
22+
return nil, err
23+
}
24+
port, err := wrapMessagePort(self)
25+
if err != nil {
26+
return nil, err
27+
}
28+
return &GlobalSelf{
29+
self: self,
30+
port: port,
31+
}, nil
32+
}
33+
34+
// PostMessage sends data in a message to the main thread that spawned it,
35+
// optionally transferring ownership of all items in transfers.
36+
//
37+
// The data may be any value handled by the "structured clone algorithm", which includes cyclical references.
38+
//
39+
// Transfers is an optional array of Transferable objects to transfer ownership of.
40+
// 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.
41+
// Transferable objects are instances of classes like ArrayBuffer, MessagePort or ImageBitmap objects that can be transferred.
42+
// null is not an acceptable value for transfer.
43+
func (s *GlobalSelf) PostMessage(message safejs.Value, transfers []safejs.Value) error {
44+
return s.port.PostMessage(message, transfers)
45+
}
46+
47+
// Listen sends message events on a channel for events fired by worker.postMessage() calls inside the main thread's global scope.
48+
// Stops the listener and closes the channel when ctx is canceled.
49+
func (s *GlobalSelf) Listen(ctx context.Context) (<-chan MessageEvent, error) {
50+
return s.port.Listen(ctx)
51+
}
52+
53+
// Close discards any tasks queued in the global scope's event loop, effectively closing this particular scope.
54+
func (s *GlobalSelf) Close() error {
55+
_, err := s.self.Call("close")
56+
return err
57+
}
58+
59+
// Name returns the name that the Worker was (optionally) given when it was created.
60+
func (s *GlobalSelf) Name() (string, error) {
61+
name, err := s.self.Get("name")
62+
if err != nil {
63+
return "", err
64+
}
65+
return name.String()
66+
}

worker/self_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//go:build js && wasm
2+
3+
package worker
4+
5+
import (
6+
"testing"
7+
8+
"github.com/hack-pad/safejs"
9+
)
10+
11+
func TestSelf(t *testing.T) {
12+
t.Parallel()
13+
self, err := Self()
14+
if err != nil {
15+
t.Fatal(err)
16+
}
17+
if !self.self.Equal(safejs.MustGetGlobal("self")) {
18+
t.Error("self is not equal to the global self")
19+
}
20+
}
21+
22+
func TestSelfName(t *testing.T) {
23+
t.Parallel()
24+
self, err := Self()
25+
if err != nil {
26+
t.Fatal(err)
27+
}
28+
name, err := self.Name()
29+
if err != nil {
30+
t.Fatal(err)
31+
}
32+
if name != "" {
33+
t.Errorf("Expected %q, got %q", "", name)
34+
}
35+
}

worker/worker.go

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,101 @@
11
//go:build js && wasm
2-
// +build js,wasm
32

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

7-
import "errors"
6+
import (
7+
"context"
88

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

13-
// NewWorker returns a new Worker
14-
func NewWorker() (*Worker, error) {
15-
return nil, errors.New("not implemented")
12+
var (
13+
jsWorker = safejs.MustGetGlobal("Worker")
14+
jsURL = safejs.MustGetGlobal("URL")
15+
jsBlob = safejs.MustGetGlobal("Blob")
16+
)
17+
18+
// Worker is a Web Worker, which represents a background task created via a script.
19+
// Use Listen() and PostMessage() to communicate with the worker.
20+
type Worker struct {
21+
worker safejs.Value
22+
port *messagePort
23+
}
24+
25+
// Options contains optional configuration for new Workers
26+
type Options struct {
27+
// Name specifies an identifying name for the DedicatedWorkerGlobalScope representing the scope of the worker, which is mainly useful for debugging purposes.
28+
Name string
29+
}
30+
31+
func (w Options) toJSValue() (safejs.Value, error) {
32+
options := make(map[string]any)
33+
if w.Name != "" {
34+
options["name"] = w.Name
35+
}
36+
return safejs.ValueOf(options)
37+
}
38+
39+
// New starts a worker with the given script's URL and returns it
40+
func New(url string, options Options) (*Worker, error) {
41+
jsOptions, err := options.toJSValue()
42+
if err != nil {
43+
return nil, err
44+
}
45+
worker, err := jsWorker.New(url, jsOptions)
46+
if err != nil {
47+
return nil, err
48+
}
49+
port, err := wrapMessagePort(worker)
50+
if err != nil {
51+
return nil, err
52+
}
53+
return &Worker{
54+
port: port,
55+
worker: worker,
56+
}, nil
57+
}
58+
59+
// NewFromScript is like New, but starts the worker with the given script (in JavaScript)
60+
func NewFromScript(jsScript string, options Options) (*Worker, error) {
61+
blob, err := jsBlob.New([]any{jsScript}, map[string]any{
62+
"type": "text/javascript",
63+
})
64+
if err != nil {
65+
return nil, err
66+
}
67+
objectURL, err := jsURL.Call("createObjectURL", blob)
68+
if err != nil {
69+
return nil, err
70+
}
71+
objectURLStr, err := objectURL.String()
72+
if err != nil {
73+
return nil, err
74+
}
75+
return New(objectURLStr, options)
76+
}
77+
78+
// Terminate immediately terminates the Worker.
79+
// This does not offer the worker an opportunity to finish its operations; it is stopped at once.
80+
func (w *Worker) Terminate() error {
81+
_, err := w.worker.Call("terminate")
82+
return err
83+
}
84+
85+
// PostMessage sends data in a message to the worker, optionally transferring ownership of all items in transfers.
86+
//
87+
// The data may be any value handled by the "structured clone algorithm", which includes cyclical references.
88+
//
89+
// Transfers is an optional array of Transferable objects to transfer ownership of.
90+
// 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.
91+
// Transferable objects are instances of classes like ArrayBuffer, MessagePort or ImageBitmap objects that can be transferred.
92+
// null is not an acceptable value for transfer.
93+
func (w *Worker) PostMessage(data safejs.Value, transfers []safejs.Value) error {
94+
return w.port.PostMessage(data, transfers)
95+
}
96+
97+
// Listen sends message events on a channel for events fired by self.postMessage() calls inside the Worker's global scope.
98+
// Stops the listener and closes the channel when ctx is canceled.
99+
func (w *Worker) Listen(ctx context.Context) (<-chan MessageEvent, error) {
100+
return w.port.Listen(ctx)
16101
}

0 commit comments

Comments
 (0)