1use crate::acl;
15use crate::channel::Channel;
16use crate::state::{ChannelId, ConnState, CorrelationContext, ImState, SendStatus, TemporaryId};
17use helix_core::effect::TimerId;
18use helix_core::{CoreError, EffectSink, Module, Tick};
19
20mod chain;
21mod trait_impl;
22
23pub const RUNTIME_IDENTITY_COMMAND: &str = "__helix_runtime_identity";
28
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
31pub enum ClientPlatform {
32 #[default]
33 Native,
34 Ffi,
35 Web,
36}
37
38impl ClientPlatform {
39 pub const fn as_str(self) -> &'static str {
41 match self {
42 Self::Native => "native",
43 Self::Ffi => "ffi",
44 Self::Web => "web",
45 }
46 }
47}
48
49pub struct ImConfig {
51 pub ws_url: String,
53 pub api_base_url: String,
55 pub default_api_base_url: String,
59 pub auth_user_id: String,
61 pub company_id: String,
63 pub user_name: String,
64 pub org_name: String,
65 pub dept_name: String,
66 pub client_platform: ClientPlatform,
68 pub ping_interval_ms: u64,
69 pub send_timeout_ms: u64,
70 pub offline_sync_diagnostics: crate::sync::SyncDiagnosticsConfig,
72}
73
74impl ImConfig {
75 pub fn user_identity(&self) -> crate::outbound::send_build::UserIdentity<'_> {
77 crate::outbound::send_build::UserIdentity {
78 user_id: &self.auth_user_id,
79 team_id: &self.company_id,
80 user_name: &self.user_name,
81 org_name: &self.org_name,
82 dept_name: &self.dept_name,
83 }
84 }
85}
86
87impl Default for ImConfig {
88 fn default() -> Self {
89 Self {
90 ws_url: String::new(),
91 api_base_url: String::new(),
92 default_api_base_url: String::new(),
93 auth_user_id: String::new(),
94 company_id: String::new(),
95 user_name: String::new(),
96 org_name: String::new(),
97 dept_name: String::new(),
98 client_platform: ClientPlatform::Native,
99 ping_interval_ms: 8_000,
100 send_timeout_ms: 15_000,
101 offline_sync_diagnostics: crate::sync::SyncDiagnosticsConfig::disabled(),
102 }
103 }
104}
105
106pub struct ImModule {
108 pub(crate) config: ImConfig,
109 pub(crate) render_scope: crate::render_scope::RenderScope,
110 pub(crate) scope_hydration: crate::scope_hydration::ScopeHydration,
111 pub(crate) sync_timing: crate::sync_observation::Timing,
112 pub(crate) diagnostics: crate::diagnostics::Session,
113 pub(crate) state: ImState,
114 pub(crate) local_store_mode: crate::query::LocalStoreMode,
116 next_corr: u64,
121 next_timer: u64,
125 next_temporary_id: u64,
127}
128
129impl ImModule {
130 #[doc(hidden)]
132 pub fn queue_message_v3_commit(
133 &mut self,
134 ops: Vec<helix_core::effect::StorageOp>,
135 terminal_events: Vec<crate::event::MessageV3Event>,
136 out: &mut EffectSink,
137 ) -> helix_core::Correlation {
138 let corr = self.alloc_corr_internal();
139 self.render_scope.stage_ops(corr, &ops);
140 let terminal_events = terminal_events
141 .into_iter()
142 .map(crate::event::MessageV3Event::into_bytes)
143 .collect();
144 self.state.corr_map.insert(
145 corr,
146 CorrelationContext::MessageV3Commit { terminal_events },
147 );
148 out.push(helix_core::Effect::PersistAtomic { corr, ops });
149 corr
150 }
151
152 pub fn new(config: ImConfig) -> Self {
154 Self::new_with_local_store(config, crate::query::LocalStoreMode::Durable)
155 }
156
157 pub fn new_with_local_store(
159 config: ImConfig,
160 local_store_mode: crate::query::LocalStoreMode,
161 ) -> Self {
162 Self {
163 config,
164 render_scope: Default::default(),
165 scope_hydration: Default::default(),
166 diagnostics: crate::diagnostics::Session::default(),
167 sync_timing: Default::default(),
168 state: ImState::new(),
169 local_store_mode,
170 next_corr: 1,
171 next_timer: 100,
172 next_temporary_id: 1,
173 }
174 }
175
176 pub fn local_store_mode(&self) -> crate::query::LocalStoreMode {
177 self.local_store_mode
178 }
179
180 pub fn alloc_corr(&mut self) -> helix_core::Correlation {
181 let c = helix_core::Correlation::from_raw(self.next_corr);
182 self.next_corr += 1;
183 c
184 }
185
186 pub fn alloc_timer(&mut self) -> TimerId {
187 let t = TimerId::from_raw(self.next_timer);
188 self.next_timer += 1;
189 t
190 }
191
192 pub(crate) fn alloc_temporary_id(
193 &mut self,
194 now_ms: u64,
195 ) -> Result<TemporaryId, crate::error::ImError> {
196 let sequence = self.next_temporary_id;
197 self.next_temporary_id = sequence.checked_add(1).ok_or_else(|| {
198 crate::error::ImError::Parse("temporary_id sequence exhausted".to_string())
199 })?;
200 Ok(TemporaryId::mint(now_ms, sequence))
201 }
202
203 pub fn register_channel(&mut self, id: ChannelId, initial_cursor: u64) {
205 self.state
206 .channels
207 .insert(id, Channel::new(id, initial_cursor));
208 }
209
210 pub fn cursor_for(&self, channel_id: ChannelId) -> Option<u64> {
212 self.state
213 .channels
214 .get(&channel_id)
215 .map(|ch| ch.cursor.value().0)
216 }
217
218 pub fn terminal_event_seq_for(&self, channel_id: ChannelId) -> Option<u64> {
220 self.state
221 .channels
222 .get(&channel_id)
223 .and_then(|channel| channel.terminal_event_seq())
224 .map(|seq| seq.0)
225 }
226
227 pub fn pending_send_count(&self) -> usize {
229 self.state.pending_sends.len()
230 }
231
232 pub fn chain_mutation_state(
234 &self,
235 client_mutation_id: &str,
236 ) -> Option<crate::chain::ChainMutationState> {
237 self.state
238 .chain_mutations
239 .get(client_mutation_id)
240 .map(|mutation| mutation.state)
241 }
242
243 pub fn chain_operation_id(&self, client_mutation_id: &str) -> Option<&str> {
245 self.state
246 .chain_mutations
247 .get(client_mutation_id)
248 .map(|mutation| mutation.operation_id.as_str())
249 }
250
251 pub fn sync_inflight(&self) -> usize {
253 self.state.sync_scheduler.inflight()
254 }
255 pub fn sync_pending_len(&self) -> usize {
256 self.state.sync_scheduler.pending_len()
257 }
258
259 pub fn pending_send_status(&self, temporary_id: &str) -> Option<SendStatus> {
262 self.state
263 .pending_sends
264 .get(&TemporaryId(temporary_id.to_string()))
265 .map(|ps| ps.status)
266 }
267
268 pub fn increment_fetched_contains(&self, channel_id: ChannelId) -> bool {
270 self.state.increment_fetched.contains(&channel_id)
271 }
272
273 pub fn increment_target_for(&self, channel_id: ChannelId) -> Option<u64> {
276 self.state
277 .increment_target
278 .get(&channel_id)
279 .map(|seq| seq.0)
280 }
281
282 pub fn ingest_increment_end(&mut self, ch: Option<ChannelId>, out: &mut EffectSink) {
285 let start = out.as_slice().len();
286 let api_base_url = self.config.api_base_url.as_str();
287 let auth_user_id = self.config.auth_user_id.as_str();
288 let next_corr = &mut self.next_corr;
289 let mut alloc_corr = || {
290 let corr = helix_core::Correlation::from_raw(*next_corr);
291 *next_corr += 1;
292 corr
293 };
294 let mut ctx = crate::ws::ImWsContext::new(
295 &mut self.state,
296 0,
297 api_base_url,
298 auth_user_id,
299 &mut alloc_corr,
300 );
301 crate::ws::handlers::increment_channel_end::apply_increment_end(&mut ctx, ch, out);
302 self.render_scope.guard_effects(&self.config, start, out);
303 }
304
305 pub(crate) fn alloc_corr_internal(&mut self) -> helix_core::Correlation {
307 let c = helix_core::Correlation::from_raw(self.next_corr);
308 self.next_corr += 1;
309 c
310 }
311
312 pub(crate) fn with_state_and_corr_allocator<R>(
314 &mut self,
315 f: impl FnOnce(&mut ImState, &mut dyn FnMut() -> helix_core::Correlation) -> R,
316 ) -> R {
317 let next_corr = &mut self.next_corr;
318 let mut alloc_corr = || {
319 let corr = helix_core::Correlation::from_raw(*next_corr);
320 *next_corr += 1;
321 corr
322 };
323 f(&mut self.state, &mut alloc_corr)
324 }
325
326 fn dispatch_ws_frame(
331 &mut self,
332 frame: &crate::ws::WsFrame,
333 now_ms: u64,
334 out: &mut EffectSink,
335 ) -> Result<(), CoreError> {
336 self.diagnose_inbound(frame);
337 let module_name = self.name();
338 let api_base_url = self.config.api_base_url.as_str();
339 let auth_user_id = self.config.auth_user_id.as_str();
340 let hello_scope = (frame.action().ok() == Some("hello"))
343 .then(|| self.render_scope.scoped_channel_ids(&self.config));
344 let next_corr = &mut self.next_corr;
345 let mut alloc_corr = || {
346 let corr = helix_core::Correlation::from_raw(*next_corr);
347 *next_corr += 1;
348 corr
349 };
350 let (result, timeline_refreshes) = {
351 let mut ctx = crate::ws::ImWsContext::new(
352 &mut self.state,
353 now_ms,
354 api_base_url,
355 auth_user_id,
356 &mut alloc_corr,
357 );
358 if let Some(channel_ids) = hello_scope.as_deref() {
359 ctx.set_scoped_channel_ids(channel_ids);
360 }
361 let result = crate::ws::dispatch_ws(&mut ctx, frame, out);
362 let timeline_refreshes = ctx.take_attached_timeline_refreshes();
363 (result, timeline_refreshes)
364 };
365 result.map_err(|e| CoreError::ModuleError {
366 module: module_name,
367 source: Box::new(e),
368 })?;
369
370 for (channel_id, causation_id) in timeline_refreshes {
375 self.refresh_attached_latest_timeline(channel_id, causation_id, out)
376 .map_err(|e| CoreError::ModuleError {
377 module: module_name,
378 source: Box::new(e),
379 })?;
380 }
381 Ok(())
382 }
383}