Guest mode
Trying Solid Memo before logging in. A guest studies in a pod kept in their browser. Once they log in with a real WebID they can move what they studied into their own Pod. Everything else in the app works the same for a guest: the deck library, studying, statistics, preferences and the data check. A guest's instance is made as any new one is, so it studies by FSRS-6 (srs.md).
A pod in the browser
A guest gets a small Solid pod of their own, served by
localPod.ts as a fetch, so the
app's Solid adapters read and write it exactly as they do a server. A
router (routedFetch.ts) sends
each request to the guest's pod or to the network, by URL.
The diagram's source
graph LR
accTitle: Routing a guest's requests
accDescr: Requests to the guest pod's address go to the local pod in IndexedDB, and every other URL goes through the authenticated fetch.
UI --> UseCases --> Adapters["Solid adapters"]
Adapters --> Fence[writeFence] --> Router[routedFetch]
Router -- "guest.solid-memo.invalid" --> LocalPod[localPod] --> Store[(IndexedDB)]
Router -- every other URL --> authFetch --> Pod[(the user's pod)]- Origin:
https://guest.solid-memo.invalid/(GUEST_ORIGINin domain/guest.ts). The.invaliddomain is reserved and never resolves, so a guest URL that slips past the router fails instead of reaching anyone. - WebID:
…/profile/card#me. Its profile names the pod's root as itspim:storage, and is written when the guest starts (GuestPod.start). The instance and its private type index are created the way they are on any pod, bycreateInstance. - What the pod speaks: GET, HEAD, PUT, DELETE, and PATCH with SPARQL
INSERT DATA/DELETE DATA. It gives ETags, honorsIf-Match,If-None-Match: *andIf-None-Match: <etag>, keepsldp:containslistings, and marks the root as apim:Storage. It has no access control. Each request runs exclusively on the store, under a Web Lock shared by every tab. - Storage: the
ResourceStoreport. In the browser this is indexedDbResourceStore.ts, which is needed because the login redirect leaves the page and the guest's study must survive it. Where IndexedDB is missing, an in-memory store keeps the study for the current page only. This is the one place pod data lives in browser storage, and only a guest's.
restoreSession prefers a real session. Without one, it returns a
guest's session ({ webId: GUEST_WEBID, guest: true }) whenever a
guest pod exists in this browser.
From a link
A link to a library deck, #/try?deck=…, is for anyone, so it does not
wait for "Try it without logging in": without a session, the app starts
a guest's study as that button does, and opens the deck there
(routing.md). A browser that
has a guest pod already goes on with it, since restoreSession returns
the guest's session, and the deck opens in the guest's instance. So does
a logged-in user's session, in their own instance. A guest's study that
cannot start leaves the landing page, with the reason.
Keeping the study
The diagram's source
stateDiagram-v2
accTitle: Keeping a guest's study
accDescr: A guest logs in and is offered to move the study into their Pod, where it is added or moved, and a failure leaves the study untouched.
[*] --> Guest: Try it without logging in
Guest --> Login: Keep my study — log in
Login --> Guest: Back to my study
Login --> Offer: logged in (guest pod still here)
Offer --> Adding: Move it into my Pod (the user has an instance)
Offer --> Moving: Move it into my Pod (the user has none)
Offer --> [*]: Discard it / Not now
Adding --> Added: every deck in, the guest's study unchanged meanwhile
Adding --> Offer: failed — the decks added stay, whole — study untouched
Moving --> Moved: registered in the user's type index
Moving --> Offer: failed — copy removed, study untouched
Guest --> [*]: DiscardThe guest pod itself marks that there is something to keep, so nothing
has to survive the redirect. After any login, or any restored session,
GuestStudyOffer asks findGuestStudy and makes the offer while a
guest pod exists. Move it into my Pod first lists the user's
instances. When there is one, or several, the study is added to the one
they choose (below); when there is none (or
the instances cannot be listed), it moves into a new instance
(below). Logging in does not discard the guest
pod: the guest can choose to keep it for later.
Adding to an instance
mergeGuestStudy adds the guest's decks to the instance the user chose
(the first listed, unless they pick another), deck by deck. The form
lists the guest's decks (planGuestMerge), each checked; one the user
unchecks is left out, and goes with the rest of the guest's study (at
least one must stay checked, unless the study has none). A guest's deck
copied from the same library release as one of the
instance's (the same prov:wasDerivedFrom) says so: it is added beside
that deck, as a deck of its own with its own progress, never merged
into it card by card, and the user may uncheck it. The form also says
that the instance keeps its own preferences: the guest's are not
carried over. Nor is the guest's digest, which is this browser's. The
drafts of releases the guest wrote in the Studio
are copied too: when the guest wrote any, the form says how many, and
under what names they are copied (step drafts, below).
It runs these steps:
- read: list the guest's instance, and note the version of each of
its documents, before anything of it is read. Any answers still
waiting to be logged go to the guest's log first, and the write fence
keeps the guest's instance read-only meanwhile. Then the guest's
whole instance, its answer log included, is checked against the
shapes: one that does not fully conform adds nothing
(
guestStudyInvalid), nor does one with a subject a newer version of the app wrote, which this one could not write without losing what it says (guestStudyTooNew). Nor does a study with deck groups when the instance's arrangement was written by a newer version, which this one cannot edit (deckTreeTooNew): it is refused here, before any deck is written, rather than at arrange, where every retry would fail again. - decks: each deck the user kept, in the guest's order, becomes a
new deck of the instance (data-model.md):
- a new id (
deck-<uuid>), with its cards and reviews documents named after it in the instance'sdecks/andreviews/; everything else its entry says is the guest's: its title, description, authors, license, topics and keywords, its direction and pace, when it was made, the release it came from (prov:wasDerivedFrom) and a course's chapters completed (sm:completedChapter); - its cards keep their fragment ids (a deck's documents are its own, so no id can clash), their distractors and when they were made, written in this app's format, the document saying it is part of the deck; its review states keep their SM-2 fields and snapshots, each naming its card, direction and scheduler, and its FSRS-6 states come with them, as they are;
- the cards document is created first (one PUT, only where nothing
is), then the reviews document (likewise), and only then the
deck's catalog entry, with
If-Match(the catalog read and the entry added again on 412, a few times at most): until that last write, nothing names the documents, and a failure before it deletes the ones it wrote (and one whose write's answer was lost, which may have been made), never one the pod refused to create; unless the entry was written after all, its answer lost on the way: then the deck is whole. A deck with no cards, or no review states, has no such document yet, as any deck; - then its answers: each answer of the guest's log given to that
deck, its deck, card and wrong option chosen named in the new
deck's documents by the same fragment ids, the rest of it (grade,
intervals, response times) as it was given, and its own id the
guest's followed by the new deck's (
answer-<time>-<random>-deck-<uuid>), is added to the instance's log for its month, insert-only, unread (appendAll, one PATCH per month, more where one would pass 64 KiB). Answers of a deck the guest removed, or left out, are not added.
guestUrlsLeft). Nothing is registered: the instance's registrations name its catalog and its folders, and the decks added are in them. - a new id (
- arrange: when the guest made deck groups, they are made around
the decks added, each a new group (
#group-<uuid>) with the guest's name, holding what the guest's held: the decks added and the guest's groups, in the guest's order, go after the instance's own decks and groups, at the top level (agraftedit of the arrangement, written as any edit: one PATCH withIf-Match, applied again on 412, which numbers the top level anew). An empty group the guest made comes along empty. With no group, nothing is written, and the decks added are listed after the instance's, as new decks are. - drafts: each draft the guest's catalog links is copied to the
instance (
copyReleaseDraft): read whole, every subject of it moved to its place in the instance (drafts/<name>/v<N>/), at its version, under its name, or, when the instance has a draft of that name and version, the next free one (<name>-2, …). It is written as a new draft is (data-model.md): each document only where none is, then the link on the catalog, withIf-Match. A draft whose link cannot be written is taken back, so none is left that the catalog does not link, and none of the instance's is ever written over. A copy that would still name the guest's pod is not written (guestUrlsLeft). - verify: the guest's instance must still list the same
resources, each at the version noted in read
(
guestStudyChanged). - tidy: delete the guest's instance as any instance is deleted
(data-model.md), and the whole
guest pod once no instance is left. A failure to delete it only
means
tidied: false: the study is in the instance, and the notes below are kept while the guest's study is still in the browser.
A failure at any step leaves the instance valid: every deck it reports
added (added) is whole, its documents, its entry and, unless the
failure was in adding them, its answers; nothing else of the guest's was
written but the groups, when it failed after arrange, the drafts
it reports copied (copied), each whole, and a deck's
document whose deletion failed, which nothing names. The guest's study
is as it was, and the user is told which decks are in the instance now,
and which drafts, each with the name it has there.
Trying again adds what is not there yet: as each deck is added, and
each group about to be, the update journal of this browser notes it
(GuestMergeNote: the new deck or group, and what it was made from:
for a deck, the guest's catalog entry and the versions of the guest's
documents it was read at; for a group, the whole arrangement of the
guest's groups around the decks added; for a draft, the versions of
its documents). The next run finds a deck
whose entry and documents are as noted in the instance and adds only
its answers again, which changes nothing that is there (the same
entries, the same triples), and makes each group, when the arrangement
is as noted, under the URL it had, so a graft already made is left as
it was. A deck the guest changed since, in its entry or its documents,
is added anew, beside the one added before (which the user may
remove), and so are a deck the instance no longer has and every deck
where the browser kept no note (the journal is best effort); its
answers are then entries of their own, their ids naming the new deck,
so no entry of the log names two decks. Groups arranged otherwise than
noted (a deck added anew among them) are made anew, holding the decks
added, and the ones made before stay, without them. A draft noted, at
the same versions, whose copy the instance still links, is not copied
again; one the guest changed since, or whose copy is gone, is copied
anew, under the next free name. The notes are
forgotten once a merge has deleted the guest's study; a study
discarded instead leaves them in the browser, where they do no harm: a
later guest's decks have new URLs, which no note names.
As a new instance
transferGuestStudy copies the guest's instance into a container the
user chooses (by default <storage>solid-memo/main/, where a first
instance goes). It runs these steps:
- stage: list the guest's instance. The digest stays behind, because its versions are this browser's. Make sure the target is free, then create it. Any answers still waiting to be logged go to the guest's log first.
- copy: copy each resource with every IRI under the guest's
instance moved under the target, and the guest's WebID renamed to the
user's (
ContainerMove.renames). - adopt: name the catalog's publisher as the user's profile names
them. No document of the copy may still mention
GUEST_ORIGIN, as an IRI or in text (guestUrlsLeft). - validate: the copy must fully conform to the shapes
(
movedCopyInvalid). - verify: the guest's instance must be unchanged since it was copied. The write fence keeps it read-only meanwhile, in the site's other open tabs too.
- register:
attachInstancein the type index the user chose. From here on the study is theirs. - tidy: register each class of the instance's data (its
catalog, decks, cards, review states, answers and drafts,
data-model.md), then delete the guest's instance
as any instance is deleted (data-model.md),
and the whole guest pod once no instance is left. A registration
that fails is left for Register what is missing in Preferences,
and the guest's instance is deleted all the same. A failure to
delete it only means
tidied: false.
A failure before register deletes the copy, whole, and leaves the
guest's study exactly as it was: Solid Memo created the target's
container where nothing was, and nothing names it before register,
so all it holds is Solid Memo's copies. The update journal of this
browser notes the run, so a copy left behind by a closed tab is offered
for removal with the offer to keep the study, the next time the user is
logged in (InterruptedMoveContainer), and removed whole. A move tried
again first, at the place the copy takes, fails before it writes
anything, and leaves the note. A copy another tab's
move is still writing is not offered, nor removed. A guest's
instance has no access control of its own, so the copy inherits the
user's pod's defaults, the same as a newly created instance.
The other open tabs of the site (the web app's and the Studio's, which
share one origin) hold the guest's instance and its copy while a move
runs, so a write there fails with the same message as in the tab moving it. The
tab running the move holds a Web Lock named after its journal entry
until the move is over, failed or not; the browser lets the lock go when
the tab closes or crashes. Each other tab reads the entries when it
opens, then follows them through the browser's storage events, and
holds what an entry names only while its lock is held. An entry a
failed move keeps, one a closed tab left, or one noting a deck added to
an instance fences nothing. A browser without Web Locks fences no other
tab; the verify step catches what it misses.