Browse Source

More work on async methods

tags/v0.11.0
Chris Smith 5 years ago
parent
commit
3289bb8e99

+ 17
- 8
src/main/kotlin/com/dmdirc/ktirc/IrcClient.kt View File

3
 import com.dmdirc.ktirc.events.IrcEvent
3
 import com.dmdirc.ktirc.events.IrcEvent
4
 import com.dmdirc.ktirc.io.CaseMapping
4
 import com.dmdirc.ktirc.io.CaseMapping
5
 import com.dmdirc.ktirc.messages.sendJoin
5
 import com.dmdirc.ktirc.messages.sendJoin
6
+import com.dmdirc.ktirc.messages.tagMap
6
 import com.dmdirc.ktirc.model.*
7
 import com.dmdirc.ktirc.model.*
7
 import com.dmdirc.ktirc.util.RemoveIn
8
 import com.dmdirc.ktirc.util.RemoveIn
8
 import kotlinx.coroutines.Deferred
9
 import kotlinx.coroutines.Deferred
143
      * This should only be needed to send raw/custom commands; standard messages can be sent using the
144
      * This should only be needed to send raw/custom commands; standard messages can be sent using the
144
      * extension methods in [com.dmdirc.ktirc.messages] such as TODO: sendJoinAsync.
145
      * extension methods in [com.dmdirc.ktirc.messages] such as TODO: sendJoinAsync.
145
      *
146
      *
146
-     * This method will return immediately. If the server supports the labeled-responses capability,
147
-     * the returned [Deferred] will be eventually populated with the response from the server. If
148
-     * the server does not support the capability, or the response times out, `null` will be supplied.
147
+     * This method will return immediately. The returned [Deferred] will eventually be populated with
148
+     * the server's response. If the server supports the labeled-responses capability, a label will
149
+     * be added to the outgoing message to identify the correct response; otherwise the [matcher]
150
+     * will be invoked on all incoming events to select the appropriate response.
151
+     *
152
+     * If the response times out, `null` will be supplied instead of an event.
149
      *
153
      *
150
      * @param command The command to be sent.
154
      * @param command The command to be sent.
151
      * @param arguments The arguments to the command.
155
      * @param arguments The arguments to the command.
156
+     * @param matcher The matcher to use to find a matching event.
152
      * @return A deferred [IrcEvent]? that contains the server's response to the command.
157
      * @return A deferred [IrcEvent]? that contains the server's response to the command.
153
      */
158
      */
154
-    fun sendAsync(command: String, vararg arguments: String) = sendAsync(emptyMap(),  command, *arguments)
159
+    fun sendAsync(command: String, arguments: Array<String>, matcher: (IrcEvent) -> Boolean) = sendAsync(tagMap(), command, arguments, matcher)
155
 
160
 
156
     /**
161
     /**
157
      * Sends the given command to the IRC server, and waits for a response back.
162
      * Sends the given command to the IRC server, and waits for a response back.
159
      * This should only be needed to send raw/custom commands; standard messages can be sent using the
164
      * This should only be needed to send raw/custom commands; standard messages can be sent using the
160
      * extension methods in [com.dmdirc.ktirc.messages] such as TODO: sendJoinAsync.
165
      * extension methods in [com.dmdirc.ktirc.messages] such as TODO: sendJoinAsync.
161
      *
166
      *
162
-     * This method will return immediately. If the server supports the labeled-responses capability,
163
-     * the returned [Deferred] will be eventually populated with the response from the server. If
164
-     * the server does not support the capability, or the response times out, `null` will be supplied.
167
+     * This method will return immediately. The returned [Deferred] will eventually be populated with
168
+     * the server's response. If the server supports the labeled-responses capability, a label will
169
+     * be added to the outgoing message to identify the correct response; otherwise the [matcher]
170
+     * will be invoked on all incoming events to select the appropriate response.
171
+     *
172
+     * If the response times out, `null` will be supplied instead of an event.
165
      *
173
      *
166
      * @param tags The IRCv3 tags to prefix the message with, if any.
174
      * @param tags The IRCv3 tags to prefix the message with, if any.
167
      * @param command The command to be sent.
175
      * @param command The command to be sent.
168
      * @param arguments The arguments to the command.
176
      * @param arguments The arguments to the command.
177
+     * @param matcher The matcher to use to find a matching event.
169
      * @return A deferred [IrcEvent]? that contains the server's response to the command.
178
      * @return A deferred [IrcEvent]? that contains the server's response to the command.
170
      */
179
      */
171
-    fun sendAsync(tags: Map<MessageTag, String>, command: String, vararg arguments: String): Deferred<IrcEvent?>
180
+    fun sendAsync(tags: Map<MessageTag, String>, command: String, arguments: Array<String>, matcher: (IrcEvent) -> Boolean): Deferred<IrcEvent?>
172
 
181
 
173
 }
182
 }
174
 
183
 

+ 11
- 7
src/main/kotlin/com/dmdirc/ktirc/IrcClientImpl.kt View File

64
                 ?: log.warning { "No send channel for command: $command" }
64
                 ?: log.warning { "No send channel for command: $command" }
65
     }
65
     }
66
 
66
 
67
-    override fun sendAsync(tags: Map<MessageTag, String>, command: String, vararg arguments: String) = async {
68
-        if (serverState.supportsLabeledResponses) {
69
-            val label = generateLabel(this@IrcClientImpl)
70
-            val channel = Channel<IrcEvent>(1)
71
-            serverState.labelChannels[label] = channel
67
+    override fun sendAsync(tags: Map<MessageTag, String>, command: String, arguments: Array<String>, matcher: (IrcEvent) -> Boolean) = async {
68
+        val label = generateLabel(this@IrcClientImpl)
69
+        val channel = Channel<IrcEvent>(1)
70
+
71
+        if (serverState.asyncResponseState.supportsLabeledResponses) {
72
+            serverState.asyncResponseState.pendingResponses[label] = channel to { event -> event.metadata.label == label }
72
             send(tags + (MessageTag.Label to label), command, *arguments)
73
             send(tags + (MessageTag.Label to label), command, *arguments)
73
-            withTimeoutOrNull(asyncTimeout) { channel.receive() }.also { serverState.labelChannels.remove(label) }
74
         } else {
74
         } else {
75
+            serverState.asyncResponseState.pendingResponses[label] = channel to matcher
75
             send(tags, command, *arguments)
76
             send(tags, command, *arguments)
76
-            null
77
         }
77
         }
78
+
79
+        withTimeoutOrNull(asyncTimeout) {
80
+            channel.receive()
81
+        }.also { serverState.asyncResponseState.pendingResponses.remove(label) }
78
     }
82
     }
79
 
83
 
80
     override fun connect() {
84
     override fun connect() {

+ 7
- 5
src/main/kotlin/com/dmdirc/ktirc/events/handlers/LabelledResponseHandler.kt View File

8
 internal class LabelledResponseHandler : EventHandler {
8
 internal class LabelledResponseHandler : EventHandler {
9
 
9
 
10
     override fun processEvent(client: IrcClient, event: IrcEvent) {
10
     override fun processEvent(client: IrcClient, event: IrcEvent) {
11
-        event.metadata.label?.let {
12
-            GlobalScope.launch {
13
-                client.serverState.labelChannels[it]?.send(event)
14
-            }
15
-        }
11
+        client.serverState.asyncResponseState.pendingResponses.values
12
+                .filter { it.second(event) }
13
+                .forEach {
14
+                    GlobalScope.launch {
15
+                        it.first.send(event)
16
+                    }
17
+                }
16
     }
18
     }
17
 
19
 
18
 }
20
 }

+ 28
- 14
src/main/kotlin/com/dmdirc/ktirc/model/ServerState.kt View File

73
     internal val batches = mutableMapOf<String, Batch>()
73
     internal val batches = mutableMapOf<String, Batch>()
74
 
74
 
75
     /**
75
     /**
76
-     * Counter for ensuring sent labels are unique.
77
-     */
78
-    internal val labelCounter = AtomicLong(0)
79
-
80
-    /**
81
-     * Whether or not the server supports labeled responses.
82
-     */
83
-    internal val supportsLabeledResponses: Boolean
84
-        get() = Capability.LabeledResponse in capabilities.enabledCapabilities
85
-
86
-    /**
87
-     * Channels waiting for a label to be received.
76
+     * Asynchronous command state.
88
      */
77
      */
89
-    internal val labelChannels = mutableMapOf<String, SendChannel<IrcEvent>>()
78
+    internal val asyncResponseState = AsyncResponseState(capabilities)
90
 
79
 
91
     /**
80
     /**
92
      * Determines if the given mode is one applied to a user of a channel, such as 'o' for operator.
81
      * Determines if the given mode is one applied to a user of a channel, such as 'o' for operator.
118
         capabilities.reset()
107
         capabilities.reset()
119
         sasl.reset()
108
         sasl.reset()
120
         batches.clear()
109
         batches.clear()
110
+        asyncResponseState.reset()
111
+    }
112
+
113
+}
114
+
115
+internal class AsyncResponseState(private val capabilities : CapabilitiesState) {
116
+
117
+
118
+    /**
119
+     * Counter for ensuring sent labels are unique.
120
+     */
121
+    internal val labelCounter = AtomicLong(0)
122
+
123
+    /**
124
+     * Whether or not the server supports labeled responses.
125
+     */
126
+    internal val supportsLabeledResponses: Boolean
127
+        get() = Capability.LabeledResponse in capabilities.enabledCapabilities
128
+
129
+    /**
130
+     * Channels waiting for a response to be received.
131
+     */
132
+    internal val pendingResponses = mutableMapOf<String, Pair<SendChannel<IrcEvent>, (IrcEvent) -> Boolean>>()
133
+
134
+    internal fun reset() {
121
         labelCounter.set(0)
135
         labelCounter.set(0)
122
-        labelChannels.clear()
136
+        pendingResponses.clear()
123
     }
137
     }
124
 
138
 
125
 }
139
 }

+ 1
- 1
src/main/kotlin/com/dmdirc/ktirc/util/Labels.kt View File

8
 
8
 
9
 internal fun defaultGenerateLabel(ircClient: IrcClient): String {
9
 internal fun defaultGenerateLabel(ircClient: IrcClient): String {
10
     val time = currentTimeProvider().toEpochSecond(ZoneOffset.UTC)
10
     val time = currentTimeProvider().toEpochSecond(ZoneOffset.UTC)
11
-    val counter = ircClient.serverState.labelCounter.incrementAndGet()
11
+    val counter = ircClient.serverState.asyncResponseState.labelCounter.incrementAndGet()
12
     return ByteArray(6) {
12
     return ByteArray(6) {
13
         when {
13
         when {
14
             it < 3 -> (time shr it and 0xff).toByte()
14
             it < 3 -> (time shr it and 0xff).toByte()

+ 3
- 3
src/test/kotlin/com/dmdirc/ktirc/IrcClientImplTest.kt View File

294
         client.socketFactory = mockSocketFactory
294
         client.socketFactory = mockSocketFactory
295
         client.connect()
295
         client.connect()
296
 
296
 
297
-        client.sendAsync(tagMap(), "testing", "123")
297
+        client.sendAsync(tagMap(), "testing", arrayOf("123")) { false }
298
 
298
 
299
         assertLineReceived("testing 123")
299
         assertLineReceived("testing 123")
300
     }
300
     }
307
         client.serverState.capabilities.enabledCapabilities[Capability.LabeledResponse] = ""
307
         client.serverState.capabilities.enabledCapabilities[Capability.LabeledResponse] = ""
308
         client.connect()
308
         client.connect()
309
 
309
 
310
-        client.sendAsync(tagMap(), "testing", "123")
310
+        client.sendAsync(tagMap(), "testing", arrayOf("123")) { false }
311
 
311
 
312
         assertLineReceived("@draft/label=abc123 testing 123")
312
         assertLineReceived("@draft/label=abc123 testing 123")
313
     }
313
     }
320
         client.serverState.capabilities.enabledCapabilities[Capability.LabeledResponse] = ""
320
         client.serverState.capabilities.enabledCapabilities[Capability.LabeledResponse] = ""
321
         client.connect()
321
         client.connect()
322
 
322
 
323
-        client.sendAsync(tagMap(MessageTag.AccountName to "x"), "testing", "123")
323
+        client.sendAsync(tagMap(MessageTag.AccountName to "x"), "testing", arrayOf("123")) { false }
324
 
324
 
325
         assertLineReceived("@account=x;draft/label=abc123 testing 123")
325
         assertLineReceived("@account=x;draft/label=abc123 testing 123")
326
     }
326
     }

+ 7
- 6
src/test/kotlin/com/dmdirc/ktirc/model/ServerStateTest.kt View File

2
 
2
 
3
 import com.dmdirc.ktirc.TestConstants
3
 import com.dmdirc.ktirc.TestConstants
4
 import com.dmdirc.ktirc.events.EventMetadata
4
 import com.dmdirc.ktirc.events.EventMetadata
5
+import com.dmdirc.ktirc.events.IrcEvent
5
 import kotlinx.coroutines.channels.Channel
6
 import kotlinx.coroutines.channels.Channel
6
 import org.junit.jupiter.api.Assertions.*
7
 import org.junit.jupiter.api.Assertions.*
7
 import org.junit.jupiter.api.Test
8
 import org.junit.jupiter.api.Test
65
     fun `indicates labels are enabled when cap is present`() {
66
     fun `indicates labels are enabled when cap is present`() {
66
         val serverState = ServerState("acidBurn", "")
67
         val serverState = ServerState("acidBurn", "")
67
         serverState.capabilities.enabledCapabilities[Capability.LabeledResponse] = ""
68
         serverState.capabilities.enabledCapabilities[Capability.LabeledResponse] = ""
68
-        assertTrue(serverState.supportsLabeledResponses)
69
+        assertTrue(serverState.asyncResponseState.supportsLabeledResponses)
69
     }
70
     }
70
 
71
 
71
     @Test
72
     @Test
72
     fun `indicates labels are not enabled when cap is absent`() {
73
     fun `indicates labels are not enabled when cap is absent`() {
73
         val serverState = ServerState("acidBurn", "")
74
         val serverState = ServerState("acidBurn", "")
74
-        assertFalse(serverState.supportsLabeledResponses)
75
+        assertFalse(serverState.asyncResponseState.supportsLabeledResponses)
75
     }
76
     }
76
 
77
 
77
     @Test
78
     @Test
84
         capabilities.advertisedCapabilities["sasl"] = "sure"
85
         capabilities.advertisedCapabilities["sasl"] = "sure"
85
         sasl.saslBuffer = "in progress"
86
         sasl.saslBuffer = "in progress"
86
         batches["batch"] = Batch("type", emptyList(), EventMetadata(TestConstants.time))
87
         batches["batch"] = Batch("type", emptyList(), EventMetadata(TestConstants.time))
87
-        labelCounter.set(100)
88
-        labelChannels["#thegibson"] = Channel(1)
88
+        asyncResponseState.labelCounter.set(100)
89
+        asyncResponseState.pendingResponses["#thegibson"] = Pair<Channel<IrcEvent>, (IrcEvent) -> Boolean>(Channel(1)) { false }
89
 
90
 
90
         reset()
91
         reset()
91
 
92
 
97
         assertTrue(capabilities.advertisedCapabilities.isEmpty())
98
         assertTrue(capabilities.advertisedCapabilities.isEmpty())
98
         assertEquals("", sasl.saslBuffer)
99
         assertEquals("", sasl.saslBuffer)
99
         assertTrue(batches.isEmpty())
100
         assertTrue(batches.isEmpty())
100
-        assertEquals(0, labelCounter.get())
101
-        assertTrue(labelChannels.isEmpty())
101
+        assertEquals(0, asyncResponseState.labelCounter.get())
102
+        assertTrue(asyncResponseState.pendingResponses.isEmpty())
102
     }
103
     }
103
 
104
 
104
 }
105
 }

+ 4
- 4
src/test/kotlin/com/dmdirc/ktirc/util/LabelsTest.kt View File

18
 
18
 
19
     @Test
19
     @Test
20
     fun `increments the label id when generating labels`() {
20
     fun `increments the label id when generating labels`() {
21
-        assertEquals(0L, fakeServerState.labelCounter.get())
21
+        assertEquals(0L, fakeServerState.asyncResponseState.labelCounter.get())
22
         defaultGenerateLabel(ircClient)
22
         defaultGenerateLabel(ircClient)
23
-        assertEquals(1L, fakeServerState.labelCounter.get())
23
+        assertEquals(1L, fakeServerState.asyncResponseState.labelCounter.get())
24
         defaultGenerateLabel(ircClient)
24
         defaultGenerateLabel(ircClient)
25
-        assertEquals(2L, fakeServerState.labelCounter.get())
25
+        assertEquals(2L, fakeServerState.asyncResponseState.labelCounter.get())
26
     }
26
     }
27
 
27
 
28
     @Test
28
     @Test
36
     fun `generates unique labels at different times with the same counter value`() {
36
     fun `generates unique labels at different times with the same counter value`() {
37
         currentTimeProvider = { TestConstants.time }
37
         currentTimeProvider = { TestConstants.time }
38
         val label1 = defaultGenerateLabel(ircClient)
38
         val label1 = defaultGenerateLabel(ircClient)
39
-        fakeServerState.labelCounter.set(0L)
39
+        fakeServerState.asyncResponseState.labelCounter.set(0L)
40
         currentTimeProvider = { TestConstants.otherTime }
40
         currentTimeProvider = { TestConstants.otherTime }
41
         val label2 = defaultGenerateLabel(ircClient)
41
         val label2 = defaultGenerateLabel(ircClient)
42
         assertNotEquals(label1, label2)
42
         assertNotEquals(label1, label2)

Loading…
Cancel
Save