|
1 | 1 | 'use strict'; |
2 | 2 |
|
3 | 3 |
|
4 | | -import {PadQueryResult, PadSearchQuery} from "../../types/PadSearchQuery"; |
| 4 | +import {PAD_FILTERS, PadFilter, PadQueryResult, PadSearchQuery} from "../../types/PadSearchQuery"; |
5 | 5 | import log4js from 'log4js'; |
6 | 6 |
|
7 | 7 | const fsp = require('fs').promises; |
@@ -110,137 +110,111 @@ exports.socketio = (hookName: string, {io}: any) => { |
110 | 110 | socket.on('padLoad', async (query: PadSearchQuery) => { |
111 | 111 | const {padIDs} = await padManager.listAllPads(); |
112 | 112 |
|
113 | | - const data: { |
114 | | - total: number, |
115 | | - results?: PadQueryResult[] |
116 | | - } = { |
117 | | - total: padIDs.length, |
118 | | - }; |
119 | | - let result: string[] = padIDs; |
120 | | - let maxResult; |
121 | | - |
122 | | - // Filter out matches |
| 113 | + // ── 1. Pattern filter (cheap, by name only) ───────────────────── |
| 114 | + let candidateNames: string[] = padIDs; |
123 | 115 | if (query.pattern) { |
124 | | - result = result.filter((padName: string) => padName.includes(query.pattern)); |
| 116 | + candidateNames = candidateNames.filter( |
| 117 | + (padName: string) => padName.includes(query.pattern)); |
125 | 118 | } |
126 | 119 |
|
127 | | - data.total = result.length; |
| 120 | + // ── 2. Resolve filter chip ────────────────────────────────────── |
| 121 | + // PadPage sends a chip id; "all" (default) means no additional |
| 122 | + // filtering. We accept missing values from older clients gracefully. |
| 123 | + const filter: PadFilter = |
| 124 | + (query.filter && PAD_FILTERS.includes(query.filter)) ? query.filter : 'all'; |
| 125 | + |
| 126 | + // ── 3. Decide whether we need full metadata for every candidate ── |
| 127 | + // The fast path — name-sort with no filter chip — only needs to |
| 128 | + // hydrate metadata for the 12-row page slice. Any other path |
| 129 | + // (filter chip OR non-name sort) requires every candidate's revs |
| 130 | + // / users / lastEdited up front so we can sort and slice against |
| 131 | + // the right universe. The expensive call is `padManager.getPad`; |
| 132 | + // user counts come from an in-memory map. |
| 133 | + const needsFullScan = filter !== 'all' || query.sortBy !== 'padName'; |
| 134 | + |
| 135 | + const loadMeta = async (padName: string): Promise<PadQueryResult> => { |
| 136 | + const pad = await padManager.getPad(padName); |
| 137 | + return { |
| 138 | + padName, |
| 139 | + lastEdited: await pad.getLastEdit(), |
| 140 | + userCount: api.padUsersCount(padName).padUsersCount, |
| 141 | + revisionNumber: pad.getHeadRevisionNumber(), |
| 142 | + }; |
| 143 | + }; |
| 144 | + |
| 145 | + // Lazily lifted so we don't load every pad twice on the fast path. |
| 146 | + let hydrated: PadQueryResult[] | null = null; |
| 147 | + const hydrateAll = async () => { |
| 148 | + if (hydrated == null) { |
| 149 | + hydrated = await Promise.all(candidateNames.map(loadMeta)); |
| 150 | + } |
| 151 | + return hydrated; |
| 152 | + }; |
| 153 | + |
| 154 | + // ── 4. Filter chip — applied to hydrated metadata ──────────────── |
| 155 | + // Bucket boundaries match the client chips in PadPage.tsx so the |
| 156 | + // counts on the stats cards keep meaning the same thing. Compute |
| 157 | + // `now` once per request so a pad doesn't slip between buckets |
| 158 | + // mid-loop. |
| 159 | + const now = Date.now(); |
| 160 | + const isRecent = (lastEdited: number) => now - lastEdited < 86_400_000 * 7; |
| 161 | + const isStale = (lastEdited: number) => now - lastEdited > 86_400_000 * 365; |
| 162 | + const matchesFilter = (m: PadQueryResult) => { |
| 163 | + switch (filter) { |
| 164 | + case 'active': return m.userCount > 0; |
| 165 | + case 'recent': return isRecent(Number(m.lastEdited)); |
| 166 | + case 'empty': return m.revisionNumber === 0; |
| 167 | + case 'stale': return isStale(Number(m.lastEdited)); |
| 168 | + default: return true; |
| 169 | + } |
| 170 | + }; |
128 | 171 |
|
129 | | - maxResult = result.length - 1; |
130 | | - if (maxResult < 0) { |
131 | | - maxResult = 0; |
| 172 | + // ── 5. Total — i.e. the count the pagination footer reflects ──── |
| 173 | + // For the fast path this is just the pattern-filtered name list; |
| 174 | + // for full-scan we report the post-chip total. |
| 175 | + let totalNames: string[] | null = needsFullScan ? null : candidateNames; |
| 176 | + let postFilterMetas: PadQueryResult[] | null = null; |
| 177 | + if (needsFullScan) { |
| 178 | + postFilterMetas = (await hydrateAll()).filter(matchesFilter); |
132 | 179 | } |
| 180 | + const total = needsFullScan ? postFilterMetas!.length : totalNames!.length; |
133 | 181 |
|
134 | | - // Reset to default values if out of bounds |
| 182 | + // ── 6. Clamp offset/limit ────────────────────────────────────── |
| 183 | + const maxOffset = Math.max(total - 1, 0); |
135 | 184 | if (query.offset && query.offset < 0) { |
136 | 185 | query.offset = 0; |
137 | | - } else if (query.offset > maxResult) { |
138 | | - query.offset = maxResult; |
| 186 | + } else if (query.offset > maxOffset) { |
| 187 | + query.offset = maxOffset; |
139 | 188 | } |
140 | | - |
141 | 189 | if (query.limit && query.limit < 0) { |
142 | | - // Too small |
143 | 190 | query.limit = 0; |
144 | 191 | } else if (query.limit > queryPadLimit) { |
145 | | - // Too big |
146 | 192 | query.limit = queryPadLimit; |
147 | 193 | } |
148 | 194 |
|
149 | | - |
150 | | - if (query.sortBy === 'padName') { |
151 | | - result = result.sort((a, b) => { |
152 | | - if (a < b) return query.ascending ? -1 : 1; |
153 | | - if (a > b) return query.ascending ? 1 : -1; |
154 | | - return 0; |
155 | | - }).slice(query.offset, query.offset + query.limit); |
156 | | - |
157 | | - data.results = await Promise.all(result.map(async (padName: string) => { |
158 | | - const pad = await padManager.getPad(padName); |
159 | | - const revisionNumber = pad.getHeadRevisionNumber() |
160 | | - const userCount = api.padUsersCount(padName).padUsersCount; |
161 | | - const lastEdited = await pad.getLastEdit(); |
162 | | - |
163 | | - return { |
164 | | - padName, |
165 | | - lastEdited, |
166 | | - userCount, |
167 | | - revisionNumber |
| 195 | + // ── 7. Sort + slice ──────────────────────────────────────────── |
| 196 | + const dir = query.ascending ? 1 : -1; |
| 197 | + const cmpStr = (a: string, b: string) => a < b ? -dir : a > b ? dir : 0; |
| 198 | + const cmpNum = (a: number, b: number) => a < b ? -dir : a > b ? dir : 0; |
| 199 | + |
| 200 | + let results: PadQueryResult[]; |
| 201 | + if (needsFullScan) { |
| 202 | + const sorted = postFilterMetas!.sort((a, b) => { |
| 203 | + switch (query.sortBy) { |
| 204 | + case 'padName': return cmpStr(a.padName, b.padName); |
| 205 | + case 'revisionNumber': return cmpNum(a.revisionNumber, b.revisionNumber); |
| 206 | + case 'userCount': return cmpNum(a.userCount, b.userCount); |
| 207 | + case 'lastEdited': return cmpStr(String(a.lastEdited), String(b.lastEdited)); |
| 208 | + default: return 0; |
168 | 209 | } |
169 | | - })); |
170 | | - } else if (query.sortBy === "revisionNumber") { |
171 | | - const currentWinners: PadQueryResult[] = [] |
172 | | - const padMapping = [] as {padId: string, revisionNumber: number}[] |
173 | | - for (let res of result) { |
174 | | - const pad = await padManager.getPad(res); |
175 | | - const revisionNumber = pad.getHeadRevisionNumber() |
176 | | - padMapping.push({padId: res, revisionNumber}) |
177 | | - } |
178 | | - padMapping.sort((a, b) => { |
179 | | - if (a.revisionNumber < b.revisionNumber) return query.ascending ? -1 : 1; |
180 | | - if (a.revisionNumber > b.revisionNumber) return query.ascending ? 1 : -1; |
181 | | - return 0; |
182 | | - }) |
183 | | - |
184 | | - for (const padRetrieval of padMapping.slice(query.offset, query.offset + query.limit)) { |
185 | | - let pad = await padManager.getPad(padRetrieval.padId); |
186 | | - currentWinners.push({ |
187 | | - padName: padRetrieval.padId, |
188 | | - lastEdited: await pad.getLastEdit(), |
189 | | - userCount: api.padUsersCount(pad.padName).padUsersCount, |
190 | | - revisionNumber: padRetrieval.revisionNumber |
191 | | - }) |
192 | | - } |
193 | | - |
194 | | - data.results = currentWinners; |
195 | | - } else if (query.sortBy === "userCount") { |
196 | | - const currentWinners: PadQueryResult[] = [] |
197 | | - const padMapping = [] as {padId: string, userCount: number}[] |
198 | | - for (let res of result) { |
199 | | - const userCount = api.padUsersCount(res).padUsersCount |
200 | | - padMapping.push({padId: res, userCount}) |
201 | | - } |
202 | | - padMapping.sort((a, b) => { |
203 | | - if (a.userCount < b.userCount) return query.ascending ? -1 : 1; |
204 | | - if (a.userCount > b.userCount) return query.ascending ? 1 : -1; |
205 | | - return 0; |
206 | | - }) |
207 | | - |
208 | | - for (const padRetrieval of padMapping.slice(query.offset, query.offset + query.limit)) { |
209 | | - let pad = await padManager.getPad(padRetrieval.padId); |
210 | | - currentWinners.push({ |
211 | | - padName: padRetrieval.padId, |
212 | | - lastEdited: await pad.getLastEdit(), |
213 | | - userCount: padRetrieval.userCount, |
214 | | - revisionNumber: pad.getHeadRevisionNumber() |
215 | | - }) |
216 | | - } |
217 | | - data.results = currentWinners; |
218 | | - } else if (query.sortBy === "lastEdited") { |
219 | | - const currentWinners: PadQueryResult[] = [] |
220 | | - const padMapping = [] as {padId: string, lastEdited: string}[] |
221 | | - for (let res of result) { |
222 | | - const pad = await padManager.getPad(res); |
223 | | - const lastEdited = await pad.getLastEdit(); |
224 | | - padMapping.push({padId: res, lastEdited}) |
225 | | - } |
226 | | - padMapping.sort((a, b) => { |
227 | | - if (a.lastEdited < b.lastEdited) return query.ascending ? -1 : 1; |
228 | | - if (a.lastEdited > b.lastEdited) return query.ascending ? 1 : -1; |
229 | | - return 0; |
230 | | - }) |
231 | | - |
232 | | - for (const padRetrieval of padMapping.slice(query.offset, query.offset + query.limit)) { |
233 | | - let pad = await padManager.getPad(padRetrieval.padId); |
234 | | - currentWinners.push({ |
235 | | - padName: padRetrieval.padId, |
236 | | - lastEdited: padRetrieval.lastEdited, |
237 | | - userCount: api.padUsersCount(pad.padName).padUsersCount, |
238 | | - revisionNumber: pad.getHeadRevisionNumber() |
239 | | - }) |
240 | | - } |
241 | | - data.results = currentWinners; |
| 210 | + }); |
| 211 | + results = sorted.slice(query.offset, query.offset + query.limit); |
| 212 | + } else { |
| 213 | + const sliceNames = totalNames!.sort(cmpStr).slice(query.offset, query.offset + query.limit); |
| 214 | + results = await Promise.all(sliceNames.map(loadMeta)); |
242 | 215 | } |
243 | 216 |
|
| 217 | + const data: {total: number, results?: PadQueryResult[]} = {total, results}; |
244 | 218 | socket.emit('results:padLoad', data); |
245 | 219 | }) |
246 | 220 |
|
|
0 commit comments