starpc 0.49.14 → 0.49.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -232,7 +232,7 @@ import { WebSocketConn } from 'starpc'
232
232
  import { EchoerClient } from './echo/index.js'
233
233
 
234
234
  const ws = new WebSocket('ws://localhost:8080/api')
235
- const conn = new WebSocketConn(ws)
235
+ const conn = new WebSocketConn(ws, 'outbound')
236
236
  const client = conn.buildClient()
237
237
  const echoer = new EchoerClient(client)
238
238
 
@@ -10,12 +10,14 @@ export declare class CommonRPC {
10
10
  protected service?: string;
11
11
  protected method?: string;
12
12
  private closed?;
13
+ private readonly writeDrainAbort;
13
14
  constructor();
14
15
  get isClosed(): boolean | Error;
15
16
  writeCallData(data?: Uint8Array, complete?: boolean, error?: string): Promise<void>;
17
+ private writeCallDataPacket;
16
18
  writeCallCancel(): Promise<void>;
17
19
  writeCallDataFromSource(dataSource: AsyncIterable<Uint8Array>): Promise<void>;
18
- protected writePacket(packet: Packet): Promise<void>;
20
+ protected writePacket(packet: Packet, options?: WritePacketOptions): Promise<void>;
19
21
  handleMessage(message: Uint8Array): Promise<void>;
20
22
  handlePacket(packet: Packet): Promise<void>;
21
23
  handleCallStart(packet: Partial<CallStart>): Promise<void>;
@@ -25,3 +27,8 @@ export declare class CommonRPC {
25
27
  close(err?: Error): Promise<void>;
26
28
  private _createSink;
27
29
  }
30
+ interface WritePacketOptions {
31
+ allowClosed?: boolean;
32
+ waitForDrain?: boolean;
33
+ }
34
+ export {};
@@ -1,6 +1,7 @@
1
1
  import { pushable } from 'it-pushable';
2
2
  import { Packet } from './rpcproto.pb.js';
3
3
  import { ERR_RPC_ABORT, RemoteRPCError } from './errors.js';
4
+ const maxBufferedOutgoingPackets = 1;
4
5
  // CommonRPC is common logic between server and client RPCs.
5
6
  export class CommonRPC {
6
7
  // sink is the data sink for incoming messages.
@@ -23,6 +24,8 @@ export class CommonRPC {
23
24
  method;
24
25
  // closed indicates this rpc has been closed already.
25
26
  closed;
27
+ // writeDrainAbort wakes writers waiting for outbound stream drain on close.
28
+ writeDrainAbort = new AbortController();
26
29
  constructor() {
27
30
  this.sink = this._createSink();
28
31
  this.source = this._source;
@@ -34,6 +37,10 @@ export class CommonRPC {
34
37
  }
35
38
  // writeCallData writes the call data packet.
36
39
  async writeCallData(data, complete, error) {
40
+ await this.writeCallDataPacket(data, complete, error);
41
+ }
42
+ // writeCallDataPacket writes a call-data packet with optional drain control.
43
+ async writeCallDataPacket(data, complete, error, writeOptions) {
37
44
  const callData = {
38
45
  data: data || new Uint8Array(0),
39
46
  dataIsZero: !!data && data.length === 0,
@@ -45,7 +52,7 @@ export class CommonRPC {
45
52
  case: 'callData',
46
53
  value: callData,
47
54
  },
48
- });
55
+ }, writeOptions);
49
56
  }
50
57
  // writeCallCancel writes the call cancel packet.
51
58
  async writeCallCancel() {
@@ -54,7 +61,7 @@ export class CommonRPC {
54
61
  case: 'callCancel',
55
62
  value: true,
56
63
  },
57
- });
64
+ }, { waitForDrain: false });
58
65
  }
59
66
  // writeCallDataFromSource writes all call data from the iterable.
60
67
  async writeCallDataFromSource(dataSource) {
@@ -69,8 +76,24 @@ export class CommonRPC {
69
76
  }
70
77
  }
71
78
  // writePacket writes a packet to the stream.
72
- async writePacket(packet) {
79
+ async writePacket(packet, options) {
80
+ if (this.closed && !options?.allowClosed) {
81
+ throw new Error(ERR_RPC_ABORT);
82
+ }
73
83
  this._source.push(packet);
84
+ if (options?.waitForDrain === false ||
85
+ this._source.readableLength <= maxBufferedOutgoingPackets) {
86
+ return;
87
+ }
88
+ try {
89
+ await this._source.onEmpty({ signal: this.writeDrainAbort.signal });
90
+ }
91
+ catch (err) {
92
+ if (this.closed) {
93
+ throw new Error(ERR_RPC_ABORT, { cause: err });
94
+ }
95
+ throw err;
96
+ }
74
97
  }
75
98
  // handleMessage handles an incoming encoded Packet.
76
99
  //
@@ -149,8 +172,12 @@ export class CommonRPC {
149
172
  this.closed = err ?? true;
150
173
  // note: this does nothing if _source is already ended.
151
174
  if (err && err.message) {
152
- await this.writeCallData(undefined, true, err.message);
175
+ await this.writeCallDataPacket(undefined, true, err.message, {
176
+ allowClosed: true,
177
+ waitForDrain: false,
178
+ });
153
179
  }
180
+ this.writeDrainAbort.abort();
154
181
  this._source.end();
155
182
  this._rpcDataSource.end(err);
156
183
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,178 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Client } from './client.js';
3
+ import { CommonRPC } from './common-rpc.js';
4
+ import { Packet } from './rpcproto.pb.js';
5
+ describe('CommonRPC', () => {
6
+ it('backpressures call-data sources until outbound packets drain', async () => {
7
+ const rpc = new CommonRPC();
8
+ const yielded = { count: 0 };
9
+ const secondYield = deferred();
10
+ const thirdYield = deferred();
11
+ const writeDone = rpc.writeCallDataFromSource(chunkSource(yielded, new Map([
12
+ [1, secondYield],
13
+ [2, thirdYield],
14
+ ])));
15
+ await promiseWithTimeout(secondYield.promise, 'second call-data chunk');
16
+ expect(yielded.count).toBe(2);
17
+ await expectPending(thirdYield.promise, 'third call-data chunk');
18
+ expect(yielded.count).toBe(2);
19
+ const received = new Array();
20
+ const readComplete = (async () => {
21
+ for await (const packet of rpc.source) {
22
+ const body = packet.body;
23
+ if (body?.case !== 'callData') {
24
+ continue;
25
+ }
26
+ if (body.value.complete) {
27
+ return;
28
+ }
29
+ if (!body.value.data) {
30
+ throw new Error('call data packet missing data');
31
+ }
32
+ received.push(body.value.data[0]);
33
+ }
34
+ throw new Error('outbound call data ended before completion');
35
+ })();
36
+ await promiseWithTimeout(Promise.all([writeDone, readComplete]), 'backpressured call data drain');
37
+ await rpc.close();
38
+ expect(yielded.count).toBe(32);
39
+ expect(received).toHaveLength(32);
40
+ expect(received[0]).toBe(0);
41
+ expect(received[31]).toBe(31);
42
+ });
43
+ it('wakes call-data writers when the rpc closes while waiting for drain', async () => {
44
+ const rpc = new CommonRPC();
45
+ const yielded = { count: 0 };
46
+ const secondYield = deferred();
47
+ const thirdYield = deferred();
48
+ const writeDone = rpc.writeCallDataFromSource(chunkSource(yielded, new Map([
49
+ [1, secondYield],
50
+ [2, thirdYield],
51
+ ])));
52
+ await promiseWithTimeout(secondYield.promise, 'second call-data chunk');
53
+ expect(yielded.count).toBe(2);
54
+ await expectPending(thirdYield.promise, 'third call-data chunk');
55
+ expect(yielded.count).toBe(2);
56
+ await rpc.close();
57
+ await expect(promiseWithTimeout(writeDone, 'call data writer close')).resolves.toBeUndefined();
58
+ expect(yielded.count).toBeLessThan(32);
59
+ });
60
+ it('wakes call-data writers when an error closes the rpc while waiting for drain', async () => {
61
+ const rpc = new CommonRPC();
62
+ const yielded = { count: 0 };
63
+ const secondYield = deferred();
64
+ const thirdYield = deferred();
65
+ const writeDone = rpc.writeCallDataFromSource(chunkSource(yielded, new Map([
66
+ [1, secondYield],
67
+ [2, thirdYield],
68
+ ])));
69
+ await promiseWithTimeout(secondYield.promise, 'second call-data chunk');
70
+ expect(yielded.count).toBe(2);
71
+ await expectPending(thirdYield.promise, 'third call-data chunk');
72
+ expect(yielded.count).toBe(2);
73
+ await rpc.close(new Error('boom'));
74
+ await expect(promiseWithTimeout(writeDone, 'call data writer error close')).resolves.toBeUndefined();
75
+ expect(yielded.count).toBeLessThan(32);
76
+ });
77
+ it('does not backpressure call-cancel packets behind queued call data', async () => {
78
+ const rpc = new CommonRPC();
79
+ const yielded = { count: 0 };
80
+ const secondYield = deferred();
81
+ const thirdYield = deferred();
82
+ const writeDone = rpc.writeCallDataFromSource(chunkSource(yielded, new Map([
83
+ [1, secondYield],
84
+ [2, thirdYield],
85
+ ])));
86
+ await promiseWithTimeout(secondYield.promise, 'second call-data chunk');
87
+ expect(yielded.count).toBe(2);
88
+ await expectPending(thirdYield.promise, 'third call-data chunk');
89
+ expect(yielded.count).toBe(2);
90
+ const cancelDone = rpc.writeCallCancel();
91
+ await rpc.close(new Error('abort'));
92
+ await expect(promiseWithTimeout(cancelDone, 'call cancel write')).resolves.toBeUndefined();
93
+ await expect(promiseWithTimeout(writeDone, 'blocked call data close')).resolves.toBeUndefined();
94
+ });
95
+ it('backpressures client-stream sources through the Client request path', async () => {
96
+ const yielded = { count: 0 };
97
+ const firstYield = deferred();
98
+ const secondYield = deferred();
99
+ const sinkConsumed = { count: 0 };
100
+ const sinkGate = deferred();
101
+ const responseGate = deferred();
102
+ const response = new Uint8Array([7]);
103
+ const client = new Client(async () => ({
104
+ source: (async function* () {
105
+ await responseGate.promise;
106
+ yield Packet.toBinary({
107
+ body: {
108
+ case: 'callData',
109
+ value: {
110
+ data: response,
111
+ dataIsZero: false,
112
+ complete: false,
113
+ error: '',
114
+ },
115
+ },
116
+ });
117
+ })(),
118
+ sink: async (source) => {
119
+ await sinkGate.promise;
120
+ for await (const _packet of source) {
121
+ sinkConsumed.count++;
122
+ }
123
+ },
124
+ }));
125
+ const request = client.clientStreamingRequest('test.Service', 'Upload', chunkSource(yielded, new Map([
126
+ [0, firstYield],
127
+ [1, secondYield],
128
+ ])));
129
+ await promiseWithTimeout(firstYield.promise, 'first upload chunk');
130
+ expect(yielded.count).toBe(1);
131
+ await expectPending(secondYield.promise, 'second upload chunk');
132
+ expect(yielded.count).toBe(1);
133
+ expect(sinkConsumed.count).toBe(0);
134
+ await expectPending(request, 'client-stream request');
135
+ sinkGate.resolve();
136
+ responseGate.resolve();
137
+ await expect(promiseWithTimeout(request, 'client-stream response')).resolves.toEqual(response);
138
+ });
139
+ });
140
+ async function* chunkSource(yielded, yieldMarks) {
141
+ for (const i of Array.from({ length: 32 }, (_, index) => index)) {
142
+ yielded.count++;
143
+ yieldMarks?.get(i)?.resolve();
144
+ yield new Uint8Array([i]);
145
+ }
146
+ }
147
+ function promiseWithTimeout(promise, label) {
148
+ return Promise.race([
149
+ promise,
150
+ new Promise((_, reject) => {
151
+ setTimeout(() => reject(new Error(`timed out waiting for ${label}`)), 500);
152
+ }),
153
+ ]);
154
+ }
155
+ async function expectPending(promise, label) {
156
+ await expect(Promise.race([
157
+ promise.then(() => 'settled'),
158
+ new Promise((resolve) => {
159
+ setTimeout(() => resolve('pending'), 50);
160
+ }),
161
+ ]), `${label} should not be requested while outbound packets are blocked`).resolves.toBe('pending');
162
+ }
163
+ function deferred() {
164
+ const callbacks = {};
165
+ const promise = new Promise((resolve, reject) => {
166
+ callbacks.resolve = resolve;
167
+ callbacks.reject = reject;
168
+ });
169
+ return {
170
+ promise,
171
+ resolve() {
172
+ callbacks.resolve?.();
173
+ },
174
+ reject(reason) {
175
+ callbacks.reject?.(reason);
176
+ },
177
+ };
178
+ }
@@ -94,6 +94,71 @@ describe('srpc server', () => {
94
94
  }
95
95
  expect([...body.value.data]).toEqual([...response]);
96
96
  });
97
+ it('keeps StreamConn server-streaming responses open after Go-style request close', async () => {
98
+ const mux = createMux();
99
+ const response = new TextEncoder().encode('streamconn init');
100
+ mux.registerLookupMethod(async (service, method) => {
101
+ if (service !== 'test.ResourceService' || method !== 'ResourceClient') {
102
+ return null;
103
+ }
104
+ return async (_dataSource, dataSink) => {
105
+ await dataSink((async function* () {
106
+ await new Promise((resolve) => setTimeout(resolve, 10));
107
+ yield response;
108
+ })());
109
+ };
110
+ });
111
+ const server = new Server(mux.lookupMethod);
112
+ const clientConn = new StreamConn();
113
+ const serverConn = new StreamConn(server, { direction: 'inbound' });
114
+ const { port1: clientPort, port2: serverPort } = new MessageChannel();
115
+ const clientChannelStream = new ChannelStream('client', clientPort);
116
+ const serverChannelStream = new ChannelStream('server', serverPort);
117
+ pipe(clientChannelStream, clientConn, combineUint8ArrayListTransform(), clientChannelStream)
118
+ .catch((err) => clientConn.close(err))
119
+ .then(() => clientConn.close());
120
+ pipe(serverChannelStream, serverConn, combineUint8ArrayListTransform(), serverChannelStream)
121
+ .catch((err) => serverConn.close(err))
122
+ .then(() => serverConn.close());
123
+ try {
124
+ const stream = await clientConn.openStream();
125
+ const firstResponse = (async () => {
126
+ for await (const packetData of stream.source) {
127
+ const packet = Packet.fromBinary(packetData);
128
+ if (packet.body?.case === 'callData') {
129
+ return packet;
130
+ }
131
+ }
132
+ throw new Error('server response stream ended before call data');
133
+ })();
134
+ await stream.sink((async function* () {
135
+ yield Packet.toBinary({
136
+ body: {
137
+ case: 'callStart',
138
+ value: {
139
+ rpcService: 'test.ResourceService',
140
+ rpcMethod: 'ResourceClient',
141
+ data: new Uint8Array(0),
142
+ dataIsZero: true,
143
+ },
144
+ },
145
+ });
146
+ })());
147
+ const packet = await promiseWithTimeout(firstResponse, 'StreamConn server-streaming response');
148
+ const body = packet.body;
149
+ expect(body?.case).toBe('callData');
150
+ if (body?.case !== 'callData' || !body.value?.data) {
151
+ throw new Error('expected callData packet');
152
+ }
153
+ expect([...body.value.data]).toEqual([...response]);
154
+ }
155
+ finally {
156
+ clientConn.close();
157
+ serverConn.close();
158
+ clientChannelStream.close();
159
+ serverChannelStream.close();
160
+ }
161
+ });
97
162
  it('removes abort listeners after a request completes', async () => {
98
163
  const controller = new AbortController();
99
164
  const removeEventListener = vi.spyOn(controller.signal, 'removeEventListener');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starpc",
3
- "version": "0.49.14",
3
+ "version": "0.49.16",
4
4
  "description": "Streaming protobuf RPC service protocol over any two-way channel.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -31,7 +31,7 @@ func (r *ClientRPC) Start(writer PacketWriter, writeFirstMsg bool, firstMsg []by
31
31
  }
32
32
 
33
33
  if err := r.ctx.Err(); err != nil {
34
- r.ctxCancel()
34
+ r.cancelContext()
35
35
  _ = writer.Close()
36
36
  return context.Canceled
37
37
  }
@@ -48,7 +48,7 @@ func (r *ClientRPC) Start(writer PacketWriter, writeFirstMsg bool, firstMsg []by
48
48
  pkt := NewCallStartPacket(r.service, r.method, firstMsg, firstMsgEmpty)
49
49
  err = writer.WritePacket(pkt)
50
50
  if err != nil {
51
- r.ctxCancel()
51
+ r.cancelContext()
52
52
  _ = writer.Close()
53
53
  }
54
54
 
@@ -74,7 +74,7 @@ func (r *ClientRPC) HandleStreamClose(closeErr error) {
74
74
  r.remoteErr = closeErr
75
75
  }
76
76
  r.dataClosed = true
77
- r.ctxCancel()
77
+ r.cancelContext()
78
78
  locked.Broadcast()
79
79
  locked.Unlock()
80
80
  }
@@ -109,10 +109,14 @@ func (r *ClientRPC) HandleCallStart(pkt *CallStart) error {
109
109
  // Close releases any resources held by the ClientRPC.
110
110
  func (r *ClientRPC) Close() {
111
111
  locked := r.bcast.Lock()
112
+ var writer PacketWriter
112
113
  // call did not start yet if writer is nil.
113
114
  if r.writer != nil {
114
115
  _ = r.WriteCallCancel()
115
- r.closeLocked(&locked)
116
+ writer = r.closeLocked(&locked)
116
117
  }
117
118
  locked.Unlock()
119
+ if writer != nil {
120
+ _ = writer.Close()
121
+ }
118
122
  }
package/srpc/client.go CHANGED
@@ -91,7 +91,7 @@ func (c *client) NewStream(ctx context.Context, service, method string, firstMsg
91
91
  }
92
92
 
93
93
  return NewMsgStream(ctx, clientRPC, func() {
94
- clientRPC.ctxCancel()
94
+ clientRPC.cancelContext()
95
95
  _ = writer.Close()
96
96
  }), nil
97
97
  }
package/srpc/client.rs CHANGED
@@ -262,6 +262,10 @@ pub mod transport {
262
262
 
263
263
  /// Re-export TransportPacketWriter for direct use.
264
264
  pub use crate::transport::TransportPacketWriter;
265
+
266
+ /// Re-export YamuxStreamOpener for multiplexed transports.
267
+ #[cfg(feature = "yamux")]
268
+ pub use crate::yamux::YamuxStreamOpener;
265
269
  }
266
270
 
267
271
  #[cfg(test)]
@@ -5,16 +5,19 @@ import (
5
5
  "io"
6
6
  "sync/atomic"
7
7
 
8
+ "github.com/aperturerobotics/starpc/internal/contextutil"
8
9
  "github.com/aperturerobotics/util/broadcast"
9
10
  "github.com/pkg/errors"
10
11
  )
11
12
 
12
13
  // commonRPC contains common logic between server/client rpc.
13
14
  type commonRPC struct {
14
- // ctx is the context, canceled when the rpc ends.
15
+ // ctx is the RPC context, canceled when the RPC is canceled.
15
16
  ctx context.Context
16
- // ctxCancel is called when the rpc ends.
17
+ // ctxCancel cancels ctx.
17
18
  ctxCancel context.CancelFunc
19
+ // ctxCanceled tracks whether ctxCancel has already been called.
20
+ ctxCanceled atomic.Bool
18
21
  // service is the rpc service
19
22
  service string
20
23
  // method is the rpc method
@@ -26,6 +29,13 @@ type commonRPC struct {
26
29
  bcast broadcast.Broadcast
27
30
  // writer is the writer to write messages to
28
31
  writer PacketWriter
32
+ // writerClosed is set after writer has been closed locally.
33
+ writerClosed bool
34
+ // localCompleting is set while the local handler is publishing its terminal
35
+ // packet and closing the writer.
36
+ localCompleting bool
37
+ // localDone is set after the local handler has completed normally.
38
+ localDone bool
29
39
  // dataQueue contains incoming data packets.
30
40
  // note: packets may be len() == 0
31
41
  dataQueue [][]byte
@@ -38,7 +48,14 @@ type commonRPC struct {
38
48
 
39
49
  // initCommonRPC initializes the commonRPC.
40
50
  func initCommonRPC(ctx context.Context, rpc *commonRPC) {
41
- rpc.ctx, rpc.ctxCancel = context.WithCancel(ctx)
51
+ rpc.ctx, rpc.ctxCancel = contextutil.WithCancel(ctx)
52
+ }
53
+
54
+ func (c *commonRPC) cancelContext() {
55
+ if c.ctxCanceled.Swap(true) {
56
+ return
57
+ }
58
+ c.ctxCancel()
42
59
  }
43
60
 
44
61
  // Context is canceled when the rpc has finished.
@@ -51,10 +68,13 @@ func (c *commonRPC) Wait(ctx context.Context) error {
51
68
  for {
52
69
  var err error
53
70
  var waitCh <-chan struct{}
54
- var rpcCtx context.Context
71
+ var rpcCanceled bool
72
+ var localDone bool
55
73
  locked := c.bcast.Lock()
56
- rpcCtx, err = c.ctx, c.remoteErr
57
- if err == nil && rpcCtx.Err() == nil {
74
+ err = c.remoteErr
75
+ rpcCanceled = c.ctx.Err() != nil
76
+ localDone = c.localDone
77
+ if err == nil && !rpcCanceled && !localDone {
58
78
  waitCh = locked.WaitCh()
59
79
  }
60
80
  locked.Unlock()
@@ -62,15 +82,16 @@ func (c *commonRPC) Wait(ctx context.Context) error {
62
82
  if err != nil {
63
83
  return err
64
84
  }
65
- if rpcCtx.Err() != nil {
66
- // rpc must have ended w/o an error being set
85
+ if localDone {
86
+ return nil
87
+ }
88
+ if rpcCanceled {
67
89
  return context.Canceled
68
90
  }
69
91
 
70
92
  select {
71
93
  case <-ctx.Done():
72
94
  return context.Canceled
73
- case <-rpcCtx.Done():
74
95
  case <-waitCh:
75
96
  }
76
97
  }
@@ -86,8 +107,11 @@ func (c *commonRPC) ReadOne() ([]byte, error) {
86
107
  locked := c.bcast.Lock()
87
108
  if ctxDone && !c.dataClosed {
88
109
  // context must have been canceled locally
89
- c.closeLocked(&locked)
110
+ writer := c.closeLocked(&locked)
90
111
  locked.Unlock()
112
+ if writer != nil {
113
+ _ = writer.Close()
114
+ }
91
115
  return nil, context.Canceled
92
116
  }
93
117
 
@@ -144,16 +168,19 @@ func (c *commonRPC) WriteCallData(data []byte, dataIsZero, complete bool, err er
144
168
  func (c *commonRPC) HandleStreamClose(closeErr error) {
145
169
  var writer PacketWriter
146
170
  locked := c.bcast.Lock()
147
- if c.dataClosed {
171
+ if c.dataClosed && c.writerClosed {
148
172
  locked.Unlock()
149
173
  return
150
174
  }
175
+ normalRemoteCloseAfterLocalComplete := closeErr == nil && (c.localCompleting || c.localDone)
151
176
  if closeErr != nil && c.remoteErr == nil {
152
177
  c.remoteErr = closeErr
153
178
  }
154
179
  c.dataClosed = true
155
- c.ctxCancel()
156
- writer = c.writer
180
+ if !normalRemoteCloseAfterLocalComplete {
181
+ c.cancelContext()
182
+ writer = c.closeWriterLocked()
183
+ }
157
184
  locked.Broadcast()
158
185
  locked.Unlock()
159
186
  if writer != nil {
@@ -212,16 +239,44 @@ func (c *commonRPC) WriteCallCancel() error {
212
239
  }
213
240
 
214
241
  // closeLocked releases resources held by the RPC.
215
- func (c *commonRPC) closeLocked(locked *broadcast.Locked) {
216
- if c.dataClosed {
217
- return
218
- }
242
+ func (c *commonRPC) closeLocked(locked *broadcast.Locked) PacketWriter {
219
243
  c.dataClosed = true
220
244
  c.localCompleted.Store(true)
221
245
  if c.remoteErr == nil {
222
246
  c.remoteErr = context.Canceled
223
247
  }
224
- _ = c.writer.Close()
248
+ writer := c.closeWriterLocked()
225
249
  locked.Broadcast()
226
- c.ctxCancel()
250
+ c.cancelContext()
251
+ return writer
252
+ }
253
+
254
+ func (c *commonRPC) closeWriterLocked() PacketWriter {
255
+ if c.writerClosed || c.writer == nil {
256
+ return nil
257
+ }
258
+ c.writerClosed = true
259
+ return c.writer
260
+ }
261
+
262
+ func (c *commonRPC) beginLocalCompletion() {
263
+ locked := c.bcast.Lock()
264
+ c.localCompleted.Store(true)
265
+ c.localCompleting = true
266
+ locked.Unlock()
267
+ }
268
+
269
+ func (c *commonRPC) finishLocalCompletion() {
270
+ locked := c.bcast.Lock()
271
+ c.localCompleted.Store(true)
272
+ writer := c.closeWriterLocked()
273
+ locked.Unlock()
274
+ if writer != nil {
275
+ _ = writer.Close()
276
+ }
277
+ locked = c.bcast.Lock()
278
+ c.localCompleting = false
279
+ c.localDone = true
280
+ locked.Broadcast()
281
+ locked.Unlock()
227
282
  }