Data model in the pod
Solid Memo keeps everything a learner makes and studies in their own Solid Pod, as linked data files in one folder, the instance. This page says which file holds what, how the app finds the files again after a login, and how it writes them without losing a change made on another device.
Where Solid Memo stores data in a user's pod and how it finds it again.
Implemented in packages/solid/src/ (vocabulary in
vocab.ts, discovery in
typeIndex.ts). The pod is read and
written by the Solid Protocol,
and Solid Memo's data is found through the
type indexes, an editor's
draft (standards.md).
Vocabulary and shapes
Solid Memo mints its own terms under https://solid-memo.com/ns/vocab/v1.ttl#
(prefix sm: below) — no existing RDF vocabulary covers spaced repetition.
The terms are defined in ns/vocab/v1.ttl (vocab.md);
what a valid subject of each class looks like, version by version, is a
SHACL shape under ns/shapes/ (shapes.md). Both
are published with the site, and the app's constants, record types and
descriptors are generated from them. Readers ignore unknown triples and
writers never delete triples they don't understand. Every subject
Solid Memo writes carries sm:formatVersion; a subject with neither
that stamp nor a Solid Memo class, that is not the catalog and that
no subject of Solid Memo's names as its creator, publisher or
distribution, is another app's, and so is a review state of another
scheduler; the data check only warns about them
(validation.md).
The diagram's source
flowchart LR
accTitle: From pod data to the domain model
accDescr: A Thing is read as a versioned shape record, migrated to the latest record and mapped to the domain model, and a model is written back the same way in reverse.
thing["Inrupt Thing"] -->|readVersioned| record["shape record<br/>e.g. CardV1"]
record -->|migrate| latest["latest record<br/>CardV2"]
latest -->|cardFromRecord| model["domain model<br/>Card"]
model -->|cardToRecord| latest
latest -->|recordThing| thingDiscovery chain
The diagram's source
flowchart LR
accTitle: The discovery chain
accDescr: From the WebID, the app follows the storage and the private and public type indexes to the instance container, and to the registrations other apps read.
W[WebID] -->|pim:storage| S[Storage root]
W -->|solid:privateTypeIndex<br/>directly or via pim:preferencesFile| PTI[Private type index]
W -->|solid:publicTypeIndex| PUB[Public type index]
PTI -->|solid:TypeRegistration<br/>forClass sm:Instance| I1[Instance container]
PUB -->|solid:TypeRegistration| I2[Instance container]
PTI -.->|"forClass dcat:Catalog, sm:Deck,<br/>sm:Card, sm:ReviewState, sm:Answer,<br/>sm:Deck (the drafts)"| D1[The instance's data,<br/>for other apps]
PUB -.->|"forClass dcat:Catalog, sm:Deck,<br/>sm:Card"| D1- Storage:
pim:storagetriples in the (extended) profile viagetPodUrlAll; when absent, the Solid Protocol Link-header walk-up (rel="type"targetingpim:Storage— capital S) from the WebID URL; manual URL entry as last resort. 0, 1, or N storages are all handled. - Instances: one
solid:TypeRegistrationper instance,solid:forClass sm:Instance, in the private type index by default.dcterms:titleon the registration names the instance (harmless extra triples). Reading accepts bothsolid:instanceContainerandsolid:instance. This is how Solid Memo finds its instances, and the only registration it reads to do so. - Each class of an instance's data has a registration of its own,
so another app finds the kind of data it knows by its class, without
knowing Solid Memo's layout (examples below):
the catalog (
dcat:Catalog,solid:instanceits subjectcatalog.ttl#catalog), the decks (sm:Deck,solid:instancecatalog.ttl, the one document that holds every deck subject), the cards (sm:Card,solid:instanceContainerdecks/), the review states (sm:ReviewState,reviews/), the answers (sm:Answer,history/) and the drafts of releases (sm:Deckagain,solid:instanceContainerdrafts/: a draft's release document is a deck too, told from the instance's decks by where it is), each titled with the instance's name. Per the Type Indexes spec,solid:instancenames the one resource holding the class's subjects, andsolid:instanceContainera container whose documents hold them. The catalog's, the decks' and the cards' registrations go in each index that registers the instance. The review states', answers' and drafts' go in the private index only, even for an instance registered publicly: the first two say what the user studied and how well, and a draft is its author's until it is published. Without a private index they are not registered anywhere. They are written when an instance is created, by a format update once it updated every document (a failure there leaves the update done), when a guest's study moves into the pod as a new instance (guest-mode.md), and by Register what is missing under Findable by other apps in Preferences, which lists every registration that belongs in an index and whether it is there (not offered to a guest, whose pod no other app sees). A type index the profile links that cannot be read is left out there, and said to be unreadable; nothing is added to it. Never on opening an instance: that would fight other editors of the index, and add back what the user removed. Attaching an instance by URL writes itssm:Instanceregistration alone, and adding a guest's study to an instance writes none: the decks it adds are in the catalog and the folders the registrations name. - Writing a type index reads it, makes the change and saves it with
If-Matchthe version read; when the index changed meanwhile (412), it is read and the change made again, three attempts in all (changeIndexin typeIndex.ts). A registration is added only when no equal one is there: the same class, naming the same resource under either predicate (the instance's container with or without its trailing slash, as reading takes it). Every registration of the instance's data, read back from the type indexes of real servers, is in typeIndex.integration.test.ts. - Type index links are read from the WebID subject in the WebID
document and in its extended profile documents (
rdfs:seeAlso/foaf:isPrimaryTopicOf), pluspim:preferencesFilefor the private one. - Missing indexes (normal on Community Solid Server pods): the user is
warned and chooses — create the private index (document under
<storage>settings/plus a profile link) or register publicly. The link goes in the WebID document when it is writable, else in the first extended profile that accepts it (Inrupt PodSpaces: the WebID document onid.inrupt.comis read-only;<storage>profileis the writable one). An index document already at the target URL is adopted, not overwritten. If registration fails, instance creation fails loudly and the created container is cleaned up; there is no local fallback. For an instance registered publicly, the review states' and answers' registrations are written to the private index afterward, and a private index that refuses them does not fail the creation: they are left for Register what is missing. - Deleting an instance deletes what Solid Memo wrote in its
container and nothing else (
deleteInstanceDatain instanceData.ts), then removes its registrations, of every class, from both type indexes (below). What it wrote: every deck's cards and reviews documents as the catalog names them (only those below the container: the catalog is data, and one naming a document elsewhere must not lead a delete there), the answer log's months (history/<YYYY-MM>.ttl), the drafts the catalog links, those in the instance'sdrafts/only, as deleting a draft deletes them (Drafts and releases), thenpreferences.ttl,digest.ttlandcatalog.ttl, in that order, so a delete that fails before the catalog goes can be retried and find the decks and the drafts again (a catalog the pod serves but that cannot be read, as another app might leave it, is kept, and so are the decks' documents and the drafts, which nothing else names; the rest goes as ever); thendecks/,reviews/,history/anddrafts/, each only if it is then empty; thenmeta.ttl, last of the documents, so a half-deleted instance still attaches by URL; and the container itself only if it is then empty. A document's access control goes with it: the Solid Protocol has the server delete a resource's ACL with it. Whether each document is there is asked first (HEAD), since node-solid-server answers a DELETE of what is not there with 401; what is gone counts as deleted. Anything else in the folder, which another app put there, is kept, as are the releases published from the instance (releases/), which others may have added by their address; so is every container on its path: the user is told the folder was kept, and why (another app's files, or the releases,keptReleases), with a link to it (KeptFolderNotice). The delete's confirmation says both are kept. Data goes before registration so a failure leaves the instance listed and the delete retryable. Every blocking server the end-to-end tests start (testing.md), and Pivot and Community Solid Server 8, keeps another app's file and its folder, and deletes a folder holding only Solid Memo's documents with its access rules (foreignData.integration.test.ts). Solid-Nextcloud lists a folder's ACL among what the folder contains, so there such a folder is kept, its access rules in it; what the advisory servers fail, and why, is theirexpected-failures.json.
Registrations
In the type index, the instance's own registration, by which Solid Memo finds it:
<#sm-inst-9f3c1a> a solid:TypeRegistration ;
solid:forClass sm:Instance ;
solid:instanceContainer <https://pod.example/solid-memo/main/> ;
dcterms:title "Japanese study" .
Beside it, one registration for each class of its data, so other applications find each kind without knowing Solid Memo: its catalog as a DCAT catalog, its decks, and the containers of its cards, review states and answers.
<#sm-cat-4b7d2e> a solid:TypeRegistration ;
solid:forClass dcat:Catalog ;
solid:instance <https://pod.example/solid-memo/main/catalog.ttl#catalog> ;
dcterms:title "Japanese study" .
<#sm-deck-1c8e40> a solid:TypeRegistration ;
solid:forClass sm:Deck ;
solid:instance <https://pod.example/solid-memo/main/catalog.ttl> ;
dcterms:title "Japanese study" .
<#sm-card-7a21d9> a solid:TypeRegistration ;
solid:forClass sm:Card ;
solid:instanceContainer <https://pod.example/solid-memo/main/decks/> ;
dcterms:title "Japanese study" .
and, in the private type index only:
<#sm-review-52f0b3> a solid:TypeRegistration ;
solid:forClass sm:ReviewState ;
solid:instanceContainer <https://pod.example/solid-memo/main/reviews/> ;
dcterms:title "Japanese study" .
<#sm-answer-e9d417> a solid:TypeRegistration ;
solid:forClass sm:Answer ;
solid:instanceContainer <https://pod.example/solid-memo/main/history/> ;
dcterms:title "Japanese study" .
<#sm-draft-3d90c6> a solid:TypeRegistration ;
solid:forClass sm:Deck ;
solid:instanceContainer <https://pod.example/solid-memo/main/drafts/> ;
dcterms:title "Japanese study" .
meta.ttl makes a container self-describing: attach-by-URL reads it to
recover an instance that lost its registration. Deleting an instance
removes all of its registrations: the sm:Instance one by its
container, each other by the exact IRI it names
(<container>catalog.ttl#catalog, <container>catalog.ttl,
<container>decks/, <container>reviews/, <container>history/,
<container>drafts/),
never by what lies under the container, where another app may have
registered data of its own. A registration that also registers
something else keeps it, and loses only the link to the instance's
data.
Instance layout
<storage>solid-memo/<name>/ (default path; user-editable)
├── meta.ttl #it: a sm:Instance; dcterms:title; dcterms:created;
│ on an instance an earlier version's format
│ update made, dcterms:replaces and
│ dcterms:modified, ignored and kept;
│ sm:formatVersion 2
├── preferences.ttl #it: a sm:Preferences (created on first explicit save):
│ study caps, sm:answerScale, sm:developerMode,
│ sm:invalidDataPolicy, sm:theme,
│ sm:formatVersion 4
├── catalog.ttl one subject per deck (titles live ONLY here), a
│ sm:Deck and dcat:Dataset: sm:formatVersion 6,
│ dcterms:description, sm:studyDirection, optional
│ dcterms:creator/license, dcat:theme,
│ dcat:keyword (language-tagged),
│ prov:wasDerivedFrom, the deck's own study caps
│ (sm:deckNewCardsPerDay/MaxReviewsPerDay), and
│ for a course the chapters completed
│ (sm:completedChapter);
│ beside each deck its
│ dcat:Distribution (#deck-X-cards) and the
│ foaf:Agent nodes of its creators (#agent-…);
│ and the deck groups (#group-…), a sm:DeckGroup
│ and dcat:Catalog each: sm:formatVersion 1;
│ on the catalog, its drafts (sm:releaseDraft)
├── decks/<deckId>.ttl card corpus: one sm:Card per fragment (slow churn),
│ sm:front/back text and/or sm:frontImage/backImage
│ IRIs, optional sm:frontImageDescription/
│ backImageDescription (a picture's alt text),
│ sm:frontNote/backNote under each
│ side and sm:frontLabel/backLabel above it, each
│ with sm:formatVersion; a retired card
│ (owl:deprecated true) is kept but not studied;
│ a card names its wrong options
│ (sm:distractor, and schema:suggestedAnswer),
│ sm:Distractor subjects beside it; a retired
│ one (owl:deprecated true) is kept but never
│ offered; the document itself (<>)
│ dcterms:isPartOf the deck's entry
├── reviews/<deckId>.ttl SM-2 state: one sm:ReviewState per card and
│ direction (fast churn) — #<cardId> front→back,
│ #<cardId>@back-to-front the other way — naming
│ its card (sm:reviewOf), its direction
│ (sm:reviewDirection) and sm:scheduler sm:sm2;
│ optional sm:previous* snapshot = state before
│ the day's first review (restored by "reset the
│ day"); sm:formatVersion 2
├── history/<YYYY-MM>.ttl the answer log (below): one sm:Answer per grade
│ given in study or a course that month,
│ appended, never edited; sm:formatVersion 1
├── drafts/<name>/v<N>/ a draft of a release, linked from the catalog
│ (sm:releaseDraft): release.ttl, its root (the
│ draft deck, sm:formatVersion 1), agents, sources
│ and how it was made; chapter-<id>.ttl per
│ chapter; cards.ttl, the cards no chapter asks
│ ([below](#drafts-and-releases))
├── releases/<name>/v<N>.ttl a release published from a draft, linked
│ from the catalog (sm:publishedRelease): one
│ document, readable by everyone, never written
│ again (the user may publish elsewhere instead)
└── digest.ttl derived data (below): a sm:DocumentReceipt per document
(#receipt-<path>) and a sm:DeckSchedule per deck
(#schedule-<path>), each stamped with the versions
it was learned from; sm:formatVersion 1
A deck's documents are found through its catalog entry
(sm:cardsDocument, sm:reviewsDocument), never by name: a library
upgrade by an earlier version of the app moved them to
decks/<deckId>-<uuid>.ttl and reviews/<deckId>-<uuid>.ttl. This
app's upgrade writes them where they are
(migrations).
Decks and cards
Granularity is chosen around the N+1 problem (no batch requests, no SPARQL on Solid servers): reading a whole deck is one GET, and a study session never rewrites card content.
The diagram's source
graph LR
accTitle: An instance's documents
accDescr: A deck in the catalog links its creator, its distribution, its cards document and its reviews document, and each review state points to its card.
C["catalog.ttl#deck-X<br/>a sm:Deck, dcat:Dataset<br/>dcterms:title, dcterms:description<br/>sm:formatVersion<br/>sm:studyDirection<br/>sm:cardsDocument<br/>sm:reviewsDocument"]
A["catalog.ttl#agent-…<br/>a foaf:Agent<br/>foaf:name, foaf:mbox"]
X["catalog.ttl#deck-X-cards<br/>a dcat:Distribution<br/>dcat:accessURL"]
C -->|dcterms:creator| A
C -->|dcat:distribution| X
D["decks/deck-X.ttl#card-N<br/>a sm:Card<br/>sm:front / sm:back<br/>sm:frontImage / sm:backImage<br/>sm:formatVersion<br/>owl:deprecated (retired)"]
R["reviews/deck-X.ttl#card-N<br/>reviews/deck-X.ttl#card-N@back-to-front<br/>a sm:ReviewState<br/>SM-2 fields<br/>sm:reviewDirection, sm:scheduler"]
C -->|sm:cardsDocument| D
C -->|sm:reviewsDocument| R
D -.->|the document: dcterms:isPartOf| C
R -->|sm:reviewOf, else the same fragment id| D- The catalog holds one subject per deck with its title and links to the
two documents — the deck list renders from a single fetch. A deck's
title and description are language-tagged text, one value per
language: since deck format 5 in any language the user states, no
English required (format 4 required one; see
i18n.md). Since deck
format 3 a deck is a DCAT dataset (
dcat:Dataset, see vocab.md and validation.md): it always has a description (dcterms:description: what it covers and where its content came from — shown on the deck page with its URLs as links; a deck without one gets"Flashcards: <title>."@enand"Kortlek: <title>."@sv, app-written text for any title), and may name its authors (dcterms:creator, each afoaf:Agentnode#agent-<slug>beside the deck, withfoaf:nameand amailto:foaf:mbox; the app shows them asName <email>; the slug is the author folded to ASCII,unnamedwhen nothing is left of it, and an author the folding loses letters or digits of, such as 김민수 or "Kim 민수", or that has none to lose, such as "—", gets six hex digits of a hash of the whole author after it,#agent-unnamed-5a5e6eand#agent-kim-ad54ba, so that two such authors stay two agents; an agent is read by the IRI the deck names, so one written under an older rule is still read, and replaced on the deck's next save), license (dcterms:license, a URL), topics (dcat:theme, concepts of the topics scheme) and keywords (dcat:keyword: since deck format 6 language-tagged, several per language; untagged ones saved before are kept, their language unknown). Its cards document is named as itsdcat:distribution(#deck-X-cards, withdcat:accessURL). A deck copied from the deck library, or from a release added from a link, inherits the provenance and says which release it came from withprov:wasDerivedFrom(dcterms:sourcebefore format 3), wherever that release is: it is read, never written. Agents no deck names any more are removed with the deck that named them. The Studio edits the authors and the license (setDeckProvenance, deckProvenance.ts): two authors that would share an agent node are refused, and a license is one ofKNOWN_LICENSES(license.ts) or the one the deck has. - Card sides: each side is text (
sm:front/sm:back, a literal), a picture (sm:frontImage/sm:backImage, always an IRI — a string in its place is ignored) or both; a side with neither makes the subject not a card. A side's text is language-tagged, one value per language (zxxfor codes, numbers and symbols), or, as saved before card format 5, one untagged literal whose language is not known, never both; the app keeps untagged text only while it is untouched. A note under a side (sm:frontNote/sm:backNote) and the label above it (sm:frontLabel/sm:backLabel, the front's since card format 6) are language-tagged text in any language. Pictures are shown only when their URL is http(s); pod data is untrusted. A picture may have a description (sm:frontImageDescription/sm:backImageDescription, language-tagged), read to whoever cannot see it in its place; without one it is named by its side ("Picture on the front of the card"). The description is read while its side is asked, so it says what the picture shows without giving the answer away. A card may say how its texts are written,sm:textFormat(sm:markdownorsm:plainText, vocab.md): it covers the sides, the notes, the labels and the card's distractors, never the pictures' descriptions. Without it the text is plain. The app keeps the marker through every edit and copy, an import or a course question joining the deck included. An edit trims plain text; text in any other format loses only the blank lines before it and the white space after it, so the spaces that start a Markdown code block survive an edit of another field. Wherever a side is shown whole (study, a library preview, a card's page, a library card), a tap (or Enter or Space on the button over it) enlarges its picture to the viewport's width or height, keeping its shape, and another tap, Escape, the focus moving on or a scroll puts it back (ZoomableImage); in study, Space on a picture's button does not reveal the answer, while a grade's key still answers. - Direction: a deck's
sm:studyDirectionsays how it is studied — a concept ofsm:StudyDirections:sm:frontToBack,sm:backToFrontorsm:bidirectional(every card asked both ways). Formats 1 and 2 said it with the stringsm:direction(absent meaning front→back, the only way there was before the field existed). Changed in the Browser; a library deck brings its own. - Study pace: a deck may set its own daily limits,
sm:deckNewCardsPerDayandsm:deckMaxReviewsPerDay, in place of the instance'ssm:newCardsPerDayandsm:maxReviewsPerDay; a limit it does not set follows the preferences. Changed on the deck's own Preferences page (#/deck-preferences, from the deck's page); a library release never sets them, and a library upgrade keeps the copy's. - Format versions: every subject the app writes carries
sm:formatVersion, saying which version of its class's shape it conforms to: instance 1, decks 3 (DCAT), cards 2 (pictures), review states 2, preferences 4; the catalog, agent and distribution nodes 1. Readers treat a missing version as 1 — data written before the field existed — read older versions as they are, and pass a newer stored version through unchanged. Bringing a pod up to the current versions is the user's call; see migrations.md. A developer can check an instance against the shapes in the browser (validation.md). - Cards are hash-fragment subjects (
#card-<uuid>, or the library's own ids such as#swedenfor imported decks) inside one document per deck. Fragment ids are generated once at creation and never re-derived. - Review state lives in a separate document per deck, one subject per
card and direction, at
#<cardId>for front→back (every state written before directions existed, which is what they all were) and#<cardId>@back-to-frontfor the other way. Since vocabulary 1.16 every state the app writes also says what it is of, so another app needs no naming rule to join it to its card:sm:reviewOfnames the card (<cards document>#<cardId>),sm:reviewDirectionthe direction (sm:frontToBackorsm:backToFront) andsm:scheduler sm:sm2the algorithm its fields belong to. A reader joins a state to its card in reviewStateMapper.ts and reviewRecord.ts:What the state says wins. The card is the one
sm:reviewOfnames and the direction the onesm:reviewDirectionnames, whatever the subject is called; each one the state leaves out comes from the fragment rule above (no@back-to-frontsuffix: front→back). So a state another app named#state-7f3ais read when it names its card, and#card-1naming card 2 is a state of card 2.A link into a cards document the deck had names its card too. A library upgrade by an earlier version of the app moved the cards to
decks/<deckId>-<uuid>.ttland may have kept the reviews document, whose links still name the old document; sosm:reviewOfinto any document beside the cards named<deckId>.ttlor<deckId>-<…>.ttlis read by its fragment, and the state's next write names the current cards document.What is not read: a state whose
sm:reviewOfnames a subject outside the deck's cards documents (a card of another deck, or the document itself), one whose direction is neither way (saysm:bidirectional), and one of another scheduler (any value ofsm:schedulerbut thesm:sm2IRI, a literal too; absent means SM-2, as every state was before 1.16). Another scheduler's state is another app's data: it is never read as SM-2, never written over and never removed, and the check only warns about it. It does not stop the app scheduling the card itself, beside it.One state per card and direction. Where several are of the same, the one at the subject the fragment rule names counts, else the first by subject IRI (code-unit order); the others stay in the document, unread.
Writing a state goes onto the subject read for it, so a review of another app's
#state-7f3aedits it in place. A state not yet in the document goes to the subject the fragment rule names, unless that subject is another's (a state of another card, another scheduler's, any other app's subject): then to a new#review-<uuid>, which names its card. A subject the fragment rule names for the same card and direction that cannot be read (half a snapshot, say) is written over, as before. A review of a state another app named its own way, beside another scheduler's state, is held against real servers in foreignData.integration.test.ts.Resetting the day removes only the state read for each card and direction it undoes. A second state of the same, which the app never read, stays, and is the one read from then on.
- Cards/reviews documents are created lazily on first write; deck removal
deletes both documents and the catalog subject; card removal also
removes the card's review states, both ways: every
sm:ReviewStateof the reviews document that is of it, by itssm:reviewOfor the fragment rule, read or not, and no other subject (another scheduler's state stays). The Studio's edits do the same (studio.md): a card deleted, moved away or forgotten loses every SM-2 state it has, read or not (applyReviewChangeswithevery), where a day reset removes only the one it read. - The cards document says whose it is. Every write of a deck's cards
document by the deck's own operations (import, adding, editing or
removing a card, stating its languages, a course's question joining
the deck, the format update's rewrite of outdated cards, a library
upgrade's write of the cards, a guest's deck added to an instance) adds
<> dcterms:isPartOf <catalog.ttl#deck-X>once, on the document itself, so an app that finds a cards document finds its deck. A repair from the data check and a copy of the instance (a guest's study moved into a new instance) leave the document as it was, so one untouched since vocabulary 1.16 does not say it yet. Anotherdcterms:isPartOfthere stays: another app may point two decks at one document. The subject has no class, so no shape checks it, and the data check lists it as not a Solid Memo subject.
An instance's URL is permanent, and so is every document's and subject's in it: the format update and the library upgrade write the documents where they are, and change no registration (the format update only adds those missing). Another app may keep a link to the instance, a deck, a card or a review state.
The catalog
catalog.ttl#catalog is a dcat:Catalog (shape CatalogV1) of the
instance's decks: a dcterms:title (the instance's name), a
dcterms:description, dcterms:publisher (the pod owner's WebID,
described in the same document as a foaf:Agent with the foaf:name
of their profile), the topics scheme and the EU data themes as
dcat:themeTaxonomy, and a dcat:dataset per deck, kept in step as
decks are added and removed. It may state a license (dcterms:license),
which the same document then types a dcterms:LicenseDocument, as
DCAT-AP asks; a license it no longer states loses that type. Saving a
deck adds only that deck's link,
and removing a deck removes only its own: a dataset another app listed
in the catalog stays listed through every deck save, and writing the
catalog itself keeps it too, beside every deck of the document. The
one link the catalog loses when it is written whole is one to a
subject of its own document that is no dcat:Dataset (left by a deck
removed some other way): it would fail DCAT-AP's class check, and the
catalog could not be written. A member another app described in the
document keeps its link whatever its class: the check only warns about
that link (validation.md). A dataset in another document is
described there, so that class check does not hold it to its class
(validation.md), and an
instance listing one stays valid (held against real servers in
foreignData.integration.test.ts). It is written when an instance is created,
and by the format update for an instance made before
there were catalogs. Its registration is written with the instance's
when the instance is created or a guest's study moves into the pod as a
new instance, and
by the format update and Preferences when missing
(Discovery chain). The whole document
conforms to DCAT-AP (a test holds what the app writes to it).
The instance's name is kept in the meta document's dcterms:title,
the catalog's dcterms:title, and, in each type index, the
dcterms:title of every registration of the instance's data
(Discovery chain): its sm:Instance registration, which
names it in the instance list, and those of its catalog, decks,
cards, review states and answers. Renaming it in the Studio
(renameInstance, studio.md) writes all of
them, each document read and written again on a 412. A catalog
description that is the one the old name gave (defaultCatalogDescription)
follows the new name; one the user wrote stays.
A deck's description, topics and keywords are edited in its Browser ("Describe deck") and on its about screen in the Studio, the keywords one comma-separated list per language the user states; the description is required, as DCAT-AP asks of every dataset. Only the keyword languages the user states in an edit are checked and stored in their canonical form ("iw" → "he"); a language tag the deck already has, even one another app wrote that the app would not accept from the user, is kept as stored.
Deck groups
The user arranges the deck list into named groups that can nest. A group
is a subject of catalog.ttl beside the decks, a sm:DeckGroup and a
dcat:Catalog (shape DeckGroupV1), never a dcat:Dataset: DCAT
nests catalogs, and DCAT-AP lets a catalog list sub-catalogs with
dcat:catalog and datasets with dcat:dataset. DCAT has no ordered
membership (dcat:prev/next are series versions, which the library
uses as such), so the order is Solid Memo's own sm:position.
<#catalog> a dcat:Catalog ; …
dcat:dataset <#deck-a>, <#deck-b>, <#deck-c> ; # still every deck
dcat:catalog <#group-x1> . # the top-level groups
<#group-x1> a sm:DeckGroup, dcat:Catalog ;
sm:formatVersion 1 ;
dcterms:title "Languages"@en ; # one name, in the UI's language
dcterms:description "Deck group: Languages."@en, "Kortleksgrupp: Languages."@sv ; # generated
dcterms:publisher <https://alice.example/profile/card#me> ; # the catalog's
dcat:dataset <#deck-a>, <#deck-b> ;
dcat:catalog <#group-y2> ;
sm:position 0 .
<#deck-a> sm:position 0 .
- The catalog still lists every deck with
dcat:dataset; a group lists only its own members. The catalog'sdcat:cataloglists the top-level groups. An edit writes again only the links to the decks and groups the arrangement places: a member link to a dataset or catalog in another document, or to one the document describes as such but the app cannot read, stays as it is. - A deck or group is in one parent: the catalog (top level) or one group. A deck no group lists is at the top level.
- Positions count within one parent, and the decks and groups of a parent share one sequence, 0 first. Members sort by position, those without one last, in document order. The first arrangement of a level writes positions 0…n−1 for every member at that level; new and imported decks get none.
- An empty group stays until the user removes it; removing a group moves its members, in order, into its place in its parent.
sm:positionon a deck and the catalog'sdcat:catalogbelong to no shape (DeckV6andCatalogV1are unchanged), so writing a deck or the catalog keeps them, and no deck format version was needed.
What is not one valid tree is read as one, rather than refused. A link
to nothing, from a group to itself, or of the wrong kind (a
dcat:dataset link to a group, a dcat:catalog link to a deck) is
ignored. A node several parents list goes to the group with the lowest
URL, except a group the catalog's dcat:catalog lists, which stays at
the top level. A cycle is broken by moving the lowest-URL group in it to
the top level. A negative position counts as none, and on a tie of
positions decks come before groups, each kind in document order. A
group in a format newer than the app's makes the arrangement read-only,
and so, under the "Set invalid data aside" policy (sm:blockSubject,
the default), does a catalog or group that does not conform, and so
does every list while that check is still running
(validation.md);
a group the app cannot read is never edited: its members show at
the top level, and the catalog's dcat:catalog keeps its link.
Writing an arrangement. The screen sends an edit, not a finished
layout (DeckTreeEdit in
deckTree.ts: move a node after a
sibling, combine two into a new group, rename or remove a group,
gather several decks at the end of a group, as the Studio's bulk move
does, or, as keeping a guest's study
does, graft new groups around decks the tree has at the end of the top
level, which changes nothing once one of its groups is there), and
the repository (editDeckTree in
solidDeckRepository.ts)
applies it to catalog.ttl as the pod holds it then:
- read, apply, write once: the document is read, the edit applied to
the tree it states, and what changed written in one PATCH with
If-Match(write discipline); an edit that changes nothing sends nothing; - on 412 the edit is applied again to the document as it is now,
three attempts in all, so another tab's change is kept, not undone. A
move names its neighbor, not an index, and a new group's URL is
minted by the screen (
newDeckGroup), so an edit means the same on the fresh tree and a retried combine is a no-op. An edit that no longer fits (its group or node gone) fails withdeckTreeChanged; - a PATCH, never a whole PUT: where the pod has no strong ETag, a PUT would undo what changed since the read, and a PATCH keeps it;
- only the groups written and the catalog are checked before the
write (
checkWrite): a deck'ssm:positionbelongs to no shape, and a deck that is set aside beside the one moved must not stop the move; - removing a deck also removes it from every group's
dcat:dataset, in the same write; its siblings keep their positions, and the next arrangement closes the gap.
Several decks at once. The Studio changes several decks in one
write of catalog.ttl (studio.md). A move into a group is
one gather edit, which leaves a deck already in the group where it is,
so a retry changes nothing. A pace or a direction is one save of every
entry (saveDecks). Removing decks deletes their cards and reviews
documents first, then their entries in one save (removeDecks).
Wrong options
Any card may have wrong options, shown beside its back when it is asked
as a multiple-choice question (courses.md). Each is an
sm:Distractor subject of the card's document, typed schema:Answer
too, with sm:formatVersion 1, its text (sm:distractorText) and,
optionally, why it is wrong (sm:distractorNote). The card names each
with sm:distractor, and with schema:suggestedAnswer too (since
vocabulary 1.16, below): every write of a card keeps the two
in step, whoever makes it (the card editor, the Studio's inspector, bulk
edits, moves, copies and imports, a course's question joining the deck).
Solid Memo's card editor and the Studio's card
inspector edit them (studio.md; the domain
is distractors.ts):
- A new one is written once it has text, as
#<card>-d<n>,none more than the highest such id the card or the release the deck came from has used. An id the release published is never used again: a learner's answers may name one (sm:chosenDistractor). One deleted before it was published may be. - Retiring one writes
owl:deprecated trueon its subject, and restoring it removes that. A retired one stays named by the card, and is never offered. Readers keep it, marked retired (toDistractor), so the editors show it, a learner's history names it, and a write of the card keeps it; only the choices of a question leave it out (choicesOf). - Deleting one removes its subject and the card's link to it, in the one write of the card. Only one the deck's release never published can be deleted; one it did is retired instead.
- Each change is a write of the card (
updateCard): the card and its distractors are written, those it names no more removed, triples this app does not know kept (withDistractors). - A wrong option is meant to be in the back's languages. One that is not is a warning, never a refusal, in a pod's deck.
Courses
A course is a library release with an outline of chapters and steps. Its state in the pod uses the documents above and no new type-index entries:
- The outline stays in the release. Chapters and steps are never
copied: the app reads them from the release the deck's
prov:wasDerivedFromnames. - The deck holds only the cards answered. Starting a course writes
an empty deck. A question answered for the first time writes its card,
its
sm:Distractorsubjects and its first review state, under the release's fragment ids (#q-why-solid-1a,#q-why-solid-1a-d1), with itssm:textFormatwhen the release states one. The card names each distractor withschema:suggestedAnswertoo (since vocabulary 1.16), for an app that knows schema.org's questions but not Solid Memo's; the app reads onlysm:distractor, and a write of the card adds or removes only the suggested answers that are its own distractors, so another app's stay.schema:suggestedAnswerbelongs to no shape, assm:positiondoes. Library releases are frozen and do not state it: only the copy in the pod does. Removing a card removes the distractors it names. - Completed chapters are on the catalog entry: one
sm:completedChapter <release#ch-…>per chapter whose final review was passed. Like a deck'ssm:position, it belongs to no shape, so every write of the entry keeps it and the deck format did not move. It is added with an If-Match PATCH, read and added again on a 412, three attempts in all, as a deck group edit is. A deck added from a guest's study (addDeck) has the guest's completed chapters written with its new entry, in the same way. The Studio marks a chapter not done, or restarts the course, the same way (setCompletedChapters); that write passes the shape check, as any other edit of the entry does. - Answers go to the answer log, with
sm:answerMode. - Everything else is derived: a step is done when each card it is checked by has a review state, and a chapter opens when the one before it is completed.
# catalog.ttl
<#deck-x> a solid-memo:Deck , dcat:Dataset ; …
prov:wasDerivedFrom <https://solid-memo.com/decks/solid-fundamentals/v1.ttl> ;
solid-memo:completedChapter <https://solid-memo.com/decks/solid-fundamentals/v1.ttl#ch-why-solid> .
Drafts and releases
Vocabulary 1.18 and its shapes let a creator write a release in their own pod, as a draft, before publishing it (vocab.md, studio.md):
- A draft is a container of the instance,
drafts/<name>/v<N>/(the version it drafts), holdingrelease.ttl: the release's root, the draft deck, which is the document itself (<>) as a release is its document, with its agents, its distribution, its sources, how it was made (itsprov:Activitysubjects, license documents) and the series it describes;chapter-<id>.ttlfor each chapter: the chapter, its steps, the cards they and the chapter ask, and those cards' distractors;cards.ttl: the cards no chapter asks (a plain deck's, or questions not yet placed) and their distractors.
isDraftDocumentNamein draftLayout.ts) is another app's, and left alone. - Links on the catalog.
sm:releaseDraftnames a draft's release document,sm:publishedReleasea release published from the instance. Likesm:newcomerCourseon the library's, they belong to no shape, soCatalogV1keeps them through every write of the catalog. Only a link to a draft's place in the same instance is followed. A learner's app readssm:publishedReleasein a creator's catalog, when the creator shares it, to find newer versions of a release it added from a link. - A draft is checked as a draft. Its deck, chapters and steps are
checked against the draft shapes, which do not yet ask what only a
published release needs; its cards and distractors as anywhere else
(validation.md), every write and the
instance's check alike. What no shape describes (how the release was
made, its sources) is written as it is, and kept through every edit.
The Studio's release screen writes these as the library's releases
state them (deck-library.md): the
license typed
dcterms:LicenseDocument, each source's title, creator and license or the evidence for one, the making's attribution and notes (rdfs:comment), and its checks, never a human review (studio.md). A draft names the release it was published as withsm:releasedAs, and is then no longer edited. - Writes, as everywhere (Write discipline):
making a draft puts each document only where none is
(
If-None-Match: *), the release document first, then links it from the catalog (If-Match, read and made again on a 412). A release document there already means the name is taken: nothing was written, and the Studio tries the next name. Any later failure deletes what was written before it is shown, so no draft is left that the catalog does not link and deleting the instance would miss. An edit is one write of each document it changes, made only if the document is as it was read (If-Match; a new chapter's document, only where none is), and the Studio makes the edit again on the draft as it is when one changed elsewhere, three times in all. An edit is made from the read it names by its version: one made from a read that another edit has overtaken is refused (changedElsewhere), never written over the newer documents. Each document an edit changes is one PUT of it whole,If-Match: Community Solid Server's in-memory store cuts text short after a PATCH, and node-solid-server stores a boolean a PATCH inserts as false. A server with no strong ETag gets noIf-Match, so there an edit made elsewhere meanwhile is written over. Deleting a draft deletes its documents, the release document last, then its container and the ones above it (drafts/<name>/,drafts/) where they are then empty, then the link. - A release is frozen. It is written once, at library deck format 6,
and describes its series and publisher itself
(deck-library.md).
A draft is made a release's text (
assemble) with every subject moved to the release's address,@basethat address, its release time set. - Publishing a release
(solidReleasePublisher.ts,
studio.md) puts that text, as it is,
at
releases/<name>/v<N>.ttlof the instance, or in another folder the user chooses: one PUT, only where nothing is (If-None-Match: *; a 412 isreleaseTaken, nothing written, unless the document there states the same statements, itsdcterms:issuedanddcterms:modifiedaside: that is this release, written by a publishing cut short, and publishing goes on from there; not when another draft of the instance names it as its release). Then the release alone is made readable by everyone: its own ACL, or its ACR, gets public read and nothing else, and no folder's access control is changed (a server that names the folder's as the document's is left as it is, the release private). Then the catalog links it (sm:publishedRelease, If-Match, read and made again on a 412), and the draft names it (sm:releasedAs), If-Match on the version the draft was checked and assembled at, never on a later one (changedElsewhere). A failure after the PUT leaves the release written and the draft unmarked; publishing again at the same address finishes it. A release is never written again, nor deleted by the app: deleting the instance keeps it, and the folder holding it, since others may have added it by its address. It is in no type index: the catalog's link is how its author's releases are found.
The answer log
Every grade given in study is kept, so statistics can be computed
(statistics.ts): activity by
study day, streaks, how well reviews were remembered, in all and month
by month (monthlyRetention), and the running total of prompts
introduced (introducedOverTime). How many prompts are new, young and
mature, and the reviews due ahead, are not from the log but from the
review states (srs.md). Once a day has
answers, a slim strip above the deck list (where a session usually
ends) says what the day came to (todayOf), led by the streak (with the record to beat,
or a new record cheered), then a word of praise and the day's numbers
(statistics.md says what each screen shows).
A review state
says only where a card stands now; the log is the history, and source
data, since nothing could rebuild it.
- One document per study month for the whole instance,
history/<YYYY-MM>.ttl(historyUrlOf, by the month of the study day, so a day is never split): reading a year of statistics is twelve GETs whatever the number of decks, and reading it again a304for each month that did not change. - One subject per answer,
#answer-<time>-<random>(one added from a guest's study followed by the id of the deck it was added to) (answer.ts): the deck's catalog entry and the card it was given to (either may since be removed; a removed deck's answers stay, as a removed deck), the direction, the SM-2 grade (whichever answer scale gave it), when it was given and the study day it counts toward, fixed then so a later day-boundary change does not move it, and the prompt's interval before (absent on its first answer, which introduced it) and after. An answer to a course question also says how it was given (sm:answerMode sm:multipleChoice) and, when it was wrong, which wrong option was chosen (sm:chosenDistractor, the distractor in the deck's cards document). An answer given in study says neither, which means recalled, so study writes its answers as before. Since vocabulary 1.19 an answer may also say how long it took, in milliseconds the app was shown: from the prompt to the answer revealed, or to the option chosen (sm:recallMillis), and from there to the grade (sm:gradeMillis), each left out above ten minutes; the day the prompt was due before it (sm:priorDue, absent on a first answer); and the device's offset from UTC then, in minutes east of Greenwich (sm:utcOffsetMinutes). Each is optional, so an answer without them is read as before, and an older app ignores them (migrations.md). - Added without reading (
appendToDocument): one insert-only PATCH, without a precondition, since an answer names a subject no other writer does. Every server tested creates the document and its container when missing and keeps every one of several concurrent inserts (e2e/pod/src/history.integration.test.ts). A guest's study added to an instance brings its answers the same way, a month's in one PATCH, or several where one would pass 64 KiB (appendAll); each takes the guest's id followed by the new deck's, so adding it again to the same deck changes nothing, and adding it to another deck (the guest's deck added anew) makes an entry of its own, never one naming two decks. - After the review, not in its way:
recordReviewsaves the review state, then queues the answer, with how long it took as the study screen measured it (statistics.md); it is added in the background, and one that fails waits, with those after it, for the next answer, the end of the session or the statistics page. An answer still queued when the tab closes is lost; the review it belongs to is not. - Resetting the day removes that deck's answers of the day
(
removeDay: read, remove, save with If-Match, again on 412). - A card moved to another deck leaves its answers behind. The Studio moves or copies cards between decks of an instance. The log is never rewritten: past answers keep naming the deck and the card they were given to, in the source. In the target the card's history starts again, though its review state may come along. A copy's history starts in the target; the original keeps its own.
- Read whole by the Studio (
loadAnswerLog): every month, for a card's history, its lapses and the deck's leeches (studio.md); the statistics (getStatistics) read the months of their range. Both go through one cache (readLogMonths): each month's answers are kept in the page with the version they were read at, and a month is read again only once it changed (readMonthSince, a conditional GET). A read adds to the cache and never replaces it, so the statistics' months and the Studio's whole log do not undo each other; a month the log no longer lists is dropped (statistics.md). A library upgrade moves a deck's cards into a new document, so the Studio names a deck's past answers by their card's id in its cards document as it is now (deckAnswers). - Checked in the full check only (validation.md): the current month changes every session, so checking it on every visit would download it every time.
The digest
digest.ttl lets a visit skip what has not changed since the last one.
It holds only what Solid Memo learned of the other documents, each fact
stamped with the version (ETag) of the document it was learned from
(studyDigest.ts):
- a receipt per document: at that version it conformed to the shapes
(
sm:conformedTonames the rules, a hash of the shape files the site was built with) and/or nothing in it was in an older format (sm:latestFormat); - a schedule per deck, from given versions of its cards and reviews
documents: prompts due by study day (
sm:dueOnDay "2026-10-01 12"), prompts never reviewed, and the reviews and introductions of the day it was computed on. With today's preferences that gives the deck list the same counts asbuildStudyQueue(srs.md).
The diagram's source
sequenceDiagram
accTitle: Counting a deck through the digest
accDescr: The app reads the digest, then asks for each deck's cards with the version the digest holds, and the pod answers 304 when nothing changed or 200 with the document when it did.
participant App
participant Pod
App->>Pod: GET digest.ttl
App->>Pod: GET decks/d.ttl If-None-Match: <its version in the digest>
alt unchanged
Pod-->>App: 304 — counts from the schedule, no check
else changed (or no digest yet)
Pod-->>App: 200 + the document — counts and check from it
App->>Pod: PATCH digest.ttl (in the background)
end- A stale digest is never wrong, only slow. Every fact is used only after the pod says its document is still at the stamped version; a document changed elsewhere (another device, another app) is read and checked in full, and the digest is repaired with what that read learned. So nothing else keeps it up to date: a write anywhere simply makes the facts about that document stale.
- When a study session ends (End session, leaving the screen, or the page being hidden) the deck's schedule and its reviews document's receipt are brought up to date, so the next visit finds them current.
- Writes are edits like any other (If-Match; write check). Changes learned while a write is under way go in the next one; if the digest changed elsewhere meanwhile (412), it is read and changed again, six times in all, waiting a little longer before each (50 ms, then 100 ms, …): another page learning a check's documents writes the digest several times running, and a write that tried again at once could lose to each. A failed write is dropped: the next visit learns it again.
- Read leniently. A subject that does not fit its shape is left out (and so relearned). The digest is not one of the documents an instance is checked by: it is derived, and an invalid copy must not block the instance.
- It is as good as the pod's ETags. Community Solid Server 6 stamps
its ETags in whole seconds, so a document changed elsewhere in the
same second as Solid Memo read it keeps its ETag, and the digest takes
it for unchanged until it changes again (7 stamps milliseconds). The
same holds for
If-Match(below). - A pod without ETags gains nothing. node-solid-server (5.x, 6.0.0) gives no ETag on a read: nothing can be known to be unchanged, so nothing is kept, and every visit reads everything, as before.
- The format update does not write the digest; the documents it writes have new versions, so what it says of them is relearned on the next visit.
Decks as files
The Studio saves a deck as a file, and makes a deck of a file
(studio.md). The file is Turtle or
JSON-LD, one deck to a file. The domain names its parts
(deckFile.ts), and the
DeckArchive port reads and writes it
(solidDeckArchive.ts).
An export holds triples as the pod has them, at the pod's IRIs:
- the deck's entry in
catalog.ttl, its distribution and the agents it names as its creators. Itssm:positionamong its groups is left behind, and itssm:completedChapters unless the progress goes too; - every subject of its cards document: its cards and their wrong options, retired ones too, and whatever another app put there;
- with the progress, every subject of its reviews document.
Nothing is rewritten: a deck still at an older format is exported at
it. A JSON-LD file is flattened: every subject a node of its @graph,
its @context the prefixes the Turtle uses too (sm, dcterms,
dcat and so on), written in full.
An import reads the file as the pod is read, with the same mappers:
- the file must hold one deck (
sm:Deck) that fits its shape; - its cards are the cards of its cards document, its review states those of its reviews document, of cards it holds;
- an older format is brought up to date as it is read
(migrations.md), and the screen says how many parts
were. A deck, card or review state in a newer format than the app
reads is refused (
deckFileTooNew), and nothing is written. It is notwrittenByNewerApp, which refuses to save over pod data a newer version wrote: here nothing is saved over, and the file is what the app cannot read; - a subject of these kinds that cannot be read is left out, and the screen says how many were;
- relative IRIs are resolved against
https://file.solid-memo.invalid/and the file's name (DECK_FILE_BASE): a file has no address; - a JSON-LD context named by its address (or
@import) is refused: reading it would fetch that address.
The deck is then written as a new deck of the instance
(importedDeck). It keeps the file's deck id, so a deck exported,
removed and imported again comes back at its URLs, and the answers the
log keeps of it name it again. It takes a fresh id when the instance
has a deck by that id, or one using a document of that name, or when
the id or its distribution's (<id>-cards) is a subject of the
catalog already. An id this app would not give (deck-…) takes a
fresh one too, so no deck lands on #catalog, a group or an agent. Its cards
keep their ids. Only what Solid Memo knows of the deck and its cards is
written, as when cards move to another deck: triples another app put
there are in the file, but not imported. It is written as Solid Memo
writes any deck: each card names its wrong options with sm:distractor
and schema:suggestedAnswer, each typed sm:Distractor and
schema:Answer, and each review state names its card in the new cards
document (sm:reviewOf), its direction and sm:sm2; a state of
another scheduler in the file is not imported. The deck needs no
type-index registration of its own: the instance's registrations of its
decks (catalog.ttl), cards (decks/) and review states (reviews/)
cover it, as they cover a deck made in Solid Memo
(Registrations). The writes follow the
write discipline: the cards document is created
(If-None-Match), the reviews document written, and then the deck's
entry added to the catalog, each checked against the shapes.
UI gaps
What the data model holds that no screen edits yet. Most of it is shown in Solid Memo; the Studio closes the gaps one by one.
- Edited in the Studio: a deck's authors and license, its course progress (a chapter marked not done, the course restarted), the instance's name and the catalog's description and license, a card's wrong options, edits of many cards at once, a card's review state (forgotten, or given a due day), the deck a card is in (moved or copied to another), several library copies upgraded at once, a deck exported to a file or imported from one, and a draft of a release: what it says of itself, a course's chapters, steps and questions and their order, and its cards' wrong options.
- Shown only: the answer log, as statistics, and in the Studio as a card's history, the wrong options chosen, lapses and leeches; the schedule in the digest, as counts, and in the Studio as a forecast; the check of the shapes, in developer mode, and in the Studio as a deck's or the instance's health; a draft's release check, and its listing as the library will show it.
- Not shown: a catalog's
dcterms:modified, which the app keeps but never writes; a library copy's release provenance beyond its authors, license and sources.
Write discipline
- Mutations always follow read → modify → save
(datasets.ts:
readDatasetorgetSolidDatasetOrNull, thensaveDataset), which issues a PATCH of the delta, or for a large edit one PUT of the whole document as read and edited (below), so unknown triples survive either way. Only brand-new documents are saved fromcreateSolidDataset(). - The answer log is the one exception: answers are added unread, with an insert-only PATCH, since adding a new subject cannot undo anyone else's write.
- A card moved to another deck in the Studio keeps only what Solid Memo knows of it. It is written afresh in the target, and its subject and its wrong options' are removed from the source, triples another app put on them included. A copy leaves them in the source, and does not carry them over either.
- Every write states what it expects to find, as an HTTP precondition,
so a write never silently undoes someone else's (another tab, device or
app):
- an edit is sent with
If-Match: <the ETag the document was read with>; had it changed since, the pod answers 412 and nothing is written. An edit is a PATCH or, when the PATCH would be larger than 64 KiB, one PUT of the whole document: node-solid-server reads at most 100 kB of a PATCH and fails past that, which an edit of every card of a large deck (a format update) exceeds. WithoutIf-Match(no strong ETag, below), such a PUT would undo a change made elsewhere since the read, where a PATCH keeps it. Stating the language of a deck's untagged card sides (stateCardLanguages) and the Studio's bulk edits of cards (editCards, studio.md) are always one PUT (saveDataset'swhole): Community Solid Server's in-memory store cuts a document short after a PATCH holding text beyond ASCII, and such a bulk edit is all text; - a creation is sent with
If-None-Match: *(by@inrupt/solid-clientfor datasets and containers, by the copier for files); had something appeared there meanwhile, 412; - deleting a document read before sends
If-Matchtoo. Deleting an instance sends none: its documents go whatever they hold. A deck another tab adds meanwhile is missed, and its documents keep the folder in place, as another app's file would.
PreconditionFailedErrornaming the document ("changed elsewhere … Reload and try again"); nothing retries on its own, except the writes that re-apply a change to the document as it is then: the digest, an edit of the deck groups, which is read, applied and written again, three times in all, before the 412 surfaces, the Studio's edits of metadata (a deck's authors and license, the instance's name, the catalog, a course's chapters completed), likewise, and a bulk edit of cards, planned again on the cards as they are, three times in all, which stops at once when that plan is not the one the user previewed. A grade's edit of the reviews document is made from the document as last read or written, at the version that left (srs.md), and on a 412 read and graded again on the state it then holds, once. Weak ETags (W/"…") are never sent inIf-Match, whose comparison is strong; a document the pod gives no ETag is written withoutIf-Match. A write the pod answers with a strong ETag, made on a precondition (If-Match, orIf-None-Match: *), so the document holds just what was written, keeps the written dataset at that version, as a read keeps what it read: the next edit sendsIf-Matchwith it, and the next read asksIf-None-Matchwith it. Pods need not give the new ETag (Community Solid Server 7 answers a PATCH with none): without it, a dataset saved is written again withoutIf-Match, and the next read downloads the document. node-solid-server gives no ETag on a read and ignoresIf-Match(5.7.4 alsoIf-None-Match: *; 5.8.8 and 6.0.0 enforce that), so there no edit is conditional: a PATCH still keeps a change made elsewhere, a large edit's PUT does not. Community Solid Server 6's ETag only changes from one second to the next, so it misses a change made in the same second as the read. The end-to-end tests hold all of this against real servers, that one included (testing.md). - an edit is sent with
- An edit's PATCH body is written by Solid Memo (
patchBodyin datasets.ts), not by@inrupt/solid-client: one triple a line, a space before each.. node-solid-server's parser fails on a triple whose closing.touches the term before it (<#a> <#b> 1.}), which is how@inrupt/solid-clientwrites every patch. - A document is downloaded once while it is unchanged. A screen asks
for the same document several times (a deck's cards for its study
queue, the format check and the data check): a read already under way
is shared, and a document read before is asked for with
If-None-Match: <its ETag>; on 304 the dataset read then is returned (datasets are immutable). A write to the document forgets both, unless it kept what it wrote at the version the pod answered with (above). This is memory only, for the open page: nothing is stored in the browser. Logged in, the browser's own cache does not do this, so without it a login downloaded every deck three times; and it is never asked (cache: "no-store"), as it may keep a document the pod gives a modification time and noCache-Controlfor days, and answer a plain read with it. - Reading builds the dataset in one pass
(linearDataset.ts):
@inrupt/solid-clientcopies the whole graph for every quad, which took 9 s for a deck of 3,000 cards. - 404 is a normal state for not-yet-created documents; repositories treat it as empty, not as an error.
- What Solid Memo did not write, it does not delete or unlink. A
write touches only the triples it changes (a deck save adds or
removes its own
dcat:datasetlink, never another app's), and a delete only resources Solid Memo knows it wrote: an instance is deleted document by document, its folder only once empty (deleting an instance); the cards and reviews documents adding a guest's deck to an instance just created, when it fails before the deck's entry names them, are deleted each as read, withIf-Match(guest-mode.md). Only a folder Solid Memo made whole and nothing names yet is deleted recursively: the guest's study moved into a new instance, when it fails half-way or a closed tab left it behind: Solid Memo created it at a URL it found free, and all it holds is copies whose originals stay where they were. - An update writes each document in one edit, held to its read. The
format update and the library upgrade write each document where it
is, as any save does: one edit made only if the document is still at
the version read (
If-Match), its outdated subjects brought up to date from that very read, or, for an upgrade's cards, the changes planned on it (migrations.md). A document whose write fails is as it was, and nothing is put back: every document is in a format the app reads. No write puts an older format over a subject a newer version of the app wrote (migrations.md). - No
.acl/.acrresource is ever written: a resource without its own ACL safely inherits its ancestors' access, while a malformed one replaces inheritance entirely and can lock the owner out (WAC) or expose data. Access control stays whatever the user's server dictates.