Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/models/DocumentRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,25 @@ class DocumentRoot<T extends DocumentType> {
* applied afterwards.
*/
get allDocuments() {
// TODO: Check usages, consider switching to allAuthoritativeDocuments.
if (!this.store.root.userStore.current?.hasElevatedAccess) {
return this.documents;
}
return this.store.root.documentStore.findByDocumentRoot(this.id);
}

/**
* All **authoritative** documents which are related to this document root.
* This method should be used only for admin users or when the author-filtering is
* applied afterwards.
*
* @see {@link iDocument#isAuthoritative}
*/
@computed
get allAuthoritativeDocuments() {
return this.allDocuments.filter((doc) => doc.isAuthoritative);
}

/**
* TODO: replace this placeholder to the currently viewed user
* @default: should return the current viewed user id
Expand Down
10 changes: 5 additions & 5 deletions src/models/documents/Assessable/iAssessable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import React from 'react';
import { AssessableMeta, ExpandedOption } from './AssessableMeta';
import Quiz from './Quiz';
import { iTaskableDocument } from '@tdev-models/iTaskableDocument';
import { mdiTooltipQuestionOutline } from '@mdi/js';
import { IfmColors } from '@tdev-components/shared/Colors';

export enum Correctness {
Expand Down Expand Up @@ -154,7 +153,7 @@ abstract class iAssessable<T extends AssessableType> extends iDocument<T> implem
if (this.type === 'quiz' || !this.inQuiz) {
return undefined;
}
const quiz = this.root?.allDocuments.find(
const quiz = this.root?.allAuthoritativeDocuments.find(
(doc) => doc.authorId === this.authorId && doc.type === 'quiz'
);
return quiz as Quiz | undefined;
Expand Down Expand Up @@ -298,9 +297,10 @@ abstract class iAssessable<T extends AssessableType> extends iDocument<T> implem
return;
}
if (this.inQuiz && this.quiz) {
if (this.quiz.questionCount === 0) {
// A real quiz always has at least one questionId. If this is empty, the quiz hasn't loaded yet
// and we shouldn't delete anything.
if (!this.quiz.isAuthoritative) {
console.error(
`iAssessable with documentRootId='${this.root?.id}' encountered a non-authoritative quiz (id='${this.quiz.id}'). This should not happen.`
);
return;
}
// ensure the current document is unique for the given qid and authorId
Expand Down
45 changes: 45 additions & 0 deletions src/models/iDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,46 @@ abstract class iDocument<Type extends DocumentType> {
return !!this.root && this.root.isLoaded;
}

/**
* Invariant. If true, this document represents the ground truth of what its persisted data should look like.
*
* A document is authoritative, if and only if one of the following are true:
* - This document represents the **data loaded from the API** + potential local changes yet to be committed.
* - This document represents an **initial creation** after the **API** has **explicitly confirmed** that **no document**
* for this document root + user + document type exists yet.
* - This is a **dummy document** **AND** there is no active **or residual** login session available.
*
* Examples where a document is **NOT** authoritative:
* - It is a dummy document, there is no active login session or server connection, but a residual login session exists
* (i.e. waiting for a possible reconnect)
* - It is a non-dummy document that has been created after the API failed to return an existing document e.g. due to a network error
* (but hasn't explicitly confirmed that none exist).
*
* If a document is **NOT** authoritative:
* - it must **not be persisted** to the database.
* - it must **not be edited**.
* - it must **not be relied** upon as accurate (e.g. when deriving state or actions for another document).
*/
@computed
get isAuthoritative() {
if (this.isDummy) {
// Dummy documents are authoritative as long as we can guarantee that the user is not logged in.
const sessionStore = this.store.root.sessionStore;
return !sessionStore.isLoggedIn && !sessionStore.sessionStatusArbitrary;
}
// TODO: Does the source remain API after local edits?
// TODO: Can we guarantee that a source=local can only happen after API has confirmed non-existence?
return true;
}

@computed
get canEdit() {
if (!this.root) {
return false;
}
if (!this.isAuthoritative) {
return false;
}
if (this.sideEffects.some((se) => !se.canEdit)) {
return false;
}
Expand Down Expand Up @@ -236,6 +271,10 @@ abstract class iDocument<Type extends DocumentType> {

@action
save(skipStreamUpdate: boolean = false, onBeforeSave?: (() => Promise<void>) | undefined) {
if (!this.isAuthoritative) {
throw `Trying to save a non-authoritative document (id=${this.id}, documentRootId=${this.root?.id})`;
}

const res = this.saveFn(onBeforeSave);
if (!skipStreamUpdate) {
this.streamUpdate();
Expand All @@ -245,6 +284,8 @@ abstract class iDocument<Type extends DocumentType> {

@action
streamUpdate() {
// TODO: Authoritative invariant required?

if (!this.isPresenting) {
return;
}
Expand All @@ -265,6 +306,10 @@ abstract class iDocument<Type extends DocumentType> {

@action
_save(onBeforeSave: () => Promise<void> = () => Promise.resolve()) {
if (!this.isAuthoritative) {
throw `Trying to save a non-authoritative document (id=${this.id}, documentRootId=${this.root?.id})`;
}

/**
* call the api to save the code...
*/
Expand Down
18 changes: 18 additions & 0 deletions src/stores/SessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export class SessionStore extends iStore<'checkLogin'> {

@observable accessor initialized = false;
@observable accessor isLoggedIn = false;
@observable accessor sessionStatusArbitrary = true;

@observable accessor currentUserId: string | undefined;
@observable accessor storageSyncInitialized = false;
Expand All @@ -23,6 +24,11 @@ export class SessionStore extends iStore<'checkLogin'> {

@action
setCurrentUserId(userId: string | undefined) {
if (!userId) {
this.sessionStatusArbitrary = true;
} else {
this.sessionStatusArbitrary = false;
}
this.currentUserId = userId;
}

Expand All @@ -31,6 +37,18 @@ export class SessionStore extends iStore<'checkLogin'> {
this.isLoggedIn = loggedIn;
}

@action
cleanup(sessionStatusArbitrary: boolean) {
this.sessionStatusArbitrary = sessionStatusArbitrary;
this.isLoggedIn = false;
this.currentUserId = undefined;
}

@action
markSessionStatusArbitrary() {
this.sessionStatusArbitrary = true;
}

get apiMode(): 'indexedDB' | 'memory' | 'api' {
return api.mode ?? 'api';
}
Expand Down
9 changes: 3 additions & 6 deletions src/stores/rootStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,9 @@ export class RootStore {
}

@action
cleanup() {
/**
* could be probably ignored since the page gets reloaded on logout?
*/
console.log('cleanup data stores');
this.sessionStore.setIsLoggedIn(false);
cleanup(sessionStatusArbitrary: boolean) {
console.log('cleanup data stores; session status arbitrary:', sessionStatusArbitrary);
this.sessionStore.cleanup(sessionStatusArbitrary);
this.userStore.cleanup();
this.socketStore.cleanup();
this.studentGroupStore.cleanup();
Expand Down
8 changes: 5 additions & 3 deletions src/theme/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,19 @@ const ExposeRootStoreToWindow = observer(() => {
});

const Authentication = observer(() => {
const { data: session } = authClient.useSession();
const { data: session, isPending, error } = authClient.useSession();
React.useEffect(() => {
if (!rootStore) {
return;
}

if (session?.user) {
rootStore.load(session.user.id);
} else {
rootStore.cleanup();
const sessionStatusArbitrary = isPending || !!error;
rootStore.cleanup(sessionStatusArbitrary);
}
}, [session?.user, rootStore]);
}, [session?.user, isPending, error, rootStore]);
return null;
});

Expand Down