1use std::any::Any;
13use std::cell::RefCell;
14use std::collections::HashMap;
15use std::rc::Rc;
16
17use tilo_actions::{Action as _, ActionContext, ActionError, ActionInvoker, ChangeKind};
18use tilo_resource::{Invoke, Resource};
19use ui_core::LocalState;
20use ui_core::history_mode::HistoryOwner;
21use ui_core::permissions::{LivePermissions, PermissionScope, TreePath};
22use ui_core::record::StoredRecord;
23use ui_core::record::workspace::WorkspaceRecords;
24use ui_core::wake::WakeHandle;
25
26use super::live::Live;
27
28use super::chain::{Chain, Chained};
29
30pub struct WorkspaceRecord<P: Resource> {
33 shared: Rc<Shared<P>>,
34}
35
36impl<P: Resource> Clone for WorkspaceRecord<P> {
37 fn clone(&self) -> Self {
38 Self {
39 shared: Rc::clone(&self.shared),
40 }
41 }
42}
43
44struct Shared<P: Resource> {
45 record: StoredRecord,
46 owner: &'static str,
48 app_id: &'static str,
49 app_title: &'static str,
50 me: String,
51 permissions: Option<LivePermissions>,
52 live: Live<P>,
54 state: LocalState<Rc<P::State>>,
57 wake: Option<WakeHandle>,
59 waiting: RefCell<Vec<Waiting<P>>>,
62}
63
64struct Waiting<P: Resource> {
67 action: P::Action,
68 then: Box<dyn FnOnce(Result<(), ActionError>)>,
69 woken: u8,
73}
74
75const WAITING_WAKES: u8 = 4;
77
78enum Applied {
80 Done(Result<(), ActionError>),
81 Waits,
83}
84
85impl<P: Resource> Shared<P> {
86 fn refresh(&self) -> bool {
88 self.apply_waiting();
89 let moved = match self.record.read(|doc| self.live.sync(doc)) {
90 Some(Ok(moved)) => moved,
91 Some(Err(error)) => {
92 tracing::warn!(kind = P::KIND, %error, "workspace record unreadable");
93 false
94 }
95 None => false,
96 };
97 let _ = self.live.take_change();
98 if moved {
99 self.state.set(self.live.get());
100 }
101 moved
102 }
103
104 fn apply_waiting(&self) {
108 let Ok(mut waiting) = self.waiting.try_borrow_mut() else {
109 return;
110 };
111 if waiting.is_empty() {
112 return;
113 }
114 let queue = std::mem::take(&mut *waiting);
115 drop(waiting);
116 let mut left = Vec::new();
117 for entry in queue {
118 if !left.is_empty() {
120 left.push(entry);
121 continue;
122 }
123 match self.apply(&entry.action) {
124 Applied::Done(result) => (entry.then)(result),
125 Applied::Waits => left.push(entry),
126 }
127 }
128 let Some(first) = left.first_mut() else {
129 return;
130 };
131 let wake = first.woken < WAITING_WAKES;
132 if wake {
133 first.woken = first.woken.saturating_add(1);
134 }
135 let mut waiting = self.waiting.borrow_mut();
136 left.append(&mut waiting);
137 *waiting = left;
138 drop(waiting);
139 if wake && let Some(window) = &self.wake {
140 window.wake();
141 }
142 }
143
144 fn apply(&self, action: &P::Action) -> Applied {
145 let encoded = match tilo_resource::encode::<P>(action) {
146 Ok(encoded) => encoded,
147 Err(error) => return Applied::Done(Err(error)),
148 };
149 let change = action.change_kind();
150 let context = ActionContext::for_resource(P::KIND, P::KIND, P::LABEL)
151 .with_app(self.app_id, self.app_title)
152 .with_invoker(ActionInvoker::actor(&self.me))
153 .with_change(change);
154 let edited = self.record.try_edit(change == ChangeKind::Content, |doc| {
155 let writer = ui_core::history_source::authors::device_actor(&self.me, doc.get_actor());
156 tilo_actions::apply_local_with_context(doc, action, encoded, Some(&writer), context)
157 .map(|_| ())
158 .map_err(|error| error.to_string())
159 });
160 match edited {
161 Ok(result) => Applied::Done(result.map_err(ActionError::Refused)),
162 Err(unavailable) if unavailable.is_momentary() => Applied::Waits,
163 Err(unavailable) => Applied::Done(Err(ActionError::Refused(unavailable.to_string()))),
164 }
165 }
166}
167
168impl<P: Resource> WorkspaceRecord<P> {
169 pub fn open(
178 records: &WorkspaceRecords,
179 owner: &'static str,
180 app: (&'static str, &'static str),
181 permissions: Option<LivePermissions>,
182 wake: Option<WakeHandle>,
183 ) -> Option<Self> {
184 let opened = records.open_record(owner, P::KIND, &|doc| {
185 P::initialize(doc).map_err(|error| error.to_string())
186 });
187 Self::hold(
188 opened,
189 owner,
190 app,
191 permissions,
192 wake,
193 records.author().0.clone(),
194 )
195 }
196
197 pub fn open_at(
203 records: &WorkspaceRecords,
204 owner: &'static str,
205 id: &str,
206 app: (&'static str, &'static str),
207 permissions: Option<LivePermissions>,
208 wake: Option<WakeHandle>,
209 ) -> Option<Self> {
210 let opened = records.open_record_at(owner, id, P::KIND, &|doc| {
211 P::initialize(doc).map_err(|error| error.to_string())
212 });
213 Self::hold(
214 opened,
215 owner,
216 app,
217 permissions,
218 wake,
219 records.author().0.clone(),
220 )
221 }
222
223 pub fn open_primary(
230 records: &WorkspaceRecords,
231 owner: &'static str,
232 app: (&'static str, &'static str),
233 permissions: Option<LivePermissions>,
234 wake: Option<WakeHandle>,
235 ) -> Option<Self> {
236 let opened = records.open_primary(owner, P::KIND, &|doc| {
237 P::initialize(doc).map_err(|error| error.to_string())
238 });
239 Self::hold(
240 opened,
241 owner,
242 app,
243 permissions,
244 wake,
245 records.author().0.clone(),
246 )
247 }
248
249 fn hold(
253 opened: Result<StoredRecord, ui_core::record::RecordOpenError>,
254 owner: &'static str,
255 app: (&'static str, &'static str),
256 permissions: Option<LivePermissions>,
257 wake: Option<WakeHandle>,
258 me: String,
259 ) -> Option<Self> {
260 let record = opened
261 .map_err(|error| tracing::warn!(kind = P::KIND, %error, "workspace record unavailable"))
262 .ok()?;
263 if record.read(tilo_resource::evolution::needs_migrate::<P>)?
264 && let Err(error) = record.edit(false, |doc| {
265 tilo_resource::evolution::migrate::<P>(doc).map_err(|error| error.to_string())
266 })
267 {
268 tracing::warn!(kind = P::KIND, %error, "workspace record not migrated");
269 }
270 let reader = record
271 .read(tilo_resource::Reader::<P>::read)?
272 .map_err(|error| tracing::warn!(kind = P::KIND, %error, "workspace record unreadable"))
273 .ok()?;
274 if let Some(wake) = &wake {
275 record.wake_on_arrival(wake.clone());
276 }
277 let live = Live::new(reader);
278 Some(Self {
279 shared: Rc::new(Shared::<P> {
280 record,
281 owner,
282 app_id: app.0,
283 app_title: app.1,
284 me,
285 permissions,
286 state: LocalState::new(live.get()),
287 live,
288 wake,
289 waiting: RefCell::default(),
290 }),
291 })
292 }
293
294 #[must_use]
296 pub fn id(&self) -> &ui_core::record::RecordId {
297 &self.shared.record.checkout().id
298 }
299
300 pub(crate) fn live(&self) -> Live<P> {
303 self.shared.live.clone()
304 }
305
306 pub fn get(&self) -> Rc<P::State> {
308 self.shared.refresh();
309 self.shared.state.with(Rc::clone)
310 }
311
312 pub fn revision(&self) -> u64 {
314 self.shared.refresh();
315 self.shared.live.revision()
316 }
317
318 pub fn me(&self) -> &str {
320 &self.shared.me
321 }
322
323 pub fn may(&self, verb: &str) -> bool {
327 let Some(permissions) = &self.shared.permissions else {
328 return true;
329 };
330 let Some(declared) = P::declarations().into_iter().find(|decl| decl.id == verb) else {
331 return false;
332 };
333 permissions.check(&PermissionScope::Action {
334 node: tilo_actions::action_node(
335 &TreePath::root(),
336 self.shared.owner,
337 P::KIND,
338 self.shared.record.checkout().id.as_str(),
339 declared.field,
340 verb,
341 ),
342 kind: declared.kind,
343 })
344 }
345
346 pub fn invoke(&self, invoke: &Invoke<P>) -> Result<(), ActionError> {
355 let action = self.admit(invoke)?;
356 match self.shared.apply(&action) {
357 Applied::Done(result) => {
358 self.shared.refresh();
359 result
360 }
361 Applied::Waits => Err(ActionError::Refused(
362 ui_core::record::HistoryUnavailable::StoreInUse.to_string(),
363 )),
364 }
365 }
366
367 pub fn invoke_then(
373 &self,
374 invoke: &Invoke<P>,
375 then: impl FnOnce(Result<(), ActionError>) + 'static,
376 ) {
377 let action = match self.admit(invoke) {
378 Ok(action) => action,
379 Err(error) => return then(Err(error)),
380 };
381 let shared = &self.shared;
382 let first = shared
383 .waiting
384 .try_borrow()
385 .is_ok_and(|waiting| waiting.is_empty());
386 if first && let Applied::Done(result) = shared.apply(&action) {
387 shared.refresh();
388 return then(result);
389 }
390 shared.waiting.borrow_mut().push(Waiting {
391 action,
392 then: Box::new(then),
393 woken: 0,
394 });
395 if let Some(window) = &shared.wake {
396 window.wake();
397 }
398 }
399
400 fn admit(&self, invoke: &Invoke<P>) -> Result<P::Action, ActionError> {
402 let action = invoke
403 .build(tilo_resource::origin_now_ms())
404 .map_err(ActionError::Refused)?;
405 if let Some(verb) = verb_of::<P>(&action)
406 && !self.may(&verb)
407 {
408 return Err(ActionError::Refused(format!(
409 "you may not change the workspace's {}",
410 P::LABEL.to_lowercase()
411 )));
412 }
413 Ok(action)
414 }
415
416 #[must_use]
419 pub fn record(&self) -> &StoredRecord {
420 &self.shared.record
421 }
422}
423
424fn verb_of<P: Resource>(action: &P::Action) -> Option<String> {
427 let mut doc = ui_core::automerge::AutoCommit::new();
428 P::initialize(&mut doc).ok()?;
429 match action.describe(&doc)?.what {
430 tilo_actions::Did::Verb(call) => Some(call.action.action),
431 _ => None,
432 }
433}
434
435#[derive(Clone)]
439pub struct WorkspaceHandle {
440 state: Rc<WorkspaceState>,
441}
442
443impl WorkspaceHandle {
444 pub fn record<P: Resource>(&self, owner: &'static str, id: &str) -> Option<WorkspaceRecord<P>> {
448 self.state.open_at::<P>(owner, id)
449 }
450
451 #[must_use]
454 pub fn holds_record<P: Resource>(&self, owner: &'static str, id: &str) -> bool {
455 self.state.holds_record(owner, id, P::KIND)
456 }
457
458 pub fn chain_beside<P: Chained>(
463 &self,
464 owner: &'static str,
465 base: &WorkspaceRecord<P::Base>,
466 ) -> Option<Chain<P>> {
467 self.state.chain_beside::<P>(owner, base)
468 }
469
470 #[must_use]
472 pub fn wake(&self) -> Option<&WakeHandle> {
473 self.state.wake()
474 }
475}
476
477type OpenKey = (&'static str, &'static str, String);
480
481pub(crate) struct WorkspaceState {
484 records: WorkspaceRecords,
485 app_id: &'static str,
486 app_title: &'static str,
487 permissions: Option<LivePermissions>,
488 wake: Option<WakeHandle>,
490 opened: RefCell<HashMap<OpenKey, Rc<dyn Any>>>,
491 chains: super::chain::Chains,
494 owners: RefCell<Vec<HistoryOwner>>,
495 refreshers: RefCell<Vec<Box<dyn Fn() -> bool>>>,
496}
497
498impl WorkspaceState {
499 pub(crate) fn new(
500 records: WorkspaceRecords,
501 app_id: &'static str,
502 app_title: &'static str,
503 permissions: Option<LivePermissions>,
504 wake: Option<WakeHandle>,
505 ) -> Self {
506 Self {
507 records,
508 app_id,
509 app_title,
510 permissions,
511 wake,
512 opened: RefCell::default(),
513 chains: super::chain::Chains::default(),
514 owners: RefCell::default(),
515 refreshers: RefCell::default(),
516 }
517 }
518
519 pub(crate) fn wake(&self) -> Option<&WakeHandle> {
522 self.wake.as_ref()
523 }
524
525 pub(crate) fn handle(self: &Rc<Self>) -> WorkspaceHandle {
528 WorkspaceHandle {
529 state: Rc::clone(self),
530 }
531 }
532
533 pub(crate) fn app_id(&self) -> &'static str {
535 self.app_id
536 }
537
538 pub(crate) fn holds_record(&self, owner: &str, id: &str, kind: &str) -> bool {
541 self.records
542 .holds_record(owner, id, kind)
543 .is_ok_and(|held| held)
544 }
545
546 pub(crate) fn open<P: Resource>(&self, owner: &'static str) -> Option<WorkspaceRecord<P>> {
549 let id = ui_core::record::workspace::record_id(P::KIND);
550 self.open_with::<P>(owner, &id, |records| {
551 WorkspaceRecord::open(
552 records,
553 owner,
554 (self.app_id, self.app_title),
555 self.permissions.clone(),
556 self.wake.clone(),
557 )
558 })
559 }
560
561 pub(crate) fn open_at<P: Resource>(
565 &self,
566 owner: &'static str,
567 id: &str,
568 ) -> Option<WorkspaceRecord<P>> {
569 self.open_with::<P>(owner, id, |records| {
570 WorkspaceRecord::open_at(
571 records,
572 owner,
573 id,
574 (self.app_id, self.app_title),
575 self.permissions.clone(),
576 self.wake.clone(),
577 )
578 })
579 }
580
581 fn open_with<P: Resource>(
584 &self,
585 owner: &'static str,
586 id: &str,
587 open: impl FnOnce(&WorkspaceRecords) -> Option<WorkspaceRecord<P>>,
588 ) -> Option<WorkspaceRecord<P>> {
589 let key = (owner, P::KIND, id.to_owned());
590 if let Some(open) = self.opened.borrow().get(&key) {
591 return Rc::clone(open)
592 .downcast::<Shared<P>>()
593 .ok()
594 .map(|shared| WorkspaceRecord { shared });
595 }
596 if owner == self.app_id {
599 super::controls::opened_record::<P>();
600 }
601 let opened = open(&self.records);
602 self.held(key, opened)
603 }
604
605 pub(crate) fn chain_beside<P: Chained>(
611 &self,
612 owner: &'static str,
613 base: &WorkspaceRecord<P::Base>,
614 ) -> Option<Chain<P>> {
615 super::controls::opened_record::<P>();
616 let workspace = super::chain::WorkspaceRecords {
617 store: self.records.store_of(owner).ok()?,
618 me: self.records.author().0.clone(),
619 permissions: self.permissions.clone(),
620 app_id: self.app_id,
621 app_title: self.app_title,
622 };
623 let live = base.live();
624 let chain = P::chain_beside(base.id());
625 let state = self
626 .chains
627 .open_at::<P>(&workspace, &chain, move || live.get())?;
628 Some(Chain::applying(&state, self.app_id, base.me()))
629 }
630
631 fn held<P: Resource>(
635 &self,
636 key: OpenKey,
637 opened: Option<WorkspaceRecord<P>>,
638 ) -> Option<WorkspaceRecord<P>> {
639 let WorkspaceRecord { shared } = opened?;
640 self.owners.borrow_mut().push(shared.record.history_owner());
641 let weak = Rc::downgrade(&shared);
642 self.refreshers.borrow_mut().push(Box::new(move || {
643 weak.upgrade().is_some_and(|shared| shared.refresh())
644 }));
645 self.opened
646 .borrow_mut()
647 .insert(key, Rc::clone(&shared) as Rc<dyn Any>);
648 Some(WorkspaceRecord { shared })
649 }
650
651 pub(crate) fn refresh(&self) -> bool {
653 let moved = self
654 .refreshers
655 .borrow()
656 .iter()
657 .fold(false, |moved, refresh| refresh() | moved);
658 for chain in self.chains.doors() {
661 let _ = chain.revision();
662 }
663 moved
664 }
665
666 pub(crate) fn owners(&self) -> Vec<HistoryOwner> {
668 self.owners.borrow().clone()
669 }
670}