1package systemviewsbridge
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "strings"
8)
9
10type browserRequest struct {
11 messageType string
12 requestID string
13 body []byte
14 directAPI bool
15}
16
17func decodeBrowserRequest(body []byte) (browserRequest, error) {
18 var envelope struct {
19 Type string `json:"type"`
20 Payload json.RawMessage `json:"payload"`
21 }
22 if err := json.Unmarshal(body, &envelope); err != nil {
23 return browserRequest{}, fmt.Errorf("unmarshal browser request: %w", err)
24 }
25 if envelope.Type == "" {
26 return browserRequest{}, fmt.Errorf("browser request missing type")
27 }
28
29 var payload struct {
30 RequestID string `json:"requestId"`
31 }
32 if len(envelope.Payload) != 0 {
33 if err := json.Unmarshal(envelope.Payload, &payload); err != nil {
34 return browserRequest{}, fmt.Errorf("unmarshal request payload: %w", err)
35 }
36 }
37
38 return browserRequest{
39 messageType: envelope.Type,
40 requestID: payload.RequestID,
41 body: body,
42 directAPI: strings.HasPrefix(envelope.Type, "/api/"),
43 }, nil
44}
45
46func wrapDirectAPIResponse(requestID string, payload []byte) ([]byte, error) {
47 return json.Marshal(bridgeResponse{
48 RequestID: requestID,
49 Success: true,
50 Data: json.RawMessage(payload),
51 })
52}
53
54// normalizeBridgeShimResponse accepts the bridge shapes emitted by native and
55// fallback shims: a direct bridgeResponse envelope, a wrapper with data/error,
56// or a wrapper whose data field itself contains a bridgeResponse envelope.
57func normalizeBridgeShimResponse(requestID string, body []byte) ([]byte, error) {
58 trimmed := bytes.TrimSpace(body)
59 if len(trimmed) == 0 {
60 return nil, fmt.Errorf("empty bridge shim response")
61 }
62
63 var topLevel map[string]json.RawMessage
64 if err := json.Unmarshal(trimmed, &topLevel); err != nil {
65 return nil, fmt.Errorf("unmarshal top-level bridge response: %w", err)
66 }
67
68 _, hasSuccess := topLevel["success"]
69 _, hasRequestID := topLevel["requestId"]
70 _, hasCallback := topLevel["_callback"]
71 _, hasData := topLevel["data"]
72
73 // Native callbacks can already return the final bridgeResponse shape.
74 if (hasSuccess || hasRequestID) && !hasCallback {
75 var resp bridgeResponse
76 if err := json.Unmarshal(trimmed, &resp); err != nil {
77 return nil, fmt.Errorf("unmarshal direct bridge response: %w", err)
78 }
79 if resp.RequestID == "" {
80 // Some direct shims omit requestId; preserve correlation for callers.
81 resp.RequestID = requestID
82 }
83 return json.Marshal(resp)
84 }
85
86 var wrapped struct {
87 Data json.RawMessage `json:"data,omitempty"`
88 Error string `json:"error,omitempty"`
89 }
90 if err := json.Unmarshal(trimmed, &wrapped); err != nil {
91 return nil, fmt.Errorf("unmarshal wrapped bridge response: %w", err)
92 }
93 if wrapped.Error != "" {
94 // Fetch-style wrappers report transport errors outside the data payload.
95 return json.Marshal(bridgeResponse{RequestID: requestID, Error: wrapped.Error})
96 }
97 if !hasData {
98 // Wrapped shim responses must carry either data or a top-level error.
99 return nil, fmt.Errorf("wrapped bridge response missing data")
100 }
101 if len(wrapped.Data) == 0 {
102 return json.Marshal(bridgeResponse{RequestID: requestID, Success: true})
103 }
104
105 var nestedFields map[string]json.RawMessage
106 if err := json.Unmarshal(wrapped.Data, &nestedFields); err == nil {
107 nestedSuccess, nestedHasSuccess := nestedFields["success"]
108 _, nestedHasRequestID := nestedFields["requestId"]
109 _, nestedHasError := nestedFields["error"]
110 successIsBool := false
111 if nestedHasSuccess {
112 var successValue bool
113 successIsBool = json.Unmarshal(nestedSuccess, &successValue) == nil
114 }
115 hasNestedEnvelope := successIsBool || (nestedHasRequestID && nestedHasError)
116 if hasNestedEnvelope {
117 var nested bridgeResponse
118 if err := json.Unmarshal(wrapped.Data, &nested); err == nil {
119 if nested.RequestID == "" {
120 // Nested envelopes from direct API wrappers also need caller correlation.
121 nested.RequestID = requestID
122 }
123 return json.Marshal(nested)
124 }
125 }
126 }
127
128 return json.Marshal(bridgeResponse{
129 RequestID: requestID,
130 Success: true,
131 Data: wrapped.Data,
132 })
133}