Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New package sharedworker to support shared worker #8

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
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