From 5df684fa6b0fc3f2b58310749d838702f557b315 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 11 Mar 2021 14:33:57 +1000 Subject: [PATCH] rework backend codegen to support multiple services; split out sched Rust requires all methods of impl Trait to be in a single file, which means we had a giant backend/mod.rs covering all exposed methods. By using separate service definitions for the separate areas, and updating the code generation, we can split it into more manageable chunks - this commit starts with the scheduling code. In the long run, we'll probably want to split up the protobuf file into multiple files as well. Also dropped want_release_gil() from rsbridge, and the associated method enum. While it allows us to skip the thread save/restore and mutex unlock/ lock, it looks to only be buying about 2.5% extra performance in the best case (tested with timeit+format_timespan), and the majority of the backend methods deal with I/O, and thus were already releasing the GIL. --- pylib/anki/_backend/BUILD.bazel | 2 +- pylib/anki/_backend/__init__.py | 4 +- pylib/anki/_backend/genbackend.py | 31 ++- pylib/anki/_backend/rsbridge.pyi | 2 +- pylib/rsbridge/lib.rs | 60 ++--- rslib/backend.proto | 56 +++-- rslib/build/protobuf.rs | 36 ++- rslib/src/backend/mod.rs | 384 ++++++++--------------------- rslib/src/backend/scheduler/mod.rs | 172 +++++++++++++ 9 files changed, 366 insertions(+), 381 deletions(-) diff --git a/pylib/anki/_backend/BUILD.bazel b/pylib/anki/_backend/BUILD.bazel index c2667acf5..08bef7231 100644 --- a/pylib/anki/_backend/BUILD.bazel +++ b/pylib/anki/_backend/BUILD.bazel @@ -36,7 +36,7 @@ py_binary( genrule( name = "rsbackend_gen", outs = ["generated.py"], - cmd = "$(location genbackend) > $@", + cmd = "$(location genbackend) $@", tools = ["genbackend"], ) diff --git a/pylib/anki/_backend/__init__.py b/pylib/anki/_backend/__init__.py index d27f32c3a..92d07818a 100644 --- a/pylib/anki/_backend/__init__.py +++ b/pylib/anki/_backend/__init__.py @@ -95,10 +95,10 @@ class RustBackend(RustBackendGenerated): ) return self.format_timespan(seconds=seconds, context=context) - def _run_command(self, method: int, input: Any) -> bytes: + def _run_command(self, service: int, method: int, input: Any) -> bytes: input_bytes = input.SerializeToString() try: - return self._backend.command(method, input_bytes) + return self._backend.command(service, method, input_bytes) except Exception as e: err_bytes = bytes(e.args[0]) err = pb.BackendError() diff --git a/pylib/anki/_backend/genbackend.py b/pylib/anki/_backend/genbackend.py index 808c31689..31c44ee86 100755 --- a/pylib/anki/_backend/genbackend.py +++ b/pylib/anki/_backend/genbackend.py @@ -1,10 +1,13 @@ #!/usr/bin/env python3 # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + import os import re import sys +import google.protobuf.descriptor + import pylib.anki._backend.backend_pb2 as pb import stringcase @@ -97,7 +100,7 @@ def get_input_assign(msg): return ", ".join(f"{f.name}={f.name}" for f in fields) -def render_method(method, idx): +def render_method(service_idx, method_idx, method): input_name = method.input_type.name if ( (input_name.endswith("In") or len(method.input_type.fields) < 2) @@ -134,11 +137,11 @@ def render_method(method, idx): {input_assign_outer}""" if method.name in SKIP_DECODE: - buf += f"""return self._run_command({idx+1}, input) + buf += f"""return self._run_command({service_idx}, {method_idx+1}, input) """ else: buf += f"""output = pb.{method.output_type.name}() - output.ParseFromString(self._run_command({idx+1}, input)) + output.ParseFromString(self._run_command({service_idx}, {method_idx+1}, input)) return output{single_field} """ @@ -146,13 +149,27 @@ def render_method(method, idx): out = [] -for idx, method in enumerate(pb._BACKENDSERVICE.methods): - out.append(render_method(method, idx)) + + +def render_service( + service: google.protobuf.descriptor.ServiceDescriptor, service_index: int +) -> None: + for method_index, method in enumerate(service.methods): + out.append(render_method(service_index, method_index, method)) + + +for service in pb.ServiceIndex.DESCRIPTOR.values: + # SERVICE_INDEX_TEST -> _TESTSERVICE + service_var = service.name.replace("SERVICE_INDEX", "") + "SERVICE" + service_obj = getattr(pb, service_var) + service_index = service.number + render_service(service_obj, service_index) + out = "\n".join(out) -sys.stdout.buffer.write( +open(sys.argv[1], "wb").write( ( '''# Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html @@ -174,7 +191,7 @@ from typing import * import anki._backend.backend_pb2 as pb class RustBackendGenerated: - def _run_command(self, method: int, input: Any) -> bytes: + def _run_command(self, service: int, method: int, input: Any) -> bytes: raise Exception("not implemented") ''' diff --git a/pylib/anki/_backend/rsbridge.pyi b/pylib/anki/_backend/rsbridge.pyi index 126678bc9..9d0e23d6a 100644 --- a/pylib/anki/_backend/rsbridge.pyi +++ b/pylib/anki/_backend/rsbridge.pyi @@ -3,5 +3,5 @@ def open_backend(data: bytes) -> Backend: ... class Backend: @classmethod - def command(self, method: int, data: bytes) -> bytes: ... + def command(self, service: int, method: int, data: bytes) -> bytes: ... def db_command(self, data: bytes) -> bytes: ... diff --git a/pylib/rsbridge/lib.rs b/pylib/rsbridge/lib.rs index 6c780f751..b7ce73955 100644 --- a/pylib/rsbridge/lib.rs +++ b/pylib/rsbridge/lib.rs @@ -1,15 +1,11 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use anki::backend::{init_backend, Backend as RustBackend, BackendMethod}; +use anki::backend::{init_backend, Backend as RustBackend}; use pyo3::exceptions::PyException; use pyo3::prelude::*; use pyo3::types::PyBytes; use pyo3::{create_exception, wrap_pyfunction}; -use std::convert::TryFrom; - -// Regular backend -////////////////////////////////// #[pyclass(module = "rsbridge")] struct Backend { @@ -31,50 +27,22 @@ fn open_backend(init_msg: &PyBytes) -> PyResult { } } -fn want_release_gil(method: u32) -> bool { - if let Ok(method) = BackendMethod::try_from(method) { - !matches!( - method, - BackendMethod::ExtractAVTags - | BackendMethod::ExtractLatex - | BackendMethod::RenderExistingCard - | BackendMethod::RenderUncommittedCard - | BackendMethod::StripAVTags - | BackendMethod::SchedTimingToday - | BackendMethod::AddOrUpdateDeckLegacy - | BackendMethod::NewDeckLegacy - | BackendMethod::NewDeckConfigLegacy - | BackendMethod::GetStockNotetypeLegacy - | BackendMethod::StudiedToday - | BackendMethod::TranslateString - | BackendMethod::FormatTimespan - | BackendMethod::LatestProgress - | BackendMethod::SetWantsAbort - | BackendMethod::I18nResources - | BackendMethod::JoinSearchNodes - | BackendMethod::ReplaceSearchNode - | BackendMethod::BuildSearchString - | BackendMethod::StateIsLeech - ) - } else { - false - } -} - #[pymethods] impl Backend { - fn command(&self, py: Python, method: u32, input: &PyBytes) -> PyResult { + fn command( + &self, + py: Python, + service: u32, + method: u32, + input: &PyBytes, + ) -> PyResult { let in_bytes = input.as_bytes(); - if want_release_gil(method) { - py.allow_threads(|| self.backend.run_command_bytes(method, in_bytes)) - } else { - self.backend.run_command_bytes(method, in_bytes) - } - .map(|out_bytes| { - let out_obj = PyBytes::new(py, &out_bytes); - out_obj.into() - }) - .map_err(BackendError::new_err) + py.allow_threads(|| self.backend.run_method(service, method, in_bytes)) + .map(|out_bytes| { + let out_obj = PyBytes::new(py, &out_bytes); + out_obj.into() + }) + .map_err(BackendError::new_err) } /// This takes and returns JSON, due to Python's slow protobuf diff --git a/rslib/backend.proto b/rslib/backend.proto index 78e07aeae..11a390895 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -72,33 +72,18 @@ message DeckConfigID { int64 dcid = 1; } -// New style RPC definitions +// Backend methods /////////////////////////////////////////////////////////// -service BackendService { - rpc LatestProgress(Empty) returns (Progress); - rpc SetWantsAbort(Empty) returns (Empty); - - // card rendering - - rpc ExtractAVTags(ExtractAVTagsIn) returns (ExtractAVTagsOut); - rpc ExtractLatex(ExtractLatexIn) returns (ExtractLatexOut); - rpc GetEmptyCards(Empty) returns (EmptyCardsReport); - rpc RenderExistingCard(RenderExistingCardIn) returns (RenderCardOut); - rpc RenderUncommittedCard(RenderUncommittedCardIn) returns (RenderCardOut); - rpc StripAVTags(String) returns (String); - - // searching - - rpc BuildSearchString(SearchNode) returns (String); - rpc SearchCards(SearchCardsIn) returns (SearchCardsOut); - rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut); - rpc JoinSearchNodes(JoinSearchNodesIn) returns (String); - rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String); - rpc FindAndReplace(FindAndReplaceIn) returns (UInt32); - - // scheduling +/// while the protobuf descriptors expose the order services are defined in, +/// that information is not available in prost, so we define an enum to make +/// sure all clients agree on the same service indices +enum ServiceIndex { + SERVICE_INDEX_SCHEDULING = 0; + SERVICE_INDEX_BACKEND = 1; +} +service SchedulingService { rpc SchedTimingToday(Empty) returns (SchedTimingTodayOut); rpc StudiedToday(Empty) returns (String); rpc StudiedTodayMessage(StudiedTodayMessageIn) returns (String); @@ -121,6 +106,29 @@ service BackendService { rpc AnswerCard(AnswerCardIn) returns (Empty); rpc UpgradeScheduler(Empty) returns (Empty); rpc GetQueuedCards(GetQueuedCardsIn) returns (GetQueuedCardsOut); +} + +service BackendService { + rpc LatestProgress(Empty) returns (Progress); + rpc SetWantsAbort(Empty) returns (Empty); + + // card rendering + + rpc ExtractAVTags(ExtractAVTagsIn) returns (ExtractAVTagsOut); + rpc ExtractLatex(ExtractLatexIn) returns (ExtractLatexOut); + rpc GetEmptyCards(Empty) returns (EmptyCardsReport); + rpc RenderExistingCard(RenderExistingCardIn) returns (RenderCardOut); + rpc RenderUncommittedCard(RenderUncommittedCardIn) returns (RenderCardOut); + rpc StripAVTags(String) returns (String); + + // searching + + rpc BuildSearchString(SearchNode) returns (String); + rpc SearchCards(SearchCardsIn) returns (SearchCardsOut); + rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut); + rpc JoinSearchNodes(JoinSearchNodesIn) returns (String); + rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String); + rpc FindAndReplace(FindAndReplaceIn) returns (UInt32); // stats diff --git a/rslib/build/protobuf.rs b/rslib/build/protobuf.rs index 93143af4a..aad63055b 100644 --- a/rslib/build/protobuf.rs +++ b/rslib/build/protobuf.rs @@ -6,32 +6,14 @@ use std::{env, fmt::Write}; struct CustomGenerator {} -fn write_method_enum(buf: &mut String, service: &prost_build::Service) { - buf.push_str( - r#" -use num_enum::TryFromPrimitive; -#[derive(PartialEq,TryFromPrimitive)] -#[repr(u32)] -pub enum BackendMethod { -"#, - ); - for (idx, method) in service.methods.iter().enumerate() { - writeln!(buf, " {} = {},", method.proto_name, idx + 1).unwrap(); - } - buf.push_str("}\n\n"); -} - fn write_method_trait(buf: &mut String, service: &prost_build::Service) { buf.push_str( r#" -use prost::Message; -pub type BackendResult = std::result::Result; -pub trait BackendService { - fn run_command_bytes2_inner(&self, method: u32, input: &[u8]) -> std::result::Result, crate::err::AnkiError> { +pub trait Service { + fn run_method(&self, method: u32, input: &[u8]) -> Result> { match method { "#, ); - for (idx, method) in service.methods.iter().enumerate() { write!( buf, @@ -58,7 +40,7 @@ pub trait BackendService { buf, concat!( " fn {method_name}(&self, input: {input_type}) -> ", - "BackendResult<{output_type}>;\n" + "Result<{output_type}>;\n" ), method_name = method.name, input_type = method.input_type, @@ -71,8 +53,18 @@ pub trait BackendService { impl prost_build::ServiceGenerator for CustomGenerator { fn generate(&mut self, service: prost_build::Service, buf: &mut String) { - write_method_enum(buf, &service); + write!( + buf, + "pub mod {name} {{ + use super::*; + use prost::Message; + use crate::err::Result; + ", + name = service.name.replace("Service", "").to_ascii_lowercase() + ) + .unwrap(); write_method_trait(buf, &service); + buf.push('}'); } } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 6c8d6c39b..da51c86a5 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -13,13 +13,13 @@ mod scheduler; mod search; mod sync; -pub use crate::backend_proto::BackendMethod; +use self::scheduler::SchedulingService; +use crate::backend_proto::backend::Service as BackendService; + use crate::{ backend::dbproxy::db_command_bytes, backend_proto as pb, - backend_proto::{ - AddOrUpdateDeckConfigLegacyIn, BackendResult, Empty, RenderedTemplateReplacement, - }, + backend_proto::{AddOrUpdateDeckConfigLegacyIn, Empty, RenderedTemplateReplacement}, card::{Card, CardID}, cloze::add_cloze_numbers_in_string, collection::{open_collection, Collection}, @@ -37,14 +37,8 @@ use crate::{ notetype::{ all_stock_notetypes, CardTemplateSchema11, NoteType, NoteTypeSchema11, RenderCardOutput, }, - scheduler::{ - new::NewCardSortOrder, - parse_due_date_str, - states::{CardState, NextCardStates}, - timespan::{answer_button_time, time_span}, - }, + scheduler::timespan::{answer_button_time, time_span}, search::{concatenate_searches, replace_search_node, write_nodes, Node}, - stats::studied_today, sync::{http::SyncRequest, LocalServer}, template::RenderedNode, text::{extract_av_tags, sanitize_html_no_images, strip_av_tags, AVTag}, @@ -55,7 +49,6 @@ use fluent::FluentValue; use futures::future::AbortHandle; use log::error; use once_cell::sync::OnceCell; -use pb::BackendService; use progress::{AbortHandleSlot, Progress}; use prost::Message; use serde_json::Value as JsonValue; @@ -107,19 +100,19 @@ pub fn init_backend(init_msg: &[u8]) -> std::result::Result { } impl BackendService for Backend { - fn latest_progress(&self, _input: Empty) -> BackendResult { + fn latest_progress(&self, _input: Empty) -> Result { let progress = self.progress_state.lock().unwrap().last_progress; Ok(progress_to_proto(progress, &self.i18n)) } - fn set_wants_abort(&self, _input: Empty) -> BackendResult { + fn set_wants_abort(&self, _input: Empty) -> Result { self.progress_state.lock().unwrap().want_abort = true; Ok(().into()) } // card rendering - fn extract_av_tags(&self, input: pb::ExtractAvTagsIn) -> BackendResult { + fn extract_av_tags(&self, input: pb::ExtractAvTagsIn) -> Result { let (text, tags) = extract_av_tags(&input.text, input.question_side); let pt_tags = tags .into_iter() @@ -151,7 +144,7 @@ impl BackendService for Backend { }) } - fn extract_latex(&self, input: pb::ExtractLatexIn) -> BackendResult { + fn extract_latex(&self, input: pb::ExtractLatexIn) -> Result { let func = if input.expand_clozes { extract_latex_expanding_clozes } else { @@ -193,10 +186,7 @@ impl BackendService for Backend { }) } - fn render_existing_card( - &self, - input: pb::RenderExistingCardIn, - ) -> BackendResult { + fn render_existing_card(&self, input: pb::RenderExistingCardIn) -> Result { self.with_col(|col| { col.render_existing_card(CardID(input.card_id), input.browser) .map(Into::into) @@ -206,7 +196,7 @@ impl BackendService for Backend { fn render_uncommitted_card( &self, input: pb::RenderUncommittedCardIn, - ) -> BackendResult { + ) -> Result { let schema11: CardTemplateSchema11 = serde_json::from_slice(&input.template)?; let template = schema11.into(); let mut note = input @@ -221,7 +211,7 @@ impl BackendService for Backend { }) } - fn strip_av_tags(&self, input: pb::String) -> BackendResult { + fn strip_av_tags(&self, input: pb::String) -> Result { Ok(pb::String { val: strip_av_tags(&input.val).into(), }) @@ -277,7 +267,7 @@ impl BackendService for Backend { Ok(replace_search_node(existing, replacement).into()) } - fn find_and_replace(&self, input: pb::FindAndReplaceIn) -> BackendResult { + fn find_and_replace(&self, input: pb::FindAndReplaceIn) -> Result { let mut search = if input.regex { input.search } else { @@ -298,187 +288,23 @@ impl BackendService for Backend { .map(|cnt| pb::UInt32 { val: cnt as u32 }) }) } - - // scheduling - //----------------------------------------------- - - /// This behaves like _updateCutoff() in older code - it also unburies at the start of - /// a new day. - fn sched_timing_today(&self, _input: pb::Empty) -> Result { - self.with_col(|col| { - let timing = col.timing_today()?; - col.unbury_if_day_rolled_over(timing)?; - Ok(timing.into()) - }) - } - - /// Fetch data from DB and return rendered string. - fn studied_today(&self, _input: pb::Empty) -> BackendResult { - self.with_col(|col| col.studied_today().map(Into::into)) - } - - /// Message rendering only, for old graphs. - fn studied_today_message(&self, input: pb::StudiedTodayMessageIn) -> BackendResult { - Ok(studied_today(input.cards, input.seconds as f32, &self.i18n).into()) - } - - fn update_stats(&self, input: pb::UpdateStatsIn) -> BackendResult { - self.with_col(|col| { - col.transact(None, |col| { - let today = col.current_due_day(0)?; - let usn = col.usn()?; - col.update_deck_stats(today, usn, input).map(Into::into) - }) - }) - } - - fn extend_limits(&self, input: pb::ExtendLimitsIn) -> BackendResult { - self.with_col(|col| { - col.transact(None, |col| { - let today = col.current_due_day(0)?; - let usn = col.usn()?; - col.extend_limits( - today, - usn, - input.deck_id.into(), - input.new_delta, - input.review_delta, - ) - .map(Into::into) - }) - }) - } - - fn counts_for_deck_today(&self, input: pb::DeckId) -> BackendResult { - self.with_col(|col| col.counts_for_deck_today(input.did.into())) - } - - fn congrats_info(&self, _input: Empty) -> BackendResult { - self.with_col(|col| col.congrats_info()) - } - - fn restore_buried_and_suspended_cards(&self, input: pb::CardIDs) -> BackendResult { - let cids: Vec<_> = input.into(); - self.with_col(|col| col.unbury_or_unsuspend_cards(&cids).map(Into::into)) - } - - fn unbury_cards_in_current_deck( - &self, - input: pb::UnburyCardsInCurrentDeckIn, - ) -> BackendResult { - self.with_col(|col| { - col.unbury_cards_in_current_deck(input.mode()) - .map(Into::into) - }) - } - - fn bury_or_suspend_cards(&self, input: pb::BuryOrSuspendCardsIn) -> BackendResult { - self.with_col(|col| { - let mode = input.mode(); - let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); - col.bury_or_suspend_cards(&cids, mode).map(Into::into) - }) - } - - fn empty_filtered_deck(&self, input: pb::DeckId) -> BackendResult { - self.with_col(|col| col.empty_filtered_deck(input.did.into()).map(Into::into)) - } - - fn rebuild_filtered_deck(&self, input: pb::DeckId) -> BackendResult { - self.with_col(|col| col.rebuild_filtered_deck(input.did.into()).map(Into::into)) - } - - fn schedule_cards_as_new(&self, input: pb::ScheduleCardsAsNewIn) -> BackendResult { - self.with_col(|col| { - let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); - let log = input.log; - col.reschedule_cards_as_new(&cids, log).map(Into::into) - }) - } - - fn set_due_date(&self, input: pb::SetDueDateIn) -> BackendResult { - let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); - let spec = parse_due_date_str(&input.days)?; - self.with_col(|col| col.set_due_date(&cids, spec).map(Into::into)) - } - - fn sort_cards(&self, input: pb::SortCardsIn) -> BackendResult { - let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); - let (start, step, random, shift) = ( - input.starting_from, - input.step_size, - input.randomize, - input.shift_existing, - ); - let order = if random { - NewCardSortOrder::Random - } else { - NewCardSortOrder::Preserve - }; - self.with_col(|col| { - col.sort_cards(&cids, start, step, order, shift) - .map(Into::into) - }) - } - - fn sort_deck(&self, input: pb::SortDeckIn) -> BackendResult { - self.with_col(|col| { - col.sort_deck(input.deck_id.into(), input.randomize) - .map(Into::into) - }) - } - - fn get_next_card_states(&self, input: pb::CardId) -> BackendResult { - let cid: CardID = input.into(); - self.with_col(|col| col.get_next_card_states(cid)) - .map(Into::into) - } - - fn describe_next_states(&self, input: pb::NextCardStates) -> BackendResult { - let states: NextCardStates = input.into(); - self.with_col(|col| col.describe_next_states(states)) - .map(Into::into) - } - - fn state_is_leech(&self, input: pb::SchedulingState) -> BackendResult { - let state: CardState = input.into(); - Ok(state.leeched().into()) - } - - fn answer_card(&self, input: pb::AnswerCardIn) -> BackendResult { - self.with_col(|col| col.answer_card(&input.into())) - .map(Into::into) - } - - fn upgrade_scheduler(&self, _input: Empty) -> BackendResult { - self.with_col(|col| col.transact(None, |col| col.upgrade_to_v2_scheduler())) - .map(Into::into) - } - - fn get_queued_cards( - &self, - input: pb::GetQueuedCardsIn, - ) -> BackendResult { - self.with_col(|col| col.get_queued_cards(input.fetch_limit, input.intraday_learning_only)) - } - // statistics //----------------------------------------------- - fn card_stats(&self, input: pb::CardId) -> BackendResult { + fn card_stats(&self, input: pb::CardId) -> Result { self.with_col(|col| col.card_stats(input.into())) .map(Into::into) } - fn graphs(&self, input: pb::GraphsIn) -> BackendResult { + fn graphs(&self, input: pb::GraphsIn) -> Result { self.with_col(|col| col.graph_data_for_search(&input.search, input.days)) } - fn get_graph_preferences(&self, _input: pb::Empty) -> BackendResult { + fn get_graph_preferences(&self, _input: pb::Empty) -> Result { self.with_col(|col| col.get_graph_preferences()) } - fn set_graph_preferences(&self, input: pb::GraphPreferences) -> BackendResult { + fn set_graph_preferences(&self, input: pb::GraphPreferences) -> Result { self.with_col(|col| col.set_graph_preferences(input)) .map(Into::into) } @@ -508,7 +334,7 @@ impl BackendService for Backend { }) } - fn trash_media_files(&self, input: pb::TrashMediaFilesIn) -> BackendResult { + fn trash_media_files(&self, input: pb::TrashMediaFilesIn) -> Result { self.with_col(|col| { let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; let mut ctx = mgr.dbctx(); @@ -517,7 +343,7 @@ impl BackendService for Backend { .map(Into::into) } - fn add_media_file(&self, input: pb::AddMediaFileIn) -> BackendResult { + fn add_media_file(&self, input: pb::AddMediaFileIn) -> Result { self.with_col(|col| { let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; let mut ctx = mgr.dbctx(); @@ -528,7 +354,7 @@ impl BackendService for Backend { }) } - fn empty_trash(&self, _input: Empty) -> BackendResult { + fn empty_trash(&self, _input: Empty) -> Result { let mut handler = self.new_progress_handler(); let progress_fn = move |progress| handler.update(Progress::MediaCheck(progress as u32), true); @@ -544,7 +370,7 @@ impl BackendService for Backend { .map(Into::into) } - fn restore_trash(&self, _input: Empty) -> BackendResult { + fn restore_trash(&self, _input: Empty) -> Result { let mut handler = self.new_progress_handler(); let progress_fn = move |progress| handler.update(Progress::MediaCheck(progress as u32), true); @@ -595,7 +421,7 @@ impl BackendService for Backend { }) } - fn deck_tree_legacy(&self, _input: pb::Empty) -> BackendResult { + fn deck_tree_legacy(&self, _input: pb::Empty) -> Result { self.with_col(|col| { let tree = col.legacy_deck_tree()?; serde_json::to_vec(&tree) @@ -604,7 +430,7 @@ impl BackendService for Backend { }) } - fn get_all_decks_legacy(&self, _input: Empty) -> BackendResult { + fn get_all_decks_legacy(&self, _input: Empty) -> Result { self.with_col(|col| { let decks = col.storage.get_all_decks_as_schema11()?; serde_json::to_vec(&decks).map_err(Into::into) @@ -650,7 +476,7 @@ impl BackendService for Backend { }) } - fn new_deck_legacy(&self, input: pb::Bool) -> BackendResult { + fn new_deck_legacy(&self, input: pb::Bool) -> Result { let deck = if input.val { Deck::new_filtered() } else { @@ -662,12 +488,12 @@ impl BackendService for Backend { .map(Into::into) } - fn remove_deck(&self, input: pb::DeckId) -> BackendResult { + fn remove_deck(&self, input: pb::DeckId) -> Result { self.with_col(|col| col.remove_deck_and_child_decks(input.into())) .map(Into::into) } - fn drag_drop_decks(&self, input: pb::DragDropDecksIn) -> BackendResult { + fn drag_drop_decks(&self, input: pb::DragDropDecksIn) -> Result { let source_dids: Vec<_> = input.source_deck_ids.into_iter().map(Into::into).collect(); let target_did = if input.target_deck_id == 0 { None @@ -684,7 +510,7 @@ impl BackendService for Backend { fn add_or_update_deck_config_legacy( &self, input: AddOrUpdateDeckConfigLegacyIn, - ) -> BackendResult { + ) -> Result { let conf: DeckConfSchema11 = serde_json::from_slice(&input.config)?; let mut conf: DeckConf = conf.into(); self.with_col(|col| { @@ -696,7 +522,7 @@ impl BackendService for Backend { .map(Into::into) } - fn all_deck_config_legacy(&self, _input: Empty) -> BackendResult { + fn all_deck_config_legacy(&self, _input: Empty) -> Result { self.with_col(|col| { let conf: Vec = col .storage @@ -709,7 +535,7 @@ impl BackendService for Backend { .map(Into::into) } - fn get_deck_config_legacy(&self, input: pb::DeckConfigId) -> BackendResult { + fn get_deck_config_legacy(&self, input: pb::DeckConfigId) -> Result { self.with_col(|col| { let conf = col.get_deck_config(input.into(), true)?.unwrap(); let conf: DeckConfSchema11 = conf.into(); @@ -718,13 +544,13 @@ impl BackendService for Backend { .map(Into::into) } - fn new_deck_config_legacy(&self, _input: Empty) -> BackendResult { + fn new_deck_config_legacy(&self, _input: Empty) -> Result { serde_json::to_vec(&DeckConfSchema11::default()) .map_err(Into::into) .map(Into::into) } - fn remove_deck_config(&self, input: pb::DeckConfigId) -> BackendResult { + fn remove_deck_config(&self, input: pb::DeckConfigId) -> Result { self.with_col(|col| col.transact(None, |col| col.remove_deck_config(input.into()))) .map(Into::into) } @@ -732,7 +558,7 @@ impl BackendService for Backend { // cards //------------------------------------------------------------------- - fn get_card(&self, input: pb::CardId) -> BackendResult { + fn get_card(&self, input: pb::CardId) -> Result { self.with_col(|col| { col.storage .get_card(input.into()) @@ -741,7 +567,7 @@ impl BackendService for Backend { }) } - fn update_card(&self, input: pb::UpdateCardIn) -> BackendResult { + fn update_card(&self, input: pb::UpdateCardIn) -> Result { self.with_col(|col| { let op = if input.skip_undo_entry { None @@ -754,7 +580,7 @@ impl BackendService for Backend { .map(Into::into) } - fn remove_cards(&self, input: pb::RemoveCardsIn) -> BackendResult { + fn remove_cards(&self, input: pb::RemoveCardsIn) -> Result { self.with_col(|col| { col.transact(None, |col| { col.remove_cards_and_orphaned_notes( @@ -769,7 +595,7 @@ impl BackendService for Backend { }) } - fn set_deck(&self, input: pb::SetDeckIn) -> BackendResult { + fn set_deck(&self, input: pb::SetDeckIn) -> Result { let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); let deck_id = input.deck_id.into(); self.with_col(|col| col.set_deck(&cids, deck_id).map(Into::into)) @@ -778,14 +604,14 @@ impl BackendService for Backend { // notes //------------------------------------------------------------------- - fn new_note(&self, input: pb::NoteTypeId) -> BackendResult { + fn new_note(&self, input: pb::NoteTypeId) -> Result { self.with_col(|col| { let nt = col.get_notetype(input.into())?.ok_or(AnkiError::NotFound)?; Ok(nt.new_note().into()) }) } - fn add_note(&self, input: pb::AddNoteIn) -> BackendResult { + fn add_note(&self, input: pb::AddNoteIn) -> Result { self.with_col(|col| { let mut note: Note = input.note.ok_or(AnkiError::NotFound)?.into(); col.add_note(&mut note, DeckID(input.deck_id)) @@ -793,17 +619,14 @@ impl BackendService for Backend { }) } - fn defaults_for_adding( - &self, - input: pb::DefaultsForAddingIn, - ) -> BackendResult { + fn defaults_for_adding(&self, input: pb::DefaultsForAddingIn) -> Result { self.with_col(|col| { let home_deck: DeckID = input.home_deck_of_current_review_card.into(); col.defaults_for_adding(home_deck).map(Into::into) }) } - fn default_deck_for_notetype(&self, input: pb::NoteTypeId) -> BackendResult { + fn default_deck_for_notetype(&self, input: pb::NoteTypeId) -> Result { self.with_col(|col| { Ok(col .default_deck_for_notetype(input.into())? @@ -812,7 +635,7 @@ impl BackendService for Backend { }) } - fn update_note(&self, input: pb::UpdateNoteIn) -> BackendResult { + fn update_note(&self, input: pb::UpdateNoteIn) -> Result { self.with_col(|col| { let op = if input.skip_undo_entry { None @@ -825,7 +648,7 @@ impl BackendService for Backend { .map(Into::into) } - fn get_note(&self, input: pb::NoteId) -> BackendResult { + fn get_note(&self, input: pb::NoteId) -> Result { self.with_col(|col| { col.storage .get_note(input.into())? @@ -834,7 +657,7 @@ impl BackendService for Backend { }) } - fn remove_notes(&self, input: pb::RemoveNotesIn) -> BackendResult { + fn remove_notes(&self, input: pb::RemoveNotesIn) -> Result { self.with_col(|col| { if !input.note_ids.is_empty() { col.remove_notes( @@ -859,7 +682,7 @@ impl BackendService for Backend { }) } - fn add_note_tags(&self, input: pb::AddNoteTagsIn) -> BackendResult { + fn add_note_tags(&self, input: pb::AddNoteTagsIn) -> Result { self.with_col(|col| { col.add_tags_to_notes(&to_nids(input.nids), &input.tags) .map(|n| n as u32) @@ -867,7 +690,7 @@ impl BackendService for Backend { .map(Into::into) } - fn update_note_tags(&self, input: pb::UpdateNoteTagsIn) -> BackendResult { + fn update_note_tags(&self, input: pb::UpdateNoteTagsIn) -> Result { self.with_col(|col| { col.replace_tags_for_notes( &to_nids(input.nids), @@ -879,7 +702,7 @@ impl BackendService for Backend { }) } - fn cloze_numbers_in_note(&self, note: pb::Note) -> BackendResult { + fn cloze_numbers_in_note(&self, note: pb::Note) -> Result { let mut set = HashSet::with_capacity(4); for field in ¬e.fields { add_cloze_numbers_in_string(field, &mut set); @@ -889,7 +712,7 @@ impl BackendService for Backend { }) } - fn after_note_updates(&self, input: pb::AfterNoteUpdatesIn) -> BackendResult { + fn after_note_updates(&self, input: pb::AfterNoteUpdatesIn) -> Result { self.with_col(|col| { col.transact(None, |col| { col.after_note_updates( @@ -905,7 +728,7 @@ impl BackendService for Backend { fn field_names_for_notes( &self, input: pb::FieldNamesForNotesIn, - ) -> BackendResult { + ) -> Result { self.with_col(|col| { let nids: Vec<_> = input.nids.into_iter().map(NoteID).collect(); col.storage @@ -914,10 +737,7 @@ impl BackendService for Backend { }) } - fn note_is_duplicate_or_empty( - &self, - input: pb::Note, - ) -> BackendResult { + fn note_is_duplicate_or_empty(&self, input: pb::Note) -> Result { let note: Note = input.into(); self.with_col(|col| { col.note_is_duplicate_or_empty(¬e) @@ -925,7 +745,7 @@ impl BackendService for Backend { }) } - fn cards_of_note(&self, input: pb::NoteId) -> BackendResult { + fn cards_of_note(&self, input: pb::NoteId) -> Result { self.with_col(|col| { col.storage .all_card_ids_of_note(NoteID(input.nid)) @@ -938,10 +758,7 @@ impl BackendService for Backend { // notetypes //------------------------------------------------------------------- - fn add_or_update_notetype( - &self, - input: pb::AddOrUpdateNotetypeIn, - ) -> BackendResult { + fn add_or_update_notetype(&self, input: pb::AddOrUpdateNotetypeIn) -> Result { self.with_col(|col| { let legacy: NoteTypeSchema11 = serde_json::from_slice(&input.json)?; let mut nt: NoteType = legacy.into(); @@ -954,7 +771,7 @@ impl BackendService for Backend { }) } - fn get_stock_notetype_legacy(&self, input: pb::StockNoteType) -> BackendResult { + fn get_stock_notetype_legacy(&self, input: pb::StockNoteType) -> Result { // fixme: use individual functions instead of full vec let mut all = all_stock_notetypes(&self.i18n); let idx = (input.kind as usize).min(all.len() - 1); @@ -965,7 +782,7 @@ impl BackendService for Backend { .map(Into::into) } - fn get_notetype_legacy(&self, input: pb::NoteTypeId) -> BackendResult { + fn get_notetype_legacy(&self, input: pb::NoteTypeId) -> Result { self.with_col(|col| { let schema11: NoteTypeSchema11 = col .storage @@ -976,7 +793,7 @@ impl BackendService for Backend { }) } - fn get_notetype_names(&self, _input: Empty) -> BackendResult { + fn get_notetype_names(&self, _input: Empty) -> Result { self.with_col(|col| { let entries: Vec<_> = col .storage @@ -988,7 +805,7 @@ impl BackendService for Backend { }) } - fn get_notetype_names_and_counts(&self, _input: Empty) -> BackendResult { + fn get_notetype_names_and_counts(&self, _input: Empty) -> Result { self.with_col(|col| { let entries: Vec<_> = col .storage @@ -1004,7 +821,7 @@ impl BackendService for Backend { }) } - fn get_notetype_id_by_name(&self, input: pb::String) -> BackendResult { + fn get_notetype_id_by_name(&self, input: pb::String) -> Result { self.with_col(|col| { col.storage .get_notetype_id(&input.val) @@ -1013,7 +830,7 @@ impl BackendService for Backend { }) } - fn remove_notetype(&self, input: pb::NoteTypeId) -> BackendResult { + fn remove_notetype(&self, input: pb::NoteTypeId) -> Result { self.with_col(|col| col.remove_notetype(input.into())) .map(Into::into) } @@ -1021,7 +838,7 @@ impl BackendService for Backend { // collection //------------------------------------------------------------------- - fn open_collection(&self, input: pb::OpenCollectionIn) -> BackendResult { + fn open_collection(&self, input: pb::OpenCollectionIn) -> Result { let mut col = self.col.lock().unwrap(); if col.is_some() { return Err(AnkiError::CollectionAlreadyOpen); @@ -1050,7 +867,7 @@ impl BackendService for Backend { Ok(().into()) } - fn close_collection(&self, input: pb::CloseCollectionIn) -> BackendResult { + fn close_collection(&self, input: pb::CloseCollectionIn) -> Result { self.abort_media_sync_and_wait(); let mut col = self.col.lock().unwrap(); @@ -1069,7 +886,7 @@ impl BackendService for Backend { Ok(().into()) } - fn check_database(&self, _input: pb::Empty) -> BackendResult { + fn check_database(&self, _input: pb::Empty) -> Result { let mut handler = self.new_progress_handler(); let progress_fn = move |progress, throttle| { handler.update(Progress::DatabaseCheck(progress), throttle); @@ -1103,11 +920,11 @@ impl BackendService for Backend { // sync //------------------------------------------------------------------- - fn sync_media(&self, input: pb::SyncAuth) -> BackendResult { + fn sync_media(&self, input: pb::SyncAuth) -> Result { self.sync_media_inner(input).map(Into::into) } - fn abort_sync(&self, _input: Empty) -> BackendResult { + fn abort_sync(&self, _input: Empty) -> Result { if let Some(handle) = self.sync_abort.lock().unwrap().take() { handle.abort(); } @@ -1115,7 +932,7 @@ impl BackendService for Backend { } /// Abort the media sync. Does not wait for completion. - fn abort_media_sync(&self, _input: Empty) -> BackendResult { + fn abort_media_sync(&self, _input: Empty) -> Result { let guard = self.state.lock().unwrap(); if let Some(handle) = &guard.media_sync_abort { handle.abort(); @@ -1123,33 +940,33 @@ impl BackendService for Backend { Ok(().into()) } - fn before_upload(&self, _input: Empty) -> BackendResult { + fn before_upload(&self, _input: Empty) -> Result { self.with_col(|col| col.before_upload().map(Into::into)) } - fn sync_login(&self, input: pb::SyncLoginIn) -> BackendResult { + fn sync_login(&self, input: pb::SyncLoginIn) -> Result { self.sync_login_inner(input) } - fn sync_status(&self, input: pb::SyncAuth) -> BackendResult { + fn sync_status(&self, input: pb::SyncAuth) -> Result { self.sync_status_inner(input) } - fn sync_collection(&self, input: pb::SyncAuth) -> BackendResult { + fn sync_collection(&self, input: pb::SyncAuth) -> Result { self.sync_collection_inner(input) } - fn full_upload(&self, input: pb::SyncAuth) -> BackendResult { + fn full_upload(&self, input: pb::SyncAuth) -> Result { self.full_sync_inner(input, true)?; Ok(().into()) } - fn full_download(&self, input: pb::SyncAuth) -> BackendResult { + fn full_download(&self, input: pb::SyncAuth) -> Result { self.full_sync_inner(input, false)?; Ok(().into()) } - fn sync_server_method(&self, input: pb::SyncServerMethodIn) -> BackendResult { + fn sync_server_method(&self, input: pb::SyncServerMethodIn) -> Result { let req = SyncRequest::from_method_and_data(input.method(), input.data)?; self.sync_server_method_inner(req).map(Into::into) } @@ -1157,7 +974,7 @@ impl BackendService for Backend { // i18n/messages //------------------------------------------------------------------- - fn translate_string(&self, input: pb::TranslateStringIn) -> BackendResult { + fn translate_string(&self, input: pb::TranslateStringIn) -> Result { let key = match crate::fluent_proto::FluentString::from_i32(input.key) { Some(key) => key, None => return Ok("invalid key".to_string().into()), @@ -1172,7 +989,7 @@ impl BackendService for Backend { Ok(self.i18n.trn(key, map).into()) } - fn format_timespan(&self, input: pb::FormatTimespanIn) -> BackendResult { + fn format_timespan(&self, input: pb::FormatTimespanIn) -> Result { use pb::format_timespan_in::Context; Ok(match input.context() { Context::Precise => time_span(input.seconds, &self.i18n, true), @@ -1182,13 +999,13 @@ impl BackendService for Backend { .into()) } - fn i18n_resources(&self, _input: Empty) -> BackendResult { + fn i18n_resources(&self, _input: Empty) -> Result { serde_json::to_vec(&self.i18n.resources_for_js()) .map(Into::into) .map_err(Into::into) } - fn render_markdown(&self, input: pb::RenderMarkdownIn) -> BackendResult { + fn render_markdown(&self, input: pb::RenderMarkdownIn) -> Result { let mut text = render_markdown(&input.markdown); if input.sanitize { // currently no images @@ -1200,11 +1017,11 @@ impl BackendService for Backend { // tags //------------------------------------------------------------------- - fn clear_unused_tags(&self, _input: pb::Empty) -> BackendResult { + fn clear_unused_tags(&self, _input: pb::Empty) -> Result { self.with_col(|col| col.transact(None, |col| col.clear_unused_tags().map(Into::into))) } - fn all_tags(&self, _input: Empty) -> BackendResult { + fn all_tags(&self, _input: Empty) -> Result { Ok(pb::StringList { vals: self.with_col(|col| { Ok(col @@ -1217,7 +1034,7 @@ impl BackendService for Backend { }) } - fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> BackendResult { + fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> Result { self.with_col(|col| { col.transact(None, |col| { col.set_tag_expanded(&input.name, input.expanded)?; @@ -1226,7 +1043,7 @@ impl BackendService for Backend { }) } - fn clear_tag(&self, tag: pb::String) -> BackendResult { + fn clear_tag(&self, tag: pb::String) -> Result { self.with_col(|col| { col.transact(None, |col| { col.storage.clear_tag_and_children(tag.val.as_str())?; @@ -1239,7 +1056,7 @@ impl BackendService for Backend { self.with_col(|col| col.tag_tree()) } - fn drag_drop_tags(&self, input: pb::DragDropTagsIn) -> BackendResult { + fn drag_drop_tags(&self, input: pb::DragDropTagsIn) -> Result { let source_tags = input.source_tags; let target_tag = if input.target_tag.is_empty() { None @@ -1253,7 +1070,7 @@ impl BackendService for Backend { // config/preferences //------------------------------------------------------------------- - fn get_config_json(&self, input: pb::String) -> BackendResult { + fn get_config_json(&self, input: pb::String) -> Result { self.with_col(|col| { let val: Option = col.get_config_optional(input.val.as_str()); val.ok_or(AnkiError::NotFound) @@ -1262,7 +1079,7 @@ impl BackendService for Backend { }) } - fn set_config_json(&self, input: pb::SetConfigJsonIn) -> BackendResult { + fn set_config_json(&self, input: pb::SetConfigJsonIn) -> Result { self.with_col(|col| { col.transact(None, |col| { // ensure it's a well-formed object @@ -1273,12 +1090,12 @@ impl BackendService for Backend { .map(Into::into) } - fn remove_config(&self, input: pb::String) -> BackendResult { + fn remove_config(&self, input: pb::String) -> Result { self.with_col(|col| col.transact(None, |col| col.remove_config(input.val.as_str()))) .map(Into::into) } - fn get_all_config(&self, _input: Empty) -> BackendResult { + fn get_all_config(&self, _input: Empty) -> Result { self.with_col(|col| { let conf = col.storage.get_all_config()?; serde_json::to_vec(&conf).map_err(Into::into) @@ -1286,7 +1103,7 @@ impl BackendService for Backend { .map(Into::into) } - fn get_config_bool(&self, input: pb::config::Bool) -> BackendResult { + fn get_config_bool(&self, input: pb::config::Bool) -> Result { self.with_col(|col| { Ok(pb::Bool { val: col.get_bool(input.key().into()), @@ -1294,12 +1111,12 @@ impl BackendService for Backend { }) } - fn set_config_bool(&self, input: pb::SetConfigBoolIn) -> BackendResult { + fn set_config_bool(&self, input: pb::SetConfigBoolIn) -> Result { self.with_col(|col| col.transact(None, |col| col.set_bool(input.key().into(), input.value))) .map(Into::into) } - fn get_config_string(&self, input: pb::config::String) -> BackendResult { + fn get_config_string(&self, input: pb::config::String) -> Result { self.with_col(|col| { Ok(pb::String { val: col.get_string(input.key().into()), @@ -1307,18 +1124,18 @@ impl BackendService for Backend { }) } - fn set_config_string(&self, input: pb::SetConfigStringIn) -> BackendResult { + fn set_config_string(&self, input: pb::SetConfigStringIn) -> Result { self.with_col(|col| { col.transact(None, |col| col.set_string(input.key().into(), &input.value)) }) .map(Into::into) } - fn get_preferences(&self, _input: Empty) -> BackendResult { + fn get_preferences(&self, _input: Empty) -> Result { self.with_col(|col| col.get_preferences()) } - fn set_preferences(&self, input: pb::Preferences) -> BackendResult { + fn set_preferences(&self, input: pb::Preferences) -> Result { self.with_col(|col| col.set_preferences(input)) .map(Into::into) } @@ -1344,13 +1161,24 @@ impl Backend { &self.i18n } - pub fn run_command_bytes(&self, method: u32, input: &[u8]) -> result::Result, Vec> { - self.run_command_bytes2_inner(method, input).map_err(|err| { - let backend_err = anki_error_to_proto_error(err, &self.i18n); - let mut bytes = Vec::new(); - backend_err.encode(&mut bytes).unwrap(); - bytes - }) + pub fn run_method( + &self, + service: u32, + method: u32, + input: &[u8], + ) -> result::Result, Vec> { + pb::ServiceIndex::from_i32(service as i32) + .ok_or_else(|| AnkiError::invalid_input("invalid service")) + .and_then(|service| match service { + pb::ServiceIndex::Scheduling => SchedulingService::run_method(self, method, input), + pb::ServiceIndex::Backend => BackendService::run_method(self, method, input), + }) + .map_err(|err| { + let backend_err = anki_error_to_proto_error(err, &self.i18n); + let mut bytes = Vec::new(); + backend_err.encode(&mut bytes).unwrap(); + bytes + }) } /// If collection is open, run the provided closure while holding diff --git a/rslib/src/backend/scheduler/mod.rs b/rslib/src/backend/scheduler/mod.rs index 8663da96c..73b0b1536 100644 --- a/rslib/src/backend/scheduler/mod.rs +++ b/rslib/src/backend/scheduler/mod.rs @@ -3,3 +3,175 @@ mod answering; mod states; + +use super::Backend; +use crate::{ + backend_proto::{self as pb}, + prelude::*, + scheduler::{ + new::NewCardSortOrder, + parse_due_date_str, + states::{CardState, NextCardStates}, + }, + stats::studied_today, +}; +pub(super) use pb::scheduling::Service as SchedulingService; + +impl SchedulingService for Backend { + /// This behaves like _updateCutoff() in older code - it also unburies at the start of + /// a new day. + fn sched_timing_today(&self, _input: pb::Empty) -> Result { + self.with_col(|col| { + let timing = col.timing_today()?; + col.unbury_if_day_rolled_over(timing)?; + Ok(timing.into()) + }) + } + + /// Fetch data from DB and return rendered string. + fn studied_today(&self, _input: pb::Empty) -> Result { + self.with_col(|col| col.studied_today().map(Into::into)) + } + + /// Message rendering only, for old graphs. + fn studied_today_message(&self, input: pb::StudiedTodayMessageIn) -> Result { + Ok(studied_today(input.cards, input.seconds as f32, &self.i18n).into()) + } + + fn update_stats(&self, input: pb::UpdateStatsIn) -> Result { + self.with_col(|col| { + col.transact(None, |col| { + let today = col.current_due_day(0)?; + let usn = col.usn()?; + col.update_deck_stats(today, usn, input).map(Into::into) + }) + }) + } + + fn extend_limits(&self, input: pb::ExtendLimitsIn) -> Result { + self.with_col(|col| { + col.transact(None, |col| { + let today = col.current_due_day(0)?; + let usn = col.usn()?; + col.extend_limits( + today, + usn, + input.deck_id.into(), + input.new_delta, + input.review_delta, + ) + .map(Into::into) + }) + }) + } + + fn counts_for_deck_today(&self, input: pb::DeckId) -> Result { + self.with_col(|col| col.counts_for_deck_today(input.did.into())) + } + + fn congrats_info(&self, _input: pb::Empty) -> Result { + self.with_col(|col| col.congrats_info()) + } + + fn restore_buried_and_suspended_cards(&self, input: pb::CardIDs) -> Result { + let cids: Vec<_> = input.into(); + self.with_col(|col| col.unbury_or_unsuspend_cards(&cids).map(Into::into)) + } + + fn unbury_cards_in_current_deck( + &self, + input: pb::UnburyCardsInCurrentDeckIn, + ) -> Result { + self.with_col(|col| { + col.unbury_cards_in_current_deck(input.mode()) + .map(Into::into) + }) + } + + fn bury_or_suspend_cards(&self, input: pb::BuryOrSuspendCardsIn) -> Result { + self.with_col(|col| { + let mode = input.mode(); + let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); + col.bury_or_suspend_cards(&cids, mode).map(Into::into) + }) + } + + fn empty_filtered_deck(&self, input: pb::DeckId) -> Result { + self.with_col(|col| col.empty_filtered_deck(input.did.into()).map(Into::into)) + } + + fn rebuild_filtered_deck(&self, input: pb::DeckId) -> Result { + self.with_col(|col| col.rebuild_filtered_deck(input.did.into()).map(Into::into)) + } + + fn schedule_cards_as_new(&self, input: pb::ScheduleCardsAsNewIn) -> Result { + self.with_col(|col| { + let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); + let log = input.log; + col.reschedule_cards_as_new(&cids, log).map(Into::into) + }) + } + + fn set_due_date(&self, input: pb::SetDueDateIn) -> Result { + let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); + let spec = parse_due_date_str(&input.days)?; + self.with_col(|col| col.set_due_date(&cids, spec).map(Into::into)) + } + + fn sort_cards(&self, input: pb::SortCardsIn) -> Result { + let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); + let (start, step, random, shift) = ( + input.starting_from, + input.step_size, + input.randomize, + input.shift_existing, + ); + let order = if random { + NewCardSortOrder::Random + } else { + NewCardSortOrder::Preserve + }; + self.with_col(|col| { + col.sort_cards(&cids, start, step, order, shift) + .map(Into::into) + }) + } + + fn sort_deck(&self, input: pb::SortDeckIn) -> Result { + self.with_col(|col| { + col.sort_deck(input.deck_id.into(), input.randomize) + .map(Into::into) + }) + } + + fn get_next_card_states(&self, input: pb::CardId) -> Result { + let cid: CardID = input.into(); + self.with_col(|col| col.get_next_card_states(cid)) + .map(Into::into) + } + + fn describe_next_states(&self, input: pb::NextCardStates) -> Result { + let states: NextCardStates = input.into(); + self.with_col(|col| col.describe_next_states(states)) + .map(Into::into) + } + + fn state_is_leech(&self, input: pb::SchedulingState) -> Result { + let state: CardState = input.into(); + Ok(state.leeched().into()) + } + + fn answer_card(&self, input: pb::AnswerCardIn) -> Result { + self.with_col(|col| col.answer_card(&input.into())) + .map(Into::into) + } + + fn upgrade_scheduler(&self, _input: pb::Empty) -> Result { + self.with_col(|col| col.transact(None, |col| col.upgrade_to_v2_scheduler())) + .map(Into::into) + } + + fn get_queued_cards(&self, input: pb::GetQueuedCardsIn) -> Result { + self.with_col(|col| col.get_queued_cards(input.fetch_limit, input.intraday_learning_only)) + } +}