@tishlang/tish-format 3.10.9 → 3.10.11

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.
@@ -0,0 +1,250 @@
1
+ //! Prepared-statement bookkeeping for the sync `cargo:tish_pg` facade.
2
+ //!
3
+ //! This is the `STATEMENTS` slab from `tish_sync`, extracted as a pure data
4
+ //! structure so its lifecycle semantics are unit-testable without a live
5
+ //! Postgres (`tokio_postgres::Statement` cannot be constructed outside a real
6
+ //! connection, so the registry is generic over the statement type `S`).
7
+ //!
8
+ //! Lifecycle contract (tishlang/tish#712):
9
+ //!
10
+ //! * `get` validates the stored client id — a handle prepared on one client
11
+ //! never resolves against another. `close()` frees CLIENTS slab indices
12
+ //! for reuse, so without this check a stale handle would be replayed
13
+ //! against whichever new connection reused the index.
14
+ //! * `insert` interns on `(client_id, sql)` so repeated prepares of
15
+ //! identical SQL reuse one entry — bounding the table by distinct SQL
16
+ //! text (program text) instead of call count.
17
+ //! * `sweep_client` drops every entry belonging to a closed client,
18
+ //! including its intern-map slice, so a later client that reuses the slab
19
+ //! index starts clean.
20
+ //! * `remove` (the `unprepare` export) frees a single entry.
21
+ //!
22
+ //! Removal hands the statement back to the caller: dropping a
23
+ //! `tokio_postgres::Statement` emits the server-side `DEALLOCATE` (`Close`
24
+ //! message) while its connection is still alive, so "drop what sweep/remove
25
+ //! return" is the deallocation path.
26
+
27
+ use slab::Slab;
28
+ use std::collections::HashMap;
29
+
30
+ pub(crate) struct StatementRegistry<S> {
31
+ /// stmt_id -> (client_id, sql, statement). The SQL text rides along so
32
+ /// `remove`/`sweep_client` can purge the matching intern entry; `get`
33
+ /// clones only the statement, never the SQL, keeping the per-query hot
34
+ /// path allocation-free.
35
+ slab: Slab<(usize, String, S)>,
36
+ /// client_id -> sql -> stmt_id. Nested (rather than keyed on a
37
+ /// `(usize, String)` tuple) so `lookup` borrows the SQL without
38
+ /// allocating and `sweep_client` drops a client's slice in O(1).
39
+ intern: HashMap<usize, HashMap<String, usize>>,
40
+ }
41
+
42
+ impl<S: Clone> StatementRegistry<S> {
43
+ pub(crate) fn new() -> Self {
44
+ Self {
45
+ slab: Slab::new(),
46
+ intern: HashMap::new(),
47
+ }
48
+ }
49
+
50
+ /// Register a statement prepared on `client_id` for `sql`.
51
+ ///
52
+ /// Idempotent per `(client_id, sql)`: if an entry is already interned
53
+ /// its existing handle is returned and `stmt` is discarded — for a
54
+ /// `tokio_postgres::Statement` the drop emits the server-side
55
+ /// `DEALLOCATE`, so losing a race to a concurrent identical `prepare`
56
+ /// leaves nothing pinned on either side.
57
+ pub(crate) fn insert(&mut self, client_id: usize, sql: &str, stmt: S) -> usize {
58
+ if let Some(id) = self.lookup(client_id, sql) {
59
+ return id;
60
+ }
61
+ let id = self.slab.insert((client_id, sql.to_string(), stmt));
62
+ self.intern
63
+ .entry(client_id)
64
+ .or_default()
65
+ .insert(sql.to_string(), id);
66
+ id
67
+ }
68
+
69
+ /// Resolve a statement handle on behalf of `client_id`.
70
+ ///
71
+ /// A mismatch between the stored and presented client id is a MISS:
72
+ /// after `close()` recycles a CLIENTS slab index, a stale statement
73
+ /// handle must not replay against the new connection.
74
+ pub(crate) fn get(&self, stmt_id: usize, client_id: usize) -> Option<S> {
75
+ match self.slab.get(stmt_id) {
76
+ Some((cid, _sql, stmt)) if *cid == client_id => Some(stmt.clone()),
77
+ _ => None,
78
+ }
79
+ }
80
+
81
+ /// Free one statement entry (the `unprepare` export), returning the
82
+ /// statement so the caller can drop it (which sends `DEALLOCATE`).
83
+ pub(crate) fn remove(&mut self, stmt_id: usize) -> Option<S> {
84
+ if !self.slab.contains(stmt_id) {
85
+ return None;
86
+ }
87
+ let (cid, sql, stmt) = self.slab.remove(stmt_id);
88
+ if let Some(per_client) = self.intern.get_mut(&cid) {
89
+ per_client.remove(&sql);
90
+ if per_client.is_empty() {
91
+ self.intern.remove(&cid);
92
+ }
93
+ }
94
+ Some(stmt)
95
+ }
96
+
97
+ /// Drop every entry belonging to `client_id` (the `close()` sweep);
98
+ /// returns how many were removed. The dropped statements emit their
99
+ /// `DEALLOCATE`s if the connection still lives; purging the intern
100
+ /// slice keeps a later client that reuses this index from inheriting
101
+ /// stale handles.
102
+ pub(crate) fn sweep_client(&mut self, client_id: usize) -> usize {
103
+ let before = self.slab.len();
104
+ self.slab.retain(|_, (cid, _sql, _stmt)| *cid != client_id);
105
+ self.intern.remove(&client_id);
106
+ before - self.slab.len()
107
+ }
108
+
109
+ /// Find the interned handle for `(client_id, sql)`, if one exists.
110
+ pub(crate) fn lookup(&self, client_id: usize, sql: &str) -> Option<usize> {
111
+ self.intern.get(&client_id)?.get(sql).copied()
112
+ }
113
+
114
+ #[cfg(test)]
115
+ pub(crate) fn len(&self) -> usize {
116
+ self.slab.len()
117
+ }
118
+ }
119
+
120
+ #[cfg(test)]
121
+ mod tests {
122
+ use super::StatementRegistry;
123
+
124
+ fn s(x: &str) -> String {
125
+ x.to_string()
126
+ }
127
+
128
+ #[test]
129
+ fn get_with_matching_client_id_hits() {
130
+ let mut r = StatementRegistry::new();
131
+ let id = r.insert(3, "SELECT 1", s("stmt-a"));
132
+ assert_eq!(r.get(id, 3), Some(s("stmt-a")));
133
+ }
134
+
135
+ #[test]
136
+ fn get_with_mismatched_client_id_misses() {
137
+ // The stale-replay guard: a handle prepared on client 3 must never
138
+ // resolve for client 4. After close() frees a CLIENTS slab index for
139
+ // reuse, an old statement handle would otherwise be replayed against
140
+ // whichever new connection reused the index.
141
+ let mut r = StatementRegistry::new();
142
+ let id = r.insert(3, "SELECT 1", s("stmt-a"));
143
+ assert_eq!(
144
+ r.get(id, 4),
145
+ None,
146
+ "statement prepared on client 3 must MISS when resolved for client 4"
147
+ );
148
+ }
149
+
150
+ #[test]
151
+ fn close_sweep_removes_only_that_clients_statements() {
152
+ let mut r = StatementRegistry::new();
153
+ let a = r.insert(1, "SELECT 1", s("c1-a"));
154
+ let b = r.insert(1, "SELECT 2", s("c1-b"));
155
+ let c = r.insert(2, "SELECT 1", s("c2-a"));
156
+ let swept = r.sweep_client(1);
157
+ assert_eq!(swept, 2, "both of client 1's statements must be swept");
158
+ assert_eq!(r.get(a, 1), None);
159
+ assert_eq!(r.get(b, 1), None);
160
+ assert_eq!(r.get(c, 2), Some(s("c2-a")), "client 2 must be untouched");
161
+ assert_eq!(r.len(), 1);
162
+ }
163
+
164
+ #[test]
165
+ fn unprepare_removes_entry() {
166
+ let mut r = StatementRegistry::new();
167
+ let id = r.insert(1, "SELECT 1", s("stmt-a"));
168
+ assert_eq!(
169
+ r.remove(id),
170
+ Some(s("stmt-a")),
171
+ "remove must yield the statement so its Drop can DEALLOCATE"
172
+ );
173
+ assert_eq!(r.get(id, 1), None);
174
+ assert_eq!(r.remove(id), None, "double-unprepare is a miss");
175
+ assert_eq!(r.len(), 0);
176
+ }
177
+
178
+ #[test]
179
+ fn reprepare_same_sql_reuses_interned_entry() {
180
+ // prepare() must be idempotent per (client, sql): the misuse pattern
181
+ // of preparing inside a request handler must not grow the table.
182
+ let mut r = StatementRegistry::new();
183
+ let first = r.insert(1, "SELECT 1", s("stmt-a"));
184
+ let second = r.insert(1, "SELECT 1", s("stmt-b"));
185
+ assert_eq!(
186
+ second, first,
187
+ "identical (client, sql) must reuse the entry"
188
+ );
189
+ assert_eq!(r.len(), 1);
190
+ // The original statement wins; the racing duplicate is dropped by the
191
+ // caller (its Drop emits the server-side DEALLOCATE).
192
+ assert_eq!(r.get(first, 1), Some(s("stmt-a")));
193
+ }
194
+
195
+ #[test]
196
+ fn distinct_sql_or_client_gets_distinct_entries() {
197
+ // The intern bound is (client, distinct SQL) — different SQL on the
198
+ // same client, and the same SQL on different clients, each get their
199
+ // own entry.
200
+ let mut r = StatementRegistry::new();
201
+ let a = r.insert(1, "SELECT 1", s("c1-sel1"));
202
+ let b = r.insert(1, "SELECT 2", s("c1-sel2"));
203
+ let c = r.insert(2, "SELECT 1", s("c2-sel1"));
204
+ assert_ne!(a, b);
205
+ assert_ne!(a, c);
206
+ assert_eq!(r.len(), 3);
207
+ assert_eq!(r.get(a, 1), Some(s("c1-sel1")));
208
+ assert_eq!(r.get(b, 1), Some(s("c1-sel2")));
209
+ assert_eq!(r.get(c, 2), Some(s("c2-sel1")));
210
+ }
211
+
212
+ #[test]
213
+ fn lookup_finds_interned_entry() {
214
+ let mut r = StatementRegistry::new();
215
+ assert_eq!(r.lookup(1, "SELECT 1"), None);
216
+ let id = r.insert(1, "SELECT 1", s("stmt-a"));
217
+ assert_eq!(r.lookup(1, "SELECT 1"), Some(id));
218
+ assert_eq!(r.lookup(2, "SELECT 1"), None, "interning is per client");
219
+ assert_eq!(r.lookup(1, "SELECT 2"), None, "interning is per SQL text");
220
+ }
221
+
222
+ #[test]
223
+ fn sweep_purges_intern_state_for_reused_client_index() {
224
+ // After close(client 0), a NEW client that reuses slab index 0 must
225
+ // not inherit the old client's statements or interned handles.
226
+ let mut r = StatementRegistry::new();
227
+ r.insert(0, "SELECT 1", s("old-stmt"));
228
+ r.sweep_client(0);
229
+ assert_eq!(
230
+ r.lookup(0, "SELECT 1"),
231
+ None,
232
+ "a reused client index must re-prepare, not inherit the old client's intern entry"
233
+ );
234
+ assert_eq!(r.len(), 0, "the closed client's statements must be gone");
235
+ let new_id = r.insert(0, "SELECT 1", s("new-stmt"));
236
+ assert_eq!(r.get(new_id, 0), Some(s("new-stmt")));
237
+ }
238
+
239
+ #[test]
240
+ fn unprepare_then_reprepare_gets_fresh_entry() {
241
+ // remove() must purge the intern entry too, or a later prepare of
242
+ // the same SQL would return a dangling handle.
243
+ let mut r = StatementRegistry::new();
244
+ let id = r.insert(1, "SELECT 1", s("stmt-a"));
245
+ r.remove(id);
246
+ assert_eq!(r.lookup(1, "SELECT 1"), None);
247
+ let id2 = r.insert(1, "SELECT 1", s("stmt-b"));
248
+ assert_eq!(r.get(id2, 1), Some(s("stmt-b")));
249
+ }
250
+ }
@@ -64,6 +64,25 @@ where
64
64
  })
65
65
  }
66
66
 
67
+ /// Runtime hosting settlement-subscription waiters registered by `Promise.race` /
68
+ /// `Promise.any` / `Promise.allSettled` on oneshot-backed fetch/IO promises
69
+ /// (`TishPromise::subscribe`, issue #702). Process-global and immortal ON PURPOSE:
70
+ /// a subscription must be able to outlive the subscribing thread (the promise may
71
+ /// settle long after the racer moved on), so it cannot live on the thread-local
72
+ /// [`RUNTIME`]. One lazily-created worker thread total, shared by every waiter —
73
+ /// never one per pending input.
74
+ pub(crate) fn subscriber_runtime() -> &'static Runtime {
75
+ static RT: OnceLock<Runtime> = OnceLock::new();
76
+ RT.get_or_init(|| {
77
+ tokio::runtime::Builder::new_multi_thread()
78
+ .worker_threads(1)
79
+ .thread_name("tish-promise-subscriber")
80
+ .enable_all()
81
+ .build()
82
+ .expect("Failed to create promise subscriber runtime")
83
+ })
84
+ }
85
+
67
86
  pub fn await_fetch(args: Vec<Value>) -> Value {
68
87
  crate::promise::await_promise(crate::native_promise::fetch_promise(args))
69
88
  }
@@ -15,24 +15,54 @@ use crate::http::{
15
15
  MultipartPart,
16
16
  };
17
17
 
18
- // --- Promises (Send payloads only; Value built on awaiting thread) ---
18
+ // --- Promises (Send payloads only; Value built on awaiting/settling thread) ---
19
+ //
20
+ // These are one-shot by construction (the oneshot receiver is taken by the first
21
+ // await OR the first race subscription). `subscribe` awaits the oneshot as a task
22
+ // on the shared subscriber runtime instead of parking a blocking OS thread (plus
23
+ // a per-call tokio runtime) per raced input — issue #702.
24
+
25
+ fn consumed() -> Value {
26
+ Value::String("Promise already consumed".into())
27
+ }
19
28
 
20
29
  struct FetchResponsePromise {
21
30
  rx: Mutex<Option<tokio::sync::oneshot::Receiver<Result<reqwest::Response, String>>>>,
22
31
  }
23
32
 
33
+ impl FetchResponsePromise {
34
+ fn convert(
35
+ r: Result<Result<reqwest::Response, String>, tokio::sync::oneshot::error::RecvError>,
36
+ ) -> std::result::Result<Value, Value> {
37
+ match r {
38
+ Ok(Ok(resp)) => Ok(response_value_from_reqwest(resp)),
39
+ Ok(Err(e)) => Ok(build_error_response(&e)),
40
+ Err(_) => Err(Value::String("Promise dropped".into())),
41
+ }
42
+ }
43
+ }
44
+
24
45
  impl TishPromise for FetchResponsePromise {
25
46
  fn block_until_settled(&self) -> std::result::Result<Value, Value> {
26
47
  let rx = self.rx.lock().unwrap().take();
27
48
  if let Some(rx) = rx {
28
- let r = crate::http::block_on_http(rx);
29
- match r {
30
- Ok(Ok(resp)) => Ok(response_value_from_reqwest(resp)),
31
- Ok(Err(e)) => Ok(build_error_response(&e)),
32
- Err(_) => Err(Value::String("Promise dropped".into())),
33
- }
49
+ Self::convert(crate::http::block_on_http(rx))
34
50
  } else {
35
- Err(Value::String("Promise already consumed".into()))
51
+ Err(consumed())
52
+ }
53
+ }
54
+
55
+ fn subscribe(
56
+ self: Arc<Self>,
57
+ on_settled: Box<dyn FnOnce(std::result::Result<Value, Value>) + Send>,
58
+ ) {
59
+ match self.rx.lock().unwrap().take() {
60
+ Some(rx) => {
61
+ crate::http::subscriber_runtime().spawn(async move {
62
+ on_settled(Self::convert(rx.await));
63
+ });
64
+ }
65
+ None => on_settled(Err(consumed())),
36
66
  }
37
67
  }
38
68
  }
@@ -46,27 +76,52 @@ struct FetchAllResponsesPromise {
46
76
  >,
47
77
  }
48
78
 
79
+ impl FetchAllResponsesPromise {
80
+ #[allow(clippy::type_complexity)]
81
+ fn convert(
82
+ r: Result<
83
+ Result<Vec<Result<reqwest::Response, String>>, String>,
84
+ tokio::sync::oneshot::error::RecvError,
85
+ >,
86
+ ) -> std::result::Result<Value, Value> {
87
+ match r {
88
+ Ok(Ok(vec)) => {
89
+ let out: Vec<Value> = vec
90
+ .into_iter()
91
+ .map(|x| {
92
+ x.map(response_value_from_reqwest)
93
+ .unwrap_or_else(|e| build_error_response(&e))
94
+ })
95
+ .collect();
96
+ Ok(Value::Array(VmRef::new(out)))
97
+ }
98
+ Ok(Err(e)) => Ok(build_error_response(&e)),
99
+ Err(_) => Err(Value::String("Promise dropped".into())),
100
+ }
101
+ }
102
+ }
103
+
49
104
  impl TishPromise for FetchAllResponsesPromise {
50
105
  fn block_until_settled(&self) -> std::result::Result<Value, Value> {
51
106
  let rx = self.rx.lock().unwrap().take();
52
107
  if let Some(rx) = rx {
53
- let r = crate::http::block_on_http(rx);
54
- match r {
55
- Ok(Ok(vec)) => {
56
- let out: Vec<Value> = vec
57
- .into_iter()
58
- .map(|x| {
59
- x.map(response_value_from_reqwest)
60
- .unwrap_or_else(|e| build_error_response(&e))
61
- })
62
- .collect();
63
- Ok(Value::Array(VmRef::new(out)))
64
- }
65
- Ok(Err(e)) => Ok(build_error_response(&e)),
66
- Err(_) => Err(Value::String("Promise dropped".into())),
67
- }
108
+ Self::convert(crate::http::block_on_http(rx))
68
109
  } else {
69
- Err(Value::String("Promise already consumed".into()))
110
+ Err(consumed())
111
+ }
112
+ }
113
+
114
+ fn subscribe(
115
+ self: Arc<Self>,
116
+ on_settled: Box<dyn FnOnce(std::result::Result<Value, Value>) + Send>,
117
+ ) {
118
+ match self.rx.lock().unwrap().take() {
119
+ Some(rx) => {
120
+ crate::http::subscriber_runtime().spawn(async move {
121
+ on_settled(Self::convert(rx.await));
122
+ });
123
+ }
124
+ None => on_settled(Err(consumed())),
70
125
  }
71
126
  }
72
127
  }
@@ -80,34 +135,55 @@ struct ReadChunkPromise {
80
135
  rx: Mutex<Option<tokio::sync::oneshot::Receiver<Result<ReadChunk, String>>>>,
81
136
  }
82
137
 
138
+ impl ReadChunkPromise {
139
+ fn convert(
140
+ r: Result<Result<ReadChunk, String>, tokio::sync::oneshot::error::RecvError>,
141
+ ) -> std::result::Result<Value, Value> {
142
+ match r {
143
+ Ok(Ok(ReadChunk::Done)) => {
144
+ let mut o = ObjectMap::default();
145
+ o.insert(Arc::from("done"), Value::Bool(true));
146
+ o.insert(Arc::from("value"), Value::Null);
147
+ Ok(Value::object(o))
148
+ }
149
+ Ok(Ok(ReadChunk::Bytes(b))) => {
150
+ let arr: Vec<Value> = b.iter().map(|u| Value::Number(*u as f64)).collect();
151
+ let mut o = ObjectMap::default();
152
+ o.insert(Arc::from("done"), Value::Bool(false));
153
+ o.insert(Arc::from("value"), Value::Array(VmRef::new(arr)));
154
+ Ok(Value::object(o))
155
+ }
156
+ Ok(Err(e)) => Err({
157
+ let mut obj = ObjectMap::default();
158
+ obj.insert(Arc::from("error"), Value::String(e.into()));
159
+ Value::object(obj)
160
+ }),
161
+ Err(_) => Err(Value::String("Promise dropped".into())),
162
+ }
163
+ }
164
+ }
165
+
83
166
  impl TishPromise for ReadChunkPromise {
84
167
  fn block_until_settled(&self) -> std::result::Result<Value, Value> {
85
168
  let rx = self.rx.lock().unwrap().take();
86
169
  if let Some(rx) = rx {
87
- let r = crate::http::block_on_http(rx);
88
- match r {
89
- Ok(Ok(ReadChunk::Done)) => {
90
- let mut o = ObjectMap::default();
91
- o.insert(Arc::from("done"), Value::Bool(true));
92
- o.insert(Arc::from("value"), Value::Null);
93
- Ok(Value::object(o))
94
- }
95
- Ok(Ok(ReadChunk::Bytes(b))) => {
96
- let arr: Vec<Value> = b.iter().map(|u| Value::Number(*u as f64)).collect();
97
- let mut o = ObjectMap::default();
98
- o.insert(Arc::from("done"), Value::Bool(false));
99
- o.insert(Arc::from("value"), Value::Array(VmRef::new(arr)));
100
- Ok(Value::object(o))
101
- }
102
- Ok(Err(e)) => Err({
103
- let mut obj = ObjectMap::default();
104
- obj.insert(Arc::from("error"), Value::String(e.into()));
105
- Value::object(obj)
106
- }),
107
- Err(_) => Err(Value::String("Promise dropped".into())),
108
- }
170
+ Self::convert(crate::http::block_on_http(rx))
109
171
  } else {
110
- Err(Value::String("Promise already consumed".into()))
172
+ Err(consumed())
173
+ }
174
+ }
175
+
176
+ fn subscribe(
177
+ self: Arc<Self>,
178
+ on_settled: Box<dyn FnOnce(std::result::Result<Value, Value>) + Send>,
179
+ ) {
180
+ match self.rx.lock().unwrap().take() {
181
+ Some(rx) => {
182
+ crate::http::subscriber_runtime().spawn(async move {
183
+ on_settled(Self::convert(rx.await));
184
+ });
185
+ }
186
+ None => on_settled(Err(consumed())),
111
187
  }
112
188
  }
113
189
  }
@@ -116,29 +192,47 @@ struct JsonTextPromise {
116
192
  rx: Mutex<Option<tokio::sync::oneshot::Receiver<Result<String, String>>>>,
117
193
  }
118
194
 
195
+ impl JsonTextPromise {
196
+ fn convert(
197
+ r: Result<Result<String, String>, tokio::sync::oneshot::error::RecvError>,
198
+ ) -> std::result::Result<Value, Value> {
199
+ fn error_obj(e: String) -> Value {
200
+ let mut obj = ObjectMap::default();
201
+ obj.insert(Arc::from("error"), Value::String(e.into()));
202
+ Value::object(obj)
203
+ }
204
+ match r {
205
+ Ok(Ok(s)) => match tishlang_core::json_parse(&s) {
206
+ Ok(v) => Ok(v),
207
+ Err(e) => Err(error_obj(e)),
208
+ },
209
+ Ok(Err(e)) => Err(error_obj(e)),
210
+ Err(_) => Err(Value::String("Promise dropped".into())),
211
+ }
212
+ }
213
+ }
214
+
119
215
  impl TishPromise for JsonTextPromise {
120
216
  fn block_until_settled(&self) -> std::result::Result<Value, Value> {
121
217
  let rx = self.rx.lock().unwrap().take();
122
218
  if let Some(rx) = rx {
123
- let r = crate::http::block_on_http(rx);
124
- match r {
125
- Ok(Ok(s)) => match tishlang_core::json_parse(&s) {
126
- Ok(v) => Ok(v),
127
- Err(e) => Err({
128
- let mut obj = ObjectMap::default();
129
- obj.insert(Arc::from("error"), Value::String(e.into()));
130
- Value::object(obj)
131
- }),
132
- },
133
- Ok(Err(e)) => Err({
134
- let mut obj = ObjectMap::default();
135
- obj.insert(Arc::from("error"), Value::String(e.into()));
136
- Value::object(obj)
137
- }),
138
- Err(_) => Err(Value::String("Promise dropped".into())),
139
- }
219
+ Self::convert(crate::http::block_on_http(rx))
140
220
  } else {
141
- Err(Value::String("Promise already consumed".into()))
221
+ Err(consumed())
222
+ }
223
+ }
224
+
225
+ fn subscribe(
226
+ self: Arc<Self>,
227
+ on_settled: Box<dyn FnOnce(std::result::Result<Value, Value>) + Send>,
228
+ ) {
229
+ match self.rx.lock().unwrap().take() {
230
+ Some(rx) => {
231
+ crate::http::subscriber_runtime().spawn(async move {
232
+ on_settled(Self::convert(rx.await));
233
+ });
234
+ }
235
+ None => on_settled(Err(consumed())),
142
236
  }
143
237
  }
144
238
  }