Skip to content

Commit 25510b2

Browse files
committed
Add WASM-Compatible PublishTo Function
Add version of PublishTo for wasm builds that uses the REST API's /composite/sobjects endpoint to publish platform events since gRPC doesn't work in the browser without a proxy.
1 parent 04460e4 commit 25510b2

2 files changed

Lines changed: 153 additions & 2 deletions

File tree

pubsub.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build !wasm
2+
13
package batch
24

35
import (
@@ -19,11 +21,11 @@ func (u uncheckableResult) NumberRecordsFailed() int {
1921
return 0
2022
}
2123

22-
func PublishTo(session *force.Force, channel string) RecordWriter {
24+
func PublishTo(session *force.Force, eventChannel string) RecordWriter {
2325
return func(ctx context.Context, records <-chan force.ForceRecord) (Result, error) {
2426
force.Log = log.StandardLogger()
2527
res := uncheckableResult{}
26-
err := pubsub.PublishMessagesWithContext(ctx, session, channel, records)
28+
err := pubsub.PublishMessagesWithContext(ctx, session, eventChannel, records)
2729
return res, err
2830
}
2931
}

pubsub_wasm.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
//go:build wasm
2+
3+
package batch
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
"strings"
10+
11+
force "github.com/ForceCLI/force/lib"
12+
log "github.com/sirupsen/logrus"
13+
)
14+
15+
type uncheckableResult struct {
16+
}
17+
18+
func (u uncheckableResult) NumberBatchesFailed() int {
19+
return 0
20+
}
21+
22+
func (u uncheckableResult) NumberRecordsFailed() int {
23+
return 0
24+
}
25+
26+
// PublishTo creates a RecordWriter that publishes platform events using REST API (WASM compatible)
27+
func PublishTo(session *force.Force, eventChannel string) RecordWriter {
28+
return func(ctx context.Context, records <-chan force.ForceRecord) (Result, error) {
29+
res := uncheckableResult{}
30+
31+
// Collect records to batch publish
32+
var events []force.ForceRecord
33+
for record := range records {
34+
select {
35+
case <-ctx.Done():
36+
return res, ctx.Err()
37+
default:
38+
events = append(events, record)
39+
40+
// Batch publish every 200 events (Salesforce limit)
41+
if len(events) >= 200 {
42+
if err := publishBatch(session, eventChannel, events); err != nil {
43+
return res, err
44+
}
45+
events = events[:0] // Clear slice but keep capacity
46+
}
47+
}
48+
}
49+
50+
// Publish any remaining events
51+
if len(events) > 0 {
52+
if err := publishBatch(session, eventChannel, events); err != nil {
53+
return res, err
54+
}
55+
}
56+
57+
return res, nil
58+
}
59+
}
60+
61+
func publishBatch(session *force.Force, eventChannel string, events []force.ForceRecord) error {
62+
if len(events) == 0 {
63+
return nil
64+
}
65+
66+
// Platform events need to specify the event type
67+
eventType := eventChannel
68+
if strings.HasPrefix(eventType, "/event/") {
69+
eventType = strings.TrimPrefix(eventType, "/event/")
70+
}
71+
72+
// Ensure each record has the attributes field with the correct type
73+
recordsWithAttributes := make([]map[string]interface{}, len(events))
74+
for i, event := range events {
75+
record := make(map[string]interface{})
76+
// Copy all fields from the event
77+
for k, v := range event {
78+
record[k] = v
79+
}
80+
// Ensure attributes field exists with the type
81+
if _, hasAttrs := record["attributes"]; !hasAttrs {
82+
record["attributes"] = map[string]string{"type": eventType}
83+
}
84+
recordsWithAttributes[i] = record
85+
}
86+
87+
// Construct the composite request body
88+
compositeRequest := map[string]interface{}{
89+
"allOrNone": false,
90+
"records": recordsWithAttributes,
91+
}
92+
93+
// PostAbsolute expects a relative URL starting with /services/...
94+
endpoint := fmt.Sprintf("/services/data/%s/composite/sobjects", force.ApiVersion())
95+
96+
body, err := json.Marshal(compositeRequest)
97+
if err != nil {
98+
return fmt.Errorf("failed to marshal events: %v", err)
99+
}
100+
101+
log.Infof("Publishing %d events to %s", len(events), eventChannel)
102+
103+
// Use the Force client's PostAbsolute method for raw JSON
104+
responseStr, err := session.PostAbsolute(endpoint, string(body))
105+
if err != nil {
106+
if err == force.SessionExpiredError {
107+
// Try to refresh session
108+
if refreshErr := session.RefreshSession(); refreshErr != nil {
109+
return fmt.Errorf("session expired and refresh failed: %v", refreshErr)
110+
}
111+
// Retry after refresh
112+
responseStr, err = session.PostAbsolute(endpoint, string(body))
113+
if err != nil {
114+
return fmt.Errorf("failed to publish events after session refresh: %v", err)
115+
}
116+
} else {
117+
return fmt.Errorf("failed to publish events: %v", err)
118+
}
119+
}
120+
121+
responseBody := []byte(responseStr)
122+
123+
// Parse the response to check for errors
124+
var results []struct {
125+
Id string `json:"id"`
126+
Success bool `json:"success"`
127+
Errors []map[string]interface{} `json:"errors"`
128+
}
129+
130+
if err := json.Unmarshal(responseBody, &results); err != nil {
131+
return fmt.Errorf("failed to parse response: %v", err)
132+
}
133+
134+
// Check for any failed events
135+
var failedCount int
136+
for i, result := range results {
137+
if !result.Success {
138+
failedCount++
139+
log.Errorf("Failed to publish event %d: %v", i, result.Errors)
140+
}
141+
}
142+
143+
if failedCount > 0 {
144+
return fmt.Errorf("%d events failed to publish", failedCount)
145+
}
146+
147+
log.Infof("Successfully published %d events", len(events))
148+
return nil
149+
}

0 commit comments

Comments
 (0)