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 self.apply_as(action, None)
146 }
147
148 fn apply_as(&self, action: &P::Action, invoker: Option<&ActionInvoker>) -> Applied {
152 let encoded = match tilo_resource::encode::<P>(action) {
153 Ok(encoded) => encoded,
154 Err(error) => return Applied::Done(Err(error)),
155 };
156 let change = action.change_kind();
157 let me = invoker
160 .and_then(|invoker| (!invoker.actor.is_empty()).then_some(invoker.actor.clone()));
161 let actor = me.as_deref().unwrap_or(&self.me);
162 let context = ActionContext::for_resource(P::KIND, P::KIND, P::LABEL)
163 .with_app(self.app_id, self.app_title)
164 .with_invoker(
165 invoker
166 .cloned()
167 .unwrap_or_else(|| ActionInvoker::actor(actor)),
168 )
169 .with_change(change);
170 let edited = self.record.try_edit(change == ChangeKind::Content, |doc| {
171 let writer = ui_core::history_source::authors::device_actor(actor, doc.get_actor());
172 tilo_actions::apply_local_with_context(doc, action, encoded, Some(&writer), context)
173 .map(|_| ())
174 .map_err(|error| error.to_string())
175 });
176 match edited {
177 Ok(result) => Applied::Done(result.map_err(ActionError::Refused)),
178 Err(unavailable) if unavailable.is_momentary() => Applied::Waits,
179 Err(unavailable) => Applied::Done(Err(ActionError::Refused(unavailable.to_string()))),
180 }
181 }
182}
183
184impl<P: Resource> WorkspaceRecord<P> {
185 pub fn open(
194 records: &WorkspaceRecords,
195 owner: &'static str,
196 app: (&'static str, &'static str),
197 permissions: Option<LivePermissions>,
198 wake: Option<WakeHandle>,
199 ) -> Option<Self> {
200 let opened = records.open_record(owner, P::KIND, &|doc| {
201 P::initialize(doc).map_err(|error| error.to_string())
202 });
203 Self::hold(
204 opened,
205 owner,
206 app,
207 permissions,
208 wake,
209 records.author().0.clone(),
210 )
211 }
212
213 pub fn open_at(
219 records: &WorkspaceRecords,
220 owner: &'static str,
221 id: &str,
222 app: (&'static str, &'static str),
223 permissions: Option<LivePermissions>,
224 wake: Option<WakeHandle>,
225 ) -> Option<Self> {
226 let opened = records.open_record_at(owner, id, P::KIND, &|doc| {
227 P::initialize(doc).map_err(|error| error.to_string())
228 });
229 Self::hold(
230 opened,
231 owner,
232 app,
233 permissions,
234 wake,
235 records.author().0.clone(),
236 )
237 }
238
239 pub fn open_primary(
246 records: &WorkspaceRecords,
247 owner: &'static str,
248 app: (&'static str, &'static str),
249 permissions: Option<LivePermissions>,
250 wake: Option<WakeHandle>,
251 ) -> Option<Self> {
252 let opened = records.open_primary(owner, P::KIND, &|doc| {
253 P::initialize(doc).map_err(|error| error.to_string())
254 });
255 Self::hold(
256 opened,
257 owner,
258 app,
259 permissions,
260 wake,
261 records.author().0.clone(),
262 )
263 }
264
265 fn hold(
269 opened: Result<StoredRecord, ui_core::record::RecordOpenError>,
270 owner: &'static str,
271 app: (&'static str, &'static str),
272 permissions: Option<LivePermissions>,
273 wake: Option<WakeHandle>,
274 me: String,
275 ) -> Option<Self> {
276 let record = opened
277 .map_err(|error| tracing::warn!(kind = P::KIND, %error, "workspace record unavailable"))
278 .ok()?;
279 if record.read(tilo_resource::evolution::needs_migrate::<P>)?
280 && let Err(error) = record.edit(false, |doc| {
281 tilo_resource::evolution::migrate::<P>(doc).map_err(|error| error.to_string())
282 })
283 {
284 tracing::warn!(kind = P::KIND, %error, "workspace record not migrated");
285 }
286 let reader = record
287 .read(tilo_resource::Reader::<P>::read)?
288 .map_err(|error| tracing::warn!(kind = P::KIND, %error, "workspace record unreadable"))
289 .ok()?;
290 if let Some(wake) = &wake {
291 record.wake_on_arrival(wake.clone());
292 }
293 let live = Live::new(reader);
294 Some(Self {
295 shared: Rc::new(Shared::<P> {
296 record,
297 owner,
298 app_id: app.0,
299 app_title: app.1,
300 me,
301 permissions,
302 state: LocalState::new(live.get()),
303 live,
304 wake,
305 waiting: RefCell::default(),
306 }),
307 })
308 }
309
310 #[must_use]
312 pub fn id(&self) -> &ui_core::record::RecordId {
313 &self.shared.record.checkout().id
314 }
315
316 pub(crate) fn live(&self) -> Live<P> {
319 self.shared.live.clone()
320 }
321
322 pub fn get(&self) -> Rc<P::State> {
324 self.shared.refresh();
325 self.shared.state.with(Rc::clone)
326 }
327
328 pub fn revision(&self) -> u64 {
330 self.shared.refresh();
331 self.shared.live.revision()
332 }
333
334 pub fn me(&self) -> &str {
336 &self.shared.me
337 }
338
339 pub fn may(&self, verb: &str) -> bool {
343 let Some(permissions) = &self.shared.permissions else {
344 return true;
345 };
346 let Some(declared) = P::declarations().into_iter().find(|decl| decl.id == verb) else {
347 return false;
348 };
349 permissions.check(&PermissionScope::Action {
350 node: tilo_actions::action_node(
351 &TreePath::root(),
352 self.shared.owner,
353 P::KIND,
354 self.shared.record.checkout().id.as_str(),
355 declared.field,
356 verb,
357 ),
358 kind: declared.kind,
359 })
360 }
361
362 pub fn invoke(&self, invoke: &Invoke<P>) -> Result<(), ActionError> {
371 let action = self.admit(invoke)?;
372 match self.shared.apply(&action) {
373 Applied::Done(result) => {
374 self.shared.refresh();
375 result
376 }
377 Applied::Waits => Err(ActionError::Refused(
378 ui_core::record::HistoryUnavailable::StoreInUse.to_string(),
379 )),
380 }
381 }
382
383 pub fn invoke_as(
390 &self,
391 invoker: &ActionInvoker,
392 invoke: &Invoke<P>,
393 ) -> Result<(), ActionError> {
394 let action = self.admit(invoke)?;
395 match self.shared.apply_as(&action, Some(invoker)) {
396 Applied::Done(result) => {
397 self.shared.refresh();
398 result
399 }
400 Applied::Waits => Err(ActionError::Refused(
401 ui_core::record::HistoryUnavailable::StoreInUse.to_string(),
402 )),
403 }
404 }
405
406 pub fn invoke_then(
412 &self,
413 invoke: &Invoke<P>,
414 then: impl FnOnce(Result<(), ActionError>) + 'static,
415 ) {
416 let action = match self.admit(invoke) {
417 Ok(action) => action,
418 Err(error) => return then(Err(error)),
419 };
420 let shared = &self.shared;
421 let first = shared
422 .waiting
423 .try_borrow()
424 .is_ok_and(|waiting| waiting.is_empty());
425 if first && let Applied::Done(result) = shared.apply(&action) {
426 shared.refresh();
427 return then(result);
428 }
429 shared.waiting.borrow_mut().push(Waiting {
430 action,
431 then: Box::new(then),
432 woken: 0,
433 });
434 if let Some(window) = &shared.wake {
435 window.wake();
436 }
437 }
438
439 fn admit(&self, invoke: &Invoke<P>) -> Result<P::Action, ActionError> {
441 let action = invoke
442 .build(tilo_resource::origin_now_ms())
443 .map_err(ActionError::Refused)?;
444 if let Some(verb) = verb_of::<P>(&action)
445 && !self.may(&verb)
446 {
447 return Err(ActionError::Refused(format!(
448 "you may not change the workspace's {}",
449 P::LABEL.to_lowercase()
450 )));
451 }
452 Ok(action)
453 }
454
455 #[must_use]
458 pub fn record(&self) -> &StoredRecord {
459 &self.shared.record
460 }
461}
462
463fn verb_of<P: Resource>(action: &P::Action) -> Option<String> {
466 let mut doc = ui_core::automerge::AutoCommit::new();
467 P::initialize(&mut doc).ok()?;
468 match action.describe(&doc)?.what {
469 tilo_actions::Did::Verb(call) => Some(call.action.action),
470 _ => None,
471 }
472}
473
474#[derive(Clone)]
478pub struct WorkspaceHandle {
479 state: Rc<WorkspaceState>,
480}
481
482impl WorkspaceHandle {
483 pub fn record<P: Resource>(&self, owner: &'static str, id: &str) -> Option<WorkspaceRecord<P>> {
487 self.state.open_at::<P>(owner, id)
488 }
489
490 #[must_use]
493 pub fn holds_record<P: Resource>(&self, owner: &'static str, id: &str) -> bool {
494 self.state.holds_record(owner, id, P::KIND)
495 }
496
497 pub fn chain_beside<P: Chained>(
502 &self,
503 owner: &'static str,
504 base: &WorkspaceRecord<P::Base>,
505 ) -> Option<Chain<P>> {
506 self.state.chain_beside::<P>(owner, base)
507 }
508
509 #[must_use]
511 pub fn wake(&self) -> Option<&WakeHandle> {
512 self.state.wake()
513 }
514}
515
516type OpenKey = (&'static str, &'static str, String);
519
520pub(crate) struct WorkspaceState {
523 records: WorkspaceRecords,
524 app_id: &'static str,
525 app_title: &'static str,
526 permissions: Option<LivePermissions>,
527 wake: Option<WakeHandle>,
529 opened: RefCell<HashMap<OpenKey, Rc<dyn Any>>>,
530 chains: super::chain::Chains,
533 owners: RefCell<Vec<HistoryOwner>>,
534 refreshers: RefCell<Vec<Box<dyn Fn() -> bool>>>,
535}
536
537impl WorkspaceState {
538 pub(crate) fn new(
539 records: WorkspaceRecords,
540 app_id: &'static str,
541 app_title: &'static str,
542 permissions: Option<LivePermissions>,
543 wake: Option<WakeHandle>,
544 ) -> Self {
545 Self {
546 records,
547 app_id,
548 app_title,
549 permissions,
550 wake,
551 opened: RefCell::default(),
552 chains: super::chain::Chains::default(),
553 owners: RefCell::default(),
554 refreshers: RefCell::default(),
555 }
556 }
557
558 pub(crate) fn wake(&self) -> Option<&WakeHandle> {
561 self.wake.as_ref()
562 }
563
564 pub(crate) fn handle(self: &Rc<Self>) -> WorkspaceHandle {
567 WorkspaceHandle {
568 state: Rc::clone(self),
569 }
570 }
571
572 pub(crate) fn app_id(&self) -> &'static str {
574 self.app_id
575 }
576
577 pub(crate) fn holds_record(&self, owner: &str, id: &str, kind: &str) -> bool {
580 self.records
581 .holds_record(owner, id, kind)
582 .is_ok_and(|held| held)
583 }
584
585 pub(crate) fn open<P: Resource>(&self, owner: &'static str) -> Option<WorkspaceRecord<P>> {
588 let id = ui_core::record::workspace::record_id(P::KIND);
589 self.open_with::<P>(owner, &id, |records| {
590 WorkspaceRecord::open(
591 records,
592 owner,
593 (self.app_id, self.app_title),
594 self.permissions.clone(),
595 self.wake.clone(),
596 )
597 })
598 }
599
600 pub(crate) fn open_at<P: Resource>(
604 &self,
605 owner: &'static str,
606 id: &str,
607 ) -> Option<WorkspaceRecord<P>> {
608 self.open_with::<P>(owner, id, |records| {
609 WorkspaceRecord::open_at(
610 records,
611 owner,
612 id,
613 (self.app_id, self.app_title),
614 self.permissions.clone(),
615 self.wake.clone(),
616 )
617 })
618 }
619
620 fn open_with<P: Resource>(
623 &self,
624 owner: &'static str,
625 id: &str,
626 open: impl FnOnce(&WorkspaceRecords) -> Option<WorkspaceRecord<P>>,
627 ) -> Option<WorkspaceRecord<P>> {
628 let key = (owner, P::KIND, id.to_owned());
629 if let Some(open) = self.opened.borrow().get(&key) {
630 return Rc::clone(open)
631 .downcast::<Shared<P>>()
632 .ok()
633 .map(|shared| WorkspaceRecord { shared });
634 }
635 if owner == self.app_id {
638 super::controls::opened_record::<P>();
639 }
640 let opened = open(&self.records);
641 self.held(key, opened)
642 }
643
644 pub(crate) fn chain_beside<P: Chained>(
650 &self,
651 owner: &'static str,
652 base: &WorkspaceRecord<P::Base>,
653 ) -> Option<Chain<P>> {
654 super::controls::opened_record::<P>();
655 let workspace = super::chain::WorkspaceRecords {
656 store: self.records.store_of(owner).ok()?,
657 me: self.records.author().0.clone(),
658 permissions: self.permissions.clone(),
659 app_id: self.app_id,
660 app_title: self.app_title,
661 };
662 let live = base.live();
663 let chain = P::chain_beside(base.id());
664 let state = self
665 .chains
666 .open_at::<P>(&workspace, &chain, move || live.get())?;
667 Some(Chain::applying(&state, self.app_id, base.me()))
668 }
669
670 fn held<P: Resource>(
674 &self,
675 key: OpenKey,
676 opened: Option<WorkspaceRecord<P>>,
677 ) -> Option<WorkspaceRecord<P>> {
678 let WorkspaceRecord { shared } = opened?;
679 self.owners.borrow_mut().push(shared.record.history_owner());
680 let weak = Rc::downgrade(&shared);
681 self.refreshers.borrow_mut().push(Box::new(move || {
682 weak.upgrade().is_some_and(|shared| shared.refresh())
683 }));
684 self.opened
685 .borrow_mut()
686 .insert(key, Rc::clone(&shared) as Rc<dyn Any>);
687 Some(WorkspaceRecord { shared })
688 }
689
690 pub(crate) fn refresh(&self) -> bool {
692 let moved = self
693 .refreshers
694 .borrow()
695 .iter()
696 .fold(false, |moved, refresh| refresh() | moved);
697 for chain in self.chains.doors() {
700 let _ = chain.revision();
701 }
702 moved
703 }
704
705 pub(crate) fn owners(&self) -> Vec<HistoryOwner> {
707 self.owners.borrow().clone()
708 }
709}