@@ -36,61 +36,67 @@ export function createEvaluator(runClassification = classify) {
3636 } ) ;
3737 const clear = lock . withPermits ( 1 ) ( attempt ( ( ) => chrome . storage . local . remove ( CACHE_KEY ) ) ) ;
3838
39+ /** The lock covers the cache read and the cache write, never the provider
40+ * call, so requests overlap. The merge re-reads under the lock rather than
41+ * writing back the snapshot it started from, so overlapping batches cannot
42+ * drop each other's verdicts. */
3943 function evaluate ( settings : Settings , posts : readonly Post [ ] ) {
40- return lock
41- . withPermits ( 1 ) (
42- Effect . gen ( function * ( ) {
43- const cache = yield * load ;
44- const now = Date . now ( ) ;
45- const keys = yield * Effect . forEach ( posts , ( post ) => cacheKey ( settings , post ) ) ;
46- const unique = new Map < string , Post > ( ) ;
47- posts . forEach ( ( post , index ) => {
48- const key = keys [ index ] ! ;
49- if ( ! cache [ key ] || now - cache [ key ] . at > TTL ) unique . set ( key , post ) ;
50- } ) ;
51- const updates : Record < string , typeof CacheEntry . Type > = { } ;
52- if ( unique . size ) {
53- // A paid request is never automatically retried here. The content controller
54- // owns a bounded retry budget and leaves the timeline visible on failure.
55- yield * bumpStats ( { requests : 1 } ) ;
56- const result = yield * runClassification ( settings , [ ...unique . values ( ) ] ) . pipe (
57- Effect . tapError ( ( error ) =>
58- bumpStats ( {
59- tokens : 'tokens' in error && typeof error . tokens === 'number' ? error . tokens : 0 ,
60- } ) ,
61- ) ,
62- ) ;
63- yield * bumpStats ( { tokens : result . tokens } ) ;
64- const byId = new Map ( result . verdicts . map ( ( verdict ) => [ verdict . key , verdict ] ) ) ;
65- for ( const [ key , post ] of unique ) {
66- const verdict = byId . get ( post . key ) ;
67- if ( verdict && ! verdict . failed )
68- updates [ key ] = { hide : verdict . hide , reason : verdict . reason , at : now } ;
69- }
44+ return Effect . gen ( function * ( ) {
45+ const keys = yield * Effect . forEach ( posts , ( post ) => cacheKey ( settings , post ) ) ;
46+ const cache = yield * lock . withPermits ( 1 ) ( load ) ;
47+ const now = Date . now ( ) ;
48+ const unique = new Map < string , Post > ( ) ;
49+ posts . forEach ( ( post , index ) => {
50+ const key = keys [ index ] ! ;
51+ if ( ! cache [ key ] || now - cache [ key ] . at > TTL ) unique . set ( key , post ) ;
52+ } ) ;
53+ const updates : Record < string , typeof CacheEntry . Type > = { } ;
54+ if ( unique . size ) {
55+ // A paid request is never automatically retried here. The content controller
56+ // owns a bounded retry budget and leaves the timeline visible on failure.
57+ yield * bumpStats ( { requests : 1 } ) ;
58+ const result = yield * runClassification ( settings , [ ...unique . values ( ) ] ) . pipe (
59+ Effect . tapError ( ( error ) =>
60+ bumpStats ( {
61+ tokens : 'tokens' in error && typeof error . tokens === 'number' ? error . tokens : 0 ,
62+ } ) ,
63+ ) ,
64+ ) ;
65+ yield * bumpStats ( { tokens : result . tokens } ) ;
66+ const byId = new Map ( result . verdicts . map ( ( verdict ) => [ verdict . key , verdict ] ) ) ;
67+ for ( const [ key , post ] of unique ) {
68+ const verdict = byId . get ( post . key ) ;
69+ if ( verdict && ! verdict . failed )
70+ updates [ key ] = { hide : verdict . hide , reason : verdict . reason , at : now } ;
71+ }
72+ yield * lock . withPermits ( 1 ) (
73+ Effect . gen ( function * ( ) {
74+ const current = yield * load ;
7075 const next = Object . fromEntries (
71- Object . entries ( { ...cache , ...updates } )
76+ Object . entries ( { ...current , ...updates } )
7277 . filter ( ( [ , entry ] ) => now - entry . at <= TTL )
7378 . sort ( ( a , b ) => b [ 1 ] . at - a [ 1 ] . at )
7479 . slice ( 0 , LIMIT ) ,
7580 ) ;
7681 yield * writeLocal ( { [ CACHE_KEY ] : next } ) ;
77- yield * setStatus (
78- result . verdicts . some ( ( verdict ) => verdict . failed )
79- ? 'The model skipped some posts. They remain visible.'
80- : '' ,
81- ) ;
82- }
83- return posts . map ( ( post , index ) : Verdict => {
84- const key = keys [ index ] ! ;
85- const entry = updates [ key ] ?? cache [ key ] ;
86- return entry && now - entry . at <= TTL
87- ? { key : post . key , hide : entry . hide , reason : entry . reason }
88- : { key : post . key , hide : false , reason : '' , failed : true } ;
89- } ) ;
90- } ) ,
91- )
92- . pipe ( Effect . timeout ( '32 seconds' ) ) ;
82+ } ) ,
83+ ) ;
84+ yield * setStatus (
85+ result . verdicts . some ( ( verdict ) => verdict . failed )
86+ ? 'The model skipped some posts. They remain visible.'
87+ : '' ,
88+ ) ;
89+ }
90+ return posts . map ( ( post , index ) : Verdict => {
91+ const key = keys [ index ] ! ;
92+ const entry = updates [ key ] ?? cache [ key ] ;
93+ return entry && now - entry . at <= TTL
94+ ? { key : post . key , hide : entry . hide , reason : entry . reason }
95+ : { key : post . key , hide : false , reason : '' , failed : true } ;
96+ } ) ;
97+ } ) . pipe ( Effect . timeout ( '32 seconds' ) ) ;
9398 }
99+
94100 return {
95101 evaluate,
96102 clear,
0 commit comments