Overview
The store.js module is the single source of truth for application state in Estudo Organizado. It handles:
State Schema : Centralized application state
IndexedDB Persistence : Asynchronous storage with fallback to localStorage
Migrations : Schema versioning and data transformations
Save Debouncing : Optimized write operations
Sync Queue : Sequential cloud sync operations
State Schema
Default State Structure
Location: store.js:36-57
export let state = {
schemaVersion: DEFAULT_SCHEMA_VERSION ,
ciclo: { ativo: false , ciclosCompletos: 0 , disciplinas: [] },
planejamento: {
ativo: false ,
tipo: null ,
disciplinas: [],
relevancia: {},
horarios: {},
sequencia: [],
ciclosCompletos: 0 ,
dataInicioCicloAtual: null
},
editais: [],
eventos: [],
arquivo: [], // concluded events older than 90 days
habitos: {
questoes: [],
revisao: [],
discursiva: [],
simulado: [],
leitura: [],
informativo: [],
sumula: [],
videoaula: []
},
revisoes: [],
config: {
visualizacao: 'mes' ,
primeirodiaSemana: 1 ,
mostrarNumeroSemana: false ,
agruparEventos: true ,
frequenciaRevisao: [ 1 , 7 , 30 , 90 ],
materiasPorDia: 3
},
cronoLivre: { _timerStart: null , tempoAcumulado: 0 },
bancaRelevance: { hotTopics: [], userMappings: {}, lessonMappings: {} },
driveFileId: null ,
lastSync: null
};
State Properties Explained
Property Type Purpose schemaVersionnumberCurrent schema version for migrations cicloobjectStudy cycle configuration and progress planejamentoobjectAdvanced study planning (weekly/cycle) editaisarrayExam syllabi with disciplines and topics eventosarrayStudy events (scheduled, in-progress, completed) arquivoarrayArchived old events for data cleanup habitosobjectHabit tracking logs (questions, simulations, etc) revisoesarrayRevision history configobjectUser preferences and settings cronoLivreobjectFree-form timer state bancaRelevanceobjectExam board analysis data driveFileIdstringGoogle Drive file ID for sync lastSyncstringLast sync timestamp
Database Initialization
IndexedDB Setup
Location: store.js:60-84
export function initDB () {
return new Promise (( resolve , reject ) => {
const request = indexedDB . open ( DB_NAME , DB_VERSION );
request . onerror = ( event ) => {
console . error ( 'IndexedDB Error:' , event . target . error );
loadLegacyState (); // Fallback to localStorage
resolve ();
};
request . onupgradeneeded = ( event ) => {
db = event . target . result ;
if ( ! db . objectStoreNames . contains ( STORE_NAME )) {
db . createObjectStore ( STORE_NAME );
}
};
request . onsuccess = ( event ) => {
db = event . target . result ;
loadStateFromDB (). then (() => {
resolve ();
});
};
});
}
Database Constants:
DB_NAME: 'EstudoOrganizadoDB'
DB_VERSION: 1
STORE_NAME: 'app_state'
DEFAULT_SCHEMA_VERSION: 7
Loading State from IndexedDB
Location: store.js:86-123
export function loadStateFromDB () {
return new Promise (( resolve ) => {
const transaction = db . transaction ([ STORE_NAME ], 'readonly' );
const store = transaction . objectStore ( STORE_NAME );
const request = store . get ( 'main_state' );
request . onsuccess = ( event ) => {
if ( request . result ) {
const loadedState = request . result ;
// BUG FIX: Prevent timer persistence across sessions
const isSameSession = sessionStorage . getItem ( 'estudo_session_active' );
if ( ! isSameSession ) {
if ( loadedState . cronoLivre && loadedState . cronoLivre . _timerStart ) {
loadedState . cronoLivre . _timerStart = null ;
}
if ( loadedState . eventos ) {
loadedState . eventos . forEach ( ev => {
if ( ev . _timerStart ) ev . _timerStart = null ;
});
}
}
sessionStorage . setItem ( 'estudo_session_active' , '1' );
setState ( loadedState );
runMigrations ();
} else {
loadLegacyState (); // Try migration from localStorage
}
resolve ();
};
request . onerror = () => {
loadLegacyState ();
resolve ();
};
});
}
Timer Reset on New Session To prevent timer drift when the app is closed and reopened, all _timerStart timestamps are cleared unless the session is continuous (tracked via sessionStorage).
State Normalization
The setState() Function
Location: store.js:14-34
export function setState ( newState ) {
const normalized = {
schemaVersion: newState . schemaVersion || DEFAULT_SCHEMA_VERSION ,
ciclo: newState . ciclo || { ativo: false , ciclosCompletos: 0 , disciplinas: [] },
planejamento: newState . planejamento || {
ativo: false ,
tipo: null ,
disciplinas: [],
relevancia: {},
horarios: {},
sequencia: [],
ciclosCompletos: 0 ,
dataInicioCicloAtual: null
},
editais: newState . editais || [],
eventos: newState . eventos || [],
arquivo: newState . arquivo || [],
habitos: Object . assign ({
questoes: [],
revisao: [],
discursiva: [],
simulado: [],
leitura: [],
informativo: [],
sumula: [],
videoaula: []
}, newState . habitos || {}),
revisoes: newState . revisoes || [],
config: Object . assign ({
visualizacao: 'mes' ,
primeirodiaSemana: 1 ,
mostrarNumeroSemana: false ,
agruparEventos: true ,
frequenciaRevisao: [ 1 , 7 , 30 , 90 ],
materiasPorDia: 3
}, newState . config || {}),
cronoLivre: newState . cronoLivre || { _timerStart: null , tempoAcumulado: 0 },
bancaRelevance: newState . bancaRelevance || { hotTopics: [], userMappings: {}, lessonMappings: {} },
driveFileId: newState . driveFileId || null ,
lastSync: newState . lastSync || null
};
// Replace the state object properties instead of the reference
Object . keys ( state ). forEach ( k => delete state [ k ]);
Object . assign ( state , normalized );
}
Why Not Reassign state? Direct reassignment (state = newState) would break references. Instead, we mutate the existing object in place using Object.assign() to preserve reactivity.
Saving State
Debounced Save with scheduleSave()
Location: store.js:182-192
export function scheduleSave () {
document . dispatchEvent ( new Event ( 'app:invalidateCaches' ));
if ( saveTimeout ) clearTimeout ( saveTimeout );
// Update badges instantly without waiting for the save
document . dispatchEvent ( new Event ( 'app:updateBadges' ));
saveTimeout = setTimeout (() => {
saveStateToDB ();
}, 2000 ); // 2 second debounce
}
Why Debounce?
Unsaved Changes Warning
Frequent state updates (e.g., timer ticking every second) would cause excessive writes to IndexedDB. Debouncing ensures we only save after 2 seconds of inactivity.
window . addEventListener ( 'beforeunload' , ( e ) => {
if ( saveTimeout !== null ) {
e . preventDefault ();
e . returnValue = 'Há alterações pendentes aguardando salvamento. Deseja sair assim mesmo?' ;
return e . returnValue ;
}
});
Warns users if they try to close the tab with pending saves.
Location: store.js:195-219
export function saveStateToDB () {
if ( saveTimeout ) {
clearTimeout ( saveTimeout );
saveTimeout = null ;
}
if ( ! db ) return Promise . resolve ();
return new Promise (( resolve , reject ) => {
const transaction = db . transaction ([ STORE_NAME ], 'readwrite' );
const store = transaction . objectStore ( STORE_NAME );
const request = store . put ( state , 'main_state' );
request . onsuccess = () => {
document . dispatchEvent ( new Event ( 'stateSaved' ));
// Cascade Sync: Local -> Cloudflare
if ( state . config && state . config . cfSyncSyncEnabled ) {
SyncQueue . add (() => pushToCloudflare ());
}
resolve ();
};
request . onerror = ( e ) => reject ( e );
});
}
Sync Queue
The sync queue ensures cloud operations run sequentially, preventing race conditions.
Location: store.js:151-180
export const SyncQueue = {
isProcessing: false ,
tasks: [],
add ( taskFn ) {
return new Promise (( resolve , reject ) => {
this . tasks . push ( async () => {
try {
await taskFn ();
resolve ();
} catch ( err ) {
reject ( err );
}
});
this . process ();
});
},
async process () {
if ( this . isProcessing ) return ;
this . isProcessing = true ;
while ( this . tasks . length > 0 ) {
const fn = this . tasks . shift ();
try {
await fn ();
} catch ( err ) {
console . error ( 'SyncQueue Error:' , err );
}
}
this . isProcessing = false ;
}
};
Example: Add Sync Task
Example: Chained Syncs
SyncQueue . add (() => pushToCloudflare ());
Schema Migrations
Migration System
As the app evolves, the state schema changes. The migration system ensures backward compatibility.
Location: store.js:222-343
Version 2: Add Events & Config
if ( ! state . schemaVersion || state . schemaVersion < 2 ) {
if ( ! state . eventos ) state . eventos = [];
if ( ! state . revisoes ) state . revisoes = [];
if ( ! state . config ) state . config = { visualizacao: 'mes' , agruparEventos: true };
if ( ! state . config . frequenciaRevisao ) state . config . frequenciaRevisao = [ 1 , 7 , 30 , 90 ];
if ( ! state . habitos ) state . habitos = { questoes: [], revisao: [], ... };
// Add missing IDs to existing data
state . editais . forEach ( ed => {
if ( ! ed . id ) ed . id = 'ed_' + uid ();
if ( ! ed . cor ) ed . cor = '#10b981' ;
// ... more fixes
});
state . schemaVersion = 2 ;
changed = true ;
}
Version 3: Archive System
if ( state . schemaVersion === 2 ) {
if ( ! state . arquivo ) state . arquivo = [];
if ( state . config . frequenciaRevisao && typeof state . config . frequenciaRevisao === 'string' ) {
state . config . frequenciaRevisao = state . config . frequenciaRevisao . split ( ',' ). map ( Number ). filter ( n => ! isNaN ( n ));
}
state . schemaVersion = 3 ;
changed = true ;
}
Version 4: Study Cycles
if ( state . schemaVersion === 3 ) {
if ( ! state . ciclo ) {
state . ciclo = { ativo: false , ciclosCompletos: 0 , disciplinas: [] };
}
state . schemaVersion = 4 ;
changed = true ;
}
Version 5: Planning Module
if ( state . schemaVersion === 4 ) {
if ( ! state . planejamento ) {
state . planejamento = {
ativo: false ,
tipo: null ,
disciplinas: [],
relevancia: {},
horarios: {},
sequencia: []
};
}
state . schemaVersion = 5 ;
changed = true ;
}
Version 7: Lessons vs Topics
if ( state . schemaVersion < 7 ) {
if ( ! state . bancaRelevance ) state . bancaRelevance = { hotTopics: [], userMappings: {}, lessonMappings: {} };
if ( ! state . bancaRelevance . lessonMappings ) state . bancaRelevance . lessonMappings = {};
const classRegex = / ( ^ aula \s * \d + ) | ( ^ modulo \s * \d + ) / i ;
state . editais . forEach ( ed => {
ed . disciplinas . forEach ( d => {
if ( ! d . aulas ) d . aulas = [];
const remainingAssuntos = [];
d . assuntos . forEach ( ass => {
if ( classRegex . test ( ass . nome . trim ())) {
// It's a lesson! Move to d.aulas
const newAula = {
id: 'aula_' + uid (),
legacyAssid: ass . id ,
nome: ass . nome ,
descricao: ass . descricao || '' ,
estudada: !! ass . concluido ,
dataEstudo: ass . dataConclusao || null ,
progress: 0 ,
linkedAssuntoIds: [],
_migratedFromV6: true
};
d . aulas . push ( newAula );
} else {
remainingAssuntos . push ( ass );
}
});
d . assuntos = remainingAssuntos ;
});
});
state . schemaVersion = 7 ;
changed = true ;
}
This migration separates course lessons (“Aula 1”, “Módulo 2”) from exam topics for better organization.
Data Cleanup
Clear All Data
Location: store.js:346-366
export function clearData () {
setState ({
schemaVersion: DEFAULT_SCHEMA_VERSION ,
ciclo: { ativo: false , ciclosCompletos: 0 , disciplinas: [] },
planejamento: { ativo: false , tipo: null , disciplinas: [], relevancia: {}, horarios: {}, sequencia: [], ciclosCompletos: 0 , dataInicioCicloAtual: null },
editais: [],
eventos: [],
arquivo: [],
habitos: { questoes: [], revisao: [], discursiva: [], simulado: [], leitura: [], informativo: [], sumula: [], videoaula: [] },
revisoes: [],
config: { visualizacao: 'mes' , primeirodiaSemana: 1 , mostrarNumeroSemana: false , agruparEventos: true , frequenciaRevisao: [ 1 , 7 , 30 , 90 ] },
cronoLivre: { _timerStart: null , tempoAcumulado: 0 },
bancaRelevance: { hotTopics: [], userMappings: {}, lessonMappings: {} },
driveFileId: null ,
lastSync: null
});
saveStateToDB (). then (() => {
document . dispatchEvent ( new CustomEvent ( 'app:showToast' , { detail: { msg: 'Dados apagados com sucesso.' , type: 'info' } }));
document . dispatchEvent ( new Event ( 'app:renderCurrentView' ));
});
}
This action is irreversible! Users should be prompted with a double-confirmation before calling clearData().
Summary
State Management Centralized state object with normalized schema via setState()
Persistence IndexedDB with localStorage fallback, debounced saves, and unsaved warning
Migrations Schema versioning from v2 to v7 with backward compatibility
Sync Queue Sequential cloud operations to prevent race conditions