blob: 45cfb15a1f8b0582310ad7f5e1c61a814f79aa22 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::*;
use std::sync::Mutex;
use tokio::sync::{Notify, watch};
enum Request {
Load,
Save(Box<Save>),
}
#[derive(Default)]
struct Pending {
request: Option<Request>,
closed: bool,
unsaved: Option<String>,
}
struct Mailbox {
pending: Mutex<Pending>,
wake: Notify,
}
pub(super) struct Worker {
mailbox: Arc<Mailbox>,
reply: watch::Receiver<Option<Reply>>,
}
impl Worker {
pub fn start(
scope: Scope,
policy: EditorPersistence,
resources: &Arc<Resources>,
budget: NativeBudget,
) -> Result<Self, String> {
let store = scope
.store
.upgrade()
.ok_or("original UI Root is no longer available")?;
let mut ticket = resources.reserve().map_err(|error| error.to_string())?;
let mailbox = Arc::new(Mailbox {
pending: Mutex::new(Pending {
request: Some(Request::Load),
..Default::default()
}),
wake: Notify::new(),
});
let (sender, reply) = watch::channel(None);
ticket.start();
let owned = mailbox.clone();
let wake = resources.clone();
scope.scheduler.clone().spawn(async move {
let result = run(store, scope, policy, budget, owned, sender, wake).await;
ticket.complete(result);
});
Ok(Self { mailbox, reply })
}
pub fn poll(&mut self) -> Option<Reply> {
self.reply.has_changed().ok().filter(|changed| *changed)?;
self.reply.borrow_and_update().clone()
}
pub fn load(&self) {
self.mailbox.pending.lock().unwrap().request = Some(Request::Load);
self.mailbox.wake.notify_one();
}
pub fn save(&self, save: Save) {
let mut pending = self.mailbox.pending.lock().unwrap();
pending.request = Some(Request::Save(Box::new(save)));
pending.unsaved = None;
drop(pending);
self.mailbox.wake.notify_one();
}
pub fn unsaved(&self, diagnostic: String) {
self.mailbox.pending.lock().unwrap().unsaved = Some(diagnostic);
}
pub fn close(self) {
self.mailbox.pending.lock().unwrap().closed = true;
self.mailbox.wake.notify_one();
}
}
fn diagnostic(error: impl std::fmt::Display) -> String {
error.to_string().chars().take(1024).collect()
}
async fn run(
store: Arc<EventLog>,
scope: Scope,
policy: EditorPersistence,
budget: NativeBudget,
mailbox: Arc<Mailbox>,
reply: watch::Sender<Option<Reply>>,
wake: Arc<Resources>,
) -> Result<(), String> {
let mut checkpoints = None;
let mut loaded = false;
let mut revision = None;
let mut failed_save = None;
loop {
let notified = mailbox.wake.notified();
let request = {
let mut pending = mailbox.pending.lock().unwrap();
if let Some(request) = pending.request.take() {
Some(request)
} else if pending.closed {
return pending.unsaved.take().or(failed_save).map_or(Ok(()), Err);
} else {
None
}
};
let Some(request) = request else {
notified.await;
continue;
};
match request {
Request::Load => {
let result = async {
if checkpoints.is_none() {
checkpoints = Some(scope.open(&store).map_err(diagnostic)?);
}
let admission_budget = budget.clone();
let record = checkpoints
.as_ref()
.unwrap()
.read(&policy.key, move |size| {
admission_budget
.reserve(Kind::Encoded, size)
.map(|permit| (permit, size))
.map_err(|error| {
maka_event_log::StoreError::InvalidTransition(error.to_string())
})
})
.await
.map_err(diagnostic)?;
let Some((record, (mut memory, admitted))) = record else {
return Ok(None);
};
if record.format != policy.stored_format()
|| record.data_source != policy.data_source
{
return Err(
"saved editor format or business data source does not match".into()
);
}
let saved: Saved =
serde_json::from_value(record.metadata).map_err(diagnostic)?;
let text = String::from_utf8(record.content).map_err(diagnostic)?;
let snapshot = &saved.snapshot;
if snapshot.text.owner.is_empty()
|| snapshot.text.bytes != text.len()
|| snapshot.text.revision > (1 << 53) - 1
|| snapshot.marked.is_some()
|| snapshot.selection.start > snapshot.selection.end
|| snapshot.selection.end > text.encode_utf16().count()
{
return Err("saved native editor observation is invalid".into());
}
let retained = text
.len()
.checked_add(
maka_plugin_core::json::encoded_size(&saved, usize::MAX)
.map_err(diagnostic)?,
)
.ok_or("editor checkpoint size overflow")?;
let extra = if retained > admitted {
Some(
budget
.reserve(Kind::Encoded, retained - admitted)
.map_err(diagnostic)?,
)
} else {
memory.shrink_to(retained);
None
};
Ok::<_, String>(Some(Arc::new(Loaded {
version: record.revision,
saved,
text,
_memory: (memory, extra),
})))
}
.await;
match result {
Ok(value) => {
revision = value.as_ref().map(|record| record.version);
loaded = true;
reply.send_replace(Some(Reply::Loaded(value)));
}
Err(error) => {
loaded = false;
reply.send_replace(Some(Reply::Failed {
writing: false,
diagnostic: error,
}));
}
}
}
Request::Save(save) => {
let save = *save;
let result = async {
if !loaded {
return Err("original editor checkpoint has not been loaded".into());
}
checkpoints
.as_ref()
.unwrap()
.write(
&policy.key,
revision,
&policy.stored_format(),
&policy.data_source,
&serde_json::to_value(&save.saved).map_err(diagnostic)?,
save.text,
)
.await
.map_err(diagnostic)
}
.await;
match result {
Ok(next) => {
revision = Some(next);
failed_save = None;
reply.send_replace(Some(Reply::Saved(save.sequence)));
}
Err(error) => {
failed_save = Some(error.clone());
reply.send_replace(Some(Reply::Failed {
writing: true,
diagnostic: error,
}));
}
}
}
}
wake.notify();
}
}