@@ -252,14 +252,19 @@ document.addEventListener("alpine:init", () => {
252252 /* Subscriptions editor state — three-column layout:
253253 left (subscribed) | middle (catalog search) | right (single-run).
254254 The left column persists to GitHub Secret on demand; the right
255- column is session-only and cleared after triggering a workflow. */
256- allCourses : [ ] , allCoursesTerms : [ ] ,
255+ column is session-only and cleared after triggering a workflow.
256+ Columns are driven by SQL queries against the in-memory shard DB
257+ so we never hold the full 20k-row catalog in JS. */
258+ allCoursesTerms : [ ] ,
257259 subsTerms : [ ] , subsDepts : [ ] , deptSearchQuery : "" ,
258260 subsSearchTitle : "" , subsSearchTeacher : "" ,
259261 subsTermOpen : false , subsDeptOpen : false ,
260262 subscribedIds : [ ] , singleRunIds : [ ] ,
261263 subsSelLeft : [ ] , subsSelMiddle : [ ] , subsSelRight : [ ] ,
262- subsFiltered : [ ] ,
264+ subsFiltered : [ ] , subsFilteredTotal : 0 ,
265+ subsLimit : 200 ,
266+ _subsSubscribedCache : [ ] , _subsSingleRunCache : [ ] , _subsDeptCache : [ ] ,
267+ _subsFilterTimer : null , _deptFilterTimer : null ,
263268 subsSaving : false , subsError : "" ,
264269 singleRunTriggering : false ,
265270 /* Per-browser pinned-courses set, lazily synced to localStorage. */
@@ -613,13 +618,9 @@ document.addEventListener("alpine:init", () => {
613618
614619 // ── Subscriptions editor (three-column) ──────────────────────────
615620 openSubscriptions ( ) {
616- // Debug: log entry
617- console . log ( "openSubscriptions called, view=" , this . view ) ;
618- // Enter the page FIRST — before any DB work.
619621 try {
620622 this . _go ( "subscriptions" ) ;
621- console . log ( "_go returned, view now=" , this . view ) ;
622- } catch ( e ) {
623+ } catch ( e ) {
623624 console . error ( "_go failed:" , e ) ;
624625 return ;
625626 }
@@ -632,13 +633,17 @@ document.addEventListener("alpine:init", () => {
632633 this . subsSearchTeacher = "" ;
633634 this . subsTermOpen = false ;
634635 this . subsDeptOpen = false ;
635- this . allCourses = [ ] ;
636636 this . subsFiltered = [ ] ;
637+ this . subsFilteredTotal = 0 ;
637638 this . singleRunIds = [ ] ;
638639 this . subsSelLeft = [ ] ;
639640 this . subsSelMiddle = [ ] ;
640641 this . subsSelRight = [ ] ;
642+ this . _subsSubscribedCache = [ ] ;
643+ this . _subsSingleRunCache = [ ] ;
644+ this . _subsDeptCache = [ ] ;
641645 this . subsError = "" ;
646+
642647 // Load subscription (localStorage → meta table fallback)
643648 this . subscribedIds = [ ] ;
644649 try {
@@ -661,45 +666,22 @@ document.addEventListener("alpine:init", () => {
661666 } catch { }
662667 }
663668 }
664- // Background-load the catalog after the page has rendered
665- var self = this ;
666- setTimeout ( function ( ) { self . _loadCoursesForTerms ( ) ; } , 200 ) ;
669+ this . _refreshSubscribedCache ( ) ;
670+ this . _refreshSingleRunCache ( ) ;
671+ this . _refreshDeptCache ( ) ;
672+ this . rebuildSubsFiltered ( ) ;
667673 } ,
668- // ── Async batch-load courses to avoid freezing the UI ───────────
669- _loadCoursesForTerms ( ) {
670- var self = this ;
671- var terms = this . subsTerms . length
672- ? this . subsTerms
673- : this . allCoursesTerms ;
674-
675- var tIdx = 0 ;
676- var CHUNK = 200 ;
677- var skipCount = 0 ;
678-
679- function nextTerm ( ) {
680- if ( tIdx >= terms . length ) return ;
681- var rows = ICS . db . getAllCourses ( terms [ tIdx ] ) ;
682- tIdx ++ ;
683- feed ( 0 , rows ) ;
684- }
685-
686- function feed ( offset , rows ) {
687- var sub = rows . slice ( offset , offset + CHUNK ) ;
688- self . allCourses = self . allCourses . concat ( sub ) ;
689- // Only re-filter every 3 chunks (~600 rows) to reduce DOM churn
690- skipCount ++ ;
691- if ( skipCount % 3 === 0 ) {
692- self . rebuildSubsFiltered ( ) ;
693- }
694- if ( offset + CHUNK >= rows . length ) {
695- self . rebuildSubsFiltered ( ) ; // Final flush
696- setTimeout ( nextTerm , 80 ) ;
697- } else {
698- setTimeout ( function ( ) { feed ( offset + CHUNK , rows ) ; } , 80 ) ;
699- }
700- }
701-
702- setTimeout ( nextTerm , 300 ) ; // Wait 300ms after page render
674+ // ── Cache refreshers (called explicitly when underlying IDs change) ─
675+ _refreshSubscribedCache ( ) {
676+ this . _subsSubscribedCache = ICS . db . getCoursesByIds ( this . subscribedIds ) ;
677+ } ,
678+ _refreshSingleRunCache ( ) {
679+ this . _subsSingleRunCache = ICS . db . getCoursesByIds ( this . singleRunIds ) ;
680+ } ,
681+ _refreshDeptCache ( ) {
682+ this . _subsDeptCache = ICS . db . getAllCoursesDepts (
683+ this . subsTerms , this . deptSearchQuery ,
684+ ) ;
703685 } ,
704686 // ── Term badge color (cyclic palette for the search results) ─────
705687 _TERM_COLORS : [
@@ -716,15 +698,9 @@ document.addEventListener("alpine:init", () => {
716698 for ( var i = 0 ; i < term . length ; i ++ ) idx = ( idx * 31 + term . charCodeAt ( i ) ) | 0 ;
717699 return this . _TERM_COLORS [ Math . abs ( idx ) % this . _TERM_COLORS . length ] ;
718700 } ,
719- // ── Column data ─────────────────────────────────────────────────
720- get subscribedCourses ( ) {
721- var ids = new Set ( this . subscribedIds . map ( String ) ) ;
722- return this . allCourses . filter ( function ( c ) { return ids . has ( String ( c . course_id ) ) ; } ) ;
723- } ,
724- get singleRunCourses ( ) {
725- var ids = new Set ( this . singleRunIds . map ( String ) ) ;
726- return this . allCourses . filter ( function ( c ) { return ids . has ( String ( c . course_id ) ) ; } ) ;
727- } ,
701+ // ── Column data (plain arrays kept in sync by cache refreshers) ─
702+ get subscribedCourses ( ) { return this . _subsSubscribedCache ; } ,
703+ get singleRunCourses ( ) { return this . _subsSingleRunCache ; } ,
728704 // ── Dropdown labels ─────────────────────────────────────────────
729705 get subsTermLabel ( ) {
730706 if ( ! this . subsTerms . length ) return '全部学期' ;
@@ -734,39 +710,46 @@ document.addEventListener("alpine:init", () => {
734710 if ( ! this . subsDepts . length ) return '全部院系' ;
735711 return this . subsDepts . length + '个院系' ;
736712 } ,
737- get subsDeptFiltered ( ) {
738- var q = ( this . deptSearchQuery || '' ) . toLowerCase ( ) ;
739- var deptSet = new Set ( ) ;
740- for ( var i = 0 ; i < this . allCourses . length ; i ++ ) {
741- var d = this . allCourses [ i ] . dept ;
742- if ( d && ( ! q || d . toLowerCase ( ) . indexOf ( q ) !== - 1 ) ) deptSet . add ( d ) ;
743- }
744- return Array . from ( deptSet ) . sort ( ) ;
713+ get subsDeptFiltered ( ) { return this . _subsDeptCache ; } ,
714+ onDeptSearchInput ( ) {
715+ // Debounced — typing in the dept search box requeries distinct depts
716+ // from sqlite filtered by term + substring.
717+ var self = this ;
718+ clearTimeout ( this . _deptFilterTimer ) ;
719+ this . _deptFilterTimer = setTimeout ( function ( ) {
720+ self . _refreshDeptCache ( ) ;
721+ } , 150 ) ;
745722 } ,
746723 // ── Toggle multi-select ─────────────────────────────────────────
747724 toggleSubsTerm ( term , checked ) {
748725 var s = new Set ( this . subsTerms ) ;
749726 if ( checked ) s . add ( term ) ; else s . delete ( term ) ;
750727 this . subsTerms = Array . from ( s ) ;
751- this . _loadCoursesForTerms ( ) ;
728+ // Term change invalidates both the dept dropdown (term-narrowed)
729+ // and the middle column (different rows match).
730+ this . _refreshDeptCache ( ) ;
731+ this . rebuildSubsFiltered ( ) ;
752732 } ,
753733 toggleSubsDept ( dept , checked ) {
754734 var s = new Set ( this . subsDepts ) ;
755735 if ( checked ) s . add ( dept ) ; else s . delete ( dept ) ;
756736 this . subsDepts = Array . from ( s ) ;
757737 this . rebuildSubsFiltered ( ) ;
758738 } ,
759- // ── Filter middle column ────────────────────── ───────────────────
739+ // ── Filter middle column (debounced SQL query) ───────────────────
760740 rebuildSubsFiltered ( ) {
761- var deptSet = new Set ( this . subsDepts . map ( function ( d ) { return d . toLowerCase ( ) ; } ) ) ;
762- var titleQ = ( this . subsSearchTitle || "" ) . trim ( ) . toLowerCase ( ) ;
763- var teacherQ = ( this . subsSearchTeacher || "" ) . trim ( ) . toLowerCase ( ) ;
764- this . subsFiltered = this . allCourses . filter ( function ( c ) {
765- if ( deptSet . size && ( ! c . dept || ! deptSet . has ( c . dept . toLowerCase ( ) ) ) ) return false ;
766- if ( titleQ && ( ! c . title || c . title . toLowerCase ( ) . indexOf ( titleQ ) === - 1 ) ) return false ;
767- if ( teacherQ && ( ! c . teacher || c . teacher . toLowerCase ( ) . indexOf ( teacherQ ) === - 1 ) ) return false ;
768- return true ;
769- } ) ;
741+ var self = this ;
742+ clearTimeout ( this . _subsFilterTimer ) ;
743+ this . _subsFilterTimer = setTimeout ( function ( ) {
744+ var filters = {
745+ terms : self . subsTerms ,
746+ depts : self . subsDepts ,
747+ title : self . subsSearchTitle ,
748+ teacher : self . subsSearchTeacher ,
749+ } ;
750+ self . subsFiltered = ICS . db . searchAllCourses ( filters , self . subsLimit ) ;
751+ self . subsFilteredTotal = ICS . db . countAllCourses ( filters ) ;
752+ } , 150 ) ;
770753 } ,
771754 // ── Per-column multi-select ──────────────────────────────────────
772755 _toggleSel ( arr , id , checked ) {
@@ -791,27 +774,31 @@ document.addEventListener("alpine:init", () => {
791774 for ( var i = 0 ; i < selected . length ; i ++ ) target . add ( selected [ i ] ) ;
792775 this . subscribedIds = Array . from ( target ) ;
793776 this . subsSelMiddle = [ ] ;
777+ this . _refreshSubscribedCache ( ) ;
794778 } ,
795779 moveFromSubscribed ( ) {
796780 var target = new Set ( this . subscribedIds . map ( String ) ) ;
797781 var selected = this . subsSelLeft ;
798782 for ( var i = 0 ; i < selected . length ; i ++ ) target . delete ( selected [ i ] ) ;
799783 this . subscribedIds = Array . from ( target ) ;
800784 this . subsSelLeft = [ ] ;
785+ this . _refreshSubscribedCache ( ) ;
801786 } ,
802787 moveToSingleRun ( ) {
803788 var target = new Set ( this . singleRunIds . map ( String ) ) ;
804789 var selected = this . subsSelMiddle ;
805790 for ( var i = 0 ; i < selected . length ; i ++ ) target . add ( selected [ i ] ) ;
806791 this . singleRunIds = Array . from ( target ) ;
807792 this . subsSelMiddle = [ ] ;
793+ this . _refreshSingleRunCache ( ) ;
808794 } ,
809795 moveFromSingleRun ( ) {
810796 var target = new Set ( this . singleRunIds . map ( String ) ) ;
811797 var selected = this . subsSelRight ;
812798 for ( var i = 0 ; i < selected . length ; i ++ ) target . delete ( selected [ i ] ) ;
813799 this . singleRunIds = Array . from ( target ) ;
814800 this . subsSelRight = [ ] ;
801+ this . _refreshSingleRunCache ( ) ;
815802 } ,
816803 // ── Save left column to GitHub Secret ────────────────────────────
817804 async saveSubscriptions ( ) {
0 commit comments