Repository navigation
Architecture: Note/Annotation/Bookmark memory data, and sync model #3859
panaC
started this conversation in
Developer corner
Replies: 4 comments
|
Algorithm used when importing a Readium .annotation file into Thorium. flowchart TD
subgraph USER["User journey"]
A([Start]) --> B["Open a publication<br/>and choose “Import notes”"]
B --> C{File selected?}
C -- No / Cancel --> END1([Import cancelled])
C -- Yes: .annotation file --> D["Read and parse the file"]
end
subgraph VALIDATION["File and publication validation"]
D --> E{Valid Readium<br/>Annotation Set?}
E -- No --> ERR1["Show parsing error"]
E -- Yes --> F{File contains notes?}
F -- No --> EMPTY["Show “No notes available”"]
F -- Yes --> G["Load the current publication spine"]
G --> H["Resolve every target.source:<br/>1. Exact spine href<br/>2. Normalized path or URL<br/>3. Unique suffix match"]
H --> I{Every source resolves to<br/>exactly one spine item?}
I -- No --> ERR2["Reject the entire file<br/>At least one note does not<br/>belong to the publication"]
I -- Yes --> J["Replace each source with<br/>the exact spine href"]
J --> K["Load existing notes<br/>for the publication"]
end
subgraph ITEM["Per-note conversion and classification"]
K --> L["Process each incoming note"]
L --> M{Has a supported selector?}
M -- No --> SKIP["Skip this note"]
M -- Yes --> N["Convert to a Thorium note:<br/>• Extract or generate UUID<br/>• Convert text, color and highlight style<br/>• Use the filename as a tag<br/>• Clamp future timestamps<br/>• Preserve the normalized target<br/>• Classify as bookmark or annotation"]
N --> O{Existing note with<br/>the same UUID?}
O -- No --> NC["Conflict-free"]
O -- Yes --> P{Compare modified timestamps}
P -- "Incoming is later, or only<br/>incoming has modified" --> NEWER["Newer conflict"]
P -- "Incoming is earlier, or only<br/>existing has modified" --> OLDER["Older conflict"]
P -- "Equal timestamps, or neither<br/>has modified" --> SAME["Already represented<br/>No import bucket"]
SKIP --> LOOP{More incoming notes?}
NC --> LOOP
NEWER --> LOOP
OLDER --> LOOP
SAME --> LOOP
LOOP -- Yes --> L
LOOP -- No --> Q{Any note passed<br/>selector conversion?}
Q -- No --> NOTHING["Show “No note ready<br/>to be imported”"]
Q -- Yes --> R{Any conflict-free,<br/>newer or older notes?}
R -- No --> ALREADY["Show “All notes are<br/>already imported”"]
R -- Yes --> MODAL["Show confirmation dialog:<br/>• Origin and annotation-set title<br/>• Creator information<br/>• Conflict-free count<br/>• Newer-conflict count<br/>• Older-conflict count"]
end
subgraph DECISION["User decision"]
MODAL --> S{User choice}
S -- Cancel --> END2([Import aborted])
S -- "Import only conflict-free notes" --> T["Selected set =<br/>conflict-free notes"]
S -- "Import all notes" --> U["Selected set =<br/>conflict-free + newer + older conflicts"]
T --> V["Send each selected note<br/>through the add/update pipeline"]
U --> V
V --> W["Record import analytics<br/>and show success message"]
W --> END3([Import completed])
end
ERR1 --> END4([Import stopped])
EMPTY --> END4
ERR2 --> END4
NOTHING --> END4
ALREADY --> END4
P -. "Conflict detection uses only<br/>UUID and modified timestamps.<br/>Content, position, color and creator<br/>are not compared." .-> NOTE["Heuristic"]
Key points
Questions for discussion
|
0 replies
|
Algorithm used when exporting notes from Thorium as a Readium flowchart TD
subgraph USER["User journey"]
A([Start]) --> B{Export entry point}
B -- "Publication menu in the library" --> C["Choose “Export notes”"]
C --> D["Fetch all stored notes<br/>for the publication"]
D --> E{Fetch and JSON<br/>parsing successful?}
E -- No --> ERR1["Log an error<br/>No file is downloaded"]
E -- Yes --> F["Use all stored notes<br/>Title = localized publication title<br/>Format = Readium .annotation"]
B -- "Annotations or bookmarks panel<br/>in the reader" --> G{Current filtered list<br/>contains notes?}
G -- No --> DISABLED["Export button is disabled"]
G -- Yes --> H["Open export options"]
H --> I["Optionally edit the title"]
I --> J["Choose output format:<br/>• Readium Annotations<br/>• HTML"]
J --> K["Use the current filtered and sorted<br/>annotation or bookmark list"]
F --> L["Start annotation-set conversion"]
K --> L
end
subgraph SET["Build the Readium Anno99tation Set"]
L --> M["Read the current UI locale"]
M --> N["Create set metadata:<br/>• New set UUID<br/>• Thorium name and version<br/>• Generation timestamp<br/>• User-provided title<br/>• Publication identifiers<br/>• Title, authors, publishers and date"]
N --> O["Process each selected note"]
O --> P{PDF annotation?}
P -- Yes --> SKIP["Skip the note<br/>No PDF target mapping is exported"]
P -- No --> Q["Convert to a Readium annotation"]
Q --> R["Map identity and dates:<br/>• UUID → urn:uuid<br/>• Created timestamp<br/>• Optional modified timestamp"]
R --> S["Map the body:<br/>• Note text<br/>• Color or default color<br/>• First tag only<br/>• Highlight style<br/>• text/plain format"]
S --> T["Map optional creator data<br/>only when a creator URN exists"]
T --> U["Map the target:<br/>• Locator href as source<br/>• Headings and page metadata<br/>• Previously generated export selectors"]
U --> V["Set motivation:<br/>• bookmarking for bookmarks<br/>• highlighting otherwise"]
SKIP --> W{More notes?}
V --> W
W -- Yes --> O
W -- No --> X["Complete the annotation set<br/>The items array may be empty"]
end
subgraph SERIALIZATION["Serialize the selected output format"]
X --> Y{Output format}
Y -- "Readium Annotations" --> JSON["Serialize as sorted, indented JSON<br/>Extension: .annotation<br/>MIME: application/rd-annotations+json"]
Y -- HTML --> TEMPLATE{Custom HTML template<br/>enabled?}
TEMPLATE -- Yes --> CUSTOM["Use the configured template"]
TEMPLATE -- No --> DEFAULT["Use Thorium’s default template"]
CUSTOM --> MARKDOWN
DEFAULT --> MARKDOWN
MARKDOWN["For each annotation body:<br/>• Parse note text as GitHub-flavored Markdown<br/>• Sanitize the generated HTML<br/>• Keep the original body if conversion fails"]
MARKDOWN --> RENDER["Render the annotation set<br/>through the Mustache template"]
RENDER --> EMBED["Append the complete annotation-set JSON<br/>as Base64 inside an HTML comment"]
end
subgraph DOWNLOAD["Generate and download the file"]
JSON --> NAME["Append the selected extension<br/>and sanitize the filename"]
EMBED --> NAME
NAME --> BLOB["Create a Blob with the corresponding MIME type"]
BLOB --> URL["Create a temporary object URL"]
URL --> CLICK["Trigger the browser download<br/>through a temporary anchor"]
CLICK --> REVOKE["Revoke the object URL"]
REVOKE --> ANALYTICS["Record the export analytics event"]
ANALYTICS --> END([Export completed])
end
ERR1 --> STOP([Export stopped])
DISABLED --> STOP
Q -. "A non-PDF note without a locator is still exported,<br/>but its target source and selector list may be empty." .-> HEURISTIC["Export behavior"]
X -. "Skipped notes and empty output sets<br/>are not reported to the user." .-> HEURISTIC
Key points
Questions for discussion
|
0 replies
|
Current UML representation of notes and drawing highlights The active model uses one unified atomic record, classDiagram
direction LR
class NotesCategory {
<<Redux collection>>
+note: INoteState[]
}
class INoteState {
<<atomic persisted item>>
+uuid: string
+index: number
+textualValue: string?
+color: IColor
+drawType: EDrawType
+tags: string[]?
+created: number
+modified: number?
+group: annotation | bookmark
+locatorExtended: MiniLocatorExtended?
+pdfAnnotation: IPdfTextAnnotationTarget?
+creator: INoteCreator?
+readiumAnnotation: ReadiumAnnotationBridge?
}
class AnnotationItem {
<<logical specialization>>
+group: annotation
+target: EPUB locator or PDF rectangles
}
class BookmarkItem {
<<logical specialization>>
+group: bookmark
+target: EPUB caret location
}
class EDrawType {
<<enumeration>>
solid_background = 0
underline = 1
strikethrough = 2
outline = 3
bookmark = 6
}
class IColor {
<<value object>>
+red: number
+green: number
+blue: number
}
class INoteCreator {
+id: string
+urn: string
+type: Person | Organization
+name: string?
}
class MiniLocatorExtended {
<<EPUB target>>
+locator.href: string
+locator.locations
+selectionInfo
+epubPage: string?
+headings
}
class IPdfTextAnnotationTarget {
<<PDF target>>
+type: pdf-text-highlight
+page: number
+rects: IPdfAnnotationRect[]
+quote: string?
}
class IPdfAnnotationRect {
+x1: number
+y1: number
+x2: number
+y2: number
}
class ReadiumAnnotationBridge {
<<import/export bridge>>
+export.selector: ISelector[]?
+import.target: ReadiumTarget?
}
class IHighlightHandlerState {
<<EPUB runtime state>>
+uuid: string
+href: string
+def: IHighlightDefinition
}
class IHighlightDefinition {
<<drawing definition>>
+selectionInfo
+color: IColor
+drawType: number
+group: string
+textPopup
}
class IHighlight {
<<mounted EPUB drawing>>
+id: string
+selectionInfo
+color: IColor
+drawType: number
+group: string
+textPopup
+pointerInteraction: boolean
}
class TPdfAnnotationTransport {
<<PDF drawing definition>>
+id: string
+page: number
+rects: Rect[]
+quote: string?
+color: IColor
+drawType: solid_background | underline | strikethrough | outline
}
NotesCategory "1" *-- "0..*" INoteState : contains
INoteState <|-- AnnotationItem : logical view
INoteState <|-- BookmarkItem : logical view
INoteState *-- IColor : appearance
INoteState --> EDrawType : drawing style
INoteState o-- INoteCreator : authored by
INoteState o-- MiniLocatorExtended : EPUB target
INoteState o-- IPdfTextAnnotationTarget : PDF target
INoteState o-- ReadiumAnnotationBridge : interchange data
IPdfTextAnnotationTarget *-- "1..*" IPdfAnnotationRect : rectangles
INoteState ..> IHighlightHandlerState : EPUB drawing adapter
IHighlightHandlerState *-- IHighlightDefinition
IHighlightDefinition --> IColor
IHighlightDefinition --> EDrawType
IHighlightDefinition ..> IHighlight : mounted by navigator
AnnotationItem ..> TPdfAnnotationTransport : PDF drawing adapter
TPdfAnnotationTransport --> IColor
TPdfAnnotationTransport *-- "1..*" IPdfAnnotationRect
note for AnnotationItem "Not a separate TypeScript type. Derived when group equals annotation."
note for BookmarkItem "Not a separate TypeScript type. Derived when group equals bookmark."
note for EDrawType "group and drawType are independent fields; their consistency is not enforced by the type system."
Drawing mappingFor EPUB content, an atomic note is converted into
For PDF content, A significant current-model detail is that
|
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Document the current note state flow after the migration from persisted reader Redux state to SQLite-backed note storage.
Current Model
Notes are represented in renderer Redux as
INoteState[], but SQLite is now the durable storage layer.reader.note[]in each reader BrowserWindow Redux storenotestable in the main processSYNCREADER_NOTE_ADD_UPDATEREADER_NOTE_REMOVEDiagram
flowchart LR subgraph Startup["App startup / migration in main"] OldRedux["old persisted Redux state\nreader.reduxState.note[]"] OldBookmark["old bookmark map"] OldAnnotation["old annotation map"] Migration["main redux store migration"] DB[("SQLite notes table\npub_id, note_id, note_json")] OldRedux --> Migration OldBookmark --> Migration OldAnnotation --> Migration Migration -->|"insert / replace when JSON wins"| DB end subgraph OpenReader["Open publication reader BrowserWindow"] DB -->|"select notes by pubId"| MainOpen["main winOpen saga"] MainOpen -->|"readerIpc request payload"| ReaderA["Reader A Redux\nreader.note[]"] MainOpen -->|"same publication opened again"| ReaderB["Reader B Redux\nreader.note[]"] end subgraph Mutation["Reader note mutation"] UI["reader UI / sagas\nbookmark, annotation, PDF"] --> Action["note add/update/remove action"] Action --> Reducer["reader reducer\nupdates reader.note[]"] Action --> RendererIPC["renderer sync middleware\nSYNC RendererAction"] RendererIPC --> MainIPC["main IPC saga"] MainIPC --> MainRedux["main Redux action stream"] MainRedux --> Persist["main note saga\ninsert/update/delete SQLite"] MainRedux --> Broadcast["main sync middleware\nbroadcast MainAction"] Persist --> DB Broadcast -->|"same publication add/update"| ReaderB Broadcast -->|"remove currently broader"| OtherReaders["other reader windows"] endStartup Migration
During main store initialization:
notestable is created if needed.reader.reduxState.note[]is compared against SQLite by latest modified timestamp.bookmarkandannotationmaps are converted intoINoteStateand inserted into SQLite.noteTotalCountremains separate publication-data JSON, not part of the SQLite notes table.Reader Hydration
When a reader BrowserWindow opens:
readerIpc.CHANNEL.payload.reader.note.noteTagsIndexlocally from the incoming notes.Mutation Flow
For local mutations:
readerActions.note.addUpdateorreaderActions.note.remove.reader.note[].SYNC.Multi-reader Same-publication Sync
Multiple reader windows for the same publication stay synchronized through main-process action broadcast.
Important detail:
READER_NOTE_ADD_UPDATEincludesdestination.publicationIdentifier, so main sync can broadcast only to reader windows for the same publication.READER_NOTE_REMOVEdoes not currently carry a publication destination, so it is broadcast more broadly and relies on globally unique note UUIDs to avoid cross-publication side effects.All reactions