Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ jobs:

- name: Make standard binary file
run: make unix

- name: Make it globally available
run: sudo cp ./emi /usr/local/bin/emi && sudo chmod +x /usr/local/bin/emi

- name: Build Playground
run: cd playground && npm run build
Expand All @@ -48,14 +51,18 @@ jobs:

- name: Javascript tests
run: cd ./examples/js && npm i -f && npx vitest run



- name: The the virtual ecosystem
run: cd ./examples/virtual-eco-system && make build


- name: Compile the web veresion (docs, and only after running tests because they create document)
run: cd ./examples/emi-web && npm i -f && npm run build

- name: Regenerate allegro
run: cd ./examples/allegro-sdk && make


- name: Add web version and playground into same
run: make compile-github
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,7 @@ collectiontest:


test_examples:
cd examples/fullstack && make
cd examples/fullstack && make

check_swift:
cd examples/allegro-sdk/swift && find . -type f -name "*.swift" -print0 | xargs -0 swiftc -typecheck
178 changes: 178 additions & 0 deletions emigo/WasmReactive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//go:build wasm

package emigo

import (
"net/url"
"sync"
"sync/atomic"
"syscall/js"
)

// WasmReactiveConn is the server-side view of one logical client connection.
// It is the websocket-conn analogue for the browser: there is no socket, just
// two channels bridged to JS callbacks.
//
// Read carries client -> server frames (fed by wasmWsSend)
// Write carries server -> client frames (drained into the onMessage callback)
// Done fires once when either side tears the connection down
//
// This mirrors the channel contract of the gorilla-backed reactive session, so
// the same developer factory can drive either transport.
type WasmReactiveConn struct {
Read chan []byte
Write chan []byte
Done chan bool
Query url.Values
}

// WasmReactiveHandler sets up one connection. It runs once per WebSocketWasm the
// browser opens. Wire your read/write loops against conn here; return an error
// to reject the connection (the error text is delivered as one final message).
type WasmReactiveHandler func(conn *WasmReactiveConn) error

// WasmReactor is the reactive counterpart of LiftWasmServer. Where LiftWasmServer
// turns a single JS call into one HTTP request/response, the reactor manages
// long-lived connections and exposes three JS entry points the WebSocketWasm
// class drives:
//
// wasmWsOpen(path, query, onMessage, onClose) -> connId | -1
// wasmWsSend(connId, data) -> bool
// wasmWsClose(connId) -> bool
//
// The reactive path deliberately bypasses net/http: gorilla's Upgrade needs to
// hijack a real net.Conn, which the in-browser httptest server has no notion of.
// Talking channels straight to JS sidesteps the handshake and framing entirely.
type WasmReactor struct {
mu sync.Mutex
handlers map[string]WasmReactiveHandler
conns map[int]*WasmReactiveConn
nextID int64
}

func NewWasmReactor() *WasmReactor {
return &WasmReactor{
handlers: map[string]WasmReactiveHandler{},
conns: map[int]*WasmReactiveConn{},
}
}

// Handle registers a reactive handler under a path. The browser selects it via
// the pathname of the URL passed to new WebSocketWasm(url).
func (r *WasmReactor) Handle(path string, h WasmReactiveHandler) {
r.mu.Lock()
r.handlers[path] = h
r.mu.Unlock()
}

// Lift exposes the JS entry points. Call it once, after registering handlers.
func (r *WasmReactor) Lift() {
js.Global().Set("wasmWsOpen", js.FuncOf(r.jsOpen))
js.Global().Set("wasmWsSend", js.FuncOf(r.jsSend))
js.Global().Set("wasmWsClose", js.FuncOf(r.jsClose))
}

func (r *WasmReactor) drop(id int) {
r.mu.Lock()
delete(r.conns, id)
r.mu.Unlock()
}

// jsOpen: wasmWsOpen(path, query, onMessage, onClose) -> connId | -1
func (r *WasmReactor) jsOpen(_ js.Value, a []js.Value) any {
path := a[0].String()
rawQuery := a[1].String()
onMessage := a[2] // function(dataString)
onClose := a[3] // function(reasonString)

r.mu.Lock()
h, ok := r.handlers[path]
r.mu.Unlock()
if !ok {
return -1
}

q, _ := url.ParseQuery(rawQuery)
conn := &WasmReactiveConn{
Read: make(chan []byte, 16),
Write: make(chan []byte, 16),
Done: make(chan bool, 1),
Query: q,
}
id := int(atomic.AddInt64(&r.nextID, 1))
r.mu.Lock()
r.conns[id] = conn
r.mu.Unlock()

// server -> client pump. Exactly one onClose fires: whichever of Write-close
// or Done lands first wins, then the goroutine returns.
go func() {
for {
select {
case msg, ok := <-conn.Write:
if !ok {
onClose.Invoke("server closed")
r.drop(id)
return
}
onMessage.Invoke(string(msg))
case <-conn.Done:
onClose.Invoke("closed")
r.drop(id)
return
}
}
}()

// Run the developer handler. On setup error, deliver the text and tear down.
go func() {
if err := h(conn); err != nil {
onMessage.Invoke("error: " + err.Error())
select {
case conn.Done <- true:
default:
}
}
}()

return id
}

// jsSend: wasmWsSend(connId, data) -> bool
func (r *WasmReactor) jsSend(_ js.Value, a []js.Value) any {
id := a[0].Int()
data := a[1].String()

r.mu.Lock()
conn := r.conns[id]
r.mu.Unlock()
if conn == nil {
return false
}
// Feed the frame on a goroutine: a synchronous JS->Go call must not block
// the event loop waiting on a busy handler.
go func() {
select {
case conn.Read <- []byte(data):
case <-conn.Done:
}
}()
return true
}

// jsClose: wasmWsClose(connId) -> bool
func (r *WasmReactor) jsClose(_ js.Value, a []js.Value) any {
id := a[0].Int()

r.mu.Lock()
conn := r.conns[id]
r.mu.Unlock()
if conn == nil {
return false
}
select {
case conn.Done <- true:
default:
}
return true
}
12 changes: 6 additions & 6 deletions examples/allegro-sdk/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ gen:

gen-js:
rm -rf javascript/gen/ && \
emi js --path definitions/offer/user-offer-information.emi.yml --output javascript/gen/offer/user-offer-information && \
emi js --path definitions/offer/offer-translations.emi.yml --output javascript/gen/offer/offer-translations && \
emi js --path definitions/offer/offer-management.emi.yml --output javascript/gen/offer/offer-management
emi js --tags no-sdk --path definitions/offer/user-offer-information.emi.yml --output javascript/gen/offer/user-offer-information && \
emi js --tags no-sdk --path definitions/offer/offer-translations.emi.yml --output javascript/gen/offer/offer-translations && \
emi js --tags no-sdk --path definitions/offer/offer-management.emi.yml --output javascript/gen/offer/offer-management

gen-ts:
rm -rf typescript/gen/ && \
emi js --path definitions/offer/user-offer-information.emi.yml --output typescript/gen/offer/user-offer-information --tags typescript && \
emi js --path definitions/offer/offer-translations.emi.yml --output typescript/gen/offer/offer-translations --tags typescript && \
emi js --path definitions/offer/offer-management.emi.yml --output typescript/gen/offer/offer-management --tags typescript
emi js --path definitions/offer/user-offer-information.emi.yml --output typescript/gen/offer/user-offer-information --tags typescript,no-sdk && \
emi js --path definitions/offer/offer-translations.emi.yml --output typescript/gen/offer/offer-translations --tags typescript,no-sdk && \
emi js --path definitions/offer/offer-management.emi.yml --output typescript/gen/offer/offer-management --tags typescript,no-sdk

gen-kotlin:
rm -rf kotlin/gen/ && \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,3 +621,77 @@ func (x BatchOfferPromotionPackageModificationActionRequest) IsCli() bool {
}
return true
}

// BatchOfferPromotionPackageModificationActionHttpHandler returns the HTTP method, the ServeMux pattern, and a
// typed net/http handler for the BatchOfferPromotionPackageModificationAction action. Developers implement
// their business logic as a function that receives a typed request object and
// returns either an *BatchOfferPromotionPackageModificationActionResponse or nil. JSON marshalling, headers,
// status codes, and errors are handled automatically.
func BatchOfferPromotionPackageModificationActionHttpHandler(
handler func(c BatchOfferPromotionPackageModificationActionRequest) (*BatchOfferPromotionPackageModificationActionResponse, error),
) (method, pattern string, h http.HandlerFunc) {
meta := BatchOfferPromotionPackageModificationActionMeta()
return meta.Method, meta.URL, func(w http.ResponseWriter, r *http.Request) {
var body BatchOfferPromotionPackageModificationActionReq
if r.Body != nil {
defer r.Body.Close()
if data, _ := io.ReadAll(r.Body); len(data) > 0 {
if err := json.Unmarshal(data, &body); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "invalid JSON: " + err.Error()})
return
}
}
}
// Build typed request wrapper. GinCtx stays nil here (this is not gin),
// which is what the IsGin() helper keys off.
req := BatchOfferPromotionPackageModificationActionRequest{
Body: body,
QueryParams: r.URL.Query(),
Headers: r.Header,
}
resp, err := handler(req)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
// If the handler returned nil (and no error), the response was handled
// manually.
if resp == nil {
return
}
// Apply headers
for k, v := range resp.Headers {
w.Header().Set(k, v)
}
// Apply status and payload
status := resp.StatusCode
if status == 0 {
status = http.StatusOK
}
if resp.Payload != nil {
if w.Header().Get("Content-Type") == "" {
w.Header().Set("Content-Type", "application/json")
}
w.WriteHeader(status)
json.NewEncoder(w).Encode(resp.Payload)
} else {
w.WriteHeader(status)
}
}
}

// BatchOfferPromotionPackageModificationActionHttp is a high-level convenience wrapper around
// BatchOfferPromotionPackageModificationActionHttpHandler. It registers the typed route on a standard
// *http.ServeMux using Go 1.22+ method-aware pattern syntax (e.g. "POST /").
// Use this when you don't need custom middleware.
func BatchOfferPromotionPackageModificationActionHttp(
mux *http.ServeMux,
handler func(c BatchOfferPromotionPackageModificationActionRequest) (*BatchOfferPromotionPackageModificationActionResponse, error),
) {
method, pattern, h := BatchOfferPromotionPackageModificationActionHttpHandler(handler)
mux.HandleFunc(method+" "+pattern, h)
}
Original file line number Diff line number Diff line change
Expand Up @@ -468,3 +468,77 @@ func (x BatchOfferPublishUnpublishActionRequest) IsCli() bool {
}
return true
}

// BatchOfferPublishUnpublishActionHttpHandler returns the HTTP method, the ServeMux pattern, and a
// typed net/http handler for the BatchOfferPublishUnpublishAction action. Developers implement
// their business logic as a function that receives a typed request object and
// returns either an *BatchOfferPublishUnpublishActionResponse or nil. JSON marshalling, headers,
// status codes, and errors are handled automatically.
func BatchOfferPublishUnpublishActionHttpHandler(
handler func(c BatchOfferPublishUnpublishActionRequest) (*BatchOfferPublishUnpublishActionResponse, error),
) (method, pattern string, h http.HandlerFunc) {
meta := BatchOfferPublishUnpublishActionMeta()
return meta.Method, meta.URL, func(w http.ResponseWriter, r *http.Request) {
var body BatchOfferPublishUnpublishActionReq
if r.Body != nil {
defer r.Body.Close()
if data, _ := io.ReadAll(r.Body); len(data) > 0 {
if err := json.Unmarshal(data, &body); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "invalid JSON: " + err.Error()})
return
}
}
}
// Build typed request wrapper. GinCtx stays nil here (this is not gin),
// which is what the IsGin() helper keys off.
req := BatchOfferPublishUnpublishActionRequest{
Body: body,
QueryParams: r.URL.Query(),
Headers: r.Header,
}
resp, err := handler(req)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
// If the handler returned nil (and no error), the response was handled
// manually.
if resp == nil {
return
}
// Apply headers
for k, v := range resp.Headers {
w.Header().Set(k, v)
}
// Apply status and payload
status := resp.StatusCode
if status == 0 {
status = http.StatusOK
}
if resp.Payload != nil {
if w.Header().Get("Content-Type") == "" {
w.Header().Set("Content-Type", "application/json")
}
w.WriteHeader(status)
json.NewEncoder(w).Encode(resp.Payload)
} else {
w.WriteHeader(status)
}
}
}

// BatchOfferPublishUnpublishActionHttp is a high-level convenience wrapper around
// BatchOfferPublishUnpublishActionHttpHandler. It registers the typed route on a standard
// *http.ServeMux using Go 1.22+ method-aware pattern syntax (e.g. "POST /").
// Use this when you don't need custom middleware.
func BatchOfferPublishUnpublishActionHttp(
mux *http.ServeMux,
handler func(c BatchOfferPublishUnpublishActionRequest) (*BatchOfferPublishUnpublishActionResponse, error),
) {
method, pattern, h := BatchOfferPublishUnpublishActionHttpHandler(handler)
mux.HandleFunc(method+" "+pattern, h)
}
Loading
Loading