-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathanon_channel.go
More file actions
165 lines (152 loc) · 5.23 KB
/
Copy pathanon_channel.go
File metadata and controls
165 lines (152 loc) · 5.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package wrapper
import (
"context"
"errors"
"strconv"
)
// errNotReady is returned by Emit and Request when called before the anonymous
// channel has been returned by the handler.
var errNotReady = errors.New("anonymous channel not yet ready")
// AnonymousChannel is a request-scoped channel created when a handler returns
// *AnonymousChannel in response to a request. It allows streaming or
// multi-message patterns over a single WebSocket connection.
//
// Emit and Request are only available once the channel has been delivered to
// the requestor (i.e. after the handler returns it). Before that point, both
// methods return errNotReady. On and Once may be called freely at any time,
// including inside the handler before the channel is returned.
type AnonymousChannel struct {
id int
client *Client
ctx context.Context
ctxCancel context.CancelCauseFunc
ready bool // true once the creation response has been sent to the requestor
}
// newAnonymousChannel creates an AnonymousChannel with the given ID and client.
// The channel's context is derived from ctx. Pass the client connection context
// so the channel stays alive as long as the connection is open; when the
// connection closes, closeWithCause is called explicitly via ch.ctxCancel.
func newAnonymousChannel(
ctx context.Context,
reqCtx context.Context,
id int,
c *Client,
) *AnonymousChannel {
ctx, ctxCancel := context.WithCancelCause(ctx)
anon := &AnonymousChannel{
id: id,
client: c,
ctx: ctx,
}
// When the caller's request context is cancelled, abort the anonymous
// channel.
stopReqCtxWatch := context.AfterFunc(reqCtx, func() {
_ = anon.Abort(context.Cause(reqCtx))
})
anon.ctxCancel = func(cause error) {
if stopReqCtxWatch != nil {
stopReqCtxWatch()
stopReqCtxWatch = nil
}
ctxCancel(cause)
}
return anon
}
// On adds an event handler for the specified event on this anonymous channel.
// See ClientChannel.On for handler signature details.
func (ch *AnonymousChannel) On(eventName string, handler any) *AnonymousChannel {
c := ch.client
if c != nil {
key := handlerName{AnonymousChannel: ch.id, Event: eventName}
registerClientHandler(c, key, handler, false)
}
return ch
}
// Once adds a one-time event handler for the specified event on this anonymous
// channel. See ClientChannel.On for handler signature details.
func (ch *AnonymousChannel) Once(eventName string, handler any) *AnonymousChannel {
c := ch.client
if c != nil {
key := handlerName{AnonymousChannel: ch.id, Event: eventName}
registerClientHandler(c, key, handler, true)
}
return ch
}
// Emit sends an event on this anonymous channel. The passed context can be used
// to cancel writing the message to the client. The first argument must be
// the event name string. Returns an error if the channel is not yet ready,
// is closed, or if the message could not be sent.
func (ch *AnonymousChannel) Emit(ctx context.Context, arguments ...any) error {
if !ch.ready {
return errNotReady
}
c := ch.client
if c == nil {
return ChannelClosedError{Channel: strconv.Itoa(ch.id)}
}
_, err := checkEventName(arguments)
if err != nil {
return err
}
return c.sendEvent(ctx, "", ch.id, arguments...)
}
// Request sends a request on this anonymous channel and returns the response.
// The passed context can be used to cancel the request. The first argument must
// be the event name string. Returns an error if the channel is not yet ready
// or is closed.
func (ch *AnonymousChannel) Request(
ctx context.Context, arguments ...any,
) (any, error) {
if !ch.ready {
return nil, errNotReady
}
c := ch.client
if c == nil {
return nil, ChannelClosedError{Channel: strconv.Itoa(ch.id)}
}
eventName, err := checkEventName(arguments)
if err != nil {
return nil, err
}
_ = eventName
return c.sendRequest(ctx, "", ch.id, arguments...)
}
// Close removes all event handlers for this anonymous channel and cancels its
// context. It does NOT send an abort message to the remote end; use Abort for
// that. Close is idempotent and safe to call multiple times.
func (ch *AnonymousChannel) Close() error {
ch.closeWithCause(context.Canceled)
return nil
}
// closeWithCause closes the anonymous channel with a specific cause. This is
// used internally when the connection closes or when an abort is received from
// the remote end; it does not send an abort message.
func (ch *AnonymousChannel) closeWithCause(cause error) {
c := ch.client
if c == nil {
return // already closed
}
ch.client = nil
c.handlersMu.Lock()
closeHandlersForChannel("", ch.id, c.handlers, c.handlersOnce)
c.handlersMu.Unlock()
ch.ctxCancel(cause)
c.connReqMu.Lock()
delete(c.anonChans, ch.id)
c.connReqMu.Unlock()
}
// Abort sends an abort message to the remote end with the provided reason and
// closes this anonymous channel. If err is nil, it defaults to context.Canceled.
func (ch *AnonymousChannel) Abort(err error) error {
c := ch.client
if c == nil {
return nil // already closed
}
return c.sendAnonCancel(ch.ctx, ch.id, err)
}
// Context returns a context that is cancelled when this anonymous channel is
// closed or aborted. The cancellation cause reflects the abort reason when the
// remote end sends an abort message.
func (ch *AnonymousChannel) Context() context.Context {
return ch.ctx
}