1use std::cell::RefCell;
21use std::rc::Rc;
22
23use tilo_actions::door::{ActionAddress, ActionDescriptor, DoorError};
24use tilo_actions::{ActionEnvelope, ActionOutput, ActionTransport};
25use ui_core::application::{
26 ApplicationDefinition, ComponentRunnerInputs, HostContext, HostContextInputs, LaunchTarget,
27 RestoreInput,
28};
29use ui_core::application_view::InstanceViewStore;
30use ui_core::collaboration::DocumentMutation;
31use ui_core::headless::FrameHarness;
32use ui_core::permissions::{PermissionContext, UserId};
33use ui_core::portable_storage::{PortableStorage, StorageKey};
34use ui_core::record::{RecordCheckout, RecordOwners, RecordStore};
35use ui_core::state::DocumentFrontier;
36
37const INSTANCE: u64 = 1;
39
40const VIEWPORT: (u32, u32) = (1280, 720);
43
44const DEVICE: &str = "attached-view";
46
47#[derive(Clone, Default)]
49struct Landed(Rc<RefCell<Vec<ActionEnvelope>>>);
50
51impl ActionTransport for Landed {
52 fn replicate(&mut self, env: &ActionEnvelope) {
53 self.0.borrow_mut().push(env.clone());
54 }
55}
56
57pub struct AttachedView {
60 definition: &'static ApplicationDefinition,
61 checkout: RecordCheckout,
62 frame: Box<FrameHarness>,
64 records: RecordStore,
66 permissions: Rc<RefCell<PermissionContext>>,
67 me: String,
69 invoker: tilo_actions::ActionInvoker,
71 context: Option<tilo_actions::ActionContext>,
73 landed: Landed,
74 frontier: DocumentFrontier,
77 fresh: bool,
79 unsent: Vec<Vec<u8>>,
81}
82
83pub struct OpenView<'a> {
86 definition: &'static ApplicationDefinition,
87 room: &'a str,
88 checkout: RecordCheckout,
89 document: Vec<u8>,
90 invoker: tilo_actions::ActionInvoker,
91 permissions: PermissionContext,
92 created: bool,
93 content: Option<tilo_content::ContentStore>,
94}
95
96impl OpenView<'_> {
97 #[must_use]
99 pub fn for_invoker(mut self, invoker: &tilo_actions::ActionInvoker) -> Self {
100 self.invoker = invoker.clone();
101 self
102 }
103
104 #[must_use]
106 pub fn with_permissions(mut self, permissions: PermissionContext) -> Self {
107 self.permissions = permissions;
108 self
109 }
110
111 #[must_use]
114 pub const fn created(mut self, created: bool) -> Self {
115 self.created = created;
116 self
117 }
118
119 #[must_use]
122 pub fn lending(mut self, store: tilo_content::ContentStore) -> Self {
123 self.content = Some(store);
124 self
125 }
126
127 pub fn open(self) -> Result<AttachedView, String> {
132 AttachedView::open_view(self)
133 }
134}
135
136impl AttachedView {
137 pub fn open(
145 definition: &'static ApplicationDefinition,
146 room: &str,
147 checkout: RecordCheckout,
148 document: Vec<u8>,
149 me: &str,
150 permissions: PermissionContext,
151 created: bool,
152 ) -> Result<Self, String> {
153 Self::open_for(
154 definition,
155 room,
156 checkout,
157 document,
158 &tilo_actions::ActionInvoker::actor(me),
159 permissions,
160 created,
161 )
162 }
163
164 pub fn open_for(
171 definition: &'static ApplicationDefinition,
172 room: &str,
173 checkout: RecordCheckout,
174 document: Vec<u8>,
175 invoker: &tilo_actions::ActionInvoker,
176 permissions: PermissionContext,
177 created: bool,
178 ) -> Result<Self, String> {
179 Self::opening(definition, room, checkout, document)
180 .for_invoker(invoker)
181 .with_permissions(permissions)
182 .created(created)
183 .open()
184 }
185
186 #[must_use]
190 pub fn opening<'a>(
191 definition: &'static ApplicationDefinition,
192 room: &'a str,
193 checkout: RecordCheckout,
194 document: Vec<u8>,
195 ) -> OpenView<'a> {
196 OpenView {
197 definition,
198 room,
199 checkout,
200 document,
201 invoker: tilo_actions::ActionInvoker::actor("owner"),
202 permissions: PermissionContext::permissive(),
203 created: false,
204 content: None,
205 }
206 }
207
208 fn open_view(view: OpenView<'_>) -> Result<Self, String> {
209 let OpenView {
210 definition,
211 room,
212 checkout,
213 document,
214 invoker,
215 permissions,
216 created,
217 content,
218 } = view;
219 let invoker = &invoker;
220 let me = invoker.actor.as_str();
221 let cache = PortableStorage::new("attached-views", DEVICE)?;
222 let key = if checkout.id.is_package_primary() {
223 StorageKey::Primary {
224 package: definition.app_id.into(),
225 workspace: room.to_owned(),
226 }
227 } else {
228 StorageKey::Record {
229 package: definition.app_id.into(),
230 workspace: room.to_owned(),
231 record: checkout.id.as_str().into(),
232 }
233 };
234 cache.hydrate(key, document, Some(me.as_bytes().to_vec()))?;
235 let records = RecordOwners::portable(cache.clone())
236 .open(definition.app_id, room)
237 .map_err(|error| error.to_string())?;
238 let views =
239 Rc::new(InstanceViewStore::portable(cache, room).map_err(|error| error.to_string())?);
240 let permissions = Rc::new(RefCell::new(permissions));
241 let target = LaunchTarget::Record {
242 checkout: checkout.clone(),
243 reveal: None,
244 };
245 let context = HostContext::prepare(
246 definition,
247 HostContextInputs {
248 records: records.clone(),
249 views,
250 device_id: DEVICE.into(),
251 instance_id: INSTANCE,
252 permissions: permissions.clone(),
253 author: UserId(me.to_owned()),
254 action_context: tilo_actions::ActionContext::default()
255 .with_invoker(invoker.clone()),
256 },
257 &target,
258 )
259 .map_err(|error| error.to_string())?;
260 let mut context = context;
261 if let Some(store) = content {
262 context.provide_content(store);
263 }
264 let component = definition
265 .instantiate(context, target, RestoreInput::None)
266 .map_err(|error| error.to_string())?;
267 let mut frame = FrameHarness::from_component(
268 component,
269 VIEWPORT,
270 ComponentRunnerInputs {
271 theme: ui_core::Theme::default(),
272 text_renderer: ui_core::TextRenderer::new(),
273 },
274 )
275 .map_err(|error| error.to_string())?;
276 let landed = Landed::default();
277 frame.set_action_transport(Box::new(landed.clone()))?;
278 let mut frontier = DocumentFrontier::default();
279 if let Some(heads) = records.with(&checkout.id, ui_core::automerge::AutoCommit::get_heads) {
280 frontier.advance(heads);
281 }
282 Ok(Self {
283 definition,
284 checkout,
285 frame: Box::new(frame),
286 records,
287 permissions,
288 me: me.to_owned(),
289 invoker: invoker.clone(),
290 context: None,
291 landed,
292 frontier,
293 fresh: created,
294 unsent: Vec::new(),
295 })
296 }
297
298 #[must_use]
300 pub const fn definition(&self) -> &'static ApplicationDefinition {
301 self.definition
302 }
303
304 #[must_use]
306 pub const fn checkout(&self) -> &RecordCheckout {
307 &self.checkout
308 }
309
310 #[must_use]
312 pub fn me(&self) -> &str {
313 &self.me
314 }
315
316 pub fn set_permissions(&mut self, permissions: PermissionContext) {
318 *self.permissions.borrow_mut() = permissions;
319 self.frame.relayout();
320 }
321
322 #[must_use]
324 pub fn resources(&self) -> Vec<tilo_actions::Resource> {
325 let permissions = self.permissions.borrow();
326 self.frame
327 .resources()
328 .into_iter()
329 .filter_map(|resource| permissions.permitted_resource(self.definition.app_id, resource))
330 .collect()
331 }
332
333 #[must_use]
336 pub fn operations(&self) -> Vec<ActionDescriptor> {
337 tilo_actions::door::survey(&self.resources())
338 .into_iter()
339 .filter(|descriptor| descriptor.kind == tilo_actions::OperationKind::Document)
340 .collect()
341 }
342
343 pub fn invoke(
354 &mut self,
355 address: &ActionAddress,
356 arguments: &serde_json::Map<String, serde_json::Value>,
357 confirmed: bool,
358 ) -> Result<ActionOutput, DoorError> {
359 self.invoke_appearing(address, arguments, confirmed, None)
360 }
361
362 pub fn invoke_appearing(
369 &mut self,
370 address: &ActionAddress,
371 arguments: &serde_json::Map<String, serde_json::Value>,
372 confirmed: bool,
373 appearance: Option<tilo_actions::Appearance>,
374 ) -> Result<ActionOutput, DoorError> {
375 let invoker = self
377 .context
378 .as_ref()
379 .and_then(|context| context.invoker.clone())
380 .filter(|invoker| invoker.actor == self.invoker.actor)
381 .unwrap_or_else(|| self.invoker.clone())
382 .with_appearance(appearance);
383 self.invoke_as(invoker, address, arguments, confirmed)
384 }
385
386 pub fn invoke_as(
395 &mut self,
396 invoker: tilo_actions::ActionInvoker,
397 address: &ActionAddress,
398 arguments: &serde_json::Map<String, serde_json::Value>,
399 confirmed: bool,
400 ) -> Result<ActionOutput, DoorError> {
401 if !tilo_actions::door::survey(&self.resources())
402 .iter()
403 .any(|entry| &entry.address == address)
404 {
405 return Err(DoorError::Unknown(address.clone()));
406 }
407 self.landed.0.borrow_mut().clear();
408 let ambient = self.context.clone().unwrap_or_default().enriched_by(
411 &tilo_actions::ActionContext::default()
412 .with_app(self.definition.app_id, self.definition.title),
413 );
414 let (_, started) = self.frame.start_bound_job_within(
415 Some(invoker),
416 Some(&ambient),
417 &ui_core::bound_dispatch::BoundActionAddress {
418 instance_id: INSTANCE,
419 address: address.clone(),
420 },
421 arguments,
422 confirmed,
423 )?;
424 self.frame.relayout();
425 Ok(started.completed().unwrap_or_default())
426 }
427
428 pub fn invoke_typed<R: tilo_resource::Resource>(
436 &mut self,
437 invoke: &tilo_resource::Invoke<R>,
438 confirmed: bool,
439 ) -> Result<ActionOutput, DoorError> {
440 if let Some(refusal) = invoke.refusal() {
441 return Err(DoorError::InvalidArguments(refusal.to_owned()));
442 }
443 let resource = self
444 .resources()
445 .into_iter()
446 .find(|resource| resource.kind == R::KIND)
447 .ok_or_else(|| DoorError::Unknown(ActionAddress::new(R::KIND, invoke.id)))?;
448 self.invoke(
449 &ActionAddress::new(resource.id.as_str(), invoke.id),
450 &invoke.arguments,
451 confirmed,
452 )
453 }
454
455 pub fn invoke_at(
463 &mut self,
464 place: &tilo_resource::Place,
465 address: &ActionAddress,
466 arguments: &serde_json::Map<String, serde_json::Value>,
467 confirmed: bool,
468 ) -> Result<ActionOutput, DoorError> {
469 let resources = self.resources();
470 let param = tilo_actions::door::survey(&resources)
471 .into_iter()
472 .find(|entry| &entry.address == address)
473 .ok_or_else(|| DoorError::Unknown(address.clone()))?
474 .params
475 .iter()
476 .find(|param| param.is_place())
477 .map(|param| param.name.to_string())
478 .ok_or_else(|| {
479 DoorError::InvalidArguments(format!(
480 "`{}` takes no place to act at",
481 address.action
482 ))
483 })?;
484 let mut arguments = arguments.clone();
485 arguments.insert(
486 param,
487 serde_json::to_value(place)
488 .map_err(|error| DoorError::InvalidArguments(error.to_string()))?,
489 );
490 self.invoke(address, &arguments, confirmed)
491 }
492
493 pub fn set_context(&mut self, context: Option<tilo_actions::ActionContext>) {
496 self.context = context;
497 }
498
499 #[must_use]
501 pub const fn context(&self) -> Option<&tilo_actions::ActionContext> {
502 self.context.as_ref()
503 }
504
505 pub fn record_run(
509 &mut self,
510 label: &str,
511 issued_by: Option<&str>,
512 decision: Option<&str>,
513 steps: &[ui_core::RunStep],
514 ) -> bool {
515 self.records
516 .with(&self.checkout.id, |doc| {
517 let by = doc.get_actor().to_hex_string();
518 let recorded = ui_core::record_run_with_details(
519 doc,
520 label,
521 Some(&by),
522 issued_by,
523 Some(label),
524 decision,
525 steps,
526 );
527 if recorded.is_some() {
528 tilo_resource::history::commit_stamped_with_message(
529 doc,
530 Some(tilo_actions::FrameworkChange::Run.change_message()),
531 );
532 }
533 recorded.is_some()
534 })
535 .unwrap_or(false)
536 }
537
538 #[must_use]
541 pub fn landed(&self) -> Option<ActionEnvelope> {
542 self.landed.0.borrow().last().cloned()
543 }
544
545 pub fn read<R>(&self, read: impl FnOnce(&ui_core::automerge::AutoCommit) -> R) -> Option<R> {
547 self.records.read(&self.checkout.id, read)
548 }
549
550 pub fn take_changes(&mut self) -> Option<DocumentMutation> {
553 if std::mem::take(&mut self.fresh) {
554 self.unsent.clear();
555 let (bytes, heads) = self.records.replication_snapshot(&self.checkout.id).ok()?;
556 self.frontier.advance(heads);
557 return Some(DocumentMutation::Snapshot(bytes));
558 }
559 self.set_aside_own();
560 let own: Vec<u8> = std::mem::take(&mut self.unsent).concat();
561 (!own.is_empty()).then_some(DocumentMutation::Incremental(own))
562 }
563
564 pub fn merge(&mut self, mutation: &DocumentMutation) -> Result<bool, String> {
571 if let DocumentMutation::Incremental(bytes) = mutation {
572 let mut incoming = ui_core::automerge::AutoCommit::new();
573 if tilo_automerge::ApplyIncoming::load_incoming(&mut incoming, bytes).is_err() {
574 return Ok(false);
575 }
576 let missing = incoming.get_missing_deps(&[]);
577 let known = self
578 .records
579 .with(&self.checkout.id, |doc| {
580 missing
581 .iter()
582 .all(|hash| doc.get_change_by_hash(hash).is_some())
583 })
584 .unwrap_or(false);
585 if !known {
586 return Ok(false);
587 }
588 }
589 if !self.fresh {
591 self.set_aside_own();
592 }
593 let (changed, observed) = self
594 .records
595 .merge_replica(&self.checkout, mutation.clone())?;
596 self.frontier.observe(observed);
597 self.frame.relayout();
598 Ok(changed)
599 }
600
601 fn set_aside_own(&mut self) {
603 if let Ok((delta, heads)) = self
604 .records
605 .replication_delta(&self.checkout.id, &mut self.frontier)
606 {
607 self.frontier.advance(heads);
608 if !delta.is_empty() {
609 self.unsent.push(delta);
610 }
611 }
612 }
613}