Skip to content
Open
57 changes: 57 additions & 0 deletions sharedworker/self.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//go:build js && wasm

package sharedworker

import (
"context"

"github.com/hack-pad/go-webworkers/types"

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

// GlobalSelf represents the global scope, named "self", in the context of using SharedWorkers.
// Supports receiving connection via Listen(), where each of the ConnectEvent has Ports() whose
// first element represents the MessagePort connected with the channel with its parent,
// which in turns support receiving message via its Listen() and PostMessage().
type GlobalSelf struct {
self *types.SharedWorkerGlobalScope
}

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

// Listen sends message events representing the connect event on a channel for events fired
// by connection calls to this worker from within the parent scope.
// Users are expected to call the Ports() on the MessageEvent, and take the 1st one as the target MessagePort.
// Stops the listener and closes the channel when ctx is canceled.
func (s *GlobalSelf) Listen(ctx context.Context) (<-chan types.MessageEventConnect, error) {
return s.self.Listen(ctx)
}

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

// Name returns the name that the Worker was (optionally) given when it was created.
func (s *GlobalSelf) Name() (string, error) {
return s.self.Name()
}

// Location returns the WorkerLocation in the form of url.URL for this worker.
func (s *GlobalSelf) Location() (*types.WorkerLocation, error) {
return s.self.Location()
}
39 changes: 39 additions & 0 deletions sharedworker/self_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//go:build js && wasm

package sharedworker

import (
"testing"
)

func TestSelfName(t *testing.T) {
t.Skip("This test case only runs inside a worker")
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)
}
}

func TestSelfLocation(t *testing.T) {
t.Skip("This test case only runs inside a worker")
t.Parallel()
self, err := Self()
if err != nil {
t.Fatal(err)
}
loc, err := self.Location()
if err != nil {
t.Fatal(err)
}
if loc.String() == "" {
t.Errorf("Expected %q, got %q", loc.String(), "")
}
}
100 changes: 100 additions & 0 deletions sharedworker/shared_worker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//go:build js && wasm

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

import (
"context"

"github.com/hack-pad/go-webworkers/types"

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

var (
jsURL = safejs.MustGetGlobal("URL")
jsBlob = safejs.MustGetGlobal("Blob")
)

// SharedWorker is a Shared Web Worker, which represents a background task created via a script.
// Use Listen() and PostMessage() to communicate with the worker.
type SharedWorker struct {
url string
name string
worker safejs.Value
msgport *types.MessagePort
}

// New starts a worker with the given script's URL and name
func New(url, name string) (*SharedWorker, error) {
worker, err := safejs.MustGetGlobal("SharedWorker").New(url, name)
if err != nil {
return nil, err
}
port, err := worker.Get("port")
if err != nil {
return nil, err
}
msgport, err := types.WrapMessagePort(port)
if err != nil {
return nil, err
}
return &SharedWorker{
url: url,
name: name,
msgport: msgport,
worker: worker,
}, nil
}

// NewFromScript is like New, but starts the worker with the given script (in JavaScript)
func NewFromScript(jsScript, name string) (*SharedWorker, 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, name)
}

// URL returns the script URL of the worker
func (w *SharedWorker) URL() string {
return w.url
}

// Name returns the name of the worker
func (w *SharedWorker) Name() string {
return w.name
}

// 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 *SharedWorker) PostMessage(data safejs.Value, transfers []safejs.Value) error {
return w.msgport.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 *SharedWorker) Listen(ctx context.Context) (<-chan types.MessageEventMessage, error) {
return w.msgport.Listen(ctx)
}

// Close closes the message port of this worker.
func (w *SharedWorker) Close() error {
return w.msgport.Close()
}
182 changes: 182 additions & 0 deletions sharedworker/shared_worker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//go:build js && wasm

package sharedworker

import (
"context"
"fmt"
"testing"

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

var (
jsJSON = safejs.MustGetGlobal("JSON")
jsUint8Array = safejs.MustGetGlobal("Uint8Array")
)

func makeBlobURL(t *testing.T, contents []byte, contentType string) string {
t.Helper()
jsContents, err := jsUint8Array.New(len(contents))
if err != nil {
t.Fatal(err)
}
_, err = safejs.CopyBytesToJS(jsContents, contents)
if err != nil {
t.Fatal(err)
}
blob, err := jsBlob.New([]any{jsContents}, map[string]any{
"type": contentType,
})
if err != nil {
t.Fatal(err)
}
url, err := jsURL.Call("createObjectURL", blob)
if err != nil {
t.Fatal(err)
}
urlString, err := url.String()
if err != nil {
t.Fatal(err)
}
return urlString
}

func TestNew(t *testing.T) {
t.Parallel()
const messageText = "Hello, world!"
blobURL := makeBlobURL(t, []byte(fmt.Sprintf(`"use strict";
onconnect = (e) => {
const port = e.ports[0];
port.postMessage(self.name + ": " + %q);
};
`, messageText)), "text/javascript")
workerName := "worker"
worker, err := New(blobURL, workerName)
if err != nil {
t.Fatal(err)
}

if worker.URL() != blobURL {
t.Fatalf("url expect=%q, got=%q", blobURL, worker.URL())
}

if worker.Name() != workerName {
t.Fatalf("url expect=%q, got=%q", workerName, worker.Name())
}

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
messages, err := worker.Listen(ctx)
if err != nil {
t.Fatal(err)
}
message := <-messages
data, err := message.Data()
if err != nil {
t.Fatal(err)
}
dataStr, err := data.String()
if err != nil {
t.Fatal(err)
}
if msg := workerName + ": " + messageText; dataStr != msg {
t.Errorf("Expected %q, got %q", msg, dataStr)
}
}

func TestNewFromScript(t *testing.T) {
t.Parallel()
const messageText = "Hello, world!"
script := fmt.Sprintf(`
"use strict";

onconnect = (e) => {
const port = e.ports[0];
port.postMessage(self.name + ": " + %q);
};
`, messageText)
workerName := "worker"
worker, err := NewFromScript(script, workerName)
if err != nil {
t.Fatal(err)
}
if worker.URL() == "" {
t.Fatal("url unexpect to be empty")
}

if worker.Name() != workerName {
t.Fatalf("url expect=%q, got=%q", workerName, worker.Name())
}

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
messages, err := worker.Listen(ctx)
if err != nil {
t.Fatal(err)
}
message := <-messages
data, err := message.Data()
if err != nil {
t.Fatal(err)
}
dataStr, err := data.String()
if err != nil {
t.Fatal(err)
}
if msg := workerName + ": " + messageText; dataStr != msg {
t.Errorf("Expected %q, got %q", msg, dataStr)
}
}

func TestWorkerPostMessage(t *testing.T) {
t.Parallel()
const pingPongScript = `
"use strict";

onconnect = (e) => {
const port = e.ports[0];
port.onmessage = (event) => {
port.postMessage(event.data + " pong!");
};
};
`
pingMessage, err := safejs.ValueOf("ping!")
if err != nil {
t.Fatal(err)
}

t.Run("listen before post", func(t *testing.T) {
t.Parallel()
worker, err := NewFromScript(pingPongScript, "")
if err != nil {
t.Fatal(err)
}

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
messages, err := worker.Listen(ctx)
if err != nil {
t.Fatal(err)
}

err = worker.PostMessage(pingMessage, nil)
if err != nil {
t.Fatal(err)
}

message := <-messages
data, err := message.Data()
if err != nil {
t.Fatal(err)
}
dataStr, err := data.String()
if err != nil {
t.Error(err)
}
expectedResponse := "ping! pong!"
if dataStr != expectedResponse {
t.Errorf("Expected response %q, got: %q", expectedResponse, dataStr)
}
})
}
Loading