anki/ts/reviewer/lib.test.ts

148 lines
5.5 KiB
TypeScript
Raw Normal View History

Migrate to protobuf-es (#2547) * Fix .no-reduce-motion missing from graphs spinner, and not being honored * Begin migration from protobuf.js -> protobuf-es Motivation: - Protobuf-es has a nicer API: messages are represented as classes, and fields which should exist are not marked as nullable. - As it uses modules, only the proto messages we actually use get included in our bundle output. Protobuf.js put everything in a namespace, which prevented tree-shaking, and made it awkward to access inner messages. - ./run after touching a proto file drops from about 8s to 6s on my machine. The tradeoff is slower decoding/encoding (#2043), but that was mainly a concern for the graphs page, and was unblocked by https://github.com/ankitects/anki/commit/37151213cd9d431f449ba4b3bc4c0329a1d9af78 Approach/notes: - We generate the new protobuf-es interface in addition to existing protobuf.js interface, so we can migrate a module at a time, starting with the graphs module. - rslib:proto now generates RPC methods for TS in addition to the Python interface. The input-arg-unrolling behaviour of the Python generation is not required here, as we declare the input arg as a PlainMessage<T>, which marks it as requiring all fields to be provided. - i64 is represented as bigint in protobuf-es. We were using a patch to protobuf.js to get it to output Javascript numbers instead of long.js types, but now that our supported browser versions support bigint, it's probably worth biting the bullet and migrating to bigint use. Our IDs fit comfortably within MAX_SAFE_INTEGER, but that may not hold for future fields we add. - Oneofs are handled differently in protobuf-es, and are going to need some refactoring. Other notable changes: - Added a --mkdir arg to our build runner, so we can create a dir easily during the build on Windows. - Simplified the preference handling code, by wrapping the preferences in an outer store, instead of a separate store for each individual preference. This means a change to one preference will trigger a redraw of all components that depend on the preference store, but the redrawing is cheap after moving the data processing to Rust, and it makes the code easier to follow. - Drop async(Reactive).ts in favour of more explicit handling with await blocks/updating. - Renamed add_inputs_to_group() -> add_dependency(), and fixed it not adding dependencies to parent groups. Renamed add() -> add_action() for clarity. * Remove a couple of unused proto imports * Migrate card info * Migrate congrats, image occlusion, and tag editor + Fix imports for multi-word proto files. * Migrate change-notetype * Migrate deck options * Bump target to es2020; simplify ts lib list Have used caniuse.com to confirm Chromium 77, iOS 14.5 and the Chrome on Android support the full es2017-es2020 features. * Migrate import-csv * Migrate i18n and fix missing output types in .js * Migrate custom scheduling, and remove protobuf.js To mostly maintain our old API contract, we make use of protobuf-es's ability to convert to JSON, which follows the same format as protobuf.js did. It doesn't cover all case: users who were previously changing the variant of a type will need to update their code, as assigning to a new variant no longer automatically removes the old one, which will cause an error when we try to convert back from JSON. But I suspect the large majority of users are adjusting the current variant rather than creating a new one, and this saves us having to write proxy wrappers, so it seems like a reasonable compromise. One other change I made at the same time was to rename value->kind for the oneofs in our custom study protos, as 'value' was easily confused with the 'case/value' output that protobuf-es has. With protobuf.js codegen removed, touching a proto file and invoking ./run drops from about 8s to 6s. This closes #2043. * Allow tree-shaking on protobuf types * Display backend error messages in our ts alert() * Make sourcemap generation opt-in for ts-run Considerably slows down build, and not used most of the time.
2023-06-14 14:47:37 +02:00
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
Improve presentation of importing results (#2568) * Implement import log screen in Svelte * Show filename in import log screen title * Remove unused NoteRow property * Show number of imported notes * Use a single nid expression * Use 'count' as variable name for consistency * Import from @tslib/backend instead * Fix summary_template typing * Fix clippy warning * Apply suggestions from code review * Fix imports * Contents -> Fields * Increase max length of browser search bar https://github.com/ankitects/anki/pull/2568/files#r1255227035 * Fix race condition in Bootstrap tooltip destruction https://github.com/twbs/bootstrap/issues/37474 * summary_template -> summaryTemplate * Make show link a button * Run import ops on Svelte side * Fix geometry not being restored in CSV Import page * Make VirtualTable fill available height * Keep CSV dialog modal * Reword importing-existing-notes-skipped * Avoid mentioning matching based on first field * Change tick and cross icons * List skipped notes last * Pure CSS spinner * Move set_wants_abort() call to relevant dialogs * Show number of imported cards * Remove bold from first sentence and indent summaries * Update UI after import operations * Add close button to import log page Also make virtual table react to resize event. * Fix typing * Make CSV dialog non-modal again Otherwise user can't interact with browser window. * Update window modality after import * Commit DB and update undo actions after import op * Split frontend proto into separate file, so backend can ignore it Currently the automatically-generated frontend RPC methods get placed in 'backend.js' with all the backend methods; we could optionally split them into a separate 'frontend.js' file in the future. * Migrate import_done from a bridgecmd to a HTTP request * Update plural form of importing-notes-added * Move import response handling to mediasrv * Move task callback to script section * Avoid unnecessary :global() * .log cannot be missing if result exists * Move import log search handling to mediasrv * Type common params of ImportLogDialog * Use else if * Remove console.log() * Add way to test apkg imports in new log screen * Remove unused import * Get actual card count for CSV imports * Use import type * Fix typing error * Ignore import log when checking for changes in Python layer * Apply suggestions from code review * Remove imported card count for now * Avoid non-null assertion in assignment * Change showInBrowser to take an array of notes * Use dataclasses for import log args * Simplify ResultWithChanges in TS * Only abort import when window is modal * Fix ResultWithChanges typing * Fix Rust warnings * Only log one duplicate per incoming note * Update wording about note updates * Remove caveat about found_notes * Reduce font size * Remove redundant map * Give credit to loading.io * Remove unused line --------- Co-authored-by: RumovZ <gp5glkw78@relay.firefox.com>
2023-08-02 12:29:44 +02:00
import { SchedulingStatesWithContext } from "@tslib/anki/frontend_pb";
import { SchedulingContext, SchedulingStates } from "@tslib/anki/scheduler_pb";
Migrate to protobuf-es (#2547) * Fix .no-reduce-motion missing from graphs spinner, and not being honored * Begin migration from protobuf.js -> protobuf-es Motivation: - Protobuf-es has a nicer API: messages are represented as classes, and fields which should exist are not marked as nullable. - As it uses modules, only the proto messages we actually use get included in our bundle output. Protobuf.js put everything in a namespace, which prevented tree-shaking, and made it awkward to access inner messages. - ./run after touching a proto file drops from about 8s to 6s on my machine. The tradeoff is slower decoding/encoding (#2043), but that was mainly a concern for the graphs page, and was unblocked by https://github.com/ankitects/anki/commit/37151213cd9d431f449ba4b3bc4c0329a1d9af78 Approach/notes: - We generate the new protobuf-es interface in addition to existing protobuf.js interface, so we can migrate a module at a time, starting with the graphs module. - rslib:proto now generates RPC methods for TS in addition to the Python interface. The input-arg-unrolling behaviour of the Python generation is not required here, as we declare the input arg as a PlainMessage<T>, which marks it as requiring all fields to be provided. - i64 is represented as bigint in protobuf-es. We were using a patch to protobuf.js to get it to output Javascript numbers instead of long.js types, but now that our supported browser versions support bigint, it's probably worth biting the bullet and migrating to bigint use. Our IDs fit comfortably within MAX_SAFE_INTEGER, but that may not hold for future fields we add. - Oneofs are handled differently in protobuf-es, and are going to need some refactoring. Other notable changes: - Added a --mkdir arg to our build runner, so we can create a dir easily during the build on Windows. - Simplified the preference handling code, by wrapping the preferences in an outer store, instead of a separate store for each individual preference. This means a change to one preference will trigger a redraw of all components that depend on the preference store, but the redrawing is cheap after moving the data processing to Rust, and it makes the code easier to follow. - Drop async(Reactive).ts in favour of more explicit handling with await blocks/updating. - Renamed add_inputs_to_group() -> add_dependency(), and fixed it not adding dependencies to parent groups. Renamed add() -> add_action() for clarity. * Remove a couple of unused proto imports * Migrate card info * Migrate congrats, image occlusion, and tag editor + Fix imports for multi-word proto files. * Migrate change-notetype * Migrate deck options * Bump target to es2020; simplify ts lib list Have used caniuse.com to confirm Chromium 77, iOS 14.5 and the Chrome on Android support the full es2017-es2020 features. * Migrate import-csv * Migrate i18n and fix missing output types in .js * Migrate custom scheduling, and remove protobuf.js To mostly maintain our old API contract, we make use of protobuf-es's ability to convert to JSON, which follows the same format as protobuf.js did. It doesn't cover all case: users who were previously changing the variant of a type will need to update their code, as assigning to a new variant no longer automatically removes the old one, which will cause an error when we try to convert back from JSON. But I suspect the large majority of users are adjusting the current variant rather than creating a new one, and this saves us having to write proxy wrappers, so it seems like a reasonable compromise. One other change I made at the same time was to rename value->kind for the oneofs in our custom study protos, as 'value' was easily confused with the 'case/value' output that protobuf-es has. With protobuf.js codegen removed, touching a proto file and invoking ./run drops from about 8s to 6s. This closes #2043. * Allow tree-shaking on protobuf types * Display backend error messages in our ts alert() * Make sourcemap generation opt-in for ts-run Considerably slows down build, and not used most of the time.
2023-06-14 14:47:37 +02:00
import { applyStateTransform } from "./answering";
/* eslint
@typescript-eslint/no-explicit-any: "off",
*/
function exampleInput(): SchedulingStatesWithContext {
return SchedulingStatesWithContext.fromJson(
{
"states": {
"current": {
"normal": {
"review": {
"scheduledDays": 1,
"elapsedDays": 2,
"easeFactor": 1.850000023841858,
"lapses": 4,
"leeched": false,
},
},
"customData": "{\"v\":\"v3.20.0\",\"seed\":2104,\"d\":5.39,\"s\":11.06}",
},
"again": {
"normal": {
"relearning": {
"review": {
"scheduledDays": 1,
"elapsedDays": 0,
"easeFactor": 1.649999976158142,
"lapses": 5,
"leeched": false,
},
"learning": {
"remainingSteps": 1,
"scheduledSecs": 600,
},
},
},
},
"hard": {
"normal": {
"review": {
"scheduledDays": 2,
"elapsedDays": 0,
"easeFactor": 1.7000000476837158,
"lapses": 4,
"leeched": false,
},
},
},
"good": {
"normal": {
"review": {
"scheduledDays": 4,
"elapsedDays": 0,
"easeFactor": 1.850000023841858,
"lapses": 4,
"leeched": false,
},
},
},
"easy": {
"normal": {
"review": {
"scheduledDays": 6,
"elapsedDays": 0,
"easeFactor": 2,
"lapses": 4,
"leeched": false,
},
},
},
},
"context": { "deckName": "hello", "seed": 123 },
},
);
}
test("can change oneof", () => {
let states = exampleInput().states!;
const jsonStates = states.toJson({ "emitDefaultValues": true }) as any;
// again should be a relearning state
const inner = states.again?.kind?.value?.kind;
assert(inner?.case === "relearning");
expect(inner.value.learning?.remainingSteps).toBe(1);
// change it to a review state
jsonStates.again.normal = { "review": jsonStates.again.normal.relearning.review };
states = SchedulingStates.fromJson(jsonStates);
const inner2 = states.again?.kind?.value?.kind;
assert(inner2?.case === "review");
// however, it's not valid to have multiple oneofs set
jsonStates.again.normal = { "review": jsonStates.again.normal.review, "learning": {} };
expect(() => {
SchedulingStates.fromJson(jsonStates);
}).toThrow();
});
test("no-op transform", async () => {
const input = exampleInput();
const output = await applyStateTransform(input, async (states: any, customData, ctx) => {
expect(ctx.deckName).toBe("hello");
expect(customData.easy.seed).toBe(2104);
expect(states!.again!.normal!.relearning!.learning!.remainingSteps).toBe(1);
});
// the input only has customData set on `current`, so we need to update it
// before we compare the two as equal
input.states!.again!.customData = input.states!.current!.customData;
input.states!.hard!.customData = input.states!.current!.customData;
input.states!.good!.customData = input.states!.current!.customData;
input.states!.easy!.customData = input.states!.current!.customData;
expect(output).toStrictEqual(input.states);
});
test("custom data change", async () => {
const output = await applyStateTransform(exampleInput(), async (_states: any, customData, _ctx) => {
customData.easy = { foo: "hello world" };
});
expect(output!.hard!.customData).not.toMatch(/hello world/);
expect(output!.easy!.customData).toBe("{\"foo\":\"hello world\"}");
});
test("adjust interval", async () => {
const output = await applyStateTransform(exampleInput(), async (states: any, _customData, _ctx) => {
states.good.normal.review.scheduledDays = 10;
});
const kind = output.good?.kind?.value?.kind;
assert(kind?.case === "review");
expect(kind.value.scheduledDays).toBe(10);
});
test("default context values exist", async () => {
const ctx = SchedulingContext.fromBinary(new Uint8Array());
expect(ctx.deckName).toBe("");
expect(ctx.seed).toBe(0n);
});
function assert(condition: boolean): asserts condition {
if (!condition) {
throw new Error();
}
}